[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;
|
return EngineSemanticsRole.link;
|
||||||
} else if (isHeader) {
|
} else if (isHeader) {
|
||||||
return EngineSemanticsRole.header;
|
return EngineSemanticsRole.header;
|
||||||
|
} else if (isButtonLike) {
|
||||||
|
return EngineSemanticsRole.button;
|
||||||
} else {
|
} else {
|
||||||
return EngineSemanticsRole.generic;
|
return EngineSemanticsRole.generic;
|
||||||
}
|
}
|
||||||
@ -1852,8 +1854,14 @@ class SemanticsObject {
|
|||||||
hasAction(ui.SemanticsAction.increase) || hasAction(ui.SemanticsAction.decrease);
|
hasAction(ui.SemanticsAction.increase) || hasAction(ui.SemanticsAction.decrease);
|
||||||
|
|
||||||
/// Whether the object represents a button.
|
/// Whether the object represents a button.
|
||||||
|
///
|
||||||
|
/// See also [isButtonLike].
|
||||||
bool get isButton => hasFlag(ui.SemanticsFlag.isButton);
|
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,
|
/// Represents a tappable or clickable widget, such as button, icon button,
|
||||||
/// "hamburger" menu, etc.
|
/// "hamburger" menu, etc.
|
||||||
bool get isTappable => hasAction(ui.SemanticsAction.tap);
|
bool get isTappable => hasAction(ui.SemanticsAction.tap);
|
||||||
|
@ -62,9 +62,6 @@ void runSemanticsTests() {
|
|||||||
group('Roles', () {
|
group('Roles', () {
|
||||||
_testRoleLifecycle();
|
_testRoleLifecycle();
|
||||||
});
|
});
|
||||||
group('Text', () {
|
|
||||||
_testText();
|
|
||||||
});
|
|
||||||
group('labels', () {
|
group('labels', () {
|
||||||
_testLabels();
|
_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() {
|
void _testLabels() {
|
||||||
test('computeDomSemanticsLabel combines tooltip, label, value, and hint', () {
|
test('computeDomSemanticsLabel combines tooltip, label, value, and hint', () {
|
||||||
expect(computeDomSemanticsLabel(tooltip: 'tooltip'), 'tooltip');
|
expect(computeDomSemanticsLabel(tooltip: 'tooltip'), 'tooltip');
|
||||||
@ -2306,7 +2251,7 @@ void _testSelectables() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _testTappable() {
|
void _testTappable() {
|
||||||
test('renders an enabled tappable widget', () async {
|
test('renders an enabled button', () async {
|
||||||
semantics()
|
semantics()
|
||||||
..debugOverrideTimestampFunction(() => _testTime)
|
..debugOverrideTimestampFunction(() => _testTime)
|
||||||
..semanticsEnabled = true;
|
..semanticsEnabled = true;
|
||||||
@ -2335,21 +2280,23 @@ void _testTappable() {
|
|||||||
semantics().semanticsEnabled = false;
|
semantics().semanticsEnabled = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders a disabled tappable widget', () async {
|
test('renders a disabled button', () async {
|
||||||
semantics()
|
semantics()
|
||||||
..debugOverrideTimestampFunction(() => _testTime)
|
..debugOverrideTimestampFunction(() => _testTime)
|
||||||
..semanticsEnabled = true;
|
..semanticsEnabled = true;
|
||||||
|
|
||||||
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
|
final SemanticsTester tester = SemanticsTester(owner());
|
||||||
updateNode(
|
tester.updateNode(
|
||||||
builder,
|
id: 0,
|
||||||
actions: 0 | ui.SemanticsAction.tap.index,
|
isFocusable: true,
|
||||||
flags: 0 | ui.SemanticsFlag.hasEnabledState.index | ui.SemanticsFlag.isButton.index,
|
hasTap: true,
|
||||||
transform: Matrix4.identity().toFloat64(),
|
hasEnabledState: true,
|
||||||
|
isEnabled: false,
|
||||||
|
isButton: true,
|
||||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||||
);
|
);
|
||||||
|
tester.apply();
|
||||||
|
|
||||||
owner().updateSemantics(builder.build());
|
|
||||||
expectSemanticsTree(owner(), '''
|
expectSemanticsTree(owner(), '''
|
||||||
<sem role="button" aria-disabled="true"></sem>
|
<sem role="button" aria-disabled="true"></sem>
|
||||||
''');
|
''');
|
||||||
@ -2357,7 +2304,40 @@ void _testTappable() {
|
|||||||
semantics().semanticsEnabled = false;
|
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()
|
semantics()
|
||||||
..debugOverrideTimestampFunction(() => _testTime)
|
..debugOverrideTimestampFunction(() => _testTime)
|
||||||
..semanticsEnabled = true;
|
..semanticsEnabled = true;
|
||||||
@ -2390,7 +2370,7 @@ void _testTappable() {
|
|||||||
semantics().semanticsEnabled = false;
|
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()
|
semantics()
|
||||||
..debugOverrideTimestampFunction(() => _testTime)
|
..debugOverrideTimestampFunction(() => _testTime)
|
||||||
..semanticsEnabled = true;
|
..semanticsEnabled = true;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user