Skip to content

Commit 6c65a89

Browse files
Artur-claude
andcommitted
feat: add header and footer slots to vaadin-grid
Add customizable header and footer slots to the grid component that appear above and below the grid table. These slots use flexbox layout by default, allowing multiple elements to be arranged without extra wrapper divs. Key features: - Header slot above the grid table for toolbars and controls - Footer slot below the grid table for status information - Both slots use display:flex with align-items:center by default - Automatic hiding of empty slots - Full integration with existing grid functionality Resolves #986 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 587c730 commit 6c65a89

File tree

5 files changed

+454
-13
lines changed

5 files changed

+454
-13
lines changed

dev/grid.html

Lines changed: 179 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,191 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Grid</title>
88
<script type="module" src="./common.js"></script>
9+
<style>
10+
body {
11+
font-family: var(--lumo-font-family);
12+
padding: 20px;
13+
}
14+
15+
h2 {
16+
margin-top: 40px;
17+
margin-bottom: 20px;
18+
color: var(--lumo-header-text-color);
19+
}
20+
21+
vaadin-grid {
22+
margin-bottom: 40px;
23+
height: 400px;
24+
}
25+
26+
/* Header and footer slot styles */
27+
[part="header"] {
28+
background: var(--lumo-contrast-5pct);
29+
border-bottom: 1px solid var(--lumo-contrast-10pct);
30+
padding: var(--lumo-space-s) var(--lumo-space-m);
31+
gap: var(--lumo-space-m);
32+
}
33+
34+
[part="footer"] {
35+
background: var(--lumo-contrast-5pct);
36+
border-top: 1px solid var(--lumo-contrast-10pct);
37+
padding: var(--lumo-space-xs) var(--lumo-space-m);
38+
font-size: var(--lumo-font-size-s);
39+
color: var(--lumo-secondary-text-color);
40+
gap: var(--lumo-space-s);
41+
}
42+
43+
.flex-grow {
44+
flex: 1;
45+
}
46+
</style>
947
</head>
1048
<body>
49+
<h1>Grid Examples</h1>
1150
<script type="module">
1251
import '@vaadin/grid/all-imports';
1352
import '@vaadin/tooltip';
53+
import '@vaadin/button';
54+
import '@vaadin/text-field';
55+
import '@vaadin/icon';
56+
import '@vaadin/icons';
57+
</script>
58+
59+
<h2>Grid with Header and Footer Slots (Flex Layout)</h2>
60+
<vaadin-grid id="header-footer-grid">
61+
<!-- Multiple elements directly in header slot using flex layout -->
62+
<vaadin-icon slot="header" icon="vaadin:grid-h" style="color: var(--lumo-primary-color);"></vaadin-icon>
63+
<strong slot="header">User Management</strong>
64+
<span slot="header" class="flex-grow"></span>
65+
<vaadin-text-field slot="header" placeholder="Search users..." clear-button-visible style="width: 250px;">
66+
<vaadin-icon slot="prefix" icon="vaadin:search"></vaadin-icon>
67+
</vaadin-text-field>
68+
<vaadin-button slot="header" theme="primary">
69+
<vaadin-icon icon="vaadin:plus" slot="prefix"></vaadin-icon>
70+
Add User
71+
</vaadin-button>
72+
73+
<vaadin-grid-selection-column></vaadin-grid-selection-column>
74+
<vaadin-grid-column path="name" header="Name"></vaadin-grid-column>
75+
<vaadin-grid-column path="email" header="Email"></vaadin-grid-column>
76+
<vaadin-grid-column path="role" header="Role"></vaadin-grid-column>
77+
<vaadin-grid-column path="status" header="Status"></vaadin-grid-column>
78+
79+
<!-- Multiple elements in footer -->
80+
<span slot="footer">Total: <strong id="user-count">0</strong> users</span>
81+
<span slot="footer" class="flex-grow"></span>
82+
<span slot="footer">Selected: <strong id="selected-count">0</strong></span>
83+
<span slot="footer" style="margin-left: var(--lumo-space-xl);">Last updated: <span id="update-time">Never</span></span>
84+
</vaadin-grid>
85+
86+
<h2>Grid with Toolbar Actions</h2>
87+
<vaadin-grid id="toolbar-grid">
88+
<!-- Header with title and actions -->
89+
<h3 slot="header" style="margin: 0;">Products</h3>
90+
<div slot="header" style="margin-left: auto; display: flex; gap: var(--lumo-space-s);">
91+
<vaadin-button theme="tertiary">
92+
<vaadin-icon icon="vaadin:download" slot="prefix"></vaadin-icon>
93+
Export
94+
</vaadin-button>
95+
<vaadin-button theme="tertiary">
96+
<vaadin-icon icon="vaadin:upload" slot="prefix"></vaadin-icon>
97+
Import
98+
</vaadin-button>
99+
<vaadin-button theme="error tertiary">
100+
<vaadin-icon icon="vaadin:trash" slot="prefix"></vaadin-icon>
101+
Delete
102+
</vaadin-button>
103+
</div>
104+
105+
<vaadin-grid-selection-column></vaadin-grid-selection-column>
106+
<vaadin-grid-filter-column path="product" header="Product"></vaadin-grid-filter-column>
107+
<vaadin-grid-filter-column path="category" header="Category"></vaadin-grid-filter-column>
108+
<vaadin-grid-column path="price" header="Price"></vaadin-grid-column>
109+
<vaadin-grid-column path="stock" header="Stock"></vaadin-grid-column>
110+
111+
<!-- Footer with status indicator -->
112+
<span slot="footer" style="display: flex; align-items: center; gap: var(--lumo-space-xs);">
113+
<span style="width: 8px; height: 8px; background: var(--lumo-success-color); border-radius: 50%;"></span>
114+
All systems operational
115+
</span>
116+
<span slot="footer" style="margin-left: auto;">
117+
Showing <strong id="product-count">0</strong> of <strong id="product-total">0</strong> products
118+
</span>
119+
</vaadin-grid>
120+
121+
<h2>Original Tree Grid Example</h2>
122+
<vaadin-grid id="tree-grid" item-id-path="name">
123+
<vaadin-grid-selection-column auto-select frozen drag-select></vaadin-grid-selection-column>
124+
<vaadin-grid-tree-column frozen path="name" width="200px" flex-shrink="0"></vaadin-grid-tree-column>
125+
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
126+
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
127+
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
14128

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

