stimeo-ui 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +184 -0
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +3 -5
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +195 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +5 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +90 -10
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +31 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js +8 -3
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +4 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +9 -3
- package/dist/controllers/color_picker_controller.js +41 -11
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js +2 -2
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +5 -3
- package/dist/controllers/countdown_controller.js +5 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +162 -31
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +1 -1
- package/dist/controllers/direct_upload_controller.js +3 -3
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +27 -7
- package/dist/controllers/empty_state_controller.js +107 -16
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +7 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +25 -5
- package/dist/controllers/flash_controller.js +161 -21
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -3
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +8 -2
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +19 -2
- package/dist/controllers/frame_loading_controller.js +94 -22
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +8 -4
- package/dist/controllers/highlight_controller.js +38 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +2 -2
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +5 -4
- package/dist/controllers/idle_controller.js +39 -6
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +5 -3
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +3 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +46 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +15 -13
- package/dist/controllers/network_status_controller.js +1 -3
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +29 -3
- package/dist/controllers/number_input_controller.js +455 -64
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +5 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js +38 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +4 -4
- package/dist/controllers/password_strength_controller.js +21 -3
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +4 -4
- package/dist/controllers/persist_controller.js +24 -5
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +10 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -2
- package/dist/controllers/portal_controller.js +10 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +1 -1
- package/dist/controllers/progress_controller.js +7 -3
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +41 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +23 -0
- package/dist/controllers/range_slider_controller.js +385 -94
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +36 -31
- package/dist/controllers/rating_controller.js +274 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +2 -0
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +4 -0
- package/dist/controllers/roving_controller.js +60 -5
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -1
- package/dist/controllers/scroll_area_controller.js +155 -23
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js +13 -17
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -2
- package/dist/controllers/skeleton_controller.js +71 -3
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +17 -1
- package/dist/controllers/slider_controller.js +325 -48
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +15 -10
- package/dist/controllers/spinner_controller.js +18 -3
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +1 -1
- package/dist/controllers/step_indicator_controller.js +3 -1
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.js +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -8
- package/dist/controllers/switch_controller.js +162 -18
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +41 -11
- package/dist/controllers/tags_input_controller.js +356 -120
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +42 -10
- package/dist/controllers/time_picker_controller.js +296 -104
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +39 -13
- package/dist/controllers/toggle_group_controller.js +378 -55
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +5 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +2 -2
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +2 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +1 -2
- package/dist/controllers/tree_view_controller.js +24 -4
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +3811 -1048
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +92 -4
- package/dist/inspector/cli.js +174 -6
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +234 -8
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +24 -24
- package/dist/inspector/manifest.json +573 -59
- package/dist/positioning/index.d.ts +2 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -8,11 +8,46 @@ function isRtl(element) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
// src/utils/arrow_step.ts
|
|
11
|
+
function logicalArrowStep(key, element) {
|
|
12
|
+
if (key === "ArrowDown") return 1;
|
|
13
|
+
if (key === "ArrowUp") return -1;
|
|
14
|
+
if (key !== "ArrowRight" && key !== "ArrowLeft") return 0;
|
|
15
|
+
const forward = isRtl(element) ? "ArrowLeft" : "ArrowRight";
|
|
16
|
+
return key === forward ? 1 : -1;
|
|
17
|
+
}
|
|
11
18
|
function isReservedArrowChord(event, allow = []) {
|
|
12
19
|
if (!event.key.startsWith("Arrow")) return false;
|
|
13
20
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
14
21
|
}
|
|
15
22
|
|
|
23
|
+
// src/utils/focus_candidate.ts
|
|
24
|
+
function inheritsFieldsetDisabled(control) {
|
|
25
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
26
|
+
while (fieldset) {
|
|
27
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
28
|
+
if (!legend?.contains(control)) return true;
|
|
29
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/utils/interactive_host.ts
|
|
35
|
+
var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
|
|
36
|
+
function isInteractiveHost(element) {
|
|
37
|
+
if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
|
|
38
|
+
let current = element;
|
|
39
|
+
while (current) {
|
|
40
|
+
const raw = current.getAttribute("contenteditable");
|
|
41
|
+
if (raw !== null) {
|
|
42
|
+
const value = raw.trim().toLowerCase();
|
|
43
|
+
if (value === "false") return false;
|
|
44
|
+
if (value === "" || value === "true" || value === "plaintext-only") return true;
|
|
45
|
+
}
|
|
46
|
+
current = current.parentElement;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
16
51
|
// src/utils/roving_tabindex.ts
|
|
17
52
|
var RovingTabindex = class {
|
|
18
53
|
/** Returns the current ordered item elements; called on every operation. */
|
|
@@ -35,10 +70,12 @@ var RovingTabindex = class {
|
|
|
35
70
|
* "nothing is currently tabbable".
|
|
36
71
|
*
|
|
37
72
|
* @param index - Position of the item to make tabbable.
|
|
38
|
-
* @param options - Pass `{ focus: true }` to also move DOM focus to that item
|
|
73
|
+
* @param options - Pass `{ focus: true }` to also move DOM focus to that item,
|
|
74
|
+
* and `items` to reuse an event-scoped collection snapshot.
|
|
39
75
|
*/
|
|
40
|
-
setActive(index,
|
|
41
|
-
const
|
|
76
|
+
setActive(index, options = {}) {
|
|
77
|
+
const { focus = false } = options;
|
|
78
|
+
const items = options.items ?? this.#getItems();
|
|
42
79
|
items.forEach((item, i) => {
|
|
43
80
|
item.tabIndex = i === index ? 0 : -1;
|
|
44
81
|
});
|
|
@@ -52,6 +89,16 @@ function rovingMove(current, length, delta, wrap) {
|
|
|
52
89
|
}
|
|
53
90
|
|
|
54
91
|
// src/controllers/toggle_group_controller.ts
|
|
92
|
+
var ITEM_ATTRIBUTES = [
|
|
93
|
+
"aria-pressed",
|
|
94
|
+
"disabled",
|
|
95
|
+
"hidden",
|
|
96
|
+
"type",
|
|
97
|
+
"href",
|
|
98
|
+
"contenteditable",
|
|
99
|
+
"controls"
|
|
100
|
+
];
|
|
101
|
+
var hasModifier = (event) => event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
55
102
|
var ToggleGroupController = class extends Controller {
|
|
56
103
|
static targets = ["item"];
|
|
57
104
|
static values = {
|
|
@@ -59,86 +106,362 @@ var ToggleGroupController = class extends Controller {
|
|
|
59
106
|
};
|
|
60
107
|
static actions = ["onKeydown", "toggle"];
|
|
61
108
|
static events = ["change"];
|
|
62
|
-
#roving = new RovingTabindex(() => this
|
|
63
|
-
|
|
109
|
+
#roving = new RovingTabindex(() => this.#managedTargets);
|
|
110
|
+
#handledEvents = /* @__PURE__ */ new WeakSet();
|
|
111
|
+
#managedItems = /* @__PURE__ */ new Set();
|
|
112
|
+
#originalTabindex = /* @__PURE__ */ new Map();
|
|
113
|
+
#ownedPressed = /* @__PURE__ */ new Set();
|
|
114
|
+
#internalPressedValues = /* @__PURE__ */ new Map();
|
|
115
|
+
#observer = null;
|
|
116
|
+
#connected = false;
|
|
117
|
+
/**
|
|
118
|
+
* Normalizes state and establishes one roving entry point. A single authored
|
|
119
|
+
* `tabindex="0"` survives reconnect; otherwise the first pressed, navigable
|
|
120
|
+
* item wins, falling back to the first navigable item.
|
|
121
|
+
*/
|
|
64
122
|
connect() {
|
|
65
|
-
const
|
|
66
|
-
|
|
123
|
+
const currentItems = new Set(this.itemTargets);
|
|
124
|
+
for (const item of this.#managedItems) {
|
|
125
|
+
if (!currentItems.has(item)) this.#releaseItem(item);
|
|
126
|
+
}
|
|
127
|
+
const authoredStops = this.itemTargets.filter(
|
|
128
|
+
(item) => this.#isSupportedHost(item) && item.getAttribute("tabindex") === "0" && this.#isNavigable(item)
|
|
129
|
+
);
|
|
130
|
+
const preferred = authoredStops.length === 1 ? authoredStops[0] : null;
|
|
131
|
+
for (const item of this.itemTargets) this.#reconcileHost(item, false);
|
|
132
|
+
this.#normalizeSelection();
|
|
133
|
+
this.#ensureTabStop(preferred);
|
|
134
|
+
this.element.addEventListener("click", this.#onClickCapture, true);
|
|
135
|
+
this.element.addEventListener("keydown", this.#onKeydownCapture, true);
|
|
136
|
+
this.element.addEventListener("click", this.#onClick);
|
|
137
|
+
this.element.addEventListener("keydown", this.#onKeydown);
|
|
138
|
+
this.element.addEventListener("focusin", this.#onFocusin);
|
|
139
|
+
this.#connected = true;
|
|
140
|
+
this.#internalPressedValues.clear();
|
|
141
|
+
this.#observeMutations();
|
|
142
|
+
}
|
|
143
|
+
/** Releases every listener and observer while retaining DOM state for Turbo cache/reconnect. */
|
|
144
|
+
disconnect() {
|
|
145
|
+
this.#connected = false;
|
|
146
|
+
this.element.removeEventListener("click", this.#onClickCapture, true);
|
|
147
|
+
this.element.removeEventListener("keydown", this.#onKeydownCapture, true);
|
|
148
|
+
this.element.removeEventListener("click", this.#onClick);
|
|
149
|
+
this.element.removeEventListener("keydown", this.#onKeydown);
|
|
150
|
+
this.element.removeEventListener("focusin", this.#onFocusin);
|
|
151
|
+
this.#observer?.disconnect();
|
|
152
|
+
this.#observer = null;
|
|
153
|
+
this.#internalPressedValues.clear();
|
|
154
|
+
}
|
|
155
|
+
/** Drops a newly connected item from the Tab sequence before reconciling the group. */
|
|
156
|
+
itemTargetConnected(item) {
|
|
157
|
+
if (!this.#connected) return;
|
|
158
|
+
this.#reconcileHost(item, true);
|
|
159
|
+
this.#normalizeSelection();
|
|
160
|
+
this.#ensureTabStop();
|
|
67
161
|
}
|
|
68
|
-
/**
|
|
162
|
+
/** Restores attributes owned only while an element is an item and repairs the Tab stop. */
|
|
163
|
+
itemTargetDisconnected(item) {
|
|
164
|
+
if (!this.#connected) return;
|
|
165
|
+
this.#releaseItem(item);
|
|
166
|
+
this.#normalizeSelection();
|
|
167
|
+
this.#ensureTabStop();
|
|
168
|
+
}
|
|
169
|
+
/** Reconciles the single-selection invariant when the Stimulus Value changes. */
|
|
170
|
+
modeValueChanged() {
|
|
171
|
+
if (!this.#connected) return;
|
|
172
|
+
this.#normalizeSelection();
|
|
173
|
+
this.#ensureTabStop();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Toggles the action's item. Per-item action wiring is optional because click
|
|
177
|
+
* is also delegated from the group.
|
|
178
|
+
*/
|
|
69
179
|
toggle(event) {
|
|
70
|
-
|
|
180
|
+
if (event.defaultPrevented) return;
|
|
181
|
+
const item = event.currentTarget;
|
|
182
|
+
if (!item || !this.itemTargets.includes(item)) return;
|
|
183
|
+
this.#handledEvents.add(event);
|
|
184
|
+
this.#toggleItem(item);
|
|
71
185
|
}
|
|
72
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Moves focus or activates the action's item. Per-item action wiring is
|
|
188
|
+
* optional because keydown is also delegated from the group.
|
|
189
|
+
*/
|
|
73
190
|
onKeydown(event) {
|
|
74
|
-
|
|
191
|
+
const item = event.currentTarget;
|
|
192
|
+
if (!item || !this.itemTargets.includes(item)) return;
|
|
193
|
+
this.#handleKeydown(event, item);
|
|
194
|
+
}
|
|
195
|
+
/** Blocks disabled activation before per-item or consumer event handlers run. */
|
|
196
|
+
#onClickCapture = (event) => {
|
|
197
|
+
const item = this.#itemForEventTarget(event.target);
|
|
198
|
+
if (!item || !this.#isSupportedHost(item) || !this.#isActivationDisabled(item)) return;
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
event.stopImmediatePropagation();
|
|
201
|
+
};
|
|
202
|
+
/** Blocks disabled keyboard activation while leaving navigation keys available. */
|
|
203
|
+
#onKeydownCapture = (event) => {
|
|
204
|
+
if (event.key !== " " && event.key !== "Enter") return;
|
|
205
|
+
const item = this.#itemForEventTarget(event.target);
|
|
206
|
+
if (!item || !this.#isSupportedHost(item) || !this.#isActivationDisabled(item)) return;
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
event.stopImmediatePropagation();
|
|
209
|
+
};
|
|
210
|
+
/** Delegated click path for static and runtime-added items without actions. */
|
|
211
|
+
#onClick = (event) => {
|
|
212
|
+
if (this.#handledEvents.delete(event) || event.defaultPrevented) return;
|
|
213
|
+
const item = this.#itemForEventTarget(event.target);
|
|
214
|
+
if (item) this.#toggleItem(item);
|
|
215
|
+
};
|
|
216
|
+
/** Delegated keydown path for static and runtime-added items without actions. */
|
|
217
|
+
#onKeydown = (event) => {
|
|
218
|
+
const item = this.#itemForEventTarget(event.target);
|
|
219
|
+
if (item) this.#handleKeydown(event, item);
|
|
220
|
+
};
|
|
221
|
+
/** Keeps programmatic and pointer focus as the group's next Tab entry point. */
|
|
222
|
+
#onFocusin = (event) => {
|
|
223
|
+
const item = this.#itemForEventTarget(event.target);
|
|
224
|
+
if (!item || !this.#isNavigable(item)) return;
|
|
225
|
+
this.#setActive(item);
|
|
226
|
+
};
|
|
227
|
+
/** Applies the APG navigation and activation key map to one supported item. */
|
|
228
|
+
#handleKeydown(event, item) {
|
|
229
|
+
if (event.defaultPrevented || !this.#isSupportedHost(item)) return;
|
|
230
|
+
if (event.isComposing) return;
|
|
75
231
|
if (isReservedArrowChord(event)) return;
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
case "ArrowDown":
|
|
84
|
-
next = rovingMove(
|
|
85
|
-
current,
|
|
86
|
-
this.itemTargets.length,
|
|
87
|
-
event.key === "ArrowDown" ? 1 : horizontalStep(event.key));
|
|
88
|
-
break;
|
|
89
|
-
case "ArrowLeft":
|
|
90
|
-
case "ArrowUp":
|
|
91
|
-
next = rovingMove(
|
|
92
|
-
current,
|
|
93
|
-
this.itemTargets.length,
|
|
94
|
-
event.key === "ArrowUp" ? -1 : horizontalStep(event.key));
|
|
95
|
-
break;
|
|
96
|
-
case "Home":
|
|
97
|
-
next = 0;
|
|
98
|
-
break;
|
|
99
|
-
case "End":
|
|
100
|
-
next = this.itemTargets.length - 1;
|
|
101
|
-
break;
|
|
102
|
-
case " ":
|
|
103
|
-
case "Enter":
|
|
104
|
-
if (event.currentTarget instanceof HTMLButtonElement) return;
|
|
232
|
+
if ((event.key === "Home" || event.key === "End") && hasModifier(event)) return;
|
|
233
|
+
if (!this.#managedItems.has(item)) this.#reconcileHost(item, true);
|
|
234
|
+
this.#ensureTabStop();
|
|
235
|
+
const items = this.#managedTargets;
|
|
236
|
+
const current = items.indexOf(item);
|
|
237
|
+
if (event.key === " " || event.key === "Enter") {
|
|
238
|
+
if (this.#isActivationDisabled(item)) {
|
|
105
239
|
event.preventDefault();
|
|
106
|
-
|
|
240
|
+
event.stopImmediatePropagation();
|
|
107
241
|
return;
|
|
108
|
-
|
|
242
|
+
}
|
|
243
|
+
if (item instanceof HTMLButtonElement) {
|
|
244
|
+
if (event.repeat) event.preventDefault();
|
|
109
245
|
return;
|
|
246
|
+
}
|
|
247
|
+
event.preventDefault();
|
|
248
|
+
if (!event.repeat) this.#toggleItem(item);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const step = logicalArrowStep(event.key, this.element);
|
|
252
|
+
let destination;
|
|
253
|
+
if (step !== 0) {
|
|
254
|
+
destination = this.#nextNavigable(current, step);
|
|
255
|
+
} else if (event.key === "Home") {
|
|
256
|
+
destination = this.#navigableItems[0];
|
|
257
|
+
} else if (event.key === "End") {
|
|
258
|
+
const navigable = this.#navigableItems;
|
|
259
|
+
destination = navigable[navigable.length - 1];
|
|
260
|
+
} else {
|
|
261
|
+
return;
|
|
110
262
|
}
|
|
111
263
|
event.preventDefault();
|
|
112
|
-
this.#
|
|
264
|
+
if (destination) this.#setActive(destination, true);
|
|
113
265
|
}
|
|
114
|
-
/** Applies
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
if (!item)
|
|
266
|
+
/** Applies one user toggle and dispatches the documented change detail. */
|
|
267
|
+
#toggleItem(item) {
|
|
268
|
+
if (!this.#isSupportedHost(item) || this.#isActivationDisabled(item)) return;
|
|
269
|
+
if (!this.#managedItems.has(item)) this.#reconcileHost(item, true);
|
|
118
270
|
const willPress = !this.#isPressed(item);
|
|
119
271
|
if (this.modeValue === "single") {
|
|
120
|
-
|
|
121
|
-
this.#setPressed(other,
|
|
122
|
-
});
|
|
272
|
+
for (const other of this.#managedTargets)
|
|
273
|
+
this.#setPressed(other, other === item && willPress);
|
|
123
274
|
} else {
|
|
124
275
|
this.#setPressed(item, willPress);
|
|
125
276
|
}
|
|
126
|
-
this.#
|
|
277
|
+
this.#setActive(item);
|
|
127
278
|
this.dispatch("change", {
|
|
128
279
|
detail: { value: this.#itemValue(item), pressed: willPress, values: this.#pressedValues() }
|
|
129
280
|
});
|
|
130
281
|
}
|
|
282
|
+
/** Reconciles supported hosts, pressed state, and the roving invariant after a DOM mutation. */
|
|
283
|
+
#reconcile() {
|
|
284
|
+
for (const item of this.itemTargets) this.#reconcileHost(item, true);
|
|
285
|
+
this.#normalizeSelection();
|
|
286
|
+
this.#ensureTabStop();
|
|
287
|
+
}
|
|
288
|
+
/** Begins or ends ownership according to the item's current host semantics. */
|
|
289
|
+
#reconcileHost(item, dropFromTabSequence) {
|
|
290
|
+
if (!this.#isSupportedHost(item)) {
|
|
291
|
+
this.#releaseItem(item);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (!this.#managedItems.has(item)) {
|
|
295
|
+
this.#managedItems.add(item);
|
|
296
|
+
this.#originalTabindex.set(item, item.getAttribute("tabindex"));
|
|
297
|
+
if (dropFromTabSequence) item.tabIndex = -1;
|
|
298
|
+
}
|
|
299
|
+
this.#normalizePressed(item);
|
|
300
|
+
}
|
|
301
|
+
/** Restores only defaults owned because an element belonged to this group. */
|
|
302
|
+
#releaseItem(item) {
|
|
303
|
+
if (!this.#managedItems.delete(item)) return;
|
|
304
|
+
const original = this.#originalTabindex.get(item);
|
|
305
|
+
if (original === null) item.removeAttribute("tabindex");
|
|
306
|
+
else if (original !== void 0) item.setAttribute("tabindex", original);
|
|
307
|
+
this.#originalTabindex.delete(item);
|
|
308
|
+
if (this.#ownedPressed.delete(item)) item.removeAttribute("aria-pressed");
|
|
309
|
+
this.#internalPressedValues.delete(item);
|
|
310
|
+
}
|
|
311
|
+
/** Supplies a missing pressed state and normalizes invalid ARIA tokens. */
|
|
312
|
+
#normalizePressed(item) {
|
|
313
|
+
const value = item.getAttribute("aria-pressed");
|
|
314
|
+
if (value === null) {
|
|
315
|
+
this.#ownedPressed.add(item);
|
|
316
|
+
this.#internalPressedValues.set(item, "false");
|
|
317
|
+
item.setAttribute("aria-pressed", "false");
|
|
318
|
+
} else if (value !== "true" && value !== "false") {
|
|
319
|
+
item.setAttribute("aria-pressed", "false");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** Makes the first DOM-ordered pressed item the sole pressed item in single mode. */
|
|
323
|
+
#normalizeSelection() {
|
|
324
|
+
for (const item of this.#managedTargets) this.#normalizePressed(item);
|
|
325
|
+
if (this.modeValue !== "single") return;
|
|
326
|
+
let found = false;
|
|
327
|
+
for (const item of this.#managedTargets) {
|
|
328
|
+
if (!this.#isPressed(item)) continue;
|
|
329
|
+
if (!found) found = true;
|
|
330
|
+
else this.#setPressed(item, false);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** Keeps one navigable Tab stop, preferring current DOM state after initial connect. */
|
|
334
|
+
#ensureTabStop(initialPreference) {
|
|
335
|
+
const navigable = this.#navigableItems;
|
|
336
|
+
let active;
|
|
337
|
+
if (initialPreference !== void 0) {
|
|
338
|
+
active = initialPreference ?? void 0;
|
|
339
|
+
} else {
|
|
340
|
+
const current = this.#managedTargets.filter(
|
|
341
|
+
(item) => item.tabIndex === 0 && this.#isNavigable(item)
|
|
342
|
+
);
|
|
343
|
+
if (current.length === 1) active = current[0];
|
|
344
|
+
}
|
|
345
|
+
active ??= navigable.find((item) => this.#isPressed(item));
|
|
346
|
+
active ??= navigable[0];
|
|
347
|
+
this.#setActive(active ?? null);
|
|
348
|
+
}
|
|
349
|
+
/** Moves through the full item order until a navigable destination is found. */
|
|
350
|
+
#nextNavigable(fromIndex, delta) {
|
|
351
|
+
const items = this.#managedTargets;
|
|
352
|
+
let index = fromIndex;
|
|
353
|
+
for (let step = 0; step < items.length; step++) {
|
|
354
|
+
index = rovingMove(index, items.length, delta);
|
|
355
|
+
const candidate = items[index];
|
|
356
|
+
if (candidate && this.#isNavigable(candidate)) return candidate;
|
|
357
|
+
}
|
|
358
|
+
return void 0;
|
|
359
|
+
}
|
|
360
|
+
/** Assigns the single roving Tab stop and optionally moves DOM focus. */
|
|
361
|
+
#setActive(item, focus = false) {
|
|
362
|
+
this.#roving.setActive(this.#managedTargets.indexOf(item), { focus });
|
|
363
|
+
}
|
|
364
|
+
/** Finds this group's item containing an event target, excluding nested groups. */
|
|
365
|
+
#itemForEventTarget(target) {
|
|
366
|
+
if (!this.#ownsEventTarget(target)) return void 0;
|
|
367
|
+
const node = target;
|
|
368
|
+
return this.itemTargets.find((item) => item === node || item.contains(node));
|
|
369
|
+
}
|
|
370
|
+
/** Whether the closest Toggle Group scope around a target is this instance. */
|
|
371
|
+
#ownsEventTarget(target) {
|
|
372
|
+
return target instanceof Element && target.closest('[data-controller~="stimeo--toggle-group"]') === this.element;
|
|
373
|
+
}
|
|
374
|
+
/** Hosts whose activation model can be owned without conflicting native behavior. */
|
|
375
|
+
#isSupportedHost(item) {
|
|
376
|
+
if (item instanceof HTMLButtonElement) return item.type === "button";
|
|
377
|
+
return !isInteractiveHost(item);
|
|
378
|
+
}
|
|
379
|
+
/** Items eligible for roving focus (ARIA-disabled deliberately remains eligible). */
|
|
380
|
+
#isNavigable(item) {
|
|
381
|
+
if (!this.#isSupportedHost(item) || this.#isHidden(item)) return false;
|
|
382
|
+
if (!(item instanceof HTMLButtonElement)) return true;
|
|
383
|
+
return !item.disabled && !inheritsFieldsetDisabled(item);
|
|
384
|
+
}
|
|
385
|
+
/** Whether hidden applies on the path from an item up to the group root. */
|
|
386
|
+
#isHidden(item) {
|
|
387
|
+
let current = item;
|
|
388
|
+
while (current && current !== this.element) {
|
|
389
|
+
if (current.hasAttribute("hidden")) return true;
|
|
390
|
+
current = current.parentElement;
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
/** Whether ARIA, visibility, or native HTML semantics suppress activation. */
|
|
395
|
+
#isActivationDisabled(item) {
|
|
396
|
+
if (!this.#isNavigable(item)) return true;
|
|
397
|
+
let current = item;
|
|
398
|
+
while (current) {
|
|
399
|
+
if (current.getAttribute("aria-disabled") === "true") return true;
|
|
400
|
+
current = current.parentElement;
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
/** Releases default ownership when a Turbo morph authors `aria-pressed`. */
|
|
405
|
+
#releaseAuthoredPressed(records) {
|
|
406
|
+
for (const record of records) {
|
|
407
|
+
if (record.attributeName !== "aria-pressed") continue;
|
|
408
|
+
const item = record.target;
|
|
409
|
+
const value = item.getAttribute("aria-pressed");
|
|
410
|
+
if (value !== null && this.#internalPressedValues.get(item) !== value) {
|
|
411
|
+
this.#ownedPressed.delete(item);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
this.#internalPressedValues.clear();
|
|
415
|
+
}
|
|
416
|
+
/** Watches target membership, retained item hosts/state, and inherited host/fieldset state. */
|
|
417
|
+
#observeMutations() {
|
|
418
|
+
const observer = new MutationObserver((records) => {
|
|
419
|
+
this.#releaseAuthoredPressed(records);
|
|
420
|
+
observer.disconnect();
|
|
421
|
+
this.#reconcile();
|
|
422
|
+
this.#observeWith(observer);
|
|
423
|
+
});
|
|
424
|
+
this.#observer = observer;
|
|
425
|
+
this.#observeWith(observer);
|
|
426
|
+
}
|
|
427
|
+
/** Registers every root and ancestor observation on one observer instance. */
|
|
428
|
+
#observeWith(observer) {
|
|
429
|
+
observer.observe(this.element, {
|
|
430
|
+
subtree: true,
|
|
431
|
+
childList: true,
|
|
432
|
+
attributes: true,
|
|
433
|
+
attributeFilter: ITEM_ATTRIBUTES
|
|
434
|
+
});
|
|
435
|
+
let ancestor = this.element.parentElement;
|
|
436
|
+
while (ancestor) {
|
|
437
|
+
observer.observe(ancestor, {
|
|
438
|
+
attributes: true,
|
|
439
|
+
attributeFilter: ["contenteditable", "disabled"]
|
|
440
|
+
});
|
|
441
|
+
ancestor = ancestor.parentElement;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/** Supported targets currently managed by the roving primitive. */
|
|
445
|
+
get #managedTargets() {
|
|
446
|
+
return this.itemTargets.filter((item) => this.#managedItems.has(item));
|
|
447
|
+
}
|
|
448
|
+
/** Managed items eligible for the roving Tab stop. */
|
|
449
|
+
get #navigableItems() {
|
|
450
|
+
return this.#managedTargets.filter((item) => this.#isNavigable(item));
|
|
451
|
+
}
|
|
131
452
|
/** Whether an item is currently pressed. */
|
|
132
453
|
#isPressed(item) {
|
|
133
454
|
return item.getAttribute("aria-pressed") === "true";
|
|
134
455
|
}
|
|
135
|
-
/** Reflects
|
|
456
|
+
/** Reflects pressed state while distinguishing controller writes from authored morphs. */
|
|
136
457
|
#setPressed(item, pressed) {
|
|
137
|
-
|
|
458
|
+
const value = pressed ? "true" : "false";
|
|
459
|
+
this.#internalPressedValues.set(item, value);
|
|
460
|
+
item.setAttribute("aria-pressed", value);
|
|
138
461
|
}
|
|
139
|
-
/** The
|
|
462
|
+
/** The DOM-ordered values of every currently pressed managed item. */
|
|
140
463
|
#pressedValues() {
|
|
141
|
-
return this.
|
|
464
|
+
return this.#managedTargets.filter((item) => this.#isPressed(item)).map((item) => this.#itemValue(item));
|
|
142
465
|
}
|
|
143
466
|
/** An item's value (`data-value`, defaulting to empty). */
|
|
144
467
|
#itemValue(item) {
|