stimeo-ui 0.2.0 → 0.2.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/CHANGELOG.md +59 -0
- package/dist/controllers/accordion_controller.d.ts +3 -2
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +64 -0
- package/dist/controllers/alert_dialog_controller.js +318 -0
- package/dist/controllers/alert_dialog_controller.js.map +1 -0
- package/dist/controllers/carousel_controller.d.ts +100 -0
- package/dist/controllers/carousel_controller.js +272 -0
- package/dist/controllers/carousel_controller.js.map +1 -0
- package/dist/controllers/clipboard_controller.d.ts +72 -0
- package/dist/controllers/clipboard_controller.js +144 -0
- package/dist/controllers/clipboard_controller.js.map +1 -0
- package/dist/controllers/collapsible_controller.d.ts +70 -0
- package/dist/controllers/collapsible_controller.js +327 -0
- package/dist/controllers/collapsible_controller.js.map +1 -0
- package/dist/controllers/color_picker_controller.d.ts +76 -0
- package/dist/controllers/color_picker_controller.js +213 -0
- package/dist/controllers/color_picker_controller.js.map +1 -0
- package/dist/controllers/count_up_controller.js +8 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +72 -0
- package/dist/controllers/currency_input_controller.js +147 -0
- package/dist/controllers/currency_input_controller.js.map +1 -0
- package/dist/controllers/data_grid_controller.d.ts +71 -0
- package/dist/controllers/data_grid_controller.js +168 -0
- package/dist/controllers/data_grid_controller.js.map +1 -0
- package/dist/controllers/date_range_picker_controller.d.ts +87 -0
- package/dist/controllers/date_range_picker_controller.js +417 -0
- package/dist/controllers/date_range_picker_controller.js.map +1 -0
- package/dist/controllers/dismissible_controller.d.ts +57 -0
- package/dist/controllers/dismissible_controller.js +117 -0
- package/dist/controllers/dismissible_controller.js.map +1 -0
- package/dist/controllers/drawer_controller.d.ts +96 -0
- package/dist/controllers/drawer_controller.js +630 -0
- package/dist/controllers/drawer_controller.js.map +1 -0
- package/dist/controllers/editable_controller.d.ts +73 -0
- package/dist/controllers/editable_controller.js +168 -0
- package/dist/controllers/editable_controller.js.map +1 -0
- package/dist/controllers/file_dropzone_controller.d.ts +92 -0
- package/dist/controllers/file_dropzone_controller.js +165 -0
- package/dist/controllers/file_dropzone_controller.js.map +1 -0
- package/dist/controllers/filter_controller.d.ts +60 -0
- package/dist/controllers/filter_controller.js +86 -0
- package/dist/controllers/filter_controller.js.map +1 -0
- package/dist/controllers/flash_controller.js +36 -5
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +6 -4
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +41 -18
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +33 -11
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +54 -0
- package/dist/controllers/masonry_controller.js +142 -0
- package/dist/controllers/masonry_controller.js.map +1 -0
- package/dist/controllers/menubar_controller.d.ts +62 -0
- package/dist/controllers/menubar_controller.js +433 -0
- package/dist/controllers/menubar_controller.js.map +1 -0
- package/dist/controllers/multi_select_controller.d.ts +115 -0
- package/dist/controllers/multi_select_controller.js +472 -0
- package/dist/controllers/multi_select_controller.js.map +1 -0
- package/dist/controllers/navigation_menu_controller.d.ts +99 -0
- package/dist/controllers/navigation_menu_controller.js +384 -0
- package/dist/controllers/navigation_menu_controller.js.map +1 -0
- package/dist/controllers/overflow_indicator_controller.d.ts +12 -4
- package/dist/controllers/overflow_indicator_controller.js +178 -27
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +49 -0
- package/dist/controllers/password_reveal_controller.js +117 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -0
- package/dist/controllers/range_slider_controller.d.ts +87 -0
- package/dist/controllers/range_slider_controller.js +166 -0
- package/dist/controllers/range_slider_controller.js.map +1 -0
- package/dist/controllers/read_more_controller.d.ts +52 -0
- package/dist/controllers/read_more_controller.js +194 -0
- package/dist/controllers/read_more_controller.js.map +1 -0
- package/dist/controllers/scroll_area_controller.js +15 -2
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +54 -0
- package/dist/controllers/scroll_restore_controller.js +93 -0
- package/dist/controllers/scroll_restore_controller.js.map +1 -0
- package/dist/controllers/scroll_visibility_controller.js +8 -4
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js +33 -11
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +54 -0
- package/dist/controllers/separator_controller.js +87 -0
- package/dist/controllers/separator_controller.js.map +1 -0
- package/dist/controllers/sidebar_controller.d.ts +107 -0
- package/dist/controllers/sidebar_controller.js +761 -0
- package/dist/controllers/sidebar_controller.js.map +1 -0
- package/dist/controllers/stepper_controller.d.ts +4 -0
- package/dist/controllers/stepper_controller.js +28 -12
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.js +8 -4
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +17 -4
- package/dist/controllers/sticky_observer_controller.js +88 -20
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +3 -2
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +91 -0
- package/dist/controllers/tags_input_controller.js +275 -0
- package/dist/controllers/tags_input_controller.js.map +1 -0
- package/dist/controllers/theme_controller.js +20 -10
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +67 -0
- package/dist/controllers/time_picker_controller.js +212 -0
- package/dist/controllers/time_picker_controller.js.map +1 -0
- package/dist/controllers/toast_controller.d.ts +1 -9
- package/dist/controllers/toast_controller.js +36 -9
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +14 -8
- package/dist/controllers/transition_controller.js +153 -38
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +54 -0
- package/dist/controllers/tree_view_controller.js +275 -0
- package/dist/controllers/tree_view_controller.js.map +1 -0
- package/dist/index.d.ts +27 -1818
- package/dist/index.js +811 -295
- package/dist/index.js.map +1 -1
- package/dist/inspector/examples.json +2 -2
- package/dist/inspector/manifest.json +15 -3
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/aria_ids.ts","../../src/utils/composition_tracker.ts","../../src/utils/option_scroll.ts","../../src/utils/roving_tabindex.ts","../../src/controllers/multi_select_controller.ts"],"names":[],"mappings":"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","file":"multi_select_controller.js","sourcesContent":["/**\n * Minimal id primitives for wiring ARIA relationships.\n *\n * Accessible relationships such as `aria-describedby`, `aria-errormessage`,\n * `aria-labelledby`, and `aria-activedescendant` reference their targets by DOM\n * `id`. When a controller must establish such a relationship but the consumer's\n * markup left the target without an `id`, it needs to mint one that is stable for\n * the page's lifetime and guaranteed not to collide with another generated id.\n *\n * These helpers own *only* id generation/assignment. The linking policy (which\n * attribute, what order, when to add or remove it) stays in each controller —\n * this deliberately avoids a premature generic \"ARIA linker\" abstraction.\n */\n\n/**\n * Monotonic counter backing {@link uniqueId}. Module-scoped so every generated\n * id is unique across all controller instances sharing this module in a document.\n */\nlet counter = 0;\n\n/**\n * Returns a unique, DOM-id-safe string of the form `` `${prefix}-${n}` `` where\n * `n` increases on each call.\n *\n * The counter alone guarantees uniqueness against other *generated* ids, but a\n * consumer-authored element could already own `` `${prefix}-${n}` ``. When a\n * `document` is available the candidate is therefore advanced until no element\n * with that id exists, so a generated id never collides with author markup\n * either — the minimal \"id registry\" guarantee these helpers exist to provide.\n *\n * @param prefix - Leading segment of the id (e.g. a controller's identifier).\n * Defaults to `\"stimeo\"`.\n */\nexport function uniqueId(prefix = \"stimeo\"): string {\n let candidate: string;\n do {\n counter += 1;\n candidate = `${prefix}-${counter}`;\n } while (typeof document !== \"undefined\" && document.getElementById(candidate) !== null);\n return candidate;\n}\n\n/**\n * Returns `element`'s existing `id`, or assigns it a freshly generated\n * {@link uniqueId} (with the given `prefix`) and returns that. Idempotent: an\n * element that already has an id keeps it, so consumer-authored ids are never\n * overwritten.\n *\n * @param element - The element to read or stamp an `id` onto.\n * @param prefix - Prefix forwarded to {@link uniqueId} when one must be generated.\n * @returns The element's id (existing or newly assigned).\n */\nexport function ensureId(element: Element, prefix = \"stimeo\"): string {\n if (element.id) return element.id;\n const id = uniqueId(prefix);\n element.id = id;\n return id;\n}\n","/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\n };\n}\n","/**\n * Scroll-follow for virtually-focused listbox options.\n *\n * Listbox-style widgets that track their active option with\n * `aria-activedescendant` keep DOM focus on the input/trigger, so the\n * browser's native \"scroll the focused element into view\" never runs — in a\n * scrollable list (`max-height` + `overflow`) the active option can walk right\n * out of sight on ArrowDown/ArrowUp. This helper keeps it visible.\n *\n * It adjusts the LIST's own `scrollTop` only — deliberately not\n * `scrollIntoView({ block: \"nearest\" })`, which may also scroll ancestor\n * scrolling boxes (the page) when a floating popup pokes past a viewport\n * edge, desyncing anchored/marker-composed placements. (Command-palette keeps\n * its `scrollIntoView` because its page is scroll-locked behind the modal;\n * page-floating popups must use this helper instead.)\n */\n\n/**\n * Scrolls `option` into view within `list` by minimally adjusting\n * `list.scrollTop`. No-ops when the list does not actually scroll. Never\n * touches any other scrolling box.\n *\n * @param list - The scrollable listbox container.\n * @param option - The (virtually focused) option to keep visible.\n */\nexport function scrollOptionIntoView(list: HTMLElement, option: HTMLElement): void {\n if (list.scrollHeight <= list.clientHeight) return;\n const listRect = list.getBoundingClientRect();\n const optionRect = option.getBoundingClientRect();\n if (optionRect.top < listRect.top) {\n list.scrollTop -= listRect.top - optionRect.top;\n } else if (optionRect.bottom > listRect.bottom) {\n list.scrollTop += optionRect.bottom - listRect.bottom;\n }\n}\n","/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item.\n */\n setActive(index: number, { focus = false }: { focus?: boolean } = {}): void {\n const items = this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { ensureId } from \"../utils/aria_ids\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\nimport { scrollOptionIntoView } from \"../utils/option_scroll\";\nimport { RovingTabindex } from \"../utils/roving_tabindex\";\n\n/**\n * Headless, accessible multi-select combobox with chips.\n *\n * Markup contract (identifier: `stimeo--multi-select`):\n * <div data-controller=\"stimeo--multi-select\">\n * <ul data-stimeo--multi-select-target=\"tags\" aria-label=\"Selected\"></ul>\n * <input type=\"text\" role=\"combobox\" aria-expanded=\"false\"\n * aria-autocomplete=\"list\" aria-controls=\"ms-list\"\n * data-stimeo--multi-select-target=\"input\"\n * data-action=\"input->stimeo--multi-select#filter\n * keydown->stimeo--multi-select#onKeydown\n * focus->stimeo--multi-select#open\" />\n * <ul id=\"ms-list\" role=\"listbox\" aria-multiselectable=\"true\" hidden\n * data-stimeo--multi-select-target=\"list\">\n * <li id=\"ms-opt-1\" role=\"option\" aria-selected=\"false\" data-value=\"apple\"\n * data-stimeo--multi-select-target=\"option\"\n * data-action=\"click->stimeo--multi-select#toggleOption\">Apple</li>\n * </ul>\n * <span role=\"status\" aria-live=\"polite\" class=\"visually-hidden\"\n * data-stimeo--multi-select-target=\"status\"></span>\n * <!-- Optional: submit the selection as name=\"fruits[]\" hidden inputs. -->\n * <div data-stimeo--multi-select-target=\"fields\"></div>\n * <template data-stimeo--multi-select-target=\"tagTemplate\">…</template>\n * </div>\n *\n * Implements the WAI-ARIA APG **Combobox** pattern in its list-autocomplete,\n * multi-select form. Focus stays on the input; the active option is tracked with\n * `aria-activedescendant` and selection with `aria-selected`. Selected options are\n * mirrored as removable chips. For single selection use {@link ListboxController}\n * or Combobox; for free-text tags use {@link TagsInputController}.\n *\n * Behavior provided:\n * - Typing filters options by substring and opens the list; `ArrowDown`/`ArrowUp`\n * (wrapping), `Home`/`End` move the active option; `Enter` toggles it (the list\n * stays open); `Escape`/`Tab`/outside click close.\n * - Toggling syncs `aria-selected`, adds/removes a `Remove {label}` chip, mirrors\n * the live region, and dispatches `stimeo--multi-select:change` with `values`;\n * filtering dispatches `stimeo--multi-select:filter` for async candidates.\n * - The chips are one roving Tab stop: `ArrowLeft`/`ArrowRight` move between them,\n * `Delete`/`Backspace` remove the focused chip, and `Backspace` on an empty\n * input removes the last; removal re-homes focus to a neighbor or the input.\n * - `max` caps the selection (`0` = unlimited).\n * - With a `fields` target the selected values are mirrored into named hidden\n * inputs (default name `options[]`), so the selection submits with a normal\n * form and no consumer JS — parity with {@link TagsInputController}. An optional\n * `form` value sets the hidden inputs' `form` attribute, associating them with a\n * `<form>` by id even when the picker lives outside it. Purely additive: with no\n * `fields` target the behavior is unchanged.\n */\nexport class MultiSelectController extends Controller<HTMLElement> {\n static override targets = [\n \"input\",\n \"list\",\n \"option\",\n \"tags\",\n \"tag\",\n \"tagTemplate\",\n \"status\",\n \"fields\",\n ];\n static override values = {\n max: { type: Number, default: 0 },\n name: { type: String, default: \"options[]\" },\n form: { type: String, default: \"\" },\n };\n static actions = [\"close\", \"filter\", \"onKeydown\", \"open\", \"toggleOption\"] as const;\n static events = [\"change\", \"filter\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly listTarget: HTMLElement;\n declare readonly optionTargets: HTMLElement[];\n declare readonly tagsTarget: HTMLElement;\n declare readonly tagTargets: HTMLElement[];\n declare readonly tagTemplateTarget: HTMLTemplateElement;\n declare readonly statusTarget: HTMLElement;\n declare readonly fieldsTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasInputTarget: boolean;\n declare readonly hasTagsTarget: boolean;\n declare readonly hasTagTemplateTarget: boolean;\n declare readonly hasStatusTarget: boolean;\n declare readonly hasFieldsTarget: boolean;\n\n declare maxValue: number;\n declare nameValue: string;\n declare formValue: string;\n declare readonly hasFormValue: boolean;\n\n /** The active option (tracked via `aria-activedescendant`), or null. */\n #activeOption: HTMLElement | null = null;\n /** Absorbs the browser's redundant final input after compositionend. */\n #ignorePostCompositionInput = false;\n /** Owns IME lifecycle state; confirmed text emits one filter result. */\n readonly #composition = new CompositionTracker({\n onStart: () => {\n this.#ignorePostCompositionInput = false;\n },\n onEnd: () => {\n // Apply the confirmed query even in browsers that omit a final input event.\n // When the usual final input follows synchronously, absorb it so async\n // consumers receive one filter event for the confirmed text, not two.\n this.#ignorePostCompositionInput = true;\n queueMicrotask(() => {\n this.#ignorePostCompositionInput = false;\n });\n this.filter();\n },\n });\n readonly #roving = new RovingTabindex(() => this.#removeButtons);\n\n /** Starts closed, syncs chips for any pre-selected options, and listens out. */\n override connect(): void {\n if (this.hasInputTarget) this.#composition.observe(this.inputTarget);\n this.close();\n if (this.hasTagsTarget) {\n this.tagsTarget.addEventListener(\"keydown\", this.#onTagKeydown);\n this.tagsTarget.addEventListener(\"click\", this.#onTagClick);\n // Rebuild chips idempotently: a Turbo Drive cache restore or morph can\n // re-connect with chips already in the DOM, so clear them before deriving\n // afresh from the selected options to avoid duplicate chips.\n for (const tag of this.tagTargets) tag.remove();\n for (const option of this.#selectedOptions) this.#appendTag(option);\n if (this.#removeButtons.length > 0) this.#roving.setActive(0);\n }\n // Seed the hidden fields from any pre-selected options so the form submits\n // the initial selection without an interaction.\n this.#syncFields();\n document.addEventListener(\"click\", this.#onOutsideClick);\n }\n\n /** Tears down document and chip listeners on disconnect (Turbo included). */\n override disconnect(): void {\n this.#composition.disconnect();\n this.#ignorePostCompositionInput = false;\n if (this.hasTagsTarget) {\n this.tagsTarget.removeEventListener(\"keydown\", this.#onTagKeydown);\n this.tagsTarget.removeEventListener(\"click\", this.#onTagClick);\n }\n document.removeEventListener(\"click\", this.#onOutsideClick);\n }\n\n /** Tracks an input added initially or after connect. */\n inputTargetConnected(input: HTMLInputElement): void {\n this.#composition.observe(input);\n }\n\n /** Removes composition listeners when the active input is replaced or removed. */\n inputTargetDisconnected(input: HTMLInputElement): void {\n this.#composition.unobserve(input);\n this.#ignorePostCompositionInput = false;\n }\n\n /** Filters confirmed input text, opens, and re-seeds the active option. */\n filter(event?: InputEvent): void {\n if (event && this.#ignorePostCompositionInput) {\n this.#ignorePostCompositionInput = false;\n return;\n }\n if (this.#composition.isComposing(event)) return;\n const query = this.inputTarget.value.trim().toLowerCase();\n for (const option of this.optionTargets) {\n const label = (option.textContent ?? \"\").trim().toLowerCase();\n option.hidden = query !== \"\" && !label.includes(query);\n }\n this.open();\n const visible = this.#visibleOptions;\n this.element.toggleAttribute(\"data-stimeo--multi-select-empty\", visible.length === 0);\n this.#setActive(visible[0] ?? null);\n this.dispatch(\"filter\", { detail: { query } });\n }\n\n /** Opens the list and activates the first visible option when none is active. */\n open(): void {\n if (!this.hasListTarget) return;\n this.listTarget.hidden = false;\n this.inputTarget.setAttribute(\"aria-expanded\", \"true\");\n if (!this.#activeOption) this.#setActive(this.#visibleOptions[0] ?? null);\n }\n\n /** Closes the list and clears the active option. */\n close(): void {\n if (!this.hasListTarget) return;\n this.listTarget.hidden = true;\n this.inputTarget.setAttribute(\"aria-expanded\", \"false\");\n this.#setActive(null);\n }\n\n /** Routes input keyboard interaction per the multi-select combobox model. */\n onKeydown(event: KeyboardEvent): void {\n // Ignore keys fired during IME composition: the `Enter` that confirms a\n // candidate (and arrows that move within it) must not select an option or\n // navigate the chip list. Controller-owned lifecycle state covers confirming\n // events that omit the standard per-event signal.\n if (this.#composition.isComposing(event)) return;\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n if (this.#isClosed) this.open();\n else this.#moveActive(1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n if (this.#isClosed) this.open();\n else this.#moveActive(-1);\n break;\n case \"Home\":\n if (!this.#isClosed) {\n event.preventDefault();\n this.#setActive(this.#visibleOptions[0] ?? null);\n }\n break;\n case \"End\": {\n if (!this.#isClosed) {\n event.preventDefault();\n const visible = this.#visibleOptions;\n this.#setActive(visible[visible.length - 1] ?? null);\n }\n break;\n }\n case \"Enter\":\n if (!this.#isClosed && this.#activeOption) {\n event.preventDefault();\n this.#toggleSelection(this.#activeOption);\n }\n break;\n case \"Escape\":\n // Layered-Escape contract: leave a press an inner handler already\n // owned (rule 1), and consume only while owning a dismissable state —\n // with the list already closed the press stays free for the shared\n // resolver, so an enclosing dialog still closes from a focused input.\n if (event.defaultPrevented || this.#isClosed) break;\n event.preventDefault();\n this.close();\n break;\n case \"Backspace\":\n if (this.inputTarget.value === \"\") {\n const buttons = this.#removeButtons;\n if (buttons.length > 0) {\n event.preventDefault();\n this.#removeTagAt(buttons.length - 1);\n }\n }\n break;\n case \"ArrowLeft\":\n if (this.inputTarget.value === \"\" && this.#removeButtons.length > 0) {\n event.preventDefault();\n this.#roving.setActive(this.#removeButtons.length - 1, { focus: true });\n }\n break;\n case \"Tab\":\n this.close();\n break;\n default:\n break;\n }\n }\n\n /**\n * Toggles the clicked option's selection. Bound via `data-action`. Focus is\n * re-homed to the input afterwards: options are non-focusable, so the click blurs\n * the input to `body` — and with the list deliberately staying open, every\n * keyboard affordance (Escape, arrows, typing) is bound to the input and would\n * otherwise go dead until the user clicks back in (\"focus stays on the input\").\n */\n toggleOption(event: Event): void {\n const option = (event.currentTarget as HTMLElement).closest<HTMLElement>('[role=\"option\"]');\n if (!option) return;\n this.#toggleSelection(option);\n this.inputTarget.focus();\n }\n\n /**\n * Removes the chip whose remove button was clicked, deselecting its option.\n * Delegated on the tags container (like `#onTagKeydown`) rather than bound\n * per chip via `data-action`, so it works the instant a chip is appended without\n * waiting on Stimulus to wire a freshly created element.\n */\n readonly #onTagClick = (event: MouseEvent): void => {\n const button = (event.target as HTMLElement).closest(\"button\");\n if (!button || !this.tagsTarget.contains(button)) return;\n const index = this.#removeButtons.indexOf(button as HTMLButtonElement);\n if (index !== -1) this.#removeTagAt(index);\n };\n\n /** Moves the active option by `delta` among visible options, wrapping. */\n #moveActive(delta: number): void {\n const visible = this.#visibleOptions;\n if (visible.length === 0) return;\n const current = this.#activeOption ? visible.indexOf(this.#activeOption) : -1;\n const next = (current + delta + visible.length) % visible.length;\n this.#setActive(visible[next] ?? null);\n }\n\n /** Selects/deselects `option`, honoring `max`, and syncs chip + live region. */\n #toggleSelection(option: HTMLElement): void {\n const selected = option.getAttribute(\"aria-selected\") === \"true\";\n if (!selected && this.maxValue > 0 && this.#selectedOptions.length >= this.maxValue) {\n return;\n }\n option.setAttribute(\"aria-selected\", String(!selected));\n if (selected) {\n this.#removeTagFor(option);\n } else {\n this.#appendTag(option);\n }\n this.#announce(this.#optionLabel(option));\n this.#refreshRoving();\n this.#syncFields();\n this.dispatch(\"change\", { detail: { values: this.#values } });\n }\n\n /** Builds one chip from the template for `option`. */\n #appendTag(option: HTMLElement): void {\n if (!this.hasTagTemplateTarget || !this.hasTagsTarget) return;\n const fragment = this.tagTemplateTarget.content.cloneNode(true) as DocumentFragment;\n const tag = fragment.querySelector<HTMLElement>('[data-stimeo--multi-select-target=\"tag\"]');\n const label = fragment.querySelector<HTMLElement>('[data-multi-select-slot=\"label\"]');\n const button = fragment.querySelector<HTMLButtonElement>(\"button\");\n if (!tag || !button) return;\n const text = this.#optionLabel(option);\n tag.dataset.value = this.#optionValue(option);\n if (label) label.textContent = text;\n button.setAttribute(\"aria-label\", `Remove ${text}`);\n button.tabIndex = -1;\n this.tagsTarget.appendChild(fragment);\n }\n\n /** Removes the chip mirroring `option`, if present. */\n #removeTagFor(option: HTMLElement): void {\n const value = this.#optionValue(option);\n const tag = this.tagTargets.find((candidate) => candidate.dataset.value === value);\n tag?.remove();\n }\n\n /** Removes chip `index` and deselects its option, re-homing focus. */\n #removeTagAt(index: number): void {\n const tag = this.tagTargets[index];\n if (!tag) return;\n const value = tag.dataset.value ?? \"\";\n // Match by the option's stable value, which falls back to its label when no\n // data-value is present, so a label-keyed chip still finds its option.\n const option = this.optionTargets.find((candidate) => this.#optionValue(candidate) === value);\n if (option) option.setAttribute(\"aria-selected\", \"false\");\n tag.remove();\n // Prefer the option's display label for the announcement (e.g. \"Apple\"),\n // which can differ from its data-value (e.g. \"apple\").\n this.#announce(option ? this.#optionLabel(option) : value);\n this.#refreshRoving();\n this.#syncFields();\n this.dispatch(\"change\", { detail: { values: this.#values } });\n\n const remaining = this.#removeButtons;\n if (remaining.length === 0) {\n this.inputTarget.focus();\n } else {\n this.#roving.setActive(Math.min(index, remaining.length - 1), { focus: true });\n }\n }\n\n /** Arrow navigation and deletion within the chip list (delegated). */\n readonly #onTagKeydown = (event: KeyboardEvent): void => {\n const button = (event.target as HTMLElement).closest(\"button\");\n if (!button) return;\n const buttons = this.#removeButtons;\n const index = buttons.indexOf(button);\n if (index === -1) return;\n switch (event.key) {\n case \"ArrowLeft\":\n if (index > 0) {\n event.preventDefault();\n this.#roving.setActive(index - 1, { focus: true });\n }\n break;\n case \"ArrowRight\":\n event.preventDefault();\n if (index < buttons.length - 1) this.#roving.setActive(index + 1, { focus: true });\n else this.inputTarget.focus();\n break;\n case \"Delete\":\n case \"Backspace\":\n event.preventDefault();\n this.#removeTagAt(index);\n break;\n default:\n break;\n }\n };\n\n /**\n * Marks `option` active via `data-active` and the input's\n * `aria-activedescendant` (the attribute is removed, not emptied, when null).\n */\n #setActive(option: HTMLElement | null): void {\n this.#activeOption = option;\n for (const candidate of this.optionTargets) {\n candidate.toggleAttribute(\"data-active\", candidate === option);\n }\n if (option) {\n this.inputTarget.setAttribute(\"aria-activedescendant\", ensureId(option, \"stimeo-ms-opt\"));\n // Virtual focus never triggers the browser's native focus-scrolling, so a\n // scrollable list must follow the active option itself (list-only scroll).\n if (this.hasListTarget) scrollOptionIntoView(this.listTarget, option);\n } else {\n this.inputTarget.removeAttribute(\"aria-activedescendant\");\n }\n }\n\n /**\n * Mirrors the selected values into named hidden inputs under the `fields`\n * target so the selection submits with a normal form (parity with tags-input).\n * No-ops without a `fields` target, keeping the control back-compat. When the\n * `form` value is set, each input gets a matching `form` attribute so the picker\n * can submit with a `<form>` it lives outside of.\n */\n #syncFields(): void {\n if (!this.hasFieldsTarget) return;\n this.fieldsTarget.replaceChildren(\n ...this.#values.map((value) => {\n const input = document.createElement(\"input\");\n input.type = \"hidden\";\n input.name = this.nameValue;\n input.value = value;\n if (this.hasFormValue && this.formValue !== \"\") input.setAttribute(\"form\", this.formValue);\n return input;\n }),\n );\n }\n\n /** Keeps exactly one chip remove button tabbable after the set changes. */\n #refreshRoving(): void {\n if (this.#removeButtons.length > 0 && this.#roving.activeIndex === -1)\n this.#roving.setActive(0);\n }\n\n /** Mirrors the changed option label into the live region. */\n #announce(text: string): void {\n if (this.hasStatusTarget) this.statusTarget.textContent = text;\n }\n\n /** Closes the list on a click outside the controller element. */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n if (!this.#isClosed && !this.element.contains(event.target as Node)) this.close();\n };\n\n /** Trimmed visible label of an option. */\n #optionLabel(option: HTMLElement): string {\n return (option.textContent ?? \"\").trim();\n }\n\n /** An option's stable value: its `data-value`, else its display label. */\n #optionValue(option: HTMLElement): string {\n return option.dataset.value ?? this.#optionLabel(option);\n }\n\n /** Options not hidden by the current filter. */\n get #visibleOptions(): HTMLElement[] {\n return this.optionTargets.filter((option) => !option.hidden);\n }\n\n /** Options currently selected. */\n get #selectedOptions(): HTMLElement[] {\n return this.optionTargets.filter((option) => option.getAttribute(\"aria-selected\") === \"true\");\n }\n\n /** Selected values in option order. */\n get #values(): string[] {\n return this.#selectedOptions.map((option) => this.#optionValue(option));\n }\n\n /** The chip remove buttons in order (the roving navigation set). */\n get #removeButtons(): HTMLButtonElement[] {\n return this.hasTagsTarget\n ? Array.from(this.tagsTarget.querySelectorAll<HTMLButtonElement>(\"button\"))\n : [];\n }\n\n /** Whether the list is currently hidden. */\n get #isClosed(): boolean {\n return !this.hasListTarget || this.listTarget.hidden !== false;\n }\n}\n"]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless, accessible **navigation menu** behavior (disclosure navigation).
|
|
5
|
+
*
|
|
6
|
+
* Markup contract (identifier: `stimeo--navigation-menu`):
|
|
7
|
+
* <nav data-controller="stimeo--navigation-menu" aria-label="Main">
|
|
8
|
+
* <ul>
|
|
9
|
+
* <li>
|
|
10
|
+
* <button data-stimeo--navigation-menu-target="trigger"
|
|
11
|
+
* aria-expanded="false" aria-controls="nav-products"
|
|
12
|
+
* data-action="click->stimeo--navigation-menu#toggle
|
|
13
|
+
* keydown->stimeo--navigation-menu#onTriggerKeydown">
|
|
14
|
+
* Products
|
|
15
|
+
* </button>
|
|
16
|
+
* <div id="nav-products" data-stimeo--navigation-menu-target="panel" hidden>
|
|
17
|
+
* <a href="/a">Product A</a><a href="/b">Product B</a>
|
|
18
|
+
* </div>
|
|
19
|
+
* </li>
|
|
20
|
+
* </ul>
|
|
21
|
+
* </nav>
|
|
22
|
+
*
|
|
23
|
+
* Implements the WAI-ARIA APG **Disclosure** navigation pattern: each top-level
|
|
24
|
+
* button toggles its sub-panel (`aria-expanded` + `hidden` synced), only one panel
|
|
25
|
+
* is open at a time, and the panel content is a plain set of links (not a
|
|
26
|
+
* `role="menu"`). Focus is **not** trapped — `Tab` moves through the links
|
|
27
|
+
* naturally. `Escape` closes the open panel and returns focus to its trigger.
|
|
28
|
+
* While a panel is open the nav is a layer on the shared {@link EscapeLayer}
|
|
29
|
+
* stack; it claims a press only while focus is inside the nav or fell to the
|
|
30
|
+
* body (a click on non-focusable panel content), so one keypress closes exactly
|
|
31
|
+
* one layer (the shared layered-Escape contract). An outside click, or focus
|
|
32
|
+
* leaving the nav for a known external destination, closes it. `ArrowLeft`/
|
|
33
|
+
* `ArrowRight` move focus between triggers without rewriting `tabindex` (they
|
|
34
|
+
* keep their natural Tab order). Hover open/close is opt-in via `openOnHover`.
|
|
35
|
+
*
|
|
36
|
+
* By default the hover region is each trigger and its panel. An optional
|
|
37
|
+
* `hoverArea` target widens it: mark a wrapper that contains a trigger (e.g. the
|
|
38
|
+
* `<li>` holding a top-level *link*, its disclosure button, and its panel — the
|
|
39
|
+
* APG "Disclosure Navigation with Top-Level Links" arrangement) and hovering
|
|
40
|
+
* anywhere over that wrapper opens the contained trigger's panel. A trigger or
|
|
41
|
+
* panel inside a `hoverArea` defers to the wrapper (its own edges stop scheduling
|
|
42
|
+
* open/close), so pointer movement within the area never flickers the panel.
|
|
43
|
+
* Keep a trigger's panel inside its `hoverArea`: a panel left outside simply
|
|
44
|
+
* falls back to the default two-region behavior — moving straight across a
|
|
45
|
+
* shared edge never schedules a close (the leave handler checks
|
|
46
|
+
* `relatedTarget`), and `hoverDelay` bridges the pointer's travel across an
|
|
47
|
+
* actual gap. Targets added or removed while connected (e.g. a Turbo Stream
|
|
48
|
+
* append) re-wire the hover listeners via Stimulus target callbacks.
|
|
49
|
+
* `hoverArea` has no effect unless `openOnHover` is enabled.
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* Behavior only. Panel layout, mega-menu styling, and animation are the
|
|
53
|
+
* consumer's CSS. Static placement is CSS; viewport-edge collision avoidance is
|
|
54
|
+
* delegated to the opt-in `stimeo-ui/positioning` module (never imported here, so
|
|
55
|
+
* the core stays zero-dependency). For an app command menu with arrow roving and
|
|
56
|
+
* `role="menu"`, use `stimeo--menubar` instead.
|
|
57
|
+
*/
|
|
58
|
+
declare class NavigationMenuController extends Controller<HTMLElement> {
|
|
59
|
+
#private;
|
|
60
|
+
static targets: string[];
|
|
61
|
+
static values: {
|
|
62
|
+
openOnHover: {
|
|
63
|
+
type: BooleanConstructor;
|
|
64
|
+
default: boolean;
|
|
65
|
+
};
|
|
66
|
+
hoverDelay: {
|
|
67
|
+
type: NumberConstructor;
|
|
68
|
+
default: number;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
static actions: readonly ["onTriggerKeydown", "toggle"];
|
|
72
|
+
readonly triggerTargets: HTMLElement[];
|
|
73
|
+
readonly panelTargets: HTMLElement[];
|
|
74
|
+
readonly hoverAreaTargets: HTMLElement[];
|
|
75
|
+
openOnHoverValue: boolean;
|
|
76
|
+
hoverDelayValue: number;
|
|
77
|
+
/** Establishes the closed baseline and the dismissal listeners. */
|
|
78
|
+
connect(): void;
|
|
79
|
+
/** Removes every listener, pending hover timer, and stack membership registered while connected. */
|
|
80
|
+
disconnect(): void;
|
|
81
|
+
/** Re-wires hover listeners when a target is added after connect (Turbo Streams etc.). */
|
|
82
|
+
triggerTargetConnected(): void;
|
|
83
|
+
/** Re-wires hover listeners when a target is removed while connected. */
|
|
84
|
+
triggerTargetDisconnected(): void;
|
|
85
|
+
/** See {@link NavigationMenuController.triggerTargetConnected}. */
|
|
86
|
+
panelTargetConnected(): void;
|
|
87
|
+
/** See {@link NavigationMenuController.triggerTargetDisconnected}. */
|
|
88
|
+
panelTargetDisconnected(): void;
|
|
89
|
+
/** See {@link NavigationMenuController.triggerTargetConnected}. */
|
|
90
|
+
hoverAreaTargetConnected(): void;
|
|
91
|
+
/** See {@link NavigationMenuController.triggerTargetDisconnected}. */
|
|
92
|
+
hoverAreaTargetDisconnected(): void;
|
|
93
|
+
/** Toggles a trigger's panel (single-open). Bound via `data-action` (click). */
|
|
94
|
+
toggle(event: Event): void;
|
|
95
|
+
/** `ArrowLeft`/`ArrowRight` move focus between triggers (keeping Tab order). */
|
|
96
|
+
onTriggerKeydown(event: KeyboardEvent): void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { NavigationMenuController };
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/navigation_menu_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/escape_layer.ts
|
|
6
|
+
function claimsWhileFocusWithin(element) {
|
|
7
|
+
return () => {
|
|
8
|
+
const active = element.ownerDocument.activeElement;
|
|
9
|
+
return active === null || active === element.ownerDocument.body || element.contains(active);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
var EscapeLayer = class _EscapeLayer {
|
|
13
|
+
static #registries = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
#ownerDocument = null;
|
|
15
|
+
/** Dismissal callback while active; `null` when inactive. */
|
|
16
|
+
#onDismiss = null;
|
|
17
|
+
/** Live predicate deciding whether the layer claims a press; `null` = always. */
|
|
18
|
+
#claims = null;
|
|
19
|
+
/**
|
|
20
|
+
* Activates this layer at the top of its document's Escape stack, installing
|
|
21
|
+
* the document's shared resolver listener if this is its first layer.
|
|
22
|
+
* Re-activating an already-active layer moves it to the top.
|
|
23
|
+
*/
|
|
24
|
+
activate(ownerDocument = document, options) {
|
|
25
|
+
this.deactivate();
|
|
26
|
+
let registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
27
|
+
if (!registry) {
|
|
28
|
+
registry = _EscapeLayer.#createRegistry();
|
|
29
|
+
_EscapeLayer.#registries.set(ownerDocument, registry);
|
|
30
|
+
ownerDocument.addEventListener("keydown", registry.onKeydown);
|
|
31
|
+
}
|
|
32
|
+
registry.stack.push(this);
|
|
33
|
+
this.#ownerDocument = ownerDocument;
|
|
34
|
+
this.#onDismiss = options.onDismiss;
|
|
35
|
+
this.#claims = options.claims ?? null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Removes this layer from its document's Escape stack, uninstalling the
|
|
39
|
+
* shared listener when the stack empties. Safe to call when inactive.
|
|
40
|
+
*/
|
|
41
|
+
deactivate() {
|
|
42
|
+
const ownerDocument = this.#ownerDocument;
|
|
43
|
+
if (!ownerDocument) return;
|
|
44
|
+
const registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
45
|
+
if (registry) {
|
|
46
|
+
const index = registry.stack.lastIndexOf(this);
|
|
47
|
+
if (index >= 0) registry.stack.splice(index, 1);
|
|
48
|
+
if (registry.stack.length === 0) {
|
|
49
|
+
ownerDocument.removeEventListener("keydown", registry.onKeydown);
|
|
50
|
+
_EscapeLayer.#registries.delete(ownerDocument);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.#ownerDocument = null;
|
|
54
|
+
this.#onDismiss = null;
|
|
55
|
+
this.#claims = null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether this active layer would own a press right now: it is the topmost
|
|
59
|
+
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
60
|
+
* and diagnostics — production dismissal goes through the shared listener.
|
|
61
|
+
*/
|
|
62
|
+
get ownsEscape() {
|
|
63
|
+
const ownerDocument = this.#ownerDocument;
|
|
64
|
+
if (!ownerDocument) return false;
|
|
65
|
+
const registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
66
|
+
if (!registry) return false;
|
|
67
|
+
return _EscapeLayer.#resolveOwner(registry.stack) === this;
|
|
68
|
+
}
|
|
69
|
+
/** Builds a document's registry with its shared resolver listener. */
|
|
70
|
+
static #createRegistry() {
|
|
71
|
+
const registry = {
|
|
72
|
+
stack: [],
|
|
73
|
+
onKeydown: (event) => {
|
|
74
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
75
|
+
const owner = _EscapeLayer.#resolveOwner(registry.stack);
|
|
76
|
+
if (!owner) return;
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
owner.#onDismiss?.();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return registry;
|
|
82
|
+
}
|
|
83
|
+
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
84
|
+
static #resolveOwner(stack) {
|
|
85
|
+
for (let index = stack.length - 1; index >= 0; index--) {
|
|
86
|
+
const layer = stack[index];
|
|
87
|
+
if (!layer) continue;
|
|
88
|
+
if (layer.#claims && !layer.#claims()) continue;
|
|
89
|
+
return layer;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// src/utils/safe_timeout.ts
|
|
96
|
+
var TimerRegistry = class {
|
|
97
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
98
|
+
ids = /* @__PURE__ */ new Set();
|
|
99
|
+
/**
|
|
100
|
+
* Cancels a single tracked timer.
|
|
101
|
+
*
|
|
102
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
103
|
+
* registry), so callers can clear defensively without guarding.
|
|
104
|
+
*/
|
|
105
|
+
clear(id) {
|
|
106
|
+
if (this.ids.delete(id)) {
|
|
107
|
+
this.cancel(id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
112
|
+
* to guarantee no timer outlives the element.
|
|
113
|
+
*/
|
|
114
|
+
clearAll() {
|
|
115
|
+
for (const id of this.ids) {
|
|
116
|
+
this.cancel(id);
|
|
117
|
+
}
|
|
118
|
+
this.ids.clear();
|
|
119
|
+
}
|
|
120
|
+
/** Number of timers currently tracked (pending). */
|
|
121
|
+
get size() {
|
|
122
|
+
return this.ids.size;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var SafeTimeout = class extends TimerRegistry {
|
|
126
|
+
/**
|
|
127
|
+
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
128
|
+
*
|
|
129
|
+
* The id is removed from the registry automatically when the timeout fires,
|
|
130
|
+
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
131
|
+
*/
|
|
132
|
+
set(callback, delay) {
|
|
133
|
+
const id = this.schedule(() => {
|
|
134
|
+
this.ids.delete(id);
|
|
135
|
+
callback();
|
|
136
|
+
}, delay);
|
|
137
|
+
this.ids.add(id);
|
|
138
|
+
return id;
|
|
139
|
+
}
|
|
140
|
+
schedule(callback, delay) {
|
|
141
|
+
return window.setTimeout(callback, delay);
|
|
142
|
+
}
|
|
143
|
+
cancel(id) {
|
|
144
|
+
window.clearTimeout(id);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/controllers/navigation_menu_controller.ts
|
|
149
|
+
var NavigationMenuController = class extends Controller {
|
|
150
|
+
static targets = ["trigger", "panel", "hoverArea"];
|
|
151
|
+
static values = {
|
|
152
|
+
openOnHover: { type: Boolean, default: false },
|
|
153
|
+
hoverDelay: { type: Number, default: 150 }
|
|
154
|
+
};
|
|
155
|
+
static actions = ["onTriggerKeydown", "toggle"];
|
|
156
|
+
/** Open/close delay timers for hover mode; cleared together on disconnect. */
|
|
157
|
+
#hoverTimers = new SafeTimeout();
|
|
158
|
+
/**
|
|
159
|
+
* Elements currently carrying hover listeners. Removal always mirrors this
|
|
160
|
+
* set (not a recomputed target snapshot), so add/remove stays symmetric even
|
|
161
|
+
* when targets churn between connect and disconnect.
|
|
162
|
+
*/
|
|
163
|
+
#hoverWired = /* @__PURE__ */ new Set();
|
|
164
|
+
/** Escape-stack membership while any panel is open; the shared resolver dismisses via it. */
|
|
165
|
+
#escapeLayer = new EscapeLayer();
|
|
166
|
+
/** Establishes the closed baseline and the dismissal listeners. */
|
|
167
|
+
connect() {
|
|
168
|
+
this.#closeAll();
|
|
169
|
+
document.addEventListener("click", this.#onOutsideClick);
|
|
170
|
+
this.element.addEventListener("focusout", this.#onFocusOut);
|
|
171
|
+
if (this.openOnHoverValue) this.#addHoverListeners();
|
|
172
|
+
}
|
|
173
|
+
/** Removes every listener, pending hover timer, and stack membership registered while connected. */
|
|
174
|
+
disconnect() {
|
|
175
|
+
this.#escapeLayer.deactivate();
|
|
176
|
+
document.removeEventListener("click", this.#onOutsideClick);
|
|
177
|
+
this.element.removeEventListener("focusout", this.#onFocusOut);
|
|
178
|
+
this.#removeHoverListeners();
|
|
179
|
+
this.#hoverTimers.clearAll();
|
|
180
|
+
}
|
|
181
|
+
/** Re-wires hover listeners when a target is added after connect (Turbo Streams etc.). */
|
|
182
|
+
triggerTargetConnected() {
|
|
183
|
+
this.#rewireHoverListeners();
|
|
184
|
+
}
|
|
185
|
+
/** Re-wires hover listeners when a target is removed while connected. */
|
|
186
|
+
triggerTargetDisconnected() {
|
|
187
|
+
this.#rewireHoverListeners();
|
|
188
|
+
}
|
|
189
|
+
/** See {@link NavigationMenuController.triggerTargetConnected}. */
|
|
190
|
+
panelTargetConnected() {
|
|
191
|
+
this.#rewireHoverListeners();
|
|
192
|
+
}
|
|
193
|
+
/** See {@link NavigationMenuController.triggerTargetDisconnected}. */
|
|
194
|
+
panelTargetDisconnected() {
|
|
195
|
+
this.#rewireHoverListeners();
|
|
196
|
+
}
|
|
197
|
+
/** See {@link NavigationMenuController.triggerTargetConnected}. */
|
|
198
|
+
hoverAreaTargetConnected() {
|
|
199
|
+
this.#rewireHoverListeners();
|
|
200
|
+
}
|
|
201
|
+
/** See {@link NavigationMenuController.triggerTargetDisconnected}. */
|
|
202
|
+
hoverAreaTargetDisconnected() {
|
|
203
|
+
this.#rewireHoverListeners();
|
|
204
|
+
}
|
|
205
|
+
/** Toggles a trigger's panel (single-open). Bound via `data-action` (click). */
|
|
206
|
+
toggle(event) {
|
|
207
|
+
const trigger = event.currentTarget;
|
|
208
|
+
if (this.#isExpanded(trigger)) {
|
|
209
|
+
this.#closePanel(trigger);
|
|
210
|
+
} else {
|
|
211
|
+
this.#openPanel(trigger);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** `ArrowLeft`/`ArrowRight` move focus between triggers (keeping Tab order). */
|
|
215
|
+
onTriggerKeydown(event) {
|
|
216
|
+
const triggers = this.triggerTargets;
|
|
217
|
+
const index = triggers.indexOf(event.currentTarget);
|
|
218
|
+
if (index === -1) return;
|
|
219
|
+
const length = triggers.length;
|
|
220
|
+
if (event.key === "ArrowRight") {
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
triggers[(index + 1) % length]?.focus();
|
|
223
|
+
} else if (event.key === "ArrowLeft") {
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
triggers[(index - 1 + length) % length]?.focus();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Opens `trigger`'s panel, closing any other open panel first. */
|
|
229
|
+
#openPanel(trigger) {
|
|
230
|
+
this.#closeAll();
|
|
231
|
+
const panel = this.#panelFor(trigger);
|
|
232
|
+
if (!panel) return;
|
|
233
|
+
panel.hidden = false;
|
|
234
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
235
|
+
this.#syncEscapeLayer();
|
|
236
|
+
}
|
|
237
|
+
/** Closes `trigger`'s panel and reflects the collapsed state. */
|
|
238
|
+
#closePanel(trigger) {
|
|
239
|
+
const panel = this.#panelFor(trigger);
|
|
240
|
+
if (panel) panel.hidden = true;
|
|
241
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
242
|
+
this.#syncEscapeLayer();
|
|
243
|
+
}
|
|
244
|
+
/** Closes every open panel. */
|
|
245
|
+
#closeAll() {
|
|
246
|
+
for (const trigger of this.triggerTargets) this.#closePanel(trigger);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Aligns Escape-stack membership with the open state: joins (or re-asserts to
|
|
250
|
+
* the top) while a panel is open, leaves once none is. Re-asserting when the
|
|
251
|
+
* user switches panels is deliberate — the nav is again the newest layer.
|
|
252
|
+
*/
|
|
253
|
+
#syncEscapeLayer() {
|
|
254
|
+
if (this.#isAnyOpen) {
|
|
255
|
+
this.#escapeLayer.activate(document, {
|
|
256
|
+
onDismiss: () => this.#closeAndRestore(),
|
|
257
|
+
claims: claimsWhileFocusWithin(this.element)
|
|
258
|
+
});
|
|
259
|
+
} else {
|
|
260
|
+
this.#escapeLayer.deactivate();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Closes any open panel and returns focus to its trigger (Escape path). */
|
|
264
|
+
#closeAndRestore() {
|
|
265
|
+
const open = this.#openTrigger;
|
|
266
|
+
if (!open) return;
|
|
267
|
+
this.#closePanel(open);
|
|
268
|
+
open.focus();
|
|
269
|
+
}
|
|
270
|
+
/** Closes panels when a click lands outside the nav element. */
|
|
271
|
+
#onOutsideClick = (event) => {
|
|
272
|
+
if (this.#isAnyOpen && !this.element.contains(event.target)) this.#closeAll();
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Closes (without restoring focus) when focus leaves the nav for a known
|
|
276
|
+
* external destination. A null/non-Node destination is indeterminate:
|
|
277
|
+
* browsers use it for clicks on non-focusable content and for window
|
|
278
|
+
* deactivation, so those never close the nav here (matching the popover
|
|
279
|
+
* convention) — the outside-click handler decides pointer dismissal, and the
|
|
280
|
+
* Escape stack's body-focus claim keeps keyboard dismissal working.
|
|
281
|
+
*/
|
|
282
|
+
#onFocusOut = (event) => {
|
|
283
|
+
const next = event.relatedTarget;
|
|
284
|
+
if (!(next instanceof Node) || this.element.contains(next)) return;
|
|
285
|
+
this.#closeAll();
|
|
286
|
+
};
|
|
287
|
+
/** Opens a trigger's panel after the hover delay (hover mode). */
|
|
288
|
+
#onPointerEnter = (event) => {
|
|
289
|
+
const trigger = this.#triggerForHover(event.currentTarget);
|
|
290
|
+
if (!trigger) return;
|
|
291
|
+
this.#hoverTimers.clearAll();
|
|
292
|
+
this.#hoverTimers.set(() => this.#openPanel(trigger), this.hoverDelayValue);
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* Closes the open panel after the hover delay (hover mode) — unless the
|
|
296
|
+
* pointer moved directly into another part of the hover region (an adjacent
|
|
297
|
+
* trigger, panel, or hoverArea): crossing a shared edge must not schedule a
|
|
298
|
+
* spurious close, e.g. a hoverArea whose panel sits outside it as a sibling.
|
|
299
|
+
*/
|
|
300
|
+
#onPointerLeave = (event) => {
|
|
301
|
+
const next = event instanceof MouseEvent ? event.relatedTarget : null;
|
|
302
|
+
if (next instanceof Node && this.#hoverElements.some((el) => el.contains(next))) return;
|
|
303
|
+
this.#hoverTimers.clearAll();
|
|
304
|
+
this.#hoverTimers.set(() => this.#closeAll(), this.hoverDelayValue);
|
|
305
|
+
};
|
|
306
|
+
/** Wires hover open/close on each hover element (opt-in), tracking what was wired. */
|
|
307
|
+
#addHoverListeners() {
|
|
308
|
+
for (const element of this.#hoverElements) {
|
|
309
|
+
element.addEventListener("mouseenter", this.#onPointerEnter);
|
|
310
|
+
element.addEventListener("mouseleave", this.#onPointerLeave);
|
|
311
|
+
this.#hoverWired.add(element);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** Removes the hover listeners from exactly the elements that were wired. */
|
|
315
|
+
#removeHoverListeners() {
|
|
316
|
+
for (const element of this.#hoverWired) {
|
|
317
|
+
element.removeEventListener("mouseenter", this.#onPointerEnter);
|
|
318
|
+
element.removeEventListener("mouseleave", this.#onPointerLeave);
|
|
319
|
+
}
|
|
320
|
+
this.#hoverWired.clear();
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Rebuilds the hover wiring from the current targets. Target callbacks call
|
|
324
|
+
* this so items added or removed after connect (e.g. a Turbo Stream append)
|
|
325
|
+
* participate in hover; the wired-set removal keeps the rebuild symmetric.
|
|
326
|
+
*/
|
|
327
|
+
#rewireHoverListeners() {
|
|
328
|
+
if (!this.openOnHoverValue) return;
|
|
329
|
+
this.#removeHoverListeners();
|
|
330
|
+
this.#addHoverListeners();
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Elements that participate in hover: each `hoverArea`, plus every trigger and
|
|
334
|
+
* panel **not** wrapped by one. A wrapped trigger/panel must defer to its
|
|
335
|
+
* wrapper — its own mouseleave would otherwise schedule a close while the
|
|
336
|
+
* pointer is still inside the area (mouseenter does not re-fire on the wrapper
|
|
337
|
+
* when moving among its descendants), flickering the panel shut.
|
|
338
|
+
*/
|
|
339
|
+
get #hoverElements() {
|
|
340
|
+
const areas = this.hoverAreaTargets;
|
|
341
|
+
const covered = (element) => areas.some((area) => area.contains(element));
|
|
342
|
+
return [
|
|
343
|
+
...areas,
|
|
344
|
+
...this.triggerTargets.filter((trigger) => !covered(trigger)),
|
|
345
|
+
...this.panelTargets.filter((panel) => !covered(panel))
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Resolves the trigger for a hovered element: the trigger itself, the trigger
|
|
350
|
+
* controlling a hovered panel, or the first trigger contained in a hovered
|
|
351
|
+
* `hoverArea` wrapper.
|
|
352
|
+
*/
|
|
353
|
+
#triggerForHover(element) {
|
|
354
|
+
if (this.triggerTargets.includes(element)) return element;
|
|
355
|
+
if (this.panelTargets.includes(element)) {
|
|
356
|
+
return this.triggerTargets.find((trigger) => this.#panelFor(trigger) === element) ?? null;
|
|
357
|
+
}
|
|
358
|
+
if (this.hoverAreaTargets.includes(element)) {
|
|
359
|
+
return this.triggerTargets.find((trigger) => element.contains(trigger)) ?? null;
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
/** The panel controlled by `trigger` (matched by `aria-controls`/`id`). */
|
|
364
|
+
#panelFor(trigger) {
|
|
365
|
+
const id = trigger.getAttribute("aria-controls");
|
|
366
|
+
return id ? this.panelTargets.find((panel) => panel.id === id) ?? null : null;
|
|
367
|
+
}
|
|
368
|
+
/** Whether `trigger`'s panel is currently expanded. */
|
|
369
|
+
#isExpanded(trigger) {
|
|
370
|
+
return trigger.getAttribute("aria-expanded") === "true";
|
|
371
|
+
}
|
|
372
|
+
/** The trigger whose panel is currently open, if any. */
|
|
373
|
+
get #openTrigger() {
|
|
374
|
+
return this.triggerTargets.find((trigger) => this.#isExpanded(trigger)) ?? null;
|
|
375
|
+
}
|
|
376
|
+
/** Whether any panel is currently open. */
|
|
377
|
+
get #isAnyOpen() {
|
|
378
|
+
return this.#openTrigger !== null;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
export { NavigationMenuController };
|
|
383
|
+
//# sourceMappingURL=navigation_menu_controller.js.map
|
|
384
|
+
//# sourceMappingURL=navigation_menu_controller.js.map
|