Skip to content

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

Draft
ghiscoding wants to merge 124 commits into
masterfrom
feat/pinning-and-sticky
Draft

ghiscoding wants to merge 124 commits into
masterfrom
feat/pinning-and-sticky

Conversation

@ghiscoding

@ghiscoding ghiscoding commented Sep 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

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

This is an intentional v11 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:
    • Vanilla Example 47;
    • Angular, Aurelia, React, and Vue Example 58.
  • Updated Example 04 to demonstrate permanent left/right column and top/bottom row pinning.
  • Updated Header Menu pinning commands:
    • Pin Left;
    • Pin Right;
    • bulk directional pinning;
    • Unpin Column;
    • Unpin All Columns.
  • 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 v11 migration guide and pinning/sticky documentation across the supported frameworks.
  • 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.

  • Pinning-related state now uses GridState.pinning.

  • 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.
  • Angular, Aurelia, React, and Vue demo builds.
  • 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, Angular, Aurelia, React, and Vue 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 v11 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.

TODOs

  • Change merge branch to next-v11

@ghiscoding
ghiscoding marked this pull request as draft September 8, 2026 01:17
@codecov

codecov Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.0%. Comparing base (297c2b1) to head (240d1fe).
⚠️ Report is 13 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff            @@
##           master    #2782     +/-   ##
=========================================
  Coverage   100.0%   100.0%             
=========================================
  Files         199      200      +1     
  Lines       25868    27035   +1167     
  Branches     9177     9501    +324     
=========================================
+ Hits        25868    27035   +1167     
Flag Coverage Δ
angular 100.0% <ø> (ø)
universal 100.0% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new

pkg-pr-new Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
angular-slickgrid

npm i https://pkg.pr.new/angular-slickgrid@2782

aurelia-slickgrid

npm i https://pkg.pr.new/aurelia-slickgrid@2782

slickgrid-react

npm i https://pkg.pr.new/slickgrid-react@2782

slickgrid-vue

npm i https://pkg.pr.new/slickgrid-vue@2782

@slickgrid-universal/angular-row-detail-plugin

npm i https://pkg.pr.new/@slickgrid-universal/angular-row-detail-plugin@2782

@slickgrid-universal/aurelia-row-detail-plugin

npm i https://pkg.pr.new/@slickgrid-universal/aurelia-row-detail-plugin@2782

@slickgrid-universal/react-row-detail-plugin

npm i https://pkg.pr.new/@slickgrid-universal/react-row-detail-plugin@2782

@slickgrid-universal/vue-row-detail-plugin

npm i https://pkg.pr.new/@slickgrid-universal/vue-row-detail-plugin@2782

@slickgrid-universal/binding

npm i https://pkg.pr.new/@slickgrid-universal/binding@2782

@slickgrid-universal/common

npm i https://pkg.pr.new/@slickgrid-universal/common@2782

@slickgrid-universal/composite-editor-component

npm i https://pkg.pr.new/@slickgrid-universal/composite-editor-component@2782

@slickgrid-universal/custom-footer-component

npm i https://pkg.pr.new/@slickgrid-universal/custom-footer-component@2782

@slickgrid-universal/custom-tooltip-plugin

npm i https://pkg.pr.new/@slickgrid-universal/custom-tooltip-plugin@2782

@slickgrid-universal/empty-warning-component

npm i https://pkg.pr.new/@slickgrid-universal/empty-warning-component@2782

@slickgrid-universal/event-pub-sub

npm i https://pkg.pr.new/@slickgrid-universal/event-pub-sub@2782

@slickgrid-universal/excel-export

npm i https://pkg.pr.new/@slickgrid-universal/excel-export@2782

@slickgrid-universal/graphql

npm i https://pkg.pr.new/@slickgrid-universal/graphql@2782

@slickgrid-universal/odata

npm i https://pkg.pr.new/@slickgrid-universal/odata@2782

@slickgrid-universal/pagination-component

npm i https://pkg.pr.new/@slickgrid-universal/pagination-component@2782

@slickgrid-universal/pdf-export

npm i https://pkg.pr.new/@slickgrid-universal/pdf-export@2782

