зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1625196 - Support select[multiple] and role=listbox/option. r=morgan
Differential Revision: https://phabricator.services.mozilla.com/D72485
This commit is contained in:
Родитель
4b897ca7d9
Коммит
c5398f4399
|
@ -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"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
Загрузка…
Ссылка в новой задаче