From 7cf38fd93c0e99df10f53f7659b01013a9be78e4 Mon Sep 17 00:00:00 2001 From: Hannah Jin Date: Thu, 13 Feb 2025 18:01:28 -0800 Subject: [PATCH] Add table related semantics role (#162339) issue: https://github.com/flutter/flutter/issues/45205 This PR added roles: table,cell,row,columnheader in engine side. Also added table, cell , columnheader in framework widgets `Table` and `DataTable`. This PR didn't add `row` in framework side because right now TableCell is ParentDataWidget expecting its parent to be a Table, tableRow is not a widget or a renderObject, but just some data. If we want to add a role to `row`, we need to do some refactor to `TableCell`, `TableRow` and `Table`. If we need to add row, I will do that in another PR. ## 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]. [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 --- .../ci/licenses_golden/licenses_flutter | 2 + engine/src/flutter/lib/ui/semantics.dart | 32 ++++++- .../flutter/lib/ui/semantics/semantics_node.h | 4 + .../src/flutter/lib/web_ui/lib/semantics.dart | 14 ++- .../flutter/lib/web_ui/lib/src/engine.dart | 1 + .../lib/web_ui/lib/src/engine/semantics.dart | 1 + .../lib/src/engine/semantics/semantics.dart | 25 +++++ .../lib/src/engine/semantics/table.dart | 82 +++++++++++++++++ .../test/engine/semantics/semantics_test.dart | 91 +++++++++++++++++++ .../flutter/lib/src/material/data_table.dart | 36 ++++---- packages/flutter/lib/src/rendering/table.dart | 9 ++ .../flutter/lib/src/semantics/semantics.dart | 20 ++++ packages/flutter/lib/src/widgets/table.dart | 4 +- .../test/material/data_table_test.dart | 74 +++++++++++++++ packages/flutter/test/widgets/table_test.dart | 63 ++++++++++++- 15 files changed, 437 insertions(+), 21 deletions(-) create mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/semantics/table.dart diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 43dd4bd6b6..a595ac041d 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -42731,6 +42731,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart + ../../ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/table.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/tabs.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/tappable.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/text_field.dart + ../../../flutter/LICENSE @@ -45705,6 +45706,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/table.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/tabs.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/tappable.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/text_field.dart diff --git a/engine/src/flutter/lib/ui/semantics.dart b/engine/src/flutter/lib/ui/semantics.dart index 681b3ab0d7..0628722dd0 100644 --- a/engine/src/flutter/lib/ui/semantics.dart +++ b/engine/src/flutter/lib/ui/semantics.dart @@ -348,14 +348,14 @@ enum SemanticsRole { /// A tab button. /// - /// see also: + /// See also: /// /// * [tabBar], which is the role for containers of tab buttons. tab, /// Contains tab buttons. /// - /// see also: + /// See also: /// /// * [tab], which is the role for tab buttons. tabBar, @@ -368,6 +368,34 @@ enum SemanticsRole { /// An alert dialog. alertDialog, + + /// A table structure containing data arranged in rows and columns. + /// + /// See also: + /// + /// * [cell], [row], [columnHeader] for table related roles. + table, + + /// A cell in a [table] that does not contain column or row header information. + /// + /// See also: + /// + /// * [table],[row], [columnHeader] for table related roles. + cell, + + /// A row of [cell]s or or [columnHeader]s in a [table]. + /// + /// See also: + /// + /// * [table] ,[cell],[columnHeader] for table related roles. + row, + + /// A cell in a [table] contains header information for a column. + /// + /// See also: + /// + /// * [table] ,[cell], [row] for table related roles. + columnHeader, } /// A Boolean value that can be associated with a semantics node. diff --git a/engine/src/flutter/lib/ui/semantics/semantics_node.h b/engine/src/flutter/lib/ui/semantics/semantics_node.h index 463ccbcab5..3178b71ab7 100644 --- a/engine/src/flutter/lib/ui/semantics/semantics_node.h +++ b/engine/src/flutter/lib/ui/semantics/semantics_node.h @@ -68,6 +68,10 @@ enum class SemanticsRole : int32_t { kTab = 1, kTabBar = 2, kTabPanel = 3, + kTable = 4, + kCell = 5, + kRow = 6, + kColumnHeader = 7, }; /// C/C++ representation of `SemanticsFlags` defined in diff --git a/engine/src/flutter/lib/web_ui/lib/semantics.dart b/engine/src/flutter/lib/web_ui/lib/semantics.dart index 1ae7efab33..3dcfce67d4 100644 --- a/engine/src/flutter/lib/web_ui/lib/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/semantics.dart @@ -256,7 +256,19 @@ class SemanticsFlag { } // Mirrors engine/src/flutter/lib/ui/semantics.dart -enum SemanticsRole { none, tab, tabBar, tabPanel, dialog, alertDialog } + +enum SemanticsRole { + none, + tab, + tabBar, + tabPanel, + dialog, + alertDialog, + table, + cell, + row, + columnHeader, +} // When adding a new StringAttributeType, the classes in these file must be // updated as well. 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 da467d0ad5..9df319b582 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -160,6 +160,7 @@ export 'engine/semantics/route.dart'; export 'engine/semantics/scrollable.dart'; export 'engine/semantics/semantics.dart'; export 'engine/semantics/semantics_helper.dart'; +export 'engine/semantics/table.dart'; export 'engine/semantics/tabs.dart'; export 'engine/semantics/tappable.dart'; export 'engine/semantics/text_field.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 a33fd833e5..ff2726bae5 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 @@ -16,6 +16,7 @@ export 'semantics/platform_view.dart'; export 'semantics/scrollable.dart'; export 'semantics/semantics.dart'; export 'semantics/semantics_helper.dart'; +export 'semantics/table.dart'; export 'semantics/tabs.dart'; export 'semantics/tappable.dart'; export 'semantics/text_field.dart'; 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 78a66b0d2f..842bd949cb 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 @@ -32,6 +32,7 @@ import 'platform_view.dart'; import 'route.dart'; import 'scrollable.dart'; import 'semantics_helper.dart'; +import 'table.dart'; import 'tabs.dart'; import 'tappable.dart'; import 'text_field.dart'; @@ -422,6 +423,18 @@ enum EngineSemanticsRole { /// An alert dialog. alertDialog, + /// A table structure containing data arranged in rows and columns. + table, + + /// A cell in a [table] that does not contain column or row header information. + cell, + + /// A row of [cell]s or or [columnHeader]s in a [table]. + row, + + /// A cell in a [table] contains header information for a column. + columnHeader, + /// A role used when a more specific role cannot be assigend to /// a [SemanticsObject]. /// @@ -1755,6 +1768,14 @@ class SemanticsObject { return EngineSemanticsRole.dialog; case ui.SemanticsRole.alertDialog: return EngineSemanticsRole.alertDialog; + case ui.SemanticsRole.table: + return EngineSemanticsRole.table; + case ui.SemanticsRole.cell: + return EngineSemanticsRole.cell; + case ui.SemanticsRole.row: + return EngineSemanticsRole.row; + case ui.SemanticsRole.columnHeader: + return EngineSemanticsRole.columnHeader; case ui.SemanticsRole.none: // fallback to checking semantics properties. } @@ -1806,6 +1827,10 @@ class SemanticsObject { EngineSemanticsRole.tabPanel => SemanticTabPanel(this), EngineSemanticsRole.dialog => SemanticDialog(this), EngineSemanticsRole.alertDialog => SemanticAlertDialog(this), + EngineSemanticsRole.table => SemanticTable(this), + EngineSemanticsRole.cell => SemanticCell(this), + EngineSemanticsRole.row => SemanticRow(this), + EngineSemanticsRole.columnHeader => SemanticColumnHeader(this), EngineSemanticsRole.generic => GenericRole(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 new file mode 100644 index 0000000000..d06cd43370 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/table.dart @@ -0,0 +1,82 @@ +// 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 table element. +/// +/// Uses aria table role to convey this semantic information to the element. +/// +/// Screen-readers takes advantage of "aria-label" to describe the visual. +class SemanticTable extends SemanticRole { + SemanticTable(SemanticsObject semanticsObject) + : super.withBasics( + EngineSemanticsRole.table, + semanticsObject, + preferredLabelRepresentation: LabelRepresentation.ariaLabel, + ) { + setAriaRole('table'); + } + + @override + bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false; +} + +/// Indicates a table cell element. +/// +/// Uses aria cell role to convey this semantic information to the element. +/// +/// Screen-readers takes advantage of "aria-label" to describe the visual. +class SemanticCell extends SemanticRole { + SemanticCell(SemanticsObject semanticsObject) + : super.withBasics( + EngineSemanticsRole.cell, + semanticsObject, + preferredLabelRepresentation: LabelRepresentation.ariaLabel, + ) { + setAriaRole('cell'); + } + + @override + bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false; +} + +/// Indicates a table row element. +/// +/// Uses aria row role to convey this semantic information to the element. +/// +/// Screen-readers takes advantage of "aria-label" to describe the visual. +class SemanticRow extends SemanticRole { + SemanticRow(SemanticsObject semanticsObject) + : super.withBasics( + EngineSemanticsRole.row, + semanticsObject, + preferredLabelRepresentation: LabelRepresentation.ariaLabel, + ) { + setAriaRole('row'); + } + + @override + bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false; +} + +/// Indicates a table column header element. +/// +/// Uses aria columnheader role to convey this semantic information to the element. +/// +/// Screen-readers takes advantage of "aria-label" to describe the visual. +class SemanticColumnHeader extends SemanticRole { + SemanticColumnHeader(SemanticsObject semanticsObject) + : super.withBasics( + EngineSemanticsRole.columnHeader, + semanticsObject, + preferredLabelRepresentation: LabelRepresentation.ariaLabel, + ) { + setAriaRole('columnheader'); + } + + @override + bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false; +} 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 e326dedc75..bc8712e827 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 @@ -123,6 +123,9 @@ void runSemanticsTests() { group('tabs', () { _testTabs(); }); + group('table', () { + _testTables(); + }); } void _testSemanticRole() { @@ -3723,6 +3726,94 @@ void _testTabs() { }); } +void _testTables() { + test('nodes with table role', () { + semantics() + ..debugOverrideTimestampFunction(() => _testTime) + ..semanticsEnabled = true; + + SemanticsObject pumpSemantics() { + final SemanticsTester tester = SemanticsTester(owner()); + tester.updateNode( + id: 0, + role: ui.SemanticsRole.table, + rect: const ui.Rect.fromLTRB(0, 0, 100, 50), + ); + tester.apply(); + return tester.getSemanticsObject(0); + } + + final SemanticsObject object = pumpSemantics(); + expect(object.semanticRole?.kind, EngineSemanticsRole.table); + expect(object.element.getAttribute('role'), 'table'); + }); + + test('nodes with cell role', () { + semantics() + ..debugOverrideTimestampFunction(() => _testTime) + ..semanticsEnabled = true; + + SemanticsObject pumpSemantics() { + final SemanticsTester tester = SemanticsTester(owner()); + tester.updateNode( + id: 0, + role: ui.SemanticsRole.cell, + rect: const ui.Rect.fromLTRB(0, 0, 100, 50), + ); + tester.apply(); + return tester.getSemanticsObject(0); + } + + final SemanticsObject object = pumpSemantics(); + expect(object.semanticRole?.kind, EngineSemanticsRole.cell); + expect(object.element.getAttribute('role'), 'cell'); + }); + + test('nodes with row role', () { + semantics() + ..debugOverrideTimestampFunction(() => _testTime) + ..semanticsEnabled = true; + + SemanticsObject pumpSemantics() { + final SemanticsTester tester = SemanticsTester(owner()); + tester.updateNode( + id: 0, + role: ui.SemanticsRole.row, + rect: const ui.Rect.fromLTRB(0, 0, 100, 50), + ); + tester.apply(); + return tester.getSemanticsObject(0); + } + + final SemanticsObject object = pumpSemantics(); + expect(object.semanticRole?.kind, EngineSemanticsRole.row); + expect(object.element.getAttribute('role'), 'row'); + }); + + test('nodes with column header role', () { + semantics() + ..debugOverrideTimestampFunction(() => _testTime) + ..semanticsEnabled = true; + + SemanticsObject pumpSemantics() { + final SemanticsTester tester = SemanticsTester(owner()); + tester.updateNode( + id: 0, + role: ui.SemanticsRole.columnHeader, + rect: const ui.Rect.fromLTRB(0, 0, 100, 50), + ); + tester.apply(); + return tester.getSemanticsObject(0); + } + + final SemanticsObject object = pumpSemantics(); + expect(object.semanticRole?.kind, EngineSemanticsRole.columnHeader); + expect(object.element.getAttribute('role'), 'columnheader'); + }); + + semantics().semanticsEnabled = false; +} + /// A facade in front of [ui.SemanticsUpdateBuilder.updateNode] that /// supplies default values for semantics attributes. void updateNode( diff --git a/packages/flutter/lib/src/material/data_table.dart b/packages/flutter/lib/src/material/data_table.dart index 0f0fb97577..1d84ea23d4 100644 --- a/packages/flutter/lib/src/material/data_table.dart +++ b/packages/flutter/lib/src/material/data_table.dart @@ -7,6 +7,7 @@ library; import 'dart:math' as math; +import 'dart:ui' show SemanticsRole; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -879,22 +880,25 @@ class DataTable extends StatelessWidget { }) { final ThemeData themeData = Theme.of(context); final DataTableThemeData dataTableTheme = DataTableTheme.of(context); - label = Row( - textDirection: numeric ? TextDirection.rtl : null, - mainAxisAlignment: headingRowAlignment, - children: [ - if (headingRowAlignment == MainAxisAlignment.center && onSort != null) - const SizedBox(width: _SortArrowState._arrowIconSize + _sortArrowPadding), - label, - if (onSort != null) ...[ - _SortArrow( - visible: sorted, - up: sorted ? ascending : null, - duration: _sortArrowAnimationDuration, - ), - const SizedBox(width: _sortArrowPadding), + label = Semantics( + role: SemanticsRole.columnHeader, + child: Row( + textDirection: numeric ? TextDirection.rtl : null, + mainAxisAlignment: headingRowAlignment, + children: [ + if (headingRowAlignment == MainAxisAlignment.center && onSort != null) + const SizedBox(width: _SortArrowState._arrowIconSize + _sortArrowPadding), + label, + if (onSort != null) ...[ + _SortArrow( + visible: sorted, + up: sorted ? ascending : null, + duration: _sortArrowAnimationDuration, + ), + const SizedBox(width: _sortArrowPadding), + ], ], - ], + ), ); final TextStyle effectiveHeadingTextStyle = @@ -1013,7 +1017,7 @@ class DataTable extends StatelessWidget { child: label, ); } - return label; + return TableCell(child: label); } @override diff --git a/packages/flutter/lib/src/rendering/table.dart b/packages/flutter/lib/src/rendering/table.dart index f90c7c293d..4a90a64cd1 100644 --- a/packages/flutter/lib/src/rendering/table.dart +++ b/packages/flutter/lib/src/rendering/table.dart @@ -4,8 +4,10 @@ import 'dart:collection'; import 'dart:math' as math; +import 'dart:ui' show SemanticsRole; import 'package:flutter/foundation.dart'; +import 'package:flutter/semantics.dart'; import 'box.dart'; import 'object.dart'; @@ -601,6 +603,13 @@ class RenderTable extends RenderBox { } } + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.role = SemanticsRole.table; + config.explicitChildNodes = true; + } + /// Replaces the children of this table with the given cells. /// /// The cells are divided into the specified number of columns before diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index fe7731844f..157cb420ce 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -110,8 +110,14 @@ sealed class _DebugSemanticsRoleChecks { SemanticsRole.tab => _semanticsTab, SemanticsRole.tabBar => _semanticsTabBar, SemanticsRole.tabPanel => _noCheckRequired, + SemanticsRole.table => _noCheckRequired, + SemanticsRole.cell => _semanticsCell, + SemanticsRole.row => _unimplementedError, + SemanticsRole.columnHeader => _semanticsColumnHeader, }(node); + static FlutterError? _unimplementedError(SemanticsNode node) => + FlutterError('This semantics role is not implemented'); static FlutterError? _noCheckRequired(SemanticsNode node) => null; static FlutterError? _semanticsTab(SemanticsNode node) { @@ -140,6 +146,20 @@ sealed class _DebugSemanticsRoleChecks { }); return error; } + + static FlutterError? _semanticsCell(SemanticsNode node) { + if (node.parent?.role != SemanticsRole.table) { + return FlutterError('A cell must be a child of a table'); + } + return null; + } + + static FlutterError? _semanticsColumnHeader(SemanticsNode node) { + if (node.parent?.role != SemanticsRole.table) { + return FlutterError('A columnHeader must be a child of a table'); + } + return null; + } } /// A tag for a [SemanticsNode]. diff --git a/packages/flutter/lib/src/widgets/table.dart b/packages/flutter/lib/src/widgets/table.dart index e879426eb8..ce315ef9f9 100644 --- a/packages/flutter/lib/src/widgets/table.dart +++ b/packages/flutter/lib/src/widgets/table.dart @@ -7,6 +7,7 @@ library; import 'dart:collection'; +import 'dart:ui' show SemanticsRole; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -438,7 +439,8 @@ class _TableElement extends RenderObjectElement { /// as the [child]. class TableCell extends ParentDataWidget { /// Creates a widget that controls how a child of a [Table] is aligned. - const TableCell({super.key, this.verticalAlignment, required super.child}); + TableCell({super.key, this.verticalAlignment, required Widget child}) + : super(child: Semantics(role: SemanticsRole.cell, child: child)); /// How this cell is aligned vertically. final TableCellVerticalAlignment? verticalAlignment; diff --git a/packages/flutter/test/material/data_table_test.dart b/packages/flutter/test/material/data_table_test.dart index 0e5d62c64e..80dc2072eb 100644 --- a/packages/flutter/test/material/data_table_test.dart +++ b/packages/flutter/test/material/data_table_test.dart @@ -6,6 +6,7 @@ library; import 'dart:math' as math; +import 'dart:ui' show SemanticsRole; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -13,6 +14,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:vector_math/vector_math_64.dart' show Matrix3; +import '../widgets/semantics_tester.dart'; import 'data_table_test_utils.dart'; void main() { @@ -2211,6 +2213,78 @@ void main() { expect(table.columnWidths![1], const IntrinsicColumnWidth()); expect(table.columnWidths![2], const IntrinsicColumnWidth(flex: 1)); }); + + testWidgets('DataTable has correct roles in semantics', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DataTable( + columns: const [ + DataColumn(label: Text('Column 1')), + DataColumn(label: Text('Column 2')), + ], + rows: const [ + DataRow( + cells: [DataCell(Text('Data Cell 1')), DataCell(Text('Data Cell 2'))], + ), + ], + ), + ), + ), + ); + + final TestSemantics expectedSemantics = TestSemantics.root( + children: [ + TestSemantics( + textDirection: TextDirection.ltr, + children: [ + TestSemantics( + children: [ + TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + role: SemanticsRole.table, + children: [ + TestSemantics( + label: 'Column 1', + textDirection: TextDirection.ltr, + role: SemanticsRole.columnHeader, + ), + TestSemantics( + label: 'Column 2', + textDirection: TextDirection.ltr, + role: SemanticsRole.columnHeader, + ), + TestSemantics( + label: 'Data Cell 1', + textDirection: TextDirection.ltr, + role: SemanticsRole.cell, + ), + TestSemantics( + label: 'Data Cell 2', + textDirection: TextDirection.ltr, + role: SemanticsRole.cell, + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ); + + expect( + semantics, + hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true), + ); + + semantics.dispose(); + }); } RenderParagraph _getTextRenderObject(WidgetTester tester, String text) { diff --git a/packages/flutter/test/widgets/table_test.dart b/packages/flutter/test/widgets/table_test.dart index 868a6ca777..32c3352e6b 100644 --- a/packages/flutter/test/widgets/table_test.dart +++ b/packages/flutter/test/widgets/table_test.dart @@ -2,9 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show SemanticsRole; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'semantics_tester.dart'; class TestStatefulWidget extends StatefulWidget { const TestStatefulWidget({super.key}); @@ -951,5 +953,64 @@ void main() { expect(boxD.size.height, greaterThan(boxA.size.height)); }); - // TODO(ianh): Test handling of TableCell object + testWidgets('Table has correct roles in semantics', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Table( + children: [ + TableRow( + children: [ + TableCell(child: const Text('Data Cell 1')), + TableCell(child: const Text('Data Cell 2')), + ], + ), + ], + ), + ), + ), + ); + + final TestSemantics expectedSemantics = TestSemantics.root( + children: [ + TestSemantics( + textDirection: TextDirection.ltr, + children: [ + TestSemantics( + children: [ + TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + role: SemanticsRole.table, + children: [ + TestSemantics( + label: 'Data Cell 1', + textDirection: TextDirection.ltr, + role: SemanticsRole.cell, + ), + TestSemantics( + label: 'Data Cell 2', + textDirection: TextDirection.ltr, + role: SemanticsRole.cell, + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ); + + expect( + semantics, + hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true), + ); + + semantics.dispose(); + }); }