Skip to content

Commit a595cfb

Browse files
authored
feat(grid): improve column reorder auto-scroll (#1286)
* feat(grid): improve column reorder auto-scroll * chore: fix failing Cypress test
1 parent b99f79d commit a595cfb

3 files changed

Lines changed: 87 additions & 44 deletions

File tree

cypress/e2e/example-frozen-columns-reorder.cy.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -147,46 +147,65 @@ describe('Example - Frozen Columns - Column Header Reorder (characterization)',
147147
cy.get(RIGHT_VIEWPORT).scrollTo(0, 0, { ensureScrollable: false });
148148
});
149149

150-
it('should auto-scroll the right viewport when a header drag starts beyond the right edge of the grid', () => {
150+
it('should auto-scroll the right viewport when a header drag moves past the right edge of the grid', () => {
151151
cy.get(RIGHT_VIEWPORT).scrollTo(0, 0, { ensureScrollable: false });
152152
cy.wait(50);
153153
cy.get(RIGHT_VIEWPORT).should(($v) => expect($v[0].scrollLeft).to.eq(0));
154154

155-
// start a drag on "Finish" with the pointer already past the grid's right edge, then also fire a
156-
// document-level `drag` event at those coordinates (that is what a real browser does continuously
157-
// during a native drag, and what the future native reorder engine listens to for auto-scrolling)
155+
// Start the drag inside the viewport first, then move past the grid's right edge via document-level
156+
// drag events. This characterizes the live drag tracking behavior rather than a start-outside shortcut.
158157
cy.window().then((win: any) => {
159158
const finishHeader = getRightHeader(win, 'Finish');
160159
expect(finishHeader).to.exist;
161160
const rect = finishHeader.getBoundingClientRect();
161+
const startX = rect.left + rect.width / 2;
162162
const sy = rect.top + rect.height / 2;
163-
const dragX = (win.document.querySelector('#myGrid') as HTMLElement).clientWidth + 100;
164163
const dataTransfer = new DataTransfer();
165164

166-
pressPointer(finishHeader, rect.left + rect.width / 2, sy);
167-
finishHeader.dispatchEvent(createDragLikeEvent('dragstart', dragX, sy, dataTransfer));
168-
win.document.dispatchEvent(createDragLikeEvent('drag', dragX, sy, dataTransfer));
165+
pressPointer(finishHeader, startX, sy);
166+
finishHeader.dispatchEvent(createDragLikeEvent('dragstart', startX, sy, dataTransfer));
169167
});
170168

171-
cy.wait(250);
169+
// SortableJS dispatches its start callback on the next macrotask. Yield so the
170+
// grid can bind its document-level auto-scroll listeners before moving outside.
171+
cy.wait(50);
172172
cy.window().then((win: any) => {
173173
const finishHeader = getRightHeader(win, 'Finish');
174-
const sy = finishHeader.getBoundingClientRect().top;
175-
const dragX = (win.document.querySelector('#myGrid') as HTMLElement).clientWidth + 100;
176-
win.document.dispatchEvent(createDragLikeEvent('drag', dragX, sy, new DataTransfer()));
174+
const rect = finishHeader.getBoundingClientRect();
175+
const gridRect = (win.document.querySelector('#myGrid') as HTMLElement).getBoundingClientRect();
176+
const sy = rect.top + rect.height / 2;
177+
const dragX = gridRect.right + 100;
178+
const dataTransfer = new DataTransfer();
179+
win.document.dispatchEvent(createDragLikeEvent('drag', dragX, sy, dataTransfer));
180+
win.document.dispatchEvent(createDragLikeEvent('mousemove', dragX, sy, dataTransfer));
177181
});
178182
cy.wait(250);
179183

180-
cy.get(RIGHT_VIEWPORT).should(($v) => expect($v[0].scrollLeft).to.be.greaterThan(10));
184+
cy.window().then((win: any) => {
185+
const finishHeader = getRightHeader(win, 'Finish');
186+
const rect = finishHeader.getBoundingClientRect();
187+
const sy = rect.top + rect.height / 2;
188+
const safeX = rect.left + rect.width / 2;
189+
const dataTransfer = new DataTransfer();
190+
win.document.dispatchEvent(createDragLikeEvent('drag', safeX, sy, dataTransfer));
191+
win.document.dispatchEvent(createDragLikeEvent('mousemove', safeX, sy, dataTransfer));
192+
});
193+
194+
cy.get(RIGHT_VIEWPORT).then(($v) => {
195+
expect($v[0].scrollLeft).to.be.greaterThan(10);
196+
const scrollLeftAfterSafeZone = $v[0].scrollLeft;
197+
cy.wait(250);
198+
cy.get(RIGHT_VIEWPORT).should(($v2) => expect($v2[0].scrollLeft).to.eq(scrollLeftAfterSafeZone));
199+
});
181200

182-
// end the drag on the source itself: no reorder, and the auto-scroll must stop
201+
// end the drag on the source itself: no reorder, and the auto-scroll must remain stopped
183202
cy.window().then((win: any) => {
184203
const finishHeader = getRightHeader(win, 'Finish');
185204
const rect = finishHeader.getBoundingClientRect();
186205
const sy = rect.top + rect.height / 2;
187-
const dragX = (win.document.querySelector('#myGrid') as HTMLElement).clientWidth + 100;
188-
finishHeader.dispatchEvent(createDragLikeEvent('dragend', dragX, sy, new DataTransfer()));
189-
releasePointer(finishHeader, dragX, sy);
206+
const safeX = rect.left + rect.width / 2;
207+
finishHeader.dispatchEvent(createDragLikeEvent('dragend', safeX, sy, new DataTransfer()));
208+
releasePointer(finishHeader, safeX, sy);
190209
});
191210

192211
cy.get(RIGHT_VIEWPORT).then(($v) => {

cypress/e2e/example14-column-reorder.cy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ describe('Example 14 - Column Header Reorder (characterization)', { retries: 1 }
137137
// get no header element rendered and the SortableJS toArray() read-back therefore omits them.
138138
// The native reorder engine (PR 2 of the SortableJS removal) fixes this with column-map reconciliation.
139139
// Enable this test when that engine lands.
140-
it.skip('should keep hidden columns in the column set when reordering', () => {
140+
it('should keep hidden columns in the column set when reordering', () => {
141141
cy.window().then((win: any) => {
142142
const cols = win.grid.getColumns();
143143
cols[2].hidden = true; // hide "CPU1"

src/slick.grid.ts

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1951,13 +1951,41 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
19511951
this.sortableSideLeftInstance?.destroy();
19521952
this.sortableSideRightInstance?.destroy();
19531953

1954-
let columnScrollTimer: any = null;
1955-
1956-
const scrollColumnsRight = () => this._viewportScrollContainerX.scrollLeft = this._viewportScrollContainerX.scrollLeft + 10;
1957-
const scrollColumnsLeft = () => this._viewportScrollContainerX.scrollLeft = this._viewportScrollContainerX.scrollLeft - 10;
1954+
let columnScrollTimer: number | undefined;
1955+
let columnScrollDirection = 0;
19581956
let prevColumnIds: Array<string | number> = [];
1957+
let hiddenColumns = new Map<string | number, C>();
1958+
1959+
const stopAutoScroll = () => {
1960+
if (columnScrollTimer) {
1961+
clearInterval(columnScrollTimer);
1962+
columnScrollTimer = undefined;
1963+
}
1964+
columnScrollDirection = 0;
1965+
};
1966+
1967+
const autoScrollHandler = (event: DragEvent | MouseEvent) => {
1968+
const { clientX, clientY, pageX } = event;
1969+
if (!clientX || !clientY) {
1970+
return;
1971+
}
1972+
1973+
const viewportLeft = Utils.offset(this._viewportScrollContainerX)!.left;
1974+
const containerRight = Utils.offset(this._container)!.left + this._container.clientWidth;
1975+
const direction = pageX > containerRight ? 1 : pageX < viewportLeft ? -1 : 0;
1976+
1977+
if (direction !== columnScrollDirection) {
1978+
stopAutoScroll();
1979+
columnScrollDirection = direction;
1980+
1981+
if (direction) {
1982+
columnScrollTimer = window.setInterval(() => {
1983+
this._viewportScrollContainerX.scrollLeft += direction * 10;
1984+
}, 30);
1985+
}
1986+
}
1987+
};
19591988

1960-
let canDragScroll = false;
19611989
const sortableOptions = {
19621990
animation: 50,
19631991
direction: 'horizontal',
@@ -1975,42 +2003,38 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
19752003
},
19762004
onStart: (e: SortableEvent) => {
19772005
e.item.classList.add('slick-header-column-active');
1978-
canDragScroll = !this.hasFrozenColumns() || Utils.offset(e.item)!.left > Utils.offset(this._viewportScrollContainerX)!.left;
2006+
this._bindingEventService.unbindAll('colreorder');
2007+
stopAutoScroll();
19792008

1980-
if (canDragScroll && e.originalEvent.pageX > this._container.clientWidth) {
1981-
if (!(columnScrollTimer)) {
1982-
columnScrollTimer = window.setInterval(scrollColumnsRight, 100);
1983-
}
1984-
} else if (canDragScroll && e.originalEvent.pageX < Utils.offset(this._viewportScrollContainerX)!.left) {
1985-
if (!(columnScrollTimer)) {
1986-
columnScrollTimer = window.setInterval(scrollColumnsLeft, 100);
1987-
}
1988-
} else {
1989-
window.clearInterval(columnScrollTimer);
1990-
columnScrollTimer = null;
2009+
if (!this.hasFrozenColumns() || this._headerR.contains(e.item)) {
2010+
this._bindingEventService.bind(document, 'drag', autoScrollHandler as EventListener, {}, 'colreorder');
2011+
this._bindingEventService.bind(document, 'mousemove', autoScrollHandler as EventListener, {}, 'colreorder');
19912012
}
2013+
19922014
prevColumnIds = this.columns.map((c) => c.id);
2015+
hiddenColumns = new Map(this.columns.filter((column) => column.hidden).map((column) => [column.id, column] as [string | number, C]));
19932016
},
19942017
onEnd: (e: SortableEvent) => {
19952018
e.item.classList.remove('slick-header-column-active');
1996-
clearInterval(columnScrollTimer);
2019+
stopAutoScroll();
2020+
this._bindingEventService.unbindAll('colreorder');
19972021
const prevScrollLeft = this.scrollLeft;
19982022

19992023
if (!this.getEditorLock()?.commitCurrentEdit()) {
20002024
return;
20012025
}
20022026

2003-
let reorderedIds = this.sortableSideLeftInstance?.toArray() ?? [];
2004-
reorderedIds = reorderedIds.concat(this.sortableSideRightInstance?.toArray() ?? []);
2005-
2006-
const reorderedColumns: C[] = [];
2007-
for (let i = 0; i < reorderedIds.length; i++) {
2008-
reorderedColumns.push(this.columns[this.getColumnIndex(reorderedIds[i])]);
2009-
}
2027+
const reorderedIds = [
2028+
...(this.sortableSideLeftInstance?.toArray() ?? []),
2029+
...(this.sortableSideRightInstance?.toArray() ?? []),
2030+
];
2031+
const reorderedColumns = reorderedIds.map((columnId) => this.columns[this.getColumnIndex(columnId)]);
2032+
let visibleColumnIdx = 0;
2033+
const finalColumns = this.columns.map((column) => hiddenColumns.get(column.id) ?? reorderedColumns[visibleColumnIdx++]);
20102034

20112035
e.stopPropagation();
20122036
if (!this.arrayEquals(prevColumnIds, reorderedIds)) {
2013-
this.setColumns(reorderedColumns);
2037+
this.setColumns(finalColumns);
20142038
// reapply previous scroll position since it might move back to x=0 after calling `setColumns()` (especially when `frozenColumn` is set)
20152039
this.scrollToX(prevScrollLeft);
20162040
this.trigger(this.onColumnsReordered, { impactedColumns: this.columns, previousColumnOrder: prevColumnIds });

0 commit comments

Comments
 (0)