@slickgrid-universal/row-detail-view-plugin

npm i https://pkg.pr.new/@slickgrid-universal/row-detail-view-plugin@2782

@slickgrid-universal/rxjs-observable

npm i https://pkg.pr.new/@slickgrid-universal/rxjs-observable@2782

@slickgrid-universal/sql

npm i https://pkg.pr.new/@slickgrid-universal/sql@2782

@slickgrid-universal/text-export

npm i https://pkg.pr.new/@slickgrid-universal/text-export@2782

@slickgrid-universal/utils

npm i https://pkg.pr.new/@slickgrid-universal/utils@2782

@slickgrid-universal/vanilla-bundle

npm i https://pkg.pr.new/@slickgrid-universal/vanilla-bundle@2782

@slickgrid-universal/vanilla-force-bundle

npm i https://pkg.pr.new/@slickgrid-universal/vanilla-force-bundle@2782

@slickgrid-universal/web-mcp

npm i https://pkg.pr.new/@slickgrid-universal/web-mcp@2782

commit: 244075d

@ghiscoding

ghiscoding commented Sep 8, 2026 •

Copy link
Copy Markdown
Owner Author

Sticky Columns / Rows

Screencast_20260903_184235.webm

Pinning Columns / Rows

Screencast_20260910_223012-1.webm
Screencast_20260911_013813.webm

@ghiscoding ghiscoding added breaking change ⚠️ major version not supported until next major version labels Sep 8, 2026
@ghiscoding
ghiscoding force-pushed the feat/pinning-and-sticky branch from cefcc2e to 96b1d94 Compare September 9, 2026 13:41
ghiscoding and others added 30 commits September 22, 2026 22:20
Docking has one horizontal scroll owner: the proxy scrollbar. The row and chrome transforms in the native-scroll fallback are unreachable whenever docking is configured, because the proxy scrollbar is introduced and removed with the docking layout. Remove those helpers and their call sites, retaining the proxy CSS offset and full-width-group transform.

Remove unit assertions for the retired fallback while preserving proxy-scroll and docking-region coverage.
Three pinning validation sites shared the same alert-once behavior, differing only in callback and message. Route each rejection through one helper so callers that force an alert still get one, while render-time validation stays quiet after the first alert.
Full-width group cells already consume the published horizontal scroll offset through the docking stylesheet, so remove the per-row inline transform scan.

Group the shared pinned and sticky edge separator declarations while preserving their selector specificity and left/right shadows.

Port of 6pac/SlickGrid@d17e104.
Resolve contested sticky columns with one trailing-edge map and a leading-edge filter, avoiding repeated findIndex and splice operations while preserving edge preference and tie behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…a resize

When a column resize changes whether the proxy scrollbar reserves layout height, resize the canvas on that transition so the viewport stays flush with the track. Track the reservation through its existing dimension update and guard the resize against re-entry.

Add unit coverage for scrollbar appearance, removal, stable overflow, and the re-entry guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Firefox on Linux can report a zero-width overlay scrollbar even though the
visible scrollbar occupies a narrow strip. The docked-row overlay then clips
to the viewport edge and paints over the scrollbar in pinned sections.

Add an 8px clip inset only for Firefox/Linux when a vertical scrollbar is
present and its measured width is zero. Keep the existing geometry for other
browsers and for scrollbars with a measurable width.
Move the docked-row overlay inside the vertical viewport as a zero-height sticky layer. This lets Firefox paint its native scrollbar above pinned rows, so the scrollbar spans the pinned sections without clipping their right edge.

Remove the Firefox-specific scrollbar-width inset and clip-path workaround. Use the viewport’s height for docked-row synchronization and bottom-row positioning, since the overlay itself no longer has height.

Co-authored-by: ChatGPT <noreply@openai.com>
Resolve pinned and sticky columns along the logical inline axis so leading and trailing bands mirror correctly in RTL. Keep docked rows and proxy scrolling aligned with the RTL viewport.\n\nDemonstrate pinned columns and top/bottom docked rows in the Vanilla and framework RTL examples, and document the mirrored pinning semantics.
…eation

