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..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; @@ -1392,6 +1373,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 +1412,50 @@ export class SlickGrid = Column, O e } } + /** + * 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); + + 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.