@atlaskit/top-layer 0.1.4 → 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 +32 -0
  2. package/dist/cjs/arrow/index.js +2 -2
  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 +181 -37
  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 +24 -5
  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 +174 -29
  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 +2 -2
  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 +180 -37
  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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @atlaskit/top-layer
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`997849983979b`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/997849983979b) -
8
+ Add `placement.offset = { gap, crossAxisShift }`.
9
+ - `gap` is the distance from the trigger along the placement axis (default
10
+ `token('space.100', '8px')`).
11
+ - `crossAxisShift` is a perpendicular nudge with `{ value, direction: 'forwards' | 'backwards' }`.
12
+ - Both accept numbers or CSS length strings (tokens, `calc()`, `var()`, etc).
13
+
14
+ The JS fallback path now also honours `gap` and `crossAxisShift`. CSS length strings are resolved
15
+ to pixels per measurement via a hidden DOM probe (the popover stays at `opacity: 0` until the
16
+ first measurement completes, so there is no flash). Previously, the JS fallback applied a fixed
17
+ 8px gap and ignored `crossAxisShift`.
18
+
19
+ The standalone `Popup.Content` `offset` prop is removed in favour of `placement.offset`.
20
+ `fromLegacyPlacement` continues to accept a popper-style `[along, away]` tuple for migrations and
21
+ now produces the new field names internally.
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies
26
+
27
+ ## 0.1.5
28
+
29
+ ### Patch Changes
30
+
31
+ - [`034e29787df79`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/034e29787df79) -
32
+ Fix arrow tips being squared off instead of coming to a point by changing clip-path inset from 1px
33
+ to 0px
34
+
3
35
  ## 0.1.4
4
36
 
5
37
  ### Patch Changes
@@ -13,7 +13,7 @@ var _useAnchorPositioning = require("../internal/use-anchor-positioning");
13
13
  * How it works:
14
14
  * 1. Four arrow shapes exist simultaneously as `::before` (top/bottom) and
15
15
  * `::after` (left/right) hexagonal pseudo-elements.
16
- * 2. `clip-path: inset(1px) margin-box` clips the popover to its margin-box.
16
+ * 2. `clip-path: inset(0) margin-box` clips the popover to its margin-box.
17
17
  * 3. Margin on the anchor-facing side pushes the popover away, creating space
18
18
  * where one arrow "escapes" the clip boundary and becomes visible.
19
19
  * 4. `@position-try` rules change both `position-area` and `margin` when
@@ -26,7 +26,7 @@ var _useAnchorPositioning = require("../internal/use-anchor-positioning");
26
26
  *
27
27
  * @see https://goulet.dev/posts/tooltip-with-popover-and-anchor-positioning/
28
28
  */
29
- var ARROW_CSS = "\n[data-ds-popover-arrow] {\n clip-path: inset(var(--ds-arrow-offset, 1px)) 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";
29
+ 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";
30
30
 
31
31
  // ── Placement utilities ──
32
32
 