Demonstrate a colspan crossing from the leading pinned band into scrolling columns across Vanilla and framework RTL examples, with coverage for its seam and hit-testing.\n\nIgnore runtime rtl changes in setOptions so the initialized DOM direction cannot diverge from docking geometry.
Prevent transformed header content from appearing above the vertical scrollbar when pinning or sticky columns are enabled.
The grid now measures the body scrollbar width and overlays that strip across the affected header containers. Added regression coverage and documentation.
Synchronize body-cell geometry and sticky transforms while resizing columns so header and cell positions remain aligned without requiring a scroll event.

Preserve Grid Menu scrollbar-cover behavior and add regression coverage for sticky column resizing.
… grids

in a right-to-left grid the separator of a
pinned band was on the wrong side of its edge column, so the band looked one
column off.

Every docking separator is an inset box-shadow with a physical offset: -1px
draws it on the right edge and +1px on the left, which is the inner edge only
when reading left to right. The offsets are now multiplied by
--slick-docking-direction, which the grid already sets to 1 or -1, so the
separator faces the scrolling columns in both directions. Left to right, the
computed values do not change.

A docked sticky column becomes the band edge, and its header cell gets
slick-column-sticky-left-edge or -right-edge. No rule styled those classes, so
the header lost its separator when a sticky column docked, in both directions.
They now share the header rules of the pinned edges.

The new case in example-pinning-rtl.cy.ts checks that every separator faces the
scrolling columns, before and after the sticky column docks. It fails on
7d9555c and passes with this change. example-sticky-financial-report.cy.ts
also checks the header separator at the initial viewport.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restore fork-compatible band-relative positioning for colspan continuation fragments in LTR and RTL grids. Remove the extra proxy transform that caused fragments to drift while scrolling, and update pinning unit expectations.
Description: Skip grouped-header synchronization when the horizontal offset is unchanged, and reuse pinned-column width measurements until the rendered layout changes or columns are resized. Remove a duplicate pinning-menu cleanup call. Add focused coverage for scroll and resize behavior.
Validation: Header grouping spec passes (16 tests); line coverage is 100%. Prettier and diff checks pass.
…mn docks

In the body, the last permanently pinned cell always draws the band's
separator, and a docked sticky cell draws its own further in. The header
instead moved its one separator from the pinned column to the sticky column,
so with a sticky column docked next to permanent pins the body showed two
lines and the header one.

The header's pinned edge is now the last permanently pinned column of each
band, as in the body. It no longer changes while scrolling, so
updateStickyColumnTransforms() no longer needs the loop that moved it.

The new case in example-pinning-rtl.cy.ts checks that every header separator
lines up with a body separator, before and after the sticky column docks. It
fails without this change: 2 header separators against 3 in the body.

The far-right test in example-sticky-financial-report.cy.ts asserted the state
from before the scroll. Q3, Q4 and YTD are sticky at scroll 0, and cy.scrollTo()
may not deliver a scroll event before the assertions run. The test now checks
the far-right state, which is false before the scroll: Q3, Q4 and YTD are
released, and Q2 is the edge of the left band, with its separator in the header
and in the body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…says

A permanently pinned column that also carried `sticky` was routed onto the sticky transform path by sites that read the column flag alone, while the docking layout kept it in its permanent band. Its header, filter cell, and body cells must all stay in that band. One predicate, isStickyTransformColumn(), now decides the path for cells, chrome, region sync, and sticky transforms: a pinned column is never a sticky candidate.

The focused unit regression covers boundary pinning, column pinning at runtime, and the rendered body and header. It fails on the previous implementation for those cases.
…hidden-column scan

scrollToX() now publishes the shared horizontal offset variable directly; the per-column proxy transform writes had no effect because the stylesheet consumes that variable.

updateColumnPositionCaches() now resolves each hidden-column block once instead of rescanning it for every hidden column. Remove the class-keyed measurement cache, which did not hit because filter and footer cells have per-column classes, and remove the unused frozen-pane height fields. appendColspanFragments() reuses the row cached regions.

The pinning unit test verifies that scrolling updates the shared variable without rewriting pinned header or filter transforms.

This branch has not been deployed

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

Labels

breaking change ⚠️ major version not supported until next major version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants