stimeo-ui 0.2.1 → 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 +104 -0
- package/dist/cable/index.d.ts +16 -16
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.js +10 -0
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- 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 +16 -1
- package/dist/controllers/carousel_controller.js +47 -6
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.js +2 -2
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +12 -1
- package/dist/controllers/color_picker_controller.js +46 -7
- package/dist/controllers/color_picker_controller.js.map +1 -1
- 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.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +19 -1
- package/dist/controllers/data_grid_controller.js +82 -4
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +1 -0
- package/dist/controllers/date_range_picker_controller.js +27 -3
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +1 -2
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.js +1 -0
- package/dist/controllers/editable_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.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 +36 -11
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +31 -10
- 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/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 +91 -11
- package/dist/controllers/menubar_controller.js +415 -63
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +31 -5
- package/dist/controllers/multi_select_controller.js +312 -29
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +65 -18
- package/dist/controllers/navigation_menu_controller.js +154 -27
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- 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 +3 -2
- package/dist/controllers/overflow_indicator_controller.js +81 -13
- 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/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 +14 -4
- package/dist/controllers/range_slider_controller.js +32 -6
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.js +16 -2
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.js +63 -19
- package/dist/controllers/read_more_controller.js.map +1 -1
- 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 +86 -12
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +1 -1
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
- package/dist/controllers/scroll_visibility_controller.js +33 -3
- 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 +346 -73
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +20 -5
- package/dist/controllers/separator_controller.js +9 -0
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +1 -1
- package/dist/controllers/sidebar_controller.js.map +1 -1
- 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/stick_to_bottom_controller.d.ts +5 -6
- package/dist/controllers/stick_to_bottom_controller.js +2 -1
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.js +32 -11
- 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 +11 -3
- package/dist/controllers/tabs_controller.js +26 -3
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.js +22 -2
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +1 -1
- package/dist/controllers/theme_controller.js +22 -3
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js +20 -1
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +4 -5
- package/dist/controllers/toast_controller.js +4 -5
- 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.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +87 -6
- package/dist/controllers/tree_view_controller.js +467 -51
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +3514 -689
- 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 +24 -24
- package/dist/inspector/manifest.json +1040 -125
- package/package.json +5 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/layout_observer.ts","../../src/controllers/breadcrumb_controller.ts"],"names":[],"mappings":"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","file":"breadcrumb_controller.js","sourcesContent":["/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Layout-sensitive widgets (sliders, resizable panes, scroll spies, popovers)\n * need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Headless, accessible responsive breadcrumb behavior.\n *\n * Markup contract (identifier: `stimeo--breadcrumb`):\n * <nav data-controller=\"stimeo--breadcrumb\" aria-label=\"Breadcrumb\">\n * <ol data-stimeo--breadcrumb-target=\"list\">\n * <li><a href=\"/\">Home</a></li>\n * <li data-stimeo--breadcrumb-target=\"ellipsis\" hidden>\n * <button type=\"button\" aria-expanded=\"false\" aria-controls=\"bc-collapsed\"\n * aria-label=\"Show full path\"\n * data-stimeo--breadcrumb-target=\"trigger\"\n * data-action=\"stimeo--breadcrumb#toggle\">…</button>\n * </li>\n * <li id=\"bc-collapsed\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a\">Section A</a></li>\n * <li data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a/b\">Sub B</a></li>\n * <li><a href=\"/a/b/c\" aria-current=\"page\">Item C</a></li>\n * </ol>\n * </nav>\n *\n * Implements the WAI-ARIA APG **Breadcrumb** pattern. The base structure (`nav`\n * + `ol` + `aria-current=\"page\"`) lives in the markup; this controller adds the\n * responsive behavior: when the trail overflows its container it collapses the\n * author-marked middle items behind a disclosure (`…`) button, which expands\n * them back on demand.\n *\n * @remarks\n * Behavior only — the consumer owns separators (CSS) and the look. The items to\n * collapse are the author-marked `collapsible` targets (the source of truth),\n * placed between the leading and trailing items that must always stay visible.\n *\n * Behavior provided:\n * - Detects overflow via {@link LayoutObserver} (element + viewport resize).\n * - While overflowing and not expanded, hides the `collapsible` items and shows\n * the `ellipsis` item; when it fits, shows everything and hides the ellipsis.\n * - The disclosure `trigger` toggles `aria-expanded` and the collapsed items,\n * dispatching `stimeo--breadcrumb:toggle`.\n */\nexport class BreadcrumbController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"collapsible\", \"ellipsis\", \"trigger\"];\n static actions = [\"toggle\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly collapsibleTargets: HTMLElement[];\n declare readonly ellipsisTarget: HTMLElement;\n declare readonly triggerTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasEllipsisTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n /** Whether the trail currently overflows its container. */\n #overflowing = false;\n /** Whether the user has expanded the collapsed items via the disclosure. */\n #expanded = false;\n\n readonly #layout = new LayoutObserver(() => this.#update());\n\n /** Starts observing for overflow and renders the initial state. */\n override connect(): void {\n if (this.hasListTarget) this.#layout.observe(this.listTarget);\n this.#layout.observeViewport();\n this.#update();\n }\n\n /** Releases the resize observation (Turbo navigation included). */\n override disconnect(): void {\n this.#layout.disconnect();\n }\n\n /** Expands or re-collapses the trail and dispatches `toggle`. */\n toggle(): void {\n this.#expanded = !this.#expanded;\n this.#render();\n this.dispatch(\"toggle\", { detail: { expanded: this.#expanded } });\n }\n\n /** Re-measures overflow and re-renders (e.g. on resize). */\n #update(): void {\n this.#overflowing = this.#measureOverflow();\n if (!this.#overflowing) this.#expanded = false;\n this.#render();\n }\n\n /**\n * Measures overflow against the **fully expanded** layout so hiding items does\n * not make the condition oscillate. Reveals every item, then compares the list's\n * own scroll width to its own client width; `#render` immediately re-applies\n * the correct hidden state afterward.\n *\n * Both widths are read from the list element itself (not the host `nav`) so the\n * check is independent of any padding/border on the host — comparing against the\n * host's `clientWidth` would over-report the available space by its padding and\n * miss real overflow in a padded container.\n */\n #measureOverflow(): boolean {\n if (!this.hasListTarget) return false;\n for (const item of this.collapsibleTargets) item.hidden = false;\n if (this.hasEllipsisTarget) this.ellipsisTarget.hidden = true;\n return this.listTarget.scrollWidth > this.listTarget.clientWidth;\n }\n\n /** Applies the collapsed/expanded state to the items, ellipsis, and trigger. */\n #render(): void {\n const collapsed = this.#overflowing && !this.#expanded;\n for (const item of this.collapsibleTargets) item.hidden = collapsed;\n if (this.hasEllipsisTarget) this.ellipsisTarget.hidden = !this.#overflowing;\n if (this.hasTriggerTarget) {\n this.triggerTarget.setAttribute(\"aria-expanded\", this.#expanded ? \"true\" : \"false\");\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/blur_deferral.ts","../../src/utils/layout_observer.ts","../../src/controllers/breadcrumb_controller.ts"],"names":[],"mappings":"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","file":"breadcrumb_controller.js","sourcesContent":["/**\n * Shared bookkeeping for updates that must wait until an element loses focus.\n *\n * A control that becomes redundant while the user is standing on it cannot be\n * removed from the page immediately: hiding a focused element (or turning it\n * into a native `disabled` one) drops focus to `<body>`, stranding a keyboard\n * user mid-widget. The fix every affected controller reached for is the same —\n * keep the element reachable, hold the destructive update, listen for `blur`,\n * and apply it then.\n *\n * {@link BlurDeferral} owns *only* the registry part of that: which elements are\n * currently holding an update back, attaching and detaching the `blur` listener,\n * and guaranteeing teardown. It is deliberately **policy-free** — what the\n * interim state looks like (`aria-disabled`, an ownership marker, nothing at\n * all), and what the deferred update actually is, stay in the controller, which\n * receives the element back through its release callback.\n *\n * Three contracts are worth stating up front, because each one is a hazard the\n * consumers would otherwise hit:\n *\n * - **The release callback fires only on a real `blur`.** `release()` /\n * `releaseAll()` *cancel* a deferral; they do not complete it. Conflating the\n * two makes `overflow-indicator` disable a button that just left the boundary.\n * - **`elements` is a snapshot.** Iterating the live key set while releasing\n * inside the loop is the failure this registry exists to prevent.\n * - **The entry is detached before the callback runs**, so a controller that\n * decides the update is still unsafe can simply defer again.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and contracts belong on the members.\n *\n * @example\n * ```ts\n * readonly #deferred = new BlurDeferral<HTMLElement>((trigger) => {\n * if (this.#connected) this.#evaluate();\n * });\n *\n * #hide(trigger: HTMLElement) {\n * if (document.activeElement === trigger) {\n * this.#deferred.deferOnly(trigger); // stays visible until blur\n * return;\n * }\n * this.#deferred.releaseAll();\n * trigger.hidden = true;\n * }\n *\n * disconnect() {\n * this.#deferred.releaseAll();\n * }\n * ```\n */\nexport class BlurDeferral<T extends HTMLElement = HTMLElement> {\n /** Elements currently holding an update back, mapped to their `blur` listener. */\n readonly #pending = new Map<T, () => void>();\n /** Called after a pending element blurs and has been detached. */\n readonly #onRelease: (element: T) => void;\n\n /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */\n constructor(onRelease: (element: T) => void) {\n this.#onRelease = onRelease;\n }\n\n /** Number of elements currently holding an update back. */\n get size(): number {\n return this.#pending.size;\n }\n\n /** Snapshot of the pending elements, safe to iterate while releasing them. */\n get elements(): T[] {\n return [...this.#pending.keys()];\n }\n\n /** Whether `element` is currently holding an update back. */\n has(element: T): boolean {\n return this.#pending.has(element);\n }\n\n /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */\n defer(element: T): void {\n if (this.#pending.has(element)) return;\n const onBlur = (): void => {\n this.#detach(element);\n this.#onRelease(element);\n };\n this.#pending.set(element, onBlur);\n element.addEventListener(\"blur\", onBlur);\n }\n\n /** Defers `element` as the only pending entry, cancelling any others. */\n deferOnly(element: T): void {\n for (const pending of this.elements) {\n if (pending !== element) this.#detach(pending);\n }\n this.defer(element);\n }\n\n /** Cancels `element`'s deferral without completing it; no-ops when not pending. */\n release(element: T): void {\n this.#detach(element);\n }\n\n /** Cancels every deferral without completing any of them. */\n releaseAll(): void {\n for (const element of this.elements) this.#detach(element);\n }\n\n /** Removes the `blur` listener for `element` and forgets it. */\n #detach(element: T): void {\n const onBlur = this.#pending.get(element);\n if (onBlur) element.removeEventListener(\"blur\", onBlur);\n this.#pending.delete(element);\n }\n}\n","/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Layout-sensitive widgets (sliders, resizable panes, scroll spies, popovers)\n * need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BlurDeferral } from \"../utils/blur_deferral\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Sub-pixel tolerance (px) applied to the overflow comparison.\n *\n * Fractional layout metrics (zoom, `border-box` rounding, fractional font\n * metrics) routinely make `scrollWidth` exceed `clientWidth` by a fraction of a\n * pixel on a trail that visually fits, which would collapse it for no reason.\n * Same constant and rationale as `scroll_area_controller.ts`'s `EDGE_EPSILON`.\n */\nconst OVERFLOW_EPSILON = 1;\n\n/**\n * Headless, accessible responsive breadcrumb behavior.\n *\n * Markup contract (identifier: `stimeo--breadcrumb`):\n * <nav data-controller=\"stimeo--breadcrumb\" aria-label=\"Breadcrumb\">\n * <ol data-stimeo--breadcrumb-target=\"list\">\n * <li><a href=\"/\">Home</a></li>\n * <li data-stimeo--breadcrumb-target=\"ellipsis\" hidden>\n * <button type=\"button\" aria-expanded=\"false\" aria-controls=\"bc-a bc-b\"\n * aria-label=\"Show full path\"\n * data-stimeo--breadcrumb-target=\"trigger\"\n * data-action=\"click->stimeo--breadcrumb#toggle\">…</button>\n * </li>\n * <li id=\"bc-a\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a\">Section A</a></li>\n * <li id=\"bc-b\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a/b\">Sub B</a></li>\n * <li><a href=\"/a/b/c\" aria-current=\"page\">Item C</a></li>\n * </ol>\n * </nav>\n *\n * Implements the WAI-ARIA APG **Breadcrumb** pattern. The base structure (`nav`\n * + `ol` + `aria-current=\"page\"`) lives in the markup; this controller adds the\n * responsive behavior: when the trail overflows its container it collapses the\n * author-marked middle items behind a disclosure (`…`) button, which expands\n * them back on demand.\n *\n * @remarks\n * Behavior only — the consumer owns separators (CSS) and the look. The items to\n * collapse are the author-marked `collapsible` targets (the source of truth),\n * placed between the leading and trailing items that must always stay visible.\n *\n * Behavior provided:\n * - Detects overflow via {@link LayoutObserver} (element + viewport resize) and a\n * `MutationObserver` on the list (content edits, Turbo Stream / morph item\n * swaps); the public `update` action re-measures on demand.\n * - While overflowing and not expanded, hides the `collapsible` items and shows\n * the `ellipsis` item; when it fits, shows everything, hides the ellipsis and\n * resets the expanded state (so a later collapse starts collapsed).\n * - The disclosure `trigger` toggles `aria-expanded` and the collapsed items,\n * dispatching `stimeo--breadcrumb:toggle`. `toggle` is a no-op (no state\n * change, no event) while the trail fits, so a hidden trigger can never be\n * left reporting `aria-expanded=\"true\"`.\n *\n * Layout requirements the behavior depends on:\n * - **The list must not wrap** (`white-space: nowrap` / `flex-wrap: nowrap`). A\n * wrapping list grows in height instead of scroll width, so\n * `scrollWidth === clientWidth` always holds and overflow is never detected.\n * - **The list width must follow its container** (no shrink-to-fit / intrinsic\n * width). Overflow is re-measured synchronously inside the resize\n * notification, so a list that widens to its content would never report\n * overflow. There is deliberately no debounce: the pass is one write → one\n * forced read → one write on a single element.\n * - Consumers must not neutralize `[hidden]` (e.g. `display: inline-flex` on the\n * items wins over the UA's `[hidden] { display: none }`); collapsing is\n * expressed exclusively through the `hidden` attribute.\n *\n * Ownership of `hidden`: this controller owns the attribute on an element **for as\n * long as that element is a `collapsible` target and the controller is connected**.\n * Outside that window it does not write it:\n * - lose the marker while connected → the element becomes an always-visible item\n * and the controller clears the `hidden` it owned;\n * - controller `disconnect()` → the collapsed state is left in the DOM on purpose,\n * because that DOM is what Turbo caches and restores.\n *\n * A consumer that also wants to own `hidden` on the same element (for a reason of\n * its own) is not supported: `#measureOverflow` clears it on every measurement to\n * read the expanded width.\n *\n * Focus safety: hiding the element the user is standing on would drop focus to\n * `<body>`. When a collapse would hide the focused item, focus moves to the\n * disclosure `trigger` first; when the ellipsis would be hidden while it holds\n * focus, the hide waits for `blur`.\n *\n * **Fail-safe when the disclosure is incomplete.** Collapsing needs all of\n * `list`, one `collapsible`, `ellipsis`, and `trigger`; miss any and the trail\n * degrades to a plain APG breadcrumb (every item visible, ellipsis hidden,\n * `aria-expanded=\"false\"`, `toggle` a silent no-op). Hiding the middle items\n * with no control that can reveal them puts the path out of reach for good — a\n * too-wide trail is the cheaper failure. Watched at runtime, so a swap that\n * breaks the set releases the items and one that completes it starts collapsing.\n */\nexport class BreadcrumbController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"collapsible\", \"ellipsis\", \"trigger\"];\n static actions = [\"toggle\", \"update\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly collapsibleTargets: HTMLElement[];\n declare readonly ellipsisTarget: HTMLElement;\n declare readonly triggerTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasEllipsisTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n /** Guards target callbacks that can fire outside the connected window. */\n #connected = false;\n /** Whether the trail currently overflows its container. */\n #overflowing = false;\n /** Whether the user has expanded the collapsed items via the disclosure. */\n #expanded = false;\n /** The list element currently observed (resize + mutations), if any. */\n #observedList: HTMLElement | null = null;\n #listMutations: MutationObserver | null = null;\n readonly #layout = new LayoutObserver(() => this.update());\n\n readonly #onListMutation = (): void => {\n this.update();\n };\n\n /** Holds the ellipsis hide back while it contains focus; re-renders on blur. */\n readonly #deferredHide = new BlurDeferral(() => {\n this.#render();\n });\n\n /** Starts observing for overflow and renders the initial state. */\n override connect(): void {\n this.#connected = true;\n // The DOM is the source of truth on reconnect (Turbo cache restore / morph):\n // the trigger's `aria-expanded` is where the expanded state lives, so a trail\n // the user opened survives a back-navigation instead of silently re-collapsing.\n this.#expanded = this.#initialExpanded();\n this.#layout.observeViewport();\n this.#syncListObservation();\n this.update();\n }\n\n /** Releases the resize/mutation observation (Turbo navigation included). */\n override disconnect(): void {\n this.#connected = false;\n this.#deferredHide.releaseAll();\n this.#stopObservingList();\n this.#layout.disconnect();\n }\n\n listTargetConnected(): void {\n this.#resync();\n }\n\n listTargetDisconnected(): void {\n this.#resync();\n }\n\n collapsibleTargetConnected(): void {\n this.#resync();\n }\n\n /**\n * Re-measures when the disclosure set gains or loses a member. The set is a\n * precondition for collapsing at all, so it needs the same watching the list\n * and items get: without these callbacks a swap that breaks or completes the\n * set would only take effect at the next resize or list mutation, which may\n * never come.\n */\n ellipsisTargetConnected(): void {\n this.#resync();\n }\n\n ellipsisTargetDisconnected(): void {\n this.#resync();\n }\n\n triggerTargetConnected(): void {\n this.#resync();\n }\n\n triggerTargetDisconnected(): void {\n this.#resync();\n }\n\n /**\n * Hands `hidden` back when an element stops being a `collapsible` target.\n *\n * The marker is the source of truth for what may be collapsed, so an element\n * that loses it while the controller is live is an always-visible item again —\n * and `#render` only walks the *current* targets, so nothing else would ever\n * clear the `hidden` this controller put there.\n *\n * The `#connected` guard is load-bearing, not defensive. Stimulus fires this\n * callback for **every** target during teardown as well (`Context.disconnect()`\n * stops the target observer after `disconnect()` has run), and there the\n * collapsed DOM must survive untouched so Turbo's cache restores the trail in\n * the state the user left it (see the ownership note in {@link BreadcrumbController}).\n *\n * @param element - the element that just left the `collapsible` target set\n */\n collapsibleTargetDisconnected(element: HTMLElement): void {\n if (this.#connected) element.hidden = false;\n this.#resync();\n }\n\n /**\n * Expands or re-collapses the trail and dispatches `toggle`.\n *\n * No-op while the trail fits: there is nothing collapsed to reveal, and the\n * (hidden) trigger would otherwise keep a stale `aria-expanded=\"true\"` that\n * surfaces pre-expanded the next time the trail overflows. No event is\n * dispatched in that case.\n */\n toggle(): void {\n if (!this.#connected || !this.#overflowing) return;\n this.#expanded = !this.#expanded;\n this.#render();\n this.dispatch(\"toggle\", { detail: { expanded: this.#expanded } });\n }\n\n /**\n * Re-measures overflow and re-renders. Wired to resizes and list mutations;\n * exposed as an action so consumers can re-measure after a change the\n * observers cannot see (e.g. a web font swap that only alters text width).\n */\n update(): void {\n if (!this.#connected) return;\n this.#overflowing = this.#measureOverflow();\n if (!this.#overflowing) this.#expanded = false;\n this.#render();\n }\n\n /** Re-attaches the list observation (targets may have been swapped) and re-measures. */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncListObservation();\n this.update();\n }\n\n /** Reads the expanded state back from its DOM home (the trigger's `aria-expanded`). */\n #initialExpanded(): boolean {\n return this.hasTriggerTarget && this.triggerTarget.getAttribute(\"aria-expanded\") === \"true\";\n }\n\n /**\n * Points the resize + mutation observation at the current `list` target.\n *\n * The list is re-resolved rather than captured at connect so a target swapped\n * at runtime (Turbo Stream replacement of the `<ol>`) is observed too. Only\n * `childList` / `subtree` / `characterData` are watched — deliberately **not**\n * `attributes`, which the controller's own `hidden` writes would re-trigger.\n */\n #syncListObservation(): void {\n const next = this.hasListTarget ? this.listTarget : null;\n if (next === this.#observedList) return;\n\n this.#stopObservingList();\n if (!next) return;\n\n this.#observedList = next;\n this.#layout.observe(next);\n if (typeof MutationObserver !== \"undefined\") {\n this.#listMutations = new MutationObserver(this.#onListMutation);\n this.#listMutations.observe(next, { childList: true, subtree: true, characterData: true });\n }\n }\n\n #stopObservingList(): void {\n if (this.#observedList) this.#layout.unobserve(this.#observedList);\n this.#observedList = null;\n this.#listMutations?.disconnect();\n this.#listMutations = null;\n }\n\n /**\n * Measures overflow against the **fully expanded** layout so hiding items does\n * not make the condition oscillate. Reveals every item, then compares the list's\n * own scroll width to its own client width; `#render` immediately re-applies\n * the correct hidden state afterward.\n *\n * Both widths are read from the list element itself (not the host `nav`) so the\n * check is independent of any padding/border on the host — comparing against the\n * host's `clientWidth` would over-report the available space by its padding and\n * miss real overflow in a padded container.\n *\n * With no `collapsible` items there is nothing to collapse, so the trail is\n * never reported as overflowing (which also keeps the disclosure out of the\n * tab order — a button controlling nothing).\n *\n * The full disclosure set is a precondition: collapsing without an `ellipsis`\n * *and* a `trigger` would hide items behind a control that does not exist, so\n * an incomplete set reports \"not overflowing\" and `#render` degrades the trail\n * to a plain breadcrumb. Losing layout is recoverable; losing the path is not.\n */\n #measureOverflow(): boolean {\n if (!this.hasListTarget || this.collapsibleTargets.length === 0) return false;\n if (!this.hasEllipsisTarget || !this.hasTriggerTarget) return false;\n for (const item of this.collapsibleTargets) item.hidden = false;\n // No `hasEllipsisTarget` check: the line above already returned without one.\n this.ellipsisTarget.hidden = true;\n return this.listTarget.scrollWidth > this.listTarget.clientWidth + OVERFLOW_EPSILON;\n }\n\n /** Applies the collapsed/expanded state to the items, ellipsis, and trigger. */\n #render(): void {\n const collapsed = this.#overflowing && !this.#expanded;\n const showEllipsis = this.#overflowing && this.collapsibleTargets.length > 0;\n\n // Decided *before* the writes below: once a focused item is hidden the browser\n // drops focus to <body> and the information is gone (same reasoning as\n // `overflow_menu_controller`'s pre-move focus decision).\n const rescueFocus =\n collapsed &&\n this.hasTriggerTarget &&\n this.collapsibleTargets.some((i) => this.#holdsFocus(i));\n\n for (const item of this.collapsibleTargets) item.hidden = collapsed;\n if (this.hasEllipsisTarget) this.#applyEllipsisVisibility(showEllipsis);\n if (this.hasTriggerTarget) {\n // Never report expanded while the trail fits: the trigger is hidden then.\n const expanded = this.#overflowing && this.#expanded;\n this.triggerTarget.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n }\n if (rescueFocus) this.triggerTarget.focus();\n }\n\n /**\n * Shows or hides the ellipsis item, deferring a hide that would blur the user.\n *\n * Hiding the element that currently holds focus drops focus to `<body>`, so the\n * hide waits for that element's `blur` and is re-applied then (the shared\n * `BlurDeferral` registry).\n */\n #applyEllipsisVisibility(show: boolean): void {\n const ellipsis = this.ellipsisTarget;\n if (show) {\n this.#deferredHide.releaseAll();\n ellipsis.hidden = false;\n return;\n }\n\n const active = document.activeElement;\n if (active instanceof HTMLElement && ellipsis.contains(active)) {\n ellipsis.hidden = false;\n this.#deferredHide.deferOnly(active);\n return;\n }\n\n this.#deferredHide.releaseAll();\n ellipsis.hidden = true;\n }\n\n /** Whether `element` contains (or is) the currently focused element. */\n #holdsFocus(element: HTMLElement): boolean {\n const active = document.activeElement;\n return active instanceof HTMLElement && element.contains(active);\n }\n}\n"]}
|
|
@@ -92,7 +92,17 @@ declare class CalendarController extends Controller<HTMLElement> {
|
|
|
92
92
|
selectByClick(event: MouseEvent): void;
|
|
93
93
|
/** Handles grid cell keyboard navigation and triggers selection. */
|
|
94
94
|
onKeydown(event: KeyboardEvent): void;
|
|
95
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* Renders the grid days and updates the month/year label, then reports the
|
|
97
|
+
* painted month once it differs from the one already announced.
|
|
98
|
+
*
|
|
99
|
+
* The report belongs to the paint, not to the `month` Value: the Value is only
|
|
100
|
+
* one of the things that decide the month on screen. While a malformed `month`
|
|
101
|
+
* falls back, changing `selected` and selecting a neighbouring month's cell
|
|
102
|
+
* move the painted month too, and both repaint from here. Reading the report
|
|
103
|
+
* off the paint keeps the event naming the month the grid shows, in the
|
|
104
|
+
* `YYYY-MM` the detail contract promises, whichever route repainted.
|
|
105
|
+
*/
|
|
96
106
|
render(): void;
|
|
97
107
|
selectDayElement(dayElement: HTMLElement): void;
|
|
98
108
|
}
|
|
@@ -2,6 +2,22 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/calendar_controller.ts
|
|
4
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
|
+
|
|
5
21
|
// src/utils/dates.ts
|
|
6
22
|
function toISODateString(date) {
|
|
7
23
|
const year = date.getFullYear();
|
|
@@ -32,6 +48,9 @@ function parseISOMonthString(monthStr) {
|
|
|
32
48
|
if (month < 1 || month > 12) return null;
|
|
33
49
|
return { year: Number(y), month };
|
|
34
50
|
}
|
|
51
|
+
function toISOMonthString(date) {
|
|
52
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
53
|
+
}
|
|
35
54
|
|
|
36
55
|
// src/utils/safe_timeout.ts
|
|
37
56
|
var TimerRegistry = class {
|
|
@@ -87,6 +106,7 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
87
106
|
};
|
|
88
107
|
|
|
89
108
|
// src/controllers/calendar_controller.ts
|
|
109
|
+
var OWNED_DISABLED = "data-stimeo--calendar-owns-disabled";
|
|
90
110
|
var CalendarController = class extends Controller {
|
|
91
111
|
static targets = ["label", "grid", "day"];
|
|
92
112
|
static values = {
|
|
@@ -107,6 +127,13 @@ var CalendarController = class extends Controller {
|
|
|
107
127
|
* controller never steals focus after the element leaves the DOM (Turbo).
|
|
108
128
|
*/
|
|
109
129
|
#focusTimer = new SafeTimeout();
|
|
130
|
+
/**
|
|
131
|
+
* The painted month the last `monthchange` reported, or `null` before the first
|
|
132
|
+
* paint. Settling on the initial month — whether it comes from the attribute or
|
|
133
|
+
* is derived here — is the grid describing itself, not a navigation, so it must
|
|
134
|
+
* not reach a listener that refetches inventory or pushes history.
|
|
135
|
+
*/
|
|
136
|
+
#announcedMonth = null;
|
|
110
137
|
connect() {
|
|
111
138
|
if (!this.monthValue) {
|
|
112
139
|
const today = /* @__PURE__ */ new Date();
|
|
@@ -129,7 +156,6 @@ var CalendarController = class extends Controller {
|
|
|
129
156
|
if (!this.monthValue) return;
|
|
130
157
|
this.#syncFocusedDateWithMonth();
|
|
131
158
|
this.render();
|
|
132
|
-
this.dispatch("monthchange", { detail: { month: this.monthValue } });
|
|
133
159
|
}
|
|
134
160
|
/**
|
|
135
161
|
* Stimulus lifecycle callback triggered automatically when the selectedValue changes.
|
|
@@ -156,25 +182,25 @@ var CalendarController = class extends Controller {
|
|
|
156
182
|
}
|
|
157
183
|
/** Handles day selection when a gridcell is clicked. */
|
|
158
184
|
selectByClick(event) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
185
|
+
const dayElement = event.target?.closest(
|
|
186
|
+
"[data-stimeo--calendar-target='day']"
|
|
187
|
+
);
|
|
162
188
|
if (!dayElement) return;
|
|
163
189
|
this.selectDayElement(dayElement);
|
|
164
190
|
}
|
|
165
191
|
/** Handles grid cell keyboard navigation and triggers selection. */
|
|
166
192
|
onKeydown(event) {
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
const dayElement = target
|
|
193
|
+
if (event.defaultPrevented) return;
|
|
194
|
+
if (isReservedArrowChord(event)) return;
|
|
195
|
+
const dayElement = event.target?.closest(
|
|
196
|
+
"[data-stimeo--calendar-target='day']"
|
|
197
|
+
);
|
|
170
198
|
if (!dayElement) return;
|
|
171
|
-
const
|
|
172
|
-
if (!dateStr) return;
|
|
173
|
-
const date = parseISODateString(dateStr);
|
|
199
|
+
const date = parseISODateString(dayElement.getAttribute("data-date") ?? "");
|
|
174
200
|
if (!date) return;
|
|
175
201
|
let handled = true;
|
|
176
202
|
let nextDate = new Date(date);
|
|
177
|
-
switch (event.key) {
|
|
203
|
+
switch (logicalArrowKey(event.key, this.element)) {
|
|
178
204
|
case "ArrowLeft":
|
|
179
205
|
nextDate.setDate(nextDate.getDate() - 1);
|
|
180
206
|
break;
|
|
@@ -209,6 +235,10 @@ var CalendarController = class extends Controller {
|
|
|
209
235
|
break;
|
|
210
236
|
case "t":
|
|
211
237
|
case "T": {
|
|
238
|
+
if (event.ctrlKey || event.metaKey || event.altKey) {
|
|
239
|
+
handled = false;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
212
242
|
const now = /* @__PURE__ */ new Date();
|
|
213
243
|
nextDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
214
244
|
break;
|
|
@@ -227,25 +257,50 @@ var CalendarController = class extends Controller {
|
|
|
227
257
|
this.#focusAndNavigateToDate(nextDate);
|
|
228
258
|
}
|
|
229
259
|
}
|
|
230
|
-
/**
|
|
231
|
-
|
|
260
|
+
/**
|
|
261
|
+
* First day of the month the grid paints: the `month` Value when it parses,
|
|
262
|
+
* otherwise the focused date's month.
|
|
263
|
+
*
|
|
264
|
+
* A malformed `month` falls back instead of stopping the paint: every cell
|
|
265
|
+
* keeps its `aria-selected` and the grid keeps its tab stop, so a Value typo
|
|
266
|
+
* still leaves the grid reachable by Tab and the author can see what they
|
|
267
|
+
* typed. Every consumer of "which month is on screen" reads it from here, so
|
|
268
|
+
* the paint and the `monthchange` report cannot name different months.
|
|
269
|
+
*/
|
|
270
|
+
#paintedMonthStart() {
|
|
232
271
|
const monthInfo = parseISOMonthString(this.monthValue);
|
|
233
|
-
|
|
234
|
-
|
|
272
|
+
return monthInfo ? new Date(monthInfo.year, monthInfo.month - 1, 1) : new Date(this.focusedDate.getFullYear(), this.focusedDate.getMonth(), 1);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Renders the grid days and updates the month/year label, then reports the
|
|
276
|
+
* painted month once it differs from the one already announced.
|
|
277
|
+
*
|
|
278
|
+
* The report belongs to the paint, not to the `month` Value: the Value is only
|
|
279
|
+
* one of the things that decide the month on screen. While a malformed `month`
|
|
280
|
+
* falls back, changing `selected` and selecting a neighbouring month's cell
|
|
281
|
+
* move the painted month too, and both repaint from here. Reading the report
|
|
282
|
+
* off the paint keeps the event naming the month the grid shows, in the
|
|
283
|
+
* `YYYY-MM` the detail contract promises, whichever route repainted.
|
|
284
|
+
*/
|
|
285
|
+
render() {
|
|
286
|
+
const monthStart = this.#paintedMonthStart();
|
|
287
|
+
const year = monthStart.getFullYear();
|
|
288
|
+
const month = monthStart.getMonth() + 1;
|
|
235
289
|
if (this.hasLabelTarget) {
|
|
236
290
|
const lang = document.documentElement.lang || "en";
|
|
237
|
-
const labelDate = new Date(year, month - 1, 1);
|
|
238
291
|
const formatter = new Intl.DateTimeFormat(lang, { month: "long", year: "numeric" });
|
|
239
|
-
this.labelTarget.textContent = formatter.format(
|
|
292
|
+
this.labelTarget.textContent = formatter.format(monthStart);
|
|
240
293
|
}
|
|
241
294
|
const days = this.#calculateGridDays(year, month);
|
|
242
295
|
const dayElements = this.dayTargets;
|
|
243
|
-
for (
|
|
244
|
-
const el = dayElements[
|
|
296
|
+
for (const [index, date] of days.entries()) {
|
|
297
|
+
const el = dayElements[index];
|
|
245
298
|
if (!el) continue;
|
|
246
|
-
const date = days[i];
|
|
247
|
-
if (!date) continue;
|
|
248
299
|
const dateStr = toISODateString(date);
|
|
300
|
+
if (el.getAttribute("data-date") !== dateStr) {
|
|
301
|
+
el.removeAttribute("aria-disabled");
|
|
302
|
+
el.removeAttribute(OWNED_DISABLED);
|
|
303
|
+
}
|
|
249
304
|
el.setAttribute("data-date", dateStr);
|
|
250
305
|
el.textContent = String(date.getDate());
|
|
251
306
|
const isOutside = date.getFullYear() !== year || date.getMonth() !== month - 1;
|
|
@@ -258,11 +313,23 @@ var CalendarController = class extends Controller {
|
|
|
258
313
|
el.setAttribute("tabindex", isFocused ? "0" : "-1");
|
|
259
314
|
const isDisabled = this.#isDateOutOfBounds(dateStr);
|
|
260
315
|
if (isDisabled) {
|
|
316
|
+
if (!el.hasAttribute("aria-disabled")) el.setAttribute(OWNED_DISABLED, "");
|
|
261
317
|
el.setAttribute("aria-disabled", "true");
|
|
262
|
-
} else {
|
|
318
|
+
} else if (el.hasAttribute(OWNED_DISABLED)) {
|
|
263
319
|
el.removeAttribute("aria-disabled");
|
|
320
|
+
el.removeAttribute(OWNED_DISABLED);
|
|
264
321
|
}
|
|
265
322
|
}
|
|
323
|
+
if (!dayElements.some((el) => el.getAttribute("tabindex") === "0")) {
|
|
324
|
+
const fallback = dayElements.find((el) => el.getAttribute("data-outside") === "false") ?? dayElements[0];
|
|
325
|
+
fallback?.setAttribute("tabindex", "0");
|
|
326
|
+
}
|
|
327
|
+
const painted = toISOMonthString(monthStart);
|
|
328
|
+
const previous = this.#announcedMonth;
|
|
329
|
+
this.#announcedMonth = painted;
|
|
330
|
+
if (previous !== null && previous !== painted) {
|
|
331
|
+
this.dispatch("monthchange", { detail: { month: painted } });
|
|
332
|
+
}
|
|
266
333
|
}
|
|
267
334
|
selectDayElement(dayElement) {
|
|
268
335
|
if (dayElement.getAttribute("aria-disabled") === "true") return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/dates.ts","../../src/utils/safe_timeout.ts","../../src/controllers/calendar_controller.ts"],"names":[],"mappings":";;;;;AAWO,SAAS,gBAAgB,IAAA,EAAoB;AAClD,EAAA,MAAM,IAAA,GAAO,KAAK,WAAA,EAAY;AAC9B,EAAA,MAAM,KAAA,GAAQ,OAAO,IAAA,CAAK,QAAA,KAAa,CAAC,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAA;AACzD,EAAA,MAAM,GAAA,GAAM,OAAO,IAAA,CAAK,OAAA,EAAS,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAA;AAClD,EAAA,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,IAAI,GAAG,CAAA,CAAA;AAChC;AASO,SAAS,mBAAmB,OAAA,EAA8B;AAC/D,EAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AACrB,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,2BAA2B,CAAA;AACvD,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,GAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,KAAA;AACpB,EAAA,MAAM,IAAA,GAAO,OAAO,CAAC,CAAA;AACrB,EAAA,MAAM,KAAA,GAAQ,OAAO,CAAC,CAAA;AACtB,EAAA,MAAM,GAAA,GAAM,OAAO,CAAC,CAAA;AACpB,EAAA,MAAM,OAAO,IAAI,IAAA,CAAK,IAAA,EAAM,KAAA,GAAQ,GAAG,GAAG,CAAA;AAC1C,EAAA,IAAI,IAAA,CAAK,WAAA,EAAY,KAAM,IAAA,IAAQ,IAAA,CAAK,QAAA,EAAS,KAAM,KAAA,GAAQ,CAAA,IAAK,IAAA,CAAK,OAAA,EAAQ,KAAM,GAAA,EAAK;AAC1F,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAO,IAAA;AACT;AAMO,SAAS,oBAAoB,QAAA,EAA0D;AAC5F,EAAA,IAAI,CAAC,UAAU,OAAO,IAAA;AACtB,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,mBAAmB,CAAA;AAChD,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,GAAG,CAAA,EAAG,CAAC,CAAA,GAAI,KAAA;AACjB,EAAA,MAAM,KAAA,GAAQ,OAAO,CAAC,CAAA;AACtB,EAAA,IAAI,KAAA,GAAQ,CAAA,IAAK,KAAA,GAAQ,EAAA,EAAI,OAAO,IAAA;AACpC,EAAA,OAAO,EAAE,IAAA,EAAM,MAAA,CAAO,CAAC,GAAG,KAAA,EAAM;AAClC;;;AC5BA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;;;ACnEO,IAAM,kBAAA,GAAN,cAAiC,UAAA,CAAwB;AAAA,EAC9D,OAAgB,OAAA,GAAU,CAAC,OAAA,EAAS,QAAQ,KAAK,CAAA;AAAA,EACjD,OAAgB,MAAA,GAAS;AAAA,IACvB,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACnC,QAAA,EAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACtC,GAAA,EAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACjC,GAAA,EAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACjC,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA;AAAE;AAAA,GACxC;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,MAAA,EAAQ,WAAA,EAAa,QAAQ,eAAe,CAAA;AAAA,EAC9D,OAAO,MAAA,GAAS,CAAC,aAAA,EAAe,QAAQ,CAAA;AAAA;AAAA,EAexC,WAAA,uBAAwB,IAAA,EAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7B,WAAA,GAAc,IAAI,WAAA,EAAY;AAAA,EAErB,OAAA,GAAgB;AAEvB,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACpB,MAAA,MAAM,KAAA,uBAAY,IAAA,EAAK;AACvB,MAAA,MAAM,QAAA,GAAW,CAAA,EAAG,KAAA,CAAM,WAAA,EAAa,CAAA,CAAA,EAAI,MAAA,CAAO,KAAA,CAAM,QAAA,KAAa,CAAC,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAA;AACxF,MAAA,IAAA,CAAK,UAAA,GAAa,QAAA;AAClB,MAAA;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,sBAAA,EAAuB;AAC5B,IAAA,IAAA,CAAK,MAAA,EAAO;AAAA,EACd;AAAA;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,YAAY,QAAA,EAAS;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,iBAAA,GAA0B;AACxB,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,yBAAA,EAA0B;AAC/B,IAAA,IAAA,CAAK,MAAA,EAAO;AACZ,IAAA,IAAA,CAAK,QAAA,CAAS,eAAe,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,IAAA,CAAK,UAAA,EAAW,EAAG,CAAA;AAAA,EACrE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,oBAAA,GAA6B;AAC3B,IAAA,IAAI,KAAK,aAAA,EAAe;AACtB,MAAA,MAAM,QAAA,GAAW,kBAAA,CAAmB,IAAA,CAAK,aAAa,CAAA;AACtD,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA,IAAA,CAAK,WAAA,GAAc,QAAA;AAAA,MACrB;AAAA,IACF;AACA,IAAA,IAAA,CAAK,MAAA,EAAO;AAAA,EACd;AAAA;AAAA,EAGA,KAAK,KAAA,EAAqB;AACxB,IAAA,IAAI,KAAA,QAAa,cAAA,EAAe;AAChC,IAAA,IAAA,CAAK,YAAY,EAAE,CAAA;AAAA,EACrB;AAAA;AAAA,EAGA,KAAK,KAAA,EAAqB;AACxB,IAAA,IAAI,KAAA,QAAa,cAAA,EAAe;AAChC,IAAA,IAAA,CAAK,YAAY,CAAC,CAAA;AAAA,EACpB;AAAA;AAAA,EAGA,cAAc,KAAA,EAAyB;AACrC,IAAA,MAAM,SAAS,KAAA,CAAM,MAAA;AACrB,IAAA,IAAI,CAAC,MAAA,EAAQ;AACb,IAAA,MAAM,UAAA,GAAa,MAAA,CAAO,OAAA,CAAQ,sCAAsC,CAAA;AACxE,IAAA,IAAI,CAAC,UAAA,EAAY;AAEjB,IAAA,IAAA,CAAK,iBAAiB,UAAU,CAAA;AAAA,EAClC;AAAA;AAAA,EAGA,UAAU,KAAA,EAA4B;AACpC,IAAA,MAAM,SAAS,KAAA,CAAM,MAAA;AACrB,IAAA,IAAI,CAAC,MAAA,EAAQ;AACb,IAAA,MAAM,UAAA,GAAa,MAAA,CAAO,OAAA,CAAQ,sCAAsC,CAAA;AACxE,IAAA,IAAI,CAAC,UAAA,EAAY;AAEjB,IAAA,MAAM,OAAA,GAAU,UAAA,CAAW,YAAA,CAAa,WAAW,CAAA;AACnD,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,MAAM,IAAA,GAAO,mBAAmB,OAAO,CAAA;AACvC,IAAA,IAAI,CAAC,IAAA,EAAM;AAEX,IAAA,IAAI,OAAA,GAAU,IAAA;AACd,IAAA,IAAI,QAAA,GAAW,IAAI,IAAA,CAAK,IAAI,CAAA;AAE5B,IAAA,QAAQ,MAAM,GAAA;AAAK,MACjB,KAAK,WAAA;AACH,QAAA,QAAA,CAAS,OAAA,CAAQ,QAAA,CAAS,OAAA,EAAQ,GAAI,CAAC,CAAA;AACvC,QAAA;AAAA,MACF,KAAK,YAAA;AACH,QAAA,QAAA,CAAS,OAAA,CAAQ,QAAA,CAAS,OAAA,EAAQ,GAAI,CAAC,CAAA;AACvC,QAAA;AAAA,MACF,KAAK,SAAA;AACH,QAAA,QAAA,CAAS,OAAA,CAAQ,QAAA,CAAS,OAAA,EAAQ,GAAI,CAAC,CAAA;AACvC,QAAA;AAAA,MACF,KAAK,WAAA;AACH,QAAA,QAAA,CAAS,OAAA,CAAQ,QAAA,CAAS,OAAA,EAAQ,GAAI,CAAC,CAAA;AACvC,QAAA;AAAA,MACF,KAAK,QAAA;AACH,QAAA,IAAI,MAAM,QAAA,EAAU;AAClB,UAAA,QAAA,GAAW,IAAA,CAAK,yBAAA,CAA0B,IAAA,EAAM,EAAE,CAAA;AAAA,QACpD,CAAA,MAAO;AACL,UAAA,QAAA,GAAW,IAAA,CAAK,0BAAA,CAA2B,IAAA,EAAM,EAAE,CAAA;AAAA,QACrD;AACA,QAAA;AAAA,MACF,KAAK,UAAA;AACH,QAAA,IAAI,MAAM,QAAA,EAAU;AAClB,UAAA,QAAA,GAAW,IAAA,CAAK,yBAAA,CAA0B,IAAA,EAAM,CAAC,CAAA;AAAA,QACnD,CAAA,MAAO;AACL,UAAA,QAAA,GAAW,IAAA,CAAK,0BAAA,CAA2B,IAAA,EAAM,CAAC,CAAA;AAAA,QACpD;AACA,QAAA;AAAA,MACF,KAAK,MAAA;AACH,QAAA,QAAA,GAAW,IAAA,CAAK,oBAAoB,IAAI,CAAA;AACxC,QAAA;AAAA,MACF,KAAK,KAAA;AACH,QAAA,QAAA,GAAW,IAAA,CAAK,kBAAkB,IAAI,CAAA;AACtC,QAAA;AAAA,MACF,KAAK,GAAA;AAAA,MACL,KAAK,GAAA,EAAK;AACR,QAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,QAAA,QAAA,GAAW,IAAI,IAAA,CAAK,GAAA,CAAI,WAAA,EAAY,EAAG,IAAI,QAAA,EAAS,EAAG,GAAA,CAAI,OAAA,EAAS,CAAA;AACpE,QAAA;AAAA,MACF;AAAA,MACA,KAAK,OAAA;AAAA,MACL,KAAK,GAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,iBAAiB,UAAU,CAAA;AAChC,QAAA;AAAA,MACF;AACE,QAAA,OAAA,GAAU,KAAA;AACV,QAAA;AAAA;AAGJ,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,IAAA,CAAK,wBAAwB,QAAQ,CAAA;AAAA,IACvC;AAAA,EACF;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,MAAM,SAAA,GAAY,mBAAA,CAAoB,IAAA,CAAK,UAAU,CAAA;AACrD,IAAA,IAAI,CAAC,SAAA,EAAW;AAEhB,IAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,SAAA;AAGxB,IAAA,IAAI,KAAK,cAAA,EAAgB;AACvB,MAAA,MAAM,IAAA,GAAO,QAAA,CAAS,eAAA,CAAgB,IAAA,IAAQ,IAAA;AAC9C,MAAA,MAAM,YAAY,IAAI,IAAA,CAAK,IAAA,EAAM,KAAA,GAAQ,GAAG,CAAC,CAAA;AAC7C,MAAA,MAAM,SAAA,GAAY,IAAI,IAAA,CAAK,cAAA,CAAe,IAAA,EAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,SAAA,EAAW,CAAA;AAClF,MAAA,IAAA,CAAK,WAAA,CAAY,WAAA,GAAc,SAAA,CAAU,MAAA,CAAO,SAAS,CAAA;AAAA,IAC3D;AAEA,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,kBAAA,CAAmB,IAAA,EAAM,KAAK,CAAA;AAChD,IAAA,MAAM,cAAc,IAAA,CAAK,UAAA;AAGzB,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,EAAA,EAAI,CAAA,EAAA,EAAK;AAC3B,MAAA,MAAM,EAAA,GAAK,YAAY,CAAC,CAAA;AACxB,MAAA,IAAI,CAAC,EAAA,EAAI;AAET,MAAA,MAAM,IAAA,GAAO,KAAK,CAAC,CAAA;AACnB,MAAA,IAAI,CAAC,IAAA,EAAM;AAEX,MAAA,MAAM,OAAA,GAAU,gBAAgB,IAAI,CAAA;AACpC,MAAA,EAAA,CAAG,YAAA,CAAa,aAAa,OAAO,CAAA;AACpC,MAAA,EAAA,CAAG,WAAA,GAAc,MAAA,CAAO,IAAA,CAAK,OAAA,EAAS,CAAA;AAGtC,MAAA,MAAM,SAAA,GAAY,KAAK,WAAA,EAAY,KAAM,QAAQ,IAAA,CAAK,QAAA,OAAe,KAAA,GAAQ,CAAA;AAC7E,MAAA,EAAA,CAAG,YAAA,CAAa,cAAA,EAAgB,MAAA,CAAO,SAAS,CAAC,CAAA;AAGjD,MAAA,MAAM,QAAA,GAAW,eAAA,iBAAgB,IAAI,IAAA,EAAM,CAAA;AAC3C,MAAA,EAAA,CAAG,YAAA,CAAa,YAAA,EAAc,MAAA,CAAO,OAAA,KAAY,QAAQ,CAAC,CAAA;AAG1D,MAAA,MAAM,UAAA,GAAa,YAAY,IAAA,CAAK,aAAA;AACpC,MAAA,EAAA,CAAG,YAAA,CAAa,eAAA,EAAiB,MAAA,CAAO,UAAU,CAAC,CAAA;AAGnD,MAAA,MAAM,SAAA,GAAY,eAAA,CAAgB,IAAA,CAAK,WAAW,CAAA,KAAM,OAAA;AACxD,MAAA,EAAA,CAAG,YAAA,CAAa,UAAA,EAAY,SAAA,GAAY,GAAA,GAAM,IAAI,CAAA;AAGlD,MAAA,MAAM,UAAA,GAAa,IAAA,CAAK,kBAAA,CAAmB,OAAO,CAAA;AAClD,MAAA,IAAI,UAAA,EAAY;AACd,QAAA,EAAA,CAAG,YAAA,CAAa,iBAAiB,MAAM,CAAA;AAAA,MACzC,CAAA,MAAO;AACL,QAAA,EAAA,CAAG,gBAAgB,eAAe,CAAA;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,iBAAiB,UAAA,EAA+B;AAC9C,IAAA,IAAI,UAAA,CAAW,YAAA,CAAa,eAAe,CAAA,KAAM,MAAA,EAAQ;AAEzD,IAAA,MAAM,OAAA,GAAU,UAAA,CAAW,YAAA,CAAa,WAAW,CAAA;AACnD,IAAA,IAAI,CAAC,OAAA,EAAS;AAEd,IAAA,IAAA,CAAK,aAAA,GAAgB,OAAA;AAMrB,IAAA,MAAM,QAAA,GAAW,mBAAmB,OAAO,CAAA;AAC3C,IAAA,IAAI,QAAA,OAAe,WAAA,GAAc,QAAA;AACjC,IAAA,IAAA,CAAK,MAAA,EAAO;AACZ,IAAA,IAAA,CAAK,QAAA,CAAS,UAAU,EAAE,MAAA,EAAQ,EAAE,IAAA,EAAM,OAAA,IAAW,CAAA;AAAA,EACvD;AAAA,EAEA,wBAAwB,IAAA,EAAkB;AACxC,IAAA,IAAA,CAAK,WAAA,GAAc,IAAA;AACnB,IAAA,MAAM,cAAA,GAAiB,CAAA,EAAG,IAAA,CAAK,WAAA,EAAa,CAAA,CAAA,EAAI,MAAA,CAAO,IAAA,CAAK,QAAA,KAAa,CAAC,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAA;AAC5F,IAAA,MAAM,iBAAA,GAAoB,mBAAmB,IAAA,CAAK,UAAA;AAElD,IAAA,IAAI,iBAAA,EAAmB;AAMrB,MAAA,IAAA,CAAK,UAAA,GAAa,cAAA;AAAA,IACpB,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,MAAA,EAAO;AAAA,IACd;AAEA,IAAA,MAAM,cAAc,MAAM;AACxB,MAAA,MAAM,OAAA,GAAU,gBAAgB,IAAI,CAAA;AACpC,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,UAAA,CAAW,IAAA,CAAK,CAAC,OAAO,EAAA,CAAG,YAAA,CAAa,WAAW,CAAA,KAAM,OAAO,CAAA;AACtF,MAAA,QAAA,EAAU,KAAA,EAAM;AAAA,IAClB,CAAA;AAEA,IAAA,IAAI,iBAAA,EAAmB;AAErB,MAAA,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,WAAA,EAAa,CAAC,CAAA;AAAA,IACrC,CAAA,MAAO;AACL,MAAA,WAAA,EAAY;AAAA,IACd;AAAA,EACF;AAAA,EAEA,mBAAmB,OAAA,EAA0B;AAC3C,IAAA,IAAI,IAAA,CAAK,QAAA,IAAY,OAAA,GAAU,IAAA,CAAK,UAAU,OAAO,IAAA;AACrD,IAAA,IAAI,IAAA,CAAK,QAAA,IAAY,OAAA,GAAU,IAAA,CAAK,UAAU,OAAO,IAAA;AACrD,IAAA,OAAO,KAAA;AAAA,EACT;AAAA,EAEA,YAAY,KAAA,EAAqB;AAC/B,IAAA,MAAM,SAAA,GAAY,mBAAA,CAAoB,IAAA,CAAK,UAAU,CAAA;AACrD,IAAA,IAAI,CAAC,SAAA,EAAW;AAEhB,IAAA,MAAM,aAAA,GAAgB,IAAI,IAAA,CAAK,SAAA,CAAU,MAAM,SAAA,CAAU,KAAA,GAAQ,CAAA,GAAI,KAAA,EAAO,CAAC,CAAA;AAC7E,IAAA,IAAA,CAAK,UAAA,GAAa,CAAA,EAAG,aAAA,CAAc,WAAA,EAAa,CAAA,CAAA,EAAI,MAAA,CAAO,aAAA,CAAc,QAAA,KAAa,CAAC,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAA;AAAA,EAC3G;AAAA,EAEA,0BAAA,CAA2B,UAAgB,KAAA,EAAqB;AAC9D,IAAA,MAAM,UAAA,GAAa,IAAI,IAAA,CAAK,QAAA,CAAS,WAAA,IAAe,QAAA,CAAS,QAAA,EAAS,GAAI,KAAA,EAAO,CAAC,CAAA;AAClF,IAAA,MAAM,kBAAkB,IAAI,IAAA;AAAA,MAC1B,WAAW,WAAA,EAAY;AAAA,MACvB,UAAA,CAAW,UAAS,GAAI,CAAA;AAAA,MACxB;AAAA,MACA,OAAA,EAAQ;AACV,IAAA,MAAM,aAAa,IAAA,CAAK,GAAA,CAAI,QAAA,CAAS,OAAA,IAAW,eAAe,CAAA;AAC/D,IAAA,UAAA,CAAW,QAAQ,UAAU,CAAA;AAC7B,IAAA,OAAO,UAAA;AAAA,EACT;AAAA,EAEA,yBAAA,CAA0B,UAAgB,KAAA,EAAqB;AAC7D,IAAA,MAAM,UAAA,GAAa,IAAI,IAAA,CAAK,QAAA,CAAS,WAAA,KAAgB,KAAA,EAAO,QAAA,CAAS,QAAA,EAAS,EAAG,CAAC,CAAA;AAClF,IAAA,MAAM,kBAAkB,IAAI,IAAA;AAAA,MAC1B,WAAW,WAAA,EAAY;AAAA,MACvB,UAAA,CAAW,UAAS,GAAI,CAAA;AAAA,MACxB;AAAA,MACA,OAAA,EAAQ;AACV,IAAA,MAAM,aAAa,IAAA,CAAK,GAAA,CAAI,QAAA,CAAS,OAAA,IAAW,eAAe,CAAA;AAC/D,IAAA,UAAA,CAAW,QAAQ,UAAU,CAAA;AAC7B,IAAA,OAAO,UAAA;AAAA,EACT;AAAA,EAEA,oBAAoB,IAAA,EAAkB;AACpC,IAAA,MAAM,UAAA,GAAa,KAAK,MAAA,EAAO;AAC/B,IAAA,MAAM,KAAA,GAAA,CAAS,UAAA,GAAa,IAAA,CAAK,cAAA,GAAiB,CAAA,IAAK,CAAA;AACvD,IAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,IAAI,CAAA;AAC5B,IAAA,MAAA,CAAO,OAAA,CAAQ,IAAA,CAAK,OAAA,EAAQ,GAAI,KAAK,CAAA;AACrC,IAAA,OAAO,MAAA;AAAA,EACT;AAAA,EAEA,kBAAkB,IAAA,EAAkB;AAClC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,mBAAA,CAAoB,IAAI,CAAA;AAC3C,IAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,KAAK,CAAA;AAC7B,IAAA,MAAA,CAAO,OAAA,CAAQ,KAAA,CAAM,OAAA,EAAQ,GAAI,CAAC,CAAA;AAClC,IAAA,OAAO,MAAA;AAAA,EACT;AAAA,EAEA,sBAAA,GAA+B;AAC7B,IAAA,IAAI,KAAK,aAAA,EAAe;AACtB,MAAA,MAAM,QAAA,GAAW,kBAAA,CAAmB,IAAA,CAAK,aAAa,CAAA;AACtD,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA,IAAA,CAAK,WAAA,GAAc,QAAA;AACnB,QAAA;AAAA,MACF;AAAA,IACF;AAEA,IAAA,MAAM,SAAA,GAAY,mBAAA,CAAoB,IAAA,CAAK,UAAU,CAAA;AACrD,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,MAAM,KAAA,uBAAY,IAAA,EAAK;AACvB,MAAA,IAAI,KAAA,CAAM,WAAA,EAAY,KAAM,SAAA,CAAU,IAAA,IAAQ,MAAM,QAAA,EAAS,KAAM,SAAA,CAAU,KAAA,GAAQ,CAAA,EAAG;AACtF,QAAA,IAAA,CAAK,WAAA,GAAc,KAAA;AAAA,MACrB,CAAA,MAAO;AACL,QAAA,IAAA,CAAK,WAAA,GAAc,IAAI,IAAA,CAAK,SAAA,CAAU,MAAM,SAAA,CAAU,KAAA,GAAQ,GAAG,CAAC,CAAA;AAAA,MACpE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,yBAAA,GAAkC;AAChC,IAAA,MAAM,SAAA,GAAY,mBAAA,CAAoB,IAAA,CAAK,UAAU,CAAA;AACrD,IAAA,IAAI,CAAC,SAAA,EAAW;AAGhB,IAAA,IACE,IAAA,CAAK,WAAA,CAAY,WAAA,EAAY,KAAM,SAAA,CAAU,IAAA,IAC7C,IAAA,CAAK,WAAA,CAAY,QAAA,EAAS,KAAM,SAAA,CAAU,KAAA,GAAQ,CAAA,EAClD;AACA,MAAA,MAAM,KAAA,uBAAY,IAAA,EAAK;AACvB,MAAA,IAAI,KAAA,CAAM,WAAA,EAAY,KAAM,SAAA,CAAU,IAAA,IAAQ,MAAM,QAAA,EAAS,KAAM,SAAA,CAAU,KAAA,GAAQ,CAAA,EAAG;AACtF,QAAA,IAAA,CAAK,WAAA,GAAc,KAAA;AAAA,MACrB,CAAA,MAAO;AAEL,QAAA,MAAM,UAAA,GAAa,IAAI,IAAA,CAAK,SAAA,CAAU,MAAM,SAAA,CAAU,KAAA,GAAQ,GAAG,CAAC,CAAA;AAClE,QAAA,MAAM,eAAA,GAAkB,IAAI,IAAA,CAAK,SAAA,CAAU,MAAM,SAAA,CAAU,KAAA,EAAO,CAAC,CAAA,CAAE,OAAA,EAAQ;AAC7E,QAAA,MAAM,aAAa,IAAA,CAAK,GAAA,CAAI,KAAK,WAAA,CAAY,OAAA,IAAW,eAAe,CAAA;AACvE,QAAA,UAAA,CAAW,QAAQ,UAAU,CAAA;AAC7B,QAAA,IAAA,CAAK,WAAA,GAAc,UAAA;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,kBAAA,CAAmB,MAAc,KAAA,EAAuB;AACtD,IAAA,MAAM,WAAW,IAAI,IAAA,CAAK,IAAA,EAAM,KAAA,GAAQ,GAAG,CAAC,CAAA;AAC5C,IAAA,MAAM,SAAA,GAAY,SAAS,MAAA,EAAO;AAGlC,IAAA,MAAM,MAAA,GAAA,CAAU,SAAA,GAAY,IAAA,CAAK,cAAA,GAAiB,CAAA,IAAK,CAAA;AAEvD,IAAA,MAAM,OAAe,EAAC;AACtB,IAAA,MAAM,OAAA,GAAU,IAAI,IAAA,CAAK,QAAQ,CAAA;AACjC,IAAA,OAAA,CAAQ,OAAA,CAAQ,QAAA,CAAS,OAAA,EAAQ,GAAI,MAAM,CAAA;AAE3C,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,EAAA,EAAI,CAAA,EAAA,EAAK;AAC3B,MAAA,IAAA,CAAK,IAAA,CAAK,IAAI,IAAA,CAAK,OAAO,CAAC,CAAA;AAC3B,MAAA,OAAA,CAAQ,OAAA,CAAQ,OAAA,CAAQ,OAAA,EAAQ,GAAI,CAAC,CAAA;AAAA,IACvC;AAEA,IAAA,OAAO,IAAA;AAAA,EACT;AACF","file":"calendar_controller.js","sourcesContent":["/**\n * Local-time date helpers shared by the calendar-family controllers\n * (`calendar`, `date-range-picker`).\n *\n * All conversions are intentionally **local time** (not UTC): the grid a user\n * sees is built from `Date` components in their own timezone, so round-tripping\n * through ISO strings must use the same frame to avoid off-by-one-day drift near\n * midnight. Strings are bare `YYYY-MM-DD` / `YYYY-MM` with no time or zone.\n */\n\n/** Formats a {@link Date} as a local-time `YYYY-MM-DD` string. */\nexport function toISODateString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Parses a `YYYY-MM-DD` string into a local-time {@link Date}, or `null`.\n *\n * Rejects calendar-invalid dates (e.g. `2026-02-31`): the constructed `Date`\n * would silently roll over to another day, so the parsed components are checked\n * back against it and a mismatch returns `null`.\n */\nexport function parseISODateString(dateStr: string): Date | null {\n if (!dateStr) return null;\n const match = dateStr.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (!match) return null;\n const [, y, m, d] = match;\n const year = Number(y);\n const month = Number(m);\n const day = Number(d);\n const date = new Date(year, month - 1, day);\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\n/**\n * Parses a `YYYY-MM` string into its year/month (1-based) parts, or `null`.\n * Rejects months outside `1..12`.\n */\nexport function parseISOMonthString(monthStr: string): { year: number; month: number } | null {\n if (!monthStr) return null;\n const match = monthStr.match(/^(\\d{4})-(\\d{2})$/);\n if (!match) return null;\n const [, y, m] = match;\n const month = Number(m);\n if (month < 1 || month > 12) return null;\n return { year: Number(y), month };\n}\n\n/** Formats a {@link Date} as the local-time `YYYY-MM` string of its month. */\nexport function toISOMonthString(date: Date): string {\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}`;\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { parseISODateString, parseISOMonthString, toISODateString } from \"../utils/dates\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless, highly accessible calendar grid behavior.\n *\n * Markup contract (identifier: `stimeo--calendar`):\n * <div data-controller=\"stimeo--calendar\"\n * data-stimeo--calendar-month-value=\"2026-05\"\n * data-stimeo--calendar-selected-value=\"2026-05-31\">\n * <button data-action=\"stimeo--calendar#prev\">Previous</button>\n * <span data-stimeo--calendar-target=\"label\">May 2026</span>\n * <button data-action=\"stimeo--calendar#next\">Next</button>\n * <table role=\"grid\">\n * <tbody data-stimeo--calendar-target=\"grid\"\n * data-action=\"keydown->stimeo--calendar#onKeydown\n * click->stimeo--calendar#selectByClick\">\n * <!-- Markup must contain exactly 42 day targets (7 days x 6 rows) -->\n * <tr role=\"row\">\n * <td role=\"gridcell\" data-stimeo--calendar-target=\"day\" tabindex=\"-1\"></td>\n * </tr>\n * </tbody>\n * </table>\n * </div>\n *\n * Implements the WAI-ARIA APG **Date Picker Dialog** grid navigation pattern:\n * - Local-aware month/year labels using native `Intl.DateTimeFormat`.\n * - Roving tabindex focus tracking (exactly one focusable day at any time).\n * - Full grid keyboard controls (arrows, PageUp/Down, Home/End, Shift+PageUp/Down).\n * - Automatic month wrapping and date clamping for missing dates (e.g. leap years, 31st to 30th).\n *\n * @remarks\n * Behavior only. The controller updates classes, attributes (aria-selected, data-outside, etc.),\n * and text contents dynamically on 42 pre-allocated `day` targets.\n */\nexport class CalendarController extends Controller<HTMLElement> {\n static override targets = [\"label\", \"grid\", \"day\"];\n static override values = {\n month: { type: String, default: \"\" },\n selected: { type: String, default: \"\" },\n min: { type: String, default: \"\" },\n max: { type: String, default: \"\" },\n weekStart: { type: Number, default: 0 }, // 0 = Sunday, 1 = Monday, etc.\n };\n static actions = [\"next\", \"onKeydown\", \"prev\", \"selectByClick\"] as const;\n static events = [\"monthchange\", \"select\"] as const;\n\n declare readonly labelTarget: HTMLElement;\n declare readonly gridTarget: HTMLElement;\n declare readonly dayTargets: HTMLElement[];\n declare readonly hasLabelTarget: boolean;\n declare readonly hasGridTarget: boolean;\n\n declare monthValue: string;\n declare selectedValue: string;\n declare minValue: string;\n declare maxValue: string;\n declare weekStartValue: number;\n\n /** The date currently receiving focus in the grid (local time). */\n focusedDate: Date = new Date();\n\n /**\n * Deferred focus moves scheduled after an asynchronous month transition.\n * Tracked so {@link disconnect} can cancel any pending move and a detached\n * controller never steals focus after the element leaves the DOM (Turbo).\n */\n #focusTimer = new SafeTimeout();\n\n override connect(): void {\n // Initialize monthValue to current month if not provided\n if (!this.monthValue) {\n const today = new Date();\n const monthStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, \"0\")}`;\n this.monthValue = monthStr;\n return;\n }\n\n this.#initializeFocusedDate();\n this.render();\n }\n\n /** Cancels any pending deferred focus so it never fires on a detached element. */\n override disconnect(): void {\n this.#focusTimer.clearAll();\n }\n\n /**\n * Stimulus lifecycle callback triggered automatically when the monthValue changes.\n * Forces a re-render of the date grid and updates labels.\n */\n monthValueChanged(): void {\n if (!this.monthValue) return;\n this.#syncFocusedDateWithMonth();\n this.render();\n this.dispatch(\"monthchange\", { detail: { month: this.monthValue } });\n }\n\n /**\n * Stimulus lifecycle callback triggered automatically when the selectedValue changes.\n * Re-renders grid cells to update `aria-selected` indicators.\n */\n selectedValueChanged(): void {\n if (this.selectedValue) {\n const selected = parseISODateString(this.selectedValue);\n if (selected) {\n this.focusedDate = selected;\n }\n }\n this.render();\n }\n\n /** Navigates to the previous month. */\n prev(event?: Event): void {\n if (event) event.preventDefault();\n this.#shiftMonth(-1);\n }\n\n /** Navigates to the next month. */\n next(event?: Event): void {\n if (event) event.preventDefault();\n this.#shiftMonth(1);\n }\n\n /** Handles day selection when a gridcell is clicked. */\n selectByClick(event: MouseEvent): void {\n const target = event.target as HTMLElement | null;\n if (!target) return;\n const dayElement = target.closest(\"[data-stimeo--calendar-target='day']\") as HTMLElement | null;\n if (!dayElement) return;\n\n this.selectDayElement(dayElement);\n }\n\n /** Handles grid cell keyboard navigation and triggers selection. */\n onKeydown(event: KeyboardEvent): void {\n const target = event.target as HTMLElement | null;\n if (!target) return;\n const dayElement = target.closest(\"[data-stimeo--calendar-target='day']\") as HTMLElement | null;\n if (!dayElement) return;\n\n const dateStr = dayElement.getAttribute(\"data-date\");\n if (!dateStr) return;\n const date = parseISODateString(dateStr);\n if (!date) return;\n\n let handled = true;\n let nextDate = new Date(date);\n\n switch (event.key) {\n case \"ArrowLeft\":\n nextDate.setDate(nextDate.getDate() - 1);\n break;\n case \"ArrowRight\":\n nextDate.setDate(nextDate.getDate() + 1);\n break;\n case \"ArrowUp\":\n nextDate.setDate(nextDate.getDate() - 7);\n break;\n case \"ArrowDown\":\n nextDate.setDate(nextDate.getDate() + 7);\n break;\n case \"PageUp\":\n if (event.shiftKey) {\n nextDate = this.#calculateShiftedYearDate(date, -1);\n } else {\n nextDate = this.#calculateShiftedMonthDate(date, -1);\n }\n break;\n case \"PageDown\":\n if (event.shiftKey) {\n nextDate = this.#calculateShiftedYearDate(date, 1);\n } else {\n nextDate = this.#calculateShiftedMonthDate(date, 1);\n }\n break;\n case \"Home\":\n nextDate = this.#getStartOfWeekDate(date);\n break;\n case \"End\":\n nextDate = this.#getEndOfWeekDate(date);\n break;\n case \"t\":\n case \"T\": {\n const now = new Date();\n nextDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n break;\n }\n case \"Enter\":\n case \" \":\n event.preventDefault();\n this.selectDayElement(dayElement);\n return;\n default:\n handled = false;\n break;\n }\n\n if (handled) {\n event.preventDefault();\n this.#focusAndNavigateToDate(nextDate);\n }\n }\n\n /** Renders the grid days and updates the month/year label. */\n render(): void {\n const monthInfo = parseISOMonthString(this.monthValue);\n if (!monthInfo) return;\n\n const { year, month } = monthInfo;\n\n // Update label with localized month/year\n if (this.hasLabelTarget) {\n const lang = document.documentElement.lang || \"en\";\n const labelDate = new Date(year, month - 1, 1);\n const formatter = new Intl.DateTimeFormat(lang, { month: \"long\", year: \"numeric\" });\n this.labelTarget.textContent = formatter.format(labelDate);\n }\n\n const days = this.#calculateGridDays(year, month);\n const dayElements = this.dayTargets;\n\n // Iterate through pre-allocated day targets and bind state\n for (let i = 0; i < 42; i++) {\n const el = dayElements[i];\n if (!el) continue;\n\n const date = days[i];\n if (!date) continue;\n\n const dateStr = toISODateString(date);\n el.setAttribute(\"data-date\", dateStr);\n el.textContent = String(date.getDate());\n\n // outside current month\n const isOutside = date.getFullYear() !== year || date.getMonth() !== month - 1;\n el.setAttribute(\"data-outside\", String(isOutside));\n\n // today\n const todayStr = toISODateString(new Date());\n el.setAttribute(\"data-today\", String(dateStr === todayStr));\n\n // selection state\n const isSelected = dateStr === this.selectedValue;\n el.setAttribute(\"aria-selected\", String(isSelected));\n\n // roving tabindex\n const isFocused = toISODateString(this.focusedDate) === dateStr;\n el.setAttribute(\"tabindex\", isFocused ? \"0\" : \"-1\");\n\n // min/max limits\n const isDisabled = this.#isDateOutOfBounds(dateStr);\n if (isDisabled) {\n el.setAttribute(\"aria-disabled\", \"true\");\n } else {\n el.removeAttribute(\"aria-disabled\");\n }\n }\n }\n\n selectDayElement(dayElement: HTMLElement): void {\n if (dayElement.getAttribute(\"aria-disabled\") === \"true\") return;\n\n const dateStr = dayElement.getAttribute(\"data-date\");\n if (!dateStr) return;\n\n this.selectedValue = dateStr;\n // Reflect the selection synchronously (move roving focus to the selected day and\n // repaint aria-selected) instead of waiting on the async value observer, so a\n // click or Enter updates the grid in the same tick — consistent with how the\n // navigation handler re-renders directly. selectedValueChanged remains for\n // external (consumer-driven) value changes.\n const selected = parseISODateString(dateStr);\n if (selected) this.focusedDate = selected;\n this.render();\n this.dispatch(\"select\", { detail: { date: dateStr } });\n }\n\n #focusAndNavigateToDate(date: Date): void {\n this.focusedDate = date;\n const targetMonthStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}`;\n const isMonthTransition = targetMonthStr !== this.monthValue;\n\n if (isMonthTransition) {\n // Automatic month transition: assigning monthValue triggers\n // `monthValueChanged`, which re-renders the grid and keeps the already-set\n // focusedDate (it sits in the target month, so `#syncFocusedDateWithMonth`\n // is a no-op). Do NOT render synchronously here too — that double-renders;\n // focus is deferred below until after that async re-render lands.\n this.monthValue = targetMonthStr;\n } else {\n this.render();\n }\n\n const focusTarget = () => {\n const dateStr = toISODateString(date);\n const targetEl = this.dayTargets.find((el) => el.getAttribute(\"data-date\") === dateStr);\n targetEl?.focus();\n };\n\n if (isMonthTransition) {\n // Defer focus until Stimulus async lifecycle (monthValueChanged) has fully resolved\n this.#focusTimer.set(focusTarget, 0);\n } else {\n focusTarget();\n }\n }\n\n #isDateOutOfBounds(dateStr: string): boolean {\n if (this.minValue && dateStr < this.minValue) return true;\n if (this.maxValue && dateStr > this.maxValue) return true;\n return false;\n }\n\n #shiftMonth(delta: number): void {\n const monthInfo = parseISOMonthString(this.monthValue);\n if (!monthInfo) return;\n\n const nextMonthDate = new Date(monthInfo.year, monthInfo.month - 1 + delta, 1);\n this.monthValue = `${nextMonthDate.getFullYear()}-${String(nextMonthDate.getMonth() + 1).padStart(2, \"0\")}`;\n }\n\n #calculateShiftedMonthDate(baseDate: Date, delta: number): Date {\n const targetDate = new Date(baseDate.getFullYear(), baseDate.getMonth() + delta, 1);\n const lastDayInTarget = new Date(\n targetDate.getFullYear(),\n targetDate.getMonth() + 1,\n 0,\n ).getDate();\n const clampedDay = Math.min(baseDate.getDate(), lastDayInTarget);\n targetDate.setDate(clampedDay);\n return targetDate;\n }\n\n #calculateShiftedYearDate(baseDate: Date, delta: number): Date {\n const targetDate = new Date(baseDate.getFullYear() + delta, baseDate.getMonth(), 1);\n const lastDayInTarget = new Date(\n targetDate.getFullYear(),\n targetDate.getMonth() + 1,\n 0,\n ).getDate();\n const clampedDay = Math.min(baseDate.getDate(), lastDayInTarget);\n targetDate.setDate(clampedDay);\n return targetDate;\n }\n\n #getStartOfWeekDate(date: Date): Date {\n const currentDay = date.getDay();\n const shift = (currentDay - this.weekStartValue + 7) % 7;\n const target = new Date(date);\n target.setDate(date.getDate() - shift);\n return target;\n }\n\n #getEndOfWeekDate(date: Date): Date {\n const start = this.#getStartOfWeekDate(date);\n const target = new Date(start);\n target.setDate(start.getDate() + 6);\n return target;\n }\n\n #initializeFocusedDate(): void {\n if (this.selectedValue) {\n const selected = parseISODateString(this.selectedValue);\n if (selected) {\n this.focusedDate = selected;\n return;\n }\n }\n\n const monthInfo = parseISOMonthString(this.monthValue);\n if (monthInfo) {\n const today = new Date();\n if (today.getFullYear() === monthInfo.year && today.getMonth() === monthInfo.month - 1) {\n this.focusedDate = today;\n } else {\n this.focusedDate = new Date(monthInfo.year, monthInfo.month - 1, 1);\n }\n }\n }\n\n #syncFocusedDateWithMonth(): void {\n const monthInfo = parseISOMonthString(this.monthValue);\n if (!monthInfo) return;\n\n // Only align if focusedDate is outside current monthValue\n if (\n this.focusedDate.getFullYear() !== monthInfo.year ||\n this.focusedDate.getMonth() !== monthInfo.month - 1\n ) {\n const today = new Date();\n if (today.getFullYear() === monthInfo.year && today.getMonth() === monthInfo.month - 1) {\n this.focusedDate = today;\n } else {\n // Keep same day if possible, otherwise clamp to end of target month\n const targetDate = new Date(monthInfo.year, monthInfo.month - 1, 1);\n const lastDayInTarget = new Date(monthInfo.year, monthInfo.month, 0).getDate();\n const clampedDay = Math.min(this.focusedDate.getDate(), lastDayInTarget);\n targetDate.setDate(clampedDay);\n this.focusedDate = targetDate;\n }\n }\n }\n\n #calculateGridDays(year: number, month: number): Date[] {\n const firstDay = new Date(year, month - 1, 1);\n const dayOfWeek = firstDay.getDay();\n\n // calculate offset days based on weekStartValue\n const offset = (dayOfWeek - this.weekStartValue + 7) % 7;\n\n const days: Date[] = [];\n const current = new Date(firstDay);\n current.setDate(firstDay.getDate() - offset);\n\n for (let i = 0; i < 42; i++) {\n days.push(new Date(current));\n current.setDate(current.getDate() + 1);\n }\n\n return days;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/dates.ts","../../src/utils/safe_timeout.ts","../../src/controllers/calendar_controller.ts"],"names":[],"mappings":"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","file":"calendar_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 * Local-time date helpers shared by the calendar-family controllers\n * (`calendar`, `date-range-picker`).\n *\n * All conversions are intentionally **local time** (not UTC): the grid a user\n * sees is built from `Date` components in their own timezone, so round-tripping\n * through ISO strings must use the same frame to avoid off-by-one-day drift near\n * midnight. Strings are bare `YYYY-MM-DD` / `YYYY-MM` with no time or zone.\n */\n\n/** Formats a {@link Date} as a local-time `YYYY-MM-DD` string. */\nexport function toISODateString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Parses a `YYYY-MM-DD` string into a local-time {@link Date}, or `null`.\n *\n * Rejects calendar-invalid dates (e.g. `2026-02-31`): the constructed `Date`\n * would silently roll over to another day, so the parsed components are checked\n * back against it and a mismatch returns `null`.\n */\nexport function parseISODateString(dateStr: string): Date | null {\n if (!dateStr) return null;\n const match = dateStr.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (!match) return null;\n const [, y, m, d] = match;\n const year = Number(y);\n const month = Number(m);\n const day = Number(d);\n const date = new Date(year, month - 1, day);\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\n/**\n * Parses a `YYYY-MM` string into its year/month (1-based) parts, or `null`.\n * Rejects months outside `1..12`.\n */\nexport function parseISOMonthString(monthStr: string): { year: number; month: number } | null {\n if (!monthStr) return null;\n const match = monthStr.match(/^(\\d{4})-(\\d{2})$/);\n if (!match) return null;\n const [, y, m] = match;\n const month = Number(m);\n if (month < 1 || month > 12) return null;\n return { year: Number(y), month };\n}\n\n/** Formats a {@link Date} as the local-time `YYYY-MM` string of its month. */\nexport function toISOMonthString(date: Date): string {\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}`;\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\nimport {\n parseISODateString,\n parseISOMonthString,\n toISODateString,\n toISOMonthString,\n} from \"../utils/dates\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Marks an `aria-disabled` this controller wrote, so a re-paint takes back only\n * its own. A consumer marks holidays and booked days with the same attribute;\n * without the marker a paint would clear their intent.\n *\n * The marker is scoped to the date the cell currently shows, not to the cell: the\n * 42 cells are recycled every month, so a value that outlived its date would\n * disable a different day. A consumer that marks specific dates applies them once\n * for the month the grid opens on, then re-applies after\n * `stimeo--calendar:monthchange`, which reports a move to a different month.\n */\nconst OWNED_DISABLED = \"data-stimeo--calendar-owns-disabled\";\n\n/**\n * Headless, highly accessible calendar grid behavior.\n *\n * Markup contract (identifier: `stimeo--calendar`):\n * <div data-controller=\"stimeo--calendar\"\n * data-stimeo--calendar-month-value=\"2026-05\"\n * data-stimeo--calendar-selected-value=\"2026-05-31\">\n * <button data-action=\"stimeo--calendar#prev\">Previous</button>\n * <span data-stimeo--calendar-target=\"label\">May 2026</span>\n * <button data-action=\"stimeo--calendar#next\">Next</button>\n * <table role=\"grid\">\n * <tbody data-stimeo--calendar-target=\"grid\"\n * data-action=\"keydown->stimeo--calendar#onKeydown\n * click->stimeo--calendar#selectByClick\">\n * <!-- Markup must contain exactly 42 day targets (7 days x 6 rows) -->\n * <tr role=\"row\">\n * <td role=\"gridcell\" data-stimeo--calendar-target=\"day\" tabindex=\"-1\"></td>\n * </tr>\n * </tbody>\n * </table>\n * </div>\n *\n * Implements the WAI-ARIA APG **Date Picker Dialog** grid navigation pattern:\n * - Local-aware month/year labels using native `Intl.DateTimeFormat`.\n * - Roving tabindex focus tracking (exactly one focusable day at any time).\n * - Full grid keyboard controls (arrows, PageUp/Down, Home/End, Shift+PageUp/Down).\n * - Automatic month wrapping and date clamping for missing dates (e.g. leap years, 31st to 30th).\n *\n * @remarks\n * Behavior only. The controller updates classes, attributes (aria-selected, data-outside, etc.),\n * and text contents dynamically on 42 pre-allocated `day` targets.\n */\nexport class CalendarController extends Controller<HTMLElement> {\n static override targets = [\"label\", \"grid\", \"day\"];\n static override values = {\n month: { type: String, default: \"\" },\n selected: { type: String, default: \"\" },\n min: { type: String, default: \"\" },\n max: { type: String, default: \"\" },\n weekStart: { type: Number, default: 0 }, // 0 = Sunday, 1 = Monday, etc.\n };\n static actions = [\"next\", \"onKeydown\", \"prev\", \"selectByClick\"] as const;\n static events = [\"monthchange\", \"select\"] as const;\n\n declare readonly labelTarget: HTMLElement;\n declare readonly gridTarget: HTMLElement;\n declare readonly dayTargets: HTMLElement[];\n declare readonly hasLabelTarget: boolean;\n declare readonly hasGridTarget: boolean;\n\n declare monthValue: string;\n declare selectedValue: string;\n declare minValue: string;\n declare maxValue: string;\n declare weekStartValue: number;\n\n /** The date currently receiving focus in the grid (local time). */\n focusedDate: Date = new Date();\n\n /**\n * Deferred focus moves scheduled after an asynchronous month transition.\n * Tracked so {@link disconnect} can cancel any pending move and a detached\n * controller never steals focus after the element leaves the DOM (Turbo).\n */\n #focusTimer = new SafeTimeout();\n\n /**\n * The painted month the last `monthchange` reported, or `null` before the first\n * paint. Settling on the initial month — whether it comes from the attribute or\n * is derived here — is the grid describing itself, not a navigation, so it must\n * not reach a listener that refetches inventory or pushes history.\n */\n #announcedMonth: string | null = null;\n\n override connect(): void {\n // Initialize monthValue to current month if not provided\n if (!this.monthValue) {\n const today = new Date();\n const monthStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, \"0\")}`;\n this.monthValue = monthStr;\n return;\n }\n\n this.#initializeFocusedDate();\n this.render();\n }\n\n /** Cancels any pending deferred focus so it never fires on a detached element. */\n override disconnect(): void {\n this.#focusTimer.clearAll();\n }\n\n /**\n * Stimulus lifecycle callback triggered automatically when the monthValue changes.\n * Forces a re-render of the date grid and updates labels.\n */\n monthValueChanged(): void {\n if (!this.monthValue) return;\n this.#syncFocusedDateWithMonth();\n this.render();\n }\n\n /**\n * Stimulus lifecycle callback triggered automatically when the selectedValue changes.\n * Re-renders grid cells to update `aria-selected` indicators.\n */\n selectedValueChanged(): void {\n if (this.selectedValue) {\n const selected = parseISODateString(this.selectedValue);\n if (selected) {\n this.focusedDate = selected;\n }\n }\n this.render();\n }\n\n /** Navigates to the previous month. */\n prev(event?: Event): void {\n if (event) event.preventDefault();\n this.#shiftMonth(-1);\n }\n\n /** Navigates to the next month. */\n next(event?: Event): void {\n if (event) event.preventDefault();\n this.#shiftMonth(1);\n }\n\n /** Handles day selection when a gridcell is clicked. */\n selectByClick(event: MouseEvent): void {\n const dayElement = (event.target as HTMLElement | null)?.closest<HTMLElement>(\n \"[data-stimeo--calendar-target='day']\",\n );\n if (!dayElement) return;\n\n this.selectDayElement(dayElement);\n }\n\n /** Handles grid cell keyboard navigation and triggers selection. */\n onKeydown(event: KeyboardEvent): void {\n // A widget that already claimed the key (a nested control, an enclosing\n // composite) must not ALSO move the roving focus or select a day.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n const dayElement = (event.target as HTMLElement | null)?.closest<HTMLElement>(\n \"[data-stimeo--calendar-target='day']\",\n );\n if (!dayElement) return;\n\n // A cell the paint never reached carries no `data-date`, and an empty string\n // parses to null — one exit covers both.\n const date = parseISODateString(dayElement.getAttribute(\"data-date\") ?? \"\");\n if (!date) return;\n\n let handled = true;\n let nextDate = new Date(date);\n\n // Logical, not physical. The key is normalised rather than the\n // delta negated: these two branches are not mirror images — their guards\n // differ — so swapping the key keeps each guard with its own direction.\n switch (logicalArrowKey(event.key, this.element)) {\n case \"ArrowLeft\":\n nextDate.setDate(nextDate.getDate() - 1);\n break;\n case \"ArrowRight\":\n nextDate.setDate(nextDate.getDate() + 1);\n break;\n case \"ArrowUp\":\n nextDate.setDate(nextDate.getDate() - 7);\n break;\n case \"ArrowDown\":\n nextDate.setDate(nextDate.getDate() + 7);\n break;\n case \"PageUp\":\n if (event.shiftKey) {\n nextDate = this.#calculateShiftedYearDate(date, -1);\n } else {\n nextDate = this.#calculateShiftedMonthDate(date, -1);\n }\n break;\n case \"PageDown\":\n if (event.shiftKey) {\n nextDate = this.#calculateShiftedYearDate(date, 1);\n } else {\n nextDate = this.#calculateShiftedMonthDate(date, 1);\n }\n break;\n case \"Home\":\n nextDate = this.#getStartOfWeekDate(date);\n break;\n case \"End\":\n nextDate = this.#getEndOfWeekDate(date);\n break;\n case \"t\":\n case \"T\": {\n // A single printable key must not be claimed out of a modifier chord:\n // Ctrl/Cmd/Alt+T belongs to the browser, not to the grid.\n if (event.ctrlKey || event.metaKey || event.altKey) {\n handled = false;\n break;\n }\n const now = new Date();\n nextDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n break;\n }\n case \"Enter\":\n case \" \":\n event.preventDefault();\n this.selectDayElement(dayElement);\n return;\n default:\n handled = false;\n break;\n }\n\n if (handled) {\n event.preventDefault();\n this.#focusAndNavigateToDate(nextDate);\n }\n }\n\n /**\n * First day of the month the grid paints: the `month` Value when it parses,\n * otherwise the focused date's month.\n *\n * A malformed `month` falls back instead of stopping the paint: every cell\n * keeps its `aria-selected` and the grid keeps its tab stop, so a Value typo\n * still leaves the grid reachable by Tab and the author can see what they\n * typed. Every consumer of \"which month is on screen\" reads it from here, so\n * the paint and the `monthchange` report cannot name different months.\n */\n #paintedMonthStart(): Date {\n const monthInfo = parseISOMonthString(this.monthValue);\n return monthInfo\n ? new Date(monthInfo.year, monthInfo.month - 1, 1)\n : new Date(this.focusedDate.getFullYear(), this.focusedDate.getMonth(), 1);\n }\n\n /**\n * Renders the grid days and updates the month/year label, then reports the\n * painted month once it differs from the one already announced.\n *\n * The report belongs to the paint, not to the `month` Value: the Value is only\n * one of the things that decide the month on screen. While a malformed `month`\n * falls back, changing `selected` and selecting a neighbouring month's cell\n * move the painted month too, and both repaint from here. Reading the report\n * off the paint keeps the event naming the month the grid shows, in the\n * `YYYY-MM` the detail contract promises, whichever route repainted.\n */\n render(): void {\n const monthStart = this.#paintedMonthStart();\n const year = monthStart.getFullYear();\n const month = monthStart.getMonth() + 1;\n\n // Update label with localized month/year\n if (this.hasLabelTarget) {\n const lang = document.documentElement.lang || \"en\";\n const formatter = new Intl.DateTimeFormat(lang, { month: \"long\", year: \"numeric\" });\n this.labelTarget.textContent = formatter.format(monthStart);\n }\n\n const days = this.#calculateGridDays(year, month);\n const dayElements = this.dayTargets;\n\n // Driven by the dates, so every iteration has one. A short `day` target set\n // is the consumer's markup contradicting the documented 42 cells: paint the\n // ones that exist rather than dropping the grid out of the tab order.\n for (const [index, date] of days.entries()) {\n const el = dayElements[index];\n if (!el) continue;\n\n const dateStr = toISODateString(date);\n // The 42 cells are recycled across months, so an `aria-disabled` the\n // consumer wrote describes the date the cell showed before this paint.\n // Carrying it over would silently disable an unrelated day, so a date change\n // resets the attribute to whatever this render decides below.\n if (el.getAttribute(\"data-date\") !== dateStr) {\n el.removeAttribute(\"aria-disabled\");\n el.removeAttribute(OWNED_DISABLED);\n }\n el.setAttribute(\"data-date\", dateStr);\n el.textContent = String(date.getDate());\n\n // outside current month\n const isOutside = date.getFullYear() !== year || date.getMonth() !== month - 1;\n el.setAttribute(\"data-outside\", String(isOutside));\n\n // today\n const todayStr = toISODateString(new Date());\n el.setAttribute(\"data-today\", String(dateStr === todayStr));\n\n // selection state\n const isSelected = dateStr === this.selectedValue;\n el.setAttribute(\"aria-selected\", String(isSelected));\n\n // roving tabindex\n const isFocused = toISODateString(this.focusedDate) === dateStr;\n el.setAttribute(\"tabindex\", isFocused ? \"0\" : \"-1\");\n\n // min/max limits. Only the value this controller wrote comes back off: a\n // consumer marks holidays and booked days with the same attribute, and\n // clearing it on every paint would silently throw their intent away.\n const isDisabled = this.#isDateOutOfBounds(dateStr);\n if (isDisabled) {\n if (!el.hasAttribute(\"aria-disabled\")) el.setAttribute(OWNED_DISABLED, \"\");\n el.setAttribute(\"aria-disabled\", \"true\");\n } else if (el.hasAttribute(OWNED_DISABLED)) {\n el.removeAttribute(\"aria-disabled\");\n el.removeAttribute(OWNED_DISABLED);\n }\n }\n\n // Roving contract: exactly one focusable cell, always. `focusedDate` can sit\n // outside the rendered range — `selected` names a day in another month, and\n // nothing forces the two Values to agree — in which case no cell matched\n // above and the grid would be unreachable by Tab. Fall back to the first day\n // of the shown month so there is always a way in; arrow navigation reads the\n // date off the focused cell, so it continues from wherever the tab stop is.\n if (!dayElements.some((el) => el.getAttribute(\"tabindex\") === \"0\")) {\n const fallback =\n dayElements.find((el) => el.getAttribute(\"data-outside\") === \"false\") ?? dayElements[0];\n fallback?.setAttribute(\"tabindex\", \"0\");\n }\n\n // Post-paint, so a listener that re-marks holidays and booked days sees the\n // cells of the month it is being told about.\n const painted = toISOMonthString(monthStart);\n const previous = this.#announcedMonth;\n this.#announcedMonth = painted;\n if (previous !== null && previous !== painted) {\n this.dispatch(\"monthchange\", { detail: { month: painted } });\n }\n }\n\n selectDayElement(dayElement: HTMLElement): void {\n if (dayElement.getAttribute(\"aria-disabled\") === \"true\") return;\n\n const dateStr = dayElement.getAttribute(\"data-date\");\n if (!dateStr) return;\n\n this.selectedValue = dateStr;\n // Reflect the selection synchronously (move roving focus to the selected day and\n // repaint aria-selected) instead of waiting on the async value observer, so a\n // click or Enter updates the grid in the same tick — consistent with how the\n // navigation handler re-renders directly. `selectedValueChanged` covers\n // external (consumer-driven) value changes.\n const selected = parseISODateString(dateStr);\n if (selected) this.focusedDate = selected;\n this.render();\n this.dispatch(\"select\", { detail: { date: dateStr } });\n }\n\n #focusAndNavigateToDate(date: Date): void {\n this.focusedDate = date;\n const targetMonthStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}`;\n const isMonthTransition = targetMonthStr !== this.monthValue;\n\n if (isMonthTransition) {\n // Automatic month transition: assigning monthValue triggers\n // `monthValueChanged`, which re-renders the grid and keeps the already-set\n // focusedDate (it sits in the target month, so `#syncFocusedDateWithMonth`\n // is a no-op). Do NOT render synchronously here too — that double-renders;\n // focus is deferred below until after that async re-render lands.\n this.monthValue = targetMonthStr;\n } else {\n this.render();\n }\n\n const focusTarget = () => {\n const dateStr = toISODateString(date);\n const targetEl = this.dayTargets.find((el) => el.getAttribute(\"data-date\") === dateStr);\n targetEl?.focus();\n };\n\n if (isMonthTransition) {\n // Defer focus until Stimulus async lifecycle (monthValueChanged) has fully resolved\n this.#focusTimer.set(focusTarget, 0);\n } else {\n focusTarget();\n }\n }\n\n #isDateOutOfBounds(dateStr: string): boolean {\n if (this.minValue && dateStr < this.minValue) return true;\n if (this.maxValue && dateStr > this.maxValue) return true;\n return false;\n }\n\n #shiftMonth(delta: number): void {\n const monthInfo = parseISOMonthString(this.monthValue);\n if (!monthInfo) return;\n\n const nextMonthDate = new Date(monthInfo.year, monthInfo.month - 1 + delta, 1);\n this.monthValue = `${nextMonthDate.getFullYear()}-${String(nextMonthDate.getMonth() + 1).padStart(2, \"0\")}`;\n }\n\n #calculateShiftedMonthDate(baseDate: Date, delta: number): Date {\n const targetDate = new Date(baseDate.getFullYear(), baseDate.getMonth() + delta, 1);\n const lastDayInTarget = new Date(\n targetDate.getFullYear(),\n targetDate.getMonth() + 1,\n 0,\n ).getDate();\n const clampedDay = Math.min(baseDate.getDate(), lastDayInTarget);\n targetDate.setDate(clampedDay);\n return targetDate;\n }\n\n #calculateShiftedYearDate(baseDate: Date, delta: number): Date {\n const targetDate = new Date(baseDate.getFullYear() + delta, baseDate.getMonth(), 1);\n const lastDayInTarget = new Date(\n targetDate.getFullYear(),\n targetDate.getMonth() + 1,\n 0,\n ).getDate();\n const clampedDay = Math.min(baseDate.getDate(), lastDayInTarget);\n targetDate.setDate(clampedDay);\n return targetDate;\n }\n\n #getStartOfWeekDate(date: Date): Date {\n const currentDay = date.getDay();\n const shift = (currentDay - this.weekStartValue + 7) % 7;\n const target = new Date(date);\n target.setDate(date.getDate() - shift);\n return target;\n }\n\n #getEndOfWeekDate(date: Date): Date {\n const start = this.#getStartOfWeekDate(date);\n const target = new Date(start);\n target.setDate(start.getDate() + 6);\n return target;\n }\n\n #initializeFocusedDate(): void {\n if (this.selectedValue) {\n const selected = parseISODateString(this.selectedValue);\n if (selected) {\n this.focusedDate = selected;\n return;\n }\n }\n\n const monthInfo = parseISOMonthString(this.monthValue);\n if (monthInfo) {\n const today = new Date();\n if (today.getFullYear() === monthInfo.year && today.getMonth() === monthInfo.month - 1) {\n this.focusedDate = today;\n } else {\n this.focusedDate = new Date(monthInfo.year, monthInfo.month - 1, 1);\n }\n }\n }\n\n #syncFocusedDateWithMonth(): void {\n const monthInfo = parseISOMonthString(this.monthValue);\n if (!monthInfo) return;\n\n // Only align if focusedDate is outside current monthValue\n if (\n this.focusedDate.getFullYear() !== monthInfo.year ||\n this.focusedDate.getMonth() !== monthInfo.month - 1\n ) {\n const today = new Date();\n if (today.getFullYear() === monthInfo.year && today.getMonth() === monthInfo.month - 1) {\n this.focusedDate = today;\n } else {\n // Keep same day if possible, otherwise clamp to end of target month\n const targetDate = new Date(monthInfo.year, monthInfo.month - 1, 1);\n const lastDayInTarget = new Date(monthInfo.year, monthInfo.month, 0).getDate();\n const clampedDay = Math.min(this.focusedDate.getDate(), lastDayInTarget);\n targetDate.setDate(clampedDay);\n this.focusedDate = targetDate;\n }\n }\n }\n\n #calculateGridDays(year: number, month: number): Date[] {\n const firstDay = new Date(year, month - 1, 1);\n const dayOfWeek = firstDay.getDay();\n\n // calculate offset days based on weekStartValue\n const offset = (dayOfWeek - this.weekStartValue + 7) % 7;\n\n const days: Date[] = [];\n const current = new Date(firstDay);\n current.setDate(firstDay.getDate() - offset);\n\n for (let i = 0; i < 42; i++) {\n days.push(new Date(current));\n current.setDate(current.getDate() + 1);\n }\n\n return days;\n }\n}\n"]}
|
|
@@ -69,8 +69,23 @@ declare class CarouselController extends Controller<HTMLElement> {
|
|
|
69
69
|
autoplayValue: boolean;
|
|
70
70
|
intervalValue: number;
|
|
71
71
|
loopValue: boolean;
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Renders the initial slide and starts autoplay when requested.
|
|
74
|
+
*
|
|
75
|
+
* `findIndex` makes the authored pre-selection first-wins when several pickers
|
|
76
|
+
* are marked; `#render` then writes an explicit value onto every picker.
|
|
77
|
+
*/
|
|
73
78
|
connect(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Re-establishes the single selected picker when one is added after connect.
|
|
81
|
+
*
|
|
82
|
+
* Without this an appended picker that arrives `aria-selected="true"` leaves two
|
|
83
|
+
* marked at once — the authored pre-selection is only read on connect, so
|
|
84
|
+
* nothing else ever resolves the conflict. The current slide is kept: the
|
|
85
|
+
* repaint re-derives every picker from `#index`, so a late arrival never steals
|
|
86
|
+
* the selection.
|
|
87
|
+
*/
|
|
88
|
+
pickerTargetConnected(): void;
|
|
74
89
|
/** Clears the autoplay interval so it never fires after teardown. */
|
|
75
90
|
disconnect(): void;
|
|
76
91
|
/** Advances to the next slide. Bound via `data-action`. */
|