17-
grid.dataProvider = ({ parentItem, page, pageSize }, cb) => {
132+
<script type="module">
133+
// Sample data for header-footer grid
134+
const users = [
135+
{ name: 'John Smith', email: '[email protected]', role: 'Administrator', status: 'Active' },
136+
{ name: 'Jane Doe', email: '[email protected]', role: 'Editor', status: 'Active' },
137+
{ name: 'Bob Johnson', email: '[email protected]', role: 'Viewer', status: 'Inactive' },
138+
{ name: 'Alice Williams', email: '[email protected]', role: 'Editor', status: 'Active' },
139+
{ name: 'Charlie Brown', email: '[email protected]', role: 'Viewer', status: 'Active' },
140+
{ name: 'Diana Prince', email: '[email protected]', role: 'Administrator', status: 'Active' },
141+
{ name: 'Edward Norton', email: '[email protected]', role: 'Editor', status: 'Active' },
142+
{ name: 'Fiona Green', email: '[email protected]', role: 'Viewer', status: 'Inactive' },
143+
];
144+
145+
const headerFooterGrid = document.querySelector('#header-footer-grid');
146+
headerFooterGrid.items = users;
147+
document.querySelector('#user-count').textContent = users.length;
148+
document.querySelector('#update-time').textContent = new Date().toLocaleTimeString();
149+
150+
// Update selected count
151+
headerFooterGrid.addEventListener('selected-items-changed', (e) => {
152+
document.querySelector('#selected-count').textContent = e.detail.value.length;
153+
});
154+
155+
// Handle search
156+
const searchField = headerFooterGrid.querySelector('vaadin-text-field[slot="header"]');
157+
searchField.addEventListener('value-changed', (e) => {
158+
const searchTerm = e.detail.value.toLowerCase();
159+
if (searchTerm) {
160+
headerFooterGrid.items = users.filter(
161+
(user) =>
162+
user.name.toLowerCase().includes(searchTerm) ||
163+
user.email.toLowerCase().includes(searchTerm) ||
164+
user.role.toLowerCase().includes(searchTerm),
165+
);
166+
} else {
167+
headerFooterGrid.items = users;
168+
}
169+
document.querySelector('#user-count').textContent = headerFooterGrid.items.length;
170+
});
171+
172+
// Sample data for toolbar grid
173+
const products = [
174+
{ product: 'Laptop Pro', category: 'Electronics', price: '$1,299', stock: 45 },
175+
{ product: 'Wireless Mouse', category: 'Accessories', price: '$29', stock: 120 },
176+
{ product: 'USB-C Cable', category: 'Accessories', price: '$19', stock: 200 },
177+
{ product: 'Monitor 27"', category: 'Electronics', price: '$399', stock: 30 },
178+
{ product: 'Keyboard Mechanical', category: 'Accessories', price: '$79', stock: 85 },
179+
{ product: 'Webcam HD', category: 'Electronics', price: '$99', stock: 60 },
180+
{ product: 'Desk Lamp LED', category: 'Office', price: '$49', stock: 95 },
181+
{ product: 'Office Chair', category: 'Furniture', price: '$299', stock: 25 },
182+
{ product: 'Standing Desk', category: 'Furniture', price: '$599', stock: 15 },
183+
{ product: 'Headphones', category: 'Electronics', price: '$149', stock: 70 },
184+
];
185+
186+
const toolbarGrid = document.querySelector('#toolbar-grid');
187+
toolbarGrid.items = products;
188+
document.querySelector('#product-count').textContent = products.length;
189+
document.querySelector('#product-total').textContent = products.length;
190+
191+
// Original tree grid setup
192+
const treeGrid = document.querySelector('#tree-grid');
193+
treeGrid.dataProvider = ({ parentItem, page, pageSize }, cb) => {
18194
// Let's have 100 root-level items and 5 items on every child level
19195
const levelSize = parentItem ? 5 : 100;
20196

@@ -30,20 +206,10 @@
30206
cb(pageItems, levelSize);
31207
};
32208

33-
const tooltip = document.querySelector('[slot="tooltip"]');
209+
const tooltip = treeGrid.querySelector('[slot="tooltip"]');
34210
tooltip.generator = ({ column, item }) => {
35211
return column && column.path && item ? `Tooltip ${column.path} ${item.name}` : '';
36212
};
37213
</script>
38-
39-
<vaadin-grid item-id-path="name">
40-
<vaadin-grid-selection-column auto-select frozen drag-select></vaadin-grid-selection-column>
41-
<vaadin-grid-tree-column frozen path="name" width="200px" flex-shrink="0"></vaadin-grid-tree-column>
42-
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
43-
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
44-
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
45-
46-
<vaadin-tooltip slot="tooltip" hover-delay="500" hide-delay="500"></vaadin-tooltip>
47-
</vaadin-grid>
48214
</body>
49215
</html>

packages/grid/src/styles/vaadin-grid-base-styles.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,28 @@ export const gridStyles = css`
6464
border-radius: calc(var(--_border-radius) - var(--_border-width));
6565
position: relative;
6666
display: flex;
67+
flex-direction: column;
6768
width: 100%;
6869
min-width: 0;
6970
min-height: 0;
7071
align-self: stretch;
7172
overflow: hidden;
7273
}
7374
75+
[part='header'],
76+
[part='footer'] {
77+
display: flex;
78+
align-items: center;
79+
width: 100%;
80+
flex-shrink: 0;
81+
box-sizing: border-box;
82+
}
83+
84+
:host([header-hidden]) [part='header'],
85+
:host([footer-hidden]) [part='footer'] {
86+
display: none;
87+
}
88+
7489
#items {
7590
flex-grow: 1;
7691
flex-shrink: 0;
@@ -85,6 +100,8 @@ export const gridStyles = css`
85100
display: flex;
86101
flex-direction: column;
87102
width: 100%;
103+
flex: 1 1 auto;
104+
min-height: 0;
88105
overflow: auto;
89106
position: relative;
90107
border-radius: inherit;

packages/grid/src/vaadin-grid.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ export type GridDefaultItem = any;
210210
* `resize-handle` | Handle for resizing the columns
211211
* `empty-state` | The container for the content to be displayed when there are no body rows to show
212212
* `reorder-ghost` | Ghost element of the header cell being dragged
213+
* `header` | Grid header toolbar container for custom content above the grid table
214+
* `footer` | Grid footer toolbar container for custom content below the grid table
213215
*
214216
* The following state attributes are available for styling:
215217
*

packages/grid/src/vaadin-grid.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,11 @@ import { GridMixin } from './vaadin-grid-mixin.js';
258258
* @fires {CustomEvent} selected-items-changed - Fired when the `selectedItems` property changes.
259259
* @fires {CustomEvent} size-changed - Fired when the `size` property changes.
260260
*
261+
* @slot header - Slot for custom content to be placed above the grid table, typically used for toolbars and controls
262+
* @slot footer - Slot for custom content to be placed below the grid table, typically used for status information
263+
* @slot empty-state - Slot for content to be displayed when there are no body rows to show
264+
* @slot tooltip - Slot for tooltip overlay
265+
*
261266
* @customElement
262267
* @extends HTMLElement
263268
* @mixes GridMixin
@@ -272,6 +277,32 @@ class Grid extends GridMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjecti
272277
return gridStyles;
273278
}
274279

