Skip to content

feat(grid)!: replace frozen panes with pinning and sticky docking - #1302

Open
ghiscoding wants to merge 21 commits into
next-v6from
feat/pinning-sticky
Open

ghiscoding wants to merge 21 commits into
next-v6from
feat/pinning-sticky

Conversation

@ghiscoding

@ghiscoding ghiscoding commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

supersede #1238
fixes #410
fixes #443
fixes #739
fixes #1219

Summary

Introduce a single-viewport docking architecture for permanent pinned columns/rows and
scroll-activated sticky columns/rows.

This is an intentional v6 breaking change. The previous multi-pane frozen implementation has
been removed from the runtime and replaced with one virtualized body viewport, one vertical
scroll owner, one horizontal scroll owner, and stable per-row left/center/right cell regions.

Why

The legacy frozen-pane implementation required multiple synchronized panes and scroll
containers. This increased complexity around scrolling, resizing, virtualization, editing,
grouping, and framework integrations.

The new architecture provides a simpler and more predictable model:

  • one live viewport and canvas;
  • one horizontal scroll owner;
  • one native vertical scrollbar;
  • stable left/center/right regions within each rendered row;
  • independent, non-contiguous column and row pinning;
  • shared resolution logic for permanent pinning and scroll-activated sticky docking.

Unlike the previous freeze-until-column/row behavior, users can now pin individual columns or
rows independently. For example, columns 0 and 2 can be pinned while column 1 remains in the
center region.

Changes

  • Added canonical GridOption.pinning support for:
    • columns.left / columns.right;
    • rows.top / rows.bottom.
  • Added explicit per-column Column.pinned and CurrentColumn.pinning state support.
  • Added Column.sticky and GridOption.stickyRows for scroll-activated docking.
  • Added the shared internal DockingController for permanent and sticky column/row resolution.
  • Added viewport-based sticky-row budgets, variable-height support, and conveyor/clamp
    overflow strategies.
  • Added stable left/center/right DOM regions for:
    • body rows;
    • headers;
    • header rows;
    • footers;
    • pre-header/grouped header content.
  • Added permanent right-column and bottom-row pinning.
  • Added support for non-contiguous pinned columns and rows.
  • Added cross-band colspan/rowspan rendering with one logical host cell and visual continuation
    fragments.
  • Preserved virtualization, editing, selection, grouping, resizing, auto-sizing, RTL behavior,
    and framework integrations.
  • Added sticky financial-report demonstrations:
    • example-sticky-financial-report.html
  • Updated Example pinning to demonstrate permanent left/right column and top/bottom row pinning.
  • Added Column.pinnable support for controlling Header Menu pinning commands.
  • Added Grid State/Preset serialization for the nested pinning shape.
  • Kept sticky configuration option-based because active sticky membership is scroll-dependent and
    is intentionally not serialized.
  • Added stable .slick-horizontal-scroller and .slick-vertical-scroller selectors.
  • Removed the legacy frozen options, interfaces, state fields, pane runtime branches, synchronized
    scroll branches, redundant viewport/canvas aliases, and old pane CSS classes.
  • Removed the legacy -1000px header coordinate workaround and HEADER_WIDTH_SLACK.
  • Updated the v6 migration guide and pinning/sticky documentation.
  • Added the repository pinning-sticky skill as implementation and documentation guidance.

Breaking changes

  • The old frozen-pane configuration and APIs are removed.

  • The canonical configuration is now:

    {
      pinning: {
        columns: { left, right },
        rows: { top, bottom }
      }
    }
  • Legacy flat pinning options and temporary aliases are no longer supported.

  • Sticky state is not serialized because it changes with scrolling.

  • The old multi-pane DOM structure and pane selectors are no longer available.

  • Column reordering remains within each docking band; moving a column between pinned and center
    bands is an explicit pinning operation.

  • Legacy names and theme variables are retained only as migration documentation references.

References

Ag-Grid Column Pinning was used as key concept reference for the idea of a single horizontal scroller and single vertical scroller, also for its declaration of left/center/right cell docking regions

Validation

The following checks pass:

  • Common package TypeScript validation.
  • Vanilla demo type-check.
  • Focused common pinning, docking, grouping, accessibility, span, and interaction tests.
  • Changed-range coverage for the updated SlickGrid implementation.
  • Oxlint.
  • Prettier.
  • Sass compilation for affected themes.
  • git diff --check.
  • User-confirmed Vanilla Cypress CI workflows, including
    pinning/sticky, resizing, reordering, RTL, variable row heights, editing, selection,
    grouping, spans, and framework parity.

The accessibility audit found no pinning/sticky-specific semantic-tree or keyboard-navigation
regressions. Automated axe/WCAG integration and manual screen-reader validation are not included
in this PR.

Implementation status

The single-viewport rewrite and legacy runtime cleanup are complete. This is no longer a POC
that runs alongside the old frozen-pane implementation.

The approximate library-only production diff is:

  • +3,989 / -1,550;
  • approximately +2,439 net LOC relative to the base commit.

These figures exclude demos, tests, generated output, and framework-wrapper changes.

Follow-up work

The following items are intentionally separate from the v6 implementation:

  • optional manual UX trials for sticky transitions and held-scroll performance;
  • a separate investigation into fast vertical-scroll blanking;
  • grouped sticky header bands, such as quarterly group headers;
  • framework-specific migration guides if required for the release.

None of these requires restoring the legacy pane architecture or changing the current pinning/sticky
runtime design.

AI / LLM assistance

  • AI / LLM assistance used:
    • No
    • Yes
  • If Yes:
    • which tool/model: OpenAI Codex 5.6 Sol and Luna
    • how was it used: Architecture analysis, implementation, refactoring, debugging, demo and
      documentation updates, test maintenance, and validation support.

Checklist

  • The changes are limited to the pinning/sticky docking rewrite and required demos,
    documentation, tests, and cleanup.
  • Tests were added or updated where appropriate.
  • Documentation was updated where appropriate.
  • Legacy frozen-pane runtime behavior and compatibility branches were removed.

Print Screens

image image image

@ghiscoding

ghiscoding commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

@6pac I think you should close your previous PR #1238 since this is the new approach that includes Pinning and Sticky. Please note that I would ask if you can ask Claude to audit and verify the entire PR to detect any possible problem, there's a progress file written by AI and read by AI to keep it focused, you should tell Claude to read that file .agents/plans/pinning-sticky-progress.md so that it understand the PR and you should also tell it that the original PR was ghiscoding/slickgrid-universal#2782

Side note, with the code now you can at least start testing it out (including the new example-sticky-financial-report.html)

Also important, the +/- 1000px that we carried from the original SlickGrid lib is officially gone in this PR, I'm pretty sure that it was in place to support legacy IE browser back in the day but there's no reason to keep such old code and approach that caused alignment issues when implementing this PR and so I told the AI to remove it all, which is a lot easier to read the DOM now

Comment thread src/slick.core.ts
@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

OK, Claude Fable is done with the evaluation. There's a lot of it!

@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Evaluation of 6pac/SlickGrid PR #1302 — "feat(grid)!: replace frozen panes with pinning and sticky docking"

PR #1302 (head feat/pinning-sticky @ 8faa2f0e, base next-v6 @ 66e842ae)
Origin Port of ghiscoding/slickgrid-universal#2782 (461 files, +29,399/−16,859) into the flat 6pac repo (81 files, +10,281/−5,589)
Author's guide .agents/plans/pinning-sticky-progress.md (1,138 lines, written for the multi-package fork) and .agents/skills/pinning-sticky/SKILL.md
Evaluated 2026-09-17/18 by Claude, read-only. No repository file was modified; the PR build used for testing was produced with npm run build:prod.
Evidence Screenshots referenced below are in the accompanying PR-1302-evidence folder (PR build vs the published base at 6pac.github.io).

1. Verdict

Not mergeable as it stands. The architecture is sound and the big-ticket claims (one viewport, one row per data row with left/centre/right regions, one horizontal scroll owner, HEADER_WIDTH_SLACK gone, a DOM-free resolver) are true. The build, type-check, lint and GitHub CI are green. But the port carries a set of concrete regressions and correctness bugs that the test suite does not exercise, several of the PR description's claims are not true for this repository, and three renamed Cypress specs pass tautologically. The list below is ordered by what should be fixed before merge.

Top issues (details in §4):

  1. pinning.columns.left: N pins one column too many whenever column ids are numeric, because index references are also matched against column.id. Reproduced: the spreadsheet example (left: 3) pins five columns where the base frozenColumn: 3 pinned four, and the new Cypress spec asserts the wrong number.
  2. Every autoHeight: true grid, pinned or not, now renders an empty band (about one header height) below its last row. Reproduced on the plain autoHeight example against the published base.
  3. Vertical mouse-wheel scrolling now moves exactly one row per notch on every grid, because the handler always calls preventDefault() (base only did so for frozen grids).
  4. Ctrl/Meta+drag multi-selection with HybridSelectionModel({ enableMultiSelection: true }) no longer works; the grid now reads a slickgrid-universal-only selectionOptions grid option instead of the selection model's option. The example was patched to add that option so its spec stays green.
  5. The LongText editor (appended to document.body) opens about one grid-offset away from its cell because absBox()/getActiveCellPosition()/getGridPosition() now return container-relative instead of document-relative coordinates while the editors were not changed. Reproduced live on the editing example against the published base.
  6. Row references resolve inconsistently: the controller matches numeric references by data id or index, the id-to-index cache is never invalidated on a count-preserving DataView sort/filter, and a custom DataView idProperty is ignored. Pinned/sticky rows can dock the wrong row.
  7. Cell hit-testing (getCellFromPoint) is not docking-aware, so CellRangeSelector drag selection resolves the wrong cells over pinned columns/rows; wheel events over docked rows scroll the page, not the grid.
  8. Legacy frozenColumn/frozenRow/frozenBottom options are still declared with live JSDoc and silently ignored; the Grid Menu still branches on frozenColumn and can throw. No migration guide exists in this repo although the PR says one was updated.
  9. Three quirk-pinning-* specs are byte-identical renames that still configure the removed frozenRow/frozenBottom and therefore test nothing; other specs had assertions weakened in ways that lock in behaviour changes (notably auto-scroll direction while dragging).
  10. A local Cypress run on Windows: 703 passing, 1 failing. The header-menu sub-menu alignment test fails on Windows in both Electron and Chrome while the base version of the spec passes in the same environment; CI on Linux is green, so it is a platform-dependent geometry shift introduced by the PR (§3).

Nothing found requires abandoning the design. Most items are local fixes; the largest are the row-reference model (§4.1 group B) and the hit-testing/wheel routing for docked content (group D).

2. What was verified and how

