@radix-ng/primitives 1.0.10 → 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 (50) hide show
  1. package/README.md +26 -5
  2. package/fesm2022/radix-ng-primitives-autocomplete.mjs +24 -16
  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 +35 -18
  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 +181 -21
  11. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  12. package/fesm2022/radix-ng-primitives-date-field.mjs +11 -5
  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-form.mjs +35 -4
  17. package/fesm2022/radix-ng-primitives-form.mjs.map +1 -1
  18. package/fesm2022/radix-ng-primitives-input.mjs +87 -15
  19. package/fesm2022/radix-ng-primitives-input.mjs.map +1 -1
  20. package/fesm2022/radix-ng-primitives-menu.mjs +34 -7
  21. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  22. package/fesm2022/radix-ng-primitives-popper.mjs +80 -11
  23. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  24. package/fesm2022/radix-ng-primitives-scroll-area.mjs +6 -3
  25. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  26. package/fesm2022/radix-ng-primitives-select.mjs +36 -12
  27. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  28. package/fesm2022/radix-ng-primitives-signal-forms.mjs +27 -11
  29. package/fesm2022/radix-ng-primitives-signal-forms.mjs.map +1 -1
  30. package/fesm2022/radix-ng-primitives-time-field.mjs +12 -6
  31. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  32. package/package.json +8 -3
  33. package/types/radix-ng-primitives-autocomplete.d.ts +7 -2
  34. package/types/radix-ng-primitives-checkbox.d.ts +10 -1
  35. package/types/radix-ng-primitives-combobox.d.ts +29 -2
  36. package/types/radix-ng-primitives-composite.d.ts +48 -2
  37. package/types/radix-ng-primitives-core.d.ts +152 -94
  38. package/types/radix-ng-primitives-date-field.d.ts +7 -4
  39. package/types/radix-ng-primitives-field.d.ts +55 -8
  40. package/types/radix-ng-primitives-form.d.ts +9 -0
  41. package/types/radix-ng-primitives-input.d.ts +10 -2
  42. package/types/radix-ng-primitives-menu.d.ts +45 -4
  43. package/types/radix-ng-primitives-navigation-menu.d.ts +2 -2
  44. package/types/radix-ng-primitives-popover.d.ts +2 -2
  45. package/types/radix-ng-primitives-popper.d.ts +60 -15
  46. package/types/radix-ng-primitives-preview-card.d.ts +2 -2
  47. package/types/radix-ng-primitives-select.d.ts +17 -3
  48. package/types/radix-ng-primitives-signal-forms.d.ts +22 -8
  49. package/types/radix-ng-primitives-time-field.d.ts +9 -6
  50. package/types/radix-ng-primitives-tooltip.d.ts +2 -2
@@ -4,11 +4,12 @@ import * as _radix_ng_primitives_core from '@radix-ng/primitives/core';
4
4
  import { RdxFormUiControlBase, RdxFormValueControl, DateMatcher, HourCycle, Granularity, Direction, BooleanInput, DateStep, Formatter, SegmentValueObj, SegmentPart } from '@radix-ng/primitives/core';
5
5
  import { DateValue } from '@internationalized/date';
6
6
 
