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