@kubex/zinc 1.1.161 → 1.1.162

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.
@@ -156,7 +156,7 @@ Use `hide-pagination` to hide pagination controls even when multiple pages are a
156
156
 
157
157
  ### Row Selection
158
158
 
159
- By default, rows can be selected by clicking. Selected rows are tracked and can be used with bulk actions.
159
+ Rows get a checkbox column and can be selected by clicking once the table has an action to use them: something in the `delete-action`, `modify-action`, `create-action` or `actions` slot. Without one, the table has no checkboxes.
160
160
 
161
161
  ```html:preview
162
162
  <zn-data-table
@@ -176,28 +176,11 @@ By default, rows can be selected by clicking. Selected rows are tracked and can
176
176
  </zn-data-table>
177
177
  ```
178
178
 
179
- ### Hide Checkboxes
180
-
181
- Use `hide-checkboxes` to disable row selection functionality.
182
-
183
- ```html:preview
184
- <zn-data-table
185
- data-uri="/data/data-table.json"
186
- method="GET"
187
- hide-checkboxes
188
- headers='[
189
- {"key":"id","label":"ID"},
190
- {"key":"name","label":"Name"},
191
- {"key":"email","label":"Email"}
192
- ]'>
193
- </zn-data-table>
194
- ```
195
-
196
179
  ### Bulk Actions
197
180
 
198
181
  Add action buttons to the table header for performing operations on selected rows. Use the `delete-action`, `modify-action`, and `create-action` slots.
199
182
 
200
- `delete-action` and `modify-action` sit beside the caption alongside a select-all toggle, and only appear once rows are selected. `create-action` is a primary action rather than a selection one, so it renders at the end of the header's right-hand group, after the search field.
183
+ `delete-action` and `modify-action` appear at the start of the header's right-hand group once rows are selected. `create-action` is a primary action rather than a selection one, so it renders at the end of that group, after the search field.
201
184
 
202
185
  ```html:preview
203
186
  <zn-data-table
@@ -225,6 +208,32 @@ Add action buttons to the table header for performing operations on selected row
225
208
  </zn-data-table>
226
209
  ```
227
210
 
211
+ ### Actions Menu
212
+
213
+ Elements in the `actions` slot are listed in an ellipsis menu at the start of the header's right-hand group. The menu is disabled until at least one row is checked. Each `zn-button` becomes a menu item using its label, `icon` and `color`, and choosing it clicks the button. A `zn-confirm` takes its label and icon from its `trigger` slot button, and choosing it opens the confirm dialog. An `input[name="keys"]` inside a slotted element is kept filled with the selected row keys, the same as for `delete-action`.
214
+
215
+ ```html:preview
216
+ <zn-data-table
217
+ data-uri="/data/data-table.json"
218
+ method="GET"
219
+ caption="Customers"
220
+ headers='[
221
+ {"key":"id","label":"ID"},
222
+ {"key":"name","label":"Name"},
223
+ {"key":"email","label":"Email"},
224
+ {"key":"status","label":"Status"}
225
+ ]'>
226
+
227
+ <zn-button slot="actions" icon="archive@lu">Archive</zn-button>
228
+
229
+ <zn-confirm slot="actions" type="error" caption="Delete customers" content="Delete the selected customers?">
230
+ <zn-button slot="trigger" icon="trash-2@lu" color="error">Delete</zn-button>
231
+ <form><input type="hidden" name="keys"></form>
232
+ </zn-confirm>
233
+
234
+ </zn-data-table>
235
+ ```
236
+
228
237
  ### Secondary Columns
229
238
 
230
239
  Mark columns as secondary to hide them by default. Users can expand rows to view secondary data.
@@ -862,6 +871,7 @@ Access table methods programmatically to control behavior.
862
871
  {"key":"email","label":"Email"},
863
872
  {"key":"status","label":"Status"}
864
873
  ]'>
