@dile/ui 3.11.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.
@@ -45,6 +45,7 @@ describe('dile-data-grid', () => {
45
45
  emptyMessage: 'Nothing found',
46
46
  });
47
47
  const emptyCell = el.shadowRoot.querySelector('.empty-cell');
48
+ expect(emptyCell).toBeTruthy();
48
49
  expect(emptyCell.textContent.trim()).toBe('Nothing found');
49
50
  });
50
51
 
@@ -276,7 +277,7 @@ describe('dile-data-grid', () => {
276
277
  expect(tds[2].classList.contains('sticky-right')).toBe(true);
277
278
  });
278
279
 
279
- it('applies sticky-left to the first column when stickyFirstColumn property is set', async () => {
280
+ it('applies sticky-left to the first column when stickyFirstColumn property is set without selection', async () => {
280
281
  const el = await createDataGrid({
281
282
  columns: sampleColumns,
282
283
  items: sampleItems,
@@ -290,4 +291,131 @@ describe('dile-data-grid', () => {
290
291
  expect(tds[0].classList.contains('sticky-left')).toBe(true);
291
292
  expect(ths[1].classList.contains('sticky-left')).toBe(false);
292
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
+ });
293
421
  });
@@ -1,5 +1,6 @@
1
1
  import { html, css, LitElement } from 'lit';
2
2
  import { arrowDropUpIcon, arrowDropDownIcon } from '@dile/icons';
3
+ import '../../checkbox/checkbox.js';
3
4
 
4
5
  export class DileDataGrid extends LitElement {
5
6
  static get properties() {
@@ -17,7 +18,7 @@ export class DileDataGrid extends LitElement {
17
18
  sortDirection: { type: String },
18
19
 
19
20
  /** Mode of sorting: 'client' (sorts data internally) or 'external' (only emits event) */
20
- sortMode: { type: String },
21
+ sortMode: { type: String, attribute: 'sort-mode' },
21
22
 
22
23
  /** Responsive mode: 'auto' (container query card switch), 'cards' (always card), or 'scroll' (table scroll) */
23
24
  responsiveMode: { type: String, attribute: 'responsive-mode', reflect: true },
@@ -26,13 +27,25 @@ export class DileDataGrid extends LitElement {
26
27
  stickyFirstColumn: { type: Boolean, attribute: 'sticky-first-column' },
27
28
 
28
29
  /** Message to display when there are no items */
29
- emptyMessage: { type: String },
30
+ emptyMessage: { type: String, attribute: 'empty-message' },
30
31
 
31
32
  /** Unique property name for identifying rows */
32
33
  rowIdField: { type: String },
33
34
 
34
35
  /** Enables striped rows styling */
35
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 },
36
49
  };
37
50
  }
38
51
 
@@ -48,6 +61,10 @@ export class DileDataGrid extends LitElement {
48
61
  this.emptyMessage = 'No data available';
49
62
  this.rowIdField = 'id';
50
63
  this.striped = false;
64
+ this.selectable = false;
65
+ this.selectedIds = [];
66
+ this.computeRowId = null;
67
+ this.rowClass = null;
51
68
  }
52
69
 
53
70
  static get styles() {
@@ -149,6 +166,12 @@ export class DileDataGrid extends LitElement {
149
166
  background-color: var(--dile-data-grid-row-hover-background-color, #f8fafc);
150
167
  }
151
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
+
152
175
  td {
153
176
  padding: var(--dile-data-grid-row-padding, 0.75rem 1rem);
154
177
  color: var(--dile-data-grid-row-color, #1e293b);
@@ -175,7 +198,42 @@ export class DileDataGrid extends LitElement {
175
198
  font-size: var(--dile-data-grid-empty-font-size, 0.875rem);
176
199
  }
177
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
+
178
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
+
179
237
  th.sticky-left,
180
238
  td.sticky-left {
181
239
  position: sticky;
@@ -190,6 +248,20 @@ export class DileDataGrid extends LitElement {
190
248
  background-color: var(--dile-data-grid-sticky-header-background-color, var(--dile-data-grid-header-background-color, #f8fafc));
191
249
  }
192
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
+
193
265
  th.sticky-right,
194
266
  td.sticky-right {
195
267
  position: sticky;
@@ -206,12 +278,18 @@ export class DileDataGrid extends LitElement {
206
278
 
207
279
  :host([striped]) tbody tr:nth-child(even) td.sticky-left,
208
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,
209
285
  :host([striped]) tbody tr:nth-child(even) td.sticky-right,
210
286
  .striped tbody tr:nth-child(even) td.sticky-right {
211
287
  background-color: var(--dile-data-grid-sticky-striped-background-color, var(--dile-data-grid-row-striped-background-color, #fcfdfe));
212
288
  }
213
289
 
214
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,
215
293
  tbody tr:hover td.sticky-right {
216
294
  background-color: var(--dile-data-grid-row-hover-background-color, #f8fafc);
217
295
  }
@@ -264,6 +342,20 @@ export class DileDataGrid extends LitElement {
264
342
  box-shadow: none;
265
343
  }
266
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
+
267
359
  :host([responsive-mode="cards"]) td:last-child,
268
360
  :host([responsive-mode="card"]) td:last-child {
269
361
  border-bottom: none;
@@ -352,6 +444,18 @@ export class DileDataGrid extends LitElement {
352
444
  box-shadow: none;
353
445
  }
354
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
+
355
459
  :host(:not([responsive-mode="scroll"]):not([responsive-mode="cards"]):not([responsive-mode="card"])) td:last-child {
356
460
  border-bottom: none;
357
461
  }
@@ -430,8 +534,32 @@ export class DileDataGrid extends LitElement {
430
534
  });
431
535
  }
432
536
 
433
- _isStickyLeft(column, colIndex) {
434
- return column.sticky === true || column.sticky === 'left' || (this.stickyFirstColumn && colIndex === 0);
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';
435
563
  }
436
564
 
437
565
  _isStickyRight(column) {
@@ -441,12 +569,14 @@ export class DileDataGrid extends LitElement {
441
569
  render() {
442
570
  const items = this.displayedItems;
443
571
  const cols = this.columns || [];
572
+ const totalCols = (cols.length || 1) + (this.selectable ? 1 : 0);
444
573
 
445
574
  return html`
446
575
  <div class="table-container ${this.striped ? 'striped' : ''}">
447
576
  <table>
448
577
  <thead>
449
578
  <tr>
579
+ ${this.selectable ? this._renderSelectionHeaderCell() : ''}
450
580
  ${cols.map((col, colIndex) => this._renderHeaderCell(col, colIndex))}
451
581
  </tr>
452
582
  </thead>
@@ -454,7 +584,7 @@ export class DileDataGrid extends LitElement {
454
584
  ${items.length === 0
455
585
  ? html`
456
586
  <tr class="empty-row">
457
- <td class="empty-cell" colspan="${cols.length || 1}" data-label="">
587
+ <td class="empty-cell" colspan="${totalCols}" data-label="">
458
588
  ${this.emptyMessage}
459
589
  </td>
460
590
  </tr>
@@ -466,15 +596,23 @@ export class DileDataGrid extends LitElement {
466
596
  `;
467
597
  }
468
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
+
469
609
  _renderHeaderCell(column, colIndex) {
470
610
  const isSorted = this.sortField === column.field && !!this.sortDirection;
471
611
  const alignClass = column.align ? `align-${column.align}` : 'align-left';
472
612
  const sortableClass = column.sortable ? 'sortable' : '';
473
- const stickyClass = this._isStickyLeft(column, colIndex)
474
- ? 'sticky-left'
475
- : this._isStickyRight(column)
476
- ? 'sticky-right'
477
- : '';
613
+ const stickyLeftClass = this._getStickyLeftClass(column, colIndex);
614
+ const stickyRightClass = this._isStickyRight(column) ? 'sticky-right' : '';
615
+ const stickyClass = `${stickyLeftClass} ${stickyRightClass}`.trim();
478
616
  const styleWidth = column.width ? `width: ${column.width};` : '';
479
617
 
480
618
  return html`
@@ -508,22 +646,77 @@ export class DileDataGrid extends LitElement {
508
646
  }
509
647
 
510
648
  _renderRow(row, rowIndex, columns) {
649
+ let rowClassName = '';
650
+ if (typeof this.rowClass === 'function') {
651
+ rowClassName = this.rowClass(row, rowIndex) || '';
652
+ }
653
+
511
654
  return html`
512
- <tr @click=${(e) => this._handleRowClick(row, rowIndex, e)}>
655
+ <tr class="${rowClassName}" @click=${(e) => this._handleRowClick(row, rowIndex, e)}>
656
+ ${this.selectable ? this._renderSelectionCell(row, rowIndex) : ''}
513
657
  ${columns.map((col, colIndex) => this._renderCell(row, rowIndex, col, colIndex))}
514
658
  </tr>
515
659
  `;
516
660
  }
517
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
+
518
713
  _renderCell(row, rowIndex, column, colIndex) {
519
714
  const alignClass = column.align ? `align-${column.align}` : 'align-left';
520
715
  const hideOnCardClass = column.hideOnCard ? 'hide-on-card' : '';
521
716
  const noCardLabelClass = column.hideCardLabel ? 'no-card-label' : '';
522
- const stickyClass = this._isStickyLeft(column, colIndex)
523
- ? 'sticky-left'
524
- : this._isStickyRight(column)
525
- ? 'sticky-right'
526
- : '';
717
+ const stickyLeftClass = this._getStickyLeftClass(column, colIndex);
718
+ const stickyRightClass = this._isStickyRight(column) ? 'sticky-right' : '';
719
+ const stickyClass = `${stickyLeftClass} ${stickyRightClass}`.trim();
527
720
  const label = column.hideCardLabel ? '' : (column.header || column.field || '');
528
721
 
529
722
  let content;
@@ -26,6 +26,6 @@ export const DileCloseOnEscPressed = (SuperClass) =>
26
26
  }
27
27
 
28
28
  close() {
29
- console.log('You need to implement a close method!');
29
+ console.log('You need to implement a close method. Read docs to find more information.');
30
30
  }
31
31
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dile/ui",
3
- "version": "3.11.0",
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": "5ebe41acdb9345d9f416a10c7e2819ac02e054c1"
31
+ "gitHead": "add1124d218762f20cd86bd72bb04562541f9833"
32
32
  }