stimeo-ui 0.2.0 → 0.3.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 +163 -0
- package/dist/cable/index.d.ts +16 -16
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +3 -2
- package/dist/controllers/accordion_controller.js +10 -0
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +64 -0
- package/dist/controllers/alert_dialog_controller.js +318 -0
- package/dist/controllers/alert_dialog_controller.js.map +1 -0
- package/dist/controllers/breadcrumb_controller.d.ts +97 -10
- package/dist/controllers/breadcrumb_controller.js +225 -13
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +11 -1
- package/dist/controllers/calendar_controller.js +89 -22
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +115 -0
- package/dist/controllers/carousel_controller.js +313 -0
- package/dist/controllers/carousel_controller.js.map +1 -0
- package/dist/controllers/clipboard_controller.d.ts +72 -0
- package/dist/controllers/clipboard_controller.js +144 -0
- package/dist/controllers/clipboard_controller.js.map +1 -0
- package/dist/controllers/collapsible_controller.d.ts +70 -0
- package/dist/controllers/collapsible_controller.js +327 -0
- package/dist/controllers/collapsible_controller.js.map +1 -0
- package/dist/controllers/color_picker_controller.d.ts +87 -0
- package/dist/controllers/color_picker_controller.js +252 -0
- package/dist/controllers/color_picker_controller.js.map +1 -0
- package/dist/controllers/combobox_controller.d.ts +14 -1
- package/dist/controllers/combobox_controller.js +162 -23
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +2 -0
- package/dist/controllers/command_palette_controller.js +194 -17
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +7 -6
- package/dist/controllers/context_menu_controller.js +32 -10
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js +8 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +72 -0
- package/dist/controllers/currency_input_controller.js +147 -0
- package/dist/controllers/currency_input_controller.js.map +1 -0
- package/dist/controllers/data_grid_controller.d.ts +89 -0
- package/dist/controllers/data_grid_controller.js +246 -0
- package/dist/controllers/data_grid_controller.js.map +1 -0
- package/dist/controllers/date_range_picker_controller.d.ts +88 -0
- package/dist/controllers/date_range_picker_controller.js +441 -0
- package/dist/controllers/date_range_picker_controller.js.map +1 -0
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +57 -0
- package/dist/controllers/dismissible_controller.js +117 -0
- package/dist/controllers/dismissible_controller.js.map +1 -0
- package/dist/controllers/drawer_controller.d.ts +96 -0
- package/dist/controllers/drawer_controller.js +630 -0
- package/dist/controllers/drawer_controller.js.map +1 -0
- package/dist/controllers/dropdown_controller.d.ts +1 -2
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +73 -0
- package/dist/controllers/editable_controller.js +169 -0
- package/dist/controllers/editable_controller.js.map +1 -0
- package/dist/controllers/file_dropzone_controller.d.ts +92 -0
- package/dist/controllers/file_dropzone_controller.js +165 -0
- package/dist/controllers/file_dropzone_controller.js.map +1 -0
- package/dist/controllers/filter_controller.d.ts +60 -0
- package/dist/controllers/filter_controller.js +86 -0
- package/dist/controllers/filter_controller.js.map +1 -0
- package/dist/controllers/flash_controller.js +36 -5
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +1 -1
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +6 -4
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +1 -1
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +67 -19
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +54 -11
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +9 -2
- package/dist/controllers/listbox_controller.js +257 -53
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -2
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +54 -0
- package/dist/controllers/masonry_controller.js +142 -0
- package/dist/controllers/masonry_controller.js.map +1 -0
- package/dist/controllers/menu_controller.d.ts +29 -11
- package/dist/controllers/menu_controller.js +104 -17
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +142 -0
- package/dist/controllers/menubar_controller.js +785 -0
- package/dist/controllers/menubar_controller.js.map +1 -0
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +141 -0
- package/dist/controllers/multi_select_controller.js +755 -0
- package/dist/controllers/multi_select_controller.js.map +1 -0
- package/dist/controllers/navigation_menu_controller.d.ts +146 -0
- package/dist/controllers/navigation_menu_controller.js +511 -0
- package/dist/controllers/navigation_menu_controller.js.map +1 -0
- package/dist/controllers/nested_form_controller.d.ts +4 -5
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +1 -1
- package/dist/controllers/number_input_controller.js +7 -0
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js +18 -1
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +15 -6
- package/dist/controllers/overflow_indicator_controller.js +246 -27
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +78 -19
- package/dist/controllers/overflow_menu_controller.js +381 -57
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +36 -5
- package/dist/controllers/pagination_controller.js +163 -32
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +49 -0
- package/dist/controllers/password_reveal_controller.js +117 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -0
- package/dist/controllers/persist_controller.js +6 -6
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +2 -3
- package/dist/controllers/pointer_drag_controller.js +9 -1
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +1 -2
- package/dist/controllers/popover_controller.js +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +4 -4
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.js +22 -3
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +97 -0
- package/dist/controllers/range_slider_controller.js +192 -0
- package/dist/controllers/range_slider_controller.js.map +1 -0
- package/dist/controllers/rating_controller.js +16 -2
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +52 -0
- package/dist/controllers/read_more_controller.js +238 -0
- package/dist/controllers/read_more_controller.js.map +1 -0
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +3 -1
- package/dist/controllers/resizable_controller.js +65 -1
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +17 -2
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +101 -14
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +54 -0
- package/dist/controllers/scroll_restore_controller.js +93 -0
- package/dist/controllers/scroll_restore_controller.js.map +1 -0
- package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
- package/dist/controllers/scroll_visibility_controller.js +40 -6
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +64 -15
- package/dist/controllers/scrollspy_controller.js +369 -74
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +69 -0
- package/dist/controllers/separator_controller.js +96 -0
- package/dist/controllers/separator_controller.js.map +1 -0
- package/dist/controllers/sidebar_controller.d.ts +107 -0
- package/dist/controllers/sidebar_controller.js +761 -0
- package/dist/controllers/sidebar_controller.js.map +1 -0
- package/dist/controllers/skeleton_controller.js +1 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +11 -1
- package/dist/controllers/slider_controller.js +32 -6
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +3 -1
- package/dist/controllers/sortable_controller.js +34 -3
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +1 -1
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +4 -0
- package/dist/controllers/stepper_controller.js +28 -12
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +5 -6
- package/dist/controllers/stick_to_bottom_controller.js +9 -4
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +17 -4
- package/dist/controllers/sticky_observer_controller.js +109 -20
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +2 -2
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js +1 -0
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +13 -4
- package/dist/controllers/tabs_controller.js +26 -3
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +91 -0
- package/dist/controllers/tags_input_controller.js +295 -0
- package/dist/controllers/tags_input_controller.js.map +1 -0
- package/dist/controllers/theme_controller.d.ts +1 -1
- package/dist/controllers/theme_controller.js +42 -13
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +67 -0
- package/dist/controllers/time_picker_controller.js +231 -0
- package/dist/controllers/time_picker_controller.js.map +1 -0
- package/dist/controllers/toast_controller.d.ts +5 -14
- package/dist/controllers/toast_controller.js +40 -14
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js +23 -2
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +53 -13
- package/dist/controllers/toolbar_controller.js +230 -31
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +1 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +14 -8
- package/dist/controllers/transition_controller.js +153 -38
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +135 -0
- package/dist/controllers/tree_view_controller.js +691 -0
- package/dist/controllers/tree_view_controller.js.map +1 -0
- package/dist/index.d.ts +27 -1818
- package/dist/index.js +4256 -915
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +375 -39
- package/dist/inspector/cli.js +228 -14
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +419 -48
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +26 -26
- package/dist/inspector/manifest.json +1054 -127
- package/package.json +5 -6
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/read_more_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/blur_deferral.ts
|
|
6
|
+
var BlurDeferral = class {
|
|
7
|
+
/** Elements currently holding an update back, mapped to their `blur` listener. */
|
|
8
|
+
#pending = /* @__PURE__ */ new Map();
|
|
9
|
+
/** Called after a pending element blurs and has been detached. */
|
|
10
|
+
#onRelease;
|
|
11
|
+
/** @param onRelease - Invoked once `element` actually blurs; never on `release`. */
|
|
12
|
+
constructor(onRelease) {
|
|
13
|
+
this.#onRelease = onRelease;
|
|
14
|
+
}
|
|
15
|
+
/** Number of elements currently holding an update back. */
|
|
16
|
+
get size() {
|
|
17
|
+
return this.#pending.size;
|
|
18
|
+
}
|
|
19
|
+
/** Snapshot of the pending elements, safe to iterate while releasing them. */
|
|
20
|
+
get elements() {
|
|
21
|
+
return [...this.#pending.keys()];
|
|
22
|
+
}
|
|
23
|
+
/** Whether `element` is currently holding an update back. */
|
|
24
|
+
has(element) {
|
|
25
|
+
return this.#pending.has(element);
|
|
26
|
+
}
|
|
27
|
+
/** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */
|
|
28
|
+
defer(element) {
|
|
29
|
+
if (this.#pending.has(element)) return;
|
|
30
|
+
const onBlur = () => {
|
|
31
|
+
this.#detach(element);
|
|
32
|
+
this.#onRelease(element);
|
|
33
|
+
};
|
|
34
|
+
this.#pending.set(element, onBlur);
|
|
35
|
+
element.addEventListener("blur", onBlur);
|
|
36
|
+
}
|
|
37
|
+
/** Defers `element` as the only pending entry, cancelling any others. */
|
|
38
|
+
deferOnly(element) {
|
|
39
|
+
for (const pending of this.elements) {
|
|
40
|
+
if (pending !== element) this.#detach(pending);
|
|
41
|
+
}
|
|
42
|
+
this.defer(element);
|
|
43
|
+
}
|
|
44
|
+
/** Cancels `element`'s deferral without completing it; no-ops when not pending. */
|
|
45
|
+
release(element) {
|
|
46
|
+
this.#detach(element);
|
|
47
|
+
}
|
|
48
|
+
/** Cancels every deferral without completing any of them. */
|
|
49
|
+
releaseAll() {
|
|
50
|
+
for (const element of this.elements) this.#detach(element);
|
|
51
|
+
}
|
|
52
|
+
/** Removes the `blur` listener for `element` and forgets it. */
|
|
53
|
+
#detach(element) {
|
|
54
|
+
const onBlur = this.#pending.get(element);
|
|
55
|
+
if (onBlur) element.removeEventListener("blur", onBlur);
|
|
56
|
+
this.#pending.delete(element);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/utils/layout_observer.ts
|
|
61
|
+
var LayoutObserver = class {
|
|
62
|
+
#callback;
|
|
63
|
+
#resizeObserverFactory;
|
|
64
|
+
#resizeObserver = null;
|
|
65
|
+
#observingViewport = false;
|
|
66
|
+
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
67
|
+
#handleViewportResize = () => {
|
|
68
|
+
this.#callback();
|
|
69
|
+
};
|
|
70
|
+
constructor(callback, options = {}) {
|
|
71
|
+
this.#callback = callback;
|
|
72
|
+
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Starts observing an element's size. Repeated calls observe additional
|
|
76
|
+
* elements through the same shared observer. No-ops when no
|
|
77
|
+
* `ResizeObserver` implementation is available.
|
|
78
|
+
*/
|
|
79
|
+
observe(element) {
|
|
80
|
+
if (!this.#resizeObserverFactory) return;
|
|
81
|
+
if (!this.#resizeObserver) {
|
|
82
|
+
this.#resizeObserver = this.#resizeObserverFactory(() => {
|
|
83
|
+
this.#callback();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
this.#resizeObserver.observe(element);
|
|
87
|
+
}
|
|
88
|
+
/** Stops observing a single element while leaving any others in place. */
|
|
89
|
+
unobserve(element) {
|
|
90
|
+
this.#resizeObserver?.unobserve(element);
|
|
91
|
+
}
|
|
92
|
+
/** Starts observing viewport resizes. Idempotent: the listener is added once. */
|
|
93
|
+
observeViewport() {
|
|
94
|
+
if (this.#observingViewport) return;
|
|
95
|
+
this.#observingViewport = true;
|
|
96
|
+
window.addEventListener("resize", this.#handleViewportResize);
|
|
97
|
+
}
|
|
98
|
+
/** Stops observing viewport resizes without affecting element observation. */
|
|
99
|
+
unobserveViewport() {
|
|
100
|
+
if (!this.#observingViewport) return;
|
|
101
|
+
this.#observingViewport = false;
|
|
102
|
+
window.removeEventListener("resize", this.#handleViewportResize);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Releases every observation: disconnects the {@link ResizeObserver} and
|
|
106
|
+
* removes the viewport listener. Safe to call multiple times. Call this from a
|
|
107
|
+
* controller's `disconnect()`.
|
|
108
|
+
*/
|
|
109
|
+
disconnect() {
|
|
110
|
+
this.#resizeObserver?.disconnect();
|
|
111
|
+
this.#resizeObserver = null;
|
|
112
|
+
this.unobserveViewport();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// src/controllers/read_more_controller.ts
|
|
117
|
+
var ReadMoreController = class extends Controller {
|
|
118
|
+
static targets = ["content", "trigger"];
|
|
119
|
+
static values = {
|
|
120
|
+
collapsed: { type: Boolean, default: true }
|
|
121
|
+
};
|
|
122
|
+
static actions = ["toggle"];
|
|
123
|
+
#connected = false;
|
|
124
|
+
#collapsed = true;
|
|
125
|
+
#observedContent = null;
|
|
126
|
+
#contentMutationObserver = null;
|
|
127
|
+
#update = () => {
|
|
128
|
+
if (this.#connected) this.#evaluateOverflow();
|
|
129
|
+
};
|
|
130
|
+
#layout = new LayoutObserver(this.#update);
|
|
131
|
+
/** Holds the trigger's hide back while it has focus; re-evaluates on blur. */
|
|
132
|
+
#deferredHide = new BlurDeferral(() => {
|
|
133
|
+
this.#update();
|
|
134
|
+
});
|
|
135
|
+
connect() {
|
|
136
|
+
this.#connected = true;
|
|
137
|
+
this.#collapsed = this.#initialCollapsed();
|
|
138
|
+
this.#syncTargets();
|
|
139
|
+
}
|
|
140
|
+
disconnect() {
|
|
141
|
+
this.#connected = false;
|
|
142
|
+
this.#stopObservingContent();
|
|
143
|
+
this.#layout.disconnect();
|
|
144
|
+
}
|
|
145
|
+
contentTargetConnected() {
|
|
146
|
+
this.#syncTargets();
|
|
147
|
+
}
|
|
148
|
+
contentTargetDisconnected() {
|
|
149
|
+
this.#syncTargets();
|
|
150
|
+
}
|
|
151
|
+
triggerTargetConnected() {
|
|
152
|
+
this.#syncTargets();
|
|
153
|
+
}
|
|
154
|
+
triggerTargetDisconnected(trigger) {
|
|
155
|
+
this.#deferredHide.release(trigger);
|
|
156
|
+
this.#syncTargets();
|
|
157
|
+
}
|
|
158
|
+
/** Toggles between the collapsed (clamped) and expanded states. */
|
|
159
|
+
toggle() {
|
|
160
|
+
if (!this.#connected) return;
|
|
161
|
+
this.#collapsed = !this.#collapsed;
|
|
162
|
+
this.#reflect();
|
|
163
|
+
this.#evaluateOverflow();
|
|
164
|
+
}
|
|
165
|
+
#initialCollapsed() {
|
|
166
|
+
if (this.hasContentTarget) {
|
|
167
|
+
const state = this.contentTarget.getAttribute("data-state");
|
|
168
|
+
if (state === "expanded") return false;
|
|
169
|
+
if (state === "collapsed") return true;
|
|
170
|
+
}
|
|
171
|
+
return this.collapsedValue;
|
|
172
|
+
}
|
|
173
|
+
#reflect() {
|
|
174
|
+
if (this.hasContentTarget) {
|
|
175
|
+
this.contentTarget.setAttribute("data-state", this.#collapsed ? "collapsed" : "expanded");
|
|
176
|
+
}
|
|
177
|
+
if (this.hasTriggerTarget) {
|
|
178
|
+
this.triggerTarget.setAttribute("aria-expanded", this.#collapsed ? "false" : "true");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
#syncTargets() {
|
|
182
|
+
if (!this.#connected) return;
|
|
183
|
+
this.#syncContentObservation();
|
|
184
|
+
this.#reflect();
|
|
185
|
+
this.#evaluateOverflow();
|
|
186
|
+
}
|
|
187
|
+
#syncContentObservation() {
|
|
188
|
+
const next = this.hasContentTarget ? this.contentTarget : null;
|
|
189
|
+
if (next === this.#observedContent) return;
|
|
190
|
+
this.#stopObservingContent();
|
|
191
|
+
if (!next) return;
|
|
192
|
+
this.#observedContent = next;
|
|
193
|
+
this.#layout.observe(next);
|
|
194
|
+
this.#layout.observeViewport();
|
|
195
|
+
next.addEventListener("load", this.#update, true);
|
|
196
|
+
if (typeof MutationObserver !== "undefined") {
|
|
197
|
+
this.#contentMutationObserver = new MutationObserver(this.#update);
|
|
198
|
+
this.#contentMutationObserver.observe(next, {
|
|
199
|
+
childList: true,
|
|
200
|
+
subtree: true,
|
|
201
|
+
characterData: true
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
#stopObservingContent() {
|
|
206
|
+
this.#deferredHide.releaseAll();
|
|
207
|
+
if (this.#observedContent) {
|
|
208
|
+
this.#layout.unobserve(this.#observedContent);
|
|
209
|
+
this.#observedContent.removeEventListener("load", this.#update, true);
|
|
210
|
+
}
|
|
211
|
+
this.#observedContent = null;
|
|
212
|
+
this.#contentMutationObserver?.disconnect();
|
|
213
|
+
this.#contentMutationObserver = null;
|
|
214
|
+
this.#layout.unobserveViewport();
|
|
215
|
+
}
|
|
216
|
+
#evaluateOverflow() {
|
|
217
|
+
if (!this.hasTriggerTarget || !this.hasContentTarget) return;
|
|
218
|
+
const trigger = this.triggerTarget;
|
|
219
|
+
const content = this.contentTarget;
|
|
220
|
+
const useful = !this.#collapsed || content.scrollHeight > content.clientHeight;
|
|
221
|
+
if (useful) {
|
|
222
|
+
this.#deferredHide.releaseAll();
|
|
223
|
+
trigger.hidden = false;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (document.activeElement === trigger) {
|
|
227
|
+
trigger.hidden = false;
|
|
228
|
+
this.#deferredHide.deferOnly(trigger);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.#deferredHide.releaseAll();
|
|
232
|
+
trigger.hidden = true;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export { ReadMoreController };
|
|
237
|
+
//# sourceMappingURL=read_more_controller.js.map
|
|
238
|
+
//# sourceMappingURL=read_more_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/blur_deferral.ts","../../src/utils/layout_observer.ts","../../src/controllers/read_more_controller.ts"],"names":[],"mappings":";;;;;AAoDO,IAAM,eAAN,MAAwD;AAAA;AAAA,EAEpD,QAAA,uBAAe,GAAA,EAAmB;AAAA;AAAA,EAElC,UAAA;AAAA;AAAA,EAGT,YAAY,SAAA,EAAiC;AAC3C,IAAA,IAAA,CAAK,UAAA,GAAa,SAAA;AAAA,EACpB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,QAAA,CAAS,IAAA;AAAA,EACvB;AAAA;AAAA,EAGA,IAAI,QAAA,GAAgB;AAClB,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA;AAAA,EACjC;AAAA;AAAA,EAGA,IAAI,OAAA,EAAqB;AACvB,IAAA,OAAO,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA;AAAA,EAClC;AAAA;AAAA,EAGA,MAAM,OAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA,EAAG;AAChC,IAAA,MAAM,SAAS,MAAY;AACzB,MAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AACpB,MAAA,IAAA,CAAK,WAAW,OAAO,CAAA;AAAA,IACzB,CAAA;AACA,IAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAA,EAAS,MAAM,CAAA;AACjC,IAAA,OAAA,CAAQ,gBAAA,CAAiB,QAAQ,MAAM,CAAA;AAAA,EACzC;AAAA;AAAA,EAGA,UAAU,OAAA,EAAkB;AAC1B,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,QAAA,EAAU;AACnC,MAAA,IAAI,OAAA,KAAY,OAAA,EAAS,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAAA,IAC/C;AACA,IAAA,IAAA,CAAK,MAAM,OAAO,CAAA;AAAA,EACpB;AAAA;AAAA,EAGA,QAAQ,OAAA,EAAkB;AACxB,IAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AAAA,EACtB;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,QAAQ,OAAO,CAAA;AAAA,EAC3D;AAAA;AAAA,EAGA,QAAQ,OAAA,EAAkB;AACxB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA;AACxC,IAAA,IAAI,MAAA,EAAQ,OAAA,CAAQ,mBAAA,CAAoB,MAAA,EAAQ,MAAM,CAAA;AACtD,IAAA,IAAA,CAAK,QAAA,CAAS,OAAO,OAAO,CAAA;AAAA,EAC9B;AACF,CAAA;;;ACjEO,IAAM,iBAAN,MAAqB;AAAA,EACjB,SAAA;AAAA,EACA,sBAAA;AAAA,EACT,eAAA,GAAyC,IAAA;AAAA,EACzC,kBAAA,GAAqB,KAAA;AAAA;AAAA,EAGZ,wBAAwB,MAAY;AAC3C,IAAA,IAAA,CAAK,SAAA,EAAU;AAAA,EACjB,CAAA;AAAA,EAEA,WAAA,CAAY,QAAA,EAA0B,OAAA,GAAiC,EAAC,EAAG;AACzE,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AACjB,IAAA,IAAA,CAAK,sBAAA,GACH,OAAA,CAAQ,qBAAA,KACP,OAAO,cAAA,KAAmB,WAAA,GAAc,IAAA,GAAO,CAAC,EAAA,KAAO,IAAI,cAAA,CAAe,EAAE,CAAA,CAAA;AAAA,EACjF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAQ,OAAA,EAAwB;AAC9B,IAAA,IAAI,CAAC,KAAK,sBAAA,EAAwB;AAClC,IAAA,IAAI,CAAC,KAAK,eAAA,EAAiB;AACzB,MAAA,IAAA,CAAK,eAAA,GAAkB,IAAA,CAAK,sBAAA,CAAuB,MAAM;AACvD,QAAA,IAAA,CAAK,SAAA,EAAU;AAAA,MACjB,CAAC,CAAA;AAAA,IACH;AACA,IAAA,IAAA,CAAK,eAAA,CAAgB,QAAQ,OAAO,CAAA;AAAA,EACtC;AAAA;AAAA,EAGA,UAAU,OAAA,EAAwB;AAChC,IAAA,IAAA,CAAK,eAAA,EAAiB,UAAU,OAAO,CAAA;AAAA,EACzC;AAAA;AAAA,EAGA,eAAA,GAAwB;AACtB,IAAA,IAAI,KAAK,kBAAA,EAAoB;AAC7B,IAAA,IAAA,CAAK,kBAAA,GAAqB,IAAA;AAC1B,IAAA,MAAA,CAAO,gBAAA,CAAiB,QAAA,EAAU,IAAA,CAAK,qBAAqB,CAAA;AAAA,EAC9D;AAAA;AAAA,EAGA,iBAAA,GAA0B;AACxB,IAAA,IAAI,CAAC,KAAK,kBAAA,EAAoB;AAC9B,IAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAC1B,IAAA,MAAA,CAAO,mBAAA,CAAoB,QAAA,EAAU,IAAA,CAAK,qBAAqB,CAAA;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,iBAAiB,UAAA,EAAW;AACjC,IAAA,IAAA,CAAK,eAAA,GAAkB,IAAA;AACvB,IAAA,IAAA,CAAK,iBAAA,EAAkB;AAAA,EACzB;AACF,CAAA;;;AClFO,IAAM,kBAAA,GAAN,cAAiC,UAAA,CAAwB;AAAA,EAC9D,OAAgB,OAAA,GAAU,CAAC,SAAA,EAAW,SAAS,CAAA;AAAA,EAC/C,OAAgB,MAAA,GAAS;AAAA,IACvB,SAAA,EAAW,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA;AAAK,GAC5C;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,QAAQ,CAAA;AAAA,EAS1B,UAAA,GAAa,KAAA;AAAA,EACb,UAAA,GAAa,IAAA;AAAA,EACb,gBAAA,GAAuC,IAAA;AAAA,EACvC,wBAAA,GAAoD,IAAA;AAAA,EAE3C,UAAU,MAAY;AAC7B,IAAA,IAAI,IAAA,CAAK,UAAA,EAAY,IAAA,CAAK,iBAAA,EAAkB;AAAA,EAC9C,CAAA;AAAA,EACS,OAAA,GAAU,IAAI,cAAA,CAAe,IAAA,CAAK,OAAO,CAAA;AAAA;AAAA,EAGzC,aAAA,GAAgB,IAAI,YAAA,CAAa,MAAM;AAC9C,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf,CAAC,CAAA;AAAA,EAEQ,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAMlB,IAAA,IAAA,CAAK,UAAA,GAAa,KAAK,iBAAA,EAAkB;AACzC,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,qBAAA,EAAsB;AAC3B,IAAA,IAAA,CAAK,QAAQ,UAAA,EAAW;AAAA,EAC1B;AAAA,EAEA,sBAAA,GAA+B;AAC7B,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA,EAEA,yBAAA,GAAkC;AAChC,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA,EAEA,sBAAA,GAA+B;AAC7B,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA,EAEA,0BAA0B,OAAA,EAA4B;AACpD,IAAA,IAAA,CAAK,aAAA,CAAc,QAAQ,OAAO,CAAA;AAClC,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,UAAA,GAAa,CAAC,IAAA,CAAK,UAAA;AACxB,IAAA,IAAA,CAAK,QAAA,EAAS;AACd,IAAA,IAAA,CAAK,iBAAA,EAAkB;AAAA,EACzB;AAAA,EAEA,iBAAA,GAA6B;AAC3B,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,aAAA,CAAc,YAAA,CAAa,YAAY,CAAA;AAC1D,MAAA,IAAI,KAAA,KAAU,YAAY,OAAO,KAAA;AACjC,MAAA,IAAI,KAAA,KAAU,aAAa,OAAO,IAAA;AAAA,IACpC;AACA,IAAA,OAAO,IAAA,CAAK,cAAA;AAAA,EACd;AAAA,EAEA,QAAA,GAAiB;AACf,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,IAAA,CAAK,cAAc,YAAA,CAAa,YAAA,EAAc,IAAA,CAAK,UAAA,GAAa,cAAc,UAAU,CAAA;AAAA,IAC1F;AACA,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,IAAA,CAAK,cAAc,YAAA,CAAa,eAAA,EAAiB,IAAA,CAAK,UAAA,GAAa,UAAU,MAAM,CAAA;AAAA,IACrF;AAAA,EACF;AAAA,EAEA,YAAA,GAAqB;AACnB,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,uBAAA,EAAwB;AAC7B,IAAA,IAAA,CAAK,QAAA,EAAS;AACd,IAAA,IAAA,CAAK,iBAAA,EAAkB;AAAA,EACzB;AAAA,EAEA,uBAAA,GAAgC;AAC9B,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,gBAAA,GAAmB,IAAA,CAAK,aAAA,GAAgB,IAAA;AAC1D,IAAA,IAAI,IAAA,KAAS,KAAK,gBAAA,EAAkB;AAEpC,IAAA,IAAA,CAAK,qBAAA,EAAsB;AAC3B,IAAA,IAAI,CAAC,IAAA,EAAM;AAEX,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA;AACxB,IAAA,IAAA,CAAK,OAAA,CAAQ,QAAQ,IAAI,CAAA;AACzB,IAAA,IAAA,CAAK,QAAQ,eAAA,EAAgB;AAC7B,IAAA,IAAA,CAAK,gBAAA,CAAiB,MAAA,EAAQ,IAAA,CAAK,OAAA,EAAS,IAAI,CAAA;AAEhD,IAAA,IAAI,OAAO,qBAAqB,WAAA,EAAa;AAC3C,MAAA,IAAA,CAAK,wBAAA,GAA2B,IAAI,gBAAA,CAAiB,IAAA,CAAK,OAAO,CAAA;AACjE,MAAA,IAAA,CAAK,wBAAA,CAAyB,QAAQ,IAAA,EAAM;AAAA,QAC1C,SAAA,EAAW,IAAA;AAAA,QACX,OAAA,EAAS,IAAA;AAAA,QACT,aAAA,EAAe;AAAA,OAChB,CAAA;AAAA,IACH;AAAA,EACF;AAAA,EAEA,qBAAA,GAA8B;AAC5B,IAAA,IAAA,CAAK,cAAc,UAAA,EAAW;AAC9B,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,IAAA,CAAK,OAAA,CAAQ,SAAA,CAAU,IAAA,CAAK,gBAAgB,CAAA;AAC5C,MAAA,IAAA,CAAK,gBAAA,CAAiB,mBAAA,CAAoB,MAAA,EAAQ,IAAA,CAAK,SAAS,IAAI,CAAA;AAAA,IACtE;AACA,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA;AACxB,IAAA,IAAA,CAAK,0BAA0B,UAAA,EAAW;AAC1C,IAAA,IAAA,CAAK,wBAAA,GAA2B,IAAA;AAChC,IAAA,IAAA,CAAK,QAAQ,iBAAA,EAAkB;AAAA,EACjC;AAAA,EAEA,iBAAA,GAA0B;AACxB,IAAA,IAAI,CAAC,IAAA,CAAK,gBAAA,IAAoB,CAAC,KAAK,gBAAA,EAAkB;AAEtD,IAAA,MAAM,UAAU,IAAA,CAAK,aAAA;AACrB,IAAA,MAAM,UAAU,IAAA,CAAK,aAAA;AACrB,IAAA,MAAM,SAAS,CAAC,IAAA,CAAK,UAAA,IAAc,OAAA,CAAQ,eAAe,OAAA,CAAQ,YAAA;AAClE,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,IAAA,CAAK,cAAc,UAAA,EAAW;AAC9B,MAAA,OAAA,CAAQ,MAAA,GAAS,KAAA;AACjB,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,QAAA,CAAS,kBAAkB,OAAA,EAAS;AACtC,MAAA,OAAA,CAAQ,MAAA,GAAS,KAAA;AACjB,MAAA,IAAA,CAAK,aAAA,CAAc,UAAU,OAAO,CAAA;AACpC,MAAA;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,cAAc,UAAA,EAAW;AAC9B,IAAA,OAAA,CAAQ,MAAA,GAAS,IAAA;AAAA,EACnB;AACF","file":"read_more_controller.js","sourcesContent":["/**\n * Shared bookkeeping for updates that must wait until an element loses focus.\n *\n * A control that becomes redundant while the user is standing on it cannot be\n * removed from the page immediately: hiding a focused element (or turning it\n * into a native `disabled` one) drops focus to `<body>`, stranding a keyboard\n * user mid-widget. The fix every affected controller reached for is the same —\n * keep the element reachable, hold the destructive update, listen for `blur`,\n * and apply it then.\n *\n * {@link BlurDeferral} owns *only* the registry part of that: which elements are\n * currently holding an update back, attaching and detaching the `blur` listener,\n * and guaranteeing teardown. It is deliberately **policy-free** — what the\n * interim state looks like (`aria-disabled`, an ownership marker, nothing at\n * all), and what the deferred update actually is, stay in the controller, which\n * receives the element back through its release callback.\n *\n * Three contracts are worth stating up front, because each one is a hazard the\n * consumers would otherwise hit:\n *\n * - **The release callback fires only on a real `blur`.** `release()` /\n * `releaseAll()` *cancel* a deferral; they do not complete it. Conflating the\n * two makes `overflow-indicator` disable a button that just left the boundary.\n * - **`elements` is a snapshot.** Iterating the live key set while releasing\n * inside the loop is the failure this registry exists to prevent.\n * - **The entry is detached before the callback runs**, so a controller that\n * decides the update is still unsafe can simply defer again.\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 contracts belong on the members.\n *\n * @example\n * ```ts\n * readonly #deferred = new BlurDeferral<HTMLElement>((trigger) => {\n * if (this.#connected) this.#evaluate();\n * });\n *\n * #hide(trigger: HTMLElement) {\n * if (document.activeElement === trigger) {\n * this.#deferred.deferOnly(trigger); // stays visible until blur\n * return;\n * }\n * this.#deferred.releaseAll();\n * trigger.hidden = true;\n * }\n *\n * disconnect() {\n * this.#deferred.releaseAll();\n * }\n * ```\n */\nexport class BlurDeferral<T extends HTMLElement = HTMLElement> {\n /** Elements currently holding an update back, mapped to their `blur` listener. */\n readonly #pending = new Map<T, () => void>();\n /** Called after a pending element blurs and has been detached. */\n readonly #onRelease: (element: T) => void;\n\n /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */\n constructor(onRelease: (element: T) => void) {\n this.#onRelease = onRelease;\n }\n\n /** Number of elements currently holding an update back. */\n get size(): number {\n return this.#pending.size;\n }\n\n /** Snapshot of the pending elements, safe to iterate while releasing them. */\n get elements(): T[] {\n return [...this.#pending.keys()];\n }\n\n /** Whether `element` is currently holding an update back. */\n has(element: T): boolean {\n return this.#pending.has(element);\n }\n\n /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */\n defer(element: T): void {\n if (this.#pending.has(element)) return;\n const onBlur = (): void => {\n this.#detach(element);\n this.#onRelease(element);\n };\n this.#pending.set(element, onBlur);\n element.addEventListener(\"blur\", onBlur);\n }\n\n /** Defers `element` as the only pending entry, cancelling any others. */\n deferOnly(element: T): void {\n for (const pending of this.elements) {\n if (pending !== element) this.#detach(pending);\n }\n this.defer(element);\n }\n\n /** Cancels `element`'s deferral without completing it; no-ops when not pending. */\n release(element: T): void {\n this.#detach(element);\n }\n\n /** Cancels every deferral without completing any of them. */\n releaseAll(): void {\n for (const element of this.elements) this.#detach(element);\n }\n\n /** Removes the `blur` listener for `element` and forgets it. */\n #detach(element: T): void {\n const onBlur = this.#pending.get(element);\n if (onBlur) element.removeEventListener(\"blur\", onBlur);\n this.#pending.delete(element);\n }\n}\n","/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Layout-sensitive widgets (sliders, resizable panes, scroll spies, popovers)\n * need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BlurDeferral } from \"../utils/blur_deferral\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Headless \"read more / truncate\" behavior for visually clamped text.\n *\n * Markup contract (identifier: `stimeo--read-more`):\n * <div data-controller=\"stimeo--read-more\">\n * <p id=\"bio\" data-stimeo--read-more-target=\"content\" data-state=\"collapsed\">…</p>\n * <button data-stimeo--read-more-target=\"trigger\"\n * data-action=\"stimeo--read-more#toggle\"\n * aria-expanded=\"false\" aria-controls=\"bio\" hidden>Read more</button>\n * </div>\n *\n * There is no dedicated APG widget; the toggle borrows the **Disclosure**\n * convention (`aria-expanded`). The visual clamp itself (`-webkit-line-clamp`\n * etc.) is the consumer's CSS, keyed off `data-state`.\n *\n * @remarks\n * Behavior only. The full text always stays in the DOM — the clamp is purely\n * visual, so assistive technology reads everything regardless of state; here\n * `aria-expanded` therefore signals the *visual* expansion, not content hidden\n * from AT. The controller's extra job is **overflow detection**: when the\n * content is not actually clamped (it fits), the toggle is `hidden` so no\n * pointless \"read more\" is offered. Resizes, content changes, and target\n * replacement re-evaluate it; hiding a focused toggle waits until blur.\n */\nexport class ReadMoreController extends Controller<HTMLElement> {\n static override targets = [\"content\", \"trigger\"];\n static override values = {\n collapsed: { type: Boolean, default: true },\n };\n static actions = [\"toggle\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly triggerTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n declare collapsedValue: boolean;\n\n #connected = false;\n #collapsed = true;\n #observedContent: HTMLElement | null = null;\n #contentMutationObserver: MutationObserver | null = null;\n\n readonly #update = (): void => {\n if (this.#connected) this.#evaluateOverflow();\n };\n readonly #layout = new LayoutObserver(this.#update);\n\n /** Holds the trigger's hide back while it has focus; re-evaluates on blur. */\n readonly #deferredHide = new BlurDeferral(() => {\n this.#update();\n });\n\n override connect(): void {\n this.#connected = true;\n // The DOM is the source of truth on reconnect (Turbo cache restore / morph): an\n // explicit `data-state=\"expanded\"`/`\"collapsed\"` is honored verbatim so a block\n // the user expanded *or* collapsed survives a back-navigation, even when the\n // declarative `collapsed` Value disagrees. The Value seeds only a genuinely fresh\n // render (no `data-state` yet). Mirrors `sidebar`'s `#restoreCollapsed`.\n this.#collapsed = this.#initialCollapsed();\n this.#syncTargets();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#stopObservingContent();\n this.#layout.disconnect();\n }\n\n contentTargetConnected(): void {\n this.#syncTargets();\n }\n\n contentTargetDisconnected(): void {\n this.#syncTargets();\n }\n\n triggerTargetConnected(): void {\n this.#syncTargets();\n }\n\n triggerTargetDisconnected(trigger: HTMLElement): void {\n this.#deferredHide.release(trigger);\n this.#syncTargets();\n }\n\n /** Toggles between the collapsed (clamped) and expanded states. */\n toggle(): void {\n if (!this.#connected) return;\n this.#collapsed = !this.#collapsed;\n this.#reflect();\n this.#evaluateOverflow();\n }\n\n #initialCollapsed(): boolean {\n if (this.hasContentTarget) {\n const state = this.contentTarget.getAttribute(\"data-state\");\n if (state === \"expanded\") return false;\n if (state === \"collapsed\") return true;\n }\n return this.collapsedValue;\n }\n\n #reflect(): void {\n if (this.hasContentTarget) {\n this.contentTarget.setAttribute(\"data-state\", this.#collapsed ? \"collapsed\" : \"expanded\");\n }\n if (this.hasTriggerTarget) {\n this.triggerTarget.setAttribute(\"aria-expanded\", this.#collapsed ? \"false\" : \"true\");\n }\n }\n\n #syncTargets(): void {\n if (!this.#connected) return;\n this.#syncContentObservation();\n this.#reflect();\n this.#evaluateOverflow();\n }\n\n #syncContentObservation(): void {\n const next = this.hasContentTarget ? this.contentTarget : null;\n if (next === this.#observedContent) return;\n\n this.#stopObservingContent();\n if (!next) return;\n\n this.#observedContent = next;\n this.#layout.observe(next);\n this.#layout.observeViewport();\n next.addEventListener(\"load\", this.#update, true);\n\n if (typeof MutationObserver !== \"undefined\") {\n this.#contentMutationObserver = new MutationObserver(this.#update);\n this.#contentMutationObserver.observe(next, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n }\n\n #stopObservingContent(): void {\n this.#deferredHide.releaseAll();\n if (this.#observedContent) {\n this.#layout.unobserve(this.#observedContent);\n this.#observedContent.removeEventListener(\"load\", this.#update, true);\n }\n this.#observedContent = null;\n this.#contentMutationObserver?.disconnect();\n this.#contentMutationObserver = null;\n this.#layout.unobserveViewport();\n }\n\n #evaluateOverflow(): void {\n if (!this.hasTriggerTarget || !this.hasContentTarget) return;\n\n const trigger = this.triggerTarget;\n const content = this.contentTarget;\n const useful = !this.#collapsed || content.scrollHeight > content.clientHeight;\n if (useful) {\n this.#deferredHide.releaseAll();\n trigger.hidden = false;\n return;\n }\n\n if (document.activeElement === trigger) {\n trigger.hidden = false;\n this.#deferredHide.deferOnly(trigger);\n return;\n }\n\n this.#deferredHide.releaseAll();\n trigger.hidden = true;\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/reading_progress_controller.ts"],"names":[],"mappings":";;;AAGA,IAAM,iBAAA,GAAoB,4BAAA;AAkCnB,IAAM,yBAAA,GAAN,cAAwC,UAAA,CAAwB;AAAA,EACrE,OAAO,MAAA,GAAS,CAAC,QAAA,EAAU,UAAU,CAAA;AAAA,EAErC,MAAA,GAAwB,IAAA;AAAA;AAAA,EAExB,SAAA,GAAY,EAAA;AAAA,EAEH,YAAY,MAAY;AAC/B,IAAA,IAAI,IAAA,CAAK,WAAW,IAAA,EAAM;AAC1B,IAAA,IAAA,CAAK,MAAA,GAAS,sBAAsB,MAAM;AACxC,MAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AACd,MAAA,IAAA,CAAK,QAAA,EAAS;AAAA,IAChB,CAAC,CAAA;AAAA,EACH,CAAA;AAAA,EAES,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,SAAA,GAAY,EAAA;AAIjB,IAAA,MAAA,CAAO,gBAAA,CAAiB,UAAU,IAAA,CAAK,SAAA,EAAW,EAAE,OAAA,EAAS,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,CAAA;AAClF,IAAA,MAAA,CAAO,iBAAiB,QAAA,EAAU,IAAA,CAAK,WAAW,EAAE,OAAA,EAAS,MAAM,CAAA;AACnE,IAAA,IAAA,CAAK,QAAA,EAAS;AAAA,EAChB;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,MAAA,CAAO,oBAAoB,QAAA,EAAU,IAAA,CAAK,WAAW,EAAE,OAAA,EAAS,MAAM,CAAA;AACtE,IAAA,MAAA,CAAO,mBAAA,CAAoB,QAAA,EAAU,IAAA,CAAK,SAAS,CAAA;AACnD,IAAA,IAAI,IAAA,CAAK,MAAA,KAAW,IAAA,EAAM,oBAAA,CAAqB,KAAK,MAAM,CAAA;AAC1D,IAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AACd,IAAA,QAAA,CAAS,eAAA,CAAgB,KAAA,CAAM,cAAA,CAAe,iBAAiB,CAAA;AAAA,EACjE;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,qBAAA,EAAsB;AAChD,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,MAAA,GAAS,MAAA,CAAO,WAAA;AAElC,IAAA,MAAM,GAAA,GAAM,IAAA,GAAO,CAAA,GAAI,CAAC,IAAA,CAAK,MAAM,IAAA,GAAO,IAAA,CAAK,GAAA,IAAO,CAAA,GAAI,CAAA,GAAI,CAAA;AAC9D,IAAA,MAAM,QAAA,GAAW,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,GAAG,CAAC,CAAA;AAC7C,IAAA,IAAI,QAAA,KAAa,KAAK,SAAA,EAAW;AAEjC,IAAA,MAAM,WAAW,IAAA,CAAK,SAAA;AACtB,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AACjB,IAAA,MAAM,KAAA,GAAQ,OAAO,QAAQ,CAAA;AAC7B,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,WAAA,CAAY,iBAAA,EAAmB,KAAK,CAAA;AACvD,IAAA,QAAA,CAAS,eAAA,CAAgB,KAAA,CAAM,WAAA,CAAY,iBAAA,EAAmB,KAAK,CAAA;AACnE,IAAA,IAAA,CAAK,SAAS,QAAA,EAAU,EAAE,QAAQ,EAAE,QAAA,IAAY,CAAA;AAChD,IAAA,IAAI,aAAa,CAAA,IAAK,QAAA,KAAa,EAAA,EAAI,IAAA,CAAK,SAAS,UAAU,CAAA;AAAA,EACjE;AACF","file":"reading_progress_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Name of the CSS custom property exposing the reading progress (0..1). */\nconst PROGRESS_PROPERTY = \"--stimeo--reading-progress\";\n\n/**\n * Headless **reading progress**: how far the reader has scrolled *through* this\n * element (an article), published as a CSS custom property and a `change`\n * event stream — the classic top-of-page progress bar.\n * `IntersectionObserver` alone cannot express this (the ratio is constant\n * while a tall article scrolls through the viewport), so this controller owns\n * the scroll math; compose with `stimeo--intersection` when you also need\n * enter/exit triggers. Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--reading-progress`):\n * <article data-controller=\"stimeo--reading-progress\">…</article>\n * <div class=\"progress-bar\" aria-hidden=\"true\"></div>\n * <!-- .progress-bar { width: calc(var(--stimeo--reading-progress, 0) * 100%); } -->\n *\n * Progress is `0` before the article's top reaches the viewport top and `1`\n * once its bottom fits the viewport: `-top / (height - viewportHeight)`,\n * clamped. The property is written on the controller element **and** on\n * `document.documentElement`, so a fixed bar anywhere in the page can consume\n * it without being a descendant. The `:root` copy makes this a\n * one-instance-per-page contract (two articles would fight last-writer-wins);\n * `complete` fires on *reaching* 1 — a connect-time measurement that is\n * already 1 (e.g. a Turbo restore at the bottom) only establishes the\n * baseline and does not fire it.\n *\n * @remarks\n * Behavior only — the bar itself (and hiding it, e.g. before any scroll) is\n * the consumer's CSS; the progress value carries no ARIA (a decorative\n * indicator — mark the bar `aria-hidden`; a *semantic* progress belongs to\n * `stimeo--progress`). Scroll/resize work is rAF-throttled; the listeners and\n * any pending frame are released and the root custom property removed on\n * `disconnect()` (Turbo navigation included).\n */\nexport class ReadingProgressController extends Controller<HTMLElement> {\n static events = [\"change\", \"complete\"] as const;\n\n #frame: number | null = null;\n /** Last published progress, so `change`/`complete` fire only on movement. */\n #progress = -1;\n\n readonly #onScroll = (): void => {\n if (this.#frame !== null) return;\n this.#frame = requestAnimationFrame(() => {\n this.#frame = null;\n this.#measure();\n });\n };\n\n override connect(): void {\n this.#progress = -1;\n // Capture phase: element scrolls do not bubble, but they ARE observable at\n // the window in capture — so an article inside an overflow container
|
|
1
|
+
{"version":3,"sources":["../../src/controllers/reading_progress_controller.ts"],"names":[],"mappings":";;;AAGA,IAAM,iBAAA,GAAoB,4BAAA;AAkCnB,IAAM,yBAAA,GAAN,cAAwC,UAAA,CAAwB;AAAA,EACrE,OAAO,MAAA,GAAS,CAAC,QAAA,EAAU,UAAU,CAAA;AAAA,EAErC,MAAA,GAAwB,IAAA;AAAA;AAAA,EAExB,SAAA,GAAY,EAAA;AAAA,EAEH,YAAY,MAAY;AAC/B,IAAA,IAAI,IAAA,CAAK,WAAW,IAAA,EAAM;AAC1B,IAAA,IAAA,CAAK,MAAA,GAAS,sBAAsB,MAAM;AACxC,MAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AACd,MAAA,IAAA,CAAK,QAAA,EAAS;AAAA,IAChB,CAAC,CAAA;AAAA,EACH,CAAA;AAAA,EAES,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,SAAA,GAAY,EAAA;AAIjB,IAAA,MAAA,CAAO,gBAAA,CAAiB,UAAU,IAAA,CAAK,SAAA,EAAW,EAAE,OAAA,EAAS,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,CAAA;AAClF,IAAA,MAAA,CAAO,iBAAiB,QAAA,EAAU,IAAA,CAAK,WAAW,EAAE,OAAA,EAAS,MAAM,CAAA;AACnE,IAAA,IAAA,CAAK,QAAA,EAAS;AAAA,EAChB;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,MAAA,CAAO,oBAAoB,QAAA,EAAU,IAAA,CAAK,WAAW,EAAE,OAAA,EAAS,MAAM,CAAA;AACtE,IAAA,MAAA,CAAO,mBAAA,CAAoB,QAAA,EAAU,IAAA,CAAK,SAAS,CAAA;AACnD,IAAA,IAAI,IAAA,CAAK,MAAA,KAAW,IAAA,EAAM,oBAAA,CAAqB,KAAK,MAAM,CAAA;AAC1D,IAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AACd,IAAA,QAAA,CAAS,eAAA,CAAgB,KAAA,CAAM,cAAA,CAAe,iBAAiB,CAAA;AAAA,EACjE;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,qBAAA,EAAsB;AAChD,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,MAAA,GAAS,MAAA,CAAO,WAAA;AAElC,IAAA,MAAM,GAAA,GAAM,IAAA,GAAO,CAAA,GAAI,CAAC,IAAA,CAAK,MAAM,IAAA,GAAO,IAAA,CAAK,GAAA,IAAO,CAAA,GAAI,CAAA,GAAI,CAAA;AAC9D,IAAA,MAAM,QAAA,GAAW,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,GAAG,CAAC,CAAA;AAC7C,IAAA,IAAI,QAAA,KAAa,KAAK,SAAA,EAAW;AAEjC,IAAA,MAAM,WAAW,IAAA,CAAK,SAAA;AACtB,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AACjB,IAAA,MAAM,KAAA,GAAQ,OAAO,QAAQ,CAAA;AAC7B,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,WAAA,CAAY,iBAAA,EAAmB,KAAK,CAAA;AACvD,IAAA,QAAA,CAAS,eAAA,CAAgB,KAAA,CAAM,WAAA,CAAY,iBAAA,EAAmB,KAAK,CAAA;AACnE,IAAA,IAAA,CAAK,SAAS,QAAA,EAAU,EAAE,QAAQ,EAAE,QAAA,IAAY,CAAA;AAChD,IAAA,IAAI,aAAa,CAAA,IAAK,QAAA,KAAa,EAAA,EAAI,IAAA,CAAK,SAAS,UAAU,CAAA;AAAA,EACjE;AACF","file":"reading_progress_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Name of the CSS custom property exposing the reading progress (0..1). */\nconst PROGRESS_PROPERTY = \"--stimeo--reading-progress\";\n\n/**\n * Headless **reading progress**: how far the reader has scrolled *through* this\n * element (an article), published as a CSS custom property and a `change`\n * event stream — the classic top-of-page progress bar.\n * `IntersectionObserver` alone cannot express this (the ratio is constant\n * while a tall article scrolls through the viewport), so this controller owns\n * the scroll math; compose with `stimeo--intersection` when you also need\n * enter/exit triggers. Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--reading-progress`):\n * <article data-controller=\"stimeo--reading-progress\">…</article>\n * <div class=\"progress-bar\" aria-hidden=\"true\"></div>\n * <!-- .progress-bar { width: calc(var(--stimeo--reading-progress, 0) * 100%); } -->\n *\n * Progress is `0` before the article's top reaches the viewport top and `1`\n * once its bottom fits the viewport: `-top / (height - viewportHeight)`,\n * clamped. The property is written on the controller element **and** on\n * `document.documentElement`, so a fixed bar anywhere in the page can consume\n * it without being a descendant. The `:root` copy makes this a\n * one-instance-per-page contract (two articles would fight last-writer-wins);\n * `complete` fires on *reaching* 1 — a connect-time measurement that is\n * already 1 (e.g. a Turbo restore at the bottom) only establishes the\n * baseline and does not fire it.\n *\n * @remarks\n * Behavior only — the bar itself (and hiding it, e.g. before any scroll) is\n * the consumer's CSS; the progress value carries no ARIA (a decorative\n * indicator — mark the bar `aria-hidden`; a *semantic* progress belongs to\n * `stimeo--progress`). Scroll/resize work is rAF-throttled; the listeners and\n * any pending frame are released and the root custom property removed on\n * `disconnect()` (Turbo navigation included).\n */\nexport class ReadingProgressController extends Controller<HTMLElement> {\n static events = [\"change\", \"complete\"] as const;\n\n #frame: number | null = null;\n /** Last published progress, so `change`/`complete` fire only on movement. */\n #progress = -1;\n\n readonly #onScroll = (): void => {\n if (this.#frame !== null) return;\n this.#frame = requestAnimationFrame(() => {\n this.#frame = null;\n this.#measure();\n });\n };\n\n override connect(): void {\n this.#progress = -1;\n // Capture phase: element scrolls do not bubble, but they ARE observable at\n // the window in capture — so an article inside an overflow container still\n // drives the progress.\n window.addEventListener(\"scroll\", this.#onScroll, { passive: true, capture: true });\n window.addEventListener(\"resize\", this.#onScroll, { passive: true });\n this.#measure();\n }\n\n override disconnect(): void {\n window.removeEventListener(\"scroll\", this.#onScroll, { capture: true });\n window.removeEventListener(\"resize\", this.#onScroll);\n if (this.#frame !== null) cancelAnimationFrame(this.#frame);\n this.#frame = null;\n document.documentElement.style.removeProperty(PROGRESS_PROPERTY);\n }\n\n /** Computes and publishes the progress; emits on movement only. */\n #measure(): void {\n const rect = this.element.getBoundingClientRect();\n const span = rect.height - window.innerHeight;\n // Shorter than the viewport: reading it is binary (reached or not).\n const raw = span > 0 ? -rect.top / span : rect.top <= 0 ? 1 : 0;\n const progress = Math.min(1, Math.max(0, raw));\n if (progress === this.#progress) return;\n\n const previous = this.#progress;\n this.#progress = progress;\n const value = String(progress);\n this.element.style.setProperty(PROGRESS_PROPERTY, value);\n document.documentElement.style.setProperty(PROGRESS_PROPERTY, value);\n this.dispatch(\"change\", { detail: { progress } });\n if (progress === 1 && previous !== -1) this.dispatch(\"complete\");\n }\n}\n"]}
|
|
@@ -23,7 +23,9 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
23
23
|
* even when pointers quickly stray outside the separator area.
|
|
24
24
|
* - Auto-clamped flex-percentage updates with bounds safety.
|
|
25
25
|
* - Smooth step keyboard adjustments for Arrow keys based on vertical/horizontal orientation.
|
|
26
|
-
* - Root-level CSS custom property `--stimeo--resizable-fraction` (0..1)
|
|
26
|
+
* - Root-level CSS custom property `--stimeo--resizable-fraction` (0..1) driving
|
|
27
|
+
* presentation styles.
|
|
28
|
+
* - `F6` cycles focus through the panes, which the pattern lists as optional.
|
|
27
29
|
*
|
|
28
30
|
* @remarks
|
|
29
31
|
* Behavior only. The controller adjusts the CSS custom property on the root element,
|
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/resizable_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/arrow_step.ts
|
|
6
|
+
function isReservedArrowChord(event, allow = []) {
|
|
7
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
8
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/utils/tabindex_loan.ts
|
|
12
|
+
var TabindexLoan = class {
|
|
13
|
+
#value;
|
|
14
|
+
#lent = /* @__PURE__ */ new Set();
|
|
15
|
+
/**
|
|
16
|
+
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
17
|
+
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
18
|
+
* which a scroll region with no focusable content of its own needs.
|
|
19
|
+
*/
|
|
20
|
+
constructor(value = "-1") {
|
|
21
|
+
this.#value = value;
|
|
22
|
+
}
|
|
23
|
+
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
24
|
+
lend(element) {
|
|
25
|
+
if (element.hasAttribute("tabindex")) return;
|
|
26
|
+
element.setAttribute("tabindex", this.#value);
|
|
27
|
+
this.#lent.add(element);
|
|
28
|
+
}
|
|
29
|
+
/** Takes back every loan whose value is still the one that was lent. */
|
|
30
|
+
returnAll() {
|
|
31
|
+
for (const element of this.#lent) {
|
|
32
|
+
if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
|
|
33
|
+
}
|
|
34
|
+
this.#lent.clear();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
3
38
|
// src/controllers/resizable_controller.ts
|
|
4
39
|
var ResizableController = class extends Controller {
|
|
5
40
|
static targets = ["primary", "secondary", "separator"];
|
|
@@ -11,18 +46,46 @@ var ResizableController = class extends Controller {
|
|
|
11
46
|
};
|
|
12
47
|
static actions = ["onKeydown", "onPointerDown", "toggle"];
|
|
13
48
|
static events = ["change"];
|
|
14
|
-
/**
|
|
49
|
+
/** The value held before the current collapse, restored when toggling back. */
|
|
15
50
|
#valueBeforeCollapse = 50;
|
|
16
51
|
/** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
|
|
17
52
|
#dragAbort = null;
|
|
53
|
+
/** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */
|
|
54
|
+
#paneTabindex = new TabindexLoan();
|
|
55
|
+
/** Releases the pane cycle listener bound in {@link connect}. */
|
|
56
|
+
#cycleAbort = null;
|
|
18
57
|
connect() {
|
|
19
58
|
this.#clampAndSync();
|
|
59
|
+
this.#cycleAbort = new AbortController();
|
|
60
|
+
this.element.addEventListener("keydown", this.#onCycleKeydown, {
|
|
61
|
+
signal: this.#cycleAbort.signal
|
|
62
|
+
});
|
|
20
63
|
}
|
|
21
64
|
/** Cancels any active pointer drag so listeners never leak past disconnect. */
|
|
22
65
|
disconnect() {
|
|
23
66
|
this.#dragAbort?.abort();
|
|
24
67
|
this.#dragAbort = null;
|
|
68
|
+
this.#cycleAbort?.abort();
|
|
69
|
+
this.#cycleAbort = null;
|
|
70
|
+
this.#paneTabindex.returnAll();
|
|
25
71
|
}
|
|
72
|
+
/** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */
|
|
73
|
+
#onCycleKeydown = (event) => {
|
|
74
|
+
if (event.key !== "F6") return;
|
|
75
|
+
if (event.defaultPrevented) return;
|
|
76
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
77
|
+
const panes = [];
|
|
78
|
+
if (this.hasPrimaryTarget) panes.push(this.primaryTarget);
|
|
79
|
+
if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);
|
|
80
|
+
if (panes.length === 0) return;
|
|
81
|
+
const target = event.target;
|
|
82
|
+
const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));
|
|
83
|
+
const next = panes[(current + 1) % panes.length];
|
|
84
|
+
if (!next) return;
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
this.#paneTabindex.lend(next);
|
|
87
|
+
next.focus();
|
|
88
|
+
};
|
|
26
89
|
/**
|
|
27
90
|
* Stimulus lifecycle callback when the valueValue changes.
|
|
28
91
|
* Keeps CSS fractions and ARIA status completely aligned.
|
|
@@ -47,6 +110,7 @@ var ResizableController = class extends Controller {
|
|
|
47
110
|
}
|
|
48
111
|
/** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */
|
|
49
112
|
onKeydown(event) {
|
|
113
|
+
if (isReservedArrowChord(event)) return;
|
|
50
114
|
if (!this.hasSeparatorTarget) return;
|
|
51
115
|
const orientation = this.separatorTarget.getAttribute("aria-orientation") || "vertical";
|
|
52
116
|
const isVertical = orientation === "vertical";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/resizable_controller.ts"],"names":[],"mappings":"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","file":"resizable_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\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-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 * - Auto-clamped flex-percentage updates with bounds safety.\n * - Smooth step keyboard adjustments for Arrow keys based on vertical/horizontal orientation.\n * - Root-level CSS custom property `--stimeo--resizable-fraction` (0..1) to drive presentation styles.\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 */\nexport class ResizableController extends Controller<HTMLElement> {\n static override targets = [\"primary\", \"secondary\", \"separator\"];\n static override values = {\n min: { type: Number, default: 0 },\n max: { type: Number, default: 100 },\n step: { type: Number, default: 1 },\n value: { type: Number, default: 50 },\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 /** Track previously held value before collapse toggles. */\n #valueBeforeCollapse = 50;\n\n /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */\n #dragAbort: AbortController | null = null;\n\n override connect(): void {\n this.#clampAndSync();\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 }\n\n /**\n * Stimulus lifecycle callback when the valueValue changes.\n * Keeps CSS fractions and ARIA status completely aligned.\n */\n valueValueChanged(): void {\n this.#clampAndSync();\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 (!this.hasSeparatorTarget) return;\n\n const orientation = this.separatorTarget.getAttribute(\"aria-orientation\") || \"vertical\";\n const isVertical = orientation === \"vertical\";\n\n let handled = true;\n let nextValue = this.valueValue;\n\n switch (event.key) {\n case \"ArrowLeft\":\n if (isVertical) {\n nextValue -= this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowRight\":\n if (isVertical) {\n nextValue += this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowUp\":\n if (!isVertical) {\n nextValue -= this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowDown\":\n if (!isVertical) {\n nextValue += this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"Home\":\n nextValue = this.minValue;\n break;\n case \"End\":\n nextValue = this.maxValue;\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(this.minValue, Math.min(nextValue, this.maxValue));\n this.#clampAndSync();\n this.#dispatchChange();\n }\n }\n\n /** Double-click or Enter to collapse/restore pane to min/max levels. */\n toggle(): void {\n const threshold = this.minValue + (this.maxValue - this.minValue) / 2;\n if (this.valueValue > this.minValue) {\n this.#valueBeforeCollapse = this.valueValue;\n this.valueValue = this.minValue;\n } else {\n this.valueValue =\n this.#valueBeforeCollapse >= threshold ? this.#valueBeforeCollapse : this.maxValue;\n }\n this.#clampAndSync();\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 orientation = this.separatorTarget.getAttribute(\"aria-orientation\") || \"vertical\";\n const isVertical = orientation === \"vertical\";\n\n let fraction = 0.5;\n\n if (isVertical) {\n fraction = (event.clientX - rect.left) / rect.width;\n } else {\n fraction = (event.clientY - rect.top) / rect.height;\n }\n\n // Restrict within absolute boundaries\n fraction = Math.max(0, Math.min(fraction, 1));\n const percent = Math.round(fraction * 100);\n\n this.valueValue = Math.max(this.minValue, Math.min(percent, this.maxValue));\n this.#clampAndSync();\n };\n\n readonly #onPointerUp = (event: PointerEvent): void => {\n if (!this.hasSeparatorTarget) return;\n\n const separator = this.separatorTarget;\n separator.releasePointerCapture(event.pointerId);\n\n this.element.removeAttribute(\"data-dragging\");\n\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n\n this.#dispatchChange();\n };\n\n #clampAndSync(): void {\n const clamped = Math.max(this.minValue, Math.min(this.valueValue, this.maxValue));\n\n // Keep the stored value in range so valueValue, ARIA, CSS, and dispatched\n // events never diverge (e.g. an initial value above max). Writing back\n // re-enters valueValueChanged once, but the value is already clamped there\n // so it does not recurse.\n if (this.valueValue !== clamped) {\n this.valueValue = clamped;\n }\n\n // Expose local fraction to consumer CSS custom property\n const fraction = clamped / 100;\n this.element.style.setProperty(\"--stimeo--resizable-fraction\", String(fraction));\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.setAttribute(\"aria-valuenow\", String(clamped));\n this.separatorTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n this.separatorTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n }\n }\n\n #dispatchChange(): void {\n const fraction = this.valueValue / 100;\n this.dispatch(\"change\", { detail: { value: this.valueValue, fraction } });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/arrow_step.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 * 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 * 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\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 }\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 }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\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-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 * - Auto-clamped flex-percentage updates with bounds safety.\n * - Smooth step keyboard adjustments for Arrow keys based on vertical/horizontal orientation.\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 * @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 */\nexport class ResizableController extends Controller<HTMLElement> {\n static override targets = [\"primary\", \"secondary\", \"separator\"];\n static override values = {\n min: { type: Number, default: 0 },\n max: { type: Number, default: 100 },\n step: { type: Number, default: 1 },\n value: { type: Number, default: 50 },\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 /** The value held before the current collapse, restored when toggling back. */\n #valueBeforeCollapse = 50;\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 override connect(): void {\n this.#clampAndSync();\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.#paneTabindex.returnAll();\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 // 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 if (panes.length === 0) return;\n\n const target = event.target as Node | null;\n const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));\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 /**\n * Stimulus lifecycle callback when the valueValue changes.\n * Keeps CSS fractions and ARIA status completely aligned.\n */\n valueValueChanged(): void {\n this.#clampAndSync();\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 orientation = this.separatorTarget.getAttribute(\"aria-orientation\") || \"vertical\";\n const isVertical = orientation === \"vertical\";\n\n let handled = true;\n let nextValue = this.valueValue;\n\n switch (event.key) {\n case \"ArrowLeft\":\n if (isVertical) {\n nextValue -= this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowRight\":\n if (isVertical) {\n nextValue += this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowUp\":\n if (!isVertical) {\n nextValue -= this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowDown\":\n if (!isVertical) {\n nextValue += this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"Home\":\n nextValue = this.minValue;\n break;\n case \"End\":\n nextValue = this.maxValue;\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(this.minValue, Math.min(nextValue, this.maxValue));\n this.#clampAndSync();\n this.#dispatchChange();\n }\n }\n\n /** Double-click or Enter to collapse/restore pane to min/max levels. */\n toggle(): void {\n const threshold = this.minValue + (this.maxValue - this.minValue) / 2;\n if (this.valueValue > this.minValue) {\n this.#valueBeforeCollapse = this.valueValue;\n this.valueValue = this.minValue;\n } else {\n this.valueValue =\n this.#valueBeforeCollapse >= threshold ? this.#valueBeforeCollapse : this.maxValue;\n }\n this.#clampAndSync();\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 orientation = this.separatorTarget.getAttribute(\"aria-orientation\") || \"vertical\";\n const isVertical = orientation === \"vertical\";\n\n let fraction = 0.5;\n\n if (isVertical) {\n fraction = (event.clientX - rect.left) / rect.width;\n } else {\n fraction = (event.clientY - rect.top) / rect.height;\n }\n\n // Restrict within absolute boundaries\n fraction = Math.max(0, Math.min(fraction, 1));\n const percent = Math.round(fraction * 100);\n\n this.valueValue = Math.max(this.minValue, Math.min(percent, this.maxValue));\n this.#clampAndSync();\n };\n\n readonly #onPointerUp = (event: PointerEvent): void => {\n if (!this.hasSeparatorTarget) return;\n\n const separator = this.separatorTarget;\n separator.releasePointerCapture(event.pointerId);\n\n this.element.removeAttribute(\"data-dragging\");\n\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n\n this.#dispatchChange();\n };\n\n #clampAndSync(): void {\n const clamped = Math.max(this.minValue, Math.min(this.valueValue, this.maxValue));\n\n // Keep the stored value in range so valueValue, ARIA, CSS, and dispatched\n // events never diverge (e.g. an initial value above max). Writing back\n // re-enters valueValueChanged once, but the value is already clamped there\n // so it does not recurse.\n if (this.valueValue !== clamped) {\n this.valueValue = clamped;\n }\n\n // Expose local fraction to consumer CSS custom property\n const fraction = clamped / 100;\n this.element.style.setProperty(\"--stimeo--resizable-fraction\", String(fraction));\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.setAttribute(\"aria-valuenow\", String(clamped));\n this.separatorTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n this.separatorTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n }\n }\n\n #dispatchChange(): void {\n const fraction = this.valueValue / 100;\n this.dispatch(\"change\", { detail: { value: this.valueValue, fraction } });\n }\n}\n"]}
|
|
@@ -2,6 +2,17 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/roving_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/logical_scroll.ts
|
|
6
|
+
function isRtl(element) {
|
|
7
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/utils/arrow_step.ts
|
|
11
|
+
function isReservedArrowChord(event, allow = []) {
|
|
12
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
13
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
// src/utils/roving_tabindex.ts
|
|
6
17
|
var RovingTabindex = class {
|
|
7
18
|
/** Returns the current ordered item elements; called on every operation. */
|
|
@@ -64,6 +75,7 @@ var RovingController = class extends Controller {
|
|
|
64
75
|
/** Arrow keys move focus + the tab stop; Home/End jump to the ends. */
|
|
65
76
|
#onKeydown = (event) => {
|
|
66
77
|
if (event.defaultPrevented) return;
|
|
78
|
+
if (isReservedArrowChord(event)) return;
|
|
67
79
|
const items = this.itemTargets;
|
|
68
80
|
const current = this.#indexOf(event.target);
|
|
69
81
|
if (current === -1) return;
|
|
@@ -72,10 +84,13 @@ var RovingController = class extends Controller {
|
|
|
72
84
|
const orientation = this.orientationValue;
|
|
73
85
|
const horizontal = orientation === "horizontal" || orientation === "both";
|
|
74
86
|
const vertical = orientation === "vertical" || orientation === "both";
|
|
87
|
+
const rtl = horizontal && isRtl(this.element);
|
|
88
|
+
const forwardKey = rtl ? "ArrowLeft" : "ArrowRight";
|
|
89
|
+
const backwardKey = rtl ? "ArrowRight" : "ArrowLeft";
|
|
75
90
|
let next;
|
|
76
|
-
if (horizontal && event.key ===
|
|
91
|
+
if (horizontal && event.key === forwardKey || vertical && event.key === "ArrowDown") {
|
|
77
92
|
next = rovingMove(current, length, 1, wrap);
|
|
78
|
-
} else if (horizontal && event.key ===
|
|
93
|
+
} else if (horizontal && event.key === backwardKey || vertical && event.key === "ArrowUp") {
|
|
79
94
|
next = rovingMove(current, length, -1, wrap);
|
|
80
95
|
} else if (this.homeEndValue && event.key === "Home") {
|
|
81
96
|
next = 0;
|