From 321a57847f69e3897c47617060d0628541c8e033 Mon Sep 17 00:00:00 2001 From: "aaronleventhal@moonset.net" Date: Sun, 11 Nov 2007 17:05:37 -0800 Subject: [PATCH] Bug 343137. Multiple ARIA role support inconsistent with ARIA spec. r=surkov, sr=neil, a=dsicore --- accessible/public/nsPIAccessible.idl | 14 +++- accessible/src/base/nsARIAMap.cpp | 13 +++- accessible/src/base/nsARIAMap.h | 2 + accessible/src/base/nsAccessNode.cpp | 61 +-------------- .../src/base/nsAccessibilityService.cpp | 20 +++-- accessible/src/base/nsAccessibilityService.h | 8 +- accessible/src/base/nsAccessibilityUtils.cpp | 78 ++++++++++++++++++- accessible/src/base/nsAccessibilityUtils.h | 17 ++++ accessible/src/base/nsAccessible.cpp | 46 +++++------ accessible/src/base/nsAccessible.h | 13 +++- 10 files changed, 177 insertions(+), 95 deletions(-) diff --git a/accessible/public/nsPIAccessible.idl b/accessible/public/nsPIAccessible.idl index 53c32889177..a5e935f9326 100644 --- a/accessible/public/nsPIAccessible.idl +++ b/accessible/public/nsPIAccessible.idl @@ -40,8 +40,12 @@ interface nsIAccessible; interface nsIAccessibleEvent; +%{C++ + struct nsRoleMapEntry; +%} +[ptr] native nsRoleMapEntryPtr(nsRoleMapEntry); -[uuid(af05f83c-6fd2-48c1-b1c3-811857472421)] +[uuid(2d552ed0-3f38-4c7f-94c1-419de4d693ed)] interface nsPIAccessible : nsISupports { /** @@ -107,5 +111,13 @@ interface nsPIAccessible : nsISupports */ void appendTextTo(out AString aString, in unsigned long aStartOffset, in unsigned long aLength); + + /** + * Set the ARIA role map entry for a new accessible. + * For a newly created accessible, specify which role map entry should be used. + * @param aRoleMapEntry The ARIA nsRoleMapEntry* for the accessible, or + * nsnull if none. + */ + void setRoleMapEntry(in nsRoleMapEntryPtr aRoleMapEntry); }; diff --git a/accessible/src/base/nsARIAMap.cpp b/accessible/src/base/nsARIAMap.cpp index 493799b3515..1e8e48a6c1b 100644 --- a/accessible/src/base/nsARIAMap.cpp +++ b/accessible/src/base/nsARIAMap.cpp @@ -151,6 +151,7 @@ nsRoleMapEntry nsARIAMap::gWAIRoleMap[] = {eAria_checked, kBoolState, nsIAccessibleStates::STATE_CHECKED | nsIAccessibleStates::STATE_CHECKABLE}, {eAria_checked, "mixed", nsIAccessibleStates::STATE_MIXED | nsIAccessibleStates::STATE_CHECKABLE}, {eAria_checked, "false", nsIAccessibleStates::STATE_CHECKABLE}, kEndEntry}, + {"presentation", nsIAccessibleRole::ROLE_NOTHING, eNameLabelOrTitle, eNoValue, kNoReqStates, kEndEntry}, {"progressbar", nsIAccessibleRole::ROLE_PROGRESSBAR, eNameLabelOrTitle, eHasValueMinMax, nsIAccessibleStates::STATE_READONLY, {eAria_disabled, kBoolState, nsIAccessibleStates::STATE_UNAVAILABLE}, kEndEntry}, {"radio", nsIAccessibleRole::ROLE_RADIOBUTTON, eNameOkFromChildren, eNoValue, kNoReqStates, @@ -211,7 +212,17 @@ nsRoleMapEntry nsARIAMap::gWAIRoleMap[] = {eAria_checked, kBoolState, nsIAccessibleStates::STATE_CHECKED | nsIAccessibleStates::STATE_CHECKABLE}, {eAria_checked, "mixed", nsIAccessibleStates::STATE_MIXED | nsIAccessibleStates::STATE_CHECKABLE}, {eAria_checked, "false", nsIAccessibleStates::STATE_CHECKABLE},}, - {nsnull, nsIAccessibleRole::ROLE_NOTHING, eNameLabelOrTitle, eNoValue, kNoReqStates, kEndEntry} // Last item +}; + +PRUint32 nsARIAMap::gWAIRoleMapLength = NS_ARRAY_LENGTH(nsARIAMap::gWAIRoleMap); + +nsRoleMapEntry nsARIAMap::gLandmarkRoleMap = { + "", + nsIAccessibleRole::ROLE_NOTHING, + eNameLabelOrTitle, + eNoValue, + kNoReqStates, + kEndEntry }; /** diff --git a/accessible/src/base/nsARIAMap.h b/accessible/src/base/nsARIAMap.h index 7894f313b7e..91755917b4a 100644 --- a/accessible/src/base/nsARIAMap.h +++ b/accessible/src/base/nsARIAMap.h @@ -134,6 +134,8 @@ struct nsARIAMap static nsIAtom** gAriaAtomPtrsNS[eAria_none]; static nsIAtom** gAriaAtomPtrsHyphenated[eAria_none]; static nsRoleMapEntry gWAIRoleMap[]; + static PRUint32 gWAIRoleMapLength; + static nsRoleMapEntry gLandmarkRoleMap; static nsStateMapEntry gWAIUnivStateMap[]; }; diff --git a/accessible/src/base/nsAccessNode.cpp b/accessible/src/base/nsAccessNode.cpp index e88a0178321..a9361e317bc 100755 --- a/accessible/src/base/nsAccessNode.cpp +++ b/accessible/src/base/nsAccessNode.cpp @@ -47,7 +47,6 @@ #include "nsIDocShellTreeItem.h" #include "nsIDocument.h" #include "nsIDocumentViewer.h" -#include "nsIDOM3Node.h" #include "nsIDOMCSSStyleDeclaration.h" #include "nsIDOMCSSPrimitiveValue.h" #include "nsIDOMDocument.h" @@ -844,66 +843,10 @@ nsAccessNode::GetARIARole(nsIContent *aContent, nsString& aRole) { aRole.Truncate(); - PRBool allowPrefixLookup = PR_TRUE; - if (aContent->IsNodeOfType(nsINode::eHTML)) { // Allow non-namespaced role attribute in HTML - if (!aContent->GetAttr(kNameSpaceID_None, nsAccessibilityAtoms::role, aRole)) { - return PR_FALSE; - } - nsCOMPtr doc(do_QueryInterface(aContent->GetDocument())); - if (doc) { - nsAutoString mimeType; - doc->GetContentType(mimeType); - if (mimeType.EqualsLiteral("text/html")) { - allowPrefixLookup = PR_FALSE; - } - } + return aContent->GetAttr(kNameSpaceID_None, nsAccessibilityAtoms::role, aRole); } // In non-HTML content, use XHTML namespaced-role attribute - else if (!aContent->GetAttr(kNameSpaceID_XHTML, nsAccessibilityAtoms::role, aRole)) { - return PR_FALSE; - } - - PRBool hasPrefix = (aRole.Find(":") >= 0); - - if (!hasPrefix) { - // * No prefix* -- not a QName - // Just return entire string as long as prefix is not currently required - return PR_TRUE; - } - - // Has prefix -- is a QName (role="prefix:rolename") - - // Check hardcoded 'wairole:' prefix - NS_NAMED_LITERAL_STRING(hardcodedWairolePrefix, "wairole:"); - if (StringBeginsWith(aRole, hardcodedWairolePrefix)) { - // The exact prefix "wairole:" is reserved to - // always indicate that we are using WAI roles. - aRole.Cut(0, hardcodedWairolePrefix.Length()); - return PR_TRUE; - } - - // Check for prefix mapped with xmlns:prefixname="" - nsAutoString prefix; - if (allowPrefixLookup) { // Not text/html, so we will try to find the WAIRole prefix - // QI to nsIDOM3Node causes some overhead. Unfortunately we need to do this each - // time there is a prefixed role attribute, because the prefix to namespace mappings - // can change within any subtree via the xmlns attribute - nsCOMPtr dom3Node(do_QueryInterface(aContent)); - if (dom3Node) { - // Look up exact prefix name for WAI Roles - NS_NAMED_LITERAL_STRING(kWAIRoles_Namespace, "http://www.w3.org/2005/01/wai-rdf/GUIRoleTaxonomy#"); - dom3Node->LookupPrefix(kWAIRoles_Namespace, prefix); - prefix += ':'; - PRUint32 length = prefix.Length(); - if (length > 1 && StringBeginsWith(aRole, prefix)) { - // Is a QName (role="prefix:rolename"), and prefix matches WAI Role prefix - // Trim the WAI Role prefix off - aRole.Cut(0, length); - } - } - } - - return PR_TRUE; + return aContent->GetAttr(kNameSpaceID_XHTML, nsAccessibilityAtoms::role, aRole); } diff --git a/accessible/src/base/nsAccessibilityService.cpp b/accessible/src/base/nsAccessibilityService.cpp index 92ded97cfde..66eda6d71d3 100644 --- a/accessible/src/base/nsAccessibilityService.cpp +++ b/accessible/src/base/nsAccessibilityService.cpp @@ -40,6 +40,7 @@ #include "nsAccessibilityAtoms.h" #include "nsAccessibilityService.h" #include "nsAccessibilityUtils.h" +#include "nsARIAMap.h" #include "nsCURILoader.h" #include "nsDocAccessible.h" #include "nsHTMLImageAccessibleWrap.h" @@ -1175,7 +1176,8 @@ NS_IMETHODIMP nsAccessibilityService::GetAccessibleInWeakShell(nsIDOMNode *aNode } nsresult nsAccessibilityService::InitAccessible(nsIAccessible *aAccessibleIn, - nsIAccessible **aAccessibleOut) + nsIAccessible **aAccessibleOut, + nsRoleMapEntry *aRoleMapEntry) { if (!aAccessibleIn) { return NS_ERROR_FAILURE; // No accessible to init @@ -1186,6 +1188,9 @@ nsresult nsAccessibilityService::InitAccessible(nsIAccessible *aAccessibleIn, NS_ASSERTION(privateAccessNode, "All accessibles must support nsPIAccessNode"); nsresult rv = privateAccessNode->Init(); // Add to cache, etc. if (NS_SUCCEEDED(rv)) { + nsCOMPtr privateAccessible = + do_QueryInterface(privateAccessNode); + privateAccessible->SetRoleMapEntry(aRoleMapEntry); NS_ADDREF(*aAccessibleOut = aAccessibleIn); } return rv; @@ -1350,12 +1355,13 @@ NS_IMETHODIMP nsAccessibilityService::GetAccessible(nsIDOMNode *aNode, return NS_OK; } frame->GetAccessible(getter_AddRefs(newAcc)); - return InitAccessible(newAcc, aAccessible); + return InitAccessible(newAcc, aAccessible, nsnull); } - nsAutoString role; - if (nsAccessNode::GetARIARole(content, role) && role.EqualsLiteral("presentation") && !content->IsFocusable()) { - // Only create accessible for role=":presentation" if it is focusable -- + nsRoleMapEntry *roleMapEntry = nsAccUtils::GetRoleMapEntry(aNode); + if (roleMapEntry && !nsCRT::strcmp(roleMapEntry->roleString, "presentation") && + !content->IsFocusable()) { // For presentation only + // Only create accessible for role of "presentation" if it is focusable -- // in that case we need an accessible in case it gets focused, we // don't want focus ever to be 'lost' return NS_OK; @@ -1449,7 +1455,7 @@ NS_IMETHODIMP nsAccessibilityService::GetAccessible(nsIDOMNode *aNode, if (!newAcc && content->Tag() != nsAccessibilityAtoms::body && content->GetParent() && (content->IsFocusable() || (isHTML && nsAccUtils::HasListener(content, NS_LITERAL_STRING("click"))) || - HasUniversalAriaProperty(content, aWeakShell) || !role.IsEmpty())) { + HasUniversalAriaProperty(content, aWeakShell) || roleMapEntry)) { // 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. @@ -1463,7 +1469,7 @@ NS_IMETHODIMP nsAccessibilityService::GetAccessible(nsIDOMNode *aNode, } } - return InitAccessible(newAcc, aAccessible); + return InitAccessible(newAcc, aAccessible, roleMapEntry); } PRBool diff --git a/accessible/src/base/nsAccessibilityService.h b/accessible/src/base/nsAccessibilityService.h index 82e0ad30eaa..bfff2758ef6 100644 --- a/accessible/src/base/nsAccessibilityService.h +++ b/accessible/src/base/nsAccessibilityService.h @@ -51,6 +51,7 @@ class nsObjectFrame; class nsIDocShell; class nsIPresShell; class nsIContent; +struct nsRoleMapEntry; class nsAccessibilityService : public nsIAccessibilityService, public nsIObserver, @@ -99,8 +100,13 @@ private: * every created accessible. * * @param aAccessibleIn - accessible to initialize. + * @param aAcccessibleOut - set to the same thing as aAccessibleIn, unless there was + * an error initializing the accessible, in which case + * it is set to nsnull + * @param aRoleMapEntry - The role map entry role the ARIA role or nsnull if none */ - nsresult InitAccessible(nsIAccessible *aAccessibleIn, nsIAccessible **aAccessibleOut); + nsresult InitAccessible(nsIAccessible *aAccessibleIn, nsIAccessible **aAccessibleOut, + nsRoleMapEntry *aRoleMapEntry = nsnull); /** * Return accessible object for elements implementing nsIAccessibleProvider diff --git a/accessible/src/base/nsAccessibilityUtils.cpp b/accessible/src/base/nsAccessibilityUtils.cpp index 2117de93456..f78b880d36a 100755 --- a/accessible/src/base/nsAccessibilityUtils.cpp +++ b/accessible/src/base/nsAccessibilityUtils.cpp @@ -48,6 +48,7 @@ #include "nsARIAMap.h" #include "nsIDocument.h" #include "nsIDOMAbstractView.h" +#include "nsIDOM3Node.h" #include "nsIDOMDocument.h" #include "nsIDOMDocumentView.h" #include "nsIDOMDocumentXBL.h" @@ -67,6 +68,7 @@ #include "nsContentCID.h" #include "nsComponentManagerUtils.h" #include "nsIInterfaceRequestorUtils.h" +#include "nsWhitespaceTokenizer.h" static NS_DEFINE_IID(kRangeCID, NS_RANGE_CID); @@ -194,7 +196,7 @@ nsAccUtils::SetAccAttrsForXULSelectControlItem(nsIDOMNode *aNode, if (!itemAcc || nsAccessible::State(itemAcc) & nsIAccessibleStates::STATE_INVISIBLE) { setSize--; - if (index < indexOf) + if (index < static_cast(indexOf)) posInSet--; } } @@ -755,3 +757,77 @@ nsAccUtils::FindDescendantPointingToIDImpl(nsCString& aIdWithSpaces, return nsnull; } +const char * +nsAccUtils::TrimmedRole(const char *aRole, nsIContent *aContent) +{ + const char kWaiRolePrefix[] = "wairole:"; + const PRUint32 kWaiRolePrefixLen = NS_ARRAY_LENGTH(kWaiRolePrefix) - 1; + + if (!PL_strncmp(aRole, kWaiRolePrefix, kWaiRolePrefixLen)) { + return aRole + kWaiRolePrefixLen; + } +//#ifdef ALLOW_PREFIX_LOOKUP + // Check if prefix was mapped via xmlns:[prefix] to the official WAI role namespace + char *colon = PL_strchr(aRole, ':'); + if (colon) { + nsCOMPtr dom3Node = do_QueryInterface(aContent); + if (dom3Node) { + // Look up exact prefix name for WAI Roles + nsAutoString prefix; + NS_NAMED_LITERAL_STRING(kWAIRoles_Namespace, "http://www.w3.org/2005/01/wai-rdf/GUIRoleTaxonomy#"); + dom3Node->LookupPrefix(kWAIRoles_Namespace, prefix); + prefix += ':'; + PRUint32 prefixLength = colon - aRole + 1; + + if (!PL_strncmp(aRole, NS_LossyConvertUTF16toASCII(prefix).get(), prefixLength)) { + // Is a QName (role="prefix:rolename"), and prefix is mapped to WAI Role namespace + // Trim the prefix off + return aRole + prefixLength; + } + } + } + + return aRole; +} + +nsRoleMapEntry* +nsAccUtils::GetRoleMapEntry(nsIDOMNode *aNode) +{ + nsIContent *content = nsAccessible::GetRoleContent(aNode); + if (!content) { + return nsnull; + } + nsAutoString roleString; + if (!nsAccessNode::GetARIARole(content, roleString)) { + return nsnull; + } + + nsWhitespaceTokenizer tokenizer(roleString); + while (tokenizer.hasMoreTokens()) { + // Do a binary search through table for the next role in role list + const char *rawRole = NS_LossyConvertUTF16toASCII(tokenizer.nextToken()).get(); + const char *trimmedRole = TrimmedRole(rawRole, content); +//#endif + PRInt32 low = 0; + PRInt32 high = nsARIAMap::gWAIRoleMapLength; + while (low <= high) { + PRInt32 index = low + ((high - low) / 2); + PRInt32 compare = PL_strcmp(trimmedRole, nsARIAMap::gWAIRoleMap[index].roleString); + if (compare == 0) { + // The role attribute maps to an entry in the role table + return &nsARIAMap::gWAIRoleMap[index]; + } + if (compare < 0) { + high = index - 1; + } + else { + low = index + 1; + } + } + } + + // Always use some entry if there is a role string + // To ensure an accessible object is created + return &nsARIAMap::gLandmarkRoleMap; +} + diff --git a/accessible/src/base/nsAccessibilityUtils.h b/accessible/src/base/nsAccessibilityUtils.h index e6da866238e..d2baab4f7cb 100755 --- a/accessible/src/base/nsAccessibilityUtils.h +++ b/accessible/src/base/nsAccessibilityUtils.h @@ -272,6 +272,23 @@ public: EAriaProperty aProperty, nsAString& aValue, PRUint32 aCheckFlags = 0); + /** + * Given a role string, return the role with any WAI role prefix trimmed off + * @param aRole The role to start with + * @param aContent What content nodes the role is set on + * @return The entire role if there is no prefix that is a WAI role prefix, + * or the role without the prefix, if it was mapped to WAI roles + */ + static const char *TrimmedRole(const char *aRole, nsIContent *aContent); + + /** + * Get the role map entry for a given DOM node. This will use the first + * ARIA role if the role attribute provides a space delimited list of roles. + * @param aNode The DOM node to get the role map entry for + * @return A pointer to the role map entry for the ARIA role, or nsnull if none + */ + static nsRoleMapEntry* GetRoleMapEntry(nsIDOMNode *aNode); + /** * Search element in neighborhood of the given element by tag name and * attribute value that equals to ID attribute of the given element. diff --git a/accessible/src/base/nsAccessible.cpp b/accessible/src/base/nsAccessible.cpp index 4d0375973a8..dac596a0cf1 100644 --- a/accessible/src/base/nsAccessible.cpp +++ b/accessible/src/base/nsAccessible.cpp @@ -85,6 +85,7 @@ #include "nsIMutableArray.h" #include "nsIObserverService.h" #include "nsIServiceManager.h" +#include "nsWhitespaceTokenizer.h" #ifdef NS_DEBUG #include "nsIFrameDebug.h" @@ -272,6 +273,12 @@ nsAccessible::~nsAccessible() { } +NS_IMETHODIMP nsAccessible::SetRoleMapEntry(nsRoleMapEntry* aRoleMapEntry) +{ + mRoleMapEntry = aRoleMapEntry; + return NS_OK; +} + NS_IMETHODIMP nsAccessible::GetName(nsAString& aName) { aName.Truncate(); @@ -487,27 +494,6 @@ NS_IMETHODIMP nsAccessible::SetNextSibling(nsIAccessible *aNextSibling) return NS_OK; } -NS_IMETHODIMP nsAccessible::Init() -{ - nsIContent *content = GetRoleContent(mDOMNode); - nsAutoString roleString; - if (content && GetARIARole(content, roleString)) { - nsCString utf8Role = NS_ConvertUTF16toUTF8(roleString); // For easy comparison - ToLowerCase(utf8Role); - PRUint32 index; - for (index = 0; nsARIAMap::gWAIRoleMap[index].roleString; index ++) { - if (utf8Role.Equals(nsARIAMap::gWAIRoleMap[index].roleString)) { - break; // The dynamic role attribute maps to an entry in our table - } - } - // Always use some entry if there is a role string - // If no match, we use the last entry which maps to ROLE_NOTHING - mRoleMapEntry = &nsARIAMap::gWAIRoleMap[index]; - } - - return nsAccessNodeWrap::Init(); -} - nsIContent *nsAccessible::GetRoleContent(nsIDOMNode *aDOMNode) { // Given the DOM node for an acessible, return content node that @@ -2042,7 +2028,23 @@ nsAccessible::GetAttributes(nsIPersistentProperties **aAttributes) // through this attribute nsAutoString xmlRole; if (GetARIARole(content, xmlRole)) { - attributes->SetStringProperty(NS_LITERAL_CSTRING("xml-roles"), xmlRole, oldValueUnused); + nsWhitespaceTokenizer tokenizer(xmlRole); + nsAutoString trimmedRoles; + while (tokenizer.hasMoreTokens()) { + // Trim off prefixes for WAI roles so they are easier for ATs to recognize -- + // they will always appear the same, and the AT need not understand prefixes + const char *rawRole = NS_LossyConvertUTF16toASCII(tokenizer.nextToken()).get(); + const char *trimmedRole = nsAccUtils::TrimmedRole(rawRole, content); + if (*trimmedRole) { + if (!trimmedRoles.IsEmpty()) { + trimmedRoles.AppendLiteral(" "); + } + trimmedRoles.Append(NS_ConvertASCIItoUTF16(trimmedRole)); + } + } + if (!trimmedRoles.IsEmpty()) { + attributes->SetStringProperty(NS_LITERAL_CSTRING("xml-roles"), trimmedRoles, oldValueUnused); + } } // Make sure to keep these two arrays in sync diff --git a/accessible/src/base/nsAccessible.h b/accessible/src/base/nsAccessible.h index 8ae21d6b59e..1eab1313a2c 100644 --- a/accessible/src/base/nsAccessible.h +++ b/accessible/src/base/nsAccessible.h @@ -1,4 +1,4 @@ - /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * @@ -114,7 +114,6 @@ public: NS_DECL_NSIACCESSIBLEVALUE // nsIAccessNode - NS_IMETHOD Init(); NS_IMETHOD Shutdown(); /** @@ -161,6 +160,15 @@ public: return parent; } + /** + * Return the nsIContent* to check for ARIA attributes on -- this may not always + * be the DOM node for the accessible. Specifically, for doc accessibles, it is not + * the document node, but either the root element or in HTML. + * @param aDOMNode The DOM node for the accessible that may be affected by ARIA + * @return The nsIContent which may have ARIA markup + */ + static nsIContent *GetRoleContent(nsIDOMNode *aDOMNode); + protected: PRBool MappedAttrState(nsIContent *aContent, PRUint32 *aStateInOut, nsStateMapEntry *aStateMapEntry); virtual nsIFrame* GetBoundsFrame(); @@ -181,7 +189,6 @@ protected: static nsIContent *GetHTMLLabelContent(nsIContent *aForNode); static nsIContent *GetLabelContent(nsIContent *aForNode); - static nsIContent *GetRoleContent(nsIDOMNode *aDOMNode); // Name helpers nsresult GetHTMLName(nsAString& _retval, PRBool aCanAggregateSubtree = PR_TRUE);