@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cjs/arrow/index.js +1 -1
  3. package/dist/cjs/internal/anchor-positioning-fallback.js +82 -22
  4. package/dist/cjs/internal/resolve-css-length-to-pixels.js +53 -0
  5. package/dist/cjs/internal/resolve-css-length.js +18 -0
  6. package/dist/cjs/internal/resolve-placement.js +47 -6
  7. package/dist/cjs/internal/use-anchor-positioning.js +180 -36
  8. package/dist/cjs/placement-map/index.js +31 -3
  9. package/dist/cjs/popup/popup-content.js +0 -2
  10. package/dist/es2019/arrow/index.js +22 -3
  11. package/dist/es2019/internal/anchor-positioning-fallback.js +80 -17
  12. package/dist/es2019/internal/resolve-css-length-to-pixels.js +48 -0
  13. package/dist/es2019/internal/resolve-css-length.js +13 -0
  14. package/dist/es2019/internal/resolve-placement.js +48 -6
  15. package/dist/es2019/internal/use-anchor-positioning.js +173 -28
  16. package/dist/es2019/placement-map/index.js +25 -3
  17. package/dist/es2019/popup/popup-content.js +0 -2
  18. package/dist/esm/arrow/index.js +1 -1
  19. package/dist/esm/internal/anchor-positioning-fallback.js +82 -22
  20. package/dist/esm/internal/resolve-css-length-to-pixels.js +47 -0
  21. package/dist/esm/internal/resolve-css-length.js +12 -0
  22. package/dist/esm/internal/resolve-placement.js +48 -6
  23. package/dist/esm/internal/use-anchor-positioning.js +179 -36
  24. package/dist/esm/placement-map/index.js +30 -3
  25. package/dist/esm/popup/popup-content.js +0 -2
  26. package/dist/types/internal/anchor-positioning-fallback.d.ts +11 -9
  27. package/dist/types/internal/resolve-css-length-to-pixels.d.ts +22 -0
  28. package/dist/types/internal/resolve-css-length.d.ts +8 -0
  29. package/dist/types/internal/resolve-placement.d.ts +47 -5
  30. package/dist/types/internal/use-anchor-positioning.d.ts +18 -7
  31. package/dist/types/placement-map/index.d.ts +11 -5
  32. package/dist/types/popup/types.d.ts +15 -30
  33. package/dist/types-ts4.5/internal/anchor-positioning-fallback.d.ts +11 -9
  34. package/dist/types-ts4.5/internal/resolve-css-length-to-pixels.d.ts +22 -0
  35. package/dist/types-ts4.5/internal/resolve-css-length.d.ts +8 -0
  36. package/dist/types-ts4.5/internal/resolve-placement.d.ts +47 -5
  37. package/dist/types-ts4.5/internal/use-anchor-positioning.d.ts +18 -7
  38. package/dist/types-ts4.5/placement-map/index.d.ts +14 -5
  39. package/dist/types-ts4.5/popup/types.d.ts +15 -30
  40. package/package.json +2 -2
@@ -1,9 +1,23 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
3
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
+ /**
5
+ * JS fallback positioning math. Used only when the browser does not
6
+ * support CSS Anchor Positioning (less than 6% of users).
7
+ *
8
+ * Consumer-supplied `offset.gap` and `offset.crossAxisShift` are
9
+ * honored: the caller (`useAnchorPosition`) resolves CSS length strings
10
+ * (tokens, calc, var, etc) to pixels via a DOM probe before passing them
11
+ * in. The caller also keeps the popover hidden (`opacity: 0`) until the
12
+ * first measurement completes, so the popover never paints at the wrong
13
+ * position.
14
+ */
15
+
4
16
  /**
5
17
  * Computes the base {top, left} position based on the primary edge,
6
18
  * flipping to the opposite side when there is not enough space.
19
+ *
20
+ * `gap` is applied along the main axis between trigger and popover.
7
21
  */
