@radix-ng/primitives 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +26 -5
  2. package/fesm2022/radix-ng-primitives-autocomplete.mjs +35 -19
  3. package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -1
  4. package/fesm2022/radix-ng-primitives-checkbox.mjs +55 -7
  5. package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
  6. package/fesm2022/radix-ng-primitives-combobox.mjs +46 -21
  7. package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
  8. package/fesm2022/radix-ng-primitives-composite.mjs +221 -36
  9. package/fesm2022/radix-ng-primitives-composite.mjs.map +1 -1
  10. package/fesm2022/radix-ng-primitives-core.mjs +189 -26
  11. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  12. package/fesm2022/radix-ng-primitives-date-field.mjs +29 -13
  13. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  14. package/fesm2022/radix-ng-primitives-field.mjs +127 -23
  15. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
  16. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +9 -0
  17. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -1
  18. package/fesm2022/radix-ng-primitives-focus-scope.mjs +241 -95
  19. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  20. package/fesm2022/radix-ng-primitives-form.mjs +35 -4
  21. package/fesm2022/radix-ng-primitives-form.mjs.map +1 -1
  22. package/fesm2022/radix-ng-primitives-input.mjs +87 -15
  23. package/fesm2022/radix-ng-primitives-input.mjs.map +1 -1
  24. package/fesm2022/radix-ng-primitives-menu.mjs +131 -16
  25. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  26. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +17 -3
  27. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  28. package/fesm2022/radix-ng-primitives-popper.mjs +223 -29
  29. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  30. package/fesm2022/radix-ng-primitives-portal.mjs +16 -5
  31. package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
  32. package/fesm2022/radix-ng-primitives-scroll-area.mjs +6 -3
  33. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  34. package/fesm2022/radix-ng-primitives-select.mjs +49 -17
  35. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  36. package/fesm2022/radix-ng-primitives-signal-forms.mjs +27 -11
  37. package/fesm2022/radix-ng-primitives-signal-forms.mjs.map +1 -1
  38. package/fesm2022/radix-ng-primitives-time-field.mjs +24 -10
  39. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  40. package/package.json +8 -7
  41. package/types/radix-ng-primitives-autocomplete.d.ts +7 -2
  42. package/types/radix-ng-primitives-checkbox.d.ts +10 -1
  43. package/types/radix-ng-primitives-combobox.d.ts +29 -2
  44. package/types/radix-ng-primitives-composite.d.ts +48 -2
  45. package/types/radix-ng-primitives-core.d.ts +165 -104
  46. package/types/radix-ng-primitives-date-field.d.ts +21 -8
  47. package/types/radix-ng-primitives-field.d.ts +55 -8
  48. package/types/radix-ng-primitives-floating-focus-manager.d.ts +7 -0
  49. package/types/radix-ng-primitives-focus-scope.d.ts +61 -44
  50. package/types/radix-ng-primitives-form.d.ts +9 -0
  51. package/types/radix-ng-primitives-input.d.ts +10 -2
  52. package/types/radix-ng-primitives-menu.d.ts +59 -7
  53. package/types/radix-ng-primitives-navigation-menu.d.ts +2 -2
  54. package/types/radix-ng-primitives-popover.d.ts +2 -2
  55. package/types/radix-ng-primitives-popper.d.ts +186 -48
  56. package/types/radix-ng-primitives-portal.d.ts +18 -8
  57. package/types/radix-ng-primitives-preview-card.d.ts +2 -2
  58. package/types/radix-ng-primitives-select.d.ts +17 -3
  59. package/types/radix-ng-primitives-signal-forms.d.ts +22 -8
  60. package/types/radix-ng-primitives-time-field.d.ts +21 -10
  61. package/types/radix-ng-primitives-tooltip.d.ts +2 -2
  62. package/fesm2022/radix-ng-primitives-focus-guards.mjs +0 -53
  63. package/fesm2022/radix-ng-primitives-focus-guards.mjs.map +0 -1
  64. package/focus-guards/README.md +0 -1
  65. package/types/radix-ng-primitives-focus-guards.d.ts +0 -15
@@ -25,7 +25,10 @@ declare function createAriaOwnsAnchor(ownerDocument: Document, portalId: string)
25
25
  * trigger steps onto the guard instead of jumping into the content.
26
26
  */
27
27
  declare function disableFocusInside(container: HTMLElement): void;
