@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
|
@@ -43,8 +43,11 @@
|
|
|
43
43
|
* Design tokens used: --muted, --muted-foreground, --foreground, --background,
|
|
44
44
|
* --input, --ring.
|
|
45
45
|
*/
|
|
46
|
-
import { WebComponent, html } from '@webjsdev/core';
|
|
47
|
-
import { cn } from '../lib/utils.ts';
|
|
46
|
+
import { WebComponent, html, prop } from '@webjsdev/core';
|
|
47
|
+
import { cn, ensureId } from '../lib/utils.ts';
|
|
48
|
+
|
|
49
|
+
// A tab `value` becomes part of an id, so reduce it to id-safe characters.
|
|
50
|
+
const idSafe = (s: string): string => s.replace(/[^A-Za-z0-9_-]/g, '-');
|
|
48
51
|
|
|
49
52
|
// --------------------------------------------------------------------------
|
|
50
53
|
// Class helpers
|
|
@@ -92,20 +95,39 @@ const TABS_CONTENT_CLASS = 'flex-1 outline-none';
|
|
|
92
95
|
// requestUpdate() to re-render against the new parent value.
|
|
93
96
|
// --------------------------------------------------------------------------
|
|
94
97
|
|
|
95
|
-
export class UiTabs extends WebComponent
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
};
|
|
100
|
-
declare value: string;
|
|
101
|
-
declare orientation: 'horizontal' | 'vertical';
|
|
102
|
-
|
|
98
|
+
export class UiTabs extends WebComponent({
|
|
99
|
+
value: prop(String, { reflect: true }),
|
|
100
|
+
orientation: prop<'horizontal' | 'vertical'>(String, { reflect: true }),
|
|
101
|
+
}) {
|
|
103
102
|
constructor() {
|
|
104
103
|
super();
|
|
105
104
|
this.value = '';
|
|
106
105
|
this.orientation = 'horizontal';
|
|
107
106
|
}
|
|
108
107
|
|
|
108
|
+
// Assign the scope id BEFORE render so it is serialized onto this host's
|
|
109
|
+
// opening tag at SSR (willUpdate now runs server-side). Without this, the id
|
|
110
|
+
// was minted lazily during a CHILD trigger's render, AFTER this host's tag
|
|
111
|
+
// was already serialized without an id, so the host shipped no id; on
|
|
112
|
+
// hydration the client re-minted a DIFFERENT counter id and the trigger /
|
|
113
|
+
// panel ids mismatched SSR vs client, breaking hydration (#730).
|
|
114
|
+
willUpdate(): void {
|
|
115
|
+
ensureId(this, 'ui-tabs');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Stable per-instance scope so a trigger and its panel agree on the
|
|
119
|
+
// shared id stem (`<scope>-trigger-<value>` / `<scope>-panel-<value>`)
|
|
120
|
+
// even when several tab sets reuse the same `value` names on one page.
|
|
121
|
+
get scopeId(): string {
|
|
122
|
+
return ensureId(this, 'ui-tabs');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Builds the trigger / panel id pair a child derives from its `value`.
|
|
126
|
+
idsFor(value: string): { trigger: string; panel: string } {
|
|
127
|
+
const v = idSafe(value);
|
|
128
|
+
return { trigger: `${this.scopeId}-trigger-${v}`, panel: `${this.scopeId}-panel-${v}` };
|
|
129
|
+
}
|
|
130
|
+
|
|
109
131
|
render() {
|
|
110
132
|
return html`<div
|
|
111
133
|
data-slot="tabs"
|
|
@@ -130,7 +152,7 @@ export class UiTabs extends WebComponent {
|
|
|
130
152
|
|
|
131
153
|
_broadcast(): void {
|
|
132
154
|
this.querySelectorAll<WebComponent>(
|
|
133
|
-
'ui-tabs-trigger, ui-tabs-content',
|
|
155
|
+
'ui-tabs-list, ui-tabs-trigger, ui-tabs-content',
|
|
134
156
|
).forEach((el) => el.requestUpdate?.());
|
|
135
157
|
}
|
|
136
158
|
}
|
|
@@ -140,21 +162,25 @@ UiTabs.register('ui-tabs');
|
|
|
140
162
|
// <ui-tabs-list>
|
|
141
163
|
// --------------------------------------------------------------------------
|
|
142
164
|
|
|
143
|
-
export class UiTabsList extends WebComponent
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
};
|
|
147
|
-
declare variant: TabsListVariant;
|
|
148
|
-
|
|
165
|
+
export class UiTabsList extends WebComponent({
|
|
166
|
+
variant: prop<TabsListVariant>(String, { reflect: true }),
|
|
167
|
+
}) {
|
|
149
168
|
constructor() {
|
|
150
169
|
super();
|
|
151
170
|
this.variant = 'default';
|
|
152
171
|
}
|
|
153
172
|
|
|
173
|
+
get _tabs(): UiTabs | null {
|
|
174
|
+
if (typeof this.closest !== 'function') return null;
|
|
175
|
+
return this.closest('ui-tabs') as UiTabs | null;
|
|
176
|
+
}
|
|
177
|
+
|
|
154
178
|
render() {
|
|
179
|
+
const orientation = this._tabs?.orientation ?? 'horizontal';
|
|
155
180
|
return html`<div
|
|
156
181
|
data-slot="tabs-list"
|
|
157
182
|
role="tablist"
|
|
183
|
+
aria-orientation=${orientation}
|
|
158
184
|
data-variant=${this.variant}
|
|
159
185
|
class=${tabsListClass({ variant: this.variant })}
|
|
160
186
|
><slot></slot></div>`;
|
|
@@ -166,12 +192,9 @@ UiTabsList.register('ui-tabs-list');
|
|
|
166
192
|
// <ui-tabs-trigger value="...">
|
|
167
193
|
// --------------------------------------------------------------------------
|
|
168
194
|
|
|
169
|
-
export class UiTabsTrigger extends WebComponent
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
};
|
|
173
|
-
declare value: string;
|
|
174
|
-
|
|
195
|
+
export class UiTabsTrigger extends WebComponent({
|
|
196
|
+
value: prop(String, { reflect: true }),
|
|
197
|
+
}) {
|
|
175
198
|
constructor() {
|
|
176
199
|
super();
|
|
177
200
|
this.value = '';
|
|
@@ -188,10 +211,13 @@ export class UiTabsTrigger extends WebComponent {
|
|
|
188
211
|
render() {
|
|
189
212
|
const tabs = this._tabs;
|
|
190
213
|
const active = !!tabs && tabs.value === this.value && this.value !== '';
|
|
214
|
+
const ids = tabs && this.value ? tabs.idsFor(this.value) : null;
|
|
191
215
|
return html`<button
|
|
192
216
|
type="button"
|
|
193
217
|
role="tab"
|
|
194
218
|
data-slot="tabs-trigger"
|
|
219
|
+
id=${ids ? ids.trigger : ''}
|
|
220
|
+
aria-controls=${ids ? ids.panel : ''}
|
|
195
221
|
data-state=${active ? 'active' : 'inactive'}
|
|
196
222
|
aria-selected=${String(active)}
|
|
197
223
|
tabindex=${active ? '0' : '-1'}
|
|
@@ -235,12 +261,9 @@ UiTabsTrigger.register('ui-tabs-trigger');
|
|
|
235
261
|
// <ui-tabs-content value="...">
|
|
236
262
|
// --------------------------------------------------------------------------
|
|
237
263
|
|
|
238
|
-
export class UiTabsContent extends WebComponent
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
};
|
|
242
|
-
declare value: string;
|
|
243
|
-
|
|
264
|
+
export class UiTabsContent extends WebComponent({
|
|
265
|
+
value: prop(String, { reflect: true }),
|
|
266
|
+
}) {
|
|
244
267
|
constructor() {
|
|
245
268
|
super();
|
|
246
269
|
this.value = '';
|
|
@@ -254,14 +277,20 @@ export class UiTabsContent extends WebComponent {
|
|
|
254
277
|
render() {
|
|
255
278
|
const tabs = this._tabs;
|
|
256
279
|
const active = !!tabs && tabs.value === this.value && this.value !== '';
|
|
280
|
+
const ids = tabs && this.value ? tabs.idsFor(this.value) : null;
|
|
257
281
|
// The host needs to be hidden when inactive so its rendered <section>
|
|
258
282
|
// is removed from layout (light DOM has no :host CSS to scope this).
|
|
259
283
|
// Use the native `hidden` IDL property rather than imperative
|
|
260
|
-
// setAttribute, so it reads as a property assignment.
|
|
284
|
+
// setAttribute, so it reads as a property assignment. `inert` on the
|
|
285
|
+
// host additionally pulls an inactive panel (and anything focusable
|
|
286
|
+
// inside it) out of the tab order and the accessibility tree.
|
|
261
287
|
this.hidden = !active;
|
|
288
|
+
this.inert = !active;
|
|
262
289
|
return html`<section
|
|
263
290
|
data-slot="tabs-content"
|
|
264
291
|
role="tabpanel"
|
|
292
|
+
id=${ids ? ids.panel : ''}
|
|
293
|
+
aria-labelledby=${ids ? ids.trigger : ''}
|
|
265
294
|
tabindex="0"
|
|
266
295
|
data-state=${active ? 'active' : 'inactive'}
|
|
267
296
|
class=${TABS_CONTENT_CLASS}
|
|
@@ -41,12 +41,14 @@
|
|
|
41
41
|
* Events:
|
|
42
42
|
* `ui-value-change` on <ui-toggle-group>: `{ detail: { value } }` after selection changes.
|
|
43
43
|
*
|
|
44
|
-
* Keyboard:
|
|
44
|
+
* Keyboard: Arrow keys move focus between items (roving tabindex, so the
|
|
45
|
+
* group is a single Tab stop), Home / End jump to the first / last item, and
|
|
46
|
+
* Enter / Space toggles the focused item.
|
|
45
47
|
*
|
|
46
48
|
* Design tokens used: inherited from toggleClass (--muted, --accent, --ring,
|
|
47
49
|
* --input, --destructive).
|
|
48
50
|
*/
|
|
49
|
-
import { WebComponent, html } from '@webjsdev/core';
|
|
51
|
+
import { WebComponent, html, prop } from '@webjsdev/core';
|
|
50
52
|
import { cn } from '../lib/utils.ts';
|
|
51
53
|
import { toggleClass, type ToggleVariant, type ToggleSize } from './toggle.ts';
|
|
52
54
|
|
|
@@ -66,22 +68,14 @@ const ITEM_EXTRA =
|
|
|
66
68
|
// their own renders before we read / write their state.
|
|
67
69
|
// --------------------------------------------------------------------------
|
|
68
70
|
|
|
69
|
-
export class UiToggleGroup extends WebComponent
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
};
|
|
78
|
-
declare value: string;
|
|
79
|
-
declare type: 'single' | 'multiple';
|
|
80
|
-
declare variant: ToggleVariant;
|
|
81
|
-
declare size: ToggleSize;
|
|
82
|
-
declare spacing: string;
|
|
83
|
-
declare orientation: 'horizontal' | 'vertical';
|
|
84
|
-
|
|
71
|
+
export class UiToggleGroup extends WebComponent({
|
|
72
|
+
value: prop(String, { reflect: true }),
|
|
73
|
+
type: prop<'single' | 'multiple'>(String, { reflect: true }),
|
|
74
|
+
variant: prop<ToggleVariant>(String, { reflect: true }),
|
|
75
|
+
size: prop<ToggleSize>(String, { reflect: true }),
|
|
76
|
+
spacing: prop(String, { reflect: true }),
|
|
77
|
+
orientation: prop<'horizontal' | 'vertical'>(String, { reflect: true }),
|
|
78
|
+
}) {
|
|
85
79
|
constructor() {
|
|
86
80
|
super();
|
|
87
81
|
this.value = '';
|
|
@@ -117,9 +111,15 @@ export class UiToggleGroup extends WebComponent {
|
|
|
117
111
|
queueMicrotask(() => this._reflectItems());
|
|
118
112
|
}
|
|
119
113
|
|
|
114
|
+
_items(): UiToggleGroupItem[] {
|
|
115
|
+
return Array.from(this.querySelectorAll<UiToggleGroupItem>('ui-toggle-group-item'));
|
|
116
|
+
}
|
|
117
|
+
|
|
120
118
|
_reflectItems(): void {
|
|
121
119
|
const values = this._values;
|
|
122
|
-
this.
|
|
120
|
+
const items = this._items();
|
|
121
|
+
const current = items.find((el) => el.tabIndex === 0);
|
|
122
|
+
items.forEach((item) => {
|
|
123
123
|
const on = !!item.value && values.has(item.value);
|
|
124
124
|
// Reflect both on the host (for CSS sibling selectors like
|
|
125
125
|
// data-[spacing=0]:first:rounded-l-md that need to target the host
|
|
@@ -127,6 +127,31 @@ export class UiToggleGroup extends WebComponent {
|
|
|
127
127
|
// item's render() refreshes its inner styling.
|
|
128
128
|
item.pressed = on;
|
|
129
129
|
});
|
|
130
|
+
this._roving(items, current);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Roving tabindex (APG): exactly one item is in the tab order. Prefer the
|
|
134
|
+
// currently-focused item, then whichever was tabbable, then the first
|
|
135
|
+
// selected item, then the first item. Arrow keys move focus and shift the
|
|
136
|
+
// single tabbable slot via `focusItem`.
|
|
137
|
+
_roving(items: UiToggleGroupItem[], current?: UiToggleGroupItem): void {
|
|
138
|
+
if (!items.length) return;
|
|
139
|
+
const values = this._values;
|
|
140
|
+
const active =
|
|
141
|
+
typeof document !== 'undefined' ? (document.activeElement as Element | null) : null;
|
|
142
|
+
const focused = active ? items.find((i) => i === active) : undefined;
|
|
143
|
+
const selected = items.find((i) => !!i.value && values.has(i.value));
|
|
144
|
+
const tabbable = focused ?? current ?? selected ?? items[0];
|
|
145
|
+
items.forEach((item) => {
|
|
146
|
+
item.tabIndex = item === tabbable ? 0 : -1;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
focusItem(item: UiToggleGroupItem): void {
|
|
151
|
+
this._items().forEach((el) => {
|
|
152
|
+
el.tabIndex = el === item ? 0 : -1;
|
|
153
|
+
});
|
|
154
|
+
item.focus();
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
_onItemClick = (e: Event): void => {
|
|
@@ -159,14 +184,10 @@ UiToggleGroup.register('ui-toggle-group');
|
|
|
159
184
|
// Tailwind variant selectors on the joined-spacing rounded corners).
|
|
160
185
|
// --------------------------------------------------------------------------
|
|
161
186
|
|
|
162
|
-
export class UiToggleGroupItem extends WebComponent
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
};
|
|
167
|
-
declare value: string;
|
|
168
|
-
declare pressed: boolean;
|
|
169
|
-
|
|
187
|
+
export class UiToggleGroupItem extends WebComponent({
|
|
188
|
+
value: prop(String, { reflect: true }),
|
|
189
|
+
pressed: prop(Boolean, { reflect: true }),
|
|
190
|
+
}) {
|
|
170
191
|
constructor() {
|
|
171
192
|
super();
|
|
172
193
|
this.value = '';
|
|
@@ -193,7 +214,10 @@ export class UiToggleGroupItem extends WebComponent {
|
|
|
193
214
|
connectedCallback(): void {
|
|
194
215
|
this.dataset.slot = 'toggle-group-item';
|
|
195
216
|
this.role = 'button';
|
|
196
|
-
|
|
217
|
+
// Roving tabindex: start outside the tab order. The group promotes
|
|
218
|
+
// exactly one item to tabindex 0 in _reflectItems (runs after first
|
|
219
|
+
// render); Arrow keys move focus and the tabbable slot from there.
|
|
220
|
+
this.tabIndex = -1;
|
|
197
221
|
this.addEventListener('click', this._onClick);
|
|
198
222
|
this.addEventListener('keydown', this._onKeyDown);
|
|
199
223
|
super.connectedCallback?.();
|
|
@@ -230,6 +254,24 @@ export class UiToggleGroupItem extends WebComponent {
|
|
|
230
254
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
231
255
|
e.preventDefault();
|
|
232
256
|
this._onClick();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const group = this._group;
|
|
260
|
+
if (!group) return;
|
|
261
|
+
const horizontal = group.orientation !== 'vertical';
|
|
262
|
+
const nextKey = horizontal ? 'ArrowRight' : 'ArrowDown';
|
|
263
|
+
const prevKey = horizontal ? 'ArrowLeft' : 'ArrowUp';
|
|
264
|
+
const items = group._items();
|
|
265
|
+
const idx = items.indexOf(this);
|
|
266
|
+
if (idx === -1) return;
|
|
267
|
+
let target: UiToggleGroupItem | null = null;
|
|
268
|
+
if (e.key === nextKey) target = items[(idx + 1) % items.length] ?? null;
|
|
269
|
+
else if (e.key === prevKey) target = items[(idx - 1 + items.length) % items.length] ?? null;
|
|
270
|
+
else if (e.key === 'Home') target = items[0] ?? null;
|
|
271
|
+
else if (e.key === 'End') target = items[items.length - 1] ?? null;
|
|
272
|
+
if (target) {
|
|
273
|
+
e.preventDefault();
|
|
274
|
+
group.focusItem(target);
|
|
233
275
|
}
|
|
234
276
|
};
|
|
235
277
|
}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* Design tokens used: --muted, --muted-foreground, --accent, --accent-foreground,
|
|
39
39
|
* --input, --background, --ring, --destructive.
|
|
40
40
|
*/
|
|
41
|
-
import { WebComponent, html } from '@webjsdev/core';
|
|
41
|
+
import { WebComponent, html, prop } from '@webjsdev/core';
|
|
42
42
|
import { cn } from '../lib/utils.ts';
|
|
43
43
|
|
|
44
44
|
// cursor-pointer + select-none on BASE for both call sites: the
|
|
@@ -76,18 +76,12 @@ export function toggleClass(opts: { variant?: ToggleVariant; size?: ToggleSize }
|
|
|
76
76
|
// project through the default slot inside the inner button.
|
|
77
77
|
// --------------------------------------------------------------------------
|
|
78
78
|
|
|
79
|
-
export class UiToggle extends WebComponent
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
};
|
|
86
|
-
declare pressed: boolean;
|
|
87
|
-
declare variant: ToggleVariant;
|
|
88
|
-
declare size: ToggleSize;
|
|
89
|
-
declare disabled: boolean;
|
|
90
|
-
|
|
79
|
+
export class UiToggle extends WebComponent({
|
|
80
|
+
pressed: prop(Boolean, { reflect: true }),
|
|
81
|
+
variant: prop<ToggleVariant>(String, { reflect: true }),
|
|
82
|
+
size: prop<ToggleSize>(String, { reflect: true }),
|
|
83
|
+
disabled: prop(Boolean, { reflect: true }),
|
|
84
|
+
}) {
|
|
91
85
|
constructor() {
|
|
92
86
|
super();
|
|
93
87
|
this.pressed = false;
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
*
|
|
41
41
|
* Design tokens used: --foreground, --background.
|
|
42
42
|
*/
|
|
43
|
-
import { WebComponent, html } from '@webjsdev/core';
|
|
43
|
+
import { WebComponent, html, prop } from '@webjsdev/core';
|
|
44
|
+
import { ensureId, onBeforeCache } from '../lib/utils.ts';
|
|
44
45
|
import { positionFloating, type PopoverSide, type PopoverAlign } from './popover.ts';
|
|
45
46
|
|
|
46
47
|
// UA `[popover]` defaults paint a bordered, padded panel centered with
|
|
@@ -61,18 +62,13 @@ let lastTooltipHideAt = 0;
|
|
|
61
62
|
// <ui-tooltip>
|
|
62
63
|
// --------------------------------------------------------------------------
|
|
63
64
|
|
|
64
|
-
export class UiTooltip extends WebComponent
|
|
65
|
-
|
|
66
|
-
open: { type: Boolean, reflect: true },
|
|
67
|
-
delayDuration: { type: Number },
|
|
68
|
-
skipDelayDuration: { type: Number },
|
|
69
|
-
};
|
|
70
|
-
declare open: boolean;
|
|
65
|
+
export class UiTooltip extends WebComponent({
|
|
66
|
+
open: prop(Boolean, { reflect: true }),
|
|
71
67
|
// `delayDuration` / `skipDelayDuration` ride the `delay-duration` /
|
|
72
68
|
// `skip-delay-duration` attributes (shadcn parity), read as typed props.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
delayDuration: Number,
|
|
70
|
+
skipDelayDuration: Number,
|
|
71
|
+
}) {
|
|
76
72
|
_showTimer: number | undefined;
|
|
77
73
|
_hideTimer: number | undefined;
|
|
78
74
|
|
|
@@ -83,6 +79,45 @@ export class UiTooltip extends WebComponent {
|
|
|
83
79
|
this.skipDelayDuration = 300;
|
|
84
80
|
}
|
|
85
81
|
|
|
82
|
+
_disposeBeforeCache?: () => void;
|
|
83
|
+
|
|
84
|
+
connectedCallback(): void {
|
|
85
|
+
super.connectedCallback?.();
|
|
86
|
+
// webjs projects slotted light-DOM children in a pass after the first
|
|
87
|
+
// render, so the trigger's focusable control is not in place yet at
|
|
88
|
+
// connect / firstUpdated. Defer to the next frame, by which point the
|
|
89
|
+
// projection has run and both the control and the tip content exist.
|
|
90
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
91
|
+
requestAnimationFrame(() => this._wireAria());
|
|
92
|
+
}
|
|
93
|
+
// Close before a back/forward snapshot so it does not restore open (#766).
|
|
94
|
+
this._disposeBeforeCache = onBeforeCache(() => { this.open = false; });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
disconnectedCallback(): void {
|
|
98
|
+
this._disposeBeforeCache?.();
|
|
99
|
+
super.disconnectedCallback?.();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// APG tooltip wiring: the focusable trigger references the tip via
|
|
103
|
+
// aria-describedby, so a screen reader appends the tip text to the
|
|
104
|
+
// trigger's own name ("Help, button, Helpful tip"). The tip is JS-driven,
|
|
105
|
+
// so doing this at runtime is correct.
|
|
106
|
+
_wireAria(): void {
|
|
107
|
+
const triggerHost = this.querySelector('ui-tooltip-trigger');
|
|
108
|
+
const content = this.querySelector<HTMLElement>('ui-tooltip-content [role="tooltip"]');
|
|
109
|
+
if (!triggerHost || !content) return;
|
|
110
|
+
const control =
|
|
111
|
+
triggerHost.querySelector<HTMLElement>('button, a[href], [tabindex], [role="button"]') ??
|
|
112
|
+
(triggerHost as HTMLElement);
|
|
113
|
+
const id = ensureId(content, 'ui-tooltip');
|
|
114
|
+
const existing = control.getAttribute('aria-describedby');
|
|
115
|
+
if (!existing) control.setAttribute('aria-describedby', id);
|
|
116
|
+
else if (!existing.split(/\s+/).includes(id)) {
|
|
117
|
+
control.setAttribute('aria-describedby', `${existing} ${id}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
86
121
|
// Back-compat getter for tests + external code that read `el.isOpen`
|
|
87
122
|
// alongside the reactive `open` prop.
|
|
88
123
|
get isOpen(): boolean { return this.open; }
|
|
@@ -100,6 +100,33 @@ function variantPrefix(token: string): string {
|
|
|
100
100
|
return i === -1 ? '' : token.slice(0, i + 1);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Stable DOM ids for wiring ARIA relationships (aria-controls /
|
|
105
|
+
// aria-labelledby / aria-describedby) between sibling light-DOM nodes.
|
|
106
|
+
// A monotonic counter is fine for uniqueness within a document: the id is
|
|
107
|
+
// only consumed as an attribute value, never persisted. When SSR emits an
|
|
108
|
+
// id on a host element, the upgraded element reuses it (ensureId is a no-op
|
|
109
|
+
// when an id is already present), so the server and client agree.
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
let _idSeq = 0;
|
|
113
|
+
|
|
114
|
+
/** A fresh, document-unique id string with a readable prefix. */
|
|
115
|
+
export function domId(prefix = 'ui'): string {
|
|
116
|
+
_idSeq += 1;
|
|
117
|
+
return `${prefix}-${_idSeq}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Returns `el.id`, assigning a generated one (prefix-based) when absent.
|
|
122
|
+
* Idempotent, so an id already present (author-set, or carried over from
|
|
123
|
+
* SSR) is reused unchanged.
|
|
124
|
+
*/
|
|
125
|
+
export function ensureId(el: { id: string }, prefix = 'ui'): string {
|
|
126
|
+
if (!el.id) el.id = domId(prefix);
|
|
127
|
+
return el.id;
|
|
128
|
+
}
|
|
129
|
+
|
|
103
130
|
// ---------------------------------------------------------------------------
|
|
104
131
|
// Custom-element base: SSR-safe. In the browser `Base = HTMLElement`. On
|
|
105
132
|
// the server (Node, during SSR) `HTMLElement` is undefined; we substitute
|
|
@@ -239,3 +266,19 @@ export const helpClass = () => 'text-xs text-muted-foreground';
|
|
|
239
266
|
|
|
240
267
|
/** Validation error text: replaces hint when the field is invalid. */
|
|
241
268
|
export const errorClass = () => 'text-sm font-medium text-destructive';
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Run `reset` just before the webjs client router snapshots the page into its
|
|
272
|
+
* back/forward cache (the `webjs:before-cache` event). Transient overlays use
|
|
273
|
+
* this to close themselves so a restored snapshot is clean rather than, say, a
|
|
274
|
+
* hover-card frozen open after Back then Forward (#766). Wire it in
|
|
275
|
+
* `connectedCallback` and call the returned disposer in `disconnectedCallback`
|
|
276
|
+
* so the listener never leaks across soft navigations.
|
|
277
|
+
*
|
|
278
|
+
* SSR-safe: a no-op when there is no `document` (server / no client router).
|
|
279
|
+
*/
|
|
280
|
+
export function onBeforeCache(reset: () => void): () => void {
|
|
281
|
+
if (typeof document === 'undefined') return () => {};
|
|
282
|
+
document.addEventListener('webjs:before-cache', reset);
|
|
283
|
+
return () => document.removeEventListener('webjs:before-cache', reset);
|
|
284
|
+
}
|
|
@@ -188,7 +188,8 @@
|
|
|
188
188
|
"name": "dialog",
|
|
189
189
|
"type": "registry:ui",
|
|
190
190
|
"registryDependencies": [
|
|
191
|
-
"lib-utils"
|
|
191
|
+
"lib-utils",
|
|
192
|
+
"button"
|
|
192
193
|
],
|
|
193
194
|
"dependencies": [
|
|
194
195
|
"@webjsdev/core"
|
|
@@ -203,6 +204,9 @@
|
|
|
203
204
|
{
|
|
204
205
|
"name": "dropdown-menu",
|
|
205
206
|
"type": "registry:ui",
|
|
207
|
+
"registryDependencies": [
|
|
208
|
+
"popover"
|
|
209
|
+
],
|
|
206
210
|
"dependencies": [
|
|
207
211
|
"@webjsdev/core"
|
|
208
212
|
],
|
|
@@ -216,6 +220,9 @@
|
|
|
216
220
|
{
|
|
217
221
|
"name": "hover-card",
|
|
218
222
|
"type": "registry:ui",
|
|
223
|
+
"registryDependencies": [
|
|
224
|
+
"popover"
|
|
225
|
+
],
|
|
219
226
|
"dependencies": [
|
|
220
227
|
"@webjsdev/core"
|
|
221
228
|
],
|
|
@@ -452,6 +459,9 @@
|
|
|
452
459
|
{
|
|
453
460
|
"name": "toggle-group",
|
|
454
461
|
"type": "registry:ui",
|
|
462
|
+
"registryDependencies": [
|
|
463
|
+
"toggle"
|
|
464
|
+
],
|
|
455
465
|
"dependencies": [
|
|
456
466
|
"@webjsdev/core"
|
|
457
467
|
],
|
|
@@ -465,6 +475,9 @@
|
|
|
465
475
|
{
|
|
466
476
|
"name": "tooltip",
|
|
467
477
|
"type": "registry:ui",
|
|
478
|
+
"registryDependencies": [
|
|
479
|
+
"popover"
|
|
480
|
+
],
|
|
468
481
|
"dependencies": [
|
|
469
482
|
"@webjsdev/core"
|
|
470
483
|
],
|