feat(table): add support for colSpan and rowSpan in TableCell#177102
feat(table): add support for colSpan and rowSpan in TableCell#177102hm21 wants to merge 29 commits intoflutter:masterfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a significant and well-implemented feature, adding colSpan and rowSpan support to the Table widget. The changes are comprehensive, covering rendering logic, widget API, border painting, and extensive testing. The implementation includes thoughtful performance optimizations, such as caching span information and using typed data lists. Additionally, the new assertions and detailed error messages will greatly improve the developer experience when working with complex table layouts. Overall, this is a high-quality contribution.
- Enhanced TableCell to include colSpan and rowSpan properties, allowing cells to span multiple columns and rows. - Updated Table widget to validate colSpan and rowSpan values, ensuring they do not exceed table dimensions. - Improved error handling for irregular row lengths and empty TableRow scenarios. - Added tests for colSpan and rowSpan functionality, including edge cases and layout validation. - Updated documentation to reflect new features and usage guidelines for TableCell. refactor(RenderTable): improve comments for column and row span logic
Piinks
left a comment
There was a problem hiding this comment.
Hey @hm21 thanks for contributing! This is a big change, to help facilitate review, would you be willing to draft an outline of your changes and the motivations behind the decisions you made here? We usually do these using this template: https://github.com/flutter/flutter/blob/master/docs/contributing/Design-Documents.md
|
I am not sure what the implications might be on performance here, have you see the TableView widget that supports cell merging with lazy loading for children? |
|
Hey @Piinks, thanks for the fast response to that PR, really appreciate it cuz I thought it would take way longer for someone to take a look, as my other older PR #176393 is still waiting, but maybe that one’s not very interesting for you guys😅🙈
Sure, I can fill it out but not sure I’ll have time this weekend, but definitely next week I should find a bit of time.
I'm not sure, but I guess you're referring to the Reasons for that PR
Compare-PerformanceTo measure the average render duration, I repeatedly called Test 1: Very Large (10,000 cells: 500 rows × 20 columns)
Test 2: Large (500 cells: 50 rows × 10 columns)
Btw, I hope you didn’t get me wrong Thanks again for the fast response to that PR and I'll fill out the document you mentioned as soon as possible. |
Actually, we use labels to route PRs to the right sub-team for review. I noticed that PR did not have a team label on it, so I applied one and reached out to the team to confirm. I hope you will hear back soon on it, and thanks again for contributing!
Thanks as well for your detailed response. If you are able to fill out the design dic template (the various sections are suggestive, not all required) it would be helpful for review. Otherwise I can start digging into this, it just might take a minute to get through it all. :) |
|
Hey @Piinks, thanks for sharing those two links and my bad for not reading carefully enough that I was supposed to mention the open PR in the Discord channel after two weeks🙈 Still, really appreciate your help with tagging the team on the other PR. The link to the design-document is https://flutter.dev/go/table-cell-span. Also, sorry again, I missed that the usual process is to first create an issue referencing the design doc before opening a PR. If you'd still prefer I create a new issue for the design doc but I just thought it might be a bit pointless now that the PR is already there😅 |
## Description of what this PR is changing or adding, and why: Adds a new shortlink redirect for the Flutter design document describing TableCell span support. ## Issues fixed by this PR (if any): ## PRs or commits this PR depends on (if any): [flutter/flutter#177102](flutter/flutter#177102)
Piinks
left a comment
There was a problem hiding this comment.
Still working through, but a couple questions to start. Thanks for writing a doc! Very helpful!
Piinks
left a comment
There was a problem hiding this comment.
Thanks for the updates and your patience here. :)
Couple tests I want to make sure we've covered..
From your doc (thanks again for providing!):
If a developer forgets to include the required TableCell.none entries, assertions during layout report the conflict and specify which row and column overlap occurred.
Is that covered here? I might have missed it.
Also, what happens if the configuration of spans causes overlapping merged cells? How is that handled?
| expect(find.text('Bottom Middle'), findsOneWidget); | ||
| }); | ||
|
|
||
| testWidgets('Table with all cells using TableCell.none in spanning area', ( |
There was a problem hiding this comment.
What happens if the user puts another widget in a hidden cell instead of TableCell.none?
There was a problem hiding this comment.
Currently, nothing will happen and it will just use it as a placeholder same as TableCell.none. I did consider adding an assert, but decided to leave it optional and allow the use of TableCell.none for a couple of reasons:
- Some users might prefer to use
SizedBox.shrink()or another widget, so this gives them the flexibility to choose. - Imagine a scenario where cells can be dynamically expanded or collapsed. In such cases, developers can simply update the
colSpanorrowSpanvalue without having to switch betweenTableCell.noneand an actual widget.
Anyway if you’d prefer to enforce the use of TableCell.none, I can add an assert, I'm fine with that too :)
There was a problem hiding this comment.
Connecting the other thread - we checked against TableView, and overlap like this is allowed. It is in the realm of possibility folks may want to be able to do this for their UI.
There was a problem hiding this comment.
I am slightly confused about the reasoning for allowing use of other widget in placed for the hidden cell? If I understand correctly
TableRow(
children: <Widget>[
TableCell(colSpan: 2, child: Text('Spanning Cell')),
Text('hidden'), // This is ignored?
],
),This feels like code smell that they should use TableCell.none to be more descriptive.
There was a problem hiding this comment.
@chunhtai Got your point and yes in your example it would feel odd if the Text widget gets ignored.
That said, I still think the two reasons I explained here could be relevant for why we might want to allow it. But I’m also totally fine with adding an assert to enforce the use of TableCell.none. So I’ll hold back for now and if you guys decide to go that route, I can extend it with the assert later :)
There was a problem hiding this comment.
I vote for add assertion to enforce the use of TableCell.none, we can always relax later if this indeed becomes a pain point.
|
Also, FYI it looks like there are some failing tests here. |
|
Hey @Piinks, so sorry for my late response, I was on vacation in Iceland, and afterwards got a bit caught up with some personal work projects🙈
Currently it detects two different behaviors:
At the moment, there's no assert that throws an error in that case, so if it happens, the merged cells will overlap the other cells in reverse order (the last widget ends up on top of the others). I guess it would make sense to implement an assert for that too, right? Or how do you handle that case in
Oh, I thought it was cuz of the infrastructure issue with the Linux tests that were failing around the time I created that PR. In another PR, I had the same problem and was told here that it was related to that. But my bad for not double-checking also here more carefully.
EDIT: So I’m guessing that part is only available to Flutter team members, or in this case, to “you,” right? Or is there something else I need to do first? |
|
Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change). If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
Thanks for the updates! I hope you had a nice time in Iceland. Let me check how TableView handles overlapping merged cells. I do not recall, and there are probably some fundamental differences here since it lays out lazily. |
|
Golden file changes are available for triage from new commit, Click here to view. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
Golden file changes are available for triage from new commit, Click here to view. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
@hm21 FYI this PR has a merge conflict. |
|
@justinmc Thanks for pointing that out! I just fixed it, so it's ready for review now :) |
|
Golden file changes are available for triage from new commit, Click here to view. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
This pull request executed golden file tests, but it has not been updated in a while (20+ days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
Golden file changes are available for triage from new commit, Click here to view. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
I think this is ready to go, let me check in with @chunhtai |
chunhtai
left a comment
There was a problem hiding this comment.
Sorry for the late review. I think the logic and calculation looks good to me, but I left some comments on the way we cache data and some readability suggestions.
| required Iterable<double> rows, | ||
| required Iterable<double> columns, | ||
| required Float64List rowHeights, | ||
| List<Set<int>> spannedColumnsPerRow = const <Set<int>>[], |
There was a problem hiding this comment.
Since this is going to be public, we may need to pick a more straightforward API. Can we use a single bit map to encapsure the hidden cell?
There was a problem hiding this comment.
We can do it with a bitmap, but I’m not sure if that would make things more complicated to understand right now. However, the commit that implements it is a0cf25d in case we want to undo it.
| expect(find.text('Bottom Middle'), findsOneWidget); | ||
| }); | ||
|
|
||
| testWidgets('Table with all cells using TableCell.none in spanning area', ( |
There was a problem hiding this comment.
I vote for add assertion to enforce the use of TableCell.none, we can always relax later if this indeed becomes a pain point.
|
Sorry I came across this implementation, but I wanted to ask why there is a need of a public I would expect to span automatically without the need to specify a https://www.w3schools.com/html/html_table_colspan_rowspan.asp <table style="width:100%">
<tr>
<th colspan="2">Name</th>
<th>Age</th>
</tr>
<tr>
<td>Jill</td>
<td>Smith</td>
<td>43</td>
</tr>
<tr>
<td>Eve</td>
<td>Jackson</td>
<td>57</td>
</tr>
</table>I did a similar implementation for the PDF package, which calculates the missing TableCells automatically: https://github.com/DavBfr/dart_pdf/pull/1736/changes#diff-7dd0c0420d9a8667875d298f38592fb595a24957ee3291f9ca6c1c36e5e8593bR330 |
|
@Gustl22 That’s a good question. My main idea was not that the HTML-style approach could not work, but rather that I wanted to keep the table structure fully explicit in the widget tree.
I did think about the HTML table model, which is also why I chose That said, I’d still love to hear feedback from the Flutter team (@Piinks, @chunhtai) on whether moving closer to the HTML-style approach would make more sense here, even though I personally leaned towards the explicit version. |
…Table and TableBorder
…cells in RenderTable
|
I understand your intention. An idea that I throw in would be to have a convenience factory, which is able to implictly add |
…tection in RenderTable and TableBorder
Description
This PR adds support for
colSpanandrowSpanto theTableCellwidget.With this change, each
TableCellcan now span multiple columns and/or rows, similar to how HTML tables work.This allows developers to create more flexible and complex table layouts without manually merging cells or resorting to nested tables.
Examples
Example 1
Example 2
Issues Fixed
Fixes #21594
Questions for Reviewers
TableCell.noneto represent a skipped cell (for when another cell usesrowSpanorcolSpan). I’m not sure ifnoneis the best name, alternatives likeplaceholderorskippedmight also fit. I avoidedemptysinceDataCell.emptyalready exists with different semantics.DataTable? I initially didn’t include it cuz the Material Design table doesn’t normally supportrowSpanorcolSpan.Pre-launch Checklist
///).