Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 179 additions & 13 deletions dev/grid.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,191 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Grid</title>
<script type="module" src="./common.js"></script>
<style>
body {
font-family: var(--lumo-font-family);
padding: 20px;
}

h2 {
margin-top: 40px;
margin-bottom: 20px;
color: var(--lumo-header-text-color);
}

vaadin-grid {
margin-bottom: 40px;
height: 400px;
}

/* Header and footer slot styles */
[part="header"] {
background: var(--lumo-contrast-5pct);
border-bottom: 1px solid var(--lumo-contrast-10pct);
padding: var(--lumo-space-s) var(--lumo-space-m);
gap: var(--lumo-space-m);
}

[part="footer"] {
background: var(--lumo-contrast-5pct);
border-top: 1px solid var(--lumo-contrast-10pct);
padding: var(--lumo-space-xs) var(--lumo-space-m);
font-size: var(--lumo-font-size-s);
color: var(--lumo-secondary-text-color);
gap: var(--lumo-space-s);
}

.flex-grow {
flex: 1;
}
</style>
</head>
<body>
<h1>Grid Examples</h1>
<script type="module">
import '@vaadin/grid/all-imports';
import '@vaadin/tooltip';
import '@vaadin/button';
import '@vaadin/text-field';
import '@vaadin/icon';
import '@vaadin/icons';
</script>

<h2>Grid with Header and Footer Slots (Flex Layout)</h2>
<vaadin-grid id="header-footer-grid">
<!-- Multiple elements directly in header slot using flex layout -->
<vaadin-icon slot="header" icon="vaadin:grid-h" style="color: var(--lumo-primary-color);"></vaadin-icon>
<strong slot="header">User Management</strong>
<span slot="header" class="flex-grow"></span>
<vaadin-text-field slot="header" placeholder="Search users..." clear-button-visible style="width: 250px;">
<vaadin-icon slot="prefix" icon="vaadin:search"></vaadin-icon>
</vaadin-text-field>
<vaadin-button slot="header" theme="primary">
<vaadin-icon icon="vaadin:plus" slot="prefix"></vaadin-icon>
Add User
</vaadin-button>

<vaadin-grid-selection-column></vaadin-grid-selection-column>
<vaadin-grid-column path="name" header="Name"></vaadin-grid-column>
<vaadin-grid-column path="email" header="Email"></vaadin-grid-column>
<vaadin-grid-column path="role" header="Role"></vaadin-grid-column>
<vaadin-grid-column path="status" header="Status"></vaadin-grid-column>

<!-- Multiple elements in footer -->
<span slot="footer">Total: <strong id="user-count">0</strong> users</span>
<span slot="footer" class="flex-grow"></span>
<span slot="footer">Selected: <strong id="selected-count">0</strong></span>
<span slot="footer" style="margin-left: var(--lumo-space-xl);">Last updated: <span id="update-time">Never</span></span>
</vaadin-grid>

<h2>Grid with Toolbar Actions</h2>
<vaadin-grid id="toolbar-grid">
<!-- Header with title and actions -->
<h3 slot="header" style="margin: 0;">Products</h3>
<div slot="header" style="margin-left: auto; display: flex; gap: var(--lumo-space-s);">
<vaadin-button theme="tertiary">
<vaadin-icon icon="vaadin:download" slot="prefix"></vaadin-icon>
Export
</vaadin-button>
<vaadin-button theme="tertiary">
<vaadin-icon icon="vaadin:upload" slot="prefix"></vaadin-icon>
Import
</vaadin-button>
<vaadin-button theme="error tertiary">
<vaadin-icon icon="vaadin:trash" slot="prefix"></vaadin-icon>
Delete
</vaadin-button>
</div>

<vaadin-grid-selection-column></vaadin-grid-selection-column>
<vaadin-grid-filter-column path="product" header="Product"></vaadin-grid-filter-column>
<vaadin-grid-filter-column path="category" header="Category"></vaadin-grid-filter-column>
<vaadin-grid-column path="price" header="Price"></vaadin-grid-column>
<vaadin-grid-column path="stock" header="Stock"></vaadin-grid-column>

<!-- Footer with status indicator -->
<span slot="footer" style="display: flex; align-items: center; gap: var(--lumo-space-xs);">
<span style="width: 8px; height: 8px; background: var(--lumo-success-color); border-radius: 50%;"></span>
All systems operational
</span>
<span slot="footer" style="margin-left: auto;">
Showing <strong id="product-count">0</strong> of <strong id="product-total">0</strong> products
</span>
</vaadin-grid>

<h2>Original Tree Grid Example</h2>
<vaadin-grid id="tree-grid" item-id-path="name">
<vaadin-grid-selection-column auto-select frozen drag-select></vaadin-grid-selection-column>
<vaadin-grid-tree-column frozen path="name" width="200px" flex-shrink="0"></vaadin-grid-tree-column>
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>

