@radix-ng/primitives 1.0.0-beta.3 → 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 (97) hide show
  1. package/README.md +1 -1
  2. package/fesm2022/radix-ng-primitives-accordion.mjs +5 -3
  3. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  4. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +3 -2
  5. package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
  6. package/fesm2022/radix-ng-primitives-autocomplete.mjs +617 -659
  7. package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -1
  8. package/fesm2022/radix-ng-primitives-calendar.mjs +5 -3
  9. package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
  10. package/fesm2022/radix-ng-primitives-combobox.mjs +1305 -572
  11. package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
  12. package/fesm2022/radix-ng-primitives-config.mjs +13 -4
  13. package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
  14. package/fesm2022/radix-ng-primitives-context-menu.mjs +51 -10
  15. package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
  16. package/fesm2022/radix-ng-primitives-core.mjs +1345 -64
  17. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  18. package/fesm2022/radix-ng-primitives-date-field.mjs +5 -3
  19. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  20. package/fesm2022/radix-ng-primitives-dialog.mjs +240 -112
  21. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  22. package/fesm2022/radix-ng-primitives-direction-provider.mjs +70 -0
  23. package/fesm2022/radix-ng-primitives-direction-provider.mjs.map +1 -0
  24. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +519 -184
  25. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
  26. package/fesm2022/radix-ng-primitives-drawer.mjs +3 -3
  27. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
  28. package/fesm2022/radix-ng-primitives-field.mjs +3 -2
  29. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
  30. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +517 -0
  31. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -0
  32. package/fesm2022/radix-ng-primitives-focus-scope.mjs +296 -70
  33. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  34. package/fesm2022/radix-ng-primitives-menu.mjs +861 -286
  35. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  36. package/fesm2022/radix-ng-primitives-menubar.mjs +32 -4
  37. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  38. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +144 -159
  39. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  40. package/fesm2022/radix-ng-primitives-popover.mjs +220 -205
  41. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  42. package/fesm2022/radix-ng-primitives-popper.mjs +94 -51
  43. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  44. package/fesm2022/radix-ng-primitives-presence.mjs +1 -1
  45. package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
  46. package/fesm2022/radix-ng-primitives-preview-card.mjs +141 -173
  47. package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -1
  48. package/fesm2022/radix-ng-primitives-roving-focus.mjs +4 -2
  49. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  50. package/fesm2022/radix-ng-primitives-scroll-area.mjs +5 -4
  51. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  52. package/fesm2022/radix-ng-primitives-select.mjs +211 -156
  53. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  54. package/fesm2022/radix-ng-primitives-slider.mjs +5 -3
  55. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  56. package/fesm2022/radix-ng-primitives-stepper.mjs +5 -3
  57. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  58. package/fesm2022/radix-ng-primitives-time-field.mjs +5 -3
  59. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  60. package/fesm2022/radix-ng-primitives-toast.mjs +15 -36
  61. package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -1
  62. package/fesm2022/radix-ng-primitives-toggle-group.mjs +5 -3
  63. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  64. package/fesm2022/radix-ng-primitives-toolbar.mjs +5 -3
  65. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  66. package/fesm2022/radix-ng-primitives-tooltip.mjs +73 -110
  67. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  68. package/package.json +10 -1
  69. package/types/radix-ng-primitives-accordion.d.ts +4 -3
  70. package/types/radix-ng-primitives-autocomplete.d.ts +217 -152
  71. package/types/radix-ng-primitives-calendar.d.ts +5 -3
  72. package/types/radix-ng-primitives-combobox.d.ts +672 -283
  73. package/types/radix-ng-primitives-config.d.ts +1 -1
  74. package/types/radix-ng-primitives-context-menu.d.ts +15 -5
  75. package/types/radix-ng-primitives-core.d.ts +762 -14
  76. package/types/radix-ng-primitives-date-field.d.ts +3 -2
  77. package/types/radix-ng-primitives-dialog.d.ts +77 -32
  78. package/types/radix-ng-primitives-direction-provider.d.ts +41 -0
  79. package/types/radix-ng-primitives-dismissable-layer.d.ts +147 -99
  80. package/types/radix-ng-primitives-field.d.ts +1 -0
  81. package/types/radix-ng-primitives-floating-focus-manager.d.ts +175 -0
  82. package/types/radix-ng-primitives-focus-scope.d.ts +132 -1
  83. package/types/radix-ng-primitives-menu.d.ts +186 -103
  84. package/types/radix-ng-primitives-navigation-menu.d.ts +37 -75
  85. package/types/radix-ng-primitives-popover.d.ts +59 -92
  86. package/types/radix-ng-primitives-popper.d.ts +39 -9
  87. package/types/radix-ng-primitives-preview-card.d.ts +39 -72
  88. package/types/radix-ng-primitives-roving-focus.d.ts +7 -6
  89. package/types/radix-ng-primitives-scroll-area.d.ts +2 -2
  90. package/types/radix-ng-primitives-select.d.ts +145 -108
  91. package/types/radix-ng-primitives-slider.d.ts +5 -4
  92. package/types/radix-ng-primitives-stepper.d.ts +4 -3
  93. package/types/radix-ng-primitives-time-field.d.ts +3 -2
  94. package/types/radix-ng-primitives-toast.d.ts +7 -7
  95. package/types/radix-ng-primitives-toggle-group.d.ts +5 -4
  96. package/types/radix-ng-primitives-toolbar.d.ts +3 -2
  97. package/types/radix-ng-primitives-tooltip.d.ts +24 -67
@@ -1,13 +1,15 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Directive, inject, booleanAttribute, Injector, model, input, computed, numberAttribute, output, signal, effect, untracked, isDevMode, ElementRef, afterNextRender, afterRenderEffect, DestroyRef, NgModule } from '@angular/core';
2
+ import { Directive, inject, booleanAttribute, Injector, ElementRef, model, input, computed, numberAttribute, output, signal, effect, untracked, isDevMode, ChangeDetectionStrategy, Component, afterNextRender, afterRenderEffect, DestroyRef, NgModule } from '@angular/core';
3
3
  import * as i1 from '@radix-ng/primitives/combobox';
4
- import { RdxComboboxAnchor, RdxComboboxBackdrop, provideComboboxRootContext, RdxComboboxEmpty, RdxComboboxGroup, RdxComboboxGroupLabel, RdxComboboxIcon, injectComboboxRootContext, injectComboboxGroupContext, provideComboboxItemContext, RdxComboboxItemIndicator, RdxComboboxLabel, RdxComboboxPortal, RdxComboboxStatus, RdxComboboxTrigger } from '@radix-ng/primitives/combobox';
4
+ import { RdxComboboxAnchor, RdxComboboxBackdrop, useComboboxEngine, provideComboboxRootContext, injectComboboxRootContext, RdxComboboxGroup, RdxComboboxGroupLabel, RdxComboboxIcon, injectComboboxGroupContext, provideComboboxItemContext, RdxComboboxItemIndicator, RdxComboboxLabel, RdxComboboxPortal, RdxComboboxSeparator, RdxComboboxStatus, RdxComboboxTrigger } from '@radix-ng/primitives/combobox';
5
5
  import * as i1$1 from '@radix-ng/primitives/popper';
6
- import { RdxPopperArrow, RdxPopper, RdxPopperAnchor, injectPopperContentWrapperContext, RdxPopperContent, RdxPopperContentWrapper } from '@radix-ng/primitives/popper';
7
- import * as i2 from '@radix-ng/primitives/dismissable-layer';
8
- import { RdxDismissableLayerBranch, RdxDismissableLayer, provideRdxDismissableLayerConfig } from '@radix-ng/primitives/dismissable-layer';
6
+ import { RdxPopperArrow, RdxPopper, RdxPopperContentWrapper, provideRdxPopperContentWrapper, provideRdxPopperContentConfig, RdxPopperAnchor, injectPopperContentWrapperContext, RdxPopperContent } from '@radix-ng/primitives/popper';
7
+ import * as i1$2 from '@radix-ng/primitives/dismissable-layer';
8
+ import { RdxFloatingInsideElement, RdxDismiss } from '@radix-ng/primitives/dismissable-layer';
9
9
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
10
- import { useTransitionStatus, injectId, useFilter, useListHighlight, isItemEqualToValue, itemToStringLabel, useScrollLock } from '@radix-ng/primitives/core';
10
+ import * as i2 from '@radix-ng/primitives/core';
11
+ import { createFloatingRootContext, rdxDevWarning, isItemEqualToValue, itemToStringLabel, createCancelableChangeEventDetails, provideFloatingTree, provideFloatingRootContext, setupInternalBackdrop, injectId, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, useAnchoredScrollLock, RdxFloatingNodeRegistration, rdxDevError } from '@radix-ng/primitives/core';
12
+ import { injectDirection } from '@radix-ng/primitives/direction-provider';
11
13
  import { injectFieldRootContext } from '@radix-ng/primitives/field';
12
14
 
13
15
  /**
@@ -68,23 +70,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
68
70
  }]
69
71
  }] });
70
72
 
73
+ // The engine stays private to the root; the context factory (a free function) reads it through this registry.
74
+ const engineRegistry = new WeakMap();
71
75
  /**
72
76
  * Builds the {@link RdxComboboxRootContext} the autocomplete parts consume. Autocomplete reuses the
73
- * combobox parts (List, Popup, Positioner, Item, …) verbatim, so the root exposes the exact combobox
74
- * context shape — configured for a single-value, `selectionMode: 'none'` control whose value is the
75
- * input string. Autocomplete-specific state (mode, inline completion, grid) is read by the local
76
- * parts via direct injection of {@link RdxAutocompleteRoot}.
77
+ * combobox parts (List, Popup, Positioner, Item, …) verbatim, so it provides the exact combobox context
78
+ * shape — configured for a single-value, `selectionMode: 'none'` control whose value **is** the input
79
+ * string. The shared engine supplies registry / filtering / highlight / inline; the root supplies the
80
+ * string-value semantics. Autocomplete-specific parts (mode, inline, grid) inject {@link RdxAutocompleteRoot}.
77
81
  */
