Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit ad75e93

Browse files
authored
[web] remove Tappable from basic set of a11y roles; add it case by case (#51466)
Remove the `Tappable` role from the `PrimaryRoleManager.withBasics` constructor. Only add `Tappable` to primary roles that know for sure they need it. Fixes flutter/flutter#144364
1 parent 90c4d64 commit ad75e93

File tree

5 files changed

+89
-4
lines changed

5 files changed

+89
-4
lines changed

lib/web_ui/lib/src/engine/semantics/checkable.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ class Checkable extends PrimaryRoleManager {
5656
PrimaryRole.checkable,
5757
semanticsObject,
5858
labelRepresentation: LeafLabelRepresentation.ariaLabel,
59-
);
59+
) {
60+
addTappable();
61+
}
6062

6163
final _CheckableKind _kind;
6264

lib/web_ui/lib/src/engine/semantics/link.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ class Link extends PrimaryRoleManager {
1111
PrimaryRole.link,
1212
semanticsObject,
1313
labelRepresentation: LeafLabelRepresentation.domText,
14-
);
14+
) {
15+
addTappable();
16+
}
1517

1618
@override
1719
DomElement createElement() {

lib/web_ui/lib/src/engine/semantics/semantics.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,6 @@ abstract class PrimaryRoleManager {
439439
addLiveRegion();
440440
addRouteName();
441441
addLabelAndValue(labelRepresentation: labelRepresentation);
442-
addTappable();
443442
}
444443

445444
/// Initializes a blank role for a [semanticsObject].
@@ -625,7 +624,17 @@ final class GenericRole extends PrimaryRoleManager {
625624
PrimaryRole.generic,
626625
semanticsObject,
627626
labelRepresentation: LeafLabelRepresentation.domText,
628-
);
627+
) {
628+
// Typically a tappable widget would have a more specific role, such as
629+
// "link", "button", "checkbox", etc. However, there are situations when a
630+
// tappable is not a leaf node, but contains other nodes, which can also be
631+
// tappable. For example, the dismiss barrier of a pop-up menu is a tappable
632+
// ancestor of the menu itself, while the menu may contain tappable
633+
// children.
634+
if (semanticsObject.isTappable) {
635+
addTappable();
636+
}
637+
}
629638

630639
@override
631640
void update() {

lib/web_ui/lib/src/engine/semantics/tappable.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Button extends PrimaryRoleManager {
1212
semanticsObject,
1313
labelRepresentation: LeafLabelRepresentation.domText,
1414
) {
15+
addTappable();
1516
setAriaRole('button');
1617
}
1718

lib/web_ui/test/engine/semantics/semantics_test.dart

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ void runSemanticsTests() {
5151
group('Role managers', () {
5252
_testRoleManagerLifecycle();
5353
});
54+
group('Text', () {
55+
_testText();
56+
});
5457
group('container', () {
5558
_testContainer();
5659
});
@@ -718,6 +721,74 @@ void _testLongestIncreasingSubsequence() {
718721
});
719722
}
720723

724+
void _testText() {
725+
test('renders a piece of plain text', () async {
726+
semantics()
727+
..debugOverrideTimestampFunction(() => _testTime)
728+
..semanticsEnabled = true;
729+
730+
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
731+
updateNode(
732+
builder,
733+
label: 'plain text',
734+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
735+
);
736+
owner().updateSemantics(builder.build());
737+
738+
expectSemanticsTree(
739+
owner(),
740+
'''<sem role="text" style="$rootSemanticStyle">plain text</sem>''',
741+
);
742+
743+
final SemanticsObject node = owner().debugSemanticsTree![0]!;
744+
expect(node.primaryRole?.role, PrimaryRole.generic);
745+
expect(
746+
node.primaryRole!.secondaryRoleManagers!.map((RoleManager m) => m.runtimeType).toList(),
747+
<Type>[
748+
Focusable,
749+
LiveRegion,
750+
RouteName,
751+
LabelAndValue,
752+
],
753+
);
754+
semantics().semanticsEnabled = false;
755+
});
756+
757+
test('renders a tappable piece of text', () async {
758+
semantics()
759+
..debugOverrideTimestampFunction(() => _testTime)
760+
..semanticsEnabled = true;
761+
762+
final SemanticsTester tester = SemanticsTester(owner());
763+
tester.updateNode(
764+
id: 0,
765+
hasTap: true,
766+
label: 'tappable text',
767+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
768+
);
769+
tester.apply();
770+
771+
expectSemanticsTree(
772+
owner(),
773+
'''<sem flt-tappable="" role="text" style="$rootSemanticStyle">tappable text</sem>''',
774+
);
775+
776+
final SemanticsObject node = owner().debugSemanticsTree![0]!;
777+
expect(node.primaryRole?.role, PrimaryRole.generic);
778+
expect(
779+
node.primaryRole!.secondaryRoleManagers!.map((RoleManager m) => m.runtimeType).toList(),
780+
<Type>[
781+
Focusable,
782+
LiveRegion,
783+
RouteName,
784+
LabelAndValue,
785+
Tappable,
786+
],
787+
);
788+
semantics().semanticsEnabled = false;
789+
});
790+
}
791+
721792
void _testContainer() {
722793
test('container node has no transform when there is no rect offset',
723794
() async {

0 commit comments

Comments
 (0)