stimeo-ui 0.16.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 +31 -0
- package/README.md +8 -1
- package/dist/cable/index.d.ts +79 -23
- package/dist/cable/index.js +363 -436
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +4 -2
- package/dist/controllers/accordion_controller.js +166 -55
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +12 -2
- 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 +35 -5
- package/dist/controllers/announcer_controller.js +135 -198
- 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 +271 -127
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +171 -105
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +0 -130
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +18 -0
- package/dist/controllers/bulk_select_controller.js +262 -52
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +24 -5
- package/dist/controllers/calendar_controller.js +175 -209
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +34 -7
- package/dist/controllers/carousel_controller.js +461 -399
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +17 -1
- package/dist/controllers/character_counter_controller.js +246 -156
- 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 +32 -74
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +16 -5
- package/dist/controllers/clipboard_controller.js +168 -147
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +16 -4
- package/dist/controllers/collapsible_controller.js +301 -120
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +18 -6
- package/dist/controllers/color_picker_controller.js +149 -139
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +23 -5
- package/dist/controllers/combobox_controller.js +360 -133
- 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 +970 -358
- 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 +166 -124
- 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 -248
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +31 -4
- package/dist/controllers/context_menu_controller.js +256 -128
- 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 +45 -1
- package/dist/controllers/countdown_controller.js +183 -148
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +21 -2
- package/dist/controllers/currency_input_controller.js +134 -158
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +15 -4
- package/dist/controllers/data_grid_controller.js +114 -186
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +30 -5
- package/dist/controllers/date_range_picker_controller.js +288 -206
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +13 -1
- package/dist/controllers/dialog_controller.js +858 -223
- 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 +73 -153
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js +14 -48
- 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 -21
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +39 -11
- package/dist/controllers/drawer_controller.js +933 -341
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +31 -1
- package/dist/controllers/dropdown_controller.js +312 -82
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +158 -68
- 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 +311 -207
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +67 -39
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +31 -8
- package/dist/controllers/flash_controller.js +172 -314
- 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 -240
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +147 -132
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +13 -86
- 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 +264 -213
- 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 +107 -75
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +56 -3
- package/dist/controllers/hover_card_controller.js +309 -118
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +42 -1
- package/dist/controllers/idle_controller.js +267 -101
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js +71 -72
- 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 -106
- 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 -65
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +34 -2
- package/dist/controllers/listbox_controller.js +306 -204
- 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 -93
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +32 -7
- package/dist/controllers/menu_controller.js +214 -156
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +4 -4
- package/dist/controllers/menubar_controller.js +170 -329
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +20 -0
- package/dist/controllers/meter_controller.js +151 -67
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +32 -4
- package/dist/controllers/multi_select_controller.js +593 -358
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +8 -1
- package/dist/controllers/navigation_menu_controller.js +124 -232
- 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 +352 -182
- 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 +242 -54
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +26 -2
- package/dist/controllers/number_input_controller.js +320 -263
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +16 -76
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +20 -4
- package/dist/controllers/otp_controller.js +198 -193
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
- package/dist/controllers/overflow_indicator_controller.js +116 -113
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +48 -11
- package/dist/controllers/overflow_menu_controller.js +343 -283
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +35 -6
- package/dist/controllers/pagination_controller.js +295 -119
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +45 -8
- package/dist/controllers/password_reveal_controller.js +389 -121
- 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 +237 -103
- 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 +185 -165
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +38 -2
- package/dist/controllers/popover_controller.js +236 -97
- 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 -76
- 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 +232 -116
- 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 +125 -56
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +14 -5
- package/dist/controllers/radio_group_controller.js +117 -137
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +43 -3
- package/dist/controllers/range_slider_controller.js +192 -105
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +20 -4
- package/dist/controllers/rating_controller.js +418 -207
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +2 -0
- package/dist/controllers/read_more_controller.js +134 -63
- 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 +28 -2
- package/dist/controllers/resizable_controller.js +255 -139
- 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 -225
- 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 -145
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +6 -1
- package/dist/controllers/scrollspy_controller.js +181 -242
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +19 -4
- package/dist/controllers/separator_controller.js +267 -140
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +64 -7
- package/dist/controllers/sidebar_controller.js +1023 -386
- 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 -97
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +27 -1
- package/dist/controllers/slider_controller.js +159 -79
- 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 +162 -63
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +0 -112
- 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 -169
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +5 -0
- package/dist/controllers/step_indicator_controller.js +99 -46
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +12 -3
- package/dist/controllers/stepper_controller.js +114 -73
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +6 -0
- package/dist/controllers/stick_to_bottom_controller.js +262 -138
- 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 +23 -8
- package/dist/controllers/submit_once_controller.js +294 -205
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -1
- package/dist/controllers/switch_controller.js +33 -60
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +2 -2
- package/dist/controllers/tabs_controller.js +35 -29
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +12 -0
- package/dist/controllers/tags_input_controller.js +136 -145
- 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 +21 -9
- package/dist/controllers/theme_controller.js +130 -112
- 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 +146 -80
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +36 -11
- package/dist/controllers/toast_controller.js +186 -297
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +12 -3
- package/dist/controllers/toggle_group_controller.js +95 -136
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +68 -98
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +45 -3
- package/dist/controllers/tooltip_controller.js +158 -124
- 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 -119
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +21 -11
- package/dist/controllers/tree_view_controller.js +150 -345
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6370 -9346
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +23 -3
- package/dist/inspector/cli.js +92 -37
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +92 -56
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +8 -8
- package/dist/inspector/manifest.json +787 -100
- package/dist/positioning/index.d.ts +8 -0
- package/dist/positioning/index.js +148 -47
- package/dist/positioning/index.js.map +1 -1
- package/package.json +6 -6
- package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
- package/dist/controllers/reset_before_cache_controller.js.map +0 -1
|
@@ -1,19 +1,36 @@
|
|
|
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/logical_scroll.ts
|
|
6
25
|
function isRtl(element) {
|
|
7
26
|
return window.getComputedStyle(element).direction === "rtl";
|
|
8
27
|
}
|
|
9
28
|
|
|
10
|
-
// src/utils/arrow_step.ts
|
|
11
29
|
function isReservedArrowChord(event, allow = []) {
|
|
12
30
|
if (!event.key.startsWith("Arrow")) return false;
|
|
13
31
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
14
32
|
}
|
|
15
33
|
|
|
16
|
-
// src/utils/field_mirror.ts
|
|
17
34
|
function writeField(field, value) {
|
|
18
35
|
if (field.value === value) return false;
|
|
19
36
|
field.value = value;
|
|
@@ -23,7 +40,6 @@ function commitField(target) {
|
|
|
23
40
|
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
24
41
|
}
|
|
25
42
|
|
|
26
|
-
// src/utils/focus_candidate.ts
|
|
27
43
|
function inheritsFieldsetDisabled(control) {
|
|
28
44
|
let fieldset = control.closest("fieldset[disabled]");
|
|
29
45
|
while (fieldset) {
|
|
@@ -33,15 +49,25 @@ function inheritsFieldsetDisabled(control) {
|
|
|
33
49
|
}
|
|
34
50
|
return false;
|
|
35
51
|
}
|
|
52
|
+
function unslotted(element) {
|
|
53
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
54
|
+
}
|
|
55
|
+
function flatTreeParent(element) {
|
|
56
|
+
if (unslotted(element)) return null;
|
|
57
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
58
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
59
|
+
return parent instanceof Element ? parent : null;
|
|
60
|
+
}
|
|
36
61
|
function canTakeFocus(element) {
|
|
37
|
-
|
|
62
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
63
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
64
|
+
}
|
|
38
65
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
39
66
|
if (!("disabled" in element)) return true;
|
|
40
67
|
if (element.disabled) return false;
|
|
41
68
|
return !inheritsFieldsetDisabled(element);
|
|
42
69
|
}
|
|
43
70
|
|
|
44
|
-
// src/utils/interactive_host.ts
|
|
45
71
|
var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
|
|
46
72
|
function isInteractiveHost(element) {
|
|
47
73
|
if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
|
|
@@ -58,27 +84,33 @@ function isInteractiveHost(element) {
|
|
|
58
84
|
return false;
|
|
59
85
|
}
|
|
60
86
|
|
|
61
|
-
|
|
87
|
+
var ListenerSet = class {
|
|
88
|
+
#abort = new AbortController();
|
|
89
|
+
add(target, type, handler, options) {
|
|
90
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
91
|
+
}
|
|
92
|
+
dispose() {
|
|
93
|
+
this.#abort.abort();
|
|
94
|
+
this.#abort = new AbortController();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
62
98
|
var MicrotaskCoalescer = class {
|
|
63
99
|
#run;
|
|
64
100
|
#queued = false;
|
|
65
101
|
#active = false;
|
|
66
102
|
#generation = 0;
|
|
67
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
68
103
|
constructor(run) {
|
|
69
104
|
this.#run = run;
|
|
70
105
|
}
|
|
71
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
72
106
|
activate() {
|
|
73
107
|
this.#active = true;
|
|
74
108
|
}
|
|
75
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
76
109
|
cancel() {
|
|
77
110
|
this.#active = false;
|
|
78
111
|
this.#queued = false;
|
|
79
112
|
this.#generation += 1;
|
|
80
113
|
}
|
|
81
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
82
114
|
schedule() {
|
|
83
115
|
if (!this.#active || this.#queued) return;
|
|
84
116
|
this.#queued = true;
|
|
@@ -91,31 +123,50 @@ var MicrotaskCoalescer = class {
|
|
|
91
123
|
}
|
|
92
124
|
};
|
|
93
125
|
|
|
94
|
-
|
|
126
|
+
var MorphRenderWatcher = class {
|
|
127
|
+
#listeners = new ListenerSet();
|
|
128
|
+
#pass;
|
|
129
|
+
#element = null;
|
|
130
|
+
#onMorph = () => this.#pass.schedule();
|
|
131
|
+
constructor(run) {
|
|
132
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
133
|
+
}
|
|
134
|
+
observe(element) {
|
|
135
|
+
if (this.#element === element) return;
|
|
136
|
+
this.disconnect();
|
|
137
|
+
this.#element = element;
|
|
138
|
+
this.#pass.activate();
|
|
139
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
140
|
+
}
|
|
141
|
+
schedule() {
|
|
142
|
+
this.#pass.schedule();
|
|
143
|
+
}
|
|
144
|
+
disconnect() {
|
|
145
|
+
this.#listeners.dispose();
|
|
146
|
+
this.#pass.cancel();
|
|
147
|
+
this.#element = null;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
var MoveCounter = class {
|
|
152
|
+
#sequence = 0;
|
|
153
|
+
record() {
|
|
154
|
+
this.#sequence += 1;
|
|
155
|
+
return this.#sequence;
|
|
156
|
+
}
|
|
157
|
+
isLatest(token) {
|
|
158
|
+
return token === this.#sequence;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
95
162
|
var RovingTabindex = class {
|
|
96
|
-
/** Returns the current ordered item elements; called on every operation. */
|
|
97
163
|
#getItems;
|
|
98
|
-
/**
|
|
99
|
-
* @param getItems - Returns the current ordered item elements. Called on every
|
|
100
|
-
* operation so the live target list is always used.
|
|
101
|
-
*/
|
|
102
164
|
constructor(getItems) {
|
|
103
165
|
this.#getItems = getItems;
|
|
104
166
|
}
|
|
105
|
-
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
106
167
|
get activeIndex() {
|
|
107
168
|
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
108
169
|
}
|
|
109
|
-
/**
|
|
110
|
-
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
111
|
-
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
112
|
-
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
113
|
-
* "nothing is currently tabbable".
|
|
114
|
-
*
|
|
115
|
-
* @param index - Position of the item to make tabbable.
|
|
116
|
-
* @param options - Pass `{ focus: true }` to also move DOM focus to that item,
|
|
117
|
-
* and `items` to reuse an event-scoped collection snapshot.
|
|
118
|
-
*/
|
|
119
170
|
setActive(index, options = {}) {
|
|
120
171
|
const { focus = false } = options;
|
|
121
172
|
const items = options.items ?? this.#getItems();
|
|
@@ -126,43 +177,28 @@ var RovingTabindex = class {
|
|
|
126
177
|
}
|
|
127
178
|
};
|
|
128
179
|
|
|
129
|
-
|
|
180
|
+
function targetSelector(identifier, name) {
|
|
181
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
182
|
+
}
|
|
183
|
+
|
|
130
184
|
var TimerRegistry = class {
|
|
131
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
132
185
|
ids = /* @__PURE__ */ new Set();
|
|
133
|
-
/**
|
|
134
|
-
* Cancels a single tracked timer.
|
|
135
|
-
*
|
|
136
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
137
|
-
* registry), so callers can clear defensively without guarding.
|
|
138
|
-
*/
|
|
139
186
|
clear(id) {
|
|
140
187
|
if (this.ids.delete(id)) {
|
|
141
188
|
this.cancel(id);
|
|
142
189
|
}
|
|
143
190
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
146
|
-
* to guarantee no timer outlives the element.
|
|
147
|
-
*/
|
|
148
191
|
clearAll() {
|
|
149
192
|
for (const id of this.ids) {
|
|
150
193
|
this.cancel(id);
|
|
151
194
|
}
|
|
152
195
|
this.ids.clear();
|
|
153
196
|
}
|
|
154
|
-
/** Number of timers currently tracked (pending). */
|
|
155
197
|
get size() {
|
|
156
198
|
return this.ids.size;
|
|
157
199
|
}
|
|
158
200
|
};
|
|
159
201
|
var SafeTimeout = class extends TimerRegistry {
|
|
160
|
-
/**
|
|
161
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
162
|
-
*
|
|
163
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
164
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
165
|
-
*/
|
|
166
202
|
set(callback, delay) {
|
|
167
203
|
const id = this.schedule(() => {
|
|
168
204
|
this.ids.delete(id);
|
|
@@ -179,29 +215,18 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
179
215
|
}
|
|
180
216
|
};
|
|
181
217
|
|
|
182
|
-
// src/utils/typeahead.ts
|
|
183
218
|
var TYPEAHEAD_RESET_MS = 500;
|
|
184
219
|
var Typeahead = class {
|
|
185
|
-
/** Timer registry for the pending idle reset; private so `reset()` is the only exit. */
|
|
186
220
|
#timers = new SafeTimeout();
|
|
187
|
-
/** Idle window before the query resets, in milliseconds. */
|
|
188
221
|
#resetMs;
|
|
189
|
-
/** The accumulated lowercase query, empty when idle. */
|
|
190
222
|
#query = "";
|
|
191
|
-
/** Id of the pending reset timer, `0` when none is scheduled. */
|
|
192
223
|
#timerId = 0;
|
|
193
|
-
/** @param options - Overrides for the idle window. */
|
|
194
224
|
constructor({ resetMs = TYPEAHEAD_RESET_MS } = {}) {
|
|
195
225
|
this.#resetMs = resetMs;
|
|
196
226
|
}
|
|
197
|
-
/** The query a search would currently run with; empty while idle. */
|
|
198
227
|
get query() {
|
|
199
228
|
return this.#query;
|
|
200
229
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Folds `key` into the query, restarts the idle window, and returns the query to
|
|
203
|
-
* search with. A repeated character collapses the query to that one character.
|
|
204
|
-
*/
|
|
205
230
|
push(key) {
|
|
206
231
|
const char = key.toLowerCase();
|
|
207
232
|
const repeated = this.#query.length > 0 && [...this.#query].every((c) => c === char);
|
|
@@ -210,7 +235,6 @@ var Typeahead = class {
|
|
|
210
235
|
this.#timerId = this.#timers.set(() => this.reset(), this.#resetMs);
|
|
211
236
|
return this.#query;
|
|
212
237
|
}
|
|
213
|
-
/** Clears the query and cancels the pending idle reset. */
|
|
214
238
|
reset() {
|
|
215
239
|
this.#query = "";
|
|
216
240
|
this.#timers.clear(this.#timerId);
|
|
@@ -237,62 +261,28 @@ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel
|
|
|
237
261
|
return -1;
|
|
238
262
|
}
|
|
239
263
|
|
|
240
|
-
// src/controllers/tree_view_controller.ts
|
|
241
264
|
var OBSERVED_ATTRIBUTES = ["aria-selected", "data-value"];
|
|
242
265
|
function setAttributeIfChanged(element, name, value) {
|
|
243
266
|
if (element.getAttribute(name) !== value) element.setAttribute(name, value);
|
|
244
267
|
}
|
|
245
268
|
var NESTED_INTERACTIVE = INTERACTIVE_HOST_SELECTOR;
|
|
246
269
|
var TreeViewController = class extends Controller {
|
|
270
|
+
#moves = new MoveCounter();
|
|
247
271
|
static targets = ["item", "group", "field"];
|
|
248
|
-
static actions = ["
|
|
272
|
+
static actions = ["onKeydown", "select", "toggle"];
|
|
249
273
|
static events = ["select", "toggle", "reconcile"];
|
|
250
274
|
#roving = new RovingTabindex(() => this.itemTargets);
|
|
251
275
|
#typeahead = new Typeahead();
|
|
252
|
-
#
|
|
253
|
-
/**
|
|
254
|
-
* Collapses the item and field callbacks and observed item writes of one DOM
|
|
255
|
-
* mutation into a single selection pass; refused before `connect()` and after
|
|
256
|
-
* `disconnect()`.
|
|
257
|
-
*/
|
|
258
|
-
#reconcile = new MicrotaskCoalescer(() => this.#reconcileSelection());
|
|
259
|
-
/** Watches the item attributes a page can rewrite in place; set while connected. */
|
|
276
|
+
#reconcile = new MorphRenderWatcher(() => this.#reconcileSelection());
|
|
260
277
|
#observer = null;
|
|
261
|
-
/** The selection last settled: on connect, by the user, or by a reported pass. */
|
|
262
278
|
#settled = { item: null, value: "" };
|
|
263
279
|
#connected = false;
|
|
264
|
-
/**
|
|
265
|
-
* Item targets in DOM order as of the last connect / target change. A removed
|
|
266
|
-
* item is gone from `itemTargets` by the time Stimulus reports it, so this is
|
|
267
|
-
* the only record of where in the order the gap opened.
|
|
268
|
-
*/
|
|
269
280
|
#order = [];
|
|
270
|
-
/** The `treeitem` that last took DOM focus inside this tree, if any. */
|
|
271
281
|
#focused = null;
|
|
272
|
-
/**
|
|
273
|
-
* Records which row owns DOM focus. `focusin` bubbles, so one listener covers
|
|
274
|
-
* rows and anything nested in them; the tree needs this because a removal is
|
|
275
|
-
* only reported *after* the fact, when the browser has already reset
|
|
276
|
-
* `document.activeElement`.
|
|
277
|
-
*/
|
|
278
282
|
#onFocusIn = (event) => {
|
|
279
283
|
const target = event.target;
|
|
280
284
|
this.#focused = target?.closest('[role="treeitem"]') ?? null;
|
|
281
285
|
};
|
|
282
|
-
/**
|
|
283
|
-
* Forgets the tracked row when focus genuinely leaves the tree, so a later
|
|
284
|
-
* removal of that row cannot be mistaken for "focus died with it" and pull
|
|
285
|
-
* focus back in.
|
|
286
|
-
*
|
|
287
|
-
* A *removed* focused row must not clear the record — that is exactly the case
|
|
288
|
-
* {@link TreeViewController.itemTargetDisconnected} needs it for. The two are
|
|
289
|
-
* indistinguishable while the event is dispatching: Chromium fires `focusout`
|
|
290
|
-
* for a removed row *before* the detach lands — `isConnected` still reads
|
|
291
|
-
* `true` — with `document.activeElement` already on `<body>`, exactly like a
|
|
292
|
-
* plain `blur()`. Deciding on a microtask separates them — a row that is gone
|
|
293
|
-
* by then was removed. Focus moving between two rows needs no handling:
|
|
294
|
-
* `focusin` overwrites the record right after.
|
|
295
|
-
*/
|
|
296
286
|
#onFocusOut = (event) => {
|
|
297
287
|
const target = event.target;
|
|
298
288
|
const row = target?.closest('[role="treeitem"]') ?? null;
|
|
@@ -303,12 +293,6 @@ var TreeViewController = class extends Controller {
|
|
|
303
293
|
if (this.#focused === row && row.isConnected) this.#focused = null;
|
|
304
294
|
});
|
|
305
295
|
};
|
|
306
|
-
/**
|
|
307
|
-
* Reconciles authored expansion state, establishes the single tab stop, and
|
|
308
|
-
* settles the selection without reporting it. Idempotent, so a Turbo cache
|
|
309
|
-
* restore / morph re-runs it safely (re-adding the same listener reference is a
|
|
310
|
-
* no-op per the DOM spec).
|
|
311
|
-
*/
|
|
312
296
|
connect() {
|
|
313
297
|
this.element.addEventListener("focusin", this.#onFocusIn);
|
|
314
298
|
this.element.addEventListener("focusout", this.#onFocusOut);
|
|
@@ -320,64 +304,35 @@ var TreeViewController = class extends Controller {
|
|
|
320
304
|
this.#order = [...this.itemTargets];
|
|
321
305
|
this.#settled = this.#selection();
|
|
322
306
|
this.#connected = true;
|
|
323
|
-
this.#reconcile.
|
|
307
|
+
this.#reconcile.observe(this.element);
|
|
324
308
|
this.#observeItems();
|
|
325
309
|
}
|
|
326
|
-
/** Fills a form field inserted or replaced at runtime with the current selection. */
|
|
327
310
|
fieldTargetConnected() {
|
|
328
311
|
this.#reconcile.schedule();
|
|
329
312
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
*/
|
|
313
|
+
fieldTargetDisconnected() {
|
|
314
|
+
this.#reconcile.schedule();
|
|
315
|
+
}
|
|
334
316
|
disconnect() {
|
|
335
317
|
this.element.removeEventListener("focusin", this.#onFocusIn);
|
|
336
318
|
this.element.removeEventListener("focusout", this.#onFocusOut);
|
|
337
319
|
this.#connected = false;
|
|
338
|
-
this.#reconcile.
|
|
320
|
+
this.#reconcile.disconnect();
|
|
339
321
|
this.#observer?.disconnect();
|
|
340
322
|
this.#observer = null;
|
|
341
323
|
this.#typeahead.reset();
|
|
342
|
-
this.#timers.clearAll();
|
|
343
324
|
this.#order = [];
|
|
344
325
|
this.#focused = null;
|
|
345
326
|
}
|
|
346
|
-
/**
|
|
347
|
-
* Seeds the focus record from the live document. A row can already hold focus
|
|
348
|
-
* when the controller connects (a Turbo restore, or `data-controller` added to
|
|
349
|
-
* a tree the user was already in), and the `focusin` listener alone would
|
|
350
|
-
* never learn about it.
|
|
351
|
-
*/
|
|
352
327
|
#seedFocused() {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Re-normalizes the tab stop when a `treeitem` is added at runtime, so an
|
|
358
|
-
* appended item carrying `tabindex="0"` cannot turn the tree into two Tab
|
|
359
|
-
* stops, and settles the selection once the batch is in. Skipped before
|
|
360
|
-
* `connect()`: Stimulus registers the initial targets first, and `connect()`
|
|
361
|
-
* owns the initial tab-stop policy.
|
|
362
|
-
*/
|
|
328
|
+
this.#focused = this.element.ownerDocument.activeElement?.closest('[role="treeitem"]') ?? null;
|
|
329
|
+
}
|
|
363
330
|
itemTargetConnected() {
|
|
364
331
|
if (!this.#connected) return;
|
|
365
332
|
this.#normalizeTabStop();
|
|
366
333
|
this.#trackOrder();
|
|
367
334
|
this.#reconcile.schedule();
|
|
368
335
|
}
|
|
369
|
-
/**
|
|
370
|
-
* Brings authored `aria-selected` to the shape the APG requires, without
|
|
371
|
-
* changing which item the author chose.
|
|
372
|
-
*
|
|
373
|
-
* Every item gets an explicit value — an absent `aria-selected` means "not
|
|
374
|
-
* selectable" in ARIA, so a forgotten attribute hides a selectable row — and a
|
|
375
|
-
* single-select tree keeps at most one `true`, first in DOM order: an item the
|
|
376
|
-
* page inserts ahead of the selection, already selected, takes it over.
|
|
377
|
-
* `connect()` reconciles authored expansion the same way. The scan is the
|
|
378
|
-
* `item` target set: a `role="treeitem"` without the target is outside the
|
|
379
|
-
* contract and is neither counted nor written.
|
|
380
|
-
*/
|
|
381
336
|
#normalizeSelection() {
|
|
382
337
|
const items = this.itemTargets;
|
|
383
338
|
const selected = items.find((item) => item.getAttribute("aria-selected") === "true");
|
|
@@ -385,48 +340,29 @@ var TreeViewController = class extends Controller {
|
|
|
385
340
|
setAttributeIfChanged(item, "aria-selected", item === selected ? "true" : "false");
|
|
386
341
|
}
|
|
387
342
|
}
|
|
388
|
-
/**
|
|
389
|
-
* Mirrors the selected item's `data-value` into the optional form field.
|
|
390
|
-
* Nothing selected, and an item that carries no value, both submit `""`.
|
|
391
|
-
*/
|
|
392
343
|
#mirrorField(notify) {
|
|
393
344
|
if (!this.hasFieldTarget) return;
|
|
394
345
|
const moved = writeField(this.fieldTarget, this.#selection().value);
|
|
395
346
|
if (moved && notify) commitField(this.fieldTarget);
|
|
396
347
|
}
|
|
397
|
-
/** The selected item target, if any, and the value its field submits. */
|
|
398
348
|
#selection() {
|
|
399
349
|
const item = this.itemTargets.find((candidate) => candidate.getAttribute("aria-selected") === "true") ?? null;
|
|
400
350
|
return { item, value: item?.dataset.value ?? "" };
|
|
401
351
|
}
|
|
402
|
-
/**
|
|
403
|
-
* The pass the page's changes run: the selection is normalized and mirrored,
|
|
404
|
-
* then reported if it moved. The report comes last, so items a subscriber
|
|
405
|
-
* rewrites are the next pass's to settle.
|
|
406
|
-
*/
|
|
407
352
|
#reconcileSelection() {
|
|
353
|
+
this.#reconcileExpansion();
|
|
408
354
|
this.#normalizeSelection();
|
|
355
|
+
this.#normalizeTabStop();
|
|
409
356
|
this.#dropOwnRecords();
|
|
410
357
|
this.#mirrorField(false);
|
|
411
358
|
this.#reportMove();
|
|
412
359
|
}
|
|
413
|
-
/**
|
|
414
|
-
* Reports the selection as `reconcile` when its item or value differs from the
|
|
415
|
-
* selection last settled. The settled selection is replaced before dispatching,
|
|
416
|
-
* so a move a subscriber makes is measured against what it was told.
|
|
417
|
-
*/
|
|
418
360
|
#reportMove() {
|
|
419
361
|
const selection = this.#selection();
|
|
420
362
|
if (selection.item === this.#settled.item && selection.value === this.#settled.value) return;
|
|
421
363
|
this.#settled = selection;
|
|
422
364
|
this.dispatch("reconcile", { detail: { item: selection.item } });
|
|
423
365
|
}
|
|
424
|
-
/**
|
|
425
|
-
* Watches the items' `aria-selected` and `data-value`. The tree's own writes to
|
|
426
|
-
* them are dropped from the queue as they happen, so each record the callback
|
|
427
|
-
* receives is the page's, and only a record on one of the tree's own items
|
|
428
|
-
* schedules a pass.
|
|
429
|
-
*/
|
|
430
366
|
#observeItems() {
|
|
431
367
|
const observer = new MutationObserver((records) => {
|
|
432
368
|
if (this.#concernsItems(records)) this.#reconcile.schedule();
|
|
@@ -438,45 +374,13 @@ var TreeViewController = class extends Controller {
|
|
|
438
374
|
});
|
|
439
375
|
this.#observer = observer;
|
|
440
376
|
}
|
|
441
|
-
/**
|
|
442
|
-
* Whether any record is about one of this tree's own items. A widget nested in a
|
|
443
|
-
* row writes the same attributes on its own elements, and those are no reason to
|
|
444
|
-
* reconcile the tree.
|
|
445
|
-
*/
|
|
446
377
|
#concernsItems(records) {
|
|
447
378
|
const items = new Set(this.itemTargets);
|
|
448
379
|
return records.some((record) => items.has(record.target));
|
|
449
380
|
}
|
|
450
|
-
/**
|
|
451
|
-
* Drops the records the tree's own writes just queued, so the observer never
|
|
452
|
-
* takes them for the page's. A page write queued just before goes with them and
|
|
453
|
-
* owes nothing: every such write leaves every item's `aria-selected` at the value
|
|
454
|
-
* the selection gives it, and the selection is then read back from the DOM, so it
|
|
455
|
-
* already holds what the page wrote.
|
|
456
|
-
*/
|
|
457
381
|
#dropOwnRecords() {
|
|
458
382
|
this.#observer?.takeRecords();
|
|
459
383
|
}
|
|
460
|
-
/**
|
|
461
|
-
* Restores the roving invariants when a `treeitem` leaves at runtime — a Turbo
|
|
462
|
-
* Stream `remove`, a morph, or plain DOM surgery. The mirror of
|
|
463
|
-
* {@link TreeViewController.itemTargetConnected}: without it, removing the row
|
|
464
|
-
* that held `tabindex="0"` leaves every survivor at `-1`, i.e. a tree with
|
|
465
|
-
* **no** Tab stop, and drops DOM focus to `<body>` when the removed subtree
|
|
466
|
-
* held it.
|
|
467
|
-
*
|
|
468
|
-
* The replacement is the nearest surviving *visible* item in the pre-removal
|
|
469
|
-
* order — the one after the gap, else the one before it — which is the row
|
|
470
|
-
* `ArrowDown` would have reached. Removing a non-active item changes nothing.
|
|
471
|
-
* When no visible item survives, a tree with zero Tab stops is the correct end
|
|
472
|
-
* state.
|
|
473
|
-
*
|
|
474
|
-
* Focus is only *restored*, never *stolen*: DOM focus moves solely when it was
|
|
475
|
-
* inside the removed subtree and the document has nowhere left to put it. The
|
|
476
|
-
* roving recovery runs here, while the pre-removal order is still on record;
|
|
477
|
-
* the selection is settled once the batch is in, and a selected item that left
|
|
478
|
-
* is reported as `reconcile` with no item.
|
|
479
|
-
*/
|
|
480
384
|
itemTargetDisconnected(item) {
|
|
481
385
|
if (!this.#connected || !this.element.isConnected) return;
|
|
482
386
|
const stranded = this.#focusDiedWith(item);
|
|
@@ -488,12 +392,6 @@ var TreeViewController = class extends Controller {
|
|
|
488
392
|
this.#reconcile.schedule();
|
|
489
393
|
this.#order = this.#order.filter((tracked) => tracked !== item);
|
|
490
394
|
}
|
|
491
|
-
/**
|
|
492
|
-
* Folds newly connected targets into the DOM-order snapshot, keeping the slot
|
|
493
|
-
* of any item whose disconnect callback has not arrived yet — a morph that
|
|
494
|
-
* both adds and removes rows reports one batch as several callbacks, and
|
|
495
|
-
* {@link TreeViewController.#neighborOf} needs those slots.
|
|
496
|
-
*/
|
|
497
395
|
#trackOrder() {
|
|
498
396
|
const live = this.itemTargets;
|
|
499
397
|
const pending = this.#order.filter((tracked) => !live.includes(tracked));
|
|
@@ -507,12 +405,6 @@ var TreeViewController = class extends Controller {
|
|
|
507
405
|
}
|
|
508
406
|
this.#order = merged;
|
|
509
407
|
}
|
|
510
|
-
/**
|
|
511
|
-
* Routes tree keyboard interaction. Because `treeitem`s nest, only the handler
|
|
512
|
-
* on the nearest item to the event target acts; the same keydown bubbling to an
|
|
513
|
-
* ancestor item's handler is ignored to avoid double moves / selections. Keys
|
|
514
|
-
* from a nested interactive control (or an IME composition) are left untouched.
|
|
515
|
-
*/
|
|
516
408
|
onKeydown(event) {
|
|
517
409
|
if (event.defaultPrevented) return;
|
|
518
410
|
if (isReservedArrowChord(event)) return;
|
|
@@ -528,15 +420,6 @@ var TreeViewController = class extends Controller {
|
|
|
528
420
|
event.preventDefault();
|
|
529
421
|
this.#moveBy(item, -1);
|
|
530
422
|
break;
|
|
531
|
-
// Logical, not physical: APG describes these as "to the child level" and
|
|
532
|
-
// "to the parent level" — a spatial move — so they follow the writing
|
|
533
|
-
// direction, exactly as horizontal roving does. `isRtl()` is the shared
|
|
534
|
-
// detector; never introduce a second one.
|
|
535
|
-
//
|
|
536
|
-
// Direction comes from the tree, not from `item`: the tree is what lays
|
|
537
|
-
// the rows out, and a row may carry its own `dir` (an LTR path inside an
|
|
538
|
-
// RTL browser is ordinary bidi authoring). Probing the focused row instead
|
|
539
|
-
// would let one row's arrows mean the opposite of its sibling's.
|
|
540
423
|
case "ArrowRight":
|
|
541
424
|
event.preventDefault();
|
|
542
425
|
if (isRtl(this.element)) this.#collapseOrLeave(item);
|
|
@@ -568,53 +451,43 @@ var TreeViewController = class extends Controller {
|
|
|
568
451
|
break;
|
|
569
452
|
}
|
|
570
453
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const item = this
|
|
574
|
-
if (!item) return;
|
|
454
|
+
select(source) {
|
|
455
|
+
const { event, host, origin, reason } = actionSource(source);
|
|
456
|
+
const item = host?.closest(targetSelector(this.identifier, "item"));
|
|
457
|
+
if (!item || !this.itemTargets.includes(item)) return;
|
|
458
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
459
|
+
return;
|
|
460
|
+
if (event && this.#ownerItem(event) !== item) return;
|
|
575
461
|
const group = this.#childGroup(item);
|
|
576
|
-
if (group?.contains(
|
|
577
|
-
this
|
|
578
|
-
this.#
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
this.#setExpanded(item, !this.#isExpanded(item));
|
|
596
|
-
this.#focusItem(item);
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* The item a raw event belongs to, or `null` when the tree must not act: the
|
|
600
|
-
* event came from a nested `treeitem` (it bubbled to an ancestor's handler) or
|
|
601
|
-
* from an interactive control inside the item.
|
|
602
|
-
*/
|
|
462
|
+
if (event && origin && group?.contains(origin)) return;
|
|
463
|
+
const moveFocus = event !== null || this.element.contains(document.activeElement);
|
|
464
|
+
this.#focusItem(item, moveFocus);
|
|
465
|
+
this.#select(item, reason);
|
|
466
|
+
}
|
|
467
|
+
toggle(source) {
|
|
468
|
+
const { event, origin, reason } = actionSource(source);
|
|
469
|
+
const item = origin?.closest(targetSelector(this.identifier, "item"));
|
|
470
|
+
if (!item || !this.itemTargets.includes(item)) return;
|
|
471
|
+
if (origin?.closest(`[data-controller~="${this.identifier}"]`) !== this.element) return;
|
|
472
|
+
const group = this.#childGroup(item);
|
|
473
|
+
if (!group) return;
|
|
474
|
+
const moveFocus = event !== null || this.element.contains(document.activeElement);
|
|
475
|
+
this.#setExpanded(item, group, !this.#isExpanded(item), reason);
|
|
476
|
+
this.#focusItem(
|
|
477
|
+
item,
|
|
478
|
+
moveFocus && (event !== null || this.element.contains(document.activeElement))
|
|
479
|
+
);
|
|
480
|
+
}
|
|
603
481
|
#ownerItem(event) {
|
|
604
|
-
const
|
|
605
|
-
const
|
|
482
|
+
const { host, origin: target } = actionSource(event);
|
|
483
|
+
const item = host?.closest('[role="treeitem"]');
|
|
484
|
+
if (!item || !target) return null;
|
|
606
485
|
if (target.closest('[role="treeitem"]') !== item) return null;
|
|
607
486
|
const control = target.closest(NESTED_INTERACTIVE);
|
|
608
487
|
if (control && item.contains(control)) return null;
|
|
609
|
-
if (isInteractiveHost(target)) return null;
|
|
488
|
+
if (target instanceof HTMLElement && isInteractiveHost(target)) return null;
|
|
610
489
|
return item;
|
|
611
490
|
}
|
|
612
|
-
/**
|
|
613
|
-
* The surviving visible item that inherits `removed`'s position: the next one
|
|
614
|
-
* in the pre-removal order, else the previous one. Falls back to the first
|
|
615
|
-
* visible item when `removed` is not in that order — a target that arrived and
|
|
616
|
-
* left between two callbacks — and to `undefined` when nothing visible is left.
|
|
617
|
-
*/
|
|
618
491
|
#neighborOf(removed) {
|
|
619
492
|
const visible = this.#visibleItems;
|
|
620
493
|
const index = this.#order.indexOf(removed);
|
|
@@ -628,14 +501,6 @@ var TreeViewController = class extends Controller {
|
|
|
628
501
|
}
|
|
629
502
|
return visible[0];
|
|
630
503
|
}
|
|
631
|
-
/**
|
|
632
|
-
* Whether DOM focus went down with `removed`: it sat on that row (or inside
|
|
633
|
-
* it) and the document now has nowhere to put focus. A browser falls back to
|
|
634
|
-
* `<body>`; a still-referenced but detached `activeElement` is the same
|
|
635
|
-
* condition seen from the other side. Focus that landed on a real element —
|
|
636
|
-
* inside this tree or anywhere else on the page — is left alone, so a removal
|
|
637
|
-
* can never steal it.
|
|
638
|
-
*/
|
|
639
504
|
#focusDiedWith(removed) {
|
|
640
505
|
const focused = this.#focused;
|
|
641
506
|
if (!focused || focused !== removed && !removed.contains(focused)) return false;
|
|
@@ -644,7 +509,6 @@ var TreeViewController = class extends Controller {
|
|
|
644
509
|
if (active === null || active === doc.body || active === doc.documentElement) return true;
|
|
645
510
|
return !active.isConnected;
|
|
646
511
|
}
|
|
647
|
-
/** Moves focus to the next (`delta=1`) or previous visible item, if any. */
|
|
648
512
|
#moveBy(item, delta) {
|
|
649
513
|
const visible = this.#visibleItems;
|
|
650
514
|
const current = visible.indexOf(item);
|
|
@@ -652,17 +516,11 @@ var TreeViewController = class extends Controller {
|
|
|
652
516
|
const next = visible[current + delta];
|
|
653
517
|
if (next) this.#focusItem(next);
|
|
654
518
|
}
|
|
655
|
-
/**
|
|
656
|
-
* Expands a collapsed parent, else steps into the first child.
|
|
657
|
-
*
|
|
658
|
-
* Bound to `ArrowRight` under LTR and `ArrowLeft` under RTL — the caller picks;
|
|
659
|
-
* this method only knows "toward the child level".
|
|
660
|
-
*/
|
|
661
519
|
#expandOrEnter(item) {
|
|
662
520
|
const group = this.#childGroup(item);
|
|
663
521
|
if (!group) return;
|
|
664
522
|
if (!this.#isExpanded(item)) {
|
|
665
|
-
this.#setExpanded(item, true);
|
|
523
|
+
this.#setExpanded(item, group, true);
|
|
666
524
|
return;
|
|
667
525
|
}
|
|
668
526
|
if (group.hidden) return;
|
|
@@ -671,34 +529,20 @@ var TreeViewController = class extends Controller {
|
|
|
671
529
|
).find((child) => !child.hidden);
|
|
672
530
|
if (firstChild) this.#focusItem(firstChild);
|
|
673
531
|
}
|
|
674
|
-
/** `ArrowLeft`: collapse an expanded parent, else step out to the parent item. */
|
|
675
532
|
#collapseOrLeave(item) {
|
|
676
|
-
|
|
677
|
-
|
|
533
|
+
const group = this.#childGroup(item);
|
|
534
|
+
if (group && this.#isExpanded(item)) {
|
|
535
|
+
this.#setExpanded(item, group, false);
|
|
678
536
|
return;
|
|
679
537
|
}
|
|
680
538
|
const parent = this.#parentItem(item);
|
|
681
539
|
if (parent) this.#focusItem(parent);
|
|
682
540
|
}
|
|
683
|
-
/** Focuses the first (`0`) or last (`-1`) visible item. */
|
|
684
541
|
#focusEdge(index) {
|
|
685
542
|
const visible = this.#visibleItems;
|
|
686
543
|
const target = index < 0 ? visible[visible.length - 1] : visible[index];
|
|
687
544
|
if (target) this.#focusItem(target);
|
|
688
545
|
}
|
|
689
|
-
/**
|
|
690
|
-
* Whether the item is disabled — **its own attribute or an ancestor's**.
|
|
691
|
-
*
|
|
692
|
-
* ARIA is explicit that the state carries down: "The state of being disabled
|
|
693
|
-
* applies to the current element *and all focusable descendant elements* of the
|
|
694
|
-
* element on which the `aria-disabled` attribute is applied." A tree is the only
|
|
695
|
-
* pattern here whose items nest, so it is the only place the inheritance is
|
|
696
|
-
* observable — but the rule is the shared one.
|
|
697
|
-
*
|
|
698
|
-
* The walk stops at the controller element: an `aria-disabled` on the tree root
|
|
699
|
-
* would otherwise disable every row, and a disabled *tree* is the consumer's
|
|
700
|
-
* call to make with `inert`, not something this controller infers.
|
|
701
|
-
*/
|
|
702
546
|
#isDisabled(item) {
|
|
703
547
|
let current = item;
|
|
704
548
|
while (current && current !== this.element) {
|
|
@@ -707,38 +551,30 @@ var TreeViewController = class extends Controller {
|
|
|
707
551
|
}
|
|
708
552
|
return false;
|
|
709
553
|
}
|
|
710
|
-
|
|
711
|
-
* Applies single selection and dispatches `select`. An `aria-disabled="true"`
|
|
712
|
-
* item stays focusable (APG keeps disabled nodes reachable) but is never
|
|
713
|
-
* activated, so consumers see no `select` for it.
|
|
714
|
-
*/
|
|
715
|
-
#select(item) {
|
|
554
|
+
#select(item, reason = "user") {
|
|
716
555
|
if (this.#isDisabled(item)) return;
|
|
717
556
|
for (const candidate of this.itemTargets) {
|
|
718
557
|
setAttributeIfChanged(candidate, "aria-selected", candidate === item ? "true" : "false");
|
|
719
558
|
}
|
|
720
559
|
this.#dropOwnRecords();
|
|
721
|
-
|
|
560
|
+
const selection = this.#selection();
|
|
561
|
+
const changed = selection.item !== this.#settled.item || selection.value !== this.#settled.value;
|
|
562
|
+
this.#settled = selection;
|
|
563
|
+
if (!changed) {
|
|
564
|
+
this.#mirrorField(false);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const token = this.#moves.record();
|
|
722
568
|
this.#mirrorField(true);
|
|
723
|
-
this.
|
|
569
|
+
if (!this.#moves.isLatest(token)) return;
|
|
570
|
+
this.dispatch("select", { detail: { item, reason } });
|
|
724
571
|
}
|
|
725
|
-
|
|
726
|
-
#setExpanded(item, expanded) {
|
|
727
|
-
const group = this.#childGroup(item);
|
|
728
|
-
if (!group) return;
|
|
572
|
+
#setExpanded(item, group, expanded, reason = "user") {
|
|
729
573
|
item.setAttribute("aria-expanded", String(expanded));
|
|
730
574
|
group.hidden = !expanded;
|
|
731
575
|
if (!expanded) this.#escapeCollapsedSubtree(item, group);
|
|
732
|
-
this.dispatch("toggle", { detail: { item, expanded } });
|
|
733
|
-
}
|
|
734
|
-
/**
|
|
735
|
-
* Reconciles focus and the roving tab stop before a synchronous `toggle` event
|
|
736
|
-
* observes a newly collapsed group. If DOM focus remains inside the group,
|
|
737
|
-
* both move to the parent. Otherwise only a stranded tab stop moves, preserving
|
|
738
|
-
* DOM focus. A native chevron click takes this path when mousedown has already
|
|
739
|
-
* focused the button. {@link TreeViewController.toggle} completes its focus
|
|
740
|
-
* hand-off after this reconciliation and event dispatch.
|
|
741
|
-
*/
|
|
576
|
+
this.dispatch("toggle", { detail: { item, expanded, reason } });
|
|
577
|
+
}
|
|
742
578
|
#escapeCollapsedSubtree(item, group) {
|
|
743
579
|
const active = document.activeElement;
|
|
744
580
|
if (active instanceof HTMLElement && group.contains(active)) {
|
|
@@ -746,23 +582,21 @@ var TreeViewController = class extends Controller {
|
|
|
746
582
|
return;
|
|
747
583
|
}
|
|
748
584
|
const activeIndex = this.#roving.activeIndex;
|
|
749
|
-
const tabbable =
|
|
585
|
+
const tabbable = this.itemTargets[activeIndex];
|
|
750
586
|
if (!tabbable || !group.contains(tabbable)) return;
|
|
751
587
|
const index = this.itemTargets.indexOf(item);
|
|
752
588
|
if (index !== -1) this.#roving.setActive(index);
|
|
753
589
|
}
|
|
754
|
-
|
|
755
|
-
#focusItem(item) {
|
|
590
|
+
#focusItem(item, focus = true) {
|
|
756
591
|
const index = this.itemTargets.indexOf(item);
|
|
757
592
|
if (index !== -1) {
|
|
758
|
-
this.#roving.setActive(index, { focus
|
|
593
|
+
this.#roving.setActive(index, { focus });
|
|
759
594
|
return;
|
|
760
595
|
}
|
|
761
596
|
if (!canTakeFocus(item)) return;
|
|
762
597
|
item.tabIndex = -1;
|
|
763
598
|
item.focus();
|
|
764
599
|
}
|
|
765
|
-
/** Advances the typeahead query and focuses the next matching visible item. */
|
|
766
600
|
#typeaheadTo(item, char) {
|
|
767
601
|
const query = this.#typeahead.push(char);
|
|
768
602
|
const visible = this.#visibleItems;
|
|
@@ -775,13 +609,6 @@ var TreeViewController = class extends Controller {
|
|
|
775
609
|
const match = visible[index];
|
|
776
610
|
if (match) this.#focusItem(match);
|
|
777
611
|
}
|
|
778
|
-
/**
|
|
779
|
-
* Aligns each parent's `aria-expanded` with its child `group`'s `hidden`.
|
|
780
|
-
* `aria-expanded` wins when authored (it is what assistive tech reads); a
|
|
781
|
-
* parent without it derives one from `hidden` (establish-only-when-absent, so
|
|
782
|
-
* a restored snapshot is never clobbered). Disagreeing markup would otherwise
|
|
783
|
-
* strand the only tab stop on an invisible item.
|
|
784
|
-
*/
|
|
785
612
|
#reconcileExpansion() {
|
|
786
613
|
for (const item of this.itemTargets) {
|
|
787
614
|
const group = this.#childGroup(item);
|
|
@@ -793,32 +620,19 @@ var TreeViewController = class extends Controller {
|
|
|
793
620
|
}
|
|
794
621
|
}
|
|
795
622
|
}
|
|
796
|
-
/**
|
|
797
|
-
* Leaves exactly one item tabbable. An existing, still-visible tab stop wins
|
|
798
|
-
* (the DOM is the source of truth after a Turbo restore, so the user's roving
|
|
799
|
-
* position survives); otherwise the selected item takes it (APG: a single-select
|
|
800
|
-
* tree puts initial focus on its selected node), else the first visible item.
|
|
801
|
-
*/
|
|
802
623
|
#normalizeTabStop() {
|
|
803
624
|
const items = this.itemTargets;
|
|
804
625
|
const visible = this.#visibleItems;
|
|
805
626
|
const activeIndex = this.#roving.activeIndex;
|
|
806
|
-
const active =
|
|
627
|
+
const active = items[activeIndex];
|
|
807
628
|
if (active && visible.includes(active)) {
|
|
808
629
|
this.#roving.setActive(activeIndex);
|
|
809
630
|
return;
|
|
810
631
|
}
|
|
811
632
|
const selected = visible.find((item) => item.getAttribute("aria-selected") === "true");
|
|
812
633
|
const next = selected ?? visible[0];
|
|
813
|
-
this.#roving.setActive(
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* The visible items: those with no `hidden` ancestor up to the tree, and not
|
|
817
|
-
* `hidden` themselves. A hidden row is out of the move set — it can hold
|
|
818
|
-
* neither DOM focus nor the only Tab stop — and neither can anything nested
|
|
819
|
-
* under one, which is why the walk tests every ancestor rather than only the
|
|
820
|
-
* collapsed `role="group"` containers.
|
|
821
|
-
*/
|
|
634
|
+
this.#roving.setActive(items.findIndex((item) => item === next));
|
|
635
|
+
}
|
|
822
636
|
get #visibleItems() {
|
|
823
637
|
return this.itemTargets.filter((item) => {
|
|
824
638
|
let node = item;
|
|
@@ -829,23 +643,15 @@ var TreeViewController = class extends Controller {
|
|
|
829
643
|
return true;
|
|
830
644
|
});
|
|
831
645
|
}
|
|
832
|
-
/** The child `group` owned directly by `item`, or `null` for a leaf. */
|
|
833
646
|
#childGroup(item) {
|
|
834
647
|
return item.querySelector(':scope > [role="group"]');
|
|
835
648
|
}
|
|
836
|
-
/** The nearest ancestor `treeitem`, or `null` at the root level. */
|
|
837
649
|
#parentItem(item) {
|
|
838
650
|
return item.parentElement?.closest('[role="treeitem"]') ?? null;
|
|
839
651
|
}
|
|
840
|
-
/** Whether a parent item is currently expanded. */
|
|
841
652
|
#isExpanded(item) {
|
|
842
653
|
return item.getAttribute("aria-expanded") === "true";
|
|
843
654
|
}
|
|
844
|
-
/**
|
|
845
|
-
* `item`'s name for typeahead. The fallback is the item's *own* text: a parent
|
|
846
|
-
* row's nested child group is rendered inside it, and folding a whole subtree
|
|
847
|
-
* into one name would let "readme" match the folder containing it.
|
|
848
|
-
*/
|
|
849
655
|
#label(item) {
|
|
850
656
|
return typeaheadLabel(item, () => {
|
|
851
657
|
let text = "";
|
|
@@ -861,5 +667,4 @@ var TreeViewController = class extends Controller {
|
|
|
861
667
|
};
|
|
862
668
|
|
|
863
669
|
export { TreeViewController };
|
|
864
|
-
//# sourceMappingURL=tree_view_controller.js.map
|
|
865
670
|
//# sourceMappingURL=tree_view_controller.js.map
|