78
82
  const context = () => {
79
83
  const root = inject(RdxAutocompleteRoot);
84
+ const engine = engineRegistry.get(root);
80
85
  return {
81
- listId: root.listId,
82
- labelId: root.labelId,
83
- setLabelId: (id) => root.labelId.set(id),
86
+ listId: engine.listId,
87
+ labelId: engine.labelId,
88
+ setLabelId: (id) => engine.setLabelId(id),
84
89
  dir: root.dir,
85
90
  value: root.value,
86
91
  inputValue: root.value,
87
92
  open: root.open,
93
+ present: root.present,
88
94
  multiple: root.alwaysFalse,
89
95
  selectionMode: root.noneMode,
90
96
  disabledState: root.disabledState,
@@ -93,42 +99,52 @@ const context = () => {
93
99
  openOnInputClick: root.openOnInputClick,
94
100
  modal: root.modal,
95
101
  virtualized: root.virtualized,
96
- filteredItems: root.filteredItems,
97
- highlightedItem: root.highlightedItem,
98
- highlightedIndex: root.highlightedIndex.asReadonly(),
99
- activeId: root.activeId,
100
- itemId: (index) => root.itemId(index),
101
- isKeyboardActive: () => root.isKeyboardActive(),
102
- setKeyboardActive: (value) => root.setKeyboardActive(value),
103
- transitionStatus: root.transitionStatus,
104
- registerTransitionElement: root.registerTransitionElement,
105
- visibleCount: root.visibleCount,
106
- isVisible: (item) => root.isVisible(item),
102
+ grid: root.grid,
103
+ filteredItems: engine.filteredItems,
104
+ highlightedItem: engine.highlightedItem,
105
+ highlightedIndex: engine.highlightedIndex,
106
+ activeId: engine.activeId,
107
+ itemId: (index) => engine.itemId(index),
108
+ isKeyboardActive: () => engine.isKeyboardActive(),
109
+ setKeyboardActive: (value) => engine.setKeyboardActive(value),
110
+ transitionStatus: engine.transitionStatus,
111
+ registerTransitionElement: engine.registerTransitionElement,
112
+ visibleCount: engine.visibleCount,
113
+ isVisible: (item) => engine.isVisible(item),
107
114
  isSelected: (value) => root.isSelectedValue(value),
108
- registerItem: (item) => root.registerItem(item),
109
- unregisterItem: (item) => root.unregisterItem(item),
110
- highlight: root.highlight,
111
- highlightNext: () => root.moveDown(),
112
- highlightPrevious: () => root.moveUp(),
113
- highlightFirst: () => root.highlightFirst('keyboard'),
114
- highlightLast: () => root.highlightLast('keyboard'),
115
- highlightIndex: (index, reason) => root.highlightIndex(index, reason),
116
- setHighlight: (item, reason) => root.setHighlight(item, reason),
117
- clearHighlight: () => root.clearHighlightState(),
115
+ registerItem: (item) => engine.registerItem(item),
116
+ unregisterItem: (item) => engine.unregisterItem(item),
117
+ highlight: engine.highlight,
118
+ highlightNext: () => engine.highlightNext('keyboard'),
119
+ highlightPrevious: () => engine.highlightPrevious('keyboard'),
120
+ highlightNextColumn: () => engine.highlightNextColumn('keyboard'),
121
+ highlightPreviousColumn: () => engine.highlightPreviousColumn('keyboard'),
122
+ highlightFirst: () => engine.highlightFirst('keyboard'),
123
+ highlightLast: () => engine.highlightLast('keyboard'),
124
+ highlightIndex: (index, reason) => engine.highlightIndex(index, reason),
125
+ setHighlight: (item, reason) => engine.setHighlight(item, reason),
126
+ clearHighlight: () => engine.clearHighlightState(),
118
127
  highlightItemOnHover: root.highlightItemOnHover,
119
128
  keepHighlight: root.keepHighlight,
120
- inputElement: root.inputElement.asReadonly(),
121
- setInputElement: (el) => root.inputElement.set(el),
122
- registerTrigger: (el) => (root.triggerElement = el),
123
- focusInput: () => root.focusInput(),
124
- openPopup: () => root.setOpen(true),
125
- openForBrowse: () => root.openForBrowse(),
126
- closePopup: (revert = true) => root.closePopup(revert),
129
+ inputElement: engine.inputElement,
130
+ setInputElement: (el) => engine.setInputElement(el),
131
+ inputLayout: engine.inputLayout,
132
+ setInputLayout: (layout) => engine.setInputLayout(layout),
133
+ openedByTouch: engine.openedByTouch,
134
+ setOpenedByTouch: (value) => engine.setOpenedByTouch(value),
135
+ popupMounted: engine.popupMounted,
136
+ setPopupMounted: (value) => engine.setPopupMounted(value),
137
+ registerTrigger: (el) => engine.setTrigger(el),
138
+ focusInput: () => engine.focusInput(),
139
+ openPopup: (reason, event) => root.setOpen(true, reason, event),
140
+ openForBrowse: (reason, event) => root.openForBrowse(reason, event),
141
+ closePopup: (revert = true, reason, event) => root.closePopup(revert, reason, event),
127
142
  setInputValue: (value) => root.setQuery(value),
128
- openAndHighlight: (edge) => root.openAndHighlight(edge),
129
- select: (item) => root.handleSelect(item),
130
- selectIndex: (index) => root.selectIndex(index),
131
- selectHighlighted: () => root.selectHighlighted(),
143
+ openAndHighlight: (edge, reason, event) => root.openAndHighlight(edge, reason, event),
144
+ navigateByKeyboard: (direction, event) => root.navigateByKeyboard(direction, event),
145
+ select: (item, event) => root.handleSelect(item, event),
146
+ selectIndex: (index, event) => root.selectIndex(index, event),
147
+ selectHighlighted: (event) => root.selectHighlighted(event),
132
148
  clearSelection: () => root.clearValue(),
133
149
  removeValue: () => undefined,
134
150
  removeLastValue: () => undefined,
@@ -136,31 +152,91 @@ const context = () => {
136
152
  focusLastChip: () => false,
137
153
  labelFor: (value) => root.labelFor(value),
138
154
  markAsTouched: () => root.markAsTouched()
155
+ // `value`/`inputValue` are the input string here, not `ComboboxValue`.
139
156
  };
140
157
  };
141
158
  /**
142
159
  * `autoHighlight` transform: pass `'always'` through verbatim, coerce everything else as a boolean
143
- * attribute (so the bare `autoHighlight` attribute reads `true`).
144
- *
145
- * Kept as a named module-level function rather than an inline `transform` arrow: compodoc 1.2.1
146
- * (the metadata source for the API contract and Storybook ArgTypes) hangs parsing an inline arrow
147
- * combined with explicit generic union type arguments on `input()`. A plain function reference sidesteps it.
160
+ * attribute (so the bare `autoHighlight` attribute reads `true`). Kept as a named function (compodoc
161
+ * hangs on an inline arrow combined with union-generic `input()`).
148
162
  */
149
163
  function coerceAutoHighlight(value) {
150
164
  return value === 'always' ? 'always' : booleanAttribute(value);
151
165
  }
152
166
  /**
153
- * Root of an Autocomplete — a text input with a filtered list of suggestions. Built on the combobox
154
- * engine with `selectionMode: 'none'`, so its value **is** the input string: typing, selecting an
155
- * item, or clearing all change a single string value. Owns input text, open state, filtering, inline
156
- * completion, and highlight-model navigation, and provides them to the combobox parts. Implements
167
+ * Root of an Autocomplete — a text input with a filtered list of suggestions. A thin configuration over
168
+ * the shared combobox engine (ADR 0014) with `selectionMode: 'none'`, so its value **is** the input
169
+ * string: typing, selecting an item, or clearing all change a single string value. Implements
157
170
  * `ControlValueAccessor` (the form value is the input string).
158
171
  *
159
172
  * @group Components
160
173
  */
161
174
  class RdxAutocompleteRoot {
175
+ // --- engine-backed surface read by the parts / tests ---
176
+ get listId() {
177
+ return this.engine.listId;
178
+ }
179
+ get labelId() {
180
+ return this.engine.labelId;
181
+ }
182
+ get inputElement() {
183
+ return this.engine.inputElement;
184
+ }
185
+ setInputElement(el) {
186
+ this.engine.setInputElement(el);
187
+ }
188
+ setInputLayout(layout) {
189
+ this.engine.setInputLayout(layout);
190
+ }
191
+ setPopupMounted(value) {
192
+ this.engine.setPopupMounted(value);
193
+ }
194
+ get inputLayout() {
195
+ return this.engine.inputLayout;
196
+ }
197
+ get openedByTouch() {
198
+ return this.engine.openedByTouch;
199
+ }
200
+ get popupMounted() {
201
+ return this.engine.popupMounted;
202
+ }
203
+ get highlight() {
204
+ return this.engine.highlight;
205
+ }
206
+ get highlightedItem() {
207
+ return this.engine.highlightedItem;
208
+ }
209
+ get highlightedIndex() {
210
+ return this.engine.highlightedIndex;
211
+ }
212
+ get activeId() {
213
+ return this.engine.activeId;
214
+ }
215
+ get filteredItems() {
216
+ return this.engine.filteredItems;
217
+ }
218
+ get visibleCount() {
219
+ return this.engine.visibleCount;
220
+ }
221
+ get inlinePreview() {
222
+ return this.engine.inlinePreview;
223
+ }
224
+ get transitionStatus() {
225
+ return this.engine.transitionStatus;
226
+ }
227
+ get registerTransitionElement() {
228
+ return this.engine.registerTransitionElement;
229
+ }
230
+ get triggerElement() {
231
+ return this.engine.triggerElement;
232
+ }
162
233
  constructor() {
163
234
  this.injector = inject(Injector);
235
+ /** Per-popup floating root context (ADR 0015) — `open` / `triggers` / reference for the dismissal engine. */
236
+ this.floatingContext = createFloatingRootContext({
237
+ ownerDocument: inject(ElementRef).nativeElement.ownerDocument,
238
+ open: () => this.open()
239
+ });
164
240
  /** The input text. This is the form value (controlled / uncontrolled via {@link defaultValue}). */
165
241
  this.value = model('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
166
242
  /** Initial input text when uncontrolled. */
@@ -172,7 +248,8 @@ class RdxAutocompleteRoot {
172
248
  /** Filtering / inline-completion behavior. See {@link AutocompleteMode}. */
173
249
  this.mode = input('list', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
174
250
  /** Text direction. */
175
- this.dir = input('ltr', ...(ngDevMode ? [{ debugName: "dir" }] : /* istanbul ignore next */ []));
251
+ this.dirInput = input(undefined, { ...(ngDevMode ? { debugName: "dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
252
+ this.dir = injectDirection(this.dirInput);
176
253
  /** Whether the autocomplete is disabled. */
177
254
  this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
178
255
  /** Whether the autocomplete is read-only. */
@@ -199,17 +276,9 @@ class RdxAutocompleteRoot {
199
276
  }
200
277
  return 'off';
201
278
  }, ...(ngDevMode ? [{ debugName: "autoHighlightMode" }] : /* istanbul ignore next */ []));
202
- /**
203
- * Whether moving the pointer over an item highlights it. `true` (default) paints `data-highlighted`
204
- * on hover; `false` suppresses hover-driven highlight entirely, letting CSS `:hover` stay distinct
205
- * from the `data-highlighted` (keyboard) state. Clicking an item still selects it.
206
- */
279
+ /** Whether moving the pointer over an item highlights it. */
207
280
  this.highlightItemOnHover = input(true, { ...(ngDevMode ? { debugName: "highlightItemOnHover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
208
- /**
209
- * Whether a pointer-driven highlight is kept when the cursor leaves the list. `false` (default)
210
- * clears the highlight on pointer-leave; `true` retains the last hovered item highlighted. Keyboard
211
- * navigation and auto-highlight are unaffected.
212
- */
281
+ /** Whether a pointer-driven highlight is kept when the cursor leaves the list. */
213
282
  this.keepHighlight = input(false, { ...(ngDevMode ? { debugName: "keepHighlight" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
214
283
  /** Whether clicking the input opens the popup. Defaults to `false` (Base UI default). */
215
284
  this.openOnInputClick = input(false, { ...(ngDevMode ? { debugName: "openOnInputClick" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
@@ -221,22 +290,18 @@ class RdxAutocompleteRoot {
221
290
  this.grid = input(false, { ...(ngDevMode ? { debugName: "grid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
222
291
  /**
223
292
  * Filter applied to items against the input query (only when {@link mode} is `'list'` / `'both'`).
224
- * - `undefined` (default): locale-aware "contains" via {@link useFilter};
225
- * - a function: custom matching;
226
- * - `null`: built-in filtering disabled (the consumer controls which items exist).
293
+ * `undefined` locale-aware contains; a function → custom `(value, query, itemToString)`; `null` →
294
+ * built-in filtering disabled.
227
295
  */
228
296
  this.filter = input(undefined, ...(ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []));
229
297
  /** Maximum number of matching items to show. `-1` (default) means no limit. */
230
298
  this.limit = input(-1, { ...(ngDevMode ? { debugName: "limit" } : /* istanbul ignore next */ {}), transform: numberAttribute });
231
- /**
232
- * The full set of item values, used as the source of truth for filtering and navigation in
233
- * {@link virtualized} mode (where only a window of `RdxAutocompleteItem` is mounted).
234
- */
299
+ /** The full set of item values for {@link virtualized} mode. */
235
300
  this.items = input(...(ngDevMode ? [undefined, { debugName: "items" }] : /* istanbul ignore next */ []));
236
301
  /** Whether the list is externally virtualized (navigation runs over {@link items} by index). */
237
302
  this.virtualized = input(false, { ...(ngDevMode ? { debugName: "virtualized" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
238
- /** How item values are compared for equality (function or object key). */
239
- this.by = input(...(ngDevMode ? [undefined, { debugName: "by" }] : /* istanbul ignore next */ []));
303
+ /** How item values are compared for equality (a comparator function or an object key). Base UI prop name. */
304
+ this.isItemEqualToValue = input(...(ngDevMode ? [undefined, { debugName: "isItemEqualToValue" }] : /* istanbul ignore next */ []));
240
305
  /** Converts an item value to its string label (filter text + input text on selection). */
241
306
  this.itemToStringValue = input(...(ngDevMode ? [undefined, { debugName: "itemToStringValue" }] : /* istanbul ignore next */ []));
242
307
  /** Emits when the input value changes (typing, selection, or clear), with the reason. */
@@ -247,100 +312,54 @@ class RdxAutocompleteRoot {
247
312
  this.onItemHighlighted = output();
248
313
  /** Emits after the open/close transition (including any exit animation) finishes. */
249
314
  this.onOpenChangeComplete = output();
250
- this.transition = useTransitionStatus((open) => this.onOpenChangeComplete.emit(open));
251
- /** Open/close transition phase, for `data-starting-style` / `data-ending-style`. */
252
- this.transitionStatus = this.transition.status;
253
- /** Registers the popup element whose animation determines transition completion. */
254
- this.registerTransitionElement = this.transition.registerElement;
255
- this.listId = injectId('rdx-autocomplete-list-');
256
- this.labelId = signal(undefined, ...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
257
- this.inputElement = signal(null, ...(ngDevMode ? [{ debugName: "inputElement" }] : /* istanbul ignore next */ []));
258
315
  /** Constant signals exposed to the combobox context (autocomplete is always single-value). */
259
316
  this.alwaysFalse = signal(false, ...(ngDevMode ? [{ debugName: "alwaysFalse" }] : /* istanbul ignore next */ []));
260
317
  this.noneMode = signal('none', ...(ngDevMode ? [{ debugName: "noneMode" }] : /* istanbul ignore next */ []));
261
318
  this.cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
262
319
  this.disabledState = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
263
320
  this.requiredState = computed(() => this.required(), ...(ngDevMode ? [{ debugName: "requiredState" }] : /* istanbul ignore next */ []));
264
- this.defaultFilter = useFilter();
265
- /** Whether built-in filtering applies in the current mode. */
321
+ this.preventUnmountOnClose = signal(false, ...(ngDevMode ? [{ debugName: "preventUnmountOnClose" }] : /* istanbul ignore next */ []));
322
+ this.present = computed(() => this.open() || this.preventUnmountOnClose(), ...(ngDevMode ? [{ debugName: "present" }] : /* istanbul ignore next */ []));
323
+ /** Whether built-in filtering applies in the current mode (`list` / `both`). */
266
324
  this.filteringMode = computed(() => this.mode() === 'list' || this.mode() === 'both', ...(ngDevMode ? [{ debugName: "filteringMode" }] : /* istanbul ignore next */ []));
267
- /** Whether inline completion applies in the current mode. */
325
+ /** Whether inline completion applies in the current mode (`both` / `inline`). */
268
326
  this.inlineMode = computed(() => this.mode() === 'both' || this.mode() === 'inline', ...(ngDevMode ? [{ debugName: "inlineMode" }] : /* istanbul ignore next */ []));
269
327
  /**
270
328
  * Whether the input text is a fresh user query rather than a committed selection's label. While
271
- * `false` (just opened, or showing a committed selection), the list is unfiltered so the user can
272
- * browse; it flips `true` on the first keystroke.
329
+ * `false` (just opened, or showing a committed selection), the list is unfiltered; it flips `true`
330
+ * on the first keystroke.
273
331
  */
274
332
  this.typed = signal(false, ...(ngDevMode ? [{ debugName: "typed" }] : /* istanbul ignore next */ []));
275
333
  /** The text the user actually typed, used as the filter query. */
276
334
  this.query = computed(() => (this.typed() ? (this.value() ?? '') : ''), ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
277
- /** Transient inline-completion preview (the active item's label), or `null`. */
278
- this.inlinePreview = signal(null, ...(ngDevMode ? [{ debugName: "inlinePreview" }] : /* istanbul ignore next */ []));
279
- /** What the input element displays: the inline preview if any, else the committed value. */
280
- this.displayValue = computed(() => this.inlinePreview() ?? this.value() ?? '', ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
281
- /** Suppresses inline completion for the current edit (set while a delete key is pressed). */
282
- this.suppressInline = false;
283
- this._items = signal([], ...(ngDevMode ? [{ debugName: "_items" }] : /* istanbul ignore next */ []));
284
- /** Registered items, sorted into DOM order. */
285
- this.orderedItems = computed(() => [...this._items()].sort((a, b) => domOrder(a.element, b.element)), ...(ngDevMode ? [{ debugName: "orderedItems" }] : /* istanbul ignore next */ []));
286
- /** Matching items in DOM order, capped at `limit`. The set of items the list shows. */
287
- this.visibleItems = computed(() => {
288
- const matching = this.orderedItems().filter((item) => this.matchesFilter(item));
289
- const limit = this.limit();
290
- return limit >= 0 ? matching.slice(0, limit) : matching;
291
- }, ...(ngDevMode ? [{ debugName: "visibleItems" }] : /* istanbul ignore next */ []));
292
- this.visibleSet = computed(() => new Set(this.visibleItems()), ...(ngDevMode ? [{ debugName: "visibleSet" }] : /* istanbul ignore next */ []));
293
- /** The filtered item values an external virtualizer should render. */
294
- this.filteredItems = computed(() => {
295
- const data = this.items();
296
- if (data === undefined) {
297
- return this.visibleItems().map((item) => item.value());
298
- }
299
- const limit = this.limit();
300
- const cap = (arr) => (limit >= 0 ? arr.slice(0, limit) : arr);
301
- if (!this.filteringMode()) {
302
- return cap(data);
303
- }
304
- const filter = this.filter();
305
- if (filter === null) {
306
- return cap(data);
307
- }
308
- const query = this.query();
309
- if (!query) {
310
- return cap(data);
311
- }
312
- const matcher = filter ?? this.defaultFilter.contains;
313
- return cap(data.filter((value) => matcher(this.textFor(value), query, value)));
314
- }, ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
315
- this.visibleCount = computed(() => this.virtualized() ? this.filteredItems().length : this.visibleItems().length, ...(ngDevMode ? [{ debugName: "visibleCount" }] : /* istanbul ignore next */ []));
316
- this.highlight = useListHighlight({
317
- items: this.orderedItems,
318
- isNavigable: (item) => this.isVisible(item) && !item.disabled(),
319
- getId: (item) => item.id,
320
- loop: this.loopFocus,
321
- injector: this.injector
335
+ /** The shared engine: item registry, filtering, highlight navigation (grid), inline, transition. */
336
+ this.engine = useComboboxEngine({
337
+ injector: this.injector,
338
+ listIdPrefix: 'rdx-autocomplete-list-',
339
+ popupSelector: '[rdxAutocompletePopup]',
340
+ open: this.open,
341
+ query: this.query,
342
+ filteringEnabled: this.filteringMode,
343
+ loopFocus: this.loopFocus,
344
+ autoHighlightMode: this.autoHighlightMode,
345
+ virtualized: this.virtualized,
346
+ items: this.items,
347
+ filter: this.filter,
348
+ limit: this.limit,
349
+ grid: this.grid,
350
+ rowOf: (element) => element.closest('[rdxAutocompleteRow]'),
351
+ inlineMode: this.inlineMode,
352
+ itemToString: (value) => this.labelFor(value),
353
+ onItemHighlighted: (details) => this.onItemHighlighted.emit(details),
354
+ onOpenChange: () => { },
355
+ onOpenChangeComplete: (open) => this.onOpenChangeComplete.emit(open)
322
356
  });
323
- this.highlightedItem = this.highlight.highlightedItem;
324
- /** Highlighted index into {@link filteredItems} in virtualized mode (`-1` when cleared). */
325
- this.highlightedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightedIndex" }] : /* istanbul ignore next */ []));
326
- /** Why the highlight last moved; read when emitting {@link onItemHighlighted}. */
327
- this.highlightReason = signal('none', ...(ngDevMode ? [{ debugName: "highlightReason" }] : /* istanbul ignore next */ []));
328
- this.activeId = computed(() => {
329
- if (this.virtualized()) {
330
- const index = this.highlightedIndex();
331
- return index >= 0 ? this.itemId(index) : undefined;
332
- }
333
- return this.highlight.activeId();
334
- }, ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
335
- /**
336
- * What to highlight once the list has mounted (items register asynchronously after opening):
337
- * an end edge, or `'first-match'` (the first item whose label starts with the query — used by
338
- * auto-highlight so inline completion lands on a real prefix match even when the list is static).
339
- */
340
- this.pendingHighlightEdge = signal(null, ...(ngDevMode ? [{ debugName: "pendingHighlightEdge" }] : /* istanbul ignore next */ []));
341
- this.keyboardActive = false;
342
- /** The trigger element, used as a focus fallback when the input lives inside the popup. */
343
- this.triggerElement = null;
357
+ /** What the input element displays: the inline preview if any, else the committed value. */
358
+ this.displayValue = computed(() => this.engine.inlinePreview() ?? this.value() ?? '', ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
359
+ engineRegistry.set(this, this.engine);
360
+ // Keep the dismissal reference in sync with the input (the anchor) so a press / focus on it counts
361
+ // as "inside" and never dismisses (ADR 0015).
362
+ effect(() => this.floatingContext.setReferenceElement(this.engine.inputElement() ?? null));
344
363
  // Apply uncontrolled defaults once.
345
364
  effect(() => {
346
365
  const initial = this.defaultValue();
@@ -353,219 +372,41 @@ class RdxAutocompleteRoot {
353
372
  this.open.set(true);
354
373
  }
355
374
  });
356
- // Emit open changes and drive the open/close transition (skip the initial run).
357
- let previousOpen = untracked(this.open);
358
- effect(() => {
359
- const open = this.open();
360
- if (open === previousOpen) {
361
- return;
362
- }
363
- previousOpen = open;
364
- untracked(() => {
365
- this.onOpenChange.emit(open);
366
- this.transition.start(open);
367
- });
368
- });
369
- // Emit highlight changes (skip the initial run).
370
- let highlightInitialized = false;
371
- effect(() => {
372
- const item = this.highlightedItem();
373
- const index = this.highlightedIndex();
374
- if (!highlightInitialized) {
375
- highlightInitialized = true;
376
- return;
377
- }
378
- untracked(() => {
379
- const reason = this.highlightReason();
380
- if (this.virtualized()) {
381
- const value = index >= 0 ? (this.filteredItems()[index] ?? null) : null;
382
- this.onItemHighlighted.emit({ value, index, reason });
383
- }
384
- else {
385
- const value = item ? item.value() : null;
386
- const itemIndex = item ? this.visibleItems().indexOf(item) : -1;
387
- this.onItemHighlighted.emit({ value, index: itemIndex, reason });
388
- }
389
- });
390
- });
391
- // Inline completion: mirror the active item's label into the input (with the completed suffix
392
- // selected by the input directive). Recomputes whenever the highlight or query changes.
393
- effect(() => {
394
- const item = this.highlightedItem();
395
- const query = this.query();
396
- const reason = this.highlightReason();
397
- untracked(() => this.recomputeInlinePreview(item, query, reason));
398
- });
399
- // Apply a deferred open-edge highlight once items (DOM refs) or filtered data have registered.
400
- effect(() => {
401
- const edge = this.pendingHighlightEdge();
402
- const count = this.virtualized() ? this.filteredItems().length : this.orderedItems().length;
403
- if (!this.open() || edge === null || count === 0) {
404
- return;
405
- }
406
- untracked(() => {
407
- this.highlightReason.set('none');
408
- if (this.virtualized()) {
409
- this.highlightedIndex.set(this.resolveVirtualizedEdge(edge, count));
410
- }
411
- else if (edge === 'last') {
412
- this.highlight.last();
413
- }
414
- else if (edge === 'first-match') {
415
- this.highlight.set(this.firstMatchItem() ?? this.firstVisibleNavigable());
416
- }
417
- else {
418
- this.highlight.first();
419
- }
420
- this.pendingHighlightEdge.set(null);
421
- });
422
- });
423
- // autoHighlight 'always': keep the first navigable item highlighted whenever the popup is open.
424
- effect(() => {
425
- this.orderedItems();
426
- this.visibleCount();
427
- if (this.autoHighlightMode() === 'always' && this.open()) {
428
- untracked(() => {
429
- if (this.virtualized()) {
430
- const length = this.filteredItems().length;
431
- const index = this.highlightedIndex();
432
- if ((index < 0 || index >= length) && length > 0) {
433
- this.highlightReason.set('none');
434
- this.highlightedIndex.set(0);
435
- }
436
- }
437
- else if (this.highlightedItem() === null) {
438
- this.highlightReason.set('none');
439
- this.highlight.first();
440
- }
441
- });
442
- }
443
- });
444
- // Virtualized self-heal: clear a highlight that filtering pushed out of range.
445
- effect(() => {
446
- if (!this.virtualized()) {
447
- return;
448
- }
449
- const length = this.filteredItems().length;
450
- untracked(() => {
451
- const index = this.highlightedIndex();
452
- if (index >= length && index !== -1) {
453
- this.highlightReason.set('none');
454
- this.highlightedIndex.set(-1);
455
- }
456
- });
457
- });
458
375
  // Virtualized object values can't be labelled from the DOM — warn once in dev.
459
376
  if (isDevMode()) {
460
- let warned = false;
461
377
  effect(() => {
462
- if (warned || !this.virtualized() || this.itemToStringValue()) {
378
+ if (!this.virtualized() || this.itemToStringValue()) {
463
379
  return;
464
380
  }
465
381
  if (this.items()?.some((value) => value !== null && typeof value === 'object')) {
466
- warned = true;
467
- console.warn('[rdxAutocompleteRoot] `virtualized` with object item values needs `itemToStringValue` ' +
468
- 'to render correct labels; falling back to a generic label.');
382
+ rdxDevWarning('autocomplete/virtualized-item-label', '`rdxAutocompleteRoot` `virtualized` with object item values needs `itemToStringValue` ' +
383
+ 'to render correct labels; falling back to a generic label.', 'components/autocomplete');
469
384
  }
470
385
  });
471
386
  }
472
387
  }
473
- recomputeInlinePreview(item, query, reason) {
474
- // Pointer hover must not rewrite the input (matches Base UI); only typing / keyboard nav complete it.
475
- if (!this.inlineMode() || this.suppressInline || !item || reason === 'pointer') {
476
- this.inlinePreview.set(null);
477
- return;
478
- }
479
- const label = item.textValue();
480
- if (label && query && label.toLowerCase().startsWith(query.toLowerCase())) {
481
- // Type-ahead: keep the typed prefix (preserving its casing) and complete the rest.
482
- this.inlinePreview.set(query + label.slice(query.length));
483
- return;
484
- }
485
- // Keyboard navigation to an item that doesn't prefix-match the query: show its full label so the
486
- // input reflects the highlighted option. Typing (reason 'none') never jumps to a non-prefix label.
487
- if (reason === 'keyboard' && label) {
488
- this.inlinePreview.set(label);
489
- return;
490
- }
491
- this.inlinePreview.set(null);
492
- }
493
388
  setSuppressInline(value) {
494
- this.suppressInline = value;
389
+ this.engine.setSuppressInline(value);
495
390
  }
496
391
  /** Opens the popup for browsing (resets the query to "pristine" and selects the input text). */
497
- openForBrowse() {
392
+ openForBrowse(reason = 'none', event = new Event('autocomplete.open-change')) {
498
393
  if (!this.open()) {
499
394
  this.typed.set(false);
500
395
  }
501
- this.setOpen(true);
502
- this.selectInputText();
396
+ this.setOpen(true, reason, event);
397
+ this.engine.selectInputText();
503
398
  if (this.autoHighlightMode() === 'always') {
504
- this.pendingHighlightEdge.set('first');
399
+ this.engine.setPendingHighlightEdge('first');
505
400
  }
506
401
  }
507
402
  /** Opens the popup and highlights the given edge once the list mounts. */
508
- openAndHighlight(edge) {
403
+ openAndHighlight(edge, reason = 'list-navigation', event = new Event('autocomplete.open-change')) {
509
404
  if (!this.open()) {
510
405
  this.typed.set(false);
511
406
  }
512
- this.setOpen(true);
513
- this.selectInputText();
514
- this.pendingHighlightEdge.set(edge);
515
- }
516
- /** Whether the item matches the active query (ignores the `limit` cap). */
517
- matchesFilter(item) {
518
- if (!this.filteringMode()) {
519
- return true;
520
- }
521
- const filter = this.filter();
522
- if (filter === null) {
523
- return true;
524
- }
525
- const query = this.query();
526
- const matcher = filter ?? this.defaultFilter.contains;
527
- return matcher(item.textValue(), query, item.value());
528
- }
529
- /** Whether the item is shown in the list (matches the query and is within `limit`). */
530
- isVisible(item) {
531
- return this.visibleSet().has(item);
532
- }
533
- /** The first visible, navigable item whose label starts with the query (for inline completion). */
534
- firstMatchItem() {
535
- const query = this.query();
536
- if (!query) {
537
- return null;
538
- }
539
- const lower = query.toLowerCase();
540
- return (this.visibleItems().find((item) => !item.disabled() && item.textValue().toLowerCase().startsWith(lower)) ??
541
- null);
542
- }
543
- /** The first visible, navigable item (auto-highlight fallback when no prefix match exists). */
544
- firstVisibleNavigable() {
545
- return this.visibleItems().find((item) => !item.disabled()) ?? null;
546
- }
547
- /** Resolves a pending edge to a virtualized index. */
548
- resolveVirtualizedEdge(edge, count) {
549
- if (edge === 'last') {
550
- return count - 1;
551
- }
552
- if (edge === 'first-match') {
553
- const query = this.query();
554
- if (query) {
555
- const lower = query.toLowerCase();
556
- const index = this.filteredItems().findIndex((value) => this.textFor(value).toLowerCase().startsWith(lower));
557
- if (index >= 0) {
558
- return index;
559
- }
560
- }
561
- }
562
- return 0;
563
- }
564
- isKeyboardActive() {
565
- return this.keyboardActive;
566
- }
567
- setKeyboardActive(value) {
568
- this.keyboardActive = value;
407
+ this.setOpen(true, reason, event);
408
+ this.engine.selectInputText();
409
+ this.engine.setPendingHighlightEdge(edge);
569
410
  }
570
411
  /** Whether the item's value/label matches the current input value (combobox context contract). */
571
412
  isSelectedValue(value) {
@@ -573,27 +414,33 @@ class RdxAutocompleteRoot {
573
414
  if (!current) {
574
415
  return false;
575
416
  }
576
- return value === current || isItemEqualToValue(value, current, this.by());
417
+ return value === current || isItemEqualToValue(value, current, this.isItemEqualToValue());
577
418
  }
578
- registerItem(item) {
579
- this._items.update((items) => [...items, item]);
580
- }
581
- unregisterItem(item) {
582
- this._items.update((items) => items.filter((i) => i !== item));
583
- }
584
- setOpen(open) {
585
- if (this.disabledState() || this.readOnly()) {
586
- return;
419
+ setOpen(open, reason = 'none', event = new Event('autocomplete.open-change')) {
420
+ if (open === this.open()) {
421
+ return true;
587
422
  }
423
+ if (open && (this.disabledState() || this.readOnly())) {
424
+ return false;
425
+ }
426
+ const change = this.createOpenChangeEvent(open, reason, event);
427
+ this.onOpenChange.emit(change.payload);
428
+ if (change.eventDetails.isCanceled()) {
429
+ return false;
430
+ }
431
+ this.preventUnmountOnClose.set(open ? false : change.shouldPreventUnmountOnClose());
588
432
  this.open.set(open);
433
+ return true;
589
434
  }
590
- closePopup(revert = true) {
435
+ closePopup(revert = true, reason = 'none', event = new Event('autocomplete.open-change')) {
591
436
  if (!this.open()) {
592
437
  return;
593
438
  }
594
- this.open.set(false);
595
- this.clearHighlightState();
596
- this.inlinePreview.set(null);
439
+ if (!this.setOpen(false, reason, event)) {
440
+ return;
441
+ }
442
+ this.engine.clearHighlightState();
443
+ this.engine.clearInlinePreview();
597
444
  if (revert) {
598
445
  this.typed.set(false);
599
446
  }
@@ -606,236 +453,169 @@ class RdxAutocompleteRoot {
606
453
  // Inline modes (`both` / `inline`) implicitly highlight the first prefix match so the input can
607
454
  // be inline-completed even without an explicit `autoHighlight`.
608
455
  if (this.autoHighlightMode() !== 'off' || (this.inlineMode() && value.length > 0)) {
609
- this.pendingHighlightEdge.set('first-match');
456
+ this.engine.setPendingHighlightEdge('first-match');
610
457
  }
611
458
  }
612
- /** Selects all input text so the next keystroke replaces a committed label. */
613
- selectInputText() {
614
- this.inputElement()?.select();
615
- }
616
459
  labelFor(value) {
617
460
  const custom = this.itemToStringValue();
618
461
  if (custom) {
619
462
  return custom(value);
620
463
  }
621
- const item = this.orderedItems().find((i) => isItemEqualToValue(i.value(), value, this.by()));
464
+ const item = this.engine.orderedItems().find((i) => isItemEqualToValue(i.value(), value, this.isItemEqualToValue()));
622
465
  return item ? item.textValue() : itemToStringLabel(value);
623
466
  }
624
- /** Label text for a raw item value (virtualized mode, no DOM element to read from). */
625
- textFor(value) {
626
- const custom = this.itemToStringValue();
627
- return custom ? custom(value) : itemToStringLabel(value);
628
- }
629
- /** Deterministic id for the item at `index` in virtualized mode (matches `aria-activedescendant`). */
630
467
  itemId(index) {
631
- return `${this.listId}-item-${index}`;
468
+ return this.engine.itemId(index);
469
+ }
470
+ isVisible(item) {
471
+ return this.engine.isVisible(item);
632
472
  }
633
- handleSelect(item) {
473
+ registerItem(item) {
474
+ this.engine.registerItem(item);
475
+ }
476
+ unregisterItem(item) {
477
+ this.engine.unregisterItem(item);
478
+ }
479
+ handleSelect(item, event = new Event('autocomplete.item-press')) {
634
480
  if (this.disabledState() || this.readOnly() || item.disabled()) {
635
481
  return;
636
482
  }
637
- this.commitSelection(item.textValue() || this.labelFor(item.value()));
483
+ this.commitSelection(item.textValue() || this.labelFor(item.value()), event);
638
484
  }
639
485
  /** Selects the filtered item at `index` (virtualized mode). */
640
- selectIndex(index) {
486
+ selectIndex(index, event = new Event('autocomplete.item-press')) {
641
487
  if (this.disabledState() || this.readOnly()) {
642
488
  return;
643
489
  }
644
- const value = this.filteredItems()[index];
490
+ const value = this.engine.filteredItems()[index];
645
491
  if (value === undefined) {
646
492
  return;
647
493
  }
648
- this.commitSelection(this.labelFor(value));
494
+ this.commitSelection(this.labelFor(value), event);
649
495
  }
650
496
  /** Commits a selection: the input value becomes the item's label, the popup closes. */
651
- commitSelection(label) {
652
- this.inlinePreview.set(null);
497
+ commitSelection(label, event = new Event('autocomplete.item-press')) {
498
+ // Capture focus before `commitValue` emits `onValueChange`, so restoration is skipped when the
499
+ // consumer moves focus in that callback (e.g. focusing a message field after an emoji press).
500
+ const activeBefore = typeof document !== 'undefined' ? document.activeElement : null;
501
+ this.engine.clearInlinePreview();
653
502
  this.commitValue(label, 'item-press');
654
503
  this.typed.set(false);
655
- this.open.set(false);
656
- this.clearHighlightState();
657
- this.restoreFocusAfterSelect();
504
+ this.closePopup(false, 'item-press', event);
505
+ this.engine.restoreFocusAfterSelect(activeBefore);
658
506
  this.maybeSubmit();
659
507
  }
660
508
  maybeSubmit() {
661
509
  if (this.submitOnItemClick()) {
662
- this.inputElement()?.form?.requestSubmit?.();
510
+ this.engine.inputElement()?.form?.requestSubmit?.();
663
511
  }
664
512
  }
665
- selectHighlighted() {
513
+ selectHighlighted(event = new Event('autocomplete.item-press')) {
666
514
  if (this.virtualized()) {
667
- const index = this.highlightedIndex();
515
+ const index = this.engine.highlightedIndex();
668
516
  if (index >= 0) {
669
- this.selectIndex(index);
517
+ this.selectIndex(index, event);
670
518
  }
671
519
  return;
672
520
  }
673
- const item = this.highlightedItem();
521
+ const item = this.engine.highlightedItem();
674
522
  if (item) {
675
- this.handleSelect(item);
523
+ this.handleSelect(item, event);
676
524
  }
677
525
  }
678
- // --- Highlight navigation facade (mode-aware: index-based when virtualized, grid-aware, else DOM-ref) ---
679
- moveDown() {
680
- this.highlightReason.set('keyboard');
681
- if (this.virtualized()) {
682
- this.stepIndex(1);
526
+ // --- highlight navigation facade (delegates to the engine; grid-aware) ---
527
+ navigateByKeyboard(direction, event = new Event('autocomplete.open-change')) {
528
+ this.engine.setKeyboardActive(true);
529
+ if (!this.open()) {
530
+ this.openAndHighlight(direction === 1 ? 'first' : 'last', 'list-navigation', event);
683
531
  }
684
- else if (this.grid()) {
685
- this.gridVertical(1);
532
+ else if (direction === 1) {
533
+ this.engine.highlightNext();
686
534
  }
687
535
  else {
688
- this.highlight.next();
536
+ this.engine.highlightPrevious();
689
537
  }
690
538
  }
539
+ moveDown() {
540
+ this.engine.highlightNext();
541
+ }
691
542
  moveUp() {
692
- this.highlightReason.set('keyboard');
693
- if (this.virtualized()) {
694
- this.stepIndex(-1);
695
- }
696
- else if (this.grid()) {
697
- this.gridVertical(-1);
698
- }
699
- else {
700
- this.highlight.previous();
701
- }
543
+ this.engine.highlightPrevious();
702
544
  }
703
- /** Grid: move to the next cell in DOM order. Non-grid: no-op (caret movement). */
704
545
  moveRight() {
705
- if (!this.grid() || this.virtualized()) {
706
- return;
707
- }
708
- this.highlightReason.set('keyboard');
709
- this.highlight.next();
546
+ this.engine.highlightNextColumn();
710
547
  }
711
548
  moveLeft() {
712
- if (!this.grid() || this.virtualized()) {
713
- return;
714
- }
715
- this.highlightReason.set('keyboard');
716
- this.highlight.previous();
549
+ this.engine.highlightPreviousColumn();
717
550
  }
718
551
  highlightFirst(reason = 'keyboard') {
719
- this.highlightReason.set(reason);
720
- if (this.virtualized()) {
721
- this.highlightedIndex.set(this.filteredItems().length > 0 ? 0 : -1);
722
- }
723
- else {
724
- this.highlight.first();
725
- }
552
+ this.engine.highlightFirst(reason);
726
553
  }
727
554
  highlightLast(reason = 'keyboard') {
728
- this.highlightReason.set(reason);
729
- if (this.virtualized()) {
730
- const length = this.filteredItems().length;
731
- this.highlightedIndex.set(length > 0 ? length - 1 : -1);
732
- }
733
- else {
734
- this.highlight.last();
735
- }
555
+ this.engine.highlightLast(reason);
736
556
  }
737
557
  highlightIndex(index, reason) {
738
- if (index < 0 || index >= this.filteredItems().length) {
739
- return;
740
- }
741
- this.highlightReason.set(reason);
742
- this.highlightedIndex.set(index);
558
+ this.engine.highlightIndex(index, reason);
743
559
  }
744
560
  setHighlight(item, reason) {
745
- this.highlightReason.set(reason);
746
- this.highlight.set(item);
561
+ this.engine.setHighlight(item, reason);
747
562
  }
748
563
  clearHighlightState() {
749
- this.highlight.clear();
750
- this.highlightedIndex.set(-1);
564
+ this.engine.clearHighlightState();
751
565
  }
752
- stepIndex(direction) {
753
- const length = this.filteredItems().length;
754
- if (length === 0) {
755
- this.highlightedIndex.set(-1);
756
- return;
757
- }
758
- const current = this.highlightedIndex();
759
- if (current < 0) {
760
- this.highlightedIndex.set(direction === 1 ? 0 : length - 1);
761
- return;
762
- }
763
- let next = current + direction;
764
- const loop = this.loopFocus();
765
- if (next < 0) {
766
- next = loop ? length - 1 : 0;
767
- }
768
- else if (next >= length) {
769
- next = loop ? 0 : length - 1;
770
- }
771
- this.highlightedIndex.set(next);
566
+ isKeyboardActive() {
567
+ return this.engine.isKeyboardActive();
772
568
  }
773
- /** Grid vertical move: keep the column index, jump to the adjacent row. */
774
- gridVertical(direction) {
775
- const rows = this.gridRows();
776
- if (rows.length === 0) {
777
- return;
778
- }
779
- const current = this.highlightedItem();
780
- if (!current) {
781
- const row = direction === 1 ? rows[0] : rows[rows.length - 1];
782
- this.highlight.set(row[0] ?? null);
783
- return;
784
- }
785
- let rowIndex = rows.findIndex((row) => row.includes(current));
786
- const col = rowIndex >= 0 ? rows[rowIndex].indexOf(current) : 0;
787
- const loop = this.loopFocus();
788
- rowIndex += direction;
789
- if (rowIndex < 0) {
790
- rowIndex = loop ? rows.length - 1 : 0;
791
- }
792
- else if (rowIndex >= rows.length) {
793
- rowIndex = loop ? 0 : rows.length - 1;
794
- }
795
- const targetRow = rows[rowIndex];
796
- this.highlight.set(targetRow[Math.min(col, targetRow.length - 1)] ?? null);
797
- }
798
- /** Visible items grouped into rows by their nearest `[rdxAutocompleteRow]` ancestor (DOM order). */
799
- gridRows() {
800
- const rows = new Map();
801
- for (const item of this.visibleItems()) {
802
- const key = item.element.closest('[rdxAutocompleteRow]');
803
- const bucket = rows.get(key);
804
- if (bucket) {
805
- bucket.push(item);
806
- }
807
- else {
808
- rows.set(key, [item]);
809
- }
810
- }
811
- return [...rows.values()];
569
+ setKeyboardActive(value) {
570
+ this.engine.setKeyboardActive(value);
812
571
  }
813
572
  clearValue() {
573
+ if (this.disabledState() || this.readOnly()) {
574
+ return;
575
+ }
814
576
  this.commitValue('', 'input-clear');
815
577
  this.typed.set(true);
816
- this.inlinePreview.set(null);
817
- this.focusInput();
578
+ this.engine.clearInlinePreview();
579
+ this.engine.clearHighlightState();
580
+ this.engine.focusInput();
818
581
  }
819
582
  focusInput() {
820
- this.inputElement()?.focus();
821
- }
822
- restoreFocusAfterSelect() {
823
- const input = this.inputElement();
824
- if (input && !input.closest('[rdxAutocompletePopup]')) {
825
- input.focus();
826
- }
827
- else {
828
- this.triggerElement?.focus();
829
- }
583
+ this.engine.focusInput();
830
584
  }
831
585
  markAsTouched() {
832
586
  this.onTouched?.();
833
587
  }
834
588
  commitValue(value, reason) {
589
+ // Mirror combobox's guarded commit: never mutate the value while disabled or read-only
590
+ // (the input is the form value here, so Clear / item-press / typing must all be inert).
591
+ if (this.disabledState() || this.readOnly()) {
592
+ return;
593
+ }
835
594
  this.value.set(value);
836
595
  this.onValueChange.emit({ value, reason });
837
596
  this.onChange?.(value);
838
597
  }
598
+ createOpenChangeEvent(open, reason, event) {
599
+ const change = createCancelableChangeEventDetails(reason, event, this.resolveOpenChangeTrigger(event));
600
+ return {
601
+ payload: {
602
+ open,
603
+ reason,
604
+ event: change.eventDetails.event,
605
+ trigger: change.eventDetails.trigger,
606
+ eventDetails: change.eventDetails
607
+ },
608
+ eventDetails: change.eventDetails,
609
+ shouldPreventUnmountOnClose: change.shouldPreventUnmountOnClose
610
+ };
611
+ }
612
+ resolveOpenChangeTrigger(event) {
613
+ const target = event.target;
614
+ if (target instanceof HTMLElement) {
615
+ return target;
616
+ }
617
+ return this.engine.triggerElement ?? this.engine.inputElement() ?? undefined;
618
+ }
839
619
  // ControlValueAccessor (the form value is the input string)
840
620
  writeValue(value) {
841
621
  untracked(() => this.value.set(value ?? ''));
@@ -850,9 +630,12 @@ class RdxAutocompleteRoot {
850
630
  this.cvaDisabled.set(isDisabled);
851
631
  }
852
632
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
853
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxAutocompleteRoot, isStandalone: true, selector: "[rdxAutocompleteRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", 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 }, mode: { classPropertyName: "mode", publicName: "mode", 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 }, 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 }, grid: { classPropertyName: "grid", publicName: "grid", 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 }, itemToStringValue: { classPropertyName: "itemToStringValue", publicName: "itemToStringValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", open: "openChange", onValueChange: "onValueChange", onOpenChange: "onOpenChange", onItemHighlighted: "onItemHighlighted", onOpenChangeComplete: "onOpenChangeComplete" }, host: { properties: { "attr.data-disabled": "disabledState() ? \"\" : undefined" } }, providers: [
633
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxAutocompleteRoot, isStandalone: true, selector: "[rdxAutocompleteRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", 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 }, mode: { classPropertyName: "mode", publicName: "mode", 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 }, grid: { classPropertyName: "grid", publicName: "grid", 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 }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToStringValue: { classPropertyName: "itemToStringValue", publicName: "itemToStringValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", open: "openChange", onValueChange: "onValueChange", onOpenChange: "onOpenChange", onItemHighlighted: "onItemHighlighted", onOpenChangeComplete: "onOpenChangeComplete" }, host: { properties: { "attr.data-disabled": "disabledState() ? \"\" : undefined" } }, providers: [
854
634
  provideComboboxRootContext(context),
855
- { provide: NG_VALUE_ACCESSOR, useExisting: RdxAutocompleteRoot, multi: true }
635
+ { provide: NG_VALUE_ACCESSOR, useExisting: RdxAutocompleteRoot, multi: true },
636
+ // New floating foundation (ADR 0015/0017) — the dismissal capability reads this shared context.
637
+ provideFloatingTree(),
638
+ provideFloatingRootContext(() => inject(RdxAutocompleteRoot).floatingContext)
856
639
  ], exportAs: ["rdxAutocompleteRoot"], hostDirectives: [{ directive: i1$1.RdxPopper }], ngImport: i0 }); }
857
640
  }
858
641
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteRoot, decorators: [{
@@ -862,25 +645,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
862
645
  exportAs: 'rdxAutocompleteRoot',
863
646
  providers: [
864
647
  provideComboboxRootContext(context),
865
- { provide: NG_VALUE_ACCESSOR, useExisting: RdxAutocompleteRoot, multi: true }
648
+ { provide: NG_VALUE_ACCESSOR, useExisting: RdxAutocompleteRoot, multi: true },
649
+ // New floating foundation (ADR 0015/0017) — the dismissal capability reads this shared context.
650
+ provideFloatingTree(),
651
+ provideFloatingRootContext(() => inject(RdxAutocompleteRoot).floatingContext)
866
652
  ],
867
653
  hostDirectives: [RdxPopper],
868
654
  host: {
869
655
  '[attr.data-disabled]': 'disabledState() ? "" : undefined'
870
656
  }
871
657
  }]
872
- }], 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 }] }], 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 }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", 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 }] }], 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 }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", 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 }] }], itemToStringValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringValue", required: false }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onItemHighlighted: [{ type: i0.Output, args: ["onItemHighlighted"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
873
- /** DOM-order comparator for two elements (precedes → -1, follows → 1). */
874
- function domOrder(a, b) {
875
- const position = a.compareDocumentPosition(b);
876
- if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
877
- return -1;
878
- }
879
- if (position & Node.DOCUMENT_POSITION_PRECEDING) {
880
- return 1;
881
- }
882
- return 0;
883
- }
658
+ }], 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 }] }], 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 }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", 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 }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", 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 }] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToStringValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringValue", required: false }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onItemHighlighted: [{ type: i0.Output, args: ["onItemHighlighted"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
884
659
 
885
660
  /**
886
661
  * Clears the input value. Hidden when there is nothing to clear.
@@ -891,45 +666,71 @@ class RdxAutocompleteClear {
891
666
  constructor() {
892
667
  this.root = inject(RdxAutocompleteRoot);
893
668
  this.isEmpty = computed(() => !this.root.value(), ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
669
+ /** Disabled when the control is disabled or read-only (clearing is a mutation). */
670
+ this.isDisabled = computed(() => this.root.disabledState() || this.root.readOnly(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
894
671
  }
895
672
  onClick() {
896
673
  this.root.clearValue();
897
674
  }
898
675
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteClear, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
899
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxAutocompleteClear, isStandalone: true, selector: "button[rdxAutocompleteClear]", host: { attributes: { "type": "button", "tabindex": "-1", "aria-label": "Clear" }, listeners: { "click": "onClick()" }, properties: { "hidden": "isEmpty()", "attr.disabled": "root.disabledState() ? \"\" : undefined" } }, exportAs: ["rdxAutocompleteClear"], hostDirectives: [{ directive: i2.RdxDismissableLayerBranch }], ngImport: i0 }); }
676
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxAutocompleteClear, isStandalone: true, selector: "button[rdxAutocompleteClear]", host: { attributes: { "type": "button", "tabindex": "-1", "aria-label": "Clear" }, listeners: { "click": "onClick()" }, properties: { "hidden": "isEmpty()", "attr.disabled": "isDisabled() ? \"\" : undefined" } }, exportAs: ["rdxAutocompleteClear"], hostDirectives: [{ directive: i1$2.RdxFloatingInsideElement }], ngImport: i0 }); }
900
677
  }
901
678
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteClear, decorators: [{
902
679
  type: Directive,
903
680
  args: [{
904
681
  selector: 'button[rdxAutocompleteClear]',
905
682
  exportAs: 'rdxAutocompleteClear',
906
- hostDirectives: [RdxDismissableLayerBranch],
683
+ hostDirectives: [RdxFloatingInsideElement],
907
684
  host: {
908
685
  type: 'button',
909
686
  tabindex: '-1',
910
687
  'aria-label': 'Clear',
911
688
  '[hidden]': 'isEmpty()',
912
- '[attr.disabled]': 'root.disabledState() ? "" : undefined',
689
+ '[attr.disabled]': 'isDisabled() ? "" : undefined',
913
690
  '(click)': 'onClick()'
914
691
  }
915
692
  }]
916
693
  }] });
917
694
 
918
695
  /**
919
- * Shown only when no items match the current query. Reuses the combobox empty part.
696
+ * A polite, atomic live region announcing the "no results" message. Like the combobox empty part, the
697
+ * element stays mounted and visible at all times (never hidden/unmounted) so the transition to empty is
698
+ * announced; only its projected content is rendered conditionally.
920
699
  *
921
700
  * @group Components
922
701
  */
923
702
  class RdxAutocompleteEmpty {
924
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteEmpty, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
925
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxAutocompleteEmpty, isStandalone: true, selector: "[rdxAutocompleteEmpty]", exportAs: ["rdxAutocompleteEmpty"], hostDirectives: [{ directive: i1.RdxComboboxEmpty }], ngImport: i0 }); }
703
+ constructor() {
704
+ this.rootContext = injectComboboxRootContext();
705
+ /** Whether no items match the current query (drives projection of the message). */
706
+ this.isEmpty = computed(() => this.rootContext.visibleCount() === 0, ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
707
+ }
708
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteEmpty, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
709
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: RdxAutocompleteEmpty, isStandalone: true, selector: "[rdxAutocompleteEmpty]", host: { attributes: { "role": "status", "aria-live": "polite", "aria-atomic": "true" }, properties: { "attr.data-empty": "isEmpty() ? \"\" : undefined" } }, exportAs: ["rdxAutocompleteEmpty"], ngImport: i0, template: `
710
+ @if (isEmpty()) {
711
+ <ng-content />
712
+ }
713
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
926
714
  }
927
715
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteEmpty, decorators: [{
928
- type: Directive,
716
+ type: Component,
929
717
  args: [{
930
718
  selector: '[rdxAutocompleteEmpty]',
931
719
  exportAs: 'rdxAutocompleteEmpty',
932
- hostDirectives: [RdxComboboxEmpty]
720
+ changeDetection: ChangeDetectionStrategy.OnPush,
721
+ template: `
722
+ @if (isEmpty()) {
723
+ <ng-content />
724
+ }
725
+ `,
726
+ host: {
727
+ role: 'status',
728
+ 'aria-live': 'polite',
729
+ 'aria-atomic': 'true',
730
+ // Present only while the message is shown. Lets consumers collapse the always-mounted region
731
+ // (e.g. `data-[empty]:py-6`) without `display:none`/`hidden`, which would break the announcement.
732
+ '[attr.data-empty]': 'isEmpty() ? "" : undefined'
733
+ }
933
734
  }]
934
735
  }] });
935
736
 
@@ -989,6 +790,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
989
790
  }]
990
791
  }] });
991
792
 
793
+ /**
794
+ * Positions the autocomplete popup relative to the input anchor using the popper engine.
795
+ *
796
+ * A "thin" positioner (ADR 0012): it inherits the full popper positioning surface — the inputs
797
+ * (`side`, `sideOffset`, `align`, …), the `placed` output, and the host bindings — from
798
+ * {@link RdxPopperContentWrapper}, and only declares autocomplete's Base UI-aligned defaults through
799
+ * the config provider (the same building block the combobox positioner uses).
800
+ *
801
+ * @group Components
802
+ */
803
+ class RdxAutocompletePositioner extends RdxPopperContentWrapper {
804
+ constructor() {
805
+ super();
806
+ const root = inject(RdxAutocompleteRoot);
807
+ const injector = inject(Injector);
808
+ const host = inject(ElementRef).nativeElement;
809
+ // A modal autocomplete isolates the background with an internal backdrop (Base UI); the input stays
810
+ // clickable through a cutout. (Autocomplete is non-modal by default — usually no backdrop.)
811
+ afterNextRender(() => setupInternalBackdrop(host, injector, {
812
+ isOpen: () => root.open(),
813
+ shouldRender: () => root.modal(),
814
+ cutout: () => root.inputElement() ?? null
815
+ }));
816
+ }
817
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompletePositioner, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
818
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxAutocompletePositioner, isStandalone: true, selector: "[rdxAutocompletePositioner]", providers: [
819
+ ...provideRdxPopperContentWrapper(RdxAutocompletePositioner),
820
+ provideRdxPopperContentConfig({ sideOffset: 4, align: 'start' })
821
+ ], exportAs: ["rdxAutocompletePositioner"], usesInheritance: true, ngImport: i0 }); }
822
+ }
823
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompletePositioner, decorators: [{
824
+ type: Directive,
825
+ args: [{
826
+ selector: '[rdxAutocompletePositioner]',
827
+ exportAs: 'rdxAutocompletePositioner',
828
+ providers: [
829
+ ...provideRdxPopperContentWrapper(RdxAutocompletePositioner),
830
+ provideRdxPopperContentConfig({ sideOffset: 4, align: 'start' })
831
+ ]
832
+ }]
833
+ }], ctorParameters: () => [] });
834
+
992
835
  const attr$1 = (value) => (value ? '' : undefined);
993
836
  /**
994
837
  * The autocomplete text input. Holds DOM focus at all times; the highlighted option is referenced via
@@ -1027,7 +870,11 @@ class RdxAutocompleteInput {
1027
870
  /** Whether an IME composition is in progress (CJK). While composing, don't filter or select. */
1028
871
  this.composing = false;
1029
872
  this.dataAttr = attr$1;
1030
- this.root.inputElement.set(this.element);
873
+ this.root.setInputElement(this.element);
874
+ // Report the layout (Base UI's `inputInsidePopup`): a positioner ancestor means the input lives
875
+ // inside the popup, so the Trigger becomes the focusable `role="combobox"`; otherwise the input
876
+ // is the tab stop and the Trigger is a `tabindex="-1"` toggle.
877
+ this.root.setInputLayout(inject(RdxAutocompletePositioner, { optional: true }) ? 'inside' : 'outside');
1031
878
  afterNextRender(() => {
1032
879
  this.fieldRootContext?.setControlId(this.id());
1033
880
  });
@@ -1044,7 +891,7 @@ class RdxAutocompleteInput {
1044
891
  });
1045
892
  inject(DestroyRef).onDestroy(() => {
1046
893
  if (this.root.inputElement() === this.element) {
1047
- this.root.inputElement.set(null);
894
+ this.root.setInputElement(null);
1048
895
  }
1049
896
  });
1050
897
  }
@@ -1052,21 +899,22 @@ class RdxAutocompleteInput {
1052
899
  if (this.composing || event.isComposing) {
1053
900
  return;
1054
901
  }
1055
- this.commitInput(event.target.value);
902
+ this.commitInput(event.target.value, event);
1056
903
  }
1057
904
  onCompositionEnd(event) {
1058
905
  this.composing = false;
1059
- this.commitInput(event.target.value);
906
+ this.commitInput(event.target.value, event);
1060
907
  }
1061
- commitInput(value) {
1062
- if (!this.root.open()) {
1063
- this.root.setOpen(true);
908
+ commitInput(value, event) {
909
+ // Base UI opens on input only for a non-empty trimmed value — whitespace alone won't open it.
910
+ if (!this.root.open() && value.trim() !== '') {
911
+ this.root.setOpen(true, 'input-change', event);
1064
912
  }
1065
913
  this.root.setQuery(value);
1066
914
  }
1067
- onClick() {
915
+ onClick(event) {
1068
916
  if (this.root.openOnInputClick()) {
1069
- this.root.openForBrowse();
917
+ this.root.openForBrowse('input-press', event);
1070
918
  }
1071
919
  }
1072
920
  onFocus() {
@@ -1091,7 +939,7 @@ class RdxAutocompleteInput {
1091
939
  event.preventDefault();
1092
940
  this.root.setKeyboardActive(true);
1093
941
  if (!open) {
1094
- this.root.openAndHighlight('first');
942
+ this.root.openAndHighlight('first', 'list-navigation', event);
1095
943
  }
1096
944
  else {
1097
945
  this.root.moveDown();
@@ -1101,7 +949,7 @@ class RdxAutocompleteInput {
1101
949
  event.preventDefault();
1102
950
  this.root.setKeyboardActive(true);
1103
951
  if (!open) {
1104
- this.root.openAndHighlight('last');
952
+ this.root.openAndHighlight('last', 'list-navigation', event);
1105
953
  }
1106
954
  else {
1107
955
  this.root.moveUp();
@@ -1121,6 +969,22 @@ class RdxAutocompleteInput {
1121
969
  this.root.moveLeft();
1122
970
  }
1123
971
  break;
972
+ case 'Home':
973
+ // In a grid the search box is a filter, so Home/End jump to the first/last cell rather
974
+ // than moving the caret (outside a grid they keep their native text-editing behavior).
975
+ if (open && this.root.grid()) {
976
+ event.preventDefault();
977
+ this.root.setKeyboardActive(true);
978
+ this.root.highlightFirst();
979
+ }
980
+ break;
981
+ case 'End':
982
+ if (open && this.root.grid()) {
983
+ event.preventDefault();
984
+ this.root.setKeyboardActive(true);
985
+ this.root.highlightLast();
986
+ }
987
+ break;
1124
988
  case 'Enter':
1125
989
  if (open) {
1126
990
  const hasHighlight = this.root.virtualized()
@@ -1128,42 +992,52 @@ class RdxAutocompleteInput {
1128
992
  : this.root.highlightedItem() !== null;
1129
993
  if (hasHighlight) {
1130
994
  event.preventDefault();
1131
- this.root.selectHighlighted();
995
+ this.root.selectHighlighted(event);
1132
996
  }
1133
997
  else if (!this.root.inlineMode()) {
1134
998
  // Non-inline: close and let native form submission proceed. Inline modes keep the
1135
999
  // popup open on Enter without a highlight (matches Base UI).
1136
- this.root.closePopup(true);
1000
+ this.root.closePopup(true, 'none', event);
1137
1001
  }
1138
1002
  }
1139
1003
  break;
1140
1004
  case 'Escape':
1141
1005
  if (open) {
1142
1006
  event.preventDefault();
1143
- this.root.closePopup(true);
1007
+ this.root.closePopup(true, 'escape-key', event);
1008
+ }
1009
+ else if (!this.root.popupMounted()) {
1010
+ // Base UI: Escape on a closed autocomplete clears the input value (a no-op while
1011
+ // read-only / disabled). Guard on `popupMounted` so the same Escape that just closed
1012
+ // an open popup (the `open` branch above) doesn't also clear.
1013
+ this.root.clearValue();
1144
1014
  }
1145
1015
  break;
1146
1016
  case 'Tab':
1147
- if (open) {
1148
- this.root.closePopup(true);
1017
+ // Tab dismisses a real popup and lets focus move on. With no popup (the always-open,
1018
+ // inline "command palette" layout), Tab must NOT close — it just moves focus within the
1019
+ // surrounding dialog. Guard on `popupMounted` so closing doesn't tear down that dialog.
1020
+ if (open && this.root.popupMounted()) {
1021
+ this.root.closePopup(true, 'none', event);
1149
1022
  }
1150
1023
  break;
1151
1024
  }
1152
1025
  }
1153
1026
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteInput, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1154
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxAutocompleteInput, isStandalone: true, selector: "input[rdxAutocompleteInput]", 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" }, listeners: { "input": "onInput($event)", "click": "onClick()", "focus": "onFocus()", "blur": "onBlur()", "keydown": "onKeydown($event)", "compositionstart": "composing = true", "compositionend": "onCompositionEnd($event)" }, properties: { "attr.aria-autocomplete": "ariaAutocomplete()", "attr.id": "id()", "attr.aria-expanded": "root.open()", "attr.aria-controls": "root.listId", "attr.aria-labelledby": "root.labelId()", "attr.aria-activedescendant": "root.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": "root.readOnly() ? \"\" : undefined", "attr.required": "requiredState() ? \"\" : undefined", "value": "root.displayValue()", "attr.data-popup-open": "dataAttr(root.open())", "attr.data-list-empty": "dataAttr(root.visibleCount() === 0)", "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: ["rdxAutocompleteInput"], hostDirectives: [{ directive: i1$1.RdxPopperAnchor }, { directive: i2.RdxDismissableLayerBranch }], ngImport: i0 }); }
1027
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxAutocompleteInput, isStandalone: true, selector: "input[rdxAutocompleteInput]", 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" }, listeners: { "input": "onInput($event)", "click": "onClick($event)", "focus": "onFocus()", "blur": "onBlur()", "keydown": "onKeydown($event)", "compositionstart": "composing = true", "compositionend": "onCompositionEnd($event)" }, properties: { "attr.aria-autocomplete": "ariaAutocomplete()", "attr.id": "id()", "attr.aria-haspopup": "root.grid() ? \"grid\" : \"listbox\"", "attr.aria-expanded": "root.open()", "attr.aria-controls": "root.listId", "attr.aria-labelledby": "root.labelId()", "attr.aria-activedescendant": "root.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": "root.readOnly() ? \"\" : undefined", "attr.required": "requiredState() ? \"\" : undefined", "value": "root.displayValue()", "attr.data-popup-open": "dataAttr(root.open())", "attr.data-list-empty": "dataAttr(root.visibleCount() === 0)", "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: ["rdxAutocompleteInput"], hostDirectives: [{ directive: i1$1.RdxPopperAnchor }, { directive: i1$2.RdxFloatingInsideElement }], ngImport: i0 }); }
1155
1028
  }
1156
1029
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteInput, decorators: [{
1157
1030
  type: Directive,
1158
1031
  args: [{
1159
1032
  selector: 'input[rdxAutocompleteInput]',
1160
1033
  exportAs: 'rdxAutocompleteInput',
1161
- hostDirectives: [RdxPopperAnchor, RdxDismissableLayerBranch],
1034
+ hostDirectives: [RdxPopperAnchor, RdxFloatingInsideElement],
1162
1035
  host: {
1163
1036
  role: 'combobox',
1164
1037
  autocomplete: 'off',
1165
1038
  '[attr.aria-autocomplete]': 'ariaAutocomplete()',
1166
1039
  '[attr.id]': 'id()',
1040
+ '[attr.aria-haspopup]': 'root.grid() ? "grid" : "listbox"',
1167
1041
  '[attr.aria-expanded]': 'root.open()',
1168
1042
  '[attr.aria-controls]': 'root.listId',
1169
1043
  '[attr.aria-labelledby]': 'root.labelId()',
@@ -1185,7 +1059,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1185
1059
  '[attr.data-filled]': 'dataAttr(filledState())',
1186
1060
  '[attr.data-focused]': 'dataAttr(focusedState())',
1187
1061
  '(input)': 'onInput($event)',
1188
- '(click)': 'onClick()',
1062
+ '(click)': 'onClick($event)',
1189
1063
  '(focus)': 'onFocus()',
1190
1064
  '(blur)': 'onBlur()',
1191
1065
  '(keydown)': 'onKeydown($event)',
@@ -1224,6 +1098,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1224
1098
  }]
1225
1099
  }] });
1226
1100
 
1101
+ /**
1102
+ * A row in a grid-layout autocomplete list. Groups the items in one row so the root can navigate by
1103
+ * row (ArrowUp / ArrowDown) and within a row (ArrowLeft / ArrowRight). Only meaningful when the root
1104
+ * has `grid` enabled; the root resolves an item's row from its nearest `[rdxAutocompleteRow]` ancestor.
1105
+ *
1106
+ * @group Components
1107
+ */
1108
+ class RdxAutocompleteRow {
1109
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteRow, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1110
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxAutocompleteRow, isStandalone: true, selector: "[rdxAutocompleteRow]", host: { attributes: { "role": "row" } }, exportAs: ["rdxAutocompleteRow"], ngImport: i0 }); }
1111
+ }
1112
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteRow, decorators: [{
1113
+ type: Directive,
1114
+ args: [{
1115
+ selector: '[rdxAutocompleteRow]',
1116
+ exportAs: 'rdxAutocompleteRow',
1117
+ host: {
1118
+ role: 'row'
1119
+ }
1120
+ }]
1121
+ }] });
1122
+
1227
1123
  const itemContext = () => {
1228
1124
  const item = inject(RdxAutocompleteItem);
1229
1125
  return {
@@ -1246,8 +1142,12 @@ class RdxAutocompleteItem {
1246
1142
  this.rootContext = injectComboboxRootContext();
1247
1143
  this.element = inject(ElementRef).nativeElement;
1248
1144
  this.id = injectId('rdx-autocomplete-item-');
1249
- /** The option's value. When omitted, selecting the item writes its text content into the input. */
1250
- this.value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1145
+ /**
1146
+ * The explicit `[value]`, or `undefined` when omitted. Read the resolved {@link value} instead
1147
+ * it falls back to the text content only when no value was bound (so explicit falsy values like
1148
+ * `0` / `''` / `null` are preserved for the filter and selection).
1149
+ */
1150
+ this.valueInput = input(undefined, { ...(ngDevMode ? { debugName: "valueInput" } : /* istanbul ignore next */ {}), alias: 'value' });
1251
1151
  /** Explicit text matched against the query and written to the input. Defaults to text content. */
1252
1152
  this.textValueInput = input('', { ...(ngDevMode ? { debugName: "textValueInput" } : /* istanbul ignore next */ {}), alias: 'textValue' });
1253
1153
  /** Whether the option is disabled. */
@@ -1257,15 +1157,31 @@ class RdxAutocompleteItem {
1257
1157
  this.virtualized = this.rootContext.virtualized;
1258
1158
  this.autoTextValue = signal('', ...(ngDevMode ? [{ debugName: "autoTextValue" }] : /* istanbul ignore next */ []));
1259
1159
  this.textValue = computed(() => this.textValueInput() || this.autoTextValue(), ...(ngDevMode ? [{ debugName: "textValue" }] : /* istanbul ignore next */ []));
1160
+ /**
1161
+ * The option's effective value: the explicit `[value]` if bound (preserving `0` / `''` / `null`),
1162
+ * otherwise the text content (autocomplete's value is the input string). Only an absent input —
1163
+ * `undefined` — falls back, so a custom filter still sees the real `itemValue` for falsy values.
1164
+ */
1165
+ this.value = computed(() => {
1166
+ const bound = this.valueInput();
1167
+ return bound === undefined ? this.textValue() : bound;
1168
+ }, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1260
1169
  this.elementId = computed(() => this.virtualized() ? this.rootContext.itemId(this.index() ?? -1) : this.id, ...(ngDevMode ? [{ debugName: "elementId" }] : /* istanbul ignore next */ []));
1261
1170
  this.ariaSetSize = computed(() => this.virtualized() ? this.rootContext.filteredItems().length : undefined, ...(ngDevMode ? [{ debugName: "ariaSetSize" }] : /* istanbul ignore next */ []));
1262
1171
  this.ariaPosInSet = computed(() => (this.virtualized() ? (this.index() ?? -1) + 1 : undefined), ...(ngDevMode ? [{ debugName: "ariaPosInSet" }] : /* istanbul ignore next */ []));
1172
+ /** The nearest enclosing grid row, if any (drives the `gridcell` role). */
1173
+ this.row = inject(RdxAutocompleteRow, { optional: true });
1174
+ /** `gridcell` only when actually inside a `RdxAutocompleteRow` of a grid list; otherwise `option`. */
1175
+ this.role = computed(() => (this.rootContext.grid() && this.row ? 'gridcell' : 'option'), ...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));
1263
1176
  this.isVisible = computed(() => (this.virtualized() ? true : this.rootContext.isVisible(this)), ...(ngDevMode ? [{ debugName: "isVisible" }] : /* istanbul ignore next */ []));
1264
1177
  this.isSelected = computed(() => this.rootContext.isSelected(this.value()), ...(ngDevMode ? [{ debugName: "isSelected" }] : /* istanbul ignore next */ []));
1265
1178
  this.isHighlighted = computed(() => this.virtualized()
1266
1179
  ? this.rootContext.highlightedIndex() === this.index()
1267
1180
  : this.rootContext.highlightedItem() === this, ...(ngDevMode ? [{ debugName: "isHighlighted" }] : /* istanbul ignore next */ []));
1268
1181
  this.group = injectComboboxGroupContext(true);
1182
+ // Whether a primary-button pointerdown started on **this** item. A normal press+release here is
1183
+ // committed by `click`; `mouseup` is only the drag-end fallback for a press that began *elsewhere*.
1184
+ this.pointerDownStarted = false;
1269
1185
  const destroyRef = inject(DestroyRef);
1270
1186
  afterNextRender(() => {
1271
1187
  if (this.virtualized()) {
@@ -1293,12 +1209,45 @@ class RdxAutocompleteItem {
1293
1209
  this.element.scrollIntoView({ block: 'nearest' });
1294
1210
  }
1295
1211
  });
1212
+ // Reset the press flag whenever the popup closes (matches Base UI), so a later drag-end onto
1213
+ // this item isn't blocked by a stale press from an earlier interaction.
1214
+ effect(() => {
1215
+ if (!this.rootContext.open()) {
1216
+ this.pointerDownStarted = false;
1217
+ }
1218
+ });
1296
1219
  }
1297
1220
  onPointerDown(event) {
1221
+ if (event.button !== 0) {
1222
+ return;
1223
+ }
1298
1224
  event.preventDefault();
1299
1225
  this.rootContext.setKeyboardActive(false);
1226
+ this.pointerDownStarted = true;
1227
+ }
1228
+ onMouseDown(event) {
1229
+ // Belt-and-suspenders for keeping focus on the input (and iOS Safari blur on tap).
1230
+ if (event.button === 0) {
1231
+ event.preventDefault();
1232
+ }
1233
+ }
1234
+ onMouseUp(event) {
1235
+ // Read-and-reset the press flag first (matches Base UI), so a press+release here doesn't leave
1236
+ // it set and block a later drag-end onto this same item. Drag-end: commit when the primary
1237
+ // button is released over the highlighted item while the press began on a *different* element
1238
+ // (so `click` won't fire here). A press that began on this item is committed by `click` instead.
1239
+ const startedHere = this.pointerDownStarted;
1240
+ this.pointerDownStarted = false;
1241
+ if (event.button !== 0 || startedHere || !this.isHighlighted()) {
1242
+ return;
1243
+ }
1244
+ this.commitSelection();
1245
+ }
1246
+ onClick() {
1247
+ // Primary selection trigger; also fires for programmatic `.click()`.
1248
+ this.commitSelection();
1300
1249
  }
1301
- onPointerUp() {
1250
+ commitSelection() {
1302
1251
  if (this.virtualized()) {
1303
1252
  this.rootContext.selectIndex(this.index() ?? -1);
1304
1253
  }
@@ -1342,7 +1291,7 @@ class RdxAutocompleteItem {
1342
1291
  this.rootContext.clearHighlight();
1343
1292
  }
1344
1293
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1345
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxAutocompleteItem, isStandalone: true, selector: "[rdxAutocompleteItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, 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()", "pointerleave": "onPointerLeave($event)" }, 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: ["rdxAutocompleteItem"], ngImport: i0 }); }
1294
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxAutocompleteItem, isStandalone: true, selector: "[rdxAutocompleteItem]", inputs: { valueInput: { classPropertyName: "valueInput", publicName: "value", isSignal: true, isRequired: false, 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()", "pointermove": "onPointerMove()", "pointerleave": "onPointerLeave($event)" }, properties: { "attr.role": "role()", "attr.id": "elementId()", "attr.aria-disabled": "disabled() ? \"true\" : undefined", "attr.aria-setsize": "ariaSetSize()", "attr.aria-posinset": "ariaPosInSet()", "attr.data-highlighted": "isHighlighted() ? \"\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined", "hidden": "!isVisible()", "attr.data-hidden": "isVisible() ? undefined : \"\"" } }, providers: [provideComboboxItemContext(itemContext)], exportAs: ["rdxAutocompleteItem"], ngImport: i0 }); }
1346
1295
  }
1347
1296
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteItem, decorators: [{
1348
1297
  type: Directive,
@@ -1351,25 +1300,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1351
1300
  exportAs: 'rdxAutocompleteItem',
1352
1301
  providers: [provideComboboxItemContext(itemContext)],
1353
1302
  host: {
1354
- role: 'option',
1303
+ '[attr.role]': 'role()',
1355
1304
  '[attr.id]': 'elementId()',
1356
- '[attr.aria-selected]': 'isSelected()',
1305
+ // Autocomplete is always `selectionMode="none"`, so options carry no selection state: Base UI
1306
+ // omits `aria-selected` / `data-selected` here entirely (rather than rendering `false`).
1357
1307
  '[attr.aria-disabled]': 'disabled() ? "true" : undefined',
1358
1308
  '[attr.aria-setsize]': 'ariaSetSize()',
1359
1309
  '[attr.aria-posinset]': 'ariaPosInSet()',
1360
- '[attr.data-selected]': 'isSelected() ? "" : undefined',
1361
1310
  '[attr.data-highlighted]': 'isHighlighted() ? "" : undefined',
1362
1311
  '[attr.data-disabled]': 'disabled() ? "" : undefined',
1363
1312
  '[hidden]': '!isVisible()',
1364
1313
  '[attr.data-hidden]': 'isVisible() ? undefined : ""',
1365
1314
  '(pointerdown)': 'onPointerDown($event)',
1366
- '(mousedown)': 'onPointerDown($event)',
1367
- '(pointerup)': 'onPointerUp()',
1315
+ '(mousedown)': 'onMouseDown($event)',
1316
+ '(mouseup)': 'onMouseUp($event)',
1317
+ '(click)': 'onClick()',
1368
1318
  '(pointermove)': 'onPointerMove()',
1369
1319
  '(pointerleave)': 'onPointerLeave($event)'
1370
1320
  }
1371
1321
  }]
1372
- }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], 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 }] }] } });
1322
+ }], ctorParameters: () => [], propDecorators: { valueInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], 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 }] }] } });
1373
1323
 
1374
1324
  /**
1375
1325
  * Renders only when its item is selected (e.g. a checkmark). Reuses the combobox item indicator,
@@ -1419,8 +1369,27 @@ class RdxAutocompleteList {
1419
1369
  constructor() {
1420
1370
  this.root = inject(RdxAutocompleteRoot);
1421
1371
  }
1372
+ onKeydown(event) {
1373
+ if (event.key !== 'Enter') {
1374
+ return;
1375
+ }
1376
+ // Base UI bails early when disabled / read-only — don't swallow Enter (e.g. a form submit).
1377
+ if (this.root.disabledState() || this.root.readOnly()) {
1378
+ return;
1379
+ }
1380
+ const hasHighlight = this.root.virtualized()
1381
+ ? this.root.highlightedIndex() >= 0
1382
+ : this.root.highlightedItem() !== null;
1383
+ if (hasHighlight) {
1384
+ // Base UI `stopEvent`: also stop propagation so a parent keydown handler doesn't re-handle
1385
+ // Enter after the selection.
1386
+ event.preventDefault();
1387
+ event.stopPropagation();
1388
+ this.root.selectHighlighted();
1389
+ }
1390
+ }
1422
1391
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteList, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1423
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxAutocompleteList, isStandalone: true, selector: "[rdxAutocompleteList]", host: { properties: { "attr.role": "root.grid() ? \"grid\" : \"listbox\"", "attr.id": "root.listId" } }, exportAs: ["rdxAutocompleteList"], ngImport: i0 }); }
1392
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxAutocompleteList, isStandalone: true, selector: "[rdxAutocompleteList]", host: { attributes: { "tabindex": "-1" }, listeners: { "keydown": "onKeydown($event)" }, properties: { "attr.role": "root.grid() ? \"grid\" : \"listbox\"", "attr.id": "root.listId" } }, exportAs: ["rdxAutocompleteList"], ngImport: i0 }); }
1424
1393
  }
1425
1394
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteList, decorators: [{
1426
1395
  type: Directive,
@@ -1428,8 +1397,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1428
1397
  selector: '[rdxAutocompleteList]',
1429
1398
  exportAs: 'rdxAutocompleteList',
1430
1399
  host: {
1400
+ // Base UI: the list is a programmatic focus target (`tabindex="-1"`) and selects the highlighted
1401
+ // item on Enter, for custom layouts that move focus onto the list rather than the input.
1402
+ tabindex: '-1',
1431
1403
  '[attr.role]': 'root.grid() ? "grid" : "listbox"',
1432
- '[attr.id]': 'root.listId'
1404
+ '[attr.id]': 'root.listId',
1405
+ '(keydown)': 'onKeydown($event)'
1433
1406
  }
1434
1407
  }]
1435
1408
  }] });
@@ -1445,13 +1418,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1445
1418
  class RdxAutocompletePopup {
1446
1419
  constructor() {
1447
1420
  this.root = inject(RdxAutocompleteRoot);
1448
- this.dismissableLayer = inject(RdxDismissableLayer);
1421
+ this.floatingContext = inject(RDX_FLOATING_ROOT_CONTEXT);
1422
+ this.registration = inject(RDX_FLOATING_REGISTRATION, { optional: true });
1449
1423
  this.popper = injectPopperContentWrapperContext();
1450
1424
  this.element = inject(ElementRef).nativeElement;
1451
- useScrollLock(this.root.modal);
1425
+ // Activation policy (ADR 0016 §2 + §3): lock page scroll while a modal popup is OPEN, gated on
1426
+ // `open` (not mounted) so the lock releases at close-start. For a **touch** open the anchored
1427
+ // helper only locks when the popup is effectively viewport-width (a small dropdown stays
1428
+ // swipe-to-dismissable on mobile, §3).
1429
+ useAnchoredScrollLock(computed(() => this.root.open() && this.root.modal()), {
1430
+ touchOpen: () => this.root.openedByTouch(),
1431
+ element: () => this.element
1432
+ });
1452
1433
  const unregister = this.root.registerTransitionElement(this.element);
1453
- inject(DestroyRef).onDestroy(unregister);
1454
- this.dismissableLayer.dismiss.subscribe(() => this.root.closePopup(true));
1434
+ // Track mounted state so Escape can tell "closing this open popup" from "already closed".
1435
+ this.root.setPopupMounted(true);
1436
+ inject(DestroyRef).onDestroy(() => {
1437
+ unregister();
1438
+ this.root.setPopupMounted(false);
1439
+ });
1440
+ // The popup is this layer's floating element (the inside surface for containment checks).
1441
+ this.floatingContext.setFloatingElement(this.element);
1442
+ // Dismissal (ADR 0015): an outside press, or focus leaving everything, closes the autocomplete. The
1443
+ // input / trigger / clear are registered as "inside" (RdxFloatingInsideElement), so the input keeping
1444
+ // focus — or a press on those parts — never self-dismisses. Escape is owned by the input (it
1445
+ // preventDefaults + closes), so the capability does not handle it (`escapeKey: false`).
1446
+ new RdxDismiss(this.floatingContext, () => this.registration?.node() ?? null, {
1447
+ escapeKey: () => false,
1448
+ outsidePress: () => true,
1449
+ focusOutside: () => true,
1450
+ onDismiss: (reason, event) => this.root.closePopup(true, reason === 'focus-outside' ? 'focus-out' : 'outside-press', event)
1451
+ });
1455
1452
  // For the "input inside the popup" pattern, move focus to the input once positioned. Use
1456
1453
  // `afterRenderEffect` (not `effect`): when `isPositioned` flips true the popup's final
1457
1454
  // position/visibility is applied in the *following* render, so a synchronous `effect` would
@@ -1463,35 +1460,57 @@ class RdxAutocompletePopup {
1463
1460
  }
1464
1461
  const input = this.root.inputElement();
1465
1462
  if (input && input.closest('[rdxAutocompletePopup]')) {
1466
- input.focus();
1467
- input.select();
1463
+ // Base UI: a touch-open focuses the popup itself so Android keeps the virtual keyboard
1464
+ // closed; mouse/keyboard opens focus (and select) the search input as usual.
1465
+ if (this.root.openedByTouch()) {
1466
+ this.element.focus();
1467
+ }
1468
+ else {
1469
+ input.focus();
1470
+ input.select();
1471
+ }
1468
1472
  }
1469
1473
  });
1470
1474
  }
1475
+ /**
1476
+ * Base UI focus handoff: if focus lands on the popup or the list (the `tabindex="-1"` programmatic
1477
+ * focus targets), hand it back to the input so arrow-key navigation (`aria-activedescendant`) keeps
1478
+ * working. Skipped for a touch interaction, where focus is parked on the popup to keep the Android
1479
+ * virtual keyboard closed.
1480
+ */
1481
+ onFocusIn(event) {
1482
+ if (this.root.openedByTouch()) {
1483
+ return;
1484
+ }
1485
+ const input = this.root.inputElement();
1486
+ const target = event.target;
1487
+ if (!input || !target || target === input) {
1488
+ return;
1489
+ }
1490
+ if (target === this.element || target.matches('[rdxAutocompleteList]')) {
1491
+ input.focus();
1492
+ }
1493
+ }
1471
1494
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompletePopup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1472
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxAutocompletePopup, isStandalone: true, selector: "[rdxAutocompletePopup]", host: { properties: { "attr.data-state": "root.open() ? \"open\" : \"closed\"", "attr.data-open": "root.open() ? \"\" : undefined", "attr.data-closed": "root.open() ? undefined : \"\"", "attr.data-starting-style": "root.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-ending-style": "root.transitionStatus() === \"ending\" ? \"\" : undefined" } }, providers: [
1473
- provideRdxDismissableLayerConfig(() => ({
1474
- disableOutsidePointerEvents: inject(RdxAutocompleteRoot).modal
1475
- }))
1476
- ], exportAs: ["rdxAutocompletePopup"], hostDirectives: [{ directive: i1$1.RdxPopperContent }, { directive: i2.RdxDismissableLayer }], ngImport: i0 }); }
1495
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxAutocompletePopup, isStandalone: true, selector: "[rdxAutocompletePopup]", host: { attributes: { "tabindex": "-1" }, listeners: { "focusin": "onFocusIn($event)" }, properties: { "attr.role": "root.inputLayout() === \"inside\" ? \"dialog\" : \"presentation\"", "attr.data-state": "root.open() ? \"open\" : \"closed\"", "attr.data-open": "root.open() ? \"\" : undefined", "attr.data-closed": "root.open() ? undefined : \"\"", "attr.data-starting-style": "root.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-ending-style": "root.transitionStatus() === \"ending\" ? \"\" : undefined" } }, exportAs: ["rdxAutocompletePopup"], hostDirectives: [{ directive: i1$1.RdxPopperContent }, { directive: i2.RdxFloatingNodeRegistration }], ngImport: i0 }); }
1477
1496
  }
1478
1497
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompletePopup, decorators: [{
1479
1498
  type: Directive,
1480
1499
  args: [{
1481
1500
  selector: '[rdxAutocompletePopup]',
1482
1501
  exportAs: 'rdxAutocompletePopup',
1483
- hostDirectives: [RdxPopperContent, RdxDismissableLayer],
1484
- providers: [
1485
- provideRdxDismissableLayerConfig(() => ({
1486
- disableOutsidePointerEvents: inject(RdxAutocompleteRoot).modal
1487
- }))
1488
- ],
1502
+ hostDirectives: [RdxPopperContent, RdxFloatingNodeRegistration],
1489
1503
  host: {
1504
+ // Base UI: a `dialog` (focusable, tabindex -1) when the input lives inside the popup, otherwise
1505
+ // a presentational wrapper around the `listbox` (the List part owns the listbox role).
1506
+ tabindex: '-1',
1507
+ '[attr.role]': 'root.inputLayout() === "inside" ? "dialog" : "presentation"',
1490
1508
  '[attr.data-state]': 'root.open() ? "open" : "closed"',
1491
1509
  '[attr.data-open]': 'root.open() ? "" : undefined',
1492
1510
  '[attr.data-closed]': 'root.open() ? undefined : ""',
1493
1511
  '[attr.data-starting-style]': 'root.transitionStatus() === "starting" ? "" : undefined',
1494
- '[attr.data-ending-style]': 'root.transitionStatus() === "ending" ? "" : undefined'
1512
+ '[attr.data-ending-style]': 'root.transitionStatus() === "ending" ? "" : undefined',
1513
+ '(focusin)': 'onFocusIn($event)'
1495
1514
  }
1496
1515
  }]
1497
1516
  }], ctorParameters: () => [] });
@@ -1529,9 +1548,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1529
1548
  class RdxAutocompletePortalMisuseGuard {
1530
1549
  constructor() {
1531
1550
  if (isDevMode()) {
1532
- throw new Error('[rdxAutocompletePortal] is now a structural directive. ' +
1551
+ rdxDevError('autocomplete/portal-on-element', '`rdxAutocompletePortal` is now a structural directive. ' +
1533
1552
  'Use `*rdxAutocompletePortal` on the positioner element or `<ng-template rdxAutocompletePortal>`. ' +
1534
- 'rdxAutocompletePortalPresence has been removed. See https://radix-ng.com/components/autocomplete.md');
1553
+ 'rdxAutocompletePortalPresence has been removed.', 'components/autocomplete');
1535
1554
  }
1536
1555
  }
1537
1556
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompletePortalMisuseGuard, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
@@ -1545,84 +1564,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1545
1564
  }], ctorParameters: () => [] });
1546
1565
 
1547
1566
  /**
1548
- * Positions the popup relative to the input anchor using the popper engine. Composes the popper
1549
- * content wrapper directly (the same building block the combobox positioner uses) and re-exposes its
1550
- * positioning inputs.
1551
- *
1552
- * @group Components
1553
- */
1554
- class RdxAutocompletePositioner {
1555
- constructor() {
1556
- this.side = input('bottom', ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
1557
- this.sideOffset = input(4, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
1558
- this.align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
1559
- this.alignOffset = input(0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
1560
- this.arrowPadding = input(0, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
1561
- this.avoidCollisions = input(true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1562
- this.collisionBoundary = input(...(ngDevMode ? [undefined, { debugName: "collisionBoundary" }] : /* istanbul ignore next */ []));
1563
- this.collisionPadding = input(0, ...(ngDevMode ? [{ debugName: "collisionPadding" }] : /* istanbul ignore next */ []));
1564
- this.sticky = input('partial', ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
1565
- this.hideWhenDetached = input(false, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1566
- this.updatePositionStrategy = input('optimized', ...(ngDevMode ? [{ debugName: "updatePositionStrategy" }] : /* istanbul ignore next */ []));
1567
- }
1568
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompletePositioner, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1569
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxAutocompletePositioner, isStandalone: true, selector: "[rdxAutocompletePositioner]", 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-autocomplete-content-transform-origin': 'var(--radix-popper-transform-origin)',\n '--radix-autocomplete-content-available-width': 'var(--radix-popper-available-width)',\n '--radix-autocomplete-content-available-height': 'var(--radix-popper-available-height)',\n '--radix-autocomplete-trigger-width': 'var(--radix-popper-anchor-width)',\n '--radix-autocomplete-trigger-height': 'var(--radix-popper-anchor-height)'\n }" } }, exportAs: ["rdxAutocompletePositioner"], hostDirectives: [{ directive: i1$1.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 }); }
1570
- }
1571
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompletePositioner, decorators: [{
1572
- type: Directive,
1573
- args: [{
1574
- selector: '[rdxAutocompletePositioner]',
1575
- exportAs: 'rdxAutocompletePositioner',
1576
- hostDirectives: [
1577
- {
1578
- directive: RdxPopperContentWrapper,
1579
- inputs: [
1580
- 'side',
1581
- 'sideOffset',
1582
- 'align',
1583
- 'alignOffset',
1584
- 'arrowPadding',
1585
- 'avoidCollisions',
1586
- 'collisionBoundary',
1587
- 'collisionPadding',
1588
- 'sticky',
1589
- 'hideWhenDetached',
1590
- 'updatePositionStrategy'
1591
- ]
1592
- }
1593
- ],
1594
- host: {
1595
- '[style]': `{
1596
- 'boxSizing': 'border-box',
1597
- '--radix-autocomplete-content-transform-origin': 'var(--radix-popper-transform-origin)',
1598
- '--radix-autocomplete-content-available-width': 'var(--radix-popper-available-width)',
1599
- '--radix-autocomplete-content-available-height': 'var(--radix-popper-available-height)',
1600
- '--radix-autocomplete-trigger-width': 'var(--radix-popper-anchor-width)',
1601
- '--radix-autocomplete-trigger-height': 'var(--radix-popper-anchor-height)'
1602
- }`
1603
- }
1604
- }]
1605
- }], 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 }] }] } });
1606
-
1607
- /**
1608
- * A row in a grid-layout autocomplete list. Groups the items in one row so the root can navigate by
1609
- * row (ArrowUp / ArrowDown) and within a row (ArrowLeft / ArrowRight). Only meaningful when the root
1610
- * has `grid` enabled; the root resolves an item's row from its nearest `[rdxAutocompleteRow]` ancestor.
1567
+ * A visual separator between groups of suggestions (`role="separator"`). Reuses the combobox separator.
1611
1568
  *
1612
1569
  * @group Components
1613
1570
  */
1614
- class RdxAutocompleteRow {
1615
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteRow, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1616
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxAutocompleteRow, isStandalone: true, selector: "[rdxAutocompleteRow]", host: { attributes: { "role": "row" } }, exportAs: ["rdxAutocompleteRow"], ngImport: i0 }); }
1571
+ class RdxAutocompleteSeparator {
1572
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteSeparator, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1573
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxAutocompleteSeparator, isStandalone: true, selector: "[rdxAutocompleteSeparator]", exportAs: ["rdxAutocompleteSeparator"], hostDirectives: [{ directive: i1.RdxComboboxSeparator }], ngImport: i0 }); }
1617
1574
  }
1618
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteRow, decorators: [{
1575
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxAutocompleteSeparator, decorators: [{
1619
1576
  type: Directive,
1620
1577
  args: [{
1621
- selector: '[rdxAutocompleteRow]',
1622
- exportAs: 'rdxAutocompleteRow',
1623
- host: {
1624
- role: 'row'
1625
- }
1578
+ selector: '[rdxAutocompleteSeparator]',
1579
+ exportAs: 'rdxAutocompleteSeparator',
1580
+ hostDirectives: [RdxComboboxSeparator]
1626
1581
  }]
1627
1582
  }] });
1628
1583
 
@@ -1646,8 +1601,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1646
1601
  }] });
1647
1602
 
1648
1603
  /**
1649
- * Toggles the autocomplete popup. Carries `tabindex="-1"` so it never steals focus from the input.
1650
- * Reuses the combobox trigger behavior.
1604
+ * Toggles the autocomplete popup. Reuses the combobox trigger: a `tabindex="-1"` toggle when the input
1605
+ * sits outside the popup, or the focusable `role="combobox"` control when the input is inside it.
1651
1606
  *
1652
1607
  * @group Components
1653
1608
  */
@@ -1714,6 +1669,7 @@ const _importsAutocomplete = [
1714
1669
  RdxAutocompleteArrow,
1715
1670
  RdxAutocompleteList,
1716
1671
  RdxAutocompleteRow,
1672
+ RdxAutocompleteSeparator,
1717
1673
  RdxAutocompleteItem,
1718
1674
  RdxAutocompleteItemIndicator,
1719
1675
  RdxAutocompleteGroup,
@@ -1740,6 +1696,7 @@ class RdxAutocompleteModule {
1740
1696
  RdxAutocompleteArrow,
1741
1697
  RdxAutocompleteList,
1742
1698
  RdxAutocompleteRow,
1699
+ RdxAutocompleteSeparator,
1743
1700
  RdxAutocompleteItem,
1744
1701
  RdxAutocompleteItemIndicator,
1745
1702
  RdxAutocompleteGroup,
@@ -1762,6 +1719,7 @@ class RdxAutocompleteModule {
1762
1719
  RdxAutocompleteArrow,
1763
1720
  RdxAutocompleteList,
1764
1721
  RdxAutocompleteRow,
1722
+ RdxAutocompleteSeparator,
1765
1723
  RdxAutocompleteItem,
1766
1724
  RdxAutocompleteItemIndicator,
1767
1725
  RdxAutocompleteGroup,
@@ -1782,5 +1740,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1782
1740
  * Generated bundle index. Do not edit.
1783
1741
  */
1784
1742
 
1785
- export { RdxAutocompleteAnchor, RdxAutocompleteArrow, RdxAutocompleteBackdrop, RdxAutocompleteClear, RdxAutocompleteEmpty, RdxAutocompleteGroup, RdxAutocompleteGroupLabel, RdxAutocompleteIcon, RdxAutocompleteInput, RdxAutocompleteInputGroup, RdxAutocompleteItem, RdxAutocompleteItemIndicator, RdxAutocompleteLabel, RdxAutocompleteList, RdxAutocompleteModule, RdxAutocompletePopup, RdxAutocompletePortal, RdxAutocompletePortalMisuseGuard, RdxAutocompletePositioner, RdxAutocompleteRoot, RdxAutocompleteRow, RdxAutocompleteStatus, RdxAutocompleteTrigger, RdxAutocompleteValue, _importsAutocomplete };
1743
+ export { RdxAutocompleteAnchor, RdxAutocompleteArrow, RdxAutocompleteBackdrop, RdxAutocompleteClear, RdxAutocompleteEmpty, RdxAutocompleteGroup, RdxAutocompleteGroupLabel, RdxAutocompleteIcon, RdxAutocompleteInput, RdxAutocompleteInputGroup, RdxAutocompleteItem, RdxAutocompleteItemIndicator, RdxAutocompleteLabel, RdxAutocompleteList, RdxAutocompleteModule, RdxAutocompletePopup, RdxAutocompletePortal, RdxAutocompletePortalMisuseGuard, RdxAutocompletePositioner, RdxAutocompleteRoot, RdxAutocompleteRow, RdxAutocompleteSeparator, RdxAutocompleteStatus, RdxAutocompleteTrigger, RdxAutocompleteValue, _importsAutocomplete };
1786
1744
  //# sourceMappingURL=radix-ng-primitives-autocomplete.mjs.map