@latty-ds/web 0.10.4 → 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 +1969 -228
- 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 +124 -17
- package/dist/components/avatar/avatar.d.ts +2 -0
- package/dist/components/avatar/index.js +10 -3
- 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 +135 -12
- 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 +78 -25
- 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 +216 -53
- 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 +237 -90
- package/dist/components/divider/index.js +2 -2
- 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 +36 -1
- 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 +275 -72
- 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 +2458 -683
- package/dist/index.js +2469 -690
- 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 +3 -7
|
@@ -34,6 +34,16 @@ __decorateClass([
|
|
|
34
34
|
property({ reflect: true })
|
|
35
35
|
], ThemeableElement.prototype, "theme", 2);
|
|
36
36
|
|
|
37
|
+
// src/utils/color.ts
|
|
38
|
+
var resolveColorValue = (value) => value.startsWith("--") ? `var(${value})` : value;
|
|
39
|
+
|
|
40
|
+
// src/utils/dev-warn.ts
|
|
41
|
+
var devWarn = (tag, message) => {
|
|
42
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
43
|
+
console.warn(`[${tag}] ${message}`);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
37
47
|
// src/components/dialog/dialog.ts
|
|
38
48
|
import { customElement as customElement4, property as property5, query } from "lit/decorators.js";
|
|
39
49
|
|
|
@@ -44,80 +54,83 @@ var dialogStyles = css`
|
|
|
44
54
|
font-family: var(--lt-typography-fontFamilyPrimary, 'Hanken Grotesk', sans-serif);
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
/*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
:host(:not([open])) .backdrop {
|
|
69
|
-
pointer-events: none;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/* lt-surface host — flex item inside .backdrop, sized per size variant */
|
|
73
|
-
lt-surface {
|
|
74
|
-
width: 100%;
|
|
57
|
+
/* The native <dialog> is the modal surface now — its own showModal()/close() give the top-layer stacking,
|
|
58
|
+
focus trap and inert background for free (DLG-1). Reset its UA-default border/padding/background (a native
|
|
59
|
+
<dialog> renders with a visible border and white background by default) so lt-surface, not the browser's
|
|
60
|
+
own chrome, controls how it looks. Width/height are viewport-relative with a spacing-4 inset on each side
|
|
61
|
+
(matching the old .backdrop's padding), capped per size variant below. */
|
|
62
|
+
dialog {
|
|
63
|
+
background: transparent;
|
|
64
|
+
border: none;
|
|
65
|
+
max-height: calc(100vh - var(--lt-spacing-8));
|
|
66
|
+
overflow: visible;
|
|
67
|
+
padding: 0;
|
|
68
|
+
width: calc(100vw - var(--lt-spacing-8));
|
|
69
|
+
/* display/overlay (top-layer membership) are the only things animated on <dialog> itself — the entrance/exit
|
|
70
|
+
motion (opacity + translateY) lives on lt-surface below, unaffected by any of this. Without these,
|
|
71
|
+
close() would remove the dialog (and its ::backdrop) from rendering instantly, cutting that motion off
|
|
72
|
+
partway through; allow-discrete keeps it rendered for the transition's duration instead, then removes it. */
|
|
73
|
+
transition:
|
|
74
|
+
display 220ms allow-discrete,
|
|
75
|
+
overlay 220ms allow-discrete;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
:host([size='sm'])
|
|
78
|
+
:host([size='sm']) dialog {
|
|
78
79
|
max-width: 400px;
|
|
79
80
|
}
|
|
80
|
-
:host([size='md'])
|
|
81
|
+
:host([size='md']) dialog {
|
|
81
82
|
max-width: 600px;
|
|
82
83
|
}
|
|
83
|
-
:host([size='lg'])
|
|
84
|
+
:host([size='lg']) dialog {
|
|
84
85
|
max-width: 800px;
|
|
85
86
|
}
|
|
86
|
-
:host([size='xl'])
|
|
87
|
+
:host([size='xl']) dialog {
|
|
87
88
|
max-width: 1000px;
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
-
:host([size='fullscreen']) .backdrop {
|
|
91
|
-
padding: 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
:host([size='fullscreen']) lt-surface {
|
|
90
|
+
:host([size='fullscreen']) dialog {
|
|
95
91
|
height: 100vh;
|
|
96
92
|
max-height: 100vh;
|
|
97
93
|
max-width: 100vw;
|
|
94
|
+
width: 100vw;
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
/*
|
|
101
|
-
|
|
97
|
+
/* ::backdrop replaces the old .backdrop div — it's a real paintable box (just not a real DOM node), so its
|
|
98
|
+
own background-color transition plays independently of lt-surface's. */
|
|
99
|
+
dialog::backdrop {
|
|
100
|
+
background: rgba(0, 0, 0, 0);
|
|
101
|
+
transition:
|
|
102
|
+
background-color 200ms ease,
|
|
103
|
+
display 220ms allow-discrete,
|
|
104
|
+
overlay 220ms allow-discrete;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
dialog[open]::backdrop {
|
|
108
|
+
background: rgba(0, 0, 0, var(--lt-dialog-overlay-opacity, 0.5));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* lt-surface — fills the <dialog>, sized by it (max-width/height above); the entrance/exit animation */
|
|
112
|
+
lt-surface {
|
|
102
113
|
display: flex;
|
|
103
114
|
flex-direction: column;
|
|
104
|
-
|
|
115
|
+
height: 100%;
|
|
105
116
|
opacity: 0;
|
|
106
|
-
position: relative;
|
|
107
117
|
transform: translateY(16px);
|
|
108
118
|
transition:
|
|
109
119
|
opacity 220ms ease,
|
|
110
120
|
transform 220ms cubic-bezier(0.34, 1.2, 0.64, 1);
|
|
121
|
+
width: 100%;
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
|
|
124
|
+
dialog[open] lt-surface {
|
|
114
125
|
opacity: 1;
|
|
115
126
|
transform: translateY(0);
|
|
116
127
|
}
|
|
117
128
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
lt-surface::part(surface) {
|
|
130
|
+
display: flex;
|
|
131
|
+
flex: 1;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
min-height: 0;
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
/* Header */
|
|
@@ -144,7 +157,7 @@ var dialogStyles = css`
|
|
|
144
157
|
|
|
145
158
|
/* Body */
|
|
146
159
|
.body {
|
|
147
|
-
color: var(--lt-
|
|
160
|
+
color: var(--lt-text-neutral);
|
|
148
161
|
flex: 1;
|
|
149
162
|
overflow-y: auto;
|
|
150
163
|
padding: var(--lt-spacing-5);
|
|
@@ -164,18 +177,46 @@ var dialogStyles = css`
|
|
|
164
177
|
letter-spacing: 0.05rem;
|
|
165
178
|
text-transform: lowercase;
|
|
166
179
|
}
|
|
180
|
+
|
|
181
|
+
/* ── Reduced motion ─────────────────────────────────────────────────────── */
|
|
182
|
+
|
|
183
|
+
@media (prefers-reduced-motion: reduce) {
|
|
184
|
+
dialog,
|
|
185
|
+
dialog::backdrop,
|
|
186
|
+
lt-surface {
|
|
187
|
+
transition: none;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
167
190
|
`;
|
|
168
191
|
|
|
169
192
|
// src/components/shared/backdrop.styles.ts
|
|
170
193
|
import { css as css2 } from "lit";
|
|
171
194
|
var backdropFeatureStyles = css2`
|
|
172
|
-
:host([backdrop-blur])
|
|
173
|
-
:host([backdrop-blur]) [part='overlay'] {
|
|
195
|
+
:host([backdrop-blur]) dialog::backdrop {
|
|
174
196
|
backdrop-filter: blur(6px);
|
|
175
197
|
-webkit-backdrop-filter: blur(6px);
|
|
176
198
|
}
|
|
177
199
|
`;
|
|
178
200
|
|
|
201
|
+
// src/components/shared/scroll-lock.ts
|
|
202
|
+
var lockCount = 0;
|
|
203
|
+
var previousOverflow = null;
|
|
204
|
+
function lockBodyScroll() {
|
|
205
|
+
if (lockCount === 0) {
|
|
206
|
+
previousOverflow = document.body.style.overflow;
|
|
207
|
+
document.body.style.overflow = "hidden";
|
|
208
|
+
}
|
|
209
|
+
lockCount++;
|
|
210
|
+
}
|
|
211
|
+
function unlockBodyScroll() {
|
|
212
|
+
if (lockCount === 0) return;
|
|
213
|
+
lockCount--;
|
|
214
|
+
if (lockCount === 0) {
|
|
215
|
+
document.body.style.overflow = previousOverflow ?? "";
|
|
216
|
+
previousOverflow = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
179
220
|
// src/components/icon-button/icon-button.ts
|
|
180
221
|
import { html, nothing } from "lit";
|
|
181
222
|
import { customElement, property as property2 } from "lit/decorators.js";
|
|
@@ -427,6 +468,14 @@ var iconButtonStyles = css3`
|
|
|
427
468
|
:host([round]) [part='base'] {
|
|
428
469
|
border-radius: 50%;
|
|
429
470
|
}
|
|
471
|
+
|
|
472
|
+
/* ── Reduced motion ─────────────────────────────────────────────────────── */
|
|
473
|
+
|
|
474
|
+
@media (prefers-reduced-motion: reduce) {
|
|
475
|
+
[part='base'] {
|
|
476
|
+
transition: none;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
430
479
|
`;
|
|
431
480
|
|
|
432
481
|
// src/components/shared/aria-forwarding.ts
|
|
@@ -435,7 +484,8 @@ var FORWARDED_ARIA_ATTRIBUTES = [
|
|
|
435
484
|
"aria-expanded",
|
|
436
485
|
"aria-haspopup",
|
|
437
486
|
"aria-controls",
|
|
438
|
-
"aria-current"
|
|
487
|
+
"aria-current",
|
|
488
|
+
"aria-description"
|
|
439
489
|
];
|
|
440
490
|
var AriaForwardingController = class {
|
|
441
491
|
constructor(host, getTarget) {
|
|
@@ -475,11 +525,30 @@ var IconButton = class extends ThemeableElement {
|
|
|
475
525
|
this.bordered = false;
|
|
476
526
|
this.size = "md";
|
|
477
527
|
this.disabled = false;
|
|
528
|
+
this._blockClickWhileDisabled = (e) => {
|
|
529
|
+
if (this.disabled) {
|
|
530
|
+
e.preventDefault();
|
|
531
|
+
e.stopImmediatePropagation();
|
|
532
|
+
}
|
|
533
|
+
};
|
|
478
534
|
this.href = "";
|
|
479
535
|
this.target = "";
|
|
480
536
|
this.rel = "";
|
|
481
537
|
this.round = false;
|
|
482
538
|
}
|
|
539
|
+
firstUpdated() {
|
|
540
|
+
if (!this.label && !this.textContent?.trim()) {
|
|
541
|
+
devWarn("lt-icon-button", "no label or visible text \u2014 set `label` so screen readers can name the button");
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
connectedCallback() {
|
|
545
|
+
super.connectedCallback();
|
|
546
|
+
this.addEventListener("click", this._blockClickWhileDisabled, { capture: true });
|
|
547
|
+
}
|
|
548
|
+
disconnectedCallback() {
|
|
549
|
+
super.disconnectedCallback();
|
|
550
|
+
this.removeEventListener("click", this._blockClickWhileDisabled, { capture: true });
|
|
551
|
+
}
|
|
483
552
|
_inner() {
|
|
484
553
|
return this.icon ? html`<lt-icon name=${this.icon}></lt-icon>` : html`<slot></slot>`;
|
|
485
554
|
}
|
|
@@ -599,10 +668,15 @@ var surfaceStyles = css4`
|
|
|
599
668
|
:host([appearance='outlined']:hover) .surface {
|
|
600
669
|
border-color: var(--lt-border-strong);
|
|
601
670
|
}
|
|
602
|
-
`;
|
|
603
671
|
|
|
604
|
-
|
|
605
|
-
|
|
672
|
+
/* ── Reduced motion ─────────────────────────────────────────────────────── */
|
|
673
|
+
|
|
674
|
+
@media (prefers-reduced-motion: reduce) {
|
|
675
|
+
.surface {
|
|
676
|
+
transition: none;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
`;
|
|
606
680
|
|
|
607
681
|
// src/components/surface/surface.ts
|
|
608
682
|
var Surface = class extends ThemeableElement {
|
|
@@ -804,6 +878,20 @@ var textStyles = css5`
|
|
|
804
878
|
`;
|
|
805
879
|
|
|
806
880
|
// src/components/text/text.ts
|
|
881
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
882
|
+
"h1",
|
|
883
|
+
"h2",
|
|
884
|
+
"h3",
|
|
885
|
+
"h4",
|
|
886
|
+
"h5",
|
|
887
|
+
"h6",
|
|
888
|
+
"p",
|
|
889
|
+
"span",
|
|
890
|
+
"div",
|
|
891
|
+
"label",
|
|
892
|
+
"strong",
|
|
893
|
+
"em"
|
|
894
|
+
]);
|
|
807
895
|
var VARIANT_TAG = {
|
|
808
896
|
"display-2xl": "p",
|
|
809
897
|
"display-xl": "p",
|
|
@@ -828,7 +916,8 @@ var Text = class extends ThemeableElement {
|
|
|
828
916
|
this.as = "";
|
|
829
917
|
}
|
|
830
918
|
render() {
|
|
831
|
-
const
|
|
919
|
+
const requested = this.as && ALLOWED_TAGS.has(this.as) ? this.as : "";
|
|
920
|
+
const tag = unsafeStatic(requested || VARIANT_TAG[this.variant] || "p");
|
|
832
921
|
return html3`<${tag} part="base"><slot></slot></${tag}>`;
|
|
833
922
|
}
|
|
834
923
|
};
|
|
@@ -844,12 +933,15 @@ Text = __decorateClass([
|
|
|
844
933
|
], Text);
|
|
845
934
|
|
|
846
935
|
// src/components/dialog/dialog.ts
|
|
936
|
+
var DEFAULT_LABELS = {
|
|
937
|
+
close: "Close dialog"
|
|
938
|
+
};
|
|
847
939
|
var Dialog = class extends ThemeableElement {
|
|
848
940
|
constructor() {
|
|
849
941
|
super(...arguments);
|
|
850
942
|
this.size = "md";
|
|
851
943
|
this.open = false;
|
|
852
|
-
this.
|
|
944
|
+
this.heading = "";
|
|
853
945
|
this.noCloseButton = false;
|
|
854
946
|
this.closeOnBackdropClick = true;
|
|
855
947
|
this.closeOnEscape = true;
|
|
@@ -857,13 +949,53 @@ var Dialog = class extends ThemeableElement {
|
|
|
857
949
|
this.backdropBlur = false;
|
|
858
950
|
this.elevation = "4";
|
|
859
951
|
this.appearance = "filled";
|
|
860
|
-
this.
|
|
861
|
-
|
|
862
|
-
|
|
952
|
+
this.labels = {};
|
|
953
|
+
this._scrollLocked = false;
|
|
954
|
+
// `updated()` also runs after the very first render, where `changed.has('open')` is true even though nothing
|
|
955
|
+
// was ever shown — `open` starts false, so the else branch ran, firing a spurious `close` and clobbering
|
|
956
|
+
// `document.body.style.overflow` on page load (DLG-2). Same fix as lt-dropdown's `_skipNextOpenChange` (DDN-1)
|
|
957
|
+
// and lt-snackbar's (SNK-1).
|
|
958
|
+
this._skipNextOpenChange = true;
|
|
959
|
+
// Native <dialog> fires 'cancel' (cancelable) on Escape before it closes itself — preventDefault() here is
|
|
960
|
+
// how close-on-escape="false" is implemented now, instead of an old keydown listener racing the platform's
|
|
961
|
+
// own handling (confirmed working in the spike across all three engines).
|
|
962
|
+
this._handleCancel = (e) => {
|
|
963
|
+
if (!this.closeOnEscape) {
|
|
964
|
+
e.preventDefault();
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
// The native element's own 'close' event fires whenever IT closes, for any reason — our own close() call
|
|
968
|
+
// above, but also Escape (which closes it directly; nothing else tells us). Sync `open` back so the two never
|
|
969
|
+
// drift apart. If we're the ones who just called close() (inside handleClose(), above), `open` is already
|
|
970
|
+
// false and this is a no-op — Lit ignores a property set to its current value.
|
|
971
|
+
this._handleNativeClose = () => {
|
|
972
|
+
this.open = false;
|
|
973
|
+
};
|
|
974
|
+
// The clicked-outside-the-content area of a shown <dialog> resolves e.target to the <dialog> element itself
|
|
975
|
+
// (::backdrop isn't a real node and can't be an event target) — clicking any actual descendant instead
|
|
976
|
+
// targets that descendant, so this only ever fires for a genuine backdrop click (confirmed in the spike).
|
|
977
|
+
this._handleDialogClick = (e) => {
|
|
978
|
+
if (this.closeOnBackdropClick && e.target === this.dialogElement) {
|
|
863
979
|
this.hide();
|
|
864
980
|
}
|
|
865
981
|
};
|
|
866
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* @deprecated Use `heading` instead. `title` collides with the browser's own native tooltip attribute — every
|
|
985
|
+
* HTML element has one regardless of custom-element status, so `<lt-dialog title="…">` made hovering anywhere
|
|
986
|
+
* over the dialog show a browser tooltip, and assistive tech could read it as a duplicate description (DLG-6).
|
|
987
|
+
* This alias keeps existing markup/JS working; the native `title` attribute is always stripped as soon as
|
|
988
|
+
* it's read, so the tooltip never appears.
|
|
989
|
+
*/
|
|
990
|
+
get title() {
|
|
991
|
+
return this.heading;
|
|
992
|
+
}
|
|
993
|
+
set title(value) {
|
|
994
|
+
this.heading = value;
|
|
995
|
+
}
|
|
996
|
+
get _labels() {
|
|
997
|
+
return { ...DEFAULT_LABELS, ...this.labels };
|
|
998
|
+
}
|
|
867
999
|
/**
|
|
868
1000
|
* Opens the dialog.
|
|
869
1001
|
*/
|
|
@@ -882,18 +1014,31 @@ var Dialog = class extends ThemeableElement {
|
|
|
882
1014
|
toggle() {
|
|
883
1015
|
this.open = !this.open;
|
|
884
1016
|
}
|
|
1017
|
+
firstUpdated() {
|
|
1018
|
+
if (!this.heading && !this.hasAttribute("aria-label") && !this.hasAttribute("aria-labelledby")) {
|
|
1019
|
+
devWarn("lt-dialog", "no `heading` \u2014 the dialog has no accessible name; set `heading` or `aria-label`");
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
885
1022
|
connectedCallback() {
|
|
886
1023
|
super.connectedCallback();
|
|
887
|
-
this.
|
|
1024
|
+
if (this.hasAttribute("title")) {
|
|
1025
|
+
if (!this.heading) this.heading = this.getAttribute("title");
|
|
1026
|
+
this.removeAttribute("title");
|
|
1027
|
+
}
|
|
888
1028
|
}
|
|
889
1029
|
disconnectedCallback() {
|
|
890
1030
|
super.disconnectedCallback();
|
|
891
|
-
|
|
1031
|
+
if (this._scrollLocked) {
|
|
1032
|
+
unlockBodyScroll();
|
|
1033
|
+
this._scrollLocked = false;
|
|
1034
|
+
}
|
|
892
1035
|
this.restoreFocus();
|
|
893
1036
|
}
|
|
894
1037
|
updated(changedProperties) {
|
|
895
1038
|
if (changedProperties.has("open")) {
|
|
896
|
-
if (this.
|
|
1039
|
+
if (this._skipNextOpenChange) {
|
|
1040
|
+
this._skipNextOpenChange = false;
|
|
1041
|
+
} else if (this.open) {
|
|
897
1042
|
this.handleOpen();
|
|
898
1043
|
} else {
|
|
899
1044
|
this.handleClose();
|
|
@@ -902,19 +1047,25 @@ var Dialog = class extends ThemeableElement {
|
|
|
902
1047
|
}
|
|
903
1048
|
handleOpen() {
|
|
904
1049
|
this.previouslyFocusedElement = document.activeElement;
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
this.
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1050
|
+
if (!this._scrollLocked) {
|
|
1051
|
+
lockBodyScroll();
|
|
1052
|
+
this._scrollLocked = true;
|
|
1053
|
+
}
|
|
1054
|
+
if (this.dialogElement && !this.dialogElement.open) {
|
|
1055
|
+
this.dialogElement.showModal();
|
|
1056
|
+
}
|
|
1057
|
+
this.dispatchEvent(new CustomEvent("open", { bubbles: true, composed: true }));
|
|
912
1058
|
}
|
|
913
1059
|
handleClose() {
|
|
914
|
-
|
|
1060
|
+
if (this._scrollLocked) {
|
|
1061
|
+
unlockBodyScroll();
|
|
1062
|
+
this._scrollLocked = false;
|
|
1063
|
+
}
|
|
1064
|
+
if (this.dialogElement?.open) {
|
|
1065
|
+
this.dialogElement.close();
|
|
1066
|
+
}
|
|
915
1067
|
this.restoreFocus();
|
|
916
1068
|
this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
|
|
917
|
-
this.dispatchEvent(new CustomEvent("dialog-close", { bubbles: true, composed: true }));
|
|
918
1069
|
}
|
|
919
1070
|
restoreFocus() {
|
|
920
1071
|
if (this.previouslyFocusedElement) {
|
|
@@ -922,30 +1073,23 @@ var Dialog = class extends ThemeableElement {
|
|
|
922
1073
|
this.previouslyFocusedElement = void 0;
|
|
923
1074
|
}
|
|
924
1075
|
}
|
|
925
|
-
handleBackdropClick(event) {
|
|
926
|
-
if (this.closeOnBackdropClick && event.target === event.currentTarget) {
|
|
927
|
-
this.hide();
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
1076
|
handleCloseClick() {
|
|
931
1077
|
this.hide();
|
|
932
1078
|
}
|
|
933
1079
|
render() {
|
|
934
1080
|
return html4`
|
|
935
|
-
<
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
part="dialog"
|
|
944
|
-
>
|
|
1081
|
+
<dialog
|
|
1082
|
+
part="dialog"
|
|
1083
|
+
aria-labelledby="dialog-title"
|
|
1084
|
+
@cancel=${this._handleCancel}
|
|
1085
|
+
@close=${this._handleNativeClose}
|
|
1086
|
+
@click=${this._handleDialogClick}
|
|
1087
|
+
>
|
|
1088
|
+
<lt-surface elevation=${this.elevation} appearance=${this.appearance} part="surface">
|
|
945
1089
|
<div class="header" part="header">
|
|
946
1090
|
<div class="header-content">
|
|
947
1091
|
<slot name="header">
|
|
948
|
-
${this.
|
|
1092
|
+
${this.heading ? html4`<lt-text variant="h4" as="h2" id="dialog-title" class="title">${this.heading}</lt-text>` : ""}
|
|
949
1093
|
</slot>
|
|
950
1094
|
</div>
|
|
951
1095
|
${!this.noCloseButton ? html4`
|
|
@@ -953,7 +1097,7 @@ var Dialog = class extends ThemeableElement {
|
|
|
953
1097
|
class="close-button"
|
|
954
1098
|
part="close-button"
|
|
955
1099
|
icon="xmark"
|
|
956
|
-
label
|
|
1100
|
+
label=${this._labels.close}
|
|
957
1101
|
size="sm"
|
|
958
1102
|
@click=${this.handleCloseClick}
|
|
959
1103
|
></lt-icon-button>
|
|
@@ -970,7 +1114,7 @@ var Dialog = class extends ThemeableElement {
|
|
|
970
1114
|
</div>
|
|
971
1115
|
` : ""}
|
|
972
1116
|
</lt-surface>
|
|
973
|
-
</
|
|
1117
|
+
</dialog>
|
|
974
1118
|
`;
|
|
975
1119
|
}
|
|
976
1120
|
hasSlot(name) {
|
|
@@ -986,7 +1130,7 @@ __decorateClass([
|
|
|
986
1130
|
], Dialog.prototype, "open", 2);
|
|
987
1131
|
__decorateClass([
|
|
988
1132
|
property5()
|
|
989
|
-
], Dialog.prototype, "
|
|
1133
|
+
], Dialog.prototype, "heading", 2);
|
|
990
1134
|
__decorateClass([
|
|
991
1135
|
property5({ type: Boolean, attribute: "no-close-button" })
|
|
992
1136
|
], Dialog.prototype, "noCloseButton", 2);
|
|
@@ -1009,7 +1153,10 @@ __decorateClass([
|
|
|
1009
1153
|
property5({ reflect: true })
|
|
1010
1154
|
], Dialog.prototype, "appearance", 2);
|
|
1011
1155
|
__decorateClass([
|
|
1012
|
-
|
|
1156
|
+
property5({ type: Object })
|
|
1157
|
+
], Dialog.prototype, "labels", 2);
|
|
1158
|
+
__decorateClass([
|
|
1159
|
+
query("dialog")
|
|
1013
1160
|
], Dialog.prototype, "dialogElement", 2);
|
|
1014
1161
|
Dialog = __decorateClass([
|
|
1015
1162
|
customElement4("lt-dialog")
|
|
@@ -80,8 +80,8 @@ var dividerStyles = css`
|
|
|
80
80
|
|
|
81
81
|
:host([orientation='vertical']) [part='base']::before,
|
|
82
82
|
:host([orientation='vertical']) [part='base']::after {
|
|
83
|
-
border-
|
|
84
|
-
border-
|
|
83
|
+
border-inline-start: 1px solid var(--lt-border-default);
|
|
84
|
+
border-inline-start-style: var(--_divider-style, solid);
|
|
85
85
|
content: '';
|
|
86
86
|
flex: 1;
|
|
87
87
|
}
|
|
@@ -12,6 +12,7 @@ import '../surface/surface';
|
|
|
12
12
|
* @fires {CustomEvent} close - Fired when the menu closes
|
|
13
13
|
*
|
|
14
14
|
* @cssprop [--lt-dropdown-menu-padding] - Overrides the menu surface's default vertical padding.
|
|
15
|
+
* @cssprop [--lt-dropdown-max-height=min(24rem, 60vh)] - Tallest the menu grows before it scrolls.
|
|
15
16
|
*/
|
|
16
17
|
export declare class Dropdown extends ThemeableElement {
|
|
17
18
|
static styles: import("lit").CSSResult[];
|
|
@@ -19,8 +20,11 @@ export declare class Dropdown extends ThemeableElement {
|
|
|
19
20
|
placement: DropdownPlacement;
|
|
20
21
|
private _cleanupClickOutside;
|
|
21
22
|
private _floatingCleanup;
|
|
23
|
+
private _skipNextOpenChange;
|
|
22
24
|
connectedCallback(): void;
|
|
23
25
|
disconnectedCallback(): void;
|
|
26
|
+
private _syncTriggerAria;
|
|
27
|
+
private _onFocusOut;
|
|
24
28
|
updated(changed: Map<string, unknown>): void;
|
|
25
29
|
show(): void;
|
|
26
30
|
hide(): void;
|
|
@@ -1446,7 +1446,9 @@ var dropdownStyles = [
|
|
|
1446
1446
|
}
|
|
1447
1447
|
|
|
1448
1448
|
lt-surface.menu::part(surface) {
|
|
1449
|
-
|
|
1449
|
+
max-height: var(--lt-dropdown-max-height, min(24rem, 60vh));
|
|
1450
|
+
overflow-x: hidden;
|
|
1451
|
+
overflow-y: auto;
|
|
1450
1452
|
padding-block: var(--lt-dropdown-menu-padding, var(--lt-spacing-2));
|
|
1451
1453
|
}
|
|
1452
1454
|
`
|
|
@@ -1492,7 +1494,7 @@ var dropdownItemStyles = css3`
|
|
|
1492
1494
|
font-size: 0.875rem;
|
|
1493
1495
|
gap: var(--lt-spacing-2);
|
|
1494
1496
|
padding: var(--lt-spacing-2) var(--lt-spacing-4);
|
|
1495
|
-
text-align:
|
|
1497
|
+
text-align: start;
|
|
1496
1498
|
text-decoration: none;
|
|
1497
1499
|
width: 100%;
|
|
1498
1500
|
}
|
|
@@ -1633,6 +1635,14 @@ var surfaceStyles = css4`
|
|
|
1633
1635
|
:host([appearance='outlined']:hover) .surface {
|
|
1634
1636
|
border-color: var(--lt-border-strong);
|
|
1635
1637
|
}
|
|
1638
|
+
|
|
1639
|
+
/* ── Reduced motion ─────────────────────────────────────────────────────── */
|
|
1640
|
+
|
|
1641
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1642
|
+
.surface {
|
|
1643
|
+
transition: none;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1636
1646
|
`;
|
|
1637
1647
|
|
|
1638
1648
|
// src/components/surface/surface.ts
|
|
@@ -1674,6 +1684,22 @@ var Dropdown = class extends ThemeableElement {
|
|
|
1674
1684
|
this.placement = "bottom-start";
|
|
1675
1685
|
this._cleanupClickOutside = null;
|
|
1676
1686
|
this._floatingCleanup = null;
|
|
1687
|
+
// `updated()` also runs after the very first render, where `changed.has('open')` is true even though nothing
|
|
1688
|
+
// ever opened. Without this, mounting a closed dropdown fired `close` and moved focus to the trigger on page
|
|
1689
|
+
// load (verified: activeElement jumped to a native <button> trigger). Ships with lt-button's `delegatesFocus`
|
|
1690
|
+
// (BTN-4) — before that, the same first-update run was a no-op for an `lt-button` trigger anyway, since its
|
|
1691
|
+
// `.focus()` did nothing.
|
|
1692
|
+
this._skipNextOpenChange = true;
|
|
1693
|
+
// Tab away from the trigger/menu (as opposed to Escape, or a click outside, which are already handled) left
|
|
1694
|
+
// the menu open — nothing closed it once focus moved on. relatedTarget is the element focus is moving *to*;
|
|
1695
|
+
// if that's still somewhere inside this dropdown (the trigger, or an item — both light DOM children of this
|
|
1696
|
+
// element), there's nothing to do.
|
|
1697
|
+
this._onFocusOut = (e) => {
|
|
1698
|
+
if (!this.open) return;
|
|
1699
|
+
const next = e.relatedTarget;
|
|
1700
|
+
if (next && this.contains(next)) return;
|
|
1701
|
+
this.hide();
|
|
1702
|
+
};
|
|
1677
1703
|
this._onKeydown = (e) => {
|
|
1678
1704
|
if (!this.open) return;
|
|
1679
1705
|
const items = this._items();
|
|
@@ -1706,18 +1732,32 @@ var Dropdown = class extends ThemeableElement {
|
|
|
1706
1732
|
super.connectedCallback();
|
|
1707
1733
|
this.addEventListener("keydown", this._onKeydown);
|
|
1708
1734
|
this.addEventListener("select", this.hide);
|
|
1735
|
+
this.addEventListener("focusout", this._onFocusOut);
|
|
1709
1736
|
}
|
|
1710
1737
|
disconnectedCallback() {
|
|
1711
1738
|
super.disconnectedCallback();
|
|
1712
1739
|
this.removeEventListener("keydown", this._onKeydown);
|
|
1713
1740
|
this.removeEventListener("select", this.hide);
|
|
1741
|
+
this.removeEventListener("focusout", this._onFocusOut);
|
|
1714
1742
|
this._cleanupClickOutside?.();
|
|
1715
1743
|
this._cleanupClickOutside = null;
|
|
1716
1744
|
this._floatingCleanup?.();
|
|
1717
1745
|
this._floatingCleanup = null;
|
|
1718
1746
|
}
|
|
1747
|
+
// The slotted trigger otherwise has no indication it opens a menu, or whether that menu is currently open.
|
|
1748
|
+
_syncTriggerAria() {
|
|
1749
|
+
const trigger = this._triggerEl();
|
|
1750
|
+
if (!trigger) return;
|
|
1751
|
+
trigger.setAttribute("aria-haspopup", "menu");
|
|
1752
|
+
trigger.setAttribute("aria-expanded", this.open ? "true" : "false");
|
|
1753
|
+
}
|
|
1719
1754
|
updated(changed) {
|
|
1755
|
+
this._syncTriggerAria();
|
|
1720
1756
|
if (!changed.has("open")) return;
|
|
1757
|
+
if (this._skipNextOpenChange) {
|
|
1758
|
+
this._skipNextOpenChange = false;
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1721
1761
|
if (this.open) {
|
|
1722
1762
|
this._cleanupClickOutside = createClickOutsideHandler(this, () => this.hide(), { event: "click", capture: true });
|
|
1723
1763
|
const triggerEl = this._triggerEl() ?? this;
|
|
@@ -1761,7 +1801,7 @@ var Dropdown = class extends ThemeableElement {
|
|
|
1761
1801
|
render() {
|
|
1762
1802
|
return html3`
|
|
1763
1803
|
<div class="trigger-wrap" @click=${this.toggle}>
|
|
1764
|
-
<slot name="trigger"></slot>
|
|
1804
|
+
<slot name="trigger" @slotchange=${this._syncTriggerAria}></slot>
|
|
1765
1805
|
</div>
|
|
1766
1806
|
<lt-surface
|
|
1767
1807
|
class="menu"
|
|
@@ -49,6 +49,10 @@ export declare class IconButton extends ThemeableElement {
|
|
|
49
49
|
size: IconButtonSize;
|
|
50
50
|
/** Whether the button is disabled. */
|
|
51
51
|
disabled: boolean;
|
|
52
|
+
protected firstUpdated(): void;
|
|
53
|
+
connectedCallback(): void;
|
|
54
|
+
disconnectedCallback(): void;
|
|
55
|
+
private _blockClickWhileDisabled;
|
|
52
56
|
/** When set, renders an `<a>` tag instead of `<button>`. */
|
|
53
57
|
href: string;
|
|
54
58
|
/** Forwarded to the anchor's `target` attribute when `href` is set. */
|