diff --git a/AGENTS.md b/AGENTS.md index ed738ed16..fea85f6e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,7 @@ Changes in `packages/` can affect every framework. Preserve backward compatibili - Unit tests use Vitest with `test/vitest.config.mts`. - E2E tests use Cypress with `test/cypress.config.ts`. - Cypress tests use `testIsolation: false`; preserve their execution order and inherited state. +- For the Vanilla demo suite, start the watch server with `pnpm serve:vite`, then run the root Cypress CI suite with `pnpm cypress:ci`. To run one spec while iterating, pass its path directly (for example, `pnpm cypress:ci --spec test/cypress/e2e/example33.cy.ts`). - Framework demos provide headless Cypress CI scripts. Start the matching demo server first (`pnpm angular:serve`, `pnpm aurelia:serve`, `pnpm react:serve`, or `pnpm vue:serve`). - Run the corresponding root CI command: `pnpm angular:cypress:ci`, `pnpm aurelia:cypress:ci`, `pnpm react:cypress:ci`, or `pnpm vue:cypress:ci` (for example, `pnpm aurelia:cypress:ci`). These commands use each framework's Cypress config and are preferred for validating framework-specific E2E suites. - Add or update tests for behavior changes, especially in core packages. diff --git a/demos/aurelia/src/examples/slickgrid/example44.html b/demos/aurelia/src/examples/slickgrid/example44.html index 8a3eef20f..64094aff9 100644 --- a/demos/aurelia/src/examples/slickgrid/example44.html +++ b/demos/aurelia/src/examples/slickgrid/example44.html @@ -22,6 +22,10 @@

responsability). This demo does not show this because it is up to you to decide what to do when the span changes shape (i.e. you default to 3 rowspan but you filter a row in the middle, how do you want to proceed?).

+

+ autoHeaderHeight is enabled: the narrow Revenue Growth column wraps its header and the grid measures the required + height. +

