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 "AccAttributes.h"
11 #include "nsAccUtils.h"
12 #include "nsCoreUtils.h"
13 #include "mozilla/a11y/Role.h"
16 #include "nsAttrName.h"
17 #include "nsWhitespaceTokenizer.h"
19 #include "mozilla/BinarySearch.h"
20 #include "mozilla/dom/Element.h"
22 #include "nsUnicharUtils.h"
24 using namespace mozilla
;
25 using namespace mozilla::a11y
;
26 using namespace mozilla::a11y::aria
;
28 static const uint32_t kGenericAccType
= 0;
31 * This list of WAI-defined roles are currently hardcoded.
32 * Eventually we will most likely be loading an RDF resource that contains this
33 * information Using RDF will also allow for role extensibility. See bug 280138.
35 * Definition of nsRoleMapEntry contains comments explaining this table.
37 * When no Role enum mapping exists for an ARIA role, the role will be exposed
38 * via the object attribute "xml-roles".
40 * Note: the list must remain alphabetically ordered to support binary search.
43 static const nsRoleMapEntry sWAIRoleMaps
[] = {
51 #if defined(XP_MACOSX)
60 nsGkAtoms::alertdialog
,
70 nsGkAtoms::application
,
88 eReadonlyUntilEditable
101 nsGkAtoms::blockquote
,
118 // eARIAPressed is auto applied on any button
161 nsGkAtoms::columnheader
,
169 eARIASelectableIfDefined
,
172 { // combobox, which consists of text input and popup
180 states::COLLAPSED
| states::HASPOPUP
,
195 nsGkAtoms::complementary
,
205 nsGkAtoms::contentinfo
,
215 nsGkAtoms::definition
,
225 roles::CONTENT_DELETION
,
243 nsGkAtoms::directory
,
253 nsGkAtoms::docAbstract
,
262 { // doc-acknowledgments
263 nsGkAtoms::docAcknowledgments
,
273 nsGkAtoms::docAfterword
,
283 nsGkAtoms::docAppendix
,
293 nsGkAtoms::docBacklink
,
303 nsGkAtoms::docBiblioentry
,
312 { // doc-bibliography
313 nsGkAtoms::docBibliography
,
323 nsGkAtoms::docBiblioref
,
333 nsGkAtoms::docChapter
,
343 nsGkAtoms::docColophon
,
353 nsGkAtoms::docConclusion
,
373 nsGkAtoms::docCredit
,
383 nsGkAtoms::docCredits
,
393 nsGkAtoms::docDedication
,
403 nsGkAtoms::docEndnote
,
413 nsGkAtoms::docEndnotes
,
423 nsGkAtoms::docEpigraph
,
433 nsGkAtoms::docEpilogue
,
443 nsGkAtoms::docErrata
,
453 nsGkAtoms::docExample
,
463 nsGkAtoms::docFootnote
,
473 nsGkAtoms::docForeword
,
483 nsGkAtoms::docGlossary
,
493 nsGkAtoms::docGlossref
,
512 { // doc-introduction
513 nsGkAtoms::docIntroduction
,
523 nsGkAtoms::docNoteref
,
533 nsGkAtoms::docNotice
,
543 nsGkAtoms::docPagebreak
,
553 nsGkAtoms::docPagelist
,
573 nsGkAtoms::docPreface
,
583 nsGkAtoms::docPrologue
,
593 nsGkAtoms::docPullquote
,
613 nsGkAtoms::docSubtitle
,
644 roles::NON_NATIVE_DOCUMENT
,
651 eReadonlyUntilEditable
703 { // graphics-document
704 nsGkAtoms::graphicsDocument
,
705 roles::NON_NATIVE_DOCUMENT
,
712 eReadonlyUntilEditable
715 nsGkAtoms::graphicsObject
,
725 nsGkAtoms::graphicsSymbol
,
743 eARIAMultiSelectable
,
745 eFocusableUntilDisabled
800 nsGkAtoms::insertion
,
801 roles::CONTENT_INSERTION
,
846 eListControl
| eSelect
,
848 eARIAMultiSelectable
,
850 eFocusableUntilDisabled
,
858 eNoAction
, // XXX: should depend on state, parent accessible
904 roles::FLAT_EQUATION
,
917 eNoAction
, // XXX: technically accessibles of menupopup role haven't
918 // any action, but menu can be open or close.
945 { // menuitemcheckbox
946 nsGkAtoms::menuitemcheckbox
,
947 roles::CHECK_MENU_ITEM
,
958 nsGkAtoms::menuitemradio
,
959 roles::RADIO_MENU_ITEM
,
980 nsGkAtoms::navigation
,
1022 nsGkAtoms::paragraph
,
1031 nsGkAtoms::presentation
,
1041 nsGkAtoms::progressbar
,
1049 eIndeterminateIfNoValue
1063 nsGkAtoms::radiogroup
,
1096 nsGkAtoms::rowgroup
,
1106 nsGkAtoms::rowheader
,
1114 eARIASelectableIfDefined
,
1118 nsGkAtoms::scrollbar
,
1140 nsGkAtoms::searchbox
,
1150 eARIAReadonlyOrEditable
1153 nsGkAtoms::separator_
,
1156 eHasValueMinMaxIfFocusable
,
1176 nsGkAtoms::spinbutton
,
1207 nsGkAtoms::subscript
,
1216 nsGkAtoms::suggestion
,
1225 nsGkAtoms::superscript
,
1234 nsGkAtoms::svgSwitch
,
1238 eCheckUncheckAction
,
1277 eARIAMultiSelectable
1280 nsGkAtoms::tabpanel
,
1281 roles::PROPERTYPAGE
,
1310 eARIAReadonlyOrEditable
1361 eARIAMultiSelectable
,
1362 eFocusableUntilDisabled
,
1366 nsGkAtoms::treegrid
,
1375 eARIAMultiSelectable
,
1376 eFocusableUntilDisabled
,
1380 nsGkAtoms::treeitem
,
1384 eActivateAction
, // XXX: should expose second 'expand/collapse' action based
1395 static const nsRoleMapEntry sLandmarkRoleMap
= {
1396 nsGkAtoms::_empty
, roles::NOTHING
, kUseNativeRole
, eNoValue
,
1397 eNoAction
, eNoLiveAttr
, kGenericAccType
, kNoReqStates
};
1399 nsRoleMapEntry
aria::gEmptyRoleMap
= {
1400 nsGkAtoms::_empty
, roles::TEXT_CONTAINER
, kUseMapRole
, eNoValue
,
1401 eNoAction
, eNoLiveAttr
, kGenericAccType
, kNoReqStates
};
1404 * Universal (Global) states:
1405 * The following state rules are applied to any accessible element,
1406 * whether there is an ARIA role or not:
1408 static const EStateRule sWAIUnivStateMap
[] = {
1409 eARIABusy
, eARIACurrent
, eARIADisabled
,
1410 eARIAExpanded
, // Currently under spec review but precedent exists
1411 eARIAHasPopup
, // Note this is a tokenised attribute starting in ARIA 1.1
1412 eARIAInvalid
, eARIAModal
,
1413 eARIARequired
, // XXX not global, Bug 553117
1417 * ARIA attribute map for attribute characteristics.
1418 * @note ARIA attributes that don't have any flags are not included here.
1421 struct AttrCharacteristics
{
1422 const nsStaticAtom
* const attributeName
;
1423 const uint8_t characteristics
;
1426 static const AttrCharacteristics gWAIUnivAttrMap
[] = {
1428 {nsGkAtoms::aria_activedescendant
, ATTR_BYPASSOBJ
},
1429 {nsGkAtoms::aria_atomic
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1430 {nsGkAtoms::aria_busy
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1431 {nsGkAtoms::aria_checked
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
}, /* exposes checkable obj attr */
1432 {nsGkAtoms::aria_colcount
, ATTR_VALINT
},
1433 {nsGkAtoms::aria_colindex
, ATTR_VALINT
},
1434 {nsGkAtoms::aria_controls
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1435 {nsGkAtoms::aria_current
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1436 {nsGkAtoms::aria_describedby
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1437 // XXX Ideally, aria-description shouldn't expose a description object
1438 // attribute (i.e. it should have ATTR_BYPASSOBJ). However, until the
1439 // description-from attribute is implemented (bug 1726087), clients such as
1440 // NVDA depend on the description object attribute to work out whether the
1441 // accDescription originated from aria-description.
1442 {nsGkAtoms::aria_description
, ATTR_GLOBAL
},
1443 {nsGkAtoms::aria_details
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1444 {nsGkAtoms::aria_disabled
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1445 {nsGkAtoms::aria_dropeffect
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1446 {nsGkAtoms::aria_errormessage
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1447 {nsGkAtoms::aria_expanded
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1448 {nsGkAtoms::aria_flowto
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1449 {nsGkAtoms::aria_grabbed
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1450 {nsGkAtoms::aria_haspopup
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1451 {nsGkAtoms::aria_hidden
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
}, /* handled special way */
1452 {nsGkAtoms::aria_invalid
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1453 {nsGkAtoms::aria_label
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1454 {nsGkAtoms::aria_labelledby
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1455 {nsGkAtoms::aria_level
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1456 {nsGkAtoms::aria_live
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1457 {nsGkAtoms::aria_modal
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1458 {nsGkAtoms::aria_multiline
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1459 {nsGkAtoms::aria_multiselectable
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1460 {nsGkAtoms::aria_owns
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1461 {nsGkAtoms::aria_orientation
, ATTR_VALTOKEN
},
1462 {nsGkAtoms::aria_posinset
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1463 {nsGkAtoms::aria_pressed
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1464 {nsGkAtoms::aria_readonly
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1465 {nsGkAtoms::aria_relevant
, ATTR_GLOBAL
},
1466 {nsGkAtoms::aria_required
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1467 {nsGkAtoms::aria_rowcount
, ATTR_VALINT
},
1468 {nsGkAtoms::aria_rowindex
, ATTR_VALINT
},
1469 {nsGkAtoms::aria_selected
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1470 {nsGkAtoms::aria_setsize
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1471 {nsGkAtoms::aria_sort
, ATTR_VALTOKEN
},
1472 {nsGkAtoms::aria_valuenow
, ATTR_BYPASSOBJ
},
1473 {nsGkAtoms::aria_valuemin
, ATTR_BYPASSOBJ
},
1474 {nsGkAtoms::aria_valuemax
, ATTR_BYPASSOBJ
},
1475 {nsGkAtoms::aria_valuetext
, ATTR_BYPASSOBJ
}
1479 const nsRoleMapEntry
* aria::GetRoleMap(dom::Element
* aEl
) {
1480 return GetRoleMapFromIndex(GetRoleMapIndex(aEl
));
1483 uint8_t aria::GetFirstValidRoleMapIndexExcluding(
1484 dom::Element
* aEl
, std::initializer_list
<nsStaticAtom
*> aRolesToSkip
) {
1486 if (!aEl
|| !nsAccUtils::GetARIAAttr(aEl
, nsGkAtoms::role
, roles
) ||
1488 // We treat role="" as if the role attribute is absent (per aria spec:8.1.1)
1489 return NO_ROLE_MAP_ENTRY_INDEX
;
1492 nsWhitespaceTokenizer
tokenizer(roles
);
1493 while (tokenizer
.hasMoreTokens()) {
1494 // Do a binary search through table for the next role in role list
1495 const nsDependentSubstring role
= tokenizer
.nextToken();
1497 // Skip any roles that we aren't interested in.
1498 bool shouldSkip
= false;
1499 for (nsStaticAtom
* atomRole
: aRolesToSkip
) {
1500 if (role
.Equals(atomRole
->GetUTF16String())) {
1510 auto comparator
= [&role
](const nsRoleMapEntry
& aEntry
) {
1511 return Compare(role
, aEntry
.ARIARoleString(),
1512 nsCaseInsensitiveStringComparator
);
1514 if (BinarySearchIf(sWAIRoleMaps
, 0, ArrayLength(sWAIRoleMaps
), comparator
,
1520 // Always use some entry index if there is a non-empty role string
1521 // To ensure an accessible object is created
1522 return LANDMARK_ROLE_MAP_ENTRY_INDEX
;
1525 uint8_t aria::GetRoleMapIndex(dom::Element
* aEl
) {
1526 // Get the rolemap index of the first valid role, excluding nothing.
1527 return GetFirstValidRoleMapIndexExcluding(aEl
, {});
1530 const nsRoleMapEntry
* aria::GetRoleMapFromIndex(uint8_t aRoleMapIndex
) {
1531 switch (aRoleMapIndex
) {
1532 case NO_ROLE_MAP_ENTRY_INDEX
:
1534 case EMPTY_ROLE_MAP_ENTRY_INDEX
:
1535 return &gEmptyRoleMap
;
1536 case LANDMARK_ROLE_MAP_ENTRY_INDEX
:
1537 return &sLandmarkRoleMap
;
1539 return sWAIRoleMaps
+ aRoleMapIndex
;
1543 uint8_t aria::GetIndexFromRoleMap(const nsRoleMapEntry
* aRoleMapEntry
) {
1544 if (aRoleMapEntry
== nullptr) {
1545 return NO_ROLE_MAP_ENTRY_INDEX
;
1546 } else if (aRoleMapEntry
== &gEmptyRoleMap
) {
1547 return EMPTY_ROLE_MAP_ENTRY_INDEX
;
1548 } else if (aRoleMapEntry
== &sLandmarkRoleMap
) {
1549 return LANDMARK_ROLE_MAP_ENTRY_INDEX
;
1551 uint8_t index
= aRoleMapEntry
- sWAIRoleMaps
;
1552 MOZ_ASSERT(aria::IsRoleMapIndexValid(index
));
1557 bool aria::IsRoleMapIndexValid(uint8_t aRoleMapIndex
) {
1558 switch (aRoleMapIndex
) {
1559 case NO_ROLE_MAP_ENTRY_INDEX
:
1560 case EMPTY_ROLE_MAP_ENTRY_INDEX
:
1561 case LANDMARK_ROLE_MAP_ENTRY_INDEX
:
1564 return aRoleMapIndex
< ArrayLength(sWAIRoleMaps
);
1567 uint64_t aria::UniversalStatesFor(mozilla::dom::Element
* aElement
) {
1570 while (MapToState(sWAIUnivStateMap
[index
], aElement
, &state
)) index
++;
1575 uint8_t aria::AttrCharacteristicsFor(nsAtom
* aAtom
) {
1576 for (uint32_t i
= 0; i
< ArrayLength(gWAIUnivAttrMap
); i
++) {
1577 if (gWAIUnivAttrMap
[i
].attributeName
== aAtom
) {
1578 return gWAIUnivAttrMap
[i
].characteristics
;
1585 bool aria::HasDefinedARIAHidden(nsIContent
* aContent
) {
1586 return aContent
&& aContent
->IsElement() &&
1587 nsAccUtils::ARIAAttrValueIs(aContent
->AsElement(),
1588 nsGkAtoms::aria_hidden
, nsGkAtoms::_true
,
1592 ////////////////////////////////////////////////////////////////////////////////
1593 // AttrIterator class
1595 AttrIterator::AttrIterator(nsIContent
* aContent
)
1596 : mElement(dom::Element::FromNode(aContent
)),
1597 mIteratingDefaults(false),
1599 mAttrCharacteristics(0) {
1600 mAttrs
= mElement
? &mElement
->GetAttrs() : nullptr;
1601 mAttrCount
= mAttrs
? mAttrs
->AttrCount() : 0;
1604 bool AttrIterator::Next() {
1605 while (mAttrIdx
< mAttrCount
) {
1606 const nsAttrName
* attr
= mAttrs
->GetSafeAttrNameAt(mAttrIdx
);
1608 if (attr
->NamespaceEquals(kNameSpaceID_None
)) {
1609 mAttrAtom
= attr
->Atom();
1610 nsDependentAtomString
attrStr(mAttrAtom
);
1611 if (!StringBeginsWith(attrStr
, u
"aria-"_ns
)) continue; // Not ARIA
1613 if (mIteratingDefaults
) {
1614 if (mOverriddenAttrs
.Contains(mAttrAtom
)) {
1618 mOverriddenAttrs
.Insert(mAttrAtom
);
1621 // AttrCharacteristicsFor has to search for the entry, so cache it here
1622 // rather than having to search again later.
1623 mAttrCharacteristics
= aria::AttrCharacteristicsFor(mAttrAtom
);
1624 if (mAttrCharacteristics
& ATTR_BYPASSOBJ
) {
1625 continue; // No need to handle exposing as obj attribute here
1628 if ((mAttrCharacteristics
& ATTR_VALTOKEN
) &&
1629 !nsAccUtils::HasDefinedARIAToken(mAttrs
, mAttrAtom
)) {
1630 continue; // only expose token based attributes if they are defined
1633 if ((mAttrCharacteristics
& ATTR_BYPASSOBJ_IF_FALSE
) &&
1634 mAttrs
->AttrValueIs(kNameSpaceID_None
, mAttrAtom
, nsGkAtoms::_false
,
1636 continue; // only expose token based attribute if value is not 'false'.
1643 mAttrCharacteristics
= 0;
1644 mAttrAtom
= nullptr;
1646 if (const auto* defaults
= nsAccUtils::GetARIADefaults(mElement
);
1647 !mIteratingDefaults
&& defaults
) {
1648 mIteratingDefaults
= true;
1650 mAttrCount
= mAttrs
->AttrCount();
1658 nsAtom
* AttrIterator::AttrName() const { return mAttrAtom
; }
1660 void AttrIterator::AttrValue(nsAString
& aAttrValue
) const {
1662 if (mAttrs
->GetAttr(mAttrAtom
, value
)) {
1663 if (mAttrCharacteristics
& ATTR_VALTOKEN
) {
1664 nsAtom
* normalizedValue
=
1665 nsAccUtils::NormalizeARIAToken(mAttrs
, mAttrAtom
);
1666 if (normalizedValue
) {
1667 nsDependentAtomString
normalizedValueStr(normalizedValue
);
1668 aAttrValue
.Assign(normalizedValueStr
);
1672 aAttrValue
.Assign(value
);
1676 bool AttrIterator::ExposeAttr(AccAttributes
* aTargetAttrs
) const {
1677 if (mAttrCharacteristics
& ATTR_VALTOKEN
) {
1678 nsAtom
* normalizedValue
= nsAccUtils::NormalizeARIAToken(mAttrs
, mAttrAtom
);
1679 if (normalizedValue
) {
1680 aTargetAttrs
->SetAttribute(mAttrAtom
, normalizedValue
);
1683 } else if (mAttrCharacteristics
& ATTR_VALINT
) {
1685 if (nsCoreUtils::GetUIntAttrValue(mAttrs
->GetAttr(mAttrAtom
), &intVal
)) {
1686 aTargetAttrs
->SetAttribute(mAttrAtom
, intVal
);
1689 if (mAttrAtom
== nsGkAtoms::aria_colcount
||
1690 mAttrAtom
== nsGkAtoms::aria_rowcount
) {
1691 // These attributes allow a value of -1.
1692 if (mAttrs
->AttrValueIs(kNameSpaceID_None
, mAttrAtom
, u
"-1"_ns
,
1694 aTargetAttrs
->SetAttribute(mAttrAtom
, -1);
1698 return false; // Invalid value.
1701 if (mAttrs
->GetAttr(mAttrAtom
, value
)) {
1702 aTargetAttrs
->SetAttribute(mAttrAtom
, std::move(value
));