28
- /** Restores the tabbability that {@link disableFocusInside} suspended. Base UI `enableFocusInside`. */
28
+ /**
29
+ * Restores the tabbability that {@link disableFocusInside} suspended. Base UI `enableFocusInside`.
30
+ * Collects markers across the composed tree so guards inside shadow roots / slots are restored too.
31
+ */
29
32
  declare function enableFocusInside(container: HTMLElement): void;
30
33
  /**
31
34
  * Whether a focus event crossed the `container` boundary — its `relatedTarget` (the other side of the
@@ -134,6 +137,54 @@ type RdxFocusScopeConfig = {
134
137
  declare const RdxFocusScopeConfigToken: InjectionToken<RdxFocusScopeConfig>;
135
138
  declare function provideRdxFocusScopeConfig(factory: () => RdxFocusScopeConfig): Provider;
136
139
 
140
+ /**
141
+ * Tabbable / focusable detection ported from Base UI (`floating-ui-react/utils/tabbable.ts`). Unlike the
142
+ * old `TreeWalker` approximation this:
143
+ * - **pierces shadow DOM and `<slot>`** via composed traversal (`getComposedChildren`), so guards / portal
144
+ * tab-order see candidates inside web components and projected content;
145
+ * - honours **`inert`** ancestors, **`hidden`**, and computed-style visibility (`display`/`visibility`/
146
+ * `checkVisibility()` content-visibility);
147
+ * - treats **`<details>`/`<summary>`**, **`contenteditable`**, and **`audio`/`video[controls]`** as
148
+ * focusable with the right implicit tab index;
149
+ * - applies **radio-group** semantics (only the checked radio of a named group — or the first when none is
150
+ * checked — is tabbable).
151
+ *
152
+ * `composedContains` is the shadow-aware `Node.contains`, kept here as the low-level primitive so this
153
+ * module stays a leaf (only `@radix-ng/primitives/core`); `utils.ts` re-exports it for back-compat.
154
+ */
155
+ type FocusableElement = HTMLElement | SVGElement;
156
+ /**
157
+ * Shadow-DOM-aware containment: whether `node` is `container` or lives inside it, crossing shadow roots
158
+ * via their `host` (unlike `Node.contains`, which stops at a shadow boundary). Base UI `contains`.
159
+ */
160
+ declare function composedContains(container: Node, node: Node | null): boolean;
161
+ /** Collects elements matching `selector` across the composed tree (shadow / slot aware). */
162
+ declare function queryComposedAll(container: ParentNode, selector: string): HTMLElement[];
163
+ /** Whether `element` is reachable by Tab right now (Base UI `isTabbable`). */
164
+ declare function isTabbable(element: Element | null): boolean;
165
+ /** All programmatically focusable elements inside `container`, in composed document order. */
166
+ declare function focusable(container: Element): FocusableElement[];
167
+ /** All keyboard-tabbable elements inside `container`, in composed document order (Base UI `tabbable`). */
168
+ declare function tabbable(container: Element): FocusableElement[];
169
+ /**
170
+ * Back-compat alias for {@link tabbable} — historically returned a looser `TreeWalker` approximation;
171
+ * now the full Base UI tabbable set. Kept so existing imports (`getTabbableCandidates`) keep working.
172
+ */
173
+ declare function getTabbableCandidates(container: Element): FocusableElement[];
174
+ /** The first and last tabbable elements inside `container`. */
175
+ declare function getTabbableEdges(container: Element): readonly [FocusableElement | undefined, FocusableElement | undefined];
176
+ /**
177
+ * The next tabbable in the document after the current focus (Base UI `getNextTabbable`) — used by the
178
+ * portal-focus bridge's trailing guard to step focus past the popup. Falls back to `reference`.
179
+ */
180
+ declare function getNextTabbable(reference: Element | null): FocusableElement | null;
181
+ /** The previous tabbable in the document before the current focus (Base UI `getPreviousTabbable`). */
182
+ declare function getPreviousTabbable(reference: Element | null): FocusableElement | null;
183
+ /** The tabbable immediately after `reference` in the document, wrapping (Base UI `getTabbableAfterElement`). */
184
+ declare function getTabbableAfterElement(reference: Element | null): FocusableElement | null;
185
+ /** The tabbable immediately before `reference` in the document, wrapping (Base UI `getTabbableBeforeElement`). */
186
+ declare function getTabbableBeforeElement(reference: Element | null): FocusableElement | null;
187
+
137
188
  declare const AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount";
