diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 22db36f9f4..073e005ea7 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -42625,6 +42625,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart + ../../ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/list.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart + ../../../flutter/LICENSE 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 FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/list.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart index 3fa9ce9b6c..69347c9a74 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -114,6 +114,7 @@ export 'engine/semantics/image.dart'; export 'engine/semantics/incrementable.dart'; export 'engine/semantics/label_and_value.dart'; export 'engine/semantics/link.dart'; +export 'engine/semantics/list.dart'; export 'engine/semantics/live_region.dart'; export 'engine/semantics/platform_view.dart'; export 'engine/semantics/route.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart index b25d6e9eab..cd30dbda03 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart @@ -13,6 +13,7 @@ export 'semantics/image.dart'; export 'semantics/incrementable.dart'; export 'semantics/label_and_value.dart'; export 'semantics/link.dart'; +export 'semantics/list.dart'; export 'semantics/live_region.dart'; export 'semantics/platform_view.dart'; export 'semantics/scrollable.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/image.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/image.dart index bd99543a17..f737c3b6e7 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/image.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/image.dart @@ -9,7 +9,7 @@ import 'semantics.dart'; /// /// Uses aria img role to convey this semantic information to the element. /// -/// Screen-readers takes advantage of "aria-label" to describe the visual. +/// Screen-readers take advantage of "aria-label" to describe the visual. class SemanticImage extends SemanticRole { SemanticImage(SemanticsObject semanticsObject) : super.blank(EngineSemanticsRole.image, semanticsObject) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/list.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/list.dart new file mode 100644 index 0000000000..2b54416d72 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/list.dart @@ -0,0 +1,44 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'label_and_value.dart'; +import 'semantics.dart'; + +/// Indicates a list container. +/// +/// Uses aria list role to convey this semantic information to the element. +/// +/// Screen-readers take advantage of "aria-label" to describe the visual. +class SemanticList extends SemanticRole { + SemanticList(SemanticsObject semanticsObject) + : super.withBasics( + EngineSemanticsRole.list, + semanticsObject, + preferredLabelRepresentation: LabelRepresentation.ariaLabel, + ) { + setAriaRole('list'); + } + + @override + bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false; +} + +/// Indicates an item in a list. +/// +/// Uses aria listitem role to convey this semantic information to the element. +/// +/// Screen-readers take advantage of "aria-label" to describe the visual. +class SemanticListItem extends SemanticRole { + SemanticListItem(SemanticsObject semanticsObject) + : super.withBasics( + EngineSemanticsRole.listItem, + semanticsObject, + preferredLabelRepresentation: LabelRepresentation.ariaLabel, + ) { + setAriaRole('listitem'); + } + + @override + bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false; +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/route.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/route.dart index b8300c9703..30de3de023 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/route.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/route.dart @@ -137,7 +137,7 @@ class SemanticDialog extends SemanticRouteBase { /// Setting this role will also set aria-modal to true, which helps screen /// reader better understand this section of screen. /// -/// Screen-readers takes advantage of "aria-label" to describe the visual. +/// Screen-readers take advantage of "aria-label" to describe the visual. /// /// See also: /// diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart index 375c3ba0a2..f12aa7fdb4 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -29,6 +29,7 @@ import 'image.dart'; import 'incrementable.dart'; import 'label_and_value.dart'; import 'link.dart'; +import 'list.dart'; import 'live_region.dart'; import 'platform_view.dart'; import 'route.dart'; @@ -451,6 +452,12 @@ enum EngineSemanticsRole { /// A component provide important and usually time-sensitive information. alert, + /// A container whose children are logically a list of items. + list, + + /// An item in a [list]. + listItem, + /// A role used when a more specific role cannot be assigend to /// a [SemanticsObject]. /// @@ -1859,6 +1866,10 @@ class SemanticsObject { return EngineSemanticsRole.alert; case ui.SemanticsRole.status: return EngineSemanticsRole.status; + case ui.SemanticsRole.list: + return EngineSemanticsRole.list; + case ui.SemanticsRole.listItem: + return EngineSemanticsRole.listItem; // TODO(chunhtai): implement these roles. // https://github.com/flutter/flutter/issues/159741. case ui.SemanticsRole.searchBox: @@ -1868,8 +1879,6 @@ class SemanticsObject { case ui.SemanticsRole.menuBar: case ui.SemanticsRole.menu: case ui.SemanticsRole.menuItem: - case ui.SemanticsRole.list: - case ui.SemanticsRole.listItem: case ui.SemanticsRole.form: case ui.SemanticsRole.tooltip: case ui.SemanticsRole.loadingSpinner: @@ -1920,6 +1929,8 @@ class SemanticsObject { EngineSemanticsRole.image => SemanticImage(this), EngineSemanticsRole.platformView => SemanticPlatformView(this), EngineSemanticsRole.link => SemanticLink(this), + EngineSemanticsRole.list => SemanticList(this), + EngineSemanticsRole.listItem => SemanticListItem(this), EngineSemanticsRole.heading => SemanticHeading(this), EngineSemanticsRole.header => SemanticHeader(this), EngineSemanticsRole.tab => SemanticTab(this), diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/table.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/table.dart index d06cd43370..e43b496639 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/table.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/table.dart @@ -9,7 +9,7 @@ import 'semantics.dart'; /// /// Uses aria table role to convey this semantic information to the element. /// -/// Screen-readers takes advantage of "aria-label" to describe the visual. +/// Screen-readers take advantage of "aria-label" to describe the visual. class SemanticTable extends SemanticRole { SemanticTable(SemanticsObject semanticsObject) : super.withBasics( @@ -28,7 +28,7 @@ class SemanticTable extends SemanticRole { /// /// Uses aria cell role to convey this semantic information to the element. /// -/// Screen-readers takes advantage of "aria-label" to describe the visual. +/// Screen-readers take advantage of "aria-label" to describe the visual. class SemanticCell extends SemanticRole { SemanticCell(SemanticsObject semanticsObject) : super.withBasics( @@ -47,7 +47,7 @@ class SemanticCell extends SemanticRole { /// /// Uses aria row role to convey this semantic information to the element. /// -/// Screen-readers takes advantage of "aria-label" to describe the visual. +/// Screen-readers take advantage of "aria-label" to describe the visual. class SemanticRow extends SemanticRole { SemanticRow(SemanticsObject semanticsObject) : super.withBasics( @@ -66,7 +66,7 @@ class SemanticRow extends SemanticRole { /// /// Uses aria columnheader role to convey this semantic information to the element. /// -/// Screen-readers takes advantage of "aria-label" to describe the visual. +/// Screen-readers take advantage of "aria-label" to describe the visual. class SemanticColumnHeader extends SemanticRole { SemanticColumnHeader(SemanticsObject semanticsObject) : super.withBasics( diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/tabs.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/tabs.dart index 9966344cfc..b1b95b394c 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/tabs.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/tabs.dart @@ -10,7 +10,7 @@ import 'semantics.dart'; /// /// Uses aria tab role to convey this semantic information to the element. /// -/// Screen-readers takes advantage of "aria-label" to describe the visual. +/// Screen-readers take advantage of "aria-label" to describe the visual. class SemanticTab extends SemanticRole { SemanticTab(SemanticsObject semanticsObject) : super.withBasics( @@ -29,7 +29,7 @@ class SemanticTab extends SemanticRole { /// /// Uses aria tabpanel role to convey this semantic information to the element. /// -/// Screen-readers takes advantage of "aria-label" to describe the visual. +/// Screen-readers take advantage of "aria-label" to describe the visual. class SemanticTabPanel extends SemanticRole { SemanticTabPanel(SemanticsObject semanticsObject) : super.withBasics( @@ -48,7 +48,7 @@ class SemanticTabPanel extends SemanticRole { /// /// Uses aria tablist role to convey this semantic information to the element. /// -/// Screen-readers takes advantage of "aria-label" to describe the visual. +/// Screen-readers take advantage of "aria-label" to describe the visual. class SemanticTabList extends SemanticRole { SemanticTabList(SemanticsObject semanticsObject) : super.withBasics( diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart index 1e3d786c40..e7b256b355 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -132,6 +132,9 @@ void runSemanticsTests() { group('table', () { _testTables(); }); + group('list', () { + _testLists(); + }); group('controlsNodes', () { _testControlsNodes(); }); @@ -4088,6 +4091,52 @@ void _testTables() { semantics().semanticsEnabled = false; } +void _testLists() { + test('nodes with list role', () { + semantics() + ..debugOverrideTimestampFunction(() => _testTime) + ..semanticsEnabled = true; + + SemanticsObject pumpSemantics() { + final SemanticsTester tester = SemanticsTester(owner()); + tester.updateNode( + id: 0, + role: ui.SemanticsRole.list, + rect: const ui.Rect.fromLTRB(0, 0, 100, 50), + ); + tester.apply(); + return tester.getSemanticsObject(0); + } + + final SemanticsObject object = pumpSemantics(); + expect(object.semanticRole?.kind, EngineSemanticsRole.list); + expect(object.element.getAttribute('role'), 'list'); + }); + + test('nodes with list item role', () { + semantics() + ..debugOverrideTimestampFunction(() => _testTime) + ..semanticsEnabled = true; + + SemanticsObject pumpSemantics() { + final SemanticsTester tester = SemanticsTester(owner()); + tester.updateNode( + id: 0, + role: ui.SemanticsRole.listItem, + rect: const ui.Rect.fromLTRB(0, 0, 100, 50), + ); + tester.apply(); + return tester.getSemanticsObject(0); + } + + final SemanticsObject object = pumpSemantics(); + expect(object.semanticRole?.kind, EngineSemanticsRole.listItem); + expect(object.element.getAttribute('role'), 'listitem'); + }); + + semantics().semanticsEnabled = false; +} + void _testControlsNodes() { test('can have multiple controlled nodes', () { semantics() diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index ca60d96dba..44b30abe7f 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -117,6 +117,8 @@ sealed class _DebugSemanticsRoleChecks { SemanticsRole.radioGroup => _semanticsRadioGroup, SemanticsRole.alert => _noLiveRegion, SemanticsRole.status => _noLiveRegion, + SemanticsRole.list => _noCheckRequired, + SemanticsRole.listItem => _semanticsListItem, // TODO(chunhtai): add checks when the roles are used in framework. // https://github.com/flutter/flutter/issues/159741. SemanticsRole.searchBox => _unimplemented, @@ -126,8 +128,6 @@ sealed class _DebugSemanticsRoleChecks { SemanticsRole.menuBar => _unimplemented, SemanticsRole.menu => _unimplemented, SemanticsRole.menuItem => _unimplemented, - SemanticsRole.list => _unimplemented, - SemanticsRole.listItem => _unimplemented, SemanticsRole.form => _unimplemented, SemanticsRole.tooltip => _unimplemented, SemanticsRole.loadingSpinner => _unimplemented, @@ -251,6 +251,25 @@ sealed class _DebugSemanticsRoleChecks { } return null; } + + static FlutterError? _semanticsListItem(SemanticsNode node) { + final SemanticsData data = node.getSemanticsData(); + final SemanticsNode? parent = node.parent; + if (parent == null) { + return FlutterError( + "Semantics node ${node.id} has role ${data.role} but doesn't have a parent", + ); + } + final SemanticsData parentSemanticsData = parent.getSemanticsData(); + if (parentSemanticsData.role != SemanticsRole.list) { + return FlutterError( + 'Semantics node ${node.id} has role ${data.role}, but its ' + "parent node ${parent.id} doesn't have the role ${SemanticsRole.list}. " + 'Please assign the ${SemanticsRole.list} to node ${parent.id}', + ); + } + return null; + } } /// A tag for a [SemanticsNode]. diff --git a/packages/flutter/test/widgets/basic_test.dart b/packages/flutter/test/widgets/basic_test.dart index 5c1a4ecb8a..5310051e57 100644 --- a/packages/flutter/test/widgets/basic_test.dart +++ b/packages/flutter/test/widgets/basic_test.dart @@ -504,6 +504,33 @@ void main() { expect(attributedHint.attributes[1].range, const TextRange(start: 6, end: 7)); }); + testWidgets('Semantics can use list and list item', (WidgetTester tester) async { + final UniqueKey key1 = UniqueKey(); + final UniqueKey key2 = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Semantics( + key: key1, + role: SemanticsRole.list, + container: true, + child: Semantics( + key: key2, + role: SemanticsRole.listItem, + container: true, + child: const Placeholder(), + ), + ), + ), + ), + ); + final SemanticsNode listNode = tester.getSemantics(find.byKey(key1)); + final SemanticsNode listItemNode = tester.getSemantics(find.byKey(key2)); + + expect(listNode.role, SemanticsRole.list); + expect(listItemNode.role, SemanticsRole.listItem); + }); + testWidgets('Semantics can merge attributed strings with non attributed string', ( WidgetTester tester, ) async { diff --git a/packages/flutter/test/widgets/semantics_role_checks_test.dart b/packages/flutter/test/widgets/semantics_role_checks_test.dart index c7dc02a233..fefeb4f9eb 100644 --- a/packages/flutter/test/widgets/semantics_role_checks_test.dart +++ b/packages/flutter/test/widgets/semantics_role_checks_test.dart @@ -51,6 +51,38 @@ void main() { }); }); + group('list', () { + testWidgets('failure case, list item without list parent', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Semantics(role: SemanticsRole.listItem, child: const Text('some child')), + ), + ); + final Object? exception = tester.takeException(); + expect(exception, isFlutterError); + final FlutterError error = exception! as FlutterError; + expect( + error.message, + startsWith('Semantics node 1 has role ${SemanticsRole.listItem}, but its parent'), + ); + }); + + testWidgets('Success case', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Semantics( + role: SemanticsRole.list, + explicitChildNodes: true, + child: Semantics(role: SemanticsRole.listItem, child: const Text('some child')), + ), + ), + ); + expect(tester.takeException(), isNull); + }); + }); + group('tabBar', () { testWidgets('failure case, empty child', (WidgetTester tester) async { await tester.pumpWidget(