diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:43:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:43:14 +0000 |
commit | 8dd16259287f58f9273002717ec4d27e97127719 (patch) | |
tree | 3863e62a53829a84037444beab3abd4ed9dfc7d0 /accessible | |
parent | Releasing progress-linux version 126.0.1-1~progress7.99u1. (diff) | |
download | firefox-8dd16259287f58f9273002717ec4d27e97127719.tar.xz firefox-8dd16259287f58f9273002717ec4d27e97127719.zip |
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'accessible')
49 files changed, 2576 insertions, 266 deletions
diff --git a/accessible/base/ARIAMap.cpp b/accessible/base/ARIAMap.cpp index d53592acf0..a557405497 100644 --- a/accessible/base/ARIAMap.cpp +++ b/accessible/base/ARIAMap.cpp @@ -256,7 +256,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-acknowledgments @@ -286,7 +286,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-backlink @@ -296,7 +296,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eJumpAction, eNoLiveAttr, - kGenericAccType, + eDPub, states::LINKED }, { // doc-biblioentry @@ -306,7 +306,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, states::READONLY }, { // doc-bibliography @@ -316,7 +316,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-biblioref @@ -326,7 +326,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eJumpAction, eNoLiveAttr, - kGenericAccType, + eDPub, states::LINKED }, { // doc-chapter @@ -336,7 +336,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-colophon @@ -346,7 +346,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-conclusion @@ -356,7 +356,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-cover @@ -366,7 +366,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-credit @@ -376,7 +376,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-credits @@ -386,7 +386,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-dedication @@ -396,7 +396,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-endnote @@ -406,7 +406,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, states::READONLY }, { // doc-endnotes @@ -416,7 +416,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-epigraph @@ -426,7 +426,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-epilogue @@ -436,7 +436,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-errata @@ -446,17 +446,17 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-example nsGkAtoms::docExample, - roles::SECTION, + roles::FIGURE, kUseMapRole, eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-footnote @@ -466,7 +466,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-foreword @@ -476,7 +476,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-glossary @@ -486,7 +486,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-glossref @@ -496,7 +496,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eJumpAction, eNoLiveAttr, - kGenericAccType, + eDPub, states::LINKED }, { // doc-index @@ -506,7 +506,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-introduction @@ -516,7 +516,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-noteref @@ -526,7 +526,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eJumpAction, eNoLiveAttr, - kGenericAccType, + eDPub, states::LINKED }, { // doc-notice @@ -536,7 +536,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-pagebreak @@ -546,7 +546,27 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, + kNoReqStates + }, + { // doc-pagefooter + nsGkAtoms::docPagefooter, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eDPub, + kNoReqStates + }, + { // doc-pageheader + nsGkAtoms::docPageheader, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eDPub, kNoReqStates }, { // doc-pagelist @@ -556,7 +576,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-part @@ -566,7 +586,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-preface @@ -576,7 +596,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-prologue @@ -586,7 +606,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // doc-pullquote @@ -596,7 +616,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-qna @@ -606,7 +626,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-subtitle @@ -616,7 +636,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-tip @@ -626,7 +646,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - kGenericAccType, + eDPub, kNoReqStates }, { // doc-toc @@ -636,7 +656,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = { eNoValue, eNoAction, eNoLiveAttr, - eLandmark, + eDPub | eLandmark, kNoReqStates }, { // document @@ -1589,6 +1609,19 @@ bool aria::HasDefinedARIAHidden(nsIContent* aContent) { eCaseMatters); } +const nsRoleMapEntry* aria::GetRoleMap(const nsStaticAtom* aAriaRole) { + const nsDependentAtomString role(aAriaRole); + auto comparator = [&role](const nsRoleMapEntry& aEntry) { + return Compare(role, aEntry.ARIARoleString()); + }; + size_t idx; + if (BinarySearchIf(sWAIRoleMaps, 0, ArrayLength(sWAIRoleMaps), comparator, + &idx)) { + return GetRoleMapFromIndex(idx); + } + return nullptr; +} + //////////////////////////////////////////////////////////////////////////////// // AttrIterator class diff --git a/accessible/base/ARIAMap.h b/accessible/base/ARIAMap.h index 58a96b7112..2fa3ac80d4 100644 --- a/accessible/base/ARIAMap.h +++ b/accessible/base/ARIAMap.h @@ -305,6 +305,11 @@ uint8_t AttrCharacteristicsFor(nsAtom* aAtom); bool HasDefinedARIAHidden(nsIContent* aContent); /** + * Get the role map entry for a given ARIA role. + */ +const nsRoleMapEntry* GetRoleMap(const nsStaticAtom* aAriaRole); + +/** * Represents a simple enumerator for iterating through ARIA attributes * exposed as object attributes on a given accessible. */ diff --git a/accessible/base/AccTypes.h b/accessible/base/AccTypes.h index 3e9d88e486..76fdaae2d7 100644 --- a/accessible/base/AccTypes.h +++ b/accessible/base/AccTypes.h @@ -87,6 +87,7 @@ enum AccGenericType { eText = 1 << 14, eNumericValue = 1 << 15, eActionable = 1 << 16, // This is for remote accessibles + eDPub = 1 << 17, eLastAccGenericType = eActionable, eAllGenericTypes = (eLastAccGenericType << 1) - 1 diff --git a/accessible/base/nsAccessibilityService.cpp b/accessible/base/nsAccessibilityService.cpp index 615af596a7..7b5f127672 100644 --- a/accessible/base/nsAccessibilityService.cpp +++ b/accessible/base/nsAccessibilityService.cpp @@ -12,6 +12,7 @@ #include "DocAccessible-inl.h" #include "DocAccessibleChild.h" #include "FocusManager.h" +#include "mozilla/FocusModel.h" #include "HTMLCanvasAccessible.h" #include "HTMLElementAccessibles.h" #include "HTMLImageMapAccessible.h" @@ -136,48 +137,6 @@ static LocalAccessible* MaybeCreateSpecificARIAAccessible( } /** - * Return true if the element has an attribute (ARIA, title, or relation) that - * requires the creation of an Accessible for the element. - */ -static bool AttributesMustBeAccessible(nsIContent* aContent, - DocAccessible* aDocument) { - if (aContent->IsElement()) { - uint32_t attrCount = aContent->AsElement()->GetAttrCount(); - for (uint32_t attrIdx = 0; attrIdx < attrCount; attrIdx++) { - const nsAttrName* attr = aContent->AsElement()->GetAttrNameAt(attrIdx); - if (attr->NamespaceEquals(kNameSpaceID_None)) { - nsAtom* attrAtom = attr->Atom(); - if (attrAtom == nsGkAtoms::title && aContent->IsHTMLElement()) { - // If the author provided a title on an element that would not - // be accessible normally, assume an intent and make it accessible. - return true; - } - - nsDependentAtomString attrStr(attrAtom); - if (!StringBeginsWith(attrStr, u"aria-"_ns)) continue; // not ARIA - - // A global state or a property and in case of token defined. - uint8_t attrFlags = aria::AttrCharacteristicsFor(attrAtom); - if ((attrFlags & ATTR_GLOBAL) && - (!(attrFlags & ATTR_VALTOKEN) || - nsAccUtils::HasDefinedARIAToken(aContent, attrAtom))) { - return true; - } - } - } - - // If the given ID is referred by relation attribute then create an - // Accessible for it. - nsAutoString id; - if (nsCoreUtils::GetID(aContent, id) && !id.IsEmpty()) { - return aDocument->IsDependentID(aContent->AsElement(), id); - } - } - - return false; -} - -/** * Return true if the element must be a generic Accessible, even if it has been * marked presentational with role="presentation", etc. MustBeAccessible causes * an Accessible to be created as if it weren't marked presentational at all; @@ -224,19 +183,53 @@ static bool MustBeGenericAccessible(nsIContent* aContent, * Return true if the element must be accessible. */ static bool MustBeAccessible(nsIContent* aContent, DocAccessible* aDocument) { - nsIFrame* frame = aContent->GetPrimaryFrame(); - MOZ_ASSERT(frame); - // This document might be invisible when it first loads. Therefore, we must - // check focusability irrespective of visibility here. Otherwise, we might not - // create Accessibles for some focusable elements; e.g. a span with only a - // tabindex. Elements that are invisible within this document are excluded - // earlier in CreateAccessible. - if (frame->IsFocusable(/* aWithMouse */ false, - /* aCheckVisibility */ false)) { - return true; + if (nsIFrame* frame = aContent->GetPrimaryFrame()) { + // This document might be invisible when it first loads. Therefore, we must + // check focusability irrespective of visibility here. Otherwise, we might + // not create Accessibles for some focusable elements; e.g. a span with only + // a tabindex. Elements that are invisible within this document are excluded + // earlier in CreateAccessible. + if (frame->IsFocusable(IsFocusableFlags::IgnoreVisibility)) { + return true; + } + } + + // Return true if the element has an attribute (ARIA, title, or relation) that + // requires the creation of an Accessible for the element. + if (aContent->IsElement()) { + uint32_t attrCount = aContent->AsElement()->GetAttrCount(); + for (uint32_t attrIdx = 0; attrIdx < attrCount; attrIdx++) { + const nsAttrName* attr = aContent->AsElement()->GetAttrNameAt(attrIdx); + if (attr->NamespaceEquals(kNameSpaceID_None)) { + nsAtom* attrAtom = attr->Atom(); + if (attrAtom == nsGkAtoms::title && aContent->IsHTMLElement()) { + // If the author provided a title on an element that would not + // be accessible normally, assume an intent and make it accessible. + return true; + } + + nsDependentAtomString attrStr(attrAtom); + if (!StringBeginsWith(attrStr, u"aria-"_ns)) continue; // not ARIA + + // A global state or a property and in case of token defined. + uint8_t attrFlags = aria::AttrCharacteristicsFor(attrAtom); + if ((attrFlags & ATTR_GLOBAL) && + (!(attrFlags & ATTR_VALTOKEN) || + nsAccUtils::HasDefinedARIAToken(aContent, attrAtom))) { + return true; + } + } + } + + // If the given ID is referred by relation attribute then create an + // Accessible for it. + nsAutoString id; + if (nsCoreUtils::GetID(aContent, id) && !id.IsEmpty()) { + return aDocument->IsDependentID(aContent->AsElement(), id); + } } - return AttributesMustBeAccessible(aContent, aDocument); + return false; } bool nsAccessibilityService::ShouldCreateImgAccessible( @@ -294,6 +287,38 @@ static bool MustSVGElementBeAccessible(nsIContent* aContent, } /** + * Return an accessible for the content if the SVG element requires the creation + * of an Accessible. + */ +static RefPtr<LocalAccessible> MaybeCreateSVGAccessible( + nsIContent* aContent, DocAccessible* aDocument) { + if (aContent->IsSVGGeometryElement() || + aContent->IsSVGElement(nsGkAtoms::image)) { + // Shape elements: rect, circle, ellipse, line, path, polygon, and polyline. + // 'use' and 'text' graphic elements require special support. + if (MustSVGElementBeAccessible(aContent, aDocument)) { + return new EnumRoleAccessible<roles::GRAPHIC>(aContent, aDocument); + } + } else if (aContent->IsSVGElement(nsGkAtoms::text)) { + return new HyperTextAccessible(aContent->AsElement(), aDocument); + } else if (aContent->IsSVGElement(nsGkAtoms::svg)) { + // An <svg> element could contain <foreignObject>, which contains HTML but + // does not normally create its own Accessible. This means that the <svg> + // Accessible could have TextLeafAccessible children, so it must be a + // HyperTextAccessible. + return new EnumRoleHyperTextAccessible<roles::DIAGRAM>(aContent, aDocument); + } else if (aContent->IsSVGElement(nsGkAtoms::g) && + MustSVGElementBeAccessible(aContent, aDocument)) { + // <g> can also contain <foreignObject>. + return new EnumRoleHyperTextAccessible<roles::GROUPING>(aContent, + aDocument); + } else if (aContent->IsSVGElement(nsGkAtoms::a)) { + return new HTMLLinkAccessible(aContent, aDocument); + } + return nullptr; +} + +/** * Used by XULMap.h to map both menupopup and popup elements */ LocalAccessible* CreateMenupopupAccessible(Element* aElement, @@ -1138,13 +1163,19 @@ LocalAccessible* nsAccessibilityService::CreateAccessible( } } + // SVG elements are not in a markup map, but we may still need to create an + // accessible for one, even in the case of display:contents. + if (!newAcc && content->IsSVGElement()) { + newAcc = MaybeCreateSVGAccessible(content, document); + } + // Check whether this element has an ARIA role or attribute that requires // us to create an Accessible. const bool hasNonPresentationalARIARole = roleMapEntry && !roleMapEntry->Is(nsGkAtoms::presentation) && !roleMapEntry->Is(nsGkAtoms::none); - if (!newAcc && (hasNonPresentationalARIARole || - AttributesMustBeAccessible(content, document))) { + if (!newAcc && + (hasNonPresentationalARIARole || MustBeAccessible(content, document))) { newAcc = new HyperTextAccessible(content, document); } @@ -1365,32 +1396,7 @@ LocalAccessible* nsAccessibilityService::CreateAccessible( if (!newAcc) { if (content->IsSVGElement()) { - if (content->IsSVGGeometryElement() || - content->IsSVGElement(nsGkAtoms::image)) { - // Shape elements: rect, circle, ellipse, line, path, polygon, - // and polyline. 'use' and 'text' graphic elements require - // special support. - if (MustSVGElementBeAccessible(content, document)) { - newAcc = new EnumRoleAccessible<roles::GRAPHIC>(content, document); - } - } else if (content->IsSVGElement(nsGkAtoms::text)) { - newAcc = new HyperTextAccessible(content->AsElement(), document); - } else if (content->IsSVGElement(nsGkAtoms::svg)) { - // An <svg> element could contain <foreignObject>, which contains HTML - // but does not normally create its own Accessible. This means that the - // <svg> Accessible could have TextLeafAccessible children, so it must - // be a HyperTextAccessible. - newAcc = - new EnumRoleHyperTextAccessible<roles::DIAGRAM>(content, document); - } else if (content->IsSVGElement(nsGkAtoms::g) && - MustSVGElementBeAccessible(content, document)) { - // <g> can also contain <foreignObject>. - newAcc = - new EnumRoleHyperTextAccessible<roles::GROUPING>(content, document); - } else if (content->IsSVGElement(nsGkAtoms::a)) { - newAcc = new HTMLLinkAccessible(content, document); - } - + newAcc = MaybeCreateSVGAccessible(content, document); } else if (content->IsMathMLElement()) { const MarkupMapInfo* markupMap = mMathMLMarkupMap.Get(content->NodeInfo()->NameAtom()); diff --git a/accessible/base/nsCoreUtils.cpp b/accessible/base/nsCoreUtils.cpp index c5e89258fa..3e66fa9c23 100644 --- a/accessible/base/nsCoreUtils.cpp +++ b/accessible/base/nsCoreUtils.cpp @@ -547,32 +547,6 @@ bool nsCoreUtils::IsWhitespaceString(const nsAString& aString) { return iterBegin == iterEnd; } -void nsCoreUtils::TrimNonBreakingSpaces(nsAString& aString) { - if (aString.IsEmpty()) { - return; - } - - // Find the index past the last nbsp prefix character. - constexpr char16_t nbsp{0xA0}; - size_t startIndex = 0; - while (aString.CharAt(startIndex) == nbsp) { - startIndex++; - } - - // Find the index before the first nbsp suffix character. - size_t endIndex = aString.Length() - 1; - while (endIndex > startIndex && aString.CharAt(endIndex) == nbsp) { - endIndex--; - } - if (startIndex > endIndex) { - aString.Truncate(); - return; - } - - // Trim the string down, removing the non-breaking space characters. - aString = Substring(aString, startIndex, endIndex - startIndex + 1); -} - bool nsCoreUtils::AccEventObserversExist() { nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); NS_ENSURE_TRUE(obsService, false); diff --git a/accessible/base/nsCoreUtils.h b/accessible/base/nsCoreUtils.h index 5e77d6bfe0..28ead982af 100644 --- a/accessible/base/nsCoreUtils.h +++ b/accessible/base/nsCoreUtils.h @@ -307,11 +307,6 @@ class nsCoreUtils { aChar == 0xa0; } - /** - * Remove non-breaking spaces from the beginning and end of the string. - */ - static void TrimNonBreakingSpaces(nsAString& aString); - /* * Return true if there are any observers of accessible events. */ diff --git a/accessible/basetypes/Accessible.cpp b/accessible/basetypes/Accessible.cpp index 8b433a0ddd..234c7aa4a1 100644 --- a/accessible/basetypes/Accessible.cpp +++ b/accessible/basetypes/Accessible.cpp @@ -508,6 +508,24 @@ const Accessible* Accessible::ActionAncestor() const { } nsStaticAtom* Accessible::LandmarkRole() const { + // For certain cases below (e.g. ARIA region, HTML <header>), whether it is + // actually a landmark is conditional. Rather than duplicating that + // conditional logic here, we check the Gecko role. + if (const nsRoleMapEntry* roleMapEntry = ARIARoleMap()) { + // Explicit ARIA role should take precedence. + if (roleMapEntry->Is(nsGkAtoms::region)) { + if (Role() == roles::REGION) { + return nsGkAtoms::region; + } + } else if (roleMapEntry->Is(nsGkAtoms::form)) { + if (Role() == roles::FORM) { + return nsGkAtoms::form; + } + } else if (roleMapEntry->IsOfType(eLandmark)) { + return roleMapEntry->roleAtom; + } + } + nsAtom* tagName = TagName(); if (!tagName) { // Either no associated content, or no cache. @@ -539,13 +557,13 @@ nsStaticAtom* Accessible::LandmarkRole() const { } if (tagName == nsGkAtoms::section) { - if (!NameIsEmpty()) { + if (Role() == roles::REGION) { return nsGkAtoms::region; } } if (tagName == nsGkAtoms::form) { - if (!NameIsEmpty()) { + if (Role() == roles::FORM_LANDMARK) { return nsGkAtoms::form; } } @@ -554,14 +572,14 @@ nsStaticAtom* Accessible::LandmarkRole() const { return nsGkAtoms::search; } - const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); - return roleMapEntry && roleMapEntry->IsOfType(eLandmark) - ? roleMapEntry->roleAtom - : nullptr; + return nullptr; } nsStaticAtom* Accessible::ComputedARIARole() const { const nsRoleMapEntry* roleMap = ARIARoleMap(); + if (roleMap && roleMap->IsOfType(eDPub)) { + return roleMap->roleAtom; + } if (roleMap && roleMap->roleAtom != nsGkAtoms::_empty && // region and form have their own Gecko roles and need to be handled // specially. @@ -605,12 +623,15 @@ void Accessible::ApplyImplicitState(uint64_t& aState) const { } } - // If this is an ARIA item of the selectable widget and if it's focused and - // not marked unselected explicitly (i.e. aria-selected="false") then expose - // it as selected to make ARIA widget authors life easier. + // If this is an option, tab or treeitem and if it's focused and not marked + // unselected explicitly (i.e. aria-selected="false") then expose it as + // selected to make ARIA widget authors life easier. const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); - if (roleMapEntry && !(aState & states::SELECTED) && - ARIASelected().valueOr(true)) { + if (roleMapEntry && + (roleMapEntry->Is(nsGkAtoms::option) || + roleMapEntry->Is(nsGkAtoms::tab) || + roleMapEntry->Is(nsGkAtoms::treeitem)) && + !(aState & states::SELECTED) && ARIASelected().valueOr(true)) { // Special case for tabs: focused tab or focus inside related tab panel // implies selected state. if (roleMapEntry->role == roles::PAGETAB) { diff --git a/accessible/basetypes/Accessible.h b/accessible/basetypes/Accessible.h index 9068115d01..1848fc6b37 100644 --- a/accessible/basetypes/Accessible.h +++ b/accessible/basetypes/Accessible.h @@ -438,10 +438,15 @@ class Accessible { nsStaticAtom* LandmarkRole() const; /** - * Return the id of the dom node this accessible represents. + * Return the id of the DOM node this Accessible represents. */ virtual void DOMNodeID(nsString& aID) const = 0; + /** + * Return the class of the DOM node this Accessible represents. + */ + virtual void DOMNodeClass(nsString& aClass) const = 0; + ////////////////////////////////////////////////////////////////////////////// // ActionAccessible diff --git a/accessible/docs/index.rst b/accessible/docs/index.rst index 881d49ca9b..5626eb1dbc 100644 --- a/accessible/docs/index.rst +++ b/accessible/docs/index.rst @@ -6,6 +6,8 @@ They live in the mozilla-central repository under the accessible/docs directory. The `Accessibility page on the Mozilla Wiki <https://wiki.mozilla.org/Accessibility>`__ contains general information about accessibility and the accessibility team at Mozilla. +The `Mochitest FAQ docs <../testing/mochitest-plain/faq.html#how-can-i-run-accessibility-tests-a11y-checks>`__ contain guidance on debugging a11y-checks failures and working with AccessibilityUtils when writing frontend test cases. + .. toctree:: :maxdepth: 1 diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp index aaf0337a1a..b01b758945 100644 --- a/accessible/generic/LocalAccessible.cpp +++ b/accessible/generic/LocalAccessible.cpp @@ -14,6 +14,7 @@ #include "mozilla/a11y/AccAttributes.h" #include "mozilla/a11y/DocAccessibleChild.h" #include "mozilla/a11y/Platform.h" +#include "mozilla/FocusModel.h" #include "nsAccUtils.h" #include "nsAccessibilityService.h" #include "ApplicationAccessible.h" @@ -127,7 +128,6 @@ ENameValueFlag LocalAccessible::Name(nsString& aName) const { if (!aName.IsEmpty()) return eNameOK; ENameValueFlag nameFlag = NativeName(aName); - nsCoreUtils::TrimNonBreakingSpaces(aName); if (!aName.IsEmpty()) return nameFlag; // In the end get the name from tooltip. @@ -408,6 +408,7 @@ uint64_t LocalAccessible::NativeInteractiveState() const { if (NativelyUnavailable()) return states::UNAVAILABLE; nsIFrame* frame = GetFrame(); + auto flags = IsFocusableFlags(0); // If we're caching this remote document in the parent process, we // need to cache focusability irrespective of visibility. Otherwise, // if this document is invisible when it first loads, we'll cache that @@ -418,13 +419,12 @@ uint64_t LocalAccessible::NativeInteractiveState() const { // Although ignoring visibility means IsFocusable will return true for // visibility: hidden, etc., this isn't a problem because we don't include // those hidden elements in the a11y tree anyway. - const bool ignoreVisibility = mDoc->IPCDoc(); - if (frame && frame->IsFocusable( - /* aWithMouse */ false, - /* aCheckVisibility */ !ignoreVisibility)) { + if (mDoc->IPCDoc()) { + flags |= IsFocusableFlags::IgnoreVisibility; + } + if (frame && frame->IsFocusable(flags)) { return states::FOCUSABLE; } - return 0; } @@ -689,7 +689,8 @@ nsRect LocalAccessible::RelativeBounds(nsIFrame** aBoundingFrame) const { if (frame && mContent) { *aBoundingFrame = nsLayoutUtils::GetContainingBlockForClientRect(frame); nsRect unionRect = nsLayoutUtils::GetAllInFlowRectsUnion( - frame, *aBoundingFrame, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS); + frame, *aBoundingFrame, + nsLayoutUtils::GetAllInFlowRectsFlag::AccountForTransforms); if (unionRect.IsEmpty()) { // If we end up with a 0x0 rect from above (or one with negative @@ -1625,6 +1626,14 @@ void LocalAccessible::ApplyARIAState(uint64_t* aState) const { *aState |= aria::UniversalStatesFor(element); const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (!roleMapEntry && IsHTMLTableCell() && Role() == roles::GRID_CELL) { + // This is a <td> inside a role="grid", so it gets an implicit role of + // GRID_CELL in ARIATransformRole. However, because it's implicit, we + // don't have a role map entry, and without that, we can't apply ARIA states + // below. Therefore, we get the role map entry here. + roleMapEntry = aria::GetRoleMap(nsGkAtoms::gridcell); + MOZ_ASSERT(roleMapEntry, "Should have role map entry for gridcell"); + } if (roleMapEntry) { // We only force the readonly bit off if we have a real mapping for the aria // role. This preserves the ability for screen readers to use readonly @@ -3794,14 +3803,12 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache( } else if (aUpdateType == CacheUpdateType::Update) { fields->SetAttribute(CacheKey::DOMNodeID, DeleteEntry()); } - if (auto* el = dom::Element::FromNodeOrNull(mContent)) { - nsAutoString className; - el->GetClassName(className); - if (!className.IsEmpty()) { - fields->SetAttribute(CacheKey::DOMNodeClass, std::move(className)); - } else if (aUpdateType == CacheUpdateType::Update) { - fields->SetAttribute(CacheKey::DOMNodeClass, DeleteEntry()); - } + nsString className; + DOMNodeClass(className); + if (!className.IsEmpty()) { + fields->SetAttribute(CacheKey::DOMNodeClass, std::move(className)); + } else if (aUpdateType == CacheUpdateType::Update) { + fields->SetAttribute(CacheKey::DOMNodeClass, DeleteEntry()); } } @@ -4272,6 +4279,13 @@ void LocalAccessible::DOMNodeID(nsString& aID) const { } } +void LocalAccessible::DOMNodeClass(nsString& aClass) const { + aClass.Truncate(); + if (auto* el = dom::Element::FromNodeOrNull(mContent)) { + el->GetClassName(aClass); + } +} + void LocalAccessible::LiveRegionAttributes(nsAString* aLive, nsAString* aRelevant, Maybe<bool>* aAtomic, diff --git a/accessible/generic/LocalAccessible.h b/accessible/generic/LocalAccessible.h index 51c4cc9424..eb684b9291 100644 --- a/accessible/generic/LocalAccessible.h +++ b/accessible/generic/LocalAccessible.h @@ -736,6 +736,8 @@ class LocalAccessible : public nsISupports, public Accessible { virtual void DOMNodeID(nsString& aID) const override; + virtual void DOMNodeClass(nsString& aClass) const override; + virtual void LiveRegionAttributes(nsAString* aLive, nsAString* aRelevant, Maybe<bool>* aAtomic, nsAString* aBusy) const override; diff --git a/accessible/ipc/DocAccessibleChild.h b/accessible/ipc/DocAccessibleChild.h index 0a6164cce8..01673ba689 100644 --- a/accessible/ipc/DocAccessibleChild.h +++ b/accessible/ipc/DocAccessibleChild.h @@ -24,7 +24,9 @@ class AccShowEvent; */ class DocAccessibleChild : public PDocAccessibleChild { public: - DocAccessibleChild(DocAccessible* aDoc, IProtocol* aManager) : mDoc(aDoc) { + DocAccessibleChild(DocAccessible* aDoc, + mozilla::ipc::IRefCountedProtocol* aManager) + : mDoc(aDoc) { MOZ_COUNT_CTOR(DocAccessibleChild); SetManager(aManager); } diff --git a/accessible/ipc/RemoteAccessible.cpp b/accessible/ipc/RemoteAccessible.cpp index 772fc58776..0077750ed3 100644 --- a/accessible/ipc/RemoteAccessible.cpp +++ b/accessible/ipc/RemoteAccessible.cpp @@ -1329,6 +1329,13 @@ void RemoteAccessible::DOMNodeID(nsString& aID) const { } } +void RemoteAccessible::DOMNodeClass(nsString& aClass) const { + if (mCachedFields) { + mCachedFields->GetAttribute(CacheKey::DOMNodeClass, aClass); + VERIFY_CACHE(CacheDomain::DOMNodeIDAndClass); + } +} + void RemoteAccessible::ScrollToPoint(uint32_t aScrollType, int32_t aX, int32_t aY) { Unused << mDoc->SendScrollToPoint(mID, aScrollType, aX, aY); @@ -1551,7 +1558,7 @@ already_AddRefed<AccAttributes> RemoteAccessible::Attributes() { } nsString className; - mCachedFields->GetAttribute(CacheKey::DOMNodeClass, className); + DOMNodeClass(className); if (!className.IsEmpty()) { attributes->SetAttribute(nsGkAtoms::_class, std::move(className)); } diff --git a/accessible/ipc/RemoteAccessible.h b/accessible/ipc/RemoteAccessible.h index 9215fd7bc5..45f41b8fb5 100644 --- a/accessible/ipc/RemoteAccessible.h +++ b/accessible/ipc/RemoteAccessible.h @@ -365,6 +365,8 @@ class RemoteAccessible : public Accessible, public HyperTextAccessibleBase { virtual void DOMNodeID(nsString& aID) const override; + virtual void DOMNodeClass(nsString& aClass) const override; + virtual void ScrollToPoint(uint32_t aScrollType, int32_t aX, int32_t aY) override; diff --git a/accessible/mac/AccessibleWrap.mm b/accessible/mac/AccessibleWrap.mm index ef2f4ba779..e8107b1690 100644 --- a/accessible/mac/AccessibleWrap.mm +++ b/accessible/mac/AccessibleWrap.mm @@ -192,6 +192,9 @@ Class a11y::GetTypeFromRole(roles::Role aRole) { case roles::RADIO_MENU_ITEM: return [mozRadioButtonAccessible class]; + case roles::PROGRESSBAR: + return [mozRangeAccessible class]; + case roles::SPINBUTTON: case roles::SLIDER: return [mozIncrementableAccessible class]; diff --git a/accessible/mac/MOXWebAreaAccessible.mm b/accessible/mac/MOXWebAreaAccessible.mm index c1ae585fa1..37168b1be2 100644 --- a/accessible/mac/MOXWebAreaAccessible.mm +++ b/accessible/mac/MOXWebAreaAccessible.mm @@ -211,7 +211,7 @@ using namespace mozilla::a11y; - (NSArray*)moxUnignoredChildren { if (id rootGroup = [self rootGroup]) { - return @[ [self rootGroup] ]; + return @[ rootGroup ]; } // There is no root group, expose the children here directly. diff --git a/accessible/mac/mozActionElements.h b/accessible/mac/mozActionElements.h index f9940c793a..d5b286ff97 100644 --- a/accessible/mac/mozActionElements.h +++ b/accessible/mac/mozActionElements.h @@ -65,15 +65,14 @@ @end /** - * Base accessible for an incrementable + * Base accessible for a range, an acc with a min, max that cannot + * be modified by the user directly. */ -@interface mozIncrementableAccessible : mozAccessible -// override -- (id)moxValue; +@interface mozRangeAccessible : mozAccessible // override -- (NSString*)moxValueDescription; +- (id)moxValue; // override - (id)moxMinValue; @@ -82,19 +81,29 @@ - (id)moxMaxValue; // override -- (void)moxSetValue:(id)value; +- (NSString*)moxOrientation; // override -- (void)moxPerformIncrement; +- (void)handleAccessibleEvent:(uint32_t)eventType; + +@end + +/** + * Base accessible for an incrementable, a settable range + */ +@interface mozIncrementableAccessible : mozRangeAccessible // override -- (void)moxPerformDecrement; +- (NSString*)moxValueDescription; // override -- (NSString*)moxOrientation; +- (void)moxSetValue:(id)value; // override -- (void)handleAccessibleEvent:(uint32_t)eventType; +- (void)moxPerformIncrement; + +// override +- (void)moxPerformDecrement; - (void)changeValueBySteps:(int)factor; diff --git a/accessible/mac/mozActionElements.mm b/accessible/mac/mozActionElements.mm index f39f2c8ad5..e3a2ff9598 100644 --- a/accessible/mac/mozActionElements.mm +++ b/accessible/mac/mozActionElements.mm @@ -134,17 +134,12 @@ using namespace mozilla::a11y; @end -@implementation mozIncrementableAccessible +@implementation mozRangeAccessible - (id)moxValue { return [NSNumber numberWithDouble:mGeckoAccessible->CurValue()]; } -- (NSString*)moxValueDescription { - nsAutoString valueDesc; - mGeckoAccessible->Value(valueDesc); - return nsCocoaUtils::ToNSString(valueDesc); -} - (id)moxMinValue { return [NSNumber numberWithDouble:mGeckoAccessible->MinValue()]; } @@ -153,18 +148,6 @@ using namespace mozilla::a11y; return [NSNumber numberWithDouble:mGeckoAccessible->MaxValue()]; } -- (void)moxSetValue:(id)value { - [self setValue:([value doubleValue])]; -} - -- (void)moxPerformIncrement { - [self changeValueBySteps:1]; -} - -- (void)moxPerformDecrement { - [self changeValueBySteps:-1]; -} - - (NSString*)moxOrientation { RefPtr<AccAttributes> attributes = mGeckoAccessible->Attributes(); if (attributes) { @@ -192,6 +175,28 @@ using namespace mozilla::a11y; } } +@end + +@implementation mozIncrementableAccessible + +- (NSString*)moxValueDescription { + nsAutoString valueDesc; + mGeckoAccessible->Value(valueDesc); + return nsCocoaUtils::ToNSString(valueDesc); +} + +- (void)moxSetValue:(id)value { + [self setValue:([value doubleValue])]; +} + +- (void)moxPerformIncrement { + [self changeValueBySteps:1]; +} + +- (void)moxPerformDecrement { + [self changeValueBySteps:-1]; +} + /* * Updates the accessible's current value by factor and step. * diff --git a/accessible/tests/browser/e10s/browser.toml b/accessible/tests/browser/e10s/browser.toml index dff9b1c712..a4205d38e6 100644 --- a/accessible/tests/browser/e10s/browser.toml +++ b/accessible/tests/browser/e10s/browser.toml @@ -31,6 +31,7 @@ prefs = [ ["browser_caching_description.js"] ["browser_caching_document_props.js"] +https_first_disabled = true ["browser_caching_domnodeid.js"] diff --git a/accessible/tests/browser/e10s/browser_caching_states.js b/accessible/tests/browser/e10s/browser_caching_states.js index 7292228f25..6f674f8c48 100644 --- a/accessible/tests/browser/e10s/browser_caching_states.js +++ b/accessible/tests/browser/e10s/browser_caching_states.js @@ -425,6 +425,11 @@ addAccessibleTask( <div role="listbox" aria-multiselectable="true"> <div id="multiNoSel" role="option" tabindex="0">multiNoSel</div> </div> +<div role="grid"> + <div role="row"> + <div id="gridcell" role="gridcell" tabindex="0">gridcell</div> + </div> +</div> `, async function (browser, docAcc) { const noSel = findAccessibleChildByID(docAcc, "noSel"); @@ -450,6 +455,14 @@ addAccessibleTask( multiNoSel.takeFocus(); await focused; testStates(multiNoSel, STATE_FOCUSED, 0, STATE_SELECTED, 0); + + const gridcell = findAccessibleChildByID(docAcc, "gridcell"); + testStates(gridcell, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0); + info("Focusing gridcell"); + focused = waitForEvent(EVENT_FOCUS, gridcell); + gridcell.takeFocus(); + await focused; + testStates(gridcell, STATE_FOCUSED, 0, STATE_SELECTED, 0); }, { topLevel: true, iframe: true, remoteIframe: true, chrome: true } ); diff --git a/accessible/tests/browser/e10s/head.js b/accessible/tests/browser/e10s/head.js index bdbcb7445f..e72af914d4 100644 --- a/accessible/tests/browser/e10s/head.js +++ b/accessible/tests/browser/e10s/head.js @@ -170,6 +170,11 @@ async function testRelated( expected: [null, host, dependant2], }, { + desc: "Change attribute to multiple targets", + attrs: [{ key: attr, value: "dependant1 dependant2" }], + expected: [host, host, [dependant1, dependant2]], + }, + { desc: "Remove attribute", attrs: [{ key: attr }], expected: [null, null, null], diff --git a/accessible/tests/browser/mac/browser_range.js b/accessible/tests/browser/mac/browser_range.js index 430e41d6ea..8a5bafba50 100644 --- a/accessible/tests/browser/mac/browser_range.js +++ b/accessible/tests/browser/mac/browser_range.js @@ -188,3 +188,57 @@ addAccessibleTask( is(slider.getAttributeValue("AXMaxValue"), 5, "Correct max value"); } ); + +/** + * Verify progress HTML elements expose their min, max, and value to VO. + * Progress elements should not expose a value description, and should not + * expose increment/decrement actions. + */ +addAccessibleTask( + `<progress id="progress" value="70" max="100"></progress>`, + async (browser, accDoc) => { + const progress = getNativeInterface(accDoc, "progress"); + is(progress.getAttributeValue("AXValue"), 70, "Correct value"); + is(progress.getAttributeValue("AXMaxValue"), 100, "Correct max value"); + is(progress.getAttributeValue("AXMinValue"), 0, "Correct min value"); + is( + progress.getAttributeValue("AXValueDescription"), + null, + "Progress elements should not expose a value description" + ); + for (let action of progress.actionNames) { + isnot( + action, + "AXIncrement", + "Progress elements should not expose increment action" + ); + isnot( + action, + "AXDecrement", + "Progress elements should not expose decrement action" + ); + } + } +); + +/** + * Verify progress HTML elements expose changes to their value. + */ +addAccessibleTask( + `<progress id="progress" value="70" max="100"></progress>`, + async (browser, accDoc) => { + const progress = getNativeInterface(accDoc, "progress"); + is(progress.getAttributeValue("AXValue"), 70, "Correct value"); + is(progress.getAttributeValue("AXMaxValue"), 100, "Correct max value"); + is(progress.getAttributeValue("AXMinValue"), 0, "Correct min value"); + + const evt = waitForMacEvent("AXValueChanged"); + await invokeContentTask(browser, [], () => { + const p = content.document.getElementById("progress"); + p.setAttribute("value", "90"); + }); + await evt; + + is(progress.getAttributeValue("AXValue"), 90, "Correct updated value"); + } +); diff --git a/accessible/tests/browser/python_runner_wsh.py b/accessible/tests/browser/python_runner_wsh.py index 488051240f..2686967cfb 100644 --- a/accessible/tests/browser/python_runner_wsh.py +++ b/accessible/tests/browser/python_runner_wsh.py @@ -9,6 +9,7 @@ It is intended to be called from JS browser tests. """ import json +import math import os import sys import traceback @@ -83,6 +84,9 @@ def web_socket_transfer_data(request): exec(code, namespace) # Run the function we just defined. ret = namespace["run"]() + if isinstance(ret, float) and math.isnan(ret): + # NaN can't be serialized by JSON. + ret = None send("return", ret) except Exception: send("exception", traceback.format_exc()) diff --git a/accessible/tests/browser/selectable/browser_test_aria_select.js b/accessible/tests/browser/selectable/browser_test_aria_select.js index dbc36956f8..2c3a80586e 100644 --- a/accessible/tests/browser/selectable/browser_test_aria_select.js +++ b/accessible/tests/browser/selectable/browser_test_aria_select.js @@ -117,7 +117,8 @@ addAccessibleTask( // //////////////////////////////////////////////////////////////////////// // role="grid" aria-multiselectable, selectable children in subtree addAccessibleTask( - `<table tabindex="0" border="2" cellspacing="0" id="grid" role="grid" + ` + <table tabindex="0" border="2" cellspacing="0" id="grid" role="grid" aria-multiselectable="true"> <thead> <tr> @@ -133,19 +134,29 @@ addAccessibleTask( <tr> <td tabindex="-1" role="rowheader" id="grid_rowhead" aria-readonly="true">1</td> - <td tabindex="-1" role="gridcell" id="grid_cell1" + <td tabindex="-1" id="grid_cell1" aria-selected="true">03/14/05</td> - <td tabindex="-1" role="gridcell" id="grid_cell2" + <td tabindex="-1" id="grid_cell2" aria-selected="false">Conference Fee</td> </tr> - </tobdy> - </table>`, + </tbody> + </table> + <table id="table"> + <tr><th>a</th><td id="tableB" aria-selected="true">b</td></tr> + </table> + `, async function (browser, docAcc) { info('role="grid" aria-multiselectable, selectable children in subtree'); - let grid = findAccessibleChildByID(docAcc, "grid", [ + const grid = findAccessibleChildByID(docAcc, "grid", [ nsIAccessibleSelectable, ]); + // grid_cell1 is a <td> with an implicit role of gridcell. testSelectableSelection(grid, ["grid_colhead1", "grid_cell1"]); + info("Verify aria-selected doesn't apply to <td> that isn't gridcell"); + // We can't use testSelectableSelection here because table (rightly) isn't a + // selectable container. + const tableB = findAccessibleChildByID(docAcc, "tableB"); + testStates(tableB, 0, 0, STATE_SELECTED, 0); }, { chrome: true, diff --git a/accessible/tests/browser/windows/uia/browser.toml b/accessible/tests/browser/windows/uia/browser.toml index 75728f56d7..13b1c12cc0 100644 --- a/accessible/tests/browser/windows/uia/browser.toml +++ b/accessible/tests/browser/windows/uia/browser.toml @@ -11,8 +11,15 @@ support-files = ["head.js"] ["browser_elementFromPoint.js"] ["browser_focus.js"] + ["browser_generalProps.js"] +["browser_gridPatterns.js"] + +["browser_relationProps.js"] + +["browser_selectionPatterns.js"] + ["browser_simplePatterns.js"] ["browser_tree.js"] diff --git a/accessible/tests/browser/windows/uia/browser_generalProps.js b/accessible/tests/browser/windows/uia/browser_generalProps.js index 5cfda226d0..244c9e4b1b 100644 --- a/accessible/tests/browser/windows/uia/browser_generalProps.js +++ b/accessible/tests/browser/windows/uia/browser_generalProps.js @@ -4,6 +4,12 @@ "use strict"; +/* eslint-disable camelcase */ +// From https://learn.microsoft.com/en-us/windows/win32/winauto/landmark-type-identifiers +const UIA_CustomLandmarkTypeId = 80000; +const UIA_MainLandmarkTypeId = 80002; +/* eslint-enable camelcase */ + /** * Test the Name property. */ @@ -103,3 +109,347 @@ addUiaTask( ok(await runPython(`p.CurrentIsEnabled`), "p has IsEnabled true"); } ); + +async function testGroupPos(id, level, pos, size) { + await assignPyVarToUiaWithId(id); + is(await runPython(`${id}.CurrentLevel`), level, `${id} Level correct`); + is( + await runPython(`${id}.CurrentPositionInSet`), + pos, + `${id} PositionInSet correct` + ); + is( + await runPython(`${id}.CurrentSizeOfSet`), + size, + `${id} SizeOfSet correct` + ); +} + +/** + * Test the Level, PositionInSet and SizeOfSet properties. + */ +addUiaTask( + ` +<ul> + <li id="li1">li1<ul id="ul1"> + <li id="li2a">li2a</li> + <li id="li2b" hidden>li2b</li> + <li id="li2c">li2c</li> + </ul></li> +</ul> +<h2 id="h2">h2</h2> +<button id="button">button</button> + `, + async function testGroupPosProps(browser) { + await definePyVar("doc", `getDocUia()`); + await testGroupPos("li1", 1, 1, 1); + await testGroupPos("li2a", 2, 1, 2); + await testGroupPos("li2c", 2, 2, 2); + info("Showing li2b"); + // There aren't events in any API for a change to group position properties + // because this would be too spammy and isn't particularly useful given + // how frequently these can change. + let shown = waitForEvent(EVENT_SHOW, "li2b"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("li2b").hidden = false; + }); + await shown; + await testGroupPos("li2a", 2, 1, 3); + await testGroupPos("li2b", 2, 2, 3); + await testGroupPos("li2c", 2, 3, 3); + + // The IA2 -> UIA proxy doesn't map heading level to the Level property. + if (gIsUiaEnabled) { + await testGroupPos("h2", 2, 0, 0); + } + await testGroupPos("button", 0, 0, 0); + } +); + +/** + * Test the FrameworkId property. + */ +addUiaTask( + `<button id="button">button</button>`, + async function testFrameworkId() { + await definePyVar("doc", `getDocUia()`); + is( + await runPython(`doc.CurrentFrameworkId`), + "Gecko", + "doc FrameworkId is correct" + ); + await assignPyVarToUiaWithId("button"); + is( + await runPython(`button.CurrentFrameworkId`), + "Gecko", + "button FrameworkId is correct" + ); + } +); + +/** + * Test the ClassName property. + */ +addUiaTask( + ` +<p id="p">p</p> +<button id="button" class="c1">button</button> + `, + async function testClassName(browser, docAcc) { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("p"); + ok(!(await runPython(`p.CurrentClassName`)), "p has no ClassName"); + + await assignPyVarToUiaWithId("button"); + is( + await runPython(`button.CurrentClassName`), + "c1", + "button has correct ClassName" + ); + info("Changing button class"); + await invokeSetAttribute(browser, "button", "class", "c2 c3"); + // Gecko doesn't fire an event for class changes, as this isn't useful for + // clients. + const button = findAccessibleChildByID(docAcc, "button"); + await untilCacheIs( + () => button.attributes.getStringProperty("class"), + "c2 c3", + "button class updated" + ); + is( + await runPython(`button.CurrentClassName`), + "c2 c3", + "button has correct ClassName" + ); + }, + // The IA2 -> UIA proxy doesn't support ClassName. + { uiaEnabled: true, uiaDisabled: false } +); + +/** + * Test the AriaRole property. + */ +addUiaTask( + ` +<div id="button" role="button">button</div> +<div id="main" role="main">main</div> +<div id="unknown" role="unknown">unknown</div> +<button id="computedButton">computedButton</button> +<h1 id="computedHeading">computedHeading</h1> +<main id="computedMain">computedMain</main> +<div id="generic">generic</div> + `, + async function testAriaRole() { + await definePyVar("doc", `getDocUia()`); + is( + await runPython(`findUiaByDomId(doc, "button").CurrentAriaRole`), + "button", + "button has correct AriaRole" + ); + is( + await runPython(`findUiaByDomId(doc, "main").CurrentAriaRole`), + "main", + "main has correct AriaRole" + ); + is( + await runPython(`findUiaByDomId(doc, "unknown").CurrentAriaRole`), + "unknown", + "unknown has correct AriaRole" + ); + // The IA2 -> UIA proxy doesn't compute ARIA roles. + if (gIsUiaEnabled) { + is( + await runPython( + `findUiaByDomId(doc, "computedButton").CurrentAriaRole` + ), + "button", + "computedButton has correct AriaRole" + ); + is( + await runPython(`findUiaByDomId(doc, "computedMain").CurrentAriaRole`), + "main", + "computedMain has correct AriaRole" + ); + is( + await runPython( + `findUiaByDomId(doc, "computedHeading").CurrentAriaRole` + ), + "heading", + "computedHeading has correct AriaRole" + ); + is( + await runPython(`findUiaByDomId(doc, "generic").CurrentAriaRole`), + "generic", + "generic has correct AriaRole" + ); + } + } +); + +/** + * Test the LocalizedControlType property. We don't support this ourselves, but + * the system provides it based on ControlType and AriaRole. + */ +addUiaTask( + ` +<button id="button">button</button> +<h1 id="h1">h1</h1> +<main id="main">main</main> + `, + async function testLocalizedControlType() { + await definePyVar("doc", `getDocUia()`); + is( + await runPython( + `findUiaByDomId(doc, "button").CurrentLocalizedControlType` + ), + "button", + "button has correct LocalizedControlType" + ); + // The IA2 -> UIA proxy doesn't compute ARIA roles, so it can't compute the + // correct LocalizedControlType for these either. + if (gIsUiaEnabled) { + is( + await runPython( + `findUiaByDomId(doc, "h1").CurrentLocalizedControlType` + ), + "heading", + "h1 has correct LocalizedControlType" + ); + is( + await runPython( + `findUiaByDomId(doc, "main").CurrentLocalizedControlType` + ), + "main", + "main has correct LocalizedControlType" + ); + } + } +); + +/** + * Test the LandmarkType property. + */ +addUiaTask( + ` +<div id="main" role="main">main</div> +<main id="htmlMain">htmlMain</main> +<div id="banner" role="banner">banner</div> +<header id="header">header</header> +<div id="region" role="region" aria-label="region">region</div> +<div id="unnamedRegion" role="region">unnamedRegion</div> +<main id="mainBanner" role="banner">mainBanner</main> +<div id="none">none</div> + `, + async function testLandmarkType() { + await definePyVar("doc", `getDocUia()`); + is( + await runPython(`findUiaByDomId(doc, "main").CurrentLandmarkType`), + UIA_MainLandmarkTypeId, + "main has correct LandmarkType" + ); + is( + await runPython(`findUiaByDomId(doc, "htmlMain").CurrentLandmarkType`), + UIA_MainLandmarkTypeId, + "htmlMain has correct LandmarkType" + ); + is( + await runPython(`findUiaByDomId(doc, "banner").CurrentLandmarkType`), + UIA_CustomLandmarkTypeId, + "banner has correct LandmarkType" + ); + is( + await runPython(`findUiaByDomId(doc, "header").CurrentLandmarkType`), + UIA_CustomLandmarkTypeId, + "header has correct LandmarkType" + ); + is( + await runPython(`findUiaByDomId(doc, "region").CurrentLandmarkType`), + UIA_CustomLandmarkTypeId, + "region has correct LandmarkType" + ); + is( + await runPython( + `findUiaByDomId(doc, "unnamedRegion").CurrentLandmarkType` + ), + 0, + "unnamedRegion has correct LandmarkType" + ); + // ARIA role takes precedence. + is( + await runPython(`findUiaByDomId(doc, "mainBanner").CurrentLandmarkType`), + UIA_CustomLandmarkTypeId, + "mainBanner has correct LandmarkType" + ); + is( + await runPython(`findUiaByDomId(doc, "none").CurrentLandmarkType`), + 0, + "none has correct LandmarkType" + ); + } +); + +/** + * Test the LocalizedLandmarkType property. + */ +addUiaTask( + ` +<div id="main" role="main">main</div> +<div id="contentinfo" role="contentinfo">contentinfo</div> +<div id="region" role="region" aria-label="region">region</div> +<div id="unnamedRegion" role="region">unnamedRegion</div> +<main id="mainBanner" role="banner">mainBanner</main> +<div id="none">none</div> + `, + async function testLocalizedLandmarkType() { + await definePyVar("doc", `getDocUia()`); + // Provided by the system. + is( + await runPython( + `findUiaByDomId(doc, "main").CurrentLocalizedLandmarkType` + ), + "main", + "main has correct LocalizedLandmarkType" + ); + // The IA2 -> UIA proxy doesn't follow the Core AAM spec for this role. + if (gIsUiaEnabled) { + // Provided by us. + is( + await runPython( + `findUiaByDomId(doc, "contentinfo").CurrentLocalizedLandmarkType` + ), + "content information", + "contentinfo has correct LocalizedLandmarkType" + ); + } + is( + await runPython( + `findUiaByDomId(doc, "region").CurrentLocalizedLandmarkType` + ), + "region", + "region has correct LocalizedLandmarkType" + ); + // Invalid landmark. + is( + await runPython( + `findUiaByDomId(doc, "unnamedRegion").CurrentLocalizedLandmarkType` + ), + "", + "unnamedRegion has correct LocalizedLandmarkType" + ); + // ARIA role takes precedence. + is( + await runPython( + `findUiaByDomId(doc, "mainBanner").CurrentLocalizedLandmarkType` + ), + "banner", + "mainBanner has correct LocalizedLandmarkType" + ); + is( + await runPython( + `findUiaByDomId(doc, "none").CurrentLocalizedLandmarkType` + ), + "", + "none has correct LocalizedLandmarkType" + ); + } +); diff --git a/accessible/tests/browser/windows/uia/browser_gridPatterns.js b/accessible/tests/browser/windows/uia/browser_gridPatterns.js new file mode 100644 index 0000000000..24c80a6340 --- /dev/null +++ b/accessible/tests/browser/windows/uia/browser_gridPatterns.js @@ -0,0 +1,161 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* eslint-disable camelcase */ +const RowOrColumnMajor_RowMajor = 0; +/* eslint-enable camelcase */ + +const SNIPPET = ` +<table id="table"> + <tr><th id="a">a</th><th id="b">b</th><th id="c">c</th></tr> + <tr><th id="dg" rowspan="2">dg</th><td id="ef" colspan="2" headers="b c">ef</td></tr> + <tr><th id="h">h</th><td id="i" headers="dg h">i</td></tr> + <tr><td id="jkl" colspan="3" headers="a b c">jkl</td></tr> +</table> +<button id="button">button</button> +`; + +async function testGridGetItem(row, col, cellId) { + is( + await runPython(`pattern.GetItem(${row}, ${col}).CurrentAutomationId`), + cellId, + `GetItem with row ${row} and col ${col} returned ${cellId}` + ); +} + +async function testGridItemProps(id, row, col, rowSpan, colSpan, gridId) { + await assignPyVarToUiaWithId(id); + await definePyVar("pattern", `getUiaPattern(${id}, "GridItem")`); + ok(await runPython(`bool(pattern)`), `${id} has GridItem pattern`); + is(await runPython(`pattern.CurrentRow`), row, `${id} has correct Row`); + is(await runPython(`pattern.CurrentColumn`), col, `${id} has correct Column`); + is( + await runPython(`pattern.CurrentRowSpan`), + rowSpan, + `${id} has correct RowSpan` + ); + is( + await runPython(`pattern.CurrentColumnSpan`), + colSpan, + `${id} has correct ColumnSpan` + ); + is( + await runPython(`pattern.CurrentContainingGrid.CurrentAutomationId`), + gridId, + `${id} ContainingGridItem is ${gridId}` + ); +} + +async function testTableItemProps(id, rowHeaders, colHeaders) { + await assignPyVarToUiaWithId(id); + await definePyVar("pattern", `getUiaPattern(${id}, "TableItem")`); + ok(await runPython(`bool(pattern)`), `${id} has TableItem pattern`); + await isUiaElementArray( + `pattern.GetCurrentRowHeaderItems()`, + rowHeaders, + `${id} has correct RowHeaderItems` + ); + await isUiaElementArray( + `pattern.GetCurrentColumnHeaderItems()`, + colHeaders, + `${id} has correct ColumnHeaderItems` + ); +} + +/** + * Test the Grid pattern. + */ +addUiaTask(SNIPPET, async function testGrid() { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("table"); + await definePyVar("pattern", `getUiaPattern(table, "Grid")`); + ok(await runPython(`bool(pattern)`), "table has Grid pattern"); + is( + await runPython(`pattern.CurrentRowCount`), + 4, + "table has correct RowCount" + ); + is( + await runPython(`pattern.CurrentColumnCount`), + 3, + "table has correct ColumnCount" + ); + await testGridGetItem(0, 0, "a"); + await testGridGetItem(0, 1, "b"); + await testGridGetItem(0, 2, "c"); + await testGridGetItem(1, 0, "dg"); + await testGridGetItem(1, 1, "ef"); + await testGridGetItem(1, 2, "ef"); + await testGridGetItem(2, 0, "dg"); + await testGridGetItem(2, 1, "h"); + await testGridGetItem(2, 2, "i"); + + await testPatternAbsent("button", "Grid"); +}); + +/** + * Test the GridItem pattern. + */ +addUiaTask(SNIPPET, async function testGridItem() { + await definePyVar("doc", `getDocUia()`); + await testGridItemProps("a", 0, 0, 1, 1, "table"); + await testGridItemProps("b", 0, 1, 1, 1, "table"); + await testGridItemProps("c", 0, 2, 1, 1, "table"); + await testGridItemProps("dg", 1, 0, 2, 1, "table"); + await testGridItemProps("ef", 1, 1, 1, 2, "table"); + await testGridItemProps("jkl", 3, 0, 1, 3, "table"); + + await testPatternAbsent("button", "GridItem"); +}); + +/** + * Test the Table pattern. + */ +addUiaTask( + SNIPPET, + async function testTable() { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("table"); + await definePyVar("pattern", `getUiaPattern(table, "Table")`); + ok(await runPython(`bool(pattern)`), "table has Table pattern"); + await isUiaElementArray( + `pattern.GetCurrentRowHeaders()`, + ["dg", "h"], + "table has correct RowHeaders" + ); + await isUiaElementArray( + `pattern.GetCurrentColumnHeaders()`, + ["a", "b", "c"], + "table has correct ColumnHeaders" + ); + is( + await runPython(`pattern.CurrentRowOrColumnMajor`), + RowOrColumnMajor_RowMajor, + "table has correct RowOrColumnMajor" + ); + + await testPatternAbsent("button", "Table"); + }, + // The IA2 -> UIA proxy doesn't support the Row/ColumnHeaders properties. + { uiaEnabled: true, uiaDisabled: false } +); + +/** + * Test the TableItem pattern. + */ +addUiaTask(SNIPPET, async function testTableItem() { + await definePyVar("doc", `getDocUia()`); + await testTableItemProps("a", [], []); + await testTableItemProps("b", [], []); + await testTableItemProps("c", [], []); + await testTableItemProps("dg", [], ["a"]); + await testTableItemProps("ef", ["dg"], ["b", "c"]); + await testTableItemProps("h", ["dg"], ["b"]); + await testTableItemProps("i", ["dg", "h"], ["c"]); + await testTableItemProps("jkl", [], ["a", "b", "c"]); + + await testPatternAbsent("button", "TableItem"); +}); diff --git a/accessible/tests/browser/windows/uia/browser_relationProps.js b/accessible/tests/browser/windows/uia/browser_relationProps.js new file mode 100644 index 0000000000..ff4059f99e --- /dev/null +++ b/accessible/tests/browser/windows/uia/browser_relationProps.js @@ -0,0 +1,143 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function testUiaRelationArray(id, prop, targets) { + return isUiaElementArray( + `findUiaByDomId(doc, "${id}").Current${prop}`, + targets, + `${id} has correct ${prop} targets` + ); +} + +/** + * Test the ControllerFor property. + */ +addUiaTask( + ` +<input id="controls" aria-controls="t1 t2"> +<input id="error" aria-errormessage="t3 t4" aria-invalid="true"> +<input id="controlsError" aria-controls="t1 t2" aria-errormessage="t3 t4" aria-invalid="true"> +<div id="t1">t1</div> +<div id="t2">t2</div> +<div id="t3">t3</div> +<div id="t4">t4</div> +<button id="none">none</button> + `, + async function testControllerFor() { + await definePyVar("doc", `getDocUia()`); + await testUiaRelationArray("controls", "ControllerFor", ["t1", "t2"]); + // The IA2 -> UIA proxy doesn't support IA2_RELATION_ERROR. + if (gIsUiaEnabled) { + await testUiaRelationArray("error", "ControllerFor", ["t3", "t4"]); + await testUiaRelationArray("controlsError", "ControllerFor", [ + "t1", + "t2", + "t3", + "t4", + ]); + } + await testUiaRelationArray("none", "ControllerFor", []); + } +); + +/** + * Test the DescribedBy property. + */ +addUiaTask( + ` +<input id="describedby" aria-describedby="t1 t2"> +<input id="details" aria-details="t3 t4"> +<input id="describedbyDetails" aria-describedby="t1 t2" aria-details="t3 t4" aria-invalid="true"> +<div id="t1">t1</div> +<div id="t2">t2</div> +<div id="t3">t3</div> +<div id="t4">t4</div> +<button id="none">none</button> + `, + async function testDescribedBy() { + await definePyVar("doc", `getDocUia()`); + await testUiaRelationArray("describedby", "DescribedBy", ["t1", "t2"]); + // The IA2 -> UIA proxy doesn't support IA2_RELATION_DETAILS. + if (gIsUiaEnabled) { + await testUiaRelationArray("details", "DescribedBy", ["t3", "t4"]); + await testUiaRelationArray("describedbyDetails", "DescribedBy", [ + "t1", + "t2", + "t3", + "t4", + ]); + } + await testUiaRelationArray("none", "DescribedBy", []); + } +); + +/** + * Test the FlowsFrom and FlowsTo properties. + */ +addUiaTask( + ` +<div id="t1" aria-flowto="t2">t1</div> +<div id="t2">t2</div> +<button id="none">none</button> + `, + async function testFlows() { + await definePyVar("doc", `getDocUia()`); + await testUiaRelationArray("t1", "FlowsTo", ["t2"]); + await testUiaRelationArray("t2", "FlowsFrom", ["t1"]); + await testUiaRelationArray("none", "FlowsFrom", []); + await testUiaRelationArray("none", "FlowsTo", []); + } +); + +/** + * Test the LabeledBy property. + */ +addUiaTask( + ` +<label id="label">label</label> +<input id="input" aria-labelledby="label"> +<label id="wrappingLabel"> + <input id="wrappedInput" value="wrappedInput"> + <p id="wrappingLabelP">wrappingLabel</p> +</label> +<button id="button" aria-labelledby="label">content</button> +<button id="noLabel">noLabel</button> + `, + async function testLabeledBy() { + await definePyVar("doc", `getDocUia()`); + // input's LabeledBy should be label's text leaf. + let result = await runPython(` + input = findUiaByDomId(doc, "input") + label = findUiaByDomId(doc, "label") + labelLeaf = uiaClient.RawViewWalker.GetFirstChildElement(label) + return uiaClient.CompareElements(input.CurrentLabeledBy, labelLeaf) + `); + ok(result, "input has correct LabeledBy"); + // wrappedInput's LabeledBy should be wrappingLabelP's text leaf. + result = await runPython(` + wrappedInput = findUiaByDomId(doc, "wrappedInput") + wrappingLabelP = findUiaByDomId(doc, "wrappingLabelP") + wrappingLabelLeaf = uiaClient.RawViewWalker.GetFirstChildElement(wrappingLabelP) + return uiaClient.CompareElements(wrappedInput.CurrentLabeledBy, wrappingLabelLeaf) + `); + ok(result, "wrappedInput has correct LabeledBy"); + // button has aria-labelledby, but UIA prohibits LabeledBy on buttons. + ok( + !(await runPython( + `bool(findUiaByDomId(doc, "button").CurrentLabeledBy)` + )), + "button has no LabeledBy" + ); + ok( + !(await runPython( + `bool(findUiaByDomId(doc, "noLabel").CurrentLabeledBy)` + )), + "noLabel has no LabeledBy" + ); + }, + // The IA2 -> UIA proxy doesn't expose LabeledBy properly. + { uiaEnabled: true, uiaDisabled: false } +); diff --git a/accessible/tests/browser/windows/uia/browser_selectionPatterns.js b/accessible/tests/browser/windows/uia/browser_selectionPatterns.js new file mode 100644 index 0000000000..a1f70b886a --- /dev/null +++ b/accessible/tests/browser/windows/uia/browser_selectionPatterns.js @@ -0,0 +1,226 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const SNIPPET = ` +<select id="selectList" size="2"> + <option id="sl1" selected>sl1</option> + <option id="sl2">sl2</option> +</select> +<select id="selectRequired" size="2" required> + <option id="sr1">sr1</option> +</select> +<select id="selectMulti" size="2" multiple> + <option id="sm1" selected>sm1</option> + <option id="sm2">sm2</option> + <option id="sm3" selected>sm3</option> + <option id="sm4">sm4</option> + <option id="sm5">sm5</option> + <option id="sm6">sm6</option> +</select> +<select id="selectCombo" size="1"> + <option>sc1</option> +</select> +<div id="ariaListbox" role="listbox"> + <div id="al1" role="option" aria-selected="true">al1</div> + <div id="al2" role="option">al2</div> +</div> +<div id="tablist" role="tablist"> + <div id="t1" role="tab">t1</div> + <div id="t2" role="tab" aria-selected="true">t2</div> +</div> +<table id="grid" role="grid" aria-multiselectable="true"> + <tr> + <td id="g1">g1</td> + <td id="g2" role="gridcell" aria-selected="true">g2</td> + </tr> + <tr> + <td id="g3">g3</td> + <td id="g4" role="gridcell" aria-selected="true">g4</td> + </tr> +</table> +<div id="radiogroup" role="radiogroup"> + <label><input id="r1" type="radio" name="r" checked>r1</label> + <label><input id="r2" type="radio" name="r">r2</label> +</div> +<div id="menu" role="menu"> + <div id="m1" role="menuitem">m1</div> + <div id="m2" role="menuitemradio">m2</div> + <div id="m3" role="menuitemradio" aria-checked="true">m3</div> +</div> +<button id="button">button</button> +`; + +async function testSelectionProps(id, selection, multiple, required) { + await assignPyVarToUiaWithId(id); + await definePyVar("pattern", `getUiaPattern(${id}, "Selection")`); + ok(await runPython(`bool(pattern)`), `${id} has Selection pattern`); + await isUiaElementArray( + `pattern.GetCurrentSelection()`, + selection, + `${id} has correct Selection` + ); + is( + !!(await runPython(`pattern.CurrentCanSelectMultiple`)), + multiple, + `${id} has correct CanSelectMultiple` + ); + // The IA2 -> UIA proxy doesn't reflect the required state correctly. + if (gIsUiaEnabled) { + is( + !!(await runPython(`pattern.CurrentIsSelectionRequired`)), + required, + `${id} has correct IsSelectionRequired` + ); + } +} + +async function testSelectionItemProps(id, selected, container) { + await assignPyVarToUiaWithId(id); + await definePyVar("pattern", `getUiaPattern(${id}, "SelectionItem")`); + ok(await runPython(`bool(pattern)`), `${id} has SelectionItem pattern`); + is( + !!(await runPython(`pattern.CurrentIsSelected`)), + selected, + `${id} has correct IsSelected` + ); + if (container) { + is( + await runPython(`pattern.CurrentSelectionContainer.CurrentAutomationId`), + container, + `${id} has correct SelectionContainer` + ); + } else { + ok( + !(await runPython(`bool(pattern.CurrentSelectionContainer)`)), + `${id} has no SelectionContainer` + ); + } +} + +/** + * Test the Selection pattern. + */ +addUiaTask(SNIPPET, async function testSelection(browser) { + await definePyVar("doc", `getDocUia()`); + await testSelectionProps("selectList", ["sl1"], false, false); + await testSelectionProps("selectRequired", [], false, true); + + await testSelectionProps("selectMulti", ["sm1", "sm3"], true, false); + // The Selection pattern only has an event for a complete invalidation of the + // container's selection, which only happens when there are too many selection + // events. Smaller selection changes fire events in the SelectionItem pattern. + info("Changing selectMulti selection"); + await setUpWaitForUiaEvent("Selection_Invalidated", "selectMulti"); + await invokeContentTask(browser, [], () => { + const multi = content.document.getElementById("selectMulti"); + multi[0].selected = false; + multi[1].selected = true; + multi[2].selected = false; + multi[3].selected = true; + multi[4].selected = true; + multi[5].selected = true; + }); + await waitForUiaEvent(); + ok(true, "select got Invalidated event"); + await testSelectionProps( + "selectMulti", + ["sm2", "sm4", "sm5", "sm6"], + true, + false + ); + + await testPatternAbsent("selectCombo", "Selection"); + + await testSelectionProps("ariaListbox", ["al1"], false, false); + await testSelectionProps("tablist", ["t2"], false, false); + // The IA2 -> UIA proxy doesn't expose the Selection pattern on grids. + if (gIsUiaEnabled) { + await testSelectionProps("grid", ["g2", "g4"], true, false); + } + + // radio gets the SelectionItem pattern, but radiogroup doesn't get the + // Selection pattern for now. Same for menu/menuitemradio. + await testPatternAbsent("radiogroup", "Selection"); + await testPatternAbsent("menu", "Selection"); + + await testPatternAbsent("button", "Selection"); +}); + +/** + * Test the SelectionItem pattern. + */ +addUiaTask(SNIPPET, async function testSelection() { + await definePyVar("doc", `getDocUia()`); + await testPatternAbsent("selectList", "SelectionItem"); + await testSelectionItemProps("sl1", true, "selectList"); + await testSelectionItemProps("sl2", false, "selectList"); + info("Calling Select on sl2"); + await setUpWaitForUiaEvent("SelectionItem_ElementSelected", "sl2"); + await runPython(`pattern.Select()`); + await waitForUiaEvent(); + ok(true, "sl2 got ElementSelected event"); + await testSelectionItemProps("sl1", false, "selectList"); + await testSelectionItemProps("sl2", true, "selectList"); + + await testSelectionItemProps("sr1", false, "selectRequired"); + + await testSelectionItemProps("sm1", true, "selectMulti"); + await testSelectionItemProps("sm2", false, "selectMulti"); + info("Calling AddToSelection on sm2"); + await setUpWaitForUiaEvent("SelectionItem_ElementAddedToSelection", "sm2"); + await runPython(`pattern.AddToSelection()`); + await waitForUiaEvent(); + ok(true, "sm2 got ElementAddedToSelection event"); + await testSelectionItemProps("sm2", true, "selectMulti"); + await testSelectionItemProps("sm3", true, "selectMulti"); + info("Calling RemoveFromSelection on sm3"); + await setUpWaitForUiaEvent( + "SelectionItem_ElementRemovedFromSelection", + "sm3" + ); + await runPython(`pattern.RemoveFromSelection()`); + await waitForUiaEvent(); + ok(true, "sm3 got ElementRemovedFromSelection event"); + await testSelectionItemProps("sm3", false, "selectMulti"); + + await testSelectionItemProps("t1", false, "tablist"); + await testSelectionItemProps("t2", true, "tablist"); + + // The IA2 -> UIA proxy doesn't expose the SelectionItem pattern on grid + // cells. + if (gIsUiaEnabled) { + await testSelectionItemProps("g1", false, "grid"); + await testSelectionItemProps("g2", true, "grid"); + } + + await testSelectionItemProps("r1", true, null); + await testSelectionItemProps("r2", false, null); + // The IA2 -> UIA proxy doesn't fire correct events for radio buttons. + if (gIsUiaEnabled) { + info("Calling Select on r2"); + await setUpWaitForUiaEvent("SelectionItem_ElementSelected", "r2"); + await runPython(`pattern.Select()`); + await waitForUiaEvent(); + ok(true, "r2 got ElementSelected event"); + await testSelectionItemProps("r1", false, null); + await testSelectionItemProps("r2", true, null); + info("Calling RemoveFromSelection on r2"); + await testPythonRaises( + `pattern.RemoveFromSelection()`, + "RemoveFromSelection failed on r2" + ); + } + + await testPatternAbsent("m1", "SelectionItem"); + // The IA2 -> UIA proxy doesn't expose the SelectionItem pattern for radio + // menu items. + if (gIsUiaEnabled) { + await testSelectionItemProps("m2", false, null); + await testSelectionItemProps("m3", true, null); + } + + await testPatternAbsent("button", "SelectionItem"); +}); diff --git a/accessible/tests/browser/windows/uia/browser_simplePatterns.js b/accessible/tests/browser/windows/uia/browser_simplePatterns.js index f464db0e13..484d217af2 100644 --- a/accessible/tests/browser/windows/uia/browser_simplePatterns.js +++ b/accessible/tests/browser/windows/uia/browser_simplePatterns.js @@ -27,6 +27,7 @@ addUiaTask( <button id="button">button</button> <p id="p">p</p> <input id="checkbox" type="checkbox"> +<input id="radio" type="radio"> `, async function testInvoke() { await definePyVar("doc", `getDocUia()`); @@ -54,6 +55,8 @@ addUiaTask( // Check boxes expose the Toggle pattern, so they should not expose the // Invoke pattern. await testPatternAbsent("checkbox", "Invoke"); + // Ditto for radio buttons. + await testPatternAbsent("radio", "Invoke"); } } ); @@ -317,6 +320,7 @@ addUiaTask( await setUpWaitForUiaPropEvent("ValueValue", "text"); await runPython(`pattern.SetValue("after")`); await waitForUiaEvent(); + ok(true, "Got ValueValue prop change event on text"); is( await runPython(`pattern.CurrentValue`), "after", @@ -434,6 +438,7 @@ addUiaTask( await setUpWaitForUiaPropEvent("ValueValue", "ariaTextbox"); await runPython(`pattern.SetValue("after")`); await waitForUiaEvent(); + ok(true, "Got ValueValue prop change event on ariaTextbox"); is( await runPython(`pattern.CurrentValue`), "after", @@ -443,3 +448,95 @@ addUiaTask( await testPatternAbsent("button", "Value"); } ); + +async function testRangeValueProps(id, ro, val, min, max, small, large) { + await assignPyVarToUiaWithId(id); + await definePyVar("pattern", `getUiaPattern(${id}, "RangeValue")`); + ok(await runPython(`bool(pattern)`), `${id} has RangeValue pattern`); + is( + !!(await runPython(`pattern.CurrentIsReadOnly`)), + ro, + `${id} has IsReadOnly ${ro}` + ); + is(await runPython(`pattern.CurrentValue`), val, `${id} has correct Value`); + is( + await runPython(`pattern.CurrentMinimum`), + min, + `${id} has correct Minimum` + ); + is( + await runPython(`pattern.CurrentMaximum`), + max, + `${id} has correct Maximum` + ); + // IA2 doesn't support small/large change, so the IA2 -> UIA proxy can't + // either. + if (gIsUiaEnabled) { + is( + await runPython(`pattern.CurrentSmallChange`), + small, + `${id} has correct SmallChange` + ); + is( + await runPython(`pattern.CurrentLargeChange`), + large, + `${id} has correct LargeChange` + ); + } +} + +/** + * Test the RangeValue pattern. + */ +addUiaTask( + ` +<input id="range" type="range"> +<input id="rangeBig" type="range" max="1000"> +<progress id="progress" value="0.5"></progress> +<input id="numberRo" type="number" min="0" max="10" value="5" readonly> +<div id="ariaSlider" role="slider">slider</div> +<button id="button">button</button> + `, + async function testRangeValue(browser) { + await definePyVar("doc", `getDocUia()`); + await testRangeValueProps("range", false, 50, 0, 100, 1, 10); + info("SetValue on range"); + await setUpWaitForUiaPropEvent("RangeValueValue", "range"); + await runPython(`pattern.SetValue(20)`); + await waitForUiaEvent(); + ok(true, "Got RangeValueValue prop change event on range"); + is(await runPython(`pattern.CurrentValue`), 20, "range has correct Value"); + + await testRangeValueProps("rangeBig", false, 500, 0, 1000, 1, 100); + + // Gecko a11y doesn't expose progress bars as read only, but it probably + // should. + await testRangeValueProps("progress", false, 0.5, 0, 1, 0, 0.1); + info("Calling SetValue on progress"); + await testPythonRaises( + `pattern.SetValue(0.6)`, + "SetValue on progress failed" + ); + + await testRangeValueProps("numberRo", true, 5, 0, 10, 1, 1); + info("Calling SetValue on numberRo"); + await testPythonRaises( + `pattern.SetValue(6)`, + "SetValue on numberRo failed" + ); + + await testRangeValueProps("ariaSlider", false, 50, 0, 100, null, null); + info("Setting aria-valuenow on ariaSlider"); + await setUpWaitForUiaPropEvent("RangeValueValue", "ariaSlider"); + await invokeSetAttribute(browser, "ariaSlider", "aria-valuenow", "60"); + await waitForUiaEvent(); + ok(true, "Got RangeValueValue prop change event on ariaSlider"); + is( + await runPython(`pattern.CurrentValue`), + 60, + "ariaSlider has correct Value" + ); + + await testPatternAbsent("button", "RangeValue"); + } +); diff --git a/accessible/tests/browser/windows/uia/head.js b/accessible/tests/browser/windows/uia/head.js index 5b453ce6fe..217b8cb844 100644 --- a/accessible/tests/browser/windows/uia/head.js +++ b/accessible/tests/browser/windows/uia/head.js @@ -4,7 +4,7 @@ "use strict"; -/* exported gIsUiaEnabled, addUiaTask, definePyVar, assignPyVarToUiaWithId, setUpWaitForUiaEvent, setUpWaitForUiaPropEvent, waitForUiaEvent, testPatternAbsent, testPythonRaises */ +/* exported gIsUiaEnabled, addUiaTask, definePyVar, assignPyVarToUiaWithId, setUpWaitForUiaEvent, setUpWaitForUiaPropEvent, waitForUiaEvent, testPatternAbsent, testPythonRaises, isUiaElementArray */ // Load the shared-head file first. Services.scriptloader.loadSubScript( @@ -126,3 +126,15 @@ async function testPythonRaises(expression, message) { } ok(failed, message); } + +/** + * Verify that an array of UIA elements contains (only) elements with the given + * DOM ids. + */ +async function isUiaElementArray(pyExpr, ids, message) { + const result = await runPython(` + uias = (${pyExpr}) + return [uias.GetElement(i).CurrentAutomationId for i in range(uias.Length)] + `); + SimpleTest.isDeeply(result, ids, message); +} diff --git a/accessible/tests/mochitest/attributes.js b/accessible/tests/mochitest/attributes.js index ebb5a54b85..65afebd83a 100644 --- a/accessible/tests/mochitest/attributes.js +++ b/accessible/tests/mochitest/attributes.js @@ -295,13 +295,10 @@ const kBoldFontWeight = function equalsToBold(aWeight) { return aWeight > 400; }; -let isNNT = SpecialPowers.getBoolPref("widget.non-native-theme.enabled"); // The pt font size of the input element can vary by Linux distro. const kInputFontSize = - WIN || (MAC && isNNT) + WIN || MAC ? "10pt" - : MAC - ? "8pt" : function () { return true; }; diff --git a/accessible/tests/mochitest/role/test_dpub_aria.html b/accessible/tests/mochitest/role/test_dpub_aria.html index 621c86a59b..8294669caf 100644 --- a/accessible/tests/mochitest/role/test_dpub_aria.html +++ b/accessible/tests/mochitest/role/test_dpub_aria.html @@ -36,7 +36,7 @@ testRole("doc-epigraph", ROLE_SECTION); testRole("doc-epilogue", ROLE_LANDMARK); testRole("doc-errata", ROLE_LANDMARK); - testRole("doc-example", ROLE_SECTION); + testRole("doc-example", ROLE_FIGURE); testRole("doc-footnote", ROLE_FOOTNOTE); testRole("doc-foreword", ROLE_LANDMARK); testRole("doc-glossary", ROLE_LANDMARK); @@ -46,6 +46,8 @@ testRole("doc-noteref", ROLE_LINK); testRole("doc-notice", ROLE_NOTE); testRole("doc-pagebreak", ROLE_SEPARATOR); + testRole("doc-pagefooter", ROLE_SECTION); + testRole("doc-pageheader", ROLE_SECTION); testRole("doc-pagelist", ROLE_NAVIGATION); testRole("doc-part", ROLE_LANDMARK); testRole("doc-preface", ROLE_LANDMARK); @@ -101,6 +103,8 @@ <div id="doc-noteref" role="doc-noteref">noteref</div> <div id="doc-notice" role="doc-notice">notice</div> <div id="doc-pagebreak" role="doc-pagebreak">pagebreak</div> + <div id="doc-pagefooter" role="doc-pagefooter">pagefooter</div> + <div id="doc-pageheader" role="doc-pageheader">pageheader</div> <div id="doc-pagelist" role="doc-pagelist">pagelist</div> <div id="doc-part" role="doc-part">part</div> <div id="doc-preface" role="doc-preface">preface</div> diff --git a/accessible/windows/ia2/ia2AccessibleTable.cpp b/accessible/windows/ia2/ia2AccessibleTable.cpp index 50bdc79967..9ce94e8348 100644 --- a/accessible/windows/ia2/ia2AccessibleTable.cpp +++ b/accessible/windows/ia2/ia2AccessibleTable.cpp @@ -21,7 +21,7 @@ using namespace mozilla::a11y; TableAccessible* ia2AccessibleTable::TableAcc() { - Accessible* acc = Acc(); + Accessible* acc = MsaaAccessible::Acc(); return acc ? acc->AsTable() : nullptr; } @@ -46,6 +46,18 @@ ia2AccessibleTable::QueryInterface(REFIID iid, void** ppv) { return S_OK; } + if (IID_IGridProvider == iid) { + *ppv = static_cast<IGridProvider*>(this); + (reinterpret_cast<IUnknown*>(*ppv))->AddRef(); + return S_OK; + } + + if (IID_ITableProvider == iid) { + *ppv = static_cast<ITableProvider*>(this); + (reinterpret_cast<IUnknown*>(*ppv))->AddRef(); + return S_OK; + } + return ia2AccessibleHypertext::QueryInterface(iid, ppv); } diff --git a/accessible/windows/ia2/ia2AccessibleTable.h b/accessible/windows/ia2/ia2AccessibleTable.h index 622187c379..e8479b1733 100644 --- a/accessible/windows/ia2/ia2AccessibleTable.h +++ b/accessible/windows/ia2/ia2AccessibleTable.h @@ -12,6 +12,7 @@ #include "AccessibleTable2.h" #include "ia2AccessibleHypertext.h" #include "IUnknownImpl.h" +#include "UiaGrid.h" namespace mozilla { namespace a11y { @@ -20,6 +21,7 @@ class TableAccessible; class ia2AccessibleTable : public IAccessibleTable, public IAccessibleTable2, + public UiaGrid, public ia2AccessibleHypertext { public: // IUnknown diff --git a/accessible/windows/ia2/ia2AccessibleTableCell.cpp b/accessible/windows/ia2/ia2AccessibleTableCell.cpp index 0204983a08..c8e98d6a2f 100644 --- a/accessible/windows/ia2/ia2AccessibleTableCell.cpp +++ b/accessible/windows/ia2/ia2AccessibleTableCell.cpp @@ -27,6 +27,8 @@ TableCellAccessible* ia2AccessibleTableCell::CellAcc() { // IUnknown IMPL_IUNKNOWN_QUERY_HEAD(ia2AccessibleTableCell) IMPL_IUNKNOWN_QUERY_IFACE(IAccessibleTableCell) +IMPL_IUNKNOWN_QUERY_IFACE(IGridItemProvider) +IMPL_IUNKNOWN_QUERY_IFACE(ITableItemProvider) IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(ia2AccessibleHypertext) //////////////////////////////////////////////////////////////////////////////// diff --git a/accessible/windows/ia2/ia2AccessibleTableCell.h b/accessible/windows/ia2/ia2AccessibleTableCell.h index 04e978cb96..6925cd7b17 100644 --- a/accessible/windows/ia2/ia2AccessibleTableCell.h +++ b/accessible/windows/ia2/ia2AccessibleTableCell.h @@ -11,12 +11,14 @@ #include "AccessibleTableCell.h" #include "ia2AccessibleHypertext.h" #include "IUnknownImpl.h" +#include "UiaGridItem.h" namespace mozilla { namespace a11y { class TableCellAccessible; class ia2AccessibleTableCell : public IAccessibleTableCell, + public UiaGridItem, public ia2AccessibleHypertext { public: // IUnknown diff --git a/accessible/windows/msaa/IUnknownImpl.h b/accessible/windows/msaa/IUnknownImpl.h index 7935ebedda..757bff44bf 100644 --- a/accessible/windows/msaa/IUnknownImpl.h +++ b/accessible/windows/msaa/IUnknownImpl.h @@ -47,31 +47,31 @@ class AutoRefCnt { } // namespace a11y } // namespace mozilla -#define DECL_IUNKNOWN \ - public: \ - virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**); \ - ULONG STDMETHODCALLTYPE AddRef() override { \ - MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); \ - ++mRefCnt; \ - return mRefCnt; \ - } \ - ULONG STDMETHODCALLTYPE Release() override { \ - MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); \ - --mRefCnt; \ - if (mRefCnt) return mRefCnt; \ - \ - delete this; \ - return 0; \ - } \ - \ - private: \ - mozilla::a11y::AutoRefCnt mRefCnt; \ - \ +#define DECL_IUNKNOWN \ + public: \ + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**) override; \ + ULONG STDMETHODCALLTYPE AddRef() override { \ + MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); \ + ++mRefCnt; \ + return mRefCnt; \ + } \ + ULONG STDMETHODCALLTYPE Release() override { \ + MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); \ + --mRefCnt; \ + if (mRefCnt) return mRefCnt; \ + \ + delete this; \ + return 0; \ + } \ + \ + private: \ + mozilla::a11y::AutoRefCnt mRefCnt; \ + \ public: #define DECL_IUNKNOWN_INHERITED \ public: \ - virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**); + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**) override; #define IMPL_IUNKNOWN_QUERY_HEAD(Class) \ STDMETHODIMP \ diff --git a/accessible/windows/msaa/LazyInstantiator.cpp b/accessible/windows/msaa/LazyInstantiator.cpp index 61814d059a..3ace26d9c0 100644 --- a/accessible/windows/msaa/LazyInstantiator.cpp +++ b/accessible/windows/msaa/LazyInstantiator.cpp @@ -12,6 +12,7 @@ #include "mozilla/a11y/Platform.h" #include "mozilla/Assertions.h" #include "mozilla/mscom/ProcessRuntime.h" +#include "mozilla/StaticPrefs_accessibility.h" #include "mozilla/UniquePtr.h" #include "mozilla/WinHeaderOnlyUtils.h" #include "MsaaRootAccessible.h" @@ -41,9 +42,9 @@ static const wchar_t kLazyInstantiatorProp[] = Maybe<bool> LazyInstantiator::sShouldBlockUia; -/* static */ -already_AddRefed<IAccessible> LazyInstantiator::GetRootAccessible(HWND aHwnd) { - RefPtr<IAccessible> result; +template <class T> +already_AddRefed<T> LazyInstantiator::GetRoot(HWND aHwnd) { + RefPtr<T> result; // At this time we only want to check whether the acc service is running. We // don't actually want to create the acc service yet. if (!GetAccService()) { @@ -80,7 +81,7 @@ already_AddRefed<IAccessible> LazyInstantiator::GetRootAccessible(HWND aHwnd) { if (!rootAcc->IsRoot()) { // rootAcc might represent a popup as opposed to a true root accessible. // In that case we just use the regular LocalAccessible::GetNativeInterface. - rootAcc->GetNativeInterface(getter_AddRefs(result)); + result = MsaaAccessible::GetFrom(rootAcc); return result.forget(); } @@ -90,7 +91,7 @@ already_AddRefed<IAccessible> LazyInstantiator::GetRootAccessible(HWND aHwnd) { // don't need LazyInstantiator's capabilities anymore (since a11y is already // running). We can bypass LazyInstantiator by retrieving the internal // unknown (which is not wrapped by the LazyInstantiator) and then querying - // that for IID_IAccessible. + // that for the interface we want. RefPtr<IUnknown> punk(msaaRoot->GetInternalUnknown()); MOZ_ASSERT(punk); @@ -98,10 +99,24 @@ already_AddRefed<IAccessible> LazyInstantiator::GetRootAccessible(HWND aHwnd) { return nullptr; } - punk->QueryInterface(IID_IAccessible, getter_AddRefs(result)); + punk->QueryInterface(__uuidof(T), getter_AddRefs(result)); return result.forget(); } +/* static */ +already_AddRefed<IAccessible> LazyInstantiator::GetRootAccessible(HWND aHwnd) { + return GetRoot<IAccessible>(aHwnd); +} + +/* static */ +already_AddRefed<IRawElementProviderSimple> LazyInstantiator::GetRootUia( + HWND aHwnd) { + if (!StaticPrefs::accessibility_uia_enable()) { + return nullptr; + } + return GetRoot<IRawElementProviderSimple>(aHwnd); +} + /** * When marshaling an interface, COM makes a whole bunch of QueryInterface * calls to determine what kind of marshaling the interface supports. We need @@ -135,7 +150,8 @@ LazyInstantiator::LazyInstantiator(HWND aHwnd) mAllowBlindAggregation(false), mWeakMsaaRoot(nullptr), mWeakAccessible(nullptr), - mWeakDispatch(nullptr) { + mWeakDispatch(nullptr), + mWeakUia(nullptr) { MOZ_ASSERT(aHwnd); // Assign ourselves as the designated LazyInstantiator for aHwnd DebugOnly<BOOL> setPropOk = @@ -374,9 +390,16 @@ LazyInstantiator::MaybeResolveRoot() { if (FAILED(hr)) { return hr; } - // mWeakAccessible is weak, so don't hold a strong ref mWeakAccessible->Release(); + if (StaticPrefs::accessibility_uia_enable()) { + hr = mRealRootUnk->QueryInterface(IID_IRawElementProviderSimple, + (void**)&mWeakUia); + if (FAILED(hr)) { + return hr; + } + mWeakUia->Release(); + } // Now that a11y is running, we don't need to remain registered with our // HWND anymore. @@ -401,6 +424,9 @@ IMPL_IUNKNOWN_QUERY_IFACE_AMBIGIOUS(IUnknown, IAccessible) IMPL_IUNKNOWN_QUERY_IFACE(IAccessible) IMPL_IUNKNOWN_QUERY_IFACE(IDispatch) IMPL_IUNKNOWN_QUERY_IFACE(IServiceProvider) +if (StaticPrefs::accessibility_uia_enable()) { + IMPL_IUNKNOWN_QUERY_IFACE(IRawElementProviderSimple) +} // See EnableBlindAggregation for comments. if (!mAllowBlindAggregation) { return E_NOINTERFACE; @@ -771,5 +797,45 @@ LazyInstantiator::QueryService(REFGUID aServiceId, REFIID aServiceIid, return servProv->QueryService(aServiceId, aServiceIid, aOutInterface); } +STDMETHODIMP +LazyInstantiator::get_ProviderOptions( + __RPC__out enum ProviderOptions* aOptions) { + // This method is called before a UIA connection is fully established and thus + // before we can detect the client. We must not call RESOLVE_ROOT here because + // this might turn out to be a client we want to block. + if (!aOptions) { + return E_INVALIDARG; + } + *aOptions = uiaRawElmProvider::kProviderOptions; + return S_OK; +} + +STDMETHODIMP +LazyInstantiator::GetPatternProvider( + PATTERNID aPatternId, __RPC__deref_out_opt IUnknown** aPatternProvider) { + RESOLVE_ROOT; + return mWeakUia->GetPatternProvider(aPatternId, aPatternProvider); +} + +STDMETHODIMP +LazyInstantiator::GetPropertyValue(PROPERTYID aPropertyId, + __RPC__out VARIANT* aPropertyValue) { + RESOLVE_ROOT; + return mWeakUia->GetPropertyValue(aPropertyId, aPropertyValue); +} + +STDMETHODIMP +LazyInstantiator::get_HostRawElementProvider( + __RPC__deref_out_opt IRawElementProviderSimple** aRawElmProvider) { + // This method is called before a UIA connection is fully established and thus + // before we can detect the client. We must not call RESOLVE_ROOT here because + // this might turn out to be a client we want to block. + if (!aRawElmProvider) { + return E_INVALIDARG; + } + *aRawElmProvider = nullptr; + return UiaHostProviderFromHwnd(mHwnd, aRawElmProvider); +} + } // namespace a11y } // namespace mozilla diff --git a/accessible/windows/msaa/LazyInstantiator.h b/accessible/windows/msaa/LazyInstantiator.h index 00fa4ba6ed..adbb0f70b5 100644 --- a/accessible/windows/msaa/LazyInstantiator.h +++ b/accessible/windows/msaa/LazyInstantiator.h @@ -13,6 +13,7 @@ #include "nsString.h" #include <oleacc.h> +#include <uiautomation.h> class nsIFile; @@ -29,10 +30,14 @@ class MsaaRootAccessible; * services in order to fulfill; and * (2) LazyInstantiator::ShouldInstantiate returns true. */ -class LazyInstantiator final : public IAccessible, public IServiceProvider { +class LazyInstantiator final : public IAccessible, + public IServiceProvider, + public IRawElementProviderSimple { public: [[nodiscard]] static already_AddRefed<IAccessible> GetRootAccessible( HWND aHwnd); + [[nodiscard]] static already_AddRefed<IRawElementProviderSimple> GetRootUia( + HWND aHwnd); static void EnableBlindAggregation(HWND aHwnd); // IUnknown @@ -83,6 +88,22 @@ class LazyInstantiator final : public IAccessible, public IServiceProvider { STDMETHODIMP QueryService(REFGUID aServiceId, REFIID aServiceIid, void** aOutInterface) override; + // IRawElementProviderSimple + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_ProviderOptions( + /* [retval][out] */ __RPC__out enum ProviderOptions* aProviderOptions); + + virtual HRESULT STDMETHODCALLTYPE GetPatternProvider( + /* [in] */ PATTERNID aPatternId, + /* [retval][out] */ __RPC__deref_out_opt IUnknown** aPatternProvider); + + virtual HRESULT STDMETHODCALLTYPE GetPropertyValue( + /* [in] */ PROPERTYID aPropertyId, + /* [retval][out] */ __RPC__out VARIANT* aPropertyValue); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_HostRawElementProvider( + /* [retval][out] */ __RPC__deref_out_opt IRawElementProviderSimple** + aRawElmProvider); + /** * We cache the result of UIA detection because it could be expensive if a * client repeatedly queries us. This function is called to reset that cache @@ -117,6 +138,9 @@ class LazyInstantiator final : public IAccessible, public IServiceProvider { void TransplantRefCnt(); void ClearProp(); + template <class T> + static already_AddRefed<T> GetRoot(HWND aHwnd); + private: mozilla::a11y::AutoRefCnt mRefCnt; HWND mHwnd; @@ -133,6 +157,7 @@ class LazyInstantiator final : public IAccessible, public IServiceProvider { MsaaRootAccessible* mWeakMsaaRoot; IAccessible* mWeakAccessible; IDispatch* mWeakDispatch; + IRawElementProviderSimple* mWeakUia; static Maybe<bool> sShouldBlockUia; }; diff --git a/accessible/windows/msaa/Platform.cpp b/accessible/windows/msaa/Platform.cpp index f4d1c7b176..eaf9e7c126 100644 --- a/accessible/windows/msaa/Platform.cpp +++ b/accessible/windows/msaa/Platform.cpp @@ -129,6 +129,7 @@ void a11y::PlatformShowHideEvent(Accessible* aTarget, Accessible*, bool aInsert, void a11y::PlatformSelectionEvent(Accessible* aTarget, Accessible*, uint32_t aType) { MsaaAccessible::FireWinEvent(aTarget, aType); + uiaRawElmProvider::RaiseUiaEventForGeckoEvent(aTarget, aType); } static bool GetInstantiatorExecutable(const DWORD aPid, diff --git a/accessible/windows/uia/UiaGrid.cpp b/accessible/windows/uia/UiaGrid.cpp new file mode 100644 index 0000000000..2a22a1f4da --- /dev/null +++ b/accessible/windows/uia/UiaGrid.cpp @@ -0,0 +1,151 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ia2AccessibleTable.h" +#include "mozilla/a11y/TableAccessible.h" +#include "nsIAccessiblePivot.h" +#include "Pivot.h" +#include "UiaGrid.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +// Helpers + +// Used to search for all row and column headers in a table. This could be slow, +// as it potentially walks all cells in the table. However, it's unclear if, +// when or how often clients will use this. If this proves to be a performance +// problem, we will need to add methods to TableAccessible to get all row and +// column headers in a faster way. +class HeaderRule : public PivotRule { + public: + explicit HeaderRule(role aRole) : mRole(aRole) {} + + virtual uint16_t Match(Accessible* aAcc) override { + role accRole = aAcc->Role(); + if (accRole == mRole) { + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + if (accRole == roles::CAPTION || aAcc->IsTableCell()) { + return nsIAccessibleTraversalRule::FILTER_IGNORE | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + return nsIAccessibleTraversalRule::FILTER_IGNORE; + } + + private: + role mRole; +}; + +static SAFEARRAY* GetAllHeaders(Accessible* aTable, role aRole) { + AutoTArray<Accessible*, 20> headers; + Pivot pivot(aTable); + HeaderRule rule(aRole); + for (Accessible* header = pivot.Next(aTable, rule); header; + header = pivot.Next(header, rule)) { + headers.AppendElement(header); + } + return AccessibleArrayToUiaArray(headers); +} + +// UiaGrid + +Accessible* UiaGrid::Acc() { + auto* ia2t = static_cast<ia2AccessibleTable*>(this); + return ia2t->MsaaAccessible::Acc(); +} + +TableAccessible* UiaGrid::TableAcc() { + Accessible* acc = Acc(); + return acc ? acc->AsTable() : nullptr; +} + +// IGridProvider methods + +STDMETHODIMP +UiaGrid::GetItem(int aRow, int aColumn, + __RPC__deref_out_opt IRawElementProviderSimple** aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + *aRetVal = nullptr; + TableAccessible* table = TableAcc(); + if (!table) { + return CO_E_OBJNOTCONNECTED; + } + Accessible* cell = table->CellAt(aRow, aColumn); + if (!cell) { + return E_INVALIDARG; + } + RefPtr<IRawElementProviderSimple> uia = MsaaAccessible::GetFrom(cell); + uia.forget(aRetVal); + return S_OK; +} + +STDMETHODIMP +UiaGrid::get_RowCount(__RPC__out int* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + TableAccessible* table = TableAcc(); + if (!table) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = table->RowCount(); + return S_OK; +} + +STDMETHODIMP +UiaGrid::get_ColumnCount(__RPC__out int* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + TableAccessible* table = TableAcc(); + if (!table) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = table->ColCount(); + return S_OK; +} + +// ITableProvider methods + +STDMETHODIMP +UiaGrid::GetRowHeaders(__RPC__deref_out_opt SAFEARRAY** aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = GetAllHeaders(acc, roles::ROWHEADER); + return S_OK; +} + +STDMETHODIMP +UiaGrid::GetColumnHeaders(__RPC__deref_out_opt SAFEARRAY** aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = GetAllHeaders(acc, roles::COLUMNHEADER); + return S_OK; +} + +STDMETHODIMP +UiaGrid::get_RowOrColumnMajor(__RPC__out enum RowOrColumnMajor* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + // HTML and ARIA tables are always in row major order. + *aRetVal = RowOrColumnMajor_RowMajor; + return S_OK; +} diff --git a/accessible/windows/uia/UiaGrid.h b/accessible/windows/uia/UiaGrid.h new file mode 100644 index 0000000000..c38442246c --- /dev/null +++ b/accessible/windows/uia/UiaGrid.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_UiaGrid_h__ +#define mozilla_a11y_UiaGrid_h__ + +#include "objbase.h" +#include "uiautomation.h" + +namespace mozilla::a11y { +class Accessible; +class TableAccessible; + +/** + * IGridProvider and ITableProvider implementations. + */ +class UiaGrid : public IGridProvider, public ITableProvider { + public: + // IGridProvider + virtual HRESULT STDMETHODCALLTYPE GetItem( + /* [in] */ int aRow, + /* [in] */ int aColumn, + /* [retval][out] */ + __RPC__deref_out_opt IRawElementProviderSimple** aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_RowCount( + /* [retval][out] */ __RPC__out int* aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_ColumnCount( + /* [retval][out] */ __RPC__out int* aRetVal); + + // ITableProvider + virtual HRESULT STDMETHODCALLTYPE GetRowHeaders( + /* [retval][out] */ __RPC__deref_out_opt SAFEARRAY** aRetVal); + + virtual HRESULT STDMETHODCALLTYPE GetColumnHeaders( + /* [retval][out] */ __RPC__deref_out_opt SAFEARRAY** aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_RowOrColumnMajor( + /* [retval][out] */ __RPC__out enum RowOrColumnMajor* aRetVal); + + private: + Accessible* Acc(); + TableAccessible* TableAcc(); +}; + +} // namespace mozilla::a11y + +#endif diff --git a/accessible/windows/uia/UiaGridItem.cpp b/accessible/windows/uia/UiaGridItem.cpp new file mode 100644 index 0000000000..89c56b8d45 --- /dev/null +++ b/accessible/windows/uia/UiaGridItem.cpp @@ -0,0 +1,130 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ia2AccessibleTableCell.h" +#include "mozilla/a11y/TableAccessible.h" +#include "mozilla/a11y/TableCellAccessible.h" +#include "UiaGridItem.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +// UiaGridItem + +TableCellAccessible* UiaGridItem::CellAcc() { + auto* derived = static_cast<ia2AccessibleTableCell*>(this); + Accessible* acc = derived->Acc(); + return acc ? acc->AsTableCell() : nullptr; +} + +// IGridItemProvider methods + +STDMETHODIMP +UiaGridItem::get_Row(__RPC__out int* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + TableCellAccessible* cell = CellAcc(); + if (!cell) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = cell->RowIdx(); + return S_OK; +} + +STDMETHODIMP +UiaGridItem::get_Column(__RPC__out int* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + TableCellAccessible* cell = CellAcc(); + if (!cell) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = cell->ColIdx(); + return S_OK; +} + +STDMETHODIMP +UiaGridItem::get_RowSpan(__RPC__out int* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + TableCellAccessible* cell = CellAcc(); + if (!cell) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = cell->RowExtent(); + return S_OK; +} + +STDMETHODIMP +UiaGridItem::get_ColumnSpan(__RPC__out int* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + TableCellAccessible* cell = CellAcc(); + if (!cell) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = cell->ColExtent(); + return S_OK; +} + +STDMETHODIMP +UiaGridItem::get_ContainingGrid( + __RPC__deref_out_opt IRawElementProviderSimple** aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + *aRetVal = nullptr; + TableCellAccessible* cell = CellAcc(); + if (!cell) { + return CO_E_OBJNOTCONNECTED; + } + TableAccessible* table = cell->Table(); + if (!table) { + return E_FAIL; + } + Accessible* tableAcc = table->AsAccessible(); + RefPtr<IRawElementProviderSimple> uia = MsaaAccessible::GetFrom(tableAcc); + uia.forget(aRetVal); + return S_OK; +} + +// ITableItemProvider methods + +STDMETHODIMP +UiaGridItem::GetRowHeaderItems(__RPC__deref_out_opt SAFEARRAY** aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + *aRetVal = nullptr; + TableCellAccessible* cell = CellAcc(); + if (!cell) { + return CO_E_OBJNOTCONNECTED; + } + AutoTArray<Accessible*, 10> cells; + cell->RowHeaderCells(&cells); + *aRetVal = AccessibleArrayToUiaArray(cells); + return S_OK; +} + +STDMETHODIMP +UiaGridItem::GetColumnHeaderItems(__RPC__deref_out_opt SAFEARRAY** aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + *aRetVal = nullptr; + TableCellAccessible* cell = CellAcc(); + if (!cell) { + return CO_E_OBJNOTCONNECTED; + } + AutoTArray<Accessible*, 10> cells; + cell->ColHeaderCells(&cells); + *aRetVal = AccessibleArrayToUiaArray(cells); + return S_OK; +} diff --git a/accessible/windows/uia/UiaGridItem.h b/accessible/windows/uia/UiaGridItem.h new file mode 100644 index 0000000000..6c59630b0f --- /dev/null +++ b/accessible/windows/uia/UiaGridItem.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_UiaGridItem_h__ +#define mozilla_a11y_UiaGridItem_h__ + +#include "objbase.h" +#include "uiautomation.h" + +namespace mozilla::a11y { +class TableCellAccessible; + +/** + * IGridItemProvider and ITableItemProvider implementations. + */ +class UiaGridItem : public IGridItemProvider, public ITableItemProvider { + public: + // IGridItemProvider + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_Row( + /* [retval][out] */ __RPC__out int* aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_Column( + /* [retval][out] */ __RPC__out int* aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_RowSpan( + /* [retval][out] */ __RPC__out int* aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_ColumnSpan( + /* [retval][out] */ __RPC__out int* aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_ContainingGrid( + /* [retval][out] */ __RPC__deref_out_opt IRawElementProviderSimple** + aRetVal); + + // ITableItemProvider + virtual HRESULT STDMETHODCALLTYPE GetRowHeaderItems( + /* [retval][out] */ __RPC__deref_out_opt SAFEARRAY** aRetVal); + + virtual HRESULT STDMETHODCALLTYPE GetColumnHeaderItems( + /* [retval][out] */ __RPC__deref_out_opt SAFEARRAY** aRetVal); + + private: + TableCellAccessible* CellAcc(); +}; + +} // namespace mozilla::a11y + +#endif diff --git a/accessible/windows/uia/moz.build b/accessible/windows/uia/moz.build index c52a24d612..2061261494 100644 --- a/accessible/windows/uia/moz.build +++ b/accessible/windows/uia/moz.build @@ -5,6 +5,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. SOURCES += [ + "UiaGrid.cpp", + "UiaGridItem.cpp", "uiaRawElmProvider.cpp", "UiaRoot.cpp", ] @@ -13,6 +15,7 @@ LOCAL_INCLUDES += [ "/accessible/base", "/accessible/generic", "/accessible/html", + "/accessible/windows/ia2", "/accessible/windows/msaa", "/accessible/xpcom", "/accessible/xul", diff --git a/accessible/windows/uia/uiaRawElmProvider.cpp b/accessible/windows/uia/uiaRawElmProvider.cpp index c022e40cef..82726bb9aa 100644 --- a/accessible/windows/uia/uiaRawElmProvider.cpp +++ b/accessible/windows/uia/uiaRawElmProvider.cpp @@ -11,7 +11,10 @@ #include "AccAttributes.h" #include "AccessibleWrap.h" +#include "ApplicationAccessible.h" #include "ARIAMap.h" +#include "ia2AccessibleTable.h" +#include "ia2AccessibleTableCell.h" #include "LocalAccessible-inl.h" #include "mozilla/a11y/RemoteAccessible.h" #include "mozilla/StaticPrefs_accessibility.h" @@ -19,12 +22,25 @@ #include "MsaaRootAccessible.h" #include "nsAccessibilityService.h" #include "nsAccUtils.h" +#include "nsIAccessiblePivot.h" #include "nsTextEquivUtils.h" +#include "Pivot.h" +#include "Relation.h" #include "RootAccessible.h" using namespace mozilla; using namespace mozilla::a11y; +#ifdef __MINGW32__ +// These constants are missing in mingw-w64. This code should be removed once +// we update to a version which includes them. +const long UIA_CustomLandmarkTypeId = 80000; +const long UIA_FormLandmarkTypeId = 80001; +const long UIA_MainLandmarkTypeId = 80002; +const long UIA_NavigationLandmarkTypeId = 80003; +const long UIA_SearchLandmarkTypeId = 80004; +#endif // __MINGW32__ + // Helper functions static ToggleState ToToggleState(uint64_t aState) { @@ -51,6 +67,34 @@ static ExpandCollapseState ToExpandCollapseState(uint64_t aState) { return ExpandCollapseState_LeafNode; } +static bool IsRadio(Accessible* aAcc) { + role r = aAcc->Role(); + return r == roles::RADIOBUTTON || r == roles::RADIO_MENU_ITEM; +} + +// Used to search for a text leaf descendant for the LabeledBy property. +class LabelTextLeafRule : public PivotRule { + public: + virtual uint16_t Match(Accessible* aAcc) override { + if (aAcc->IsTextLeaf()) { + nsAutoString name; + aAcc->Name(name); + if (name.IsEmpty() || name.EqualsLiteral(" ")) { + // An empty or white space text leaf isn't useful as a label. + return nsIAccessibleTraversalRule::FILTER_IGNORE; + } + return nsIAccessibleTraversalRule::FILTER_MATCH; + } + if (!nsTextEquivUtils::HasNameRule(aAcc, eNameFromSubtreeIfReqRule)) { + // Don't descend into things that can't be used as label content; e.g. + // text boxes. + return nsIAccessibleTraversalRule::FILTER_IGNORE | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + return nsIAccessibleTraversalRule::FILTER_IGNORE; + } +}; + //////////////////////////////////////////////////////////////////////////////// // uiaRawElmProvider //////////////////////////////////////////////////////////////////////////////// @@ -84,12 +128,32 @@ void uiaRawElmProvider::RaiseUiaEventForGeckoEvent(Accessible* aAcc, case nsIAccessibleEvent::EVENT_NAME_CHANGE: property = UIA_NamePropertyId; break; + case nsIAccessibleEvent::EVENT_SELECTION: + ::UiaRaiseAutomationEvent(uia, UIA_SelectionItem_ElementSelectedEventId); + return; + case nsIAccessibleEvent::EVENT_SELECTION_ADD: + ::UiaRaiseAutomationEvent( + uia, UIA_SelectionItem_ElementAddedToSelectionEventId); + return; + case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: + ::UiaRaiseAutomationEvent( + uia, UIA_SelectionItem_ElementRemovedFromSelectionEventId); + return; + case nsIAccessibleEvent::EVENT_SELECTION_WITHIN: + ::UiaRaiseAutomationEvent(uia, UIA_Selection_InvalidatedEventId); + return; case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE: property = UIA_ValueValuePropertyId; newVal.vt = VT_BSTR; uia->get_Value(&newVal.bstrVal); gotNewVal = true; break; + case nsIAccessibleEvent::EVENT_VALUE_CHANGE: + property = UIA_RangeValueValuePropertyId; + newVal.vt = VT_R8; + uia->get_Value(&newVal.dblVal); + gotNewVal = true; + break; } if (property && ::UiaClientsAreListening()) { // We can't get the old value. Thankfully, clients don't seem to need it. @@ -117,6 +181,13 @@ void uiaRawElmProvider::RaiseUiaEventForStateChange(Accessible* aAcc, _variant_t newVal; switch (aState) { case states::CHECKED: + if (aEnabled && IsRadio(aAcc)) { + ::UiaRaiseAutomationEvent(uia, + UIA_SelectionItem_ElementSelectedEventId); + return; + } + // For other checkable things, the Toggle pattern is used. + [[fallthrough]]; case states::MIXED: case states::PRESSED: property = UIA_ToggleToggleStatePropertyId; @@ -161,8 +232,14 @@ uiaRawElmProvider::QueryInterface(REFIID aIid, void** aInterface) { *aInterface = static_cast<IExpandCollapseProvider*>(this); } else if (aIid == IID_IInvokeProvider) { *aInterface = static_cast<IInvokeProvider*>(this); + } else if (aIid == IID_IRangeValueProvider) { + *aInterface = static_cast<IRangeValueProvider*>(this); } else if (aIid == IID_IScrollItemProvider) { *aInterface = static_cast<IScrollItemProvider*>(this); + } else if (aIid == IID_ISelectionItemProvider) { + *aInterface = static_cast<ISelectionItemProvider*>(this); + } else if (aIid == IID_ISelectionProvider) { + *aInterface = static_cast<ISelectionProvider*>(this); } else if (aIid == IID_IToggleProvider) { *aInterface = static_cast<IToggleProvider*>(this); } else if (aIid == IID_IValueProvider) { @@ -248,9 +325,7 @@ uiaRawElmProvider::get_ProviderOptions( __RPC__out enum ProviderOptions* aOptions) { if (!aOptions) return E_INVALIDARG; - *aOptions = static_cast<enum ProviderOptions>( - ProviderOptions_ServerSideProvider | ProviderOptions_UseComThreading | - ProviderOptions_HasNativeIAccessible); + *aOptions = kProviderOptions; return S_OK; } @@ -270,21 +345,78 @@ uiaRawElmProvider::GetPatternProvider( expand.forget(aPatternProvider); } return S_OK; + case UIA_GridPatternId: + if (acc->IsTable()) { + auto grid = GetPatternFromDerived<ia2AccessibleTable, IGridProvider>(); + grid.forget(aPatternProvider); + } + return S_OK; + case UIA_GridItemPatternId: + if (acc->IsTableCell()) { + auto item = + GetPatternFromDerived<ia2AccessibleTableCell, IGridItemProvider>(); + item.forget(aPatternProvider); + } + return S_OK; case UIA_InvokePatternId: // Per the UIA documentation, we should only expose the Invoke pattern "if // the same behavior is not exposed through another control pattern // provider". if (acc->ActionCount() > 0 && !HasTogglePattern() && - !HasExpandCollapsePattern()) { + !HasExpandCollapsePattern() && !HasSelectionItemPattern()) { RefPtr<IInvokeProvider> invoke = this; invoke.forget(aPatternProvider); } return S_OK; + case UIA_RangeValuePatternId: + if (acc->HasNumericValue()) { + RefPtr<IValueProvider> value = this; + value.forget(aPatternProvider); + } + return S_OK; case UIA_ScrollItemPatternId: { RefPtr<IScrollItemProvider> scroll = this; scroll.forget(aPatternProvider); return S_OK; } + case UIA_SelectionItemPatternId: + if (HasSelectionItemPattern()) { + RefPtr<ISelectionItemProvider> item = this; + item.forget(aPatternProvider); + } + return S_OK; + case UIA_SelectionPatternId: + // According to the UIA documentation, radio button groups should support + // the Selection pattern. However: + // 1. The Core AAM spec doesn't specify the Selection pattern for + // the radiogroup role. + // 2. HTML radio buttons might not be contained by a dedicated group. + // 3. Chromium exposes the Selection pattern on radio groups, but it + // doesn't expose any selected items, even when there is a checked radio + // child. + // 4. Radio menu items are similar to radio buttons and all the above + // also applies to menus. + // For now, we don't support the Selection pattern for radio groups or + // menus, only for list boxes, tab lists, etc. + if (acc->IsSelect()) { + RefPtr<ISelectionProvider> selection = this; + selection.forget(aPatternProvider); + } + return S_OK; + case UIA_TablePatternId: + if (acc->IsTable()) { + auto table = + GetPatternFromDerived<ia2AccessibleTable, ITableProvider>(); + table.forget(aPatternProvider); + } + return S_OK; + case UIA_TableItemPatternId: + if (acc->IsTableCell()) { + auto item = + GetPatternFromDerived<ia2AccessibleTableCell, ITableItemProvider>(); + item.forget(aPatternProvider); + } + return S_OK; case UIA_TogglePatternId: if (HasTogglePattern()) { RefPtr<IToggleProvider> toggle = this; @@ -349,19 +481,19 @@ uiaRawElmProvider::GetPropertyValue(PROPERTYID aPropertyId, break; } - // ARIA Role / shortcut case UIA_AriaRolePropertyId: { - nsAutoString xmlRoles; - - RefPtr<AccAttributes> attributes = acc->Attributes(); - attributes->GetAttribute(nsGkAtoms::xmlroles, xmlRoles); - - if (!xmlRoles.IsEmpty()) { + nsAutoString role; + if (acc->HasARIARole()) { + RefPtr<AccAttributes> attributes = acc->Attributes(); + attributes->GetAttribute(nsGkAtoms::xmlroles, role); + } else if (nsStaticAtom* computed = acc->ComputedARIARole()) { + computed->ToString(role); + } + if (!role.IsEmpty()) { aPropertyValue->vt = VT_BSTR; - aPropertyValue->bstrVal = ::SysAllocString(xmlRoles.get()); + aPropertyValue->bstrVal = ::SysAllocString(role.get()); return S_OK; } - break; } @@ -415,11 +547,57 @@ uiaRawElmProvider::GetPropertyValue(PROPERTYID aPropertyId, break; } + case UIA_ClassNamePropertyId: { + nsAutoString className; + acc->DOMNodeClass(className); + if (!className.IsEmpty()) { + aPropertyValue->vt = VT_BSTR; + aPropertyValue->bstrVal = ::SysAllocString(className.get()); + return S_OK; + } + break; + } + + case UIA_ControllerForPropertyId: + aPropertyValue->vt = VT_UNKNOWN | VT_ARRAY; + aPropertyValue->parray = AccRelationsToUiaArray( + {RelationType::CONTROLLER_FOR, RelationType::ERRORMSG}); + return S_OK; + case UIA_ControlTypePropertyId: aPropertyValue->vt = VT_I4; aPropertyValue->lVal = GetControlType(); break; + case UIA_DescribedByPropertyId: + aPropertyValue->vt = VT_UNKNOWN | VT_ARRAY; + aPropertyValue->parray = AccRelationsToUiaArray( + {RelationType::DESCRIBED_BY, RelationType::DETAILS}); + return S_OK; + + case UIA_FlowsFromPropertyId: + aPropertyValue->vt = VT_UNKNOWN | VT_ARRAY; + aPropertyValue->parray = + AccRelationsToUiaArray({RelationType::FLOWS_FROM}); + return S_OK; + + case UIA_FlowsToPropertyId: + aPropertyValue->vt = VT_UNKNOWN | VT_ARRAY; + aPropertyValue->parray = AccRelationsToUiaArray({RelationType::FLOWS_TO}); + return S_OK; + + case UIA_FrameworkIdPropertyId: + if (ApplicationAccessible* app = ApplicationAcc()) { + nsAutoString name; + app->PlatformName(name); + if (!name.IsEmpty()) { + aPropertyValue->vt = VT_BSTR; + aPropertyValue->bstrVal = ::SysAllocString(name.get()); + return S_OK; + } + } + break; + case UIA_FullDescriptionPropertyId: { nsAutoString desc; acc->Description(desc); @@ -459,6 +637,39 @@ uiaRawElmProvider::GetPropertyValue(PROPERTYID aPropertyId, (acc->State() & states::FOCUSABLE) ? VARIANT_TRUE : VARIANT_FALSE; return S_OK; + case UIA_LabeledByPropertyId: + if (Accessible* target = GetLabeledBy()) { + aPropertyValue->vt = VT_UNKNOWN; + RefPtr<IRawElementProviderSimple> uia = MsaaAccessible::GetFrom(target); + uia.forget(&aPropertyValue->punkVal); + return S_OK; + } + break; + + case UIA_LandmarkTypePropertyId: + if (long type = GetLandmarkType()) { + aPropertyValue->vt = VT_I4; + aPropertyValue->lVal = type; + return S_OK; + } + break; + + case UIA_LevelPropertyId: + aPropertyValue->vt = VT_I4; + aPropertyValue->lVal = acc->GroupPosition().level; + return S_OK; + + case UIA_LocalizedLandmarkTypePropertyId: { + nsAutoString landmark; + GetLocalizedLandmarkType(landmark); + if (!landmark.IsEmpty()) { + aPropertyValue->vt = VT_BSTR; + aPropertyValue->bstrVal = ::SysAllocString(landmark.get()); + return S_OK; + } + break; + } + case UIA_NamePropertyId: { nsAutoString name; acc->Name(name); @@ -469,6 +680,16 @@ uiaRawElmProvider::GetPropertyValue(PROPERTYID aPropertyId, } break; } + + case UIA_PositionInSetPropertyId: + aPropertyValue->vt = VT_I4; + aPropertyValue->lVal = acc->GroupPosition().posInSet; + return S_OK; + + case UIA_SizeOfSetPropertyId: + aPropertyValue->vt = VT_I4; + aPropertyValue->lVal = acc->GroupPosition().setSize; + return S_OK; } return S_OK; @@ -749,6 +970,223 @@ uiaRawElmProvider::get_IsReadOnly(__RPC__out BOOL* aRetVal) { return S_OK; } +// IRangeValueProvider methods + +STDMETHODIMP +uiaRawElmProvider::SetValue(double aVal) { + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + if (!acc->SetCurValue(aVal)) { + return UIA_E_INVALIDOPERATION; + } + return S_OK; +} + +STDMETHODIMP +uiaRawElmProvider::get_Value(__RPC__out double* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = acc->CurValue(); + return S_OK; +} + +STDMETHODIMP +uiaRawElmProvider::get_Maximum(__RPC__out double* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = acc->MaxValue(); + return S_OK; +} + +STDMETHODIMP +uiaRawElmProvider::get_Minimum( + /* [retval][out] */ __RPC__out double* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = acc->MinValue(); + return S_OK; +} + +STDMETHODIMP +uiaRawElmProvider::get_LargeChange( + /* [retval][out] */ __RPC__out double* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + // We want the change that would occur if the user pressed page up or page + // down. For HTML input elements, this is 10% of the total range unless step + // is larger. See: + // https://searchfox.org/mozilla-central/rev/c7df16ffad1f12a19c81c16bce0b65e4a15304d0/dom/html/HTMLInputElement.cpp#3878 + double step = acc->Step(); + double min = acc->MinValue(); + double max = acc->MaxValue(); + if (std::isnan(step) || std::isnan(min) || std::isnan(max)) { + *aRetVal = UnspecifiedNaN<double>(); + } else { + *aRetVal = std::max(step, (max - min) / 10); + } + return S_OK; +} + +STDMETHODIMP +uiaRawElmProvider::get_SmallChange( + /* [retval][out] */ __RPC__out double* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = acc->Step(); + return S_OK; +} + +// ISelectionProvider methods + +STDMETHODIMP +uiaRawElmProvider::GetSelection(__RPC__deref_out_opt SAFEARRAY** aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + *aRetVal = nullptr; + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + AutoTArray<Accessible*, 10> items; + acc->SelectedItems(&items); + *aRetVal = AccessibleArrayToUiaArray(items); + return S_OK; +} + +STDMETHODIMP +uiaRawElmProvider::get_CanSelectMultiple(__RPC__out BOOL* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = acc->State() & states::MULTISELECTABLE; + return S_OK; +} + +STDMETHODIMP +uiaRawElmProvider::get_IsSelectionRequired(__RPC__out BOOL* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + *aRetVal = acc->State() & states::REQUIRED; + return S_OK; +} + +// ISelectionItemProvider methods + +STDMETHODIMP +uiaRawElmProvider::Select() { + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + if (IsRadio(acc)) { + acc->DoAction(0); + } else { + acc->TakeSelection(); + } + return S_OK; +} + +STDMETHODIMP +uiaRawElmProvider::AddToSelection() { + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + if (IsRadio(acc)) { + acc->DoAction(0); + } else { + acc->SetSelected(true); + } + return S_OK; +} + +STDMETHODIMP +uiaRawElmProvider::RemoveFromSelection() { + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + if (IsRadio(acc)) { + return UIA_E_INVALIDOPERATION; + } + acc->SetSelected(false); + return S_OK; +} + +STDMETHODIMP +uiaRawElmProvider::get_IsSelected(__RPC__out BOOL* aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + if (IsRadio(acc)) { + *aRetVal = acc->State() & states::CHECKED; + } else { + *aRetVal = acc->State() & states::SELECTED; + } + return S_OK; +} + +STDMETHODIMP +uiaRawElmProvider::get_SelectionContainer( + __RPC__deref_out_opt IRawElementProviderSimple** aRetVal) { + if (!aRetVal) { + return E_INVALIDARG; + } + *aRetVal = nullptr; + Accessible* acc = Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + Accessible* container = nsAccUtils::GetSelectableContainer(acc, acc->State()); + if (!container) { + return E_FAIL; + } + RefPtr<IRawElementProviderSimple> uia = MsaaAccessible::GetFrom(container); + uia.forget(aRetVal); + return S_OK; +} + // Private methods bool uiaRawElmProvider::IsControl() { @@ -854,3 +1292,133 @@ bool uiaRawElmProvider::HasValuePattern() const { const nsRoleMapEntry* roleMapEntry = acc->ARIARoleMap(); return roleMapEntry && roleMapEntry->Is(nsGkAtoms::textbox); } + +template <class Derived, class Interface> +RefPtr<Interface> uiaRawElmProvider::GetPatternFromDerived() { + // MsaaAccessible inherits from uiaRawElmProvider. Derived + // inherits from MsaaAccessible and Interface. The compiler won't let us + // directly static_cast to Interface, hence the intermediate casts. + auto* msaa = static_cast<MsaaAccessible*>(this); + auto* derived = static_cast<Derived*>(msaa); + return derived; +} + +bool uiaRawElmProvider::HasSelectionItemPattern() { + Accessible* acc = Acc(); + MOZ_ASSERT(acc); + // In UIA, radio buttons and radio menu items are exposed as selected or + // unselected. + return acc->State() & states::SELECTABLE || IsRadio(acc); +} + +SAFEARRAY* uiaRawElmProvider::AccRelationsToUiaArray( + std::initializer_list<RelationType> aTypes) const { + Accessible* acc = Acc(); + MOZ_ASSERT(acc); + AutoTArray<Accessible*, 10> targets; + for (RelationType type : aTypes) { + Relation rel = acc->RelationByType(type); + while (Accessible* target = rel.Next()) { + targets.AppendElement(target); + } + } + return AccessibleArrayToUiaArray(targets); +} + +Accessible* uiaRawElmProvider::GetLabeledBy() const { + // Per the UIA documentation, some control types should never get a value for + // the LabeledBy property. + switch (GetControlType()) { + case UIA_ButtonControlTypeId: + case UIA_CheckBoxControlTypeId: + case UIA_DataItemControlTypeId: + case UIA_MenuControlTypeId: + case UIA_MenuBarControlTypeId: + case UIA_RadioButtonControlTypeId: + case UIA_ScrollBarControlTypeId: + case UIA_SeparatorControlTypeId: + case UIA_StatusBarControlTypeId: + case UIA_TabItemControlTypeId: + case UIA_TextControlTypeId: + case UIA_ToolBarControlTypeId: + case UIA_ToolTipControlTypeId: + case UIA_TreeItemControlTypeId: + return nullptr; + } + + Accessible* acc = Acc(); + MOZ_ASSERT(acc); + // Even when LabeledBy is supported, it can only return a single "static text" + // element. + Relation rel = acc->RelationByType(RelationType::LABELLED_BY); + LabelTextLeafRule rule; + while (Accessible* target = rel.Next()) { + // If target were a text leaf, we should return that, but that shouldn't be + // possible because only an element (not a text node) can be the target of a + // relation. + MOZ_ASSERT(!target->IsTextLeaf()); + Pivot pivot(target); + if (Accessible* leaf = pivot.Next(target, rule)) { + return leaf; + } + } + return nullptr; +} + +long uiaRawElmProvider::GetLandmarkType() const { + Accessible* acc = Acc(); + MOZ_ASSERT(acc); + nsStaticAtom* landmark = acc->LandmarkRole(); + if (!landmark) { + return 0; + } + if (landmark == nsGkAtoms::form) { + return UIA_FormLandmarkTypeId; + } + if (landmark == nsGkAtoms::main) { + return UIA_MainLandmarkTypeId; + } + if (landmark == nsGkAtoms::navigation) { + return UIA_NavigationLandmarkTypeId; + } + if (landmark == nsGkAtoms::search) { + return UIA_SearchLandmarkTypeId; + } + return UIA_CustomLandmarkTypeId; +} + +void uiaRawElmProvider::GetLocalizedLandmarkType(nsAString& aLocalized) const { + Accessible* acc = Acc(); + MOZ_ASSERT(acc); + nsStaticAtom* landmark = acc->LandmarkRole(); + // The system provides strings for landmarks explicitly supported by the UIA + // LandmarkType property; i.e. form, main, navigation and search. We must + // provide strings for landmarks considered custom by UIA. For now, we only + // support landmarks in the core ARIA specification, not other ARIA modules + // such as DPub. + if (landmark == nsGkAtoms::banner || landmark == nsGkAtoms::complementary || + landmark == nsGkAtoms::contentinfo || landmark == nsGkAtoms::region) { + nsAutoString unlocalized; + landmark->ToString(unlocalized); + Accessible::TranslateString(unlocalized, aLocalized); + } +} + +SAFEARRAY* a11y::AccessibleArrayToUiaArray(const nsTArray<Accessible*>& aAccs) { + if (aAccs.IsEmpty()) { + // The UIA documentation is unclear about this, but the UIA client + // framework seems to treat a null value the same as an empty array. This + // is also what Chromium does. + return nullptr; + } + SAFEARRAY* uias = SafeArrayCreateVector(VT_UNKNOWN, 0, aAccs.Length()); + LONG indices[1] = {0}; + for (Accessible* acc : aAccs) { + // SafeArrayPutElement calls AddRef on the element, so we use a raw pointer + // here. + IRawElementProviderSimple* uia = MsaaAccessible::GetFrom(acc); + SafeArrayPutElement(uias, indices, uia); + ++indices[0]; + } + return uias; +} diff --git a/accessible/windows/uia/uiaRawElmProvider.h b/accessible/windows/uia/uiaRawElmProvider.h index 0e05d1a030..ea713d8bc1 100644 --- a/accessible/windows/uia/uiaRawElmProvider.h +++ b/accessible/windows/uia/uiaRawElmProvider.h @@ -11,10 +11,20 @@ #include <stdint.h> #include <uiautomation.h> +#include <initializer_list> + +#include "nsString.h" + +template <class T> +class nsTArray; +template <class T> +class RefPtr; + namespace mozilla { namespace a11y { class Accessible; +enum class RelationType; /** * IRawElementProviderSimple implementation (maintains IAccessibleEx approach). @@ -26,8 +36,16 @@ class uiaRawElmProvider : public IAccessibleEx, public IToggleProvider, public IExpandCollapseProvider, public IScrollItemProvider, - public IValueProvider { + public IValueProvider, + public IRangeValueProvider, + public ISelectionProvider, + public ISelectionItemProvider { public: + static constexpr enum ProviderOptions kProviderOptions = + static_cast<enum ProviderOptions>(ProviderOptions_ServerSideProvider | + ProviderOptions_UseComThreading | + ProviderOptions_HasNativeIAccessible); + static void RaiseUiaEventForGeckoEvent(Accessible* aAcc, uint32_t aGeckoEvent); static void RaiseUiaEventForStateChange(Accessible* aAcc, uint64_t aState, @@ -118,6 +136,51 @@ class uiaRawElmProvider : public IAccessibleEx, virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_IsReadOnly( /* [retval][out] */ __RPC__out BOOL* pRetVal); + // IRangeValueProvider + virtual HRESULT STDMETHODCALLTYPE SetValue( + /* [in] */ double aVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_Value( + /* [retval][out] */ __RPC__out double* aRetVal); + + // get_IsReadOnly is shared with IValueProvider. + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_Maximum( + /* [retval][out] */ __RPC__out double* aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_Minimum( + /* [retval][out] */ __RPC__out double* aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_LargeChange( + /* [retval][out] */ __RPC__out double* aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_SmallChange( + /* [retval][out] */ __RPC__out double* aRetVal); + + // ISelectionProvider + virtual HRESULT STDMETHODCALLTYPE GetSelection( + /* [retval][out] */ __RPC__deref_out_opt SAFEARRAY** aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_CanSelectMultiple( + /* [retval][out] */ __RPC__out BOOL* aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_IsSelectionRequired( + /* [retval][out] */ __RPC__out BOOL* aRetVal); + + // ISelectionItemProvider methods + virtual HRESULT STDMETHODCALLTYPE Select(void); + + virtual HRESULT STDMETHODCALLTYPE AddToSelection(void); + + virtual HRESULT STDMETHODCALLTYPE RemoveFromSelection(void); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_IsSelected( + /* [retval][out] */ __RPC__out BOOL* aRetVal); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_SelectionContainer( + /* [retval][out] */ __RPC__deref_out_opt IRawElementProviderSimple** + aRetVal); + private: Accessible* Acc() const; bool IsControl(); @@ -125,8 +188,18 @@ class uiaRawElmProvider : public IAccessibleEx, bool HasTogglePattern(); bool HasExpandCollapsePattern(); bool HasValuePattern() const; + template <class Derived, class Interface> + RefPtr<Interface> GetPatternFromDerived(); + bool HasSelectionItemPattern(); + SAFEARRAY* AccRelationsToUiaArray( + std::initializer_list<RelationType> aTypes) const; + Accessible* GetLabeledBy() const; + long GetLandmarkType() const; + void GetLocalizedLandmarkType(nsAString& aLocalized) const; }; +SAFEARRAY* AccessibleArrayToUiaArray(const nsTArray<Accessible*>& aAccs); + } // namespace a11y } // namespace mozilla diff --git a/accessible/xpcom/xpcAccessibilityService.cpp b/accessible/xpcom/xpcAccessibilityService.cpp index a7b70b6f33..f6692bffbb 100644 --- a/accessible/xpcom/xpcAccessibilityService.cpp +++ b/accessible/xpcom/xpcAccessibilityService.cpp @@ -123,7 +123,8 @@ xpcAccessibilityService::GetAccessibleFor(nsINode* aNode, DocAccessible* document = accService->GetDocAccessible(aNode->OwnerDoc()); if (document) { - NS_IF_ADDREF(*aAccessible = ToXPC(document->GetAccessible(aNode))); + NS_IF_ADDREF(*aAccessible = + ToXPC(document->GetAccessibleEvenIfNotInMap(aNode))); } return NS_OK; |