const grid = document.querySelector('vaadin-grid');
<vaadin-tooltip slot="tooltip" hover-delay="500" hide-delay="500"></vaadin-tooltip>
</vaadin-grid>

grid.dataProvider = ({ parentItem, page, pageSize }, cb) => {
<script type="module">
// Sample data for header-footer grid
const users = [
{ name: 'John Smith', email: '[email protected]', role: 'Administrator', status: 'Active' },
{ name: 'Jane Doe', email: '[email protected]', role: 'Editor', status: 'Active' },
{ name: 'Bob Johnson', email: '[email protected]', role: 'Viewer', status: 'Inactive' },
{ name: 'Alice Williams', email: '[email protected]', role: 'Editor', status: 'Active' },
{ name: 'Charlie Brown', email: '[email protected]', role: 'Viewer', status: 'Active' },
{ name: 'Diana Prince', email: '[email protected]', role: 'Administrator', status: 'Active' },
{ name: 'Edward Norton', email: '[email protected]', role: 'Editor', status: 'Active' },
{ name: 'Fiona Green', email: '[email protected]', role: 'Viewer', status: 'Inactive' },
];

const headerFooterGrid = document.querySelector('#header-footer-grid');
headerFooterGrid.items = users;
document.querySelector('#user-count').textContent = users.length;
document.querySelector('#update-time').textContent = new Date().toLocaleTimeString();

// Update selected count
headerFooterGrid.addEventListener('selected-items-changed', (e) => {
document.querySelector('#selected-count').textContent = e.detail.value.length;
});

// Handle search
const searchField = headerFooterGrid.querySelector('vaadin-text-field[slot="header"]');
searchField.addEventListener('value-changed', (e) => {
const searchTerm = e.detail.value.toLowerCase();
if (searchTerm) {
headerFooterGrid.items = users.filter(
(user) =>
user.name.toLowerCase().includes(searchTerm) ||
user.email.toLowerCase().includes(searchTerm) ||
user.role.toLowerCase().includes(searchTerm),
);
} else {
headerFooterGrid.items = users;
}
document.querySelector('#user-count').textContent = headerFooterGrid.items.length;
});

// Sample data for toolbar grid
const products = [
{ product: 'Laptop Pro', category: 'Electronics', price: '$1,299', stock: 45 },
{ product: 'Wireless Mouse', category: 'Accessories', price: '$29', stock: 120 },
{ product: 'USB-C Cable', category: 'Accessories', price: '$19', stock: 200 },
{ product: 'Monitor 27"', category: 'Electronics', price: '$399', stock: 30 },
{ product: 'Keyboard Mechanical', category: 'Accessories', price: '$79', stock: 85 },
{ product: 'Webcam HD', category: 'Electronics', price: '$99', stock: 60 },
{ product: 'Desk Lamp LED', category: 'Office', price: '$49', stock: 95 },
{ product: 'Office Chair', category: 'Furniture', price: '$299', stock: 25 },
{ product: 'Standing Desk', category: 'Furniture', price: '$599', stock: 15 },
{ product: 'Headphones', category: 'Electronics', price: '$149', stock: 70 },
];

const toolbarGrid = document.querySelector('#toolbar-grid');
toolbarGrid.items = products;
document.querySelector('#product-count').textContent = products.length;
document.querySelector('#product-total').textContent = products.length;

// Original tree grid setup
const treeGrid = document.querySelector('#tree-grid');
treeGrid.dataProvider = ({ parentItem, page, pageSize }, cb) => {
// Let's have 100 root-level items and 5 items on every child level
const levelSize = parentItem ? 5 : 100;

Expand All @@ -30,20 +206,10 @@
cb(pageItems, levelSize);
};

const tooltip = document.querySelector('[slot="tooltip"]');
const tooltip = treeGrid.querySelector('[slot="tooltip"]');
tooltip.generator = ({ column, item }) => {
return column && column.path && item ? `Tooltip ${column.path} ${item.name}` : '';
};
</script>

<vaadin-grid item-id-path="name">
<vaadin-grid-selection-column auto-select frozen drag-select></vaadin-grid-selection-column>
<vaadin-grid-tree-column frozen path="name" width="200px" flex-shrink="0"></vaadin-grid-tree-column>
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>

<vaadin-tooltip slot="tooltip" hover-delay="500" hide-delay="500"></vaadin-tooltip>
</vaadin-grid>
</body>
</html>
14 changes: 14 additions & 0 deletions packages/grid/src/styles/vaadin-grid-base-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const gridStyles = css`

:host {
display: flex;
flex-direction: column;
animation: 1ms vaadin-grid-appear;
max-width: 100%;
height: 400px;
Expand Down Expand Up @@ -63,13 +64,24 @@ export const gridStyles = css`
border-radius: calc(var(--_border-radius) - var(--_border-width));
position: relative;
display: flex;
flex-direction: column;
Copy link
Preview

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding flex-direction: column to the existing #scroller selector modifies the layout behavior of the entire scroller container. This change should be accompanied by verification that existing grid layouts still work correctly, as this could affect how child elements are arranged within the scroller.

Suggested change
flex-direction: column;
/* flex-direction: column; Removed to preserve original grid layout */

Copilot uses AI. Check for mistakes.

width: 100%;
min-width: 0;
min-height: 0;
flex: 1 1 auto;
align-self: stretch;
overflow: hidden;
}

