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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +76 -6
  3. package/fesm2022/radix-ng-primitives-accordion.mjs +5 -3
  4. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  5. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +31 -24
  6. package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
  7. package/fesm2022/radix-ng-primitives-autocomplete.mjs +1744 -0
  8. package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -0
  9. package/fesm2022/radix-ng-primitives-calendar.mjs +5 -3
  10. package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
  11. package/fesm2022/radix-ng-primitives-combobox.mjs +1399 -606
  12. package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
  13. package/fesm2022/radix-ng-primitives-config.mjs +13 -4
  14. package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
  15. package/fesm2022/radix-ng-primitives-context-menu.mjs +51 -10
  16. package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
  17. package/fesm2022/radix-ng-primitives-core.mjs +1345 -64
  18. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  19. package/fesm2022/radix-ng-primitives-date-field.mjs +5 -3
  20. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  21. package/fesm2022/radix-ng-primitives-dialog.mjs +271 -145
  22. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  23. package/fesm2022/radix-ng-primitives-direction-provider.mjs +70 -0
  24. package/fesm2022/radix-ng-primitives-direction-provider.mjs.map +1 -0
  25. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +519 -184
  26. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
  27. package/fesm2022/radix-ng-primitives-drawer.mjs +154 -64
  28. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
  29. package/fesm2022/radix-ng-primitives-field.mjs +3 -2
  30. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
  31. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +517 -0
  32. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -0
  33. package/fesm2022/radix-ng-primitives-focus-scope.mjs +296 -70
  34. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  35. package/fesm2022/radix-ng-primitives-menu.mjs +894 -299
  36. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  37. package/fesm2022/radix-ng-primitives-menubar.mjs +32 -4
  38. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  39. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +176 -207
  40. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  41. package/fesm2022/radix-ng-primitives-popover.mjs +250 -250
  42. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  43. package/fesm2022/radix-ng-primitives-popper.mjs +94 -45
  44. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  45. package/fesm2022/radix-ng-primitives-portal.mjs +107 -17
  46. package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
  47. package/fesm2022/radix-ng-primitives-presence.mjs +262 -79
  48. package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
  49. package/fesm2022/radix-ng-primitives-preview-card.mjs +172 -218
  50. package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -1
  51. package/fesm2022/radix-ng-primitives-roving-focus.mjs +4 -2
  52. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  53. package/fesm2022/radix-ng-primitives-scroll-area.mjs +5 -4
  54. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  55. package/fesm2022/radix-ng-primitives-select.mjs +303 -234
  56. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  57. package/fesm2022/radix-ng-primitives-slider.mjs +5 -3
  58. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  59. package/fesm2022/radix-ng-primitives-stepper.mjs +5 -3
  60. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  61. package/fesm2022/radix-ng-primitives-time-field.mjs +5 -3
  62. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  63. package/fesm2022/radix-ng-primitives-toast.mjs +15 -36
  64. package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -1
  65. package/fesm2022/radix-ng-primitives-toggle-group.mjs +5 -3
  66. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  67. package/fesm2022/radix-ng-primitives-toolbar.mjs +5 -3
  68. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  69. package/fesm2022/radix-ng-primitives-tooltip.mjs +105 -145
  70. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  71. package/package.json +14 -1
  72. package/types/radix-ng-primitives-accordion.d.ts +4 -3
  73. package/types/radix-ng-primitives-alert-dialog.d.ts +17 -11
  74. package/types/radix-ng-primitives-autocomplete.d.ts +661 -0
  75. package/types/radix-ng-primitives-calendar.d.ts +5 -3
  76. package/types/radix-ng-primitives-combobox.d.ts +727 -293
  77. package/types/radix-ng-primitives-config.d.ts +1 -1
  78. package/types/radix-ng-primitives-context-menu.d.ts +15 -5
  79. package/types/radix-ng-primitives-core.d.ts +762 -14
  80. package/types/radix-ng-primitives-date-field.d.ts +3 -2
  81. package/types/radix-ng-primitives-dialog.d.ts +107 -55
  82. package/types/radix-ng-primitives-direction-provider.d.ts +41 -0
  83. package/types/radix-ng-primitives-dismissable-layer.d.ts +147 -99
  84. package/types/radix-ng-primitives-drawer.d.ts +49 -22
  85. package/types/radix-ng-primitives-field.d.ts +1 -0
  86. package/types/radix-ng-primitives-floating-focus-manager.d.ts +175 -0
  87. package/types/radix-ng-primitives-focus-scope.d.ts +132 -1
  88. package/types/radix-ng-primitives-menu.d.ts +204 -112
  89. package/types/radix-ng-primitives-navigation-menu.d.ts +61 -101
  90. package/types/radix-ng-primitives-popover.d.ts +82 -115
  91. package/types/radix-ng-primitives-popper.d.ts +46 -10
  92. package/types/radix-ng-primitives-portal.d.ts +53 -8
  93. package/types/radix-ng-primitives-presence.d.ts +98 -17
  94. package/types/radix-ng-primitives-preview-card.d.ts +63 -95
  95. package/types/radix-ng-primitives-roving-focus.d.ts +7 -6
  96. package/types/radix-ng-primitives-scroll-area.d.ts +2 -2
  97. package/types/radix-ng-primitives-select.d.ts +192 -158
  98. package/types/radix-ng-primitives-slider.d.ts +5 -4
  99. package/types/radix-ng-primitives-stepper.d.ts +4 -3
  100. package/types/radix-ng-primitives-time-field.d.ts +3 -2
  101. package/types/radix-ng-primitives-toast.d.ts +7 -7
  102. package/types/radix-ng-primitives-toggle-group.d.ts +5 -4
  103. package/types/radix-ng-primitives-toolbar.d.ts +3 -2
  104. package/types/radix-ng-primitives-tooltip.d.ts +48 -84
@@ -1,19 +1,19 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, model, input, booleanAttribute, output, computed, signal, untracked, effect, Directive, InjectionToken, ElementRef, Injector, DestroyRef, afterNextRender, ContentChild, linkedSignal, numberAttribute, forwardRef } from '@angular/core';
3
- import { isEqual, getActiveElement, createContext, useTransitionStatus, isNullish, injectId, useListHighlight, useScrollLock, handleAndDispatchCustomEvent } from '@radix-ng/primitives/core';
2
+ import { inject, model, signal, computed, ElementRef, input, booleanAttribute, output, untracked, effect, Directive, InjectionToken, Injector, DestroyRef, afterNextRender, linkedSignal, isDevMode, forwardRef } from '@angular/core';
3
+ import * as i3 from '@radix-ng/primitives/core';
4
+ import { isEqual, getActiveElement, createContext, createFloatingRootContext, useTransitionStatus, isNullish, provideFloatingTree, provideFloatingRootContext, injectId, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, useListHighlight, useAnchoredScrollLock, RdxFloatingNodeRegistration, handleAndDispatchCustomEvent, rdxDevError, setupInternalBackdrop } from '@radix-ng/primitives/core';
5
+ import { injectDirection } from '@radix-ng/primitives/direction-provider';
6
+ import { getInteractionTypeFromEvent } from '@radix-ng/primitives/floating-focus-manager';
4
7
  import * as i1 from '@radix-ng/primitives/popper';
