diff --git a/.github/tasks/FORMATTED_DATA_CACHE_IMPLEMENTATION.md b/.agents/plans/formatted-data-cache-implementation.md similarity index 100% rename from .github/tasks/FORMATTED_DATA_CACHE_IMPLEMENTATION.md rename to .agents/plans/formatted-data-cache-implementation.md diff --git a/.agents/plans/pinning-sticky-progress.md b/.agents/plans/pinning-sticky-progress.md new file mode 100644 index 0000000000..ce96db9ea6 --- /dev/null +++ b/.agents/plans/pinning-sticky-progress.md @@ -0,0 +1,1119 @@ +# Single-viewport pinning/stickiness — implementation progress + +Last updated: 2026-09-15 (Firefox/Linux overlay-scrollbar findings and visual fixes, profiler-guided scroll-offset optimization, minCenterRowCount resize fix, and user-confirmed green Vanilla/framework Cypress CI) + +## Goal + +Replace SlickGrid's multi-pane column/row architecture with an AG Grid-style docking model: + +- performance is a highest-priority invariant: preserve smooth scrolling and rendering efficiency, + especially with very large datasets (500K+ rows), and avoid per-scroll layout, DOM, or style work; +- exactly one live body viewport with one native vertical scrollbar; ordinary grids use the + viewport for horizontal scrolling, while pinning/sticky grids use one dedicated docking + horizontal scrollbar; +- one virtualized DOM row per data row; +- each rendered row contains stable sibling left, center, and right cell regions; +- permanent pinning and scroll-activated stickiness use the same internal docking resolver; +- vertical and horizontal virtualization must remain viable for large datasets; +- this is intentionally a major-version breaking change; compatibility with the old pane renderer is not a design goal. + +## Accessibility audit (2026-09-15) + +The pinning/sticky renderer was audited for semantic-tree integrity, keyboard navigation, and +ARIA handling. No pinning/sticky-specific semantic regression was found in the current scope. + +- **Pass:** The grid keeps one semantic `grid`/`row`/`gridcell` tree. Left/center/right docking + wrappers and the row overlay use `role="presentation"`, so visual docking layers do not create + duplicate rows or cells for assistive technology. +- **Pass:** Docked rows reuse the existing row node rather than cloning it. Overlay event binding + covers keyboard, click, double-click, and context-menu interactions. +- **Pass:** Cross-band colspan/rowspan hosts expose `aria-colspan`/`aria-rowspan`; visual + continuation fragments are `aria-hidden="true"`, `role="presentation"`, and not focusable. +- **Pass:** Sticky keyboard navigation reveals a candidate's natural position before activating + it, and sticky summary rows remain keyboard-addressable after vertical scrolling. Focused + coverage exists in the Vanilla, Angular, Aurelia, React, and Vue sticky Example 58 suites. +- **Verified:** The focused common tests passed: 51 pinning tests and 19 targeted ARIA, + accessibility, docking-wrapper, and colspan tests in `slickGrid.spec.ts`. +- **Coverage gap:** The repository has no automated axe/WCAG integration for these demos, and no + screen-reader session was run. The audit therefore verifies DOM contracts and keyboard behavior, + not complete assistive-technology compatibility. +- **Resolved (minimal):** Virtualized/docked rows and cells now expose `aria-rowindex` and + `aria-colindex`, preserving their logical dataset and column positions through non-contiguous + pinning, band reordering, and docking-overlay moves. Visual colspan fragments omit the index. +- **Resolved (minimal):** The dedicated docking horizontal scroller is keyboard-focusable and + labelled `Horizontal grid scroll`. It remains the browser's native overflow control rather than + a custom `role="scrollbar"`; screen-reader behaviour still needs manual validation. + +The implementation supports per-column pinning and the canonical nested `pinning` option. +`pinning.columns.left` accepts an inclusive edge-boundary number for contiguous +left pinning, while `pinning.columns.right` accepts a count from the trailing +edge. Either side also accepts arrays of stable column ids/indexes for +non-contiguous pinning. An inclusive v11-and-lower boundary is written as +`pinning.columns.left: 2`; users do not need to expand it into an index array. +Legacy option names are documented only in the v11 migration guide. +There is no separate `pinnedColumn` or `pinnedRows` grid option; those temporary +aliases were removed after the canonical shape was wired through core and state. +The implementation does not target compatibility with the old pane-based UX. + +For ordinary colspans that cross docking bands, pinning is accepted only when the +resolved bands remain sequential (`left → center → right`). A non-sequential +change such as pinning the second column while leaving the first column in the +center is rejected through `invalidColumnPinningPickerCallback`; the default +message can be customized with `invalidColumnPinningSequenceMessage`. This +validation runs during pinning changes and does not add work to horizontal +scrolling. + +The canonical grid-state shape is now a single nested `GridOption.pinning` object: +`{ columns: { left, right }, rows: { top, bottom } }`. `Column.pinned` remains the +per-column representation. `GridService.setPinning()` and `GridStateService` now read +and write the unified shape; sticky configuration remains separate because it has +different scroll-activated semantics. `CurrentColumn.pinning` also carries the +per-column side in column layouts, providing a hybrid preset representation for +consumers that do not want to persist a separate aggregate pinning object. +`Column.pinnable` defaults to `true`; setting it to `false` prevents Header Menu pinning changes +for protected columns while leaving programmatic pinning available. Sticky columns do not expose +Header Menu commands, so there is no separate `Column.stickable` option. + +Vanilla Example 11 serializes the new nested `CurrentPinning` shape in its +saved views and intentionally enables the pinning header commands to +exercise the new behavior. The single-column menu action calls +`SlickGrid.setColumnPinning` and updates `Column.pinned`; the bulk “Pin +Columns” menu action updates +`pinning.columns.left`, which applies the same left pins through the unified +pinning resolver. Neither action uses removed legacy options or validation. +The `headerMenu.showPinningCommands` option controls whether the Header Menu exposes these +commands, while defining `pinning` automatically enables the same UI for +declarative pinning configurations. Individual command visibility is handled by +`headerMenu.hideCommands`. + +Vanilla Example 04 and the Angular, Aurelia, React, and Vue Example 20 fixtures mark +`City of Origin` as `pinnable: false`; their Cypress suites verify that its Header Menu omits +the `Column Pinning` commands while programmatic right pinning remains available. + +Sticky usage is documented separately in `docs/grid-functionalities/sticky.md` and the matching +framework documentation, with links from each pinning guide and documentation TOC. + +The sticky financial-report fixture from Vanilla Example 47 is also available as Example 58 in +the Angular, Aurelia, React, and Vue demos. Each framework route includes the same 18-column +report, two-sided sticky columns, sticky summary rows, docking budgets, and a focused Cypress +smoke test. The recent `Column.pinnable` behavior is covered by Vanilla Example 04 and all +framework Example 20 equivalents. + +All available pinning locale assets and translation stubs were reviewed. The French singular +`PIN_COLUMN`/`TEXT_PIN_COLUMN` label is now `Épinglage de colonne`, matching the singular +`UNPIN_COLUMN` label; plural bulk actions remain plural. The English `Column Pinning` text is +intentionally retained as the Header Menu root label. + +The Header Menu now exposes a `pin-column` root command displayed as `Column Pinning`. Its +sub-menu contains three command groups: `pin-left`/`pin-right`, +`pin-columns-left`/`pin-columns-right`, and `unpin-column`/`unpin-columns`, with separators only +between groups that still contain visible commands. The first group sets the selected column's +`Column.pinned` side, the bulk directional commands write the corresponding aggregate left/right +boundary, and the unpin commands clear the selected column or all aggregate column edges. +Setting `pinnable: false` removes the `Column Pinning` menu for that column and excludes it from +bulk pin-through operations. None of these commands recreates the old two-pane layout. + +Pinning is opt-in in the Header Menu through `headerMenu.showPinningCommands`, which defaults to +false when no `pinning` state is supplied, or automatically when the `pinning` option is defined. +Applications set `headerMenu.showPinningCommands: true` when they want the `pin-column` root command before any pin state is +configured. Explicit `headerMenu.showPinningCommands: false` keeps pinning programmatic-only, and use +`headerMenu.hideCommands` only for individual command visibility. The former dedicated +`hidePinningColumnsCommand` and `hidePinColumnCommand` options are removed rather than carried +forward into v11. + +The pinning Header Menu uses the directional labels +`pinningColumnsLeftCommand` and `pinningColumnsRightCommand`; the former generic +`pinningColumnsCommand` and `pinningColumnsCommandKey` compatibility aliases are removed because +the pinning API is still unreleased and the directional commands are the complete v11 design. + +Horizontal scrolling now uses the browser's native `WheelEvent` pixel deltas for trackpads and +physical horizontal-wheel mice. Legacy horizontal-wheel clicks advance by at least 40px instead +of the old 10px increment, while Shift+wheel falls back to the vertical delta when needed. In +docking mode a scroll event applies compositor transforms once rather than twice, and horizontal +virtual-cell rendering is coalesced on `requestAnimationFrame`. Sticky-column band resolution +uses the same frame cadence, keeping Vanilla Example 47's sticky transitions responsive without +performing repeated resolver/render work during a rapid horizontal scroll. The financial-report +examples also reuse one `Intl.NumberFormat` instance instead of allocating one per rendered cell. +Unchanged sticky passes now preserve the active layout/map, per-scroll updates no longer rewrite +invariant docking offsets, and the moving sticky-row clip is compositor-promoted. + +### Firefox/Linux scrollbar and scroll-linked-effect notes (2026-09-15) + +Firefox on Linux may use GTK overlay scrollbars. In that mode the scrollbar can be hidden until +the grid is hovered, can appear as an overlay before the track is hovered, and can report zero +width/height through DOM scrollbar measurements. This is browser/desktop scrollbar policy, not a +missing SlickGrid scroll owner, and library CSS cannot force the user's Firefox scrollbar +preference to become permanently visible. The docking proxy therefore uses a 15px fallback +height when Firefox reports zero, while retaining measured dimensions everywhere else. + +When vertical overflow exists but Firefox reports a zero scrollbar width, the docked-row overlay +clips an 8px trailing strip so the overlay scrollbar cannot paint behind top/bottom pinned rows. +The last right-pinned filter/footer cell also restores the Grid Menu allowance in this zero-width +case, preventing an adjacent center filter from showing through the `Action` column. These +fallbacks are metric-based and are not Firefox user-agent branches. + +Firefox may also log its standard [“scroll-linked positioning effect” warning](https://firefox-source-docs.mozilla.org/performance/scroll-linked_effects.html). CSS `position: sticky` +for the ordinary left-pinned region is compositor-aware; the warning specifically reflects the +JavaScript scroll listener that synchronizes the dedicated horizontal proxy with the sibling +canvas, overlay, and chrome transforms/clip updates. This diagnostic is expected for the current +single-proxy architecture and is not an application exception. Async panning can still make this +path feel different across browsers, and Firefox/Safari should be validated manually where +available. The implementation keeps the scroll path compositor-oriented and does not attempt to +suppress the browser warning. + +The Firefox profile supplied for Example 04/47 showed only a small JavaScript scroll-handler +cost, but 18 scroll-triggered style passes restyled 386 descendants each (156.8ms total, +8.7ms average, 20.2ms maximum). The cause was the inherited per-scroll +`--slick-docking-scroll-left` value on the grid root. The optimization registers that property +as non-inheriting, updates it only on moving docking targets, and writes the overlay `clip-path` +directly. This preserves the stable DOM/compositor design for Chrome, Firefox, and Safari without +user-agent detection. Focused tests and static checks pass; manual Firefox held-scroll and +resize/scroll feel confirmation remains the final performance check. + +A follow-up Firefox capture from the localhost Example 04 tab confirms the profiler signature +improved: the previous 18 style passes traversing/styling 386 elements (156.8ms total, 20.2ms +maximum) are gone. The new capture has 15 larger style passes traversing 121 elements and styling +77 (59.0ms total, 3.9ms average, 7.7ms maximum). Refresh-driver work also improved in this +capture (3 frames over 16.7ms versus 7 previously). These captures are separate sessions, so +they are directional rather than a controlled benchmark, but they confirm that the full-grid +inherited-property restyle was removed. Manual perceived-smoothness validation remains useful. + +A horizontal-wheel mouse (a second, dedicated tilt/horizontal wheel, as opposed to Shift+wheel) +could push `scrollLeft` below zero because `handleMouseWheel` added the raw wheel delta without a +floor and `_handleScroll` only ceilinged `scrollTop`/`scrollLeft` against their max scroll +distances without flooring either at zero. A negative `scrollLeft` produced a negative +`--slick-docking-scroll-left` custom property, which showed up as a white gap on the left side of +pinned/docked examples (e.g. vanilla Example 04) along with misaligned pinned-right columns. +Both `handleMouseWheel` and `_handleScroll` now floor `scrollLeft` (and `scrollTop`) at zero. + +Full-span group headers now render as one viewport-wide row above all three docking regions, matching +the group-row model used by AG Grid: pinned columns still clip ordinary data rows, but group labels +remain fully visible across the grid. Ordinary cells (including injected row-selection checkboxes) +and group-total cells remain in their resolved bands. This fixes the blank/misaligned left side +described by the long-standing SlickGrid grouping-plus-frozen-columns issue. + +HeaderGroupingService pre-header titles are also split at docking boundaries and rendered in the +same left/center/right band order as the column headers. A group such as `Period` therefore gets +separate correctly aligned title segments when `Start` is pinned and `Finish` remains scrollable. +Unchanged pre-header layouts are now identified by their dimensions, visible column groups, and +docking bands so ordinary grid renders do not destroy and recreate identical grouped-header DOM. + +Draggable Grouping now creates a Sortable source for the center header band in addition to the left +and right bands, so dragging a scrollable column into the grouping dropzone continues to work with +either edge pinned. Focused full-span group cells also retain their full viewport width and remain +above the pinned-band backgrounds instead of hiding their group label. +The three Sortable source instances share one cleanup loop, and column-width application resolves +the rendered center width once per pass instead of once per column. The related cell-render branch +also no longer evaluates a duplicated docking-band predicate. + +Vanilla Example 03 Cypress coverage now pins a right column temporarily and verifies split +pre-header titles, center-band grouping drag/drop, viewport-wide active group rows without pinned +separator cells, ordinary left/right separator overlays, and matching odd-row backgrounds across +all three row regions before restoring the original right-pin state. + +The equivalent framework Example 18 Cypress suites now cover the same grouping/pinning contract +using their native column set: left and right pinning, split `Period` pre-header bands, grouping a +center column, viewport-wide active group rows, pinned-band separators, matching odd-row backgrounds, +and clearing pinning after the check. + +Full-width group rows no longer paint left/right pinned separators through the group label. Regular +rows retain their existing pinned-band separators. Pinned edge filter/footer cells use their header +title's measured outer width but no longer extend into the vertical-scrollbar gutter; this keeps the +header chrome aligned and prevents a right-edge filter such as `Effort-Driven` from overlapping its +neighboring `Action` cell. + +Ordinary colspans that cross left, center, or right docking bands now keep one logical/content host +cell and render lightweight visual continuation fragments in each affected band. Fragments share +the host's styling but are excluded from logical-cell caching and are removed/rebuilt with the host, +so formatters, selection, and virtualization continue to operate on one cell. Clicking any +fragment activates the complete span; keyboard arrows continue to navigate between logical cells, +skipping continuation fragments. The docking separator is suppressed only at an internal colspan +split, so the span remains visually continuous while real outer docking boundaries keep their cue. + +Docked body cells now calculate center-band right offsets from the rendered center-region width when +left/right pinning expands that region to the viewport. This prevents the last remaining center +cell, such as `Action` after hiding `Finish`, from stretching away from its header. Vanilla Example +03 Cypress coverage compares the header and body bounds for this case. + +Removed the old Angular Example 14/20 last-pinned-cell `border-right` override so it cannot add a +second separator beside the docking pinning cue. The same stale override was removed from the +equivalent Aurelia, React, React Fluent, and Vanilla Example 17 demo styles. + +Header columns now rely exclusively on their existing flex root and `flex: 0 0 auto`; the obsolete +column-level inline-block and LTR/RTL float declarations were removed after the old ±1000px header +offset disappeared. Vanilla Example 42 and framework Example 53 Cypress coverage verify flex +layout, `float: none`, and the configured `--slick-header-row-count`, while Example 33 retains +auto-header-height coverage. + +The implementation has gone through visual hardening, selected Cypress migration, framework demo parity, +and removal of the legacy pane options/interfaces and runtime branches. The common unit suite, +focused coverage checks, and user-confirmed Vanilla/framework browser CI workflows pass. The +remaining legacy terminology is limited to historical CSS variable names and intentional +migration-facing documentation. + +## Refactoring status and immediate follow-up + +The legacy option/interface/state/service branches have been removed from the runtime +implementation. The current code no longer defines or reads the former flat pinning +configuration or its legacy state fields. + +A structural cleanup of the internal `_viewport*` and `_canvas*` aliases is complete: the +single live nodes are now `_viewportNode` and `_canvasNode`. The former `_pane*` fields and +`.slick-pane*` classes have been removed; they did not create additional panes in the current +implementation. The Migration documentation retains the old theme variable names as v11-and-lower +references, while the active stylesheet now uses `--slick-pinned-*`. Old command +IDs, locale keys, and demo selectors are removed from active examples/runtime and remain only in +migration docs where needed. Do not reintroduce legacy runtime branches. + +The production LOC estimate below has been recalculated after the alias/style audit. + +## Maintainability acceptance gate + +The original PR 1238 motivation was reviewed as part of this work: multi-pane layouts +made `slickGrid.ts` harder to maintain because ordinary operations had to know about left/right +headers, footers, viewports, and canvases. The single-viewport rewrite is successful only if it +removes that model; it is **not** sufficient to create the old panes and force their options +off at runtime. + +The final implementation must satisfy all of the following: + +- construct one live header, header-row, footer-row, viewport, and canvas; the remaining pane- + shaped fields must be aliases only and must not become separate DOM/scroll containers; +- make ordinary header/footer creation and column-element lookup direct single-container + operations, without legacy pane target selection; +- keep the old option fields, state/menu/service plumbing, synchronized-scroll branches, + and resize branches deleted; intentionally retained migration-facing command IDs, locale + wording, and theme variable names must not turn into compatibility code. The obsolete + `-1000px` header-container offset is also deleted; +- keep pinning-specific behavior in the DOM-free `DockingController` plus a small docking DOM + layer that applies per-row left/center/right regions and pinned chrome offsets; +- remove the obsolete `HEADER_WIDTH_SLACK`/`1000px` header-coordinate workaround as part of the + rewrite; header titles, grouped headers, and header regions now use ordinary coordinates; +- keep the neutral viewport/canvas node names so the old `L`/`R` pane model cannot leak back into + normal code; +- complete sticky docking or remove any temporary feature-flag path; sticky docking is now + implemented through the shared controller and renderer path, with no dormant feature flag. + +Do not revive a `ViewportMgr` merely to conceal the old multi-pane renderer. In this design, +deleting the multi-pane renderer is simpler and better aligned with the major-version breaking +change. The single-renderer acceptance gate is satisfied; optional reduction of remaining +compatibility aliases is recorded below as a maintainability follow-up. + +## leftover TODOs identified by user +- [x] Unified grid options support pinning (left, right, top, bottom) +- [x] Header Menu exposes a `Column Pinning` sub-menu with `Pin Left`, `Pin Right`, directional `Pin Columns` commands, and `Unpin Column`/`Unpin All Columns`; separators are added only between visible command groups +- [x] `CurrentColumn.pinning` provides a per-column Grid State/Preset representation alongside aggregate `GridState.pinning` +- [x] Row/body/header/footer docking regions have a predictable left/center/right DOM shape. Row + regions use the compatibility-oriented names `.slick-pinned-left-cells`, + `.slick-scrolling-cells`, and `.slick-pinned-right-cells`; they are per-row regions, not old + full-height panes or independent scroll containers. Header, header-row, and footer regions use + `.slick-*-columns-left/center/right` wrappers. +- [x] The optional `.slick-docking-overlay` is not created for a grid without row pinning or + sticky-row configuration. Once row docking is configured, the overlay remains a stable row layer + even when no row is currently active. +- [x] Rowspan stacking was reviewed for the docking overlay. The spanning cell retains its own + elevated z-index while the host row keeps normal stacking, and active rowspan rows no longer + receive padding that can clip the span. +- [x] Restored the original `.slick-viewport` horizontal scroll element for ordinary grids. The + active horizontal scroll element always receives the generic `.slick-horizontal-scroller` + class: ordinary grids apply it to `.slick-viewport`, while grids with pinning/sticky docking + apply it to `.slick-docking-horizontal-scroller`. The docking-specific class remains available + for code that needs to identify the docking scrollbar. +- [x] Added `.slick-vertical-scroller` as the stable selector for the native vertical scroll + element. It currently points to the single `.slick-viewport` in all grid configurations. +- [x] Pinning validation now uses the canonical `invalidColumnPinning*` and + `skipPinningValidation` options. Requests that pin every visible column or whose permanent + left/right bands consume the viewport are rejected and preserve the previous state. +- [x] Header regions expose `.slick-header-columns-left/center/right` (and equivalent header-row/ + footer-row classes), so consumers can identify each region without relying on removed pane roots. +- [x] Sticky keyboard navigation now scrolls to a candidate's natural position before activating + it, so ArrowRight does not unexpectedly jump from a center cell into a docked sticky cell. + Example 47 Cypress coverage also verifies sticky summary rows remain keyboard-addressable. +- [x] Added dedicated right-pinning Cypress coverage to Vanilla Example 04, including multiple + right columns, chrome alignment, scrolling, dynamic disable/re-enable, and edge removal. +- [x] Root context menus are clamped to the visible grid container when a target cell is outside + the viewport, preventing the accessibility sub-menu tests from opening the menu off-grid. +- [x] Audited legacy configuration names. The former flat pinning options are removed from + runtime code; historical references remain only in the + migration guide and documented theme-variable compatibility notes. +- [x] Addressed the curated-skills suggestion for the pinning/sticky feature by adding the + repository-shipped `.agents/skills/pinning-sticky/SKILL.md` guidance and registering it in the + repository skills index: + > I think the major version would indicate this well enough. yeah its a bit more than a break, its a feature deprecation sort of, but the replacement is subjectively better for me. + > what the latest push in AI development made me think of though is that we might should start thinking about shipping curated skills along with the library. that would serve two purposes. first, LLMs would know better how to apply specific features from slickgrid on the consumer end. but secondly, the skills could also act as a verification of the docs and thus overall improve the development of new features as LLMs could check up on skills when touching existing features +- [x] Identify and document breaking changes in the v11 migration guide, including canonical + pinning, sticky docking, `pinnable`, removed legacy options, and Header Menu terminology. +- [x] Reviewed and documented the pinning impact on Grid State and Presets. `GridState.pinning` + uses the canonical nested shape, `CurrentColumn.pinning` preserves granular column sides, + Vanilla Example 11 persists/restores pinning, and both the Grid State/Presets guide and v11 + migration guide document the saved-state migration. Example 11 Cypress coverage now asserts + the persisted nested pinning payload. Sticky configuration remains option-based because active + sticky membership is scroll-dependent and is intentionally not serialized. +- **COMPLETED MAJOR CLEANUP:** removed the legacy grid options, public interfaces, + runtime validation names, state/service plumbing, old multi-pane behavior, and redundant + viewport/canvas aliases across `SlickGrid`, GridState/GridService, header grouping, resizer, + extensions, and framework integrations. Remaining historical CSS/demo terminology is + intentional; do not add compatibility branches. + +## Consolidated remaining work before declaring v1 complete + +All required v1 production behavior, policy decisions, focused tests, documentation, and +user-confirmed Vanilla/framework Cypress validation are complete. The items below are retained +for transparency, but are optional validation, maintainability cleanup, or intentionally separate +future work; none currently requires a pinning/sticky runtime change. + +- [x] Removed the unused `priority` overflow strategy. It was never requested and had no + priority metadata or callback. A future release may add explicit priority support if users ask + for hierarchy-aware sticky selection. +- [x] Changed `clamp` so it never selects a candidate larger than the remaining pixel budget; + oversized sticky candidates remain in their normal scroll flow. +- [x] Added focused resolver coverage for oversized sticky candidates; they are skipped when they + exceed the remaining pixel budget. A new cross-framework demo is intentionally deferred because + this is an overflow-policy edge case, not a separate user-facing feature. +- [x] Added focused resolver coverage for simultaneous top/bottom sticky stacks. The v1 rule is + one shared total budget after permanent rows, with the top stack resolved first; the bottom + stack uses the remaining space. +- [x] Added focused coverage for sticky rows with variable heights, including measured offsets, + top/bottom budget sharing, and candidates that remain in the center when they do not fit. +- [x] Added focused coverage for a sticky row containing a colspan/rowspan across docking + regions. Cross-band permanent-pinning spans remain covered; no new demo is needed for this + uncommon combination. +- [x] Added dedicated coexistence coverage for permanent pins and sticky docking across both axes. + Permanent and scroll-activated bands retain their respective positions and offsets. +- [x] Resolved permanent pinned-row overflow semantics: permanent rows always remain pinned and + part of the dataset height, even when their combined height exceeds the configured budget. + Sticky rows use the remaining space and remain in normal flow when no space remains. +- [x] Fixed a permanent top/bottom row overlap bug reported against Example 04: `maxRowViewportHeightPercent` + only ever budgeted *sticky* rows (`applyBudget()` in `DockingController.resolveRows()`); permanent + `pinning.rows.top`/`bottom` rows have no budget and always render in full, by design. The bottom + band's screen position was computed as `viewportHeight - bottomHeight`, with no floor, so shrinking + the browser below `topHeight + bottomHeight` moved the bottom band above the bottom edge of the top + band, visually overlapping/cutting off rows instead of degrading gracefully. `SlickGrid.applyRowTopOffset()` + now anchors the bottom band at `Math.max(topHeight, viewportHeight - bottomHeight)` so the two permanent + bands never overlap; when there truly is not enough height for both, the bottom band is pushed down + and its own trailing rows are clipped at the viewport edge instead. There is intentionally no + automatic reduction of the number of pinned rows and no console warning (unlike the analogous + `invalidColumnPinningWidthCallback` used for columns) — reducing `pinning.rows.top`/`bottom` counts, + or ensuring the grid has enough height for its configured pinned rows, remains the consumer's + responsibility. +- [x] Decided hierarchical sticky-row push-off/priority behavior is a separate future product + feature, not part of v1. v11 uses natural-order stacking plus conveyor/clamp overflow. +- [x] Fixed `docking.minCenterRowCount` end-to-end for auto-resized Vanilla grids. The grid still + clears and recomputes its `min-height` budget during `resizeCanvas()`, but `getViewportHeight()` + now measures the effective rendered container height (the larger of inline `height` and the + `getBoundingClientRect().height` produced by `min-height`). This lets the expanded container + size the child viewport correctly instead of continuing to calculate from the smaller inline + height that `ResizerService.resizeGridWithDimensions()` writes on each pass. The controller's + required defaults also now include `minCenterRowCount: 3`, fixing the strict TypeScript build. + Unit coverage remains in `slickGrid-pinning.spec.ts`; the user confirmed the live Example 04 + UI now reserves the center rows. Note for future debugging: a watch server that stops rebuilding + after a TypeScript error can make this fix appear absent until the compile error is resolved. +- [x] A post-Firefox cleanup inlined the single-use overlay-scrollbar-width fallback and simplified + the proxy scrollbar-height fallback without changing their metric-based behavior (`-5` production LOC). +- [ ] Optional validation: run targeted UX trials for sticky-row transitions, fast scrolling, and + changing visible sticky sets. CI verifies correctness, while manual trials can assess feel and + transition comfort. +- [ ] Separate virtual-rendering task: revisit fast vertical-scroll blanking after pinning/sticky + work is merged. This includes auditing the row-docking synchronization that still runs during + vertical scrolling; it is not part of sticky activation correctness. +- [x] Removed the five verified redundant right-side header aliases + (`_headerScrollerR`, `_headerR`, `_headerRowScrollerR`, `_headerRowR`, `_headerRowSpacerR`), + reducing the production implementation by 14 net LOC. The widely used one-item arrays remain + unchanged because they still represent the active single-viewport collections. +- [x] Removed the remaining unused single-viewport pane aliases and duplicate footer/pre-header + references, including dead group-header fields and top-panel aliases. This reduced the + production implementation by an additional 31 net LOC while preserving the public pre-header + right-panel getter and existing one-item collections. +- [ ] Separate future feature: support grouped sticky header bands, such as a quarterly group + header spanning several columns. This would require approximately 150–300 additional library + LOC and explicit cross-band and push-off rules; ordinary sticky columns do not require it. +- [ ] Deferred documentation: add framework-specific v11 migration guides if the release requires + them. The root migration guide is current, and the framework guides are intentionally deferred. + +## Starting point + +- Branch: `master` +- Base commit: `e757539c2` +- Worktree was clean before this implementation. +- The earlier `feat/viewport-mgr`/PR 1238 approach was inspected but not reused because it extends the old full-height pane architecture. +- GitHub Discussion 1237 was reviewed for arbitrary sticky rows/columns, pixel budgets, overflow policies, variable row heights, and hierarchical sticky-row semantics. +- AG Grid v36's single-scroll DOM change was used as the structural reference. + +## Implemented architecture + +### One live scroll viewport + +`SlickGrid.activateSingleViewportLayout()` configures the public/internal active collections to +one live viewport and one live canvas: + +- `_viewport = [_viewportNode]` +- `_canvas = [_canvasNode]` +- the active header/header-row/top-panel/footer collections likewise contain only their left/single instance. + +The viewport and canvas are represented by neutral node fields; they do not create separate DOM +panes or own additional scrollbars. + +### Per-row left/center/right regions + +In the single-viewport docking renderer, every rendered row has this shape, including grids +with no active pinned columns (the side regions are then empty and have no active separator): + +```html +
The grid below uses the optional resize by cell content (with a fixed 1000px for demo purposes), you can click on the 2 buttons to diff --git a/demos/vanilla/src/examples/example14.ts b/demos/vanilla/src/examples/example14.ts index 1cbc44ed1c..9757dd6bc5 100644 --- a/demos/vanilla/src/examples/example14.ts +++ b/demos/vanilla/src/examples/example14.ts @@ -104,6 +104,7 @@ export default class Example14 { { value: 3, label: 'Complex' }, { value: 4, label: 'Very Complex' }, ]; + subTitleStyle = 'display: block'; get slickerGridInstance() { return this.sgb?.instances; @@ -1093,4 +1094,9 @@ export default class Example14 { Type: ${item.itemTypeName === 'I' ? 'Item' : item.itemTypeName === 'C' ? 'PdCat' : 'Cat'}
Grid - using enableExcelCopyBuffer which uses SlickHybridSelectionModel
The complete first row and the cells C - E of the second row are not allowing to paste values.
diff --git a/demos/vanilla/src/examples/example19.ts b/demos/vanilla/src/examples/example19.ts index d43a1d64de..cb791d26f5 100644 --- a/demos/vanilla/src/examples/example19.ts +++ b/demos/vanilla/src/examples/example19.ts @@ -27,6 +27,7 @@ export default class Example19 { sgb: SlickVanillaGridBundle; isGridEditable = true; enableMultiSelection = false; + subTitleStyle = 'display: block'; attached() { this._eventHandler = new SlickEventHandler(); @@ -273,4 +274,9 @@ export default class Example19 { // remove unsaved css class from that cell this.sgb.slickGrid?.removeCellCssStyles(`unsaved_highlight_${[column.field]}${row}`); } + + toggleSubTitle() { + this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block'; + this.sgb.resizerService.resizeGrid(); + } } diff --git a/demos/vanilla/src/examples/example22.html b/demos/vanilla/src/examples/example22.html index 208fca6467..eef4118eb4 100644 --- a/demos/vanilla/src/examples/example22.html +++ b/demos/vanilla/src/examples/example22.html @@ -1,6 +1,9 @@
excelExportOptions.valueParserCallback
groupTotalsExcelExportOptions.valueParserCallback instead.
diff --git a/demos/vanilla/src/examples/example23.ts b/demos/vanilla/src/examples/example23.ts
index 208b27109a..0f0b991fa9 100644
--- a/demos/vanilla/src/examples/example23.ts
+++ b/demos/vanilla/src/examples/example23.ts
@@ -105,6 +105,7 @@ export default class Example23 {
excelExportService: ExcelExportService;
isDataGrouped = false;
taxRate = 7.5;
+ subTitleStyle = 'display: block';
constructor() {
this.excelExportService = new ExcelExportService();
@@ -515,4 +516,9 @@ export default class Example23 {
this.sgb?.dataView?.refresh();
}
+
+ toggleSubTitle() {
+ this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block';
+ this.sgb.resizerService.resizeGrid();
+ }
}
diff --git a/demos/vanilla/src/examples/example26.html b/demos/vanilla/src/examples/example26.html
index a8c5ebdf9e..90cc437a2a 100644
--- a/demos/vanilla/src/examples/example26.html
+++ b/demos/vanilla/src/examples/example26.html
@@ -1,6 +1,12 @@
NOTES:
This page demonstrates rowspan is an opt-in feature, because of its small perf hit (it needs to loop through all row metadatas to
diff --git a/demos/vanilla/src/examples/example32.ts b/demos/vanilla/src/examples/example32.ts
index b18f9efd59..11601a2689 100644
--- a/demos/vanilla/src/examples/example32.ts
+++ b/demos/vanilla/src/examples/example32.ts
@@ -17,6 +17,7 @@ export default class Example32 {
sgb: SlickVanillaGridBundle;
gridContainerElm: HTMLDivElement;
showEmployeeId = true;
+ subTitleStyle = 'display: block';
metadata: ItemMetadata | Record
Example 33 - colspan/rowspan with large dataset
+
+
-colspan & rowspan using DataView with item metadata. Note:
diff --git a/demos/vanilla/src/examples/example33.ts b/demos/vanilla/src/examples/example33.ts
index bbd544e6d8..6839f8b40e 100644
--- a/demos/vanilla/src/examples/example33.ts
+++ b/demos/vanilla/src/examples/example33.ts
@@ -23,6 +23,7 @@ export default class Example33 {
gridContainerElm: HTMLDivElement;
filteringEnabledClass = 'mdi mdi-toggle-switch';
scrollToRow = 100;
+ subTitleStyle = 'display: block';
metadata: ItemMetadata | Record
Example 34 - Infinite Scroll with Row Move & Row Selections
+
+
-
+
Example 35 - Tree Data with Lazy Loading
(from a Hierarchical Dataset)
+
+
-
+
Example 37 - Hybrid Selection Model
+
+
+
-
+
SlickHybridSelectionModel This Selection Model is an hybrid approach that uses a combination of the row or cell
diff --git a/demos/vanilla/src/examples/example37.ts b/demos/vanilla/src/examples/example37.ts
index 7320929549..ae64f8563d 100644
--- a/demos/vanilla/src/examples/example37.ts
+++ b/demos/vanilla/src/examples/example37.ts
@@ -19,6 +19,7 @@ export default class Example37 {
sgb2!: SlickVanillaGridBundle;
enableMultiSelection = false;
excelExportService = new ExcelExportService();
+ subTitleStyle = 'display: block';
gridFocus() {
this.sgb1.slickGrid?.focus();
@@ -255,4 +256,9 @@ export default class Example37 {
exportGrid1ToExcel() {
this.excelExportService.exportToExcel({ filename: 'export', format: 'xlsx' });
}
+
+ toggleSubTitle() {
+ this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block';
+ this.sgb1.resizerService.resizeGrid();
+ }
}
diff --git a/demos/vanilla/src/examples/example38.html b/demos/vanilla/src/examples/example38.html
index cd204c6291..fe91eda356 100644
--- a/demos/vanilla/src/examples/example38.html
+++ b/demos/vanilla/src/examples/example38.html
@@ -7,6 +7,9 @@
Example 38 - Spreadsheet Drag-Fill
+
+
Spreadsheet with drag-fill, hybrid selection model. Type a few values in the grid and then select those cells and use the bottom right
drag handle spread the selection and auto-fill the values to other cells. Use
onDragReplaceCells event to customize the
drag-fill behavior. Use { enableSelection: true, selectionOptions: { selectionType: 'mixed' }}
diff --git a/demos/vanilla/src/examples/example38.ts b/demos/vanilla/src/examples/example38.ts
index 4eba6c9e0e..fbe271a4a6 100644
--- a/demos/vanilla/src/examples/example38.ts
+++ b/demos/vanilla/src/examples/example38.ts
@@ -15,6 +15,7 @@ export default class Example38 {
gridOptions!: GridOption;
gridContainerElm: HTMLDivElement;
sgb: SlickVanillaGridBundle;
+ subTitleStyle = 'display: block';
attached() {
// define the grid options & columns and then create the grid itself
@@ -146,4 +147,9 @@ export default class Example38 {
document.querySelector('.demo-container')?.classList.remove('dark-mode');
}
}
+
+ toggleSubTitle() {
+ this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block';
+ this.sgb.resizerService.resizeGrid();
+ }
}
diff --git a/demos/vanilla/src/examples/example40.html b/demos/vanilla/src/examples/example40.html
index 806a648edd..ec01d0b298 100644
--- a/demos/vanilla/src/examples/example40.html
+++ b/demos/vanilla/src/examples/example40.html
@@ -2,9 +2,11 @@
Example 40 - Menus with Slots
(Custom Menu Item Renderer)
-
+
(*) NO DATA SHOWN
- just change any of Filters/Sorting/Pages and look at the "SQL Query" changing. This example uses the SQL Backend Service.
diff --git a/demos/vanilla/src/examples/example41.ts b/demos/vanilla/src/examples/example41.ts
index 12773894d0..9fe25786f3 100644
--- a/demos/vanilla/src/examples/example41.ts
+++ b/demos/vanilla/src/examples/example41.ts
@@ -21,6 +21,7 @@ export default class Example41 {
serverWaitDelay = FAKE_SERVER_DELAY;
sqlQuery = '';
sqlService = new SqlService();
+ subTitleStyle = 'display: block';
constructor() {
this._bindingEventService = new BindingEventService();
@@ -300,4 +301,9 @@ export default class Example41 {
this.sgb?.gridOptions && (this.sgb.gridOptions.darkMode = !this.sgb.gridOptions.darkMode);
this.sgb?.slickGrid?.setOptions({ darkMode: this.sgb?.gridOptions.darkMode });
}
+
+ toggleSubTitle() {
+ this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block';
+ this.sgb?.resizerService.resizeGrid();
+ }
}
diff --git a/demos/vanilla/src/examples/example42.html b/demos/vanilla/src/examples/example42.html
index a2971d27f4..4cbb9a0a46 100644
--- a/demos/vanilla/src/examples/example42.html
+++ b/demos/vanilla/src/examples/example42.html
@@ -1,5 +1,10 @@
Example 42 - Custom Filter Bar
+
+
-
+
Display Custom Filters in the top header bar, which is similar to how the MSSQL Extension does it. Please note that the html code used is
not important, what is important though is that we can use
filterService.updateFilters(),
filterService.getColumnFilters(), filterService.clearFilterByColumnId(). Also note that the demo creates custom
diff --git a/demos/vanilla/src/examples/example42.ts b/demos/vanilla/src/examples/example42.ts
index 2df23b2c35..b21465b1ff 100644
--- a/demos/vanilla/src/examples/example42.ts
+++ b/demos/vanilla/src/examples/example42.ts
@@ -19,6 +19,7 @@ export default class Example42 {
gridOptions: GridOption;
dataset: any[];
sgb: SlickVanillaGridBundle;
+ subTitleStyle = 'display: block';
constructor() {
this._bindingEventService = new BindingEventService();
@@ -326,4 +327,9 @@ export default class Example42 {
columnEl.style.color = 'black';
}
}
+
+ toggleSubTitle() {
+ this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block';
+ this.sgb.resizerService.resizeGrid();
+ }
}
diff --git a/demos/vanilla/src/examples/example43.html b/demos/vanilla/src/examples/example43.html
index b5da3db620..7d174d7718 100644
--- a/demos/vanilla/src/examples/example43.html
+++ b/demos/vanilla/src/examples/example43.html
@@ -1,5 +1,10 @@
Example 43 - AI / Web MCP Toolkit
+
+
-
+
Demonstrates the optional
@slickgrid-universal/web-mcp package (WebMcpService), which exposes the grid as
Model Context Protocol (MCP) tools so that AI assistants can read and
manipulate the grid via natural language. The buttons below simulate what an LLM would call — in a real WebMCP-capable browser the same
diff --git a/demos/vanilla/src/examples/example43.ts b/demos/vanilla/src/examples/example43.ts
index f06fd500cb..3aa3a3d4fe 100644
--- a/demos/vanilla/src/examples/example43.ts
+++ b/demos/vanilla/src/examples/example43.ts
@@ -13,6 +13,7 @@ export default class Example43 {
dataset: any[] = [];
sgb!: SlickVanillaGridBundle;
mcpService!: WebMcpService;
+ subTitleStyle = 'display: block';
textResult = '';
attached() {
@@ -88,6 +89,11 @@ export default class Example43 {
this.showOutput('// Grid state reset.');
}
+ toggleSubTitle() {
+ this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block';
+ this.sgb.resizerService.resizeGrid();
+ }
+
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
diff --git a/demos/vanilla/src/examples/example44.html b/demos/vanilla/src/examples/example44.html
index 05eb3595a9..aeb9b4dbc9 100644
--- a/demos/vanilla/src/examples/example44.html
+++ b/demos/vanilla/src/examples/example44.html
@@ -1,6 +1,11 @@
Example 44 - Variable Row Height (provider)
+
+
-
+
Variable row heights driven by
diff --git a/demos/vanilla/src/examples/example44.ts b/demos/vanilla/src/examples/example44.ts
index 109143235f..a347e3369b 100644
--- a/demos/vanilla/src/examples/example44.ts
+++ b/demos/vanilla/src/examples/example44.ts
@@ -22,6 +22,7 @@ export default class Example44 {
sgb!: SlickVanillaGridBundle;
excelExportService = new ExcelExportService();
pdfExportService = new PdfExportService();
+ subTitleStyle = 'display: block';
attached() {
this.defineGrid();
@@ -89,7 +90,7 @@ export default class Example44 {
const owners = ['Alex', 'Priya', 'Mia', 'Sam', 'Chris'];
const fragments = [
'Refactor keyboard shortcut handling for better readability.',
- 'Adjust frozen rows when view-model updates after grouping.',
+ 'Adjust pinned rows when view-model updates after grouping.',
'Improve screen-reader labels on grid menu actions.',
'Align batch editor validation with backend constraints.',
'Capture edge-case around hidden columns and row-span.',
@@ -120,4 +121,9 @@ export default class Example44 {
exportToPdf() {
this.pdfExportService.exportToPdf({ filename: 'Export' });
}
+
+ toggleSubTitle() {
+ this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block';
+ this.sgb.resizerService.resizeGrid();
+ }
}
diff --git a/demos/vanilla/src/examples/example45.html b/demos/vanilla/src/examples/example45.html
index 9d92b96c5f..3c942a4203 100644
--- a/demos/vanilla/src/examples/example45.html
+++ b/demos/vanilla/src/examples/example45.html
@@ -1,6 +1,11 @@
rowHeightProvider. Short summaries stay compact while longer summaries grow.
Example 45 - Variable Row Height (item metadata)
+
+
-
+
Variable row heights via
diff --git a/demos/vanilla/src/examples/example45.ts b/demos/vanilla/src/examples/example45.ts
index 0cb26d8225..0b5eb4395e 100644
--- a/demos/vanilla/src/examples/example45.ts
+++ b/demos/vanilla/src/examples/example45.ts
@@ -22,6 +22,7 @@ export default class Example45 {
isCompact = false;
excelExportService = new ExcelExportService();
pdfExportService = new PdfExportService();
+ subTitleStyle = 'display: block';
attached() {
this.defineGrid();
@@ -86,7 +87,10 @@ export default class Example45 {
includeColumnWidth: true,
},
rowHeight: 40,
- frozenRow: 2,
+ // Use the single-viewport pinning model for the two top rows. The
+ // legacy pinned-row option creates a pane that no longer participates in
+ // the POC renderer.
+ pinning: { rows: { top: [0, 1] } },
gridHeight: 560,
gridWidth: 1080,
dataView: {
@@ -119,7 +123,7 @@ export default class Example45 {
const statuses: ArrayItemMetadata.height fallback, with compact mode rebuilding heights through
invalidateRowHeights().
+ Example 47 - Sticky Financial Report
+
+
+
+
+ Financial report fixture for the sticky rows/columns design. Account, Q1–Q4, and YTD dock to whichever edge is closest after normal
+ scrolling would clip them. Total Revenue, Total Expenses, and Net Profit also use two-sided stickiness and dock to whichever vertical edge
+ is closest after they have been seen.
+
+
+
+
+
- Grid 2 (with Header Grouping & Frozen/Pinned Columns)
+ Grid 2 (with Header Grouping & Pinned Columns)
- Example 20: Pinned (frozen) Columns/Rows
-
-
- code
-
-
-
-
-
+ >) City of Origin is intentionally non-pinnable and is highlighted with a subtle pink background. Columns and rows can also be pinned
+ selectively, without requiring them to be sequential (for example, rows 0, 2, and 4). You can pin any number of columns or rows, place
+ rows at the bottom, change these options through SlickGrid setOptions(), and customize the border styling between pinned columns and
+ rows.
-
-
-
- Example 6: Grid connected to Backend Server with GraphQL
+ Example 6: Grid with Backend GraphQL Service
this.handleOnCommand(_e, args, 2),
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example14.component.html b/frameworks/angular-slickgrid/src/demos/examples/example14.component.html
index 25990e7017..b3b09dec50 100644
--- a/frameworks/angular-slickgrid/src/demos/examples/example14.component.html
+++ b/frameworks/angular-slickgrid/src/demos/examples/example14.component.html
@@ -52,13 +52,13 @@
- Grid 2 (with Header Grouping & Frozen/Pinned Columns)
+ Grid 2 (with Header Grouping & Pinned Columns)
- Example 20: Pinned (frozen) Columns/Rows
-
-
- code
-
-
+ Example 20: Pinned Columns/Rows
-
-
+ >) City of Origin is intentionally non-pinnable and is highlighted with a subtle pink background. Columns and rows can also be pinned
+ selectively, without requiring them to be sequential (for example, rows 0, 2, and 4).
-