stimeo-ui 0.16.0 → 0.18.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +136 -0
- data/README.md +27 -11
- data/dist/cable/index.js +384 -449
- data/dist/controllers/accordion_controller.js +174 -55
- data/dist/controllers/alert_dialog_controller.js +903 -235
- data/dist/controllers/announcer_controller.js +238 -205
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +414 -145
- data/dist/controllers/avatar_controller.js +185 -107
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +282 -58
- data/dist/controllers/calendar_controller.js +228 -260
- data/dist/controllers/carousel_controller.js +536 -436
- data/dist/controllers/character_counter_controller.js +493 -207
- data/dist/controllers/checkbox_controller.js +38 -74
- data/dist/controllers/clipboard_controller.js +261 -149
- data/dist/controllers/collapsible_controller.js +304 -120
- data/dist/controllers/color_picker_controller.js +178 -146
- data/dist/controllers/combobox_controller.js +522 -156
- data/dist/controllers/command_palette_controller.js +1156 -395
- data/dist/controllers/conditional_fields_controller.js +300 -171
- data/dist/controllers/confirm_controller.js +980 -262
- data/dist/controllers/context_menu_controller.js +346 -153
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +281 -158
- data/dist/controllers/currency_input_controller.js +575 -225
- data/dist/controllers/data_grid_controller.js +248 -214
- data/dist/controllers/date_range_picker_controller.js +338 -235
- data/dist/controllers/dialog_controller.js +899 -237
- data/dist/controllers/direct_upload_controller.js +97 -167
- data/dist/controllers/dirty_form_controller.js +72 -69
- data/dist/controllers/dismissible_controller.js +196 -24
- data/dist/controllers/drawer_controller.js +974 -355
- data/dist/controllers/dropdown_controller.js +339 -96
- data/dist/controllers/editable_controller.js +315 -88
- data/dist/controllers/empty_state_controller.js +79 -58
- data/dist/controllers/file_dropzone_controller.js +322 -211
- data/dist/controllers/filter_controller.js +77 -42
- data/dist/controllers/flash_controller.js +238 -347
- data/dist/controllers/focus_controller.js +679 -240
- data/dist/controllers/form_field_controller.js +156 -132
- data/dist/controllers/form_validation_controller.js +31 -97
- data/dist/controllers/frame_loading_controller.js +275 -217
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +327 -130
- data/dist/controllers/idle_controller.js +271 -101
- data/dist/controllers/input_mask_controller.js +483 -97
- data/dist/controllers/intersection_controller.js +150 -106
- data/dist/controllers/lazy_frame_controller.js +24 -68
- data/dist/controllers/listbox_controller.js +523 -240
- data/dist/controllers/local_time_controller.js +102 -77
- data/dist/controllers/masonry_controller.js +80 -96
- data/dist/controllers/menu_controller.js +302 -188
- data/dist/controllers/menubar_controller.js +232 -345
- data/dist/controllers/meter_controller.js +417 -87
- data/dist/controllers/multi_select_controller.js +785 -393
- data/dist/controllers/navigation_menu_controller.js +190 -250
- data/dist/controllers/nested_form_controller.js +396 -192
- data/dist/controllers/network_status_controller.js +268 -62
- data/dist/controllers/number_input_controller.js +610 -303
- data/dist/controllers/optimistic_controller.js +45 -87
- data/dist/controllers/otp_controller.js +658 -255
- data/dist/controllers/overflow_indicator_controller.js +122 -113
- data/dist/controllers/overflow_menu_controller.js +470 -295
- data/dist/controllers/pagination_controller.js +332 -123
- data/dist/controllers/password_reveal_controller.js +392 -121
- data/dist/controllers/password_strength_controller.js +111 -158
- data/dist/controllers/persist_controller.js +165 -111
- data/dist/controllers/pointer_drag_controller.js +434 -188
- data/dist/controllers/popover_controller.js +269 -113
- data/dist/controllers/portal_controller.js +215 -76
- data/dist/controllers/preview_guard_controller.js +243 -116
- data/dist/controllers/progress_controller.js +391 -78
- data/dist/controllers/radio_group_controller.js +278 -211
- data/dist/controllers/range_slider_controller.js +220 -113
- data/dist/controllers/rating_controller.js +458 -211
- data/dist/controllers/read_more_controller.js +138 -63
- data/dist/controllers/reading_progress_controller.js +166 -137
- data/dist/controllers/relative_time_controller.js +211 -112
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
- data/dist/controllers/resizable_controller.js +299 -150
- data/dist/controllers/roving_controller.js +49 -88
- data/dist/controllers/scroll_area_controller.js +490 -246
- data/dist/controllers/scroll_restore_controller.js +14 -55
- data/dist/controllers/scroll_visibility_controller.js +432 -149
- data/dist/controllers/scrollspy_controller.js +193 -248
- data/dist/controllers/separator_controller.js +285 -142
- data/dist/controllers/sidebar_controller.js +1069 -404
- data/dist/controllers/skeleton_controller.js +259 -101
- data/dist/controllers/slider_controller.js +187 -86
- data/dist/controllers/smart_sticky_header_controller.js +173 -66
- data/dist/controllers/sortable_controller.js +26 -125
- data/dist/controllers/spinner_controller.js +288 -173
- data/dist/controllers/step_indicator_controller.js +105 -46
- data/dist/controllers/stepper_controller.js +120 -73
- data/dist/controllers/stick_to_bottom_controller.js +275 -140
- data/dist/controllers/sticky_observer_controller.js +220 -49
- data/dist/controllers/submit_once_controller.js +376 -227
- data/dist/controllers/switch_controller.js +201 -84
- data/dist/controllers/tabs_controller.js +41 -30
- data/dist/controllers/tags_input_controller.js +293 -173
- data/dist/controllers/textarea_autosize_controller.js +108 -74
- data/dist/controllers/theme_controller.js +140 -116
- data/dist/controllers/time_picker_controller.js +331 -127
- data/dist/controllers/toast_controller.js +244 -324
- data/dist/controllers/toggle_group_controller.js +252 -191
- data/dist/controllers/toolbar_controller.js +219 -120
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +283 -369
- data/dist/index.js +8573 -10410
- data/dist/positioning/index.js +154 -47
- data/lib/generators/stimeo/install/install_generator.rb +7 -2
- data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,104 +1,207 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/toolbar_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/logical_scroll.ts
|
|
6
3
|
function isRtl(element) {
|
|
7
4
|
return window.getComputedStyle(element).direction === "rtl";
|
|
8
5
|
}
|
|
9
6
|
|
|
10
|
-
// src/utils/arrow_step.ts
|
|
11
7
|
function isReservedArrowChord(event, allow = []) {
|
|
12
8
|
if (!event.key.startsWith("Arrow")) return false;
|
|
13
9
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
14
10
|
}
|
|
11
|
+
function hasModifierChord(event) {
|
|
12
|
+
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
13
|
+
}
|
|
15
14
|
|
|
16
|
-
// src/utils/composition_tracker.ts
|
|
17
15
|
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
18
16
|
"insertCompositionText",
|
|
19
17
|
"insertFromComposition",
|
|
20
18
|
"deleteCompositionText",
|
|
21
19
|
"deleteByComposition"
|
|
22
20
|
]);
|
|
21
|
+
function isCompositionInputType(inputType) {
|
|
22
|
+
return inputType !== void 0 && COMPOSITION_INPUT_TYPES.has(inputType);
|
|
23
|
+
}
|
|
24
|
+
var BEFORE_MORPH_ATTRIBUTE = "turbo:before-morph-attribute";
|
|
25
|
+
var textOf = (element) => element.value;
|
|
26
|
+
var holdsFocus = (element) => element instanceof Node && element.getRootNode().activeElement === element;
|
|
23
27
|
var CompositionTracker = class {
|
|
24
28
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
25
|
-
#
|
|
29
|
+
#composition = null;
|
|
26
30
|
#onStart;
|
|
27
31
|
#onEnd;
|
|
28
|
-
|
|
32
|
+
#onAbandon;
|
|
29
33
|
#confirmedTarget = null;
|
|
34
|
+
#keyComposing = false;
|
|
35
|
+
#connected = false;
|
|
36
|
+
#abandoned = null;
|
|
30
37
|
constructor(options = {}) {
|
|
31
38
|
this.#onStart = options.onStart;
|
|
32
39
|
this.#onEnd = options.onEnd;
|
|
40
|
+
this.#onAbandon = options.onAbandon;
|
|
33
41
|
}
|
|
34
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
35
42
|
observe(target) {
|
|
43
|
+
this.#connected = true;
|
|
36
44
|
if (this.#observedTargets.has(target)) return;
|
|
37
45
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
38
46
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
39
|
-
target.addEventListener("keydown", this.#handleKeydown);
|
|
47
|
+
target.addEventListener("keydown", this.#handleKeydown, true);
|
|
48
|
+
target.addEventListener("beforeinput", this.#handleBeforeInput, true);
|
|
49
|
+
target.addEventListener("input", this.#handleInput, true);
|
|
50
|
+
target.addEventListener("blur", this.#handleBlur, true);
|
|
51
|
+
target.addEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
|
|
40
52
|
this.#observedTargets.add(target);
|
|
41
53
|
}
|
|
42
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
43
54
|
unobserve(target) {
|
|
44
55
|
if (!this.#observedTargets.delete(target)) return;
|
|
45
|
-
|
|
46
|
-
target
|
|
47
|
-
target.removeEventListener("keydown", this.#handleKeydown);
|
|
48
|
-
this.#activeTargets.delete(target);
|
|
56
|
+
this.#release(target);
|
|
57
|
+
if (this.#composition?.target === target) this.#letGo();
|
|
49
58
|
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
50
59
|
}
|
|
51
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
52
60
|
disconnect() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
target.removeEventListener("compositionend", this.#handleEnd);
|
|
56
|
-
target.removeEventListener("keydown", this.#handleKeydown);
|
|
57
|
-
}
|
|
61
|
+
this.#connected = false;
|
|
62
|
+
for (const target of this.#observedTargets) this.#release(target);
|
|
58
63
|
this.#observedTargets.clear();
|
|
59
|
-
this.#
|
|
64
|
+
this.#letGo();
|
|
60
65
|
this.#confirmedTarget = null;
|
|
66
|
+
this.#keyComposing = false;
|
|
67
|
+
this.#abandoned = null;
|
|
61
68
|
}
|
|
62
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
63
69
|
isComposing(event) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
this.#settle();
|
|
71
|
+
return event?.isComposing === true || this.#composition !== null;
|
|
72
|
+
}
|
|
73
|
+
composingElement() {
|
|
74
|
+
this.#settle();
|
|
75
|
+
return this.#composition?.element ?? null;
|
|
76
|
+
}
|
|
77
|
+
owns(event) {
|
|
78
|
+
const element = this.composingElement();
|
|
79
|
+
return event.isComposing === true || element !== null && element === event.target;
|
|
80
|
+
}
|
|
72
81
|
consumesConfirmedInput(event) {
|
|
73
82
|
const confirmed = this.#confirmedTarget;
|
|
74
83
|
this.#confirmedTarget = null;
|
|
75
84
|
if (confirmed === null || confirmed !== event.target) return false;
|
|
76
85
|
const inputType = event.inputType;
|
|
77
|
-
return !inputType ||
|
|
86
|
+
return !inputType || isCompositionInputType(inputType);
|
|
87
|
+
}
|
|
88
|
+
#release(target) {
|
|
89
|
+
target.removeEventListener("compositionstart", this.#handleStart);
|
|
90
|
+
target.removeEventListener("compositionend", this.#handleEnd);
|
|
91
|
+
target.removeEventListener("keydown", this.#handleKeydown, true);
|
|
92
|
+
target.removeEventListener("beforeinput", this.#handleBeforeInput, true);
|
|
93
|
+
target.removeEventListener("input", this.#handleInput, true);
|
|
94
|
+
target.removeEventListener("blur", this.#handleBlur, true);
|
|
95
|
+
target.removeEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
|
|
96
|
+
}
|
|
97
|
+
#letGo() {
|
|
98
|
+
const composition = this.#composition;
|
|
99
|
+
if (composition === null) return;
|
|
100
|
+
if (!holdsFocus(composition.element)) {
|
|
101
|
+
this.#composition = null;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
queueMicrotask(() => {
|
|
105
|
+
if (this.#composition !== composition || this.#observedTargets.has(composition.target)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.#composition = null;
|
|
109
|
+
if (!this.#connected) return;
|
|
110
|
+
this.#abandoned = { element: composition.element, inPlace: true };
|
|
111
|
+
this.#runAbandon();
|
|
112
|
+
});
|
|
78
113
|
}
|
|
114
|
+
#settle() {
|
|
115
|
+
const composition = this.#composition;
|
|
116
|
+
if (composition === null) return;
|
|
117
|
+
const { target, element, text } = composition;
|
|
118
|
+
if (target instanceof Node && element instanceof Node && !target.contains(element)) {
|
|
119
|
+
this.#composition = null;
|
|
120
|
+
} else if (textOf(element) !== text) {
|
|
121
|
+
this.#abandon(false);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
#abandon(inPlace) {
|
|
125
|
+
this.#owe(inPlace);
|
|
126
|
+
this.#composition = null;
|
|
127
|
+
queueMicrotask(this.#runAbandon);
|
|
128
|
+
}
|
|
129
|
+
#owe(inPlace) {
|
|
130
|
+
const composition = this.#composition;
|
|
131
|
+
if (composition !== null) this.#abandoned = { element: composition.element, inPlace };
|
|
132
|
+
}
|
|
133
|
+
#runAbandon = () => {
|
|
134
|
+
const abandoned = this.#abandoned;
|
|
135
|
+
if (abandoned === null) return;
|
|
136
|
+
this.#abandoned = null;
|
|
137
|
+
this.#onAbandon?.(abandoned.element, abandoned.inPlace);
|
|
138
|
+
};
|
|
79
139
|
#handleStart = (event) => {
|
|
80
140
|
this.#confirmedTarget = null;
|
|
81
|
-
|
|
141
|
+
this.#settle();
|
|
142
|
+
this.#owe(true);
|
|
143
|
+
const target = event.currentTarget;
|
|
144
|
+
const element = event.target;
|
|
145
|
+
this.#composition = { target, element, text: textOf(element) };
|
|
146
|
+
this.#runAbandon();
|
|
82
147
|
this.#onStart?.(event);
|
|
83
148
|
};
|
|
84
149
|
#handleEnd = (event) => {
|
|
85
|
-
|
|
150
|
+
this.#runAbandon();
|
|
151
|
+
if (this.#composition?.target === event.currentTarget) this.#composition = null;
|
|
86
152
|
this.#confirmedTarget = event.target;
|
|
87
153
|
this.#onEnd?.(event);
|
|
88
154
|
};
|
|
89
|
-
|
|
90
|
-
|
|
155
|
+
#handleKeydown = (event) => {
|
|
156
|
+
this.#settle();
|
|
157
|
+
this.#runAbandon();
|
|
91
158
|
this.#confirmedTarget = null;
|
|
159
|
+
this.#keyComposing = event.isComposing === true;
|
|
160
|
+
};
|
|
161
|
+
#handleBeforeInput = (event) => {
|
|
162
|
+
const keyComposing = this.#keyComposing;
|
|
163
|
+
this.#keyComposing = false;
|
|
164
|
+
this.#settle();
|
|
165
|
+
const { isComposing, inputType } = event;
|
|
166
|
+
if (this.#composition !== null && !keyComposing && !isComposing && inputType && !isCompositionInputType(inputType)) {
|
|
167
|
+
this.#abandon(true);
|
|
168
|
+
}
|
|
169
|
+
this.#runAbandon();
|
|
170
|
+
};
|
|
171
|
+
#handleInput = (event) => {
|
|
172
|
+
const composition = this.#composition;
|
|
173
|
+
const { isTrusted, isComposing, inputType } = event;
|
|
174
|
+
if (composition !== null && event.target === composition.element && (isTrusted || isComposing || isCompositionInputType(inputType))) {
|
|
175
|
+
composition.text = textOf(composition.element);
|
|
176
|
+
} else {
|
|
177
|
+
this.#settle();
|
|
178
|
+
}
|
|
179
|
+
this.#runAbandon();
|
|
180
|
+
};
|
|
181
|
+
#handleBlur = (event) => {
|
|
182
|
+
this.#settle();
|
|
183
|
+
this.#runAbandon();
|
|
184
|
+
const composition = this.#composition;
|
|
185
|
+
if (composition === null || composition.target === composition.element) return;
|
|
186
|
+
const { element } = composition;
|
|
187
|
+
if (!(element instanceof Node) || event.target !== element) return;
|
|
188
|
+
const root = element.getRootNode();
|
|
189
|
+
if (root.activeElement !== element) this.#composition = null;
|
|
190
|
+
};
|
|
191
|
+
#handleBeforeMorphAttribute = (event) => {
|
|
192
|
+
this.#settle();
|
|
193
|
+
const composition = this.#composition;
|
|
194
|
+
if (composition?.target !== event.currentTarget || event.target !== composition.element) return;
|
|
195
|
+
const detail = event.detail;
|
|
196
|
+
if (detail?.attributeName === "value") event.preventDefault();
|
|
92
197
|
};
|
|
93
198
|
};
|
|
94
199
|
|
|
95
|
-
// src/utils/event_owner.ts
|
|
96
200
|
function ownerIndex(candidates, node) {
|
|
97
201
|
if (!(node instanceof Node)) return -1;
|
|
98
202
|
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
99
203
|
}
|
|
100
204
|
|
|
101
|
-
// src/utils/focus_candidate.ts
|
|
102
205
|
function inheritsFieldsetDisabled(control) {
|
|
103
206
|
let fieldset = control.closest("fieldset[disabled]");
|
|
104
207
|
while (fieldset) {
|
|
@@ -109,31 +212,84 @@ function inheritsFieldsetDisabled(control) {
|
|
|
109
212
|
return false;
|
|
110
213
|
}
|
|
111
214
|
|
|
112
|
-
|
|
215
|
+
var ListenerSet = class {
|
|
216
|
+
#abort = new AbortController();
|
|
217
|
+
add(target, type, handler, options) {
|
|
218
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
219
|
+
}
|
|
220
|
+
dispose() {
|
|
221
|
+
this.#abort.abort();
|
|
222
|
+
this.#abort = new AbortController();
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
var MicrotaskCoalescer = class {
|
|
227
|
+
#run;
|
|
228
|
+
#queued = false;
|
|
229
|
+
#active = false;
|
|
230
|
+
#generation = 0;
|
|
231
|
+
constructor(run) {
|
|
232
|
+
this.#run = run;
|
|
233
|
+
}
|
|
234
|
+
activate() {
|
|
235
|
+
this.#active = true;
|
|
236
|
+
}
|
|
237
|
+
cancel() {
|
|
238
|
+
this.#active = false;
|
|
239
|
+
this.#queued = false;
|
|
240
|
+
this.#generation += 1;
|
|
241
|
+
}
|
|
242
|
+
get pending() {
|
|
243
|
+
return this.#queued;
|
|
244
|
+
}
|
|
245
|
+
schedule() {
|
|
246
|
+
if (!this.#active || this.#queued) return;
|
|
247
|
+
this.#queued = true;
|
|
248
|
+
const generation = this.#generation;
|
|
249
|
+
queueMicrotask(() => {
|
|
250
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
251
|
+
this.#queued = false;
|
|
252
|
+
this.#run();
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
var MorphRenderWatcher = class {
|
|
258
|
+
#listeners = new ListenerSet();
|
|
259
|
+
#pass;
|
|
260
|
+
#element = null;
|
|
261
|
+
#onMorph = () => this.#pass.schedule();
|
|
262
|
+
constructor(run) {
|
|
263
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
264
|
+
}
|
|
265
|
+
observe(element) {
|
|
266
|
+
if (this.#element === element) return;
|
|
267
|
+
this.disconnect();
|
|
268
|
+
this.#element = element;
|
|
269
|
+
this.#pass.activate();
|
|
270
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
271
|
+
}
|
|
272
|
+
schedule() {
|
|
273
|
+
this.#pass.schedule();
|
|
274
|
+
}
|
|
275
|
+
get pending() {
|
|
276
|
+
return this.#pass.pending;
|
|
277
|
+
}
|
|
278
|
+
disconnect() {
|
|
279
|
+
this.#listeners.dispose();
|
|
280
|
+
this.#pass.cancel();
|
|
281
|
+
this.#element = null;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
113
285
|
var RovingTabindex = class {
|
|
114
|
-
/** Returns the current ordered item elements; called on every operation. */
|
|
115
286
|
#getItems;
|
|
116
|
-
/**
|
|
117
|
-
* @param getItems - Returns the current ordered item elements. Called on every
|
|
118
|
-
* operation so the live target list is always used.
|
|
119
|
-
*/
|
|
120
287
|
constructor(getItems) {
|
|
121
288
|
this.#getItems = getItems;
|
|
122
289
|
}
|
|
123
|
-
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
124
290
|
get activeIndex() {
|
|
125
291
|
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
126
292
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
129
|
-
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
130
|
-
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
131
|
-
* "nothing is currently tabbable".
|
|
132
|
-
*
|
|
133
|
-
* @param index - Position of the item to make tabbable.
|
|
134
|
-
* @param options - Pass `{ focus: true }` to also move DOM focus to that item,
|
|
135
|
-
* and `items` to reuse an event-scoped collection snapshot.
|
|
136
|
-
*/
|
|
137
293
|
setActive(index, options = {}) {
|
|
138
294
|
const { focus = false } = options;
|
|
139
295
|
const items = options.items ?? this.#getItems();
|
|
@@ -150,7 +306,6 @@ function rovingMove(current, length, delta, wrap) {
|
|
|
150
306
|
return Math.min(length - 1, Math.max(0, next));
|
|
151
307
|
}
|
|
152
308
|
|
|
153
|
-
// src/controllers/toolbar_controller.ts
|
|
154
309
|
var STATE_ATTRIBUTES = ["disabled", "hidden"];
|
|
155
310
|
var ToolbarController = class extends Controller {
|
|
156
311
|
static targets = ["control"];
|
|
@@ -162,20 +317,17 @@ var ToolbarController = class extends Controller {
|
|
|
162
317
|
#roving = new RovingTabindex(() => this.controlTargets);
|
|
163
318
|
#composition = new CompositionTracker();
|
|
164
319
|
#observer = null;
|
|
165
|
-
/**
|
|
166
|
-
* Live between `connect()` and `disconnect()`. Stimulus reports the *initial*
|
|
167
|
-
* targets before `connect()` and re-reports them as disconnected after
|
|
168
|
-
* `disconnect()`; gating on this keeps the target callbacks from clobbering
|
|
169
|
-
* the authored Tab stop on mount and from resurrecting one after teardown.
|
|
170
|
-
*/
|
|
171
320
|
#connected = false;
|
|
321
|
+
#morph = new MorphRenderWatcher(() => this.#ensureTabStop());
|
|
322
|
+
#listeners = new ListenerSet();
|
|
172
323
|
connect() {
|
|
324
|
+
this.#morph.observe(this.element);
|
|
173
325
|
this.#ensureTabStop();
|
|
174
|
-
this.element
|
|
175
|
-
this.element
|
|
326
|
+
this.#listeners.add(this.element, "keydown", this.#onKeydown);
|
|
327
|
+
this.#listeners.add(this.element, "focusin", this.#onFocusin);
|
|
176
328
|
this.#composition.observe(this.element);
|
|
177
329
|
if (typeof MutationObserver !== "undefined") {
|
|
178
|
-
const observer = new MutationObserver(() => this.#
|
|
330
|
+
const observer = new MutationObserver(() => this.#morph.schedule());
|
|
179
331
|
observer.observe(this.element, {
|
|
180
332
|
subtree: true,
|
|
181
333
|
childList: true,
|
|
@@ -190,48 +342,28 @@ var ToolbarController = class extends Controller {
|
|
|
190
342
|
this.#connected = true;
|
|
191
343
|
}
|
|
192
344
|
disconnect() {
|
|
345
|
+
this.#morph.disconnect();
|
|
193
346
|
this.#connected = false;
|
|
194
|
-
this.
|
|
195
|
-
this.element.removeEventListener("focusin", this.#onFocusin);
|
|
347
|
+
this.#listeners.dispose();
|
|
196
348
|
this.#composition.disconnect();
|
|
197
349
|
this.#observer?.disconnect();
|
|
198
350
|
this.#observer = null;
|
|
199
351
|
}
|
|
200
|
-
/**
|
|
201
|
-
* A control added at runtime is dropped out of the Tab sequence first — a
|
|
202
|
-
* fresh `<button>` is tabbable by default, which would make the group two Tab
|
|
203
|
-
* stops — and then the lone stop is re-established.
|
|
204
|
-
*/
|
|
205
352
|
controlTargetConnected(control) {
|
|
206
353
|
if (!this.#connected) return;
|
|
207
354
|
control.tabIndex = -1;
|
|
208
355
|
this.#ensureTabStop();
|
|
209
356
|
}
|
|
210
|
-
/** Removing the control that held the Tab stop must not orphan the group. */
|
|
211
357
|
controlTargetDisconnected() {
|
|
212
358
|
if (!this.#connected) return;
|
|
213
359
|
this.#ensureTabStop();
|
|
214
360
|
}
|
|
215
|
-
/**
|
|
216
|
-
* Arrow/Home/End move focus and the single tab stop.
|
|
217
|
-
*
|
|
218
|
-
* Binding this per control with `data-action` is **optional** — the same
|
|
219
|
-
* handling runs from the container's delegated listener. It stays a declared
|
|
220
|
-
* action so per-control wiring keeps working; wiring both does not
|
|
221
|
-
* double-move, because the delegated pass then yields on `defaultPrevented`.
|
|
222
|
-
*/
|
|
223
361
|
onKeydown(event) {
|
|
224
362
|
this.#handleKeydown(event);
|
|
225
363
|
}
|
|
226
|
-
/** Delegated counterpart of {@link onKeydown}; bound on the container. */
|
|
227
364
|
#onKeydown = (event) => {
|
|
228
365
|
this.#handleKeydown(event);
|
|
229
366
|
};
|
|
230
|
-
/**
|
|
231
|
-
* Syncs the Tab stop to a control focused by other means (click, programmatic
|
|
232
|
-
* `focus()`) so Tab re-entry returns there. A non-navigable control is ignored:
|
|
233
|
-
* the lone Tab stop must never sit on something the user cannot operate.
|
|
234
|
-
*/
|
|
235
367
|
#onFocusin = (event) => {
|
|
236
368
|
const index = this.#indexOf(event.target);
|
|
237
369
|
const control = index === -1 ? void 0 : this.controlTargets[index];
|
|
@@ -241,6 +373,7 @@ var ToolbarController = class extends Controller {
|
|
|
241
373
|
#handleKeydown(event) {
|
|
242
374
|
if (event.defaultPrevented) return;
|
|
243
375
|
if (isReservedArrowChord(event)) return;
|
|
376
|
+
if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
|
|
244
377
|
if (this.#composition.isComposing(event)) return;
|
|
245
378
|
this.#ensureTabStop();
|
|
246
379
|
const from = this.#indexOf(event.target);
|
|
@@ -265,13 +398,6 @@ var ToolbarController = class extends Controller {
|
|
|
265
398
|
event.preventDefault();
|
|
266
399
|
if (target) this.#roving.setActive(this.controlTargets.indexOf(target), { focus: true });
|
|
267
400
|
}
|
|
268
|
-
/**
|
|
269
|
-
* Re-establishes the single Tab stop: keep the current one while it is still
|
|
270
|
-
* navigable, else hand it to the first navigable control. `-1` (no Tab stop at
|
|
271
|
-
* all) is reached only when every control is unavailable, and is recovered
|
|
272
|
-
* from as soon as one becomes navigable again. Idempotent by construction, so
|
|
273
|
-
* connect, the target callbacks, the observer, and keydown can all call it.
|
|
274
|
-
*/
|
|
275
401
|
#ensureTabStop() {
|
|
276
402
|
const active = this.#roving.activeIndex;
|
|
277
403
|
const activeEl = active === -1 ? null : this.controlTargets[active];
|
|
@@ -282,13 +408,6 @@ var ToolbarController = class extends Controller {
|
|
|
282
408
|
const first = this.#navigableControls[0];
|
|
283
409
|
this.#roving.setActive(first ? this.controlTargets.indexOf(first) : -1);
|
|
284
410
|
}
|
|
285
|
-
/**
|
|
286
|
-
* First navigable control strictly in the `delta` direction from `fromIndex`,
|
|
287
|
-
* scanning the **full** control list rather than the navigable subset so a
|
|
288
|
-
* non-navigable origin is never a dead end. When the direction yields nothing
|
|
289
|
-
* the origin keeps focus; when the origin itself is not navigable the first
|
|
290
|
-
* navigable control is used instead, so an arrow key always escapes.
|
|
291
|
-
*/
|
|
292
411
|
#nextNavigable(fromIndex, delta) {
|
|
293
412
|
const controls = this.controlTargets;
|
|
294
413
|
const length = controls.length;
|
|
@@ -304,37 +423,18 @@ var ToolbarController = class extends Controller {
|
|
|
304
423
|
const origin = controls[fromIndex];
|
|
305
424
|
return origin && this.#isNavigable(origin) ? origin : this.#navigableControls[0];
|
|
306
425
|
}
|
|
307
|
-
/** Index in `controlTargets` of the control owning `target` (it or a descendant). */
|
|
308
426
|
#indexOf(target) {
|
|
309
427
|
return ownerIndex(this.controlTargets, target);
|
|
310
428
|
}
|
|
311
|
-
/** Controls eligible for the roving tab stop (excludes native disabled / hidden). */
|
|
312
429
|
get #navigableControls() {
|
|
313
430
|
return this.controlTargets.filter((control) => this.#isNavigable(control));
|
|
314
431
|
}
|
|
315
|
-
/**
|
|
316
|
-
* A control can hold the tab stop unless it is `hidden` or a disabled form
|
|
317
|
-
* control. `aria-disabled` remains navigable per the APG; activation suppression
|
|
318
|
-
* belongs to the control itself or its owning behavior. Native disabledness is
|
|
319
|
-
* read from the `disabled` property, narrowed by an `in` check rather than
|
|
320
|
-
* asserted since `<a>` and `<div role="button">` are legitimate controls that
|
|
321
|
-
* are unaffected by a disabled fieldset. CSS-only visibility cannot be detected
|
|
322
|
-
* headlessly and stays the consumer's responsibility.
|
|
323
|
-
*/
|
|
324
432
|
#isNavigable(control) {
|
|
325
433
|
if (this.#isHidden(control)) return false;
|
|
326
434
|
if (!("disabled" in control)) return true;
|
|
327
435
|
if (control.disabled === true) return false;
|
|
328
436
|
return !inheritsFieldsetDisabled(control);
|
|
329
437
|
}
|
|
330
|
-
/**
|
|
331
|
-
* Whether `control`, or anything between it and the toolbar root, is `hidden`.
|
|
332
|
-
*
|
|
333
|
-
* Reading only the control's own attribute lets an invisible control hold the
|
|
334
|
-
* single Tab stop, which takes the *whole* toolbar out of the Tab sequence — an
|
|
335
|
-
* ordinary `hidden` wrapper is enough. The walk stops at the root: a toolbar
|
|
336
|
-
* inside a `hidden` region is already out of the page's Tab order.
|
|
337
|
-
*/
|
|
338
438
|
#isHidden(control) {
|
|
339
439
|
let node = control;
|
|
340
440
|
while (node && node !== this.element) {
|
|
@@ -346,5 +446,4 @@ var ToolbarController = class extends Controller {
|
|
|
346
446
|
};
|
|
347
447
|
|
|
348
448
|
export { ToolbarController };
|
|
349
|
-
//# sourceMappingURL=toolbar_controller.js.map
|
|
350
449
|
//# sourceMappingURL=toolbar_controller.js.map
|