@radix-ng/primitives 1.0.0-beta.2 → 1.0.0-beta.4

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 (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +76 -6
  3. package/fesm2022/radix-ng-primitives-accordion.mjs +5 -3
  4. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  5. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +31 -24
  6. package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
  7. package/fesm2022/radix-ng-primitives-autocomplete.mjs +1744 -0
  8. package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -0
  9. package/fesm2022/radix-ng-primitives-calendar.mjs +5 -3
  10. package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
  11. package/fesm2022/radix-ng-primitives-combobox.mjs +1399 -606
  12. package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
  13. package/fesm2022/radix-ng-primitives-config.mjs +13 -4
  14. package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
  15. package/fesm2022/radix-ng-primitives-context-menu.mjs +51 -10
  16. package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
  17. package/fesm2022/radix-ng-primitives-core.mjs +1345 -64
  18. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  19. package/fesm2022/radix-ng-primitives-date-field.mjs +5 -3
  20. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  21. package/fesm2022/radix-ng-primitives-dialog.mjs +271 -145
  22. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  23. package/fesm2022/radix-ng-primitives-direction-provider.mjs +70 -0
  24. package/fesm2022/radix-ng-primitives-direction-provider.mjs.map +1 -0
  25. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +519 -184
  26. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
  27. package/fesm2022/radix-ng-primitives-drawer.mjs +154 -64
  28. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
  29. package/fesm2022/radix-ng-primitives-field.mjs +3 -2
  30. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
  31. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +517 -0
  32. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -0
  33. package/fesm2022/radix-ng-primitives-focus-scope.mjs +296 -70
  34. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  35. package/fesm2022/radix-ng-primitives-menu.mjs +894 -299
  36. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  37. package/fesm2022/radix-ng-primitives-menubar.mjs +32 -4
  38. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  39. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +176 -207
  40. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  41. package/fesm2022/radix-ng-primitives-popover.mjs +250 -250
  42. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  43. package/fesm2022/radix-ng-primitives-popper.mjs +94 -45
  44. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  45. package/fesm2022/radix-ng-primitives-portal.mjs +107 -17
  46. package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
  47. package/fesm2022/radix-ng-primitives-presence.mjs +262 -79
  48. package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
  49. package/fesm2022/radix-ng-primitives-preview-card.mjs +172 -218
  50. package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -1
  51. package/fesm2022/radix-ng-primitives-roving-focus.mjs +4 -2
  52. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  53. package/fesm2022/radix-ng-primitives-scroll-area.mjs +5 -4
  54. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  55. package/fesm2022/radix-ng-primitives-select.mjs +303 -234
  56. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  57. package/fesm2022/radix-ng-primitives-slider.mjs +5 -3
  58. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  59. package/fesm2022/radix-ng-primitives-stepper.mjs +5 -3
  60. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  61. package/fesm2022/radix-ng-primitives-time-field.mjs +5 -3
  62. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  63. package/fesm2022/radix-ng-primitives-toast.mjs +15 -36
  64. package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -1
  65. package/fesm2022/radix-ng-primitives-toggle-group.mjs +5 -3
  66. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  67. package/fesm2022/radix-ng-primitives-toolbar.mjs +5 -3
  68. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  69. package/fesm2022/radix-ng-primitives-tooltip.mjs +105 -145
  70. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  71. package/package.json +14 -1
  72. package/types/radix-ng-primitives-accordion.d.ts +4 -3
  73. package/types/radix-ng-primitives-alert-dialog.d.ts +17 -11
  74. package/types/radix-ng-primitives-autocomplete.d.ts +661 -0
  75. package/types/radix-ng-primitives-calendar.d.ts +5 -3
  76. package/types/radix-ng-primitives-combobox.d.ts +727 -293
  77. package/types/radix-ng-primitives-config.d.ts +1 -1
  78. package/types/radix-ng-primitives-context-menu.d.ts +15 -5
  79. package/types/radix-ng-primitives-core.d.ts +762 -14
  80. package/types/radix-ng-primitives-date-field.d.ts +3 -2
  81. package/types/radix-ng-primitives-dialog.d.ts +107 -55
  82. package/types/radix-ng-primitives-direction-provider.d.ts +41 -0
  83. package/types/radix-ng-primitives-dismissable-layer.d.ts +147 -99
  84. package/types/radix-ng-primitives-drawer.d.ts +49 -22
  85. package/types/radix-ng-primitives-field.d.ts +1 -0
  86. package/types/radix-ng-primitives-floating-focus-manager.d.ts +175 -0
  87. package/types/radix-ng-primitives-focus-scope.d.ts +132 -1
  88. package/types/radix-ng-primitives-menu.d.ts +204 -112
  89. package/types/radix-ng-primitives-navigation-menu.d.ts +61 -101
  90. package/types/radix-ng-primitives-popover.d.ts +82 -115
  91. package/types/radix-ng-primitives-popper.d.ts +46 -10
  92. package/types/radix-ng-primitives-portal.d.ts +53 -8
  93. package/types/radix-ng-primitives-presence.d.ts +98 -17
  94. package/types/radix-ng-primitives-preview-card.d.ts +63 -95
  95. package/types/radix-ng-primitives-roving-focus.d.ts +7 -6
  96. package/types/radix-ng-primitives-scroll-area.d.ts +2 -2
  97. package/types/radix-ng-primitives-select.d.ts +192 -158
  98. package/types/radix-ng-primitives-slider.d.ts +5 -4
  99. package/types/radix-ng-primitives-stepper.d.ts +4 -3
  100. package/types/radix-ng-primitives-time-field.d.ts +3 -2
  101. package/types/radix-ng-primitives-toast.d.ts +7 -7
  102. package/types/radix-ng-primitives-toggle-group.d.ts +5 -4
  103. package/types/radix-ng-primitives-toolbar.d.ts +3 -2
  104. package/types/radix-ng-primitives-tooltip.d.ts +48 -84
@@ -1,16 +1,17 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Directive, inject, Injector, model, input, booleanAttribute, computed, numberAttribute, output, signal, effect, untracked, isDevMode, ElementRef, DestroyRef, afterNextRender, NgModule } from '@angular/core';
2
+ import { Directive, signal, computed, untracked, effect, inject, booleanAttribute, Injector, ElementRef, model, input, numberAttribute, output, isDevMode, DestroyRef, ChangeDetectionStrategy, Component, afterNextRender, afterRenderEffect, NgModule } from '@angular/core';
3
3
  import * as i1 from '@radix-ng/primitives/popper';
4
- import { RdxPopperAnchor, RdxPopperArrow, RdxPopper, injectPopperContentWrapperContext, RdxPopperContent, RdxPopperContentWrapper } from '@radix-ng/primitives/popper';
4
+ import { RdxPopperAnchor, RdxPopperArrow, RdxPopper, RdxPopperContentWrapper, provideRdxPopperContentWrapper, provideRdxPopperContentConfig, injectPopperContentWrapperContext, RdxPopperContent } from '@radix-ng/primitives/popper';
5
5
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
6
- import { createContext, useTransitionStatus, injectId, useFilter, useListHighlight, isItemEqualToValue, isNullish, itemToStringLabel, useScrollLock } from '@radix-ng/primitives/core';
6
+ import * as i2 from '@radix-ng/primitives/core';
7
+ import { useFilter, injectId, useListHighlight, useTransitionStatus, createContext, createFloatingRootContext, isItemEqualToValue, isNullish, itemToStringLabel, createCancelableChangeEventDetails, provideFloatingTree, provideFloatingRootContext, setupInternalBackdrop, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, useAnchoredScrollLock, RdxFloatingNodeRegistration, rdxDevError } from '@radix-ng/primitives/core';
8
+ import { injectDirection } from '@radix-ng/primitives/direction-provider';
7
9
  import * as i1$1 from '@radix-ng/primitives/dismissable-layer';
8
- import { RdxDismissableLayerBranch, RdxDismissableLayer, provideRdxDismissableLayerConfig } from '@radix-ng/primitives/dismissable-layer';
10
+ import { RdxFloatingInsideElement, RdxDismiss } from '@radix-ng/primitives/dismissable-layer';
9
11
  import { injectFieldRootContext } from '@radix-ng/primitives/field';
10
12
  import * as i1$2 from '@radix-ng/primitives/portal';
11
- import { RdxPortal } from '@radix-ng/primitives/portal';
12
- import * as i1$3 from '@radix-ng/primitives/presence';
13
- import { provideRdxPresenceContext, RdxPresenceDirective } from '@radix-ng/primitives/presence';
13
+ import { RdxPortalPresence } from '@radix-ng/primitives/portal';
14
+ import { provideRdxPresenceContext } from '@radix-ng/primitives/presence';
14
15
 
