From b99c8c44b6678de351d29fad152128e6147fc161 Mon Sep 17 00:00:00 2001 From: Morgan Reschenberg Date: Mon, 14 Sep 2020 21:22:50 +0000 Subject: [PATCH] Bug 1662147: Add AXControlSearchKey to rotor r=eeejay Differential Revision: https://phabricator.services.mozilla.com/D89073 --- accessible/base/AccessibleOrProxy.h | 2 +- accessible/base/MarkupMap.h | 2 +- accessible/base/Role.h | 7 +- accessible/base/RoleMap.h | 12 +- accessible/interfaces/nsIAccessibleRole.idl | 5 + accessible/mac/MOXWebAreaAccessible.mm | 8 +- accessible/mac/RotorRules.h | 11 + accessible/mac/RotorRules.mm | 68 +++ accessible/tests/browser/mac/browser_rotor.js | 415 ++++++++++++++++++ accessible/tests/mochitest/role.js | 1 + 10 files changed, 526 insertions(+), 5 deletions(-) diff --git a/accessible/base/AccessibleOrProxy.h b/accessible/base/AccessibleOrProxy.h index 9f95333c9b9c..415ce7273d3c 100644 --- a/accessible/base/AccessibleOrProxy.h +++ b/accessible/base/AccessibleOrProxy.h @@ -70,7 +70,7 @@ class AccessibleOrProxy { * Return the child object either an accessible or a proxied accessible at * the given index. */ - AccessibleOrProxy ChildAt(uint32_t aIdx) { + AccessibleOrProxy ChildAt(uint32_t aIdx) const { if (IsProxy()) { return AsProxy()->ChildAt(aIdx); } diff --git a/accessible/base/MarkupMap.h b/accessible/base/MarkupMap.h index 1a0e06cf87c9..e529c809d199 100644 --- a/accessible/base/MarkupMap.h +++ b/accessible/base/MarkupMap.h @@ -214,7 +214,7 @@ MARKUPMAP( } if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::time, eIgnoreCase)) { - return new HTMLDateTimeAccessible( + return new HTMLDateTimeAccessible( aElement, aContext->Document()); } if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, diff --git a/accessible/base/Role.h b/accessible/base/Role.h index b37e70e0ebfe..933bf314be9d 100644 --- a/accessible/base/Role.h +++ b/accessible/base/Role.h @@ -1066,7 +1066,12 @@ enum Role { */ CODE = 182, - LAST_ROLE = CODE + /** + * Represents control whose purpose is to allow a user to edit a time. + */ + TIME_EDITOR = 183, + + LAST_ROLE = TIME_EDITOR }; } // namespace roles diff --git a/accessible/base/RoleMap.h b/accessible/base/RoleMap.h index ae01a0d29876..9130b93c2a77 100644 --- a/accessible/base/RoleMap.h +++ b/accessible/base/RoleMap.h @@ -711,7 +711,7 @@ ROLE(COLOR_CHOOSER, ROLE(DATE_EDITOR, "date editor", ATK_ROLE_DATE_EDITOR, - NSAccessibilityUnknownRole, + @"AXDateField", NSAccessibilityUnknownSubrole, USE_ROLE_STRING, IA2_ROLE_DATE_EDITOR, @@ -1856,4 +1856,14 @@ ROLE(CODE, IA2_ROLE_TEXT_FRAME, java::SessionAccessibility::CLASSNAME_VIEW, eNoNameRule) + +ROLE(TIME_EDITOR, + "time editor", + ATK_ROLE_PANEL, + @"AXTimeField", + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) // clang-format on diff --git a/accessible/interfaces/nsIAccessibleRole.idl b/accessible/interfaces/nsIAccessibleRole.idl index 8053c6ffd888..a7f3de1b2c0e 100644 --- a/accessible/interfaces/nsIAccessibleRole.idl +++ b/accessible/interfaces/nsIAccessibleRole.idl @@ -1060,4 +1060,9 @@ interface nsIAccessibleRole : nsISupports */ const unsigned long ROLE_CODE = 182; + /** + * Represents control whose purpose is to allow a user to edit a time. + */ + const unsigned long ROLE_TIME_EDITOR = 183; + }; diff --git a/accessible/mac/MOXWebAreaAccessible.mm b/accessible/mac/MOXWebAreaAccessible.mm index 592f8e6ff406..30778cadef5c 100644 --- a/accessible/mac/MOXWebAreaAccessible.mm +++ b/accessible/mac/MOXWebAreaAccessible.mm @@ -76,7 +76,6 @@ using namespace mozilla::a11y; MOXSearchInfo* search = [[MOXSearchInfo alloc] initWithParameters:searchPredicate andRoot:mGeckoAccessible]; - return [search performSearch]; } @@ -205,6 +204,13 @@ using namespace mozilla::a11y; : RotorLandmarkRule(); [matches addObjectsFromArray:[self getMatchesForRule:rule]]; } + + if ([key isEqualToString:@"AXControlSearchKey"]) { + RotorControlRule rule = mImmediateDescendantsOnly + ? RotorControlRule(mStartElem) + : RotorControlRule(); + [matches addObjectsFromArray:[self getMatchesForRule:rule]]; + } } return matches; diff --git a/accessible/mac/RotorRules.h b/accessible/mac/RotorRules.h index 4d8c583bd609..e507e150518b 100644 --- a/accessible/mac/RotorRules.h +++ b/accessible/mac/RotorRules.h @@ -39,6 +39,17 @@ class RotorLandmarkRule final : public PivotRoleRule { explicit RotorLandmarkRule(AccessibleOrProxy& aDirectDescendantsFrom); }; +class RotorControlRule final : public PivotRule { + public: + explicit RotorControlRule(AccessibleOrProxy& aDirectDescendantsFrom); + explicit RotorControlRule(); + + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; + + private: + AccessibleOrProxy mDirectDescendantsFrom; +}; + /** * This rule matches all accessibles, filtering out non-direct * descendants if necessary. diff --git a/accessible/mac/RotorRules.mm b/accessible/mac/RotorRules.mm index b7477626408d..ccbb7741a0c3 100644 --- a/accessible/mac/RotorRules.mm +++ b/accessible/mac/RotorRules.mm @@ -34,6 +34,74 @@ RotorLandmarkRule::RotorLandmarkRule() : PivotRoleRule(roles::LANDMARK) {} RotorLandmarkRule::RotorLandmarkRule(AccessibleOrProxy& aDirectDescendantsFrom) : PivotRoleRule(roles::LANDMARK, aDirectDescendantsFrom) {} +RotorControlRule::RotorControlRule(AccessibleOrProxy& aDirectDescendantsFrom) + : mDirectDescendantsFrom(aDirectDescendantsFrom) {} + +RotorControlRule::RotorControlRule() : mDirectDescendantsFrom(nullptr) {} + +uint16_t RotorControlRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (nsAccUtils::MustPrune(aAccOrProxy)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (!mDirectDescendantsFrom.IsNull() && + (aAccOrProxy != mDirectDescendantsFrom)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + switch (aAccOrProxy.Role()) { + case roles::PUSHBUTTON: + case roles::SPINBUTTON: + case roles::DETAILS: + case roles::CHECKBUTTON: + case roles::COLOR_CHOOSER: + case roles::BUTTONDROPDOWNGRID: // xul colorpicker + case roles::LISTBOX: + case roles::COMBOBOX: + case roles::EDITCOMBOBOX: + case roles::RADIOBUTTON: + case roles::RADIO_GROUP: + case roles::PAGETAB: + case roles::SLIDER: + case roles::SWITCH: + case roles::ENTRY: + case roles::OUTLINE: + case roles::PASSWORD_TEXT: + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + break; + + case roles::GROUPING: { + // Groupings are sometimes used (like radio groups) to denote + // sets of controls. If that's the case, we want to surface + // them. We also want to surface grouped time and date controls. + for (unsigned int i = 0; i < aAccOrProxy.ChildCount(); i++) { + AccessibleOrProxy currChild = aAccOrProxy.ChildAt(i); + if (currChild.Role() == roles::CHECKBUTTON || + currChild.Role() == roles::SWITCH || + currChild.Role() == roles::SPINBUTTON || + currChild.Role() == roles::RADIOBUTTON) { + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + break; + } + } + break; + } + + case roles::DATE_EDITOR: + case roles::TIME_EDITOR: + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + break; + + default: + break; + } + + return result; +} + // Match All Rule RotorAllRule::RotorAllRule(AccessibleOrProxy& aDirectDescendantsFrom) diff --git a/accessible/tests/browser/mac/browser_rotor.js b/accessible/tests/browser/mac/browser_rotor.js index d1bf55cc3b78..5cdd99dc9777 100644 --- a/accessible/tests/browser/mac/browser_rotor.js +++ b/accessible/tests/browser/mac/browser_rotor.js @@ -399,3 +399,418 @@ addAccessibleTask( ); } ); + +/** + * Test rotor with heading + */ +addAccessibleTask( + `