5
- import { RdxPopper, RdxPopperContentWrapper, RdxPopperContent, RdxPopperAnchor } from '@radix-ng/primitives/popper';
6
- import * as i3 from '@radix-ng/primitives/collection';
8
+ import { RdxPopper, RdxPopperContent, RdxPopperContentWrapper, legacyPopperVars, provideRdxPopperContentWrapper, provideRdxPopperContentConfig, RdxPopperAnchor } from '@radix-ng/primitives/popper';
9
+ import * as i4 from '@radix-ng/primitives/collection';
7
10
  import { RdxCollectionProvider, RdxCollectionItem } from '@radix-ng/primitives/collection';
8
- import { outputFromObservable, outputToObservable } from '@angular/core/rxjs-interop';
9
- import * as i2 from '@radix-ng/primitives/dismissable-layer';
10
- import { RdxDismissableLayer, provideRdxDismissableLayerConfig } from '@radix-ng/primitives/dismissable-layer';
11
- import * as i1$1 from '@radix-ng/primitives/focus-scope';
11
+ import { RdxDismiss } from '@radix-ng/primitives/dismissable-layer';
12
+ import * as i2 from '@radix-ng/primitives/focus-scope';
12
13
  import { RdxFocusScope } from '@radix-ng/primitives/focus-scope';
13
- import * as i1$2 from '@radix-ng/primitives/portal';
14
- import { RdxPortal } from '@radix-ng/primitives/portal';
15
- import * as i1$3 from '@radix-ng/primitives/presence';
16
- import { provideRdxPresenceContext, RdxPresenceDirective } from '@radix-ng/primitives/presence';
14
+ import * as i1$1 from '@radix-ng/primitives/portal';
15
+ import { RdxPortalPresence } from '@radix-ng/primitives/portal';
16
+ import { provideRdxPresenceContext } from '@radix-ng/primitives/presence';
17
17
  import { injectFieldRootContext } from '@radix-ng/primitives/field';
18
18
 
19
19
  const OPEN_KEYS = [' ', 'Enter', 'ArrowUp', 'ArrowDown'];
@@ -53,6 +53,17 @@ function focusFirst(candidates) {
53
53
  }
54
54
  }
55
55
 
56
+ function createChangeEventDetails(reason, event) {
57
+ let canceled = false;
58
+ return {
59
+ reason,
60
+ event,
61
+ cancel: () => {
62
+ canceled = true;
63
+ },
64
+ isCanceled: () => canceled
65
+ };
66
+ }
56
67
  const context$2 = () => {
57
68
  const context = inject(RdxSelectRoot);
58
69
  return {
@@ -66,6 +77,10 @@ const context$2 = () => {
66
77
  isItemEqualToValue: context.isItemEqualToValue,
67
78
  itemToStringLabel: context.itemToStringLabel,
68
79
  open: context.open,
80
+ openedByTouch: context.openedByTouch,
81
+ openMethod: context.openMethod,
82
+ openInteractionType: context.openInteractionType,
83
+ closeInteractionType: context.closeInteractionType,
69
84
  disabled: context.disabled,
70
85
  modal: context.modal,
71
86
  isEmptyModelValue: context.isEmptyModelValue,
@@ -85,32 +100,48 @@ const context$2 = () => {
85
100
  context.optionsSet().delete(existingOption);
86
101
  }
87
102
  },
88
- onValueChange: context.handleValueChange,
103
+ onValueChange: (value, reason, event) => context.setValue(value, reason, event),
89
104
  onTriggerChange: (node) => {
90
105
  context.triggerElement.set(node.nativeElement);
91
106
  },
92
107
  onValueElementChange: (node) => {
93
108
  context.valueElement.set(node);
94
109
  },
95
- onOpenChange: (value) => {
96
- context.open.set(value);
97
- }
110
+ onOpenChange: (value, reason, event) => context.setOpen(value, reason, event)
98
111
  };
99
112
  };
100
113
  const [injectSelectRootContext, provideSelectRootContext] = createContext('RdxSelectRootContext', 'components/select');
