@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.
- package/README.md +26 -5
- package/fesm2022/radix-ng-primitives-autocomplete.mjs +35 -19
- package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-checkbox.mjs +55 -7
- package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-combobox.mjs +46 -21
- package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-composite.mjs +221 -36
- package/fesm2022/radix-ng-primitives-composite.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-core.mjs +189 -26
- package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-date-field.mjs +29 -13
- package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-field.mjs +127 -23
- package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +9 -0
- package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-focus-scope.mjs +241 -95
- package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-form.mjs +35 -4
- package/fesm2022/radix-ng-primitives-form.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-input.mjs +87 -15
- package/fesm2022/radix-ng-primitives-input.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-menu.mjs +131 -16
- package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs +17 -3
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-popper.mjs +223 -29
- package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-portal.mjs +16 -5
- package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-scroll-area.mjs +6 -3
- package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-select.mjs +49 -17
- package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-signal-forms.mjs +27 -11
- package/fesm2022/radix-ng-primitives-signal-forms.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-time-field.mjs +24 -10
- package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
- package/package.json +8 -7
- package/types/radix-ng-primitives-autocomplete.d.ts +7 -2
- package/types/radix-ng-primitives-checkbox.d.ts +10 -1
- package/types/radix-ng-primitives-combobox.d.ts +29 -2
- package/types/radix-ng-primitives-composite.d.ts +48 -2
- package/types/radix-ng-primitives-core.d.ts +165 -104
- package/types/radix-ng-primitives-date-field.d.ts +21 -8
- package/types/radix-ng-primitives-field.d.ts +55 -8
- package/types/radix-ng-primitives-floating-focus-manager.d.ts +7 -0
- package/types/radix-ng-primitives-focus-scope.d.ts +61 -44
- package/types/radix-ng-primitives-form.d.ts +9 -0
- package/types/radix-ng-primitives-input.d.ts +10 -2
- package/types/radix-ng-primitives-menu.d.ts +59 -7
- package/types/radix-ng-primitives-navigation-menu.d.ts +2 -2
- package/types/radix-ng-primitives-popover.d.ts +2 -2
- package/types/radix-ng-primitives-popper.d.ts +186 -48
- package/types/radix-ng-primitives-portal.d.ts +18 -8
- package/types/radix-ng-primitives-preview-card.d.ts +2 -2
- package/types/radix-ng-primitives-select.d.ts +17 -3
- package/types/radix-ng-primitives-signal-forms.d.ts +22 -8
- package/types/radix-ng-primitives-time-field.d.ts +21 -10
- package/types/radix-ng-primitives-tooltip.d.ts +2 -2
- package/fesm2022/radix-ng-primitives-focus-guards.mjs +0 -53
- package/fesm2022/radix-ng-primitives-focus-guards.mjs.map +0 -1
- package/focus-guards/README.md +0 -1
- 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,
|
|
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 {
|
|
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
|
-
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
/**
|
|
151
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|