15
16
  /**
16
17
  * Optional positioning anchor for the popup. Put it on the element the popup should align to — for
@@ -53,58 +54,606 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
53
54
  }]
54
55
  }] });
55
56
 
57
+ /**
58
+ * The shared Combobox engine (ADR 0014): item registry, filtering, highlight-model navigation,
59
+ * open/close transition, and the reactive effects that tie them together — everything Combobox and
60
+ * Autocomplete have in common. Value/selection semantics, open orchestration, and forms integration
61
+ * stay in each root; the root configures the engine and reads its state for the DI context.
62
+ *
63
+ * Must be called in an injection context (it runs effects and `inject`-based hooks).
64
+ *
65
+ * @internal Not part of the public API — exported only for the autocomplete entry; may change without notice.
66
+ */
67
+ function useComboboxEngine(config) {
68
+ const { injector } = config;
69
+ const defaultFilter = useFilter();
70
+ const listId = injectId(config.listIdPrefix);
71
+ const labelId = signal(undefined, ...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
72
+ const inputElement = signal(null, ...(ngDevMode ? [{ debugName: "inputElement" }] : /* istanbul ignore next */ []));
73
+ // Where the text input lives relative to the popup (Base UI's `inputInsidePopup`), reported by the
74
+ // input on mount from its positioner ancestor. Drives the Trigger's role:
75
+ // - `outside` → the input is the tab stop; the Trigger is a `tabindex="-1"` toggle.
76
+ // - `inside` → the Trigger is the focusable `role="combobox"` control (`aria-haspopup="dialog"`).
77
+ // - `unknown` → no input has mounted yet (e.g. the input lives in a not-yet-opened popup); the
78
+ // Trigger stays a normal focusable button so it is reachable by `Tab` before the first open.
79
+ const inputLayout = signal('unknown', ...(ngDevMode ? [{ debugName: "inputLayout" }] : /* istanbul ignore next */ []));
80
+ // Whether the popup was opened by a touch interaction. When the input lives inside the popup, a
81
+ // touch-open focuses the popup (not the input) so Android doesn't raise the virtual keyboard
82
+ // (Base UI; iOS handles this itself). Reset to `false` on keyboard/mouse opens.
83
+ const openedByTouch = signal(false, ...(ngDevMode ? [{ debugName: "openedByTouch" }] : /* istanbul ignore next */ []));
84
+ // Whether the popup directive is currently mounted (open through the close/exit animation, until the
85
+ // presence machine unmounts it). Distinguishes "Escape closed the popup" (still mounted this tick)
86
+ // from "Escape on an already-closed combobox" (unmounted) — Base UI's `mounted`, since `open()`
87
+ // flips synchronously when the input's Escape handler (or the dismiss mechanism) closes the popup.
88
+ const popupMounted = signal(false, ...(ngDevMode ? [{ debugName: "popupMounted" }] : /* istanbul ignore next */ []));
89
+ let triggerElement = null;
90
+ // Tracks whether the last interaction was the keyboard, so the highlight doesn't jump to an item
91
+ // the cursor happens to rest on when arrow-key navigation scrolls the list under a still pointer.
92
+ let keyboardActive = false;
93
+ const _items = signal([], ...(ngDevMode ? [{ debugName: "_items" }] : /* istanbul ignore next */ []));
94
+ const orderedItems = computed(() => {
95
+ const items = [..._items()];
96
+ return items.sort((a, b) => domOrder(a.element, b.element));
97
+ }, ...(ngDevMode ? [{ debugName: "orderedItems" }] : /* istanbul ignore next */ []));
98
+ const matchesFilter = (item) => {
99
+ if (!config.filteringEnabled()) {
100
+ return true;
101
+ }
102
+ const filter = config.filter();
103
+ if (filter === null) {
104
+ return true;
105
+ }
106
+ const query = config.query();
107
+ // Custom filter: Base UI shape `(value, query, itemToString)`. Default: locale-aware contains
108
+ // on the item's own text (the element content), no value→text round-trip.
109
+ return filter
110
+ ? filter(item.value(), query, config.itemToString)
111
+ : defaultFilter.contains(item.textValue(), query);
112
+ };
113
+ const visibleItems = computed(() => {
114
+ const matching = orderedItems().filter((item) => matchesFilter(item));
115
+ const limit = config.limit();
116
+ return limit >= 0 ? matching.slice(0, limit) : matching;
117
+ }, ...(ngDevMode ? [{ debugName: "visibleItems" }] : /* istanbul ignore next */ []));
118
+ const visibleSet = computed(() => new Set(visibleItems()), ...(ngDevMode ? [{ debugName: "visibleSet" }] : /* istanbul ignore next */ []));
119
+ const isVisible = (item) => visibleSet().has(item);
120
+ const filteredItems = computed(() => {
121
+ const data = config.items();
122
+ if (data === undefined) {
123
+ return visibleItems().map((item) => item.value());
124
+ }
125
+ const limit = config.limit();
126
+ const cap = (arr) => (limit >= 0 ? arr.slice(0, limit) : arr);
127
+ if (!config.filteringEnabled()) {
128
+ return cap(data);
129
+ }
130
+ const filter = config.filter();
131
+ if (filter === null) {
132
+ return cap(data);
133
+ }
134
+ const query = config.query();
135
+ if (!query) {
136
+ return cap(data);
137
+ }
138
+ // Virtualized: no DOM to read text from, so resolve each value through `itemToString`.
139
+ return cap(data.filter((value) => filter
140
+ ? filter(value, query, config.itemToString)
141
+ : defaultFilter.contains(config.itemToString(value), query)));
142
+ }, ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
143
+ const visibleCount = computed(() => (config.virtualized() ? filteredItems().length : visibleItems().length), ...(ngDevMode ? [{ debugName: "visibleCount" }] : /* istanbul ignore next */ []));
144
+ const highlight = useListHighlight({
145
+ items: orderedItems,
146
+ isNavigable: (item) => isVisible(item) && !item.disabled(),
147
+ getId: (item) => item.id,
148
+ loop: config.loopFocus,
149
+ injector
150
+ });
151
+ const highlightedItem = highlight.highlightedItem;
152
+ const highlightedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightedIndex" }] : /* istanbul ignore next */ []));
153
+ const highlightReason = signal('none', ...(ngDevMode ? [{ debugName: "highlightReason" }] : /* istanbul ignore next */ []));
154
+ const itemId = (index) => `${listId}-item-${index}`;
155
+ const activeId = computed(() => {
156
+ if (config.virtualized()) {
157
+ const index = highlightedIndex();
158
+ return index >= 0 ? itemId(index) : undefined;
159
+ }
160
+ return highlight.activeId();
161
+ }, ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
162
+ // `'first-match'` highlights the first item whose label prefix-matches the query (inline modes),
163
+ // so inline completion lands on a real prefix even when the list is static.
164
+ const pendingHighlightEdge = signal(null, ...(ngDevMode ? [{ debugName: "pendingHighlightEdge" }] : /* istanbul ignore next */ []));
165
+ // Inline completion (autocomplete `both` / `inline`): a transient preview of the highlighted item's
166
+ // label mirrored into the input. `null` when off. `suppressInline` skips it for a deleting edit.
167
+ const inlinePreview = signal(null, ...(ngDevMode ? [{ debugName: "inlinePreview" }] : /* istanbul ignore next */ []));
168
+ let suppressInline = false;
169
+ /** The first visible, navigable item whose label starts with the query (for inline completion). */
170
+ const firstMatchItem = () => {
171
+ const query = config.query();
172
+ if (!query) {
173
+ return null;
174
+ }
175
+ const lower = query.toLowerCase();
176
+ return (visibleItems().find((item) => !item.disabled() && item.textValue().toLowerCase().startsWith(lower)) ?? null);
177
+ };
178
+ /** The first visible, navigable item (auto-highlight fallback when no prefix match exists). */
179
+ const firstVisibleNavigable = () => visibleItems().find((item) => !item.disabled()) ?? null;
180
+ /** Resolves a pending edge to a virtualized index (`'first-match'` → first prefix match, else 0). */
181
+ const resolveVirtualizedEdge = (edge, count) => {
182
+ if (edge === 'last') {
183
+ return count - 1;
184
+ }
185
+ if (edge === 'first-match') {
186
+ const query = config.query();
187
+ if (query) {
188
+ const lower = query.toLowerCase();
189
+ const index = filteredItems().findIndex((value) => config.itemToString(value).toLowerCase().startsWith(lower));
190
+ if (index >= 0) {
191
+ return index;
192
+ }
193
+ }
194
+ }
195
+ return 0;
196
+ };
197
+ /** The active highlight's label, DOM-ref or virtualized (resolved from the index). `null` when none. */
198
+ const activeLabel = () => {
199
+ if (config.virtualized()) {
200
+ const index = highlightedIndex();
201
+ const value = index >= 0 ? filteredItems()[index] : undefined;
202
+ return value === undefined ? null : config.itemToString(value);
203
+ }
204
+ return highlightedItem()?.textValue() ?? null;
205
+ };
206
+ const recomputeInlinePreview = (label, query, reason) => {
207
+ // Pointer hover must not rewrite the input (matches Base UI); only typing / keyboard nav complete it.
208
+ if (!config.inlineMode() || suppressInline || !label || reason === 'pointer') {
209
+ inlinePreview.set(null);
210
+ return;
211
+ }
212
+ if (query && label.toLowerCase().startsWith(query.toLowerCase())) {
213
+ // Type-ahead: keep the typed prefix (preserving its casing) and complete the rest.
214
+ inlinePreview.set(query + label.slice(query.length));
215
+ return;
216
+ }
217
+ // Keyboard navigation to a non-prefix item: show its full label so the input reflects it.
218
+ if (reason === 'keyboard') {
219
+ inlinePreview.set(label);
220
+ return;
221
+ }
222
+ inlinePreview.set(null);
223
+ };
224
+ const transition = useTransitionStatus(config.onOpenChangeComplete);
225
+ // --- effects ---
226
+ // Emit open changes and drive the open/close transition (skip the initial run).
227
+ let previousOpen = untracked(config.open);
228
+ effect(() => {
229
+ const open = config.open();
230
+ if (open === previousOpen) {
231
+ return;
232
+ }
233
+ previousOpen = open;
234
+ untracked(() => {
235
+ // Drop a deferred open-edge highlight when the popup closes — otherwise a pending
236
+ // 'first' (e.g. ArrowDown opened an empty list, then it closed) would unexpectedly
237
+ // highlight on the next plain open, even without autoHighlight.
238
+ if (!open) {
239
+ pendingHighlightEdge.set(null);
240
+ }
241
+ config.onOpenChange(open);
242
+ transition.start(open);
243
+ });
244
+ }, { injector });
245
+ // Emit highlight changes (skip the initial run). Tracks both the DOM-ref highlight and the
246
+ // virtualized index; only one is active per mode, so the other never fires spuriously.
247
+ let highlightInitialized = false;
248
+ effect(() => {
249
+ const item = highlightedItem();
250
+ const index = highlightedIndex();
251
+ if (!highlightInitialized) {
252
+ highlightInitialized = true;
253
+ return;
254
+ }
255
+ untracked(() => {
256
+ if (config.virtualized()) {
257
+ const value = index >= 0 ? (filteredItems()[index] ?? null) : null;
258
+ // No active highlight (e.g. filtering pushed the index out of range) carries no
259
+ // interaction — report 'none', not a stale 'keyboard'/'pointer' reason.
260
+ config.onItemHighlighted({ value, index, reason: value === null ? 'none' : highlightReason() });
261
+ }
262
+ else {
263
+ const value = item ? item.value() : null;
264
+ const itemIndex = item ? visibleItems().indexOf(item) : -1;
265
+ // DOM-mode self-heal in `useListHighlight` clears `highlighted` without touching
266
+ // `highlightReason`; treat a null highlight as 'none' so the emit isn't mis-reported.
267
+ config.onItemHighlighted({
268
+ value,
269
+ index: itemIndex,
270
+ reason: item === null ? 'none' : highlightReason()
271
+ });
272
+ }
273
+ });
274
+ }, { injector });
275
+ // Apply a deferred open-edge highlight once items (DOM refs) or filtered data have registered.
276
+ effect(() => {
277
+ const edge = pendingHighlightEdge();
278
+ const count = config.virtualized() ? filteredItems().length : orderedItems().length;
279
+ if (!config.open() || edge === null || count === 0) {
280
+ return;
281
+ }
282
+ untracked(() => {
283
+ // Programmatic move — reset the reason so the emit reports 'none', not a stale interaction.
284
+ highlightReason.set('none');
285
+ if (config.virtualized()) {
286
+ highlightedIndex.set(resolveVirtualizedEdge(edge, count));
287
+ }
288
+ else if (edge === 'last') {
289
+ highlight.last();
290
+ }
291
+ else if (edge === 'first-match') {
292
+ highlight.set(firstMatchItem() ?? firstVisibleNavigable());
293
+ }
294
+ else {
295
+ highlight.first();
296
+ }
297
+ pendingHighlightEdge.set(null);
298
+ });
299
+ }, { injector });
300
+ // Inline completion: mirror the active item's label into the input. Tracks the DOM-ref highlight,
301
+ // the virtualized index, the query, the reason, and `inlineMode` — so virtualized navigation and a
302
+ // `both → list` mode switch both recompute (and clear) the preview. No-op (null) when off (combobox).
303
+ effect(() => {
304
+ config.inlineMode();
305
+ highlightedItem();
306
+ highlightedIndex();
307
+ const query = config.query();
308
+ const reason = highlightReason();
309
+ untracked(() => recomputeInlinePreview(activeLabel(), query, reason));
310
+ }, { injector });
311
+ // autoHighlight 'always': keep the first navigable item highlighted whenever the popup is open.
312
+ effect(() => {
313
+ orderedItems();
314
+ visibleCount();
315
+ if (config.autoHighlightMode() === 'always' && config.open()) {
316
+ untracked(() => {
317
+ if (config.virtualized()) {
318
+ const length = filteredItems().length;
319
+ const index = highlightedIndex();
320
+ if ((index < 0 || index >= length) && length > 0) {
321
+ highlightReason.set('none');
322
+ highlightedIndex.set(0);
323
+ }
324
+ }
325
+ else if (highlightedItem() === null) {
326
+ highlightReason.set('none');
327
+ highlight.first();
328
+ }
329
+ });
330
+ }
331
+ }, { injector });
332
+ // Virtualized self-heal: clear a highlight that filtering has pushed out of range.
333
+ effect(() => {
334
+ if (!config.virtualized()) {
335
+ return;
336
+ }
337
+ const length = filteredItems().length;
338
+ untracked(() => {
339
+ const index = highlightedIndex();
340
+ if (index >= length && index !== -1) {
341
+ highlightReason.set('none');
342
+ highlightedIndex.set(-1);
343
+ }
344
+ });
345
+ }, { injector });
346
+ // --- navigation facade (mode-aware: index-based when virtualized, else DOM-ref) ---
347
+ const stepIndex = (direction) => {
348
+ const length = filteredItems().length;
349
+ if (length === 0) {
350
+ highlightedIndex.set(-1);
351
+ return;
352
+ }
353
+ const current = highlightedIndex();
354
+ if (current < 0) {
355
+ highlightedIndex.set(direction === 1 ? 0 : length - 1);
356
+ return;
357
+ }
358
+ let next = current + direction;
359
+ const loop = config.loopFocus();
360
+ if (next < 0) {
361
+ next = loop ? length - 1 : 0;
362
+ }
363
+ else if (next >= length) {
364
+ next = loop ? 0 : length - 1;
365
+ }
366
+ highlightedIndex.set(next);
367
+ };
368
+ // --- grid navigation (DOM-ref mode only) ---
369
+ /** Visible items grouped into rows by their enclosing row element (DOM order). */
370
+ const gridRows = () => {
371
+ const rows = new Map();
372
+ for (const item of visibleItems()) {
373
+ const key = config.rowOf(item.element);
374
+ const bucket = rows.get(key);
375
+ if (bucket) {
376
+ bucket.push(item);
377
+ }
378
+ else {
379
+ rows.set(key, [item]);
380
+ }
381
+ }
382
+ return [...rows.values()];
383
+ };
384
+ /** Grid vertical move: keep the column index, jump to the adjacent row (wraps when looping). */
385
+ const gridVertical = (direction) => {
386
+ const rows = gridRows();
387
+ if (rows.length === 0) {
388
+ return;
389
+ }
390
+ const current = highlightedItem();
391
+ if (!current) {
392
+ const row = direction === 1 ? rows[0] : rows[rows.length - 1];
393
+ highlight.set(row[0] ?? null);
394
+ return;
395
+ }
396
+ let rowIndex = rows.findIndex((row) => row.includes(current));
397
+ const col = rowIndex >= 0 ? rows[rowIndex].indexOf(current) : 0;
398
+ const loop = config.loopFocus();
399
+ rowIndex += direction;
400
+ if (rowIndex < 0) {
401
+ rowIndex = loop ? rows.length - 1 : 0;
402
+ }
403
+ else if (rowIndex >= rows.length) {
404
+ rowIndex = loop ? 0 : rows.length - 1;
405
+ }
406
+ const targetRow = rows[rowIndex];
407
+ highlight.set(targetRow[Math.min(col, targetRow.length - 1)] ?? null);
408
+ };
409
+ return {
410
+ listId,
411
+ labelId: labelId.asReadonly(),
412
+ setLabelId(id) {
413
+ labelId.set(id);
414
+ },
415
+ inputElement: inputElement.asReadonly(),
416
+ setInputElement(el) {
417
+ inputElement.set(el);
418
+ },
419
+ inputLayout: inputLayout.asReadonly(),
420
+ setInputLayout(layout) {
421
+ inputLayout.set(layout);
422
+ },
423
+ openedByTouch: openedByTouch.asReadonly(),
424
+ setOpenedByTouch(value) {
425
+ openedByTouch.set(value);
426
+ },
427
+ popupMounted: popupMounted.asReadonly(),
428
+ setPopupMounted(value) {
429
+ popupMounted.set(value);
430
+ },
431
+ get triggerElement() {
432
+ return triggerElement;
433
+ },
434
+ setTrigger(el) {
435
+ triggerElement = el;
436
+ },
437
+ orderedItems,
438
+ visibleItems,
439
+ visibleCount,
440
+ filteredItems,
441
+ isVisible,
442
+ registerItem(item) {
443
+ _items.update((items) => [...items, item]);
444
+ },
445
+ unregisterItem(item) {
446
+ _items.update((items) => items.filter((i) => i !== item));
447
+ },
448
+ highlight,
449
+ highlightedItem,
450
+ highlightedIndex: highlightedIndex.asReadonly(),
451
+ activeId,
452
+ itemId,
453
+ setHighlightReason(reason) {
454
+ highlightReason.set(reason);
455
+ },
456
+ setPendingHighlightEdge(edge) {
457
+ pendingHighlightEdge.set(edge);
458
+ },
459
+ /** Transient inline-completion preview (autocomplete inline modes); `null` when inactive. */
460
+ inlinePreview: inlinePreview.asReadonly(),
461
+ /** Suppress inline completion for the current edit (e.g. while a delete key is held). */
462
+ setSuppressInline(value) {
463
+ suppressInline = value;
464
+ },
465
+ /** Clear the inline preview synchronously (on select / clear / close, before the effect runs). */
466
+ clearInlinePreview() {
467
+ inlinePreview.set(null);
468
+ },
469
+ highlightNext(reason = 'keyboard') {
470
+ highlightReason.set(reason);
471
+ if (config.virtualized()) {
472
+ stepIndex(1);
473
+ }
474
+ else if (config.grid()) {
475
+ gridVertical(1);
476
+ }
477
+ else {
478
+ highlight.next();
479
+ }
480
+ },
481
+ highlightPrevious(reason = 'keyboard') {
482
+ highlightReason.set(reason);
483
+ if (config.virtualized()) {
484
+ stepIndex(-1);
485
+ }
486
+ else if (config.grid()) {
487
+ gridVertical(-1);
488
+ }
489
+ else {
490
+ highlight.previous();
491
+ }
492
+ },
493
+ // Grid horizontal moves (next/previous cell in DOM order). No-op outside grid mode.
494
+ highlightNextColumn(reason = 'keyboard') {
495
+ if (!config.grid() || config.virtualized()) {
496
+ return;
497
+ }
498
+ highlightReason.set(reason);
499
+ highlight.next();
500
+ },
501
+ highlightPreviousColumn(reason = 'keyboard') {
502
+ if (!config.grid() || config.virtualized()) {
503
+ return;
504
+ }
505
+ highlightReason.set(reason);
506
+ highlight.previous();
507
+ },
508
+ highlightFirst(reason = 'keyboard') {
509
+ highlightReason.set(reason);
510
+ if (config.virtualized()) {
511
+ highlightedIndex.set(filteredItems().length > 0 ? 0 : -1);
512
+ }
513
+ else {
514
+ highlight.first();
515
+ }
516
+ },
517
+ highlightLast(reason = 'keyboard') {
518
+ highlightReason.set(reason);
519
+ if (config.virtualized()) {
520
+ const length = filteredItems().length;
521
+ highlightedIndex.set(length > 0 ? length - 1 : -1);
522
+ }
523
+ else {
524
+ highlight.last();
525
+ }
526
+ },
527
+ highlightIndex(index, reason) {
528
+ if (index < 0 || index >= filteredItems().length) {
529
+ return;
530
+ }
531
+ highlightReason.set(reason);
532
+ highlightedIndex.set(index);
533
+ },
534
+ setHighlight(item, reason) {
535
+ highlightReason.set(reason);
536
+ highlight.set(item);
537
+ },
538
+ clearHighlightState() {
539
+ highlight.clear();
540
+ highlightedIndex.set(-1);
541
+ },
542
+ isKeyboardActive() {
543
+ return keyboardActive;
544
+ },
545
+ setKeyboardActive(value) {
546
+ keyboardActive = value;
547
+ },
548
+ transitionStatus: transition.status,
549
+ registerTransitionElement: transition.registerElement,
550
+ focusInput() {
551
+ inputElement()?.focus();
552
+ },
553
+ selectInputText() {
554
+ inputElement()?.select();
555
+ },
556
+ /**
557
+ * Restore focus after a selection: the input when it sits outside the popup, otherwise the
558
+ * trigger. Skipped when the consumer moved focus during the `onValueChange` callback — pass the
559
+ * `document.activeElement` captured *before* the emit so we don't clobber a consumer's choice
560
+ * (e.g. focusing an external message field after inserting an emoji).
561
+ */
562
+ restoreFocusAfterSelect(previousActiveElement) {
563
+ if (typeof document !== 'undefined' && document.activeElement !== previousActiveElement) {
564
+ return;
565
+ }
566
+ const input = inputElement();
567
+ if (input && !input.closest(config.popupSelector)) {
568
+ input.focus();
569
+ }
570
+ else {
571
+ triggerElement?.focus();
572
+ }
573
+ }
574
+ };
575
+ }
576
+ /** DOM-order comparator for two elements (precedes → -1, follows → 1). */
577
+ function domOrder(a, b) {
578
+ const position = a.compareDocumentPosition(b);
579
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
580
+ return -1;
581
+ }
582
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) {
583
+ return 1;
584
+ }
585
+ return 0;
586
+ }
587
+
588
+ // The engine stays private to the root (it owns mutable internals); the context factory — a free
589
+ // function, so it can't reach a `private` field — reads it through this registry instead.
590
+ const engineRegistry = new WeakMap();
56
591
  const context = () => {
57
592
  const root = inject(RdxComboboxRoot);
593
+ const engine = engineRegistry.get(root);
58
594
  return {
59
- listId: root.listId,
60
- labelId: root.labelId,
61
- setLabelId: (id) => root.labelId.set(id),
595
+ listId: engine.listId,
596
+ labelId: engine.labelId,
597
+ setLabelId: (id) => engine.setLabelId(id),
62
598
  dir: root.dir,
63
599
  value: root.value,
64
600
  inputValue: root.inputValue,
65
601
  open: root.open,
602
+ present: root.present,
66
603
  multiple: root.multiple,
67
604
  selectionMode: root.mode,
68
605
  disabledState: root.disabledState,
69
- readonly: root.readonly,
606
+ readonly: root.readOnly,
70
607
  requiredState: root.requiredState,
71
608
  openOnInputClick: root.openOnInputClick,
72
609
  modal: root.modal,
73
610
  virtualized: root.virtualized,
74
- filteredItems: root.filteredItems,
75
- highlightedItem: root.highlightedItem,
76
- highlightedIndex: root.highlightedIndex.asReadonly(),
77
- activeId: root.activeId,
78
- itemId: (index) => root.itemId(index),
79
- isKeyboardActive: () => root.isKeyboardActive(),
80
- setKeyboardActive: (value) => root.setKeyboardActive(value),
81
- transitionStatus: root.transitionStatus,
82
- registerTransitionElement: root.registerTransitionElement,
83
- visibleCount: root.visibleCount,
84
- isVisible: (item) => root.isVisible(item),
611
+ grid: root.grid,
612
+ filteredItems: engine.filteredItems,
613
+ highlightedItem: engine.highlightedItem,
614
+ highlightedIndex: engine.highlightedIndex,
615
+ activeId: engine.activeId,
616
+ itemId: (index) => engine.itemId(index),
617
+ isKeyboardActive: () => engine.isKeyboardActive(),
618
+ setKeyboardActive: (value) => engine.setKeyboardActive(value),
619
+ transitionStatus: engine.transitionStatus,
620
+ registerTransitionElement: engine.registerTransitionElement,
621
+ visibleCount: engine.visibleCount,
622
+ isVisible: (item) => engine.isVisible(item),
85
623
  isSelected: (value) => root.isSelected(value),
86
- registerItem: (item) => root.registerItem(item),
87
- unregisterItem: (item) => root.unregisterItem(item),
88
- highlight: root.highlight,
89
- highlightNext: () => root.highlightNext('keyboard'),
90
- highlightPrevious: () => root.highlightPrevious('keyboard'),
91
- highlightFirst: () => root.highlightFirst('keyboard'),
92
- highlightLast: () => root.highlightLast('keyboard'),
93
- highlightIndex: (index, reason) => root.highlightIndex(index, reason),
94
- setHighlight: (item, reason) => root.setHighlight(item, reason),
95
- clearHighlight: () => root.clearHighlightState(),
96
- inputElement: root.inputElement.asReadonly(),
97
- setInputElement: (el) => root.inputElement.set(el),
98
- registerTrigger: (el) => (root.triggerElement = el),
99
- focusInput: () => root.focusInput(),
100
- openPopup: () => root.setOpen(true),
101
- openForBrowse: () => root.openForBrowse(),
102
- closePopup: (revert = true) => root.closePopup(revert),
624
+ registerItem: (item) => engine.registerItem(item),
625
+ unregisterItem: (item) => engine.unregisterItem(item),
626
+ highlight: engine.highlight,
627
+ highlightNext: () => engine.highlightNext('keyboard'),
628
+ highlightPrevious: () => engine.highlightPrevious('keyboard'),
629
+ highlightNextColumn: () => engine.highlightNextColumn('keyboard'),
630
+ highlightPreviousColumn: () => engine.highlightPreviousColumn('keyboard'),
631
+ highlightFirst: () => engine.highlightFirst('keyboard'),
632
+ highlightLast: () => engine.highlightLast('keyboard'),
633
+ highlightIndex: (index, reason) => engine.highlightIndex(index, reason),
634
+ setHighlight: (item, reason) => engine.setHighlight(item, reason),
635
+ clearHighlight: () => engine.clearHighlightState(),
636
+ highlightItemOnHover: root.highlightItemOnHover,
637
+ keepHighlight: root.keepHighlight,
638
+ inputElement: engine.inputElement,
639
+ setInputElement: (el) => engine.setInputElement(el),
640
+ inputLayout: engine.inputLayout,
641
+ setInputLayout: (layout) => engine.setInputLayout(layout),
642
+ openedByTouch: engine.openedByTouch,
643
+ setOpenedByTouch: (value) => engine.setOpenedByTouch(value),
644
+ popupMounted: engine.popupMounted,
645
+ setPopupMounted: (value) => engine.setPopupMounted(value),
646
+ registerTrigger: (el) => engine.setTrigger(el),
647
+ focusInput: () => engine.focusInput(),
648
+ openPopup: (reason, event) => root.setOpen(true, reason, event),
649
+ openForBrowse: (reason, event) => root.openForBrowse(reason, event),
650
+ closePopup: (revert = true, reason, event) => root.closePopup(revert, reason, event),
103
651
  setInputValue: (value) => root.setInputValue(value),
104
- openAndHighlight: (edge) => root.openAndHighlight(edge),
105
- select: (item) => root.handleSelect(item),
106
- selectIndex: (index) => root.selectIndex(index),
107
- selectHighlighted: () => root.selectHighlighted(),
652
+ openAndHighlight: (edge, reason, event) => root.openAndHighlight(edge, reason, event),
653
+ navigateByKeyboard: (direction, event) => root.navigateByKeyboard(direction, event),
654
+ select: (item, event) => root.handleSelect(item, event),
655
+ selectIndex: (index, event) => root.selectIndex(index, event),
656
+ selectHighlighted: (event) => root.selectHighlighted(event),
108
657
  clearSelection: () => root.clearSelection(),
109
658
  removeValue: (value) => root.removeValue(value),
110
659
  removeLastValue: () => root.removeLastValue(),
@@ -116,15 +665,56 @@ const context = () => {
116
665
  };
117
666
  const [injectComboboxRootContext, provideComboboxRootContext] = createContext('RdxComboboxRootContext', 'components/combobox');
118
667
  /**
119
- * Root of a Combobox a filterable select. Owns selection, input text, open state, filtering, and
120
- * highlight-model navigation, and exposes them to the parts through {@link RdxComboboxRootContext}.
121
- * Implements `ControlValueAccessor` for forms.
668
+ * `autoHighlight` transform: pass the `'always'` / `'input-change'` string modes through verbatim,
669
+ * coerce everything else as a boolean attribute (so the bare `autoHighlight` attribute reads `true`).
670
+ *
671
+ * Kept as a named module-level function rather than an inline `transform` arrow: compodoc 1.2.1
672
+ * (the metadata source for the API contract and Storybook ArgTypes) hangs parsing an inline arrow
673
+ * combined with explicit generic union type arguments on `input()`. A plain function reference sidesteps it.
674
+ */
675
+ function coerceAutoHighlight(value) {
676
+ return value === 'always' || value === 'input-change' ? value : booleanAttribute(value);
677
+ }
678
+ /**
679
+ * Root of a Combobox — a filterable select. Owns selection, input text, open state, and the forms
680
+ * integration, and delegates filtering / highlight-model navigation / the open-close transition to the
681
+ * shared {@link useComboboxEngine} (ADR 0014). Exposes everything to the parts through
682
+ * {@link RdxComboboxRootContext}. Implements `ControlValueAccessor`.
122
683
  *
123
684
  * @group Components
124
685
  */
125
686
  class RdxComboboxRoot {
687
+ /** The list element id referenced by `aria-controls` / `aria-activedescendant` (engine-backed). */
688
+ get listId() {
689
+ return this.engine.listId;
690
+ }
691
+ /** The currently highlighted item (engine-backed; read by parts and tests). */
692
+ get highlightedItem() {
693
+ return this.engine.highlightedItem;
694
+ }
695
+ /** Number of items the list currently shows (engine-backed). */
696
+ get visibleCount() {
697
+ return this.engine.visibleCount;
698
+ }
699
+ /** The filtered item values an external virtualizer should render (engine-backed). */
700
+ get filteredItems() {
701
+ return this.engine.filteredItems;
702
+ }
703
+ /** Highlighted index into {@link filteredItems} in virtualized mode (engine-backed). */
704
+ get highlightedIndex() {
705
+ return this.engine.highlightedIndex;
706
+ }
707
+ /** The active option's id for `aria-activedescendant` (engine-backed). */
708
+ get activeId() {
709
+ return this.engine.activeId;
710
+ }
126
711
  constructor() {
127
712
  this.injector = inject(Injector);
713
+ /** Per-popup floating root context (ADR 0015) — `open` / `triggers` / reference for the dismissal engine. */
714
+ this.floatingContext = createFloatingRootContext({
715
+ ownerDocument: inject(ElementRef).nativeElement.ownerDocument,
716
+ open: () => this.open()
717
+ });
128
718
  /** Selected value(s). A single value in single mode, an array in `multiple` mode. */
129
719
  this.value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
130
720
  /** Initial value when uncontrolled. */
@@ -149,11 +739,12 @@ class RdxComboboxRoot {
149
739
  /** In `'none'` mode, whether pressing an item fills the input with its label. */
150
740
  this.fillInputOnItemPress = input(true, { ...(ngDevMode ? { debugName: "fillInputOnItemPress" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
151
741
  /** Text direction. */
152
- this.dir = input('ltr', ...(ngDevMode ? [{ debugName: "dir" }] : /* istanbul ignore next */ []));
742
+ this.dirInput = input(undefined, { ...(ngDevMode ? { debugName: "dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
743
+ this.dir = injectDirection(this.dirInput);
153
744
  /** Whether the combobox is disabled. */
154
745
  this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
155
- /** Whether the combobox is read-only. */
156
- this.readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
746
+ /** Whether the combobox is read-only. Base UI prop name. */
747
+ this.readOnly = input(false, { ...(ngDevMode ? { debugName: "readOnly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
157
748
  /** Whether a value is required (for forms). */
158
749
  this.required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
159
750
  /** Whether keyboard navigation wraps at the list boundaries. */
@@ -161,10 +752,10 @@ class RdxComboboxRoot {
161
752
  /**
162
753
  * Auto-highlight behavior:
163
754
  * - `false` (default): never auto-highlight;
164
- * - `true` / `'input-change'`: highlight the first match as the query changes;
755
+ * - `true` (also the bare `autoHighlight` attribute) / `'input-change'`: highlight the first match as the query changes;
165
756
  * - `'always'`: keep the first navigable item highlighted whenever the popup is open.
166
757
  */
167
- this.autoHighlight = input(false, ...(ngDevMode ? [{ debugName: "autoHighlight" }] : /* istanbul ignore next */ []));
758
+ this.autoHighlight = input(false, { ...(ngDevMode ? { debugName: "autoHighlight" } : /* istanbul ignore next */ {}), transform: coerceAutoHighlight });
168
759
  /** Resolved auto-highlight mode. */
169
760
  this.autoHighlightMode = computed(() => {
170
761
  const value = this.autoHighlight();
@@ -176,6 +767,18 @@ class RdxComboboxRoot {
176
767
  }
177
768
  return 'off';
178
769
  }, ...(ngDevMode ? [{ debugName: "autoHighlightMode" }] : /* istanbul ignore next */ []));
770
+ /**
771
+ * Whether moving the pointer over an item highlights it. `true` (default) paints `data-highlighted`
772
+ * on hover; `false` suppresses hover-driven highlight entirely, letting CSS `:hover` stay distinct
773
+ * from the `data-highlighted` (keyboard) state. Clicking an item still selects it.
774
+ */
775
+ this.highlightItemOnHover = input(true, { ...(ngDevMode ? { debugName: "highlightItemOnHover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
776
+ /**
777
+ * Whether a pointer-driven highlight is kept when the cursor leaves the list. `false` (default)
778
+ * clears the highlight on pointer-leave; `true` retains the last hovered item highlighted. Keyboard
779
+ * navigation and auto-highlight are unaffected.
780
+ */
781
+ this.keepHighlight = input(false, { ...(ngDevMode ? { debugName: "keepHighlight" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
179
782
  /** Whether clicking the input opens the popup. */
180
783
  this.openOnInputClick = input(true, { ...(ngDevMode ? { debugName: "openOnInputClick" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
181
784
  /** Whether the popup is modal: locks page scroll and makes outside content inert while open. */
@@ -204,8 +807,14 @@ class RdxComboboxRoot {
204
807
  * items outside the rendered window are not skipped by keyboard navigation.
205
808
  */
206
809
  this.virtualized = input(false, { ...(ngDevMode ? { debugName: "virtualized" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
207
- /** How item values are compared for equality (function or object key). */
208
- this.by = input(...(ngDevMode ? [undefined, { debugName: "by" }] : /* istanbul ignore next */ []));
810
+ /**
811
+ * Whether the list is laid out as a 2D grid: `ArrowUp`/`ArrowDown` move between rows (keeping the
812
+ * column), `ArrowLeft`/`ArrowRight` move within a row. Wrap items in `RdxComboboxRow`; the list
813
+ * switches to `role="grid"`. Not supported together with {@link virtualized}.
814
+ */
815
+ this.grid = input(false, { ...(ngDevMode ? { debugName: "grid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
816
+ /** How item values are compared for equality (a comparator function or an object key). Base UI prop name. */
817
+ this.isItemEqualToValue = input(...(ngDevMode ? [undefined, { debugName: "isItemEqualToValue" }] : /* istanbul ignore next */ []));
209
818
  /** Converts a value to its display label. Defaults to the matching item's text. */
210
819
  this.itemToStringLabel = input(...(ngDevMode ? [undefined, { debugName: "itemToStringLabel" }] : /* istanbul ignore next */ []));
211
820
  /** Emits when the selection changes. */
@@ -221,97 +830,51 @@ class RdxComboboxRoot {
221
830
  this.onItemHighlighted = output();
222
831
  /** Emits after the open/close transition (including any exit animation) finishes. */
223
832
  this.onOpenChangeComplete = output();
224
- this.transition = useTransitionStatus((open) => this.onOpenChangeComplete.emit(open));
225
- /** Open/close transition phase, for `data-starting-style` / `data-ending-style`. */
226
- this.transitionStatus = this.transition.status;
227
- /** Registers the popup element whose animation determines transition completion. */
228
- this.registerTransitionElement = this.transition.registerElement;
229
- this.listId = injectId('rdx-combobox-list-');
230
- this.labelId = signal(undefined, ...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
231
- this.inputElement = signal(null, ...(ngDevMode ? [{ debugName: "inputElement" }] : /* istanbul ignore next */ []));
232
833
  this.cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
233
834
  this.disabledState = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
234
835
  this.requiredState = computed(() => this.required(), ...(ngDevMode ? [{ debugName: "requiredState" }] : /* istanbul ignore next */ []));
235
- this.defaultFilter = useFilter();
836
+ this.preventUnmountOnClose = signal(false, ...(ngDevMode ? [{ debugName: "preventUnmountOnClose" }] : /* istanbul ignore next */ []));
837
+ this.present = computed(() => this.open() || this.preventUnmountOnClose(), ...(ngDevMode ? [{ debugName: "present" }] : /* istanbul ignore next */ []));
236
838
  /**
237
839
  * Whether the input text is a fresh user query rather than the current selection's label. While
238
840
  * `false` (just opened, or showing a selected label), the list is unfiltered so the user can
239
841
  * browse; it flips `true` on the first keystroke.
240
842
  */
241
843
  this.typed = signal(false, ...(ngDevMode ? [{ debugName: "typed" }] : /* istanbul ignore next */ []));
242
- this._items = signal([], ...(ngDevMode ? [{ debugName: "_items" }] : /* istanbul ignore next */ []));
243
- /** Registered items, sorted into DOM order. */
244
- this.orderedItems = computed(() => {
245
- const items = [...this._items()];
246
- return items.sort((a, b) => {
247
- const position = a.element.compareDocumentPosition(b.element);
248
- if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
249
- return -1;
250
- }
251
- if (position & Node.DOCUMENT_POSITION_PRECEDING) {
252
- return 1;
253
- }
254
- return 0;
255
- });
256
- }, ...(ngDevMode ? [{ debugName: "orderedItems" }] : /* istanbul ignore next */ []));
257
- /** Matching items in DOM order, capped at `limit`. The set of items the list shows. */
258
- this.visibleItems = computed(() => {
259
- const matching = this.orderedItems().filter((item) => this.matchesFilter(item));
260
- const limit = this.limit();
261
- return limit >= 0 ? matching.slice(0, limit) : matching;
262
- }, ...(ngDevMode ? [{ debugName: "visibleItems" }] : /* istanbul ignore next */ []));
263
- this.visibleSet = computed(() => new Set(this.visibleItems()), ...(ngDevMode ? [{ debugName: "visibleSet" }] : /* istanbul ignore next */ []));
264
- /**
265
- * The filtered item values an external virtualizer should render (the analogue of Base UI's
266
- * `useFilteredItems`). Driven by {@link items} when provided (virtualized mode), capped by
267
- * {@link limit}; otherwise mirrors the values of the mounted {@link visibleItems}.
268
- */
269
- this.filteredItems = computed(() => {
270
- const data = this.items();
271
- if (data === undefined) {
272
- return this.visibleItems().map((item) => item.value());
273
- }
274
- const limit = this.limit();
275
- const cap = (arr) => (limit >= 0 ? arr.slice(0, limit) : arr);
276
- const filter = this.filter();
277
- if (filter === null) {
278
- return cap(data);
279
- }
280
- const query = this.typed() ? (this.inputValue() ?? '') : '';
281
- if (!query) {
282
- return cap(data);
283
- }
284
- const matcher = filter ?? this.defaultFilter.contains;
285
- return cap(data.filter((value) => matcher(this.textFor(value), query)));
286
- }, ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
287
- this.visibleCount = computed(() => this.virtualized() ? this.filteredItems().length : this.visibleItems().length, ...(ngDevMode ? [{ debugName: "visibleCount" }] : /* istanbul ignore next */ []));
288
- this.highlight = useListHighlight({
289
- items: this.orderedItems,
290
- isNavigable: (item) => this.isVisible(item) && !item.disabled(),
291
- getId: (item) => item.id,
292
- loop: this.loopFocus,
293
- injector: this.injector
844
+ /** The active query: the typed text once the user starts typing, otherwise empty (browse mode). */
845
+ this.query = computed(() => (this.typed() ? (this.inputValue() ?? '') : ''), ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
846
+ /** Built-in filtering always applies for combobox (the `none` filter is handled inside the engine). */
847
+ this.filteringEnabled = signal(true, ...(ngDevMode ? [{ debugName: "filteringEnabled" }] : /* istanbul ignore next */ []));
848
+ /** Combobox never inline-completes (that's an autocomplete mode). */
849
+ this.noInline = signal(false, ...(ngDevMode ? [{ debugName: "noInline" }] : /* istanbul ignore next */ []));
850
+ /** The shared engine: item registry, filtering, highlight navigation, and the open-close transition. */
851
+ this.engine = useComboboxEngine({
852
+ injector: this.injector,
853
+ listIdPrefix: 'rdx-combobox-list-',
854
+ popupSelector: '[rdxComboboxPopup]',
855
+ open: this.open,
856
+ query: this.query,
857
+ filteringEnabled: this.filteringEnabled,
858
+ loopFocus: this.loopFocus,
859
+ autoHighlightMode: this.autoHighlightMode,
860
+ virtualized: this.virtualized,
861
+ items: this.items,
862
+ filter: this.filter,
863
+ limit: this.limit,
864
+ grid: this.grid,
865
+ rowOf: (element) => element.closest('[rdxComboboxRow]'),
866
+ inlineMode: this.noInline,
867
+ itemToString: (value) => this.labelFor(value),
868
+ onItemHighlighted: (details) => this.onItemHighlighted.emit(details),
869
+ onOpenChange: () => { },
870
+ onOpenChangeComplete: (open) => this.onOpenChangeComplete.emit(open)
294
871
  });
295
- this.highlightedItem = this.highlight.highlightedItem;
296
- /** Highlighted index into {@link filteredItems} in virtualized mode (`-1` when cleared). */
297
- this.highlightedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightedIndex" }] : /* istanbul ignore next */ []));
298
- /** Why the highlight last moved; read when emitting {@link onItemHighlighted}. */
299
- this.highlightReason = signal('none', ...(ngDevMode ? [{ debugName: "highlightReason" }] : /* istanbul ignore next */ []));
300
- this.activeId = computed(() => {
301
- if (this.virtualized()) {
302
- const index = this.highlightedIndex();
303
- return index >= 0 ? this.itemId(index) : undefined;
304
- }
305
- return this.highlight.activeId();
306
- }, ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
307
- /** Edge to highlight once the list has mounted (items register asynchronously after opening). */
308
- this.pendingHighlightEdge = signal(null, ...(ngDevMode ? [{ debugName: "pendingHighlightEdge" }] : /* istanbul ignore next */ []));
309
- // Tracks whether the last interaction was the keyboard, so the highlight doesn't jump to an item
310
- // the cursor happens to rest on when arrow-key navigation scrolls the list under a still pointer.
311
- this.keyboardActive = false;
312
- /** The trigger element, used as a focus fallback when the input lives inside the popup. */
313
- this.triggerElement = null;
314
872
  this.chipsFocusLast = null;
873
+ // Expose the (private) engine to the context factory, which is a free function.
874
+ engineRegistry.set(this, this.engine);
875
+ // Keep the dismissal reference in sync with the input (the anchor) so a press / focus on it counts
876
+ // as "inside" and never dismisses (ADR 0015).
877
+ effect(() => this.floatingContext.setReferenceElement(this.engine.inputElement() ?? null));
315
878
  // Apply uncontrolled defaults once.
316
879
  effect(() => {
317
880
  const initial = this.defaultValue();
@@ -324,105 +887,6 @@ class RdxComboboxRoot {
324
887
  this.open.set(true);
325
888
  }
326
889
  });
327
- // Emit open changes and drive the open/close transition (skip the initial run).
328
- let previousOpen = untracked(this.open);
329
- effect(() => {
330
- const open = this.open();
331
- if (open === previousOpen) {
332
- return;
333
- }
334
- previousOpen = open;
335
- untracked(() => {
336
- this.onOpenChange.emit(open);
337
- this.transition.start(open);
338
- });
339
- });
340
- // Emit highlight changes (skip the initial run). Tracks both the DOM-ref highlight and the
341
- // virtualized index; only one is active per mode, so the other never fires spuriously.
342
- let highlightInitialized = false;
343
- effect(() => {
344
- const item = this.highlightedItem();
345
- const index = this.highlightedIndex();
346
- if (!highlightInitialized) {
347
- highlightInitialized = true;
348
- return;
349
- }
350
- untracked(() => {
351
- const reason = this.highlightReason();
352
- if (this.virtualized()) {
353
- const value = index >= 0 ? (this.filteredItems()[index] ?? null) : null;
354
- this.onItemHighlighted.emit({ value, index, reason });
355
- }
356
- else {
357
- const value = item ? item.value() : null;
358
- const itemIndex = item ? this.visibleItems().indexOf(item) : -1;
359
- this.onItemHighlighted.emit({ value, index: itemIndex, reason });
360
- }
361
- });
362
- });
363
- // Apply a deferred open-edge highlight once items (DOM refs) or filtered data have registered.
364
- effect(() => {
365
- const edge = this.pendingHighlightEdge();
366
- const count = this.virtualized() ? this.filteredItems().length : this.orderedItems().length;
367
- if (!this.open() || edge === null || count === 0) {
368
- return;
369
- }
370
- untracked(() => {
371
- // Programmatic move — reset the reason in both modes so the emit reports 'none', not a
372
- // stale 'keyboard'/'pointer' left by the previous user interaction.
373
- this.highlightReason.set('none');
374
- if (this.virtualized()) {
375
- this.highlightedIndex.set(edge === 'first' ? 0 : count - 1);
376
- }
377
- else if (edge === 'first') {
378
- this.highlight.first();
379
- }
380
- else {
381
- this.highlight.last();
382
- }
383
- this.pendingHighlightEdge.set(null);
384
- });
385
- });
386
- // autoHighlight 'always': keep the first navigable item highlighted whenever the popup is
387
- // open. `visibleCount` re-runs this when filtering changes; the current highlight is read
388
- // untracked to re-establish a highlight only after the self-heal clears it (no loop).
389
- effect(() => {
390
- this.orderedItems();
391
- this.visibleCount();
392
- if (this.autoHighlightMode() === 'always' && this.open()) {
393
- untracked(() => {
394
- if (this.virtualized()) {
395
- // Re-seed when the index is cleared OR has fallen out of range, so this works
396
- // regardless of whether the self-heal effect ran first (no ordering dependency).
397
- const length = this.filteredItems().length;
398
- const index = this.highlightedIndex();
399
- if ((index < 0 || index >= length) && length > 0) {
400
- this.highlightReason.set('none');
401
- this.highlightedIndex.set(0);
402
- }
403
- }
404
- else if (this.highlightedItem() === null) {
405
- this.highlightReason.set('none');
406
- this.highlight.first();
407
- }
408
- });
409
- }
410
- });
411
- // Virtualized self-heal: clear a highlight that filtering has pushed out of range, so
412
- // `activeId` never references an index past the end of the filtered list.
413
- effect(() => {
414
- if (!this.virtualized()) {
415
- return;
416
- }
417
- const length = this.filteredItems().length;
418
- untracked(() => {
419
- const index = this.highlightedIndex();
420
- if (index >= length && index !== -1) {
421
- this.highlightReason.set('none');
422
- this.highlightedIndex.set(-1);
423
- }
424
- });
425
- });
426
890
  // Virtualized object values can't be labelled from the DOM (items aren't registered) — without
427
891
  // `itemToStringLabel`, selection/revert fall back to a generic label. Warn once in dev.
428
892
  if (isDevMode()) {
@@ -440,46 +904,41 @@ class RdxComboboxRoot {
440
904
  }
441
905
  }
442
906
  /** Opens the popup for browsing (resets the query to "pristine" and selects the input text). */
443
- openForBrowse() {
907
+ openForBrowse(reason = 'none', event = new Event('combobox.open-change')) {
444
908
  if (!this.open()) {
445
909
  this.typed.set(false);
446
910
  }
447
- this.setOpen(true);
448
- this.selectInputText();
911
+ this.setOpen(true, reason, event);
912
+ this.engine.selectInputText();
449
913
  if (this.autoHighlightMode() === 'always') {
450
- this.pendingHighlightEdge.set('first');
914
+ this.engine.setPendingHighlightEdge('first');
451
915
  }
452
916
  }
453
917
  /** Opens the popup and highlights the given edge once the list mounts. */
454
- openAndHighlight(edge) {
918
+ openAndHighlight(edge, reason = 'list-navigation', event = new Event('combobox.open-change')) {
455
919
  if (!this.open()) {
456
920
  this.typed.set(false);
457
921
  }
458
- this.setOpen(true);
459
- this.selectInputText();
460
- this.pendingHighlightEdge.set(edge);
922
+ this.setOpen(true, reason, event);
923
+ this.engine.selectInputText();
924
+ this.engine.setPendingHighlightEdge(edge);
461
925
  }
462
- /** Whether the item matches the active query (ignores the `limit` cap). */
463
- matchesFilter(item) {
464
- const filter = this.filter();
465
- if (filter === null) {
466
- return true;
926
+ /**
927
+ * Keyboard list navigation shared by the input and the chips: opens the popup and highlights the
928
+ * leading/trailing edge when closed, otherwise steps the highlight. `direction` is `1` (down) or
929
+ * `-1` (up). Centralized so the input and chip key handlers can't drift apart.
930
+ */
931
+ navigateByKeyboard(direction, event = new Event('combobox.open-change')) {
932
+ this.engine.setKeyboardActive(true);
933
+ if (!this.open()) {
934
+ this.openAndHighlight(direction === 1 ? 'first' : 'last', 'list-navigation', event);
935
+ }
936
+ else if (direction === 1) {
937
+ this.engine.highlightNext();
938
+ }
939
+ else {
940
+ this.engine.highlightPrevious();
467
941
  }
468
- // Until the user types a fresh query, show the whole list (the input may still hold the
469
- // selected item's label, which must not filter everything down to just that item).
470
- const query = this.typed() ? (this.inputValue() ?? '') : '';
471
- const matcher = filter ?? this.defaultFilter.contains;
472
- return matcher(item.textValue(), query);
473
- }
474
- /** Whether the item is shown in the list (matches the query and is within `limit`). */
475
- isVisible(item) {
476
- return this.visibleSet().has(item);
477
- }
478
- isKeyboardActive() {
479
- return this.keyboardActive;
480
- }
481
- setKeyboardActive(value) {
482
- this.keyboardActive = value;
483
942
  }
484
943
  isSelected(value) {
485
944
  if (this.mode() === 'none') {
@@ -487,28 +946,34 @@ class RdxComboboxRoot {
487
946
  }
488
947
  const current = this.value();
489
948
  if (this.multiple()) {
490
- return Array.isArray(current) && current.some((v) => isItemEqualToValue(v, value, this.by()));
949
+ return Array.isArray(current) && current.some((v) => isItemEqualToValue(v, value, this.isItemEqualToValue()));
491
950
  }
492
- return !isNullish(current) && isItemEqualToValue(current, value, this.by());
493
- }
494
- registerItem(item) {
495
- this._items.update((items) => [...items, item]);
496
- }
497
- unregisterItem(item) {
498
- this._items.update((items) => items.filter((i) => i !== item));
951
+ return !isNullish(current) && isItemEqualToValue(current, value, this.isItemEqualToValue());
499
952
  }
500
- setOpen(open) {
501
- if (this.disabledState() || this.readonly()) {
502
- return;
953
+ setOpen(open, reason = 'none', event = new Event('combobox.open-change')) {
954
+ if (open === this.open()) {
955
+ return true;
956
+ }
957
+ if (open && (this.disabledState() || this.readOnly())) {
958
+ return false;
503
959
  }
960
+ const change = this.createOpenChangeEvent(open, reason, event);
961
+ this.onOpenChange.emit(change.payload);
962
+ if (change.eventDetails.isCanceled()) {
963
+ return false;
964
+ }
965
+ this.preventUnmountOnClose.set(open ? false : change.shouldPreventUnmountOnClose());
504
966
  this.open.set(open);
967
+ return true;
505
968
  }
506
- closePopup(revert = true) {
969
+ closePopup(revert = true, reason = 'none', event = new Event('combobox.open-change')) {
507
970
  if (!this.open()) {
508
971
  return;
509
972
  }
510
- this.open.set(false);
511
- this.clearHighlightState();
973
+ if (!this.setOpen(false, reason, event)) {
974
+ return;
975
+ }
976
+ this.engine.clearHighlightState();
512
977
  if (revert) {
513
978
  this.revertInputValue();
514
979
  }
@@ -519,9 +984,19 @@ class RdxComboboxRoot {
519
984
  this.inputValue.set(value);
520
985
  this.typed.set(true);
521
986
  this.onInputValueChange.emit(value);
987
+ // Base UI: emptying the field clears a single selection — but only when the input is OUTSIDE the
988
+ // popup. With the input inside the popup, the search box and the committed value are independent,
989
+ // so clearing the search must not deselect. (multiple keeps its chips; `none` has no committed
990
+ // value.) The guarded `commitValue` is a no-op when read-only / disabled.
991
+ if (value === '' &&
992
+ this.mode() === 'single' &&
993
+ !isNullish(this.value()) &&
994
+ this.engine.inputLayout() !== 'inside') {
995
+ this.commitValue(null);
996
+ }
522
997
  // Auto-highlight the first match as the query changes (deferred so it lands after items mount).
523
998
  if (this.autoHighlightMode() !== 'off') {
524
- this.pendingHighlightEdge.set('first');
999
+ this.engine.setPendingHighlightEdge('first');
525
1000
  }
526
1001
  }
527
1002
  /** Sets the input text programmatically (a selection label / revert) — not a user query. */
@@ -530,10 +1005,6 @@ class RdxComboboxRoot {
530
1005
  this.typed.set(false);
531
1006
  this.onInputValueChange.emit(value);
532
1007
  }
533
- /** Selects all input text so the next keystroke replaces a stale selection label. */
534
- selectInputText() {
535
- this.inputElement()?.select();
536
- }
537
1008
  /** Resets the input text to the current selection's label (single mode) or empty. */
538
1009
  revertInputValue() {
539
1010
  if (this.multiple()) {
@@ -548,37 +1019,31 @@ class RdxComboboxRoot {
548
1019
  if (custom) {
549
1020
  return custom(value);
550
1021
  }
551
- const item = this.orderedItems().find((i) => isItemEqualToValue(i.value(), value, this.by()));
1022
+ const item = this.engine.orderedItems().find((i) => isItemEqualToValue(i.value(), value, this.isItemEqualToValue()));
552
1023
  return item ? item.textValue() : itemToStringLabel(value);
553
1024
  }
554
- /** Filter/label text for a raw item value (virtualized mode, no DOM element to read from). */
555
- textFor(value) {
556
- const custom = this.itemToStringLabel();
557
- return custom ? custom(value) : itemToStringLabel(value);
558
- }
559
- /** Deterministic id for the item at `index` in virtualized mode (matches `aria-activedescendant`). */
560
- itemId(index) {
561
- return `${this.listId}-item-${index}`;
562
- }
563
- handleSelect(item) {
564
- if (this.disabledState() || this.readonly() || item.disabled()) {
1025
+ handleSelect(item, event = new Event('combobox.item-press')) {
1026
+ if (this.disabledState() || this.readOnly() || item.disabled()) {
565
1027
  return;
566
1028
  }
567
- this.handleSelectValue(item.value(), item.textValue() || this.labelFor(item.value()));
1029
+ this.handleSelectValue(item.value(), item.textValue() || this.labelFor(item.value()), event);
568
1030
  }
569
1031
  /** Selects the filtered item at `index` (virtualized mode). The label comes from {@link labelFor}. */
570
- selectIndex(index) {
571
- if (this.disabledState() || this.readonly()) {
1032
+ selectIndex(index, event = new Event('combobox.item-press')) {
1033
+ if (this.disabledState() || this.readOnly()) {
572
1034
  return;
573
1035
  }
574
- const value = this.filteredItems()[index];
1036
+ const value = this.engine.filteredItems()[index];
575
1037
  if (value === undefined) {
576
1038
  return;
577
1039
  }
578
- this.handleSelectValue(value, this.labelFor(value));
1040
+ this.handleSelectValue(value, this.labelFor(value), event);
579
1041
  }
580
1042
  /** Commits a selection from a resolved value/label, independent of whether a DOM item exists. */
581
- handleSelectValue(value, textValue) {
1043
+ handleSelectValue(value, textValue, event = new Event('combobox.item-press')) {
1044
+ // Capture focus *before* emitting `onValueChange` so focus restoration can be skipped when the
1045
+ // consumer moves focus in that callback (e.g. focusing an external field after an emoji press).
1046
+ const activeBefore = typeof document !== 'undefined' ? document.activeElement : null;
582
1047
  if (this.mode() === 'none') {
583
1048
  // No value is committed; `onValueChange` fires as a pointer/keyboard activation signal so
584
1049
  // command-palette consumers can react. Optionally fill the input, then close.
@@ -586,15 +1051,14 @@ class RdxComboboxRoot {
586
1051
  if (this.fillInputOnItemPress()) {
587
1052
  this.setLabel(textValue);
588
1053
  }
589
- this.open.set(false);
590
- this.clearHighlightState();
591
- this.restoreFocusAfterSelect();
1054
+ this.closePopup(false, 'item-press', event);
1055
+ this.engine.restoreFocusAfterSelect(activeBefore);
592
1056
  this.maybeSubmit();
593
1057
  return;
594
1058
  }
595
1059
  if (this.multiple()) {
596
1060
  const current = Array.isArray(this.value()) ? [...this.value()] : [];
597
- const index = current.findIndex((v) => isItemEqualToValue(v, value, this.by()));
1061
+ const index = current.findIndex((v) => isItemEqualToValue(v, value, this.isItemEqualToValue()));
598
1062
  if (index === -1) {
599
1063
  current.push(value);
600
1064
  }
@@ -603,124 +1067,57 @@ class RdxComboboxRoot {
603
1067
  }
604
1068
  this.commitValue(current);
605
1069
  this.setLabel('');
606
- this.focusInput();
1070
+ // Keep the input focused for adding more values — unless the consumer moved focus.
1071
+ if (typeof document === 'undefined' || document.activeElement === activeBefore) {
1072
+ this.engine.focusInput();
1073
+ }
607
1074
  }
608
1075
  else {
609
1076
  this.commitValue(value);
610
1077
  this.setLabel(textValue);
611
- this.open.set(false);
612
- this.clearHighlightState();
613
- this.restoreFocusAfterSelect();
1078
+ this.closePopup(false, 'item-press', event);
1079
+ this.engine.restoreFocusAfterSelect(activeBefore);
614
1080
  }
615
1081
  this.maybeSubmit();
616
1082
  }
617
1083
  /** Requests submit of the closest form when `submitOnItemClick` is enabled. */
618
1084
  maybeSubmit() {
619
1085
  if (this.submitOnItemClick()) {
620
- this.inputElement()?.form?.requestSubmit?.();
1086
+ this.engine.inputElement()?.form?.requestSubmit?.();
621
1087
  }
622
1088
  }
623
- selectHighlighted() {
1089
+ selectHighlighted(event = new Event('combobox.item-press')) {
624
1090
  if (this.virtualized()) {
625
- const index = this.highlightedIndex();
1091
+ const index = this.engine.highlightedIndex();
626
1092
  if (index >= 0) {
627
- this.selectIndex(index);
1093
+ this.selectIndex(index, event);
628
1094
  }
629
1095
  return;
630
1096
  }
631
- const item = this.highlightedItem();
1097
+ const item = this.engine.highlightedItem();
632
1098
  if (item) {
633
- this.handleSelect(item);
634
- }
635
- }
636
- // --- Highlight navigation facade (mode-aware: index-based when virtualized, else DOM-ref) ---
637
- highlightNext(reason = 'keyboard') {
638
- this.highlightReason.set(reason);
639
- if (this.virtualized()) {
640
- this.stepIndex(1);
641
- }
642
- else {
643
- this.highlight.next();
644
- }
645
- }
646
- highlightPrevious(reason = 'keyboard') {
647
- this.highlightReason.set(reason);
648
- if (this.virtualized()) {
649
- this.stepIndex(-1);
650
- }
651
- else {
652
- this.highlight.previous();
653
- }
654
- }
655
- highlightFirst(reason = 'keyboard') {
656
- this.highlightReason.set(reason);
657
- if (this.virtualized()) {
658
- this.highlightedIndex.set(this.filteredItems().length > 0 ? 0 : -1);
659
- }
660
- else {
661
- this.highlight.first();
1099
+ this.handleSelect(item, event);
662
1100
  }
663
1101
  }
664
- highlightLast(reason = 'keyboard') {
665
- this.highlightReason.set(reason);
666
- if (this.virtualized()) {
667
- const length = this.filteredItems().length;
668
- this.highlightedIndex.set(length > 0 ? length - 1 : -1);
669
- }
670
- else {
671
- this.highlight.last();
672
- }
673
- }
674
- /** Highlights a specific index in virtualized mode (e.g. pointer hover). Ignored if out of range. */
675
- highlightIndex(index, reason) {
676
- if (index < 0 || index >= this.filteredItems().length) {
677
- return;
678
- }
679
- this.highlightReason.set(reason);
680
- this.highlightedIndex.set(index);
681
- }
682
- /** Highlights a DOM-ref item (non-virtualized pointer hover). */
683
- setHighlight(item, reason) {
684
- this.highlightReason.set(reason);
685
- this.highlight.set(item);
686
- }
687
- /** Clears whichever highlight model is active. */
688
- clearHighlightState() {
689
- this.highlight.clear();
690
- this.highlightedIndex.set(-1);
691
- }
692
- /** Steps the virtualized highlight index by `direction`, wrapping when {@link loopFocus}. */
693
- stepIndex(direction) {
694
- const length = this.filteredItems().length;
695
- if (length === 0) {
696
- this.highlightedIndex.set(-1);
1102
+ clearSelection() {
1103
+ // Read-only / disabled comboboxes are not user-mutable (Base UI blocks Clear here too).
1104
+ if (this.disabledState() || this.readOnly()) {
697
1105
  return;
698
1106
  }
699
- const current = this.highlightedIndex();
700
- if (current < 0) {
701
- this.highlightedIndex.set(direction === 1 ? 0 : length - 1);
702
- return;
1107
+ // In `none` mode there is no committed value to clear — only the input text. Otherwise reset
1108
+ // the selection. Also drop any highlight (Base UI resets the active/selected indices here).
1109
+ if (this.mode() !== 'none') {
1110
+ this.commitValue(this.multiple() ? [] : null);
703
1111
  }
704
- let next = current + direction;
705
- const loop = this.loopFocus();
706
- if (next < 0) {
707
- next = loop ? length - 1 : 0;
708
- }
709
- else if (next >= length) {
710
- next = loop ? 0 : length - 1;
711
- }
712
- this.highlightedIndex.set(next);
713
- }
714
- clearSelection() {
715
- this.commitValue(this.multiple() ? [] : null);
716
1112
  this.setLabel('');
717
- this.focusInput();
1113
+ this.engine.clearHighlightState();
1114
+ this.engine.focusInput();
718
1115
  }
719
1116
  removeValue(value) {
720
1117
  if (!this.multiple() || !Array.isArray(this.value())) {
721
1118
  return;
722
1119
  }
723
- const next = this.value().filter((v) => !isItemEqualToValue(v, value, this.by()));
1120
+ const next = this.value().filter((v) => !isItemEqualToValue(v, value, this.isItemEqualToValue()));
724
1121
  this.commitValue(next);
725
1122
  }
726
1123
  removeLastValue() {
@@ -733,21 +1130,7 @@ class RdxComboboxRoot {
733
1130
  }
734
1131
  }
735
1132
  focusInput() {
736
- this.inputElement()?.focus();
737
- }
738
- /**
739
- * Restores focus after a selection closes the popup, so the keyboard can reopen it. When the
740
- * input lives inside the popup it is about to unmount, so focus goes to the trigger instead;
741
- * otherwise it returns to the input. Done synchronously while the input is still in the DOM.
742
- */
743
- restoreFocusAfterSelect() {
744
- const input = this.inputElement();
745
- if (input && !input.closest('[rdxComboboxPopup]')) {
746
- input.focus();
747
- }
748
- else {
749
- this.triggerElement?.focus();
750
- }
1133
+ this.engine.focusInput();
751
1134
  }
752
1135
  /** Registered by `RdxComboboxChips` so the input can hand keyboard focus to the chips. */
753
1136
  registerChipsNav(fn) {
@@ -760,11 +1143,40 @@ class RdxComboboxRoot {
760
1143
  markAsTouched() {
761
1144
  this.onTouched?.();
762
1145
  }
1146
+ /**
1147
+ * The single guarded value-mutation point for all user-driven changes (selection toggle, Clear,
1148
+ * chip removal, Backspace). Read-only / disabled comboboxes never mutate here — programmatic form
1149
+ * writes go through {@link writeValue}, which is intentionally not guarded. (ADR 0014, Finding 1.)
1150
+ */
763
1151
  commitValue(value) {
1152
+ if (this.disabledState() || this.readOnly()) {
1153
+ return;
1154
+ }
764
1155
  this.value.set(value);
765
1156
  this.onValueChange.emit(value);
766
1157
  this.onChange?.(value);
767
1158
  }
1159
+ createOpenChangeEvent(open, reason, event) {
1160
+ const change = createCancelableChangeEventDetails(reason, event, this.resolveOpenChangeTrigger(event));
1161
+ return {
1162
+ payload: {
1163
+ open,
1164
+ reason,
1165
+ event: change.eventDetails.event,
1166
+ trigger: change.eventDetails.trigger,
1167
+ eventDetails: change.eventDetails
1168
+ },
1169
+ eventDetails: change.eventDetails,
1170
+ shouldPreventUnmountOnClose: change.shouldPreventUnmountOnClose
1171
+ };
1172
+ }
1173
+ resolveOpenChangeTrigger(event) {
1174
+ const target = event.target;
1175
+ if (target instanceof HTMLElement) {
1176
+ return target;
1177
+ }
1178
+ return this.engine.triggerElement ?? this.engine.inputElement() ?? undefined;
1179
+ }
768
1180
  // ControlValueAccessor
769
1181
  writeValue(value) {
770
1182
  untracked(() => this.value.set(value));
@@ -779,9 +1191,12 @@ class RdxComboboxRoot {
779
1191
  this.cvaDisabled.set(isDisabled);
780
1192
  }
781
1193
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
782
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxRoot, isStandalone: true, selector: "[rdxComboboxRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, inputValue: { classPropertyName: "inputValue", publicName: "inputValue", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaultOpen: { classPropertyName: "defaultOpen", publicName: "defaultOpen", isSignal: true, isRequired: false, transformFunction: null }, multipleInput: { classPropertyName: "multipleInput", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, fillInputOnItemPress: { classPropertyName: "fillInputOnItemPress", publicName: "fillInputOnItemPress", isSignal: true, isRequired: false, transformFunction: null }, dir: { classPropertyName: "dir", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, loopFocus: { classPropertyName: "loopFocus", publicName: "loopFocus", isSignal: true, isRequired: false, transformFunction: null }, autoHighlight: { classPropertyName: "autoHighlight", publicName: "autoHighlight", isSignal: true, isRequired: false, transformFunction: null }, openOnInputClick: { classPropertyName: "openOnInputClick", publicName: "openOnInputClick", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, submitOnItemClick: { classPropertyName: "submitOnItemClick", publicName: "submitOnItemClick", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, virtualized: { classPropertyName: "virtualized", publicName: "virtualized", isSignal: true, isRequired: false, transformFunction: null }, by: { classPropertyName: "by", publicName: "by", isSignal: true, isRequired: false, transformFunction: null }, itemToStringLabel: { classPropertyName: "itemToStringLabel", publicName: "itemToStringLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", inputValue: "inputValueChange", open: "openChange", onValueChange: "onValueChange", onInputValueChange: "onInputValueChange", onOpenChange: "onOpenChange", onItemHighlighted: "onItemHighlighted", onOpenChangeComplete: "onOpenChangeComplete" }, host: { properties: { "attr.data-disabled": "disabledState() ? \"\" : undefined" } }, providers: [
1194
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxRoot, isStandalone: true, selector: "[rdxComboboxRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, inputValue: { classPropertyName: "inputValue", publicName: "inputValue", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaultOpen: { classPropertyName: "defaultOpen", publicName: "defaultOpen", isSignal: true, isRequired: false, transformFunction: null }, multipleInput: { classPropertyName: "multipleInput", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, fillInputOnItemPress: { classPropertyName: "fillInputOnItemPress", publicName: "fillInputOnItemPress", isSignal: true, isRequired: false, transformFunction: null }, dirInput: { classPropertyName: "dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readOnly: { classPropertyName: "readOnly", publicName: "readOnly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, loopFocus: { classPropertyName: "loopFocus", publicName: "loopFocus", isSignal: true, isRequired: false, transformFunction: null }, autoHighlight: { classPropertyName: "autoHighlight", publicName: "autoHighlight", isSignal: true, isRequired: false, transformFunction: null }, highlightItemOnHover: { classPropertyName: "highlightItemOnHover", publicName: "highlightItemOnHover", isSignal: true, isRequired: false, transformFunction: null }, keepHighlight: { classPropertyName: "keepHighlight", publicName: "keepHighlight", isSignal: true, isRequired: false, transformFunction: null }, openOnInputClick: { classPropertyName: "openOnInputClick", publicName: "openOnInputClick", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, submitOnItemClick: { classPropertyName: "submitOnItemClick", publicName: "submitOnItemClick", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, virtualized: { classPropertyName: "virtualized", publicName: "virtualized", isSignal: true, isRequired: false, transformFunction: null }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToStringLabel: { classPropertyName: "itemToStringLabel", publicName: "itemToStringLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", inputValue: "inputValueChange", open: "openChange", onValueChange: "onValueChange", onInputValueChange: "onInputValueChange", onOpenChange: "onOpenChange", onItemHighlighted: "onItemHighlighted", onOpenChangeComplete: "onOpenChangeComplete" }, host: { properties: { "attr.data-disabled": "disabledState() ? \"\" : undefined" } }, providers: [
783
1195
  provideComboboxRootContext(context),
784
- { provide: NG_VALUE_ACCESSOR, useExisting: RdxComboboxRoot, multi: true }
1196
+ { provide: NG_VALUE_ACCESSOR, useExisting: RdxComboboxRoot, multi: true },
1197
+ // New floating foundation (ADR 0015/0017) — the dismissal capability reads this shared context.
1198
+ provideFloatingTree(),
1199
+ provideFloatingRootContext(() => inject(RdxComboboxRoot).floatingContext)
785
1200
  ], exportAs: ["rdxComboboxRoot"], hostDirectives: [{ directive: i1.RdxPopper }], ngImport: i0 }); }
786
1201
  }
787
1202
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxRoot, decorators: [{
@@ -791,14 +1206,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
791
1206
  exportAs: 'rdxComboboxRoot',
792
1207
  providers: [
793
1208
  provideComboboxRootContext(context),
794
- { provide: NG_VALUE_ACCESSOR, useExisting: RdxComboboxRoot, multi: true }
1209
+ { provide: NG_VALUE_ACCESSOR, useExisting: RdxComboboxRoot, multi: true },
1210
+ // New floating foundation (ADR 0015/0017) — the dismissal capability reads this shared context.
1211
+ provideFloatingTree(),
1212
+ provideFloatingRootContext(() => inject(RdxComboboxRoot).floatingContext)
795
1213
  ],
796
1214
  hostDirectives: [RdxPopper],
797
1215
  host: {
798
1216
  '[attr.data-disabled]': 'disabledState() ? "" : undefined'
799
1217
  }
800
1218
  }]
801
- }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], inputValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputValue", required: false }] }, { type: i0.Output, args: ["inputValueChange"] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], defaultOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultOpen", required: false }] }], multipleInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], fillInputOnItemPress: [{ type: i0.Input, args: [{ isSignal: true, alias: "fillInputOnItemPress", required: false }] }], dir: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], loopFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "loopFocus", required: false }] }], autoHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoHighlight", required: false }] }], openOnInputClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnInputClick", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], submitOnItemClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitOnItemClick", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], limit: [{ type: i0.Input, args: [{ isSignal: true, alias: "limit", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], virtualized: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualized", required: false }] }], by: [{ type: i0.Input, args: [{ isSignal: true, alias: "by", required: false }] }], itemToStringLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringLabel", required: false }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }], onInputValueChange: [{ type: i0.Output, args: ["onInputValueChange"] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onItemHighlighted: [{ type: i0.Output, args: ["onItemHighlighted"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
1219
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], inputValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputValue", required: false }] }, { type: i0.Output, args: ["inputValueChange"] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], defaultOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultOpen", required: false }] }], multipleInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], fillInputOnItemPress: [{ type: i0.Input, args: [{ isSignal: true, alias: "fillInputOnItemPress", required: false }] }], dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readOnly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], loopFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "loopFocus", required: false }] }], autoHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoHighlight", required: false }] }], highlightItemOnHover: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightItemOnHover", required: false }] }], keepHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepHighlight", required: false }] }], openOnInputClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnInputClick", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], submitOnItemClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitOnItemClick", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], limit: [{ type: i0.Input, args: [{ isSignal: true, alias: "limit", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], virtualized: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualized", required: false }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", required: false }] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToStringLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringLabel", required: false }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }], onInputValueChange: [{ type: i0.Output, args: ["onInputValueChange"] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onItemHighlighted: [{ type: i0.Output, args: ["onItemHighlighted"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
802
1220
 
803
1221
  /**
804
1222
  * An overlay rendered beneath the popup in `modal` mode. Place it inside the portal/presence; style
@@ -812,7 +1230,7 @@ class RdxComboboxBackdrop {
812
1230
  this.rootContext = injectComboboxRootContext();
813
1231
  }
814
1232
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxBackdrop, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
815
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxBackdrop, isStandalone: true, selector: "[rdxComboboxBackdrop]", host: { attributes: { "aria-hidden": "true" }, properties: { "attr.data-state": "rootContext.open() ? \"open\" : \"closed\"", "attr.data-open": "rootContext.open() ? \"\" : undefined", "attr.data-closed": "rootContext.open() ? undefined : \"\"" } }, exportAs: ["rdxComboboxBackdrop"], ngImport: i0 }); }
1233
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxBackdrop, isStandalone: true, selector: "[rdxComboboxBackdrop]", host: { attributes: { "role": "presentation" }, properties: { "attr.data-state": "rootContext.open() ? \"open\" : \"closed\"", "attr.data-open": "rootContext.open() ? \"\" : undefined", "attr.data-closed": "rootContext.open() ? undefined : \"\"" } }, exportAs: ["rdxComboboxBackdrop"], ngImport: i0 }); }
816
1234
  }
817
1235
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxBackdrop, decorators: [{
818
1236
  type: Directive,
@@ -820,7 +1238,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
820
1238
  selector: '[rdxComboboxBackdrop]',
821
1239
  exportAs: 'rdxComboboxBackdrop',
822
1240
  host: {
823
- 'aria-hidden': 'true',
1241
+ // A decorative overlay — Base UI marks it `role="presentation"` (excluded from the a11y tree).
1242
+ role: 'presentation',
824
1243
  '[attr.data-state]': 'rootContext.open() ? "open" : "closed"',
825
1244
  '[attr.data-open]': 'rootContext.open() ? "" : undefined',
826
1245
  '[attr.data-closed]': 'rootContext.open() ? undefined : ""'
@@ -830,7 +1249,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
830
1249
 
831
1250
  /**
832
1251
  * Container for the selected-value chips in `multiple` mode. Sits before the input and coordinates
833
- * arrow-key navigation across the chips (the chips themselves handle the key events).
1252
+ * arrow-key navigation across the chips (the chips themselves handle the key events). Uses
1253
+ * `role="toolbar"` (Base UI): it keeps NVDA in focus mode while arrow-navigating the chips, where a
1254
+ * plain `list` would drop into browse mode.
834
1255
  *
835
1256
  * @group Components
836
1257
  */
@@ -855,16 +1276,16 @@ class RdxComboboxChips {
855
1276
  return true;
856
1277
  }
857
1278
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChips, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
858
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxChips, isStandalone: true, selector: "[rdxComboboxChips]", host: { attributes: { "role": "list" } }, exportAs: ["rdxComboboxChips"], hostDirectives: [{ directive: i1$1.RdxDismissableLayerBranch }], ngImport: i0 }); }
1279
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxChips, isStandalone: true, selector: "[rdxComboboxChips]", host: { attributes: { "role": "toolbar" } }, exportAs: ["rdxComboboxChips"], hostDirectives: [{ directive: i1$1.RdxFloatingInsideElement }], ngImport: i0 }); }
859
1280
  }
860
1281
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChips, decorators: [{
861
1282
  type: Directive,
862
1283
  args: [{
863
1284
  selector: '[rdxComboboxChips]',
864
1285
  exportAs: 'rdxComboboxChips',
865
- hostDirectives: [RdxDismissableLayerBranch],
1286
+ hostDirectives: [RdxFloatingInsideElement],
866
1287
  host: {
867
- role: 'list'
1288
+ role: 'toolbar'
868
1289
  }
869
1290
  }]
870
1291
  }], ctorParameters: () => [] });
@@ -892,21 +1313,35 @@ class RdxComboboxChip {
892
1313
  const list = this.chips?.getChips() ?? [];
893
1314
  const index = list.indexOf(this.element);
894
1315
  switch (event.key) {
1316
+ case 'ArrowDown':
1317
+ case 'ArrowUp':
1318
+ // Leave the chips and engage the list: focus must return to the input so
1319
+ // `aria-activedescendant` navigation works, then run the same nav as the input.
1320
+ event.preventDefault();
1321
+ this.rootContext.focusInput();
1322
+ this.rootContext.navigateByKeyboard(event.key === 'ArrowDown' ? 1 : -1);
1323
+ break;
895
1324
  case 'ArrowLeft':
896
- if (index > 0) {
1325
+ case 'ArrowRight': {
1326
+ // Direction-aware: in RTL the visual arrows flip. "Forward" steps toward the input
1327
+ // (the next chip, then the input); "backward" steps toward the first chip.
1328
+ const rtl = this.rootContext.dir() === 'rtl';
1329
+ const forward = (event.key === 'ArrowRight') !== rtl;
1330
+ if (forward) {
897
1331
  event.preventDefault();
898
- list[index - 1].focus();
899
- }
900
- break;
901
- case 'ArrowRight':
902
- event.preventDefault();
903
- if (index < list.length - 1) {
904
- list[index + 1].focus();
1332
+ if (index < list.length - 1) {
1333
+ list[index + 1].focus();
1334
+ }
1335
+ else {
1336
+ this.rootContext.focusInput();
1337
+ }
905
1338
  }
906
- else {
907
- this.rootContext.focusInput();
1339
+ else if (index > 0) {
1340
+ event.preventDefault();
1341
+ list[index - 1].focus();
908
1342
  }
909
1343
  break;
1344
+ }
910
1345
  case 'Home':
911
1346
  if (list.length) {
912
1347
  event.preventDefault();
@@ -941,7 +1376,7 @@ class RdxComboboxChip {
941
1376
  }
942
1377
  }
943
1378
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChip, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
944
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxChip, isStandalone: true, selector: "[rdxComboboxChip]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "listitem", "tabindex": "-1" }, listeners: { "keydown": "onKeydown($event)" } }, providers: [provideComboboxChipContext(chipContext)], exportAs: ["rdxComboboxChip"], ngImport: i0 }); }
1379
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxChip, isStandalone: true, selector: "[rdxComboboxChip]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "tabindex": "-1" }, listeners: { "keydown": "onKeydown($event)" } }, providers: [provideComboboxChipContext(chipContext)], exportAs: ["rdxComboboxChip"], ngImport: i0 }); }
945
1380
  }
946
1381
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChip, decorators: [{
947
1382
  type: Directive,
@@ -950,7 +1385,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
950
1385
  exportAs: 'rdxComboboxChip',
951
1386
  providers: [provideComboboxChipContext(chipContext)],
952
1387
  host: {
953
- role: 'listitem',
1388
+ // No explicit role (Base UI): a focusable child of the `toolbar` chips container.
954
1389
  tabindex: '-1',
955
1390
  '(keydown)': 'onKeydown($event)'
956
1391
  }
@@ -996,7 +1431,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
996
1431
  class RdxComboboxClear {
997
1432
  constructor() {
998
1433
  this.rootContext = injectComboboxRootContext();
1434
+ /** Disables just this clear button (in addition to the combobox's own disabled / read-only state). */
1435
+ this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1436
+ this.isDisabled = computed(() => this.disabled() || this.rootContext.disabledState() || this.rootContext.readonly(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
1437
+ /**
1438
+ * Whether there is nothing to clear, mirroring Base UI's visibility rule: in `none` mode the
1439
+ * button tracks the input text (a pure search field has no selection), otherwise it tracks the
1440
+ * selected value(s).
1441
+ */
999
1442
  this.isEmpty = computed(() => {
1443
+ if (this.rootContext.selectionMode() === 'none') {
1444
+ return (this.rootContext.inputValue() ?? '') === '';
1445
+ }
1000
1446
  const value = this.rootContext.value();
1001
1447
  if (Array.isArray(value)) {
1002
1448
  return value.length === 0;
@@ -1004,49 +1450,76 @@ class RdxComboboxClear {
1004
1450
  return value === null || value === undefined;
1005
1451
  }, ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
1006
1452
  }
1453
+ // Keep focus on the input — don't let the button take it on pointer/mouse down.
1454
+ onPointerDown(event) {
1455
+ event.preventDefault();
1456
+ }
1007
1457
  onClick() {
1008
1458
  this.rootContext.clearSelection();
1009
1459
  }
1010
1460
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxClear, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1011
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxClear, isStandalone: true, selector: "button[rdxComboboxClear]", host: { attributes: { "type": "button", "tabindex": "-1", "aria-label": "Clear" }, listeners: { "click": "onClick()" }, properties: { "hidden": "isEmpty()", "attr.disabled": "rootContext.disabledState() ? \"\" : undefined" } }, exportAs: ["rdxComboboxClear"], hostDirectives: [{ directive: i1$1.RdxDismissableLayerBranch }], ngImport: i0 }); }
1461
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxClear, isStandalone: true, selector: "button[rdxComboboxClear]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button", "tabindex": "-1", "aria-label": "Clear" }, listeners: { "pointerdown": "onPointerDown($event)", "mousedown": "onPointerDown($event)", "click": "onClick()" }, properties: { "hidden": "isEmpty()", "attr.disabled": "isDisabled() ? \"\" : undefined" } }, exportAs: ["rdxComboboxClear"], hostDirectives: [{ directive: i1$1.RdxFloatingInsideElement }], ngImport: i0 }); }
1012
1462
  }
1013
1463
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxClear, decorators: [{
1014
1464
  type: Directive,
1015
1465
  args: [{
1016
1466
  selector: 'button[rdxComboboxClear]',
1017
1467
  exportAs: 'rdxComboboxClear',
1018
- hostDirectives: [RdxDismissableLayerBranch],
1468
+ hostDirectives: [RdxFloatingInsideElement],
1019
1469
  host: {
1020
1470
  type: 'button',
1021
1471
  tabindex: '-1',
1022
1472
  'aria-label': 'Clear',
1023
1473
  '[hidden]': 'isEmpty()',
1024
- '[attr.disabled]': 'rootContext.disabledState() ? "" : undefined',
1474
+ '[attr.disabled]': 'isDisabled() ? "" : undefined',
1475
+ '(pointerdown)': 'onPointerDown($event)',
1476
+ '(mousedown)': 'onPointerDown($event)',
1025
1477
  '(click)': 'onClick()'
1026
1478
  }
1027
1479
  }]
1028
- }] });
1480
+ }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
1029
1481
 
1030
1482
  /**
1031
- * Shown only when no items match the current query.
1483
+ * A polite, atomic live region announcing the "no results" message. Mirrors Base UI: the element
1484
+ * stays **mounted and visible at all times** so screen readers reliably announce the transition to
1485
+ * empty — only its *content* is rendered conditionally (projected when nothing matches, removed
1486
+ * otherwise). It must never be hidden/unmounted (`hidden`, `display:none`, `aria-hidden`, `@if`):
1487
+ * pulling the region out of the accessibility tree at the same instant its text appears is exactly
1488
+ * what suppresses the announcement.
1032
1489
  *
1033
1490
  * @group Components
1034
1491
  */
1035
1492
  class RdxComboboxEmpty {
1036
1493
  constructor() {
1037
1494
  this.rootContext = injectComboboxRootContext();
1495
+ /** Whether no items match the current query (drives projection of the message). */
1496
+ this.isEmpty = computed(() => this.rootContext.visibleCount() === 0, ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
1038
1497
  }
1039
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxEmpty, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1040
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxEmpty, isStandalone: true, selector: "[rdxComboboxEmpty]", host: { attributes: { "role": "presentation" }, properties: { "hidden": "rootContext.visibleCount() > 0" } }, exportAs: ["rdxComboboxEmpty"], ngImport: i0 }); }
1498
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxEmpty, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1499
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: RdxComboboxEmpty, isStandalone: true, selector: "[rdxComboboxEmpty]", host: { attributes: { "role": "status", "aria-live": "polite", "aria-atomic": "true" }, properties: { "attr.data-empty": "isEmpty() ? \"\" : undefined" } }, exportAs: ["rdxComboboxEmpty"], ngImport: i0, template: `
1500
+ @if (isEmpty()) {
1501
+ <ng-content />
1502
+ }
1503
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1041
1504
  }
1042
1505
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxEmpty, decorators: [{
1043
- type: Directive,
1506
+ type: Component,
1044
1507
  args: [{
1045
1508
  selector: '[rdxComboboxEmpty]',
1046
1509
  exportAs: 'rdxComboboxEmpty',
1510
+ changeDetection: ChangeDetectionStrategy.OnPush,
1511
+ template: `
1512
+ @if (isEmpty()) {
1513
+ <ng-content />
1514
+ }
1515
+ `,
1047
1516
  host: {
1048
- role: 'presentation',
1049
- '[hidden]': 'rootContext.visibleCount() > 0'
1517
+ role: 'status',
1518
+ 'aria-live': 'polite',
1519
+ 'aria-atomic': 'true',
1520
+ // Present only while the message is shown. Lets consumers collapse the always-mounted region
1521
+ // (e.g. `data-[empty]:py-6`) without `display:none`/`hidden`, which would break the announcement.
1522
+ '[attr.data-empty]': 'isEmpty() ? "" : undefined'
1050
1523
  }
1051
1524
  }]
1052
1525
  }] });
@@ -1107,6 +1580,12 @@ class RdxComboboxGroupLabel {
1107
1580
  this.groupContext = injectComboboxGroupContext();
1108
1581
  this.id = injectId('rdx-combobox-group-label-');
1109
1582
  this.groupContext.labelId.set(this.id);
1583
+ // Clear the registration on unmount so the group doesn't reference a removed label's id.
1584
+ inject(DestroyRef).onDestroy(() => {
1585
+ if (this.groupContext.labelId() === this.id) {
1586
+ this.groupContext.labelId.set(undefined);
1587
+ }
1588
+ });
1110
1589
  }
1111
1590
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxGroupLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1112
1591
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxGroupLabel, isStandalone: true, selector: "[rdxComboboxGroupLabel]", host: { properties: { "attr.id": "id" } }, exportAs: ["rdxComboboxGroupLabel"], ngImport: i0 }); }
@@ -1142,7 +1621,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1142
1621
  }]
1143
1622
  }] });
1144
1623
 
1145
- const attr = (value) => (value ? '' : undefined);
1624
+ /**
1625
+ * Positions the combobox popup relative to the input anchor using the popper engine.
1626
+ *
1627
+ * A "thin" positioner (ADR 0012): it inherits the full popper positioning surface — the inputs
1628
+ * (`side`, `sideOffset`, `align`, …), the `placed` output, and the host bindings — from
1629
+ * {@link RdxPopperContentWrapper}, and only declares combobox's Base UI-aligned defaults through the
1630
+ * config provider. `provideRdxPopperContentWrapper` re-wires the `useExisting` alias + context that
1631
+ * the popup and arrow resolve (Angular does not inherit a base directive's `providers`).
1632
+ *
1633
+ * @group Components
1634
+ */
1635
+ class RdxComboboxPositioner extends RdxPopperContentWrapper {
1636
+ constructor() {
1637
+ super();
1638
+ const rootContext = injectComboboxRootContext();
1639
+ const injector = inject(Injector);
1640
+ const host = inject(ElementRef).nativeElement;
1641
+ // A modal combobox isolates the background with an internal backdrop (Base UI); the input stays
1642
+ // clickable through a cutout. (Combobox is non-modal by default — usually no backdrop.)
1643
+ afterNextRender(() => setupInternalBackdrop(host, injector, {
1644
+ isOpen: () => rootContext.open(),
1645
+ shouldRender: () => rootContext.modal(),
1646
+ cutout: () => rootContext.inputElement() ?? null
1647
+ }));
1648
+ }
1649
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPositioner, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1650
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxPositioner, isStandalone: true, selector: "[rdxComboboxPositioner]", providers: [
1651
+ ...provideRdxPopperContentWrapper(RdxComboboxPositioner),
1652
+ provideRdxPopperContentConfig({ sideOffset: 4, align: 'start' })
1653
+ ], exportAs: ["rdxComboboxPositioner"], usesInheritance: true, ngImport: i0 }); }
1654
+ }
1655
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPositioner, decorators: [{
1656
+ type: Directive,
1657
+ args: [{
1658
+ selector: '[rdxComboboxPositioner]',
1659
+ exportAs: 'rdxComboboxPositioner',
1660
+ providers: [
1661
+ ...provideRdxPopperContentWrapper(RdxComboboxPositioner),
1662
+ provideRdxPopperContentConfig({ sideOffset: 4, align: 'start' })
1663
+ ]
1664
+ }]
1665
+ }], ctorParameters: () => [] });
1666
+
1667
+ const attr$1 = (value) => (value ? '' : undefined);
1146
1668
  /**
1147
1669
  * The combobox text input. Holds DOM focus at all times; the highlighted option is referenced via
1148
1670
  * `aria-activedescendant`. Integrates with Field for labeling, description, and validation state.
@@ -1182,8 +1704,12 @@ class RdxComboboxInput {
1182
1704
  }, ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
1183
1705
  /** Whether an IME composition is in progress (CJK). While composing, don't filter or select. */
1184
1706
  this.composing = false;
1185
- this.dataAttr = attr;
1707
+ this.dataAttr = attr$1;
1186
1708
  this.rootContext.setInputElement(this.element);
1709
+ // Report the layout (Base UI's `inputInsidePopup`): a positioner ancestor means the input lives
1710
+ // inside the popup (e.g. a command palette), so the Trigger becomes the focusable
1711
+ // `role="combobox"`; otherwise the input is the tab stop and the Trigger is a `tabindex="-1"` toggle.
1712
+ this.rootContext.setInputLayout(inject(RdxComboboxPositioner, { optional: true }) ? 'inside' : 'outside');
1187
1713
  afterNextRender(() => {
1188
1714
  this.fieldRootContext?.setControlId(this.id());
1189
1715
  });
@@ -1198,22 +1724,31 @@ class RdxComboboxInput {
1198
1724
  if (this.composing || event.isComposing) {
1199
1725
  return;
1200
1726
  }
1201
- this.commitInput(event.target.value);
1727
+ this.commitInput(event.target.value, event);
1202
1728
  }
1203
1729
  onCompositionEnd(event) {
1204
1730
  this.composing = false;
1205
- this.commitInput(event.target.value);
1206
- }
1207
- commitInput(value) {
1208
- if (!this.rootContext.open()) {
1209
- this.rootContext.openPopup();
1731
+ this.commitInput(event.target.value, event);
1732
+ }
1733
+ commitInput(value, event) {
1734
+ // Base UI: clearing the field closes the popup only when the input is OUTSIDE it (and doesn't
1735
+ // open on click). When the input lives inside the popup, emptying the search must keep the popup
1736
+ // open (closing it would dismiss the field the user is typing in); otherwise typing (including
1737
+ // down to empty in browse mode) opens it.
1738
+ if (value === '' && !this.rootContext.openOnInputClick() && this.rootContext.inputLayout() !== 'inside') {
1739
+ this.rootContext.closePopup(false, 'input-clear', event);
1210
1740
  }
1211
- // setInputValue applies any autoHighlight (deferred until items mount).
1741
+ else if (!this.rootContext.open() && value.trim() !== '') {
1742
+ // Base UI opens on input only for a non-empty trimmed value — whitespace alone won't open it.
1743
+ this.rootContext.openPopup('input-change', event);
1744
+ }
1745
+ // setInputValue applies any autoHighlight (deferred until items mount) and, in single mode,
1746
+ // deselects when the field is emptied.
1212
1747
  this.rootContext.setInputValue(value);
1213
1748
  }
1214
- onClick() {
1749
+ onClick(event) {
1215
1750
  if (this.rootContext.openOnInputClick()) {
1216
- this.rootContext.openForBrowse();
1751
+ this.rootContext.openForBrowse('input-press', event);
1217
1752
  }
1218
1753
  }
1219
1754
  onFocus() {
@@ -1224,8 +1759,9 @@ class RdxComboboxInput {
1224
1759
  this.fieldRootContext?.setTouched(true);
1225
1760
  }
1226
1761
  onKeydown(event) {
1227
- // Don't interfere with IME composition or text-editing shortcuts / range selection. Home/End
1228
- // and Shift+Arrows must keep moving the caret, Ctrl/Meta combos stay browser shortcuts.
1762
+ // Don't interfere with IME composition or text-editing shortcuts / range selection. Shift+Arrows
1763
+ // and modified Home/End keep moving/extending the caret; Ctrl/Meta combos stay browser shortcuts.
1764
+ // (Plain Home/End navigate the grid below, but only in `grid` mode.)
1229
1765
  if (event.isComposing || this.composing) {
1230
1766
  return;
1231
1767
  }
@@ -1236,23 +1772,11 @@ class RdxComboboxInput {
1236
1772
  switch (event.key) {
1237
1773
  case 'ArrowDown':
1238
1774
  event.preventDefault();
1239
- this.rootContext.setKeyboardActive(true);
1240
- if (!open) {
1241
- this.rootContext.openAndHighlight('first');
1242
- }
1243
- else {
1244
- this.rootContext.highlightNext();
1245
- }
1775
+ this.rootContext.navigateByKeyboard(1, event);
1246
1776
  break;
1247
1777
  case 'ArrowUp':
1248
1778
  event.preventDefault();
1249
- this.rootContext.setKeyboardActive(true);
1250
- if (!open) {
1251
- this.rootContext.openAndHighlight('last');
1252
- }
1253
- else {
1254
- this.rootContext.highlightPrevious();
1255
- }
1779
+ this.rootContext.navigateByKeyboard(-1, event);
1256
1780
  break;
1257
1781
  case 'Enter':
1258
1782
  if (open) {
@@ -1262,33 +1786,69 @@ class RdxComboboxInput {
1262
1786
  if (hasHighlight) {
1263
1787
  // Select the highlighted item (and prevent an accidental form submit).
1264
1788
  event.preventDefault();
1265
- this.rootContext.selectHighlighted();
1789
+ this.rootContext.selectHighlighted(event);
1266
1790
  }
1267
1791
  else {
1268
1792
  // Nothing highlighted: just close, and let the form submit.
1269
- this.rootContext.closePopup(true);
1793
+ this.rootContext.closePopup(true, 'none', event);
1270
1794
  }
1271
1795
  }
1272
1796
  break;
1273
1797
  case 'Escape':
1274
- // Just close the popup (reverting the in-progress query); never clear the selection.
1275
1798
  if (open) {
1799
+ // Close the popup, reverting the in-progress query; keep the selection.
1276
1800
  event.preventDefault();
1277
- this.rootContext.closePopup(true);
1801
+ this.rootContext.closePopup(true, 'escape-key', event);
1802
+ }
1803
+ else if (!this.rootContext.popupMounted()) {
1804
+ // Base UI: Escape on a closed combobox clears the input text and the selection
1805
+ // (`clearSelection` resets both, a no-op while read-only / disabled). Guard on
1806
+ // `popupMounted`: the input's own Escape handler (the `open` branch above) already set
1807
+ // `open()` false when this same Escape just closed an open popup — in that case the
1808
+ // popup is still mounted (exiting) and we must not also clear.
1809
+ this.rootContext.clearSelection();
1278
1810
  }
1279
1811
  break;
1280
1812
  case 'Tab':
1281
- if (open) {
1282
- this.rootContext.closePopup(true);
1813
+ // Tab dismisses a real popup and lets focus move on. With no popup mounted (an always-open
1814
+ // inline layout) Tab must NOT close — it just moves focus on. Guard on `popupMounted`.
1815
+ if (open && this.rootContext.popupMounted()) {
1816
+ this.rootContext.closePopup(true, 'none', event);
1283
1817
  }
1284
1818
  break;
1819
+ case 'ArrowRight':
1820
+ // In a grid, the horizontal arrows move within a row.
1821
+ if (open && this.rootContext.grid()) {
1822
+ event.preventDefault();
1823
+ this.rootContext.setKeyboardActive(true);
1824
+ this.rootContext.highlightNextColumn();
1825
+ break;
1826
+ }
1827
+ this.maybeStepIntoChips('ArrowRight', event);
1828
+ break;
1285
1829
  case 'ArrowLeft':
1286
- // From the very start of the input in multiple mode, step into the chips.
1287
- if (this.rootContext.multiple() &&
1288
- this.element.selectionStart === 0 &&
1289
- this.element.selectionEnd === 0 &&
1290
- this.rootContext.focusLastChip()) {
1830
+ if (open && this.rootContext.grid()) {
1831
+ event.preventDefault();
1832
+ this.rootContext.setKeyboardActive(true);
1833
+ this.rootContext.highlightPreviousColumn();
1834
+ break;
1835
+ }
1836
+ this.maybeStepIntoChips('ArrowLeft', event);
1837
+ break;
1838
+ case 'Home':
1839
+ // In a grid the search box is a filter, so Home/End jump to the first/last cell rather
1840
+ // than moving the caret (outside a grid they keep their native text-editing behavior).
1841
+ if (open && this.rootContext.grid()) {
1842
+ event.preventDefault();
1843
+ this.rootContext.setKeyboardActive(true);
1844
+ this.rootContext.highlightFirst();
1845
+ }
1846
+ break;
1847
+ case 'End':
1848
+ if (open && this.rootContext.grid()) {
1291
1849
  event.preventDefault();
1850
+ this.rootContext.setKeyboardActive(true);
1851
+ this.rootContext.highlightLast();
1292
1852
  }
1293
1853
  break;
1294
1854
  case 'Backspace':
@@ -1298,20 +1858,35 @@ class RdxComboboxInput {
1298
1858
  break;
1299
1859
  }
1300
1860
  }
1861
+ /**
1862
+ * From the very start of the input in `multiple` mode, step into the chips. The key that points
1863
+ * toward the chips is direction-aware: `ArrowLeft` in LTR, `ArrowRight` in RTL.
1864
+ */
1865
+ maybeStepIntoChips(key, event) {
1866
+ const towardChips = this.rootContext.dir() === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
1867
+ if (key === towardChips &&
1868
+ this.rootContext.multiple() &&
1869
+ this.element.selectionStart === 0 &&
1870
+ this.element.selectionEnd === 0 &&
1871
+ this.rootContext.focusLastChip()) {
1872
+ event.preventDefault();
1873
+ }
1874
+ }
1301
1875
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxInput, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1302
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxInput, isStandalone: true, selector: "input[rdxComboboxInput]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "combobox", "autocomplete": "off", "aria-autocomplete": "list" }, listeners: { "input": "onInput($event)", "click": "onClick()", "focus": "onFocus()", "blur": "onBlur()", "keydown": "onKeydown($event)", "compositionstart": "composing = true", "compositionend": "onCompositionEnd($event)" }, properties: { "attr.id": "id()", "attr.aria-expanded": "rootContext.open()", "attr.aria-controls": "rootContext.listId", "attr.aria-labelledby": "rootContext.labelId()", "attr.aria-activedescendant": "rootContext.activeId()", "attr.aria-describedby": "describedBy()", "attr.aria-invalid": "invalidState() ? \"true\" : undefined", "attr.aria-required": "requiredState() ? \"true\" : undefined", "attr.aria-disabled": "disabledState() ? \"true\" : undefined", "attr.disabled": "disabledState() ? \"\" : undefined", "attr.readonly": "rootContext.readonly() ? \"\" : undefined", "attr.required": "requiredState() ? \"\" : undefined", "value": "rootContext.inputValue()", "attr.data-popup-open": "dataAttr(rootContext.open())", "attr.data-list-empty": "dataAttr(rootContext.visibleCount() === 0)", "attr.data-placeholder": "dataAttr(isEmptyValue())", "attr.data-invalid": "dataAttr(invalidState())", "attr.data-valid": "dataAttr(!invalidState())", "attr.data-disabled": "dataAttr(disabledState())", "attr.data-required": "dataAttr(requiredState())", "attr.data-filled": "dataAttr(filledState())", "attr.data-focused": "dataAttr(focusedState())" } }, exportAs: ["rdxComboboxInput"], hostDirectives: [{ directive: i1.RdxPopperAnchor }, { directive: i1$1.RdxDismissableLayerBranch }], ngImport: i0 }); }
1876
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxInput, isStandalone: true, selector: "input[rdxComboboxInput]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "combobox", "autocomplete": "off", "aria-autocomplete": "list" }, listeners: { "input": "onInput($event)", "click": "onClick($event)", "focus": "onFocus()", "blur": "onBlur()", "keydown": "onKeydown($event)", "compositionstart": "composing = true", "compositionend": "onCompositionEnd($event)" }, properties: { "attr.id": "id()", "attr.aria-haspopup": "rootContext.grid() ? \"grid\" : \"listbox\"", "attr.aria-expanded": "rootContext.open()", "attr.aria-controls": "rootContext.listId", "attr.aria-labelledby": "rootContext.labelId()", "attr.aria-activedescendant": "rootContext.activeId()", "attr.aria-describedby": "describedBy()", "attr.aria-invalid": "invalidState() ? \"true\" : undefined", "attr.aria-required": "requiredState() ? \"true\" : undefined", "attr.aria-disabled": "disabledState() ? \"true\" : undefined", "attr.disabled": "disabledState() ? \"\" : undefined", "attr.readonly": "rootContext.readonly() ? \"\" : undefined", "attr.required": "requiredState() ? \"\" : undefined", "value": "rootContext.inputValue()", "attr.data-popup-open": "dataAttr(rootContext.open())", "attr.data-list-empty": "dataAttr(rootContext.visibleCount() === 0)", "attr.data-placeholder": "dataAttr(isEmptyValue())", "attr.data-invalid": "dataAttr(invalidState())", "attr.data-valid": "dataAttr(!invalidState())", "attr.data-disabled": "dataAttr(disabledState())", "attr.data-required": "dataAttr(requiredState())", "attr.data-filled": "dataAttr(filledState())", "attr.data-focused": "dataAttr(focusedState())" } }, exportAs: ["rdxComboboxInput"], hostDirectives: [{ directive: i1.RdxPopperAnchor }, { directive: i1$1.RdxFloatingInsideElement }], ngImport: i0 }); }
1303
1877
  }
1304
1878
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxInput, decorators: [{
1305
1879
  type: Directive,
1306
1880
  args: [{
1307
1881
  selector: 'input[rdxComboboxInput]',
1308
1882
  exportAs: 'rdxComboboxInput',
1309
- hostDirectives: [RdxPopperAnchor, RdxDismissableLayerBranch],
1883
+ hostDirectives: [RdxPopperAnchor, RdxFloatingInsideElement],
1310
1884
  host: {
1311
1885
  role: 'combobox',
1312
1886
  autocomplete: 'off',
1313
1887
  'aria-autocomplete': 'list',
1314
1888
  '[attr.id]': 'id()',
1889
+ '[attr.aria-haspopup]': 'rootContext.grid() ? "grid" : "listbox"',
1315
1890
  '[attr.aria-expanded]': 'rootContext.open()',
1316
1891
  '[attr.aria-controls]': 'rootContext.listId',
1317
1892
  '[attr.aria-labelledby]': 'rootContext.labelId()',
@@ -1334,7 +1909,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1334
1909
  '[attr.data-filled]': 'dataAttr(filledState())',
1335
1910
  '[attr.data-focused]': 'dataAttr(focusedState())',
1336
1911
  '(input)': 'onInput($event)',
1337
- '(click)': 'onClick()',
1912
+ '(click)': 'onClick($event)',
1338
1913
  '(focus)': 'onFocus()',
1339
1914
  '(blur)': 'onBlur()',
1340
1915
  '(keydown)': 'onKeydown($event)',
@@ -1344,12 +1919,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1344
1919
  }]
1345
1920
  }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }] } });
1346
1921
 
1922
+ const attr = (value) => (value ? '' : undefined);
1923
+ /**
1924
+ * Optional wrapper around the input and its adornments (icon, clear, trigger). Mirrors the combobox
1925
+ * state via `data-*` so the whole group can be styled together (focus ring, disabled, etc.).
1926
+ *
1927
+ * @group Components
1928
+ */
1929
+ class RdxComboboxInputGroup {
1930
+ constructor() {
1931
+ this.rootContext = injectComboboxRootContext();
1932
+ /** Whether a value is selected (a non-empty array in multiple mode, or a non-nullish single value). */
1933
+ this.filled = computed(() => {
1934
+ const value = this.rootContext.value();
1935
+ if (Array.isArray(value)) {
1936
+ return value.length > 0;
1937
+ }
1938
+ return value !== null && value !== undefined;
1939
+ }, ...(ngDevMode ? [{ debugName: "filled" }] : /* istanbul ignore next */ []));
1940
+ this.dataAttr = attr;
1941
+ }
1942
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxInputGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1943
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxInputGroup, isStandalone: true, selector: "[rdxComboboxInputGroup]", host: { properties: { "attr.data-popup-open": "dataAttr(rootContext.open())", "attr.data-disabled": "dataAttr(rootContext.disabledState())", "attr.data-required": "dataAttr(rootContext.requiredState())", "attr.data-filled": "dataAttr(filled())" } }, exportAs: ["rdxComboboxInputGroup"], ngImport: i0 }); }
1944
+ }
1945
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxInputGroup, decorators: [{
1946
+ type: Directive,
1947
+ args: [{
1948
+ selector: '[rdxComboboxInputGroup]',
1949
+ exportAs: 'rdxComboboxInputGroup',
1950
+ host: {
1951
+ '[attr.data-popup-open]': 'dataAttr(rootContext.open())',
1952
+ '[attr.data-disabled]': 'dataAttr(rootContext.disabledState())',
1953
+ '[attr.data-required]': 'dataAttr(rootContext.requiredState())',
1954
+ '[attr.data-filled]': 'dataAttr(filled())'
1955
+ }
1956
+ }]
1957
+ }] });
1958
+
1959
+ /**
1960
+ * A row in a grid-layout combobox list. Groups the items in one row so the root can navigate by row
1961
+ * (`ArrowUp` / `ArrowDown`) and within a row (`ArrowLeft` / `ArrowRight`). Only meaningful when the
1962
+ * root has `grid` enabled; the root resolves an item's row from its nearest `[rdxComboboxRow]` ancestor.
1963
+ *
1964
+ * @group Components
1965
+ */
1966
+ class RdxComboboxRow {
1967
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxRow, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1968
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxRow, isStandalone: true, selector: "[rdxComboboxRow]", host: { attributes: { "role": "row" } }, exportAs: ["rdxComboboxRow"], ngImport: i0 }); }
1969
+ }
1970
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxRow, decorators: [{
1971
+ type: Directive,
1972
+ args: [{
1973
+ selector: '[rdxComboboxRow]',
1974
+ exportAs: 'rdxComboboxRow',
1975
+ host: {
1976
+ role: 'row'
1977
+ }
1978
+ }]
1979
+ }] });
1980
+
1347
1981
  const itemContext = () => {
1348
1982
  const item = inject(RdxComboboxItem);
1349
1983
  return {
1350
1984
  isSelected: item.isSelected,
1351
1985
  isHighlighted: item.isHighlighted,
1352
1986
  disabled: item.disabled,
1987
+ // Read-only `Signal` (not `InputSignal`) so autocomplete's computed `value` is assignable too.
1353
1988
  value: item.value
1354
1989
  };
1355
1990
  };
@@ -1383,6 +2018,16 @@ class RdxComboboxItem {
1383
2018
  this.elementId = computed(() => this.virtualized() ? this.rootContext.itemId(this.index() ?? -1) : this.id, ...(ngDevMode ? [{ debugName: "elementId" }] : /* istanbul ignore next */ []));
1384
2019
  this.ariaSetSize = computed(() => this.virtualized() ? this.rootContext.filteredItems().length : undefined, ...(ngDevMode ? [{ debugName: "ariaSetSize" }] : /* istanbul ignore next */ []));
1385
2020
  this.ariaPosInSet = computed(() => (this.virtualized() ? (this.index() ?? -1) + 1 : undefined), ...(ngDevMode ? [{ debugName: "ariaPosInSet" }] : /* istanbul ignore next */ []));
2021
+ /** The nearest enclosing grid row, if any (drives the `gridcell` role). */
2022
+ this.row = inject(RdxComboboxRow, { optional: true });
2023
+ /** `gridcell` only when actually inside a `RdxComboboxRow` of a grid list; otherwise `option`. */
2024
+ this.role = computed(() => (this.rootContext.grid() && this.row ? 'gridcell' : 'option'), ...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));
2025
+ /**
2026
+ * Whether selection is a meaningful concept here (Base UI's `selectable`). In `selectionMode="none"`
2027
+ * (every autocomplete option, and a pure-search combobox) options carry no selection state, so
2028
+ * `aria-selected` / `data-selected` are omitted entirely rather than rendered as `false`.
2029
+ */
2030
+ this.selectable = computed(() => this.rootContext.selectionMode() !== 'none', ...(ngDevMode ? [{ debugName: "selectable" }] : /* istanbul ignore next */ []));
1386
2031
  // Virtualized items are always rendered (the consumer only mounts the filtered window).
1387
2032
  this.isVisible = computed(() => (this.virtualized() ? true : this.rootContext.isVisible(this)), ...(ngDevMode ? [{ debugName: "isVisible" }] : /* istanbul ignore next */ []));
1388
2033
  this.isSelected = computed(() => this.rootContext.isSelected(this.value()), ...(ngDevMode ? [{ debugName: "isSelected" }] : /* istanbul ignore next */ []));
@@ -1390,6 +2035,9 @@ class RdxComboboxItem {
1390
2035
  ? this.rootContext.highlightedIndex() === this.index()
1391
2036
  : this.rootContext.highlightedItem() === this, ...(ngDevMode ? [{ debugName: "isHighlighted" }] : /* istanbul ignore next */ []));
1392
2037
  this.group = injectComboboxGroupContext(true);
2038
+ // Whether a primary-button pointerdown started on **this** item. A normal press+release here is
2039
+ // committed by `click`; `mouseup` is only the drag-end fallback for a press that began *elsewhere*.
2040
+ this.pointerDownStarted = false;
1393
2041
  const destroyRef = inject(DestroyRef);
1394
2042
  afterNextRender(() => {
1395
2043
  // Virtualized items are not registered: the root navigates over `items` data by index, and
@@ -1414,21 +2062,65 @@ class RdxComboboxItem {
1414
2062
  this.rootContext.unregisterItem(this);
1415
2063
  this.group?.unregisterItem(this);
1416
2064
  });
2065
+ // Keep the highlighted option in view while navigating a scrollable popup. `block: 'nearest'`
2066
+ // makes hover a no-op (the item is already visible) and only scrolls on keyboard navigation.
2067
+ afterRenderEffect(() => {
2068
+ if (!this.virtualized() && this.isHighlighted()) {
2069
+ this.element.scrollIntoView({ block: 'nearest' });
2070
+ }
2071
+ });
2072
+ // Reset the press flag whenever the popup closes (matches Base UI), so a later drag-end onto
2073
+ // this item isn't blocked by a stale press from an earlier interaction.
2074
+ effect(() => {
2075
+ if (!this.rootContext.open()) {
2076
+ this.pointerDownStarted = false;
2077
+ }
2078
+ });
1417
2079
  }
1418
2080
  onPointerDown(event) {
1419
- // Keep focus on the input; prevent the item from stealing focus on pointer/mouse down.
2081
+ if (event.button !== 0) {
2082
+ return;
2083
+ }
2084
+ // Keep focus on the input; prevent the item from stealing focus.
1420
2085
  event.preventDefault();
1421
2086
  this.rootContext.setKeyboardActive(false);
2087
+ this.pointerDownStarted = true;
1422
2088
  }
1423
- onPointerUp() {
2089
+ onMouseDown(event) {
2090
+ // Belt-and-suspenders for keeping focus on the input (and iOS Safari blur on tap).
2091
+ if (event.button === 0) {
2092
+ event.preventDefault();
2093
+ }
2094
+ }
2095
+ onMouseUp(event) {
2096
+ // Read-and-reset the press flag first (matches Base UI), so a press+release here doesn't leave
2097
+ // it set and block a later drag-end onto this same item. Drag-end: commit when the primary
2098
+ // button is released over the highlighted item while the press began on a *different* element
2099
+ // (so `click` won't fire here). A press that began on this item is committed by `click` instead.
2100
+ const startedHere = this.pointerDownStarted;
2101
+ this.pointerDownStarted = false;
2102
+ if (event.button !== 0 || startedHere || !this.isHighlighted()) {
2103
+ return;
2104
+ }
2105
+ this.commitSelection(event);
2106
+ }
2107
+ onClick(event) {
2108
+ // Primary selection trigger; also fires for programmatic `.click()`.
2109
+ this.commitSelection(event);
2110
+ }
2111
+ commitSelection(event) {
1424
2112
  if (this.virtualized()) {
1425
- this.rootContext.selectIndex(this.index() ?? -1);
2113
+ this.rootContext.selectIndex(this.index() ?? -1, event);
1426
2114
  }
1427
2115
  else {
1428
- this.rootContext.select(this);
2116
+ this.rootContext.select(this, event);
1429
2117
  }
1430
2118
  }
1431
2119
  onPointerMove() {
2120
+ // Hover highlighting disabled: leave `data-highlighted` to keyboard/auto-highlight only.
2121
+ if (!this.rootContext.highlightItemOnHover()) {
2122
+ return;
2123
+ }
1432
2124
  // Ignore the first move after keyboard navigation: arrow keys scroll the list under a still
1433
2125
  // cursor, and the resulting pointer event must not yank the highlight off the keyboard target.
1434
2126
  if (this.rootContext.isKeyboardActive()) {
@@ -1445,8 +2137,24 @@ class RdxComboboxItem {
1445
2137
  this.rootContext.setHighlight(this, 'pointer');
1446
2138
  }
1447
2139
  }
2140
+ // Clear a pointer-driven highlight when the cursor leaves the list (unless `keepHighlight`).
2141
+ // Moving to another element inside the list keeps it (the next item's move re-highlights).
2142
+ onPointerLeave(event) {
2143
+ if (event.pointerType === 'touch' ||
2144
+ !this.rootContext.open() ||
2145
+ !this.rootContext.highlightItemOnHover() ||
2146
+ this.rootContext.keepHighlight()) {
2147
+ return;
2148
+ }
2149
+ const related = event.relatedTarget;
2150
+ const list = related && document.getElementById(this.rootContext.listId);
2151
+ if (list && list.contains(related)) {
2152
+ return;
2153
+ }
2154
+ this.rootContext.clearHighlight();
2155
+ }
1448
2156
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1449
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxItem, isStandalone: true, selector: "[rdxComboboxItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, textValueInput: { classPropertyName: "textValueInput", publicName: "textValue", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "option" }, listeners: { "pointerdown": "onPointerDown($event)", "mousedown": "onPointerDown($event)", "pointerup": "onPointerUp()", "pointermove": "onPointerMove()" }, properties: { "attr.id": "elementId()", "attr.aria-selected": "isSelected()", "attr.aria-disabled": "disabled() ? \"true\" : undefined", "attr.aria-setsize": "ariaSetSize()", "attr.aria-posinset": "ariaPosInSet()", "attr.data-selected": "isSelected() ? \"\" : undefined", "attr.data-highlighted": "isHighlighted() ? \"\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined", "hidden": "!isVisible()", "attr.data-hidden": "isVisible() ? undefined : \"\"" } }, providers: [provideComboboxItemContext(itemContext)], exportAs: ["rdxComboboxItem"], ngImport: i0 }); }
2157
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxItem, isStandalone: true, selector: "[rdxComboboxItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, textValueInput: { classPropertyName: "textValueInput", publicName: "textValue", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "pointerdown": "onPointerDown($event)", "mousedown": "onMouseDown($event)", "mouseup": "onMouseUp($event)", "click": "onClick($event)", "pointermove": "onPointerMove()", "pointerleave": "onPointerLeave($event)" }, properties: { "attr.role": "role()", "attr.id": "elementId()", "attr.aria-selected": "selectable() ? isSelected() : undefined", "attr.aria-disabled": "disabled() ? \"true\" : undefined", "attr.aria-setsize": "ariaSetSize()", "attr.aria-posinset": "ariaPosInSet()", "attr.data-selected": "selectable() && isSelected() ? \"\" : undefined", "attr.data-highlighted": "isHighlighted() ? \"\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined", "hidden": "!isVisible()", "attr.data-hidden": "isVisible() ? undefined : \"\"" } }, providers: [provideComboboxItemContext(itemContext)], exportAs: ["rdxComboboxItem"], ngImport: i0 }); }
1450
2158
  }
1451
2159
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxItem, decorators: [{
1452
2160
  type: Directive,
@@ -1455,21 +2163,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1455
2163
  exportAs: 'rdxComboboxItem',
1456
2164
  providers: [provideComboboxItemContext(itemContext)],
1457
2165
  host: {
1458
- role: 'option',
2166
+ '[attr.role]': 'role()',
1459
2167
  '[attr.id]': 'elementId()',
1460
- '[attr.aria-selected]': 'isSelected()',
2168
+ '[attr.aria-selected]': 'selectable() ? isSelected() : undefined',
1461
2169
  '[attr.aria-disabled]': 'disabled() ? "true" : undefined',
1462
2170
  '[attr.aria-setsize]': 'ariaSetSize()',
1463
2171
  '[attr.aria-posinset]': 'ariaPosInSet()',
1464
- '[attr.data-selected]': 'isSelected() ? "" : undefined',
2172
+ '[attr.data-selected]': 'selectable() && isSelected() ? "" : undefined',
1465
2173
  '[attr.data-highlighted]': 'isHighlighted() ? "" : undefined',
1466
2174
  '[attr.data-disabled]': 'disabled() ? "" : undefined',
1467
2175
  '[hidden]': '!isVisible()',
1468
2176
  '[attr.data-hidden]': 'isVisible() ? undefined : ""',
1469
2177
  '(pointerdown)': 'onPointerDown($event)',
1470
- '(mousedown)': 'onPointerDown($event)',
1471
- '(pointerup)': 'onPointerUp()',
1472
- '(pointermove)': 'onPointerMove()'
2178
+ '(mousedown)': 'onMouseDown($event)',
2179
+ '(mouseup)': 'onMouseUp($event)',
2180
+ '(click)': 'onClick($event)',
2181
+ '(pointermove)': 'onPointerMove()',
2182
+ '(pointerleave)': 'onPointerLeave($event)'
1473
2183
  }
1474
2184
  }]
1475
2185
  }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], textValueInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "textValue", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }] } });
@@ -1526,7 +2236,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1526
2236
  }], ctorParameters: () => [] });
1527
2237
 
1528
2238
  /**
1529
- * The listbox container for options. Carries the id referenced by the input's `aria-controls`.
2239
+ * The listbox container for options. Carries the id referenced by the input's `aria-controls`, exposes
2240
+ * `data-empty` while no options match the current query (Base UI's `ComboboxList` empty state), and
2241
+ * switches its `role` to `grid` when the root has `grid` enabled.
1530
2242
  *
1531
2243
  * @group Components
1532
2244
  */
@@ -1534,8 +2246,27 @@ class RdxComboboxList {
1534
2246
  constructor() {
1535
2247
  this.rootContext = injectComboboxRootContext();
1536
2248
  }
2249
+ onKeydown(event) {
2250
+ if (event.key !== 'Enter') {
2251
+ return;
2252
+ }
2253
+ // Base UI bails early when disabled / read-only — don't swallow Enter (e.g. a form submit).
2254
+ if (this.rootContext.disabledState() || this.rootContext.readonly()) {
2255
+ return;
2256
+ }
2257
+ const hasHighlight = this.rootContext.virtualized()
2258
+ ? this.rootContext.highlightedIndex() >= 0
2259
+ : this.rootContext.highlightedItem() !== null;
2260
+ if (hasHighlight) {
2261
+ // Base UI `stopEvent`: also stop propagation so a parent keydown handler doesn't re-handle
2262
+ // Enter after the selection.
2263
+ event.preventDefault();
2264
+ event.stopPropagation();
2265
+ this.rootContext.selectHighlighted();
2266
+ }
2267
+ }
1537
2268
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxList, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1538
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxList, isStandalone: true, selector: "[rdxComboboxList]", host: { attributes: { "role": "listbox" }, properties: { "attr.id": "rootContext.listId", "attr.aria-multiselectable": "rootContext.multiple() ? \"true\" : undefined" } }, exportAs: ["rdxComboboxList"], ngImport: i0 }); }
2269
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxList, isStandalone: true, selector: "[rdxComboboxList]", host: { attributes: { "tabindex": "-1" }, listeners: { "keydown": "onKeydown($event)" }, properties: { "attr.role": "rootContext.grid() ? \"grid\" : \"listbox\"", "attr.id": "rootContext.listId", "attr.aria-multiselectable": "rootContext.multiple() ? \"true\" : undefined", "attr.data-empty": "rootContext.visibleCount() === 0 ? \"\" : undefined" } }, exportAs: ["rdxComboboxList"], ngImport: i0 }); }
1539
2270
  }
1540
2271
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxList, decorators: [{
1541
2272
  type: Directive,
@@ -1543,9 +2274,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1543
2274
  selector: '[rdxComboboxList]',
1544
2275
  exportAs: 'rdxComboboxList',
1545
2276
  host: {
1546
- role: 'listbox',
2277
+ // Base UI: the list is a programmatic focus target (`tabindex="-1"`) and selects the highlighted
2278
+ // item on Enter, for custom layouts that move focus onto the list rather than the input.
2279
+ tabindex: '-1',
2280
+ '[attr.role]': 'rootContext.grid() ? "grid" : "listbox"',
1547
2281
  '[attr.id]': 'rootContext.listId',
1548
- '[attr.aria-multiselectable]': 'rootContext.multiple() ? "true" : undefined'
2282
+ '[attr.aria-multiselectable]': 'rootContext.multiple() ? "true" : undefined',
2283
+ '[attr.data-empty]': 'rootContext.visibleCount() === 0 ? "" : undefined',
2284
+ '(keydown)': 'onKeydown($event)'
1549
2285
  }
1550
2286
  }]
1551
2287
  }] });
@@ -1559,166 +2295,181 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1559
2295
  class RdxComboboxPopup {
1560
2296
  constructor() {
1561
2297
  this.rootContext = injectComboboxRootContext();
1562
- this.dismissableLayer = inject(RdxDismissableLayer);
2298
+ this.floatingContext = inject(RDX_FLOATING_ROOT_CONTEXT);
2299
+ this.registration = inject(RDX_FLOATING_REGISTRATION, { optional: true });
1563
2300
  this.popper = injectPopperContentWrapperContext();
1564
2301
  this.element = inject(ElementRef).nativeElement;
1565
- // The popup mounts only while open, so locking on `modal` locks scroll for as long as a modal
1566
- // popup is open and releases it on close.
1567
- useScrollLock(this.rootContext.modal);
2302
+ // Activation policy (ADR 0016 §2 + §3): lock page scroll while a modal popup is OPEN. The gate
2303
+ // keys on `open` (not mounted), so the lock releases at close-start — before the exit animation
2304
+ // finishes — even though the popup stays mounted through it. For a **touch** open the anchored
2305
+ // helper only locks when the popup is effectively viewport-width, so a small dropdown stays
2306
+ // swipe-to-dismissable on mobile (§3).
2307
+ useAnchoredScrollLock(computed(() => this.rootContext.open() && this.rootContext.modal()), {
2308
+ touchOpen: () => this.rootContext.openedByTouch(),
2309
+ element: () => this.element
2310
+ });
1568
2311
  // The popup's animation determines when the open/close transition (onOpenChangeComplete) is done.
1569
2312
  const unregister = this.rootContext.registerTransitionElement(this.element);
1570
- inject(DestroyRef).onDestroy(unregister);
1571
- // The input keeps focus while the popup is open; it is registered as a layer branch, so
1572
- // focus/pointer interactions on it don't count as "outside" and won't self-dismiss. Escape
1573
- // is handled by the input (which calls preventDefault), so the layer won't dismiss for it.
1574
- this.dismissableLayer.dismiss.subscribe(() => this.rootContext.closePopup(true));
2313
+ // Track mounted state so Escape can tell "closing this open popup" from "already closed".
2314
+ this.rootContext.setPopupMounted(true);
2315
+ inject(DestroyRef).onDestroy(() => {
2316
+ unregister();
2317
+ this.rootContext.setPopupMounted(false);
2318
+ });
2319
+ // The popup is this layer's floating element (the inside surface for containment checks).
2320
+ this.floatingContext.setFloatingElement(this.element);
2321
+ // Dismissal (ADR 0015): an outside press, or focus leaving everything, closes the combobox. The
2322
+ // input / trigger / chips / clear are registered as "inside" (RdxFloatingInsideElement), so the
2323
+ // input keeping focus — or a press on those parts — never self-dismisses. Escape is owned by the
2324
+ // input (it preventDefaults + closes), so the capability does not handle it (`escapeKey: false`).
2325
+ new RdxDismiss(this.floatingContext, () => this.registration?.node() ?? null, {
2326
+ escapeKey: () => false,
2327
+ outsidePress: () => true,
2328
+ focusOutside: () => true,
2329
+ onDismiss: (reason, event) => this.rootContext.closePopup(true, reason === 'focus-outside' ? 'focus-out' : 'outside-press', event)
2330
+ });
1575
2331
  // For the "input inside the popup" pattern, move focus to the input once the popup is
1576
- // positioned. Focusing earlier fails in the browser: the portal `appendChild` blurs the
1577
- // input and an unplaced popup isn't yet visible/focusable.
1578
- effect(() => {
2332
+ // positioned. Use `afterRenderEffect` (not `effect`): when `isPositioned` flips true the
2333
+ // popup's final position/visibility is applied in the *following* render, so a synchronous
2334
+ // `effect` would call `focus()` while the element is still unfocusable and silently no-op.
2335
+ afterRenderEffect(() => {
1579
2336
  if (!this.popper.isPositioned() || !this.rootContext.open()) {
1580
2337
  return;
1581
2338
  }
1582
2339
  const input = this.rootContext.inputElement();
1583
2340
  if (input && input.closest('[rdxComboboxPopup]')) {
1584
- untracked(() => {
2341
+ // Base UI: a touch-open focuses the popup itself so Android keeps the virtual keyboard
2342
+ // closed; mouse/keyboard opens focus (and select) the search input as usual.
2343
+ if (this.rootContext.openedByTouch()) {
2344
+ this.element.focus();
2345
+ }
2346
+ else {
1585
2347
  input.focus();
1586
2348
  input.select();
1587
- });
2349
+ }
1588
2350
  }
1589
2351
  });
1590
2352
  }
2353
+ /**
2354
+ * Base UI focus handoff: if focus lands on the popup or the list (the `tabindex="-1"` programmatic
2355
+ * focus targets), hand it back to the input so arrow-key navigation (`aria-activedescendant`) keeps
2356
+ * working. Skipped for a touch interaction, where focus is intentionally parked on the popup to keep
2357
+ * the Android virtual keyboard closed.
2358
+ */
2359
+ onFocusIn(event) {
2360
+ if (this.rootContext.openedByTouch()) {
2361
+ return;
2362
+ }
2363
+ const input = this.rootContext.inputElement();
2364
+ const target = event.target;
2365
+ if (!input || !target || target === input) {
2366
+ return;
2367
+ }
2368
+ if (target === this.element || target.matches('[rdxComboboxList]')) {
2369
+ input.focus();
2370
+ }
2371
+ }
1591
2372
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPopup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1592
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxPopup, isStandalone: true, selector: "[rdxComboboxPopup]", host: { properties: { "attr.data-state": "rootContext.open() ? \"open\" : \"closed\"", "attr.data-open": "rootContext.open() ? \"\" : undefined", "attr.data-closed": "rootContext.open() ? undefined : \"\"", "attr.data-starting-style": "rootContext.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-ending-style": "rootContext.transitionStatus() === \"ending\" ? \"\" : undefined" } }, providers: [
1593
- // In modal mode, make content outside the popup inert (Base UI's `modal`).
1594
- provideRdxDismissableLayerConfig(() => ({ disableOutsidePointerEvents: injectComboboxRootContext().modal }))
1595
- ], exportAs: ["rdxComboboxPopup"], hostDirectives: [{ directive: i1.RdxPopperContent }, { directive: i1$1.RdxDismissableLayer }], ngImport: i0 }); }
2373
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxPopup, isStandalone: true, selector: "[rdxComboboxPopup]", host: { attributes: { "tabindex": "-1" }, listeners: { "focusin": "onFocusIn($event)" }, properties: { "attr.role": "rootContext.inputLayout() === \"inside\" ? \"dialog\" : \"presentation\"", "attr.data-state": "rootContext.open() ? \"open\" : \"closed\"", "attr.data-open": "rootContext.open() ? \"\" : undefined", "attr.data-closed": "rootContext.open() ? undefined : \"\"", "attr.data-starting-style": "rootContext.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-ending-style": "rootContext.transitionStatus() === \"ending\" ? \"\" : undefined" } }, exportAs: ["rdxComboboxPopup"], hostDirectives: [{ directive: i1.RdxPopperContent }, { directive: i2.RdxFloatingNodeRegistration }], ngImport: i0 }); }
1596
2374
  }
1597
2375
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPopup, decorators: [{
1598
2376
  type: Directive,
1599
2377
  args: [{
1600
2378
  selector: '[rdxComboboxPopup]',
1601
2379
  exportAs: 'rdxComboboxPopup',
1602
- hostDirectives: [RdxPopperContent, RdxDismissableLayer],
1603
- providers: [
1604
- // In modal mode, make content outside the popup inert (Base UI's `modal`).
1605
- provideRdxDismissableLayerConfig(() => ({ disableOutsidePointerEvents: injectComboboxRootContext().modal }))
1606
- ],
2380
+ hostDirectives: [RdxPopperContent, RdxFloatingNodeRegistration],
1607
2381
  host: {
2382
+ // Base UI: a `dialog` (focusable, tabindex -1) when the input lives inside the popup, otherwise
2383
+ // a presentational wrapper around the `listbox` (the List part owns the listbox role).
2384
+ tabindex: '-1',
2385
+ '[attr.role]': 'rootContext.inputLayout() === "inside" ? "dialog" : "presentation"',
1608
2386
  '[attr.data-state]': 'rootContext.open() ? "open" : "closed"',
1609
2387
  '[attr.data-open]': 'rootContext.open() ? "" : undefined',
1610
2388
  '[attr.data-closed]': 'rootContext.open() ? undefined : ""',
1611
2389
  '[attr.data-starting-style]': 'rootContext.transitionStatus() === "starting" ? "" : undefined',
1612
- '[attr.data-ending-style]': 'rootContext.transitionStatus() === "ending" ? "" : undefined'
2390
+ '[attr.data-ending-style]': 'rootContext.transitionStatus() === "ending" ? "" : undefined',
2391
+ '(focusin)': 'onFocusIn($event)'
1613
2392
  }
1614
2393
  }]
1615
2394
  }], ctorParameters: () => [] });
1616
2395
 
1617
2396
  /**
1618
- * Teleports the popup into a container (default `document.body`).
2397
+ * Structural directive that teleports the combobox popup into a container (default `document.body`)
2398
+ * while the combobox is open, and keeps it mounted until any CSS exit `@keyframes` finishes.
2399
+ *
2400
+ * Apply it with the `*` microsyntax on the positioner —
2401
+ * `<div *rdxComboboxPortal rdxComboboxPositioner>` — or as an explicit `<ng-template rdxComboboxPortal>`.
2402
+ * For a custom container use the explicit form with `[container]`.
1619
2403
  *
1620
2404
  * @group Components
1621
2405
  */
1622
2406
  class RdxComboboxPortal {
2407
+ constructor() {
2408
+ /**
2409
+ * Optional container to portal the content into. Defaults to `document.body`. Declared here (and
2410
+ * forwarded to the composed {@link RdxPortalPresence}) so the autocomplete portal can re-expose it
2411
+ * through its own `hostDirectives`.
2412
+ */
2413
+ this.container = input(...(ngDevMode ? [undefined, { debugName: "container" }] : /* istanbul ignore next */ []));
2414
+ }
1623
2415
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortal, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1624
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxPortal, isStandalone: true, selector: "[rdxComboboxPortal]", exportAs: ["rdxComboboxPortal"], hostDirectives: [{ directive: i1$2.RdxPortal, inputs: ["container", "container"] }], ngImport: i0 }); }
2416
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxPortal, isStandalone: true, selector: "ng-template[rdxComboboxPortal]", inputs: { container: { classPropertyName: "container", publicName: "container", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideRdxPresenceContext(() => ({ present: injectComboboxRootContext().present }))], exportAs: ["rdxComboboxPortal"], hostDirectives: [{ directive: i1$2.RdxPortalPresence, inputs: ["container", "container"] }], ngImport: i0 }); }
1625
2417
  }
1626
2418
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortal, decorators: [{
1627
2419
  type: Directive,
1628
2420
  args: [{
1629
- selector: '[rdxComboboxPortal]',
2421
+ selector: 'ng-template[rdxComboboxPortal]',
1630
2422
  exportAs: 'rdxComboboxPortal',
1631
- hostDirectives: [{ directive: RdxPortal, inputs: ['container'] }]
2423
+ hostDirectives: [{ directive: RdxPortalPresence, inputs: ['container'] }],
2424
+ providers: [provideRdxPresenceContext(() => ({ present: injectComboboxRootContext().present }))]
1632
2425
  }]
1633
- }] });
1634
-
2426
+ }], propDecorators: { container: [{ type: i0.Input, args: [{ isSignal: true, alias: "container", required: false }] }] } });
1635
2427
  /**
1636
- * Wraps the popup template in {@link RdxPresenceDirective} so it mounts/unmounts with the open state
1637
- * and can run enter/leave animations.
2428
+ * Dev-mode guard: `rdxComboboxPortal` used to be an attribute directive on a `<div>`. It is now
2429
+ * structural, so the old `<div rdxComboboxPortal>` markup would silently stop portaling — fail loudly
2430
+ * instead.
1638
2431
  *
1639
2432
  * @group Components
1640
2433
  */
1641
- class RdxComboboxPortalPresence {
1642
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortalPresence, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1643
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxPortalPresence, isStandalone: true, selector: "ng-template[rdxComboboxPortalPresence]", providers: [
1644
- provideRdxPresenceContext(() => {
1645
- const context = injectComboboxRootContext();
1646
- return { present: context.open };
1647
- })
1648
- ], hostDirectives: [{ directive: i1$3.RdxPresenceDirective }], ngImport: i0 }); }
2434
+ class RdxComboboxPortalMisuseGuard {
2435
+ constructor() {
2436
+ if (isDevMode()) {
2437
+ rdxDevError('combobox/portal-on-element', '`rdxComboboxPortal` is now a structural directive. ' +
2438
+ 'Use `*rdxComboboxPortal` on the positioner element or `<ng-template rdxComboboxPortal>`. ' +
2439
+ 'rdxComboboxPortalPresence has been removed.', 'components/combobox');
2440
+ }
2441
+ }
2442
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortalMisuseGuard, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2443
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxPortalMisuseGuard, isStandalone: true, selector: "[rdxComboboxPortal]:not(ng-template)", ngImport: i0 }); }
1649
2444
  }
1650
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortalPresence, decorators: [{
2445
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortalMisuseGuard, decorators: [{
1651
2446
  type: Directive,
1652
2447
  args: [{
1653
- selector: 'ng-template[rdxComboboxPortalPresence]',
1654
- hostDirectives: [RdxPresenceDirective],
1655
- providers: [
1656
- provideRdxPresenceContext(() => {
1657
- const context = injectComboboxRootContext();
1658
- return { present: context.open };
1659
- })
1660
- ]
2448
+ selector: '[rdxComboboxPortal]:not(ng-template)'
1661
2449
  }]
1662
- }] });
2450
+ }], ctorParameters: () => [] });
1663
2451
 
1664
2452
  /**
1665
- * Positions the popup relative to the input anchor using the popper engine. Re-exposes the popper
1666
- * positioning inputs.
2453
+ * A visual separator between groups of options. Carries `role="separator"` with a horizontal
2454
+ * orientation (it divides rows in a vertical list), so assistive tech announces the grouping break.
1667
2455
  *
1668
2456
  * @group Components
1669
2457
  */
1670
- class RdxComboboxPositioner {
1671
- constructor() {
1672
- this.side = input('bottom', ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
1673
- this.sideOffset = input(4, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
1674
- this.align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
1675
- this.alignOffset = input(0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
1676
- this.arrowPadding = input(0, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
1677
- this.avoidCollisions = input(true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1678
- this.collisionBoundary = input(...(ngDevMode ? [undefined, { debugName: "collisionBoundary" }] : /* istanbul ignore next */ []));
1679
- this.collisionPadding = input(0, ...(ngDevMode ? [{ debugName: "collisionPadding" }] : /* istanbul ignore next */ []));
1680
- this.sticky = input('partial', ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
1681
- this.hideWhenDetached = input(false, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1682
- this.updatePositionStrategy = input('optimized', ...(ngDevMode ? [{ debugName: "updatePositionStrategy" }] : /* istanbul ignore next */ []));
1683
- }
1684
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPositioner, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1685
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxPositioner, isStandalone: true, selector: "[rdxComboboxPositioner]", inputs: { side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, sideOffset: { classPropertyName: "sideOffset", publicName: "sideOffset", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, alignOffset: { classPropertyName: "alignOffset", publicName: "alignOffset", isSignal: true, isRequired: false, transformFunction: null }, arrowPadding: { classPropertyName: "arrowPadding", publicName: "arrowPadding", isSignal: true, isRequired: false, transformFunction: null }, avoidCollisions: { classPropertyName: "avoidCollisions", publicName: "avoidCollisions", isSignal: true, isRequired: false, transformFunction: null }, collisionBoundary: { classPropertyName: "collisionBoundary", publicName: "collisionBoundary", isSignal: true, isRequired: false, transformFunction: null }, collisionPadding: { classPropertyName: "collisionPadding", publicName: "collisionPadding", 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 }, updatePositionStrategy: { classPropertyName: "updatePositionStrategy", publicName: "updatePositionStrategy", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style": "{\n 'boxSizing': 'border-box',\n '--radix-combobox-content-transform-origin': 'var(--radix-popper-transform-origin)',\n '--radix-combobox-content-available-width': 'var(--radix-popper-available-width)',\n '--radix-combobox-content-available-height': 'var(--radix-popper-available-height)',\n '--radix-combobox-trigger-width': 'var(--radix-popper-anchor-width)',\n '--radix-combobox-trigger-height': 'var(--radix-popper-anchor-height)'\n }" } }, exportAs: ["rdxComboboxPositioner"], hostDirectives: [{ directive: i1.RdxPopperContentWrapper, inputs: ["side", "side", "sideOffset", "sideOffset", "align", "align", "alignOffset", "alignOffset", "arrowPadding", "arrowPadding", "avoidCollisions", "avoidCollisions", "collisionBoundary", "collisionBoundary", "collisionPadding", "collisionPadding", "sticky", "sticky", "hideWhenDetached", "hideWhenDetached", "updatePositionStrategy", "updatePositionStrategy"] }], ngImport: i0 }); }
2458
+ class RdxComboboxSeparator {
2459
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxSeparator, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2460
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxSeparator, isStandalone: true, selector: "[rdxComboboxSeparator]", host: { attributes: { "role": "separator", "aria-orientation": "horizontal" } }, exportAs: ["rdxComboboxSeparator"], ngImport: i0 }); }
1686
2461
  }
1687
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPositioner, decorators: [{
2462
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxSeparator, decorators: [{
1688
2463
  type: Directive,
1689
2464
  args: [{
1690
- selector: '[rdxComboboxPositioner]',
1691
- exportAs: 'rdxComboboxPositioner',
1692
- hostDirectives: [
1693
- {
1694
- directive: RdxPopperContentWrapper,
1695
- inputs: [
1696
- 'side',
1697
- 'sideOffset',
1698
- 'align',
1699
- 'alignOffset',
1700
- 'arrowPadding',
1701
- 'avoidCollisions',
1702
- 'collisionBoundary',
1703
- 'collisionPadding',
1704
- 'sticky',
1705
- 'hideWhenDetached',
1706
- 'updatePositionStrategy'
1707
- ]
1708
- }
1709
- ],
2465
+ selector: '[rdxComboboxSeparator]',
2466
+ exportAs: 'rdxComboboxSeparator',
1710
2467
  host: {
1711
- '[style]': `{
1712
- 'boxSizing': 'border-box',
1713
- '--radix-combobox-content-transform-origin': 'var(--radix-popper-transform-origin)',
1714
- '--radix-combobox-content-available-width': 'var(--radix-popper-available-width)',
1715
- '--radix-combobox-content-available-height': 'var(--radix-popper-available-height)',
1716
- '--radix-combobox-trigger-width': 'var(--radix-popper-anchor-width)',
1717
- '--radix-combobox-trigger-height': 'var(--radix-popper-anchor-height)'
1718
- }`
2468
+ role: 'separator',
2469
+ 'aria-orientation': 'horizontal'
1719
2470
  }
1720
2471
  }]
1721
- }], propDecorators: { side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], sideOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideOffset", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], alignOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignOffset", required: false }] }], arrowPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrowPadding", required: false }] }], avoidCollisions: [{ type: i0.Input, args: [{ isSignal: true, alias: "avoidCollisions", required: false }] }], collisionBoundary: [{ type: i0.Input, args: [{ isSignal: true, alias: "collisionBoundary", required: false }] }], collisionPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "collisionPadding", required: false }] }], sticky: [{ type: i0.Input, args: [{ isSignal: true, alias: "sticky", required: false }] }], hideWhenDetached: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideWhenDetached", required: false }] }], updatePositionStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "updatePositionStrategy", required: false }] }] } });
2472
+ }] });
1722
2473
 
1723
2474
  /**
1724
2475
  * A polite live region for async status (loading, result counts) announced without moving focus.
@@ -1727,7 +2478,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1727
2478
  */
1728
2479
  class RdxComboboxStatus {
1729
2480
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxStatus, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1730
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxStatus, isStandalone: true, selector: "[rdxComboboxStatus]", host: { attributes: { "role": "status", "aria-live": "polite" } }, exportAs: ["rdxComboboxStatus"], ngImport: i0 }); }
2481
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxStatus, isStandalone: true, selector: "[rdxComboboxStatus]", host: { attributes: { "role": "status", "aria-live": "polite", "aria-atomic": "true" } }, exportAs: ["rdxComboboxStatus"], ngImport: i0 }); }
1731
2482
  }
1732
2483
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxStatus, decorators: [{
1733
2484
  type: Directive,
@@ -1736,13 +2487,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1736
2487
  exportAs: 'rdxComboboxStatus',
1737
2488
  host: {
1738
2489
  role: 'status',
1739
- 'aria-live': 'polite'
2490
+ 'aria-live': 'polite',
2491
+ 'aria-atomic': 'true'
1740
2492
  }
1741
2493
  }]
1742
2494
  }] });
1743
2495
 
1744
2496
  /**
1745
- * Toggles the combobox popup. Carries `tabindex="-1"` so it never steals focus from the input.
2497
+ * Toggles the combobox popup. Its semantics depend on the layout (Base UI's `inputInsidePopup`):
2498
+ *
2499
+ * - **Input outside the popup** (default): a `tabindex="-1"` toggle button (`aria-haspopup="listbox"`)
2500
+ * that never steals focus from the input — `Tab` lands directly on the input.
2501
+ * - **Input inside the popup** (e.g. a command palette / emoji picker): the trigger becomes the primary
2502
+ * `role="combobox"` control — `tabindex="0"` (reachable via `Tab`), `aria-haspopup="dialog"`, and
2503
+ * `ArrowDown`/`ArrowUp` open the popup (which moves focus to the inner input and highlights an item).
2504
+ *
2505
+ * The trigger stays `Tab`-reachable by default and is demoted to `tabindex="-1"` only once an input is
2506
+ * detected *outside* the popup — so a trigger whose input lives in a not-yet-opened popup is focusable
2507
+ * from the first render (`inputLayout` is `unknown` until that input mounts).
1746
2508
  *
1747
2509
  * @group Components
1748
2510
  */
@@ -1753,35 +2515,57 @@ class RdxComboboxTrigger {
1753
2515
  this.rootContext.registerTrigger(this.element);
1754
2516
  inject(DestroyRef).onDestroy(() => this.rootContext.registerTrigger(null));
1755
2517
  }
1756
- onClick() {
2518
+ // Record whether the opening interaction is touch, so the popup can keep focus off the inner input
2519
+ // (and Android's virtual keyboard closed) when the input lives inside the popup.
2520
+ onPointerDown(event) {
2521
+ this.rootContext.setOpenedByTouch(event.pointerType === 'touch');
2522
+ }
2523
+ onClick(event) {
1757
2524
  if (this.rootContext.open()) {
1758
- this.rootContext.closePopup(true);
2525
+ this.rootContext.closePopup(true, 'trigger-press', event);
1759
2526
  }
1760
2527
  else {
1761
2528
  this.rootContext.focusInput();
1762
- this.rootContext.openForBrowse();
2529
+ this.rootContext.openForBrowse('trigger-press', event);
2530
+ }
2531
+ }
2532
+ onKeydown(event) {
2533
+ if (this.rootContext.disabledState() || this.rootContext.readonly()) {
2534
+ return;
2535
+ }
2536
+ // ArrowDown/ArrowUp open the popup and seed the first/last highlight; the popup's own
2537
+ // auto-focus then moves focus to the input (whether it lives inside or outside the popup).
2538
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
2539
+ event.preventDefault();
2540
+ // A keyboard open must focus the input, not the popup — clear any prior touch flag.
2541
+ this.rootContext.setOpenedByTouch(false);
2542
+ this.rootContext.openAndHighlight(event.key === 'ArrowUp' ? 'last' : 'first', 'list-navigation', event);
1763
2543
  }
1764
2544
  }
1765
2545
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1766
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxTrigger, isStandalone: true, selector: "button[rdxComboboxTrigger]", host: { attributes: { "type": "button", "tabindex": "-1", "aria-label": "Open" }, listeners: { "click": "onClick()" }, properties: { "attr.aria-expanded": "rootContext.open()", "attr.aria-controls": "rootContext.listId", "attr.aria-labelledby": "rootContext.labelId()", "attr.disabled": "rootContext.disabledState() ? \"\" : undefined", "attr.data-popup-open": "rootContext.open() ? \"\" : undefined", "attr.data-disabled": "rootContext.disabledState() ? \"\" : undefined" } }, exportAs: ["rdxComboboxTrigger"], hostDirectives: [{ directive: i1$1.RdxDismissableLayerBranch }], ngImport: i0 }); }
2546
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxTrigger, isStandalone: true, selector: "button[rdxComboboxTrigger]", host: { attributes: { "type": "button" }, listeners: { "pointerdown": "onPointerDown($event)", "click": "onClick($event)", "keydown": "onKeydown($event)" }, properties: { "attr.tabindex": "rootContext.inputLayout() === \"outside\" ? \"-1\" : \"0\"", "attr.role": "rootContext.inputLayout() === \"inside\" ? \"combobox\" : undefined", "attr.aria-haspopup": "rootContext.inputLayout() === \"inside\" ? \"dialog\" : \"listbox\"", "attr.aria-expanded": "rootContext.open()", "attr.aria-controls": "rootContext.listId", "attr.aria-labelledby": "rootContext.labelId()", "attr.aria-required": "rootContext.inputLayout() === \"inside\" && rootContext.requiredState() ? \"true\" : undefined", "attr.disabled": "rootContext.disabledState() ? \"\" : undefined", "attr.data-popup-open": "rootContext.open() ? \"\" : undefined", "attr.data-disabled": "rootContext.disabledState() ? \"\" : undefined" } }, exportAs: ["rdxComboboxTrigger"], hostDirectives: [{ directive: i1$1.RdxFloatingInsideElement }], ngImport: i0 }); }
1767
2547
  }
1768
2548
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxTrigger, decorators: [{
1769
2549
  type: Directive,
1770
2550
  args: [{
1771
2551
  selector: 'button[rdxComboboxTrigger]',
1772
2552
  exportAs: 'rdxComboboxTrigger',
1773
- hostDirectives: [RdxDismissableLayerBranch],
2553
+ hostDirectives: [RdxFloatingInsideElement],
1774
2554
  host: {
1775
2555
  type: 'button',
1776
- tabindex: '-1',
1777
- 'aria-label': 'Open',
2556
+ '[attr.tabindex]': 'rootContext.inputLayout() === "outside" ? "-1" : "0"',
2557
+ '[attr.role]': 'rootContext.inputLayout() === "inside" ? "combobox" : undefined',
2558
+ '[attr.aria-haspopup]': 'rootContext.inputLayout() === "inside" ? "dialog" : "listbox"',
1778
2559
  '[attr.aria-expanded]': 'rootContext.open()',
1779
2560
  '[attr.aria-controls]': 'rootContext.listId',
1780
2561
  '[attr.aria-labelledby]': 'rootContext.labelId()',
2562
+ '[attr.aria-required]': 'rootContext.inputLayout() === "inside" && rootContext.requiredState() ? "true" : undefined',
1781
2563
  '[attr.disabled]': 'rootContext.disabledState() ? "" : undefined',
1782
2564
  '[attr.data-popup-open]': 'rootContext.open() ? "" : undefined',
1783
2565
  '[attr.data-disabled]': 'rootContext.disabledState() ? "" : undefined',
1784
- '(click)': 'onClick()'
2566
+ '(pointerdown)': 'onPointerDown($event)',
2567
+ '(click)': 'onClick($event)',
2568
+ '(keydown)': 'onKeydown($event)'
1785
2569
  }
1786
2570
  }]
1787
2571
  }], ctorParameters: () => [] });
@@ -1835,21 +2619,24 @@ const _importsCombobox = [
1835
2619
  RdxComboboxAnchor,
1836
2620
  RdxComboboxLabel,
1837
2621
  RdxComboboxInput,
2622
+ RdxComboboxInputGroup,
1838
2623
  RdxComboboxValue,
1839
2624
  RdxComboboxTrigger,
1840
2625
  RdxComboboxIcon,
1841
2626
  RdxComboboxClear,
1842
2627
  RdxComboboxPortal,
1843
- RdxComboboxPortalPresence,
2628
+ RdxComboboxPortalMisuseGuard,
1844
2629
  RdxComboboxBackdrop,
1845
2630
  RdxComboboxPositioner,
1846
2631
  RdxComboboxPopup,
1847
2632
  RdxComboboxArrow,
1848
2633
  RdxComboboxList,
2634
+ RdxComboboxRow,
1849
2635
  RdxComboboxItem,
1850
2636
  RdxComboboxItemIndicator,
1851
2637
  RdxComboboxGroup,
1852
2638
  RdxComboboxGroupLabel,
2639
+ RdxComboboxSeparator,
1853
2640
  RdxComboboxEmpty,
1854
2641
  RdxComboboxStatus,
1855
2642
  RdxComboboxChips,
@@ -1862,21 +2649,24 @@ class RdxComboboxModule {
1862
2649
  RdxComboboxAnchor,
1863
2650
  RdxComboboxLabel,
1864
2651
  RdxComboboxInput,
2652
+ RdxComboboxInputGroup,
1865
2653
  RdxComboboxValue,
1866
2654
  RdxComboboxTrigger,
1867
2655
  RdxComboboxIcon,
1868
2656
  RdxComboboxClear,
1869
2657
  RdxComboboxPortal,
1870
- RdxComboboxPortalPresence,
2658
+ RdxComboboxPortalMisuseGuard,
1871
2659
  RdxComboboxBackdrop,
1872
2660
  RdxComboboxPositioner,
1873
2661
  RdxComboboxPopup,
1874
2662
  RdxComboboxArrow,
1875
2663
  RdxComboboxList,
2664
+ RdxComboboxRow,
1876
2665
  RdxComboboxItem,
1877
2666
  RdxComboboxItemIndicator,
1878
2667
  RdxComboboxGroup,
1879
2668
  RdxComboboxGroupLabel,
2669
+ RdxComboboxSeparator,
1880
2670
  RdxComboboxEmpty,
1881
2671
  RdxComboboxStatus,
1882
2672
  RdxComboboxChips,
@@ -1885,21 +2675,24 @@ class RdxComboboxModule {
1885
2675
  RdxComboboxAnchor,
1886
2676
  RdxComboboxLabel,
1887
2677
  RdxComboboxInput,
2678
+ RdxComboboxInputGroup,
1888
2679
  RdxComboboxValue,
1889
2680
  RdxComboboxTrigger,
1890
2681
  RdxComboboxIcon,
1891
2682
  RdxComboboxClear,
1892
2683
  RdxComboboxPortal,
1893
- RdxComboboxPortalPresence,
2684
+ RdxComboboxPortalMisuseGuard,
1894
2685
  RdxComboboxBackdrop,
1895
2686
  RdxComboboxPositioner,
1896
2687
  RdxComboboxPopup,
1897
2688
  RdxComboboxArrow,
1898
2689
  RdxComboboxList,
2690
+ RdxComboboxRow,
1899
2691
  RdxComboboxItem,
1900
2692
  RdxComboboxItemIndicator,
1901
2693
  RdxComboboxGroup,
1902
2694
  RdxComboboxGroupLabel,
2695
+ RdxComboboxSeparator,
1903
2696
  RdxComboboxEmpty,
1904
2697
  RdxComboboxStatus,
1905
2698
  RdxComboboxChips,
@@ -1919,5 +2712,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1919
2712
  * Generated bundle index. Do not edit.
1920
2713
  */
1921
2714
 
1922
- export { RdxComboboxAnchor, RdxComboboxArrow, RdxComboboxBackdrop, RdxComboboxChip, RdxComboboxChipRemove, RdxComboboxChips, RdxComboboxClear, RdxComboboxEmpty, RdxComboboxGroup, RdxComboboxGroupLabel, RdxComboboxIcon, RdxComboboxInput, RdxComboboxItem, RdxComboboxItemIndicator, RdxComboboxLabel, RdxComboboxList, RdxComboboxModule, RdxComboboxPopup, RdxComboboxPortal, RdxComboboxPortalPresence, RdxComboboxPositioner, RdxComboboxRoot, RdxComboboxStatus, RdxComboboxTrigger, RdxComboboxValue, _importsCombobox, injectComboboxChipContext, injectComboboxGroupContext, injectComboboxItemContext, injectComboboxRootContext, provideComboboxChipContext, provideComboboxGroupContext, provideComboboxItemContext, provideComboboxRootContext };
2715
+ export { RdxComboboxAnchor, RdxComboboxArrow, RdxComboboxBackdrop, RdxComboboxChip, RdxComboboxChipRemove, RdxComboboxChips, RdxComboboxClear, RdxComboboxEmpty, RdxComboboxGroup, RdxComboboxGroupLabel, RdxComboboxIcon, RdxComboboxInput, RdxComboboxInputGroup, RdxComboboxItem, RdxComboboxItemIndicator, RdxComboboxLabel, RdxComboboxList, RdxComboboxModule, RdxComboboxPopup, RdxComboboxPortal, RdxComboboxPortalMisuseGuard, RdxComboboxPositioner, RdxComboboxRoot, RdxComboboxRow, RdxComboboxSeparator, RdxComboboxStatus, RdxComboboxTrigger, RdxComboboxValue, _importsCombobox, injectComboboxChipContext, injectComboboxGroupContext, injectComboboxItemContext, injectComboboxRootContext, provideComboboxChipContext, provideComboboxGroupContext, provideComboboxItemContext, provideComboboxRootContext, useComboboxEngine };
1923
2716
  //# sourceMappingURL=radix-ng-primitives-combobox.mjs.map