874
+ <zn-button slot="delete-action" color="error" icon="delete">Delete Selected</zn-button>
865
875
  </zn-data-table>
866
876
 
867
877
  <br />
@@ -877,7 +887,7 @@ Access table methods programmatically to control behavior.
877
887
  });
878
888
 
879
889
  document.querySelector('#select-all-btn').addEventListener('click', () => {
880
- table.selectAll(new Event('click'));
890
+ table.selectAll();
881
891
  });
882
892
  </script>
883
893
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.161",
3
+ "version": "1.1.162",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -274,6 +274,7 @@
274
274
  --zn-input-border-color-hover: rgb(var(--zn-color-border-active));
275
275
  --zn-input-border-color-focus: var(--zn-color-primary-500);
276
276
  --zn-input-border-color-disabled: hsl(280, 6%, 9%);
277
+ --zn-checkbox-border-color: rgb(var(--zn-color-border-active));
277
278
 
278
279
  /* Focus rings */
279
280
  --zn-focus-ring-color: var(--zn-color-primary-700);
@@ -217,6 +217,7 @@
217
217
  --zn-input-border-color-hover: var(--zn-color-neutral-400);
218
218
  --zn-input-border-color-focus: var(--zn-color-primary-500);
219
219
  --zn-input-border-color-disabled: var(--zn-color-neutral-300);
220
+ --zn-checkbox-border-color: var(--zn-color-neutral-400);
220
221
  --zn-input-border-width: 1px;
221
222
 
222
223
  /* Input Required Content */
@@ -487,6 +487,11 @@
487
487
  }
488
488
  }
489
489
 
