@vielzeug/refine 2.4.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +5 -18
  2. package/dist/accordion-item.cjs.map +1 -1
  3. package/dist/accordion-item.js.map +1 -1
  4. package/dist/accordion.cjs.map +1 -1
  5. package/dist/accordion.js.map +1 -1
  6. package/dist/async.cjs +1 -1
  7. package/dist/async.cjs.map +1 -1
  8. package/dist/async.js +3 -3
  9. package/dist/async.js.map +1 -1
  10. package/dist/box.cjs.map +1 -1
  11. package/dist/box.js.map +1 -1
  12. package/dist/button.cjs +6 -4
  13. package/dist/button.cjs.map +1 -1
  14. package/dist/button.js +11 -9
  15. package/dist/button.js.map +1 -1
  16. package/dist/card.cjs +1 -1
  17. package/dist/card.cjs.map +1 -1
  18. package/dist/card.js +6 -4
  19. package/dist/card.js.map +1 -1
  20. package/dist/checkbox-group.cjs +1 -1
  21. package/dist/checkbox-group.cjs.map +1 -1
  22. package/dist/checkbox-group.js +1 -1
  23. package/dist/checkbox-group.js.map +1 -1
  24. package/dist/combobox.cjs +2 -2
  25. package/dist/combobox.cjs.map +1 -1
  26. package/dist/combobox.js +7 -3
  27. package/dist/combobox.js.map +1 -1
  28. package/dist/command-palette.cjs +1 -1
  29. package/dist/command-palette.cjs.map +1 -1
  30. package/dist/command-palette.js +6 -2
  31. package/dist/command-palette.js.map +1 -1
  32. package/dist/content/card/card.css.cjs +1 -1
  33. package/dist/content/card/card.css.cjs.map +1 -1
  34. package/dist/content/card/card.css.js +1 -1
  35. package/dist/content/card/card.css.js.map +1 -1
  36. package/dist/content/card/card.d.ts +2 -2
  37. package/dist/content/card/card.d.ts.map +1 -1
  38. package/dist/content/pagination/pagination.css.cjs +1 -1
  39. package/dist/content/pagination/pagination.css.cjs.map +1 -1
  40. package/dist/content/pagination/pagination.css.js +1 -1
  41. package/dist/content/pagination/pagination.css.js.map +1 -1
  42. package/dist/core/field-base.cjs +1 -1
  43. package/dist/core/field-base.cjs.map +1 -1
  44. package/dist/core/field-base.d.ts +1 -1
  45. package/dist/core/field-base.js +1 -1
  46. package/dist/core/field-base.js.map +1 -1
  47. package/dist/core/nav.cjs +1 -1
  48. package/dist/core/nav.cjs.map +1 -1
  49. package/dist/core/nav.d.ts +4 -1
  50. package/dist/core/nav.d.ts.map +1 -1
  51. package/dist/core/nav.js +41 -28
  52. package/dist/core/nav.js.map +1 -1
  53. package/dist/core/option-list.cjs +1 -1
  54. package/dist/core/option-list.cjs.map +1 -1
  55. package/dist/core/option-list.d.ts.map +1 -1
  56. package/dist/core/option-list.js +30 -28
  57. package/dist/core/option-list.js.map +1 -1
  58. package/dist/core/slider.cjs +1 -1
  59. package/dist/core/slider.cjs.map +1 -1
  60. package/dist/core/slider.js +1 -1
  61. package/dist/core/slider.js.map +1 -1
  62. package/dist/core/spinner.cjs +1 -1
  63. package/dist/core/spinner.cjs.map +1 -1
  64. package/dist/core/spinner.js +1 -1
  65. package/dist/core/spinner.js.map +1 -1
  66. package/dist/custom-elements.json +231 -19
  67. package/dist/datagrid.cjs +73 -73
  68. package/dist/datagrid.cjs.map +1 -1
  69. package/dist/datagrid.js +331 -332
  70. package/dist/datagrid.js.map +1 -1
  71. package/dist/disclosure/accordion/accordion.css.cjs +1 -1
  72. package/dist/disclosure/accordion/accordion.css.cjs.map +1 -1
  73. package/dist/disclosure/accordion/accordion.css.js +1 -1
  74. package/dist/disclosure/accordion/accordion.css.js.map +1 -1
  75. package/dist/disclosure/accordion/accordion.d.ts +5 -5
  76. package/dist/disclosure/accordion/accordion.d.ts.map +1 -1
  77. package/dist/disclosure/accordion-item/accordion-item.css.cjs +1 -1
  78. package/dist/disclosure/accordion-item/accordion-item.css.cjs.map +1 -1
  79. package/dist/disclosure/accordion-item/accordion-item.css.js +1 -1
  80. package/dist/disclosure/accordion-item/accordion-item.css.js.map +1 -1
  81. package/dist/disclosure/accordion-item/accordion-item.d.ts +3 -3
  82. package/dist/disclosure/accordion-item/accordion-item.d.ts.map +1 -1
  83. package/dist/disclosure/tab-item/tab-item.css.cjs +1 -1
  84. package/dist/disclosure/tab-item/tab-item.css.cjs.map +1 -1
  85. package/dist/disclosure/tab-item/tab-item.css.js +1 -1
  86. package/dist/disclosure/tab-item/tab-item.css.js.map +1 -1
  87. package/dist/disclosure/tab-item/tab-item.d.ts +3 -3
  88. package/dist/disclosure/tab-item/tab-item.d.ts.map +1 -1
  89. package/dist/disclosure/tabs/tabs.css.cjs +1 -1
  90. package/dist/disclosure/tabs/tabs.css.cjs.map +1 -1
  91. package/dist/disclosure/tabs/tabs.css.js +1 -1
  92. package/dist/disclosure/tabs/tabs.css.js.map +1 -1
  93. package/dist/disclosure/tabs/tabs.d.ts +5 -5
  94. package/dist/disclosure/tabs/tabs.d.ts.map +1 -1
  95. package/dist/drawer.cjs +1 -1
  96. package/dist/drawer.cjs.map +1 -1
  97. package/dist/drawer.js +1 -1
  98. package/dist/drawer.js.map +1 -1
  99. package/dist/feedback/skeleton/skeleton.css.cjs +1 -1
  100. package/dist/feedback/skeleton/skeleton.css.cjs.map +1 -1
  101. package/dist/feedback/skeleton/skeleton.css.js +1 -1
  102. package/dist/feedback/skeleton/skeleton.css.js.map +1 -1
  103. package/dist/feedback/skeleton/skeleton.d.ts +1 -1
  104. package/dist/feedback/toast/toast.d.ts.map +1 -1
  105. package/dist/file-input.cjs +1 -1
  106. package/dist/file-input.cjs.map +1 -1
  107. package/dist/file-input.js +2 -2
  108. package/dist/file-input.js.map +1 -1
  109. package/dist/frameworks/elements.d.ts +4 -86
  110. package/dist/frameworks/react.d.ts +5 -4
  111. package/dist/frameworks/vue.d.ts +4 -4
  112. package/dist/grid.cjs +2 -2
  113. package/dist/grid.cjs.map +1 -1
  114. package/dist/grid.js +21 -18
  115. package/dist/grid.js.map +1 -1
  116. package/dist/inputs/combobox/combobox.d.ts +12 -1
  117. package/dist/inputs/combobox/combobox.d.ts.map +1 -1
  118. package/dist/inputs/datagrid/datagrid.d.ts +24 -32
  119. package/dist/inputs/datagrid/datagrid.d.ts.map +1 -1
  120. package/dist/inputs/date-picker/date-picker.css.cjs +1 -1
  121. package/dist/inputs/date-picker/date-picker.css.cjs.map +1 -1
  122. package/dist/inputs/date-picker/date-picker.css.js +1 -1
  123. package/dist/inputs/date-picker/date-picker.css.js.map +1 -1
  124. package/dist/inputs/file-input/file-input-upload.cjs +1 -1
  125. package/dist/inputs/file-input/file-input-upload.cjs.map +1 -1
  126. package/dist/inputs/file-input/file-input-upload.js +1 -1
  127. package/dist/inputs/file-input/file-input-upload.js.map +1 -1
  128. package/dist/inputs/index.d.ts +2 -3
  129. package/dist/inputs/index.d.ts.map +1 -1
  130. package/dist/inputs/input/input.css.cjs +1 -1
  131. package/dist/inputs/input/input.css.cjs.map +1 -1
  132. package/dist/inputs/input/input.css.js +1 -1
  133. package/dist/inputs/input/input.css.js.map +1 -1
  134. package/dist/inputs/shared/field-binding.cjs +1 -1
  135. package/dist/inputs/shared/field-binding.cjs.map +1 -1
  136. package/dist/inputs/shared/field-binding.js +6 -6
  137. package/dist/inputs/shared/field-binding.js.map +1 -1
  138. package/dist/layout/box/box.css.cjs +1 -1
  139. package/dist/layout/box/box.css.cjs.map +1 -1
  140. package/dist/layout/box/box.css.js +1 -1
  141. package/dist/layout/box/box.css.js.map +1 -1
  142. package/dist/layout/box/box.d.ts +3 -3
  143. package/dist/layout/box/box.d.ts.map +1 -1
  144. package/dist/layout/grid/grid.d.ts.map +1 -1
  145. package/dist/layout/navbar/navbar.css.cjs +1 -1
  146. package/dist/layout/navbar/navbar.css.cjs.map +1 -1
  147. package/dist/layout/navbar/navbar.css.js +1 -1
  148. package/dist/layout/navbar/navbar.css.js.map +1 -1
  149. package/dist/layout/navbar/navbar.d.ts +7 -1
  150. package/dist/layout/navbar/navbar.d.ts.map +1 -1
  151. package/dist/layout/sidebar/sidebar-item.css.cjs +1 -1
  152. package/dist/layout/sidebar/sidebar-item.css.cjs.map +1 -1
  153. package/dist/layout/sidebar/sidebar-item.css.js +1 -1
  154. package/dist/layout/sidebar/sidebar-item.css.js.map +1 -1
  155. package/dist/layout/sidebar/sidebar.css.cjs +1 -1
  156. package/dist/layout/sidebar/sidebar.css.cjs.map +1 -1
  157. package/dist/layout/sidebar/sidebar.css.js +1 -1
  158. package/dist/layout/sidebar/sidebar.css.js.map +1 -1
  159. package/dist/layout/sidebar/sidebar.d.ts.map +1 -1
  160. package/dist/menu.cjs +1 -1
  161. package/dist/menu.cjs.map +1 -1
  162. package/dist/menu.js +6 -6
  163. package/dist/menu.js.map +1 -1
  164. package/dist/navbar.cjs +10 -10
  165. package/dist/navbar.cjs.map +1 -1
  166. package/dist/navbar.js +99 -95
  167. package/dist/navbar.js.map +1 -1
  168. package/dist/navigation-menu.cjs +1 -1
  169. package/dist/navigation-menu.cjs.map +1 -1
  170. package/dist/navigation-menu.js +1 -1
  171. package/dist/navigation-menu.js.map +1 -1
  172. package/dist/number-input.cjs +1 -1
  173. package/dist/number-input.cjs.map +1 -1
  174. package/dist/number-input.js +1 -1
  175. package/dist/number-input.js.map +1 -1
  176. package/dist/overlay/popover/popover.d.ts.map +1 -1
  177. package/dist/overlay/tooltip/tooltip.d.ts.map +1 -1
  178. package/dist/popover.cjs +1 -1
  179. package/dist/popover.cjs.map +1 -1
  180. package/dist/popover.js +6 -6
  181. package/dist/popover.js.map +1 -1
  182. package/dist/radio-group.cjs +1 -1
  183. package/dist/radio-group.cjs.map +1 -1
  184. package/dist/radio-group.js +1 -1
  185. package/dist/radio-group.js.map +1 -1
  186. package/dist/radio.cjs +1 -1
  187. package/dist/radio.cjs.map +1 -1
  188. package/dist/radio.js +5 -5
  189. package/dist/radio.js.map +1 -1
  190. package/dist/rating.cjs +1 -1
  191. package/dist/rating.cjs.map +1 -1
  192. package/dist/rating.js +1 -1
  193. package/dist/rating.js.map +1 -1
  194. package/dist/refine.cjs +241 -224
  195. package/dist/refine.cjs.map +1 -1
  196. package/dist/refine.iife.js +297 -280
  197. package/dist/refine.iife.js.map +1 -1
  198. package/dist/refine.js +251 -234
  199. package/dist/refine.js.map +1 -1
  200. package/dist/shared/types.d.ts +0 -2
  201. package/dist/shared/types.d.ts.map +1 -1
  202. package/dist/sidebar.cjs +1 -1
  203. package/dist/sidebar.cjs.map +1 -1
  204. package/dist/sidebar.js +83 -83
  205. package/dist/sidebar.js.map +1 -1
  206. package/dist/skeleton.cjs +1 -1
  207. package/dist/skeleton.cjs.map +1 -1
  208. package/dist/skeleton.js +11 -9
  209. package/dist/skeleton.js.map +1 -1
  210. package/dist/slider.cjs +1 -1
  211. package/dist/slider.cjs.map +1 -1
  212. package/dist/slider.js +20 -20
  213. package/dist/slider.js.map +1 -1
  214. package/dist/stats.cjs +1 -1
  215. package/dist/stats.cjs.map +1 -1
  216. package/dist/stats.js +2 -2
  217. package/dist/stats.js.map +1 -1
  218. package/dist/styles/mixins/field-variants.css.cjs +15 -0
  219. package/dist/styles/mixins/field-variants.css.cjs.map +1 -1
  220. package/dist/styles/mixins/field-variants.css.d.ts.map +1 -1
  221. package/dist/styles/mixins/field-variants.css.js +15 -0
  222. package/dist/styles/mixins/field-variants.css.js.map +1 -1
  223. package/dist/styles/mixins/variants.css.cjs +4 -4
  224. package/dist/styles/mixins/variants.css.cjs.map +1 -1
  225. package/dist/styles/mixins/variants.css.js +4 -4
  226. package/dist/styles/mixins/variants.css.js.map +1 -1
  227. package/dist/tab-item.cjs.map +1 -1
  228. package/dist/tab-item.js.map +1 -1
  229. package/dist/tabs.cjs.map +1 -1
  230. package/dist/tabs.js.map +1 -1
  231. package/dist/toast.cjs +8 -8
  232. package/dist/toast.cjs.map +1 -1
  233. package/dist/toast.js +32 -33
  234. package/dist/toast.js.map +1 -1
  235. package/dist/tooltip.cjs +1 -1
  236. package/dist/tooltip.cjs.map +1 -1
  237. package/dist/tooltip.js +2 -2
  238. package/dist/tooltip.js.map +1 -1
  239. package/dist/types/elements.d.ts +207 -0
  240. package/package.json +13 -13
