stimeo-ui 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/controllers/accordion_controller.d.ts +59 -0
- package/dist/controllers/accordion_controller.js +76 -0
- package/dist/controllers/accordion_controller.js.map +1 -0
- package/dist/controllers/announcer_controller.d.ts +78 -0
- package/dist/controllers/announcer_controller.js +184 -0
- package/dist/controllers/announcer_controller.js.map +1 -0
- package/dist/controllers/aspect_ratio_controller.d.ts +39 -0
- package/dist/controllers/aspect_ratio_controller.js +36 -0
- package/dist/controllers/aspect_ratio_controller.js.map +1 -0
- package/dist/controllers/auto_submit_controller.d.ts +74 -0
- package/dist/controllers/auto_submit_controller.js +147 -0
- package/dist/controllers/auto_submit_controller.js.map +1 -0
- package/dist/controllers/avatar_controller.d.ts +50 -0
- package/dist/controllers/avatar_controller.js +66 -0
- package/dist/controllers/avatar_controller.js.map +1 -0
- package/dist/controllers/breadcrumb_controller.d.ts +60 -0
- package/dist/controllers/breadcrumb_controller.js +123 -0
- package/dist/controllers/breadcrumb_controller.js.map +1 -0
- package/dist/controllers/bulk_select_controller.d.ts +67 -0
- package/dist/controllers/bulk_select_controller.js +104 -0
- package/dist/controllers/bulk_select_controller.js.map +1 -0
- package/dist/controllers/calendar_controller.d.ts +100 -0
- package/dist/controllers/calendar_controller.js +394 -0
- package/dist/controllers/calendar_controller.js.map +1 -0
- package/dist/controllers/character_counter_controller.d.ts +63 -0
- package/dist/controllers/character_counter_controller.js +179 -0
- package/dist/controllers/character_counter_controller.js.map +1 -0
- package/dist/controllers/checkbox_controller.d.ts +52 -0
- package/dist/controllers/checkbox_controller.js +73 -0
- package/dist/controllers/checkbox_controller.js.map +1 -0
- package/dist/controllers/combobox_controller.d.ts +76 -0
- package/dist/controllers/combobox_controller.js +186 -0
- package/dist/controllers/combobox_controller.js.map +1 -0
- package/dist/controllers/command_palette_controller.d.ts +103 -0
- package/dist/controllers/command_palette_controller.js +381 -0
- package/dist/controllers/command_palette_controller.js.map +1 -0
- package/dist/controllers/conditional_fields_controller.d.ts +56 -0
- package/dist/controllers/conditional_fields_controller.js +112 -0
- package/dist/controllers/conditional_fields_controller.js.map +1 -0
- package/dist/controllers/confirm_controller.d.ts +83 -0
- package/dist/controllers/confirm_controller.js +276 -0
- package/dist/controllers/confirm_controller.js.map +1 -0
- package/dist/controllers/context_menu_controller.d.ts +81 -0
- package/dist/controllers/context_menu_controller.js +112 -0
- package/dist/controllers/context_menu_controller.js.map +1 -0
- package/dist/controllers/countdown_controller.d.ts +93 -0
- package/dist/controllers/countdown_controller.js +202 -0
- package/dist/controllers/countdown_controller.js.map +1 -0
- package/dist/controllers/dialog_controller.d.ts +57 -0
- package/dist/controllers/dialog_controller.js +207 -0
- package/dist/controllers/dialog_controller.js.map +1 -0
- package/dist/controllers/direct_upload_controller.d.ts +77 -0
- package/dist/controllers/direct_upload_controller.js +212 -0
- package/dist/controllers/direct_upload_controller.js.map +1 -0
- package/dist/controllers/dirty_form_controller.d.ts +52 -0
- package/dist/controllers/dirty_form_controller.js +128 -0
- package/dist/controllers/dirty_form_controller.js.map +1 -0
- package/dist/controllers/dropdown_controller.d.ts +49 -0
- package/dist/controllers/dropdown_controller.js +66 -0
- package/dist/controllers/dropdown_controller.js.map +1 -0
- package/dist/controllers/empty_state_controller.d.ts +60 -0
- package/dist/controllers/empty_state_controller.js +67 -0
- package/dist/controllers/empty_state_controller.js.map +1 -0
- package/dist/controllers/flash_controller.d.ts +63 -0
- package/dist/controllers/flash_controller.js +221 -0
- package/dist/controllers/flash_controller.js.map +1 -0
- package/dist/controllers/focus_controller.d.ts +69 -0
- package/dist/controllers/focus_controller.js +216 -0
- package/dist/controllers/focus_controller.js.map +1 -0
- package/dist/controllers/form_field_controller.d.ts +79 -0
- package/dist/controllers/form_field_controller.js +154 -0
- package/dist/controllers/form_field_controller.js.map +1 -0
- package/dist/controllers/form_validation_controller.d.ts +114 -0
- package/dist/controllers/form_validation_controller.js +202 -0
- package/dist/controllers/form_validation_controller.js.map +1 -0
- package/dist/controllers/frame_loading_controller.d.ts +58 -0
- package/dist/controllers/frame_loading_controller.js +177 -0
- package/dist/controllers/frame_loading_controller.js.map +1 -0
- package/dist/controllers/highlight_controller.d.ts +50 -0
- package/dist/controllers/highlight_controller.js +107 -0
- package/dist/controllers/highlight_controller.js.map +1 -0
- package/dist/controllers/hover_card_controller.d.ts +79 -0
- package/dist/controllers/hover_card_controller.js +165 -0
- package/dist/controllers/hover_card_controller.js.map +1 -0
- package/dist/controllers/idle_controller.d.ts +51 -0
- package/dist/controllers/idle_controller.js +141 -0
- package/dist/controllers/idle_controller.js.map +1 -0
- package/dist/controllers/input_mask_controller.d.ts +74 -0
- package/dist/controllers/input_mask_controller.js +166 -0
- package/dist/controllers/input_mask_controller.js.map +1 -0
- package/dist/controllers/lazy_frame_controller.d.ts +53 -0
- package/dist/controllers/lazy_frame_controller.js +68 -0
- package/dist/controllers/lazy_frame_controller.js.map +1 -0
- package/dist/controllers/listbox_controller.d.ts +88 -0
- package/dist/controllers/listbox_controller.js +256 -0
- package/dist/controllers/listbox_controller.js.map +1 -0
- package/dist/controllers/local_time_controller.d.ts +60 -0
- package/dist/controllers/local_time_controller.js +81 -0
- package/dist/controllers/local_time_controller.js.map +1 -0
- package/dist/controllers/menu_controller.d.ts +81 -0
- package/dist/controllers/menu_controller.js +134 -0
- package/dist/controllers/menu_controller.js.map +1 -0
- package/dist/controllers/meter_controller.d.ts +93 -0
- package/dist/controllers/meter_controller.js +96 -0
- package/dist/controllers/meter_controller.js.map +1 -0
- package/dist/controllers/nested_form_controller.d.ts +85 -0
- package/dist/controllers/nested_form_controller.js +131 -0
- package/dist/controllers/nested_form_controller.js.map +1 -0
- package/dist/controllers/network_status_controller.d.ts +49 -0
- package/dist/controllers/network_status_controller.js +126 -0
- package/dist/controllers/network_status_controller.js.map +1 -0
- package/dist/controllers/number_input_controller.d.ts +93 -0
- package/dist/controllers/number_input_controller.js +306 -0
- package/dist/controllers/number_input_controller.js.map +1 -0
- package/dist/controllers/otp_controller.d.ts +68 -0
- package/dist/controllers/otp_controller.js +201 -0
- package/dist/controllers/otp_controller.js.map +1 -0
- package/dist/controllers/overflow_indicator_controller.d.ts +62 -0
- package/dist/controllers/overflow_indicator_controller.js +169 -0
- package/dist/controllers/overflow_indicator_controller.js.map +1 -0
- package/dist/controllers/overflow_menu_controller.d.ts +64 -0
- package/dist/controllers/overflow_menu_controller.js +274 -0
- package/dist/controllers/overflow_menu_controller.js.map +1 -0
- package/dist/controllers/pagination_controller.d.ts +69 -0
- package/dist/controllers/pagination_controller.js +89 -0
- package/dist/controllers/pagination_controller.js.map +1 -0
- package/dist/controllers/password_strength_controller.d.ts +67 -0
- package/dist/controllers/password_strength_controller.js +175 -0
- package/dist/controllers/password_strength_controller.js.map +1 -0
- package/dist/controllers/persist_controller.d.ts +63 -0
- package/dist/controllers/persist_controller.js +259 -0
- package/dist/controllers/persist_controller.js.map +1 -0
- package/dist/controllers/popover_controller.d.ts +63 -0
- package/dist/controllers/popover_controller.js +94 -0
- package/dist/controllers/popover_controller.js.map +1 -0
- package/dist/controllers/portal_controller.d.ts +58 -0
- package/dist/controllers/portal_controller.js +63 -0
- package/dist/controllers/portal_controller.js.map +1 -0
- package/dist/controllers/preview_guard_controller.d.ts +47 -0
- package/dist/controllers/preview_guard_controller.js +69 -0
- package/dist/controllers/preview_guard_controller.js.map +1 -0
- package/dist/controllers/progress_controller.d.ts +85 -0
- package/dist/controllers/progress_controller.js +93 -0
- package/dist/controllers/progress_controller.js.map +1 -0
- package/dist/controllers/radio_group_controller.d.ts +49 -0
- package/dist/controllers/radio_group_controller.js +128 -0
- package/dist/controllers/radio_group_controller.js.map +1 -0
- package/dist/controllers/rating_controller.d.ts +82 -0
- package/dist/controllers/rating_controller.js +179 -0
- package/dist/controllers/rating_controller.js.map +1 -0
- package/dist/controllers/relative_time_controller.d.ts +48 -0
- package/dist/controllers/relative_time_controller.js +129 -0
- package/dist/controllers/relative_time_controller.js.map +1 -0
- package/dist/controllers/reset_before_cache_controller.d.ts +54 -0
- package/dist/controllers/reset_before_cache_controller.js +62 -0
- package/dist/controllers/reset_before_cache_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +81 -0
- package/dist/controllers/resizable_controller.js +163 -0
- package/dist/controllers/resizable_controller.js.map +1 -0
- package/dist/controllers/roving_controller.d.ts +63 -0
- package/dist/controllers/roving_controller.js +116 -0
- package/dist/controllers/roving_controller.js.map +1 -0
- package/dist/controllers/scroll_area_controller.d.ts +45 -0
- package/dist/controllers/scroll_area_controller.js +183 -0
- package/dist/controllers/scroll_area_controller.js.map +1 -0
- package/dist/controllers/scroll_visibility_controller.d.ts +72 -0
- package/dist/controllers/scroll_visibility_controller.js +103 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -0
- package/dist/controllers/scrollspy_controller.d.ts +65 -0
- package/dist/controllers/scrollspy_controller.js +171 -0
- package/dist/controllers/scrollspy_controller.js.map +1 -0
- package/dist/controllers/skeleton_controller.d.ts +50 -0
- package/dist/controllers/skeleton_controller.js +125 -0
- package/dist/controllers/skeleton_controller.js.map +1 -0
- package/dist/controllers/slider_controller.d.ts +76 -0
- package/dist/controllers/slider_controller.js +109 -0
- package/dist/controllers/slider_controller.js.map +1 -0
- package/dist/controllers/spinner_controller.d.ts +62 -0
- package/dist/controllers/spinner_controller.js +164 -0
- package/dist/controllers/spinner_controller.js.map +1 -0
- package/dist/controllers/step_indicator_controller.d.ts +55 -0
- package/dist/controllers/step_indicator_controller.js +55 -0
- package/dist/controllers/step_indicator_controller.js.map +1 -0
- package/dist/controllers/stepper_controller.d.ts +66 -0
- package/dist/controllers/stepper_controller.js +78 -0
- package/dist/controllers/stepper_controller.js.map +1 -0
- package/dist/controllers/stick_to_bottom_controller.d.ts +61 -0
- package/dist/controllers/stick_to_bottom_controller.js +100 -0
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -0
- package/dist/controllers/sticky_observer_controller.d.ts +54 -0
- package/dist/controllers/sticky_observer_controller.js +53 -0
- package/dist/controllers/sticky_observer_controller.js.map +1 -0
- package/dist/controllers/submit_once_controller.d.ts +66 -0
- package/dist/controllers/submit_once_controller.js +206 -0
- package/dist/controllers/submit_once_controller.js.map +1 -0
- package/dist/controllers/switch_controller.d.ts +43 -0
- package/dist/controllers/switch_controller.js +50 -0
- package/dist/controllers/switch_controller.js.map +1 -0
- package/dist/controllers/tabs_controller.d.ts +48 -0
- package/dist/controllers/tabs_controller.js +63 -0
- package/dist/controllers/tabs_controller.js.map +1 -0
- package/dist/controllers/textarea_autosize_controller.d.ts +47 -0
- package/dist/controllers/textarea_autosize_controller.js +72 -0
- package/dist/controllers/textarea_autosize_controller.js.map +1 -0
- package/dist/controllers/theme_controller.d.ts +70 -0
- package/dist/controllers/theme_controller.js +154 -0
- package/dist/controllers/theme_controller.js.map +1 -0
- package/dist/controllers/toast_controller.d.ts +123 -0
- package/dist/controllers/toast_controller.js +310 -0
- package/dist/controllers/toast_controller.js.map +1 -0
- package/dist/controllers/toggle_group_controller.d.ts +56 -0
- package/dist/controllers/toggle_group_controller.js +130 -0
- package/dist/controllers/toggle_group_controller.js.map +1 -0
- package/dist/controllers/toolbar_controller.d.ts +59 -0
- package/dist/controllers/toolbar_controller.js +113 -0
- package/dist/controllers/toolbar_controller.js.map +1 -0
- package/dist/controllers/tooltip_controller.d.ts +74 -0
- package/dist/controllers/tooltip_controller.js +165 -0
- package/dist/controllers/tooltip_controller.js.map +1 -0
- package/dist/controllers/transition_controller.d.ts +86 -0
- package/dist/controllers/transition_controller.js +203 -0
- package/dist/controllers/transition_controller.js.map +1 -0
- package/dist/index.d.ts +1931 -0
- package/dist/index.js +12241 -0
- package/dist/index.js.map +1 -0
- package/dist/inspector/cli.d.ts +149 -0
- package/dist/inspector/cli.js +663 -0
- package/dist/inspector/cli.js.map +1 -0
- package/dist/inspector/cli_bin.d.ts +2 -0
- package/dist/inspector/cli_bin.js +665 -0
- package/dist/inspector/cli_bin.js.map +1 -0
- package/dist/inspector/manifest.json +2235 -0
- package/dist/positioning/index.d.ts +226 -0
- package/dist/positioning/index.js +145 -0
- package/dist/positioning/index.js.map +1 -0
- package/package.json +93 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../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":["/**\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"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless character-counter behavior for a text field (no dedicated APG
|
|
5
|
+
* pattern; follows the WCAG 2.2 "status messages" practice, 4.1.3).
|
|
6
|
+
*
|
|
7
|
+
* Markup contract (identifier: `stimeo--character-counter`):
|
|
8
|
+
* <div data-controller="stimeo--character-counter"
|
|
9
|
+
* data-stimeo--character-counter-max-value="140">
|
|
10
|
+
* <textarea data-stimeo--character-counter-target="input"
|
|
11
|
+
* aria-describedby="cc"></textarea>
|
|
12
|
+
* <span id="cc" data-stimeo--character-counter-target="output"
|
|
13
|
+
* aria-live="polite"></span>
|
|
14
|
+
* </div>
|
|
15
|
+
*
|
|
16
|
+
* Watches the field's length, writes the remaining/used count into `output`, and
|
|
17
|
+
* toggles the `data-near-limit` / `data-over-limit` state hooks (plus
|
|
18
|
+
* `aria-invalid` on the field once the limit is exceeded). The field is the
|
|
19
|
+
* `input` target, or — when the controller is attached straight onto an
|
|
20
|
+
* `<input>`/`<textarea>` — the controller element itself.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* Behavior only — the count text is updated, not styled. Outside IME composition
|
|
24
|
+
* the non-text state (`data-*` hooks, `aria-invalid`, and the `change` event)
|
|
25
|
+
* updates **immediately** on every input so styling and consumers stay responsive,
|
|
26
|
+
* while the visible count — in the `aria-live="polite"` region — is written on a
|
|
27
|
+
* short debounce so a screen reader is not flooded during fast typing (it hears the
|
|
28
|
+
* settled count when the user pauses). During IME composition both are deferred to
|
|
29
|
+
* `compositionend`, so they reflect the confirmed characters rather than the
|
|
30
|
+
* intermediate (pre-conversion) kana. The debounce timer is owned by
|
|
31
|
+
* {@link SafeTimeout} and torn down on `disconnect()` (Turbo navigation included);
|
|
32
|
+
* `connect()` re-reads the field so the count is correct after a cache restore.
|
|
33
|
+
*/
|
|
34
|
+
declare class CharacterCounterController extends Controller<HTMLElement> {
|
|
35
|
+
#private;
|
|
36
|
+
static targets: string[];
|
|
37
|
+
static values: {
|
|
38
|
+
max: {
|
|
39
|
+
type: NumberConstructor;
|
|
40
|
+
default: number;
|
|
41
|
+
};
|
|
42
|
+
warnAt: {
|
|
43
|
+
type: NumberConstructor;
|
|
44
|
+
default: number;
|
|
45
|
+
};
|
|
46
|
+
mode: {
|
|
47
|
+
type: StringConstructor;
|
|
48
|
+
default: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
static events: readonly ["change"];
|
|
52
|
+
readonly inputTarget: HTMLInputElement | HTMLTextAreaElement;
|
|
53
|
+
readonly outputTarget: HTMLElement;
|
|
54
|
+
readonly hasInputTarget: boolean;
|
|
55
|
+
readonly hasOutputTarget: boolean;
|
|
56
|
+
maxValue: number;
|
|
57
|
+
warnAtValue: number;
|
|
58
|
+
modeValue: string;
|
|
59
|
+
connect(): void;
|
|
60
|
+
disconnect(): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { CharacterCounterController };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/character_counter_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/safe_timeout.ts
|
|
6
|
+
var TimerRegistry = class {
|
|
7
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
8
|
+
ids = /* @__PURE__ */ new Set();
|
|
9
|
+
/**
|
|
10
|
+
* Cancels a single tracked timer.
|
|
11
|
+
*
|
|
12
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
13
|
+
* registry), so callers can clear defensively without guarding.
|
|
14
|
+
*/
|
|
15
|
+
clear(id) {
|
|
16
|
+
if (this.ids.delete(id)) {
|
|
17
|
+
this.cancel(id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
22
|
+
* to guarantee no timer outlives the element.
|
|
23
|
+
*/
|
|
24
|
+
clearAll() {
|
|
25
|
+
for (const id of this.ids) {
|
|
26
|
+
this.cancel(id);
|
|
27
|
+
}
|
|
28
|
+
this.ids.clear();
|
|
29
|
+
}
|
|
30
|
+
/** Number of timers currently tracked (pending). */
|
|
31
|
+
get size() {
|
|
32
|
+
return this.ids.size;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var SafeTimeout = class extends TimerRegistry {
|
|
36
|
+
/**
|
|
37
|
+
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
38
|
+
*
|
|
39
|
+
* The id is removed from the registry automatically when the timeout fires,
|
|
40
|
+
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
41
|
+
*/
|
|
42
|
+
set(callback, delay) {
|
|
43
|
+
const id = this.schedule(() => {
|
|
44
|
+
this.ids.delete(id);
|
|
45
|
+
callback();
|
|
46
|
+
}, delay);
|
|
47
|
+
this.ids.add(id);
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
schedule(callback, delay) {
|
|
51
|
+
return window.setTimeout(callback, delay);
|
|
52
|
+
}
|
|
53
|
+
cancel(id) {
|
|
54
|
+
window.clearTimeout(id);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/controllers/character_counter_controller.ts
|
|
59
|
+
var ORIGINAL_INVALID = "data-character-counter-original-invalid";
|
|
60
|
+
var CharacterCounterController = class _CharacterCounterController extends Controller {
|
|
61
|
+
static targets = ["input", "output"];
|
|
62
|
+
static values = {
|
|
63
|
+
max: { type: Number, default: 0 },
|
|
64
|
+
warnAt: { type: Number, default: 0 },
|
|
65
|
+
mode: { type: String, default: "remaining" }
|
|
66
|
+
};
|
|
67
|
+
static events = ["change"];
|
|
68
|
+
/** Delay (ms) before the live-region count is written, to throttle SR flooding. */
|
|
69
|
+
static #announceDelay = 200;
|
|
70
|
+
#timeouts = new SafeTimeout();
|
|
71
|
+
#announceId = null;
|
|
72
|
+
/** True while an IME composition is active; intermediate input is skipped. */
|
|
73
|
+
#composing = false;
|
|
74
|
+
#onInput = (event) => {
|
|
75
|
+
if (this.#composing || event.isComposing) return;
|
|
76
|
+
this.#update();
|
|
77
|
+
};
|
|
78
|
+
#onCompositionStart = () => {
|
|
79
|
+
this.#composing = true;
|
|
80
|
+
};
|
|
81
|
+
#onCompositionEnd = () => {
|
|
82
|
+
this.#composing = false;
|
|
83
|
+
this.#update();
|
|
84
|
+
};
|
|
85
|
+
connect() {
|
|
86
|
+
const field = this.#field;
|
|
87
|
+
if (!field) return;
|
|
88
|
+
field.addEventListener("input", this.#onInput);
|
|
89
|
+
field.addEventListener("compositionstart", this.#onCompositionStart);
|
|
90
|
+
field.addEventListener("compositionend", this.#onCompositionEnd);
|
|
91
|
+
this.#update({ announce: false });
|
|
92
|
+
}
|
|
93
|
+
disconnect() {
|
|
94
|
+
const field = this.#field;
|
|
95
|
+
field?.removeEventListener("input", this.#onInput);
|
|
96
|
+
field?.removeEventListener("compositionstart", this.#onCompositionStart);
|
|
97
|
+
field?.removeEventListener("compositionend", this.#onCompositionEnd);
|
|
98
|
+
this.#timeouts.clearAll();
|
|
99
|
+
this.#announceId = null;
|
|
100
|
+
this.#composing = false;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Recomputes length-derived state. Non-text state (data hooks, `aria-invalid`)
|
|
104
|
+
* and the `change` event apply immediately; the live-region count text is
|
|
105
|
+
* debounced unless `announce` is `false` (initial render).
|
|
106
|
+
*/
|
|
107
|
+
#update(options = {}) {
|
|
108
|
+
const field = this.#field;
|
|
109
|
+
if (!field) return;
|
|
110
|
+
const length = field.value.length;
|
|
111
|
+
const hasLimit = this.maxValue > 0;
|
|
112
|
+
const remaining = hasLimit ? this.maxValue - length : null;
|
|
113
|
+
const over = hasLimit && length > this.maxValue;
|
|
114
|
+
const near = hasLimit && this.warnAtValue > 0 && !over && remaining !== null && remaining <= this.warnAtValue;
|
|
115
|
+
this.#toggle("data-over-limit", over);
|
|
116
|
+
this.#toggle("data-near-limit", near);
|
|
117
|
+
if (hasLimit && over) {
|
|
118
|
+
if (!field.hasAttribute(ORIGINAL_INVALID)) {
|
|
119
|
+
field.setAttribute(ORIGINAL_INVALID, field.getAttribute("aria-invalid") ?? "");
|
|
120
|
+
}
|
|
121
|
+
field.setAttribute("aria-invalid", "true");
|
|
122
|
+
} else if (field.hasAttribute(ORIGINAL_INVALID)) {
|
|
123
|
+
const original = field.getAttribute(ORIGINAL_INVALID);
|
|
124
|
+
if (original) {
|
|
125
|
+
field.setAttribute("aria-invalid", original);
|
|
126
|
+
} else {
|
|
127
|
+
field.removeAttribute("aria-invalid");
|
|
128
|
+
}
|
|
129
|
+
field.removeAttribute(ORIGINAL_INVALID);
|
|
130
|
+
}
|
|
131
|
+
const text = this.#format(length, remaining);
|
|
132
|
+
if (options.announce === false) {
|
|
133
|
+
this.#writeOutput(text);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.dispatch("change", { detail: { length, remaining, over } });
|
|
137
|
+
if (this.#announceId !== null) this.#timeouts.clear(this.#announceId);
|
|
138
|
+
this.#announceId = this.#timeouts.set(() => {
|
|
139
|
+
this.#writeOutput(text);
|
|
140
|
+
this.#announceId = null;
|
|
141
|
+
}, _CharacterCounterController.#announceDelay);
|
|
142
|
+
}
|
|
143
|
+
/** Builds the count text for the active `mode`. */
|
|
144
|
+
#format(length, remaining) {
|
|
145
|
+
if (remaining === null) return String(length);
|
|
146
|
+
switch (this.modeValue) {
|
|
147
|
+
case "used":
|
|
148
|
+
return String(length);
|
|
149
|
+
case "both":
|
|
150
|
+
return `${length}/${this.maxValue}`;
|
|
151
|
+
default:
|
|
152
|
+
return String(remaining);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
#writeOutput(text) {
|
|
156
|
+
if (this.hasOutputTarget) this.outputTarget.textContent = text;
|
|
157
|
+
}
|
|
158
|
+
/** Sets a presence-style boolean data hook (attribute present when `on`). */
|
|
159
|
+
#toggle(name, on) {
|
|
160
|
+
if (on) {
|
|
161
|
+
this.element.setAttribute(name, "true");
|
|
162
|
+
} else {
|
|
163
|
+
this.element.removeAttribute(name);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** The watched field: the `input` target, or the element itself when it is one. */
|
|
167
|
+
get #field() {
|
|
168
|
+
if (this.hasInputTarget) return this.inputTarget;
|
|
169
|
+
const el = this.element;
|
|
170
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
171
|
+
return el;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export { CharacterCounterController };
|
|
178
|
+
//# sourceMappingURL=character_counter_controller.js.map
|
|
179
|
+
//# sourceMappingURL=character_counter_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/character_counter_controller.ts"],"names":[],"mappings":"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","file":"character_counter_controller.js","sourcesContent":["/**\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 { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Parks the field's original `aria-invalid` while we override it for over-limit. */\nconst ORIGINAL_INVALID = \"data-character-counter-original-invalid\";\n\n/**\n * Headless character-counter behavior for a text field (no dedicated APG\n * pattern; follows the WCAG 2.2 \"status messages\" practice, 4.1.3).\n *\n * Markup contract (identifier: `stimeo--character-counter`):\n * <div data-controller=\"stimeo--character-counter\"\n * data-stimeo--character-counter-max-value=\"140\">\n * <textarea data-stimeo--character-counter-target=\"input\"\n * aria-describedby=\"cc\"></textarea>\n * <span id=\"cc\" data-stimeo--character-counter-target=\"output\"\n * aria-live=\"polite\"></span>\n * </div>\n *\n * Watches the field's length, writes the remaining/used count into `output`, and\n * toggles the `data-near-limit` / `data-over-limit` state hooks (plus\n * `aria-invalid` on the field once the limit is exceeded). The field is the\n * `input` target, or — when the controller is attached straight onto an\n * `<input>`/`<textarea>` — the controller element itself.\n *\n * @remarks\n * Behavior only — the count text is updated, not styled. Outside IME composition\n * the non-text state (`data-*` hooks, `aria-invalid`, and the `change` event)\n * updates **immediately** on every input so styling and consumers stay responsive,\n * while the visible count — in the `aria-live=\"polite\"` region — is written on a\n * short debounce so a screen reader is not flooded during fast typing (it hears the\n * settled count when the user pauses). During IME composition both are deferred to\n * `compositionend`, so they reflect the confirmed characters rather than the\n * intermediate (pre-conversion) kana. The debounce timer is owned by\n * {@link SafeTimeout} and torn down on `disconnect()` (Turbo navigation included);\n * `connect()` re-reads the field so the count is correct after a cache restore.\n */\nexport class CharacterCounterController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"output\"];\n static override values = {\n max: { type: Number, default: 0 },\n warnAt: { type: Number, default: 0 },\n mode: { type: String, default: \"remaining\" },\n };\n static events = [\"change\"] as const;\n\n declare readonly inputTarget: HTMLInputElement | HTMLTextAreaElement;\n declare readonly outputTarget: HTMLElement;\n declare readonly hasInputTarget: boolean;\n declare readonly hasOutputTarget: boolean;\n\n declare maxValue: number;\n declare warnAtValue: number;\n declare modeValue: string;\n\n /** Delay (ms) before the live-region count is written, to throttle SR flooding. */\n static readonly #announceDelay = 200;\n\n readonly #timeouts = new SafeTimeout();\n #announceId: number | null = null;\n\n /** True while an IME composition is active; intermediate input is skipped. */\n #composing = false;\n\n readonly #onInput = (event: Event): void => {\n // During IME composition the field holds unconverted text; defer the count to\n // `compositionend` so it reflects the confirmed characters, not each keystroke.\n if (this.#composing || (event as InputEvent).isComposing) return;\n this.#update();\n };\n\n readonly #onCompositionStart = (): void => {\n this.#composing = true;\n };\n\n readonly #onCompositionEnd = (): void => {\n this.#composing = false;\n this.#update();\n };\n\n override connect(): void {\n const field = this.#field;\n if (!field) return;\n field.addEventListener(\"input\", this.#onInput);\n field.addEventListener(\"compositionstart\", this.#onCompositionStart);\n field.addEventListener(\"compositionend\", this.#onCompositionEnd);\n // Initial render is synchronous (no announce debounce): reflect the current\n // value on connect/cache-restore without queueing a screen-reader message.\n this.#update({ announce: false });\n }\n\n override disconnect(): void {\n const field = this.#field;\n field?.removeEventListener(\"input\", this.#onInput);\n field?.removeEventListener(\"compositionstart\", this.#onCompositionStart);\n field?.removeEventListener(\"compositionend\", this.#onCompositionEnd);\n this.#timeouts.clearAll();\n this.#announceId = null;\n // Reset so a same-instance reconnect (e.g. Turbo cache restore mid-composition)\n // never starts with input suppressed.\n this.#composing = false;\n }\n\n /**\n * Recomputes length-derived state. Non-text state (data hooks, `aria-invalid`)\n * and the `change` event apply immediately; the live-region count text is\n * debounced unless `announce` is `false` (initial render).\n */\n #update(options: { announce?: boolean } = {}): void {\n const field = this.#field;\n if (!field) return;\n\n const length = field.value.length;\n const hasLimit = this.maxValue > 0;\n const remaining = hasLimit ? this.maxValue - length : null;\n const over = hasLimit && length > this.maxValue;\n const near =\n hasLimit &&\n this.warnAtValue > 0 &&\n !over &&\n remaining !== null &&\n remaining <= this.warnAtValue;\n\n this.#toggle(\"data-over-limit\", over);\n this.#toggle(\"data-near-limit\", near);\n // `aria-invalid` is a shared attribute (Form Field / server validation also use\n // it), so we never clobber an authored value: park the original when flagging\n // over-limit and restore it once back within the limit.\n if (hasLimit && over) {\n if (!field.hasAttribute(ORIGINAL_INVALID)) {\n field.setAttribute(ORIGINAL_INVALID, field.getAttribute(\"aria-invalid\") ?? \"\");\n }\n field.setAttribute(\"aria-invalid\", \"true\");\n } else if (field.hasAttribute(ORIGINAL_INVALID)) {\n const original = field.getAttribute(ORIGINAL_INVALID);\n if (original) {\n field.setAttribute(\"aria-invalid\", original);\n } else {\n field.removeAttribute(\"aria-invalid\");\n }\n field.removeAttribute(ORIGINAL_INVALID);\n }\n\n const text = this.#format(length, remaining);\n if (options.announce === false) {\n // Initial render (connect / cache-restore): reflect the current state without\n // emitting a change event or queueing a screen-reader announcement.\n this.#writeOutput(text);\n return;\n }\n\n this.dispatch(\"change\", { detail: { length, remaining, over } });\n if (this.#announceId !== null) this.#timeouts.clear(this.#announceId);\n this.#announceId = this.#timeouts.set(() => {\n this.#writeOutput(text);\n this.#announceId = null;\n }, CharacterCounterController.#announceDelay);\n }\n\n /** Builds the count text for the active `mode`. */\n #format(length: number, remaining: number | null): string {\n if (remaining === null) return String(length); // no limit → used count only\n switch (this.modeValue) {\n case \"used\":\n return String(length);\n case \"both\":\n return `${length}/${this.maxValue}`;\n default:\n return String(remaining); // \"remaining\"\n }\n }\n\n #writeOutput(text: string): void {\n if (this.hasOutputTarget) this.outputTarget.textContent = text;\n }\n\n /** Sets a presence-style boolean data hook (attribute present when `on`). */\n #toggle(name: string, on: boolean): void {\n if (on) {\n this.element.setAttribute(name, \"true\");\n } else {\n this.element.removeAttribute(name);\n }\n }\n\n /** The watched field: the `input` target, or the element itself when it is one. */\n get #field(): HTMLInputElement | HTMLTextAreaElement | null {\n if (this.hasInputTarget) return this.inputTarget;\n const el: HTMLElement = this.element;\n if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {\n return el;\n }\n return null;\n }\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless, accessible checkbox behavior (including the tri-state / parent-child
|
|
5
|
+
* "select all" pattern).
|
|
6
|
+
*
|
|
7
|
+
* Markup contract (identifier: `stimeo--checkbox`):
|
|
8
|
+
* <fieldset data-controller="stimeo--checkbox" role="group" aria-labelledby="all-label">
|
|
9
|
+
* <label id="all-label">
|
|
10
|
+
* <input type="checkbox" data-stimeo--checkbox-target="parent"
|
|
11
|
+
* data-action="change->stimeo--checkbox#onParentChange" /> Select all
|
|
12
|
+
* </label>
|
|
13
|
+
* <label><input type="checkbox" data-stimeo--checkbox-target="child"
|
|
14
|
+
* data-action="change->stimeo--checkbox#onChildChange" /> Item A</label>
|
|
15
|
+
* <!-- more children -->
|
|
16
|
+
* </fieldset>
|
|
17
|
+
*
|
|
18
|
+
* Implements the WAI-ARIA APG **Checkbox** pattern. Native `<input type="checkbox">`
|
|
19
|
+
* is used throughout (so Space toggle, focus order, and `mixed` exposure are the
|
|
20
|
+
* browser's); the controller adds what HTML cannot express: the parent's
|
|
21
|
+
* `indeterminate` property (announced as `mixed`) derived from its children, and
|
|
22
|
+
* the cascade from a parent toggle to its children.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* Behavior only — the check mark and any "mixed" affordance are the consumer's
|
|
26
|
+
* CSS (keyed off `:checked` / `:indeterminate` / `data-state`). A lone tri-state
|
|
27
|
+
* checkbox can use just `parent` and drive `indeterminate` externally.
|
|
28
|
+
*
|
|
29
|
+
* Behavior provided:
|
|
30
|
+
* - Parent toggle checks/unchecks every child and clears its own `indeterminate`.
|
|
31
|
+
* - A child change recomputes the parent: all → checked, none → unchecked,
|
|
32
|
+
* some → `indeterminate`.
|
|
33
|
+
* - The aggregate (`all` / `partial` / `none`) is mirrored to `data-state` on the
|
|
34
|
+
* root, and `stimeo--checkbox:change` is dispatched on every change.
|
|
35
|
+
*/
|
|
36
|
+
declare class CheckboxController extends Controller<HTMLElement> {
|
|
37
|
+
#private;
|
|
38
|
+
static targets: string[];
|
|
39
|
+
static actions: readonly ["onChildChange", "onParentChange"];
|
|
40
|
+
static events: readonly ["change"];
|
|
41
|
+
readonly parentTarget: HTMLInputElement;
|
|
42
|
+
readonly hasParentTarget: boolean;
|
|
43
|
+
readonly childTargets: HTMLInputElement[];
|
|
44
|
+
/** Reflects the initial aggregate (e.g. from server-rendered child states). */
|
|
45
|
+
connect(): void;
|
|
46
|
+
/** Cascades the parent's state to every child. Bound via `data-action` (change). */
|
|
47
|
+
onParentChange(): void;
|
|
48
|
+
/** Recomputes the parent from its children. Bound via `data-action` (change). */
|
|
49
|
+
onChildChange(): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { CheckboxController };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/checkbox_controller.ts
|
|
4
|
+
var CheckboxController = class extends Controller {
|
|
5
|
+
static targets = ["parent", "child"];
|
|
6
|
+
static actions = ["onChildChange", "onParentChange"];
|
|
7
|
+
static events = ["change"];
|
|
8
|
+
/** Reflects the initial aggregate (e.g. from server-rendered child states). */
|
|
9
|
+
connect() {
|
|
10
|
+
if (this.childTargets.length > 0) {
|
|
11
|
+
this.#syncFromChildren(false);
|
|
12
|
+
} else {
|
|
13
|
+
this.element.setAttribute("data-state", this.#aggregate());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Cascades the parent's state to every child. Bound via `data-action` (change). */
|
|
17
|
+
onParentChange() {
|
|
18
|
+
if (!this.hasParentTarget) return;
|
|
19
|
+
const checked = this.parentTarget.checked;
|
|
20
|
+
for (const child of this.childTargets) {
|
|
21
|
+
child.checked = checked;
|
|
22
|
+
}
|
|
23
|
+
this.parentTarget.indeterminate = false;
|
|
24
|
+
const state = checked ? "all" : "none";
|
|
25
|
+
this.element.setAttribute("data-state", state);
|
|
26
|
+
this.dispatch("change", { detail: { checked, indeterminate: false, state } });
|
|
27
|
+
}
|
|
28
|
+
/** Recomputes the parent from its children. Bound via `data-action` (change). */
|
|
29
|
+
onChildChange() {
|
|
30
|
+
this.#syncFromChildren(true);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Derives the parent's `checked`/`indeterminate` and the root `data-state` from
|
|
34
|
+
* the children, optionally dispatching `change`.
|
|
35
|
+
*/
|
|
36
|
+
#syncFromChildren(dispatch) {
|
|
37
|
+
const state = this.#aggregate();
|
|
38
|
+
if (this.hasParentTarget) {
|
|
39
|
+
this.parentTarget.checked = state === "all";
|
|
40
|
+
this.parentTarget.indeterminate = state === "partial";
|
|
41
|
+
}
|
|
42
|
+
this.element.setAttribute("data-state", state);
|
|
43
|
+
if (dispatch) {
|
|
44
|
+
this.dispatch("change", {
|
|
45
|
+
detail: {
|
|
46
|
+
checked: this.hasParentTarget ? this.parentTarget.checked : state === "all",
|
|
47
|
+
indeterminate: this.hasParentTarget ? this.parentTarget.indeterminate : false,
|
|
48
|
+
state
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Computes the aggregate state. With children it counts them; with none it
|
|
55
|
+
* reads the parent so a lone tri-state checkbox still reports a state without
|
|
56
|
+
* its externally-set `indeterminate` being clobbered.
|
|
57
|
+
*/
|
|
58
|
+
#aggregate() {
|
|
59
|
+
const children = this.childTargets;
|
|
60
|
+
if (children.length === 0) {
|
|
61
|
+
if (this.hasParentTarget && this.parentTarget.indeterminate) return "partial";
|
|
62
|
+
return this.hasParentTarget && this.parentTarget.checked ? "all" : "none";
|
|
63
|
+
}
|
|
64
|
+
const checked = children.filter((child) => child.checked).length;
|
|
65
|
+
if (checked === 0) return "none";
|
|
66
|
+
if (checked === children.length) return "all";
|
|
67
|
+
return "partial";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export { CheckboxController };
|
|
72
|
+
//# sourceMappingURL=checkbox_controller.js.map
|
|
73
|
+
//# sourceMappingURL=checkbox_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/controllers/checkbox_controller.ts"],"names":[],"mappings":"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","file":"checkbox_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Aggregate selection state of a parent/child checkbox group. */\ntype CheckboxState = \"all\" | \"partial\" | \"none\";\n\n/**\n * Headless, accessible checkbox behavior (including the tri-state / parent-child\n * \"select all\" pattern).\n *\n * Markup contract (identifier: `stimeo--checkbox`):\n * <fieldset data-controller=\"stimeo--checkbox\" role=\"group\" aria-labelledby=\"all-label\">\n * <label id=\"all-label\">\n * <input type=\"checkbox\" data-stimeo--checkbox-target=\"parent\"\n * data-action=\"change->stimeo--checkbox#onParentChange\" /> Select all\n * </label>\n * <label><input type=\"checkbox\" data-stimeo--checkbox-target=\"child\"\n * data-action=\"change->stimeo--checkbox#onChildChange\" /> Item A</label>\n * <!-- more children -->\n * </fieldset>\n *\n * Implements the WAI-ARIA APG **Checkbox** pattern. Native `<input type=\"checkbox\">`\n * is used throughout (so Space toggle, focus order, and `mixed` exposure are the\n * browser's); the controller adds what HTML cannot express: the parent's\n * `indeterminate` property (announced as `mixed`) derived from its children, and\n * the cascade from a parent toggle to its children.\n *\n * @remarks\n * Behavior only — the check mark and any \"mixed\" affordance are the consumer's\n * CSS (keyed off `:checked` / `:indeterminate` / `data-state`). A lone tri-state\n * checkbox can use just `parent` and drive `indeterminate` externally.\n *\n * Behavior provided:\n * - Parent toggle checks/unchecks every child and clears its own `indeterminate`.\n * - A child change recomputes the parent: all → checked, none → unchecked,\n * some → `indeterminate`.\n * - The aggregate (`all` / `partial` / `none`) is mirrored to `data-state` on the\n * root, and `stimeo--checkbox:change` is dispatched on every change.\n */\nexport class CheckboxController extends Controller<HTMLElement> {\n static override targets = [\"parent\", \"child\"];\n static actions = [\"onChildChange\", \"onParentChange\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly parentTarget: HTMLInputElement;\n declare readonly hasParentTarget: boolean;\n declare readonly childTargets: HTMLInputElement[];\n\n /** Reflects the initial aggregate (e.g. from server-rendered child states). */\n override connect(): void {\n if (this.childTargets.length > 0) {\n this.#syncFromChildren(false);\n } else {\n this.element.setAttribute(\"data-state\", this.#aggregate());\n }\n }\n\n /** Cascades the parent's state to every child. Bound via `data-action` (change). */\n onParentChange(): void {\n if (!this.hasParentTarget) return;\n const checked = this.parentTarget.checked;\n for (const child of this.childTargets) {\n child.checked = checked;\n }\n this.parentTarget.indeterminate = false;\n const state: CheckboxState = checked ? \"all\" : \"none\";\n this.element.setAttribute(\"data-state\", state);\n this.dispatch(\"change\", { detail: { checked, indeterminate: false, state } });\n }\n\n /** Recomputes the parent from its children. Bound via `data-action` (change). */\n onChildChange(): void {\n this.#syncFromChildren(true);\n }\n\n /**\n * Derives the parent's `checked`/`indeterminate` and the root `data-state` from\n * the children, optionally dispatching `change`.\n */\n #syncFromChildren(dispatch: boolean): void {\n const state = this.#aggregate();\n if (this.hasParentTarget) {\n this.parentTarget.checked = state === \"all\";\n this.parentTarget.indeterminate = state === \"partial\";\n }\n this.element.setAttribute(\"data-state\", state);\n if (dispatch) {\n this.dispatch(\"change\", {\n detail: {\n checked: this.hasParentTarget ? this.parentTarget.checked : state === \"all\",\n indeterminate: this.hasParentTarget ? this.parentTarget.indeterminate : false,\n state,\n },\n });\n }\n }\n\n /**\n * Computes the aggregate state. With children it counts them; with none it\n * reads the parent so a lone tri-state checkbox still reports a state without\n * its externally-set `indeterminate` being clobbered.\n */\n #aggregate(): CheckboxState {\n const children = this.childTargets;\n if (children.length === 0) {\n if (this.hasParentTarget && this.parentTarget.indeterminate) return \"partial\";\n return this.hasParentTarget && this.parentTarget.checked ? \"all\" : \"none\";\n }\n const checked = children.filter((child) => child.checked).length;\n if (checked === 0) return \"none\";\n if (checked === children.length) return \"all\";\n return \"partial\";\n }\n}\n"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless, accessible combobox behavior (list autocomplete).
|
|
5
|
+
*
|
|
6
|
+
* Markup contract (identifier: `stimeo--combobox`):
|
|
7
|
+
* <div data-controller="stimeo--combobox">
|
|
8
|
+
* <input type="text" role="combobox" aria-expanded="false"
|
|
9
|
+
* aria-autocomplete="list" aria-controls="listbox"
|
|
10
|
+
* data-stimeo--combobox-target="input"
|
|
11
|
+
* data-action="input->stimeo--combobox#filter
|
|
12
|
+
* keydown->stimeo--combobox#onKeydown
|
|
13
|
+
* focus->stimeo--combobox#open
|
|
14
|
+
* click->stimeo--combobox#open" />
|
|
15
|
+
* <ul id="listbox" role="listbox" data-stimeo--combobox-target="list" hidden>
|
|
16
|
+
* <li role="option" id="opt-apple" data-value="apple"
|
|
17
|
+
* data-stimeo--combobox-target="option"
|
|
18
|
+
* data-action="click->stimeo--combobox#selectByClick">Apple</li>
|
|
19
|
+
* <!-- more options -->
|
|
20
|
+
* </ul>
|
|
21
|
+
* </div>
|
|
22
|
+
*
|
|
23
|
+
* Implements the WAI-ARIA APG **Combobox** pattern with a listbox popup and
|
|
24
|
+
* list-autocomplete. Focus stays in the input; the active option is tracked with
|
|
25
|
+
* `aria-activedescendant` rather than by moving DOM focus.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* Behavior only. Options are authored in the DOM; the controller filters them by
|
|
29
|
+
* toggling each option's `hidden` attribute (case-insensitive substring match on
|
|
30
|
+
* its text). The consumer owns styling, typically keyed off `[aria-selected]`.
|
|
31
|
+
* When an open listbox has no matching options, the root element gets
|
|
32
|
+
* `data-stimeo--combobox-empty` so the consumer can style the empty state (hide
|
|
33
|
+
* the list, show a "no results" node, …) — the library imposes no visuals.
|
|
34
|
+
*
|
|
35
|
+
* Behavior provided:
|
|
36
|
+
* - Typing filters the options and opens the listbox.
|
|
37
|
+
* - Focusing or clicking the input opens the listbox, re-filtered against the
|
|
38
|
+
* current value (so re-opening with a non-matching value keeps the empty state).
|
|
39
|
+
* - `ArrowDown`/`ArrowUp` move the active option (wrapping); `Enter` selects it;
|
|
40
|
+
* `Escape` closes the listbox; `Home`/`End` jump to the first/last visible
|
|
41
|
+
* option.
|
|
42
|
+
* - Selecting an option fills the input (with the option's `data-value` if set,
|
|
43
|
+
* otherwise its text) and closes the listbox.
|
|
44
|
+
* - A click outside the combobox closes the listbox.
|
|
45
|
+
*/
|
|
46
|
+
declare class ComboboxController extends Controller<HTMLElement> {
|
|
47
|
+
#private;
|
|
48
|
+
static targets: string[];
|
|
49
|
+
static actions: readonly ["close", "filter", "onKeydown", "open", "selectByClick"];
|
|
50
|
+
static events: readonly ["selected"];
|
|
51
|
+
readonly inputTarget: HTMLInputElement;
|
|
52
|
+
readonly listTarget: HTMLElement;
|
|
53
|
+
readonly optionTargets: HTMLElement[];
|
|
54
|
+
readonly hasInputTarget: boolean;
|
|
55
|
+
readonly hasListTarget: boolean;
|
|
56
|
+
/** Starts closed with no active option and registers the outside-click listener. */
|
|
57
|
+
connect(): void;
|
|
58
|
+
/** Removes the document-level listener registered in {@link connect}. */
|
|
59
|
+
disconnect(): void;
|
|
60
|
+
/** Filters options by the current input value and opens the listbox. */
|
|
61
|
+
filter(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Opens the listbox, re-filtering the options against the current input value
|
|
64
|
+
* so the visible options and empty state always match what is typed (e.g.
|
|
65
|
+
* re-opening with a stale non-matching value still surfaces the empty state).
|
|
66
|
+
*/
|
|
67
|
+
open(): void;
|
|
68
|
+
/** Closes the listbox, clears the active option, and updates ARIA state. */
|
|
69
|
+
close(): void;
|
|
70
|
+
/** Routes keyboard interaction per the APG combobox model. */
|
|
71
|
+
onKeydown(event: KeyboardEvent): void;
|
|
72
|
+
/** Selects the clicked option. Bound via `data-action` (click). */
|
|
73
|
+
selectByClick(event: Event): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { ComboboxController };
|