490
+ // Lift the focused button so later siblings don't paint over its focus ring
491
+ :host([data-zn-button-group__button]:focus-within) {
492
+ z-index: 1;
493
+ }
494
+
490
495
  :host([data-zn-button-group__button--first]:not([data-zn-button-group__button--last])) .button {
491
496
  border-radius: 5px 0 0 5px !important;
492
497
  border-right: 0 !important;
@@ -43,7 +43,7 @@ type ColorOption = 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'suc
43
43
  * @csspart control--indeterminate - Matches the control part when the checkbox is indeterminate.
44
44
  * @csspart checked-icon - The checked icon, an `<zn-icon>` element.
45
45
  * @csspart unchecked-icon - The unchecked icon, an `<zn-icon>` element.
46
- * @csspart indeterminate-icon - The indeterminate icon, an `<zn-icon>` element.
46
+ * @csspart indeterminate-icon - The indeterminate icon, an `<svg>` element.
47
47
  * @csspart image-container - The wrapper around the built-in image or image slot.
48
48
  * @csspart image - The built-in image.
49
49
  * @csspart card-title - The title inside a selection card.
@@ -404,11 +404,15 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
404
404
 
405
405
  ${!this.checked && this.indeterminate
406
406
  ? html`
407
- <zn-icon
408
- part="indeterminate-icon"
409
- class="checkbox__indeterminate-icon"
410
- size="16"
411
- src="check_indeterminate_small"></zn-icon>`
407
+ <svg viewBox="0 0 24 24"
408
+ fill="none"
409
+ stroke="currentColor"
410
+ stroke-width="4"
411
+ stroke-linecap="round"
412
+ part="indeterminate-icon"
413
+ class="checkbox__indeterminate-icon">
414
+ <line x1="5" y1="12" x2="19" y2="12"></line>
415
+ </svg>`
412
416
  : ''}
413
417
  </span>
414
418
 
@@ -28,13 +28,13 @@
28
28
  --checkbox-color: var(--zn-color-primary-600);
29
29
  --checkbox-color-hover: var(--zn-color-primary-500);
30
30
  --checkbox-icon-color: var(--zn-color-neutral-0);
31
- --checkbox-border-color: var(--zn-input-border-color);
31
+ --checkbox-border-color: var(--zn-checkbox-border-color, var(--zn-input-border-color));
32
32
  --checkbox-bg-color: var(--zn-input-background-color);
33
33
 
34
34
  &.checkbox--default {
35
35
  --checkbox-color: var(--zn-color-primary-600);
36
36
  --checkbox-color-hover: var(--zn-color-primary-500);
37
- --checkbox-border-color: var(--zn-input-border-color);
37
+ --checkbox-border-color: var(--zn-checkbox-border-color, var(--zn-input-border-color));
38
38
  }
39
39
 
40
40
  &.checkbox--primary {
@@ -131,12 +131,8 @@
131
131
  }
132
132
 
133
133
  .checkbox__indeterminate-icon {
134
- --icon-size: var(--toggle-size);
135
- --icon-color: var(--checkbox-icon-color);
136
-
137
- display: inline-flex;
138
- width: var(--toggle-size);
139
- height: var(--toggle-size);
134
+ height: max(16px, 70%);
135
+ color: var(--checkbox-icon-color);
140
136
  }
141
137
 
142
138
  .svg-icon {
@@ -2,7 +2,7 @@ import {classMap} from "lit/directives/class-map.js";
2
2
  import {type CSSResultGroup, html, nothing, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
3
3
  import {HasSlotController} from "../../internal/slot";
4
4
  import {ifDefined} from "lit/directives/if-defined.js";
5
- import {property, query, state} from 'lit/decorators.js';
5
+ import {property, state} from 'lit/decorators.js';
6
6
  import {ref} from "lit/directives/ref.js";
7
7
  import {ResizeController} from '@lit-labs/observers/resize-controller.js';
8
8
  import {Task} from "@lit/task";
@@ -91,6 +91,7 @@ interface Response {
91
91
  export enum ActionSlots {
92
92
  delete = 'delete-action',
93
93
  modify = 'modify-action',
94
+ actions = 'actions',
94
95
  create = 'create-action',
95
96
  filter = 'filter',
96
97
  filter_top = 'filter-top',
@@ -188,8 +189,9 @@ type AllowedInputElement =
188
189
  * @slot sort - Slot for sort component, in the header's right-hand group.
189
190
  * @slot filter - Slot for filter component, in the header's right-hand group.
190
191
  * @slot filter-top - Slot for a top-level filter component, rendered above the table's panel.
191
- * @slot delete-action - Slot for delete action button, shown beside the caption once rows are selected.
192
- * @slot modify-action - Slot for modify action button, shown beside the caption once rows are selected.
192
+ * @slot delete-action - Slot for delete action button, shown in the header once rows are selected.
193
+ * @slot modify-action - Slot for modify action button, shown in the header once rows are selected.
194
+ * @slot actions - `zn-button` or `zn-confirm` elements listed in a header menu, enabled once rows are selected.
193
195
  * @slot create-action - Slot for create action button, at the end of the header's right-hand group.
194
196
  * @slot inputs - Slot for additional input controls.
195
197
  * @slot empty-state - Slot for custom empty state.
@@ -261,9 +263,6 @@ export default class ZnDataTable extends ZincElement {
261
263
 
262
264
  @property({attribute: "empty-state-icon"}) emptyStateIcon: string = "data_alert";
263
265
 
264
- // Hide the checkbox column
265
- @property({attribute: 'hide-checkboxes', type: Boolean}) hideCheckboxes: boolean;
266
-
267
266
  @property() filters: [] = [];
268
267
 
269
268
  @property() method: 'GET' | 'POST' = 'POST';
@@ -288,7 +287,6 @@ export default class ZnDataTable extends ZincElement {
288
287
  @property() groups = '';
289
288
 
290
289
  @property({attribute: 'per-page-size', type: Number}) itemsPerPage: number = DEFAULT_PER_PAGE;
291
- @query('#select-all-rows') selectAllButton: ZnButton;
292
290
 
293
291
  // Data Table Properties
294
292
  private _initialLoad = true;
@@ -329,6 +327,7 @@ export default class ZnDataTable extends ZincElement {
329
327
  ActionSlots.search.valueOf(),
330
328
  ActionSlots.delete.valueOf(),
331
329
  ActionSlots.modify.valueOf(),
330
+ ActionSlots.actions.valueOf(),
332
331
  ActionSlots.create.valueOf(),
333
332
  ActionSlots.filter.valueOf(),
334
333
  ActionSlots.sort.valueOf(),
@@ -673,8 +672,7 @@ export default class ZnDataTable extends ZincElement {
673
672
  return html`
674
673
  <div>${this.loadingTable()}</div>`;
675
674
  }
676
- return html`
677
- <div class="reduced-opacity">${this._lastTableContent}</div>`;
675
+ return this.tableContent(this._lastTableContent, true);
678
676
  },
679
677
  complete: (data) => {
680
678
  this._initialLoad = false;
@@ -683,7 +681,7 @@ export default class ZnDataTable extends ZincElement {
683
681
  emptyBody = !this._lastLoadHadRows;
684
682
  this._lastTableContent = html`
685
683
  <div>${this.renderTable(data as Response)}</div>`;
686
- return this._lastTableContent;
684
+ return this.tableContent(this._lastTableContent, false);
687
685
  },
688
686
  error: (error) => {
689
687
  if (error instanceof Error) {
@@ -719,6 +717,7 @@ export default class ZnDataTable extends ZincElement {
719
717
 
720
718
  const hasActions = this.hasSlotController.test(ActionSlots.delete.valueOf())
721
719
  || this.hasSlotController.test(ActionSlots.modify.valueOf())
720
+ || this.hasSlotController.test(ActionSlots.actions.valueOf())
722
721
  || this.hasSlotController.test(ActionSlots.create.valueOf())
723
722
  || this.hasSlotController.test(ActionSlots.sort.valueOf())
724
723
  || this.hasSlotController.test(ActionSlots.search.valueOf());
@@ -902,11 +901,14 @@ export default class ZnDataTable extends ZincElement {
902
901
  const error = data?.error?.text ? data.error : undefined;
903
902
 
904
903
  if (!data?.rows || data.rows.length === 0) {
904
+ this._rows = [];
905
+ this.pruneSelectedRows();
905
906
  return html`${error ? html`
906
907
  <div class="table__error">${this.renderErrorAlert(error)}</div>` : nothing}${this.emptyState()}`;
907
908
  }
908
909
 
909
910
  this._rows = this.getRows(data);
911
+ this.pruneSelectedRows();
910
912
 
911
913
  if (this.groupBy) {
912
914
  // we need to group the rows by the group column
@@ -1029,16 +1031,18 @@ export default class ZnDataTable extends ZincElement {
1029
1031
  });
1030
1032
 
1031
1033
  const anyHidden = this.hasHiddenColumns();
1032
- const colCount = filteredHeaders.length + (this.rowHasActions ? 1 : 0) + (anyHidden ? 1 : 0);
1034
+ const showCheckboxes = this.isSelectable();
1035
+ const colCount = filteredHeaders.length + (this.rowHasActions ? 1 : 0) + (anyHidden ? 1 : 0) + (showCheckboxes ? 1 : 0);
1033
1036
 
1034
1037
  return html`
1035
1038
  <div class="table__scroll">
1036
1039
  <table class="${classMap({
1037
1040
  'table': true,
1038
- 'with-hover': !this.unsortable && !this.hideCheckboxes,
1041
+ 'with-hover': !this.unsortable && this.isSelectable(),
1039
1042
  })}">
