@dile/ui 3.10.0 → 3.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/card/src/DileCard.js +1 -1
- package/components/data-grid/__screenshots__/data-grid.spec.js/dile-data-grid-dispatches-item-checkbox-changed-and-dile-data-grid-item-selected-when-a-checkbox-is-toggled-1.png +0 -0
- package/components/data-grid/data-grid.js +5 -0
- package/components/data-grid/data-grid.spec.js +421 -0
- package/components/data-grid/index.js +1 -0
- package/components/data-grid/src/DileDataGrid.js +786 -0
- package/mixins/close-on-esc-pressed/src/DileCloseOnEscPressed.js +1 -1
- package/package.json +2 -2
|
@@ -47,7 +47,7 @@ export class DileCard extends LitElement {
|
|
|
47
47
|
margin-bottom: var(--dile-card-title-margin-bottom, 0);
|
|
48
48
|
}
|
|
49
49
|
.card-title-with-icon {
|
|
50
|
-
padding-top: var(--dile-card-title-padding-top-with-icon, 0.
|
|
50
|
+
padding-top: var(--dile-card-title-padding-top-with-icon, 0.75rem);
|
|
51
51
|
}
|
|
52
52
|
main {
|
|
53
53
|
padding-top: var(--dile-card-padding-y, 1rem);
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
import './data-grid.js';
|
|
4
|
+
|
|
5
|
+
describe('dile-data-grid', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
document.body.innerHTML = '';
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
async function createDataGrid(props = {}) {
|
|
11
|
+
const el = document.createElement('dile-data-grid');
|
|
12
|
+
Object.assign(el, props);
|
|
13
|
+
document.body.appendChild(el);
|
|
14
|
+
await el.updateComplete;
|
|
15
|
+
return el;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sampleColumns = [
|
|
19
|
+
{ field: 'id', header: 'ID', sortable: true, width: '80px', align: 'center' },
|
|
20
|
+
{ field: 'name', header: 'Name', sortable: true },
|
|
21
|
+
{ field: 'age', header: 'Age', sortable: true, align: 'right' },
|
|
22
|
+
{
|
|
23
|
+
header: 'Custom',
|
|
24
|
+
render: (row) => html`<span class="custom-badge">${row.name} (${row.age})</span>`,
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const sampleItems = [
|
|
29
|
+
{ id: 1, name: 'Carlos', age: 30 },
|
|
30
|
+
{ id: 2, name: 'Ana', age: 25 },
|
|
31
|
+
{ id: 3, name: 'Beatriz', age: 35 },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
it('renders default empty message when items is empty', async () => {
|
|
35
|
+
const el = await createDataGrid({ columns: sampleColumns, items: [] });
|
|
36
|
+
const emptyCell = el.shadowRoot.querySelector('.empty-cell');
|
|
37
|
+
expect(emptyCell).toBeTruthy();
|
|
38
|
+
expect(emptyCell.textContent.trim()).toBe('No data available');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders customized empty message', async () => {
|
|
42
|
+
const el = await createDataGrid({
|
|
43
|
+
columns: sampleColumns,
|
|
44
|
+
items: [],
|
|
45
|
+
emptyMessage: 'Nothing found',
|
|
46
|
+
});
|
|
47
|
+
const emptyCell = el.shadowRoot.querySelector('.empty-cell');
|
|
48
|
+
expect(emptyCell).toBeTruthy();
|
|
49
|
+
expect(emptyCell.textContent.trim()).toBe('Nothing found');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders column headers and data rows correctly', async () => {
|
|
53
|
+
const el = await createDataGrid({
|
|
54
|
+
columns: sampleColumns,
|
|
55
|
+
items: sampleItems,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const headers = el.shadowRoot.querySelectorAll('th');
|
|
59
|
+
expect(headers.length).toBe(4);
|
|
60
|
+
expect(headers[0].textContent).toContain('ID');
|
|
61
|
+
expect(headers[1].textContent).toContain('Name');
|
|
62
|
+
expect(headers[2].textContent).toContain('Age');
|
|
63
|
+
expect(headers[3].textContent).toContain('Custom');
|
|
64
|
+
|
|
65
|
+
const rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
66
|
+
expect(rows.length).toBe(3);
|
|
67
|
+
|
|
68
|
+
const firstRowCells = rows[0].querySelectorAll('td');
|
|
69
|
+
expect(firstRowCells[0].textContent.trim()).toBe('1');
|
|
70
|
+
expect(firstRowCells[1].textContent.trim()).toBe('Carlos');
|
|
71
|
+
expect(firstRowCells[2].textContent.trim()).toBe('30');
|
|
72
|
+
expect(firstRowCells[3].querySelector('.custom-badge')).toBeTruthy();
|
|
73
|
+
expect(firstRowCells[3].textContent.trim()).toBe('Carlos (30)');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('sorts strings ascending and descending on client sortMode', async () => {
|
|
77
|
+
const el = await createDataGrid({
|
|
78
|
+
columns: sampleColumns,
|
|
79
|
+
items: sampleItems,
|
|
80
|
+
sortMode: 'client',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const nameHeader = el.shadowRoot.querySelectorAll('th')[1];
|
|
84
|
+
|
|
85
|
+
// Click 1: sort asc by name
|
|
86
|
+
nameHeader.click();
|
|
87
|
+
await el.updateComplete;
|
|
88
|
+
|
|
89
|
+
let rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
90
|
+
expect(rows[0].querySelectorAll('td')[1].textContent.trim()).toBe('Ana');
|
|
91
|
+
expect(rows[1].querySelectorAll('td')[1].textContent.trim()).toBe('Beatriz');
|
|
92
|
+
expect(rows[2].querySelectorAll('td')[1].textContent.trim()).toBe('Carlos');
|
|
93
|
+
|
|
94
|
+
// Click 2: sort desc by name
|
|
95
|
+
nameHeader.click();
|
|
96
|
+
await el.updateComplete;
|
|
97
|
+
|
|
98
|
+
rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
99
|
+
expect(rows[0].querySelectorAll('td')[1].textContent.trim()).toBe('Carlos');
|
|
100
|
+
expect(rows[1].querySelectorAll('td')[1].textContent.trim()).toBe('Beatriz');
|
|
101
|
+
expect(rows[2].querySelectorAll('td')[1].textContent.trim()).toBe('Ana');
|
|
102
|
+
|
|
103
|
+
// Click 3: reset sort
|
|
104
|
+
nameHeader.click();
|
|
105
|
+
await el.updateComplete;
|
|
106
|
+
|
|
107
|
+
rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
108
|
+
expect(rows[0].querySelectorAll('td')[1].textContent.trim()).toBe('Carlos');
|
|
109
|
+
expect(rows[1].querySelectorAll('td')[1].textContent.trim()).toBe('Ana');
|
|
110
|
+
expect(rows[2].querySelectorAll('td')[1].textContent.trim()).toBe('Beatriz');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('sorts numbers properly in client mode', async () => {
|
|
114
|
+
const el = await createDataGrid({
|
|
115
|
+
columns: sampleColumns,
|
|
116
|
+
items: sampleItems,
|
|
117
|
+
sortMode: 'client',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const ageHeader = el.shadowRoot.querySelectorAll('th')[2];
|
|
121
|
+
|
|
122
|
+
// Click: sort asc by age
|
|
123
|
+
ageHeader.click();
|
|
124
|
+
await el.updateComplete;
|
|
125
|
+
|
|
126
|
+
let rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
127
|
+
expect(rows[0].querySelectorAll('td')[2].textContent.trim()).toBe('25');
|
|
128
|
+
expect(rows[1].querySelectorAll('td')[2].textContent.trim()).toBe('30');
|
|
129
|
+
expect(rows[2].querySelectorAll('td')[2].textContent.trim()).toBe('35');
|
|
130
|
+
|
|
131
|
+
// Click: sort desc by age
|
|
132
|
+
ageHeader.click();
|
|
133
|
+
await el.updateComplete;
|
|
134
|
+
|
|
135
|
+
rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
136
|
+
expect(rows[0].querySelectorAll('td')[2].textContent.trim()).toBe('35');
|
|
137
|
+
expect(rows[1].querySelectorAll('td')[2].textContent.trim()).toBe('30');
|
|
138
|
+
expect(rows[2].querySelectorAll('td')[2].textContent.trim()).toBe('25');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('supports custom column comparators', async () => {
|
|
142
|
+
const customColumns = [
|
|
143
|
+
{
|
|
144
|
+
field: 'date',
|
|
145
|
+
header: 'Date',
|
|
146
|
+
sortable: true,
|
|
147
|
+
comparator: (valA, valB) => new Date(valA).getTime() - new Date(valB).getTime(),
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
const items = [
|
|
151
|
+
{ date: '2026-05-10' },
|
|
152
|
+
{ date: '2025-01-01' },
|
|
153
|
+
{ date: '2026-01-15' },
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const el = await createDataGrid({
|
|
157
|
+
columns: customColumns,
|
|
158
|
+
items,
|
|
159
|
+
sortMode: 'client',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const dateHeader = el.shadowRoot.querySelector('th');
|
|
163
|
+
dateHeader.click();
|
|
164
|
+
await el.updateComplete;
|
|
165
|
+
|
|
166
|
+
let rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
167
|
+
expect(rows[0].querySelector('td').textContent.trim()).toBe('2025-01-01');
|
|
168
|
+
expect(rows[1].querySelector('td').textContent.trim()).toBe('2026-01-15');
|
|
169
|
+
expect(rows[2].querySelector('td').textContent.trim()).toBe('2026-05-10');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('dispatches dile-data-grid-sort event on header click', async () => {
|
|
173
|
+
const el = await createDataGrid({
|
|
174
|
+
columns: sampleColumns,
|
|
175
|
+
items: sampleItems,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
let sortEventDetail = null;
|
|
179
|
+
el.addEventListener('dile-data-grid-sort', (e) => {
|
|
180
|
+
sortEventDetail = e.detail;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const nameHeader = el.shadowRoot.querySelectorAll('th')[1];
|
|
184
|
+
nameHeader.click();
|
|
185
|
+
|
|
186
|
+
expect(sortEventDetail).toBeTruthy();
|
|
187
|
+
expect(sortEventDetail.field).toBe('name');
|
|
188
|
+
expect(sortEventDetail.direction).toBe('asc');
|
|
189
|
+
expect(sortEventDetail.column.field).toBe('name');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('dispatches dile-data-grid-row-click event on row click', async () => {
|
|
193
|
+
const el = await createDataGrid({
|
|
194
|
+
columns: sampleColumns,
|
|
195
|
+
items: sampleItems,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
let rowClickDetail = null;
|
|
199
|
+
el.addEventListener('dile-data-grid-row-click', (e) => {
|
|
200
|
+
rowClickDetail = e.detail;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const secondRow = el.shadowRoot.querySelectorAll('tbody tr')[1];
|
|
204
|
+
secondRow.click();
|
|
205
|
+
|
|
206
|
+
expect(rowClickDetail).toBeTruthy();
|
|
207
|
+
expect(rowClickDetail.row.id).toBe(2);
|
|
208
|
+
expect(rowClickDetail.row.name).toBe('Ana');
|
|
209
|
+
expect(rowClickDetail.index).toBe(1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('does not re-order displayed items internally when sortMode is external', async () => {
|
|
213
|
+
const el = await createDataGrid({
|
|
214
|
+
columns: sampleColumns,
|
|
215
|
+
items: sampleItems,
|
|
216
|
+
sortMode: 'external',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const nameHeader = el.shadowRoot.querySelectorAll('th')[1];
|
|
220
|
+
nameHeader.click();
|
|
221
|
+
await el.updateComplete;
|
|
222
|
+
|
|
223
|
+
// Items order should remain unchanged
|
|
224
|
+
const rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
225
|
+
expect(rows[0].querySelectorAll('td')[1].textContent.trim()).toBe('Carlos');
|
|
226
|
+
expect(rows[1].querySelectorAll('td')[1].textContent.trim()).toBe('Ana');
|
|
227
|
+
expect(rows[2].querySelectorAll('td')[1].textContent.trim()).toBe('Beatriz');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('sets data-label attribute for responsive card mode', async () => {
|
|
231
|
+
const el = await createDataGrid({
|
|
232
|
+
columns: [
|
|
233
|
+
{ field: 'name', header: 'Full Name' },
|
|
234
|
+
{ field: 'role', header: 'Role', hideCardLabel: true },
|
|
235
|
+
{ field: 'notes', header: 'Notes', hideOnCard: true },
|
|
236
|
+
],
|
|
237
|
+
items: [{ name: 'Elena', role: 'Dev', notes: 'Secret' }],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const cells = el.shadowRoot.querySelectorAll('tbody tr td');
|
|
241
|
+
expect(cells[0].getAttribute('data-label')).toBe('Full Name');
|
|
242
|
+
expect(cells[1].getAttribute('data-label')).toBe('');
|
|
243
|
+
expect(cells[1].classList.contains('no-card-label')).toBe(true);
|
|
244
|
+
expect(cells[2].classList.contains('hide-on-card')).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('reflects responsiveMode attribute', async () => {
|
|
248
|
+
const el = await createDataGrid({
|
|
249
|
+
columns: sampleColumns,
|
|
250
|
+
items: sampleItems,
|
|
251
|
+
responsiveMode: 'cards',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(el.getAttribute('responsive-mode')).toBe('cards');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('applies sticky-left class when stickyFirstColumn is true or column.sticky is set', async () => {
|
|
258
|
+
const el = await createDataGrid({
|
|
259
|
+
columns: [
|
|
260
|
+
{ field: 'name', header: 'Name', sticky: true },
|
|
261
|
+
{ field: 'city', header: 'City' },
|
|
262
|
+
{ field: 'action', header: 'Action', sticky: 'right' },
|
|
263
|
+
],
|
|
264
|
+
items: [{ name: 'Carlos', city: 'Madrid', action: 'Edit' }],
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const ths = el.shadowRoot.querySelectorAll('th');
|
|
268
|
+
const tds = el.shadowRoot.querySelectorAll('tbody tr td');
|
|
269
|
+
|
|
270
|
+
expect(ths[0].classList.contains('sticky-left')).toBe(true);
|
|
271
|
+
expect(tds[0].classList.contains('sticky-left')).toBe(true);
|
|
272
|
+
|
|
273
|
+
expect(ths[1].classList.contains('sticky-left')).toBe(false);
|
|
274
|
+
expect(tds[1].classList.contains('sticky-left')).toBe(false);
|
|
275
|
+
|
|
276
|
+
expect(ths[2].classList.contains('sticky-right')).toBe(true);
|
|
277
|
+
expect(tds[2].classList.contains('sticky-right')).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('applies sticky-left to the first column when stickyFirstColumn property is set without selection', async () => {
|
|
281
|
+
const el = await createDataGrid({
|
|
282
|
+
columns: sampleColumns,
|
|
283
|
+
items: sampleItems,
|
|
284
|
+
stickyFirstColumn: true,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const ths = el.shadowRoot.querySelectorAll('th');
|
|
288
|
+
const tds = el.shadowRoot.querySelectorAll('tbody tr td');
|
|
289
|
+
|
|
290
|
+
expect(ths[0].classList.contains('sticky-left')).toBe(true);
|
|
291
|
+
expect(tds[0].classList.contains('sticky-left')).toBe(true);
|
|
292
|
+
expect(ths[1].classList.contains('sticky-left')).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('handles sticky columns properly when selectable is true and stickyFirstColumn is true', async () => {
|
|
296
|
+
const el = await createDataGrid({
|
|
297
|
+
columns: sampleColumns,
|
|
298
|
+
items: sampleItems,
|
|
299
|
+
selectable: true,
|
|
300
|
+
stickyFirstColumn: true,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const ths = el.shadowRoot.querySelectorAll('th');
|
|
304
|
+
const tds = el.shadowRoot.querySelectorAll('tbody tr td');
|
|
305
|
+
|
|
306
|
+
// th[0] is selection cell -> sticky-left
|
|
307
|
+
expect(ths[0].classList.contains('selection-cell')).toBe(true);
|
|
308
|
+
expect(ths[0].classList.contains('sticky-left')).toBe(true);
|
|
309
|
+
expect(tds[0].classList.contains('selection-cell')).toBe(true);
|
|
310
|
+
expect(tds[0].classList.contains('sticky-left')).toBe(true);
|
|
311
|
+
|
|
312
|
+
// th[1] is first data column -> sticky-left-after-selection
|
|
313
|
+
expect(ths[1].classList.contains('sticky-left-after-selection')).toBe(true);
|
|
314
|
+
expect(tds[1].classList.contains('sticky-left-after-selection')).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('renders selection column when selectable is true and checks items matching selectedIds', async () => {
|
|
318
|
+
const el = await createDataGrid({
|
|
319
|
+
columns: sampleColumns,
|
|
320
|
+
items: sampleItems,
|
|
321
|
+
selectable: true,
|
|
322
|
+
selectedIds: [2],
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const ths = el.shadowRoot.querySelectorAll('th');
|
|
326
|
+
expect(ths.length).toBe(5);
|
|
327
|
+
expect(ths[0].classList.contains('selection-cell')).toBe(true);
|
|
328
|
+
|
|
329
|
+
const rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
330
|
+
expect(rows.length).toBe(3);
|
|
331
|
+
|
|
332
|
+
const checkboxes = el.shadowRoot.querySelectorAll('dile-checkbox');
|
|
333
|
+
expect(checkboxes.length).toBe(3);
|
|
334
|
+
expect(checkboxes[0].checked).toBe(false);
|
|
335
|
+
expect(checkboxes[1].checked).toBe(true);
|
|
336
|
+
expect(checkboxes[2].checked).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('dispatches item-checkbox-changed and dile-data-grid-item-selected when a checkbox is toggled', async () => {
|
|
340
|
+
const el = await createDataGrid({
|
|
341
|
+
columns: sampleColumns,
|
|
342
|
+
items: sampleItems,
|
|
343
|
+
selectable: true,
|
|
344
|
+
selectedIds: [],
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
let itemCheckboxDetail = null;
|
|
348
|
+
let gridItemSelectedDetail = null;
|
|
349
|
+
|
|
350
|
+
el.addEventListener('item-checkbox-changed', (e) => {
|
|
351
|
+
itemCheckboxDetail = e.detail;
|
|
352
|
+
});
|
|
353
|
+
el.addEventListener('dile-data-grid-item-selected', (e) => {
|
|
354
|
+
gridItemSelectedDetail = e.detail;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const firstCheckbox = el.shadowRoot.querySelectorAll('dile-checkbox')[0];
|
|
358
|
+
await firstCheckbox.updateComplete;
|
|
359
|
+
const clickTarget = firstCheckbox.shadowRoot?.querySelector('div') || firstCheckbox;
|
|
360
|
+
clickTarget.click();
|
|
361
|
+
await el.updateComplete;
|
|
362
|
+
|
|
363
|
+
expect(itemCheckboxDetail).toBeTruthy();
|
|
364
|
+
expect(itemCheckboxDetail.checked).toBe(true);
|
|
365
|
+
expect(itemCheckboxDetail.itemId).toBe(1);
|
|
366
|
+
expect(itemCheckboxDetail.index).toBe(0);
|
|
367
|
+
|
|
368
|
+
expect(gridItemSelectedDetail).toBeTruthy();
|
|
369
|
+
expect(gridItemSelectedDetail.checked).toBe(true);
|
|
370
|
+
expect(gridItemSelectedDetail.itemId).toBe(1);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('applies rowClass custom class to table rows', async () => {
|
|
374
|
+
const el = await createDataGrid({
|
|
375
|
+
columns: sampleColumns,
|
|
376
|
+
items: [
|
|
377
|
+
{ id: 1, name: 'Carlos', deleted_at: null },
|
|
378
|
+
{ id: 2, name: 'Ana', deleted_at: '2026-01-01' },
|
|
379
|
+
],
|
|
380
|
+
rowClass: (row) => (row.deleted_at ? 'is-deleted' : ''),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
384
|
+
expect(rows[0].classList.contains('is-deleted')).toBe(false);
|
|
385
|
+
expect(rows[1].classList.contains('is-deleted')).toBe(true);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('supports custom computeRowId function', async () => {
|
|
389
|
+
const customItems = [
|
|
390
|
+
{ uuid: 'abc-1', name: 'Carlos' },
|
|
391
|
+
{ uuid: 'xyz-2', name: 'Ana' },
|
|
392
|
+
];
|
|
393
|
+
const el = await createDataGrid({
|
|
394
|
+
columns: [{ field: 'name', header: 'Name' }],
|
|
395
|
+
items: customItems,
|
|
396
|
+
selectable: true,
|
|
397
|
+
computeRowId: (row) => row.uuid,
|
|
398
|
+
selectedIds: ['xyz-2'],
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const checkboxes = el.shadowRoot.querySelectorAll('dile-checkbox');
|
|
402
|
+
expect(checkboxes[0].checked).toBe(false);
|
|
403
|
+
expect(checkboxes[1].checked).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('reads sortMode and emptyMessage from their kebab-case HTML attributes', async () => {
|
|
407
|
+
document.body.innerHTML = `
|
|
408
|
+
<dile-data-grid sort-mode="external" empty-message="Nothing here"></dile-data-grid>
|
|
409
|
+
`;
|
|
410
|
+
const el = document.body.querySelector('dile-data-grid');
|
|
411
|
+
await el.updateComplete;
|
|
412
|
+
|
|
413
|
+
expect(el.sortMode).toBe('external');
|
|
414
|
+
expect(el.emptyMessage).toBe('Nothing here');
|
|
415
|
+
|
|
416
|
+
el.items = [];
|
|
417
|
+
await el.updateComplete;
|
|
418
|
+
const emptyCell = el.shadowRoot.querySelector('.empty-cell');
|
|
419
|
+
expect(emptyCell.textContent.trim()).toBe('Nothing here');
|
|
420
|
+
});
|
|
421
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DileDataGrid } from './src/DileDataGrid.js';
|
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import { html, css, LitElement } from 'lit';
|
|
2
|
+
import { arrowDropUpIcon, arrowDropDownIcon } from '@dile/icons';
|
|
3
|
+
import '../../checkbox/checkbox.js';
|
|
4
|
+
|
|
5
|
+
export class DileDataGrid extends LitElement {
|
|
6
|
+
static get properties() {
|
|
7
|
+
return {
|
|
8
|
+
/** Array of row objects to display in the grid */
|
|
9
|
+
items: { type: Array },
|
|
10
|
+
|
|
11
|
+
/** Column configurations: { field, header, sortable, width, align, render, comparator, hideOnCard, hideCardLabel, sticky } */
|
|
12
|
+
columns: { type: Array },
|
|
13
|
+
|
|
14
|
+
/** Currently sorted column field */
|
|
15
|
+
sortField: { type: String },
|
|
16
|
+
|
|
17
|
+
/** Sort direction: 'asc', 'desc', or '' */
|
|
18
|
+
sortDirection: { type: String },
|
|
19
|
+
|
|
20
|
+
/** Mode of sorting: 'client' (sorts data internally) or 'external' (only emits event) */
|
|
21
|
+
sortMode: { type: String, attribute: 'sort-mode' },
|
|
22
|
+
|
|
23
|
+
/** Responsive mode: 'auto' (container query card switch), 'cards' (always card), or 'scroll' (table scroll) */
|
|
24
|
+
responsiveMode: { type: String, attribute: 'responsive-mode', reflect: true },
|
|
25
|
+
|
|
26
|
+
/** Makes the first column sticky to the left */
|
|
27
|
+
stickyFirstColumn: { type: Boolean, attribute: 'sticky-first-column' },
|
|
28
|
+
|
|
29
|
+
/** Message to display when there are no items */
|
|
30
|
+
emptyMessage: { type: String, attribute: 'empty-message' },
|
|
31
|
+
|
|
32
|
+
/** Unique property name for identifying rows */
|
|
33
|
+
rowIdField: { type: String },
|
|
34
|
+
|
|
35
|
+
/** Enables striped rows styling */
|
|
36
|
+
striped: { type: Boolean },
|
|
37
|
+
|
|
38
|
+
/** Enables selection checkboxes column for batch operations */
|
|
39
|
+
selectable: { type: Boolean },
|
|
40
|
+
|
|
41
|
+
/** Array of selected row IDs */
|
|
42
|
+
selectedIds: { type: Array },
|
|
43
|
+
|
|
44
|
+
/** Optional function to compute row ID from a row object: (row) => string|number */
|
|
45
|
+
computeRowId: { type: Function },
|
|
46
|
+
|
|
47
|
+
/** Optional function to compute custom CSS class for a row: (row, index) => string */
|
|
48
|
+
rowClass: { type: Function },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
super();
|
|
54
|
+
this.items = [];
|
|
55
|
+
this.columns = [];
|
|
56
|
+
this.sortField = '';
|
|
57
|
+
this.sortDirection = '';
|
|
58
|
+
this.sortMode = 'client';
|
|
59
|
+
this.responsiveMode = 'auto';
|
|
60
|
+
this.stickyFirstColumn = false;
|
|
61
|
+
this.emptyMessage = 'No data available';
|
|
62
|
+
this.rowIdField = 'id';
|
|
63
|
+
this.striped = false;
|
|
64
|
+
this.selectable = false;
|
|
65
|
+
this.selectedIds = [];
|
|
66
|
+
this.computeRowId = null;
|
|
67
|
+
this.rowClass = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static get styles() {
|
|
71
|
+
return css`
|
|
72
|
+
:host {
|
|
73
|
+
display: block;
|
|
74
|
+
width: 100%;
|
|
75
|
+
box-sizing: border-box;
|
|
76
|
+
container-type: inline-size;
|
|
77
|
+
font-family: var(--dile-data-grid-font-family, inherit);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
*, *::before, *::after {
|
|
81
|
+
box-sizing: border-box;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.table-container {
|
|
85
|
+
width: 100%;
|
|
86
|
+
overflow-x: auto;
|
|
87
|
+
background-color: var(--dile-data-grid-background-color, #ffffff);
|
|
88
|
+
border: var(--dile-data-grid-border, 1px solid #e2e8f0);
|
|
89
|
+
border-radius: var(--dile-data-grid-border-radius, 6px);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
table {
|
|
93
|
+
width: 100%;
|
|
94
|
+
border-collapse: collapse;
|
|
95
|
+
text-align: left;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
thead {
|
|
99
|
+
background-color: var(--dile-data-grid-header-background-color, #f8fafc);
|
|
100
|
+
border-bottom: var(--dile-data-grid-header-border-bottom, 2px solid #e2e8f0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
th {
|
|
104
|
+
padding: var(--dile-data-grid-header-padding, 0.75rem 1rem);
|
|
105
|
+
color: var(--dile-data-grid-header-color, #334155);
|
|
106
|
+
font-weight: var(--dile-data-grid-header-font-weight, 600);
|
|
107
|
+
font-size: var(--dile-data-grid-header-font-size, 0.875rem);
|
|
108
|
+
user-select: none;
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
th.sortable {
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
transition: background-color 0.15s ease-in-out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
th.sortable:hover {
|
|
118
|
+
background-color: var(--dile-data-grid-header-hover-background-color, #f1f5f9);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.header-content {
|
|
122
|
+
display: inline-flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 0.35rem;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.sort-icon {
|
|
128
|
+
display: inline-flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
width: 1.1rem;
|
|
132
|
+
height: 1.1rem;
|
|
133
|
+
color: var(--dile-data-grid-sort-icon-color, #94a3b8);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.sort-icon.active {
|
|
137
|
+
color: var(--dile-data-grid-sort-icon-active-color, #2563eb);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.sort-icon svg {
|
|
141
|
+
width: 100%;
|
|
142
|
+
height: 100%;
|
|
143
|
+
fill: currentColor;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
tbody tr {
|
|
147
|
+
border-bottom: var(--dile-data-grid-row-border-bottom, 1px solid #f1f5f9);
|
|
148
|
+
transition: background-color 0.15s ease-in-out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
tbody tr:last-child {
|
|
152
|
+
border-bottom: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
tbody tr:hover {
|
|
156
|
+
background-color: var(--dile-data-grid-row-hover-background-color, #f8fafc);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
:host([striped]) tbody tr:nth-child(even),
|
|
160
|
+
.striped tbody tr:nth-child(even) {
|
|
161
|
+
background-color: var(--dile-data-grid-row-striped-background-color, #fcfdfe);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
:host([striped]) tbody tr:nth-child(even):hover,
|
|
165
|
+
.striped tbody tr:nth-child(even):hover {
|
|
166
|
+
background-color: var(--dile-data-grid-row-hover-background-color, #f8fafc);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
tbody tr.is-deleted,
|
|
170
|
+
tbody tr.deleted {
|
|
171
|
+
opacity: var(--dile-data-grid-deleted-opacity, 0.6);
|
|
172
|
+
background-color: var(--dile-data-grid-deleted-background-color, rgba(239, 68, 68, 0.04));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
td {
|
|
176
|
+
padding: var(--dile-data-grid-row-padding, 0.75rem 1rem);
|
|
177
|
+
color: var(--dile-data-grid-row-color, #1e293b);
|
|
178
|
+
font-size: var(--dile-data-grid-row-font-size, 0.875rem);
|
|
179
|
+
vertical-align: middle;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.align-left {
|
|
183
|
+
text-align: left;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.align-center {
|
|
187
|
+
text-align: center;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.align-right {
|
|
191
|
+
text-align: right;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.empty-cell {
|
|
195
|
+
text-align: center;
|
|
196
|
+
padding: var(--dile-data-grid-empty-padding, 2.5rem 1rem);
|
|
197
|
+
color: var(--dile-data-grid-empty-color, #64748b);
|
|
198
|
+
font-size: var(--dile-data-grid-empty-font-size, 0.875rem);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Selection Checkbox Cell */
|
|
202
|
+
th.selection-cell,
|
|
203
|
+
td.selection-cell {
|
|
204
|
+
width: var(--dile-data-grid-selection-width, 44px);
|
|
205
|
+
min-width: var(--dile-data-grid-selection-width, 44px);
|
|
206
|
+
max-width: var(--dile-data-grid-selection-width, 44px);
|
|
207
|
+
text-align: center;
|
|
208
|
+
padding-left: 0.5rem;
|
|
209
|
+
padding-right: 0.5rem;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
td.selection-cell dile-checkbox {
|
|
213
|
+
display: inline-flex;
|
|
214
|
+
vertical-align: middle;
|
|
215
|
+
--dile-checkbox-unchecked-color: var(--dile-data-grid-checkbox-unchecked-color, #888);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Sticky Columns */
|
|
219
|
+
th.selection-cell.sticky-left,
|
|
220
|
+
td.selection-cell.sticky-left {
|
|
221
|
+
position: sticky;
|
|
222
|
+
left: 0;
|
|
223
|
+
z-index: 2;
|
|
224
|
+
background-color: var(--dile-data-grid-sticky-background-color, var(--dile-data-grid-background-color, #ffffff));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
th.selection-cell.sticky-left {
|
|
228
|
+
z-index: 3;
|
|
229
|
+
background-color: var(--dile-data-grid-sticky-header-background-color, var(--dile-data-grid-header-background-color, #f8fafc));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
th.selection-cell.sticky-left.has-shadow,
|
|
233
|
+
td.selection-cell.sticky-left.has-shadow {
|
|
234
|
+
box-shadow: var(--dile-data-grid-sticky-shadow, 2px 0 5px -2px rgba(0, 0, 0, 0.12));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
th.sticky-left,
|
|
238
|
+
td.sticky-left {
|
|
239
|
+
position: sticky;
|
|
240
|
+
left: 0;
|
|
241
|
+
z-index: 2;
|
|
242
|
+
background-color: var(--dile-data-grid-sticky-background-color, var(--dile-data-grid-background-color, #ffffff));
|
|
243
|
+
box-shadow: var(--dile-data-grid-sticky-shadow, 2px 0 5px -2px rgba(0, 0, 0, 0.12));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
th.sticky-left {
|
|
247
|
+
z-index: 3;
|
|
248
|
+
background-color: var(--dile-data-grid-sticky-header-background-color, var(--dile-data-grid-header-background-color, #f8fafc));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
th.sticky-left-after-selection,
|
|
252
|
+
td.sticky-left-after-selection {
|
|
253
|
+
position: sticky;
|
|
254
|
+
left: var(--dile-data-grid-selection-width, 44px);
|
|
255
|
+
z-index: 2;
|
|
256
|
+
background-color: var(--dile-data-grid-sticky-background-color, var(--dile-data-grid-background-color, #ffffff));
|
|
257
|
+
box-shadow: var(--dile-data-grid-sticky-shadow, 2px 0 5px -2px rgba(0, 0, 0, 0.12));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
th.sticky-left-after-selection {
|
|
261
|
+
z-index: 3;
|
|
262
|
+
background-color: var(--dile-data-grid-sticky-header-background-color, var(--dile-data-grid-header-background-color, #f8fafc));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
th.sticky-right,
|
|
266
|
+
td.sticky-right {
|
|
267
|
+
position: sticky;
|
|
268
|
+
right: 0;
|
|
269
|
+
z-index: 2;
|
|
270
|
+
background-color: var(--dile-data-grid-sticky-background-color, var(--dile-data-grid-background-color, #ffffff));
|
|
271
|
+
box-shadow: var(--dile-data-grid-sticky-right-shadow, -2px 0 5px -2px rgba(0, 0, 0, 0.12));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
th.sticky-right {
|
|
275
|
+
z-index: 3;
|
|
276
|
+
background-color: var(--dile-data-grid-sticky-header-background-color, var(--dile-data-grid-header-background-color, #f8fafc));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
:host([striped]) tbody tr:nth-child(even) td.sticky-left,
|
|
280
|
+
.striped tbody tr:nth-child(even) td.sticky-left,
|
|
281
|
+
:host([striped]) tbody tr:nth-child(even) td.sticky-left-after-selection,
|
|
282
|
+
.striped tbody tr:nth-child(even) td.sticky-left-after-selection,
|
|
283
|
+
:host([striped]) tbody tr:nth-child(even) td.selection-cell.sticky-left,
|
|
284
|
+
.striped tbody tr:nth-child(even) td.selection-cell.sticky-left,
|
|
285
|
+
:host([striped]) tbody tr:nth-child(even) td.sticky-right,
|
|
286
|
+
.striped tbody tr:nth-child(even) td.sticky-right {
|
|
287
|
+
background-color: var(--dile-data-grid-sticky-striped-background-color, var(--dile-data-grid-row-striped-background-color, #fcfdfe));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
tbody tr:hover td.sticky-left,
|
|
291
|
+
tbody tr:hover td.sticky-left-after-selection,
|
|
292
|
+
tbody tr:hover td.selection-cell.sticky-left,
|
|
293
|
+
tbody tr:hover td.sticky-right {
|
|
294
|
+
background-color: var(--dile-data-grid-row-hover-background-color, #f8fafc);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* Explicit Cards layout (responsive-mode="cards" / "card") */
|
|
298
|
+
:host([responsive-mode="cards"]) .table-container,
|
|
299
|
+
:host([responsive-mode="card"]) .table-container {
|
|
300
|
+
background-color: transparent;
|
|
301
|
+
border: none;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
:host([responsive-mode="cards"]) thead,
|
|
305
|
+
:host([responsive-mode="card"]) thead {
|
|
306
|
+
display: none;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
:host([responsive-mode="cards"]) table,
|
|
310
|
+
:host([responsive-mode="cards"]) tbody,
|
|
311
|
+
:host([responsive-mode="card"]) table,
|
|
312
|
+
:host([responsive-mode="card"]) tbody {
|
|
313
|
+
display: block;
|
|
314
|
+
width: 100%;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
:host([responsive-mode="cards"]) tbody tr,
|
|
318
|
+
:host([responsive-mode="card"]) tbody tr {
|
|
319
|
+
display: block;
|
|
320
|
+
background-color: var(--dile-data-grid-card-background-color, #ffffff);
|
|
321
|
+
border: var(--dile-data-grid-card-border, 1px solid #e2e8f0);
|
|
322
|
+
border-radius: var(--dile-data-grid-card-border-radius, 8px);
|
|
323
|
+
margin-bottom: var(--dile-data-grid-card-gap, 0.75rem);
|
|
324
|
+
padding: var(--dile-data-grid-card-padding, 0.5rem 0);
|
|
325
|
+
box-shadow: var(--dile-data-grid-card-box-shadow, 0 1px 3px rgba(0, 0, 0, 0.05));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
:host([responsive-mode="cards"]) tbody tr:last-child,
|
|
329
|
+
:host([responsive-mode="card"]) tbody tr:last-child {
|
|
330
|
+
margin-bottom: 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
:host([responsive-mode="cards"]) td,
|
|
334
|
+
:host([responsive-mode="card"]) td {
|
|
335
|
+
display: flex;
|
|
336
|
+
justify-content: space-between;
|
|
337
|
+
align-items: center;
|
|
338
|
+
padding: var(--dile-data-grid-card-cell-padding, 0.5rem 1rem);
|
|
339
|
+
border-bottom: var(--dile-data-grid-card-cell-border-bottom, 1px solid #f1f5f9);
|
|
340
|
+
text-align: right;
|
|
341
|
+
position: static;
|
|
342
|
+
box-shadow: none;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
:host([responsive-mode="cards"]) td.selection-cell,
|
|
346
|
+
:host([responsive-mode="card"]) td.selection-cell {
|
|
347
|
+
justify-content: flex-start;
|
|
348
|
+
padding-top: 0.35rem;
|
|
349
|
+
padding-bottom: 0.35rem;
|
|
350
|
+
width: 100%;
|
|
351
|
+
max-width: 100%;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
:host([responsive-mode="cards"]) td.selection-cell::before,
|
|
355
|
+
:host([responsive-mode="card"]) td.selection-cell::before {
|
|
356
|
+
display: none !important;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
:host([responsive-mode="cards"]) td:last-child,
|
|
360
|
+
:host([responsive-mode="card"]) td:last-child {
|
|
361
|
+
border-bottom: none;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
:host([responsive-mode="cards"]) td.hide-on-card,
|
|
365
|
+
:host([responsive-mode="card"]) td.hide-on-card {
|
|
366
|
+
display: none !important;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
:host([responsive-mode="cards"]) td::before,
|
|
370
|
+
:host([responsive-mode="card"]) td::before {
|
|
371
|
+
content: attr(data-label);
|
|
372
|
+
font-weight: var(--dile-data-grid-card-label-font-weight, 600);
|
|
373
|
+
color: var(--dile-data-grid-card-label-color, #64748b);
|
|
374
|
+
font-size: var(--dile-data-grid-card-label-font-size, 0.8rem);
|
|
375
|
+
text-align: left;
|
|
376
|
+
margin-right: 1rem;
|
|
377
|
+
flex-shrink: 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
:host([responsive-mode="cards"]) td[data-label=""]::before,
|
|
381
|
+
:host([responsive-mode="cards"]) td.no-card-label::before,
|
|
382
|
+
:host([responsive-mode="card"]) td[data-label=""]::before,
|
|
383
|
+
:host([responsive-mode="card"]) td.no-card-label::before {
|
|
384
|
+
display: none;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
:host([responsive-mode="cards"]) td[data-label=""],
|
|
388
|
+
:host([responsive-mode="cards"]) td.no-card-label,
|
|
389
|
+
:host([responsive-mode="card"]) td[data-label=""],
|
|
390
|
+
:host([responsive-mode="card"]) td.no-card-label {
|
|
391
|
+
justify-content: flex-end;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
:host([responsive-mode="cards"]) .empty-cell,
|
|
395
|
+
:host([responsive-mode="card"]) .empty-cell {
|
|
396
|
+
display: block;
|
|
397
|
+
text-align: center;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
:host([responsive-mode="cards"]) .empty-cell::before,
|
|
401
|
+
:host([responsive-mode="card"]) .empty-cell::before {
|
|
402
|
+
display: none;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* Container Query: responsive card transformation when container width <= 600px */
|
|
406
|
+
@container (max-width: 600px) {
|
|
407
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) .table-container {
|
|
408
|
+
background-color: transparent;
|
|
409
|
+
border: none;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) thead {
|
|
413
|
+
display: none;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) table,
|
|
417
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) tbody {
|
|
418
|
+
display: block;
|
|
419
|
+
width: 100%;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) tbody tr {
|
|
423
|
+
display: block;
|
|
424
|
+
background-color: var(--dile-data-grid-card-background-color, #ffffff);
|
|
425
|
+
border: var(--dile-data-grid-card-border, 1px solid #e2e8f0);
|
|
426
|
+
border-radius: var(--dile-data-grid-card-border-radius, 8px);
|
|
427
|
+
margin-bottom: var(--dile-data-grid-card-gap, 0.75rem);
|
|
428
|
+
padding: var(--dile-data-grid-card-padding, 0.5rem 0);
|
|
429
|
+
box-shadow: var(--dile-data-grid-card-box-shadow, 0 1px 3px rgba(0, 0, 0, 0.05));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) tbody tr:last-child {
|
|
433
|
+
margin-bottom: 0;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) td {
|
|
437
|
+
display: flex;
|
|
438
|
+
justify-content: space-between;
|
|
439
|
+
align-items: center;
|
|
440
|
+
padding: var(--dile-data-grid-card-cell-padding, 0.5rem 1rem);
|
|
441
|
+
border-bottom: var(--dile-data-grid-card-cell-border-bottom, 1px solid #f1f5f9);
|
|
442
|
+
text-align: right;
|
|
443
|
+
position: static;
|
|
444
|
+
box-shadow: none;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) td.selection-cell {
|
|
448
|
+
justify-content: flex-start;
|
|
449
|
+
padding-top: 0.35rem;
|
|
450
|
+
padding-bottom: 0.35rem;
|
|
451
|
+
width: 100%;
|
|
452
|
+
max-width: 100%;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) td.selection-cell::before {
|
|
456
|
+
display: none !important;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) td:last-child {
|
|
460
|
+
border-bottom: none;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) td.hide-on-card {
|
|
464
|
+
display: none !important;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) td::before {
|
|
468
|
+
content: attr(data-label);
|
|
469
|
+
font-weight: var(--dile-data-grid-card-label-font-weight, 600);
|
|
470
|
+
color: var(--dile-data-grid-card-label-color, #64748b);
|
|
471
|
+
font-size: var(--dile-data-grid-card-label-font-size, 0.8rem);
|
|
472
|
+
text-align: left;
|
|
473
|
+
margin-right: 1rem;
|
|
474
|
+
flex-shrink: 0;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) td[data-label=""]::before,
|
|
478
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) td.no-card-label::before {
|
|
479
|
+
display: none;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) td[data-label=""],
|
|
483
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) td.no-card-label {
|
|
484
|
+
justify-content: flex-end;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) .empty-cell {
|
|
488
|
+
display: block;
|
|
489
|
+
text-align: center;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
:host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) .empty-cell::before {
|
|
493
|
+
display: none;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
`;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
get displayedItems() {
|
|
500
|
+
if (!this.items || !Array.isArray(this.items)) {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (this.sortMode !== 'client' || !this.sortField || !this.sortDirection) {
|
|
505
|
+
return this.items;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const column = (this.columns || []).find((col) => col.field === this.sortField);
|
|
509
|
+
const directionFactor = this.sortDirection === 'desc' ? -1 : 1;
|
|
510
|
+
|
|
511
|
+
return [...this.items].sort((a, b) => {
|
|
512
|
+
const valA = a ? a[this.sortField] : undefined;
|
|
513
|
+
const valB = b ? b[this.sortField] : undefined;
|
|
514
|
+
|
|
515
|
+
if (column && typeof column.comparator === 'function') {
|
|
516
|
+
return directionFactor * column.comparator(valA, valB, a, b);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Safe fallback comparisons
|
|
520
|
+
if (valA === undefined || valA === null) {
|
|
521
|
+
return valB === undefined || valB === null ? 0 : 1;
|
|
522
|
+
}
|
|
523
|
+
if (valB === undefined || valB === null) {
|
|
524
|
+
return -1;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (typeof valA === 'string' && typeof valB === 'string') {
|
|
528
|
+
return directionFactor * valA.localeCompare(valB, undefined, { numeric: true, sensitivity: 'base' });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (valA < valB) return -1 * directionFactor;
|
|
532
|
+
if (valA > valB) return 1 * directionFactor;
|
|
533
|
+
return 0;
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
_getRowId(row) {
|
|
538
|
+
if (!row) return undefined;
|
|
539
|
+
if (typeof this.computeRowId === 'function') {
|
|
540
|
+
return this.computeRowId(row);
|
|
541
|
+
}
|
|
542
|
+
return row[this.rowIdField];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
_isRowSelected(row) {
|
|
546
|
+
if (!this.selectedIds || !Array.isArray(this.selectedIds)) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
const rowId = this._getRowId(row);
|
|
550
|
+
const stringIds = this.selectedIds.map(String);
|
|
551
|
+
return stringIds.includes(String(rowId));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
_getStickyLeftClass(column, colIndex) {
|
|
555
|
+
const isSticky = column.sticky === true || column.sticky === 'left' || (this.stickyFirstColumn && colIndex === 0);
|
|
556
|
+
if (!isSticky) {
|
|
557
|
+
return '';
|
|
558
|
+
}
|
|
559
|
+
if (this.selectable) {
|
|
560
|
+
return 'sticky-left-after-selection';
|
|
561
|
+
}
|
|
562
|
+
return 'sticky-left';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
_isStickyRight(column) {
|
|
566
|
+
return column.sticky === 'right';
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
render() {
|
|
570
|
+
const items = this.displayedItems;
|
|
571
|
+
const cols = this.columns || [];
|
|
572
|
+
const totalCols = (cols.length || 1) + (this.selectable ? 1 : 0);
|
|
573
|
+
|
|
574
|
+
return html`
|
|
575
|
+
<div class="table-container ${this.striped ? 'striped' : ''}">
|
|
576
|
+
<table>
|
|
577
|
+
<thead>
|
|
578
|
+
<tr>
|
|
579
|
+
${this.selectable ? this._renderSelectionHeaderCell() : ''}
|
|
580
|
+
${cols.map((col, colIndex) => this._renderHeaderCell(col, colIndex))}
|
|
581
|
+
</tr>
|
|
582
|
+
</thead>
|
|
583
|
+
<tbody>
|
|
584
|
+
${items.length === 0
|
|
585
|
+
? html`
|
|
586
|
+
<tr class="empty-row">
|
|
587
|
+
<td class="empty-cell" colspan="${totalCols}" data-label="">
|
|
588
|
+
${this.emptyMessage}
|
|
589
|
+
</td>
|
|
590
|
+
</tr>
|
|
591
|
+
`
|
|
592
|
+
: items.map((row, rowIndex) => this._renderRow(row, rowIndex, cols))}
|
|
593
|
+
</tbody>
|
|
594
|
+
</table>
|
|
595
|
+
</div>
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
_renderSelectionHeaderCell() {
|
|
600
|
+
const isSticky = this.stickyFirstColumn;
|
|
601
|
+
const hasShadow = !this.columns.some((col, idx) => (col.sticky === true || col.sticky === 'left' || (this.stickyFirstColumn && idx === 0)));
|
|
602
|
+
const stickyClass = isSticky ? `sticky-left ${hasShadow ? 'has-shadow' : ''}` : '';
|
|
603
|
+
|
|
604
|
+
return html`
|
|
605
|
+
<th class="selection-cell ${stickyClass}" aria-label="Select"></th>
|
|
606
|
+
`;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
_renderHeaderCell(column, colIndex) {
|
|
610
|
+
const isSorted = this.sortField === column.field && !!this.sortDirection;
|
|
611
|
+
const alignClass = column.align ? `align-${column.align}` : 'align-left';
|
|
612
|
+
const sortableClass = column.sortable ? 'sortable' : '';
|
|
613
|
+
const stickyLeftClass = this._getStickyLeftClass(column, colIndex);
|
|
614
|
+
const stickyRightClass = this._isStickyRight(column) ? 'sticky-right' : '';
|
|
615
|
+
const stickyClass = `${stickyLeftClass} ${stickyRightClass}`.trim();
|
|
616
|
+
const styleWidth = column.width ? `width: ${column.width};` : '';
|
|
617
|
+
|
|
618
|
+
return html`
|
|
619
|
+
<th
|
|
620
|
+
class="${alignClass} ${sortableClass} ${stickyClass}"
|
|
621
|
+
style="${styleWidth}"
|
|
622
|
+
@click=${() => this._handleHeaderClick(column)}
|
|
623
|
+
aria-sort="${isSorted ? (this.sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}"
|
|
624
|
+
>
|
|
625
|
+
<div class="header-content">
|
|
626
|
+
<span>${column.header || column.field || ''}</span>
|
|
627
|
+
${column.sortable ? this._renderSortIcon(column) : ''}
|
|
628
|
+
</div>
|
|
629
|
+
</th>
|
|
630
|
+
`;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
_renderSortIcon(column) {
|
|
634
|
+
const isSorted = this.sortField === column.field && !!this.sortDirection;
|
|
635
|
+
if (!isSorted) {
|
|
636
|
+
return html`<span class="sort-icon" aria-hidden="true"></span>`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const icon = this.sortDirection === 'asc' ? arrowDropUpIcon : arrowDropDownIcon;
|
|
640
|
+
|
|
641
|
+
return html`
|
|
642
|
+
<span class="sort-icon active" aria-hidden="true">
|
|
643
|
+
${icon}
|
|
644
|
+
</span>
|
|
645
|
+
`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
_renderRow(row, rowIndex, columns) {
|
|
649
|
+
let rowClassName = '';
|
|
650
|
+
if (typeof this.rowClass === 'function') {
|
|
651
|
+
rowClassName = this.rowClass(row, rowIndex) || '';
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return html`
|
|
655
|
+
<tr class="${rowClassName}" @click=${(e) => this._handleRowClick(row, rowIndex, e)}>
|
|
656
|
+
${this.selectable ? this._renderSelectionCell(row, rowIndex) : ''}
|
|
657
|
+
${columns.map((col, colIndex) => this._renderCell(row, rowIndex, col, colIndex))}
|
|
658
|
+
</tr>
|
|
659
|
+
`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
_renderSelectionCell(row, rowIndex) {
|
|
663
|
+
const isSticky = this.stickyFirstColumn;
|
|
664
|
+
const hasShadow = !this.columns.some((col, idx) => (col.sticky === true || col.sticky === 'left' || (this.stickyFirstColumn && idx === 0)));
|
|
665
|
+
const stickyClass = isSticky ? `sticky-left ${hasShadow ? 'has-shadow' : ''}` : '';
|
|
666
|
+
const isSelected = this._isRowSelected(row);
|
|
667
|
+
|
|
668
|
+
return html`
|
|
669
|
+
<td
|
|
670
|
+
class="selection-cell ${stickyClass}"
|
|
671
|
+
data-label=""
|
|
672
|
+
@click=${(e) => e.stopPropagation()}
|
|
673
|
+
>
|
|
674
|
+
<dile-checkbox
|
|
675
|
+
?checked=${isSelected}
|
|
676
|
+
@dile-checkbox-changed=${(e) => this._handleCheckboxChange(row, rowIndex, e)}
|
|
677
|
+
></dile-checkbox>
|
|
678
|
+
</td>
|
|
679
|
+
`;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
_handleCheckboxChange(row, rowIndex, event) {
|
|
683
|
+
const itemId = this._getRowId(row);
|
|
684
|
+
const checked = Boolean(event.detail.checked);
|
|
685
|
+
|
|
686
|
+
this.dispatchEvent(
|
|
687
|
+
new CustomEvent('item-checkbox-changed', {
|
|
688
|
+
bubbles: true,
|
|
689
|
+
composed: true,
|
|
690
|
+
detail: {
|
|
691
|
+
checked,
|
|
692
|
+
itemId,
|
|
693
|
+
row,
|
|
694
|
+
index: rowIndex,
|
|
695
|
+
},
|
|
696
|
+
})
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
this.dispatchEvent(
|
|
700
|
+
new CustomEvent('dile-data-grid-item-selected', {
|
|
701
|
+
bubbles: true,
|
|
702
|
+
composed: true,
|
|
703
|
+
detail: {
|
|
704
|
+
checked,
|
|
705
|
+
itemId,
|
|
706
|
+
row,
|
|
707
|
+
index: rowIndex,
|
|
708
|
+
},
|
|
709
|
+
})
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
_renderCell(row, rowIndex, column, colIndex) {
|
|
714
|
+
const alignClass = column.align ? `align-${column.align}` : 'align-left';
|
|
715
|
+
const hideOnCardClass = column.hideOnCard ? 'hide-on-card' : '';
|
|
716
|
+
const noCardLabelClass = column.hideCardLabel ? 'no-card-label' : '';
|
|
717
|
+
const stickyLeftClass = this._getStickyLeftClass(column, colIndex);
|
|
718
|
+
const stickyRightClass = this._isStickyRight(column) ? 'sticky-right' : '';
|
|
719
|
+
const stickyClass = `${stickyLeftClass} ${stickyRightClass}`.trim();
|
|
720
|
+
const label = column.hideCardLabel ? '' : (column.header || column.field || '');
|
|
721
|
+
|
|
722
|
+
let content;
|
|
723
|
+
if (typeof column.render === 'function') {
|
|
724
|
+
content = column.render(row, rowIndex, column);
|
|
725
|
+
} else if (column.field && row && row[column.field] !== undefined) {
|
|
726
|
+
content = row[column.field];
|
|
727
|
+
} else {
|
|
728
|
+
content = '';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return html`
|
|
732
|
+
<td
|
|
733
|
+
class="${alignClass} ${hideOnCardClass} ${noCardLabelClass} ${stickyClass}"
|
|
734
|
+
data-label="${label}"
|
|
735
|
+
>
|
|
736
|
+
${content}
|
|
737
|
+
</td>
|
|
738
|
+
`;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
_handleHeaderClick(column) {
|
|
742
|
+
if (!column.sortable || !column.field) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
let nextDirection = 'asc';
|
|
747
|
+
if (this.sortField === column.field) {
|
|
748
|
+
if (this.sortDirection === 'asc') {
|
|
749
|
+
nextDirection = 'desc';
|
|
750
|
+
} else if (this.sortDirection === 'desc') {
|
|
751
|
+
nextDirection = '';
|
|
752
|
+
} else {
|
|
753
|
+
nextDirection = 'asc';
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
this.sortField = nextDirection ? column.field : '';
|
|
758
|
+
this.sortDirection = nextDirection;
|
|
759
|
+
|
|
760
|
+
this.dispatchEvent(
|
|
761
|
+
new CustomEvent('dile-data-grid-sort', {
|
|
762
|
+
bubbles: true,
|
|
763
|
+
composed: true,
|
|
764
|
+
detail: {
|
|
765
|
+
field: this.sortField,
|
|
766
|
+
direction: this.sortDirection,
|
|
767
|
+
column,
|
|
768
|
+
},
|
|
769
|
+
})
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
_handleRowClick(row, index, event) {
|
|
774
|
+
this.dispatchEvent(
|
|
775
|
+
new CustomEvent('dile-data-grid-row-click', {
|
|
776
|
+
bubbles: true,
|
|
777
|
+
composed: true,
|
|
778
|
+
detail: {
|
|
779
|
+
row,
|
|
780
|
+
index,
|
|
781
|
+
event,
|
|
782
|
+
},
|
|
783
|
+
})
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.1",
|
|
4
4
|
"description": "UI Core components from dile-components.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "add1124d218762f20cd86bd72bb04562541f9833"
|
|
32
32
|
}
|