diff --git a/packages/grid/src/vaadin-grid-active-item-mixin.js b/packages/grid/src/vaadin-grid-active-item-mixin.js
index 607f35b166..2145b4ab7c 100644
--- a/packages/grid/src/vaadin-grid-active-item-mixin.js
+++ b/packages/grid/src/vaadin-grid-active-item-mixin.js
@@ -83,7 +83,7 @@ export const ActiveItemMixin = (superClass) =>
const path = e.composedPath();
const cell = path[path.indexOf(this.$.table) - 3];
- if (!cell || cell.getAttribute('part').indexOf('details-cell') > -1) {
+ if (!cell || cell.getAttribute('part').indexOf('details-cell') > -1 || cell === this.$.emptystatecell) {
return;
}
const cellContent = cell._content;
diff --git a/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js b/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js
index 101f84d051..3a3402df47 100644
--- a/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js
+++ b/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js
@@ -675,7 +675,7 @@ export const KeyboardNavigationMixin = (superClass) =>
const tabOrder = [
this.$.table,
this._headerFocusable,
- this._itemsFocusable,
+ this.__emptyState ? this.$.emptystatecell : this._itemsFocusable,
this._footerFocusable,
this.$.focusexit,
];
@@ -860,7 +860,7 @@ export const KeyboardNavigationMixin = (superClass) =>
if (cell) {
const context = this.getEventContext(e);
this.__pendingBodyCellFocus = this.loading && context.section === 'body';
- if (!this.__pendingBodyCellFocus) {
+ if (!this.__pendingBodyCellFocus && cell !== this.$.emptystatecell) {
// Fire a cell-focus event for the cell
cell.dispatchEvent(new CustomEvent('cell-focus', { bubbles: true, composed: true, detail: { context } }));
}
@@ -907,7 +907,7 @@ export const KeyboardNavigationMixin = (superClass) =>
* @private
*/
_detectInteracting(e) {
- const isInteracting = e.composedPath().some((el) => el.localName === 'vaadin-grid-cell-content');
+ const isInteracting = e.composedPath().some((el) => el.localName === 'slot' && this.shadowRoot.contains(el));
this._setInteracting(isInteracting);
this.__updateHorizontalScrollPosition();
}
diff --git a/packages/grid/src/vaadin-grid-mixin.js b/packages/grid/src/vaadin-grid-mixin.js
index 85d4df21fb..1535a1fe39 100644
--- a/packages/grid/src/vaadin-grid-mixin.js
+++ b/packages/grid/src/vaadin-grid-mixin.js
@@ -9,6 +9,7 @@ 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 { 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';
import { processTemplates } from '@vaadin/component-base/src/templates.js';
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
import { Virtualizer } from '@vaadin/component-base/src/virtualizer.js';
@@ -156,6 +157,18 @@ export const GridMixin = (superClass) =>
type: Boolean,
value: true,
},
+
+ /** @private */
+ __hasEmptyStateContent: {
+ type: Boolean,
+ value: false,
+ },
+
+ /** @private */
+ __emptyState: {
+ type: Boolean,
+ computed: '__computeEmptyState(_flatSize, __hasEmptyStateContent)',
+ },
};
}
@@ -261,6 +274,11 @@ export const GridMixin = (superClass) =>
this._tooltipController = new TooltipController(this);
this.addController(this._tooltipController);
this._tooltipController.setManual(true);
+
+ this.__emptyStateContentObserver = new SlotObserver(this.$.emptystateslot, ({ currentNodes }) => {
+ this.$.emptystatecell._content = currentNodes[0];
+ this.__hasEmptyStateContent = !!this.$.emptystatecell._content;
+ });
}
/** @private */
@@ -864,6 +882,11 @@ export const GridMixin = (superClass) =>
});
}
+ /** @private */
+ __computeEmptyState(flatSize, hasEmptyStateContent) {
+ return flatSize === 0 && hasEmptyStateContent;
+ }
+
/**
* @param {!Array} columnTree
* @protected
diff --git a/packages/grid/src/vaadin-grid-styles.js b/packages/grid/src/vaadin-grid-styles.js
index 419491ceab..a23ad3a9eb 100644
--- a/packages/grid/src/vaadin-grid-styles.js
+++ b/packages/grid/src/vaadin-grid-styles.js
@@ -189,6 +189,32 @@ export const gridStyles = css`
overflow: hidden;
}
+ /* Empty state */
+
+ #scroller:not([empty-state]) #emptystatebody,
+ #scroller[empty-state] #items {
+ display: none;
+ }
+
+ #emptystatebody {
+ display: flex;
+ position: sticky;
+ inset: 0;
+ flex: 1;
+ overflow: hidden;
+ }
+
+ #emptystaterow {
+ display: flex;
+ flex: 1;
+ }
+
+ #emptystatecell {
+ display: block;
+ flex: 1;
+ overflow: auto;
+ }
+
/* Reordering styles */
:host([reordering]) [part~='cell'] ::slotted(vaadin-grid-cell-content),
:host([reordering]) [part~='resize-handle'],
diff --git a/packages/grid/src/vaadin-grid.d.ts b/packages/grid/src/vaadin-grid.d.ts
index 084efbddd4..058306e0e3 100644
--- a/packages/grid/src/vaadin-grid.d.ts
+++ b/packages/grid/src/vaadin-grid.d.ts
@@ -205,6 +205,7 @@ export type GridDefaultItem = any;
* `reorder-allowed-cell` | Cell in a column where another column can be reordered
* `reorder-dragging-cell` | Cell in a column currently being reordered
* `resize-handle` | Handle for resizing the columns
+ * `empty-state` | The container for the content to be displayed when there are no body rows to show
* `reorder-ghost` | Ghost element of the header cell being dragged
*
* The following state attributes are available for styling:
diff --git a/packages/grid/src/vaadin-grid.js b/packages/grid/src/vaadin-grid.js
index 6b23311e1b..00442bb8f0 100644
--- a/packages/grid/src/vaadin-grid.js
+++ b/packages/grid/src/vaadin-grid.js
@@ -205,6 +205,7 @@ registerStyles('vaadin-grid', gridStyles, { moduleId: 'vaadin-grid-styles' });
* `reorder-allowed-cell` | Cell in a column where another column can be reordered
* `reorder-dragging-cell` | Cell in a column currently being reordered
* `resize-handle` | Handle for resizing the columns
+ * `empty-state` | The container for the content to be displayed when there are no body rows to show
* `reorder-ghost` | Ghost element of the header cell being dragged
*
* The following state attributes are available for styling:
@@ -268,11 +269,19 @@ class Grid extends GridMixin(ElementMixin(ThemableMixin(ControllerMixin(PolymerE
ios$="[[_ios]]"
loading$="[[loading]]"
column-reordering-allowed$="[[columnReorderingAllowed]]"
+ empty-state$="[[__emptyState]]"
>
diff --git a/packages/grid/src/vaadin-lit-grid.js b/packages/grid/src/vaadin-lit-grid.js
index a19746ff8b..8ea129a989 100644
--- a/packages/grid/src/vaadin-lit-grid.js
+++ b/packages/grid/src/vaadin-lit-grid.js
@@ -40,11 +40,19 @@ class Grid extends GridMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement)
ios="${isIOS}"
?loading="${this.loading}"
column-reordering-allowed="${this.columnReorderingAllowed}"
+ ?empty-state="${this.__emptyState}"
>
diff --git a/packages/grid/test/accessibility.common.js b/packages/grid/test/accessibility.common.js
index b16dd7a79b..4e00b3e6b2 100644
--- a/packages/grid/test/accessibility.common.js
+++ b/packages/grid/test/accessibility.common.js
@@ -269,7 +269,9 @@ describe('accessibility', () => {
col.path = 'value';
flushGrid(grid);
- const rowCount = Array.from(grid.$.table.querySelectorAll('tr')).filter((tr) => !tr.hidden).length;
+ const rowCount = Array.from(grid.$.table.querySelectorAll('tr:not(#emptystaterow)')).filter(
+ (tr) => !tr.hidden,
+ ).length;
expect(grid.$.table.getAttribute('aria-rowcount')).to.equal(String(rowCount));
expect(grid.$.table.getAttribute('aria-rowcount')).to.equal('3');
});
@@ -279,7 +281,9 @@ describe('accessibility', () => {
col.header = null;
flushGrid(grid);
- const rowCount = Array.from(grid.$.table.querySelectorAll('tr')).filter((tr) => !tr.hidden).length;
+ const rowCount = Array.from(grid.$.table.querySelectorAll('tr:not(#emptystaterow)')).filter(
+ (tr) => !tr.hidden,
+ ).length;
expect(grid.$.table.getAttribute('aria-rowcount')).to.equal(String(rowCount));
expect(grid.$.table.getAttribute('aria-rowcount')).to.equal('2');
});
@@ -323,7 +327,7 @@ describe('accessibility', () => {
}
it('should have aria-rowindex on rows', () => {
- Array.from(grid.$.table.querySelectorAll('tr')).forEach((row, index) => {
+ Array.from(grid.$.table.querySelectorAll('tr:not(#emptystaterow)')).forEach((row, index) => {
expect(row.getAttribute('aria-rowindex')).to.equal((index + 1).toString());
});
});
diff --git a/packages/grid/test/basic.common.js b/packages/grid/test/basic.common.js
index 06dcb29969..bae8ceccb1 100644
--- a/packages/grid/test/basic.common.js
+++ b/packages/grid/test/basic.common.js
@@ -50,7 +50,7 @@ describe('basic features', () => {
it('check visible item count', () => {
grid.size = 10;
flushGrid(grid);
- expect(grid.shadowRoot.querySelectorAll('tbody tr:not([hidden])').length).to.eql(10);
+ expect(grid.shadowRoot.querySelectorAll('tbody tr:not([hidden]):not(#emptystaterow)').length).to.eql(10);
});
it('first visible item', () => {
@@ -363,3 +363,126 @@ describe('flex child', () => {
});
});
});
+
+describe('empty state', () => {
+ let grid;
+
+ function getEmptyState() {
+ return grid.querySelector('[slot="empty-state"]');
+ }
+
+ function emptyStateVisible() {
+ return getEmptyState()?.offsetHeight > 0;
+ }
+
+ function itemsBodyVisible() {
+ return grid.$.items.offsetHeight > 0;
+ }
+
+ beforeEach(async () => {
+ grid = fixtureSync(`
+
+
+
+ No items
+
+
+ `);
+
+ grid.querySelector('vaadin-grid-column').footerRenderer = (root) => {
+ root.textContent = 'Footer';
+ };
+ await nextFrame();
+ });
+
+ it('should show empty state', () => {
+ expect(emptyStateVisible()).to.be.true;
+ expect(itemsBodyVisible()).to.be.false;
+ });
+
+ it('should not show empty state when grid has items', async () => {
+ grid.items = [{ name: 'foo' }];
+ await nextFrame();
+ expect(emptyStateVisible()).to.be.false;
+ expect(itemsBodyVisible()).to.be.true;
+ });
+
+ it('should not show empty state when empty state content is not defined', async () => {
+ grid.removeChild(getEmptyState());
+ await nextFrame();
+ expect(emptyStateVisible()).to.be.false;
+ expect(itemsBodyVisible()).to.be.true;
+ });
+
+ it('should not throw on empty state click', () => {
+ expect(() => getEmptyState().click()).not.to.throw();
+ });
+
+ it('should not dispatch cell-activate on empty state click', () => {
+ const spy = sinon.spy();
+ grid.addEventListener('cell-activate', spy);
+ getEmptyState().click();
+ expect(spy.called).to.be.false;
+ });
+
+ describe('bounds', () => {
+ let gridRect, emptyStateCellRect, headerRect, footerRect;
+
+ beforeEach(() => {
+ gridRect = grid.getBoundingClientRect();
+ emptyStateCellRect = grid.$.emptystatecell.getBoundingClientRect();
+ headerRect = grid.$.header.getBoundingClientRect();
+ footerRect = grid.$.footer.getBoundingClientRect();
+ });
+
+ it('should cover the viewport', () => {
+ expect(emptyStateCellRect.top).to.be.closeTo(headerRect.bottom, 1);
+ expect(emptyStateCellRect.bottom).to.be.closeTo(footerRect.top, 1);
+ expect(emptyStateCellRect.left).to.be.closeTo(gridRect.left, 1);
+ expect(emptyStateCellRect.right).to.be.closeTo(gridRect.right, 1);
+ });
+
+ it('should push footer to the bottom of the viewport', () => {
+ expect(footerRect.bottom).to.be.closeTo(gridRect.bottom, 1);
+ });
+
+ it('should not scroll horizontally with the columns', () => {
+ grid.append(
+ ...Array.from({ length: 10 }, () => fixtureSync('')),
+ );
+ flushGrid(grid);
+
+ grid.$.table.scrollLeft = grid.$.table.scrollWidth;
+ emptyStateCellRect = grid.$.emptystatecell.getBoundingClientRect();
+ expect(emptyStateCellRect.left).to.be.closeTo(gridRect.left, 1);
+ expect(emptyStateCellRect.right).to.be.closeTo(gridRect.right, 1);
+ });
+
+ it('should not scroll verticaly with the columns', () => {
+ getEmptyState().innerHTML = Array.from({ length: 10 }, () => 'Lorem ipsum dolor sit amet
').join('');
+ flushGrid(grid);
+
+ grid.$.emptystatecell.scrollTop = grid.$.emptystatecell.scrollHeight;
+ expect(grid.$.emptystatecell.scrollTop).to.be.greaterThan(0);
+ emptyStateCellRect = grid.$.emptystatecell.getBoundingClientRect();
+ expect(emptyStateCellRect.top).to.be.closeTo(headerRect.bottom, 1);
+ expect(emptyStateCellRect.bottom).to.be.closeTo(footerRect.top, 1);
+ });
+
+ it('should not overflow on all-rows-visible', () => {
+ grid.allRowsVisible = true;
+ grid.style.width = '200px';
+ getEmptyState().innerHTML = Array.from({ length: 10 }, () => 'Lorem ipsum dolor sit amet
').join('');
+ flushGrid(grid);
+
+ const emptyStateRect = getEmptyState().getBoundingClientRect();
+ gridRect = grid.getBoundingClientRect();
+ headerRect = grid.$.header.getBoundingClientRect();
+ footerRect = grid.$.footer.getBoundingClientRect();
+ expect(emptyStateRect.top).to.be.greaterThan(headerRect.bottom);
+ expect(emptyStateRect.bottom).to.be.lessThan(footerRect.top);
+ expect(emptyStateRect.left).to.be.greaterThan(gridRect.left);
+ expect(emptyStateRect.right).to.be.lessThan(gridRect.right);
+ });
+ });
+});
diff --git a/packages/grid/test/dom/__snapshots__/grid.test.snap.js b/packages/grid/test/dom/__snapshots__/grid.test.snap.js
index 745425f8a4..13ea28d52f 100644
--- a/packages/grid/test/dom/__snapshots__/grid.test.snap.js
+++ b/packages/grid/test/dom/__snapshots__/grid.test.snap.js
@@ -196,6 +196,21 @@ snapshots["vaadin-grid shadow default"] =
+
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+