stimeo-ui 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +163 -0
- package/dist/cable/index.d.ts +16 -16
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +3 -2
- package/dist/controllers/accordion_controller.js +10 -0
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +64 -0
- package/dist/controllers/alert_dialog_controller.js +318 -0
- package/dist/controllers/alert_dialog_controller.js.map +1 -0
- package/dist/controllers/breadcrumb_controller.d.ts +97 -10
- package/dist/controllers/breadcrumb_controller.js +225 -13
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +11 -1
- package/dist/controllers/calendar_controller.js +89 -22
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +115 -0
- package/dist/controllers/carousel_controller.js +313 -0
- package/dist/controllers/carousel_controller.js.map +1 -0
- package/dist/controllers/clipboard_controller.d.ts +72 -0
- package/dist/controllers/clipboard_controller.js +144 -0
- package/dist/controllers/clipboard_controller.js.map +1 -0
- package/dist/controllers/collapsible_controller.d.ts +70 -0
- package/dist/controllers/collapsible_controller.js +327 -0
- package/dist/controllers/collapsible_controller.js.map +1 -0
- package/dist/controllers/color_picker_controller.d.ts +87 -0
- package/dist/controllers/color_picker_controller.js +252 -0
- package/dist/controllers/color_picker_controller.js.map +1 -0
- package/dist/controllers/combobox_controller.d.ts +14 -1
- package/dist/controllers/combobox_controller.js +162 -23
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +2 -0
- package/dist/controllers/command_palette_controller.js +194 -17
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +7 -6
- package/dist/controllers/context_menu_controller.js +32 -10
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js +8 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +72 -0
- package/dist/controllers/currency_input_controller.js +147 -0
- package/dist/controllers/currency_input_controller.js.map +1 -0
- package/dist/controllers/data_grid_controller.d.ts +89 -0
- package/dist/controllers/data_grid_controller.js +246 -0
- package/dist/controllers/data_grid_controller.js.map +1 -0
- package/dist/controllers/date_range_picker_controller.d.ts +88 -0
- package/dist/controllers/date_range_picker_controller.js +441 -0
- package/dist/controllers/date_range_picker_controller.js.map +1 -0
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +57 -0
- package/dist/controllers/dismissible_controller.js +117 -0
- package/dist/controllers/dismissible_controller.js.map +1 -0
- package/dist/controllers/drawer_controller.d.ts +96 -0
- package/dist/controllers/drawer_controller.js +630 -0
- package/dist/controllers/drawer_controller.js.map +1 -0
- package/dist/controllers/dropdown_controller.d.ts +1 -2
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +73 -0
- package/dist/controllers/editable_controller.js +169 -0
- package/dist/controllers/editable_controller.js.map +1 -0
- package/dist/controllers/file_dropzone_controller.d.ts +92 -0
- package/dist/controllers/file_dropzone_controller.js +165 -0
- package/dist/controllers/file_dropzone_controller.js.map +1 -0
- package/dist/controllers/filter_controller.d.ts +60 -0
- package/dist/controllers/filter_controller.js +86 -0
- package/dist/controllers/filter_controller.js.map +1 -0
- package/dist/controllers/flash_controller.js +36 -5
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +1 -1
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +6 -4
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +1 -1
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +67 -19
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +54 -11
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +9 -2
- package/dist/controllers/listbox_controller.js +257 -53
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -2
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +54 -0
- package/dist/controllers/masonry_controller.js +142 -0
- package/dist/controllers/masonry_controller.js.map +1 -0
- package/dist/controllers/menu_controller.d.ts +29 -11
- package/dist/controllers/menu_controller.js +104 -17
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +142 -0
- package/dist/controllers/menubar_controller.js +785 -0
- package/dist/controllers/menubar_controller.js.map +1 -0
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +141 -0
- package/dist/controllers/multi_select_controller.js +755 -0
- package/dist/controllers/multi_select_controller.js.map +1 -0
- package/dist/controllers/navigation_menu_controller.d.ts +146 -0
- package/dist/controllers/navigation_menu_controller.js +511 -0
- package/dist/controllers/navigation_menu_controller.js.map +1 -0
- package/dist/controllers/nested_form_controller.d.ts +4 -5
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +1 -1
- package/dist/controllers/number_input_controller.js +7 -0
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js +18 -1
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +15 -6
- package/dist/controllers/overflow_indicator_controller.js +246 -27
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +78 -19
- package/dist/controllers/overflow_menu_controller.js +381 -57
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +36 -5
- package/dist/controllers/pagination_controller.js +163 -32
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +49 -0
- package/dist/controllers/password_reveal_controller.js +117 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -0
- package/dist/controllers/persist_controller.js +6 -6
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +2 -3
- package/dist/controllers/pointer_drag_controller.js +9 -1
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +1 -2
- package/dist/controllers/popover_controller.js +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +4 -4
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.js +22 -3
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +97 -0
- package/dist/controllers/range_slider_controller.js +192 -0
- package/dist/controllers/range_slider_controller.js.map +1 -0
- package/dist/controllers/rating_controller.js +16 -2
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +52 -0
- package/dist/controllers/read_more_controller.js +238 -0
- package/dist/controllers/read_more_controller.js.map +1 -0
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +3 -1
- package/dist/controllers/resizable_controller.js +65 -1
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +17 -2
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +101 -14
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +54 -0
- package/dist/controllers/scroll_restore_controller.js +93 -0
- package/dist/controllers/scroll_restore_controller.js.map +1 -0
- package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
- package/dist/controllers/scroll_visibility_controller.js +40 -6
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +64 -15
- package/dist/controllers/scrollspy_controller.js +369 -74
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +69 -0
- package/dist/controllers/separator_controller.js +96 -0
- package/dist/controllers/separator_controller.js.map +1 -0
- package/dist/controllers/sidebar_controller.d.ts +107 -0
- package/dist/controllers/sidebar_controller.js +761 -0
- package/dist/controllers/sidebar_controller.js.map +1 -0
- package/dist/controllers/skeleton_controller.js +1 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +11 -1
- package/dist/controllers/slider_controller.js +32 -6
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +3 -1
- package/dist/controllers/sortable_controller.js +34 -3
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +1 -1
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +4 -0
- package/dist/controllers/stepper_controller.js +28 -12
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +5 -6
- package/dist/controllers/stick_to_bottom_controller.js +9 -4
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +17 -4
- package/dist/controllers/sticky_observer_controller.js +109 -20
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +2 -2
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js +1 -0
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +13 -4
- package/dist/controllers/tabs_controller.js +26 -3
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +91 -0
- package/dist/controllers/tags_input_controller.js +295 -0
- package/dist/controllers/tags_input_controller.js.map +1 -0
- package/dist/controllers/theme_controller.d.ts +1 -1
- package/dist/controllers/theme_controller.js +42 -13
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +67 -0
- package/dist/controllers/time_picker_controller.js +231 -0
- package/dist/controllers/time_picker_controller.js.map +1 -0
- package/dist/controllers/toast_controller.d.ts +5 -14
- package/dist/controllers/toast_controller.js +40 -14
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js +23 -2
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +53 -13
- package/dist/controllers/toolbar_controller.js +230 -31
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +1 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +14 -8
- package/dist/controllers/transition_controller.js +153 -38
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +135 -0
- package/dist/controllers/tree_view_controller.js +691 -0
- package/dist/controllers/tree_view_controller.js.map +1 -0
- package/dist/index.d.ts +27 -1818
- package/dist/index.js +4256 -915
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +375 -39
- package/dist/inspector/cli.js +228 -14
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +419 -48
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +26 -26
- package/dist/inspector/manifest.json +1054 -127
- package/package.json +5 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\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/**\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-stimeo--theme-mode-param=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"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 * @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, and 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 (documented), not this controller.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: \"system\" },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: \"html\" },\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 /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n if (this.modeValue === \"system\") {\n this.#applyTheme();\n // Re-sync controls too: a 2-value toggle's `aria-pressed` reflects the\n // *resolved* theme, so it must follow the OS flip — not only `data-theme`.\n this.#syncControls();\n this.#dispatchChange();\n }\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n if (options.length === 0) return;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n const last = options.length - 1;\n let next = current;\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n next = current === last ? 0 : current + 1;\n break;\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = current === 0 ? last : current - 1;\n break;\n case \"Home\":\n next = 0;\n break;\n case \"End\":\n next = last;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n this.#setMode(this.#optionMode(option));\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 if (this.hasOptionTarget) this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#applyTheme();\n this.#syncControls();\n }\n\n override disconnect(): void {\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Selects an explicit mode from the `mode` action param (radiogroup option). */\n set(event: Event): void {\n const mode = (event as { params?: Record<string, unknown> }).params?.mode;\n if (isMode(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.#applyTheme();\n this.#syncControls();\n this.#dispatchChange();\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 let hasTabbable = false;\n for (const option of options) {\n const selected = this.#optionMode(option) === this.modeValue;\n option.setAttribute(\"aria-checked\", String(selected));\n option.tabIndex = selected ? 0 : -1;\n hasTabbable ||= selected;\n }\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n const first = options[0];\n if (!hasTabbable && first) first.tabIndex = 0;\n return;\n }\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n\n /** Emits `change` with the selected mode and the resolved theme. */\n #dispatchChange(): void {\n this.dispatch(\"change\", { detail: { mode: this.modeValue, resolved: this.#resolved() } });\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n if (this.modeValue === \"dark\") return \"dark\";\n if (this.modeValue === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** Reads an option's mode from its action param attribute. */\n #optionMode(option: HTMLElement): ThemeMode {\n const mode = option.getAttribute(\"data-stimeo--theme-mode-param\");\n return isMode(mode) ? mode : \"system\";\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n if (this.targetValue === \"html\" || this.targetValue === \":root\")\n return document.documentElement;\n return document.querySelector<HTMLElement>(this.targetValue);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n try {\n const value = window.localStorage.getItem(this.storageKeyValue);\n return isMode(value) ? value : null;\n } catch {\n return null;\n }\n }\n\n /** Persists the mode, swallowing storage errors (private mode / quota). */\n #writeStored(mode: ThemeMode): void {\n try {\n window.localStorage.setItem(this.storageKeyValue, mode);\n } catch {\n /* storage unavailable — the in-DOM state hooks still apply this session */\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.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 * Guarded `localStorage` access shared by the preference-persisting controllers\n * (sidebar, theme).\n *\n * `window.localStorage` can throw on access or read/write — storage disabled by\n * browser policy, sandboxed frames, private modes, or quota exhaustion. These\n * helpers centralize the try/catch so callers treat persistence as best-effort:\n * a failed read behaves like an unset key and a failed write is dropped,\n * leaving the in-DOM state as this session's source of truth.\n *\n * @remarks\n * Only the guarded access lives here — interpreting the stored string\n * (validation, defaults, key derivation) stays in each controller. The\n * `stimeo--persist` controller keeps its own storage layer on purpose: storage\n * *is* its domain (engine selection, per-field bookkeeping, TTL semantics).\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns The stored string, or `null` when the key is unset or storage is\n * unavailable (the read threw).\n */\nexport function readLocalStorage(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`, ignoring failures (private\n * mode / quota / storage disabled) — persistence is best-effort by contract.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n */\nexport function writeLocalStorage(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Best-effort: the in-DOM state still applies for this session.\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowStep } from \"../utils/arrow_step\";\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/**\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-stimeo--theme-mode-param=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"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 * @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, and 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 */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: \"system\" },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: \"html\" },\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 /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n if (this.modeValue === \"system\") {\n this.#applyTheme();\n // Re-sync controls too: a 2-value toggle's `aria-pressed` reflects the\n // *resolved* theme, so it must follow the OS flip — not only `data-theme`.\n this.#syncControls();\n this.#dispatchChange();\n }\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 if (options.length === 0) return;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n const last = options.length - 1;\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 =\n step === 1 ? (current === last ? 0 : current + 1) : current === 0 ? last : current - 1;\n break;\n case \"Home\":\n next = 0;\n break;\n case \"End\":\n next = last;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n this.#setMode(this.#optionMode(option));\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 if (this.hasOptionTarget) this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#applyTheme();\n this.#syncControls();\n }\n\n override disconnect(): void {\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Selects an explicit mode from the `mode` action param (radiogroup option). */\n set(event: Event): void {\n const mode = (event as { params?: Record<string, unknown> }).params?.mode;\n if (isMode(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.#applyTheme();\n this.#syncControls();\n this.#dispatchChange();\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 let hasTabbable = false;\n for (const option of options) {\n const selected = this.#optionMode(option) === this.modeValue;\n option.setAttribute(\"aria-checked\", String(selected));\n option.tabIndex = selected ? 0 : -1;\n hasTabbable ||= selected;\n }\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n const first = options[0];\n if (!hasTabbable && first) first.tabIndex = 0;\n return;\n }\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n\n /** Emits `change` with the selected mode and the resolved theme. */\n #dispatchChange(): void {\n this.dispatch(\"change\", { detail: { mode: this.modeValue, resolved: this.#resolved() } });\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n if (this.modeValue === \"dark\") return \"dark\";\n if (this.modeValue === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** Reads an option's mode from its action param attribute. */\n #optionMode(option: HTMLElement): ThemeMode {\n const mode = option.getAttribute(\"data-stimeo--theme-mode-param\");\n return isMode(mode) ? mode : \"system\";\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n if (this.targetValue === \"html\" || this.targetValue === \":root\")\n return document.documentElement;\n return document.querySelector<HTMLElement>(this.targetValue);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const value = readLocalStorage(this.storageKeyValue);\n return isMode(value) ? 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"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless, accessible **time picker** behavior. Each segment (hour, minute,
|
|
5
|
+
* optional second, optional AM/PM) is an APG **Spinbutton**; the controller
|
|
6
|
+
* composes them into an `HH:MM[:SS]` value mirrored to a hidden field.
|
|
7
|
+
*
|
|
8
|
+
* Markup contract (identifier: `stimeo--time-picker`):
|
|
9
|
+
* <div data-controller="stimeo--time-picker"
|
|
10
|
+
* data-stimeo--time-picker-hour-cycle-value="24"
|
|
11
|
+
* role="group" aria-label="Time">
|
|
12
|
+
* <span role="spinbutton" aria-label="Hours" tabindex="0"
|
|
13
|
+
* aria-valuenow="9" aria-valuemin="0" aria-valuemax="23" aria-valuetext="09"
|
|
14
|
+
* data-segment="hour" data-stimeo--time-picker-target="segment"
|
|
15
|
+
* data-action="keydown->stimeo--time-picker#onKeydown">09</span>
|
|
16
|
+
* <span aria-hidden="true">:</span>
|
|
17
|
+
* <span role="spinbutton" aria-label="Minutes" tabindex="0"
|
|
18
|
+
* data-segment="minute" data-stimeo--time-picker-target="segment"
|
|
19
|
+
* data-action="keydown->stimeo--time-picker#onKeydown">30</span>
|
|
20
|
+
* <input type="hidden" data-stimeo--time-picker-target="field" />
|
|
21
|
+
* </div>
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* Behavior only — no styling, no locale formatting. Every segment is its own Tab
|
|
25
|
+
* stop (multi-tabstop, *not* roving); `ArrowLeft`/`ArrowRight` are an auxiliary
|
|
26
|
+
* move between segments. `ArrowUp`/`ArrowDown` step the focused segment, wrapping
|
|
27
|
+
* (and, when `wrap` is set, carrying minutes→hours and seconds→minutes). Typing
|
|
28
|
+
* digits enters a value directly and advances after two digits. AM/PM is modeled
|
|
29
|
+
* as a `meridiem` spinbutton that toggles; the hidden field is always 24-hour.
|
|
30
|
+
*/
|
|
31
|
+
declare class TimePickerController extends Controller<HTMLElement> {
|
|
32
|
+
#private;
|
|
33
|
+
static targets: string[];
|
|
34
|
+
static values: {
|
|
35
|
+
hourCycle: {
|
|
36
|
+
type: NumberConstructor;
|
|
37
|
+
default: number;
|
|
38
|
+
};
|
|
39
|
+
step: {
|
|
40
|
+
type: NumberConstructor;
|
|
41
|
+
default: number;
|
|
42
|
+
};
|
|
43
|
+
seconds: {
|
|
44
|
+
type: BooleanConstructor;
|
|
45
|
+
default: boolean;
|
|
46
|
+
};
|
|
47
|
+
wrap: {
|
|
48
|
+
type: BooleanConstructor;
|
|
49
|
+
default: boolean;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
static actions: readonly ["onKeydown"];
|
|
53
|
+
static events: readonly ["change"];
|
|
54
|
+
readonly segmentTargets: HTMLElement[];
|
|
55
|
+
readonly fieldTarget: HTMLInputElement;
|
|
56
|
+
readonly hasFieldTarget: boolean;
|
|
57
|
+
hourCycleValue: number;
|
|
58
|
+
stepValue: number;
|
|
59
|
+
secondsValue: boolean;
|
|
60
|
+
wrapValue: boolean;
|
|
61
|
+
/** Seeds each segment from its initial `aria-valuenow` and syncs the field. */
|
|
62
|
+
connect(): void;
|
|
63
|
+
/** Handles stepping, inter-segment focus moves, jumps, and direct entry. */
|
|
64
|
+
onKeydown(event: KeyboardEvent): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { TimePickerController };
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/time_picker_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/logical_scroll.ts
|
|
6
|
+
function isRtl(element) {
|
|
7
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/utils/arrow_step.ts
|
|
11
|
+
function logicalArrowKey(key, element) {
|
|
12
|
+
if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
|
|
13
|
+
if (!isRtl(element)) return key;
|
|
14
|
+
return key === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
|
|
15
|
+
}
|
|
16
|
+
function isReservedArrowChord(event, allow = []) {
|
|
17
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
18
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/controllers/time_picker_controller.ts
|
|
22
|
+
var AM = 0;
|
|
23
|
+
var PM = 1;
|
|
24
|
+
var TimePickerController = class extends Controller {
|
|
25
|
+
static targets = ["segment", "field"];
|
|
26
|
+
static values = {
|
|
27
|
+
hourCycle: { type: Number, default: 24 },
|
|
28
|
+
step: { type: Number, default: 1 },
|
|
29
|
+
seconds: { type: Boolean, default: false },
|
|
30
|
+
wrap: { type: Boolean, default: true }
|
|
31
|
+
};
|
|
32
|
+
static actions = ["onKeydown"];
|
|
33
|
+
static events = ["change"];
|
|
34
|
+
/** Current numeric value per segment kind (hours are the *displayed* hours). */
|
|
35
|
+
#state = { hour: 0, minute: 0, second: 0, meridiem: AM };
|
|
36
|
+
/** Direct-entry digit buffer and the segment it belongs to. */
|
|
37
|
+
#typeBuffer = "";
|
|
38
|
+
#typeSegment = null;
|
|
39
|
+
/** Last composed field value, to suppress duplicate `change` dispatches. */
|
|
40
|
+
#lastValue = "";
|
|
41
|
+
/** Seeds each segment from its initial `aria-valuenow` and syncs the field. */
|
|
42
|
+
connect() {
|
|
43
|
+
for (const segment of this.segmentTargets) {
|
|
44
|
+
const kind = this.#kindOf(segment);
|
|
45
|
+
if (!kind) continue;
|
|
46
|
+
const now = Number(segment.getAttribute("aria-valuenow"));
|
|
47
|
+
const { min, max } = this.#bounds(kind);
|
|
48
|
+
this.#state[kind] = Number.isFinite(now) ? Math.min(max, Math.max(min, now)) : min;
|
|
49
|
+
}
|
|
50
|
+
for (const segment of this.segmentTargets) this.#renderSegment(segment);
|
|
51
|
+
this.#syncField(false);
|
|
52
|
+
}
|
|
53
|
+
/** Handles stepping, inter-segment focus moves, jumps, and direct entry. */
|
|
54
|
+
onKeydown(event) {
|
|
55
|
+
if (isReservedArrowChord(event)) return;
|
|
56
|
+
const segment = event.target?.closest(
|
|
57
|
+
"[data-stimeo--time-picker-target='segment']"
|
|
58
|
+
);
|
|
59
|
+
const kind = segment ? this.#kindOf(segment) : null;
|
|
60
|
+
if (!segment || !kind) return;
|
|
61
|
+
switch (logicalArrowKey(event.key, this.element)) {
|
|
62
|
+
case "ArrowUp":
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
this.#step(kind, this.#delta(kind));
|
|
65
|
+
break;
|
|
66
|
+
case "ArrowDown":
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
this.#step(kind, -this.#delta(kind));
|
|
69
|
+
break;
|
|
70
|
+
case "ArrowLeft":
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
this.#focusSibling(segment, -1);
|
|
73
|
+
break;
|
|
74
|
+
case "ArrowRight":
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
this.#focusSibling(segment, 1);
|
|
77
|
+
break;
|
|
78
|
+
case "Home":
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
this.#set(kind, this.#bounds(kind).min);
|
|
81
|
+
break;
|
|
82
|
+
case "End":
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
this.#set(kind, this.#bounds(kind).max);
|
|
85
|
+
break;
|
|
86
|
+
default:
|
|
87
|
+
if (/^[0-9]$/.test(event.key)) {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
this.#typeDigit(segment, kind, event.key);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.#typeBuffer = "";
|
|
94
|
+
this.#typeSegment = null;
|
|
95
|
+
}
|
|
96
|
+
/** The per-step delta: minutes step by `step`, others by 1, meridiem toggles. */
|
|
97
|
+
#delta(kind) {
|
|
98
|
+
return kind === "minute" ? this.stepValue : 1;
|
|
99
|
+
}
|
|
100
|
+
/** Steps a segment, wrapping at its bounds and carrying over when enabled. */
|
|
101
|
+
#step(kind, delta) {
|
|
102
|
+
if (kind === "meridiem") {
|
|
103
|
+
this.#set("meridiem", this.#state.meridiem === AM ? PM : AM);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const { min, max } = this.#bounds(kind);
|
|
107
|
+
const span = max - min + 1;
|
|
108
|
+
const raw = this.#state[kind] + delta;
|
|
109
|
+
if (raw > max || raw < min) {
|
|
110
|
+
if (!this.wrapValue) {
|
|
111
|
+
this.#set(kind, Math.min(max, Math.max(min, raw)));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const wrapped = ((raw - min) % span + span) % span + min;
|
|
115
|
+
const carry = Math.floor((raw - min) / span);
|
|
116
|
+
this.#set(kind, wrapped);
|
|
117
|
+
this.#carry(kind, carry);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.#set(kind, raw);
|
|
121
|
+
}
|
|
122
|
+
/** Propagates a wrap carry from `kind` into the next larger segment. */
|
|
123
|
+
#carry(kind, amount) {
|
|
124
|
+
if (amount === 0) return;
|
|
125
|
+
if (kind === "second") this.#step("minute", amount);
|
|
126
|
+
else if (kind === "minute") this.#step("hour", amount);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Sets a segment's value (clamped to its `[min, max]` bounds), re-renders it,
|
|
130
|
+
* and resyncs the composed field. Clamping here guards the direct-entry path:
|
|
131
|
+
* typing `0` into a 12-hour hour (min 1) must not commit an out-of-range
|
|
132
|
+
* `aria-valuenow="0"`. The stepping path already passes in-bounds values, so
|
|
133
|
+
* the clamp is a no-op there.
|
|
134
|
+
*/
|
|
135
|
+
#set(kind, value) {
|
|
136
|
+
const { min, max } = this.#bounds(kind);
|
|
137
|
+
this.#state[kind] = Math.min(max, Math.max(min, value));
|
|
138
|
+
const segment = this.segmentTargets.find((s) => this.#kindOf(s) === kind);
|
|
139
|
+
if (segment) this.#renderSegment(segment);
|
|
140
|
+
this.#syncField(true);
|
|
141
|
+
}
|
|
142
|
+
/** Accumulates a typed digit, committing and advancing after two digits. */
|
|
143
|
+
#typeDigit(segment, kind, digit) {
|
|
144
|
+
if (kind === "meridiem") return;
|
|
145
|
+
if (this.#typeSegment !== kind) this.#typeBuffer = "";
|
|
146
|
+
this.#typeSegment = kind;
|
|
147
|
+
const { max } = this.#bounds(kind);
|
|
148
|
+
const candidate = Number(`${this.#typeBuffer}${digit}`);
|
|
149
|
+
if (candidate <= max) this.#typeBuffer = `${this.#typeBuffer}${digit}`;
|
|
150
|
+
else this.#typeBuffer = digit;
|
|
151
|
+
this.#set(kind, Number(this.#typeBuffer));
|
|
152
|
+
if (this.#typeBuffer.length >= 2 || Number(this.#typeBuffer) * 10 > max) {
|
|
153
|
+
this.#typeBuffer = "";
|
|
154
|
+
this.#typeSegment = null;
|
|
155
|
+
this.#focusSibling(segment, 1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Moves focus to the previous/next segment, if one exists. */
|
|
159
|
+
#focusSibling(segment, direction) {
|
|
160
|
+
const index = this.segmentTargets.indexOf(segment);
|
|
161
|
+
const next = this.segmentTargets[index + direction];
|
|
162
|
+
next?.focus();
|
|
163
|
+
}
|
|
164
|
+
/** Reflects a segment's current value onto its ARIA/text representation. */
|
|
165
|
+
#renderSegment(segment) {
|
|
166
|
+
const kind = this.#kindOf(segment);
|
|
167
|
+
if (!kind) return;
|
|
168
|
+
const value = this.#state[kind];
|
|
169
|
+
if (kind === "meridiem") {
|
|
170
|
+
const text2 = value === PM ? "PM" : "AM";
|
|
171
|
+
segment.setAttribute("aria-valuenow", String(value));
|
|
172
|
+
segment.setAttribute("aria-valuetext", text2);
|
|
173
|
+
segment.setAttribute("aria-valuemin", String(AM));
|
|
174
|
+
segment.setAttribute("aria-valuemax", String(PM));
|
|
175
|
+
segment.textContent = text2;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const { min, max } = this.#bounds(kind);
|
|
179
|
+
const text = String(value).padStart(2, "0");
|
|
180
|
+
segment.setAttribute("aria-valuenow", String(value));
|
|
181
|
+
segment.setAttribute("aria-valuetext", text);
|
|
182
|
+
segment.setAttribute("aria-valuemin", String(min));
|
|
183
|
+
segment.setAttribute("aria-valuemax", String(max));
|
|
184
|
+
segment.textContent = text;
|
|
185
|
+
}
|
|
186
|
+
/** Composes `HH:MM[:SS]` (24-hour) into the hidden field; dispatches `change`. */
|
|
187
|
+
#syncField(notify) {
|
|
188
|
+
const h24 = this.#hours24();
|
|
189
|
+
const parts = [pad(h24), pad(this.#state.minute)];
|
|
190
|
+
if (this.secondsValue) parts.push(pad(this.#state.second));
|
|
191
|
+
const value = parts.join(":");
|
|
192
|
+
if (this.hasFieldTarget && this.fieldTarget.value !== value) {
|
|
193
|
+
this.fieldTarget.value = value;
|
|
194
|
+
}
|
|
195
|
+
if (notify && value !== this.#lastValue) this.dispatch("change", { detail: { value } });
|
|
196
|
+
this.#lastValue = value;
|
|
197
|
+
}
|
|
198
|
+
/** Converts the displayed hour (+ meridiem in 12-hour mode) to 24-hour. */
|
|
199
|
+
#hours24() {
|
|
200
|
+
if (this.hourCycleValue !== 12) return this.#state.hour;
|
|
201
|
+
const base = this.#state.hour % 12;
|
|
202
|
+
return base + (this.#state.meridiem === PM ? 12 : 0);
|
|
203
|
+
}
|
|
204
|
+
/** The inclusive `[min, max]` bounds for a segment kind. */
|
|
205
|
+
#bounds(kind) {
|
|
206
|
+
switch (kind) {
|
|
207
|
+
case "hour":
|
|
208
|
+
return this.hourCycleValue === 12 ? { min: 1, max: 12 } : { min: 0, max: 23 };
|
|
209
|
+
case "minute":
|
|
210
|
+
case "second":
|
|
211
|
+
return { min: 0, max: 59 };
|
|
212
|
+
case "meridiem":
|
|
213
|
+
return { min: AM, max: PM };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Reads a segment's declared kind, or null when absent/invalid. */
|
|
217
|
+
#kindOf(segment) {
|
|
218
|
+
const kind = segment.getAttribute("data-segment");
|
|
219
|
+
if (kind === "hour" || kind === "minute" || kind === "second" || kind === "meridiem") {
|
|
220
|
+
return kind;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
function pad(value) {
|
|
226
|
+
return String(value).padStart(2, "0");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export { TimePickerController };
|
|
230
|
+
//# sourceMappingURL=time_picker_controller.js.map
|
|
231
|
+
//# sourceMappingURL=time_picker_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/controllers/time_picker_controller.ts"],"names":["text"],"mappings":"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","file":"time_picker_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","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\n\n/** A time segment kind, as declared by `data-segment` on each spinbutton. */\ntype SegmentKind = \"hour\" | \"minute\" | \"second\" | \"meridiem\";\n\n/** Meridiem encoding: 0 = AM, 1 = PM. */\nconst AM = 0;\nconst PM = 1;\n\n/**\n * Headless, accessible **time picker** behavior. Each segment (hour, minute,\n * optional second, optional AM/PM) is an APG **Spinbutton**; the controller\n * composes them into an `HH:MM[:SS]` value mirrored to a hidden field.\n *\n * Markup contract (identifier: `stimeo--time-picker`):\n * <div data-controller=\"stimeo--time-picker\"\n * data-stimeo--time-picker-hour-cycle-value=\"24\"\n * role=\"group\" aria-label=\"Time\">\n * <span role=\"spinbutton\" aria-label=\"Hours\" tabindex=\"0\"\n * aria-valuenow=\"9\" aria-valuemin=\"0\" aria-valuemax=\"23\" aria-valuetext=\"09\"\n * data-segment=\"hour\" data-stimeo--time-picker-target=\"segment\"\n * data-action=\"keydown->stimeo--time-picker#onKeydown\">09</span>\n * <span aria-hidden=\"true\">:</span>\n * <span role=\"spinbutton\" aria-label=\"Minutes\" tabindex=\"0\"\n * data-segment=\"minute\" data-stimeo--time-picker-target=\"segment\"\n * data-action=\"keydown->stimeo--time-picker#onKeydown\">30</span>\n * <input type=\"hidden\" data-stimeo--time-picker-target=\"field\" />\n * </div>\n *\n * @remarks\n * Behavior only — no styling, no locale formatting. Every segment is its own Tab\n * stop (multi-tabstop, *not* roving); `ArrowLeft`/`ArrowRight` are an auxiliary\n * move between segments. `ArrowUp`/`ArrowDown` step the focused segment, wrapping\n * (and, when `wrap` is set, carrying minutes→hours and seconds→minutes). Typing\n * digits enters a value directly and advances after two digits. AM/PM is modeled\n * as a `meridiem` spinbutton that toggles; the hidden field is always 24-hour.\n */\nexport class TimePickerController extends Controller<HTMLElement> {\n static override targets = [\"segment\", \"field\"];\n static override values = {\n hourCycle: { type: Number, default: 24 },\n step: { type: Number, default: 1 },\n seconds: { type: Boolean, default: false },\n wrap: { type: Boolean, default: true },\n };\n static actions = [\"onKeydown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly segmentTargets: HTMLElement[];\n declare readonly fieldTarget: HTMLInputElement;\n declare readonly hasFieldTarget: boolean;\n declare hourCycleValue: number;\n declare stepValue: number;\n declare secondsValue: boolean;\n declare wrapValue: boolean;\n\n /** Current numeric value per segment kind (hours are the *displayed* hours). */\n #state: Record<SegmentKind, number> = { hour: 0, minute: 0, second: 0, meridiem: AM };\n /** Direct-entry digit buffer and the segment it belongs to. */\n #typeBuffer = \"\";\n #typeSegment: SegmentKind | null = null;\n /** Last composed field value, to suppress duplicate `change` dispatches. */\n #lastValue = \"\";\n\n /** Seeds each segment from its initial `aria-valuenow` and syncs the field. */\n override connect(): void {\n for (const segment of this.segmentTargets) {\n const kind = this.#kindOf(segment);\n if (!kind) continue;\n const now = Number(segment.getAttribute(\"aria-valuenow\"));\n const { min, max } = this.#bounds(kind);\n // Clamp the seeded value so malformed markup (out-of-range aria-valuenow)\n // never propagates into the rendered state or the composed field.\n this.#state[kind] = Number.isFinite(now) ? Math.min(max, Math.max(min, now)) : min;\n }\n for (const segment of this.segmentTargets) this.#renderSegment(segment);\n this.#syncField(false);\n }\n\n /** Handles stepping, inter-segment focus moves, jumps, and direct entry. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n const segment = (event.target as HTMLElement | null)?.closest<HTMLElement>(\n \"[data-stimeo--time-picker-target='segment']\",\n );\n const kind = segment ? this.#kindOf(segment) : null;\n if (!segment || !kind) return;\n // Logical, not physical, for the horizontal pair: the segments form an\n // ordered row laid out inline, so a right-to-left writing direction mirrors\n // them. `ArrowUp` / `ArrowDown` change the *value* and pass through\n // untouched — `logicalArrowKey` only ever rewrites the horizontal pair.\n\n switch (logicalArrowKey(event.key, this.element)) {\n case \"ArrowUp\":\n event.preventDefault();\n this.#step(kind, this.#delta(kind));\n break;\n case \"ArrowDown\":\n event.preventDefault();\n this.#step(kind, -this.#delta(kind));\n break;\n case \"ArrowLeft\":\n event.preventDefault();\n this.#focusSibling(segment, -1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n this.#focusSibling(segment, 1);\n break;\n case \"Home\":\n event.preventDefault();\n this.#set(kind, this.#bounds(kind).min);\n break;\n case \"End\":\n event.preventDefault();\n this.#set(kind, this.#bounds(kind).max);\n break;\n default:\n if (/^[0-9]$/.test(event.key)) {\n event.preventDefault();\n this.#typeDigit(segment, kind, event.key);\n }\n return;\n }\n // Any non-digit action ends the current direct-entry sequence.\n this.#typeBuffer = \"\";\n this.#typeSegment = null;\n }\n\n /** The per-step delta: minutes step by `step`, others by 1, meridiem toggles. */\n #delta(kind: SegmentKind): number {\n return kind === \"minute\" ? this.stepValue : 1;\n }\n\n /** Steps a segment, wrapping at its bounds and carrying over when enabled. */\n #step(kind: SegmentKind, delta: number): void {\n if (kind === \"meridiem\") {\n this.#set(\"meridiem\", this.#state.meridiem === AM ? PM : AM);\n return;\n }\n const { min, max } = this.#bounds(kind);\n const span = max - min + 1;\n const raw = this.#state[kind] + delta;\n\n if (raw > max || raw < min) {\n if (!this.wrapValue) {\n this.#set(kind, Math.min(max, Math.max(min, raw)));\n return;\n }\n // Wrap within the segment, carrying the overflow into the larger unit.\n const wrapped = ((((raw - min) % span) + span) % span) + min;\n const carry = Math.floor((raw - min) / span);\n this.#set(kind, wrapped);\n this.#carry(kind, carry);\n return;\n }\n this.#set(kind, raw);\n }\n\n /** Propagates a wrap carry from `kind` into the next larger segment. */\n #carry(kind: SegmentKind, amount: number): void {\n if (amount === 0) return;\n if (kind === \"second\") this.#step(\"minute\", amount);\n else if (kind === \"minute\") this.#step(\"hour\", amount);\n // Hours wrap on their own (no day rollover); meridiem has no carry.\n }\n\n /**\n * Sets a segment's value (clamped to its `[min, max]` bounds), re-renders it,\n * and resyncs the composed field. Clamping here guards the direct-entry path:\n * typing `0` into a 12-hour hour (min 1) must not commit an out-of-range\n * `aria-valuenow=\"0\"`. The stepping path already passes in-bounds values, so\n * the clamp is a no-op there.\n */\n #set(kind: SegmentKind, value: number): void {\n const { min, max } = this.#bounds(kind);\n this.#state[kind] = Math.min(max, Math.max(min, value));\n const segment = this.segmentTargets.find((s) => this.#kindOf(s) === kind);\n if (segment) this.#renderSegment(segment);\n this.#syncField(true);\n }\n\n /** Accumulates a typed digit, committing and advancing after two digits. */\n #typeDigit(segment: HTMLElement, kind: SegmentKind, digit: string): void {\n if (kind === \"meridiem\") return;\n if (this.#typeSegment !== kind) this.#typeBuffer = \"\";\n this.#typeSegment = kind;\n\n const { max } = this.#bounds(kind);\n const candidate = Number(`${this.#typeBuffer}${digit}`);\n if (candidate <= max) this.#typeBuffer = `${this.#typeBuffer}${digit}`;\n else this.#typeBuffer = digit; // restart from this digit when it overflows\n\n this.#set(kind, Number(this.#typeBuffer));\n\n // Two digits (or a value that can't grow further) completes the segment.\n if (this.#typeBuffer.length >= 2 || Number(this.#typeBuffer) * 10 > max) {\n this.#typeBuffer = \"\";\n this.#typeSegment = null;\n this.#focusSibling(segment, 1);\n }\n }\n\n /** Moves focus to the previous/next segment, if one exists. */\n #focusSibling(segment: HTMLElement, direction: 1 | -1): void {\n const index = this.segmentTargets.indexOf(segment);\n const next = this.segmentTargets[index + direction];\n next?.focus();\n }\n\n /** Reflects a segment's current value onto its ARIA/text representation. */\n #renderSegment(segment: HTMLElement): void {\n const kind = this.#kindOf(segment);\n if (!kind) return;\n const value = this.#state[kind];\n\n if (kind === \"meridiem\") {\n const text = value === PM ? \"PM\" : \"AM\";\n segment.setAttribute(\"aria-valuenow\", String(value));\n segment.setAttribute(\"aria-valuetext\", text);\n segment.setAttribute(\"aria-valuemin\", String(AM));\n segment.setAttribute(\"aria-valuemax\", String(PM));\n segment.textContent = text;\n return;\n }\n\n const { min, max } = this.#bounds(kind);\n const text = String(value).padStart(2, \"0\");\n segment.setAttribute(\"aria-valuenow\", String(value));\n segment.setAttribute(\"aria-valuetext\", text);\n segment.setAttribute(\"aria-valuemin\", String(min));\n segment.setAttribute(\"aria-valuemax\", String(max));\n segment.textContent = text;\n }\n\n /** Composes `HH:MM[:SS]` (24-hour) into the hidden field; dispatches `change`. */\n #syncField(notify: boolean): void {\n const h24 = this.#hours24();\n const parts = [pad(h24), pad(this.#state.minute)];\n if (this.secondsValue) parts.push(pad(this.#state.second));\n const value = parts.join(\":\");\n\n if (this.hasFieldTarget && this.fieldTarget.value !== value) {\n this.fieldTarget.value = value;\n }\n // Dispatch only when the composed value actually changed, so a no-op step\n // (e.g. Home at the minimum) does not emit a redundant event.\n if (notify && value !== this.#lastValue) this.dispatch(\"change\", { detail: { value } });\n this.#lastValue = value;\n }\n\n /** Converts the displayed hour (+ meridiem in 12-hour mode) to 24-hour. */\n #hours24(): number {\n if (this.hourCycleValue !== 12) return this.#state.hour;\n const base = this.#state.hour % 12; // 12 → 0\n return base + (this.#state.meridiem === PM ? 12 : 0);\n }\n\n /** The inclusive `[min, max]` bounds for a segment kind. */\n #bounds(kind: SegmentKind): { min: number; max: number } {\n switch (kind) {\n case \"hour\":\n return this.hourCycleValue === 12 ? { min: 1, max: 12 } : { min: 0, max: 23 };\n case \"minute\":\n case \"second\":\n return { min: 0, max: 59 };\n case \"meridiem\":\n return { min: AM, max: PM };\n }\n }\n\n /** Reads a segment's declared kind, or null when absent/invalid. */\n #kindOf(segment: HTMLElement): SegmentKind | null {\n const kind = segment.getAttribute(\"data-segment\");\n if (kind === \"hour\" || kind === \"minute\" || kind === \"second\" || kind === \"meridiem\") {\n return kind;\n }\n return null;\n }\n}\n\n/** Zero-pads a number to two digits. */\nfunction pad(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n"]}
|
|
@@ -101,21 +101,12 @@ declare class ToastController extends Controller<HTMLElement> {
|
|
|
101
101
|
/**
|
|
102
102
|
* Removes the oldest toasts when the list exceeds `maxValue`.
|
|
103
103
|
*
|
|
104
|
-
* Public (not `#private`) as a deterministic
|
|
105
|
-
*
|
|
106
|
-
* MutationObserver
|
|
107
|
-
*
|
|
108
|
-
* NON_ACTION_ALLOWLIST (it is not a user-wired action).
|
|
104
|
+
* Public (not `#private`) as a deterministic seam: enforcement normally runs
|
|
105
|
+
* from `itemTargetConnected`, a Stimulus callback delivered through a
|
|
106
|
+
* MutationObserver, which a DOM-only environment does not reliably fire — so
|
|
107
|
+
* it can also be invoked directly. It is not a user-wired action.
|
|
109
108
|
*/
|
|
110
109
|
enforceMaxLimit(): void;
|
|
111
110
|
}
|
|
112
|
-
/**
|
|
113
|
-
* Converts the first value of a CSS `transition-duration` string to milliseconds.
|
|
114
|
-
* Browsers normalize computed `<time>` values to seconds (e.g. `0.2s`), but parse
|
|
115
|
-
* `ms` defensively so a consumer's `ms` transition is not delayed 1000x.
|
|
116
|
-
*
|
|
117
|
-
* Pure (no `this`); exported so it can be unit-tested directly.
|
|
118
|
-
*/
|
|
119
|
-
declare function cssTimeToMs(value: string): number;
|
|
120
111
|
|
|
121
|
-
export { ToastController
|
|
112
|
+
export { ToastController };
|
|
@@ -55,6 +55,40 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
// src/utils/transition_completion.ts
|
|
59
|
+
function timeMs(value) {
|
|
60
|
+
const trimmed = value.trim();
|
|
61
|
+
const amount = Number.parseFloat(trimmed);
|
|
62
|
+
if (!Number.isFinite(amount)) return 0;
|
|
63
|
+
if (trimmed.endsWith("ms")) return amount;
|
|
64
|
+
if (trimmed.endsWith("s")) return amount * 1e3;
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
function cssList(value) {
|
|
68
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
function transitionTimings(style) {
|
|
71
|
+
const properties = cssList(style.transitionProperty);
|
|
72
|
+
const durations = cssList(style.transitionDuration).map(timeMs);
|
|
73
|
+
const delays = cssList(style.transitionDelay).map(timeMs);
|
|
74
|
+
const effectiveProperties = properties.length > 0 ? properties : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => "all");
|
|
75
|
+
const effectiveDurations = durations.length > 0 ? durations : [0];
|
|
76
|
+
const effectiveDelays = delays.length > 0 ? delays : [0];
|
|
77
|
+
return effectiveProperties.filter((property) => property !== "none").map((property, index) => ({
|
|
78
|
+
property,
|
|
79
|
+
totalMs: Math.max(
|
|
80
|
+
0,
|
|
81
|
+
(effectiveDurations[index % effectiveDurations.length] ?? 0) + (effectiveDelays[index % effectiveDelays.length] ?? 0)
|
|
82
|
+
)
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
function maxTotalMs(timings) {
|
|
86
|
+
return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);
|
|
87
|
+
}
|
|
88
|
+
function maxTransitionTotalMs(style) {
|
|
89
|
+
return maxTotalMs(transitionTimings(style));
|
|
90
|
+
}
|
|
91
|
+
|
|
58
92
|
// src/controllers/toast_controller.ts
|
|
59
93
|
var DELEGATED_EVENTS = ["click", "focusin", "focusout", "keydown", "mouseover", "mouseout"];
|
|
60
94
|
var ToastController = class extends Controller {
|
|
@@ -304,8 +338,7 @@ var ToastController = class extends Controller {
|
|
|
304
338
|
this.listTarget.removeChild(element);
|
|
305
339
|
this.dispatch("dismiss", { detail: { item: element, reason } });
|
|
306
340
|
};
|
|
307
|
-
const
|
|
308
|
-
const duration = cssTimeToMs(transitions);
|
|
341
|
+
const duration = maxTransitionTotalMs(window.getComputedStyle(element));
|
|
309
342
|
if (duration > 0) {
|
|
310
343
|
this.#timers.set(finalize, duration);
|
|
311
344
|
} else {
|
|
@@ -315,11 +348,10 @@ var ToastController = class extends Controller {
|
|
|
315
348
|
/**
|
|
316
349
|
* Removes the oldest toasts when the list exceeds `maxValue`.
|
|
317
350
|
*
|
|
318
|
-
* Public (not `#private`) as a deterministic
|
|
319
|
-
*
|
|
320
|
-
* MutationObserver
|
|
321
|
-
*
|
|
322
|
-
* NON_ACTION_ALLOWLIST (it is not a user-wired action).
|
|
351
|
+
* Public (not `#private`) as a deterministic seam: enforcement normally runs
|
|
352
|
+
* from `itemTargetConnected`, a Stimulus callback delivered through a
|
|
353
|
+
* MutationObserver, which a DOM-only environment does not reliably fire — so
|
|
354
|
+
* it can also be invoked directly. It is not a user-wired action.
|
|
323
355
|
*/
|
|
324
356
|
enforceMaxLimit() {
|
|
325
357
|
const currentItems = this.itemTargets;
|
|
@@ -377,13 +409,7 @@ var ToastController = class extends Controller {
|
|
|
377
409
|
this.#rafHandles.delete(element);
|
|
378
410
|
}
|
|
379
411
|
};
|
|
380
|
-
function cssTimeToMs(value) {
|
|
381
|
-
const first = value.split(",")[0]?.trim() ?? "";
|
|
382
|
-
const amount = Number.parseFloat(first);
|
|
383
|
-
if (Number.isNaN(amount)) return 0;
|
|
384
|
-
return first.endsWith("ms") ? amount : amount * 1e3;
|
|
385
|
-
}
|
|
386
412
|
|
|
387
|
-
export { ToastController
|
|
413
|
+
export { ToastController };
|
|
388
414
|
//# sourceMappingURL=toast_controller.js.map
|
|
389
415
|
//# sourceMappingURL=toast_controller.js.map
|