Skip to content

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

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

ghiscoding wants to merge 10 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.

@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 gone in this PR, I'm pretty sure that was 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 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.

@ghiscoding

Copy link
Copy Markdown
Collaborator Author

Print Screens

image image image

@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
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.

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

Projects

None yet

2 participants