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,2596 @@
1
+ import * as i0 from '@angular/core';
2
+ import { linkedSignal, untracked, InjectionToken, inject, computed, model, input, booleanAttribute, numberAttribute, output, signal, effect, Directive, ElementRef, DOCUMENT } from '@angular/core';
3
+ import { tryReadHandle, createDefaults, foldSnapshotOnTotalCountTransition, VirtualizedNavigator as VirtualizedNavigator$1, FormUiControlBase, IdGenerator, Collection, defaultItemToFormValue, injectTextDirection, createPointerSuppression, singleSelected, injectHiddenInput, adoptHostId, isInArray, toggleInArray, nextEnabledHandle, emitVetoableEvent, emitVetoableNativeEvent, registerHandle, reflectDisabled, injectOverlayShell, hostId, registerA11yName } from 'forty-cdk/core';
4
+
5
+ /**
6
+ * Combobox-local re-exports of the shared snapshot-fold helpers. The single
7
+ * sources live in `_internal/`: `tryReadHandle` (the NG0950 read guard) in
8
+ * `_internal/signal-graph/read-handle.ts` and `foldSnapshotOnTotalCountTransition`
9
+ * (the `totalCount`-reset `linkedSignal` fold) in
10
+ * `_internal/collection/fold-snapshot.ts`. Kept as a barrel so `OptionLabelCache`
11
+ * and the combobox virtualized navigator import from one combobox-local path.
12
+ */
13
+
14
+ /**
15
+ * Build the host's activedescendant pointer as a `linkedSignal` so the
16
+ * "what should be highlighted given open / items / autoHighlight" decision is a
17
+ * **pure derivation**, never a write from an `effect` (the banned
18
+ * state-propagation pattern). The returned signal is still writable for the
19
+ * genuinely imperative moves that own their own scroll — arrow / Home / End
20
+ * navigation, pointer-move hover, multi-mode activation, and the virtualized
21
+ * pending-nav resolution (`navigate` / `seedFromIndexedSnapshot` /
22
+ * `tryResolvePending` in `combobox-virtualized-navigator.ts`).
23
+ *
24
+ * The reset/seed rule, in order:
25
+ * - On a `query` change the previously-active option may have been filtered
26
+ * out, so the prior pointer is dropped.
27
+ * - A pointer that no longer matches a registered option is dropped (covers the
28
+ * consumer mutating the list without touching `query`).
29
+ * - When a valid pointer survives, it is preserved (arrow nav, hover).
30
+ * - Otherwise, in the **non-virtualized** case, auto-highlight seeds the
31
+ * first / last enabled option (per `initialFocus`) while the listbox is open
32
+ * via {@link resolveAutoHighlightSeed}. The virtualized case returns `null`
33
+ * here and the host's effect seeds it imperatively, because that seed must
34
+ * order options by absolute `posInSet` and must lose to a pending
35
+ * `(scrollToIndex)` resolution.
36
+ *
37
+ * Internal — not re-exported from `combobox/index.ts` or `public-api.ts`.
38
+ */
39
+ function createActiveIdSignal(source) {
40
+ return linkedSignal({
41
+ source: () => ({
42
+ query: source.query(),
43
+ open: source.open(),
44
+ autoHighlight: source.autoHighlight(),
45
+ virtualized: source.virtualized(),
46
+ initialFocus: source.initialFocus(),
47
+ items: source.items(),
48
+ value: source.value(),
49
+ equals: source.equals(),
50
+ }),
51
+ computation: ({ query, open, autoHighlight, virtualized, initialFocus, items, value, equals }, prev) => {
52
+ const queryChanged = prev !== undefined && prev.source.query !== query;
53
+ let current = queryChanged ? null : (prev?.value ?? null);
54
+ if (current !== null && !items.some((o) => o.id() === current)) {
55
+ current = null;
56
+ }
57
+ if (current !== null) {
58
+ return current;
59
+ }
60
+ if (virtualized || !autoHighlight || !open || items.length === 0) {
61
+ return null;
62
+ }
63
+ return resolveAutoHighlightSeed({ items, initialFocus, value, equals });
64
+ },
65
+ });
66
+ }
67
+ /**
68
+ * Resolve the auto-highlight seed for the **non-virtualized** combobox: the id
69
+ * of the option `aria-activedescendant` should fall on when the listbox is open
70
+ * and no pointer survives. Returns `null` when no enabled option qualifies (or
71
+ * when a `'selected'` seed can't yet read its options — see `tryReadHandle`).
72
+ *
73
+ * Pure over its inputs — no signal reads, no DOM access — so the host can call
74
+ * it from inside its `#activeId` linkedSignal computation without leaking the
75
+ * banned write-from-effect pattern. The virtualized branch seeds imperatively
76
+ * via the navigator instead (it orders by absolute `posInSet` and must lose to
77
+ * a pending `(scrollToIndex)` resolution).
78
+ *
79
+ * Internal — not re-exported from `combobox/index.ts` or `public-api.ts`.
80
+ */
81
+ function resolveAutoHighlightSeed(input) {
82
+ const { items, initialFocus, value, equals } = input;
83
+ if (initialFocus === 'selected') {
84
+ const selected = findSelectedEnabled(items, value, equals);
85
+ if (selected === NOT_READY) {
86
+ return null;
87
+ }
88
+ if (selected) {
89
+ return selected.id();
90
+ }
91
+ }
92
+ const target = initialFocus === 'last' ? findLastEnabled(items) : findFirstEnabled(items);
93
+ return target?.id() ?? null;
94
+ }
95
+ function findFirstEnabled(items) {
96
+ for (const item of items) {
97
+ if (!item.disabled()) {
98
+ return item;
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ function findLastEnabled(items) {
104
+ for (let i = items.length - 1; i >= 0; i--) {
105
+ const item = items[i];
106
+ if (item && !item.disabled()) {
107
+ return item;
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+ const NOT_READY = Symbol('forty-cdk/combobox:not-ready');
113
+ function findSelectedEnabled(items, values, equals) {
114
+ if (values.length === 0) {
115
+ return null;
116
+ }
117
+ for (const item of items) {
118
+ if (item.disabled()) {
119
+ continue;
120
+ }
121
+ const read = tryReadHandle(() => ({ value: item.value() }));
122
+ if (read === null) {
123
+ return NOT_READY;
124
+ }
125
+ if (values.some((sel) => equals(read.value, sel))) {
126
+ return item;
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+ /**
132
+ * The **imperative tail** of the auto-highlight flow, run from the host's
133
+ * constructor `effect`. The activedescendant *decision* is a pure derivation in
134
+ * {@link createActiveIdSignal}; this only performs the side effects that escape
135
+ * the reactive graph. It reacts to `items()`, `autoHighlight()` and `open()`:
136
+ *
137
+ * 1. Primes the label cache so its `linkedSignal` `prev` slot gets seeded while
138
+ * the listbox is open — without an eager pull the lazy cache never runs
139
+ * during the open cycle in non-virtualized usage and persistence across
140
+ * close → re-open would start from an empty `prev`. The virtualization
141
+ * navigator (and its position-map) is primed only when the consumer set
142
+ * `totalCount()`, so a plain combobox never builds it.
143
+ * 2. Virtualized only: resolves a pending `(scrollToIndex)` navigation — once
144
+ * the option for the requested posInSet mounts, `tryResolvePending` seeds
145
+ * activedescendant to its id and scrolls it into view. This is the single
146
+ * sanctioned activedescendant write from an effect, and it is a legitimate
147
+ * side effect (not state propagation): it integrates the consumer's
148
+ * virtualizer mounting a row asynchronously, and it must win over the
149
+ * auto-highlight seed — which is why the virtualized seed can't live in the
150
+ * linkedSignal. `seedFromIndexedSnapshot` then seeds the topmost / bottommost
151
+ * *rendered* enabled option (ordered by absolute `posInSet`) deliberately
152
+ * passively — it only moves the pointer, never the consumer's scroll position.
153
+ * 3. Non-virtualized: scrolls the auto-highlight-seeded option into view so a
154
+ * seed that lands below the fold is visible, for parity with `navigate()`.
155
+ * The seed itself comes from the linkedSignal; this is its imperative tail.
156
+ * The activedescendant is read `untracked` so the scroll never re-triggers
157
+ * the effect, and pointer-move hover doesn't reach here (it changes none of
158
+ * the tracked reads), so hovering never scrolls.
159
+ *
160
+ * Internal — not re-exported from `combobox/index.ts` or `public-api.ts`.
161
+ */
162
+ function runAutoHighlightBridge(deps) {
163
+ deps.labelCache.prime();
164
+ const items = deps.items();
165
+ const open = deps.open();
166
+ const autoHighlight = deps.autoHighlight();
167
+ const virtualized = deps.virtualized();
168
+ if (virtualized) {
169
+ const navigator = deps.requireNavigator();
170
+ navigator.prime();
171
+ if (navigator.tryResolvePending()) {
172
+ return;
173
+ }
174
+ if (autoHighlight && open && untracked(() => deps.getActiveId()) === null && items.length > 0) {
175
+ navigator.seedFromIndexedSnapshot(deps.initialFocus() === 'last' ? 'last' : 'first');
176
+ }
177
+ return;
178
+ }
179
+ if (!open) {
180
+ deps.setLastPositionedId(null);
181
+ return;
182
+ }
183
+ const activeId = untracked(() => deps.getActiveId());
184
+ if (activeId === null || activeId === deps.getLastPositionedId()) {
185
+ return;
186
+ }
187
+ const active = items.find((o) => o.id() === activeId);
188
+ active?.host.scrollIntoView?.({ block: 'nearest' });
189
+ deps.setLastPositionedId(activeId);
190
+ }
191
+
192
+ /**
193
+ * `ForCombobox<T>`'s generic does NOT flow to this token: an `InjectionToken`
194
+ * is a single runtime instance, so it is published at `ForComboboxContext<unknown>`.
195
+ * `injectComboboxContext<T>()` re-applies `T` with an `as unknown as` cast, and
196
+ * each piece (input, option, chip) relies on consumer discipline — the
197
+ * `[forComboboxOption][value]` and the root `[(value)]` must be the same `T`.
198
+ * There is no clean fix without abandoning the token pattern; the contract is
199
+ * the consumer's to honor. Object identity is reconciled at runtime via
200
+ * `isItemEqualToValue`, which bounds the practical blast radius of a mismatch.
201
+ */
202
+ const FOR_COMBOBOX_CONTEXT = new InjectionToken('FOR_COMBOBOX_CONTEXT');
203
+ /**
204
+ * Resolve the surrounding combobox context, re-applying the caller's `T`. The
205
+ * cast through `unknown` is unavoidable (see {@link FOR_COMBOBOX_CONTEXT}): the
206
+ * token can't carry the per-instance generic, so `T` correctness is a
207
+ * consumer-honored contract, not a compiler-enforced one.
208
+ */
209
+ function injectComboboxContext(piece) {
210
+ const ctx = inject(FOR_COMBOBOX_CONTEXT, { optional: true });
211
+ if (!ctx) {
212
+ throw new Error(`[forty-cdk/combobox] ${piece} must be used inside a [forCombobox] element. ` +
213
+ "If it is declared inside an ng-template, DI resolves at the template's declaration site — " +
214
+ 'not where it is stamped (e.g. via ngTemplateOutlet) — so declare the template inside the ' +
215
+ '[forCombobox] root.');
216
+ }
217
+ return ctx;
218
+ }
219
+ /**
220
+ * Resolves the trigger's root context: the explicit reference when the
221
+ * `[forComboboxTrigger]` input carries one, the injected `FOR_COMBOBOX_CONTEXT`
222
+ * otherwise. The orphan error only fires when neither resolves, on first read
223
+ * of the returned signal. Must be called in an injection context.
224
+ */
225
+ function injectComboboxTriggerContext(explicitRoot) {
226
+ const injected = inject(FOR_COMBOBOX_CONTEXT, { optional: true });
227
+ return computed(() => {
228
+ const explicit = explicitRoot();
229
+ if (explicit !== '') {
230
+ return explicit;
231
+ }
232
+ if (injected) {
233
+ return injected;
234
+ }
235
+ throw new Error('[forty-cdk/combobox] ForComboboxTrigger could not resolve its [forCombobox] root: ' +
236
+ 'no FOR_COMBOBOX_CONTEXT provider is visible and no explicit root reference was passed. ' +
237
+ "If this trigger is declared inside an ng-template, DI resolves at the template's declaration " +
238
+ 'site — not where it is stamped — so either declare the template inside the root or pass the ' +
239
+ 'root explicitly: [forComboboxTrigger]="root" with #root="forCombobox".');
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Library fallback for combobox defaults, read at the root injector when no
245
+ * consumer has called `provideForComboboxDefaults`. Exported for the shared
246
+ * defaults contract spec; not re-exported from the primitive's public entry.
247
+ */
248
+ const FOR_COMBOBOX_FALLBACK_DEFAULTS = {
249
+ sideOffset: 4,
250
+ collisionPadding: 8,
251
+ };
252
+ const { token, provideDefaults } = createDefaults('FOR_COMBOBOX_DEFAULTS', FOR_COMBOBOX_FALLBACK_DEFAULTS);
253
+ /** Token holding the resolved combobox defaults for the current scope. */
254
+ const FOR_COMBOBOX_DEFAULTS = token;
255
+ /**
256
+ * Configures forty-cdk combobox defaults for this injector scope. Partial
257
+ * overrides inherit unspecified keys from the parent scope (or library
258
+ * defaults at the root).
259
+ */
260
+ function provideForComboboxDefaults(defaults = {}) {
261
+ return provideDefaults(defaults);
262
+ }
263
+
264
+ /**
265
+ * Always-on label cache for `ForCombobox`. Keeps a flat list of
266
+ * `{ id, value, label }` tuples that persists across listbox close / open
267
+ * cycles, driving inline autocomplete and the `selected` label fallback.
268
+ *
269
+ * Persistence: when the consumer's `@if` unmounts the content
270
+ * (`items().length === 0`) the prior cache is carried over so chips outside
271
+ * the listbox area and inline completion after close keep resolving labels.
272
+ * Resets only when the consumer's `totalCount` transitions — a query / source
273
+ * rebuild signal in the virtualized case (the shared stale-window invariant
274
+ * lives in `combobox-snapshot-fold.ts`).
275
+ *
276
+ * The option's `label` is itself a `Signal<string>` so we never peek at
277
+ * `textContent`. Internal helper — not re-exported from `combobox/index.ts`
278
+ * or `public-api.ts`. Constructed once per host directive, on every combobox
279
+ * (virtualized or not).
280
+ */
281
+ class OptionLabelCache {
282
+ #cachedOptions;
283
+ constructor(deps) {
284
+ this.#cachedOptions = foldSnapshotOnTotalCountTransition(deps.items, deps.totalCount, () => [], (prev, items) => {
285
+ if (items.length === 0) {
286
+ return prev;
287
+ }
288
+ const merged = new Map();
289
+ for (const entry of prev) {
290
+ merged.set(entry.id, entry);
291
+ }
292
+ for (const item of items) {
293
+ // A static option (rendered outside `@for`) registers before its
294
+ // `[value]` binding is written; skip it this fold and pick it up on
295
+ // the re-run the binding triggers. See `tryReadHandle`.
296
+ const entry = tryReadHandle(() => ({
297
+ id: item.id(),
298
+ value: item.value(),
299
+ label: item.label(),
300
+ }));
301
+ if (entry === null) {
302
+ continue;
303
+ }
304
+ merged.set(entry.id, entry);
305
+ }
306
+ return [...merged.values()];
307
+ });
308
+ }
309
+ /**
310
+ * Pull the cache so its `linkedSignal` `prev` slot gets seeded while the
311
+ * listbox is open. Called from the host's bridge effect — without an eager
312
+ * pull the lazy cache never runs during the open cycle (no other consumer
313
+ * pulls it in non-virtualized usage), and persistence across close → re-open
314
+ * would start from an empty `prev`.
315
+ */
316
+ prime() {
317
+ this.#cachedOptions();
318
+ }
319
+ /**
320
+ * Snapshot of the registered options as plain `{ id, value, label }` tuples.
321
+ * Drives inline-autocomplete matching and the label-resolution fallback in
322
+ * `ForCombobox.selected`.
323
+ */
324
+ entries() {
325
+ return this.#cachedOptions();
326
+ }
327
+ /**
328
+ * Live entries merged with any off-window entries from the virtualized
329
+ * position-map, so typeahead and inline autocomplete can match against
330
+ * options scrolled out of view. Live entries take precedence (freshest data)
331
+ * and appear first, followed by off-window indexed entries sorted by absolute
332
+ * position. Pass an empty / zero-size map (the non-virtualized case) to get
333
+ * the live entries unchanged.
334
+ */
335
+ mergedEntries(indexed) {
336
+ const live = this.#cachedOptions();
337
+ if (indexed.size === 0) {
338
+ return live;
339
+ }
340
+ const seen = new Set(live.map((o) => o.id));
341
+ const merged = [...live];
342
+ const positions = [...indexed.keys()].sort((a, b) => a - b);
343
+ for (const pos of positions) {
344
+ const entry = indexed.get(pos);
345
+ if (seen.has(entry.id)) {
346
+ continue;
347
+ }
348
+ merged.push({ id: entry.id, value: entry.value, label: entry.label });
349
+ }
350
+ return merged;
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Virtualization navigation engine for `ForCombobox`. A thin adapter over the
356
+ * shared `_internal/virtualized-navigator` engine. Maps the combobox option
357
+ * handle (whose `posInSet` is optional) onto the engine's accessors, reads the
358
+ * option through the single NG0950 read guard, and overrides scroll-into-view
359
+ * with the host's pointer-suppression wrapper. Adds the combobox-only
360
+ * auto-highlight seed that stays passive (never scrolls the window).
361
+ *
362
+ * Internal — not re-exported from `combobox/index.ts` or `public-api.ts`.
363
+ */
364
+ class VirtualizedNavigator {
365
+ #deps;
366
+ #core;
367
+ constructor(deps) {
368
+ this.#deps = deps;
369
+ this.#core = new VirtualizedNavigator$1({ ...deps, loop: deps.loop }, {
370
+ posOf: (o) => o.posInSet?.() ?? null,
371
+ idOf: (o) => o.id(),
372
+ hostOf: (o) => o.host,
373
+ readEntry: (o) => tryReadHandle(() => ({
374
+ id: o.id(),
375
+ value: o.value(),
376
+ label: o.label(),
377
+ disabled: o.disabled(),
378
+ })),
379
+ scrollIntoView: (host) => deps.scrollActiveIntoView(host),
380
+ }, { deferFoldOnTotalTransition: true });
381
+ }
382
+ /** @see VirtualizedNavigator.prime */
383
+ prime() {
384
+ this.#core.prime();
385
+ }
386
+ /**
387
+ * Position-keyed snapshot for `ForCombobox.selected`'s scrolled-out-of-view
388
+ * fallback and for the merged-label lookup. Read-only for callers.
389
+ */
390
+ snapshotByPos() {
391
+ return this.#core.snapshotByPos();
392
+ }
393
+ /** @see VirtualizedNavigator.tryResolvePending */
394
+ tryResolvePending() {
395
+ return this.#core.tryResolvePending();
396
+ }
397
+ /**
398
+ * Auto-highlight the first or last enabled option that is **currently
399
+ * rendered**, ordered by absolute `posInSet`. Used in the virtualized branch
400
+ * of the host's auto-highlight effect — for non-virtualized lists the host
401
+ * walks the live `items()` array directly. No-op if `totalCount` is unset /
402
+ * zero or nothing is rendered.
403
+ *
404
+ * Deliberately *passive*: it only ever moves `aria-activedescendant`, never
405
+ * the consumer's scroll position. Auto-highlight re-runs every time the
406
+ * activedescendant is cleared, and scrolling the active option out of the
407
+ * rendered window clears it (see `ForCombobox.unregisterOption`). If this
408
+ * seed emitted `(scrollToIndex)` toward the absolute first option, every
409
+ * wheel tick that unmounted the active row would snap the listbox straight
410
+ * back to the top. Off-window targets are reached only through explicit
411
+ * keyboard navigation (`navigate`), which owns scroll-into-view.
412
+ */
413
+ seedFromIndexedSnapshot(direction) {
414
+ const total = this.#deps.totalCount();
415
+ if (total === undefined || total <= 0) {
416
+ return;
417
+ }
418
+ const items = this.#deps.items();
419
+ if (items.length === 0) {
420
+ return;
421
+ }
422
+ const ordered = [...items].sort((a, b) => {
423
+ const pa = a.posInSet?.() ?? 0;
424
+ const pb = b.posInSet?.() ?? 0;
425
+ return direction === 'last' ? pb - pa : pa - pb;
426
+ });
427
+ for (const item of ordered) {
428
+ if (!item.disabled()) {
429
+ this.#deps.setActiveId(item.id());
430
+ return;
431
+ }
432
+ }
433
+ }
434
+ /** @see VirtualizedNavigator.navigate */
435
+ navigate(direction) {
436
+ this.#core.navigate(direction);
437
+ }
438
+ /**
439
+ * Drop any pending virtualized navigation. Called by `closeMenu` so a
440
+ * pending request from the previous open cycle doesn't seed activedescendant
441
+ * after the listbox re-opens.
442
+ */
443
+ resetPending() {
444
+ this.#core.resetPending();
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Headless implementation of the [WAI-ARIA combobox with listbox popup pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
450
+ * Implements `FormValueControl<readonly T[]>` from `@angular/forms/signals`
451
+ * for `[formField]` auto-wiring.
452
+ *
453
+ * Generic over the option value type `T` (default `string`). When the
454
+ * consumer binds object items the directive infers `T` from `[(value)]`
455
+ * and per-piece signatures (`[forComboboxOption][value]`,
456
+ * `[forComboboxChip][value]`) specialize accordingly. Object identity is
457
+ * resolved by the consumer-supplied `[isItemEqualToValue]` and labels by
458
+ * `[itemToStringLabel]`; the hidden inputs serialize via
459
+ * `[itemToFormValue]` (defaults to `JSON.stringify` for non-strings).
460
+ *
461
+ * Selection is always modeled as `readonly T[]`:
462
+ * - In single mode (`multiple=false`, default), the array has 0 or 1
463
+ * element and option activation closes the listbox.
464
+ * - In multi mode, option activation toggles in/out and the listbox stays
465
+ * open. Selected entries are typically rendered as chips inside
466
+ * `[forComboboxChips]` next to the input.
467
+ *
468
+ * The visible input ("query") and the form value are separate two-way
469
+ * bindable models — the consumer keeps them in sync via filtering /
470
+ * display logic, and the primitive only commits to `value` when an option
471
+ * is explicitly activated.
472
+ *
473
+ * Filtering is **always** the consumer's responsibility — the primitive is
474
+ * headless and doesn't filter the registered options. Render the filtered
475
+ * subset with `@for` and the registry tracks them automatically.
476
+ */
477
+ class ForCombobox extends FormUiControlBase {
478
+ #idGen = inject(IdGenerator);
479
+ #defaults = inject(FOR_COMBOBOX_DEFAULTS);
480
+ #items = new Collection();
481
+ #chips = new Collection();
482
+ /**
483
+ * Two-way bindable. Visible input text. The `model()` change emitter
484
+ * (`(queryChange)`) fires only on internal mutations (option activation
485
+ * commit, `clear()`, multi-mode select reset, picker-anatomy reset on
486
+ * close), never on consumer writes via `[(query)]`.
487
+ */
488
+ query = model('', /* @ts-ignore */
489
+ ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
490
+ /**
491
+ * Two-way bindable. Selected option values. Single mode (`multiple=false`)
492
+ * keeps 0 or 1 element; multi mode keeps any number. The `model()` change
493
+ * emitter (`(valueChange)`) fires only on internal selection changes,
494
+ * never on consumer writes via `[(value)]`.
495
+ */
496
+ value = model([], /* @ts-ignore */
497
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
498
+ /**
499
+ * Compare two items for equality. Defaults to `===`, which is the
500
+ * correct identity for primitive `T` (e.g. strings, numbers). Override
501
+ * when binding object items so the directive can locate selected /
502
+ * removed entries by id (or any other stable key) instead of by
503
+ * reference: `[isItemEqualToValue]="(a, b) => a.id === b.id"`.
504
+ */
505
+ isItemEqualToValue = input((a, b) => a === b, /* @ts-ignore */
506
+ ...(ngDevMode ? [{ debugName: "isItemEqualToValue" }] : /* istanbul ignore next */ []));
507
+ /**
508
+ * Render an item as a string label. Defaults to `String(item)`, which is
509
+ * identity for strings. Drives the visible input text after activation
510
+ * (when `commitOnSelect`) and the chip label fallback in multi mode.
511
+ * Override when binding object items so the directive can fall back to
512
+ * a meaningful label without relying on the option cache being warm:
513
+ * `[itemToStringLabel]="(it) => it.name"`.
514
+ */
515
+ itemToStringLabel = input((item) => String(item), /* @ts-ignore */
516
+ ...(ngDevMode ? [{ debugName: "itemToStringLabel" }] : /* istanbul ignore next */ []));
517
+ /**
518
+ * Serialize an item for the hidden input that participates in native
519
+ * form submission. Defaults to identity for strings and to
520
+ * `JSON.stringify` for non-string items so the primitive works out of
521
+ * the box round-tripping objects. Override to emit a specific wire
522
+ * format — typically a per-item id — when the backend expects that:
523
+ * `[itemToFormValue]="(it) => it.id"`.
524
+ */
525
+ itemToFormValue = input(defaultItemToFormValue, /* @ts-ignore */
526
+ ...(ngDevMode ? [{ debugName: "itemToFormValue" }] : /* istanbul ignore next */ []));
527
+ /**
528
+ * Two-way bindable. Whether the listbox is currently shown. Internal
529
+ * transitions: input typing (when `openOnQuery`), focus (when
530
+ * `openOnFocus`), ArrowDown / ArrowUp, Escape, outside dismissal,
531
+ * single-mode option activation.
532
+ */
533
+ open = model(false, /* @ts-ignore */
534
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
535
+ multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
536
+ /**
537
+ * Autocomplete mode applied to the input. Mirrors the
538
+ * [WAI-ARIA `aria-autocomplete` property](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/#wai-ariaroles,states,andproperties)
539
+ * and drives whether the listbox auto-opens on query and whether the
540
+ * input gets inline-completed with the first match. Renamed from
541
+ * `autocomplete` so consumers don't conflate it with the native HTML
542
+ * `autocomplete` attribute (which the directive forces to `"off"`).
543
+ */
544
+ autocompleteMode = input('list', /* @ts-ignore */
545
+ ...(ngDevMode ? [{ debugName: "autocompleteMode" }] : /* istanbul ignore next */ []));
546
+ /** Open the listbox when the input gains focus. Off by default — opening on query / arrow keys is the standard ecosystem behavior. */
547
+ openOnFocus = input(false, { ...(ngDevMode ? { debugName: "openOnFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
548
+ /** Open the listbox when the user starts typing. On by default. Only honored when `autocompleteMode` includes a listbox (`'list'` or `'both'`). */
549
+ openOnQuery = input(true, { ...(ngDevMode ? { debugName: "openOnQuery" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
550
+ /**
551
+ * In single mode, copy the activated option's label into `query`. In
552
+ * multi mode, instead **clear** the query so the user can search the
553
+ * next item. On by default in both. Set `false` to leave `query`
554
+ * untouched on activation in either mode.
555
+ *
556
+ * Governs the **editable anatomy** only. In the picker anatomy (a
557
+ * `[forComboboxTrigger]` is registered) the in-panel input is a transient
558
+ * filter, not the value display: the single-mode label copy is always
559
+ * skipped and `query` resets to `''` on close regardless of this flag.
560
+ */
561
+ commitOnSelect = input(true, { ...(ngDevMode ? { debugName: "commitOnSelect" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
562
+ /** When the user edits the query, automatically clear the committed `value`. Off by default — most apps want the value preserved across query edits. Single-mode only. */
563
+ clearOnQueryChange = input(false, { ...(ngDevMode ? { debugName: "clearOnQueryChange" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
564
+ /**
565
+ * Auto-highlight the first enabled option whenever the listbox is open
566
+ * and no activedescendant is set (e.g. after the consumer's filter
567
+ * removed the previously-active option). On by default. Set `false` for
568
+ * "user must arrow before anything is highlighted" behavior.
569
+ */
570
+ autoHighlight = input(true, { ...(ngDevMode ? { debugName: "autoHighlight" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
571
+ /**
572
+ * Writing direction. Drives chip-cluster keyboard navigation (ArrowLeft /
573
+ * ArrowRight semantics swap in RTL so they follow the visual order, not DOM
574
+ * order) and the default `align` of the listbox (anchors to the right edge
575
+ * of the input in RTL). When unset (default `null`), the inherited ambient
576
+ * direction is resolved from the nearest ancestor carrying a `dir` attribute
577
+ * (or `<html dir>`), defaulting to `'ltr'`. An explicit `[dir]` always wins
578
+ * and the resolved value is reflected to the host `dir` attribute.
579
+ */
580
+ _dirInput = input(null, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
581
+ dir = injectTextDirection(this._dirInput);
582
+ /**
583
+ * Side the listbox is anchored to. Defaults to `'bottom'`. Pair with
584
+ * `align` for the full positioning API.
585
+ */
586
+ side = input('bottom', /* @ts-ignore */
587
+ ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
588
+ /**
589
+ * Alignment along the chosen `side`. When unset, defaults to `'start'`
590
+ * in LTR and `'end'` in RTL (per `dir`). Set explicitly to pin an
591
+ * alignment regardless of writing direction.
592
+ *
593
+ * The input is aliased to `align`; consumers bind `[align]="..."` and
594
+ * read the effective value via the public `align` computed below.
595
+ */
596
+ _alignInput = input(undefined, { ...(ngDevMode ? { debugName: "_alignInput" } : /* istanbul ignore next */ {}), alias: 'align' });
597
+ align = computed(() => this._alignInput() ?? (this.dir() === 'rtl' ? 'end' : 'start'), /* @ts-ignore */
598
+ ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
599
+ /** Gap (px) between input and listbox along the main axis. Default `4`. */
600
+ sideOffset = input(this.#defaults.sideOffset, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
601
+ /** Gap (px) along the cross axis. Default `0`. */
602
+ alignOffset = input(0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
603
+ /** When `true` (default), `flip` and `shift` keep the listbox inside the viewport. */
604
+ avoidCollisions = input(true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
605
+ /** Padding (px) applied uniformly to flip / shift / size. Default `8`. */
606
+ collisionPadding = input(this.#defaults.collisionPadding, { ...(ngDevMode ? { debugName: "collisionPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
607
+ /** Padding (px) for the `arrow` middleware. Default `0`. */
608
+ arrowPadding = input(0, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
609
+ /** Stickiness behaviour for `shift`. Default `'partial'`. */
610
+ sticky = input('partial', /* @ts-ignore */
611
+ ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
612
+ /** When `true`, sets `data-detached=""` while the input is scrolled off-screen. */
613
+ hideWhenDetached = input(false, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
614
+ /**
615
+ * When `true` (default), the content is clipped until floating-ui resolves
616
+ * its first position, preventing a flash at the viewport corner. Set to
617
+ * `false` so a dramatic `animate.enter` plays from its first frame (the
618
+ * surface may flash briefly at the unresolved position while positioning
619
+ * computes).
620
+ */
621
+ clipUntilPositioned = input(true, { ...(ngDevMode ? { debugName: "clipUntilPositioned" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
622
+ loop = input(true, { ...(ngDevMode ? { debugName: "loop" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
623
+ /** When true (default), Escape, pointer-down outside, and focus outside close the listbox. */
624
+ dismissible = input(true, { ...(ngDevMode ? { debugName: "dismissible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
625
+ /**
626
+ * When true (default), focus returns to the `[forComboboxTrigger]` on close.
627
+ * Only relevant in the picker anatomy (a trigger is registered) — in the
628
+ * editable anatomy focus never leaves the input, so there is nothing to
629
+ * return.
630
+ */
631
+ returnFocus = input(true, { ...(ngDevMode ? { debugName: "returnFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
632
+ /** Manual `aria-label` on the listbox (`[forComboboxList]`, or `[forComboboxContent]` in the editable anatomy) when the input isn't a meaningful name. */
633
+ ariaLabel = input(null, /* @ts-ignore */
634
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
635
+ /**
636
+ * Total number of options in the consumer's source array. Set when wiring
637
+ * up a virtualized listbox (only the visible window is rendered) so the
638
+ * directive can reflect `aria-setsize` and walk the snapshot for
639
+ * navigation past the rendered range. Defaults to `undefined`, in which
640
+ * case the directive falls back to `options().length` (the live registry).
641
+ */
642
+ totalCount = input(undefined, { ...(ngDevMode ? { debugName: "totalCount" } : /* istanbul ignore next */ {}), transform: (v) => (v == null ? undefined : numberAttribute(v)) });
643
+ /**
644
+ * Inclusive-exclusive `[start, end)` range of options currently rendered
645
+ * in the DOM. Used by `navigate()` to translate "move to absolute
646
+ * position N" into either an in-window highlight update or a request to
647
+ * the consumer to scroll N into view (`(scrollToIndex)`). When
648
+ * `undefined` (default), navigation assumes every option in the snapshot
649
+ * is rendered — appropriate for non-virtualized lists.
650
+ */
651
+ visibleRange = input(undefined, /* @ts-ignore */
652
+ ...(ngDevMode ? [{ debugName: "visibleRange" }] : /* istanbul ignore next */ []));
653
+ /**
654
+ * Emitted when keyboard navigation needs to land on an option whose
655
+ * absolute index falls outside `visibleRange()`. Wire this to the
656
+ * consumer's virtualizer (`scrollToIndex(idx)` on `@tanstack/virtual`,
657
+ * `virtua`, etc.); once the option mounts, the directive seeds
658
+ * `aria-activedescendant` automatically.
659
+ */
660
+ scrollToIndex = output();
661
+ escapeKeyDown = output();
662
+ pointerDownOutside = output();
663
+ focusOutside = output();
664
+ interactOutside = output();
665
+ /**
666
+ * _(picker anatomy only)_ Fires just before focus moves into the input on
667
+ * open. Call `preventDefault()` on the emitted veto to skip the imperative
668
+ * focus move. Only emitted when a `[forComboboxTrigger]` is registered — the
669
+ * editable anatomy keeps focus in the input the whole time and has no move to
670
+ * veto.
671
+ */
672
+ autoFocusOnOpen = output();
673
+ /**
674
+ * _(picker anatomy only)_ Fires just before focus returns to the trigger on
675
+ * close. Call `preventDefault()` on the veto to suppress the return-focus.
676
+ */
677
+ autoFocusOnClose = output();
678
+ inputId = signal(this.#idGen.next('for-combobox-input'), /* @ts-ignore */
679
+ ...(ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []));
680
+ contentId = signal(this.#idGen.next('for-combobox-content'), /* @ts-ignore */
681
+ ...(ngDevMode ? [{ debugName: "contentId" }] : /* istanbul ignore next */ []));
682
+ listId = signal(this.#idGen.next('for-combobox-list'), /* @ts-ignore */
683
+ ...(ngDevMode ? [{ debugName: "listId" }] : /* istanbul ignore next */ []));
684
+ #inputEl = signal(null, /* @ts-ignore */
685
+ ...(ngDevMode ? [{ debugName: "#inputEl" }] : /* istanbul ignore next */ []));
686
+ input = this.#inputEl.asReadonly();
687
+ #anchorEl = signal(null, /* @ts-ignore */
688
+ ...(ngDevMode ? [{ debugName: "#anchorEl" }] : /* istanbul ignore next */ []));
689
+ #triggerEl = signal(null, /* @ts-ignore */
690
+ ...(ngDevMode ? [{ debugName: "#triggerEl" }] : /* istanbul ignore next */ []));
691
+ trigger = this.#triggerEl.asReadonly();
692
+ /**
693
+ * Element floating-ui anchors the listbox against. Resolution order:
694
+ * explicit `[forComboboxAnchor]` → `[forComboboxTrigger]` (picker anatomy) →
695
+ * the input (editable anatomy fallback, so existing comboboxes keep their
696
+ * behavior). Decoupled from `input` so the input keeps driving
697
+ * `aria-controls`, `aria-activedescendant`, keyboard interaction, and its
698
+ * dismissal exemption regardless of where the listbox paints.
699
+ */
700
+ anchor = computed(() => this.#anchorEl() ?? this.#triggerEl() ?? this.#inputEl(), /* @ts-ignore */
701
+ ...(ngDevMode ? [{ debugName: "anchor" }] : /* istanbul ignore next */ []));
702
+ #contentEl = signal(null, /* @ts-ignore */
703
+ ...(ngDevMode ? [{ debugName: "#contentEl" }] : /* istanbul ignore next */ []));
704
+ content = this.#contentEl.asReadonly();
705
+ #listEl = signal(null, /* @ts-ignore */
706
+ ...(ngDevMode ? [{ debugName: "#listEl" }] : /* istanbul ignore next */ []));
707
+ list = this.#listEl.asReadonly();
708
+ /** True once a `[forComboboxList]` has registered (picker anatomy). */
709
+ hasList = computed(() => this.#listEl() !== null, /* @ts-ignore */
710
+ ...(ngDevMode ? [{ debugName: "hasList" }] : /* istanbul ignore next */ []));
711
+ /**
712
+ * Id of the element carrying `role="listbox"`: the list when one is
713
+ * registered (picker anatomy), otherwise the content surface (editable
714
+ * anatomy). The input targets this with `aria-controls`.
715
+ */
716
+ listboxId = computed(() => (this.hasList() ? this.listId() : this.contentId()), /* @ts-ignore */
717
+ ...(ngDevMode ? [{ debugName: "listboxId" }] : /* istanbul ignore next */ []));
718
+ options = this.#items.items;
719
+ chips = this.#chips.items;
720
+ #initialFocus = signal('first', /* @ts-ignore */
721
+ ...(ngDevMode ? [{ debugName: "#initialFocus" }] : /* istanbul ignore next */ []));
722
+ initialFocus = this.#initialFocus.asReadonly();
723
+ #lastCloseReason = signal(null, /* @ts-ignore */
724
+ ...(ngDevMode ? [{ debugName: "#lastCloseReason" }] : /* istanbul ignore next */ []));
725
+ lastCloseReason = this.#lastCloseReason.asReadonly();
726
+ /**
727
+ * The activedescendant pointer. Built by {@link createActiveIdSignal} as a
728
+ * `linkedSignal` so the highlight decision is a pure derivation, never a write
729
+ * from an `effect`; it stays writable for the imperative moves that own their
730
+ * own scroll (arrow / Home / End navigation, hover, multi-mode activation, and
731
+ * the virtualized pending-nav resolution). The reset/seed rule lives with the
732
+ * factory in `combobox-auto-highlight.ts`.
733
+ */
734
+ #activeId = createActiveIdSignal({
735
+ query: this.query,
736
+ open: this.open,
737
+ autoHighlight: this.autoHighlight,
738
+ virtualized: () => this.totalCount() !== undefined,
739
+ initialFocus: this.#initialFocus,
740
+ items: this.#items.items,
741
+ value: this.value,
742
+ equals: this.isItemEqualToValue,
743
+ });
744
+ activeId = this.#activeId.asReadonly();
745
+ #lastPositionedId = null;
746
+ #pointerSuppression = createPointerSuppression();
747
+ /**
748
+ * Always-on label cache — keeps `{ id, value, label }` tuples across
749
+ * close/open and scroll-out-of-view to drive inline autocomplete matching
750
+ * and the `selected` label fallback. See `combobox-label-cache.ts`.
751
+ */
752
+ #labelCache = new OptionLabelCache({
753
+ items: this.#items.items,
754
+ totalCount: this.totalCount,
755
+ });
756
+ /**
757
+ * Virtualization navigation engine — constructed lazily the first time the
758
+ * consumer sets `totalCount()`, so a non-virtualized combobox never pulls
759
+ * the position-map machinery. Powers keyboard navigation past the rendered
760
+ * window and the scrolled-out-of-view label fallback.
761
+ */
762
+ #navigator = null;
763
+ #requireNavigator() {
764
+ return (this.#navigator ??= new VirtualizedNavigator({
765
+ items: this.#items.items,
766
+ totalCount: this.totalCount,
767
+ visibleRange: this.visibleRange,
768
+ loop: this.loop,
769
+ getActiveId: () => this.#activeId(),
770
+ setActiveId: (id) => this.#activeId.set(id),
771
+ emitScrollToIndex: (idx) => this.scrollToIndex.emit(idx),
772
+ scrollActiveIntoView: (host) => this.#scrollActiveIntoView(host),
773
+ }));
774
+ }
775
+ selected = computed(() => {
776
+ const values = this.value();
777
+ if (values.length === 0) {
778
+ return [];
779
+ }
780
+ // `cachedOptions()` already merges off-window entries from the indexed
781
+ // snapshot when virtualizing, so a selected option scrolled out of view
782
+ // still resolves here without a separate position-map lookup.
783
+ const cached = this.cachedOptions();
784
+ const equals = this.isItemEqualToValue();
785
+ const toLabel = this.itemToStringLabel();
786
+ return values.map((v) => {
787
+ const opt = cached.find((o) => equals(o.value, v));
788
+ if (opt) {
789
+ return { value: v, label: opt.label };
790
+ }
791
+ // Fall back to the consumer-provided label fn so non-string items
792
+ // still render a meaningful string before the option cache warms up.
793
+ return { value: v, label: typeof v === 'string' ? v : toLabel(v) };
794
+ });
795
+ }, /* @ts-ignore */
796
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
797
+ /**
798
+ * Read-only single-select convenience view of {@link value}. Returns the
799
+ * sole selected item when exactly one is selected, otherwise `null` (empty
800
+ * selection, or multiple selections in `multiple` mode). Lets single-select
801
+ * consumers read `selectedItem()` instead of unwrapping `value()[0]`. The
802
+ * array-backed `value` model remains the source of truth and the
803
+ * `FormValueControl` contract; this is a derived accessor. Distinct from
804
+ * {@link selected}, which pairs every selected value with its resolved
805
+ * label for chip rendering.
806
+ */
807
+ selectedItem = singleSelected(this.value);
808
+ fieldLabelledElement() {
809
+ return this.input();
810
+ }
811
+ fieldLabelledElementId() {
812
+ return this.inputId();
813
+ }
814
+ constructor() {
815
+ super();
816
+ injectHiddenInput({
817
+ name: this.name,
818
+ values: this.value,
819
+ serialize: (item) => this.itemToFormValue()(item),
820
+ disabled: this.effectiveDisabled,
821
+ });
822
+ // The activedescendant *decision* is a pure derivation in `#activeId`; this
823
+ // effect runs only its imperative tail (label-cache priming, virtualized
824
+ // pending-nav resolution + passive seed, non-virtualized scroll-into-view).
825
+ // Full rationale lives with `runAutoHighlightBridge` in
826
+ // `combobox-auto-highlight.ts`.
827
+ effect(() => {
828
+ runAutoHighlightBridge({
829
+ labelCache: this.#labelCache,
830
+ requireNavigator: () => this.#requireNavigator(),
831
+ items: this.#items.items,
832
+ open: this.open,
833
+ autoHighlight: this.autoHighlight,
834
+ virtualized: () => this.totalCount() !== undefined,
835
+ initialFocus: this.#initialFocus,
836
+ getActiveId: () => this.#activeId(),
837
+ getLastPositionedId: () => this.#lastPositionedId,
838
+ setLastPositionedId: (id) => (this.#lastPositionedId = id),
839
+ });
840
+ });
841
+ }
842
+ registerInput(el) {
843
+ adoptHostId(el, this.inputId);
844
+ this.#inputEl.set(el);
845
+ }
846
+ unregisterInput(el) {
847
+ if (this.#inputEl() === el) {
848
+ this.#inputEl.set(null);
849
+ }
850
+ }
851
+ registerAnchor(el) {
852
+ const current = this.#anchorEl();
853
+ if (current !== null && current !== el) {
854
+ throw new Error('[forty-cdk/combobox] Multiple [forComboboxAnchor] inside the same [forCombobox]; only one is allowed.');
855
+ }
856
+ this.#anchorEl.set(el);
857
+ }
858
+ unregisterAnchor(el) {
859
+ if (this.#anchorEl() === el) {
860
+ this.#anchorEl.set(null);
861
+ }
862
+ }
863
+ registerTrigger(el) {
864
+ this.#triggerEl.set(el);
865
+ }
866
+ unregisterTrigger(el) {
867
+ if (this.#triggerEl() === el) {
868
+ this.#triggerEl.set(null);
869
+ }
870
+ }
871
+ registerContent(el) {
872
+ adoptHostId(el, this.contentId);
873
+ this.#contentEl.set(el);
874
+ }
875
+ unregisterContent(el) {
876
+ if (this.#contentEl() === el) {
877
+ this.#contentEl.set(null);
878
+ }
879
+ }
880
+ registerList(el) {
881
+ adoptHostId(el, this.listId);
882
+ this.#listEl.set(el);
883
+ }
884
+ unregisterList(el) {
885
+ if (this.#listEl() === el) {
886
+ this.#listEl.set(null);
887
+ }
888
+ }
889
+ registerOption(handle) {
890
+ this.#items.register(handle);
891
+ }
892
+ unregisterOption(handle) {
893
+ this.#items.unregister(handle);
894
+ if (this.#activeId() === handle.id()) {
895
+ this.#activeId.set(null);
896
+ }
897
+ }
898
+ registerChip(handle) {
899
+ this.#chips.register(handle);
900
+ }
901
+ unregisterChip(handle) {
902
+ this.#chips.unregister(handle);
903
+ }
904
+ isSelected(v) {
905
+ return isInArray(this.value(), v, this.isItemEqualToValue());
906
+ }
907
+ isActive(id) {
908
+ return this.#activeId() === id;
909
+ }
910
+ isPointerSuppressed() {
911
+ return this.#pointerSuppression.isSuppressed();
912
+ }
913
+ activate(handle) {
914
+ if (this.effectiveDisabled() || this.readonly() || handle.disabled()) {
915
+ return;
916
+ }
917
+ const v = handle.value();
918
+ if (this.multiple()) {
919
+ // Toggle in/out of the array. Stay open so the user can keep picking.
920
+ this.value.set(toggleInArray(this.value(), v, this.isItemEqualToValue()));
921
+ if (this.commitOnSelect()) {
922
+ // Reset query so the next typed prefix searches afresh.
923
+ this.query.set('');
924
+ this.#syncInputValue('');
925
+ }
926
+ this.#activeId.set(handle.id());
927
+ return;
928
+ }
929
+ // Single mode: replace + close + commit label.
930
+ this.value.set([v]);
931
+ if (this.commitOnSelect() && this.trigger() === null) {
932
+ this.query.set(handle.label());
933
+ this.#syncInputValue(handle.label());
934
+ }
935
+ this.closeMenu('select');
936
+ }
937
+ /**
938
+ * Imperatively align the visible `<input>` text with `query()` even
939
+ * while the input has focus. The directive's reactive sync skips
940
+ * focused syncs to avoid clobbering inline-autocomplete selection
941
+ * during typing — but activation / clear writes happen *outside* the
942
+ * typing flow and should be visible immediately.
943
+ */
944
+ #syncInputValue(value) {
945
+ const el = this.#inputEl();
946
+ if (el && el.value !== value) {
947
+ el.value = value;
948
+ }
949
+ }
950
+ removeValue(v) {
951
+ if (this.effectiveDisabled() || this.readonly()) {
952
+ return;
953
+ }
954
+ const equals = this.isItemEqualToValue();
955
+ const current = this.value();
956
+ if (!current.some((x) => equals(x, v))) {
957
+ return;
958
+ }
959
+ this.value.set(current.filter((x) => !equals(x, v)));
960
+ }
961
+ activateActive() {
962
+ const id = this.#activeId();
963
+ if (!id) {
964
+ return false;
965
+ }
966
+ const handle = this.#items.items().find((o) => o.id() === id);
967
+ if (!handle || handle.disabled()) {
968
+ return false;
969
+ }
970
+ this.activate(handle);
971
+ return true;
972
+ }
973
+ navigate(direction) {
974
+ if (this.effectiveDisabled()) {
975
+ return;
976
+ }
977
+ if (this.totalCount() !== undefined) {
978
+ this.#requireNavigator().navigate(direction);
979
+ return;
980
+ }
981
+ const items = this.#items.items();
982
+ if (items.length === 0) {
983
+ return;
984
+ }
985
+ const currentId = this.#activeId();
986
+ const currentIndex = currentId === null ? -1 : items.findIndex((o) => o.id() === currentId);
987
+ // No activedescendant yet + arrow nav: jump to the natural extreme
988
+ // (ArrowDown → first enabled, ArrowUp → last enabled).
989
+ let action = direction;
990
+ if (currentIndex < 0 && direction === 'next') {
991
+ action = 'first';
992
+ }
993
+ else if (currentIndex < 0 && direction === 'prev') {
994
+ action = 'last';
995
+ }
996
+ const target = nextEnabledHandle(items, currentIndex, action, { loop: this.loop() });
997
+ if (target === null) {
998
+ return;
999
+ }
1000
+ this.#activeId.set(target.id());
1001
+ this.#scrollActiveIntoView(target.host);
1002
+ this.#lastPositionedId = target.id();
1003
+ }
1004
+ setQueryFromInput(query) {
1005
+ if (this.effectiveDisabled() || this.readonly()) {
1006
+ return;
1007
+ }
1008
+ this.query.set(query);
1009
+ if (this.clearOnQueryChange() && !this.multiple() && this.value().length > 0) {
1010
+ this.value.set([]);
1011
+ }
1012
+ const mode = this.autocompleteMode();
1013
+ const hasListbox = mode === 'list' || mode === 'both';
1014
+ if (this.openOnQuery() && hasListbox && query.length > 0 && !this.open()) {
1015
+ this.openMenu();
1016
+ }
1017
+ }
1018
+ setActiveId(id) {
1019
+ this.#activeId.set(id);
1020
+ this.#lastPositionedId = id;
1021
+ }
1022
+ #scrollActiveIntoView(host) {
1023
+ this.#pointerSuppression.suppress();
1024
+ host.scrollIntoView?.({ block: 'nearest' });
1025
+ }
1026
+ #cachedOptionsMemo = computed(() => {
1027
+ if (this.totalCount() === undefined) {
1028
+ return this.#labelCache.entries();
1029
+ }
1030
+ return this.#labelCache.mergedEntries(this.#requireNavigator().snapshotByPos());
1031
+ }, /* @ts-ignore */
1032
+ ...(ngDevMode ? [{ debugName: "#cachedOptionsMemo" }] : /* istanbul ignore next */ []));
1033
+ cachedOptions() {
1034
+ return this.#cachedOptionsMemo();
1035
+ }
1036
+ clear(clearQuery = true) {
1037
+ if (this.effectiveDisabled() || this.readonly()) {
1038
+ return;
1039
+ }
1040
+ this.value.set([]);
1041
+ if (clearQuery) {
1042
+ this.query.set('');
1043
+ this.#syncInputValue('');
1044
+ }
1045
+ this.#activeId.set(null);
1046
+ }
1047
+ setInitialFocus(target) {
1048
+ this.#initialFocus.set(target);
1049
+ }
1050
+ toggle() {
1051
+ if (this.effectiveDisabled()) {
1052
+ return;
1053
+ }
1054
+ if (this.open()) {
1055
+ this.closeMenu('programmatic');
1056
+ }
1057
+ else {
1058
+ this.openMenu(this.trigger() !== null ? 'selected' : 'first');
1059
+ }
1060
+ }
1061
+ openMenu(initialFocus = 'first') {
1062
+ if (this.effectiveDisabled() || this.open()) {
1063
+ return;
1064
+ }
1065
+ this.#initialFocus.set(initialFocus);
1066
+ this.#lastCloseReason.set(null);
1067
+ this.open.set(true);
1068
+ }
1069
+ closeMenu(reason) {
1070
+ if (!this.open()) {
1071
+ return;
1072
+ }
1073
+ this.#lastCloseReason.set(reason);
1074
+ this.open.set(false);
1075
+ this.#activeId.set(null);
1076
+ this.#navigator?.resetPending();
1077
+ if (this.trigger() !== null) {
1078
+ this.query.set('');
1079
+ this.#syncInputValue('');
1080
+ }
1081
+ }
1082
+ /** Fire `(autoFocusOnOpen)` and report whether the consumer vetoed the focus move. */
1083
+ emitAutoFocusOnOpen() {
1084
+ return emitVetoableEvent(this.autoFocusOnOpen);
1085
+ }
1086
+ /** Fire `(autoFocusOnClose)` and report whether the consumer vetoed the return-focus. */
1087
+ emitAutoFocusOnClose() {
1088
+ return emitVetoableEvent(this.autoFocusOnClose);
1089
+ }
1090
+ emitEscapeKeyDown(event) {
1091
+ const vetoed = emitVetoableNativeEvent(this.escapeKeyDown, event);
1092
+ if (!vetoed && this.dismissible()) {
1093
+ event.stopPropagation();
1094
+ this.closeMenu('escape');
1095
+ }
1096
+ }
1097
+ /**
1098
+ * Outside-interaction emit forwarders. The shared `#pendingOutsideVeto`
1099
+ * reuse between the specific outside channels and the composite
1100
+ * `interactOutside` lives in `injectOverlayShell`; these only fire the
1101
+ * matching output with the veto the shell built.
1102
+ */
1103
+ emitPointerDownOutside(veto) {
1104
+ this.pointerDownOutside.emit(veto);
1105
+ }
1106
+ emitFocusOutside(veto) {
1107
+ this.focusOutside.emit(veto);
1108
+ }
1109
+ emitInteractOutside(veto) {
1110
+ this.interactOutside.emit(veto);
1111
+ }
1112
+ /**
1113
+ * Implicit close requested by the shell after an un-vetoed outside
1114
+ * interaction. Marks the control touched and closes with the channel's
1115
+ * reason.
1116
+ */
1117
+ requestClose(reason) {
1118
+ this.markTouched();
1119
+ this.closeMenu(reason);
1120
+ }
1121
+ markTouched() {
1122
+ super.markTouched();
1123
+ }
1124
+ onFocusOut(event) {
1125
+ const next = event.relatedTarget;
1126
+ const inputEl = this.#inputEl();
1127
+ const content = this.#contentEl();
1128
+ if (next) {
1129
+ if (inputEl && inputEl.contains(next)) {
1130
+ return;
1131
+ }
1132
+ if (content && content.contains(next)) {
1133
+ return;
1134
+ }
1135
+ // Focus moving to a chip (multi mode) is "inside" — chips are an
1136
+ // extension of the input area, not an outside boundary.
1137
+ const chips = this.#chips.items();
1138
+ for (const chip of chips) {
1139
+ if (chip.host.contains(next)) {
1140
+ return;
1141
+ }
1142
+ }
1143
+ }
1144
+ this.markTouched();
1145
+ }
1146
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCombobox, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1147
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForCombobox, isStandalone: true, selector: "[forCombobox]", inputs: { query: { classPropertyName: "query", publicName: "query", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToStringLabel: { classPropertyName: "itemToStringLabel", publicName: "itemToStringLabel", isSignal: true, isRequired: false, transformFunction: null }, itemToFormValue: { classPropertyName: "itemToFormValue", publicName: "itemToFormValue", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, autocompleteMode: { classPropertyName: "autocompleteMode", publicName: "autocompleteMode", isSignal: true, isRequired: false, transformFunction: null }, openOnFocus: { classPropertyName: "openOnFocus", publicName: "openOnFocus", isSignal: true, isRequired: false, transformFunction: null }, openOnQuery: { classPropertyName: "openOnQuery", publicName: "openOnQuery", isSignal: true, isRequired: false, transformFunction: null }, commitOnSelect: { classPropertyName: "commitOnSelect", publicName: "commitOnSelect", isSignal: true, isRequired: false, transformFunction: null }, clearOnQueryChange: { classPropertyName: "clearOnQueryChange", publicName: "clearOnQueryChange", isSignal: true, isRequired: false, transformFunction: null }, autoHighlight: { classPropertyName: "autoHighlight", publicName: "autoHighlight", isSignal: true, isRequired: false, transformFunction: null }, _dirInput: { classPropertyName: "_dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, _alignInput: { classPropertyName: "_alignInput", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, sideOffset: { classPropertyName: "sideOffset", publicName: "sideOffset", isSignal: true, isRequired: false, transformFunction: null }, alignOffset: { classPropertyName: "alignOffset", publicName: "alignOffset", isSignal: true, isRequired: false, transformFunction: null }, avoidCollisions: { classPropertyName: "avoidCollisions", publicName: "avoidCollisions", isSignal: true, isRequired: false, transformFunction: null }, collisionPadding: { classPropertyName: "collisionPadding", publicName: "collisionPadding", isSignal: true, isRequired: false, transformFunction: null }, arrowPadding: { classPropertyName: "arrowPadding", publicName: "arrowPadding", isSignal: true, isRequired: false, transformFunction: null }, sticky: { classPropertyName: "sticky", publicName: "sticky", isSignal: true, isRequired: false, transformFunction: null }, hideWhenDetached: { classPropertyName: "hideWhenDetached", publicName: "hideWhenDetached", isSignal: true, isRequired: false, transformFunction: null }, clipUntilPositioned: { classPropertyName: "clipUntilPositioned", publicName: "clipUntilPositioned", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, returnFocus: { classPropertyName: "returnFocus", publicName: "returnFocus", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", 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 } }, outputs: { query: "queryChange", value: "valueChange", open: "openChange", scrollToIndex: "scrollToIndex", escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside", focusOutside: "focusOutside", interactOutside: "interactOutside", autoFocusOnOpen: "autoFocusOnOpen", autoFocusOnClose: "autoFocusOnClose" }, host: { listeners: { "focusout": "onFocusOut($event)" }, properties: { "attr.data-state": "open() ? \"open\" : \"closed\"", "attr.data-disabled": "effectiveDisabled() ? \"\" : null", "attr.dir": "dir()" } }, providers: [{ provide: FOR_COMBOBOX_CONTEXT, useExisting: ForCombobox }], exportAs: ["forCombobox"], usesInheritance: true, ngImport: i0 });
1148
+ }
1149
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCombobox, decorators: [{
1150
+ type: Directive,
1151
+ args: [{
1152
+ selector: '[forCombobox]',
1153
+ exportAs: 'forCombobox',
1154
+ host: {
1155
+ '[attr.data-state]': 'open() ? "open" : "closed"',
1156
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
1157
+ '[attr.dir]': 'dir()',
1158
+ '(focusout)': 'onFocusOut($event)',
1159
+ },
1160
+ providers: [{ provide: FOR_COMBOBOX_CONTEXT, useExisting: ForCombobox }],
1161
+ }]
1162
+ }], ctorParameters: () => [], propDecorators: { query: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }, { type: i0.Output, args: ["queryChange"] }], 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 }] }], itemToStringLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringLabel", required: false }] }], itemToFormValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToFormValue", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], autocompleteMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocompleteMode", required: false }] }], openOnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnFocus", required: false }] }], openOnQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnQuery", required: false }] }], commitOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitOnSelect", required: false }] }], clearOnQueryChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearOnQueryChange", required: false }] }], autoHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoHighlight", required: false }] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], _alignInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], sideOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideOffset", required: false }] }], alignOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignOffset", required: false }] }], avoidCollisions: [{ type: i0.Input, args: [{ isSignal: true, alias: "avoidCollisions", required: false }] }], collisionPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "collisionPadding", required: false }] }], arrowPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrowPadding", required: false }] }], sticky: [{ type: i0.Input, args: [{ isSignal: true, alias: "sticky", required: false }] }], hideWhenDetached: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideWhenDetached", required: false }] }], clipUntilPositioned: [{ type: i0.Input, args: [{ isSignal: true, alias: "clipUntilPositioned", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], returnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "returnFocus", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", 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"] }], escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }], pointerDownOutside: [{ type: i0.Output, args: ["pointerDownOutside"] }], focusOutside: [{ type: i0.Output, args: ["focusOutside"] }], interactOutside: [{ type: i0.Output, args: ["interactOutside"] }], autoFocusOnOpen: [{ type: i0.Output, args: ["autoFocusOnOpen"] }], autoFocusOnClose: [{ type: i0.Output, args: ["autoFocusOnClose"] }] } });
1163
+
1164
+ /**
1165
+ * Optional positioning anchor. When present, `[forComboboxContent]` is
1166
+ * positioned against this element instead of `[forComboboxInput]` — useful
1167
+ * when the input lives inside a decorated field box (padding, prefix icon,
1168
+ * clear button, chip cluster) and the listbox should match the visible field
1169
+ * rather than the inner `<input>`.
1170
+ *
1171
+ * Only positioning changes: the input still owns `aria-controls`,
1172
+ * `aria-expanded`, `aria-activedescendant`, keyboard interaction, and its
1173
+ * exemption from outside-pointer dismissal. If no anchor is registered the
1174
+ * listbox falls back to anchoring against the input, so existing usages are
1175
+ * unaffected.
1176
+ *
1177
+ * At most one `[forComboboxAnchor]` may be registered per `[forCombobox]`; a
1178
+ * second one throws.
1179
+ *
1180
+ * ```html
1181
+ * <div forCombobox [(value)]="v" [(query)]="q">
1182
+ * <div forComboboxAnchor class="field-box">
1183
+ * <icon />
1184
+ * <input forComboboxInput />
1185
+ * <button class="clear">×</button>
1186
+ * </div>
1187
+ * @if (open()) {
1188
+ * <div forComboboxContent>…</div>
1189
+ * }
1190
+ * </div>
1191
+ * ```
1192
+ */
1193
+ class ForComboboxAnchor {
1194
+ #ctx = injectComboboxContext('ForComboboxAnchor');
1195
+ #host = inject(ElementRef);
1196
+ constructor() {
1197
+ registerHandle(this.#host.nativeElement, (el) => this.#ctx.registerAnchor(el), (el) => this.#ctx.unregisterAnchor(el));
1198
+ }
1199
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxAnchor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1200
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxAnchor, isStandalone: true, selector: "[forComboboxAnchor]", exportAs: ["forComboboxAnchor"], ngImport: i0 });
1201
+ }
1202
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxAnchor, decorators: [{
1203
+ type: Directive,
1204
+ args: [{
1205
+ selector: '[forComboboxAnchor]',
1206
+ exportAs: 'forComboboxAnchor',
1207
+ }]
1208
+ }], ctorParameters: () => [] });
1209
+
1210
+ /**
1211
+ * Button that opens the listbox and keeps showing the committed selection
1212
+ * (label + icon) while the search input lives **inside** the panel — the
1213
+ * "combobox with trigger" / picker anatomy. Apply on
1214
+ * a real `<button>` so Space / Enter dispatch native click events that toggle
1215
+ * via `(click)`.
1216
+ *
1217
+ * Registering a trigger switches the combobox into the picker anatomy: it
1218
+ * becomes the default positioning anchor (after an explicit
1219
+ * `[forComboboxAnchor]`), focus moves into `[forComboboxInput]` on open and
1220
+ * returns to the trigger on close (the standard `(autoFocusOnOpen)` /
1221
+ * `(autoFocusOnClose)` vetoable hooks now fire), and `[forComboboxContent]`
1222
+ * expects an inner `[forComboboxList]` to carry the listbox role.
1223
+ *
1224
+ * Wires `aria-haspopup="listbox"`, `aria-expanded`, and `aria-controls`
1225
+ * (pointing to the popup surface while open). Native `disabled` reflects
1226
+ * `effectiveDisabled` (single-purpose trigger). The trigger is exempt from the
1227
+ * popup's dismissable layer — clicks on it route through `(click)` instead of
1228
+ * racing an outside-pointer dismissal.
1229
+ *
1230
+ * The root is normally resolved via DI from the enclosing `[forCombobox]`.
1231
+ * When the trigger is declared inside an `ng-template` stamped into the root
1232
+ * (e.g. via `ngTemplateOutlet`), DI resolves at the template's declaration
1233
+ * site and misses the root — pass it explicitly through the selector input,
1234
+ * `routerLink`-style: `[forComboboxTrigger]="root"` with `#root="forCombobox"`.
1235
+ *
1236
+ * Keyboard:
1237
+ * - **Click / Enter / Space** — toggle (open moves focus into the input).
1238
+ * - **ArrowDown** — open with the first enabled option highlighted.
1239
+ * - **ArrowUp** — open with the last enabled option highlighted.
1240
+ */
1241
+ class ForComboboxTrigger {
1242
+ #host = inject(ElementRef);
1243
+ /**
1244
+ * Optional explicit reference to the `[forCombobox]` root, named after the
1245
+ * selector `routerLink`-style. The bare valueless attribute keeps resolving
1246
+ * the enclosing root via DI; pass the root explicitly
1247
+ * (`[forComboboxTrigger]="root"`, with `#root="forCombobox"`) when the
1248
+ * trigger is declared in an `ng-template` stamped inside the root — DI
1249
+ * resolves at the template's declaration site, so the enclosing root is
1250
+ * invisible there. The empty string (what the valueless attribute yields) is
1251
+ * treated as unset.
1252
+ */
1253
+ forComboboxTrigger = input('', /* @ts-ignore */
1254
+ ...(ngDevMode ? [{ debugName: "forComboboxTrigger" }] : /* istanbul ignore next */ []));
1255
+ ctx = injectComboboxTriggerContext(this.forComboboxTrigger);
1256
+ constructor() {
1257
+ const el = this.#host.nativeElement;
1258
+ // Registration is an imperative call into the resolved root's registry,
1259
+ // not state derivation — the effect only re-registers the element when the
1260
+ // resolved root changes (explicit reference swapped at runtime).
1261
+ effect((onCleanup) => {
1262
+ const ctx = this.ctx();
1263
+ ctx.registerTrigger(el);
1264
+ onCleanup(() => ctx.unregisterTrigger(el));
1265
+ });
1266
+ reflectDisabled(computed(() => this.ctx().effectiveDisabled()));
1267
+ }
1268
+ onClick() {
1269
+ this.ctx().toggle();
1270
+ }
1271
+ onKeyDown(event) {
1272
+ if (this.ctx().effectiveDisabled()) {
1273
+ return;
1274
+ }
1275
+ if (event.key === 'ArrowDown') {
1276
+ event.preventDefault();
1277
+ this.ctx().openMenu('first');
1278
+ return;
1279
+ }
1280
+ if (event.key === 'ArrowUp') {
1281
+ event.preventDefault();
1282
+ this.ctx().openMenu('last');
1283
+ }
1284
+ }
1285
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1286
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForComboboxTrigger, isStandalone: true, selector: "[forComboboxTrigger]", inputs: { forComboboxTrigger: { classPropertyName: "forComboboxTrigger", publicName: "forComboboxTrigger", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button" }, listeners: { "click": "onClick()", "keydown": "onKeyDown($event)" }, properties: { "attr.aria-haspopup": "\"listbox\"", "attr.aria-expanded": "ctx().open() ? \"true\" : \"false\"", "attr.aria-controls": "ctx().open() ? ctx().contentId() : null", "attr.data-state": "ctx().open() ? \"open\" : \"closed\"", "attr.data-disabled": "ctx().effectiveDisabled() ? \"\" : null" } }, exportAs: ["forComboboxTrigger"], ngImport: i0 });
1287
+ }
1288
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxTrigger, decorators: [{
1289
+ type: Directive,
1290
+ args: [{
1291
+ selector: '[forComboboxTrigger]',
1292
+ exportAs: 'forComboboxTrigger',
1293
+ host: {
1294
+ type: 'button',
1295
+ '[attr.aria-haspopup]': '"listbox"',
1296
+ '[attr.aria-expanded]': 'ctx().open() ? "true" : "false"',
1297
+ '[attr.aria-controls]': 'ctx().open() ? ctx().contentId() : null',
1298
+ '[attr.data-state]': 'ctx().open() ? "open" : "closed"',
1299
+ '[attr.data-disabled]': 'ctx().effectiveDisabled() ? "" : null',
1300
+ '(click)': 'onClick()',
1301
+ '(keydown)': 'onKeyDown($event)',
1302
+ },
1303
+ }]
1304
+ }], ctorParameters: () => [], propDecorators: { forComboboxTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "forComboboxTrigger", required: false }] }] } });
1305
+
1306
+ /**
1307
+ * The combobox `<input role="combobox">`. Owns the visible text and all
1308
+ * keyboard interaction. Apply on a real `<input>` so the browser's native
1309
+ * caret / selection semantics drive inline-autocomplete and so the form
1310
+ * still behaves like a text field for screen readers.
1311
+ *
1312
+ * Wires `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls`,
1313
+ * `aria-autocomplete`, and `aria-activedescendant` (per APG combobox).
1314
+ * Focus stays on the input — arrow keys move the listbox's *active
1315
+ * descendant* via id reference, never moving DOM focus into the listbox.
1316
+ *
1317
+ * Keyboard:
1318
+ * - **ArrowDown / ArrowUp** — open + move activedescendant.
1319
+ * - **Home / End** (when open) — first / last enabled option.
1320
+ * - **PageUp / PageDown** (when open) — first / last enabled option.
1321
+ * - **Enter** (when open) — activate the activedescendant; no-op otherwise.
1322
+ * - **Escape** (when open) — close (focus stays in input). On the
1323
+ * open→closed transition the input value is re-synced to `query()` even
1324
+ * while focused, so a consumer restoring the committed label from an
1325
+ * `(openChange)` handler renders without reaching for the DOM.
1326
+ * - **Tab** (when open) — close and let Tab flow to the next focusable.
1327
+ * - Printable keys: update `query` and (if `autocompleteMode` includes `'inline'`)
1328
+ * complete the rest of the first match in the input as selected text.
1329
+ */
1330
+ class ForComboboxInput {
1331
+ // The input doesn't introspect the value type — it reads/writes the
1332
+ // query string and reads cached labels for inline autocomplete.
1333
+ ctx = injectComboboxContext('ForComboboxInput');
1334
+ #host = inject(ElementRef);
1335
+ #composing = false;
1336
+ ariaAutocomplete = computed(() => this.ctx.autocompleteMode(), /* @ts-ignore */
1337
+ ...(ngDevMode ? [{ debugName: "ariaAutocomplete" }] : /* istanbul ignore next */ []));
1338
+ constructor() {
1339
+ registerHandle(this.#host.nativeElement, (el) => this.ctx.registerInput(el), (el) => this.ctx.unregisterInput(el));
1340
+ reflectDisabled(this.ctx.effectiveDisabled);
1341
+ const doc = inject(DOCUMENT);
1342
+ // Sync DOM input.value to query() when the input doesn't currently have
1343
+ // focus (e.g. consumer set [(query)] programmatically, or option
1344
+ // activation copied the label into query via commitOnSelect). When the
1345
+ // input is focused the user is mid-edit; clobbering input.value would
1346
+ // jump the caret and break inline autocomplete state.
1347
+ //
1348
+ // The open→closed transition is the documented exception: the edit
1349
+ // session is over (Escape closes but keeps focus in the input, per APG),
1350
+ // so a consumer restoring the committed label — `query.set(label)` from an
1351
+ // `(openChange)` handler — must reach the DOM even while focused. The
1352
+ // caret-protection rationale no longer applies and a caret jump to the end
1353
+ // of the restored text is the expected outcome. `wasOpen` tracks the prior
1354
+ // open state to detect the edge; it's a plain transition latch, not derived
1355
+ // signal state.
1356
+ let wasOpen = this.ctx.open();
1357
+ effect(() => {
1358
+ const q = this.ctx.query();
1359
+ const closing = wasOpen && !this.ctx.open();
1360
+ wasOpen = this.ctx.open();
1361
+ const el = this.#host.nativeElement;
1362
+ if (el.value === q) {
1363
+ return;
1364
+ }
1365
+ if (doc.activeElement !== el || closing) {
1366
+ el.value = q;
1367
+ }
1368
+ });
1369
+ }
1370
+ onCompositionStart() {
1371
+ this.#composing = true;
1372
+ }
1373
+ onCompositionEnd() {
1374
+ this.#composing = false;
1375
+ if (this.ctx.effectiveDisabled() || this.ctx.readonly()) {
1376
+ return;
1377
+ }
1378
+ this.#syncQuery(true, false);
1379
+ }
1380
+ onInput(event) {
1381
+ if (this.ctx.effectiveDisabled() || this.ctx.readonly()) {
1382
+ return;
1383
+ }
1384
+ if (this.#composing) {
1385
+ return;
1386
+ }
1387
+ const inputEvent = event;
1388
+ const allowInline = !inputEvent.isComposing;
1389
+ const isDelete = (inputEvent.inputType ?? '').startsWith('delete');
1390
+ this.#syncQuery(allowInline, isDelete);
1391
+ }
1392
+ #syncQuery(allowInline, isDelete) {
1393
+ const el = this.#host.nativeElement;
1394
+ // The "user prefix" is everything before the caret. After inline
1395
+ // autocomplete sets a selection on chars [prefixLen..end], typing replaces
1396
+ // the selection (caret lands right after the new char) so this still
1397
+ // captures the user's intent. On Backspace, the browser deletes the
1398
+ // selection without touching the prefix — re-completing here would mean
1399
+ // the user can never shorten the query, so we skip completion on delete.
1400
+ const caret = el.selectionStart ?? el.value.length;
1401
+ const prefix = el.value.slice(0, caret);
1402
+ const atEnd = caret === el.value.length;
1403
+ const mode = this.ctx.autocompleteMode();
1404
+ const inlineActive = allowInline &&
1405
+ !isDelete &&
1406
+ atEnd &&
1407
+ (mode === 'inline' || mode === 'both') &&
1408
+ prefix.length > 0;
1409
+ if (inlineActive) {
1410
+ this.ctx.setQueryFromInput(prefix);
1411
+ this.#applyInlineCompletion(prefix);
1412
+ }
1413
+ else {
1414
+ this.ctx.setQueryFromInput(el.value);
1415
+ }
1416
+ }
1417
+ #applyInlineCompletion(prefix) {
1418
+ const cached = this.ctx.cachedOptions();
1419
+ const lower = prefix.toLowerCase();
1420
+ const match = cached.find((o) => o.label.toLowerCase().startsWith(lower));
1421
+ const el = this.#host.nativeElement;
1422
+ if (!match || match.label.toLowerCase() === lower) {
1423
+ // No completion to apply — strip any prior selection so the user's
1424
+ // prefix is plainly visible.
1425
+ if (el.value !== prefix) {
1426
+ el.value = prefix;
1427
+ }
1428
+ return;
1429
+ }
1430
+ // Use the option's full label as the visible text (preserving its case),
1431
+ // with chars [prefixLen..end] selected so the next keystroke replaces
1432
+ // the appended suggestion. Matches native browser autocomplete behavior.
1433
+ const composed = match.label;
1434
+ el.value = composed;
1435
+ el.setSelectionRange(prefix.length, composed.length);
1436
+ }
1437
+ onKeyDown(event) {
1438
+ if (this.ctx.effectiveDisabled()) {
1439
+ return;
1440
+ }
1441
+ // Multi-mode Backspace heuristic: an empty input sends focus to the
1442
+ // last chip so a second Backspace there removes it. Skip when there's a
1443
+ // non-empty selection in the input (the user's mid-edit) — Backspace falls
1444
+ // through to the native delete-char handler.
1445
+ if (event.key === 'Backspace' &&
1446
+ this.ctx.multiple() &&
1447
+ this.#host.nativeElement.value === '' &&
1448
+ !this.ctx.readonly() &&
1449
+ !this.ctx.effectiveDisabled()) {
1450
+ const chips = this.ctx.chips();
1451
+ const last = chips[chips.length - 1];
1452
+ if (last) {
1453
+ event.preventDefault();
1454
+ last.host.focus();
1455
+ return;
1456
+ }
1457
+ }
1458
+ switch (event.key) {
1459
+ case 'ArrowDown':
1460
+ event.preventDefault();
1461
+ if (!this.ctx.open()) {
1462
+ // Open with the natural extreme as the auto-highlight bias —
1463
+ // navigate('next') won't fire here because items aren't registered
1464
+ // yet; the root's `activeId` linkedSignal seeds the activedescendant
1465
+ // once the options register.
1466
+ this.ctx.openMenu('first');
1467
+ }
1468
+ else {
1469
+ this.ctx.navigate('next');
1470
+ }
1471
+ break;
1472
+ case 'ArrowUp':
1473
+ event.preventDefault();
1474
+ if (!this.ctx.open()) {
1475
+ this.ctx.openMenu('last');
1476
+ }
1477
+ else {
1478
+ this.ctx.navigate('prev');
1479
+ }
1480
+ break;
1481
+ case 'Home':
1482
+ if (this.ctx.open()) {
1483
+ event.preventDefault();
1484
+ this.ctx.navigate('first');
1485
+ }
1486
+ break;
1487
+ case 'End':
1488
+ if (this.ctx.open()) {
1489
+ event.preventDefault();
1490
+ this.ctx.navigate('last');
1491
+ }
1492
+ break;
1493
+ case 'PageUp':
1494
+ if (this.ctx.open()) {
1495
+ event.preventDefault();
1496
+ this.ctx.navigate('first');
1497
+ }
1498
+ break;
1499
+ case 'PageDown':
1500
+ if (this.ctx.open()) {
1501
+ event.preventDefault();
1502
+ this.ctx.navigate('last');
1503
+ }
1504
+ break;
1505
+ case 'Enter':
1506
+ if (this.ctx.open() && this.ctx.activateActive()) {
1507
+ event.preventDefault();
1508
+ }
1509
+ break;
1510
+ case 'Escape':
1511
+ // Routed through emitEscapeKeyDown so the consumer's
1512
+ // (escapeKeyDown) output fires and can veto the close. We still
1513
+ // handle Escape inline (not via the dismissable layer) because
1514
+ // focus stays in the input — the Escape belongs to this input
1515
+ // and shouldn't bubble through nested layers before it sees it.
1516
+ if (this.ctx.open()) {
1517
+ event.preventDefault();
1518
+ this.ctx.emitEscapeKeyDown(event);
1519
+ }
1520
+ break;
1521
+ case 'Tab':
1522
+ if (this.ctx.open()) {
1523
+ this.ctx.closeMenu('tab');
1524
+ }
1525
+ // Don't preventDefault — Tab still flows to the next focusable.
1526
+ break;
1527
+ }
1528
+ }
1529
+ onFocus() {
1530
+ if (this.ctx.openOnFocus() && !this.ctx.open()) {
1531
+ this.ctx.openMenu();
1532
+ }
1533
+ }
1534
+ onClick() {
1535
+ // A click on the input while focused mirrors the focus-open intent —
1536
+ // useful when the user closed via Escape and wants to re-open without
1537
+ // typing or reaching for the keyboard.
1538
+ if (this.ctx.openOnFocus() && !this.ctx.open()) {
1539
+ this.ctx.openMenu();
1540
+ }
1541
+ }
1542
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1543
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxInput, isStandalone: true, selector: "[forComboboxInput]", host: { attributes: { "type": "text", "role": "combobox", "autocomplete": "off", "autocorrect": "off", "spellcheck": "false" }, listeners: { "input": "onInput($event)", "compositionstart": "onCompositionStart()", "compositionend": "onCompositionEnd()", "keydown": "onKeyDown($event)", "focus": "onFocus()", "click": "onClick()" }, properties: { "id": "ctx.inputId()", "attr.aria-haspopup": "\"listbox\"", "attr.aria-expanded": "ctx.open() ? \"true\" : \"false\"", "attr.aria-controls": "ctx.open() ? ctx.listboxId() : null", "attr.aria-autocomplete": "ariaAutocomplete()", "attr.aria-activedescendant": "ctx.open() ? (ctx.activeId() ?? null) : null", "attr.aria-disabled": "ctx.effectiveDisabled() ? \"true\" : null", "attr.aria-readonly": "ctx.readonly() ? \"true\" : null", "attr.aria-required": "ctx.required() ? \"true\" : null", "attr.aria-invalid": "ctx.invalid() ? \"true\" : null", "attr.aria-busy": "ctx.pending() ? \"true\" : null", "attr.readonly": "ctx.readonly() ? \"\" : null", "attr.data-state": "ctx.open() ? \"open\" : \"closed\"", "attr.data-disabled": "ctx.effectiveDisabled() ? \"\" : null" } }, exportAs: ["forComboboxInput"], ngImport: i0 });
1544
+ }
1545
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxInput, decorators: [{
1546
+ type: Directive,
1547
+ args: [{
1548
+ selector: '[forComboboxInput]',
1549
+ exportAs: 'forComboboxInput',
1550
+ host: {
1551
+ type: 'text',
1552
+ role: 'combobox',
1553
+ autocomplete: 'off',
1554
+ autocorrect: 'off',
1555
+ spellcheck: 'false',
1556
+ '[id]': 'ctx.inputId()',
1557
+ '[attr.aria-haspopup]': '"listbox"',
1558
+ '[attr.aria-expanded]': 'ctx.open() ? "true" : "false"',
1559
+ '[attr.aria-controls]': 'ctx.open() ? ctx.listboxId() : null',
1560
+ '[attr.aria-autocomplete]': 'ariaAutocomplete()',
1561
+ '[attr.aria-activedescendant]': 'ctx.open() ? (ctx.activeId() ?? null) : null',
1562
+ '[attr.aria-disabled]': 'ctx.effectiveDisabled() ? "true" : null',
1563
+ '[attr.aria-readonly]': 'ctx.readonly() ? "true" : null',
1564
+ '[attr.aria-required]': 'ctx.required() ? "true" : null',
1565
+ '[attr.aria-invalid]': 'ctx.invalid() ? "true" : null',
1566
+ '[attr.aria-busy]': 'ctx.pending() ? "true" : null',
1567
+ '[attr.readonly]': 'ctx.readonly() ? "" : null',
1568
+ '[attr.data-state]': 'ctx.open() ? "open" : "closed"',
1569
+ '[attr.data-disabled]': 'ctx.effectiveDisabled() ? "" : null',
1570
+ '(input)': 'onInput($event)',
1571
+ '(compositionstart)': 'onCompositionStart()',
1572
+ '(compositionend)': 'onCompositionEnd()',
1573
+ '(keydown)': 'onKeyDown($event)',
1574
+ '(focus)': 'onFocus()',
1575
+ '(click)': 'onClick()',
1576
+ },
1577
+ }]
1578
+ }], ctorParameters: () => [] });
1579
+
1580
+ /**
1581
+ * The floating surface, portaled to `document.body` and positioned by
1582
+ * `@floating-ui/dom` against the anchor (explicit `[forComboboxAnchor]` →
1583
+ * `[forComboboxTrigger]` → input).
1584
+ *
1585
+ * Two anatomies, picked by whether an inner `[forComboboxList]` is present:
1586
+ *
1587
+ * - **Editable (no list)** — content itself carries `role="listbox"`,
1588
+ * `tabindex="-1"`, `aria-multiselectable`, `aria-setsize`, and the labelled
1589
+ * role (`aria-label` / `aria-labelledby`). The input's `aria-controls` points
1590
+ * here. This is the original combobox; nothing about it changes.
1591
+ * - **Picker (list present)** — content drops the listbox semantics and becomes
1592
+ * a neutral popup surface; `[forComboboxList]` takes over the listbox role and
1593
+ * owns the options, the input lives inside the panel, and the input's
1594
+ * `aria-controls` points to the list. Keeps `data-state` and the positioner +
1595
+ * dismissable layer unchanged.
1596
+ *
1597
+ * Mount/unmount of the visible content is the consumer's responsibility —
1598
+ * wrap with `@if (open())` so `animate.enter` / `animate.leave` fire on the
1599
+ * natural mount cycle. While mounted, a `DismissableLayer` activates for
1600
+ * pointer-down outside / focus outside; the input and (picker anatomy) the
1601
+ * trigger are exempt from outside checks.
1602
+ *
1603
+ * Focus:
1604
+ * - **Editable anatomy** — focus stays in the input across the whole open
1605
+ * lifecycle; the directive never moves DOM focus and exposes no focus hooks.
1606
+ * Active-option highlighting is `aria-activedescendant`-driven.
1607
+ * - **Picker anatomy** — on open, focus moves into the input (the search field
1608
+ * inside the panel); on close it returns to the trigger. Both moves are
1609
+ * vetoable via `(autoFocusOnOpen)` / `(autoFocusOnClose)` on `[forCombobox]`,
1610
+ * and the return is gated by `[returnFocus]`. Escape stays owned by the input.
1611
+ *
1612
+ * The lifecycle (positioner + dismissable layer, plus the picker anatomy's
1613
+ * focus bundles) is owned by the shared `injectOverlayShell` helper.
1614
+ */
1615
+ class ForComboboxContent {
1616
+ ctx = injectComboboxContext('ForComboboxContent');
1617
+ #host = inject(ElementRef);
1618
+ /** When a `[forComboboxList]` is registered the listbox semantics live there, not here. */
1619
+ hasList = this.ctx.hasList;
1620
+ /**
1621
+ * Reflects `aria-setsize` when the consumer wires up `[totalCount]` for
1622
+ * virtualization (editable anatomy only — the picker anatomy's list owns it).
1623
+ * Falls back to `null` (omitted) otherwise — leaving the default
1624
+ * option-count semantics screen readers already infer.
1625
+ */
1626
+ ariaSetSize = computed(() => {
1627
+ const total = this.ctx.totalCount();
1628
+ return total === undefined ? null : String(total);
1629
+ }, /* @ts-ignore */
1630
+ ...(ngDevMode ? [{ debugName: "ariaSetSize" }] : /* istanbul ignore next */ []));
1631
+ constructor() {
1632
+ const ctx = this.ctx;
1633
+ registerHandle(this.#host.nativeElement, (el) => ctx.registerContent(el), (el) => ctx.unregisterContent(el));
1634
+ // The picker anatomy opts back into the shell's focus bundles. `trigger()`
1635
+ // is read once on construction: the trigger renders outside the `@if (open())`
1636
+ // that gates this content, so it has already registered by the time the
1637
+ // surface mounts. Without a trigger (editable anatomy) the bundles are
1638
+ // omitted entirely and focus stays in the input — byte-for-byte unchanged.
1639
+ const hasTrigger = ctx.trigger() !== null;
1640
+ const config = {
1641
+ positioner: {
1642
+ kind: 'floating',
1643
+ reference: ctx.anchor,
1644
+ open: ctx.open,
1645
+ side: ctx.side,
1646
+ align: ctx.align,
1647
+ sideOffset: ctx.sideOffset,
1648
+ alignOffset: ctx.alignOffset,
1649
+ avoidCollisions: ctx.avoidCollisions,
1650
+ collisionPadding: ctx.collisionPadding,
1651
+ arrowPadding: ctx.arrowPadding,
1652
+ sticky: ctx.sticky,
1653
+ hideWhenDetached: ctx.hideWhenDetached,
1654
+ clipUntilPositioned: ctx.clipUntilPositioned,
1655
+ },
1656
+ dismiss: {
1657
+ dismissible: ctx.dismissible,
1658
+ requestClose: (reason) => ctx.requestClose(reason),
1659
+ // Escape is handled by the input directive (focus stays in the
1660
+ // input, so Escape there shouldn't bubble through nested layers
1661
+ // before the input sees it). Omitted here intentionally.
1662
+ emitPointerDownOutside: (veto) => ctx.emitPointerDownOutside(veto),
1663
+ emitFocusOutside: (veto) => ctx.emitFocusOutside(veto),
1664
+ emitInteractOutside: (veto) => ctx.emitInteractOutside(veto),
1665
+ // The input owns the visible focus and toggles via its own click /
1666
+ // focus handlers; the trigger (picker anatomy) toggles via its own
1667
+ // click handler. Without exemption a pointer-down on either would race
1668
+ // the dismissal layer.
1669
+ exemptElements: () => {
1670
+ const els = [];
1671
+ const input = ctx.input();
1672
+ if (input)
1673
+ els.push(input);
1674
+ const trigger = ctx.trigger();
1675
+ if (trigger)
1676
+ els.push(trigger);
1677
+ return els;
1678
+ },
1679
+ },
1680
+ // Picker anatomy only: move focus into the input on open, return it to the
1681
+ // trigger on close. Editable anatomy keeps focus in the input throughout,
1682
+ // so both bundles stay absent (no imperative move, no hooks).
1683
+ ...(hasTrigger
1684
+ ? {
1685
+ initialFocus: {
1686
+ move: () => {
1687
+ const input = ctx.input();
1688
+ if (input) {
1689
+ input.focus();
1690
+ return true;
1691
+ }
1692
+ return false;
1693
+ },
1694
+ veto: () => ctx.emitAutoFocusOnOpen(),
1695
+ },
1696
+ returnFocus: {
1697
+ enabled: ctx.returnFocus,
1698
+ target: () => ctx.trigger(),
1699
+ veto: () => ctx.emitAutoFocusOnClose(),
1700
+ // Tab already advanced focus past the closing surface; re-focusing
1701
+ // the trigger would steal it back.
1702
+ skip: () => ctx.lastCloseReason() === 'tab',
1703
+ },
1704
+ }
1705
+ : {}),
1706
+ };
1707
+ injectOverlayShell(config);
1708
+ }
1709
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1710
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxContent, isStandalone: true, selector: "[forComboboxContent]", host: { properties: { "attr.role": "hasList() ? null : \"listbox\"", "attr.tabindex": "hasList() ? null : \"-1\"", "id": "ctx.contentId()", "attr.aria-labelledby": "hasList() ? null : (ctx.ariaLabel() ? null : ctx.inputId())", "attr.aria-label": "hasList() ? null : ctx.ariaLabel()", "attr.aria-multiselectable": "hasList() ? null : (ctx.multiple() ? \"true\" : null)", "attr.aria-setsize": "hasList() ? null : ariaSetSize()", "attr.data-state": "ctx.open() ? \"open\" : \"closed\"" } }, exportAs: ["forComboboxContent"], ngImport: i0 });
1711
+ }
1712
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxContent, decorators: [{
1713
+ type: Directive,
1714
+ args: [{
1715
+ selector: '[forComboboxContent]',
1716
+ exportAs: 'forComboboxContent',
1717
+ host: {
1718
+ '[attr.role]': 'hasList() ? null : "listbox"',
1719
+ '[attr.tabindex]': 'hasList() ? null : "-1"',
1720
+ '[id]': 'ctx.contentId()',
1721
+ '[attr.aria-labelledby]': 'hasList() ? null : (ctx.ariaLabel() ? null : ctx.inputId())',
1722
+ '[attr.aria-label]': 'hasList() ? null : ctx.ariaLabel()',
1723
+ '[attr.aria-multiselectable]': 'hasList() ? null : (ctx.multiple() ? "true" : null)',
1724
+ '[attr.aria-setsize]': 'hasList() ? null : ariaSetSize()',
1725
+ '[attr.data-state]': 'ctx.open() ? "open" : "closed"',
1726
+ },
1727
+ }]
1728
+ }], ctorParameters: () => [] });
1729
+
1730
+ /**
1731
+ * The `role="listbox"` element of the picker anatomy. Nest it inside
1732
+ * `[forComboboxContent]` next to `[forComboboxInput]` so the popup surface can
1733
+ * hold a search field without violating `aria-required-owned-elements` (a
1734
+ * `listbox` may only own `option` / `group` children). The list owns the
1735
+ * options; `[forComboboxContent]` becomes a neutral popup surface.
1736
+ *
1737
+ * ```html
1738
+ * <div forComboboxContent>
1739
+ * <input forComboboxInput />
1740
+ * <div forComboboxList>
1741
+ * @for (item of filtered(); track item.id) {
1742
+ * <div forComboboxOption [value]="item">{{ item.label }}</div>
1743
+ * }
1744
+ * </div>
1745
+ * </div>
1746
+ * ```
1747
+ *
1748
+ * Carries `role="listbox"`, `tabindex="-1"` (focus stays in the input,
1749
+ * activedescendant-driven), `aria-multiselectable` in multi mode,
1750
+ * `aria-setsize` when virtualizing, and the labelled-role `aria-label` /
1751
+ * `aria-labelledby`. Its id is what the input's `aria-controls` references in
1752
+ * the picker anatomy.
1753
+ *
1754
+ * When no `[forComboboxList]` is present, `[forComboboxContent]` itself carries
1755
+ * the listbox semantics (the editable anatomy) — this part is additive and
1756
+ * non-breaking.
1757
+ */
1758
+ class ForComboboxList {
1759
+ ctx = injectComboboxContext('ForComboboxList');
1760
+ #host = inject(ElementRef);
1761
+ /**
1762
+ * Reflects `aria-setsize` when the consumer wires up `[totalCount]` for
1763
+ * virtualization. Falls back to `null` (omitted) otherwise.
1764
+ */
1765
+ ariaSetSize = computed(() => {
1766
+ const total = this.ctx.totalCount();
1767
+ return total === undefined ? null : String(total);
1768
+ }, /* @ts-ignore */
1769
+ ...(ngDevMode ? [{ debugName: "ariaSetSize" }] : /* istanbul ignore next */ []));
1770
+ constructor() {
1771
+ registerHandle(this.#host.nativeElement, (el) => this.ctx.registerList(el), (el) => this.ctx.unregisterList(el));
1772
+ }
1773
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxList, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1774
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxList, isStandalone: true, selector: "[forComboboxList]", host: { attributes: { "role": "listbox", "tabindex": "-1" }, properties: { "id": "ctx.listId()", "attr.aria-labelledby": "ctx.ariaLabel() ? null : ctx.inputId()", "attr.aria-label": "ctx.ariaLabel()", "attr.aria-multiselectable": "ctx.multiple() ? \"true\" : null", "attr.aria-setsize": "ariaSetSize()" } }, exportAs: ["forComboboxList"], ngImport: i0 });
1775
+ }
1776
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxList, decorators: [{
1777
+ type: Directive,
1778
+ args: [{
1779
+ selector: '[forComboboxList]',
1780
+ exportAs: 'forComboboxList',
1781
+ host: {
1782
+ role: 'listbox',
1783
+ tabindex: '-1',
1784
+ '[id]': 'ctx.listId()',
1785
+ '[attr.aria-labelledby]': 'ctx.ariaLabel() ? null : ctx.inputId()',
1786
+ '[attr.aria-label]': 'ctx.ariaLabel()',
1787
+ '[attr.aria-multiselectable]': 'ctx.multiple() ? "true" : null',
1788
+ '[attr.aria-setsize]': 'ariaSetSize()',
1789
+ },
1790
+ }]
1791
+ }], ctorParameters: () => [] });
1792
+
1793
+ /**
1794
+ * Injection key the `[forComboboxIndicator]` uses to resolve its parent
1795
+ * option, decoupled from the concrete `ForComboboxOption` class.
1796
+ * `ForComboboxOption` provides itself under this token, so a design system
1797
+ * wrapping the option by subclassing re-points it at the subclass with a
1798
+ * single provider (`{ provide: FOR_COMBOBOX_OPTION, useExisting: MtxComboboxOption }`)
1799
+ * and the indicator keeps resolving — see `docs/wrapping-form-primitives.md`.
1800
+ */
1801
+ const FOR_COMBOBOX_OPTION = new InjectionToken('FOR_COMBOBOX_OPTION');
1802
+ /**
1803
+ * One option inside a `[forComboboxContent]`. Apply on whatever element
1804
+ * fits the design — typically a `<div>` or `<li>`. Click activates:
1805
+ * single mode replaces `[(value)]` and closes the listbox; multi mode
1806
+ * toggles the value in/out and keeps the listbox open.
1807
+ *
1808
+ * Generic over the option value type `T` (default `string`). Inferred from
1809
+ * the `[value]` binding so consumers can pass either primitive ids or
1810
+ * full objects (`[value]="city"` infers `T = City`); the parent
1811
+ * `[forCombobox]` must be parameterized over the same `T`.
1812
+ *
1813
+ * `aria-selected` reflects different things in single vs. multi:
1814
+ * - **Single mode**: the option that's the current activedescendant
1815
+ * (Enter would activate it). Matches APG select-only-combobox.
1816
+ * - **Multi mode**: every option currently in `value()` carries
1817
+ * `aria-selected="true"` (multiple "selected" entries simultaneously).
1818
+ * Matches APG multi-select combobox.
1819
+ *
1820
+ * `data-state="checked" | "unchecked"` always reflects membership in
1821
+ * `value()` regardless of mode, so consumers can paint a checkmark with
1822
+ * pure CSS in either mode. `data-highlighted` marks the option that is
1823
+ * the current activedescendant — shared across the
1824
+ * library's roving / activedescendant primitives.
1825
+ *
1826
+ * Hovering an option also makes it the activedescendant, mirroring native
1827
+ * menu / select behavior so mouse and keyboard intent stay synchronized.
1828
+ */
1829
+ class ForComboboxOption {
1830
+ #ctx = injectComboboxContext('ForComboboxOption');
1831
+ #host = inject(ElementRef);
1832
+ /**
1833
+ * Stable identifier serialized into `[(value)]` and the hidden input.
1834
+ * Defaults to `string` for back-compat; bind an object to specialize
1835
+ * the parent `[forCombobox]` over a richer `T`. The parent's
1836
+ * `[isItemEqualToValue]` decides how options are matched against the
1837
+ * committed selection.
1838
+ */
1839
+ value = input.required(/* @ts-ignore */
1840
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1841
+ /**
1842
+ * Visible label used by `[forComboboxInput]` for inline autocomplete
1843
+ * matching, by `commitOnSelect` to populate the input on selection,
1844
+ * and by typeahead / display utilities. When omitted: for string
1845
+ * `value` falls back to the trimmed `textContent` of the host element;
1846
+ * for object `value` falls back to the parent's `itemToStringLabel(value)`.
1847
+ */
1848
+ label = input(null, /* @ts-ignore */
1849
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1850
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1851
+ /**
1852
+ * Absolute index of this option in the consumer's source array. Required
1853
+ * when wiring up a virtualized listbox so navigation past the rendered
1854
+ * window can resolve indices to options (and emit `(scrollToIndex)` when
1855
+ * needed). Leave `null` for non-virtualized lists — the directive then
1856
+ * falls back to DOM order.
1857
+ */
1858
+ posInSet = input(null, /* @ts-ignore */
1859
+ ...(ngDevMode ? [{ debugName: "posInSet" }] : /* istanbul ignore next */ []));
1860
+ id = hostId('for-combobox-option');
1861
+ selected = computed(() => this.#ctx.isSelected(this.value()), /* @ts-ignore */
1862
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1863
+ /** True when this option is the current activedescendant. Reflected as `data-highlighted`. */
1864
+ highlighted = computed(() => this.#ctx.isActive(this.id()), /* @ts-ignore */
1865
+ ...(ngDevMode ? [{ debugName: "highlighted" }] : /* istanbul ignore next */ []));
1866
+ effectiveDisabled = computed(() => this.disabled() || this.#ctx.effectiveDisabled(), /* @ts-ignore */
1867
+ ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
1868
+ ariaSelected = computed(() => {
1869
+ if (this.#ctx.multiple()) {
1870
+ return this.selected() ? 'true' : 'false';
1871
+ }
1872
+ return this.highlighted() ? 'true' : 'false';
1873
+ }, /* @ts-ignore */
1874
+ ...(ngDevMode ? [{ debugName: "ariaSelected" }] : /* istanbul ignore next */ []));
1875
+ /** Reflects `aria-posinset` (1-based) when virtualizing. Falls back to DOM order otherwise. */
1876
+ ariaPosInSet = computed(() => {
1877
+ const explicit = this.posInSet();
1878
+ if (explicit !== null) {
1879
+ return String(explicit + 1);
1880
+ }
1881
+ if (this.#ctx.totalCount() === undefined) {
1882
+ return null;
1883
+ }
1884
+ const items = this.#ctx.options();
1885
+ const idx = items.findIndex((it) => it.id() === this.id());
1886
+ return idx < 0 ? null : String(idx + 1);
1887
+ }, /* @ts-ignore */
1888
+ ...(ngDevMode ? [{ debugName: "ariaPosInSet" }] : /* istanbul ignore next */ []));
1889
+ /** Reflects `aria-setsize` when the consumer wires up `[totalCount]`. */
1890
+ ariaSetSize = computed(() => {
1891
+ const total = this.#ctx.totalCount();
1892
+ return total === undefined ? null : String(total);
1893
+ }, /* @ts-ignore */
1894
+ ...(ngDevMode ? [{ debugName: "ariaSetSize" }] : /* istanbul ignore next */ []));
1895
+ #effectiveLabel = computed(() => {
1896
+ const explicit = this.label();
1897
+ if (explicit !== null) {
1898
+ return explicit;
1899
+ }
1900
+ const v = this.value();
1901
+ if (typeof v === 'string') {
1902
+ // String mode: the trimmed `textContent` is the canonical fallback,
1903
+ // identical to the pre-generic behaviour. Lets consumers omit
1904
+ // `[label]` and have it just work for projected text.
1905
+ return (this.#host.nativeElement.textContent ?? '').trim();
1906
+ }
1907
+ // Object mode: lean on the parent's `itemToStringLabel` so the
1908
+ // resolved label is consistent with chip rendering and `commitOnSelect`.
1909
+ return this.#ctx.itemToStringLabel()(v);
1910
+ }, /* @ts-ignore */
1911
+ ...(ngDevMode ? [{ debugName: "#effectiveLabel" }] : /* istanbul ignore next */ []));
1912
+ #handle = {
1913
+ host: this.#host.nativeElement,
1914
+ id: this.id,
1915
+ value: this.value,
1916
+ label: this.#effectiveLabel,
1917
+ disabled: this.effectiveDisabled,
1918
+ posInSet: this.posInSet,
1919
+ };
1920
+ constructor() {
1921
+ registerHandle(this.#handle, (h) => this.#ctx.registerOption(h), (h) => this.#ctx.unregisterOption(h));
1922
+ }
1923
+ onClick() {
1924
+ if (this.effectiveDisabled() || this.#ctx.readonly()) {
1925
+ return;
1926
+ }
1927
+ this.#ctx.activate(this.#handle);
1928
+ }
1929
+ onPointerMove() {
1930
+ if (this.effectiveDisabled() || this.#ctx.isPointerSuppressed()) {
1931
+ return;
1932
+ }
1933
+ if (!this.#ctx.isActive(this.id())) {
1934
+ this.#ctx.setActiveId(this.id());
1935
+ }
1936
+ }
1937
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxOption, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1938
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForComboboxOption, isStandalone: true, selector: "[forComboboxOption]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, 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" }, listeners: { "click": "onClick()", "pointermove": "onPointerMove()" }, properties: { "id": "id()", "attr.aria-selected": "ariaSelected()", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.aria-posinset": "ariaPosInSet()", "attr.aria-setsize": "ariaSetSize()", "attr.data-state": "selected() ? \"checked\" : \"unchecked\"", "attr.data-highlighted": "highlighted() ? \"\" : null", "attr.data-disabled": "effectiveDisabled() ? \"\" : null" } }, providers: [{ provide: FOR_COMBOBOX_OPTION, useExisting: ForComboboxOption }], exportAs: ["forComboboxOption"], ngImport: i0 });
1939
+ }
1940
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxOption, decorators: [{
1941
+ type: Directive,
1942
+ args: [{
1943
+ selector: '[forComboboxOption]',
1944
+ exportAs: 'forComboboxOption',
1945
+ providers: [{ provide: FOR_COMBOBOX_OPTION, useExisting: ForComboboxOption }],
1946
+ host: {
1947
+ role: 'option',
1948
+ '[id]': 'id()',
1949
+ '[attr.aria-selected]': 'ariaSelected()',
1950
+ '[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
1951
+ '[attr.aria-posinset]': 'ariaPosInSet()',
1952
+ '[attr.aria-setsize]': 'ariaSetSize()',
1953
+ '[attr.data-state]': 'selected() ? "checked" : "unchecked"',
1954
+ '[attr.data-highlighted]': 'highlighted() ? "" : null',
1955
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
1956
+ '(click)': 'onClick()',
1957
+ '(pointermove)': 'onPointerMove()',
1958
+ },
1959
+ }]
1960
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], posInSet: [{ type: i0.Input, args: [{ isSignal: true, alias: "posInSet", required: false }] }] } });
1961
+
1962
+ /**
1963
+ * Visibility helper inside a `[forComboboxOption]`. The directive flips a
1964
+ * `[hidden]` host binding so the consumer can keep the checkmark / icon
1965
+ * inline without extra `@if` glue:
1966
+ *
1967
+ * ```html
1968
+ * <div forComboboxOption value="apple">
1969
+ * <span forComboboxIndicator>✓</span>
1970
+ * Apple
1971
+ * </div>
1972
+ * ```
1973
+ *
1974
+ * Reflects the parent option's `data-state` (`"checked" | "unchecked"`) so
1975
+ * the consumer can also style it from CSS. Visibility while unselected is
1976
+ * enforced with an inline `display: none` (which beats any author `display`
1977
+ * rule a consumer applies via a class) in addition to the `hidden` attribute
1978
+ * that removes it from the a11y tree.
1979
+ *
1980
+ * In multi mode the indicator follows membership in `value()`, matching the
1981
+ * option's own `data-state` semantics (a checkmark per selected option).
1982
+ */
1983
+ class ForComboboxIndicator {
1984
+ option = injectParentOption();
1985
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxIndicator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1986
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxIndicator, isStandalone: true, selector: "[forComboboxIndicator]", host: { attributes: { "aria-hidden": "true" }, properties: { "attr.data-state": "option.selected() ? \"checked\" : \"unchecked\"", "hidden": "!option.selected()", "style.display": "option.selected() ? null : \"none\"" } }, exportAs: ["forComboboxIndicator"], ngImport: i0 });
1987
+ }
1988
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxIndicator, decorators: [{
1989
+ type: Directive,
1990
+ args: [{
1991
+ selector: '[forComboboxIndicator]',
1992
+ exportAs: 'forComboboxIndicator',
1993
+ host: {
1994
+ 'aria-hidden': 'true',
1995
+ '[attr.data-state]': 'option.selected() ? "checked" : "unchecked"',
1996
+ '[hidden]': '!option.selected()',
1997
+ '[style.display]': 'option.selected() ? null : "none"',
1998
+ },
1999
+ }]
2000
+ }] });
2001
+ function injectParentOption() {
2002
+ const option = inject(FOR_COMBOBOX_OPTION, { optional: true });
2003
+ if (!option) {
2004
+ throw new Error('[forty-cdk/combobox] ForComboboxIndicator must be used inside a [forComboboxOption] element.');
2005
+ }
2006
+ return option;
2007
+ }
2008
+
2009
+ /**
2010
+ * Empty-state slot, shown when the listbox has no registered options.
2011
+ * The directive hides the message when options exist so the consumer can
2012
+ * keep it inline in the template — no `@if` needed. Visibility is enforced
2013
+ * with an inline `display: none` (which beats any author `display` rule a
2014
+ * consumer applies via a class) in addition to the `hidden` attribute that
2015
+ * removes it from the a11y tree:
2016
+ *
2017
+ * ```html
2018
+ * <div forComboboxContent>
2019
+ * @for (option of filtered(); track option.id) {
2020
+ * <div forComboboxOption [value]="option.id">{{ option.label }}</div>
2021
+ * }
2022
+ * <div forComboboxEmpty>No matches.</div>
2023
+ * </div>
2024
+ * ```
2025
+ *
2026
+ * Carries `role="status"` and `aria-live="polite"` so screen readers
2027
+ * announce the message when it appears.
2028
+ */
2029
+ class ForComboboxEmpty {
2030
+ #ctx = injectComboboxContext('ForComboboxEmpty');
2031
+ shouldShow = computed(() => this.#ctx.options().length === 0, /* @ts-ignore */
2032
+ ...(ngDevMode ? [{ debugName: "shouldShow" }] : /* istanbul ignore next */ []));
2033
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxEmpty, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2034
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxEmpty, isStandalone: true, selector: "[forComboboxEmpty]", host: { attributes: { "role": "status", "aria-live": "polite" }, properties: { "hidden": "!shouldShow()", "style.display": "shouldShow() ? null : \"none\"" } }, exportAs: ["forComboboxEmpty"], ngImport: i0 });
2035
+ }
2036
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxEmpty, decorators: [{
2037
+ type: Directive,
2038
+ args: [{
2039
+ selector: '[forComboboxEmpty]',
2040
+ exportAs: 'forComboboxEmpty',
2041
+ host: {
2042
+ role: 'status',
2043
+ 'aria-live': 'polite',
2044
+ '[hidden]': '!shouldShow()',
2045
+ '[style.display]': 'shouldShow() ? null : "none"',
2046
+ },
2047
+ }]
2048
+ }] });
2049
+
2050
+ /**
2051
+ * Live-region slot for async-filtering feedback (loading, result count,
2052
+ * "no matches", error messages). Apply on a `<div>` inside the listbox or
2053
+ * next to the input. The directive sets `role="status"` and
2054
+ * `aria-live="polite"` so messages projected as content are announced to
2055
+ * screen readers when they change.
2056
+ *
2057
+ * The directive is **content-driven** — it does not pick or render a
2058
+ * message. Project whatever the consumer wants and use the exposed
2059
+ * `count` signal (or `forComboboxStatus` template ref) to interpolate
2060
+ * the option count when relevant.
2061
+ *
2062
+ * ```html
2063
+ * <div forCombobox [(query)]="query" [(value)]="value" [(open)]="open">
2064
+ * <input forComboboxInput placeholder="Search…" />
2065
+ * @if (open()) {
2066
+ * <div forComboboxContent>
2067
+ * <div forComboboxStatus #status="forComboboxStatus">
2068
+ * @if (loading()) {
2069
+ * Searching…
2070
+ * } @else if (status.count() === 0) {
2071
+ * No matches.
2072
+ * } @else {
2073
+ * {{ status.count() }} results.
2074
+ * }
2075
+ * </div>
2076
+ * @for (it of filtered(); track it.id) {
2077
+ * <div forComboboxOption [value]="it.id">{{ it.label }}</div>
2078
+ * }
2079
+ * </div>
2080
+ * }
2081
+ * </div>
2082
+ * ```
2083
+ *
2084
+ * For an empty-only slot that auto-hides when there are options, use
2085
+ * `[forComboboxEmpty]`. `[forComboboxStatus]` stays mounted regardless
2086
+ * so transitions like "loading → 5 results" are announced as a single
2087
+ * change to the same live region.
2088
+ */
2089
+ class ForComboboxStatus {
2090
+ #ctx = injectComboboxContext('ForComboboxStatus');
2091
+ /**
2092
+ * Number of currently registered options. Reflects the live size of the
2093
+ * filtered listbox so the consumer can interpolate `{{ status.count() }}`
2094
+ * inside the live region.
2095
+ */
2096
+ count = computed(() => this.#ctx.options().length, /* @ts-ignore */
2097
+ ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
2098
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxStatus, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2099
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxStatus, isStandalone: true, selector: "[forComboboxStatus]", host: { attributes: { "role": "status", "aria-live": "polite", "aria-atomic": "true" } }, exportAs: ["forComboboxStatus"], ngImport: i0 });
2100
+ }
2101
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxStatus, decorators: [{
2102
+ type: Directive,
2103
+ args: [{
2104
+ selector: '[forComboboxStatus]',
2105
+ exportAs: 'forComboboxStatus',
2106
+ host: {
2107
+ role: 'status',
2108
+ 'aria-live': 'polite',
2109
+ 'aria-atomic': 'true',
2110
+ },
2111
+ }]
2112
+ }] });
2113
+
2114
+ /**
2115
+ * Optional clear button. Apply on a `<button type="button">` so Space /
2116
+ * Enter dispatch a native click. Clicking calls `clear()` on the root,
2117
+ * which resets `[(value)]` and `[(query)]` and the activedescendant.
2118
+ *
2119
+ * The directive hides the button when there's nothing to clear (no value,
2120
+ * empty query) so the consumer can leave it inline in the template without
2121
+ * an extra `@if`. Visibility is enforced with an inline `display: none`
2122
+ * (which beats any author `display` rule a consumer applies via a class) in
2123
+ * addition to the `hidden` attribute that removes it from the a11y tree.
2124
+ */
2125
+ class ForComboboxClear {
2126
+ ctx = injectComboboxContext('ForComboboxClear');
2127
+ hasContent = computed(() => this.ctx.value().length > 0 || this.ctx.query().length > 0, /* @ts-ignore */
2128
+ ...(ngDevMode ? [{ debugName: "hasContent" }] : /* istanbul ignore next */ []));
2129
+ /** Disabled when the combobox is disabled or read-only — the clear action is unavailable. */
2130
+ isDisabled = computed(() => this.ctx.effectiveDisabled() || this.ctx.readonly(), /* @ts-ignore */
2131
+ ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
2132
+ constructor() {
2133
+ reflectDisabled(this.isDisabled);
2134
+ }
2135
+ onClick() {
2136
+ if (this.isDisabled()) {
2137
+ return;
2138
+ }
2139
+ this.ctx.clear(true);
2140
+ // Restore focus to the input so the user can keep typing without
2141
+ // chasing the caret.
2142
+ this.ctx.input()?.focus();
2143
+ }
2144
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxClear, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2145
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxClear, isStandalone: true, selector: "[forComboboxClear]", host: { attributes: { "type": "button", "aria-label": "Clear" }, listeners: { "click": "onClick()" }, properties: { "hidden": "!hasContent()", "style.display": "hasContent() ? null : \"none\"", "attr.tabindex": "-1" } }, exportAs: ["forComboboxClear"], ngImport: i0 });
2146
+ }
2147
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxClear, decorators: [{
2148
+ type: Directive,
2149
+ args: [{
2150
+ selector: '[forComboboxClear]',
2151
+ exportAs: 'forComboboxClear',
2152
+ host: {
2153
+ type: 'button',
2154
+ 'aria-label': 'Clear',
2155
+ '[hidden]': '!hasContent()',
2156
+ '[style.display]': 'hasContent() ? null : "none"',
2157
+ '[attr.tabindex]': '-1',
2158
+ '(click)': 'onClick()',
2159
+ },
2160
+ }]
2161
+ }], ctorParameters: () => [] });
2162
+
2163
+ /**
2164
+ * Multi-mode chips area. Wrap the chips and the `<input>` together so the
2165
+ * combobox visually presents a single editable region:
2166
+ *
2167
+ * ```html
2168
+ * <div forCombobox multiple [(value)]="tags" [(query)]="query" [(open)]="open">
2169
+ * <div forComboboxChips>
2170
+ * @for (chip of selected(); track chip.value) {
2171
+ * <span forComboboxChip [value]="chip.value">
2172
+ * {{ chip.label }}
2173
+ * <button forComboboxChipRemove></button>
2174
+ * </span>
2175
+ * }
2176
+ * <input forComboboxInput />
2177
+ * </div>
2178
+ * …
2179
+ * </div>
2180
+ * ```
2181
+ *
2182
+ * Carries `role="group"` with `aria-label="Selected items"` (override via
2183
+ * `[ariaLabel]`) so screen readers announce the chip cluster as a single
2184
+ * unit. The directive itself doesn't manage focus or selection — the
2185
+ * chips and the input own that — but its presence groups them for
2186
+ * assistive tech.
2187
+ */
2188
+ class ForComboboxChips {
2189
+ ctx = injectComboboxContext('ForComboboxChips');
2190
+ /**
2191
+ * Accessible name for the chip cluster, exposed as `role="group"`'s
2192
+ * `aria-label` so screen readers announce the selected chips as a single
2193
+ * unit. Defaults to `'Selected items'`; set `[ariaLabel]` to localize or
2194
+ * override it.
2195
+ */
2196
+ ariaLabel = input('Selected items', /* @ts-ignore */
2197
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
2198
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxChips, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2199
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForComboboxChips, isStandalone: true, selector: "[forComboboxChips]", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "group" }, properties: { "attr.aria-label": "ariaLabel()" } }, exportAs: ["forComboboxChips"], ngImport: i0 });
2200
+ }
2201
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxChips, decorators: [{
2202
+ type: Directive,
2203
+ args: [{
2204
+ selector: '[forComboboxChips]',
2205
+ exportAs: 'forComboboxChips',
2206
+ host: {
2207
+ role: 'group',
2208
+ '[attr.aria-label]': 'ariaLabel()',
2209
+ },
2210
+ }]
2211
+ }], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
2212
+
2213
+ /**
2214
+ * One chip representing a selected value in multi mode. Apply on a
2215
+ * `<span>` (or any inline-block element) inside `[forComboboxChips]`.
2216
+ * The chip is **out of the Tab cycle** (`tabindex="-1"`) by design:
2217
+ * the user reaches it via the input's Backspace heuristic (Backspace on
2218
+ * an empty input focuses the last chip), then navigates between chips
2219
+ * with ArrowLeft / ArrowRight or removes them with Backspace / Delete.
2220
+ *
2221
+ * Keyboard while the chip has focus (LTR; the ArrowLeft/Right roles swap
2222
+ * in RTL so they always follow visual order):
2223
+ * - **ArrowLeft** — focus the previous chip; bounces if first.
2224
+ * - **ArrowRight** — focus the next chip; if at the last, focus the input.
2225
+ * - **Backspace / Delete** — remove this chip + focus the previous chip,
2226
+ * or the next chip when there is no previous one (so removing the first
2227
+ * chip lands on the new first chip), falling back to the input only when
2228
+ * the removed chip was the last one standing.
2229
+ * - **Escape** — return focus to the input.
2230
+ *
2231
+ * Click on the chip body (excluding the remove button) just focuses the
2232
+ * chip — useful as an alternative to the Backspace path.
2233
+ */
2234
+ class ForComboboxChip {
2235
+ ctx = injectComboboxContext('ForComboboxChip');
2236
+ #host = inject(ElementRef);
2237
+ /**
2238
+ * The value this chip represents — must match an entry in
2239
+ * `[forCombobox][(value)]` per the parent's `[isItemEqualToValue]`.
2240
+ * Generic over `T` (default `string`); inferred from the binding
2241
+ * (`[value]="someObject"` specializes `T`).
2242
+ */
2243
+ value = input.required(/* @ts-ignore */
2244
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
2245
+ /**
2246
+ * `data-value` reflection — for string `T` this is the value verbatim
2247
+ * (unchanged from the pre-generic behaviour); for object `T` it uses
2248
+ * the parent's `itemToFormValue` so the attribute carries the same
2249
+ * wire format as the hidden inputs (typically JSON or a per-item id).
2250
+ */
2251
+ dataValue = computed(() => {
2252
+ const v = this.value();
2253
+ return typeof v === 'string' ? v : this.ctx.itemToFormValue()(v);
2254
+ }, /* @ts-ignore */
2255
+ ...(ngDevMode ? [{ debugName: "dataValue" }] : /* istanbul ignore next */ []));
2256
+ /** Resolved label of the underlying option, used by `[forComboboxChipRemove]` for its `aria-label`. */
2257
+ label = computed(() => {
2258
+ const v = this.value();
2259
+ const equals = this.ctx.isItemEqualToValue();
2260
+ const cached = this.ctx.cachedOptions().find((o) => equals(o.value, v));
2261
+ if (cached) {
2262
+ return cached.label;
2263
+ }
2264
+ return typeof v === 'string' ? v : this.ctx.itemToStringLabel()(v);
2265
+ }, /* @ts-ignore */
2266
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2267
+ constructor() {
2268
+ const handle = {
2269
+ host: this.#host.nativeElement,
2270
+ value: this.value,
2271
+ };
2272
+ registerHandle(handle, (h) => this.ctx.registerChip(h), (h) => this.ctx.unregisterChip(h));
2273
+ }
2274
+ onKeyDown(event) {
2275
+ if (this.ctx.effectiveDisabled() || this.ctx.readonly()) {
2276
+ return;
2277
+ }
2278
+ const chips = this.ctx.chips();
2279
+ const index = chips.findIndex((c) => c.host === this.#host.nativeElement);
2280
+ if (index < 0) {
2281
+ return;
2282
+ }
2283
+ // In RTL the chip cluster lays out right-to-left, so ArrowRight must
2284
+ // move to the visually-next chip (= DOM-previous) and ArrowLeft to the
2285
+ // visually-previous one (= DOM-next), with the input sitting at the
2286
+ // visual leftmost edge.
2287
+ const rtl = this.ctx.dir() === 'rtl';
2288
+ const prevKey = rtl ? 'ArrowRight' : 'ArrowLeft';
2289
+ const nextKey = rtl ? 'ArrowLeft' : 'ArrowRight';
2290
+ switch (event.key) {
2291
+ case prevKey: {
2292
+ event.preventDefault();
2293
+ const prev = chips[index - 1];
2294
+ if (prev) {
2295
+ prev.host.focus();
2296
+ }
2297
+ break;
2298
+ }
2299
+ case nextKey: {
2300
+ event.preventDefault();
2301
+ const next = chips[index + 1];
2302
+ if (next) {
2303
+ next.host.focus();
2304
+ }
2305
+ else {
2306
+ // At the last chip (visual edge in either direction) — hop to the input.
2307
+ this.ctx.input()?.focus();
2308
+ }
2309
+ break;
2310
+ }
2311
+ case 'Backspace':
2312
+ case 'Delete': {
2313
+ event.preventDefault();
2314
+ const v = this.value();
2315
+ const prev = chips[index - 1];
2316
+ const next = chips[index + 1];
2317
+ this.ctx.removeValue(v);
2318
+ // Keep focus inside the chip cluster whenever a neighbour survives the
2319
+ // removal: prefer the previous chip, otherwise the next one (which
2320
+ // slides into the freed slot — so removing the first chip lands on the
2321
+ // new first chip rather than ejecting the user to the input). Only drop
2322
+ // focus into the input when the removed chip was the last one standing,
2323
+ // so the user can keep typing without chasing the caret.
2324
+ if (prev) {
2325
+ prev.host.focus();
2326
+ }
2327
+ else if (next) {
2328
+ next.host.focus();
2329
+ }
2330
+ else {
2331
+ this.ctx.input()?.focus();
2332
+ }
2333
+ break;
2334
+ }
2335
+ case 'Escape': {
2336
+ event.preventDefault();
2337
+ this.ctx.input()?.focus();
2338
+ break;
2339
+ }
2340
+ }
2341
+ }
2342
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxChip, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2343
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForComboboxChip, isStandalone: true, selector: "[forComboboxChip]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "tabindex": "-1" }, listeners: { "keydown": "onKeyDown($event)" }, properties: { "attr.data-value": "dataValue()", "attr.data-disabled": "ctx.effectiveDisabled() ? \"\" : null" } }, exportAs: ["forComboboxChip"], ngImport: i0 });
2344
+ }
2345
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxChip, decorators: [{
2346
+ type: Directive,
2347
+ args: [{
2348
+ selector: '[forComboboxChip]',
2349
+ exportAs: 'forComboboxChip',
2350
+ host: {
2351
+ tabindex: '-1',
2352
+ '[attr.data-value]': 'dataValue()',
2353
+ '[attr.data-disabled]': 'ctx.effectiveDisabled() ? "" : null',
2354
+ '(keydown)': 'onKeyDown($event)',
2355
+ },
2356
+ }]
2357
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }] } });
2358
+
2359
+ /**
2360
+ * Remove button inside a `[forComboboxChip]`. Apply on a
2361
+ * `<button type="button">` so click and Space / Enter (when focused) all
2362
+ * trigger the same removal flow. The button is out of the Tab cycle
2363
+ * (`tabindex="-1"`) so the user navigates the chip cluster via the chip's
2364
+ * own ArrowLeft/Right + Backspace/Delete keys; this button exists for
2365
+ * mouse / touch users.
2366
+ *
2367
+ * The host carries a generated `aria-label` ("Remove `<chip label>`")
2368
+ * derived from the parent chip's resolved option label.
2369
+ */
2370
+ class ForComboboxChipRemove {
2371
+ // The value type is opaque to chip-remove: it just forwards whatever the
2372
+ // parent chip exposes back into `removeValue`. At runtime the chip and
2373
+ // the parent `[forCombobox]` share the same `T` because the consumer
2374
+ // binds them with the same value source.
2375
+ ctx = injectComboboxContext('ForComboboxChipRemove');
2376
+ #chip = inject(ForComboboxChip, { optional: true });
2377
+ /** Disabled when the combobox is disabled or read-only — chip removal is unavailable. */
2378
+ isDisabled = computed(() => this.ctx.effectiveDisabled() || this.ctx.readonly(), /* @ts-ignore */
2379
+ ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
2380
+ constructor() {
2381
+ if (!this.#chip) {
2382
+ throw new Error('[forty-cdk/combobox] ForComboboxChipRemove must be used inside a [forComboboxChip] element.');
2383
+ }
2384
+ reflectDisabled(this.isDisabled);
2385
+ }
2386
+ ariaLabel = computed(() => `Remove ${this.#chip.label()}`, /* @ts-ignore */
2387
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
2388
+ onClick(event) {
2389
+ if (this.isDisabled()) {
2390
+ return;
2391
+ }
2392
+ // Don't let the click bubble up to the chip body — that would trigger
2393
+ // its keyboard-triggered handlers in some browsers via synthesized events.
2394
+ event.stopPropagation();
2395
+ this.ctx.removeValue(this.#chip.value());
2396
+ // Restore focus to the input so the user can continue typing.
2397
+ this.ctx.input()?.focus();
2398
+ }
2399
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxChipRemove, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2400
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxChipRemove, isStandalone: true, selector: "[forComboboxChipRemove]", host: { attributes: { "type": "button", "tabindex": "-1" }, listeners: { "click": "onClick($event)" }, properties: { "attr.aria-label": "ariaLabel()" } }, exportAs: ["forComboboxChipRemove"], ngImport: i0 });
2401
+ }
2402
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxChipRemove, decorators: [{
2403
+ type: Directive,
2404
+ args: [{
2405
+ selector: '[forComboboxChipRemove]',
2406
+ exportAs: 'forComboboxChipRemove',
2407
+ host: {
2408
+ type: 'button',
2409
+ tabindex: '-1',
2410
+ '[attr.aria-label]': 'ariaLabel()',
2411
+ '(click)': 'onClick($event)',
2412
+ },
2413
+ }]
2414
+ }], ctorParameters: () => [] });
2415
+
2416
+ /**
2417
+ * Optional grouping wrapper inside a `[forComboboxContent]`. Renders
2418
+ * `role="group"` and references the descendant `[forComboboxGroupLabel]`
2419
+ * (if any) via `aria-labelledby`. Options inside a group still register
2420
+ * with the root listbox normally, so keyboard navigation traverses across
2421
+ * groups in DOM order without special handling.
2422
+ */
2423
+ class ForComboboxGroup {
2424
+ #labelId = signal(null, /* @ts-ignore */
2425
+ ...(ngDevMode ? [{ debugName: "#labelId" }] : /* istanbul ignore next */ []));
2426
+ /** The id of the registered group label (or `null` if none). */
2427
+ labelId = this.#labelId.asReadonly();
2428
+ /** Called by `ForComboboxGroupLabel` on mount. */
2429
+ registerLabel(id) {
2430
+ this.#labelId.set(id);
2431
+ }
2432
+ /** Called by `ForComboboxGroupLabel` on destroy. Idempotent. */
2433
+ unregisterLabel(id) {
2434
+ if (this.#labelId() === id) {
2435
+ this.#labelId.set(null);
2436
+ }
2437
+ }
2438
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2439
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxGroup, isStandalone: true, selector: "[forComboboxGroup]", host: { attributes: { "role": "group" }, properties: { "attr.aria-labelledby": "labelId() || null" } }, exportAs: ["forComboboxGroup"], ngImport: i0 });
2440
+ }
2441
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxGroup, decorators: [{
2442
+ type: Directive,
2443
+ args: [{
2444
+ selector: '[forComboboxGroup]',
2445
+ exportAs: 'forComboboxGroup',
2446
+ host: {
2447
+ role: 'group',
2448
+ '[attr.aria-labelledby]': 'labelId() || null',
2449
+ },
2450
+ }]
2451
+ }] });
2452
+
2453
+ /**
2454
+ * Visible label for a `[forComboboxGroup]`. Generates a stable id and
2455
+ * registers with its parent group so the group's `aria-labelledby` points
2456
+ * at this element.
2457
+ */
2458
+ class ForComboboxGroupLabel {
2459
+ /** Stable host id used by the parent group's `aria-labelledby`. */
2460
+ id;
2461
+ constructor() {
2462
+ const group = inject(ForComboboxGroup, { optional: true });
2463
+ if (!group) {
2464
+ throw new Error('[forty-cdk/combobox] ForComboboxGroupLabel must be used inside a [forComboboxGroup] element.');
2465
+ }
2466
+ this.id = registerA11yName(group, 'for-combobox-group-label');
2467
+ }
2468
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxGroupLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2469
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxGroupLabel, isStandalone: true, selector: "[forComboboxGroupLabel]", host: { properties: { "id": "id()" } }, exportAs: ["forComboboxGroupLabel"], ngImport: i0 });
2470
+ }
2471
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxGroupLabel, decorators: [{
2472
+ type: Directive,
2473
+ args: [{
2474
+ selector: '[forComboboxGroupLabel]',
2475
+ exportAs: 'forComboboxGroupLabel',
2476
+ host: {
2477
+ '[id]': 'id()',
2478
+ },
2479
+ }]
2480
+ }], ctorParameters: () => [] });
2481
+
2482
+ /**
2483
+ * Decorative separator between options or groups inside a
2484
+ * `[forComboboxContent]`. Carries `role="separator"` and is intentionally
2485
+ * not registered with the listbox's option collection, so keyboard
2486
+ * navigation skips it automatically.
2487
+ */
2488
+ class ForComboboxSeparator {
2489
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxSeparator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2490
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForComboboxSeparator, isStandalone: true, selector: "[forComboboxSeparator]", host: { attributes: { "role": "separator", "aria-orientation": "horizontal" } }, exportAs: ["forComboboxSeparator"], ngImport: i0 });
2491
+ }
2492
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForComboboxSeparator, decorators: [{
2493
+ type: Directive,
2494
+ args: [{
2495
+ selector: '[forComboboxSeparator]',
2496
+ exportAs: 'forComboboxSeparator',
2497
+ host: {
2498
+ role: 'separator',
2499
+ 'aria-orientation': 'horizontal',
2500
+ },
2501
+ }]
2502
+ }] });
2503
+
2504
+ /**
2505
+ * Exact public names of every `ForCombobox` input, its models included. Spread it into the
2506
+ * `inputs` array of a `hostDirectives` entry so a wrapper component re-exposes the
2507
+ * primitive's full surface — the Signal Forms members `[formField]` binds among them —
2508
+ * without hand-maintaining the list. Always spread into an inline object literal as shown
2509
+ * below: the literal is what keeps the entry statically analyzable for consumers compiling
2510
+ * against the published package. An anti-drift spec fails when this list no longer matches
2511
+ * the directive's actual API. See `docs/wrapping-form-primitives.md` for both supported
2512
+ * wrapping patterns.
2513
+ *
2514
+ * @example
2515
+ * ```ts
2516
+ * @Component({
2517
+ * selector: 'div[myCombobox]',
2518
+ * template: '',
2519
+ * hostDirectives: [
2520
+ * {
2521
+ * directive: ForCombobox,
2522
+ * inputs: [...FOR_COMBOBOX_HOST_DIRECTIVE_INPUTS],
2523
+ * outputs: [...FOR_COMBOBOX_HOST_DIRECTIVE_OUTPUTS],
2524
+ * },
2525
+ * ],
2526
+ * })
2527
+ * export class MyCombobox {}
2528
+ * ```
2529
+ */
2530
+ const FOR_COMBOBOX_HOST_DIRECTIVE_INPUTS = [
2531
+ 'query',
2532
+ 'value',
2533
+ 'open',
2534
+ 'align',
2535
+ 'alignOffset',
2536
+ 'ariaLabel',
2537
+ 'arrowPadding',
2538
+ 'autoHighlight',
2539
+ 'autocompleteMode',
2540
+ 'avoidCollisions',
2541
+ 'clearOnQueryChange',
2542
+ 'clipUntilPositioned',
2543
+ 'collisionPadding',
2544
+ 'commitOnSelect',
2545
+ 'dir',
2546
+ 'dirty',
2547
+ 'disabled',
2548
+ 'dismissible',
2549
+ 'errors',
2550
+ 'hideWhenDetached',
2551
+ 'invalid',
2552
+ 'isItemEqualToValue',
2553
+ 'itemToFormValue',
2554
+ 'itemToStringLabel',
2555
+ 'loop',
2556
+ 'multiple',
2557
+ 'name',
2558
+ 'openOnFocus',
2559
+ 'openOnQuery',
2560
+ 'pending',
2561
+ 'readonly',
2562
+ 'required',
2563
+ 'returnFocus',
2564
+ 'side',
2565
+ 'sideOffset',
2566
+ 'sticky',
2567
+ 'totalCount',
2568
+ 'visibleRange',
2569
+ 'touched',
2570
+ ];
2571
+ /**
2572
+ * Exact public names of every `ForCombobox` output, the Signal Forms `touch` output
2573
+ * included. Spread it into the `outputs` array of the same `hostDirectives` entry as
2574
+ * {@link FOR_COMBOBOX_HOST_DIRECTIVE_INPUTS}.
2575
+ */
2576
+ const FOR_COMBOBOX_HOST_DIRECTIVE_OUTPUTS = [
2577
+ 'queryChange',
2578
+ 'valueChange',
2579
+ 'openChange',
2580
+ 'scrollToIndex',
2581
+ 'escapeKeyDown',
2582
+ 'pointerDownOutside',
2583
+ 'focusOutside',
2584
+ 'interactOutside',
2585
+ 'autoFocusOnOpen',
2586
+ 'autoFocusOnClose',
2587
+ 'touchedChange',
2588
+ 'touch',
2589
+ ];
2590
+
2591
+ /**
2592
+ * Generated bundle index. Do not edit.
2593
+ */
2594
+
2595
+ export { FOR_COMBOBOX_CONTEXT, FOR_COMBOBOX_DEFAULTS, FOR_COMBOBOX_HOST_DIRECTIVE_INPUTS, FOR_COMBOBOX_HOST_DIRECTIVE_OUTPUTS, FOR_COMBOBOX_OPTION, ForCombobox, ForComboboxAnchor, ForComboboxChip, ForComboboxChipRemove, ForComboboxChips, ForComboboxClear, ForComboboxContent, ForComboboxEmpty, ForComboboxGroup, ForComboboxGroupLabel, ForComboboxIndicator, ForComboboxInput, ForComboboxList, ForComboboxOption, ForComboboxSeparator, ForComboboxStatus, ForComboboxTrigger, provideForComboboxDefaults };
2596
+ //# sourceMappingURL=forty-cdk-combobox.mjs.map