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 https://github.com/flutter/flutter/issues/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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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:
|
||||
///
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user