@atlaskit/top-layer 4.4.5 → 5.0.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 (130) hide show
  1. package/CHANGELOG.md +41 -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/{anchor-positioning-fallback.js → javascript-fallback/anchor-positioning-fallback.js} +12 -33
  15. package/dist/cjs/internal/javascript-fallback/apply-javascript-fallback-positioning.js +297 -0
  16. package/dist/cjs/internal/javascript-fallback/resolve-css-length-to-pixels.js +90 -0
  17. package/dist/cjs/internal/javascript-fallback/warn-fallback-failure.js +19 -0
  18. package/dist/cjs/internal/resolve-placement.js +14 -6
  19. package/dist/cjs/internal/supports-anchor-positioning.js +17 -0
  20. package/dist/cjs/internal/supports-anchor-size.js +17 -0
  21. package/dist/cjs/internal/supports-css-declaration.js +25 -0
  22. package/dist/cjs/internal/surface-reset.js +5 -0
  23. package/dist/cjs/internal/use-anchored-popover-at-point.js +124 -0
  24. package/dist/cjs/internal/use-anchored-popover.js +277 -0
  25. package/dist/cjs/popover/popover.compiled.css +4 -0
  26. package/dist/cjs/popover/popover.js +11 -4
  27. package/dist/cjs/testing/polyfill.js +6 -7
  28. package/dist/es2019/dialog/dialog-content.js +2 -0
  29. package/dist/es2019/entry-points/use-popover-id.js +6 -10
  30. package/dist/es2019/internal/anchor-positioning/apply-anchor-positioning.js +83 -0
  31. package/dist/es2019/internal/anchor-positioning/cross-axis-shift-margins.js +29 -0
  32. package/dist/es2019/internal/anchor-positioning/edge-margin.js +18 -0
  33. package/dist/es2019/internal/anchor-positioning/fit-margins.js +69 -0
  34. package/dist/es2019/internal/anchor-positioning/get-existing-anchor-name.js +12 -0
  35. package/dist/es2019/internal/{placement-to-position-area.js → anchor-positioning/placement-to-position-area.js} +9 -9
  36. package/dist/es2019/internal/anchor-positioning/placement-to-try-fallbacks.js +94 -0
  37. package/dist/es2019/internal/anchored-popover-size.js +343 -0
  38. package/dist/es2019/internal/get-aria-for-trigger.js +7 -13
  39. package/dist/es2019/internal/{anchor-positioning-fallback.js → javascript-fallback/anchor-positioning-fallback.js} +12 -33
  40. package/dist/es2019/internal/javascript-fallback/apply-javascript-fallback-positioning.js +291 -0
  41. package/dist/es2019/internal/javascript-fallback/resolve-css-length-to-pixels.js +87 -0
  42. package/dist/es2019/internal/javascript-fallback/warn-fallback-failure.js +14 -0
  43. package/dist/es2019/internal/resolve-placement.js +14 -6
  44. package/dist/es2019/internal/supports-anchor-positioning.js +12 -0
  45. package/dist/es2019/internal/supports-anchor-size.js +12 -0
  46. package/dist/es2019/internal/supports-css-declaration.js +20 -0
  47. package/dist/es2019/internal/surface-reset.js +1 -0
  48. package/dist/es2019/internal/use-anchored-popover-at-point.js +116 -0
  49. package/dist/es2019/internal/use-anchored-popover.js +265 -0
  50. package/dist/es2019/popover/popover.compiled.css +4 -0
  51. package/dist/es2019/popover/popover.js +11 -4
  52. package/dist/es2019/testing/polyfill.js +6 -7
  53. package/dist/esm/dialog/dialog-content.js +2 -0
  54. package/dist/esm/entry-points/use-popover-id.js +6 -10
  55. package/dist/esm/internal/anchor-positioning/apply-anchor-positioning.js +80 -0
  56. package/dist/esm/internal/anchor-positioning/cross-axis-shift-margins.js +28 -0
  57. package/dist/esm/internal/anchor-positioning/edge-margin.js +15 -0
  58. package/dist/esm/internal/anchor-positioning/fit-margins.js +71 -0
  59. package/dist/esm/internal/anchor-positioning/get-existing-anchor-name.js +12 -0
  60. package/dist/esm/internal/{placement-to-position-area.js → anchor-positioning/placement-to-position-area.js} +11 -12
  61. package/dist/esm/internal/anchor-positioning/placement-to-try-fallbacks.js +89 -0
  62. package/dist/esm/internal/anchored-popover-size.js +338 -0
  63. package/dist/esm/internal/get-aria-for-trigger.js +7 -13
  64. package/dist/esm/internal/{anchor-positioning-fallback.js → javascript-fallback/anchor-positioning-fallback.js} +12 -33
  65. package/dist/esm/internal/javascript-fallback/apply-javascript-fallback-positioning.js +290 -0
  66. package/dist/esm/internal/javascript-fallback/resolve-css-length-to-pixels.js +85 -0
  67. package/dist/esm/internal/javascript-fallback/warn-fallback-failure.js +13 -0
  68. package/dist/esm/internal/resolve-placement.js +14 -6
  69. package/dist/esm/internal/supports-anchor-positioning.js +12 -0
  70. package/dist/esm/internal/supports-anchor-size.js +12 -0
  71. package/dist/esm/internal/supports-css-declaration.js +19 -0
  72. package/dist/esm/internal/surface-reset.js +1 -0
  73. package/dist/esm/internal/use-anchored-popover-at-point.js +118 -0
  74. package/dist/esm/internal/use-anchored-popover.js +271 -0
  75. package/dist/esm/popover/popover.compiled.css +4 -0
  76. package/dist/esm/popover/popover.js +11 -4
  77. package/dist/esm/testing/polyfill.js +6 -7
  78. package/dist/types/entry-points/placement-map.d.ts +2 -2
  79. package/dist/types/entry-points/use-popover-id.d.ts +3 -7
  80. package/dist/types/internal/anchor-positioning/apply-anchor-positioning.d.ts +26 -0
  81. package/dist/types/internal/anchor-positioning/cross-axis-shift-margins.d.ts +19 -0
  82. package/dist/types/internal/anchor-positioning/edge-margin.d.ts +10 -0
  83. package/dist/types/internal/anchor-positioning/fit-margins.d.ts +30 -0
  84. package/dist/types/internal/anchor-positioning/get-existing-anchor-name.d.ts +8 -0
  85. package/dist/types/internal/{placement-to-position-area.d.ts → anchor-positioning/placement-to-position-area.d.ts} +10 -6
  86. package/dist/types/internal/anchor-positioning/placement-to-try-fallbacks.d.ts +30 -0
  87. package/dist/types/internal/anchored-popover-size.d.ts +126 -0
  88. package/dist/types/internal/get-aria-for-trigger.d.ts +13 -25
  89. package/dist/types/internal/javascript-fallback/anchor-positioning-fallback.d.ts +36 -0
  90. package/dist/types/internal/javascript-fallback/apply-javascript-fallback-positioning.d.ts +15 -0
  91. package/dist/types/internal/javascript-fallback/resolve-css-length-to-pixels.d.ts +20 -0
  92. package/dist/types/internal/javascript-fallback/warn-fallback-failure.d.ts +7 -0
  93. package/dist/types/internal/resolve-placement.d.ts +18 -2
  94. package/dist/types/internal/supports-anchor-positioning.d.ts +5 -0
  95. package/dist/types/internal/supports-anchor-size.d.ts +5 -0
  96. package/dist/types/internal/supports-css-declaration.d.ts +11 -0
  97. package/dist/types/internal/surface-reset.d.ts +25 -0
  98. package/dist/types/internal/use-anchored-popover-at-point.d.ts +48 -0
  99. package/dist/types/internal/use-anchored-popover.d.ts +143 -0
  100. package/dist/types/popover/index.d.ts +2 -1
  101. package/dist/types/popover/popover.d.ts +1 -1
  102. package/dist/types/popover/types.d.ts +15 -17
  103. package/dist/types/testing/polyfill.d.ts +3 -3
  104. package/package.json +2 -2
  105. package/use-anchored-popover/package.json +10 -0
  106. package/use-anchored-popover-at-point/package.json +10 -0
  107. package/dist/cjs/internal/placement-to-try-fallbacks.js +0 -108
  108. package/dist/cjs/internal/resolve-css-length-to-pixels.js +0 -104
  109. package/dist/cjs/internal/use-anchor-position-at-point.js +0 -129
  110. package/dist/cjs/internal/use-anchor-position.js +0 -501
  111. package/dist/cjs/internal/use-width-from-anchor.js +0 -124
  112. package/dist/es2019/internal/placement-to-try-fallbacks.js +0 -108
  113. package/dist/es2019/internal/resolve-css-length-to-pixels.js +0 -101
  114. package/dist/es2019/internal/use-anchor-position-at-point.js +0 -124
  115. package/dist/es2019/internal/use-anchor-position.js +0 -480
  116. package/dist/es2019/internal/use-width-from-anchor.js +0 -120
  117. package/dist/esm/internal/placement-to-try-fallbacks.js +0 -103
  118. package/dist/esm/internal/resolve-css-length-to-pixels.js +0 -98
  119. package/dist/esm/internal/use-anchor-position-at-point.js +0 -124
  120. package/dist/esm/internal/use-anchor-position.js +0 -494
  121. package/dist/esm/internal/use-width-from-anchor.js +0 -118
  122. package/dist/types/internal/anchor-positioning-fallback.d.ts +0 -51
  123. package/dist/types/internal/placement-to-try-fallbacks.d.ts +0 -13
  124. package/dist/types/internal/resolve-css-length-to-pixels.d.ts +0 -20
  125. package/dist/types/internal/use-anchor-position-at-point.d.ts +0 -67
  126. package/dist/types/internal/use-anchor-position.d.ts +0 -87
  127. package/dist/types/internal/use-width-from-anchor.d.ts +0 -19
  128. package/use-anchor-position/package.json +0 -10
  129. package/use-anchor-position-at-point/package.json +0 -10
  130. package/use-width-from-anchor/package.json +0 -10
