@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
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 +
|
|
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
|
-
*
|
|
124
|
+
* Applies the cross-axis shift offset.
|
|
103
125
|
*
|
|
104
|
-
*
|
|
105
|
-
* -
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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
|
-
*
|
|
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(
|
|
112
|
-
var triggerRect =
|
|
113
|
-
popoverEl =
|
|
114
|
-
placement =
|
|
115
|
-
viewport =
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
145
|
-
left: Math.max(0, Math.min(
|
|
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
|
-
*
|
|
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(_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 =
|
|
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(
|
|
167
|
-
var anchorRef =
|
|
168
|
-
popoverRef =
|
|
169
|
-
placement =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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:
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
// `
|
|
294
|
-
//
|
|
295
|
-
// `
|
|
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
|
-
|
|
446
|
+
popover.style.setProperty('opacity', '0');
|
|
447
|
+
resizeObserver.observe(popover);
|
|
305
448
|
}
|
|
306
449
|
}
|
|
307
|
-
}),
|
|
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,
|
|
472
|
+
}, [anchorRef, popoverRef, placement, anchorName, forceFallbackPositioning, arrow]);
|
|
330
473
|
}
|