Skip to content

Commit fdb7b21

Browse files
6pac-aiclaude
andcommitted
fix(grid): keep pinned band geometry after restoring pinning and while resizing
Ported from Slickgrid-Universal (ghiscoding/slickgrid-universal#2782, c4112428 and a490c253): - applyColumnWidths() and the colspan-fragment offsets size pinned bands from the rendered band widths, so restoring permanent pinning while a sticky column is docked no longer collapses the leading pinned cell - applyDockingToColumnChrome() clears inactive native chrome scroll offsets before measuring pinned header placement - updateCanvasWidth() re-applies column widths, chrome and row band sizes whenever the grid has docked columns, so a resize drag that leaves the canvas width unchanged keeps filter, footer, cells and row bands in step Adds quirk-pinning-restore-and-resize-geometry.cy.ts (LTR and RTL), which fails 6/6 without the fix and passes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d03d4d0 commit fdb7b21

2 files changed

Lines changed: 172 additions & 10 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/**
2+
* Regression tests for pinned-band geometry, in both reading directions.
3+
*
4+
* - Removing and restoring permanent pinning while a sticky column is docked keeps every body
5+
* cell aligned with its header. Without the fix the leading pinned cell collapsed to a few
6+
* pixels, because its column rule was sized from the layout band width rather than the
7+
* rendered one.
8+
* - Dragging a column's resize handle on a pinned grid whose canvas width does not change keeps
9+
* the filter cell, the footer cell, the body cells and the row's band template at the column's
10+
* new width during the drag. Without the fix they stayed at the old widths until mouseup.
11+
*
12+
* Reported from Slickgrid-Universal (ghiscoding/slickgrid-universal#2782, commits c4112428 and
13+
* a490c253). The harness is served from this file via cy.intercept.
14+
*/
15+
16+
const harnessHtml = `<!doctype html>
17+
<html lang="en">
18+
<head>
19+
<meta charset="utf-8">
20+
<title>Harness: pinning restore and resize geometry</title>
21+
<link rel="stylesheet" href="/dist/styles/css/slick-alpine-theme.css"/>
22+
<style> #myGrid { width: 700px; height: 300px; } </style>
23+
</head>
24+
<body>
25+
<div id="myGrid"></div>
26+
<script src="/dist/browser/slick.core.js"></script>
27+
<script src="/dist/browser/slick.interactions.js"></script>
28+
<script src="/dist/browser/slick.grid.js"></script>
29+
<script>
30+
var params = new URLSearchParams(location.search);
31+
var resizeMode = params.get('mode') === 'resize';
32+
var rtl = params.get('rtl') === '1';
33+
if (rtl) { document.getElementById('myGrid').setAttribute('dir', 'rtl'); }
34+
var colCount = resizeMode ? 5 : 12;
35+
var colWidth = resizeMode ? 80 : 100;
36+
var columns = [];
37+
for (var c = 0; c < colCount; c++) {
38+
columns.push({ id: 'c' + c, name: 'C' + c, field: 'f' + c, width: colWidth, minWidth: 30, sticky: (!resizeMode && c === 2) ? true : undefined });
39+
}
40+
var data = [];
41+
for (var r = 0; r < 30; r++) {
42+
var item = { id: r };
43+
for (var k = 0; k < colCount; k++) { item['f' + k] = 'r' + r + 'c' + k; }
44+
data.push(item);
45+
}
46+
var pinning = { columns: { left: ['c0'], right: ['c' + (colCount - 1)] } };
47+
window.grid = new Slick.Grid('#myGrid', data, columns, {
48+
enableCellNavigation: true,
49+
enableColumnReorder: false,
50+
rtl: rtl,
51+
showHeaderRow: resizeMode,
52+
headerRowHeight: 30,
53+
createFooterRow: resizeMode,
54+
showFooterRow: resizeMode,
55+
footerRowHeight: 25,
56+
pinning: pinning
57+
});
58+
59+
function headerOf(id) { return document.getElementById(window.grid.getUID() + id); }
60+
61+
window.misalignedCells = function () {
62+
var out = [];
63+
window.grid.getColumns().forEach(function (col, i) {
64+
var cell = document.querySelector('#myGrid .slick-row[data-row="1"] .slick-cell.l' + i);
65+
var header = headerOf(col.id);
66+
if (!cell || !header) { out.push(col.id + ' missing'); return; }
67+
var cb = cell.getBoundingClientRect(), hb = header.getBoundingClientRect();
68+
if (Math.abs(cb.left - hb.left) > 1.5 || Math.abs(cb.right - hb.right) > 1.5) {
69+
out.push(col.id + ' cell ' + Math.round(cb.left) + '..' + Math.round(cb.right) + ' header ' + Math.round(hb.left) + '..' + Math.round(hb.right));
70+
}
71+
});
72+
return out;
73+
};
74+
75+
window.scrollProxy = function (amount) {
76+
document.querySelector('#myGrid .slick-docking-horizontal-scroller').scrollLeft = rtl ? -amount : amount;
77+
};
78+
window.stickyDocked = function () { return headerOf('c2').classList.contains('slick-column-sticky'); };
79+
window.clearPinning = function () { window.grid.setOptions({ pinning: null }); };
80+
window.restorePinning = function () { window.grid.setOptions({ pinning: pinning }); };
81+
82+
window.wrongWidths = function () {
83+
var out = [];
84+
var widths = window.grid.getColumns().map(function (col) { return col.width; });
85+
widths.forEach(function (w, i) {
86+
[['filter', '.slick-headerrow-column.l' + i], ['footer', '.slick-footerrow-column.l' + i], ['cell', '.slick-row[data-row="1"] .slick-cell.l' + i]].forEach(function (p) {
87+
var el = document.querySelector('#myGrid ' + p[1]);
88+
if (!el) { out.push('c' + i + ' ' + p[0] + ' missing'); }
89+
else if (Math.abs(el.offsetWidth - w) > 1.5) { out.push('c' + i + ' ' + p[0] + ' ' + el.offsetWidth + ' vs ' + w); }
90+
});
91+
});
92+
var centre = widths.slice(1, -1).reduce(function (a, b) { return a + b; }, 0);
93+
var row = document.querySelector('#myGrid .slick-row[data-row="1"]');
94+
var tracks = row.style.gridTemplateColumns.split(' ').map(parseFloat);
95+
if (tracks[0] !== widths[0] || tracks[2] !== widths[widths.length - 1] || tracks[1] < centre) {
96+
out.push('row bands ' + row.style.gridTemplateColumns + ' for widths ' + widths.join(','));
97+
}
98+
return out;
99+
};
100+
101+
var drag = {};
102+
function mouse(type, target, dx) {
103+
target.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, clientX: drag.x + dx, clientY: drag.y, button: 0 }));
104+
}
105+
window.dragStart = function (colId) {
106+
var handle = headerOf(colId).querySelector('.slick-resizable-handle');
107+
var b = handle.getBoundingClientRect();
108+
drag.x = b.left + b.width / 2; drag.y = b.top + b.height / 2;
109+
mouse('mousedown', handle, 0);
110+
};
111+
window.dragMove = function (dx) { mouse('mousemove', document.body, dx); };
112+
window.dragEnd = function (dx) { mouse('mouseup', document.body, dx); };
113+
</script>
114+
</body>
115+
</html>`;
116+
117+
const visit = (query: string) => {
118+
cy.intercept('GET', '/quirk-pinning-restore-and-resize-geometry-harness.html*', {
119+
headers: { 'content-type': 'text/html' },
120+
body: harnessHtml,
121+
});
122+
cy.visit(`${Cypress.config('baseUrl')}/quirk-pinning-restore-and-resize-geometry-harness.html?${query}`);
123+
cy.window().its('grid').should('exist');
124+
};
125+
126+
describe('Quirk - pinned band geometry after restoring pinning and while resizing', () => {
127+
[false, true].forEach((rtl) => {
128+
const dir = rtl ? 'rtl' : 'ltr';
129+
130+
it(`keeps cells aligned with their headers after pinning is removed and restored with a docked sticky (${dir})`, () => {
131+
visit(`mode=restore&rtl=${rtl ? 1 : 0}`);
132+
cy.window().then((win: any) => win.scrollProxy(400));
133+
cy.window().should((win: any) => {
134+
expect(win.stickyDocked(), 'precondition: the sticky column is docked').to.eq(true);
135+
expect(win.misalignedCells(), 'precondition: aligned before the change').to.deep.eq([]);
136+
});
137+
cy.window().then((win: any) => win.clearPinning());
138+
cy.window().then((win: any) => win.restorePinning());
139+
cy.window().should((win: any) => {
140+
expect(win.stickyDocked(), 'the sticky column is still docked').to.eq(true);
141+
expect(win.misalignedCells(), 'cells aligned with headers after restore').to.deep.eq([]);
142+
});
143+
});
144+
145+
['c0', 'c1'].forEach((colId) => {
146+
it(`keeps filter, footer, cells and row bands at the new width while dragging ${colId}'s resize handle (${dir})`, () => {
147+
visit(`mode=resize&rtl=${rtl ? 1 : 0}`);
148+
const shrink = rtl ? 20 : -20;
149+
cy.window().then((win: any) => {
150+
expect(win.wrongWidths(), 'precondition: consistent before the drag').to.deep.eq([]);
151+
win.dragStart(colId);
152+
win.dragMove(shrink);
153+
});
154+
cy.window().should((win: any) => {
155+
expect(win.grid.getColumns().find((c: any) => c.id === colId).width, 'the drag changed the width').to.eq(60);
156+
expect(win.wrongWidths(), 'geometry follows the drag').to.deep.eq([]);
157+
});
158+
cy.window().then((win: any) => win.dragEnd(shrink));
159+
cy.window().should((win: any) => {
160+
expect(win.wrongWidths(), 'geometry after the drop').to.deep.eq([]);
161+
});
162+
});
163+
});
164+
});
165+
});

‎src/slick.grid.ts‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3445,7 +3445,8 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
34453445
*/
34463446
protected applyColumnWidths(): void {
34473447
let rule: any;
3448-
const centerWidth = this.hasDockedColumns() ? this.getDockingRenderedWidths().center : this.dockingLayout.centerWidth;
3448+
const renderedWidths = this.getDockingRenderedWidths();
3449+
const centerWidth = this.hasDockedColumns() ? renderedWidths.center : this.dockingLayout.centerWidth;
34493450
for (let i = 0; i < this.columns.length; i++) {
34503451
if (this.columns[i]) {
34513452
const w = this.columns[i].hidden ? 0 : this.columns[i].width || 0;
@@ -3457,8 +3458,8 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
34573458
useNaturalCenterPosition || docked?.band === 'center'
34583459
? centerWidth
34593460
: docked?.band === 'left'
3460-
? this.dockingLayout.leftWidth
3461-
: this.dockingLayout.rightWidth;
3461+
? renderedWidths.left
3462+
: renderedWidths.right;
34623463

34633464
rule = this.getColumnCssRules(i);
34643465
if (this._options.rtl) {
@@ -5141,7 +5142,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
51415142
}
51425143
}
51435144

5144-
if (widthChanged || forceColumnWidthsUpdate) {
5145+
if (widthChanged || forceColumnWidthsUpdate || this.hasDockedColumns()) {
51455146
this.applyColumnWidths();
51465147
this.applyDockingToColumnChrome();
51475148
this.applyDockingDimensionsToRows();
@@ -9657,6 +9658,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
96579658
if (!this.usesDockingChromeRegions()) {
96589659
return;
96599660
}
9661+
this.clearDockingNativeHorizontalScrollOffsets();
96609662
this.syncDockingChromeRegions();
96619663
this.dockingChromeByColumn.clear();
96629664
const columnIndexOf = (element: HTMLElement) => /(?:^|\s)l(\d+)(?:\s|$)/.exec(element.className)?.[1] ?? '';
@@ -11175,12 +11177,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
1117511177
}
1117611178
consumedWidth += widthOf(segment);
1117711179

11178-
const bandWidth =
11179-
segment.band === 'left'
11180-
? this.dockingLayout.leftWidth
11181-
: segment.band === 'right'
11182-
? this.dockingLayout.rightWidth
11183-
: this.getDockingRenderedWidths().center;
11180+
const bandWidth = this.getDockingRenderedWidths()[segment.band];
1118411181
const left = this.columnPosLeft[segment.start] ?? 0;
1118511182
const right = this.columnPosRight[segment.end] ?? left;
1118611183
if (this._options.rtl) {

0 commit comments

Comments
 (0)