From bf9306afc6f0e2d786eaf44f401b88e22dc3776a Mon Sep 17 00:00:00 2001 From: 6pac-ai <6pac@dharpa.com> Date: Fri, 7 Aug 2026 18:46:25 +0930 Subject: [PATCH 1/2] feat: build the footer row lazily when createFooterRow is enabled at runtime Footer-row DOM was built only in the init path and internal_setOptions never created it, so setOptions({ createFooterRow: true }) on a live grid flowed into setColumns -> createColumnFooter, dereferenced the undefined _footerRowL and threw - leaving the grid with a half-mutated options state. internal_setOptions now materializes the footer DOM when the flag flips true: materializeFooterRow() mirrors the init construction (R-before-L scroller order, spacers, columns containers, hide when !showFooterRow) and binds the footer contextmenu/click/scroll handlers on an already-initialized grid. It runs before setScroller (which selects the footer scroll container) and before setColumns (which populates footer cells), so the normal pipeline completes the job. Disabling createFooterRow at runtime hides the footer rather than destroying it, symmetric with showFooterRow; setFooterRowVisibility works on the runtime-built footer as on an init-built one. Adds a permanent SELF-HOSTING regression test (cypress/e2e/quirk-runtime- footer-enable.cy.ts): enable on a live grid -> no throw, scrollers visible, one footer cell and one onFooterRowCellRendered per column, getFooterRow() returns the element, setFooterRowVisibility(false) hides. Verified to fail pre-fix (TypeError: Cannot read properties of undefined) and pass with the fix; footer + composite-editor + frozen suites ran 27/27 as a regression gate. NOTE: examples/example-quirk-runtime-footer-enable.html is a TEMPORARY human-review repro page intended to be deleted before merge - the cypress test does not depend on it. Co-Authored-By: Claude Fable 5 --- cypress/e2e/quirk-runtime-footer-enable.cy.ts | 113 ++++++++++++++++++ .../example-quirk-runtime-footer-enable.html | 71 +++++++++++ src/slick.grid.ts | 48 ++++++++ 3 files changed, 232 insertions(+) create mode 100644 cypress/e2e/quirk-runtime-footer-enable.cy.ts create mode 100644 examples/example-quirk-runtime-footer-enable.html diff --git a/cypress/e2e/quirk-runtime-footer-enable.cy.ts b/cypress/e2e/quirk-runtime-footer-enable.cy.ts new file mode 100644 index 00000000..c6440c70 --- /dev/null +++ b/cypress/e2e/quirk-runtime-footer-enable.cy.ts @@ -0,0 +1,113 @@ +/** + * Regression test for enabling createFooterRow at runtime. + * + * Footer-row DOM was built only in the init path, and internal_setOptions never + * created it — so `setOptions({ createFooterRow: true })` on a live grid flowed + * into setColumns → createColumnFooter, which dereferenced the undefined + * `_footerRowL` and threw, leaving the grid with a half-mutated options state. + * + * The grid now materializes the footer DOM lazily when the flag flips true + * (mirroring the init construction and binding footer events on the live grid); + * runtime disable hides the footer rather than destroying it, symmetric with + * showFooterRow. + * + * The spec is SELF-HOSTING (harness served via cy.intercept; no example page). + * The harness wraps the enabling setOptions in try/catch so the pre-fix + * TypeError reports as a graceful check failure. Verified to FAIL pre-fix + * (TypeError) and PASS with the fix. + */ + +const harnessHtml = ` + + + + Harness: runtime footer enable + + + + +
+
+ + + + + +`; + +describe('Quirk - createFooterRow must be enableable at runtime', { retries: 1 }, () => { + it('should build, populate, wire and toggle the footer when enabled after init', () => { + cy.intercept('GET', '/quirk-runtime-footer-enable-harness.html', { + headers: { 'content-type': 'text/html' }, + body: harnessHtml, + }); + cy.visit(`${Cypress.config('baseUrl')}/quirk-runtime-footer-enable-harness.html`); + cy.window().its('grid').should('exist'); + + cy.window().then((win: any) => { + const ok = win.runChecks(); + const detail = win.document.getElementById('checkResults').textContent; + expect(ok, `in-page runtime-footer self-checks:\n${detail}`).to.eq(true); + }); + cy.get('#checkResults').should('contain', 'ALL CHECKS PASSED'); + }); +}); diff --git a/examples/example-quirk-runtime-footer-enable.html b/examples/example-quirk-runtime-footer-enable.html new file mode 100644 index 00000000..9f7becbf --- /dev/null +++ b/examples/example-quirk-runtime-footer-enable.html @@ -0,0 +1,71 @@ + + + + + + SlickGrid quirk repro: runtime footer enable (temporary, do not merge) + + + + + +

Runtime createFooterRow enable (TEMPORARY repro, do not merge)