101
114
  class RdxSelectRoot {
102
115
  constructor() {
103
116
  this.open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
117
+ /** Whether the current open was initiated by **touch** (ADR 0016 §3 — gates the anchored scroll lock). */
118
+ this.openedByTouch = signal(false, ...(ngDevMode ? [{ debugName: "openedByTouch" }] : /* istanbul ignore next */ []));
119
+ /** How the select was opened. Base UI names this state `openMethod`. */
120
+ this.openInteractionType = signal(null, ...(ngDevMode ? [{ debugName: "openInteractionType" }] : /* istanbul ignore next */ []));
121
+ /** How the select was closed. */
122
+ this.closeInteractionType = signal(null, ...(ngDevMode ? [{ debugName: "closeInteractionType" }] : /* istanbul ignore next */ []));
123
+ /** Public Base UI-aligned alias for the open interaction type. */
124
+ this.openMethod = computed(() => this.openInteractionType(), ...(ngDevMode ? [{ debugName: "openMethod" }] : /* istanbul ignore next */ []));
125
+ /** Per-popup floating root context (ADR 0015) — `open` / `triggers` / reference for the dismissal engine. */
126
+ this.floatingContext = createFloatingRootContext({
127
+ ownerDocument: inject(ElementRef).nativeElement.ownerDocument,
128
+ open: () => this.open()
129
+ });
104
130
  this.value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
105
131
  this.multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
106
132
  this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
107
133
  /** Whether the popup is modal: locks page scroll and makes outside content inert while open. */
108
134
  this.modal = input(true, { ...(ngDevMode ? { debugName: "modal" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
109
- this.dir = input('ltr', ...(ngDevMode ? [{ debugName: "dir" }] : /* istanbul ignore next */ []));
135
+ this.dirInput = input(undefined, { ...(ngDevMode ? { debugName: "dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
136
+ this.dir = injectDirection(this.dirInput);
110
137
  /** How item values are compared for equality — a function `(a, b) => boolean` or an object key. */
111
138
  this.isItemEqualToValue = input(...(ngDevMode ? [undefined, { debugName: "isItemEqualToValue" }] : /* istanbul ignore next */ []));
112
139
  /** Converts a value to its display label (used by `RdxSelectValue`). */
113
140
  this.itemToStringLabel = input(...(ngDevMode ? [undefined, { debugName: "itemToStringLabel" }] : /* istanbul ignore next */ []));
141
+ /** Emits before an open-state change is committed; call `eventDetails.cancel()` to veto it. */
142
+ this.onOpenChange = output();
143
+ /** Emits before a value change is committed; call `eventDetails.cancel()` to veto it. */
144
+ this.onValueChange = output();
114
145
  /** Emits after the open/close transition (including any exit animation) finishes. */
115
146
  this.onOpenChangeComplete = output();
116
147
  this.transition = useTransitionStatus((open) => this.onOpenChangeComplete.emit(open));
@@ -151,34 +182,89 @@ class RdxSelectRoot {
151
182
  previousOpen = open;
152
183
  untracked(() => this.transition.start(open));
153
184
  });
185
+ // A fresh open starts non-touch; the trigger flips it on a touch open. Reset whenever it closes.
186
+ effect(() => {
187
+ if (!this.open()) {
188
+ untracked(() => this.openedByTouch.set(false));
189
+ }
190
+ });
191
+ // Bridge the trigger into the floating context: the dismissal capability treats a press on the
192
+ // trigger (the reference) as "inside", and uses it for positioning containment (ADR 0015).
193
+ effect((onCleanup) => {
194
+ const trigger = this.triggerElement();
195
+ this.floatingContext.setReferenceElement(trigger);
196
+ if (trigger) {
197
+ this.floatingContext.triggers.add(trigger);
198
+ onCleanup(() => this.floatingContext.triggers.delete(trigger));
199
+ }
200
+ });
154
201
  }
155
202
  getOption(value) {
156
203
  return Array.from(this.optionsSet()).find((option) => valueComparator(value, option.value, this.isItemEqualToValue()));
157
204
  }
158
- handleValueChange(value) {
205
+ setValue(value, reason = 'none', event = new Event('select.value-change')) {
206
+ const nextValue = this.multiple()
207
+ ? (() => {
208
+ const current = this.value();
209
+ const array = Array.isArray(current) ? [...current] : [];
210
+ const index = array.findIndex((i) => compare(i, value, this.isItemEqualToValue()));
211
+ index === -1 ? array.push(value) : array.splice(index, 1);
212
+ return [...array];
213
+ })()
214
+ : value;
215
+ const eventDetails = createChangeEventDetails(reason, event);
216
+ this.onValueChange.emit({ value: nextValue, eventDetails });
217
+ if (eventDetails.isCanceled()) {
218
+ return false;
219
+ }
159
220
  if (this.multiple()) {
160
- const current = this.value();
161
- const array = Array.isArray(current) ? [...current] : [];
162
- const index = array.findIndex((i) => compare(i, value, this.isItemEqualToValue()));
163
- index === -1 ? array.push(value) : array.splice(index, 1);
164
- this.value.set([...array]);
221
+ this.value.set(nextValue);
165
222
  }
166
223
  else {
167
- this.value.set(value);
224
+ this.value.set(nextValue);
168
225
  }
226
+ return true;
227
+ }
228
+ setOpen(open, reason = 'none', event) {
229
+ const resolvedEvent = event ?? new Event('select.open-change');
230
+ const interactionType = getInteractionTypeFromEvent(event);
231
+ const eventDetails = createChangeEventDetails(reason, resolvedEvent);
232
+ this.onOpenChange.emit({ open, eventDetails });
233
+ if (eventDetails.isCanceled()) {
234
+ return false;
235
+ }
236
+ this.open.set(open);
237
+ if (open) {
238
+ this.openedByTouch.set(event?.pointerType === 'touch');
239
+ this.openInteractionType.set(interactionType);
240
+ }
241
+ else {
242
+ this.closeInteractionType.set(interactionType);
243
+ }
244
+ return true;
169
245
  }
170
246
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
171
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxSelectRoot, isStandalone: true, selector: "[rdxSelectRoot]", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, dir: { classPropertyName: "dir", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToStringLabel: { classPropertyName: "itemToStringLabel", publicName: "itemToStringLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", value: "valueChange", onOpenChangeComplete: "onOpenChangeComplete" }, providers: [provideSelectRootContext(context$2)], exportAs: ["rdxSelectRoot"], hostDirectives: [{ directive: i1.RdxPopper }], ngImport: i0 }); }
247
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxSelectRoot, isStandalone: true, selector: "[rdxSelectRoot]", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, dirInput: { classPropertyName: "dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToStringLabel: { classPropertyName: "itemToStringLabel", publicName: "itemToStringLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", value: "valueChange", onOpenChange: "onOpenChange", onValueChange: "onValueChange", onOpenChangeComplete: "onOpenChangeComplete" }, providers: [
248
+ provideSelectRootContext(context$2),
249
+ // New floating foundation (ADR 0015/0017) — the dismissal capability reads this shared context.
250
+ provideFloatingTree(),
251
+ provideFloatingRootContext(() => inject(RdxSelectRoot).floatingContext)
252
+ ], exportAs: ["rdxSelectRoot"], hostDirectives: [{ directive: i1.RdxPopper }], ngImport: i0 }); }
172
253
  }
173
254
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectRoot, decorators: [{
174
255
  type: Directive,
175
256
  args: [{
176
257
  selector: '[rdxSelectRoot]',
177
258
  exportAs: 'rdxSelectRoot',
178
- providers: [provideSelectRootContext(context$2)],
259
+ providers: [
260
+ provideSelectRootContext(context$2),
261
+ // New floating foundation (ADR 0015/0017) — the dismissal capability reads this shared context.
262
+ provideFloatingTree(),
263
+ provideFloatingRootContext(() => inject(RdxSelectRoot).floatingContext)
264
+ ],
179
265
  hostDirectives: [RdxPopper]
180
266
  }]
181
- }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], dir: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToStringLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringLabel", required: false }] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
267
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToStringLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringLabel", required: false }] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
182
268
 
183
269
  /**
184
270
  * An overlay rendered beneath the popup in `modal` mode. Place it inside the portal/presence; style
@@ -300,32 +386,41 @@ const [injectSelectPopupContext, provideSelectPopupContext] = createContext('Rdx
300
386
  const RDX_SELECT_POSITIONER_TOKEN = new InjectionToken('RDX_SELECT_POSITIONER_TOKEN');
301
387
  /**
302
388
  * The popup listbox. Holds DOM focus while open and navigates with the highlight model
303
- * (`aria-activedescendant`) — items are not individually focusable. (Renamed to `RdxSelectPopup` in a
304
- * later step; selector kept here during the navigation migration.)
389
+ * (`aria-activedescendant`) — items are not individually focusable.
390
+ *
391
+ * Since ADR 0010 §6 the popup is the **inner** element (the positioner is its ancestor): it carries
392
+ * `role="listbox"`, the `contentId`, and — via the composed {@link RdxPopperContent} — the
393
+ * `data-side` / `data-align` attributes and the until-positioned animation guard previously held by
394
+ * the deleted `rdxSelectPositionerContent`. `RdxPopperContent` also makes the popup the element the
395
+ * `RdxPopperContentWrapper` ancestor reads its content z-index from. In item-aligned mode there is no
396
+ * wrapper, so `RdxPopperContent` no-ops.
305
397
  *
306
398
  * @group Components
307
399
  */
308
400
  class RdxSelectPopup {
309
- set positioner(port) {
310
- if (port) {
311
- port.placed.subscribe(() => {
312
- this.highlightSelectedItem();
313
- this.scrollSelectedIntoView();
314
- this.isPositioned.set(true);
315
- });
316
- }
317
- }
318
401
  constructor() {
319
- this.dismissableLayer = inject(RdxDismissableLayer);
402
+ this.floatingContext = inject(RDX_FLOATING_ROOT_CONTEXT);
403
+ this.registration = inject(RDX_FLOATING_REGISTRATION, { optional: true });
320
404
  this.currentElement = inject(ElementRef);
321
405
  this.collection = inject(RdxCollectionProvider);
322
406
  this.injector = inject(Injector);
323
407
  this.rootContext = injectSelectRootContext();
324
- /** Highlight-model navigation over the collected items (DOM order). */
408
+ /**
409
+ * The collected items (DOM order). Exposed so the `item-aligned` positioner — now the popup's
410
+ * **ancestor** — can read them without injecting {@link RdxCollectionProvider} (which the popup
411
+ * provides as a descendant, so an upward `inject` would not find it).
412
+ */
413
+ this.items = this.collection.items;
414
+ /**
415
+ * Highlight-model navigation over the collected items (DOM order). `loop` is disabled so arrow
416
+ * navigation stops at the first / last item instead of wrapping around — matching native
417
+ * `<select>` behavior.
418
+ */
325
419
  this.highlight = useListHighlight({
326
420
  items: this.collection.items,
327
421
  isNavigable: (item) => !item.disabled(),
328
422
  getId: (item) => item.element.id,
423
+ loop: signal(false),
329
424
  injector: this.injector
330
425
  });
331
426
  this.selectedItem = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedItem" }] : /* istanbul ignore next */ []));
@@ -340,20 +435,69 @@ class RdxSelectPopup {
340
435
  * Event handler called when the escape key is down.
341
436
  * Can be prevented.
342
437
  */
343
- this.escapeKeyDown = outputFromObservable(outputToObservable(this.dismissableLayer.escapeKeyDown));
438
+ this.escapeKeyDown = output();
344
439
  /**
345
- * Event handler called when a `pointerdown` event happens outside of the `DismissableLayer`.
440
+ * Event handler called when a `pointerdown` event happens outside of the popup.
346
441
  * Can be prevented.
347
442
  */
348
- this.pointerDownOutside = outputFromObservable(outputToObservable(this.dismissableLayer.pointerDownOutside));
443
+ this.pointerDownOutside = output();
349
444
  this.content = signal(null, ...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
350
- // Lock page scroll while a modal popup is open (content mounts only while open).
351
- useScrollLock(this.rootContext.modal);
445
+ /**
446
+ * The positioner — now an **ancestor** element — provides {@link RDX_SELECT_POSITIONER_TOKEN}
447
+ * (Popper or item-aligned). We react to its `placed` to highlight and scroll the selected item
448
+ * into view and flag the popup as positioned.
449
+ */
450
+ this.positioner = inject(RDX_SELECT_POSITIONER_TOKEN, { optional: true });
451
+ this.positioner?.placed.subscribe(() => {
452
+ this.highlightSelectedItem();
453
+ this.scrollSelectedIntoView();
454
+ this.isPositioned.set(true);
455
+ // In Popper mode the popup lives inside the positioner, which stays `visibility: hidden`
456
+ // until it is placed — so the mount-time `mountAutoFocus` call no-ops on the hidden
457
+ // listbox and keyboard navigation never starts. Focus it now that it is visible (skip if
458
+ // focus already moved inside, e.g. item-aligned mode or a re-placement).
459
+ const popup = this.content();
460
+ if (popup && !popup.contains(document.activeElement)) {
461
+ popup.focus({ preventScroll: true });
462
+ }
463
+ });
464
+ // Keep the highlighted item in view during keyboard navigation. The highlight model is pure
465
+ // state (it never moves DOM focus or scrolls), so without this the highlight can move past the
466
+ // visible viewport — behind the scroll buttons. Only keyboard moves scroll; hover highlights
467
+ // must not (the cursor is already over a visible item).
468
+ effect(() => {
469
+ const item = this.highlight.highlightedItem();
470
+ if (item && this.keyboardActive) {
471
+ item.element.scrollIntoView?.({ block: 'nearest' });
472
+ }
473
+ });
474
+ // Activation policy (ADR 0016 §2 + §3). Lock page scroll while the popup is OPEN and either modal
475
+ // **or** item-aligned — Base UI `(alignItemWithTriggerActive || modal) && open` (AC #3): an
476
+ // item-aligned select overlays the trigger, so the page must not scroll behind it even when
477
+ // `modal === false`. The gate keys on `open` (not mounted) so it releases at close-start. A
478
+ // **touch** open never uses item-aligned mode (the positioner falls back), so the lock there is
479
+ // driven by `modal` alone and the anchored helper only engages when the popup is viewport-width (§3).
480
+ const itemAlignedActive = computed(() => this.positioner?.alignItemWithTriggerActive?.() ?? false, ...(ngDevMode ? [{ debugName: "itemAlignedActive" }] : /* istanbul ignore next */ []));
481
+ useAnchoredScrollLock(computed(() => (itemAlignedActive() || this.rootContext.modal()) && this.rootContext.open()), {
482
+ touchOpen: () => this.rootContext.openedByTouch(),
483
+ element: () => this.currentElement.nativeElement
484
+ });
352
485
  // The popup's animation determines when the open/close transition (onOpenChangeComplete) is done.
353
486
  const unregisterTransition = this.rootContext.registerTransitionElement(this.currentElement.nativeElement);
354
487
  inject(DestroyRef).onDestroy(unregisterTransition);
355
- this.dismissableLayer.focusOutside.subscribe((e) => e.preventDefault());
356
- this.dismissableLayer.dismiss.subscribe(() => this.rootContext.onOpenChange(false));
488
+ // The popup (listbox) is this layer's floating element — the inside surface for containment.
489
+ this.floatingContext.setFloatingElement(this.currentElement.nativeElement);
490
+ // Dismissal (ADR 0015): Escape or an outside press closes the select. Focus-out does NOT close it
491
+ // — the listbox holds focus while open (items are navigated virtually), so a focus-out is not a
492
+ // dismissal (the legacy preventDefaulted it too).
493
+ new RdxDismiss(this.floatingContext, () => this.registration?.node() ?? null, {
494
+ escapeKey: () => true,
495
+ outsidePress: () => true,
496
+ focusOutside: () => false,
497
+ onEscapeKeyDown: (event) => this.escapeKeyDown.emit(event),
498
+ onPointerDownOutside: (event) => this.pointerDownOutside.emit(event),
499
+ onDismiss: (reason, event) => this.rootContext.onOpenChange(false, reason === 'escape-key' ? 'escape-key' : reason === 'focus-outside' ? 'focus-out' : 'outside-press', event)
500
+ });
357
501
  const focusScope = inject(RdxFocusScope);
358
502
  afterNextRender(() => {
359
503
  focusScope.unmountAutoFocus.subscribe((event) => {
@@ -368,7 +512,8 @@ class RdxSelectPopup {
368
512
  event.preventDefault();
369
513
  this.content()?.focus({ preventScroll: true });
370
514
  });
371
- this.content.set(this.currentElement.nativeElement.firstElementChild);
515
+ // The popup is now the listbox host itself (no longer the positioner's first child).
516
+ this.content.set(this.currentElement.nativeElement);
372
517
  });
373
518
  effect((onCleanup) => {
374
519
  if (!this.content())
@@ -390,8 +535,9 @@ class RdxSelectPopup {
390
535
  }
391
536
  else {
392
537
  // otherwise, if the event was outside the content, close.
393
- if (!this.content()?.contains(event.target))
394
- this.rootContext.onOpenChange(false);
538
+ if (!this.content()?.contains(event.target)) {
539
+ this.rootContext.onOpenChange(false, 'outside-press', event);
540
+ }
395
541
  }
396
542
  document.removeEventListener('pointermove', handlePointerMove);
397
543
  this.rootContext.triggerPointerDownPosRef.set(null);
@@ -410,6 +556,21 @@ class RdxSelectPopup {
410
556
  });
411
557
  });
412
558
  });
559
+ effect((onCleanup) => {
560
+ if (!itemAlignedActive() || !this.rootContext.open()) {
561
+ return;
562
+ }
563
+ const popup = this.content();
564
+ const view = popup?.ownerDocument.defaultView;
565
+ if (!view) {
566
+ return;
567
+ }
568
+ const handleResize = (event) => {
569
+ this.rootContext.onOpenChange(false, 'window-resize', event);
570
+ };
571
+ view.addEventListener('resize', handleResize);
572
+ onCleanup(() => view.removeEventListener('resize', handleResize));
573
+ });
413
574
  }
414
575
  /** Highlights the selected item (or the first enabled one) when the popup opens. */
415
576
  highlightSelectedItem() {
@@ -444,9 +605,10 @@ class RdxSelectPopup {
444
605
  event.preventDefault();
445
606
  const item = this.highlight.highlightedItem();
446
607
  if (item && !item.disabled()) {
447
- this.rootContext.onValueChange(item.value());
448
- if (!this.rootContext.multiple())
449
- this.rootContext.onOpenChange(false);
608
+ this.rootContext.onValueChange(item.value(), 'item-press', event);
609
+ if (!this.rootContext.multiple()) {
610
+ this.rootContext.onOpenChange(false, 'item-press', event);
611
+ }
450
612
  }
451
613
  return;
452
614
  }
@@ -470,31 +632,18 @@ class RdxSelectPopup {
470
632
  }
471
633
  }
472
634
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPopup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
473
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxSelectPopup, isStandalone: true, selector: "[rdxSelectPopup]", outputs: { escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside" }, host: { attributes: { "role": "listbox", "tabindex": "-1" }, listeners: { "keydown": "handleKeyDown($event)" }, properties: { "attr.aria-activedescendant": "highlight.activeId()", "attr.aria-multiselectable": "rootContext.multiple() ? \"true\" : undefined", "attr.data-state": "rootContext.open() ? \"open\" : \"closed\"", "attr.data-open": "rootContext.open() ? \"\" : undefined", "attr.data-closed": "rootContext.open() ? undefined : \"\"", "attr.data-starting-style": "rootContext.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-ending-style": "rootContext.transitionStatus() === \"ending\" ? \"\" : undefined", "dir": "rootContext.dir()", "style": "{\n display: 'flex',\n flexDirection: 'column',\n outline: 'none'\n }" } }, providers: [
474
- provideSelectPopupContext(context$1),
475
- provideRdxDismissableLayerConfig(() => {
476
- return {
477
- disableOutsidePointerEvents: injectSelectRootContext().modal
478
- };
479
- })
480
- ], queries: [{ propertyName: "positioner", first: true, predicate: RDX_SELECT_POSITIONER_TOKEN, descendants: true }], hostDirectives: [{ directive: i1$1.RdxFocusScope }, { directive: i2.RdxDismissableLayer }, { directive: i3.RdxCollectionProvider }], ngImport: i0 }); }
635
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxSelectPopup, isStandalone: true, selector: "[rdxSelectPopup]", outputs: { escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside" }, host: { attributes: { "role": "listbox", "tabindex": "-1" }, listeners: { "keydown": "handleKeyDown($event)" }, properties: { "id": "rootContext.contentId", "attr.aria-activedescendant": "highlight.activeId()", "attr.aria-multiselectable": "rootContext.multiple() ? \"true\" : undefined", "attr.data-state": "rootContext.open() ? \"open\" : \"closed\"", "attr.data-open": "rootContext.open() ? \"\" : undefined", "attr.data-closed": "rootContext.open() ? undefined : \"\"", "attr.data-starting-style": "rootContext.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-ending-style": "rootContext.transitionStatus() === \"ending\" ? \"\" : undefined", "dir": "rootContext.dir()", "style": "{\n display: 'flex',\n flexDirection: 'column',\n outline: 'none'\n }" } }, providers: [provideSelectPopupContext(context$1)], hostDirectives: [{ directive: i1.RdxPopperContent }, { directive: i2.RdxFocusScope }, { directive: i3.RdxFloatingNodeRegistration }, { directive: i4.RdxCollectionProvider }], ngImport: i0 }); }
481
636
  }
