diff --git a/packages/common/src/core/__tests__/slickGrid.spec.ts b/packages/common/src/core/__tests__/slickGrid.spec.ts index 762626727..73f03d9f4 100644 --- a/packages/common/src/core/__tests__/slickGrid.spec.ts +++ b/packages/common/src/core/__tests__/slickGrid.spec.ts @@ -1694,6 +1694,54 @@ describe('SlickGrid core file', () => { expect(grid.getFooterRowColumn('firstName')).toBeUndefined(); }); + it('should lazily create footer row when "createFooterRow" is enabled at runtime', () => { + const columns = [ + { id: 'firstName', field: 'firstName', name: 'First Name' }, + { id: 'lastName', field: 'lastName', name: 'Last Name' }, + ] as Column[]; + grid = new SlickGrid(container, [], columns, { + ...defaultOptions, + createFooterRow: false, + showFooterRow: true, + }); + grid.init(); + + const onFooterRowCellRenderedSpy = vi.spyOn(grid.onFooterRowCellRendered, 'notify'); + + expect(grid.getFooterRow()).toBeFalsy(); + expect(container.querySelector('.slick-footerrow')).toBeFalsy(); + + expect(() => grid.setOptions({ createFooterRow: true, showFooterRow: true })).not.toThrow(); + + let footerElms = container.querySelectorAll('.slick-footerrow'); + expect(footerElms).toBeTruthy(); + expect(footerElms.length).toBe(2); + expect(footerElms[0].style.display).not.toBe('none'); + expect(footerElms[1].style.display).not.toBe('none'); + expect(grid.getFooterRow()).toBeTruthy(); + expect(grid.getFooterRowColumn('firstName')).toEqual(footerElms[0].querySelector('.slick-footerrow-column')); + expect(onFooterRowCellRenderedSpy).toHaveBeenCalledTimes(2); + + grid.setFooterRowVisibility(false); + footerElms = container.querySelectorAll('.slick-footerrow'); + expect(footerElms[0].style.display).toBe('none'); + expect(footerElms[1].style.display).toBe('none'); + + grid.setOptions({ createFooterRow: false }); + expect(footerElms[0].style.display).toBe('none'); + expect(footerElms[1].style.display).toBe('none'); + + grid.setOptions({ createFooterRow: true, showFooterRow: true }); + footerElms = container.querySelectorAll('.slick-footerrow'); + expect(footerElms[0].style.display).toBe('none'); + expect(footerElms[1].style.display).toBe('none'); + + grid.setFooterRowVisibility(false); + grid.setFooterRowVisibility(true); + expect(footerElms[0].style.display).not.toBe('none'); + expect(footerElms[1].style.display).not.toBe('none'); + }); + it('should show footer when "showFooterRow" is enabled but not return any row when columns to the right are outside the range', () => { const columns = [ { id: 'firstName', field: 'firstName', name: 'First Name' }, diff --git a/packages/common/src/core/slickGrid.ts b/packages/common/src/core/slickGrid.ts index 8f930996a..eea22d1b2 100755 --- a/packages/common/src/core/slickGrid.ts +++ b/packages/common/src/core/slickGrid.ts @@ -925,42 +925,7 @@ export class SlickGrid = Column, O e // footer Row if (this._options.createFooterRow) { - this._footerRowScrollerR = createDomElement('div', { className: 'slick-footerrow slick-state-default' }, this._paneTopR); - this._footerRowScrollerL = createDomElement('div', { className: 'slick-footerrow slick-state-default' }, this._paneTopL); - - this._footerRowScroller = [this._footerRowScrollerL, this._footerRowScrollerR]; - - this._footerRowSpacerL = createDomElement( - 'div', - { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, - this._footerRowScrollerL - ); - Utils.width(this._footerRowSpacerL, canvasWithScrollbarWidth); - this._footerRowSpacerR = createDomElement( - 'div', - { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, - this._footerRowScrollerR - ); - Utils.width(this._footerRowSpacerR, canvasWithScrollbarWidth); - - this._footerRowL = createDomElement( - 'div', - { className: 'slick-footerrow-columns slick-footerrow-columns-left' }, - this._footerRowScrollerL - ); - this._footerRowR = 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; @@ -1800,6 +1765,61 @@ 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 = createDomElement('div', { className: 'slick-footerrow slick-state-default' }, this._paneTopR); + this._footerRowScrollerL = createDomElement('div', { className: 'slick-footerrow slick-state-default' }, this._paneTopL); + this._footerRowScroller = [this._footerRowScrollerL, this._footerRowScrollerR]; + + this._footerRowSpacerL = createDomElement( + 'div', + { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, + this._footerRowScrollerL + ); + Utils.width(this._footerRowSpacerL, canvasWithScrollbarWidth); + + this._footerRowSpacerR = createDomElement( + 'div', + { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, + this._footerRowScrollerR + ); + Utils.width(this._footerRowSpacerR, canvasWithScrollbarWidth); + + this._footerRowL = createDomElement( + 'div', + { className: 'slick-footerrow-columns slick-footerrow-columns-left' }, + this._footerRowScrollerL + ); + this._footerRowR = 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); + }); + } + + // Bind footer events only when footer row is created after init. + if (this.initialized) { + this._bindingEventService.bind(this._footerRow, 'contextmenu', this.handleFooterContextMenu.bind(this) as EventListener); + this._bindingEventService.bind(this._footerRow, 'click', this.handleFooterClick.bind(this) as EventListener); + this._bindingEventService.bind(this._footerRowScroller, 'scroll', this.handleFooterRowScroll.bind(this) as EventListener); + } + } + protected handleHeaderMouseHoverOn(e: Event | SlickEventData): void { (e as any)?.target.classList.add('slick-state-hover'); } @@ -3732,6 +3752,14 @@ 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;