stimeo-ui 0.5.0 → 0.6.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 +116 -0
- package/dist/controllers/aspect_ratio_controller.d.ts +1 -3
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +195 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +85 -9
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +31 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +7 -1
- package/dist/controllers/color_picker_controller.js +35 -9
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +157 -30
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +7 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -3
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +2 -2
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +3 -3
- package/dist/controllers/idle_controller.js +27 -5
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +5 -3
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +46 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +24 -6
- package/dist/controllers/number_input_controller.js +275 -51
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +4 -0
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js +33 -0
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +3 -3
- package/dist/controllers/password_strength_controller.js +20 -2
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +4 -4
- package/dist/controllers/persist_controller.js +24 -5
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +41 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +36 -31
- package/dist/controllers/rating_controller.js +272 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +4 -0
- package/dist/controllers/roving_controller.js +60 -5
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +6 -0
- package/dist/controllers/scroll_area_controller.js +154 -22
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +41 -11
- package/dist/controllers/tags_input_controller.js +356 -120
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +42 -13
- package/dist/controllers/time_picker_controller.js +296 -107
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +39 -13
- package/dist/controllers/toggle_group_controller.js +378 -55
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +5 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +2 -2
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +2 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.js +5 -3
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +2637 -781
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +16 -3
- package/dist/inspector/cli.js +22 -6
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +22 -6
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +8 -8
- package/dist/inspector/manifest.json +169 -37
- package/dist/positioning/index.d.ts +2 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
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
|
+
{"version":3,"sources":["../../src/utils/reduced_motion.ts","../../src/utils/before_cache_reset.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 * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\n\n/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * Every live loan also owns a shared `turbo:before-cache` subscription. The loan\n * is returned before Turbo can copy it into a snapshot, so consumers get cache\n * safety without duplicating a lifecycle hook; `returnAll()` removes the\n * subscription again as soon as no loan remains.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n /** Returns live loans before Turbo can copy them into its page snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.returnAll());\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n // Subscribe only while a real loan exists. Keeping this guarantee here means\n // every consumer is Turbo-safe without another lifecycle hook to remember.\n this.#beforeCache.activate();\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n this.#beforeCache.deactivate();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { 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"]}
|
|
@@ -5,19 +5,20 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
5
5
|
*
|
|
6
6
|
* Markup contract (identifier: `stimeo--tags-input`):
|
|
7
7
|
* <div data-controller="stimeo--tags-input"
|
|
8
|
-
* data-stimeo--tags-input-delimiter-value=","
|
|
8
|
+
* data-stimeo--tags-input-delimiter-value=","
|
|
9
|
+
* data-stimeo--tags-input-announce-text-value="{label} added; {count} total"
|
|
10
|
+
* data-stimeo--tags-input-announce-removed-text-value="{label} removed; {count} total">
|
|
9
11
|
* <ul role="list" aria-label="Tags" data-stimeo--tags-input-target="tags"></ul>
|
|
10
12
|
* <input type="text" aria-label="Add tag" aria-describedby="tags-help"
|
|
11
13
|
* data-stimeo--tags-input-target="input"
|
|
12
14
|
* data-action="keydown->stimeo--tags-input#onKeydown" />
|
|
13
|
-
* <span role="status" aria-live="polite" class="visually-hidden"
|
|
14
|
-
* data-stimeo--tags-input-target="status"></span>
|
|
15
15
|
* <div data-stimeo--tags-input-target="fields"></div>
|
|
16
16
|
* <template data-stimeo--tags-input-target="tagTemplate">
|
|
17
17
|
* <li role="listitem" data-stimeo--tags-input-target="tag">
|
|
18
|
-
* <span data-tags-input-
|
|
18
|
+
* <span data-stimeo--tags-input-target="label"></span>
|
|
19
19
|
* <!-- Removal is delegated on the tags container; no per-chip action needed. -->
|
|
20
|
-
* <button type="button" tabindex="-1"
|
|
20
|
+
* <button type="button" tabindex="-1" aria-label="Remove {label}"
|
|
21
|
+
* data-stimeo--tags-input-target="remove">×</button>
|
|
21
22
|
* </li>
|
|
22
23
|
* </template>
|
|
23
24
|
* </div>
|
|
@@ -31,13 +32,17 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
31
32
|
* - `Enter` or the configured delimiter commits the trimmed input as a tag.
|
|
32
33
|
* - Empty / duplicate / over-limit additions are rejected with
|
|
33
34
|
* `stimeo--tags-input:reject`; duplicates are allowed when `allowDuplicates`.
|
|
34
|
-
* - Tags render from the `tagTemplate
|
|
35
|
+
* - Tags render transactionally from the `tagTemplate`; its author-localized
|
|
36
|
+
* remove-button `aria-label` expands `{label}` / `{value}`. The
|
|
35
37
|
* `fields` container mirrors the tag set as `name`d hidden inputs for form
|
|
36
|
-
* submission
|
|
38
|
+
* submission. User changes dispatch `stimeo--tags-input:change`; DOM/Turbo tag
|
|
39
|
+
* changes dispatch `stimeo--tags-input:reconcile`; authored templates route
|
|
40
|
+
* localized addition/removal text to the shared announcer.
|
|
37
41
|
* - The remove buttons form one roving Tab stop: `ArrowLeft`/`ArrowRight` move
|
|
38
42
|
* between them (right past the end returns to the input), `Delete`/`Backspace`
|
|
39
43
|
* delete the focused tag, and `Backspace` on an empty input deletes the last.
|
|
40
|
-
* - Removing a
|
|
44
|
+
* - Removing from a chip moves focus to its neighbor (else the input); removing
|
|
45
|
+
* via empty-input `Backspace` keeps focus in the input for continued editing.
|
|
41
46
|
*/
|
|
42
47
|
declare class TagsInputController extends Controller<HTMLElement> {
|
|
43
48
|
#private;
|
|
@@ -59,23 +64,34 @@ declare class TagsInputController extends Controller<HTMLElement> {
|
|
|
59
64
|
type: StringConstructor;
|
|
60
65
|
default: string;
|
|
61
66
|
};
|
|
67
|
+
announceText: {
|
|
68
|
+
type: StringConstructor;
|
|
69
|
+
default: string;
|
|
70
|
+
};
|
|
71
|
+
announceRemovedText: {
|
|
72
|
+
type: StringConstructor;
|
|
73
|
+
default: string;
|
|
74
|
+
};
|
|
62
75
|
};
|
|
63
76
|
static actions: readonly ["onKeydown"];
|
|
64
|
-
static events: readonly ["change", "reject"];
|
|
77
|
+
static events: readonly ["change", "reconcile", "reject"];
|
|
65
78
|
readonly inputTarget: HTMLInputElement;
|
|
66
79
|
readonly tagsTarget: HTMLElement;
|
|
67
80
|
readonly tagTargets: HTMLElement[];
|
|
68
81
|
readonly tagTemplateTarget: HTMLTemplateElement;
|
|
69
|
-
readonly
|
|
82
|
+
readonly labelTargets: HTMLElement[];
|
|
83
|
+
readonly removeTargets: HTMLButtonElement[];
|
|
70
84
|
readonly fieldsTarget: HTMLElement;
|
|
71
|
-
readonly hasStatusTarget: boolean;
|
|
72
85
|
readonly hasFieldsTarget: boolean;
|
|
73
86
|
readonly hasTagTemplateTarget: boolean;
|
|
74
87
|
readonly hasInputTarget: boolean;
|
|
88
|
+
readonly hasTagsTarget: boolean;
|
|
75
89
|
delimiterValue: string;
|
|
76
90
|
maxValue: number;
|
|
77
91
|
allowDuplicatesValue: boolean;
|
|
78
92
|
nameValue: string;
|
|
93
|
+
announceTextValue: string;
|
|
94
|
+
announceRemovedTextValue: string;
|
|
79
95
|
/** Wires tag-list keyboard navigation and removal, and seeds the single Tab stop. */
|
|
80
96
|
connect(): void;
|
|
81
97
|
/** Releases the delegated listeners so no handler outlives the element. */
|
|
@@ -84,6 +100,20 @@ declare class TagsInputController extends Controller<HTMLElement> {
|
|
|
84
100
|
inputTargetConnected(input: HTMLInputElement): void;
|
|
85
101
|
/** Removes composition listeners when the active input is replaced or removed. */
|
|
86
102
|
inputTargetDisconnected(input: HTMLInputElement): void;
|
|
103
|
+
/** Rebinds delegation and derived state when the tags container is replaced. */
|
|
104
|
+
tagsTargetConnected(_tags: HTMLElement): void;
|
|
105
|
+
/** Releases only the row that disconnected, leaving a newer target intact. */
|
|
106
|
+
tagsTargetDisconnected(tags: HTMLElement): void;
|
|
107
|
+
/** Reconciles a tag inserted by Turbo, a server render, or another controller. */
|
|
108
|
+
tagTargetConnected(_tag: HTMLElement): void;
|
|
109
|
+
/** Reconciles a tag removed by Turbo, a server render, or another controller. */
|
|
110
|
+
tagTargetDisconnected(_tag: HTMLElement): void;
|
|
111
|
+
/** Seeds a fields target inserted after connect. */
|
|
112
|
+
fieldsTargetConnected(): void;
|
|
113
|
+
/** Rebuilds submitted fields when the public name changes at runtime. */
|
|
114
|
+
nameValueChanged(): void;
|
|
115
|
+
/** Recomputes the full hook when the cap changes at runtime. */
|
|
116
|
+
maxValueChanged(): void;
|
|
87
117
|
/** Commits on `Enter`/delimiter and deletes the last tag on empty `Backspace`. */
|
|
88
118
|
onKeydown(event: KeyboardEvent): void;
|
|
89
119
|
}
|