7
- declare class RdxDateFieldRootDirective extends RdxFormUiControlBase implements RdxFormValueControl<DateValue | undefined> {
7
+ declare class RdxDateFieldRootDirective extends RdxFormUiControlBase implements RdxFormValueControl<DateValue | null> {
8
8
  /**
9
- * The controlled value of the date field.
9
+ * The controlled value of the date field. `null` represents an empty field and keeps the value
10
+ * addressable as an Angular Signal Forms child field (`undefined` denotes an absent optional path).
10
11
  */
11
- readonly value: _angular_core.ModelSignal<DateValue | undefined>;
12
+ readonly value: _angular_core.ModelSignal<DateValue | null>;
12
13
  /**
13
14
  * A matcher that marks specific dates as unavailable; a matched value makes the field invalid.
14
15
  */
@@ -122,6 +123,8 @@ declare class RdxDateFieldRootDirective extends RdxFormUiControlBase implements
122
123
  private readonly userInteracted;
123
124
  /** @ignore Mark the field touched (model + `touch` output) for Signal Forms. Called on segment blur. */
124
125
  markAsTouched(): void;
126
+ /** Reset segment interaction so the form-owned value write remains pristine. */
127
+ reset(): void;
125
128
  /**
126
129
  * @ignore
127
130
  */
@@ -235,7 +238,7 @@ declare class RdxDateFieldInputDirective {
235
238
 
236
239
  interface DateFieldContextToken {
237
240
  locale: InputSignal<string>;
238
- value: ModelSignal<DateValue | undefined>;
241
+ value: ModelSignal<DateValue | null>;
239
242
  disabled: Signal<boolean>;
240
243
  readonly: Signal<boolean>;
241
244
  isInvalid: Signal<boolean>;
@@ -63,6 +63,15 @@ declare class RdxFieldError {
63
63
  * @group Props
64
64
  */
65
65
  readonly id: _angular_core.InputSignal<string>;
66
+ /**
67
+ * Shows this error only for a matching validation-error key. Bind `true` to keep it visible under
68
+ * external control; omit it for the field's default error behavior.
69
+ *
70
+ * @group Props
71
+ */
72
+ readonly match: _angular_core.InputSignal<string | boolean | undefined>;
73
+ /** Whether this error part is currently presented. */
74
+ readonly visible: _angular_core.Signal<boolean>;
66
75
  /**
67
76
  * The field's validation messages — client (provider / form name-routing, once `validationMode`
68
77
  * reveals them) then server (the Form's `errors` input, always); `[]` when none. Render them
@@ -73,12 +82,11 @@ declare class RdxFieldError {
73
82
  constructor();
74
83
  protected readonly dataAttr: (value: boolean) => "" | undefined;
75
84
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxFieldError, never>;
76
- static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxFieldError, "[rdxFieldError]", ["rdxFieldError"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
85
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxFieldError, "[rdxFieldError]", ["rdxFieldError"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "match": { "alias": "match"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
77
86
  }
78
87
 
79
88
  /**
80
- * External owner of field state. An adapter (e.g. a future Signal Forms
81
- * `[rdxSignalField]` directive, or a Reactive Forms bridge) registers one via
89
+ * External owner of field state. An adapter (`rdxSignalField` or `rdxNgControlField`) registers one via
82
90
  * `setStateProvider` so Field reads authoritative form state instead of
83
91
  * self-computing it from the DOM. Each member is an optional signal-like
84
92
  * accessor; only the states the adapter owns need to be provided — the rest
@@ -86,15 +94,19 @@ declare class RdxFieldError {
86
94
  * `() => boolean` accessors keeps Field framework-agnostic (no dependency on
87
95
  * `@angular/forms/signals`).
88
96
  *
89
- * See ADR 0004 and `signal-forms-readiness.md` (prep #4).
97
+ * See ADR 0004, ADR 0021, and `signal-forms-readiness.md` (prep #4).
90
98
  */
91
99
  interface RdxFieldState {
100
+ /** Field name inferred by an adapter. An explicit `rdxFieldRoot[name]` always wins. */
101
+ name?: () => string | undefined;
92
102
  /**
93
103
  * The control's **actual** invalidity (ungated). The Field decides *when* to display it from its
94
104
  * `validationMode` (e.g. only after blur) — the adapter just reports the real state. A non-empty
95
105
  * `errors()` also counts as invalid.
96
106
  */
97
107
  invalid?: () => boolean;
108
+ /** Whether async validation is pending. Pending validity remains neutral, never falsely valid. */
109
+ pending?: () => boolean;
98
110
  disabled?: () => boolean;
99
111
  required?: () => boolean;
100
112
  dirty?: () => boolean;
@@ -108,7 +120,7 @@ interface RdxFieldState {
108
120
  * any enclosing Form's server messages. Uses `core`'s framework-free shim type so the seam stays free of
109
121
  * `@angular/forms/signals` (ADR 0004 amendment).
110
122
  */
111
- errors?: () => RdxValidationError[];
123
+ errors?: () => readonly RdxValidationError[];
112
124
  }
113
125
  interface RdxFieldRootContext {
114
126
  controlId: Signal<string>;
@@ -116,6 +128,8 @@ interface RdxFieldRootContext {
116
128
  descriptionIds: Signal<string[]>;
117
129
  errorIds: Signal<string[]>;
118
130
  messages: Signal<string[]>;
131
+ /** Whether the currently displayed client errors contain this validation kind. */
132
+ matchesError: (kind: string) => boolean;
119
133
  notifyEdited: () => void;
120
134
  /** Tri-state *displayed* validity (`true` valid / `false` invalid / `null` neutral), gated by the
121
135
  * field's `validationMode`. The source for `data-valid` / `data-invalid` on the field and its controls. */
@@ -229,6 +243,8 @@ declare class RdxFieldRoot {
229
243
  private readonly stateProvider;
230
244
  /** Whether an external adapter currently owns field state. */
231
245
  readonly hasStateProvider: Signal<boolean>;
246
+ /** Explicit field name wins; an adapter may infer one from its form-control binding. */
247
+ readonly effectiveName: Signal<string | undefined>;
232
248
  /** Error content from a registered state provider (e.g. a Signal Forms adapter). */
233
249
  private readonly providerErrors;
234
250
  /** **Client** validation errors routed by a form-level provider (`rdxSignalForm`) — gated by `validationMode`. */
@@ -246,6 +262,8 @@ declare class RdxFieldRoot {
246
262
  * field state), so deduping renders it once while still surfacing distinct messages from each source.
247
263
  */
248
264
  readonly messages: Signal<string[]>;
265
+ /** Whether a client validation error of `kind` is currently revealed by the field. */
266
+ matchesError(kind: string): boolean;
249
267
  /** Effective validation-display mode: this field's override → the enclosing Form's → the default. */
250
268
  readonly effectiveValidationMode: Signal<RdxValidationMode>;
251
269
  /** Whether client-side validity is revealed yet, per {@link effectiveValidationMode} + interaction. */
@@ -276,6 +294,7 @@ declare class RdxFieldRoot {
276
294
  * the provider/input `invalid`.
277
295
  */
278
296
  readonly actualInvalidState: Signal<boolean>;
297
+ readonly pendingState: Signal<boolean>;
279
298
  readonly disabledState: Signal<boolean>;
280
299
  readonly requiredState: Signal<boolean>;
281
300
  readonly dirtyState: Signal<boolean>;
@@ -310,8 +329,8 @@ declare class RdxFieldRoot {
310
329
  clearStateProvider(provider: RdxFieldState | null, previous: RdxFieldState | null): void;
311
330
  /**
312
331
  * Prefer the registered provider's value for `key` when it exposes one,
313
- * otherwise fall back to the root inputs / DOM-derived signals. `errors` (content) and `valid`
314
- * (tri-state `boolean | null`) are resolved separately, so they're excluded from this boolean key.
332
+ * otherwise fall back to the root inputs / DOM-derived signals. `name` and `errors` are resolved
333
+ * separately, so they're excluded from this boolean key.
315
334
  */
316
335
  private resolve;
317
336
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxFieldRoot, never>;
@@ -369,5 +388,33 @@ declare class RdxFieldLabel {
369
388
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxFieldLabel, "[rdxFieldLabel]", ["rdxFieldLabel"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
370
389
  }
371
390
 
372
- export { RdxFieldControl, RdxFieldDescription, RdxFieldError, RdxFieldItem, RdxFieldLabel, RdxFieldRoot, injectFieldRootContext, provideFieldRootContext };
391
+ /**
392
+ * Bridges a same-host Reactive Forms or template-driven `NgControl` into an enclosing `rdxFieldRoot`.
393
+ * Place it next to `formControl`, `formControlName`, or `ngModel`; Angular remains the source of value,
394
+ * validation, and interaction state while Field owns when that state is displayed.
395
+ *
396
+ * ```html
397
+ * <div rdxFieldRoot>
398
+ * <label rdxFieldLabel>Email</label>
399
+ * <input formControlName="email" rdxFieldControl rdxNgControlField />
400
+ * <p match="required" rdxFieldError>Email is required.</p>
401
+ * </div>
402
+ * ```
403
+ *
404
+ * The adapter also infers the Field name from `formControlName` / `ngModel` for Form-level server-error
405
+ * routing. An explicit `name` on `rdxFieldRoot` takes precedence (useful for nested or remapped keys).
406
+ *
407
+ * @group Components
408
+ */
409
+ declare class RdxNgControlField {
410
+ private readonly ngControlState;
411
+ private readonly fieldContext;
412
+ /** Normalized Angular validation errors (`{ kind, message? }[]`). */
413
+ readonly validationErrors: _angular_core.Signal<readonly _radix_ng_primitives_core.RdxValidationError[]>;
414
+ constructor();
415
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxNgControlField, never>;
416
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxNgControlField, "[formControl][rdxNgControlField], [formControlName][rdxNgControlField], [ngModel][rdxNgControlField]", ["rdxNgControlField"], {}, {}, never, never, true, never>;
417
+ }
418
+
419
+ export { RdxFieldControl, RdxFieldDescription, RdxFieldError, RdxFieldItem, RdxFieldLabel, RdxFieldRoot, RdxNgControlField, injectFieldRootContext, provideFieldRootContext };
373
420
  export type { RdxFieldRootContext, RdxFieldState };
@@ -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;
@@ -433,7 +474,7 @@ declare class RdxMenuPopup {
433
474
  private search;
434
475
  private searchTimer;
435
476
  protected readonly align: _angular_core.Signal<"center" | "end" | "start" | undefined>;
436
- protected readonly side: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
477
+ protected readonly side: _angular_core.Signal<i1.SideOrLogical | undefined>;
437
478
  /**
438
479
  * Event handler called when the escape key is pressed. Can be prevented.
439
480
  */
@@ -520,7 +561,7 @@ declare class RdxMenuBackdrop {
520
561
  declare class RdxMenuArrow {
521
562
  protected readonly rootContext: _radix_ng_primitives_menu.RdxMenuRootContext;
522
563
  private readonly wrapper;
523
- protected readonly side: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
564
+ protected readonly side: _angular_core.Signal<i1.SideOrLogical | undefined>;
524
565
  protected readonly align: _angular_core.Signal<"center" | "end" | "start" | undefined>;
525
566
  protected readonly uncentered: _angular_core.Signal<boolean>;
526
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>;
@@ -1,8 +1,9 @@
1
1
  import * as _angular_core from '@angular/core';
2
2
  import { ElementRef, Signal, Provider, Type, InjectionToken } from '@angular/core';
3
3
  import { ReferenceElement, Placement, Middleware, VirtualElement } from '@floating-ui/dom';
4
+ import * as _radix_ng_primitives_popper from '@radix-ng/primitives/popper';
4
5
  import * as _radix_ng_primitives_core from '@radix-ng/primitives/core';
5
- import { NumberInput, BooleanInput } from '@radix-ng/primitives/core';
6
+ import { Direction, NumberInput, BooleanInput } from '@radix-ng/primitives/core';
6
7
 
7
8
  declare class RdxPopperAnchor {
8
9
  readonly elementRef: ElementRef<HTMLElement>;
@@ -25,7 +26,8 @@ declare class RdxPopperContent {
25
26
  * simply no-op.
26
27
  */
27
28
  protected readonly popperContentWrapper: {
28
- placedSide: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
29
+ placedSide: _angular_core.Signal<_radix_ng_primitives_popper.SideOrLogical | undefined>;
30
+ physicalPlacedSide: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
29
31
  placedAlign: _angular_core.Signal<"start" | "center" | "end" | undefined>;
30
32
  arrowX: _angular_core.Signal<number | undefined>;
31
33
  arrowY: _angular_core.Signal<number | undefined>;
@@ -48,6 +50,23 @@ declare const SIDE_OPTIONS: readonly ["top", "right", "bottom", "left"];
48
50
  declare const ALIGN_OPTIONS: readonly ["start", "center", "end"];
49
51
  type Side = (typeof SIDE_OPTIONS)[number];
50
52
  type Align = (typeof ALIGN_OPTIONS)[number];
53
+ /** Logical (writing-direction-relative) inline sides, resolved to a physical {@link Side} by text direction. */
54
+ type LogicalSide = 'inline-start' | 'inline-end';
55
+ /** A physical {@link Side} or a {@link LogicalSide} — accepted by the positioner's `side` input. */
56
+ type SideOrLogical = Side | LogicalSide;
57
+ /**
58
+ * Resolve a logical inline side to a physical one for the current text direction; physical sides pass
59
+ * through unchanged (Base UI `useAnchorPositioning`). `inline-start` is the reading-start edge
60
+ * (`left` in LTR, `right` in RTL); `inline-end` is the reading-end edge.
61
+ */
62
+ declare function resolvePhysicalSide(side: SideOrLogical, isRtl: boolean): Side;
63
+ /**
64
+ * Report a rendered physical side back in the same "kind" the consumer requested (Base UI
65
+ * `getLogicalSide`): when a logical side was requested, a `left`/`right` rendered side is mapped back
66
+ * to `inline-start`/`inline-end` for the direction (so a post-collision flip surfaces logically); a
67
+ * physical request, or a `top`/`bottom` rendered side, passes through unchanged.
68
+ */
69
+ declare function toLogicalSide(requestedSide: SideOrLogical, renderedSide: Side, isRtl: boolean): SideOrLogical;
51
70
  /**
52
71
  * How the popper avoids collisions with the boundary edges (Base UI `collisionAvoidance`).
53
72
  *
@@ -72,11 +91,12 @@ type ResolvedCollisionAvoidance = Required<RdxCollisionAvoidance>;
72
91
  /**
73
92
  * Function form of `sideOffset` / `alignOffset` (Base UI `OffsetFunction`). Receives the resolved
74
93
  * placement and the measured anchor / positioner dimensions, and returns the offset in pixels — e.g.
75
- * `({ anchor }) => anchor.width` to offset by the trigger's own width. `side` is the physical side the
76
- * popup is placed against.
94
+ * `({ anchor }) => anchor.width` to offset by the trigger's own width. `side` is the side the popup is
95
+ * placed against reported logically (`inline-start` / `inline-end`) when a logical side was
96
+ * requested, otherwise physical (Base UI parity).
77
97
  */
78
98
  type OffsetFunction = (data: {
79
- side: Side;
99
+ side: SideOrLogical;
80
100
  align: Align;
81
101
  anchor: {
82
102
  width: number;
@@ -96,7 +116,8 @@ declare function getSideAndAlignFromPlacement(placement: Placement): readonly ["
96
116
 
97
117
  type RdxPopperAnchorElement = Element | ElementRef<Element> | VirtualElement | (() => Element | VirtualElement | null) | null | undefined;
98
118
  declare const context: () => {
99
- placedSide: Signal<"top" | "right" | "bottom" | "left" | undefined>;
119
+ placedSide: Signal<SideOrLogical | undefined>;
120
+ physicalPlacedSide: Signal<"top" | "right" | "bottom" | "left" | undefined>;
100
121
  placedAlign: Signal<"start" | "center" | "end" | undefined>;
101
122
  arrowX: Signal<number | undefined>;
102
123
  arrowY: Signal<number | undefined>;
@@ -106,7 +127,8 @@ declare const context: () => {
106
127
  };
107
128
  type PopperContentWrapperContext = ReturnType<typeof context>;
108
129
  declare const injectPopperContentWrapperContext: _radix_ng_primitives_core.InjectContext<{
109
- placedSide: Signal<"top" | "right" | "bottom" | "left" | undefined>;
130
+ placedSide: Signal<SideOrLogical | undefined>;
131
+ physicalPlacedSide: Signal<"top" | "right" | "bottom" | "left" | undefined>;
110
132
  placedAlign: Signal<"start" | "center" | "end" | undefined>;
111
133
  arrowX: Signal<number | undefined>;
112
134
  arrowY: Signal<number | undefined>;
@@ -115,7 +137,8 @@ declare const injectPopperContentWrapperContext: _radix_ng_primitives_core.Injec
115
137
  anchorHidden: Signal<boolean>;
116
138
  }>;
117
139
  declare const providePopperContentWrapperContext: (useFactory: () => {
118
- placedSide: Signal<"top" | "right" | "bottom" | "left" | undefined>;
140
+ placedSide: Signal<SideOrLogical | undefined>;
141
+ physicalPlacedSide: Signal<"top" | "right" | "bottom" | "left" | undefined>;
119
142
  placedAlign: Signal<"start" | "center" | "end" | undefined>;
120
143
  arrowX: Signal<number | undefined>;
121
144
  arrowY: Signal<number | undefined>;
@@ -136,8 +159,21 @@ declare class RdxPopperContentWrapper {
136
159
  /**
137
160
  * The preferred side of the anchor to render against when open.
138
161
  * Will be reversed when collisions occur and avoidCollisions is enabled.
162
+ *
163
+ * Accepts the logical `'inline-start'` / `'inline-end'` in addition to the physical sides; those
164
+ * resolve to `left` / `right` for the text direction (Base UI parity). The placed side reported via
165
+ * `data-side` / `placedSide()` echoes the requested kind: logical in → logical out (even after a
166
+ * collision flip), physical in → physical out.
139
167
  */
140
- readonly side: _angular_core.InputSignal<"top" | "right" | "bottom" | "left">;
168
+ readonly side: _angular_core.InputSignal<SideOrLogical>;
169
+ /**
170
+ * Text direction, used only to resolve a logical `side` (`inline-start` / `inline-end`) to a
171
+ * physical one. Falls back to an enclosing direction provider, then `ltr`.
172
+ */
173
+ readonly dir: _angular_core.InputSignal<Direction | undefined>;
174
+ private readonly direction;
175
+ /** The `side` input with any logical inline value resolved to a physical side for the direction. */
176
+ private readonly resolvedSide;
141
177
  /**
142
178
  * Distance between the anchor and the popup in pixels. Also accepts an {@link OffsetFunction} that
143
179
  * reads the anchor / positioner dimensions and the resolved side / align.
@@ -253,9 +289,18 @@ declare class RdxPopperContentWrapper {
253
289
  align: Align;
254
290
  } | undefined>;
255
291
  /**
256
- * The side the panel is currently placed against.
292
+ * The physical side the panel is currently placed against. Internal geometry (arrow position,
293
+ * transform origins) must use this — never {@link placedSide}, which can be logical.
294
+ */
295
+ readonly physicalPlacedSide: Signal<"top" | "right" | "bottom" | "left" | undefined>;
296
+ /**
297
+ * The side the panel is currently placed against, reported in the kind the consumer requested
298
+ * (Base UI positioner `side`): when a logical `side` was passed, a placed `left`/`right` maps back
299
+ * to `inline-start`/`inline-end` for the direction — so a post-collision flip stays logical and
300
+ * `[data-side="inline-start"]` CSS is direction-agnostic. A physical request is always reported
301
+ * physically.
257
302
  */
258
- readonly placedSide: Signal<"top" | "right" | "bottom" | "left" | undefined>;
303
+ readonly placedSide: Signal<SideOrLogical | undefined>;
259
304
  /**
260
305
  * The current alignment of the panel.
261
306
  */
@@ -292,7 +337,7 @@ declare class RdxPopperContentWrapper {
292
337
  private readonly afterRenderEffect;
293
338
  constructor();
294
339
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxPopperContentWrapper, never>;
295
- static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxPopperContentWrapper, "[rdxPopperContentWrapper]", never, { "anchor": { "alias": "anchor"; "required": false; "isSignal": true; }; "side": { "alias": "side"; "required": false; "isSignal": true; }; "sideOffset": { "alias": "sideOffset"; "required": false; "isSignal": true; }; "align": { "alias": "align"; "required": false; "isSignal": true; }; "alignOffset": { "alias": "alignOffset"; "required": false; "isSignal": true; }; "arrowPadding": { "alias": "arrowPadding"; "required": false; "isSignal": true; }; "avoidCollisions": { "alias": "avoidCollisions"; "required": false; "isSignal": true; }; "collisionAvoidance": { "alias": "collisionAvoidance"; "required": false; "isSignal": true; }; "collisionBoundary": { "alias": "collisionBoundary"; "required": false; "isSignal": true; }; "collisionPadding": { "alias": "collisionPadding"; "required": false; "isSignal": true; }; "sticky": { "alias": "sticky"; "required": false; "isSignal": true; }; "hideWhenDetached": { "alias": "hideWhenDetached"; "required": false; "isSignal": true; }; "positionStrategy": { "alias": "positionStrategy"; "required": false; "isSignal": true; }; "updatePositionStrategy": { "alias": "updatePositionStrategy"; "required": false; "isSignal": true; }; }, { "placed": "placed"; }, ["arrow"], never, true, never>;
340
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RdxPopperContentWrapper, "[rdxPopperContentWrapper]", never, { "anchor": { "alias": "anchor"; "required": false; "isSignal": true; }; "side": { "alias": "side"; "required": false; "isSignal": true; }; "dir": { "alias": "dir"; "required": false; "isSignal": true; }; "sideOffset": { "alias": "sideOffset"; "required": false; "isSignal": true; }; "align": { "alias": "align"; "required": false; "isSignal": true; }; "alignOffset": { "alias": "alignOffset"; "required": false; "isSignal": true; }; "arrowPadding": { "alias": "arrowPadding"; "required": false; "isSignal": true; }; "avoidCollisions": { "alias": "avoidCollisions"; "required": false; "isSignal": true; }; "collisionAvoidance": { "alias": "collisionAvoidance"; "required": false; "isSignal": true; }; "collisionBoundary": { "alias": "collisionBoundary"; "required": false; "isSignal": true; }; "collisionPadding": { "alias": "collisionPadding"; "required": false; "isSignal": true; }; "sticky": { "alias": "sticky"; "required": false; "isSignal": true; }; "hideWhenDetached": { "alias": "hideWhenDetached"; "required": false; "isSignal": true; }; "positionStrategy": { "alias": "positionStrategy"; "required": false; "isSignal": true; }; "updatePositionStrategy": { "alias": "updatePositionStrategy"; "required": false; "isSignal": true; }; }, { "placed": "placed"; }, ["arrow"], never, true, never>;
296
341
  }
297
342
  /**
298
343
  * Providers a "thin" positioner that `extends RdxPopperContentWrapper` must include. Angular
@@ -351,7 +396,7 @@ declare class RdxPopperArrow {
351
396
  * `undefined` falls back to the wrapper's built-in default. Consumer template bindings always win.
352
397
  */
353
398
  interface RdxPopperContentConfig {
354
- side?: Side;
399
+ side?: SideOrLogical;
355
400
  align?: Align;
356
401
  sideOffset?: number | OffsetFunction;
357
402
  alignOffset?: number | OffsetFunction;
@@ -390,5 +435,5 @@ declare function provideRdxPopperContentConfig(config: RdxPopperContentConfig):
390
435
 
391
436
  declare const popperImports: (typeof RdxPopperAnchor | typeof RdxPopper | typeof RdxPopperContentWrapper | typeof RdxPopperContent)[];
392
437
 
393
- export { DROPDOWN_COLLISION_AVOIDANCE, POPUP_COLLISION_AVOIDANCE, RdxPopper, RdxPopperAnchor, RdxPopperArrow, RdxPopperContent, RdxPopperContentConfigToken, RdxPopperContentWrapper, getSideAndAlignFromPlacement, injectPopperContentWrapperContext, isNotNull, legacyPopperVars, popperImports, providePopperContentWrapperContext, provideRdxPopperContentConfig, provideRdxPopperContentWrapper, transformOrigin };
394
- export type { Align, OffsetFunction, PopperContentWrapperContext, RdxCollisionAvoidance, RdxPopperAnchorElement, RdxPopperContentConfig, ResolvedCollisionAvoidance, Side };
438
+ export { DROPDOWN_COLLISION_AVOIDANCE, POPUP_COLLISION_AVOIDANCE, RdxPopper, RdxPopperAnchor, RdxPopperArrow, RdxPopperContent, RdxPopperContentConfigToken, RdxPopperContentWrapper, getSideAndAlignFromPlacement, injectPopperContentWrapperContext, isNotNull, legacyPopperVars, popperImports, providePopperContentWrapperContext, provideRdxPopperContentConfig, provideRdxPopperContentWrapper, resolvePhysicalSide, toLogicalSide, transformOrigin };
439
+ export type { Align, LogicalSide, OffsetFunction, PopperContentWrapperContext, RdxCollisionAvoidance, RdxPopperAnchorElement, RdxPopperContentConfig, ResolvedCollisionAvoidance, Side, SideOrLogical };
@@ -268,7 +268,7 @@ declare class RdxPreviewCardPopup {
268
268
  private readonly registration;
269
269
  private readonly wrapper;
270
270
  protected readonly align: _angular_core.Signal<"start" | "center" | "end" | undefined>;
271
- protected readonly side: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
271
+ protected readonly side: _angular_core.Signal<i1$1.SideOrLogical | undefined>;
272
272
  /**
273
273
  * Event handler called when the escape key is down. Can be prevented.
274
274
  */
@@ -296,7 +296,7 @@ declare class RdxPreviewCardPopup {
296
296
  declare class RdxPreviewCardArrow {
297
297
  protected readonly rootContext: _radix_ng_primitives_preview_card.RdxPreviewCardRootContext;
298
298
  private readonly wrapper;
299
- protected readonly side: _angular_core.Signal<"top" | "right" | "bottom" | "left" | undefined>;
299
+ protected readonly side: _angular_core.Signal<i1$1.SideOrLogical | undefined>;
300
300
  protected readonly align: _angular_core.Signal<"start" | "center" | "end" | undefined>;
301
301
  protected readonly uncentered: _angular_core.Signal<boolean>;
302
302
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RdxPreviewCardArrow, never>;