flutter / flutter/flutter

Table: synthesized semantics cell wrapper is not span-aware

Open
#192,849 1 comment 0 reactions 0 assignees View on GitHub
a: accessibility framework has reproducible steps team-accessibility
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.