Repository navigation
Conversation
Freeze activation options — ViewportMgr refactor (SlickGrid)Reference for the grid-option surface controlling frozen rows/columns after the Option surface
Activation model
Plugin-facing band API (for completeness)
|
|
Will investigate ways to reduce the line count tomorrow, but this PR is the start for manual testing. |
|
looking into that, but I think it's worth testing at this point to sanity check the overall usability. Claude is indicating that it kept a lot of architecture the same way just to maintain byte-identical HTML in thos PR, but we can lose about 600 further lines through a subsequent stage 2 refactor. I'm investigating adding the sticky rows feature as well - I'm interested to see if that would use a lot of the same code. At the moment I'm not sreally sure how to roll this out though - this PR applies to main and can be merged locally without merging in the main repo, for testing. A further PR could I suppose also be pulled and merged locally on top of the previous merge. Is that workflow OK for you? |
|
I think that considering this is probably going to be released under a major (aka breaking) version, then there's no point in carrying over old code if you can rewrite it in a better way that might change certain flags and behaviours (that's what major versions are for). Like I said a while ago, we can create a branch named The only PR that could be merged and released under current v5 is the variable row height, I would put more focus on finishing that one. Anything else (freeze/pinning rewrite, sticky columns, drop SortableJS, ...) should be done in a separate
I did test the UI quickly, that's how I saw that you did stop rendering the pane-right/pane-bottom/... when not displayed (which I agree with). But apart from the right column freeze (which seems a little off in the demo since left columns weren't covering all left space though it becomes ok when grid has lots of columns), that feature alone surely doesn't explain the addition of over 1400 loc |
…ruction (Phase 1) Moves the construction of the 6 panes, 4 viewports, 4 canvases, header/headerrow/ top-panel/footer-row/pre-header containers out of SlickGrid.initialize() into a new internal ViewportMgr class (same file — a separate file would break script-tag consumers since the iife build emits one file per source). The grid keeps aliases to every element, so all logic is unchanged and the DOM is byte-identical, as proven by the dom-shape-characterization spec. Full suite green: 600 tests (599 pass, 1 pending), matching the Phase 0 baseline exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ty and width distribution into ViewportMgr (Phase 2, milestones 1-2) ViewportMgr now owns a freeze-state snapshot (pushed by setFrozenOptions) and: - paneCellIndex() — the (col,row)->pane index math from _getContainerElement - applyPaneFrozenClasses / applyPaneVisibility / applyOverflow - selectScrollContainers() — the setScroller X/Y-owner + follower selection - applyCanvasWidths() — the pane/viewport/canvas/header width distribution from updateCanvasWidth (width computations stay in the grid) The grid methods are now thin delegates. Behaviour identical: full Cypress suite green (600 tests, 599 pass / 1 pending), matching the Phase 0 baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n into ViewportMgr (Phase 2, milestone 3) ViewportMgr.applyPaneHeights() now computes paneTopH/paneBottomH/viewportTopH from the freeze configuration and sizes every pane/viewport/canvas, returning the heights for the grid's layout pipeline. The container VBox delta is passed as a lazy callback so the autoHeight-only style recalc is not made unconditional. Full Cypress suite green (600 tests, 599 pass / 1 pending), matching baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o ViewportMgr (Phase 2, milestones 4-5) - attachRow(): the 4-way canvas attachment from renderRows, returning the rowNode array. Preserves the historical render-side band threshold (rowIdx >= actualFrozenRow without the +1 non-frozenBottom adjustment used by paneCellIndex) verbatim. - syncHorizontalScroll(): the scrollToX follower fan-out (scroll owner, header, top panel, footer row, pre-header, frozen header-row/viewport followers); the grid-owned top-header panel stays in scrollToX. - syncVerticalFollowers(): the frozen-left viewport scrollTop mirroring from _handleScroll. - selectScrollContainers() now caches its result as the authoritative owner set. Full Cypress suite green (600 tests, 599 pass / 1 pending), matching baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…C-eligible Found by an adversarial transcription audit of the Phase 1-2 refactor: the manager retained references to every pane/viewport/canvas element and the container after destroy(), keeping the detached subtree alive if the app held onto the grid instance. Also restores the original post-destroy failure mode for the delegated methods. Full Cypress suite green (600 tests, 599 pass / 1 pending). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o ViewportMgr (Phase 2, milestone 6) - frozenRowOffset(): the getFrozenRowOffset computation (grid method stays as a public delegate), historical commented-out one-liner preserved. - isRowInFrozenBand(): the cleanupRows keep-frozen-rows predicate. - isRowCellCleanupExempt(): the cleanUpCells skip predicate, transcribed verbatim including the long-standing quirk that the second disjunct is not guarded by !frozenBottom (documented at the definition). - isColumnInFrozenBand(): the frozen-column cleanup exemption. Full Cypress suite green (600 tests, 599 pass / 1 pending), matching baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ase 2, milestone 7) New ViewportMgr helpers absorb the recurring 'which side of the freeze' patterns: - isColumnRightOfFreeze() — the hasFrozenColumns() && idx > frozenColumn test - sideLocalColumnIdx() — the right-side child-index rebase (idx - frozenColumn - 1) - sideForColumn() — pick the L or R element of a pair for a column Rewritten call sites: getHeader, getHeaderColumn, getHeaderRow, getFooterRow, getHeaderRowColumn, getFooterRowColumn, getHeadersWidth, getCanvasWidth, appendRowHtml (rowDivR clone guard + cell routing + always-render frozen band), appendCellHtml (frozen cell class), cleanUpAndRenderCells (node reattachment). The compound 'hasFrozenColumns() && idx <= frozenColumn' collapses to isColumnInFrozenBand() alone — equivalent for non-negative indices since the unfrozen sentinel is -1. Full Cypress suite green (600 tests, 599 pass / 1 pending), matching baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tes through ViewportMgr (Phase 2, milestone 8) Adds hasFrozenRows() accessor and bodyCanvasL() (the scrollable-body left canvas selector shared by updateRowCount and bindAncestorScrollEvents) to ViewportMgr, and rewrites the remaining ~34 grid-side sites onto the manager's accessors and column- side helpers: setupColumnResize, setupColumnReorder, createColumnHeaders, createColumnFooter, updateRowCount, scrollTo, scrollRowIntoView, render, renderRows, getCellFromEvent, setActiveCellInternal, navigateToPos, bindAncestorScrollEvents, appendRowHtml, updateCanvasWidth. Grid-side frozen-flag branching is now zero outside setFrozenOptions (the state owner): 95 sites at baseline, 3 writes remaining. frozenBottom/frozenRow option reads intentionally stay grid-side where they carry option semantics rather than band routing. Full Cypress suite green (600 tests, 599 pass / 1 pending). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(Phase 3, milestone 9) Introduces the opt-in lazyPanes grid option (no behaviour yet) and makes every ViewportMgr method plus the four remaining unconditional grid-side right-element writes (initialize spacer width, createColumnHeaders/headerRow emptying, updateRowCount canvasTopR height) tolerate absent panes. All guards are inert while every pane exists: full Cypress suite green (600 tests, 599 pass / 1 pending), matching baseline exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…zen (Phase 3, milestone 10) With lazyPanes: true and no frozen rows/columns at init, buildPanes/buildFooterRows now create just 2 panes / 1 viewport / 1 canvas plus left-side chrome, using inline conditionals so element order stays canonical in both modes. getHeaderChildren now iterates the _headers array instead of assuming two containers (the one crash the existence-guard pass had not covered). Adds examples/example-lazy-panes.html and a 7-test characterization spec proving the single-pane DOM shape and basic function (render, navigation, scrolling). Default-mode DOM unchanged: full suite green (607 tests, 606 pass / 1 pending — 600 baseline + 7 new minus overlap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Panes grid (Phase 3, milestone 11) - ViewportMgr.materializeSecondaryPanes(): builds the right/bottom panes, chrome, viewports and canvases at their canonical sibling positions and pushes them into the shared element arrays in place; idempotent. - SlickGrid.materializeLazyPanes(): hooked into setFrozenOptions (so it runs before setScroller/setColumns in the internal_setOptions pipeline); re-aliases fields, wires events for the new elements only, binds sort clicks on the new right header, and re-anchors ancestor-scroll bindings. - Event wiring extracted from finishInitialization into bindPaneEvents() shared by init and materialization — this also closes the audited gap where late-created viewports would miss MouseWheel handling. - Alias block extracted to syncViewportMgrAliases(); setupColumnSort parameterized. - Un-freezing keeps materialized panes (hidden), matching historical behaviour. - Tests: viewportmgr-lazy-materialization.cy.ts (6 tests: freeze columns from lazy, header split + row routing, unfreeze, refreeze with rows, fresh-page frozen-rows- first), deliberately sorted after the example-* specs and self-contained. Verification: 3 consecutive full-suite runs — green, green except one failure in example-excel-compatible-spreadsheet's native-clipboard paste test, green. That test is probabilistically flaky under full-suite load on this Windows machine (passes in isolation and in 2/3 full runs; also failed historically only in full runs); Linux CI remains the arbiter. All 13 lazy/materialization tests passed in all three runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stone 12) ViewportFreezeState gains frozenRowCount (the frozenRow option value) and updateFreezeState derives a FreezeBandCounts view — frozenLeftCols / frozenRightCols (0 until right-frozen columns land) / frozenTopRows / frozenBottomRows — alongside the authoritative legacy snapshot. hasFrozenColumns() is the first predicate re-expressed in band terms (frozenLeftCols > 0, provably equivalent to frozenColumnIdx > -1). Pure refactor, no behaviour change. Full suite: one ambient flake (example-plugin-headerbuttons, passes 9/9 in isolation — third distinct victim of the machine-level ~1-in-4 full-run flake, after excel-clipboard on the phase branch and row-span on pristine master), then a fully green re-run (613 tests, 612 pass / 1 pending). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oad flakes
Local full-suite runs showed an ambient ~1-in-4 flake in timing-sensitive tests
(native clipboard paste, render waits) reproduced on pristine master with three
different victim specs across runs. runMode-only retry absorbs these while
Cypress still flags retried tests as flaky; openMode stays at 0 so interactive
debugging sees raw failures. Matches the { retries: 1 } several specs already
carry individually.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… inert) New grid option frozenRightColumn: a COUNT of columns to freeze at the right edge (not an index like frozenColumn — counts stay correct under column reorder/hide). setFrozenOptions normalizes it (non-negative integer; the left and right bands must leave at least one scrollable column between them) and pushes it into ViewportMgr's band-count state as frozenRightCols. Nothing consumes the band yet — the right- frozen DOM, routing and geometry arrive in the following M13 stages — so behaviour is unchanged: full Cypress suite green, matching baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e 13b) ViewportMgr.materializeRightFrozenBand() builds three new panes with NEW *-right-frozen css classes (appended after the classic six so classic sibling positions are untouched — the historical 'right' elements keep their names and become the scrollable middle band), plus header/header-row/top-panel chrome, viewports, canvases and footer row; shared element arrays extend at the END so classic indexes 0-3 stay valid. Wired for both init-time (setFrozenOptions runs before the event-binding loops) and runtime enabling, with events bound to the new elements only; applyPaneVisibility shows/hides the band; un-freezing keeps it hidden in the DOM, matching classic pane behaviour. Staged state, pinned by viewportmgr-right-frozen-band.cy.ts (8 tests + new example page): the band exists and is visible, but cells still render in the classic canvases until geometry (M13c) and routing (M13d) land. Full suite green: 623 tests (622 pass / 1 pending). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…owing (Phase 4, milestone 13c) - Three-way width split: getCanvasWidth/getHeadersWidth accumulate canvasWidthRF/ headersWidthRF for the last frozenRightColumn VISIBLE columns (boundary via getFrozenRightStartIdx; identical behaviour when the band is off). - applyCanvasWidths pins the RF panes at the right edge and shrinks the scrollable middle band by the band width — in both the left-frozen layout and the no-left-freeze layout (where the historical '100%' widths become pixel widths while the band is active). - applyPaneHeights mirrors the classic right-pane vertical geometry for the band, including frozen-row canvas heights. - RF viewports keep both overflows hidden (never own a scrollbar) and follow Y via syncVerticalFollowers, scrollTo and updateRowCount. Scroll-owner selection needed NO change: the middle band was already the owner under the historical naming. Cells still render in the classic canvases until M13d routing. Spec grows a geometry test (band sized, right-pinned, middle+RF fill the container). Full suite green (622 tests, 621 pass / 1 pending). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (Phase 4, milestone 13d) Right-frozen columns now work end-to-end: - createColumnHeaders/createColumnFooter route the band's columns via the new three-way bandElementForColumn (headerRF/headerRowRF/footerRowRF included in the destroy/empty/width passes). - appendRowHtml builds a third row fragment; renderRows collects divArrayRF and attachRow appends it to the RF canvases, growing rowsCache[].rowNode to 3 entries (RF fragment always last; rowNodeIdxForColumn encapsulates the index math). - applyColumnWidths and getCellNodeBox rebase RF columns to band-local coordinates; paneCellIndex maps RF cells to array slots 4/5 (materializeRightFrozenPanes canonicalizes the classic set first so those slots hold under lazyPanes). - cleanUpCells exempts the band (always horizontally visible); RF cells get the 'frozen' css class; ensureCellNodesInRowsCache generalizes to N fragments; the public getHeader/getHeaderColumn/getHeaderRowColumn/getFooterRowColumn getters resolve three-way with band-local child indexes. Spec asserts real routing: header split, RF row fragments with band-local x=0, runtime toggle restoring classic routing both directions. Verification note: the first full-suite run after this change failed 13 rowspan tests in example-0032 during an overnight machine-load window (3:41 vs the usual ~2:45); the spec passes 45/45 in isolation, with its preceding spec subset, and the full-suite re-run is completely green (623 tests, 622 pass / 1 pending) — consistent with the documented ambient flake on this machine, which also struck this same spec on pristine master. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…audit (Phase 4, M13 hardening) A three-way adversarial audit proved the M13c/M13d changes behaviourally inert with frozenRightColumn=0 (sealing the example-0032 incident as the documented ambient machine flake) and surfaced four RF-on transition weaknesses, fixed here: - appendRowHtml: cell routing now requires the RF fragment to exist (guard was band-count-based while the fragment clone was DOM-based), with a same-fragment fallback in the three-way pick — no undefined targets during materialization transitions. - attachRow: the RF append guard now checks the actual target canvas (bottom-band rows previously checked canvasTopRF but appended to canvasBottomRF). - appendRowHtml: divArrayRF moved to a trailing optional parameter so downstream subclass overrides of the protected method keep compiling and binding correctly. - updateCanvasWidth: the getHeadersWidth() recompute guard gains the RF term, keeping it symmetric with applyCanvasWidths' distribution guard (stale headersWidthRF was otherwise possible with RF on and nothing else frozen). Full suite verified green BEFORE this commit: 623 tests, 622 pass / 1 pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…IntoView (Phase 4, milestone 13e) Right-frozen cells are always horizontally visible, so scrollCellIntoView now returns early for them — mirroring the historical left-frozen guard. New spec test drives arrow-key navigation across the middle↔right-frozen boundary in both directions, which also exercises the RF canvas keydown wiring and pane-index cell lookup end-to-end, and asserts the middle viewport does not scroll on band entry. Full suite verified green BEFORE this commit: 624 tests, 623 pass / 1 pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nert) New grid option frozenBottomRow: a COUNT of rows to freeze at the bottom, usable TOGETHER with frozenRow (which then always means top rows; the legacy frozenBottom flag is ignored when the count is set — it only positions the single-band case). setFrozenOptions normalizes it (non-negative integer; top + bottom bands must leave at least one scrollable body row) and pushes it into ViewportMgr's band derivation, which now expresses all three row-band configurations while every legacy configuration derives to exactly the same values as before. Nothing consumes the simultaneous-bands state yet — the bottom-frozen band DOM, geometry and routing arrive in M14b-d. Verification: first full run failed only the documented clipboard flake (passes 8/8 isolated); full re-run verified green BEFORE this commit (624 tests, 623 pass / 1 pending). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uble-binding (Phase 4, milestone 14b) - materializeBottomFrozenBand() builds one pane+viewport+canvas per active column band with *-bottom-frozen classes, appended after all existing panes; the shared bottom-frozen × right-frozen corner pane is added by WHICHEVER band materializes second (ensureBottomFrozenRightVariant called from both), with events bound on either path. applyPaneVisibility shows the band only in simultaneous top+bottom mode, mirroring classic bottom-pane column visibility. - Slot registry: dynamically materialized viewports/canvases record their array positions at push time (rfTopSlot/rfBottomSlot/bfSlot*), replacing paneCellIndex's hardcoded RF slots 4/5 — band materialization order can no longer skew lookups. - Fixes a latent M13b bug: finishInitialization sets initialized=true BEFORE its array-wide bindPaneEvents pass, so bands materialized during setFrozenOptions in that window were bound twice (once by the materializer, once by the array pass) — affected right-frozen-at-init grids. Materializers now gate on a _paneEventsBound flag set after the array-wide pass. Staged state pinned by viewportmgr-bottom-frozen-band.cy.ts (6 tests + example page): the band exists (init-time and runtime) but classic top-frozen rendering is unchanged until M14c geometry and M14d routing. Full suite verified green BEFORE this commit: 630 tests, 629 pass / 1 pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lowing (Phase 4, milestone 14c) In simultaneous top+bottom mode the scrollable body pane shrinks by the band height inside the height computation itself (single write, no post-adjustment); the band pins directly below the body with per-column-band widths mirroring the classic bottom panes across all three column configurations (left-frozen / right-frozen / plain), including the shared BF×RF corner pane. Band viewports are scrollbar-less on both axes; the band's scrollable-column viewport joins the X-followers in syncHorizontalScroll, mirroring how frozen-top viewports follow horizontal scrolling. The body pane remains the X/Y scroll owner (v1 layout: the horizontal scrollbar sits at the body's bottom edge, above the band). Rows still render in the classic canvases until M14d routing; the spec's new geometry test pins band height = frozenBottomRow * rowHeight and its position directly below the body pane. Full suite verified green BEFORE this commit: 631 tests, 630 pass / 1 pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…m frozen rows work end-to-end (Phase 4, milestone 14d) The 3x3 band model is complete: with frozenRow + frozenBottomRow set, the last frozenBottomRow rows render in the band's canvases across all column bands. - isRowInBottomFrozenBand: BOUNDED membership (splitRow <= row < splitRow + count) so the add-new row can never be captured; the same test serves both render and lookup sides — the new band deliberately has no threshold asymmetry (the historical one-row asymmetry remains only in the legacy single-band logic). - bottomFrozenSplitRow = dataLength - frozenBottomRow, computed in setFrozenOptions (staleness semantics deliberately match actualFrozenRow's). - attachRow routes three row bands x three column bands (incl. the BF x RF corner fragment); paneCellIndex resolves BF cells via the slot registry. - render() gains a second frozen pass + a band range in the horizontal-scroll cell pass; updateRowCount excludes the band from the scrollable body count; frozenRowOffset rebases band rows to band-local coordinates; cleanupRows/ cleanUpCells exempt the band; band rows get the frozen css class; scrollRowIntoView never scrolls for band rows; getCellFromEvent resolves clicks in the band canvas (the -bottom-frozen class token cannot collide with the classic -bottom selector). Spec now asserts real routing: Task 498/499 in the band with band-local y=0, frozen class, click-activation without body scroll, runtime toggle restoring classic routing. Full suite verified green BEFORE this commit: 632 tests, 631 pass / 1 pending. BF-off equivalence audit running as the closing check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two-agent adversarial audit of the M14b-d delta vs the M13e tip; the render-routing area proved fully inert when simultaneous mode is off, and one real regression plus two tightenings were found and fixed: - disableSelection ordering (REAL): the _paneEventsBound guard swap had silently dropped disableSelection() for headers materialized during the init window (right-frozen-at-init and lazy+right-frozen grids) — it was never part of the double-binding the flag fixed, and the array-wide disableSelection(this._headers) ran BEFORE setFrozenOptions created those headers. finishInitialization now applies it AFTER setFrozenOptions so init-materialized headers are covered via the shared array, with no double-application. - setFrozenOptions consults getDataLength() only when frozenBottomRow is actually in use, restoring BASE's call cadence for legacy grids (matters for side-effectful CustomDataView.getLength implementations and the null-data pre-init edge). - getCellFromEvent short-circuits the '.grid-canvas-bottom-frozen' ancestor walk behind hasBottomFrozenBand() (perf; the class token could never false-match). Full suite verified green BEFORE this commit: 632 tests, 631 pass / 1 pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Formula-transcribed pins computed from live column data (no hardcoded px): the +1000 left/single-band header slack (exact), the right-frozen band as a PLAIN column sum with no slack or scrollbar (exact, header and canvas), the cumulative headersWidthR floor under a left freeze, plain per-band canvas sums, and the plain-grid quirk that the hidden R header container still gets width: 0px written. All 11 tests pass against current code before the computeHeaderWidths/computeCanvasWidths relocation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
computeHeaderWidths/computeCanvasWidths move VERBATIM from getHeadersWidth/ getCanvasWidth (reverse iteration and all), guarded by the golden spec committed first (viewportmgr-width-golden.cy.ts, 11 formula pins). Quirks preserved in place: the +1000 left/single-band slack, CUMULATIVE r (includes the post-slack l) under a left freeze, the RF band as a plain sum, the out-of-range isColumnRightOfFreeze(columns.length) scrollbar probe after the loop, and fullWidthRows extra width to the scrollable band. Two deliberate non-moves: rfStartIdx stays a GRID-derived fresh input (these call sites historically re-derive it per call; the freeze snapshot is not substituted), and the grid keeps headersWidthL/R/RF + headersWidth + canvasWidthL/R/RF as synced mirrors - they are protected fields visible to subclass wrappers (slickgrid-universal); deleting them is a semver-major follow-up, not part of this work. tsc/eslint clean; gate 77/77 (golden spec leading); full suite 679 / 0 failing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion) - columnBandGeometry(colIdx, geometry): applyColumnWidths' three-way band width pick plus BOTH historical x-reset conventions as named booleans - the RF band resets BEFORE its first column, the left freeze resets AFTER the frozen column (which itself does not accumulate). frozenColumnIdx and rfStartIdx stay grid-fresh inputs (live options read + per-call derivation preserved). - accumulateBandWidths: setupColumnResize's two CLEAN bucketing passes collapse to one call. Honest scope note: the report claimed six repeats; the other four interleave bucketing with forceFit width mutation and are a different shape - they stay inline. - setLiveResizeLeftWidth: the drag-time header slack (+1000) and middle header pane re-anchor move behind the facade. - updateColumnCaches keeps its left-only reset INLINE, now documented: RF columns deliberately continue its coordinate space (preserved asymmetry vs the band oracle). tsc/eslint clean; gate 81/81; full suite 679 / 0 failing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- createRowFragments(rowDiv): clone-per-active-band moves into the vm (the
clone-not-share requirement documented at the source); appendRowHtml keeps
the divArray push plumbing verbatim - the holding-div drain in renderRows
depends on its exact pattern, so the alignment-hazard zone is untouched.
- fragmentForColumn(frags, i, { alwaysRenderColumn, branch }): the cell
routing rules keyed by the caller's EXACT control branch. The viewport
branch keeps the RF->L fallback (transition safety); the offViewport
branch keeps alwaysRender/left-frozen->l and right-frozen->rf. The two
rule sets are deliberately NOT unified into one inViewport predicate:
outer-viewport-test-true with isRenderCell false must render NOTHING,
including frozen cells (historical).
- collectRowCellNodes: the fragment-flatten with its ascending-column-order
invariant (cellRenderQueue tail-draining depends on it) named and moved.
- updateRowPositions keeps its [0]-only reposition INLINE, now documented
as inherited upstream behavior.
Considered and skipped (no facade gain): appendCellToRow (the band routing
is already the vm's rowNodeIdxForColumn), setRowTop as a member, and any
restructure of the renderRows holding-div drain.
tsc/eslint clean; gate 180/180 (rowspan/colspan suites leading); full
suite 679 / 0 failing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sion) The facade plan (FACADE-FEASIBILITY.md) scheduled these for terminal deletion as compat scaffolding. The M19f census says otherwise: ~57 legitimate references remain at grid chrome/geometry sites where vm.paneHeaderL reads better than a paneAt()/at() chain with a non-null assertion, and the vm's own geometry appliers and materializer manifests use them throughout. Deleting them would trade readable code for ~66 lines - the same no-facade-gain test that trimmed other report members (appendCellToRow, setRowTop, isBandBoundaryAfter) applies. The getter block's comment now states the retention decision and their role. M19 series complete: a (collections + alias deletion), b (column/row routing + predicates, spec-guarded), c (materialization manifests), d (width arithmetic relocation, golden-guarded), e (render-path minimal cut), f (this decision). slick.grid.ts net -422 vs master with the full 3x3 band feature set included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This PR has undergone three stages: 1) wrap the viewport/panes in an object, 2) rework individial viewport related objects (eg, ViewportL, ViewportR, HeaderL, HeaderR, HeaderRowL, HeaderRowR) into an array 3) encapsulate all multi pane concerns into the ViewportMgr object and take the slick.grid.ts code back to what it looked like before the frozen panes change. The primary reason I wanted to do this change was to simplify the code in 'slick.grid.js'. This goal has not been met. To illustrate what I mean, take the following examples: SlickGrid-pre-frozen: (in createColumnHeaders()) SlickGrid-post-frozen: (in createColumnHeaders()) 6pac master: (in createColumnHeaders()) 6pac viewportmgr-phase4: (in createColumnHeaders()) The pre-frozen code is simple and says exactly what it does. Then post-frozen, rather that reading the logical flow, you suddenly have to deal with multiple headers etc - and with the LazyPanes option we used the multiple headers may not even be actively used. desired: This would require wrapping all of the headers and footers into the ViewportMgr object - and that's what I really wanted, a single object that slick.grid.js can manipulate like a single pane that keeps all of the concerns about how many pane/header/footer elements there are and hides them from the grid code. Another example: SlickGrid-pre-frozen: SlickGrid-post-frozen: 6pac master: 6pac viewportmgr-phase4: } desired: Report on the feasibility of this, and list the proposed new members of ViewportMgr, and before and after code in slick.grid.ts for each use case in that file. It impacts on both the viewportmgr-phase4 and the m16 plan currently in progress. |
|
@6pac so I asked Codex 5.6 Sol (which is the equivalent to Claude Fable 5) to see if I can replicate this PR but to do it in a target of 300-400 LOC (line of code) and it was able to achieve that (instead of 1400 loc which is what you have so far), but... then I had another idea, what if we implement the same approach as Ag-Grid (column pinning) and/or SyncFusion grid (frozen row/column)? The biggest advantage is that the UI is much more appealing since it only has 1 horizontal scroll and 1 vertical scroll for the entire grid even if it's 3x3 or more, so the UI is much more user friendly I find (versus SlickGrid freezing which has multiple scrollbars with most of them disable but visible for alignments) So I asked Codex to try to implement the same concept as Ag-Grid, it called that a Docking Controller, this is still a POC but after an evening on it, I have a rough working POC. I like this approach much better and the bonus is that I asked Codex to eventually add sticky in combo with pinning and it would totally be doable without that much more loc addition. Screencast_20260903_005128-2.webmI asked the AI to give me an LOC estimate after what I have working so far including the addition of pinning and sticky, the result is shown below (please note that the LOC count includes CSS stylesheet lines)
so in the end, I'm much more interested in this new approach of a Docking Controller with single scrollbar instead of your Viewport Manager (this PR). I've burned almost all my weekly tokens for reasoning and implementing this, so I might only finish it next week, but it's looking good so far... what do you think? Also again a reminder that this would only be released in our next major version because there's obviously some breaking changes on the entire DOM and also the grid options changes and all. Also compared to Ag-Grid, my POC keeps pinning as column index (pin all columns up to x), but Ag-Grid actually separates each column pinning, meaning that you pin each column independently (you could pin column 1 and 3, but leave 2 unpin which would move it to the right pane since it isn't pinned). I'm not sure if we should use this approach or keep using "pin up to x" approach, Ag-Grid is more flexible but it requires adding cc @zewa666 |
|
jeez this looks mighty as hell. lots of tasks on my table but I definitely need to check this out soon |
|
Interesting! What you are proposing sounds better than the SlickGrid frozen columns feature presently, and the sticky grids feature would be awesome. But go up a few comments and read where I say 'This is the prompt for part 3, just to make clear what the motivation was.' I don't think it fulfils that requirement at all. So I have to rethink how important that is to me. TBH, it was quite important when I thought I would be hand-coding going forwards, but now that AI is doing all the work, it probably doesn't matter if the codebase is hard to read. And honestly, I think we're nearly there re the feature set. How much more can we implement? I am quite keen at this point to do the next breaking release with what we have and just clean the slate. Then I think we're well-positioned to run these kind of experiments with much less rebase churn. |
|
@6pac I understand your concern but this new concept (which again is follow Ag-Grid approach) is totally different compare to the previous div containers (I see them as independent silos on a farm) but with the new approach it's rather working with divs and rows, it's very different in the DOM (which is totally a breaking change especially for E2E tests, I'm assuming this would break a lot of the Cypress tests and will require lots of refactoring). At this point, I'm only focusing on making it work, I haven't looked that much at the code but your comment above is part of the AI completion task. I think it's also a good time to get rid of that +/- 1000px offset that I never understood what it does exactly. It might have been an IE fix of some kind, it's time to get rid of these old hacks I think Anyway, I asked the AI to compare current POC with your point 3 above (I literraly provided your entire comment above) and it replied with: Codex analysis of your previous commentThe original PR’s principle is exactly right—and the current POC has not reached that maintainable end state yet. The architectural direction is better: one viewport, one canvas, one horizontal scrollbar, and a pure
The clearest warning sign is [`setFrozenOptions()`](/home/ghislainb/sourcecode/slickgrid-universal/packages/common/src/core/slickGrid.ts): it force-disables the old API, but all of the fields and branches it protects are still present. That is useful for a POC, but it should never be the final form. There are still hundreds of freeze/pane-related references in I would not revive the old The desired final code should look close to the pre-frozen examples from the PR:
Pinning-specific logic should be confined to:
Everything else should no longer know “left pane”, “right pane”, “top pane”, or “frozen”. Because this is intentionally breaking, I recommend making this a merge/acceptance condition: no dormant So: the POC is a good substrate, but merging it before the deletion/consolidation pass would make maintenance worse. The cleanup pass is not optional polish; it is the part that realizes the original PR’s simplification goal. updated DOMbelow you can see that we're dealing more with rows instead of previously entire div container silo, we have 3 possible div container for each row, we have a
|
|
@6pac @zewa666 there you go, another 200-300 loc maybe and we have sticky columns and rows (in addition to pinning feature). I have 2 separate examples, 1 example for pinning and 1 example for sticky. They are 2 separate features but they share a lot of code together. Can we use both pinning and sticky in the same example? I'm not sure maybe... but I would think that the basic idea is to use pinning or sticky separately. after having both pinning and features, I asked the AI to give me another LOC count update: The +1,656 additions / +1,469 net LOC covers the combined POC implementation for:
It does not include Example 04, Example 47, tests, or framework demos. It also still includes legacy frozen-pane code that is planned for removal, so the final cleaned net LOC should be lower. Screencast_20260903_184235.webmisn't that awesome? Also note that I did not do a code cleanup or review yet, I mean I'm focused on making the POC work. There's most probably lot of cleanup that we could do to decrease LOC count and drop any legacy frozen related stuff (because again this is definitely a breaking change and totally different UI/UX)... and now I'm really close to be out of tokens (less 8% left), so I will probably have to pause for couple days. |
|
OK, it sounds like this will also meet my goal. The only issue is the change for those who might be wedded to the way the frozen rows/column feature works already. So I'm 100% happy with this, but I think it's probably worth asking some consumers of the frozen feature what they think. |
|
Great work by the way, this is exactly what the project needs - new ways of looking at problems! |
|
@6pac That's what breaking version (major) really mean and that is also why I want this renamed to pinning rather than freezing. Also nowadays, AI will do most of the work anyway. I mean you could ask @zewa666 but he's probably going to answer the same as me... Ideally, just a global search & replace should do the work but it might depend on the answer for the question below. What I'm unsure at this point though is as I mentioned above, should we use pinning on
The sticky part is done on the column basis, so perhaps we should do the same for pinning? |
|
Sure, don't take this as a criticism. I think this is a good change. What I'm saying is that a major version usually means 'we've had to break some things to move forward and you'll need to check your code'. |
|
Re the config, I'd tend to favour 1) but again be good to get opinions from people who want to use it, ie. try to think of a use case that will break it. |
|
no worries, I did take it as a constructive comment. So on the breaking change side of things, I do typically like to offer some kind of transitions... but in this case I don't see any possible way of doing it because I certainly don't want to support both legacy freezing and this new pinning. For the implementation choice, I had another thought and I think that going with an hybrid approach is the best possible way to avoid this from being an even bigger breaking change. Let me explain, in Slickgrid-Universal, we also have Grid State & Preset and I did add the freezing columns in there and we do use it on a big project at work. So if we move it to the column instead of the grid option, that would a much bigger breaking change and an hybrid approach (with grid option index) would smoothen the blow I think. For a survey, we could open one in the Discussion tab, but the problem is that barely anyone would see it, the only other option would be to tag people directly, typically the big users and/or recent contributors. I did that couple times. But I think I need to go further with the POC before doing any such survey. Like I said earlier, I didn't even look much at the code, neither the grid options, and so I need to advance the POC more into an MVP before making any surveying. I'm pretty sure that most user would simply be happy with the breaking change especially considering the better UI/UX... I also have to make sure that we're not decreasing in perf for very large dataset with this new pinning/sticky approach. Once I'm done, I would ask you to make a second view analysis with Claude, having 2 reviews from 2 different AI is even better |
|
I think the major version would indicate this well enough. yeah its a bit more than a break, its a feature deprecation sort of, but the replacement is subjectively better for me. what the latest push in AI development made me think of though is that we might should start thinking about shipping curated skills along with the library. that would serve two purposes. first, LLMs would know better how to apply specific features from slickgrid on the consumer end. but secondly, the skills could also act as a verification of the docs and thus overall improve the development of new features as LLMs could check up on skills when touching existing features |
|
@zewa666 I agree there should be something along those lines. A comprehensive reference manual, would be a good start. |
|
@ghiscoding I've had a bit of time to think about this now. As I said, personally I have no preference around these; I'm fine with what you've proposed above. But I wonder, there do seem to be a lot of developers using SlickGrid essentially as an Excel replacement. I think this new proposal is different to how Excel behaves? So I do wonder if that will be a problem for them. Also, is it possible for me to get access to the PR/code branch for this? Or has it been done on SlickGrid-Universal? It seems a bit crazy, but now I'm wondering if we should offer all three options - the classic frozen rows/columns, the viewport-based frozen rows/columns and the sticky columns. It would definitely increase the codebase size (and complexity) significantly, but a lot of the more complex logic could be spun off to plugins. I know at least one organisation that's using this for an Excel-like purpose, so I've contacted them and sent some links for comment. |
|
@6pac I'm still working on it in Slickgrid-Universal only via PR ghiscoding/slickgrid-universal#2782, once I'm done then I'll ask Codex to replicate the entire thing in here. Should we keep the freezing option, the answer is No... but we can replicate similar behaviour to what freezing was. So let me explain, the new feature is being renamed to Pinning (the freezing wording is being dropped) because I wanted to start with new naming since it's a full rewrite, but also because it's really different single it's just single column Pinning (it adds The other great thing that I love with the new UI is that we now have what it calls Docking and we no longer have separate divs with separate scrollbars, we now 1 horizontal scrollbar and 1 vertical scrollbar with/without Pinning. My main reference was Ag-Grid, Codex implemented the same approach, which I like a lot (single scrollbar is so much nicer and usable) in the print screen below, we can now have all Pinning sides all at once without any issues
It now works correctly with Grouping as well, this will fix the super old issue #443, you can also see below that I pinned the 2nd column which keeps the checkboxes (1st column) to the unpin section, single column pinning is just so much more flexible than before Screencast_20260910_223012-1.webmI'll probably be done by early next week and replicate the PR in here. It did a lot of rewrite, lot of variable changes (there's no pane, nor freezing container anymore) and re-implemented pretty much the entire way of showing things. Also as part of my acceptance, I did provide your original goal feedback (making it more readable) to my goals list, so it is being considered. I do keep a progress file for the AI to stay focus, you can see the current Pinning Progress Also since I have full unit test suite, codecov currently reports ~750 LOC (that's raw LOC, without comments and without any CSS) and that does include Sticky feature as well. So I'm very satisfied with the end result, this fixes a lot of UX problems I've encountered over the years. |
|
Great, thanks for providing the progress update. |
|
and I've just fixed the last bug I found, it now works with colspan and pinning too (though it required ~75 LOC to fix), with the small exception that the pinning must be sequential (e.g. works with Screencast_20260911_013813.webmalso I forgot to provide some code sample to explain what I meant above gridOptions = {
- frozenColumn: 2,
- frozenRow: 2,
- frozenBottom: false,
+ pinning: {
+ columns: {
+ left: 2, // pin from 0 to 2.. or use `[0, 1, 2]` which is equivalent
+ // you can "right" too
+ },
+ rows: {
+ top: [0, 1], // or integer from 0 to X
+ // you can add "bottom" too
+ }
}or you could just simply define it directly in the column definition via columns = [{
id: 'firstName',
field: 'firstName',
+ pinned: true,
}, {
id: 'lastName',
field: 'lastName',
+ pinnabled: false, // cannot be pinned dynamically
}]what about the Sticky feature? That one is via column definition for the sticky columns, and via grid options for the sticky rows gridOptions = {
+ stickyRows: {
+ both: ['total-revenue', 'total-expenses', 'net-profit']
+ },
}sticky columns columns = [{
id: 'firstName',
field: 'firstName',
+ sticky: 'both', // can be "left", "right" or "both" for which side to stick
}]that's it, pretty straightforward I think... oh 1 very important thing I forgot to mention the +/- 1000px offset is gone, I assume that was in place for issues with IE6 but we no longer need this and it was causing issues at first, so yeah it's gone! |
|
Closed- superseded by new single-pane approach. |
Merging next-v6 brought in the VitePress site, which still documents the 3x3 frozen band model from #1238. That PR was closed without merging, and this one removes frozen panes, so: - docs/in-depth/frozen.md is replaced by docs/in-depth/pinning-sticky.md, the page this branch kept at the docs root. It now has the site's front matter, a v6 note, the complete list of examples and links into the reference. The sidebar, the In-depth index, the home page and four other pages that linked to or described frozen panes now point to it or describe pinning. - The migration guide describes the move from frozen panes to pinning. Its API list for this PR is the actual delta against next-v6: 11 options, 2 column properties, 4 methods and 5 events added, and 10 options and 4 methods removed. validateColumnFreeze() from #1299 is removed again, so that entry is updated too. - The option, event and column-property counts on the introduction pages follow the new API (120, 62 and 52). Two corrections to the page itself: permanent pins are rejected only when they are wider than the whole grid, because the column budget limits sticky columns only; and getColumnByIdx(), getColumnHeaderByIndex() and removeCellCssStylesBatch() are no longer listed as removed, because no 6pac release had them. The Docs workflow runs a strict drift gate on every push to next-v6, and it would have failed on this merge: five new options had no JSDoc and five new events had no reference note. The options now have JSDoc, and the events have overlays, as the other 57 events do. The overlay for the removed option throwWhenFrozenNotAllViewable is deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) * feat(grid)!: replace frozen panes with pinning and sticky docking * chore: fix cypress failures * chore: colspan and grouping should spread left to right over pinning * chore: part 1 fixes of audit review * chore: part 2 fixes of audit review * chore: part 3 fixes of audit review * chore: part 4 final pass fixes of audit review * chore: fix cypress failing tests * chore: fix cypress failing tests * chore: fix cypress failing tests * fix(grid): drop DOM references reflectively in destroy(true) `destroy(true)` nulled a hand-maintained list of 46 property names, so every new element field (the docking overlay, proxy scroller and chrome regions among them) had to be added in two places and `dockingChromeByColumn` was missed. The element references are now cleared by content: any field holding an element, a non-empty array of elements or a plain record of elements is nulled, and the chrome map is cleared. A self-hosted spec checks single, array and record fields on a plain and a pinned grid. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(grid): remove slickgrid-universal plumbing that is inert in 6pac Drop the options, fields and code paths that were carried over from the multi-package fork and have no implementation here: the formatted-data cache planner and its DataView hooks (`SlickDataView` implements neither), `enableGridMenu` last-column compensation, `enableRowDetailView`/`rowDetailView.renderMode`, `silenceWarnings` and the zoom warning, `selectionOptions` (the selection model's own option is authoritative again), `datasetIdPropertyName` (the DataView's id property is used), `Column.editorClass`, `exportCustomFormatter`, `exportWithFormatter`, the unread `Column.pinnable`, `EditorArguments.isCompositeEditor`, and the `FormattedDataCachePlanner`/`TrustedHTML` type stubs. `sanitizeHtmlString` is back to its 6pac signature with the `logSanitizedHtml` logging path. The two functional additions that were kept (`allowDragFromClosest`, `columnResizingDelay`) are now documented. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(grid): resolve getCellFromPoint through the rendered docking layout `getCellFromPoint()` on a docking grid used `document.elementFromPoint()` and fell back to the natural layout when the point was off-screen or not over a cell, so drag auto-scroll and non-contiguous row pins still resolved the wrong cell. The point is now mapped geometrically: pinned/sticky rows through the overlay bands, scrolling rows through the inverse of `getRenderedRowTop()`, and columns through the left/centre/right band offsets at the current scroll position. RTL and non-docking grids keep the original calculation. With the end cell resolved correctly, dragging upward from the bottom-right of a pinned grid auto-scrolls up again, which is what the base spec asserted; the weakened `equal` assertion is restored to `greaterThan`. A self-hosted spec covers pinned columns and rows while scrolled, a row shifted by a non-contiguous pin, and a row that is not rendered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(grid): keep every scrolling row reachable above a bottom-pinned band The previous change removed bottom-pinned rows from the canvas flow by shortening the canvas by their height. The viewport and the overlay band were unchanged, so at maximum scroll the last unpinned row (or the add-new row) sat exactly under the band and could not be reached. The canvas now keeps its full height: rows after a bottom pin are still rendered one pinned height higher, so the pinned row's slot collapses to the end of the canvas where the band covers it, and every scrolling row stays visible above it. A self-hosted spec checks a pinned last row, a pinned last row with `enableAddRow`, and a pinned middle row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(grid): mirror cell CSS class changes onto cross-band colspan fragments `updateCellCssStylesOnRenderedRows()` only touched the logical host cell, so a colspan that crosses a docking boundary kept the `selected` (or any custom) class on its continuation fragment after the host lost it. Added and removed classes are now applied to the host and to every fragment of the span. The colspan spec selects a fragment, checks both pieces carry `selected`, then selects another cell and checks both are cleared. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(grid): announce chrome cells before docking activation empties them Enabling pinning on an initialized grid (and removing it again) rebuilt the header, header-row and footer chrome through the docking region helpers, which emptied the roots before `createColumnHeaders()`/`createColumnFooter()` could fire `onBeforeHeaderCellDestroy`, `onBeforeHeaderRowCellDestroy` and `onBeforeFooterRowCellDestroy`, so plugins attached to those cells never cleaned up. The destroy events are now fired by the region set/reset helpers themselves right before they empty a root, which covers the initial build, lazy activation and deactivation from one place; the duplicated loops in the header and footer builders are removed and the header-row event now passes the cell as `node` as its type declares. A self-hosted spec counts each event per column across activation and removal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(grid): virtualize the centre cells of docked rows horizontally `cleanUpAndRenderCells()` only walked the vertical render range, so a pinned row outside it (typically a pinned last row on a long dataset) never received new centre cells when the grid scrolled right, and `cleanUpCells()` exempted every pinned row, so docked rows inside the range accumulated cells without bound. Docked rows are now appended to the rows processed against the horizontal range and cleaned like any other row; the pinned bands themselves are still always materialized. A self-hosted spec scrolls a 40-column grid with a pinned first and last row and checks that both gain the newly visible column and drop the off-screen one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(grid): forward native chrome scrolls as deltas in proxy mode With the docking horizontal scrollbar, the viewport and the header/header-row/footer containers stay at scrollLeft 0 while their content is translated by the proxy position. A native scroll on one of them (the browser revealing a focused filter, an integration scrolling `.slick-viewport`) is therefore a delta from the current position, but it was forwarded as an absolute position, jumping the grid back towards the left edge. It is now added to the proxy position, and `handleElementScroll()` no longer mirrors the reset-to-zero echo of those containers as an absolute position. A self-hosted spec scrolls the proxy to 400, scrolls the header-row container by 60 and expects 460. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(grid): map column reorder slots to the DOM band of each header On the sticky transform path an active sticky column is listed in the resolved left/right band while its header stays in the centre header region. The drop handler mapped the resolved bands onto the three Sortable arrays, so a docked sticky column produced an undefined slot and the drop threw while destructuring it. Slots are now derived from the band each header actually lives in (permanent pins left/right, everything else centre in column order), and a slot/array length mismatch leaves the order unchanged instead of throwing. A self-hosted spec docks a sticky column by scrolling, drags one centre header onto another and checks the resulting column order and the reorder event. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(examples): keep the header-menu demo command wide enough for the sub-menu alignment spec Renaming the demo command from "Freeze/Pinning" to "Pinning" made the level-0 header menu 13px narrower, which moved the drop-left level-1 menu 14px to the right. The level-2 alignment rule compares `item left + sub-menu width + item width` with the grid width; on Windows font metrics that sum went from 563 to 577 against a 575px grid, so the spec's `dropright` expectation failed there while Linux CI stayed green. The command is now "Column Pinning", the terminology used by the pinning feature, which restores the geometry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: document pinning/sticky docking for this repository and add the frozen-pane migration table `docs/pinning-sticky.md` now covers the option semantics (visible-column shorthands, index vs id references, row references), the runtime API, the docking budgets, rendering notes, the stable selectors, a migration table from the v5 frozen options/methods/classes, and the known limitations. The pinning skill and the implementation status file are rewritten to describe this flat repository only: no unit-test layer, no `Column.pinnable`, no fork-only Grid State, Header Menu or locale claims, and a verification section that lists the actual Cypress coverage. The row/column reference JSDoc states that numbers are indexes and strings are ids. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(grid): row references by id, null pinning removal, mirrored bottom band - `pinning.rows` / `stickyRows` entries accept `{ id }` so datasets with numeric ids can pin by id; the grid resolves every reference (index, string id, `{ id }`) to a row index before calling the docking controller, which now matches indexes only. - Invalidating rows marks the row docking as stale and the next `render()` re-resolves it, so a DataView sort or filter that keeps the row count moves the pins with their rows. - `setOptions({ pinning: null })` behaves like `pinning: undefined`; `stickyRows: null` clears the sticky lists instead of throwing. - The bottom band mirrors the top band: docked sticky rows sit inside (nearest the centre) and permanent rows at the edge; the inner row carries the edge class. - `docking.minCenterRowCount` budgets permanent rows only, as its JSDoc says. - `docking.stickyHysteresis` is renamed `stickyActivationBuffer` (it is an activation buffer for sticky columns, not stateful hysteresis). A self-hosted spec covers `{ id }` references across a sort, index references, null/undefined removal and the bottom band order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(grid): restore 6pac API contracts and make setColumns() report rejection Restore the public contracts that the pinning rewrite drifted away from: - applyHtmlCode(target, value, { emptyTarget, skipEmptyReassignment }) is back to the base signature and is the single HTML-assignment path (the module-level applyHtmlToElement helper is removed). - setTopPanelVisibility() and the other five panel setters take (visible, animate) again and slide the panel unless animate is false. - the internal event dispatcher is trigger() again, not triggerEvent(). - validateAndEnforceOptions() is public again. - onHeaderKeyDown publishes { event, column } (OnHeaderKeyDownEventArgs). - the pinned-width check rejects only when the pins exceed the available width, not when they exactly fill it. setColumns() now validates the prospective pinning on a copy before it mutates the caller's columns or fires onBeforeSetColumns, and returns false when the request is rejected (true otherwise). Adds cypress/e2e/quirk-grid-api-contracts.cy.ts covering each contract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(grid): drop the fork's keyboard focus routing, keep the base sinks Remove the header/grid-menu focus routing that the pinning rewrite brought over from the fork: focusHeaderRowFilter(), focusHeaderMenuOrColumn(), focusGridMenu(), focusHeaderColumn(), focusGridCell(), the container Tab handler, the viewport focus listener, the F6 shortcut and the Shift+Tab-from-first-cell hop. Shift+Tab is navigatePrev() again, the key handler is handleKeyDown() again, and focus() has no mode argument. The focus sinks are created inside the grid container with tabIndex 0, as in the base grid, instead of as tabIndex -1 siblings of the container. handleClick() no longer treats defaultPrevented as a reason to skip cell activation; only the Slick event's immediate-propagation flag does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(grid): take the docking hot paths off O(n²) lookups and layout thrash - applyDockingToColumnChrome() indexes header, header-row and footer cells once per pass instead of scanning the whole header for every column, batches every measurement before any geometry write (one forced layout instead of one per column) and moves the four placement branches into placeDockedChromeElement(). - syncDockedRowContainers() skips rows already synchronized against the same row-docking revision, band heights, scrollLeft and overlay height. - applyRowTopOffset() caches the rowspan-host flag on the row cache entry (isRowSpanHost()) instead of querying the DOM and scanning metadata on every sync. - applyDockingScrollOffsetToRow() no longer removes an absent inline transform on every horizontal scroll. - getRowDockingRegion() uses the cached cell regions and returns the row itself for undocked rows instead of a per-cell querySelector. - handleMouseWheel() reads the resolved docking flag instead of re-normalizing the pinning references on every wheel event. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(grid): one Utils.replaceList() helper for the setOptions() list replacement setOptions() replaced the sticky and pinned row/column lists with seven hand-written spread expressions. Utils.replaceList(incoming, current) now expresses the rule once: a given list is copied, an omitted one keeps a copy of the current value (or an empty list). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(grid): drop unused public methods and the duplicated top panel Remove getColumnByIdx() (no callers, returned undefined rather than null), getColumnHeaderByIndex() (an alias of getColumnByIndex()) and removeCellCssStylesBatch() (no callers). getTopPanels() returns the one top panel the single-viewport renderer has instead of the same element twice; the two examples that appended a filter panel to each side now append both to that panel. The header elements keep both `ui-state-default` and `slick-state-default` in the same order as master, and the stylesheet comments no longer claim that `slick-state-default` replaced the legacy class. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(grid): condense narrative docking comments to their intent Thirty comment blocks in slick.grid.ts told the history of a fix (the measured pixel values, the earlier approach, the proof-of-concept name) rather than what the code does. Each now states the behaviour or the constraint in one or two lines; no code changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: restore the assertions the pinning rewrite weakened Several specs were loosened rather than retargeted at the new scroll owner: - example-auto-scroll-when-dragging asserted only `lte` for the row auto-scroll (so no scrolling also passed) and no longer waited for row 16 to appear. Both are restored. - example-auto-header-height lost both container-overflow checks with pinning active; both are back, plus the case that only existed for frozen rows. - headers-width-scroll-sync no longer compared the header position with the scroll owner. It now asserts the header content is translated by exactly -scrollLeft while the header scroller itself stays at 0. - example-plugin-hybridselectionmodel is back on cy.trigger() instead of hand-built MouseEvents. - example-pinning-columns-and-rows checks one cell value per band in both the overlay and the canvas, not just cell counts. - example-pinning-columns-reorder gains the header-drag auto-scroll case that was lost with the frozen reorder spec. quirk-fractional-height-bottom-render had its precondition inverted so the quirk no longer had to reproduce. The single scroll owner genuinely removed the sub-pixel divergence it was built on, so the spec now asserts that the two limits agree and says why; the render assertions that are the actual regression guard are unchanged. cypress/support/commands.ts drops the dead multi-viewport branch from getCell/getNthCell and the unused getTransformValue helper. force: true goes from 162 to 150 uses; the ones that remain are commented where the target is covered by design (an aria-hidden colspan fragment, a docked sticky column, a stale duplicate node during docked virtualized scroll). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(examples): follow the data with pinned rows, and drop the last frozen names example-pinning-columns-and-rows hard-coded `bottom: [49999]`. Rows are pinned by index, so any filter that shortened the data left that reference past the end of the set and the bottom band silently disappeared. The example now recomputes both edges whenever the row count changes, skipping the update when the references are unchanged and suppressing the column reset so the header-row filter input is not rebuilt while someone is typing in it. Covered by a new spec case that filters the grid down to 111 rows and expects the bottom band on the last one. example-draggable-header-grouping passed `rows: { left, right }`, which the row pinning option ignores; it takes top/bottom. The two `example-quirk-frozen-row-*.html` pages were renamed to `-pinning-`: their content had already been converted, only the file names still said frozen. No frozen file names remain under examples/. Also: the index listed example-pinning-rows as "Pinned Columns & Rows" though it only pins rows, the `.slick-pane` and `.slick-pane-header` rules match nothing now that the six-pane layout is gone, and AGENTS.md said to preserve changes under dist/ when the point of the rule is that dist/ is disposable build output that must not be hand-written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(examples): keep the dev server's CSP needs out of the CSP example example-csp-header advertised a Content-Security-Policy carrying 'nonce-browser-sync' and a browser-sync Trusted Types name, and shipped example-csp-policy.js whose only job was to create that policy. Anyone copying the example's policy would have granted a nonce to a script that does not exist in their application. The example now declares the policy it actually needs. scripts/dev-watch.mjs adds the two BrowserSync tokens and injects the Trusted Types policy through a BrowserSync rewrite rule, so they exist only while the dev server is the one serving the page, and only on pages that declare a CSP. Verified by serving the page through dev-watch (tokens and policy present) and through the test server (clean production policy, spec green). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: record the row reference forms, the API restorations and the RTL gap docs/pinning-sticky.md and the pinning-sticky skill now describe the three row reference forms (index, string id and the new `{ id }` object for numeric dataset ids), note that `pinning: null` clears pinning like `undefined`, and state that index references must be recomputed when a filter changes the row count. The option table uses `stickyActivationBuffer`, the bottom band's nesting order is documented alongside the top band's, and the migration section covers `setColumns()` returning a boolean, the `OnHeaderKeyDownEventArgs` shape, the three removed methods and the unchanged `applyHtmlCode` / `trigger` / `set*Visibility` signatures. RTL is promoted from "not exercised by the RTL tests" to a measured limitation: an RTL grid builds the docking scrollbar but uses the non-proxy geometry, which places a right-pinned column outside the viewport, and the docked hit-test path is skipped for RTL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: rename the last frozen-named spec to match the example it drives example-variable-row-height-frozen.cy.ts visits example-variable-row-height-pinning.html and its suite is already named for pinned columns and rows; only the file name still said frozen. No frozen file names remain outside the migration documentation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(grid): clip a cross-band colspan instead of painting it over the next band A colspan starting in a pinned band was rendered as one host cell stretched to the full span width, given `overflow: visible` and `z-index: 21`. The host sits in the sticky pinned band, so it stayed put while the centre band scrolled and covered whatever passed beneath it: with a four-column span the Owner, Effort Driven and Region cells of that row were invisible at any non-zero scroll position, while the same cells were readable in every neighbouring row. Each piece of the span is now clipped to its own band. The host renders the part of the content that belongs to its band, and each continuation carries a presentational copy of the host's content shifted left by what the earlier bands already showed, so the text reads continuously across the boundary instead of restarting or being elided. The copy is `aria-hidden` and the host keeps the role, the value and the event wiring, so selection, navigation and formatters are unchanged. example-colspan.cy.ts asserted the old behaviour directly (the host's right edge had to lie beyond its band). It now asserts the host is clipped to the band, the continuation starts at the host's edge, its copy is aligned with the host's text, and a scrolling cell stays the topmost element under the pointer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(grid): follow a docked centre column that is resized past the viewport edge Dragging a centre column's resize handle past the right edge grew the column to about a viewport width and then stopped: the column froze, the grid never scrolled, and the handle sat outside the visible area with no way to continue. A centre column's cached coordinates are relative to the centre band, but the test that decides whether to scroll compared them against the whole scroll owner's clientWidth. The pinned bands' width therefore acted as dead room in which the column could grow past the edge without the grid following it. Since nothing scrolled, the auto-scroll interval's target never moved either, so each tick re-applied the same width and the drag stalled. The comparison now uses the visible width of the centre band. Measured on the pinning example, the scroll owner advances 0, 62, 254, 434, 590, 746 over two seconds while the column grows 80 to 993, and the column's trailing edge stays at the viewport edge throughout. Adds the resize auto-scroll case to example-pinning-columns-reorder.cy.ts; it was the one case from the deleted frozen reorder spec that could not be ported while this was broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: stop forcing clicks that the runner's own pre-click scroll was breaking The spreadsheet and colspan specs forced their clicks past the actionability check, and the spreadsheet spec's helper blamed a duplicate cell node left by docked virtualized rendering. There is no duplicate: sampling the DOM once per animation frame across the scroll never finds more than one node for the cell, and the node is the topmost element at its own centre. The runner scrolls a subject into view before clicking it. On a virtualized grid that scroll re-renders the row, detaching the element the test just resolved, and the live node that replaces it is then reported as "covering" the detached one, which is why both elements in the error looked identical. Passing scrollBehavior: false to the click, on a cell that is already in view, removes the cause instead of ignoring the symptom. The colspan fragment clicks no longer need forcing either, now that a span is clipped to its band rather than rendered on top of the next one. force: true across the suite goes from 150 to 141. The one use left in example-sticky-financial-report is genuine: a right-docked sticky column really does cover the natural cell beneath it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(grid): correct four docking geometry measurements - internalScrollColumnIntoView() measured the scroll owner's border box and then subtracted the vertical scrollbar again. In proxy mode the docking scrollbar is already sized to the inner width (measured 583 against a 598 container with a 15px gutter), so the usable width came out 15px short and the grid scrolled when it did not need to. It now reads clientWidth, which excludes the scrollbar in both the proxy and the native scroll-owner modes. - viewportHasHScroll used `canvasWidth >= viewportW - scrollbarWidth` while the docking scrollbar decides its own visibility with `contentWidth > clientWidth`. Content that exactly filled the viewport therefore had room reserved for a scrollbar the proxy never showed. Both now make the same test. - getRightDockedChromeLeft() subtracted two getBoundingClientRect() values, which are screen pixels, from terms that are layout pixels; a CSS scale on any ancestor skewed the right-pinned chrome. The measured distance is converted back to layout pixels, which is identity for an unscaled grid. - validateColspanPinningSequence() only inspected rendered rows, so a colspan that a non-sequential pinning would split went unnoticed until it scrolled into view. It now scans every row that can carry metadata, stopping at the first match. Only a non-sequential request reaches that scan, and a data provider that exposes no length still falls back to the rendered rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * perf(grid): publish the docking scroll offset once instead of per element Every horizontal scroll wrote --slick-docking-scroll-left onto both cell regions of every cached docked row, every active sticky cell, every full-width group cell and every pinned chrome element. All of those writes carried the same value, and custom properties inherit, so one write on the grid container reaches all of them. On a grid with 30 docked rows and a few pinned columns that is roughly 70 style writes per scroll event replaced by one. The property is refreshed by the proxy scroll pass and whenever the docking scrollbar is resized, so it is current before the first paint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * perf(grid): measure a chrome cell's box once per class signature The docking chrome pass called getComputedStyle() twice per column to read the padding and borders of the header-row and footer cells. Those come from the cell's classes, not from its column, so cells that look alike share one measurement; the pass now memoises it by class signature. No stylesheet rule selects a chrome cell by position, so the signature is a safe key. This is a reduction in style queries rather than a measured speedup: on a 211-column grid the pass times between 16 and 33ms across runs, which is too noisy to attribute a difference to. The pass is left running on every resize mousemove deliberately, because the pinned chrome has to track the column width while the drag is in progress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(grid): drop the colspan host fallback and let auto header height reach the docking root updateRenderedColspanFragmentGeometry() searched a row's DOM for the span host whenever the cell map had no entry for it. The map is only incomplete while a row's render queue is still pending, so the row's queue is drained first and the host is read from the map, which is what the fallback was standing in for. The auto header height rule sized .slick-header-columns-left and -right. Inside a docking chrome root both are display: contents and have no box, so the height went nowhere; it now also targets .slick-header-columns-root, which is the real element there. Plain grids are unaffected, since the left wrapper is a real box for them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(examples): survive a column-picker hide from the pre-header Porting the pre-header column-picker case from the deleted frozen spec found a crash in the example itself: renderHeaderGroups() and syncPinnedGroupHeaders() both read getComputedStyle(grid.getHeaderColumn(0)), and that header is briefly absent while the columns are rebuilt. Hiding any column from the pre-header picker therefore threw. Both now fall back to the default background. The restored case also covers what the frozen spec asserted and the pinning one did not: the picker names each column by its group, and hiding the first pinned column leaves the remaining pinned columns consistent. Because columns.left is an inclusive boundary over the visible columns, hiding the first one moves Start into the pinned band, which the case now pins down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(grid): draw a cross-band colspan as one cell at the boundary A colspan that crosses a docking boundary renders as a host plus a continuation, and both are cells, so both drew an edge where the two meet. The active-cell outline showed it in every theme: the rule that drops a shared edge matched only continuations, and the host is not one, so the host drew its trailing edge down the middle of the span. The theme's own column separator showed it wherever a theme draws one; every shipped colspan example uses alpine, which draws none. The piece whose right edge is shared with another piece of the same span no longer paints that separator, and the active outline drops the shared edge on any piece that is not the last. Both are direction-aware: the shared edge is the following piece's in a left-to-right grid and the preceding piece's in a right-to-left one. Measured on example-colspan with the stock separator restored, the host's border-right goes from 1px dotted silver to 1px dotted transparent and its active outline's trailing edge from 1px to 0, while the pieces stay where they were (host 102..202, continuation 202..402). The colour is dropped rather than the width so the geometry does not move. A click on either half already reported one cell; the continuation is cloned from the host and carries its column classes. getCellFromPoint still resolves to the column under the pointer, which is how it behaves for any colspan, pinned or not. The existing resize case asserted the seam as correct and is corrected here. Reported by @ghiscoding on #1302. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(grid): drop the pre-proxy horizontal scroll path Docking has one horizontal scroll owner: the proxy scrollbar. The methods that positioned pinned rows and chrome by writing a transform per scroll event are from the design that preceded it, and each begins by returning when the proxy scrollbar exists. It always exists when it matters. Configured docking and the proxy scrollbar are introduced and removed together at three sites: activateSingleViewportLayout() on init, the lazy activation in setColumns(), and the teardown in setOptions(). So whenever a docked row or a docked chrome element exists the guard returns, and with no docking configured the loops iterate over an empty band list and a cache with no docked rows. Measured before removing: instrumenting the three methods to count only the invocations that would do work, then calling applyDockingScrollOffsets() directly in 25 states across five examples - as loaded, scrolled, pinning added, pinning removed, pinning re-added - the invariant held in every one and the work count was zero in every one, including states with 44 docked rows and three bands. quirk-docking-scroll-owner.cy.ts now guards that invariant. applyDockingScrollOffsetToRow() goes entirely rather than keeping its proxy half. That half cleared inline transforms on the cell regions and the removed tail was their only writer; the proxy stylesheet sets those transforms with !important, so an inline value never applied. This also leaves one copy of the chrome natural/docked offset geometry, which until now was written out in both placeDockedChromeElement() and applyDockingChromeScrollOffsets(). Suite: 78 specs, 731 passing, 0 failing, 1 pending. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(grid): raise the pinning rejection alert from one place Three validation sites wrote out the same alert-once block, differing only in which callback and message they used: alert when the caller forces it or the grid has not alerted yet, latch the flag, return false. rejectPinning() does that once and returns false, so each site is a single return. The file is the same length either way - the helper costs what the three sites give back - but the rule that a validation running on every render alerts only the first time now lives in one place rather than three. Suite: 78 specs, 731 passing, 0 failing, 1 pending. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(grid): assert the docking invariant the single scroll path actually needs The spec asserted that configured docking and the proxy scrollbar are always in step. They are not: setOptions() with suppressColumnSet skips the lazy creation in setColumns(), so pinning can be configured and resolved into bands with no scrollbar. Reported in the audit on #1302 and reproduced. Nothing is rendered as docked in that state, which is the property the single scroll path relies on, so that is what the spec now asserts: without the scroll owner there are no docked rows, no pinned header classes, and no band entry resolving to a chrome element. Measured in that state: 3 bands, an empty chrome map, no docked rows, and no work for a scroll to do. Covers the suppressColumnSet state directly, including a horizontal scroll while half-configured and the recovery once setColumns() runs. The chrome lookup cache is deliberately excluded from the assertion: it still holds elements from an earlier docking after pinning is removed, which no band resolves to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(grid): drop three redundancies found by the LOC audit From the audit posted to #1302. Each was confirmed before being applied; the candidates it raised that did not hold up are not here. The rendered-row scan in applyDockingProxyScrollOffsets() wrote an inline transform onto each full-width group cell. _slick-docking.scss already applies the identical translate3d to `.slick-row-docked > .slick-cell-full-width-group` from the custom property the method publishes one line earlier. The CSS needs the cell to be a direct child of its row, which it always is: getRowDockingRegion() returns the row itself for a full-width group row, so such a cell is never placed inside a band wrapper. The rest of the method stays - the permanent-column header loop matters when a column is both pinned and sticky. normalizeColumnPinningReferences() built its visible-column list with a reduce that is character-for-character getVisibleColumnIndexes(). Its empty-list early return went with it: with no visible columns, `count` is Math.min(requested, 0), which is 0 on both sides, so the existing `count === 0` guard already returned. The pinned-edge and sticky-edge separators repeated the same five pseudo-element declarations, and the two left rules carried an identical box-shadow, as did the two right ones. Grouped, keeping left before right so cascade order is unchanged. Verified on the built CSS rather than the source: all 12 separator selectors across the three stylesheets still carry the same six declarations, every left selector keeps `inset -1px 0 0` and every right one `inset 1px 0 0`. Suite: 78 specs, 732 passing, 0 failing, 1 pending. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * perf(core): reconcile two-sided sticky columns in one pass The last candidate from the audit on #1302. A column configured sticky on both edges is a candidate for each band, and the reconciliation kept two maps, then removed the loser from whichever list with findIndex() + splice() - a scan and a shift per contested column. It now builds one map of the trailing candidates and filters the leading ones against it, deleting from the map when the leading edge wins, so the step is linear. Behaviour is unchanged, including the tie: a column equidistant from both edges still goes to the leading band. Both lists keep their order, filter() preserving the leading one and Map iteration the trailing one. Verified by comparison rather than by the suite, because sticky: 'both' is the case being changed and no shipped example configures one. The built resolver was run over 4,000 deterministic pseudo-random cases before and after - 3,827 of them containing a two-sided sticky column - comparing full band membership, every offset and width, and the revision counter, with each case resolved twice so the counter's change detection is exercised. Identical throughout. Suite: 78 specs, 731 passing, 1 failing, 1 pending; the failure is the timing test in example-0032-row-span-many-columns, which passes on its own (46/46) and exercises a grid with no pinning or sticky columns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(grid): stop the proxy scrollbar painting over the last row after a resize Reported by @ghiscoding on #1302 as a Chrome note beside the Firefox report; it reproduces in both. The proxy track is a sibling of the viewport rather than part of it, so the viewport has to give up its height when the track appears. Only resizeCanvas() recomputes that, and a column-resize drag never reaches it: updateCanvasWidth() gave the track its height and left the viewport at its old height, so the track took its strip out of the viewport's area and painted over the last data row. It did not correct on mouseup either, so the artefact stayed for the session. Driving the same transition through setColumns() was always fine, which is why nothing caught it. updateCanvasWidth() now resizes the canvas when that reservation changes, in either direction. The reservation is recorded by updateDockingHorizontalScrollerDimensions(), which already computes it, so the check costs no extra layout read on a path that runs per mousemove, and resizeCanvas() runs only on the transition. It is guarded against re-entry because resizeCanvas() reaches updateCanvasWidth() again through updateRowCount(). Measured on example-pinning-columns-and-rows, dragging a header from no horizontal overflow to well past the viewport: before mid-drag viewport h=281 track 429.2..444.2 overlapping by 15px mouseup viewport h=281 track 429.2..444.2 still overlapping after mid-drag viewport h=266 track 429.2..444.2 flush mouseup viewport h=266 track 429.2..444.2 flush narrowed viewport h=281 track h=0 Firefox 151 behaves the same with its 17px scrollbar (281 -> 264), so the fix follows the measured scrollbar rather than a constant. The new spec asserts the mid-drag state, not just the end state, and the reverse transition, so the height cannot be fixed by simply never giving it back. Suite: 79 specs, 733 passing, 0 failing, 1 pending. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(grid): reveal Firefox/Linux scrollbar beside docked rows Firefox on Linux can report a zero-width overlay scrollbar even though the visible scrollbar occupies a narrow strip. The docked-row overlay then clips to the viewport edge and paints over the scrollbar in pinned sections. Add an 8px clip inset only for Firefox/Linux when a vertical scrollbar is present and its measured width is zero. Keep the existing geometry for other browsers and for scrollbars with a measurable width. * feat(grid): support pinned and sticky columns in right-to-left grids A right-to-left grid created the docking scrollbar but took the non-proxy geometry path, and the two disagreed. Measured on a 598px viewport, the trailing pinned column rendered at -859..-779 and an activated sticky column at -179..-99, both entirely outside the viewport; a pointer position resolved to the column mirrored across the grid; and the pinned-row overlay showed a different horizontal slice from the rows beneath it. The docking geometry now works along the inline axis, which runs from the leading edge in both reading directions, and converts to physical pixels only where a style needs one: - `usesStickyColumnTransformPath()` no longer excludes RTL, so both directions share the compositor path, and `--slick-docking-direction` carries the sign to the stylesheet. - The offsets that move a band to the trailing edge change sign with the direction, as does the sticky column's own offset and the viewport width the trailing sticky transform adds. - Chrome is positioned through `setInlinePosition()`, and the trailing band's start is measured from the scroller's leading edge in either direction. - `resolveColumns()` receives the scroll position as an inline distance, since a right-to-left browser reports `scrollLeft` as a negative physical offset, and the leading band is `left` in both directions, so `sticky: true` docks at the edge the column actually reaches. - The docked-row overlay is anchored on the inline start rather than by an explicit `left`, and its clip keeps the window measured from the leading edge. This is what left the pinned row showing the wrong columns. - `getCellFromPoint()` maps x onto the inline axis, so a point resolves to the column drawn there. It also matches the centre band against the natural offsets the canvas renders, rather than the compacted ones, which was wrong in both directions whenever a sticky column was active. Adds `examples/example-pinning-rtl.html` and a spec covering the edges, the sticky transition, the pinned row's alignment and the pointer round trip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(grid): cover a cross-band colspan in RTL and pin the direction to creation The RTL example now carries a colspan that starts in the leading pinned band and continues into the scrolling one, which is the case the colspan seam fix handles in both reading directions and which nothing exercised: no shipped example had both RTL and a span crossing a boundary. The new case asserts that the two halves meet at the boundary, that the shared edge belongs to the continuation rather than the host when the grid reads right to left, and that a click on either half reports the same single cell. setOptions() now drops an rtl change that differs from the current value, and warns. The direction is applied once, by applyRTL() during initialisation, and the docking geometry reads the option on every measurement; accepting a later change left the option and the rendered direction disagreeing, with the container keeping its old class and dir attribute. Suite: 79 specs, 739 passing, 0 failing, 1 pending. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(grid): render docked rows beneath Firefox scrollbar Move the docked-row overlay inside the vertical viewport as a zero-height sticky layer. This lets Firefox paint its native scrollbar above pinned rows, so the scrollbar spans the pinned sections without clipping their right edge. Remove the Firefox-specific scrollbar-width inset and clip-path workaround. Use the viewport’s height for docked-row synchronization and bottom-row positioning, since the overlay itself no longer has height. Co-authored-by: ChatGPT <noreply@openai.com> * refactor(grid): place sticky and centre chrome through one branch Audit candidate 2 from #1302, deferred until the RTL work merged because that rewrote the method. The sticky-transform and centre branches of placeDockedChromeElement() wrote the same five things - the chrome-offset property, position, both inline edges, order and transform - and differed only in which offset they used, whether a non-header element is absolutely positioned, and whether the sticky transform follows. One branch now covers both, with those three differences as expressions. The saving is larger than the audit's estimate of 8 because the RTL rewrite made each branch's setInlinePosition() call four lines long. Verified outside the browser: both versions were run against a recording fake element across 3,744 input combinations (header or not, sticky transform or not, every band, pinned or not, transform path or not, both directions, missing offsets and widths). They take the same branch in every combination and make the same style writes and helper calls in the same order. The harness caught all three bugs deliberately planted to check that it could. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(grid): never render docked rows without their scroll owner The proxy scrollbar is what positions docked rows; nothing else does since the pre-proxy scroll path was removed. Docking can still be configured before it is applied - setOptions() with suppressColumnSet leaves the scroll owner to setColumns(), the state the ChatGPT audit on #1302 found - and in that state usesDockingRowRegions() already reported true, because it only checked that docking was configured. So the next render built docked rows with nothing to position them. Measured on example1-simple after setOptions({ pinning: { columns: { left: 1 } } }, false, true): 0 docked rows, then 42 after a vertical scroll rendered new rows, with no scrollbar in the grid. The earlier invariant spec missed it because it never re-rendered rows in that state. usesDockingRowRegions() now also requires the scroll owner, so rows stay plain until setColumns() applies the docking. One property read, so the per-cell cost that made the flag a cached value is unchanged; the scroller is created before the first render at initialisation, so initial rows still dock. The spec now scrolls vertically in that state. Without this change it fails with 42 docked rows against 0; with it, 4 of 4 on three runs. This also narrows a claim made for the removal of the pre-proxy scroll path: that proof measured the half-configured state before any re-render, when no rows were docked. After a re-render they were, and the removed code would have positioned them. With this change no docked row can exist without its owner, so there is nothing for it to position. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(grid): remove the sticky reparenting renderer updateRenderedCellDocking() and the fallback in enqueueStickyColumnLayout() that called it re-homed rendered cells between row regions when a sticky column crossed an edge without the transform path. The ChatGPT audit on #1302 deferred their removal because RTL was routed through that fallback; since the RTL work merged, usesStickyColumnTransformPath() is simply "the proxy scroller exists", so RTL takes the transform path like everything else. What still reached the fallback was a sticky column crossing an edge with no scroll owner: sticky set on the column definitions without setColumns(), or the suppressColumnSet state. It was worse than unused there. Measured on example1-simple with a sticky column and no scroller, a horizontal scroll past the column ran it once; the reparent found no docked rows and returned false, so the fallback's full re-render built 39 docked rows with nothing to position them. setColumns() recovered them afterwards. The sticky layout now returns before resolving anything when there is no scroll owner. It has to return before refreshDockingLayout(), not after, because that call switches row regions on. Docking is then applied by setColumns(), as it already is for pinning in the same state. Measured again after the change: 0 docked rows until setColumns(), then 38. The helpers the removed code called all have other callers. A new case in the invariant spec crosses a sticky column over the edge before setColumns() has run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(grid): assert the sticky docked-row overlay, not the clipped one 4c0f178c moved the docked-row overlay inside the viewport as a zero-height sticky layer, so Firefox paints its scrollbar above the pinned rows. Two specs still described the overlay it replaced and failed in CI on that commit. example-auto-header-height expected the overlay to be as tall as the viewport and to carry an inset() clip-path - exactly the two properties the redesign removes on purpose. It now asserts what the new design guarantees: the overlay is a sticky child of the viewport, aligned with its top and leading edge, takes no layout height, needs no clip-path, and relies on the viewport's hidden horizontal overflow; the first pinned row starts at the top of the viewport. example-auto-scroll-when-dragging looked for .slick-group:visible after the auto-scroll. The pinned group row is drawn inside the viewport (y 83..108 in a viewport of 83..384) but Cypress's own visibility check, which replaces jQuery's :visible, reports anything inside a zero-height container as hidden. It now compares each group row's box with the viewport's box. Worth knowing for later specs: Cypress will treat pinned-row content as hidden from now on, so be.visible and :visible on it need a geometric check instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(grid): keep docked sticky cells opaque on unstriped rows Reported by @ghiscoding on #1302 as an RTL bug: past the first sticky column, the text of the columns scrolling beneath it showed through, until the row was hovered. It is not specific to right-to-left grids. A docked sticky cell floats over the cells scrolling beneath it and takes its background from its row, through the row's docking region - `background: inherit` at every step. The alpine theme stripes only odd rows, so an even row had no colour of its own: measured on example-pinning-rtl with two cells beneath each sticky cell, the sticky cell, its region and its row were all rgba(0, 0, 0, 0) on even rows and rgb(251, 251, 251) on odd ones. Hover gives every row a colour, which is why hovering hid the problem. The LTR sticky example masks it with its own sticky-cell colour; its even rows are just as transparent underneath. Docked rows now have an opaque default background, with the fallback chain the pinned rows already use. It is set on the row rather than on the scrolling region on purpose: an opaque region would paint over the row's own colour, and with it the theme's striping and hover, the highlight animation, the active-row state and any row class. As a default on the row, all of those still set the row's own background and win, and the sticky cell inherits whatever the row ends up with. The new case in example-pinning-rtl fails without this change (row 4: rgba(0, 0, 0, 0)) and passes with it. example-sticky-financial-report and the pinned-rows examples still pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(grid): mask scrollbar strip in docked headers Prevent transformed header content from appearing above the vertical scrollbar when pinning or sticky columns are enabled. The grid now measures the body scrollbar width and overlays that strip across the affected header containers. Added regression coverage and documentation. * fix(grid): align sticky columns during resize Synchronize body-cell geometry and sticky transforms while resizing columns so header and cell positions remain aligned without requiring a scroll event. Preserve Grid Menu scrollbar-cover behavior and add regression coverage for sticky column resizing. * fix(grid): keep the grid menu button above the header scrollbar cover The cover from baabece4 hides the strip above the vertical scrollbar at z-index 30. The grid menu button sits in that same corner on purpose, at z-index 9, so in example-pinning-columns-and-column-group it was painted under the cover: still clickable, because the cover lets clicks through, but invisible. Docked grids now lift the button to z-index 31. The new spec checks what is painted, not only the cover's styles. The cover lets clicks through and elementFromPoint skips anything that does, so the spec makes pseudo-elements hit-testable while it samples. It sweeps the whole strip at 21 scroll positions: left to right with sticky columns, left to right with the header row, right to left, and with the classic stylesheet, where the header is positioned and the cover anchors to the header instead of the header root. It also reports a cover that is see-through. The four sweeps pass on baabece4, and fail without a cover (sep, oct, nov, dec) or with a transparent one. The grid menu case fails on baabece4 and passes with this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(grid): draw pinning separators on the inner edge in right-to-left grids Reported by @ghiscoding on #1302: in a right-to-left grid the separator of a pinned band was on the wrong side of its edge column, so the band looked one column off. Every docking separator is an inset box-shadow with a physical offset: -1px draws it on the right edge and +1px on the left, which is the inner edge only when reading left to right. The offsets are now multiplied by --slick-docking-direction, which the grid already sets to 1 or -1, so the separator faces the scrolling columns in both directions. Left to right, the computed values do not change. A docked sticky column becomes the band edge, and its header cell gets slick-column-sticky-left-edge or -right-edge. No rule styled those classes, so the header lost its separator when a sticky column docked, in both directions. They now share the header rules of the pinned edges. The new case in example-pinning-rtl.cy.ts checks that every separator faces the scrolling columns, before and after the sticky column docks. It fails on 7d9555c8 and passes with this change. example-sticky-financial-report.cy.ts also checks the header separator at the initial viewport. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: add few missing example links * chore(tests): remove invalid test * docs: replace the frozen-pane pages with pinning and sticky docking Merging next-v6 brought in the VitePress site, which still documents the 3x3 frozen band model from #1238. That PR was closed without merging, and this one removes frozen panes, so: - docs/in-depth/frozen.md is replaced by docs/in-depth/pinning-sticky.md, the page this branch kept at the docs root. It now has the site's front matter, a v6 note, the complete list of examples and links into the reference. The sidebar, the In-depth index, the home page and four other pages that linked to or described frozen panes now point to it or describe pinning. - The migration guide describes the move from frozen panes to pinning. Its API list for this PR is the actual delta against next-v6: 11 options, 2 column properties, 4 methods and 5 events added, and 10 options and 4 methods removed. validateColumnFreeze() from #1299 is removed again, so that entry is updated too. - The option, event and column-property counts on the introduction pages follow the new API (120, 62 and 52). Two corrections to the page itself: permanent pins are rejected only when they are wider than the whole grid, because the column budget limits sticky columns only; and getColumnByIdx(), getColumnHeaderByIndex() and removeCellCssStylesBatch() are no longer listed as removed, because no 6pac release had them. The Docs workflow runs a strict drift gate on every push to next-v6, and it would have failed on this merge: five new options had no JSDoc and five new events had no reference note. The options now have JSDoc, and the events have overlays, as the other 57 events do. The overlay for the removed option throwWhenFrozenNotAllViewable is deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(grid): keep the pinned band's header separator when a sticky column docks In the body, the last permanently pinned cell always draws the band's separator, and a docked sticky cell draws its own further in. The header instead moved its one separator from the pinned column to the sticky column, so with a sticky column docked next to permanent pins the body showed two lines and the header one. The header's pinned edge is now the last permanently pinned column of each band, as in the body. It no longer changes while scrolling, so updateStickyColumnTransforms() no longer needs the loop that moved it. The new case in example-pinning-rtl.cy.ts checks that every header separator lines up with a body separator, before and after the sticky column docks. It fails without this change: 2 header separators against 3 in the body. The far-right test in example-sticky-financial-report.cy.ts asserted the state from before the scroll. Q3, Q4 and YTD are sticky at scroll 0, and cy.scrollTo() may not deliver a scroll event before the assertions run. The test now checks the far-right state, which is false before the scroll: Q3, Q4 and YTD are released, and Q2 is the edge of the left band, with its separator in the header and in the body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(grid): keep a pinned column in its band whatever its sticky flag says A permanently pinned column that also carried `sticky` was routed onto the sticky transform path by every site that read the column flag alone, while the docking layout kept it in its permanent band. Its header stayed docked only through the per-scroll inline transform, its filter cell was placed at the sticky offset, and its body cells rendered in the scrolling region and scrolled away. One predicate, isStickyTransformColumn(), now decides the path for cells, chrome, the region sync and the sticky transforms: a pinned column is never a sticky candidate. The new spec fails on the previous commit for the boundary form, column-level pins and setColumnPinning() alike. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(grid): remove dead scroll-time transform writes and a quadratic hidden-column scan Follows the audit posted on #1302. applyDockingProxyScrollOffsets() wrote an inline transform to every permanently pinned header, filter and footer cell on each horizontal scroll; the proxy-mode stylesheet rules already set those transforms from --slick-docking-scroll-left with !important, so the writes had no effect. scrollToX() now publishes the variable alone. updateColumnPositionCaches() rescanned a hidden block once per hidden column (25 million reads of `hidden` for 5,000 columns, 197 ms on every step of a column-resize drag); each block is resolved once. The class-keyed measurement cache in applyDockingToColumnChrome() never hit, because filter and footer cells carry per-column l/r classes, and a working one saved nothing measurable, so it is gone. paneBottomH and viewportBottomH were frozen-pane remnants with no readers. appendColspanFragments() passes the row's cached regions. The column-group examples skip their group-header sync when scrollLeft is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(grid): align the pinning option contracts with Slickgrid-Universal Follows the comparison ghiscoding posted on #1302. Pins whose widths add up to the available width are rejected, since they leave the centre band no width. A rejected setColumns() no longer records the request's columns in the saved pinning state, where a flag carried only by the rejected request could later come back as a column's original pin. setColumnPinning() records every reference as a column id, so the pins follow their columns through a reorder and the changed column can be unpinned afterwards. syncDockingChromeRegions() appends chrome elements only when their order differs from the sorted one. The scrollLeft clamp, two early-return guards and one statement order are aligned so the methods read the same in both projects. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(grid): re-place docked chrome on resize and apply rtl before the first layout resizeCanvas() re-applied the docked chrome geometry only when the docking layout changed, so after the container changed width the right-pinned header, filter and footer cells stayed at the old edge while the body moved. It now re-applies it on every resize, as Universal does. With implicit initialisation the headers were built before applyRTL() set the container's own direction, so an rtl grid on a left-to-right page laid its trailing band out at the wrong edge; the direction is applied first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(grid): measure docked chrome against the body cells in the contracts spec The resize and rtl cases compared the docked chrome with the viewport's outer box, which includes the vertical scrollbar, so a correctly placed band read as one scrollbar width off on Linux. The cases now compare the chrome with the band's body cells and only check that the band itself lies within the scrollbar of the viewport edge. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 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> * style(grid): simplify pinned docking rules Ported from Slickgrid-Universal f5dbce68: - share z-index on the combined pinned-left/right cells rule - drop the .slick-docking-chrome-right transform override; the grid sets that class under the same condition as slick-column-pinned-right with a zero chrome offset, so the generic pinned rule already applies the same transform - merge two identical rules in the financial report example Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(grid): skip the docked geometry refresh when only the row count changed updateCanvasWidth() refreshes column widths, chrome and row bands whenever the grid has docked columns, and updateRowCount() calls it on every row-count change, for example on each filter keystroke. updateRowCount() passes an explicit false; that now means "no refresh unless a canvas width changed". Calls without an argument, as during a column-resize drag, still refresh a docked grid. Proposed by the audit on #1302; measured here at 0.7 ms (Firefox) and 2.1 ms (Chrome) per row-count update on a pinned grid, with identical docked geometry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(grid): cache unresolved pinned and sticky row references resolveDockingRowIndex() cached only the references it could resolve, so a pinned or sticky row id that is not in the data was searched for again on every vertical scroll: a full findIndex() over a plain array, or a getRowById() call on a DataView. An unresolved reference is now cached as undefined until the existing invalidation paths clear the cache, as in Slickgrid-Universal 8b6d8d1. The spec counts the searches during 60 scroll steps (120 before, none now) and checks that the pins resolve once their rows exist and the rows are invalidated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: 6pac-ai <6pac@dharpa.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: ChatGPT <noreply@openai.com>



Summary
Completes the 3×3 band model. Rebased onto current master (includes the #1229 squash and the #1231/#1232 fixes — the scrollTo paging fix verified intact alongside the band code); supersedes the stacked #1236. Contains the full ViewportMgr series: the #1234 commits replay here identically, so #1234 can be closed in favour of this PR or merged first — either order works. SlickGrid now supports left AND right frozen columns and top AND bottom frozen rows in any combination — up to nine panes — all managed by
ViewportMgr, with every legacy configuration byte-identical (proven by the phase 0–3 characterization suites, which run unchanged).New capabilities (all opt-in via new options; defaults preserve current behaviour exactly)
frozenRightColumn(count from the right edge — counts survive column reorder/hide, unlike index-basedfrozenColumn): a right-frozen column band with new*-right-frozencss classes. The historical "right" elements keep their names and become the scrollable middle band. Column-side selection, geometry (band pinned at the right edge, middle band shrinks), row-fragment routing (rowNodegrows to 3 entries), band-local cell coordinates, keyboard navigation across the boundary, and Y-scroll following are all implemented and spec-pinned.frozenBottomRow(count, usable TOGETHER withfrozenRow, which then always means top rows; the legacyfrozenBottomflag keeps its exact historical single-band meaning): a bottom-frozen row band (*-bottom-frozenclasses) with three-way row routing, geometry (band pinned below the body, above its scrollbar), X-scroll following, and runtime toggling. The shared bottom-frozen × right-frozen corner pane is created by whichever band materializes second.getFrozenBandCounts()/getFrozenRightStartIndex();cellrangeselectoris migrated onto it, which also fixes its cross-canvas measurement under the new modes and adds correct offsets/clamps for the new bands.ViewportMgrextracted toslick.core.ts(registered on the Slick namespace besideDragExtendHandle; contracts insrc/models/viewportMgr.interface.ts) —slick.grid.tsends ~1,500 lines lighter than before the whole refactor, with zero frozen-flag branching outsidesetFrozenOptions. No consumer impact: core already loads before the grid everywhere.Hardening
Three adversarial equivalence audits ran during Phase 4 (right-band transitions, bottom-band-off equivalence, init-window binding). Real defects found and fixed by them: stale-band routing/geometry guards after un-freezing, an init-window event double-binding (latent since the right-band milestone), and a dropped
disableSelectionon init-materialized headers. Known preserved quirks and invariants are documented in the source at their definitions.Testing
Three new example pages and 25 new band tests across three specs (
viewportmgr-right-frozen-band,viewportmgr-bottom-frozen-band, extendedviewportmgr-lazy-materialization), covering init-time and runtime materialization, geometry, routing, navigation, and un-freeze restoration. Every commit in the series landed only after the full Cypress suite passed locally (632 tests). Headless-onlyretries: { runMode: 1 }added to the Cypress config after reproducing a ~1-in-4 machine-level flake on pristine master (three different victim specs across runs; CI here is the authoritative check).🤖 Generated with Claude Code