Skip to content

Commit 5122ffd

Browse files
authored
Adds list and list item roles (#164809)
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> fixes flutter/flutter#162121 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 212d7d8 commit 5122ffd

File tree

13 files changed

+199
-13
lines changed

13 files changed

+199
-13
lines changed

engine/src/flutter/ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42625,6 +42625,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart + ../../
4262542625
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart + ../../../flutter/LICENSE
4262642626
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart + ../../../flutter/LICENSE
4262742627
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart + ../../../flutter/LICENSE
42628+
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/list.dart + ../../../flutter/LICENSE
4262842629
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart + ../../../flutter/LICENSE
4262942630
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart + ../../../flutter/LICENSE
4263042631
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart + ../../../flutter/LICENSE
@@ -45594,6 +45595,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart
4559445595
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart
4559545596
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart
4559645597
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart
45598+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/list.dart
4559745599
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart
4559845600
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart
4559945601
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart

engine/src/flutter/lib/web_ui/lib/src/engine.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export 'engine/semantics/image.dart';
114114
export 'engine/semantics/incrementable.dart';
115115
export 'engine/semantics/label_and_value.dart';
116116
export 'engine/semantics/link.dart';
117+
export 'engine/semantics/list.dart';
117118
export 'engine/semantics/live_region.dart';
118119
export 'engine/semantics/platform_view.dart';
119120
export 'engine/semantics/route.dart';

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export 'semantics/image.dart';
1313
export 'semantics/incrementable.dart';
1414
export 'semantics/label_and_value.dart';
1515
export 'semantics/link.dart';
16+
export 'semantics/list.dart';
1617
export 'semantics/live_region.dart';
1718
export 'semantics/platform_view.dart';
1819
export 'semantics/scrollable.dart';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import 'semantics.dart';
99
///
1010
/// Uses aria img role to convey this semantic information to the element.
1111
///
12-
/// Screen-readers takes advantage of "aria-label" to describe the visual.
12+
/// Screen-readers take advantage of "aria-label" to describe the visual.
1313
class SemanticImage extends SemanticRole {
1414
SemanticImage(SemanticsObject semanticsObject)
1515
: super.blank(EngineSemanticsRole.image, semanticsObject) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'label_and_value.dart';
6+
import 'semantics.dart';
7+
8+
/// Indicates a list container.
9+
///
10+
/// Uses aria list role to convey this semantic information to the element.
11+
///
12+
/// Screen-readers take advantage of "aria-label" to describe the visual.
13+
class SemanticList extends SemanticRole {
14+
SemanticList(SemanticsObject semanticsObject)
15+
: super.withBasics(
16+
EngineSemanticsRole.list,
17+
semanticsObject,
18+
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
19+
) {
20+
setAriaRole('list');
21+
}
22+
23+
@override
24+
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
25+
}
26+
27+
/// Indicates an item in a list.
28+
///
29+
/// Uses aria listitem role to convey this semantic information to the element.
30+
///
31+
/// Screen-readers take advantage of "aria-label" to describe the visual.
32+
class SemanticListItem extends SemanticRole {
33+
SemanticListItem(SemanticsObject semanticsObject)
34+
: super.withBasics(
35+
EngineSemanticsRole.listItem,
36+
semanticsObject,
37+
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
38+
) {
39+
setAriaRole('listitem');
40+
}
41+
42+
@override
43+
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
44+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ class SemanticDialog extends SemanticRouteBase {
137137
/// Setting this role will also set aria-modal to true, which helps screen
138138
/// reader better understand this section of screen.
139139
///
140-
/// Screen-readers takes advantage of "aria-label" to describe the visual.
140+
/// Screen-readers take advantage of "aria-label" to describe the visual.
141141
///
142142
/// See also:
143143
///

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import 'image.dart';
2929
import 'incrementable.dart';
3030
import 'label_and_value.dart';
3131
import 'link.dart';
32+
import 'list.dart';
3233
import 'live_region.dart';
3334
import 'platform_view.dart';
3435
import 'route.dart';
@@ -451,6 +452,12 @@ enum EngineSemanticsRole {
451452
/// A component provide important and usually time-sensitive information.
452453
alert,
453454

455+
/// A container whose children are logically a list of items.
456+
list,
457+
458+
/// An item in a [list].
459+
listItem,
460+
454461
/// A role used when a more specific role cannot be assigend to
455462
/// a [SemanticsObject].
456463
///
@@ -1859,6 +1866,10 @@ class SemanticsObject {
18591866
return EngineSemanticsRole.alert;
18601867
case ui.SemanticsRole.status:
18611868
return EngineSemanticsRole.status;
1869+
case ui.SemanticsRole.list:
1870+
return EngineSemanticsRole.list;
1871+
case ui.SemanticsRole.listItem:
1872+
return EngineSemanticsRole.listItem;
18621873
// TODO(chunhtai): implement these roles.
18631874
// https://github.com/flutter/flutter/issues/159741.
18641875
case ui.SemanticsRole.searchBox:
@@ -1868,8 +1879,6 @@ class SemanticsObject {
18681879
case ui.SemanticsRole.menuBar:
18691880
case ui.SemanticsRole.menu:
18701881
case ui.SemanticsRole.menuItem:
1871-
case ui.SemanticsRole.list:
1872-
case ui.SemanticsRole.listItem:
18731882
case ui.SemanticsRole.form:
18741883
case ui.SemanticsRole.tooltip:
18751884
case ui.SemanticsRole.loadingSpinner:
@@ -1920,6 +1929,8 @@ class SemanticsObject {
19201929
EngineSemanticsRole.image => SemanticImage(this),
19211930
EngineSemanticsRole.platformView => SemanticPlatformView(this),
19221931
EngineSemanticsRole.link => SemanticLink(this),
1932+
EngineSemanticsRole.list => SemanticList(this),
1933+
EngineSemanticsRole.listItem => SemanticListItem(this),
19231934
EngineSemanticsRole.heading => SemanticHeading(this),
19241935
EngineSemanticsRole.header => SemanticHeader(this),
19251936
EngineSemanticsRole.tab => SemanticTab(this),

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import 'semantics.dart';
99
///
1010
/// Uses aria table role to convey this semantic information to the element.
1111
///
12-
/// Screen-readers takes advantage of "aria-label" to describe the visual.
12+
/// Screen-readers take advantage of "aria-label" to describe the visual.
1313
class SemanticTable extends SemanticRole {
1414
SemanticTable(SemanticsObject semanticsObject)
1515
: super.withBasics(
@@ -28,7 +28,7 @@ class SemanticTable extends SemanticRole {
2828
///
2929
/// Uses aria cell role to convey this semantic information to the element.
3030
///
31-
/// Screen-readers takes advantage of "aria-label" to describe the visual.
31+
/// Screen-readers take advantage of "aria-label" to describe the visual.
3232
class SemanticCell extends SemanticRole {
3333
SemanticCell(SemanticsObject semanticsObject)
3434
: super.withBasics(
@@ -47,7 +47,7 @@ class SemanticCell extends SemanticRole {
4747
///
4848
/// Uses aria row role to convey this semantic information to the element.
4949
///
50-
/// Screen-readers takes advantage of "aria-label" to describe the visual.
50+
/// Screen-readers take advantage of "aria-label" to describe the visual.
5151
class SemanticRow extends SemanticRole {
5252
SemanticRow(SemanticsObject semanticsObject)
5353
: super.withBasics(
@@ -66,7 +66,7 @@ class SemanticRow extends SemanticRole {
6666
///
6767
/// Uses aria columnheader role to convey this semantic information to the element.
6868
///
69-
/// Screen-readers takes advantage of "aria-label" to describe the visual.
69+
/// Screen-readers take advantage of "aria-label" to describe the visual.
7070
class SemanticColumnHeader extends SemanticRole {
7171
SemanticColumnHeader(SemanticsObject semanticsObject)
7272
: super.withBasics(

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import 'semantics.dart';
1010
///
1111
/// Uses aria tab role to convey this semantic information to the element.
1212
///
13-
/// Screen-readers takes advantage of "aria-label" to describe the visual.
13+
/// Screen-readers take advantage of "aria-label" to describe the visual.
1414
class SemanticTab extends SemanticRole {
1515
SemanticTab(SemanticsObject semanticsObject)
1616
: super.withBasics(
@@ -29,7 +29,7 @@ class SemanticTab extends SemanticRole {
2929
///
3030
/// Uses aria tabpanel role to convey this semantic information to the element.
3131
///
32-
/// Screen-readers takes advantage of "aria-label" to describe the visual.
32+
/// Screen-readers take advantage of "aria-label" to describe the visual.
3333
class SemanticTabPanel extends SemanticRole {
3434
SemanticTabPanel(SemanticsObject semanticsObject)
3535
: super.withBasics(
@@ -48,7 +48,7 @@ class SemanticTabPanel extends SemanticRole {
4848
///
4949
/// Uses aria tablist role to convey this semantic information to the element.
5050
///
51-
/// Screen-readers takes advantage of "aria-label" to describe the visual.
51+
/// Screen-readers take advantage of "aria-label" to describe the visual.
5252
class SemanticTabList extends SemanticRole {
5353
SemanticTabList(SemanticsObject semanticsObject)
5454
: super.withBasics(

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ void runSemanticsTests() {
132132
group('table', () {
133133
_testTables();
134134
});
135+
group('list', () {
136+
_testLists();
137+
});
135138
group('controlsNodes', () {
136139
_testControlsNodes();
137140
});
@@ -4088,6 +4091,52 @@ void _testTables() {
40884091
semantics().semanticsEnabled = false;
40894092
}
40904093

4094+
void _testLists() {
4095+
test('nodes with list role', () {
4096+
semantics()
4097+
..debugOverrideTimestampFunction(() => _testTime)
4098+
..semanticsEnabled = true;
4099+
4100+
SemanticsObject pumpSemantics() {
4101+
final SemanticsTester tester = SemanticsTester(owner());
4102+
tester.updateNode(
4103+
id: 0,
4104+
role: ui.SemanticsRole.list,
4105+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
4106+
);
4107+
tester.apply();
4108+
return tester.getSemanticsObject(0);
4109+
}
4110+
4111+
final SemanticsObject object = pumpSemantics();
4112+
expect(object.semanticRole?.kind, EngineSemanticsRole.list);
4113+
expect(object.element.getAttribute('role'), 'list');
4114+
});
4115+
4116+
test('nodes with list item role', () {
4117+
semantics()
4118+
..debugOverrideTimestampFunction(() => _testTime)
4119+
..semanticsEnabled = true;
4120+
4121+
SemanticsObject pumpSemantics() {
4122+
final SemanticsTester tester = SemanticsTester(owner());
4123+
tester.updateNode(
4124+
id: 0,
4125+
role: ui.SemanticsRole.listItem,
4126+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
4127+
);
4128+
tester.apply();
4129+
return tester.getSemanticsObject(0);
4130+
}
4131+
4132+
final SemanticsObject object = pumpSemantics();
4133+
expect(object.semanticRole?.kind, EngineSemanticsRole.listItem);
4134+
expect(object.element.getAttribute('role'), 'listitem');
4135+
});
4136+
4137+
semantics().semanticsEnabled = false;
4138+
}
4139+
40914140
void _testControlsNodes() {
40924141
test('can have multiple controlled nodes', () {
40934142
semantics()

packages/flutter/lib/src/semantics/semantics.dart

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ sealed class _DebugSemanticsRoleChecks {
117117
SemanticsRole.radioGroup => _semanticsRadioGroup,
118118
SemanticsRole.alert => _noLiveRegion,
119119
SemanticsRole.status => _noLiveRegion,
120+
SemanticsRole.list => _noCheckRequired,
121+
SemanticsRole.listItem => _semanticsListItem,
120122
// TODO(chunhtai): add checks when the roles are used in framework.
121123
// https://github.com/flutter/flutter/issues/159741.
122124
SemanticsRole.searchBox => _unimplemented,
@@ -126,8 +128,6 @@ sealed class _DebugSemanticsRoleChecks {
126128
SemanticsRole.menuBar => _unimplemented,
127129
SemanticsRole.menu => _unimplemented,
128130
SemanticsRole.menuItem => _unimplemented,
129-
SemanticsRole.list => _unimplemented,
130-
SemanticsRole.listItem => _unimplemented,
131131
SemanticsRole.form => _unimplemented,
132132
SemanticsRole.tooltip => _unimplemented,
133133
SemanticsRole.loadingSpinner => _unimplemented,
@@ -251,6 +251,25 @@ sealed class _DebugSemanticsRoleChecks {
251251
}
252252
return null;
253253
}
254+
255+
static FlutterError? _semanticsListItem(SemanticsNode node) {
256+
final SemanticsData data = node.getSemanticsData();
257+
final SemanticsNode? parent = node.parent;
258+
if (parent == null) {
259+
return FlutterError(
260+
"Semantics node ${node.id} has role ${data.role} but doesn't have a parent",
261+
);
262+
}
263+
final SemanticsData parentSemanticsData = parent.getSemanticsData();
264+
if (parentSemanticsData.role != SemanticsRole.list) {
265+
return FlutterError(
266+
'Semantics node ${node.id} has role ${data.role}, but its '
267+
"parent node ${parent.id} doesn't have the role ${SemanticsRole.list}. "
268+
'Please assign the ${SemanticsRole.list} to node ${parent.id}',
269+
);
270+
}
271+
return null;
272+
}
254273
}
255274

256275
/// A tag for a [SemanticsNode].

packages/flutter/test/widgets/basic_test.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,33 @@ void main() {
504504
expect(attributedHint.attributes[1].range, const TextRange(start: 6, end: 7));
505505
});
506506

507+
testWidgets('Semantics can use list and list item', (WidgetTester tester) async {
508+
final UniqueKey key1 = UniqueKey();
509+
final UniqueKey key2 = UniqueKey();
510+
await tester.pumpWidget(
511+
MaterialApp(
512+
home: Scaffold(
513+
body: Semantics(
514+
key: key1,
515+
role: SemanticsRole.list,
516+
container: true,
517+
child: Semantics(
518+
key: key2,
519+
role: SemanticsRole.listItem,
520+
container: true,
521+
child: const Placeholder(),
522+
),
523+
),
524+
),
525+
),
526+
);
527+
final SemanticsNode listNode = tester.getSemantics(find.byKey(key1));
528+
final SemanticsNode listItemNode = tester.getSemantics(find.byKey(key2));
529+
530+
expect(listNode.role, SemanticsRole.list);
531+
expect(listItemNode.role, SemanticsRole.listItem);
532+
});
533+
507534
testWidgets('Semantics can merge attributed strings with non attributed string', (
508535
WidgetTester tester,
509536
) async {

packages/flutter/test/widgets/semantics_role_checks_test.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,38 @@ void main() {
5151
});
5252
});
5353

54+
group('list', () {
55+
testWidgets('failure case, list item without list parent', (WidgetTester tester) async {
56+
await tester.pumpWidget(
57+
Directionality(
58+
textDirection: TextDirection.ltr,
59+
child: Semantics(role: SemanticsRole.listItem, child: const Text('some child')),
60+
),
61+
);
62+
final Object? exception = tester.takeException();
63+
expect(exception, isFlutterError);
64+
final FlutterError error = exception! as FlutterError;
65+
expect(
66+
error.message,
67+
startsWith('Semantics node 1 has role ${SemanticsRole.listItem}, but its parent'),
68+
);
69+
});
70+
71+
testWidgets('Success case', (WidgetTester tester) async {
72+
await tester.pumpWidget(
73+
Directionality(
74+
textDirection: TextDirection.ltr,
75+
child: Semantics(
76+
role: SemanticsRole.list,
77+
explicitChildNodes: true,
78+
child: Semantics(role: SemanticsRole.listItem, child: const Text('some child')),
79+
),
80+
),
81+
);
82+
expect(tester.takeException(), isNull);
83+
});
84+
});
85+
5486
group('tabBar', () {
5587
testWidgets('failure case, empty child', (WidgetTester tester) async {
5688
await tester.pumpWidget(

0 commit comments

Comments
 (0)