From b1d92a134040d5bbaec9d4da8562d2aeaebc6f02 Mon Sep 17 00:00:00 2001
From: ghiscoding
Date: Tue, 1 Sep 2026 19:27:14 -0400
Subject: [PATCH 1/2] feat(common): add automatic height for multiline column
headers
---
AGENTS.md | 1 +
.../src/examples/slickgrid/example44.html | 4 +
.../src/examples/slickgrid/example44.ts | 5 +-
.../aurelia/test/cypress/e2e/example44.cy.ts | 13 +++
.../src/examples/slickgrid/Example44.tsx | 9 +-
demos/react/test/cypress/e2e/example44.cy.ts | 13 +++
demos/vanilla/src/examples/example33.html | 4 +
demos/vanilla/src/examples/example33.ts | 5 +-
demos/vue/src/components/Example44.vue | 9 +-
demos/vue/test/cypress/e2e/example44.cy.ts | 13 +++
docs/styling/multiple-column-header-rows.md | 14 ++-
.../styling/multiple-column-header-rows.md | 14 ++-
.../demos/examples/example44.component.html | 4 +
.../src/demos/examples/example44.component.ts | 5 +-
.../test/cypress/e2e/example44.cy.ts | 13 +++
.../styling/multiple-column-header-rows.md | 14 ++-
.../styling/multiple-column-header-rows.md | 14 ++-
.../styling/multiple-column-header-rows.md | 14 ++-
.../src/core/__tests__/slickGrid.spec.ts | 91 ++++++++++++++++++-
packages/common/src/core/slickGrid.ts | 72 ++++++++++++++-
.../src/interfaces/gridOption.interface.ts | 6 ++
packages/common/src/styles/_variables.scss | 1 +
packages/common/src/styles/slick-grid.scss | 25 +++++
test/cypress/e2e/example33.cy.ts | 13 +++
24 files changed, 364 insertions(+), 12 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index ed738ed169..fea85f6e49 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 8a3eef20f6..64094aff99 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 f07cc69197..640a2914aa 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 589ebd01e9..d2af7d8ea4 100644
--- a/demos/aurelia/test/cypress/e2e/example44.cy.ts
+++ b/demos/aurelia/test/cypress/e2e/example44.cy.ts
@@ -30,6 +30,19 @@ 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);
+ });
+ });
+
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 44edcab148..7f896de514 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 589ebd01e9..d2af7d8ea4 100644
--- a/demos/react/test/cypress/e2e/example44.cy.ts
+++ b/demos/react/test/cypress/e2e/example44.cy.ts
@@ -30,6 +30,19 @@ 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);
+ });
+ });
+
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 691ffb2e34..0086f44ee7 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 62695a910e..bbd544e6d8 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 ff96e74494..75e6ebb196 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 589ebd01e9..d2af7d8ea4 100644
--- a/demos/vue/test/cypress/e2e/example44.cy.ts
+++ b/demos/vue/test/cypress/e2e/example44.cy.ts
@@ -30,6 +30,19 @@ 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);
+ });
+ });
+
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 4a5c66fda6..37ec8f1165 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 4a5c66fda6..37ec8f1165 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 ec33635670..5a0cbae6eb 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 048b16b73e..8e0b927c7f 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 589ebd01e9..d2af7d8ea4 100644
--- a/frameworks/angular-slickgrid/test/cypress/e2e/example44.cy.ts
+++ b/frameworks/angular-slickgrid/test/cypress/e2e/example44.cy.ts
@@ -30,6 +30,19 @@ 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);
+ });
+ });
+
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 4a5c66fda6..37ec8f1165 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 4a5c66fda6..37ec8f1165 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 4a5c66fda6..37ec8f1165 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 0636f950ae..51d3196972 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 73af2b6e4c..68a565e1c2 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 c65c9ef4c7..7895ec4228 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 f36316aae0..5e975b50d9 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 17c94f6d56..a4b6326d82 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 928c0629ed..a23ca122a9 100644
--- a/test/cypress/e2e/example33.cy.ts
+++ b/test/cypress/e2e/example33.cy.ts
@@ -30,6 +30,19 @@ 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);
+ });
+ });
+
it('should have exact column titles', () => {
cy.get('.slick-header-columns')
.children()
From aa488392d2c30b5f2206cbebd15936124a8aca7d Mon Sep 17 00:00:00 2001
From: ghiscoding
Date: Tue, 1 Sep 2026 19:40:12 -0400
Subject: [PATCH 2/2] chore improve Cypress test to see column on multi-lines
---
demos/aurelia/test/cypress/e2e/example44.cy.ts | 4 ++++
demos/react/test/cypress/e2e/example44.cy.ts | 4 ++++
demos/vue/test/cypress/e2e/example44.cy.ts | 4 ++++
frameworks/angular-slickgrid/test/cypress/e2e/example44.cy.ts | 4 ++++
test/cypress/e2e/example33.cy.ts | 4 ++++
5 files changed, 20 insertions(+)
diff --git a/demos/aurelia/test/cypress/e2e/example44.cy.ts b/demos/aurelia/test/cypress/e2e/example44.cy.ts
index d2af7d8ea4..5d5bf6f477 100644
--- a/demos/aurelia/test/cypress/e2e/example44.cy.ts
+++ b/demos/aurelia/test/cypress/e2e/example44.cy.ts
@@ -40,6 +40,10 @@ describe('Example 44 - Column & Row Span', { retries: 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);
});
});
diff --git a/demos/react/test/cypress/e2e/example44.cy.ts b/demos/react/test/cypress/e2e/example44.cy.ts
index d2af7d8ea4..5d5bf6f477 100644
--- a/demos/react/test/cypress/e2e/example44.cy.ts
+++ b/demos/react/test/cypress/e2e/example44.cy.ts
@@ -40,6 +40,10 @@ describe('Example 44 - Column & Row Span', { retries: 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);
});
});
diff --git a/demos/vue/test/cypress/e2e/example44.cy.ts b/demos/vue/test/cypress/e2e/example44.cy.ts
index d2af7d8ea4..5d5bf6f477 100644
--- a/demos/vue/test/cypress/e2e/example44.cy.ts
+++ b/demos/vue/test/cypress/e2e/example44.cy.ts
@@ -40,6 +40,10 @@ describe('Example 44 - Column & Row Span', { retries: 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);
});
});
diff --git a/frameworks/angular-slickgrid/test/cypress/e2e/example44.cy.ts b/frameworks/angular-slickgrid/test/cypress/e2e/example44.cy.ts
index d2af7d8ea4..5d5bf6f477 100644
--- a/frameworks/angular-slickgrid/test/cypress/e2e/example44.cy.ts
+++ b/frameworks/angular-slickgrid/test/cypress/e2e/example44.cy.ts
@@ -40,6 +40,10 @@ describe('Example 44 - Column & Row Span', { retries: 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);
});
});
diff --git a/test/cypress/e2e/example33.cy.ts b/test/cypress/e2e/example33.cy.ts
index a23ca122a9..8e8310f31b 100644
--- a/test/cypress/e2e/example33.cy.ts
+++ b/test/cypress/e2e/example33.cy.ts
@@ -40,6 +40,10 @@ describe('Example 33 - Column & Row Span', { retries: 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);
});
});