[web:a11y] treat empty tappables as buttons (#161360)
The situation will improve even further when we have proper ARIA roles, but for now if a node is a leaf node and has a tap action, present it to semantics as a button even if the `isButton` flag is missing. Fixes https://github.com/flutter/flutter/issues/157743
This commit is contained in:
parent
067afb7e69
commit
0d906f5ecf
@ -1771,6 +1771,8 @@ class SemanticsObject {
|
||||
return EngineSemanticsRole.link;
|
||||
} else if (isHeader) {
|
||||
return EngineSemanticsRole.header;
|
||||
} else if (isButtonLike) {
|
||||
return EngineSemanticsRole.button;
|
||||
} else {
|
||||
return EngineSemanticsRole.generic;
|
||||
}
|
||||
@ -1852,8 +1854,14 @@ class SemanticsObject {
|
||||
hasAction(ui.SemanticsAction.increase) || hasAction(ui.SemanticsAction.decrease);
|
||||
|
||||
/// Whether the object represents a button.
|
||||
///
|
||||
/// See also [isButtonLike].
|
||||
bool get isButton => hasFlag(ui.SemanticsFlag.isButton);
|
||||
|
||||
/// Whether the object behaves like a button even if it does not formally have
|
||||
/// the [ui.SemanticsFlag.isButton] flag.
|
||||
bool get isButtonLike => isTappable && !hasChildren;
|
||||
|
||||
/// Represents a tappable or clickable widget, such as button, icon button,
|
||||
/// "hamburger" menu, etc.
|
||||
bool get isTappable => hasAction(ui.SemanticsAction.tap);
|
||||
|
@ -62,9 +62,6 @@ void runSemanticsTests() {
|
||||
group('Roles', () {
|
||||
_testRoleLifecycle();
|
||||
});
|
||||
group('Text', () {
|
||||
_testText();
|
||||
});
|
||||
group('labels', () {
|
||||
_testLabels();
|
||||
});
|
||||
@ -859,58 +856,6 @@ void _testLongestIncreasingSubsequence() {
|
||||
});
|
||||
}
|
||||
|
||||
void _testText() {
|
||||
test('renders a piece of plain text', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
|
||||
updateNode(builder, label: 'plain text', rect: const ui.Rect.fromLTRB(0, 0, 100, 50));
|
||||
owner().updateSemantics(builder.build());
|
||||
|
||||
expectSemanticsTree(owner(), '''<sem><span>plain text</span></sem>''');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.generic);
|
||||
expect(node.semanticRole!.behaviors!.map((m) => m.runtimeType).toList(), <Type>[
|
||||
Focusable,
|
||||
LiveRegion,
|
||||
RouteName,
|
||||
LabelAndValue,
|
||||
]);
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
|
||||
test('renders a tappable piece of text', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
hasTap: true,
|
||||
label: 'tappable text',
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
expectSemanticsTree(owner(), '''<sem flt-tappable=""><span>tappable text</span></sem>''');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.generic);
|
||||
expect(node.semanticRole!.behaviors!.map((m) => m.runtimeType).toList(), <Type>[
|
||||
Focusable,
|
||||
LiveRegion,
|
||||
RouteName,
|
||||
LabelAndValue,
|
||||
Tappable,
|
||||
]);
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _testLabels() {
|
||||
test('computeDomSemanticsLabel combines tooltip, label, value, and hint', () {
|
||||
expect(computeDomSemanticsLabel(tooltip: 'tooltip'), 'tooltip');
|
||||
@ -2306,7 +2251,7 @@ void _testSelectables() {
|
||||
}
|
||||
|
||||
void _testTappable() {
|
||||
test('renders an enabled tappable widget', () async {
|
||||
test('renders an enabled button', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
@ -2335,21 +2280,23 @@ void _testTappable() {
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
|
||||
test('renders a disabled tappable widget', () async {
|
||||
test('renders a disabled button', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
|
||||
updateNode(
|
||||
builder,
|
||||
actions: 0 | ui.SemanticsAction.tap.index,
|
||||
flags: 0 | ui.SemanticsFlag.hasEnabledState.index | ui.SemanticsFlag.isButton.index,
|
||||
transform: Matrix4.identity().toFloat64(),
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
isFocusable: true,
|
||||
hasTap: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: false,
|
||||
isButton: true,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
owner().updateSemantics(builder.build());
|
||||
expectSemanticsTree(owner(), '''
|
||||
<sem role="button" aria-disabled="true"></sem>
|
||||
''');
|
||||
@ -2357,7 +2304,40 @@ void _testTappable() {
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
|
||||
test('can switch tappable between enabled and disabled', () async {
|
||||
test('tappable leaf node is a button', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
isFocusable: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: true,
|
||||
|
||||
// Not a button
|
||||
isButton: false,
|
||||
|
||||
// But has a tap action and no children
|
||||
hasTap: true,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
expectSemanticsTree(owner(), '''
|
||||
<sem role="button" flt-tappable></sem>
|
||||
''');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.button);
|
||||
expect(node.semanticRole?.debugSemanticBehaviorTypes, containsAll(<Type>[Focusable, Tappable]));
|
||||
expect(tester.getSemanticsObject(0).element.tabIndex, 0);
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
|
||||
test('can switch a button between enabled and disabled', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
@ -2390,7 +2370,7 @@ void _testTappable() {
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
|
||||
test('focuses on tappable after element has been attached', () async {
|
||||
test('focuses on a button after element has been attached', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
Loading…
x
Reference in New Issue
Block a user