@@ -4176,6 +4176,223 @@ void main() {
41764176 },
41774177 );
41784178
4179+ testWidgets (
4180+ 'Merged cells should not unmerge when the first cell is overlaid by a pinned column' ,
4181+ (WidgetTester tester) async {
4182+ // Regression test for https://github.com/flutter/flutter/issues/174862
4183+ final horizontalController = ScrollController ();
4184+ addTearDown (horizontalController.dispose);
4185+
4186+ await tester.pumpWidget (
4187+ MaterialApp (
4188+ home: Scaffold (
4189+ body: SizedBox (
4190+ width: 400 ,
4191+ height: 400 ,
4192+ child: TableView .builder (
4193+ cacheExtent: 0.0 ,
4194+ horizontalDetails: ScrollableDetails .horizontal (
4195+ controller: horizontalController,
4196+ ),
4197+ pinnedColumnCount: 1 ,
4198+ columnCount: 10 ,
4199+ rowCount: 10 ,
4200+ columnBuilder: (int index) => TableSpan (
4201+ extent: FixedTableSpanExtent (index == 0 ? 100 : 50 ),
4202+ ),
4203+ rowBuilder: (int index) =>
4204+ const TableSpan (extent: FixedTableSpanExtent (50 )),
4205+ cellBuilder: (BuildContext context, TableVicinity vicinity) {
4206+ final isColumn1 = vicinity.column == 1 ;
4207+ return TableViewCell (
4208+ columnMergeStart: isColumn1 ? 1 : null ,
4209+ columnMergeSpan: isColumn1 ? 3 : null ,
4210+ child: Center (
4211+ child: Text ('Cell ${vicinity .column },${vicinity .row }' ),
4212+ ),
4213+ );
4214+ },
4215+ ),
4216+ ),
4217+ ),
4218+ ),
4219+ );
4220+
4221+ // Initially, column 1 is visible next to pinned column 0.
4222+ expect (find.text ('Cell 1,0' ), findsOneWidget);
4223+ // Column 2 and 3 should be part of the merge, so they are not built.
4224+ expect (find.text ('Cell 2,0' ), findsNothing);
4225+ expect (find.text ('Cell 3,0' ), findsNothing);
4226+
4227+ // Scroll horizontally so that column 1 is entirely behind pinned column 0.
4228+ horizontalController.jumpTo (100 );
4229+ await tester.pump ();
4230+
4231+ // With the fix, column 1 is still built because it is under the pinned area.
4232+ // Since column 1 is built, its merge info is found and applied to columns 2 and 3.
4233+ expect (find.text ('Cell 1,0' ), findsOneWidget);
4234+ expect (find.text ('Cell 2,0' ), findsNothing);
4235+ expect (find.text ('Cell 3,0' ), findsNothing);
4236+ },
4237+ );
4238+
4239+ testWidgets (
4240+ 'Merged cells should not unmerge when the first cell is overlaid by a pinned row' ,
4241+ (WidgetTester tester) async {
4242+ final verticalController = ScrollController ();
4243+ addTearDown (verticalController.dispose);
4244+
4245+ await tester.pumpWidget (
4246+ MaterialApp (
4247+ home: Scaffold (
4248+ body: SizedBox (
4249+ width: 400 ,
4250+ height: 400 ,
4251+ child: TableView .builder (
4252+ cacheExtent: 0.0 ,
4253+ verticalDetails: ScrollableDetails .vertical (
4254+ controller: verticalController,
4255+ ),
4256+ pinnedRowCount: 1 ,
4257+ columnCount: 10 ,
4258+ rowCount: 10 ,
4259+ columnBuilder: (int index) =>
4260+ const TableSpan (extent: FixedTableSpanExtent (50 )),
4261+ rowBuilder: (int index) => TableSpan (
4262+ extent: FixedTableSpanExtent (index == 0 ? 100 : 50 ),
4263+ ),
4264+ cellBuilder: (BuildContext context, TableVicinity vicinity) {
4265+ // Merged cell spanning rows 1, 2, and 3.
4266+ final isRow1 = vicinity.row == 1 ;
4267+ return TableViewCell (
4268+ rowMergeStart: isRow1 ? 1 : null ,
4269+ rowMergeSpan: isRow1 ? 3 : null ,
4270+ child: Center (
4271+ child: Text ('Cell ${vicinity .column },${vicinity .row }' ),
4272+ ),
4273+ );
4274+ },
4275+ ),
4276+ ),
4277+ ),
4278+ ),
4279+ );
4280+
4281+ // Initially, row 1 is visible below pinned row 0.
4282+ expect (find.text ('Cell 0,1' ), findsOneWidget);
4283+ expect (find.text ('Cell 0,2' ), findsNothing);
4284+ expect (find.text ('Cell 0,3' ), findsNothing);
4285+
4286+ // Scroll vertically so that row 1 is entirely behind pinned row 0.
4287+ verticalController.jumpTo (100 );
4288+ await tester.pump ();
4289+
4290+ // Row 1 should still be built, maintaining the merge.
4291+ expect (find.text ('Cell 0,1' ), findsOneWidget);
4292+ expect (find.text ('Cell 0,2' ), findsNothing);
4293+ expect (find.text ('Cell 0,3' ), findsNothing);
4294+ },
4295+ );
4296+
4297+ testWidgets (
4298+ 'Table does not crash when focusing outside of the table while focused text field is not in the view' ,
4299+ (WidgetTester tester) async {
4300+ // Regression test for https://github.com/flutter/flutter/issues/137112
4301+ final verticalController = ScrollController ();
4302+ final horizontalController = ScrollController ();
4303+ addTearDown (() {
4304+ verticalController.dispose ();
4305+ horizontalController.dispose ();
4306+ });
4307+
4308+ await tester.pumpWidget (
4309+ MaterialApp (
4310+ home: Scaffold (
4311+ body: Column (
4312+ children: [
4313+ const TextField (key: Key ('outside_textfield' )),
4314+ Expanded (
4315+ child: TableView .builder (
4316+ verticalDetails: ScrollableDetails .vertical (
4317+ controller: verticalController,
4318+ ),
4319+ horizontalDetails: ScrollableDetails .horizontal (
4320+ controller: horizontalController,
4321+ ),
4322+ cellBuilder:
4323+ (BuildContext context, TableVicinity vicinity) {
4324+ return TableViewCell (
4325+ child: Center (
4326+ child: TextField (
4327+ key: Key (
4328+ 'cell_${vicinity .row }_${vicinity .column }' ,
4329+ ),
4330+ ),
4331+ ),
4332+ );
4333+ },
4334+ columnCount: 20 ,
4335+ columnBuilder: (int index) {
4336+ return const TableSpan (
4337+ foregroundDecoration: TableSpanDecoration (
4338+ border: TableSpanBorder (trailing: BorderSide ()),
4339+ ),
4340+ extent: FixedTableSpanExtent (100 ),
4341+ );
4342+ },
4343+ rowCount: 40 ,
4344+ rowBuilder: (int index) {
4345+ return TableSpan (
4346+ backgroundDecoration: TableSpanDecoration (
4347+ color: index.isEven ? Colors .purple[100 ] : null ,
4348+ border: const TableSpanBorder (
4349+ trailing: BorderSide (width: 3 ),
4350+ ),
4351+ ),
4352+ extent: const FixedTableSpanExtent (50 ),
4353+ );
4354+ },
4355+ ),
4356+ ),
4357+ ],
4358+ ),
4359+ ),
4360+ ),
4361+ );
4362+
4363+ // 1. Select a TextField in the table.
4364+ // Use the vicinity from the original crash report.
4365+ const vicinity = TableVicinity (row: 5 , column: 6 );
4366+ final Finder cellTextField = find.byKey (
4367+ Key ('cell_${vicinity .row }_${vicinity .column }' ),
4368+ );
4369+ // Bring it into view.
4370+ verticalController.jumpTo (250 );
4371+ horizontalController.jumpTo (600 );
4372+ await tester.pumpAndSettle ();
4373+
4374+ await tester.tap (cellTextField);
4375+ await tester.pumpAndSettle ();
4376+ expect (FocusManager .instance.primaryFocus, isNotNull);
4377+
4378+ // 2. Scroll until it disappears from the view, without unfocusing it.
4379+ verticalController.jumpTo (verticalController.offset + 1000 );
4380+ await tester.pumpAndSettle ();
4381+
4382+ // 3. Select another TextField outside of the table.
4383+ final Finder outsideTextField = find.byKey (
4384+ const Key ('outside_textfield' ),
4385+ );
4386+ await tester.tap (outsideTextField);
4387+ await tester.pumpAndSettle ();
4388+
4389+ // 4. Scroll back and ensure the table does not crash.
4390+ verticalController.jumpTo (verticalController.offset - 1000 );
4391+ await tester.pumpAndSettle ();
4392+ expect (cellTextField, findsOneWidget);
4393+ },
4394+ );
4395+
41794396 testWidgets ('Trailing pinned columns and rows - smoke test' , (
41804397 WidgetTester tester,
41814398 ) async {
0 commit comments