@@ -8,9 +8,23 @@ exports.computeFallbackPosition = computeFallbackPosition;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  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; }
10
10
  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) { (0, _defineProperty2.default)(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; }
11
+ /**
12
+ * JS fallback positioning math. Used only when the browser does not
13
+ * support CSS Anchor Positioning (less than 6% of users).
14
+ *
15
+ * Consumer-supplied `offset.gap` and `offset.crossAxisShift` are
16
+ * honored: the caller (`useAnchorPosition`) resolves CSS length strings
17
+ * (tokens, calc, var, etc) to pixels via a DOM probe before passing them
18
+ * in. The caller also keeps the popover hidden (`opacity: 0`) until the
19
+ * first measurement completes, so the popover never paints at the wrong
20
+ * position.
21
+ */
22
+
11
23
  /**
12
24
  * Computes the base {top, left} position based on the primary edge,
13
25
  * flipping to the opposite side when there is not enough space.
26
+ *
27
+ * `gap` is applied along the main axis between trigger and popover.
14
28
  */
15
29
  function computeEdgePosition(_ref) {
16
30
  var axis = _ref.axis,
@@ -19,13 +33,15 @@ function computeEdgePosition(_ref) {
19
33
  popoverWidth = _ref.popoverWidth,
20
34
  popoverHeight = _ref.popoverHeight,
21
35
  viewport = _ref.viewport,
22
- offset = _ref.offset;
36
+ gap = _ref.gap;
23
37
  if (axis === 'block' && edge === 'end') {
24
38
  // Below trigger
25
39
  var spaceBelow = viewport.height - triggerRect.bottom;
26
40
  var spaceAbove = triggerRect.top;
41
+ var fitsBelow = spaceBelow >= popoverHeight + gap;
42
+ var useBelow = fitsBelow || spaceBelow >= spaceAbove;
27
43
  return {
28
- top: spaceBelow >= popoverHeight + offset || spaceBelow >= spaceAbove ? triggerRect.bottom + offset : triggerRect.top - popoverHeight - offset,
44
+ top: useBelow ? triggerRect.bottom + gap : triggerRect.top - popoverHeight - gap,
29
45
  left: triggerRect.left + triggerRect.width / 2 - popoverWidth / 2
30
46
  };
31
47
  }
@@ -33,8 +49,10 @@ function computeEdgePosition(_ref) {
33
49
  // Above trigger
34
50
  var _spaceAbove = triggerRect.top;
35
51
  var _spaceBelow = viewport.height - triggerRect.bottom;
52
+ var fitsAbove = _spaceAbove >= popoverHeight + gap;
53
+ var useAbove = fitsAbove || _spaceAbove >= _spaceBelow;
36
54
  return {
37
- top: _spaceAbove >= popoverHeight + offset || _spaceAbove >= _spaceBelow ? triggerRect.top - popoverHeight - offset : triggerRect.bottom + offset,
55
+ top: useAbove ? triggerRect.top - popoverHeight - gap : triggerRect.bottom + gap,
38
56
  left: triggerRect.left + triggerRect.width / 2 - popoverWidth / 2
39
57
  };
40
58
  }
@@ -42,24 +60,28 @@ function computeEdgePosition(_ref) {
42
60
  // Right of trigger
43
61
  var spaceRight = viewport.width - triggerRect.right;
44
62
  var spaceLeft = triggerRect.left;
63
+ var fitsRight = spaceRight >= popoverWidth + gap;
64
+ var useRight = fitsRight || spaceRight >= spaceLeft;
45
65
  return {
46
66
  top: triggerRect.top + triggerRect.height / 2 - popoverHeight / 2,
47
- left: spaceRight >= popoverWidth + offset || spaceRight >= spaceLeft ? triggerRect.right + offset : triggerRect.left - popoverWidth - offset
67
+ left: useRight ? triggerRect.right + gap : triggerRect.left - popoverWidth - gap
48
68
  };
49
69
  }
50
70
  if (axis === 'inline' && edge === 'start') {
51
71
  // Left of trigger
52
72
  var _spaceLeft = triggerRect.left;
53
73
  var _spaceRight = viewport.width - triggerRect.right;
74
+ var fitsLeft = _spaceLeft >= popoverWidth + gap;
75
+ var useLeft = fitsLeft || _spaceLeft >= _spaceRight;
54
76
  return {
55
77
  top: triggerRect.top + triggerRect.height / 2 - popoverHeight / 2,
56
- left: _spaceLeft >= popoverWidth + offset || _spaceLeft >= _spaceRight ? triggerRect.left - popoverWidth - offset : triggerRect.right + offset
78
+ left: useLeft ? triggerRect.left - popoverWidth - gap : triggerRect.right + gap
57
79
  };
58
80
  }
59
81
 
60
82
  // Unreachable for valid placements; defaults to below trigger
61
83
  return {
62
- top: triggerRect.bottom + offset,
84
+ top: triggerRect.bottom + gap,
63
85
  left: triggerRect.left + triggerRect.width / 2 - popoverWidth / 2
64
86
  };
65
87
  }
@@ -106,26 +128,58 @@ function applyAlignment(_ref2) {
106
128
  }
107
129
 
108
130
  /**
109
- * JS fallback: compute popover position from trigger rect and viewport.
131
+ * Applies the cross-axis shift offset.
110
132
  *
111
- * Placement algorithm (from the document):
112
- * - "block" placement: show above/below, picking the side with more space if flipping needed
113
- * - "inline" placement: show left/right, picking the side with more space if flipping needed
114
- * - "auto" placement: show on the edge with the most available space
133
+ * The sign mirrors the CSS path (see `crossAxisShiftMargin` in
134
+ * `use-anchor-positioning.tsx`):
135
+ * - `align: 'start' | 'center'` + `forwards` → push toward end (positive)
136
+ * - `align: 'end'` + `forwards` → push toward start (negative)
137
+ * - `backwards` flips the sign in both cases.
138
+ */
139
+ function applyCrossAxisShift(_ref3) {
140
+ var position = _ref3.position,
141
+ axis = _ref3.axis,
142
+ align = _ref3.align,
143
+ crossAxisShift = _ref3.crossAxisShift;
144
+ if (crossAxisShift.value === 0) {
145
+ return position;
146
+ }
147
+ var directionSign = crossAxisShift.direction === 'forwards' ? 1 : -1;
148
+ var sideSign = align === 'end' ? -1 : 1;
149
+ var delta = crossAxisShift.value * directionSign * sideSign;
150
+
151
+ // Cross-axis is the OPPOSITE of the placement axis.
152
+ if (axis === 'block') {
153
+ return _objectSpread(_objectSpread({}, position), {}, {
154
+ left: position.left + delta
155
+ });
156
+ }
157
+ return _objectSpread(_objectSpread({}, position), {}, {
158
+ top: position.top + delta
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Computes popover position from trigger rect and viewport, flipping to
164
+ * the side with more space when needed. Re-runs on scroll/resize.
115
165
  *
116
- * Re-runs on scroll (capture on window) and resize events.
166
+ * `gap` and `crossAxisShift` are pre-resolved to pixels by the
167
+ * caller (see `resolveCssLengthToPixels`), so this function stays purely
168
+ * numeric.
117
169
  */
118
- function computeFallbackPosition(_ref3) {
119
- var triggerRect = _ref3.triggerRect,
120
- popoverEl = _ref3.popoverEl,
121
- placement = _ref3.placement,
122
- viewport = _ref3.viewport,
123
- offset = _ref3.offset;
170
+ function computeFallbackPosition(_ref4) {
171
+ var triggerRect = _ref4.triggerRect,
172
+ popoverEl = _ref4.popoverEl,
173
+ placement = _ref4.placement,
174
+ viewport = _ref4.viewport,
175
+ gap = _ref4.gap,
176
+ crossAxisShift = _ref4.crossAxisShift;
124
177
  var axis = placement.axis,
125
178
  edge = placement.edge,
126
179
  align = placement.align;
127
180
 
128
- // Measure popover dimensions (it might not have layout yet if just shown)
181
+ // Measure popover dimensions. Callers must ensure the popover has
182
+ // laid out before calling this (it should not be `display: none`).
129
183
  var popoverWidth = popoverEl.offsetWidth;
130
184
  var popoverHeight = popoverEl.offsetHeight;
131
185
  var basePosition = computeEdgePosition({
@@ -135,7 +189,7 @@ function computeFallbackPosition(_ref3) {
135
189
  popoverWidth: popoverWidth,
136
190
  popoverHeight: popoverHeight,
137
191
  viewport: viewport,
138
- offset: offset
192
+ gap: gap
139
193
  });
140
194
  var alignedPosition = applyAlignment({
141
195
  position: basePosition,
@@ -145,10 +199,16 @@ function computeFallbackPosition(_ref3) {
145
199
  popoverWidth: popoverWidth,
146
200
  popoverHeight: popoverHeight
147
201
  });
202
+ var shiftedPosition = applyCrossAxisShift({
203
+ position: alignedPosition,
204
+ axis: axis,
205
+ align: align,
206
+ crossAxisShift: crossAxisShift
207
+ });
148
208
 
149
209
  // Clamp to viewport so the popover is never offscreen
150
210
  return {
151
- top: Math.max(0, Math.min(alignedPosition.top, viewport.height - popoverHeight)),
152
- left: Math.max(0, Math.min(alignedPosition.left, viewport.width - popoverWidth))
211
+ top: Math.max(0, Math.min(shiftedPosition.top, viewport.height - popoverHeight)),
212
+ left: Math.max(0, Math.min(shiftedPosition.left, viewport.width - popoverWidth))
153
213
  };
154
214
  }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.resolveCssLengthToPixels = resolveCssLengthToPixels;
7
+ /**
8
+ * Resolves a CSS length value to a pixel number using a hidden DOM probe.
9
+ *
10
+ * Used by the JS positioning fallback: consumer offsets can be numbers,
11
+ * `${n}px` strings, tokenised `var(--ds-space-100, 8px)` strings, `calc()`
12
+ * expressions, viewport units, etc. Parsing arbitrary CSS in JS is fragile
13
+ * (and impossible for `calc(var(...))` without a layout context), so we let
14
+ * the browser do the math.
15
+ *
16
+ * The probe is appended to `container` so it inherits the same containing
17
+ * block, font size, and custom-property scope as the popover. We use `width`
18
+ * because it accepts any CSS length, including negative `calc()` results
19
+ * (negative widths are clamped to 0 — see note below).
20
+ *
21
+ * Cost: one synchronous reflow per call. The fallback only resolves on
22
+ * placement/offset changes, not on every scroll/resize update, so this is
23
+ * not a hot path.
24
+ */
25
+ function resolveCssLengthToPixels(_ref) {
26
+ var value = _ref.value,
27
+ container = _ref.container;
28
+ if (typeof value === 'number') {
29
+ return value;
30
+ }
31
+
32
+ // Fast path: bare `${n}px` strings come from the consumer normalisation
33
+ // in `getPlacement`. Avoid a reflow when we can.
34
+ var pxMatch = value.match(/^(-?\d+(?:\.\d+)?)px$/);
35
+ if (pxMatch) {
36
+ return Number(pxMatch[1]);
37
+ }
38
+ var probe = container.ownerDocument.createElement('div');
39
+ probe.style.position = 'absolute';
40
+ probe.style.visibility = 'hidden';
41
+ probe.style.pointerEvents = 'none';
42
+ probe.style.height = '0';
43
+ probe.style.padding = '0';
44
+ probe.style.border = '0';
45
+ // `width` clamps negatives to 0; use `margin-left` instead so signs are
46
+ // preserved. `getComputedStyle` returns the resolved pixel value for
47
+ // margin-* properties even when the source was a token / calc / var.
48
+ probe.style.marginLeft = value;
49
+ container.appendChild(probe);
50
+ var resolved = parseFloat(getComputedStyle(probe).marginLeft);
51
+ container.removeChild(probe);
52
+ return Number.isFinite(resolved) ? resolved : 0;
53
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.toCssLengthString = toCssLengthString;
7
+ /**
8
+ * Normalizes an offset to a CSS length string. Numbers become `${n}px`;
9
+ * strings pass through. Apply at the API boundary so internal code only
10
+ * deals with strings.
11
+ */
12
+ function toCssLengthString(_ref) {
13
+ var value = _ref.value;
14
+ if (typeof value === 'number') {
15
+ return "".concat(value, "px");
16
+ }
17
+ return value;
18
+ }
@@ -4,21 +4,62 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.getPlacement = getPlacement;
7
+ var _resolveCssLength = require("./resolve-css-length");
7
8
  /**
8
- * Fully-resolved placement (internal). All fields are required.
9
- * Use getPlacement({ placement: partial }) to obtain from partial input.
9
+ * Default gap between trigger and popover. Resolves to a design
10
+ * token (`space.100`) so the value follows the design system's spacing
11
+ * scale. Used when `placement.offset.gap` is not provided.
10
12
  */
13
+ var DEFAULT_GAP = "var(--ds-space-100, 8px)";
11
14
 
12
15
  /**
13
- * Resolves a partial placement to its fully-specified form.
14
- * Defaults: `axis: 'block'`, `edge: 'end'`, `align: 'center'`.
16
+ * Cross-axis shift. `value` is a CSS length string (consumer numbers are
17
+ * normalized to `${n}px`). `direction` is `'forwards'` (toward end) or
18
+ * `'backwards'` (toward start).
19
+ */
20
+
21
+ /**
22
+ * Fully-resolved placement (internal). Use `getPlacement` for partial input.
23
+ * `offset.gap` and `offset.crossAxisShift.value` are always CSS
24
+ * length strings.
25
+ */
26
+
27
+ /**
28
+ * Partial placement options. `offset.gap` and
29
+ * `offset.crossAxisShift.value` accept either a number (pixels) or a
30
+ * CSS length string (e.g. `token('space.100')`).
31
+ *
32
+ * The two offset axes are named for clarity at the call site:
33
+ * - `gap` — distance away from the trigger along the placement axis
34
+ * - `crossAxisShift` — nudge along the trigger edge (perpendicular to the placement axis)
35
+ */
36
+
37
+ /**
38
+ * Resolves a partial placement to its fully-specified form. Number offsets
39
+ * are normalized to `${n}px` strings.
40
+ *
41
+ * Defaults: `axis: 'block'`, `edge: 'end'`, `align: 'center'`,
42
+ * `offset.gap: token('space.100', '8px')`,
43
+ * `offset.crossAxisShift: { value: '0px', direction: 'forwards' }`.
15
44
  */
16
45
  function getPlacement(_ref) {
17
- var _placement$axis, _placement$edge, _placement$align;
46
+ var _placement$offset, _placement$axis, _placement$edge, _placement$align, _placement$offset$cro, _placement$offset2, _placement$offset$cro2, _placement$offset3;
18
47
  var placement = _ref.placement;
48
+ var consumerGap = (_placement$offset = placement.offset) === null || _placement$offset === void 0 ? void 0 : _placement$offset.gap;
19
49
  return {
20
50
  axis: (_placement$axis = placement.axis) !== null && _placement$axis !== void 0 ? _placement$axis : 'block',
21
51
  edge: (_placement$edge = placement.edge) !== null && _placement$edge !== void 0 ? _placement$edge : 'end',
22
- align: (_placement$align = placement.align) !== null && _placement$align !== void 0 ? _placement$align : 'center'
52
+ align: (_placement$align = placement.align) !== null && _placement$align !== void 0 ? _placement$align : 'center',
53
+ offset: {
54
+ gap: consumerGap === undefined ? DEFAULT_GAP : (0, _resolveCssLength.toCssLengthString)({
55
+ value: consumerGap
56
+ }),
57
+ crossAxisShift: {
58
+ value: (0, _resolveCssLength.toCssLengthString)({
59
+ 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
60
+ }),
61
+ 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'
62
+ }
63
+ }
23
64
  };
24
65
  }