stimeo-ui 0.9.0 → 0.10.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 +74 -0
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.js +17 -7
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.js +1 -1
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.js +2 -2
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +1 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +1 -1
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js +15 -5
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +15 -5
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +5 -4
- 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.js +1 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.js +1 -1
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +15 -5
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +2 -2
- package/dist/controllers/direct_upload_controller.js +1 -5
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +2 -2
- package/dist/controllers/drawer_controller.js +17 -7
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +5 -0
- package/dist/controllers/focus_controller.js +45 -7
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +1 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +4 -3
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +2 -2
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +3 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +1 -1
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +2 -2
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +1 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.js +1 -1
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +2 -2
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +1 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +27 -2
- package/dist/controllers/password_reveal_controller.js +102 -3
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +71 -17
- package/dist/controllers/password_strength_controller.js +287 -42
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +3 -2
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -1
- 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.map +1 -1
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_before_cache_controller.d.ts +1 -2
- package/dist/controllers/reset_before_cache_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +1 -1
- package/dist/controllers/resizable_controller.js +1 -1
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +2 -1
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +33 -11
- package/dist/controllers/scroll_restore_controller.js +127 -27
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +42 -10
- package/dist/controllers/scroll_visibility_controller.js +167 -14
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.js +15 -5
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.js +0 -5
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +3 -2
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +32 -6
- package/dist/controllers/theme_controller.js +151 -40
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +2 -2
- package/dist/controllers/toast_controller.js +2 -2
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +11 -7
- package/dist/controllers/transition_controller.js +79 -15
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +740 -178
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +3 -4
- package/dist/inspector/cli.js +1 -0
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +6 -5
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +3 -3
- package/dist/inspector/manifest.json +7 -4
- package/dist/positioning/index.d.ts +18 -4
- package/dist/positioning/index.js +84 -25
- package/dist/positioning/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
// src/controllers/scroll_restore_controller.ts
|
|
4
|
+
function parseStored(raw) {
|
|
5
|
+
if (raw === null) return null;
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(raw);
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function storedOffset(value) {
|
|
13
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
14
|
+
}
|
|
4
15
|
var ScrollRestoreController = class extends Controller {
|
|
5
16
|
static values = {
|
|
6
17
|
key: { type: String, default: "" },
|
|
@@ -13,6 +24,26 @@ var ScrollRestoreController = class extends Controller {
|
|
|
13
24
|
/** Last offset captured while the element was live; persisted as-is on teardown. */
|
|
14
25
|
#lastTop = 0;
|
|
15
26
|
#lastLeft = 0;
|
|
27
|
+
/**
|
|
28
|
+
* The offsets this controller's own restore produced, awaiting the `scroll`
|
|
29
|
+
* the engine fires for them. A restore the layout cannot reach yet lands short,
|
|
30
|
+
* so treating that echo as the reader's position would cut the saved offset
|
|
31
|
+
* down to whatever the unfinished layout allowed.
|
|
32
|
+
*/
|
|
33
|
+
#echoTop = null;
|
|
34
|
+
#echoLeft = null;
|
|
35
|
+
/** Offsets stored for an axis that is not tracked, carried through saves. */
|
|
36
|
+
#carried = {};
|
|
37
|
+
/**
|
|
38
|
+
* Whether each axis holds an offset worth saving — one this namespace restored
|
|
39
|
+
* or the reader moved. A save writes only the axes that are marked, so an
|
|
40
|
+
* offset taken under a different key or axis is never re-published as if the
|
|
41
|
+
* reader had left it there.
|
|
42
|
+
*/
|
|
43
|
+
#capturedTop = false;
|
|
44
|
+
#capturedLeft = false;
|
|
45
|
+
/** Whether the controller is connected; Value callbacks outside that window do not resync. */
|
|
46
|
+
#connected = false;
|
|
16
47
|
#onScroll = () => {
|
|
17
48
|
this.#capture();
|
|
18
49
|
if (this.#rafId !== null) return;
|
|
@@ -22,58 +53,127 @@ var ScrollRestoreController = class extends Controller {
|
|
|
22
53
|
});
|
|
23
54
|
};
|
|
24
55
|
connect() {
|
|
56
|
+
this.#connected = true;
|
|
25
57
|
this.#storageKey = this.#resolveKey();
|
|
26
|
-
if (!this.#storageKey) return;
|
|
27
|
-
this.#restore();
|
|
28
58
|
this.element.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
59
|
+
if (this.#storageKey) this.#restore();
|
|
29
60
|
}
|
|
30
61
|
disconnect() {
|
|
31
|
-
|
|
62
|
+
this.#connected = false;
|
|
32
63
|
this.element.removeEventListener("scroll", this.#onScroll);
|
|
64
|
+
this.#cancelFrame();
|
|
65
|
+
this.#persist();
|
|
66
|
+
}
|
|
67
|
+
/** Re-derives the namespace when application code or a Turbo morph moves `key`. */
|
|
68
|
+
keyValueChanged() {
|
|
69
|
+
this.#resync();
|
|
70
|
+
}
|
|
71
|
+
/** Re-derives the tracked axes when application code or a Turbo morph moves `axis`. */
|
|
72
|
+
axisValueChanged() {
|
|
73
|
+
this.#resync();
|
|
74
|
+
}
|
|
75
|
+
/** Rebuilds the persistence state around the Values as they now read. */
|
|
76
|
+
#resync() {
|
|
77
|
+
if (!this.#connected) return;
|
|
78
|
+
this.#cancelFrame();
|
|
79
|
+
this.#storageKey = this.#resolveKey();
|
|
80
|
+
this.#echoTop = null;
|
|
81
|
+
this.#echoLeft = null;
|
|
82
|
+
this.#carried = {};
|
|
83
|
+
this.#capturedTop = false;
|
|
84
|
+
this.#capturedLeft = false;
|
|
85
|
+
if (this.#storageKey) this.#restore();
|
|
86
|
+
}
|
|
87
|
+
/** Drops the pending coalesced save, if one is queued. */
|
|
88
|
+
#cancelFrame() {
|
|
33
89
|
if (this.#rafId !== null) {
|
|
34
90
|
cancelAnimationFrame(this.#rafId);
|
|
35
91
|
this.#rafId = null;
|
|
36
92
|
}
|
|
37
|
-
this.#persist();
|
|
38
93
|
}
|
|
39
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Records the live scroll offset for the configured axis.
|
|
96
|
+
*
|
|
97
|
+
* An axis holds its echo until that axis actually moves. A scroll event names
|
|
98
|
+
* no axis, so consuming both on the first one to arrive would leave the other
|
|
99
|
+
* unguarded: on `both`, moving one axis would take the clamped reading of the
|
|
100
|
+
* untouched axis as the reader's own position and save it, cutting the stored
|
|
101
|
+
* offset down to whatever the layout could reach at restore time.
|
|
102
|
+
*/
|
|
40
103
|
#capture() {
|
|
41
|
-
if (this.#tracksVertical)
|
|
42
|
-
|
|
104
|
+
if (this.#tracksVertical) {
|
|
105
|
+
const top = this.element.scrollTop;
|
|
106
|
+
if (top !== this.#echoTop) {
|
|
107
|
+
this.#echoTop = null;
|
|
108
|
+
this.#lastTop = top;
|
|
109
|
+
this.#capturedTop = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (this.#tracksHorizontal) {
|
|
113
|
+
const left = this.element.scrollLeft;
|
|
114
|
+
if (left !== this.#echoLeft) {
|
|
115
|
+
this.#echoLeft = null;
|
|
116
|
+
this.#lastLeft = left;
|
|
117
|
+
this.#capturedLeft = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
43
120
|
}
|
|
44
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Writes the marked axes, carrying through any offset held for an untracked one.
|
|
123
|
+
* Without a key, or with neither axis marked, it writes nothing.
|
|
124
|
+
*/
|
|
45
125
|
#persist() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (this.#
|
|
126
|
+
if (!this.#storageKey || !(this.#capturedTop || this.#capturedLeft)) return;
|
|
127
|
+
const data = { ...this.#carried };
|
|
128
|
+
if (this.#tracksVertical && this.#capturedTop) data.top = this.#lastTop;
|
|
129
|
+
if (this.#tracksHorizontal && this.#capturedLeft) data.left = this.#lastLeft;
|
|
49
130
|
try {
|
|
50
|
-
sessionStorage.setItem(this.#storageKey, JSON.stringify(data));
|
|
131
|
+
window.sessionStorage.setItem(this.#storageKey, JSON.stringify(data));
|
|
51
132
|
} catch {
|
|
52
133
|
}
|
|
53
134
|
}
|
|
54
135
|
/** Applies the persisted scroll offset, if any, without moving focus. */
|
|
55
136
|
#restore() {
|
|
56
|
-
let raw
|
|
57
|
-
try {
|
|
58
|
-
raw = sessionStorage.getItem(this.#storageKey);
|
|
59
|
-
} catch {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (raw === null) return;
|
|
63
|
-
let data;
|
|
137
|
+
let raw;
|
|
64
138
|
try {
|
|
65
|
-
|
|
139
|
+
raw = window.sessionStorage.getItem(this.#storageKey);
|
|
66
140
|
} catch {
|
|
67
141
|
return;
|
|
68
142
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
143
|
+
const data = parseStored(raw);
|
|
144
|
+
if (data === null) return;
|
|
145
|
+
this.#echoTop = null;
|
|
146
|
+
this.#echoLeft = null;
|
|
147
|
+
const top = storedOffset(data.top);
|
|
148
|
+
const left = storedOffset(data.left);
|
|
149
|
+
const options = { behavior: "instant" };
|
|
150
|
+
let requested = false;
|
|
151
|
+
if (this.#tracksVertical) {
|
|
152
|
+
if (top !== null) {
|
|
153
|
+
options.top = top;
|
|
154
|
+
this.#lastTop = top;
|
|
155
|
+
this.#capturedTop = true;
|
|
156
|
+
requested = true;
|
|
157
|
+
}
|
|
158
|
+
} else if (data.top !== void 0) {
|
|
159
|
+
this.#carried.top = data.top;
|
|
72
160
|
}
|
|
73
|
-
if (this.#tracksHorizontal
|
|
74
|
-
|
|
75
|
-
|
|
161
|
+
if (this.#tracksHorizontal) {
|
|
162
|
+
if (left !== null) {
|
|
163
|
+
options.left = left;
|
|
164
|
+
this.#lastLeft = left;
|
|
165
|
+
this.#capturedLeft = true;
|
|
166
|
+
requested = true;
|
|
167
|
+
}
|
|
168
|
+
} else if (data.left !== void 0) {
|
|
169
|
+
this.#carried.left = data.left;
|
|
76
170
|
}
|
|
171
|
+
if (!requested) return;
|
|
172
|
+
const beforeTop = this.element.scrollTop;
|
|
173
|
+
const beforeLeft = this.element.scrollLeft;
|
|
174
|
+
this.element.scrollTo(options);
|
|
175
|
+
if (this.element.scrollTop !== beforeTop) this.#echoTop = this.element.scrollTop;
|
|
176
|
+
if (this.element.scrollLeft !== beforeLeft) this.#echoLeft = this.element.scrollLeft;
|
|
77
177
|
}
|
|
78
178
|
/** The `sessionStorage` key: explicit `key`, else the element `id`, else none. */
|
|
79
179
|
#resolveKey() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/scroll_restore_controller.ts"],"names":[],"mappings":"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","file":"scroll_restore_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Persisted scroll position; fields are present only for the saved axis. */\ninterface StoredScroll {\n top?: number;\n left?: number;\n}\n\n/**\n * Headless **Scroll Restore** behavior: persists and restores the scroll\n * position of an inner scrolling region across Turbo Drive navigations (and full\n * reloads within the same tab session). No APG widget — a pure state-preservation\n * utility, like {@link import(\"./scroll_visibility_controller\").ScrollVisibilityController}\n * and the Sticky State Observer.\n *\n * Markup contract (identifier: `stimeo--scroll-restore`):\n * <div data-controller=\"stimeo--scroll-restore\"\n * data-stimeo--scroll-restore-key-value=\"sidebar\"\n * style=\"overflow: auto\">\n * …long content…\n * </div>\n *\n * Turbo swaps the whole `<body>` on navigation, so an inner scroll container is\n * rebuilt with `scrollTop` reset to 0. Rather than have every app hand-write a\n * controller for this (which contradicts a \"ship the behavior\" library), this\n * persists the offset under a stable key in `sessionStorage` and restores it on\n * `connect()`.\n *\n * @remarks\n * Behavior only — it sets no ARIA/`data-*`/CSS and never moves focus (restore is\n * a plain `scrollTop`/`scrollLeft` assignment). The `scroll` listener is internal\n * and `passive` (no consumer `data-action` needed): each event records the live\n * offset synchronously, and a `requestAnimationFrame` coalesces the writes to\n * `sessionStorage`. On `disconnect()` (Turbo navigation included) it flushes the\n * **last captured** offset rather than re-reading the element — by the time Turbo\n * fires `disconnect` it has already detached the node, whose `scrollTop` then\n * reads `0`, so a fresh read would clobber the saved position with `0`. Keying by\n * `key` (falling back to the element `id`) in `sessionStorage` makes it\n * multi-instance safe and survives full reloads — unlike a module-scope variable,\n * which only survives Turbo Drive and assumes a single instance.\n */\nexport class ScrollRestoreController extends Controller<HTMLElement> {\n static override values = {\n key: { type: String, default: \"\" },\n axis: { type: String, default: \"vertical\" },\n };\n\n declare keyValue: string;\n declare axisValue: string;\n\n /** Pending rAF id that coalesces scroll bursts into one save. */\n #rafId: number | null = null;\n /** Resolved storage key; empty disables persistence (no key and no id). */\n #storageKey = \"\";\n /** Last offset captured while the element was live; persisted as-is on teardown. */\n #lastTop = 0;\n #lastLeft = 0;\n\n readonly #onScroll = (): void => {\n // Capture synchronously while the element is still connected and measurable;\n // the rAF only debounces the sessionStorage write, never the read.\n this.#capture();\n if (this.#rafId !== null) return;\n this.#rafId = requestAnimationFrame(() => {\n this.#rafId = null;\n this.#persist();\n });\n };\n\n override connect(): void {\n this.#storageKey = this.#resolveKey();\n if (!this.#storageKey) return; // No stable key → do nothing (avoid mixing pages).\n this.#restore();\n this.element.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n }\n\n override disconnect(): void {\n if (!this.#storageKey) return;\n this.element.removeEventListener(\"scroll\", this.#onScroll);\n if (this.#rafId !== null) {\n cancelAnimationFrame(this.#rafId);\n this.#rafId = null;\n }\n // Flush the last captured offset (a throttled burst may have left the final\n // frame unsaved). We do NOT re-read the element here: Turbo detaches the node\n // before disconnect, so its scrollTop is 0 and would overwrite the real value.\n this.#persist();\n }\n\n /** Records the live scroll offset for the configured axis. */\n #capture(): void {\n if (this.#tracksVertical) this.#lastTop = this.element.scrollTop;\n if (this.#tracksHorizontal) this.#lastLeft = this.element.scrollLeft;\n }\n\n /** Persists the last captured scroll offset for the configured axis. */\n #persist(): void {\n const data: StoredScroll = {};\n if (this.#tracksVertical) data.top = this.#lastTop;\n if (this.#tracksHorizontal) data.left = this.#lastLeft;\n try {\n sessionStorage.setItem(this.#storageKey, JSON.stringify(data));\n } catch {\n // sessionStorage can throw (private mode / quota); persistence is best-effort.\n }\n }\n\n /** Applies the persisted scroll offset, if any, without moving focus. */\n #restore(): void {\n let raw: string | null = null;\n try {\n raw = sessionStorage.getItem(this.#storageKey);\n } catch {\n return;\n }\n if (raw === null) return;\n let data: StoredScroll;\n try {\n data = JSON.parse(raw) as StoredScroll;\n } catch {\n return;\n }\n // Seed the capture cache from the restored value so an immediate teardown\n // (before any scroll) re-persists the restored position, not 0. Guard by the\n // configured axis so a stale field from a different axis is never applied.\n if (this.#tracksVertical && typeof data.top === \"number\") {\n this.element.scrollTop = data.top;\n this.#lastTop = data.top;\n }\n if (this.#tracksHorizontal && typeof data.left === \"number\") {\n this.element.scrollLeft = data.left;\n this.#lastLeft = data.left;\n }\n }\n\n /** The `sessionStorage` key: explicit `key`, else the element `id`, else none. */\n #resolveKey(): string {\n const base = this.keyValue || this.element.id;\n return base ? `stimeo--scroll-restore:${base}` : \"\";\n }\n\n get #tracksVertical(): boolean {\n return this.axisValue !== \"horizontal\";\n }\n\n get #tracksHorizontal(): boolean {\n return this.axisValue === \"horizontal\" || this.axisValue === \"both\";\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/controllers/scroll_restore_controller.ts"],"names":[],"mappings":"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","file":"scroll_restore_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Persisted scroll position; fields are present only for the saved axis. */\ninterface StoredScroll {\n top?: number;\n left?: number;\n}\n\n/**\n * Reads one persisted entry, or `null` when there is nothing usable to read.\n *\n * `JSON.parse` returns `null` for the literal `null` without throwing, so the\n * parse guard alone would hand back a value with no fields to read. Every field the\n * caller applies is validated there, which is what makes a bare literal harmless here.\n */\nfunction parseStored(raw: string | null): StoredScroll | null {\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as StoredScroll | null;\n } catch {\n return null;\n }\n}\n\n/** One axis offset from a persisted entry, or `null` when it cannot be applied. */\nfunction storedOffset(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\n/**\n * Headless **Scroll Restore** behavior: persists and restores the scroll\n * position of an inner scrolling region across Turbo Drive navigations (and full\n * reloads within the same tab session). No APG widget — a pure state-preservation\n * utility, like {@link import(\"./scroll_visibility_controller\").ScrollVisibilityController}\n * and the Sticky State Observer.\n *\n * Markup contract (identifier: `stimeo--scroll-restore`):\n * <div data-controller=\"stimeo--scroll-restore\"\n * data-stimeo--scroll-restore-key-value=\"sidebar\"\n * style=\"overflow: auto\">\n * …long content…\n * </div>\n *\n * Turbo swaps the whole `<body>` on navigation, so an inner scroll container is\n * rebuilt with `scrollTop` reset to 0. Rather than have every app hand-write a\n * controller for this (which contradicts a \"ship the behavior\" library), this\n * persists the offset under a stable key in `sessionStorage` and restores it on\n * `connect()`.\n *\n * Values:\n * - `key` — the `sessionStorage` namespace. Empty falls back to the element `id`;\n * with neither, nothing is saved or restored, so positions from unrelated pages\n * cannot mix.\n * - `axis` — which offset is tracked: `vertical` (default, `scrollTop`),\n * `horizontal` (`scrollLeft`) or `both`. An offset stored for an axis that is\n * not tracked is neither applied nor discarded.\n *\n * Both Values are re-derived when they change at runtime, so a morph that\n * rewrites the attribute in place switches namespace or axis instead of going on\n * writing under the one that held at connect. A `key` left empty resolves through\n * the element `id`, which no Value callback observes; runtime `id` changes are\n * out of scope.\n *\n * @remarks\n * Behavior only — it sets no ARIA/`data-*`/CSS and never moves focus. Restoring\n * goes through `scrollTo({ behavior: \"instant\" })` so that a consumer's\n * `scroll-behavior: smooth` cannot turn it into an animation whose intermediate\n * offsets would be recorded as the reader's own scrolling. The `scroll` listener\n * is internal and `passive` (no consumer `data-action` needed): each event\n * records the live offset synchronously, and a `requestAnimationFrame` coalesces\n * the writes to `sessionStorage`. On `disconnect()` (Turbo navigation included)\n * it flushes the **last captured** offset rather than re-reading the element — by\n * the time Turbo fires `disconnect` it has already detached the node, whose\n * `scrollTop` then reads `0`, so a fresh read would clobber the saved position\n * with `0`. Keying by `key` (falling back to the element `id`) in\n * `sessionStorage` makes it multi-instance safe and survives full reloads —\n * unlike a module-scope variable, which only survives Turbo Drive and assumes a\n * single instance. Storage access is best-effort: a browser that refuses it\n * (private mode, quota) leaves the element working, just without persistence.\n */\nexport class ScrollRestoreController extends Controller<HTMLElement> {\n static override values = {\n key: { type: String, default: \"\" },\n axis: { type: String, default: \"vertical\" },\n };\n\n declare keyValue: string;\n declare axisValue: string;\n\n /** Pending rAF id that coalesces scroll bursts into one save. */\n #rafId: number | null = null;\n /** Resolved storage key; empty disables persistence (no key and no id). */\n #storageKey = \"\";\n /** Last offset captured while the element was live; persisted as-is on teardown. */\n #lastTop = 0;\n #lastLeft = 0;\n /**\n * The offsets this controller's own restore produced, awaiting the `scroll`\n * the engine fires for them. A restore the layout cannot reach yet lands short,\n * so treating that echo as the reader's position would cut the saved offset\n * down to whatever the unfinished layout allowed.\n */\n #echoTop: number | null = null;\n #echoLeft: number | null = null;\n /** Offsets stored for an axis that is not tracked, carried through saves. */\n #carried: StoredScroll = {};\n /**\n * Whether each axis holds an offset worth saving — one this namespace restored\n * or the reader moved. A save writes only the axes that are marked, so an\n * offset taken under a different key or axis is never re-published as if the\n * reader had left it there.\n */\n #capturedTop = false;\n #capturedLeft = false;\n /** Whether the controller is connected; Value callbacks outside that window do not resync. */\n #connected = false;\n\n readonly #onScroll = (): void => {\n // Capture synchronously while the element is still connected and measurable;\n // the rAF only debounces the sessionStorage write, never the read.\n this.#capture();\n if (this.#rafId !== null) return;\n this.#rafId = requestAnimationFrame(() => {\n this.#rafId = null;\n this.#persist();\n });\n };\n\n override connect(): void {\n this.#connected = true;\n this.#storageKey = this.#resolveKey();\n this.element.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n if (this.#storageKey) this.#restore();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.element.removeEventListener(\"scroll\", this.#onScroll);\n this.#cancelFrame();\n // Flush the last captured offset (a throttled burst may have left the final\n // frame unsaved). We do NOT re-read the element here: Turbo detaches the node\n // before disconnect, so its scrollTop is 0 and would overwrite the real value.\n this.#persist();\n }\n\n /** Re-derives the namespace when application code or a Turbo morph moves `key`. */\n keyValueChanged(): void {\n this.#resync();\n }\n\n /** Re-derives the tracked axes when application code or a Turbo morph moves `axis`. */\n axisValueChanged(): void {\n this.#resync();\n }\n\n /** Rebuilds the persistence state around the Values as they now read. */\n #resync(): void {\n if (!this.#connected) return;\n // The pending frame holds an offset belonging to the previous namespace and\n // axis, so it is dropped rather than written under the new ones.\n this.#cancelFrame();\n this.#storageKey = this.#resolveKey();\n this.#echoTop = null;\n this.#echoLeft = null;\n this.#carried = {};\n // An offset is only ever written for an axis this namespace seeded or the\n // reader moved. Clearing both marks drops the ones taken under the previous\n // key and axis, which the restore below re-establishes only where the new\n // key actually holds a value.\n this.#capturedTop = false;\n this.#capturedLeft = false;\n if (this.#storageKey) this.#restore();\n }\n\n /** Drops the pending coalesced save, if one is queued. */\n #cancelFrame(): void {\n if (this.#rafId !== null) {\n cancelAnimationFrame(this.#rafId);\n this.#rafId = null;\n }\n }\n\n /**\n * Records the live scroll offset for the configured axis.\n *\n * An axis holds its echo until that axis actually moves. A scroll event names\n * no axis, so consuming both on the first one to arrive would leave the other\n * unguarded: on `both`, moving one axis would take the clamped reading of the\n * untouched axis as the reader's own position and save it, cutting the stored\n * offset down to whatever the layout could reach at restore time.\n */\n #capture(): void {\n if (this.#tracksVertical) {\n const top = this.element.scrollTop;\n if (top !== this.#echoTop) {\n this.#echoTop = null;\n this.#lastTop = top;\n this.#capturedTop = true;\n }\n }\n if (this.#tracksHorizontal) {\n const left = this.element.scrollLeft;\n if (left !== this.#echoLeft) {\n this.#echoLeft = null;\n this.#lastLeft = left;\n this.#capturedLeft = true;\n }\n }\n }\n\n /**\n * Writes the marked axes, carrying through any offset held for an untracked one.\n * Without a key, or with neither axis marked, it writes nothing.\n */\n #persist(): void {\n if (!this.#storageKey || !(this.#capturedTop || this.#capturedLeft)) return;\n const data: StoredScroll = { ...this.#carried };\n if (this.#tracksVertical && this.#capturedTop) data.top = this.#lastTop;\n if (this.#tracksHorizontal && this.#capturedLeft) data.left = this.#lastLeft;\n try {\n window.sessionStorage.setItem(this.#storageKey, JSON.stringify(data));\n } catch {\n // sessionStorage can throw (private mode / quota); persistence is best-effort.\n }\n }\n\n /** Applies the persisted scroll offset, if any, without moving focus. */\n #restore(): void {\n let raw: string | null;\n try {\n raw = window.sessionStorage.getItem(this.#storageKey);\n } catch {\n return;\n }\n const data = parseStored(raw);\n if (data === null) return;\n\n // A restore writes its own echoes below; one left over from an earlier pass\n // would otherwise swallow the first matching move the reader makes.\n this.#echoTop = null;\n this.#echoLeft = null;\n\n const top = storedOffset(data.top);\n const left = storedOffset(data.left);\n const options: ScrollToOptions = { behavior: \"instant\" };\n let requested = false;\n // Seed the capture cache from the restored value so an immediate teardown\n // (before any scroll) re-persists the restored position, not 0. Guard by the\n // configured axis so a stale field from a different axis is never applied —\n // and keep it, since the next save would otherwise drop it.\n if (this.#tracksVertical) {\n if (top !== null) {\n options.top = top;\n this.#lastTop = top;\n this.#capturedTop = true;\n requested = true;\n }\n } else if (data.top !== undefined) {\n this.#carried.top = data.top;\n }\n if (this.#tracksHorizontal) {\n if (left !== null) {\n options.left = left;\n this.#lastLeft = left;\n this.#capturedLeft = true;\n requested = true;\n }\n } else if (data.left !== undefined) {\n this.#carried.left = data.left;\n }\n if (!requested) return;\n\n const beforeTop = this.element.scrollTop;\n const beforeLeft = this.element.scrollLeft;\n this.element.scrollTo(options);\n if (this.element.scrollTop !== beforeTop) this.#echoTop = this.element.scrollTop;\n if (this.element.scrollLeft !== beforeLeft) this.#echoLeft = this.element.scrollLeft;\n }\n\n /** The `sessionStorage` key: explicit `key`, else the element `id`, else none. */\n #resolveKey(): string {\n const base = this.keyValue || this.element.id;\n return base ? `stimeo--scroll-restore:${base}` : \"\";\n }\n\n get #tracksVertical(): boolean {\n return this.axisValue !== \"horizontal\";\n }\n\n get #tracksHorizontal(): boolean {\n return this.axisValue === \"horizontal\" || this.axisValue === \"both\";\n }\n}\n"]}
|
|
@@ -17,8 +17,11 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
17
17
|
*
|
|
18
18
|
* In `offset` mode the element is shown once the scroll source is scrolled past
|
|
19
19
|
* `offset` px; in `direction` mode it is hidden while scrolling down and shown
|
|
20
|
-
* while scrolling up.
|
|
21
|
-
*
|
|
20
|
+
* while scrolling up. A `scroll` that carries no vertical movement — a
|
|
21
|
+
* horizontal scroll, an overscroll bounce — has no direction, so `direction`
|
|
22
|
+
* mode leaves the current visibility alone. Visibility is reflected through the
|
|
23
|
+
* `hidden` attribute (so a hidden control also leaves the focus order) and
|
|
24
|
+
* `data-state`.
|
|
22
25
|
*
|
|
23
26
|
* By default the **window** is the scroll source. When the page itself does not
|
|
24
27
|
* scroll — e.g. a fixed-height app shell whose main column scrolls in a container
|
|
@@ -26,18 +29,27 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
26
29
|
* controller observes the element's scroll instead of the (never-scrolling)
|
|
27
30
|
* window. `toTop` then scrolls that same container.
|
|
28
31
|
*
|
|
29
|
-
* `change` dispatches `{ visible: boolean }
|
|
32
|
+
* `change` dispatches `{ visible: boolean }` on transitions only: connecting
|
|
33
|
+
* reflects the current scroll state onto the hooks without announcing it, so a
|
|
34
|
+
* Turbo restore does not replay the state the snapshot already carries.
|
|
30
35
|
*
|
|
31
36
|
* @remarks
|
|
32
37
|
* Behavior only — the look and any transition are the consumer's CSS. The scroll
|
|
33
38
|
* listener is `passive`, coalesced through `requestAnimationFrame`, and removed on
|
|
34
|
-
* `disconnect()` (Turbo navigation included). `
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* `disconnect()` (Turbo navigation included). `offset` / `mode` are re-read at
|
|
40
|
+
* runtime, so a Turbo morph that swaps either attribute is followed without
|
|
41
|
+
* waiting for the next scroll; a selector (`root`, `focusSelector`) or a
|
|
42
|
+
* threshold that cannot be parsed reads as its default, keeping the rest of the
|
|
43
|
+
* element alive. **A scroll never hides the control while it owns focus**: that
|
|
44
|
+
* hide waits for it to blur and is decided again from the scroll position at that
|
|
45
|
+
* moment. `toTop` honors `prefers-reduced-motion` by forcing an
|
|
46
|
+
* instant jump independently of the consumer's CSS `scroll-behavior`, and can
|
|
47
|
+
* move focus to a `focusSelector` target (given `tabindex="-1"` if needed, and
|
|
48
|
+
* focused without scrolling so the smooth scroll survives) to keep keyboard
|
|
49
|
+
* users oriented after the scroll. A live disconnect removes only a
|
|
50
|
+
* `tabindex="-1"` this controller instance added; authored tabindex values
|
|
51
|
+
* remain. This teardown does not claim to rewrite a Turbo cache snapshot that
|
|
52
|
+
* was cloned before disconnect.
|
|
41
53
|
*/
|
|
42
54
|
declare class ScrollVisibilityController extends Controller<HTMLElement> {
|
|
43
55
|
#private;
|
|
@@ -70,6 +82,26 @@ declare class ScrollVisibilityController extends Controller<HTMLElement> {
|
|
|
70
82
|
rootValue: string;
|
|
71
83
|
connect(): void;
|
|
72
84
|
disconnect(): void;
|
|
85
|
+
/** Writes the current visibility onto a control that arrives after connect. */
|
|
86
|
+
elementTargetConnected(element: HTMLElement): void;
|
|
87
|
+
/** Drops a held-back hide together with the control it was waiting on. */
|
|
88
|
+
elementTargetDisconnected(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Validates `offset` once, then re-renders.
|
|
91
|
+
*
|
|
92
|
+
* Re-renders when application code (or a Turbo morph) changes `offset` at
|
|
93
|
+
* runtime. A declaration that is not a finite number reads as the default, so
|
|
94
|
+
* the comparison path never sees `NaN` — which would answer `false` to every
|
|
95
|
+
* comparison and strand the element (in `direction` mode, even the guarantee
|
|
96
|
+
* that the very top always reveals).
|
|
97
|
+
*/
|
|
98
|
+
offsetValueChanged(): void;
|
|
99
|
+
/** Re-renders when application code (or a Turbo morph) changes `mode` at runtime. */
|
|
100
|
+
modeValueChanged(): void;
|
|
101
|
+
/** Validates `root` once so connect never parses a selector that throws. */
|
|
102
|
+
rootValueChanged(): void;
|
|
103
|
+
/** Validates `focusSelector` once so `toTop` never parses a selector that throws. */
|
|
104
|
+
focusSelectorValueChanged(): void;
|
|
73
105
|
/** Scrolls the source to the top and, optionally, moves focus to a safe target. */
|
|
74
106
|
toTop(): void;
|
|
75
107
|
}
|
|
@@ -2,6 +2,61 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/scroll_visibility_controller.ts
|
|
4
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
|
+
|
|
5
60
|
// src/utils/reduced_motion.ts
|
|
6
61
|
function prefersReducedMotion() {
|
|
7
62
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
@@ -68,10 +123,11 @@ var TabindexLoan = class {
|
|
|
68
123
|
};
|
|
69
124
|
|
|
70
125
|
// src/controllers/scroll_visibility_controller.ts
|
|
126
|
+
var DEFAULT_OFFSET = 400;
|
|
71
127
|
var ScrollVisibilityController = class extends Controller {
|
|
72
128
|
static targets = ["element"];
|
|
73
129
|
static values = {
|
|
74
|
-
offset: { type: Number, default:
|
|
130
|
+
offset: { type: Number, default: DEFAULT_OFFSET },
|
|
75
131
|
mode: { type: String, default: "offset" },
|
|
76
132
|
focusSelector: { type: String, default: "" },
|
|
77
133
|
root: { type: String, default: "" }
|
|
@@ -89,8 +145,32 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
89
145
|
* the window. Captured on connect so teardown detaches from the same source.
|
|
90
146
|
*/
|
|
91
147
|
#scrollSource = window;
|
|
148
|
+
/**
|
|
149
|
+
* Gates the declaration callbacks to the connected window.
|
|
150
|
+
*
|
|
151
|
+
* Stimulus delivers a Value callback ahead of `connect()` and again for every
|
|
152
|
+
* runtime change; without the gate, merely connecting would evaluate — and
|
|
153
|
+
* announce — before `connect()` runs its own first reflection.
|
|
154
|
+
*/
|
|
155
|
+
#connected = false;
|
|
156
|
+
/** Validated threshold; a non-finite declaration reads as the default. */
|
|
157
|
+
#offset = DEFAULT_OFFSET;
|
|
158
|
+
/** Validated `root` selector; an unparsable declaration reads as absent. */
|
|
159
|
+
#rootSelector = "";
|
|
160
|
+
/** Validated `focusSelector`; an unparsable declaration reads as absent. */
|
|
161
|
+
#focusSelector = "";
|
|
92
162
|
/** Focus targets this instance lent a `tabindex` to. */
|
|
93
163
|
#tabindex = new TabindexLoan();
|
|
164
|
+
/**
|
|
165
|
+
* The hide held back while the control itself owns focus.
|
|
166
|
+
*
|
|
167
|
+
* Completing the deferral re-runs the ordinary evaluation rather than applying
|
|
168
|
+
* the stale decision: by the time focus leaves, the scroll position may have
|
|
169
|
+
* moved back past the threshold.
|
|
170
|
+
*/
|
|
171
|
+
#pendingHide = new BlurDeferral(() => {
|
|
172
|
+
if (this.#connected) this.#evaluate();
|
|
173
|
+
});
|
|
94
174
|
#onScroll = () => {
|
|
95
175
|
if (this.#rafId !== null) return;
|
|
96
176
|
this.#rafId = requestAnimationFrame(() => {
|
|
@@ -102,61 +182,134 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
102
182
|
this.#scrollSource = this.#resolveScrollSource();
|
|
103
183
|
this.#lastScrollY = this.#scrollY();
|
|
104
184
|
this.#scrollSource.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
105
|
-
this.#evaluate();
|
|
185
|
+
this.#evaluate(false);
|
|
186
|
+
this.#connected = true;
|
|
106
187
|
}
|
|
107
188
|
disconnect() {
|
|
189
|
+
this.#connected = false;
|
|
108
190
|
this.#scrollSource.removeEventListener("scroll", this.#onScroll);
|
|
109
191
|
if (this.#rafId !== null) {
|
|
110
192
|
cancelAnimationFrame(this.#rafId);
|
|
111
193
|
this.#rafId = null;
|
|
112
194
|
}
|
|
195
|
+
this.#pendingHide.releaseAll();
|
|
113
196
|
this.#tabindex.returnAll();
|
|
114
197
|
this.#visible = null;
|
|
115
198
|
}
|
|
199
|
+
/** Writes the current visibility onto a control that arrives after connect. */
|
|
200
|
+
elementTargetConnected(element) {
|
|
201
|
+
if (this.#visible !== null) element.hidden = !this.#visible;
|
|
202
|
+
}
|
|
203
|
+
/** Drops a held-back hide together with the control it was waiting on. */
|
|
204
|
+
elementTargetDisconnected() {
|
|
205
|
+
this.#pendingHide.releaseAll();
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Validates `offset` once, then re-renders.
|
|
209
|
+
*
|
|
210
|
+
* Re-renders when application code (or a Turbo morph) changes `offset` at
|
|
211
|
+
* runtime. A declaration that is not a finite number reads as the default, so
|
|
212
|
+
* the comparison path never sees `NaN` — which would answer `false` to every
|
|
213
|
+
* comparison and strand the element (in `direction` mode, even the guarantee
|
|
214
|
+
* that the very top always reveals).
|
|
215
|
+
*/
|
|
216
|
+
offsetValueChanged() {
|
|
217
|
+
this.#offset = Number.isFinite(this.offsetValue) ? this.offsetValue : DEFAULT_OFFSET;
|
|
218
|
+
if (this.#connected) this.#evaluate();
|
|
219
|
+
}
|
|
220
|
+
/** Re-renders when application code (or a Turbo morph) changes `mode` at runtime. */
|
|
221
|
+
modeValueChanged() {
|
|
222
|
+
if (this.#connected) this.#evaluate();
|
|
223
|
+
}
|
|
224
|
+
/** Validates `root` once so connect never parses a selector that throws. */
|
|
225
|
+
rootValueChanged() {
|
|
226
|
+
this.#rootSelector = this.#validSelector(this.rootValue);
|
|
227
|
+
}
|
|
228
|
+
/** Validates `focusSelector` once so `toTop` never parses a selector that throws. */
|
|
229
|
+
focusSelectorValueChanged() {
|
|
230
|
+
this.#focusSelector = this.#validSelector(this.focusSelectorValue);
|
|
231
|
+
}
|
|
116
232
|
/** Scrolls the source to the top and, optionally, moves focus to a safe target. */
|
|
117
233
|
toTop() {
|
|
118
234
|
const behavior = prefersReducedMotion() ? "instant" : "smooth";
|
|
119
235
|
this.#scrollSource.scrollTo({ top: 0, behavior });
|
|
120
|
-
if (this
|
|
121
|
-
const target = document.querySelector(this
|
|
236
|
+
if (this.#focusSelector) {
|
|
237
|
+
const target = document.querySelector(this.#focusSelector);
|
|
122
238
|
if (target) {
|
|
123
239
|
this.#tabindex.lend(target);
|
|
124
|
-
target.focus();
|
|
240
|
+
target.focus({ preventScroll: true });
|
|
125
241
|
}
|
|
126
242
|
}
|
|
127
243
|
}
|
|
128
|
-
/**
|
|
129
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Decides the next visibility from the current scroll state and applies it.
|
|
246
|
+
*
|
|
247
|
+
* @param notify - whether a transition announces itself. The reflection
|
|
248
|
+
* `connect()` performs is the current state, not a change.
|
|
249
|
+
*
|
|
250
|
+
* @stimeoRenderRoot
|
|
251
|
+
*/
|
|
252
|
+
#evaluate(notify = true) {
|
|
130
253
|
const y = this.#scrollY();
|
|
131
254
|
let nextVisible;
|
|
132
255
|
if (this.modeValue === "direction") {
|
|
133
|
-
if (y <= this
|
|
256
|
+
if (y <= this.#offset) {
|
|
134
257
|
nextVisible = true;
|
|
258
|
+
} else if (y === this.#lastScrollY) {
|
|
259
|
+
return;
|
|
135
260
|
} else {
|
|
136
261
|
nextVisible = y < this.#lastScrollY;
|
|
137
262
|
}
|
|
138
263
|
} else {
|
|
139
|
-
nextVisible = y > this
|
|
264
|
+
nextVisible = y > this.#offset;
|
|
140
265
|
}
|
|
141
266
|
this.#lastScrollY = y;
|
|
142
|
-
this.#setVisible(nextVisible);
|
|
267
|
+
this.#setVisible(nextVisible, notify);
|
|
143
268
|
}
|
|
144
269
|
/** Applies visibility to the target, syncing `hidden`, `data-state`, `change`. */
|
|
145
|
-
#setVisible(next) {
|
|
270
|
+
#setVisible(next, notify) {
|
|
146
271
|
if (next === this.#visible) return;
|
|
272
|
+
const focused = !next && this.hasElementTarget ? this.#focusedWithin() : null;
|
|
273
|
+
if (focused) {
|
|
274
|
+
this.#pendingHide.deferOnly(focused);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
147
277
|
this.#visible = next;
|
|
148
278
|
if (this.hasElementTarget) this.elementTarget.hidden = !next;
|
|
149
279
|
this.element.setAttribute("data-state", next ? "visible" : "hidden");
|
|
150
|
-
this.dispatch("change", { detail: { visible: next } });
|
|
280
|
+
if (notify) this.dispatch("change", { detail: { visible: next } });
|
|
151
281
|
}
|
|
152
282
|
/** Resolves the scroll source from `root` (falling back to the window). */
|
|
153
283
|
#resolveScrollSource() {
|
|
154
|
-
if (this
|
|
155
|
-
const root = document.querySelector(this
|
|
284
|
+
if (this.#rootSelector) {
|
|
285
|
+
const root = document.querySelector(this.#rootSelector);
|
|
156
286
|
if (root) return root;
|
|
157
287
|
}
|
|
158
288
|
return window;
|
|
159
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* The focus owner inside the target, or `null` when focus is elsewhere.
|
|
292
|
+
*
|
|
293
|
+
* `blur` does not bubble, so the deferral has to ride the focused element
|
|
294
|
+
* itself: waiting on a container that never receives the event would hold the
|
|
295
|
+
* hide forever.
|
|
296
|
+
*/
|
|
297
|
+
#focusedWithin() {
|
|
298
|
+
const focused = document.activeElement;
|
|
299
|
+
if (focused instanceof HTMLElement && this.elementTarget.contains(focused)) return focused;
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
/** Returns `declared` when it parses as a selector, and `""` when it does not. */
|
|
303
|
+
#validSelector(declared) {
|
|
304
|
+
if (declared.length > 0) {
|
|
305
|
+
try {
|
|
306
|
+
this.element.matches(declared);
|
|
307
|
+
return declared;
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return "";
|
|
312
|
+
}
|
|
160
313
|
#scrollY() {
|
|
161
314
|
if (this.#scrollSource === window) {
|
|
162
315
|
return window.scrollY ?? window.pageYOffset ?? 0;
|