1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:expandtab:shiftwidth=2:tabstop=2:
4 /* This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
10 #include "nsAccUtils.h"
11 #include "nsCoreUtils.h"
15 #include "nsAttrName.h"
16 #include "nsWhitespaceTokenizer.h"
18 #include "mozilla/BinarySearch.h"
19 #include "mozilla/dom/Element.h"
21 #include "nsUnicharUtils.h"
23 using namespace mozilla
;
24 using namespace mozilla::a11y
;
25 using namespace mozilla::a11y::aria
;
27 static const uint32_t kGenericAccType
= 0;
30 * This list of WAI-defined roles are currently hardcoded.
31 * Eventually we will most likely be loading an RDF resource that contains this
32 * information Using RDF will also allow for role extensibility. See bug 280138.
34 * Definition of nsRoleMapEntry contains comments explaining this table.
36 * When no Role enum mapping exists for an ARIA role, the role will be exposed
37 * via the object attribute "xml-roles".
40 static const nsRoleMapEntry sWAIRoleMaps
[] = {
48 #if defined(XP_MACOSX)
57 nsGkAtoms::alertdialog
,
67 nsGkAtoms::application
,
85 eReadonlyUntilEditable
98 nsGkAtoms::blockquote
,
115 // eARIAPressed is auto applied on any button
158 nsGkAtoms::columnheader
,
166 eARIASelectableIfDefined
,
169 { // combobox, which consists of text input and popup
177 states::COLLAPSED
| states::HASPOPUP
,
192 nsGkAtoms::complementary
,
202 nsGkAtoms::contentinfo
,
213 roles::CONTENT_DELETION
,
231 nsGkAtoms::directory
,
241 nsGkAtoms::docAbstract
,
250 { // doc-acknowledgments
251 nsGkAtoms::docAcknowledgments
,
261 nsGkAtoms::docAfterword
,
271 nsGkAtoms::docAppendix
,
281 nsGkAtoms::docBacklink
,
291 nsGkAtoms::docBiblioentry
,
300 { // doc-bibliography
301 nsGkAtoms::docBibliography
,
311 nsGkAtoms::docBiblioref
,
321 nsGkAtoms::docChapter
,
331 nsGkAtoms::docColophon
,
341 nsGkAtoms::docConclusion
,
361 nsGkAtoms::docCredit
,
371 nsGkAtoms::docCredits
,
381 nsGkAtoms::docDedication
,
391 nsGkAtoms::docEndnote
,
401 nsGkAtoms::docEndnotes
,
411 nsGkAtoms::docEpigraph
,
421 nsGkAtoms::docEpilogue
,
431 nsGkAtoms::docErrata
,
441 nsGkAtoms::docExample
,
451 nsGkAtoms::docFootnote
,
461 nsGkAtoms::docForeword
,
471 nsGkAtoms::docGlossary
,
481 nsGkAtoms::docGlossref
,
500 { // doc-introduction
501 nsGkAtoms::docIntroduction
,
511 nsGkAtoms::docNoteref
,
521 nsGkAtoms::docNotice
,
531 nsGkAtoms::docPagebreak
,
541 nsGkAtoms::docPagelist
,
561 nsGkAtoms::docPreface
,
571 nsGkAtoms::docPrologue
,
581 nsGkAtoms::docPullquote
,
601 nsGkAtoms::docSubtitle
,
632 roles::NON_NATIVE_DOCUMENT
,
639 eReadonlyUntilEditable
671 { // graphics-document
672 nsGkAtoms::graphicsDocument
,
673 roles::NON_NATIVE_DOCUMENT
,
680 eReadonlyUntilEditable
683 nsGkAtoms::graphicsObject
,
693 nsGkAtoms::graphicsSymbol
,
711 eARIAMultiSelectable
,
713 eFocusableUntilDisabled
758 nsGkAtoms::insertion
,
759 roles::CONTENT_INSERTION
,
804 eListControl
| eSelect
,
806 eARIAMultiSelectable
,
808 eFocusableUntilDisabled
,
816 eNoAction
, // XXX: should depend on state, parent accessible
862 roles::FLAT_EQUATION
,
875 eNoAction
, // XXX: technically accessibles of menupopup role haven't
876 // any action, but menu can be open or close.
903 { // menuitemcheckbox
904 nsGkAtoms::menuitemcheckbox
,
905 roles::CHECK_MENU_ITEM
,
916 nsGkAtoms::menuitemradio
,
917 roles::RADIO_MENU_ITEM
,
938 nsGkAtoms::navigation
,
980 nsGkAtoms::paragraph
,
989 nsGkAtoms::presentation
,
999 nsGkAtoms::progressbar
,
1007 eIndeterminateIfNoValue
1021 nsGkAtoms::radiogroup
,
1054 nsGkAtoms::rowgroup
,
1064 nsGkAtoms::rowheader
,
1072 eARIASelectableIfDefined
,
1076 nsGkAtoms::scrollbar
,
1098 nsGkAtoms::searchbox
,
1108 eARIAReadonlyOrEditable
1111 nsGkAtoms::separator_
,
1114 eHasValueMinMaxIfFocusable
,
1134 nsGkAtoms::spinbutton
,
1155 nsGkAtoms::suggestion
,
1164 nsGkAtoms::svgSwitch
,
1168 eCheckUncheckAction
,
1207 eARIAMultiSelectable
1210 nsGkAtoms::tabpanel
,
1211 roles::PROPERTYPAGE
,
1240 eARIAReadonlyOrEditable
1282 eARIAMultiSelectable
,
1283 eFocusableUntilDisabled
,
1287 nsGkAtoms::treegrid
,
1296 eARIAMultiSelectable
,
1297 eFocusableUntilDisabled
,
1301 nsGkAtoms::treeitem
,
1305 eActivateAction
, // XXX: should expose second 'expand/collapse' action based
1316 static const nsRoleMapEntry sLandmarkRoleMap
= {
1317 nsGkAtoms::_empty
, roles::NOTHING
, kUseNativeRole
, eNoValue
,
1318 eNoAction
, eNoLiveAttr
, kGenericAccType
, kNoReqStates
};
1320 nsRoleMapEntry
aria::gEmptyRoleMap
= {
1321 nsGkAtoms::_empty
, roles::TEXT_CONTAINER
, kUseMapRole
, eNoValue
,
1322 eNoAction
, eNoLiveAttr
, kGenericAccType
, kNoReqStates
};
1325 * Universal (Global) states:
1326 * The following state rules are applied to any accessible element,
1327 * whether there is an ARIA role or not:
1329 static const EStateRule sWAIUnivStateMap
[] = {
1330 eARIABusy
, eARIACurrent
, eARIADisabled
,
1331 eARIAExpanded
, // Currently under spec review but precedent exists
1332 eARIAHasPopup
, // Note this is a tokenised attribute starting in ARIA 1.1
1333 eARIAInvalid
, eARIAModal
,
1334 eARIARequired
, // XXX not global, Bug 553117
1338 * ARIA attribute map for attribute characteristics.
1339 * @note ARIA attributes that don't have any flags are not included here.
1342 struct AttrCharacteristics
{
1343 const nsStaticAtom
* const attributeName
;
1344 const uint8_t characteristics
;
1347 static const AttrCharacteristics gWAIUnivAttrMap
[] = {
1349 {nsGkAtoms::aria_activedescendant
, ATTR_BYPASSOBJ
},
1350 {nsGkAtoms::aria_atomic
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1351 {nsGkAtoms::aria_busy
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1352 {nsGkAtoms::aria_checked
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
}, /* exposes checkable obj attr */
1353 {nsGkAtoms::aria_controls
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1354 {nsGkAtoms::aria_current
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1355 {nsGkAtoms::aria_describedby
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1356 // XXX Ideally, aria-description shouldn't expose a description object
1357 // attribute (i.e. it should have ATTR_BYPASSOBJ). However, until the
1358 // description-from attribute is implemented (bug 1726087), clients such as
1359 // NVDA depend on the description object attribute to work out whether the
1360 // accDescription originated from aria-description.
1361 {nsGkAtoms::aria_description
, ATTR_GLOBAL
},
1362 {nsGkAtoms::aria_details
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1363 {nsGkAtoms::aria_disabled
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1364 {nsGkAtoms::aria_dropeffect
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1365 {nsGkAtoms::aria_errormessage
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1366 {nsGkAtoms::aria_expanded
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1367 {nsGkAtoms::aria_flowto
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1368 {nsGkAtoms::aria_grabbed
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1369 {nsGkAtoms::aria_haspopup
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1370 {nsGkAtoms::aria_hidden
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
}, /* handled special way */
1371 {nsGkAtoms::aria_invalid
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1372 {nsGkAtoms::aria_label
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1373 {nsGkAtoms::aria_labelledby
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1374 {nsGkAtoms::aria_level
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1375 {nsGkAtoms::aria_live
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1376 {nsGkAtoms::aria_modal
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1377 {nsGkAtoms::aria_multiline
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1378 {nsGkAtoms::aria_multiselectable
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1379 {nsGkAtoms::aria_owns
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1380 {nsGkAtoms::aria_orientation
, ATTR_VALTOKEN
},
1381 {nsGkAtoms::aria_posinset
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1382 {nsGkAtoms::aria_pressed
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1383 {nsGkAtoms::aria_readonly
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1384 {nsGkAtoms::aria_relevant
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1385 {nsGkAtoms::aria_required
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1386 {nsGkAtoms::aria_selected
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1387 {nsGkAtoms::aria_setsize
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1388 {nsGkAtoms::aria_sort
, ATTR_VALTOKEN
},
1389 {nsGkAtoms::aria_valuenow
, ATTR_BYPASSOBJ
},
1390 {nsGkAtoms::aria_valuemin
, ATTR_BYPASSOBJ
},
1391 {nsGkAtoms::aria_valuemax
, ATTR_BYPASSOBJ
},
1392 {nsGkAtoms::aria_valuetext
, ATTR_BYPASSOBJ
}
1396 const nsRoleMapEntry
* aria::GetRoleMap(dom::Element
* aEl
) {
1397 return GetRoleMapFromIndex(GetRoleMapIndex(aEl
));
1400 uint8_t aria::GetRoleMapIndex(dom::Element
* aEl
) {
1402 if (!aEl
|| !aEl
->GetAttr(kNameSpaceID_None
, nsGkAtoms::role
, roles
) ||
1404 // We treat role="" as if the role attribute is absent (per aria spec:8.1.1)
1405 return NO_ROLE_MAP_ENTRY_INDEX
;
1408 nsWhitespaceTokenizer
tokenizer(roles
);
1409 while (tokenizer
.hasMoreTokens()) {
1410 // Do a binary search through table for the next role in role list
1411 const nsDependentSubstring role
= tokenizer
.nextToken();
1413 auto comparator
= [&role
](const nsRoleMapEntry
& aEntry
) {
1414 return Compare(role
, aEntry
.ARIARoleString(),
1415 nsCaseInsensitiveStringComparator
);
1417 if (BinarySearchIf(sWAIRoleMaps
, 0, ArrayLength(sWAIRoleMaps
), comparator
,
1423 // Always use some entry index if there is a non-empty role string
1424 // To ensure an accessible object is created
1425 return LANDMARK_ROLE_MAP_ENTRY_INDEX
;
1428 const nsRoleMapEntry
* aria::GetRoleMapFromIndex(uint8_t aRoleMapIndex
) {
1429 switch (aRoleMapIndex
) {
1430 case NO_ROLE_MAP_ENTRY_INDEX
:
1432 case EMPTY_ROLE_MAP_ENTRY_INDEX
:
1433 return &gEmptyRoleMap
;
1434 case LANDMARK_ROLE_MAP_ENTRY_INDEX
:
1435 return &sLandmarkRoleMap
;
1437 return sWAIRoleMaps
+ aRoleMapIndex
;
1441 uint8_t aria::GetIndexFromRoleMap(const nsRoleMapEntry
* aRoleMapEntry
) {
1442 if (aRoleMapEntry
== nullptr) {
1443 return NO_ROLE_MAP_ENTRY_INDEX
;
1444 } else if (aRoleMapEntry
== &gEmptyRoleMap
) {
1445 return EMPTY_ROLE_MAP_ENTRY_INDEX
;
1446 } else if (aRoleMapEntry
== &sLandmarkRoleMap
) {
1447 return LANDMARK_ROLE_MAP_ENTRY_INDEX
;
1449 return aRoleMapEntry
- sWAIRoleMaps
;
1453 uint64_t aria::UniversalStatesFor(mozilla::dom::Element
* aElement
) {
1456 while (MapToState(sWAIUnivStateMap
[index
], aElement
, &state
)) index
++;
1461 uint8_t aria::AttrCharacteristicsFor(nsAtom
* aAtom
) {
1462 for (uint32_t i
= 0; i
< ArrayLength(gWAIUnivAttrMap
); i
++) {
1463 if (gWAIUnivAttrMap
[i
].attributeName
== aAtom
) {
1464 return gWAIUnivAttrMap
[i
].characteristics
;
1471 bool aria::HasDefinedARIAHidden(nsIContent
* aContent
) {
1472 return aContent
&& aContent
->IsElement() &&
1473 aContent
->AsElement()->AttrValueIs(kNameSpaceID_None
,
1474 nsGkAtoms::aria_hidden
,
1475 nsGkAtoms::_true
, eCaseMatters
);
1478 ////////////////////////////////////////////////////////////////////////////////
1479 // AttrIterator class
1481 AttrIterator::AttrIterator(nsIContent
* aContent
)
1482 : mElement(dom::Element::FromNode(aContent
)), mAttrIdx(0) {
1483 mAttrCount
= mElement
? mElement
->GetAttrCount() : 0;
1486 bool AttrIterator::Next() {
1487 while (mAttrIdx
< mAttrCount
) {
1488 const nsAttrName
* attr
= mElement
->GetAttrNameAt(mAttrIdx
);
1490 if (attr
->NamespaceEquals(kNameSpaceID_None
)) {
1491 mAttrAtom
= attr
->Atom();
1492 nsDependentAtomString
attrStr(mAttrAtom
);
1493 if (!StringBeginsWith(attrStr
, u
"aria-"_ns
)) continue; // Not ARIA
1495 uint8_t attrFlags
= aria::AttrCharacteristicsFor(mAttrAtom
);
1496 if (attrFlags
& ATTR_BYPASSOBJ
) {
1497 continue; // No need to handle exposing as obj attribute here
1500 if ((attrFlags
& ATTR_VALTOKEN
) &&
1501 !nsAccUtils::HasDefinedARIAToken(mElement
, mAttrAtom
)) {
1502 continue; // only expose token based attributes if they are defined
1505 if ((attrFlags
& ATTR_BYPASSOBJ_IF_FALSE
) &&
1506 mElement
->AttrValueIs(kNameSpaceID_None
, mAttrAtom
, nsGkAtoms::_false
,
1508 continue; // only expose token based attribute if value is not 'false'.
1515 mAttrAtom
= nullptr;
1520 void AttrIterator::AttrName(nsAString
& aAttrName
) const {
1521 nsDependentAtomString
attrStr(mAttrAtom
);
1522 MOZ_ASSERT(StringBeginsWith(attrStr
, u
"aria-"_ns
),
1523 "Stored atom is an aria attribute.");
1524 aAttrName
.Assign(Substring(attrStr
, 5));
1527 nsAtom
* AttrIterator::AttrName() const { return mAttrAtom
; }
1529 void AttrIterator::AttrValue(nsAString
& aAttrValue
) const {
1531 if (mElement
->GetAttr(kNameSpaceID_None
, mAttrAtom
, value
)) {
1532 if (aria::AttrCharacteristicsFor(mAttrAtom
) & ATTR_VALTOKEN
) {
1533 nsAtom
* normalizedValue
=
1534 nsAccUtils::NormalizeARIAToken(mElement
, mAttrAtom
);
1535 if (normalizedValue
) {
1536 nsDependentAtomString
normalizedValueStr(normalizedValue
);
1537 aAttrValue
.Assign(normalizedValueStr
);
1541 aAttrValue
.Assign(value
);