Skip to content

Commit 6aaa4eb

Browse files
authored
[framework]Add semantics role to table rows. (#163337)
**1. framework side:** This PR Create semantics node for rows in table's `assembleSemanticsNode` function. **2. web side:** I tested on my mac, and i need to remove the `<flt-semantics-container>` between table and row, row and cell to traverse inside table, removing those transfom intermediate containers on web will be a bit of hassle and will be in another separate PR. For example this code can only announce table but can’t get into cells. ``` <flt-semantics id="flt-semantic-node-4" role="table" style="position: absolute; overflow: visible; width: 751px; height: 56px; transform-origin: 0px 0px 0px; transform: matrix(1, 0, 0, 1, 0, 56); pointer-events: none; z-index: 1;"> <flt-semantics-container style="position: absolute; pointer-events: none; top: 0px; left: 0px;"> <flt-semantics id="flt-semantic-node-6" role="row" style="position: absolute; overflow: visible; width: 751px; height: 56px; top: 0px; left: 0px; pointer-events: none;"> <flt-semantics-container style="position: absolute; pointer-events: none; top: 0px; left: 0px;"> <flt-semantics id="flt-semantic-node-5" role="columnheader" aria-label="Name" style="position: absolute; overflow: visible; width: 751px; height: 56px; top: 0px; left: 0px; pointer-events: all;"></flt-semantics> </flt-semantics-container> </flt-semantics> </flt-semantics-container> </flt-semantics> ``` If I removed the in between `</flt-semantics-container>`, the code come ``` <flt-semantics id="flt-semantic-node-4" role="table" style="position: absolute; overflow: visible; width: 751px; height: 56px; transform-origin: 0px 0px 0px; transform: matrix(1, 0, 0, 1, 0, 56); pointer-events: none; z-index: 1;"> <flt-semantics id="flt-semantic-node-6" role="row" style="position: absolute; overflow: visible; width: 751px; height: 56px; top: 0px; left: 0px; pointer-events: none;"> <flt-semantics id="flt-semantic-node-5" role="columnheader" aria-label="Name" style="position: absolute; overflow: visible; width: 751px; height: 56px; top: 0px; left: 0px; pointer-events: all;"></flt-semantics> </flt-semantics> </flt-semantics> ``` And I can get into table cells. **3. Other aria-attributes:** [aria-colcount](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-colcount) ,[aria-rowcount](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-rowcount)  [aria-colindex](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-colindex)  [aria-rowindex](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-rowindex)  * theres attributes are only needed if some rows and columns are hidden in the Dom tree. havn't added them yet aria-rowspan , aria-colspan : *we currently don't support row span and col span in our widgets. related issue: flutter/flutter#21594 related: flutter/flutter#162339 issue: flutter/flutter#45205 ## 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 4670159 commit 6aaa4eb

File tree

6 files changed

+284
-36
lines changed

6 files changed

+284
-36
lines changed

packages/flutter/lib/src/rendering/table.dart

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,9 +607,191 @@ class RenderTable extends RenderBox {
607607
void describeSemanticsConfiguration(SemanticsConfiguration config) {
608608
super.describeSemanticsConfiguration(config);
609609
config.role = SemanticsRole.table;
610+
config.isSemanticBoundary = true;
610611
config.explicitChildNodes = true;
611612
}
612613

614+
final Map<int, _Index> _idToIndexMap = <int, _Index>{};
615+
final Map<int, SemanticsNode> _cachedRows = <int, SemanticsNode>{};
616+
final Map<_Index, SemanticsNode> _cachedCells = <_Index, SemanticsNode>{};
617+
618+
/// Provides custom semantics for tables by generating nodes for rows and maybe cells.
619+
///
620+
/// Table rows are not RenderObjects, so their semantics nodes must be created separately.
621+
/// And if a cell has mutiple semantics node or has a different semantic role, we create
622+
/// a new semantics node to wrap it.
623+
@override
624+
void assembleSemanticsNode(
625+
SemanticsNode node,
626+
SemanticsConfiguration config,
627+
Iterable<SemanticsNode> children,
628+
) {
629+
final List<SemanticsNode> rows = <SemanticsNode>[];
630+
631+
final List<List<List<SemanticsNode>>> rawCells = List<List<List<SemanticsNode>>>.generate(
632+
_rows,
633+
(int rowIndex) =>
634+
List<List<SemanticsNode>>.generate(_columns, (int columnIndex) => <SemanticsNode>[]),
635+
);
636+
637+
Rect rectWithOffset(SemanticsNode node) {
638+
final Offset offset =
639+
(node.transform != null ? MatrixUtils.getAsTranslation(node.transform!) : null) ??
640+
Offset.zero;
641+
return node.rect.shift(offset);
642+
}
643+
644+
int findRowIndex(double top) {
645+
for (int i = _rowTops.length - 1; i >= 0; i--) {
646+
if (_rowTops[i] <= top) {
647+
return i;
648+
}
649+
}
650+
return -1;
651+
}
652+
653+
int findColumnIndex(double left) {
654+
if (_columnLefts == null) {
655+
return -1;
656+
}
657+
for (int i = _columnLefts!.length - 1; i >= 0; i--) {
658+
if (_columnLefts!.elementAt(i) <= left) {
659+
return i;
660+
}
661+
}
662+
return -1;
663+
}
664+
665+
void shiftTransform(SemanticsNode node, double dx, double dy) {
666+
final Matrix4? previousTransform = node.transform;
667+
final Offset offset =
668+
(previousTransform != null ? MatrixUtils.getAsTranslation(previousTransform) : null) ??
669+
Offset.zero;
670+
final Matrix4 newTransform = Matrix4.translationValues(offset.dx + dx, offset.dy + dy, 0);
671+
node.transform = newTransform;
672+
}
673+
674+
for (final SemanticsNode child in children) {
675+
if (_idToIndexMap.containsKey(child.id)) {
676+
final _Index index = _idToIndexMap[child.id]!;
677+
final int y = index.y;
678+
final int x = index.x;
679+
if (y < _rows && x < _columns) {
680+
rawCells[y][x].add(child);
681+
}
682+
} else {
683+
final Rect rect = rectWithOffset(child);
684+
final int y = findRowIndex(rect.top);
685+
final int x = findColumnIndex(rect.left);
686+
if (y != -1 && x != -1) {
687+
rawCells[y][x].add(child);
688+
}
689+
}
690+
}
691+
692+
for (int y = 0; y < _rows; y++) {
693+
final Rect rowBox = getRowBox(y);
694+
// Skip row if it's empty
695+
if (rowBox.height == 0) {
696+
continue;
697+
}
698+
699+
final SemanticsNode newRow =
700+
_cachedRows[y] ??
701+
(_cachedRows[y] = SemanticsNode(
702+
showOnScreen: () {
703+
showOnScreen(descendant: this, rect: rowBox);
704+
},
705+
));
706+
707+
// The list of cells of this Row.
708+
final List<SemanticsNode> cells = <SemanticsNode>[];
709+
710+
for (int x = 0; x < columns; x++) {
711+
final List<SemanticsNode> rawChildrens = rawCells[y][x];
712+
if (rawChildrens.isEmpty) {
713+
continue;
714+
}
715+
716+
// If the cell has multiple children or the only child is not a cell or columnHeader,
717+
// create a new semantic node with role cell to wrap it.
718+
// This can happen when the cell has a different semantic role, or the cell doesn't have a semantic
719+
// role because user is not using the `TableCell` widget.
720+
final bool addCellWrapper =
721+
rawChildrens.length > 1 ||
722+
(rawChildrens.single.role != SemanticsRole.cell &&
723+
rawChildrens.single.role != SemanticsRole.columnHeader);
724+
725+
final SemanticsNode cell =
726+
addCellWrapper
727+
? (_cachedCells[_Index(y, x)] ??
728+
(_cachedCells[_Index(y, x)] =
729+
SemanticsNode()..updateWith(
730+
config: SemanticsConfiguration()..role = SemanticsRole.cell,
731+
childrenInInversePaintOrder: rawChildrens,
732+
)))
733+
: rawChildrens.single;
734+
735+
final double cellWidth =
736+
x == _columns - 1
737+
? rowBox.width - _columnLefts!.elementAt(x)
738+
: _columnLefts!.elementAt(x + 1) - _columnLefts!.elementAt(x);
739+
740+
// Skip cell if it's invisible
741+
if (cellWidth <= 0.0) {
742+
continue;
743+
}
744+
// Add wrapper transform
745+
if (addCellWrapper) {
746+
cell
747+
..transform = Matrix4.translationValues(_columnLefts!.elementAt(x), 0, 0)
748+
..rect = Rect.fromLTWH(0, 0, cellWidth, rowBox.height);
749+
}
750+
for (final SemanticsNode child in rawChildrens) {
751+
_idToIndexMap[child.id] = _Index(y, x);
752+
753+
// Shift child transform.
754+
final Rect localRect = rectWithOffset(child);
755+
// The rect should satisfy 0 <= localRect.top < localRect.bottom <= rowBox.height
756+
final double dy = localRect.top >= rowBox.height ? -_rowTops.elementAt(y) : 0.0;
757+
758+
// if addCellWrapper is true, the rect is relative to the cell
759+
// The rect should satisfy 0 <= localRect.left < localRect.right <= cellWidth
760+
// if addCellWrapper is false, the rect is relative to the raw
761+
// The rect should satisfy _columnLefts!.elementAt(x) <= localRect.left < localRect.right <= _columnLefts!.elementAt(x+1)
762+
final double dx =
763+
addCellWrapper
764+
? ((localRect.left >= cellWidth) ? -_columnLefts!.elementAt(x) : 0.0)
765+
: (localRect.right <= _columnLefts!.elementAt(x)
766+
? _columnLefts!.elementAt(x)
767+
: 0.0);
768+
769+
if (dx != 0 || dy != 0) {
770+
shiftTransform(child, dx, dy);
771+
}
772+
}
773+
774+
cell.indexInParent = x;
775+
cells.add(cell);
776+
}
777+
778+
newRow
779+
..updateWith(
780+
config:
781+
SemanticsConfiguration()
782+
..indexInParent = y
783+
..role = SemanticsRole.row,
784+
childrenInInversePaintOrder: cells,
785+
)
786+
..transform = Matrix4.translationValues(rowBox.left, rowBox.top, 0)
787+
..rect = Rect.fromLTWH(0, 0, rowBox.width, rowBox.height);
788+
789+
rows.add(newRow);
790+
}
791+
792+
node.updateWith(config: config, childrenInInversePaintOrder: rows);
793+
}
794+
613795
/// Replaces the children of this table with the given cells.
614796
///
615797
/// The cells are divided into the specified number of columns before
@@ -1375,3 +1557,10 @@ class RenderTable extends RenderBox {
13751557
];
13761558
}
13771559
}
1560+
1561+
/// Index for a cell.
1562+
class _Index {
1563+
_Index(this.y, this.x);
1564+
int y;
1565+
int x;
1566+
}

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,13 @@ sealed class _DebugSemanticsRoleChecks {
110110
SemanticsRole.tab => _semanticsTab,
111111
SemanticsRole.tabBar => _semanticsTabBar,
112112
SemanticsRole.tabPanel => _noCheckRequired,
113-
SemanticsRole.table => _noCheckRequired,
113+
SemanticsRole.table => _semanticsTable,
114114
SemanticsRole.cell => _semanticsCell,
115+
SemanticsRole.row => _semanticsRow,
115116
SemanticsRole.columnHeader => _semanticsColumnHeader,
116117
SemanticsRole.radioGroup => _semanticsRadioGroup,
117118
// TODO(chunhtai): add checks when the roles are used in framework.
118119
// https://github.com/flutter/flutter/issues/159741.
119-
SemanticsRole.row => _unimplemented,
120120
SemanticsRole.searchBox => _unimplemented,
121121
SemanticsRole.dragHandle => _unimplemented,
122122
SemanticsRole.spinButton => _unimplemented,
@@ -165,16 +165,42 @@ sealed class _DebugSemanticsRoleChecks {
165165
return error;
166166
}
167167

168-
static FlutterError? _semanticsCell(SemanticsNode node) {
168+
static FlutterError? _semanticsTable(SemanticsNode node) {
169+
FlutterError? error;
170+
node.visitChildren((SemanticsNode child) {
171+
if (child.getSemanticsData().role != SemanticsRole.row) {
172+
error = FlutterError('Children of Table must have the row role');
173+
}
174+
return error == null;
175+
});
176+
return error;
177+
}
178+
179+
static FlutterError? _semanticsRow(SemanticsNode node) {
169180
if (node.parent?.role != SemanticsRole.table) {
170-
return FlutterError('A cell must be a child of a table');
181+
return FlutterError('A row must be a child of a table');
182+
}
183+
FlutterError? error;
184+
node.visitChildren((SemanticsNode child) {
185+
if (child.getSemanticsData().role != SemanticsRole.cell &&
186+
child.getSemanticsData().role != SemanticsRole.columnHeader) {
187+
error = FlutterError('Children of Row must have the cell or columnHeader role');
188+
}
189+
return error == null;
190+
});
191+
return error;
192+
}
193+
194+
static FlutterError? _semanticsCell(SemanticsNode node) {
195+
if (node.parent?.role != SemanticsRole.row && node.parent?.role != SemanticsRole.cell) {
196+
return FlutterError('A cell must be a child of a row or another cell');
171197
}
172198
return null;
173199
}
174200

175201
static FlutterError? _semanticsColumnHeader(SemanticsNode node) {
176-
if (node.parent?.role != SemanticsRole.table) {
177-
return FlutterError('A columnHeader must be a child of a table');
202+
if (node.parent?.role != SemanticsRole.row && node.parent?.role != SemanticsRole.cell) {
203+
return FlutterError('A columnHeader must be a child or another cell');
178204
}
179205
return null;
180206
}

packages/flutter/lib/src/widgets/table.dart

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,14 +437,30 @@ class _TableElement extends RenderObjectElement {
437437
///
438438
/// To create an empty [TableCell], provide a [SizedBox.shrink]
439439
/// as the [child].
440-
class TableCell extends ParentDataWidget<TableCellParentData> {
440+
class TableCell extends StatelessWidget {
441441
/// Creates a widget that controls how a child of a [Table] is aligned.
442-
TableCell({super.key, this.verticalAlignment, required Widget child})
443-
: super(child: Semantics(role: SemanticsRole.cell, child: child));
442+
const TableCell({super.key, this.verticalAlignment, required this.child});
444443

445444
/// How this cell is aligned vertically.
446445
final TableCellVerticalAlignment? verticalAlignment;
447446

447+
/// The child of this cell.
448+
final Widget child;
449+
450+
@override
451+
Widget build(BuildContext context) {
452+
return _TableCell(
453+
verticalAlignment: verticalAlignment,
454+
child: Semantics(role: SemanticsRole.cell, child: child),
455+
);
456+
}
457+
}
458+
459+
class _TableCell extends ParentDataWidget<TableCellParentData> {
460+
const _TableCell({this.verticalAlignment, required super.child});
461+
462+
final TableCellVerticalAlignment? verticalAlignment;
463+
448464
@override
449465
void applyParentData(RenderObject renderObject) {
450466
final TableCellParentData parentData = renderObject.parentData! as TableCellParentData;

packages/flutter/test/material/data_table_test.dart

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2248,24 +2248,34 @@ void main() {
22482248
role: SemanticsRole.table,
22492249
children: <TestSemantics>[
22502250
TestSemantics(
2251-
label: 'Column 1',
2252-
textDirection: TextDirection.ltr,
2253-
role: SemanticsRole.columnHeader,
2251+
role: SemanticsRole.row,
2252+
children: <TestSemantics>[
2253+
TestSemantics(
2254+
label: 'Column 1',
2255+
textDirection: TextDirection.ltr,
2256+
role: SemanticsRole.columnHeader,
2257+
),
2258+
TestSemantics(
2259+
label: 'Column 2',
2260+
textDirection: TextDirection.ltr,
2261+
role: SemanticsRole.columnHeader,
2262+
),
2263+
],
22542264
),
22552265
TestSemantics(
2256-
label: 'Column 2',
2257-
textDirection: TextDirection.ltr,
2258-
role: SemanticsRole.columnHeader,
2259-
),
2260-
TestSemantics(
2261-
label: 'Data Cell 1',
2262-
textDirection: TextDirection.ltr,
2263-
role: SemanticsRole.cell,
2264-
),
2265-
TestSemantics(
2266-
label: 'Data Cell 2',
2267-
textDirection: TextDirection.ltr,
2268-
role: SemanticsRole.cell,
2266+
role: SemanticsRole.row,
2267+
children: <TestSemantics>[
2268+
TestSemantics(
2269+
label: 'Data Cell 1',
2270+
textDirection: TextDirection.ltr,
2271+
role: SemanticsRole.cell,
2272+
),
2273+
TestSemantics(
2274+
label: 'Data Cell 2',
2275+
textDirection: TextDirection.ltr,
2276+
role: SemanticsRole.cell,
2277+
),
2278+
],
22692279
),
22702280
],
22712281
),

packages/flutter/test/rendering/table_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ void main() {
2828
'RenderTable#00000 NEEDS-PAINT\n'
2929
' │ parentData: <none>\n'
3030
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
31+
' │ semantic boundary\n'
3132
' │ size: Size(800.0, 600.0)\n'
3233
' │ default column width: FlexColumnWidth(1.0)\n'
3334
' │ table size: 0×0\n'
@@ -98,6 +99,7 @@ void main() {
9899
'RenderTable#00000 relayoutBoundary=up1 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE\n'
99100
' │ parentData: offset=Offset(335.0, 185.0) (can use size)\n'
100101
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
102+
' │ semantic boundary\n'
101103
' │ size: Size(130.0, 230.0)\n'
102104
' │ default column width: IntrinsicColumnWidth(flex: null)\n'
103105
' │ table size: 5×5\n'

0 commit comments

Comments
 (0)