forty-cdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/accordion/README.md +122 -0
  2. package/aspect-ratio/README.md +76 -0
  3. package/avatar/README.md +100 -0
  4. package/breadcrumbs/README.md +49 -0
  5. package/breakpoints/README.md +81 -0
  6. package/button/README.md +49 -0
  7. package/calendar/README.md +458 -0
  8. package/carousel/README.md +358 -0
  9. package/checkbox/README.md +146 -0
  10. package/combobox/README.md +535 -0
  11. package/context-menu/README.md +139 -0
  12. package/date-field/README.md +184 -0
  13. package/date-picker/README.md +338 -0
  14. package/dialog/README.md +388 -0
  15. package/disclosure/README.md +114 -0
  16. package/drag-drop/README.md +359 -0
  17. package/drawer/README.md +560 -0
  18. package/dropdown-menu/README.md +176 -0
  19. package/fesm2022/forty-cdk-accordion.mjs +348 -0
  20. package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
  21. package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
  22. package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
  23. package/fesm2022/forty-cdk-avatar.mjs +308 -0
  24. package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
  25. package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
  26. package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
  27. package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
  28. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
  29. package/fesm2022/forty-cdk-button.mjs +134 -0
  30. package/fesm2022/forty-cdk-button.mjs.map +1 -0
  31. package/fesm2022/forty-cdk-calendar.mjs +2034 -0
  32. package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
  33. package/fesm2022/forty-cdk-carousel.mjs +968 -0
  34. package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
  35. package/fesm2022/forty-cdk-checkbox.mjs +226 -0
  36. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
  37. package/fesm2022/forty-cdk-combobox.mjs +2596 -0
  38. package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
  39. package/fesm2022/forty-cdk-context-menu.mjs +413 -0
  40. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
  41. package/fesm2022/forty-cdk-core.mjs +9022 -0
  42. package/fesm2022/forty-cdk-core.mjs.map +1 -0
  43. package/fesm2022/forty-cdk-date-field.mjs +744 -0
  44. package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
  45. package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
  46. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
  47. package/fesm2022/forty-cdk-dialog.mjs +707 -0
  48. package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
  49. package/fesm2022/forty-cdk-disclosure.mjs +190 -0
  50. package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
  51. package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
  52. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
  53. package/fesm2022/forty-cdk-drawer.mjs +1641 -0
  54. package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
  55. package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
  56. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
  57. package/fesm2022/forty-cdk-field.mjs +425 -0
  58. package/fesm2022/forty-cdk-field.mjs.map +1 -0
  59. package/fesm2022/forty-cdk-fieldset.mjs +164 -0
  60. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
  61. package/fesm2022/forty-cdk-file-upload.mjs +221 -0
  62. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
  63. package/fesm2022/forty-cdk-hover-card.mjs +496 -0
  64. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
  65. package/fesm2022/forty-cdk-input.mjs +274 -0
  66. package/fesm2022/forty-cdk-input.mjs.map +1 -0
  67. package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
  68. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  69. package/fesm2022/forty-cdk-listbox.mjs +1279 -0
  70. package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
  71. package/fesm2022/forty-cdk-menu.mjs +1439 -0
  72. package/fesm2022/forty-cdk-menu.mjs.map +1 -0
  73. package/fesm2022/forty-cdk-menubar.mjs +787 -0
  74. package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
  75. package/fesm2022/forty-cdk-meter.mjs +211 -0
  76. package/fesm2022/forty-cdk-meter.mjs.map +1 -0
  77. package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
  78. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
  79. package/fesm2022/forty-cdk-number-input.mjs +559 -0
  80. package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
  81. package/fesm2022/forty-cdk-otp-input.mjs +527 -0
  82. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
  83. package/fesm2022/forty-cdk-pagination.mjs +323 -0
  84. package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
  85. package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
  86. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
  87. package/fesm2022/forty-cdk-popover.mjs +698 -0
  88. package/fesm2022/forty-cdk-popover.mjs.map +1 -0
  89. package/fesm2022/forty-cdk-progress.mjs +226 -0
  90. package/fesm2022/forty-cdk-progress.mjs.map +1 -0
  91. package/fesm2022/forty-cdk-radio-group.mjs +378 -0
  92. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
  93. package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
  94. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
  95. package/fesm2022/forty-cdk-search.mjs +205 -0
  96. package/fesm2022/forty-cdk-search.mjs.map +1 -0
  97. package/fesm2022/forty-cdk-select.mjs +1661 -0
  98. package/fesm2022/forty-cdk-select.mjs.map +1 -0
  99. package/fesm2022/forty-cdk-separator.mjs +82 -0
  100. package/fesm2022/forty-cdk-separator.mjs.map +1 -0
  101. package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
  102. package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
  103. package/fesm2022/forty-cdk-slider.mjs +803 -0
  104. package/fesm2022/forty-cdk-slider.mjs.map +1 -0
  105. package/fesm2022/forty-cdk-stepper.mjs +886 -0
  106. package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
  107. package/fesm2022/forty-cdk-switch.mjs +137 -0
  108. package/fesm2022/forty-cdk-switch.mjs.map +1 -0
  109. package/fesm2022/forty-cdk-table.mjs +1518 -0
  110. package/fesm2022/forty-cdk-table.mjs.map +1 -0
  111. package/fesm2022/forty-cdk-tabs.mjs +400 -0
  112. package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
  113. package/fesm2022/forty-cdk-time-field.mjs +593 -0
  114. package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
  115. package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
  116. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
  117. package/fesm2022/forty-cdk-toast.mjs +1153 -0
  118. package/fesm2022/forty-cdk-toast.mjs.map +1 -0
  119. package/fesm2022/forty-cdk-toggle.mjs +516 -0
  120. package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
  121. package/fesm2022/forty-cdk-toolbar.mjs +374 -0
  122. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
  123. package/fesm2022/forty-cdk-tooltip.mjs +672 -0
  124. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
  125. package/fesm2022/forty-cdk-tree.mjs +2007 -0
  126. package/fesm2022/forty-cdk-tree.mjs.map +1 -0
  127. package/fesm2022/forty-cdk-virtualization.mjs +1 -1
  128. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  129. package/fesm2022/forty-cdk.mjs +0 -43310
  130. package/fesm2022/forty-cdk.mjs.map +1 -1
  131. package/field/README.md +97 -0
  132. package/fieldset/README.md +86 -0
  133. package/file-upload/README.md +73 -0
  134. package/hover-card/README.md +171 -0
  135. package/input/README.md +156 -0
  136. package/listbox/README.md +424 -0
  137. package/menu/README.md +181 -0
  138. package/menubar/README.md +140 -0
  139. package/meter/README.md +128 -0
  140. package/navigation-menu/README.md +253 -0
  141. package/number-input/README.md +171 -0
  142. package/otp-input/README.md +198 -0
  143. package/package.json +213 -1
  144. package/pagination/README.md +61 -0
  145. package/pane-resizer/README.md +136 -0
  146. package/popover/README.md +262 -0
  147. package/progress/README.md +115 -0
  148. package/radio-group/README.md +129 -0
  149. package/scroll-area/README.md +184 -0
  150. package/search/README.md +42 -0
  151. package/select/README.md +488 -0
  152. package/separator/README.md +84 -0
  153. package/signal-forms/README.md +72 -0
  154. package/slider/README.md +152 -0
  155. package/stepper/README.md +292 -0
  156. package/switch/README.md +116 -0
  157. package/table/README.md +769 -0
  158. package/tabs/README.md +130 -0
  159. package/time-field/README.md +157 -0
  160. package/time-picker/README.md +172 -0
  161. package/toast/README.md +398 -0
  162. package/toggle/README.md +224 -0
  163. package/toolbar/README.md +109 -0
  164. package/tooltip/README.md +274 -0
  165. package/tree/README.md +708 -0
  166. package/types/forty-cdk-accordion.d.ts +242 -0
  167. package/types/forty-cdk-aspect-ratio.d.ts +59 -0
  168. package/types/forty-cdk-avatar.d.ts +133 -0
  169. package/types/forty-cdk-breadcrumbs.d.ts +92 -0
  170. package/types/forty-cdk-breakpoints.d.ts +141 -0
  171. package/types/forty-cdk-button.d.ts +80 -0
  172. package/types/forty-cdk-calendar.d.ts +914 -0
  173. package/types/forty-cdk-carousel.d.ts +530 -0
  174. package/types/forty-cdk-checkbox.d.ts +141 -0
  175. package/types/forty-cdk-combobox.d.ts +1259 -0
  176. package/types/forty-cdk-context-menu.d.ts +313 -0
  177. package/types/forty-cdk-core.d.ts +5774 -0
  178. package/types/forty-cdk-date-field.d.ts +307 -0
  179. package/types/forty-cdk-date-picker.d.ts +622 -0
  180. package/types/forty-cdk-dialog.d.ts +546 -0
  181. package/types/forty-cdk-disclosure.d.ts +127 -0
  182. package/types/forty-cdk-drag-drop.d.ts +456 -0
  183. package/types/forty-cdk-drawer.d.ts +871 -0
  184. package/types/forty-cdk-dropdown-menu.d.ts +242 -0
  185. package/types/forty-cdk-field.d.ts +236 -0
  186. package/types/forty-cdk-fieldset.d.ts +119 -0
  187. package/types/forty-cdk-file-upload.d.ts +124 -0
  188. package/types/forty-cdk-hover-card.d.ts +320 -0
  189. package/types/forty-cdk-input.d.ts +169 -0
  190. package/types/forty-cdk-internationalized-date.d.ts +1 -1
  191. package/types/forty-cdk-listbox.d.ts +513 -0
  192. package/types/forty-cdk-menu.d.ts +629 -0
  193. package/types/forty-cdk-menubar.d.ts +451 -0
  194. package/types/forty-cdk-meter.d.ts +122 -0
  195. package/types/forty-cdk-navigation-menu.d.ts +514 -0
  196. package/types/forty-cdk-number-input.d.ts +319 -0
  197. package/types/forty-cdk-otp-input.d.ts +248 -0
  198. package/types/forty-cdk-pagination.d.ts +214 -0
  199. package/types/forty-cdk-pane-resizer.d.ts +145 -0
  200. package/types/forty-cdk-popover.d.ts +509 -0
  201. package/types/forty-cdk-progress.d.ts +143 -0
  202. package/types/forty-cdk-radio-group.d.ts +222 -0
  203. package/types/forty-cdk-scroll-area.d.ts +258 -0
  204. package/types/forty-cdk-search.d.ts +142 -0
  205. package/types/forty-cdk-select.d.ts +899 -0
  206. package/types/forty-cdk-separator.d.ts +59 -0
  207. package/types/forty-cdk-signal-forms.d.ts +58 -0
  208. package/types/forty-cdk-slider.d.ts +379 -0
  209. package/types/forty-cdk-stepper.d.ts +650 -0
  210. package/types/forty-cdk-switch.d.ts +87 -0
  211. package/types/forty-cdk-table.d.ts +723 -0
  212. package/types/forty-cdk-tabs.d.ts +235 -0
  213. package/types/forty-cdk-time-field.d.ts +307 -0
  214. package/types/forty-cdk-time-picker.d.ts +578 -0
  215. package/types/forty-cdk-toast.d.ts +598 -0
  216. package/types/forty-cdk-toggle.d.ts +310 -0
  217. package/types/forty-cdk-toolbar.d.ts +217 -0
  218. package/types/forty-cdk-tooltip.d.ts +436 -0
  219. package/types/forty-cdk-tree.d.ts +688 -0
  220. package/types/forty-cdk.d.ts +1 -19743