@@ -1,480 +0,0 @@
1
- /**
2
- * Anchor positioning for top-layer popovers. See
3
- * `notes/architecture/positioning.md` for the full model.
4
- *
5
- * Two runtime paths: CSS Anchor Positioning (modern browsers) and a JS
6
- * fallback that measures the trigger and writes `top`/`left`.
7
- *
8
- * Both paths honour `offset.gap` and `offset.crossAxisShift`. The
9
- * CSS path passes the values through as strings; the JS fallback resolves
10
- * them to pixels via a hidden DOM probe (see `resolveCssLengthToPixels`)
11
- * so tokens, `calc()`, `var()`, etc all work. The JS fallback hides the
12
- * popover with `opacity: 0` until the first measurement completes, so it
13
- * is never painted at the wrong location.
14
- */
15
- import { useId, useLayoutEffect, useMemo } from 'react';
16
- import { bind } from 'bind-event-listener';
17
- import rafSchedule from 'raf-schd';
18
- import once from '@atlaskit/ds-lib/once';
19
- import { computeFallbackPosition } from './anchor-positioning-fallback';
20
- import { combine } from './combine';
21
- import { placementToPositionArea } from './placement-to-position-area';
22
- import { placementToTryFallbacks } from './placement-to-try-fallbacks';
23
- import { resolveCssLengthToPixels } from './resolve-css-length-to-pixels';
24
- import { resolvePlacement } from './resolve-placement';
25
- import { setStyle } from './set-style';
26
-
27
- // `resolvePlacement` is intentionally NOT re-exported here - import it directly
28
- // from `./resolve-placement` so search-and-jump lands at the source of truth.
29
-
30
- /**
31
- * Module-scope no-op `ResizeObserver` used in non-DOM jest environments where
32
- * `ResizeObserver` is `undefined`. Hoisted out of the effect to avoid
33
- * allocating a new class declaration on every run.
34
- */
35
- const NoopResizeObserver = class {
36
- observe() {}
37
- unobserve() {}
38
- disconnect() {}
39
- };
40
-
41
- /**
42
- * Returns true when the popover is currently open. Wrapped because the
43
- * `:popover-open` pseudo-class is not implemented in some test environments
44
- * (e.g. jsdom), where `matches` throws on unknown selectors.
45
- */
46
- function isPopoverOpen(popover) {
47
- try {
48
- return popover.matches(':popover-open');
49
- } catch {
50
- return false;
51
- }
52
- }
53
-
54
- /**
55
- * Detects whether the browser supports CSS Anchor Positioning.
56
- * Uses `once()` for lazy evaluation: safe for SSR and avoids hydration mismatches.
57
- */
58
- const supportsAnchorPositioning = once(() => {
59
- if (typeof window === 'undefined' || typeof CSS === 'undefined' || typeof CSS.supports !== 'function') {
60
- return false;
61
- }
62
- return CSS.supports('anchor-name', '--a');
63
- });
64
-
65
- /**
66
- * Every logical margin property this module can write: one per axis, per side.
67
- *
68
- * Deriving the union from `TPlacementAxis` and `TPlacementEdge` rather than
69
- * writing the four names out keeps it in step with the placement model, and
70
- * makes `CROSS_AXIS_SHIFT_CUSTOM_PROPERTY` below exhaustive by construction.
71
- */
72
-
73
- /**
74
- * The custom property mirroring each cross-axis shift margin.
75
- *
76
- * These are written for the planned named arrow `@position-try` rules. Nothing
77
- * reads them yet, and the shift no longer depends on them: the antisymmetric
78
- * margins written by `crossAxisShiftMargins` survive a cross-axis slide on
79
- * their own.
80
- *
81
- * Keyed on `TLogicalMarginProperty` and not on `string`, so a margin property
82
- * that has no custom property (or a custom property whose margin no longer
83
- * exists) is a compile error rather than an `undefined` lookup that reaches
84
- * `setStyle` as `{ property: undefined, value }`.
85
- */
86
- const CROSS_AXIS_SHIFT_CUSTOM_PROPERTY = {
87
- 'margin-inline-start': '--ds-cross-axis-shift-margin-start',
88
- 'margin-inline-end': '--ds-cross-axis-shift-margin-end',
89
- 'margin-block-start': '--ds-cross-axis-shift-margin-block-start',
90
- 'margin-block-end': '--ds-cross-axis-shift-margin-block-end'
91
- };
92
- const ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES = Object.values(CROSS_AXIS_SHIFT_CUSTOM_PROPERTY).map(property => ({
93
- property,
94
- value: '0px'
95
- }));
96
-
97
- /**
98
- * Returns the CSS margin declaration that creates a gap between the
99
- * popover and its trigger on the side facing the anchor.
100
- *
101
- * For example, a `block-end` placement (popover below) gets
102
- * `margin-block-start: 8px` to push it away from the trigger's bottom edge.
103
- */
104
- function edgeMargin({
105
- placement,
106
- offset
107
- }) {
108
- const {
109
- axis,
110
- edge
111
- } = placement;
112
- // The gap sits on the side FACING the anchor, which is always the side
113
- // opposite the placement edge.
114
- const anchorFacingEdge = edge === 'end' ? 'start' : 'end';
115
- return {
116
- property: `margin-${axis}-${anchorFacingEdge}`,
117
- value: offset
118
- };
119
- }
120
-
121
- /**
122
- * **The cross-axis shift margins.**
123
- *
124
- * The shift is written antisymmetrically on BOTH cross-axis sides
125
- * (`start: +value`, `end: -value`) rather than as a single margin on the side
126
- * picked from `align`. Two separate behaviours require it:
127
- *
128
- * 1. **`align: 'center'` is centered with `anchor-center`, which centers the
129
- * popover's MARGIN box on the anchor.** A single-sided margin widens the
130
- * margin box, so it displaces the border box by only half its value. An
131
- * antisymmetric pair leaves the margin box width unchanged (`+value` and
132
- * `-value` sum to zero) while moving its center by the full value.
133
- * 2. **`position-try-fallbacks` can slide the popover onto the opposite
134
- * cross-axis side.** A `<position-area>` fallback keeps the base style's
135
- * margins, and margin on the un-anchored side has no effect, so a
136
- * single-sided margin is silently dropped once the browser slides across the
137
- * cross axis. With both sides written, whichever side ends up anchored moves
138
- * the popover the same physical direction.
139
- *
140
- * Leaving the margin box width unchanged also means the shift does not move
141
- * the point at which `position-try-fallbacks` decides the popover overflows.
142
- *
143
- * A `<try-tactic>` fallback (`flip-block`, `flip-inline`) behaves differently to
144
- * a `<position-area>` one: it SWAPS the start and end margins rather than
145
- * keeping them, which is what preserves the gap through a flip. So a flip across
146
- * the cross axis mirrors the shift along with the rest of the placement, rather
147
- * than preserving its physical direction. Only the diagonal fallback does that,
148
- * and only for `align: 'start' | 'end'`. See `placementToTryFallbacks`.
149
- *
150
- * A positive value always moves the popover toward the cross-axis END, so
151
- * `direction: 'forwards'` means the same thing for every `align` value. See
152
- * `notes/decisions/placement-offset.md`.
153
- */
154
- function crossAxisShiftMargins({
155
- placement,
156
- crossAxisShiftCssValue,
157
- direction
158
- }) {
159
- const crossAxis = placement.axis === 'block' ? 'inline' : 'block';
160
- // Wrap any string in calc() with the sign factor. CSS handles the math,
161
- // so opaque values such as design tokens negate correctly.
162
- const negated = `calc(-1 * ${crossAxisShiftCssValue})`;
163
- const isForwards = direction === 'forwards';
164
- return [{
165
- property: `margin-${crossAxis}-start`,
166
- value: isForwards ? crossAxisShiftCssValue : negated
167
- }, {
168
- property: `margin-${crossAxis}-end`,
169
- value: isForwards ? negated : crossAxisShiftCssValue
170
- }];
171
- }
172
-
173
- /**
174
- * Returns a referentially-stable, fully-resolved `TPlacement`. The memo is
175
- * keyed on the resolved primitive fields so a fresh inline `placement` object
176
- * (or undefined fields vs explicit defaults) does not produce a fresh result.
177
- */
178
- function useStablePlacement(placement) {
179
- const resolved = resolvePlacement({
180
- placement
181
- });
182
- const axis = resolved.axis;
183
- const edge = resolved.edge;
184
- const align = resolved.align;
185
- const gap = resolved.offset.gap;
186
- const shiftValue = resolved.offset.crossAxisShift.value;
187
- const shiftDirection = resolved.offset.crossAxisShift.direction;
188
- // Rebuild inside the memo from primitive deps so the factory does not
189
- // close over the outer `resolved` reference and exhaustive-deps can verify.
190
- return useMemo(() => ({
191
- axis,
192
- edge,
193
- align,
194
- offset: {
195
- gap,
196
- crossAxisShift: {
197
- value: shiftValue,
198
- direction: shiftDirection
199
- }
200
- }
201
- }), [axis, edge, align, gap, shiftValue, shiftDirection]);
202
- }
203
-
204
- /**
205
- * Hook that positions an element relative to an anchor element using
206
- * CSS Anchor Positioning (with a JS fallback for older browsers).
207
- *
208
- * This hook is the positioning primitive. It has no knowledge of popovers,
209
- * visibility, or animation. Compose it with `Popover` for anchor-positioned
210
- * top-layer content.
211
- *
212
- * When CSS Anchor Positioning is supported, it sets CSS properties
213
- * (`anchor-name`, `position-anchor`, `position-area`, `position-try-fallbacks`)
214
- * directly on the elements via `el.style.setProperty()`.
215
- *
216
- * When not supported, it falls back to JavaScript-based positioning using
217
- * `position: fixed` with measured coordinates, re-running on scroll (capture)
218
- * and resize events.
219
- */
220
- // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
221
- export function useAnchorPosition({
222
- anchorRef,
223
- popoverRef,
224
- placement = {},
225
- forceFallbackPositioning = false,
226
- isEnabled = true,
227
- isOpen
228
- }) {
229
- const id = useId();
230
- const stablePlacement = useStablePlacement(placement);
231
- useLayoutEffect(() => {
232
- if (!isEnabled) {
233
- return;
234
- }
235
- const trigger = anchorRef.current;
236
- const popover = popoverRef.current;
237
- if (!trigger || !popover) {
238
- return;
239
- }
240
- const gapCssValue = stablePlacement.offset.gap;
241
- const crossAxisShiftCssValue = stablePlacement.offset.crossAxisShift.value;
242
- const shouldForceFallback = Boolean(forceFallbackPositioning);
243
- if (supportsAnchorPositioning() && !shouldForceFallback) {
244
- // Reuse an existing `anchor-name` if the element already has one.
245
- // Another popover might already be anchored to the old name.
246
- const anchorName = trigger.style.getPropertyValue('anchor-name') || `--anchor-${id.replace(/:/g, '')}`;
247
- const gap = edgeMargin({
248
- placement: stablePlacement,
249
- offset: gapCssValue
250
- });
251
- // Computed once; also mirrored into the
252
- // `--ds-cross-axis-shift-margin-*` custom properties below.
253
- const crossAxisShift = crossAxisShiftMargins({
254
- placement: stablePlacement,
255
- crossAxisShiftCssValue,
256
- direction: stablePlacement.offset.crossAxisShift.direction
257
- });
258
- const popoverStyles = [{
259
- property: 'position-anchor',
260
- value: anchorName
261
- }, {
262
- property: 'position-area',
263
- value: placementToPositionArea({
264
- placement: stablePlacement
265
- })
266
- }, {
267
- property: 'position-try-fallbacks',
268
- value: placementToTryFallbacks({
269
- placement: stablePlacement
270
- })
271
- },
272
- // Reset browser default popover positioning that conflicts
273
- // with anchor positioning (UA: `inset: 0; margin: auto;`)
274
- {
275
- property: 'margin',
276
- value: '0'
277
- }, {
278
- property: 'inset',
279
- value: 'auto'
280
- }, gap, ...crossAxisShift,
281
- // Zero every side first so the two sides that are NOT on the
282
- // active cross axis do not keep a value from a previous placement.
283
- ...ZEROED_CROSS_AXIS_SHIFT_CUSTOM_PROPERTIES, ...crossAxisShift.map(({
284
- property,
285
- value
286
- }) => ({
287
- property: CROSS_AXIS_SHIFT_CUSTOM_PROPERTY[property],
288
- value
289
- }))];
290
-
291
- /**
292
- * **We are never cleaning up anchor names**
293
- *
294
- * _Rationale_
295
- *
296
- * - Multiple popovers can share the same anchor element.
297
- * - Sometimes the `useAnchorPosition` hook and `Popover`
298
- * are not in the same component.
299
- * - There can be times when, even with reference counting, we
300
- * could remove the anchor-name while it is still being used
301
- * by something else - especially with async react updates
302
- * which can cause tearing
303
- *
304
- * _More_
305
- *
306
- * See `notes/decisions/anchor-name-lifetime.md`
307
- */
308
- trigger.style.setProperty('anchor-name', anchorName);
309
- return setStyle({
310
- element: popover,
311
- styles: popoverStyles
312
- });
313
- }
314
-
315
- // JS fallback. The popover is already in the top layer via
316
- // popover="auto", so we only reset UA defaults and set top/left
317
- // based on measurements.
318
- const cleanupBaseStyles = setStyle({
319
- element: popover,
320
- styles: [{
321
- property: 'margin',
322
- value: '0'
323
- }, {
324
- property: 'inset',
325
- value: 'auto'
326
- }]
327
- });
328
-
329
- // The popover can be consumer-owned (`@atlaskit/popper`'s `createPopper`),
330
- // so restore prior inline values rather than removing ours.
331
- let undoPosition;
332
- let undoHide;
333
-
334
- // `opacity: 0` not `visibility: hidden`: Firefox skips visibility-hidden
335
- // elements during `<dialog>` initial-focus traversal.
336
- function hideUntilPositioned() {
337
- // Keep the first snapshot, else we capture our own `opacity: 0`.
338
- // (`!popover` is for TS: narrowing does not reach nested functions.)
339
- if (!popover || undoHide) {
340
- return;
341
- }
342
- undoHide = setStyle({
343
- element: popover,
344
- styles: [{
345
- property: 'opacity',
346
- value: '0'
347
- }]
348
- });
349
- }
350
- function reveal() {
351
- var _undoHide;
352
- (_undoHide = undoHide) === null || _undoHide === void 0 ? void 0 : _undoHide();
353
- undoHide = undefined;
354
- }
355
- function restorePosition() {
356
- var _undoPosition;
357
- (_undoPosition = undoPosition) === null || _undoPosition === void 0 ? void 0 : _undoPosition();
358
- undoPosition = undefined;
359
- }
360
- function update() {
361
- if (!trigger || !popover) {
362
- return;
363
- }
364
- const triggerRect = trigger.getBoundingClientRect();
365
- const viewport = {
366
- width: window.innerWidth,
367
- height: window.innerHeight
368
- };
369
-
370
- // Resolve consumer-supplied CSS length strings (tokens, calc, var,
371
- // px, rem, etc) to pixels. The probe is mounted INSIDE the popover
372
- // itself so the resolved length matches the scope a token would
373
- // resolve against from inside the popover content. (The popover
374
- // lives in the top layer, not the trigger's DOM tree, so probing
375
- // at `popover.parentElement` would resolve in a different scope
376
- // than the consumer's token authoring expects.)
377
- const gapPx = resolveCssLengthToPixels({
378
- value: gapCssValue,
379
- container: popover
380
- });
381
- const crossAxisShiftPx = resolveCssLengthToPixels({
382
- value: crossAxisShiftCssValue,
383
- container: popover
384
- });
385
- const {
386
- top,
387
- left
388
- } = computeFallbackPosition({
389
- triggerRect,
390
- popoverEl: popover,
391
- placement: stablePlacement,
392
- viewport,
393
- gap: gapPx,
394
- crossAxisShift: {
395
- value: crossAxisShiftPx,
396
- direction: stablePlacement.offset.crossAxisShift.direction
397
- }
398
- });
399
-
400
- // Restore first so the snapshot stays the consumer's value.
401
- restorePosition();
402
- undoPosition = setStyle({
403
- element: popover,
404
- styles: [{
405
- property: 'top',
406
- value: `${top}px`
407
- }, {
408
- property: 'left',
409
- value: `${left}px`
410
- }]
411
- });
412
- reveal();
413
- }
414
-
415
- // Throttle scroll/resize updates to one per animation frame
416
- const scheduledUpdate = rafSchedule(update);
417
-
418
- // Wait for the FIRST valid layout before measuring: the popover
419
- // is `display: none` until `showPopover()`, and some browsers
420
- // fire `toggle` before layout, so RAF-after-toggle is unreliable.
421
- // ResizeObserver fires once the browser has real dimensions.
422
- // Self-disconnects after one valid measurement; ongoing
423
- // scroll/resize is handled by the window listeners below.
424
- // `ResizeObserver` is missing in some non-DOM jest environments
425
- // (e.g. post-office's `node` environment). Fall back to a no-op
426
- // observer there. The scroll/resize listeners below still keep
427
- // the popover positioned in the rare case the consumer also
428
- // polyfilled `showPopover` but not `ResizeObserver`. Real
429
- // browsers always have it. The `NoopResizeObserver` class is
430
- // hoisted to module scope (see top of file) so we do not allocate
431
- // a new constructor on every effect run.
432
- const ResizeObserverImpl = typeof ResizeObserver !== 'undefined' ? ResizeObserver : NoopResizeObserver;
433
- const resizeObserver = new ResizeObserverImpl(() => {
434
- if (popover.offsetWidth > 0 && popover.offsetHeight > 0) {
435
- update();
436
- resizeObserver.disconnect();
437
- }
438
- });
439
-
440
- // If the popover is already open by the time this effect runs (for
441
- // example, the parent `useAnchorPosition` effect runs after the child
442
- // `Popover` effect that called `showPopover()`), the initial `toggle`
443
- // event has already fired and our listener below would miss it. Start
444
- // observing immediately so the first measurement still happens.
445
- if (isPopoverOpen(popover)) {
446
- hideUntilPositioned();
447
- resizeObserver.observe(popover);
448
- }
449
- const undoPositioning = combine(cleanupBaseStyles,
450
- // Each open needs a fresh measurement, so hide and re-observe.
451
- bind(popover, {
452
- type: 'toggle',
453
- listener: event => {
454
- const toggleEvent = event;
455
- if (toggleEvent.newState === 'open') {
456
- hideUntilPositioned();
457
- resizeObserver.observe(popover);
458
- }
459
- }
460
- }), () => resizeObserver.disconnect(), bind(window, {
461
- type: 'scroll',
462
- listener: scheduledUpdate,
463
- options: {
464
- capture: true,
465
- passive: true
466
- }
467
- }), bind(window, {
468
- type: 'resize',
469
- listener: scheduledUpdate,
470
- options: {
471
- passive: true
472
- }
473
- }), () => {
474
- scheduledUpdate.cancel();
475
- restorePosition();
476
- reveal();
477
- });
478
- return undoPositioning;
479
- }, [anchorRef, popoverRef, stablePlacement, forceFallbackPositioning, isEnabled, id, isOpen]);
480
- }
@@ -1,120 +0,0 @@
1
- import { useLayoutEffect } from 'react';
2
- import once from '@atlaskit/ds-lib/once';
3
- import { setStyle } from './set-style';
4
-
5
- /**
6
- * Probes anchor SIZING, separately from `anchor-name` (see
7
- * `supportsAnchorPositioning`): some browsers have positioning but not sizing.
8
- * `once()` for SSR safety.
9
- */
10
- const supportsAnchorSize = once(() => {
11
- if (typeof window === 'undefined' || typeof CSS === 'undefined' || typeof CSS.supports !== 'function') {
12
- return false;
13
- }
14
- return CSS.supports('min-inline-size', 'anchor-size(self-inline)');
15
- });
16
-
17
- /**
18
- * The JS fallback's stand-in for `anchor-size(self-inline)`. `offsetWidth` / `offsetHeight` are
19
- * physical, so the axis is picked from the popover's `writing-mode`. Anything not horizontal is
20
- * treated as vertical, and an unknown or empty value falls back to `offsetWidth`.
21
- */
22
- function getFallbackAnchorInlineSize({
23
- anchor,
24
- popover
25
- }) {
26
- // `String()` because not every test environment implements `writing-mode`.
27
- const writingMode = String(getComputedStyle(popover).writingMode);
28
- const isVertical = writingMode.startsWith('vertical') || writingMode.startsWith('sideways');
29
- return isVertical ? anchor.offsetHeight : anchor.offsetWidth;
30
- }
31
-
32
- /**
33
- * Sets the width of a popover element relative to its anchor element.
34
- *
35
- * - `'match-anchor'` → `inline-size: anchor-size(self-inline)`, exact
36
- * - `'min-anchor'` → `min-inline-size: anchor-size(self-inline)`, floor only
37
- * - `'none'` → `min-inline-size: max-content`, content floor (default)
38
- *
39
- * `self-inline` is the inline axis of the element USING the function (the popover), never the
40
- * anchor, so it matches the axis `inline-size` constrains. See
41
- * `notes/decisions/width-from-anchor-floors.md`.
42
- */
43
- export function useWidthFromAnchor({
44
- mode,
45
- popoverRef,
46
- anchorRef,
47
- isOpen
48
- }) {
49
- useLayoutEffect(() => {
50
- const node = popoverRef.current;
51
- if (!node) {
52
- return;
53
- }
54
- if (mode === 'match-anchor') {
55
- // No floor here: it would let the popover grow past the anchor.
56
- if (supportsAnchorSize()) {
57
- return setStyle({
58
- element: node,
59
- styles: [{
60
- property: 'inline-size',
61
- value: 'anchor-size(self-inline)'
62
- }]
63
- });
64
- }
65
- const anchor = anchorRef.current;
66
- if (anchor) {
67
- return setStyle({
68
- element: node,
69
- styles: [{
70
- property: 'inline-size',
71
- value: `${getFallbackAnchorInlineSize({
72
- anchor,
73
- popover: node
74
- })}px`
75
- }]
76
- });
77
- }
78
- return;
79
- }
80
- if (mode === 'min-anchor') {
81
- // One floor only. The content floor from mode 'none' deliberately does not also apply
82
- // here, and cannot: it would need this same property. See the decision note.
83
- if (supportsAnchorSize()) {
84
- return setStyle({
85
- element: node,
86
- styles: [{
87
- property: 'min-inline-size',
88
- value: 'anchor-size(self-inline)'
89
- }]
90
- });
91
- }
92
- const anchor = anchorRef.current;
93
- if (anchor) {
94
- return setStyle({
95
- element: node,
96
- styles: [{
97
- property: 'min-inline-size',
98
- value: `${getFallbackAnchorInlineSize({
99
- anchor,
100
- popover: node
101
- })}px`
102
- }]
103
- });
104
- }
105
-
106
- // Nothing to measure, so no floor.
107
- return;
108
- }
109
-
110
- // Makes a too-narrow span overflow and drive `position-try-fallbacks` instead of wrapping.
111
- // `@atlaskit/popper` resets this exact property to `0` for its fit-to-viewport mode.
112
- return setStyle({
113
- element: node,
114
- styles: [{
115
- property: 'min-inline-size',
116
- value: 'max-content'
117
- }]
118
- });
119
- }, [mode, popoverRef, anchorRef, isOpen]);
120
- }
@@ -1,103 +0,0 @@
1
- import { resolvePlacement } from './resolve-placement';
2
-
3
- /**
4
- * A `position-area` primary edge — the placement axis joined to its edge, e.g.
5
- * `block-end` or `inline-start`. These are the four edges a popover can sit on.
6
- */
7
-
8
- function slideOnEdge(_ref) {
9
- var align = _ref.align,
10
- edgeKey = _ref.edgeKey,
11
- crossAxis = _ref.crossAxis;
12
- if (align === 'center') {
13
- return ["".concat(edgeKey, " span-").concat(crossAxis, "-end"), "".concat(edgeKey, " span-").concat(crossAxis, "-start")];
14
- }
15
- // For `align: 'start'` the popover expands toward `end`, so the "near"
16
- // span is `end` and the "far" span is `start`; `align: 'end'` mirrors
17
- // this. The bare `edgeKey` (re-centered) sits between them.
18
- var near = align === 'start' ? 'end' : 'start';
19
- var far = align === 'start' ? 'start' : 'end';
20
- return ["".concat(edgeKey, " span-").concat(crossAxis, "-").concat(near), edgeKey, "".concat(edgeKey, " span-").concat(crossAxis, "-").concat(far)];
21
- }
22
- function flipDiagonally(_ref2) {
23
- var axis = _ref2.axis;
24
- var flipOnMainAxis = axis === 'block' ? 'flip-block' : 'flip-inline';
25
- var flipToCrossAxis = axis === 'block' ? 'flip-inline' : 'flip-block';
26
- return "".concat(flipOnMainAxis, " ").concat(flipToCrossAxis);
27
- }
28
-
29
- /**
30
- * Returns the `position-try-fallbacks` value for a placement.
31
- *
32
- * @example
33
- * // Centered on the block-end edge (the default placement):
34
- * placementToTryFallbacks({ placement: { axis: 'block', edge: 'end', align: 'center' } });
35
- * // => 'block-end span-inline-end, block-end span-inline-start, flip-block,
36
- * // block-start span-inline-end, block-start span-inline-start'
37
- */
38
- export function placementToTryFallbacks(_ref3) {
39
- var placement = _ref3.placement;
40
- /**
41
- * **Build the fallback list**
42
- *
43
- * Each entry is a complete alternative position.
44
- * The browser tries them _in order_ and uses the _first_ one that fits.
45
- */
46
-
47
- var resolved = resolvePlacement({
48
- placement: placement
49
- });
50
- var axis = resolved.axis,
51
- edge = resolved.edge,
52
- align = resolved.align;
53
- var crossAxis = axis === 'block' ? 'inline' : 'block';
54
- var flippedEdge = edge === 'start' ? 'end' : 'start';
55
- var sameEdgeArea = "".concat(axis, "-").concat(edge);
56
- var oppositeEdgeArea = "".concat(axis, "-").concat(flippedEdge);
57
- var flipOnMainAxis = axis === 'block' ? 'flip-block' : 'flip-inline';
58
-
59
- /**
60
- * **Fallback algorithm**
61
- *
62
- * We have two things we can do to keep something visible:
63
- *
64
- * 1. _Slide_ along edges
65
- * 2. _Flip_ to another edge
66
- *
67
- * _Centered popover_
68
- *
69
- * Desired: Sits in middle of main edge
70
- *
71
- * Fallbacks:
72
- * 1: _Slide_ along main edge on the cross axis
73
- * 2: _Flip_ on main axis to opposite edge (centered)
74
- * 3: _Slide_ along opposite edge on the cross axis
75
- *
76
- * _Start or end aligned popover_
77
- *
78
- * Desired: Sits at start / end of main edge
79
- *
80
- * Fallbacks:
81
- * 1: _Slide_ along main edge on the cross axis
82
- * 2: _Flip_ on main axis to opposite edge
83
- * 3: _Flip_ on the diagonal (flip main axis + flip cross axis) into the
84
- * opposite corner. For start / end alignment, this is the clean escape
85
- * when the popover would otherwise land offscreen on both axes (ie in a
86
- * corner). Rule 4 gets the job done, but a slide drops the gap
87
- * as named position-area values don't carry margins).
88
- * The diagonal flip keeps the gap and the alignment.
89
- * 4: _Slide_ along opposite edge on the cross axis
90
- */
91
-
92
- return [slideOnEdge({
93
- align: align,
94
- edgeKey: sameEdgeArea,
95
- crossAxis: crossAxis
96
- }), flipOnMainAxis, align !== 'center' ? flipDiagonally({
97
- axis: axis
98
- }) : null, slideOnEdge({
99
- align: align,
100
- edgeKey: oppositeEdgeArea,
101
- crossAxis: crossAxis
102
- })].filter(Boolean).flat().join(', ');
103
- }