@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
@@ -1,8 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ElementRef, Directive, contentChild, signal, InjectionToken, Injector, input, numberAttribute, booleanAttribute, output, forwardRef, computed, resource, linkedSignal, afterRenderEffect, afterNextRender, EnvironmentInjector, effect, createComponent, inputBinding } from '@angular/core';
2
+ import { inject, ElementRef, Directive, contentChild, signal, InjectionToken, numberAttribute, Injector, input, computed, booleanAttribute, output, forwardRef, resource, linkedSignal, afterRenderEffect, afterNextRender, EnvironmentInjector, effect, createComponent, inputBinding } from '@angular/core';
3
3
  import { RdxArrow } from '@radix-ng/primitives/arrow';
4
- import { computePosition, offset, shift, limitShift, flip, size, arrow, hide, autoUpdate } from '@floating-ui/dom';
4
+ import { flip, shift, limitShift, computePosition, offset, size, arrow, hide, autoUpdate } from '@floating-ui/dom';
5
5
  import { createContext, elementSize, watch } from '@radix-ng/primitives/core';
6
+ import { injectDirection } from '@radix-ng/primitives/direction-provider';
6
7
 
7
8
  class RdxPopperAnchor {
8
9
  constructor() {
@@ -35,6 +36,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
35
36
  }]
36
37
  }], propDecorators: { anchor: [{ type: i0.ContentChild, args: [i0.forwardRef(() => RdxPopperAnchor), { isSignal: true }] }] } });
37
38
 
39
+ /**
40
+ * Collision-avoidance preset for dropdown-style popups (Base UI `DROPDOWN_COLLISION_AVOIDANCE`).
41
+ *
42
+ * Dropdowns (select / combobox / autocomplete / menu) strictly prefer their top/bottom placement and
43
+ * cap their height with `var(--available-height)`, so they must **not** fall back to the perpendicular
44
+ * (left/right) axis. `side`/`align` fall back to the wrapper default (`'flip'`).
45
+ */
46
+ const DROPDOWN_COLLISION_AVOIDANCE = { fallbackAxisSide: 'none' };
47
+ /**
48
+ * Collision-avoidance preset for regular popups (Base UI `POPUP_COLLISION_AVOIDANCE`).
49
+ *
50
+ * Popovers / tooltips / preview cards aren't height-capped and may freely flip to the perpendicular
51
+ * axis, preferring the logical end side. This equals the wrapper's built-in default; it is exported so
52
+ * popups can state the intent explicitly.
53
+ */
54
+ const POPUP_COLLISION_AVOIDANCE = { fallbackAxisSide: 'end' };
38
55
  const RdxPopperContentConfigToken = new InjectionToken('RdxPopperContentConfig', {
39
56
  factory: () => ({})
40
57
  });
