@kubex/zinc 1.1.160 → 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.
Files changed (34) hide show
  1. package/dist/custom-elements.json +1086 -892
  2. package/dist/vscode.html-custom-data.json +95 -87
  3. package/dist/web-types.json +192 -181
  4. package/dist/zn.d.ts +32 -9
  5. package/dist/zn.min.css +1 -1
  6. package/dist/zn.min.js +367 -330
  7. package/docs/pages/components/data-table.md +30 -20
  8. package/package.json +1 -1
  9. package/scss/themes/_dark.scss +1 -0
  10. package/scss/themes/_light.scss +1 -0
  11. package/src/components/button/button.component.ts +4 -0
  12. package/src/components/button/button.scss +5 -0
  13. package/src/components/button/button.test.ts +10 -0
  14. package/src/components/checkbox/checkbox.component.ts +10 -6
  15. package/src/components/checkbox/checkbox.scss +4 -8
  16. package/src/components/content-block/content-block.component.ts +2 -3
  17. package/src/components/data-table/data-table.component.ts +171 -76
  18. package/src/components/data-table/data-table.scss +16 -60
  19. package/src/components/dropdown/dropdown.component.ts +6 -3
  20. package/src/components/flow-builder/modules/flow-node/flow-node.test.ts +2 -3
  21. package/src/components/flow-builder/modules/flow-step-group/flow-step-group.test.ts +14 -8
  22. package/src/components/form-actions/form-actions.test.ts +4 -1
  23. package/src/components/form-group/form-group.component.ts +45 -15
  24. package/src/components/navbar/navbar.test.ts +3 -2
  25. package/src/components/page/page.component.ts +1 -1
  26. package/src/components/page/page.test.ts +16 -16
  27. package/src/components/panel/panel.scss +23 -0
  28. package/src/components/popup/popup.component.ts +8 -1
  29. package/src/components/sp/sp.test.ts +2 -2
  30. package/src/components/theme-editor/theme-editor.component.ts +3 -2
  31. package/src/components/theme-editor/theme-editor.test.ts +30 -28
  32. package/src/components/tooltip/tooltip.component.ts +10 -1
  33. package/src/components/tooltip/tooltip.scss +5 -0
  34. package/src/components/translation-group/translation-group.test.ts +0 -2
