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