Table: synthesized semantics cell wrapper is not span-aware
- Dominant language
- Dart
- Stars
- 179k
- Forks
- 31.1k
- PR merge metrics
- PR metrics pending
Description
Follow-up to #177102 (`colSpan` / `rowSpan` support in `TableCell`), tracking the `TODO(hm21)` in `RenderTable.assembleSemanticsNode` in `packages/flutter/lib/src/rendering/table.dart` (see https://github.com/flutter/flutter/pull/177102#discussion_r3662098994).
### Steps to reproduce
1. Check out the branch of #177102.
2. Build a `Table` in which a spanning cell's semantics are not a single node with role `cell` / `columnHeader`, so that `assembleSemanticsNode` synthesizes a cell wrapper for it. `TableCell` wraps its child in `Semantics(role: SemanticsRole.cell)`, so this needs `TableCellParentData.colSpan` / `rowSpan` to be set without `TableCell`, as a custom widget built on `RenderTable` would do (code sample below).
3. Enable semantics and dump the tree.
### Expected results
The synthesized cell node covers the whole span, the same way the child does.
### Actual results
The wrapper rect is built from a single column width and the row height:
```dart
cell
..transform = Matrix4.translationValues(_columnLefts!.elementAt(x), 0, 0)
..rect = Rect.fromLTWH(0, 0, cellWidth, rowBox.height);
```
so it only covers the first cell of the span, while its child covers all of it. With three `FixedColumnWidth(100)` columns stretched to 800 wide, a `colSpan: 2` cell and a `rowSpan: 2` cell in the first row:
```
├─SemanticsNode#7
│ │ Rect.fromLTRB(0.0, 0.0, 266.7, 20.0) <- wrapper: one column wide
│ │ role: cell
│ └─SemanticsNode#2
│ Rect.fromLTRB(0.0, 0.0, 533.3, 20.0) <- child: two columns wide
│ label: "span"
└─SemanticsNode#8
│ Rect.fromLTRB(533.3, 0.0, 800.0, 20.0) <- wrapper: one row tall
│ role: cell
└─SemanticsNode#3
Rect.fromLTRB(0.0, 0.0, 266.7, 40.0) <- child: two rows tall
label: "tall"
```
The per-child `dx` / `dy` shift heuristics right below make the same one-cell assumption. Through the `Table` widget a spanning cell currently keeps the bounds of its own cell node, so this is not user-visible yet, but the wrapper path is taken for anything that drives `RenderTable` directly. A `rowSpan` cell also extends past its row node's rect, since rows are sized to a single row height.
Proposed follow-up: compute the wrapper rect (and the shift heuristics) from the spanned width / height, ideally expose `colSpan` / `rowSpan` to the accessibility tree, and add semantics tests for spanning cells. The RTL column mapping in the same method is tracked separately in #192848.
### Code sample
Code sample
```dart
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
// Sets colSpan / rowSpan on TableCellParentData without the
// Semantics(role: cell) node that TableCell adds.
class RawSpan extends ParentDataWidget {
const RawSpan({super.key, this.colSpan = 1, this.rowSpan = 1, required super.child});
final int colSpan;
final int rowSpan;
@override
void applyParentData(RenderObject renderObject) {
final TableCellParentData parentData = renderObject.parentData! as TableCellParentData;
if (parentData.colSpan != colSpan || parentData.rowSpan != rowSpan) {
parentData.colSpan = colSpan;
parentData.rowSpan = rowSpan;
renderObject.parent?.markNeedsLayout();
}
}
@override
Type get debugTypicalAncestorWidgetClass => Table;
}
void main() {
testWidgets('wrapper bounds of spanning cells', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Table(
defaultColumnWidth: const FixedColumnWidth(100),
children: const [
TableRow(
children: [
RawSpan(colSpan: 2, child: SizedBox(height: 20, child: Text('span'))),
TableCell.none,
RawSpan(rowSpan: 2, child: SizedBox(height: 40, child: Text('tall'))),
],
),
TableRow(
children: [
SizedBox(height: 20, child: Text('a')),
SizedBox(height: 20, child: Text('b')),
TableCell.none,
],
),
],
),
),
);
debugPrint(tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!.toStringDeep());
handle.dispose();
});
}
```
### Flutter Doctor output
Framework at flutter/flutter@3064d63c5d3a5ae847ba528c932b39742b9be614 (branch of #177102, based on master 0b1bac2ff48). Not reproducible on master yet, since spans only exist on that branch.
Contributor guide
Research direction
Start in packages/flutter/lib/src/rendering/table.dart at RenderTable.assembleSemanticsNode, then review the span sizing and per-child dx/dy shift logic described in the issue. Add semantics tests for colSpan and rowSpan wrappers and verify that synthesized wrapper bounds cover the full span, including row height.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- accessibility
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100