@@ -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() {
@@ -44,7 +44,7 @@ table {
44
44
  border-bottom: 1px solid rgb(var(--zn-border-color)) !important;
45
45
  }
46
46
 
47
- &.with-hover tbody tr.table__row--data:not(.table__row--selected):hover td {
47
+ &.with-hover tbody tr.table__row--data:hover td {
48
48
  background-color: rgba(var(--zn-primary), 0.05) !important;
49
49
  cursor: pointer;
50
50
  }
@@ -414,68 +414,24 @@ zn-menu::part(base) {
414
414
  background-color: rgb(var(--zn-panel, 255, 255, 255));
415
415
  }
416
416
 
417
- /* Table Select Styles */
418
- table tbody tr.table__row--selected {
419
- background-color: var(--zn-color-purple-100);
420
-
421
- &:hover {
422
- background-color: var(--zn-color-purple-200) !important;
423
- }
424
-
425
- td {
426
- border-top: 1px solid var(--zn-color-purple-600) !important;
427
- border-bottom: 1px solid var(--zn-color-purple-600) !important;
428
- }
429
-
430
- td:first-of-type {
431
- border-left: 1px solid var(--zn-color-purple-600);
432
- }
433
-
434
- td:last-of-type {
435
- border-right: 1px solid var(--zn-color-purple-600);
436
- }
437
-
438
- &:last-of-type td:first-of-type {
439
- border-bottom-left-radius: var(--zn-border-radius);
440
- }
441
-
442
- &:last-of-type td:last-of-type {
443
- border-bottom-right-radius: var(--zn-border-radius);
444
- }
445
-
446
- & + tr.table__row--selected td {
447
- border-top: 0 !important;
448
-
449
- &:first-of-type {
450
- border-top-left-radius: 0;
451
- }
452
-
453
- &:last-of-type {
454
- border-top-right-radius: 0;
455
- }
456
- }
457
-
458
- &:has(+ tr.table__row--selected) td {
459
- border-bottom: 0 !important;
460
- }
461
-
462
- &:first-of-type td {
463
- border-top: 0 !important;
464
- }
465
-
466
- &:has(+ tr.table__row--selected) {
467
- td:first-of-type {
468
- border-bottom-left-radius: 0;
469
- }
417
+ .table tr .table__head[tabindex]:focus-visible {
418
+ outline: var(--zn-focus-ring);
419
+ outline-offset: calc(-1 * var(--zn-focus-ring-width));
420
+ }
470
421
 
471
- td:last-of-type {
472
- border-bottom-right-radius: 0;
473
- }
474
- }
422
+ .table tr .table__head--checkbox,
423
+ .table tr .table__cell--checkbox {
424
+ width: 1px;
425
+ min-width: 0;
426
+ padding-right: 0;
475
427
  }
476
428
 
477
- table:has(tbody tr:first-of-type.table__row--selected) thead tr th {
478
- border-bottom-color: var(--zn-color-purple-600) !important;
429
+ // Visually hidden rather than display: none, which would stop a slotted zn-confirm's dialog rendering
430
+ .table__actions-source {
431
+ position: absolute;
432
+ width: 0;
433
+ height: 0;
434
+ overflow: hidden;
479
435
  }
480
436
 
481
437
  tbody tr.table__row--group th {
@@ -182,7 +182,7 @@ export default class ZnDropdown extends ZincElement {
182
182
  }
183
183
 
184
184
  private async handleTriggerKeyDown(event: KeyboardEvent) {
185
- if ([' ', 'Enter', 'Tab'].includes(event.key)) {
185
+ if ([' ', 'Enter'].includes(event.key)) {
186
186
  event.preventDefault();
187
187
  await this.handleTriggerClick();
188
188
  return;
@@ -252,9 +252,12 @@ export default class ZnDropdown extends ZincElement {
252
252
 
253
253
  if (event.key === 'Tab') {
254
254
  if (this.open) {
255
- event.preventDefault();
256
255
  this.hide().then(r => r);
257
- this.focusOnTrigger();
256
+ // Tabbing out of the menu resumes from the trigger; from the trigger, Tab moves on as usual
257
+ if (this.panel?.assignedElements({flatten: true}).some(el => el.matches(':focus-within'))) {
258
+ event.preventDefault();
259
+ this.focusOnTrigger();
260
+ }
258
261
  return;
259
262
  }
260
263
 
@@ -19,7 +19,7 @@ describe('<zn-flow-node>', () => {
19
19
  expect(el).to.exist;
20
20
  });
21
21
 
22
- it('should render the type label and one port per output', async () => {
22
+ it('should render the type label and a single output stem', async () => {
23
23
  const el = await fixture<ZnFlowNode>(html`
24
24
  <zn-flow-node></zn-flow-node>`);
25
25
  el.node = node;
@@ -27,8 +27,7 @@ describe('<zn-flow-node>', () => {
27
27
  await el.updateComplete;
28
28
 
29
29
  expect(el.shadowRoot?.querySelector('.title')?.textContent).to.contain('Conditional Split');
30
- expect(el.shadowRoot?.querySelectorAll('.port--out')?.length).to.equal(2);
31
- expect(el.shadowRoot?.querySelector('.port-label')?.textContent).to.contain('TRUE');
30
+ expect(el.shadowRoot?.querySelectorAll('.port--out')?.length).to.equal(1);
32
31
  });
33
32
 
34
33
  it('should emit flow-node-grab when the card body is pressed', async () => {
@@ -1,20 +1,26 @@
1
1
  import '../../../../../dist/zn.min.js';
2
2
  import {expect, fixture, html} from '@open-wc/testing';
3
+ import type ZnCollapsible from '../../../collapsible/collapsible.component';
3
4
  import type ZnFlowStepGroup from './flow-step-group.component';
4
5
 
6
+ async function collapsibleOf(el: ZnFlowStepGroup): Promise<ZnCollapsible> {
7
+ const collapsible = el.shadowRoot!.querySelector<ZnCollapsible>('zn-collapsible')!;
8
+ await collapsible.updateComplete;
9
+ return collapsible;
10
+ }
11
+
5
12
  describe('<zn-flow-step-group>', () => {
6
- it('should render a component with its caption', async () => {
13
+ it('should render a collapsible with its caption', async () => {
7
14
  const el = await fixture<ZnFlowStepGroup>(html`
8
15
  <zn-flow-step-group caption="Contacts"></zn-flow-step-group>`);
9
- expect(el.shadowRoot?.querySelector('.header')?.textContent).to.contain('Contacts');
16
+ const collapsible = await collapsibleOf(el);
17
+ expect(collapsible.caption).to.equal('Contacts');
18
+ expect(collapsible.expanded).to.equal(true);
10
19
  });
11
20
 
12
- it('should toggle collapsed when the header is clicked', async () => {
21
+ it('should start closed when collapsed', async () => {
13
22
  const el = await fixture<ZnFlowStepGroup>(html`
14
- <zn-flow-step-group caption="Contacts"></zn-flow-step-group>`);
15
- const header = el.shadowRoot?.querySelector('.header') as HTMLElement;
16
- header.click();
17
- await el.updateComplete;
18
- expect(el.collapsed).to.equal(true);
23
+ <zn-flow-step-group caption="Contacts" collapsed></zn-flow-step-group>`);
24
+ expect((await collapsibleOf(el)).expanded).to.equal(false);
19
25
  });
20
26
  });
@@ -5,6 +5,9 @@ import { expect, fixture, html, oneEvent } from '@open-wc/testing';
5
5
  const clickButton = (button: HTMLElement) =>
6
6
  button.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true, cancelable: true }));
7
7
 
8
+ // The test harness loads no theme stylesheet, so a layout test stamps on the spacing token a themed page provides.
9
+ const spacingTokens = '--zn-spacing-small: 16px;';
10
+
8
11
  describe('<zn-form-actions>', () => {
9
12
  it('should render a component', async () => {
10
13
  const el = await fixture(html` <zn-form-actions></zn-form-actions> `);
@@ -24,7 +27,7 @@ describe('<zn-form-actions>', () => {
24
27
 
25
28
  it('holds align="start" children left and the rest right, one gap apart on each side', async () => {
26
29
  const el = await fixture<HTMLElement>(html`
27
- <zn-form-actions with-cancel style="width: 600px">
30
+ <zn-form-actions with-cancel style="width: 600px; ${spacingTokens}">
28
31
  <zn-button align="start" id="translate">Translate</zn-button>
29
32
  <zn-button align="start" id="slug">Slug</zn-button>
30
33
  </zn-form-actions>