138
189
  declare const AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount";
139
190
  declare const EVENT_OPTIONS: {
@@ -148,52 +199,18 @@ type FocusableTarget = HTMLElement | {
148
199
  * Falls back to `event.target` when `composedPath` is unavailable.
149
200
  */
150
201
  declare function getEventTarget(event: Event): EventTarget | null;
151
- /**
152
- * Shadow-DOM-aware containment: whether `node` is `container` or lives inside it, crossing shadow roots
153
- * via their `host` (unlike `Node.contains`, which stops at a shadow boundary).
154
- */
155
- declare function composedContains(container: Node, node: Node | null): boolean;
156
202
  /**
157
203
  * Attempts focusing the first element in a list of candidates.
158
204
  * Stops when focus has actually moved.
159
- */
160
- declare function focusFirst(candidates: HTMLElement[], { select }?: {
161
- select?: boolean | undefined;
162
- }): true | undefined;
163
- /**
164
- * Returns a list of potential tabbable candidates.
165
- *
166
- * NOTE: This is only a close approximation. For example it doesn't take into account cases like when
167
- * elements are not visible. This cannot be worked out easily by just reading a property, but rather
168
- * necessitate runtime knowledge (computed styles, etc). We deal with these cases separately.
169
205
  *
170
- * See: https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
171
- * Credit: https://github.com/discord/focus-layers/blob/master/src/util/wrapFocus.tsx#L1
172
- */
173
- declare function getTabbableCandidates(container: HTMLElement): HTMLElement[];
174
- declare function isHidden(node: HTMLElement, { upTo }: {
175
- upTo?: HTMLElement;
176
- }): boolean;
177
- /**
178
- * Returns the first visible element in a list.
179
- * NOTE: Only checks visibility up to the `container`.
206
+ * Reads the active element from `root` (a focus scope passes its host's `ownerDocument`); falling back to
207
+ * the global `document` would, in an iframe / multi-document scope, never detect that focus moved inside
208
+ * the inner document and so walk past every candidate, leaving focus on the last one instead of the first.
180
209
  */
181
- declare function findVisible(elements: HTMLElement[], container: HTMLElement): HTMLElement | undefined;
182
- /**
183
- * Returns the first and last tabbable elements inside a container.
184
- */
185
- declare function getTabbableEdges(container: HTMLElement): readonly [HTMLElement | undefined, HTMLElement | undefined];
186
- /**
187
- * The next tabbable in the document after the current focus (Base UI `getNextTabbable`) — used by the
188
- * portal-focus bridge's trailing guard to step focus past the popup. Falls back to `reference`.
189
- */
190
- declare function getNextTabbable(reference: Element | null): HTMLElement | null;
191
- /** The previous tabbable in the document before the current focus (Base UI `getPreviousTabbable`). */
192
- declare function getPreviousTabbable(reference: Element | null): HTMLElement | null;
193
- /** The tabbable immediately after `reference` in the document, wrapping (Base UI `getTabbableAfterElement`). */
194
- declare function getTabbableAfterElement(reference: Element | null): HTMLElement | null;
195
- /** The tabbable immediately before `reference` in the document, wrapping (Base UI `getTabbableBeforeElement`). */
196
- declare function getTabbableBeforeElement(reference: Element | null): HTMLElement | null;
210
+ declare function focusFirst(candidates: FocusableElement[], { select, root }?: {
211
+ select?: boolean;
212
+ root?: DocumentOrShadowRoot;
213
+ }): true | undefined;
197
214
  declare function isSelectableInput(element: any): element is FocusableTarget & {
198
215
  select: () => void;
199
216
  };
@@ -201,5 +218,5 @@ declare function focus(element?: FocusableTarget | null, { select }?: {
201
218
  select?: boolean | undefined;
202
219
  }): void;
203
220
 
204
- export { AUTOFOCUS_ON_MOUNT, AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS, FOCUS_GUARD_ATTR, FOCUS_GUARD_STYLE, RdxFocusScope, RdxFocusScopeConfigToken, composedContains, createAriaOwnsAnchor, createFocusGuard, disableFocusInside, enableFocusInside, findVisible, focus, focusFirst, getEventTarget, getNextTabbable, getPreviousTabbable, getTabbableAfterElement, getTabbableBeforeElement, getTabbableCandidates, getTabbableEdges, injectFocusScopeContext, isHidden, isOutsideEvent, isSelectableInput, provideFocusScopeContext, provideRdxFocusScopeConfig, useFocusGuardsTabbability };
205
- export type { FocusScopeContext, RdxFocusScopeConfig };
221
+ export { AUTOFOCUS_ON_MOUNT, AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS, FOCUS_GUARD_ATTR, FOCUS_GUARD_STYLE, RdxFocusScope, RdxFocusScopeConfigToken, composedContains, createAriaOwnsAnchor, createFocusGuard, disableFocusInside, enableFocusInside, focus, focusFirst, focusable, getEventTarget, getNextTabbable, getPreviousTabbable, getTabbableAfterElement, getTabbableBeforeElement, getTabbableCandidates, getTabbableEdges, injectFocusScopeContext, isOutsideEvent, isSelectableInput, isTabbable, provideFocusScopeContext, provideRdxFocusScopeConfig, queryComposedAll, tabbable, useFocusGuardsTabbability };
222
+ export type { FocusScopeContext, FocusableElement, RdxFocusScopeConfig };
@@ -49,6 +49,13 @@ interface RdxFormState {
49
49
  dirty?: () => boolean;
50
50
  touched?: () => boolean;
51
51
  submitting?: () => boolean;
52
+ /**
53
+ * Optional Angular-owned submission lifecycle. Returning `undefined` keeps the Form's Base UI-style
54
+ * synchronous submit path; returning a Promise delegates this submit to the adapter. Signal Forms
55
+ * uses this only when its opt-in `rdxSignalSubmit` input is present, and calls Angular's public
56
+ * `submit()` API so validation, touched state, concurrency, and submission errors stay Angular-owned.
57
+ */
58
+ submit?: () => Promise<boolean> | undefined;
52
59
  /**
53
60
  * Per-name **client** validation errors (e.g. `rdxSignalForm`'s Signal Forms name-routing). Surfaced
54
61
  * through the Form's `clientErrorsFor` channel and gated by `validationMode` like any client validity.
@@ -190,6 +197,8 @@ declare class RdxFormRoot {
190
197
  */
191
198
  clearStateProvider(provider: RdxFormState | null, previous: RdxFormState | null): void;
192
199
  onSubmit(event: SubmitEvent): void;
200
+ private finishDelegatedSubmit;
201
+ private focusFirstInvalidField;
193
202
  onReset(): void;
194
203
  private readonly resetTimers;
195
204
  constructor();
@@ -24,6 +24,7 @@ interface RdxInputValueChangeEvent {
24
24
  declare class RdxInputDirective implements RdxFormValueControl<RdxInputValue | undefined> {
25
25
  private readonly element;
26
26
  private readonly fieldRootContext;
27
+ private readonly ngControlState;
27
28
  private initialValue;
28
29
  private defaultValueApplied;
29
30
  private readonly filledValue;
@@ -78,6 +79,8 @@ declare class RdxInputDirective implements RdxFormValueControl<RdxInputValue | u
78
79
  * @group Props
79
80
  */
80
81
  readonly invalid: _angular_core.InputSignalWithTransform<boolean, BooleanInput>;
82
+ /** Whether async validation is pending. Pending inputs publish neither valid nor invalid state. */
83
+ readonly pending: _angular_core.InputSignalWithTransform<boolean, BooleanInput>;
81
84
  /**
82
85
  * Whether the input has been touched. A two-way model: the input sets it on
83
86
  * blur (emitting `touchedChange`, which Signal Forms' `[formField]` listens
@@ -133,8 +136,11 @@ declare class RdxInputDirective implements RdxFormValueControl<RdxInputValue | u
133
136
  * @group Emits
134
137
  */
135
138
  readonly touch: _angular_core.OutputEmitterRef<void>;
136
- /** The input's own binary invalidity (its `invalid` input or a non-empty `errors` list). */
139
+ /** Validation errors from the input and a same-host Reactive/template-driven form control. */
140
+ readonly validationErrors: _angular_core.Signal<readonly RdxValidationError[]>;
141
+ /** The input's binary invalidity, including a same-host Reactive/template-driven form control. */
137
142
  private readonly ownInvalid;
143
+ private readonly pendingState;
138
144
  /**
139
145
  * Tri-state *displayed* validity: inside a `rdxFieldRoot` the field's gated `validState` is the single
140
146
  * source (so a field whose `validationMode` defers display (e.g. `onBlur`) keeps the input neutral until revealed), otherwise
@@ -153,11 +159,13 @@ declare class RdxInputDirective implements RdxFormValueControl<RdxInputValue | u
153
159
  onFocus(): void;
154
160
  onBlur(): void;
155
161
  onInput(event: Event): void;
162
+ /** Reset the interaction baseline; Angular Signal Forms calls this from `FieldState.reset()`. */
163
+ reset(): void;
156
164
  syncFieldState(): void;
157
165
  private writeValue;
158
166
  protected readonly dataAttr: (value: boolean) => "" | undefined;
159
167
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxInputDirective, never>;
160
- static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxInputDirective, "input[rdxInput]", ["rdxInput"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "defaultValue": { "alias": "defaultValue"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "invalid": { "alias": "invalid"; "required": false; "isSignal": true; }; "touched": { "alias": "touched"; "required": false; "isSignal": true; }; "dirty": { "alias": "dirty"; "required": false; "isSignal": true; }; "errors": { "alias": "errors"; "required": false; "isSignal": true; }; "minLength": { "alias": "minLength"; "required": false; "isSignal": true; }; "maxLength": { "alias": "maxLength"; "required": false; "isSignal": true; }; "pattern": { "alias": "pattern"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "touched": "touchedChange"; "onValueChange": "onValueChange"; "touch": "touch"; }, never, never, true, never>;
168
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxInputDirective, "input[rdxInput]", ["rdxInput"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "defaultValue": { "alias": "defaultValue"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "invalid": { "alias": "invalid"; "required": false; "isSignal": true; }; "pending": { "alias": "pending"; "required": false; "isSignal": true; }; "touched": { "alias": "touched"; "required": false; "isSignal": true; }; "dirty": { "alias": "dirty"; "required": false; "isSignal": true; }; "errors": { "alias": "errors"; "required": false; "isSignal": true; }; "minLength": { "alias": "minLength"; "required": false; "isSignal": true; }; "maxLength": { "alias": "maxLength"; "required": false; "isSignal": true; }; "pattern": { "alias": "pattern"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "touched": "touchedChange"; "onValueChange": "onValueChange"; "touch": "touch"; }, never, never, true, never>;
161
169
  }
162
170
 
163
171
  export { RdxInputDirective };
@@ -11,6 +11,29 @@ import * as i1$1 from '@radix-ng/primitives/composite';
11
11
  import * as i1$2 from '@radix-ng/primitives/portal';
12
12
  import { RdxOutsidePressDomEvent } from '@radix-ng/primitives/dismissable-layer';
13
13
 
14
+ /**
15
+ * Submenu "safe polygon" — a faithful port of Floating UI's `safePolygon` algorithm
16
+ * (https://floating-ui.com/docs/useHover#safepolygon), adapted to this library.
17
+ *
18
+ * While a submenu is open by hover, the parent submenu owns the decision to close itself: a
19
+ * document-level `mousemove` handler keeps it open as long as the cursor is heading toward the
20
+ * popup inside a safe quadrilateral (built from the cursor's exit point and the popup rect), and
21
+ * closes it once the cursor leaves that area. Combined with the pointer-events "tunnel" below
22
+ * (`applyPointerTunnel`), siblings cannot steal the open submenu during a diagonal traversal.
23
+ *
24
+ * Differences from the upstream implementation:
25
+ * - `elements.domReference` / `elements.floating` → `reference` / `floating` (plain elements).
26
+ * - `placement.split('-')[0]` → the `side` option (read live from the popup's `data-side`).
27
+ * - the Floating UI tree (`tree` / `nodeId`) → the `hasOpenChild` callback, backed by the
28
+ * module-level open-submenu registry.
29
+ *
30
+ * This deliberately does NOT reuse the core `useGraceArea` composable (tooltip / navigation-menu /
31
+ * popover): that one is a simpler convex-hull grace area with no velocity gating, trough handling, or
32
+ * pointer-events tunnel — none of which it needs, but all of which the submenu does to match Base UI
33
+ * and to stop sibling triggers from stealing the open submenu mid-traversal.
34
+ */
35
+ type MenuSide = 'top' | 'bottom' | 'left' | 'right';
36
+
14
37
  type RdxMenuTransitionStatus = 'starting' | 'ending' | undefined;
15
38
  /**
16
39
  * Why an open/close happened instantly (no transition). Mirrors Base UI's menu `data-instant`:
@@ -81,6 +104,12 @@ interface RdxMenuRootContext {
81
104
  trigger: Signal<HTMLElement | undefined>;
82
105
  /** The popup element, once mounted. Used by submenu safe-polygon geometry. */
83
106
  popupElement: Signal<HTMLElement | undefined>;
107
+ /**
108
+ * The popup's **physical** placed side (`top`/`bottom`/`left`/`right`) for safe-polygon geometry —
109
+ * never the popup's `data-side`, which is the logical `inline-start`/`inline-end` echo when a logical
110
+ * side was requested and which the geometry cannot interpret.
111
+ */
112
+ popupPhysicalSide: Signal<MenuSide | undefined>;
84
113
  beforeContentFocusGuard: Signal<HTMLElement | null>;
85
114
  transitionStatus: Signal<RdxMenuTransitionStatus>;
86
115
  close: (reason?: RdxMenuOpenChangeReason, event?: Event) => void;
@@ -92,7 +121,7 @@ interface RdxMenuRootContext {
92
121
  /** Open the menu without moving focus into the popup (used for menubar hover-switching). */
93
122
  showWithoutAutoFocus: (reason?: RdxMenuOpenChangeReason, event?: Event) => void;
94
123
  registerTrigger: (el: HTMLElement) => () => void;
95
- registerPopup: (el: HTMLElement) => () => void;
124
+ registerPopup: (el: HTMLElement, physicalSide?: Signal<MenuSide | undefined>) => () => void;
96
125
  setBeforeContentFocusGuard: (element: HTMLElement | null) => void;
97
126
  registerTransitionElement: (element: HTMLElement) => () => void;
98
127
  registerPopupArrowNavigationHandler: (handler: (offset: 1 | -1) => boolean) => () => void;
@@ -195,6 +224,9 @@ declare class RdxMenuRoot {
195
224
  readonly onOpenChangeComplete: _angular_core.OutputEmitterRef<boolean>;
196
225
  readonly trigger: _angular_core.WritableSignal<HTMLElement | undefined>;
197
226
  readonly popupElement: _angular_core.WritableSignal<HTMLElement | undefined>;
227
+ private readonly popupPhysicalSideSource;
228
+ /** Physical placed side read through the popup's own signal (undefined until the popup is placed). */
229
+ readonly popupPhysicalSide: Signal<MenuSide | undefined>;
198
230
  readonly beforeContentFocusGuard: _angular_core.WritableSignal<HTMLElement | null>;
199
231
  readonly transitionStatus: Signal<_radix_ng_primitives_core.RdxTransitionStatus>;
200
232
  readonly activeIndex: _angular_core.WritableSignal<number | null>;
@@ -239,7 +271,7 @@ declare class RdxMenuRoot {
239
271
  */
240
272
  closeEntireMenu(reason?: RdxMenuOpenChangeReason, event?: Event): void;
241
273
  registerTrigger(el: HTMLElement): () => void;
242
- registerPopup(el: HTMLElement): () => void;
274
+ registerPopup(el: HTMLElement, physicalSide?: Signal<MenuSide | undefined>): () => void;
243
275
  setBeforeContentFocusGuard(element: HTMLElement | null): void;
244
276
  registerTransitionElement(element: HTMLElement): () => void;
245
277
  registerPopupArrowNavigationHandler(handler: (offset: 1 | -1) => boolean): () => void;
@@ -359,6 +391,15 @@ declare class RdxMenuSubTrigger {
359
391
  protected onArrowLeft(event: Event): void;
360
392
  protected onPointerMove(event: PointerEvent): void;
361
393
  protected onPointerLeave(): void;
394
+ /**
395
+ * Backup cancellation for a `pointerleave` that Chrome can drop during a fast pointer sweep across
396
+ * adjacent triggers, which would otherwise leave the delayed hover-open armed and open a stale
397
+ * submenu for a trigger the pointer has already left (stranding the parent at `pointer-events: none`).
398
+ * `pointerout` bubbles, so it also fires while crossing between the trigger's own descendants — those
399
+ * (where `relatedTarget` is still inside the trigger) must not cancel the pending open. Base UI parity
400
+ * (`guardStaleOpen`); safe on this single, hover-driven trigger.
401
+ */
402
+ protected onPointerOut(event: PointerEvent): void;
362
403
  protected clearHighlight(): void;
363
404
  private closeSiblingSubmenus;
364
405
  private clearSiblingHighlights;
@@ -376,11 +417,15 @@ declare class RdxMenuSubTrigger {
376
417
  * This replaces the consumer-owned `@if (root.open())` mount: it adds both teleporting *and*
377
418
  * exit-animation support. Apply it with the `*` microsyntax on the positioner —
378
419
  * `<div *rdxMenuPortal rdxMenuPositioner>` — or as an explicit `<ng-template rdxMenuPortal>`. For a
379
- * custom container, or a backdrop alongside the positioner (multi-root), use the explicit form.
420
+ * custom container, a backdrop alongside the positioner (multi-root), or `[keepMounted]`, use the
421
+ * explicit form.
422
+ *
423
+ * Set `[keepMounted]="true"` to keep the popup mounted while the menu is closed (the positioner's
424
+ * `data-closed` state hides it) — inherited from {@link RdxPortalPresence}.
380
425
  */
381
426
  declare class RdxMenuPortal {
382
427
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxMenuPortal, never>;
383
- static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxMenuPortal, "ng-template[rdxMenuPortal]", ["rdxMenuPortal"], {}, {}, never, never, true, [{ directive: typeof i1$2.RdxPortalPresence; inputs: { "container": "container"; }; outputs: {}; }]>;
428
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxMenuPortal, "ng-template[rdxMenuPortal]", ["rdxMenuPortal"], {}, {}, never, never, true, [{ directive: typeof i1$2.RdxPortalPresence; inputs: { "container": "container"; "keepMounted": "keepMounted"; }; outputs: {}; }]>;
384
429
  }
385
430
  /**
386
431
  * Dev-mode guard: `rdxMenuPortal` is a structural directive. The old `<div rdxMenuPortal>` markup
@@ -402,6 +447,13 @@ declare class RdxMenuPortalMisuseGuard {
402
447
  declare class RdxMenuPositioner extends RdxPopperContentWrapper {
403
448
  protected readonly rootContext: _radix_ng_primitives_menu.RdxMenuRootContext;
404
449
  protected readonly legacyVars: Record<string, string>;
450
+ /**
451
+ * Whether the popup is shown: open, or still running its closing transition. Drives both the native
452
+ * `hidden` (finding: a closed keep-mounted popup must hide itself) and — via `positioningActive` —
453
+ * whether the inherited wrapper keeps tracking the anchor, so a closed keep-mounted positioner stops
454
+ * its `autoUpdate` `requestAnimationFrame` loop (finding: endless rAF while closed).
455
+ */
456
+ protected readonly visible: _angular_core.Signal<boolean>;
405
457
  constructor();
406
458
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxMenuPositioner, never>;
407
459
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxMenuPositioner, "[rdxMenuPositioner]", ["rdxMenuPositioner"], {}, {}, never, never, true, never>;
@@ -422,7 +474,7 @@ declare class RdxMenuPopup {
422
474
  private search;
423
475
  private searchTimer;
424
476
  protected readonly align: _angular_core.Signal<"center" | "end" | "start" | undefined>;
425
- protected readonly side: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
477
+ protected readonly side: _angular_core.Signal<i1.SideOrLogical | undefined>;
426
478
  /**
427
479
  * Event handler called when the escape key is pressed. Can be prevented.
428
480
  */
@@ -456,7 +508,7 @@ declare class RdxMenuPopup {
456
508
  private currentItemIndex;
457
509
  private focusMenuItem;
458
510
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxMenuPopup, never>;
459
- static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxMenuPopup, "[rdxMenuPopup]", ["rdxMenuPopup"], {}, { "escapeKeyDown": "escapeKeyDown"; "pointerDownOutside": "pointerDownOutside"; "focusOutside": "focusOutside"; "interactOutside": "interactOutside"; "openAutoFocus": "openAutoFocus"; "closeAutoFocus": "closeAutoFocus"; }, never, never, true, [{ directive: typeof i1.RdxPopperContent; inputs: {}; outputs: {}; }, { directive: typeof _radix_ng_primitives_core.RdxFloatingNodeRegistration; inputs: {}; outputs: {}; }, { directive: typeof _radix_ng_primitives_floating_focus_manager.RdxFloatingFocusManager; inputs: {}; outputs: {}; }, { directive: typeof i1$1.RdxCompositeList; inputs: {}; outputs: {}; }]>;
511
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxMenuPopup, "[rdxMenuPopup]", ["rdxMenuPopup"], {}, { "escapeKeyDown": "escapeKeyDown"; "pointerDownOutside": "pointerDownOutside"; "focusOutside": "focusOutside"; "interactOutside": "interactOutside"; "openAutoFocus": "openAutoFocus"; "closeAutoFocus": "closeAutoFocus"; }, never, never, true, [{ directive: typeof i1.RdxPopperContent; inputs: {}; outputs: {}; }, { directive: typeof _radix_ng_primitives_core.RdxFloatingNodeRegistration; inputs: {}; outputs: {}; }, { directive: typeof _radix_ng_primitives_floating_focus_manager.RdxFloatingFocusManager; inputs: { "returnFocus": "finalFocus"; }; outputs: {}; }, { directive: typeof i1$1.RdxCompositeList; inputs: {}; outputs: {}; }]>;
460
512
  }
461
513
 
462
514
  /**
@@ -509,7 +561,7 @@ declare class RdxMenuBackdrop {
509
561
  declare class RdxMenuArrow {
510
562
  protected readonly rootContext: _radix_ng_primitives_menu.RdxMenuRootContext;
511
563
  private readonly wrapper;
512
- protected readonly side: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
564
+ protected readonly side: _angular_core.Signal<i1.SideOrLogical | undefined>;
513
565
  protected readonly align: _angular_core.Signal<"center" | "end" | "start" | undefined>;
514
566
  protected readonly uncentered: _angular_core.Signal<boolean>;
515
567
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxMenuArrow, never>;
@@ -383,7 +383,7 @@ declare class RdxNavigationMenuPopup {
383
383
  private readonly registration;
384
384
  private readonly wrapper;
385
385
  private readonly elementRef;
386
- protected readonly side: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
386
+ protected readonly side: _angular_core.Signal<i1$1.SideOrLogical | undefined>;
387
387
  protected readonly align: _angular_core.Signal<"start" | "center" | "end" | undefined>;
388
388
  protected readonly id: _angular_core.Signal<string>;
389
389
  /**
@@ -420,7 +420,7 @@ declare class RdxNavigationMenuPopup {
420
420
  declare class RdxNavigationMenuArrow {
421
421
  protected readonly rootContext: _radix_ng_primitives_navigation_menu.RdxNavigationMenuRootContext;
422
422
  private readonly wrapper;
423
- protected readonly side: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
423
+ protected readonly side: _angular_core.Signal<i1$1.SideOrLogical | undefined>;
424
424
  protected readonly align: _angular_core.Signal<"start" | "center" | "end" | undefined>;
425
425
  protected readonly uncentered: _angular_core.Signal<boolean>;
426
426
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxNavigationMenuArrow, never>;
@@ -304,7 +304,7 @@ declare class RdxPopoverPopup {
304
304
  private readonly focusScope;
305
305
  private readonly wrapper;
306
306
  protected readonly align: _angular_core.Signal<"start" | "center" | "end" | undefined>;
307
- protected readonly side: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
307
+ protected readonly side: _angular_core.Signal<i1$1.SideOrLogical | undefined>;
308
308
  /** Event handler called when the escape key is down. Can be prevented. */
309
309
  readonly escapeKeyDown: _angular_core.OutputEmitterRef<KeyboardEvent>;
310
310
  /** Event handler called when a pointerdown event happens outside of the popup. Can be prevented. */
@@ -328,7 +328,7 @@ declare class RdxPopoverPopup {
328
328
  declare class RdxPopoverArrow {
329
329
  protected readonly rootContext: _radix_ng_primitives_popover.RdxPopoverRootContext;
330
330
  private readonly wrapper;
331
- protected readonly side: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
331
+ protected readonly side: _angular_core.Signal<i1$1.SideOrLogical | undefined>;
332
332
  protected readonly align: _angular_core.Signal<"start" | "center" | "end" | undefined>;
333
333
  protected readonly uncentered: _angular_core.Signal<boolean>;
334
334
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxPopoverArrow, never>;