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
,
928 nsGkAtoms::navigation
,
970 nsGkAtoms::paragraph
,
979 nsGkAtoms::presentation
,
989 nsGkAtoms::progressbar
,
997 eIndeterminateIfNoValue
1011 nsGkAtoms::radiogroup
,
1044 nsGkAtoms::rowgroup
,
1054 nsGkAtoms::rowheader
,
1062 eARIASelectableIfDefined
,
1066 nsGkAtoms::scrollbar
,
1088 nsGkAtoms::searchbox
,
1098 eARIAReadonlyOrEditable
1101 nsGkAtoms::separator_
,
1104 eHasValueMinMaxIfFocusable
,
1124 nsGkAtoms::spinbutton
,
1145 nsGkAtoms::suggestion
,
1154 nsGkAtoms::svgSwitch
,
1158 eCheckUncheckAction
,
1197 eARIAMultiSelectable
1200 nsGkAtoms::tabpanel
,
1201 roles::PROPERTYPAGE
,
1230 eARIAReadonlyOrEditable
1272 eARIAMultiSelectable
,
1273 eFocusableUntilDisabled
,
1277 nsGkAtoms::treegrid
,
1286 eARIAMultiSelectable
,
1287 eFocusableUntilDisabled
,
1291 nsGkAtoms::treeitem
,
1295 eActivateAction
, // XXX: should expose second 'expand/collapse' action based
1306 static const nsRoleMapEntry sLandmarkRoleMap
= {
1307 nsGkAtoms::_empty
, roles::NOTHING
, kUseNativeRole
, eNoValue
,
1308 eNoAction
, eNoLiveAttr
, kGenericAccType
, kNoReqStates
};
1310 nsRoleMapEntry
aria::gEmptyRoleMap
= {
1311 nsGkAtoms::_empty
, roles::TEXT_CONTAINER
, kUseMapRole
, eNoValue
,
1312 eNoAction
, eNoLiveAttr
, kGenericAccType
, kNoReqStates
};
1315 * Universal (Global) states:
1316 * The following state rules are applied to any accessible element,
1317 * whether there is an ARIA role or not:
1319 static const EStateRule sWAIUnivStateMap
[] = {
1320 eARIABusy
, eARIACurrent
, eARIADisabled
,
1321 eARIAExpanded
, // Currently under spec review but precedent exists
1322 eARIAHasPopup
, // Note this is a tokenised attribute starting in ARIA 1.1
1323 eARIAInvalid
, eARIAModal
,
1324 eARIARequired
, // XXX not global, Bug 553117
1328 * ARIA attribute map for attribute characteristics.
1329 * @note ARIA attributes that don't have any flags are not included here.
1332 struct AttrCharacteristics
{
1333 const nsStaticAtom
* const attributeName
;
1334 const uint8_t characteristics
;
1337 static const AttrCharacteristics gWAIUnivAttrMap
[] = {
1339 {nsGkAtoms::aria_activedescendant
, ATTR_BYPASSOBJ
},
1340 {nsGkAtoms::aria_atomic
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1341 {nsGkAtoms::aria_busy
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1342 {nsGkAtoms::aria_checked
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
}, /* exposes checkable obj attr */
1343 {nsGkAtoms::aria_controls
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1344 {nsGkAtoms::aria_current
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1345 {nsGkAtoms::aria_describedby
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1346 {nsGkAtoms::aria_details
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1347 {nsGkAtoms::aria_disabled
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1348 {nsGkAtoms::aria_dropeffect
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1349 {nsGkAtoms::aria_errormessage
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1350 {nsGkAtoms::aria_expanded
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1351 {nsGkAtoms::aria_flowto
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1352 {nsGkAtoms::aria_grabbed
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1353 {nsGkAtoms::aria_haspopup
, ATTR_BYPASSOBJ_IF_FALSE
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1354 {nsGkAtoms::aria_hidden
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
}, /* handled special way */
1355 {nsGkAtoms::aria_invalid
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1356 {nsGkAtoms::aria_label
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1357 {nsGkAtoms::aria_labelledby
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1358 {nsGkAtoms::aria_level
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1359 {nsGkAtoms::aria_live
, ATTR_VALTOKEN
| ATTR_GLOBAL
},
1360 {nsGkAtoms::aria_modal
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
| ATTR_GLOBAL
},
1361 {nsGkAtoms::aria_multiline
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1362 {nsGkAtoms::aria_multiselectable
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1363 {nsGkAtoms::aria_owns
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1364 {nsGkAtoms::aria_orientation
, ATTR_VALTOKEN
},
1365 {nsGkAtoms::aria_posinset
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1366 {nsGkAtoms::aria_pressed
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1367 {nsGkAtoms::aria_readonly
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1368 {nsGkAtoms::aria_relevant
, ATTR_BYPASSOBJ
| ATTR_GLOBAL
},
1369 {nsGkAtoms::aria_required
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1370 {nsGkAtoms::aria_selected
, ATTR_BYPASSOBJ
| ATTR_VALTOKEN
},
1371 {nsGkAtoms::aria_setsize
, ATTR_BYPASSOBJ
}, /* handled via groupPosition */
1372 {nsGkAtoms::aria_sort
, ATTR_VALTOKEN
},
1373 {nsGkAtoms::aria_valuenow
, ATTR_BYPASSOBJ
},
1374 {nsGkAtoms::aria_valuemin
, ATTR_BYPASSOBJ
},
1375 {nsGkAtoms::aria_valuemax
, ATTR_BYPASSOBJ
},
1376 {nsGkAtoms::aria_valuetext
, ATTR_BYPASSOBJ
}
1380 const nsRoleMapEntry
* aria::GetRoleMap(dom::Element
* aEl
) {
1381 return GetRoleMapFromIndex(GetRoleMapIndex(aEl
));
1384 uint8_t aria::GetRoleMapIndex(dom::Element
* aEl
) {
1386 if (!aEl
|| !aEl
->GetAttr(kNameSpaceID_None
, nsGkAtoms::role
, roles
) ||
1388 // We treat role="" as if the role attribute is absent (per aria spec:8.1.1)
1389 return NO_ROLE_MAP_ENTRY_INDEX
;
1392 nsWhitespaceTokenizer
tokenizer(roles
);
1393 while (tokenizer
.hasMoreTokens()) {
1394 // Do a binary search through table for the next role in role list
1395 const nsDependentSubstring role
= tokenizer
.nextToken();
1397 auto comparator
= [&role
](const nsRoleMapEntry
& aEntry
) {
1398 return Compare(role
, aEntry
.ARIARoleString(),
1399 nsCaseInsensitiveStringComparator
);
1401 if (BinarySearchIf(sWAIRoleMaps
, 0, ArrayLength(sWAIRoleMaps
), comparator
,
1407 // Always use some entry index if there is a non-empty role string
1408 // To ensure an accessible object is created
1409 return LANDMARK_ROLE_MAP_ENTRY_INDEX
;
1412 const nsRoleMapEntry
* aria::GetRoleMapFromIndex(uint8_t aRoleMapIndex
) {
1413 switch (aRoleMapIndex
) {
1414 case NO_ROLE_MAP_ENTRY_INDEX
:
1416 case EMPTY_ROLE_MAP_ENTRY_INDEX
:
1417 return &gEmptyRoleMap
;
1418 case LANDMARK_ROLE_MAP_ENTRY_INDEX
:
1419 return &sLandmarkRoleMap
;
1421 return sWAIRoleMaps
+ aRoleMapIndex
;
1425 uint8_t aria::GetIndexFromRoleMap(const nsRoleMapEntry
* aRoleMapEntry
) {
1426 if (aRoleMapEntry
== nullptr) {
1427 return NO_ROLE_MAP_ENTRY_INDEX
;
1428 } else if (aRoleMapEntry
== &gEmptyRoleMap
) {
1429 return EMPTY_ROLE_MAP_ENTRY_INDEX
;
1430 } else if (aRoleMapEntry
== &sLandmarkRoleMap
) {
1431 return LANDMARK_ROLE_MAP_ENTRY_INDEX
;
1433 return aRoleMapEntry
- sWAIRoleMaps
;
1437 uint64_t aria::UniversalStatesFor(mozilla::dom::Element
* aElement
) {
1440 while (MapToState(sWAIUnivStateMap
[index
], aElement
, &state
)) index
++;
1445 uint8_t aria::AttrCharacteristicsFor(nsAtom
* aAtom
) {
1446 for (uint32_t i
= 0; i
< ArrayLength(gWAIUnivAttrMap
); i
++) {
1447 if (gWAIUnivAttrMap
[i
].attributeName
== aAtom
) {
1448 return gWAIUnivAttrMap
[i
].characteristics
;
1455 bool aria::HasDefinedARIAHidden(nsIContent
* aContent
) {
1456 return aContent
&& aContent
->IsElement() &&
1457 aContent
->AsElement()->AttrValueIs(kNameSpaceID_None
,
1458 nsGkAtoms::aria_hidden
,
1459 nsGkAtoms::_true
, eCaseMatters
);
1462 ////////////////////////////////////////////////////////////////////////////////
1463 // AttrIterator class
1465 AttrIterator::AttrIterator(nsIContent
* aContent
)
1466 : mElement(dom::Element::FromNode(aContent
)), mAttrIdx(0) {
1467 mAttrCount
= mElement
? mElement
->GetAttrCount() : 0;
1470 bool AttrIterator::Next(nsAString
& aAttrName
, nsAString
& aAttrValue
) {
1471 while (mAttrIdx
< mAttrCount
) {
1472 const nsAttrName
* attr
= mElement
->GetAttrNameAt(mAttrIdx
);
1474 if (attr
->NamespaceEquals(kNameSpaceID_None
)) {
1475 nsAtom
* attrAtom
= attr
->Atom();
1476 nsDependentAtomString
attrStr(attrAtom
);
1477 if (!StringBeginsWith(attrStr
, u
"aria-"_ns
)) continue; // Not ARIA
1479 uint8_t attrFlags
= aria::AttrCharacteristicsFor(attrAtom
);
1480 if (attrFlags
& ATTR_BYPASSOBJ
) {
1481 continue; // No need to handle exposing as obj attribute here
1484 if ((attrFlags
& ATTR_VALTOKEN
) &&
1485 !nsAccUtils::HasDefinedARIAToken(mElement
, attrAtom
)) {
1486 continue; // only expose token based attributes if they are defined
1489 if ((attrFlags
& ATTR_BYPASSOBJ_IF_FALSE
) &&
1490 mElement
->AttrValueIs(kNameSpaceID_None
, attrAtom
, nsGkAtoms::_false
,
1492 continue; // only expose token based attribute if value is not 'false'.
1496 if (mElement
->GetAttr(kNameSpaceID_None
, attrAtom
, value
)) {
1497 aAttrName
.Assign(Substring(attrStr
, 5));
1498 if (attrFlags
& ATTR_VALTOKEN
) {
1499 nsAtom
* normalizedValue
=
1500 nsAccUtils::NormalizeARIAToken(mElement
, attrAtom
);
1501 if (normalizedValue
) {
1502 nsDependentAtomString
normalizedValueStr(normalizedValue
);
1503 aAttrValue
.Assign(normalizedValueStr
);
1507 aAttrValue
.Assign(value
);