Bug 809751 - presentational table related elements referred or having global ARIA attributes must be accessible, r=tbsaunde

This commit is contained in:
Alexander Surkov 2012-11-10 19:26:58 +09:00
parent e4edf28611
commit 788e8fce72
5 changed files with 152 additions and 114 deletions

View File

@ -600,40 +600,40 @@ static const EStateRule sWAIUnivStateMap[] = {
* @note ARIA attributes that don't have any flags are not included here
*/
nsAttributeCharacteristics nsARIAMap::gWAIUnivAttrMap[] = {
{&nsGkAtoms::aria_activedescendant, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_atomic, ATTR_VALTOKEN },
{&nsGkAtoms::aria_busy, ATTR_VALTOKEN },
{&nsGkAtoms::aria_checked, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, /* exposes checkable obj attr */
{&nsGkAtoms::aria_controls, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_describedby, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_disabled, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_dropeffect, ATTR_VALTOKEN },
{&nsGkAtoms::aria_expanded, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_flowto, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_grabbed, ATTR_VALTOKEN },
{&nsGkAtoms::aria_haspopup, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_hidden, ATTR_VALTOKEN },/* always expose obj attr */
{&nsGkAtoms::aria_invalid, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_label, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_labelledby, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_level, ATTR_BYPASSOBJ }, /* handled via groupPosition */
{&nsGkAtoms::aria_live, ATTR_VALTOKEN },
{&nsGkAtoms::aria_multiline, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_multiselectable, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_owns, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_orientation, ATTR_VALTOKEN },
{&nsGkAtoms::aria_posinset, ATTR_BYPASSOBJ }, /* handled via groupPosition */
{&nsGkAtoms::aria_pressed, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_readonly, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_relevant, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_required, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_selected, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_setsize, ATTR_BYPASSOBJ }, /* handled via groupPosition */
{&nsGkAtoms::aria_sort, ATTR_VALTOKEN },
{&nsGkAtoms::aria_valuenow, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_valuemin, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_valuemax, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_valuetext, ATTR_BYPASSOBJ }
{&nsGkAtoms::aria_activedescendant, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_atomic, ATTR_VALTOKEN | ATTR_GLOBAL },
{&nsGkAtoms::aria_busy, ATTR_VALTOKEN | ATTR_GLOBAL },
{&nsGkAtoms::aria_checked, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, /* exposes checkable obj attr */
{&nsGkAtoms::aria_controls, ATTR_BYPASSOBJ | ATTR_GLOBAL },
{&nsGkAtoms::aria_describedby, ATTR_BYPASSOBJ | ATTR_GLOBAL },
{&nsGkAtoms::aria_disabled, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL },
{&nsGkAtoms::aria_dropeffect, ATTR_VALTOKEN | ATTR_GLOBAL },
{&nsGkAtoms::aria_expanded, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_flowto, ATTR_BYPASSOBJ | ATTR_GLOBAL },
{&nsGkAtoms::aria_grabbed, ATTR_VALTOKEN | ATTR_GLOBAL },
{&nsGkAtoms::aria_haspopup, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL },
{&nsGkAtoms::aria_hidden, ATTR_VALTOKEN | ATTR_GLOBAL },/* always expose obj attr */
{&nsGkAtoms::aria_invalid, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL },
{&nsGkAtoms::aria_label, ATTR_BYPASSOBJ | ATTR_GLOBAL },
{&nsGkAtoms::aria_labelledby, ATTR_BYPASSOBJ | ATTR_GLOBAL },
{&nsGkAtoms::aria_level, ATTR_BYPASSOBJ }, /* handled via groupPosition */
{&nsGkAtoms::aria_live, ATTR_VALTOKEN | ATTR_GLOBAL },
{&nsGkAtoms::aria_multiline, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_multiselectable, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_owns, ATTR_BYPASSOBJ | ATTR_GLOBAL },
{&nsGkAtoms::aria_orientation, ATTR_VALTOKEN },
{&nsGkAtoms::aria_posinset, ATTR_BYPASSOBJ }, /* handled via groupPosition */
{&nsGkAtoms::aria_pressed, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_readonly, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_relevant, ATTR_BYPASSOBJ | ATTR_GLOBAL },
{&nsGkAtoms::aria_required, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_selected, ATTR_BYPASSOBJ | ATTR_VALTOKEN },
{&nsGkAtoms::aria_setsize, ATTR_BYPASSOBJ }, /* handled via groupPosition */
{&nsGkAtoms::aria_sort, ATTR_VALTOKEN },
{&nsGkAtoms::aria_valuenow, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_valuemin, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_valuemax, ATTR_BYPASSOBJ },
{&nsGkAtoms::aria_valuetext, ATTR_BYPASSOBJ }
};
uint32_t

View File

@ -96,13 +96,19 @@ const bool kUseNativeRole = false;
* This means it either isn't mean't to be exposed as an object attribute, or
* that it should, but is already handled in other code.
*/
const uint8_t ATTR_BYPASSOBJ = 0x0001;
const uint8_t ATTR_BYPASSOBJ = 0x1 << 0;
/**
* This mask indicates the attribute is expected to have an NMTOKEN or bool value.
* (See for example usage in Accessible::Attributes())
*/
const uint8_t ATTR_VALTOKEN = 0x0010;
const uint8_t ATTR_VALTOKEN = 0x1 << 1;
/**
* Indicate the attribute is global state or property (refer to
* http://www.w3.org/TR/wai-aria/states_and_properties#global_states).
*/
const uint8_t ATTR_GLOBAL = 0x1 << 2;
/**
* Small footprint storage of persistent aria attribute characteristics.

View File

@ -78,6 +78,46 @@
using namespace mozilla;
using namespace mozilla::a11y;
////////////////////////////////////////////////////////////////////////////////
// Statics
////////////////////////////////////////////////////////////////////////////////
/**
* Return true if the element must be accessible.
*/
static bool
MustBeAccessible(nsIContent* aContent, DocAccessible* aDocument)
{
if (aContent->GetPrimaryFrame()->IsFocusable())
return true;
PRUint32 attrCount = aContent->GetAttrCount();
for (PRUint32 attrIdx = 0; attrIdx < attrCount; attrIdx++) {
const nsAttrName* attr = aContent->GetAttrNameAt(attrIdx);
if (attr->NamespaceEquals(kNameSpaceID_None)) {
nsIAtom* attrAtom = attr->Atom();
nsDependentAtomString attrStr(attrAtom);
if (!StringBeginsWith(attrStr, NS_LITERAL_STRING("aria-")))
continue; // not ARIA
// A global state or a property and in case of token defined.
uint8_t attrFlags = nsAccUtils::GetAttributeCharacteristics(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(id);
return false;
}
////////////////////////////////////////////////////////////////////////////////
// nsAccessibilityService
////////////////////////////////////////////////////////////////////////////////
@ -638,19 +678,6 @@ nsAccessibilityService::IsLogged(const nsAString& aModule, bool* aIsLogged)
////////////////////////////////////////////////////////////////////////////////
// nsAccessibilityService public
static bool HasRelatedContent(nsIContent *aContent)
{
nsAutoString id;
if (!aContent || !nsCoreUtils::GetID(aContent, id) || id.IsEmpty()) {
return false;
}
// If the given ID is referred by relation attribute then create an accessible
// for it. Take care of HTML elements only for now.
return aContent->IsHTML() &&
nsAccUtils::GetDocAccessibleFor(aContent)->IsDependentID(id);
}
Accessible*
nsAccessibilityService::GetOrCreateAccessible(nsINode* aNode,
DocAccessible* aDoc,
@ -777,8 +804,7 @@ nsAccessibilityService::GetOrCreateAccessible(nsINode* aNode,
// it is referenced by ARIA relationship then treat role="presentation" on
// the element as the role is not there.
if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::presentation)) {
if (!content->IsFocusable() && !HasUniversalAriaProperty(content) &&
!HasRelatedContent(content))
if (!MustBeAccessible(content, aDoc))
return nullptr;
roleMapEntry = nullptr;
@ -825,7 +851,7 @@ nsAccessibilityService::GetOrCreateAccessible(nsINode* aNode,
"No accessible for parent table and it didn't have role of presentation");
#endif
if (!roleMapEntry && !content->IsFocusable()) {
if (!roleMapEntry && !MustBeAccessible(content, aDoc)) {
// Table-related descendants of presentation table are also
// presentation if they aren't focusable and have not explicit ARIA
// role (don't create accessibles for them unless they need to fire
@ -927,10 +953,8 @@ nsAccessibilityService::GetOrCreateAccessible(nsINode* aNode,
// We don't do this for <body>, <html>, <window>, <dialog> etc. which
// correspond to the doc accessible and will be created in any case
if (!newAcc && content->Tag() != nsGkAtoms::body && content->GetParent() &&
(frame->IsFocusable() ||
(isHTML && nsCoreUtils::HasClickListener(content)) ||
HasUniversalAriaProperty(content) || roleMapEntry ||
HasRelatedContent(content))) {
(roleMapEntry || MustBeAccessible(content, aDoc) ||
(isHTML && nsCoreUtils::HasClickListener(content)))) {
// This content is focusable or has an interesting dynamic content accessibility property.
// If it's interesting we need it in the accessibility hierarchy so that events or
// other accessibles can point to it, or so that it can hold a state, etc.
@ -1023,29 +1047,6 @@ nsAccessibilityService::Shutdown()
gApplicationAccessible = nullptr;
}
bool
nsAccessibilityService::HasUniversalAriaProperty(nsIContent *aContent)
{
// ARIA attributes that take token values (NMTOKEN, bool) are special cased
// because of special value "undefined" (see HasDefinedARIAToken).
return nsAccUtils::HasDefinedARIAToken(aContent, nsGkAtoms::aria_atomic) ||
nsAccUtils::HasDefinedARIAToken(aContent, nsGkAtoms::aria_busy) ||
aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_controls) ||
aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_describedby) ||
aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_disabled) ||
nsAccUtils::HasDefinedARIAToken(aContent, nsGkAtoms::aria_dropeffect) ||
aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_flowto) ||
nsAccUtils::HasDefinedARIAToken(aContent, nsGkAtoms::aria_grabbed) ||
nsAccUtils::HasDefinedARIAToken(aContent, nsGkAtoms::aria_haspopup) ||
aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden) ||
nsAccUtils::HasDefinedARIAToken(aContent, nsGkAtoms::aria_invalid) ||
aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label) ||
aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_labelledby) ||
nsAccUtils::HasDefinedARIAToken(aContent, nsGkAtoms::aria_live) ||
nsAccUtils::HasDefinedARIAToken(aContent, nsGkAtoms::aria_owns) ||
nsAccUtils::HasDefinedARIAToken(aContent, nsGkAtoms::aria_relevant);
}
already_AddRefed<Accessible>
nsAccessibilityService::CreateAccessibleByType(nsIContent* aContent,
DocAccessible* aDoc)

View File

@ -231,15 +231,6 @@ private:
*/
static bool gIsShutdown;
/**
* Does this content node have a universal ARIA property set on it?
* A universal ARIA property is one that can be defined on any element even if there is no role.
*
* @param aContent The content node to test
* @return true if there is a universal ARIA property set on the node
*/
bool HasUniversalAriaProperty(nsIContent *aContent);
friend nsAccessibilityService* GetAccService();
friend mozilla::a11y::FocusManager* mozilla::a11y::FocusMgr();
friend mozilla::a11y::ApplicationAccessible* mozilla::a11y::ApplicationAcc();

View File

@ -16,29 +16,46 @@
<script type="application/javascript">
function doTest()
{
var accTree = {
role: ROLE_GROUPING,
children: [
{ role: ROLE_TEXT_CONTAINER }, // pawn
{ role: ROLE_TEXT_CONTAINER }, // aria-atomic
{ role: ROLE_TEXT_CONTAINER }, // aria-busy
{ role: ROLE_TEXT_CONTAINER }, // aria-controls
{ role: ROLE_TEXT_CONTAINER }, // aria-describedby
{ role: ROLE_TEXT_CONTAINER }, // aria-disabled
{ role: ROLE_TEXT_CONTAINER }, // aria-dropeffect
{ role: ROLE_TEXT_CONTAINER }, // aria-flowto
{ role: ROLE_TEXT_CONTAINER }, // aria-grabbed
{ role: ROLE_TEXT_CONTAINER }, // aria-haspopup
{ role: ROLE_TEXT_CONTAINER }, // aria-invalid
{ role: ROLE_TEXT_CONTAINER }, // aria-label
{ role: ROLE_TEXT_CONTAINER }, // aria-labelledby
{ role: ROLE_TEXT_CONTAINER }, // aria-live
{ role: ROLE_TEXT_CONTAINER }, // aria-owns
{ role: ROLE_TEXT_CONTAINER } // aria-relevant
]
};
var globalIds = [
"atomic",
"busy",
"controls",
"describedby",
"disabled",
"dropeffect",
"flowto",
"grabbed",
"haspopup",
"hidden",
"invalid",
"label",
"labelledby",
"live",
"owns",
"relevant"
];
testAccessibleTree("global_aria_states_and_props", accTree);
// Elements having ARIA global state or properties or referred by another
// element must be accessible.
ok(isAccessible("pawn"),
"Must be accessible because referred by another elemnet.");
for (var idx = 0; idx < globalIds.length; idx++) {
ok(isAccessible(globalIds[idx]),
"Must be accessible becuase of " + "aria-" + globalIds[idx] +
" presence");
}
// Unfocusable elements, having ARIA global state or property with a valid
// IDREF value, and an inherited presentation role.
ok(!isAccessible("td_nothing"),
"inherited presentation role takes a place");
for (var idx = 0; idx < globalIds.length; idx++) {
ok(isAccessible("td_" + globalIds[idx]),
"Inherited presentation role must be ignored becuase of " +
"aria-" + globalIds[idx] + " presence");
}
SimpleTest.finish();
}
@ -54,6 +71,11 @@
href="https://bugzilla.mozilla.org/show_bug.cgi?id=551978">
Mozilla Bug 551978
</a>
<a target="_blank"
title="Presentational table related elements referred or having global ARIA attributes must be accessible"
href="https://bugzilla.mozilla.org/show_bug.cgi?id=809751">
Mozilla Bug 809751
</a>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test">
@ -72,10 +94,7 @@
<span id="flowto" aria-flowto="pawn"></span>
<span id="grabbed" aria-grabbed="false"></span>
<span id="haspopup" aria-haspopup="false"></span>
<!-- we use gecko to detect hidden-ness so we currently ignore aria-hidden
we might want to special case for aria-hidden="false", but we would need
a good case for it. aria-hidden is only in spec as an author aid to DOM
based AT -->
<span id="hidden" aria-hidden="true"></span>
<span id="invalid" aria-invalid="false"></span>
<span id="label" aria-label="hi"></span>
<span id="labelledby" aria-labelledby="label"></span>
@ -84,5 +103,26 @@
<span id="relevant" aria-relevant="additions"></span>
</div>
<table role="presentation">
<tr>
<td id="td_nothing"></td>
<td id="td_atomic" aria-atomic="true"></td>
<td id="td_busy" aria-busy="false"></td>
<td id="td_controls" aria-controls="pawn"></td>
<td id="td_describedby" aria-describedby="pawn"></td>
<td id="td_disabled" aria-disabled="true"></td>
<td id="td_dropeffect" aria-dropeffect="move"></td>
<td id="td_flowto" aria-flowto="pawn"></td>
<td id="td_grabbed" aria-grabbed="false"></td>
<td id="td_haspopup" aria-haspopup="false"></td>
<td id="td_hidden" aria-hidden="true"></td>
<td id="td_invalid" aria-invalid="false"></td>
<td id="td_label" aria-label="hi"></td>
<td id="td_labelledby" aria-labelledby="label"></td>
<td id="td_live" aria-live="polite"></td>
<td id="td_owns" aria-owns="pawn"></td>
<td id="td_relevant" aria-relevant="additions"></td>
</tr>
</table>
</body>
</html>