@@ -46,6 +63,39 @@ function provideRdxPopperContentConfig(config) {
46
63
  const SIDE_OPTIONS = ['top', 'right', 'bottom', 'left'];
47
64
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
48
65
  const ALIGN_OPTIONS = ['start', 'center', 'end'];
66
+ /**
67
+ * Resolve a logical inline side to a physical one for the current text direction; physical sides pass
68
+ * through unchanged (Base UI `useAnchorPositioning`). `inline-start` is the reading-start edge
69
+ * (`left` in LTR, `right` in RTL); `inline-end` is the reading-end edge.
70
+ */
71
+ function resolvePhysicalSide(side, isRtl) {
72
+ if (side === 'inline-start') {
73
+ return isRtl ? 'right' : 'left';
74
+ }
75
+ if (side === 'inline-end') {
76
+ return isRtl ? 'left' : 'right';
77
+ }
78
+ return side;
79
+ }
80
+ /**
81
+ * Report a rendered physical side back in the same "kind" the consumer requested (Base UI
82
+ * `getLogicalSide`): when a logical side was requested, a `left`/`right` rendered side is mapped back
83
+ * to `inline-start`/`inline-end` for the direction (so a post-collision flip surfaces logically); a
84
+ * physical request, or a `top`/`bottom` rendered side, passes through unchanged.
85
+ */
86
+ function toLogicalSide(requestedSide, renderedSide, isRtl) {
87
+ const requestedLogical = requestedSide === 'inline-start' || requestedSide === 'inline-end';
88
+ if (!requestedLogical) {
89
+ return renderedSide;
90
+ }
91
+ if (renderedSide === 'left') {
92
+ return isRtl ? 'inline-end' : 'inline-start';
93
+ }
94
+ if (renderedSide === 'right') {
95
+ return isRtl ? 'inline-start' : 'inline-end';
96
+ }
97
+ return renderedSide;
98
+ }
49
99
  function isNotNull(value) {
50
100
  return value !== null;
51
101
  }
@@ -90,10 +140,19 @@ function getSideAndAlignFromPlacement(placement) {
90
140
  return [side, align];
91
141
  }
92
142
 
143
+ /**
144
+ * `input()` transform for `sideOffset` / `alignOffset`: coerce string/number attribute values to a
145
+ * number but pass an {@link OffsetFunction} through untouched. A **named** module-level function (not
146
+ * an inline arrow) so compodoc parses the file — see CLAUDE.md's compodoc gotcha.
147
+ */
148
+ function coerceOffset(value) {
149
+ return typeof value === 'function' ? value : numberAttribute(value);
150
+ }
93
151
  const context = () => {
94
152
  const popperContentWrapper = inject(RdxPopperContentWrapper);
95
153
  return {
96
154
  placedSide: popperContentWrapper.placedSide,
155
+ physicalPlacedSide: popperContentWrapper.physicalPlacedSide,
97
156
  placedAlign: popperContentWrapper.placedAlign,
98
157
  arrowX: popperContentWrapper.arrowX,
99
158
  arrowY: popperContentWrapper.arrowY,
@@ -106,14 +165,18 @@ const [injectPopperContentWrapperContext, providePopperContentWrapperContext] =
106
165
  class RdxPopperContentWrapper {
107
166
  positionParams() {
108
167
  return {
168
+ positioningActive: this.positioningActive(),
109
169
  anchor: this.resolvedAnchor(),
110
170
  strategy: this.positionStrategy(),
111
171
  placement: this.desiredPlacement(),
172
+ side: this.side(),
173
+ isRtl: this.direction() === 'rtl',
174
+ align: this.align(),
112
175
  sideOffset: this.sideOffset(),
113
176
  alignOffset: this.alignOffset(),
114
177
  arrowHeight: this.arrowSize()().height,
115
178
  arrowWidth: this.arrowSize()().width,
116
- avoidCollisions: this.avoidCollisions(),
179
+ collisionAvoidance: this.effectiveCollisionAvoidance(),
117
180
  sticky: this.sticky(),
118
181
  detectOverflowOptions: this.detectOverflowOptions(),
119
182
  arrow: this.arrow(),
@@ -135,27 +198,83 @@ class RdxPopperContentWrapper {
135
198
  /**
136
199
  * The preferred side of the anchor to render against when open.
137
200
  * Will be reversed when collisions occur and avoidCollisions is enabled.
201
+ *
202
+ * Accepts the logical `'inline-start'` / `'inline-end'` in addition to the physical sides; those
203
+ * resolve to `left` / `right` for the text direction (Base UI parity). The placed side reported via
204
+ * `data-side` / `placedSide()` echoes the requested kind: logical in → logical out (even after a
205
+ * collision flip), physical in → physical out.
138
206
  */
139
207
  this.side = input(this.config.side ?? 'bottom', /* @ts-ignore */
140
208
  ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
141
209
  /**
142
- * Distance between the anchor and the popup in pixels.
210
+ * Text direction, used only to resolve a logical `side` (`inline-start` / `inline-end`) to a
211
+ * physical one. Falls back to an enclosing direction provider, then `ltr`.
212
+ */
213
+ this.dir = input(undefined, /* @ts-ignore */
214
+ ...(ngDevMode ? [{ debugName: "dir" }] : /* istanbul ignore next */ []));
215
+ this.direction = injectDirection(this.dir);
216
+ /** The `side` input with any logical inline value resolved to a physical side for the direction. */
217
+ this.resolvedSide = computed(() => resolvePhysicalSide(this.side(), this.direction() === 'rtl'), /* @ts-ignore */
218
+ ...(ngDevMode ? [{ debugName: "resolvedSide" }] : /* istanbul ignore next */ []));
219
+ /**
220
+ * Distance between the anchor and the popup in pixels. Also accepts an {@link OffsetFunction} that
221
+ * reads the anchor / positioner dimensions and the resolved side / align.
143
222
  */
144
- this.sideOffset = input(this.config.sideOffset ?? 0, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
223
+ this.sideOffset = input(this.config.sideOffset ?? 0, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: coerceOffset });
145
224
  /**
146
225
  * How to align the popup relative to the specified side. May change when collisions occur.
147
226
  */
148
227
  this.align = input(this.config.align ?? 'center', /* @ts-ignore */
149
228
  ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
150
- /** An offset in pixels from the `start` or `end` alignment options. */
151
- this.alignOffset = input(this.config.alignOffset ?? 0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
229
+ /**
230
+ * An offset in pixels from the `start` or `end` alignment options. Also accepts an
231
+ * {@link OffsetFunction} (same signature as `sideOffset`).
232
+ */
233
+ this.alignOffset = input(this.config.alignOffset ?? 0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: coerceOffset });
152
234
  /**
153
235
  * Minimum distance to maintain between the arrow and the edges of the popup.
154
236
  * If your content has border-radius, this will prevent it from overflowing the corners.
155
237
  */
156
238
  this.arrowPadding = input(this.config.arrowPadding ?? 0, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
157
- /** When `true`, overrides the `side` and `align` preferences to prevent collisions with boundary edges. */
239
+ /**
240
+ * When `true`, overrides the `side` and `align` preferences to prevent collisions with boundary edges.
241
+ *
242
+ * @deprecated Use {@link collisionAvoidance} for fine-grained control (Base UI parity). Still honored:
243
+ * `false` disables all avoidance (unless a `collisionAvoidance` object is provided, which wins).
244
+ */
158
245
  this.avoidCollisions = input(this.config.avoidCollisions ?? true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
246
+ /**
247
+ * How the popup avoids colliding with the boundary edges, per axis (Base UI `collisionAvoidance`).
248
+ * Overrides the deprecated `avoidCollisions` and any per-primitive preset. An object here **fully
249
+ * replaces** the preset — omitted fields fall back to `side: 'flip'`, `align: 'flip'`,
250
+ * `fallbackAxisSide: 'end'`, never to the preset. See {@link RdxCollisionAvoidance}.
251
+ */
252
+ this.collisionAvoidance = input(undefined, /* @ts-ignore */
253
+ ...(ngDevMode ? [{ debugName: "collisionAvoidance" }] : /* istanbul ignore next */ []));
254
+ /**
255
+ * Collision avoidance resolved against the deprecated `avoidCollisions` flag, the per-primitive
256
+ * config preset, and the wrapper defaults. Precedence: an explicit `collisionAvoidance` input →
257
+ * a legacy `avoidCollisions=false` (disables everything) → the config preset → the Base UI defaults.
258
+ */
259
+ this.effectiveCollisionAvoidance = computed(() => {
260
+ const explicit = this.collisionAvoidance();
261
+ // Legacy escape hatch (deprecated): `avoidCollisions=false` disables avoidance entirely — unless
262
+ // the consumer passes an explicit `collisionAvoidance` object, which always wins. Checked before
263
+ // the preset so `[avoidCollisions]="false"` still works on a positioner that ships a preset.
264
+ if (!explicit && this.avoidCollisions() === false) {
265
+ return { side: 'none', align: 'none', fallbackAxisSide: 'none' };
266
+ }
267
+ // A consumer object fully REPLACES the preset (Base UI parity — the preset is only the default
268
+ // when the consumer passes nothing); omitted fields fall back to the global defaults, not the
269
+ // preset. `??` (not spread-merge) is what enforces the "replaces, not merges" semantics.
270
+ const source = explicit ?? this.config.collisionAvoidance;
271
+ return {
272
+ side: source?.side ?? 'flip',
273
+ align: source?.align ?? 'flip',
274
+ fallbackAxisSide: source?.fallbackAxisSide ?? 'end'
275
+ };
276
+ }, /* @ts-ignore */
277
+ ...(ngDevMode ? [{ debugName: "effectiveCollisionAvoidance" }] : /* istanbul ignore next */ []));
159
278
  /**
160
279
  * The element used as the collision boundary.
161
280
  * By default this is the viewport, though you can provide additional element(s) to be included in this check.
@@ -214,7 +333,7 @@ class RdxPopperContentWrapper {
214
333
  ...(ngDevMode ? [{ debugName: "arrowY" }] : /* istanbul ignore next */ []));
215
334
  this.anchorHidden = computed(() => this.position.value()?.middlewareData.hide?.referenceHidden === true, /* @ts-ignore */
216
335
  ...(ngDevMode ? [{ debugName: "anchorHidden" }] : /* istanbul ignore next */ []));
217
- this.desiredPlacement = computed(() => (this.side() + (this.align() !== 'center' ? '-' + this.align() : '')), /* @ts-ignore */
336
+ this.desiredPlacement = computed(() => (this.resolvedSide() + (this.align() !== 'center' ? '-' + this.align() : '')), /* @ts-ignore */
218
337
  ...(ngDevMode ? [{ debugName: "desiredPlacement" }] : /* istanbul ignore next */ []));
219
338
  this.arrowSize = computed(() => {
220
339
  const arrowElementRef = this.arrow()?.elementRef;
@@ -264,26 +383,71 @@ class RdxPopperContentWrapper {
264
383
  ...(ngDevMode ? [{ debugName: "resolvedAnchor" }] : /* istanbul ignore next */ []));
265
384
  this.position = resource({ ...(ngDevMode ? { debugName: "position" } : /* istanbul ignore next */ {}), params: () => this.positionParams(),
266
385
  loader: ({ params }) => {
267
- if (!params.anchor) {
386
+ // Skip computing a position for a paused (closed keep-mounted) positioner.
387
+ if (!params.positioningActive || !params.anchor) {
268
388
  return Promise.resolve(null);
269
389
  }
390
+ const ca = params.collisionAvoidance;
391
+ // Ported from Base UI `useAnchorPositioning`: `side`/`align`/`fallbackAxisSide` select and
392
+ // configure the `flip()` / `shift()` middleware instead of the old boolean toggle.
393
+ const shiftDisabled = ca.align === 'none' && ca.side !== 'shift';
394
+ const crossAxisShiftEnabled = !shiftDisabled && (params.sticky === 'always' || ca.side === 'shift');
395
+ const flipMiddleware = ca.side === 'none'
396
+ ? undefined
397
+ : flip({
398
+ ...params.detectOverflowOptions,
399
+ mainAxis: ca.side === 'flip',
400
+ crossAxis: ca.align === 'flip' ? 'alignment' : false,
401
+ fallbackAxisSideDirection: ca.fallbackAxisSide
402
+ });
403
+ const shiftMiddleware = shiftDisabled
404
+ ? undefined
405
+ : shift({
406
+ ...params.detectOverflowOptions,
407
+ mainAxis: ca.align !== 'none',
408
+ crossAxis: crossAxisShiftEnabled,
409
+ // `partial` keeps the popup attached to the anchor (limitShift); `always` keeps it
410
+ // fully inside the boundary regardless (no limiter).
411
+ limiter: params.sticky === 'partial' ? limitShift() : undefined
412
+ });
413
+ // https://floating-ui.com/docs/flip#combining-with-shift — run shift first when shifting is
414
+ // the primary correction (or for center alignment), otherwise flip first.
415
+ const collisionMiddleware = ca.side === 'shift' || ca.align === 'shift' || params.align === 'center'
416
+ ? [shiftMiddleware, flipMiddleware]
417
+ : [flipMiddleware, shiftMiddleware];
270
418
  return computePosition(params.anchor, this.elementRef.nativeElement, {
271
419
  // default to `fixed` strategy so users don't have to pick and we also avoid focus scroll issues
272
420
  strategy: params.strategy,
273
421
  placement: params.placement,
274
422
  middleware: [
275
- offset({
276
- mainAxis: params.sideOffset + params.arrowHeight || 0,
277
- alignmentAxis: params.alignOffset
423
+ offset((state) => {
424
+ const [placedSide, placedAlign] = getSideAndAlignFromPlacement(state.placement);
425
+ const data = {
426
+ // Report the placed side in the same kind the consumer requested: logical
427
+ // when a logical `side` was passed (so a post-collision flip stays logical),
428
+ // physical otherwise (Base UI parity).
429
+ side: toLogicalSide(params.side, placedSide, params.isRtl),
430
+ align: placedAlign,
431
+ anchor: {
432
+ width: state.rects.reference.width,
433
+ height: state.rects.reference.height
434
+ },
435
+ positioner: {
436
+ width: state.rects.floating.width,
437
+ height: state.rects.floating.height
438
+ }
439
+ };
440
+ const sideAxis = typeof params.sideOffset === 'function' ? params.sideOffset(data) : params.sideOffset;
441
+ const alignAxis = typeof params.alignOffset === 'function' ? params.alignOffset(data) : params.alignOffset;
442
+ return {
443
+ mainAxis: sideAxis + params.arrowHeight || 0,
444
+ // both axes so `alignOffset` also applies to center-aligned popups
445
+ // (`alignmentAxis` overrides `crossAxis` for start/end placements)
446
+ crossAxis: alignAxis,
447
+ alignmentAxis: alignAxis
448
+ };
278
449
  }),
279
- params.avoidCollisions &&
280
- shift({
281
- mainAxis: true,
282
- crossAxis: false,
283
- limiter: params.sticky === 'partial' ? limitShift() : undefined,
284
- ...params.detectOverflowOptions
285
- }),
286
- params.avoidCollisions && flip({ ...params.detectOverflowOptions }),
450
+ ...collisionMiddleware,
287
451
  size({
288
452
  ...params.detectOverflowOptions,
289
453
  apply: ({ elements, rects, availableWidth, availableHeight }) => {
@@ -344,9 +508,22 @@ class RdxPopperContentWrapper {
344
508
  }, /* @ts-ignore */
345
509
  ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
346
510
  /**
347
- * The side the panel is currently placed against.
511
+ * The physical side the panel is currently placed against. Internal geometry (arrow position,
512
+ * transform origins) must use this — never {@link placedSide}, which can be logical.
513
+ */
514
+ this.physicalPlacedSide = computed(() => this.placement()?.side, /* @ts-ignore */
515
+ ...(ngDevMode ? [{ debugName: "physicalPlacedSide" }] : /* istanbul ignore next */ []));
516
+ /**
517
+ * The side the panel is currently placed against, reported in the kind the consumer requested
518
+ * (Base UI positioner `side`): when a logical `side` was passed, a placed `left`/`right` maps back
519
+ * to `inline-start`/`inline-end` for the direction — so a post-collision flip stays logical and
520
+ * `[data-side="inline-start"]` CSS is direction-agnostic. A physical request is always reported
521
+ * physically.
348
522
  */
349
- this.placedSide = computed(() => this.placement()?.side, /* @ts-ignore */
523
+ this.placedSide = computed(() => {
524
+ const side = this.placement()?.side;
525
+ return side === undefined ? undefined : toLogicalSide(this.side(), side, this.direction() === 'rtl');
526
+ }, /* @ts-ignore */
350
527
  ...(ngDevMode ? [{ debugName: "placedSide" }] : /* istanbul ignore next */ []));
351
528
  /**
352
529
  * The current alignment of the panel.
@@ -395,7 +572,22 @@ class RdxPopperContentWrapper {
395
572
  };
396
573
  }, /* @ts-ignore */
397
574
  ...(ngDevMode ? [{ debugName: "style" }] : /* istanbul ignore next */ []));
575
+ /**
576
+ * Whether the positioner should actively track its anchor (recompute + `autoUpdate`). Defaults to
577
+ * always-on — the historical behavior, correct for a positioner that unmounts when its layer closes.
578
+ * A positioner that can stay mounted while closed (`keepMounted`) **overrides** this with its
579
+ * open/presence signal so a closed, hidden positioner doesn't spin an endless `autoUpdate`
580
+ * `requestAnimationFrame` loop (only relevant when `updatePositionStrategy: 'always'`). It's a
581
+ * reassignable field (not `readonly`) because the render effect below reads it lazily, after the
582
+ * subclass constructor has swapped it in.
583
+ */
584
+ this.positioningActive = computed(() => true, /* @ts-ignore */
585
+ ...(ngDevMode ? [{ debugName: "positioningActive" }] : /* istanbul ignore next */ []));
398
586
  this.afterRenderEffect = afterRenderEffect((onCleanup) => {
587
+ // Closed keep-mounted positioner: skip positioning entirely so `autoUpdate` doesn't keep firing.
588
+ if (!this.positioningActive()) {
589
+ return;
590
+ }
399
591
  this.position.reload();
400
592
  const anchor = this.resolvedAnchor();
401
593
  if (!anchor) {
@@ -416,7 +608,7 @@ class RdxPopperContentWrapper {
416
608
  });
417
609
  }
418
610
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxPopperContentWrapper, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
419
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.0.2", type: RdxPopperContentWrapper, isStandalone: true, selector: "[rdxPopperContentWrapper]", inputs: { anchor: { classPropertyName: "anchor", publicName: "anchor", isSignal: true, isRequired: false, transformFunction: null }, 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 }, positionStrategy: { classPropertyName: "positionStrategy", publicName: "positionStrategy", isSignal: true, isRequired: false, transformFunction: null }, updatePositionStrategy: { classPropertyName: "updatePositionStrategy", publicName: "updatePositionStrategy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { placed: "placed" }, host: { attributes: { "data-radix-popper-content-wrapper": "" }, properties: { "attr.data-side": "placedSide()", "attr.data-align": "placedAlign()", "attr.data-anchor-hidden": "anchorHidden() ? \"\" : undefined", "style": "style()" } }, providers: [providePopperContentWrapperContext(context)], queries: [{ propertyName: "arrow", first: true, predicate: i0.forwardRef(() => RdxPopperArrow), descendants: true, isSignal: true }], ngImport: i0 }); }
611
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.0.2", type: RdxPopperContentWrapper, isStandalone: true, selector: "[rdxPopperContentWrapper]", inputs: { anchor: { classPropertyName: "anchor", publicName: "anchor", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, dir: { classPropertyName: "dir", publicName: "dir", 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 }, collisionAvoidance: { classPropertyName: "collisionAvoidance", publicName: "collisionAvoidance", 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 }, positionStrategy: { classPropertyName: "positionStrategy", publicName: "positionStrategy", isSignal: true, isRequired: false, transformFunction: null }, updatePositionStrategy: { classPropertyName: "updatePositionStrategy", publicName: "updatePositionStrategy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { placed: "placed" }, host: { attributes: { "data-radix-popper-content-wrapper": "" }, properties: { "attr.data-side": "placedSide()", "attr.data-align": "placedAlign()", "attr.data-anchor-hidden": "anchorHidden() ? \"\" : undefined", "style": "style()" } }, providers: [providePopperContentWrapperContext(context)], queries: [{ propertyName: "arrow", first: true, predicate: i0.forwardRef(() => RdxPopperArrow), descendants: true, isSignal: true }], ngImport: i0 }); }
420
612
  }
421
613
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxPopperContentWrapper, decorators: [{
422
614
  type: Directive,
@@ -433,7 +625,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
433
625
  '[style]': 'style()'
434
626
  }
435
627
  }]
436
- }], ctorParameters: () => [], propDecorators: { anchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchor", required: false }] }], 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 }] }], positionStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "positionStrategy", required: false }] }], updatePositionStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "updatePositionStrategy", required: false }] }], placed: [{ type: i0.Output, args: ["placed"] }], arrow: [{ type: i0.ContentChild, args: [forwardRef(() => RdxPopperArrow), { isSignal: true }] }] } });
628
+ }], ctorParameters: () => [], propDecorators: { anchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchor", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], dir: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", 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 }] }], collisionAvoidance: [{ type: i0.Input, args: [{ isSignal: true, alias: "collisionAvoidance", 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 }] }], positionStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "positionStrategy", required: false }] }], updatePositionStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "updatePositionStrategy", required: false }] }], placed: [{ type: i0.Output, args: ["placed"] }], arrow: [{ type: i0.ContentChild, args: [forwardRef(() => RdxPopperArrow), { isSignal: true }] }] } });
437
629
  /**
438
630
  * Providers a "thin" positioner that `extends RdxPopperContentWrapper` must include. Angular
439
631
  * inherits a base directive's inputs/outputs/host bindings/queries but **not** its `providers`, so
@@ -485,7 +677,9 @@ class RdxPopperArrow {
485
677
  this.elementRef = inject(ElementRef);
486
678
  this.width = input(10, { ...(ngDevMode ? { debugName: "width" } : /* istanbul ignore next */ {}), transform: numberAttribute });
