@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,116 @@
1
+ /**
2
+ * Positioning and sizing for a popover anchored to a viewport COORDINATE. No
3
+ * positioning logic here: CSS Anchor Positioning needs a real element to carry
4
+ * `anchor-name`, so this hook creates a hidden, zero-size one at the requested
5
+ * point and delegates everything else to `useAnchoredPopover`.
6
+ *
7
+ * See `notes/decisions/anchored-popover-at-point.md` for why this is a sibling
8
+ * hook instead of an anchor union inside `useAnchoredPopover`.
9
+ */
10
+ import { useLayoutEffect, useRef } from 'react';
11
+ import { useAnchoredPopover } from './use-anchored-popover';
12
+
13
+ /**
14
+ * A point in viewport coordinates (CSS pixels). Axis-named rather than
15
+ * edge-named, because viewport coordinates are always physical.
16
+ */
17
+
18
+ /**
19
+ * Appended to `document.body` to escape any transformed ancestor, which would
20
+ * break viewport-relative resolution of `position: fixed`. `Object.assign`
21
+ * rather than `setStyle`: we own the element, so there is nothing to restore.
22
+ */
23
+ function mountAnchorElementAtPoint({
24
+ point
25
+ }) {
26
+ const element = document.createElement('div');
27
+ element.setAttribute('aria-hidden', 'true');
28
+ Object.assign(element.style, {
29
+ position: 'fixed',
30
+ // Physical insets: `inset-inline-start` would need `viewportWidth - point.x`
31
+ // in RTL.
32
+ top: `${point.y}px`,
33
+ left: `${point.x}px`,
34
+ // Zero size, so placement alone decides which side the popover sits on.
35
+ width: '0',
36
+ height: '0',
37
+ pointerEvents: 'none'
38
+ });
39
+ document.body.appendChild(element);
40
+ return {
41
+ element,
42
+ cleanup: function cleanup() {
43
+ element.remove();
44
+ }
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Positions and sizes a popover at a viewport coordinate. Takes every option
50
+ * `useAnchoredPopover` does except `anchorRef`, with the same meaning. One caveat
51
+ * is specific to a point: it has no size, so `'match-anchor'` and `'min-anchor'`
52
+ * resolve to zero against it.
53
+ *
54
+ * A consumer that anchors to an element sometimes and to a point at other times
55
+ * calls BOTH hooks with complementary `isEnabled` values, since a hook cannot be
56
+ * called conditionally. Nothing enforces that; see `isEnabled` on
57
+ * `useAnchoredPopover` for why two enabled hooks on one popover is a bug.
58
+ *
59
+ * @example Anchor at the cursor, falling back to the trigger element
60
+ * ```tsx
61
+ * const shared = { popoverRef, placement, isOpen };
62
+ *
63
+ * useAnchoredPopover({ ...shared, anchorRef: triggerRef, isEnabled: !mousePos });
64
+ * useAnchoredPopoverAtPoint({
65
+ * ...shared,
66
+ * isEnabled: Boolean(mousePos),
67
+ * // Read through a ref: `getPoint` is latched, see below.
68
+ * getPoint: () => mousePosRef.current,
69
+ * });
70
+ * ```
71
+ */
72
+ export function useAnchoredPopoverAtPoint({
73
+ getPoint,
74
+ isEnabled = true,
75
+ ...rest
76
+ }) {
77
+ const anchorRef = useRef(null);
78
+
79
+ // Latched, keyed on `isEnabled`: consumers read mutable refs inside `getPoint`,
80
+ // so re-reading it would move an already-open popover, and keying on
81
+ // `getPoint`'s identity would re-latch on every parent render.
82
+ const getPointRef = useRef(getPoint);
83
+ getPointRef.current = getPoint;
84
+
85
+ // Declared BEFORE `useAnchoredPopover` below, so the element exists when that
86
+ // hook's effect reads the ref (React runs layout effects in source order). This
87
+ // effect must not depend on anything the delegated effect does not: a dependency
88
+ // here alone re-creates the element without that hook writing an `anchor-name`
89
+ // onto it, and positioning silently dies.
90
+ useLayoutEffect(() => {
91
+ if (!isEnabled) {
92
+ return;
93
+ }
94
+ const point = getPointRef.current();
95
+ if (!point) {
96
+ return;
97
+ }
98
+ const {
99
+ element,
100
+ cleanup: unmountAnchorElement
101
+ } = mountAnchorElementAtPoint({
102
+ point
103
+ });
104
+ anchorRef.current = element;
105
+ return function cleanup() {
106
+ unmountAnchorElement();
107
+ anchorRef.current = null;
108
+ };
109
+ // The latch. See `getPointRef` above: `getPoint` is deliberately absent.
110
+ }, [isEnabled]);
111
+ useAnchoredPopover({
112
+ ...rest,
113
+ anchorRef,
114
+ isEnabled
115
+ });
116
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Positioning AND anchor-relative sizing for top-layer popovers. Anchors an
3
+ * ELEMENT; to anchor a viewport coordinate use `useAnchoredPopoverAtPoint`.
4
+ *
5
+ * Two runtime paths, each a function that applies its styles and returns the
6
+ * cleanup: CSS Anchor Positioning in `./anchor-positioning`, and the JavaScript
7
+ * fallback in `./javascript-fallback`. The path is resolved ONCE, at the top of
8
+ * the hook, and threaded through positioning, sizing and margins.
9
+ *
10
+ * See `notes/architecture/positioning.md` and
11
+ * `notes/decisions/fit-available-space.md`.
12
+ */
13
+ import { useLayoutEffect, useMemo } from 'react';
14
+ import { useId } from '@atlaskit/ds-lib/use-id';
15
+ import { applyAnchorPositioning } from './anchor-positioning/apply-anchor-positioning';
16
+ import { getAnchoredPopoverSizeDeclarations } from './anchored-popover-size';
17
+ import { applyJavaScriptFallbackPositioning } from './javascript-fallback/apply-javascript-fallback-positioning';
18
+ import { resolvePlacement } from './resolve-placement';
19
+ import { supportsAnchorPositioning } from './supports-anchor-positioning';
20
+ import { supportsAnchorSize } from './supports-anchor-size';
21
+
22
+ /**
23
+ * Which way the popover's inline axis runs. Maps every physical measurement below
24
+ * onto a logical axis: the viewport unit of each cap and the anchor's measured
25
+ * size. A style read, not a layout read, so it is cheap enough to take for every
26
+ * popover.
27
+ */
28
+ function getPopoverWritingMode({
29
+ popover
30
+ }) {
31
+ // `String()` because not every test environment implements `writing-mode`.
32
+ const writingMode = String(getComputedStyle(popover).writingMode);
33
+ const isVertical = writingMode.startsWith('vertical') || writingMode.startsWith('sideways');
34
+ return isVertical ? 'vertical' : 'horizontal';
35
+ }
36
+
37
+ /**
38
+ * Measured stand-in for `anchor-size(self-*)`, used wherever `anchor-size()` is
39
+ * not: the JavaScript fallback, and browsers with anchor positioning but no
40
+ * `anchor-size()`.
41
+ *
42
+ * `offsetWidth` / `offsetHeight` are physical, so the axis is picked from the
43
+ * POPOVER's `writing-mode`. Deliberately not memoized: a cached anchor size is a
44
+ * stale one. Runs once per effect, not per frame.
45
+ */
46
+ function measureAnchorSize({
47
+ anchor,
48
+ writingMode
49
+ }) {
50
+ if (writingMode === 'vertical') {
51
+ return {
52
+ inline: `${anchor.offsetHeight}px`,
53
+ block: `${anchor.offsetWidth}px`
54
+ };
55
+ }
56
+ return {
57
+ inline: `${anchor.offsetWidth}px`,
58
+ block: `${anchor.offsetHeight}px`
59
+ };
60
+ }
61
+ const NO_ANCHOR_SIZE = {
62
+ inline: null,
63
+ block: null
64
+ };
65
+ const CSS_ANCHOR_SIZE = {
66
+ inline: 'anchor-size(self-inline)',
67
+ block: 'anchor-size(self-block)'
68
+ };
69
+ function isAnchorRelative(size) {
70
+ return size === 'match-anchor' || size === 'min-anchor';
71
+ }
72
+
73
+ /**
74
+ * The `anchor-name` written to an anchor that does not already carry one, as a CSS
75
+ * `<dashed-ident>`. `useId()` is the same source as every other id in the design
76
+ * system, including the popover's own `id` that `aria-controls` and `popovertarget`
77
+ * point at, so it is unique exactly where those are and collides exactly where
78
+ * they do. See `useAnchoredPopover` for the one case that needs the host's help.
79
+ *
80
+ * `@atlaskit/ds-lib`'s `useId` already swaps React's `:` and `«»` for `_`. The
81
+ * replace covers a host `identifierPrefix`, which may carry characters a
82
+ * `<dashed-ident>` cannot.
83
+ */
84
+ function useNewAnchorName() {
85
+ const id = useId();
86
+ return useMemo(() => `--anchor-${id.replace(/[^A-Za-z0-9_-]/g, '_')}`, [id]);
87
+ }
88
+
89
+ /**
90
+ * A referentially-stable, fully-resolved `TPlacement`, so a fresh inline
91
+ * `placement` object does not re-run the effect.
92
+ */
93
+ function useStablePlacement(placement) {
94
+ const resolved = resolvePlacement({
95
+ placement
96
+ });
97
+ const axis = resolved.axis;
98
+ const edge = resolved.edge;
99
+ const align = resolved.align;
100
+ const minSize = resolved.minSize;
101
+ const gap = resolved.offset.gap;
102
+ const shiftValue = resolved.offset.crossAxisShift.value;
103
+ const shiftDirection = resolved.offset.crossAxisShift.direction;
104
+ // Rebuilt from primitive deps, so the factory does not close over `resolved`
105
+ // and exhaustive-deps can verify it.
106
+ return useMemo(() => ({
107
+ axis,
108
+ edge,
109
+ align,
110
+ minSize,
111
+ offset: {
112
+ gap,
113
+ crossAxisShift: {
114
+ value: shiftValue,
115
+ direction: shiftDirection
116
+ }
117
+ }
118
+ }), [axis, edge, align, minSize, gap, shiftValue, shiftDirection]);
119
+ }
120
+
121
+ /**
122
+ * Exported so `useAnchoredPopoverAtPoint` can forward the options it does not own.
123
+ */
124
+
125
+ /**
126
+ * Positions and sizes a popover relative to an anchor. Owns every inline style on
127
+ * the popover host: where it goes, how big it is relative to its anchor, and the
128
+ * margins that keep it clear of the anchor and the viewport edge. It knows nothing
129
+ * about visibility or animation - compose it with `Popover`.
130
+ *
131
+ * On modern browsers the browser handles flipping via CSS Anchor Positioning;
132
+ * otherwise it falls back to measured `position: fixed` coordinates, re-run on
133
+ * scroll and resize.
134
+ *
135
+ * `inlineSize` and `blockSize` both default to `'content'`. Two rules are
136
+ * surprising: fitting one axis also fits an axis left on `'content'`, and fitting
137
+ * either axis floors the placement axis. See `TPopoverAxisSize`.
138
+ *
139
+ * **Several React roots on one page.** The `anchor-name` this hook writes to an
140
+ * anchor that has none is minted from `useId()`, like every other id in the design
141
+ * system. `useId()` is unique within a root; across roots it is unique for
142
+ * client-rendered roots but NOT for roots that hydrate the same markup, where it
143
+ * is derived from tree position. Two anchors carrying one name resolve to the last
144
+ * in tree order, so a popover can attach to another root's trigger. A host that
145
+ * hydrates more than one root must give each a distinct `identifierPrefix` (on
146
+ * `hydrateRoot` and the matching server render). That is already required for the
147
+ * popover's own `id`, which `aria-controls` and `popovertarget` point at, and for
148
+ * every `aria-*` id the design system mints. A bundle with two copies of
149
+ * `react-dom` has the same exposure for client-rendered roots.
150
+ *
151
+ * @example Position a popover below its trigger
152
+ * ```tsx
153
+ * const triggerRef = useRef<HTMLButtonElement>(null);
154
+ * const popoverRef = useRef<HTMLDivElement>(null);
155
+ *
156
+ * useAnchoredPopover({
157
+ * anchorRef: triggerRef,
158
+ * popoverRef,
159
+ * placement: { axis: 'block', edge: 'end' },
160
+ * isOpen,
161
+ * });
162
+ * ```
163
+ *
164
+ * @example Match the trigger's width and keep the popover on screen
165
+ * ```tsx
166
+ * // What `<Popup shouldFitContainer shouldFitViewport>` maps to.
167
+ * useAnchoredPopover({
168
+ * anchorRef: triggerRef,
169
+ * popoverRef,
170
+ * placement: { axis: 'block', edge: 'end' },
171
+ * isOpen,
172
+ * inlineSize: 'match-anchor',
173
+ * blockSize: 'max-available',
174
+ * });
175
+ * ```
176
+ *
177
+ * @example Switch to a cursor position mid-mount
178
+ * ```tsx
179
+ * // Complementary `isEnabled`, so exactly one hook owns the popover.
180
+ * const shared = { popoverRef, placement, isOpen };
181
+ *
182
+ * useAnchoredPopover({ ...shared, anchorRef: triggerRef, isEnabled: !mousePos });
183
+ * useAnchoredPopoverAtPoint({
184
+ * ...shared,
185
+ * isEnabled: Boolean(mousePos),
186
+ * getPoint: () => mousePosRef.current,
187
+ * });
188
+ * ```
189
+ */
190
+ export function useAnchoredPopover({
191
+ anchorRef,
192
+ popoverRef,
193
+ placement,
194
+ isOpen,
195
+ isEnabled = true,
196
+ inlineSize = 'content',
197
+ blockSize = 'content',
198
+ shouldPreserveInlineSize = false,
199
+ forceFallbackPositioning = false
200
+ }) {
201
+ // Stable for the life of the hook instance, so it follows a replacement anchor
202
+ // element (the point hook remounts its `<div>`).
203
+ const fallbackAnchorName = useNewAnchorName();
204
+ const stablePlacement = useStablePlacement(placement);
205
+ useLayoutEffect(() => {
206
+ if (!isEnabled) {
207
+ return;
208
+ }
209
+
210
+ // Resolved once, so positioning, sizing and margins cannot disagree about it.
211
+ const isUsingCssAnchorPositioning = supportsAnchorPositioning() && !forceFallbackPositioning;
212
+
213
+ // BOTH probes: a browser can have positioning without sizing, and
214
+ // `anchor-size()` needs the `position-anchor` only the CSS path writes.
215
+ const canUseAnchorSize = isUsingCssAnchorPositioning && supportsAnchorSize();
216
+ const trigger = anchorRef.current;
217
+ const popover = popoverRef.current;
218
+ if (!trigger || !popover) {
219
+ return;
220
+ }
221
+
222
+ // Fitting also reserves padding from the viewport edge as a margin.
223
+ const isFitting = inlineSize === 'max-available' || blockSize === 'max-available';
224
+
225
+ // Always read: the viewport caps are always written, and their unit follows
226
+ // the popover's writing mode.
227
+ const writingMode = getPopoverWritingMode({
228
+ popover
229
+ });
230
+
231
+ // Skipped where possible, so a position-only consumer pays no layout read.
232
+ // Fitting on the CSS path needs it to clamp the default flip floor; see
233
+ // `getDefaultPlacementAxisFloor`.
234
+ const needsAnchorSize = isAnchorRelative(inlineSize) || isAnchorRelative(blockSize) || isFitting && isUsingCssAnchorPositioning;
235
+ const anchorSize = needsAnchorSize ? canUseAnchorSize ? CSS_ANCHOR_SIZE : measureAnchorSize({
236
+ anchor: trigger,
237
+ writingMode
238
+ }) : NO_ANCHOR_SIZE;
239
+ const sizeStyles = getAnchoredPopoverSizeDeclarations({
240
+ placement: stablePlacement,
241
+ inlineSize,
242
+ blockSize,
243
+ isUsingCssAnchorPositioning,
244
+ anchorSize,
245
+ writingMode,
246
+ shouldPreserveInlineSize
247
+ });
248
+ if (isUsingCssAnchorPositioning) {
249
+ return applyAnchorPositioning({
250
+ anchor: trigger,
251
+ popover,
252
+ placement: stablePlacement,
253
+ sizeStyles,
254
+ isFitting,
255
+ fallbackAnchorName
256
+ });
257
+ }
258
+ return applyJavaScriptFallbackPositioning({
259
+ anchor: trigger,
260
+ popover,
261
+ placement: stablePlacement,
262
+ sizeStyles
263
+ });
264
+ }, [isEnabled, anchorRef, popoverRef, stablePlacement, inlineSize, blockSize, shouldPreserveInlineSize, fallbackAnchorName, forceFallbackPositioning, isOpen]);
265
+ }
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useLayoutEffect, useReducer, useRef } from 'react';
2
+ import { isNativeElementOpen } from './is-native-element-open';
2
3
  import { prefersReducedMotion } from './reduced-motion';
3
4
  function runCancellableSettlement({
4
5
  schedule,
@@ -34,20 +35,6 @@ function runOnAnimationsSettled({
34
35
  }
35
36
  });
36
37
  }
37
- function isNativeElementOpen({
38
- element
39
- }) {
40
- if (element instanceof HTMLDialogElement) {
41
- return element.open;
42
- }
43
-
44
- // Some DOM implementations throw for unsupported pseudo-classes.
45
- try {
46
- return element.matches(':popover-open');
47
- } catch {
48
- return false;
49
- }
50
- }
51
38
 
52
39
  /**
53
40
  * Visibility lifecycle phase shared between `useAnimatedVisibility`,
@@ -22,6 +22,10 @@
22
22
  ._n3tdidpf{padding-bottom:0}
23
23
  ._o5724jg8{white-space:normal}
24
24
  ._otyridpf{margin-bottom:0}
25
+ ._p6861txw:popover-open{display:flex}
25
26
  ._u5f3idpf{padding-right:0}
26
27
  ._y3gnv2br{text-align:start}
28
+ :where(._1yp3idpf)>*{min-inline-size:0}
29
+ :where(._51hckb7n)>*{flex-grow:1}
30
+ :where(._f5r5idpf)>*{min-block-size:0}
27
31
  @media (prefers-reduced-motion:reduce){._bl8aglyw{animation-name:none}._156y147i{transition-duration:var(--ds-duration-instant,0ms)}}
@@ -7,8 +7,8 @@ import { bind } from 'bind-event-listener';
7
7
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
8
8
  import once from '@atlaskit/ds-lib/once';
9
9
  import { useNotifyOpenLayerObserver } from '@atlaskit/layering/use-notify-open-layer-observer';
10
+ import { isNativeElementOpen } from '../internal/is-native-element-open';
10
11
  import { resolvePlacement } from '../internal/resolve-placement';
11
- import { shouldFocusIntoPopover } from '../internal/should-focus-into-popover';
12
12
  import { useAnimatedVisibility } from '../internal/use-animated-visibility';
13
13
  import { useFocusWrap } from '../internal/use-focus-wrap';
14
14
  import { useInitialFocus } from '../internal/use-initial-focus';
@@ -33,15 +33,22 @@ const supportsPopoverHint = once(() => {
33
33
  // avoided this by rendering at `<body>`). Excludes `color`/`font` (theming) and
34
34
  // `direction`/`unicode-bidi` (RTL must inherit).
35
35
  //
36
+ // Not `all: initial`: the target is what `<body>` gave the portal path, not the
37
+ // spec initial values (UA serif, black, `color-scheme: normal`), and as an author
38
+ // declaration it would also override the UA `[popover]` rules (`position: fixed`,
39
+ // `display: none` when closed).
40
+ //
36
41
  // KEEP IN SYNC with the identical `surfaceResetStyles` in `dialog/dialog-content.tsx`.
37
42
  // ADS forbids sharing styles across files (`no-exported-styles` /
38
43
  // `no-imported-style-values` — Compiled styles are null at runtime), so the reset is
39
- // co-located and duplicated deliberately.
44
+ // co-located and duplicated deliberately. The `satisfies` check below fails the
45
+ // build if either copy drifts.
40
46
  const surfaceResetStyles = {
41
47
  root: "_lcxv1wug _o5724jg8 _1nmz4jg8 _1i4q4jg8 _y3gnv2br _azhwidpf _1p1dglyw"
42
48
  };
49
+ true;
43
50
  const styles = {
44
- root: "_19itglyw _ca0qidpf _u5f3idpf _n3tdidpf _19bvidpf _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1r041wug _1reoewfl _18m9ewfl _bfhk1j28 _4t3i1wug",
51
+ root: "_19itglyw _ca0qidpf _u5f3idpf _n3tdidpf _19bvidpf _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1r041wug _1reoewfl _18m9ewfl _bfhk1j28 _4t3i1wug _p6861txw _51hckb7n _1yp3idpf _f5r5idpf",
45
52
  motion: "_k8m011k7 _1oecdn4k _1s9z1a5r _1o511hrf _bl8aglyw _156y147i",
46
53
  enter: "_y44v179n",
47
54
  exit: "_y44vof1f"
@@ -79,7 +86,7 @@ const POPUP_ROLES = new Set(['menu', 'listbox', 'dialog', 'alertdialog', 'tree',
79
86
 
80
87
  /**
81
88
  * Unopinionated top-layer primitive. Owns visibility and animation only;
82
- * compose with `useAnchorPosition` / `useWidthFromAnchor` for positioning.
89
+ * compose with `useAnchoredPopover` for positioning and sizing.
83
90
  *
84
91
  * ### 🔌 Visibility
85
92
  *
@@ -131,14 +138,12 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
131
138
  // `useId()` colons are invalid in CSS selectors and popover target attributes.
132
139
  const popoverId = idProp !== null && idProp !== void 0 ? idProp : `popover-${autoId.replace(/:/g, '')}`;
133
140
 
134
- // Keep semantic callbacks current without rebinding native lifecycle
141
+ // Keep the semantic callback current without rebinding native lifecycle
135
142
  // listeners during the same commit that calls showPopover()/hidePopover().
136
143
  const onCloseRef = useRef(onClose);
137
- const roleRef = useRef(role);
138
144
  useLayoutEffect(() => {
139
145
  onCloseRef.current = onClose;
140
- roleRef.current = role;
141
- }, [onClose, role]);
146
+ }, [onClose]);
142
147
 
143
148
  // Register with open layer observer so `closeLayers()` and open-count
144
149
  // subscriptions work. Only popup-like roles register as `popup`; passive
@@ -173,65 +178,66 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
173
178
  return popupMotionStyles[axis][edge];
174
179
  }, [placement, shouldAnimate]);
175
180
 
176
- // Prevents toggle handler from calling onClose for our own hidePopover() calls.
177
- const programmaticCloseRef = useRef(false);
178
-
179
- // Set in capture-phase keydown so the toggle handler knows close reason.
181
+ // Tracks the close reason, including our own hidePopover() calls.
180
182
  const closeReasonRef = useRef('light-dismiss');
181
183
 
182
184
  // Snapshot of pre-open focus, used to restore focus for nested popovers
183
- // with focus-capturing roles (browser only restores the outermost).
184
- const previouslyFocusedElementRef = useRef(null);
185
+ // because the browser only restores the outermost.
186
+ const focusRestorationTargetRef = useRef(null);
185
187
  const handleBeforeToggle = useCallback(event => {
186
- if (event.newState !== 'open') {
188
+ const popover = event.currentTarget;
189
+ if (!(popover instanceof HTMLElement)) {
187
190
  return;
188
191
  }
189
- // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- need active element snapshot
190
- const active = document.activeElement;
191
- previouslyFocusedElementRef.current = active instanceof HTMLElement ? active : null;
192
- }, []);
193
- const restorePreviouslyFocusedElement = useCallback(() => {
194
- const previouslyFocused = previouslyFocusedElementRef.current;
195
- previouslyFocusedElementRef.current = null;
196
- if (previouslyFocused !== null && previouslyFocused.isConnected && shouldFocusIntoPopover({
197
- role: roleRef.current
198
- })) {
199
- previouslyFocused.focus({
200
- preventScroll: true
201
- });
192
+ const {
193
+ ownerDocument
194
+ } = popover;
195
+ const {
196
+ activeElement
197
+ } = ownerDocument;
198
+ if (event.newState === 'open') {
199
+ focusRestorationTargetRef.current = activeElement instanceof HTMLElement ? activeElement : null;
200
+ return;
201
+ }
202
+
203
+ // Check before closing so focus already on body is not mistaken for focus lost during close.
204
+ if (closeReasonRef.current === 'light-dismiss' || !popover.contains(activeElement)) {
205
+ focusRestorationTargetRef.current = null;
202
206
  }
203
207
  }, []);
204
- const handleToggleClosed = useCallback(_event => {
205
- var _onCloseRef$current2;
206
- // Nested-popover focus restoration fallback (browser only
207
- // restores outermost; Firefox skips nested entirely).
208
- // Restore only when the role moved focus in on open AND the
209
- // close was Escape/programmatic. For light dismiss, the
210
- // click target keeps focus per HTML spec
211
- // (`focusPreviousElement=false`).
212
- const reason = closeReasonRef.current;
213
- const isProgrammatic = programmaticCloseRef.current;
214
- if (reason === 'escape' || isProgrammatic) {
215
- restorePreviouslyFocusedElement();
216
- } else {
217
- previouslyFocusedElementRef.current = null;
208
+ const handleToggleClosed = useCallback(event => {
209
+ const popover = event.currentTarget;
210
+ if (!(popover instanceof HTMLElement)) {
211
+ return;
218
212
  }
219
-
213
+ const focusRestorationTarget = focusRestorationTargetRef.current;
214
+ focusRestorationTargetRef.current = null;
215
+ const reason = closeReasonRef.current;
220
216
  // Reset reason so a stale 'escape' from a race with Escape keydown
221
217
  // does not corrupt the next browser-dismiss cycle.
222
218
  closeReasonRef.current = 'light-dismiss';
223
-
224
- // Programmatic close: consumer already knows.
225
- if (isProgrammatic) {
226
- return;
219
+ if (focusRestorationTarget) {
220
+ const {
221
+ ownerDocument
222
+ } = popover;
223
+ // Native restoration has finished. Preserve focus moved outside the popover.
224
+ const activeElementAfterClose = ownerDocument.activeElement;
225
+ if (activeElementAfterClose === ownerDocument.body || popover.contains(activeElementAfterClose)) {
226
+ // Match native popover restoration, which does not scroll the viewport.
227
+ focusRestorationTarget.focus({
228
+ preventScroll: true
229
+ });
230
+ }
227
231
  }
228
232
 
229
- // Browser dismiss (Escape/click-outside).
230
- // Optional: `manual` mode has no `onClose` in the forwarded type.
231
- (_onCloseRef$current2 = onCloseRef.current) === null || _onCloseRef$current2 === void 0 ? void 0 : _onCloseRef$current2.call(onCloseRef, {
232
- reason
233
- });
234
- }, [restorePreviouslyFocusedElement]);
233
+ // Programmatic closes are already known to the consumer.
234
+ if (reason !== 'programmatic') {
235
+ var _onCloseRef$current2;
236
+ (_onCloseRef$current2 = onCloseRef.current) === null || _onCloseRef$current2 === void 0 ? void 0 : _onCloseRef$current2.call(onCloseRef, {
237
+ reason
238
+ });
239
+ }
240
+ }, []);
235
241
  const {
236
242
  phase,
237
243
  isMounted,
@@ -247,9 +253,9 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
247
253
 
248
254
  // Focus management: initial focus on entry (role-dependent), Tab cycling
249
255
  // for dialog roles. Restoration is native for outermost popovers; nested
250
- // focus-capturing roles are handled above via the `beforetoggle` snapshot.
256
+ // popovers are handled above via the `beforetoggle` snapshot.
251
257
  // Passing `phase` keeps `useFocusWrap` attached through the animated-exit
252
- // window (WCAG 2.4.3 guard). See notes/architecture/focus-restoration.md.
258
+ // window (WCAG 2.4.3 guard). See notes/architecture/focus.md.
253
259
  useFocusWrap({
254
260
  elementRef: ownRef,
255
261
  role,
@@ -264,7 +270,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
264
270
  // Bind via `useLayoutEffect`, and BEFORE the show/hide layout effect
265
271
  // below, so listeners are attached when `showPopover()` synchronously
266
272
  // dispatches `beforetoggle`. A regular `useEffect` would miss that
267
- // first dispatch, dropping the `previouslyFocusedElementRef` snapshot
273
+ // first dispatch, dropping the `focusRestorationTargetRef` snapshot
268
274
  // and breaking nested-popover focus restoration on close. React runs
269
275
  // layout effects in source order on the same commit.
270
276
  useLayoutEffect(() => {
@@ -277,7 +283,10 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
277
283
  const unbindEscape = bind(element, {
278
284
  type: 'keydown',
279
285
  listener: event => {
280
- if (event.key === 'Escape') {
286
+ // Ignore Escape events dispatched after native close.
287
+ if (event.key === 'Escape' && isNativeElementOpen({
288
+ element
289
+ })) {
281
290
  closeReasonRef.current = 'escape';
282
291
  }
283
292
  },
@@ -294,7 +303,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
294
303
  onToggle(event);
295
304
  }
296
305
  });
297
- // Snapshot `document.activeElement` before `useInitialFocus` moves it,
306
+ // Snapshot the active element before `useInitialFocus` moves it,
298
307
  // then advance the shared visibility lifecycle.
299
308
  const unbindBeforeToggle = bind(element, {
300
309
  type: 'beforetoggle',
@@ -319,7 +328,6 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
319
328
  return;
320
329
  }
321
330
  if (isOpen) {
322
- programmaticCloseRef.current = false;
323
331
  // Clear stale 'escape' from a prior cycle: the ref outlives
324
332
  // the host element, and interrupted close lifecycles may not reach
325
333
  // `handleToggleClosed` to reset it.
@@ -328,13 +336,13 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
328
336
  element.showPopover();
329
337
  } catch {}
330
338
  return () => {
331
- programmaticCloseRef.current = true;
339
+ closeReasonRef.current = 'programmatic';
332
340
  try {
333
341
  element.hidePopover();
334
342
  } catch {}
335
343
  };
336
344
  }
337
- programmaticCloseRef.current = true;
345
+ closeReasonRef.current = 'programmatic';
338
346
  try {
339
347
  element.hidePopover();
340
348
  } catch {}