diff --git a/packages/component-base/src/browser-utils.js b/packages/component-base/src/browser-utils.js
index 245c57bd8e..25e91fd2db 100644
--- a/packages/component-base/src/browser-utils.js
+++ b/packages/component-base/src/browser-utils.js
@@ -33,3 +33,6 @@ export const isTouch = (() => {
return false;
}
})();
+
+export const supportsAdoptingStyleSheets =
+ window.ShadowRoot && 'adoptedStyleSheets' in Document.prototype && 'replace' in CSSStyleSheet.prototype;
diff --git a/packages/grid/src/vaadin-grid-mixin.js b/packages/grid/src/vaadin-grid-mixin.js
index b7b885e181..a5355c368e 100644
--- a/packages/grid/src/vaadin-grid-mixin.js
+++ b/packages/grid/src/vaadin-grid-mixin.js
@@ -6,7 +6,15 @@
import { isElementHidden } from '@vaadin/a11y-base/src/focus-utils.js';
import { TabindexMixin } from '@vaadin/a11y-base/src/tabindex-mixin.js';
import { animationFrame, microTask } from '@vaadin/component-base/src/async.js';
-import { isAndroid, isChrome, isFirefox, isIOS, isSafari, isTouch } from '@vaadin/component-base/src/browser-utils.js';
+import {
+ isAndroid,
+ isChrome,
+ isFirefox,
+ isIOS,
+ isSafari,
+ isTouch,
+ supportsAdoptingStyleSheets,
+} from '@vaadin/component-base/src/browser-utils.js';
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
import { getClosestElement } from '@vaadin/component-base/src/dom-utils.js';
import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';
@@ -269,6 +277,16 @@ export const GridMixin = (superClass) =>
}),
).observe(this.$.table);
+ const minHeightObserver = new ResizeObserver(() =>
+ setTimeout(() => {
+ this.__updateMinHeight();
+ }),
+ );
+
+ minHeightObserver.observe(this.$.header);
+ minHeightObserver.observe(this.$.items);
+ minHeightObserver.observe(this.$.footer);
+
processTemplates(this);
this._tooltipController = new TooltipController(this);
@@ -1116,4 +1134,28 @@ export const GridMixin = (superClass) =>
this.__virtualizer.update(start, end);
}
}
+
+ /** @private */
+ __updateMinHeight() {
+ // Min height is calculated based on the header, footer and a single row
+ // For now use a hard-coded value for the row that matches a single default row in Lumo
+ const rowHeight = 36;
+ const headerHeight = this.$.header.clientHeight;
+ const footerHeight = this.$.footer.clientHeight;
+ const scrollbarHeight = this.$.table.offsetHeight - this.$.table.clientHeight;
+ const minHeight = headerHeight + rowHeight + footerHeight + scrollbarHeight;
+
+ // The style is set to host instead of the scroller so that the value can be overridden by the user with "grid { min-height: 0 }"
+ // Prefer setting style in adopted style sheet to avoid the need to add a confusing inline style on the host element
+ // If adopted style sheets are not supported, the style is set inline
+ if (!this.__minHeightStyleSheet && supportsAdoptingStyleSheets) {
+ this.__minHeightStyleSheet = new CSSStyleSheet();
+ this.shadowRoot.adoptedStyleSheets = [...this.shadowRoot.adoptedStyleSheets, this.__minHeightStyleSheet];
+ }
+ if (this.__minHeightStyleSheet) {
+ this.__minHeightStyleSheet.replaceSync(`:host { --_grid-min-height: ${minHeight}px; }`);
+ } else {
+ this.style.setProperty('--_grid-min-height', `${minHeight}px`);
+ }
+ }
};
diff --git a/packages/grid/src/vaadin-grid-styles.js b/packages/grid/src/vaadin-grid-styles.js
index a23ad3a9eb..7fd9129503 100644
--- a/packages/grid/src/vaadin-grid-styles.js
+++ b/packages/grid/src/vaadin-grid-styles.js
@@ -17,6 +17,7 @@ export const gridStyles = css`
flex-direction: column;
animation: 1ms vaadin-grid-appear;
height: 400px;
+ min-height: var(--_grid-min-height, 0);
flex: 1 1 auto;
align-self: stretch;
position: relative;
diff --git a/packages/grid/test/min-height-lit.test.js b/packages/grid/test/min-height-lit.test.js
new file mode 100644
index 0000000000..6e917cea80
--- /dev/null
+++ b/packages/grid/test/min-height-lit.test.js
@@ -0,0 +1,3 @@
+import '../theme/lumo/lit-all-imports.js';
+import '../src/lit-all-imports.js';
+import './min-height.common.js';
diff --git a/packages/grid/test/min-height-polymer.test.js b/packages/grid/test/min-height-polymer.test.js
new file mode 100644
index 0000000000..a40277f372
--- /dev/null
+++ b/packages/grid/test/min-height-polymer.test.js
@@ -0,0 +1,2 @@
+import '../vaadin-grid.js';
+import './min-height.common.js';
diff --git a/packages/grid/test/min-height.common.js b/packages/grid/test/min-height.common.js
new file mode 100644
index 0000000000..0e3af294a7
--- /dev/null
+++ b/packages/grid/test/min-height.common.js
@@ -0,0 +1,118 @@
+import { expect } from '@vaadin/chai-plugins';
+import { fixtureSync } from '@vaadin/testing-helpers';
+import { flushGrid, infiniteDataProvider, nextResize } from './helpers.js';
+
+describe('min-height', () => {
+ const rowHeight = 36;
+
+ let grid;
+
+ function verifyMinHeight(withHeader = false, withFooter = false) {
+ const height = grid.getBoundingClientRect().height;
+
+ const headerHeight = grid.$.header.getBoundingClientRect().height;
+ if (withHeader) {
+ expect(headerHeight).to.be.above(0);
+ } else {
+ expect(headerHeight).to.equal(0);
+ }
+
+ const footerHeight = grid.$.footer.getBoundingClientRect().height;
+ if (withFooter) {
+ expect(footerHeight).to.be.above(0);
+ } else {
+ expect(footerHeight).to.equal(0);
+ }
+
+ const expectedHeight = rowHeight + headerHeight + footerHeight;
+ expect(height).to.equal(expectedHeight);
+ }
+
+ beforeEach(async () => {
+ grid = fixtureSync(`
+
+
+
+ `);
+ flushGrid(grid);
+ await nextResize(grid);
+ });
+
+ describe('without header or footer', () => {
+ it('should should have min-height of one row', () => {
+ verifyMinHeight();
+ });
+ });
+
+ describe('with header', () => {
+ beforeEach(async () => {
+ grid.querySelector('vaadin-grid-column').header = 'Header';
+ flushGrid(grid);
+ await nextResize(grid);
+ });
+
+ it('should should have min-height of header and one row', () => {
+ verifyMinHeight(true, false);
+ });
+ });
+
+ describe('with footer', () => {
+ beforeEach(async () => {
+ grid.querySelector('vaadin-grid-column').footerRenderer = (root) => {
+ root.textContent = 'Footer';
+ };
+ flushGrid(grid);
+ await nextResize(grid);
+ });
+
+ it('should should have min-height of footer and one row', () => {
+ verifyMinHeight(false, true);
+ });
+ });
+
+ describe('with header and footer', () => {
+ beforeEach(async () => {
+ grid.querySelector('vaadin-grid-column').header = 'Header';
+ grid.querySelector('vaadin-grid-column').footerRenderer = (root) => {
+ root.textContent = 'Footer';
+ };
+ flushGrid(grid);
+ await nextResize(grid);
+ });
+
+ it('should should have min-height of header, footer and one row', () => {
+ verifyMinHeight(true, true);
+ });
+ });
+
+ describe('with data', () => {
+ beforeEach(async () => {
+ grid.querySelector('vaadin-grid-column').path = 'value';
+ grid.querySelector('vaadin-grid-column').header = null;
+ grid.dataProvider = infiniteDataProvider;
+ flushGrid(grid);
+ await nextResize(grid);
+ });
+
+ it('should should have min-height of one row', () => {
+ verifyMinHeight();
+ });
+ });
+
+ describe('override', () => {
+ beforeEach(() => {
+ fixtureSync(`
+
+ `);
+ });
+
+ it('should allow overriding min-height through stylesheet', () => {
+ const height = grid.getBoundingClientRect().height;
+ expect(height).to.equal(200);
+ });
+ });
+});
diff --git a/packages/grid/test/visual/lumo/screenshots/grid/baseline/ltr-header-footer.png b/packages/grid/test/visual/lumo/screenshots/grid/baseline/ltr-header-footer.png
index 839733d9f1..eb4d1f22fb 100644
Binary files a/packages/grid/test/visual/lumo/screenshots/grid/baseline/ltr-header-footer.png and b/packages/grid/test/visual/lumo/screenshots/grid/baseline/ltr-header-footer.png differ
diff --git a/packages/grid/test/visual/lumo/screenshots/grid/baseline/rtl-header-footer.png b/packages/grid/test/visual/lumo/screenshots/grid/baseline/rtl-header-footer.png
index 6b2a2e8132..0bb87728c4 100644
Binary files a/packages/grid/test/visual/lumo/screenshots/grid/baseline/rtl-header-footer.png and b/packages/grid/test/visual/lumo/screenshots/grid/baseline/rtl-header-footer.png differ
diff --git a/packages/grid/test/visual/material/screenshots/grid/baseline/ltr-header-footer.png b/packages/grid/test/visual/material/screenshots/grid/baseline/ltr-header-footer.png
index 937e34ceb6..8145a10849 100644
Binary files a/packages/grid/test/visual/material/screenshots/grid/baseline/ltr-header-footer.png and b/packages/grid/test/visual/material/screenshots/grid/baseline/ltr-header-footer.png differ
diff --git a/packages/grid/test/visual/material/screenshots/grid/baseline/rtl-header-footer.png b/packages/grid/test/visual/material/screenshots/grid/baseline/rtl-header-footer.png
index 68ff572e76..84c705cde5 100644
Binary files a/packages/grid/test/visual/material/screenshots/grid/baseline/rtl-header-footer.png and b/packages/grid/test/visual/material/screenshots/grid/baseline/rtl-header-footer.png differ