@atlaskit/top-layer 0.1.5 → 0.2.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/CHANGELOG.md +24 -0
- package/dist/cjs/arrow/index.js +1 -1
- package/dist/cjs/internal/anchor-positioning-fallback.js +82 -22
- package/dist/cjs/internal/resolve-css-length-to-pixels.js +53 -0
- package/dist/cjs/internal/resolve-css-length.js +18 -0
- package/dist/cjs/internal/resolve-placement.js +47 -6
- package/dist/cjs/internal/use-anchor-positioning.js +180 -36
- package/dist/cjs/placement-map/index.js +31 -3
- package/dist/cjs/popup/popup-content.js +0 -2
- package/dist/es2019/arrow/index.js +22 -3
- package/dist/es2019/internal/anchor-positioning-fallback.js +80 -17
- package/dist/es2019/internal/resolve-css-length-to-pixels.js +48 -0
- package/dist/es2019/internal/resolve-css-length.js +13 -0
- package/dist/es2019/internal/resolve-placement.js +48 -6
- package/dist/es2019/internal/use-anchor-positioning.js +173 -28
- package/dist/es2019/placement-map/index.js +25 -3
- package/dist/es2019/popup/popup-content.js +0 -2
- package/dist/esm/arrow/index.js +1 -1
- package/dist/esm/internal/anchor-positioning-fallback.js +82 -22
- package/dist/esm/internal/resolve-css-length-to-pixels.js +47 -0
- package/dist/esm/internal/resolve-css-length.js +12 -0
- package/dist/esm/internal/resolve-placement.js +48 -6
- package/dist/esm/internal/use-anchor-positioning.js +179 -36
- package/dist/esm/placement-map/index.js +30 -3
- package/dist/esm/popup/popup-content.js +0 -2
- package/dist/types/internal/anchor-positioning-fallback.d.ts +11 -9
- package/dist/types/internal/resolve-css-length-to-pixels.d.ts +22 -0
- package/dist/types/internal/resolve-css-length.d.ts +8 -0
- package/dist/types/internal/resolve-placement.d.ts +47 -5
- package/dist/types/internal/use-anchor-positioning.d.ts +18 -7
- package/dist/types/placement-map/index.d.ts +11 -5
- package/dist/types/popup/types.d.ts +15 -30
- package/dist/types-ts4.5/internal/anchor-positioning-fallback.d.ts +11 -9
- package/dist/types-ts4.5/internal/resolve-css-length-to-pixels.d.ts +22 -0
- package/dist/types-ts4.5/internal/resolve-css-length.d.ts +8 -0
- package/dist/types-ts4.5/internal/resolve-placement.d.ts +47 -5
- package/dist/types-ts4.5/internal/use-anchor-positioning.d.ts +18 -7
- package/dist/types-ts4.5/placement-map/index.d.ts +14 -5
- package/dist/types-ts4.5/popup/types.d.ts +15 -30
- package/package.json +2 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a CSS length value to a pixel number using a hidden DOM probe.
|
|
3
|
+
*
|
|
4
|
+
* Used by the JS positioning fallback: consumer offsets can be numbers,
|
|
5
|
+
* `${n}px` strings, tokenised `var(--ds-space-100, 8px)` strings, `calc()`
|
|
6
|
+
* expressions, viewport units, etc. Parsing arbitrary CSS in JS is fragile
|
|
7
|
+
* (and impossible for `calc(var(...))` without a layout context), so we let
|
|
8
|
+
* the browser do the math.
|
|
9
|
+
*
|
|
10
|
+
* The probe is appended to `container` so it inherits the same containing
|
|
11
|
+
* block, font size, and custom-property scope as the popover. We use `width`
|
|
12
|
+
* because it accepts any CSS length, including negative `calc()` results
|
|
13
|
+
* (negative widths are clamped to 0 — see note below).
|
|
14
|
+
*
|
|
15
|
+
* Cost: one synchronous reflow per call. The fallback only resolves on
|
|
16
|
+
* placement/offset changes, not on every scroll/resize update, so this is
|
|
17
|
+
* not a hot path.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveCssLengthToPixels({
|
|
20
|
+
value,
|
|
21
|
+
container
|
|
22
|
+
}) {
|
|
23
|
+
if (typeof value === 'number') {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Fast path: bare `${n}px` strings come from the consumer normalisation
|
|
28
|
+
// in `getPlacement`. Avoid a reflow when we can.
|
|
29
|
+
const pxMatch = value.match(/^(-?\d+(?:\.\d+)?)px$/);
|
|
30
|
+
if (pxMatch) {
|
|
31
|
+
return Number(pxMatch[1]);
|
|
32
|
+
}
|
|
33
|
+
const probe = container.ownerDocument.createElement('div');
|
|
34
|
+
probe.style.position = 'absolute';
|
|
35
|
+
probe.style.visibility = 'hidden';
|
|
36
|
+
probe.style.pointerEvents = 'none';
|
|
37
|
+
probe.style.height = '0';
|
|
38
|
+
probe.style.padding = '0';
|
|
39
|
+
probe.style.border = '0';
|
|
40
|
+
// `width` clamps negatives to 0; use `margin-left` instead so signs are
|
|
41
|
+
// preserved. `getComputedStyle` returns the resolved pixel value for
|
|
42
|
+
// margin-* properties even when the source was a token / calc / var.
|
|
43
|
+
probe.style.marginLeft = value;
|
|
44
|
+
container.appendChild(probe);
|
|
45
|
+
const resolved = parseFloat(getComputedStyle(probe).marginLeft);
|
|
46
|
+
container.removeChild(probe);
|
|
47
|
+
return Number.isFinite(resolved) ? resolved : 0;
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes an offset to a CSS length string. Numbers become `${n}px`;
|
|
3
|
+
* strings pass through. Apply at the API boundary so internal code only
|
|
4
|
+
* deals with strings.
|
|
5
|
+
*/
|
|
6
|
+
export function toCssLengthString({
|
|
7
|
+
value
|
|
8
|
+
}) {
|
|
9
|
+
if (typeof value === 'number') {
|
|
10
|
+
return `${value}px`;
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
@@ -1,19 +1,61 @@
|
|
|
1
|
+
import { toCssLengthString } from './resolve-css-length';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default gap between trigger and popover. Resolves to a design
|
|
5
|
+
* token (`space.100`) so the value follows the design system's spacing
|
|
6
|
+
* scale. Used when `placement.offset.gap` is not provided.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_GAP = "var(--ds-space-100, 8px)";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cross-axis shift. `value` is a CSS length string (consumer numbers are
|
|
12
|
+
* normalized to `${n}px`). `direction` is `'forwards'` (toward end) or
|
|
13
|
+
* `'backwards'` (toward start).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fully-resolved placement (internal). Use `getPlacement` for partial input.
|
|
18
|
+
* `offset.gap` and `offset.crossAxisShift.value` are always CSS
|
|
19
|
+
* length strings.
|
|
20
|
+
*/
|
|
21
|
+
|
|
1
22
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
23
|
+
* Partial placement options. `offset.gap` and
|
|
24
|
+
* `offset.crossAxisShift.value` accept either a number (pixels) or a
|
|
25
|
+
* CSS length string (e.g. `token('space.100')`).
|
|
26
|
+
*
|
|
27
|
+
* The two offset axes are named for clarity at the call site:
|
|
28
|
+
* - `gap` — distance away from the trigger along the placement axis
|
|
29
|
+
* - `crossAxisShift` — nudge along the trigger edge (perpendicular to the placement axis)
|
|
4
30
|
*/
|
|
5
31
|
|
|
6
32
|
/**
|
|
7
|
-
* Resolves a partial placement to its fully-specified form.
|
|
8
|
-
*
|
|
33
|
+
* Resolves a partial placement to its fully-specified form. Number offsets
|
|
34
|
+
* are normalized to `${n}px` strings.
|
|
35
|
+
*
|
|
36
|
+
* Defaults: `axis: 'block'`, `edge: 'end'`, `align: 'center'`,
|
|
37
|
+
* `offset.gap: token('space.100', '8px')`,
|
|
38
|
+
* `offset.crossAxisShift: { value: '0px', direction: 'forwards' }`.
|
|
9
39
|
*/
|
|
10
40
|
export function getPlacement({
|
|
11
41
|
placement
|
|
12
42
|
}) {
|
|
13
|
-
var _placement$axis, _placement$edge, _placement$align;
|
|
43
|
+
var _placement$offset, _placement$axis, _placement$edge, _placement$align, _placement$offset$cro, _placement$offset2, _placement$offset2$cr, _placement$offset$cro2, _placement$offset3, _placement$offset3$cr;
|
|
44
|
+
const consumerGap = (_placement$offset = placement.offset) === null || _placement$offset === void 0 ? void 0 : _placement$offset.gap;
|
|
14
45
|
return {
|
|
15
46
|
axis: (_placement$axis = placement.axis) !== null && _placement$axis !== void 0 ? _placement$axis : 'block',
|
|
16
47
|
edge: (_placement$edge = placement.edge) !== null && _placement$edge !== void 0 ? _placement$edge : 'end',
|
|
17
|
-
align: (_placement$align = placement.align) !== null && _placement$align !== void 0 ? _placement$align : 'center'
|
|
48
|
+
align: (_placement$align = placement.align) !== null && _placement$align !== void 0 ? _placement$align : 'center',
|
|
49
|
+
offset: {
|
|
50
|
+
gap: consumerGap === undefined ? DEFAULT_GAP : toCssLengthString({
|
|
51
|
+
value: consumerGap
|
|
52
|
+
}),
|
|
53
|
+
crossAxisShift: {
|
|
54
|
+
value: toCssLengthString({
|
|
55
|
+
value: (_placement$offset$cro = (_placement$offset2 = placement.offset) === null || _placement$offset2 === void 0 ? void 0 : (_placement$offset2$cr = _placement$offset2.crossAxisShift) === null || _placement$offset2$cr === void 0 ? void 0 : _placement$offset2$cr.value) !== null && _placement$offset$cro !== void 0 ? _placement$offset$cro : 0
|
|
56
|
+
}),
|
|
57
|
+
direction: (_placement$offset$cro2 = (_placement$offset3 = placement.offset) === null || _placement$offset3 === void 0 ? void 0 : (_placement$offset3$cr = _placement$offset3.crossAxisShift) === null || _placement$offset3$cr === void 0 ? void 0 : _placement$offset3$cr.direction) !== null && _placement$offset$cro2 !== void 0 ? _placement$offset$cro2 : 'forwards'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
18
60
|
};
|
|
19
61
|
}
|
|
@@ -1,9 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchor positioning for top-layer popovers. See
|
|
3
|
+
* `notes/architecture/positioning.md` for the full model.
|
|
4
|
+
*
|
|
5
|
+
* Two runtime paths: CSS Anchor Positioning (modern browsers) and a JS
|
|
6
|
+
* fallback that measures the trigger and writes `top`/`left`.
|
|
7
|
+
*
|
|
8
|
+
* Both paths honour `offset.gap` and `offset.crossAxisShift`. The
|
|
9
|
+
* CSS path passes the values through as strings; the JS fallback resolves
|
|
10
|
+
* them to pixels via a hidden DOM probe (see `resolveCssLengthToPixels`)
|
|
11
|
+
* so tokens, `calc()`, `var()`, etc all work. The JS fallback hides the
|
|
12
|
+
* popover with `opacity: 0` until the first measurement completes, so it
|
|
13
|
+
* is never painted at the wrong location.
|
|
14
|
+
*/
|
|
1
15
|
import { useId, useLayoutEffect } from 'react';
|
|
2
16
|
import { bind } from 'bind-event-listener';
|
|
3
17
|
import rafSchedule from 'raf-schd';
|
|
4
18
|
import once from '@atlaskit/ds-lib/once';
|
|
5
19
|
import { computeFallbackPosition } from './anchor-positioning-fallback';
|
|
6
20
|
import { combine } from './combine';
|
|
21
|
+
import { resolveCssLengthToPixels } from './resolve-css-length-to-pixels';
|
|
7
22
|
import { getPlacement } from './resolve-placement';
|
|
8
23
|
import { setStyle } from './set-style';
|
|
9
24
|
export { getPlacement } from './resolve-placement';
|
|
@@ -91,6 +106,19 @@ export function placementToTryFallbacks({
|
|
|
91
106
|
return [`${sameEdge} span-${crossAxis}-end`, `${sameEdge} span-${crossAxis}-start`, flipKeyword, `${oppositeEdge} span-${crossAxis}-end`, `${oppositeEdge} span-${crossAxis}-start`].join(', ');
|
|
92
107
|
}
|
|
93
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Returns true when the popover is currently open. Wrapped because the
|
|
111
|
+
* `:popover-open` pseudo-class is not implemented in some test environments
|
|
112
|
+
* (e.g. jsdom), where `matches` throws on unknown selectors.
|
|
113
|
+
*/
|
|
114
|
+
function isPopoverOpen(popover) {
|
|
115
|
+
try {
|
|
116
|
+
return popover.matches(':popover-open');
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
94
122
|
/**
|
|
95
123
|
* Detects whether the browser supports CSS Anchor Positioning.
|
|
96
124
|
* Uses `once()` for lazy evaluation — safe for SSR and avoids hydration mismatches.
|
|
@@ -102,12 +130,6 @@ const supportsAnchorPositioning = once(() => {
|
|
|
102
130
|
return CSS.supports('anchor-name', '--a');
|
|
103
131
|
});
|
|
104
132
|
|
|
105
|
-
/**
|
|
106
|
-
* Default gap between the popover and its trigger (in pixels).
|
|
107
|
-
* Maps to `space.100` (8px) in the design token scale.
|
|
108
|
-
*/
|
|
109
|
-
const DEFAULT_OFFSET = 8;
|
|
110
|
-
|
|
111
133
|
/**
|
|
112
134
|
* Returns the CSS margin declaration that creates a gap between the
|
|
113
135
|
* popover and its trigger on the side facing the anchor.
|
|
@@ -123,7 +145,7 @@ function edgeMargin({
|
|
|
123
145
|
axis,
|
|
124
146
|
edge
|
|
125
147
|
} = placement;
|
|
126
|
-
const value =
|
|
148
|
+
const value = offset;
|
|
127
149
|
if (axis === 'block' && edge === 'end') {
|
|
128
150
|
return {
|
|
129
151
|
property: 'margin-block-start',
|
|
@@ -154,6 +176,36 @@ function edgeMargin({
|
|
|
154
176
|
};
|
|
155
177
|
}
|
|
156
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Cross-axis shift margin. Margin side matches the popover's anchored
|
|
181
|
+
* cross-axis edge: `align: 'start'` uses the START margin, `align: 'end'`
|
|
182
|
+
* uses the END margin with the sign inverted (so `forwards` is always
|
|
183
|
+
* positive), `align: 'center'` uses START. Margin on the un-anchored side
|
|
184
|
+
* has no effect under CSS Anchor Positioning.
|
|
185
|
+
*/
|
|
186
|
+
function crossAxisShiftMargin({
|
|
187
|
+
placement,
|
|
188
|
+
crossAxisShiftCssValue,
|
|
189
|
+
direction
|
|
190
|
+
}) {
|
|
191
|
+
const {
|
|
192
|
+
axis,
|
|
193
|
+
align
|
|
194
|
+
} = placement;
|
|
195
|
+
const directionSign = direction === 'forwards' ? 1 : -1;
|
|
196
|
+
const crossAxis = axis === 'block' ? 'inline' : 'block';
|
|
197
|
+
const useEndSide = align === 'end';
|
|
198
|
+
const side = useEndSide ? 'end' : 'start';
|
|
199
|
+
const sideSign = useEndSide ? -1 : 1;
|
|
200
|
+
const finalSign = directionSign * sideSign;
|
|
201
|
+
// Wrap any string in calc() with the sign factor. CSS handles the math.
|
|
202
|
+
const value = finalSign === 1 ? crossAxisShiftCssValue : `calc(-1 * ${crossAxisShiftCssValue})`;
|
|
203
|
+
return {
|
|
204
|
+
property: `margin-${crossAxis}-${side}`,
|
|
205
|
+
value
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
157
209
|
/**
|
|
158
210
|
* Hook that positions an element relative to an anchor element using
|
|
159
211
|
* CSS Anchor Positioning (with a JS fallback for older browsers).
|
|
@@ -174,7 +226,6 @@ export function useAnchorPosition({
|
|
|
174
226
|
anchorRef,
|
|
175
227
|
popoverRef,
|
|
176
228
|
placement,
|
|
177
|
-
offset = DEFAULT_OFFSET,
|
|
178
229
|
forceFallbackPositioning = false,
|
|
179
230
|
arrow
|
|
180
231
|
}) {
|
|
@@ -189,11 +240,18 @@ export function useAnchorPosition({
|
|
|
189
240
|
const resolvedPlacement = getPlacement({
|
|
190
241
|
placement
|
|
191
242
|
});
|
|
243
|
+
const gapCssValue = resolvedPlacement.offset.gap;
|
|
244
|
+
const crossAxisShiftCssValue = resolvedPlacement.offset.crossAxisShift.value;
|
|
192
245
|
const shouldForceFallback = Boolean(forceFallbackPositioning);
|
|
193
246
|
if (supportsAnchorPositioning() && !shouldForceFallback) {
|
|
194
247
|
const gap = edgeMargin({
|
|
195
248
|
placement: resolvedPlacement,
|
|
196
|
-
offset
|
|
249
|
+
offset: gapCssValue
|
|
250
|
+
});
|
|
251
|
+
const crossAxisShift = crossAxisShiftMargin({
|
|
252
|
+
placement: resolvedPlacement,
|
|
253
|
+
crossAxisShiftCssValue,
|
|
254
|
+
direction: resolvedPlacement.offset.crossAxisShift.direction
|
|
197
255
|
});
|
|
198
256
|
const popoverStyles = [{
|
|
199
257
|
property: 'position-anchor',
|
|
@@ -220,17 +278,62 @@ export function useAnchorPosition({
|
|
|
220
278
|
property: 'inset',
|
|
221
279
|
value: 'auto'
|
|
222
280
|
},
|
|
223
|
-
//
|
|
281
|
+
// Main-axis gap between the popover and its trigger
|
|
224
282
|
{
|
|
225
283
|
property: gap.property,
|
|
226
284
|
value: gap.value
|
|
285
|
+
},
|
|
286
|
+
// Cross-axis shift
|
|
287
|
+
{
|
|
288
|
+
property: crossAxisShift.property,
|
|
289
|
+
value: crossAxisShift.value
|
|
227
290
|
}];
|
|
291
|
+
|
|
292
|
+
// Surface the active cross-axis shift value as a custom property so
|
|
293
|
+
// each named arrow @position-try rule can re-apply it after a flip.
|
|
294
|
+
// The four properties cover (cross-axis, side); only one is non-zero
|
|
295
|
+
// at a time.
|
|
296
|
+
popoverStyles.push({
|
|
297
|
+
property: '--ds-cross-axis-shift-margin-start',
|
|
298
|
+
value: '0px'
|
|
299
|
+
});
|
|
300
|
+
popoverStyles.push({
|
|
301
|
+
property: '--ds-cross-axis-shift-margin-end',
|
|
302
|
+
value: '0px'
|
|
303
|
+
});
|
|
304
|
+
popoverStyles.push({
|
|
305
|
+
property: '--ds-cross-axis-shift-margin-block-start',
|
|
306
|
+
value: '0px'
|
|
307
|
+
});
|
|
308
|
+
popoverStyles.push({
|
|
309
|
+
property: '--ds-cross-axis-shift-margin-block-end',
|
|
310
|
+
value: '0px'
|
|
311
|
+
});
|
|
312
|
+
const crossAxisShiftActive = crossAxisShiftMargin({
|
|
313
|
+
placement: resolvedPlacement,
|
|
314
|
+
crossAxisShiftCssValue,
|
|
315
|
+
direction: resolvedPlacement.offset.crossAxisShift.direction
|
|
316
|
+
});
|
|
317
|
+
// Map margin property to its corresponding custom property name.
|
|
318
|
+
const customPropertyByMarginProperty = {
|
|
319
|
+
'margin-inline-start': '--ds-cross-axis-shift-margin-start',
|
|
320
|
+
'margin-inline-end': '--ds-cross-axis-shift-margin-end',
|
|
321
|
+
'margin-block-start': '--ds-cross-axis-shift-margin-block-start',
|
|
322
|
+
'margin-block-end': '--ds-cross-axis-shift-margin-block-end'
|
|
323
|
+
};
|
|
324
|
+
const crossAxisShiftCustomProperty = customPropertyByMarginProperty[crossAxisShiftActive.property];
|
|
325
|
+
if (crossAxisShiftCustomProperty) {
|
|
326
|
+
popoverStyles.push({
|
|
327
|
+
property: crossAxisShiftCustomProperty,
|
|
328
|
+
value: crossAxisShiftActive.value
|
|
329
|
+
});
|
|
330
|
+
}
|
|
228
331
|
if (arrow) {
|
|
229
332
|
// Set the arrow size variable used by the injected arrow CSS
|
|
230
333
|
// and the @position-try rules for margin values.
|
|
231
334
|
popoverStyles.push({
|
|
232
335
|
property: '--ds-arrow-size',
|
|
233
|
-
value:
|
|
336
|
+
value: gapCssValue
|
|
234
337
|
});
|
|
235
338
|
// Mark the popover root as the arrow host. The ARROW_CSS targets
|
|
236
339
|
// [data-ds-popover-arrow] to apply clip-path: inset(0) margin-box
|
|
@@ -266,6 +369,7 @@ export function useAnchorPosition({
|
|
|
266
369
|
}]
|
|
267
370
|
});
|
|
268
371
|
function update() {
|
|
372
|
+
var _popover$parentElemen;
|
|
269
373
|
if (!trigger || !popover) {
|
|
270
374
|
return;
|
|
271
375
|
}
|
|
@@ -274,6 +378,22 @@ export function useAnchorPosition({
|
|
|
274
378
|
width: window.innerWidth,
|
|
275
379
|
height: window.innerHeight
|
|
276
380
|
};
|
|
381
|
+
|
|
382
|
+
// Resolve consumer-supplied CSS length strings (tokens, calc, var,
|
|
383
|
+
// px, rem, etc) to pixels. The probe is mounted next to the popover
|
|
384
|
+
// so it inherits the same custom-property scope and font size.
|
|
385
|
+
// `popover.parentElement` is guaranteed to exist for any rendered
|
|
386
|
+
// popover (the popover itself is appended somewhere in the tree),
|
|
387
|
+
// but fall back to the popover element if not for safety.
|
|
388
|
+
const probeContainer = (_popover$parentElemen = popover.parentElement) !== null && _popover$parentElemen !== void 0 ? _popover$parentElemen : popover;
|
|
389
|
+
const gapPx = resolveCssLengthToPixels({
|
|
390
|
+
value: gapCssValue,
|
|
391
|
+
container: probeContainer
|
|
392
|
+
});
|
|
393
|
+
const crossAxisShiftPx = resolveCssLengthToPixels({
|
|
394
|
+
value: crossAxisShiftCssValue,
|
|
395
|
+
container: probeContainer
|
|
396
|
+
});
|
|
277
397
|
const {
|
|
278
398
|
top,
|
|
279
399
|
left
|
|
@@ -282,34 +402,61 @@ export function useAnchorPosition({
|
|
|
282
402
|
popoverEl: popover,
|
|
283
403
|
placement: resolvedPlacement,
|
|
284
404
|
viewport,
|
|
285
|
-
|
|
405
|
+
gap: gapPx,
|
|
406
|
+
crossAxisShift: {
|
|
407
|
+
value: crossAxisShiftPx,
|
|
408
|
+
direction: resolvedPlacement.offset.crossAxisShift.direction
|
|
409
|
+
}
|
|
286
410
|
});
|
|
287
411
|
popover.style.setProperty('top', `${top}px`);
|
|
288
412
|
popover.style.setProperty('left', `${left}px`);
|
|
413
|
+
// Reveal the popover only after it has been positioned.
|
|
414
|
+
// See the toggle listener below for why we hide on open.
|
|
415
|
+
popover.style.removeProperty('opacity');
|
|
289
416
|
}
|
|
290
417
|
|
|
291
418
|
// Throttle scroll/resize updates to one per animation frame
|
|
292
419
|
const scheduledUpdate = rafSchedule(update);
|
|
293
|
-
|
|
420
|
+
|
|
421
|
+
// Wait for the FIRST valid layout before measuring: the popover
|
|
422
|
+
// is `display: none` until `showPopover()`, and some browsers
|
|
423
|
+
// fire `toggle` before layout, so RAF-after-toggle is unreliable.
|
|
424
|
+
// ResizeObserver fires once the browser has real dimensions.
|
|
425
|
+
// Self-disconnects after one valid measurement; ongoing
|
|
426
|
+
// scroll/resize is handled by the window listeners below.
|
|
427
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
428
|
+
if (popover.offsetWidth > 0 && popover.offsetHeight > 0) {
|
|
429
|
+
update();
|
|
430
|
+
resizeObserver.disconnect();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// If the popover is already open by the time this effect runs (for
|
|
435
|
+
// example, the parent `useAnchorPosition` effect runs after the child
|
|
436
|
+
// `Popover` effect that called `showPopover()`), the initial `toggle`
|
|
437
|
+
// event has already fired and our listener below would miss it. Start
|
|
438
|
+
// observing immediately so the first measurement still happens.
|
|
439
|
+
if (isPopoverOpen(popover)) {
|
|
440
|
+
popover.style.setProperty('opacity', '0');
|
|
441
|
+
resizeObserver.observe(popover);
|
|
442
|
+
}
|
|
294
443
|
const undoPositioning = combine(cleanupBaseStyles,
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
// `
|
|
299
|
-
//
|
|
300
|
-
// `
|
|
301
|
-
// the element has been laid out — so dimensions are still 0.
|
|
302
|
-
// Waiting one frame guarantees layout has run and measurements
|
|
303
|
-
// are accurate.
|
|
444
|
+
// On every open: hide synchronously so the user never sees
|
|
445
|
+
// the UA-default position, then re-observe for a fresh
|
|
446
|
+
// measurement. We use `opacity: 0` rather than
|
|
447
|
+
// `visibility: hidden` because Firefox skips visibility-hidden
|
|
448
|
+
// elements during `<dialog>` initial-focus traversal — see
|
|
449
|
+
// `form-in-popup.spec.tsx` on `desktop-firefox`.
|
|
304
450
|
bind(popover, {
|
|
305
451
|
type: 'toggle',
|
|
306
452
|
listener: event => {
|
|
307
453
|
const toggleEvent = event;
|
|
308
454
|
if (toggleEvent.newState === 'open') {
|
|
309
|
-
|
|
455
|
+
popover.style.setProperty('opacity', '0');
|
|
456
|
+
resizeObserver.observe(popover);
|
|
310
457
|
}
|
|
311
458
|
}
|
|
312
|
-
}), bind(window, {
|
|
459
|
+
}), () => resizeObserver.disconnect(), bind(window, {
|
|
313
460
|
type: 'scroll',
|
|
314
461
|
listener: scheduledUpdate,
|
|
315
462
|
options: {
|
|
@@ -324,12 +471,10 @@ export function useAnchorPosition({
|
|
|
324
471
|
}
|
|
325
472
|
}), () => {
|
|
326
473
|
scheduledUpdate.cancel();
|
|
327
|
-
if (pendingFrame !== null) {
|
|
328
|
-
cancelAnimationFrame(pendingFrame);
|
|
329
|
-
}
|
|
330
474
|
popover.style.removeProperty('top');
|
|
331
475
|
popover.style.removeProperty('left');
|
|
476
|
+
popover.style.removeProperty('opacity');
|
|
332
477
|
});
|
|
333
478
|
return undoPositioning;
|
|
334
|
-
}, [anchorRef, popoverRef, placement, anchorName,
|
|
479
|
+
}, [anchorRef, popoverRef, placement, anchorName, forceFallbackPositioning, arrow]);
|
|
335
480
|
}
|
|
@@ -128,12 +128,34 @@ const placementMapping = {
|
|
|
128
128
|
};
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
* Convert a legacy Popper.js placement to the new
|
|
131
|
+
* Convert a legacy Popper.js placement to the new `position-area`-based Placement.
|
|
132
|
+
* Optional `offset` is the legacy Popper `[along, away]` tuple (also known as
|
|
133
|
+
* `[skidding, distance]`); negative `along` becomes `direction: 'backwards'`.
|
|
134
|
+
*
|
|
135
|
+
* The tuple keeps the historical popper names so call sites that already pass
|
|
136
|
+
* `[along, away]` migrate without thinking; the new internal field names
|
|
137
|
+
* (`gap` / `crossAxisShift`) are produced inside this adapter.
|
|
132
138
|
*/
|
|
133
139
|
export function fromLegacyPlacement({
|
|
134
|
-
legacy
|
|
140
|
+
legacy,
|
|
141
|
+
offset
|
|
135
142
|
}) {
|
|
136
|
-
|
|
143
|
+
const basePlacement = placementMapping[legacy];
|
|
144
|
+
if (!offset) {
|
|
145
|
+
return basePlacement;
|
|
146
|
+
}
|
|
147
|
+
const [along, away] = offset;
|
|
148
|
+
return {
|
|
149
|
+
...basePlacement,
|
|
150
|
+
offset: {
|
|
151
|
+
gap: away,
|
|
152
|
+
crossAxisShift: {
|
|
153
|
+
// Sign is captured in `direction`; `value` is the magnitude.
|
|
154
|
+
value: Math.abs(along),
|
|
155
|
+
direction: along >= 0 ? 'forwards' : 'backwards'
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
137
159
|
}
|
|
138
160
|
|
|
139
161
|
/**
|
|
@@ -30,7 +30,6 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
30
30
|
placement: placementProp,
|
|
31
31
|
onClose: onCloseProp,
|
|
32
32
|
testId: testIdProp,
|
|
33
|
-
offset: offsetProp,
|
|
34
33
|
forceFallbackPositioning: forceFallbackPositioningProp
|
|
35
34
|
}, ref) {
|
|
36
35
|
var _ref, _ref2, _ctx$popoverRef;
|
|
@@ -129,7 +128,6 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
|
|
|
129
128
|
},
|
|
130
129
|
popoverRef,
|
|
131
130
|
placement,
|
|
132
|
-
offset: offsetProp,
|
|
133
131
|
forceFallbackPositioning,
|
|
134
132
|
arrow: arrowPreset !== null && arrowPreset !== void 0 ? arrowPreset : undefined
|
|
135
133
|
});
|
package/dist/esm/arrow/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { getPlacement, placementToPositionArea } from '../internal/use-anchor-po
|
|
|
19
19
|
*
|
|
20
20
|
* @see https://goulet.dev/posts/tooltip-with-popover-and-anchor-positioning/
|
|
21
21
|
*/
|
|
22
|
-
var ARROW_CSS = "\n[data-ds-popover-arrow] {\n clip-path: inset(var(--ds-arrow-offset, 0px)) margin-box;\n box-shadow: none;\n}\n\n/* Top and Bottom arrows (vertical hexagon) */\n[data-ds-popover-arrow]::before {\n content: \"\";\n position: absolute;\n z-index: -1;\n background: inherit;\n left: 50%;\n transform: translateX(-50%);\n width: calc(var(--ds-arrow-size, 8px) * 2);\n height: calc(100% + var(--ds-arrow-size, 8px) * 2);\n top: calc(var(--ds-arrow-size, 8px) * -1);\n clip-path: polygon(\n 0 var(--ds-arrow-size, 8px),\n 50% 0,\n 100% var(--ds-arrow-size, 8px),\n 100% calc(100% - var(--ds-arrow-size, 8px)),\n 50% 100%,\n 0 calc(100% - var(--ds-arrow-size, 8px))\n );\n}\n\n/* Left and Right arrows (horizontal hexagon) */\n[data-ds-popover-arrow]::after {\n content: \"\";\n position: absolute;\n z-index: -1;\n background: inherit;\n top: 50%;\n transform: translateY(-50%);\n height: calc(var(--ds-arrow-size, 8px) * 2);\n width: calc(100% + var(--ds-arrow-size, 8px) * 2);\n left: calc(var(--ds-arrow-size, 8px) * -1);\n clip-path: polygon(\n var(--ds-arrow-size, 8px) 0,\n calc(100% - var(--ds-arrow-size, 8px)) 0,\n 100% 50%,\n calc(100% - var(--ds-arrow-size, 8px)) 100%,\n var(--ds-arrow-size, 8px) 100%,\n 0 50%\n );\n}\n\n/* \u2500\u2500 @position-try rules \u2500\u2500\n * One rule per possible position-area value.\n * Each sets the position-area and the correct margin direction\n * so the arrow on the anchor-facing side is revealed.\n *\n * Margin direction per edge:\n * block-start (above anchor) \u2192 margin-block-end (bottom, facing anchor)\n * block-end (below anchor) \u2192 margin-block-start (top, facing anchor)\n * inline-start (left of anchor) \u2192 margin-inline-end (right, facing anchor)\n * inline-end (right of anchor) \u2192 margin-inline-start (left, facing anchor)\n */\n\n/* Single-axis centered */\n@position-try --ds-arrow-block-start {\n position-area: block-start;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-block-end {\n position-area: block-end;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-inline-start {\n position-area: inline-start;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-inline-end {\n position-area: inline-end;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n}\n\n/* Block edge + inline span */\n@position-try --ds-arrow-block-start-span-inline-start {\n position-area: block-start span-inline-start;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-block-start-span-inline-end {\n position-area: block-start span-inline-end;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-block-end-span-inline-start {\n position-area: block-end span-inline-start;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-block-end-span-inline-end {\n position-area: block-end span-inline-end;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n}\n\n/* Inline edge + block span */\n@position-try --ds-arrow-inline-start-span-block-start {\n position-area: inline-start span-block-start;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-inline-start-span-block-end {\n position-area: inline-start span-block-end;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-inline-end-span-block-start {\n position-area: inline-end span-block-start;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n}\n@position-try --ds-arrow-inline-end-span-block-end {\n position-area: inline-end span-block-end;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n}\n";
|
|
22
|
+
var ARROW_CSS = "\n[data-ds-popover-arrow] {\n clip-path: inset(var(--ds-arrow-offset, 0px)) margin-box;\n box-shadow: none;\n}\n\n/* Top and Bottom arrows (vertical hexagon) */\n[data-ds-popover-arrow]::before {\n content: \"\";\n position: absolute;\n z-index: -1;\n background: inherit;\n left: 50%;\n transform: translateX(-50%);\n width: calc(var(--ds-arrow-size, 8px) * 2);\n height: calc(100% + var(--ds-arrow-size, 8px) * 2);\n top: calc(var(--ds-arrow-size, 8px) * -1);\n clip-path: polygon(\n 0 var(--ds-arrow-size, 8px),\n 50% 0,\n 100% var(--ds-arrow-size, 8px),\n 100% calc(100% - var(--ds-arrow-size, 8px)),\n 50% 100%,\n 0 calc(100% - var(--ds-arrow-size, 8px))\n );\n}\n\n/* Left and Right arrows (horizontal hexagon) */\n[data-ds-popover-arrow]::after {\n content: \"\";\n position: absolute;\n z-index: -1;\n background: inherit;\n top: 50%;\n transform: translateY(-50%);\n height: calc(var(--ds-arrow-size, 8px) * 2);\n width: calc(100% + var(--ds-arrow-size, 8px) * 2);\n left: calc(var(--ds-arrow-size, 8px) * -1);\n clip-path: polygon(\n var(--ds-arrow-size, 8px) 0,\n calc(100% - var(--ds-arrow-size, 8px)) 0,\n 100% 50%,\n calc(100% - var(--ds-arrow-size, 8px)) 100%,\n var(--ds-arrow-size, 8px) 100%,\n 0 50%\n );\n}\n\n/* \u2500\u2500 @position-try rules \u2500\u2500\n * One rule per possible position-area value.\n * Each sets the position-area and the correct margin direction\n * so the arrow on the anchor-facing side is revealed.\n *\n * Margin direction per edge:\n * block-start (above anchor) \u2192 margin-block-end (bottom, facing anchor)\n * block-end (below anchor) \u2192 margin-block-start (top, facing anchor)\n * inline-start (left of anchor) \u2192 margin-inline-end (right, facing anchor)\n * inline-end (right of anchor) \u2192 margin-inline-start (left, facing anchor)\n */\n\n/* Each rule sets two margins: the anchor-facing margin for the gap\n (via --ds-arrow-size) and one cross-axis margin for the shift\n (via one of --ds-cross-axis-shift-margin-start, --ds-cross-axis-shift-margin-end,\n --ds-cross-axis-shift-margin-block-start, or --ds-cross-axis-shift-margin-block-end). The\n side matches the popover's anchored cross-axis edge per align. Only\n one of the four properties is non-zero at a time; useAnchorPosition\n picks it. See notes/architecture/positioning.md for details. */\n/* Single-axis centered (align: center) */\n@position-try --ds-arrow-block-start {\n position-area: block-start;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n@position-try --ds-arrow-block-end {\n position-area: block-end;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n@position-try --ds-arrow-inline-start {\n position-area: inline-start;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n@position-try --ds-arrow-inline-end {\n position-area: inline-end;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n\n/* Block edge + inline span (align: start uses span-inline-end; align: end uses span-inline-start) */\n@position-try --ds-arrow-block-start-span-inline-start {\n position-area: block-start span-inline-start;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n margin-inline-end: var(--ds-cross-axis-shift-margin-end, 0px);\n}\n@position-try --ds-arrow-block-start-span-inline-end {\n position-area: block-start span-inline-end;\n margin: 0;\n margin-block-end: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n@position-try --ds-arrow-block-end-span-inline-start {\n position-area: block-end span-inline-start;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n margin-inline-end: var(--ds-cross-axis-shift-margin-end, 0px);\n}\n@position-try --ds-arrow-block-end-span-inline-end {\n position-area: block-end span-inline-end;\n margin: 0;\n margin-block-start: var(--ds-arrow-size, 8px);\n margin-inline-start: var(--ds-cross-axis-shift-margin-start, 0px);\n}\n\n/* Inline edge + block span (align: start uses span-block-end; align: end uses span-block-start) */\n@position-try --ds-arrow-inline-start-span-block-start {\n position-area: inline-start span-block-start;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n margin-block-end: var(--ds-cross-axis-shift-margin-block-end, 0px);\n}\n@position-try --ds-arrow-inline-start-span-block-end {\n position-area: inline-start span-block-end;\n margin: 0;\n margin-inline-end: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n@position-try --ds-arrow-inline-end-span-block-start {\n position-area: inline-end span-block-start;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n margin-block-end: var(--ds-cross-axis-shift-margin-block-end, 0px);\n}\n@position-try --ds-arrow-inline-end-span-block-end {\n position-area: inline-end span-block-end;\n margin: 0;\n margin-inline-start: var(--ds-arrow-size, 8px);\n margin-block-start: var(--ds-cross-axis-shift-margin-block-start, 0px);\n}\n";
|
|
23
23
|
|
|
24
24
|
// ── Placement utilities ──
|
|
25
25
|
|