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,1661 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, computed, model, input, booleanAttribute, numberAttribute, output, signal, linkedSignal, effect, Directive, ElementRef } from '@angular/core';
3
+ import { createDefaults, VirtualizedNavigator, readEntryGuarded, FormUiControlBase, IdGenerator, injectTypeahead, defaultItemToFormValue, singleSelected, injectTextDirection, ListboxOverlayController, isRequiredInputUnset, injectHiddenInput, isInArray, toggleInArray, findTypeaheadMatch, resolveListNavigation, registerHandle, reflectDisabled, injectModalShell, injectOverlayShell, hostId, registerA11yName } from 'forty-cdk/core';
4
+
5
+ const FOR_SELECT_CONTEXT = new InjectionToken('FOR_SELECT_CONTEXT');
6
+ function injectSelectContext(piece) {
7
+ const ctx = inject(FOR_SELECT_CONTEXT, { optional: true });
8
+ if (!ctx) {
9
+ throw new Error(`[forty-cdk/select] ${piece} must be used inside a [forSelect] element. ` +
10
+ "If it is declared inside an ng-template, DI resolves at the template's declaration site — " +
11
+ 'not where it is stamped (e.g. via ngTemplateOutlet) — so declare the template inside the ' +
12
+ '[forSelect] root.');
13
+ }
14
+ return ctx;
15
+ }
16
+ /**
17
+ * Resolves the trigger's root context: the explicit reference when the
18
+ * `[forSelectTrigger]` input carries one, the injected `FOR_SELECT_CONTEXT`
19
+ * otherwise. The orphan error only fires when neither resolves, on first read
20
+ * of the returned signal. Must be called in an injection context.
21
+ */
22
+ function injectSelectTriggerContext(explicitRoot) {
23
+ const injected = inject(FOR_SELECT_CONTEXT, { optional: true });
24
+ return computed(() => {
25
+ const explicit = explicitRoot();
26
+ if (explicit !== '') {
27
+ return explicit;
28
+ }
29
+ if (injected) {
30
+ return injected;
31
+ }
32
+ throw new Error('[forty-cdk/select] ForSelectTrigger could not resolve its [forSelect] root: ' +
33
+ 'no FOR_SELECT_CONTEXT provider is visible and no explicit root reference was passed. ' +
34
+ "If this trigger is declared inside an ng-template, DI resolves at the template's declaration " +
35
+ 'site — not where it is stamped — so either declare the template inside the root or pass the ' +
36
+ 'root explicitly: [forSelectTrigger]="root" with #root="forSelect".');
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Library fallback for select defaults, read at the root injector when no
42
+ * consumer has called `provideForSelectDefaults`. Exported for the shared defaults
43
+ * contract spec; not re-exported from the primitive's public entry.
44
+ */
45
+ const FOR_SELECT_FALLBACK_DEFAULTS = {
46
+ sideOffset: 4,
47
+ collisionPadding: 8,
48
+ };
49
+ const { token, provideDefaults } = createDefaults('FOR_SELECT_DEFAULTS', FOR_SELECT_FALLBACK_DEFAULTS);
50
+ /** Token holding the resolved select defaults for the current scope. */
51
+ const FOR_SELECT_DEFAULTS = token;
52
+ /**
53
+ * Configures forty-cdk select defaults for this injector scope. Partial
54
+ * overrides inherit unspecified keys from the parent scope (or library
55
+ * defaults at the root).
56
+ */
57
+ function provideForSelectDefaults(defaults = {}) {
58
+ return provideDefaults(defaults);
59
+ }
60
+
61
+ /**
62
+ * Virtualization navigation engine for `ForSelect`. A thin adapter over the
63
+ * shared `_internal/virtualized-navigator` engine: it maps the select option
64
+ * handle onto the engine's accessors (the option carries its raw `value` so the
65
+ * root can resolve the committed option's index on open) and reads the option's
66
+ * `value` through the single NG0950 read guard.
67
+ *
68
+ * Internal — not re-exported from `select/index.ts` or `public-api.ts`.
69
+ */
70
+ class SelectVirtualizedNavigator {
71
+ #core;
72
+ constructor(deps) {
73
+ this.#core = new VirtualizedNavigator({ ...deps, loop: deps.loop }, {
74
+ posOf: (o) => o.posInSet(),
75
+ idOf: (o) => o.id(),
76
+ hostOf: (o) => o.host,
77
+ readEntry: (o) => readEntryGuarded(() => ({ id: o.id(), value: o.value(), disabled: o.disabled() })),
78
+ });
79
+ }
80
+ /** @see VirtualizedNavigator.prime */
81
+ prime() {
82
+ this.#core.prime();
83
+ }
84
+ /** @see VirtualizedNavigator.tryResolvePending */
85
+ tryResolvePending() {
86
+ return this.#core.tryResolvePending();
87
+ }
88
+ /** @see VirtualizedNavigator.navigate */
89
+ navigate(direction) {
90
+ this.#core.navigate(direction);
91
+ }
92
+ /**
93
+ * Read-only position snapshot for the root's committed-index resolution.
94
+ * Keyed by absolute `posInSet`; persists across close → reopen while
95
+ * `totalCount` is unchanged.
96
+ */
97
+ snapshotByPos() {
98
+ return this.#core.snapshotByPos();
99
+ }
100
+ /** @see VirtualizedNavigator.seedActive */
101
+ seedActive(index) {
102
+ this.#core.seedActive(index);
103
+ }
104
+ /** @see VirtualizedNavigator.resetPending */
105
+ resetPending() {
106
+ this.#core.resetPending();
107
+ }
108
+ }
109
+
110
+ /** Sentinel for an option handle whose `input.required` `[value]` is not yet written. */
111
+ const NO_VALUE = Symbol('forty-cdk/select:no-value');
112
+ /**
113
+ * Headless implementation of the [WAI-ARIA select-only combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/).
114
+ * Implements `FormValueControl<readonly T[]>` from
115
+ * `@angular/forms/signals` for `[formField]` auto-wiring.
116
+ *
117
+ * Generic over the option value type `T` (default `string`). When the
118
+ * consumer binds object items the directive infers `T` from `[(value)]` and
119
+ * `[forSelectOption][value]`; object identity is resolved by the
120
+ * consumer-supplied `[isItemEqualToValue]` and the hidden inputs serialize
121
+ * via `[itemToFormValue]`. Option display text is read from the rendered
122
+ * `textContent`, so no separate label function is needed — supply the
123
+ * optional `[itemToLabel]` only when a pre-set object value must render its
124
+ * label before the listbox is ever opened (the documented `@if (open())`
125
+ * pattern).
126
+ *
127
+ * Selection is always modeled as `readonly T[]`:
128
+ * - In single mode (`multiple=false`, default), the array has 0 or 1 element
129
+ * and option activation closes the listbox.
130
+ * - In multi mode, option activation toggles and the listbox stays open.
131
+ *
132
+ * Typeahead has two modes mirroring native `<select>`:
133
+ * - **Closed trigger** (single mode only): printable keys select the matching
134
+ * option immediately without opening the listbox.
135
+ * - **Open listbox**: printable keys move focus to the first matching option
136
+ * (selection still requires Enter / Space / click).
137
+ */
138
+ class ForSelect extends FormUiControlBase {
139
+ #idGen = inject(IdGenerator);
140
+ #typeahead = injectTypeahead();
141
+ #closedTypeahead = injectTypeahead();
142
+ #defaults = inject(FOR_SELECT_DEFAULTS);
143
+ /**
144
+ * Two-way bindable. Selected option values. Single-mode keeps 0 or 1
145
+ * element. The `model()` change emitter (`(valueChange)`) fires only on
146
+ * internal selection changes, never on consumer writes via `[(value)]`.
147
+ */
148
+ value = model([], /* @ts-ignore */
149
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
150
+ /**
151
+ * Compare two items for equality. Defaults to `===`, which is the
152
+ * correct identity for primitive `T` (e.g. strings, numbers). Override
153
+ * when binding object items so the directive can locate selected entries
154
+ * by id (or any other stable key) instead of by reference:
155
+ * `[isItemEqualToValue]="(a, b) => a.id === b.id"`.
156
+ */
157
+ isItemEqualToValue = input((a, b) => a === b, /* @ts-ignore */
158
+ ...(ngDevMode ? [{ debugName: "isItemEqualToValue" }] : /* istanbul ignore next */ []));
159
+ /**
160
+ * Serialize an item for the hidden input that participates in native
161
+ * form submission. Defaults to identity for strings and to
162
+ * `JSON.stringify` for non-string items so the primitive works out of
163
+ * the box round-tripping objects. Override to emit a specific wire
164
+ * format — typically a per-item id — when the backend expects that:
165
+ * `[itemToFormValue]="(it) => it.id"`.
166
+ */
167
+ itemToFormValue = input(defaultItemToFormValue, /* @ts-ignore */
168
+ ...(ngDevMode ? [{ debugName: "itemToFormValue" }] : /* istanbul ignore next */ []));
169
+ /**
170
+ * Resolve the display label for an item without the listbox mounted.
171
+ * When set, {@link selectedLabels} (and therefore `[forSelectValue]`)
172
+ * renders this for any selected value, so a pre-set object value shows
173
+ * its label on first paint — before the listbox has ever been opened, in
174
+ * the documented `@if (forSelect.open())` pattern. Without it, object-value
175
+ * labels resolve from the rendered option `textContent`, which is only
176
+ * available once the content mounts; the serialized form value is shown as
177
+ * a last-resort fallback in the meantime: `[itemToLabel]="(c) => c.name"`.
178
+ *
179
+ * Defaults to `undefined` (string mode renders the value verbatim, so no
180
+ * label function is needed).
181
+ */
182
+ itemToLabel = input(undefined, /* @ts-ignore */
183
+ ...(ngDevMode ? [{ debugName: "itemToLabel" }] : /* istanbul ignore next */ []));
184
+ /**
185
+ * Read-only single-select convenience view of {@link value}. Returns the
186
+ * sole value when exactly one is selected (regardless of `multiple`),
187
+ * otherwise `null` (zero, or 2+ selected). Lets single-select consumers
188
+ * read `selected()` instead of unwrapping `value()[0]`. The array-backed
189
+ * `value` model remains the source of truth and the `FormValueControl`
190
+ * contract; this is a derived accessor.
191
+ */
192
+ selected = singleSelected(this.value);
193
+ /**
194
+ * Two-way bindable. Whether the listbox is currently shown. The `model()`
195
+ * change emitter (`(openChange)`) fires only on internal transitions
196
+ * (trigger toggle, Escape, outside dismissal, single-mode option select),
197
+ * never on consumer writes via `[(open)]`.
198
+ */
199
+ open = model(false, /* @ts-ignore */
200
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
201
+ multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
202
+ /**
203
+ * Presentation mode. When `true`, `[forSelectContent]` mounts as a trapped /
204
+ * inert / scroll-locked modal surface (routed through `_internal/modal-shell`)
205
+ * instead of the default anchored popover — the batteries-included touch
206
+ * presentation a consumer opts into with `[modal]="isCoarsePointer()"`. The
207
+ * form-value wiring (`[(value)]`, `name`) is unchanged.
208
+ *
209
+ * Read once when the content mounts (the two shells are structurally
210
+ * different; switching at runtime would need a remount, and the surface
211
+ * mounts lazily via `@if (open())` well after `modal` settles). Every
212
+ * anchored-positioning input — `position`, `side`, `align`, `sideOffset`,
213
+ * `alignOffset`, `sticky`, `hideWhenDetached`, `clipUntilPositioned`, `avoidCollisions`,
214
+ * `collisionPadding`, `arrowPadding` — is a no-op in this mode. Default
215
+ * `false` (non-breaking). The swipe / snap-point sheet is deliberately not
216
+ * this mode — compose a `ForListbox` inside a `ForDrawer` for that.
217
+ */
218
+ modal = input(false, { ...(ngDevMode ? { debugName: "modal" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
219
+ /**
220
+ * Positioning algorithm.
221
+ *
222
+ * - `'popper'` (default): standard floating-ui anchored placement using
223
+ * `side` / `align` / `sideOffset` / `alignOffset`, with `flip` + `shift`
224
+ * collision handling. Same path as Popover / DropdownMenu.
225
+ * - `'item-aligned'`: the listbox overlays the trigger so the selected
226
+ * option's vertical center aligns with the trigger's vertical center
227
+ * — visually the menu "snaps over" the trigger when opened, mirroring
228
+ * macOS native `<select>`. Falls back to the first enabled option when
229
+ * nothing is selected. `side`, `align`, `sideOffset`, `alignOffset`,
230
+ * `sticky`, `hideWhenDetached`, `clipUntilPositioned`, and `avoidCollisions` are ignored in
231
+ * this mode; only `collisionPadding` is honored.
232
+ */
233
+ position = input('popper', /* @ts-ignore */
234
+ ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
235
+ /**
236
+ * Side the listbox is anchored to. Defaults to `'bottom'`. Pair with
237
+ * `align` for the full positioning API. Ignored when
238
+ * `position="item-aligned"`.
239
+ */
240
+ side = input('bottom', /* @ts-ignore */
241
+ ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
242
+ /** Alignment along the chosen `side`. Defaults to `'start'`. */
243
+ align = input('start', /* @ts-ignore */
244
+ ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
245
+ /**
246
+ * Gap (px) between trigger and listbox along the main axis. Default `4`.
247
+ * The default is read from
248
+ * `provideForSelectDefaults` for the surrounding scope.
249
+ */
250
+ sideOffset = input(this.#defaults.sideOffset, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
251
+ /** Gap (px) along the cross axis. Default `0`. */
252
+ alignOffset = input(0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
253
+ /** When `true` (default), `flip` and `shift` keep the listbox inside the viewport. */
254
+ avoidCollisions = input(true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
255
+ /**
256
+ * Padding (px) applied uniformly to flip / shift / size. Default `8`.
257
+ * The default is read from `provideForSelectDefaults` for the surrounding
258
+ * scope.
259
+ */
260
+ collisionPadding = input(this.#defaults.collisionPadding, { ...(ngDevMode ? { debugName: "collisionPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
261
+ /** Padding (px) for the `arrow` middleware. Default `0`. */
262
+ arrowPadding = input(0, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
263
+ /** Stickiness behaviour for `shift`. Default `'partial'`. */
264
+ sticky = input('partial', /* @ts-ignore */
265
+ ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
266
+ /** When `true`, sets `data-detached=""` while the trigger is scrolled off-screen. */
267
+ hideWhenDetached = input(false, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
268
+ /**
269
+ * When `true` (default), the content is clipped until floating-ui resolves
270
+ * its first position, preventing a flash at the viewport corner. Set to
271
+ * `false` so a dramatic `animate.enter` plays from its first frame (the
272
+ * surface may flash briefly at the unresolved position while positioning
273
+ * computes).
274
+ */
275
+ clipUntilPositioned = input(true, { ...(ngDevMode ? { debugName: "clipUntilPositioned" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
276
+ loop = input(true, { ...(ngDevMode ? { debugName: "loop" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
277
+ orientation = input('vertical', /* @ts-ignore */
278
+ ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
279
+ /**
280
+ * Total number of items in the source data. When set, switches the listbox
281
+ * to the virtualized `aria-activedescendant` focus model and populates
282
+ * `aria-setsize` on each rendered option. Leave unset (default) for the
283
+ * standard DOM-focus model.
284
+ */
285
+ totalCount = input(undefined, { ...(ngDevMode ? { debugName: "totalCount" } : /* istanbul ignore next */ {}), transform: (v) => (v == null ? undefined : numberAttribute(v)) });
286
+ /**
287
+ * Inclusive-exclusive `[start, end)` index range of the currently rendered
288
+ * options, provided by `injectVirtualizer`. Used to decide whether a
289
+ * navigation target is in the visible window.
290
+ */
291
+ visibleRange = input(undefined, /* @ts-ignore */
292
+ ...(ngDevMode ? [{ debugName: "visibleRange" }] : /* istanbul ignore next */ []));
293
+ /**
294
+ * Emitted when navigation (or open-time scroll-to-selected) reaches an
295
+ * option outside the rendered window. Pass to `injectVirtualizer`'s
296
+ * `scrollToIndex` so the correct option mounts.
297
+ */
298
+ scrollToIndex = output();
299
+ /**
300
+ * Writing direction. When unset (default `null`), the inherited ambient
301
+ * direction is resolved from the nearest ancestor carrying a `dir` attribute
302
+ * (or `<html dir>`), defaulting to `'ltr'`. An explicit `[dir]` always wins.
303
+ * The resolved value is reflected to the host `dir` attribute.
304
+ */
305
+ _dirInput = input(null, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
306
+ dir = injectTextDirection(this._dirInput);
307
+ /**
308
+ * Single-mode only. When true, arrow nav also selects the focused option
309
+ * while the listbox is open. APG calls this optional and recommends
310
+ * caution — leave off unless your UX truly benefits. Default `false`.
311
+ */
312
+ selectionFollowsFocus = input(false, { ...(ngDevMode ? { debugName: "selectionFollowsFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
313
+ /** Placeholder shown by `[forSelectValue]` when no option is selected. */
314
+ placeholder = input('', /* @ts-ignore */
315
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
316
+ /** When true (default), Escape, pointer-down outside, and focus outside close the listbox. */
317
+ dismissible = input(true, { ...(ngDevMode ? { debugName: "dismissible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
318
+ /** When true (default), focus returns to the trigger on close. */
319
+ returnFocus = input(true, { ...(ngDevMode ? { debugName: "returnFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
320
+ /** Manual `aria-label` on `[forSelectContent]` when the trigger isn't a meaningful name. */
321
+ ariaLabel = input(null, /* @ts-ignore */
322
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
323
+ escapeKeyDown = output();
324
+ pointerDownOutside = output();
325
+ focusOutside = output();
326
+ interactOutside = output();
327
+ /**
328
+ * Fires just before the listbox sends focus to the selected option
329
+ * (or first / last enabled) on mount. Call `preventDefault()` on the
330
+ * emitted veto to skip the imperative focus move.
331
+ */
332
+ autoFocusOnOpen = output();
333
+ /**
334
+ * Fires just before focus returns to the trigger on unmount. Call
335
+ * `preventDefault()` on the veto to suppress the return-focus.
336
+ */
337
+ autoFocusOnClose = output();
338
+ #virtualized = computed(() => this.totalCount() !== undefined, /* @ts-ignore */
339
+ ...(ngDevMode ? [{ debugName: "#virtualized" }] : /* istanbul ignore next */ []));
340
+ #activeId = signal(null, /* @ts-ignore */
341
+ ...(ngDevMode ? [{ debugName: "#activeId" }] : /* istanbul ignore next */ []));
342
+ /**
343
+ * Shared overlay-listbox state machine: option collection, trigger / anchor /
344
+ * content registries + ids, DOM-focus navigation, the open / close machine,
345
+ * the initial-focus / close-reason state, and the dismiss / auto-focus emit
346
+ * forwarders. The value-specific behaviour (`isSelected`, `activate`,
347
+ * `focusSelectedOption`, typeahead, the virtualized activedescendant path,
348
+ * `commitOnTab`'s value set) stays in this root; close-time virtualized
349
+ * cleanup and the post-navigate scroll / `selectionFollowsFocus` move are
350
+ * threaded through the controller's side-effect callbacks.
351
+ */
352
+ #controller = new ListboxOverlayController(this.#idGen, {
353
+ idPrefix: 'for-select',
354
+ multipleAnchorsError: '[forty-cdk/select] Multiple [forSelectAnchor] inside the same [forSelect]; only one is allowed.',
355
+ defaultInitialFocus: 'selected',
356
+ effectiveDisabled: this.effectiveDisabled,
357
+ setOpen: (open) => this.open.set(open),
358
+ isOpen: () => this.open(),
359
+ emit: {
360
+ escapeKeyDown: this.escapeKeyDown,
361
+ pointerDownOutside: this.pointerDownOutside,
362
+ focusOutside: this.focusOutside,
363
+ interactOutside: this.interactOutside,
364
+ autoFocusOnOpen: this.autoFocusOnOpen,
365
+ autoFocusOnClose: this.autoFocusOnClose,
366
+ },
367
+ markTouched: () => this.markTouched(),
368
+ onClose: () => {
369
+ if (this.#virtualized()) {
370
+ this.#activeId.set(null);
371
+ this.#navigator?.resetPending();
372
+ }
373
+ },
374
+ onNavigateFocus: (target) => {
375
+ target.host.scrollIntoView?.({ block: 'nearest' });
376
+ if (!this.multiple() && this.selectionFollowsFocus() && !this.readonly()) {
377
+ this.#setSingle(target.value());
378
+ }
379
+ },
380
+ onUnregisterOption: (handle) => {
381
+ if (this.#virtualized() && this.#activeId() === handle.id()) {
382
+ this.#activeId.set(null);
383
+ }
384
+ },
385
+ });
386
+ triggerId = this.#controller.triggerId;
387
+ contentId = this.#controller.contentId;
388
+ initialFocus = this.#controller.initialFocus;
389
+ lastCloseReason = this.#controller.lastCloseReason;
390
+ trigger = this.#controller.trigger;
391
+ /**
392
+ * Element floating-ui anchors the listbox against. Prefers an optional
393
+ * `[forSelectAnchor]` when registered, otherwise falls back to the trigger
394
+ * so existing selects without an anchor keep their behavior. Decoupled from
395
+ * `trigger` so the trigger keeps driving `aria-controls`, the click toggle,
396
+ * focus return, and its dismissal exemption regardless of where the listbox
397
+ * paints.
398
+ */
399
+ anchor = this.#controller.anchor;
400
+ content = this.#controller.content;
401
+ options = this.#controller.options;
402
+ /** The active option's `id` in the virtualized path, `null` in the default path. */
403
+ activeDescendantId = computed(() => this.#virtualized() ? this.#activeId() : null, /* @ts-ignore */
404
+ ...(ngDevMode ? [{ debugName: "activeDescendantId" }] : /* istanbul ignore next */ []));
405
+ #navigator = null;
406
+ #requireNavigator() {
407
+ return (this.#navigator ??= new SelectVirtualizedNavigator({
408
+ items: this.options,
409
+ totalCount: this.totalCount,
410
+ visibleRange: this.visibleRange,
411
+ loop: this.loop,
412
+ getActiveId: () => this.#activeId(),
413
+ setActiveId: (id) => this.#activeId.set(id),
414
+ emitScrollToIndex: (idx) => this.scrollToIndex.emit(idx),
415
+ }));
416
+ }
417
+ /**
418
+ * Persisted snapshot of the registered options as `{ value, label }` tuples,
419
+ * keyed internally by serialized form value. Drives closed-state typeahead
420
+ * and `[forSelectValue]` label rendering — the live `options` registry is
421
+ * empty whenever `[forSelectContent]` is unmounted, so the snapshot carries
422
+ * the last non-empty option set across close → re-open cycles.
423
+ *
424
+ * A `linkedSignal` that folds the live option set on every `options()`
425
+ * change: when the listbox unmounts (`items().length === 0`) it returns the
426
+ * previous accumulator unchanged so labels stay resolvable while closed. Each
427
+ * option's `label` is itself a `Signal<string>`, so this never reads
428
+ * `textContent` from inside the fold — the canonical replacement for the
429
+ * previous `afterEveryRender(() => signal.set(...))` snapshot (no
430
+ * state-propagation inside an `effect`). A statically-rendered option that
431
+ * registers before its `[value]` binding is written is skipped this fold and
432
+ * folded in on the re-run the binding triggers (see {@link #readHandleValue}).
433
+ */
434
+ #cachedOptions = linkedSignal({ ...(ngDevMode ? { debugName: "#cachedOptions" } : /* istanbul ignore next */ {}), source: () => this.options(),
435
+ computation: (items, prev) => {
436
+ if (items.length === 0) {
437
+ return prev?.value ?? [];
438
+ }
439
+ const toFormValue = this.itemToFormValue();
440
+ const merged = new Map();
441
+ for (const entry of prev?.value ?? []) {
442
+ merged.set(toFormValue(entry.value), entry);
443
+ }
444
+ for (const item of items) {
445
+ const value = this.#readHandleValue(item);
446
+ if (value === NO_VALUE) {
447
+ continue;
448
+ }
449
+ merged.set(toFormValue(value), { value, label: item.label() });
450
+ }
451
+ return [...merged.values()];
452
+ } });
453
+ /**
454
+ * Trimmed display labels of the selected values, in selection order. Pure
455
+ * derivation: when `[itemToLabel]` is supplied it is authoritative; otherwise
456
+ * resolve from the persisted option snapshot, then the serialized form value
457
+ * so non-string items still render meaningfully on a cold cache.
458
+ *
459
+ * The snapshot's labels come from each option's reactive `label` signal,
460
+ * which reads the rendered `textContent`. `textContent` is not a signal, so a
461
+ * label whose rendered text changes without a value change does not self-heal
462
+ * here — supply `[itemToLabel]` for a pure signal derivation that observes
463
+ * label changes directly.
464
+ */
465
+ selectedLabels = computed(() => {
466
+ const values = this.value();
467
+ if (values.length === 0) {
468
+ return [];
469
+ }
470
+ // When `[itemToLabel]` is supplied it is authoritative: the label resolves
471
+ // without the listbox mounted, so a pre-set object value renders correctly
472
+ // on first paint in the documented `@if (open())` pattern and never flickers
473
+ // from a serialized id to the real label once the listbox is first opened.
474
+ const itemToLabel = this.itemToLabel();
475
+ if (itemToLabel) {
476
+ return values.map(itemToLabel);
477
+ }
478
+ const cached = this.#cachedOptions();
479
+ const toFormValue = this.itemToFormValue();
480
+ const cachedByKey = new Map();
481
+ for (const o of cached) {
482
+ cachedByKey.set(toFormValue(o.value), o);
483
+ }
484
+ const labels = [];
485
+ for (const v of values) {
486
+ const key = toFormValue(v);
487
+ const opt = cachedByKey.get(key);
488
+ labels.push(opt ? opt.label : typeof v === 'string' ? v : key);
489
+ }
490
+ return labels;
491
+ }, /* @ts-ignore */
492
+ ...(ngDevMode ? [{ debugName: "selectedLabels" }] : /* istanbul ignore next */ []));
493
+ selectedOptionEl = computed(() => {
494
+ const values = this.value();
495
+ if (values.length === 0) {
496
+ return null;
497
+ }
498
+ const items = this.options();
499
+ const toFormValue = this.itemToFormValue();
500
+ const byKey = new Map();
501
+ for (const o of items) {
502
+ byKey.set(toFormValue(o.value()), o);
503
+ }
504
+ for (const v of values) {
505
+ const opt = byKey.get(toFormValue(v));
506
+ if (opt) {
507
+ return opt.host;
508
+ }
509
+ }
510
+ return null;
511
+ }, /* @ts-ignore */
512
+ ...(ngDevMode ? [{ debugName: "selectedOptionEl" }] : /* istanbul ignore next */ []));
513
+ fieldLabelledElement() {
514
+ return this.trigger();
515
+ }
516
+ fieldLabelledElementId() {
517
+ return this.triggerId();
518
+ }
519
+ /**
520
+ * Read an option handle's `value()` inside the snapshot fold, tolerating the
521
+ * NG0950 thrown while a statically-rendered option is between registering
522
+ * (its constructor, during the content view's creation pass) and having its
523
+ * `input.required` `[value]` binding written (that view's update pass). The
524
+ * fold's prime read can run in that gap, so a static option above a `@for`
525
+ * list would otherwise hard-crash on open. Returns {@link NO_VALUE} in that
526
+ * window; the fold skips the option and folds it in on the re-run the binding
527
+ * triggers (the required-input producer is accessed before the read throws,
528
+ * so the dependency is still tracked). Any non-NG0950 error propagates.
529
+ */
530
+ #readHandleValue(item) {
531
+ try {
532
+ return item.value();
533
+ }
534
+ catch (error) {
535
+ if (isRequiredInputUnset(error)) {
536
+ return NO_VALUE;
537
+ }
538
+ throw error;
539
+ }
540
+ }
541
+ constructor() {
542
+ super();
543
+ injectHiddenInput({
544
+ name: this.name,
545
+ values: this.value,
546
+ serialize: (item) => this.itemToFormValue()(item),
547
+ disabled: this.effectiveDisabled,
548
+ });
549
+ // Prime the snapshot fold while the listbox is open so its `linkedSignal`
550
+ // `prev` slot is seeded with the live options before they unmount — closed-
551
+ // state typeahead and `[forSelectValue]` then resolve labels even when no
552
+ // `[forSelectValue]` pulls the cache during the open cycle. A read, not a
553
+ // write: this is a legitimate side effect (forcing the lazy fold to run),
554
+ // not state propagation inside an `effect`.
555
+ effect(() => {
556
+ if (this.open()) {
557
+ this.#cachedOptions();
558
+ }
559
+ });
560
+ effect(() => {
561
+ this.options();
562
+ if (!this.#virtualized()) {
563
+ return;
564
+ }
565
+ const navigator = this.#requireNavigator();
566
+ navigator.prime();
567
+ navigator.tryResolvePending();
568
+ });
569
+ }
570
+ setInitialFocus(target) {
571
+ this.#controller.setInitialFocus(target);
572
+ }
573
+ registerTrigger(el) {
574
+ this.#controller.registerTrigger(el);
575
+ }
576
+ unregisterTrigger(el) {
577
+ this.#controller.unregisterTrigger(el);
578
+ }
579
+ registerAnchor(el) {
580
+ this.#controller.registerAnchor(el);
581
+ }
582
+ unregisterAnchor(el) {
583
+ this.#controller.unregisterAnchor(el);
584
+ }
585
+ registerContent(el) {
586
+ this.#controller.registerContent(el);
587
+ }
588
+ unregisterContent(el) {
589
+ this.#controller.unregisterContent(el);
590
+ }
591
+ registerOption(handle) {
592
+ this.#controller.registerOption(handle);
593
+ }
594
+ unregisterOption(handle) {
595
+ this.#controller.unregisterOption(handle);
596
+ }
597
+ isSelected(v) {
598
+ return isInArray(this.value(), v, this.isItemEqualToValue());
599
+ }
600
+ #setSingle(v) {
601
+ if (this.value().length === 1 && this.isSelected(v)) {
602
+ return;
603
+ }
604
+ this.value.set([v]);
605
+ }
606
+ activate(v) {
607
+ if (this.effectiveDisabled() || this.readonly()) {
608
+ return;
609
+ }
610
+ if (this.multiple()) {
611
+ this.value.set(toggleInArray(this.value(), v, this.isItemEqualToValue()));
612
+ // Multi-select stays open — consumer closes via outside pointer / Escape / Tab.
613
+ return;
614
+ }
615
+ // Single-mode: idempotent select + close. Skip the redundant set (and its
616
+ // `valueChange` emission) when the same sole value is already selected.
617
+ this.#setSingle(v);
618
+ this.closeMenu('select');
619
+ }
620
+ navigate(currentOption, action) {
621
+ this.#controller.navigate(currentOption, action, this.loop());
622
+ }
623
+ handleTypeahead(event) {
624
+ if (!this.#typeahead.handle(event)) {
625
+ return;
626
+ }
627
+ const match = findTypeaheadMatch(this.options(), { buffer: this.#typeahead.buffer(), repeated: false, anchorIndex: -1 }, (o) => o.host.textContent ?? '', (o) => o.disabled());
628
+ match?.host.focus();
629
+ }
630
+ handleClosedTypeahead(event) {
631
+ // Only single-mode replicates native <select>'s "type-to-select" behavior.
632
+ // Multi-select is ambiguous (which one wins?) — caller falls back to opening.
633
+ if (this.multiple() || this.effectiveDisabled() || this.readonly()) {
634
+ return false;
635
+ }
636
+ if (!this.#closedTypeahead.handle(event)) {
637
+ return false;
638
+ }
639
+ const buffer = this.#closedTypeahead.buffer().toLowerCase();
640
+ if (!buffer) {
641
+ return true;
642
+ }
643
+ // Closed-state lookup goes through the cached snapshot — `[forSelectContent]`
644
+ // is unmounted, so the live `options` registry is empty here. The cache
645
+ // populates the first time the listbox opens and renders options.
646
+ const cached = this.#cachedOptions();
647
+ const match = cached.find((o) => o.label.toLowerCase().startsWith(buffer));
648
+ if (match) {
649
+ this.value.set([match.value]);
650
+ }
651
+ return true;
652
+ }
653
+ focusFirstEnabledOption() {
654
+ return this.#controller.focusFirstEnabledOption();
655
+ }
656
+ focusLastEnabledOption() {
657
+ return this.#controller.focusLastEnabledOption();
658
+ }
659
+ focusSelectedOption() {
660
+ const values = this.value();
661
+ if (values.length === 0) {
662
+ return false;
663
+ }
664
+ const equals = this.isItemEqualToValue();
665
+ const items = this.options();
666
+ for (const v of values) {
667
+ const opt = items.find((o) => equals(o.value(), v) && !o.disabled());
668
+ if (opt) {
669
+ opt.host.focus();
670
+ return true;
671
+ }
672
+ }
673
+ return false;
674
+ }
675
+ toggle(initialFocus = 'selected') {
676
+ this.#controller.toggle(initialFocus);
677
+ }
678
+ openMenu(initialFocus = 'selected') {
679
+ this.#controller.openMenu(initialFocus);
680
+ }
681
+ closeMenu(reason) {
682
+ this.#controller.closeMenu(reason);
683
+ }
684
+ commitOnTab(value) {
685
+ if (this.effectiveDisabled()) {
686
+ return;
687
+ }
688
+ if (!this.multiple() && !this.readonly()) {
689
+ this.#setSingle(value);
690
+ }
691
+ // Move focus to the trigger BEFORE the unmount + close so the browser's
692
+ // Tab default action has a stable active element to advance from. The
693
+ // content's `DestroyRef` reads `lastCloseReason() === 'tab'` and skips
694
+ // its own re-focus — otherwise it would steal focus back from wherever
695
+ // the browser advanced it.
696
+ this.#controller.focusTrigger();
697
+ this.closeMenu('tab');
698
+ }
699
+ emitEscapeKeyDown(event) {
700
+ this.#controller.emitEscapeKeyDown(event, this.dismissible(), 'escape');
701
+ }
702
+ /**
703
+ * Outside-interaction emit forwarders. The shared `#pendingOutsideVeto`
704
+ * reuse between the specific outside channels and the composite
705
+ * `interactOutside` lives in `injectOverlayShell`; these only fire the
706
+ * matching output with the veto the shell built.
707
+ */
708
+ emitPointerDownOutside(veto) {
709
+ this.#controller.emitPointerDownOutside(veto);
710
+ }
711
+ emitFocusOutside(veto) {
712
+ this.#controller.emitFocusOutside(veto);
713
+ }
714
+ emitInteractOutside(veto) {
715
+ this.#controller.emitInteractOutside(veto);
716
+ }
717
+ /** Modal-path Escape forwarder: emit only; the modal shell owns the close. */
718
+ forwardEscapeKeyDown(veto) {
719
+ this.#controller.forwardEscapeKeyDown(veto);
720
+ }
721
+ /**
722
+ * Implicit close requested by the shell after an un-vetoed outside
723
+ * interaction. Marks the control touched (mirroring the trigger blur) and
724
+ * closes with the channel's reason.
725
+ */
726
+ requestClose(reason) {
727
+ this.#controller.requestClose(reason);
728
+ }
729
+ emitAutoFocusOnOpen() {
730
+ return this.#controller.emitAutoFocusOnOpen();
731
+ }
732
+ emitAutoFocusOnClose() {
733
+ return this.#controller.emitAutoFocusOnClose();
734
+ }
735
+ markTouched() {
736
+ super.markTouched();
737
+ }
738
+ seedVirtualizedInitialFocus() {
739
+ if (!this.#virtualized()) {
740
+ return;
741
+ }
742
+ const navigator = this.#requireNavigator();
743
+ const committed = this.#committedIndex(navigator);
744
+ if (committed !== null) {
745
+ navigator.seedActive(committed);
746
+ return;
747
+ }
748
+ navigator.navigate('first');
749
+ }
750
+ handleVirtualizedKeydown(event) {
751
+ if (!this.#virtualized() || this.effectiveDisabled()) {
752
+ return;
753
+ }
754
+ if (event.key === 'Tab') {
755
+ if (this.modal()) {
756
+ return;
757
+ }
758
+ this.#commitActiveDescendantOnTab();
759
+ return;
760
+ }
761
+ if (event.key === 'Enter' || event.key === ' ') {
762
+ event.preventDefault();
763
+ this.#activateActiveDescendant();
764
+ return;
765
+ }
766
+ const action = resolveListNavigation(event, {
767
+ orientation: this.orientation(),
768
+ dir: this.dir(),
769
+ pageKeys: true,
770
+ });
771
+ if (action) {
772
+ event.preventDefault();
773
+ this.#requireNavigator().navigate(action);
774
+ return;
775
+ }
776
+ this.#typeaheadVirtualized(event);
777
+ }
778
+ notifyOptionClick(optionId) {
779
+ if (!this.#virtualized()) {
780
+ return;
781
+ }
782
+ this.#activeId.set(optionId);
783
+ this.content()?.focus();
784
+ }
785
+ #committedIndex(navigator) {
786
+ const values = this.value();
787
+ if (values.length === 0) {
788
+ return null;
789
+ }
790
+ const equals = this.isItemEqualToValue();
791
+ const snapshot = navigator.snapshotByPos();
792
+ for (const v of values) {
793
+ for (const [pos, entry] of snapshot) {
794
+ if (!entry.disabled && equals(entry.value, v)) {
795
+ return pos;
796
+ }
797
+ }
798
+ }
799
+ return null;
800
+ }
801
+ #activateActiveDescendant() {
802
+ const id = this.#activeId();
803
+ if (id === null) {
804
+ return;
805
+ }
806
+ const handle = this.options().find((o) => o.id() === id);
807
+ if (!handle || handle.disabled()) {
808
+ return;
809
+ }
810
+ this.activate(handle.value());
811
+ }
812
+ #commitActiveDescendantOnTab() {
813
+ const id = this.#activeId();
814
+ const handle = id === null ? undefined : this.options().find((o) => o.id() === id);
815
+ if (handle && !handle.disabled()) {
816
+ this.commitOnTab(handle.value());
817
+ return;
818
+ }
819
+ this.closeMenu('tab');
820
+ }
821
+ #typeaheadVirtualized(event) {
822
+ if (!this.#typeahead.handle(event)) {
823
+ return;
824
+ }
825
+ const match = findTypeaheadMatch(this.options(), { buffer: this.#typeahead.buffer(), repeated: false, anchorIndex: -1 }, (o) => o.host.textContent ?? '', (o) => o.disabled());
826
+ if (match) {
827
+ this.#activeId.set(match.id());
828
+ match.host.scrollIntoView?.({ block: 'nearest' });
829
+ }
830
+ }
831
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
832
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForSelect, isStandalone: true, selector: "[forSelect]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToFormValue: { classPropertyName: "itemToFormValue", publicName: "itemToFormValue", isSignal: true, isRequired: false, transformFunction: null }, itemToLabel: { classPropertyName: "itemToLabel", publicName: "itemToLabel", 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 }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", 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 }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, visibleRange: { classPropertyName: "visibleRange", publicName: "visibleRange", isSignal: true, isRequired: false, transformFunction: null }, _dirInput: { classPropertyName: "_dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, selectionFollowsFocus: { classPropertyName: "selectionFollowsFocus", publicName: "selectionFollowsFocus", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", 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 } }, outputs: { value: "valueChange", open: "openChange", scrollToIndex: "scrollToIndex", escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside", focusOutside: "focusOutside", interactOutside: "interactOutside", autoFocusOnOpen: "autoFocusOnOpen", autoFocusOnClose: "autoFocusOnClose" }, host: { properties: { "attr.data-state": "open() ? \"open\" : \"closed\"", "attr.data-disabled": "effectiveDisabled() ? \"\" : null", "attr.dir": "dir()" } }, providers: [{ provide: FOR_SELECT_CONTEXT, useExisting: ForSelect }], exportAs: ["forSelect"], usesInheritance: true, ngImport: i0 });
833
+ }
834
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelect, decorators: [{
835
+ type: Directive,
836
+ args: [{
837
+ selector: '[forSelect]',
838
+ exportAs: 'forSelect',
839
+ host: {
840
+ '[attr.data-state]': 'open() ? "open" : "closed"',
841
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
842
+ '[attr.dir]': 'dir()',
843
+ },
844
+ providers: [{ provide: FOR_SELECT_CONTEXT, useExisting: ForSelect }],
845
+ }]
846
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToFormValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToFormValue", required: false }] }], itemToLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToLabel", 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 }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], align: [{ 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 }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], totalCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalCount", required: false }] }], visibleRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "visibleRange", required: false }] }], scrollToIndex: [{ type: i0.Output, args: ["scrollToIndex"] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], selectionFollowsFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionFollowsFocus", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", 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 }] }], 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"] }] } });
847
+
848
+ /**
849
+ * Optional positioning anchor. When present, `[forSelectContent]` is
850
+ * positioned against this element instead of `[forSelectTrigger]` — useful
851
+ * when the trigger lives inside a decorated field box (padding, prefix icon,
852
+ * clear / chevron buttons) and the listbox should match the visible field
853
+ * rather than the inner button.
854
+ *
855
+ * Only positioning changes: the trigger still owns `aria-controls`,
856
+ * `aria-expanded`, the click toggle, focus return on close, and its exemption
857
+ * from outside-pointer dismissal. If no anchor is registered the listbox falls
858
+ * back to anchoring against the trigger, so existing usages are unaffected.
859
+ *
860
+ * At most one `[forSelectAnchor]` may be registered per `[forSelect]`; a second
861
+ * one throws.
862
+ *
863
+ * ```html
864
+ * <div forSelect [(value)]="v">
865
+ * <div forSelectAnchor class="field-box">
866
+ * <icon />
867
+ * <button forSelectTrigger>…</button>
868
+ * <button class="clear">×</button>
869
+ * </div>
870
+ * @if (open()) {
871
+ * <div forSelectContent>…</div>
872
+ * }
873
+ * </div>
874
+ * ```
875
+ */
876
+ class ForSelectAnchor {
877
+ #ctx = injectSelectContext('ForSelectAnchor');
878
+ #host = inject(ElementRef);
879
+ constructor() {
880
+ registerHandle(this.#host.nativeElement, (el) => this.#ctx.registerAnchor(el), (el) => this.#ctx.unregisterAnchor(el));
881
+ }
882
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectAnchor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
883
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForSelectAnchor, isStandalone: true, selector: "[forSelectAnchor]", exportAs: ["forSelectAnchor"], ngImport: i0 });
884
+ }
885
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectAnchor, decorators: [{
886
+ type: Directive,
887
+ args: [{
888
+ selector: '[forSelectAnchor]',
889
+ exportAs: 'forSelectAnchor',
890
+ }]
891
+ }], ctorParameters: () => [] });
892
+
893
+ /**
894
+ * Combobox button that opens the listbox. Apply on a `<button>` so
895
+ * Space / Enter dispatch native click events that toggle via `(click)`.
896
+ *
897
+ * Wires APG select-only combobox attributes: `role="combobox"`,
898
+ * `aria-haspopup="listbox"`, `aria-expanded`, and `aria-controls` pointing
899
+ * to the listbox. The button is exempt from the listbox's dismissable
900
+ * layer — clicks on it route through `(click)` instead of triggering an
901
+ * outside-pointer dismissal race.
902
+ *
903
+ * The root is normally resolved via DI from the enclosing `[forSelect]`.
904
+ * When the trigger is declared inside an `ng-template` stamped into the root
905
+ * (e.g. via `ngTemplateOutlet`), DI resolves at the template's declaration
906
+ * site and misses the root — pass it explicitly through the selector input,
907
+ * `routerLink`-style: `[forSelectTrigger]="root"` with `#root="forSelect"`.
908
+ *
909
+ * Keyboard:
910
+ * - **Click / Enter / Space** — toggle (open focuses the selected option, or first).
911
+ * - **ArrowDown** — open + focus selected option (or first).
912
+ * - **ArrowUp** — open + focus selected option (or last when none selected).
913
+ * - **Typeahead** (single mode only) — printable keys select the matching
914
+ * option immediately without opening the listbox, mirroring native
915
+ * `<select>`. In multi mode the buffered key is ignored at the trigger;
916
+ * open the listbox first to typeahead-focus.
917
+ */
918
+ class ForSelectTrigger {
919
+ #host = inject(ElementRef);
920
+ /**
921
+ * Optional explicit reference to the `[forSelect]` root, named after the
922
+ * selector `routerLink`-style. The bare valueless attribute keeps resolving
923
+ * the enclosing root via DI; pass the root explicitly
924
+ * (`[forSelectTrigger]="root"`, with `#root="forSelect"`) when the trigger
925
+ * is declared in an `ng-template` stamped inside the root — DI resolves at
926
+ * the template's declaration site, so the enclosing root is invisible there.
927
+ * The empty string (what the valueless attribute yields) is treated as unset.
928
+ */
929
+ forSelectTrigger = input('', /* @ts-ignore */
930
+ ...(ngDevMode ? [{ debugName: "forSelectTrigger" }] : /* istanbul ignore next */ []));
931
+ ctx = injectSelectTriggerContext(this.forSelectTrigger);
932
+ constructor() {
933
+ const el = this.#host.nativeElement;
934
+ // Registration is an imperative call into the resolved root's registry,
935
+ // not state derivation — the effect only re-registers the element when the
936
+ // resolved root changes (explicit reference swapped at runtime).
937
+ effect((onCleanup) => {
938
+ const ctx = this.ctx();
939
+ ctx.registerTrigger(el);
940
+ onCleanup(() => ctx.unregisterTrigger(el));
941
+ });
942
+ reflectDisabled(computed(() => this.ctx().effectiveDisabled()));
943
+ }
944
+ onClick() {
945
+ this.ctx().toggle('selected');
946
+ }
947
+ onKeyDown(event) {
948
+ if (this.ctx().effectiveDisabled()) {
949
+ return;
950
+ }
951
+ if (event.key === 'ArrowDown') {
952
+ event.preventDefault();
953
+ this.ctx().openMenu('selected');
954
+ return;
955
+ }
956
+ if (event.key === 'ArrowUp') {
957
+ event.preventDefault();
958
+ // ArrowUp lands on the selected option if any, else the last enabled.
959
+ this.ctx().openMenu(this.ctx().value().length > 0 ? 'selected' : 'last');
960
+ return;
961
+ }
962
+ // Closed-state typeahead — single-mode shortcut to match native <select>.
963
+ // No-op (returns false) in multi mode or when the key isn't a printable
964
+ // single char, so default browser behavior (e.g. nothing) takes over.
965
+ this.ctx().handleClosedTypeahead(event);
966
+ }
967
+ onFocusOut(event) {
968
+ const next = event.relatedTarget;
969
+ if (next) {
970
+ // Focus moving into the listbox content (we just opened it) — not a leave.
971
+ const content = this.ctx().content();
972
+ if (content && content.contains(next)) {
973
+ return;
974
+ }
975
+ // Focus going to a sibling inside the [forSelect] wrapper — also not a leave.
976
+ if (this.#host.nativeElement.contains(next)) {
977
+ return;
978
+ }
979
+ }
980
+ this.ctx().markTouched();
981
+ }
982
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
983
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForSelectTrigger, isStandalone: true, selector: "[forSelectTrigger]", inputs: { forSelectTrigger: { classPropertyName: "forSelectTrigger", publicName: "forSelectTrigger", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button", "role": "combobox" }, listeners: { "click": "onClick()", "keydown": "onKeyDown($event)", "focusout": "onFocusOut($event)" }, properties: { "id": "ctx().triggerId()", "attr.aria-haspopup": "\"listbox\"", "attr.aria-expanded": "ctx().open() ? \"true\" : \"false\"", "attr.aria-controls": "ctx().open() ? ctx().contentId() : 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.data-state": "ctx().open() ? \"open\" : \"closed\"", "attr.data-disabled": "ctx().effectiveDisabled() ? \"\" : null" } }, exportAs: ["forSelectTrigger"], ngImport: i0 });
984
+ }
985
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectTrigger, decorators: [{
986
+ type: Directive,
987
+ args: [{
988
+ selector: '[forSelectTrigger]',
989
+ exportAs: 'forSelectTrigger',
990
+ host: {
991
+ type: 'button',
992
+ role: 'combobox',
993
+ '[id]': 'ctx().triggerId()',
994
+ '[attr.aria-haspopup]': '"listbox"',
995
+ '[attr.aria-expanded]': 'ctx().open() ? "true" : "false"',
996
+ '[attr.aria-controls]': 'ctx().open() ? ctx().contentId() : null',
997
+ '[attr.aria-disabled]': 'ctx().effectiveDisabled() ? "true" : null',
998
+ '[attr.aria-readonly]': 'ctx().readonly() ? "true" : null',
999
+ '[attr.aria-required]': 'ctx().required() ? "true" : null',
1000
+ '[attr.aria-invalid]': 'ctx().invalid() ? "true" : null',
1001
+ '[attr.aria-busy]': 'ctx().pending() ? "true" : null',
1002
+ '[attr.data-state]': 'ctx().open() ? "open" : "closed"',
1003
+ '[attr.data-disabled]': 'ctx().effectiveDisabled() ? "" : null',
1004
+ '(click)': 'onClick()',
1005
+ '(keydown)': 'onKeyDown($event)',
1006
+ '(focusout)': 'onFocusOut($event)',
1007
+ },
1008
+ }]
1009
+ }], ctorParameters: () => [], propDecorators: { forSelectTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "forSelectTrigger", required: false }] }] } });
1010
+
1011
+ /**
1012
+ * Renders the currently-selected option's text — or the configured
1013
+ * placeholder when nothing is selected — into its host element via
1014
+ * `textContent`. Apply on a `<span>` (or any inline element) inside
1015
+ * `[forSelectTrigger]`:
1016
+ *
1017
+ * ```html
1018
+ * <button forSelectTrigger>
1019
+ * <span forSelectValue placeholder="Select fruit…"></span>
1020
+ * </button>
1021
+ * ```
1022
+ *
1023
+ * In multi mode the labels are joined by `separator` (default `', '`). For
1024
+ * fully custom rendering, drop this directive and read
1025
+ * `forSelect.selectedLabels()` / `forSelect.value()` from your template.
1026
+ */
1027
+ class ForSelectValue {
1028
+ #ctx = injectSelectContext('ForSelectValue');
1029
+ /** Text shown when nothing is selected. Falls back to `[forSelect][placeholder]`. */
1030
+ placeholder = input('', /* @ts-ignore */
1031
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
1032
+ /** Joiner for multi-mode label rendering. Default `', '`. */
1033
+ separator = input(', ', /* @ts-ignore */
1034
+ ...(ngDevMode ? [{ debugName: "separator" }] : /* istanbul ignore next */ []));
1035
+ isPlaceholder = computed(() => this.#ctx.value().length === 0, /* @ts-ignore */
1036
+ ...(ngDevMode ? [{ debugName: "isPlaceholder" }] : /* istanbul ignore next */ []));
1037
+ displayText = computed(() => {
1038
+ const labels = this.#ctx.selectedLabels();
1039
+ if (labels.length === 0) {
1040
+ return this.placeholder() || this.#ctx.placeholder();
1041
+ }
1042
+ return labels.join(this.separator());
1043
+ }, /* @ts-ignore */
1044
+ ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
1045
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectValue, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1046
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForSelectValue, isStandalone: true, selector: "[forSelectValue]", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "textContent": "displayText()", "attr.data-placeholder": "isPlaceholder() ? \"\" : null" } }, exportAs: ["forSelectValue"], ngImport: i0 });
1047
+ }
1048
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectValue, decorators: [{
1049
+ type: Directive,
1050
+ args: [{
1051
+ selector: '[forSelectValue]',
1052
+ exportAs: 'forSelectValue',
1053
+ host: {
1054
+ '[textContent]': 'displayText()',
1055
+ '[attr.data-placeholder]': 'isPlaceholder() ? "" : null',
1056
+ },
1057
+ }]
1058
+ }], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], separator: [{ type: i0.Input, args: [{ isSignal: true, alias: "separator", required: false }] }] } });
1059
+
1060
+ /**
1061
+ * The listbox surface. Carries `role="listbox"`, is portaled to
1062
+ * `document.body`, and is positioned by `@floating-ui/dom` against the
1063
+ * trigger.
1064
+ *
1065
+ * Mount/unmount of the visible content is the consumer's responsibility —
1066
+ * wrap with `@if (open())` so `animate.enter` / `animate.leave` fire on the
1067
+ * natural mount cycle. While mounted, a `DismissableLayer` activates
1068
+ * (Escape, pointer-down outside, focus outside); the trigger element is
1069
+ * exempt from outside-pointer checks so trigger clicks toggle without
1070
+ * dismissal racing.
1071
+ *
1072
+ * Initial focus is sent to the selected option (`'selected'`), the first
1073
+ * enabled option (`'first'`), or the last enabled option (`'last'`)
1074
+ * according to the trigger's hint. On destroy, focus returns to the
1075
+ * trigger when `returnFocus` is true.
1076
+ *
1077
+ * The lifecycle is owned by one of the two shared shells, picked once on
1078
+ * construction from `[forSelect].modal`:
1079
+ *
1080
+ * - **non-modal (default)** — `injectOverlayShell` anchors the listbox to the
1081
+ * trigger and branches on `[forSelect].position`:
1082
+ * - `'popper'` (default) — standard `injectFloating` path with full
1083
+ * anchored placement (`side`, `align`, `sideOffset`,
1084
+ * `alignOffset`, `flip`, `shift`, `arrow`, `hideWhenDetached`).
1085
+ * - `'item-aligned'` — `injectItemAlignedPositioner` overlays the listbox so
1086
+ * the selected option's center aligns with the trigger's center. The
1087
+ * anchored-placement inputs are documented as no-ops in this mode.
1088
+ * - **modal** — `injectModalShell` traps focus, inerts the background, and
1089
+ * locks body scroll (a centered surface the consumer positions with CSS, not
1090
+ * trigger-anchored). The batteries-included touch presentation. Same Escape /
1091
+ * outside-pointer dismiss, `dismissible` / `returnFocus` / `ariaLabel`, and
1092
+ * `autoFocusOnOpen` / `autoFocusOnClose` veto hooks; every anchored-
1093
+ * positioning input is a no-op. `aria-modal="true"` is reflected as a hint —
1094
+ * the real modality comes from the `inert` siblings the shell applies (the
1095
+ * surface keeps `role="listbox"`, for which several screen readers ignore
1096
+ * `aria-modal`).
1097
+ *
1098
+ * Mount/unmount of the visible content is the consumer's responsibility —
1099
+ * wrap with `@if (open())` so `animate.enter` / `animate.leave` fire on the
1100
+ * natural mount cycle.
1101
+ */
1102
+ class ForSelectContent {
1103
+ ctx = injectSelectContext('ForSelectContent');
1104
+ #host = inject(ElementRef);
1105
+ constructor() {
1106
+ const ctx = this.ctx;
1107
+ registerHandle(this.#host.nativeElement, (el) => ctx.registerContent(el), (el) => ctx.unregisterContent(el));
1108
+ // Primitive-owned initial-focus algorithm shared by both shells.
1109
+ // `'selected'` falls back to the first enabled option when nothing is
1110
+ // selected; returns `false` so the shell focuses the container on a miss.
1111
+ const focusInitial = () => {
1112
+ if (ctx.totalCount() !== undefined) {
1113
+ this.#host.nativeElement.focus();
1114
+ ctx.seedVirtualizedInitialFocus();
1115
+ return true;
1116
+ }
1117
+ const target = ctx.initialFocus();
1118
+ if (target === 'selected') {
1119
+ return ctx.focusSelectedOption() || ctx.focusFirstEnabledOption();
1120
+ }
1121
+ if (target === 'last') {
1122
+ return ctx.focusLastEnabledOption();
1123
+ }
1124
+ return ctx.focusFirstEnabledOption();
1125
+ };
1126
+ // Static branch — `modal` (and `position`) is read once on construction.
1127
+ // Switching modes at runtime would require re-creating the directive
1128
+ // (mount / unmount cycle), which is the expected pattern for primitives
1129
+ // whose surface is structurally different. The surface mounts lazily via
1130
+ // `@if (open())`, well after `modal` settles.
1131
+ if (ctx.modal()) {
1132
+ injectModalShell({
1133
+ modal: ctx.modal,
1134
+ returnFocus: ctx.returnFocus,
1135
+ // The trap owns Tab; the shell runs `focusInitial()` (selected →
1136
+ // first → last) instead of its own first-focusable move.
1137
+ initialFocus: {
1138
+ move: focusInitial,
1139
+ veto: () => ctx.emitAutoFocusOnOpen(),
1140
+ },
1141
+ autoFocusOnClose: () => (event) => {
1142
+ if (ctx.emitAutoFocusOnClose()) {
1143
+ event.preventDefault();
1144
+ }
1145
+ },
1146
+ dismiss: {
1147
+ dismissible: ctx.dismissible,
1148
+ // The shell builds the veto and calls this when not vetoed; mirror
1149
+ // the anchored path's touched-on-dismiss behaviour.
1150
+ requestClose: (reason) => {
1151
+ ctx.markTouched();
1152
+ ctx.closeMenu(reason);
1153
+ },
1154
+ emitEscapeKeyDown: (veto) => ctx.forwardEscapeKeyDown(veto),
1155
+ emitPointerDownOutside: (veto) => ctx.emitPointerDownOutside(veto),
1156
+ emitFocusOutside: (veto) => ctx.emitFocusOutside(veto),
1157
+ emitInteractOutside: (veto) => ctx.emitInteractOutside(veto),
1158
+ },
1159
+ });
1160
+ return;
1161
+ }
1162
+ const positioner = ctx.position() === 'item-aligned'
1163
+ ? {
1164
+ kind: 'item-aligned',
1165
+ reference: ctx.anchor,
1166
+ open: ctx.open,
1167
+ selectedOption: ctx.selectedOptionEl,
1168
+ collisionPadding: ctx.collisionPadding,
1169
+ }
1170
+ : {
1171
+ kind: 'floating',
1172
+ reference: ctx.anchor,
1173
+ open: ctx.open,
1174
+ side: ctx.side,
1175
+ align: ctx.align,
1176
+ sideOffset: ctx.sideOffset,
1177
+ alignOffset: ctx.alignOffset,
1178
+ avoidCollisions: ctx.avoidCollisions,
1179
+ collisionPadding: ctx.collisionPadding,
1180
+ arrowPadding: ctx.arrowPadding,
1181
+ sticky: ctx.sticky,
1182
+ hideWhenDetached: ctx.hideWhenDetached,
1183
+ clipUntilPositioned: ctx.clipUntilPositioned,
1184
+ };
1185
+ injectOverlayShell({
1186
+ positioner,
1187
+ dismiss: {
1188
+ dismissible: ctx.dismissible,
1189
+ // Mirror the modal path's touched-on-dismiss behaviour.
1190
+ requestClose: (reason) => {
1191
+ ctx.markTouched();
1192
+ ctx.closeMenu(reason);
1193
+ },
1194
+ emitEscapeKeyDown: (event) => ctx.emitEscapeKeyDown(event),
1195
+ emitPointerDownOutside: (veto) => ctx.emitPointerDownOutside(veto),
1196
+ emitFocusOutside: (veto) => ctx.emitFocusOutside(veto),
1197
+ emitInteractOutside: (veto) => ctx.emitInteractOutside(veto),
1198
+ // Trigger button is exempt — its own click handler toggles open/close;
1199
+ // without exemption pointer-down-outside would race and double-close.
1200
+ exemptElements: () => {
1201
+ const t = ctx.trigger();
1202
+ return t ? [t] : [];
1203
+ },
1204
+ },
1205
+ initialFocus: {
1206
+ move: focusInitial,
1207
+ veto: () => ctx.emitAutoFocusOnOpen(),
1208
+ },
1209
+ returnFocus: {
1210
+ enabled: ctx.returnFocus,
1211
+ target: () => ctx.trigger(),
1212
+ // `(autoFocusOnClose)` lets the consumer veto the return-focus.
1213
+ veto: () => ctx.emitAutoFocusOnClose(),
1214
+ // Skip on `'tab'` closes — Tab already moved focus to the trigger
1215
+ // and let the browser advance from there; re-focusing would steal
1216
+ // it back.
1217
+ skip: () => ctx.lastCloseReason() === 'tab',
1218
+ },
1219
+ });
1220
+ }
1221
+ onKeyDown(event) {
1222
+ if (this.ctx.totalCount() !== undefined) {
1223
+ this.ctx.handleVirtualizedKeydown(event);
1224
+ }
1225
+ }
1226
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1227
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForSelectContent, isStandalone: true, selector: "[forSelectContent]", host: { attributes: { "role": "listbox" }, listeners: { "keydown": "onKeyDown($event)" }, properties: { "attr.tabindex": "ctx.totalCount() !== undefined ? \"0\" : \"-1\"", "attr.aria-activedescendant": "ctx.activeDescendantId()", "id": "ctx.contentId()", "attr.aria-labelledby": "ctx.ariaLabel() ? null : ctx.triggerId()", "attr.aria-label": "ctx.ariaLabel()", "attr.aria-modal": "ctx.modal() ? \"true\" : null", "attr.aria-multiselectable": "ctx.multiple() ? \"true\" : null", "attr.aria-orientation": "ctx.orientation()", "attr.data-state": "ctx.open() ? \"open\" : \"closed\"", "attr.data-orientation": "ctx.orientation()" } }, exportAs: ["forSelectContent"], ngImport: i0 });
1228
+ }
1229
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectContent, decorators: [{
1230
+ type: Directive,
1231
+ args: [{
1232
+ selector: '[forSelectContent]',
1233
+ exportAs: 'forSelectContent',
1234
+ host: {
1235
+ role: 'listbox',
1236
+ '[attr.tabindex]': 'ctx.totalCount() !== undefined ? "0" : "-1"',
1237
+ '[attr.aria-activedescendant]': 'ctx.activeDescendantId()',
1238
+ '[id]': 'ctx.contentId()',
1239
+ '[attr.aria-labelledby]': 'ctx.ariaLabel() ? null : ctx.triggerId()',
1240
+ '[attr.aria-label]': 'ctx.ariaLabel()',
1241
+ '[attr.aria-modal]': 'ctx.modal() ? "true" : null',
1242
+ '[attr.aria-multiselectable]': 'ctx.multiple() ? "true" : null',
1243
+ '[attr.aria-orientation]': 'ctx.orientation()',
1244
+ '[attr.data-state]': 'ctx.open() ? "open" : "closed"',
1245
+ '[attr.data-orientation]': 'ctx.orientation()',
1246
+ '(keydown)': 'onKeyDown($event)',
1247
+ },
1248
+ }]
1249
+ }], ctorParameters: () => [] });
1250
+
1251
+ /**
1252
+ * Injection key the `[forSelectIndicator]` uses to resolve its parent option,
1253
+ * decoupled from the concrete `ForSelectOption` class. `ForSelectOption`
1254
+ * provides itself under this token, so a design system wrapping the option by
1255
+ * subclassing re-points it at the subclass with a single provider
1256
+ * (`{ provide: FOR_SELECT_OPTION, useExisting: MtxSelectOption }`) and the
1257
+ * indicator keeps resolving — see `docs/wrapping-form-primitives.md`.
1258
+ */
1259
+ const FOR_SELECT_OPTION = new InjectionToken('FOR_SELECT_OPTION');
1260
+ /**
1261
+ * One option inside a `[forSelectContent]`. Apply on a `<button type="button">`
1262
+ * so Space / Enter activation come from native button behavior — printable
1263
+ * keys fall through to the listbox for typeahead matching.
1264
+ *
1265
+ * Generic over the option value type `T` (default `string`). Inferred from
1266
+ * the `[value]` binding so consumers can pass either primitive ids or full
1267
+ * objects (`[value]="city"` infers `T = City`); the parent `[forSelect]`
1268
+ * must be parameterized over the same `T`. The parent's
1269
+ * `[isItemEqualToValue]` decides how options are matched against the
1270
+ * committed selection.
1271
+ *
1272
+ * Click activates: in single mode the value replaces `[(value)]` and the
1273
+ * listbox closes; in multi mode the value toggles in/out and the listbox
1274
+ * stays open.
1275
+ *
1276
+ * Keyboard while focused:
1277
+ * - **Enter / Space** — activate (via native button click).
1278
+ * - **ArrowDown / ArrowUp / Home / End** — move focus inside the listbox.
1279
+ * - **Tab / Shift+Tab** — commit the focused option (single mode) and let
1280
+ * the browser advance focus to the next / previous focusable, mirroring
1281
+ * the WAI-ARIA select-only combobox pattern and native `<select>`.
1282
+ * - **Escape** — close the listbox.
1283
+ * - **Typeahead** — printable keys match by text content.
1284
+ */
1285
+ class ForSelectOption {
1286
+ #ctx = injectSelectContext('ForSelectOption');
1287
+ #host = inject(ElementRef);
1288
+ /**
1289
+ * Stable identifier serialized into `[(value)]` and the hidden input.
1290
+ * Defaults to `string` for back-compat; bind an object to specialize the
1291
+ * parent `[forSelect]` over a richer `T`. The parent's
1292
+ * `[isItemEqualToValue]` decides how options are matched against the
1293
+ * committed selection.
1294
+ */
1295
+ value = input.required(/* @ts-ignore */
1296
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1297
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1298
+ /**
1299
+ * Zero-based absolute position of this option in the full source data.
1300
+ * Required in the virtualized path — drives `aria-posinset` and the parent's
1301
+ * position snapshot. Leave unset (default `null`) outside it.
1302
+ */
1303
+ posInSet = input(null, /* @ts-ignore */
1304
+ ...(ngDevMode ? [{ debugName: "posInSet" }] : /* istanbul ignore next */ []));
1305
+ id = hostId('for-select-option');
1306
+ selected = computed(() => this.#ctx.isSelected(this.value()), /* @ts-ignore */
1307
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1308
+ effectiveDisabled = computed(() => this.disabled() || this.#ctx.effectiveDisabled(), /* @ts-ignore */
1309
+ ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
1310
+ ariaSetSize = computed(() => {
1311
+ const total = this.#ctx.totalCount();
1312
+ return total === undefined ? null : String(total);
1313
+ }, /* @ts-ignore */
1314
+ ...(ngDevMode ? [{ debugName: "ariaSetSize" }] : /* istanbul ignore next */ []));
1315
+ ariaPosInSet = computed(() => {
1316
+ if (this.#ctx.totalCount() === undefined) {
1317
+ return null;
1318
+ }
1319
+ const pos = this.posInSet();
1320
+ return pos === null ? null : String(pos + 1);
1321
+ }, /* @ts-ignore */
1322
+ ...(ngDevMode ? [{ debugName: "ariaPosInSet" }] : /* istanbul ignore next */ []));
1323
+ #focused = signal(false, /* @ts-ignore */
1324
+ ...(ngDevMode ? [{ debugName: "#focused" }] : /* istanbul ignore next */ []));
1325
+ /**
1326
+ * True when this option is the active candidate — DOM-focused in the
1327
+ * default path, or `aria-activedescendant` in the virtualized path.
1328
+ * Reflected as `data-highlighted`.
1329
+ */
1330
+ highlighted = computed(() => {
1331
+ const activeId = this.#ctx.activeDescendantId();
1332
+ if (activeId !== null) {
1333
+ return activeId === this.id();
1334
+ }
1335
+ return this.#focused();
1336
+ }, /* @ts-ignore */
1337
+ ...(ngDevMode ? [{ debugName: "highlighted" }] : /* istanbul ignore next */ []));
1338
+ /**
1339
+ * Reactive effective label exposed on the handle — the trimmed `textContent`
1340
+ * of the host. Mirrors `ForComboboxOption`'s `#effectiveLabel` so the root
1341
+ * can fold a per-option `Signal<string>` into its persisted snapshot instead
1342
+ * of reading `textContent` from inside a `computed`. `textContent` is not a
1343
+ * signal, so this still does not self-heal on a text-only change with no
1344
+ * value change — supply `[forSelect][itemToLabel]` for a pure signal
1345
+ * derivation when the label can change without the value.
1346
+ */
1347
+ #effectiveLabel = computed(() => (this.#host.nativeElement.textContent ?? '').trim(), /* @ts-ignore */
1348
+ ...(ngDevMode ? [{ debugName: "#effectiveLabel" }] : /* istanbul ignore next */ []));
1349
+ constructor() {
1350
+ const handle = {
1351
+ host: this.#host.nativeElement,
1352
+ value: this.value,
1353
+ label: this.#effectiveLabel,
1354
+ disabled: this.effectiveDisabled,
1355
+ id: this.id,
1356
+ posInSet: this.posInSet,
1357
+ };
1358
+ registerHandle(handle, (h) => this.#ctx.registerOption(h), (h) => this.#ctx.unregisterOption(h));
1359
+ }
1360
+ onClick() {
1361
+ if (this.effectiveDisabled() || this.#ctx.readonly()) {
1362
+ return;
1363
+ }
1364
+ this.#ctx.activate(this.value());
1365
+ this.#ctx.notifyOptionClick(this.id());
1366
+ }
1367
+ onFocus() {
1368
+ this.#focused.set(true);
1369
+ }
1370
+ onBlur() {
1371
+ this.#focused.set(false);
1372
+ }
1373
+ onKeyDown(event) {
1374
+ if (this.effectiveDisabled()) {
1375
+ return;
1376
+ }
1377
+ if (event.key === 'Tab') {
1378
+ // In modal mode the focus trap owns Tab (it cycles focus inside the
1379
+ // surface in the capture phase); committing + closing here would defeat
1380
+ // the trap. Bail and let the trap's `preventDefault` keep focus inside.
1381
+ if (this.#ctx.modal()) {
1382
+ return;
1383
+ }
1384
+ // APG (combobox-select-only): Tab commits the focused option and lets
1385
+ // the browser advance focus to the next / previous focusable. Do NOT
1386
+ // preventDefault — the browser's Tab default uses the focus we just
1387
+ // moved to the trigger as the starting point.
1388
+ this.#ctx.commitOnTab(this.value());
1389
+ return;
1390
+ }
1391
+ const action = resolveListNavigation(event, {
1392
+ orientation: this.#ctx.orientation(),
1393
+ dir: this.#ctx.dir(),
1394
+ pageKeys: true,
1395
+ });
1396
+ if (action) {
1397
+ event.preventDefault();
1398
+ this.#ctx.navigate(this.#host.nativeElement, action);
1399
+ return;
1400
+ }
1401
+ this.#ctx.handleTypeahead(event);
1402
+ }
1403
+ onPointerDown(event) {
1404
+ if (this.#ctx.totalCount() === undefined) {
1405
+ return;
1406
+ }
1407
+ event.preventDefault();
1408
+ }
1409
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectOption, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1410
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForSelectOption, isStandalone: true, selector: "[forSelectOption]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, posInSet: { classPropertyName: "posInSet", publicName: "posInSet", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "option", "type": "button", "tabindex": "-1" }, listeners: { "click": "onClick()", "keydown": "onKeyDown($event)", "focus": "onFocus()", "blur": "onBlur()", "pointerdown": "onPointerDown($event)" }, properties: { "id": "id()", "attr.aria-selected": "selected() ? \"true\" : \"false\"", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.data-state": "selected() ? \"checked\" : \"unchecked\"", "attr.data-disabled": "effectiveDisabled() ? \"\" : null", "attr.aria-setsize": "ariaSetSize()", "attr.aria-posinset": "ariaPosInSet()", "attr.data-highlighted": "highlighted() ? \"\" : null" } }, providers: [{ provide: FOR_SELECT_OPTION, useExisting: ForSelectOption }], exportAs: ["forSelectOption"], ngImport: i0 });
1411
+ }
1412
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectOption, decorators: [{
1413
+ type: Directive,
1414
+ args: [{
1415
+ selector: '[forSelectOption]',
1416
+ exportAs: 'forSelectOption',
1417
+ providers: [{ provide: FOR_SELECT_OPTION, useExisting: ForSelectOption }],
1418
+ host: {
1419
+ role: 'option',
1420
+ type: 'button',
1421
+ tabindex: '-1',
1422
+ '[id]': 'id()',
1423
+ '[attr.aria-selected]': 'selected() ? "true" : "false"',
1424
+ '[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
1425
+ '[attr.data-state]': 'selected() ? "checked" : "unchecked"',
1426
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
1427
+ '[attr.aria-setsize]': 'ariaSetSize()',
1428
+ '[attr.aria-posinset]': 'ariaPosInSet()',
1429
+ '[attr.data-highlighted]': 'highlighted() ? "" : null',
1430
+ '(click)': 'onClick()',
1431
+ '(keydown)': 'onKeyDown($event)',
1432
+ '(focus)': 'onFocus()',
1433
+ '(blur)': 'onBlur()',
1434
+ '(pointerdown)': 'onPointerDown($event)',
1435
+ },
1436
+ }]
1437
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], posInSet: [{ type: i0.Input, args: [{ isSignal: true, alias: "posInSet", required: false }] }] } });
1438
+
1439
+ /**
1440
+ * Visibility helper inside a `[forSelectOption]`. The directive flips a
1441
+ * `[hidden]` host binding so the consumer can keep the checkmark / icon
1442
+ * inline without extra `@if` glue:
1443
+ *
1444
+ * ```html
1445
+ * <button forSelectOption value="apple">
1446
+ * <span forSelectIndicator>✓</span>
1447
+ * Apple
1448
+ * </button>
1449
+ * ```
1450
+ *
1451
+ * Reflects the parent option's `data-state` (`"checked" | "unchecked"`) so
1452
+ * the consumer can also style it from CSS. Visibility while unselected is
1453
+ * enforced with an inline `display: none` (which beats any author `display`
1454
+ * rule a consumer applies via a class) in addition to the `hidden` attribute
1455
+ * that removes it from the a11y tree.
1456
+ */
1457
+ class ForSelectIndicator {
1458
+ option = injectParentOption();
1459
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectIndicator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1460
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForSelectIndicator, isStandalone: true, selector: "[forSelectIndicator]", host: { attributes: { "aria-hidden": "true" }, properties: { "attr.data-state": "option.selected() ? \"checked\" : \"unchecked\"", "hidden": "!option.selected()", "style.display": "option.selected() ? null : \"none\"" } }, exportAs: ["forSelectIndicator"], ngImport: i0 });
1461
+ }
1462
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectIndicator, decorators: [{
1463
+ type: Directive,
1464
+ args: [{
1465
+ selector: '[forSelectIndicator]',
1466
+ exportAs: 'forSelectIndicator',
1467
+ host: {
1468
+ 'aria-hidden': 'true',
1469
+ '[attr.data-state]': 'option.selected() ? "checked" : "unchecked"',
1470
+ '[hidden]': '!option.selected()',
1471
+ '[style.display]': 'option.selected() ? null : "none"',
1472
+ },
1473
+ }]
1474
+ }] });
1475
+ function injectParentOption() {
1476
+ const option = inject(FOR_SELECT_OPTION, { optional: true });
1477
+ if (!option) {
1478
+ throw new Error('[forty-cdk/select] ForSelectIndicator must be used inside a [forSelectOption] element.');
1479
+ }
1480
+ return option;
1481
+ }
1482
+
1483
+ /**
1484
+ * Optional grouping wrapper inside a `[forSelectContent]`. Renders
1485
+ * `role="group"` and references the descendant `[forSelectGroupLabel]`
1486
+ * (if any) via `aria-labelledby`. Options inside a group still register
1487
+ * with the root listbox normally, so keyboard navigation traverses across
1488
+ * groups in DOM order without special handling.
1489
+ */
1490
+ class ForSelectGroup {
1491
+ #labelId = signal(null, /* @ts-ignore */
1492
+ ...(ngDevMode ? [{ debugName: "#labelId" }] : /* istanbul ignore next */ []));
1493
+ /** The id of the registered group label (or `null` if none). */
1494
+ labelId = this.#labelId.asReadonly();
1495
+ /** Called by `ForSelectGroupLabel` on mount. */
1496
+ registerLabel(id) {
1497
+ this.#labelId.set(id);
1498
+ }
1499
+ /** Called by `ForSelectGroupLabel` on destroy. Idempotent. */
1500
+ unregisterLabel(id) {
1501
+ if (this.#labelId() === id) {
1502
+ this.#labelId.set(null);
1503
+ }
1504
+ }
1505
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1506
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForSelectGroup, isStandalone: true, selector: "[forSelectGroup]", host: { attributes: { "role": "group" }, properties: { "attr.aria-labelledby": "labelId() || null" } }, exportAs: ["forSelectGroup"], ngImport: i0 });
1507
+ }
1508
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectGroup, decorators: [{
1509
+ type: Directive,
1510
+ args: [{
1511
+ selector: '[forSelectGroup]',
1512
+ exportAs: 'forSelectGroup',
1513
+ host: {
1514
+ role: 'group',
1515
+ '[attr.aria-labelledby]': 'labelId() || null',
1516
+ },
1517
+ }]
1518
+ }] });
1519
+
1520
+ /**
1521
+ * Visible label for a `[forSelectGroup]`. Generates a stable id and
1522
+ * registers with its parent group so the group's `aria-labelledby` points
1523
+ * at this element. Apply on whatever heading element fits the design
1524
+ * (often `<div>` styled as a small caption).
1525
+ */
1526
+ class ForSelectGroupLabel {
1527
+ /** Stable host id used by the parent group's `aria-labelledby`. */
1528
+ id;
1529
+ constructor() {
1530
+ const group = inject(ForSelectGroup, { optional: true });
1531
+ if (!group) {
1532
+ throw new Error('[forty-cdk/select] ForSelectGroupLabel must be used inside a [forSelectGroup] element.');
1533
+ }
1534
+ this.id = registerA11yName(group, 'for-select-group-label');
1535
+ }
1536
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectGroupLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1537
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForSelectGroupLabel, isStandalone: true, selector: "[forSelectGroupLabel]", host: { properties: { "id": "id()" } }, exportAs: ["forSelectGroupLabel"], ngImport: i0 });
1538
+ }
1539
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectGroupLabel, decorators: [{
1540
+ type: Directive,
1541
+ args: [{
1542
+ selector: '[forSelectGroupLabel]',
1543
+ exportAs: 'forSelectGroupLabel',
1544
+ host: {
1545
+ '[id]': 'id()',
1546
+ },
1547
+ }]
1548
+ }], ctorParameters: () => [] });
1549
+
1550
+ /**
1551
+ * Decorative separator between options or groups inside a
1552
+ * `[forSelectContent]`. Carries `role="separator"` and is intentionally not
1553
+ * registered with the listbox's option collection, so keyboard navigation
1554
+ * and typeahead skip it automatically.
1555
+ */
1556
+ class ForSelectSeparator {
1557
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectSeparator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1558
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForSelectSeparator, isStandalone: true, selector: "[forSelectSeparator]", host: { attributes: { "role": "separator", "aria-orientation": "horizontal" } }, exportAs: ["forSelectSeparator"], ngImport: i0 });
1559
+ }
1560
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForSelectSeparator, decorators: [{
1561
+ type: Directive,
1562
+ args: [{
1563
+ selector: '[forSelectSeparator]',
1564
+ exportAs: 'forSelectSeparator',
1565
+ host: {
1566
+ role: 'separator',
1567
+ 'aria-orientation': 'horizontal',
1568
+ },
1569
+ }]
1570
+ }] });
1571
+
1572
+ /**
1573
+ * Exact public names of every `ForSelect` input, its models included. Spread it into the
1574
+ * `inputs` array of a `hostDirectives` entry so a wrapper component re-exposes the
1575
+ * primitive's full surface — the Signal Forms members `[formField]` binds among them —
1576
+ * without hand-maintaining the list. Always spread into an inline object literal as shown
1577
+ * below: the literal is what keeps the entry statically analyzable for consumers compiling
1578
+ * against the published package. An anti-drift spec fails when this list no longer matches
1579
+ * the directive's actual API. See `docs/wrapping-form-primitives.md` for both supported
1580
+ * wrapping patterns.
1581
+ *
1582
+ * @example
1583
+ * ```ts
1584
+ * @Component({
1585
+ * selector: 'div[mySelect]',
1586
+ * template: '',
1587
+ * hostDirectives: [
1588
+ * {
1589
+ * directive: ForSelect,
1590
+ * inputs: [...FOR_SELECT_HOST_DIRECTIVE_INPUTS],
1591
+ * outputs: [...FOR_SELECT_HOST_DIRECTIVE_OUTPUTS],
1592
+ * },
1593
+ * ],
1594
+ * })
1595
+ * export class MySelect {}
1596
+ * ```
1597
+ */
1598
+ const FOR_SELECT_HOST_DIRECTIVE_INPUTS = [
1599
+ 'value',
1600
+ 'open',
1601
+ 'align',
1602
+ 'alignOffset',
1603
+ 'ariaLabel',
1604
+ 'arrowPadding',
1605
+ 'avoidCollisions',
1606
+ 'clipUntilPositioned',
1607
+ 'collisionPadding',
1608
+ 'dir',
1609
+ 'dirty',
1610
+ 'disabled',
1611
+ 'dismissible',
1612
+ 'errors',
1613
+ 'hideWhenDetached',
1614
+ 'invalid',
1615
+ 'isItemEqualToValue',
1616
+ 'itemToFormValue',
1617
+ 'itemToLabel',
1618
+ 'loop',
1619
+ 'modal',
1620
+ 'multiple',
1621
+ 'name',
1622
+ 'orientation',
1623
+ 'pending',
1624
+ 'placeholder',
1625
+ 'position',
1626
+ 'readonly',
1627
+ 'required',
1628
+ 'returnFocus',
1629
+ 'selectionFollowsFocus',
1630
+ 'side',
1631
+ 'sideOffset',
1632
+ 'sticky',
1633
+ 'totalCount',
1634
+ 'touched',
1635
+ 'visibleRange',
1636
+ ];
1637
+ /**
1638
+ * Exact public names of every `ForSelect` output, the Signal Forms `touch` output
1639
+ * included. Spread it into the `outputs` array of the same `hostDirectives` entry as
1640
+ * {@link FOR_SELECT_HOST_DIRECTIVE_INPUTS}.
1641
+ */
1642
+ const FOR_SELECT_HOST_DIRECTIVE_OUTPUTS = [
1643
+ 'valueChange',
1644
+ 'openChange',
1645
+ 'escapeKeyDown',
1646
+ 'pointerDownOutside',
1647
+ 'focusOutside',
1648
+ 'interactOutside',
1649
+ 'autoFocusOnOpen',
1650
+ 'autoFocusOnClose',
1651
+ 'scrollToIndex',
1652
+ 'touchedChange',
1653
+ 'touch',
1654
+ ];
1655
+
1656
+ /**
1657
+ * Generated bundle index. Do not edit.
1658
+ */
1659
+
1660
+ export { FOR_SELECT_CONTEXT, FOR_SELECT_DEFAULTS, FOR_SELECT_HOST_DIRECTIVE_INPUTS, FOR_SELECT_HOST_DIRECTIVE_OUTPUTS, FOR_SELECT_OPTION, ForSelect, ForSelectAnchor, ForSelectContent, ForSelectGroup, ForSelectGroupLabel, ForSelectIndicator, ForSelectOption, ForSelectSeparator, ForSelectTrigger, ForSelectValue, provideForSelectDefaults };
1661
+ //# sourceMappingURL=forty-cdk-select.mjs.map