@webjsdev/ui 0.3.5 → 0.3.7
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/README.md +20 -2
- package/package.json +3 -2
- package/packages/registry/components/alert-dialog.ts +61 -34
- package/packages/registry/components/alert.ts +5 -0
- package/packages/registry/components/avatar.ts +5 -0
- package/packages/registry/components/badge.ts +4 -0
- package/packages/registry/components/breadcrumb.ts +5 -0
- package/packages/registry/components/button.ts +6 -0
- package/packages/registry/components/dialog.ts +63 -24
- package/packages/registry/components/dropdown-menu.ts +89 -30
- package/packages/registry/components/hover-card.ts +119 -17
- package/packages/registry/components/pagination.ts +5 -0
- package/packages/registry/components/progress.ts +5 -0
- package/packages/registry/components/separator.ts +5 -0
- package/packages/registry/components/skeleton.ts +5 -0
- package/packages/registry/components/sonner.ts +57 -17
- package/packages/registry/components/table.ts +7 -2
- package/packages/registry/components/tabs.ts +59 -30
- package/packages/registry/components/toggle-group.ts +70 -28
- package/packages/registry/components/toggle.ts +7 -13
- package/packages/registry/components/tooltip.ts +46 -11
- package/packages/registry/lib/utils.ts +43 -0
- package/packages/registry/registry.json +14 -1
package/README.md
CHANGED
|
@@ -30,14 +30,32 @@ is configured, the components render correctly. Variant names, sizes, and
|
|
|
30
30
|
data-attribute conventions mirror shadcn's so an AI agent's existing
|
|
31
31
|
knowledge of shadcn maps directly.
|
|
32
32
|
|
|
33
|
-
Tier-2 elements extend `WebComponent
|
|
34
|
-
Lit-shaped base class
|
|
33
|
+
Tier-2 elements extend the `WebComponent({ ... })` factory from
|
|
34
|
+
`@webjsdev/core`, a tiny Lit-shaped base class whose factory shape declares
|
|
35
|
+
reactive attributes,
|
|
35
36
|
`render()` returning an `` html`...` `` template, and declarative
|
|
36
37
|
bindings (`@click`, `?attr`, `attr=`). Light DOM throughout, so Tailwind
|
|
37
38
|
utility classes on authored children apply directly. The `webjsui add`
|
|
38
39
|
CLI installs `@webjsdev/core` automatically when you add a Tier-2
|
|
39
40
|
component.
|
|
40
41
|
|
|
42
|
+
## Accessibility
|
|
43
|
+
|
|
44
|
+
Tier-2 elements are accessible out of the box: they wire their own WAI-ARIA
|
|
45
|
+
pattern, so you do not hand-add ARIA. Tabs cross-links triggers and panels and
|
|
46
|
+
reports orientation, toggle-group uses roving tabindex with Arrow / Home / End,
|
|
47
|
+
dropdown-menu declares orientation and reflects `aria-disabled`, dialog and
|
|
48
|
+
alert-dialog name themselves from their title and description, tooltip wires
|
|
49
|
+
`aria-describedby`, hover-card exposes `aria-haspopup` / `aria-expanded`, and
|
|
50
|
+
sonner is a live region.
|
|
51
|
+
|
|
52
|
+
Tier-1 class helpers return only classes, so the semantic element and ARIA are
|
|
53
|
+
yours to supply. Each one's JSDoc carries an `A11y (required for accessible
|
|
54
|
+
output)` block stating exactly what to add: a name on an icon-only button, a
|
|
55
|
+
role on an alert, `scope` on table headers, `alt` on an avatar image, a
|
|
56
|
+
labelled `<nav>` with `aria-current="page"` on pagination and breadcrumb, and
|
|
57
|
+
so on. Follow that block and the markup is fully accessible.
|
|
58
|
+
|
|
41
59
|
## Install
|
|
42
60
|
|
|
43
61
|
### Option A : Webjs users (already have `@webjsdev/cli`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An AI-first component library - class-helper functions for visuals, custom elements only where state matters. Source-copied into your repo, you own it. Works with any Tailwind v4 project.",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"README.md"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"test": "node --test test/*.test.js"
|
|
22
|
+
"test": "node --test test/*.test.js",
|
|
23
|
+
"test:e2e:touch": "node test/e2e/touch.e2e.mjs"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"commander": "^14.0.0",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
* </ui-alert-dialog-trigger>
|
|
33
33
|
* <ui-alert-dialog-content>
|
|
34
34
|
* <div class=${alertDialogHeaderClass()}>
|
|
35
|
-
* <h2 class=${alertDialogTitleClass()}>Delete account?</h2>
|
|
36
|
-
* <p class=${alertDialogDescriptionClass()}>This cannot be undone.</p>
|
|
35
|
+
* <h2 data-slot="alert-dialog-title" class=${alertDialogTitleClass()}>Delete account?</h2>
|
|
36
|
+
* <p data-slot="alert-dialog-description" class=${alertDialogDescriptionClass()}>This cannot be undone.</p>
|
|
37
37
|
* </div>
|
|
38
38
|
* <div class=${alertDialogFooterClass()}>
|
|
39
39
|
* <ui-alert-dialog-cancel>Cancel</ui-alert-dialog-cancel>
|
|
@@ -63,8 +63,9 @@
|
|
|
63
63
|
*
|
|
64
64
|
* Design tokens used: --background, --border, --muted-foreground.
|
|
65
65
|
*/
|
|
66
|
-
import { WebComponent, html } from '@webjsdev/core';
|
|
66
|
+
import { WebComponent, html, prop } from '@webjsdev/core';
|
|
67
67
|
import { ref, createRef } from '@webjsdev/core/directives';
|
|
68
|
+
import { ensureId, onBeforeCache } from '../lib/utils.ts';
|
|
68
69
|
import { buttonClass, type ButtonVariant, type ButtonSize } from './button.ts';
|
|
69
70
|
|
|
70
71
|
export const alertDialogContentClass = (): string =>
|
|
@@ -86,7 +87,10 @@ ui-alert-dialog:not([open]) ui-alert-dialog-content { display: none !important;
|
|
|
86
87
|
ui-alert-dialog-content { display: grid; }
|
|
87
88
|
`;
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
// Fills the viewport (fixed inset-0), NOT 0x0 (#730): WebKit makes a
|
|
91
|
+
// top-layer <dialog> the containing block for its position:fixed descendants,
|
|
92
|
+
// so a 0x0 host collapsed the fixed content panel to 0x0 (invisible on iOS).
|
|
93
|
+
const NATIVE_DIALOG_CLASS = 'fixed inset-0 border-0 bg-transparent p-0 m-0 max-w-none max-h-none overflow-visible text-inherit backdrop:bg-black/50';
|
|
90
94
|
|
|
91
95
|
function installStyles(): void {
|
|
92
96
|
if (typeof document === 'undefined') return;
|
|
@@ -131,27 +135,30 @@ function unlockScroll(): void {
|
|
|
131
135
|
// content child via prop transitions.
|
|
132
136
|
// --------------------------------------------------------------------------
|
|
133
137
|
|
|
134
|
-
export class UiAlertDialog extends WebComponent
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
};
|
|
138
|
-
declare open: boolean;
|
|
139
|
-
|
|
138
|
+
export class UiAlertDialog extends WebComponent({
|
|
139
|
+
open: prop(Boolean, { reflect: true }),
|
|
140
|
+
}) {
|
|
140
141
|
constructor() {
|
|
141
142
|
super();
|
|
142
143
|
this.open = false;
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
_disposeBeforeCache?: () => void;
|
|
147
|
+
|
|
145
148
|
connectedCallback(): void {
|
|
146
149
|
installStyles();
|
|
147
150
|
// Legacy <ui-alert-dialog-overlay> isn't supported anymore; the native
|
|
148
151
|
// ::backdrop pseudo replaces it. Strip it if a stale doc uses it.
|
|
149
152
|
this.querySelector<HTMLElement>(':scope > ui-alert-dialog-overlay')?.remove();
|
|
150
153
|
super.connectedCallback?.();
|
|
154
|
+
// Close before a back/forward snapshot: a modal restored half-open (the
|
|
155
|
+
// `open` attr without the native showModal top-layer state) is broken (#766).
|
|
156
|
+
this._disposeBeforeCache = onBeforeCache(() => { this.open = false; });
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
disconnectedCallback(): void {
|
|
154
160
|
if (this.open) this._teardown();
|
|
161
|
+
this._disposeBeforeCache?.();
|
|
155
162
|
super.disconnectedCallback?.();
|
|
156
163
|
}
|
|
157
164
|
|
|
@@ -223,12 +230,9 @@ UiAlertDialogTrigger.register('ui-alert-dialog-trigger');
|
|
|
223
230
|
// explicit choice via Cancel / Action).
|
|
224
231
|
// --------------------------------------------------------------------------
|
|
225
232
|
|
|
226
|
-
export class UiAlertDialogContent extends WebComponent
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
};
|
|
230
|
-
declare size: 'default' | 'sm';
|
|
231
|
-
|
|
233
|
+
export class UiAlertDialogContent extends WebComponent({
|
|
234
|
+
size: prop<'default' | 'sm'>(String, { reflect: true }),
|
|
235
|
+
}) {
|
|
232
236
|
// ref to the own-rendered native <dialog>. render() creates it, so a
|
|
233
237
|
// ref() binding is the lit-idiomatic handle (no querySelector against
|
|
234
238
|
// a string selector into the component's own output).
|
|
@@ -239,13 +243,44 @@ export class UiAlertDialogContent extends WebComponent {
|
|
|
239
243
|
this.size = 'default';
|
|
240
244
|
}
|
|
241
245
|
|
|
246
|
+
// Query the native <dialog> rather than the #dialog ref: the ref's `.value`
|
|
247
|
+
// came back null on iOS WebKit (the binding did not populate through SSR
|
|
248
|
+
// hydration), so showModal() never ran (#730). querySelector is robust on
|
|
249
|
+
// every engine.
|
|
250
|
+
_native(): HTMLDialogElement | null {
|
|
251
|
+
return this.querySelector<HTMLDialogElement>('dialog[data-slot="alert-dialog-native"]');
|
|
252
|
+
}
|
|
253
|
+
|
|
242
254
|
showModal(): void {
|
|
243
|
-
|
|
255
|
+
this._wireLabels();
|
|
256
|
+
const native = this._native();
|
|
244
257
|
if (native && !native.open) native.showModal();
|
|
245
258
|
}
|
|
246
259
|
|
|
260
|
+
// Wire the alertdialog's accessible name + description to its title /
|
|
261
|
+
// description nodes at open time (an alert dialog only ever appears via
|
|
262
|
+
// showModal(), so there is no SSR id-stability concern). The title is
|
|
263
|
+
// data-slot="alert-dialog-title" (falling back to the first heading); the
|
|
264
|
+
// description is data-slot="alert-dialog-description" (falling back to the
|
|
265
|
+
// first paragraph). Author-set ARIA always wins. Inlined rather than shared
|
|
266
|
+
// with dialog.ts so `webjs ui add alert-dialog` stays self-contained.
|
|
267
|
+
_wireLabels(): void {
|
|
268
|
+
const panel = this.querySelector('[data-slot="alert-dialog-content"]');
|
|
269
|
+
if (!panel) return;
|
|
270
|
+
const title =
|
|
271
|
+
this.querySelector('[data-slot="alert-dialog-title"]') ?? this.querySelector('h1, h2, h3');
|
|
272
|
+
const desc =
|
|
273
|
+
this.querySelector('[data-slot="alert-dialog-description"]') ?? this.querySelector('p');
|
|
274
|
+
if (title && !panel.hasAttribute('aria-labelledby')) {
|
|
275
|
+
panel.setAttribute('aria-labelledby', ensureId(title as HTMLElement, 'ui-alert-title'));
|
|
276
|
+
}
|
|
277
|
+
if (desc && !panel.hasAttribute('aria-describedby')) {
|
|
278
|
+
panel.setAttribute('aria-describedby', ensureId(desc as HTMLElement, 'ui-alert-desc'));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
247
282
|
close(): void {
|
|
248
|
-
const native = this
|
|
283
|
+
const native = this._native();
|
|
249
284
|
if (native?.open) native.close();
|
|
250
285
|
}
|
|
251
286
|
|
|
@@ -311,14 +346,10 @@ UiAlertDialogContent.register('ui-alert-dialog-content');
|
|
|
311
346
|
|
|
312
347
|
const ALERT_DIALOG_ACTION_GRID_STRETCH = 'group-data-[size=sm]/alert-dialog-content:w-full';
|
|
313
348
|
|
|
314
|
-
export class UiAlertDialogCancel extends WebComponent
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
};
|
|
319
|
-
declare variant: ButtonVariant;
|
|
320
|
-
declare size: ButtonSize;
|
|
321
|
-
|
|
349
|
+
export class UiAlertDialogCancel extends WebComponent({
|
|
350
|
+
variant: prop<ButtonVariant>(String, { reflect: true }),
|
|
351
|
+
size: prop<ButtonSize>(String, { reflect: true }),
|
|
352
|
+
}) {
|
|
322
353
|
constructor() {
|
|
323
354
|
super();
|
|
324
355
|
this.variant = 'outline';
|
|
@@ -338,14 +369,10 @@ export class UiAlertDialogCancel extends WebComponent {
|
|
|
338
369
|
}
|
|
339
370
|
UiAlertDialogCancel.register('ui-alert-dialog-cancel');
|
|
340
371
|
|
|
341
|
-
export class UiAlertDialogAction extends WebComponent
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
};
|
|
346
|
-
declare variant: ButtonVariant;
|
|
347
|
-
declare size: ButtonSize;
|
|
348
|
-
|
|
372
|
+
export class UiAlertDialogAction extends WebComponent({
|
|
373
|
+
variant: prop<ButtonVariant>(String, { reflect: true }),
|
|
374
|
+
size: prop<ButtonSize>(String, { reflect: true }),
|
|
375
|
+
}) {
|
|
349
376
|
constructor() {
|
|
350
377
|
super();
|
|
351
378
|
this.variant = 'default';
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
* </div>
|
|
26
26
|
* </div>
|
|
27
27
|
*
|
|
28
|
+
* A11y (required for accessible output): put role="alert" on the container
|
|
29
|
+
* for an urgent, interrupting message, or role="status" for a polite,
|
|
30
|
+
* non-urgent update. The class helper sets no role, so without one the
|
|
31
|
+
* banner is silent to assistive tech.
|
|
32
|
+
*
|
|
28
33
|
* Design tokens used: --card, --card-foreground, --destructive, --muted-foreground.
|
|
29
34
|
*/
|
|
30
35
|
import { cn } from '../lib/utils.ts';
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* <div class=${avatarGroupCountClass()}>+3</div>
|
|
24
24
|
* </div>
|
|
25
25
|
*
|
|
26
|
+
* A11y (required for accessible output): the <img> MUST have an alt that
|
|
27
|
+
* names the person (alt="Vivek Khandelwal"), or alt="" when a visible text
|
|
28
|
+
* fallback already names them. Always provide the fallback <span> so the
|
|
29
|
+
* avatar is still named if the image fails to load.
|
|
30
|
+
*
|
|
26
31
|
* Design tokens used: --muted, --muted-foreground, --primary, --background.
|
|
27
32
|
*/
|
|
28
33
|
import { cn } from '../lib/utils.ts';
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* The `[a&]:hover:...` hover styles only apply when the element is an `<a>`,
|
|
15
15
|
* so a static `<span>` doesn't pick up an unwanted hover.
|
|
16
16
|
*
|
|
17
|
+
* A11y (required for accessible output): render a static badge as a plain
|
|
18
|
+
* <span> (not focusable, no tabindex). Only an interactive badge (an <a>
|
|
19
|
+
* or <button>) is focusable, and an icon-only one needs an aria-label.
|
|
20
|
+
*
|
|
17
21
|
* Design tokens used: --primary, --secondary, --destructive, --foreground,
|
|
18
22
|
* --accent, --border, --ring.
|
|
19
23
|
*/
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
* </ol>
|
|
27
27
|
* </nav>
|
|
28
28
|
*
|
|
29
|
+
* A11y (required for accessible output): wrap the list in <nav
|
|
30
|
+
* aria-label="breadcrumb">, set aria-current="page" on the current-page
|
|
31
|
+
* element, and mark each separator role="presentation" aria-hidden="true".
|
|
32
|
+
* The class helpers emit none of these.
|
|
33
|
+
*
|
|
29
34
|
* Design tokens used: --muted-foreground, --foreground.
|
|
30
35
|
*/
|
|
31
36
|
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
* shadcn React's `asChild` (Slot) prop has no equivalent here: just call
|
|
18
18
|
* `buttonClass(...)` and spread the classes onto whatever element you want.
|
|
19
19
|
*
|
|
20
|
+
* A11y (required for accessible output): an icon-only button (the `icon`,
|
|
21
|
+
* `icon-xs`, `icon-sm`, `icon-lg` sizes) has no visible text, so it MUST
|
|
22
|
+
* carry an accessible name via aria-label (or aria-labelledby). A button
|
|
23
|
+
* that opens an overlay should also set aria-haspopup and aria-expanded.
|
|
24
|
+
* Native <button> focus and keyboard activation are already correct.
|
|
25
|
+
*
|
|
20
26
|
* Design tokens used: --primary, --primary-foreground, --destructive,
|
|
21
27
|
* --secondary, --secondary-foreground, --accent, --accent-foreground,
|
|
22
28
|
* --background, --input, --ring.
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
* </ui-dialog-trigger>
|
|
30
30
|
* <ui-dialog-content>
|
|
31
31
|
* <div class=${dialogHeaderClass()}>
|
|
32
|
-
* <h2 class=${dialogTitleClass()}>Edit profile</h2>
|
|
33
|
-
* <p class=${dialogDescriptionClass()}>Make changes and click save.</p>
|
|
32
|
+
* <h2 data-slot="dialog-title" class=${dialogTitleClass()}>Edit profile</h2>
|
|
33
|
+
* <p data-slot="dialog-description" class=${dialogDescriptionClass()}>Make changes and click save.</p>
|
|
34
34
|
* </div>
|
|
35
35
|
* <div class="grid gap-3">
|
|
36
36
|
* <label class=${labelClass()} for="dlg-name">Name</label>
|
|
@@ -63,10 +63,33 @@
|
|
|
63
63
|
*
|
|
64
64
|
* Design tokens used: --background, --border, --muted-foreground.
|
|
65
65
|
*/
|
|
66
|
-
import { WebComponent, html, unsafeHTML } from '@webjsdev/core';
|
|
66
|
+
import { WebComponent, html, unsafeHTML, prop } from '@webjsdev/core';
|
|
67
67
|
import { ref, createRef } from '@webjsdev/core/directives';
|
|
68
|
+
import { ensureId, onBeforeCache } from '../lib/utils.ts';
|
|
68
69
|
import { buttonClass } from './button.ts';
|
|
69
70
|
|
|
71
|
+
// Wires a dialog panel's accessible name + description to its title /
|
|
72
|
+
// description nodes. A dialog only ever appears via showModal() (JS), so
|
|
73
|
+
// resolving the relationship at open time is correct and avoids any
|
|
74
|
+
// SSR id-stability concern. The title is the element marked
|
|
75
|
+
// data-slot="dialog-title" (falling back to the first heading); the
|
|
76
|
+
// description is data-slot="dialog-description" (falling back to the first
|
|
77
|
+
// paragraph). Author-set aria-labelledby / aria-describedby always win.
|
|
78
|
+
export function wireDialogLabels(host: Element, panelSelector: string): void {
|
|
79
|
+
const panel = host.querySelector(panelSelector);
|
|
80
|
+
if (!panel) return;
|
|
81
|
+
const title =
|
|
82
|
+
host.querySelector('[data-slot="dialog-title"]') ?? host.querySelector('h1, h2, h3');
|
|
83
|
+
const desc =
|
|
84
|
+
host.querySelector('[data-slot="dialog-description"]') ?? host.querySelector('p');
|
|
85
|
+
if (title && !panel.hasAttribute('aria-labelledby')) {
|
|
86
|
+
panel.setAttribute('aria-labelledby', ensureId(title as HTMLElement, 'ui-dialog-title'));
|
|
87
|
+
}
|
|
88
|
+
if (desc && !panel.hasAttribute('aria-describedby')) {
|
|
89
|
+
panel.setAttribute('aria-describedby', ensureId(desc as HTMLElement, 'ui-dialog-desc'));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
70
93
|
// --------------------------------------------------------------------------
|
|
71
94
|
// Class helpers for subparts.
|
|
72
95
|
// --------------------------------------------------------------------------
|
|
@@ -110,7 +133,16 @@ ui-dialog-content { display: grid; }
|
|
|
110
133
|
// Clears the UA defaults on <dialog> so it becomes an invisible top-layer
|
|
111
134
|
// host. The visible box is rendered by <ui-dialog-content>. The
|
|
112
135
|
// backdrop: variant styles the ::backdrop pseudo-element.
|
|
113
|
-
|
|
136
|
+
//
|
|
137
|
+
// The host FILLS THE VIEWPORT (fixed inset-0) rather than collapsing to
|
|
138
|
+
// 0x0 (#730): WebKit makes a top-layer <dialog> the containing block for
|
|
139
|
+
// its position:fixed descendants, so a 0x0 host collapsed the fixed content
|
|
140
|
+
// panel (w-full) to 0x0 and the dialog was invisible on iOS (Blink resolves
|
|
141
|
+
// the same fixed panel against the viewport, so it worked on Android). A
|
|
142
|
+
// viewport-sized transparent host gives the panel a correct containing block
|
|
143
|
+
// on both engines; the panel still centers itself and the ::backdrop covers
|
|
144
|
+
// the screen.
|
|
145
|
+
const NATIVE_DIALOG_CLASS = 'fixed inset-0 border-0 bg-transparent p-0 m-0 max-w-none max-h-none overflow-visible text-inherit backdrop:bg-black/50';
|
|
114
146
|
|
|
115
147
|
function installStyles(): void {
|
|
116
148
|
if (typeof document === 'undefined') return;
|
|
@@ -157,27 +189,30 @@ function unlockScroll(): void {
|
|
|
157
189
|
// to showModal() / close() its inner <dialog>.
|
|
158
190
|
// --------------------------------------------------------------------------
|
|
159
191
|
|
|
160
|
-
export class UiDialog extends WebComponent
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
164
|
-
declare open: boolean;
|
|
165
|
-
|
|
192
|
+
export class UiDialog extends WebComponent({
|
|
193
|
+
open: prop(Boolean, { reflect: true }),
|
|
194
|
+
}) {
|
|
166
195
|
constructor() {
|
|
167
196
|
super();
|
|
168
197
|
this.open = false;
|
|
169
198
|
}
|
|
170
199
|
|
|
200
|
+
_disposeBeforeCache?: () => void;
|
|
201
|
+
|
|
171
202
|
connectedCallback(): void {
|
|
172
203
|
installStyles();
|
|
173
204
|
// Legacy <ui-dialog-overlay> isn't supported anymore; the native
|
|
174
205
|
// ::backdrop pseudo replaces it. Strip it if a stale doc uses it.
|
|
175
206
|
this.querySelector<HTMLElement>(':scope > ui-dialog-overlay')?.remove();
|
|
176
207
|
super.connectedCallback?.();
|
|
208
|
+
// Close before a back/forward snapshot: a modal restored half-open (the
|
|
209
|
+
// `open` attr without the native showModal top-layer state) is broken (#766).
|
|
210
|
+
this._disposeBeforeCache = onBeforeCache(() => { this.open = false; });
|
|
177
211
|
}
|
|
178
212
|
|
|
179
213
|
disconnectedCallback(): void {
|
|
180
214
|
if (this.open) this._teardown();
|
|
215
|
+
this._disposeBeforeCache?.();
|
|
181
216
|
super.disconnectedCallback?.();
|
|
182
217
|
}
|
|
183
218
|
|
|
@@ -257,12 +292,9 @@ UiDialogTrigger.register('ui-dialog-trigger');
|
|
|
257
292
|
// close button. Exposes showModal() / close() so the parent <ui-dialog>
|
|
258
293
|
// can drive the open state imperatively without a named slot.
|
|
259
294
|
|
|
260
|
-
export class UiDialogContent extends WebComponent
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
};
|
|
264
|
-
declare showCloseButton: string;
|
|
265
|
-
|
|
295
|
+
export class UiDialogContent extends WebComponent({
|
|
296
|
+
showCloseButton: prop(String, { reflect: true, attribute: 'show-close-button' }),
|
|
297
|
+
}) {
|
|
266
298
|
// ref to the own-rendered native <dialog>. render() creates it, so a
|
|
267
299
|
// ref() binding is the lit-idiomatic handle (no querySelector against
|
|
268
300
|
// a string selector into the component's own output).
|
|
@@ -273,13 +305,23 @@ export class UiDialogContent extends WebComponent {
|
|
|
273
305
|
this.showCloseButton = 'true';
|
|
274
306
|
}
|
|
275
307
|
|
|
308
|
+
// Resolve the native <dialog> by query, NOT the #dialog ref: on iOS WebKit
|
|
309
|
+
// the ref's `.value` came back null (the ref binding did not populate through
|
|
310
|
+
// SSR hydration), so showModal() was never called and the dialog never opened
|
|
311
|
+
// (#730). A direct querySelector is robust on every engine, and matches what
|
|
312
|
+
// the tooltip / popover components already do.
|
|
313
|
+
_native(): HTMLDialogElement | null {
|
|
314
|
+
return this.querySelector<HTMLDialogElement>('dialog[data-slot="dialog-native"]');
|
|
315
|
+
}
|
|
316
|
+
|
|
276
317
|
showModal(): void {
|
|
277
|
-
|
|
318
|
+
wireDialogLabels(this, '[data-slot="dialog-content"]');
|
|
319
|
+
const native = this._native();
|
|
278
320
|
if (native && !native.open) native.showModal();
|
|
279
321
|
}
|
|
280
322
|
|
|
281
323
|
close(): void {
|
|
282
|
-
const native = this
|
|
324
|
+
const native = this._native();
|
|
283
325
|
if (native?.open) native.close();
|
|
284
326
|
}
|
|
285
327
|
|
|
@@ -359,12 +401,9 @@ UiDialogClose.register('ui-dialog-close');
|
|
|
359
401
|
// <ui-dialog-footer>
|
|
360
402
|
// --------------------------------------------------------------------------
|
|
361
403
|
|
|
362
|
-
export class UiDialogFooter extends WebComponent
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
};
|
|
366
|
-
declare showCloseButton: string | null;
|
|
367
|
-
|
|
404
|
+
export class UiDialogFooter extends WebComponent({
|
|
405
|
+
showCloseButton: prop<string | null>(String, { attribute: 'show-close-button' }),
|
|
406
|
+
}) {
|
|
368
407
|
constructor() {
|
|
369
408
|
super();
|
|
370
409
|
this.showCloseButton = null;
|