hello


world


goodbye`, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXHeadingSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const headingCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(2, headingCount, "Found two headings"); + + const headings = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const hello = getNativeInterface(accDoc, "hello"); + const world = getNativeInterface(accDoc, "world"); + is( + hello.getAttributeValue("AXTitle"), + headings[0].getAttributeValue("AXTitle"), + "Found correct first heading" + ); + is( + world.getAttributeValue("AXTitle"), + headings[1].getAttributeValue("AXTitle"), + "Found correct second heading" + ); + } +); + +/** + * Test rotor with buttons + */ +addAccessibleTask( + ` +
+

input[type=button]

+ + +

input[type=submit]

+ + +

input[type=image]

+ + +

input[type=reset]

+ + +

button element

+ +
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(5, controlsCount, "Found 5 controls"); + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const button1 = getNativeInterface(accDoc, "button1"); + const submit = getNativeInterface(accDoc, "submit"); + const image = getNativeInterface(accDoc, "image"); + const reset = getNativeInterface(accDoc, "reset"); + const button2 = getNativeInterface(accDoc, "button2"); + + is( + button1.getAttributeValue("AXTitle"), + controls[0].getAttributeValue("AXTitle"), + "Found correct first control" + ); + is( + submit.getAttributeValue("AXTitle"), + controls[1].getAttributeValue("AXTitle"), + "Found correct second control" + ); + is( + image.getAttributeValue("AXTitle"), + controls[2].getAttributeValue("AXTitle"), + "Found correct third control" + ); + is( + reset.getAttributeValue("AXTitle"), + controls[3].getAttributeValue("AXTitle"), + "Found correct third control" + ); + is( + button2.getAttributeValue("AXTitle"), + controls[4].getAttributeValue("AXTitle"), + "Found correct third control" + ); + } +); + +/** + * Test rotor with inputs + */ +addAccessibleTask( + ` +
+
+
+
+
+
+
+
+
+
+
+
+
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(13, controlsCount, "Found 13 controls"); + // the extra controls here come from our time control + // we can't filter out its internal buttons/incrementors + // like we do with the date entry because the time entry + // doesn't have its own specific role -- its just a grouping. + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const text = getNativeInterface(accDoc, "text"); + const implText = getNativeInterface(accDoc, "implText"); + const textarea = getNativeInterface(accDoc, "textarea"); + const tel = getNativeInterface(accDoc, "tel"); + const url = getNativeInterface(accDoc, "url"); + const email = getNativeInterface(accDoc, "email"); + const password = getNativeInterface(accDoc, "password"); + const month = getNativeInterface(accDoc, "month"); + const week = getNativeInterface(accDoc, "week"); + const number = getNativeInterface(accDoc, "number"); + const range = getNativeInterface(accDoc, "range"); + + const toCheck = [ + text, + implText, + textarea, + tel, + url, + email, + password, + month, + week, + number, + range, + ]; + + for (let i = 0; i < toCheck.length; i++) { + is( + toCheck[i].getAttributeValue("AXValue"), + controls[i].getAttributeValue("AXValue"), + "Found correct input control" + ); + } + + const date = getNativeInterface(accDoc, "date"); + const time = getNativeInterface(accDoc, "time"); + + is( + date.getAttributeValue("AXRole"), + controls[11].getAttributeValue("AXRole"), + "Found corrent date editor" + ); + + is( + time.getAttributeValue("AXRole"), + controls[12].getAttributeValue("AXRole"), + "Found corrent time editor" + ); + } +); + +/** + * Test rotor with groupings + */ +addAccessibleTask( + ` +
+ Radios +
+ Radio 1 + Radio 2 +
+
+ +
+ Checkboxes + Checkbox 1 + Checkbox 2 +
+ +
+ Switches + Switch 1 + Switch 2 +
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(9, controlsCount, "Found 9 controls"); + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const radios = getNativeInterface(accDoc, "radios"); + const radio1 = getNativeInterface(accDoc, "radio1"); + const radio2 = getNativeInterface(accDoc, "radio2"); + + is( + radios.getAttributeValue("AXRole"), + controls[0].getAttributeValue("AXRole"), + "Found correct group of radios" + ); + is( + radio1.getAttributeValue("AXRole"), + controls[1].getAttributeValue("AXRole"), + "Found correct radio 1" + ); + is( + radio2.getAttributeValue("AXRole"), + controls[2].getAttributeValue("AXRole"), + "Found correct radio 2" + ); + + const checkboxes = getNativeInterface(accDoc, "checkboxes"); + const checkbox1 = getNativeInterface(accDoc, "checkbox1"); + const checkbox2 = getNativeInterface(accDoc, "checkbox2"); + + is( + checkboxes.getAttributeValue("AXRole"), + controls[3].getAttributeValue("AXRole"), + "Found correct group of checkboxes" + ); + is( + checkbox1.getAttributeValue("AXRole"), + controls[4].getAttributeValue("AXRole"), + "Found correct checkbox 1" + ); + is( + checkbox2.getAttributeValue("AXRole"), + controls[5].getAttributeValue("AXRole"), + "Found correct checkbox 2" + ); + + const switches = getNativeInterface(accDoc, "switches"); + const switch1 = getNativeInterface(accDoc, "switch1"); + const switch2 = getNativeInterface(accDoc, "switch2"); + + is( + switches.getAttributeValue("AXRole"), + controls[6].getAttributeValue("AXRole"), + "Found correct group of switches" + ); + is( + switch1.getAttributeValue("AXRole"), + controls[7].getAttributeValue("AXRole"), + "Found correct switch 1" + ); + is( + switch2.getAttributeValue("AXRole"), + controls[8].getAttributeValue("AXRole"), + "Found correct switch 2" + ); + } +); + +/** + * Test rotor with misc controls + */ +addAccessibleTask( + ` + + +
+ Hello + world +
+ +
    +
  • item1
  • +
  • item1
  • +
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(3, controlsCount, "Found 3 controls"); + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const spin = getNativeInterface(accDoc, "spinbutton"); + const details = getNativeInterface(accDoc, "details"); + const tree = getNativeInterface(accDoc, "tree"); + + is( + spin.getAttributeValue("AXRole"), + controls[0].getAttributeValue("AXRole"), + "Found correct spinbutton" + ); + is( + details.getAttributeValue("AXRole"), + controls[1].getAttributeValue("AXRole"), + "Found correct details element" + ); + is( + tree.getAttributeValue("AXRole"), + controls[2].getAttributeValue("AXRole"), + "Found correct tree" + ); + } +); diff --git a/accessible/tests/mochitest/role.js b/accessible/tests/mochitest/role.js index beff7d16ec72..19f177fb03f0 100644 --- a/accessible/tests/mochitest/role.js +++ b/accessible/tests/mochitest/role.js @@ -131,6 +131,7 @@ const ROLE_TERM = nsIAccessibleRole.ROLE_TERM; const ROLE_TEXT = nsIAccessibleRole.ROLE_TEXT; const ROLE_TEXT_CONTAINER = nsIAccessibleRole.ROLE_TEXT_CONTAINER; const ROLE_TEXT_LEAF = nsIAccessibleRole.ROLE_TEXT_LEAF; +const ROLE_TIME_EDITOR = nsIAccessibleRole.ROLE_TIME_EDITOR; const ROLE_TOGGLE_BUTTON = nsIAccessibleRole.ROLE_TOGGLE_BUTTON; const ROLE_TOOLBAR = nsIAccessibleRole.ROLE_TOOLBAR; const ROLE_TOOLTIP = nsIAccessibleRole.ROLE_TOOLTIP;