Check Result
git diff --check base…PR Clean (one "new blank line at EOF" in docking.interface.ts)
tsc --noEmit (after npm ci) Exit 0
eslint . (whole repo, as CI's prebuild:prod) Exit 0
node scripts/builds.mjs --prod Exit 0, bundles, CSS and dist/types fresh. Produces a new, empty dist/browser/docking.controller.js (76 bytes, (() => {})();) — see §4.2 M5
GitHub CI on the PR "Node 24" job green (5m14s), conventional-commit green
Local Cypress, full suite (Windows, Electron) 67 specs, 703 passing, 1 failing, 1 pending. The failure (example-plugin-headermenu.cy.ts) reproduces in Electron (3/3) and in Chrome (1/1); the base version of the same spec passes 12/12 in the same environment against the published base build — see §3
Live browser check of editor positioning (PR build vs published base) PR misplaces the LongText editor by the grid's page offset — see C8
Headless Chrome screenshots of 19 example pages (PR build) plus 5 base pages from 6pac.github.io Used for the visual comparisons in §4; images in the evidence folder
Five parallel read-only code reviews by area (controller/types; DOM/chrome/scroll/styles; rows/cells/virtualization; API/options/interaction/plugins; tests/examples/docs/claims) Findings de-duplicated and, where marked Confirmed, re-verified in source or in the browser. Items marked Reasoned were derived from the code by a reviewer and not executed

Not done: no unit tests exist in this repository (the tests/ folder is legacy manual HTML benchmarks), so the "51 pinning tests / 100 % coverage" in the progress file could not be run here; no Firefox/Safari/RTL browser sessions; no screen-reader check.

3. Cypress results (local run, Windows)

Specs 67
Passing 703
Failing 1
Pending 1 (example-auto-scroll-when-dragging "MAX interval", skipped in base too)

The one failure is example-plugin-headermenu.cy.ts › "should open Pinning sub-menu and expect 2 options, then open Feedback->ContactUs sub-menus…": Expected to find element: .slick-header-menu.slick-menu-level-2.dropright, but never found it — the level-2 sub-menu opens to the left. Facts:

  • Fails deterministically on Windows in Electron (3/3) and in Chrome (1/1); GitHub CI (Ubuntu, Chrome) passes.
  • The PR changed only the labels in this spec and this example; src/plugins/slick.headermenu.ts is untouched.
  • The base spec (origin/next-v6 version) run with the same Cypress version in the same environment against the published base build (6pac.github.io, identical example and plugin) passes 12/12. So the flip is introduced by the PR and is platform-dependent.
  • The plugin decides dropleft when parentOffset.left + subMenuWidth + parentItemWidth >= getGridPosition().width. For this example that sum is within a few pixels of the 600px grid width, so a small change in header/menu geometry (the PR rewrote header layout CSS, renamed ui-state-default, and getGridPosition() now returns getBoundingClientRect().width) is enough to flip it under Windows font metrics. Whether users see a wrong alignment depends on their layout; the author should reproduce on Windows and either fix the geometry shift or make the threshold viewport-based.

Otherwise the suite is green locally, which matches CI. Note that green CI does not cover findings 1–7 above: the spreadsheet spec asserts the wrong pinned count, no spec drags a range over pinned cells, no spec uses non-contiguous row pins, pinning.rows + stickyRows together, numeric-id datasets with sorting, or a custom idProperty, and the wheel spec dispatches a synthetic cancelable event that states "1 notch === 1 row".

4. Findings

Severity: Blocker = wrong behaviour for ordinary configurations or silent regression for existing users; High = wrong behaviour for documented pinning/sticky configurations; Medium = correctness edge cases, performance, API hygiene; Low/Nit = polish. Status: Confirmed (re-verified in source or browser), Observed (seen in the browser), Reasoned (reviewer, from code only).

4.1 Blockers and High

A. Column pinning references

A1. Blocker — Numeric index references are also matched against column.id, so left: N over-pins when ids are numeric. Confirmed (DOM dump + base comparison).
src/slick.grid.ts:10094-10096 (applyColumnPinningOptions): const isLeftPinned = leftRefs.has(index) || leftRefs.has(column.id); while getPinnedColumnIndexes (10119-10130) and validation treat numbers as indexes only. normalizeColumnPinningReferences (10266-10281) expands left: 3 to indexes [0,1,2,3]; a column whose id is 3 (index 4) is pinned as well.
Evidence: examples/example-pinning-columns-and-rows-spreadsheet.html has pinning.columns.left: 3 with columns selector, 0, 1, 2, …. The rendered left header region contains five columns (selector,0,1,2,3); the base example with frozenColumn: 3 pinned four. cypress/e2e/example-pinning-columns-and-rows-spreadsheet.cy.ts:129-147 asserts leftHeaderIds.size === 5 with the comment "currently exposes five left-pinned header IDs in the rendered bundle", i.e. the spec encodes the bug. Screenshots: spreadsheet-PR-left3-pins-5-columns.png vs spreadsheet-BASE-frozenColumn3-pins-4-columns.png.
Also: no bounds check (!this.columns[-1]?.hidden is true), so negative/out-of-range numbers land in the pinned map; width validation is bypassed for the id-matched column.
Fix: one resolver for both paths; numeric array entries are indexes only, with Number.isInteger(n) && 0 <= n < columns.length; if ids must be addressable, use { id } objects. Then correct the spec to 4.

A2. High — Numeric shorthands count hidden columns; left is an inclusive boundary but right is a count. Confirmed.
normalizeColumnPinningReferences works on raw this.columns; right: 1 with the last column hidden: true pins nothing, silently. The progress file says the boundary expands to "the first three final visible columns". docking.interface.ts:14-25 documents the asymmetric semantics. Fix: normalise against visible columns, or document exactly.

B. Row references (pinned and sticky rows)

B1. High — DockingController.resolveRows matches every set by row.id or row.index, while the grid resolves a numeric reference as an index only. Confirmed in src/slick.core.ts:1660-1675 and src/slick.grid.ts:10546-10553.
With the default { id: i } datasets, after a descending sort the row at index N−1 has id 0; pinning.rows = { top: [0], bottom: [N-1] } puts both rows in the top band (topIds.has(row.id) short-circuits first). Same for stickyRows.*. Fix: resolve everything to indexes in the grid and match on row.index only.

B2. High — The id→index cache is never invalidated on a count-preserving DataView sort/filter. Confirmed in src/slick.grid.ts:10546-10577: cleared only when refreshRowDockingLayout(…, rebuildReferences=true) is called (init, setOptions, updateRowCount). The canonical wiring onRowsChanged → invalidateRows + render never reaches updateRowCount, so pinning.rows.bottom: ['net-profit'] keeps docking the pre-sort index. Fix: clear the map in invalidateRows/invalidateAllRows/setData, or simply re-resolve through getRowById each pass (O(1) with a DataView).

B3. High — Custom DataView idProperty is ignored. Reasoned (src/slick.grid.ts:10531-10541). getRowIdentity uses this._options.datasetIdPropertyName || 'id' (a universal-only option) instead of DataView.getIdPropertyName(), so with dataView.setItems(items, 'code') the DockingRow.id passed to the controller is undefined and top: ['ABC'] never matches. Fix: prefer this.data.getIdPropertyName?.().

B4. High — Bottom-pinned rows keep their natural slot in the canvas. Reasoned (getRenderedRowTop 10749-10754 shifts only for top pins; updateRowCount 6378-6383; scrollTo 7001-7008). With enableAddRow: true the add-new row's slot is hidden behind the bottom band; a non-trailing bottom: [5] leaves a blank gap at row 5 and hides the real last row. The example works around it: examples/example-pinning-columns-and-rows.html:243-245 "Keep the add-new row disabled so it cannot appear as an empty row below the bottom pin". Fix: treat bottom pins symmetrically to top pins, or reject/warn for enableAddRow + bottom pins and non-trailing bottom pins.

B5. High — Non-contiguous top pins break hit-testing and active-cell tracking. Reasoned. Unpinned rows render at natural + S(row) (height of permanent top pins with index ≥ row), but setActiveCellInternal (4017-4022, non-docked branch), getCellFromPoint (8373-8375) and scrollRowIntoView (7509-7532, uses the constant topHeight) map with natural coordinates. With top: [0, 2, 4], clicking row 1 sets activeRow = 3; editors/keys act on the wrong item. No example or spec uses non-contiguous pins. Fix: read rowNode.dataset.row for every row in setActiveCellInternal; give getCellFromPoint the inverse of getRenderedRowTop.

B6. High — Sticky-row thresholds ignore the permanent top band and subtract the bottom band twice. Confirmed in src/slick.core.ts:1671-1696: visibleBottom = scrollTop + max(0, viewportHeight − topHeight − bottomHeight), top test row.top < scrollTop (no + topHeight), and let stickyBottomHeight = bottomHeight on top of the already-reduced visibleBottom. With pinning.rows.top: [0,1] + stickyRows.top: [5], row 5 slides under the permanent band for two row-heights before docking; the bottom mirror docks early and leaves a blank gap. No example combines permanent and sticky rows.

B7. High — conveyor overflow keeps the wrong end for right columns and bottom rows. Confirmed in src/slick.core.ts:1749-1764: applyBudget ignores its _edge parameter and always reverses; for bottom/right the newest candidate is the first element. stickyRows.bottom: [r10, r20, r30] with a 60px budget keeps r20/r30 and drops the row the user is about to reach.

B8. Medium — stickyHysteresis is a fixed activation offset, not hysteresis (slick.core.ts:1527,1548,1559; no per-item previous state; rows use none). Document or implement.

C. Regressions for grids that do not use pinning at all

C1. Blocker — Every autoHeight grid gets an empty band below its rows. Observed + root cause confirmed.
resizeCanvas (src/slick.grid.ts:6253-6260) now unconditionally sets the container height to paneTopH + _headerScrollerL.offsetHeight + vbox + preHeader, where paneTopH was derived from viewportH, and in autoHeight mode getViewportHeight (6143-6157) already folds _headerRoot.offsetHeight, pre-header, header-row and footer into viewportH. Header and pre-header are therefore counted twice, and _contentRoot gets the inflated height. The base only set the container height for frozen autoHeight grids and left plain ones to size naturally.
Evidence: examples/example11-autoheight.html (no pinning) rendered on the PR build ends 32px lower than the published base with identical data, and the extra space is an empty strip between "Task 99" and the horizontal scrollbar (autoheight-plain-PR-bottom.png vs autoheight-plain-BASE-bottom.png). The pinned autoheight example shows ~40px (grid 1) and ~90px (grid 2, with pre-header) bands (autoheight-pinned-PR.png vs autoheight-frozen-BASE.png).

C2. Blocker — Vertical wheel now scrolls one row per notch on every grid. Confirmed by diff. handleMouseWheel (src/slick.grid.ts:4559-4581) always calls e.preventDefault() when the scroll was handled; the base (4446-4468) did so only when hasFrozenColumns(), so ordinary grids received the native ~100px/notch scroll plus the handler's nudge. Now the handler is the only motion source: deltaY * rowHeight (25px per notch by default). enableMouseWheelScrollHandler defaults to true, so all grids are affected; a 500k-row grid needs ~4× more notches on Windows/Chrome. The horizontal path was converted to native pixel deltas; the vertical path was not. The only wheel spec dispatches a synthetic event and asserts "1 notch === 1 row".

C3. Blocker — Ctrl/Meta+drag multi-selection regressed. Confirmed by diff. Base createDraggable() (1000-1017) read getSelectionModel()?.getOptions()?.enableMultiSelection === true and setSelectionModel() re-created the Draggable. PR (1146-1158) reads this._options.selectionOptions?.enableMultiSelection !== undefined (a slickgrid-universal option, typed any) once at init. Existing users of HybridSelectionModel({ enableMultiSelection: true }) lose Ctrl+drag. The PR patched examples/example-plugin-hybridselectionmodel.html:304 to add selectionOptions: { enableMultiSelection: true } so its spec stays green. Also !== undefined strips the modifier keys for enableMultiSelection: false.

C4. High — Undocumented rename ui-state-defaultslick-state-default. Confirmed (15 occurrences in base slick.grid.ts, 1 in PR; 11 slick-state-default). Consumer CSS/JS keyed on .slick-header.ui-state-default etc. stops matching; examples still add the old class themselves (example-column-group.html:73, example-draggable-header-grouping.html:185, example-pivot.html:218). Not in the PR's breaking list.

C5. High — destroy(true) is a silent no-op. Confirmed: src/slick.grid.ts:150 const destroyAllElementProps = (_target: object) => undefined; replaces base destroyAllElements() that nulled ~40 DOM fields. Same pattern for other universal helpers stubbed rather than ported: copyCellToClipboard = () => undefined (dead Ctrl+C branch at 11356-11365), type FormattedDataCachePlanner = any, type TrustedHTML = string.

C6. High — Plain grids pay O(columns) per rendered cell in appendRowHtml. Reasoned (5632, 5657/5661usesDockingRowRegions()hasConfiguredColumnDocking()this.columns.some(...) plus rowNode.querySelector(':scope > .slick-scrolling-cells') per cell). O(N²) per row for a 100-column grid with nothing pinned; base had none of this. Fix: evaluate once per render pass and use the cached cellRegions.

C7. Medium — Keyboard/focus contract changes not listed as breaking. Reasoned. Focus sinks moved outside the container with tabIndex: -1 (851-857, 1022-1023; base tabIndex: 0 inside the container), so getContainerNode().contains(document.activeElement) is false while the grid has focus; Shift+Tab at (0,0) now goes to header-row filters/grid menu instead of navigatePrev(); F6 focuses the header; onClick now also aborts on e.defaultPrevented (4672), so link-cell handlers that call preventDefault() suppress cell activation.

C8. Blocker — absBox() now returns container-relative coordinates; the LongText editor (and any custom editor/plugin positioned from args.position or getActiveCellPosition()) is misplaced. Confirmed live.
Base absBox (src/slick.grid.ts base 8497-8530) walked offsetParents and returned document coordinates. PR absBox (8497-8530) returns rect − containerRect, so getActiveCellPosition(), getGridPosition() (now always top: 0, left: 0) and the position/gridPosition passed to editors in makeActiveCellEditable (4210-4211) are relative to the grid container. src/slick.editors.ts is unchanged: LongTextEditor appends its wrapper to document.body with position: absolute and sets top/left from args.position (734-758, 832-835).
Evidence (live, examples/example3-editing.html, "Description" cell of row 3, grid container at page offset (8, 112)): PR build sets the editor to top: 112px; left: 79px (= container-relative cell position 117/81 minus the editor's 5/2 px inset), i.e. ~118px above and 10px left of the cell, after which the browser scrolls the page to the focused textarea. The published base sets top: 225px; left: 86px for the same cell at document position (89, 231) — correct. Any grid that is not at the page origin is affected; the composite-editor path is not (it appends inside the cell). CustomTooltip, RowDetailView and third-party editors that use these positions are at the same risk. The four menus that read getGridPosition().width still work because they only use the width.
Fix: keep the old document-relative contract for absBox/getActiveCellPosition/getGridPosition (or add the container offset back), or make the editors container-aware and document the change as breaking. Add a spec asserting editor placement on a grid with a non-zero page offset.

D. Interaction with docked content

D1. High — getCellFromPoint is not docking-aware; CellRangeSelector drag selection is wrong over pinned cells. Reasoned by three reviewers independently (src/slick.grid.ts:8373-8391; src/plugins/slick.cellrangeselector.ts:168-178, 333-336, 393-396, where the PR deleted the old frozen offset compensation). Pinned-left regions are counter-translated by +scrollLeft, right regions sit at the viewport edge, pinned rows live in the overlay outside the canvas, but the function walks natural widths and canvas row positions. Scroll right in the spreadsheet example and drag from a pinned cell: the range starts in a centre column. No pinning spec performs a drag selection.

D2. High — Wheel over a pinned/sticky row scrolls the page. Reasoned. MouseWheel is bound to the viewport only (1095-1103); the overlay is a sibling of the viewport (9995-10001) and bindDockingOverlayEvents (10004-10020) binds no wheel handler.

D3. High — Column reorder throws when a sticky column is docked (LTR proxy path). Reasoned. onEnd (2219-2236) maps dockingLayout[band] entries (which include active sticky entries) onto the band Sortable's toArray() (which keeps transform-path stickies in the centre band), leaving finalColumns[i] = undefined and then destructuring it.

D4. High — Forwarded chrome scrollLeft is treated as absolute but is a delta in proxy mode. Reasoned (forwardDockingHorizontalScroll 11043-11060). Header/header-row/footer containers are kept at scrollLeft = 0 and translated; when the browser auto-scrolls one of them (e.g. focusHeaderRowFilter focusing an off-screen filter on Shift+Tab), the forwarder assigns that small value as the absolute proxy position and the grid jumps to the left.

D5. Medium — Docked rows outside the vertical rendered range never receive new centre cells on horizontal scroll, and in-range docked rows are never cell-cleaned. Reasoned (render 6886-6897, cleanUpAndRenderCells iterates range.top..bottom only; cleanUpCells returns for pinned rows). Visible with a far bottom pin and > 2 viewport widths of columns.

D6. Medium — setColumns() can silently reject after mutating the input and firing onBeforeSetColumns, and validates the old column array. Reasoned (3697-3711; validateColumnPinning(undefined, true) defaults to this.columns). Grid Menu / Column Picker hide-column flows see a before-event with no after-event.

D7. Medium — Pinning cannot be switched off at runtime; the proxy scroller and chrome regions are created lazily but never removed. Confirmed by reading 1352-1380, 9880-9900, 9918-9923. setOptions({ pinning: undefined }) is skipped by the deep-extend; pinning: {} keeps prior edges; after one pin→unpin cycle the grid stays in proxy mode with overflow-x: hidden on .slick-viewport, which the PR's own comment (9890-9893) says breaks integrations that scroll the viewport directly.

D8. Medium — Lazy docking activation empties header/header-row/footer without firing the onBefore*CellDestroy events (updateColumnsInternal 3735-3741createDockingChromeRegionsUtils.emptyElement). HeaderMenu/HeaderButtons/CustomTooltip cleanup leaks for that transition.

D9. Medium — Cross-band colspan fragments freeze the host's selected/custom CSS classes at clone time (10990-10992; updateCellCssStylesOnRenderedRows touches only the host).

E. Legacy surface and claims

E1. High — Legacy frozen options remain declared with live JSDoc; the Grid Menu still branches on them. Confirmed. src/models/gridOption.interface.ts:276-289, 446-476 still declare frozenBottom, frozenColumn, frozenRow, frozenRightViewportMinWidth, skipFreezeColumnValidation, throwWhenFrozenNotAllViewable, invalidColumnFreeze* (no @deprecated); slick.grid.ts reads none of them (base had 311 "frozen" hits, PR has one comment). frozenColumn: 2 type-checks and silently does nothing. src/controls/slick.gridmenu.ts:179-187, 212-217 still compares frozenColumn in onSetOptions and, when the option is present, queries .slick-header-right (no longer emitted) and dereferences .style on null. Fix: delete the options (or @deprecated + one-time console.warn), remove the Grid Menu branches.

E2. High — PR description and progress file claim things that do not exist in this repository. Confirmed by grep/diff.

  • Header Menu "Column Pinning" sub-menu (pin-left, pin-right, bulk, unpin-*), headerMenu.showPinningCommands, and Column.pinnable gating: src/plugins/slick.headermenu.ts is unchanged; pinnable has zero readers in src/. The only "Pin Columns" in the tree is the header-menu example's custom command whose handler calls alert(); its spec asserts that alert. SKILL.md tells consumers pinnable "only controls whether built-in pinning commands are exposed" — false here.
  • Grid State / Presets (GridState.pinning, CurrentColumn.pinning, GridService.setPinning(), Example 11 persistence), locale strings, getColumnsInRenderedOrder(): absent (no such modules in 6pac).
  • "Updated the v11 migration guide and pinning/sticky documentation": docs/ is two stub files; no migration text anywhere; CHANGELOG.md untouched.
  • "51 / 454 / 71 focused unit tests, 100 % / 99.97 % coverage", "Example 04 … 42/46 tests": no unit runner exists; the Example 04 equivalent has 6 it().
  • "Removed … old pane CSS classes": .slick-pane/.slick-pane-header rules remain in slick.grid.scss:264-273 and slick-alpine-theme.scss:601-611 (dead).
  • --slick-pinned-* "theme variables": only var(--slick-pinned-…, fallback) reads in _slick-docking.scss; no theme defines them.
  • "--slick-docking-scroll-left registered as non-inheriting": no @property/registerProperty anywhere in src/.
  • src/docking.controller.ts "shared docking resolver": it is a 5-line re-export; the class lives in slick.core.ts, and it is public (ESM via index.ts, IIFE Slick.DockingController, global.d.ts) although SKILL.md says it must not be.
    The progress file's "Repository adaptation note" relabels paths but does not retract these; its "Suggested resume prompt" will make the next agent act on them.

E3. High — Test integrity. Confirmed by diff.

  • cypress/e2e/quirk-pinning-row-zero.cy.ts, quirk-pinning-bottom-hit-testing.cy.ts, quirk-pinning-bottom-cell-cleanup.cy.ts are R100 renames (zero content change) still configuring frozenRow/frozenBottom; e.g. row-zero asserts "rows render in the top canvas, none in the bottom" against a .grid-canvas-bottom that never exists, and cell-cleanup asserts getOptions().frozenBottom === true, which merely echoes the option. Bottom-pinned hit-testing and cleanup therefore have no coverage while three green specs remain. (quirk-pinning-row-boundary.cy.ts was ported properly.)
  • example-auto-scroll-when-dragging.cy.ts:207-300: scrollTop/scrollLeft equallte/lessThan; the "dragging up auto-scrolls up" case changed from greaterThan to equal (no upward auto-scroll with top-pinned rows is now the expected result); getIntervalUntilRow16Displayed no longer waits for the row. Commit 8faa2f0e "chore: fix cypress failures" is one real cellrangeselector fix (offsetWidth − scrollbarclientWidth/clientHeight, 11 lines) plus 46 lines of spec edits and a drag.ts fallback that affects every cy.drag().
  • Weakened elsewhere: example-auto-header-height.cy.ts dropped both scrollHeight <= clientHeight + 1 overflow checks; headers-width-scroll-sync.cy.ts no longer asserts header/body scrollLeft equality; quirk-fractional-height-bottom-render.cy.ts inverted its precondition (> 0.01< 1), so the quirk need not reproduce; dom-shape-characterization.cy.ts loosened assertions the base said not to loosen; example-plugin-hybridselectionmodel.cy.ts swapped Cypress trigger() for native MouseEvent to keep passing (suggests the new selector needs absolute coordinates).
  • Helpers: getNthCell changed from nth-child to .l{n}.r{n} semantics (cause of the (0,0)→(0,2) edits); a dead legacy branch and an unused getTransformValue were added; force: true count rose 142 → 159.
  • Coverage dropped vs the five deleted frozen specs: pre-header column-picker case, both reorder auto-scroll cases, nearly all per-band cell value assertions (now counts/ids). Deleted 41 it(), added 29 + 11 sticky.

4.2 Medium

M1. Performance on the per-scroll path. Reasoned by two reviewers (consistent with each other):

  • Proxy-mode horizontal scroll: applyDockingScrollOffsetToRow (9511-9537) reads row.offsetWidth and writes two inline transforms per cached row per scroll event; the stylesheet's !important translate3d(var(--slick-docking-scroll-left)) (_slick-docking.scss:221-231) overrides the inline transforms, so the writes are dead and the read forces a layout per row (read/write interleave). Contradicts the progress file's "no per-row writes during horizontal scrolling" for every column-pinned grid.
  • Vertical scroll with any row docking: refreshRowDockingLayout (10574-10620) calls ensureDockingOverlay()bindDockingOverlayEvents() (unbind + 6 fresh listeners) and syncDockedRowContainers() (per cached row: querySelector('.slick-cell.rowspan'), metadata lookup, ~8 DOM writes) on every event, even when the revision is unchanged. The progress file itself lists this as pending.
  • Column resize: updateCanvasWidth runs applyDockingToColumnChrome (9616-9775: O(n²) querySelectorAll(...).find, getBoundingClientRect + getComputedStyle interleaved with width writes) on every mousemove.

M2. pinning shape/merge issues. setOptions cannot remove pinning (see D7); mixinDefaults: true with a partial docking object leaves minCenterRowCount undefined for grid-side readers (806-812, 10835); enforceMinCenterRowBudget counts sticky rows although the doc says permanent-only and runs only on resize (10831-10846).

M3. Public API drift not listed as breaking. Reasoned/confirmed by call-site diff:

  • applyHtmlCode(target, value, skipEmptyReassignment = false) replaced the (target, val, { emptyTarget, skipEmptyReassignment }) overload; JSDoc still documents the object.
  • sanitizeHtmlString lost suppressLogging; logSanitizedHtml option is now dead; non-strings are coerced.
  • animate parameter removed from all set*Visibility methods; trigger() renamed to triggerEvent() and made public; validateAndEnforceOptions became protected; setColumns(cols, waitNextCycle), focus(mode) additive.
  • onHeaderKeyDown is typed OnKeyDownEventArgs ({ row, cell }) but notified with { event, column, grid } (285, 1870).
  • Removed: getFrozenColumnId, getFrozenRowOffset, validateColumnFreeze, validateColumnFreezeWidth (intended; no in-repo callers). Base's throwWhenFrozenNotAllViewable throw path has no replacement. Width validation changed from > to >= (10178-10188).
  • New public: getPinnedColumns, setColumnPinning, setColumnStickiness, validateColumnPinning, focusGridCell/Menu/HeaderColumn/HeaderMenuOrColumn/HeaderRowFilter, getColumnByIdx (unused, returns undefined not null), getColumnHeaderByIndex, removeCellCssStylesBatch; new events onHeaderMouseOver/Out, onHeaderRowMouseOver/Out; onContextMenu args gained { row, cell }.

M4. slickgrid-universal leakage into public types. Confirmed in the model diff. GridOption: allowDragFromClosest, enableGridMenu, enableRowDetailView, enableFormattedDataCache, enableExcelCopyBuffer, silenceWarnings, selectionOptions: any, datasetIdPropertyName, rowDetailView: any, columnResizingDelay, autoScrollResizeLeftDelay/RightDelay (never read); CustomDataView.setFormattedDataCachePlanner/getCellDisplayValue (this repo's DataView implements neither, so the whole formatted-cache planner path 320-353, 3872-3878, 10712-10727 is dead); Column.editorClass, exportCustomFormatter, exportWithFormatter, pinnable (dead); ColumnMetadata & { editorClass?: any }; EditorArguments.isCompositeEditor; rowDetailView?.renderMode === 'inline' branch (1554-1561) for a renderMode this repo's plugin does not have; gridHeight used in the sticky example is not a GridOption here. Undocumented, mostly untyped. Fix: remove the dead ones, type or drop the rest, and split the genuinely useful unrelated options (allowDragFromClosest, columnResizingDelay) into their own change with JSDoc.

M5. Dead file that ships as an empty bundle. Confirmed. src/docking.controller.ts (5-line re-export, referenced by nothing) is picked up by scripts/builds.mjs's per-file IIFE build and, because non-entry imports are stubbed, emits dist/browser/docking.controller.js containing only (() => {})();. Delete the file.

M6. Docked-row overlay artifact with zero-width scrollbars. Observed only in headless Chrome with scrollbars hidden (which is what overlay-scrollbar platforms such as macOS report): the last digit of each docked sticky row's rightmost cell is painted a second time, offset down-right, in the strip between the overlay clip and the grid border (sticky-report-ghost-digits-hidden-scrollbars.png). With classic Windows scrollbars the artifact is absent (sticky-report-PR.png). Cause not isolated; the metric-based "8px trailing strip" fallback (updateDockingOverlayClip) is the likely area. Needs a macOS/overlay-scrollbar check.

M7. Small controller/geometry issues. cancelScheduledAnimationFrame calls clearTimeout with a rAF id (11117-11122, separate id spaces); internalScrollColumnIntoView subtracts the vertical scrollbar twice in proxy mode (7281-7306); viewportHasHScroll and the proxy's overflow decision use different criteria (5159 vs 10883-10889); getRightDockedChromeLeft mixes getBoundingClientRect screen pixels with layout pixels (9829-9862), off under a scaled ancestor; validateColspanPinningSequence inspects only rendered rows (10210-10240); RTL passes the raw negative scrollLeft to resolveColumns (10496-10504) and example-rtl.cy.ts has no pinning/sticky assertions (unverified risk); bottom band stacks sticky rows below permanent rows while the top band stacks them inside (asymmetric, possibly intentional).

M8. Examples and docs. example-pinning-columns-and-rows.html:252-255 hard-codes bottom: [49999] on a page with a DataView filter and pager, so the pin silently disappears after filtering; example-draggable-header-grouping.html:488,498 uses rows: { left: [], right: [] }, not a valid PinnedRows shape; examples/index.html:219 labels example-pinning-rows.html as "Pinned Columns & Rows"; example-quirk-frozen-row-*.html keep "DO NOT MERGE" banners and frozen names (bodies ported); example-csp-policy.js/example-csp-header.html now carry a BrowserSync trusted-types allowance for the dev server; AGENTS.md says never modify dist/ "including when running builds", which contradicts npm run build:prod, CI and scripts/release.mjs; SKILL.md directs maintainers to unit tests under tests/ that do not exist; _slick-docking.scss is @used by slick.grid.scss and both themes, so a page loading grid CSS plus a theme gets the docking rules twice. The PR does not commit dist/, so the examples on the branch show the old frozen-pane build until npm run build:prod is run; worth a line in the PR text.

4.3 Low / Nits

  • src/global.d.ts:20 duplicate import type … from './slick.core.js'.
  • column.interface.ts:193 sticky JSDoc never says true = leading edge; docking.interface.ts:81 mentions hysteresis for "sticky item" though rows use none.
  • Progress file "Current APIs" omits docking.minCenterRowCount.
  • getRowIdentity falls back to the index for id-less items, which can collide with numeric ids in the row signature (10533-10544, slick.core.ts:1740).
  • Column revision ignores width/offset changes (slick.core.ts:1618-1622); document as membership-only.
  • Compat classes slick-viewport-top slick-viewport-left / grid-canvas-top grid-canvas-left are still emitted (976, 990) while -right/-bottom are gone; quirk-pinning-row-boundary.cy.ts still says "frozen-row boundary" in its title/describe.
  • slick.grid.scss:300-305 / alpine 615-620 .slick-header-auto-height .slick-header-columns-right {height; overflow} now targets a display: contents wrapper (ignored).
  • _handleScroll assigns _viewportScrollContainerY.scrollTop twice (7187-7191); updateRowPositions(dockedOnly) parameter has no caller; renderRows calls ensureDockingOverlay() per docked row; isPinnedRowIdx(i) || (band !== 'center') at 5885-5888 is the same predicate twice.
  • Array-backed grids with string id references rescan the whole array on every updateRowCount (10562-10577).
  • dev-watch.mjs now binds BrowserSync to 127.0.0.1 by default (BROWSERSYNC_HOST to override) — behaviour change for LAN/device testing, otherwise the script changes are sound and fix a real await subscribe bug.

5. Verified sound

  • Single live viewport/canvas; renderRows appends one row node per data row; row regions and chrome regions (display: contents) match the described DOM; HEADER_WIDTH_SLACK and the ±1000px pair are fully gone; .l{i}/.r{i} rules exist for all columns.
  • DockingController wiring across ESM/CJS/IIFE and global.d.ts; defaults equal DEFAULT_DOCKING_OPTIONS; setOptions replaces stickyRows and pinning.columns/rows arrays atomically; options pushed into the controller before every resolve.
  • Column band membership (null/hidden skipped, pinned beats sticky, two-sided candidates pick the nearer edge, left activation against the occupied sticky edge, right stickies iterated farthest-first); budgets deduct permanent sizes first; oversized candidates skipped; degenerate inputs (0 columns, empty data, NaN percents, zero viewport) do not throw; stateless resolver handles large scroll jumps; revision counters bump only on membership change.
  • Row cache vs overlay reparenting (same node moved with appendChild; rowsCache fields stay valid; rows moved back before the overlay is removed); no double rendering; fragments excluded from logical-cell caches, cleaned with their host, aria-hidden/role=presentation; clicks on fragments activate the host; updateRow/updateCell on docked rows; editor positioning on overlay rows via absBox; getCellNodeBox handles top/bottom bands; getRowFromNode uses closest('.slick-row').
  • Top-pin layout math (contiguous and non-contiguous, uniform and variable heights) lays unpinned rows contiguously; variable-row-height (RowPositionIndexer) integration; group rows render one viewport-wide cell.
  • destroy() tears down timers/rAF, Draggable/MouseWheel/Resizable, three Sortables, document capture listener, overlay listener group, focus sinks, <style>, proxy scroller and overlay; no Resize/MutationObserver anywhere. Repeated docking toggles do not accumulate listeners (D8 excepted).
  • scrollToX updates canvas, overlay, header, header-row, footer, pre-/top-header transforms synchronously, so no frame-level header/body desync; overlay clip maths correct for LTR; resizeCanvas reserves the proxy height only on real overflow; classic (non-overlay) scrollbars handled (proxy width = clientWidth).
  • Every public getter used by src/plugins/* and src/controls/* still exists with compatible semantics; no plugin/control depends on .slick-pane*, .slick-viewport-right, .grid-canvas-right, getCanvases().length > 1, getViewports(), getFrozenColumnId; getSelectionModel/sanitizeHtmlString still exist (generic signatures); slick.draggablegrouping.ts creates Sortables only for existing bands and destroys all three; slick.cellrangeselector.ts viewport dimensions and scroll tracking are sound apart from D1.
  • Navigation (goto*, navigateToPos) works in raw index space, skips hidden columns, guards pinned rows; scrollCellIntoView scrolls a sticky candidate to its natural position; invalidColumnPinning* defaults are alert(error) like the old freeze callbacks.
  • Event argument shapes for all pre-existing events unchanged (call-site diff); no dist/ committed; every href/src in the changed examples and every index.html link resolves; all spec selectors exist in the example markup; package.json/CHANGELOG.md untouched; scripts/builds.mjs change adds esbuild error detail only.

6. Recommended actions before merge

  1. Fix column reference resolution (A1, A2) and correct the spreadsheet spec to the intended count.
  2. Make row references index-only inside the controller, invalidate the id cache on data changes, and honour DataView.getIdPropertyName() (B1–B3); fix the sticky-row band thresholds and conveyor direction (B6, B7); decide bottom-pin flow semantics and non-contiguous hit-testing (B4, B5) or reject those configurations explicitly.
  3. Restore base behaviour for grids without pinning: autoHeight container sizing (C1), native vertical wheel (C2), selection-model-driven multi-select (C3), document-relative absBox/editor positions (C8), and reproduce the header-menu alignment flip on Windows (§3); either keep both ui-state-default and slick-state-default for a major or list the rename (C4); port destroyAllElements (C5); hoist the per-cell docking checks (C6).
  4. Make hit-testing and wheel routing docking-aware (D1, D2), fix reorder with docked stickies (D3) and delta forwarding (D4), make setColumns validate the incoming array and signal rejection (D6), allow pinning removal with symmetric teardown (D7).
  5. Remove the legacy frozen* option declarations (or deprecate with a runtime warning) and the Grid Menu branches (E1); rewrite the PR description, progress file and SKILL.md to what exists in this repo, and add a migration note for the removed options/methods/classes (E2).
  6. Port the three tautological quirk specs to pinning.rows.bottom, restore the weakened assertions where the old behaviour is still intended, and add specs for: drag selection over pinned cells, numeric-id sort with row pins, pinning.rows + stickyRows, non-contiguous pins, enableAddRow + bottom pin, native wheel delta, autoHeight height equality with the pre-PR value (E3).
  7. Remove the universal leakage and dead file (M4, M5); address the per-scroll layout thrash (M1); check the docked-row overlay on an overlay-scrollbar platform (M6).

7. Reproducing the confirmed findings

All steps use the repository's own scripts on a clean checkout of the PR branch (npm ci, then npm run build:prod); the base comparisons use the published examples at https://6pac.github.io/SlickGrid/examples/.

  • A1 — open examples/example-pinning-columns-and-rows-spreadsheet.html and count the headers inside .slick-header-columns-left (five: selector, 0, 1, 2, 3); compare with example-frozen-columns-and-rows-spreadsheet.html on the published site (four).
  • C1 — open examples/example11-autoheight.html (no pinning) and measure the grid's bottom edge against the published example11-autoheight.html with the same window size; the PR grid is one header-height taller with an empty strip above the horizontal scrollbar. example-pinning-columns-autoheight.html vs the published example-frozen-columns-autoheight.html shows the same with a larger band when a pre-header is present.
  • C2 — compare handleMouseWheel in src/slick.grid.ts between next-v6 and the PR: preventDefault() is now unconditional; wheel over any grid moves rowHeight px per notch.
  • C3git diff next-v6...feat/pinning-sticky -- examples/example-plugin-hybridselectionmodel.html shows the added selectionOptions: { enableMultiSelection: true }; remove it and Ctrl+drag range selection stops working.
  • C8 — on examples/example3-editing.html run in the console: grid.setActiveCell(3, 1); grid.editActiveCell(); then read document.querySelector('.slick-large-editor-text').style.top/left and compare with grid.getActiveCellNode().getBoundingClientRect() plus window.scrollY/X; on the PR build the editor is offset by the grid container's page position, on the published base it sits on the cell.
  • §3 header-menu alignment — run cypress/e2e/example-plugin-headermenu.cy.ts on Windows (Electron or Chrome) against the PR build; then run the next-v6 version of the spec against the published site with --config baseUrl=https://6pac.github.io/SlickGrid.

@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Hang on a minute, there's quite a bit of stuff in there that's specific to my computer and its environment. I'm just gonna remove that and repost.

@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

OK the evaluation has been updated

@ghiscoding

Copy link
Copy Markdown
Collaborator Author

wow that is a lot.... providing this to Codex, and we'll see what it's able to fix. Just curious, do you also have access to Fable 5.1? Seems like an improvement, probably more expensive though

Side note I also fixed colspan just now which can now spread on both side of the column pinning and also updated data Grouping which also spreads its grouping title (see above).

@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Yep, this review was done with Fable 5.1. It did take up about 35% of my weekly quota though! Which is fine, I usually don't use more than about 30% of it anyway.

Comment thread src/slick.grid.ts Outdated
const queueMicrotaskPolyfill = (callback: () => void) => typeof queueMicrotask === 'function' ? queueMicrotask(callback) : setTimeout(callback, 0);
const destroyAllElementProps = (_target: object) => undefined;
const destroyAllElementProps = (target: object): void => {
const elementProperties = [

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really sure why it added all of these, this seems very overkill. Shouldn't it be able to destroy and remove whatever it needs without us having to name all functions? I assume it came from Claude report

@ghiscoding

ghiscoding commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

@6pac ok the AI is done with the audit report, the remaining things it said was basically verifying the UI myself... can you do a final audit to make sure it fixed everything. Also, can you ask it to see if it there's any areas to decrease LOC (I usually ask the AI if it's the most minimalist it can do without regressing). I'm especially concerned about the comment I left just above, I don't understand the point of listing all function names to loop and and destroy (this seems ridiculous and not minimalist to do this way). If there's anything else, I'd prefer you let it fix the rest... having a different AI model to double-check is actually a very good exercise, this will be used for the next few years, so better be good :)

ahh wait, last commit caused a few test failures, let me fix them

image

@ghiscoding

Copy link
Copy Markdown
Collaborator Author

@6pac ok I'm done and fixed Cypress failures, so would you mind addressing what I wrote above

@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

In the middle of a very busy workday, but I'll point Claude at it and see how it goes. The destroyAllElementProps issue looks like a reversion rather than something Claude suggested (it found an issue with that function, but that's not the suggestion it made), from what I can see. I'll query it.

@ghiscoding

Copy link
Copy Markdown
Collaborator Author

ah yeah it might have been my old code actually, but I think we can remove that or lighten it at least. Thanks.

There's no rush on it, need to make sure that we cover all angles and that the UI/UX works for you :)
Cheers

@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Here's the eval. Let me know if you're happy to go ahead and I can get Fable to do the final commits.

@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Re-evaluation of 6pac/SlickGrid PR #1302 after the audit-fix commits

PR #1302, head now 71e2acf0 (was 8faa2f0e at the first evaluation), base next-v6 @ 66e842ae
New commits 0b58e873 colspan/grouping spread over pinning; 04066292, 69749673, 82af2240, 3f018a22 "part 1–4 fixes of audit review"; 8851bb91, 5611dd2e, 71e2acf0 "fix cypress failing tests" — 31 files, +1,030/−409 (src: +526/−266)
Previous report PR-1302-Evaluation.md (finding ids A1…M8 below refer to it)
Evaluated 2026-09-18, read-only. Static checks, build, full Cypress suite and live browser probes re-run on the new head; every previously confirmed defect re-tested the same way it was first demonstrated
Evidence PR-1302-evidence\v2\

1. Summary

The fix commits are substantive, not cosmetic. Of the eight blocker/high items that were demonstrated live in the first report, seven are fixed and verified on the new build (column over-pinning, autoHeight band, wheel behaviour, Ctrl+drag selection, editor placement, row-reference matching and cache invalidation, docked-cell hit-testing). The frozen option surface, the dead re-export file, the three tautological quirk specs and the missing documentation are dealt with. Two items were "fixed" in a way that needs another pass (bottom-pinned rows, destroyAllElementProps), one platform-specific test failure is still open, and roughly a third of the medium items were not touched. Details in §3 and §4.

Still blocking, in my view:

  1. Bottom-pinned rows make the last scrollable row unreachable. The fix removes bottom-pinned rows from the canvas flow but does not extend the scroll range, so at maximum scroll the bottom band covers the last unpinned row (and the add-new row when enabled). Reproduced live; see §3 B4.
  2. destroyAllElementProps was restored as a 46-entry hard-coded name list. It works, but it is the maintenance liability the original author avoided by stubbing it; a reflective 12-line version does the same job without a list to keep in sync. Proposal in §4.1.
  3. The Windows-only header-menu sub-menu alignment failure from the first report is unchanged (§6).

Worth doing before merge but not blocking: the remaining universal-fork leakage, the unrequested keyboard/focus feature carried in from slickgrid-universal, and the LOC/comment reductions in §5.

2. What was re-run

Check Result on 71e2acf0
tsc --noEmit, eslint ., node scripts/builds.mjs --prod All exit 0; bundles fresh; dist/browser/docking.controller.js no longer produced (file deleted)
Full Cypress suite (one spec per server, Electron, Windows) 68 specs (one new: example11-autoheight), 710 passing, 1 failing, 1 pending. The failure is the same header-menu sub-menu alignment test as before (§6)
Headless-Chrome captures vs the first report's images Plain autoHeight grid ends at exactly the base height (last painted row at y=2604 in both; the old head was 2636); pinned autoheight example shows no band; sticky financial report with hidden scrollbars shows no ghost digits
DOM dump of the spreadsheet example Left header region now holds 4 columns (selector,0,1,2); the base pinned 4
Live browser probes (local build on localhost) Editor placement, docked-cell getCellFromPoint, wheel defaultPrevented on plain vs pinned grids — see §3
Throwaway Cypress probe spec (not committed; copy and raw payloads in PR-1302-evidence\v2\) Non-contiguous pins: getCellFromPoint correct for rows 1/3/5 and scrollRowIntoView(20) lands the row fully inside the viewport. Bottom pin: last scrollable row hidden under the band (two configurations). Colspan over a pinned boundary: host paints over scrolled centre cells. Details in §3 B4/B5 and §4.2

3. Status of the first report's findings

Legend: Fixed (verified) = re-demonstrated on the new build; Fixed (code) = the diff addresses it, not executed; Partial; Open.

Column references

Id Status Notes
A1 numeric id / index collision Fixed (verified) normalizeColumnPinningReferences now returns indexes only; string references resolve by String(column.id); bounds-checked. Spreadsheet spec corrected to 4 and a new hidden-column case added.
A2 shorthands count hidden columns Fixed (code) Boundary/count are resolved over visible columns; JSDoc updated.

Row references

Id Status Notes
B1 id-or-index matching Fixed (code) matchesRowReference matches row.index or a string id. Consequence: numeric dataset ids can no longer be referenced at all — acceptable, but docs/pinning-sticky.md still says "indexes first, then data-view IDs"; it should say numeric references are always indexes.
B2 stale id→index cache Fixed (code) Cleared in invalidateRows and invalidateAllRows. setData() does not clear it directly but goes through invalidateAllRows in practice.
B3 custom idProperty Fixed (code) getDataViewIdProperty() prefers DataView.getIdPropertyName().
B4 bottom-pinned rows keep their slot Partial — new defect getRenderedRowTop now subtracts bottom-pinned heights for later rows and updateRowCount shortens the canvas by the same amount, so the natural gap is gone. But the viewport height is unchanged and the overlay band still covers the last bottomHeight px of it, so at maximum scroll the last unpinned row sits exactly under the band. Probe results (Cypress, real layout): spreadsheet with enableAddRow: false and bottom: [99], scrolled to the end — scrollHeight 2475 (= 99 rows, i.e. shortened by the pinned row), row 98 rect top 519 / bottom 544 equals the band rect, so the last data row is invisible and unreachable. With enableAddRow: true the add-new row (data-row=100) is the one under the band instead. In the 50k-row example-pinning-rows.html (bottom: [49999]) the canvas was not shortened (scrollHeight 1,250,000 = 50,000 rows) and row 49998 is fully visible above the band — so the outcome currently depends on whether updateRowCount() ran after the row layout was resolved. Fix: never rely on shortening; keep the canvas at full height and either extend the scroll range by bottomHeight (canvas padding-bottom / th += bottomHeight) or reduce the vertical scroll viewport by the band height, as the old bottom pane effectively did. Then add a spec that scrolls to the end with a bottom pin and asserts the last unpinned row's bottom ≤ band top.
B5 non-contiguous pins vs hit-testing Partial setActiveCellInternal now always trusts data-row (verified: clicking row 1 under top: [0,2,4] activates row 1). getCellFromPoint uses document.elementFromPoint when docking is configured and otherwise falls back to the natural math, which is still wrong for non-contiguous pins; the fallback is hit whenever the point is off-screen (drag auto-scroll) or the element under it is not a cell. scrollRowIntoView now uses getRenderedRowTop. Probe (Cypress): with top: [0, 2, 4], getCellFromPoint at the rendered position of rows 1, 3 and 5 returns rows 1, 3 and 5, and scrollRowIntoView(20) places row 20 exactly at the viewport bottom — the on-screen path works. Suggest replacing the fallback with the inverse of getRenderedRowTop (binary search over rendered tops) rather than relying on hit-testing.
B6 sticky thresholds Fixed (code) row.top < scrollTop + topHeight; stickyBottomHeight starts at 0.
B7 conveyor direction Fixed (code) applyBudget reverses only for left/top.
B8 hysteresis naming Documented JSDoc now says it is an activation buffer, not stateful hysteresis.

Regressions for non-pinned grids

Id Status Notes
C1 autoHeight band Fixed (verified) getViewportHeight no longer folds header/pre-header into viewportH; resizeCanvas adds _headerRoot.offsetHeight once and tracks the inline height it owns (autoHeightContainerSizeApplied). New example11-autoheight.cy.ts asserts container = header root + content root.
C2 wheel one row per notch Fixed (verified) preventDefault() only when docking is configured; plain-grid wheel event is not cancelled. Note hasConfiguredDocking() is evaluated per wheel event (it re-normalises the column shorthands); use the cached dockingRowRegionsActive flag instead.
C3 Ctrl+drag multi-select Fixed (verified by diff) createDraggable() restored, reads the selection model's option, and setSelectionModel() recreates the Draggable. The universal selectionOptions fallback remains; drop it with the option (§4.3).
C4 ui-state-default rename Fixed (both classes emitted) Every element now carries slick-state-default ui-state-default. Since no theme in this repo ever keyed on ui-state-default (0 rules in base), the cheaper option is to revert the rename entirely and delete the added slick-state-default CSS (12 rule sites).
C5 destroy(true) no-op Fixed — needs rework See §4.1. copyCellToClipboard and the dead Ctrl+C branch were removed; type FormattedDataCachePlanner = any; type TrustedHTML = string; remain.
C6 O(columns) per cell on plain grids Fixed (code) usesDockingRowRegions() returns a flag cached in refreshDockingLayout. getRowDockingRegion still does a :scope > querySelector per cell on docking grids; plain grids return early on the flag.
C7 keyboard/focus contract changes Open Focus sinks still outside the container with tabIndex -1; Shift+Tab/F6 routing and onClick + defaultPrevented unchanged and undocumented.
C8 absBox container-relative Fixed (verified) Editor top: 224.7 for a cell at document top 229.7 (5 px inset), matching the base. getGridPosition() returns document coordinates again.

Docked-content interaction

Id Status Notes
D1 getCellFromPoint / CellRangeSelector Fixed (verified for docked cells) With scrollLeft = 200, a pinned-left cell and a top-pinned cell resolve to the right {row, cell}. CellRangeSelector now prefers getCellFromEvent (target-based) and only falls back to coordinates. Same fallback caveat as B5.
D2 wheel over docked rows Fixed (code) A MouseWheel instance is bound to the overlay once; flag reset when the overlay is removed.
D3 reorder throws with a docked sticky column Open onEnd still maps dockingLayout[band] (which includes active sticky entries) onto the band Sortable arrays.
D4 forwarded scrollLeft treated as absolute Open forwardDockingHorizontalScroll unchanged.
D5 docked rows outside the vertical range never get new centre cells Open render() still only calls renderRows for docked rows.
D6 setColumns silent reject / wrong array Partial Validation now runs against newColumns when pinning is configured or any incoming column is pinned/sticky. It still returns void after onBeforeSetColumns has fired and after applyColumnPinningOptions(newColumns) mutated the input.
D7 pinning cannot be removed Fixed (verified by spec) setOptions({ pinning: undefined }) deletes the option; deactivateSingleViewportLayout() removes the proxy scroller, resets chrome regions and restores the viewport as scroll owner. example-pinning-columns-and-column-group.cy.ts asserts the scroller and proxy class are gone.
D8 lazy activation skips destroy events Open
D9 fragments freeze selected Open Only active is mirrored (now with a shared ::after outline).

Legacy surface, claims, tests

Id Status Notes
E1 frozen options declared; Grid Menu branches Fixed (code) All frozen*/*Freeze* members removed from GridOption; slick.gridmenu.ts no longer subscribes to onSetOptions for frozenColumn and always uses .slick-header-left.
E2 false claims / missing docs Partial docs/pinning-sticky.md (30 lines) added and linked from README/TOC; progress file gets a "historical, do not trust counts" banner and the resolver path corrected; SKILL.md path corrected. Column.pinnable is still declared and still read by nothing; the progress file still carries the Header Menu / GridState / unit-test narrative below the banner.
E3 test integrity Mostly fixed The three quirk specs are properly ported to pinning.rows (bottom-pinned cleanup, hit-testing, empty configs) and now test the new behaviour. example-auto-scroll-when-dragging only lost the "overlay exists with empty pinning" assertion (correct after D7). The weakened equal/lte assertions from the first report remain as they were.

Medium items

Id Status Notes
M1 per-scroll layout thrash Partial The offsetWidth read and dead inline transforms are gone from the proxy path. Each horizontal scroll still performs four style writes per cached docked row (two setProperty('--slick-docking-scroll-left'), two removeProperty('transform')); the removeProperty calls are unconditional and could be done once when the row enters the proxy mode. Overlay listeners are now bound once (getBoundedEvents() check) instead of per scroll. syncDockedRowContainers() still runs on every vertical scroll regardless of revision. Column-resize pass unchanged.
M2 mixinDefaults partial docking Fixed (code) docking deep-defaulted after applyDefaults.
M3 API drift list Open Unchanged (applyHtmlCode, sanitizeHtmlString, animate, onHeaderKeyDown type…).
M4 universal leakage Partial Removed: enableExcelCopyBuffer, autoScrollResizeLeftDelay/RightDelay, the two RESIZE_AUTOSCROLL_* constants, copyCellToClipboard. Still present: enableGridMenu, enableRowDetailView, enableFormattedDataCache + planner, silenceWarnings, selectionOptions: any, datasetIdPropertyName, rowDetailView: any + renderMode branch, columnResizingDelay, Column.editorClass/exportCustomFormatter/exportWithFormatter/pinnable, EditorArguments.isCompositeEditor.
M5 dead docking.controller.ts Fixed (verified) Deleted; no empty bundle emitted.
M6 overlay ghost digits with zero-width scrollbars Fixed (verified) The guessed 8 px inset is gone; capture with hidden scrollbars is clean.
M7 small geometry items Partial cancelScheduledAnimationFrame now tracks timeout ids in a Set (correct). The other six items are unchanged.
M8 examples/docs Partial AGENTS.md unchanged; example-pinning-columns-and-rows.html still hard-codes bottom: [49999]; CSP example still carries the BrowserSync policy.

4. Review of the fix commits themselves

4.1 destroyAllElementProps — reverted, not fixed

The new implementation (src/slick.grid.ts:147-203) is a module-level function taking object, casting to Record<string, unknown>, and nulling a hand-typed array of 46 property names. Problems:

  • It duplicates the field list in the class. Any new element field (the PR itself added _dockingOverlay, _dockingHorizontalScroller, dockingHeaderRegions…) has to be added in two places, and nothing checks that they agree. The base class had the same weakness (destroyAllElements nulled ~40 fields by hand); this PR is the opportunity to stop doing that.
  • It lives outside the class, so it cannot be typed against this and has to erase types.
  • It misses dockingChromeByColumn (a Map of header/header-row/footer elements) and _hiddenParents is nulled although restoreCssFromHiddenInit expects an array.

Proposed replacement — a protected method that nulls fields by content rather than by name:

/** Drop every DOM reference the instance still holds so a retained grid object cannot keep the detached tree alive. */
protected destroyElementReferences(): void {
  const isElement = (value: unknown): boolean => value instanceof Element;
  const holdsElements = (value: unknown): boolean =>
    isElement(value) ||
    (Array.isArray(value) && value.length > 0 && value.every(isElement)) ||
    (!!value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype &&
      Object.values(value as object).length > 0 && Object.values(value as object).every(isElement));

  for (const key of Object.keys(this)) {
    if (holdsElements((this as Record<string, unknown>)[key])) {
      (this as Record<string, unknown>)[key] = null;
    }
  }
  this.dockingChromeByColumn.clear();
}

Twelve lines, no list, covers every current and future element field (single elements, the one-item arrays, and the Record<band, HTMLDivElement> region sets), and skips rowsCache/postProcessedRows (already cleared by clearInternalDomCaches) because their values are entry objects, not elements. Call it from destroy() in place of destroyAllElementProps(this) and delete the module-level function. If a field must survive (none does today), exclude it with a small Set of names — that is a one-line exception list rather than a 46-line inclusion list.

An even smaller alternative is to keep destroy(true) as a documented no-op and remove the parameter in this major version: after Utils.emptyElement(container) and initialized = false, the retained references only matter to an application that keeps the grid instance alive after destroying it. I would not choose that, because the parameter has existed for years and the reflective version is cheap.

4.2 Other quality remarks on the fixes

  • Colspan over a pinned boundary (0b58e873). The row regions now get overflow: visible and the host cell z-index: 21, so a colspan host in the left band paints across the boundary. That is the requested "spread left to right" look, but the host lives in the sticky left band while the columns it visually covers live in the scrolling centre band. Probe (Cypress, example-colspan.html, all columns 220 px, left: 1): at scrollLeft 0 the host spans 232→892 and the row's % Complete cell starts at 892, so nothing is hidden. At scrollLeft 500 the centre band has moved — the continuation fragment is at −48→392 and % Complete at 392→612 — but the host still spans 232→892 with z-index: 21, so the "83" in % Complete (and the Effort Driven cell after it) are painted over by the host's overflow. In other words, every centre column that scrolls under a boundary-crossing colspan disappears behind it. The clean alternative is to keep the host clipped to its band and let the (already existing) fragment carry the visible text in the centre band — the fragment mechanism was built for exactly this.
  • getCellFromPoint via elementFromPoint. Hit-testing the DOM is a pragmatic fix but it changes the function's contract from pure geometry to "whatever is painted there": it returns the wrong cell when a menu, tooltip or editor overlays the point, and it silently degrades to the natural math off-screen. A geometric inverse of the render mapping (band-aware column lookup, getRenderedRowTop inverse for rows) would be exact, testable without a DOM, and ~30 lines.
  • setOptions pinning merge grew to 102 lines. The atomic replacement of pinning.columns.left/right and pinning.rows.top/bottom and stickyRows.* is the same eight-line pattern repeated seven times; a loop over the six paths (or a replaceArrays(target, source, paths) helper in Utils) cuts about 30 lines and reads better.
  • updateRenderedColspanFragmentGeometry() is called at the end of applyColumnWidths(), i.e. on every column-resize mousemove. It iterates the whole rowsCache and, for hosts not in cellNodesByColumnIdx, runs a querySelectorAll('.slick-cell') per row. Cheap when there are no fragments, but it belongs in onResizeEnd (or should early-return on a "any fragments rendered" flag).
  • matchesRowReference silently makes numeric ids unreachable; say so in the JSDoc of PinnedRows/StickyRows and in docs/pinning-sticky.md.
  • hasConfiguredRowDocking() now treats empty arrays as "not configured" (good — fixes the spurious overlay), and example-auto-scroll-when-dragging.html was changed to toggle with pinning: undefined. The removePinning detection uses hasOwnProperty + === undefined; pinning: null still deep-merges to nothing and leaves the old value. Accept null too, or document undefined as the only removal form.
  • ui-state-default restoration was done by string-concatenating both classes at ten call sites. If the rename is kept, put the pair in one constant; if not (recommended, see C4), delete slick-state-default and the 12 SCSS rule sites that were added for it.
  • docs/pinning-sticky.md is a start but is thirty lines for a major breaking change. It needs the option→option migration table (frozenColumn: Npinning.columns.left: N, frozenRow + frozenBottomrows.top/bottom, removed methods, removed CSS classes, getGridPosition semantics, Column.sticky values, docking budgets).

4.3 Still-present universal-fork material that should go

GridOption.enableGridMenu (three "last column makes room for the Grid Menu" compensations), enableRowDetailView/rowDetailView.renderMode (this repo's RowDetailView has no renderMode), enableFormattedDataCache and the whole planner path (formattedDataCachePlanner, shouldRefreshFormattedCachePlanner, syncDataViewFormattedCachePlanner, the getFormatter display-value wrapper, CustomDataView.setFormattedDataCachePlanner/getCellDisplayValue) — SlickDataView implements none of it; silenceWarnings; selectionOptions: any; datasetIdPropertyName (now only a fallback); Column.editorClass, exportCustomFormatter, exportWithFormatter, pinnable; EditorArguments.isCompositeEditor; type FormattedDataCachePlanner = any, type TrustedHTML = string. Together about 120 lines of src/slick.grid.ts plus 20 interface lines, none of which does anything in 6pac.

5. Can the PR lose lines without hurting performance or readability?

Yes, materially. src/slick.grid.ts went from 9,589 to 11,777 lines (+2,188 net; +5,668/−3,480 in the diff). Method inventory: 103 methods added (2,207 lines), 21 removed (521 lines), 14 existing methods grew by 15+ lines (+430). Comment lines went from 2,017 to 2,343, and the diff adds 887 comment lines while removing 561 — about 15 % of the added text is prose.

Candidates, most valuable first (estimates are net lines in slick.grid.ts unless noted):

# What Est. saving Effect on perf / readability
1 Unrequested keyboard/focus feature ported from universal: focusHeaderRowFilter (33), focusHeaderMenuOrColumn (15), focusGridMenu (12), focusHeaderColumn, focusGridCell, focusElementWithoutBubbling, stopFullBubbling, getVisibleElements, handleContainerKeyDown (20) and the F6/Tab/Shift+Tab routing inside handleGridKeyDown (~20). The selectors they target (.slick-header-menu-button[tabIndex="0"], .slick-grid-menu-button[tabIndex="0"]) have no producer in this repo. −130 None on perf; removes an undocumented behaviour change (C7). Ship it as its own PR with plugin support if wanted.
2 Universal leakage in §4.3 −120 src, −20 models None; removes dead branches and any types.
3 Narrative comments. Many new comments are debugging history ("placed right-pinned titles at that stale edge (for example 1537px for a 1637px proxy)", "The docking POC's one horizontal scrollbar…", 10-line justifications before one-line writes). Trim to intent-level comments. −200 to −300 Improves readability; the file already has 2,343 comment lines. Keep the ones that explain a non-obvious invariant (proxy translation, overlay clip, row shift).
4 destroyAllElementProps → reflective method (§4.1) −45 Safer.
5 applyDockingToColumnChrome (161 lines): the four branches (sticky-transform / centre / left / right) each set position/left/right/order/transform with slightly different values; a placeChrome(element, { position, left, right, order, transform, offset }) helper and building dockingChromeByColumn from getHeaderColumn(id) instead of querySelectorAll(...).find per column −50 Also removes the O(n²) header lookup on every resize step.
6 setOptions pinning/stickyRows array replacement as a loop or Utils helper −30 Neutral.
7 Revert the slick-state-default rename −10 src, −25 scss Removes a breaking change; nothing in this repo keys on either class.
8 applyRowTopOffset (74): the rowspan metadata scan can be computed once per row at render time and stored on the cache entry instead of on every syncDockedRowContainers pass −20 Faster vertical scrolling on row-docked grids.
9 Small unused/duplicate public methods: getColumnByIdx (0 callers), getColumnHeaderByIndex (alias of getColumnByIndex), removeCellCssStylesBatch (0 external callers), getTopPanels returning the same panel twice −30 Smaller public surface.
10 updateRenderedColspanFragmentGeometry host lookup fallback (querySelectorAll + find) — the host is always in cellNodesByColumnIdx −6 Neutral.

Total: roughly 650–750 lines (about a third of the net growth) without touching the docking architecture, and items 5 and 8 are also performance improvements. What should not be cut: the region-routing code in appendRowHtml/appendCellHtml/createColumnHeaders, the DockingController, the overlay/proxy sync — that is the feature.

6. Cypress

Specs 68 (67 + new example11-autoheight.cy.ts)
Passing 710
Failing 1 — example-plugin-headermenu.cy.ts › "…Feedback->ContactUs sub-menus…": level-2 sub-menu still opens dropleft on Windows (Electron); unchanged from the first report, where the base version of the spec passed on the same machine. Nothing in the fix commits touches header layout or getGridPosition().width, so this was expected
Pending 1 (example-auto-scroll-when-dragging "MAX interval", skipped in base too)

The new and reworked specs (example11-autoheight, the three quirk-pinning-* harnesses, the spreadsheet hidden-column case, example-grouping-esm pinning cases, example-colspan pinned-colspan cases) all pass and now assert the intended behaviour rather than echoing options.

7. Recommended next steps

  1. Fix bottom-pin reachability (B4) — extend the scroll range or shrink the scroll viewport by the band height; add a spec that scrolls to the end with bottom: [N-1] and asserts row N−2 is fully visible, with and without enableAddRow.
  2. Replace the destroyAllElementProps list with the reflective method (§4.1).
  3. Replace the elementFromPoint fallback with a geometric inverse (B5/D1) so off-screen drag coordinates resolve correctly under non-contiguous pins.
  4. Decide the colspan-over-pinning look: the probe shows the host does paint over scrolled centre cells (§4.2). Unless that is the intended AG-Grid-style behaviour for every cross-boundary colspan, clip the host to its band and let the fragment carry the text.
  5. Remove the universal leakage (§4.3) and, unless the focus feature is wanted now, the focus/keyboard routing (§5 item 1).
  6. Address the open Ds (D3, D4, D5, D8, D9) or list them explicitly as known limitations in docs/pinning-sticky.md.
  7. Expand docs/pinning-sticky.md into a real migration section and remove the stale narrative from the progress file (or delete the file from the PR).
  8. Reproduce the Windows header-menu alignment failure (§6) — it is deterministic here and passes on the base.

@ghiscoding

Copy link
Copy Markdown
Collaborator Author

@6pac so I would prefer if you ask Claude to finish the rest, I'm out of token until Saturday anyway. Thanks

@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Can do, might be worth looking at the '5. Can the PR lose lines without hurting performance or readability?' section especially 1,7,8 - they are all optional. Would be good for you to check. eg. 7, 8 maybe should be left for legacy purposes.

6pac-ai and others added 11 commits September 18, 2026 16:20
`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>
…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>
`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>
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>
…ments

`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>
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>
`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>
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>
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>
… 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>
… 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>
@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

PR #1302 — issues fixed since the first evaluation

Two rounds of fixes are covered: the PR author's commits 0b58e873..71e2acf0 ("part 1–4 fixes
of audit review" and follow-ups) and the audit-fix branch pr-1302-audit-fixes
(461d557c..85d0534a, patches in PR-1302-patches\). Ids refer to PR-1302-Evaluation.md.
"Verified" means re-demonstrated on the built code (browser probe, DOM dump, screenshot or a
Cypress spec that fails without the fix); "by diff" means the change was reviewed but not executed
in isolation. Final state: full Cypress suite 75 specs, 719 passing, 0 failing, 1 pending
(pre-existing skip) on Windows/Electron.

Blockers and High

Id Issue Fixed by How Verified
A1 left: N over-pinned when column ids were numeric (index refs also matched column.id); spreadsheet spec asserted the wrong count Author normalizeColumnPinningReferences returns indexes only (strings resolve by String(column.id), numbers bounds-checked); spec corrected to 4 and a hidden-column case added DOM dump: 4 left headers (was 5); spec
A2 Numeric shorthands counted hidden columns Author Boundary/count resolved over visible columns Spec (hidden column B, left: 3)
B1 Controller matched row refs by id or index; numeric ids cross-contaminated bands Author matchesRowReference: index, or string id only By diff
B2 id→index cache never invalidated on count-preserving sort/filter Author Cleared in invalidateRows / invalidateAllRows By diff
B3 Custom DataView idProperty ignored Author (+ audit removed the option fallback) getDataViewIdProperty() uses DataView.getIdPropertyName(), then 'id' By diff
B4 Bottom-pinned rows: add-new row hidden / gaps; author's fix then hid the last scrolling row Author + audit commit 4 Rows after a bottom pin render one pinned height higher (author); canvas no longer shortened, so the collapsed slot sits under the band (audit) Verified: quirk-pinning-bottom-reachability (pinned last row, with enableAddRow, pinned middle row)
B5 Non-contiguous pins broke hit-testing / active cell Author + audit commit 3 setActiveCellInternal trusts data-row (author); getCellFromPoint maps geometrically through bands, overlay and getRenderedRowTop inverse (audit), including unrendered rows Verified: quirk-pinning-hit-testing-geometry
B6 Sticky-row thresholds ignored the top band and double-subtracted the bottom band Author row.top < scrollTop + topHeight; stickyBottomHeight starts at 0 By diff
B7 conveyor kept the wrong end for right/bottom Author applyBudget reverses only for left/top By diff
C1 Every autoHeight grid had an empty band below its rows Author Header height no longer folded into viewportH; container sized once; new example11-autoheight spec Verified: plain grid ends at the base height (2604 px vs 2636 before); pinned example band gone
C2 Vertical wheel = one row per notch on every grid Author (+ audit note on per-event cost) preventDefault() only when docking is configured Verified: plain-grid wheel event not cancelled; pinned grid cancelled
C3 Ctrl/Meta+drag multi-selection regressed Author + audit commit 2 createDraggable() restored and re-created on setSelectionModel() (author); the selectionOptions fallback removed so the selection model's option is authoritative (audit) Spec example-plugin-hybridselectionmodel passes without the universal option in the example
C4 Undocumented ui-state-defaultslick-state-default rename Author Both classes emitted By diff (revert still recommended, see remaining list)
C5 destroy(true) was a no-op; other universal stubs Author → audit commit 1 Author restored a 46-name list; audit replaced it with destroyElementReferences() (fields nulled by content, dockingChromeByColumn cleared); copyCellToClipboard and the Ctrl+C dead branch removed (author); FormattedDataCachePlanner/TrustedHTML stubs removed (audit) Verified: quirk-destroy-element-references
C6 Plain grids paid O(columns) per rendered cell Author usesDockingRowRegions() returns a flag cached in refreshDockingLayout By diff
C8 absBox() container-relative → LongText editor misplaced Author Document-relative coordinates restored Verified live: editor top 224.7 for a cell at 229.7, matching the base
D1 getCellFromPoint not docking-aware; drag selection wrong over pinned cells Author + audit commit 3 CellRangeSelector prefers getCellFromEvent (author); geometric getCellFromPoint (audit) Verified: docked cells resolve correctly while scrolled; upward drag auto-scroll assertion restored
D2 Wheel over docked rows scrolled the page Author MouseWheel bound to the overlay once By diff
D3 Reorder threw with a docked sticky column Audit commit 9 Slots follow the DOM band; length mismatch leaves order unchanged Verified: quirk-sticky-column-reorder
D4 Forwarded chrome scrollLeft treated as absolute Audit commit 8 Delta forwarding; reset echo no longer mirrored Verified: quirk-pinning-chrome-scroll-forwarding (400 + 60 = 460)
D5 Docked rows outside the range never got new centre cells; in-range docked rows never cleaned Audit commit 7 Docked rows processed against the horizontal range; pinned-row exemption removed from cleanUpCells Verified: quirk-pinning-docked-row-cell-virtualization
D7 Pinning could not be removed; proxy scroller persisted Author pinning: undefined deletes the option; deactivateSingleViewportLayout() Verified by spec (example-pinning-columns-and-column-group)
D8 Lazy activation emptied chrome without destroy events Audit commit 6 Events fired by the region set/reset helpers before emptying; duplicated loops removed; header-row event passes the cell as node Verified: quirk-pinning-lazy-activation-destroy-events (once per column on activation and removal)
D9 Colspan fragments froze selected/custom classes Audit commit 5 Classes mirrored onto fragments in updateCellCssStylesOnRenderedRows Verified: selection test in example-colspan.cy.ts
E1 Legacy frozen* options declared; Grid Menu branched on frozenColumn Author Options removed from GridOption; Grid Menu always uses .slick-header-left By diff
E2 PR/progress claims false for this repo; no docs; dead pinnable Author + audit commit 11 Author: banner on the progress file, docs/pinning-sticky.md (30 lines), SKILL path fix. Audit: pinnable removed; docs expanded with semantics, API, selectors, migration table, limitations; SKILL.md and progress file rewritten to this repository only Reviewed
E3 Three tautological quirk specs; weakened assertions Author + audit Author ported quirk-pinning-row-zero, -bottom-hit-testing, -bottom-cell-cleanup to pinning.rows. Audit restored the upward auto-scroll greaterThan assertion (other weakened assertions remain, see remaining list) Specs pass and assert the new behaviour
§3 (v1/v2) Header-menu sub-menu alignment failed on Windows Audit commit 10 Root cause: demo label rename narrowed the menu by 13 px; label is now "Column Pinning" Verified: spec passes on Windows

Medium

Id Issue Fixed by How Verified
M1 (part) Per-row offsetWidth read and dead inline transforms on horizontal scroll; overlay listeners rebound per scroll Author Read and inline transforms removed; listeners bound once (getBoundedEvents() check) By diff
M2 mixinDefaults left a partial docking object Author docking deep-defaulted after applyDefaults By diff
M4 Universal-fork leakage Author (part) + audit commit 2 Author removed enableExcelCopyBuffer, autoScrollResize*Delay; audit removed the rest (cache planner + DataView hooks, enableGridMenu compensations, enableRowDetailView/renderMode, silenceWarnings + zoom warning, selectionOptions, datasetIdPropertyName, Column.editorClass/exportCustomFormatter/exportWithFormatter/pinnable, EditorArguments.isCompositeEditor); sanitizeHtmlString back to the 6pac signature with logSanitizedHtml logging (also an M3 item) Nine plugin/editor/menu specs pass
M5 Dead src/docking.controller.ts shipped as an empty bundle Author File deleted Verified: no dist/browser/docking.controller.js emitted
M6 Ghost digits at the docked-row edge with zero-width scrollbars Author Guessed 8 px overlay inset removed Verified: hidden-scrollbar capture clean
M7 (part) cancelScheduledAnimationFrame called clearTimeout with a rAF id Author Timeout ids tracked in a Set By diff
M3 (part) sanitizeHtmlString lost logging; logSanitizedHtml dead Audit commit 2 Base implementation restored By diff; type-check
§4.2 (v2) updateRenderedColspanFragmentGeometry / fragment geometry after resize Author Fragment geometry recomputed after width changes Spec (example-colspan resize case)

Documentation

  • docs/pinning-sticky.md — option semantics (visible-column shorthands, numbers = indexes,
    strings = ids), runtime API, docking budgets, rendering notes, stable selectors, v5 → pinning
    migration table (options, methods, DOM classes, behaviour), known limitations, example list.
  • .agents/skills/pinning-sticky/SKILL.md — no unit-test layer, no pinnable, self-hosted
    quirk-* harness pattern, Windows geometry note.
  • .agents/plans/pinning-sticky-progress.md — rewritten as a status file for this repository:
    architecture, public surface pointer, verification list, resolved items, limitations, resume
    checklist. Fork-only claims removed.
  • src/models/docking.interface.ts — reference JSDoc clarified.

New browser coverage added by the audit branch

quirk-destroy-element-references, quirk-pinning-hit-testing-geometry,
quirk-pinning-bottom-reachability, quirk-pinning-lazy-activation-destroy-events,
quirk-pinning-docked-row-cell-virtualization, quirk-pinning-chrome-scroll-forwarding,
quirk-sticky-column-reorder, plus a selection case in example-colspan.cy.ts and the restored
assertion in example-auto-scroll-when-dragging.cy.ts.

Size

src/slick.grid.ts: 11,777 lines at the PR head → 11,727 on the fix branch while adding six
behavioural fixes (net src/ delta +253 / −319). The larger reductions listed in
PR-1302-Evaluation-v2.md §5 (focus routing, comments, chrome helper, option merge loop, rename
revert) were not applied and remain available.

@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

@ghiscoding Next is what's left that I thought you should look at before proceeding.

Could you read it carefully and make calls? Assuming most of it's okay, probably just tell me what you don't want to do.

@6pac

6pac commented Sep 18, 2026

Copy link
Copy Markdown
Owner

PR #1302 — remaining issues after the audit-fix branch

State: PR head 71e2acf0 plus the 11 commits on pr-1302-audit-fixes (461d557c..85d0534a).
Ids refer to PR-1302-Evaluation.md (first report) and PR-1302-Evaluation-v2.md. Severity is
as judged now, after the fixes; line numbers are approximate on the fix branch and the function
name is the reliable anchor. Nothing in this list is verified to be user-visible in the shipped
examples unless marked Observed.

1. Behaviour and correctness

Id Sev Issue Where Suggested fix
§4.2 (v2) / §7.4 High (design) Colspan host paints over scrolled centre cells. A colspan that starts in a pinned band gets overflow: visible and z-index: 21; the host stays with its (sticky) band while the centre band scrolls, so centre columns that scroll under it are hidden. Observed with a real browser probe (PR-1302-evidence\v2\probe-results.txt, colspan). Left unchanged by request; documented as a limitation. _slick-docking.scss (.slick-row-colspan-crossing-docking rules), appendColspanFragments If not the intended look: keep the host clipped to its band and let the existing continuation fragment carry the visible text (copy textContent/innerHTML into the fragment, host keeps the logical role).
D6 Medium setColumns() can still reject silently: applyColumnPinningOptions(newColumns) mutates the input and onBeforeSetColumns fires before validation; a rejection returns void with no onAfterSetColumns. setColumns Validate before mutating/firing, and return a boolean (or throw) so Column Picker / Grid Menu can react.
C7 Medium Keyboard/focus contract changes are still undocumented: focus sinks live outside the container with tabIndex -1 (so container.contains(document.activeElement) is false while the grid has focus); Shift+Tab at (0,0) goes to header-row filters/grid menu instead of navigatePrev(); F6 focuses the header; onClick also aborts on e.defaultPrevented. The focus helpers target .slick-header-menu-button[tabIndex="0"] / .slick-grid-menu-button[tabIndex="0"], which no plugin in this repo produces. initialize (sinks), handleGridKeyDown, handleContainerKeyDown, focusHeaderRowFilter, focusHeaderMenuOrColumn, focusGridMenu, focusHeaderColumn, focusGridCell, focusElementWithoutBubbling, stopFullBubbling, getVisibleElements, handleClick Either drop the fork's focus routing from this PR (~130 lines) and restore tabIndex: 0 sinks inside the container, or keep it, make Header Menu / Grid Menu emit focusable buttons, and list the changes as breaking.
B8 / A2 (residual) Low stickyHysteresis is an activation buffer for columns only (documented now, name still misleading). left shorthand is an inclusive boundary while right is a count (documented, still asymmetric). docking.interface.ts, normalizeColumnPinningReferences Rename to stickyActivationBuffer or accept as documented.
M2 (residual) Low enforceMinCenterRowBudget counts docked sticky rows although its JSDoc says permanent rows only, and it runs only on resize. enforceMinCenterRowBudget Sum !entry.sticky entries (or use getTopPinnedRowsHeight()), and re-run when row docking changes.
v2 §4.2 Low setOptions({ pinning: null }) deep-merges to nothing and keeps the old pinning; only pinning: undefined removes it. setOptions (removePinning) Treat null like undefined, or document undefined as the only removal form.
M7 Low Small geometry items unchanged: internalScrollColumnIntoView subtracts the vertical scrollbar twice in proxy mode; viewportHasHScroll and the proxy's overflow decision use different criteria; getRightDockedChromeLeft mixes getBoundingClientRect screen pixels with layout pixels (wrong under a scaled ancestor); validateColspanPinningSequence inspects only rendered rows; bottom band stacks sticky rows below permanent rows while the top band stacks them inside. respective functions Individually small; the scrollbar double-subtraction and the overflow criterion are the ones users may notice (unneeded scroll / phantom scrollbar).
M7 (RTL) Low, unverified resolveColumns receives the raw negative RTL scrollLeft; getCellFromDockedPoint deliberately skips RTL; example-rtl.cy.ts has no pinning/sticky assertions. refreshDockingLayout, getCellFromPoint Add an RTL pinning/sticky spec; mirror scrollLeft before resolving.
M3 Low Public API drift still not listed as breaking: applyHtmlCode(target, value, skipEmptyReassignment) replaced the options-object overload (JSDoc still documents the object); animate removed from all set*Visibility; trigger() → public triggerEvent(); validateAndEnforceOptions now protected; onHeaderKeyDown typed OnKeyDownEventArgs but notified with { event, column, grid }; width validation >>=; base throwWhenFrozenNotAllViewable has no replacement. various Fix the onHeaderKeyDown arg type; add the rest to the migration table in docs/pinning-sticky.md.
B1 (residual) Nit Numeric dataset ids cannot be used as row references at all (numbers are always indexes). Documented; a { id } reference form would lift the restriction. resolveDockingRowIndex, DockingController.resolveRows Optional API addition.

2. Performance

Id Sev Issue Where Suggested fix
M1 Medium Horizontal scroll in proxy mode still does four style writes per cached docked row per event (two setProperty('--slick-docking-scroll-left'), two unconditional removeProperty('transform')). applyDockingScrollOffsetToRow, applyDockingProxyScrollOffsets Set the custom property once on a common ancestor and let CSS use it; do the removeProperty once when a row enters proxy mode.
M1 Medium Vertical scroll on a row-docking grid still runs syncDockedRowContainers() over the whole rowsCache on every event (per row: querySelector('.slick-cell.rowspan'), metadata lookup, ~8 DOM writes), even when the docking revision is unchanged. refreshRowDockingLayout, syncDockedRowContainers, applyRowTopOffset Early-return when rowDockingLayout.revision and scrollLeft are unchanged; cache the rowspan flag on the row cache entry at render time.
M1 Medium Column resize runs the full applyDockingToColumnChrome pass on every mousemove: O(n²) querySelectorAll(...).find per column plus getBoundingClientRect/getComputedStyle interleaved with width writes. updateRenderedColspanFragmentGeometry() also runs per mousemove. updateCanvasWidth, applyDockingToColumnChrome, applyColumnWidths Build the chrome map from getHeaderColumn(id); batch reads before writes; run the docking chrome pass once in onResizeEnd.
C2 (residual) Low hasConfiguredDocking() is evaluated on every wheel event (re-normalises the column shorthands). handleMouseWheel Use the cached dockingRowRegionsActive flag.
v1 §4.1 C6 (residual) Low getRowDockingRegion still does a :scope > querySelector per appended cell on docking grids (plain grids now return early). getRowDockingRegion, appendRowHtml Use rowsCache[row].cellRegions[band].

3. Tests

Id Sev Issue Where Suggested fix
E3 (residual) Medium Assertions weakened by the PR that were not restored: example-auto-scroll-when-dragging topLeft→bottomRight scrollTop equallte (row auto-scroll no longer asserted) and getIntervalUntilRow16Displayed no longer waits for the row; example-auto-header-height dropped both scrollHeight <= clientHeight + 1 overflow checks; headers-width-scroll-sync no longer asserts header/body scrollLeft equality (fudge headerRange += clientWidth diff); quirk-fractional-height-bottom-render precondition inverted (> 0.01< 1), so the quirk need not reproduce; dom-shape-characterization loosened the wrapper assertions the base said not to loosen; example-plugin-hybridselectionmodel swapped trigger() for native MouseEvent. the named specs Restore each assertion against the new scroll owner where the old behaviour is still intended; if a behaviour changed on purpose, say so in the spec.
E3 (residual) Low cypress/support/commands.ts: getNthCell changed from nth-child to .l{n}.r{n} semantics without a rename; a dead legacy branch ([style="transform: translateY(...)"]) and an unused getTransformValue were added; force: true count rose 142 → 159; drag.ts default distance 100 → 140. support files Rename or restore getNthCell; delete the dead branch and helper; remove force: true where targets should be clickable.
coverage Low Coverage dropped versus the deleted frozen specs: pre-header column-picker case, both reorder auto-scroll cases, most per-band cell value assertions (now counts/ids). example-pinning-* specs Re-add one value spot check per band; port the two reorder auto-scroll cases to the docking scroller.
env Nit Cypress occasionally exits non-zero after a spec that reports 0 failures (seen on example-0031-row-span-employees and quirk-pinning-chrome-scroll-forwarding); a teardown artefact, harmless for CI's cypress-io/github-action but confusing in custom runners. Ignore, or treat the JSON results as the source of truth.

4. Size and readability (from v2 §5, not applied)

# Item Est. lines Notes
1 Fork keyboard/focus routing (see C7) −130 Also removes an undocumented behaviour change.
2 Narrative comments (debugging history such as "for example 1537px for a 1637px proxy", ten-line justifications before one-line writes). The diff added 887 comment lines; the file has ~2,340. −200 to −300 Keep the invariant comments (proxy translation, overlay clip, row shift).
3 applyDockingToColumnChrome (161 lines): four near-identical branches setting position/left/right/order/transform; extract a placeChrome(element, …) helper; build dockingChromeByColumn from getHeaderColumn(id) −50 Also fixes the O(n²) lookup above.
4 setOptions pinning/stickyRows array replacement repeated seven times −30 One loop or a Utils helper.
5 Revert the ui-state-defaultslick-state-default rename (both classes are emitted at ten call sites; 12 SCSS rule sites were added; nothing in this repo keyed on either) −10 src, −25 scss Removes a breaking change for consumers' CSS.
6 applyRowTopOffset rowspan metadata scan per sync pass → cache on the row entry −20 Also perf (see M1).
7 Unused/duplicate public methods: getColumnByIdx (0 callers, returns undefined not null), getColumnHeaderByIndex (alias), removeCellCssStylesBatch (0 external callers), getTopPanels returning the same panel twice −30 Smaller public surface.
8 updateRenderedColspanFragmentGeometry host lookup fallback −6 Host is always in cellNodesByColumnIdx.

5. Examples and repo files

Id Sev Issue Suggested fix
M8 Low example-pinning-columns-and-rows.html hard-codes bottom: [49999] on a page with a DataView filter and pager; the pin silently disappears after filtering. Derive the reference from the current data length or use a string id.
M8 Low example-draggable-header-grouping.html passes rows: { left: [], right: [] }, not a valid PinnedRows shape (ignored). Use top/bottom or drop rows.
M8 Low examples/index.html labels example-pinning-rows.html as "Pinned Columns & Rows"; example-quirk-frozen-row-*.html keep "DO NOT MERGE" banners and frozen names. Fix label; rename or delete the temporary pages.
M8 Low example-csp-policy.js/example-csp-header.html carry a BrowserSync trusted-types allowance for the dev server. Inject the policy from scripts/dev-watch.mjs instead.
M8 Low AGENTS.md says never modify dist/ "including when running builds", which contradicts npm run build:prod, CI and scripts/release.mjs. Say "do not commit dist/ changes in feature PRs".
M8 Low _slick-docking.scss is @used by slick.grid.scss and both themes, so a page loading grid CSS plus a theme gets the docking rules twice; dead .slick-pane/.slick-pane-header rules remain in slick.grid.scss and the alpine theme; .slick-header-auto-height .slick-header-columns-right now targets a display: contents wrapper. Import once; delete the dead rules.
docs Low docs/pinning-sticky.md is now a reference but not a tutorial; no screenshots; the PR description on GitHub still lists fork-only features (Header Menu commands, pinnable, Grid State, unit-test counts) and should be edited to match the repository. Update the PR body when the fixes are applied.

@ghiscoding

ghiscoding commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Can do, might be worth looking at the '5. Can the PR lose lines without hurting performance or readability?' section especially 1,7,8 - they are all optional. Would be good for you to check. eg. 7, 8 maybe should be left for legacy purposes.

Yes sure, when I asked the AI to replicate my universal PR in here and it carried a lot of previous universal changes that you didn't have in your repo (e.g. step 1 or C7 above, is mostly about a11y that I've fixed in my repo which you didn't have, I also always display Header Menu in my grids, but in your repo it's an optional plugin). I would say, you could ask Claude to cleanup unused code but I think you should also ask it to do an a11y audit to improve it on your repo as well (that is you wish to do it)

For some feedback from the last audit above:

  • v2 §4.2, setOptions({ pinning: null }) or setOptions({ pinning: undefined }) should equal to the same action.
  • M7 (RTL), we could add pinning and/or sticky in the same example (probably inputs for pinning and 2nd grid for sticky), I didn't spend time on this since I'm never using RTL myself
  • M3 applyHtmlCode() it's another one that my AI decided to use my universal implementation which is different compared to yours, Claude should probably just fix the API drift
  • 4.3 "O(n²)", wow yes please fix it lol
  • 4.4 Utils helper seems like the correct approach (DRY)
    1. M8 pinning bottom disappear after filtering should definitely be fixed
    1. M8 example-quirk-frozen-row-*.html I thought I had renamed all files, but if there's leftover then please rename or cleanup, we shouldn't have frozen anywhere anymore
    1. M8 AGENTS.md says never modify dist/, I've mainly put that in to make my AI stop losing token on modifying dist/ files which was stupid since it's produced by the build. If you want to rephrase it then go ahead

The rest seems fine, so you could provide my feedback to Claude and let it finish accordingly. I will test the UI after work, Claude brought some very valid point, it's nice to have double AI audits, this will improve the features overall I think :)

Have you had a chance to try the UI yourself? Also just to make sure you understand why it's now called Pinning and Freeze name is gone (pinning is now individual, as oppose to freeze a range, and you can even skip column pinning, e.g. left: [0, 2, 4] but I've put code in place to simulate freeze behavior for easier migration, e.g. left: 2 (integer) loop through each columns from 0 to 2 and apply `pin: 'left' to each). As for the Sticky I only replicated 1 example demoed in your mockups, but you could replicate the second one if you want, have you tried it as well? I'd be happy to hear your thoughts about the new UI in general...

Also I know we did increase LOC by more than a thousand lines of real code (excluding comments/interfaces), which I complained in your other PR, but I'm much more happy with the new UI/UX and I also based my implementation on Ag-Grid UX, which I think was also a good decision and we also gain Sticky which Ag-Grid doesn't even have (AI also said we could use Pinning & Sticky in same grid as well, which I was a bit surprised, but I haven't tried that, not sure anyone would do that anyway). So in the end, I find that this will be a big modernization improving UI/UX in a good direction :)

@6pac

6pac commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Okay, I'll go ahead, noting those comments.

In terms of the specific SlickGrid-Universal code, I'll remove it from this PR. I think it's a good idea to move as many of your SlickGrid-Universal enhancements back here as possible, but that should be separate PRs. Will look at that once we're done here.
Same with the a11y review (I'll probably ask it to use SlickGrid-Universal as a reference so that we keep the codebases as similar as possible).
Let me know if there is a specific methodology you would like to use to try to keep the codebases in sync - there's really no point in doing the same thing two slightly different ways.

@ghiscoding

ghiscoding commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

yes your approach makes sense and I like the idea you had the other day to decouple the column auto-sizing, that is the main difference with our 2 slickGrid.ts and if you move it to a plugin then that would make our grid to be much more in sync.

The main thing I'd like to do after all the fixes would be to do a final audit for LOC, because I find that especially ChatGPT it adds a lot extra often overcomplex logic fix things. So I often have to ask it to do a final review with a prompt like: "is that the most minimal you can do without regressing?". It often finds by itself that his logic was too complex and it could decreases LOC by a good amount... so AI is good in general but we still have to watch it sometime and avoid spaghetti code which adds LOC and we can often decrease them. I also have access to ChatGPT 6 Astra which is like Fable 5.2 but it's expensive and I only have a Pro subscriptions (25$/month), so it burns rather quickly... still I find it's important for such code because we'll use it for the years to come

I'm pretty happy with the result so far (great UI/UX), it wouldn't have been possible without AI I think and I've been working on this for the past 3 weeks, which isn't too bad.

@6pac

6pac commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Great. Happy to use Fable where possible if you want - I've just about run my Fable quota out this week but it renews every Wed. Most weeks I use 10% or less under normal use.

It's funny, I've watched the AI churn - you can turn on a detailed view where you see every thought - and it seems to spend most of its time obsessing over where to store a file, or resolving some small ambiguity in the instructions that could be easily remedied in two words if it asked you. But it just eats up well defined complex problems.

@ghiscoding

ghiscoding commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

It's the most complex feature to work on, when that is shipped, then other tasks will be much less complex and less costly on AI usage for sure. By the way, I didn't quite understand how that works for Vitepress, does it need another CI workflow for it to publish the docs (I did look at it and it's pretty), because in comparison with the service I use which is free for open source project, it just sync it to a website but with Vitepress, do we need to do a build and the use the SlickGrid github.io link as website for the docs? I assume that part isn't done, right? Also it will need an update for current PR because I saw reference in the docs for your older 3x3 Viewport Mgr approach in the docs, will need cleanup

@6pac

6pac commented Sep 20, 2026

Copy link
Copy Markdown
Owner

With the docs, honestly, I wasn't paying that much attention to them.
Here's Claude's options: it sounds like Netlify is probably the way to go, but do you have a preference?


Deploying the SlickGrid VitePress docs

Notes on how publishing works for the VitePress docs site (PR #1301), and why it differs from a "sync markdown to a website" service.

Current state of GitHub Pages

The repo's Pages is set to "Deploy from a branch: master /" (build_type: legacy). GitHub serves the repo's files raw at http://6pac.github.io/SlickGrid/ — that's how the examples are live (no build; GitHub just serves the committed files, e.g. …/SlickGrid/examples/example1-simple.html). This matters for the options below.

How VitePress differs from a "just syncs" service

A hosted docs service renders/hosts your markdown for you. VitePress is a static-site generator: the markdown + Vue components get compiled (vitepress build) into plain HTML/CSS/JS in docs/.vitepress/dist/, and that is what gets served. So there is a build step your current host doesn't need.

You don't run the build by hand each time — something automates it on every push. Two free-for-open-source ways:

Option 1 — GitHub Actions → GitHub Pages

The docs/ci/github-pages.yml workflow (already in the PR, inert). On push it runs npm ci && npm run api && npm run docs:build, then publishes dist/ to Pages.

To turn it on:

  1. Move docs/ci/github-pages.yml.github/workflows/docs.yml.
  2. Settings → Pages → switch Source from "Deploy from a branch" to "GitHub Actions".
  3. Set DOCS_BASE to match the URL path.

⚠️ Catch: that switch replaces what Pages serves, so 6pac.github.io/SlickGrid/ would become the docs and the examples would go offline (they're served from master / today). A repo has only one Pages site. To keep both, the workflow would have to bundle the examples into the deploy too (docs on a sub-path). Doable, but more moving parts.

Option 2 — Netlify / Cloudflare Pages / Vercel

This is the "connect the repo and it syncs" experience:

  • Link the repo once.
  • Build command: cd docs && npm ci && npm run docs:build
  • Publish directory: docs/.vitepress/dist

It auto-builds + hosts on every push, free for open source, on its own URL (or a custom domain/subdomain). It doesn't touch GitHub Pages at all — the examples stay exactly where they are.

Is deployment done?

No. The PR only contains the inert workflow file. Nothing is wired, Pages is untouched (still serving the examples), and the docs aren't hosted anywhere yet. This was deliberate — it's the outward-facing step to decide.

Recommendation

Given the goal of low friction and not disturbing the examples site: Cloudflare Pages or Netlify pointed at docs/ is the cleanest — auto-builds on push, free, its own URL, examples untouched.

If everything should live on github.io instead, the alternative is to rework the Actions workflow to publish both (examples at the root, docs at /SlickGrid/docs/).

Either way, DOCS_BASE gets wired to match the final URL.

@ghiscoding

ghiscoding commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

It's up to you, if you want to create account on Netlify or Cloudflare, then you'll have to look into it. But option 1 would also work just as well, the main reason I went to GitBook on my side is actually because of 2 things (1. sync docs folder with website and 2. GitHub page only offers 1 live webpage per repo and I always use it for my own examples website, so I can't use it for docs). But in your case, you're not currently using it for anything since your examples aren't a separate live demo website like I do (because yours are just part of your Wiki links and that is also why we need to keep your dist folder in GitHub because without it, your examples wouldn't work). So because you're not using GitHub Page website for anything, you could use it for the docs. As for the sync, yeah sure Netlify/Cloudflare could offer you sync, but we can also do synching by adding a CI workflow to run on every commit and that would sync just the same... Personally, since you're not using the GitHub Page for anything, I would just go with Option 1, it's simple and doesn't require any new account of any kind. Feel free to go with Option 2 if you wish though, up to you really but I've never tried them myself (so can't really on that)

If you go with Option 1, with GitHub Page, you can configure it through your repo Settings -> Pages, then I usually create a branch named "gh-pages" (that's the default name for it) and then have the CI update it by itself. Then I use peaceiris/actions-gh-pages to deploy it. I can help you with the CI workflow if you want to go with Option 1 but you'll then have to do the setting like below on your own since I don't have access to that section

image

@6pac

6pac commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Okay, will investigate sometime soon. I understand what it's saying about VitePress, but honestly, I'm not across the different site areas available in GitHub and how CI interacts with them, so I'll have to check that out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants