Bug 1625196 - Support select[multiple] and role=listbox/option. r=morgan

Differential Revision: https://phabricator.services.mozilla.com/D72485
This commit is contained in:
Eitan Isaacson 2020-05-12 23:28:34 +00:00
Родитель 4b897ca7d9
Коммит c5398f4399
6 изменённых файлов: 445 добавлений и 16 удалений

Просмотреть файл

@ -207,6 +207,13 @@ Class a11y::GetTypeFromRole(roles::Role aRole) {
case roles::LINK:
return [mozLinkAccessible class];
case roles::LISTBOX:
return [mozListboxAccessible class];
case roles::OPTION: {
return [mozOptionAccessible class];
}
default:
return [mozAccessible class];
}

Просмотреть файл

@ -107,6 +107,9 @@ static const uintptr_t IS_PROXY = 1;
- (BOOL)isEnabled;
// should a child be disabled
- (BOOL)disableChild:(mozAccessible*)child;
// information about focus.
- (BOOL)isFocused;
- (BOOL)canBeFocused;

Просмотреть файл

@ -1294,7 +1294,22 @@ struct RoleDescrComparator {
}
- (BOOL)isEnabled {
return [self stateWithMask:states::UNAVAILABLE] == 0;
if ([self stateWithMask:states::UNAVAILABLE]) {
return NO;
}
if (![self isRoot]) {
mozAccessible* parent = (mozAccessible*)[self parent];
if (![parent isRoot]) {
return ![parent disableChild:self];
}
}
return YES;
}
- (BOOL)disableChild:(mozAccessible*)child {
return NO;
}
- (void)handleAccessibleEvent:(uint32_t)eventType {
@ -1316,6 +1331,7 @@ struct RoleDescrComparator {
case nsIAccessibleEvent::EVENT_SELECTION:
case nsIAccessibleEvent::EVENT_SELECTION_ADD:
case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
[self postNotification:NSAccessibilitySelectedChildrenChangedNotification];
break;
}

Просмотреть файл

@ -7,10 +7,12 @@
#import "mozAccessible.h"
@interface mozSelectableAccessible : mozAccessible
- (id)selectableChildren;
- (NSArray*)selectableChildren;
- (NSArray*)selectedChildren;
@end
@interface mozSelectableChildAccessible : mozAccessible
@property(getter=isSelected) BOOL selected;
@end
@interface mozTabGroupAccessible : mozSelectableAccessible
@ -18,3 +20,9 @@
@interface mozTabAccessible : mozSelectableChildAccessible
@end
@interface mozListboxAccessible : mozSelectableAccessible
@end
@interface mozOptionAccessible : mozSelectableChildAccessible
@end

Просмотреть файл

@ -22,6 +22,34 @@
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
if ([attribute isEqualToString:NSAccessibilitySelectedChildrenAttribute]) {
return YES;
}
return [super accessibilityIsAttributeSettable:attribute];
NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO);
}
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
if ([attribute isEqualToString:NSAccessibilitySelectedChildrenAttribute] &&
[value isKindOfClass:[NSArray class]]) {
for (id child in [self selectableChildren]) {
BOOL selected = [value indexOfObjectIdenticalTo:child] != NSNotFound;
[child setSelected:selected];
}
} else {
[super accessibilitySetValue:value forAttribute:attribute];
}
NS_OBJC_END_TRY_ABORT_BLOCK;
}
- (id)accessibilityAttributeValue:(NSString*)attribute {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
if ([attribute isEqualToString:NSAccessibilitySelectedChildrenAttribute]) {
@ -35,7 +63,7 @@
/**
* Return the mozAccessibles that are selectable.
*/
- (id)selectableChildren {
- (NSArray*)selectableChildren {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
return [[self children]
@ -50,33 +78,80 @@
/**
* Return the mozAccessibles that are actually selected.
*/
- (id)selectedChildren {
- (NSArray*)selectedChildren {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
return [[self children]
filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(mozAccessible* child,
NSDictionary* bindings) {
// Return mozSelectableChildAccessibles that have are selected (truthy value).
return
[child isKindOfClass:[mozSelectableChildAccessible class]] && [[child value] boolValue];
return [child isKindOfClass:[mozSelectableChildAccessible class]] &&
[(mozSelectableChildAccessible*)child isSelected];
}]];
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (id)value {
// The value of a selectable is its selected child. In the case
// of multiple selections this will return the first one.
return [[self selectedChildren] firstObject];
}
@end
@implementation mozSelectableChildAccessible
- (id)value {
// Retuens true if item is selected.
return [NSNumber numberWithBool:[self stateWithMask:states::SELECTED] != 0];
- (id)accessibilityAttributeValue:(NSString*)attribute {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
if ([attribute isEqualToString:NSAccessibilitySelectedAttribute]) {
return [NSNumber numberWithBool:[self isSelected]];
}
return [super accessibilityAttributeValue:attribute];
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
if ([attribute isEqualToString:NSAccessibilitySelectedAttribute]) {
return YES;
}
return [super accessibilityIsAttributeSettable:attribute];
NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO);
}
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
if ([attribute isEqualToString:NSAccessibilitySelectedAttribute]) {
[self setSelected:[value boolValue]];
} else {
[super accessibilitySetValue:value forAttribute:attribute];
}
NS_OBJC_END_TRY_ABORT_BLOCK;
}
- (BOOL)isSelected {
return [self stateWithMask:states::SELECTED] != 0;
}
- (void)setSelected:(BOOL)selected {
// Get SELECTABLE and UNAVAILABLE state.
uint64_t state = [self stateWithMask:(states::SELECTABLE | states::UNAVAILABLE)];
if ((state & states::SELECTABLE) == 0 || (state & states::UNAVAILABLE) != 0) {
// The object is either not selectable or is unavailable. Don't do anything.
return;
}
if (AccessibleWrap* accWrap = [self getGeckoAccessible]) {
accWrap->SetSelected(selected);
} else if (ProxyAccessible* proxy = [self getProxyAccessible]) {
proxy->SetSelected(selected);
}
// We need to invalidate the state because the accessibility service
// may check the selected attribute synchornously and not wait for
// selection events.
[self invalidateState];
}
@end
@ -84,7 +159,6 @@
@implementation mozTabGroupAccessible
- (NSArray*)accessibilityAttributeNames {
// standard attributes that are shared and supported by root accessible (AXMain) elements.
static NSMutableArray* attributes = nil;
if (!attributes) {
@ -103,6 +177,12 @@
return [super accessibilityAttributeValue:attribute];
}
- (id)value {
// The value of a tab group is its selected child. In the case
// of multiple selections this will return the first one.
return [[self selectedChildren] firstObject];
}
@end
@implementation mozTabAccessible
@ -119,4 +199,54 @@
return [super accessibilityActionDescription:action];
}
- (id)value {
// Retuens 1 if item is selected, 0 if not.
return [NSNumber numberWithBool:[self isSelected]];
}
@end
@implementation mozListboxAccessible
- (BOOL)ignoreChild:(mozAccessible*)child {
if (!child || child->mRole == roles::GROUPING) {
return YES;
}
return [super ignoreChild:child];
}
- (BOOL)disableChild:(mozAccessible*)child {
return ![child isKindOfClass:[mozSelectableChildAccessible class]];
}
- (NSArray*)accessibilityAttributeNames {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
static NSMutableArray* attributes = nil;
if (!attributes) {
attributes = [[super accessibilityAttributeNames] mutableCopy];
// VoiceOver uses the availability of AXOrientation to make
// an object an interaction group. The actual return value does
// not matter.
[attributes addObject:NSAccessibilityOrientationAttribute];
}
return attributes;
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
@end
@implementation mozOptionAccessible
- (NSString*)title {
return @"";
}
- (id)value {
// Swap title and value of option so it behaves more like a AXStaticText.
return [super title];
}
@end

Просмотреть файл

@ -11,6 +11,12 @@ loadScripts(
{ name: "states.js", dir: MOCHITESTS_DIR }
);
function getSelectedIds(selectable) {
return selectable
.getAttributeValue("AXSelectedChildren")
.map(c => c.getAttributeValue("AXDOMIdentifier"));
}
/**
* Test aria tabs
*/
@ -69,3 +75,262 @@ addAccessibleTask('<p id="p">hello</p>', async (browser, accDoc) => {
);
is(p.getAttributeValue("AXSelected"), false, "AX selected is 'false'");
});
addAccessibleTask(
`<select id="select" aria-label="Choose a number" multiple>
<option id="one" selected>One</option>
<option id="two">Two</option>
<option id="three">Three</option>
<option id="four" disabled>Four</option>
</select>`,
async (browser, accDoc) => {
let select = getNativeInterface(accDoc, "select");
let one = getNativeInterface(accDoc, "one");
let two = getNativeInterface(accDoc, "two");
let three = getNativeInterface(accDoc, "three");
let four = getNativeInterface(accDoc, "four");
is(
select.getAttributeValue("AXTitle"),
"Choose a number",
"Select titled correctly"
);
ok(
select.attributeNames.includes("AXOrientation"),
"Have orientation attribute"
);
ok(
select.isAttributeSettable("AXSelectedChildren"),
"Select can have AXSelectedChildren set"
);
is(one.getAttributeValue("AXTitle"), "", "Option should not have a title");
is(
one.getAttributeValue("AXValue"),
"One",
"Option should have correct value"
);
is(
one.getAttributeValue("AXRole"),
"AXStaticText",
"Options should have AXStaticText role"
);
ok(one.isAttributeSettable("AXSelected"), "Option can have AXSelected set");
is(select.getAttributeValue("AXSelectedChildren").length, 1);
one.setAttributeValue("AXSelected", false);
is(select.getAttributeValue("AXSelectedChildren").length, 0);
three.setAttributeValue("AXSelected", true);
is(select.getAttributeValue("AXSelectedChildren").length, 1);
ok(getSelectedIds(select).includes("three"), "'three' is selected");
select.setAttributeValue("AXSelectedChildren", [one, two]);
Assert.deepEqual(
getSelectedIds(select),
["one", "two"],
"one and two are selected"
);
select.setAttributeValue("AXSelectedChildren", [three, two, four]);
Assert.deepEqual(
getSelectedIds(select),
["two", "three"],
"two and three are selected, four is disabled so it's not"
);
ok(!four.getAttributeValue("AXEnabled"), "Disabled option is disabled");
}
);
addAccessibleTask(
`<select id="select" aria-label="Choose a thing" multiple>
<optgroup label="Fruits">
<option id="banana" selected>Banana</option>
<option id="apple">Apple</option>
<option id="orange">Orange</option>
</optgroup>
<optgroup label="Vegetables">
<option id="lettuce" selected>Lettuce</option>
<option id="tomato">Tomato</option>
<option id="onion">Onion</option>
</optgroup>
<optgroup label="Spices">
<option id="cumin">Cumin</option>
<option id="coriander">Coriander</option>
<option id="allspice" selected>Allspice</option>
</optgroup>
<option id="everything">Everything</option>
</select>`,
async (browser, accDoc) => {
let select = getNativeInterface(accDoc, "select");
is(
select.getAttributeValue("AXTitle"),
"Choose a thing",
"Select titled correctly"
);
ok(
select.attributeNames.includes("AXOrientation"),
"Have orientation attribute"
);
ok(
select.isAttributeSettable("AXSelectedChildren"),
"Select can have AXSelectedChildren set"
);
let childValueSelectablePairs = select
.getAttributeValue("AXChildren")
.map(c => [
c.getAttributeValue("AXValue"),
c.isAttributeSettable("AXSelected"),
c.getAttributeValue("AXEnabled"),
]);
Assert.deepEqual(
childValueSelectablePairs,
[
["Fruits", false, false],
["Banana", true, true],
["Apple", true, true],
["Orange", true, true],
["Vegetables", false, false],
["Lettuce", true, true],
["Tomato", true, true],
["Onion", true, true],
["Spices", false, false],
["Cumin", true, true],
["Coriander", true, true],
["Allspice", true, true],
["Everything", true, true],
],
"Options are selectable, group labels are not"
);
let allspice = getNativeInterface(accDoc, "allspice");
is(
allspice.getAttributeValue("AXTitle"),
"",
"Option should not have a title"
);
is(
allspice.getAttributeValue("AXValue"),
"Allspice",
"Option should have a value"
);
is(
allspice.getAttributeValue("AXRole"),
"AXStaticText",
"Options should have AXStaticText role"
);
ok(
allspice.isAttributeSettable("AXSelected"),
"Option can have AXSelected set"
);
is(
allspice
.getAttributeValue("AXParent")
.getAttributeValue("AXDOMIdentifier"),
"select",
"Select is direct parent of nested option"
);
let groupLabel = select.getAttributeValue("AXChildren")[0];
ok(
!groupLabel.isAttributeSettable("AXSelected"),
"Group label should not be selectable"
);
is(
groupLabel.getAttributeValue("AXValue"),
"Fruits",
"Group label should have a value"
);
is(
groupLabel.getAttributeValue("AXTitle"),
null,
"Group label should not have a title"
);
is(
groupLabel.getAttributeValue("AXRole"),
"AXStaticText",
"Group label should have AXStaticText role"
);
is(
groupLabel
.getAttributeValue("AXParent")
.getAttributeValue("AXDOMIdentifier"),
"select",
"Select is direct parent of group label"
);
Assert.deepEqual(getSelectedIds(select), ["banana", "lettuce", "allspice"]);
}
);
addAccessibleTask(
`<div role="listbox" id="select" aria-label="Choose a number" aria-multiselectable="true">
<div role="option" id="one" aria-selected="true">One</div>
<div role="option" id="two">Two</div>
<div role="option" id="three">Three</div>
<div role="option" id="four" aria-disabled="true">Four</div>
</div>`,
async (browser, accDoc) => {
let select = getNativeInterface(accDoc, "select");
let one = getNativeInterface(accDoc, "one");
let two = getNativeInterface(accDoc, "two");
let three = getNativeInterface(accDoc, "three");
let four = getNativeInterface(accDoc, "four");
is(
select.getAttributeValue("AXTitle"),
"Choose a number",
"Select titled correctly"
);
ok(
select.attributeNames.includes("AXOrientation"),
"Have orientation attribute"
);
ok(
select.isAttributeSettable("AXSelectedChildren"),
"Select can have AXSelectedChildren set"
);
is(one.getAttributeValue("AXTitle"), "", "Option should not have a title");
is(
one.getAttributeValue("AXValue"),
"One",
"Option should have correct value"
);
is(
one.getAttributeValue("AXRole"),
"AXStaticText",
"Options should have AXStaticText role"
);
ok(one.isAttributeSettable("AXSelected"), "Option can have AXSelected set");
is(select.getAttributeValue("AXSelectedChildren").length, 1);
let evt = waitForMacEvent("AXSelectedChildrenChanged");
// Change selection from content.
await SpecialPowers.spawn(browser, [], () => {
content.document.getElementById("one").removeAttribute("aria-selected");
});
await evt;
is(select.getAttributeValue("AXSelectedChildren").length, 0);
three.setAttributeValue("AXSelected", true);
is(select.getAttributeValue("AXSelectedChildren").length, 1);
ok(getSelectedIds(select).includes("three"), "'three' is selected");
select.setAttributeValue("AXSelectedChildren", [one, two]);
Assert.deepEqual(
getSelectedIds(select),
["one", "two"],
"one and two are selected"
);
select.setAttributeValue("AXSelectedChildren", [three, two, four]);
Assert.deepEqual(
getSelectedIds(select),
["two", "three"],
"two and three are selected, four is disabled so it's not"
);
}
);