stimeo-ui 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +136 -0
- package/README.md +27 -11
- package/dist/cable/index.d.ts +88 -31
- package/dist/cable/index.js +384 -449
- package/dist/cable/index.js.map +1 -1
- package/dist/coerce-JtZVNG5h.d.ts +20 -0
- package/dist/controllers/accordion_controller.d.ts +4 -2
- package/dist/controllers/accordion_controller.js +174 -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 +903 -235
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +37 -5
- package/dist/controllers/announcer_controller.js +238 -205
- 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 +414 -145
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +185 -107
- 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 +21 -3
- package/dist/controllers/bulk_select_controller.js +282 -58
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +38 -16
- package/dist/controllers/calendar_controller.js +228 -260
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +43 -12
- package/dist/controllers/carousel_controller.js +536 -436
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +44 -7
- package/dist/controllers/character_counter_controller.js +493 -207
- 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 +38 -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 +261 -149
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +17 -5
- package/dist/controllers/collapsible_controller.js +304 -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 +178 -146
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +24 -6
- package/dist/controllers/combobox_controller.js +522 -156
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +51 -20
- package/dist/controllers/command_palette_controller.js +1156 -395
- 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 +300 -171
- 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 +980 -262
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +80 -18
- package/dist/controllers/context_menu_controller.js +346 -153
- 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 +47 -3
- package/dist/controllers/countdown_controller.js +281 -158
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +93 -10
- package/dist/controllers/currency_input_controller.js +575 -225
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +25 -12
- package/dist/controllers/data_grid_controller.js +248 -214
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +42 -14
- package/dist/controllers/date_range_picker_controller.js +338 -235
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +17 -6
- package/dist/controllers/dialog_controller.js +899 -237
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +25 -8
- package/dist/controllers/direct_upload_controller.js +97 -167
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +6 -1
- package/dist/controllers/dirty_form_controller.js +72 -69
- 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 +196 -24
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +39 -11
- package/dist/controllers/drawer_controller.js +974 -355
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +31 -1
- package/dist/controllers/dropdown_controller.js +339 -96
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +315 -88
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +79 -58
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +63 -18
- package/dist/controllers/file_dropzone_controller.js +322 -211
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +77 -42
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +35 -12
- package/dist/controllers/flash_controller.js +238 -347
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +15 -6
- package/dist/controllers/focus_controller.js +679 -240
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +156 -132
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.d.ts +3 -3
- package/dist/controllers/form_validation_controller.js +31 -97
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +47 -14
- package/dist/controllers/frame_loading_controller.js +275 -217
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +27 -6
- 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 +60 -4
- package/dist/controllers/hover_card_controller.js +327 -130
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +48 -6
- package/dist/controllers/idle_controller.js +271 -101
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +30 -13
- package/dist/controllers/input_mask_controller.js +483 -97
- 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 +150 -106
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +19 -2
- package/dist/controllers/lazy_frame_controller.js +24 -68
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +44 -9
- package/dist/controllers/listbox_controller.js +523 -240
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +9 -7
- package/dist/controllers/local_time_controller.js +102 -77
- 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 +80 -96
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +55 -11
- package/dist/controllers/menu_controller.js +302 -188
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +51 -9
- package/dist/controllers/menubar_controller.js +232 -345
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +33 -20
- package/dist/controllers/meter_controller.js +417 -87
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +48 -18
- package/dist/controllers/multi_select_controller.js +785 -393
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +27 -6
- package/dist/controllers/navigation_menu_controller.js +190 -250
- 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 +396 -192
- 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 +268 -62
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +50 -8
- package/dist/controllers/number_input_controller.js +610 -303
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.d.ts +8 -5
- package/dist/controllers/optimistic_controller.js +45 -87
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +55 -8
- package/dist/controllers/otp_controller.js +658 -255
- 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 +122 -113
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +58 -20
- package/dist/controllers/overflow_menu_controller.js +470 -295
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +37 -7
- package/dist/controllers/pagination_controller.js +332 -123
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +46 -8
- package/dist/controllers/password_reveal_controller.js +392 -121
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +15 -6
- package/dist/controllers/password_strength_controller.js +111 -158
- 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 +165 -111
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +35 -12
- package/dist/controllers/pointer_drag_controller.js +434 -188
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +41 -5
- package/dist/controllers/popover_controller.js +269 -113
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +215 -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 +243 -116
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +20 -19
- package/dist/controllers/progress_controller.js +391 -78
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +21 -9
- package/dist/controllers/radio_group_controller.js +278 -211
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +47 -6
- package/dist/controllers/range_slider_controller.js +220 -113
- 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 +458 -211
- 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 +138 -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 +166 -137
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +20 -6
- package/dist/controllers/relative_time_controller.js +211 -112
- 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} +103 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +31 -2
- package/dist/controllers/resizable_controller.js +299 -150
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +3 -3
- package/dist/controllers/roving_controller.js +49 -88
- 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 +490 -246
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +3 -3
- package/dist/controllers/scroll_restore_controller.js +14 -55
- 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 +432 -149
- 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 +193 -248
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +20 -5
- package/dist/controllers/separator_controller.js +285 -142
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +65 -8
- package/dist/controllers/sidebar_controller.js +1069 -404
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +259 -101
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +29 -2
- package/dist/controllers/slider_controller.js +187 -86
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +22 -4
- package/dist/controllers/smart_sticky_header_controller.js +173 -66
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +4 -4
- package/dist/controllers/sortable_controller.js +26 -125
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +288 -173
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +9 -4
- package/dist/controllers/step_indicator_controller.js +105 -46
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +14 -6
- package/dist/controllers/stepper_controller.js +120 -73
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +9 -1
- package/dist/controllers/stick_to_bottom_controller.js +275 -140
- 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 +220 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +24 -8
- package/dist/controllers/submit_once_controller.js +376 -227
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +18 -4
- package/dist/controllers/switch_controller.js +201 -84
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +9 -6
- package/dist/controllers/tabs_controller.js +41 -30
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +16 -3
- package/dist/controllers/tags_input_controller.js +293 -173
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +11 -1
- package/dist/controllers/textarea_autosize_controller.js +108 -74
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +24 -11
- package/dist/controllers/theme_controller.js +140 -116
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +55 -5
- package/dist/controllers/time_picker_controller.js +331 -127
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +40 -16
- package/dist/controllers/toast_controller.js +244 -324
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +14 -6
- package/dist/controllers/toggle_group_controller.js +252 -191
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +8 -6
- package/dist/controllers/toolbar_controller.js +219 -120
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +49 -4
- 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 +22 -12
- package/dist/controllers/tree_view_controller.js +283 -369
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +8573 -10410
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +38 -14
- package/dist/inspector/cli.js +108 -40
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +145 -64
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +20 -20
- package/dist/inspector/manifest.json +802 -101
- package/dist/positioning/index.d.ts +15 -7
- package/dist/positioning/index.js +154 -47
- package/dist/positioning/index.js.map +1 -1
- package/package.json +11 -10
- 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,137 @@
|
|
|
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
|
}
|
|
33
|
+
function hasModifierChord(event) {
|
|
34
|
+
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setAttributeIfChanged(element, name, value, beforeWrite) {
|
|
38
|
+
if (element.getAttribute(name) === value) return false;
|
|
39
|
+
beforeWrite?.();
|
|
40
|
+
if (value === null) element.removeAttribute(name);
|
|
41
|
+
else element.setAttribute(name, value);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var AuthoredMutationWatcher = class {
|
|
46
|
+
#onAuthored;
|
|
47
|
+
#claims = null;
|
|
48
|
+
#observer = null;
|
|
49
|
+
constructor(onAuthored) {
|
|
50
|
+
this.#onAuthored = onAuthored;
|
|
51
|
+
}
|
|
52
|
+
observe(root, options) {
|
|
53
|
+
this.disconnect();
|
|
54
|
+
const observer = new MutationObserver((records) => {
|
|
55
|
+
if (this.#observer !== observer) return;
|
|
56
|
+
const authored = this.#filter(records);
|
|
57
|
+
if (authored.length > 0) this.#onAuthored(authored);
|
|
58
|
+
});
|
|
59
|
+
this.#observer = observer;
|
|
60
|
+
this.#claims = { attributes: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map() };
|
|
61
|
+
observer.observe(root, {
|
|
62
|
+
attributes: true,
|
|
63
|
+
attributeFilter: [...options.attributes],
|
|
64
|
+
subtree: options.subtree ?? false,
|
|
65
|
+
childList: options.childList ?? false
|
|
66
|
+
});
|
|
67
|
+
if (options.ancestorAttributes) {
|
|
68
|
+
let ancestor = root.parentElement;
|
|
69
|
+
while (ancestor) {
|
|
70
|
+
observer.observe(ancestor, {
|
|
71
|
+
attributes: true,
|
|
72
|
+
attributeFilter: [...options.ancestorAttributes]
|
|
73
|
+
});
|
|
74
|
+
ancestor = ancestor.parentElement;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
claim(target, name, value) {
|
|
79
|
+
const claims = this.#claims?.attributes;
|
|
80
|
+
let attributes = claims?.get(target);
|
|
81
|
+
if (!attributes) {
|
|
82
|
+
attributes = /* @__PURE__ */ new Map();
|
|
83
|
+
claims?.set(target, attributes);
|
|
84
|
+
}
|
|
85
|
+
const pending = attributes.get(name);
|
|
86
|
+
attributes.set(name, { value, count: (pending?.count ?? 0) + 1 });
|
|
87
|
+
}
|
|
88
|
+
claimCurrent(target, name) {
|
|
89
|
+
this.claim(target, name, target.getAttribute(name));
|
|
90
|
+
}
|
|
91
|
+
claimChildList(target, added, removed) {
|
|
92
|
+
const claims = this.#claims?.children;
|
|
93
|
+
const pending = claims?.get(target) ?? { added: /* @__PURE__ */ new Map(), removed: /* @__PURE__ */ new Map() };
|
|
94
|
+
for (const node of added) pending.added.set(node, (pending.added.get(node) ?? 0) + 1);
|
|
95
|
+
for (const node of removed) pending.removed.set(node, (pending.removed.get(node) ?? 0) + 1);
|
|
96
|
+
claims?.set(target, pending);
|
|
97
|
+
}
|
|
98
|
+
take() {
|
|
99
|
+
return this.#filter(this.#observer?.takeRecords() ?? []);
|
|
100
|
+
}
|
|
101
|
+
disconnect() {
|
|
102
|
+
this.#observer?.disconnect();
|
|
103
|
+
this.#observer = null;
|
|
104
|
+
this.#claims = null;
|
|
105
|
+
}
|
|
106
|
+
#filter(records) {
|
|
107
|
+
const authored = records.filter((record) => !this.#consume(record));
|
|
108
|
+
this.#claims?.attributes.clear();
|
|
109
|
+
this.#claims?.children.clear();
|
|
110
|
+
return authored;
|
|
111
|
+
}
|
|
112
|
+
#consume(record) {
|
|
113
|
+
if (record.type === "attributes") {
|
|
114
|
+
const target = record.target;
|
|
115
|
+
const pending2 = this.#claims?.attributes.get(target)?.get(record.attributeName);
|
|
116
|
+
if (!pending2 || pending2.count === 0 || target.getAttribute(record.attributeName) !== pending2.value) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
pending2.count -= 1;
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
const pending = this.#claims?.children.get(record.target);
|
|
123
|
+
if (!pending) return false;
|
|
124
|
+
const added = [...record.addedNodes];
|
|
125
|
+
const removed = [...record.removedNodes];
|
|
126
|
+
if (added.some((node) => !pending.added.get(node)) || removed.some((node) => !pending.removed.get(node))) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
for (const node of added) pending.added.set(node, (pending.added.get(node) ?? 0) - 1);
|
|
130
|
+
for (const node of removed) pending.removed.set(node, (pending.removed.get(node) ?? 0) - 1);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
15
134
|
|
|
16
|
-
// src/utils/field_mirror.ts
|
|
17
135
|
function writeField(field, value) {
|
|
18
136
|
if (field.value === value) return false;
|
|
19
137
|
field.value = value;
|
|
@@ -23,7 +141,6 @@ function commitField(target) {
|
|
|
23
141
|
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
24
142
|
}
|
|
25
143
|
|
|
26
|
-
// src/utils/focus_candidate.ts
|
|
27
144
|
function inheritsFieldsetDisabled(control) {
|
|
28
145
|
let fieldset = control.closest("fieldset[disabled]");
|
|
29
146
|
while (fieldset) {
|
|
@@ -33,15 +150,25 @@ function inheritsFieldsetDisabled(control) {
|
|
|
33
150
|
}
|
|
34
151
|
return false;
|
|
35
152
|
}
|
|
153
|
+
function unslotted(element) {
|
|
154
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
155
|
+
}
|
|
156
|
+
function flatTreeParent(element) {
|
|
157
|
+
if (unslotted(element)) return null;
|
|
158
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
159
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
160
|
+
return parent instanceof Element ? parent : null;
|
|
161
|
+
}
|
|
36
162
|
function canTakeFocus(element) {
|
|
37
|
-
|
|
163
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
164
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
165
|
+
}
|
|
38
166
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
39
167
|
if (!("disabled" in element)) return true;
|
|
40
168
|
if (element.disabled) return false;
|
|
41
169
|
return !inheritsFieldsetDisabled(element);
|
|
42
170
|
}
|
|
43
171
|
|
|
44
|
-
// src/utils/interactive_host.ts
|
|
45
172
|
var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
|
|
46
173
|
function isInteractiveHost(element) {
|
|
47
174
|
if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
|
|
@@ -58,27 +185,36 @@ function isInteractiveHost(element) {
|
|
|
58
185
|
return false;
|
|
59
186
|
}
|
|
60
187
|
|
|
61
|
-
|
|
188
|
+
var ListenerSet = class {
|
|
189
|
+
#abort = new AbortController();
|
|
190
|
+
add(target, type, handler, options) {
|
|
191
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
192
|
+
}
|
|
193
|
+
dispose() {
|
|
194
|
+
this.#abort.abort();
|
|
195
|
+
this.#abort = new AbortController();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
62
199
|
var MicrotaskCoalescer = class {
|
|
63
200
|
#run;
|
|
64
201
|
#queued = false;
|
|
65
202
|
#active = false;
|
|
66
203
|
#generation = 0;
|
|
67
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
68
204
|
constructor(run) {
|
|
69
205
|
this.#run = run;
|
|
70
206
|
}
|
|
71
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
72
207
|
activate() {
|
|
73
208
|
this.#active = true;
|
|
74
209
|
}
|
|
75
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
76
210
|
cancel() {
|
|
77
211
|
this.#active = false;
|
|
78
212
|
this.#queued = false;
|
|
79
213
|
this.#generation += 1;
|
|
80
214
|
}
|
|
81
|
-
|
|
215
|
+
get pending() {
|
|
216
|
+
return this.#queued;
|
|
217
|
+
}
|
|
82
218
|
schedule() {
|
|
83
219
|
if (!this.#active || this.#queued) return;
|
|
84
220
|
this.#queued = true;
|
|
@@ -91,31 +227,53 @@ var MicrotaskCoalescer = class {
|
|
|
91
227
|
}
|
|
92
228
|
};
|
|
93
229
|
|
|
94
|
-
|
|
230
|
+
var MorphRenderWatcher = class {
|
|
231
|
+
#listeners = new ListenerSet();
|
|
232
|
+
#pass;
|
|
233
|
+
#element = null;
|
|
234
|
+
#onMorph = () => this.#pass.schedule();
|
|
235
|
+
constructor(run) {
|
|
236
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
237
|
+
}
|
|
238
|
+
observe(element) {
|
|
239
|
+
if (this.#element === element) return;
|
|
240
|
+
this.disconnect();
|
|
241
|
+
this.#element = element;
|
|
242
|
+
this.#pass.activate();
|
|
243
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
244
|
+
}
|
|
245
|
+
schedule() {
|
|
246
|
+
this.#pass.schedule();
|
|
247
|
+
}
|
|
248
|
+
get pending() {
|
|
249
|
+
return this.#pass.pending;
|
|
250
|
+
}
|
|
251
|
+
disconnect() {
|
|
252
|
+
this.#listeners.dispose();
|
|
253
|
+
this.#pass.cancel();
|
|
254
|
+
this.#element = null;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
var MoveCounter = class {
|
|
259
|
+
#sequence = 0;
|
|
260
|
+
record() {
|
|
261
|
+
this.#sequence += 1;
|
|
262
|
+
return this.#sequence;
|
|
263
|
+
}
|
|
264
|
+
isLatest(token) {
|
|
265
|
+
return token === this.#sequence;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
95
269
|
var RovingTabindex = class {
|
|
96
|
-
/** Returns the current ordered item elements; called on every operation. */
|
|
97
270
|
#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
271
|
constructor(getItems) {
|
|
103
272
|
this.#getItems = getItems;
|
|
104
273
|
}
|
|
105
|
-
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
106
274
|
get activeIndex() {
|
|
107
275
|
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
108
276
|
}
|
|
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
277
|
setActive(index, options = {}) {
|
|
120
278
|
const { focus = false } = options;
|
|
121
279
|
const items = options.items ?? this.#getItems();
|
|
@@ -126,43 +284,28 @@ var RovingTabindex = class {
|
|
|
126
284
|
}
|
|
127
285
|
};
|
|
128
286
|
|
|
129
|
-
|
|
287
|
+
function targetSelector(identifier, name) {
|
|
288
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
289
|
+
}
|
|
290
|
+
|
|
130
291
|
var TimerRegistry = class {
|
|
131
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
132
292
|
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
293
|
clear(id) {
|
|
140
294
|
if (this.ids.delete(id)) {
|
|
141
295
|
this.cancel(id);
|
|
142
296
|
}
|
|
143
297
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
146
|
-
* to guarantee no timer outlives the element.
|
|
147
|
-
*/
|
|
148
298
|
clearAll() {
|
|
149
299
|
for (const id of this.ids) {
|
|
150
300
|
this.cancel(id);
|
|
151
301
|
}
|
|
152
302
|
this.ids.clear();
|
|
153
303
|
}
|
|
154
|
-
/** Number of timers currently tracked (pending). */
|
|
155
304
|
get size() {
|
|
156
305
|
return this.ids.size;
|
|
157
306
|
}
|
|
158
307
|
};
|
|
159
308
|
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
309
|
set(callback, delay) {
|
|
167
310
|
const id = this.schedule(() => {
|
|
168
311
|
this.ids.delete(id);
|
|
@@ -179,29 +322,18 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
179
322
|
}
|
|
180
323
|
};
|
|
181
324
|
|
|
182
|
-
// src/utils/typeahead.ts
|
|
183
325
|
var TYPEAHEAD_RESET_MS = 500;
|
|
184
326
|
var Typeahead = class {
|
|
185
|
-
/** Timer registry for the pending idle reset; private so `reset()` is the only exit. */
|
|
186
327
|
#timers = new SafeTimeout();
|
|
187
|
-
/** Idle window before the query resets, in milliseconds. */
|
|
188
328
|
#resetMs;
|
|
189
|
-
/** The accumulated lowercase query, empty when idle. */
|
|
190
329
|
#query = "";
|
|
191
|
-
/** Id of the pending reset timer, `0` when none is scheduled. */
|
|
192
330
|
#timerId = 0;
|
|
193
|
-
/** @param options - Overrides for the idle window. */
|
|
194
331
|
constructor({ resetMs = TYPEAHEAD_RESET_MS } = {}) {
|
|
195
332
|
this.#resetMs = resetMs;
|
|
196
333
|
}
|
|
197
|
-
/** The query a search would currently run with; empty while idle. */
|
|
198
334
|
get query() {
|
|
199
335
|
return this.#query;
|
|
200
336
|
}
|
|
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
337
|
push(key) {
|
|
206
338
|
const char = key.toLowerCase();
|
|
207
339
|
const repeated = this.#query.length > 0 && [...this.#query].every((c) => c === char);
|
|
@@ -210,7 +342,6 @@ var Typeahead = class {
|
|
|
210
342
|
this.#timerId = this.#timers.set(() => this.reset(), this.#resetMs);
|
|
211
343
|
return this.#query;
|
|
212
344
|
}
|
|
213
|
-
/** Clears the query and cancels the pending idle reset. */
|
|
214
345
|
reset() {
|
|
215
346
|
this.#query = "";
|
|
216
347
|
this.#timers.clear(this.#timerId);
|
|
@@ -237,62 +368,28 @@ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel
|
|
|
237
368
|
return -1;
|
|
238
369
|
}
|
|
239
370
|
|
|
240
|
-
// src/controllers/tree_view_controller.ts
|
|
241
371
|
var OBSERVED_ATTRIBUTES = ["aria-selected", "data-value"];
|
|
242
|
-
function setAttributeIfChanged(element, name, value) {
|
|
243
|
-
if (element.getAttribute(name) !== value) element.setAttribute(name, value);
|
|
244
|
-
}
|
|
245
372
|
var NESTED_INTERACTIVE = INTERACTIVE_HOST_SELECTOR;
|
|
246
373
|
var TreeViewController = class extends Controller {
|
|
374
|
+
#moves = new MoveCounter();
|
|
375
|
+
#connectionGeneration = 0;
|
|
247
376
|
static targets = ["item", "group", "field"];
|
|
248
|
-
static actions = ["
|
|
377
|
+
static actions = ["onKeydown", "select", "toggle"];
|
|
249
378
|
static events = ["select", "toggle", "reconcile"];
|
|
250
379
|
#roving = new RovingTabindex(() => this.itemTargets);
|
|
251
380
|
#typeahead = new Typeahead();
|
|
252
|
-
#
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
* `disconnect()`.
|
|
257
|
-
*/
|
|
258
|
-
#reconcile = new MicrotaskCoalescer(() => this.#reconcileSelection());
|
|
259
|
-
/** Watches the item attributes a page can rewrite in place; set while connected. */
|
|
260
|
-
#observer = null;
|
|
261
|
-
/** The selection last settled: on connect, by the user, or by a reported pass. */
|
|
381
|
+
#reconcile = new MorphRenderWatcher(() => this.#reconcileSelection());
|
|
382
|
+
#observer = new AuthoredMutationWatcher((records) => {
|
|
383
|
+
if (this.#concernsItems(records)) this.#reconcile.schedule();
|
|
384
|
+
});
|
|
262
385
|
#settled = { item: null, value: "" };
|
|
263
386
|
#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
387
|
#order = [];
|
|
270
|
-
/** The `treeitem` that last took DOM focus inside this tree, if any. */
|
|
271
388
|
#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
389
|
#onFocusIn = (event) => {
|
|
279
390
|
const target = event.target;
|
|
280
391
|
this.#focused = target?.closest('[role="treeitem"]') ?? null;
|
|
281
392
|
};
|
|
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
393
|
#onFocusOut = (event) => {
|
|
297
394
|
const target = event.target;
|
|
298
395
|
const row = target?.closest('[role="treeitem"]') ?? null;
|
|
@@ -303,15 +400,10 @@ var TreeViewController = class extends Controller {
|
|
|
303
400
|
if (this.#focused === row && row.isConnected) this.#focused = null;
|
|
304
401
|
});
|
|
305
402
|
};
|
|
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
|
-
*/
|
|
403
|
+
#listeners = new ListenerSet();
|
|
312
404
|
connect() {
|
|
313
|
-
this.element
|
|
314
|
-
this.element
|
|
405
|
+
this.#listeners.add(this.element, "focusin", this.#onFocusIn);
|
|
406
|
+
this.#listeners.add(this.element, "focusout", this.#onFocusOut);
|
|
315
407
|
this.#seedFocused();
|
|
316
408
|
this.#reconcileExpansion();
|
|
317
409
|
this.#normalizeSelection();
|
|
@@ -320,163 +412,77 @@ var TreeViewController = class extends Controller {
|
|
|
320
412
|
this.#order = [...this.itemTargets];
|
|
321
413
|
this.#settled = this.#selection();
|
|
322
414
|
this.#connected = true;
|
|
323
|
-
this.#reconcile.
|
|
415
|
+
this.#reconcile.observe(this.element);
|
|
324
416
|
this.#observeItems();
|
|
325
417
|
}
|
|
326
|
-
/** Fills a form field inserted or replaced at runtime with the current selection. */
|
|
327
418
|
fieldTargetConnected() {
|
|
328
419
|
this.#reconcile.schedule();
|
|
329
420
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
*/
|
|
421
|
+
fieldTargetDisconnected() {
|
|
422
|
+
this.#reconcile.schedule();
|
|
423
|
+
}
|
|
334
424
|
disconnect() {
|
|
335
|
-
this
|
|
336
|
-
this.
|
|
425
|
+
this.#connectionGeneration += 1;
|
|
426
|
+
this.#listeners.dispose();
|
|
337
427
|
this.#connected = false;
|
|
338
|
-
this.#reconcile.
|
|
339
|
-
this.#observer
|
|
340
|
-
this.#observer = null;
|
|
428
|
+
this.#reconcile.disconnect();
|
|
429
|
+
this.#observer.disconnect();
|
|
341
430
|
this.#typeahead.reset();
|
|
342
|
-
this.#timers.clearAll();
|
|
343
431
|
this.#order = [];
|
|
344
432
|
this.#focused = null;
|
|
345
433
|
}
|
|
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
434
|
#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
|
-
*/
|
|
435
|
+
this.#focused = this.element.ownerDocument.activeElement?.closest('[role="treeitem"]') ?? null;
|
|
436
|
+
}
|
|
363
437
|
itemTargetConnected() {
|
|
364
438
|
if (!this.#connected) return;
|
|
365
439
|
this.#normalizeTabStop();
|
|
366
440
|
this.#trackOrder();
|
|
367
441
|
this.#reconcile.schedule();
|
|
368
442
|
}
|
|
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
443
|
#normalizeSelection() {
|
|
382
444
|
const items = this.itemTargets;
|
|
383
445
|
const selected = items.find((item) => item.getAttribute("aria-selected") === "true");
|
|
384
446
|
for (const item of items) {
|
|
385
|
-
|
|
447
|
+
this.#writeSelection(item, item === selected ? "true" : "false");
|
|
386
448
|
}
|
|
387
449
|
}
|
|
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
450
|
#mirrorField(notify) {
|
|
393
451
|
if (!this.hasFieldTarget) return;
|
|
394
452
|
const moved = writeField(this.fieldTarget, this.#selection().value);
|
|
395
453
|
if (moved && notify) commitField(this.fieldTarget);
|
|
396
454
|
}
|
|
397
|
-
/** The selected item target, if any, and the value its field submits. */
|
|
398
455
|
#selection() {
|
|
399
456
|
const item = this.itemTargets.find((candidate) => candidate.getAttribute("aria-selected") === "true") ?? null;
|
|
400
457
|
return { item, value: item?.dataset.value ?? "" };
|
|
401
458
|
}
|
|
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
459
|
#reconcileSelection() {
|
|
460
|
+
this.#reconcileExpansion();
|
|
408
461
|
this.#normalizeSelection();
|
|
409
|
-
this.#
|
|
462
|
+
this.#normalizeTabStop();
|
|
463
|
+
this.#flushMutations();
|
|
410
464
|
this.#mirrorField(false);
|
|
411
465
|
this.#reportMove();
|
|
412
466
|
}
|
|
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
467
|
#reportMove() {
|
|
419
468
|
const selection = this.#selection();
|
|
420
469
|
if (selection.item === this.#settled.item && selection.value === this.#settled.value) return;
|
|
421
470
|
this.#settled = selection;
|
|
422
471
|
this.dispatch("reconcile", { detail: { item: selection.item } });
|
|
423
472
|
}
|
|
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
473
|
#observeItems() {
|
|
431
|
-
|
|
432
|
-
if (this.#concernsItems(records)) this.#reconcile.schedule();
|
|
433
|
-
});
|
|
434
|
-
observer.observe(this.element, {
|
|
474
|
+
this.#observer.observe(this.element, {
|
|
435
475
|
subtree: true,
|
|
436
|
-
attributes:
|
|
437
|
-
attributeFilter: OBSERVED_ATTRIBUTES
|
|
476
|
+
attributes: OBSERVED_ATTRIBUTES
|
|
438
477
|
});
|
|
439
|
-
this.#observer = observer;
|
|
440
478
|
}
|
|
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
479
|
#concernsItems(records) {
|
|
447
480
|
const items = new Set(this.itemTargets);
|
|
448
481
|
return records.some((record) => items.has(record.target));
|
|
449
482
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
#dropOwnRecords() {
|
|
458
|
-
this.#observer?.takeRecords();
|
|
459
|
-
}
|
|
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
|
-
*/
|
|
483
|
+
#flushMutations() {
|
|
484
|
+
if (this.#concernsItems(this.#observer.take())) this.#reconcile.schedule();
|
|
485
|
+
}
|
|
480
486
|
itemTargetDisconnected(item) {
|
|
481
487
|
if (!this.#connected || !this.element.isConnected) return;
|
|
482
488
|
const stranded = this.#focusDiedWith(item);
|
|
@@ -488,12 +494,6 @@ var TreeViewController = class extends Controller {
|
|
|
488
494
|
this.#reconcile.schedule();
|
|
489
495
|
this.#order = this.#order.filter((tracked) => tracked !== item);
|
|
490
496
|
}
|
|
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
497
|
#trackOrder() {
|
|
498
498
|
const live = this.itemTargets;
|
|
499
499
|
const pending = this.#order.filter((tracked) => !live.includes(tracked));
|
|
@@ -507,15 +507,10 @@ var TreeViewController = class extends Controller {
|
|
|
507
507
|
}
|
|
508
508
|
this.#order = merged;
|
|
509
509
|
}
|
|
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
510
|
onKeydown(event) {
|
|
517
511
|
if (event.defaultPrevented) return;
|
|
518
512
|
if (isReservedArrowChord(event)) return;
|
|
513
|
+
if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
|
|
519
514
|
if (event.isComposing) return;
|
|
520
515
|
const item = this.#ownerItem(event);
|
|
521
516
|
if (!item) return;
|
|
@@ -528,15 +523,6 @@ var TreeViewController = class extends Controller {
|
|
|
528
523
|
event.preventDefault();
|
|
529
524
|
this.#moveBy(item, -1);
|
|
530
525
|
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
526
|
case "ArrowRight":
|
|
541
527
|
event.preventDefault();
|
|
542
528
|
if (isRtl(this.element)) this.#collapseOrLeave(item);
|
|
@@ -568,53 +554,43 @@ var TreeViewController = class extends Controller {
|
|
|
568
554
|
break;
|
|
569
555
|
}
|
|
570
556
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const item = this
|
|
574
|
-
if (!item) return;
|
|
557
|
+
select(source) {
|
|
558
|
+
const { event, host, origin, reason } = actionSource(source);
|
|
559
|
+
const item = host?.closest(targetSelector(this.identifier, "item"));
|
|
560
|
+
if (!item || !this.itemTargets.includes(item)) return;
|
|
561
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
562
|
+
return;
|
|
563
|
+
if (event && this.#ownerItem(event) !== item) return;
|
|
575
564
|
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
|
-
*/
|
|
565
|
+
if (event && origin && group?.contains(origin)) return;
|
|
566
|
+
const moveFocus = event !== null || this.element.contains(document.activeElement);
|
|
567
|
+
this.#focusItem(item, moveFocus);
|
|
568
|
+
this.#select(item, reason);
|
|
569
|
+
}
|
|
570
|
+
toggle(source) {
|
|
571
|
+
const { event, origin, reason } = actionSource(source);
|
|
572
|
+
const item = origin?.closest(targetSelector(this.identifier, "item"));
|
|
573
|
+
if (!item || !this.itemTargets.includes(item)) return;
|
|
574
|
+
if (origin?.closest(`[data-controller~="${this.identifier}"]`) !== this.element) return;
|
|
575
|
+
const group = this.#childGroup(item);
|
|
576
|
+
if (!group) return;
|
|
577
|
+
const moveFocus = event !== null || this.element.contains(document.activeElement);
|
|
578
|
+
this.#setExpanded(item, group, !this.#isExpanded(item), reason);
|
|
579
|
+
this.#focusItem(
|
|
580
|
+
item,
|
|
581
|
+
moveFocus && (event !== null || this.element.contains(document.activeElement))
|
|
582
|
+
);
|
|
583
|
+
}
|
|
603
584
|
#ownerItem(event) {
|
|
604
|
-
const
|
|
605
|
-
const
|
|
585
|
+
const { host, origin: target } = actionSource(event);
|
|
586
|
+
const item = host?.closest('[role="treeitem"]');
|
|
587
|
+
if (!item || !target) return null;
|
|
606
588
|
if (target.closest('[role="treeitem"]') !== item) return null;
|
|
607
589
|
const control = target.closest(NESTED_INTERACTIVE);
|
|
608
590
|
if (control && item.contains(control)) return null;
|
|
609
|
-
if (isInteractiveHost(target)) return null;
|
|
591
|
+
if (target instanceof HTMLElement && isInteractiveHost(target)) return null;
|
|
610
592
|
return item;
|
|
611
593
|
}
|
|
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
594
|
#neighborOf(removed) {
|
|
619
595
|
const visible = this.#visibleItems;
|
|
620
596
|
const index = this.#order.indexOf(removed);
|
|
@@ -628,14 +604,6 @@ var TreeViewController = class extends Controller {
|
|
|
628
604
|
}
|
|
629
605
|
return visible[0];
|
|
630
606
|
}
|
|
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
607
|
#focusDiedWith(removed) {
|
|
640
608
|
const focused = this.#focused;
|
|
641
609
|
if (!focused || focused !== removed && !removed.contains(focused)) return false;
|
|
@@ -644,7 +612,6 @@ var TreeViewController = class extends Controller {
|
|
|
644
612
|
if (active === null || active === doc.body || active === doc.documentElement) return true;
|
|
645
613
|
return !active.isConnected;
|
|
646
614
|
}
|
|
647
|
-
/** Moves focus to the next (`delta=1`) or previous visible item, if any. */
|
|
648
615
|
#moveBy(item, delta) {
|
|
649
616
|
const visible = this.#visibleItems;
|
|
650
617
|
const current = visible.indexOf(item);
|
|
@@ -652,17 +619,11 @@ var TreeViewController = class extends Controller {
|
|
|
652
619
|
const next = visible[current + delta];
|
|
653
620
|
if (next) this.#focusItem(next);
|
|
654
621
|
}
|
|
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
622
|
#expandOrEnter(item) {
|
|
662
623
|
const group = this.#childGroup(item);
|
|
663
624
|
if (!group) return;
|
|
664
625
|
if (!this.#isExpanded(item)) {
|
|
665
|
-
this.#setExpanded(item, true);
|
|
626
|
+
this.#setExpanded(item, group, true);
|
|
666
627
|
return;
|
|
667
628
|
}
|
|
668
629
|
if (group.hidden) return;
|
|
@@ -671,34 +632,20 @@ var TreeViewController = class extends Controller {
|
|
|
671
632
|
).find((child) => !child.hidden);
|
|
672
633
|
if (firstChild) this.#focusItem(firstChild);
|
|
673
634
|
}
|
|
674
|
-
/** `ArrowLeft`: collapse an expanded parent, else step out to the parent item. */
|
|
675
635
|
#collapseOrLeave(item) {
|
|
676
|
-
|
|
677
|
-
|
|
636
|
+
const group = this.#childGroup(item);
|
|
637
|
+
if (group && this.#isExpanded(item)) {
|
|
638
|
+
this.#setExpanded(item, group, false);
|
|
678
639
|
return;
|
|
679
640
|
}
|
|
680
641
|
const parent = this.#parentItem(item);
|
|
681
642
|
if (parent) this.#focusItem(parent);
|
|
682
643
|
}
|
|
683
|
-
/** Focuses the first (`0`) or last (`-1`) visible item. */
|
|
684
644
|
#focusEdge(index) {
|
|
685
645
|
const visible = this.#visibleItems;
|
|
686
646
|
const target = index < 0 ? visible[visible.length - 1] : visible[index];
|
|
687
647
|
if (target) this.#focusItem(target);
|
|
688
648
|
}
|
|
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
649
|
#isDisabled(item) {
|
|
703
650
|
let current = item;
|
|
704
651
|
while (current && current !== this.element) {
|
|
@@ -707,38 +654,31 @@ var TreeViewController = class extends Controller {
|
|
|
707
654
|
}
|
|
708
655
|
return false;
|
|
709
656
|
}
|
|
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) {
|
|
657
|
+
#select(item, reason = "user") {
|
|
716
658
|
if (this.#isDisabled(item)) return;
|
|
659
|
+
const generation = this.#connectionGeneration;
|
|
717
660
|
for (const candidate of this.itemTargets) {
|
|
718
|
-
|
|
661
|
+
this.#writeSelection(candidate, candidate === item ? "true" : "false");
|
|
719
662
|
}
|
|
720
|
-
this.#
|
|
721
|
-
|
|
663
|
+
this.#flushMutations();
|
|
664
|
+
const selection = this.#selection();
|
|
665
|
+
const changed = selection.item !== this.#settled.item || selection.value !== this.#settled.value;
|
|
666
|
+
this.#settled = selection;
|
|
667
|
+
if (!changed) {
|
|
668
|
+
this.#mirrorField(false);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const token = this.#moves.record();
|
|
722
672
|
this.#mirrorField(true);
|
|
723
|
-
|
|
673
|
+
if (generation !== this.#connectionGeneration || !this.#moves.isLatest(token)) return;
|
|
674
|
+
this.dispatch("select", { detail: { item, reason } });
|
|
724
675
|
}
|
|
725
|
-
|
|
726
|
-
#setExpanded(item, expanded) {
|
|
727
|
-
const group = this.#childGroup(item);
|
|
728
|
-
if (!group) return;
|
|
676
|
+
#setExpanded(item, group, expanded, reason = "user") {
|
|
729
677
|
item.setAttribute("aria-expanded", String(expanded));
|
|
730
678
|
group.hidden = !expanded;
|
|
731
679
|
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
|
-
*/
|
|
680
|
+
this.dispatch("toggle", { detail: { item, expanded, reason } });
|
|
681
|
+
}
|
|
742
682
|
#escapeCollapsedSubtree(item, group) {
|
|
743
683
|
const active = document.activeElement;
|
|
744
684
|
if (active instanceof HTMLElement && group.contains(active)) {
|
|
@@ -746,23 +686,21 @@ var TreeViewController = class extends Controller {
|
|
|
746
686
|
return;
|
|
747
687
|
}
|
|
748
688
|
const activeIndex = this.#roving.activeIndex;
|
|
749
|
-
const tabbable =
|
|
689
|
+
const tabbable = this.itemTargets[activeIndex];
|
|
750
690
|
if (!tabbable || !group.contains(tabbable)) return;
|
|
751
691
|
const index = this.itemTargets.indexOf(item);
|
|
752
692
|
if (index !== -1) this.#roving.setActive(index);
|
|
753
693
|
}
|
|
754
|
-
|
|
755
|
-
#focusItem(item) {
|
|
694
|
+
#focusItem(item, focus = true) {
|
|
756
695
|
const index = this.itemTargets.indexOf(item);
|
|
757
696
|
if (index !== -1) {
|
|
758
|
-
this.#roving.setActive(index, { focus
|
|
697
|
+
this.#roving.setActive(index, { focus });
|
|
759
698
|
return;
|
|
760
699
|
}
|
|
761
700
|
if (!canTakeFocus(item)) return;
|
|
762
701
|
item.tabIndex = -1;
|
|
763
702
|
item.focus();
|
|
764
703
|
}
|
|
765
|
-
/** Advances the typeahead query and focuses the next matching visible item. */
|
|
766
704
|
#typeaheadTo(item, char) {
|
|
767
705
|
const query = this.#typeahead.push(char);
|
|
768
706
|
const visible = this.#visibleItems;
|
|
@@ -775,13 +713,6 @@ var TreeViewController = class extends Controller {
|
|
|
775
713
|
const match = visible[index];
|
|
776
714
|
if (match) this.#focusItem(match);
|
|
777
715
|
}
|
|
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
716
|
#reconcileExpansion() {
|
|
786
717
|
for (const item of this.itemTargets) {
|
|
787
718
|
const group = this.#childGroup(item);
|
|
@@ -793,32 +724,19 @@ var TreeViewController = class extends Controller {
|
|
|
793
724
|
}
|
|
794
725
|
}
|
|
795
726
|
}
|
|
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
727
|
#normalizeTabStop() {
|
|
803
728
|
const items = this.itemTargets;
|
|
804
729
|
const visible = this.#visibleItems;
|
|
805
730
|
const activeIndex = this.#roving.activeIndex;
|
|
806
|
-
const active =
|
|
731
|
+
const active = items[activeIndex];
|
|
807
732
|
if (active && visible.includes(active)) {
|
|
808
733
|
this.#roving.setActive(activeIndex);
|
|
809
734
|
return;
|
|
810
735
|
}
|
|
811
736
|
const selected = visible.find((item) => item.getAttribute("aria-selected") === "true");
|
|
812
737
|
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
|
-
*/
|
|
738
|
+
this.#roving.setActive(items.indexOf(next));
|
|
739
|
+
}
|
|
822
740
|
get #visibleItems() {
|
|
823
741
|
return this.itemTargets.filter((item) => {
|
|
824
742
|
let node = item;
|
|
@@ -829,23 +747,15 @@ var TreeViewController = class extends Controller {
|
|
|
829
747
|
return true;
|
|
830
748
|
});
|
|
831
749
|
}
|
|
832
|
-
/** The child `group` owned directly by `item`, or `null` for a leaf. */
|
|
833
750
|
#childGroup(item) {
|
|
834
751
|
return item.querySelector(':scope > [role="group"]');
|
|
835
752
|
}
|
|
836
|
-
/** The nearest ancestor `treeitem`, or `null` at the root level. */
|
|
837
753
|
#parentItem(item) {
|
|
838
754
|
return item.parentElement?.closest('[role="treeitem"]') ?? null;
|
|
839
755
|
}
|
|
840
|
-
/** Whether a parent item is currently expanded. */
|
|
841
756
|
#isExpanded(item) {
|
|
842
757
|
return item.getAttribute("aria-expanded") === "true";
|
|
843
758
|
}
|
|
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
759
|
#label(item) {
|
|
850
760
|
return typeaheadLabel(item, () => {
|
|
851
761
|
let text = "";
|
|
@@ -858,8 +768,12 @@ var TreeViewController = class extends Controller {
|
|
|
858
768
|
return text;
|
|
859
769
|
});
|
|
860
770
|
}
|
|
771
|
+
#writeSelection(target, value) {
|
|
772
|
+
setAttributeIfChanged(target, "aria-selected", value, () => {
|
|
773
|
+
this.#observer.claim(target, "aria-selected", value);
|
|
774
|
+
});
|
|
775
|
+
}
|
|
861
776
|
};
|
|
862
777
|
|
|
863
778
|
export { TreeViewController };
|
|
864
|
-
//# sourceMappingURL=tree_view_controller.js.map
|
|
865
779
|
//# sourceMappingURL=tree_view_controller.js.map
|