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].

<!-- 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:
Hannah Jin 2025-02-13 18:01:28 -08:00 committed by GitHub
parent 0fc52bc91f
commit 7cf38fd93c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 437 additions and 21 deletions

View File

@ -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/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.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/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/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/tappable.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/text_field.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/scrollable.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.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/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/tabs.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/tappable.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/tappable.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/text_field.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/text_field.dart

View File

@ -348,14 +348,14 @@ enum SemanticsRole {
/// A tab button. /// A tab button.
/// ///
/// see also: /// See also:
/// ///
/// * [tabBar], which is the role for containers of tab buttons. /// * [tabBar], which is the role for containers of tab buttons.
tab, tab,
/// Contains tab buttons. /// Contains tab buttons.
/// ///
/// see also: /// See also:
/// ///
/// * [tab], which is the role for tab buttons. /// * [tab], which is the role for tab buttons.
tabBar, tabBar,
@ -368,6 +368,34 @@ enum SemanticsRole {
/// An alert dialog. /// An alert dialog.
alertDialog, 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. /// A Boolean value that can be associated with a semantics node.

View File

@ -68,6 +68,10 @@ enum class SemanticsRole : int32_t {
kTab = 1, kTab = 1,
kTabBar = 2, kTabBar = 2,
kTabPanel = 3, kTabPanel = 3,
kTable = 4,
kCell = 5,
kRow = 6,
kColumnHeader = 7,
}; };
/// C/C++ representation of `SemanticsFlags` defined in /// C/C++ representation of `SemanticsFlags` defined in

View File

@ -256,7 +256,19 @@ class SemanticsFlag {
} }
// Mirrors engine/src/flutter/lib/ui/semantics.dart // 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 // When adding a new StringAttributeType, the classes in these file must be
// updated as well. // updated as well.

View File

@ -160,6 +160,7 @@ export 'engine/semantics/route.dart';
export 'engine/semantics/scrollable.dart'; export 'engine/semantics/scrollable.dart';
export 'engine/semantics/semantics.dart'; export 'engine/semantics/semantics.dart';
export 'engine/semantics/semantics_helper.dart'; export 'engine/semantics/semantics_helper.dart';
export 'engine/semantics/table.dart';
export 'engine/semantics/tabs.dart'; export 'engine/semantics/tabs.dart';
export 'engine/semantics/tappable.dart'; export 'engine/semantics/tappable.dart';
export 'engine/semantics/text_field.dart'; export 'engine/semantics/text_field.dart';

View File

@ -16,6 +16,7 @@ export 'semantics/platform_view.dart';
export 'semantics/scrollable.dart'; export 'semantics/scrollable.dart';
export 'semantics/semantics.dart'; export 'semantics/semantics.dart';
export 'semantics/semantics_helper.dart'; export 'semantics/semantics_helper.dart';
export 'semantics/table.dart';
export 'semantics/tabs.dart'; export 'semantics/tabs.dart';
export 'semantics/tappable.dart'; export 'semantics/tappable.dart';
export 'semantics/text_field.dart'; export 'semantics/text_field.dart';

View File

@ -32,6 +32,7 @@ import 'platform_view.dart';
import 'route.dart'; import 'route.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'semantics_helper.dart'; import 'semantics_helper.dart';
import 'table.dart';
import 'tabs.dart'; import 'tabs.dart';
import 'tappable.dart'; import 'tappable.dart';
import 'text_field.dart'; import 'text_field.dart';
@ -422,6 +423,18 @@ enum EngineSemanticsRole {
/// An alert dialog. /// An alert dialog.
alertDialog, 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 role used when a more specific role cannot be assigend to
/// a [SemanticsObject]. /// a [SemanticsObject].
/// ///
@ -1755,6 +1768,14 @@ class SemanticsObject {
return EngineSemanticsRole.dialog; return EngineSemanticsRole.dialog;
case ui.SemanticsRole.alertDialog: case ui.SemanticsRole.alertDialog:
return EngineSemanticsRole.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: case ui.SemanticsRole.none:
// fallback to checking semantics properties. // fallback to checking semantics properties.
} }
@ -1806,6 +1827,10 @@ class SemanticsObject {
EngineSemanticsRole.tabPanel => SemanticTabPanel(this), EngineSemanticsRole.tabPanel => SemanticTabPanel(this),
EngineSemanticsRole.dialog => SemanticDialog(this), EngineSemanticsRole.dialog => SemanticDialog(this),
EngineSemanticsRole.alertDialog => SemanticAlertDialog(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), EngineSemanticsRole.generic => GenericRole(this),
}; };
} }

View File

@ -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;
}

View File