1040
1043
  <thead>
1041
1044
  <tr>
1045
+ ${showCheckboxes ? this.renderSelectAllCheckbox() : nothing}
1042
1046
  ${anyHidden ? html`
1043
1047
  <th class="table__head table__head--expander"></th>` : nothing}
1044
1048
  ${filteredHeaders.map((header: HeaderConfig) => this.renderCellHeader(header))}
@@ -1057,10 +1061,8 @@ export default class ZnDataTable extends ZincElement {
1057
1061
  <th colspan="${colCount}" scope="colgroup">${group.label}</th>
1058
1062
  </tr>` : nothing}
1059
1063
  ${group.rows.map((row: Row) => html`
1060
- <tr class="${classMap({
1061
- 'table__row--selected': this.isRowSelected(row),
1062
- 'table__row--data': true,
1063
- })}" data-row-id="${row.id}">
1064
+ <tr class="table__row--data" data-row-id="${row.id}">
1065
+ ${showCheckboxes ? this.renderRowCheckbox(row) : nothing}
1064
1066
  ${anyHidden ? this.renderExpanderCell(row) : nothing}
1065
1067
  ${(visibleRowCells.get(row.id) || row.cells).map((value: Cell, index: number) => this.renderCellBody(index, value, row))}
1066
1068
  ${this.rowHasActions ? this.renderActions(row) : nothing}