[part='header'],
[part='footer'] {
display: flex;
align-items: center;
width: 100%;
flex-shrink: 0;
box-sizing: border-box;
}

#items {
flex-grow: 1;
flex-shrink: 0;
Expand All @@ -85,6 +97,8 @@ export const gridStyles = css`
display: flex;
flex-direction: column;
width: 100%;
flex: 1 1 auto;
min-height: 0;
overflow: auto;
position: relative;
border-radius: inherit;
Expand Down
16 changes: 16 additions & 0 deletions packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,22 @@ export const KeyboardNavigationMixin = (superClass) =>
return;
}

// When Tab (forward) would go to focusexit, skip the grid's Tab handling entirely
// to allow natural Tab order to work for footer content or overlay scenarios
if (focusTarget === this.$.focusexit && !e.shiftKey) {
// Prevent focus-trap logic from intercepting the event.
e.stopPropagation();
this.toggleAttribute('navigating', true);
// Remove focusexit from Tab order before the browser processes Tab
this.$.focusexit.tabIndex = -1;
// Restore it after the current event completes
setTimeout(() => {
this.$.focusexit.tabIndex = 0;
}, 0);
// Don't prevent default and don't focus anything - let browser handle Tab naturally
return;
}

// Prevent focus-trap logic from intercepting the event.
e.stopPropagation();

Expand Down
10 changes: 9 additions & 1 deletion packages/grid/src/vaadin-grid-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ export const GridMixin = (superClass) =>
minHeightObserver.observe(this.$.header);
minHeightObserver.observe(this.$.items);
minHeightObserver.observe(this.$.footer);
// Also observe the header and footer slot containers
minHeightObserver.observe(this.$.gridHeader);
minHeightObserver.observe(this.$.gridFooter);

this._tooltipController = new TooltipController(this);
this.addController(this._tooltipController);
Expand Down Expand Up @@ -961,7 +964,12 @@ export const GridMixin = (superClass) =>
const headerHeight = this.$.header.clientHeight;
const footerHeight = this.$.footer.clientHeight;
const scrollbarHeight = this.$.table.offsetHeight - this.$.table.clientHeight;
const minHeight = headerHeight + rowHeight + footerHeight + scrollbarHeight;

// Include header and footer slot container heights
const headerSlotHeight = this.$.gridHeader.clientHeight;
const footerSlotHeight = this.$.gridFooter.clientHeight;

const minHeight = headerHeight + rowHeight + footerHeight + scrollbarHeight + headerSlotHeight + footerSlotHeight;

// 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
Expand Down
2 changes: 2 additions & 0 deletions packages/grid/src/vaadin-grid.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ export type GridDefaultItem = any;
* `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
* `header` | Grid header toolbar container for custom content above the grid table
* `footer` | Grid footer toolbar container for custom content below the grid table
*
* The following state attributes are available for styling:
*
Expand Down
13 changes: 13 additions & 0 deletions packages/grid/src/vaadin-grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ import { GridMixin } from './vaadin-grid-mixin.js';
* @fires {CustomEvent} size-changed - Fired when the `size` property changes.
* @fires {CustomEvent} item-toggle - Fired when the user selects or deselects an item through the selection column.
*
* @slot header - Slot for custom content to be placed above the grid table, typically used for toolbars and controls
* @slot footer - Slot for custom content to be placed below the grid table, typically used for status information
* @slot empty-state - Slot for content to be displayed when there are no body rows to show
* @slot tooltip - Slot for tooltip overlay
*
* @customElement
* @extends HTMLElement
* @mixes GridMixin
Expand All @@ -276,6 +281,10 @@ class Grid extends GridMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjecti
/** @protected */
render() {
return html`
<div part="header" id="gridHeader">
<slot name="header"></slot>
</div>

<div
id="scroller"
?safari="${this._safari}"
Expand Down Expand Up @@ -307,6 +316,10 @@ class Grid extends GridMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjecti
<div part="reorder-ghost"></div>
</div>

<div part="footer" id="gridFooter">
<slot name="footer"></slot>
</div>

<slot name="tooltip"></slot>

<div id="focusexit" tabindex="0"></div>
Expand Down
Loading
Loading