@adia-ai/web-components 0.8.31 → 0.8.32
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 +39 -0
- package/components/accordion/accordion.d.ts +2 -2
- package/components/action-list/action-list.class.js +6 -2
- package/components/action-list/action-list.d.ts +1 -1
- package/components/agent-artifact/agent-artifact.d.ts +1 -1
- package/components/agent-feedback-bar/agent-feedback-bar.d.ts +2 -2
- package/components/agent-questions/agent-questions.d.ts +1 -1
- package/components/agent-reasoning/agent-reasoning.d.ts +3 -3
- package/components/agent-suggestions/agent-suggestions.d.ts +1 -1
- package/components/agent-trace/agent-trace.d.ts +1 -1
- package/components/alert/alert.d.ts +2 -2
- package/components/anchor-bar/anchor-bar.d.ts +2 -2
- package/components/avatar/avatar.class.js +0 -1
- package/components/avatar/avatar.css +16 -2
- package/components/badge/badge.css +30 -2
- package/components/breadcrumb/breadcrumb.css +15 -3
- package/components/button/button.d.ts +1 -1
- package/components/calendar-grid/calendar-grid.d.ts +2 -2
- package/components/card/card.css +21 -0
- package/components/card/card.yaml +9 -0
- package/components/chart/chart.d.ts +4 -4
- package/components/chart-legend/chart-legend.d.ts +1 -1
- package/components/chat-thread/chat-input.css +1 -1
- package/components/chat-thread/chat-thread.d.ts +2 -2
- package/components/check/check.css +3 -3
- package/components/command/command.d.ts +2 -2
- package/components/context-menu/context-menu.d.ts +3 -3
- package/components/datetime-picker/datetime-picker.d.ts +5 -5
- package/components/demo-toggle/demo-toggle.d.ts +1 -1
- package/components/drawer/drawer.d.ts +2 -2
- package/components/heatmap/heatmap.d.ts +5 -5
- package/components/index.js +1 -0
- package/components/inline-edit/inline-edit.d.ts +4 -4
- package/components/input/input.a2ui.json +4 -1
- package/components/input/input.css +18 -0
- package/components/input/input.yaml +11 -1
- package/components/integration-card/integration-card.d.ts +4 -4
- package/components/link/link.d.ts +1 -1
- package/components/list/list.class.js +22 -7
- package/components/list/list.d.ts +1 -1
- package/components/list-window/list-window.d.ts +5 -5
- package/components/menu/menu.css +2 -2
- package/components/menu/menu.d.ts +1 -1
- package/components/modal/modal.d.ts +1 -1
- package/components/nav/nav.css +8 -0
- package/components/nav/nav.d.ts +1 -1
- package/components/nav-group/nav-group.css +58 -9
- package/components/nav-group/nav-group.d.ts +1 -1
- package/components/nav-item/nav-item.css +7 -4
- package/components/nav-item/nav-item.d.ts +1 -1
- package/components/noodles/noodles.d.ts +3 -3
- package/components/option-card/option-card.css +1 -1
- package/components/pagination/pagination.d.ts +1 -1
- package/components/pane/pane.d.ts +1 -1
- package/components/password-strength/password-strength.d.ts +1 -1
- package/components/radio/radio.a2ui.json +4 -3
- package/components/radio/radio.css +2 -2
- package/components/radio/radio.yaml +7 -14
- package/components/radio-group/radio-group.a2ui.json +118 -0
- package/components/radio-group/radio-group.class.js +75 -0
- package/components/radio-group/radio-group.css +31 -0
- package/components/radio-group/radio-group.d.ts +20 -0
- package/components/radio-group/radio-group.examples.md +37 -0
- package/components/radio-group/radio-group.js +17 -0
- package/components/radio-group/radio-group.yaml +114 -0
- package/components/stat/stat.css +12 -8
- package/components/stat/stat.js +66 -1
- package/components/stat/stat.yaml +3 -0
- package/components/stepper/stepper-item.a2ui.json +5 -0
- package/components/stepper/stepper-item.yaml +11 -0
- package/components/stepper/stepper.a2ui.json +38 -3
- package/components/stepper/stepper.class.js +199 -2
- package/components/stepper/stepper.css +30 -0
- package/components/stepper/stepper.d.ts +28 -1
- package/components/stepper/stepper.yaml +76 -2
- package/components/stream/stream.d.ts +3 -3
- package/components/swatch/swatch.d.ts +1 -1
- package/components/swiper/swiper.d.ts +3 -3
- package/components/table/table.a2ui.json +2 -2
- package/components/table/table.d.ts +7 -7
- package/components/table/table.yaml +9 -1
- package/components/table-toolbar/table-toolbar.d.ts +4 -4
- package/components/tabs/tabs.d.ts +1 -1
- package/components/tag/tag.a2ui.json +1 -1
- package/components/tag/tag.css +78 -13
- package/components/tag/tag.d.ts +12 -8
- package/components/tag/tag.examples.md +4 -4
- package/components/tag/tag.yaml +18 -7
- package/components/tags-input/tags-input.d.ts +4 -4
- package/components/timeline/timeline.d.ts +1 -1
- package/components/toc/toc.d.ts +1 -1
- package/components/toggle-group/toggle-group.d.ts +1 -1
- package/components/toggle-scheme/toggle-scheme.d.ts +1 -1
- package/components/tour/tour.d.ts +4 -4
- package/components/tree/tree.d.ts +1 -1
- package/core/icons-phosphor.js +129 -71
- package/core/provider.js +84 -23
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +89 -87
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/styles/api/sizing-fallback.css +32 -0
- package/styles/api/sizing.css +7 -3
- package/styles/components.css +1 -0
- package/styles/tokens.css +1 -0
package/components/stat/stat.js
CHANGED
|
@@ -32,9 +32,15 @@ class UIStat extends UIElement {
|
|
|
32
32
|
|
|
33
33
|
static template = () => null;
|
|
34
34
|
|
|
35
|
+
// Trend arrows the class stamps directly (issue 805) — consumers calling
|
|
36
|
+
// installIconLoaders() manually need these two names present.
|
|
37
|
+
static requiredIcons = ['arrow-up', 'arrow-down'];
|
|
38
|
+
|
|
35
39
|
#valueEl = null;
|
|
36
40
|
#labelEl = null;
|
|
37
41
|
#changeEl = null;
|
|
42
|
+
#changeIconEl = null;
|
|
43
|
+
#changeTextEl = null;
|
|
38
44
|
#iconEl = null;
|
|
39
45
|
|
|
40
46
|
connected() {
|
|
@@ -59,6 +65,28 @@ class UIStat extends UIElement {
|
|
|
59
65
|
this.appendChild(this.#changeEl);
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
// Trend arrow (issue 805): a real icon-ui child instead of a CSS ::before
|
|
69
|
+
// unicode triangle, so the delta indicator rides the Phosphor icon
|
|
70
|
+
// system (contrast, weight, RTL) like every other icon on the page.
|
|
71
|
+
// No `slot="icon"` attribute on this child — that value is reserved for
|
|
72
|
+
// stat's own outer `[slot="icon"]` region, and an ancestor's `[slot="icon"]`
|
|
73
|
+
// descendant selector would otherwise leak margin/positioning onto it
|
|
74
|
+
// (issue 809, PR 810 shape). `[data-trend-icon]` is scoped to stat's own CSS.
|
|
75
|
+
this.#changeIconEl = this.#changeEl.querySelector(':scope > icon-ui[data-trend-icon]');
|
|
76
|
+
if (!this.#changeIconEl) {
|
|
77
|
+
this.#changeIconEl = document.createElement('icon-ui');
|
|
78
|
+
this.#changeIconEl.setAttribute('data-trend-icon', '');
|
|
79
|
+
this.#changeIconEl.hidden = true;
|
|
80
|
+
this.#changeEl.appendChild(this.#changeIconEl);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.#changeTextEl = this.#changeEl.querySelector(':scope > [data-trend-text]');
|
|
84
|
+
if (!this.#changeTextEl) {
|
|
85
|
+
this.#changeTextEl = document.createElement('span');
|
|
86
|
+
this.#changeTextEl.setAttribute('data-trend-text', '');
|
|
87
|
+
this.#changeEl.appendChild(this.#changeTextEl);
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
this.#iconEl = this.querySelector(':scope > [slot="icon"]');
|
|
63
91
|
if (!this.#iconEl) {
|
|
64
92
|
this.#iconEl = document.createElement('icon-ui');
|
|
@@ -83,6 +111,9 @@ class UIStat extends UIElement {
|
|
|
83
111
|
// value+change visual mass without being so wide as to look like text.
|
|
84
112
|
this.#valueEl.textContent = '';
|
|
85
113
|
this.#valueEl.innerHTML = '<skeleton-ui width="60%" height="2em" radius="sm"></skeleton-ui>';
|
|
114
|
+
// Skeleton replaces the trend icon + text children wholesale; the
|
|
115
|
+
// non-loading branch below detects the detached refs (parentNode
|
|
116
|
+
// check) and re-inserts them once real data arrives.
|
|
86
117
|
this.#changeEl.textContent = '';
|
|
87
118
|
this.#changeEl.innerHTML = '<skeleton-ui width="40%" height="1em" radius="sm"></skeleton-ui>';
|
|
88
119
|
this.#changeEl.hidden = false;
|
|
@@ -103,7 +134,39 @@ class UIStat extends UIElement {
|
|
|
103
134
|
|
|
104
135
|
// Change badge
|
|
105
136
|
if (this.change) {
|
|
106
|
-
|
|
137
|
+
// Contract: the generated icon+text pair owns [slot="change"]'s
|
|
138
|
+
// content — same "consumer content is overwritten" rule #valueEl /
|
|
139
|
+
// #labelEl already apply (their textContent is replaced every render
|
|
140
|
+
// regardless of what markup a consumer authored there). #changeEl
|
|
141
|
+
// additionally needs a specific two-node SHAPE (icon then text), not
|
|
142
|
+
// just overwritten text, so any deviation from that exact shape is
|
|
143
|
+
// normalized before writing: a consumer-authored `<stat-ui
|
|
144
|
+
// change="…"><span slot="change">stale</span></stat-ui>` gets its
|
|
145
|
+
// stale content cleared and the generated pair installed; loading's
|
|
146
|
+
// skeleton wipe (above) detaches the pair the same way and is
|
|
147
|
+
// recovered by the same branch; a disconnect/reconnect cycle that
|
|
148
|
+
// leaves an orphaned skeleton behind is also caught here (fresh
|
|
149
|
+
// #changeIconEl/#changeTextEl instances from the new connected()
|
|
150
|
+
// never match old stray children).
|
|
151
|
+
const hasOnlyTrendChildren =
|
|
152
|
+
this.#changeEl.childNodes.length === 2 &&
|
|
153
|
+
this.#changeEl.firstChild === this.#changeIconEl &&
|
|
154
|
+
this.#changeEl.lastChild === this.#changeTextEl;
|
|
155
|
+
if (!hasOnlyTrendChildren) {
|
|
156
|
+
this.#changeEl.textContent = '';
|
|
157
|
+
this.#changeEl.appendChild(this.#changeIconEl);
|
|
158
|
+
this.#changeEl.appendChild(this.#changeTextEl);
|
|
159
|
+
}
|
|
160
|
+
this.#changeTextEl.textContent = this.change;
|
|
161
|
+
if (this.trend === 'up') {
|
|
162
|
+
this.#changeIconEl.setAttribute('name', 'arrow-up');
|
|
163
|
+
this.#changeIconEl.hidden = false;
|
|
164
|
+
} else if (this.trend === 'down') {
|
|
165
|
+
this.#changeIconEl.setAttribute('name', 'arrow-down');
|
|
166
|
+
this.#changeIconEl.hidden = false;
|
|
167
|
+
} else {
|
|
168
|
+
this.#changeIconEl.hidden = true;
|
|
169
|
+
}
|
|
107
170
|
this.#changeEl.hidden = false;
|
|
108
171
|
} else {
|
|
109
172
|
this.#changeEl.hidden = true;
|
|
@@ -122,6 +185,8 @@ class UIStat extends UIElement {
|
|
|
122
185
|
this.#valueEl = null;
|
|
123
186
|
this.#labelEl = null;
|
|
124
187
|
this.#changeEl = null;
|
|
188
|
+
this.#changeIconEl = null;
|
|
189
|
+
this.#changeTextEl = null;
|
|
125
190
|
this.#iconEl = null;
|
|
126
191
|
}
|
|
127
192
|
}
|
|
@@ -112,6 +112,9 @@ tokens:
|
|
|
112
112
|
description: Font size for the primary value
|
|
113
113
|
--stat-value-weight:
|
|
114
114
|
description: Font weight for the primary value
|
|
115
|
+
requiredIcons:
|
|
116
|
+
- arrow-up
|
|
117
|
+
- arrow-down
|
|
115
118
|
a2ui:
|
|
116
119
|
rules:
|
|
117
120
|
- rule: 'Use for prominent metric/KPI displays inside dashboard cards. Value + label + optional delta indicator.'
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
"component": {
|
|
22
22
|
"const": "StepperItem"
|
|
23
23
|
},
|
|
24
|
+
"disabled": {
|
|
25
|
+
"description": "S2's phase-gating mechanism (gh#711 S1 review). Skipped in the parent's [interactive] roving tabindex/arrow order, unclickable, carries aria-disabled. Independent of [status] — a completed or upcoming step can still be disabled for a different reason (e.g. the visit phase hasn't unlocked it yet).",
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"default": false
|
|
28
|
+
},
|
|
24
29
|
"icon": {
|
|
25
30
|
"description": "Optional leading Phosphor icon name; overrides the auto-stamped numeric badge.",
|
|
26
31
|
"type": "string",
|
|
@@ -39,6 +39,15 @@ props:
|
|
|
39
39
|
- active
|
|
40
40
|
- completed
|
|
41
41
|
- error
|
|
42
|
+
disabled:
|
|
43
|
+
description: >-
|
|
44
|
+
S2's phase-gating mechanism (gh#711 S1 review). Skipped in the
|
|
45
|
+
parent's [interactive] roving tabindex/arrow order, unclickable,
|
|
46
|
+
carries aria-disabled. Independent of [status] — a completed or
|
|
47
|
+
upcoming step can still be disabled for a different reason (e.g.
|
|
48
|
+
the visit phase hasn't unlocked it yet).
|
|
49
|
+
type: boolean
|
|
50
|
+
default: false
|
|
42
51
|
slots:
|
|
43
52
|
label:
|
|
44
53
|
description: Custom label content; falls through to `[text]` prop if not slotted.
|
|
@@ -73,3 +82,5 @@ a2ui:
|
|
|
73
82
|
reason: 'Parent owns step progression.'
|
|
74
83
|
- rule: 'For chronological events (no completion semantics) use <timeline-item-ui> instead.'
|
|
75
84
|
reason: 'Stepper is process; timeline is history.'
|
|
85
|
+
- rule: 'Set [disabled] to phase-gate a step out of click/keyboard reach without changing [status].'
|
|
86
|
+
reason: 'gh#711 S1 review — S2 uses this to lock steps the current visit phase hasn''t unlocked.'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://adiaui.dev/a2ui/v0_9/components/Stepper.json",
|
|
4
4
|
"title": "Stepper",
|
|
5
|
-
"description": "Wizard / process stepper — parent step index drives numbered children's complete/current/upcoming states automatically. Renders horizontally as a labeled step bar with connectors. Use for multi-step forms, onboarding, or pipelines; for read-only event history use <timeline-ui> instead.",
|
|
5
|
+
"description": "Wizard / process stepper — parent step index drives numbered children's complete/current/upcoming states automatically. Renders horizontally as a labeled step bar with connectors. Use for multi-step forms, onboarding, or pipelines; for read-only event history use <timeline-ui> instead. Add [interactive] to opt into click-to-navigate + roving- tabindex keyboard (arrow keys / Home / End) — the host still owns whether navigation is permitted: listen for the cancelable [step-request] event and call preventDefault() to veto a step before it lands, or set [disabled] on a stepper-item-ui to phase-gate it out of reach entirely.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"allOf": [
|
|
8
8
|
{
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
"component": {
|
|
17
17
|
"const": "Stepper"
|
|
18
18
|
},
|
|
19
|
+
"interactive": {
|
|
20
|
+
"description": "Opt into click-to-navigate + roving-tabindex keyboard nav (arrow keys / Home / End / Enter / Space). Absent by default — a bare stepper is a read-only progress display, not a nav control (gh#711 S1).",
|
|
21
|
+
"type": "boolean",
|
|
22
|
+
"default": false
|
|
23
|
+
},
|
|
19
24
|
"orientation": {
|
|
20
25
|
"description": "horizontal | vertical",
|
|
21
26
|
"type": "string",
|
|
@@ -37,12 +42,40 @@
|
|
|
37
42
|
"composes": [
|
|
38
43
|
"icon-ui"
|
|
39
44
|
],
|
|
40
|
-
"events": {
|
|
45
|
+
"events": {
|
|
46
|
+
"change": {
|
|
47
|
+
"description": "Fired after a click or keyboard navigation changes [step] (i.e. only when the preceding [step-request] wasn't vetoed). Only fires when [interactive] is set.",
|
|
48
|
+
"detail": {
|
|
49
|
+
"step": {
|
|
50
|
+
"description": "New current step index.",
|
|
51
|
+
"type": "number"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"step-request": {
|
|
56
|
+
"description": "Cancelable — fires BEFORE [step] mutates, on a click or keyboard navigation. call preventDefault() to veto: the step does not change, [change] does not fire, and focus/roving-tabindex stay exactly where they were. A host that never listens for this event keeps the pre-veto uncontrolled behavior unchanged (gh#711 S1 review).",
|
|
57
|
+
"detail": {
|
|
58
|
+
"previous": {
|
|
59
|
+
"description": "The current step index before this navigation.",
|
|
60
|
+
"type": "number"
|
|
61
|
+
},
|
|
62
|
+
"step": {
|
|
63
|
+
"description": "The step index the navigation would land on if not vetoed.",
|
|
64
|
+
"type": "number"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
41
69
|
"examples": [
|
|
42
70
|
{
|
|
43
71
|
"description": "Three-step wizard",
|
|
44
72
|
"a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"Stepper\",\n \"step\": 1,\n \"children\": [\n \"s1\",\n \"s2\",\n \"s3\"\n ]\n },\n {\n \"id\": \"s1\",\n \"component\": \"StepperItem\",\n \"text\": \"Account\"\n },\n {\n \"id\": \"s2\",\n \"component\": \"StepperItem\",\n \"text\": \"Details\"\n },\n {\n \"id\": \"s3\",\n \"component\": \"StepperItem\",\n \"text\": \"Review\"\n }\n]",
|
|
45
73
|
"name": "basic"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"description": "Click/keyboard-navigable stepper — host allows jumping between steps.",
|
|
77
|
+
"a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"Stepper\",\n \"step\": 1,\n \"interactive\": true,\n \"children\": [\n \"s1\",\n \"s2\",\n \"s3\"\n ]\n },\n {\n \"id\": \"s1\",\n \"component\": \"StepperItem\",\n \"text\": \"Account\"\n },\n {\n \"id\": \"s2\",\n \"component\": \"StepperItem\",\n \"text\": \"Details\"\n },\n {\n \"id\": \"s3\",\n \"component\": \"StepperItem\",\n \"text\": \"Review\"\n }\n]",
|
|
78
|
+
"name": "interactive"
|
|
46
79
|
}
|
|
47
80
|
],
|
|
48
81
|
"keywords": [
|
|
@@ -51,7 +84,9 @@
|
|
|
51
84
|
"steps",
|
|
52
85
|
"process",
|
|
53
86
|
"onboarding",
|
|
54
|
-
"multi-step"
|
|
87
|
+
"multi-step",
|
|
88
|
+
"interactive",
|
|
89
|
+
"click-to-navigate"
|
|
55
90
|
],
|
|
56
91
|
"name": "UIStepper",
|
|
57
92
|
"related": [
|
|
@@ -28,21 +28,104 @@
|
|
|
28
28
|
*
|
|
29
29
|
* el.next(); el.prev(); el.goTo(2);
|
|
30
30
|
*
|
|
31
|
+
* <!-- Host-controlled click-to-navigate + keyboard (gh#711 S1) -->
|
|
32
|
+
* <stepper-ui step="1" interactive>
|
|
33
|
+
* <stepper-item-ui text="Account"></stepper-item-ui>
|
|
34
|
+
* <stepper-item-ui text="Details" disabled></stepper-item-ui>
|
|
35
|
+
* ...
|
|
36
|
+
* </stepper-ui>
|
|
37
|
+
*
|
|
38
|
+
* <!-- Veto a navigation before it lands (gh#711 S1 review) -->
|
|
39
|
+
* el.addEventListener('step-request', (e) => {
|
|
40
|
+
* if (!phaseAllows(e.detail.step)) e.preventDefault();
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
31
43
|
* Attributes:
|
|
32
44
|
* step — current step index (0-based)
|
|
33
45
|
* orientation — horizontal (default) | vertical
|
|
46
|
+
* interactive — opt-in click-to-navigate + roving-tabindex keyboard
|
|
47
|
+
* nav (arrow keys / Home / End). Absent by default —
|
|
48
|
+
* a bare stepper is a read-only progress display.
|
|
49
|
+
*
|
|
50
|
+
* Events (interactive only):
|
|
51
|
+
* step-request — cancelable, fires BEFORE `step` mutates.
|
|
52
|
+
* { detail: { step, previous } }. `preventDefault()`
|
|
53
|
+
* vetoes the navigation: no mutation, no `change`,
|
|
54
|
+
* focus/roving state unaffected. A host that never
|
|
55
|
+
* listens keeps today's uncontrolled behavior —
|
|
56
|
+
* this is additive, not a breaking change.
|
|
57
|
+
* change — { detail: { step } } — fires AFTER a click or
|
|
58
|
+
* keyboard navigation changes `step` (i.e. only if
|
|
59
|
+
* `step-request` wasn't vetoed). The host reacts;
|
|
60
|
+
* the stepper does not itself drive any other UI.
|
|
34
61
|
*/
|
|
35
62
|
|
|
36
63
|
import { UIElement } from '../../core/element.js';
|
|
37
64
|
|
|
65
|
+
// gh#736 — during static innerHTML parsing, a stepper-item-ui's attribute
|
|
66
|
+
// list is on the DOM node the instant the parser inserts it, but its
|
|
67
|
+
// `disabled` PROPERTY mirror only lands once its own attributeChangedCallback
|
|
68
|
+
// replay has run — which some environments (this repo's happy-dom test env,
|
|
69
|
+
// certain upgrade orderings) don't guarantee has happened yet by the time the
|
|
70
|
+
// PARENT's connectedCallback (and its first render()) fires, since children
|
|
71
|
+
// connect after their parent in tree order. Reading the attribute directly
|
|
72
|
+
// is synchronous with parsing and never lags, so it's the race-proof source
|
|
73
|
+
// of truth; the property read stays primary for the (overwhelmingly common)
|
|
74
|
+
// already-upgraded case.
|
|
75
|
+
function isDisabled(item) {
|
|
76
|
+
return item.disabled || item.hasAttribute('disabled');
|
|
77
|
+
}
|
|
78
|
+
|
|
38
79
|
export class UIStepper extends UIElement {
|
|
39
80
|
static properties = {
|
|
40
|
-
step: { type: Number,
|
|
41
|
-
orientation: { type: String,
|
|
81
|
+
step: { type: Number, default: 0, reflect: true },
|
|
82
|
+
orientation: { type: String, default: 'horizontal', reflect: true },
|
|
83
|
+
interactive: { type: Boolean, default: false, reflect: true },
|
|
42
84
|
};
|
|
43
85
|
|
|
44
86
|
static template = () => null;
|
|
45
87
|
|
|
88
|
+
// gh#736 — two related child-reactivity gaps, one observer:
|
|
89
|
+
// 1. Upgrade-order race: during static innerHTML parsing this element's
|
|
90
|
+
// connectedCallback (and its first synchronous render()) can fire
|
|
91
|
+
// BEFORE its stepper-item-ui children are even appended as DOM
|
|
92
|
+
// children (confirmed: this element's own render() sees zero items
|
|
93
|
+
// on that first pass) — the isDisabled() hasAttribute fallback in
|
|
94
|
+
// render() alone can't help when there's nothing to iterate yet.
|
|
95
|
+
// Watching `childList` catches children landing a tick later and
|
|
96
|
+
// re-renders once they're actually there.
|
|
97
|
+
// 2. A child's [disabled] flip after mount must re-render this parent's
|
|
98
|
+
// roving-tabindex/aria-disabled computation without the host
|
|
99
|
+
// manually poking render() (nav-group-ui's gh#501 idiom: a scoped
|
|
100
|
+
// MutationObserver, not a signal, since `disabled` lives on
|
|
101
|
+
// descendant DOM, not this element's own reactive properties).
|
|
102
|
+
// attributeFilter is narrow (['disabled']) and render() never writes
|
|
103
|
+
// childList or that attribute itself (it only reads `disabled`), so the
|
|
104
|
+
// observer can't feed back into its own render.
|
|
105
|
+
#onChildMutation = () => this.render();
|
|
106
|
+
#childObserver = null;
|
|
107
|
+
|
|
108
|
+
connected() {
|
|
109
|
+
this.addEventListener('click', this.#handleClick);
|
|
110
|
+
this.addEventListener('keydown', this.#handleKeydown);
|
|
111
|
+
if (!this.#childObserver) {
|
|
112
|
+
this.#childObserver = new MutationObserver(this.#onChildMutation);
|
|
113
|
+
this.#childObserver.observe(this, {
|
|
114
|
+
childList: true,
|
|
115
|
+
subtree: true,
|
|
116
|
+
attributes: true,
|
|
117
|
+
attributeFilter: ['disabled'],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
disconnected() {
|
|
123
|
+
this.removeEventListener('click', this.#handleClick);
|
|
124
|
+
this.removeEventListener('keydown', this.#handleKeydown);
|
|
125
|
+
this.#childObserver?.disconnect();
|
|
126
|
+
this.#childObserver = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
46
129
|
render() {
|
|
47
130
|
const items = [...this.querySelectorAll('stepper-item-ui')];
|
|
48
131
|
|
|
@@ -64,7 +147,40 @@ export class UIStepper extends UIElement {
|
|
|
64
147
|
} else {
|
|
65
148
|
item.setAttribute('status', 'idle');
|
|
66
149
|
}
|
|
150
|
+
|
|
151
|
+
// Roving tabindex + button semantics only when the host opts
|
|
152
|
+
// in via [interactive] — a bare stepper stays a non-focusable
|
|
153
|
+
// progress display (Rule: absent attribute is the default).
|
|
154
|
+
if (this.interactive) {
|
|
155
|
+
item.setAttribute('role', 'button');
|
|
156
|
+
if (i === this.step) item.setAttribute('aria-current', 'step');
|
|
157
|
+
else item.removeAttribute('aria-current');
|
|
158
|
+
} else {
|
|
159
|
+
item.removeAttribute('role');
|
|
160
|
+
item.removeAttribute('tabindex');
|
|
161
|
+
item.removeAttribute('aria-current');
|
|
162
|
+
item.removeAttribute('aria-disabled');
|
|
163
|
+
}
|
|
67
164
|
});
|
|
165
|
+
|
|
166
|
+
// Disabled items (S2's phase-gating mechanism) never take the
|
|
167
|
+
// roving tabindex stop and are never clickable/keyboard-reachable
|
|
168
|
+
// — an author-set [disabled] on a stepper-item-ui always wins,
|
|
169
|
+
// whatever [status] the parent computed for it above.
|
|
170
|
+
if (this.interactive) {
|
|
171
|
+
const enabled = items.filter((item) => !isDisabled(item));
|
|
172
|
+
const current = items[this.step];
|
|
173
|
+
const focusTarget = current && !isDisabled(current) ? current : enabled[0];
|
|
174
|
+
for (const item of items) {
|
|
175
|
+
if (isDisabled(item)) {
|
|
176
|
+
item.setAttribute('aria-disabled', 'true');
|
|
177
|
+
item.removeAttribute('tabindex');
|
|
178
|
+
} else {
|
|
179
|
+
item.removeAttribute('aria-disabled');
|
|
180
|
+
item.setAttribute('tabindex', item === focusTarget ? '0' : '-1');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
68
184
|
}
|
|
69
185
|
|
|
70
186
|
next() {
|
|
@@ -80,6 +196,82 @@ export class UIStepper extends UIElement {
|
|
|
80
196
|
const max = this.querySelectorAll('stepper-item-ui').length - 1;
|
|
81
197
|
this.step = Math.max(0, Math.min(index, max));
|
|
82
198
|
}
|
|
199
|
+
|
|
200
|
+
get #items() {
|
|
201
|
+
return [...this.querySelectorAll('stepper-item-ui')];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
get #enabledItems() {
|
|
205
|
+
return this.#items.filter((item) => !isDisabled(item));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** @returns {boolean} true if the navigation landed, false if vetoed or a no-op. */
|
|
209
|
+
#select(index) {
|
|
210
|
+
const items = this.#items;
|
|
211
|
+
const max = items.length - 1;
|
|
212
|
+
const next = Math.max(0, Math.min(index, max));
|
|
213
|
+
if (next === this.step || (items[next] && isDisabled(items[next]))) return false;
|
|
214
|
+
|
|
215
|
+
// Pre-mutation cancelable event (gh#711 S1 review) — the host
|
|
216
|
+
// gets a veto BEFORE `step` changes. `preventDefault()` stops
|
|
217
|
+
// the mutation and the `change` event; focus/roving are
|
|
218
|
+
// untouched either way. A host that never listens sees no
|
|
219
|
+
// difference from before this event existed.
|
|
220
|
+
const previous = this.step;
|
|
221
|
+
const proceed = this.dispatchEvent(new CustomEvent('step-request', {
|
|
222
|
+
bubbles: true,
|
|
223
|
+
cancelable: true,
|
|
224
|
+
detail: { step: next, previous },
|
|
225
|
+
}));
|
|
226
|
+
if (!proceed) return false; // vetoed
|
|
227
|
+
|
|
228
|
+
this.step = next;
|
|
229
|
+
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { step: this.step } }));
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#handleClick = (e) => {
|
|
234
|
+
if (!this.interactive) return;
|
|
235
|
+
const item = e.target.closest('stepper-item-ui');
|
|
236
|
+
if (!item || !this.contains(item) || isDisabled(item)) return;
|
|
237
|
+
const index = this.#items.indexOf(item);
|
|
238
|
+
if (index < 0) return;
|
|
239
|
+
this.#select(index);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
#handleKeydown = (e) => {
|
|
243
|
+
if (!this.interactive) return;
|
|
244
|
+
const item = e.target.closest('stepper-item-ui');
|
|
245
|
+
if (!item || !this.contains(item)) return;
|
|
246
|
+
|
|
247
|
+
// Disabled items are unreachable (no tabindex) and skipped in the
|
|
248
|
+
// arrow/Home/End order — the roving set is the enabled subset.
|
|
249
|
+
const items = this.#enabledItems;
|
|
250
|
+
const idx = items.indexOf(item);
|
|
251
|
+
if (idx < 0) return;
|
|
252
|
+
|
|
253
|
+
const isVertical = this.orientation === 'vertical';
|
|
254
|
+
const nextKey = isVertical ? 'ArrowDown' : 'ArrowRight';
|
|
255
|
+
const prevKey = isVertical ? 'ArrowUp' : 'ArrowLeft';
|
|
256
|
+
|
|
257
|
+
let next;
|
|
258
|
+
switch (e.key) {
|
|
259
|
+
case nextKey: next = idx < items.length - 1 ? idx + 1 : 0; break; // wraps to first
|
|
260
|
+
case prevKey: next = idx > 0 ? idx - 1 : items.length - 1; break; // wraps to last
|
|
261
|
+
case 'Home': next = 0; break;
|
|
262
|
+
case 'End': next = items.length - 1; break;
|
|
263
|
+
case 'Enter':
|
|
264
|
+
case ' ': e.preventDefault(); this.#select(this.#items.indexOf(item)); return;
|
|
265
|
+
default: return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
const target = items[next];
|
|
270
|
+
// Only move roving focus if the navigation actually landed — a
|
|
271
|
+
// veto leaves focus (and the tabindex roving stop) exactly where
|
|
272
|
+
// it was, per the review's "focus/roving unaffected" contract.
|
|
273
|
+
if (this.#select(this.#items.indexOf(target))) target.focus();
|
|
274
|
+
};
|
|
83
275
|
}
|
|
84
276
|
|
|
85
277
|
export class UIStepperItem extends UIElement {
|
|
@@ -92,6 +284,11 @@ export class UIStepperItem extends UIElement {
|
|
|
92
284
|
// values prevent the `active && completed` footgun the prior multi-
|
|
93
285
|
// Boolean shape allowed.
|
|
94
286
|
status: { type: String, default: 'idle', reflect: true },
|
|
287
|
+
// S2's phase-gating mechanism (gh#711 S1 review) — a disabled step
|
|
288
|
+
// is skipped in [interactive] roving tabindex/arrow order, is
|
|
289
|
+
// unclickable, and carries aria-disabled. Independent of `status`:
|
|
290
|
+
// a completed or upcoming step can be disabled for a different reason.
|
|
291
|
+
disabled: { type: Boolean, default: false, reflect: true },
|
|
95
292
|
};
|
|
96
293
|
|
|
97
294
|
static template = () => null;
|
|
@@ -253,6 +253,36 @@
|
|
|
253
253
|
:scope [slot]:empty {
|
|
254
254
|
display: none;
|
|
255
255
|
}
|
|
256
|
+
|
|
257
|
+
/* Interactive (click-to-navigate) — host opts in via [interactive]
|
|
258
|
+
on the parent; absent by default (Rule: bare stepper is a
|
|
259
|
+
read-only progress display, not a nav control). */
|
|
260
|
+
stepper-ui[interactive] > :scope {
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
outline: none;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
stepper-ui[interactive] > :scope:focus-visible::after {
|
|
266
|
+
outline: 2px solid var(--md-sys-color-primary);
|
|
267
|
+
outline-offset: 2px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* Disabled (S2's phase-gating mechanism) — never clickable, never
|
|
271
|
+
the roving-tabindex stop; visually recedes regardless of status. */
|
|
272
|
+
stepper-ui[interactive] > :scope[disabled] {
|
|
273
|
+
cursor: not-allowed;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
:scope[disabled]::after,
|
|
277
|
+
:scope[disabled] [slot="icon"] {
|
|
278
|
+
opacity: 0.5;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
:scope[disabled] [slot="label"],
|
|
282
|
+
:scope[disabled] [slot="description"] {
|
|
283
|
+
color: var(--stepper-item-pending-fg);
|
|
284
|
+
opacity: 0.5;
|
|
285
|
+
}
|
|
256
286
|
}
|
|
257
287
|
|
|
258
288
|
/* ── Vertical orientation rules — must reach across stepper-ui →
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<stepper-ui>` — Wizard / process stepper — parent step index drives numbered children's complete/current/upcoming states automatically. Renders horizontally as a labeled step bar with connectors. Use for multi-step forms, onboarding, or pipelines; for read-only event history use <timeline-ui> instead.
|
|
2
|
+
* `<stepper-ui>` — Wizard / process stepper — parent step index drives numbered children's complete/current/upcoming states automatically. Renders horizontally as a labeled step bar with connectors. Use for multi-step forms, onboarding, or pipelines; for read-only event history use <timeline-ui> instead. Add [interactive] to opt into click-to-navigate + roving- tabindex keyboard (arrow keys / Home / End) — the host still owns whether navigation is permitted: listen for the cancelable [step-request] event and call preventDefault() to veto a step before it lands, or set [disabled] on a stepper-item-ui to phase-gate it out of reach entirely.
|
|
3
3
|
*
|
|
4
4
|
* @see https://ui-kit.exe.xyz/site/components/stepper
|
|
5
5
|
*
|
|
@@ -12,16 +12,43 @@
|
|
|
12
12
|
|
|
13
13
|
import { UIElement } from '../../core/element.js';
|
|
14
14
|
|
|
15
|
+
export interface StepperChangeEventDetail {
|
|
16
|
+
/** New current step index. */
|
|
17
|
+
step: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type StepperChangeEvent = CustomEvent<StepperChangeEventDetail>;
|
|
21
|
+
export interface StepperStepRequestEventDetail {
|
|
22
|
+
/** The current step index before this navigation. */
|
|
23
|
+
previous: number;
|
|
24
|
+
/** The step index the navigation would land on if not vetoed. */
|
|
25
|
+
step: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type StepperStepRequestEvent = CustomEvent<StepperStepRequestEventDetail>;
|
|
29
|
+
|
|
15
30
|
export class UIStepper extends UIElement {
|
|
31
|
+
/** Opt into click-to-navigate + roving-tabindex keyboard nav (arrow keys / Home / End / Enter / Space). Absent by default — a bare stepper is a read-only progress display, not a nav control (gh#711 S1). */
|
|
32
|
+
interactive: boolean;
|
|
16
33
|
/** horizontal | vertical */
|
|
17
34
|
orientation: string;
|
|
18
35
|
/** Current step index (0-based). */
|
|
19
36
|
step: number;
|
|
37
|
+
|
|
38
|
+
addEventListener(type: 'change', listener: (ev: StepperChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
39
|
+
addEventListener(type: 'step-request', listener: (ev: StepperStepRequestEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
40
|
+
addEventListener<K extends keyof HTMLElementEventMap>(
|
|
41
|
+
type: K,
|
|
42
|
+
listener: (this: UIStepper, ev: HTMLElementEventMap[K]) => unknown,
|
|
43
|
+
options?: boolean | AddEventListenerOptions,
|
|
44
|
+
): void;
|
|
20
45
|
}
|
|
21
46
|
|
|
22
47
|
export class UIStepperItem extends UIElement {
|
|
23
48
|
/** Secondary description line under the label. */
|
|
24
49
|
description: string;
|
|
50
|
+
/** S2's phase-gating mechanism (gh#711 S1 review). Skipped in the parent's [interactive] roving tabindex/arrow order, unclickable, carries aria-disabled. Independent of [status] — a completed or upcoming step can still be disabled for a different reason (e.g. the visit phase hasn't unlocked it yet). */
|
|
51
|
+
disabled: boolean;
|
|
25
52
|
/** Optional leading Phosphor icon name; overrides the auto-stamped numeric badge. */
|
|
26
53
|
icon: string;
|
|
27
54
|
/** Canonical stage-progress state. Mutually exclusive (prevents the
|
|
@@ -12,7 +12,12 @@ description: >-
|
|
|
12
12
|
complete/current/upcoming states automatically. Renders horizontally as
|
|
13
13
|
a labeled step bar with connectors. Use for multi-step forms,
|
|
14
14
|
onboarding, or pipelines; for read-only event history use <timeline-ui>
|
|
15
|
-
instead.
|
|
15
|
+
instead. Add [interactive] to opt into click-to-navigate + roving-
|
|
16
|
+
tabindex keyboard (arrow keys / Home / End) — the host still owns
|
|
17
|
+
whether navigation is permitted: listen for the cancelable
|
|
18
|
+
[step-request] event and call preventDefault() to veto a step before
|
|
19
|
+
it lands, or set [disabled] on a stepper-item-ui to phase-gate it
|
|
20
|
+
out of reach entirely.
|
|
16
21
|
# Per ADR-0027 — primitives that programmatically create other primitives
|
|
17
22
|
# do NOT auto-import them. Consumer (or demo shell) must explicitly import.
|
|
18
23
|
composes:
|
|
@@ -26,7 +31,39 @@ props:
|
|
|
26
31
|
description: Current step index (0-based).
|
|
27
32
|
type: number
|
|
28
33
|
default: 0
|
|
29
|
-
|
|
34
|
+
interactive:
|
|
35
|
+
description: >-
|
|
36
|
+
Opt into click-to-navigate + roving-tabindex keyboard nav
|
|
37
|
+
(arrow keys / Home / End / Enter / Space). Absent by default —
|
|
38
|
+
a bare stepper is a read-only progress display, not a nav
|
|
39
|
+
control (gh#711 S1).
|
|
40
|
+
type: boolean
|
|
41
|
+
default: false
|
|
42
|
+
events:
|
|
43
|
+
step-request:
|
|
44
|
+
description: >-
|
|
45
|
+
Cancelable — fires BEFORE [step] mutates, on a click or keyboard
|
|
46
|
+
navigation. call preventDefault() to veto: the step does not
|
|
47
|
+
change, [change] does not fire, and focus/roving-tabindex stay
|
|
48
|
+
exactly where they were. A host that never listens for this
|
|
49
|
+
event keeps the pre-veto uncontrolled behavior unchanged
|
|
50
|
+
(gh#711 S1 review).
|
|
51
|
+
detail:
|
|
52
|
+
step:
|
|
53
|
+
type: number
|
|
54
|
+
description: The step index the navigation would land on if not vetoed.
|
|
55
|
+
previous:
|
|
56
|
+
type: number
|
|
57
|
+
description: The current step index before this navigation.
|
|
58
|
+
change:
|
|
59
|
+
description: >-
|
|
60
|
+
Fired after a click or keyboard navigation changes [step] (i.e.
|
|
61
|
+
only when the preceding [step-request] wasn't vetoed).
|
|
62
|
+
Only fires when [interactive] is set.
|
|
63
|
+
detail:
|
|
64
|
+
step:
|
|
65
|
+
type: number
|
|
66
|
+
description: New current step index.
|
|
30
67
|
slots:
|
|
31
68
|
default:
|
|
32
69
|
description: "Default slot — primary child content."
|
|
@@ -43,6 +80,10 @@ a2ui:
|
|
|
43
80
|
reason: 'Surface examples.'
|
|
44
81
|
- rule: 'For read-only event history use <timeline-ui>; stepper requires forward progress semantics.'
|
|
45
82
|
reason: 'Decision rule vs timeline.'
|
|
83
|
+
- rule: 'Add [interactive] only when the host permits jumping to a step out of order; leave absent for a linear wizard the host advances programmatically.'
|
|
84
|
+
reason: 'Click-to-navigate is host-controlled, not automatic (gh#711 S1).'
|
|
85
|
+
- rule: 'Listen for [step-request] and call preventDefault() to veto a navigation conditionally (e.g. block a phase the app has not unlocked); set [disabled] on a stepper-item-ui to phase-gate it out of reach unconditionally.'
|
|
86
|
+
reason: 'Two host-controlled gates at different granularities (gh#711 S1 review).'
|
|
46
87
|
anti_patterns: []
|
|
47
88
|
examples:
|
|
48
89
|
- name: basic
|
|
@@ -75,6 +116,37 @@ examples:
|
|
|
75
116
|
"text": "Review"
|
|
76
117
|
}
|
|
77
118
|
]
|
|
119
|
+
- name: interactive
|
|
120
|
+
description: Click/keyboard-navigable stepper — host allows jumping between steps.
|
|
121
|
+
a2ui: >-
|
|
122
|
+
[
|
|
123
|
+
{
|
|
124
|
+
"id": "root",
|
|
125
|
+
"component": "Stepper",
|
|
126
|
+
"step": 1,
|
|
127
|
+
"interactive": true,
|
|
128
|
+
"children": [
|
|
129
|
+
"s1",
|
|
130
|
+
"s2",
|
|
131
|
+
"s3"
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "s1",
|
|
136
|
+
"component": "StepperItem",
|
|
137
|
+
"text": "Account"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"id": "s2",
|
|
141
|
+
"component": "StepperItem",
|
|
142
|
+
"text": "Details"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": "s3",
|
|
146
|
+
"component": "StepperItem",
|
|
147
|
+
"text": "Review"
|
|
148
|
+
}
|
|
149
|
+
]
|
|
78
150
|
keywords:
|
|
79
151
|
- stepper
|
|
80
152
|
- wizard
|
|
@@ -82,6 +154,8 @@ keywords:
|
|
|
82
154
|
- process
|
|
83
155
|
- onboarding
|
|
84
156
|
- multi-step
|
|
157
|
+
- interactive
|
|
158
|
+
- click-to-navigate
|
|
85
159
|
synonyms:
|
|
86
160
|
timeline-ui mode=steps: stepper-ui
|
|
87
161
|
related:
|