@ -123,6 +123,9 @@ void runSemanticsTests() {
group('tabs', () { group('tabs', () {
_testTabs(); _testTabs();
}); });
group('table', () {
_testTables();
});
} }
void _testSemanticRole() { 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 /// A facade in front of [ui.SemanticsUpdateBuilder.updateNode] that
/// supplies default values for semantics attributes. /// supplies default values for semantics attributes.
void updateNode( void updateNode(

View File

@ -7,6 +7,7 @@
library; library;
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' show SemanticsRole;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -879,22 +880,25 @@ class DataTable extends StatelessWidget {
}) { }) {
final ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
final DataTableThemeData dataTableTheme = DataTableTheme.of(context); final DataTableThemeData dataTableTheme = DataTableTheme.of(context);
label = Row( label = Semantics(
textDirection: numeric ? TextDirection.rtl : null, role: SemanticsRole.columnHeader,
mainAxisAlignment: headingRowAlignment, child: Row(
children: <Widget>[ textDirection: numeric ? TextDirection.rtl : null,
if (headingRowAlignment == MainAxisAlignment.center && onSort != null) mainAxisAlignment: headingRowAlignment,
const SizedBox(width: _SortArrowState._arrowIconSize + _sortArrowPadding), children: <Widget>[
label, if (headingRowAlignment == MainAxisAlignment.center && onSort != null)
if (onSort != null) ...<Widget>[ const SizedBox(width: _SortArrowState._arrowIconSize + _sortArrowPadding),
_SortArrow( label,
visible: sorted, if (onSort != null) ...<Widget>[
up: sorted ? ascending : null, _SortArrow(
duration: _sortArrowAnimationDuration, visible: sorted,
), up: sorted ? ascending : null,
const SizedBox(width: _sortArrowPadding), duration: _sortArrowAnimationDuration,
),
const SizedBox(width: _sortArrowPadding),
],
], ],
], ),
); );
final TextStyle effectiveHeadingTextStyle = final TextStyle effectiveHeadingTextStyle =
@ -1013,7 +1017,7 @@ class DataTable extends StatelessWidget {
child: label, child: label,
); );
} }
return label; return TableCell(child: label);
} }
@override @override

View File

@ -4,8 +4,10 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' show SemanticsRole;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/semantics.dart';
import 'box.dart'; import 'box.dart';
import 'object.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. /// Replaces the children of this table with the given cells.
/// ///
/// The cells are divided into the specified number of columns before /// The cells are divided into the specified number of columns before

View File

@ -110,8 +110,14 @@ sealed class _DebugSemanticsRoleChecks {
SemanticsRole.tab => _semanticsTab, SemanticsRole.tab => _semanticsTab,
SemanticsRole.tabBar => _semanticsTabBar, SemanticsRole.tabBar => _semanticsTabBar,
SemanticsRole.tabPanel => _noCheckRequired, SemanticsRole.tabPanel => _noCheckRequired,
SemanticsRole.table => _noCheckRequired,
SemanticsRole.cell => _semanticsCell,
SemanticsRole.row => _unimplementedError,
SemanticsRole.columnHeader => _semanticsColumnHeader,
}(node); }(node);
static FlutterError? _unimplementedError(SemanticsNode node) =>
FlutterError('This semantics role is not implemented');
static FlutterError? _noCheckRequired(SemanticsNode node) => null; static FlutterError? _noCheckRequired(SemanticsNode node) => null;
static FlutterError? _semanticsTab(SemanticsNode node) { static FlutterError? _semanticsTab(SemanticsNode node) {
@ -140,6 +146,20 @@ sealed class _DebugSemanticsRoleChecks {
}); });
return error; 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]. /// A tag for a [SemanticsNode].

View File

@ -7,6 +7,7 @@
library; library;
import 'dart:collection'; import 'dart:collection';
import 'dart:ui' show SemanticsRole;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
@ -438,7 +439,8 @@ class _TableElement extends RenderObjectElement {
/// as the [child]. /// as the [child].
class TableCell extends ParentDataWidget<TableCellParentData> { class TableCell extends ParentDataWidget<TableCellParentData> {
/// Creates a widget that controls how a child of a [Table] is aligned. /// 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. /// How this cell is aligned vertically.
final TableCellVerticalAlignment? verticalAlignment; final TableCellVerticalAlignment? verticalAlignment;

View File

@ -6,6 +6,7 @@
library; library;
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' show SemanticsRole;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -13,6 +14,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix3; import 'package:vector_math/vector_math_64.dart' show Matrix3;
import '../widgets/semantics_tester.dart';
import 'data_table_test_utils.dart'; import 'data_table_test_utils.dart';
void main() { void main() {
@ -2211,6 +2213,78 @@ void main() {
expect(table.columnWidths![1], const IntrinsicColumnWidth()); expect(table.columnWidths![1], const IntrinsicColumnWidth());
expect(table.columnWidths![2], const IntrinsicColumnWidth(flex: 1)); 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>[
DataColumn(label: Text('Column 1')),
DataColumn(label: Text('Column 2')),
],
rows: const <DataRow>[
DataRow(
cells: <DataCell>[DataCell(Text('Data Cell 1')), DataCell(Text('Data Cell 2'))],
),
],
),
),
),
);
final TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
role: SemanticsRole.table,
children: <TestSemantics>[
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) { RenderParagraph _getTextRenderObject(WidgetTester tester, String text) {

View File

@ -2,9 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show SemanticsRole;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
class TestStatefulWidget extends StatefulWidget { class TestStatefulWidget extends StatefulWidget {
const TestStatefulWidget({super.key}); const TestStatefulWidget({super.key});
@ -951,5 +953,64 @@ void main() {
expect(boxD.size.height, greaterThan(boxA.size.height)); 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>[
TableRow(
children: <Widget>[
TableCell(child: const Text('Data Cell 1')),
TableCell(child: const Text('Data Cell 2')),
],
),
],
),
),
),
);
final TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
role: SemanticsRole.table,
children: <TestSemantics>[
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();
});
} }