@@ -1 +1 @@
1
- {"version":3,"file":"field-base.cjs","names":[],"sources":["../../src/core/field-base.ts"],"sourcesContent":["import { createStableId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\nimport { warn } from '../_dev';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n/**\n * The single implementation of \"which modifier class does this counter state map to\" —\n * every field with a character counter (`ore-input`, `ore-textarea`, `ore-message-composer`)\n * renders the identical near-limit/at-limit styling, so this is the one place that decides it.\n *\n * @example\n * ```ts\n * html`<span class=\"${() => counterClassName(counter?.value)}\">...</span>`\n * ```\n */\nexport const counterClassName = (counter: CounterState | undefined, base = 'counter'): string => {\n if (!counter) return base;\n\n if (counter.counterAtLimit) return `${base} at-limit`;\n\n if (counter.counterNearLimit) return `${base} near-limit`;\n\n return base;\n};\n\n// ── Dirty tracking (two-state reset) ───────────────────────────────────────────\n\n/**\n * Tracks whether a control's value has ever been changed by user interaction.\n *\n * Backs the \"two-state reset()\" pattern used by `createCheckable` (`checked`/\n * `indeterminate`) and `createChoiceField` (`selectedValues`): before the first\n * interaction, the live prop is still a reliable \"current default\" to resync from\n * (e.g. an async-loaded value arriving after mount); after it, the prop is\n * contaminated by interaction-driven attribute reflection, so only a snapshot taken\n * at creation still represents \"the default\" in the native sense. `reset()` callers\n * check `isDirty` to decide which source to revert to, then call `clear()`.\n */\nexport type DirtyTracker = {\n clear: () => void;\n readonly isDirty: boolean;\n markDirty: () => void;\n};\n\nexport function createDirtyTracker(): DirtyTracker {\n let dirty = false;\n\n return {\n clear: () => {\n dirty = false;\n },\n get isDirty() {\n return dirty;\n },\n markDirty: () => {\n dirty = true;\n },\n };\n}\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Assistive state (error/helper text, describedby/errormessage/invalid) ─────\n\n/**\n * A field's error/helper/disabled/validation-trigger state — everything an input\n * needs *except* a visible `<label>`. Split out from label state (below) because\n * not every field has one: `ore-message-composer` names itself via `aria-label`\n * (a chat composer's accessible name conventionally comes from context/placeholder,\n * not a floating `<label>`), so it composes this directly instead of pulling in\n * label plumbing it can't use. `createField()` composes both for fields that do\n * render a `<label>`.\n */\nexport type AssistiveStateHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n /**\n * Registers the real form field handle (the return value of `useField()`) so that\n * `triggerValidation()` can call its `reportValidity()`. See `TextFieldHandle.attachFormField`\n * for why this is a post-hoc call instead of a constructor option — the same forward\n * reference applies here for choice fields.\n */\n attachFormField: (formField: { reportValidity(): void }) => void;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\nexport type AssistiveStateOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n validateOn?: Readable<ControlValidationMode>;\n};\n\nexport const createAssistiveState = (options: AssistiveStateOptions): AssistiveStateHandle => {\n const disabled = computed(() => Boolean(options.disabled?.value));\n const assistiveId = createStableId('helper');\n const errorId = createStableId('error');\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n\n let formField: { reportValidity(): void } | null = null;\n const attachFormField = (nextFormField: { reportValidity(): void }): void => {\n formField = nextFormField;\n };\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (options.validateOn?.value !== on) return;\n\n if (!formField) {\n // Not user-facing — this only fires when a component author wired `validateOn` but\n // forgot the matching `attachFormField(useField(...))` call, so validation silently\n // never runs instead of erroring where the mistake actually is.\n warn(\n \"triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.\",\n );\n\n return;\n }\n\n formField.reportValidity();\n };\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n assistiveId,\n attachFormField,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n helperText: computed(() => resolvedAssistive.value.helperText),\n triggerValidation,\n };\n};\n\n// ── Label state (visible `<label>` + `aria-labelledby`) ────────────────────────\n\nexport type LabelStateHandle = {\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /** Stable `id` for the underlying form control — the `<label for=\"...\">` target. */\n fieldId: string;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n};\n\nexport type LabelStateOptions = {\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const label = createLabelState({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createStableId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not consumed by `createLabelState()` itself — visibility is driven solely by\n * `hasLabel`/`label`. Threaded through so component authors can apply\n * placement-specific styling (`label-placement` attribute) without a second\n * option; if placement-dependent visibility is ever needed, wire it into\n * `labelVisible` here instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n};\n\nexport const createLabelState = (options: LabelStateOptions): LabelStateHandle => {\n const fieldId = options.id ?? createStableId(options.prefix ?? 'field');\n const labelId = createStableId('label');\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible = options.hasLabel ?? computed(() => Boolean(label$.value));\n const ariaLabelledBy = computed(() => (labelVisible.value ? labelId : null));\n\n return { ariaLabelledBy, fieldId, labelId, labelVisible };\n};\n\n// ── Field handle (assistive + label, composed) ──────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`. Composes `createAssistiveState` + `createLabelState`;\n * call those directly instead if a field has no visible `<label>` (see\n * `createAssistiveState`'s doc comment).\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects. `labelId`/`labelVisible`/`ariaLabelledBy` are always\n * present but only meaningful when `label`/`hasLabel` was passed — omit both\n * and they resolve to an always-hidden, unreferenced label.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * aria-labelledby=\"${ariaLabelledBy}\"\n * aria-describedby=\"${ariaDescribedBy}\"\n * aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\nexport type FieldHandle = AssistiveStateHandle & LabelStateHandle;\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = AssistiveStateOptions & LabelStateOptions;\n\nexport const createField = (options: FieldOptions): FieldHandle => ({\n ...createAssistiveState(options),\n ...createLabelState(options),\n});\n"],"mappings":"oFAsBA,IAAa,EAA0B,IAAA,EACrC,EAAA,SAAA,MAAgB,CACd,UAAW,EAAQ,OAAO,OAAS,GACnC,WAAY,EAAQ,QAAQ,OAAS,EACvC,EAAE,EAyBS,EAAsB,IAAA,EACjC,EAAA,SAAA,KAA6B,CAC3B,IAAM,EAAQ,EAAQ,OAAO,OAAS,GAChC,EAAY,EAAQ,WAAW,MAC/B,EAAkB,OAAO,CAAS,EAClC,EAAiB,OAAO,SAAS,CAAe,GAAK,EAAkB,EAAI,EAAkB,KAC7F,EAAa,IAAmB,KAChC,EAAc,EAAa,GAAG,EAAM,OAAO,KAAK,IAAmB,GACnE,EAAQ,EAAa,EAAM,OAAS,EAAiB,EAE3D,MAAO,CACL,eAAgB,EAAa,GAAS,EAAI,GAC1C,iBAAkB,EAAa,GAAS,IAAO,EAAQ,EAAI,GAC3D,aACF,CACF,CAAC,EAYU,GAAoB,EAAmC,EAAO,YACpE,EAED,EAAQ,eAAuB,GAAG,EAAK,WAEvC,EAAQ,iBAAyB,GAAG,EAAK,aAEtC,EANc,EA4BvB,SAAgB,GAAmC,CACjD,IAAI,EAAQ,GAEZ,MAAO,CACL,UAAa,CACX,EAAQ,EACV,EACA,IAAI,SAAU,CACZ,OAAO,CACT,EACA,cAAiB,CACf,EAAQ,EACV,CACF,CACF,CAqDA,IAAa,EAAwB,GAAyD,CAC5F,IAAM,GAAA,EAAW,EAAA,SAAA,KAAe,EAAQ,EAAQ,UAAU,KAAM,EAC1D,GAAA,EAAc,EAAA,eAAA,CAAe,QAAQ,EACrC,GAAA,EAAU,EAAA,eAAA,CAAe,OAAO,EAChC,EAAoB,EAAuB,CAAE,MAAO,EAAQ,MAAO,OAAQ,EAAQ,MAAO,CAAC,EAE3F,GAAA,EAAkB,EAAA,SAAA,KACtB,EAAkB,MAAM,WAAa,EAAkB,MAAM,WAAa,EAAc,IAC1F,EACM,GAAA,EAAmB,EAAA,SAAA,KAAgB,EAAkB,MAAM,UAAY,EAAU,IAAK,EACtF,GAAA,EAAc,EAAA,SAAA,KAA+B,EAAkB,MAAM,UAAY,OAAS,IAAK,EAEjG,EAA+C,KAsBnD,MAAO,CACL,kBACA,mBACA,cACA,cACA,gBA1BuB,GAAoD,CAC3E,EAAY,CACd,EAyBE,WACA,UACA,WAAA,EAAW,EAAA,SAAA,KAAe,EAAkB,MAAM,SAAS,EAC3D,YAAA,EAAY,EAAA,SAAA,KAAe,EAAkB,MAAM,UAAU,EAC7D,kBA3ByB,GAA4D,CACjF,EAAQ,YAAY,QAAU,GAE7B,GAWL,EAAU,eAAe,CAC3B,CAaA,CACF,EAmDa,EAAoB,GAAiD,CAChF,IAAM,EAAU,EAAQ,KAAA,EAAM,EAAA,eAAA,CAAe,EAAQ,QAAU,OAAO,EAChE,GAAA,EAAU,EAAA,eAAA,CAAe,OAAO,EAChC,EAAS,EAAQ,QAAA,EAAS,EAAA,SAAA,KAAe,IAAA,EAAS,EAMlD,EAAe,EAAQ,WAAA,EAAY,EAAA,SAAA,KAAe,EAAQ,EAAO,KAAM,EAG7E,MAAO,CAAE,gBAAA,EAFc,EAAA,SAAA,KAAgB,EAAa,MAAQ,EAAU,IAE7D,EAAgB,UAAS,UAAS,cAAa,CAC1D,EA6Ba,EAAe,IAAwC,CAClE,GAAG,EAAqB,CAAO,EAC/B,GAAG,EAAiB,CAAO,CAC7B"}
1
+ {"version":3,"file":"field-base.cjs","names":[],"sources":["../../src/core/field-base.ts"],"sourcesContent":["import { createId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\nimport { warn } from '../_dev';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n/**\n * The single implementation of \"which modifier class does this counter state map to\" —\n * every field with a character counter (`ore-input`, `ore-textarea`, `ore-message-composer`)\n * renders the identical near-limit/at-limit styling, so this is the one place that decides it.\n *\n * @example\n * ```ts\n * html`<span class=\"${() => counterClassName(counter?.value)}\">...</span>`\n * ```\n */\nexport const counterClassName = (counter: CounterState | undefined, base = 'counter'): string => {\n if (!counter) return base;\n\n if (counter.counterAtLimit) return `${base} at-limit`;\n\n if (counter.counterNearLimit) return `${base} near-limit`;\n\n return base;\n};\n\n// ── Dirty tracking (two-state reset) ───────────────────────────────────────────\n\n/**\n * Tracks whether a control's value has ever been changed by user interaction.\n *\n * Backs the \"two-state reset()\" pattern used by `createCheckable` (`checked`/\n * `indeterminate`) and `createChoiceField` (`selectedValues`): before the first\n * interaction, the live prop is still a reliable \"current default\" to resync from\n * (e.g. an async-loaded value arriving after mount); after it, the prop is\n * contaminated by interaction-driven attribute reflection, so only a snapshot taken\n * at creation still represents \"the default\" in the native sense. `reset()` callers\n * check `isDirty` to decide which source to revert to, then call `clear()`.\n */\nexport type DirtyTracker = {\n clear: () => void;\n readonly isDirty: boolean;\n markDirty: () => void;\n};\n\nexport function createDirtyTracker(): DirtyTracker {\n let dirty = false;\n\n return {\n clear: () => {\n dirty = false;\n },\n get isDirty() {\n return dirty;\n },\n markDirty: () => {\n dirty = true;\n },\n };\n}\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Assistive state (error/helper text, describedby/errormessage/invalid) ─────\n\n/**\n * A field's error/helper/disabled/validation-trigger state — everything an input\n * needs *except* a visible `<label>`. Split out from label state (below) because\n * not every field has one: `ore-message-composer` names itself via `aria-label`\n * (a chat composer's accessible name conventionally comes from context/placeholder,\n * not a floating `<label>`), so it composes this directly instead of pulling in\n * label plumbing it can't use. `createField()` composes both for fields that do\n * render a `<label>`.\n */\nexport type AssistiveStateHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n /**\n * Registers the real form field handle (the return value of `useField()`) so that\n * `triggerValidation()` can call its `reportValidity()`. See `TextFieldHandle.attachFormField`\n * for why this is a post-hoc call instead of a constructor option — the same forward\n * reference applies here for choice fields.\n */\n attachFormField: (formField: { reportValidity(): void }) => void;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\nexport type AssistiveStateOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n validateOn?: Readable<ControlValidationMode>;\n};\n\nexport const createAssistiveState = (options: AssistiveStateOptions): AssistiveStateHandle => {\n const disabled = computed(() => Boolean(options.disabled?.value));\n const assistiveId = createId('helper');\n const errorId = createId('error');\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n\n let formField: { reportValidity(): void } | null = null;\n const attachFormField = (nextFormField: { reportValidity(): void }): void => {\n formField = nextFormField;\n };\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (options.validateOn?.value !== on) return;\n\n if (!formField) {\n // Not user-facing — this only fires when a component author wired `validateOn` but\n // forgot the matching `attachFormField(useField(...))` call, so validation silently\n // never runs instead of erroring where the mistake actually is.\n warn(\n \"triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.\",\n );\n\n return;\n }\n\n formField.reportValidity();\n };\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n assistiveId,\n attachFormField,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n helperText: computed(() => resolvedAssistive.value.helperText),\n triggerValidation,\n };\n};\n\n// ── Label state (visible `<label>` + `aria-labelledby`) ────────────────────────\n\nexport type LabelStateHandle = {\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /** Stable `id` for the underlying form control — the `<label for=\"...\">` target. */\n fieldId: string;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n};\n\nexport type LabelStateOptions = {\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const label = createLabelState({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not consumed by `createLabelState()` itself — visibility is driven solely by\n * `hasLabel`/`label`. Threaded through so component authors can apply\n * placement-specific styling (`label-placement` attribute) without a second\n * option; if placement-dependent visibility is ever needed, wire it into\n * `labelVisible` here instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n};\n\nexport const createLabelState = (options: LabelStateOptions): LabelStateHandle => {\n const fieldId = options.id ?? createId(options.prefix ?? 'field');\n const labelId = createId('label');\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible = options.hasLabel ?? computed(() => Boolean(label$.value));\n const ariaLabelledBy = computed(() => (labelVisible.value ? labelId : null));\n\n return { ariaLabelledBy, fieldId, labelId, labelVisible };\n};\n\n// ── Field handle (assistive + label, composed) ──────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`. Composes `createAssistiveState` + `createLabelState`;\n * call those directly instead if a field has no visible `<label>` (see\n * `createAssistiveState`'s doc comment).\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects. `labelId`/`labelVisible`/`ariaLabelledBy` are always\n * present but only meaningful when `label`/`hasLabel` was passed — omit both\n * and they resolve to an always-hidden, unreferenced label.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * aria-labelledby=\"${ariaLabelledBy}\"\n * aria-describedby=\"${ariaDescribedBy}\"\n * aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\nexport type FieldHandle = AssistiveStateHandle & LabelStateHandle;\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = AssistiveStateOptions & LabelStateOptions;\n\nexport const createField = (options: FieldOptions): FieldHandle => ({\n ...createAssistiveState(options),\n ...createLabelState(options),\n});\n"],"mappings":"oFAsBA,IAAa,EAA0B,IAAA,EACrC,EAAA,SAAA,MAAgB,CACd,UAAW,EAAQ,OAAO,OAAS,GACnC,WAAY,EAAQ,QAAQ,OAAS,EACvC,EAAE,EAyBS,EAAsB,IAAA,EACjC,EAAA,SAAA,KAA6B,CAC3B,IAAM,EAAQ,EAAQ,OAAO,OAAS,GAChC,EAAY,EAAQ,WAAW,MAC/B,EAAkB,OAAO,CAAS,EAClC,EAAiB,OAAO,SAAS,CAAe,GAAK,EAAkB,EAAI,EAAkB,KAC7F,EAAa,IAAmB,KAChC,EAAc,EAAa,GAAG,EAAM,OAAO,KAAK,IAAmB,GACnE,EAAQ,EAAa,EAAM,OAAS,EAAiB,EAE3D,MAAO,CACL,eAAgB,EAAa,GAAS,EAAI,GAC1C,iBAAkB,EAAa,GAAS,IAAO,EAAQ,EAAI,GAC3D,aACF,CACF,CAAC,EAYU,GAAoB,EAAmC,EAAO,YACpE,EAED,EAAQ,eAAuB,GAAG,EAAK,WAEvC,EAAQ,iBAAyB,GAAG,EAAK,aAEtC,EANc,EA4BvB,SAAgB,GAAmC,CACjD,IAAI,EAAQ,GAEZ,MAAO,CACL,UAAa,CACX,EAAQ,EACV,EACA,IAAI,SAAU,CACZ,OAAO,CACT,EACA,cAAiB,CACf,EAAQ,EACV,CACF,CACF,CAqDA,IAAa,EAAwB,GAAyD,CAC5F,IAAM,GAAA,EAAW,EAAA,SAAA,KAAe,EAAQ,EAAQ,UAAU,KAAM,EAC1D,GAAA,EAAc,EAAA,SAAA,CAAS,QAAQ,EAC/B,GAAA,EAAU,EAAA,SAAA,CAAS,OAAO,EAC1B,EAAoB,EAAuB,CAAE,MAAO,EAAQ,MAAO,OAAQ,EAAQ,MAAO,CAAC,EAE3F,GAAA,EAAkB,EAAA,SAAA,KACtB,EAAkB,MAAM,WAAa,EAAkB,MAAM,WAAa,EAAc,IAC1F,EACM,GAAA,EAAmB,EAAA,SAAA,KAAgB,EAAkB,MAAM,UAAY,EAAU,IAAK,EACtF,GAAA,EAAc,EAAA,SAAA,KAA+B,EAAkB,MAAM,UAAY,OAAS,IAAK,EAEjG,EAA+C,KAsBnD,MAAO,CACL,kBACA,mBACA,cACA,cACA,gBA1BuB,GAAoD,CAC3E,EAAY,CACd,EAyBE,WACA,UACA,WAAA,EAAW,EAAA,SAAA,KAAe,EAAkB,MAAM,SAAS,EAC3D,YAAA,EAAY,EAAA,SAAA,KAAe,EAAkB,MAAM,UAAU,EAC7D,kBA3ByB,GAA4D,CACjF,EAAQ,YAAY,QAAU,GAE7B,GAWL,EAAU,eAAe,CAC3B,CAaA,CACF,EAmDa,EAAoB,GAAiD,CAChF,IAAM,EAAU,EAAQ,KAAA,EAAM,EAAA,SAAA,CAAS,EAAQ,QAAU,OAAO,EAC1D,GAAA,EAAU,EAAA,SAAA,CAAS,OAAO,EAC1B,EAAS,EAAQ,QAAA,EAAS,EAAA,SAAA,KAAe,IAAA,EAAS,EAMlD,EAAe,EAAQ,WAAA,EAAY,EAAA,SAAA,KAAe,EAAQ,EAAO,KAAM,EAG7E,MAAO,CAAE,gBAAA,EAFc,EAAA,SAAA,KAAgB,EAAa,MAAQ,EAAU,IAE7D,EAAgB,UAAS,UAAS,cAAa,CAC1D,EA6Ba,EAAe,IAAwC,CAClE,GAAG,EAAqB,CAAO,EAC/B,GAAG,EAAiB,CAAO,CAC7B"}
@@ -130,7 +130,7 @@ export type LabelStateOptions = {
130
130
  hasLabel?: Readable<boolean>;
131
131
  /**
132
132
  * When provided, used directly as `fieldId` instead of generating one via
133
- * `createStableId`. Useful in tests for deterministic ID assertions.
133
+ * `createId`. Useful in tests for deterministic ID assertions.
134
134
  */
135
135
  id?: string;
136
136
  /**
@@ -1,5 +1,5 @@
1
1
  import "../_dev.js";
2
- import { createStableId as e } from "@vielzeug/ore";
2
+ import { createId as e } from "@vielzeug/ore";
3
3
  import { computed as t } from "@vielzeug/ripple";
4
4
  //#region src/core/field-base.ts
5
5
  var n = (e) => t(() => ({
@@ -1 +1 @@
1
- {"version":3,"file":"field-base.js","names":[],"sources":["../../src/core/field-base.ts"],"sourcesContent":["import { createStableId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\nimport { warn } from '../_dev';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n/**\n * The single implementation of \"which modifier class does this counter state map to\" —\n * every field with a character counter (`ore-input`, `ore-textarea`, `ore-message-composer`)\n * renders the identical near-limit/at-limit styling, so this is the one place that decides it.\n *\n * @example\n * ```ts\n * html`<span class=\"${() => counterClassName(counter?.value)}\">...</span>`\n * ```\n */\nexport const counterClassName = (counter: CounterState | undefined, base = 'counter'): string => {\n if (!counter) return base;\n\n if (counter.counterAtLimit) return `${base} at-limit`;\n\n if (counter.counterNearLimit) return `${base} near-limit`;\n\n return base;\n};\n\n// ── Dirty tracking (two-state reset) ───────────────────────────────────────────\n\n/**\n * Tracks whether a control's value has ever been changed by user interaction.\n *\n * Backs the \"two-state reset()\" pattern used by `createCheckable` (`checked`/\n * `indeterminate`) and `createChoiceField` (`selectedValues`): before the first\n * interaction, the live prop is still a reliable \"current default\" to resync from\n * (e.g. an async-loaded value arriving after mount); after it, the prop is\n * contaminated by interaction-driven attribute reflection, so only a snapshot taken\n * at creation still represents \"the default\" in the native sense. `reset()` callers\n * check `isDirty` to decide which source to revert to, then call `clear()`.\n */\nexport type DirtyTracker = {\n clear: () => void;\n readonly isDirty: boolean;\n markDirty: () => void;\n};\n\nexport function createDirtyTracker(): DirtyTracker {\n let dirty = false;\n\n return {\n clear: () => {\n dirty = false;\n },\n get isDirty() {\n return dirty;\n },\n markDirty: () => {\n dirty = true;\n },\n };\n}\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Assistive state (error/helper text, describedby/errormessage/invalid) ─────\n\n/**\n * A field's error/helper/disabled/validation-trigger state — everything an input\n * needs *except* a visible `<label>`. Split out from label state (below) because\n * not every field has one: `ore-message-composer` names itself via `aria-label`\n * (a chat composer's accessible name conventionally comes from context/placeholder,\n * not a floating `<label>`), so it composes this directly instead of pulling in\n * label plumbing it can't use. `createField()` composes both for fields that do\n * render a `<label>`.\n */\nexport type AssistiveStateHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n /**\n * Registers the real form field handle (the return value of `useField()`) so that\n * `triggerValidation()` can call its `reportValidity()`. See `TextFieldHandle.attachFormField`\n * for why this is a post-hoc call instead of a constructor option — the same forward\n * reference applies here for choice fields.\n */\n attachFormField: (formField: { reportValidity(): void }) => void;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\nexport type AssistiveStateOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n validateOn?: Readable<ControlValidationMode>;\n};\n\nexport const createAssistiveState = (options: AssistiveStateOptions): AssistiveStateHandle => {\n const disabled = computed(() => Boolean(options.disabled?.value));\n const assistiveId = createStableId('helper');\n const errorId = createStableId('error');\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n\n let formField: { reportValidity(): void } | null = null;\n const attachFormField = (nextFormField: { reportValidity(): void }): void => {\n formField = nextFormField;\n };\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (options.validateOn?.value !== on) return;\n\n if (!formField) {\n // Not user-facing — this only fires when a component author wired `validateOn` but\n // forgot the matching `attachFormField(useField(...))` call, so validation silently\n // never runs instead of erroring where the mistake actually is.\n warn(\n \"triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.\",\n );\n\n return;\n }\n\n formField.reportValidity();\n };\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n assistiveId,\n attachFormField,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n helperText: computed(() => resolvedAssistive.value.helperText),\n triggerValidation,\n };\n};\n\n// ── Label state (visible `<label>` + `aria-labelledby`) ────────────────────────\n\nexport type LabelStateHandle = {\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /** Stable `id` for the underlying form control — the `<label for=\"...\">` target. */\n fieldId: string;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n};\n\nexport type LabelStateOptions = {\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const label = createLabelState({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createStableId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not consumed by `createLabelState()` itself — visibility is driven solely by\n * `hasLabel`/`label`. Threaded through so component authors can apply\n * placement-specific styling (`label-placement` attribute) without a second\n * option; if placement-dependent visibility is ever needed, wire it into\n * `labelVisible` here instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n};\n\nexport const createLabelState = (options: LabelStateOptions): LabelStateHandle => {\n const fieldId = options.id ?? createStableId(options.prefix ?? 'field');\n const labelId = createStableId('label');\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible = options.hasLabel ?? computed(() => Boolean(label$.value));\n const ariaLabelledBy = computed(() => (labelVisible.value ? labelId : null));\n\n return { ariaLabelledBy, fieldId, labelId, labelVisible };\n};\n\n// ── Field handle (assistive + label, composed) ──────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`. Composes `createAssistiveState` + `createLabelState`;\n * call those directly instead if a field has no visible `<label>` (see\n * `createAssistiveState`'s doc comment).\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects. `labelId`/`labelVisible`/`ariaLabelledBy` are always\n * present but only meaningful when `label`/`hasLabel` was passed — omit both\n * and they resolve to an always-hidden, unreferenced label.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * aria-labelledby=\"${ariaLabelledBy}\"\n * aria-describedby=\"${ariaDescribedBy}\"\n * aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\nexport type FieldHandle = AssistiveStateHandle & LabelStateHandle;\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = AssistiveStateOptions & LabelStateOptions;\n\nexport const createField = (options: FieldOptions): FieldHandle => ({\n ...createAssistiveState(options),\n ...createLabelState(options),\n});\n"],"mappings":";;;;AAsBA,IAAa,KAA0B,MACrC,SAAgB;CACd,WAAW,EAAQ,OAAO,SAAS;CACnC,YAAY,EAAQ,QAAQ,SAAS;AACvC,EAAE,GAyBS,KAAsB,MACjC,QAA6B;CAC3B,IAAM,IAAQ,EAAQ,OAAO,SAAS,IAChC,IAAY,EAAQ,WAAW,OAC/B,IAAkB,OAAO,CAAS,GAClC,IAAiB,OAAO,SAAS,CAAe,KAAK,IAAkB,IAAI,IAAkB,MAC7F,IAAa,MAAmB,MAChC,IAAc,IAAa,GAAG,EAAM,OAAO,KAAK,MAAmB,IACnE,IAAQ,IAAa,EAAM,SAAS,IAAiB;CAE3D,OAAO;EACL,gBAAgB,IAAa,KAAS,IAAI;EAC1C,kBAAkB,IAAa,KAAS,MAAO,IAAQ,IAAI;EAC3D;CACF;AACF,CAAC,GAYU,KAAoB,GAAmC,IAAO,cACpE,IAED,EAAQ,iBAAuB,GAAG,EAAK,aAEvC,EAAQ,mBAAyB,GAAG,EAAK,eAEtC,IANc;AA4BvB,SAAgB,IAAmC;CACjD,IAAI,IAAQ;CAEZ,OAAO;EACL,aAAa;GACX,IAAQ;EACV;EACA,IAAI,UAAU;GACZ,OAAO;EACT;EACA,iBAAiB;GACf,IAAQ;EACV;CACF;AACF;AAqDA,IAAa,KAAwB,MAAyD;CAC5F,IAAM,IAAW,QAAe,EAAQ,EAAQ,UAAU,KAAM,GAC1D,IAAc,EAAe,QAAQ,GACrC,IAAU,EAAe,OAAO,GAChC,IAAoB,EAAuB;EAAE,OAAO,EAAQ;EAAO,QAAQ,EAAQ;CAAO,CAAC,GAE3F,IAAkB,QACtB,EAAkB,MAAM,aAAa,EAAkB,MAAM,aAAa,IAAc,IAC1F,GACM,IAAmB,QAAgB,EAAkB,MAAM,YAAY,IAAU,IAAK,GACtF,IAAc,QAA+B,EAAkB,MAAM,YAAY,SAAS,IAAK,GAEjG,IAA+C;CAsBnD,OAAO;EACL;EACA;EACA;EACA;EACA,kBA1BuB,MAAoD;GAC3E,IAAY;EACd;EAyBE;EACA;EACA,WAAW,QAAe,EAAkB,MAAM,SAAS;EAC3D,YAAY,QAAe,EAAkB,MAAM,UAAU;EAC7D,oBA3ByB,MAA4D;GACjF,EAAQ,YAAY,UAAU,KAE7B,KAWL,EAAU,eAAe;EAC3B;CAaA;AACF,GAmDa,KAAoB,MAAiD;CAChF,IAAM,IAAU,EAAQ,MAAM,EAAe,EAAQ,UAAU,OAAO,GAChE,IAAU,EAAe,OAAO,GAChC,IAAS,EAAQ,SAAS,QAAe,KAAA,CAAS,GAMlD,IAAe,EAAQ,YAAY,QAAe,EAAQ,EAAO,KAAM;CAG7E,OAAO;EAAE,gBAFc,QAAgB,EAAa,QAAQ,IAAU,IAE7D;EAAgB;EAAS;EAAS;CAAa;AAC1D,GA6Ba,KAAe,OAAwC;CAClE,GAAG,EAAqB,CAAO;CAC/B,GAAG,EAAiB,CAAO;AAC7B"}
1
+ {"version":3,"file":"field-base.js","names":[],"sources":["../../src/core/field-base.ts"],"sourcesContent":["import { createId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\nimport { warn } from '../_dev';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n/**\n * The single implementation of \"which modifier class does this counter state map to\" —\n * every field with a character counter (`ore-input`, `ore-textarea`, `ore-message-composer`)\n * renders the identical near-limit/at-limit styling, so this is the one place that decides it.\n *\n * @example\n * ```ts\n * html`<span class=\"${() => counterClassName(counter?.value)}\">...</span>`\n * ```\n */\nexport const counterClassName = (counter: CounterState | undefined, base = 'counter'): string => {\n if (!counter) return base;\n\n if (counter.counterAtLimit) return `${base} at-limit`;\n\n if (counter.counterNearLimit) return `${base} near-limit`;\n\n return base;\n};\n\n// ── Dirty tracking (two-state reset) ───────────────────────────────────────────\n\n/**\n * Tracks whether a control's value has ever been changed by user interaction.\n *\n * Backs the \"two-state reset()\" pattern used by `createCheckable` (`checked`/\n * `indeterminate`) and `createChoiceField` (`selectedValues`): before the first\n * interaction, the live prop is still a reliable \"current default\" to resync from\n * (e.g. an async-loaded value arriving after mount); after it, the prop is\n * contaminated by interaction-driven attribute reflection, so only a snapshot taken\n * at creation still represents \"the default\" in the native sense. `reset()` callers\n * check `isDirty` to decide which source to revert to, then call `clear()`.\n */\nexport type DirtyTracker = {\n clear: () => void;\n readonly isDirty: boolean;\n markDirty: () => void;\n};\n\nexport function createDirtyTracker(): DirtyTracker {\n let dirty = false;\n\n return {\n clear: () => {\n dirty = false;\n },\n get isDirty() {\n return dirty;\n },\n markDirty: () => {\n dirty = true;\n },\n };\n}\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Assistive state (error/helper text, describedby/errormessage/invalid) ─────\n\n/**\n * A field's error/helper/disabled/validation-trigger state — everything an input\n * needs *except* a visible `<label>`. Split out from label state (below) because\n * not every field has one: `ore-message-composer` names itself via `aria-label`\n * (a chat composer's accessible name conventionally comes from context/placeholder,\n * not a floating `<label>`), so it composes this directly instead of pulling in\n * label plumbing it can't use. `createField()` composes both for fields that do\n * render a `<label>`.\n */\nexport type AssistiveStateHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n /**\n * Registers the real form field handle (the return value of `useField()`) so that\n * `triggerValidation()` can call its `reportValidity()`. See `TextFieldHandle.attachFormField`\n * for why this is a post-hoc call instead of a constructor option — the same forward\n * reference applies here for choice fields.\n */\n attachFormField: (formField: { reportValidity(): void }) => void;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\nexport type AssistiveStateOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n validateOn?: Readable<ControlValidationMode>;\n};\n\nexport const createAssistiveState = (options: AssistiveStateOptions): AssistiveStateHandle => {\n const disabled = computed(() => Boolean(options.disabled?.value));\n const assistiveId = createId('helper');\n const errorId = createId('error');\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n\n let formField: { reportValidity(): void } | null = null;\n const attachFormField = (nextFormField: { reportValidity(): void }): void => {\n formField = nextFormField;\n };\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (options.validateOn?.value !== on) return;\n\n if (!formField) {\n // Not user-facing — this only fires when a component author wired `validateOn` but\n // forgot the matching `attachFormField(useField(...))` call, so validation silently\n // never runs instead of erroring where the mistake actually is.\n warn(\n \"triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.\",\n );\n\n return;\n }\n\n formField.reportValidity();\n };\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n assistiveId,\n attachFormField,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n helperText: computed(() => resolvedAssistive.value.helperText),\n triggerValidation,\n };\n};\n\n// ── Label state (visible `<label>` + `aria-labelledby`) ────────────────────────\n\nexport type LabelStateHandle = {\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /** Stable `id` for the underlying form control — the `<label for=\"...\">` target. */\n fieldId: string;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n};\n\nexport type LabelStateOptions = {\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const label = createLabelState({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not consumed by `createLabelState()` itself — visibility is driven solely by\n * `hasLabel`/`label`. Threaded through so component authors can apply\n * placement-specific styling (`label-placement` attribute) without a second\n * option; if placement-dependent visibility is ever needed, wire it into\n * `labelVisible` here instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n};\n\nexport const createLabelState = (options: LabelStateOptions): LabelStateHandle => {\n const fieldId = options.id ?? createId(options.prefix ?? 'field');\n const labelId = createId('label');\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible = options.hasLabel ?? computed(() => Boolean(label$.value));\n const ariaLabelledBy = computed(() => (labelVisible.value ? labelId : null));\n\n return { ariaLabelledBy, fieldId, labelId, labelVisible };\n};\n\n// ── Field handle (assistive + label, composed) ──────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`. Composes `createAssistiveState` + `createLabelState`;\n * call those directly instead if a field has no visible `<label>` (see\n * `createAssistiveState`'s doc comment).\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects. `labelId`/`labelVisible`/`ariaLabelledBy` are always\n * present but only meaningful when `label`/`hasLabel` was passed — omit both\n * and they resolve to an always-hidden, unreferenced label.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * aria-labelledby=\"${ariaLabelledBy}\"\n * aria-describedby=\"${ariaDescribedBy}\"\n * aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\nexport type FieldHandle = AssistiveStateHandle & LabelStateHandle;\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = AssistiveStateOptions & LabelStateOptions;\n\nexport const createField = (options: FieldOptions): FieldHandle => ({\n ...createAssistiveState(options),\n ...createLabelState(options),\n});\n"],"mappings":";;;;AAsBA,IAAa,KAA0B,MACrC,SAAgB;CACd,WAAW,EAAQ,OAAO,SAAS;CACnC,YAAY,EAAQ,QAAQ,SAAS;AACvC,EAAE,GAyBS,KAAsB,MACjC,QAA6B;CAC3B,IAAM,IAAQ,EAAQ,OAAO,SAAS,IAChC,IAAY,EAAQ,WAAW,OAC/B,IAAkB,OAAO,CAAS,GAClC,IAAiB,OAAO,SAAS,CAAe,KAAK,IAAkB,IAAI,IAAkB,MAC7F,IAAa,MAAmB,MAChC,IAAc,IAAa,GAAG,EAAM,OAAO,KAAK,MAAmB,IACnE,IAAQ,IAAa,EAAM,SAAS,IAAiB;CAE3D,OAAO;EACL,gBAAgB,IAAa,KAAS,IAAI;EAC1C,kBAAkB,IAAa,KAAS,MAAO,IAAQ,IAAI;EAC3D;CACF;AACF,CAAC,GAYU,KAAoB,GAAmC,IAAO,cACpE,IAED,EAAQ,iBAAuB,GAAG,EAAK,aAEvC,EAAQ,mBAAyB,GAAG,EAAK,eAEtC,IANc;AA4BvB,SAAgB,IAAmC;CACjD,IAAI,IAAQ;CAEZ,OAAO;EACL,aAAa;GACX,IAAQ;EACV;EACA,IAAI,UAAU;GACZ,OAAO;EACT;EACA,iBAAiB;GACf,IAAQ;EACV;CACF;AACF;AAqDA,IAAa,KAAwB,MAAyD;CAC5F,IAAM,IAAW,QAAe,EAAQ,EAAQ,UAAU,KAAM,GAC1D,IAAc,EAAS,QAAQ,GAC/B,IAAU,EAAS,OAAO,GAC1B,IAAoB,EAAuB;EAAE,OAAO,EAAQ;EAAO,QAAQ,EAAQ;CAAO,CAAC,GAE3F,IAAkB,QACtB,EAAkB,MAAM,aAAa,EAAkB,MAAM,aAAa,IAAc,IAC1F,GACM,IAAmB,QAAgB,EAAkB,MAAM,YAAY,IAAU,IAAK,GACtF,IAAc,QAA+B,EAAkB,MAAM,YAAY,SAAS,IAAK,GAEjG,IAA+C;CAsBnD,OAAO;EACL;EACA;EACA;EACA;EACA,kBA1BuB,MAAoD;GAC3E,IAAY;EACd;EAyBE;EACA;EACA,WAAW,QAAe,EAAkB,MAAM,SAAS;EAC3D,YAAY,QAAe,EAAkB,MAAM,UAAU;EAC7D,oBA3ByB,MAA4D;GACjF,EAAQ,YAAY,UAAU,KAE7B,KAWL,EAAU,eAAe;EAC3B;CAaA;AACF,GAmDa,KAAoB,MAAiD;CAChF,IAAM,IAAU,EAAQ,MAAM,EAAS,EAAQ,UAAU,OAAO,GAC1D,IAAU,EAAS,OAAO,GAC1B,IAAS,EAAQ,SAAS,QAAe,KAAA,CAAS,GAMlD,IAAe,EAAQ,YAAY,QAAe,EAAQ,EAAO,KAAM;CAG7E,OAAO;EAAE,gBAFc,QAAgB,EAAa,QAAQ,IAAU,IAE7D;EAAgB;EAAS;EAAS;CAAa;AAC1D,GA6Ba,KAAe,OAAwC;CAClE,GAAG,EAAqB,CAAO;CAC/B,GAAG,EAAiB,CAAO;AAC7B"}
package/dist/core/nav.cjs CHANGED
@@ -1,2 +1,2 @@
1
- let e=require("@vielzeug/ripple"),t=require("@vielzeug/focus");var n=n=>{let r=(0,e.signal)(-1),i,a=(0,t.createListNavigation)({direction:n.direction,disabled:()=>!!n.disabled?.value,getItems:n.getItems,isItemDisabled:n.isItemDisabled,keys:n.keys,loop:n.loop,onNavigate:e=>{r.value=e.index,n.onNavigate?.(e)},orientation:n.orientation,typeahead:n.typeahead}),o=()=>{r.value=a.getIndex()},s=e=>{let t=a.set(e);return o(),t},c=e=>{let t=a.navigate(e);return o(),t},l=()=>{a.reset(),o()},u=e=>{let t=a.handleKeydown(e);return o(),t},d=()=>{a.dispose(),o(),i?.(),i=void 0};return n.signal?.aborted?d():n.signal&&(n.signal?.addEventListener(`abort`,d,{once:!0}),i=()=>n.signal?.removeEventListener(`abort`,d)),{dispose:d,get disposed(){return a.disposed},focusedIndex:r,getActiveItem:()=>a.getActiveItem(),handleKeydown:u,navigate:c,reset:l,set:s,[Symbol.dispose]:d}};exports.createListControl=n;
1
+ let e=require("@vielzeug/ripple"),t=require("@vielzeug/focus");var n=n=>{let r=new AbortController,i=(0,e.signal)(-1),a=!1,o,s=(0,t.createListNavigation)({direction:n.direction,disabled:()=>!!n.disabled?.value,getItems:n.getItems,isItemDisabled:n.isItemDisabled,keys:n.keys,loop:n.loop,orientation:n.orientation,typeahead:n.typeahead}),c=()=>{i.value=s.getIndex()},l=e=>{if(e){i.value=e.index;try{n.onNavigate?.(e)}finally{c()}}return s.getIndex()},u=e=>{if(a)return-1;let t=s.set(e);return c(),t},d=e=>{if(a)return-1;let t=s.navigate(e);return l(t)},f=()=>{a||(s.reset(),c())},p=e=>{if(a)return!1;let t=s.handleKeydown(e);return t?(t.change&&l(t.change),t.handled):!1},m=()=>{a||(a=!0,s.reset(),i.value=-1,r.abort(),o?.(),o=void 0)};return n.signal?.aborted?m():n.signal&&(n.signal?.addEventListener(`abort`,m,{once:!0}),o=()=>n.signal?.removeEventListener(`abort`,m)),{get disposalSignal(){return r.signal},dispose:m,get disposed(){return a},focusedIndex:i,getActiveItem:()=>a?void 0:s.getActiveItem(),handleKeydown:p,navigate:d,reset:f,set:u,[Symbol.dispose]:m}};exports.createListControl=n;
2
2
  //# sourceMappingURL=nav.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"nav.cjs","names":[],"sources":["../../src/core/nav.ts"],"sourcesContent":["import {\n createListNavigation,\n type ListNavigationOptions as FocusListNavigationOptions,\n type ListKeyAction,\n type ListNavigation,\n type ListNavigationAction,\n type ListNavigationChange,\n type ListNavigationTypeaheadOptions,\n} from '@vielzeug/focus';\nimport { type Readable, signal } from '@vielzeug/ripple';\n\nexport type ListNavigationOptions<T> = Omit<FocusListNavigationOptions<T>, 'disabled'> & {\n disabled?: Readable<boolean | undefined>;\n};\n\nexport type ListControl<T> = {\n [Symbol.dispose](): void;\n dispose(): void;\n readonly disposed: boolean;\n readonly focusedIndex: Readable<number>;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n navigate(action: ListNavigationAction): number;\n reset(): void;\n set(index: number): number;\n};\n\nexport type { ListKeyAction, ListNavigationAction, ListNavigationChange, ListNavigationTypeaheadOptions };\n\nexport const createListControl = <T>(options: ListNavigationOptions<T>): ListControl<T> => {\n const focusedIndex = signal(-1);\n let removeAbortListener: (() => void) | undefined;\n const navigation: ListNavigation<T> = createListNavigation<T>({\n direction: options.direction,\n disabled: () => Boolean(options.disabled?.value),\n getItems: options.getItems,\n isItemDisabled: options.isItemDisabled,\n keys: options.keys,\n loop: options.loop,\n onNavigate: (change) => {\n focusedIndex.value = change.index;\n options.onNavigate?.(change);\n },\n orientation: options.orientation,\n typeahead: options.typeahead,\n });\n\n const syncIndex = (): void => {\n focusedIndex.value = navigation.getIndex();\n };\n\n const set = (index: number): number => {\n const next = navigation.set(index);\n\n syncIndex();\n\n return next;\n };\n\n const navigate = (action: ListNavigationAction): number => {\n const next = navigation.navigate(action);\n\n syncIndex();\n\n return next;\n };\n\n const reset = (): void => {\n navigation.reset();\n syncIndex();\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n const handled = navigation.handleKeydown(event);\n\n syncIndex();\n\n return handled;\n };\n\n const dispose = (): void => {\n navigation.dispose();\n syncIndex();\n removeAbortListener?.();\n removeAbortListener = undefined;\n };\n\n if (options.signal?.aborted) {\n dispose();\n } else if (options.signal) {\n options.signal?.addEventListener('abort', dispose, { once: true });\n removeAbortListener = () => options.signal?.removeEventListener('abort', dispose);\n }\n\n return {\n dispose,\n get disposed() {\n return navigation.disposed;\n },\n focusedIndex,\n getActiveItem: () => navigation.getActiveItem(),\n handleKeydown,\n navigate,\n reset,\n set,\n [Symbol.dispose]: dispose,\n };\n};\n"],"mappings":"+DA6BA,IAAa,EAAwB,GAAsD,CACzF,IAAM,GAAA,EAAe,EAAA,OAAA,CAAO,EAAE,EAC1B,EACE,GAAA,EAAgC,EAAA,qBAAA,CAAwB,CAC5D,UAAW,EAAQ,UACnB,aAAgB,EAAQ,EAAQ,UAAU,MAC1C,SAAU,EAAQ,SAClB,eAAgB,EAAQ,eACxB,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,WAAa,GAAW,CACtB,EAAa,MAAQ,EAAO,MAC5B,EAAQ,aAAa,CAAM,CAC7B,EACA,YAAa,EAAQ,YACrB,UAAW,EAAQ,SACrB,CAAC,EAEK,MAAwB,CAC5B,EAAa,MAAQ,EAAW,SAAS,CAC3C,EAEM,EAAO,GAA0B,CACrC,IAAM,EAAO,EAAW,IAAI,CAAK,EAIjC,OAFA,EAAU,EAEH,CACT,EAEM,EAAY,GAAyC,CACzD,IAAM,EAAO,EAAW,SAAS,CAAM,EAIvC,OAFA,EAAU,EAEH,CACT,EAEM,MAAoB,CACxB,EAAW,MAAM,EACjB,EAAU,CACZ,EAEM,EAAiB,GAAkC,CACvD,IAAM,EAAU,EAAW,cAAc,CAAK,EAI9C,OAFA,EAAU,EAEH,CACT,EAEM,MAAsB,CAC1B,EAAW,QAAQ,EACnB,EAAU,EACV,IAAsB,EACtB,EAAsB,IAAA,EACxB,EASA,OAPI,EAAQ,QAAQ,QAClB,EAAQ,EACC,EAAQ,SACjB,EAAQ,QAAQ,iBAAiB,QAAS,EAAS,CAAE,KAAM,EAAK,CAAC,EACjE,MAA4B,EAAQ,QAAQ,oBAAoB,QAAS,CAAO,GAG3E,CACL,UACA,IAAI,UAAW,CACb,OAAO,EAAW,QACpB,EACA,eACA,kBAAqB,EAAW,cAAc,EAC9C,gBACA,WACA,QACA,OACC,OAAO,SAAU,CACpB,CACF"}
1
+ {"version":3,"file":"nav.cjs","names":[],"sources":["../../src/core/nav.ts"],"sourcesContent":["import {\n createListNavigation,\n type ListNavigationOptions as FocusListNavigationOptions,\n type ListKeyAction,\n type ListNavigation,\n type ListNavigationAction,\n type ListNavigationChange,\n type ListNavigationTypeaheadOptions,\n} from '@vielzeug/focus';\nimport { type Readable, signal } from '@vielzeug/ripple';\n\nexport type ListNavigationOptions<T> = Omit<FocusListNavigationOptions<T>, 'disabled'> & {\n disabled?: Readable<boolean | undefined>;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n signal?: AbortSignal;\n};\n\nexport type { ListKeyAction, ListNavigationAction, ListNavigationChange, ListNavigationTypeaheadOptions };\n\nexport type ListControl<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n readonly focusedIndex: Readable<number>;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n navigate(action: ListNavigationAction): number;\n reset(): void;\n set(index: number): number;\n};\n\nexport const createListControl = <T>(options: ListNavigationOptions<T>): ListControl<T> => {\n const disposalController = new AbortController();\n const focusedIndex = signal(-1);\n let disposed = false;\n let removeAbortListener: (() => void) | undefined;\n const navigation: ListNavigation<T> = createListNavigation<T>({\n direction: options.direction,\n disabled: () => Boolean(options.disabled?.value),\n getItems: options.getItems,\n isItemDisabled: options.isItemDisabled,\n keys: options.keys,\n loop: options.loop,\n orientation: options.orientation,\n typeahead: options.typeahead,\n });\n\n const syncIndex = (): void => {\n focusedIndex.value = navigation.getIndex();\n };\n\n const applyChange = (change: ListNavigationChange<T> | null): number => {\n if (change) {\n focusedIndex.value = change.index;\n try {\n options.onNavigate?.(change);\n } finally {\n syncIndex();\n }\n }\n\n return navigation.getIndex();\n };\n\n const set = (index: number): number => {\n if (disposed) return -1;\n const next = navigation.set(index);\n\n syncIndex();\n\n return next;\n };\n\n const navigate = (action: ListNavigationAction): number => {\n if (disposed) return -1;\n const change = navigation.navigate(action);\n\n return applyChange(change);\n };\n\n const reset = (): void => {\n if (disposed) return;\n navigation.reset();\n syncIndex();\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n if (disposed) return false;\n\n const result = navigation.handleKeydown(event);\n if (!result) return false;\n if (result.change) applyChange(result.change);\n return result.handled;\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n navigation.reset();\n focusedIndex.value = -1;\n disposalController.abort();\n removeAbortListener?.();\n removeAbortListener = undefined;\n };\n\n if (options.signal?.aborted) {\n dispose();\n } else if (options.signal) {\n options.signal?.addEventListener('abort', dispose, { once: true });\n removeAbortListener = () => options.signal?.removeEventListener('abort', dispose);\n }\n\n return {\n get disposalSignal() {\n return disposalController.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n focusedIndex,\n getActiveItem: () => (disposed ? undefined : navigation.getActiveItem()),\n handleKeydown,\n navigate,\n reset,\n set,\n [Symbol.dispose]: dispose,\n };\n};\n"],"mappings":"+DAgCA,IAAa,EAAwB,GAAsD,CACzF,IAAM,EAAqB,IAAI,gBACzB,GAAA,EAAe,EAAA,OAAA,CAAO,EAAE,EAC1B,EAAW,GACX,EACE,GAAA,EAAgC,EAAA,qBAAA,CAAwB,CAC5D,UAAW,EAAQ,UACnB,aAAgB,EAAQ,EAAQ,UAAU,MAC1C,SAAU,EAAQ,SAClB,eAAgB,EAAQ,eACxB,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,YAAa,EAAQ,YACrB,UAAW,EAAQ,SACrB,CAAC,EAEK,MAAwB,CAC5B,EAAa,MAAQ,EAAW,SAAS,CAC3C,EAEM,EAAe,GAAmD,CACtE,GAAI,EAAQ,CACV,EAAa,MAAQ,EAAO,MAC5B,GAAI,CACF,EAAQ,aAAa,CAAM,CAC7B,QAAU,CACR,EAAU,CACZ,CACF,CAEA,OAAO,EAAW,SAAS,CAC7B,EAEM,EAAO,GAA0B,CACrC,GAAI,EAAU,MAAO,GACrB,IAAM,EAAO,EAAW,IAAI,CAAK,EAIjC,OAFA,EAAU,EAEH,CACT,EAEM,EAAY,GAAyC,CACzD,GAAI,EAAU,MAAO,GACrB,IAAM,EAAS,EAAW,SAAS,CAAM,EAEzC,OAAO,EAAY,CAAM,CAC3B,EAEM,MAAoB,CACpB,IACJ,EAAW,MAAM,EACjB,EAAU,EACZ,EAEM,EAAiB,GAAkC,CACvD,GAAI,EAAU,MAAO,GAErB,IAAM,EAAS,EAAW,cAAc,CAAK,EAG7C,OAFK,GACD,EAAO,QAAQ,EAAY,EAAO,MAAM,EACrC,EAAO,SAFM,EAGtB,EAEM,MAAsB,CACtB,IAEJ,EAAW,GACX,EAAW,MAAM,EACjB,EAAa,MAAQ,GACrB,EAAmB,MAAM,EACzB,IAAsB,EACtB,EAAsB,IAAA,GACxB,EASA,OAPI,EAAQ,QAAQ,QAClB,EAAQ,EACC,EAAQ,SACjB,EAAQ,QAAQ,iBAAiB,QAAS,EAAS,CAAE,KAAM,EAAK,CAAC,EACjE,MAA4B,EAAQ,QAAQ,oBAAoB,QAAS,CAAO,GAG3E,CACL,IAAI,gBAAiB,CACnB,OAAO,EAAmB,MAC5B,EACA,UACA,IAAI,UAAW,CACb,OAAO,CACT,EACA,eACA,kBAAsB,EAAW,IAAA,GAAY,EAAW,cAAc,EACtE,gBACA,WACA,QACA,OACC,OAAO,SAAU,CACpB,CACF"}
@@ -2,9 +2,13 @@ import { type ListNavigationOptions as FocusListNavigationOptions, type ListKeyA
2
2
  import { type Readable } from '@vielzeug/ripple';
3
3
  export type ListNavigationOptions<T> = Omit<FocusListNavigationOptions<T>, 'disabled'> & {
4
4
  disabled?: Readable<boolean | undefined>;
5
+ onNavigate?: (change: ListNavigationChange<T>) => void;
6
+ signal?: AbortSignal;
5
7
  };
8
+ export type { ListKeyAction, ListNavigationAction, ListNavigationChange, ListNavigationTypeaheadOptions };
6
9
  export type ListControl<T> = {
7
10
  [Symbol.dispose](): void;
11
+ readonly disposalSignal: AbortSignal;
8
12
  dispose(): void;
9
13
  readonly disposed: boolean;
10
14
  readonly focusedIndex: Readable<number>;
@@ -14,6 +18,5 @@ export type ListControl<T> = {
14
18
  reset(): void;
15
19
  set(index: number): number;
16
20
  };
17
- export type { ListKeyAction, ListNavigationAction, ListNavigationChange, ListNavigationTypeaheadOptions };
18
21
  export declare const createListControl: <T>(options: ListNavigationOptions<T>) => ListControl<T>;
19
22
  //# sourceMappingURL=nav.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../../src/core/nav.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,qBAAqB,IAAI,0BAA0B,EACxD,KAAK,aAAa,EAElB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,8BAA8B,EACpC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,KAAK,QAAQ,EAAU,MAAM,kBAAkB,CAAC;AAEzD,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI,IAAI,CAAC,0BAA0B,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG;IACvF,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;IACzB,OAAO,IAAI,IAAI,CAAC;IAChB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxC,aAAa,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAC/C,KAAK,IAAI,IAAI,CAAC;IACd,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5B,CAAC;AAEF,YAAY,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,8BAA8B,EAAE,CAAC;AAE1G,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,SAAS,qBAAqB,CAAC,CAAC,CAAC,KAAG,WAAW,CAAC,CAAC,CA8ErF,CAAC"}
1
+ {"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../../src/core/nav.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,qBAAqB,IAAI,0BAA0B,EACxD,KAAK,aAAa,EAElB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,8BAA8B,EACpC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,KAAK,QAAQ,EAAU,MAAM,kBAAkB,CAAC;AAEzD,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI,IAAI,CAAC,0BAA0B,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG;IACvF,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzC,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IACvD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,YAAY,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,8BAA8B,EAAE,CAAC;AAE1G,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;IACzB,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC;IACrC,OAAO,IAAI,IAAI,CAAC;IAChB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxC,aAAa,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAC/C,KAAK,IAAI,IAAI,CAAC;IACd,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,SAAS,qBAAqB,CAAC,CAAC,CAAC,KAAG,WAAW,CAAC,CAAC,CAkGrF,CAAC"}
package/dist/core/nav.js CHANGED
@@ -2,46 +2,59 @@ import { signal as e } from "@vielzeug/ripple";
2
2
  import { createListNavigation as t } from "@vielzeug/focus";
3
3
  //#region src/core/nav.ts
4
4
  var n = (n) => {
5
- let r = e(-1), i, a = t({
5
+ let r = new AbortController(), i = e(-1), a = !1, o, s = t({
6
6
  direction: n.direction,
7
7
  disabled: () => !!n.disabled?.value,
8
8
  getItems: n.getItems,
9
9
  isItemDisabled: n.isItemDisabled,
10
10
  keys: n.keys,
11
11
  loop: n.loop,
12
- onNavigate: (e) => {
13
- r.value = e.index, n.onNavigate?.(e);
14
- },
15
12
  orientation: n.orientation,
16
13
  typeahead: n.typeahead
17
- }), o = () => {
18
- r.value = a.getIndex();
19
- }, s = (e) => {
20
- let t = a.set(e);
21
- return o(), t;
22
- }, c = (e) => {
23
- let t = a.navigate(e);
24
- return o(), t;
25
- }, l = () => {
26
- a.reset(), o();
14
+ }), c = () => {
15
+ i.value = s.getIndex();
16
+ }, l = (e) => {
17
+ if (e) {
18
+ i.value = e.index;
19
+ try {
20
+ n.onNavigate?.(e);
21
+ } finally {
22
+ c();
23
+ }
24
+ }
25
+ return s.getIndex();
27
26
  }, u = (e) => {
28
- let t = a.handleKeydown(e);
29
- return o(), t;
30
- }, d = () => {
31
- a.dispose(), o(), i?.(), i = void 0;
27
+ if (a) return -1;
28
+ let t = s.set(e);
29
+ return c(), t;
30
+ }, d = (e) => {
31
+ if (a) return -1;
32
+ let t = s.navigate(e);
33
+ return l(t);
34
+ }, f = () => {
35
+ a || (s.reset(), c());
36
+ }, p = (e) => {
37
+ if (a) return !1;
38
+ let t = s.handleKeydown(e);
39
+ return t ? (t.change && l(t.change), t.handled) : !1;
40
+ }, m = () => {
41
+ a || (a = !0, s.reset(), i.value = -1, r.abort(), o?.(), o = void 0);
32
42
  };
33
- return n.signal?.aborted ? d() : n.signal && (n.signal?.addEventListener("abort", d, { once: !0 }), i = () => n.signal?.removeEventListener("abort", d)), {
34
- dispose: d,
43
+ return n.signal?.aborted ? m() : n.signal && (n.signal?.addEventListener("abort", m, { once: !0 }), o = () => n.signal?.removeEventListener("abort", m)), {
44
+ get disposalSignal() {
45
+ return r.signal;
46
+ },
47
+ dispose: m,
35
48
  get disposed() {
36
- return a.disposed;
49
+ return a;
37
50
  },
38
- focusedIndex: r,
39
- getActiveItem: () => a.getActiveItem(),
40
- handleKeydown: u,
41
- navigate: c,
42
- reset: l,
43
- set: s,
44
- [Symbol.dispose]: d
51
+ focusedIndex: i,
52
+ getActiveItem: () => a ? void 0 : s.getActiveItem(),
53
+ handleKeydown: p,
54
+ navigate: d,
55
+ reset: f,
56
+ set: u,
57
+ [Symbol.dispose]: m
45
58
  };
46
59
  };
47
60
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"nav.js","names":[],"sources":["../../src/core/nav.ts"],"sourcesContent":["import {\n createListNavigation,\n type ListNavigationOptions as FocusListNavigationOptions,\n type ListKeyAction,\n type ListNavigation,\n type ListNavigationAction,\n type ListNavigationChange,\n type ListNavigationTypeaheadOptions,\n} from '@vielzeug/focus';\nimport { type Readable, signal } from '@vielzeug/ripple';\n\nexport type ListNavigationOptions<T> = Omit<FocusListNavigationOptions<T>, 'disabled'> & {\n disabled?: Readable<boolean | undefined>;\n};\n\nexport type ListControl<T> = {\n [Symbol.dispose](): void;\n dispose(): void;\n readonly disposed: boolean;\n readonly focusedIndex: Readable<number>;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n navigate(action: ListNavigationAction): number;\n reset(): void;\n set(index: number): number;\n};\n\nexport type { ListKeyAction, ListNavigationAction, ListNavigationChange, ListNavigationTypeaheadOptions };\n\nexport const createListControl = <T>(options: ListNavigationOptions<T>): ListControl<T> => {\n const focusedIndex = signal(-1);\n let removeAbortListener: (() => void) | undefined;\n const navigation: ListNavigation<T> = createListNavigation<T>({\n direction: options.direction,\n disabled: () => Boolean(options.disabled?.value),\n getItems: options.getItems,\n isItemDisabled: options.isItemDisabled,\n keys: options.keys,\n loop: options.loop,\n onNavigate: (change) => {\n focusedIndex.value = change.index;\n options.onNavigate?.(change);\n },\n orientation: options.orientation,\n typeahead: options.typeahead,\n });\n\n const syncIndex = (): void => {\n focusedIndex.value = navigation.getIndex();\n };\n\n const set = (index: number): number => {\n const next = navigation.set(index);\n\n syncIndex();\n\n return next;\n };\n\n const navigate = (action: ListNavigationAction): number => {\n const next = navigation.navigate(action);\n\n syncIndex();\n\n return next;\n };\n\n const reset = (): void => {\n navigation.reset();\n syncIndex();\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n const handled = navigation.handleKeydown(event);\n\n syncIndex();\n\n return handled;\n };\n\n const dispose = (): void => {\n navigation.dispose();\n syncIndex();\n removeAbortListener?.();\n removeAbortListener = undefined;\n };\n\n if (options.signal?.aborted) {\n dispose();\n } else if (options.signal) {\n options.signal?.addEventListener('abort', dispose, { once: true });\n removeAbortListener = () => options.signal?.removeEventListener('abort', dispose);\n }\n\n return {\n dispose,\n get disposed() {\n return navigation.disposed;\n },\n focusedIndex,\n getActiveItem: () => navigation.getActiveItem(),\n handleKeydown,\n navigate,\n reset,\n set,\n [Symbol.dispose]: dispose,\n };\n};\n"],"mappings":";;;AA6BA,IAAa,KAAwB,MAAsD;CACzF,IAAM,IAAe,EAAO,EAAE,GAC1B,GACE,IAAgC,EAAwB;EAC5D,WAAW,EAAQ;EACnB,gBAAgB,EAAQ,EAAQ,UAAU;EAC1C,UAAU,EAAQ;EAClB,gBAAgB,EAAQ;EACxB,MAAM,EAAQ;EACd,MAAM,EAAQ;EACd,aAAa,MAAW;GAEtB,AADA,EAAa,QAAQ,EAAO,OAC5B,EAAQ,aAAa,CAAM;EAC7B;EACA,aAAa,EAAQ;EACrB,WAAW,EAAQ;CACrB,CAAC,GAEK,UAAwB;EAC5B,EAAa,QAAQ,EAAW,SAAS;CAC3C,GAEM,KAAO,MAA0B;EACrC,IAAM,IAAO,EAAW,IAAI,CAAK;EAIjC,OAFA,EAAU,GAEH;CACT,GAEM,KAAY,MAAyC;EACzD,IAAM,IAAO,EAAW,SAAS,CAAM;EAIvC,OAFA,EAAU,GAEH;CACT,GAEM,UAAoB;EAExB,AADA,EAAW,MAAM,GACjB,EAAU;CACZ,GAEM,KAAiB,MAAkC;EACvD,IAAM,IAAU,EAAW,cAAc,CAAK;EAI9C,OAFA,EAAU,GAEH;CACT,GAEM,UAAsB;EAI1B,AAHA,EAAW,QAAQ,GACnB,EAAU,GACV,IAAsB,GACtB,IAAsB,KAAA;CACxB;CASA,OAPI,EAAQ,QAAQ,UAClB,EAAQ,IACC,EAAQ,WACjB,EAAQ,QAAQ,iBAAiB,SAAS,GAAS,EAAE,MAAM,GAAK,CAAC,GACjE,UAA4B,EAAQ,QAAQ,oBAAoB,SAAS,CAAO,IAG3E;EACL;EACA,IAAI,WAAW;GACb,OAAO,EAAW;EACpB;EACA;EACA,qBAAqB,EAAW,cAAc;EAC9C;EACA;EACA;EACA;GACC,OAAO,UAAU;CACpB;AACF"}
1
+ {"version":3,"file":"nav.js","names":[],"sources":["../../src/core/nav.ts"],"sourcesContent":["import {\n createListNavigation,\n type ListNavigationOptions as FocusListNavigationOptions,\n type ListKeyAction,\n type ListNavigation,\n type ListNavigationAction,\n type ListNavigationChange,\n type ListNavigationTypeaheadOptions,\n} from '@vielzeug/focus';\nimport { type Readable, signal } from '@vielzeug/ripple';\n\nexport type ListNavigationOptions<T> = Omit<FocusListNavigationOptions<T>, 'disabled'> & {\n disabled?: Readable<boolean | undefined>;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n signal?: AbortSignal;\n};\n\nexport type { ListKeyAction, ListNavigationAction, ListNavigationChange, ListNavigationTypeaheadOptions };\n\nexport type ListControl<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n readonly focusedIndex: Readable<number>;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n navigate(action: ListNavigationAction): number;\n reset(): void;\n set(index: number): number;\n};\n\nexport const createListControl = <T>(options: ListNavigationOptions<T>): ListControl<T> => {\n const disposalController = new AbortController();\n const focusedIndex = signal(-1);\n let disposed = false;\n let removeAbortListener: (() => void) | undefined;\n const navigation: ListNavigation<T> = createListNavigation<T>({\n direction: options.direction,\n disabled: () => Boolean(options.disabled?.value),\n getItems: options.getItems,\n isItemDisabled: options.isItemDisabled,\n keys: options.keys,\n loop: options.loop,\n orientation: options.orientation,\n typeahead: options.typeahead,\n });\n\n const syncIndex = (): void => {\n focusedIndex.value = navigation.getIndex();\n };\n\n const applyChange = (change: ListNavigationChange<T> | null): number => {\n if (change) {\n focusedIndex.value = change.index;\n try {\n options.onNavigate?.(change);\n } finally {\n syncIndex();\n }\n }\n\n return navigation.getIndex();\n };\n\n const set = (index: number): number => {\n if (disposed) return -1;\n const next = navigation.set(index);\n\n syncIndex();\n\n return next;\n };\n\n const navigate = (action: ListNavigationAction): number => {\n if (disposed) return -1;\n const change = navigation.navigate(action);\n\n return applyChange(change);\n };\n\n const reset = (): void => {\n if (disposed) return;\n navigation.reset();\n syncIndex();\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n if (disposed) return false;\n\n const result = navigation.handleKeydown(event);\n if (!result) return false;\n if (result.change) applyChange(result.change);\n return result.handled;\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n navigation.reset();\n focusedIndex.value = -1;\n disposalController.abort();\n removeAbortListener?.();\n removeAbortListener = undefined;\n };\n\n if (options.signal?.aborted) {\n dispose();\n } else if (options.signal) {\n options.signal?.addEventListener('abort', dispose, { once: true });\n removeAbortListener = () => options.signal?.removeEventListener('abort', dispose);\n }\n\n return {\n get disposalSignal() {\n return disposalController.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n focusedIndex,\n getActiveItem: () => (disposed ? undefined : navigation.getActiveItem()),\n handleKeydown,\n navigate,\n reset,\n set,\n [Symbol.dispose]: dispose,\n };\n};\n"],"mappings":";;;AAgCA,IAAa,KAAwB,MAAsD;CACzF,IAAM,IAAqB,IAAI,gBAAgB,GACzC,IAAe,EAAO,EAAE,GAC1B,IAAW,IACX,GACE,IAAgC,EAAwB;EAC5D,WAAW,EAAQ;EACnB,gBAAgB,EAAQ,EAAQ,UAAU;EAC1C,UAAU,EAAQ;EAClB,gBAAgB,EAAQ;EACxB,MAAM,EAAQ;EACd,MAAM,EAAQ;EACd,aAAa,EAAQ;EACrB,WAAW,EAAQ;CACrB,CAAC,GAEK,UAAwB;EAC5B,EAAa,QAAQ,EAAW,SAAS;CAC3C,GAEM,KAAe,MAAmD;EACtE,IAAI,GAAQ;GACV,EAAa,QAAQ,EAAO;GAC5B,IAAI;IACF,EAAQ,aAAa,CAAM;GAC7B,UAAU;IACR,EAAU;GACZ;EACF;EAEA,OAAO,EAAW,SAAS;CAC7B,GAEM,KAAO,MAA0B;EACrC,IAAI,GAAU,OAAO;EACrB,IAAM,IAAO,EAAW,IAAI,CAAK;EAIjC,OAFA,EAAU,GAEH;CACT,GAEM,KAAY,MAAyC;EACzD,IAAI,GAAU,OAAO;EACrB,IAAM,IAAS,EAAW,SAAS,CAAM;EAEzC,OAAO,EAAY,CAAM;CAC3B,GAEM,UAAoB;EACpB,MACJ,EAAW,MAAM,GACjB,EAAU;CACZ,GAEM,KAAiB,MAAkC;EACvD,IAAI,GAAU,OAAO;EAErB,IAAM,IAAS,EAAW,cAAc,CAAK;EAG7C,OAFK,KACD,EAAO,UAAQ,EAAY,EAAO,MAAM,GACrC,EAAO,WAFM;CAGtB,GAEM,UAAsB;EACtB,MAEJ,IAAW,IACX,EAAW,MAAM,GACjB,EAAa,QAAQ,IACrB,EAAmB,MAAM,GACzB,IAAsB,GACtB,IAAsB,KAAA;CACxB;CASA,OAPI,EAAQ,QAAQ,UAClB,EAAQ,IACC,EAAQ,WACjB,EAAQ,QAAQ,iBAAiB,SAAS,GAAS,EAAE,MAAM,GAAK,CAAC,GACjE,UAA4B,EAAQ,QAAQ,oBAAoB,SAAS,CAAO,IAG3E;EACL,IAAI,iBAAiB;GACnB,OAAO,EAAmB;EAC5B;EACA;EACA,IAAI,WAAW;GACb,OAAO;EACT;EACA;EACA,qBAAsB,IAAW,KAAA,IAAY,EAAW,cAAc;EACtE;EACA;EACA;EACA;GACC,OAAO,UAAU;CACpB;AACF"}
@@ -1,2 +1,2 @@
1
- const e=require("../errors.cjs"),t=require("./overlay.cjs"),n=require("./nav.cjs"),r=require("./positioner.cjs");let i=require("@vielzeug/ripple"),a=require("@vielzeug/focus");var o=o=>{if(typeof o.getBoundary!=`function`)throw new e.RefineConfigError(`createListboxDropdown: getBoundary is required`);if(typeof o.getPanel!=`function`)throw new e.RefineConfigError(`createListboxDropdown: getPanel is required`);if(typeof o.getReference!=`function`)throw new e.RefineConfigError(`createListboxDropdown: getReference is required`);let s=(0,i.signal)(!1),c=(0,i.computed)(()=>String(s.value)),l=null,u=r.createDropdownPositioner({...o.positioning,getFloating:o.getPanel,getReference:o.getReference}),d=()=>{o.getFocusedOptionElement?.()?.scrollIntoView({block:`nearest`})},f=n.createListControl({disabled:(0,i.computed)(()=>!s.value),getItems:o.getItems,isItemDisabled:o.isItemDisabled,keys:o.keys,loop:o.loop??!0,onNavigate:e=>{d(),o.onNavigate?.(e)},orientation:o.orientation,signal:o.signal,typeahead:o.typeahead}),p=(0,i.computed)(()=>{let e=f.focusedIndex.value;return o.getOptionId&&s.value&&e>=0?o.getOptionId(e):null}),m=()=>typeof o.restoreFocus==`function`?o.restoreFocus():o.restoreFocus??!0,h=(e=`programmatic`,t=!0)=>{s.value&&(s.value=!1,f.reset(),l?.(),l=null,t&&m()&&o.getTrigger&&(0,a.restoreFocus)(o.getTrigger),o.onClose?.(e))},g=(e=`programmatic`)=>{o.isDisabled?.()||s.value||(s.value=!0,u.update(),l=u.startAutoUpdate?.()??null,o.onOpen?.(e))},_=(e=`click`,t=`trigger`)=>{s.value?h(t):g(e)};t.createOutsidePointerDismissal({getTargets:()=>[o.getBoundary(),o.getPanel()],isActive:()=>s.value,onDismiss:()=>h(`outsideClick`),signal:o.signal});let v=!1,y=()=>{v||(v=!0,s.value&&(s.value=!1,f.reset(),l?.(),l=null))};return o.signal.addEventListener(`abort`,y,{once:!0}),{ariaActiveDescendant:p,ariaExpanded:c,close:h,dispose:y,get disposed(){return v},focusedIndex:f.focusedIndex,getActiveItem:f.getActiveItem,handleKeydown:e=>s.value?e.key===`Escape`?(e.preventDefault(),h(`escape`),!0):f.handleKeydown(e):!1,isOpen:s,navigate:f.navigate,open:g,scrollFocusedIntoView:d,set:f.set,[Symbol.dispose]:y,toggle:_,updatePosition:u.update}};exports.createListboxDropdown=o;
1
+ const e=require("../errors.cjs"),t=require("./overlay.cjs"),n=require("./nav.cjs"),r=require("./positioner.cjs");let i=require("@vielzeug/ripple"),a=require("@vielzeug/focus");var o=o=>{if(typeof o.getBoundary!=`function`)throw new e.RefineConfigError(`createListboxDropdown: getBoundary is required`);if(typeof o.getPanel!=`function`)throw new e.RefineConfigError(`createListboxDropdown: getPanel is required`);if(typeof o.getReference!=`function`)throw new e.RefineConfigError(`createListboxDropdown: getReference is required`);let s=(0,i.signal)(!1),c=(0,i.computed)(()=>String(s.value)),l=!1,u=null,d=r.createDropdownPositioner({...o.positioning,getFloating:o.getPanel,getReference:o.getReference}),f=()=>{l||o.getFocusedOptionElement?.()?.scrollIntoView({block:`nearest`})},p=n.createListControl({disabled:(0,i.computed)(()=>!s.value),getItems:o.getItems,isItemDisabled:o.isItemDisabled,keys:o.keys,loop:o.loop??!0,onNavigate:e=>{f(),o.onNavigate?.(e)},orientation:o.orientation,signal:o.signal,typeahead:o.typeahead}),m=(0,i.computed)(()=>{let e=p.focusedIndex.value;return o.getOptionId&&s.value&&e>=0?o.getOptionId(e):null}),h=()=>typeof o.restoreFocus==`function`?o.restoreFocus():o.restoreFocus??!0,g=(e=`programmatic`,t=!0)=>{!l&&s.value&&(s.value=!1,p.reset(),u?.(),u=null,t&&h()&&o.getTrigger&&(0,a.restoreFocus)(o.getTrigger),o.onClose?.(e))},_=(e=`programmatic`)=>{l||o.isDisabled?.()||s.value||(s.value=!0,d.update(),u=d.startAutoUpdate?.()??null,o.onOpen?.(e))},v=(e=`click`,t=`trigger`)=>{l||(s.value?g(t):_(e))};t.createOutsidePointerDismissal({getTargets:()=>[o.getBoundary(),o.getPanel()],isActive:()=>s.value,onDismiss:()=>g(`outsideClick`),signal:o.signal});let y=()=>{l||(l=!0,p.dispose(),s.value&&(s.value=!1,p.reset(),u?.(),u=null))};return o.signal.addEventListener(`abort`,y,{once:!0}),{ariaActiveDescendant:m,ariaExpanded:c,close:g,dispose:y,get disposed(){return l},focusedIndex:p.focusedIndex,getActiveItem:p.getActiveItem,handleKeydown:e=>l||!s.value?!1:e.key===`Escape`?(e.preventDefault(),g(`escape`),!0):p.handleKeydown(e),isOpen:s,navigate:p.navigate,open:_,scrollFocusedIntoView:f,set:p.set,[Symbol.dispose]:y,toggle:v,updatePosition:()=>{l||d.update()}}};exports.createListboxDropdown=o;
2
2
  //# sourceMappingURL=option-list.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"option-list.cjs","names":[],"sources":["../../src/core/option-list.ts"],"sourcesContent":["import { restoreFocus } from '@vielzeug/focus';\nimport { computed, type Readable, signal } from '@vielzeug/ripple';\n\nimport { RefineConfigError } from '../errors';\nimport {\n createListControl,\n type ListNavigationAction,\n type ListNavigationChange,\n type ListNavigationTypeaheadOptions,\n} from './nav';\nimport { createOutsidePointerDismissal, type DropdownCloseReason, type OverlayOpenReason } from './overlay';\nimport { createDropdownPositioner, type DropdownPositionerOptions } from './positioner';\n\nexport type ListboxItem = object;\n\n/** Placement-only options for the listbox dropdown positioner. */\nexport type ListboxDropdownPlacementOptions = Omit<DropdownPositionerOptions, 'getFloating' | 'getReference'>;\n\nexport type ListboxDropdownOptions<T extends ListboxItem> = {\n getBoundary: () => HTMLElement | null;\n getFocusedOptionElement?: () => HTMLElement | null;\n getItems: () => readonly T[];\n getOptionId?: (index: number) => string;\n getPanel: () => HTMLElement | null;\n getReference: () => HTMLElement | null;\n getTrigger?: () => HTMLElement | null;\n isDisabled?: () => boolean;\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n onClose?: (reason: DropdownCloseReason) => void;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n onOpen?: (reason: OverlayOpenReason) => void;\n orientation?: 'both' | 'horizontal' | 'vertical';\n positioning?: ListboxDropdownPlacementOptions;\n restoreFocus?: boolean | (() => boolean);\n signal: AbortSignal;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListboxDropdown<T extends ListboxItem> = {\n [Symbol.dispose](): void;\n readonly ariaActiveDescendant: Readable<string | null>;\n readonly ariaExpanded: Readable<string>;\n close(reason?: DropdownCloseReason): void;\n /** Closes the dropdown (without restoring focus) and disposes list navigation state. */\n dispose(): void;\n /** `true` after `dispose()` has been called. */\n readonly disposed: boolean;\n readonly focusedIndex: Readable<number>;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n readonly isOpen: Readable<boolean>;\n navigate(action: ListNavigationAction): number;\n open(reason?: OverlayOpenReason): void;\n scrollFocusedIntoView(): void;\n set(index: number): number;\n toggle(openReason?: OverlayOpenReason, closeReason?: DropdownCloseReason): void;\n updatePosition(): void;\n};\n\n/**\n * Internal listbox dropdown behavior for select and combobox. It owns only\n * listbox interactions; menu and other overlay families keep their own logic.\n */\nexport const createListboxDropdown = <T extends ListboxItem>(\n options: ListboxDropdownOptions<T>,\n): ListboxDropdown<T> => {\n if (typeof options.getBoundary !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getBoundary is required');\n }\n\n if (typeof options.getPanel !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getPanel is required');\n }\n\n if (typeof options.getReference !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getReference is required');\n }\n\n const isOpen = signal(false);\n const ariaExpanded = computed(() => String(isOpen.value));\n let stopPositioning: (() => void) | null = null;\n\n const positioner = createDropdownPositioner({\n ...options.positioning,\n getFloating: options.getPanel,\n getReference: options.getReference,\n });\n\n const scrollFocusedIntoView = (): void => {\n options.getFocusedOptionElement?.()?.scrollIntoView({ block: 'nearest' });\n };\n\n const list = createListControl<T>({\n disabled: computed(() => !isOpen.value),\n getItems: options.getItems,\n isItemDisabled: options.isItemDisabled,\n keys: options.keys,\n loop: options.loop ?? true,\n onNavigate: (change) => {\n scrollFocusedIntoView();\n options.onNavigate?.(change);\n },\n orientation: options.orientation,\n signal: options.signal,\n typeahead: options.typeahead,\n });\n\n const ariaActiveDescendant = computed<string | null>(() => {\n const index = list.focusedIndex.value;\n\n return options.getOptionId && isOpen.value && index >= 0 ? options.getOptionId(index) : null;\n });\n\n const shouldRestoreFocus = (): boolean => {\n return typeof options.restoreFocus === 'function' ? options.restoreFocus() : (options.restoreFocus ?? true);\n };\n\n const close = (reason: DropdownCloseReason = 'programmatic', shouldRestore = true): void => {\n if (!isOpen.value) return;\n\n isOpen.value = false;\n list.reset();\n stopPositioning?.();\n stopPositioning = null;\n\n if (shouldRestore && shouldRestoreFocus() && options.getTrigger) restoreFocus(options.getTrigger);\n\n options.onClose?.(reason);\n };\n\n const open = (reason: OverlayOpenReason = 'programmatic'): void => {\n if (options.isDisabled?.() || isOpen.value) return;\n\n isOpen.value = true;\n positioner.update();\n stopPositioning = positioner.startAutoUpdate?.() ?? null;\n options.onOpen?.(reason);\n };\n\n const toggle = (openReason: OverlayOpenReason = 'click', closeReason: DropdownCloseReason = 'trigger'): void => {\n if (isOpen.value) close(closeReason);\n else open(openReason);\n };\n\n createOutsidePointerDismissal({\n getTargets: () => [options.getBoundary(), options.getPanel()],\n isActive: () => isOpen.value,\n onDismiss: () => close('outsideClick'),\n signal: options.signal,\n });\n\n let disposed = false;\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n\n if (isOpen.value) {\n isOpen.value = false;\n list.reset();\n stopPositioning?.();\n stopPositioning = null;\n }\n };\n\n options.signal.addEventListener('abort', dispose, { once: true });\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n if (!isOpen.value) return false;\n\n if (event.key === 'Escape') {\n event.preventDefault();\n close('escape');\n\n return true;\n }\n\n return list.handleKeydown(event);\n };\n\n return {\n ariaActiveDescendant,\n ariaExpanded,\n close,\n dispose,\n get disposed() {\n return disposed;\n },\n focusedIndex: list.focusedIndex,\n getActiveItem: list.getActiveItem,\n handleKeydown,\n isOpen,\n navigate: list.navigate,\n open,\n scrollFocusedIntoView,\n set: list.set,\n [Symbol.dispose]: dispose,\n toggle,\n updatePosition: positioner.update,\n };\n};\n"],"mappings":"gLAiEA,IAAa,EACX,GACuB,CACvB,GAAI,OAAO,EAAQ,aAAgB,WACjC,MAAM,IAAI,EAAA,kBAAkB,gDAAgD,EAG9E,GAAI,OAAO,EAAQ,UAAa,WAC9B,MAAM,IAAI,EAAA,kBAAkB,6CAA6C,EAG3E,GAAI,OAAO,EAAQ,cAAiB,WAClC,MAAM,IAAI,EAAA,kBAAkB,iDAAiD,EAG/E,IAAM,GAAA,EAAS,EAAA,OAAA,CAAO,EAAK,EACrB,GAAA,EAAe,EAAA,SAAA,KAAe,OAAO,EAAO,KAAK,CAAC,EACpD,EAAuC,KAErC,EAAa,EAAA,yBAAyB,CAC1C,GAAG,EAAQ,YACX,YAAa,EAAQ,SACrB,aAAc,EAAQ,YACxB,CAAC,EAEK,MAAoC,CACxC,EAAQ,0BAA0B,CAAC,EAAE,eAAe,CAAE,MAAO,SAAU,CAAC,CAC1E,EAEM,EAAO,EAAA,kBAAqB,CAChC,UAAA,EAAU,EAAA,SAAA,KAAe,CAAC,EAAO,KAAK,EACtC,SAAU,EAAQ,SAClB,eAAgB,EAAQ,eACxB,KAAM,EAAQ,KACd,KAAM,EAAQ,MAAQ,GACtB,WAAa,GAAW,CACtB,EAAsB,EACtB,EAAQ,aAAa,CAAM,CAC7B,EACA,YAAa,EAAQ,YACrB,OAAQ,EAAQ,OAChB,UAAW,EAAQ,SACrB,CAAC,EAEK,GAAA,EAAuB,EAAA,SAAA,KAA8B,CACzD,IAAM,EAAQ,EAAK,aAAa,MAEhC,OAAO,EAAQ,aAAe,EAAO,OAAS,GAAS,EAAI,EAAQ,YAAY,CAAK,EAAI,IAC1F,CAAC,EAEK,MACG,OAAO,EAAQ,cAAiB,WAAa,EAAQ,aAAa,EAAK,EAAQ,cAAgB,GAGlG,GAAS,EAA8B,eAAgB,EAAgB,KAAe,CACrF,EAAO,QAEZ,EAAO,MAAQ,GACf,EAAK,MAAM,EACX,IAAkB,EAClB,EAAkB,KAEd,GAAiB,EAAmB,GAAK,EAAQ,aAAY,EAAA,EAAA,aAAA,CAAa,EAAQ,UAAU,EAEhG,EAAQ,UAAU,CAAM,EAC1B,EAEM,GAAQ,EAA4B,iBAAyB,CAC7D,EAAQ,aAAa,GAAK,EAAO,QAErC,EAAO,MAAQ,GACf,EAAW,OAAO,EAClB,EAAkB,EAAW,kBAAkB,GAAK,KACpD,EAAQ,SAAS,CAAM,EACzB,EAEM,GAAU,EAAgC,QAAS,EAAmC,YAAoB,CAC1G,EAAO,MAAO,EAAM,CAAW,EAC9B,EAAK,CAAU,CACtB,EAEA,EAAA,8BAA8B,CAC5B,eAAkB,CAAC,EAAQ,YAAY,EAAG,EAAQ,SAAS,CAAC,EAC5D,aAAgB,EAAO,MACvB,cAAiB,EAAM,cAAc,EACrC,OAAQ,EAAQ,MAClB,CAAC,EAED,IAAI,EAAW,GAET,MAAsB,CACtB,IAEJ,EAAW,GAEP,EAAO,QACT,EAAO,MAAQ,GACf,EAAK,MAAM,EACX,IAAkB,EAClB,EAAkB,MAEtB,EAiBA,OAfA,EAAQ,OAAO,iBAAiB,QAAS,EAAS,CAAE,KAAM,EAAK,CAAC,EAezD,CACL,uBACA,eACA,QACA,UACA,IAAI,UAAW,CACb,OAAO,CACT,EACA,aAAc,EAAK,aACnB,cAAe,EAAK,cACpB,cAvBqB,GAChB,EAAO,MAER,EAAM,MAAQ,UAChB,EAAM,eAAe,EACrB,EAAM,QAAQ,EAEP,IAGF,EAAK,cAAc,CAAK,EATL,GAuB1B,SACA,SAAU,EAAK,SACf,OACA,wBACA,IAAK,EAAK,KACT,OAAO,SAAU,EAClB,SACA,eAAgB,EAAW,MAC7B,CACF"}
1
+ {"version":3,"file":"option-list.cjs","names":[],"sources":["../../src/core/option-list.ts"],"sourcesContent":["import { restoreFocus } from '@vielzeug/focus';\nimport { computed, type Readable, signal } from '@vielzeug/ripple';\n\nimport { RefineConfigError } from '../errors';\nimport {\n createListControl,\n type ListNavigationAction,\n type ListNavigationChange,\n type ListNavigationTypeaheadOptions,\n} from './nav';\nimport { createOutsidePointerDismissal, type DropdownCloseReason, type OverlayOpenReason } from './overlay';\nimport { createDropdownPositioner, type DropdownPositionerOptions } from './positioner';\n\nexport type ListboxItem = object;\n\n/** Placement-only options for the listbox dropdown positioner. */\nexport type ListboxDropdownPlacementOptions = Omit<DropdownPositionerOptions, 'getFloating' | 'getReference'>;\n\nexport type ListboxDropdownOptions<T extends ListboxItem> = {\n getBoundary: () => HTMLElement | null;\n getFocusedOptionElement?: () => HTMLElement | null;\n getItems: () => readonly T[];\n getOptionId?: (index: number) => string;\n getPanel: () => HTMLElement | null;\n getReference: () => HTMLElement | null;\n getTrigger?: () => HTMLElement | null;\n isDisabled?: () => boolean;\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n onClose?: (reason: DropdownCloseReason) => void;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n onOpen?: (reason: OverlayOpenReason) => void;\n orientation?: 'both' | 'horizontal' | 'vertical';\n positioning?: ListboxDropdownPlacementOptions;\n restoreFocus?: boolean | (() => boolean);\n signal: AbortSignal;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListboxDropdown<T extends ListboxItem> = {\n [Symbol.dispose](): void;\n readonly ariaActiveDescendant: Readable<string | null>;\n readonly ariaExpanded: Readable<string>;\n close(reason?: DropdownCloseReason): void;\n /** Closes the dropdown (without restoring focus) and disposes list navigation state. */\n dispose(): void;\n /** `true` after `dispose()` has been called. */\n readonly disposed: boolean;\n readonly focusedIndex: Readable<number>;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n readonly isOpen: Readable<boolean>;\n navigate(action: ListNavigationAction): number;\n open(reason?: OverlayOpenReason): void;\n scrollFocusedIntoView(): void;\n set(index: number): number;\n toggle(openReason?: OverlayOpenReason, closeReason?: DropdownCloseReason): void;\n updatePosition(): void;\n};\n\n/**\n * Internal listbox dropdown behavior for select and combobox. It owns only\n * listbox interactions; menu and other overlay families keep their own logic.\n */\nexport const createListboxDropdown = <T extends ListboxItem>(\n options: ListboxDropdownOptions<T>,\n): ListboxDropdown<T> => {\n if (typeof options.getBoundary !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getBoundary is required');\n }\n\n if (typeof options.getPanel !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getPanel is required');\n }\n\n if (typeof options.getReference !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getReference is required');\n }\n\n const isOpen = signal(false);\n const ariaExpanded = computed(() => String(isOpen.value));\n let disposed = false;\n let stopPositioning: (() => void) | null = null;\n\n const positioner = createDropdownPositioner({\n ...options.positioning,\n getFloating: options.getPanel,\n getReference: options.getReference,\n });\n\n const scrollFocusedIntoView = (): void => {\n if (!disposed) options.getFocusedOptionElement?.()?.scrollIntoView({ block: 'nearest' });\n };\n\n const list = createListControl<T>({\n disabled: computed(() => !isOpen.value),\n getItems: options.getItems,\n isItemDisabled: options.isItemDisabled,\n keys: options.keys,\n loop: options.loop ?? true,\n onNavigate: (change) => {\n scrollFocusedIntoView();\n options.onNavigate?.(change);\n },\n orientation: options.orientation,\n signal: options.signal,\n typeahead: options.typeahead,\n });\n\n const ariaActiveDescendant = computed<string | null>(() => {\n const index = list.focusedIndex.value;\n\n return options.getOptionId && isOpen.value && index >= 0 ? options.getOptionId(index) : null;\n });\n\n const shouldRestoreFocus = (): boolean => {\n return typeof options.restoreFocus === 'function' ? options.restoreFocus() : (options.restoreFocus ?? true);\n };\n\n const close = (reason: DropdownCloseReason = 'programmatic', shouldRestore = true): void => {\n if (disposed || !isOpen.value) return;\n\n isOpen.value = false;\n list.reset();\n stopPositioning?.();\n stopPositioning = null;\n\n if (shouldRestore && shouldRestoreFocus() && options.getTrigger) restoreFocus(options.getTrigger);\n\n options.onClose?.(reason);\n };\n\n const open = (reason: OverlayOpenReason = 'programmatic'): void => {\n if (disposed || options.isDisabled?.() || isOpen.value) return;\n\n isOpen.value = true;\n positioner.update();\n stopPositioning = positioner.startAutoUpdate?.() ?? null;\n options.onOpen?.(reason);\n };\n\n const toggle = (openReason: OverlayOpenReason = 'click', closeReason: DropdownCloseReason = 'trigger'): void => {\n if (disposed) return;\n if (isOpen.value) close(closeReason);\n else open(openReason);\n };\n\n createOutsidePointerDismissal({\n getTargets: () => [options.getBoundary(), options.getPanel()],\n isActive: () => isOpen.value,\n onDismiss: () => close('outsideClick'),\n signal: options.signal,\n });\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n list.dispose();\n\n if (isOpen.value) {\n isOpen.value = false;\n list.reset();\n stopPositioning?.();\n stopPositioning = null;\n }\n };\n\n options.signal.addEventListener('abort', dispose, { once: true });\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n if (disposed || !isOpen.value) return false;\n\n if (event.key === 'Escape') {\n event.preventDefault();\n close('escape');\n\n return true;\n }\n\n return list.handleKeydown(event);\n };\n\n return {\n ariaActiveDescendant,\n ariaExpanded,\n close,\n dispose,\n get disposed() {\n return disposed;\n },\n focusedIndex: list.focusedIndex,\n getActiveItem: list.getActiveItem,\n handleKeydown,\n isOpen,\n navigate: list.navigate,\n open,\n scrollFocusedIntoView,\n set: list.set,\n [Symbol.dispose]: dispose,\n toggle,\n updatePosition: () => {\n if (!disposed) positioner.update();\n },\n };\n};\n"],"mappings":"gLAiEA,IAAa,EACX,GACuB,CACvB,GAAI,OAAO,EAAQ,aAAgB,WACjC,MAAM,IAAI,EAAA,kBAAkB,gDAAgD,EAG9E,GAAI,OAAO,EAAQ,UAAa,WAC9B,MAAM,IAAI,EAAA,kBAAkB,6CAA6C,EAG3E,GAAI,OAAO,EAAQ,cAAiB,WAClC,MAAM,IAAI,EAAA,kBAAkB,iDAAiD,EAG/E,IAAM,GAAA,EAAS,EAAA,OAAA,CAAO,EAAK,EACrB,GAAA,EAAe,EAAA,SAAA,KAAe,OAAO,EAAO,KAAK,CAAC,EACpD,EAAW,GACX,EAAuC,KAErC,EAAa,EAAA,yBAAyB,CAC1C,GAAG,EAAQ,YACX,YAAa,EAAQ,SACrB,aAAc,EAAQ,YACxB,CAAC,EAEK,MAAoC,CACnC,GAAU,EAAQ,0BAA0B,CAAC,EAAE,eAAe,CAAE,MAAO,SAAU,CAAC,CACzF,EAEM,EAAO,EAAA,kBAAqB,CAChC,UAAA,EAAU,EAAA,SAAA,KAAe,CAAC,EAAO,KAAK,EACtC,SAAU,EAAQ,SAClB,eAAgB,EAAQ,eACxB,KAAM,EAAQ,KACd,KAAM,EAAQ,MAAQ,GACtB,WAAa,GAAW,CACtB,EAAsB,EACtB,EAAQ,aAAa,CAAM,CAC7B,EACA,YAAa,EAAQ,YACrB,OAAQ,EAAQ,OAChB,UAAW,EAAQ,SACrB,CAAC,EAEK,GAAA,EAAuB,EAAA,SAAA,KAA8B,CACzD,IAAM,EAAQ,EAAK,aAAa,MAEhC,OAAO,EAAQ,aAAe,EAAO,OAAS,GAAS,EAAI,EAAQ,YAAY,CAAK,EAAI,IAC1F,CAAC,EAEK,MACG,OAAO,EAAQ,cAAiB,WAAa,EAAQ,aAAa,EAAK,EAAQ,cAAgB,GAGlG,GAAS,EAA8B,eAAgB,EAAgB,KAAe,CACtF,IAAa,EAAO,QAExB,EAAO,MAAQ,GACf,EAAK,MAAM,EACX,IAAkB,EAClB,EAAkB,KAEd,GAAiB,EAAmB,GAAK,EAAQ,aAAY,EAAA,EAAA,aAAA,CAAa,EAAQ,UAAU,EAEhG,EAAQ,UAAU,CAAM,EAC1B,EAEM,GAAQ,EAA4B,iBAAyB,CAC7D,GAAY,EAAQ,aAAa,GAAK,EAAO,QAEjD,EAAO,MAAQ,GACf,EAAW,OAAO,EAClB,EAAkB,EAAW,kBAAkB,GAAK,KACpD,EAAQ,SAAS,CAAM,EACzB,EAEM,GAAU,EAAgC,QAAS,EAAmC,YAAoB,CAC1G,IACA,EAAO,MAAO,EAAM,CAAW,EAC9B,EAAK,CAAU,EACtB,EAEA,EAAA,8BAA8B,CAC5B,eAAkB,CAAC,EAAQ,YAAY,EAAG,EAAQ,SAAS,CAAC,EAC5D,aAAgB,EAAO,MACvB,cAAiB,EAAM,cAAc,EACrC,OAAQ,EAAQ,MAClB,CAAC,EAED,IAAM,MAAsB,CACtB,IAEJ,EAAW,GACX,EAAK,QAAQ,EAET,EAAO,QACT,EAAO,MAAQ,GACf,EAAK,MAAM,EACX,IAAkB,EAClB,EAAkB,MAEtB,EAiBA,OAfA,EAAQ,OAAO,iBAAiB,QAAS,EAAS,CAAE,KAAM,EAAK,CAAC,EAezD,CACL,uBACA,eACA,QACA,UACA,IAAI,UAAW,CACb,OAAO,CACT,EACA,aAAc,EAAK,aACnB,cAAe,EAAK,cACpB,cAvBqB,GACjB,GAAY,CAAC,EAAO,MAAc,GAElC,EAAM,MAAQ,UAChB,EAAM,eAAe,EACrB,EAAM,QAAQ,EAEP,IAGF,EAAK,cAAc,CAAK,EAc/B,SACA,SAAU,EAAK,SACf,OACA,wBACA,IAAK,EAAK,KACT,OAAO,SAAU,EAClB,SACA,mBAAsB,CACf,GAAU,EAAW,OAAO,CACnC,CACF,CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"option-list.d.ts","sourceRoot":"","sources":["../../src/core/option-list.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,QAAQ,EAAU,MAAM,kBAAkB,CAAC;AAGnE,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,8BAA8B,EACpC,MAAM,OAAO,CAAC;AACf,OAAO,EAAiC,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC5G,OAAO,EAA4B,KAAK,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAExF,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC;AAEjC,kEAAkE;AAClE,MAAM,MAAM,+BAA+B,GAAG,IAAI,CAAC,yBAAyB,EAAE,aAAa,GAAG,cAAc,CAAC,CAAC;AAE9G,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,WAAW,IAAI;IAC1D,WAAW,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACtC,uBAAuB,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACnD,QAAQ,EAAE,MAAM,SAAS,CAAC,EAAE,CAAC;IAC7B,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,QAAQ,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACnC,YAAY,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IAChE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAChD,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IACvD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,UAAU,CAAC;IACjD,WAAW,CAAC,EAAE,+BAA+B,CAAC;IAC9C,YAAY,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,CAAC,EAAE,8BAA8B,CAAC,CAAC,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,WAAW,IAAI;IACnD,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;IACzB,QAAQ,CAAC,oBAAoB,EAAE,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvD,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxC,KAAK,CAAC,MAAM,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC1C,wFAAwF;IACxF,OAAO,IAAI,IAAI,CAAC;IAChB,gDAAgD;IAChD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxC,aAAa,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAC/C,IAAI,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACvC,qBAAqB,IAAI,IAAI,CAAC;IAC9B,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,MAAM,CAAC,UAAU,CAAC,EAAE,iBAAiB,EAAE,WAAW,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAChF,cAAc,IAAI,IAAI,CAAC;CACxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,SAAS,WAAW,EACzD,SAAS,sBAAsB,CAAC,CAAC,CAAC,KACjC,eAAe,CAAC,CAAC,CAwInB,CAAC"}
1
+ {"version":3,"file":"option-list.d.ts","sourceRoot":"","sources":["../../src/core/option-list.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,QAAQ,EAAU,MAAM,kBAAkB,CAAC;AAGnE,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,8BAA8B,EACpC,MAAM,OAAO,CAAC;AACf,OAAO,EAAiC,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC5G,OAAO,EAA4B,KAAK,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAExF,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC;AAEjC,kEAAkE;AAClE,MAAM,MAAM,+BAA+B,GAAG,IAAI,CAAC,yBAAyB,EAAE,aAAa,GAAG,cAAc,CAAC,CAAC;AAE9G,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,WAAW,IAAI;IAC1D,WAAW,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACtC,uBAAuB,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACnD,QAAQ,EAAE,MAAM,SAAS,CAAC,EAAE,CAAC;IAC7B,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,QAAQ,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACnC,YAAY,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IAChE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAChD,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IACvD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,UAAU,CAAC;IACjD,WAAW,CAAC,EAAE,+BAA+B,CAAC;IAC9C,YAAY,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,CAAC,EAAE,8BAA8B,CAAC,CAAC,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,WAAW,IAAI;IACnD,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;IACzB,QAAQ,CAAC,oBAAoB,EAAE,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvD,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxC,KAAK,CAAC,MAAM,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC1C,wFAAwF;IACxF,OAAO,IAAI,IAAI,CAAC;IAChB,gDAAgD;IAChD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxC,aAAa,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAC/C,IAAI,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACvC,qBAAqB,IAAI,IAAI,CAAC;IAC9B,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,MAAM,CAAC,UAAU,CAAC,EAAE,iBAAiB,EAAE,WAAW,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAChF,cAAc,IAAI,IAAI,CAAC;CACxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,SAAS,WAAW,EACzD,SAAS,sBAAsB,CAAC,CAAC,CAAC,KACjC,eAAe,CAAC,CAAC,CA2InB,CAAC"}
@@ -9,62 +9,64 @@ var s = (s) => {
9
9
  if (typeof s.getBoundary != "function") throw new e("createListboxDropdown: getBoundary is required");
10
10
  if (typeof s.getPanel != "function") throw new e("createListboxDropdown: getPanel is required");
11
11
  if (typeof s.getReference != "function") throw new e("createListboxDropdown: getReference is required");
12
- let c = a(!1), l = i(() => String(c.value)), u = null, d = r({
12
+ let c = a(!1), l = i(() => String(c.value)), u = !1, d = null, f = r({
13
13
  ...s.positioning,
14
14
  getFloating: s.getPanel,
15
15
  getReference: s.getReference
16
- }), f = () => {
17
- s.getFocusedOptionElement?.()?.scrollIntoView({ block: "nearest" });
18
- }, p = n({
16
+ }), p = () => {
17
+ u || s.getFocusedOptionElement?.()?.scrollIntoView({ block: "nearest" });
18
+ }, m = n({
19
19
  disabled: i(() => !c.value),
20
20
  getItems: s.getItems,
21
21
  isItemDisabled: s.isItemDisabled,
22
22
  keys: s.keys,
23
23
  loop: s.loop ?? !0,
24
24
  onNavigate: (e) => {
25
- f(), s.onNavigate?.(e);
25
+ p(), s.onNavigate?.(e);
26
26
  },
27
27
  orientation: s.orientation,
28
28
  signal: s.signal,
29
29
  typeahead: s.typeahead
30
- }), m = i(() => {
31
- let e = p.focusedIndex.value;
30
+ }), h = i(() => {
31
+ let e = m.focusedIndex.value;
32
32
  return s.getOptionId && c.value && e >= 0 ? s.getOptionId(e) : null;
33
- }), h = () => typeof s.restoreFocus == "function" ? s.restoreFocus() : s.restoreFocus ?? !0, g = (e = "programmatic", t = !0) => {
34
- c.value && (c.value = !1, p.reset(), u?.(), u = null, t && h() && s.getTrigger && o(s.getTrigger), s.onClose?.(e));
35
- }, _ = (e = "programmatic") => {
36
- s.isDisabled?.() || c.value || (c.value = !0, d.update(), u = d.startAutoUpdate?.() ?? null, s.onOpen?.(e));
37
- }, v = (e = "click", t = "trigger") => {
38
- c.value ? g(t) : _(e);
33
+ }), g = () => typeof s.restoreFocus == "function" ? s.restoreFocus() : s.restoreFocus ?? !0, _ = (e = "programmatic", t = !0) => {
34
+ !u && c.value && (c.value = !1, m.reset(), d?.(), d = null, t && g() && s.getTrigger && o(s.getTrigger), s.onClose?.(e));
35
+ }, v = (e = "programmatic") => {
36
+ u || s.isDisabled?.() || c.value || (c.value = !0, f.update(), d = f.startAutoUpdate?.() ?? null, s.onOpen?.(e));
37
+ }, y = (e = "click", t = "trigger") => {
38
+ u || (c.value ? _(t) : v(e));
39
39
  };
40
40
  t({
41
41
  getTargets: () => [s.getBoundary(), s.getPanel()],
42
42
  isActive: () => c.value,
43
- onDismiss: () => g("outsideClick"),
43
+ onDismiss: () => _("outsideClick"),
44
44
  signal: s.signal
45
45
  });
46
- let y = !1, b = () => {
47
- y || (y = !0, c.value && (c.value = !1, p.reset(), u?.(), u = null));
46
+ let b = () => {
47
+ u || (u = !0, m.dispose(), c.value && (c.value = !1, m.reset(), d?.(), d = null));
48
48
  };
49
49
  return s.signal.addEventListener("abort", b, { once: !0 }), {
50
- ariaActiveDescendant: m,
50
+ ariaActiveDescendant: h,
51
51
  ariaExpanded: l,
52
- close: g,
52
+ close: _,
53
53
  dispose: b,
54
54
  get disposed() {
55
- return y;
55
+ return u;
56
56
  },
57
- focusedIndex: p.focusedIndex,
58
- getActiveItem: p.getActiveItem,
59
- handleKeydown: (e) => c.value ? e.key === "Escape" ? (e.preventDefault(), g("escape"), !0) : p.handleKeydown(e) : !1,
57
+ focusedIndex: m.focusedIndex,
58
+ getActiveItem: m.getActiveItem,
59
+ handleKeydown: (e) => u || !c.value ? !1 : e.key === "Escape" ? (e.preventDefault(), _("escape"), !0) : m.handleKeydown(e),
60
60
  isOpen: c,
61
- navigate: p.navigate,
62
- open: _,
63
- scrollFocusedIntoView: f,
64
- set: p.set,
61
+ navigate: m.navigate,
62
+ open: v,
63
+ scrollFocusedIntoView: p,
64
+ set: m.set,
65
65
  [Symbol.dispose]: b,
66
- toggle: v,
67
- updatePosition: d.update
66
+ toggle: y,
67
+ updatePosition: () => {
68
+ u || f.update();
69
+ }
68
70
  };
69
71
  };
70
72
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"option-list.js","names":[],"sources":["../../src/core/option-list.ts"],"sourcesContent":["import { restoreFocus } from '@vielzeug/focus';\nimport { computed, type Readable, signal } from '@vielzeug/ripple';\n\nimport { RefineConfigError } from '../errors';\nimport {\n createListControl,\n type ListNavigationAction,\n type ListNavigationChange,\n type ListNavigationTypeaheadOptions,\n} from './nav';\nimport { createOutsidePointerDismissal, type DropdownCloseReason, type OverlayOpenReason } from './overlay';\nimport { createDropdownPositioner, type DropdownPositionerOptions } from './positioner';\n\nexport type ListboxItem = object;\n\n/** Placement-only options for the listbox dropdown positioner. */\nexport type ListboxDropdownPlacementOptions = Omit<DropdownPositionerOptions, 'getFloating' | 'getReference'>;\n\nexport type ListboxDropdownOptions<T extends ListboxItem> = {\n getBoundary: () => HTMLElement | null;\n getFocusedOptionElement?: () => HTMLElement | null;\n getItems: () => readonly T[];\n getOptionId?: (index: number) => string;\n getPanel: () => HTMLElement | null;\n getReference: () => HTMLElement | null;\n getTrigger?: () => HTMLElement | null;\n isDisabled?: () => boolean;\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n onClose?: (reason: DropdownCloseReason) => void;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n onOpen?: (reason: OverlayOpenReason) => void;\n orientation?: 'both' | 'horizontal' | 'vertical';\n positioning?: ListboxDropdownPlacementOptions;\n restoreFocus?: boolean | (() => boolean);\n signal: AbortSignal;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListboxDropdown<T extends ListboxItem> = {\n [Symbol.dispose](): void;\n readonly ariaActiveDescendant: Readable<string | null>;\n readonly ariaExpanded: Readable<string>;\n close(reason?: DropdownCloseReason): void;\n /** Closes the dropdown (without restoring focus) and disposes list navigation state. */\n dispose(): void;\n /** `true` after `dispose()` has been called. */\n readonly disposed: boolean;\n readonly focusedIndex: Readable<number>;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n readonly isOpen: Readable<boolean>;\n navigate(action: ListNavigationAction): number;\n open(reason?: OverlayOpenReason): void;\n scrollFocusedIntoView(): void;\n set(index: number): number;\n toggle(openReason?: OverlayOpenReason, closeReason?: DropdownCloseReason): void;\n updatePosition(): void;\n};\n\n/**\n * Internal listbox dropdown behavior for select and combobox. It owns only\n * listbox interactions; menu and other overlay families keep their own logic.\n */\nexport const createListboxDropdown = <T extends ListboxItem>(\n options: ListboxDropdownOptions<T>,\n): ListboxDropdown<T> => {\n if (typeof options.getBoundary !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getBoundary is required');\n }\n\n if (typeof options.getPanel !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getPanel is required');\n }\n\n if (typeof options.getReference !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getReference is required');\n }\n\n const isOpen = signal(false);\n const ariaExpanded = computed(() => String(isOpen.value));\n let stopPositioning: (() => void) | null = null;\n\n const positioner = createDropdownPositioner({\n ...options.positioning,\n getFloating: options.getPanel,\n getReference: options.getReference,\n });\n\n const scrollFocusedIntoView = (): void => {\n options.getFocusedOptionElement?.()?.scrollIntoView({ block: 'nearest' });\n };\n\n const list = createListControl<T>({\n disabled: computed(() => !isOpen.value),\n getItems: options.getItems,\n isItemDisabled: options.isItemDisabled,\n keys: options.keys,\n loop: options.loop ?? true,\n onNavigate: (change) => {\n scrollFocusedIntoView();\n options.onNavigate?.(change);\n },\n orientation: options.orientation,\n signal: options.signal,\n typeahead: options.typeahead,\n });\n\n const ariaActiveDescendant = computed<string | null>(() => {\n const index = list.focusedIndex.value;\n\n return options.getOptionId && isOpen.value && index >= 0 ? options.getOptionId(index) : null;\n });\n\n const shouldRestoreFocus = (): boolean => {\n return typeof options.restoreFocus === 'function' ? options.restoreFocus() : (options.restoreFocus ?? true);\n };\n\n const close = (reason: DropdownCloseReason = 'programmatic', shouldRestore = true): void => {\n if (!isOpen.value) return;\n\n isOpen.value = false;\n list.reset();\n stopPositioning?.();\n stopPositioning = null;\n\n if (shouldRestore && shouldRestoreFocus() && options.getTrigger) restoreFocus(options.getTrigger);\n\n options.onClose?.(reason);\n };\n\n const open = (reason: OverlayOpenReason = 'programmatic'): void => {\n if (options.isDisabled?.() || isOpen.value) return;\n\n isOpen.value = true;\n positioner.update();\n stopPositioning = positioner.startAutoUpdate?.() ?? null;\n options.onOpen?.(reason);\n };\n\n const toggle = (openReason: OverlayOpenReason = 'click', closeReason: DropdownCloseReason = 'trigger'): void => {\n if (isOpen.value) close(closeReason);\n else open(openReason);\n };\n\n createOutsidePointerDismissal({\n getTargets: () => [options.getBoundary(), options.getPanel()],\n isActive: () => isOpen.value,\n onDismiss: () => close('outsideClick'),\n signal: options.signal,\n });\n\n let disposed = false;\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n\n if (isOpen.value) {\n isOpen.value = false;\n list.reset();\n stopPositioning?.();\n stopPositioning = null;\n }\n };\n\n options.signal.addEventListener('abort', dispose, { once: true });\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n if (!isOpen.value) return false;\n\n if (event.key === 'Escape') {\n event.preventDefault();\n close('escape');\n\n return true;\n }\n\n return list.handleKeydown(event);\n };\n\n return {\n ariaActiveDescendant,\n ariaExpanded,\n close,\n dispose,\n get disposed() {\n return disposed;\n },\n focusedIndex: list.focusedIndex,\n getActiveItem: list.getActiveItem,\n handleKeydown,\n isOpen,\n navigate: list.navigate,\n open,\n scrollFocusedIntoView,\n set: list.set,\n [Symbol.dispose]: dispose,\n toggle,\n updatePosition: positioner.update,\n };\n};\n"],"mappings":";;;;;;;AAiEA,IAAa,KACX,MACuB;CACvB,IAAI,OAAO,EAAQ,eAAgB,YACjC,MAAM,IAAI,EAAkB,gDAAgD;CAG9E,IAAI,OAAO,EAAQ,YAAa,YAC9B,MAAM,IAAI,EAAkB,6CAA6C;CAG3E,IAAI,OAAO,EAAQ,gBAAiB,YAClC,MAAM,IAAI,EAAkB,iDAAiD;CAG/E,IAAM,IAAS,EAAO,EAAK,GACrB,IAAe,QAAe,OAAO,EAAO,KAAK,CAAC,GACpD,IAAuC,MAErC,IAAa,EAAyB;EAC1C,GAAG,EAAQ;EACX,aAAa,EAAQ;EACrB,cAAc,EAAQ;CACxB,CAAC,GAEK,UAAoC;EACxC,EAAQ,0BAA0B,CAAC,EAAE,eAAe,EAAE,OAAO,UAAU,CAAC;CAC1E,GAEM,IAAO,EAAqB;EAChC,UAAU,QAAe,CAAC,EAAO,KAAK;EACtC,UAAU,EAAQ;EAClB,gBAAgB,EAAQ;EACxB,MAAM,EAAQ;EACd,MAAM,EAAQ,QAAQ;EACtB,aAAa,MAAW;GAEtB,AADA,EAAsB,GACtB,EAAQ,aAAa,CAAM;EAC7B;EACA,aAAa,EAAQ;EACrB,QAAQ,EAAQ;EAChB,WAAW,EAAQ;CACrB,CAAC,GAEK,IAAuB,QAA8B;EACzD,IAAM,IAAQ,EAAK,aAAa;EAEhC,OAAO,EAAQ,eAAe,EAAO,SAAS,KAAS,IAAI,EAAQ,YAAY,CAAK,IAAI;CAC1F,CAAC,GAEK,UACG,OAAO,EAAQ,gBAAiB,aAAa,EAAQ,aAAa,IAAK,EAAQ,gBAAgB,IAGlG,KAAS,IAA8B,gBAAgB,IAAgB,OAAe;EACrF,EAAO,UAEZ,EAAO,QAAQ,IACf,EAAK,MAAM,GACX,IAAkB,GAClB,IAAkB,MAEd,KAAiB,EAAmB,KAAK,EAAQ,cAAY,EAAa,EAAQ,UAAU,GAEhG,EAAQ,UAAU,CAAM;CAC1B,GAEM,KAAQ,IAA4B,mBAAyB;EAC7D,EAAQ,aAAa,KAAK,EAAO,UAErC,EAAO,QAAQ,IACf,EAAW,OAAO,GAClB,IAAkB,EAAW,kBAAkB,KAAK,MACpD,EAAQ,SAAS,CAAM;CACzB,GAEM,KAAU,IAAgC,SAAS,IAAmC,cAAoB;EAC9G,AAAI,EAAO,QAAO,EAAM,CAAW,IAC9B,EAAK,CAAU;CACtB;CAEA,EAA8B;EAC5B,kBAAkB,CAAC,EAAQ,YAAY,GAAG,EAAQ,SAAS,CAAC;EAC5D,gBAAgB,EAAO;EACvB,iBAAiB,EAAM,cAAc;EACrC,QAAQ,EAAQ;CAClB,CAAC;CAED,IAAI,IAAW,IAET,UAAsB;EACtB,MAEJ,IAAW,IAEP,EAAO,UACT,EAAO,QAAQ,IACf,EAAK,MAAM,GACX,IAAkB,GAClB,IAAkB;CAEtB;CAiBA,OAfA,EAAQ,OAAO,iBAAiB,SAAS,GAAS,EAAE,MAAM,GAAK,CAAC,GAezD;EACL;EACA;EACA;EACA;EACA,IAAI,WAAW;GACb,OAAO;EACT;EACA,cAAc,EAAK;EACnB,eAAe,EAAK;EACpB,gBAvBqB,MAChB,EAAO,QAER,EAAM,QAAQ,YAChB,EAAM,eAAe,GACrB,EAAM,QAAQ,GAEP,MAGF,EAAK,cAAc,CAAK,IATL;EAuB1B;EACA,UAAU,EAAK;EACf;EACA;EACA,KAAK,EAAK;GACT,OAAO,UAAU;EAClB;EACA,gBAAgB,EAAW;CAC7B;AACF"}
1
+ {"version":3,"file":"option-list.js","names":[],"sources":["../../src/core/option-list.ts"],"sourcesContent":["import { restoreFocus } from '@vielzeug/focus';\nimport { computed, type Readable, signal } from '@vielzeug/ripple';\n\nimport { RefineConfigError } from '../errors';\nimport {\n createListControl,\n type ListNavigationAction,\n type ListNavigationChange,\n type ListNavigationTypeaheadOptions,\n} from './nav';\nimport { createOutsidePointerDismissal, type DropdownCloseReason, type OverlayOpenReason } from './overlay';\nimport { createDropdownPositioner, type DropdownPositionerOptions } from './positioner';\n\nexport type ListboxItem = object;\n\n/** Placement-only options for the listbox dropdown positioner. */\nexport type ListboxDropdownPlacementOptions = Omit<DropdownPositionerOptions, 'getFloating' | 'getReference'>;\n\nexport type ListboxDropdownOptions<T extends ListboxItem> = {\n getBoundary: () => HTMLElement | null;\n getFocusedOptionElement?: () => HTMLElement | null;\n getItems: () => readonly T[];\n getOptionId?: (index: number) => string;\n getPanel: () => HTMLElement | null;\n getReference: () => HTMLElement | null;\n getTrigger?: () => HTMLElement | null;\n isDisabled?: () => boolean;\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n onClose?: (reason: DropdownCloseReason) => void;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n onOpen?: (reason: OverlayOpenReason) => void;\n orientation?: 'both' | 'horizontal' | 'vertical';\n positioning?: ListboxDropdownPlacementOptions;\n restoreFocus?: boolean | (() => boolean);\n signal: AbortSignal;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListboxDropdown<T extends ListboxItem> = {\n [Symbol.dispose](): void;\n readonly ariaActiveDescendant: Readable<string | null>;\n readonly ariaExpanded: Readable<string>;\n close(reason?: DropdownCloseReason): void;\n /** Closes the dropdown (without restoring focus) and disposes list navigation state. */\n dispose(): void;\n /** `true` after `dispose()` has been called. */\n readonly disposed: boolean;\n readonly focusedIndex: Readable<number>;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n readonly isOpen: Readable<boolean>;\n navigate(action: ListNavigationAction): number;\n open(reason?: OverlayOpenReason): void;\n scrollFocusedIntoView(): void;\n set(index: number): number;\n toggle(openReason?: OverlayOpenReason, closeReason?: DropdownCloseReason): void;\n updatePosition(): void;\n};\n\n/**\n * Internal listbox dropdown behavior for select and combobox. It owns only\n * listbox interactions; menu and other overlay families keep their own logic.\n */\nexport const createListboxDropdown = <T extends ListboxItem>(\n options: ListboxDropdownOptions<T>,\n): ListboxDropdown<T> => {\n if (typeof options.getBoundary !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getBoundary is required');\n }\n\n if (typeof options.getPanel !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getPanel is required');\n }\n\n if (typeof options.getReference !== 'function') {\n throw new RefineConfigError('createListboxDropdown: getReference is required');\n }\n\n const isOpen = signal(false);\n const ariaExpanded = computed(() => String(isOpen.value));\n let disposed = false;\n let stopPositioning: (() => void) | null = null;\n\n const positioner = createDropdownPositioner({\n ...options.positioning,\n getFloating: options.getPanel,\n getReference: options.getReference,\n });\n\n const scrollFocusedIntoView = (): void => {\n if (!disposed) options.getFocusedOptionElement?.()?.scrollIntoView({ block: 'nearest' });\n };\n\n const list = createListControl<T>({\n disabled: computed(() => !isOpen.value),\n getItems: options.getItems,\n isItemDisabled: options.isItemDisabled,\n keys: options.keys,\n loop: options.loop ?? true,\n onNavigate: (change) => {\n scrollFocusedIntoView();\n options.onNavigate?.(change);\n },\n orientation: options.orientation,\n signal: options.signal,\n typeahead: options.typeahead,\n });\n\n const ariaActiveDescendant = computed<string | null>(() => {\n const index = list.focusedIndex.value;\n\n return options.getOptionId && isOpen.value && index >= 0 ? options.getOptionId(index) : null;\n });\n\n const shouldRestoreFocus = (): boolean => {\n return typeof options.restoreFocus === 'function' ? options.restoreFocus() : (options.restoreFocus ?? true);\n };\n\n const close = (reason: DropdownCloseReason = 'programmatic', shouldRestore = true): void => {\n if (disposed || !isOpen.value) return;\n\n isOpen.value = false;\n list.reset();\n stopPositioning?.();\n stopPositioning = null;\n\n if (shouldRestore && shouldRestoreFocus() && options.getTrigger) restoreFocus(options.getTrigger);\n\n options.onClose?.(reason);\n };\n\n const open = (reason: OverlayOpenReason = 'programmatic'): void => {\n if (disposed || options.isDisabled?.() || isOpen.value) return;\n\n isOpen.value = true;\n positioner.update();\n stopPositioning = positioner.startAutoUpdate?.() ?? null;\n options.onOpen?.(reason);\n };\n\n const toggle = (openReason: OverlayOpenReason = 'click', closeReason: DropdownCloseReason = 'trigger'): void => {\n if (disposed) return;\n if (isOpen.value) close(closeReason);\n else open(openReason);\n };\n\n createOutsidePointerDismissal({\n getTargets: () => [options.getBoundary(), options.getPanel()],\n isActive: () => isOpen.value,\n onDismiss: () => close('outsideClick'),\n signal: options.signal,\n });\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n list.dispose();\n\n if (isOpen.value) {\n isOpen.value = false;\n list.reset();\n stopPositioning?.();\n stopPositioning = null;\n }\n };\n\n options.signal.addEventListener('abort', dispose, { once: true });\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n if (disposed || !isOpen.value) return false;\n\n if (event.key === 'Escape') {\n event.preventDefault();\n close('escape');\n\n return true;\n }\n\n return list.handleKeydown(event);\n };\n\n return {\n ariaActiveDescendant,\n ariaExpanded,\n close,\n dispose,\n get disposed() {\n return disposed;\n },\n focusedIndex: list.focusedIndex,\n getActiveItem: list.getActiveItem,\n handleKeydown,\n isOpen,\n navigate: list.navigate,\n open,\n scrollFocusedIntoView,\n set: list.set,\n [Symbol.dispose]: dispose,\n toggle,\n updatePosition: () => {\n if (!disposed) positioner.update();\n },\n };\n};\n"],"mappings":";;;;;;;AAiEA,IAAa,KACX,MACuB;CACvB,IAAI,OAAO,EAAQ,eAAgB,YACjC,MAAM,IAAI,EAAkB,gDAAgD;CAG9E,IAAI,OAAO,EAAQ,YAAa,YAC9B,MAAM,IAAI,EAAkB,6CAA6C;CAG3E,IAAI,OAAO,EAAQ,gBAAiB,YAClC,MAAM,IAAI,EAAkB,iDAAiD;CAG/E,IAAM,IAAS,EAAO,EAAK,GACrB,IAAe,QAAe,OAAO,EAAO,KAAK,CAAC,GACpD,IAAW,IACX,IAAuC,MAErC,IAAa,EAAyB;EAC1C,GAAG,EAAQ;EACX,aAAa,EAAQ;EACrB,cAAc,EAAQ;CACxB,CAAC,GAEK,UAAoC;EACxC,AAAK,KAAU,EAAQ,0BAA0B,CAAC,EAAE,eAAe,EAAE,OAAO,UAAU,CAAC;CACzF,GAEM,IAAO,EAAqB;EAChC,UAAU,QAAe,CAAC,EAAO,KAAK;EACtC,UAAU,EAAQ;EAClB,gBAAgB,EAAQ;EACxB,MAAM,EAAQ;EACd,MAAM,EAAQ,QAAQ;EACtB,aAAa,MAAW;GAEtB,AADA,EAAsB,GACtB,EAAQ,aAAa,CAAM;EAC7B;EACA,aAAa,EAAQ;EACrB,QAAQ,EAAQ;EAChB,WAAW,EAAQ;CACrB,CAAC,GAEK,IAAuB,QAA8B;EACzD,IAAM,IAAQ,EAAK,aAAa;EAEhC,OAAO,EAAQ,eAAe,EAAO,SAAS,KAAS,IAAI,EAAQ,YAAY,CAAK,IAAI;CAC1F,CAAC,GAEK,UACG,OAAO,EAAQ,gBAAiB,aAAa,EAAQ,aAAa,IAAK,EAAQ,gBAAgB,IAGlG,KAAS,IAA8B,gBAAgB,IAAgB,OAAe;EACtF,MAAa,EAAO,UAExB,EAAO,QAAQ,IACf,EAAK,MAAM,GACX,IAAkB,GAClB,IAAkB,MAEd,KAAiB,EAAmB,KAAK,EAAQ,cAAY,EAAa,EAAQ,UAAU,GAEhG,EAAQ,UAAU,CAAM;CAC1B,GAEM,KAAQ,IAA4B,mBAAyB;EAC7D,KAAY,EAAQ,aAAa,KAAK,EAAO,UAEjD,EAAO,QAAQ,IACf,EAAW,OAAO,GAClB,IAAkB,EAAW,kBAAkB,KAAK,MACpD,EAAQ,SAAS,CAAM;CACzB,GAEM,KAAU,IAAgC,SAAS,IAAmC,cAAoB;EAC1G,MACA,EAAO,QAAO,EAAM,CAAW,IAC9B,EAAK,CAAU;CACtB;CAEA,EAA8B;EAC5B,kBAAkB,CAAC,EAAQ,YAAY,GAAG,EAAQ,SAAS,CAAC;EAC5D,gBAAgB,EAAO;EACvB,iBAAiB,EAAM,cAAc;EACrC,QAAQ,EAAQ;CAClB,CAAC;CAED,IAAM,UAAsB;EACtB,MAEJ,IAAW,IACX,EAAK,QAAQ,GAET,EAAO,UACT,EAAO,QAAQ,IACf,EAAK,MAAM,GACX,IAAkB,GAClB,IAAkB;CAEtB;CAiBA,OAfA,EAAQ,OAAO,iBAAiB,SAAS,GAAS,EAAE,MAAM,GAAK,CAAC,GAezD;EACL;EACA;EACA;EACA;EACA,IAAI,WAAW;GACb,OAAO;EACT;EACA,cAAc,EAAK;EACnB,eAAe,EAAK;EACpB,gBAvBqB,MACjB,KAAY,CAAC,EAAO,QAAc,KAElC,EAAM,QAAQ,YAChB,EAAM,eAAe,GACrB,EAAM,QAAQ,GAEP,MAGF,EAAK,cAAc,CAAK;EAc/B;EACA,UAAU,EAAK;EACf;EACA;EACA,KAAK,EAAK;GACT,OAAO,UAAU;EAClB;EACA,sBAAsB;GACpB,AAAK,KAAU,EAAW,OAAO;EACnC;CACF;AACF"}
@@ -1,2 +1,2 @@
1
- const e=require("./numbers.cjs");let t=require("@vielzeug/arsenal/math");var n=n=>{let r=()=>e.toFiniteNumberOr(n.min?.value,0),i=()=>e.toFiniteNumberOr(n.max?.value,100),a=()=>e.toPositiveStep(n.step?.value,1),o=n=>{let a=r(),o=i();return(0,t.clamp)(e.toFiniteNumberOr(n,a),a,o)},s=t=>{let n=r();return o(Math.round(e.toFiniteNumberOr(t,n)/a())*a())},c=t=>{let n=r(),a=i(),o=e.toFiniteNumberOr(t,n);return a<=n?0:(o-n)/(a-n)*100},l=(e,t)=>{if(t.width<=0)return s(r());let n=Math.max(0,Math.min(1,(e-t.left)/t.width)),a=r(),o=i();return s(a+n*(o-a))},u=(t,n)=>{let s=e.toFiniteNumberOr(n,r());return t===`ArrowRight`||t===`ArrowUp`?o(s+a()):t===`ArrowLeft`||t===`ArrowDown`?o(s-a()):t===`Home`?r():t===`End`?i():null};return{clamp:o,fromClientX:l,handleKeydown:(e,t,r)=>{if(n.disabled?.value||n.readonly?.value)return!1;let i=u(e.key,t);return i!==null&&(e.preventDefault(),r(i),!0)},max:i,min:r,nextFromKey:u,snap:s,toPercent:c}};exports.createSliderControl=n;
1
+ const e=require("./numbers.cjs");let t=require("@vielzeug/arsenal");var n=n=>{let r=()=>e.toFiniteNumberOr(n.min?.value,0),i=()=>e.toFiniteNumberOr(n.max?.value,100),a=()=>e.toPositiveStep(n.step?.value,1),o=n=>{let a=r(),o=i();return(0,t.clamp)(e.toFiniteNumberOr(n,a),a,o)},s=t=>{let n=r();return o(Math.round(e.toFiniteNumberOr(t,n)/a())*a())},c=t=>{let n=r(),a=i(),o=e.toFiniteNumberOr(t,n);return a<=n?0:(o-n)/(a-n)*100},l=(e,t)=>{if(t.width<=0)return s(r());let n=Math.max(0,Math.min(1,(e-t.left)/t.width)),a=r(),o=i();return s(a+n*(o-a))},u=(t,n)=>{let s=e.toFiniteNumberOr(n,r());return t===`ArrowRight`||t===`ArrowUp`?o(s+a()):t===`ArrowLeft`||t===`ArrowDown`?o(s-a()):t===`Home`?r():t===`End`?i():null};return{clamp:o,fromClientX:l,handleKeydown:(e,t,r)=>{if(n.disabled?.value||n.readonly?.value)return!1;let i=u(e.key,t);return i!==null&&(e.preventDefault(),r(i),!0)},max:i,min:r,nextFromKey:u,snap:s,toPercent:c}};exports.createSliderControl=n;
2
2
  //# sourceMappingURL=slider.cjs.map