stimeo-ui 0.15.0 → 0.17.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 +209 -0
- package/README.md +128 -1
- package/dist/cable/index.d.ts +79 -23
- package/dist/cable/index.js +392 -417
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +34 -5
- package/dist/controllers/accordion_controller.js +228 -28
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +20 -9
- package/dist/controllers/alert_dialog_controller.js +862 -221
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +38 -7
- package/dist/controllers/announcer_controller.js +135 -191
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.js +0 -10
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +30 -6
- package/dist/controllers/auto_submit_controller.js +295 -110
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +172 -106
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +10 -7
- package/dist/controllers/breadcrumb_controller.js +0 -129
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +22 -4
- package/dist/controllers/bulk_select_controller.js +264 -53
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +109 -28
- package/dist/controllers/calendar_controller.js +346 -180
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +99 -27
- package/dist/controllers/carousel_controller.js +587 -300
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +18 -2
- package/dist/controllers/character_counter_controller.js +271 -143
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +7 -2
- package/dist/controllers/checkbox_controller.js +77 -75
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +44 -7
- package/dist/controllers/clipboard_controller.js +187 -108
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +41 -5
- package/dist/controllers/collapsible_controller.js +374 -108
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +47 -14
- package/dist/controllers/color_picker_controller.js +183 -127
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +33 -6
- package/dist/controllers/combobox_controller.js +426 -109
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +45 -14
- package/dist/controllers/command_palette_controller.js +994 -350
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +15 -2
- package/dist/controllers/conditional_fields_controller.js +212 -125
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +49 -12
- package/dist/controllers/confirm_controller.js +939 -245
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +36 -3
- package/dist/controllers/context_menu_controller.js +275 -127
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +13 -1
- package/dist/controllers/count_up_controller.js +73 -21
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +75 -2
- package/dist/controllers/countdown_controller.js +235 -97
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +93 -15
- package/dist/controllers/currency_input_controller.js +241 -118
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +78 -22
- package/dist/controllers/data_grid_controller.js +229 -135
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +64 -11
- package/dist/controllers/date_range_picker_controller.js +359 -165
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +22 -3
- package/dist/controllers/dialog_controller.js +887 -225
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +24 -7
- package/dist/controllers/direct_upload_controller.js +80 -142
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +2 -2
- package/dist/controllers/dirty_form_controller.js +26 -47
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +19 -0
- package/dist/controllers/dismissible_controller.js +175 -20
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +52 -16
- package/dist/controllers/drawer_controller.js +963 -336
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +39 -3
- package/dist/controllers/dropdown_controller.js +342 -85
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +182 -58
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +67 -53
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +60 -18
- package/dist/controllers/file_dropzone_controller.js +426 -229
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +10 -4
- package/dist/controllers/filter_controller.js +78 -36
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +57 -14
- package/dist/controllers/flash_controller.js +328 -211
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +12 -3
- package/dist/controllers/focus_controller.js +654 -239
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +153 -136
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +29 -96
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +41 -9
- package/dist/controllers/frame_loading_controller.js +263 -198
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +22 -2
- package/dist/controllers/highlight_controller.js +119 -70
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +65 -4
- package/dist/controllers/hover_card_controller.js +335 -118
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +58 -4
- package/dist/controllers/idle_controller.js +316 -65
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +5 -2
- package/dist/controllers/input_mask_controller.js +116 -61
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +32 -13
- package/dist/controllers/intersection_controller.js +147 -103
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +16 -0
- package/dist/controllers/lazy_frame_controller.js +9 -56
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +84 -10
- package/dist/controllers/listbox_controller.js +413 -153
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +1 -1
- package/dist/controllers/local_time_controller.js +54 -64
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +11 -3
- package/dist/controllers/masonry_controller.js +70 -95
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +40 -9
- package/dist/controllers/menu_controller.js +254 -167
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +15 -4
- package/dist/controllers/menubar_controller.js +206 -331
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +20 -0
- package/dist/controllers/meter_controller.js +159 -71
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +50 -7
- package/dist/controllers/multi_select_controller.js +797 -388
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +19 -1
- package/dist/controllers/navigation_menu_controller.js +156 -231
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +10 -0
- package/dist/controllers/nested_form_controller.js +373 -174
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +23 -0
- package/dist/controllers/network_status_controller.js +241 -45
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +62 -10
- package/dist/controllers/number_input_controller.js +387 -227
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +28 -75
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +38 -7
- package/dist/controllers/otp_controller.js +344 -196
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +13 -12
- package/dist/controllers/overflow_indicator_controller.js +167 -93
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +66 -9
- package/dist/controllers/overflow_menu_controller.js +408 -277
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +52 -15
- package/dist/controllers/pagination_controller.js +342 -120
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +60 -9
- package/dist/controllers/password_reveal_controller.js +424 -99
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +14 -5
- package/dist/controllers/password_strength_controller.js +99 -153
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +11 -4
- package/dist/controllers/persist_controller.js +252 -96
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +19 -7
- package/dist/controllers/pointer_drag_controller.js +263 -164
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +46 -4
- package/dist/controllers/popover_controller.js +267 -94
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +209 -72
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +10 -4
- package/dist/controllers/preview_guard_controller.js +243 -112
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +11 -0
- package/dist/controllers/progress_controller.js +132 -59
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +19 -8
- package/dist/controllers/radio_group_controller.js +138 -133
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +92 -4
- package/dist/controllers/range_slider_controller.js +245 -112
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +33 -6
- package/dist/controllers/rating_controller.js +429 -194
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +26 -2
- package/dist/controllers/read_more_controller.js +185 -47
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +1 -1
- package/dist/controllers/reading_progress_controller.js +147 -135
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +18 -4
- package/dist/controllers/relative_time_controller.js +145 -110
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
- package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +44 -4
- package/dist/controllers/resizable_controller.js +294 -118
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +39 -84
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -3
- package/dist/controllers/scroll_area_controller.js +428 -221
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +0 -53
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
- package/dist/controllers/scroll_visibility_controller.js +395 -146
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +9 -3
- package/dist/controllers/scrollspy_controller.js +181 -236
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +59 -17
- package/dist/controllers/separator_controller.js +288 -132
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +81 -7
- package/dist/controllers/sidebar_controller.js +1071 -375
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +248 -92
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +72 -8
- package/dist/controllers/slider_controller.js +211 -96
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
- package/dist/controllers/smart_sticky_header_controller.js +163 -54
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +13 -110
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +277 -161
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +24 -17
- package/dist/controllers/step_indicator_controller.js +107 -53
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +43 -9
- package/dist/controllers/stepper_controller.js +171 -48
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +25 -0
- package/dist/controllers/stick_to_bottom_controller.js +310 -116
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +11 -0
- package/dist/controllers/sticky_observer_controller.js +214 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +22 -6
- package/dist/controllers/submit_once_controller.js +294 -192
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +40 -7
- package/dist/controllers/switch_controller.js +107 -43
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +13 -1
- package/dist/controllers/tabs_controller.js +50 -25
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +27 -3
- package/dist/controllers/tags_input_controller.js +290 -149
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
- package/dist/controllers/textarea_autosize_controller.js +71 -58
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +37 -8
- package/dist/controllers/theme_controller.js +148 -80
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +18 -1
- package/dist/controllers/time_picker_controller.js +162 -81
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +85 -23
- package/dist/controllers/toast_controller.js +365 -193
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +60 -9
- package/dist/controllers/toggle_group_controller.js +194 -99
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +92 -90
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +52 -2
- package/dist/controllers/tooltip_controller.js +185 -125
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +22 -4
- package/dist/controllers/transition_controller.js +123 -115
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +59 -18
- package/dist/controllers/tree_view_controller.js +248 -290
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +33 -3
- package/dist/index.js +8649 -8572
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +95 -9
- package/dist/inspector/cli.js +343 -77
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +390 -96
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +45 -45
- package/dist/inspector/manifest.json +1308 -155
- package/dist/positioning/index.d.ts +8 -0
- package/dist/positioning/index.js +148 -45
- package/dist/positioning/index.js.map +1 -1
- package/package.json +7 -7
- package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
- package/dist/controllers/reset_before_cache_controller.js.map +0 -1
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
4
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
5
|
+
function stateReasonFor(event) {
|
|
6
|
+
if (!event) return "api";
|
|
7
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
8
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
9
|
+
return "user";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actionSource(source) {
|
|
13
|
+
if (source instanceof HTMLElement) {
|
|
14
|
+
return { event: null, host: source, origin: source, reason: "api" };
|
|
15
|
+
}
|
|
16
|
+
const event = source ?? null;
|
|
17
|
+
return {
|
|
18
|
+
event,
|
|
19
|
+
host: event?.currentTarget instanceof Element ? event.currentTarget : null,
|
|
20
|
+
origin: event?.target instanceof Element ? event.target : null,
|
|
21
|
+
reason: stateReasonFor(event)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
|
|
5
|
-
// src/utils/active_option.ts
|
|
6
25
|
function syncActiveOption(options, active) {
|
|
7
26
|
let written = 0;
|
|
8
27
|
for (const option of options) {
|
|
@@ -14,101 +33,267 @@ function syncActiveOption(options, active) {
|
|
|
14
33
|
return written;
|
|
15
34
|
}
|
|
16
35
|
|
|
17
|
-
// src/utils/arrow_step.ts
|
|
18
36
|
function isReservedArrowChord(event, allow = []) {
|
|
19
37
|
if (!event.key.startsWith("Arrow")) return false;
|
|
20
38
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
21
39
|
}
|
|
22
40
|
|
|
23
|
-
|
|
41
|
+
function sharedRegistry(key, create) {
|
|
42
|
+
const symbol = Symbol.for(key);
|
|
43
|
+
const scope = globalThis;
|
|
44
|
+
const existing = scope[symbol];
|
|
45
|
+
if (existing !== void 0) return existing;
|
|
46
|
+
const registry = create();
|
|
47
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
48
|
+
return registry;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isAttributeValue(value) {
|
|
52
|
+
return value === null || typeof value === "string";
|
|
53
|
+
}
|
|
54
|
+
function readLeaseRecord(element, record) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
57
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
58
|
+
const [original, written, ...rest] = parsed;
|
|
59
|
+
const linked = rest[0] !== false;
|
|
60
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
61
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
62
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
63
|
+
} catch {
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
var AttributeLease = class _AttributeLease {
|
|
68
|
+
static #registry = sharedRegistry(
|
|
69
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
70
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
71
|
+
);
|
|
72
|
+
#attribute;
|
|
73
|
+
#record;
|
|
74
|
+
#base;
|
|
75
|
+
#held = /* @__PURE__ */ new Set();
|
|
76
|
+
constructor(attribute, owner, options = {}) {
|
|
77
|
+
this.#attribute = attribute;
|
|
78
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
79
|
+
this.#base = options.base === true;
|
|
80
|
+
const names = _AttributeLease.#registry.names;
|
|
81
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
82
|
+
}
|
|
83
|
+
write(element, value) {
|
|
84
|
+
const stack = this.#stack(element);
|
|
85
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
86
|
+
this.#held.add(element);
|
|
87
|
+
const shows = !this.#covered(element, stack, hold);
|
|
88
|
+
hold.written = value;
|
|
89
|
+
this.#mark(element, stack);
|
|
90
|
+
if (shows) this.#reflect(element, value);
|
|
91
|
+
}
|
|
92
|
+
return(element) {
|
|
93
|
+
const held = this.#held.delete(element);
|
|
94
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
95
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
96
|
+
const hold = this.#take(stack);
|
|
97
|
+
if (!hold) return false;
|
|
98
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
99
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
100
|
+
const index = stack.indexOf(hold);
|
|
101
|
+
stack.splice(index, 1);
|
|
102
|
+
const above = stack[index];
|
|
103
|
+
if (above) above.linked &&= hold.linked;
|
|
104
|
+
this.#mark(element, stack);
|
|
105
|
+
if (!owns) return true;
|
|
106
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
107
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
returnAll() {
|
|
111
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
112
|
+
}
|
|
113
|
+
#stack(element) {
|
|
114
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
115
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
116
|
+
stacks.set(element, byAttribute);
|
|
117
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
118
|
+
byAttribute.set(this.#attribute, stack);
|
|
119
|
+
return stack;
|
|
120
|
+
}
|
|
121
|
+
#seed(element) {
|
|
122
|
+
const found = [];
|
|
123
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
124
|
+
const read = readLeaseRecord(element, record);
|
|
125
|
+
if (read) found.push({ ...read, record });
|
|
126
|
+
}
|
|
127
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
128
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
129
|
+
lease: null,
|
|
130
|
+
record,
|
|
131
|
+
written,
|
|
132
|
+
original,
|
|
133
|
+
linked: linked && index > 0
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
#take(stack) {
|
|
137
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
138
|
+
if (hold) hold.lease = this;
|
|
139
|
+
return hold;
|
|
140
|
+
}
|
|
141
|
+
#acquire(element, stack) {
|
|
142
|
+
const current = element.getAttribute(this.#attribute);
|
|
143
|
+
const top = stack.at(-1);
|
|
144
|
+
const bottom = stack[0];
|
|
145
|
+
const over = top?.written === current ? top : void 0;
|
|
146
|
+
const from = this.#base ? bottom : over;
|
|
147
|
+
const hold = {
|
|
148
|
+
lease: this,
|
|
149
|
+
record: this.#record,
|
|
150
|
+
written: current,
|
|
151
|
+
original: from ? from.original : current,
|
|
152
|
+
linked: !this.#base && over !== void 0
|
|
153
|
+
};
|
|
154
|
+
if (this.#base && bottom) {
|
|
155
|
+
bottom.linked = true;
|
|
156
|
+
stack.unshift(hold);
|
|
157
|
+
} else stack.push(hold);
|
|
158
|
+
return hold;
|
|
159
|
+
}
|
|
160
|
+
#covered(element, stack, hold) {
|
|
161
|
+
const current = element.getAttribute(this.#attribute);
|
|
162
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
163
|
+
}
|
|
164
|
+
#mark(element, stack) {
|
|
165
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
166
|
+
stack.forEach((hold, index) => {
|
|
167
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
168
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
169
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
170
|
+
records.set(
|
|
171
|
+
hold.record,
|
|
172
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
for (const [name, recorded] of records) {
|
|
176
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
177
|
+
if (recorded === null) element.removeAttribute(name);
|
|
178
|
+
else element.setAttribute(name, recorded);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
#reflect(element, value) {
|
|
182
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
183
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
184
|
+
else element.setAttribute(this.#attribute, value);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
189
|
+
"insertCompositionText",
|
|
190
|
+
"insertFromComposition",
|
|
191
|
+
"deleteCompositionText",
|
|
192
|
+
"deleteByComposition"
|
|
193
|
+
]);
|
|
24
194
|
var CompositionTracker = class {
|
|
25
195
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
26
196
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
27
197
|
#onStart;
|
|
28
198
|
#onEnd;
|
|
199
|
+
#confirmedTarget = null;
|
|
29
200
|
constructor(options = {}) {
|
|
30
201
|
this.#onStart = options.onStart;
|
|
31
202
|
this.#onEnd = options.onEnd;
|
|
32
203
|
}
|
|
33
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
34
204
|
observe(target) {
|
|
35
205
|
if (this.#observedTargets.has(target)) return;
|
|
36
206
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
37
207
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
208
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
38
209
|
this.#observedTargets.add(target);
|
|
39
210
|
}
|
|
40
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
41
211
|
unobserve(target) {
|
|
42
212
|
if (!this.#observedTargets.delete(target)) return;
|
|
43
213
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
44
214
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
215
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
45
216
|
this.#activeTargets.delete(target);
|
|
217
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
46
218
|
}
|
|
47
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
48
219
|
disconnect() {
|
|
49
220
|
for (const target of this.#observedTargets) {
|
|
50
221
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
51
222
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
223
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
52
224
|
}
|
|
53
225
|
this.#observedTargets.clear();
|
|
54
226
|
this.#activeTargets.clear();
|
|
227
|
+
this.#confirmedTarget = null;
|
|
55
228
|
}
|
|
56
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
57
229
|
isComposing(event) {
|
|
58
230
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
59
231
|
}
|
|
232
|
+
consumesConfirmedInput(event) {
|
|
233
|
+
const confirmed = this.#confirmedTarget;
|
|
234
|
+
this.#confirmedTarget = null;
|
|
235
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
236
|
+
const inputType = event.inputType;
|
|
237
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
238
|
+
}
|
|
60
239
|
#handleStart = (event) => {
|
|
240
|
+
this.#confirmedTarget = null;
|
|
61
241
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
62
242
|
this.#onStart?.(event);
|
|
63
243
|
};
|
|
64
244
|
#handleEnd = (event) => {
|
|
65
245
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
246
|
+
this.#confirmedTarget = event.target;
|
|
66
247
|
this.#onEnd?.(event);
|
|
67
248
|
};
|
|
249
|
+
#handleKeydown = () => {
|
|
250
|
+
this.#confirmedTarget = null;
|
|
251
|
+
};
|
|
68
252
|
};
|
|
69
253
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
static
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
activate() {
|
|
85
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
86
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
87
|
-
if (first) {
|
|
88
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
254
|
+
var DetachGate = class _DetachGate {
|
|
255
|
+
#pending = false;
|
|
256
|
+
get pending() {
|
|
257
|
+
return this.#pending;
|
|
258
|
+
}
|
|
259
|
+
static isDetached(host) {
|
|
260
|
+
return !host.element.isConnected || !_DetachGate.#listed(host);
|
|
261
|
+
}
|
|
262
|
+
disconnected(host, teardown) {
|
|
263
|
+
const carried = _DetachGate.#carried(host);
|
|
264
|
+
if (!carried && _DetachGate.isDetached(host)) {
|
|
265
|
+
this.#pending = false;
|
|
266
|
+
teardown();
|
|
267
|
+
return;
|
|
89
268
|
}
|
|
269
|
+
this.#pending = true;
|
|
270
|
+
const probe = () => {
|
|
271
|
+
if (!this.#pending) return;
|
|
272
|
+
this.#pending = false;
|
|
273
|
+
teardown();
|
|
274
|
+
};
|
|
275
|
+
if (carried) setTimeout(probe, 0);
|
|
276
|
+
else queueMicrotask(probe);
|
|
90
277
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
278
|
+
static #carried(host) {
|
|
279
|
+
const element = host.element;
|
|
280
|
+
return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
|
|
281
|
+
}
|
|
282
|
+
static #listed(host) {
|
|
283
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
284
|
+
return tokens.includes(host.identifier);
|
|
285
|
+
}
|
|
286
|
+
cancel() {
|
|
287
|
+
this.#pending = false;
|
|
96
288
|
}
|
|
97
289
|
};
|
|
98
290
|
|
|
99
|
-
// src/utils/escape_layer.ts
|
|
100
291
|
var EscapeLayer = class _EscapeLayer {
|
|
101
|
-
static #registries =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
#claims = null;
|
|
107
|
-
/**
|
|
108
|
-
* Activates this layer at the top of its document's Escape stack, installing
|
|
109
|
-
* the document's shared resolver listener if this is its first layer.
|
|
110
|
-
* Re-activating an already-active layer moves it to the top.
|
|
111
|
-
*/
|
|
292
|
+
static #registries = sharedRegistry(
|
|
293
|
+
"stimeo-ui.escape-layer.registry.v1",
|
|
294
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
295
|
+
);
|
|
296
|
+
#active = null;
|
|
112
297
|
activate(ownerDocument = document, options) {
|
|
113
298
|
this.deactivate();
|
|
114
299
|
let registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
@@ -117,44 +302,31 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
117
302
|
_EscapeLayer.#registries.set(ownerDocument, registry);
|
|
118
303
|
ownerDocument.addEventListener("keydown", registry.onKeydown);
|
|
119
304
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.#
|
|
123
|
-
this.#claims = options.claims ?? null;
|
|
305
|
+
const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
|
|
306
|
+
registry.stack.push(entry);
|
|
307
|
+
this.#active = { document: ownerDocument, entry };
|
|
124
308
|
}
|
|
125
|
-
/**
|
|
126
|
-
* Removes this layer from its document's Escape stack, uninstalling the
|
|
127
|
-
* shared listener when the stack empties. Safe to call when inactive.
|
|
128
|
-
*/
|
|
129
309
|
deactivate() {
|
|
130
|
-
const
|
|
131
|
-
if (!
|
|
132
|
-
const registry = _EscapeLayer.#registries.get(
|
|
310
|
+
const active = this.#active;
|
|
311
|
+
if (!active) return;
|
|
312
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
133
313
|
if (registry) {
|
|
134
|
-
const index = registry.stack.lastIndexOf(
|
|
314
|
+
const index = registry.stack.lastIndexOf(active.entry);
|
|
135
315
|
if (index >= 0) registry.stack.splice(index, 1);
|
|
136
316
|
if (registry.stack.length === 0) {
|
|
137
|
-
|
|
138
|
-
_EscapeLayer.#registries.delete(
|
|
317
|
+
active.document.removeEventListener("keydown", registry.onKeydown);
|
|
318
|
+
_EscapeLayer.#registries.delete(active.document);
|
|
139
319
|
}
|
|
140
320
|
}
|
|
141
|
-
this.#
|
|
142
|
-
this.#onDismiss = null;
|
|
143
|
-
this.#claims = null;
|
|
321
|
+
this.#active = null;
|
|
144
322
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Whether this active layer would own a press right now: it is the topmost
|
|
147
|
-
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
148
|
-
* and diagnostics — production dismissal goes through the shared listener.
|
|
149
|
-
*/
|
|
150
323
|
get ownsEscape() {
|
|
151
|
-
const
|
|
152
|
-
if (!
|
|
153
|
-
const registry = _EscapeLayer.#registries.get(
|
|
324
|
+
const active = this.#active;
|
|
325
|
+
if (!active) return false;
|
|
326
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
154
327
|
if (!registry) return false;
|
|
155
|
-
return _EscapeLayer.#resolveOwner(registry.stack) ===
|
|
328
|
+
return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
|
|
156
329
|
}
|
|
157
|
-
/** Builds a document's registry with its shared resolver listener. */
|
|
158
330
|
static #createRegistry() {
|
|
159
331
|
const registry = {
|
|
160
332
|
stack: [],
|
|
@@ -163,24 +335,22 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
163
335
|
const owner = _EscapeLayer.#resolveOwner(registry.stack);
|
|
164
336
|
if (!owner) return;
|
|
165
337
|
event.preventDefault();
|
|
166
|
-
owner
|
|
338
|
+
owner.onDismiss();
|
|
167
339
|
}
|
|
168
340
|
};
|
|
169
341
|
return registry;
|
|
170
342
|
}
|
|
171
|
-
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
172
343
|
static #resolveOwner(stack) {
|
|
173
344
|
for (let index = stack.length - 1; index >= 0; index--) {
|
|
174
345
|
const layer = stack[index];
|
|
175
346
|
if (!layer) continue;
|
|
176
|
-
if (layer
|
|
347
|
+
if (layer.claims && !layer.claims()) continue;
|
|
177
348
|
return layer;
|
|
178
349
|
}
|
|
179
350
|
return null;
|
|
180
351
|
}
|
|
181
352
|
};
|
|
182
353
|
|
|
183
|
-
// src/utils/focus_candidate.ts
|
|
184
354
|
function inheritsFieldsetDisabled(control) {
|
|
185
355
|
let fieldset = control.closest("fieldset[disabled]");
|
|
186
356
|
while (fieldset) {
|
|
@@ -190,8 +360,34 @@ function inheritsFieldsetDisabled(control) {
|
|
|
190
360
|
}
|
|
191
361
|
return false;
|
|
192
362
|
}
|
|
363
|
+
function unslotted(element) {
|
|
364
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
365
|
+
}
|
|
366
|
+
function flatTreeParent(element) {
|
|
367
|
+
if (unslotted(element)) return null;
|
|
368
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
369
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
370
|
+
return parent instanceof Element ? parent : null;
|
|
371
|
+
}
|
|
372
|
+
function flatTreeContains(ancestor, element) {
|
|
373
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
374
|
+
if (node === ancestor) return true;
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
function closestInFlatTree(element, selector) {
|
|
379
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
380
|
+
if (node.matches(selector)) return node;
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
function flatTreeChildren(element) {
|
|
385
|
+
return ownedScope(element) ?? Array.from(element.children);
|
|
386
|
+
}
|
|
193
387
|
function canTakeFocus(element) {
|
|
194
|
-
|
|
388
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
389
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
390
|
+
}
|
|
195
391
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
196
392
|
if (!("disabled" in element)) return true;
|
|
197
393
|
if (element.disabled) return false;
|
|
@@ -248,222 +444,676 @@ function isTabStop(element) {
|
|
|
248
444
|
if (tabindex !== null) return tabindex >= 0;
|
|
249
445
|
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
250
446
|
}
|
|
251
|
-
function
|
|
252
|
-
return
|
|
253
|
-
|
|
447
|
+
function isRadio(element) {
|
|
448
|
+
return element instanceof HTMLInputElement && element.type === "radio";
|
|
449
|
+
}
|
|
450
|
+
function sameRadioGroup(a, b) {
|
|
451
|
+
return a.name !== "" && a.name === b.name && a.form === b.form && a.getRootNode() === b.getRootNode();
|
|
452
|
+
}
|
|
453
|
+
function isInEditableContent(element) {
|
|
454
|
+
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
455
|
+
if (node.getAttribute("contenteditable")?.toLowerCase() === "false") return false;
|
|
456
|
+
if (hasEditableTabStop(node)) return true;
|
|
457
|
+
}
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
var XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
|
|
461
|
+
function isSvgLink(element) {
|
|
462
|
+
return element instanceof SVGElement && element.localName === "a" && (element.hasAttribute("href") || element.hasAttributeNS(XLINK_NAMESPACE, "href"));
|
|
463
|
+
}
|
|
464
|
+
function hasRenderedImage(area) {
|
|
465
|
+
const map = area.closest("map");
|
|
466
|
+
const key = map?.name || map?.id;
|
|
467
|
+
if (!map || !key) return false;
|
|
468
|
+
const root = map.getRootNode();
|
|
469
|
+
return Array.from(root.querySelectorAll("img[usemap]")).some(
|
|
470
|
+
(image) => image.getAttribute("usemap") === `#${key}` && isRenderedForFocus(image)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
function isFocusableInOrder(element) {
|
|
474
|
+
const tabindex = authoredTabindex(element);
|
|
475
|
+
return (tabindex === null || tabindex >= 0) && canTakeFocus(element);
|
|
476
|
+
}
|
|
477
|
+
function extraStop(element) {
|
|
478
|
+
if (element instanceof HTMLAreaElement) {
|
|
479
|
+
const focusable = element.hasAttribute("href") || authoredTabindex(element) !== null;
|
|
480
|
+
return focusable && isFocusableInOrder(element) && hasRenderedImage(element);
|
|
481
|
+
}
|
|
482
|
+
if (element instanceof HTMLObjectElement) {
|
|
483
|
+
return isFocusableInOrder(element) && isRenderedForFocus(element) && element.contentWindow !== null;
|
|
484
|
+
}
|
|
485
|
+
if (isSvgLink(element))
|
|
486
|
+
return isFocusableInOrder(element) && isRenderedForFocus(element);
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
function isSequentialStop(element) {
|
|
490
|
+
const stop = extraStop(element) ?? (element.matches(TAB_STOP_CANDIDATE_SELECTOR) && isTabStop(element));
|
|
491
|
+
if (!stop) return false;
|
|
492
|
+
const link = element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement || isSvgLink(element);
|
|
493
|
+
return !(link && authoredTabindex(element) === null && isInEditableContent(element));
|
|
494
|
+
}
|
|
495
|
+
function ownedScope(element) {
|
|
496
|
+
if (element.shadowRoot) return Array.from(element.shadowRoot.children);
|
|
497
|
+
if (element instanceof HTMLSlotElement) {
|
|
498
|
+
const assigned = element.assignedElements();
|
|
499
|
+
return assigned.length > 0 ? assigned : Array.from(element.children);
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
function orderScope(elements, walk) {
|
|
504
|
+
const entries = [];
|
|
505
|
+
const visit = (element) => {
|
|
506
|
+
if (element === walk.from) walk.fromAt = walk.tree.length;
|
|
507
|
+
const tabindex = authoredTabindex(element) ?? 0;
|
|
508
|
+
const stop = (element instanceof HTMLElement || element instanceof SVGElement) && isSequentialStop(element) ? element : null;
|
|
509
|
+
if (stop) walk.tree.push(stop);
|
|
510
|
+
const owned = ownedScope(element);
|
|
511
|
+
if (owned) {
|
|
512
|
+
if (tabindex >= 0) {
|
|
513
|
+
entries.push({ tabindex, stops: [...stop ? [stop] : [], ...orderScope(owned, walk)] });
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (stop) entries.push({ tabindex, stops: [stop] });
|
|
518
|
+
for (const child of Array.from(element.children)) visit(child);
|
|
519
|
+
};
|
|
520
|
+
for (const element of elements) visit(element);
|
|
521
|
+
const positive = entries.filter((entry) => entry.tabindex > 0);
|
|
522
|
+
positive.sort((a, b) => a.tabindex - b.tabindex);
|
|
523
|
+
const rest = entries.filter((entry) => entry.tabindex === 0);
|
|
524
|
+
return [...positive, ...rest].flatMap((entry) => entry.stops);
|
|
525
|
+
}
|
|
526
|
+
function passesRadioRule(element, tree, focusedRadio) {
|
|
527
|
+
if (!isRadio(element) || element.checked) return true;
|
|
528
|
+
const group = tree.filter(
|
|
529
|
+
(stop) => stop === element || isRadio(stop) && sameRadioGroup(stop, element)
|
|
254
530
|
);
|
|
531
|
+
if (group.some((radio) => radio.checked)) return false;
|
|
532
|
+
if (group[0] !== element) return false;
|
|
533
|
+
return focusedRadio === null || !sameRadioGroup(focusedRadio, element);
|
|
534
|
+
}
|
|
535
|
+
function startIndex(order, walk, backward, isStop) {
|
|
536
|
+
if (walk.fromAt < 0) return backward ? order.length - 1 : 0;
|
|
537
|
+
const at = order.indexOf(walk.from);
|
|
538
|
+
if (at >= 0) return backward ? at - 1 : at + 1;
|
|
539
|
+
const neighbour = backward ? walk.tree.slice(0, walk.fromAt).reverse().find(isStop) : walk.tree.slice(walk.fromAt).find(isStop);
|
|
540
|
+
if (neighbour) return order.indexOf(neighbour);
|
|
541
|
+
return backward ? order.length - 1 : 0;
|
|
542
|
+
}
|
|
543
|
+
function sequentialTabStops(root, from = null, backward = false) {
|
|
544
|
+
const walk = { from, tree: [], fromAt: from === root ? 0 : -1 };
|
|
545
|
+
const order = orderScope(ownedScope(root) ?? Array.from(root.children), walk);
|
|
546
|
+
const focusedRadio = walk.fromAt >= 0 && isRadio(from) ? from : null;
|
|
547
|
+
const isStop = (element) => passesRadioRule(element, walk.tree, focusedRadio);
|
|
548
|
+
const start = startIndex(order, walk, backward, isStop);
|
|
549
|
+
const visited = backward ? [...order.slice(0, start + 1).reverse(), ...order.slice(start + 1).reverse()] : [...order.slice(start), ...order.slice(0, start)];
|
|
550
|
+
return visited.filter((element) => element !== from && isStop(element));
|
|
551
|
+
}
|
|
552
|
+
function deepActiveElement(ownerDocument) {
|
|
553
|
+
let active = ownerDocument.activeElement;
|
|
554
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
555
|
+
return active;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
var MicrotaskCoalescer = class {
|
|
559
|
+
#run;
|
|
560
|
+
#queued = false;
|
|
561
|
+
#active = false;
|
|
562
|
+
#generation = 0;
|
|
563
|
+
constructor(run) {
|
|
564
|
+
this.#run = run;
|
|
565
|
+
}
|
|
566
|
+
activate() {
|
|
567
|
+
this.#active = true;
|
|
568
|
+
}
|
|
569
|
+
cancel() {
|
|
570
|
+
this.#active = false;
|
|
571
|
+
this.#queued = false;
|
|
572
|
+
this.#generation += 1;
|
|
573
|
+
}
|
|
574
|
+
schedule() {
|
|
575
|
+
if (!this.#active || this.#queued) return;
|
|
576
|
+
this.#queued = true;
|
|
577
|
+
const generation = this.#generation;
|
|
578
|
+
queueMicrotask(() => {
|
|
579
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
580
|
+
this.#queued = false;
|
|
581
|
+
this.#run();
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
function queryOpenDescendants(root, selector) {
|
|
587
|
+
const matches = [];
|
|
588
|
+
const roots = [root];
|
|
589
|
+
for (const scope of roots) {
|
|
590
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
591
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
592
|
+
const shadow = scope.shadowRoot;
|
|
593
|
+
if (shadow) roots.push(shadow);
|
|
594
|
+
}
|
|
595
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
596
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return matches;
|
|
255
600
|
}
|
|
256
601
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
602
|
+
var TabindexLoan = class _TabindexLoan {
|
|
603
|
+
static #registry = sharedRegistry(
|
|
604
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
605
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
606
|
+
);
|
|
607
|
+
#value;
|
|
608
|
+
#record;
|
|
609
|
+
#lent = /* @__PURE__ */ new Set();
|
|
610
|
+
constructor(value, owner) {
|
|
611
|
+
this.#value = value;
|
|
612
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
613
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
614
|
+
}
|
|
615
|
+
lend(element) {
|
|
616
|
+
const stack = this.#stack(element);
|
|
617
|
+
const current = element.getAttribute("tabindex");
|
|
618
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
619
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
620
|
+
);
|
|
621
|
+
const linked = stack.at(-1)?.value === current;
|
|
622
|
+
if (!held && current !== null && !linked) return;
|
|
623
|
+
this.#lent.add(element);
|
|
624
|
+
if (held) held.loan = this;
|
|
625
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
626
|
+
this.#mark(element, stack);
|
|
627
|
+
if (current === null || !held && current !== this.#value) {
|
|
628
|
+
element.setAttribute("tabindex", this.#value);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
returnAll() {
|
|
632
|
+
for (const element of this.#lent) {
|
|
633
|
+
const stack = this.#stack(element);
|
|
634
|
+
const hold = stack.find((held) => held.loan === this);
|
|
635
|
+
if (hold) this.#release(element, stack, hold);
|
|
636
|
+
}
|
|
637
|
+
this.#lent.clear();
|
|
638
|
+
}
|
|
639
|
+
reclaim(element) {
|
|
640
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
641
|
+
const stack = this.#stack(element);
|
|
642
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
643
|
+
if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
|
|
644
|
+
}
|
|
645
|
+
reclaimWithin(root) {
|
|
646
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
647
|
+
this.reclaim(element);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
#stack(element) {
|
|
651
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
652
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
653
|
+
stacks.set(element, stack);
|
|
654
|
+
return stack;
|
|
655
|
+
}
|
|
656
|
+
#seed(element) {
|
|
657
|
+
const current = element.getAttribute("tabindex");
|
|
658
|
+
const holds = [];
|
|
659
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
660
|
+
const value = element.getAttribute(record);
|
|
661
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
662
|
+
}
|
|
663
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
664
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
665
|
+
return holds;
|
|
666
|
+
}
|
|
667
|
+
#release(element, stack, hold) {
|
|
668
|
+
const current = element.getAttribute("tabindex");
|
|
669
|
+
const index = stack.indexOf(hold);
|
|
670
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
671
|
+
stack.splice(index, 1);
|
|
672
|
+
const above = stack[index];
|
|
673
|
+
if (above) above.linked &&= hold.linked;
|
|
674
|
+
this.#mark(element, stack);
|
|
675
|
+
if (!owns) return;
|
|
676
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
677
|
+
if (!below) element.removeAttribute("tabindex");
|
|
678
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
679
|
+
}
|
|
680
|
+
#mark(element, stack) {
|
|
681
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
682
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
683
|
+
for (const [name, recorded] of records) {
|
|
684
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
685
|
+
if (recorded === null) element.removeAttribute(name);
|
|
686
|
+
else element.setAttribute(name, recorded);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
var FOCUS_TRAP_OWNER = "stimeo-focus-trap";
|
|
692
|
+
var INERT_MARK = `data-${FOCUS_TRAP_OWNER}-inert`;
|
|
693
|
+
var OVERFLOW_MARK = `data-${FOCUS_TRAP_OWNER}-overflow`;
|
|
694
|
+
var FocusTrap = class _FocusTrap {
|
|
695
|
+
static #registries = sharedRegistry(
|
|
696
|
+
"stimeo-ui.focus-trap.registry.v2",
|
|
697
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
698
|
+
);
|
|
699
|
+
static #TEXT_ENTRY_TYPES = /* @__PURE__ */ new Set([
|
|
700
|
+
"text",
|
|
701
|
+
"search",
|
|
702
|
+
"email",
|
|
703
|
+
"url",
|
|
704
|
+
"tel",
|
|
705
|
+
"password",
|
|
706
|
+
"number"
|
|
707
|
+
]);
|
|
708
|
+
#openers = [];
|
|
709
|
+
#entry;
|
|
710
|
+
#tabindexLoans = /* @__PURE__ */ new Map();
|
|
268
711
|
#activeState = false;
|
|
269
|
-
/** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */
|
|
270
712
|
#escapeLayer = new EscapeLayer();
|
|
271
|
-
|
|
713
|
+
#gate = new DetachGate();
|
|
272
714
|
#getContainer;
|
|
273
|
-
/** Closing/focus hooks; see {@link FocusTrapOptions}. */
|
|
274
715
|
#options;
|
|
275
|
-
/**
|
|
276
|
-
* @param getContainer - Returns the trapped element. Called on every operation
|
|
277
|
-
* so the live target is always used.
|
|
278
|
-
* @param options - Closing/focus hooks; see {@link FocusTrapOptions}.
|
|
279
|
-
*/
|
|
280
716
|
constructor(getContainer, options = {}) {
|
|
281
717
|
this.#getContainer = getContainer;
|
|
282
718
|
this.#options = options;
|
|
719
|
+
this.#entry = {
|
|
720
|
+
container: () => this.#readContainer(),
|
|
721
|
+
moveFocus: (backward) => this.#moveFocus(backward),
|
|
722
|
+
refocus: () => this.#refocus(),
|
|
723
|
+
isolated: false,
|
|
724
|
+
lockedBody: null,
|
|
725
|
+
openedFrom: null,
|
|
726
|
+
handedBack: []
|
|
727
|
+
};
|
|
283
728
|
}
|
|
284
|
-
/** Whether the trap is currently active. */
|
|
285
729
|
get active() {
|
|
286
730
|
return this.#activeState;
|
|
287
731
|
}
|
|
288
|
-
/**
|
|
289
|
-
* Applies the trap: records the current focus, optionally locks background scroll
|
|
290
|
-
* and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless
|
|
291
|
-
* `autoFocus` is off) moves focus inside. No-ops if already active.
|
|
292
|
-
*/
|
|
293
732
|
activate() {
|
|
294
733
|
if (this.#activeState) return;
|
|
734
|
+
_FocusTrap.#sweep();
|
|
295
735
|
this.#activeState = true;
|
|
296
|
-
|
|
297
|
-
this.#
|
|
298
|
-
if (this.#flag(this.#options.lockScroll, true))
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
|
|
304
|
-
document.addEventListener("keydown", this.#onKeydown);
|
|
305
|
-
this.#beforeCache.activate();
|
|
736
|
+
this.#openers = _FocusTrap.#focusPath();
|
|
737
|
+
this.#entry.openedFrom = _FocusTrap.#trapHolding(this.#openers[0] ?? null);
|
|
738
|
+
if (this.#flag(this.#options.lockScroll, true)) this.#lockScroll();
|
|
739
|
+
this.#entry.isolated = this.#flag(this.#options.isolate, true);
|
|
740
|
+
this.#joinStack();
|
|
741
|
+
_FocusTrap.#reconcileBackground(document);
|
|
306
742
|
const onEscape = this.#options.onEscape;
|
|
307
743
|
if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });
|
|
308
744
|
if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
|
|
309
745
|
}
|
|
310
|
-
/**
|
|
311
|
-
* Reverts every side effect applied by {@link activate}. No-ops if inactive, so
|
|
312
|
-
* a controller can call it defensively from both `close()` and `disconnect()`.
|
|
313
|
-
*
|
|
314
|
-
* @param restoreFocus - Move focus back to the opener (default `true`). Pass
|
|
315
|
-
* `false` on teardown (`disconnect`), where yanking focus is undesirable.
|
|
316
|
-
*/
|
|
317
746
|
deactivate({ restoreFocus = true } = {}) {
|
|
318
|
-
if (!this.#activeState)
|
|
747
|
+
if (!this.#activeState) {
|
|
748
|
+
this.#returnTabindexes();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
319
751
|
this.#activeState = false;
|
|
320
752
|
this.#escapeLayer.deactivate();
|
|
321
|
-
|
|
322
|
-
this.#
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
753
|
+
const above = this.#leaveStack();
|
|
754
|
+
this.#unlockScroll();
|
|
755
|
+
_FocusTrap.#reconcileBackground(document);
|
|
756
|
+
const wayBack = this.#handOver(above);
|
|
757
|
+
if (restoreFocus) this.#restore(wayBack, above);
|
|
758
|
+
this.#returnTabindexes();
|
|
759
|
+
}
|
|
760
|
+
connect() {
|
|
761
|
+
this.#gate.cancel();
|
|
762
|
+
_FocusTrap.#sweep();
|
|
763
|
+
new TabindexLoan("-1", FOCUS_TRAP_OWNER).reclaimWithin(document);
|
|
764
|
+
}
|
|
765
|
+
disconnect(host) {
|
|
766
|
+
if (this.#activeState) this.deactivate({ restoreFocus: false });
|
|
767
|
+
this.#gate.disconnected(host, () => this.#returnTabindexes());
|
|
768
|
+
}
|
|
769
|
+
refreshIsolation() {
|
|
770
|
+
if (!this.#activeState) return;
|
|
771
|
+
const isolate = this.#flag(this.#options.isolate, true);
|
|
772
|
+
if (isolate === this.#entry.isolated) return;
|
|
773
|
+
this.#entry.isolated = isolate;
|
|
774
|
+
const stranded = _FocusTrap.#reconcileBackground(document);
|
|
775
|
+
const active = deepActiveElement(document);
|
|
776
|
+
if (stranded.some((element) => flatTreeContains(element, active))) this.#focusInitial();
|
|
777
|
+
}
|
|
778
|
+
refreshContainer() {
|
|
779
|
+
if (!this.#activeState) return;
|
|
780
|
+
_FocusTrap.#reconcileBackground(document);
|
|
781
|
+
this.#returnTabindexes();
|
|
782
|
+
if (this.#ownsTab()) this.#refocus();
|
|
332
783
|
}
|
|
333
|
-
/** Resolves a boolean-or-getter option, defaulting when it was not provided. */
|
|
334
784
|
#flag(option, fallback) {
|
|
335
785
|
if (option === void 0) return fallback;
|
|
336
786
|
return typeof option === "function" ? option() : option;
|
|
337
787
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
* is left alone mid-navigation. The listener lives only while active.
|
|
345
|
-
*/
|
|
346
|
-
#beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));
|
|
347
|
-
/**
|
|
348
|
-
* Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the
|
|
349
|
-
* shared {@link EscapeLayer} resolver, so Tab trapping stays independent of
|
|
350
|
-
* which layer currently owns Escape.
|
|
351
|
-
*/
|
|
352
|
-
#onKeydown = (event) => {
|
|
353
|
-
if (event.key === "Tab") this.#trapTab(event);
|
|
354
|
-
};
|
|
355
|
-
/** Keeps `Tab` focus cycling within the container's focusable elements. */
|
|
356
|
-
#trapTab(event) {
|
|
357
|
-
const focusable = this.#focusableElements();
|
|
358
|
-
if (focusable.length === 0) {
|
|
359
|
-
event.preventDefault();
|
|
360
|
-
return;
|
|
788
|
+
#joinStack() {
|
|
789
|
+
const registry = _FocusTrap.#registry(document);
|
|
790
|
+
if (registry.stack.length === 0) {
|
|
791
|
+
document.addEventListener("keydown", registry.onKeydown);
|
|
792
|
+
document.addEventListener("turbo:morph", registry.onMorph);
|
|
793
|
+
document.addEventListener("turbo:morph-element", registry.onMorph);
|
|
361
794
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
795
|
+
registry.stack.push(this.#entry);
|
|
796
|
+
}
|
|
797
|
+
#leaveStack() {
|
|
798
|
+
const registry = _FocusTrap.#registry(document);
|
|
799
|
+
const index = registry.stack.indexOf(this.#entry);
|
|
800
|
+
const above = registry.stack.slice(index + 1);
|
|
801
|
+
registry.stack.splice(index, 1);
|
|
802
|
+
if (registry.stack.length === 0) {
|
|
803
|
+
document.removeEventListener("keydown", registry.onKeydown);
|
|
804
|
+
document.removeEventListener("turbo:morph", registry.onMorph);
|
|
805
|
+
document.removeEventListener("turbo:morph-element", registry.onMorph);
|
|
369
806
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
807
|
+
return above;
|
|
808
|
+
}
|
|
809
|
+
static #focusPath() {
|
|
810
|
+
const path = [];
|
|
811
|
+
for (let active = document.activeElement; active; active = active.shadowRoot?.activeElement ?? null) {
|
|
812
|
+
if (active instanceof HTMLElement && active !== document.body) path.unshift(active);
|
|
813
|
+
}
|
|
814
|
+
return path;
|
|
815
|
+
}
|
|
816
|
+
static #trapHolding(element) {
|
|
817
|
+
for (const trap of [..._FocusTrap.#registry(document).stack].reverse()) {
|
|
818
|
+
const container = trap.container();
|
|
819
|
+
if (container && flatTreeContains(container, element)) return trap;
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
#handOver(above) {
|
|
824
|
+
const entry = this.#entry;
|
|
825
|
+
const wayBack = [
|
|
826
|
+
...this.#openers,
|
|
827
|
+
this.#options.fallbackFocus?.() ?? null,
|
|
828
|
+
...entry.handedBack
|
|
829
|
+
].filter((element) => element !== null);
|
|
830
|
+
for (const trap of above) {
|
|
831
|
+
if (trap.openedFrom !== entry) continue;
|
|
832
|
+
trap.handedBack = [...trap.handedBack, ...wayBack];
|
|
833
|
+
trap.openedFrom = entry.openedFrom;
|
|
834
|
+
}
|
|
835
|
+
this.#openers = [];
|
|
836
|
+
entry.openedFrom = null;
|
|
837
|
+
entry.handedBack = [];
|
|
838
|
+
return wayBack;
|
|
839
|
+
}
|
|
840
|
+
#restore(wayBack, above) {
|
|
841
|
+
const active = deepActiveElement(document);
|
|
842
|
+
const released = this.#readContainer();
|
|
843
|
+
const holds = (trap) => {
|
|
844
|
+
const container = trap.container();
|
|
845
|
+
if (!container || !_FocusTrap.#takesTab(trap) || !flatTreeContains(container, active)) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
return !released || !flatTreeContains(released, active) || flatTreeContains(released, container);
|
|
849
|
+
};
|
|
850
|
+
if (above.some(holds)) return;
|
|
851
|
+
for (const element of wayBack) {
|
|
852
|
+
if (this.#activeState) return;
|
|
853
|
+
if (!element.isConnected || !canTakeFocus(element) || !isRenderedForFocus(element)) continue;
|
|
854
|
+
const before = deepActiveElement(document);
|
|
855
|
+
element.focus();
|
|
856
|
+
const after = deepActiveElement(document);
|
|
857
|
+
if (after === element || after !== before) return;
|
|
858
|
+
}
|
|
859
|
+
_FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack)?.refocus();
|
|
860
|
+
}
|
|
861
|
+
#refocus() {
|
|
862
|
+
if (!this.#flag(this.#options.autoFocus, true)) return;
|
|
863
|
+
if (flatTreeContains(this.#getContainer(), deepActiveElement(document))) return;
|
|
864
|
+
this.#focusInitial();
|
|
865
|
+
}
|
|
866
|
+
#lendTabindex(container) {
|
|
867
|
+
const loan = this.#tabindexLoans.get(container) ?? new TabindexLoan("-1", FOCUS_TRAP_OWNER);
|
|
868
|
+
loan.lend(container);
|
|
869
|
+
this.#tabindexLoans.set(container, loan);
|
|
870
|
+
}
|
|
871
|
+
#returnTabindexes() {
|
|
872
|
+
const focused = deepActiveElement(document);
|
|
873
|
+
for (const [element, loan] of [...this.#tabindexLoans]) {
|
|
874
|
+
if (element === focused) continue;
|
|
875
|
+
loan.returnAll();
|
|
876
|
+
this.#tabindexLoans.delete(element);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
#lockScroll() {
|
|
880
|
+
const registry = _FocusTrap.#registry(document);
|
|
881
|
+
const body = document.body;
|
|
882
|
+
if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
|
|
883
|
+
registry.scrollLocks.add(this.#entry);
|
|
884
|
+
this.#entry.lockedBody = body;
|
|
885
|
+
body.style.overflow = "hidden";
|
|
886
|
+
}
|
|
887
|
+
#unlockScroll() {
|
|
888
|
+
const registry = _FocusTrap.#registry(document);
|
|
889
|
+
const body = this.#entry.lockedBody;
|
|
890
|
+
this.#entry.lockedBody = null;
|
|
891
|
+
if (!registry.scrollLocks.delete(this.#entry) || !body) return;
|
|
892
|
+
if (!_FocusTrap.#isLocked(registry, body)) _FocusTrap.#unlock(body);
|
|
893
|
+
}
|
|
894
|
+
static #isLocked(registry, body) {
|
|
895
|
+
return [...registry.scrollLocks].some((trap) => trap.lockedBody === body);
|
|
896
|
+
}
|
|
897
|
+
static #unlock(body) {
|
|
898
|
+
const saved = body.getAttribute(OVERFLOW_MARK);
|
|
899
|
+
if (saved === null) return;
|
|
900
|
+
body.removeAttribute(OVERFLOW_MARK);
|
|
901
|
+
body.style.overflow = saved;
|
|
902
|
+
}
|
|
903
|
+
static #sweep() {
|
|
904
|
+
const registry = _FocusTrap.#registry(document);
|
|
905
|
+
for (const element of queryOpenDescendants(document, `[${INERT_MARK}]`)) {
|
|
906
|
+
if (registry.inerted.has(element)) continue;
|
|
907
|
+
element.removeAttribute(INERT_MARK);
|
|
908
|
+
element.inert = false;
|
|
909
|
+
}
|
|
910
|
+
if (!_FocusTrap.#isLocked(registry, document.body)) _FocusTrap.#unlock(document.body);
|
|
911
|
+
}
|
|
912
|
+
static #registry(ownerDocument) {
|
|
913
|
+
const existing = _FocusTrap.#registries.get(ownerDocument);
|
|
914
|
+
if (existing) return existing;
|
|
915
|
+
const morphs = new MicrotaskCoalescer(() => _FocusTrap.#reapply(registry));
|
|
916
|
+
morphs.activate();
|
|
917
|
+
const registry = {
|
|
918
|
+
stack: [],
|
|
919
|
+
onKeydown: (event) => _FocusTrap.#resolveTab(registry.stack, event),
|
|
920
|
+
onMorph: () => morphs.schedule(),
|
|
921
|
+
inerted: /* @__PURE__ */ new Set(),
|
|
922
|
+
scrollLocks: /* @__PURE__ */ new Set()
|
|
923
|
+
};
|
|
924
|
+
_FocusTrap.#registries.set(ownerDocument, registry);
|
|
925
|
+
return registry;
|
|
926
|
+
}
|
|
927
|
+
static #reapply(registry) {
|
|
928
|
+
_FocusTrap.#reconcileBackground(document);
|
|
929
|
+
const body = document.body;
|
|
930
|
+
if (!_FocusTrap.#isLocked(registry, body)) return;
|
|
931
|
+
if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
|
|
932
|
+
body.style.overflow = "hidden";
|
|
933
|
+
}
|
|
934
|
+
static #reconcileBackground(ownerDocument) {
|
|
935
|
+
const registry = _FocusTrap.#registry(ownerDocument);
|
|
936
|
+
for (const element of registry.inerted) {
|
|
937
|
+
if (!element.hasAttribute(INERT_MARK)) registry.inerted.delete(element);
|
|
938
|
+
}
|
|
939
|
+
const background = _FocusTrap.#backgroundOf(registry);
|
|
940
|
+
for (const element of registry.inerted) {
|
|
941
|
+
if (background.has(element)) continue;
|
|
942
|
+
element.removeAttribute(INERT_MARK);
|
|
943
|
+
element.inert = false;
|
|
944
|
+
registry.inerted.delete(element);
|
|
945
|
+
}
|
|
946
|
+
const added = [];
|
|
947
|
+
for (const element of background) {
|
|
948
|
+
if (registry.inerted.has(element)) continue;
|
|
949
|
+
element.setAttribute(INERT_MARK, "");
|
|
950
|
+
element.inert = true;
|
|
951
|
+
registry.inerted.add(element);
|
|
952
|
+
added.push(element);
|
|
953
|
+
}
|
|
954
|
+
return added;
|
|
955
|
+
}
|
|
956
|
+
static #backgroundOf(registry) {
|
|
957
|
+
const containers = registry.stack.map((trap) => trap.container());
|
|
958
|
+
let top = -1;
|
|
959
|
+
registry.stack.forEach((trap, index) => {
|
|
960
|
+
if (trap.isolated && containers[index]) top = index;
|
|
961
|
+
});
|
|
962
|
+
const background = /* @__PURE__ */ new Set();
|
|
963
|
+
if (top < 0) return background;
|
|
964
|
+
const kept = /* @__PURE__ */ new Set();
|
|
965
|
+
const keep = (path) => {
|
|
966
|
+
for (const node of path) {
|
|
967
|
+
kept.add(node);
|
|
968
|
+
background.delete(node);
|
|
969
|
+
}
|
|
970
|
+
for (const node of path) {
|
|
971
|
+
const parent = flatTreeParent(node);
|
|
972
|
+
for (const sibling of parent ? flatTreeChildren(parent) : []) {
|
|
973
|
+
if (!(sibling instanceof HTMLElement) || kept.has(sibling)) continue;
|
|
974
|
+
if (sibling.inert && !registry.inerted.has(sibling)) continue;
|
|
975
|
+
background.add(sibling);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
containers.slice(top).forEach((container, index) => {
|
|
980
|
+
const path = [];
|
|
981
|
+
for (let node = container; node && node !== document.body && !kept.has(node); ) {
|
|
982
|
+
path.push(node);
|
|
983
|
+
node = flatTreeParent(node);
|
|
400
984
|
}
|
|
401
|
-
node
|
|
985
|
+
if (index === 0 || path.some((node) => background.has(node))) keep(path);
|
|
986
|
+
});
|
|
987
|
+
return background;
|
|
988
|
+
}
|
|
989
|
+
#readContainer() {
|
|
990
|
+
try {
|
|
991
|
+
return this.#getContainer();
|
|
992
|
+
} catch {
|
|
993
|
+
return null;
|
|
402
994
|
}
|
|
403
995
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
996
|
+
static #resolveTab(stack, event) {
|
|
997
|
+
if (event.key !== "Tab" || event.defaultPrevented) return;
|
|
998
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
999
|
+
if (event.ctrlKey || event.metaKey) return;
|
|
1000
|
+
const owner = _FocusTrap.#tabOwner(stack);
|
|
1001
|
+
if (!owner) return;
|
|
1002
|
+
event.preventDefault();
|
|
1003
|
+
owner.moveFocus(event.shiftKey);
|
|
1004
|
+
}
|
|
1005
|
+
static #tabOwner(stack) {
|
|
1006
|
+
for (const trap of [...stack].reverse()) {
|
|
1007
|
+
if (_FocusTrap.#takesTab(trap)) return trap;
|
|
1008
|
+
}
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
static #takesTab(trap) {
|
|
1012
|
+
const container = trap.container();
|
|
1013
|
+
if (!container) return false;
|
|
1014
|
+
return container.isConnected && !closestInFlatTree(container, "[hidden]") && isRenderedForFocus(container);
|
|
1015
|
+
}
|
|
1016
|
+
#ownsTab() {
|
|
1017
|
+
return _FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack) === this.#entry;
|
|
1018
|
+
}
|
|
1019
|
+
#moveFocus(backward) {
|
|
1020
|
+
const from = deepActiveElement(document);
|
|
1021
|
+
for (const stop of sequentialTabStops(this.#getContainer(), from, backward)) {
|
|
1022
|
+
if (!this.#ownsTab()) return;
|
|
1023
|
+
const before = deepActiveElement(document);
|
|
1024
|
+
stop.focus();
|
|
1025
|
+
const after = deepActiveElement(document);
|
|
1026
|
+
if (after === before) continue;
|
|
1027
|
+
if (after === stop) _FocusTrap.#selectOnEntry(stop);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
static #selectOnEntry(stop) {
|
|
1032
|
+
if (stop instanceof HTMLInputElement && _FocusTrap.#TEXT_ENTRY_TYPES.has(stop.type)) {
|
|
1033
|
+
stop.select();
|
|
408
1034
|
}
|
|
409
|
-
this.#inertedSiblings = [];
|
|
410
1035
|
}
|
|
411
|
-
/** Moves focus to the initial target, the first focusable, or the container. */
|
|
412
1036
|
#focusInitial() {
|
|
413
1037
|
const preferred = this.#options.initialFocus?.();
|
|
414
1038
|
if (preferred) {
|
|
415
1039
|
preferred.focus();
|
|
416
1040
|
return;
|
|
417
1041
|
}
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
1042
|
+
const container = this.#getContainer();
|
|
1043
|
+
const [first] = sequentialTabStops(container);
|
|
1044
|
+
if (first) {
|
|
1045
|
+
first.focus();
|
|
421
1046
|
return;
|
|
422
1047
|
}
|
|
423
|
-
|
|
424
|
-
container.tabIndex = -1;
|
|
1048
|
+
this.#lendTabindex(container);
|
|
425
1049
|
container.focus();
|
|
426
1050
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
var ListenerSet = class {
|
|
1054
|
+
#abort = new AbortController();
|
|
1055
|
+
add(target, type, handler, options) {
|
|
1056
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
1057
|
+
}
|
|
1058
|
+
dispose() {
|
|
1059
|
+
this.#abort.abort();
|
|
1060
|
+
this.#abort = new AbortController();
|
|
430
1061
|
}
|
|
431
1062
|
};
|
|
432
1063
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
#
|
|
436
|
-
#
|
|
437
|
-
#
|
|
438
|
-
#generation = 0;
|
|
439
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
1064
|
+
var MorphRenderWatcher = class {
|
|
1065
|
+
#listeners = new ListenerSet();
|
|
1066
|
+
#pass;
|
|
1067
|
+
#element = null;
|
|
1068
|
+
#onMorph = () => this.#pass.schedule();
|
|
440
1069
|
constructor(run) {
|
|
441
|
-
this.#
|
|
1070
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
442
1071
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
this
|
|
1072
|
+
observe(element) {
|
|
1073
|
+
if (this.#element === element) return;
|
|
1074
|
+
this.disconnect();
|
|
1075
|
+
this.#element = element;
|
|
1076
|
+
this.#pass.activate();
|
|
1077
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
446
1078
|
}
|
|
447
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
448
|
-
cancel() {
|
|
449
|
-
this.#active = false;
|
|
450
|
-
this.#queued = false;
|
|
451
|
-
this.#generation += 1;
|
|
452
|
-
}
|
|
453
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
454
1079
|
schedule() {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
this.#run();
|
|
462
|
-
});
|
|
1080
|
+
this.#pass.schedule();
|
|
1081
|
+
}
|
|
1082
|
+
disconnect() {
|
|
1083
|
+
this.#listeners.dispose();
|
|
1084
|
+
this.#pass.cancel();
|
|
1085
|
+
this.#element = null;
|
|
463
1086
|
}
|
|
464
1087
|
};
|
|
465
1088
|
|
|
466
|
-
|
|
1089
|
+
var LivedMark = class {
|
|
1090
|
+
#name;
|
|
1091
|
+
#lived = false;
|
|
1092
|
+
#element = null;
|
|
1093
|
+
#morph = new MorphRenderWatcher(() => this.#write());
|
|
1094
|
+
constructor(identifier) {
|
|
1095
|
+
this.#name = `data-${identifier}-lived`;
|
|
1096
|
+
}
|
|
1097
|
+
connect(element) {
|
|
1098
|
+
const connection = this.#lived ? "reconnect" : element.hasAttribute(this.#name) ? "restored" : "fresh";
|
|
1099
|
+
this.#lived = true;
|
|
1100
|
+
this.#element = element;
|
|
1101
|
+
this.#write();
|
|
1102
|
+
this.#morph.observe(element);
|
|
1103
|
+
return connection;
|
|
1104
|
+
}
|
|
1105
|
+
disconnect() {
|
|
1106
|
+
this.#morph.disconnect();
|
|
1107
|
+
}
|
|
1108
|
+
#write() {
|
|
1109
|
+
this.#element?.setAttribute(this.#name, "");
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
function targetSelector(identifier, name) {
|
|
1114
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
467
1117
|
var CommandPaletteController = class _CommandPaletteController extends Controller {
|
|
468
1118
|
static #ORIGINAL_ARIA_DISABLED = "data-command-palette-original-aria-disabled";
|
|
469
1119
|
static #ABSENT_ARIA_DISABLED = "absent";
|
|
@@ -478,85 +1128,103 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
478
1128
|
"filter",
|
|
479
1129
|
"onKeydown",
|
|
480
1130
|
"open",
|
|
481
|
-
"
|
|
1131
|
+
"select",
|
|
482
1132
|
"toggle"
|
|
483
1133
|
];
|
|
484
1134
|
static events = ["select"];
|
|
485
|
-
/** Stable ID of the active option; the live target is resolved before every use. */
|
|
486
1135
|
#activeId = null;
|
|
487
|
-
/** Visible/selectable target ID order captured for removal fallback. */
|
|
488
1136
|
#activeOrder = [];
|
|
489
1137
|
#connected = false;
|
|
490
|
-
|
|
491
|
-
#
|
|
492
|
-
|
|
1138
|
+
#lived = new LivedMark(this.identifier);
|
|
1139
|
+
#dialog = null;
|
|
1140
|
+
#hidden = new AttributeLease("hidden", this.identifier);
|
|
1141
|
+
#expanded = new AttributeLease("aria-expanded", this.identifier);
|
|
1142
|
+
#activeDescendant = new AttributeLease(
|
|
1143
|
+
"aria-activedescendant",
|
|
1144
|
+
this.identifier
|
|
1145
|
+
);
|
|
1146
|
+
#reconcile = new MicrotaskCoalescer(() => {
|
|
1147
|
+
if (this.hasInputTarget) this.#expanded.write(this.inputTarget, String(this.#isOpen));
|
|
1148
|
+
this.#reconcileActive();
|
|
1149
|
+
});
|
|
1150
|
+
#morphRender = new MorphRenderWatcher(() => this.#repair());
|
|
493
1151
|
#composition = new CompositionTracker({
|
|
494
1152
|
onEnd: () => {
|
|
495
1153
|
if (this.#isOpen) this.filter();
|
|
496
1154
|
}
|
|
497
1155
|
});
|
|
498
|
-
/**
|
|
499
|
-
* Owns the modal side effects (focus trap, scroll lock, background `inert`, focus
|
|
500
|
-
* restore). Escape closes; focus on open goes to the input, and is restored to
|
|
501
|
-
* whatever opened the palette on close.
|
|
502
|
-
*/
|
|
503
1156
|
#trap = new FocusTrap(() => this.dialogTarget, {
|
|
504
1157
|
onEscape: () => this.close(),
|
|
505
|
-
initialFocus: () =>
|
|
1158
|
+
initialFocus: () => {
|
|
1159
|
+
const dialog = this.dialogTarget;
|
|
1160
|
+
return this.inputTargets.find((input) => dialog.contains(input)) ?? null;
|
|
1161
|
+
}
|
|
506
1162
|
});
|
|
507
|
-
/**
|
|
508
|
-
* Initializes the global hotkey handler and establishes the initial open state.
|
|
509
|
-
*
|
|
510
|
-
* The DOM is the source of truth on reconnect (Turbo cache restore / morph): if
|
|
511
|
-
* the restored snapshot already shows the dialog open, honor that rather than
|
|
512
|
-
* re-deriving from the declarative `open` Value (which would slam a user-opened
|
|
513
|
-
* palette shut). The `open` Value only seeds the initial state of a genuinely
|
|
514
|
-
* fresh render. We normalize to a clean closed baseline first so {@link open}
|
|
515
|
-
* runs its full setup — the `FocusTrap` is a fresh instance after a
|
|
516
|
-
* reconnect and must be re-activated.
|
|
517
|
-
*/
|
|
518
1163
|
connect() {
|
|
1164
|
+
const connection = this.#lived.connect(this.element);
|
|
1165
|
+
this.#trap.connect();
|
|
519
1166
|
document.addEventListener("keydown", this.#onGlobalKeydown);
|
|
520
1167
|
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
521
1168
|
this.#syncOptionSemantics();
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1169
|
+
if (connection === "reconnect" && this.#isOpen) {
|
|
1170
|
+
this.#trap.activate();
|
|
1171
|
+
} else {
|
|
1172
|
+
const shouldOpen = connection !== "restored" && (this.#isOpen || this.openValue);
|
|
1173
|
+
this.#resetToClosedState();
|
|
1174
|
+
if (shouldOpen) this.open();
|
|
1175
|
+
}
|
|
1176
|
+
this.#dialog = this.hasDialogTarget ? this.dialogTarget : null;
|
|
525
1177
|
this.#connected = true;
|
|
526
1178
|
this.#reconcile.activate();
|
|
1179
|
+
this.#morphRender.observe(this.element);
|
|
527
1180
|
}
|
|
528
|
-
/** Tears down the global hotkey listener and reverts the modal side effects. */
|
|
529
1181
|
disconnect() {
|
|
530
1182
|
this.#connected = false;
|
|
1183
|
+
this.#lived.disconnect();
|
|
1184
|
+
this.#morphRender.disconnect();
|
|
531
1185
|
this.#reconcile.cancel();
|
|
532
1186
|
document.removeEventListener("keydown", this.#onGlobalKeydown);
|
|
533
1187
|
this.#composition.disconnect();
|
|
534
|
-
this.#trap.
|
|
535
|
-
this.#resetToClosedState();
|
|
1188
|
+
this.#trap.disconnect(this);
|
|
536
1189
|
}
|
|
537
|
-
/** Initializes semantics for an option added before or after the controller connects. */
|
|
538
1190
|
optionTargetConnected(option) {
|
|
539
1191
|
this.#syncOptionSemanticsFor(option);
|
|
540
1192
|
option.setAttribute("aria-selected", "false");
|
|
541
1193
|
option.removeAttribute("data-active");
|
|
542
1194
|
if (this.#connected) this.#queueOptionReconciliation();
|
|
543
1195
|
}
|
|
544
|
-
/** Clears active residue on the old node and reconciles the surviving targets. */
|
|
545
1196
|
optionTargetDisconnected(option) {
|
|
546
1197
|
option.setAttribute("aria-selected", "false");
|
|
547
1198
|
option.removeAttribute("data-active");
|
|
548
1199
|
if (this.#connected) this.#queueOptionReconciliation();
|
|
549
1200
|
}
|
|
550
|
-
/** Tracks an input added initially or after connect without extra consumer actions. */
|
|
551
1201
|
inputTargetConnected(input) {
|
|
552
1202
|
this.#composition.observe(input);
|
|
553
1203
|
if (this.#connected) this.#queueOptionReconciliation();
|
|
554
1204
|
}
|
|
555
|
-
/** Removes controller-owned listeners when the input target is replaced or removed. */
|
|
556
1205
|
inputTargetDisconnected(input) {
|
|
557
1206
|
this.#composition.unobserve(input);
|
|
1207
|
+
if (!this.inputTargets.includes(input)) {
|
|
1208
|
+
this.#expanded.return(input);
|
|
1209
|
+
this.#activeDescendant.return(input);
|
|
1210
|
+
}
|
|
1211
|
+
this.#queueOptionReconciliation();
|
|
1212
|
+
}
|
|
1213
|
+
dialogTargetConnected() {
|
|
1214
|
+
if (this.#connected) this.#adoptDialog();
|
|
1215
|
+
}
|
|
1216
|
+
dialogTargetDisconnected(dialog) {
|
|
1217
|
+
if (this.dialogTargets.includes(dialog)) return;
|
|
1218
|
+
this.#hidden.return(dialog);
|
|
1219
|
+
if (this.#connected) this.#adoptDialog();
|
|
1220
|
+
}
|
|
1221
|
+
emptyTargetConnected() {
|
|
1222
|
+
this.#queueOptionReconciliation();
|
|
1223
|
+
}
|
|
1224
|
+
emptyTargetDisconnected(empty) {
|
|
1225
|
+
if (!this.emptyTargets.includes(empty)) this.#hidden.return(empty);
|
|
1226
|
+
this.#queueOptionReconciliation();
|
|
558
1227
|
}
|
|
559
|
-
/** Toggles the open state of the command palette. */
|
|
560
1228
|
toggle() {
|
|
561
1229
|
if (this.#isOpen) {
|
|
562
1230
|
this.close();
|
|
@@ -564,25 +1232,43 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
564
1232
|
this.open();
|
|
565
1233
|
}
|
|
566
1234
|
}
|
|
567
|
-
/** Opens the palette, traps focus, and shifts focus to the input. */
|
|
568
1235
|
open() {
|
|
569
1236
|
if (!this.hasDialogTarget || this.#isOpen) return;
|
|
570
|
-
this.dialogTarget
|
|
1237
|
+
this.#hidden.write(this.dialogTarget, null);
|
|
571
1238
|
this.openValue = true;
|
|
572
|
-
if (this.hasInputTarget) this.inputTarget
|
|
1239
|
+
if (this.hasInputTarget) this.#expanded.write(this.inputTarget, "true");
|
|
573
1240
|
this.#resetFilter();
|
|
574
1241
|
this.#trap.activate();
|
|
575
1242
|
}
|
|
576
|
-
/** Closes the palette and restores focus back to the opener. */
|
|
577
1243
|
close() {
|
|
1244
|
+
this.#close(true);
|
|
1245
|
+
}
|
|
1246
|
+
#adoptDialog() {
|
|
1247
|
+
const dialog = this.hasDialogTarget ? this.dialogTarget : null;
|
|
1248
|
+
if (dialog === this.#dialog) return;
|
|
1249
|
+
this.#dialog = dialog;
|
|
1250
|
+
if (!this.#trap.active) {
|
|
1251
|
+
if (dialog) this.#hidden.write(dialog, "");
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (!dialog) {
|
|
1255
|
+
this.#resetToClosedState();
|
|
1256
|
+
this.#trap.deactivate();
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
this.#hidden.write(dialog, null);
|
|
1260
|
+
this.#trap.refreshContainer();
|
|
1261
|
+
}
|
|
1262
|
+
#close(restoreFocus) {
|
|
578
1263
|
if (!this.hasDialogTarget || !this.#isOpen) return;
|
|
579
1264
|
this.#resetToClosedState();
|
|
580
|
-
this.#trap.deactivate();
|
|
1265
|
+
this.#trap.deactivate({ restoreFocus });
|
|
581
1266
|
}
|
|
582
|
-
/** Filters option elements in-memory matching the input value. Bound to input target. */
|
|
583
1267
|
filter(event) {
|
|
584
1268
|
if (!this.hasInputTarget || this.#composition.isComposing(event)) return;
|
|
585
|
-
this.#
|
|
1269
|
+
this.#setActiveIndex(this.#applyQuery() ? 0 : -1);
|
|
1270
|
+
}
|
|
1271
|
+
#applyQuery() {
|
|
586
1272
|
const query = this.inputTarget.value.trim().toLowerCase();
|
|
587
1273
|
let hasSelectableMatch = false;
|
|
588
1274
|
for (const option of this.optionTargets) {
|
|
@@ -596,31 +1282,35 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
596
1282
|
}
|
|
597
1283
|
}
|
|
598
1284
|
if (this.hasEmptyTarget) {
|
|
599
|
-
this.emptyTarget
|
|
1285
|
+
this.#hidden.write(this.emptyTarget, hasSelectableMatch ? "" : null);
|
|
600
1286
|
}
|
|
601
|
-
|
|
1287
|
+
return hasSelectableMatch;
|
|
1288
|
+
}
|
|
1289
|
+
#repair() {
|
|
1290
|
+
if (!this.hasDialogTarget) return;
|
|
1291
|
+
const open = this.#trap.active;
|
|
1292
|
+
this.#hidden.write(this.dialogTarget, open ? null : "");
|
|
1293
|
+
if (open && this.hasInputTarget) this.#applyQuery();
|
|
1294
|
+
this.#reconcile.schedule();
|
|
602
1295
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const option =
|
|
1296
|
+
select(source) {
|
|
1297
|
+
const { event, host, origin, reason } = actionSource(source);
|
|
1298
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
1299
|
+
return;
|
|
1300
|
+
const option = host?.closest(targetSelector(this.identifier, "option"));
|
|
608
1301
|
if (!option || !this.optionTargets.includes(option)) return;
|
|
609
1302
|
this.#syncOptionSemanticsFor(option);
|
|
610
1303
|
if (!option.hasAttribute("hidden") && !this.#isDisabled(option)) {
|
|
611
|
-
this.#confirmSelection(
|
|
1304
|
+
this.#confirmSelection(
|
|
1305
|
+
option,
|
|
1306
|
+
reason,
|
|
1307
|
+
event !== null || this.element.contains(document.activeElement)
|
|
1308
|
+
);
|
|
612
1309
|
}
|
|
613
1310
|
}
|
|
614
|
-
/** Closes when the backdrop (the dialog target itself) is clicked, ignoring inner clicks. */
|
|
615
1311
|
closeOnBackdrop(event) {
|
|
616
1312
|
if (event.target === this.dialogTarget) this.close();
|
|
617
1313
|
}
|
|
618
|
-
/**
|
|
619
|
-
* Combobox navigation keys (arrows / Home / End / Enter), bound to the input.
|
|
620
|
-
* `Tab` (focus trap) and `Escape` (close) are owned by the shared `FocusTrap` at
|
|
621
|
-
* the document level, so they work no matter which element inside the dialog has
|
|
622
|
-
* focus — not only the input.
|
|
623
|
-
*/
|
|
624
1314
|
onKeydown(event) {
|
|
625
1315
|
if (event.defaultPrevented) return;
|
|
626
1316
|
if (isReservedArrowChord(event)) return;
|
|
@@ -676,14 +1366,9 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
676
1366
|
this.#activeId = activeOption?.id || null;
|
|
677
1367
|
this.#activeOrder = activeOption ? visible.map((option) => option.id).filter(Boolean) : [];
|
|
678
1368
|
if (this.hasInputTarget) {
|
|
679
|
-
|
|
680
|
-
this.inputTarget.setAttribute("aria-activedescendant", activeOption.id);
|
|
681
|
-
} else {
|
|
682
|
-
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
683
|
-
}
|
|
1369
|
+
this.#activeDescendant.write(this.inputTarget, activeOption?.id || null);
|
|
684
1370
|
}
|
|
685
1371
|
}
|
|
686
|
-
/** Re-resolves active identity and component-specific fallback against live targets. */
|
|
687
1372
|
#reconcileActive() {
|
|
688
1373
|
if (!this.#isOpen) {
|
|
689
1374
|
this.#setActiveIndex(-1);
|
|
@@ -710,7 +1395,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
710
1395
|
this.#setActiveIndex(activeId ? this.#findFallbackIndex(visible, activeId) : -1);
|
|
711
1396
|
this.#reflectEmptyState();
|
|
712
1397
|
}
|
|
713
|
-
/** Resolves stable ID first, then active markers for the existing ID-less case. */
|
|
714
1398
|
#findActiveIndex(options) {
|
|
715
1399
|
const activeId = (this.hasInputTarget ? this.inputTarget.getAttribute("aria-activedescendant") : null) ?? this.#activeId;
|
|
716
1400
|
if (activeId) return options.findIndex((option) => option.id === activeId);
|
|
@@ -718,7 +1402,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
718
1402
|
(option) => option.hasAttribute("data-active") || option.getAttribute("aria-selected") === "true"
|
|
719
1403
|
);
|
|
720
1404
|
}
|
|
721
|
-
/** Chooses a surviving former successor, then predecessor, from selectable targets only. */
|
|
722
1405
|
#findFallbackIndex(options, activeId) {
|
|
723
1406
|
const oldIndex = this.#activeOrder.indexOf(activeId);
|
|
724
1407
|
if (oldIndex < 0) return -1;
|
|
@@ -733,28 +1416,21 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
733
1416
|
}
|
|
734
1417
|
return -1;
|
|
735
1418
|
}
|
|
736
|
-
/** Reflects whether any visible, navigable command remains. */
|
|
737
1419
|
#reflectEmptyState() {
|
|
738
|
-
if (this.hasEmptyTarget)
|
|
1420
|
+
if (this.hasEmptyTarget) {
|
|
1421
|
+
this.#hidden.write(this.emptyTarget, this.#visibleOptions.length > 0 ? "" : null);
|
|
1422
|
+
}
|
|
739
1423
|
}
|
|
740
|
-
/** Coalesces all target callbacks from one MutationObserver batch. */
|
|
741
1424
|
#queueOptionReconciliation() {
|
|
742
1425
|
this.#reconcile.schedule();
|
|
743
1426
|
}
|
|
744
|
-
|
|
745
|
-
* Runs `option`: dispatches `select` and closes.
|
|
746
|
-
*
|
|
747
|
-
* **Activation is refused here, not at the call sites.** Every path that runs a
|
|
748
|
-
* command funnels through this method, so one check covers keyboard, pointer,
|
|
749
|
-
* and anything added later. Per-call-site guards would leave `aria-disabled`
|
|
750
|
-
* options — which stay within the keyboard's reach — runnable from whichever
|
|
751
|
-
* path forgets to consult the predicate.
|
|
752
|
-
*/
|
|
753
|
-
#confirmSelection(option) {
|
|
1427
|
+
#confirmSelection(option, reason = "user", restoreFocus = true) {
|
|
754
1428
|
if (this.#isDisabled(option)) return;
|
|
755
1429
|
const value = option.dataset.value || option.textContent || "";
|
|
756
|
-
this.dispatch("select", { detail: { value, option } });
|
|
757
|
-
this
|
|
1430
|
+
this.dispatch("select", { detail: { value, option, reason } });
|
|
1431
|
+
this.#close(
|
|
1432
|
+
restoreFocus && (reason !== "api" || this.element.contains(document.activeElement))
|
|
1433
|
+
);
|
|
758
1434
|
}
|
|
759
1435
|
#resetFilter() {
|
|
760
1436
|
if (this.hasInputTarget) this.inputTarget.value = "";
|
|
@@ -770,43 +1446,15 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
770
1446
|
(option) => !option.hasAttribute("hidden") && this.#isNavigable(option)
|
|
771
1447
|
);
|
|
772
1448
|
}
|
|
773
|
-
/**
|
|
774
|
-
* Whether virtual focus may land on `option`.
|
|
775
|
-
*
|
|
776
|
-
* Only `data-disabled` disqualifies. That attribute is this controller's own —
|
|
777
|
-
* authors reach for it to mark a row that is not a destination at all, such as
|
|
778
|
-
* a group heading — so it can mean "skip entirely" without contradicting ARIA.
|
|
779
|
-
*
|
|
780
|
-
* **`aria-disabled` does not disqualify.** It marks a command that must stay
|
|
781
|
-
* *discoverable*: `aria-activedescendant` names it, so a reader announces it as
|
|
782
|
-
* unavailable rather than hiding its existence. Virtual focus does
|
|
783
|
-
* not change that — pointing `aria-activedescendant` at a disabled option is
|
|
784
|
-
* ordinary ARIA. Keeping the two attributes distinct is what leaves both
|
|
785
|
-
* meanings expressible; collapsing them would take "show it, say it is
|
|
786
|
-
* unavailable, do not run it" away from consumers.
|
|
787
|
-
*/
|
|
788
1449
|
#isNavigable(option) {
|
|
789
1450
|
return option.dataset.disabled !== "true";
|
|
790
1451
|
}
|
|
791
|
-
/** Whether activating `option` may run. Suppressed for either disabled marker. */
|
|
792
1452
|
#isDisabled(option) {
|
|
793
1453
|
return option.dataset.disabled === "true" || option.getAttribute("aria-disabled") === "true";
|
|
794
1454
|
}
|
|
795
|
-
/** Synchronizes every option's controller-owned selection and disabled semantics. */
|
|
796
1455
|
#syncOptionSemantics() {
|
|
797
1456
|
for (const option of this.optionTargets) this.#syncOptionSemanticsFor(option);
|
|
798
1457
|
}
|
|
799
|
-
/**
|
|
800
|
-
* Reflects `data-disabled` to ARIA without losing a pre-existing authored value,
|
|
801
|
-
* and puts `aria-selected` back to its baseline.
|
|
802
|
-
*
|
|
803
|
-
* Here `aria-selected` marks the *active* option, not a committed choice — the
|
|
804
|
-
* palette runs a command and keeps nothing selected — so an authored value is
|
|
805
|
-
* meaningless and is overwritten rather than preserved. Every caller either
|
|
806
|
-
* establishes the active option straight after (`filter`, `#setActiveIndex`) or
|
|
807
|
-
* is handling an option that cannot be the active one yet
|
|
808
|
-
* (`optionTargetConnected`).
|
|
809
|
-
*/
|
|
810
1458
|
#syncOptionSemanticsFor(option) {
|
|
811
1459
|
const originalAttribute = _CommandPaletteController.#ORIGINAL_ARIA_DISABLED;
|
|
812
1460
|
const originalValue = option.getAttribute(originalAttribute);
|
|
@@ -836,7 +1484,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
836
1484
|
event.preventDefault();
|
|
837
1485
|
this.toggle();
|
|
838
1486
|
};
|
|
839
|
-
/** Matches the configured `mod+key` or bare-key hotkey without extra modifiers. */
|
|
840
1487
|
#matchesHotkey(event) {
|
|
841
1488
|
const hotkey = this.#parseHotkey();
|
|
842
1489
|
if (!hotkey || event.altKey || event.shiftKey) return false;
|
|
@@ -848,7 +1495,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
848
1495
|
}
|
|
849
1496
|
return event.key.toLowerCase() === hotkey.key;
|
|
850
1497
|
}
|
|
851
|
-
/** Parses the intentionally small public hotkey grammar. */
|
|
852
1498
|
#parseHotkey() {
|
|
853
1499
|
const parts = this.hotkeyValue.toLowerCase().split("+");
|
|
854
1500
|
if (parts.length === 1 && parts[0] && parts[0] !== "mod") {
|
|
@@ -859,7 +1505,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
859
1505
|
}
|
|
860
1506
|
return null;
|
|
861
1507
|
}
|
|
862
|
-
/** Resets transient open state so reconnect starts from a predictable closed snapshot. */
|
|
863
1508
|
#resetToClosedState() {
|
|
864
1509
|
this.#activeId = null;
|
|
865
1510
|
this.#activeOrder = [];
|
|
@@ -869,15 +1514,14 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
869
1514
|
option.removeAttribute("data-active");
|
|
870
1515
|
}
|
|
871
1516
|
if (this.hasDialogTarget) {
|
|
872
|
-
this.dialogTarget
|
|
1517
|
+
this.#hidden.write(this.dialogTarget, "");
|
|
873
1518
|
}
|
|
874
1519
|
if (this.hasInputTarget) {
|
|
875
|
-
this.inputTarget
|
|
876
|
-
this.inputTarget
|
|
1520
|
+
this.#expanded.write(this.inputTarget, "false");
|
|
1521
|
+
this.#activeDescendant.write(this.inputTarget, null);
|
|
877
1522
|
}
|
|
878
1523
|
}
|
|
879
1524
|
};
|
|
880
1525
|
|
|
881
1526
|
export { CommandPaletteController };
|
|
882
|
-
//# sourceMappingURL=command_palette_controller.js.map
|
|
883
1527
|
//# sourceMappingURL=command_palette_controller.js.map
|