+
+
+ + +
+
+ + + + + + + + + + diff --git a/src/slick.grid.ts b/src/slick.grid.ts index 7965379d..a24ff32b 100644 --- a/src/slick.grid.ts +++ b/src/slick.grid.ts @@ -1392,6 +1392,12 @@ export class SlickGrid = Column, O e this.validateAndEnforceOptions(); this.setFrozenOptions(); + if (this._options.createFooterRow && !this._footerRow) { + this.materializeFooterRow(); + } else if (!this._options.createFooterRow && this._footerRow) { + this._footerRowScroller.forEach((scroller) => Utils.hide(scroller)); + } + // when user changed frozen row option, we need to force a recalculation of each viewport heights if (this._options.frozenBottom !== undefined) { this.enforceFrozenRowHeightRecalc = true; @@ -1425,6 +1431,48 @@ export class SlickGrid = Column, O e } } + /** + * Builds the footer-row DOM when `createFooterRow` is enabled after initialization, + * mirroring the construction the init path performs, and binds the footer events + * on an already-initialized grid. Runtime disable hides the footer rather than + * destroying it (symmetric with `showFooterRow`). + */ + protected materializeFooterRow(): void { + const canvasWithScrollbarWidth = this.getCanvasWidth() + (this.scrollbarDimensions?.width ?? 0); + + this._footerRowScrollerR = Utils.createDomElement('div', { className: 'slick-footerrow ui-state-default slick-state-default' }, this._paneTopR); + this._footerRowScrollerL = Utils.createDomElement('div', { className: 'slick-footerrow ui-state-default slick-state-default' }, this._paneTopL); + + this._footerRowScroller = [this._footerRowScrollerL, this._footerRowScrollerR]; + + this._footerRowSpacerL = Utils.createDomElement('div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._footerRowScrollerL); + Utils.width(this._footerRowSpacerL, canvasWithScrollbarWidth); + this._footerRowSpacerR = Utils.createDomElement('div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._footerRowScrollerR); + Utils.width(this._footerRowSpacerR, canvasWithScrollbarWidth); + + this._footerRowL = Utils.createDomElement('div', { className: 'slick-footerrow-columns slick-footerrow-columns-left' }, this._footerRowScrollerL); + this._footerRowR = Utils.createDomElement('div', { className: 'slick-footerrow-columns slick-footerrow-columns-right' }, this._footerRowScrollerR); + + this._footerRow = [this._footerRowL, this._footerRowR]; + + if (!this._options.showFooterRow) { + this._footerRowScroller.forEach((scroller) => { + Utils.hide(scroller); + }); + } + + if (this.initialized) { + this._footerRow.forEach((footer) => { + this._bindingEventService.bind(footer, 'contextmenu', this.handleFooterContextMenu.bind(this) as EventListener); + this._bindingEventService.bind(footer, 'click', this.handleFooterClick.bind(this) as EventListener); + }); + + this._footerRowScroller.forEach((scroller) => { + this._bindingEventService.bind(scroller, 'scroll', this.handleFooterRowScroll.bind(this) as EventListener); + }); + } + } + /** * * Ensures consistency in option setting, by thastIF autoHeight IS enabled, leaveSpaceForNewRows is set to FALSE. From 70c08ecba066f764e66d973572f7d9076b4fc644 Mon Sep 17 00:00:00 2001 From: 6pac-ai <6pac@dharpa.com> Date: Sun, 9 Aug 2026 21:37:06 +0930 Subject: [PATCH 2/2] refactor: share materializeFooterRow between init and runtime enable The init path's inline footer-row construction is replaced by a call to materializeFooterRow(), making it the single construction path so the two cannot drift. The initialized guard keeps event binding unchanged: during init the handlers are still bound in finishInitialization; on a live grid the method binds them itself. Co-Authored-By: Claude Fable 5 --- src/slick.grid.ts | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/slick.grid.ts b/src/slick.grid.ts index a24ff32b..f6180034 100644 --- a/src/slick.grid.ts +++ b/src/slick.grid.ts @@ -853,26 +853,7 @@ export class SlickGrid = Column, O e // footer Row if (this._options.createFooterRow) { - this._footerRowScrollerR = Utils.createDomElement('div', { className: 'slick-footerrow ui-state-default slick-state-default' }, this._paneTopR); - this._footerRowScrollerL = Utils.createDomElement('div', { className: 'slick-footerrow ui-state-default slick-state-default' }, this._paneTopL); - - this._footerRowScroller = [this._footerRowScrollerL, this._footerRowScrollerR]; - - this._footerRowSpacerL = Utils.createDomElement('div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._footerRowScrollerL); - Utils.width(this._footerRowSpacerL, canvasWithScrollbarWidth); - this._footerRowSpacerR = Utils.createDomElement('div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._footerRowScrollerR); - Utils.width(this._footerRowSpacerR, canvasWithScrollbarWidth); - - this._footerRowL = Utils.createDomElement('div', { className: 'slick-footerrow-columns slick-footerrow-columns-left' }, this._footerRowScrollerL); - this._footerRowR = Utils.createDomElement('div', { className: 'slick-footerrow-columns slick-footerrow-columns-right' }, this._footerRowScrollerR); - - this._footerRow = [this._footerRowL, this._footerRowR]; - - if (!this._options.showFooterRow) { - this._footerRowScroller.forEach((scroller) => { - Utils.hide(scroller); - }); - } + this.materializeFooterRow(); } this._focusSink2 = this._focusSink.cloneNode(true) as HTMLDivElement; @@ -1432,10 +1413,12 @@ export class SlickGrid = Column, O e } /** - * Builds the footer-row DOM when `createFooterRow` is enabled after initialization, - * mirroring the construction the init path performs, and binds the footer events - * on an already-initialized grid. Runtime disable hides the footer rather than - * destroying it (symmetric with `showFooterRow`). + * Builds the footer-row DOM (scrollers, spacers and footer-row containers) in both + * panes — the single construction path shared by init and by a runtime + * `setOptions({ createFooterRow: true })` enable. On an already-initialized grid it + * also binds the footer events (during init they are bound in `finishInitialization`). + * Runtime disable hides the footer rather than destroying it (symmetric with + * `showFooterRow`). */ protected materializeFooterRow(): void { const canvasWithScrollbarWidth = this.getCanvasWidth() + (this.scrollbarDimensions?.width ?? 0);