@@ -0,0 +1,1279 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, ElementRef, model, input, booleanAttribute, numberAttribute, output, computed, signal, effect, Directive, DOCUMENT, PLATFORM_ID, DestroyRef } from '@angular/core';
3
+ import { createDefaults, VirtualizedNavigator, FormUiControlBase, defaultItemToFormValue, singleSelected, injectTextDirection, RovingTabindex, injectTypeahead, Collection, firstEnabledHost, injectHiddenInput, reconcileRovingActive, isInArray, toggleInArray, nextEnabledHandle, findTypeaheadMatch, resolveListNavigation, hostId, registerHandle, registerA11yName, resolveLiftedDragControl, LiveAnnouncer, createKeyboardDragMediator, createPointerDragSession, resolveDropTarget, isDragLiftKey, PreviewController } from 'forty-cdk/core';
4
+ import { isPlatformBrowser } from '@angular/common';
5
+
6
+ const FOR_LISTBOX_CONTEXT = new InjectionToken('FOR_LISTBOX_CONTEXT');
7
+ function injectListboxContext(piece) {
8
+ const ctx = inject(FOR_LISTBOX_CONTEXT, { optional: true });
9
+ if (!ctx) {
10
+ throw new Error(`[forty-cdk/listbox] ${piece} must be used inside a [forListbox] element.`);
11
+ }
12
+ return ctx;
13
+ }
14
+
15
+ /**
16
+ * Library fallback for listbox defaults, read at the root injector when no
17
+ * consumer has called `provideForListboxDefaults`. Exported for the shared defaults
18
+ * contract spec; not re-exported from the primitive's public entry.
19
+ */
20
+ const FOR_LISTBOX_FALLBACK_DEFAULTS = {
21
+ selectionFollowsFocus: false,
22
+ };
23
+ const { token, provideDefaults } = createDefaults('FOR_LISTBOX_DEFAULTS', FOR_LISTBOX_FALLBACK_DEFAULTS);
24
+ /** Token holding the resolved listbox defaults for the current scope. */
25
+ const FOR_LISTBOX_DEFAULTS = token;
26
+ /**
27
+ * Configures forty-cdk listbox defaults for this injector scope. Partial
28
+ * overrides inherit unspecified keys from the parent scope (or library
29
+ * defaults at the root).
30
+ */
31
+ function provideForListboxDefaults(defaults = {}) {
32
+ return provideDefaults(defaults);
33
+ }
34
+
35
+ /**
36
+ * Virtualization navigation engine for `ForListbox`. A thin adapter over the
37
+ * shared `_internal/virtualized-navigator` engine: the listbox snapshot needs
38
+ * only the id + disabled flag (it never reads option values), so no NG0950 read
39
+ * guard is required.
40
+ *
41
+ * Internal — not re-exported from `listbox/index.ts` or `public-api.ts`.
42
+ */
43
+ class ListboxVirtualizedNavigator {
44
+ #core;
45
+ constructor(deps) {
46
+ this.#core = new VirtualizedNavigator({ ...deps, loop: deps.loop }, {
47
+ posOf: (o) => o.posInSet(),
48
+ idOf: (o) => o.id(),
49
+ hostOf: (o) => o.host,
50
+ readEntry: (o) => ({ id: o.id(), disabled: o.disabled() }),
51
+ });
52
+ }
53
+ /** @see VirtualizedNavigator.prime */
54
+ prime() {
55
+ this.#core.prime();
56
+ }
57
+ /** @see VirtualizedNavigator.tryResolvePending */
58
+ tryResolvePending() {
59
+ return this.#core.tryResolvePending();
60
+ }
61
+ /** @see VirtualizedNavigator.navigate */
62
+ navigate(direction) {
63
+ this.#core.navigate(direction);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Headless implementation of the [WAI-ARIA Listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/).
69
+ * Implements `FormValueControl<readonly T[]>` from
70
+ * `@angular/forms/signals` for `[formField]` auto-wiring.
71
+ *
72
+ * Generic over the option value type `T` (default `string`). When the
73
+ * consumer binds object items the directive infers `T` from `[(value)]` and
74
+ * `[forListboxOption][value]`; object identity is resolved by the
75
+ * consumer-supplied `[isItemEqualToValue]` and the hidden inputs serialize
76
+ * via `[itemToFormValue]`. Option display text is read from the rendered
77
+ * `textContent`, so no separate label function is needed.
78
+ *
79
+ * Selection is always modeled as `readonly T[]`:
80
+ * - In single mode (`multiple=false`, default), the array has 0 or 1 element.
81
+ * - In multi mode, any number of items can be selected.
82
+ *
83
+ * Single-select consumers can read the sole value through the
84
+ * {@link ForListbox.selected} convenience accessor instead of unwrapping the
85
+ * array.
86
+ *
87
+ * Keyboard supports the full APG-recommended model: arrows + Home/End for
88
+ * focus movement, Space/Enter (via native `<button>` activation) to select
89
+ * or toggle, typeahead, plus multi-select range modifiers (Shift+Arrow,
90
+ * Shift+Space, Ctrl/Cmd+A, Ctrl+Shift+Home/End).
91
+ */
92
+ class ForListbox extends FormUiControlBase {
93
+ #host = inject(ElementRef);
94
+ #defaults = inject(FOR_LISTBOX_DEFAULTS);
95
+ /**
96
+ * Two-way bindable. Selected option values. Single-mode keeps 0 or 1
97
+ * element. The `model()` change emitter (`(valueChange)`) fires only on
98
+ * internal selection changes (option activation or `selectionFollowsFocus`
99
+ * nav), never on consumer writes via `[(value)]` — observe transitions
100
+ * without binding back.
101
+ */
102
+ value = model([], /* @ts-ignore */
103
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
104
+ /**
105
+ * Compare two items for equality. Defaults to `===`, which is the
106
+ * correct identity for primitive `T` (e.g. strings, numbers). Override
107
+ * when binding object items so the directive can locate selected entries
108
+ * by id (or any other stable key) instead of by reference:
109
+ * `[isItemEqualToValue]="(a, b) => a.id === b.id"`.
110
+ */
111
+ isItemEqualToValue = input((a, b) => a === b, /* @ts-ignore */
112
+ ...(ngDevMode ? [{ debugName: "isItemEqualToValue" }] : /* istanbul ignore next */ []));
113
+ /**
114
+ * Serialize an item for the hidden input that participates in native
115
+ * form submission. Defaults to identity for strings and to
116
+ * `JSON.stringify` for non-string items so the primitive works out of
117
+ * the box round-tripping objects. Override to emit a specific wire
118
+ * format — typically a per-item id — when the backend expects that:
119
+ * `[itemToFormValue]="(it) => it.id"`.
120
+ */
121
+ itemToFormValue = input(defaultItemToFormValue, /* @ts-ignore */
122
+ ...(ngDevMode ? [{ debugName: "itemToFormValue" }] : /* istanbul ignore next */ []));
123
+ /**
124
+ * Read-only single-select convenience view of {@link value}. Returns the
125
+ * sole value when exactly one is selected (regardless of `multiple`),
126
+ * otherwise `null` (zero, or 2+ selected). Lets single-select consumers
127
+ * read `selected()` instead of unwrapping `value()[0]`. The array-backed
128
+ * `value` model remains the source of truth and the `FormValueControl`
129
+ * contract; this is a derived accessor.
130
+ */
131
+ selected = singleSelected(this.value);
132
+ multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
133
+ /**
134
+ * Manual `aria-label` for the listbox. Use this when no visible label
135
+ * element exists; otherwise prefer pointing `aria-labelledby` at one. A
136
+ * `null` (default) or empty value emits no attribute.
137
+ */
138
+ ariaLabel = input(null, /* @ts-ignore */
139
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
140
+ orientation = input('vertical', /* @ts-ignore */
141
+ ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
142
+ /**
143
+ * When true (default), arrow navigation wraps at the ends — moving past the
144
+ * last option focuses the first and vice versa. Set `false` to stop at the
145
+ * boundaries. Mirrors the `loop` input on `ForSelect`, `ForToggleGroup` and
146
+ * `ForCombobox`. Range extension (Shift+Arrow) never wraps regardless of
147
+ * this input, per the APG.
148
+ */
149
+ loop = input(true, { ...(ngDevMode ? { debugName: "loop" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
150
+ /**
151
+ * Total number of items in the source data. When set, enables the
152
+ * virtualized activedescendant focus model and populates `aria-setsize`
153
+ * on each rendered option. Leave unset (default `undefined`) for the
154
+ * standard roving-tabindex model.
155
+ */
156
+ totalCount = input(undefined, { ...(ngDevMode ? { debugName: "totalCount" } : /* istanbul ignore next */ {}), transform: (v) => (v == null ? undefined : numberAttribute(v)) });
157
+ /**
158
+ * Inclusive-exclusive `[start, end)` index range of the currently rendered
159
+ * options. The virtualizer provides this; the listbox uses it to decide
160
+ * whether a navigation target is in the visible window.
161
+ */
162
+ visibleRange = input(undefined, /* @ts-ignore */
163
+ ...(ngDevMode ? [{ debugName: "visibleRange" }] : /* istanbul ignore next */ []));
164
+ /**
165
+ * Emitted when keyboard navigation reaches an option outside the rendered
166
+ * window. The consumer passes this index to `injectVirtualizer`'s
167
+ * `scrollToIndex` so the correct option mounts.
168
+ */
169
+ scrollToIndex = output();
170
+ /**
171
+ * Writing direction. When unset (default `null`), the inherited ambient
172
+ * direction is resolved from the nearest ancestor carrying a `dir` attribute
173
+ * (or `<html dir>`), defaulting to `'ltr'`. An explicit `[dir]` always wins.
174
+ * The resolved value is reflected to the host `dir` attribute and swaps
175
+ * ArrowLeft / ArrowRight semantics in RTL for horizontal listboxes.
176
+ */
177
+ _dirInput = input(null, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
178
+ dir = injectTextDirection(this._dirInput);
179
+ /**
180
+ * Single-mode only: when true, arrow nav also selects the focused option.
181
+ * APG calls this optional and recommends caution — leave off unless your
182
+ * UX truly benefits from selection following focus. Default `false`.
183
+ * The default is read from `provideForListboxDefaults` for the surrounding
184
+ * scope.
185
+ */
186
+ selectionFollowsFocus = input(this.#defaults.selectionFollowsFocus, { ...(ngDevMode ? { debugName: "selectionFollowsFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
187
+ roving = new RovingTabindex();
188
+ #typeahead = injectTypeahead();
189
+ #options = new Collection();
190
+ /**
191
+ * All registered options in DOM (rendered) order. Part of the public
192
+ * {@link ForListboxContext} surface so container-level coordinators (e.g.
193
+ * `ForListboxReorder`) can read the ordered hosts without each option owning a
194
+ * `[forDraggable]` that would fight the listbox's roving tabindex.
195
+ */
196
+ options = this.#options.items;
197
+ #firstEnabledHost = computed(() => firstEnabledHost(this.#options.items()), /* @ts-ignore */
198
+ ...(ngDevMode ? [{ debugName: "#firstEnabledHost" }] : /* istanbul ignore next */ []));
199
+ #firstSelectedHost = computed(() => {
200
+ const selected = this.value();
201
+ if (selected.length === 0) {
202
+ return null;
203
+ }
204
+ const equals = this.isItemEqualToValue();
205
+ for (const option of this.#options.items()) {
206
+ if (!option.disabled() && selected.some((v) => equals(v, option.value()))) {
207
+ return option.host;
208
+ }
209
+ }
210
+ return null;
211
+ }, /* @ts-ignore */
212
+ ...(ngDevMode ? [{ debugName: "#firstSelectedHost" }] : /* istanbul ignore next */ []));
213
+ #virtualized = computed(() => this.totalCount() !== undefined, /* @ts-ignore */
214
+ ...(ngDevMode ? [{ debugName: "#virtualized" }] : /* istanbul ignore next */ []));
215
+ #activeId = signal(null, /* @ts-ignore */
216
+ ...(ngDevMode ? [{ debugName: "#activeId" }] : /* istanbul ignore next */ []));
217
+ /**
218
+ * The active option's `id` when using the activedescendant focus model,
219
+ * `null` in the roving-tabindex path. The host reflects this as
220
+ * `aria-activedescendant`; options read it to compute `data-highlighted`.
221
+ */
222
+ activeDescendantId = computed(() => this.#virtualized() ? this.#activeId() : null, /* @ts-ignore */
223
+ ...(ngDevMode ? [{ debugName: "activeDescendantId" }] : /* istanbul ignore next */ []));
224
+ /**
225
+ * Tabindex for the listbox host. In the virtualized path the host is always
226
+ * the single tab stop. In the roving path the host carries `tabindex="0"`
227
+ * only when no option qualifies — an empty listbox or one whose options are
228
+ * all disabled — so the control is still reachable. A disabled listbox is
229
+ * never tabbable.
230
+ */
231
+ hostTabindex = computed(() => {
232
+ if (this.effectiveDisabled()) {
233
+ return null;
234
+ }
235
+ if (this.#virtualized()) {
236
+ return '0';
237
+ }
238
+ const hasRovingEntry = this.#firstSelectedHost() !== null || this.#firstEnabledHost() !== null;
239
+ return hasRovingEntry ? null : '0';
240
+ }, /* @ts-ignore */
241
+ ...(ngDevMode ? [{ debugName: "hostTabindex" }] : /* istanbul ignore next */ []));
242
+ /**
243
+ * Anchor value for APG range-selection actions (Shift+Space). Stored as the
244
+ * option's *value* (resolved to its current index at range time via
245
+ * {@link isItemEqualToValue}) rather than a DOM index, so reordering or
246
+ * removing options before the anchor can't silently shift the range to the
247
+ * wrong span. Set on every unmodified activation (click / Space / Enter); not
248
+ * affected by Shift+Arrow, which APG defines as per-option toggle.
249
+ */
250
+ #anchorValue = signal(null, /* @ts-ignore */
251
+ ...(ngDevMode ? [{ debugName: "#anchorValue" }] : /* istanbul ignore next */ []));
252
+ #navigator = null;
253
+ #requireNavigator() {
254
+ return (this.#navigator ??= new ListboxVirtualizedNavigator({
255
+ items: this.#options.items,
256
+ totalCount: this.totalCount,
257
+ visibleRange: this.visibleRange,
258
+ loop: this.loop,
259
+ getActiveId: () => this.#activeId(),
260
+ setActiveId: (id) => this.#activeId.set(id),
261
+ emitScrollToIndex: (idx) => this.scrollToIndex.emit(idx),
262
+ }));
263
+ }
264
+ constructor() {
265
+ super();
266
+ injectHiddenInput({
267
+ name: this.name,
268
+ values: this.value,
269
+ serialize: (item) => this.itemToFormValue()(item),
270
+ disabled: this.effectiveDisabled,
271
+ });
272
+ reconcileRovingActive(this.roving, this.#options.items);
273
+ effect(() => {
274
+ this.#options.items();
275
+ if (!this.#virtualized()) {
276
+ return;
277
+ }
278
+ const navigator = this.#requireNavigator();
279
+ navigator.prime();
280
+ navigator.tryResolvePending();
281
+ });
282
+ }
283
+ isSelected(v) {
284
+ return isInArray(this.value(), v, this.isItemEqualToValue());
285
+ }
286
+ activate(v) {
287
+ if (this.effectiveDisabled() || this.readonly()) {
288
+ return;
289
+ }
290
+ if (this.multiple()) {
291
+ this.value.set(toggleInArray(this.value(), v, this.isItemEqualToValue()));
292
+ }
293
+ else {
294
+ // Single-mode: idempotent select (no deselect on click of selected).
295
+ this.value.set([v]);
296
+ }
297
+ this.#anchorValue.set(v);
298
+ }
299
+ extendByArrow(currentOption, action) {
300
+ if (this.effectiveDisabled() || !this.multiple()) {
301
+ return;
302
+ }
303
+ const target = nextEnabledHandle(this.#options.items(), currentOption, action, { loop: false });
304
+ if (target === null) {
305
+ return;
306
+ }
307
+ // Focus moves regardless of readonly — same contract as `navigate()`. Readonly
308
+ // only blocks the selection mutation.
309
+ target.host.focus();
310
+ if (this.readonly()) {
311
+ return;
312
+ }
313
+ this.value.set(toggleInArray(this.value(), target.value(), this.isItemEqualToValue()));
314
+ }
315
+ selectRangeToFocused(currentOption) {
316
+ if (this.effectiveDisabled() || this.readonly() || !this.multiple()) {
317
+ return;
318
+ }
319
+ const options = this.#options.items();
320
+ const currentIndex = options.findIndex((o) => o.host === currentOption);
321
+ if (currentIndex < 0) {
322
+ return;
323
+ }
324
+ const anchorValue = this.#anchorValue();
325
+ const equals = this.isItemEqualToValue();
326
+ const anchorIndex = anchorValue === null ? -1 : options.findIndex((o) => equals(o.value(), anchorValue));
327
+ const start = anchorIndex < 0 ? currentIndex : anchorIndex;
328
+ const [lo, hi] = start <= currentIndex ? [start, currentIndex] : [currentIndex, start];
329
+ const next = [...this.value()];
330
+ for (let i = lo; i <= hi; i++) {
331
+ const opt = options[i];
332
+ if (!opt || opt.disabled()) {
333
+ continue;
334
+ }
335
+ const v = opt.value();
336
+ if (!next.some((x) => equals(x, v))) {
337
+ next.push(v);
338
+ }
339
+ }
340
+ this.value.set(next);
341
+ }
342
+ selectAll() {
343
+ if (this.effectiveDisabled() || this.readonly() || !this.multiple()) {
344
+ return;
345
+ }
346
+ const enabled = [];
347
+ for (const opt of this.#options.items()) {
348
+ if (opt.disabled()) {
349
+ continue;
350
+ }
351
+ enabled.push(opt.value());
352
+ }
353
+ if (enabled.length === 0) {
354
+ return;
355
+ }
356
+ const equals = this.isItemEqualToValue();
357
+ const current = this.value();
358
+ const allSelected = enabled.every((v) => current.some((x) => equals(x, v)));
359
+ this.value.set(allSelected ? [] : enabled);
360
+ }
361
+ selectFromCurrentToEdge(currentOption, edge) {
362
+ if (this.effectiveDisabled() || !this.multiple()) {
363
+ return;
364
+ }
365
+ const options = this.#options.items();
366
+ const currentIndex = options.findIndex((o) => o.host === currentOption);
367
+ if (currentIndex < 0) {
368
+ return;
369
+ }
370
+ const [lo, hi] = edge === 'first' ? [0, currentIndex] : [currentIndex, options.length - 1];
371
+ const equals = this.isItemEqualToValue();
372
+ const next = [...this.value()];
373
+ let firstEnabled = null;
374
+ let lastEnabled = null;
375
+ // Walk forward so the resulting array preserves DOM order (insertion order).
376
+ for (let i = lo; i <= hi; i++) {
377
+ const opt = options[i];
378
+ if (!opt || opt.disabled()) {
379
+ continue;
380
+ }
381
+ const v = opt.value();
382
+ if (!next.some((x) => equals(x, v))) {
383
+ next.push(v);
384
+ }
385
+ if (firstEnabled === null) {
386
+ firstEnabled = opt.host;
387
+ }
388
+ lastEnabled = opt.host;
389
+ }
390
+ const edgeFocusTarget = edge === 'first' ? firstEnabled : lastEnabled;
391
+ // Focus moves regardless of readonly — same contract as `navigate()`. Readonly
392
+ // only blocks the selection mutation.
393
+ edgeFocusTarget?.focus();
394
+ if (this.readonly()) {
395
+ return;
396
+ }
397
+ this.value.set(next);
398
+ }
399
+ navigate(currentOption, action) {
400
+ if (this.effectiveDisabled()) {
401
+ return;
402
+ }
403
+ const target = nextEnabledHandle(this.#options.items(), currentOption, action, {
404
+ loop: this.loop(),
405
+ });
406
+ if (target === null) {
407
+ return;
408
+ }
409
+ target.host.focus();
410
+ target.host.scrollIntoView?.({ block: 'nearest' });
411
+ if (!this.multiple() && this.selectionFollowsFocus() && !this.readonly()) {
412
+ this.value.set([target.value()]);
413
+ }
414
+ }
415
+ handleTypeahead(event) {
416
+ if (!this.#typeahead.handle(event)) {
417
+ return false;
418
+ }
419
+ const options = this.#options.items();
420
+ const currentIndex = options.findIndex((o) => o.host === event.target);
421
+ const match = findTypeaheadMatch(options, {
422
+ buffer: this.#typeahead.buffer(),
423
+ repeated: this.#typeahead.isRepeatedChar(),
424
+ anchorIndex: currentIndex,
425
+ }, (o) => o.host.textContent ?? '', (o) => o.disabled());
426
+ match?.host.focus();
427
+ return true;
428
+ }
429
+ isFirstFocusableOption(el) {
430
+ const firstSelected = this.#firstSelectedHost();
431
+ if (firstSelected) {
432
+ return firstSelected === el;
433
+ }
434
+ return this.#firstEnabledHost() === el;
435
+ }
436
+ isOptionHighlighted(el) {
437
+ return this.roving.active() === el;
438
+ }
439
+ optionTabindex(el) {
440
+ if (this.#virtualized()) {
441
+ return -1;
442
+ }
443
+ return this.roving.hasActive() ? this.roving.tabindexFor(el) : null;
444
+ }
445
+ setActiveOption(el) {
446
+ this.roving.setActive(el);
447
+ }
448
+ registerOption(handle) {
449
+ this.#options.register(handle);
450
+ }
451
+ notifyOptionClick(optionId) {
452
+ if (!this.#virtualized()) {
453
+ return;
454
+ }
455
+ this.#activeId.set(optionId);
456
+ this.#host.nativeElement.focus();
457
+ }
458
+ unregisterOption(handle) {
459
+ this.#options.unregister(handle);
460
+ this.roving.unregister(handle.host);
461
+ if (this.#virtualized() && this.#activeId() === handle.id()) {
462
+ this.#activeId.set(null);
463
+ }
464
+ }
465
+ onHostKeyDown(event) {
466
+ if (!this.#virtualized() || this.effectiveDisabled()) {
467
+ return;
468
+ }
469
+ if (event.key === 'Enter' || event.key === ' ') {
470
+ event.preventDefault();
471
+ this.#activateActiveDescendant();
472
+ return;
473
+ }
474
+ const action = resolveListNavigation(event, {
475
+ orientation: this.orientation(),
476
+ dir: this.dir(),
477
+ pageKeys: true,
478
+ });
479
+ if (action) {
480
+ event.preventDefault();
481
+ this.#requireNavigator().navigate(action);
482
+ return;
483
+ }
484
+ this.#typeaheadVirtualized(event);
485
+ }
486
+ #activateActiveDescendant() {
487
+ const id = this.#activeId();
488
+ if (id === null) {
489
+ return;
490
+ }
491
+ const handle = this.#options.items().find((o) => o.id() === id);
492
+ if (!handle || handle.disabled()) {
493
+ return;
494
+ }
495
+ this.activate(handle.value());
496
+ }
497
+ #typeaheadVirtualized(event) {
498
+ if (!this.#typeahead.handle(event)) {
499
+ return;
500
+ }
501
+ const options = this.#options.items();
502
+ const activeId = this.#activeId();
503
+ const anchor = activeId === null ? -1 : options.findIndex((o) => o.id() === activeId);
504
+ const match = findTypeaheadMatch(options, {
505
+ buffer: this.#typeahead.buffer(),
506
+ repeated: this.#typeahead.isRepeatedChar(),
507
+ anchorIndex: anchor,
508
+ }, (o) => o.host.textContent ?? '', (o) => o.disabled());
509
+ if (match) {
510
+ this.#activeId.set(match.id());
511
+ match.host.scrollIntoView?.({ block: 'nearest' });
512
+ }
513
+ }
514
+ onHostFocusIn() {
515
+ if (!this.#virtualized() || this.effectiveDisabled()) {
516
+ return;
517
+ }
518
+ if (this.#activeId() !== null) {
519
+ return;
520
+ }
521
+ const items = this.#options.items();
522
+ if (items.length === 0) {
523
+ return;
524
+ }
525
+ const ordered = [...items].sort((a, b) => (a.posInSet() ?? 0) - (b.posInSet() ?? 0));
526
+ const equals = this.isItemEqualToValue();
527
+ const value = this.value();
528
+ const selectedFirst = ordered.find((o) => !o.disabled() && value.some((v) => equals(v, o.value())));
529
+ const target = selectedFirst ?? ordered.find((o) => !o.disabled());
530
+ if (target) {
531
+ this.#activeId.set(target.id());
532
+ }
533
+ }
534
+ onFocusOut(event) {
535
+ const next = event.relatedTarget;
536
+ if (next && this.#host.nativeElement.contains(next)) {
537
+ return;
538
+ }
539
+ this.markTouched();
540
+ }
541
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListbox, deps: [], target: i0.ɵɵFactoryTarget.Directive });
542
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForListbox, isStandalone: true, selector: "[forListbox]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToFormValue: { classPropertyName: "itemToFormValue", publicName: "itemToFormValue", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, visibleRange: { classPropertyName: "visibleRange", publicName: "visibleRange", isSignal: true, isRequired: false, transformFunction: null }, _dirInput: { classPropertyName: "_dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, selectionFollowsFocus: { classPropertyName: "selectionFollowsFocus", publicName: "selectionFollowsFocus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", scrollToIndex: "scrollToIndex" }, host: { attributes: { "role": "listbox" }, listeners: { "keydown": "onHostKeyDown($event)", "focusin": "onHostFocusIn()", "focusout": "onFocusOut($event)" }, properties: { "attr.aria-label": "ariaLabel() || null", "attr.aria-orientation": "orientation()", "attr.aria-multiselectable": "multiple() ? \"true\" : null", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.aria-readonly": "readonly() ? \"true\" : null", "attr.aria-required": "required() ? \"true\" : null", "attr.aria-invalid": "invalid() ? \"true\" : null", "attr.aria-busy": "pending() ? \"true\" : null", "attr.aria-activedescendant": "activeDescendantId()", "attr.tabindex": "hostTabindex()", "attr.data-orientation": "orientation()", "attr.data-disabled": "effectiveDisabled() ? \"\" : null", "attr.dir": "dir()" } }, providers: [{ provide: FOR_LISTBOX_CONTEXT, useExisting: ForListbox }], exportAs: ["forListbox"], usesInheritance: true, ngImport: i0 });
543
+ }
544
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListbox, decorators: [{
545
+ type: Directive,
546
+ args: [{
547
+ selector: '[forListbox]',
548
+ exportAs: 'forListbox',
549
+ host: {
550
+ role: 'listbox',
551
+ '[attr.aria-label]': 'ariaLabel() || null',
552
+ '[attr.aria-orientation]': 'orientation()',
553
+ '[attr.aria-multiselectable]': 'multiple() ? "true" : null',
554
+ '[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
555
+ '[attr.aria-readonly]': 'readonly() ? "true" : null',
556
+ '[attr.aria-required]': 'required() ? "true" : null',
557
+ '[attr.aria-invalid]': 'invalid() ? "true" : null',
558
+ '[attr.aria-busy]': 'pending() ? "true" : null',
559
+ '[attr.aria-activedescendant]': 'activeDescendantId()',
560
+ '[attr.tabindex]': 'hostTabindex()',
561
+ '[attr.data-orientation]': 'orientation()',
562
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
563
+ '[attr.dir]': 'dir()',
564
+ '(keydown)': 'onHostKeyDown($event)',
565
+ '(focusin)': 'onHostFocusIn()',
566
+ '(focusout)': 'onFocusOut($event)',
567
+ },
568
+ providers: [{ provide: FOR_LISTBOX_CONTEXT, useExisting: ForListbox }],
569
+ }]
570
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToFormValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToFormValue", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], totalCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalCount", required: false }] }], visibleRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "visibleRange", required: false }] }], scrollToIndex: [{ type: i0.Output, args: ["scrollToIndex"] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], selectionFollowsFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionFollowsFocus", required: false }] }] } });
571
+
572
+ /**
573
+ * Injection key the `[forListboxOptionIndicator]` uses to resolve its parent
574
+ * option, decoupled from the concrete `ForListboxOption` class.
575
+ * `ForListboxOption` provides itself under this token, so a design system
576
+ * wrapping the option by subclassing re-points it at the subclass with a
577
+ * single provider (`{ provide: FOR_LISTBOX_OPTION, useExisting: MtxListboxOption }`)
578
+ * and the indicator keeps resolving — see `docs/wrapping-form-primitives.md`.
579
+ */
580
+ const FOR_LISTBOX_OPTION = new InjectionToken('FOR_LISTBOX_OPTION');
581
+ /**
582
+ * One option inside a `ForListbox`. Apply on a `<button type="button">` so
583
+ * Space / Enter activation come from native button behavior — printable keys
584
+ * fall through to the parent listbox for typeahead matching.
585
+ *
586
+ * Generic over the option value type `T` (default `string`). Inferred from
587
+ * the `[value]` binding so consumers can pass either primitive ids or full
588
+ * objects (`[value]="lang"` infers `T = Language`); the parent `[forListbox]`
589
+ * must be parameterized over the same `T`. The parent's
590
+ * `[isItemEqualToValue]` decides how options are matched against the
591
+ * committed selection.
592
+ */
593
+ class ForListboxOption {
594
+ #group = injectListboxContext('ForListboxOption');
595
+ #host = inject(ElementRef);
596
+ /**
597
+ * Stable identifier serialized into `[(value)]` and the hidden input.
598
+ * Defaults to `string` for back-compat; bind an object to specialize the
599
+ * parent `[forListbox]` over a richer `T`. The parent's
600
+ * `[isItemEqualToValue]` decides how options are matched against the
601
+ * committed selection.
602
+ */
603
+ value = input.required(/* @ts-ignore */
604
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
605
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
606
+ /**
607
+ * Zero-based absolute position of this option in the full source data.
608
+ * Required in the virtualized path — the listbox uses it to build the
609
+ * position snapshot and to emit `aria-posinset` (which is `posInSet + 1`).
610
+ * Leave unset (default `null`) outside the virtualized path.
611
+ */
612
+ posInSet = input(null, /* @ts-ignore */
613
+ ...(ngDevMode ? [{ debugName: "posInSet" }] : /* istanbul ignore next */ []));
614
+ id = hostId('for-listbox-option');
615
+ selected = computed(() => this.#group.isSelected(this.value()), /* @ts-ignore */
616
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
617
+ /**
618
+ * True when this option is the keyboard-focused / active candidate.
619
+ * In the roving-tabindex path this tracks the DOM-focused option.
620
+ * In the virtualized activedescendant path it tracks `aria-activedescendant`.
621
+ * Reflected as `data-highlighted` so consumers can style it uniformly
622
+ * with the other primitives.
623
+ */
624
+ highlighted = computed(() => {
625
+ const activeId = this.#group.activeDescendantId();
626
+ if (activeId !== null) {
627
+ return activeId === this.id();
628
+ }
629
+ return this.#group.isOptionHighlighted(this.#host.nativeElement);
630
+ }, /* @ts-ignore */
631
+ ...(ngDevMode ? [{ debugName: "highlighted" }] : /* istanbul ignore next */ []));
632
+ ariaSetSize = computed(() => {
633
+ const total = this.#group.totalCount();
634
+ return total === undefined ? null : String(total);
635
+ }, /* @ts-ignore */
636
+ ...(ngDevMode ? [{ debugName: "ariaSetSize" }] : /* istanbul ignore next */ []));
637
+ ariaPosInSet = computed(() => {
638
+ if (this.#group.totalCount() === undefined) {
639
+ return null;
640
+ }
641
+ const pos = this.posInSet();
642
+ return pos === null ? null : String(pos + 1);
643
+ }, /* @ts-ignore */
644
+ ...(ngDevMode ? [{ debugName: "ariaPosInSet" }] : /* istanbul ignore next */ []));
645
+ effectiveDisabled = computed(() => this.disabled() || this.#group.effectiveDisabled(), /* @ts-ignore */
646
+ ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
647
+ tabindex = computed(() => {
648
+ if (this.effectiveDisabled()) {
649
+ return -1;
650
+ }
651
+ const rovingTabindex = this.#group.optionTabindex(this.#host.nativeElement);
652
+ if (rovingTabindex !== null) {
653
+ return rovingTabindex;
654
+ }
655
+ return this.#group.isFirstFocusableOption(this.#host.nativeElement) ? 0 : -1;
656
+ }, /* @ts-ignore */
657
+ ...(ngDevMode ? [{ debugName: "tabindex" }] : /* istanbul ignore next */ []));
658
+ constructor() {
659
+ const handle = {
660
+ host: this.#host.nativeElement,
661
+ value: this.value,
662
+ disabled: this.effectiveDisabled,
663
+ id: this.id,
664
+ posInSet: this.posInSet,
665
+ };
666
+ registerHandle(handle, (h) => this.#group.registerOption(h), (h) => this.#group.unregisterOption(h));
667
+ }
668
+ onClick() {
669
+ if (this.effectiveDisabled() || this.#group.readonly()) {
670
+ return;
671
+ }
672
+ this.#group.activate(this.value());
673
+ this.#group.notifyOptionClick(this.id());
674
+ }
675
+ onPointerDown(event) {
676
+ if (this.#group.totalCount() === undefined) {
677
+ return;
678
+ }
679
+ event.preventDefault();
680
+ }
681
+ onFocus() {
682
+ if (this.effectiveDisabled()) {
683
+ return;
684
+ }
685
+ this.#group.setActiveOption(this.#host.nativeElement);
686
+ }
687
+ onKeyDown(event) {
688
+ if (this.effectiveDisabled()) {
689
+ return;
690
+ }
691
+ // APG-recommended multi-select range modifiers. Single-mode falls through
692
+ // to plain navigation so Shift+Arrow / Ctrl+A behave like their unmodified
693
+ // counterparts (or no-op).
694
+ if (this.#group.multiple()) {
695
+ // Ctrl/Cmd+A — select all enabled (toggle if all already selected).
696
+ if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
697
+ if (event.key === 'a' || event.key === 'A') {
698
+ event.preventDefault();
699
+ this.#group.selectAll();
700
+ return;
701
+ }
702
+ }
703
+ // Ctrl+Shift+Home / Ctrl+Shift+End — extend selection to first/last.
704
+ if ((event.ctrlKey || event.metaKey) && event.shiftKey && !event.altKey) {
705
+ if (event.key === 'Home') {
706
+ event.preventDefault();
707
+ this.#group.selectFromCurrentToEdge(this.#host.nativeElement, 'first');
708
+ return;
709
+ }
710
+ if (event.key === 'End') {
711
+ event.preventDefault();
712
+ this.#group.selectFromCurrentToEdge(this.#host.nativeElement, 'last');
713
+ return;
714
+ }
715
+ }
716
+ // Shift+Space — contiguous range from anchor to current.
717
+ if (event.shiftKey && event.key === ' ') {
718
+ event.preventDefault();
719
+ this.#group.selectRangeToFocused(this.#host.nativeElement);
720
+ return;
721
+ }
722
+ // Shift+Arrow — move focus AND toggle the destination option.
723
+ if (event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
724
+ const action = resolveListNavigation(event, {
725
+ orientation: this.#group.orientation(),
726
+ dir: this.#group.dir(),
727
+ });
728
+ if (action === 'next' || action === 'prev') {
729
+ event.preventDefault();
730
+ this.#group.extendByArrow(this.#host.nativeElement, action);
731
+ return;
732
+ }
733
+ }
734
+ }
735
+ const action = resolveListNavigation(event, {
736
+ orientation: this.#group.orientation(),
737
+ dir: this.#group.dir(),
738
+ pageKeys: true,
739
+ });
740
+ if (action) {
741
+ event.preventDefault();
742
+ this.#group.navigate(this.#host.nativeElement, action);
743
+ return;
744
+ }
745
+ this.#group.handleTypeahead(event);
746
+ }
747
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxOption, deps: [], target: i0.ɵɵFactoryTarget.Directive });
748
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForListboxOption, isStandalone: true, selector: "[forListboxOption]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, posInSet: { classPropertyName: "posInSet", publicName: "posInSet", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "option", "type": "button" }, listeners: { "click": "onClick()", "focus": "onFocus()", "keydown": "onKeyDown($event)", "pointerdown": "onPointerDown($event)" }, properties: { "id": "id()", "attr.aria-selected": "selected() ? \"true\" : \"false\"", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.aria-setsize": "ariaSetSize()", "attr.aria-posinset": "ariaPosInSet()", "attr.tabindex": "tabindex()", "attr.data-state": "selected() ? \"checked\" : \"unchecked\"", "attr.data-highlighted": "highlighted() ? \"\" : null", "attr.data-disabled": "effectiveDisabled() ? \"\" : null" } }, providers: [{ provide: FOR_LISTBOX_OPTION, useExisting: ForListboxOption }], exportAs: ["forListboxOption"], ngImport: i0 });
749
+ }
750
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxOption, decorators: [{
751
+ type: Directive,
752
+ args: [{
753
+ selector: '[forListboxOption]',
754
+ exportAs: 'forListboxOption',
755
+ providers: [{ provide: FOR_LISTBOX_OPTION, useExisting: ForListboxOption }],
756
+ host: {
757
+ role: 'option',
758
+ type: 'button',
759
+ '[id]': 'id()',
760
+ '[attr.aria-selected]': 'selected() ? "true" : "false"',
761
+ '[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
762
+ '[attr.aria-setsize]': 'ariaSetSize()',
763
+ '[attr.aria-posinset]': 'ariaPosInSet()',
764
+ '[attr.tabindex]': 'tabindex()',
765
+ '[attr.data-state]': 'selected() ? "checked" : "unchecked"',
766
+ '[attr.data-highlighted]': 'highlighted() ? "" : null',
767
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
768
+ '(click)': 'onClick()',
769
+ '(focus)': 'onFocus()',
770
+ '(keydown)': 'onKeyDown($event)',
771
+ '(pointerdown)': 'onPointerDown($event)',
772
+ },
773
+ }]
774
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], posInSet: [{ type: i0.Input, args: [{ isSignal: true, alias: "posInSet", required: false }] }] } });
775
+
776
+ /**
777
+ * Optional indicator slot inside a `ForListboxOption`. Apply on the
778
+ * element the consumer wants to show only while this option is selected
779
+ * (typically a check icon to the left of the label). Mirrors `data-state`
780
+ * and hides itself while unselected. Visibility is enforced with an inline
781
+ * `display: none` (which beats any author `display` rule a consumer applies
782
+ * via a class) in addition to the `hidden` attribute that removes it from
783
+ * the a11y tree.
784
+ */
785
+ class ForListboxOptionIndicator {
786
+ parent;
787
+ constructor() {
788
+ const parent = inject(FOR_LISTBOX_OPTION, { optional: true });
789
+ if (!parent) {
790
+ throw new Error('[forty-cdk/listbox] ForListboxOptionIndicator must be used inside a [forListboxOption] element.');
791
+ }
792
+ this.parent = parent;
793
+ }
794
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxOptionIndicator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
795
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForListboxOptionIndicator, isStandalone: true, selector: "[forListboxOptionIndicator]", host: { properties: { "attr.data-state": "parent.selected() ? \"checked\" : \"unchecked\"", "attr.hidden": "parent.selected() ? null : \"\"", "style.display": "parent.selected() ? null : \"none\"" } }, exportAs: ["forListboxOptionIndicator"], ngImport: i0 });
796
+ }
797
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxOptionIndicator, decorators: [{
798
+ type: Directive,
799
+ args: [{
800
+ selector: '[forListboxOptionIndicator]',
801
+ exportAs: 'forListboxOptionIndicator',
802
+ host: {
803
+ '[attr.data-state]': 'parent.selected() ? "checked" : "unchecked"',
804
+ '[attr.hidden]': 'parent.selected() ? null : ""',
805
+ '[style.display]': 'parent.selected() ? null : "none"',
806
+ },
807
+ }]
808
+ }], ctorParameters: () => [] });
809
+
810
+ /**
811
+ * Optional grouping wrapper inside a `ForListbox`. Renders `role="group"`
812
+ * and references the descendant `[forListboxGroupLabel]` (if any) via
813
+ * `aria-labelledby`. Options inside a group still register with the
814
+ * listbox normally, so keyboard navigation traverses across groups in DOM
815
+ * order without special handling.
816
+ */
817
+ class ForListboxGroup {
818
+ #labelId = signal(null, /* @ts-ignore */
819
+ ...(ngDevMode ? [{ debugName: "#labelId" }] : /* istanbul ignore next */ []));
820
+ /** The id of the registered group label (or `null` if none). */
821
+ labelId = this.#labelId.asReadonly();
822
+ /** Called by `ForListboxGroupLabel` on mount. */
823
+ registerLabel(id) {
824
+ this.#labelId.set(id);
825
+ }
826
+ /** Called by `ForListboxGroupLabel` on destroy. Idempotent. */
827
+ unregisterLabel(id) {
828
+ if (this.#labelId() === id) {
829
+ this.#labelId.set(null);
830
+ }
831
+ }
832
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
833
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForListboxGroup, isStandalone: true, selector: "[forListboxGroup]", host: { attributes: { "role": "group" }, properties: { "attr.aria-labelledby": "labelId() || null" } }, exportAs: ["forListboxGroup"], ngImport: i0 });
834
+ }
835
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxGroup, decorators: [{
836
+ type: Directive,
837
+ args: [{
838
+ selector: '[forListboxGroup]',
839
+ exportAs: 'forListboxGroup',
840
+ host: {
841
+ role: 'group',
842
+ '[attr.aria-labelledby]': 'labelId() || null',
843
+ },
844
+ }]
845
+ }] });
846
+
847
+ /**
848
+ * Visible label for a `ForListboxGroup`. Generates a stable id and
849
+ * registers with its parent group so the group's `aria-labelledby` points
850
+ * at this element. Apply on whatever heading element fits the design
851
+ * (often `<div>` styled as a small caption — `<h3>`/`<h4>` if the
852
+ * surrounding document has a sensible heading hierarchy).
853
+ */
854
+ class ForListboxGroupLabel {
855
+ /** Stable host id used by the parent group's `aria-labelledby`. */
856
+ id;
857
+ constructor() {
858
+ const group = inject(ForListboxGroup, { optional: true });
859
+ if (!group) {
860
+ throw new Error('[forty-cdk/listbox] ForListboxGroupLabel must be used inside a [forListboxGroup] element.');
861
+ }
862
+ this.id = registerA11yName(group, 'for-listbox-group-label');
863
+ }
864
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxGroupLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive });
865
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForListboxGroupLabel, isStandalone: true, selector: "[forListboxGroupLabel]", host: { properties: { "id": "id()" } }, exportAs: ["forListboxGroupLabel"], ngImport: i0 });
866
+ }
867
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxGroupLabel, decorators: [{
868
+ type: Directive,
869
+ args: [{
870
+ selector: '[forListboxGroupLabel]',
871
+ exportAs: 'forListboxGroupLabel',
872
+ host: {
873
+ '[id]': 'id()',
874
+ },
875
+ }]
876
+ }], ctorParameters: () => [] });
877
+
878
+ /**
879
+ * Live-region phrasing for `ForListboxReorder`. Framework-free and unit-testable in isolation.
880
+ * Not part of the public API. Mirrors the lift / move / drop / cancel cadence the drag-drop and
881
+ * tree coordinators use so a consumer hears consistent reorder announcements across primitives.
882
+ */
883
+ /** Announcement when an option is lifted. `index` / `total` are 1-based. */
884
+ function announceReorderLift(label, index, total) {
885
+ return `${label}, lifted. ${index} of ${total}.`;
886
+ }
887
+ /** Announcement when the drop position changes during a drag. `index` / `total` are 1-based. */
888
+ function announceReorderMove(label, index, total) {
889
+ return `${label}, moved to position ${index} of ${total}.`;
890
+ }
891
+ /** Announcement on a committed drop. `index` / `total` are 1-based. */
892
+ function announceReorderDrop(label, index, total) {
893
+ return `${label}, dropped at position ${index} of ${total}.`;
894
+ }
895
+ /** Announcement when a drag is cancelled. */
896
+ function announceReorderCancel(label) {
897
+ return `${label}, movement cancelled.`;
898
+ }
899
+
900
+ const POINTER_ARM_THRESHOLD_PX = 5;
901
+ /**
902
+ * Resolves a lifted-drag keydown to a reorder action. `Escape` / `Tab` cancel and
903
+ * `Space` / `Enter` commit (shared with the tree coordinator); arrow keys step the target
904
+ * linearly in DOM order (both axes, so a wrapping chip grid reorders by either pair), and
905
+ * `Home` / `End` jump to the ends. Honors writing direction for the horizontal arrows.
906
+ */
907
+ function resolveReorderAction(event, dir) {
908
+ const control = resolveLiftedDragControl(event);
909
+ if (control) {
910
+ return control;
911
+ }
912
+ const isRtl = dir === 'rtl';
913
+ switch (event.key) {
914
+ case 'ArrowDown':
915
+ return 'next';
916
+ case 'ArrowUp':
917
+ return 'prev';
918
+ case 'ArrowRight':
919
+ return isRtl ? 'prev' : 'next';
920
+ case 'ArrowLeft':
921
+ return isRtl ? 'next' : 'prev';
922
+ case 'Home':
923
+ return 'first';
924
+ case 'End':
925
+ return 'last';
926
+ default:
927
+ return null;
928
+ }
929
+ }
930
+ /**
931
+ * Opt-in **pointer + keyboard reordering** for `ForListbox`, composed onto the same element as
932
+ * `[forListbox]`.
933
+ *
934
+ * `[forDraggable]` cannot stack on a `[forListboxOption]` — both would manage the option's
935
+ * roving tabindex and keyboard, colliding on `tabindex`, on Space / Enter activation, and on the
936
+ * container `orientation` input. This coordinator is the listbox-side analogue of
937
+ * `ForTreeNodeDrag` / `ForTableRowReorder`: it lives on the container, **never touches the
938
+ * option's roving tabindex**, intercepts keys in the capture phase, and owns its own 2D drop
939
+ * geometry — so the listbox keeps full selection + typeahead behavior while becoming sortable.
940
+ *
941
+ * Keyboard: focus an option, press `Ctrl+Space` (or `Cmd+Space`) to lift. While lifted, the
942
+ * arrow keys step the target position (linearly in DOM order, so a wrapping chip grid works with
943
+ * either axis), `Home` / `End` jump to the ends, `Space` / `Enter` drop, `Escape` / `Tab` cancel.
944
+ *
945
+ * Pointer: drag an option past a small threshold to reorder; a short press without movement still
946
+ * selects (the post-drag click is suppressed). A floating preview follows the pointer; drop
947
+ * geometry is resolved in 2D, so vertical lists, horizontal lists, and wrapping grids all work
948
+ * without configuring `orientation`.
949
+ *
950
+ * It **never reorders the options itself** (BYO-data): `(optionReorder)` emits the previous / new
951
+ * index on every committed drop; apply `moveItemInArray(items, from, to)` to your own array.
952
+ * Intended for the standard roving-tabindex listbox; a virtualized listbox (`[totalCount]` set)
953
+ * is left untouched, since reordering a windowed subset is ill-defined.
954
+ *
955
+ * @example
956
+ * ```html
957
+ * <ul forListbox forListboxReorder multiple [(value)]="selected" (optionReorder)="reorder($event)">
958
+ * @for (tag of tags(); track tag) {
959
+ * <li><button type="button" forListboxOption [value]="tag">{{ tag }}</button></li>
960
+ * }
961
+ * </ul>
962
+ * ```
963
+ */
964
+ class ForListboxReorder {
965
+ #ctx = injectListboxContext('ForListboxReorder');
966
+ #host = inject(ElementRef).nativeElement;
967
+ #document = inject(DOCUMENT);
968
+ #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
969
+ #announcer = inject(LiveAnnouncer);
970
+ #destroyRef = inject(DestroyRef);
971
+ /**
972
+ * Disables reorder interactions while leaving selection / typeahead intact. Named distinctly
973
+ * from the listbox's own `disabled` so the two never share a single `[disabled]` binding on the
974
+ * shared host. The listbox being disabled (its own `disabled` or a surrounding `[forFieldset]`)
975
+ * also disables reorder.
976
+ */
977
+ reorderDisabled = input(false, { ...(ngDevMode ? { debugName: "reorderDisabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
978
+ /** Emitted once per committed reorder gesture with the previous / new option index. */
979
+ optionReorder = output();
980
+ _dragging = signal(false, /* @ts-ignore */
981
+ ...(ngDevMode ? [{ debugName: "_dragging" }] : /* istanbul ignore next */ []));
982
+ #mode = 'idle';
983
+ #liftedHost = null;
984
+ #fromIndex = 0;
985
+ #targetIndex = 0;
986
+ #label = '';
987
+ #previewController = null;
988
+ #pointerSession = null;
989
+ constructor() {
990
+ createKeyboardDragMediator({
991
+ host: this.#host,
992
+ isBrowser: this.#isBrowser,
993
+ destroyRef: this.#destroyRef,
994
+ isLifted: () => this.#mode === 'keyboard',
995
+ onIdleKeydown: (event) => this.#onIdleKeydown(event),
996
+ onLiftedKeydown: (event) => this.#onLiftedKeydown(event),
997
+ onFocusOut: (event) => this.#onFocusOut(event),
998
+ });
999
+ if (this.#isBrowser) {
1000
+ this.#pointerSession = createPointerDragSession({
1001
+ host: this.#host,
1002
+ document: this.#document,
1003
+ armThreshold: POINTER_ARM_THRESHOLD_PX,
1004
+ cancelOnEscape: true,
1005
+ capturePointer: true,
1006
+ canStart: (event) => this.#canStartPointer(event),
1007
+ onLift: (event) => this.#onPointerLift(event),
1008
+ onMove: (event) => this.#onPointerMove(event),
1009
+ onCommit: (event) => this.#onPointerCommit(event),
1010
+ onCancel: () => this.#onPointerCancel(),
1011
+ });
1012
+ this.#destroyRef.onDestroy(() => {
1013
+ this.#pointerSession?.destroy();
1014
+ if (this.#mode !== 'idle') {
1015
+ this.#clearSession();
1016
+ }
1017
+ });
1018
+ }
1019
+ }
1020
+ #reorderable() {
1021
+ return (!this.reorderDisabled() &&
1022
+ !this.#ctx.effectiveDisabled() &&
1023
+ this.#ctx.totalCount() === undefined);
1024
+ }
1025
+ #resolveOption(target) {
1026
+ if (!(target instanceof Node)) {
1027
+ return null;
1028
+ }
1029
+ const options = this.#ctx.options();
1030
+ for (let i = 0; i < options.length; i++) {
1031
+ const option = options[i];
1032
+ if (option.host === target || option.host.contains(target)) {
1033
+ return option.disabled() ? null : { host: option.host, index: i };
1034
+ }
1035
+ }
1036
+ return null;
1037
+ }
1038
+ #canStartPointer(event) {
1039
+ if (!this.#reorderable() || (event.pointerType === 'mouse' && event.button !== 0)) {
1040
+ return false;
1041
+ }
1042
+ const resolved = this.#resolveOption(event.target);
1043
+ if (!resolved) {
1044
+ return false;
1045
+ }
1046
+ this.#liftedHost = resolved.host;
1047
+ this.#fromIndex = resolved.index;
1048
+ return true;
1049
+ }
1050
+ #onPointerLift(event) {
1051
+ if (this.#liftedHost === null) {
1052
+ return false;
1053
+ }
1054
+ this.#lift(this.#liftedHost, this.#fromIndex, 'pointer', {
1055
+ x: event.clientX,
1056
+ y: event.clientY,
1057
+ });
1058
+ return true;
1059
+ }
1060
+ #onPointerMove(event) {
1061
+ if (this.#mode !== 'pointer') {
1062
+ return;
1063
+ }
1064
+ const point = { x: event.clientX, y: event.clientY };
1065
+ this.#previewController?.moveTo(point);
1066
+ this.#setTarget(this.#resolvePointerTarget(point), 'polite');
1067
+ }
1068
+ #onPointerCommit(event) {
1069
+ if (this.#mode !== 'pointer') {
1070
+ return;
1071
+ }
1072
+ this.#targetIndex = this.#resolvePointerTarget({ x: event.clientX, y: event.clientY });
1073
+ this.#commit();
1074
+ }
1075
+ #onPointerCancel() {
1076
+ if (this.#mode !== 'pointer') {
1077
+ return;
1078
+ }
1079
+ this.#cancel();
1080
+ }
1081
+ #resolvePointerTarget(point) {
1082
+ const lifted = this.#liftedHost;
1083
+ const itemRects = this.#ctx
1084
+ .options()
1085
+ .filter((option) => option.host !== lifted)
1086
+ .map((option) => option.host.getBoundingClientRect());
1087
+ const target = resolveDropTarget(point, [{ rect: this.#host.getBoundingClientRect(), itemRects }], 'mixed', this.#ctx.dir());
1088
+ return target ? target.index : this.#targetIndex;
1089
+ }
1090
+ #onIdleKeydown(event) {
1091
+ if (this.#mode !== 'idle' || !isDragLiftKey(event) || !this.#reorderable()) {
1092
+ return;
1093
+ }
1094
+ const resolved = this.#resolveOption(event.target);
1095
+ if (!resolved) {
1096
+ return;
1097
+ }
1098
+ event.preventDefault();
1099
+ event.stopPropagation();
1100
+ this.#lift(resolved.host, resolved.index, 'keyboard');
1101
+ }
1102
+ #onLiftedKeydown(event) {
1103
+ const action = resolveReorderAction(event, this.#ctx.dir());
1104
+ if (action === null) {
1105
+ return;
1106
+ }
1107
+ event.preventDefault();
1108
+ event.stopPropagation();
1109
+ switch (action) {
1110
+ case 'cancel':
1111
+ this.#cancel();
1112
+ return;
1113
+ case 'commit':
1114
+ this.#commit();
1115
+ return;
1116
+ case 'next':
1117
+ this.#setTarget(this.#targetIndex + 1, 'polite');
1118
+ return;
1119
+ case 'prev':
1120
+ this.#setTarget(this.#targetIndex - 1, 'polite');
1121
+ return;
1122
+ case 'first':
1123
+ this.#setTarget(0, 'polite');
1124
+ return;
1125
+ case 'last':
1126
+ this.#setTarget(this.#ctx.options().length - 1, 'polite');
1127
+ return;
1128
+ }
1129
+ }
1130
+ #onFocusOut(event) {
1131
+ if (this.#mode !== 'keyboard') {
1132
+ return;
1133
+ }
1134
+ const related = event.relatedTarget;
1135
+ if (related && this.#host.contains(related)) {
1136
+ return;
1137
+ }
1138
+ this.#cancel();
1139
+ }
1140
+ #lift(host, index, mode, point) {
1141
+ this.#mode = mode;
1142
+ this.#liftedHost = host;
1143
+ this.#fromIndex = index;
1144
+ this.#targetIndex = index;
1145
+ this.#label = (host.textContent ?? '').trim();
1146
+ host.setAttribute('data-dragging', '');
1147
+ this._dragging.set(true);
1148
+ if (mode === 'pointer' && point) {
1149
+ this.#previewController = new PreviewController({
1150
+ source: host,
1151
+ point,
1152
+ preview: null,
1153
+ doc: this.#document,
1154
+ boundary: null,
1155
+ lockAxis: () => null,
1156
+ });
1157
+ }
1158
+ this.#announcer.announce(announceReorderLift(this.#label, index + 1, this.#ctx.options().length), 'assertive');
1159
+ }
1160
+ #setTarget(to, politeness) {
1161
+ const total = this.#ctx.options().length;
1162
+ const clamped = Math.max(0, Math.min(total - 1, to));
1163
+ if (clamped === this.#targetIndex) {
1164
+ return;
1165
+ }
1166
+ this.#targetIndex = clamped;
1167
+ this.#announcer.announce(announceReorderMove(this.#label, clamped + 1, total), politeness);
1168
+ }
1169
+ #commit() {
1170
+ if (this.#mode === 'idle' || this.#liftedHost === null) {
1171
+ return;
1172
+ }
1173
+ const total = this.#ctx.options().length;
1174
+ const to = Math.max(0, Math.min(total - 1, this.#targetIndex));
1175
+ const from = this.#fromIndex;
1176
+ const label = this.#label;
1177
+ this.#clearSession();
1178
+ this.optionReorder.emit({ from, to });
1179
+ this.#announcer.announce(announceReorderDrop(label, to + 1, total), 'assertive');
1180
+ }
1181
+ #cancel() {
1182
+ if (this.#mode === 'idle') {
1183
+ return;
1184
+ }
1185
+ const label = this.#label;
1186
+ this.#clearSession();
1187
+ this.#announcer.announce(announceReorderCancel(label), 'assertive');
1188
+ }
1189
+ #clearSession() {
1190
+ this.#liftedHost?.removeAttribute('data-dragging');
1191
+ this.#mode = 'idle';
1192
+ this.#liftedHost = null;
1193
+ this.#fromIndex = 0;
1194
+ this.#targetIndex = 0;
1195
+ this.#label = '';
1196
+ this.#previewController?.destroy();
1197
+ this.#previewController = null;
1198
+ this._dragging.set(false);
1199
+ }
1200
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxReorder, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1201
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForListboxReorder, isStandalone: true, selector: "[forListboxReorder]", inputs: { reorderDisabled: { classPropertyName: "reorderDisabled", publicName: "reorderDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionReorder: "optionReorder" }, host: { properties: { "attr.data-dragging": "_dragging() ? \"\" : null" } }, exportAs: ["forListboxReorder"], ngImport: i0 });
1202
+ }
1203
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxReorder, decorators: [{
1204
+ type: Directive,
1205
+ args: [{
1206
+ selector: '[forListboxReorder]',
1207
+ exportAs: 'forListboxReorder',
1208
+ host: {
1209
+ '[attr.data-dragging]': '_dragging() ? "" : null',
1210
+ },
1211
+ }]
1212
+ }], ctorParameters: () => [], propDecorators: { reorderDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderDisabled", required: false }] }], optionReorder: [{ type: i0.Output, args: ["optionReorder"] }] } });
1213
+
1214
+ /**
1215
+ * Exact public names of every `ForListbox` input, its models included. Spread it into the
1216
+ * `inputs` array of a `hostDirectives` entry so a wrapper component re-exposes the
1217
+ * primitive's full surface — the Signal Forms members `[formField]` binds among them —
1218
+ * without hand-maintaining the list. Always spread into an inline object literal as shown
1219
+ * below: the literal is what keeps the entry statically analyzable for consumers compiling
1220
+ * against the published package. An anti-drift spec fails when this list no longer matches
1221
+ * the directive's actual API. See `docs/wrapping-form-primitives.md` for both supported
1222
+ * wrapping patterns.
1223
+ *
1224
+ * @example
1225
+ * ```ts
1226
+ * @Component({
1227
+ * selector: 'div[myListbox]',
1228
+ * template: '',
1229
+ * hostDirectives: [
1230
+ * {
1231
+ * directive: ForListbox,
1232
+ * inputs: [...FOR_LISTBOX_HOST_DIRECTIVE_INPUTS],
1233
+ * outputs: [...FOR_LISTBOX_HOST_DIRECTIVE_OUTPUTS],
1234
+ * },
1235
+ * ],
1236
+ * })
1237
+ * export class MyListbox {}
1238
+ * ```
1239
+ */
1240
+ const FOR_LISTBOX_HOST_DIRECTIVE_INPUTS = [
1241
+ 'value',
1242
+ 'ariaLabel',
1243
+ 'dir',
1244
+ 'dirty',
1245
+ 'disabled',
1246
+ 'errors',
1247
+ 'invalid',
1248
+ 'isItemEqualToValue',
1249
+ 'itemToFormValue',
1250
+ 'loop',
1251
+ 'multiple',
1252
+ 'name',
1253
+ 'orientation',
1254
+ 'pending',
1255
+ 'readonly',
1256
+ 'required',
1257
+ 'selectionFollowsFocus',
1258
+ 'totalCount',
1259
+ 'touched',
1260
+ 'visibleRange',
1261
+ ];
1262
+ /**
1263
+ * Exact public names of every `ForListbox` output, the Signal Forms `touch` output
1264
+ * included. Spread it into the `outputs` array of the same `hostDirectives` entry as
1265
+ * {@link FOR_LISTBOX_HOST_DIRECTIVE_INPUTS}.
1266
+ */
1267
+ const FOR_LISTBOX_HOST_DIRECTIVE_OUTPUTS = [
1268
+ 'scrollToIndex',
1269
+ 'touch',
1270
+ 'touchedChange',
1271
+ 'valueChange',
1272
+ ];
1273
+
1274
+ /**
1275
+ * Generated bundle index. Do not edit.
1276
+ */
1277
+
1278
+ export { FOR_LISTBOX_CONTEXT, FOR_LISTBOX_DEFAULTS, FOR_LISTBOX_HOST_DIRECTIVE_INPUTS, FOR_LISTBOX_HOST_DIRECTIVE_OUTPUTS, FOR_LISTBOX_OPTION, ForListbox, ForListboxGroup, ForListboxGroupLabel, ForListboxOption, ForListboxOptionIndicator, ForListboxReorder, provideForListboxDefaults };
1279
+ //# sourceMappingURL=forty-cdk-listbox.mjs.map