482
637
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPopup, decorators: [{
483
638
  type: Directive,
484
639
  args: [{
485
640
  selector: '[rdxSelectPopup]',
486
- hostDirectives: [RdxFocusScope, RdxDismissableLayer, RdxCollectionProvider],
487
- providers: [
488
- provideSelectPopupContext(context$1),
489
- provideRdxDismissableLayerConfig(() => {
490
- return {
491
- disableOutsidePointerEvents: injectSelectRootContext().modal
492
- };
493
- })
494
- ],
641
+ hostDirectives: [RdxPopperContent, RdxFocusScope, RdxFloatingNodeRegistration, RdxCollectionProvider],
642
+ providers: [provideSelectPopupContext(context$1)],
495
643
  host: {
496
644
  role: 'listbox',
497
645
  tabindex: '-1',
646
+ '[id]': 'rootContext.contentId',
498
647
  '[attr.aria-activedescendant]': 'highlight.activeId()',
499
648
  '[attr.aria-multiselectable]': 'rootContext.multiple() ? "true" : undefined',
500
649
  '[attr.data-state]': 'rootContext.open() ? "open" : "closed"',
@@ -511,10 +660,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
511
660
  }`
512
661
  }
513
662
  }]
514
- }], ctorParameters: () => [], propDecorators: { escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }], pointerDownOutside: [{ type: i0.Output, args: ["pointerDownOutside"] }], positioner: [{
515
- type: ContentChild,
516
- args: [RDX_SELECT_POSITIONER_TOKEN, { descendants: true }]
517
- }] } });
663
+ }], ctorParameters: () => [], propDecorators: { escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }], pointerDownOutside: [{ type: i0.Output, args: ["pointerDownOutside"] }] } });
518
664
 
519
665
  const context = () => {
520
666
  const context = inject(RdxSelectItem);
@@ -559,9 +705,9 @@ class RdxSelectItem {
559
705
  handleAndDispatchCustomEvent(this.SELECT_SELECT, async (event) => {
560
706
  if (event.defaultPrevented)
561
707
  return;
562
- this.rootContext.onValueChange(this.value());
708
+ this.rootContext.onValueChange(this.value(), 'item-press', event.detail.originalEvent);
563
709
  if (!this.rootContext.multiple()) {
564
- this.rootContext.onOpenChange(false);
710
+ this.rootContext.onOpenChange(false, 'item-press', event.detail.originalEvent);
565
711
  }
566
712
  }, eventDetail);
567
713
  }
@@ -590,7 +736,7 @@ class RdxSelectItem {
590
736
  this.onPointerUp(keyEvent);
591
737
  }
592
738
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
593
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxSelectItem, isStandalone: true, selector: "[rdxSelectItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, textValue: { classPropertyName: "textValue", publicName: "textValue", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "option" }, listeners: { "pointerup": "onPointerUp($event)", "pointerleave": "onPointerLeave($event)", "pointermove": "onPointerMove($event)" }, properties: { "attr.id": "id", "attr.aria-selected": "isSelected()", "attr.aria-disabled": "disabled() ? \"true\" : undefined", "attr.data-state": "isSelected() ? \"checked\" : \"unchecked\"", "attr.data-selected": "isSelected() ? \"\" : undefined", "attr.data-highlighted": "isHighlighted() ? \"\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined" } }, providers: [provideSelectItemContext(context)], exportAs: ["rdxSelectItem"], hostDirectives: [{ directive: i3.RdxCollectionItem, inputs: ["value", "value", "disabled", "disabled"] }], ngImport: i0 }); }
739
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxSelectItem, isStandalone: true, selector: "[rdxSelectItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, textValue: { classPropertyName: "textValue", publicName: "textValue", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "option" }, listeners: { "pointerup": "onPointerUp($event)", "pointerleave": "onPointerLeave($event)", "pointermove": "onPointerMove($event)" }, properties: { "attr.id": "id", "attr.aria-selected": "isSelected()", "attr.aria-disabled": "disabled() ? \"true\" : undefined", "attr.data-state": "isSelected() ? \"checked\" : \"unchecked\"", "attr.data-selected": "isSelected() ? \"\" : undefined", "attr.data-highlighted": "isHighlighted() ? \"\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined" } }, providers: [provideSelectItemContext(context)], exportAs: ["rdxSelectItem"], hostDirectives: [{ directive: i4.RdxCollectionItem, inputs: ["value", "value", "disabled", "disabled"] }], ngImport: i0 }); }
594
740
  }
595
741
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectItem, decorators: [{
596
742
  type: Directive,
@@ -708,177 +854,98 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
708
854
  }]
709
855
  }], ctorParameters: () => [] });
710
856
 
857
+ /**
858
+ * Structural directive that teleports the select popup into a container (default `document.body`)
859
+ * while the select is open, and keeps it mounted until any CSS exit `@keyframes` finishes.
860
+ *
861
+ * Apply it with the `*` microsyntax on the popup — `<div *rdxSelectPortal rdxSelectPopup>` — or as an
862
+ * explicit `<ng-template rdxSelectPortal>`. For a custom container use the explicit form with
863
+ * `[container]`. Unlike the previous attribute portal it no longer parks an empty wrapper `<div>` in
864
+ * `document.body` while the select is closed.
865
+ */
711
866
  class RdxSelectPortal {
712
- constructor() {
713
- /**
714
- * Optional container to portal the content into. Defaults to `document.body` when not set.
715
- */
716
- this.container = input(...(ngDevMode ? [undefined, { debugName: "container" }] : /* istanbul ignore next */ []));
717
- }
718
867
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPortal, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
719
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxSelectPortal, isStandalone: true, selector: "[rdxSelectPortal]", inputs: { container: { classPropertyName: "container", publicName: "container", isSignal: true, isRequired: false, transformFunction: null } }, hostDirectives: [{ directive: i1$2.RdxPortal, inputs: ["container", "container"] }], ngImport: i0 }); }
868
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxSelectPortal, isStandalone: true, selector: "ng-template[rdxSelectPortal]", providers: [provideRdxPresenceContext(() => ({ present: injectSelectRootContext().open }))], exportAs: ["rdxSelectPortal"], hostDirectives: [{ directive: i1$1.RdxPortalPresence, inputs: ["container", "container"] }], ngImport: i0 }); }
720
869
  }
721
870
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPortal, decorators: [{
722
871
  type: Directive,
723
872
  args: [{
724
- selector: '[rdxSelectPortal]',
725
- hostDirectives: [
726
- {
727
- directive: RdxPortal,
728
- inputs: ['container']
729
- }
730
- ]
873
+ selector: 'ng-template[rdxSelectPortal]',
874
+ exportAs: 'rdxSelectPortal',
875
+ hostDirectives: [{ directive: RdxPortalPresence, inputs: ['container'] }],
876
+ providers: [provideRdxPresenceContext(() => ({ present: injectSelectRootContext().open }))]
731
877
  }]
732
- }], propDecorators: { container: [{ type: i0.Input, args: [{ isSignal: true, alias: "container", required: false }] }] } });
733
-
734
- class RdxSelectPortalPresence {
735
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPortalPresence, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
736
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxSelectPortalPresence, isStandalone: true, selector: "ng-template[rdxSelectPortalPresence]", providers: [
737
- provideRdxPresenceContext(() => {
738
- const context = injectSelectRootContext();
739
- return { present: context.open };
740
- })
741
- ], hostDirectives: [{ directive: i1$3.RdxPresenceDirective }], ngImport: i0 }); }
878
+ }] });
879
+ /**
880
+ * Dev-mode guard: `rdxSelectPortal` used to be an attribute directive on a `<div>`. It is now
881
+ * structural, so the old `<div rdxSelectPortal>` markup would silently stop portaling fail loudly
882
+ * instead.
883
+ */
884
+ class RdxSelectPortalMisuseGuard {
885
+ constructor() {
886
+ if (isDevMode()) {
887
+ rdxDevError('select/portal-on-element', '`rdxSelectPortal` is now a structural directive. ' +
888
+ 'Use `*rdxSelectPortal` on the popup element or `<ng-template rdxSelectPortal>`. ' +
889
+ 'rdxSelectPortalPresence has been removed.', 'components/select');
890
+ }
891
+ }
892
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPortalMisuseGuard, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
893
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxSelectPortalMisuseGuard, isStandalone: true, selector: "[rdxSelectPortal]:not(ng-template)", ngImport: i0 }); }
742
894
  }
743
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPortalPresence, decorators: [{
895
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPortalMisuseGuard, decorators: [{
744
896
  type: Directive,
745
897
  args: [{
746
- selector: 'ng-template[rdxSelectPortalPresence]',
747
- hostDirectives: [RdxPresenceDirective],
748
- providers: [
749
- provideRdxPresenceContext(() => {
750
- const context = injectSelectRootContext();
751
- return { present: context.open };
752
- })
753
- ]
898
+ selector: '[rdxSelectPortal]:not(ng-template)'
754
899
  }]
755
- }] });
900
+ }], ctorParameters: () => [] });
756
901
 
757
- class RdxSelectPositioner {
902
+ /**
903
+ * Positions the select popup against the trigger using the popper engine.
904
+ *
905
+ * A "thin" positioner (ADR 0012): it inherits the full popper positioning surface — the inputs
906
+ * (`side`, `sideOffset`, `align`, …), the `placed` output, and the host bindings — from
907
+ * {@link RdxPopperContentWrapper}, and declares select's Base UI-aligned defaults via the config
908
+ * provider. It also satisfies {@link RdxPositionerImpl} (via the inherited `placed`) so the popup can
909
+ * resolve it through {@link RDX_SELECT_POSITIONER_TOKEN}, the same as the item-aligned positioner.
910
+ */
911
+ class RdxSelectPositioner extends RdxPopperContentWrapper {
758
912
  constructor() {
759
- /**
760
- * The preferred side of the anchor to render against when open.
761
- * Will be reversed when collisions occur and avoidCollisions is enabled.
762
- */
763
- this.side = input('bottom', ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
764
- /**
765
- * The distance in pixels from the anchor.
766
- */
767
- this.sideOffset = input(0, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
768
- /**
769
- * The preferred alignment against the anchor. May change when collisions occur.
770
- */
771
- this.align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
772
- /**
773
- * An offset in pixels from the `start` or `end` alignment options.
774
- */
775
- this.alignOffset = input(0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
776
- /**
777
- * The padding between the arrow and the edges of the content.
778
- * If your content has border-radius, this will prevent it from overflowing the corners.
779
- */
780
- this.arrowPadding = input(0, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
781
- /**
782
- * When `true`, overrides the `side` and `align` preferences to prevent collisions with boundary edges.
783
- */
784
- this.avoidCollisions = input(true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
785
- /**
786
- * The element used as the collision boundary.
787
- * By default this is the viewport, though you can provide additional element(s) to be included in this check.
788
- */
789
- this.collisionBoundary = input(...(ngDevMode ? [undefined, { debugName: "collisionBoundary" }] : /* istanbul ignore next */ []));
790
- /**
791
- * The distance in pixels from the boundary edges where collision detection should occur.
792
- * Accepts a number (same for all sides), or a partial padding object, for example: `{ top: 20, left: 20 }`.
793
- */
794
- this.collisionPadding = input(0, ...(ngDevMode ? [{ debugName: "collisionPadding" }] : /* istanbul ignore next */ []));
795
- /**
796
- * The sticky behavior on the `align` axis.
797
- * - `partial` will keep the content in the boundary as long as the trigger is at least partially in the boundary
798
- * - `always` will keep the content in the boundary regardless.
799
- */
800
- this.sticky = input('partial', ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
801
- /**
802
- * Whether to hide the content when the trigger becomes fully occluded.
803
- */
804
- this.hideWhenDetached = input(false, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
805
- /**
806
- * Whether to update the position of the floating element on every animation frame if required.
807
- */
808
- this.updatePositionStrategy = input('always', ...(ngDevMode ? [{ debugName: "updatePositionStrategy" }] : /* istanbul ignore next */ []));
809
- /**
810
- * Emits when the element is placed.
811
- */
812
- this.placed = outputFromObservable(outputToObservable(inject(RdxPopperContentWrapper).placed));
913
+ super();
914
+ this.positionerStyle = { boxSizing: 'border-box', ...legacyPopperVars('select') };
915
+ const rootContext = injectSelectRootContext();
916
+ const injector = inject(Injector);
917
+ const host = inject(ElementRef).nativeElement;
918
+ // Modal select isolates the background with an internal backdrop (finding #1 / Base UI) instead of
919
+ // a global pointer lock; the trigger stays clickable through a cutout (toggle-close).
920
+ afterNextRender(() => setupInternalBackdrop(host, injector, {
921
+ isOpen: () => rootContext.open(),
922
+ shouldRender: () => rootContext.modal(),
923
+ cutout: () => rootContext.triggerElement()
924
+ }));
813
925
  }
814
926
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPositioner, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
815
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxSelectPositioner, isStandalone: true, selector: "[rdxSelectPositioner]", 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 } }, outputs: { placed: "placed" }, host: { properties: { "style": "{\n 'boxSizing': 'border-box',\n '--radix-tooltip-content-transform-origin': 'var(--radix-popper-transform-origin)',\n '--radix-tooltip-content-available-width': 'var(--radix-popper-available-width)',\n '--radix-tooltip-content-available-height': 'var(--radix-popper-available-height)',\n '--radix-tooltip-trigger-width': 'var(--radix-popper-anchor-width)',\n '--radix-tooltip-trigger-height': 'var(--radix-popper-anchor-height)',\n }" } }, providers: [
816
- {
817
- provide: RDX_SELECT_POSITIONER_TOKEN,
818
- useExisting: forwardRef(() => RdxSelectPositioner)
819
- }
820
- ], hostDirectives: [{ directive: i1.RdxPopperContentWrapper, inputs: ["side", "side", "sideOffset", "sideOffset", "align", "align", "alignOffset", "alignOffset", "arrowPadding", "arrowPadding", "avoidCollisions", "avoidCollisions", "collisionBoundary", "collisionBoundary", "collisionPadding", "collisionPadding", "sticky", "sticky", "hideWhenDetached", "hideWhenDetached", "updatePositionStrategy", "updatePositionStrategy"] }], ngImport: i0 }); }
927
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxSelectPositioner, isStandalone: true, selector: "[rdxSelectPositioner]", host: { properties: { "style": "positionerStyle" } }, providers: [
928
+ ...provideRdxPopperContentWrapper(RdxSelectPositioner),
929
+ provideRdxPopperContentConfig({ align: 'start', updatePositionStrategy: 'always' }),
930
+ { provide: RDX_SELECT_POSITIONER_TOKEN, useExisting: forwardRef(() => RdxSelectPositioner) }
931
+ ], usesInheritance: true, ngImport: i0 }); }
821
932
  }
822
933
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPositioner, decorators: [{
823
934
  type: Directive,
824
935
  args: [{
825
936
  selector: '[rdxSelectPositioner]',
826
937
  providers: [
827
- {
828
- provide: RDX_SELECT_POSITIONER_TOKEN,
829
- useExisting: forwardRef(() => RdxSelectPositioner)
830
- }
938
+ ...provideRdxPopperContentWrapper(RdxSelectPositioner),
939
+ provideRdxPopperContentConfig({ align: 'start', updatePositionStrategy: 'always' }),
940
+ { provide: RDX_SELECT_POSITIONER_TOKEN, useExisting: forwardRef(() => RdxSelectPositioner) }
831
941
  ],
832
- hostDirectives: [
833
- {
834
- directive: RdxPopperContentWrapper,
835
- inputs: [
836
- 'side',
837
- 'sideOffset',
838
- 'align',
839
- 'alignOffset',
840
- 'arrowPadding',
841
- 'avoidCollisions',
842
- 'collisionBoundary',
843
- 'collisionPadding',
844
- 'sticky',
845
- 'hideWhenDetached',
846
- 'updatePositionStrategy'
847
- ]
848
- }
849
- ],
850
- host: {
851
- // re-namespace exposed content custom properties
852
- '[style]': `{
853
- 'boxSizing': 'border-box',
854
- '--radix-tooltip-content-transform-origin': 'var(--radix-popper-transform-origin)',
855
- '--radix-tooltip-content-available-width': 'var(--radix-popper-available-width)',
856
- '--radix-tooltip-content-available-height': 'var(--radix-popper-available-height)',
857
- '--radix-tooltip-trigger-width': 'var(--radix-popper-anchor-width)',
858
- '--radix-tooltip-trigger-height': 'var(--radix-popper-anchor-height)',
859
- }`
860
- }
861
- }]
862
- }], 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 }] }], placed: [{ type: i0.Output, args: ["placed"] }] } });
863
-
864
- class RdxSelectPositionerContent {
865
- constructor() {
866
- this.rootContext = injectSelectRootContext();
867
- }
868
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPositionerContent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
869
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxSelectPositionerContent, isStandalone: true, selector: "[rdxSelectPositionerContent]", host: { attributes: { "role": "listbox" }, properties: { "id": "rootContext.contentId" } }, hostDirectives: [{ directive: i1.RdxPopperContent }], ngImport: i0 }); }
870
- }
871
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxSelectPositionerContent, decorators: [{
872
- type: Directive,
873
- args: [{
874
- selector: '[rdxSelectPositionerContent]',
875
- hostDirectives: [RdxPopperContent],
876
942
  host: {
877
- role: 'listbox',
878
- '[id]': 'rootContext.contentId'
943
+ // The unified vars + placement attrs come from the wrapper (ADR 0012); only `box-sizing` and
944
+ // the deprecated `--radix-select-*` aliases (still consumed by demos) remain here.
945
+ '[style]': 'positionerStyle'
879
946
  }
880
947
  }]
881
- }] });
948
+ }], ctorParameters: () => [] });
882
949
 
883
950
  /**
884
951
  * A visual divider between groups of items.
@@ -932,18 +999,20 @@ class RdxSelectTrigger {
932
999
  this.fieldRootContext?.setFilled(!this.rootContext.isEmptyModelValue());
933
1000
  });
934
1001
  }
935
- handleOpen() {
1002
+ handleOpen(reason, event) {
936
1003
  if (!this.isDisabled()) {
937
- this.rootContext.onOpenChange(true);
1004
+ return this.rootContext.onOpenChange(true, reason, event);
938
1005
  }
1006
+ return false;
939
1007
  }
940
1008
  handlePointerOpen(event) {
941
1009
  const pointerEvent = event;
942
- this.handleOpen();
943
- this.rootContext.triggerPointerDownPosRef.set({
944
- x: Math.round(pointerEvent.pageX),
945
- y: Math.round(pointerEvent.pageY)
946
- });
1010
+ if (this.handleOpen('trigger-press', event)) {
1011
+ this.rootContext.triggerPointerDownPosRef.set({
1012
+ x: Math.round(pointerEvent.pageX),
1013
+ y: Math.round(pointerEvent.pageY)
1014
+ });
1015
+ }
947
1016
  }
948
1017
  onClickHandler(event) {
949
1018
  // Whilst browsers generally have no issue focusing the trigger when clicking
@@ -981,7 +1050,8 @@ class RdxSelectTrigger {
981
1050
  onKeydown(event) {
982
1051
  const keyEvent = event;
983
1052
  if (OPEN_KEYS.includes(keyEvent.key)) {
984
- this.handleOpen();
1053
+ const reason = keyEvent.key === 'ArrowUp' || keyEvent.key === 'ArrowDown' ? 'list-navigation' : 'trigger-press';
1054
+ this.handleOpen(reason, event);
985
1055
  event.preventDefault();
986
1056
  }
987
1057
  }
@@ -1071,6 +1141,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1071
1141
  const _importsSelect = [
1072
1142
  RdxSelectRoot,
1073
1143
  RdxSelectPortal,
1144
+ RdxSelectPortalMisuseGuard,
1074
1145
  RdxSelectTrigger,
1075
1146
  RdxSelectValue,
1076
1147
  RdxSelectPopup,
@@ -1080,9 +1151,7 @@ const _importsSelect = [
1080
1151
  RdxSelectItemText,
1081
1152
  RdxSelectGroupLabel,
1082
1153
  RdxSelectGroup,
1083
- RdxSelectPositionerContent,
1084
1154
  RdxSelectPositioner,
1085
- RdxSelectPortalPresence,
1086
1155
  RdxSelectIcon,
1087
1156
  RdxSelectSeparator,
1088
1157
  RdxSelectBackdrop
@@ -1092,5 +1161,5 @@ const _importsSelect = [
1092
1161
  * Generated bundle index. Do not edit.
1093
1162
  */
1094
1163
 
1095
- export { CONTENT_MARGIN, OPEN_KEYS, RDX_SELECT_POSITIONER_TOKEN, RdxSelectBackdrop, RdxSelectGroup, RdxSelectGroupLabel, RdxSelectIcon, RdxSelectItem, RdxSelectItemIndicator, RdxSelectItemText, RdxSelectList, RdxSelectPopup, RdxSelectPortal, RdxSelectPortalPresence, RdxSelectPositioner, RdxSelectPositionerContent, RdxSelectRoot, RdxSelectSeparator, RdxSelectTrigger, RdxSelectValue, SELECTION_KEYS, _importsSelect, compare, focusFirst, injectSelectItemContext, injectSelectPopupContext, injectSelectRootContext, provideSelectItemContext, provideSelectPopupContext, provideSelectRootContext, shouldShowPlaceholder, valueComparator };
1164
+ export { CONTENT_MARGIN, OPEN_KEYS, RDX_SELECT_POSITIONER_TOKEN, RdxSelectBackdrop, RdxSelectGroup, RdxSelectGroupLabel, RdxSelectIcon, RdxSelectItem, RdxSelectItemIndicator, RdxSelectItemText, RdxSelectList, RdxSelectPopup, RdxSelectPortal, RdxSelectPortalMisuseGuard, RdxSelectPositioner, RdxSelectRoot, RdxSelectSeparator, RdxSelectTrigger, RdxSelectValue, SELECTION_KEYS, _importsSelect, compare, focusFirst, injectSelectItemContext, injectSelectPopupContext, injectSelectRootContext, provideSelectItemContext, provideSelectPopupContext, provideSelectRootContext, shouldShowPlaceholder, valueComparator };
1096
1165
  //# sourceMappingURL=radix-ng-primitives-select.mjs.map