8
22
  function computeEdgePosition(_ref) {
9
23
  var axis = _ref.axis,
@@ -12,13 +26,15 @@ function computeEdgePosition(_ref) {
12
26
  popoverWidth = _ref.popoverWidth,
13
27
  popoverHeight = _ref.popoverHeight,
14
28
  viewport = _ref.viewport,
15
- offset = _ref.offset;
29
+ gap = _ref.gap;
16
30
  if (axis === 'block' && edge === 'end') {
17
31
  // Below trigger
18
32
  var spaceBelow = viewport.height - triggerRect.bottom;
19
33
  var spaceAbove = triggerRect.top;
34
+ var fitsBelow = spaceBelow >= popoverHeight + gap;
35
+ var useBelow = fitsBelow || spaceBelow >= spaceAbove;
20
36
  return {
21
- top: spaceBelow >= popoverHeight + offset || spaceBelow >= spaceAbove ? triggerRect.bottom + offset : triggerRect.top - popoverHeight - offset,
37
+ top: useBelow ? triggerRect.bottom + gap : triggerRect.top - popoverHeight - gap,
22
38
  left: triggerRect.left + triggerRect.width / 2 - popoverWidth / 2
23
39
  };
24
40
  }
@@ -26,8 +42,10 @@ function computeEdgePosition(_ref) {
26
42
  // Above trigger
27
43
  var _spaceAbove = triggerRect.top;
28
44
  var _spaceBelow = viewport.height - triggerRect.bottom;
45
+ var fitsAbove = _spaceAbove >= popoverHeight + gap;
46
+ var useAbove = fitsAbove || _spaceAbove >= _spaceBelow;
29
47
  return {
30
- top: _spaceAbove >= popoverHeight + offset || _spaceAbove >= _spaceBelow ? triggerRect.top - popoverHeight - offset : triggerRect.bottom + offset,
48
+ top: useAbove ? triggerRect.top - popoverHeight - gap : triggerRect.bottom + gap,
31
49
  left: triggerRect.left + triggerRect.width / 2 - popoverWidth / 2
32
50
  };
33
51
  }
@@ -35,24 +53,28 @@ function computeEdgePosition(_ref) {
35
53
  // Right of trigger
36
54
  var spaceRight = viewport.width - triggerRect.right;
37
55
  var spaceLeft = triggerRect.left;
56
+ var fitsRight = spaceRight >= popoverWidth + gap;
57
+ var useRight = fitsRight || spaceRight >= spaceLeft;
38
58
  return {
39
59
  top: triggerRect.top + triggerRect.height / 2 - popoverHeight / 2,
40
- left: spaceRight >= popoverWidth + offset || spaceRight >= spaceLeft ? triggerRect.right + offset : triggerRect.left - popoverWidth - offset
60
+ left: useRight ? triggerRect.right + gap : triggerRect.left - popoverWidth - gap
41
61
  };
42
62
  }
43
63
  if (axis === 'inline' && edge === 'start') {
44
64
  // Left of trigger
45
65
  var _spaceLeft = triggerRect.left;
46
66
  var _spaceRight = viewport.width - triggerRect.right;
67
+ var fitsLeft = _spaceLeft >= popoverWidth + gap;
68
+ var useLeft = fitsLeft || _spaceLeft >= _spaceRight;
47
69
  return {
48
70
  top: triggerRect.top + triggerRect.height / 2 - popoverHeight / 2,
49
- left: _spaceLeft >= popoverWidth + offset || _spaceLeft >= _spaceRight ? triggerRect.left - popoverWidth - offset : triggerRect.right + offset
71
+ left: useLeft ? triggerRect.left - popoverWidth - gap : triggerRect.right + gap
50
72
  };
51
73
  }
52
74
 
53
75
  // Unreachable for valid placements; defaults to below trigger
54
76
  return {
55
- top: triggerRect.bottom + offset,
77
+ top: triggerRect.bottom + gap,
56
78
  left: triggerRect.left + triggerRect.width / 2 - popoverWidth / 2
57
79
  };
58
80
  }
@@ -99,26 +121,58 @@ function applyAlignment(_ref2) {
99
121
  }
100
122
 
101
123
  /**
102
- * JS fallback: compute popover position from trigger rect and viewport.
124
+ * Applies the cross-axis shift offset.
103
125
  *
104
- * Placement algorithm (from the document):
105
- * - "block" placement: show above/below, picking the side with more space if flipping needed
106
- * - "inline" placement: show left/right, picking the side with more space if flipping needed
107
- * - "auto" placement: show on the edge with the most available space
126
+ * The sign mirrors the CSS path (see `crossAxisShiftMargin` in
127
+ * `use-anchor-positioning.tsx`):
128
+ * - `align: 'start' | 'center'` + `forwards` → push toward end (positive)
129
+ * - `align: 'end'` + `forwards` → push toward start (negative)
130
+ * - `backwards` flips the sign in both cases.
131
+ */
132
+ function applyCrossAxisShift(_ref3) {
133
+ var position = _ref3.position,
134
+ axis = _ref3.axis,
135
+ align = _ref3.align,
136
+ crossAxisShift = _ref3.crossAxisShift;
137
+ if (crossAxisShift.value === 0) {
138
+ return position;
139
+ }
140
+ var directionSign = crossAxisShift.direction === 'forwards' ? 1 : -1;
141
+ var sideSign = align === 'end' ? -1 : 1;
142
+ var delta = crossAxisShift.value * directionSign * sideSign;
143
+
144
+ // Cross-axis is the OPPOSITE of the placement axis.
145
+ if (axis === 'block') {
146
+ return _objectSpread(_objectSpread({}, position), {}, {
147
+ left: position.left + delta
148
+ });
149
+ }
150
+ return _objectSpread(_objectSpread({}, position), {}, {
151
+ top: position.top + delta
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Computes popover position from trigger rect and viewport, flipping to
157
+ * the side with more space when needed. Re-runs on scroll/resize.
108
158
  *
109
- * Re-runs on scroll (capture on window) and resize events.
159
+ * `gap` and `crossAxisShift` are pre-resolved to pixels by the
160
+ * caller (see `resolveCssLengthToPixels`), so this function stays purely
161
+ * numeric.
110
162
  */
111
- export function computeFallbackPosition(_ref3) {
112
- var triggerRect = _ref3.triggerRect,
113
- popoverEl = _ref3.popoverEl,
114
- placement = _ref3.placement,
115
- viewport = _ref3.viewport,
116
- offset = _ref3.offset;
163
+ export function computeFallbackPosition(_ref4) {
164
+ var triggerRect = _ref4.triggerRect,
165
+ popoverEl = _ref4.popoverEl,
166
+ placement = _ref4.placement,
167
+ viewport = _ref4.viewport,
168
+ gap = _ref4.gap,
169
+ crossAxisShift = _ref4.crossAxisShift;
117
170
  var axis = placement.axis,
118
171
  edge = placement.edge,
119
172
  align = placement.align;
120
173
 
121
- // Measure popover dimensions (it might not have layout yet if just shown)
174
+ // Measure popover dimensions. Callers must ensure the popover has
175
+ // laid out before calling this (it should not be `display: none`).
122
176
  var popoverWidth = popoverEl.offsetWidth;
123
177
  var popoverHeight = popoverEl.offsetHeight;
124
178
  var basePosition = computeEdgePosition({
@@ -128,7 +182,7 @@ export function computeFallbackPosition(_ref3) {
128
182
  popoverWidth: popoverWidth,
129
183
  popoverHeight: popoverHeight,
130
184
  viewport: viewport,
131
- offset: offset
185
+ gap: gap
132
186
  });
133
187
  var alignedPosition = applyAlignment({
134
188
  position: basePosition,
@@ -138,10 +192,16 @@ export function computeFallbackPosition(_ref3) {
138
192
  popoverWidth: popoverWidth,
139
193
  popoverHeight: popoverHeight
140
194
  });
195
+ var shiftedPosition = applyCrossAxisShift({
196
+ position: alignedPosition,
197
+ axis: axis,
198
+ align: align,
199
+ crossAxisShift: crossAxisShift
200
+ });
141
201
 
142
202
  // Clamp to viewport so the popover is never offscreen
143
203
  return {
144
- top: Math.max(0, Math.min(alignedPosition.top, viewport.height - popoverHeight)),
145
- left: Math.max(0, Math.min(alignedPosition.left, viewport.width - popoverWidth))
204
+ top: Math.max(0, Math.min(shiftedPosition.top, viewport.height - popoverHeight)),
205
+ left: Math.max(0, Math.min(shiftedPosition.left, viewport.width - popoverWidth))
146
206
  };
147
207
  }
@@ -0,0 +1,47 @@
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(_ref) {
20
+ var value = _ref.value,
21
+ container = _ref.container;
22
+ if (typeof value === 'number') {
23
+ return value;
24
+ }
25
+
26
+ // Fast path: bare `${n}px` strings come from the consumer normalisation
27
+ // in `getPlacement`. Avoid a reflow when we can.
28
+ var pxMatch = value.match(/^(-?\d+(?:\.\d+)?)px$/);
29
+ if (pxMatch) {
30
+ return Number(pxMatch[1]);
31
+ }
32
+ var probe = container.ownerDocument.createElement('div');
33
+ probe.style.position = 'absolute';
34
+ probe.style.visibility = 'hidden';
35
+ probe.style.pointerEvents = 'none';
36
+ probe.style.height = '0';
37
+ probe.style.padding = '0';
38
+ probe.style.border = '0';
39
+ // `width` clamps negatives to 0; use `margin-left` instead so signs are
40
+ // preserved. `getComputedStyle` returns the resolved pixel value for
41
+ // margin-* properties even when the source was a token / calc / var.
42
+ probe.style.marginLeft = value;
43
+ container.appendChild(probe);
44
+ var resolved = parseFloat(getComputedStyle(probe).marginLeft);
45
+ container.removeChild(probe);
46
+ return Number.isFinite(resolved) ? resolved : 0;
47
+ }
@@ -0,0 +1,12 @@
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(_ref) {
7
+ var value = _ref.value;
8
+ if (typeof value === 'number') {
9
+ return "".concat(value, "px");
10
+ }
11
+ return value;
12
+ }
@@ -1,18 +1,60 @@
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
+ var 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
- * Fully-resolved placement (internal). All fields are required.
3
- * Use getPlacement({ placement: partial }) to obtain from partial input.
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
- * Defaults: `axis: 'block'`, `edge: 'end'`, `align: 'center'`.
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(_ref) {
11
- var _placement$axis, _placement$edge, _placement$align;
41
+ var _placement$offset, _placement$axis, _placement$edge, _placement$align, _placement$offset$cro, _placement$offset2, _placement$offset$cro2, _placement$offset3;
12
42
  var placement = _ref.placement;
43
+ var consumerGap = (_placement$offset = placement.offset) === null || _placement$offset === void 0 ? void 0 : _placement$offset.gap;
13
44
  return {
14
45
  axis: (_placement$axis = placement.axis) !== null && _placement$axis !== void 0 ? _placement$axis : 'block',
15
46
  edge: (_placement$edge = placement.edge) !== null && _placement$edge !== void 0 ? _placement$edge : 'end',
16
- align: (_placement$align = placement.align) !== null && _placement$align !== void 0 ? _placement$align : 'center'
47
+ align: (_placement$align = placement.align) !== null && _placement$align !== void 0 ? _placement$align : 'center',
48
+ offset: {
49
+ gap: consumerGap === undefined ? DEFAULT_GAP : toCssLengthString({
50
+ value: consumerGap
51
+ }),
52
+ crossAxisShift: {
53
+ value: toCssLengthString({
54
+ value: (_placement$offset$cro = (_placement$offset2 = placement.offset) === null || _placement$offset2 === void 0 || (_placement$offset2 = _placement$offset2.crossAxisShift) === null || _placement$offset2 === void 0 ? void 0 : _placement$offset2.value) !== null && _placement$offset$cro !== void 0 ? _placement$offset$cro : 0
55
+ }),
56
+ direction: (_placement$offset$cro2 = (_placement$offset3 = placement.offset) === null || _placement$offset3 === void 0 || (_placement$offset3 = _placement$offset3.crossAxisShift) === null || _placement$offset3 === void 0 ? void 0 : _placement$offset3.direction) !== null && _placement$offset$cro2 !== void 0 ? _placement$offset$cro2 : 'forwards'
57
+ }
58
+ }
17
59
  };
18
60
  }
@@ -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';
@@ -87,6 +102,19 @@ export function placementToTryFallbacks(_ref2) {
87
102
  return ["".concat(sameEdge, " span-").concat(crossAxis, "-end"), "".concat(sameEdge, " span-").concat(crossAxis, "-start"), flipKeyword, "".concat(oppositeEdge, " span-").concat(crossAxis, "-end"), "".concat(oppositeEdge, " span-").concat(crossAxis, "-start")].join(', ');
88
103
  }
89
104
 
105
+ /**
106
+ * Returns true when the popover is currently open. Wrapped because the
107
+ * `:popover-open` pseudo-class is not implemented in some test environments
108
+ * (e.g. jsdom), where `matches` throws on unknown selectors.
109
+ */
110
+ function isPopoverOpen(popover) {
111
+ try {
112
+ return popover.matches(':popover-open');
113
+ } catch (_unused) {
114
+ return false;
115
+ }
116
+ }
117
+
90
118
  /**
91
119
  * Detects whether the browser supports CSS Anchor Positioning.
92
120
  * Uses `once()` for lazy evaluation — safe for SSR and avoids hydration mismatches.
@@ -98,12 +126,6 @@ var supportsAnchorPositioning = once(function () {
98
126
  return CSS.supports('anchor-name', '--a');
99
127
  });
100
128
 
101
- /**
102
- * Default gap between the popover and its trigger (in pixels).
103
- * Maps to `space.100` (8px) in the design token scale.
104
- */
105
- var DEFAULT_OFFSET = 8;
106
-
107
129
  /**
108
130
  * Returns the CSS margin declaration that creates a gap between the
109
131
  * popover and its trigger on the side facing the anchor.
@@ -116,7 +138,7 @@ function edgeMargin(_ref3) {
116
138
  offset = _ref3.offset;
117
139
  var axis = placement.axis,
118
140
  edge = placement.edge;
119
- var value = "".concat(offset, "px");
141
+ var value = offset;
120
142
  if (axis === 'block' && edge === 'end') {
121
143
  return {
122
144
  property: 'margin-block-start',
@@ -147,6 +169,33 @@ function edgeMargin(_ref3) {
147
169
  };
148
170
  }
149
171
 
172
+ /**
173
+ * Cross-axis shift margin. Margin side matches the popover's anchored
174
+ * cross-axis edge: `align: 'start'` uses the START margin, `align: 'end'`
175
+ * uses the END margin with the sign inverted (so `forwards` is always
176
+ * positive), `align: 'center'` uses START. Margin on the un-anchored side
177
+ * has no effect under CSS Anchor Positioning.
178
+ */
179
+ function crossAxisShiftMargin(_ref4) {
180
+ var placement = _ref4.placement,
181
+ crossAxisShiftCssValue = _ref4.crossAxisShiftCssValue,
182
+ direction = _ref4.direction;
183
+ var axis = placement.axis,
184
+ align = placement.align;
185
+ var directionSign = direction === 'forwards' ? 1 : -1;
186
+ var crossAxis = axis === 'block' ? 'inline' : 'block';
187
+ var useEndSide = align === 'end';
188
+ var side = useEndSide ? 'end' : 'start';
189
+ var sideSign = useEndSide ? -1 : 1;
190
+ var finalSign = directionSign * sideSign;
191
+ // Wrap any string in calc() with the sign factor. CSS handles the math.
192
+ var value = finalSign === 1 ? crossAxisShiftCssValue : "calc(-1 * ".concat(crossAxisShiftCssValue, ")");
193
+ return {
194
+ property: "margin-".concat(crossAxis, "-").concat(side),
195
+ value: value
196
+ };
197
+ }
198
+
150
199
  /**
151
200
  * Hook that positions an element relative to an anchor element using
152
201
  * CSS Anchor Positioning (with a JS fallback for older browsers).
@@ -163,15 +212,13 @@ function edgeMargin(_ref3) {
163
212
  * `position: fixed` with measured coordinates, re-running on scroll (capture)
164
213
  * and resize events.
165
214
  */
166
- export function useAnchorPosition(_ref4) {
167
- var anchorRef = _ref4.anchorRef,
168
- popoverRef = _ref4.popoverRef,
169
- placement = _ref4.placement,
170
- _ref4$offset = _ref4.offset,
171
- offset = _ref4$offset === void 0 ? DEFAULT_OFFSET : _ref4$offset,
172
- _ref4$forceFallbackPo = _ref4.forceFallbackPositioning,
173
- forceFallbackPositioning = _ref4$forceFallbackPo === void 0 ? false : _ref4$forceFallbackPo,
174
- arrow = _ref4.arrow;
215
+ export function useAnchorPosition(_ref5) {
216
+ var anchorRef = _ref5.anchorRef,
217
+ popoverRef = _ref5.popoverRef,
218
+ placement = _ref5.placement,
219
+ _ref5$forceFallbackPo = _ref5.forceFallbackPositioning,
220
+ forceFallbackPositioning = _ref5$forceFallbackPo === void 0 ? false : _ref5$forceFallbackPo,
221
+ arrow = _ref5.arrow;
175
222
  var id = useId();
176
223
  var anchorName = "--anchor-".concat(id.replace(/:/g, ''));
177
224
  useLayoutEffect(function () {
@@ -183,11 +230,18 @@ export function useAnchorPosition(_ref4) {
183
230
  var resolvedPlacement = getPlacement({
184
231
  placement: placement
185
232
  });
233
+ var gapCssValue = resolvedPlacement.offset.gap;
234
+ var crossAxisShiftCssValue = resolvedPlacement.offset.crossAxisShift.value;
186
235
  var shouldForceFallback = Boolean(forceFallbackPositioning);
187
236
  if (supportsAnchorPositioning() && !shouldForceFallback) {
188
237
  var gap = edgeMargin({
189
238
  placement: resolvedPlacement,
190
- offset: offset
239
+ offset: gapCssValue
240
+ });
241
+ var crossAxisShift = crossAxisShiftMargin({
242
+ placement: resolvedPlacement,
243
+ crossAxisShiftCssValue: crossAxisShiftCssValue,
244
+ direction: resolvedPlacement.offset.crossAxisShift.direction
191
245
  });
192
246
  var popoverStyles = [{
193
247
  property: 'position-anchor',
@@ -214,17 +268,62 @@ export function useAnchorPosition(_ref4) {
214
268
  property: 'inset',
215
269
  value: 'auto'
216
270
  },
217
- // Gap between the popover and its trigger
271
+ // Main-axis gap between the popover and its trigger
218
272
  {
219
273
  property: gap.property,
220
274
  value: gap.value
275
+ },
276
+ // Cross-axis shift
277
+ {
278
+ property: crossAxisShift.property,
279
+ value: crossAxisShift.value
221
280
  }];
281
+
282
+ // Surface the active cross-axis shift value as a custom property so
283
+ // each named arrow @position-try rule can re-apply it after a flip.
284
+ // The four properties cover (cross-axis, side); only one is non-zero
285
+ // at a time.
286
+ popoverStyles.push({
287
+ property: '--ds-cross-axis-shift-margin-start',
288
+ value: '0px'
289
+ });
290
+ popoverStyles.push({
291
+ property: '--ds-cross-axis-shift-margin-end',
292
+ value: '0px'
293
+ });
294
+ popoverStyles.push({
295
+ property: '--ds-cross-axis-shift-margin-block-start',
296
+ value: '0px'
297
+ });
298
+ popoverStyles.push({
299
+ property: '--ds-cross-axis-shift-margin-block-end',
300
+ value: '0px'
301
+ });
302
+ var crossAxisShiftActive = crossAxisShiftMargin({
303
+ placement: resolvedPlacement,
304
+ crossAxisShiftCssValue: crossAxisShiftCssValue,
305
+ direction: resolvedPlacement.offset.crossAxisShift.direction
306
+ });
307
+ // Map margin property to its corresponding custom property name.
308
+ var customPropertyByMarginProperty = {
309
+ 'margin-inline-start': '--ds-cross-axis-shift-margin-start',
310
+ 'margin-inline-end': '--ds-cross-axis-shift-margin-end',
311
+ 'margin-block-start': '--ds-cross-axis-shift-margin-block-start',
312
+ 'margin-block-end': '--ds-cross-axis-shift-margin-block-end'
313
+ };
314
+ var crossAxisShiftCustomProperty = customPropertyByMarginProperty[crossAxisShiftActive.property];
315
+ if (crossAxisShiftCustomProperty) {
316
+ popoverStyles.push({
317
+ property: crossAxisShiftCustomProperty,
318
+ value: crossAxisShiftActive.value
319
+ });
320
+ }
222
321
  if (arrow) {
223
322
  // Set the arrow size variable used by the injected arrow CSS
224
323
  // and the @position-try rules for margin values.
225
324
  popoverStyles.push({
226
325
  property: '--ds-arrow-size',
227
- value: "".concat(offset, "px")
326
+ value: gapCssValue
228
327
  });
229
328
  // Mark the popover root as the arrow host. The ARROW_CSS targets
230
329
  // [data-ds-popover-arrow] to apply clip-path: inset(0) margin-box
@@ -262,6 +361,7 @@ export function useAnchorPosition(_ref4) {
262
361
  }]
263
362
  });
264
363
  function update() {
364
+ var _popover$parentElemen;
265
365
  if (!trigger || !popover) {
266
366
  return;
267
367
  }
@@ -270,41 +370,86 @@ export function useAnchorPosition(_ref4) {
270
370
  width: window.innerWidth,
271
371
  height: window.innerHeight
272
372
  };
373
+
374
+ // Resolve consumer-supplied CSS length strings (tokens, calc, var,
375
+ // px, rem, etc) to pixels. The probe is mounted next to the popover
376
+ // so it inherits the same custom-property scope and font size.
377
+ // `popover.parentElement` is guaranteed to exist for any rendered
378
+ // popover (the popover itself is appended somewhere in the tree),
379
+ // but fall back to the popover element if not for safety.
380
+ var probeContainer = (_popover$parentElemen = popover.parentElement) !== null && _popover$parentElemen !== void 0 ? _popover$parentElemen : popover;
381
+ var gapPx = resolveCssLengthToPixels({
382
+ value: gapCssValue,
383
+ container: probeContainer
384
+ });
385
+ var crossAxisShiftPx = resolveCssLengthToPixels({
386
+ value: crossAxisShiftCssValue,
387
+ container: probeContainer
388
+ });
273
389
  var _computeFallbackPosit = computeFallbackPosition({
274
390
  triggerRect: triggerRect,
275
391
  popoverEl: popover,
276
392
  placement: resolvedPlacement,
277
393
  viewport: viewport,
278
- offset: offset
394
+ gap: gapPx,
395
+ crossAxisShift: {
396
+ value: crossAxisShiftPx,
397
+ direction: resolvedPlacement.offset.crossAxisShift.direction
398
+ }
279
399
  }),
280
400
  top = _computeFallbackPosit.top,
281
401
  left = _computeFallbackPosit.left;
282
402
  popover.style.setProperty('top', "".concat(top, "px"));
283
403
  popover.style.setProperty('left', "".concat(left, "px"));
404
+ // Reveal the popover only after it has been positioned.
405
+ // See the toggle listener below for why we hide on open.
406
+ popover.style.removeProperty('opacity');
284
407
  }
285
408
 
286
409
  // Throttle scroll/resize updates to one per animation frame
287
410
  var scheduledUpdate = rafSchedule(update);
288
- var pendingFrame = null;
411
+
412
+ // Wait for the FIRST valid layout before measuring: the popover
413
+ // is `display: none` until `showPopover()`, and some browsers
414
+ // fire `toggle` before layout, so RAF-after-toggle is unreliable.
415
+ // ResizeObserver fires once the browser has real dimensions.
416
+ // Self-disconnects after one valid measurement; ongoing
417
+ // scroll/resize is handled by the window listeners below.
418
+ var resizeObserver = new ResizeObserver(function () {
419
+ if (popover.offsetWidth > 0 && popover.offsetHeight > 0) {
420
+ update();
421
+ resizeObserver.disconnect();
422
+ }
423
+ });
424
+
425
+ // If the popover is already open by the time this effect runs (for
426
+ // example, the parent `useAnchorPosition` effect runs after the child
427
+ // `Popover` effect that called `showPopover()`), the initial `toggle`
428
+ // event has already fired and our listener below would miss it. Start
429
+ // observing immediately so the first measurement still happens.
430
+ if (isPopoverOpen(popover)) {
431
+ popover.style.setProperty('opacity', '0');
432
+ resizeObserver.observe(popover);
433
+ }
289
434
  var undoPositioning = combine(cleanupBaseStyles,
290
- // Position when the popover transitions to visible.
291
- // The popover starts hidden (`display: none` per the popover
292
- // UA stylesheet), so `offsetWidth`/`offsetHeight` are 0 until
293
- // `showPopover()` is called. We defer positioning to the next
294
- // animation frame because some browsers (Firefox) fire the
295
- // `toggle` event synchronously during `showPopover()`, before
296
- // the element has been laid out — so dimensions are still 0.
297
- // Waiting one frame guarantees layout has run and measurements
298
- // are accurate.
435
+ // On every open: hide synchronously so the user never sees
436
+ // the UA-default position, then re-observe for a fresh
437
+ // measurement. We use `opacity: 0` rather than
438
+ // `visibility: hidden` because Firefox skips visibility-hidden
439
+ // elements during `<dialog>` initial-focus traversal — see
440
+ // `form-in-popup.spec.tsx` on `desktop-firefox`.
299
441
  bind(popover, {
300
442
  type: 'toggle',
301
443
  listener: function listener(event) {
302
444
  var toggleEvent = event;
303
445
  if (toggleEvent.newState === 'open') {
304
- pendingFrame = requestAnimationFrame(update);
446
+ popover.style.setProperty('opacity', '0');
447
+ resizeObserver.observe(popover);
305
448
  }
306
449
  }
307
- }), bind(window, {
450
+ }), function () {
451
+ return resizeObserver.disconnect();
452
+ }, bind(window, {
308
453
  type: 'scroll',
309
454
  listener: scheduledUpdate,
310
455
  options: {
@@ -319,12 +464,10 @@ export function useAnchorPosition(_ref4) {
319
464
  }
320
465
  }), function () {
321
466
  scheduledUpdate.cancel();
322
- if (pendingFrame !== null) {
323
- cancelAnimationFrame(pendingFrame);
324
- }
325
467
  popover.style.removeProperty('top');
326
468
  popover.style.removeProperty('left');
469
+ popover.style.removeProperty('opacity');
327
470
  });
328
471
  return undoPositioning;
329
- }, [anchorRef, popoverRef, placement, anchorName, offset, forceFallbackPositioning, arrow]);
472
+ }, [anchorRef, popoverRef, placement, anchorName, forceFallbackPositioning, arrow]);
330
473
  }