487
679
  this.height = input(5, { ...(ngDevMode ? { debugName: "height" } : /* istanbul ignore next */ {}), transform: numberAttribute });
488
- this.baseSide = computed(() => OPPOSITE_SIDE[this.popperContentContext.placedSide()], /* @ts-ignore */
680
+ // Geometry is physical: the arrow reads `physicalPlacedSide` (`placedSide` can be logical
681
+ // `inline-start`/`inline-end` when a logical side was requested — a styling hook, not a direction).
682
+ this.baseSide = computed(() => OPPOSITE_SIDE[this.popperContentContext.physicalPlacedSide()], /* @ts-ignore */
489
683
  ...(ngDevMode ? [{ debugName: "baseSide" }] : /* istanbul ignore next */ []));
490
684
  this.style = computed(() => {
491
685
  return {
@@ -498,13 +692,13 @@ class RdxPopperArrow {
498
692
  right: '0 0',
499
693
  bottom: 'center 0',
500
694
  left: '100% 0'
501
- }[this.popperContentContext.placedSide()],
695
+ }[this.popperContentContext.physicalPlacedSide()],
502
696
  transform: {
503
697
  top: 'translateY(100%)',
504
698
  right: 'translateY(50%) rotate(90deg) translateX(-50%)',
505
699
  bottom: `rotate(180deg)`,
506
700
  left: 'translateY(50%) rotate(-90deg) translateX(50%)'
507
- }[this.popperContentContext.placedSide()],
701
+ }[this.popperContentContext.physicalPlacedSide()],
508
702
  visibility: this.popperContentContext.shouldHideArrow() ? 'hidden' : undefined
509
703
  };
510
704
  }, /* @ts-ignore */
