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
,
216 roles::CONTENT_DELETION
,
234 nsGkAtoms::directory
,
244 nsGkAtoms::docAbstract
,
253 { // doc-acknowledgments
254 nsGkAtoms::docAcknowledgments
,
264 nsGkAtoms::docAfterword
,
274 nsGkAtoms::docAppendix
,
284 nsGkAtoms::docBacklink
,
294 nsGkAtoms::docBiblioentry
,
303 { // doc-bibliography
304 nsGkAtoms::docBibliography
,
314 nsGkAtoms::docBiblioref
,
324 nsGkAtoms::docChapter
,
334 nsGkAtoms::docColophon
,
344 nsGkAtoms::docConclusion
,
364 nsGkAtoms::docCredit
,
374 nsGkAtoms::docCredits
,
384 nsGkAtoms::docDedication
,
394 nsGkAtoms::docEndnote
,
404 nsGkAtoms::docEndnotes
,
414 nsGkAtoms::docEpigraph
,
424 nsGkAtoms::docEpilogue
,
434 nsGkAtoms::docErrata
,
444 nsGkAtoms::docExample
,
454 nsGkAtoms::docFootnote
,
464 nsGkAtoms::docForeword
,
474 nsGkAtoms::docGlossary
,
484 nsGkAtoms::docGlossref
,
503 { // doc-introduction
504 nsGkAtoms::docIntroduction
,
514 nsGkAtoms::docNoteref
,
524 nsGkAtoms::docNotice
,
534 nsGkAtoms::docPagebreak
,
544 nsGkAtoms::docPagelist
,
564 nsGkAtoms::docPreface
,
574 nsGkAtoms::docPrologue
,
584 nsGkAtoms::docPullquote
,
604 nsGkAtoms::docSubtitle
,
635 roles::NON_NATIVE_DOCUMENT
,
642 eReadonlyUntilEditable
674 { // graphics-document
675 nsGkAtoms::graphicsDocument
,
676 roles::NON_NATIVE_DOCUMENT
,
683 eReadonlyUntilEditable
686 nsGkAtoms::graphicsObject
,
696 nsGkAtoms::graphicsSymbol
,
714 eARIAMultiSelectable
,
716 eFocusableUntilDisabled
771 nsGkAtoms::insertion
,
772 roles::CONTENT_INSERTION
,
817 eListControl
| eSelect
,
819 eARIAMultiSelectable
,
821 eFocusableUntilDisabled
,
829 eNoAction
, // XXX: should depend on state, parent accessible
875 roles::FLAT_EQUATION
,
888 eNoAction
, // XXX: technically accessibles of menupopup role haven't
889 // any action, but menu can be open or close.
916 { // menuitemcheckbox
917 nsGkAtoms::menuitemcheckbox
,
918 roles::CHECK_MENU_ITEM
,
929 nsGkAtoms::menuitemradio
,
930 roles::RADIO_MENU_ITEM
,
951 nsGkAtoms::navigation
,
993 nsGkAtoms::paragraph
,
1002 nsGkAtoms::presentation
,
1012 nsGkAtoms::progressbar
,
1020 eIndeterminateIfNoValue
1034 nsGkAtoms::radiogroup
,
1067 nsGkAtoms::rowgroup
,
1077 nsGkAtoms::rowheader
,
1085 eARIASelectableIfDefined
,
1089 nsGkAtoms::scrollbar
,
1111 nsGkAtoms::searchbox
,
1121 eARIAReadonlyOrEditable
1124 nsGkAtoms::separator_
,
1127 eHasValueMinMaxIfFocusable
,
1147 nsGkAtoms::spinbutton
,
1168 nsGkAtoms::subscript
,
1177 nsGkAtoms::suggestion
,
1186 nsGkAtoms::superscript
,
1195 nsGkAtoms::svgSwitch
,
1199 eCheckUncheckAction
,
1238 eARIAMultiSelectable
1241 nsGkAtoms::tabpanel
,
1242 roles::PROPERTYPAGE
,
1271 eARIAReadonlyOrEditable
1313 eARIAMultiSelectable
,
1314 eFocusableUntilDisabled
,
1318 nsGkAtoms::treegrid
,
1327 eARIAMultiSelectable
,
1328 eFocusableUntilDisabled
,
1332 nsGkAtoms::treeitem
,
1336 eActivateAction
, // XXX: should expose second 'expand/collapse' action based
1347 static const nsRoleMapEntry sLandmarkRoleMap
= {
1348 nsGkAtoms::_empty
, roles::NOTHING
, kUseNativeRole
, eNoValue
,
1349 eNoAction
, eNoLiveAttr
, kGenericAccType
, kNoReqStates
};
1351 nsRoleMapEntry
aria::gEmptyRoleMap
= {
1352 nsGkAtoms::_empty
, roles::TEXT_CONTAINER
, kUseMapRole
, eNoValue
,
1353 eNoAction
, eNoLiveAttr
, kGenericAccType
, kNoReqStates
};
1356 * Universal (Global) states:
1357 * The following state rules are applied to any accessible element,
1358 * whether there is an ARIA role or not:
1360 static const EStateRule sWAIUnivStateMap
[] = {
1361 eARIABusy
, eARIACurrent
, eARIADisabled
,
1362 eARIAExpanded
, // Currently under spec review but precedent exists
1363 eARIAHasPopup
, // Note this is a tokenised attribute starting in ARIA 1.1
1364 eARIAInvalid
, eARIAModal
,
1365 eARIARequired
, // XXX not global, Bug 553117
1369 * ARIA attribute map for attribute characteristics.
1370 * @note ARIA attributes that don't have any flags are not included here.
1373 struct AttrCharacteristics
{
1374 const nsStaticAtom
* const attributeName
;
1375 const uint8_t characteristics
;
1378 static const AttrCharacteristics gWAIUnivAttrMap
[] = {
1380 {nsGkAtoms::aria_activedescendant
, ATTR_BYPASSOBJ
},
1381 {nsGkAtoms::aria_atomic
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1382 {nsGkAtoms::aria_busy
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1383 {nsGkAtoms::aria_checked
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
}, /* exposes checkable obj attr */
1384 {nsGkAtoms::aria_colcount
, ATTR_VALINT
},
1385 {nsGkAtoms::aria_colindex
, ATTR_VALINT
},
1386 {nsGkAtoms::aria_controls
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1387 {nsGkAtoms::aria_current
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1388 {nsGkAtoms::aria_describedby
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1389 // XXX Ideally, aria-description shouldn't expose a description object
1390 // attribute (i.e. it should have ATTR_BYPASSOBJ). However, until the
1391 // description-from attribute is implemented (bug 1726087), clients such as
1392 // NVDA depend on the description object attribute to work out whether the
1393 // accDescription originated from aria-description.
1394 {nsGkAtoms::aria_description
, ATTR_GLOBAL
},
1395 {nsGkAtoms::aria_details
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1396 {nsGkAtoms::aria_disabled
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1397 {nsGkAtoms::aria_dropeffect
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1398 {nsGkAtoms::aria_errormessage
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1399 {nsGkAtoms::aria_expanded
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1400 {nsGkAtoms::aria_flowto
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1401 {nsGkAtoms::aria_grabbed
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1402 {nsGkAtoms::aria_haspopup
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1403 {nsGkAtoms::aria_hidden
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
}, /* handled special way */
1404 {nsGkAtoms::aria_invalid
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1405 {nsGkAtoms::aria_label
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1406 {nsGkAtoms::aria_labelledby
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1407 {nsGkAtoms::aria_level
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1408 {nsGkAtoms::aria_live
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1409 {nsGkAtoms::aria_modal
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1410 {nsGkAtoms::aria_multiline
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1411 {nsGkAtoms::aria_multiselectable
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1412 {nsGkAtoms::aria_owns
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1413 {nsGkAtoms::aria_orientation
, ATTR_VALTOKEN
},
1414 {nsGkAtoms::aria_posinset
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1415 {nsGkAtoms::aria_pressed
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1416 {nsGkAtoms::aria_readonly
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1417 {nsGkAtoms::aria_relevant
, ATTR_GLOBAL
},
1418 {nsGkAtoms::aria_required
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1419 {nsGkAtoms::aria_rowcount
, ATTR_VALINT
},
1420 {nsGkAtoms::aria_rowindex
, ATTR_VALINT
},
1421 {nsGkAtoms::aria_selected
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1422 {nsGkAtoms::aria_setsize
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1423 {nsGkAtoms::aria_sort
, ATTR_VALTOKEN
},
1424 {nsGkAtoms::aria_valuenow
, ATTR_BYPASSOBJ
},
1425 {nsGkAtoms::aria_valuemin
, ATTR_BYPASSOBJ
},
1426 {nsGkAtoms::aria_valuemax
, ATTR_BYPASSOBJ
},
1427 {nsGkAtoms::aria_valuetext
, ATTR_BYPASSOBJ
}
1431 const nsRoleMapEntry
* aria::GetRoleMap(dom::Element
* aEl
) {
1432 return GetRoleMapFromIndex(GetRoleMapIndex(aEl
));
1435 uint8_t aria::GetRoleMapIndex(dom::Element
* aEl
) {
1437 if (!aEl
|| !nsAccUtils::GetARIAAttr(aEl
, nsGkAtoms::role
, roles
) ||
1439 // We treat role="" as if the role attribute is absent (per aria spec:8.1.1)
1440 return NO_ROLE_MAP_ENTRY_INDEX
;
1443 nsWhitespaceTokenizer
tokenizer(roles
);
1444 while (tokenizer
.hasMoreTokens()) {
1445 // Do a binary search through table for the next role in role list
1446 const nsDependentSubstring role
= tokenizer
.nextToken();
1448 auto comparator
= [&role
](const nsRoleMapEntry
& aEntry
) {
1449 return Compare(role
, aEntry
.ARIARoleString(),
1450 nsCaseInsensitiveStringComparator
);
1452 if (BinarySearchIf(sWAIRoleMaps
, 0, ArrayLength(sWAIRoleMaps
), comparator
,
1458 // Always use some entry index if there is a non-empty role string
1459 // To ensure an accessible object is created
1460 return LANDMARK_ROLE_MAP_ENTRY_INDEX
;
1463 const nsRoleMapEntry
* aria::GetRoleMapFromIndex(uint8_t aRoleMapIndex
) {
1464 switch (aRoleMapIndex
) {
1465 case NO_ROLE_MAP_ENTRY_INDEX
:
1467 case EMPTY_ROLE_MAP_ENTRY_INDEX
:
1468 return &gEmptyRoleMap
;
1469 case LANDMARK_ROLE_MAP_ENTRY_INDEX
:
1470 return &sLandmarkRoleMap
;
1472 return sWAIRoleMaps
+ aRoleMapIndex
;
1476 uint8_t aria::GetIndexFromRoleMap(const nsRoleMapEntry
* aRoleMapEntry
) {
1477 if (aRoleMapEntry
== nullptr) {
1478 return NO_ROLE_MAP_ENTRY_INDEX
;
1479 } else if (aRoleMapEntry
== &gEmptyRoleMap
) {
1480 return EMPTY_ROLE_MAP_ENTRY_INDEX
;
1481 } else if (aRoleMapEntry
== &sLandmarkRoleMap
) {
1482 return LANDMARK_ROLE_MAP_ENTRY_INDEX
;
1484 uint8_t index
= aRoleMapEntry
- sWAIRoleMaps
;
1485 MOZ_ASSERT(aria::IsRoleMapIndexValid(index
));
1490 bool aria::IsRoleMapIndexValid(uint8_t aRoleMapIndex
) {
1491 switch (aRoleMapIndex
) {
1492 case NO_ROLE_MAP_ENTRY_INDEX
:
1493 case EMPTY_ROLE_MAP_ENTRY_INDEX
:
1494 case LANDMARK_ROLE_MAP_ENTRY_INDEX
:
1497 return aRoleMapIndex
< ArrayLength(sWAIRoleMaps
);
1500 uint64_t aria::UniversalStatesFor(mozilla::dom::Element
* aElement
) {
1503 while (MapToState(sWAIUnivStateMap
[index
], aElement
, &state
)) index
++;
1508 uint8_t aria::AttrCharacteristicsFor(nsAtom
* aAtom
) {
1509 for (uint32_t i
= 0; i
< ArrayLength(gWAIUnivAttrMap
); i
++) {
1510 if (gWAIUnivAttrMap
[i
].attributeName
== aAtom
) {
1511 return gWAIUnivAttrMap
[i
].characteristics
;
1518 bool aria::HasDefinedARIAHidden(nsIContent
* aContent
) {
1519 return aContent
&& aContent
->IsElement() &&
1520 nsAccUtils::ARIAAttrValueIs(aContent
->AsElement(),
1521 nsGkAtoms::aria_hidden
, nsGkAtoms::_true
,
1525 ////////////////////////////////////////////////////////////////////////////////
1526 // AttrIterator class
1528 AttrIterator::AttrIterator(nsIContent
* aContent
)
1529 : mElement(dom::Element::FromNode(aContent
)),
1530 mIteratingDefaults(false),
1532 mAttrCharacteristics(0) {
1533 mAttrs
= mElement
? &mElement
->GetAttrs() : nullptr;
1534 mAttrCount
= mAttrs
? mAttrs
->AttrCount() : 0;
1537 bool AttrIterator::Next() {
1538 while (mAttrIdx
< mAttrCount
) {
1539 const nsAttrName
* attr
= mAttrs
->GetSafeAttrNameAt(mAttrIdx
);
1541 if (attr
->NamespaceEquals(kNameSpaceID_None
)) {
1542 mAttrAtom
= attr
->Atom();
1543 nsDependentAtomString
attrStr(mAttrAtom
);
1544 if (!StringBeginsWith(attrStr
, u
"aria-"_ns
)) continue; // Not ARIA
1546 if (mIteratingDefaults
) {
1547 if (mOverriddenAttrs
.Contains(mAttrAtom
)) {
1551 mOverriddenAttrs
.Insert(mAttrAtom
);
1554 // AttrCharacteristicsFor has to search for the entry, so cache it here
1555 // rather than having to search again later.
1556 mAttrCharacteristics
= aria::AttrCharacteristicsFor(mAttrAtom
);
1557 if (mAttrCharacteristics
& ATTR_BYPASSOBJ
) {
1558 continue; // No need to handle exposing as obj attribute here
1561 if ((mAttrCharacteristics
& ATTR_VALTOKEN
) &&
1562 !nsAccUtils::HasDefinedARIAToken(mAttrs
, mAttrAtom
)) {
1563 continue; // only expose token based attributes if they are defined
1566 if ((mAttrCharacteristics
& ATTR_BYPASSOBJ_IF_FALSE
) &&
1567 mAttrs
->AttrValueIs(kNameSpaceID_None
, mAttrAtom
, nsGkAtoms::_false
,
1569 continue; // only expose token based attribute if value is not 'false'.
1576 mAttrCharacteristics
= 0;
1577 mAttrAtom
= nullptr;
1579 if (const auto* defaults
= nsAccUtils::GetARIADefaults(mElement
);
1580 !mIteratingDefaults
&& defaults
) {
1581 mIteratingDefaults
= true;
1583 mAttrCount
= mAttrs
->AttrCount();
1591 nsAtom
* AttrIterator::AttrName() const { return mAttrAtom
; }
1593 void AttrIterator::AttrValue(nsAString
& aAttrValue
) const {
1595 if (mAttrs
->GetAttr(mAttrAtom
, value
)) {
1596 if (mAttrCharacteristics
& ATTR_VALTOKEN
) {
1597 nsAtom
* normalizedValue
=
1598 nsAccUtils::NormalizeARIAToken(mAttrs
, mAttrAtom
);
1599 if (normalizedValue
) {
1600 nsDependentAtomString
normalizedValueStr(normalizedValue
);
1601 aAttrValue
.Assign(normalizedValueStr
);
1605 aAttrValue
.Assign(value
);
1609 bool AttrIterator::ExposeAttr(AccAttributes
* aTargetAttrs
) const {
1610 if (mAttrCharacteristics
& ATTR_VALTOKEN
) {
1611 nsAtom
* normalizedValue
= nsAccUtils::NormalizeARIAToken(mAttrs
, mAttrAtom
);
1612 if (normalizedValue
) {
1613 aTargetAttrs
->SetAttribute(mAttrAtom
, normalizedValue
);
1616 } else if (mAttrCharacteristics
& ATTR_VALINT
) {
1618 if (nsCoreUtils::GetUIntAttrValue(mAttrs
->GetAttr(mAttrAtom
), &intVal
)) {
1619 aTargetAttrs
->SetAttribute(mAttrAtom
, intVal
);
1622 if (mAttrAtom
== nsGkAtoms::aria_colcount
||
1623 mAttrAtom
== nsGkAtoms::aria_rowcount
) {
1624 // These attributes allow a value of -1.
1625 if (mAttrs
->AttrValueIs(kNameSpaceID_None
, mAttrAtom
, u
"-1"_ns
,
1627 aTargetAttrs
->SetAttribute(mAttrAtom
, -1);
1631 return false; // Invalid value.
1634 if (mAttrs
->GetAttr(mAttrAtom
, value
)) {
1635 aTargetAttrs
->SetAttribute(mAttrAtom
, std::move(value
));