stimeo-ui 0.13.0 → 0.15.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 +73 -5
- package/dist/cable/index.d.ts +1 -1
- package/dist/cable/index.js +60 -14
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +1 -1
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.js +88 -61
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js +40 -5
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +29 -3
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +20 -4
- package/dist/controllers/calendar_controller.js +51 -10
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js +8 -6
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.js +27 -2
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +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.map +1 -1
- package/dist/controllers/conditional_fields_controller.js +36 -7
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +4 -2
- package/dist/controllers/currency_input_controller.js +77 -25
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +10 -3
- package/dist/controllers/date_range_picker_controller.js +43 -10
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +2 -2
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.js +21 -10
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js +32 -12
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +1 -1
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/flash_controller.js +145 -43
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +7 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js +29 -6
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +63 -23
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +3 -2
- package/dist/controllers/input_mask_controller.js +33 -12
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +10 -5
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +10 -5
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +3 -2
- package/dist/controllers/local_time_controller.js +43 -11
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +4 -4
- package/dist/controllers/masonry_controller.js +32 -14
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.js +10 -2
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +10 -2
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.js +8 -2
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +28 -4
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js +23 -10
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.js +31 -8
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +39 -4
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +1 -1
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +10 -3
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/portal_controller.js +21 -10
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.js +11 -3
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +1 -1
- package/dist/controllers/read_more_controller.js +31 -5
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.js +65 -19
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +40 -9
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_before_cache_controller.js +23 -10
- package/dist/controllers/reset_before_cache_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.js +7 -2
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +7 -3
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +70 -26
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +37 -16
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +1 -1
- package/dist/controllers/scroll_visibility_controller.js +71 -42
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +1 -1
- package/dist/controllers/scrollspy_controller.js +76 -32
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.js +72 -37
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.js +29 -3
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.js +10 -5
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.js +29 -6
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +1 -1
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +1 -1
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +29 -3
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +2 -3
- package/dist/controllers/theme_controller.js +23 -13
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +3 -0
- package/dist/controllers/toast_controller.js +136 -73
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js +11 -3
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +7 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/index.js +629 -597
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +38 -1
- package/dist/inspector/cli.js +80 -7
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +103 -7
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +5 -5
- package/dist/inspector/manifest.json +131 -3
- package/package.json +9 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/roving_tabindex.ts","../../src/utils/safe_storage.ts","../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper. A caller\n * that already captured one event-scoped snapshot may pass it to\n * {@link RovingTabindex.setActive}, avoiding a second live-target scan while\n * preserving one coherent collection for the whole operation.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item,\n * and `items` to reuse an event-scoped collection snapshot.\n */\n setActive(\n index: number,\n options: { focus?: boolean; items?: readonly HTMLElement[] } = {},\n ): void {\n const { focus = false } = options;\n const items = options.items ?? this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","/** A successful guarded storage operation. */\nexport interface StorageSuccess<T> {\n readonly ok: true;\n readonly value: T;\n}\n\n/** A guarded storage operation rejected by the browser. */\nexport interface StorageFailure {\n readonly ok: false;\n readonly error: unknown;\n}\n\n/** Result of accessing browser storage without allowing an exception to escape. */\nexport type StorageResult<T> = StorageSuccess<T> | StorageFailure;\n\n/**\n * Guarded `localStorage` access shared by controllers that persist client state.\n *\n * `window.localStorage` can throw while resolving the storage object or while\n * reading, writing, or removing a key. The result keeps an unavailable store\n * distinct from an available store whose key is unset, so callers can expose an\n * accurate success or failure state without duplicating exception guards.\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns A successful result containing the stored string or `null`, or a\n * failure result when storage is unavailable.\n */\nexport function readLocalStorage(key: string): StorageResult<string | null> {\n try {\n return { ok: true, value: window.localStorage.getItem(key) };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n * @returns Success only after `setItem` completes; otherwise the browser error.\n */\nexport function writeLocalStorage(key: string, value: string): StorageResult<void> {\n try {\n window.localStorage.setItem(key, value);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Removes `key` from `localStorage`.\n *\n * @param key - The storage key to remove.\n * @returns Success only after `removeItem` completes; otherwise the browser error.\n */\nexport function removeLocalStorage(key: string): StorageResult<void> {\n try {\n window.localStorage.removeItem(key);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { hasModifierChord, isReservedArrowChord, logicalArrowStep } from \"../utils/arrow_step\";\nimport { RovingTabindex, rovingMove } from \"../utils/roving_tabindex\";\nimport { readLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\n\n/** The three selectable modes; `system` follows the OS `prefers-color-scheme`. */\ntype ThemeMode = \"light\" | \"dark\" | \"system\";\n/** The two effective (resolved) themes applied to the root. */\ntype ResolvedTheme = \"light\" | \"dark\";\n\nconst MODES: readonly ThemeMode[] = [\"light\", \"dark\", \"system\"];\nconst isMode = (value: unknown): value is ThemeMode =>\n typeof value === \"string\" && (MODES as readonly string[]).includes(value);\n\n/** The mode a declaration falls back to when it cannot be read as one. */\nconst DEFAULT_MODE: ThemeMode = \"system\";\n/** The state-hook target a declaration falls back to when it cannot be parsed. */\nconst DEFAULT_TARGET = \"html\";\n\n/**\n * Headless **theme / color-scheme toggle** — persists a light/dark/system choice to\n * `localStorage`, follows the OS setting while in `system`, and reflects the\n * effective theme onto the root for the consumer's CSS. Ships no colors; only state\n * hooks.\n *\n * Two markup contracts (identifier: `stimeo--theme`):\n *\n * Canonical 3-value radiogroup (use this when `system` is offered):\n * <div data-controller=\"stimeo--theme\" data-stimeo--theme-mode-value=\"system\"\n * role=\"radiogroup\" aria-label=\"Theme\">\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"system\">System</button>\n * </div>\n *\n * Auxiliary 2-value toggle (light↔dark only — `system` is not representable):\n * <button data-controller=\"stimeo--theme\" data-action=\"click->stimeo--theme#toggle\"\n * aria-pressed=\"false\">Dark mode</button>\n *\n * `change` dispatches `{ mode, resolved }`, and only when one of the two moved.\n *\n * @remarks\n * Behavior only — the actual palette is the consumer's CSS keyed off `data-theme`\n * on the root. It applies `data-theme` (the *resolved* light/dark) and a matching\n * `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup\n * `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`\n * in sync. Arrow/Home/End move focus across the radiogroup as the APG radio pattern\n * requires; applying a theme never moves focus. The `prefers-color-scheme` listener is attached on\n * `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the\n * very first paint is an inline `<head>` snippet, not this controller.\n *\n * Every declaration is validated where it enters, and an unreadable one falls back\n * to that Value's default rather than taking the widget with it: a `mode` outside\n * the three modes reads as `system`, and a `target` that is not a parsable selector\n * reads as `html`. An option whose `data-value` is outside the three can be focused\n * but never becomes the selection, so no more than one option is ever checked —\n * and none at all when the resolved mode matches no option, where the first one\n * keeps the Tab stop.\n *\n * The radiogroup stays one Tab stop through `RovingTabindex`, re-derived\n * whenever an option enters or leaves, so a set rendered after connect or swapped\n * by a Turbo morph carries the same single stop as any other.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: DEFAULT_MODE },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: DEFAULT_TARGET },\n };\n static actions = [\"set\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasOptionTarget: boolean;\n\n declare modeValue: string;\n declare storageKeyValue: string;\n declare targetValue: string;\n\n /** The OS dark-mode query, watched so `system` tracks live changes. */\n #media: MediaQueryList | null = null;\n\n /** Gate for the target callbacks, which Stimulus runs before `connect()`. */\n #connected = false;\n\n /** The `target` declaration after validation; the default when unparsable. */\n #targetSelector = DEFAULT_TARGET;\n\n /** Owns the single Tab stop across the option set (APG radiogroup). */\n readonly #roving = new RovingTabindex(() => this.optionTargets);\n\n /**\n * The pair last reported, so a move can be told from a repeat. Neither side is\n * readable after the fact — assigning the Value updates the mode before any\n * comparison, and the OS query has already flipped by the time it notifies —\n * so what was reported has to be kept rather than recomputed.\n */\n #published: { mode: ThemeMode; resolved: ResolvedTheme } = {\n mode: DEFAULT_MODE,\n resolved: \"light\",\n };\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n // Only `system` follows the OS; an explicit mode already decided the answer.\n if (this.#mode !== \"system\") return;\n this.#commit();\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n let next = current;\n // Logical, not physical. The helper reverses only the horizontal pair, so\n // folding Down/Up into the same branch stays correct.\n const step = logicalArrowStep(event.key, this.element);\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = rovingMove(current, options.length, step, \"wrap\");\n break;\n case \"Home\":\n case \"End\":\n // Control+Home jumps the document, not the widget.\n if (hasModifierChord(event)) return;\n next = event.key === \"Home\" ? 0 : options.length - 1;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n const mode = this.#optionMode(option);\n // An option outside the three modes can hold focus, but the selection does\n // not follow it there.\n if (mode) this.#setMode(mode);\n };\n\n override connect(): void {\n const stored = this.#readStored();\n if (stored) this.modeValue = stored;\n\n this.#media = window.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n this.#media?.addEventListener(\"change\", this.#onMediaChange);\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#connected = true;\n this.#applyTheme();\n this.#syncControls();\n // Connecting is not a change: seed the baseline instead of announcing one.\n this.#published = this.#current;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Validates the `target` declaration once, so the render path never parses. */\n targetValueChanged(): void {\n const selector = this.targetValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#targetSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#targetSelector = DEFAULT_TARGET;\n }\n\n /** Re-derives the single Tab stop and ARIA for an option set that changed. */\n optionTargetConnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /** Re-derives them again when an option leaves, so a Tab stop always remains. */\n optionTargetDisconnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /**\n * Selects the mode the activated option declares.\n *\n * Read through {@link ThemeController.#optionMode}, the same lane that decides\n * which option is checked, so the two can never disagree about what an option\n * declares.\n */\n set(event: Event): void {\n const option = event.currentTarget;\n if (!(option instanceof HTMLElement)) return;\n const mode = this.#optionMode(option);\n if (mode) this.#setMode(mode);\n }\n\n /** Toggles light↔dark for the 2-value single-button contract. */\n toggle(): void {\n this.#setMode(this.#resolved() === \"dark\" ? \"light\" : \"dark\");\n }\n\n /** Central mode change: persist, apply to the root, resync controls, announce. */\n #setMode(mode: ThemeMode): void {\n this.modeValue = mode;\n this.#writeStored(mode);\n this.#commit();\n }\n\n /**\n * Applies the current mode and reports it, but reports only a real move: the\n * event means \"the selection or the effective theme moved\", so re-choosing the\n * option already chosen is not one.\n */\n #commit(): void {\n this.#applyTheme();\n this.#syncControls();\n const next = this.#current;\n const last = this.#published;\n this.#published = next;\n if (last.mode !== next.mode || last.resolved !== next.resolved) {\n // A copy: the baseline has to survive a listener that writes to what it\n // was handed, or the next unchanged operation reads as a change.\n this.dispatch(\"change\", { detail: { ...next } });\n }\n }\n\n /** The pair the `change` detail carries, read from current state. */\n get #current(): { mode: ThemeMode; resolved: ResolvedTheme } {\n return { mode: this.#mode, resolved: this.#resolved() };\n }\n\n /** Writes `data-theme` + `color-scheme` (the resolved theme) onto the target. */\n #applyTheme(): void {\n const root = this.#targetElement();\n if (!root) return;\n const resolved = this.#resolved();\n root.setAttribute(\"data-theme\", resolved);\n root.style.setProperty(\"color-scheme\", resolved);\n }\n\n /** Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync. */\n #syncControls(): void {\n const options = this.optionTargets;\n if (options.length > 0) {\n const mode = this.#mode;\n let selected = -1;\n options.forEach((option, index) => {\n const isSelected = this.#optionMode(option) === mode;\n option.setAttribute(\"aria-checked\", String(isSelected));\n if (isSelected && selected === -1) selected = index;\n });\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n this.#roving.setActive(selected === -1 ? 0 : selected, { items: options });\n return;\n }\n // `aria-pressed` belongs to the 2-value contract, where the controller sits on\n // the button itself. A radiogroup whose options have not rendered yet is not\n // that, and must not be told it is a toggle.\n if (this.#isToggleButton) {\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n }\n\n /** Whether the controller element is the button of the 2-value contract. */\n get #isToggleButton(): boolean {\n return this.element.tagName === \"BUTTON\" || this.element.getAttribute(\"role\") === \"button\";\n }\n\n /** The selected mode after validation; an unreadable declaration is the default. */\n get #mode(): ThemeMode {\n return isMode(this.modeValue) ? this.modeValue : DEFAULT_MODE;\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n const mode = this.#mode;\n if (mode === \"dark\") return \"dark\";\n if (mode === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** An option's mode from its `data-value`, or `null` when that is not one of the three. */\n #optionMode(option: HTMLElement): ThemeMode | null {\n const mode = option.getAttribute(\"data-value\");\n return isMode(mode) ? mode : null;\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n const selector = this.#targetSelector;\n if (selector === DEFAULT_TARGET || selector === \":root\") return document.documentElement;\n return document.querySelector<HTMLElement>(selector);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const result = readLocalStorage(this.storageKeyValue);\n return result.ok && isMode(result.value) ? result.value : null;\n }\n\n /** Persists the mode, swallowing storage errors (private mode / quota). */\n #writeStored(mode: ThemeMode): void {\n writeLocalStorage(this.storageKeyValue, mode);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/declared_value.ts","../../src/utils/roving_tabindex.ts","../../src/utils/safe_storage.ts","../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Readers for the String Values whose text can only be checked by handing it to\n * a parser: a CSS selector, a regular-expression source, a JSON object.\n *\n * Stimulus reads each of them as an ordinary string, so the controller connects\n * and the flaw surfaces later, inside the first handler that consumes the value\n * (a `SyntaxError` from `new RegExp`, a `DOMException` from the selector engine),\n * where it takes the whole handler down on every event. Parsing here, once, in\n * the `<name>ValueChanged` callback keeps the failure local to the declaration:\n * an unreadable value falls back to its default, the element stays alive, and\n * the hot path only ever receives a value that parsed.\n *\n * Which default a broken declaration falls back to is the caller's contract, so\n * every reader takes (or returns) the fallback rather than choosing one.\n */\n\n/**\n * The result of `parse(raw)`, or `fallback` when `parse` throws.\n *\n * Any exception counts as \"does not parse\": the parsers these Values feed\n * (`RegExp`, `JSON.parse`, the selector engine) all report a malformed input by\n * throwing, and none of them throws for another reason.\n */\nexport function parseDeclared<T>(raw: string, parse: (raw: string) => T, fallback: T): T {\n try {\n return parse(raw);\n } catch {\n return fallback;\n }\n}\n\n/** What {@link validSelector} needs of the element it probes with. */\nexport interface SelectorProbe {\n matches(selector: string): boolean;\n}\n\n/**\n * `raw` when it is a non-empty selector the DOM accepts, otherwise `fallback`.\n *\n * `element.matches` is the probe: the engine parses the selector and throws for\n * one it cannot read. Whether the selector actually matches `element` plays no\n * part, so a selector aimed at another element still passes.\n *\n * The parameter names the one method the probe uses rather than `Element`, so\n * this module carries no DOM type and the readers below stay importable from\n * the Node-side Inspector, which checks the same declarations statically.\n */\nexport function validSelector(element: SelectorProbe, raw: string, fallback: string): string {\n if (raw.length === 0) return fallback;\n return parseDeclared(\n raw,\n (selector) => {\n element.matches(selector);\n return selector;\n },\n fallback,\n );\n}\n\n/** How `compileRegExp` wraps a source before compiling it. */\nexport type RegExpAnchor = \"none\" | \"exact\";\n\n/**\n * `source` compiled as a `RegExp`, or `null` when it does not compile.\n *\n * `\"exact\"` wraps the source as `^(?:source)$`, so the whole input has to match\n * and an alternation inside the source cannot escape the anchors. The source is\n * compiled on its own first, because an unbalanced source can be made to parse\n * by the wrapper's own parentheses — `0)|(1` becomes `^(?:0)|(1)$` — which would\n * accept a declaration that is not a regular expression and leave half of it\n * outside the anchors. The default that replaces a broken source is the\n * caller's, so `null` is returned rather than a fallback pattern.\n */\nexport function compileRegExp(source: string, anchor: RegExpAnchor = \"none\"): RegExp | null {\n return parseDeclared(\n source,\n (text) => {\n const bare = new RegExp(text);\n return anchor === \"exact\" ? new RegExp(`^(?:${text})$`) : bare;\n },\n null,\n );\n}\n\n/**\n * The JSON object `raw` declares, or `null` when the text does not parse or\n * parses to something other than a plain object (`null`, an array, a scalar).\n * Values are returned as parsed; narrowing them is the caller's contract.\n */\nexport function parseJsonObject(raw: string): Record<string, unknown> | null {\n const parsed = parseDeclared<unknown>(raw, (text) => JSON.parse(text), null);\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n return parsed as Record<string, unknown>;\n}\n","/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper. A caller\n * that already captured one event-scoped snapshot may pass it to\n * {@link RovingTabindex.setActive}, avoiding a second live-target scan while\n * preserving one coherent collection for the whole operation.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item,\n * and `items` to reuse an event-scoped collection snapshot.\n */\n setActive(\n index: number,\n options: { focus?: boolean; items?: readonly HTMLElement[] } = {},\n ): void {\n const { focus = false } = options;\n const items = options.items ?? this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","/** A successful guarded storage operation. */\nexport interface StorageSuccess<T> {\n readonly ok: true;\n readonly value: T;\n}\n\n/** A guarded storage operation rejected by the browser. */\nexport interface StorageFailure {\n readonly ok: false;\n readonly error: unknown;\n}\n\n/** Result of accessing browser storage without allowing an exception to escape. */\nexport type StorageResult<T> = StorageSuccess<T> | StorageFailure;\n\n/**\n * Guarded `localStorage` access shared by controllers that persist client state.\n *\n * `window.localStorage` can throw while resolving the storage object or while\n * reading, writing, or removing a key. The result keeps an unavailable store\n * distinct from an available store whose key is unset, so callers can expose an\n * accurate success or failure state without duplicating exception guards.\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns A successful result containing the stored string or `null`, or a\n * failure result when storage is unavailable.\n */\nexport function readLocalStorage(key: string): StorageResult<string | null> {\n try {\n return { ok: true, value: window.localStorage.getItem(key) };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n * @returns Success only after `setItem` completes; otherwise the browser error.\n */\nexport function writeLocalStorage(key: string, value: string): StorageResult<void> {\n try {\n window.localStorage.setItem(key, value);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Removes `key` from `localStorage`.\n *\n * @param key - The storage key to remove.\n * @returns Success only after `removeItem` completes; otherwise the browser error.\n */\nexport function removeLocalStorage(key: string): StorageResult<void> {\n try {\n window.localStorage.removeItem(key);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { hasModifierChord, isReservedArrowChord, logicalArrowStep } from \"../utils/arrow_step\";\nimport { validSelector } from \"../utils/declared_value\";\nimport { RovingTabindex, rovingMove } from \"../utils/roving_tabindex\";\nimport { readLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\n\n/** The three selectable modes; `system` follows the OS `prefers-color-scheme`. */\ntype ThemeMode = \"light\" | \"dark\" | \"system\";\n/** The two effective (resolved) themes applied to the root. */\ntype ResolvedTheme = \"light\" | \"dark\";\n\nconst MODES: readonly ThemeMode[] = [\"light\", \"dark\", \"system\"];\nconst isMode = (value: unknown): value is ThemeMode =>\n typeof value === \"string\" && (MODES as readonly string[]).includes(value);\n\n/** The mode a declaration falls back to when it cannot be read as one. */\nconst DEFAULT_MODE: ThemeMode = \"system\";\n/** The state-hook target a declaration falls back to when it cannot be parsed. */\nconst DEFAULT_TARGET = \"html\";\n\n/**\n * Headless **theme / color-scheme toggle** — persists a light/dark/system choice to\n * `localStorage`, follows the OS setting while in `system`, and reflects the\n * effective theme onto the root for the consumer's CSS. Ships no colors; only state\n * hooks.\n *\n * Two markup contracts (identifier: `stimeo--theme`):\n *\n * Canonical 3-value radiogroup (use this when `system` is offered):\n * <div data-controller=\"stimeo--theme\" data-stimeo--theme-mode-value=\"system\"\n * role=\"radiogroup\" aria-label=\"Theme\">\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"system\">System</button>\n * </div>\n *\n * Auxiliary 2-value toggle (light↔dark only — `system` is not representable):\n * <button data-controller=\"stimeo--theme\" data-action=\"click->stimeo--theme#toggle\"\n * aria-pressed=\"false\">Dark mode</button>\n *\n * `change` dispatches `{ mode, resolved }`, and only when one of the two moved.\n *\n * @remarks\n * Behavior only — the actual palette is the consumer's CSS keyed off `data-theme`\n * on the root. It applies `data-theme` (the *resolved* light/dark) and a matching\n * `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup\n * `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`\n * in sync. Arrow/Home/End move focus across the radiogroup as the APG radio pattern\n * requires; applying a theme never moves focus. The `prefers-color-scheme` listener is attached on\n * `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the\n * very first paint is an inline `<head>` snippet, not this controller.\n *\n * Every declaration is validated where it enters, and an unreadable one falls back\n * to that Value's default rather than taking the widget with it: a `mode` outside\n * the three modes reads as `system`, and a `target` that is not a parsable selector\n * reads as `html`. An option whose `data-value` is outside the three can be focused\n * but never becomes the selection, so no more than one option is ever checked —\n * and none at all when the resolved mode matches no option, where the first one\n * keeps the Tab stop.\n *\n * The radiogroup stays one Tab stop through `RovingTabindex`, re-derived\n * whenever an option enters or leaves, so a set rendered after connect or swapped\n * by a Turbo morph carries the same single stop as any other.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: DEFAULT_MODE },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: DEFAULT_TARGET },\n };\n static actions = [\"set\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasOptionTarget: boolean;\n\n declare modeValue: string;\n declare storageKeyValue: string;\n declare targetValue: string;\n\n /** The OS dark-mode query, watched so `system` tracks live changes. */\n #media: MediaQueryList | null = null;\n\n /** Gate for the target callbacks, which Stimulus runs before `connect()`. */\n #connected = false;\n\n /** The `target` declaration after validation; the default when unparsable. */\n #targetSelector = DEFAULT_TARGET;\n\n /** Owns the single Tab stop across the option set (APG radiogroup). */\n readonly #roving = new RovingTabindex(() => this.optionTargets);\n\n /**\n * The pair last reported, so a move can be told from a repeat. Neither side is\n * readable after the fact — assigning the Value updates the mode before any\n * comparison, and the OS query has already flipped by the time it notifies —\n * so what was reported has to be kept rather than recomputed.\n */\n #published: { mode: ThemeMode; resolved: ResolvedTheme } = {\n mode: DEFAULT_MODE,\n resolved: \"light\",\n };\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n // Only `system` follows the OS; an explicit mode already decided the answer.\n if (this.#mode !== \"system\") return;\n this.#commit();\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n let next = current;\n // Logical, not physical. The helper reverses only the horizontal pair, so\n // folding Down/Up into the same branch stays correct.\n const step = logicalArrowStep(event.key, this.element);\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = rovingMove(current, options.length, step, \"wrap\");\n break;\n case \"Home\":\n case \"End\":\n // Control+Home jumps the document, not the widget.\n if (hasModifierChord(event)) return;\n next = event.key === \"Home\" ? 0 : options.length - 1;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n const mode = this.#optionMode(option);\n // An option outside the three modes can hold focus, but the selection does\n // not follow it there.\n if (mode) this.#setMode(mode);\n };\n\n override connect(): void {\n const stored = this.#readStored();\n if (stored) this.modeValue = stored;\n\n this.#media = window.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n this.#media?.addEventListener(\"change\", this.#onMediaChange);\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#connected = true;\n this.#applyTheme();\n this.#syncControls();\n // Connecting is not a change: seed the baseline instead of announcing one.\n this.#published = this.#current;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Validates the `target` declaration once, so the render path never parses. */\n targetValueChanged(): void {\n this.#targetSelector = validSelector(this.element, this.targetValue, DEFAULT_TARGET);\n }\n\n /** Re-derives the single Tab stop and ARIA for an option set that changed. */\n optionTargetConnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /** Re-derives them again when an option leaves, so a Tab stop always remains. */\n optionTargetDisconnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /**\n * Selects the mode the activated option declares.\n *\n * Read through the same lane that decides which option is checked, so the two\n * can never disagree about what an option declares.\n */\n set(event: Event): void {\n const option = event.currentTarget;\n if (!(option instanceof HTMLElement)) return;\n const mode = this.#optionMode(option);\n if (mode) this.#setMode(mode);\n }\n\n /** Toggles light↔dark for the 2-value single-button contract. */\n toggle(): void {\n this.#setMode(this.#resolved() === \"dark\" ? \"light\" : \"dark\");\n }\n\n /** Central mode change: persist, apply to the root, resync controls, announce. */\n #setMode(mode: ThemeMode): void {\n this.modeValue = mode;\n this.#writeStored(mode);\n this.#commit();\n }\n\n /**\n * Applies the current mode and reports it, but reports only a real move: the\n * event means \"the selection or the effective theme moved\", so re-choosing the\n * option already chosen is not one.\n */\n #commit(): void {\n this.#applyTheme();\n this.#syncControls();\n const next = this.#current;\n const last = this.#published;\n this.#published = next;\n if (last.mode !== next.mode || last.resolved !== next.resolved) {\n // A copy: the baseline has to survive a listener that writes to what it\n // was handed, or the next unchanged operation reads as a change.\n this.dispatch(\"change\", { detail: { ...next } });\n }\n }\n\n /** The pair the `change` detail carries, read from current state. */\n get #current(): { mode: ThemeMode; resolved: ResolvedTheme } {\n return { mode: this.#mode, resolved: this.#resolved() };\n }\n\n /** Writes `data-theme` + `color-scheme` (the resolved theme) onto the target. */\n #applyTheme(): void {\n const root = this.#targetElement();\n if (!root) return;\n const resolved = this.#resolved();\n root.setAttribute(\"data-theme\", resolved);\n root.style.setProperty(\"color-scheme\", resolved);\n }\n\n /** Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync. */\n #syncControls(): void {\n const options = this.optionTargets;\n if (options.length > 0) {\n const mode = this.#mode;\n let selected = -1;\n options.forEach((option, index) => {\n const isSelected = this.#optionMode(option) === mode;\n option.setAttribute(\"aria-checked\", String(isSelected));\n if (isSelected && selected === -1) selected = index;\n });\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n this.#roving.setActive(selected === -1 ? 0 : selected, { items: options });\n return;\n }\n // `aria-pressed` belongs to the 2-value contract, where the controller sits on\n // the button itself. A radiogroup whose options have not rendered yet is not\n // that, and must not be told it is a toggle.\n if (this.#isToggleButton) {\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n }\n\n /** Whether the controller element is the button of the 2-value contract. */\n get #isToggleButton(): boolean {\n return this.element.tagName === \"BUTTON\" || this.element.getAttribute(\"role\") === \"button\";\n }\n\n /** The selected mode after validation; an unreadable declaration is the default. */\n get #mode(): ThemeMode {\n return isMode(this.modeValue) ? this.modeValue : DEFAULT_MODE;\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n const mode = this.#mode;\n if (mode === \"dark\") return \"dark\";\n if (mode === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** An option's mode from its `data-value`, or `null` when that is not one of the three. */\n #optionMode(option: HTMLElement): ThemeMode | null {\n const mode = option.getAttribute(\"data-value\");\n return isMode(mode) ? mode : null;\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n const selector = this.#targetSelector;\n if (selector === DEFAULT_TARGET || selector === \":root\") return document.documentElement;\n return document.querySelector<HTMLElement>(selector);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const result = readLocalStorage(this.storageKeyValue);\n return result.ok && isMode(result.value) ? result.value : null;\n }\n\n /** Persists the mode, swallowing storage errors (private mode / quota). */\n #writeStored(mode: ThemeMode): void {\n writeLocalStorage(this.storageKeyValue, mode);\n }\n}\n"]}
|
|
@@ -26,6 +26,9 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
26
26
|
*
|
|
27
27
|
* Implements WAI-ARIA live region status/alert announcements, limits simultaneous
|
|
28
28
|
* elements, and pauses dismiss timeouts on hover or focus to comply with WCAG 2.2.1.
|
|
29
|
+
* Pausing never dismisses: a deadline that lapsed while the timer sat queued settles
|
|
30
|
+
* once the last of hover and focus is released, so the pointer's target and the
|
|
31
|
+
* focused control stay where they are.
|
|
29
32
|
*
|
|
30
33
|
* `dismiss` dispatches `{ item: HTMLElement, reason: "timeout" | "user" }`.
|
|
31
34
|
*
|
|
@@ -55,6 +55,121 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
// src/utils/keyed_timers.ts
|
|
59
|
+
var KeyedTimers = class {
|
|
60
|
+
#timers = new SafeTimeout();
|
|
61
|
+
/** The pending timer of each key; an entry lives exactly as long as its timer. */
|
|
62
|
+
#pending = /* @__PURE__ */ new Map();
|
|
63
|
+
/**
|
|
64
|
+
* Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
|
|
65
|
+
* pending. The entry is dropped before the callback runs, so the callback sees
|
|
66
|
+
* the key unarmed and may arm it again for the next round.
|
|
67
|
+
*/
|
|
68
|
+
set(key, callback, delay) {
|
|
69
|
+
this.clear(key);
|
|
70
|
+
const id = this.#timers.set(() => {
|
|
71
|
+
this.#pending.delete(key);
|
|
72
|
+
callback();
|
|
73
|
+
}, delay);
|
|
74
|
+
this.#pending.set(key, id);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Cancels `key`'s pending timer, if it has one.
|
|
78
|
+
*
|
|
79
|
+
* A timer id is a positive integer, so `-1` stands for "nothing pending" and
|
|
80
|
+
* the registry ignores an id it does not own — the unarmed case needs no
|
|
81
|
+
* branch of its own, and no other key's timer can be reached from here.
|
|
82
|
+
*/
|
|
83
|
+
clear(key) {
|
|
84
|
+
this.#timers.clear(this.#pending.get(key) ?? -1);
|
|
85
|
+
this.#pending.delete(key);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Cancels every pending timer and forgets every key. Call this from a
|
|
89
|
+
* controller's `disconnect()` so no timer, and no entry, outlives the element.
|
|
90
|
+
*/
|
|
91
|
+
clearAll() {
|
|
92
|
+
this.#timers.clearAll();
|
|
93
|
+
this.#pending.clear();
|
|
94
|
+
}
|
|
95
|
+
/** Whether `key` has a timer pending. */
|
|
96
|
+
has(key) {
|
|
97
|
+
return this.#pending.has(key);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// src/utils/pausable_timers.ts
|
|
102
|
+
var PausableTimers = class {
|
|
103
|
+
#timers = new KeyedTimers();
|
|
104
|
+
/** Every key armed or held; an entry goes as its timer fires, or with the key. */
|
|
105
|
+
#entries = /* @__PURE__ */ new Map();
|
|
106
|
+
/**
|
|
107
|
+
* Arms `callback` after `delay` ms for `key`, replacing whatever `key` had.
|
|
108
|
+
* A held key stays held and banks `delay` for its resume.
|
|
109
|
+
*/
|
|
110
|
+
set(key, callback, delay) {
|
|
111
|
+
const reasons = this.#entries.get(key)?.reasons ?? /* @__PURE__ */ new Set();
|
|
112
|
+
const entry = { callback, startedAt: 0, remaining: delay, reasons };
|
|
113
|
+
this.#entries.set(key, entry);
|
|
114
|
+
if (reasons.size === 0) this.#arm(key, entry);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Holds `key` for `reason` and banks the time left, with a floor of one
|
|
118
|
+
* millisecond. Reports whether this call is the one that stopped a running
|
|
119
|
+
* timer, so the caller can write its held-state hook exactly once.
|
|
120
|
+
*/
|
|
121
|
+
pause(key, reason) {
|
|
122
|
+
const entry = this.#entries.get(key);
|
|
123
|
+
if (entry === void 0) return false;
|
|
124
|
+
entry.reasons.add(reason);
|
|
125
|
+
if (!this.#timers.has(key)) return false;
|
|
126
|
+
this.#timers.clear(key);
|
|
127
|
+
entry.remaining = Math.max(1, entry.remaining - (Date.now() - entry.startedAt));
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Releases `reason` on `key`, arming the banked time again once no reason is
|
|
132
|
+
* left. Reports whether this call is the one that started the timer again.
|
|
133
|
+
*/
|
|
134
|
+
resume(key, reason) {
|
|
135
|
+
const entry = this.#entries.get(key);
|
|
136
|
+
if (entry === void 0) return false;
|
|
137
|
+
entry.reasons.delete(reason);
|
|
138
|
+
if (entry.reasons.size > 0 || this.#timers.has(key)) return false;
|
|
139
|
+
this.#arm(key, entry);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
/** Whether `key` is armed or held — that is, whether this registry drives it at all. */
|
|
143
|
+
tracks(key) {
|
|
144
|
+
return this.#entries.has(key);
|
|
145
|
+
}
|
|
146
|
+
/** Cancels `key`'s timer and drops its hold. */
|
|
147
|
+
clear(key) {
|
|
148
|
+
this.#timers.clear(key);
|
|
149
|
+
this.#entries.delete(key);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Cancels every timer and forgets every key. Call this from a controller's
|
|
153
|
+
* `disconnect()` so neither a timer nor a hold outlives the element.
|
|
154
|
+
*/
|
|
155
|
+
clearAll() {
|
|
156
|
+
this.#timers.clearAll();
|
|
157
|
+
this.#entries.clear();
|
|
158
|
+
}
|
|
159
|
+
/** Starts `entry`'s banked time running for `key`, and drops it as it fires. */
|
|
160
|
+
#arm(key, entry) {
|
|
161
|
+
entry.startedAt = Date.now();
|
|
162
|
+
this.#timers.set(
|
|
163
|
+
key,
|
|
164
|
+
() => {
|
|
165
|
+
this.#entries.delete(key);
|
|
166
|
+
entry.callback();
|
|
167
|
+
},
|
|
168
|
+
entry.remaining
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
58
173
|
// src/utils/transition_completion.ts
|
|
59
174
|
function timeMs(value) {
|
|
60
175
|
const trimmed = value.trim();
|
|
@@ -99,30 +214,23 @@ var ToastController = class extends Controller {
|
|
|
99
214
|
};
|
|
100
215
|
static actions = ["dismiss", "onKeydown", "pause", "resume", "show"];
|
|
101
216
|
static events = ["dismiss", "show"];
|
|
102
|
-
/**
|
|
103
|
-
* Registry for every auto-dismiss and transition-finalize timer the controller
|
|
104
|
-
* schedules. `SafeTimeout` owns *registration and teardown only*; the
|
|
105
|
-
* pause/resume remaining-time accounting stays in `#activeTimeouts` so the
|
|
106
|
-
* per-widget WCAG 2.2.1 semantics are not flattened into the helper.
|
|
107
|
-
*/
|
|
217
|
+
/** Removal timers for the leaving transition; the auto-dismiss ones live below. */
|
|
108
218
|
#timers = new SafeTimeout();
|
|
219
|
+
/** Per-toast auto-dismiss, held open while the toast is hovered or focused. */
|
|
220
|
+
#dismiss = new PausableTimers();
|
|
109
221
|
/**
|
|
110
222
|
* Pending one-shot `requestAnimationFrame` handles (the entering→visible flip).
|
|
111
223
|
* Tracked so {@link disconnect} can cancel any that have not fired, preventing a
|
|
112
224
|
* detached element from being mutated after it leaves the DOM (Turbo).
|
|
113
225
|
*/
|
|
114
226
|
#rafHandles = /* @__PURE__ */ new Map();
|
|
115
|
-
/** Track active timeouts mapped by each toast element for safe cancellation. */
|
|
116
|
-
#activeTimeouts = /* @__PURE__ */ new Map();
|
|
117
|
-
/** Track active pause reasons (hover/focus) per toast for WCAG 2.2.1 pause/resume. */
|
|
118
|
-
#pauseReasons = /* @__PURE__ */ new Map();
|
|
119
227
|
/** The stable list that owns delegated listeners for dynamically added items. */
|
|
120
228
|
#delegatedList = null;
|
|
121
229
|
connect() {
|
|
122
230
|
this.#connectDelegatedEvents();
|
|
123
231
|
this.enforceMaxLimit();
|
|
124
232
|
for (const item of this.itemTargets) {
|
|
125
|
-
if (!this.#
|
|
233
|
+
if (!this.#dismiss.tracks(item) && item.dataset.state !== "leaving") {
|
|
126
234
|
this.#startTimer(item);
|
|
127
235
|
}
|
|
128
236
|
}
|
|
@@ -130,12 +238,11 @@ var ToastController = class extends Controller {
|
|
|
130
238
|
disconnect() {
|
|
131
239
|
this.#disconnectDelegatedEvents();
|
|
132
240
|
this.#timers.clearAll();
|
|
241
|
+
this.#dismiss.clearAll();
|
|
133
242
|
for (const handle of this.#rafHandles.values()) {
|
|
134
243
|
window.cancelAnimationFrame(handle);
|
|
135
244
|
}
|
|
136
245
|
this.#rafHandles.clear();
|
|
137
|
-
this.#activeTimeouts.clear();
|
|
138
|
-
this.#pauseReasons.clear();
|
|
139
246
|
}
|
|
140
247
|
/** Rebinds delegated interaction when Turbo replaces the list target in place. */
|
|
141
248
|
listTargetConnected(element) {
|
|
@@ -148,18 +255,9 @@ var ToastController = class extends Controller {
|
|
|
148
255
|
durationValueChanged() {
|
|
149
256
|
for (const item of this.itemTargets) {
|
|
150
257
|
if (item.dataset.state === "leaving") continue;
|
|
151
|
-
const pauseReasons = this.#pauseReasons.get(item);
|
|
152
|
-
this.#clearTimer(item);
|
|
153
258
|
if (this.durationValue <= 0) {
|
|
259
|
+
this.#dismiss.clear(item);
|
|
154
260
|
item.removeAttribute("data-paused");
|
|
155
|
-
} else if (pauseReasons && pauseReasons.size > 0) {
|
|
156
|
-
this.#pauseReasons.set(item, pauseReasons);
|
|
157
|
-
this.#activeTimeouts.set(item, {
|
|
158
|
-
id: 0,
|
|
159
|
-
startedAt: 0,
|
|
160
|
-
remaining: this.durationValue
|
|
161
|
-
});
|
|
162
|
-
item.setAttribute("data-paused", "true");
|
|
163
261
|
} else {
|
|
164
262
|
this.#startTimer(item);
|
|
165
263
|
}
|
|
@@ -188,7 +286,7 @@ var ToastController = class extends Controller {
|
|
|
188
286
|
}
|
|
189
287
|
/** Clears any active timer when a toast is removed from the DOM. */
|
|
190
288
|
itemTargetDisconnected(element) {
|
|
191
|
-
this.#
|
|
289
|
+
this.#dismiss.clear(element);
|
|
192
290
|
this.#cancelAnimation(element);
|
|
193
291
|
}
|
|
194
292
|
/**
|
|
@@ -260,35 +358,16 @@ var ToastController = class extends Controller {
|
|
|
260
358
|
*/
|
|
261
359
|
pause(event) {
|
|
262
360
|
const item = this.#itemFromEvent(event);
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
if (!timeout) return;
|
|
266
|
-
const reasons = this.#pauseReasonsFor(item);
|
|
267
|
-
const wasActive = reasons.size > 0;
|
|
268
|
-
reasons.add(this.#pauseReason(event));
|
|
269
|
-
if (wasActive || timeout.id === 0) return;
|
|
270
|
-
this.#timers.clear(timeout.id);
|
|
271
|
-
const elapsed = Date.now() - timeout.startedAt;
|
|
272
|
-
const remaining = Math.max(0, timeout.remaining - elapsed);
|
|
273
|
-
if (remaining <= 0) {
|
|
274
|
-
this.#removeWithTransition(item, "timeout");
|
|
275
|
-
return;
|
|
361
|
+
if (item && this.#dismiss.pause(item, this.#pauseReason(event))) {
|
|
362
|
+
item.setAttribute("data-paused", "true");
|
|
276
363
|
}
|
|
277
|
-
this.#activeTimeouts.set(item, { id: 0, startedAt: 0, remaining });
|
|
278
|
-
item.setAttribute("data-paused", "true");
|
|
279
364
|
}
|
|
280
365
|
/** Resumes the auto-dismiss timer once both hover and focus have been released. */
|
|
281
366
|
resume(event) {
|
|
282
367
|
const item = this.#itemFromEvent(event);
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (reasons.size > 0) return;
|
|
287
|
-
const timeout = this.#activeTimeouts.get(item);
|
|
288
|
-
if (!timeout) return;
|
|
289
|
-
if (timeout.id !== 0 || timeout.remaining <= 0) return;
|
|
290
|
-
item.removeAttribute("data-paused");
|
|
291
|
-
this.#startTimer(item, timeout.remaining);
|
|
368
|
+
if (item && this.#dismiss.resume(item, this.#pauseReason(event))) {
|
|
369
|
+
item.removeAttribute("data-paused");
|
|
370
|
+
}
|
|
292
371
|
}
|
|
293
372
|
/** Resolves the toast item element a pause/resume event targets. */
|
|
294
373
|
#itemFromEvent(event) {
|
|
@@ -301,36 +380,20 @@ var ToastController = class extends Controller {
|
|
|
301
380
|
#pauseReason(event) {
|
|
302
381
|
return event.type === "focusin" || event.type === "focusout" ? "focus" : "hover";
|
|
303
382
|
}
|
|
304
|
-
/**
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
if (!reasons) {
|
|
308
|
-
reasons = /* @__PURE__ */ new Set();
|
|
309
|
-
this.#pauseReasons.set(item, reasons);
|
|
310
|
-
}
|
|
311
|
-
return reasons;
|
|
312
|
-
}
|
|
313
|
-
#startTimer(element, duration = this.durationValue) {
|
|
314
|
-
if (duration <= 0 || element.dataset.state === "leaving" || element.parentNode !== this.listTarget) {
|
|
383
|
+
/** Arms a toast's auto-dismiss; a non-positive `duration` means it never expires. */
|
|
384
|
+
#startTimer(element) {
|
|
385
|
+
if (this.durationValue <= 0 || element.dataset.state === "leaving" || element.parentNode !== this.listTarget) {
|
|
315
386
|
return;
|
|
316
387
|
}
|
|
317
|
-
this.#
|
|
318
|
-
|
|
319
|
-
this.#removeWithTransition(element, "timeout")
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
#clearTimer(element) {
|
|
324
|
-
const timeout = this.#activeTimeouts.get(element);
|
|
325
|
-
if (timeout) {
|
|
326
|
-
if (timeout.id) this.#timers.clear(timeout.id);
|
|
327
|
-
this.#activeTimeouts.delete(element);
|
|
328
|
-
}
|
|
329
|
-
this.#pauseReasons.delete(element);
|
|
388
|
+
this.#dismiss.set(
|
|
389
|
+
element,
|
|
390
|
+
() => this.#removeWithTransition(element, "timeout"),
|
|
391
|
+
this.durationValue
|
|
392
|
+
);
|
|
330
393
|
}
|
|
331
394
|
#removeWithTransition(element, reason) {
|
|
332
395
|
if (element.dataset.state === "leaving" || element.parentNode !== this.listTarget) return;
|
|
333
|
-
this.#
|
|
396
|
+
this.#dismiss.clear(element);
|
|
334
397
|
this.#cancelAnimation(element);
|
|
335
398
|
element.setAttribute("data-state", "leaving");
|
|
336
399
|
const finalize = () => {
|