@@ -575,5 +769,5 @@ const popperImports = [RdxPopper, RdxPopperArrow, RdxPopperContentWrapper, RdxPo
575
769
  * Generated bundle index. Do not edit.
576
770
  */
577
771
 
578
- export { RdxPopper, RdxPopperAnchor, RdxPopperArrow, RdxPopperContent, RdxPopperContentConfigToken, RdxPopperContentWrapper, getSideAndAlignFromPlacement, injectPopperContentWrapperContext, isNotNull, legacyPopperVars, popperImports, providePopperContentWrapperContext, provideRdxPopperContentConfig, provideRdxPopperContentWrapper, transformOrigin };
772
+ export { DROPDOWN_COLLISION_AVOIDANCE, POPUP_COLLISION_AVOIDANCE, RdxPopper, RdxPopperAnchor, RdxPopperArrow, RdxPopperContent, RdxPopperContentConfigToken, RdxPopperContentWrapper, getSideAndAlignFromPlacement, injectPopperContentWrapperContext, isNotNull, legacyPopperVars, popperImports, providePopperContentWrapperContext, provideRdxPopperContentConfig, provideRdxPopperContentWrapper, resolvePhysicalSide, toLogicalSide, transformOrigin };
579
773
  //# sourceMappingURL=radix-ng-primitives-popper.mjs.map