[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: https://github.com/flutter/flutter/issues/21594
related: https://github.com/flutter/flutter/pull/162339
issue: https://github.com/flutter/flutter/issues/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
This commit is contained in:
@@ -607,9 +607,191 @@ class RenderTable extends RenderBox {
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
config.role = SemanticsRole.table;
|
||||
config.isSemanticBoundary = true;
|
||||
config.explicitChildNodes = true;
|
||||
}
|
||||
|
||||
final Map<int, _Index> _idToIndexMap = <int, _Index>{};
|
||||
final Map<int, SemanticsNode> _cachedRows = <int, SemanticsNode>{};
|
||||
final Map<_Index, SemanticsNode> _cachedCells = <_Index, SemanticsNode>{};
|
||||
|
||||
/// Provides custom semantics for tables by generating nodes for rows and maybe cells.
|
||||
///
|
||||
/// Table rows are not RenderObjects, so their semantics nodes must be created separately.
|
||||
/// And if a cell has mutiple semantics node or has a different semantic role, we create
|
||||
/// a new semantics node to wrap it.
|
||||
@override
|
||||
void assembleSemanticsNode(
|
||||
SemanticsNode node,
|
||||
SemanticsConfiguration config,
|
||||
Iterable<SemanticsNode> children,
|
||||
) {
|
||||
final List<SemanticsNode> rows = <SemanticsNode>[];
|
||||
|
||||
final List<List<List<SemanticsNode>>> rawCells = List<List<List<SemanticsNode>>>.generate(
|
||||
_rows,
|
||||
(int rowIndex) =>
|
||||
List<List<SemanticsNode>>.generate(_columns, (int columnIndex) => <SemanticsNode>[]),
|
||||
);
|
||||
|
||||
Rect rectWithOffset(SemanticsNode node) {
|
||||
final Offset offset =
|
||||
(node.transform != null ? MatrixUtils.getAsTranslation(node.transform!) : null) ??
|
||||
Offset.zero;
|
||||
return node.rect.shift(offset);
|
||||
}
|
||||
|
||||
int findRowIndex(double top) {
|
||||
for (int i = _rowTops.length - 1; i >= 0; i--) {
|
||||
if (_rowTops[i] <= top) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int findColumnIndex(double left) {
|
||||
if (_columnLefts == null) {
|
||||
return -1;
|
||||
}
|
||||
for (int i = _columnLefts!.length - 1; i >= 0; i--) {
|
||||
if (_columnLefts!.elementAt(i) <= left) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void shiftTransform(SemanticsNode node, double dx, double dy) {
|
||||
final Matrix4? previousTransform = node.transform;
|
||||
final Offset offset =
|
||||
(previousTransform != null ? MatrixUtils.getAsTranslation(previousTransform) : null) ??
|
||||
Offset.zero;
|
||||
final Matrix4 newTransform = Matrix4.translationValues(offset.dx + dx, offset.dy + dy, 0);
|
||||
node.transform = newTransform;
|
||||
}
|
||||
|
||||
for (final SemanticsNode child in children) {
|
||||
if (_idToIndexMap.containsKey(child.id)) {
|
||||
final _Index index = _idToIndexMap[child.id]!;
|
||||
final int y = index.y;
|
||||
final int x = index.x;
|
||||
if (y < _rows && x < _columns) {
|
||||
rawCells[y][x].add(child);
|
||||
}
|
||||
} else {
|
||||
final Rect rect = rectWithOffset(child);
|
||||
final int y = findRowIndex(rect.top);
|
||||
final int x = findColumnIndex(rect.left);
|
||||
if (y != -1 && x != -1) {
|
||||
rawCells[y][x].add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int y = 0; y < _rows; y++) {
|
||||
final Rect rowBox = getRowBox(y);
|
||||
// Skip row if it's empty
|
||||
if (rowBox.height == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final SemanticsNode newRow =
|
||||
_cachedRows[y] ??
|
||||
(_cachedRows[y] = SemanticsNode(
|
||||
showOnScreen: () {
|
||||
showOnScreen(descendant: this, rect: rowBox);
|
||||
},
|
||||
));
|
||||
|
||||
// The list of cells of this Row.
|
||||
final List<SemanticsNode> cells = <SemanticsNode>[];
|
||||
|
||||
for (int x = 0; x < columns; x++) {
|
||||
final List<SemanticsNode> rawChildrens = rawCells[y][x];
|
||||
if (rawChildrens.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the cell has multiple children or the only child is not a cell or columnHeader,
|
||||
// create a new semantic node with role cell to wrap it.
|
||||
// This can happen when the cell has a different semantic role, or the cell doesn't have a semantic
|
||||
// role because user is not using the `TableCell` widget.
|
||||
final bool addCellWrapper =
|
||||
rawChildrens.length > 1 ||
|
||||
(rawChildrens.single.role != SemanticsRole.cell &&
|
||||
rawChildrens.single.role != SemanticsRole.columnHeader);
|
||||
|
||||
final SemanticsNode cell =
|
||||
addCellWrapper
|
||||
? (_cachedCells[_Index(y, x)] ??
|
||||
(_cachedCells[_Index(y, x)] =
|
||||
SemanticsNode()..updateWith(
|
||||
config: SemanticsConfiguration()..role = SemanticsRole.cell,
|
||||
childrenInInversePaintOrder: rawChildrens,
|
||||
)))
|
||||
: rawChildrens.single;
|
||||
|
||||
final double cellWidth =
|
||||
x == _columns - 1
|
||||
? rowBox.width - _columnLefts!.elementAt(x)
|
||||
: _columnLefts!.elementAt(x + 1) - _columnLefts!.elementAt(x);
|
||||
|
||||
// Skip cell if it's invisible
|
||||
if (cellWidth <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
// Add wrapper transform
|
||||
if (addCellWrapper) {
|
||||
cell
|
||||
..transform = Matrix4.translationValues(_columnLefts!.elementAt(x), 0, 0)
|
||||
..rect = Rect.fromLTWH(0, 0, cellWidth, rowBox.height);
|
||||
}
|
||||
for (final SemanticsNode child in rawChildrens) {
|
||||
_idToIndexMap[child.id] = _Index(y, x);
|
||||
|
||||
// Shift child transform.
|
||||
final Rect localRect = rectWithOffset(child);
|
||||
// The rect should satisfy 0 <= localRect.top < localRect.bottom <= rowBox.height
|
||||
final double dy = localRect.top >= rowBox.height ? -_rowTops.elementAt(y) : 0.0;
|
||||
|
||||
// if addCellWrapper is true, the rect is relative to the cell
|
||||
// The rect should satisfy 0 <= localRect.left < localRect.right <= cellWidth
|
||||
// if addCellWrapper is false, the rect is relative to the raw
|
||||
// The rect should satisfy _columnLefts!.elementAt(x) <= localRect.left < localRect.right <= _columnLefts!.elementAt(x+1)
|
||||
final double dx =
|
||||
addCellWrapper
|
||||
? ((localRect.left >= cellWidth) ? -_columnLefts!.elementAt(x) : 0.0)
|
||||
: (localRect.right <= _columnLefts!.elementAt(x)
|
||||
? _columnLefts!.elementAt(x)
|
||||
: 0.0);
|
||||
|
||||
if (dx != 0 || dy != 0) {
|
||||
shiftTransform(child, dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
cell.indexInParent = x;
|
||||
cells.add(cell);
|
||||
}
|
||||
|
||||
newRow
|
||||
..updateWith(
|
||||
config:
|
||||
SemanticsConfiguration()
|
||||
..indexInParent = y
|
||||
..role = SemanticsRole.row,
|
||||
childrenInInversePaintOrder: cells,
|
||||
)
|
||||
..transform = Matrix4.translationValues(rowBox.left, rowBox.top, 0)
|
||||
..rect = Rect.fromLTWH(0, 0, rowBox.width, rowBox.height);
|
||||
|
||||
rows.add(newRow);
|
||||
}
|
||||
|
||||
node.updateWith(config: config, childrenInInversePaintOrder: rows);
|
||||
}
|
||||
|
||||
/// Replaces the children of this table with the given cells.
|
||||
///
|
||||
/// The cells are divided into the specified number of columns before
|
||||
@@ -1375,3 +1557,10 @@ class RenderTable extends RenderBox {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/// Index for a cell.
|
||||
class _Index {
|
||||
_Index(this.y, this.x);
|
||||
int y;
|
||||
int x;
|
||||
}
|
||||
|
||||
@@ -110,13 +110,13 @@ sealed class _DebugSemanticsRoleChecks {
|
||||
SemanticsRole.tab => _semanticsTab,
|
||||
SemanticsRole.tabBar => _semanticsTabBar,
|
||||
SemanticsRole.tabPanel => _noCheckRequired,
|
||||
SemanticsRole.table => _noCheckRequired,
|
||||
SemanticsRole.table => _semanticsTable,
|
||||
SemanticsRole.cell => _semanticsCell,
|
||||
SemanticsRole.row => _semanticsRow,
|
||||
SemanticsRole.columnHeader => _semanticsColumnHeader,
|
||||
SemanticsRole.radioGroup => _semanticsRadioGroup,
|
||||
// TODO(chunhtai): add checks when the roles are used in framework.
|
||||
// https://github.com/flutter/flutter/issues/159741.
|
||||
SemanticsRole.row => _unimplemented,
|
||||
SemanticsRole.searchBox => _unimplemented,
|
||||
SemanticsRole.dragHandle => _unimplemented,
|
||||
SemanticsRole.spinButton => _unimplemented,
|
||||
@@ -165,16 +165,42 @@ sealed class _DebugSemanticsRoleChecks {
|
||||
return error;
|
||||
}
|
||||
|
||||
static FlutterError? _semanticsCell(SemanticsNode node) {
|
||||
static FlutterError? _semanticsTable(SemanticsNode node) {
|
||||
FlutterError? error;
|
||||
node.visitChildren((SemanticsNode child) {
|
||||
if (child.getSemanticsData().role != SemanticsRole.row) {
|
||||
error = FlutterError('Children of Table must have the row role');
|
||||
}
|
||||
return error == null;
|
||||
});
|
||||
return error;
|
||||
}
|
||||
|
||||
static FlutterError? _semanticsRow(SemanticsNode node) {
|
||||
if (node.parent?.role != SemanticsRole.table) {
|
||||
return FlutterError('A cell must be a child of a table');
|
||||
return FlutterError('A row must be a child of a table');
|
||||
}
|
||||
FlutterError? error;
|
||||
node.visitChildren((SemanticsNode child) {
|
||||
if (child.getSemanticsData().role != SemanticsRole.cell &&
|
||||
child.getSemanticsData().role != SemanticsRole.columnHeader) {
|
||||
error = FlutterError('Children of Row must have the cell or columnHeader role');
|
||||
}
|
||||
return error == null;
|
||||
});
|
||||
return error;
|
||||
}
|
||||
|
||||
static FlutterError? _semanticsCell(SemanticsNode node) {
|
||||
if (node.parent?.role != SemanticsRole.row && node.parent?.role != SemanticsRole.cell) {
|
||||
return FlutterError('A cell must be a child of a row or another cell');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static FlutterError? _semanticsColumnHeader(SemanticsNode node) {
|
||||
if (node.parent?.role != SemanticsRole.table) {
|
||||
return FlutterError('A columnHeader must be a child of a table');
|
||||
if (node.parent?.role != SemanticsRole.row && node.parent?.role != SemanticsRole.cell) {
|
||||
return FlutterError('A columnHeader must be a child or another cell');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -437,14 +437,30 @@ class _TableElement extends RenderObjectElement {
|
||||
///
|
||||
/// To create an empty [TableCell], provide a [SizedBox.shrink]
|
||||
/// as the [child].
|
||||
class TableCell extends ParentDataWidget<TableCellParentData> {
|
||||
class TableCell extends StatelessWidget {
|
||||
/// Creates a widget that controls how a child of a [Table] is aligned.
|
||||
TableCell({super.key, this.verticalAlignment, required Widget child})
|
||||
: super(child: Semantics(role: SemanticsRole.cell, child: child));
|
||||
const TableCell({super.key, this.verticalAlignment, required this.child});
|
||||
|
||||
/// How this cell is aligned vertically.
|
||||
final TableCellVerticalAlignment? verticalAlignment;
|
||||
|
||||
/// The child of this cell.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _TableCell(
|
||||
verticalAlignment: verticalAlignment,
|
||||
child: Semantics(role: SemanticsRole.cell, child: child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TableCell extends ParentDataWidget<TableCellParentData> {
|
||||
const _TableCell({this.verticalAlignment, required super.child});
|
||||
|
||||
final TableCellVerticalAlignment? verticalAlignment;
|
||||
|
||||
@override
|
||||
void applyParentData(RenderObject renderObject) {
|
||||
final TableCellParentData parentData = renderObject.parentData! as TableCellParentData;
|
||||
|
||||
@@ -2248,24 +2248,34 @@ void main() {
|
||||
role: SemanticsRole.table,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: 'Column 1',
|
||||
textDirection: TextDirection.ltr,
|
||||
role: SemanticsRole.columnHeader,
|
||||
role: SemanticsRole.row,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: 'Column 1',
|
||||
textDirection: TextDirection.ltr,
|
||||
role: SemanticsRole.columnHeader,
|
||||
),
|
||||
TestSemantics(
|
||||
label: 'Column 2',
|
||||
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,
|
||||
role: SemanticsRole.row,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: 'Data Cell 1',
|
||||
textDirection: TextDirection.ltr,
|
||||
role: SemanticsRole.cell,
|
||||
),
|
||||
TestSemantics(
|
||||
label: 'Data Cell 2',
|
||||
textDirection: TextDirection.ltr,
|
||||
role: SemanticsRole.cell,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -28,6 +28,7 @@ void main() {
|
||||
'RenderTable#00000 NEEDS-PAINT\n'
|
||||
' │ parentData: <none>\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ semantic boundary\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ default column width: FlexColumnWidth(1.0)\n'
|
||||
' │ table size: 0×0\n'
|
||||
@@ -98,6 +99,7 @@ void main() {
|
||||
'RenderTable#00000 relayoutBoundary=up1 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE\n'
|
||||
' │ parentData: offset=Offset(335.0, 185.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
||||
' │ semantic boundary\n'
|
||||
' │ size: Size(130.0, 230.0)\n'
|
||||
' │ default column width: IntrinsicColumnWidth(flex: null)\n'
|
||||
' │ table size: 5×5\n'
|
||||
|
||||
@@ -959,11 +959,11 @@ void main() {
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Table(
|
||||
children: <TableRow>[
|
||||
children: const <TableRow>[
|
||||
TableRow(
|
||||
children: <Widget>[
|
||||
TableCell(child: const Text('Data Cell 1')),
|
||||
TableCell(child: const Text('Data Cell 2')),
|
||||
TableCell(child: Text('Data Cell 1')),
|
||||
TableCell(child: Text('Data Cell 2')),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -986,14 +986,19 @@ void main() {
|
||||
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,
|
||||
role: SemanticsRole.row,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: 'Data Cell 1',
|
||||
textDirection: TextDirection.ltr,
|
||||
role: SemanticsRole.cell,
|
||||
),
|
||||
TestSemantics(
|
||||
label: 'Data Cell 2',
|
||||
textDirection: TextDirection.ltr,
|
||||
role: SemanticsRole.cell,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user