diff --git a/demos/aurelia/src/examples/slickgrid/example44.ts b/demos/aurelia/src/examples/slickgrid/example44.ts index f07cc6919..640a2914a 100644 --- a/demos/aurelia/src/examples/slickgrid/example44.ts +++ b/demos/aurelia/src/examples/slickgrid/example44.ts @@ -75,10 +75,12 @@ export class Example44 { id: 'revenueGrowth', name: 'Revenue Growth', field: 'revenueGrowth', + headerCssClass: 'auto-header-height-demo', exportCustomFormatter: rowCellValueExportFormatter, formatter: rowCellValueFormatter, type: 'number', - minWidth: 120, + minWidth: 65, + width: 65, }, { id: 'pricingPolicy', @@ -267,6 +269,7 @@ export class Example44 { this.gridOptions = { enableCellNavigation: true, + autoHeaderHeight: true, enableColumnReorder: true, enableCellRowSpan: true, enableHeaderMenu: false, diff --git a/demos/aurelia/test/cypress/e2e/example44.cy.ts b/demos/aurelia/test/cypress/e2e/example44.cy.ts index 589ebd01e..5d5bf6f47 100644 --- a/demos/aurelia/test/cypress/e2e/example44.cy.ts +++ b/demos/aurelia/test/cypress/e2e/example44.cy.ts @@ -30,6 +30,23 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { cy.get('h2').should('contain', 'Example 44: colspan/rowspan with large dataset'); }); + it('should calculate a height that fits the wrapped Revenue Growth header', () => { + cy.get('.slick-header-auto-height').should('have.length', 2); + cy.get('.slick-header-auto-height') + .first() + .should(($header) => { + expect(parseFloat($header.css('--slick-auto-header-height'))).to.be.greaterThan(0); + }); + cy.get('.auto-header-height-demo').should(($header) => { + expect($header[0].scrollHeight).to.be.lte($header[0].clientHeight); + expect($header[0].clientHeight).to.be.lessThan(100); + const name = $header[0].querySelector('.slick-column-name'); + expect(name).to.exist; + const lineHeight = parseFloat(getComputedStyle(name!).lineHeight) || 16; + expect(name!.scrollHeight).to.be.within(lineHeight * 1.5, lineHeight * 2.5); + }); + }); + it('should have exact column titles', () => { cy.get('.slick-header-columns') .children() diff --git a/demos/react/src/examples/slickgrid/Example44.tsx b/demos/react/src/examples/slickgrid/Example44.tsx index 44edcab14..7f896de51 100644 --- a/demos/react/src/examples/slickgrid/Example44.tsx +++ b/demos/react/src/examples/slickgrid/Example44.tsx @@ -73,10 +73,12 @@ export default function Example44() { id: 'revenueGrowth', name: 'Revenue Growth', field: 'revenueGrowth', + headerCssClass: 'auto-header-height-demo', exportCustomFormatter: rowCellValueExportFormatter, formatter: rowCellValueFormatter, type: 'number', - minWidth: 120, + minWidth: 65, + width: 65, }, { id: 'pricingPolicy', @@ -265,6 +267,7 @@ export default function Example44() { const gridOptions: GridOption = { enableCellNavigation: true, + autoHeaderHeight: true, enableColumnReorder: true, enableHeaderMenu: false, enableCellRowSpan: true, @@ -412,6 +415,10 @@ export default function Example44() { your responsability). This demo does not show this because it is up to you to decide what to do when the span changes shape (i.e. you default to 3 rowspan but you filter a row in the middle, how do you want to proceed?).

+

+ autoHeaderHeight is enabled: the narrow Revenue Growth column wraps its header and the grid measures the + required height. +

diff --git a/demos/react/test/cypress/e2e/example44.cy.ts b/demos/react/test/cypress/e2e/example44.cy.ts index 589ebd01e..5d5bf6f47 100644 --- a/demos/react/test/cypress/e2e/example44.cy.ts +++ b/demos/react/test/cypress/e2e/example44.cy.ts @@ -30,6 +30,23 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { cy.get('h2').should('contain', 'Example 44: colspan/rowspan with large dataset'); }); + it('should calculate a height that fits the wrapped Revenue Growth header', () => { + cy.get('.slick-header-auto-height').should('have.length', 2); + cy.get('.slick-header-auto-height') + .first() + .should(($header) => { + expect(parseFloat($header.css('--slick-auto-header-height'))).to.be.greaterThan(0); + }); + cy.get('.auto-header-height-demo').should(($header) => { + expect($header[0].scrollHeight).to.be.lte($header[0].clientHeight); + expect($header[0].clientHeight).to.be.lessThan(100); + const name = $header[0].querySelector('.slick-column-name'); + expect(name).to.exist; + const lineHeight = parseFloat(getComputedStyle(name!).lineHeight) || 16; + expect(name!.scrollHeight).to.be.within(lineHeight * 1.5, lineHeight * 2.5); + }); + }); + it('should have exact column titles', () => { cy.get('.slick-header-columns') .children() diff --git a/demos/vanilla/src/examples/example33.html b/demos/vanilla/src/examples/example33.html index 691ffb2e3..0086f44ee 100644 --- a/demos/vanilla/src/examples/example33.html +++ b/demos/vanilla/src/examples/example33.html @@ -21,6 +21,10 @@

responsability). This demo does not show this because it is up to you to decide what to do when the span changes shape (i.e. you default to 3 rowspan but you filter a row in the middle, how do you want to proceed?).

+

+ autoHeaderHeight is enabled: the narrow Revenue Growth column wraps its header and the grid measures the required + height. +

diff --git a/demos/vanilla/src/examples/example33.ts b/demos/vanilla/src/examples/example33.ts index 62695a910..bbd544e6d 100644 --- a/demos/vanilla/src/examples/example33.ts +++ b/demos/vanilla/src/examples/example33.ts @@ -92,10 +92,12 @@ export default class Example33 { id: 'revenueGrowth', name: 'Revenue Growth', field: 'revenueGrowth', + headerCssClass: 'auto-header-height-demo', exportCustomFormatter: rowCellValueExportFormatter, formatter: rowCellValueFormatter, type: 'number', - minWidth: 120, + minWidth: 65, + width: 65, }, { id: 'pricingPolicy', @@ -284,6 +286,7 @@ export default class Example33 { this.gridOptions = { enableCellNavigation: true, + autoHeaderHeight: true, enableColumnReorder: true, enableHeaderMenu: false, enableCellRowSpan: true, diff --git a/demos/vue/src/components/Example44.vue b/demos/vue/src/components/Example44.vue index ff96e7449..75e6ebb19 100644 --- a/demos/vue/src/components/Example44.vue +++ b/demos/vue/src/components/Example44.vue @@ -69,10 +69,12 @@ function defineGrid() { id: 'revenueGrowth', name: 'Revenue Growth', field: 'revenueGrowth', + headerCssClass: 'auto-header-height-demo', exportCustomFormatter: rowCellValueExportFormatter, formatter: rowCellValueFormatter, type: 'number', - minWidth: 120, + minWidth: 65, + width: 65, }, { id: 'pricingPolicy', @@ -261,6 +263,7 @@ function defineGrid() { gridOptions.value = { enableCellNavigation: true, + autoHeaderHeight: true, enableColumnReorder: true, enableHeaderMenu: false, enableCellRowSpan: true, @@ -395,6 +398,10 @@ function vueGridReady(grid: SlickgridVueInstance) { responsability). This demo does not show this because it is up to you to decide what to do when the span changes shape (i.e. you default to 3 rowspan but you filter a row in the middle, how do you want to proceed?).

+

+ autoHeaderHeight is enabled: the narrow Revenue Growth column wraps its header and the grid measures the required + height. +

diff --git a/demos/vue/test/cypress/e2e/example44.cy.ts b/demos/vue/test/cypress/e2e/example44.cy.ts index 589ebd01e..5d5bf6f47 100644 --- a/demos/vue/test/cypress/e2e/example44.cy.ts +++ b/demos/vue/test/cypress/e2e/example44.cy.ts @@ -30,6 +30,23 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { cy.get('h2').should('contain', 'Example 44: colspan/rowspan with large dataset'); }); + it('should calculate a height that fits the wrapped Revenue Growth header', () => { + cy.get('.slick-header-auto-height').should('have.length', 2); + cy.get('.slick-header-auto-height') + .first() + .should(($header) => { + expect(parseFloat($header.css('--slick-auto-header-height'))).to.be.greaterThan(0); + }); + cy.get('.auto-header-height-demo').should(($header) => { + expect($header[0].scrollHeight).to.be.lte($header[0].clientHeight); + expect($header[0].clientHeight).to.be.lessThan(100); + const name = $header[0].querySelector('.slick-column-name'); + expect(name).to.exist; + const lineHeight = parseFloat(getComputedStyle(name!).lineHeight) || 16; + expect(name!.scrollHeight).to.be.within(lineHeight * 1.5, lineHeight * 2.5); + }); + }); + it('should have exact column titles', () => { cy.get('.slick-header-columns') .children() diff --git a/docs/styling/multiple-column-header-rows.md b/docs/styling/multiple-column-header-rows.md index 4a5c66fda..37ec8f116 100644 --- a/docs/styling/multiple-column-header-rows.md +++ b/docs/styling/multiple-column-header-rows.md @@ -55,4 +55,16 @@ Also note that if you use a stylesheet attached to your component (or inline), y styleUrls: ['./demo.component.scss'], templateUrl: './demo.component.html' }) -``` \ No newline at end of file +``` + +### Automatic header height + +Set `autoHeaderHeight: true` to size column headers from their rendered content instead of using a fixed row count. This supports multi-line text, HTML, and DOM elements used as column names. The calculated height is shared by frozen header panes and is recalculated after column resizing or autosizing. + +```ts +const gridOptions = { + autoHeaderHeight: true, +}; +``` + +The option is disabled by default. Use it with a theme stylesheet from Slickgrid-Universal; it provides the required header wrapping styles. diff --git a/frameworks/angular-slickgrid/docs/styling/multiple-column-header-rows.md b/frameworks/angular-slickgrid/docs/styling/multiple-column-header-rows.md index 4a5c66fda..37ec8f116 100644 --- a/frameworks/angular-slickgrid/docs/styling/multiple-column-header-rows.md +++ b/frameworks/angular-slickgrid/docs/styling/multiple-column-header-rows.md @@ -55,4 +55,16 @@ Also note that if you use a stylesheet attached to your component (or inline), y styleUrls: ['./demo.component.scss'], templateUrl: './demo.component.html' }) -``` \ No newline at end of file +``` + +### Automatic header height + +Set `autoHeaderHeight: true` to size column headers from their rendered content instead of using a fixed row count. This supports multi-line text, HTML, and DOM elements used as column names. The calculated height is shared by frozen header panes and is recalculated after column resizing or autosizing. + +```ts +const gridOptions = { + autoHeaderHeight: true, +}; +``` + +The option is disabled by default. Use it with a theme stylesheet from Slickgrid-Universal; it provides the required header wrapping styles. diff --git a/frameworks/angular-slickgrid/src/demos/examples/example44.component.html b/frameworks/angular-slickgrid/src/demos/examples/example44.component.html index ec3363567..5a0cbae6e 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example44.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example44.component.html @@ -23,6 +23,10 @@

responsability). This demo does not show this because it is up to you to decide what to do when the span changes shape (i.e. you default to 3 rowspan but you filter a row in the middle, how do you want to proceed?).

+

+ autoHeaderHeight is enabled: the narrow Revenue Growth column wraps its header and the grid measures the required + height. +

diff --git a/frameworks/angular-slickgrid/src/demos/examples/example44.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example44.component.ts index 048b16b73..8e0b927c7 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example44.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example44.component.ts @@ -89,10 +89,12 @@ export class Example44Component implements OnInit { id: 'revenueGrowth', name: 'Revenue Growth', field: 'revenueGrowth', + headerCssClass: 'auto-header-height-demo', exportCustomFormatter: rowCellValueExportFormatter, formatter: rowCellValueFormatter, type: 'number', - minWidth: 120, + minWidth: 65, + width: 65, }, { id: 'pricingPolicy', @@ -281,6 +283,7 @@ export class Example44Component implements OnInit { this.gridOptions = { enableCellNavigation: true, + autoHeaderHeight: true, enableColumnReorder: true, enableCellRowSpan: true, enableHeaderMenu: false, diff --git a/frameworks/angular-slickgrid/test/cypress/e2e/example44.cy.ts b/frameworks/angular-slickgrid/test/cypress/e2e/example44.cy.ts index 589ebd01e..5d5bf6f47 100644 --- a/frameworks/angular-slickgrid/test/cypress/e2e/example44.cy.ts +++ b/frameworks/angular-slickgrid/test/cypress/e2e/example44.cy.ts @@ -30,6 +30,23 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => { cy.get('h2').should('contain', 'Example 44: colspan/rowspan with large dataset'); }); + it('should calculate a height that fits the wrapped Revenue Growth header', () => { + cy.get('.slick-header-auto-height').should('have.length', 2); + cy.get('.slick-header-auto-height') + .first() + .should(($header) => { + expect(parseFloat($header.css('--slick-auto-header-height'))).to.be.greaterThan(0); + }); + cy.get('.auto-header-height-demo').should(($header) => { + expect($header[0].scrollHeight).to.be.lte($header[0].clientHeight); + expect($header[0].clientHeight).to.be.lessThan(100); + const name = $header[0].querySelector('.slick-column-name'); + expect(name).to.exist; + const lineHeight = parseFloat(getComputedStyle(name!).lineHeight) || 16; + expect(name!.scrollHeight).to.be.within(lineHeight * 1.5, lineHeight * 2.5); + }); + }); + it('should have exact column titles', () => { cy.get('.slick-header-columns') .children() diff --git a/frameworks/aurelia-slickgrid/docs/styling/multiple-column-header-rows.md b/frameworks/aurelia-slickgrid/docs/styling/multiple-column-header-rows.md index 4a5c66fda..37ec8f116 100644 --- a/frameworks/aurelia-slickgrid/docs/styling/multiple-column-header-rows.md +++ b/frameworks/aurelia-slickgrid/docs/styling/multiple-column-header-rows.md @@ -55,4 +55,16 @@ Also note that if you use a stylesheet attached to your component (or inline), y styleUrls: ['./demo.component.scss'], templateUrl: './demo.component.html' }) -``` \ No newline at end of file +``` + +### Automatic header height + +Set `autoHeaderHeight: true` to size column headers from their rendered content instead of using a fixed row count. This supports multi-line text, HTML, and DOM elements used as column names. The calculated height is shared by frozen header panes and is recalculated after column resizing or autosizing. + +```ts +const gridOptions = { + autoHeaderHeight: true, +}; +``` + +The option is disabled by default. Use it with a theme stylesheet from Slickgrid-Universal; it provides the required header wrapping styles. diff --git a/frameworks/slickgrid-react/docs/styling/multiple-column-header-rows.md b/frameworks/slickgrid-react/docs/styling/multiple-column-header-rows.md index 4a5c66fda..37ec8f116 100644 --- a/frameworks/slickgrid-react/docs/styling/multiple-column-header-rows.md +++ b/frameworks/slickgrid-react/docs/styling/multiple-column-header-rows.md @@ -55,4 +55,16 @@ Also note that if you use a stylesheet attached to your component (or inline), y styleUrls: ['./demo.component.scss'], templateUrl: './demo.component.html' }) -``` \ No newline at end of file +``` + +### Automatic header height + +Set `autoHeaderHeight: true` to size column headers from their rendered content instead of using a fixed row count. This supports multi-line text, HTML, and DOM elements used as column names. The calculated height is shared by frozen header panes and is recalculated after column resizing or autosizing. + +```ts +const gridOptions = { + autoHeaderHeight: true, +}; +``` + +The option is disabled by default. Use it with a theme stylesheet from Slickgrid-Universal; it provides the required header wrapping styles. diff --git a/frameworks/slickgrid-vue/docs/styling/multiple-column-header-rows.md b/frameworks/slickgrid-vue/docs/styling/multiple-column-header-rows.md index 4a5c66fda..37ec8f116 100644 --- a/frameworks/slickgrid-vue/docs/styling/multiple-column-header-rows.md +++ b/frameworks/slickgrid-vue/docs/styling/multiple-column-header-rows.md @@ -55,4 +55,16 @@ Also note that if you use a stylesheet attached to your component (or inline), y styleUrls: ['./demo.component.scss'], templateUrl: './demo.component.html' }) -``` \ No newline at end of file +``` + +### Automatic header height + +Set `autoHeaderHeight: true` to size column headers from their rendered content instead of using a fixed row count. This supports multi-line text, HTML, and DOM elements used as column names. The calculated height is shared by frozen header panes and is recalculated after column resizing or autosizing. + +```ts +const gridOptions = { + autoHeaderHeight: true, +}; +``` + +The option is disabled by default. Use it with a theme stylesheet from Slickgrid-Universal; it provides the required header wrapping styles. diff --git a/packages/common/src/core/__tests__/slickGrid.spec.ts b/packages/common/src/core/__tests__/slickGrid.spec.ts index 0636f950a..51d319697 100644 --- a/packages/common/src/core/__tests__/slickGrid.spec.ts +++ b/packages/common/src/core/__tests__/slickGrid.spec.ts @@ -12,6 +12,15 @@ import { SlickGrid } from '../slickGrid.js'; // Subclass for protected method coverage class TestGrid extends SlickGrid { + public callRecalculateHeaderHeightWithoutLeftHeader() { + const headerScrollerL = this._headerScrollerL; + (this as any)._headerScrollerL = undefined; + this.recalculateHeaderHeight(); + this._headerScrollerL = headerScrollerL; + } + public callRecalculateHeaderHeight() { + this.recalculateHeaderHeight(); + } public callHandleContainerKeyDown(e: any) { this.handleContainerKeyDown(e); } @@ -3293,6 +3302,84 @@ describe('SlickGrid core file', () => { expect(invalidateSpy).toHaveBeenCalled(); expect(renderSpy).toHaveBeenCalled(); }); + + it('should recalculate automatic header height after column widths are rerendered', () => { + grid = new SlickGrid(container, [], columns, { ...defaultOptions, autoHeaderHeight: true }); + const recalculateSpy = vi.spyOn(grid as any, 'recalculateHeaderHeight'); + + grid.reRenderColumns(); + + expect(recalculateSpy).toHaveBeenCalled(); + }); + }); + + describe('automatic header height', () => { + const columns = [ + { id: 'firstName', field: 'firstName', name: 'First Name' }, + { id: 'lastName', field: 'lastName', name: 'Last Name' }, + ] as Column[]; + + it('should synchronize frozen header panes to the largest rendered height', () => { + grid = new TestGrid(container, [], columns, { ...defaultOptions, autoHeaderHeight: true, frozenColumn: 0 }); + const [leftHeader, rightHeader] = container.querySelectorAll('.slick-header'); + vi.spyOn(leftHeader, 'getBoundingClientRect').mockReturnValue({ height: 42 } as DOMRect); + vi.spyOn(rightHeader, 'getBoundingClientRect').mockReturnValue({ height: 58 } as DOMRect); + const resizeSpy = vi.spyOn(grid, 'resizeCanvas'); + + (grid as TestGrid).callRecalculateHeaderHeight(); + + [leftHeader, rightHeader].forEach((header) => { + expect(header.classList).toContain('slick-header-auto-height'); + expect(header.style.getPropertyValue('--slick-auto-header-height')).toBe('58px'); + expect(header.style.height).toBe('58px'); + }); + expect(resizeSpy).toHaveBeenCalledTimes(1); + + (grid as TestGrid).callRecalculateHeaderHeight(); + expect(resizeSpy).toHaveBeenCalledTimes(1); + }); + + it('should ignore recalculation before the left header pane is created', () => { + grid = new TestGrid(container, [], columns, defaultOptions); + + expect(() => (grid as TestGrid).callRecalculateHeaderHeightWithoutLeftHeader()).not.toThrow(); + }); + + it('should remove automatic header-height styles when disabled with setOptions', () => { + grid = new TestGrid(container, [], columns, { ...defaultOptions, autoHeaderHeight: true, frozenColumn: 0 }); + const headers = [...container.querySelectorAll('.slick-header')]; + headers.forEach((header, index) => { + vi.spyOn(header, 'getBoundingClientRect').mockReturnValue({ height: 42 + index } as DOMRect); + }); + + (grid as TestGrid).callRecalculateHeaderHeight(); + grid.setOptions({ autoHeaderHeight: false }); + + headers.forEach((header) => { + expect(header.classList).not.toContain('slick-header-auto-height'); + expect(header.style.getPropertyValue('--slick-auto-header-height')).toBe(''); + expect(header.style.height).toBe(''); + }); + }); + + it('should recalculate automatic header height when enabled with setOptions', () => { + grid = new TestGrid(container, [], columns, { ...defaultOptions, autoHeaderHeight: false }); + const recalculateSpy = vi.spyOn(grid as any, 'recalculateHeaderHeight'); + + grid.setOptions({ autoHeaderHeight: true }); + + expect(recalculateSpy).toHaveBeenCalled(); + expect(container.querySelectorAll('.slick-header-auto-height')).toHaveLength(2); + }); + + it('should recalculate automatic header height after updating columns', () => { + grid = new SlickGrid(container, [], columns, { ...defaultOptions, autoHeaderHeight: true }); + const recalculateSpy = vi.spyOn(grid as any, 'recalculateHeaderHeight'); + + grid.updateColumns(); + + expect(recalculateSpy).toHaveBeenCalledTimes(1); + }); }); describe('Editors', () => { @@ -4548,9 +4635,10 @@ describe('SlickGrid core file', () => { }); it('should resize 2nd column that has a "width" defined using default sizing grid options', () => { - grid = new SlickGrid(container, data, columns, { ...defaultOptions, forceFitColumns: false }); + grid = new SlickGrid(container, data, columns, { ...defaultOptions, autoHeaderHeight: true, forceFitColumns: false }); grid.init(); + const recalculateSpy = vi.spyOn(grid as any, 'recalculateHeaderHeight'); const sedOnBeforeResize = new SlickEventData(); sedOnBeforeResize.addReturnValue(true); vi.spyOn(grid.onBeforeColumnsResize, 'notify').mockReturnValue(sedOnBeforeResize); @@ -4586,6 +4674,7 @@ describe('SlickGrid core file', () => { vi.advanceTimersByTime(10); expect(columnElms[1].classList.contains('slick-header-column-active')).toBeFalsy(); + expect(recalculateSpy).toHaveBeenCalledTimes(1); expect(onColumnsResizedSpy).toHaveBeenCalledWith({ triggeredByColumn: 'lastName', grid }, expect.anything(), grid); expect(columns[0].width).toBe(80); expect(columns[1].width).toBe(0); diff --git a/packages/common/src/core/slickGrid.ts b/packages/common/src/core/slickGrid.ts index 73af2b6e4..68a565e1c 100755 --- a/packages/common/src/core/slickGrid.ts +++ b/packages/common/src/core/slickGrid.ts @@ -260,6 +260,7 @@ export class SlickGrid = Column, O e asyncEditorLoading: false, asyncEditorLoadDelay: 100, forceFitColumns: false, + autoHeaderHeight: false, autoScrollOnColumnResize: true, autoScrollResizeLeftDelay: RESIZE_AUTOSCROLL_BROWSER_EDGE_LEFT_DELAY_MS, autoScrollResizeRightDelay: RESIZE_AUTOSCROLL_BROWSER_EDGE_RIGHT_DELAY_MS, @@ -977,6 +978,10 @@ export class SlickGrid = Column, O e this.setupColumnSort(); this.createCssRules(); this.resizeCanvas(); + + if (this._options.autoHeaderHeight) { + this.recalculateHeaderHeight(); + } this.bindAncestorScrollEvents(); this._bindingEventService.bind(this._container, 'resize', this.resizeCanvas.bind(this)); @@ -2033,6 +2038,57 @@ export class SlickGrid = Column, O e this.setupColumnReorder(); } } + + this.handleAutoHeaderHeightChange(); + } + + /** Adds or removes the automatic header-height styles from both header panes. */ + protected handleAutoHeaderHeightChange(): void { + const enabled = !!this._options.autoHeaderHeight; + const headers = [this._headerScrollerL, this._headerScrollerR].filter((header): header is HTMLDivElement => !!header); + + headers.forEach((header) => header.classList.toggle('slick-header-auto-height', enabled)); + + if (!enabled) { + this.clearAutoHeaderHeightStyles(headers); + } + } + + /** Measures natural header heights and applies the largest one to every header pane. */ + protected recalculateHeaderHeight(): void { + if (!this._headerScrollerL) { + return; + } + + const headers = [this._headerScrollerL, this._headerScrollerR].filter((header): header is HTMLDivElement => !!header); + const currentHeight = parseFloat(this._headerScrollerL.style.getPropertyValue('--slick-auto-header-height') || '0'); + + // Remove the previous calculated height before measuring the rendered header content. + this.clearAutoHeaderHeightStyles(headers); + const maxHeight = Math.max(...headers.map((header) => header.getBoundingClientRect().height)); + + if (maxHeight > 0) { + this.setAutoHeaderHeightStyles(maxHeight, headers); + + // A viewport resize is only necessary when the calculated height actually changed. + if (Math.abs(maxHeight - currentHeight) > 0.5) { + this.resizeCanvas(); + } + } + } + + protected clearAutoHeaderHeightStyles(headers: HTMLDivElement[]): void { + headers.forEach((header) => { + header.style.removeProperty('--slick-auto-header-height'); + header.style.height = ''; + }); + } + + protected setAutoHeaderHeightStyles(height: number, headers: HTMLDivElement[]): void { + headers.forEach((header) => { + header.style.setProperty('--slick-auto-header-height', `${height}px`); + header.style.height = `${height}px`; + }); } protected setupColumnSort(): void { @@ -2693,7 +2749,11 @@ export class SlickGrid = Column, O e } } this.updateCanvasWidth(true); - this.render(); + if (this._options.autoHeaderHeight) { + this.recalculateHeaderHeight(); + } else { + this.render(); + } this.scrollToX(this._viewportScrollContainerX.scrollLeft); this.triggerEvent(this.onColumnsResized, { triggeredByColumn }); clearTimeout(this._columnResizeTimer); @@ -3239,6 +3299,10 @@ export class SlickGrid = Column, O e this.applyColumnHeaderWidths(); this.updateCanvasWidth(true); + if (this._options.autoHeaderHeight) { + this.recalculateHeaderHeight(); + } + this.triggerEvent(this.onAutosizeColumns, { columns: this.columns }); if (reRender) { @@ -3679,6 +3743,9 @@ export class SlickGrid = Column, O e this.resizeCanvas(); this.updateCanvasWidth(); this.applyColumnWidths(); + if (this._options.autoHeaderHeight) { + this.recalculateHeaderHeight(); + } this.handleScroll(); this.getSelectionModel()?.refreshSelections(); } @@ -3829,6 +3896,9 @@ export class SlickGrid = Column, O e } else if (this._options.enableMouseWheelScrollHandler === false) { this.destroyAllInstances(this.slickMouseWheelInstances); // remove scroll handler when option is disable } + + // Keep header classes and styles synchronized when column rebuilding is suppressed. + this.handleAutoHeaderHeightChange(); } protected validateAndEnforceOptions(): void { diff --git a/packages/common/src/interfaces/gridOption.interface.ts b/packages/common/src/interfaces/gridOption.interface.ts index c65c9ef4c..7895ec422 100644 --- a/packages/common/src/interfaces/gridOption.interface.ts +++ b/packages/common/src/interfaces/gridOption.interface.ts @@ -160,6 +160,12 @@ export interface GridOption { /** Defaults to false, which leads to automatically adjust the size (height) of the grid to display the entire content without any scrolling in the grid. */ autoHeight?: boolean; + /** + * Defaults to false. When enabled, measures rendered column headers and adjusts their shared height to fit multi-line text, HTML, or DOM content. + * Header heights are kept in sync when frozen columns are used and recalculated after column width changes. + */ + autoHeaderHeight?: boolean; + /** * Defaults to 60, when "autoFixResizeWhenBrokenStyleDetected" is enabled then what will be the delay timeout before quitting? * Note that that the resize gets called every 200ms diff --git a/packages/common/src/styles/_variables.scss b/packages/common/src/styles/_variables.scss index f36316aae..5e975b50d 100644 --- a/packages/common/src/styles/_variables.scss +++ b/packages/common/src/styles/_variables.scss @@ -134,6 +134,7 @@ $slick-header-border-right: 0 none !default; $slick-header-border-bottom: 0 none !default; $slick-header-border-left: 0 none !default; $slick-header-column-height: calc(17px * #{$slick-header-row-count}) !default; // header is calculated by rows to show +$slick-auto-header-height-extra: 8px !default; $slick-header-column-background-active: color.adjust($slick-grid-header-background, $lightness: -5%) !default; $slick-header-column-background-hover: color.adjust($slick-grid-header-background, $lightness: -2%) !default; $slick-header-column-sortable-background-hover: #e0e0e0 !default; diff --git a/packages/common/src/styles/slick-grid.scss b/packages/common/src/styles/slick-grid.scss index 17c94f6d5..a4b6326d8 100644 --- a/packages/common/src/styles/slick-grid.scss +++ b/packages/common/src/styles/slick-grid.scss @@ -454,6 +454,31 @@ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } + .slick-header.slick-header-auto-height { + .slick-header-columns, + .slick-header-columns-left, + .slick-header-columns-right { + height: var(--slick-auto-header-height); + white-space: normal; + overflow: visible; + } + + .slick-header-column { + height: auto; + height: calc(var(--slick-auto-header-height) - var(--slick-auto-header-height-extra, v.$slick-auto-header-height-extra)); + overflow: visible; + } + + .slick-column-name { + display: block; + white-space: pre-wrap; + overflow: visible; + text-overflow: clip; + margin-bottom: 0; + -webkit-line-clamp: unset; + } + } + .slick-column-name { text-overflow: ellipsis; display: -webkit-box; diff --git a/test/cypress/e2e/example33.cy.ts b/test/cypress/e2e/example33.cy.ts index 928c0629e..8e8310f31 100644 --- a/test/cypress/e2e/example33.cy.ts +++ b/test/cypress/e2e/example33.cy.ts @@ -30,6 +30,23 @@ describe('Example 33 - Column & Row Span', { retries: 0 }, () => { cy.get('h3').should('contain', 'Example 33 - colspan/rowspan with large dataset'); }); + it('should calculate a height that fits the wrapped Revenue Growth header', () => { + cy.get('.slick-header-auto-height').should('have.length', 2); + cy.get('.slick-header-auto-height') + .first() + .should(($header) => { + expect(parseFloat($header.css('--slick-auto-header-height'))).to.be.greaterThan(0); + }); + cy.get('.auto-header-height-demo').should(($header) => { + expect($header[0].scrollHeight).to.be.lte($header[0].clientHeight); + expect($header[0].clientHeight).to.be.lessThan(100); + const name = $header[0].querySelector('.slick-column-name'); + expect(name).to.exist; + const lineHeight = parseFloat(getComputedStyle(name!).lineHeight) || 16; + expect(name!.scrollHeight).to.be.within(lineHeight * 1.5, lineHeight * 2.5); + }); + }); + it('should have exact column titles', () => { cy.get('.slick-header-columns') .children()