@atlaskit/top-layer 4.4.5 → 5.0.1

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 (137) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +13 -4
  3. package/dist/cjs/dialog/dialog-content.js +2 -0
  4. package/dist/cjs/entry-points/use-popover-id.js +5 -10
  5. package/dist/cjs/internal/anchor-positioning/apply-anchor-positioning.js +86 -0
  6. package/dist/cjs/internal/anchor-positioning/cross-axis-shift-margins.js +34 -0
  7. package/dist/cjs/internal/anchor-positioning/edge-margin.js +21 -0
  8. package/dist/cjs/internal/anchor-positioning/fit-margins.js +77 -0
  9. package/dist/cjs/internal/anchor-positioning/get-existing-anchor-name.js +18 -0
  10. package/dist/cjs/internal/{placement-to-position-area.js → anchor-positioning/placement-to-position-area.js} +11 -11
  11. package/dist/cjs/internal/anchor-positioning/placement-to-try-fallbacks.js +95 -0
  12. package/dist/cjs/internal/anchored-popover-size.js +345 -0
  13. package/dist/cjs/internal/get-aria-for-trigger.js +7 -13
  14. package/dist/cjs/internal/is-native-element-open.js +19 -0
  15. package/dist/cjs/internal/{anchor-positioning-fallback.js → javascript-fallback/anchor-positioning-fallback.js} +12 -33
  16. package/dist/cjs/internal/javascript-fallback/apply-javascript-fallback-positioning.js +297 -0
  17. package/dist/cjs/internal/javascript-fallback/resolve-css-length-to-pixels.js +90 -0
  18. package/dist/cjs/internal/javascript-fallback/warn-fallback-failure.js +19 -0
  19. package/dist/cjs/internal/resolve-placement.js +14 -6
  20. package/dist/cjs/internal/supports-anchor-positioning.js +17 -0
  21. package/dist/cjs/internal/supports-anchor-size.js +17 -0
  22. package/dist/cjs/internal/supports-css-declaration.js +25 -0
  23. package/dist/cjs/internal/surface-reset.js +5 -0
  24. package/dist/cjs/internal/use-anchored-popover-at-point.js +124 -0
  25. package/dist/cjs/internal/use-anchored-popover.js +277 -0
  26. package/dist/cjs/internal/use-animated-visibility.js +11 -23
  27. package/dist/cjs/popover/popover.compiled.css +4 -0
  28. package/dist/cjs/popover/popover.js +63 -61
  29. package/dist/cjs/testing/polyfill.js +6 -7
  30. package/dist/es2019/dialog/dialog-content.js +2 -0
  31. package/dist/es2019/entry-points/use-popover-id.js +6 -10
  32. package/dist/es2019/internal/anchor-positioning/apply-anchor-positioning.js +83 -0
  33. package/dist/es2019/internal/anchor-positioning/cross-axis-shift-margins.js +29 -0
  34. package/dist/es2019/internal/anchor-positioning/edge-margin.js +18 -0
  35. package/dist/es2019/internal/anchor-positioning/fit-margins.js +69 -0
  36. package/dist/es2019/internal/anchor-positioning/get-existing-anchor-name.js +12 -0
  37. package/dist/es2019/internal/{placement-to-position-area.js → anchor-positioning/placement-to-position-area.js} +9 -9
  38. package/dist/es2019/internal/anchor-positioning/placement-to-try-fallbacks.js +94 -0
  39. package/dist/es2019/internal/anchored-popover-size.js +343 -0
  40. package/dist/es2019/internal/get-aria-for-trigger.js +7 -13
  41. package/dist/es2019/internal/is-native-element-open.js +14 -0
  42. package/dist/es2019/internal/{anchor-positioning-fallback.js → javascript-fallback/anchor-positioning-fallback.js} +12 -33
  43. package/dist/es2019/internal/javascript-fallback/apply-javascript-fallback-positioning.js +291 -0
  44. package/dist/es2019/internal/javascript-fallback/resolve-css-length-to-pixels.js +87 -0
  45. package/dist/es2019/internal/javascript-fallback/warn-fallback-failure.js +14 -0
  46. package/dist/es2019/internal/resolve-placement.js +14 -6
  47. package/dist/es2019/internal/supports-anchor-positioning.js +12 -0
  48. package/dist/es2019/internal/supports-anchor-size.js +12 -0
  49. package/dist/es2019/internal/supports-css-declaration.js +20 -0
  50. package/dist/es2019/internal/surface-reset.js +1 -0
  51. package/dist/es2019/internal/use-anchored-popover-at-point.js +116 -0
  52. package/dist/es2019/internal/use-anchored-popover.js +265 -0
  53. package/dist/es2019/internal/use-animated-visibility.js +1 -14
  54. package/dist/es2019/popover/popover.compiled.css +4 -0
  55. package/dist/es2019/popover/popover.js +69 -61
  56. package/dist/es2019/testing/polyfill.js +6 -7
  57. package/dist/esm/dialog/dialog-content.js +2 -0
  58. package/dist/esm/entry-points/use-popover-id.js +6 -10
  59. package/dist/esm/internal/anchor-positioning/apply-anchor-positioning.js +80 -0
  60. package/dist/esm/internal/anchor-positioning/cross-axis-shift-margins.js +28 -0
  61. package/dist/esm/internal/anchor-positioning/edge-margin.js +15 -0
  62. package/dist/esm/internal/anchor-positioning/fit-margins.js +71 -0
  63. package/dist/esm/internal/anchor-positioning/get-existing-anchor-name.js +12 -0
  64. package/dist/esm/internal/{placement-to-position-area.js → anchor-positioning/placement-to-position-area.js} +11 -12
  65. package/dist/esm/internal/anchor-positioning/placement-to-try-fallbacks.js +89 -0
  66. package/dist/esm/internal/anchored-popover-size.js +338 -0
  67. package/dist/esm/internal/get-aria-for-trigger.js +7 -13
  68. package/dist/esm/internal/is-native-element-open.js +13 -0
  69. package/dist/esm/internal/{anchor-positioning-fallback.js → javascript-fallback/anchor-positioning-fallback.js} +12 -33
  70. package/dist/esm/internal/javascript-fallback/apply-javascript-fallback-positioning.js +290 -0
  71. package/dist/esm/internal/javascript-fallback/resolve-css-length-to-pixels.js +85 -0
  72. package/dist/esm/internal/javascript-fallback/warn-fallback-failure.js +13 -0
  73. package/dist/esm/internal/resolve-placement.js +14 -6
  74. package/dist/esm/internal/supports-anchor-positioning.js +12 -0
  75. package/dist/esm/internal/supports-anchor-size.js +12 -0
  76. package/dist/esm/internal/supports-css-declaration.js +19 -0
  77. package/dist/esm/internal/surface-reset.js +1 -0
  78. package/dist/esm/internal/use-anchored-popover-at-point.js +118 -0
  79. package/dist/esm/internal/use-anchored-popover.js +271 -0
  80. package/dist/esm/internal/use-animated-visibility.js +10 -22
  81. package/dist/esm/popover/popover.compiled.css +4 -0
  82. package/dist/esm/popover/popover.js +63 -61
  83. package/dist/esm/testing/polyfill.js +6 -7
  84. package/dist/types/entry-points/placement-map.d.ts +2 -2
  85. package/dist/types/entry-points/use-popover-id.d.ts +3 -7
  86. package/dist/types/internal/anchor-positioning/apply-anchor-positioning.d.ts +26 -0
  87. package/dist/types/internal/anchor-positioning/cross-axis-shift-margins.d.ts +19 -0
  88. package/dist/types/internal/anchor-positioning/edge-margin.d.ts +10 -0
  89. package/dist/types/internal/anchor-positioning/fit-margins.d.ts +30 -0
  90. package/dist/types/internal/anchor-positioning/get-existing-anchor-name.d.ts +8 -0
  91. package/dist/types/internal/{placement-to-position-area.d.ts → anchor-positioning/placement-to-position-area.d.ts} +10 -6
  92. package/dist/types/internal/anchor-positioning/placement-to-try-fallbacks.d.ts +30 -0
  93. package/dist/types/internal/anchored-popover-size.d.ts +126 -0
  94. package/dist/types/internal/get-aria-for-trigger.d.ts +13 -25
  95. package/dist/types/internal/is-native-element-open.d.ts +3 -0
  96. package/dist/types/internal/javascript-fallback/anchor-positioning-fallback.d.ts +36 -0
  97. package/dist/types/internal/javascript-fallback/apply-javascript-fallback-positioning.d.ts +15 -0
  98. package/dist/types/internal/javascript-fallback/resolve-css-length-to-pixels.d.ts +20 -0
  99. package/dist/types/internal/javascript-fallback/warn-fallback-failure.d.ts +7 -0
  100. package/dist/types/internal/resolve-placement.d.ts +18 -2
  101. package/dist/types/internal/supports-anchor-positioning.d.ts +5 -0
  102. package/dist/types/internal/supports-anchor-size.d.ts +5 -0
  103. package/dist/types/internal/supports-css-declaration.d.ts +11 -0
  104. package/dist/types/internal/surface-reset.d.ts +25 -0
  105. package/dist/types/internal/use-anchored-popover-at-point.d.ts +48 -0
  106. package/dist/types/internal/use-anchored-popover.d.ts +143 -0
  107. package/dist/types/popover/index.d.ts +2 -1
  108. package/dist/types/popover/popover.d.ts +1 -1
  109. package/dist/types/popover/types.d.ts +15 -17
  110. package/dist/types/testing/polyfill.d.ts +3 -3
  111. package/package.json +2 -2
  112. package/use-anchored-popover/package.json +10 -0
  113. package/use-anchored-popover-at-point/package.json +10 -0
  114. package/dist/cjs/internal/placement-to-try-fallbacks.js +0 -108
  115. package/dist/cjs/internal/resolve-css-length-to-pixels.js +0 -104
  116. package/dist/cjs/internal/use-anchor-position-at-point.js +0 -129
  117. package/dist/cjs/internal/use-anchor-position.js +0 -501
  118. package/dist/cjs/internal/use-width-from-anchor.js +0 -124
  119. package/dist/es2019/internal/placement-to-try-fallbacks.js +0 -108
  120. package/dist/es2019/internal/resolve-css-length-to-pixels.js +0 -101
  121. package/dist/es2019/internal/use-anchor-position-at-point.js +0 -124
  122. package/dist/es2019/internal/use-anchor-position.js +0 -480
  123. package/dist/es2019/internal/use-width-from-anchor.js +0 -120
  124. package/dist/esm/internal/placement-to-try-fallbacks.js +0 -103
  125. package/dist/esm/internal/resolve-css-length-to-pixels.js +0 -98
  126. package/dist/esm/internal/use-anchor-position-at-point.js +0 -124
  127. package/dist/esm/internal/use-anchor-position.js +0 -494
  128. package/dist/esm/internal/use-width-from-anchor.js +0 -118
  129. package/dist/types/internal/anchor-positioning-fallback.d.ts +0 -51
  130. package/dist/types/internal/placement-to-try-fallbacks.d.ts +0 -13
  131. package/dist/types/internal/resolve-css-length-to-pixels.d.ts +0 -20
  132. package/dist/types/internal/use-anchor-position-at-point.d.ts +0 -67
  133. package/dist/types/internal/use-anchor-position.d.ts +0 -87
  134. package/dist/types/internal/use-width-from-anchor.d.ts +0 -19
  135. package/use-anchor-position/package.json +0 -10
  136. package/use-anchor-position-at-point/package.json +0 -10
  137. package/use-width-from-anchor/package.json +0 -10
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The CSS Anchor Positioning path: `position-anchor`, `position-area`,
3
+ * `position-try-fallbacks` and the gap / shift margins, all declared on the
4
+ * popover so the browser owns placement and flipping. The twin of
5
+ * `../javascript-fallback/apply-javascript-fallback-positioning`.
6
+ */
7
+
8
+ import { setStyle } from '../set-style';
9
+ import { crossAxisShiftMargins } from './cross-axis-shift-margins';
10
+ import { edgeMargin } from './edge-margin';
11
+ import { getFitMarginDeclarations } from './fit-margins';
12
+ import { getExistingAnchorName } from './get-existing-anchor-name';
13
+ import { placementToPositionArea } from './placement-to-position-area';
14
+ import { placementToTryFallbacks } from './placement-to-try-fallbacks';
15
+
16
+ /**
17
+ * Returns the cleanup.
18
+ */
19
+ export function applyAnchorPositioning({
20
+ anchor,
21
+ popover,
22
+ placement,
23
+ sizeStyles,
24
+ isFitting,
25
+ fallbackAnchorName
26
+ }) {
27
+ // Reuse rather than mint: another popover may already be anchored to it, or a
28
+ // CONSUMER may have set it on their own element, as `post-office` does.
29
+ const existingAnchorName = getExistingAnchorName(anchor);
30
+ const anchorName = existingAnchorName !== null && existingAnchorName !== void 0 ? existingAnchorName : fallbackAnchorName;
31
+ const gap = edgeMargin({
32
+ placement,
33
+ offset: placement.offset.gap
34
+ });
35
+ const crossAxisShift = crossAxisShiftMargins({
36
+ placement,
37
+ crossAxisShiftCssValue: placement.offset.crossAxisShift.value,
38
+ direction: placement.offset.crossAxisShift.direction
39
+ });
40
+ const popoverStyles = [{
41
+ property: 'position-anchor',
42
+ value: anchorName
43
+ }, {
44
+ property: 'position-area',
45
+ value: placementToPositionArea({
46
+ placement
47
+ })
48
+ }, {
49
+ property: 'position-try-fallbacks',
50
+ value: placementToTryFallbacks({
51
+ placement
52
+ })
53
+ },
54
+ // Reset browser default popover positioning that conflicts
55
+ // with anchor positioning (UA: `inset: 0; margin: auto;`)
56
+ {
57
+ property: 'margin',
58
+ value: '0'
59
+ }, {
60
+ property: 'inset',
61
+ value: 'auto'
62
+ }, gap,
63
+ // When fitting, the shift margins come back COMPOSED with the reserved
64
+ // viewport padding, rather than being written twice.
65
+ ...(isFitting ? getFitMarginDeclarations({
66
+ placement,
67
+ crossAxisShiftMargins: crossAxisShift
68
+ }) : crossAxisShift), ...sizeStyles];
69
+
70
+ // Anchor names are never cleaned up: multiple popovers can share an anchor,
71
+ // and even with reference counting an async React update could remove the name
72
+ // while something else still uses it. See
73
+ // `notes/decisions/anchor-name-lifetime.md`. Only written when minted: a name
74
+ // from a stylesheet may be a list, which an inline copy of its first entry
75
+ // would collapse.
76
+ if (existingAnchorName === null) {
77
+ anchor.style.setProperty('anchor-name', anchorName);
78
+ }
79
+ return setStyle({
80
+ element: popover,
81
+ styles: popoverStyles
82
+ });
83
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The cross-axis shift, written antisymmetrically on BOTH cross-axis sides rather
3
+ * than as one margin. Do not collapse it to one side: a single margin widens the
4
+ * margin box, so `anchor-center` displaces the border box by only half the value,
5
+ * a `<position-area>` fallback sliding across the cross axis drops it entirely,
6
+ * and the widened box changes where the popover is judged to overflow.
7
+ *
8
+ * A positive value always moves the popover toward the cross-axis END, so
9
+ * `direction: 'forwards'` means the same thing for every `align` value. See
10
+ * `notes/decisions/placement-offset.md`.
11
+ */
12
+ export function crossAxisShiftMargins({
13
+ placement,
14
+ crossAxisShiftCssValue,
15
+ direction
16
+ }) {
17
+ const crossAxis = placement.axis === 'block' ? 'inline' : 'block';
18
+ // `calc()` rather than JavaScript math, so opaque values such as design tokens
19
+ // negate correctly.
20
+ const negated = `calc(-1 * ${crossAxisShiftCssValue})`;
21
+ const isForwards = direction === 'forwards';
22
+ return [{
23
+ property: `margin-${crossAxis}-start`,
24
+ value: isForwards ? crossAxisShiftCssValue : negated
25
+ }, {
26
+ property: `margin-${crossAxis}-end`,
27
+ value: isForwards ? negated : crossAxisShiftCssValue
28
+ }];
29
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The margin that creates the gap between the popover and its anchor.
3
+ */
4
+ export function edgeMargin({
5
+ placement,
6
+ offset
7
+ }) {
8
+ const {
9
+ axis,
10
+ edge
11
+ } = placement;
12
+ // The side FACING the anchor is always opposite the placement edge.
13
+ const anchorFacingEdge = edge === 'end' ? 'start' : 'end';
14
+ return {
15
+ property: `margin-${axis}-${anchorFacingEdge}`,
16
+ value: offset
17
+ };
18
+ }
@@ -0,0 +1,69 @@
1
+ import { VIEWPORT_PADDING } from '../anchored-popover-size';
2
+
3
+ /**
4
+ * Every logical margin property the fit recipe can write. Declared here and
5
+ * imported by `edge-margin.tsx` and `cross-axis-shift-margins.tsx`: two
6
+ * structurally identical declarations are interchangeable to the compiler, so a
7
+ * divergence would surface only as a silently-ignored property name at
8
+ * `setStyle`.
9
+ */
10
+
11
+ /**
12
+ * The cross-axis sides that face the viewport rather than the anchor, and so take
13
+ * the reserved padding.
14
+ *
15
+ * A `span-*` cell aligns the popover's MARGIN box to the anchor's edge, so padding
16
+ * on the anchor-facing side does not reserve space at the viewport: it insets the
17
+ * border box from the edge the consumer asked it to line up with. A centred
18
+ * popover has no anchor-facing side, and padding both keeps the shift pair
19
+ * antisymmetric so `anchor-center` still centres.
20
+ *
21
+ * Known cost: the slide fallbacks do not swap the inline margins the way
22
+ * `flip-inline` does, so a slid popover keeps its padding on the anchor side (or,
23
+ * re-centred, sits 2.5px off centre). Alignment in the requested position wins.
24
+ */
25
+ function viewportFacingCrossAxisSides({
26
+ align
27
+ }) {
28
+ if (align === 'center') {
29
+ return ['start', 'end'];
30
+ }
31
+ // `align: 'start'` sits on the anchor's start edge and expands toward end.
32
+ return [align === 'start' ? 'end' : 'start'];
33
+ }
34
+
35
+ /**
36
+ * The margin half of the fit recipe. The caps reserve the viewport padding by
37
+ * subtracting it, but nothing makes the reservation land on the viewport side, so
38
+ * a capped popover packs flush against the edge it slid or flipped to.
39
+ *
40
+ * The padding buys visual clearance and nothing else: at the cap the margin box
41
+ * is exactly `100%` of the cell, so these margins never make
42
+ * `position-try-fallbacks` fire. Only the unclamped placement-axis floor does
43
+ * that; see `getPlacementAxisFloor`.
44
+ *
45
+ * The cross-axis sides are composed, not overwritten: they already carry the
46
+ * antisymmetric `crossAxisShift` pair.
47
+ */
48
+ export function getFitMarginDeclarations({
49
+ placement,
50
+ crossAxisShiftMargins
51
+ }) {
52
+ const crossAxis = placement.axis === 'block' ? 'inline' : 'block';
53
+ const paddedProperties = new Set(viewportFacingCrossAxisSides({
54
+ align: placement.align
55
+ }).map(side => `margin-${crossAxis}-${side}`));
56
+ return [{
57
+ property: `margin-${placement.axis}-${placement.edge}`,
58
+ value: VIEWPORT_PADDING
59
+ }, ...crossAxisShiftMargins.map(({
60
+ property,
61
+ value
62
+ }) => paddedProperties.has(property) ? {
63
+ property,
64
+ value: `calc(${value} + ${VIEWPORT_PADDING})`
65
+ } : {
66
+ property,
67
+ value
68
+ })];
69
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The `anchor-name` an element already carries, or `null`. Computed as well as
3
+ * inline, so a name set from a STYLESHEET is found: a minted inline name would
4
+ * otherwise override it for every other rule anchored to that element.
5
+ * `anchor-name` is a list and `position-anchor` takes one ident, so the first
6
+ * name is the one to anchor to.
7
+ */
8
+ export function getExistingAnchorName(element) {
9
+ const declared = element.style.getPropertyValue('anchor-name') || getComputedStyle(element).getPropertyValue('anchor-name');
10
+ const first = declared.split(',')[0].trim();
11
+ return first !== '' && first !== 'none' ? first : null;
12
+ }
@@ -1,7 +1,5 @@
1
- import { resolvePlacement } from './resolve-placement';
2
-
3
1
  /**
4
- * Resolves placement options to CSS `position-area` syntax.
2
+ * Resolves a placement to CSS `position-area` syntax.
5
3
  *
6
4
  * The `align` field represents the *visual* alignment:
7
5
  * - `align: 'start'` means "aligned to the trigger's start edge" (left in LTR for block axis)
@@ -12,10 +10,14 @@ import { resolvePlacement } from './resolve-placement';
12
10
  * Visual start-alignment requires `span-{cross-axis}-end` (expand toward end = start-aligned).
13
11
  * This function handles the inversion internally.
14
12
  *
13
+ * Takes the RESOLVED placement, which the hook threads through positioning,
14
+ * sizing and margins alike, so nothing here can disagree with them.
15
+ *
15
16
  * @example
16
- * placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'center' } }) // → 'block-end'
17
- * placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'start' } }) // → 'block-end span-inline-end'
18
- * placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'end' } }) // → 'block-end span-inline-start'
17
+ * // For a `block-end` placement, by `align`:
18
+ * // 'center' → 'block-end'
19
+ * // 'start' → 'block-end span-inline-end'
20
+ * // 'end' → 'block-end span-inline-start'
19
21
  */
20
22
  export function placementToPositionArea({
21
23
  placement
@@ -24,9 +26,7 @@ export function placementToPositionArea({
24
26
  axis,
25
27
  edge,
26
28
  align
27
- } = resolvePlacement({
28
- placement
29
- });
29
+ } = placement;
30
30
  const edgeValue = `${axis}-${edge}`;
31
31
  if (align === 'center') {
32
32
  return edgeValue;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The placement axis joined to its edge, e.g. `block-end`.
3
+ */
4
+
5
+ /**
6
+ * The cells to slide to along one edge, on the cross axis.
7
+ *
8
+ * A try option is only worth listing if its margin box can fit where an earlier
9
+ * one did not, and two options over the SAME cell have the same margin box and
10
+ * the same fit result. That rules out three cells for an aligned placement: the
11
+ * near span on the requested edge IS the base position-area, and on the opposite
12
+ * edge the near and far spans are where `flip-{A}` and the diagonal flip land.
13
+ *
14
+ * A centred placement has no near or far, so it slides to both spans on both
15
+ * edges.
16
+ */
17
+ function slideOnEdge({
18
+ align,
19
+ edgeKey,
20
+ crossAxis,
21
+ isOppositeEdge
22
+ }) {
23
+ if (align === 'center') {
24
+ return [`${edgeKey} span-${crossAxis}-end`, `${edgeKey} span-${crossAxis}-start`];
25
+ }
26
+ if (isOppositeEdge) {
27
+ return [edgeKey];
28
+ }
29
+ // For `align: 'start'` the popover expands toward `end`, so the far span is
30
+ // `start`; `align: 'end'` mirrors this.
31
+ const far = align === 'start' ? 'start' : 'end';
32
+ return [edgeKey, `${edgeKey} span-${crossAxis}-${far}`];
33
+ }
34
+ function flipDiagonally({
35
+ axis
36
+ }) {
37
+ const flipOnMainAxis = axis === 'block' ? 'flip-block' : 'flip-inline';
38
+ const flipToCrossAxis = axis === 'block' ? 'flip-inline' : 'flip-block';
39
+ return `${flipOnMainAxis} ${flipToCrossAxis}`;
40
+ }
41
+
42
+ /**
43
+ * Returns the `position-try-fallbacks` value for a RESOLVED placement. The
44
+ * browser tries the entries in order and uses the first one whose margin box
45
+ * fits, so an entry over a cell already tried can never be chosen; `slideOnEdge`
46
+ * explains which ones those are.
47
+ *
48
+ * Centred: slide along the requested edge to either span, flip to the opposite
49
+ * edge, then slide along that edge to either span.
50
+ *
51
+ * Start / end aligned: slide along the requested edge (re-centred, then the far
52
+ * span), flip to the opposite edge keeping the alignment, flip on the diagonal
53
+ * into the opposite corner, then re-centre on the opposite edge. The diagonal is
54
+ * the clean escape when the popover would land off screen on both axes: a flip
55
+ * keyword carries the gap and shift margins across, where a position-area slide
56
+ * leaves them on the original sides.
57
+ *
58
+ * @example
59
+ * // Centred on the block-end edge (the default placement):
60
+ * placementToTryFallbacks({ placement: resolvePlacement({ placement: {} }) });
61
+ * // => 'block-end span-inline-end, block-end span-inline-start, flip-block,
62
+ * // block-start span-inline-end, block-start span-inline-start'
63
+ *
64
+ * // Start-aligned on the block-end edge:
65
+ * placementToTryFallbacks({ placement: resolvePlacement({ placement: { align: 'start' } }) });
66
+ * // => 'block-end, block-end span-inline-start, flip-block, flip-block flip-inline, block-start'
67
+ */
68
+ export function placementToTryFallbacks({
69
+ placement
70
+ }) {
71
+ const {
72
+ axis,
73
+ edge,
74
+ align
75
+ } = placement;
76
+ const crossAxis = axis === 'block' ? 'inline' : 'block';
77
+ const flippedEdge = edge === 'start' ? 'end' : 'start';
78
+ const sameEdgeArea = `${axis}-${edge}`;
79
+ const oppositeEdgeArea = `${axis}-${flippedEdge}`;
80
+ const flipOnMainAxis = axis === 'block' ? 'flip-block' : 'flip-inline';
81
+ return [slideOnEdge({
82
+ align,
83
+ edgeKey: sameEdgeArea,
84
+ crossAxis,
85
+ isOppositeEdge: false
86
+ }), flipOnMainAxis, align !== 'center' ? flipDiagonally({
87
+ axis
88
+ }) : null, slideOnEdge({
89
+ align,
90
+ edgeKey: oppositeEdgeArea,
91
+ crossAxis,
92
+ isOppositeEdge: true
93
+ })].filter(Boolean).flat().join(', ');
94
+ }
@@ -0,0 +1,343 @@
1
+ /**
2
+ * The space kept clear between a popover and the viewport edge. Matches legacy
3
+ * `@atlaskit/popper`'s `preventOverflow` padding of `5`.
4
+ */
5
+ export const VIEWPORT_PADDING = '5px';
6
+
7
+ /**
8
+ * The upper bound for the default placement-axis floor while fitting. A capped
9
+ * popover never overflows, so it never flips; this floor makes the margin box
10
+ * overflow a too-small cell so `position-try-fallbacks` fires. A flip driver and
11
+ * nothing else, so the JavaScript fallback, which has no cell to overflow, never
12
+ * writes it. See `getPlacementAxisFloor`.
13
+ *
14
+ * 150px is the value the flip was measured with, not a design constant
15
+ * (`notes/plans/should-fit-viewport.md`). Deliberately not a `space.*` token:
16
+ * theming it would move the flip threshold, and so change which side a popover
17
+ * lands on.
18
+ */
19
+ export const FALLBACK_MINIMUM_MAIN_AXIS_SIZE = '150px';
20
+
21
+ /**
22
+ * Which way the POPOVER's inline axis runs; `'vertical'` covers the `vertical-*`
23
+ * and `sideways-*` writing modes. Measured rather than assumed, because every
24
+ * logical property this module writes resolves against the popover's own writing
25
+ * mode, and so must the viewport unit paired with it.
26
+ */
27
+
28
+ /**
29
+ * The dynamic viewport unit for one of the popover's axes. Not `dvi` / `dvb`:
30
+ * css-values-4 defines those against the ROOT element's writing mode, while the
31
+ * `max-{axis}-size` they are written to follows the popover's, so a popover in a
32
+ * `vertical-rl` subtree of a horizontal document would be capped by the wrong
33
+ * dimension.
34
+ */
35
+ function viewportUnit({
36
+ axis,
37
+ writingMode
38
+ }) {
39
+ const isHorizontalAxis = axis === 'inline' === (writingMode === 'horizontal');
40
+ return isHorizontalAxis ? 'dvw' : 'dvh';
41
+ }
42
+
43
+ /**
44
+ * How one axis of an anchored popover is sized. Both axes take these four values
45
+ * under identical rules.
46
+ *
47
+ * - `'content'`: sized by its content. Named after `flex-basis: content`; NOT
48
+ * `'auto'`, whose CSS meaning for a positioned box is shrink-to-fit, the very
49
+ * behaviour rule 4 removes.
50
+ * - `'match-anchor'`: exactly the anchor's size on this axis.
51
+ * - `'min-anchor'`: at least the anchor's size on this axis, free to grow.
52
+ * - `'max-available'`: at most the space between the anchor and the viewport
53
+ * edge, content-sized below that. Capped by the `position-area` cell on the
54
+ * placement axis and by the viewport on the cross axis (a `span-*` cell is
55
+ * narrower than the viewport, so capping to it would wrap content that fits on
56
+ * screen).
57
+ *
58
+ * Four rules apply on top:
59
+ *
60
+ * 1. Fitting one axis fills a `'content'` other axis with `'max-available'` too.
61
+ * An explicit value is never overridden, so caps stay strictly per-axis.
62
+ * 2. Fitting either axis floors the placement axis, because flipping is a
63
+ * whole-popover outcome. See `getPlacementAxisFloor`.
64
+ * 3. The viewport cap is unconditional, so a non-fitting axis still cannot
65
+ * overhang the screen. `'max-available'` and `'content'` are therefore
66
+ * indistinguishable on the cross axis.
67
+ * 4. A non-anchor-relative inline axis gets `inline-size: max-content`. Under
68
+ * `position-area` the containing block is the cell, so a CSS `auto` inline
69
+ * size shrink-to-fits a too-narrow cell and WRAPS into it, never overflowing
70
+ * and so never sliding to the roomier side. The caps still beat it, because a
71
+ * max beats a definite size. Not needed on the block axis, which does not
72
+ * wrap, nor on the JavaScript fallback, whose containing block is the
73
+ * viewport. Consumers opt out with `shouldPreserveInlineSize`.
74
+ *
75
+ * See `notes/decisions/fit-available-space.md`.
76
+ */
77
+
78
+ /**
79
+ * Narrowed rather than `string`, so a typo cannot reach `setStyle` as a
80
+ * silently-ignored property name.
81
+ */
82
+
83
+ /**
84
+ * The anchor's size along each axis, as a CSS length. `null` when nothing needs
85
+ * it, so the measurement is skipped.
86
+ *
87
+ * The CSS path uses the `self-` keywords, whose axes are the POPOVER's, so each
88
+ * value matches the axis of the property it is set on (`anchor-size(inline)` and
89
+ * `anchor-size(width)` are both silently wrong in a vertical writing mode). The
90
+ * JavaScript fallback uses a measured pixel length, because `anchor-size()` needs
91
+ * a `position-anchor` that path never sets. See
92
+ * `notes/decisions/width-from-anchor-floors.md`.
93
+ */
94
+
95
+ function getCrossAxis(axis) {
96
+ return axis === 'block' ? 'inline' : 'block';
97
+ }
98
+
99
+ /**
100
+ * The whole viewport, less the reserved padding on both sides. Rule 3's backstop.
101
+ */
102
+ function viewportCap({
103
+ axis,
104
+ writingMode
105
+ }) {
106
+ return `calc(100${viewportUnit({
107
+ axis,
108
+ writingMode
109
+ })} - 2 * ${VIEWPORT_PADDING})`;
110
+ }
111
+
112
+ /**
113
+ * The position-area cell cap, for the placement axis only. The host carries
114
+ * `position-area`, so per css-anchor-position-1 the cell is its containing block:
115
+ * `100%` is the anchor edge to the viewport edge, and it follows whichever side
116
+ * `position-try-fallbacks` settles on. Percentages do not subtract margins, so
117
+ * the gap is subtracted here.
118
+ *
119
+ * Meaningless on the JavaScript fallback, whose containing block is the viewport,
120
+ * so that path gets `viewportCap` on both axes.
121
+ */
122
+ function cellCap({
123
+ gap
124
+ }) {
125
+ return `calc(100% - ${VIEWPORT_PADDING} - ${gap})`;
126
+ }
127
+
128
+ /**
129
+ * The anchor term of the placement-axis floor, and the one place it is capped.
130
+ *
131
+ * `'match-anchor'` earns a floor only where it can flip: a definite size clamps to
132
+ * the cap and shrinks below the anchor instead of flipping.
133
+ *
134
+ * The `canFlip` clamp is deliberate both ways. With no flip in play an unclamped
135
+ * floor beats `max-{axis}-size` (CSS resolves `min` last), making the viewport
136
+ * backstop inert. With a flip in play, clamping would stop the floor exceeding
137
+ * the cell cap, which is what drives the flip.
138
+ */
139
+ function getPlacementAxisAnchorFloor({
140
+ placementAxis,
141
+ placementAxisSize,
142
+ anchorValue,
143
+ canFlip,
144
+ writingMode
145
+ }) {
146
+ const hasAnchorFloor = placementAxisSize === 'min-anchor' || placementAxisSize === 'match-anchor' && canFlip;
147
+ if (!hasAnchorFloor || anchorValue === null) {
148
+ return undefined;
149
+ }
150
+ if (canFlip) {
151
+ return anchorValue;
152
+ }
153
+ return `min(${anchorValue}, ${viewportCap({
154
+ axis: placementAxis,
155
+ writingMode
156
+ })})`;
157
+ }
158
+
159
+ /**
160
+ * The default flip floor, clamped so that SOME cell can always hold it.
161
+ *
162
+ * The two cells either side of the anchor add up to the viewport less the anchor,
163
+ * so the roomier one is at least half of that. A floor whose margin box fits
164
+ * inside that half is guaranteed a cell somewhere in the fallback chain.
165
+ * Unclamped, a short viewport with a centred anchor has NO cell that holds
166
+ * 150px: every try-fallback overflows, the browser reverts to the base position,
167
+ * and the floor pushes the popover past the viewport edge.
168
+ *
169
+ * This is NOT the `min(floor, cellCap)` that suppresses flipping: it is relative
170
+ * to the VIEWPORT, so a small base cell still overflows it. `max(0px, …)` states
171
+ * the lower bound for an anchor taller than the viewport.
172
+ */
173
+ function getDefaultPlacementAxisFloor({
174
+ placementAxis,
175
+ anchorValue,
176
+ gap,
177
+ writingMode
178
+ }) {
179
+ if (anchorValue === null) {
180
+ return FALLBACK_MINIMUM_MAIN_AXIS_SIZE;
181
+ }
182
+ // The viewport term and the anchor term must measure the SAME axis; see
183
+ // `viewportUnit`.
184
+ const viewport = `100${viewportUnit({
185
+ axis: placementAxis,
186
+ writingMode
187
+ })}`;
188
+ const halfOfRemainingViewport = `calc((${viewport} - ${anchorValue}) / 2 - ${gap} - ${VIEWPORT_PADDING})`;
189
+ return `min(${FALLBACK_MINIMUM_MAIN_AXIS_SIZE}, max(0px, ${halfOfRemainingViewport}))`;
190
+ }
191
+
192
+ /**
193
+ * The placement-axis floor: explicit minimums COMPOSE, the default does not.
194
+ *
195
+ * `placement.minSize` and the anchor term are both explicit requests, so they
196
+ * compose as `max()`. `FALLBACK_MINIMUM_MAIN_AXIS_SIZE` never composes: it only
197
+ * ever won by overriding a size the consumer asked for, and the anchor term drives
198
+ * the flip on its own. Knock-on, intended: `minSize: 0` no longer removes an
199
+ * anchor floor, because `max(0px, anchor)` IS the anchor.
200
+ */
201
+ function getPlacementAxisFloor({
202
+ minSize,
203
+ placementAxis,
204
+ placementAxisSize,
205
+ anchorValue,
206
+ gap,
207
+ canFlip,
208
+ writingMode
209
+ }) {
210
+ const anchorFloor = getPlacementAxisAnchorFloor({
211
+ placementAxis,
212
+ placementAxisSize,
213
+ anchorValue,
214
+ canFlip,
215
+ writingMode
216
+ });
217
+ if (minSize !== undefined) {
218
+ return anchorFloor === undefined ? minSize : `max(${minSize}, ${anchorFloor})`;
219
+ }
220
+ if (anchorFloor !== undefined) {
221
+ return anchorFloor;
222
+ }
223
+ if (!canFlip) {
224
+ return undefined;
225
+ }
226
+ return getDefaultPlacementAxisFloor({
227
+ placementAxis,
228
+ anchorValue,
229
+ gap,
230
+ writingMode
231
+ });
232
+ }
233
+
234
+ /**
235
+ * The whole size recipe for an anchored popover, in one place. Pure, so the truth
236
+ * table is unit-testable without a browser, and every size property on the
237
+ * popover host has exactly one writer.
238
+ *
239
+ * While anything is fitting on the CSS path, the placement-axis floor is never
240
+ * clamped to the CELL cap. Its whole job is to exceed it: overflow detection
241
+ * reads the margin box AFTER the clamp (css-anchor-position-1 §6.5), so
242
+ * `min(floor, cap)` suppresses `position-try-fallbacks` entirely. The cross-axis
243
+ * floor is always clamped, because there it is a size contract rather than a flip
244
+ * driver.
245
+ *
246
+ * See `notes/decisions/fit-available-space.md` for the full recipe and the
247
+ * measured evidence.
248
+ */
249
+ export function getAnchoredPopoverSizeDeclarations({
250
+ placement,
251
+ inlineSize,
252
+ blockSize,
253
+ isUsingCssAnchorPositioning,
254
+ anchorSize,
255
+ writingMode,
256
+ shouldPreserveInlineSize = false
257
+ }) {
258
+ const placementAxis = placement.axis;
259
+ const crossAxis = getCrossAxis(placementAxis);
260
+
261
+ // Rule 1. Feeds the per-axis CAPS only.
262
+ const size = {
263
+ inline: inlineSize === 'content' && blockSize === 'max-available' ? 'max-available' : inlineSize,
264
+ block: blockSize === 'content' && inlineSize === 'max-available' ? 'max-available' : blockSize
265
+ };
266
+
267
+ // Rule 2 reads the requested values instead, un-mirrored: flipping is a
268
+ // whole-popover outcome, so a fit request on either axis turns the floor on.
269
+ const isFitting = inlineSize === 'max-available' || blockSize === 'max-available';
270
+
271
+ // ...but only where the floor is what causes the flip. On the JavaScript
272
+ // fallback a floor only inflates the measured size the side is picked from, so
273
+ // it would flip a popover that fits.
274
+ const canFlip = isFitting && isUsingCssAnchorPositioning;
275
+ const declarations = [
276
+ // Rule 3: both caps are always written. Only the placement axis can take the
277
+ // cell cap.
278
+ {
279
+ property: `max-${placementAxis}-size`,
280
+ value: size[placementAxis] === 'max-available' && isUsingCssAnchorPositioning ? cellCap({
281
+ gap: placement.offset.gap
282
+ }) : viewportCap({
283
+ axis: placementAxis,
284
+ writingMode
285
+ })
286
+ }, {
287
+ property: `max-${crossAxis}-size`,
288
+ value: viewportCap({
289
+ axis: crossAxis,
290
+ writingMode
291
+ })
292
+ }];
293
+
294
+ // Rule 4. Only for a non-anchor-relative inline axis, so `'match-anchor'` below
295
+ // stays the one writer of `inline-size` when it applies. CSS path only: the
296
+ // fallback's containing block is the viewport, where shrink-to-fit already IS
297
+ // the natural width for anything that fits on screen.
298
+ if (isUsingCssAnchorPositioning && !shouldPreserveInlineSize && (size.inline === 'content' || size.inline === 'max-available')) {
299
+ declarations.push({
300
+ property: 'inline-size',
301
+ value: 'max-content'
302
+ });
303
+ }
304
+
305
+ // A max beats a definite size, so the caps above still win over this.
306
+ for (const axis of [placementAxis, crossAxis]) {
307
+ const anchorValue = anchorSize[axis];
308
+ if (size[axis] === 'match-anchor' && anchorValue !== null) {
309
+ declarations.push({
310
+ property: `${axis}-size`,
311
+ value: anchorValue
312
+ });
313
+ }
314
+ }
315
+ const placementAxisFloor = getPlacementAxisFloor({
316
+ minSize: placement.minSize,
317
+ placementAxis,
318
+ placementAxisSize: size[placementAxis],
319
+ anchorValue: anchorSize[placementAxis],
320
+ gap: placement.offset.gap,
321
+ canFlip,
322
+ writingMode
323
+ });
324
+ if (placementAxisFloor !== undefined) {
325
+ declarations.push({
326
+ property: `min-${placementAxis}-size`,
327
+ value: placementAxisFloor
328
+ });
329
+ }
330
+
331
+ // A size contract, not a flip driver, so it is clamped to the cap.
332
+ const crossAxisAnchorValue = anchorSize[crossAxis];
333
+ if (size[crossAxis] === 'min-anchor' && crossAxisAnchorValue !== null) {
334
+ declarations.push({
335
+ property: `min-${crossAxis}-size`,
336
+ value: `min(${crossAxisAnchorValue}, ${viewportCap({
337
+ axis: crossAxis,
338
+ writingMode
339
+ })})`
340
+ });
341
+ }
342
+ return declarations;
343
+ }