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
|
@@ -30,7 +30,7 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
30
30
|
* presentation styles.
|
|
31
31
|
* - `F6` cycles focus through the panes, which the pattern lists as optional.
|
|
32
32
|
*
|
|
33
|
-
* `change`
|
|
33
|
+
* `change` and `reconcile` dispatch `{ value: number, fraction: number }`.
|
|
34
34
|
*
|
|
35
35
|
* @remarks
|
|
36
36
|
* Behavior only. The controller adjusts the CSS custom property on the root element,
|
|
@@ -42,6 +42,20 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
42
42
|
* to `1` — so an unreadable declaration narrows what the widget can do without
|
|
43
43
|
* ever publishing `NaN` to CSS or to assistive tech.
|
|
44
44
|
*
|
|
45
|
+
* The Values are inputs. A declared `value` outside the range stays in its
|
|
46
|
+
* attribute as the page wrote it; the separator's ARIA, the fraction and both
|
|
47
|
+
* events carry the clamped position instead, and only a move the user makes
|
|
48
|
+
* writes `value`. `change` reports a key press, drag or toggle that moved the
|
|
49
|
+
* position from the one last confirmed — a drag once, when it ends — and
|
|
50
|
+
* nothing for one that leaves it there: a key at an edge, a press released
|
|
51
|
+
* without moving, a drag released where it started. When a Value the page
|
|
52
|
+
* changes at runtime — a Turbo morph or application code — moves the position,
|
|
53
|
+
* `reconcile` reports it once per batch with the same detail, so a consumer can
|
|
54
|
+
* tell its user's move from the page's. Connecting reports neither. The
|
|
55
|
+
* position last confirmed is the one connecting published, moved by each
|
|
56
|
+
* `change` and `reconcile`; the positions a drag paints on the way do not move
|
|
57
|
+
* it.
|
|
58
|
+
*
|
|
45
59
|
* `aria-orientation` is the author's, and its absence means `horizontal` — the
|
|
46
60
|
* value ARIA gives a separator that does not say. Reading any other default here
|
|
47
61
|
* would make the announced axis and the answering arrow keys disagree.
|
|
@@ -68,7 +82,7 @@ declare class ResizableController extends Controller<HTMLElement> {
|
|
|
68
82
|
};
|
|
69
83
|
};
|
|
70
84
|
static actions: readonly ["onKeydown", "onPointerDown", "toggle"];
|
|
71
|
-
static events: readonly ["change"];
|
|
85
|
+
static events: readonly ["change", "reconcile"];
|
|
72
86
|
readonly primaryTarget: HTMLElement;
|
|
73
87
|
readonly secondaryTarget: HTMLElement;
|
|
74
88
|
readonly separatorTarget: HTMLElement;
|
|
@@ -107,7 +107,20 @@ var TabindexLoan = class {
|
|
|
107
107
|
}
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
// src/utils/transient_hooks.ts
|
|
111
|
+
var TransientHooks = class {
|
|
112
|
+
#attributes;
|
|
113
|
+
constructor(options) {
|
|
114
|
+
this.#attributes = options.attributes;
|
|
115
|
+
}
|
|
116
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
117
|
+
reset(element) {
|
|
118
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
110
122
|
// src/controllers/resizable_controller.ts
|
|
123
|
+
var TRANSIENT = new TransientHooks({ attributes: ["data-dragging"] });
|
|
111
124
|
var DEFAULT_MIN = 0;
|
|
112
125
|
var DEFAULT_MAX = 100;
|
|
113
126
|
var DEFAULT_VALUE = 50;
|
|
@@ -121,9 +134,20 @@ var ResizableController = class extends Controller {
|
|
|
121
134
|
value: { type: Number, default: DEFAULT_VALUE }
|
|
122
135
|
};
|
|
123
136
|
static actions = ["onKeydown", "onPointerDown", "toggle"];
|
|
124
|
-
static events = ["change"];
|
|
137
|
+
static events = ["change", "reconcile"];
|
|
125
138
|
/** Where the divider sat before the current collapse; `null` when unknown. */
|
|
126
139
|
#valueBeforeCollapse = null;
|
|
140
|
+
/**
|
|
141
|
+
* The position last painted: moved by each move the user makes — every step of
|
|
142
|
+
* a drag included — and by each repaint that finds a page-driven move.
|
|
143
|
+
*/
|
|
144
|
+
#painted = DEFAULT_VALUE;
|
|
145
|
+
/**
|
|
146
|
+
* The position last confirmed: the one connecting published, then moved by
|
|
147
|
+
* each `change` and `reconcile`. A drag moves the painted position step by
|
|
148
|
+
* step and this one only when it ends.
|
|
149
|
+
*/
|
|
150
|
+
#confirmed = DEFAULT_VALUE;
|
|
127
151
|
/** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
|
|
128
152
|
#dragAbort = null;
|
|
129
153
|
/** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */
|
|
@@ -133,8 +157,9 @@ var ResizableController = class extends Controller {
|
|
|
133
157
|
/** Folds the range and position inputs of one morph batch into a single paint. */
|
|
134
158
|
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
135
159
|
connect() {
|
|
136
|
-
this.element
|
|
160
|
+
TRANSIENT.reset(this.element);
|
|
137
161
|
this.#repaint.activate();
|
|
162
|
+
this.#confirmed = this.#position;
|
|
138
163
|
this.#render();
|
|
139
164
|
this.#cycleAbort = new AbortController();
|
|
140
165
|
this.element.addEventListener("keydown", this.#onCycleKeydown, {
|
|
@@ -251,9 +276,7 @@ var ResizableController = class extends Controller {
|
|
|
251
276
|
}
|
|
252
277
|
if (handled) {
|
|
253
278
|
event.preventDefault();
|
|
254
|
-
this
|
|
255
|
-
this.#render();
|
|
256
|
-
this.#dispatchChange();
|
|
279
|
+
this.#commit(nextValue);
|
|
257
280
|
}
|
|
258
281
|
}
|
|
259
282
|
/** Collapses the primary pane to its minimum, or returns it to the last position. */
|
|
@@ -261,22 +284,19 @@ var ResizableController = class extends Controller {
|
|
|
261
284
|
const { min, max } = this.#range;
|
|
262
285
|
if (this.#position > min) {
|
|
263
286
|
this.#valueBeforeCollapse = this.#position;
|
|
264
|
-
this
|
|
287
|
+
this.#commit(min);
|
|
265
288
|
} else {
|
|
266
|
-
|
|
289
|
+
const restored = this.#valueBeforeCollapse ?? max;
|
|
267
290
|
this.#valueBeforeCollapse = null;
|
|
291
|
+
this.#commit(restored);
|
|
268
292
|
}
|
|
269
|
-
this.#render();
|
|
270
|
-
this.#dispatchChange();
|
|
271
293
|
}
|
|
272
294
|
#onPointerMove = (event) => {
|
|
273
295
|
if (!this.hasSeparatorTarget) return;
|
|
274
296
|
const rect = this.element.getBoundingClientRect();
|
|
275
297
|
const raw = this.#isVertical ? (event.clientX - rect.left) / rect.width : (event.clientY - rect.top) / rect.height;
|
|
276
298
|
const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;
|
|
277
|
-
|
|
278
|
-
this.valueValue = Math.max(min, Math.min(Math.round(fraction * 100), max));
|
|
279
|
-
this.#render();
|
|
299
|
+
this.#move(Math.round(fraction * 100));
|
|
280
300
|
};
|
|
281
301
|
#onPointerUp = (event) => {
|
|
282
302
|
this.element.removeAttribute("data-dragging");
|
|
@@ -285,21 +305,50 @@ var ResizableController = class extends Controller {
|
|
|
285
305
|
if (this.hasSeparatorTarget) {
|
|
286
306
|
this.separatorTarget.releasePointerCapture(event.pointerId);
|
|
287
307
|
}
|
|
288
|
-
this.#
|
|
308
|
+
this.#confirm();
|
|
289
309
|
};
|
|
310
|
+
/** Moves to a position the user chose and reports it if it moved. */
|
|
311
|
+
#commit(raw) {
|
|
312
|
+
this.#move(raw);
|
|
313
|
+
this.#confirm();
|
|
314
|
+
}
|
|
290
315
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
316
|
+
* Stores the clamped position the user moved to and paints it. The painted
|
|
317
|
+
* position moves with it before anything is reported, so the repaint the Value
|
|
318
|
+
* write schedules — and any move a subscriber makes while `change` is
|
|
319
|
+
* dispatched — is measured from this position.
|
|
320
|
+
*/
|
|
321
|
+
#move(raw) {
|
|
322
|
+
const { min, max } = this.#range;
|
|
323
|
+
const position = Math.max(min, Math.min(raw, max));
|
|
324
|
+
this.valueValue = position;
|
|
325
|
+
this.#painted = position;
|
|
326
|
+
this.#paint();
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Publishes the position the Values declare without writing it back. A
|
|
330
|
+
* position the user did not paint is the page's move, reported once as
|
|
331
|
+
* `reconcile` unless it lands on the position last confirmed — a drag the page
|
|
332
|
+
* puts back where it started has nothing new to report.
|
|
294
333
|
*
|
|
295
334
|
* @stimeoRenderRoot
|
|
296
335
|
*/
|
|
297
336
|
#render() {
|
|
337
|
+
this.#paint();
|
|
338
|
+
const position = this.#position;
|
|
339
|
+
if (position === this.#painted) return;
|
|
340
|
+
this.#painted = position;
|
|
341
|
+
if (position === this.#confirmed) return;
|
|
342
|
+
this.#confirmed = position;
|
|
343
|
+
this.dispatch("reconcile", { detail: this.#detail(position) });
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Publishes the position: the fraction consumer CSS multiplies a pane by, and
|
|
347
|
+
* the range assistive tech reads off the separator.
|
|
348
|
+
*/
|
|
349
|
+
#paint() {
|
|
298
350
|
const { min, max } = this.#range;
|
|
299
351
|
const position = this.#position;
|
|
300
|
-
if (this.valueValue !== position) {
|
|
301
|
-
this.valueValue = position;
|
|
302
|
-
}
|
|
303
352
|
this.element.style.setProperty("--stimeo--resizable-fraction", String(position / 100));
|
|
304
353
|
if (this.hasSeparatorTarget) {
|
|
305
354
|
this.separatorTarget.setAttribute("aria-valuenow", String(position));
|
|
@@ -307,9 +356,20 @@ var ResizableController = class extends Controller {
|
|
|
307
356
|
this.separatorTarget.setAttribute("aria-valuemax", String(max));
|
|
308
357
|
}
|
|
309
358
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Reports the published position as `change` when the user's operation moved
|
|
361
|
+
* it from the position last confirmed. A key at an edge, a press released
|
|
362
|
+
* without moving and a drag released where it started report nothing.
|
|
363
|
+
*/
|
|
364
|
+
#confirm() {
|
|
365
|
+
const position = this.#position;
|
|
366
|
+
if (position === this.#confirmed) return;
|
|
367
|
+
this.#confirmed = position;
|
|
368
|
+
this.dispatch("change", { detail: this.#detail(position) });
|
|
369
|
+
}
|
|
370
|
+
/** The detail both events carry for `position`. */
|
|
371
|
+
#detail(position) {
|
|
372
|
+
return { value: position, fraction: position / 100 };
|
|
313
373
|
}
|
|
314
374
|
/** The declared range after validation; an unreadable bound uses its default. */
|
|
315
375
|
get #range() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/event_owner.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/before_cache_reset.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/resizable_controller.ts"],"names":[],"mappings":"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","file":"resizable_controller.js","sourcesContent":["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","/**\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 * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\n\n/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * Every live loan also owns a shared `turbo:before-cache` subscription. The loan\n * is returned before Turbo can copy it into a snapshot, so consumers get cache\n * safety without duplicating a lifecycle hook; `returnAll()` removes the\n * subscription again as soon as no loan remains.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n /** Returns live loans before Turbo can copy them into its page snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.returnAll());\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n // Subscribe only while a real loan exists. Keeping this guarantee here means\n // every consumer is Turbo-safe without another lifecycle hook to remember.\n this.#beforeCache.activate();\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n this.#beforeCache.deactivate();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { ownerIndex } from \"../utils/event_owner\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/** Value defaults, reused when a declaration cannot be read as a percentage. */\nconst DEFAULT_MIN = 0;\nconst DEFAULT_MAX = 100;\nconst DEFAULT_VALUE = 50;\nconst DEFAULT_STEP = 1;\n\n/**\n * Headless, highly accessible Window Splitter / Resizable panes logic.\n *\n * Markup contract (identifier: `stimeo--resizable`):\n * <div data-controller=\"stimeo--resizable\"\n * data-stimeo--resizable-min-value=\"20\"\n * data-stimeo--resizable-max-value=\"80\"\n * data-stimeo--resizable-step-value=\"1\"\n * data-stimeo--resizable-value-value=\"50\">\n * <div data-stimeo--resizable-target=\"primary\" id=\"pane-1\">Pane A</div>\n * <div role=\"separator\" tabindex=\"0\" aria-orientation=\"vertical\"\n * aria-controls=\"pane-1\" aria-valuemin=\"20\" aria-valuemax=\"80\"\n * aria-valuenow=\"50\" aria-label=\"Resize splitter\"\n * data-stimeo--resizable-target=\"separator\"\n * data-action=\"pointerdown->stimeo--resizable#onPointerDown\n * keydown->stimeo--resizable#onKeydown\"></div>\n * <div data-stimeo--resizable-target=\"secondary\">Pane B</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Window Splitter** pattern:\n * - Robust dragging track using W3C `setPointerCapture` to ensure uninterrupted dragging\n * even when pointers quickly stray outside the separator area.\n * - Arrow keys move the divider by `step` percent, along the axis\n * `aria-orientation` names; `Home` / `End` jump to the ends of the range.\n * - `Enter` collapses the primary pane to the minimum and puts it back where it\n * was, which is what the pattern means by restoring a previous position.\n * - Root-level CSS custom property `--stimeo--resizable-fraction` (0..1) driving\n * presentation styles.\n * - `F6` cycles focus through the panes, which the pattern lists as optional.\n *\n * `change` dispatches `{ value: number, fraction: number }`.\n *\n * @remarks\n * Behavior only. The controller adjusts the CSS custom property on the root element,\n * updates ARIA values on the separator, and emits `stimeo--resizable:change` events.\n *\n * Both the range and the position are percentages. A bound that is not a finite\n * number falls back to its Value default, a maximum below the minimum collapses\n * onto that minimum, and a step that is not a positive finite number falls back\n * to `1` — so an unreadable declaration narrows what the widget can do without\n * ever publishing `NaN` to CSS or to assistive tech.\n *\n * `aria-orientation` is the author's, and its absence means `horizontal` — the\n * value ARIA gives a separator that does not say. Reading any other default here\n * would make the announced axis and the answering arrow keys disagree.\n */\nexport class ResizableController extends Controller<HTMLElement> {\n static override targets = [\"primary\", \"secondary\", \"separator\"];\n static override values = {\n min: { type: Number, default: DEFAULT_MIN },\n max: { type: Number, default: DEFAULT_MAX },\n step: { type: Number, default: DEFAULT_STEP },\n value: { type: Number, default: DEFAULT_VALUE },\n };\n static actions = [\"onKeydown\", \"onPointerDown\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly primaryTarget: HTMLElement;\n declare readonly secondaryTarget: HTMLElement;\n declare readonly separatorTarget: HTMLElement;\n declare readonly hasPrimaryTarget: boolean;\n declare readonly hasSecondaryTarget: boolean;\n declare readonly hasSeparatorTarget: boolean;\n\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare valueValue: number;\n\n /** Where the divider sat before the current collapse; `null` when unknown. */\n #valueBeforeCollapse: number | null = null;\n\n /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */\n #dragAbort: AbortController | null = null;\n\n /** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */\n readonly #paneTabindex = new TabindexLoan();\n\n /** Releases the pane cycle listener bound in {@link connect}. */\n #cycleAbort: AbortController | null = null;\n\n /** Folds the range and position inputs of one morph batch into a single paint. */\n readonly #repaint = new MicrotaskCoalescer(() => this.#render());\n\n override connect(): void {\n // A drag cannot outlive a navigation, so a hook captured mid-drag is stale on\n // arrival. Clearing is unconditional here because disconnect() aborts the\n // drag, leaving no session that an in-page move could carry over.\n this.element.removeAttribute(\"data-dragging\");\n this.#repaint.activate();\n // Stimulus fires the value callbacks before connect, where the coalescer\n // ignores them, so the first paint has to be asked for directly.\n this.#render();\n // Bound on the root rather than the panes so the cycle continues once focus\n // has left the separator — the panes hold consumer markup and the next F6\n // arrives from inside one of them.\n this.#cycleAbort = new AbortController();\n this.element.addEventListener(\"keydown\", this.#onCycleKeydown, {\n signal: this.#cycleAbort.signal,\n });\n }\n\n /** Cancels any active pointer drag so listeners never leak past disconnect. */\n override disconnect(): void {\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n this.#cycleAbort?.abort();\n this.#cycleAbort = null;\n this.#repaint.cancel();\n this.#paneTabindex.returnAll();\n this.element.removeAttribute(\"data-dragging\");\n }\n\n /** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */\n readonly #onCycleKeydown = (event: KeyboardEvent): void => {\n if (event.key !== \"F6\") return;\n // Japanese input methods bind F6 to a conversion, so a press that belongs to\n // a composition steers that conversion and must not move focus away from it.\n if (event.isComposing) return;\n // A descendant widget that already claimed the key must not ALSO cycle the\n // panes; this listener sees consumer markup, so composition depends on it.\n if (event.defaultPrevented) return;\n // The pattern binds a bare F6 only. A chorded one is the browser's or the\n // OS's (Ctrl+F6 cycles frames in several browsers).\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n\n const panes: HTMLElement[] = [];\n if (this.hasPrimaryTarget) panes.push(this.primaryTarget);\n if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);\n\n const current = ownerIndex(panes, event.target);\n // With no panes the modulo is NaN and the read is undefined, which is the\n // same \"nowhere to go\" this guard answers for a cycle that has ends.\n const next = panes[(current + 1) % panes.length];\n if (!next) return;\n\n event.preventDefault();\n this.#paneTabindex.lend(next);\n next.focus();\n };\n\n /** Re-renders when application code (or a Turbo morph) changes `value` at runtime. */\n valueValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `min` at runtime. */\n minValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `max` at runtime. */\n maxValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-publishes range and position onto a separator swapped in after connect. */\n separatorTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Starts active pointer drag tracking and locks capture. */\n onPointerDown(event: PointerEvent): void {\n if (!this.hasSeparatorTarget || event.button !== 0) return;\n\n event.preventDefault();\n\n const separator = this.separatorTarget;\n separator.setPointerCapture(event.pointerId);\n // preventDefault() above suppresses the implicit focus, so move focus\n // explicitly — otherwise keyboard (arrow) adjustments never reach the\n // separator after a pointer interaction (WCAG 2.1.1).\n separator.focus();\n\n this.element.setAttribute(\"data-dragging\", \"true\");\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n separator.addEventListener(\"pointermove\", this.#onPointerMove, { signal: abort.signal });\n separator.addEventListener(\"pointerup\", this.#onPointerUp, { signal: abort.signal });\n separator.addEventListener(\"pointercancel\", this.#onPointerUp, { signal: abort.signal });\n }\n\n /** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n if (!this.hasSeparatorTarget) return;\n\n const { min, max } = this.#range;\n const isVertical = this.#isVertical;\n\n let handled = true;\n let nextValue = this.#position;\n\n switch (event.key) {\n case \"ArrowLeft\":\n if (isVertical) {\n nextValue -= this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowRight\":\n if (isVertical) {\n nextValue += this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowUp\":\n if (!isVertical) {\n nextValue -= this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowDown\":\n if (!isVertical) {\n nextValue += this.#step;\n } else {\n handled = false;\n }\n break;\n case \"Home\":\n nextValue = min;\n break;\n case \"End\":\n nextValue = max;\n break;\n case \"Enter\":\n event.preventDefault();\n this.toggle();\n return;\n default:\n handled = false;\n break;\n }\n\n if (handled) {\n event.preventDefault();\n this.valueValue = Math.max(min, Math.min(nextValue, max));\n this.#render();\n this.#dispatchChange();\n }\n }\n\n /** Collapses the primary pane to its minimum, or returns it to the last position. */\n toggle(): void {\n const { min, max } = this.#range;\n if (this.#position > min) {\n this.#valueBeforeCollapse = this.#position;\n this.valueValue = min;\n } else {\n // With nothing remembered — a controller that connected to an already\n // collapsed pane — there is no previous position, so open the pane fully.\n this.valueValue = this.#valueBeforeCollapse ?? max;\n this.#valueBeforeCollapse = null;\n }\n this.#render();\n this.#dispatchChange();\n }\n\n readonly #onPointerMove = (event: PointerEvent): void => {\n if (!this.hasSeparatorTarget) return;\n\n const rect = this.element.getBoundingClientRect();\n const raw = this.#isVertical\n ? (event.clientX - rect.left) / rect.width\n : (event.clientY - rect.top) / rect.height;\n\n // A container with no extent divides by zero; treat that as the near end\n // rather than letting it reach the published fraction.\n const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;\n const { min, max } = this.#range;\n\n this.valueValue = Math.max(min, Math.min(Math.round(fraction * 100), max));\n this.#render();\n };\n\n readonly #onPointerUp = (event: PointerEvent): void => {\n // Ending the drag must not depend on the separator still being there: a\n // target removed mid-drag would otherwise strand the hook and the listeners.\n this.element.removeAttribute(\"data-dragging\");\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.releasePointerCapture(event.pointerId);\n }\n\n this.#dispatchChange();\n };\n\n /**\n * Publishes the position: the fraction consumer CSS multiplies a pane by, and\n * the range assistive tech reads off the separator. The clamped position is\n * written back to the Value so it, ARIA, CSS, and dispatched events agree.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const { min, max } = this.#range;\n const position = this.#position;\n\n if (this.valueValue !== position) {\n this.valueValue = position;\n }\n\n this.element.style.setProperty(\"--stimeo--resizable-fraction\", String(position / 100));\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.setAttribute(\"aria-valuenow\", String(position));\n this.separatorTarget.setAttribute(\"aria-valuemin\", String(min));\n this.separatorTarget.setAttribute(\"aria-valuemax\", String(max));\n }\n }\n\n #dispatchChange(): void {\n const fraction = this.valueValue / 100;\n this.dispatch(\"change\", { detail: { value: this.valueValue, fraction } });\n }\n\n /** The declared range after validation; an unreadable bound uses its default. */\n get #range(): { readonly min: number; readonly max: number } {\n const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;\n const max = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;\n // A maximum below the minimum describes no range at all; collapsing it onto\n // the minimum keeps the position inside something that can be announced.\n return { min, max: Math.max(min, max) };\n }\n\n /** The declared position, clamped into the validated range. */\n get #position(): number {\n const { min, max } = this.#range;\n const value = Number.isFinite(this.valueValue) ? this.valueValue : DEFAULT_VALUE;\n return Math.max(min, Math.min(value, max));\n }\n\n /** The keyboard increment; a non-positive or unreadable one uses the default. */\n get #step(): number {\n return Number.isFinite(this.stepValue) && this.stepValue > 0 ? this.stepValue : DEFAULT_STEP;\n }\n\n /** Whether the divider runs vertically; a separator that does not say is horizontal. */\n get #isVertical(): boolean {\n return (\n this.hasSeparatorTarget &&\n this.separatorTarget.getAttribute(\"aria-orientation\") === \"vertical\"\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/event_owner.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/before_cache_reset.ts","../../src/utils/tabindex_loan.ts","../../src/utils/transient_hooks.ts","../../src/controllers/resizable_controller.ts"],"names":[],"mappings":"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","file":"resizable_controller.js","sourcesContent":["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","/**\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 * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\n\n/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * Every live loan also owns a shared `turbo:before-cache` subscription. The loan\n * is returned before Turbo can copy it into a snapshot, so consumers get cache\n * safety without duplicating a lifecycle hook; `returnAll()` removes the\n * subscription again as soon as no loan remains.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n /** Returns live loans before Turbo can copy them into its page snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.returnAll());\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n // Subscribe only while a real loan exists. Keeping this guarantee here means\n // every consumer is Turbo-safe without another lifecycle hook to remember.\n this.#beforeCache.activate();\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n this.#beforeCache.deactivate();\n }\n}\n","/**\n * Drops the transient state hooks a connection inherited, so the cycle it opens\n * starts from a DOM that claims nothing.\n *\n * A transient hook is an attribute a controller writes to say \"this is happening\n * right now\" — a drag in progress, a form with unsaved edits, a refused\n * subscription. Its lifetime belongs to the interaction, not to the element, and\n * the two come apart whenever the element outlives the instance that wrote it: a\n * Turbo cache snapshot taken mid-interaction restores the hook with the markup,\n * and an element re-inserted elsewhere in the page carries it along. Nothing\n * takes such a hook off, because the instance that owned it is gone; consumer\n * CSS then keeps claiming a state no one is in.\n *\n * **`turbo:before-cache` cannot do this job**, which is why the pass belongs in\n * `connect()`: that event fires for a navigation only, so an in-page move — the\n * other half of the population — never reaches it, and a hook that arrived with\n * a moved element would survive. `connect()` covers both, because both end in a\n * connection.\n *\n * Scope is the **drop** only. What the fresh cycle should say instead is the\n * consumer's: a value re-read from the live DOM, a roster refilled by the\n * stream, a measurement retaken from the current scroll position, a slot emptied\n * of the last result. Those answers differ per consumer and none of them is a\n * hook removal, so the pass stops where every consumer agrees.\n *\n * A declaration names one group of hooks and carries no per-instance state, so\n * it belongs at module scope, shared by every instance the page connects. A\n * consumer writes as many as it has groups — hooks it drops under different\n * conditions cannot share one. The exception is a hook whose name is only known\n * at runtime, such as one built from the registered identifier: that\n * declaration is a field, because the name is.\n *\n * **A drop is not always right.** The population splits on one question: can the\n * state a hook describes outlive the instance that wrote it? Where it cannot — a\n * drag ends with the pointer, a subscription dies with its socket — dropping on\n * every connection is correct. Where it can — a submission in flight is not\n * interrupted by an in-page move — only a connection that follows a real restore\n * may drop, and the consumer answers \"did a session survive?\" with the signal it\n * already holds for its own teardown (a live-session field, a `DetachGate`\n * probe). The drop then sits behind that answer, as the second example shows.\n *\n * @example Unconditional — the state cannot outlive the instance.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-sortable-dragging\"] });\n *\n * connect(): void {\n * TRANSIENT.reset(this.element);\n * }\n * ```\n *\n * @example Gated — an in-page move must not drop a live state.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-optimistic\"] });\n *\n * connect(): void {\n * const restored = !this.#connected && !this.#gate.pending;\n * this.#gate.cancel();\n * this.#connected = true;\n * if (restored) TRANSIENT.reset(this.element);\n * }\n * ```\n */\n\n/** What a consumer declares once about the hooks its interaction owns. */\nexport interface TransientHooksOptions {\n /**\n * The attribute names a connection may find written by an earlier one. Hooks\n * a consumer drops under different conditions belong to different\n * declarations, so each one can be reset on its own.\n */\n readonly attributes: readonly string[];\n}\n\n/**\n * One consumer's declaration of the hooks its interaction owns, dropped from an\n * element by {@link TransientHooks.reset}.\n *\n * Every member comment inside this class body is inlined into each consumer\n * bundle (`tsup` builds with `splitting: false`), so rationale belongs in this\n * file's own block, which `dist` drops, and only the contract belongs inside.\n */\nexport class TransientHooks {\n readonly #attributes: readonly string[];\n\n constructor(options: TransientHooksOptions) {\n this.#attributes = options.attributes;\n }\n\n /** Drops the declared hooks from `element`; what it carries besides stays. */\n reset(element: Element): void {\n for (const attribute of this.#attributes) element.removeAttribute(attribute);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { ownerIndex } from \"../utils/event_owner\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\nimport { TransientHooks } from \"../utils/transient_hooks\";\n\n/** The hook a connection may find written by an earlier, now-gone one. */\nconst TRANSIENT = new TransientHooks({ attributes: [\"data-dragging\"] });\n\n/** Value defaults, reused when a declaration cannot be read as a percentage. */\nconst DEFAULT_MIN = 0;\nconst DEFAULT_MAX = 100;\nconst DEFAULT_VALUE = 50;\nconst DEFAULT_STEP = 1;\n\n/**\n * Headless, highly accessible Window Splitter / Resizable panes logic.\n *\n * Markup contract (identifier: `stimeo--resizable`):\n * <div data-controller=\"stimeo--resizable\"\n * data-stimeo--resizable-min-value=\"20\"\n * data-stimeo--resizable-max-value=\"80\"\n * data-stimeo--resizable-step-value=\"1\"\n * data-stimeo--resizable-value-value=\"50\">\n * <div data-stimeo--resizable-target=\"primary\" id=\"pane-1\">Pane A</div>\n * <div role=\"separator\" tabindex=\"0\" aria-orientation=\"vertical\"\n * aria-controls=\"pane-1\" aria-valuemin=\"20\" aria-valuemax=\"80\"\n * aria-valuenow=\"50\" aria-label=\"Resize splitter\"\n * data-stimeo--resizable-target=\"separator\"\n * data-action=\"pointerdown->stimeo--resizable#onPointerDown\n * keydown->stimeo--resizable#onKeydown\"></div>\n * <div data-stimeo--resizable-target=\"secondary\">Pane B</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Window Splitter** pattern:\n * - Robust dragging track using W3C `setPointerCapture` to ensure uninterrupted dragging\n * even when pointers quickly stray outside the separator area.\n * - Arrow keys move the divider by `step` percent, along the axis\n * `aria-orientation` names; `Home` / `End` jump to the ends of the range.\n * - `Enter` collapses the primary pane to the minimum and puts it back where it\n * was, which is what the pattern means by restoring a previous position.\n * - Root-level CSS custom property `--stimeo--resizable-fraction` (0..1) driving\n * presentation styles.\n * - `F6` cycles focus through the panes, which the pattern lists as optional.\n *\n * `change` and `reconcile` dispatch `{ value: number, fraction: number }`.\n *\n * @remarks\n * Behavior only. The controller adjusts the CSS custom property on the root element,\n * updates ARIA values on the separator, and emits `stimeo--resizable:change` events.\n *\n * Both the range and the position are percentages. A bound that is not a finite\n * number falls back to its Value default, a maximum below the minimum collapses\n * onto that minimum, and a step that is not a positive finite number falls back\n * to `1` — so an unreadable declaration narrows what the widget can do without\n * ever publishing `NaN` to CSS or to assistive tech.\n *\n * The Values are inputs. A declared `value` outside the range stays in its\n * attribute as the page wrote it; the separator's ARIA, the fraction and both\n * events carry the clamped position instead, and only a move the user makes\n * writes `value`. `change` reports a key press, drag or toggle that moved the\n * position from the one last confirmed — a drag once, when it ends — and\n * nothing for one that leaves it there: a key at an edge, a press released\n * without moving, a drag released where it started. When a Value the page\n * changes at runtime — a Turbo morph or application code — moves the position,\n * `reconcile` reports it once per batch with the same detail, so a consumer can\n * tell its user's move from the page's. Connecting reports neither. The\n * position last confirmed is the one connecting published, moved by each\n * `change` and `reconcile`; the positions a drag paints on the way do not move\n * it.\n *\n * `aria-orientation` is the author's, and its absence means `horizontal` — the\n * value ARIA gives a separator that does not say. Reading any other default here\n * would make the announced axis and the answering arrow keys disagree.\n */\nexport class ResizableController extends Controller<HTMLElement> {\n static override targets = [\"primary\", \"secondary\", \"separator\"];\n static override values = {\n min: { type: Number, default: DEFAULT_MIN },\n max: { type: Number, default: DEFAULT_MAX },\n step: { type: Number, default: DEFAULT_STEP },\n value: { type: Number, default: DEFAULT_VALUE },\n };\n static actions = [\"onKeydown\", \"onPointerDown\", \"toggle\"] as const;\n static events = [\"change\", \"reconcile\"] as const;\n\n declare readonly primaryTarget: HTMLElement;\n declare readonly secondaryTarget: HTMLElement;\n declare readonly separatorTarget: HTMLElement;\n declare readonly hasPrimaryTarget: boolean;\n declare readonly hasSecondaryTarget: boolean;\n declare readonly hasSeparatorTarget: boolean;\n\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare valueValue: number;\n\n /** Where the divider sat before the current collapse; `null` when unknown. */\n #valueBeforeCollapse: number | null = null;\n\n /**\n * The position last painted: moved by each move the user makes — every step of\n * a drag included — and by each repaint that finds a page-driven move.\n */\n #painted = DEFAULT_VALUE;\n\n /**\n * The position last confirmed: the one connecting published, then moved by\n * each `change` and `reconcile`. A drag moves the painted position step by\n * step and this one only when it ends.\n */\n #confirmed = DEFAULT_VALUE;\n\n /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */\n #dragAbort: AbortController | null = null;\n\n /** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */\n readonly #paneTabindex = new TabindexLoan();\n\n /** Releases the pane cycle listener bound in {@link connect}. */\n #cycleAbort: AbortController | null = null;\n\n /** Folds the range and position inputs of one morph batch into a single paint. */\n readonly #repaint = new MicrotaskCoalescer(() => this.#render());\n\n override connect(): void {\n // A drag cannot outlive a navigation, so a hook captured mid-drag is stale on\n // arrival. Clearing is unconditional here because disconnect() aborts the\n // drag, leaving no session that an in-page move could carry over.\n TRANSIENT.reset(this.element);\n this.#repaint.activate();\n // Stimulus fires the value callbacks before connect, where the coalescer\n // ignores them, so the first paint has to be asked for directly. The\n // position it publishes is taken as reported first, so connecting reports\n // nothing.\n this.#confirmed = this.#position;\n this.#render();\n // Bound on the root rather than the panes so the cycle continues once focus\n // has left the separator — the panes hold consumer markup and the next F6\n // arrives from inside one of them.\n this.#cycleAbort = new AbortController();\n this.element.addEventListener(\"keydown\", this.#onCycleKeydown, {\n signal: this.#cycleAbort.signal,\n });\n }\n\n /** Cancels any active pointer drag so listeners never leak past disconnect. */\n override disconnect(): void {\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n this.#cycleAbort?.abort();\n this.#cycleAbort = null;\n this.#repaint.cancel();\n this.#paneTabindex.returnAll();\n this.element.removeAttribute(\"data-dragging\");\n }\n\n /** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */\n readonly #onCycleKeydown = (event: KeyboardEvent): void => {\n if (event.key !== \"F6\") return;\n // Japanese input methods bind F6 to a conversion, so a press that belongs to\n // a composition steers that conversion and must not move focus away from it.\n if (event.isComposing) return;\n // A descendant widget that already claimed the key must not ALSO cycle the\n // panes; this listener sees consumer markup, so composition depends on it.\n if (event.defaultPrevented) return;\n // The pattern binds a bare F6 only. A chorded one is the browser's or the\n // OS's (Ctrl+F6 cycles frames in several browsers).\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n\n const panes: HTMLElement[] = [];\n if (this.hasPrimaryTarget) panes.push(this.primaryTarget);\n if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);\n\n const current = ownerIndex(panes, event.target);\n // With no panes the modulo is NaN and the read is undefined, which is the\n // same \"nowhere to go\" this guard answers for a cycle that has ends.\n const next = panes[(current + 1) % panes.length];\n if (!next) return;\n\n event.preventDefault();\n this.#paneTabindex.lend(next);\n next.focus();\n };\n\n /** Re-renders when application code (or a Turbo morph) changes `value` at runtime. */\n valueValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `min` at runtime. */\n minValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `max` at runtime. */\n maxValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-publishes range and position onto a separator swapped in after connect. */\n separatorTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Starts active pointer drag tracking and locks capture. */\n onPointerDown(event: PointerEvent): void {\n if (!this.hasSeparatorTarget || event.button !== 0) return;\n\n event.preventDefault();\n\n const separator = this.separatorTarget;\n separator.setPointerCapture(event.pointerId);\n // preventDefault() above suppresses the implicit focus, so move focus\n // explicitly — otherwise keyboard (arrow) adjustments never reach the\n // separator after a pointer interaction (WCAG 2.1.1).\n separator.focus();\n\n this.element.setAttribute(\"data-dragging\", \"true\");\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n separator.addEventListener(\"pointermove\", this.#onPointerMove, { signal: abort.signal });\n separator.addEventListener(\"pointerup\", this.#onPointerUp, { signal: abort.signal });\n separator.addEventListener(\"pointercancel\", this.#onPointerUp, { signal: abort.signal });\n }\n\n /** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n if (!this.hasSeparatorTarget) return;\n\n const { min, max } = this.#range;\n const isVertical = this.#isVertical;\n\n let handled = true;\n let nextValue = this.#position;\n\n switch (event.key) {\n case \"ArrowLeft\":\n if (isVertical) {\n nextValue -= this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowRight\":\n if (isVertical) {\n nextValue += this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowUp\":\n if (!isVertical) {\n nextValue -= this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowDown\":\n if (!isVertical) {\n nextValue += this.#step;\n } else {\n handled = false;\n }\n break;\n case \"Home\":\n nextValue = min;\n break;\n case \"End\":\n nextValue = max;\n break;\n case \"Enter\":\n event.preventDefault();\n this.toggle();\n return;\n default:\n handled = false;\n break;\n }\n\n if (handled) {\n event.preventDefault();\n this.#commit(nextValue);\n }\n }\n\n /** Collapses the primary pane to its minimum, or returns it to the last position. */\n toggle(): void {\n const { min, max } = this.#range;\n if (this.#position > min) {\n this.#valueBeforeCollapse = this.#position;\n this.#commit(min);\n } else {\n // With nothing remembered — a controller that connected to an already\n // collapsed pane — there is no previous position, so open the pane fully.\n const restored = this.#valueBeforeCollapse ?? max;\n this.#valueBeforeCollapse = null;\n this.#commit(restored);\n }\n }\n\n readonly #onPointerMove = (event: PointerEvent): void => {\n if (!this.hasSeparatorTarget) return;\n\n const rect = this.element.getBoundingClientRect();\n const raw = this.#isVertical\n ? (event.clientX - rect.left) / rect.width\n : (event.clientY - rect.top) / rect.height;\n\n // A container with no extent divides by zero; treat that as the near end\n // rather than letting it reach the published fraction.\n const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;\n this.#move(Math.round(fraction * 100));\n };\n\n readonly #onPointerUp = (event: PointerEvent): void => {\n // Ending the drag must not depend on the separator still being there: a\n // target removed mid-drag would otherwise strand the hook and the listeners.\n this.element.removeAttribute(\"data-dragging\");\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.releasePointerCapture(event.pointerId);\n }\n\n this.#confirm();\n };\n\n /** Moves to a position the user chose and reports it if it moved. */\n #commit(raw: number): void {\n this.#move(raw);\n this.#confirm();\n }\n\n /**\n * Stores the clamped position the user moved to and paints it. The painted\n * position moves with it before anything is reported, so the repaint the Value\n * write schedules — and any move a subscriber makes while `change` is\n * dispatched — is measured from this position.\n */\n #move(raw: number): void {\n const { min, max } = this.#range;\n const position = Math.max(min, Math.min(raw, max));\n this.valueValue = position;\n this.#painted = position;\n this.#paint();\n }\n\n /**\n * Publishes the position the Values declare without writing it back. A\n * position the user did not paint is the page's move, reported once as\n * `reconcile` unless it lands on the position last confirmed — a drag the page\n * puts back where it started has nothing new to report.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n this.#paint();\n const position = this.#position;\n if (position === this.#painted) return;\n this.#painted = position;\n if (position === this.#confirmed) return;\n this.#confirmed = position;\n this.dispatch(\"reconcile\", { detail: this.#detail(position) });\n }\n\n /**\n * Publishes the position: the fraction consumer CSS multiplies a pane by, and\n * the range assistive tech reads off the separator.\n */\n #paint(): void {\n const { min, max } = this.#range;\n const position = this.#position;\n\n this.element.style.setProperty(\"--stimeo--resizable-fraction\", String(position / 100));\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.setAttribute(\"aria-valuenow\", String(position));\n this.separatorTarget.setAttribute(\"aria-valuemin\", String(min));\n this.separatorTarget.setAttribute(\"aria-valuemax\", String(max));\n }\n }\n\n /**\n * Reports the published position as `change` when the user's operation moved\n * it from the position last confirmed. A key at an edge, a press released\n * without moving and a drag released where it started report nothing.\n */\n #confirm(): void {\n const position = this.#position;\n if (position === this.#confirmed) return;\n this.#confirmed = position;\n this.dispatch(\"change\", { detail: this.#detail(position) });\n }\n\n /** The detail both events carry for `position`. */\n #detail(position: number): { value: number; fraction: number } {\n return { value: position, fraction: position / 100 };\n }\n\n /** The declared range after validation; an unreadable bound uses its default. */\n get #range(): { readonly min: number; readonly max: number } {\n const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;\n const max = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;\n // A maximum below the minimum describes no range at all; collapsing it onto\n // the minimum keeps the position inside something that can be announced.\n return { min, max: Math.max(min, max) };\n }\n\n /** The declared position, clamped into the validated range. */\n get #position(): number {\n const { min, max } = this.#range;\n const value = Number.isFinite(this.valueValue) ? this.valueValue : DEFAULT_VALUE;\n return Math.max(min, Math.min(value, max));\n }\n\n /** The keyboard increment; a non-positive or unreadable one uses the default. */\n get #step(): number {\n return Number.isFinite(this.stepValue) && this.stepValue > 0 ? this.stepValue : DEFAULT_STEP;\n }\n\n /** Whether the divider runs vertically; a separator that does not say is horizontal. */\n get #isVertical(): boolean {\n return (\n this.hasSeparatorTarget &&\n this.separatorTarget.getAttribute(\"aria-orientation\") === \"vertical\"\n );\n }\n}\n"]}
|
|
@@ -150,16 +150,51 @@ function hasTabStop(root) {
|
|
|
150
150
|
return firstTabStop(root) !== null;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// src/utils/frame_coalescer.ts
|
|
154
|
+
var FrameCoalescer = class {
|
|
155
|
+
#frame = null;
|
|
156
|
+
/**
|
|
157
|
+
* Runs `run` on the next frame, unless a frame is already pending — the first
|
|
158
|
+
* request of a burst wins and the rest are dropped. The pending frame is
|
|
159
|
+
* released before `run`, so `run` may request the next one.
|
|
160
|
+
*/
|
|
161
|
+
schedule(run) {
|
|
162
|
+
if (this.#frame !== null) return;
|
|
163
|
+
this.#frame = requestAnimationFrame(() => {
|
|
164
|
+
this.#frame = null;
|
|
165
|
+
run();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Drops the pending frame, and reaches the platform only when there is one.
|
|
170
|
+
*
|
|
171
|
+
* There is no handle value that stands for "nothing pending":
|
|
172
|
+
* `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
|
|
173
|
+
* arrives as a large positive number that the same allocator can hand out, and
|
|
174
|
+
* an idle cancel would then drop a frame belonging to someone else.
|
|
175
|
+
*/
|
|
176
|
+
cancel() {
|
|
177
|
+
if (this.#frame === null) return;
|
|
178
|
+
cancelAnimationFrame(this.#frame);
|
|
179
|
+
this.#frame = null;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
153
183
|
// src/utils/layout_observer.ts
|
|
154
184
|
var LayoutObserver = class {
|
|
155
185
|
#callback;
|
|
156
186
|
#resizeObserverFactory;
|
|
157
187
|
#resizeObserver = null;
|
|
158
188
|
#observingViewport = false;
|
|
189
|
+
#loadContainer = null;
|
|
159
190
|
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
160
191
|
#handleViewportResize = () => {
|
|
161
192
|
this.#callback();
|
|
162
193
|
};
|
|
194
|
+
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
195
|
+
#handleDescendantLoad = () => {
|
|
196
|
+
this.#callback();
|
|
197
|
+
};
|
|
163
198
|
constructor(callback, options = {}) {
|
|
164
199
|
this.#callback = callback;
|
|
165
200
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
@@ -195,14 +230,35 @@ var LayoutObserver = class {
|
|
|
195
230
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
196
231
|
}
|
|
197
232
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
233
|
+
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
234
|
+
* frame settling changes the box it sits in, and it measures as zero high until
|
|
235
|
+
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
236
|
+
* on the container itself and nothing the caller spells.
|
|
237
|
+
*
|
|
238
|
+
* **One container at a time.** A further call moves the observation, so a widget
|
|
239
|
+
* whose content element is swapped at runtime releases the element it let go by
|
|
240
|
+
* naming the new one — there is no second place for the release to drift from.
|
|
241
|
+
*/
|
|
242
|
+
observeDescendantLoads(container) {
|
|
243
|
+
this.unobserveDescendantLoads();
|
|
244
|
+
this.#loadContainer = container;
|
|
245
|
+
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
246
|
+
}
|
|
247
|
+
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
248
|
+
unobserveDescendantLoads() {
|
|
249
|
+
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
250
|
+
this.#loadContainer = null;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
254
|
+
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
255
|
+
* this from a controller's `disconnect()`.
|
|
201
256
|
*/
|
|
202
257
|
disconnect() {
|
|
203
258
|
this.#resizeObserver?.disconnect();
|
|
204
259
|
this.#resizeObserver = null;
|
|
205
260
|
this.unobserveViewport();
|
|
261
|
+
this.unobserveDescendantLoads();
|
|
206
262
|
}
|
|
207
263
|
};
|
|
208
264
|
|
|
@@ -359,20 +415,14 @@ var ScrollAreaController = class extends Controller {
|
|
|
359
415
|
#observedNameIds = /* @__PURE__ */ new Set();
|
|
360
416
|
#observedNameSources = [];
|
|
361
417
|
#fonts = null;
|
|
362
|
-
|
|
418
|
+
/** Coalesces scroll bursts into one position sync per frame. */
|
|
419
|
+
#frames = new FrameCoalescer();
|
|
363
420
|
#overflowing = false;
|
|
364
421
|
#lastEdge = null;
|
|
365
422
|
#ownedHostMutations = /* @__PURE__ */ new Map();
|
|
366
|
-
#onScroll = () => {
|
|
367
|
-
|
|
368
|
-
this.#
|
|
369
|
-
this.#scrollFrame = null;
|
|
370
|
-
const viewport = this.#viewport;
|
|
371
|
-
if (viewport) this.#syncPosition(viewport, this.#overflowing);
|
|
372
|
-
});
|
|
373
|
-
};
|
|
374
|
-
#onLoad = () => {
|
|
375
|
-
this.#refresh();
|
|
423
|
+
#onScroll = (event) => {
|
|
424
|
+
const viewport = event.currentTarget;
|
|
425
|
+
this.#frames.schedule(() => this.#syncPosition(viewport, this.#overflowing));
|
|
376
426
|
};
|
|
377
427
|
#onFontsSettled = () => {
|
|
378
428
|
this.#refresh();
|
|
@@ -387,7 +437,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
387
437
|
disconnect() {
|
|
388
438
|
this.#beforeCache.deactivate();
|
|
389
439
|
this.#rebind.cancel();
|
|
390
|
-
this.#
|
|
440
|
+
this.#frames.cancel();
|
|
391
441
|
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
392
442
|
this.#layout.disconnect();
|
|
393
443
|
this.#unbindFonts();
|
|
@@ -420,7 +470,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
420
470
|
return;
|
|
421
471
|
}
|
|
422
472
|
next.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
423
|
-
|
|
473
|
+
this.#layout.observeDescendantLoads(next);
|
|
424
474
|
this.#layout.observe(next);
|
|
425
475
|
this.#bindContentObserver(next);
|
|
426
476
|
this.#refresh();
|
|
@@ -455,9 +505,9 @@ var ScrollAreaController = class extends Controller {
|
|
|
455
505
|
}
|
|
456
506
|
/** Releases every resource and borrowed attribute owned by one former viewport. */
|
|
457
507
|
#unbindViewport(viewport) {
|
|
458
|
-
this.#
|
|
508
|
+
this.#frames.cancel();
|
|
459
509
|
viewport.removeEventListener("scroll", this.#onScroll);
|
|
460
|
-
|
|
510
|
+
this.#layout.unobserveDescendantLoads();
|
|
461
511
|
this.#layout.unobserve(viewport);
|
|
462
512
|
this.#content?.disconnect();
|
|
463
513
|
this.#content = null;
|
|
@@ -468,11 +518,15 @@ var ScrollAreaController = class extends Controller {
|
|
|
468
518
|
this.#overflowing = false;
|
|
469
519
|
this.#lastEdge = null;
|
|
470
520
|
}
|
|
471
|
-
/**
|
|
521
|
+
/**
|
|
522
|
+
* Runs the full structural and positional measurement pass.
|
|
523
|
+
*
|
|
524
|
+
* @stimeoRenderRoot
|
|
525
|
+
*/
|
|
472
526
|
#refresh() {
|
|
473
527
|
const viewport = this.#viewport;
|
|
474
528
|
if (!viewport) return;
|
|
475
|
-
this.#
|
|
529
|
+
this.#frames.cancel();
|
|
476
530
|
this.#syncNameSources(viewport);
|
|
477
531
|
this.#overflowing = this.#syncOverflow(viewport);
|
|
478
532
|
this.#syncKeyboardReach(viewport, this.#overflowing);
|
|
@@ -680,16 +734,10 @@ var ScrollAreaController = class extends Controller {
|
|
|
680
734
|
this.#fonts?.removeEventListener("loadingerror", this.#onFontsSettled);
|
|
681
735
|
this.#fonts = null;
|
|
682
736
|
}
|
|
683
|
-
/** Cancels a pending scroll frame. */
|
|
684
|
-
#cancelScrollFrame() {
|
|
685
|
-
if (this.#scrollFrame === null) return;
|
|
686
|
-
cancelAnimationFrame(this.#scrollFrame);
|
|
687
|
-
this.#scrollFrame = null;
|
|
688
|
-
}
|
|
689
737
|
/** Suspends live resources and returns all derived state before snapshotting. */
|
|
690
738
|
#rewindForCache() {
|
|
691
739
|
this.#rebind.cancel();
|
|
692
|
-
this.#
|
|
740
|
+
this.#frames.cancel();
|
|
693
741
|
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
694
742
|
this.#layout.disconnect();
|
|
695
743
|
this.#unbindFonts();
|