@latty-ds/web 0.10.3 → 1.0.0
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/custom-elements.json +1970 -229
- package/dist/components/accordion/index.js +36 -2
- package/dist/components/alert/alert.d.ts +29 -6
- package/dist/components/alert/alert.types.d.ts +5 -0
- package/dist/components/alert/index.js +127 -20
- package/dist/components/avatar/avatar.d.ts +2 -0
- package/dist/components/avatar/index.js +10 -3
- package/dist/components/badge/index.js +1 -1
- package/dist/components/breadcrumb/breadcrumb.d.ts +4 -0
- package/dist/components/breadcrumb/breadcrumb.types.d.ts +5 -0
- package/dist/components/breadcrumb/index.js +20 -3
- package/dist/components/button/button.d.ts +33 -0
- package/dist/components/button/index.js +138 -15
- package/dist/components/calendar/calendar.d.ts +15 -7
- package/dist/components/calendar/calendar.types.d.ts +17 -1
- package/dist/components/calendar/index.js +80 -27
- package/dist/components/checkbox/checkbox.d.ts +14 -2
- package/dist/components/checkbox/checkbox.types.d.ts +5 -0
- package/dist/components/checkbox/index.js +82 -5
- package/dist/components/chip/chip.d.ts +4 -1
- package/dist/components/chip/chip.types.d.ts +5 -0
- package/dist/components/chip/index.js +29 -1
- package/dist/components/color-input/color-input.d.ts +11 -2
- package/dist/components/color-input/color-input.types.d.ts +5 -0
- package/dist/components/color-input/index.js +92 -8
- package/dist/components/combobox/combobox.d.ts +18 -2
- package/dist/components/combobox/combobox.types.d.ts +7 -0
- package/dist/components/combobox/index.js +164 -23
- package/dist/components/date-input/date-input.d.ts +22 -7
- package/dist/components/date-input/date-input.types.d.ts +7 -0
- package/dist/components/date-input/index.js +218 -55
- package/dist/components/datepicker/datepicker.d.ts +9 -1
- package/dist/components/datepicker/index.js +108 -15
- package/dist/components/dialog/dialog.d.ts +39 -19
- package/dist/components/dialog/dialog.types.d.ts +5 -0
- package/dist/components/dialog/index.js +240 -93
- package/dist/components/divider/index.js +2 -2
- package/dist/components/dropdown/dropdown-item.d.ts +1 -1
- package/dist/components/dropdown/dropdown.d.ts +4 -0
- package/dist/components/dropdown/index.js +43 -3
- package/dist/components/icon-button/icon-button.d.ts +4 -0
- package/dist/components/icon-button/index.js +39 -4
- package/dist/components/image/image.d.ts +7 -7
- package/dist/components/image/index.js +6 -11
- package/dist/components/list/index.js +14 -4
- package/dist/components/nav/index.js +37 -17
- package/dist/components/nav/nav-item.d.ts +8 -8
- package/dist/components/pagination/index.js +57 -16
- package/dist/components/pagination/pagination.d.ts +8 -1
- package/dist/components/pagination/pagination.types.d.ts +15 -0
- package/dist/components/progress/index.js +44 -6
- package/dist/components/progress/progress.d.ts +12 -0
- package/dist/components/radio/index.js +148 -9
- package/dist/components/radio/radio.d.ts +34 -2
- package/dist/components/radio/radio.types.d.ts +5 -0
- package/dist/components/radio-group/index.js +120 -29
- package/dist/components/radio-group/radio-group.d.ts +27 -2
- package/dist/components/select/index.js +197 -44
- package/dist/components/select/select.d.ts +38 -4
- package/dist/components/select/select.types.d.ts +5 -0
- package/dist/components/shared/aria-forwarding.d.ts +1 -1
- package/dist/components/shared/form-default.d.ts +30 -0
- package/dist/components/shared/form-disabled.d.ts +40 -0
- package/dist/components/shared/form-field.d.ts +7 -0
- package/dist/components/shared/form-validity.d.ts +17 -0
- package/dist/components/shared/roving.d.ts +11 -0
- package/dist/components/shared/scroll-lock.d.ts +2 -0
- package/dist/components/sidepanel/index.js +278 -75
- package/dist/components/sidepanel/sidepanel.d.ts +32 -10
- package/dist/components/sidepanel/sidepanel.types.d.ts +10 -1
- package/dist/components/skeleton/index.js +3 -8
- package/dist/components/slider/index.js +307 -75
- package/dist/components/slider/slider.d.ts +16 -3
- package/dist/components/snackbar/index.js +82 -9
- package/dist/components/snackbar/snackbar.d.ts +17 -1
- package/dist/components/snackbar/snackbar.types.d.ts +5 -0
- package/dist/components/surface/index.js +8 -0
- package/dist/components/switch/index.js +101 -5
- package/dist/components/switch/switch.d.ts +13 -2
- package/dist/components/switch/switch.types.d.ts +5 -0
- package/dist/components/tab/index.js +15 -14
- package/dist/components/tab/tab.d.ts +0 -1
- package/dist/components/tab-group/index.js +102 -30
- package/dist/components/tab-group/tab-group.d.ts +17 -2
- package/dist/components/table/index.js +105 -61
- package/dist/components/table/table.d.ts +8 -2
- package/dist/components/table/table.types.d.ts +4 -3
- package/dist/components/text/index.js +16 -1
- package/dist/components/text/text.d.ts +1 -1
- package/dist/components/textfield/index.js +243 -35
- package/dist/components/textfield/textfield.d.ts +24 -2
- package/dist/components/textfield/textfield.types.d.ts +11 -0
- package/dist/components/tooltip/index.js +176 -49
- package/dist/components/tooltip/tooltip.d.ts +17 -0
- package/dist/components/tooltip/tooltip.types.d.ts +5 -1
- package/dist/index.cjs +2467 -692
- package/dist/index.js +2478 -699
- package/dist/manifest.json +202 -32
- package/dist/utils/dev-warn.d.ts +5 -0
- package/dist/utils/index.d.ts +1 -0
- package/package.json +4 -8
|
@@ -9,6 +9,11 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
9
9
|
return result;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// src/components/shared/roving.ts
|
|
13
|
+
var enabledItems = (items) => items.filter((item) => !item.disabled);
|
|
14
|
+
var rovingTargetOf = (enabled, value) => enabled.find((item) => item.value === value) ?? enabled[0];
|
|
15
|
+
var stepIndex = (index, length, delta) => (Math.max(index, 0) + delta + length) % length;
|
|
16
|
+
|
|
12
17
|
// src/components/tab-group/tab-group.ts
|
|
13
18
|
import { html as html3 } from "lit";
|
|
14
19
|
|
|
@@ -142,7 +147,7 @@ var tabStyles = css2`
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
:host([disabled]) .tab {
|
|
145
|
-
color: var(--lt-
|
|
150
|
+
color: var(--lt-text-disabled);
|
|
146
151
|
cursor: not-allowed;
|
|
147
152
|
opacity: 0.5;
|
|
148
153
|
}
|
|
@@ -185,6 +190,17 @@ var tabStyles = css2`
|
|
|
185
190
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
|
186
191
|
color: var(--lt-text-primary);
|
|
187
192
|
}
|
|
193
|
+
|
|
194
|
+
/* ── Reduced motion ─────────────────────────────────────────────────────── */
|
|
195
|
+
|
|
196
|
+
@media (prefers-reduced-motion: reduce) {
|
|
197
|
+
/* :host([data-variant='pills']) .tab must be listed explicitly, not just .tab — it outranks a bare .tab in
|
|
198
|
+
specificity (an extra :host() attribute selector), so it would otherwise keep its own transition. */
|
|
199
|
+
.tab,
|
|
200
|
+
:host([data-variant='pills']) .tab {
|
|
201
|
+
transition: none;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
188
204
|
`;
|
|
189
205
|
|
|
190
206
|
// src/components/tab/tab.ts
|
|
@@ -198,21 +214,13 @@ var Tab = class extends ThemeableElement {
|
|
|
198
214
|
this.active = false;
|
|
199
215
|
this.disabled = false;
|
|
200
216
|
this.size = "md";
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.dispatchEvent(
|
|
204
|
-
new CustomEvent("tab-click", {
|
|
205
|
-
detail: { value: this.value },
|
|
206
|
-
bubbles: true,
|
|
207
|
-
composed: true
|
|
208
|
-
})
|
|
209
|
-
);
|
|
210
|
-
};
|
|
217
|
+
// Enter/Space activate the tab exactly like a mouse click: a native `click` on the host, which
|
|
218
|
+
// `lt-tab-group` (or any consumer) listens for. There is no separate tab event.
|
|
211
219
|
this._handleHostKeydown = (e) => {
|
|
212
220
|
if (this.disabled) return;
|
|
213
221
|
if (e.key === "Enter" || e.key === " ") {
|
|
214
222
|
e.preventDefault();
|
|
215
|
-
this.
|
|
223
|
+
this.click();
|
|
216
224
|
}
|
|
217
225
|
};
|
|
218
226
|
}
|
|
@@ -220,12 +228,10 @@ var Tab = class extends ThemeableElement {
|
|
|
220
228
|
super.connectedCallback();
|
|
221
229
|
this.setAttribute("role", "tab");
|
|
222
230
|
this.tabIndex = 0;
|
|
223
|
-
this.addEventListener("click", this._handleHostClick);
|
|
224
231
|
this.addEventListener("keydown", this._handleHostKeydown);
|
|
225
232
|
}
|
|
226
233
|
disconnectedCallback() {
|
|
227
234
|
super.disconnectedCallback();
|
|
228
|
-
this.removeEventListener("click", this._handleHostClick);
|
|
229
235
|
this.removeEventListener("keydown", this._handleHostKeydown);
|
|
230
236
|
}
|
|
231
237
|
updated(changedProperties) {
|
|
@@ -308,31 +314,64 @@ TabPanel = __decorateClass([
|
|
|
308
314
|
], TabPanel);
|
|
309
315
|
|
|
310
316
|
// src/components/tab-group/tab-group.ts
|
|
317
|
+
var groupCount = 0;
|
|
311
318
|
var TabGroup = class extends ThemeableElement {
|
|
312
319
|
constructor() {
|
|
313
320
|
super(...arguments);
|
|
314
321
|
this.value = "";
|
|
315
322
|
this.appearance = "default";
|
|
316
323
|
this.size = "md";
|
|
324
|
+
// Scopes generated tab/panel ids to this instance, so two groups on the same page whose tabs happen to share a
|
|
325
|
+
// `value` (e.g. two groups each with a "settings" tab) don't collide.
|
|
326
|
+
this._uid = `lt-tab-group-${groupCount++}`;
|
|
327
|
+
// Reacts to a slotted tab's `disabled` attribute changing after the fact (e.g. a consumer disabling a tab
|
|
328
|
+
// dynamically, with no accompanying change to this group's own value/size/appearance), so the roving tabindex
|
|
329
|
+
// and keyboard navigation immediately stop landing on it.
|
|
330
|
+
this._tabObserver = new MutationObserver(() => this.updateTabs());
|
|
317
331
|
/**
|
|
318
332
|
* Handles click events from tabs.
|
|
319
333
|
* @private
|
|
320
334
|
*/
|
|
321
335
|
this.handleTabClick = (e) => {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
);
|
|
336
|
+
this._select(e.currentTarget);
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* ArrowLeft/Right move the roving tab stop (and, per the automatic-activation model, select the tab); Home/End
|
|
340
|
+
* jump to the first/last enabled tab. Disabled tabs are skipped entirely, matching the radiogroup/listbox
|
|
341
|
+
* patterns elsewhere in the DS.
|
|
342
|
+
* @private
|
|
343
|
+
*/
|
|
344
|
+
this.handleTabKeydown = (e) => {
|
|
345
|
+
const enabled = enabledItems(this.tabs ?? []);
|
|
346
|
+
if (enabled.length === 0) return;
|
|
347
|
+
const index = enabled.indexOf(e.currentTarget);
|
|
348
|
+
let next;
|
|
349
|
+
switch (e.key) {
|
|
350
|
+
case "ArrowRight":
|
|
351
|
+
next = enabled[stepIndex(index, enabled.length, 1)];
|
|
352
|
+
break;
|
|
353
|
+
case "ArrowLeft":
|
|
354
|
+
next = enabled[stepIndex(index, enabled.length, -1)];
|
|
355
|
+
break;
|
|
356
|
+
case "Home":
|
|
357
|
+
next = enabled[0];
|
|
358
|
+
break;
|
|
359
|
+
case "End":
|
|
360
|
+
next = enabled[enabled.length - 1];
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
if (!next) return;
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
this._select(next);
|
|
366
|
+
next.focus();
|
|
334
367
|
};
|
|
335
368
|
}
|
|
369
|
+
connectedCallback() {
|
|
370
|
+
super.connectedCallback();
|
|
371
|
+
this._tabObserver.observe(this, { attributes: true, attributeFilter: ["disabled"], subtree: true });
|
|
372
|
+
this.updateTabs();
|
|
373
|
+
this.updatePanels();
|
|
374
|
+
}
|
|
336
375
|
/**
|
|
337
376
|
* Lifecycle: Called after the element's DOM has been updated.
|
|
338
377
|
*/
|
|
@@ -341,24 +380,29 @@ var TabGroup = class extends ThemeableElement {
|
|
|
341
380
|
this.updatePanels();
|
|
342
381
|
}
|
|
343
382
|
/**
|
|
344
|
-
* Updates all tabs with the correct active state and
|
|
383
|
+
* Updates all tabs with the correct active/roving-tabindex state, size, and ARIA wiring.
|
|
345
384
|
* @private
|
|
346
385
|
*/
|
|
347
386
|
updateTabs() {
|
|
348
387
|
if (!this.tabs) return;
|
|
388
|
+
const enabled = enabledItems(this.tabs);
|
|
389
|
+
const rovingTarget = rovingTargetOf(enabled, this.value);
|
|
349
390
|
this.tabs.forEach((tab) => {
|
|
350
391
|
tab.size = this.size;
|
|
351
392
|
tab.active = tab.value === this.value;
|
|
393
|
+
tab.tabIndex = tab === rovingTarget ? 0 : -1;
|
|
394
|
+
if (!tab.id) tab.id = `${this._uid}-tab-${tab.value}`;
|
|
352
395
|
if (this.appearance === "pills") {
|
|
353
396
|
tab.setAttribute("data-variant", "pills");
|
|
354
397
|
} else {
|
|
355
398
|
tab.removeAttribute("data-variant");
|
|
356
399
|
}
|
|
357
|
-
tab.addEventListener("
|
|
400
|
+
tab.addEventListener("click", this.handleTabClick);
|
|
401
|
+
tab.addEventListener("keydown", this.handleTabKeydown);
|
|
358
402
|
});
|
|
359
403
|
}
|
|
360
404
|
/**
|
|
361
|
-
* Updates panel visibility based on selected value.
|
|
405
|
+
* Updates panel visibility and ARIA wiring based on selected value.
|
|
362
406
|
* @private
|
|
363
407
|
*/
|
|
364
408
|
updatePanels() {
|
|
@@ -370,8 +414,34 @@ var TabGroup = class extends ThemeableElement {
|
|
|
370
414
|
} else {
|
|
371
415
|
panel.removeAttribute("active");
|
|
372
416
|
}
|
|
417
|
+
if (!panel.id) panel.id = `${this._uid}-panel-${panelValue}`;
|
|
418
|
+
panel.setAttribute("role", "tabpanel");
|
|
419
|
+
panel.setAttribute("aria-labelledby", `${this._uid}-tab-${panelValue}`);
|
|
420
|
+
if (!panel.hasAttribute("tabindex")) panel.setAttribute("tabindex", "0");
|
|
421
|
+
});
|
|
422
|
+
this.tabs?.forEach((tab) => {
|
|
423
|
+
const panel = this.panels.find((p) => (p.getAttribute("value") ?? p.getAttribute("data-value")) === tab.value);
|
|
424
|
+
if (panel) tab.setAttribute("aria-controls", panel.id);
|
|
425
|
+
else tab.removeAttribute("aria-controls");
|
|
373
426
|
});
|
|
374
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Selects a tab (used by both the click and keyboard-navigation handlers below).
|
|
430
|
+
* @private
|
|
431
|
+
*/
|
|
432
|
+
_select(tab) {
|
|
433
|
+
if (tab.disabled || tab.value === this.value) return;
|
|
434
|
+
this.value = tab.value;
|
|
435
|
+
this.updateTabs();
|
|
436
|
+
this.updatePanels();
|
|
437
|
+
this.dispatchEvent(
|
|
438
|
+
new CustomEvent("change", {
|
|
439
|
+
detail: { value: this.value },
|
|
440
|
+
bubbles: true,
|
|
441
|
+
composed: true
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
}
|
|
375
445
|
/**
|
|
376
446
|
* Handles slot changes.
|
|
377
447
|
* @private
|
|
@@ -385,9 +455,11 @@ var TabGroup = class extends ThemeableElement {
|
|
|
385
455
|
*/
|
|
386
456
|
disconnectedCallback() {
|
|
387
457
|
super.disconnectedCallback();
|
|
458
|
+
this._tabObserver.disconnect();
|
|
388
459
|
if (this.tabs) {
|
|
389
460
|
this.tabs.forEach((tab) => {
|
|
390
|
-
tab.removeEventListener("
|
|
461
|
+
tab.removeEventListener("click", this.handleTabClick);
|
|
462
|
+
tab.removeEventListener("keydown", this.handleTabKeydown);
|
|
391
463
|
});
|
|
392
464
|
}
|
|
393
465
|
}
|
|
@@ -63,25 +63,40 @@ export declare class TabGroup extends ThemeableElement {
|
|
|
63
63
|
* @private
|
|
64
64
|
*/
|
|
65
65
|
private panels;
|
|
66
|
+
private readonly _uid;
|
|
67
|
+
private readonly _tabObserver;
|
|
68
|
+
connectedCallback(): void;
|
|
66
69
|
/**
|
|
67
70
|
* Lifecycle: Called after the element's DOM has been updated.
|
|
68
71
|
*/
|
|
69
72
|
firstUpdated(): void;
|
|
70
73
|
/**
|
|
71
|
-
* Updates all tabs with the correct active state and
|
|
74
|
+
* Updates all tabs with the correct active/roving-tabindex state, size, and ARIA wiring.
|
|
72
75
|
* @private
|
|
73
76
|
*/
|
|
74
77
|
private updateTabs;
|
|
75
78
|
/**
|
|
76
|
-
* Updates panel visibility based on selected value.
|
|
79
|
+
* Updates panel visibility and ARIA wiring based on selected value.
|
|
77
80
|
* @private
|
|
78
81
|
*/
|
|
79
82
|
private updatePanels;
|
|
83
|
+
/**
|
|
84
|
+
* Selects a tab (used by both the click and keyboard-navigation handlers below).
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
private _select;
|
|
80
88
|
/**
|
|
81
89
|
* Handles click events from tabs.
|
|
82
90
|
* @private
|
|
83
91
|
*/
|
|
84
92
|
private handleTabClick;
|
|
93
|
+
/**
|
|
94
|
+
* ArrowLeft/Right move the roving tab stop (and, per the automatic-activation model, select the tab); Home/End
|
|
95
|
+
* jump to the first/last enabled tab. Disabled tabs are skipped entirely, matching the radiogroup/listbox
|
|
96
|
+
* patterns elsewhere in the DS.
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
private handleTabKeydown;
|
|
85
100
|
/**
|
|
86
101
|
* Handles slot changes.
|
|
87
102
|
* @private
|
|
@@ -51,6 +51,8 @@ var tableStyles = css`
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
.table-container {
|
|
54
|
+
/* The loading overlay is absolutely positioned, so it needs this as its containing block. */
|
|
55
|
+
position: relative;
|
|
54
56
|
overflow-x: auto;
|
|
55
57
|
width: 100%;
|
|
56
58
|
}
|
|
@@ -73,38 +75,46 @@ var tableStyles = css`
|
|
|
73
75
|
th {
|
|
74
76
|
color: var(--lt-text-default);
|
|
75
77
|
font-weight: 600;
|
|
76
|
-
text-align:
|
|
78
|
+
text-align: start;
|
|
77
79
|
white-space: nowrap;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
th[data-align='center'] {
|
|
81
83
|
text-align: center;
|
|
82
84
|
}
|
|
83
|
-
th[data-align='
|
|
84
|
-
text-align:
|
|
85
|
+
th[data-align='end'] {
|
|
86
|
+
text-align: end;
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/* Sortable headers */
|
|
88
90
|
th.sortable {
|
|
89
91
|
cursor: pointer;
|
|
92
|
+
padding: 0;
|
|
90
93
|
user-select: none;
|
|
91
|
-
transition: background-color 120ms ease;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
th.sortable:hover {
|
|
95
97
|
background: var(--lt-bg-overlay);
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
/* The button (not the th) carries the density padding, so it — not just a slice of it — is the click target. */
|
|
98
101
|
th.sortable .header-content {
|
|
99
102
|
align-items: center;
|
|
103
|
+
background: none;
|
|
104
|
+
border: none;
|
|
105
|
+
color: inherit;
|
|
106
|
+
cursor: pointer;
|
|
100
107
|
display: flex;
|
|
108
|
+
font: inherit;
|
|
101
109
|
gap: var(--lt-spacing-2);
|
|
110
|
+
transition: background-color 120ms ease;
|
|
111
|
+
width: 100%;
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
th[data-align='center'] .header-content {
|
|
105
115
|
justify-content: center;
|
|
106
116
|
}
|
|
107
|
-
th[data-align='
|
|
117
|
+
th[data-align='end'] .header-content {
|
|
108
118
|
justify-content: flex-end;
|
|
109
119
|
}
|
|
110
120
|
|
|
@@ -155,8 +165,8 @@ var tableStyles = css`
|
|
|
155
165
|
td[data-align='center'] {
|
|
156
166
|
text-align: center;
|
|
157
167
|
}
|
|
158
|
-
td[data-align='
|
|
159
|
-
text-align:
|
|
168
|
+
td[data-align='end'] {
|
|
169
|
+
text-align: end;
|
|
160
170
|
}
|
|
161
171
|
|
|
162
172
|
/* Density variants */
|
|
@@ -175,11 +185,30 @@ var tableStyles = css`
|
|
|
175
185
|
padding: var(--lt-spacing-4) var(--lt-spacing-5);
|
|
176
186
|
}
|
|
177
187
|
|
|
188
|
+
/* A sortable th's own padding moves onto its button (below), so the whole visible cell — not just the area
|
|
189
|
+
inside the th's padding — is the click target. Matches the density rules above in specificity and comes
|
|
190
|
+
after them in source order, so it wins the cascade regardless of selector order elsewhere in this file. */
|
|
191
|
+
:host th.sortable {
|
|
192
|
+
padding: 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
:host([density='compact']) th.sortable .header-content {
|
|
196
|
+
padding: var(--lt-spacing-2) var(--lt-spacing-3);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
:host([density='normal']) th.sortable .header-content {
|
|
200
|
+
padding: var(--lt-spacing-3) var(--lt-spacing-4);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
:host([density='comfortable']) th.sortable .header-content {
|
|
204
|
+
padding: var(--lt-spacing-4) var(--lt-spacing-5);
|
|
205
|
+
}
|
|
206
|
+
|
|
178
207
|
/* Sticky columns */
|
|
179
208
|
th.sticky {
|
|
180
209
|
background: var(--lt-bg-surface);
|
|
181
210
|
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
|
|
182
|
-
|
|
211
|
+
inset-inline-start: 0;
|
|
183
212
|
position: sticky;
|
|
184
213
|
z-index: 2;
|
|
185
214
|
}
|
|
@@ -187,7 +216,7 @@ var tableStyles = css`
|
|
|
187
216
|
td.sticky {
|
|
188
217
|
background: var(--lt-bg-default);
|
|
189
218
|
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
|
|
190
|
-
|
|
219
|
+
inset-inline-start: 0;
|
|
191
220
|
position: sticky;
|
|
192
221
|
z-index: 1;
|
|
193
222
|
}
|
|
@@ -211,12 +240,12 @@ var tableStyles = css`
|
|
|
211
240
|
|
|
212
241
|
:host([bordered]) th,
|
|
213
242
|
:host([bordered]) td {
|
|
214
|
-
border-
|
|
243
|
+
border-inline-end: 1px solid var(--lt-border-default);
|
|
215
244
|
}
|
|
216
245
|
|
|
217
246
|
:host([bordered]) th:last-child,
|
|
218
247
|
:host([bordered]) td:last-child {
|
|
219
|
-
border-
|
|
248
|
+
border-inline-end: none;
|
|
220
249
|
}
|
|
221
250
|
|
|
222
251
|
/* Empty state */
|
|
@@ -229,11 +258,11 @@ var tableStyles = css`
|
|
|
229
258
|
/* Loading state */
|
|
230
259
|
.loading-overlay {
|
|
231
260
|
align-items: center;
|
|
232
|
-
background:
|
|
261
|
+
background: color-mix(in srgb, var(--lt-bg-default) 80%, transparent);
|
|
233
262
|
display: flex;
|
|
234
263
|
height: 100%;
|
|
235
264
|
justify-content: center;
|
|
236
|
-
|
|
265
|
+
inset-inline-start: 0;
|
|
237
266
|
position: absolute;
|
|
238
267
|
top: 0;
|
|
239
268
|
width: 100%;
|
|
@@ -273,7 +302,7 @@ var tableStyles = css`
|
|
|
273
302
|
gap: var(--lt-spacing-2);
|
|
274
303
|
grid-template-columns: 120px 1fr;
|
|
275
304
|
padding: var(--lt-spacing-2) 0;
|
|
276
|
-
text-align:
|
|
305
|
+
text-align: start;
|
|
277
306
|
}
|
|
278
307
|
|
|
279
308
|
:host([responsive-mode='stack']) td::before {
|
|
@@ -293,14 +322,30 @@ var tableStyles = css`
|
|
|
293
322
|
}
|
|
294
323
|
}
|
|
295
324
|
|
|
296
|
-
th.sortable:focus-visible {
|
|
325
|
+
th.sortable .header-content:focus-visible {
|
|
297
326
|
outline: 2px solid var(--lt-interactive-primary-bg);
|
|
298
327
|
outline-offset: -2px;
|
|
299
328
|
}
|
|
329
|
+
|
|
330
|
+
/* ── Reduced motion ─────────────────────────────────────────────────────── */
|
|
331
|
+
|
|
332
|
+
@media (prefers-reduced-motion: reduce) {
|
|
333
|
+
th.sortable .header-content,
|
|
334
|
+
.sort-icon,
|
|
335
|
+
tbody tr {
|
|
336
|
+
transition: none;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
300
339
|
`;
|
|
301
340
|
|
|
302
341
|
// src/components/table/table.ts
|
|
303
342
|
import "@latty-ds/icons";
|
|
343
|
+
var compareValues = (a, b) => {
|
|
344
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
|
|
345
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
346
|
+
if (typeof a === "bigint" && typeof b === "bigint") return a < b ? -1 : a > b ? 1 : 0;
|
|
347
|
+
return String(a).localeCompare(String(b), void 0, { numeric: true });
|
|
348
|
+
};
|
|
304
349
|
var jsonArrayConverter = {
|
|
305
350
|
fromAttribute(value) {
|
|
306
351
|
if (value === null) return [];
|
|
@@ -324,6 +369,7 @@ var Table = class extends ThemeableElement {
|
|
|
324
369
|
this.bordered = false;
|
|
325
370
|
this.loading = false;
|
|
326
371
|
this.emptyMessage = "No data available";
|
|
372
|
+
this.label = "";
|
|
327
373
|
this.rowKey = "";
|
|
328
374
|
this.internalSort = null;
|
|
329
375
|
this.sortedData = [];
|
|
@@ -365,12 +411,7 @@ var Table = class extends ThemeableElement {
|
|
|
365
411
|
if (aValue == null && bValue == null) return 0;
|
|
366
412
|
if (aValue == null) return 1;
|
|
367
413
|
if (bValue == null) return -1;
|
|
368
|
-
|
|
369
|
-
if (typeof aValue === "number" && typeof bValue === "number") {
|
|
370
|
-
comparison = aValue - bValue;
|
|
371
|
-
} else {
|
|
372
|
-
comparison = String(aValue).localeCompare(String(bValue));
|
|
373
|
-
}
|
|
414
|
+
const comparison = compareValues(aValue, bValue);
|
|
374
415
|
return this.currentSort.direction === "asc" ? comparison : -comparison;
|
|
375
416
|
});
|
|
376
417
|
this.sortedData = sorted;
|
|
@@ -393,7 +434,7 @@ var Table = class extends ThemeableElement {
|
|
|
393
434
|
this.internalSort = newSort;
|
|
394
435
|
}
|
|
395
436
|
this.dispatchEvent(
|
|
396
|
-
new CustomEvent("
|
|
437
|
+
new CustomEvent("sortchange", {
|
|
397
438
|
bubbles: true,
|
|
398
439
|
composed: true,
|
|
399
440
|
detail: {
|
|
@@ -446,15 +487,7 @@ var Table = class extends ThemeableElement {
|
|
|
446
487
|
sticky: !!column.sticky,
|
|
447
488
|
"hide-on-mobile": !!column.hideOnMobile
|
|
448
489
|
})}
|
|
449
|
-
data-align=${column.align || "
|
|
450
|
-
@click=${() => this.handleHeaderClick(column)}
|
|
451
|
-
@keydown=${(e) => {
|
|
452
|
-
if (column.sortable && (e.key === "Enter" || e.key === " ")) {
|
|
453
|
-
e.preventDefault();
|
|
454
|
-
this.handleHeaderClick(column);
|
|
455
|
-
}
|
|
456
|
-
}}
|
|
457
|
-
tabindex=${column.sortable ? "0" : "-1"}
|
|
490
|
+
data-align=${column.align || "start"}
|
|
458
491
|
aria-sort=${column.sortable ? this.currentSort?.key === column.key ? this.currentSort.direction === "asc" ? "ascending" : "descending" : "none" : nothing}
|
|
459
492
|
style=${styleMap({
|
|
460
493
|
...column.width ? { width: column.width } : {},
|
|
@@ -462,10 +495,20 @@ var Table = class extends ThemeableElement {
|
|
|
462
495
|
})}
|
|
463
496
|
>
|
|
464
497
|
${column.sortable ? html`
|
|
465
|
-
<
|
|
498
|
+
<button
|
|
499
|
+
type="button"
|
|
500
|
+
class="header-content"
|
|
501
|
+
@click=${() => this.handleHeaderClick(column)}
|
|
502
|
+
@keydown=${(e) => {
|
|
503
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
504
|
+
e.preventDefault();
|
|
505
|
+
this.handleHeaderClick(column);
|
|
506
|
+
}
|
|
507
|
+
}}
|
|
508
|
+
>
|
|
466
509
|
<span>${column.label}</span>
|
|
467
510
|
${this.renderSortIcon(column)}
|
|
468
|
-
</
|
|
511
|
+
</button>
|
|
469
512
|
` : column.label}
|
|
470
513
|
</th>
|
|
471
514
|
`
|
|
@@ -483,46 +526,44 @@ var Table = class extends ThemeableElement {
|
|
|
483
526
|
return html`
|
|
484
527
|
<tbody>
|
|
485
528
|
<tr>
|
|
486
|
-
<td colspan=${columns.length} class="empty-state">${this.emptyMessage}</td>
|
|
529
|
+
<td colspan=${Math.max(columns.length, 1)} class="empty-state">${this.emptyMessage}</td>
|
|
487
530
|
</tr>
|
|
488
531
|
</tbody>
|
|
489
532
|
`;
|
|
490
533
|
}
|
|
491
|
-
|
|
492
|
-
<
|
|
493
|
-
${
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
...column.minWidth ? { minWidth: column.minWidth } : {}
|
|
510
|
-
})}
|
|
511
|
-
>
|
|
512
|
-
${this.renderCell(column, row, index)}
|
|
513
|
-
</td>
|
|
514
|
-
`
|
|
515
|
-
)}
|
|
516
|
-
</tr>
|
|
534
|
+
const renderRow = (row, index) => html`
|
|
535
|
+
<tr>
|
|
536
|
+
${columns.map(
|
|
537
|
+
(column) => html`
|
|
538
|
+
<td
|
|
539
|
+
class=${classMap({
|
|
540
|
+
sticky: !!column.sticky,
|
|
541
|
+
"hide-on-mobile": !!column.hideOnMobile
|
|
542
|
+
})}
|
|
543
|
+
data-align=${column.align || "start"}
|
|
544
|
+
data-label=${column.label}
|
|
545
|
+
style=${styleMap({
|
|
546
|
+
...column.width ? { width: column.width } : {},
|
|
547
|
+
...column.minWidth ? { minWidth: column.minWidth } : {}
|
|
548
|
+
})}
|
|
549
|
+
>
|
|
550
|
+
${this.renderCell(column, row, index)}
|
|
551
|
+
</td>
|
|
517
552
|
`
|
|
518
553
|
)}
|
|
554
|
+
</tr>
|
|
555
|
+
`;
|
|
556
|
+
const rows = this.rowKey ? repeat(this.sortedData, (row) => row[this.rowKey], renderRow) : this.sortedData.map(renderRow);
|
|
557
|
+
return html`
|
|
558
|
+
<tbody>
|
|
559
|
+
${rows}
|
|
519
560
|
</tbody>
|
|
520
561
|
`;
|
|
521
562
|
}
|
|
522
563
|
render() {
|
|
523
564
|
return html`
|
|
524
565
|
<div class="table-container">
|
|
525
|
-
<table>
|
|
566
|
+
<table aria-label=${this.label || nothing}>
|
|
526
567
|
${this.renderHeader()} ${this.renderBody()}
|
|
527
568
|
</table>
|
|
528
569
|
${this.loading ? html`
|
|
@@ -562,6 +603,9 @@ __decorateClass([
|
|
|
562
603
|
__decorateClass([
|
|
563
604
|
property2({ attribute: "empty-message" })
|
|
564
605
|
], Table.prototype, "emptyMessage", 2);
|
|
606
|
+
__decorateClass([
|
|
607
|
+
property2({ reflect: true })
|
|
608
|
+
], Table.prototype, "label", 2);
|
|
565
609
|
__decorateClass([
|
|
566
610
|
property2({ attribute: "row-key" })
|
|
567
611
|
], Table.prototype, "rowKey", 2);
|
|
@@ -19,14 +19,14 @@ import '@latty-ds/icons';
|
|
|
19
19
|
* - Hide columns on mobile
|
|
20
20
|
* - Accessible with keyboard navigation
|
|
21
21
|
*
|
|
22
|
-
* @fires
|
|
22
|
+
* @fires sortchange - Fired when sort changes
|
|
23
23
|
*
|
|
24
24
|
* @example
|
|
25
25
|
* ```typescript
|
|
26
26
|
* const columns = [
|
|
27
27
|
* { key: 'name', label: 'Name', sortable: true },
|
|
28
28
|
* { key: 'email', label: 'Email', sortable: true },
|
|
29
|
-
* { key: 'age', label: 'Age', sortable: true, align: '
|
|
29
|
+
* { key: 'age', label: 'Age', sortable: true, align: 'end' }
|
|
30
30
|
* ];
|
|
31
31
|
*
|
|
32
32
|
* const data = [
|
|
@@ -104,6 +104,12 @@ export declare class Table<T = Record<string, unknown>> extends ThemeableElement
|
|
|
104
104
|
* @default 'No data available'
|
|
105
105
|
*/
|
|
106
106
|
emptyMessage: string;
|
|
107
|
+
/**
|
|
108
|
+
* Accessible name for the table. Required when a page has more than one table, since column labels alone
|
|
109
|
+
* don't tell assistive tech which table it's in.
|
|
110
|
+
* @default ''
|
|
111
|
+
*/
|
|
112
|
+
label: string;
|
|
107
113
|
/**
|
|
108
114
|
* Field name to use as a stable row identity key for efficient DOM reuse during sorts/updates.
|
|
109
115
|
* When omitted, falls back to the row's JSON representation.
|
|
@@ -18,9 +18,10 @@ export type TableDensity = 'compact' | 'normal' | 'comfortable';
|
|
|
18
18
|
*/
|
|
19
19
|
export type SortDirection = 'asc' | 'desc' | null;
|
|
20
20
|
/**
|
|
21
|
-
* Alignment for cell content.
|
|
21
|
+
* Alignment for cell content. Logical, not physical — `'end'` means "the trailing edge of the reading
|
|
22
|
+
* direction" (right in LTR, left in RTL), matching CSS `text-align: end` rather than `text-align: right`.
|
|
22
23
|
*/
|
|
23
|
-
export type CellAlign = '
|
|
24
|
+
export type CellAlign = 'start' | 'center' | 'end';
|
|
24
25
|
/**
|
|
25
26
|
* Configuration for a table column.
|
|
26
27
|
*/
|
|
@@ -54,7 +55,7 @@ export interface TableColumn<T = Record<string, unknown>> {
|
|
|
54
55
|
minWidth?: string;
|
|
55
56
|
/**
|
|
56
57
|
* Text alignment for this column.
|
|
57
|
-
* @default '
|
|
58
|
+
* @default 'start'
|
|
58
59
|
*/
|
|
59
60
|
align?: CellAlign;
|
|
60
61
|
/**
|
|
@@ -200,6 +200,20 @@ var textStyles = css`
|
|
|
200
200
|
`;
|
|
201
201
|
|
|
202
202
|
// src/components/text/text.ts
|
|
203
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
204
|
+
"h1",
|
|
205
|
+
"h2",
|
|
206
|
+
"h3",
|
|
207
|
+
"h4",
|
|
208
|
+
"h5",
|
|
209
|
+
"h6",
|
|
210
|
+
"p",
|
|
211
|
+
"span",
|
|
212
|
+
"div",
|
|
213
|
+
"label",
|
|
214
|
+
"strong",
|
|
215
|
+
"em"
|
|
216
|
+
]);
|
|
203
217
|
var VARIANT_TAG = {
|
|
204
218
|
"display-2xl": "p",
|
|
205
219
|
"display-xl": "p",
|
|
@@ -224,7 +238,8 @@ var Text = class extends ThemeableElement {
|
|
|
224
238
|
this.as = "";
|
|
225
239
|
}
|
|
226
240
|
render() {
|
|
227
|
-
const
|
|
241
|
+
const requested = this.as && ALLOWED_TAGS.has(this.as) ? this.as : "";
|
|
242
|
+
const tag = unsafeStatic(requested || VARIANT_TAG[this.variant] || "p");
|
|
228
243
|
return html`<${tag} part="base"><slot></slot></${tag}>`;
|
|
229
244
|
}
|
|
230
245
|
};
|