280+
/** @protected */
281+
firstUpdated() {
282+
super.firstUpdated();
283+
284+
// Handle header slot visibility
285+
const headerSlot = this.shadowRoot.querySelector('slot[name="header"]');
286+
const footerSlot = this.shadowRoot.querySelector('slot[name="footer"]');
287+
288+
const updateSlotVisibility = () => {
289+
// Check header slot
290+
const hasHeaderContent = headerSlot.assignedNodes().length > 0;
291+
this.toggleAttribute('header-hidden', !hasHeaderContent);
292+
293+
// Check footer slot
294+
const hasFooterContent = footerSlot.assignedNodes().length > 0;
295+
this.toggleAttribute('footer-hidden', !hasFooterContent);
296+
};
297+
298+
// Initial check
299+
updateSlotVisibility();
300+
301+
// Listen for slot changes
302+
headerSlot.addEventListener('slotchange', updateSlotVisibility);
303+
footerSlot.addEventListener('slotchange', updateSlotVisibility);
304+
}
305+
275306
/** @protected */
276307
render() {
277308
return html`
@@ -283,6 +314,10 @@ class Grid extends GridMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjecti
283314
?column-reordering-allowed="${this.columnReorderingAllowed}"
284315
?empty-state="${this.__emptyState}"
285316
>
317+
<div part="header" id="gridHeader">
318+
<slot name="header"></slot>
319+
</div>
320+
286321
<table
287322
id="table"
288323
role="treegrid"
@@ -303,6 +338,10 @@ class Grid extends GridMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjecti
303338
<tfoot id="footer" role="rowgroup"></tfoot>
304339
</table>
305340
341+
<div part="footer" id="gridFooter">
342+
<slot name="footer"></slot>
343+
</div>
344+
306345
<div part="reorder-ghost"></div>
307346
</div>
308347

0 commit comments

Comments
 (0)