☠☠ backed out by 53f03c928ac6 ☠ ☠ | |
author | Morgan Reschenberg <mreschenberg@mozilla.com> |
Wed, 27 May 2020 19:23:32 +0000 | |
changeset 532584 | 156d42f01488fb24914199fe4670c70cfffe4323 |
parent 532583 | 820ae8ff4e38a8eb15143684a1144ebcae0c48b2 |
child 532585 | a8499f9c0d024b7287f3e0cb13bc56f5f3f57753 |
push id | 117249 |
push user | mreschenberg@mozilla.com |
push date | Wed, 27 May 2020 19:26:54 +0000 |
treeherder | autoland@156d42f01488 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | eeejay |
bugs | 1624909 |
milestone | 78.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/accessible/base/Pivot.cpp +++ b/accessible/base/Pivot.cpp @@ -528,8 +528,26 @@ Accessible* Pivot::AtPoint(int32_t aX, i } } child = child->Parent(); } return match; } + +// Role Rule + +PivotRoleRule::PivotRoleRule(mozilla::a11y::role aRole) : mRole(aRole) {} + +uint16_t PivotRoleRule::Match(Accessible* aAccessible) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (nsAccUtils::MustPrune(aAccessible)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (aAccessible->Role() == mRole) { + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return result; +}
--- a/accessible/base/Pivot.h +++ b/accessible/base/Pivot.h @@ -78,12 +78,25 @@ class Pivot final { bool aSearchCurrent); // Search in preorder for the first text accessible. HyperTextAccessible* SearchForText(Accessible* aAnchor, bool aBackward); Accessible* mRoot; }; +/** + * This rule matches accessibles on a given role. + */ +class PivotRoleRule final : public PivotRule { + public: + explicit PivotRoleRule(mozilla::a11y::role aRole); + + virtual uint16_t Match(Accessible* aAccessible) override; + + private: + mozilla::a11y::role mRole; +}; + } // namespace a11y } // namespace mozilla #endif // mozilla_a11y_Pivot_h_
--- a/accessible/generic/Accessible.cpp +++ b/accessible/generic/Accessible.cpp @@ -14,16 +14,17 @@ #include "nsAccessiblePivot.h" #include "nsGenericHTMLElement.h" #include "NotificationController.h" #include "nsEventShell.h" #include "nsTextEquivUtils.h" #include "DocAccessibleChild.h" #include "EventTree.h" #include "GeckoProfiler.h" +#include "Pivot.h" #include "Relation.h" #include "Role.h" #include "RootAccessible.h" #include "States.h" #include "StyleInfo.h" #include "TableAccessible.h" #include "TableCellAccessible.h" #include "TreeWalker.h" @@ -1677,18 +1678,49 @@ Relation Accessible::RelationByType(Rela case RelationType::FLOWS_TO: return Relation( new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_flowto)); case RelationType::FLOWS_FROM: return Relation( new RelatedAccIterator(Document(), mContent, nsGkAtoms::aria_flowto)); - case RelationType::MEMBER_OF: + case RelationType::MEMBER_OF: { + if (Role() == roles::RADIOBUTTON) { + /* If we see a radio button role here, we're dealing with an aria + * radio button (because input=radio buttons are + * HTMLRadioButtonAccessibles) */ + Relation rel = Relation(); + Accessible* currParent = Parent(); + while (currParent && currParent->Role() != roles::RADIO_GROUP) { + currParent = currParent->Parent(); + } + + if (currParent && currParent->Role() == roles::RADIO_GROUP) { + /* If we found a radiogroup parent, search for all + * roles::RADIOBUTTON children and add them to our relation. + * This search will include the radio button this method + * was called from, which is expected. */ + Pivot p = Pivot(currParent); + PivotRoleRule rule(roles::RADIOBUTTON); + Accessible* match = currParent; + while ((match = p.Next(match, rule))) { + rel.AppendTarget(match); + } + } + + /* By webkit's standard, aria radio buttons do not get grouped + * if they lack a group parent, so we return an empty + * relation here if the above check fails. */ + + return rel; + } + return Relation(mDoc, GetAtomicRegion()); + } case RelationType::SUBWINDOW_OF: case RelationType::EMBEDS: case RelationType::EMBEDDED_BY: case RelationType::POPUP_FOR: case RelationType::PARENT_WINDOW_OF: return Relation();
--- a/accessible/html/HTMLFormControlAccessible.cpp +++ b/accessible/html/HTMLFormControlAccessible.cpp @@ -65,16 +65,22 @@ uint64_t HTMLRadioButtonAccessible::Nati HTMLInputElement* input = HTMLInputElement::FromNode(mContent); if (input && input->Checked()) state |= states::CHECKED; return state; } void HTMLRadioButtonAccessible::GetPositionAndSizeInternal(int32_t* aPosInSet, int32_t* aSetSize) { + Unused << ComputeGroupAttributes(aPosInSet, aSetSize); +} + +Relation HTMLRadioButtonAccessible::ComputeGroupAttributes( + int32_t* aPosInSet, int32_t* aSetSize) const { + Relation rel = Relation(); int32_t namespaceId = mContent->NodeInfo()->NamespaceID(); nsAutoString tagName; mContent->NodeInfo()->GetName(tagName); nsAutoString type; mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::type, type); nsAutoString name; mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::name, name); @@ -82,38 +88,49 @@ void HTMLRadioButtonAccessible::GetPosit RefPtr<nsContentList> inputElms; nsCOMPtr<nsIFormControl> formControlNode(do_QueryInterface(mContent)); dom::Element* formElm = formControlNode->GetFormElement(); if (formElm) inputElms = NS_GetContentList(formElm, namespaceId, tagName); else inputElms = NS_GetContentList(mContent->OwnerDoc(), namespaceId, tagName); - NS_ENSURE_TRUE_VOID(inputElms); + NS_ENSURE_TRUE(inputElms, rel); uint32_t inputCount = inputElms->Length(false); // Compute posinset and setsize. int32_t indexOf = 0; int32_t count = 0; for (uint32_t index = 0; index < inputCount; index++) { nsIContent* inputElm = inputElms->Item(index, false); if (inputElm->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, type, eCaseMatters) && inputElm->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name, name, eCaseMatters) && mDoc->HasAccessible(inputElm)) { count++; + rel.AppendTarget(mDoc->GetAccessible(inputElm)); if (inputElm == mContent) indexOf = count; } } *aPosInSet = indexOf; *aSetSize = count; + return rel; +} + +Relation HTMLRadioButtonAccessible::RelationByType(RelationType aType) const { + if (aType == RelationType::MEMBER_OF) { + int32_t unusedPos, unusedSetSize; + return ComputeGroupAttributes(&unusedPos, &unusedSetSize); + } + + return Accessible::RelationByType(aType); } //////////////////////////////////////////////////////////////////////////////// // HTMLButtonAccessible //////////////////////////////////////////////////////////////////////////////// HTMLButtonAccessible::HTMLButtonAccessible(nsIContent* aContent, DocAccessible* aDoc)
--- a/accessible/html/HTMLFormControlAccessible.h +++ b/accessible/html/HTMLFormControlAccessible.h @@ -4,16 +4,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef MOZILLA_A11Y_HTMLFormControlAccessible_H_ #define MOZILLA_A11Y_HTMLFormControlAccessible_H_ #include "FormControlAccessible.h" #include "HyperTextAccessibleWrap.h" #include "nsAccUtils.h" +#include "Relation.h" namespace mozilla { class TextEditor; namespace a11y { /** * Accessible for HTML input@type="radio" element. */ @@ -25,16 +26,20 @@ class HTMLRadioButtonAccessible : public // state change notification. mStateFlags |= eIgnoreDOMUIEvent; } // Accessible virtual uint64_t NativeState() const override; virtual void GetPositionAndSizeInternal(int32_t* aPosInSet, int32_t* aSetSize) override; + virtual Relation RelationByType(RelationType aType) const override; + + private: + Relation ComputeGroupAttributes(int32_t* aPosInSet, int32_t* aSetSize) const; }; /** * Accessible for HTML input@type="button", @type="submit", @type="image" * and HTML button elements. */ class HTMLButtonAccessible : public HyperTextAccessibleWrap { public:
--- a/accessible/mac/AccessibleWrap.mm +++ b/accessible/mac/AccessibleWrap.mm @@ -176,18 +176,20 @@ Class a11y::GetTypeFromRole(roles::Role case roles::PUSHBUTTON: return [mozButtonAccessible class]; case roles::PAGETAB: return [mozTabAccessible class]; case roles::CHECKBUTTON: case roles::TOGGLE_BUTTON: + return [mozCheckboxAccessible class]; + case roles::RADIOBUTTON: - return [mozCheckboxAccessible class]; + return [mozRadioButtonAccessible class]; case roles::SPINBUTTON: case roles::SLIDER: return [mozIncrementableAccessible class]; case roles::HEADING: return [mozHeadingAccessible class];
--- a/accessible/mac/mozActionElements.h +++ b/accessible/mac/mozActionElements.h @@ -16,16 +16,21 @@ @interface mozPopupButtonAccessible : mozButtonAccessible @end @interface mozCheckboxAccessible : mozButtonAccessible // returns one of the constants defined in CheckboxValue - (int)isChecked; @end +// Accessible for a radio button +@interface mozRadioButtonAccessible : mozCheckboxAccessible +- (id)accessibilityAttributeValue:(NSString*)attribute; +@end + /** * Accessible for a PANE */ @interface mozPaneAccessible : mozAccessible @end /**
--- a/accessible/mac/mozActionElements.mm +++ b/accessible/mac/mozActionElements.mm @@ -4,16 +4,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #import "mozActionElements.h" #import "MacUtils.h" #include "Accessible-inl.h" #include "DocAccessible.h" #include "XULTabAccessible.h" +#include "HTMLFormControlAccessible.h" #include "nsDeckFrame.h" #include "nsObjCExceptions.h" using namespace mozilla::a11y; enum CheckboxValue { // these constants correspond to the values in the OS @@ -149,16 +150,48 @@ enum CheckboxValue { } } return [super ignoreWithParent:parent]; } @end +@implementation mozRadioButtonAccessible + +- (id)accessibilityAttributeValue:(NSString*)attribute { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + if ([self isExpired]) { + return nil; + } + + if ([attribute isEqualToString:NSAccessibilityLinkedUIElementsAttribute]) { + if (HTMLRadioButtonAccessible* radioAcc = + (HTMLRadioButtonAccessible*)mGeckoAccessible.AsAccessible()) { + NSMutableArray* radioSiblings = [NSMutableArray new]; + Relation rel = radioAcc->RelationByType(RelationType::MEMBER_OF); + Accessible* tempAcc; + while ((tempAcc = rel.Next())) { + [radioSiblings addObject:GetNativeFromGeckoAccessible(tempAcc)]; + } + return radioSiblings; + } else { + ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); + nsTArray<ProxyAccessible*> accs = proxy->RelationByType(RelationType::MEMBER_OF); + return utils::ConvertToNSArray(accs); + } + } + + return [super accessibilityAttributeValue:attribute]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +@end + @implementation mozCheckboxAccessible - (int)isChecked { // check if we're checked or in a mixed state uint64_t state = [self stateWithMask:(states::CHECKED | states::PRESSED | states::MIXED)]; if (state & (states::CHECKED | states::PRESSED)) { return kChecked; }
--- a/accessible/tests/browser/mac/browser.ini +++ b/accessible/tests/browser/mac/browser.ini @@ -12,14 +12,15 @@ support-files = [browser_app.js] [browser_aria_current.js] [browser_details_summary.js] [browser_label_title.js] [browser_range.js] [browser_roles_elements.js] [browser_table.js] [browser_selectables.js] +[browser_radio_position.js] [browser_toggle_radio_check.js] [browser_link.js] [browser_aria_haspopup.js] [browser_required.js] [browser_popupbutton.js] [browser_mathml.js]
new file mode 100644 --- /dev/null +++ b/accessible/tests/browser/mac/browser_radio_position.js @@ -0,0 +1,321 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +function getChildRoles(parent) { + return parent + .getAttributeValue("AXChildren") + .map(c => c.getAttributeValue("AXRole")); +} + +function getLinkedTitles(element) { + return element + .getAttributeValue("AXLinkedUIElements") + .map(c => c.getAttributeValue("AXTitle")); +} + +/** + * Test radio group + */ +addAccessibleTask( + `<div role="radiogroup" id="radioGroup"> + <div role="radio" + id="radioGroupItem1"> + Regular crust + </div> + <div role="radio" + id="radioGroupItem2"> + Deep dish + </div> + <div role="radio" + id="radioGroupItem3"> + Thin crust + </div> + </div>`, + async (browser, accDoc) => { + let item1 = getNativeInterface(accDoc, "radioGroupItem1"); + let item2 = getNativeInterface(accDoc, "radioGroupItem2"); + let item3 = getNativeInterface(accDoc, "radioGroupItem3"); + let titleList = ["Regular crust", "Deep dish", "Thin crust"]; + + Assert.deepEqual( + titleList, + [item1, item2, item3].map(c => c.getAttributeValue("AXTitle")), + "Title list matches" + ); + + let linkedElems = item1.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Item 1 has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item1), + titleList, + "Item one has correctly ordered linked elements" + ); + + linkedElems = item2.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Item 2 has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item2), + titleList, + "Item two has correctly ordered linked elements" + ); + + linkedElems = item3.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Item 3 has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item3), + titleList, + "Item three has correctly ordered linked elements" + ); + } +); + +/** + * Test dynamic add to a radio group + */ +addAccessibleTask( + `<div role="radiogroup" id="radioGroup"> + <div role="radio" + id="radioGroupItem1"> + Option One + </div> + </div>`, + async (browser, accDoc) => { + let item1 = getNativeInterface(accDoc, "radioGroupItem1"); + let linkedElems = item1.getAttributeValue("AXLinkedUIElements"); + + is(linkedElems.length, 1, "Item 1 has one linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + item1.getAttributeValue("AXTitle"), + "Item 1 is first element" + ); + + let reorder = waitForEvent(EVENT_REORDER, "radioGroup"); + await SpecialPowers.spawn(browser, [], () => { + let d = content.document.createElement("div"); + d.setAttribute("role", "radio"); + content.document.getElementById("radioGroup").appendChild(d); + }); + await reorder; + + let radioGroup = getNativeInterface(accDoc, "radioGroup"); + let groupMembers = radioGroup.getAttributeValue("AXChildren"); + is(groupMembers.length, 2, "Radio group has two members"); + let item2 = groupMembers[1]; + item1 = getNativeInterface(accDoc, "radioGroupItem1"); + let titleList = ["Option One", ""]; + + Assert.deepEqual( + titleList, + [item1, item2].map(c => c.getAttributeValue("AXTitle")), + "Title list matches" + ); + + linkedElems = item1.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "Item 1 has two linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item1), + titleList, + "Item one has correctly ordered linked elements" + ); + + linkedElems = item2.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "Item 2 has two linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item2), + titleList, + "Item two has correctly ordered linked elements" + ); + } +); + +/** + * Test input[type=radio] for single group + */ +addAccessibleTask( + `<input type="radio" id="cat" name="animal"><label for="cat">Cat</label> + <input type="radio" id="dog" name="animal"><label for="dog">Dog</label> + <input type="radio" id="catdog" name="animal"><label for="catdog">CatDog</label>`, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let dog = getNativeInterface(accDoc, "dog"); + let catdog = getNativeInterface(accDoc, "catdog"); + let titleList = ["Cat", "Dog", "CatDog"]; + + Assert.deepEqual( + titleList, + [cat, dog, catdog].map(x => x.getAttributeValue("AXTitle")), + "Title list matches" + ); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Cat has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(cat), + titleList, + "Cat has correctly ordered linked elements" + ); + + linkedElems = dog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Dog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(dog), + titleList, + "Dog has correctly ordered linked elements" + ); + + linkedElems = catdog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Catdog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(catdog), + titleList, + "catdog has correctly ordered linked elements" + ); + } +); + +/** + * Test input[type=radio] for different groups + */ +addAccessibleTask( + `<input type="radio" id="cat" name="one"><label for="cat">Cat</label> + <input type="radio" id="dog" name="two"><label for="dog">Dog</label> + <input type="radio" id="catdog"><label for="catdog">CatDog</label>`, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let dog = getNativeInterface(accDoc, "dog"); + let catdog = getNativeInterface(accDoc, "catdog"); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 1, "Cat has one linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + cat.getAttributeValue("AXTitle"), + "Cat is only element" + ); + + linkedElems = dog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 1, "Dog has one linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + dog.getAttributeValue("AXTitle"), + "Dog is only element" + ); + + linkedElems = catdog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 0, "Catdog has no linked UI elem"); + } +); + +/** + * Test input[type=radio] for single group across DOM + */ +addAccessibleTask( + `<input type="radio" id="cat" name="animal"><label for="cat">Cat</label> + <div> + <span> + <input type="radio" id="dog" name="animal"><label for="dog">Dog</label> + </span> + </div> + <div> + <input type="radio" id="catdog" name="animal"><label for="catdog">CatDog</label> + </div>`, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let dog = getNativeInterface(accDoc, "dog"); + let catdog = getNativeInterface(accDoc, "catdog"); + let titleList = ["Cat", "Dog", "CatDog"]; + + Assert.deepEqual( + titleList, + [cat, dog, catdog].map(x => x.getAttributeValue("AXTitle")), + "Title list matches" + ); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Cat has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(cat), + titleList, + "cat has correctly ordered linked elements" + ); + + linkedElems = dog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Dog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(dog), + titleList, + "dog has correctly ordered linked elements" + ); + + linkedElems = catdog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Catdog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(catdog), + titleList, + "catdog has correctly ordered linked elements" + ); + } +); + +/** + * Test dynamic add of input[type=radio] in a single group + */ +addAccessibleTask( + `<div id="container"><input type="radio" id="cat" name="animal"></div>`, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let container = getNativeInterface(accDoc, "container"); + + let containerChildren = container.getAttributeValue("AXChildren"); + is(containerChildren.length, 1, "container has one button"); + is( + containerChildren[0].getAttributeValue("AXRole"), + "AXRadioButton", + "Container child is radio button" + ); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 1, "Cat has 1 linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + cat.getAttributeValue("AXTitle"), + "Cat is first element" + ); + let reorder = waitForEvent(EVENT_REORDER, "container"); + await SpecialPowers.spawn(browser, [], () => { + let input = content.document.createElement("input"); + input.setAttribute("type", "radio"); + input.setAttribute("name", "animal"); + content.document.getElementById("container").appendChild(input); + }); + await reorder; + + container = getNativeInterface(accDoc, "container"); + containerChildren = container.getAttributeValue("AXChildren"); + + is(containerChildren.length, 2, "container has two children"); + + Assert.deepEqual( + getChildRoles(container), + ["AXRadioButton", "AXRadioButton"], + "Both children are radio buttons" + ); + + linkedElems = containerChildren[0].getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "Cat has 2 linked elements"); + + linkedElems = containerChildren[1].getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "New button has 2 linked elements"); + } +);