stimeo-ui 0.14.0 → 0.16.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 +212 -0
- package/README.md +120 -0
- package/dist/cable/index.js +123 -29
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +30 -3
- package/dist/controllers/accordion_controller.js +98 -9
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +8 -7
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +3 -2
- package/dist/controllers/announcer_controller.js +96 -62
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js +83 -7
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +1 -1
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +10 -7
- package/dist/controllers/breadcrumb_controller.js +38 -11
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +4 -4
- package/dist/controllers/bulk_select_controller.js +7 -6
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +101 -23
- package/dist/controllers/calendar_controller.js +340 -123
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +67 -22
- package/dist/controllers/carousel_controller.js +263 -38
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +1 -1
- package/dist/controllers/character_counter_controller.js +40 -2
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.js +81 -12
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +28 -2
- package/dist/controllers/clipboard_controller.js +63 -5
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +25 -1
- package/dist/controllers/collapsible_controller.js +99 -14
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +29 -8
- package/dist/controllers/color_picker_controller.js +80 -34
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +10 -1
- package/dist/controllers/combobox_controller.js +106 -16
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +3 -3
- package/dist/controllers/command_palette_controller.js +35 -3
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js +85 -17
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +3 -0
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +6 -0
- package/dist/controllers/context_menu_controller.js +32 -12
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +30 -1
- package/dist/controllers/countdown_controller.js +129 -26
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +77 -16
- package/dist/controllers/currency_input_controller.js +221 -67
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +63 -18
- package/dist/controllers/data_grid_controller.js +195 -29
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +41 -6
- package/dist/controllers/date_range_picker_controller.js +151 -30
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +10 -3
- package/dist/controllers/dialog_controller.js +35 -8
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.js +22 -4
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +2 -2
- package/dist/controllers/dirty_form_controller.js +46 -13
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js +1 -0
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +18 -10
- package/dist/controllers/drawer_controller.js +54 -19
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +9 -3
- package/dist/controllers/dropdown_controller.js +36 -9
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.js +34 -0
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js +144 -51
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +10 -4
- package/dist/controllers/filter_controller.js +20 -6
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +26 -6
- package/dist/controllers/flash_controller.js +432 -71
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.js +1 -0
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +7 -5
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +19 -13
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js +45 -8
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +82 -25
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +10 -2
- package/dist/controllers/hover_card_controller.js +40 -14
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +16 -3
- package/dist/controllers/idle_controller.js +90 -5
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +5 -2
- package/dist/controllers/input_mask_controller.js +65 -9
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +3 -0
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +11 -2
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +50 -8
- package/dist/controllers/listbox_controller.js +203 -45
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +10 -5
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +3 -3
- package/dist/controllers/masonry_controller.js +31 -15
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +9 -3
- package/dist/controllers/menu_controller.js +45 -16
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +11 -0
- package/dist/controllers/menubar_controller.js +58 -24
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +9 -5
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +18 -3
- package/dist/controllers/multi_select_controller.js +278 -104
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +11 -0
- package/dist/controllers/navigation_menu_controller.js +48 -15
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js +37 -8
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js +9 -1
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +36 -8
- package/dist/controllers/number_input_controller.js +124 -21
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +42 -5
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +22 -7
- package/dist/controllers/otp_controller.js +198 -55
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
- package/dist/controllers/overflow_indicator_controller.js +115 -21
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +26 -6
- package/dist/controllers/overflow_menu_controller.js +141 -44
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +19 -11
- package/dist/controllers/pagination_controller.js +74 -28
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +15 -1
- package/dist/controllers/password_reveal_controller.js +59 -2
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js +30 -8
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +131 -52
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +9 -3
- package/dist/controllers/popover_controller.js +45 -11
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -1
- package/dist/controllers/portal_controller.js +6 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.js +16 -1
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js +8 -4
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +6 -4
- package/dist/controllers/radio_group_controller.js +42 -17
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +49 -1
- package/dist/controllers/range_slider_controller.js +88 -42
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +14 -3
- package/dist/controllers/rating_controller.js +39 -15
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +24 -2
- package/dist/controllers/read_more_controller.js +100 -7
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.js +65 -19
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +10 -5
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +16 -2
- package/dist/controllers/resizable_controller.js +82 -22
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +75 -27
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +37 -16
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +49 -30
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +3 -2
- package/dist/controllers/scrollspy_controller.js +71 -26
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +41 -14
- package/dist/controllers/separator_controller.js +66 -37
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +20 -3
- package/dist/controllers/sidebar_controller.js +77 -18
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js +6 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +45 -7
- package/dist/controllers/slider_controller.js +82 -47
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.js +60 -26
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +17 -2
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +10 -2
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +19 -17
- package/dist/controllers/step_indicator_controller.js +18 -17
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +34 -9
- package/dist/controllers/stepper_controller.js +101 -19
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
- package/dist/controllers/stick_to_bottom_controller.js +104 -8
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +3 -2
- package/dist/controllers/submit_once_controller.js +45 -9
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +29 -7
- package/dist/controllers/switch_controller.js +101 -10
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +12 -0
- package/dist/controllers/tabs_controller.js +21 -2
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +15 -3
- package/dist/controllers/tags_input_controller.js +209 -59
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +29 -3
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +20 -3
- package/dist/controllers/theme_controller.js +64 -14
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js +23 -8
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +55 -15
- package/dist/controllers/toast_controller.js +451 -105
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +49 -7
- package/dist/controllers/toggle_group_controller.js +159 -23
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +32 -0
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +8 -0
- package/dist/controllers/tooltip_controller.js +39 -13
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.js +4 -0
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +39 -8
- package/dist/controllers/tree_view_controller.js +169 -16
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +5002 -1911
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +72 -6
- package/dist/inspector/cli.js +262 -51
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +309 -51
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +40 -40
- package/dist/inspector/manifest.json +529 -61
- package/dist/positioning/index.js +2 -0
- package/dist/positioning/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -14,11 +14,19 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// src/utils/composition_tracker.ts
|
|
17
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
18
|
+
"insertCompositionText",
|
|
19
|
+
"insertFromComposition",
|
|
20
|
+
"deleteCompositionText",
|
|
21
|
+
"deleteByComposition"
|
|
22
|
+
]);
|
|
17
23
|
var CompositionTracker = class {
|
|
18
24
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
19
25
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
20
26
|
#onStart;
|
|
21
27
|
#onEnd;
|
|
28
|
+
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
29
|
+
#confirmedTarget = null;
|
|
22
30
|
constructor(options = {}) {
|
|
23
31
|
this.#onStart = options.onStart;
|
|
24
32
|
this.#onEnd = options.onEnd;
|
|
@@ -28,6 +36,7 @@ var CompositionTracker = class {
|
|
|
28
36
|
if (this.#observedTargets.has(target)) return;
|
|
29
37
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
30
38
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
39
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
31
40
|
this.#observedTargets.add(target);
|
|
32
41
|
}
|
|
33
42
|
/** Stops tracking one target and clears any active composition it owned. */
|
|
@@ -35,29 +44,52 @@ var CompositionTracker = class {
|
|
|
35
44
|
if (!this.#observedTargets.delete(target)) return;
|
|
36
45
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
37
46
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
47
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
38
48
|
this.#activeTargets.delete(target);
|
|
49
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
39
50
|
}
|
|
40
51
|
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
41
52
|
disconnect() {
|
|
42
53
|
for (const target of this.#observedTargets) {
|
|
43
54
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
44
55
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
56
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
45
57
|
}
|
|
46
58
|
this.#observedTargets.clear();
|
|
47
59
|
this.#activeTargets.clear();
|
|
60
|
+
this.#confirmedTarget = null;
|
|
48
61
|
}
|
|
49
62
|
/** True when lifecycle tracking or the current event reports composition. */
|
|
50
63
|
isComposing(event) {
|
|
51
64
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
52
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
68
|
+
*
|
|
69
|
+
* Asking closes the window either way, so one confirmation is folded at most
|
|
70
|
+
* once and a consumer asks once per `input`.
|
|
71
|
+
*/
|
|
72
|
+
consumesConfirmedInput(event) {
|
|
73
|
+
const confirmed = this.#confirmedTarget;
|
|
74
|
+
this.#confirmedTarget = null;
|
|
75
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
76
|
+
const inputType = event.inputType;
|
|
77
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
78
|
+
}
|
|
53
79
|
#handleStart = (event) => {
|
|
80
|
+
this.#confirmedTarget = null;
|
|
54
81
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
55
82
|
this.#onStart?.(event);
|
|
56
83
|
};
|
|
57
84
|
#handleEnd = (event) => {
|
|
58
85
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
86
|
+
this.#confirmedTarget = event.target;
|
|
59
87
|
this.#onEnd?.(event);
|
|
60
88
|
};
|
|
89
|
+
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
90
|
+
#handleKeydown = () => {
|
|
91
|
+
this.#confirmedTarget = null;
|
|
92
|
+
};
|
|
61
93
|
};
|
|
62
94
|
|
|
63
95
|
// src/utils/event_owner.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/composition_tracker.ts","../../src/utils/event_owner.ts","../../src/utils/focus_candidate.ts","../../src/utils/roving_tabindex.ts","../../src/controllers/toolbar_controller.ts"],"names":[],"mappings":";;;;;AAiBO,SAAS,MAAM,OAAA,EAA2B;AAC/C,EAAA,OAAO,MAAA,CAAO,gBAAA,CAAiB,OAAO,CAAA,CAAE,SAAA,KAAc,KAAA;AACxD;;;AC4EO,SAAS,oBAAA,CACd,KAAA,EACA,KAAA,GAAkC,EAAC,EAC1B;AACT,EAAA,IAAI,CAAC,KAAA,CAAM,GAAA,CAAI,UAAA,CAAW,OAAO,GAAG,OAAO,KAAA;AAC3C,EAAA,OACG,KAAA,CAAM,MAAA,IAAU,CAAC,KAAA,CAAM,QAAA,CAAS,KAAK,CAAA,IACrC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,KACvC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,IACvC,KAAA,CAAM,QAAA,IAAY,CAAC,KAAA,CAAM,QAAA,CAAS,OAAO,CAAA;AAE9C;;;ACnEO,IAAM,qBAAN,MAAyB;AAAA,EACrB,gBAAA,uBAAuB,GAAA,EAAiB;AAAA,EACxC,cAAA,uBAAqB,GAAA,EAAiB;AAAA,EACtC,QAAA;AAAA,EACA,MAAA;AAAA,EAET,WAAA,CAAY,OAAA,GAAqC,EAAC,EAAG;AACnD,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,OAAA;AACxB,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,KAAA;AAAA,EACxB;AAAA;AAAA,EAGA,QAAQ,MAAA,EAA2B;AACjC,IAAA,IAAI,IAAA,CAAK,gBAAA,CAAiB,GAAA,CAAI,MAAM,CAAA,EAAG;AACvC,IAAA,MAAA,CAAO,gBAAA,CAAiB,kBAAA,EAAoB,IAAA,CAAK,YAAY,CAAA;AAC7D,IAAA,MAAA,CAAO,gBAAA,CAAiB,gBAAA,EAAkB,IAAA,CAAK,UAAU,CAAA;AACzD,IAAA,IAAA,CAAK,gBAAA,CAAiB,IAAI,MAAM,CAAA;AAAA,EAClC;AAAA;AAAA,EAGA,UAAU,MAAA,EAA2B;AACnC,IAAA,IAAI,CAAC,IAAA,CAAK,gBAAA,CAAiB,MAAA,CAAO,MAAM,CAAA,EAAG;AAC3C,IAAA,MAAA,CAAO,mBAAA,CAAoB,kBAAA,EAAoB,IAAA,CAAK,YAAY,CAAA;AAChE,IAAA,MAAA,CAAO,mBAAA,CAAoB,gBAAA,EAAkB,IAAA,CAAK,UAAU,CAAA;AAC5D,IAAA,IAAA,CAAK,cAAA,CAAe,OAAO,MAAM,CAAA;AAAA,EACnC;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,KAAA,MAAW,MAAA,IAAU,KAAK,gBAAA,EAAkB;AAC1C,MAAA,MAAA,CAAO,mBAAA,CAAoB,kBAAA,EAAoB,IAAA,CAAK,YAAY,CAAA;AAChE,MAAA,MAAA,CAAO,mBAAA,CAAoB,gBAAA,EAAkB,IAAA,CAAK,UAAU,CAAA;AAAA,IAC9D;AACA,IAAA,IAAA,CAAK,iBAAiB,KAAA,EAAM;AAC5B,IAAA,IAAA,CAAK,eAAe,KAAA,EAAM;AAAA,EAC5B;AAAA;AAAA,EAGA,YAAY,KAAA,EAAoC;AAC9C,IAAA,OAAO,IAAA,CAAK,cAAA,CAAe,IAAA,GAAO,CAAA,IAAK,OAAO,WAAA,KAAgB,IAAA;AAAA,EAChE;AAAA,EAES,YAAA,GAAe,CAAC,KAAA,KAAuB;AAC9C,IAAA,IAAI,MAAM,aAAA,EAAe,IAAA,CAAK,cAAA,CAAe,GAAA,CAAI,MAAM,aAAa,CAAA;AACpE,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AAAA,EACvB,CAAA;AAAA,EAES,UAAA,GAAa,CAAC,KAAA,KAAuB;AAC5C,IAAA,IAAI,MAAM,aAAA,EAAe,IAAA,CAAK,cAAA,CAAe,MAAA,CAAO,MAAM,aAAa,CAAA;AACvE,IAAA,IAAA,CAAK,SAAS,KAAK,CAAA;AAAA,EACrB,CAAA;AACF,CAAA;;;AC1DO,SAAS,UAAA,CACd,YACA,IAAA,EACQ;AACR,EAAA,IAAI,EAAE,IAAA,YAAgB,IAAA,CAAA,EAAO,OAAO,EAAA;AACpC,EAAA,OAAO,WAAW,SAAA,CAAU,CAAC,cAAc,SAAA,CAAU,QAAA,CAAS,IAAI,CAAC,CAAA;AACrE;;;AC3BO,SAAS,yBAAyB,OAAA,EAA+B;AACtE,EAAA,IAAI,QAAA,GAA2B,OAAA,CAAQ,OAAA,CAAQ,oBAAoB,CAAA;AACnE,EAAA,OAAO,QAAA,EAAU;AACf,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,KAAA,KAAU,KAAA,CAAM,OAAA,KAAY,QAAQ,CAAA;AACvF,IAAA,IAAI,CAAC,MAAA,EAAQ,QAAA,CAAS,OAAO,GAAG,OAAO,IAAA;AACvC,IAAA,QAAA,GAAW,QAAA,CAAS,aAAA,EAAe,OAAA,CAAQ,oBAAoB,CAAA,IAAK,IAAA;AAAA,EACtE;AACA,EAAA,OAAO,KAAA;AACT;;;ACKO,IAAM,iBAAN,MAAqB;AAAA;AAAA,EAEjB,SAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMT,YAAY,QAAA,EAA+B;AACzC,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AAAA,EACnB;AAAA;AAAA,EAGA,IAAI,WAAA,GAAsB;AACxB,IAAA,OAAO,IAAA,CAAK,WAAU,CAAE,SAAA,CAAU,CAAC,IAAA,KAAS,IAAA,CAAK,aAAa,CAAC,CAAA;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,SAAA,CACE,KAAA,EACA,OAAA,GAA+D,EAAC,EAC1D;AACN,IAAA,MAAM,EAAE,KAAA,GAAQ,KAAA,EAAM,GAAI,OAAA;AAC1B,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,IAAS,IAAA,CAAK,SAAA,EAAU;AAC9C,IAAA,KAAA,CAAM,OAAA,CAAQ,CAAC,IAAA,EAAM,CAAA,KAAM;AACzB,MAAA,IAAA,CAAK,QAAA,GAAW,CAAA,KAAM,KAAA,GAAQ,CAAA,GAAI,EAAA;AAAA,IACpC,CAAC,CAAA;AACD,IAAA,IAAI,KAAA,EAAO,KAAA,CAAM,KAAK,CAAA,EAAG,KAAA,EAAM;AAAA,EACjC;AACF,CAAA;AAkBO,SAAS,UAAA,CACd,OAAA,EACA,MAAA,EACA,KAAA,EACA,IAAA,EACQ;AACR,EAAA,IAAI,MAAA,KAAW,GAAG,OAAO,EAAA;AACzB,EAAA,MAAM,OAAO,OAAA,GAAU,KAAA;AACvB,EAAA,IAAI,IAAA,KAAS,MAAA,EAAQ,OAAA,CAAQ,IAAA,GAAO,MAAA,IAAU,MAAA;AAC9C,EAAA,OAAO,IAAA,CAAK,IAAI,MAAA,GAAS,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,IAAI,CAAC,CAAA;AAC/C;;;ACzEA,IAAM,gBAAA,GAAmB,CAAC,UAAA,EAAY,QAAQ,CAAA;AA4DvC,IAAM,iBAAA,GAAN,cAAgC,UAAA,CAAwB;AAAA,EAC7D,OAAgB,OAAA,GAAU,CAAC,SAAS,CAAA;AAAA,EACpC,OAAgB,MAAA,GAAS;AAAA,IACvB,WAAA,EAAa,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,YAAA,EAAa;AAAA,IACnD,IAAA,EAAM,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA;AAAK,GACvC;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,WAAW,CAAA;AAAA,EAMpB,OAAA,GAAU,IAAI,cAAA,CAAe,MAAM,KAAK,cAAc,CAAA;AAAA,EACtD,YAAA,GAAe,IAAI,kBAAA,EAAmB;AAAA,EAC/C,SAAA,GAAqC,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOrC,UAAA,GAAa,KAAA;AAAA,EAEJ,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACxD,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACxD,IAAA,IAAA,CAAK,YAAA,CAAa,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA;AACtC,IAAA,IAAI,OAAO,qBAAqB,WAAA,EAAa;AAC3C,MAAA,MAAM,WAAW,IAAI,gBAAA,CAAiB,MAAM,IAAA,CAAK,gBAAgB,CAAA;AACjE,MAAA,QAAA,CAAS,OAAA,CAAQ,KAAK,OAAA,EAAS;AAAA,QAC7B,OAAA,EAAS,IAAA;AAAA,QACT,SAAA,EAAW,IAAA;AAAA,QACX,UAAA,EAAY,IAAA;AAAA,QACZ,eAAA,EAAiB;AAAA,OAClB,CAAA;AAKD,MAAA,KAAA,IACM,QAAA,GAAW,IAAA,CAAK,OAAA,CAAQ,aAAA,EAAe,QAAQ,UAAU,CAAA,IAAK,IAAA,EAClE,QAAA,EACA,WAAW,QAAA,CAAS,aAAA,EAAe,OAAA,CAAQ,UAAU,KAAK,IAAA,EAC1D;AACA,QAAA,QAAA,CAAS,OAAA,CAAQ,UAAU,EAAE,UAAA,EAAY,MAAM,eAAA,EAAiB,CAAC,UAAU,CAAA,EAAG,CAAA;AAAA,MAChF;AACA,MAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AAAA,IACnB;AACA,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAAA,EACpB;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AAC3D,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AAC3D,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,IAAA,CAAK,WAAW,UAAA,EAAW;AAC3B,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,uBAAuB,OAAA,EAA4B;AACjD,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,OAAA,CAAQ,QAAA,GAAW,EAAA;AACnB,IAAA,IAAA,CAAK,cAAA,EAAe;AAAA,EACtB;AAAA;AAAA,EAGA,yBAAA,GAAkC;AAChC,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,cAAA,EAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,UAAU,KAAA,EAA4B;AACpC,IAAA,IAAA,CAAK,eAAe,KAAK,CAAA;AAAA,EAC3B;AAAA;AAAA,EAGS,UAAA,GAAa,CAAC,KAAA,KAA+B;AACpD,IAAA,IAAA,CAAK,eAAe,KAAK,CAAA;AAAA,EAC3B,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,UAAA,GAAa,CAAC,KAAA,KAA4B;AACjD,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,QAAA,CAAS,KAAA,CAAM,MAAM,CAAA;AACxC,IAAA,MAAM,UAAU,KAAA,KAAU,EAAA,GAAK,MAAA,GAAY,IAAA,CAAK,eAAe,KAAK,CAAA;AACpE,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,IAAA,CAAK,YAAA,CAAa,OAAO,CAAA,EAAG;AAC7C,IAAA,IAAA,CAAK,OAAA,CAAQ,UAAU,KAAK,CAAA;AAAA,EAC9B,CAAA;AAAA,EAEA,eAAe,KAAA,EAA4B;AAIzC,IAAA,IAAI,MAAM,gBAAA,EAAkB;AAC5B,IAAA,IAAI,oBAAA,CAAqB,KAAK,CAAA,EAAG;AAEjC,IAAA,IAAI,IAAA,CAAK,YAAA,CAAa,WAAA,CAAY,KAAK,CAAA,EAAG;AAI1C,IAAA,IAAA,CAAK,cAAA,EAAe;AAEpB,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,QAAA,CAAS,KAAA,CAAM,MAAM,CAAA;AACvC,IAAA,IAAI,SAAS,EAAA,EAAI;AAEjB,IAAA,MAAM,QAAA,GAAW,KAAK,gBAAA,KAAqB,UAAA;AAO3C,IAAA,MAAM,GAAA,GAAM,CAAC,QAAA,IAAY,KAAA,CAAM,KAAK,OAAO,CAAA;AAC3C,IAAA,MAAM,UAAA,GAAa,QAAA,GAAW,WAAA,GAAc,GAAA,GAAM,WAAA,GAAc,YAAA;AAChE,IAAA,MAAM,WAAA,GAAc,QAAA,GAAW,SAAA,GAAY,GAAA,GAAM,YAAA,GAAe,WAAA;AAEhE,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI,KAAA,CAAM,QAAQ,UAAA,EAAY;AAC5B,MAAA,MAAA,GAAS,IAAA,CAAK,cAAA,CAAe,IAAA,EAAM,CAAC,CAAA;AAAA,IACtC,CAAA,MAAA,IAAW,KAAA,CAAM,GAAA,KAAQ,WAAA,EAAa;AACpC,MAAA,MAAA,GAAS,IAAA,CAAK,cAAA,CAAe,IAAA,EAAM,EAAE,CAAA;AAAA,IACvC,CAAA,MAAA,IAAW,KAAA,CAAM,GAAA,KAAQ,MAAA,EAAQ;AAC/B,MAAA,MAAA,GAAS,IAAA,CAAK,mBAAmB,CAAC,CAAA;AAAA,IACpC,CAAA,MAAA,IAAW,KAAA,CAAM,GAAA,KAAQ,KAAA,EAAO;AAC9B,MAAA,MAAM,YAAY,IAAA,CAAK,kBAAA;AACvB,MAAA,MAAA,GAAS,SAAA,CAAU,SAAA,CAAU,MAAA,GAAS,CAAC,CAAA;AAAA,IACzC,CAAA,MAAO;AACL,MAAA;AAAA,IACF;AAEA,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,IAAI,MAAA,EAAQ,IAAA,CAAK,OAAA,CAAQ,SAAA,CAAU,IAAA,CAAK,cAAA,CAAe,OAAA,CAAQ,MAAM,CAAA,EAAG,EAAE,KAAA,EAAO,IAAA,EAAM,CAAA;AAAA,EACzF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,cAAA,GAAuB;AACrB,IAAA,MAAM,MAAA,GAAS,KAAK,OAAA,CAAQ,WAAA;AAC5B,IAAA,MAAM,WAAW,MAAA,KAAW,EAAA,GAAK,IAAA,GAAO,IAAA,CAAK,eAAe,MAAM,CAAA;AAClE,IAAA,IAAI,QAAA,IAAY,IAAA,CAAK,YAAA,CAAa,QAAQ,CAAA,EAAG;AAC3C,MAAA,IAAA,CAAK,OAAA,CAAQ,UAAU,MAAM,CAAA;AAC7B,MAAA;AAAA,IACF;AACA,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,kBAAA,CAAmB,CAAC,CAAA;AACvC,IAAA,IAAA,CAAK,OAAA,CAAQ,UAAU,KAAA,GAAQ,IAAA,CAAK,eAAe,OAAA,CAAQ,KAAK,IAAI,EAAE,CAAA;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,cAAA,CAAe,WAAmB,KAAA,EAAwC;AACxE,IAAA,MAAM,WAAW,IAAA,CAAK,cAAA;AACtB,IAAA,MAAM,SAAS,QAAA,CAAS,MAAA;AACxB,IAAA,MAAM,IAAA,GAAmB,IAAA,CAAK,SAAA,GAAY,MAAA,GAAS,OAAA;AACnD,IAAA,IAAI,KAAA,GAAQ,SAAA;AACZ,IAAA,KAAA,IAAS,IAAA,GAAO,CAAA,EAAG,IAAA,GAAO,MAAA,EAAQ,IAAA,EAAA,EAAQ;AACxC,MAAA,MAAM,IAAA,GAAO,UAAA,CAAW,KAAA,EAAO,MAAA,EAAQ,OAAO,IAAI,CAAA;AAClD,MAAA,IAAI,SAAS,KAAA,EAAO;AACpB,MAAA,KAAA,GAAQ,IAAA;AACR,MAAA,MAAM,SAAA,GAAY,SAAS,KAAK,CAAA;AAChC,MAAA,IAAI,SAAA,IAAa,IAAA,CAAK,YAAA,CAAa,SAAS,GAAG,OAAO,SAAA;AAAA,IACxD;AACA,IAAA,MAAM,MAAA,GAAS,SAAS,SAAS,CAAA;AACjC,IAAA,OAAO,MAAA,IAAU,KAAK,YAAA,CAAa,MAAM,IAAI,MAAA,GAAS,IAAA,CAAK,mBAAmB,CAAC,CAAA;AAAA,EACjF;AAAA;AAAA,EAGA,SAAS,MAAA,EAAoC;AAC3C,IAAA,OAAO,UAAA,CAAW,IAAA,CAAK,cAAA,EAAgB,MAAM,CAAA;AAAA,EAC/C;AAAA;AAAA,EAGA,IAAI,kBAAA,GAAoC;AACtC,IAAA,OAAO,IAAA,CAAK,eAAe,MAAA,CAAO,CAAC,YAAY,IAAA,CAAK,YAAA,CAAa,OAAO,CAAC,CAAA;AAAA,EAC3E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,aAAa,OAAA,EAA+B;AAC1C,IAAA,IAAI,IAAA,CAAK,SAAA,CAAU,OAAO,CAAA,EAAG,OAAO,KAAA;AACpC,IAAA,IAAI,EAAE,UAAA,IAAc,OAAA,CAAA,EAAU,OAAO,IAAA;AACrC,IAAA,IAAI,OAAA,CAAQ,QAAA,KAAa,IAAA,EAAM,OAAO,KAAA;AACtC,IAAA,OAAO,CAAC,yBAAyB,OAAO,CAAA;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,UAAU,OAAA,EAA+B;AACvC,IAAA,IAAI,IAAA,GAA2B,OAAA;AAC/B,IAAA,OAAO,IAAA,IAAQ,IAAA,KAAS,IAAA,CAAK,OAAA,EAAS;AACpC,MAAA,IAAI,IAAA,CAAK,YAAA,CAAa,QAAQ,CAAA,EAAG,OAAO,IAAA;AACxC,MAAA,IAAA,GAAO,IAAA,CAAK,aAAA;AAAA,IACd;AACA,IAAA,OAAO,KAAA;AAAA,EACT;AACF","file":"toolbar_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\n };\n}\n","/**\n * Resolves which of a controller's elements owns a node.\n *\n * A delegated listener hears events from a whole subtree, so the handler's first\n * job is almost always the same question: which item, handle, or control does\n * this `event.target` belong to? The same question comes up for a\n * `MutationRecord.target`, for `document.activeElement`, and for a\n * `relatedTarget` on the way out of a hover region.\n *\n * The answer is `Node.contains()`, which is **inclusive** — an element contains\n * itself — so testing the candidate for identity as well would be redundant.\n *\n * **The guard is what makes this safe to call with a raw event target.**\n * `event.target` is typed `EventTarget | null`, and `contains()` takes a `Node?`:\n * browsers throw `TypeError` for anything else, and `window` — the everyday\n * `EventTarget` that is not a `Node` — is what an event dispatched at it carries.\n * Narrowing here means a caller never has to cast, and the rule cannot drift\n * between the places that ask the question.\n *\n * Scope stays with the caller. These helpers say *which candidate owns the node*,\n * not *whether the node belongs to this controller at all* — a component with\n * nested instances of itself decides that first (by comparing the closest\n * annotated ancestor) and passes the candidates it owns.\n */\n\n/**\n * Index in `candidates` of the first one that is, or contains, `node`.\n *\n * `-1` when none does, when `node` is absent, and when it is an `EventTarget`\n * that is not a `Node`. Candidates are tested in array order, so a nested pair\n * resolves to whichever the caller listed first.\n */\nexport function ownerIndex<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): number {\n if (!(node instanceof Node)) return -1;\n return candidates.findIndex((candidate) => candidate.contains(node));\n}\n\n/**\n * The first candidate that is, or contains, `node`; `null` when none does.\n *\n * A miss indexes the array at `-1`, which reads as `undefined` and lands on the\n * same `null` the absent cases produce.\n */\nexport function ownerOf<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): T | null {\n return candidates[ownerIndex(candidates, node)] ?? null;\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper. A caller\n * that already captured one event-scoped snapshot may pass it to\n * {@link RovingTabindex.setActive}, avoiding a second live-target scan while\n * preserving one coherent collection for the whole operation.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item,\n * and `items` to reuse an event-scoped collection snapshot.\n */\n setActive(\n index: number,\n options: { focus?: boolean; items?: readonly HTMLElement[] } = {},\n ): void {\n const { focus = false } = options;\n const items = options.items ?? this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\nimport { ownerIndex } from \"../utils/event_owner\";\nimport { inheritsFieldsetDisabled } from \"../utils/focus_candidate\";\nimport { isRtl } from \"../utils/logical_scroll\";\nimport { RovingTabindex, type RovingWrap, rovingMove } from \"../utils/roving_tabindex\";\n\n/**\n * Attributes that flip a control between navigable and not. Watched on the whole\n * subtree because a *sibling* controller (an editor toggling `disabled` on its\n * command buttons, a form enabling a fieldset) is what usually changes them.\n * Watched together with `childList`, which covers controls added or removed at\n * runtime without depending on Stimulus's asynchronous target callbacks.\n * `tabindex` is deliberately absent: the controller writes it itself, so\n * observing it would feed back into the observer.\n */\nconst STATE_ATTRIBUTES = [\"disabled\", \"hidden\"];\n\n/**\n * Headless, accessible toolbar behavior.\n *\n * Markup contract (identifier: `stimeo--toolbar`):\n * <div data-controller=\"stimeo--toolbar\" role=\"toolbar\" aria-label=\"Text formatting\"\n * data-stimeo--toolbar-orientation-value=\"horizontal\">\n * <button type=\"button\" data-stimeo--toolbar-target=\"control\">Bold</button>\n * <!-- more controls -->\n * </div>\n *\n * Implements the WAI-ARIA APG **Toolbar** pattern: the group is a single Tab\n * stop (roving tabindex) and the arrow keys move focus between controls. Each\n * control's own function (press, toggle, open a menu) stays with that element or\n * its own controller — the toolbar only owns navigation.\n *\n * @remarks\n * Behavior only. The roving mechanics are delegated to a shared roving-tabindex helper;\n * orientation and wrap policy stay here per the APG (they differ per widget).\n * ARIA (`role=\"toolbar\"`, the accessible name, and `aria-orientation=\"vertical\"`\n * when the axis is vertical) is the author's, exactly as with `role`.\n *\n * Behavior provided:\n * - Exactly one *navigable* control is tabbable (`tabindex=\"0\"`); the rest are\n * `-1`. The invariant is re-established on connect, on control add/remove,\n * whenever `disabled` / `hidden` changes under the toolbar, and whenever an\n * enclosing `<fieldset>` is disabled or re-enabled — disabling the control that\n * held the Tab stop must never make the whole group unreachable by Tab, and\n * unlocking the surrounding form must bring it back.\n * - `ArrowRight`/`ArrowLeft` (horizontal) or `ArrowDown`/`ArrowUp` (vertical)\n * move focus to the next/previous navigable control; `Home`/`End` to the\n * first/last. `aria-disabled=\"true\"` controls remain in that roving order and\n * can be both origins and destinations; only their activation is suppressed\n * by the control itself or its owning behavior. Native `disabled` and `hidden`\n * controls are excluded.\n * - With `wrap=true` movement cycles past the ends; with `wrap=false` it stops.\n * - `orientation` accepts `horizontal` (default) and `vertical`; any other value\n * degrades to `horizontal`. There is no `both` — for two-axis navigation use\n * `stimeo--roving`, whose same-named Value does accept it.\n * - Returning focus from outside lands on the most recently *active* control —\n * the last one moved to by key, click, or programmatic `focus()`, because that\n * is the one left tabbable (`focusin` keeps the Tab stop in sync).\n *\n * Keydown is **delegated on the container**, so controls added at runtime need\n * no per-element `data-action` (Stimulus binds those asynchronously, which is\n * unreliable for appended nodes). A `data-action=\"keydown->stimeo--toolbar#onKeydown\"`\n * on each control stays supported; it does not double-move, because the second\n * pass sees `defaultPrevented`.\n *\n * Initial Tab stop: to choose it, write `tabindex` on **every** control (exactly\n * one `0`, the rest `-1`). Natively focusable elements are `tabindex=\"0\"`\n * effectively when the attribute is absent, so annotating only the intended\n * entry point makes the *first* control win instead.\n *\n * Known constraint: a toolbar that contains a text input competes with it for\n * the arrow keys — the APG expects the toolbar to own them. Keep free-text\n * fields outside the toolbar (IME composition is yielded to, but caret movement\n * is not).\n */\nexport class ToolbarController extends Controller<HTMLElement> {\n static override targets = [\"control\"];\n static override values = {\n orientation: { type: String, default: \"horizontal\" },\n wrap: { type: Boolean, default: true },\n };\n static actions = [\"onKeydown\"] as const;\n\n declare readonly controlTargets: HTMLElement[];\n declare orientationValue: string;\n declare wrapValue: boolean;\n\n readonly #roving = new RovingTabindex(() => this.controlTargets);\n readonly #composition = new CompositionTracker();\n #observer: MutationObserver | null = null;\n /**\n * Live between `connect()` and `disconnect()`. Stimulus reports the *initial*\n * targets before `connect()` and re-reports them as disconnected after\n * `disconnect()`; gating on this keeps the target callbacks from clobbering\n * the authored Tab stop on mount and from resurrecting one after teardown.\n */\n #connected = false;\n\n override connect(): void {\n this.#ensureTabStop();\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n this.element.addEventListener(\"focusin\", this.#onFocusin);\n this.#composition.observe(this.element);\n if (typeof MutationObserver !== \"undefined\") {\n const observer = new MutationObserver(() => this.#ensureTabStop());\n observer.observe(this.element, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: STATE_ATTRIBUTES,\n });\n // A `<fieldset disabled>` above the root disables the toolbar's form\n // controls from outside the observed subtree, so each enclosing fieldset's\n // own `disabled` is watched too. The walk starts at the parent: re-observing\n // the root would replace its options and lose the subtree registration.\n for (\n let fieldset = this.element.parentElement?.closest(\"fieldset\") ?? null;\n fieldset;\n fieldset = fieldset.parentElement?.closest(\"fieldset\") ?? null\n ) {\n observer.observe(fieldset, { attributes: true, attributeFilter: [\"disabled\"] });\n }\n this.#observer = observer;\n }\n this.#connected = true;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n this.element.removeEventListener(\"focusin\", this.#onFocusin);\n this.#composition.disconnect();\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /**\n * A control added at runtime is dropped out of the Tab sequence first — a\n * fresh `<button>` is tabbable by default, which would make the group two Tab\n * stops — and then the lone stop is re-established.\n */\n controlTargetConnected(control: HTMLElement): void {\n if (!this.#connected) return;\n control.tabIndex = -1;\n this.#ensureTabStop();\n }\n\n /** Removing the control that held the Tab stop must not orphan the group. */\n controlTargetDisconnected(): void {\n if (!this.#connected) return;\n this.#ensureTabStop();\n }\n\n /**\n * Arrow/Home/End move focus and the single tab stop.\n *\n * Binding this per control with `data-action` is **optional** — the same\n * handling runs from the container's delegated listener. It stays a declared\n * action so per-control wiring keeps working; wiring both does not\n * double-move, because the delegated pass then yields on `defaultPrevented`.\n */\n onKeydown(event: KeyboardEvent): void {\n this.#handleKeydown(event);\n }\n\n /** Delegated counterpart of {@link onKeydown}; bound on the container. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n this.#handleKeydown(event);\n };\n\n /**\n * Syncs the Tab stop to a control focused by other means (click, programmatic\n * `focus()`) so Tab re-entry returns there. A non-navigable control is ignored:\n * the lone Tab stop must never sit on something the user cannot operate.\n */\n readonly #onFocusin = (event: FocusEvent): void => {\n const index = this.#indexOf(event.target);\n const control = index === -1 ? undefined : this.controlTargets[index];\n if (!control || !this.#isNavigable(control)) return;\n this.#roving.setActive(index);\n };\n\n #handleKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO move the roving focus. This is what makes the\n // per-control `data-action` and the delegated listener idempotent.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n // Arrow/Home/End belong to the IME while a composition is in flight.\n if (this.#composition.isComposing(event)) return;\n // Re-assert the invariant before acting: the attribute observer runs async\n // (and may be absent), so a Tab stop invalidated moments ago can still be in\n // place here.\n this.#ensureTabStop();\n\n const from = this.#indexOf(event.target);\n if (from === -1) return;\n\n const vertical = this.orientationValue === \"vertical\";\n // Logical, not physical. APG defines these as \"next / previous control\", and\n // says a vertical arrangement swaps in Down/Up for the same meaning — so the\n // pair is one axis's spelling of an order, and the order reverses with the\n // writing direction. Read from the controller element: the container is what\n // lays the items out, and a child may carry its own `dir` (an LTR input\n // inside an RTL form is ordinary authoring).\n const rtl = !vertical && isRtl(this.element);\n const forwardKey = vertical ? \"ArrowDown\" : rtl ? \"ArrowLeft\" : \"ArrowRight\";\n const backwardKey = vertical ? \"ArrowUp\" : rtl ? \"ArrowRight\" : \"ArrowLeft\";\n\n let target: HTMLElement | undefined;\n if (event.key === forwardKey) {\n target = this.#nextNavigable(from, 1);\n } else if (event.key === backwardKey) {\n target = this.#nextNavigable(from, -1);\n } else if (event.key === \"Home\") {\n target = this.#navigableControls[0];\n } else if (event.key === \"End\") {\n const navigable = this.#navigableControls;\n target = navigable[navigable.length - 1];\n } else {\n return;\n }\n\n event.preventDefault();\n if (target) this.#roving.setActive(this.controlTargets.indexOf(target), { focus: true });\n }\n\n /**\n * Re-establishes the single Tab stop: keep the current one while it is still\n * navigable, else hand it to the first navigable control. `-1` (no Tab stop at\n * all) is reached only when every control is unavailable, and is recovered\n * from as soon as one becomes navigable again. Idempotent by construction, so\n * connect, the target callbacks, the observer, and keydown can all call it.\n */\n #ensureTabStop(): void {\n const active = this.#roving.activeIndex;\n const activeEl = active === -1 ? null : this.controlTargets[active];\n if (activeEl && this.#isNavigable(activeEl)) {\n this.#roving.setActive(active);\n return;\n }\n const first = this.#navigableControls[0];\n this.#roving.setActive(first ? this.controlTargets.indexOf(first) : -1);\n }\n\n /**\n * First navigable control strictly in the `delta` direction from `fromIndex`,\n * scanning the **full** control list rather than the navigable subset so a\n * non-navigable origin is never a dead end. When the direction yields nothing\n * the origin keeps focus; when the origin itself is not navigable the first\n * navigable control is used instead, so an arrow key always escapes.\n */\n #nextNavigable(fromIndex: number, delta: number): HTMLElement | undefined {\n const controls = this.controlTargets;\n const length = controls.length;\n const wrap: RovingWrap = this.wrapValue ? \"wrap\" : \"clamp\";\n let index = fromIndex;\n for (let step = 0; step < length; step++) {\n const next = rovingMove(index, length, delta, wrap);\n if (next === index) break; // clamped at an end (or a single control)\n index = next;\n const candidate = controls[index];\n if (candidate && this.#isNavigable(candidate)) return candidate;\n }\n const origin = controls[fromIndex];\n return origin && this.#isNavigable(origin) ? origin : this.#navigableControls[0];\n }\n\n /** Index in `controlTargets` of the control owning `target` (it or a descendant). */\n #indexOf(target: EventTarget | null): number {\n return ownerIndex(this.controlTargets, target);\n }\n\n /** Controls eligible for the roving tab stop (excludes native disabled / hidden). */\n get #navigableControls(): HTMLElement[] {\n return this.controlTargets.filter((control) => this.#isNavigable(control));\n }\n\n /**\n * A control can hold the tab stop unless it is `hidden` or a disabled form\n * control. `aria-disabled` remains navigable per the APG; activation suppression\n * belongs to the control itself or its owning behavior. Native disabledness is\n * read from the `disabled` property, narrowed by an `in` check rather than\n * asserted since `<a>` and `<div role=\"button\">` are legitimate controls that\n * are unaffected by a disabled fieldset. CSS-only visibility cannot be detected\n * headlessly and stays the consumer's responsibility.\n */\n #isNavigable(control: HTMLElement): boolean {\n if (this.#isHidden(control)) return false;\n if (!(\"disabled\" in control)) return true;\n if (control.disabled === true) return false;\n return !inheritsFieldsetDisabled(control);\n }\n\n /**\n * Whether `control`, or anything between it and the toolbar root, is `hidden`.\n *\n * Reading only the control's own attribute lets an invisible control hold the\n * single Tab stop, which takes the *whole* toolbar out of the Tab sequence — an\n * ordinary `hidden` wrapper is enough. The walk stops at the root: a toolbar\n * inside a `hidden` region is already out of the page's Tab order.\n */\n #isHidden(control: HTMLElement): boolean {\n let node: HTMLElement | null = control;\n while (node && node !== this.element) {\n if (node.hasAttribute(\"hidden\")) return true;\n node = node.parentElement;\n }\n return false;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/composition_tracker.ts","../../src/utils/event_owner.ts","../../src/utils/focus_candidate.ts","../../src/utils/roving_tabindex.ts","../../src/controllers/toolbar_controller.ts"],"names":[],"mappings":";;;;;AAiBO,SAAS,MAAM,OAAA,EAA2B;AAC/C,EAAA,OAAO,MAAA,CAAO,gBAAA,CAAiB,OAAO,CAAA,CAAE,SAAA,KAAc,KAAA;AACxD;;;AC4EO,SAAS,oBAAA,CACd,KAAA,EACA,KAAA,GAAkC,EAAC,EAC1B;AACT,EAAA,IAAI,CAAC,KAAA,CAAM,GAAA,CAAI,UAAA,CAAW,OAAO,GAAG,OAAO,KAAA;AAC3C,EAAA,OACG,KAAA,CAAM,MAAA,IAAU,CAAC,KAAA,CAAM,QAAA,CAAS,KAAK,CAAA,IACrC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,KACvC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,IACvC,KAAA,CAAM,QAAA,IAAY,CAAC,KAAA,CAAM,QAAA,CAAS,OAAO,CAAA;AAE9C;;;AC3FA,IAAM,uBAAA,uBAA8B,GAAA,CAAI;AAAA,EACtC,uBAAA;AAAA,EACA,uBAAA;AAAA,EACA,uBAAA;AAAA,EACA;AACF,CAAC,CAAA;AAyDM,IAAM,qBAAN,MAAyB;AAAA,EACrB,gBAAA,uBAAuB,GAAA,EAAiB;AAAA,EACxC,cAAA,uBAAqB,GAAA,EAAiB;AAAA,EACtC,QAAA;AAAA,EACA,MAAA;AAAA;AAAA,EAET,gBAAA,GAAuC,IAAA;AAAA,EAEvC,WAAA,CAAY,OAAA,GAAqC,EAAC,EAAG;AACnD,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,OAAA;AACxB,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,KAAA;AAAA,EACxB;AAAA;AAAA,EAGA,QAAQ,MAAA,EAA2B;AACjC,IAAA,IAAI,IAAA,CAAK,gBAAA,CAAiB,GAAA,CAAI,MAAM,CAAA,EAAG;AACvC,IAAA,MAAA,CAAO,gBAAA,CAAiB,kBAAA,EAAoB,IAAA,CAAK,YAAY,CAAA;AAC7D,IAAA,MAAA,CAAO,gBAAA,CAAiB,gBAAA,EAAkB,IAAA,CAAK,UAAU,CAAA;AACzD,IAAA,MAAA,CAAO,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,cAAc,CAAA;AACtD,IAAA,IAAA,CAAK,gBAAA,CAAiB,IAAI,MAAM,CAAA;AAAA,EAClC;AAAA;AAAA,EAGA,UAAU,MAAA,EAA2B;AACnC,IAAA,IAAI,CAAC,IAAA,CAAK,gBAAA,CAAiB,MAAA,CAAO,MAAM,CAAA,EAAG;AAC3C,IAAA,MAAA,CAAO,mBAAA,CAAoB,kBAAA,EAAoB,IAAA,CAAK,YAAY,CAAA;AAChE,IAAA,MAAA,CAAO,mBAAA,CAAoB,gBAAA,EAAkB,IAAA,CAAK,UAAU,CAAA;AAC5D,IAAA,MAAA,CAAO,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,cAAc,CAAA;AACzD,IAAA,IAAA,CAAK,cAAA,CAAe,OAAO,MAAM,CAAA;AACjC,IAAA,IAAI,IAAA,CAAK,gBAAA,KAAqB,MAAA,EAAQ,IAAA,CAAK,gBAAA,GAAmB,IAAA;AAAA,EAChE;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,KAAA,MAAW,MAAA,IAAU,KAAK,gBAAA,EAAkB;AAC1C,MAAA,MAAA,CAAO,mBAAA,CAAoB,kBAAA,EAAoB,IAAA,CAAK,YAAY,CAAA;AAChE,MAAA,MAAA,CAAO,mBAAA,CAAoB,gBAAA,EAAkB,IAAA,CAAK,UAAU,CAAA;AAC5D,MAAA,MAAA,CAAO,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,cAAc,CAAA;AAAA,IAC3D;AACA,IAAA,IAAA,CAAK,iBAAiB,KAAA,EAAM;AAC5B,IAAA,IAAA,CAAK,eAAe,KAAA,EAAM;AAC1B,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA;AAAA,EAC1B;AAAA;AAAA,EAGA,YAAY,KAAA,EAAoC;AAC9C,IAAA,OAAO,IAAA,CAAK,cAAA,CAAe,IAAA,GAAO,CAAA,IAAK,OAAO,WAAA,KAAgB,IAAA;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,uBAAuB,KAAA,EAAsC;AAC3D,IAAA,MAAM,YAAY,IAAA,CAAK,gBAAA;AACvB,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA;AACxB,IAAA,IAAI,SAAA,KAAc,IAAA,IAAQ,SAAA,KAAc,KAAA,CAAM,QAAQ,OAAO,KAAA;AAG7D,IAAA,MAAM,YAAY,KAAA,CAAM,SAAA;AACxB,IAAA,OAAO,CAAC,SAAA,IAAa,uBAAA,CAAwB,GAAA,CAAI,SAAS,CAAA;AAAA,EAC5D;AAAA,EAES,YAAA,GAAe,CAAC,KAAA,KAAuB;AAC9C,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA;AACxB,IAAA,IAAI,MAAM,aAAA,EAAe,IAAA,CAAK,cAAA,CAAe,GAAA,CAAI,MAAM,aAAa,CAAA;AACpE,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AAAA,EACvB,CAAA;AAAA,EAES,UAAA,GAAa,CAAC,KAAA,KAAuB;AAC5C,IAAA,IAAI,MAAM,aAAA,EAAe,IAAA,CAAK,cAAA,CAAe,MAAA,CAAO,MAAM,aAAa,CAAA;AACvE,IAAA,IAAA,CAAK,mBAAmB,KAAA,CAAM,MAAA;AAC9B,IAAA,IAAA,CAAK,SAAS,KAAK,CAAA;AAAA,EACrB,CAAA;AAAA;AAAA,EAGS,iBAAiB,MAAY;AACpC,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA;AAAA,EAC1B,CAAA;AACF,CAAA;;;AC9HO,SAAS,UAAA,CACd,YACA,IAAA,EACQ;AACR,EAAA,IAAI,EAAE,IAAA,YAAgB,IAAA,CAAA,EAAO,OAAO,EAAA;AACpC,EAAA,OAAO,WAAW,SAAA,CAAU,CAAC,cAAc,SAAA,CAAU,QAAA,CAAS,IAAI,CAAC,CAAA;AACrE;;;AC3BO,SAAS,yBAAyB,OAAA,EAA+B;AACtE,EAAA,IAAI,QAAA,GAA2B,OAAA,CAAQ,OAAA,CAAQ,oBAAoB,CAAA;AACnE,EAAA,OAAO,QAAA,EAAU;AACf,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,KAAA,KAAU,KAAA,CAAM,OAAA,KAAY,QAAQ,CAAA;AACvF,IAAA,IAAI,CAAC,MAAA,EAAQ,QAAA,CAAS,OAAO,GAAG,OAAO,IAAA;AACvC,IAAA,QAAA,GAAW,QAAA,CAAS,aAAA,EAAe,OAAA,CAAQ,oBAAoB,CAAA,IAAK,IAAA;AAAA,EACtE;AACA,EAAA,OAAO,KAAA;AACT;;;ACKO,IAAM,iBAAN,MAAqB;AAAA;AAAA,EAEjB,SAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMT,YAAY,QAAA,EAA+B;AACzC,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AAAA,EACnB;AAAA;AAAA,EAGA,IAAI,WAAA,GAAsB;AACxB,IAAA,OAAO,IAAA,CAAK,WAAU,CAAE,SAAA,CAAU,CAAC,IAAA,KAAS,IAAA,CAAK,aAAa,CAAC,CAAA;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,SAAA,CACE,KAAA,EACA,OAAA,GAA+D,EAAC,EAC1D;AACN,IAAA,MAAM,EAAE,KAAA,GAAQ,KAAA,EAAM,GAAI,OAAA;AAC1B,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,IAAS,IAAA,CAAK,SAAA,EAAU;AAC9C,IAAA,KAAA,CAAM,OAAA,CAAQ,CAAC,IAAA,EAAM,CAAA,KAAM;AACzB,MAAA,IAAA,CAAK,QAAA,GAAW,CAAA,KAAM,KAAA,GAAQ,CAAA,GAAI,EAAA;AAAA,IACpC,CAAC,CAAA;AACD,IAAA,IAAI,KAAA,EAAO,KAAA,CAAM,KAAK,CAAA,EAAG,KAAA,EAAM;AAAA,EACjC;AACF,CAAA;AAkBO,SAAS,UAAA,CACd,OAAA,EACA,MAAA,EACA,KAAA,EACA,IAAA,EACQ;AACR,EAAA,IAAI,MAAA,KAAW,GAAG,OAAO,EAAA;AACzB,EAAA,MAAM,OAAO,OAAA,GAAU,KAAA;AACvB,EAAA,IAAI,IAAA,KAAS,MAAA,EAAQ,OAAA,CAAQ,IAAA,GAAO,MAAA,IAAU,MAAA;AAC9C,EAAA,OAAO,IAAA,CAAK,IAAI,MAAA,GAAS,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,IAAI,CAAC,CAAA;AAC/C;;;ACzEA,IAAM,gBAAA,GAAmB,CAAC,UAAA,EAAY,QAAQ,CAAA;AA4DvC,IAAM,iBAAA,GAAN,cAAgC,UAAA,CAAwB;AAAA,EAC7D,OAAgB,OAAA,GAAU,CAAC,SAAS,CAAA;AAAA,EACpC,OAAgB,MAAA,GAAS;AAAA,IACvB,WAAA,EAAa,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,YAAA,EAAa;AAAA,IACnD,IAAA,EAAM,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA;AAAK,GACvC;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,WAAW,CAAA;AAAA,EAMpB,OAAA,GAAU,IAAI,cAAA,CAAe,MAAM,KAAK,cAAc,CAAA;AAAA,EACtD,YAAA,GAAe,IAAI,kBAAA,EAAmB;AAAA,EAC/C,SAAA,GAAqC,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOrC,UAAA,GAAa,KAAA;AAAA,EAEJ,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACxD,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACxD,IAAA,IAAA,CAAK,YAAA,CAAa,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA;AACtC,IAAA,IAAI,OAAO,qBAAqB,WAAA,EAAa;AAC3C,MAAA,MAAM,WAAW,IAAI,gBAAA,CAAiB,MAAM,IAAA,CAAK,gBAAgB,CAAA;AACjE,MAAA,QAAA,CAAS,OAAA,CAAQ,KAAK,OAAA,EAAS;AAAA,QAC7B,OAAA,EAAS,IAAA;AAAA,QACT,SAAA,EAAW,IAAA;AAAA,QACX,UAAA,EAAY,IAAA;AAAA,QACZ,eAAA,EAAiB;AAAA,OAClB,CAAA;AAKD,MAAA,KAAA,IACM,QAAA,GAAW,IAAA,CAAK,OAAA,CAAQ,aAAA,EAAe,QAAQ,UAAU,CAAA,IAAK,IAAA,EAClE,QAAA,EACA,WAAW,QAAA,CAAS,aAAA,EAAe,OAAA,CAAQ,UAAU,KAAK,IAAA,EAC1D;AACA,QAAA,QAAA,CAAS,OAAA,CAAQ,UAAU,EAAE,UAAA,EAAY,MAAM,eAAA,EAAiB,CAAC,UAAU,CAAA,EAAG,CAAA;AAAA,MAChF;AACA,MAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AAAA,IACnB;AACA,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAAA,EACpB;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AAC3D,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AAC3D,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,IAAA,CAAK,WAAW,UAAA,EAAW;AAC3B,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,uBAAuB,OAAA,EAA4B;AACjD,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,OAAA,CAAQ,QAAA,GAAW,EAAA;AACnB,IAAA,IAAA,CAAK,cAAA,EAAe;AAAA,EACtB;AAAA;AAAA,EAGA,yBAAA,GAAkC;AAChC,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,cAAA,EAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,UAAU,KAAA,EAA4B;AACpC,IAAA,IAAA,CAAK,eAAe,KAAK,CAAA;AAAA,EAC3B;AAAA;AAAA,EAGS,UAAA,GAAa,CAAC,KAAA,KAA+B;AACpD,IAAA,IAAA,CAAK,eAAe,KAAK,CAAA;AAAA,EAC3B,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,UAAA,GAAa,CAAC,KAAA,KAA4B;AACjD,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,QAAA,CAAS,KAAA,CAAM,MAAM,CAAA;AACxC,IAAA,MAAM,UAAU,KAAA,KAAU,EAAA,GAAK,MAAA,GAAY,IAAA,CAAK,eAAe,KAAK,CAAA;AACpE,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,IAAA,CAAK,YAAA,CAAa,OAAO,CAAA,EAAG;AAC7C,IAAA,IAAA,CAAK,OAAA,CAAQ,UAAU,KAAK,CAAA;AAAA,EAC9B,CAAA;AAAA,EAEA,eAAe,KAAA,EAA4B;AAIzC,IAAA,IAAI,MAAM,gBAAA,EAAkB;AAC5B,IAAA,IAAI,oBAAA,CAAqB,KAAK,CAAA,EAAG;AAEjC,IAAA,IAAI,IAAA,CAAK,YAAA,CAAa,WAAA,CAAY,KAAK,CAAA,EAAG;AAI1C,IAAA,IAAA,CAAK,cAAA,EAAe;AAEpB,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,QAAA,CAAS,KAAA,CAAM,MAAM,CAAA;AACvC,IAAA,IAAI,SAAS,EAAA,EAAI;AAEjB,IAAA,MAAM,QAAA,GAAW,KAAK,gBAAA,KAAqB,UAAA;AAO3C,IAAA,MAAM,GAAA,GAAM,CAAC,QAAA,IAAY,KAAA,CAAM,KAAK,OAAO,CAAA;AAC3C,IAAA,MAAM,UAAA,GAAa,QAAA,GAAW,WAAA,GAAc,GAAA,GAAM,WAAA,GAAc,YAAA;AAChE,IAAA,MAAM,WAAA,GAAc,QAAA,GAAW,SAAA,GAAY,GAAA,GAAM,YAAA,GAAe,WAAA;AAEhE,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI,KAAA,CAAM,QAAQ,UAAA,EAAY;AAC5B,MAAA,MAAA,GAAS,IAAA,CAAK,cAAA,CAAe,IAAA,EAAM,CAAC,CAAA;AAAA,IACtC,CAAA,MAAA,IAAW,KAAA,CAAM,GAAA,KAAQ,WAAA,EAAa;AACpC,MAAA,MAAA,GAAS,IAAA,CAAK,cAAA,CAAe,IAAA,EAAM,EAAE,CAAA;AAAA,IACvC,CAAA,MAAA,IAAW,KAAA,CAAM,GAAA,KAAQ,MAAA,EAAQ;AAC/B,MAAA,MAAA,GAAS,IAAA,CAAK,mBAAmB,CAAC,CAAA;AAAA,IACpC,CAAA,MAAA,IAAW,KAAA,CAAM,GAAA,KAAQ,KAAA,EAAO;AAC9B,MAAA,MAAM,YAAY,IAAA,CAAK,kBAAA;AACvB,MAAA,MAAA,GAAS,SAAA,CAAU,SAAA,CAAU,MAAA,GAAS,CAAC,CAAA;AAAA,IACzC,CAAA,MAAO;AACL,MAAA;AAAA,IACF;AAEA,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,IAAI,MAAA,EAAQ,IAAA,CAAK,OAAA,CAAQ,SAAA,CAAU,IAAA,CAAK,cAAA,CAAe,OAAA,CAAQ,MAAM,CAAA,EAAG,EAAE,KAAA,EAAO,IAAA,EAAM,CAAA;AAAA,EACzF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,cAAA,GAAuB;AACrB,IAAA,MAAM,MAAA,GAAS,KAAK,OAAA,CAAQ,WAAA;AAC5B,IAAA,MAAM,WAAW,MAAA,KAAW,EAAA,GAAK,IAAA,GAAO,IAAA,CAAK,eAAe,MAAM,CAAA;AAClE,IAAA,IAAI,QAAA,IAAY,IAAA,CAAK,YAAA,CAAa,QAAQ,CAAA,EAAG;AAC3C,MAAA,IAAA,CAAK,OAAA,CAAQ,UAAU,MAAM,CAAA;AAC7B,MAAA;AAAA,IACF;AACA,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,kBAAA,CAAmB,CAAC,CAAA;AACvC,IAAA,IAAA,CAAK,OAAA,CAAQ,UAAU,KAAA,GAAQ,IAAA,CAAK,eAAe,OAAA,CAAQ,KAAK,IAAI,EAAE,CAAA;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,cAAA,CAAe,WAAmB,KAAA,EAAwC;AACxE,IAAA,MAAM,WAAW,IAAA,CAAK,cAAA;AACtB,IAAA,MAAM,SAAS,QAAA,CAAS,MAAA;AACxB,IAAA,MAAM,IAAA,GAAmB,IAAA,CAAK,SAAA,GAAY,MAAA,GAAS,OAAA;AACnD,IAAA,IAAI,KAAA,GAAQ,SAAA;AACZ,IAAA,KAAA,IAAS,IAAA,GAAO,CAAA,EAAG,IAAA,GAAO,MAAA,EAAQ,IAAA,EAAA,EAAQ;AACxC,MAAA,MAAM,IAAA,GAAO,UAAA,CAAW,KAAA,EAAO,MAAA,EAAQ,OAAO,IAAI,CAAA;AAClD,MAAA,IAAI,SAAS,KAAA,EAAO;AACpB,MAAA,KAAA,GAAQ,IAAA;AACR,MAAA,MAAM,SAAA,GAAY,SAAS,KAAK,CAAA;AAChC,MAAA,IAAI,SAAA,IAAa,IAAA,CAAK,YAAA,CAAa,SAAS,GAAG,OAAO,SAAA;AAAA,IACxD;AACA,IAAA,MAAM,MAAA,GAAS,SAAS,SAAS,CAAA;AACjC,IAAA,OAAO,MAAA,IAAU,KAAK,YAAA,CAAa,MAAM,IAAI,MAAA,GAAS,IAAA,CAAK,mBAAmB,CAAC,CAAA;AAAA,EACjF;AAAA;AAAA,EAGA,SAAS,MAAA,EAAoC;AAC3C,IAAA,OAAO,UAAA,CAAW,IAAA,CAAK,cAAA,EAAgB,MAAM,CAAA;AAAA,EAC/C;AAAA;AAAA,EAGA,IAAI,kBAAA,GAAoC;AACtC,IAAA,OAAO,IAAA,CAAK,eAAe,MAAA,CAAO,CAAC,YAAY,IAAA,CAAK,YAAA,CAAa,OAAO,CAAC,CAAA;AAAA,EAC3E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,aAAa,OAAA,EAA+B;AAC1C,IAAA,IAAI,IAAA,CAAK,SAAA,CAAU,OAAO,CAAA,EAAG,OAAO,KAAA;AACpC,IAAA,IAAI,EAAE,UAAA,IAAc,OAAA,CAAA,EAAU,OAAO,IAAA;AACrC,IAAA,IAAI,OAAA,CAAQ,QAAA,KAAa,IAAA,EAAM,OAAO,KAAA;AACtC,IAAA,OAAO,CAAC,yBAAyB,OAAO,CAAA;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,UAAU,OAAA,EAA+B;AACvC,IAAA,IAAI,IAAA,GAA2B,OAAA;AAC/B,IAAA,OAAO,IAAA,IAAQ,IAAA,KAAS,IAAA,CAAK,OAAA,EAAS;AACpC,MAAA,IAAI,IAAA,CAAK,YAAA,CAAa,QAAQ,CAAA,EAAG,OAAO,IAAA;AACxC,MAAA,IAAA,GAAO,IAAA,CAAK,aAAA;AAAA,IACd;AACA,IAAA,OAAO,KAAA;AAAA,EACT;AACF","file":"toolbar_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Minimal `input` shape needed to tell a composition echo from a fresh edit. */\nexport interface ConfirmedInputSignal {\n readonly target: EventTarget | null;\n readonly inputType?: string;\n}\n\n/**\n * The `inputType` values an engine reports on an `input` that belongs to a\n * composition rather than to an edit the user just made.\n */\nconst COMPOSITION_INPUT_TYPES = new Set([\n \"insertCompositionText\",\n \"insertFromComposition\",\n \"deleteCompositionText\",\n \"deleteByComposition\",\n]);\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * The tracker also owns the other half of that lifecycle: **the `input` some\n * engines send after `compositionend` to echo the text just confirmed.** A\n * consumer that ran `onEnd` and then handled the echo would commit the same\n * confirmation twice, so {@link CompositionTracker.consumesConfirmedInput} tells\n * the echo apart and the consumer returns early on it.\n *\n * **What the echo is not, is the harder half.** Not every engine sends one —\n * Chromium ends a confirmation at `compositionend` with no `input` after it — so\n * a window that waits for the echo and only closes on the next key would swallow\n * the first edit that arrives without one: dictation, autofill, a drop,\n * `insertText`. Two things keep that from happening. The window closes on the\n * next anything — a key, a new composition, any `input`, losing the field — and\n * an `input` that reports an `inputType` outside {@link COMPOSITION_INPUT_TYPES}\n * is an edit, so it is never folded. Only an engine that reports no `inputType`\n * at all is left relying on the window alone.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onInput(event: InputEvent): void {\n * if (this.#composition.consumesConfirmedInput(event)) return;\n * if (this.#composition.isComposing(event)) return;\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n /** The field whose confirming `input` is still owed, while the window is open. */\n #confirmedTarget: EventTarget | null = null;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n target.addEventListener(\"keydown\", this.#handleKeydown);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n target.removeEventListener(\"keydown\", this.#handleKeydown);\n this.#activeTargets.delete(target);\n if (this.#confirmedTarget === target) this.#confirmedTarget = null;\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n target.removeEventListener(\"keydown\", this.#handleKeydown);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n this.#confirmedTarget = null;\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n /**\n * Whether `event` is the `input` echoing the composition just confirmed.\n *\n * Asking closes the window either way, so one confirmation is folded at most\n * once and a consumer asks once per `input`.\n */\n consumesConfirmedInput(event: ConfirmedInputSignal): boolean {\n const confirmed = this.#confirmedTarget;\n this.#confirmedTarget = null;\n if (confirmed === null || confirmed !== event.target) return false;\n // An engine that reports no kind at all leaves `inputType` empty, and the\n // window is then the only signal there is.\n const inputType = event.inputType;\n return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);\n }\n\n readonly #handleStart = (event: Event): void => {\n this.#confirmedTarget = null;\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#confirmedTarget = event.target;\n this.#onEnd?.(event);\n };\n\n /** A key on an observed field opens an edit of its own, so no echo is owed. */\n readonly #handleKeydown = (): void => {\n this.#confirmedTarget = null;\n };\n}\n","/**\n * Resolves which of a controller's elements owns a node.\n *\n * A delegated listener hears events from a whole subtree, so the handler's first\n * job is almost always the same question: which item, handle, or control does\n * this `event.target` belong to? The same question comes up for a\n * `MutationRecord.target`, for `document.activeElement`, and for a\n * `relatedTarget` on the way out of a hover region.\n *\n * The answer is `Node.contains()`, which is **inclusive** — an element contains\n * itself — so testing the candidate for identity as well would be redundant.\n *\n * **The guard is what makes this safe to call with a raw event target.**\n * `event.target` is typed `EventTarget | null`, and `contains()` takes a `Node?`:\n * browsers throw `TypeError` for anything else, and `window` — the everyday\n * `EventTarget` that is not a `Node` — is what an event dispatched at it carries.\n * Narrowing here means a caller never has to cast, and the rule cannot drift\n * between the places that ask the question.\n *\n * Scope stays with the caller. These helpers say *which candidate owns the node*,\n * not *whether the node belongs to this controller at all* — a component with\n * nested instances of itself decides that first (by comparing the closest\n * annotated ancestor) and passes the candidates it owns.\n */\n\n/**\n * Index in `candidates` of the first one that is, or contains, `node`.\n *\n * `-1` when none does, when `node` is absent, and when it is an `EventTarget`\n * that is not a `Node`. Candidates are tested in array order, so a nested pair\n * resolves to whichever the caller listed first.\n */\nexport function ownerIndex<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): number {\n if (!(node instanceof Node)) return -1;\n return candidates.findIndex((candidate) => candidate.contains(node));\n}\n\n/**\n * The first candidate that is, or contains, `node`; `null` when none does.\n *\n * A miss indexes the array at `-1`, which reads as `undefined` and lands on the\n * same `null` the absent cases produce.\n */\nexport function ownerOf<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): T | null {\n return candidates[ownerIndex(candidates, node)] ?? null;\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper. A caller\n * that already captured one event-scoped snapshot may pass it to\n * {@link RovingTabindex.setActive}, avoiding a second live-target scan while\n * preserving one coherent collection for the whole operation.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item,\n * and `items` to reuse an event-scoped collection snapshot.\n */\n setActive(\n index: number,\n options: { focus?: boolean; items?: readonly HTMLElement[] } = {},\n ): void {\n const { focus = false } = options;\n const items = options.items ?? this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\nimport { ownerIndex } from \"../utils/event_owner\";\nimport { inheritsFieldsetDisabled } from \"../utils/focus_candidate\";\nimport { isRtl } from \"../utils/logical_scroll\";\nimport { RovingTabindex, type RovingWrap, rovingMove } from \"../utils/roving_tabindex\";\n\n/**\n * Attributes that flip a control between navigable and not. Watched on the whole\n * subtree because a *sibling* controller (an editor toggling `disabled` on its\n * command buttons, a form enabling a fieldset) is what usually changes them.\n * Watched together with `childList`, which covers controls added or removed at\n * runtime without depending on Stimulus's asynchronous target callbacks.\n * `tabindex` is deliberately absent: the controller writes it itself, so\n * observing it would feed back into the observer.\n */\nconst STATE_ATTRIBUTES = [\"disabled\", \"hidden\"];\n\n/**\n * Headless, accessible toolbar behavior.\n *\n * Markup contract (identifier: `stimeo--toolbar`):\n * <div data-controller=\"stimeo--toolbar\" role=\"toolbar\" aria-label=\"Text formatting\"\n * data-stimeo--toolbar-orientation-value=\"horizontal\">\n * <button type=\"button\" data-stimeo--toolbar-target=\"control\">Bold</button>\n * <!-- more controls -->\n * </div>\n *\n * Implements the WAI-ARIA APG **Toolbar** pattern: the group is a single Tab\n * stop (roving tabindex) and the arrow keys move focus between controls. Each\n * control's own function (press, toggle, open a menu) stays with that element or\n * its own controller — the toolbar only owns navigation.\n *\n * @remarks\n * Behavior only. The roving mechanics are delegated to a shared roving-tabindex helper;\n * orientation and wrap policy stay here per the APG (they differ per widget).\n * ARIA (`role=\"toolbar\"`, the accessible name, and `aria-orientation=\"vertical\"`\n * when the axis is vertical) is the author's, exactly as with `role`.\n *\n * Behavior provided:\n * - Exactly one *navigable* control is tabbable (`tabindex=\"0\"`); the rest are\n * `-1`. The invariant is re-established on connect, on control add/remove,\n * whenever `disabled` / `hidden` changes under the toolbar, and whenever an\n * enclosing `<fieldset>` is disabled or re-enabled — disabling the control that\n * held the Tab stop must never make the whole group unreachable by Tab, and\n * unlocking the surrounding form must bring it back.\n * - `ArrowRight`/`ArrowLeft` (horizontal) or `ArrowDown`/`ArrowUp` (vertical)\n * move focus to the next/previous navigable control; `Home`/`End` to the\n * first/last. `aria-disabled=\"true\"` controls remain in that roving order and\n * can be both origins and destinations; only their activation is suppressed\n * by the control itself or its owning behavior. Native `disabled` and `hidden`\n * controls are excluded.\n * - With `wrap=true` movement cycles past the ends; with `wrap=false` it stops.\n * - `orientation` accepts `horizontal` (default) and `vertical`; any other value\n * degrades to `horizontal`. There is no `both` — for two-axis navigation use\n * `stimeo--roving`, whose same-named Value does accept it.\n * - Returning focus from outside lands on the most recently *active* control —\n * the last one moved to by key, click, or programmatic `focus()`, because that\n * is the one left tabbable (`focusin` keeps the Tab stop in sync).\n *\n * Keydown is **delegated on the container**, so controls added at runtime need\n * no per-element `data-action` (Stimulus binds those asynchronously, which is\n * unreliable for appended nodes). A `data-action=\"keydown->stimeo--toolbar#onKeydown\"`\n * on each control stays supported; it does not double-move, because the second\n * pass sees `defaultPrevented`.\n *\n * Initial Tab stop: to choose it, write `tabindex` on **every** control (exactly\n * one `0`, the rest `-1`). Natively focusable elements are `tabindex=\"0\"`\n * effectively when the attribute is absent, so annotating only the intended\n * entry point makes the *first* control win instead.\n *\n * Known constraint: a toolbar that contains a text input competes with it for\n * the arrow keys — the APG expects the toolbar to own them. Keep free-text\n * fields outside the toolbar (IME composition is yielded to, but caret movement\n * is not).\n */\nexport class ToolbarController extends Controller<HTMLElement> {\n static override targets = [\"control\"];\n static override values = {\n orientation: { type: String, default: \"horizontal\" },\n wrap: { type: Boolean, default: true },\n };\n static actions = [\"onKeydown\"] as const;\n\n declare readonly controlTargets: HTMLElement[];\n declare orientationValue: string;\n declare wrapValue: boolean;\n\n readonly #roving = new RovingTabindex(() => this.controlTargets);\n readonly #composition = new CompositionTracker();\n #observer: MutationObserver | null = null;\n /**\n * Live between `connect()` and `disconnect()`. Stimulus reports the *initial*\n * targets before `connect()` and re-reports them as disconnected after\n * `disconnect()`; gating on this keeps the target callbacks from clobbering\n * the authored Tab stop on mount and from resurrecting one after teardown.\n */\n #connected = false;\n\n override connect(): void {\n this.#ensureTabStop();\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n this.element.addEventListener(\"focusin\", this.#onFocusin);\n this.#composition.observe(this.element);\n if (typeof MutationObserver !== \"undefined\") {\n const observer = new MutationObserver(() => this.#ensureTabStop());\n observer.observe(this.element, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: STATE_ATTRIBUTES,\n });\n // A `<fieldset disabled>` above the root disables the toolbar's form\n // controls from outside the observed subtree, so each enclosing fieldset's\n // own `disabled` is watched too. The walk starts at the parent: re-observing\n // the root would replace its options and lose the subtree registration.\n for (\n let fieldset = this.element.parentElement?.closest(\"fieldset\") ?? null;\n fieldset;\n fieldset = fieldset.parentElement?.closest(\"fieldset\") ?? null\n ) {\n observer.observe(fieldset, { attributes: true, attributeFilter: [\"disabled\"] });\n }\n this.#observer = observer;\n }\n this.#connected = true;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n this.element.removeEventListener(\"focusin\", this.#onFocusin);\n this.#composition.disconnect();\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /**\n * A control added at runtime is dropped out of the Tab sequence first — a\n * fresh `<button>` is tabbable by default, which would make the group two Tab\n * stops — and then the lone stop is re-established.\n */\n controlTargetConnected(control: HTMLElement): void {\n if (!this.#connected) return;\n control.tabIndex = -1;\n this.#ensureTabStop();\n }\n\n /** Removing the control that held the Tab stop must not orphan the group. */\n controlTargetDisconnected(): void {\n if (!this.#connected) return;\n this.#ensureTabStop();\n }\n\n /**\n * Arrow/Home/End move focus and the single tab stop.\n *\n * Binding this per control with `data-action` is **optional** — the same\n * handling runs from the container's delegated listener. It stays a declared\n * action so per-control wiring keeps working; wiring both does not\n * double-move, because the delegated pass then yields on `defaultPrevented`.\n */\n onKeydown(event: KeyboardEvent): void {\n this.#handleKeydown(event);\n }\n\n /** Delegated counterpart of {@link onKeydown}; bound on the container. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n this.#handleKeydown(event);\n };\n\n /**\n * Syncs the Tab stop to a control focused by other means (click, programmatic\n * `focus()`) so Tab re-entry returns there. A non-navigable control is ignored:\n * the lone Tab stop must never sit on something the user cannot operate.\n */\n readonly #onFocusin = (event: FocusEvent): void => {\n const index = this.#indexOf(event.target);\n const control = index === -1 ? undefined : this.controlTargets[index];\n if (!control || !this.#isNavigable(control)) return;\n this.#roving.setActive(index);\n };\n\n #handleKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO move the roving focus. This is what makes the\n // per-control `data-action` and the delegated listener idempotent.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n // Arrow/Home/End belong to the IME while a composition is in flight.\n if (this.#composition.isComposing(event)) return;\n // Re-assert the invariant before acting: the attribute observer runs async\n // (and may be absent), so a Tab stop invalidated moments ago can still be in\n // place here.\n this.#ensureTabStop();\n\n const from = this.#indexOf(event.target);\n if (from === -1) return;\n\n const vertical = this.orientationValue === \"vertical\";\n // Logical, not physical. APG defines these as \"next / previous control\", and\n // says a vertical arrangement swaps in Down/Up for the same meaning — so the\n // pair is one axis's spelling of an order, and the order reverses with the\n // writing direction. Read from the controller element: the container is what\n // lays the items out, and a child may carry its own `dir` (an LTR input\n // inside an RTL form is ordinary authoring).\n const rtl = !vertical && isRtl(this.element);\n const forwardKey = vertical ? \"ArrowDown\" : rtl ? \"ArrowLeft\" : \"ArrowRight\";\n const backwardKey = vertical ? \"ArrowUp\" : rtl ? \"ArrowRight\" : \"ArrowLeft\";\n\n let target: HTMLElement | undefined;\n if (event.key === forwardKey) {\n target = this.#nextNavigable(from, 1);\n } else if (event.key === backwardKey) {\n target = this.#nextNavigable(from, -1);\n } else if (event.key === \"Home\") {\n target = this.#navigableControls[0];\n } else if (event.key === \"End\") {\n const navigable = this.#navigableControls;\n target = navigable[navigable.length - 1];\n } else {\n return;\n }\n\n event.preventDefault();\n if (target) this.#roving.setActive(this.controlTargets.indexOf(target), { focus: true });\n }\n\n /**\n * Re-establishes the single Tab stop: keep the current one while it is still\n * navigable, else hand it to the first navigable control. `-1` (no Tab stop at\n * all) is reached only when every control is unavailable, and is recovered\n * from as soon as one becomes navigable again. Idempotent by construction, so\n * connect, the target callbacks, the observer, and keydown can all call it.\n */\n #ensureTabStop(): void {\n const active = this.#roving.activeIndex;\n const activeEl = active === -1 ? null : this.controlTargets[active];\n if (activeEl && this.#isNavigable(activeEl)) {\n this.#roving.setActive(active);\n return;\n }\n const first = this.#navigableControls[0];\n this.#roving.setActive(first ? this.controlTargets.indexOf(first) : -1);\n }\n\n /**\n * First navigable control strictly in the `delta` direction from `fromIndex`,\n * scanning the **full** control list rather than the navigable subset so a\n * non-navigable origin is never a dead end. When the direction yields nothing\n * the origin keeps focus; when the origin itself is not navigable the first\n * navigable control is used instead, so an arrow key always escapes.\n */\n #nextNavigable(fromIndex: number, delta: number): HTMLElement | undefined {\n const controls = this.controlTargets;\n const length = controls.length;\n const wrap: RovingWrap = this.wrapValue ? \"wrap\" : \"clamp\";\n let index = fromIndex;\n for (let step = 0; step < length; step++) {\n const next = rovingMove(index, length, delta, wrap);\n if (next === index) break; // clamped at an end (or a single control)\n index = next;\n const candidate = controls[index];\n if (candidate && this.#isNavigable(candidate)) return candidate;\n }\n const origin = controls[fromIndex];\n return origin && this.#isNavigable(origin) ? origin : this.#navigableControls[0];\n }\n\n /** Index in `controlTargets` of the control owning `target` (it or a descendant). */\n #indexOf(target: EventTarget | null): number {\n return ownerIndex(this.controlTargets, target);\n }\n\n /** Controls eligible for the roving tab stop (excludes native disabled / hidden). */\n get #navigableControls(): HTMLElement[] {\n return this.controlTargets.filter((control) => this.#isNavigable(control));\n }\n\n /**\n * A control can hold the tab stop unless it is `hidden` or a disabled form\n * control. `aria-disabled` remains navigable per the APG; activation suppression\n * belongs to the control itself or its owning behavior. Native disabledness is\n * read from the `disabled` property, narrowed by an `in` check rather than\n * asserted since `<a>` and `<div role=\"button\">` are legitimate controls that\n * are unaffected by a disabled fieldset. CSS-only visibility cannot be detected\n * headlessly and stays the consumer's responsibility.\n */\n #isNavigable(control: HTMLElement): boolean {\n if (this.#isHidden(control)) return false;\n if (!(\"disabled\" in control)) return true;\n if (control.disabled === true) return false;\n return !inheritsFieldsetDisabled(control);\n }\n\n /**\n * Whether `control`, or anything between it and the toolbar root, is `hidden`.\n *\n * Reading only the control's own attribute lets an invisible control hold the\n * single Tab stop, which takes the *whole* toolbar out of the Tab sequence — an\n * ordinary `hidden` wrapper is enough. The walk stops at the root: a toolbar\n * inside a `hidden` region is already out of the page's Tab order.\n */\n #isHidden(control: HTMLElement): boolean {\n let node: HTMLElement | null = control;\n while (node && node !== this.element) {\n if (node.hasAttribute(\"hidden\")) return true;\n node = node.parentElement;\n }\n return false;\n }\n}\n"]}
|
|
@@ -43,6 +43,13 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
43
43
|
* scroll-parent ancestor (or the window) hides the tooltip, the usual convention for
|
|
44
44
|
* anchored popups and useful for focus-triggered tooltips that a pointer-leave cannot
|
|
45
45
|
* dismiss. Off by default.
|
|
46
|
+
* - Each move of the shown state is reported: `stimeo--tooltip:open` and
|
|
47
|
+
* `stimeo--tooltip:close` dispatch `{ reason: StateReason }`, after the state
|
|
48
|
+
* attributes are written. The reason is taken from the event that started the
|
|
49
|
+
* move and survives the delay, so a `mouseleave` that hides `hideDelay` later
|
|
50
|
+
* still reports `"pointer"`. Both are informational, so neither is
|
|
51
|
+
* cancelable. A call that leaves the state where it already was, the
|
|
52
|
+
* normalization in {@link connect}, and {@link disconnect} are all silent.
|
|
46
53
|
*/
|
|
47
54
|
declare class TooltipController extends Controller<HTMLElement> {
|
|
48
55
|
#private;
|
|
@@ -62,6 +69,7 @@ declare class TooltipController extends Controller<HTMLElement> {
|
|
|
62
69
|
};
|
|
63
70
|
};
|
|
64
71
|
static actions: readonly ["hide", "show"];
|
|
72
|
+
static events: readonly ["close", "open"];
|
|
65
73
|
readonly contentTarget: HTMLElement;
|
|
66
74
|
readonly hasContentTarget: boolean;
|
|
67
75
|
readonly showDelayValue: number;
|
|
@@ -164,6 +164,16 @@ function isScrollable(element) {
|
|
|
164
164
|
return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// src/utils/state_reason.ts
|
|
168
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
169
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
170
|
+
function stateReasonFor(event) {
|
|
171
|
+
if (!event) return "api";
|
|
172
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
173
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
174
|
+
return "user";
|
|
175
|
+
}
|
|
176
|
+
|
|
167
177
|
// src/controllers/tooltip_controller.ts
|
|
168
178
|
var TooltipController = class extends Controller {
|
|
169
179
|
static targets = ["trigger", "content"];
|
|
@@ -173,6 +183,7 @@ var TooltipController = class extends Controller {
|
|
|
173
183
|
closeOnScroll: { type: Boolean, default: false }
|
|
174
184
|
};
|
|
175
185
|
static actions = ["hide", "show"];
|
|
186
|
+
static events = ["close", "open"];
|
|
176
187
|
/** Registry whose pending timers are cancelled individually with their guard ids. */
|
|
177
188
|
#timers = new SafeTimeout();
|
|
178
189
|
/** Escape-stack membership while shown; the shared resolver dismisses via it. */
|
|
@@ -186,15 +197,19 @@ var TooltipController = class extends Controller {
|
|
|
186
197
|
#pointerActive = false;
|
|
187
198
|
/** Cleanup for the dismiss-on-scroll listeners while shown, or `null`. */
|
|
188
199
|
#stopScrollDismiss = null;
|
|
200
|
+
/** Whether state moves are reported: set once `connect()` settled the baseline. */
|
|
201
|
+
#reporting = false;
|
|
189
202
|
/** Starts hidden with no stale timer or interaction state from a prior connection. */
|
|
190
203
|
connect() {
|
|
191
204
|
this.#cancelShow();
|
|
192
205
|
this.#cancelHide();
|
|
193
206
|
this.#resetInteractionState();
|
|
194
|
-
this.#conceal();
|
|
207
|
+
this.#conceal("api");
|
|
208
|
+
this.#reporting = true;
|
|
195
209
|
}
|
|
196
210
|
/** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
|
|
197
211
|
disconnect() {
|
|
212
|
+
this.#reporting = false;
|
|
198
213
|
this.#cancelShow();
|
|
199
214
|
this.#cancelHide();
|
|
200
215
|
this.#resetInteractionState();
|
|
@@ -204,20 +219,22 @@ var TooltipController = class extends Controller {
|
|
|
204
219
|
}
|
|
205
220
|
/** Shows after `showDelay`, recording the focus/pointer reason supplied by an action event. */
|
|
206
221
|
show(event) {
|
|
222
|
+
const reason = stateReasonFor(event);
|
|
207
223
|
this.#activateInteraction(event);
|
|
208
224
|
this.#cancelHide();
|
|
209
225
|
if (this.#isVisible || this.#pendingShow !== null) return;
|
|
210
226
|
if (this.showDelayValue <= 0) {
|
|
211
|
-
this.#reveal();
|
|
227
|
+
this.#reveal(reason);
|
|
212
228
|
return;
|
|
213
229
|
}
|
|
214
230
|
this.#pendingShow = this.#timers.set(() => {
|
|
215
231
|
this.#pendingShow = null;
|
|
216
|
-
this.#reveal();
|
|
232
|
+
this.#reveal(reason);
|
|
217
233
|
}, this.showDelayValue);
|
|
218
234
|
}
|
|
219
235
|
/** Hides after `hideDelay` once no focus/pointer reason remains; eventless calls are explicit. */
|
|
220
236
|
hide(event) {
|
|
237
|
+
const reason = stateReasonFor(event);
|
|
221
238
|
const interactionEnded = this.#deactivateInteraction(event);
|
|
222
239
|
if (interactionEnded && this.#hasActiveInteraction) {
|
|
223
240
|
this.#cancelHide();
|
|
@@ -226,38 +243,47 @@ var TooltipController = class extends Controller {
|
|
|
226
243
|
this.#cancelShow();
|
|
227
244
|
if (!this.#isVisible || this.#pendingHide !== null) return;
|
|
228
245
|
if (this.hideDelayValue <= 0) {
|
|
229
|
-
this.#conceal();
|
|
246
|
+
this.#conceal(reason);
|
|
230
247
|
return;
|
|
231
248
|
}
|
|
232
249
|
this.#pendingHide = this.#timers.set(() => {
|
|
233
250
|
this.#pendingHide = null;
|
|
234
|
-
this.#conceal();
|
|
251
|
+
this.#conceal(reason);
|
|
235
252
|
}, this.hideDelayValue);
|
|
236
253
|
}
|
|
237
|
-
/**
|
|
238
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Reveals the content, reports a move, and joins the Escape stack / scroll watcher.
|
|
256
|
+
*
|
|
257
|
+
* @stimeoRuntimeOnly `closeOnScroll` decides whether this reveal wires the scroll dismissal; what
|
|
258
|
+
* is shown does not depend on it.
|
|
259
|
+
*/
|
|
260
|
+
#reveal(reason) {
|
|
239
261
|
if (!this.hasContentTarget) return;
|
|
262
|
+
const was = this.#isVisible;
|
|
240
263
|
this.contentTarget.hidden = false;
|
|
241
264
|
this.contentTarget.setAttribute("data-state", "open");
|
|
242
|
-
this.#
|
|
265
|
+
if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
266
|
+
this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss("escape") });
|
|
243
267
|
if (this.closeOnScrollValue && !this.#stopScrollDismiss) {
|
|
244
|
-
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss());
|
|
268
|
+
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss("scroll"));
|
|
245
269
|
}
|
|
246
270
|
}
|
|
247
|
-
/** Hides the content and leaves the Escape stack /
|
|
248
|
-
#conceal() {
|
|
271
|
+
/** Hides the content, reports a move, and leaves the Escape stack / scroll watcher. */
|
|
272
|
+
#conceal(reason) {
|
|
273
|
+
const was = this.#isVisible;
|
|
249
274
|
this.#escapeLayer.deactivate();
|
|
250
275
|
this.#stopScrollDismiss?.();
|
|
251
276
|
this.#stopScrollDismiss = null;
|
|
252
277
|
if (!this.hasContentTarget) return;
|
|
253
278
|
this.contentTarget.hidden = true;
|
|
254
279
|
this.contentTarget.setAttribute("data-state", "closed");
|
|
280
|
+
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
255
281
|
}
|
|
256
282
|
/** Cancels pending timers and conceals immediately (shared Escape / scroll path). */
|
|
257
|
-
#dismiss() {
|
|
283
|
+
#dismiss(reason) {
|
|
258
284
|
this.#cancelShow();
|
|
259
285
|
this.#cancelHide();
|
|
260
|
-
this.#conceal();
|
|
286
|
+
this.#conceal(reason);
|
|
261
287
|
}
|
|
262
288
|
/** Records the modality whose enter/focus event requires the tooltip to stay visible. */
|
|
263
289
|
#activateInteraction(event) {
|