@adia-ai/web-components 0.6.34 → 0.6.35
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/CHANGELOG.md +42 -0
- package/color/index.js +1 -1
- package/components/accordion/accordion-item.yaml +2 -2
- package/components/accordion/accordion.js +1 -1
- package/components/action-list/action-item.yaml +2 -2
- package/components/action-list/action-list.js +1 -1
- package/components/agent-artifact/{class.js → agent-artifact.class.js} +1 -1
- package/components/agent-artifact/agent-artifact.js +1 -1
- package/components/agent-feedback-bar/agent-feedback-bar.js +1 -1
- package/components/agent-questions/agent-questions.js +1 -1
- package/components/agent-reasoning/agent-reasoning.js +1 -1
- package/components/agent-suggestions/agent-suggestions.js +1 -1
- package/components/alert/alert.a2ui.json +64 -1
- package/components/alert/{class.js → alert.class.js} +189 -2
- package/components/alert/alert.css +78 -0
- package/components/alert/alert.d.ts +14 -0
- package/components/alert/alert.js +1 -1
- package/components/alert/alert.test.js +184 -0
- package/components/alert/alert.yaml +114 -1
- package/components/avatar/avatar-group.yaml +2 -2
- package/components/avatar/avatar.js +1 -1
- package/components/badge/badge.js +1 -1
- package/components/block/block.js +1 -1
- package/components/breadcrumb/breadcrumb.js +1 -1
- package/components/button/button.js +1 -1
- package/components/calendar-grid/calendar-grid.a2ui.json +10 -0
- package/components/calendar-grid/{class.js → calendar-grid.class.js} +30 -4
- package/components/calendar-grid/calendar-grid.css +20 -0
- package/components/calendar-grid/calendar-grid.d.ts +4 -0
- package/components/calendar-grid/calendar-grid.js +1 -1
- package/components/calendar-grid/calendar-grid.yaml +20 -0
- package/components/calendar-picker/calendar-picker.js +1 -1
- package/components/card/card.js +1 -1
- package/components/chart/chart.js +1 -1
- package/components/chart-legend/chart-legend.js +1 -1
- package/components/chat-thread/chat-input.a2ui.json +1 -1
- package/components/chat-thread/chat-input.js +6 -1
- package/components/chat-thread/chat-input.yaml +4 -1
- package/components/chat-thread/chat-thread.js +1 -1
- package/components/check/check.js +1 -1
- package/components/code/code.js +1 -1
- package/components/col/col.js +1 -1
- package/components/color-input/color-input.js +1 -1
- package/components/color-picker/color-picker.js +1 -1
- package/components/combobox/combobox.js +1 -1
- package/components/command/command.js +1 -1
- package/components/date-range-picker/{class.js → date-range-picker.class.js} +18 -2
- package/components/date-range-picker/date-range-picker.css +51 -5
- package/components/date-range-picker/date-range-picker.js +1 -1
- package/components/datetime-picker/{class.js → datetime-picker.class.js} +1 -1
- package/components/datetime-picker/datetime-picker.js +1 -1
- package/components/demo-toggle/demo-toggle.js +1 -1
- package/components/description-list/description-list.js +1 -1
- package/components/divider/divider.js +1 -1
- package/components/drawer/drawer.js +1 -1
- package/components/embed/embed.js +1 -1
- package/components/empty-state/empty-state.js +1 -1
- package/components/feed/feed.js +1 -1
- package/components/field/field.js +1 -1
- package/components/field/field.test.js +1 -1
- package/components/fields/fields.js +1 -1
- package/components/grid/grid.js +1 -1
- package/components/heatmap/heatmap.js +1 -1
- package/components/icon/icon.js +1 -1
- package/components/image/image.js +1 -1
- package/components/index.js +3 -0
- package/components/inline-message/inline-message.a2ui.json +143 -0
- package/components/inline-message/inline-message.class.js +169 -0
- package/components/inline-message/inline-message.css +75 -0
- package/components/inline-message/inline-message.d.ts +31 -0
- package/components/inline-message/inline-message.examples.md +19 -0
- package/components/inline-message/inline-message.js +17 -0
- package/components/inline-message/inline-message.test.js +203 -0
- package/components/inline-message/inline-message.yaml +205 -0
- package/components/input/input.css +1 -1
- package/components/input/input.js +1 -1
- package/components/input/input.yaml +5 -4
- package/components/inspector/inspector.js +1 -1
- package/components/integration-card/integration-card.js +1 -1
- package/components/kbd/kbd.js +1 -1
- package/components/link/link.js +1 -1
- package/components/list/list-item.yaml +2 -2
- package/components/list/list.js +1 -1
- package/components/list-window/list-window.js +1 -1
- package/components/loading-overlay/loading-overlay.a2ui.json +176 -0
- package/components/loading-overlay/loading-overlay.class.js +203 -0
- package/components/loading-overlay/loading-overlay.css +81 -0
- package/components/loading-overlay/loading-overlay.d.ts +24 -0
- package/components/loading-overlay/loading-overlay.examples.md +50 -0
- package/components/loading-overlay/loading-overlay.js +17 -0
- package/components/loading-overlay/loading-overlay.test.js +257 -0
- package/components/loading-overlay/loading-overlay.yaml +260 -0
- package/components/menu/menu-divider.yaml +1 -1
- package/components/menu/menu-item.yaml +1 -1
- package/components/menu/menu.a2ui.json +3 -0
- package/components/menu/menu.js +1 -1
- package/components/menu/menu.yaml +7 -0
- package/components/modal/{class.js → modal.class.js} +12 -1
- package/components/modal/modal.css +11 -1
- package/components/modal/modal.js +1 -1
- package/components/nav/nav.js +1 -1
- package/components/nav-group/nav-group.js +1 -1
- package/components/nav-item/nav-item.js +1 -1
- package/components/noodles/noodles.js +1 -1
- package/components/option-card/option-card.js +1 -1
- package/components/otp-input/otp-input.js +1 -1
- package/components/page/page.js +1 -1
- package/components/pagination/pagination.js +1 -1
- package/components/pane/pane.js +1 -1
- package/components/pipeline-status/pipeline-status.js +1 -1
- package/components/popover/popover.a2ui.json +8 -1
- package/components/popover/popover.js +1 -1
- package/components/popover/popover.yaml +14 -1
- package/components/progress/progress.js +1 -1
- package/components/progress-row/progress-row.js +1 -1
- package/components/radio/radio.js +1 -1
- package/components/range/range.js +1 -1
- package/components/rating/rating.js +1 -1
- package/components/richtext/richtext.js +1 -1
- package/components/row/row.js +1 -1
- package/components/search/search.js +1 -1
- package/components/segment/segment.js +1 -1
- package/components/segmented/segmented.js +1 -1
- package/components/select/select.a2ui.json +58 -4
- package/components/select/{class.js → select.class.js} +415 -6
- package/components/select/select.css +158 -0
- package/components/select/select.d.ts +31 -1
- package/components/select/select.js +1 -1
- package/components/select/select.test.js +202 -0
- package/components/select/select.yaml +126 -5
- package/components/skeleton/skeleton.js +1 -1
- package/components/slider/slider.js +1 -1
- package/components/spinner/spinner.a2ui.json +3 -2
- package/components/spinner/{class.js → spinner.class.js} +33 -3
- package/components/spinner/spinner.css +91 -35
- package/components/spinner/spinner.d.ts +2 -2
- package/components/spinner/spinner.js +1 -1
- package/components/spinner/spinner.test.js +49 -11
- package/components/spinner/spinner.yaml +9 -1
- package/components/stack/stack.js +1 -1
- package/components/step-progress/step-progress.js +1 -1
- package/components/stepper/stepper-item.yaml +1 -1
- package/components/stepper/stepper.js +1 -1
- package/components/stream/stream.js +1 -1
- package/components/swatch/swatch.js +1 -1
- package/components/swiper/swiper.js +1 -1
- package/components/switch/switch.js +1 -1
- package/components/table/table.css +1 -1
- package/components/table/table.js +1 -1
- package/components/table-toolbar/{class.js → table-toolbar.class.js} +1 -1
- package/components/table-toolbar/table-toolbar.js +1 -1
- package/components/tabs/tab.yaml +2 -2
- package/components/tabs/tabs.js +1 -1
- package/components/tag/tag.js +1 -1
- package/components/tags-input/tags-input.a2ui.json +337 -0
- package/components/tags-input/tags-input.class.js +776 -0
- package/components/tags-input/tags-input.css +201 -0
- package/components/tags-input/tags-input.d.ts +120 -0
- package/components/tags-input/tags-input.examples.md +92 -0
- package/components/tags-input/tags-input.js +17 -0
- package/components/tags-input/tags-input.test.js +368 -0
- package/components/tags-input/tags-input.yaml +367 -0
- package/components/text/text.js +1 -1
- package/components/textarea/textarea.a2ui.json +1 -1
- package/components/textarea/textarea.js +1 -1
- package/components/textarea/textarea.yaml +11 -8
- package/components/time-picker/time-picker.js +1 -1
- package/components/timeline/timeline-item.yaml +2 -2
- package/components/timeline/{class.js → timeline.class.js} +1 -1
- package/components/timeline/timeline.js +1 -1
- package/components/toast/toast.js +1 -1
- package/components/toggle-group/toggle-group.js +1 -1
- package/components/toggle-group/toggle-option.yaml +1 -1
- package/components/toggle-scheme/toggle-scheme.js +1 -1
- package/components/toolbar/toolbar-group.yaml +1 -1
- package/components/toolbar/toolbar.js +1 -1
- package/components/tooltip/tooltip.js +1 -1
- package/components/tree/tree-item.yaml +1 -1
- package/components/tree/tree.js +1 -1
- package/components/upload/upload.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +111 -90
- package/package.json +3 -3
- package/styles/components.css +3 -0
- /package/components/accordion/{class.js → accordion.class.js} +0 -0
- /package/components/action-list/{class.js → action-list.class.js} +0 -0
- /package/components/agent-feedback-bar/{class.js → agent-feedback-bar.class.js} +0 -0
- /package/components/agent-questions/{class.js → agent-questions.class.js} +0 -0
- /package/components/agent-reasoning/{class.js → agent-reasoning.class.js} +0 -0
- /package/components/agent-suggestions/{class.js → agent-suggestions.class.js} +0 -0
- /package/components/avatar/{class.js → avatar.class.js} +0 -0
- /package/components/badge/{class.js → badge.class.js} +0 -0
- /package/components/block/{class.js → block.class.js} +0 -0
- /package/components/breadcrumb/{class.js → breadcrumb.class.js} +0 -0
- /package/components/button/{class.js → button.class.js} +0 -0
- /package/components/calendar-picker/{class.js → calendar-picker.class.js} +0 -0
- /package/components/card/{class.js → card.class.js} +0 -0
- /package/components/chart/{class.js → chart.class.js} +0 -0
- /package/components/chart-legend/{class.js → chart-legend.class.js} +0 -0
- /package/components/chat-thread/{class.js → chat-thread.class.js} +0 -0
- /package/components/check/{class.js → check.class.js} +0 -0
- /package/components/code/{class.js → code.class.js} +0 -0
- /package/components/col/{class.js → col.class.js} +0 -0
- /package/components/color-input/{class.js → color-input.class.js} +0 -0
- /package/components/color-picker/{class.js → color-picker.class.js} +0 -0
- /package/components/combobox/{class.js → combobox.class.js} +0 -0
- /package/components/command/{class.js → command.class.js} +0 -0
- /package/components/demo-toggle/{class.js → demo-toggle.class.js} +0 -0
- /package/components/description-list/{class.js → description-list.class.js} +0 -0
- /package/components/divider/{class.js → divider.class.js} +0 -0
- /package/components/drawer/{class.js → drawer.class.js} +0 -0
- /package/components/embed/{class.js → embed.class.js} +0 -0
- /package/components/empty-state/{class.js → empty-state.class.js} +0 -0
- /package/components/feed/{class.js → feed.class.js} +0 -0
- /package/components/field/{class.js → field.class.js} +0 -0
- /package/components/fields/{class.js → fields.class.js} +0 -0
- /package/components/grid/{class.js → grid.class.js} +0 -0
- /package/components/heatmap/{class.js → heatmap.class.js} +0 -0
- /package/components/icon/{class.js → icon.class.js} +0 -0
- /package/components/image/{class.js → image.class.js} +0 -0
- /package/components/input/{class.js → input.class.js} +0 -0
- /package/components/inspector/{class.js → inspector.class.js} +0 -0
- /package/components/integration-card/{class.js → integration-card.class.js} +0 -0
- /package/components/kbd/{class.js → kbd.class.js} +0 -0
- /package/components/link/{class.js → link.class.js} +0 -0
- /package/components/list/{class.js → list.class.js} +0 -0
- /package/components/list-window/{class.js → list-window.class.js} +0 -0
- /package/components/menu/{class.js → menu.class.js} +0 -0
- /package/components/nav/{class.js → nav.class.js} +0 -0
- /package/components/nav-group/{class.js → nav-group.class.js} +0 -0
- /package/components/nav-item/{class.js → nav-item.class.js} +0 -0
- /package/components/noodles/{class.js → noodles.class.js} +0 -0
- /package/components/option-card/{class.js → option-card.class.js} +0 -0
- /package/components/otp-input/{class.js → otp-input.class.js} +0 -0
- /package/components/page/{class.js → page.class.js} +0 -0
- /package/components/pagination/{class.js → pagination.class.js} +0 -0
- /package/components/pane/{class.js → pane.class.js} +0 -0
- /package/components/pipeline-status/{class.js → pipeline-status.class.js} +0 -0
- /package/components/popover/{class.js → popover.class.js} +0 -0
- /package/components/progress/{class.js → progress.class.js} +0 -0
- /package/components/progress-row/{class.js → progress-row.class.js} +0 -0
- /package/components/radio/{class.js → radio.class.js} +0 -0
- /package/components/range/{class.js → range.class.js} +0 -0
- /package/components/rating/{class.js → rating.class.js} +0 -0
- /package/components/richtext/{class.js → richtext.class.js} +0 -0
- /package/components/row/{class.js → row.class.js} +0 -0
- /package/components/search/{class.js → search.class.js} +0 -0
- /package/components/segment/{class.js → segment.class.js} +0 -0
- /package/components/segmented/{class.js → segmented.class.js} +0 -0
- /package/components/skeleton/{class.js → skeleton.class.js} +0 -0
- /package/components/slider/{class.js → slider.class.js} +0 -0
- /package/components/stack/{class.js → stack.class.js} +0 -0
- /package/components/step-progress/{class.js → step-progress.class.js} +0 -0
- /package/components/stepper/{class.js → stepper.class.js} +0 -0
- /package/components/stream/{class.js → stream.class.js} +0 -0
- /package/components/swatch/{class.js → swatch.class.js} +0 -0
- /package/components/swiper/{class.js → swiper.class.js} +0 -0
- /package/components/switch/{class.js → switch.class.js} +0 -0
- /package/components/table/{class.js → table.class.js} +0 -0
- /package/components/tabs/{class.js → tabs.class.js} +0 -0
- /package/components/tag/{class.js → tag.class.js} +0 -0
- /package/components/text/{class.js → text.class.js} +0 -0
- /package/components/textarea/{class.js → textarea.class.js} +0 -0
- /package/components/time-picker/{class.js → time-picker.class.js} +0 -0
- /package/components/toast/{class.js → toast.class.js} +0 -0
- /package/components/toggle-group/{class.js → toggle-group.class.js} +0 -0
- /package/components/toggle-scheme/{class.js → toggle-scheme.class.js} +0 -0
- /package/components/toolbar/{class.js → toolbar.class.js} +0 -0
- /package/components/tooltip/{class.js → tooltip.class.js} +0 -0
- /package/components/tree/{class.js → tree.class.js} +0 -0
- /package/components/upload/{class.js → upload.class.js} +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-side-effect class export for `<loading-overlay-ui>`.
|
|
3
|
+
*
|
|
4
|
+
* Importing this file gives you the class without auto-registering the
|
|
5
|
+
* tag. Useful for test isolation, subclassing with tag-name override, or
|
|
6
|
+
* selective composition.
|
|
7
|
+
*
|
|
8
|
+
* The auto-register path stays at `@adia-ai/web-components/components/loading-overlay`
|
|
9
|
+
* (which imports this file + calls `defineIfFree()`).
|
|
10
|
+
*
|
|
11
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* <loading-overlay-ui active label="Loading orders…"></loading-overlay-ui>
|
|
16
|
+
* <loading-overlay-ui active variant="transparent" delay="0">
|
|
17
|
+
* <skeleton-ui width="100%" height="1rem"></skeleton-ui>
|
|
18
|
+
* <skeleton-ui width="80%" height="1rem"></skeleton-ui>
|
|
19
|
+
* </loading-overlay-ui>
|
|
20
|
+
*
|
|
21
|
+
* Container-scoped busy overlay. Covers a positioned parent region with
|
|
22
|
+
* a spinner, skeleton, or custom busy indicator while async work is in
|
|
23
|
+
* flight. The host:
|
|
24
|
+
*
|
|
25
|
+
* 1. Absolutely positions to fill the offsetParent (consumer's
|
|
26
|
+
* responsibility to ensure that parent is `position: relative` or
|
|
27
|
+
* similar — documented in the spec / usage notes; we do NOT mutate
|
|
28
|
+
* parent layout styles).
|
|
29
|
+
* 2. Applies aria-busy="true" to the parent on activate; releases on
|
|
30
|
+
* deactivate / disconnect. This is screen-reader politeness; the
|
|
31
|
+
* backdrop's pointer-events:auto handles the click/focus block at
|
|
32
|
+
* the CSS level (no native [inert] attribute toggling — that's
|
|
33
|
+
* kept minimal to avoid disturbing the consumer's focus model).
|
|
34
|
+
* 3. Auto-stamps a centered <spinner-ui size="lg"> when the default
|
|
35
|
+
* slot is empty.
|
|
36
|
+
* 4. Honors a `[delay]` grace window (default 200ms) — if the active
|
|
37
|
+
* flag is cleared before the timer fires, the overlay never
|
|
38
|
+
* paints. Avoids flash on fast loads.
|
|
39
|
+
*
|
|
40
|
+
* Props (attributes):
|
|
41
|
+
* active — overlay visible (default: false)
|
|
42
|
+
* delay — grace window in ms before paint (default: 200)
|
|
43
|
+
* label — accessible label, also forwarded to auto-stamped spinner
|
|
44
|
+
* variant — default | transparent | blur
|
|
45
|
+
*
|
|
46
|
+
* Lifecycle: connected() seeds aria-label on host + caches parent
|
|
47
|
+
* reference for aria-busy bookkeeping. Disconnected() clears any
|
|
48
|
+
* pending delay timer and releases aria-busy on the parent. State
|
|
49
|
+
* changes are driven by the reactive [active] setter wrapper, which
|
|
50
|
+
* starts/cancels the timer + applies aria-busy when the timer fires.
|
|
51
|
+
*
|
|
52
|
+
* @see SPEC-034 (docs/specs/implementation-ready/SPEC-034-loading-overlay.md)
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import { UIElement } from '../../core/element.js';
|
|
56
|
+
|
|
57
|
+
export class UILoadingOverlay extends UIElement {
|
|
58
|
+
static properties = {
|
|
59
|
+
active: { type: Boolean, default: false, reflect: true },
|
|
60
|
+
delay: { type: Number, default: 200, reflect: true },
|
|
61
|
+
label: { type: String, default: 'Loading…', reflect: true },
|
|
62
|
+
variant: { type: String, default: 'default', reflect: true },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// No stamped children authored by the template engine; auto-stamp of
|
|
66
|
+
// the default spinner happens imperatively in connected() / render()
|
|
67
|
+
// when the slot is empty. Authored children (skeleton/progress/etc.)
|
|
68
|
+
// are left in place per ADR-0033 (light-DOM substrate).
|
|
69
|
+
static template = () => null;
|
|
70
|
+
|
|
71
|
+
#parent = null;
|
|
72
|
+
#delayTimer = null;
|
|
73
|
+
#activeApplied = false; // tracks whether aria-busy / opacity actually painted
|
|
74
|
+
|
|
75
|
+
constructor() {
|
|
76
|
+
super();
|
|
77
|
+
// Wrap the auto-installed `active` setter so state transitions kick
|
|
78
|
+
// the delay timer synchronously with the assignment — matching the
|
|
79
|
+
// drawer-ui Safari pattern (avoids microtask scheduling delays).
|
|
80
|
+
const desc = Object.getOwnPropertyDescriptor(this, 'active');
|
|
81
|
+
if (desc?.set) {
|
|
82
|
+
const origSet = desc.set;
|
|
83
|
+
Object.defineProperty(this, 'active', {
|
|
84
|
+
get: desc.get,
|
|
85
|
+
set: (v) => {
|
|
86
|
+
const prev = !!desc.get.call(this);
|
|
87
|
+
origSet.call(this, v);
|
|
88
|
+
if (!!v !== prev) this.#syncActive(!!v);
|
|
89
|
+
},
|
|
90
|
+
configurable: true,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
connected() {
|
|
96
|
+
// Host ARIA: role=status is a polite live region; aria-label
|
|
97
|
+
// names the operation. Both are seeded once; render() keeps the
|
|
98
|
+
// label fresh on prop changes.
|
|
99
|
+
if (!this.hasAttribute('role')) this.setAttribute('role', 'status');
|
|
100
|
+
if (!this.hasAttribute('aria-live')) this.setAttribute('aria-live', 'polite');
|
|
101
|
+
this.setAttribute('aria-label', this.label || 'Loading');
|
|
102
|
+
|
|
103
|
+
// Cache the parent so disconnected() can release aria-busy even if
|
|
104
|
+
// the element is detached before parentElement is re-readable.
|
|
105
|
+
this.#parent = this.parentElement;
|
|
106
|
+
|
|
107
|
+
// If the element mounted with [active] already set (declarative),
|
|
108
|
+
// honor it. The reactive setter wrapper only fires on subsequent
|
|
109
|
+
// assignments, so we kick the timer here too.
|
|
110
|
+
if (this.active) this.#syncActive(true);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
disconnected() {
|
|
114
|
+
// Symmetric lifecycle: clear any in-flight timer + release aria-busy
|
|
115
|
+
// on the parent we cached at connect time.
|
|
116
|
+
this.#clearTimer();
|
|
117
|
+
if (this.#activeApplied) this.#releaseBusy();
|
|
118
|
+
this.#parent = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
render() {
|
|
122
|
+
// Keep the host label fresh if [label] is mutated after connect.
|
|
123
|
+
// Forward it to an auto-stamped spinner-ui (if present) so screen
|
|
124
|
+
// readers hear a single coherent name.
|
|
125
|
+
const label = this.label || 'Loading';
|
|
126
|
+
this.setAttribute('aria-label', label);
|
|
127
|
+
|
|
128
|
+
const ownSpinner = this.querySelector(':scope > spinner-ui[data-loading-overlay-auto]');
|
|
129
|
+
if (ownSpinner && ownSpinner.getAttribute('label') !== label) {
|
|
130
|
+
ownSpinner.setAttribute('label', label);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Ensure the auto-stamped spinner exists when the slot is empty.
|
|
134
|
+
// Authored children (consumer-supplied indicators) win; we only
|
|
135
|
+
// stamp when there are no element children at all.
|
|
136
|
+
if (this.children.length === 0) {
|
|
137
|
+
this.#stampDefaultSpinner();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Internals ───────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
#syncActive(next) {
|
|
144
|
+
// Clear any prior in-flight timer first; the new state owns the
|
|
145
|
+
// grace window.
|
|
146
|
+
this.#clearTimer();
|
|
147
|
+
|
|
148
|
+
if (next) {
|
|
149
|
+
const delay = Math.max(0, Number(this.delay) || 0);
|
|
150
|
+
if (delay === 0) {
|
|
151
|
+
this.#applyBusy();
|
|
152
|
+
} else {
|
|
153
|
+
this.#delayTimer = setTimeout(() => {
|
|
154
|
+
this.#delayTimer = null;
|
|
155
|
+
// Only paint if we're still active — caller may have cleared
|
|
156
|
+
// [active] during the grace window.
|
|
157
|
+
if (this.active) this.#applyBusy();
|
|
158
|
+
}, delay);
|
|
159
|
+
}
|
|
160
|
+
} else if (this.#activeApplied) {
|
|
161
|
+
this.#releaseBusy();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#applyBusy() {
|
|
166
|
+
const parent = this.#parent || this.parentElement;
|
|
167
|
+
if (parent && !parent.hasAttribute('aria-busy')) {
|
|
168
|
+
parent.setAttribute('aria-busy', 'true');
|
|
169
|
+
}
|
|
170
|
+
this.#activeApplied = true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#releaseBusy() {
|
|
174
|
+
const parent = this.#parent || this.parentElement;
|
|
175
|
+
// Only remove aria-busy if WE set it (heuristic: matches "true").
|
|
176
|
+
// If the consumer authored their own aria-busy="true" before we
|
|
177
|
+
// mounted, we leave it; if they set "false" we leave it. Same
|
|
178
|
+
// shape as drawer-ui releasing dialog state.
|
|
179
|
+
if (parent && parent.getAttribute('aria-busy') === 'true') {
|
|
180
|
+
parent.removeAttribute('aria-busy');
|
|
181
|
+
}
|
|
182
|
+
this.#activeApplied = false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#clearTimer() {
|
|
186
|
+
if (this.#delayTimer !== null) {
|
|
187
|
+
clearTimeout(this.#delayTimer);
|
|
188
|
+
this.#delayTimer = null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#stampDefaultSpinner() {
|
|
193
|
+
// Imperative stamp — light-DOM child, marked with a data attribute
|
|
194
|
+
// so render()'s "did the consumer slot something?" check stays
|
|
195
|
+
// unambiguous on subsequent renders.
|
|
196
|
+
const sp = document.createElement('spinner-ui');
|
|
197
|
+
sp.setAttribute('size', 'lg');
|
|
198
|
+
sp.setAttribute('tone', 'subtle');
|
|
199
|
+
sp.setAttribute('label', this.label || 'Loading');
|
|
200
|
+
sp.setAttribute('data-loading-overlay-auto', '');
|
|
201
|
+
this.appendChild(sp);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
@scope (loading-overlay-ui) {
|
|
2
|
+
/* ── Block 1 — TOKENS ─────────────────────────────────────────────
|
|
3
|
+
Loading-overlay covers a positioned parent. Backdrop tint, corner
|
|
4
|
+
radius, stacking order, and motion are all consumer-overridable
|
|
5
|
+
via the `var(--public, var(--public-default))` chain (per
|
|
6
|
+
component-token-contract.md §"The two-block @scope pattern"). */
|
|
7
|
+
:where(:scope) {
|
|
8
|
+
--loading-overlay-bg-default: var(--a-scrim-default);
|
|
9
|
+
--loading-overlay-radius-default: var(--a-radius-md);
|
|
10
|
+
--loading-overlay-z-default: 50;
|
|
11
|
+
--loading-overlay-duration-default: var(--a-duration);
|
|
12
|
+
--loading-overlay-easing-default: var(--a-easing-out);
|
|
13
|
+
--loading-overlay-gap-default: var(--a-space-3);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* ── Block 2 — BASE
|
|
17
|
+
Idle state: hidden but mounted. The element stays in the DOM so
|
|
18
|
+
state transitions on `[active]` animate; `display: none` would
|
|
19
|
+
skip the transition entirely. We pair `opacity: 0` with
|
|
20
|
+
`pointer-events: none` so the dim overlay doesn't intercept
|
|
21
|
+
clicks on the underlying content when idle. */
|
|
22
|
+
:scope {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
position: absolute;
|
|
25
|
+
inset: 0;
|
|
26
|
+
display: grid;
|
|
27
|
+
place-items: center;
|
|
28
|
+
gap: var(--loading-overlay-gap, var(--loading-overlay-gap-default));
|
|
29
|
+
background: var(--loading-overlay-bg, var(--loading-overlay-bg-default));
|
|
30
|
+
border-radius: var(--loading-overlay-radius, var(--loading-overlay-radius-default));
|
|
31
|
+
z-index: var(--loading-overlay-z, var(--loading-overlay-z-default));
|
|
32
|
+
opacity: 0;
|
|
33
|
+
pointer-events: none;
|
|
34
|
+
transition:
|
|
35
|
+
opacity var(--loading-overlay-duration, var(--loading-overlay-duration-default))
|
|
36
|
+
var(--loading-overlay-easing, var(--loading-overlay-easing-default));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ── Active state — fade in + capture pointer events ─────────────
|
|
40
|
+
Active state is gated by the delay timer in JS (see loading-overlay.class.js); by
|
|
41
|
+
the time `[active]` is set, the grace window has elapsed and the
|
|
42
|
+
overlay should paint immediately on next frame. */
|
|
43
|
+
:scope[active] {
|
|
44
|
+
opacity: 1;
|
|
45
|
+
pointer-events: auto;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ── Variants ─────────────────────────────────────────────────────
|
|
49
|
+
transparent: no backdrop fill — useful when the indicator IS the
|
|
50
|
+
overlay (e.g. a full-area skeleton stack). The parent is still
|
|
51
|
+
marked aria-busy and pointer events still blocked.
|
|
52
|
+
blur: light scrim plus backdrop-filter blur on the underlying
|
|
53
|
+
content. Use sparingly — Safari < 18 paint cost. */
|
|
54
|
+
:scope[variant="transparent"] {
|
|
55
|
+
--loading-overlay-bg-default: transparent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:scope[variant="blur"] {
|
|
59
|
+
--loading-overlay-bg-default: var(--a-scrim-weak);
|
|
60
|
+
backdrop-filter: blur(4px);
|
|
61
|
+
-webkit-backdrop-filter: blur(4px);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ── Auto-stamped spinner sizing ──────────────────────────────────
|
|
65
|
+
When the default slot is empty, the JS layer auto-stamps a
|
|
66
|
+
`<spinner-ui size="lg">` child. Style it as the default centered
|
|
67
|
+
indicator so consumer slots and the auto-stamp render identically. */
|
|
68
|
+
:scope > spinner-ui {
|
|
69
|
+
--spinner-color: var(--a-fg-muted);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ── Reduced motion ───────────────────────────────────────────────
|
|
73
|
+
WCAG 2.3.3 — suppress the fade. The overlay still paints / hides
|
|
74
|
+
instantly (no transition), and the auto-stamped spinner falls
|
|
75
|
+
back to its own reduced-motion ellipsis via spinner.css. */
|
|
76
|
+
@media (prefers-reduced-motion: reduce) {
|
|
77
|
+
:scope {
|
|
78
|
+
transition: none;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<loading-overlay-ui>` — Container-scoped busy overlay. Covers a positioned parent region with a centered spinner (auto-stamped) or slotted indicator (skeleton-ui, progress-ui, custom) while async work is in flight. Wires aria-busy onto the parent on connect; applies a [delay] grace window to avoid flash on fast loads. For viewport-scoped / route loaders use a dedicated route-loader pattern; for submit-button busy use <button-ui loading>.
|
|
3
|
+
*
|
|
4
|
+
* @see https://ui-kit.exe.xyz/site/components/loading-overlay
|
|
5
|
+
*
|
|
6
|
+
* Type declarations generated by scripts/build/dts-codegen.mjs from
|
|
7
|
+
* the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
|
|
8
|
+
* run `npm run build:components`, then `npm run codegen:dts` to
|
|
9
|
+
* regenerate; or hand-author this file fully if rich event types are
|
|
10
|
+
* needed beyond what the yaml `events:` block can express.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { UIElement } from '../../core/element.js';
|
|
14
|
+
|
|
15
|
+
export class UILoadingOverlay extends UIElement {
|
|
16
|
+
/** When set, overlay is visible and the parent container is marked aria-busy. Toggle from consumer code; default hidden. */
|
|
17
|
+
active: string;
|
|
18
|
+
/** Suppress the overlay until this many ms elapse. Prevents flash on fast loads. Default 200ms. */
|
|
19
|
+
delay: string;
|
|
20
|
+
/** Accessible operation name used by the host aria-label and forwarded to the auto-stamped spinner. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** Backdrop treatment — default (muted scrim), transparent (no backdrop fill — useful with full-area skeleton indicators), blur (light scrim with backdrop-filter blur). */
|
|
23
|
+
variant: string;
|
|
24
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# loading-overlay — Examples
|
|
2
|
+
|
|
3
|
+
## Default (auto-stamped spinner)
|
|
4
|
+
|
|
5
|
+
The host parent must be positioned (`position: relative` or similar);
|
|
6
|
+
the overlay does not mutate parent layout. When the default slot is
|
|
7
|
+
empty, a centered `<spinner-ui size="lg">` is auto-stamped.
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<card-ui style="position: relative;">
|
|
11
|
+
<section>
|
|
12
|
+
<table-ui id="orders" data-stream-src="/api/orders"></table-ui>
|
|
13
|
+
<loading-overlay-ui active label="Loading orders…"></loading-overlay-ui>
|
|
14
|
+
</section>
|
|
15
|
+
</card-ui>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Skeleton placeholder
|
|
19
|
+
|
|
20
|
+
Pair `variant="transparent"` with a slotted skeleton stack to render
|
|
21
|
+
shape-of-content placeholders instead of a centered spinner.
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<div style="position: relative;">
|
|
25
|
+
<loading-overlay-ui active variant="transparent" label="Loading content">
|
|
26
|
+
<stack-ui>
|
|
27
|
+
<skeleton-ui width="100%" height="1rem"></skeleton-ui>
|
|
28
|
+
<skeleton-ui width="80%" height="1rem"></skeleton-ui>
|
|
29
|
+
<skeleton-ui width="60%" height="1rem"></skeleton-ui>
|
|
30
|
+
</stack-ui>
|
|
31
|
+
</loading-overlay-ui>
|
|
32
|
+
</div>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Fast-load (no flash)
|
|
36
|
+
|
|
37
|
+
The default 200 ms grace window suppresses paint on fast-resolving
|
|
38
|
+
loads. If the underlying work resolves before the timer fires, the
|
|
39
|
+
overlay never paints.
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<loading-overlay-ui active delay="200"></loading-overlay-ui>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For known-slow operations (server-side jobs, large uploads) opt out
|
|
46
|
+
with `delay="0"`:
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<loading-overlay-ui active delay="0" label="Generating report…"></loading-overlay-ui>
|
|
50
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<loading-overlay-ui>` — auto-registers the tag on import.
|
|
3
|
+
*
|
|
4
|
+
* For non-side-effect class import (test isolation, tag override), use
|
|
5
|
+
* the `class` subpath:
|
|
6
|
+
*
|
|
7
|
+
* import { UILoadingOverlay } from '@adia-ai/web-components/components/loading-overlay/class';
|
|
8
|
+
*
|
|
9
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { defineIfFree } from '../../core/register.js';
|
|
13
|
+
import { UILoadingOverlay } from './loading-overlay.class.js';
|
|
14
|
+
|
|
15
|
+
defineIfFree('loading-overlay-ui', UILoadingOverlay);
|
|
16
|
+
|
|
17
|
+
export { UILoadingOverlay };
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loading-overlay-ui tests — covers SPEC-034 § Verification.
|
|
3
|
+
*
|
|
4
|
+
* The component is JS-driven (delay timer + aria-busy bookkeeping) with
|
|
5
|
+
* a CSS-only fade transition. Tests split:
|
|
6
|
+
*
|
|
7
|
+
* 1. Defaults + property reflection.
|
|
8
|
+
* 2. aria-busy parent wiring (apply on activate, release on
|
|
9
|
+
* deactivate / disconnect).
|
|
10
|
+
* 3. [delay] grace-window suppression — fast-load no-flash.
|
|
11
|
+
* 4. Auto-stamped spinner-ui (default slot empty) vs slotted content.
|
|
12
|
+
* 5. CSS source contract — two-block @scope, token surface, variants.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { dirname, resolve } from 'node:path';
|
|
19
|
+
|
|
20
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const LO_CSS = readFileSync(resolve(HERE, 'loading-overlay.css'), 'utf8');
|
|
22
|
+
|
|
23
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
await import('./loading-overlay.js');
|
|
27
|
+
// Side-effect import so the auto-stamp can construct spinner-ui.
|
|
28
|
+
await import('../spinner/spinner.js');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function mountInPositionedParent(html) {
|
|
32
|
+
const parent = document.createElement('div');
|
|
33
|
+
parent.style.position = 'relative';
|
|
34
|
+
parent.style.width = '200px';
|
|
35
|
+
parent.style.height = '120px';
|
|
36
|
+
parent.id = 'lo-parent';
|
|
37
|
+
parent.innerHTML = html;
|
|
38
|
+
document.body.appendChild(parent);
|
|
39
|
+
return { parent, overlay: parent.querySelector('loading-overlay-ui') };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── 1. Defaults + property reflection ────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe('loading-overlay-ui — defaults', () => {
|
|
45
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
46
|
+
|
|
47
|
+
it('renders without props using documented defaults', async () => {
|
|
48
|
+
const { overlay } = mountInPositionedParent('<loading-overlay-ui></loading-overlay-ui>');
|
|
49
|
+
await tick();
|
|
50
|
+
expect(overlay.active).toBe(false);
|
|
51
|
+
expect(overlay.delay).toBe(200);
|
|
52
|
+
expect(overlay.label).toBe('Loading…');
|
|
53
|
+
expect(overlay.variant).toBe('default');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('sets role="status" + aria-live="polite" on the host', async () => {
|
|
57
|
+
const { overlay } = mountInPositionedParent('<loading-overlay-ui></loading-overlay-ui>');
|
|
58
|
+
await tick();
|
|
59
|
+
expect(overlay.getAttribute('role')).toBe('status');
|
|
60
|
+
expect(overlay.getAttribute('aria-live')).toBe('polite');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('seeds aria-label from the [label] prop', async () => {
|
|
64
|
+
const { overlay } = mountInPositionedParent('<loading-overlay-ui label="Saving"></loading-overlay-ui>');
|
|
65
|
+
await tick();
|
|
66
|
+
expect(overlay.getAttribute('aria-label')).toBe('Saving');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('reflects [active], [variant], [delay], [label] to the host attributes', async () => {
|
|
70
|
+
const { overlay } = mountInPositionedParent('<loading-overlay-ui active variant="blur" delay="500" label="Wait"></loading-overlay-ui>');
|
|
71
|
+
await tick();
|
|
72
|
+
expect(overlay.hasAttribute('active')).toBe(true);
|
|
73
|
+
expect(overlay.getAttribute('variant')).toBe('blur');
|
|
74
|
+
expect(overlay.getAttribute('delay')).toBe('500');
|
|
75
|
+
expect(overlay.getAttribute('label')).toBe('Wait');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── 2. aria-busy parent wiring ───────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
describe('loading-overlay-ui — aria-busy parent wiring', () => {
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
document.body.innerHTML = '';
|
|
84
|
+
vi.useFakeTimers();
|
|
85
|
+
});
|
|
86
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
87
|
+
|
|
88
|
+
it('applies aria-busy="true" to parent on activate (after delay)', async () => {
|
|
89
|
+
const { parent } = mountInPositionedParent('<loading-overlay-ui delay="0"></loading-overlay-ui>');
|
|
90
|
+
await tick();
|
|
91
|
+
const overlay = parent.querySelector('loading-overlay-ui');
|
|
92
|
+
expect(parent.hasAttribute('aria-busy')).toBe(false);
|
|
93
|
+
overlay.active = true;
|
|
94
|
+
// delay=0 → applies synchronously via setTimeout(0)
|
|
95
|
+
vi.advanceTimersByTime(0);
|
|
96
|
+
expect(parent.getAttribute('aria-busy')).toBe('true');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('releases aria-busy on deactivate', async () => {
|
|
100
|
+
const { parent } = mountInPositionedParent('<loading-overlay-ui active delay="0"></loading-overlay-ui>');
|
|
101
|
+
await tick();
|
|
102
|
+
vi.advanceTimersByTime(0);
|
|
103
|
+
expect(parent.getAttribute('aria-busy')).toBe('true');
|
|
104
|
+
|
|
105
|
+
const overlay = parent.querySelector('loading-overlay-ui');
|
|
106
|
+
overlay.active = false;
|
|
107
|
+
expect(parent.hasAttribute('aria-busy')).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('releases aria-busy on disconnect', async () => {
|
|
111
|
+
const { parent } = mountInPositionedParent('<loading-overlay-ui active delay="0"></loading-overlay-ui>');
|
|
112
|
+
await tick();
|
|
113
|
+
vi.advanceTimersByTime(0);
|
|
114
|
+
expect(parent.getAttribute('aria-busy')).toBe('true');
|
|
115
|
+
|
|
116
|
+
const overlay = parent.querySelector('loading-overlay-ui');
|
|
117
|
+
overlay.remove();
|
|
118
|
+
expect(parent.hasAttribute('aria-busy')).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('honors mount-time [active] (declarative)', async () => {
|
|
122
|
+
const { parent } = mountInPositionedParent('<loading-overlay-ui active delay="0"></loading-overlay-ui>');
|
|
123
|
+
await tick();
|
|
124
|
+
vi.advanceTimersByTime(0);
|
|
125
|
+
expect(parent.getAttribute('aria-busy')).toBe('true');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── 3. [delay] grace window ──────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
describe('loading-overlay-ui — [delay] grace window', () => {
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
document.body.innerHTML = '';
|
|
134
|
+
vi.useFakeTimers();
|
|
135
|
+
});
|
|
136
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
137
|
+
|
|
138
|
+
it('suppresses paint when [active] is cleared before delay expires', async () => {
|
|
139
|
+
const { parent } = mountInPositionedParent('<loading-overlay-ui delay="200"></loading-overlay-ui>');
|
|
140
|
+
await tick();
|
|
141
|
+
const overlay = parent.querySelector('loading-overlay-ui');
|
|
142
|
+
|
|
143
|
+
overlay.active = true;
|
|
144
|
+
// Within the grace window — parent should NOT be marked yet.
|
|
145
|
+
vi.advanceTimersByTime(100);
|
|
146
|
+
expect(parent.hasAttribute('aria-busy')).toBe(false);
|
|
147
|
+
|
|
148
|
+
// Clear before timer fires.
|
|
149
|
+
overlay.active = false;
|
|
150
|
+
vi.advanceTimersByTime(200);
|
|
151
|
+
expect(parent.hasAttribute('aria-busy')).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('paints after delay elapses when [active] stays true', async () => {
|
|
155
|
+
const { parent } = mountInPositionedParent('<loading-overlay-ui delay="200"></loading-overlay-ui>');
|
|
156
|
+
await tick();
|
|
157
|
+
const overlay = parent.querySelector('loading-overlay-ui');
|
|
158
|
+
|
|
159
|
+
overlay.active = true;
|
|
160
|
+
expect(parent.hasAttribute('aria-busy')).toBe(false);
|
|
161
|
+
vi.advanceTimersByTime(200);
|
|
162
|
+
expect(parent.getAttribute('aria-busy')).toBe('true');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ── 4. Default-slot auto-stamp ───────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
describe('loading-overlay-ui — auto-stamped spinner', () => {
|
|
169
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
170
|
+
|
|
171
|
+
it('auto-stamps a <spinner-ui size="lg"> when default slot is empty', async () => {
|
|
172
|
+
const { overlay } = mountInPositionedParent('<loading-overlay-ui></loading-overlay-ui>');
|
|
173
|
+
await tick();
|
|
174
|
+
const sp = overlay.querySelector(':scope > spinner-ui[data-loading-overlay-auto]');
|
|
175
|
+
expect(sp).not.toBeNull();
|
|
176
|
+
expect(sp.getAttribute('size')).toBe('lg');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('does NOT auto-stamp when consumer slots their own indicator', async () => {
|
|
180
|
+
const { overlay } = mountInPositionedParent(
|
|
181
|
+
'<loading-overlay-ui><skeleton-ui width="100%" height="1rem"></skeleton-ui></loading-overlay-ui>'
|
|
182
|
+
);
|
|
183
|
+
await tick();
|
|
184
|
+
expect(overlay.querySelector(':scope > spinner-ui[data-loading-overlay-auto]')).toBeNull();
|
|
185
|
+
expect(overlay.querySelector(':scope > skeleton-ui')).not.toBeNull();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('forwards [label] changes to the auto-stamped spinner', async () => {
|
|
189
|
+
const { overlay } = mountInPositionedParent('<loading-overlay-ui label="Loading"></loading-overlay-ui>');
|
|
190
|
+
await tick();
|
|
191
|
+
overlay.label = 'Uploading';
|
|
192
|
+
await tick();
|
|
193
|
+
const sp = overlay.querySelector(':scope > spinner-ui[data-loading-overlay-auto]');
|
|
194
|
+
expect(sp.getAttribute('label')).toBe('Uploading');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ── 5. CSS source contract ───────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
describe('loading-overlay-ui — CSS source contract', () => {
|
|
201
|
+
it('opens with the canonical @scope (loading-overlay-ui) block', () => {
|
|
202
|
+
expect(LO_CSS).toMatch(/^@scope \(loading-overlay-ui\)/);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('declares both the :where(:scope) token block and a :scope base block', () => {
|
|
206
|
+
expect(LO_CSS).toMatch(/:where\(:scope\)\s*\{/);
|
|
207
|
+
expect(LO_CSS).toMatch(/^\s*:scope\s*\{/m);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('declares the documented component tokens with -default suffix', () => {
|
|
211
|
+
for (const tok of [
|
|
212
|
+
'--loading-overlay-bg-default',
|
|
213
|
+
'--loading-overlay-radius-default',
|
|
214
|
+
'--loading-overlay-z-default',
|
|
215
|
+
'--loading-overlay-duration-default',
|
|
216
|
+
'--loading-overlay-easing-default',
|
|
217
|
+
'--loading-overlay-gap-default',
|
|
218
|
+
]) {
|
|
219
|
+
expect(LO_CSS).toContain(tok);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('positions absolutely with inset:0 (fills the parent)', () => {
|
|
224
|
+
expect(LO_CSS).toMatch(/position:\s*absolute/);
|
|
225
|
+
expect(LO_CSS).toMatch(/inset:\s*0/);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('idle state: opacity:0 + pointer-events:none', () => {
|
|
229
|
+
// The base :scope block carries both — fail-fast if either is dropped.
|
|
230
|
+
expect(LO_CSS).toMatch(/opacity:\s*0/);
|
|
231
|
+
expect(LO_CSS).toMatch(/pointer-events:\s*none/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('active state: opacity:1 + pointer-events:auto', () => {
|
|
235
|
+
expect(LO_CSS).toMatch(/:scope\[active\][^}]*opacity:\s*1/);
|
|
236
|
+
expect(LO_CSS).toMatch(/:scope\[active\][^}]*pointer-events:\s*auto/);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('transparent variant drops the backdrop fill', () => {
|
|
240
|
+
expect(LO_CSS).toMatch(/:scope\[variant="transparent"\][^}]*--loading-overlay-bg-default:\s*transparent/);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('blur variant applies backdrop-filter blur', () => {
|
|
244
|
+
expect(LO_CSS).toMatch(/:scope\[variant="blur"\][^}]*backdrop-filter:\s*blur/);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('reduced-motion media query suppresses the fade transition', () => {
|
|
248
|
+
expect(LO_CSS).toMatch(/@media\s*\(prefers-reduced-motion:\s*reduce\)/);
|
|
249
|
+
expect(LO_CSS).toMatch(/transition:\s*none/);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('uses semantic --a-* tokens; no raw hex / rgb / oklch in the file', () => {
|
|
253
|
+
expect(LO_CSS).not.toMatch(/#[0-9a-fA-F]{3,8}\b/);
|
|
254
|
+
expect(LO_CSS).not.toMatch(/\brgb\s*\(/);
|
|
255
|
+
expect(LO_CSS).not.toMatch(/\boklch\s*\(/);
|
|
256
|
+
});
|
|
257
|
+
});
|