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)
+
+
+
Enable footer (setOptions createFooterRow: true)
+
Toggle footer visibility (setFooterRowVisibility)
+
+
+
+
+
+
+
+
+
+
+
+
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.