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
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/scroll_visibility_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/reduced_motion.ts
|
|
6
|
+
function prefersReducedMotion() {
|
|
7
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/utils/tabindex_loan.ts
|
|
11
|
+
var TabindexLoan = class {
|
|
12
|
+
#value;
|
|
13
|
+
#lent = /* @__PURE__ */ new Set();
|
|
14
|
+
/**
|
|
15
|
+
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
16
|
+
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
17
|
+
* which a scroll region with no focusable content of its own needs.
|
|
18
|
+
*/
|
|
19
|
+
constructor(value = "-1") {
|
|
20
|
+
this.#value = value;
|
|
21
|
+
}
|
|
22
|
+
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
23
|
+
lend(element) {
|
|
24
|
+
if (element.hasAttribute("tabindex")) return;
|
|
25
|
+
element.setAttribute("tabindex", this.#value);
|
|
26
|
+
this.#lent.add(element);
|
|
27
|
+
}
|
|
28
|
+
/** Takes back every loan whose value is still the one that was lent. */
|
|
29
|
+
returnAll() {
|
|
30
|
+
for (const element of this.#lent) {
|
|
31
|
+
if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
|
|
32
|
+
}
|
|
33
|
+
this.#lent.clear();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
3
37
|
// src/controllers/scroll_visibility_controller.ts
|
|
4
38
|
var ScrollVisibilityController = class extends Controller {
|
|
5
39
|
static targets = ["element"];
|
|
@@ -11,7 +45,7 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
11
45
|
};
|
|
12
46
|
static actions = ["toTop"];
|
|
13
47
|
static events = ["change"];
|
|
14
|
-
/** Pending rAF id
|
|
48
|
+
/** Pending rAF id that coalesces scroll bursts into one measurement. */
|
|
15
49
|
#rafId = null;
|
|
16
50
|
/** Previous scroll position, for `direction` mode delta detection. */
|
|
17
51
|
#lastScrollY = 0;
|
|
@@ -22,6 +56,8 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
22
56
|
* the window. Captured on connect so teardown detaches from the same source.
|
|
23
57
|
*/
|
|
24
58
|
#scrollSource = window;
|
|
59
|
+
/** Focus targets this instance lent a `tabindex` to. */
|
|
60
|
+
#tabindex = new TabindexLoan();
|
|
25
61
|
#onScroll = () => {
|
|
26
62
|
if (this.#rafId !== null) return;
|
|
27
63
|
this.#rafId = requestAnimationFrame(() => {
|
|
@@ -41,16 +77,17 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
41
77
|
cancelAnimationFrame(this.#rafId);
|
|
42
78
|
this.#rafId = null;
|
|
43
79
|
}
|
|
80
|
+
this.#tabindex.returnAll();
|
|
44
81
|
this.#visible = null;
|
|
45
82
|
}
|
|
46
83
|
/** Scrolls the source to the top and, optionally, moves focus to a safe target. */
|
|
47
84
|
toTop() {
|
|
48
|
-
const behavior =
|
|
85
|
+
const behavior = prefersReducedMotion() ? "instant" : "smooth";
|
|
49
86
|
this.#scrollSource.scrollTo({ top: 0, behavior });
|
|
50
87
|
if (this.focusSelectorValue) {
|
|
51
88
|
const target = document.querySelector(this.focusSelectorValue);
|
|
52
89
|
if (target) {
|
|
53
|
-
|
|
90
|
+
this.#tabindex.lend(target);
|
|
54
91
|
target.focus();
|
|
55
92
|
}
|
|
56
93
|
}
|
|
@@ -93,9 +130,6 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
93
130
|
}
|
|
94
131
|
return this.#scrollSource.scrollTop;
|
|
95
132
|
}
|
|
96
|
-
#prefersReducedMotion() {
|
|
97
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
98
|
-
}
|
|
99
133
|
};
|
|
100
134
|
|
|
101
135
|
export { ScrollVisibilityController };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/scroll_visibility_controller.ts"],"names":[],"mappings":"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","file":"scroll_visibility_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless **Scroll Visibility** behavior: shows or hides an element based on\n * scroll amount or direction (back-to-top buttons, hide-on-scroll headers). No\n * dedicated APG pattern; when the element is a button it follows the Button\n * practice.\n *\n * Markup contract (identifier: `stimeo--scroll-visibility`):\n * <div data-controller=\"stimeo--scroll-visibility\"\n * data-stimeo--scroll-visibility-offset-value=\"400\"\n * data-stimeo--scroll-visibility-mode-value=\"offset\">\n * <button type=\"button\" hidden\n * data-stimeo--scroll-visibility-target=\"element\"\n * data-action=\"stimeo--scroll-visibility#toTop\">Back to top</button>\n * </div>\n *\n * In `offset` mode the element is shown once the scroll source is scrolled past\n * `offset` px; in `direction` mode it is hidden while scrolling down and shown\n * while scrolling up. Visibility is reflected through the `hidden` attribute (so a\n * hidden control also leaves the focus order) and `data-state`.\n *\n * By default the **window** is the scroll source. When the page itself does not\n * scroll — e.g. a fixed-height app shell whose main column scrolls in a container\n * (`overflow: auto`) — point `root` at that container (a CSS selector) so the\n * controller observes the element's scroll instead of the (never-scrolling)\n * window. `toTop` then scrolls that same container.\n *\n * @remarks\n * Behavior only — the look and any transition are the consumer's CSS. The scroll\n * listener is `passive`, coalesced through `requestAnimationFrame`, and removed on\n * `disconnect()` (Turbo navigation included). `toTop` honors\n * `prefers-reduced-motion` by falling back to an instant jump, and can move focus\n * to a `focusSelector` target (given `tabindex=\"-1\"` if needed) to keep keyboard\n * users oriented after the scroll.\n */\nexport class ScrollVisibilityController extends Controller<HTMLElement> {\n static override targets = [\"element\"];\n static override values = {\n offset: { type: Number, default: 400 },\n mode: { type: String, default: \"offset\" },\n focusSelector: { type: String, default: \"\" },\n root: { type: String, default: \"\" },\n };\n static actions = [\"toTop\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly elementTarget: HTMLElement;\n declare readonly hasElementTarget: boolean;\n\n declare offsetValue: number;\n declare modeValue: string;\n declare focusSelectorValue: string;\n declare rootValue: string;\n\n /** Pending rAF id used to coalesce scroll bursts into one measurement. */\n #rafId: number | null = null;\n /** Previous scroll position, for `direction` mode delta detection. */\n #lastScrollY = 0;\n /** Current visibility, tracked to dispatch `change` only on real transitions. */\n #visible: boolean | null = null;\n /**\n * The observed scroll source: a container element when `root` resolves, else\n * the window. Captured on connect so teardown detaches from the same source.\n */\n #scrollSource: HTMLElement | Window = window;\n\n readonly #onScroll = (): void => {\n if (this.#rafId !== null) return;\n this.#rafId = requestAnimationFrame(() => {\n this.#rafId = null;\n this.#evaluate();\n });\n };\n\n override connect(): void {\n this.#scrollSource = this.#resolveScrollSource();\n this.#lastScrollY = this.#scrollY();\n this.#scrollSource.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n this.#evaluate();\n }\n\n override disconnect(): void {\n this.#scrollSource.removeEventListener(\"scroll\", this.#onScroll);\n if (this.#rafId !== null) {\n cancelAnimationFrame(this.#rafId);\n this.#rafId = null;\n }\n this.#visible = null;\n }\n\n /** Scrolls the source to the top and, optionally, moves focus to a safe target. */\n toTop(): void {\n const behavior: ScrollBehavior = this.#prefersReducedMotion() ? \"auto\" : \"smooth\";\n this.#scrollSource.scrollTo({ top: 0, behavior });\n if (this.focusSelectorValue) {\n const target = document.querySelector<HTMLElement>(this.focusSelectorValue);\n if (target) {\n if (!target.hasAttribute(\"tabindex\")) target.setAttribute(\"tabindex\", \"-1\");\n target.focus();\n }\n }\n }\n\n /** Decides the next visibility from the current scroll state and applies it. */\n #evaluate(): void {\n const y = this.#scrollY();\n let nextVisible: boolean;\n if (this.modeValue === \"direction\") {\n // Near the very top, always reveal so a hide-on-scroll header is never\n // stranded off-screen when the page cannot scroll up any further.\n if (y <= this.offsetValue) {\n nextVisible = true;\n } else {\n nextVisible = y < this.#lastScrollY; // scrolling up reveals, down hides\n }\n } else {\n nextVisible = y > this.offsetValue;\n }\n this.#lastScrollY = y;\n this.#setVisible(nextVisible);\n }\n\n /** Applies visibility to the target, syncing `hidden`, `data-state`, `change`. */\n #setVisible(next: boolean): void {\n if (next === this.#visible) return;\n this.#visible = next;\n if (this.hasElementTarget) this.elementTarget.hidden = !next;\n this.element.setAttribute(\"data-state\", next ? \"visible\" : \"hidden\");\n this.dispatch(\"change\", { detail: { visible: next } });\n }\n\n /** Resolves the scroll source from `root` (falling back to the window). */\n #resolveScrollSource(): HTMLElement | Window {\n if (this.rootValue) {\n const root = document.querySelector<HTMLElement>(this.rootValue);\n if (root) return root;\n }\n return window;\n }\n\n #scrollY(): number {\n if (this.#scrollSource === window) {\n return window.scrollY ?? window.pageYOffset ?? 0;\n }\n return (this.#scrollSource as HTMLElement).scrollTop;\n }\n\n #prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/reduced_motion.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/scroll_visibility_controller.ts"],"names":[],"mappings":"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","file":"scroll_visibility_controller.js","sourcesContent":["/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers\n * (count-up, highlight, overflow-indicator, scroll-visibility, stick-to-bottom,\n * transition).\n *\n * This one-liner keeps the media query string and the environment guard\n * single-sourced across them. The preference is intentionally re-read on every\n * call — the controllers check it at each animation/scroll start (WCAG 2.2\n * **2.3.3**), so flipping the OS setting takes effect immediately without any\n * listener or cache bookkeeping here.\n */\n\n/**\n * Whether the user currently requests reduced motion.\n *\n * @returns `true` when `(prefers-reduced-motion: reduce)` matches; `false`\n * otherwise, including environments without `window.matchMedia` (treated as\n * \"no preference\").\n */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\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 { prefersReducedMotion } from \"../utils/reduced_motion\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/**\n * Headless **Scroll Visibility** behavior: shows or hides an element based on\n * scroll amount or direction (back-to-top buttons, hide-on-scroll headers). No\n * dedicated APG pattern; when the element is a button it follows the Button\n * practice.\n *\n * Markup contract (identifier: `stimeo--scroll-visibility`):\n * <div data-controller=\"stimeo--scroll-visibility\"\n * data-stimeo--scroll-visibility-offset-value=\"400\"\n * data-stimeo--scroll-visibility-mode-value=\"offset\">\n * <button type=\"button\" hidden\n * data-stimeo--scroll-visibility-target=\"element\"\n * data-action=\"stimeo--scroll-visibility#toTop\">Back to top</button>\n * </div>\n *\n * In `offset` mode the element is shown once the scroll source is scrolled past\n * `offset` px; in `direction` mode it is hidden while scrolling down and shown\n * while scrolling up. Visibility is reflected through the `hidden` attribute (so a\n * hidden control also leaves the focus order) and `data-state`.\n *\n * By default the **window** is the scroll source. When the page itself does not\n * scroll — e.g. a fixed-height app shell whose main column scrolls in a container\n * (`overflow: auto`) — point `root` at that container (a CSS selector) so the\n * controller observes the element's scroll instead of the (never-scrolling)\n * window. `toTop` then scrolls that same container.\n *\n * @remarks\n * Behavior only — the look and any transition are the consumer's CSS. The scroll\n * listener is `passive`, coalesced through `requestAnimationFrame`, and removed on\n * `disconnect()` (Turbo navigation included). `toTop` honors\n * `prefers-reduced-motion` by forcing an instant jump independently of the\n * consumer's CSS `scroll-behavior`, and can move focus to a `focusSelector`\n * target (given `tabindex=\"-1\"` if needed) to keep keyboard users oriented\n * after the scroll. A live disconnect removes only a `tabindex=\"-1\"` this\n * controller instance added; authored tabindex values remain. This teardown does\n * not claim to rewrite a Turbo cache snapshot that was cloned before disconnect.\n */\nexport class ScrollVisibilityController extends Controller<HTMLElement> {\n static override targets = [\"element\"];\n static override values = {\n offset: { type: Number, default: 400 },\n mode: { type: String, default: \"offset\" },\n focusSelector: { type: String, default: \"\" },\n root: { type: String, default: \"\" },\n };\n static actions = [\"toTop\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly elementTarget: HTMLElement;\n declare readonly hasElementTarget: boolean;\n\n declare offsetValue: number;\n declare modeValue: string;\n declare focusSelectorValue: string;\n declare rootValue: string;\n\n /** Pending rAF id that coalesces scroll bursts into one measurement. */\n #rafId: number | null = null;\n /** Previous scroll position, for `direction` mode delta detection. */\n #lastScrollY = 0;\n /** Current visibility, tracked to dispatch `change` only on real transitions. */\n #visible: boolean | null = null;\n /**\n * The observed scroll source: a container element when `root` resolves, else\n * the window. Captured on connect so teardown detaches from the same source.\n */\n #scrollSource: HTMLElement | Window = window;\n /** Focus targets this instance lent a `tabindex` to. */\n readonly #tabindex = new TabindexLoan();\n\n readonly #onScroll = (): void => {\n if (this.#rafId !== null) return;\n this.#rafId = requestAnimationFrame(() => {\n this.#rafId = null;\n this.#evaluate();\n });\n };\n\n override connect(): void {\n this.#scrollSource = this.#resolveScrollSource();\n this.#lastScrollY = this.#scrollY();\n this.#scrollSource.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n this.#evaluate();\n }\n\n override disconnect(): void {\n this.#scrollSource.removeEventListener(\"scroll\", this.#onScroll);\n if (this.#rafId !== null) {\n cancelAnimationFrame(this.#rafId);\n this.#rafId = null;\n }\n this.#tabindex.returnAll();\n this.#visible = null;\n }\n\n /** Scrolls the source to the top and, optionally, moves focus to a safe target. */\n toTop(): void {\n const behavior: ScrollBehavior = prefersReducedMotion() ? \"instant\" : \"smooth\";\n this.#scrollSource.scrollTo({ top: 0, behavior });\n if (this.focusSelectorValue) {\n const target = document.querySelector<HTMLElement>(this.focusSelectorValue);\n if (target) {\n this.#tabindex.lend(target);\n target.focus();\n }\n }\n }\n\n /** Decides the next visibility from the current scroll state and applies it. */\n #evaluate(): void {\n const y = this.#scrollY();\n let nextVisible: boolean;\n if (this.modeValue === \"direction\") {\n // Near the very top, always reveal so a hide-on-scroll header is never\n // stranded off-screen when the page cannot scroll up any further.\n if (y <= this.offsetValue) {\n nextVisible = true;\n } else {\n nextVisible = y < this.#lastScrollY; // scrolling up reveals, down hides\n }\n } else {\n nextVisible = y > this.offsetValue;\n }\n this.#lastScrollY = y;\n this.#setVisible(nextVisible);\n }\n\n /** Applies visibility to the target, syncing `hidden`, `data-state`, `change`. */\n #setVisible(next: boolean): void {\n if (next === this.#visible) return;\n this.#visible = next;\n if (this.hasElementTarget) this.elementTarget.hidden = !next;\n this.element.setAttribute(\"data-state\", next ? \"visible\" : \"hidden\");\n this.dispatch(\"change\", { detail: { visible: next } });\n }\n\n /** Resolves the scroll source from `root` (falling back to the window). */\n #resolveScrollSource(): HTMLElement | Window {\n if (this.rootValue) {\n const root = document.querySelector<HTMLElement>(this.rootValue);\n if (root) return root;\n }\n return window;\n }\n\n #scrollY(): number {\n if (this.#scrollSource === window) {\n return window.scrollY ?? window.pageYOffset ?? 0;\n }\n return (this.#scrollSource as HTMLElement).scrollTop;\n }\n}\n"]}
|
|
@@ -1,28 +1,59 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Headless, accessible Scrollspy
|
|
4
|
+
* Headless, accessible **Scrollspy**: keeps a table of contents in sync with the
|
|
5
|
+
* section the reader is currently in, published as `aria-current="location"`.
|
|
6
|
+
* There is no dedicated APG widget; it follows the `aria-current`
|
|
7
|
+
* current-location practice inside a `<nav>` landmark.
|
|
5
8
|
*
|
|
6
9
|
* Markup contract (identifier: `stimeo--scrollspy`):
|
|
7
10
|
* <nav data-controller="stimeo--scrollspy"
|
|
8
11
|
* data-stimeo--scrollspy-offset-value="80"
|
|
12
|
+
* data-stimeo--scrollspy-root-selector-value=".content"
|
|
9
13
|
* aria-label="Table of contents">
|
|
10
|
-
* <a href="#intro" data-stimeo--scrollspy-target="link"
|
|
11
|
-
*
|
|
14
|
+
* <a href="#intro" data-stimeo--scrollspy-target="link"
|
|
15
|
+
* data-action="click->stimeo--scrollspy#scrollTo">Intro</a>
|
|
16
|
+
* <a href="#usage" data-stimeo--scrollspy-target="link"
|
|
17
|
+
* data-action="click->stimeo--scrollspy#scrollTo">Usage</a>
|
|
12
18
|
* </nav>
|
|
13
|
-
* <
|
|
14
|
-
*
|
|
19
|
+
* <div class="content">
|
|
20
|
+
* <section id="intro">…</section>
|
|
21
|
+
* <section id="usage">…</section>
|
|
22
|
+
* </div>
|
|
15
23
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
24
|
+
* The active section is the one whose top edge sits closest to the **trigger
|
|
25
|
+
* line**: `offset` px below the top of the *scroll root* — the `rootSelector`
|
|
26
|
+
* container, or the viewport when that value is empty. It is not the viewport
|
|
27
|
+
* top whenever a nested container is spied on. Intersecting sections win; when
|
|
28
|
+
* none intersects (between two sections, scrolled past the last one) the
|
|
29
|
+
* closest tracked section keeps the highlight, so a table of contents never
|
|
30
|
+
* goes blank.
|
|
31
|
+
*
|
|
32
|
+
* `data-action` on the links is optional and opts into {@link scrollTo}, which
|
|
33
|
+
* scrolls the nested container instead of bouncing the whole window.
|
|
22
34
|
*
|
|
23
35
|
* @remarks
|
|
24
|
-
* Behavior only
|
|
25
|
-
*
|
|
36
|
+
* Behavior only — how a current link looks is the consumer's CSS
|
|
37
|
+
* (`[aria-current="location"] { … }`). `connect()` reads the current location
|
|
38
|
+
* back from the DOM so a Turbo cache restore re-establishes it without a
|
|
39
|
+
* redundant `change`, and `disconnect()` severs every resource acquired here:
|
|
40
|
+
* the observers, the scroll listener, and any pending frame or queued rebuild.
|
|
41
|
+
*
|
|
42
|
+
* **What is followed automatically**, and what is not (the boundary a consumer
|
|
43
|
+
* has to know, because everything outside it needs a re-`connect()`):
|
|
44
|
+
*
|
|
45
|
+
* - `link` targets added or removed — Stimulus's target callbacks.
|
|
46
|
+
* - a `link` target's `href` / `data-href` rewritten in place — a Turbo 8 morph
|
|
47
|
+
* keeps the element *and* its target marker, so no target callback fires;
|
|
48
|
+
* {@link ANCHOR_ATTRIBUTES} is watched for exactly this case.
|
|
49
|
+
* - the reader's scroll position, including inside a stretch where no section
|
|
50
|
+
* crosses an observation threshold.
|
|
51
|
+
*
|
|
52
|
+
* Not followed: a *split* lifecycle in which the nav survives while the scroll
|
|
53
|
+
* root or the sections are replaced underneath it. `rootSelector` is resolved
|
|
54
|
+
* against the whole document and re-resolved once the cached container leaves
|
|
55
|
+
* it, but section elements are looked up only while the observation set is
|
|
56
|
+
* (re)built — swap those alone and nothing tells this controller to look again.
|
|
26
57
|
*/
|
|
27
58
|
declare class ScrollspyController extends Controller<HTMLElement> {
|
|
28
59
|
#private;
|
|
@@ -40,6 +71,10 @@ declare class ScrollspyController extends Controller<HTMLElement> {
|
|
|
40
71
|
type: StringConstructor;
|
|
41
72
|
default: string;
|
|
42
73
|
};
|
|
74
|
+
focusSection: {
|
|
75
|
+
type: BooleanConstructor;
|
|
76
|
+
default: boolean;
|
|
77
|
+
};
|
|
43
78
|
};
|
|
44
79
|
static actions: readonly ["scrollTo"];
|
|
45
80
|
static events: readonly ["change"];
|
|
@@ -47,6 +82,7 @@ declare class ScrollspyController extends Controller<HTMLElement> {
|
|
|
47
82
|
offsetValue: number;
|
|
48
83
|
rootMarginValue: string;
|
|
49
84
|
rootSelectorValue: string;
|
|
85
|
+
focusSectionValue: boolean;
|
|
50
86
|
connect(): void;
|
|
51
87
|
disconnect(): void;
|
|
52
88
|
/**
|
|
@@ -56,8 +92,21 @@ declare class ScrollspyController extends Controller<HTMLElement> {
|
|
|
56
92
|
rootMarginValueChanged(): void;
|
|
57
93
|
rootSelectorValueChanged(): void;
|
|
58
94
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
95
|
+
* Re-syncs the observation set when a Turbo Stream/morph swaps the table of
|
|
96
|
+
* contents. Stimulus fires these before `connect()` for the links already in
|
|
97
|
+
* the markup, hence the guard: the initial observer is built exactly once, by
|
|
98
|
+
* `connect()`.
|
|
99
|
+
*/
|
|
100
|
+
linkTargetConnected(): void;
|
|
101
|
+
linkTargetDisconnected(): void;
|
|
102
|
+
/**
|
|
103
|
+
* Scrolls to the section the clicked link anchors, honoring `offset` and any
|
|
104
|
+
* nested scroll container (a plain fragment jump would scroll the window).
|
|
105
|
+
*
|
|
106
|
+
* Honors `prefers-reduced-motion` (WCAG 2.2 **2.3.3**) by forcing an instant
|
|
107
|
+
* jump independently of the consumer's CSS `scroll-behavior`. With
|
|
108
|
+
* `focusSection` enabled it also moves the sequential focus starting point
|
|
109
|
+
* into the destination; the URL fragment is deliberately not touched.
|
|
61
110
|
*/
|
|
62
111
|
scrollTo(event: Event): void;
|
|
63
112
|
}
|