diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index e977d872d..6ac46fb0d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -16,8 +16,17 @@ jobs: steps: # Without this, there are no files in the directory. - uses: actions/checkout@v3 + - name: Cache Flutter dependencies + uses: actions/cache@v4 + with: + path: | + ~/.pub-cache + ~/.dartServer/.analysis-driver + key: ${{ runner.os }}-pub-${{ hashFiles('pubspec.yaml', 'analysis_options.yaml') }} + restore-keys: | + ${{ runner.os }}-pub- # using flutter - - uses: subosito/flutter-action@v2.4.0 + - uses: subosito/flutter-action@v2 with: channel: 'stable' - run: dart --version diff --git a/lib/src/manager/event/pluto_grid_cell_gesture_event.dart b/lib/src/manager/event/pluto_grid_cell_gesture_event.dart index 8714d59a4..809c5aeb9 100644 --- a/lib/src/manager/event/pluto_grid_cell_gesture_event.dart +++ b/lib/src/manager/event/pluto_grid_cell_gesture_event.dart @@ -55,8 +55,12 @@ class PlutoGridCellGestureEvent extends PlutoGridEvent { if (stateManager.isCurrentCell(cell) && stateManager.isEditing != true) { stateManager.setEditing(true); + + _ensureCellVisibility(stateManager, column, rowIdx); } else { stateManager.setCurrentCell(cell, rowIdx); + + _ensureCellVisibility(stateManager, column, rowIdx); } } @@ -160,6 +164,8 @@ class PlutoGridCellGestureEvent extends PlutoGridEvent { if (stateManager.isCurrentCell(cell) == false) { stateManager.setCurrentCell(cell, rowIdx); + _ensureCellVisibility(stateManager, column, rowIdx); + if (!stateManager.mode.isSelectWithOneTap) { return; } @@ -182,6 +188,134 @@ class PlutoGridCellGestureEvent extends PlutoGridEvent { stateManager.setCurrentCell(cell, rowIdx, notify: false); } } + + void _ensureCellVisibility( + PlutoGridStateManager stateManager, + PlutoColumn column, + int rowIdx, + ) { + _ensureRowVisibility(stateManager, rowIdx); + + _ensureColumnVisibility(stateManager, column); + } + + void _ensureRowVisibility( + PlutoGridStateManager stateManager, + int rowIdx, + ) { + final verticalScroll = stateManager.scroll.vertical; + final bodyRowsVertical = stateManager.scroll.bodyRowsVertical; + + if (verticalScroll == null || bodyRowsVertical?.hasClients != true) { + return; + } + + final double rowSize = stateManager.rowTotalHeight; + final double rowStart = rowIdx * rowSize; + final double rowEnd = rowStart + rowSize; + + final double topOffset = stateManager.scroll.verticalOffset; + final double viewportHeight = stateManager.columnRowContainerHeight - + stateManager.columnGroupHeight - + stateManager.columnHeight - + stateManager.columnFilterHeight - + stateManager.columnFooterHeight - + PlutoGridSettings.rowBorderWidth; + + if (viewportHeight <= 0) { + return; + } + + final double bottomOffset = topOffset + viewportHeight; + + double? targetOffset; + + if (rowStart < topOffset) { + targetOffset = rowStart; + } else if (rowEnd > bottomOffset) { + targetOffset = topOffset + (rowEnd - bottomOffset); + } + + if (targetOffset == null) { + return; + } + + if (targetOffset < 0) { + targetOffset = 0; + } + + final double maxScroll = stateManager.scroll.maxScrollVertical; + + if (targetOffset > maxScroll) { + targetOffset = maxScroll; + } + + verticalScroll.jumpTo(targetOffset); + } + + void _ensureColumnVisibility( + PlutoGridStateManager stateManager, + PlutoColumn column, + ) { + if (stateManager.scroll.horizontal == null || + stateManager.scroll.bodyRowsHorizontal?.hasClients != true) { + return; + } + + if (stateManager.showFrozenColumn && column.frozen.isFrozen) { + return; + } + + final double? maxWidth = stateManager.maxWidth; + + if (maxWidth == null) { + return; + } + + final double bodyWidth = stateManager.showFrozenColumn + ? maxWidth - + stateManager.leftFrozenColumnsWidth - + stateManager.rightFrozenColumnsWidth + : maxWidth; + + final double viewportWidth = + bodyWidth - stateManager.scrollOffsetByFrozenColumn; + + if (viewportWidth <= 0) { + return; + } + + final double currentOffset = stateManager.scroll.horizontal!.offset; + final double columnStart = column.startPosition; + final double columnEnd = columnStart + column.width; + + final double visibleStart = currentOffset; + final double visibleEnd = visibleStart + viewportWidth; + + double? targetOffset; + + if (columnStart < visibleStart) { + targetOffset = columnStart; + } else if (columnEnd > visibleEnd) { + targetOffset = columnEnd - viewportWidth; + } + + if (targetOffset == null) { + return; + } + + if (targetOffset < 0) { + targetOffset = 0; + } + + final double maxScroll = stateManager.scroll.maxScrollHorizontal; + + if (targetOffset > maxScroll) { + targetOffset = maxScroll; + } + + stateManager.scroll.horizontal!.jumpTo(targetOffset); + } } enum PlutoGridGestureType { diff --git a/test/src/manager/event/pluto_grid_cell_gesture_event_test.dart b/test/src/manager/event/pluto_grid_cell_gesture_event_test.dart index 2d78cb017..1c043a01c 100644 --- a/test/src/manager/event/pluto_grid_cell_gesture_event_test.dart +++ b/test/src/manager/event/pluto_grid_cell_gesture_event_test.dart @@ -15,6 +15,8 @@ void main() { late MockScrollController verticalScrollController; late MockPlutoGridEventManager eventManager; late PlutoGridKeyPressed keyPressed; + late MockScrollPosition horizontalScrollPosition; + late MockScrollPosition verticalScrollPosition; eventBuilder({ required PlutoGridGestureType gestureType, @@ -45,6 +47,8 @@ void main() { verticalScrollController = MockScrollController(); eventManager = MockPlutoGridEventManager(); keyPressed = MockPlutoGridKeyPressed(); + horizontalScrollPosition = MockScrollPosition(); + verticalScrollPosition = MockScrollPosition(); when(stateManager.eventManager).thenReturn(eventManager); when(stateManager.scroll).thenReturn(scroll); @@ -54,8 +58,25 @@ void main() { when(scroll.bodyRowsHorizontal).thenReturn(horizontalScrollController); when(scroll.vertical).thenReturn(verticalScroll); when(scroll.bodyRowsVertical).thenReturn(verticalScrollController); - when(horizontalScrollController.offset).thenReturn(0.0); - when(verticalScrollController.offset).thenReturn(0.0); + when(horizontalScroll.offset).thenReturn(0.0); + when(verticalScroll.offset).thenReturn(0.0); + when(horizontalScrollController.hasClients).thenReturn(true); + when(verticalScrollController.hasClients).thenReturn(true); + when(horizontalScrollController.position).thenReturn(horizontalScrollPosition); + when(verticalScrollController.position).thenReturn(verticalScrollPosition); + when(horizontalScrollPosition.maxScrollExtent).thenReturn(1000); + when(verticalScrollPosition.maxScrollExtent).thenReturn(1000); + when(stateManager.columnRowContainerHeight).thenReturn(300); + when(stateManager.columnGroupHeight).thenReturn(0); + when(stateManager.columnHeight).thenReturn(0); + when(stateManager.columnFilterHeight).thenReturn(0); + when(stateManager.columnFooterHeight).thenReturn(0); + when(stateManager.rowTotalHeight).thenReturn(50); + when(stateManager.maxWidth).thenReturn(400); + when(stateManager.leftFrozenColumnsWidth).thenReturn(0); + when(stateManager.rightFrozenColumnsWidth).thenReturn(0); + when(stateManager.scrollOffsetByFrozenColumn).thenReturn(0); + when(stateManager.showFrozenColumn).thenReturn(false); }); group('onTapUp', () { @@ -122,6 +143,74 @@ void main() { }, ); + test('should reposition vertical scroll when tapped row is above viewport.', + () { + // given + when(stateManager.hasFocus).thenReturn(true); + when(stateManager.isCurrentCell(any)).thenReturn(false); + when(stateManager.isSelectingInteraction()).thenReturn(false); + when(stateManager.mode).thenReturn(PlutoGridMode.normal); + when(stateManager.isEditing).thenReturn(true); + when(verticalScroll.offset).thenReturn(40); + when(scroll.verticalOffset).thenReturn(40); + when(scroll.maxScrollVertical).thenReturn(1000); + clearInteractions(stateManager); + clearInteractions(verticalScroll); + + final cell = PlutoCell(value: 'value'); + const rowIdx = 0; + + // when + final event = eventBuilder( + gestureType: PlutoGridGestureType.onTapUp, + cell: cell, + rowIdx: rowIdx, + ); + event.handler(stateManager); + + // then + verify(stateManager.setCurrentCell(cell, rowIdx)).called(1); + verify(verticalScroll.jumpTo(0)).called(1); + }); + + test('should reposition horizontal scroll when tapped column is clipped.', + () { + // given + when(stateManager.hasFocus).thenReturn(true); + when(stateManager.isCurrentCell(any)).thenReturn(false); + when(stateManager.isSelectingInteraction()).thenReturn(false); + when(stateManager.mode).thenReturn(PlutoGridMode.normal); + when(stateManager.isEditing).thenReturn(true); + when(horizontalScroll.offset).thenReturn(0); + when(scroll.horizontalOffset).thenReturn(0); + when(scroll.maxScrollHorizontal).thenReturn(1000); + clearInteractions(stateManager); + clearInteractions(horizontalScroll); + + final column = PlutoColumn( + title: 'column', + field: 'column', + type: PlutoColumnType.text(), + width: 120, + ) + ..startPosition = 350; + final cell = PlutoCell(value: 'value'); + const rowIdx = 1; + + // when + final event = eventBuilder( + gestureType: PlutoGridGestureType.onTapUp, + cell: cell, + column: column, + rowIdx: rowIdx, + ); + event.handler(stateManager); + + // then + verify(stateManager.setCurrentCell(cell, rowIdx)).called(1); + verify(horizontalScroll.jumpTo(70)).called(1); + }); + test( 'When, ' 'hasFocus = true, '