@@ -1074,15 +1076,21 @@ export default class ZnDataTable extends ZincElement {
1074
1076
  `;
1075
1077
  }
1076
1078
 
1079
+ // Pending and loaded states share this template so Lit keeps the table's DOM (and focus) across loads
1080
+ private tableContent(content: TemplateResult, pending: boolean) {
1081
+ return html`
1082
+ <div class="${classMap({'reduced-opacity': pending})}">${content}</div>`;
1083
+ }
1084
+
1077
1085
  getTableHeader() {
1078
1086
  return html`
1079
1087
  <div class="table__header">
1080
1088
  <div class="table__header__actions">
1081
1089
  ${this.caption ? html`
1082
1090
  <h3 class="table__header__caption">${this.caption}</h3>` : nothing}
1083
- ${this.getActions()}
1084
1091
  </div>
1085
1092
  <div class="table__header__right">
1093
+ ${this.getActions()}
1086
1094
  ${this.getHeaderControls()}
1087
1095
  </div>
1088
1096
  </div>
@@ -1302,37 +1310,7 @@ export default class ZnDataTable extends ZincElement {
1302
1310
  getActions() {
1303
1311
  const actions = [];
1304
1312
 
1305
- // Select-all only earns its place alongside the bulk actions it feeds
1306
- const hasBulkActions = this.hasSlotController.test(ActionSlots.delete.valueOf())
1307
- || this.hasSlotController.test(ActionSlots.modify.valueOf());
1308
-
1309
- if (!hasBulkActions) {
1310
- return [];
1311
- }
1312
-
1313
- if (!this.hideCheckboxes && this._rows.length > 0) {
1314
- actions.push(html`
1315
- <zn-button @click="${this.selectAll}"
1316
- id="select-all-rows"
1317
- color="transparent"
1318
- icon="indeterminate_check_box"
1319
- icon-size="22"
1320
- icon-color="primary"
1321
- tooltip="Select All"
1322
- slot="trigger">
1323
- </zn-button>`);
1324
- }
1325
-
1326
1313
  if (this.selectedRows.length > 0) {
1327
- actions.push(html`
1328
- <zn-button @click="${this.clearSelectedRows}"
1329
- color="transparent"
1330
- icon="disabled_by_default"
1331
- icon-size="22"
1332
- tooltip="Clear Selection"
1333
- slot="trigger">
1334
- </zn-button>`);
1335
-
1336
1314
  if (this.hasSlotController.test(ActionSlots.delete.valueOf())) {
1337
1315
  actions.push(html`
1338
1316
  <slot name="${ActionSlots.delete.valueOf()}"></slot>`);
@@ -1344,9 +1322,72 @@ export default class ZnDataTable extends ZincElement {
1344
1322
  }
1345
1323
  }
1346
1324
 
1325
+ if (this.hasSlotController.test(ActionSlots.actions.valueOf())) {
1326
+ actions.push(this.getActionsMenu());
1327
+ }
1328
+
1347
1329
  return actions;
1348
1330
  }
1349
1331
 
1332
+ private getActionsMenu() {
1333
+ const items = [...this.hasSlotController.getSlots(ActionSlots.actions.valueOf())];
1334
+ const disabled = this.selectedRows.length === 0;
1335
+
1336
+ return html`
1337
+ <zn-dropdown placement="bottom-end" ?disabled="${disabled}">
1338
+ <zn-button slot="trigger"
1339
+ icon-button="small"
1340
+ icon="ellipsis@lu"
1341
+ icon-size="18"
1342
+ tooltip="Actions"
1343
+ aria-label="Selection actions"
1344
+ ?disabled="${disabled}">
1345
+ </zn-button>
1346
+ <zn-menu variant="shell">
1347
+ ${items.map((item: HTMLElement) => {
1348
+ const trigger = this.actionTrigger(item);
1349
+ const icon = trigger?.getAttribute('icon') || item.getAttribute('icon');
1350
+ return html`
1351
+ <zn-menu-item color="${ifDefined(trigger?.getAttribute('color') || undefined)}"
1352
+ ?disabled="${trigger?.hasAttribute('disabled') ?? false}"
1353
+ @click="${() => this.runAction(item)}">
1354
+ ${icon ? html`
1355
+ <zn-icon src="${icon}" size="20" slot="prefix"></zn-icon>` : nothing}
1356
+ ${this.actionLabel(item, trigger)}
1357
+ </zn-menu-item>`;
1358
+ })}
1359
+ </zn-menu>
1360
+ </zn-dropdown>
1361
+ <div class="table__actions-source">
1362
+ <slot name="${ActionSlots.actions.valueOf()}"></slot>
1363
+ </div>`;
1364
+ }
1365
+
1366
+ private actionTrigger(item: HTMLElement): HTMLElement | null {
1367
+ if (item instanceof ZnConfirm) {
1368
+ return item.querySelector<HTMLElement>(':scope > [slot="trigger"]');
1369
+ }
1370
+ return item;
1371
+ }
1372
+
1373
+ private actionLabel(item: HTMLElement, trigger: HTMLElement | null): string {
1374
+ return trigger?.textContent?.trim()
1375
+ || trigger?.getAttribute('content')
1376
+ || trigger?.getAttribute('tooltip')
1377
+ || item.getAttribute('caption')
1378
+ || '';
1379
+ }
1380
+
1381
+ private runAction(item: HTMLElement) {
1382
+ if (item instanceof ZnConfirm) {
1383
+ item.show();
1384
+ return;
1385
+ }
1386
+ // ZnButton.click() skips the native click, so listeners and href would never fire
1387
+ const target = item instanceof ZnButton ? item.button : item;
1388
+ target?.click();
1389
+ }
1390
+
1350
1391
  goToPage(page: number) {
1351
1392
  this.page = page;
1352
1393
  this.selectedRows = [];
@@ -1380,12 +1421,12 @@ export default class ZnDataTable extends ZincElement {
1380
1421
  this._dataTask.run().then(r => r);
1381
1422
  }
1382
1423
 
1383
- selectAll(event: Event) {
1384
- const button = event.target as ZnButton;
1385
- if (button.disabled) return;
1424
+ /** Selects every row on the page, or clears the selection when every row is already selected. */
1425
+ selectAll() {
1426
+ if (!this.isSelectable()) return;
1386
1427
 
1387
1428
  if (this.numberOfRowsSelected === this._rows.length) {
1388
- this.clearSelectedRows(event);
1429
+ this.clearSelectedRows();
1389
1430
  return;
1390
1431
  }
1391
1432
 
@@ -1396,7 +1437,7 @@ export default class ZnDataTable extends ZincElement {
1396
1437
  }
1397
1438
 
1398
1439
  selectRow(e: Event) {
1399
- if (this.hideCheckboxes) {
1440
+ if (!this.isSelectable()) {
1400
1441
  return;
1401
1442
  }
1402
1443
 
@@ -1420,13 +1461,14 @@ export default class ZnDataTable extends ZincElement {
1420
1461
  return;
1421
1462
  }
1422
1463
 
1423
- const rows = Array.from(this.renderRoot.querySelectorAll('tbody tr.table__row--data'));
1424
- const index = rows.indexOf(parent as HTMLTableRowElement);
1425
- if (index === -1) return;
1426
-
1427
- const row = this._rows[index] as Row;
1464
+ const rowId = (parent as HTMLElement).dataset.rowId;
1465
+ const row = this._rows.find((r: Row) => r.id === rowId);
1428
1466
  if (!row) return;
1429
1467
 
1468
+ this.toggleRowSelection(row);
1469
+ }
1470
+
1471
+ private toggleRowSelection(row: Row) {
1430
1472
  const alreadySelected = this.selectedRows.some((r: Row) => r.id === row.id);
1431
1473
  if (alreadySelected) {
1432
1474
  this.selectedRows = this.selectedRows.filter((r: Row) => r.id !== row.id);
@@ -1439,10 +1481,7 @@ export default class ZnDataTable extends ZincElement {
1439
1481
  this.requestUpdate();
1440
1482
  }
1441
1483
 
1442
- clearSelectedRows(event: Event) {
1443
- const button = event.target as ZnButton;
1444
- if (button.disabled) return;
1445
-
1484
+ clearSelectedRows() {
1446
1485
  this.selectedRows = [];
1447
1486
  this.numberOfRowsSelected = 0;
1448
1487
  this.updateKeys();
@@ -1642,7 +1681,10 @@ export default class ZnDataTable extends ZincElement {
1642
1681
  'table__head--last': header.key === lastVisibleHeaderKey,
1643
1682
  'table__head--hidden': Object.values(this.hiddenHeaders).includes(header.key),
1644
1683
  })}"
1645
- @click="${sortable ? this.updateSort(header.key) : undefined}">
1684
+ tabindex="${ifDefined(sortable ? '0' : undefined)}"
1685
+ aria-sort="${ifDefined(sortable ? this.sortState(header.key) : undefined)}"
1686
+ @click="${sortable ? this.updateSort(header.key) : undefined}"
1687
+ @keydown="${sortable ? (e: KeyboardEvent) => this.handleSortKeyDown(e, header.key) : undefined}">
1646
1688
  <div>
1647
1689
  ${header.label}
1648
1690
  ${sortable ? this.getTableSortIcon(header.key) : nothing}
@@ -1651,6 +1693,17 @@ export default class ZnDataTable extends ZincElement {
1651
1693
  `;
1652
1694
  }
1653
1695
 
1696
+ private sortState(key: string): 'ascending' | 'descending' | 'none' {
1697
+ if (this.sortColumn !== key) return 'none';
1698
+ return this.sortDirection === 'asc' ? 'ascending' : 'descending';
1699
+ }
1700
+
1701
+ private handleSortKeyDown(e: KeyboardEvent, key: string) {
1702
+ if (e.key !== 'Enter' && e.key !== ' ') return;
1703
+ e.preventDefault();
1704
+ this.updateSort(key)();
1705
+ }
1706
+
1654
1707
  private renderCellBody(index: number, value: Cell, row: Row) {
1655
1708
  const filteredHeaders = this.visibleHeaders();
1656
1709
  const header: HeaderConfig | undefined = filteredHeaders[index];
@@ -1658,7 +1711,7 @@ export default class ZnDataTable extends ZincElement {
1658
1711
 
1659
1712
  return html`
1660
1713
  <td
1661
- @click="${this.hideCheckboxes ? undefined : this.selectRow}"
1714
+ @click="${this.isSelectable() ? this.selectRow : undefined}"
1662
1715
  class="${classMap({
1663
1716
  'table__cell': true,
1664
1717
  'table__cell--wide': headerKey === this.wideColumn,
@@ -1731,6 +1784,60 @@ export default class ZnDataTable extends ZincElement {
1731
1784
  return this.selectedRows.some((r: Row) => r.id === row.id);
1732
1785
  }
1733
1786
 
1787
+ // Drop selections for rows no longer in the result set (filtered out, archived, paged away)
1788
+ // and swap survivors for the fresh row objects so selected keys reflect current cell data.
1789
+ private pruneSelectedRows() {
1790
+ if (this.selectedRows.length === 0) return;
1791
+
1792
+ const current = new Map(this._rows.map((row: Row) => [row.id, row]));
1793
+ const kept = this.selectedRows
1794
+ .map((row: Row) => current.get(row.id))
1795
+ .filter((row): row is Row => row !== undefined);
1796
+
1797
+ const changed = kept.length !== this.selectedRows.length;
1798
+ this.selectedRows = kept;
1799
+ this.numberOfRowsSelected = kept.length;
1800
+ if (changed) this.updateKeys();
1801
+ }
1802
+
1803
+ private isSelectable(): boolean {
1804
+ return [ActionSlots.delete, ActionSlots.modify, ActionSlots.actions, ActionSlots.create]
1805
+ .some(slot => this.hasSlotController.test(slot.valueOf()));
1806
+ }
1807
+
1808
+ private renderSelectAllCheckbox() {
1809
+ const allSelected = this.numberOfRowsSelected === this._rows.length;
1810
+ const someSelected = this.numberOfRowsSelected > 0 && !allSelected;
1811
+ return html`
1812
+ <th class="table__head table__head--checkbox">
1813
+ <div>
1814
+ <zn-checkbox aria-label="Select all rows"
1815
+ .checked="${allSelected}"
1816
+ .indeterminate="${someSelected}"
1817
+ @zn-change="${() => this.selectAll()}">
1818
+ </zn-checkbox>
1819
+ </div>
1820
+ </th>`;
1821
+ }
1822
+
1823
+ private renderRowCheckbox(row: Row) {
1824
+ return html`
1825
+ <td class="table__cell table__cell--checkbox" @click="${(e: Event) => this.handleCheckboxCellClick(e, row)}">
1826
+ <div>
1827
+ <zn-checkbox aria-label="Select row"
1828
+ .checked="${this.isRowSelected(row)}"
1829
+ @zn-change="${() => this.toggleRowSelection(row)}">
1830
+ </zn-checkbox>
1831
+ </div>
1832
+ </td>`;
1833
+ }
1834
+
1835
+ // A click on the checkbox itself arrives twice (label, then input), so leave it to zn-change
1836
+ private handleCheckboxCellClick(e: Event, row: Row) {
1837
+ if ((e.target as Element).closest('zn-checkbox')) return;
1838
+ this.toggleRowSelection(row);
1839
+ }
1840
+
1734
1841
  private getRows(data: Response): Row[] {
1735
1842
  // Copy rows to avoid mutating original data
1736
1843
  const sourceRows = Array.isArray(data.rows) ? data.rows.slice() : [];
@@ -1761,21 +1868,9 @@ export default class ZnDataTable extends ZincElement {
1761
1868
  }
1762
1869
 
1763
1870
  private updateKeys() {
1764
- this.updateSelectAll();
1765
1871
  this.updateModifyKeys();
1766
1872
  this.updateDeleteKeys();
1767
- }
1768
-
1769
- private updateSelectAll() {
1770
- if (!this.selectAllButton) {
1771
- return;
1772
- }
1773
-
1774
- if (this.numberOfRowsSelected === this._rows.length) {
1775
- this.selectAllButton.icon = 'check_box';
1776
- } else {
1777
- this.selectAllButton.icon = 'indeterminate_check_box';
1778
- }
1873
+ this.updateActionKeys(ActionSlots.actions.valueOf());
1779
1874
  }
1780
1875
 
1781
1876
  private updateModifyKeys() {