@atlaskit/top-layer 0.13.0 → 0.15.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 (179) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +61 -34
  3. package/dist/cjs/animations/presets.js +4 -4
  4. package/dist/cjs/dialog/create-close-event.js +1 -2
  5. package/dist/cjs/entry-points/dialog.js +6 -0
  6. package/dist/cjs/entry-points/get-aria-for-trigger.js +12 -0
  7. package/dist/cjs/entry-points/popover-surface.js +12 -0
  8. package/dist/cjs/entry-points/popover.js +6 -0
  9. package/dist/cjs/entry-points/use-popover-id.js +21 -0
  10. package/dist/cjs/internal/get-aria-for-trigger.js +88 -0
  11. package/dist/cjs/internal/role-types.js +64 -1
  12. package/dist/cjs/internal/slot.js +1 -1
  13. package/dist/cjs/internal/use-anchor-position.js +30 -14
  14. package/dist/cjs/popover/create-close-event.js +1 -2
  15. package/dist/cjs/popover/popover.js +79 -80
  16. package/dist/cjs/popover-surface/index.js +12 -0
  17. package/dist/cjs/{popup/popup-surface.js → popover-surface/popover-surface.js} +9 -12
  18. package/dist/cjs/testing/polyfill.js +9 -6
  19. package/dist/es2019/animations/presets.js +4 -4
  20. package/dist/es2019/dialog/create-close-event.js +1 -2
  21. package/dist/es2019/entry-points/dialog.js +1 -1
  22. package/dist/es2019/entry-points/get-aria-for-trigger.js +1 -0
  23. package/dist/es2019/entry-points/popover-surface.js +1 -0
  24. package/dist/es2019/entry-points/popover.js +1 -1
  25. package/dist/es2019/entry-points/use-popover-id.js +15 -0
  26. package/dist/es2019/internal/get-aria-for-trigger.js +84 -0
  27. package/dist/es2019/internal/role-types.js +59 -0
  28. package/dist/es2019/internal/slot.js +1 -1
  29. package/dist/es2019/internal/use-anchor-position.js +16 -12
  30. package/dist/es2019/popover/create-close-event.js +1 -2
  31. package/dist/es2019/popover/popover.js +79 -80
  32. package/dist/es2019/popover-surface/index.js +1 -0
  33. package/dist/es2019/popover-surface/popover-surface.js +27 -0
  34. package/dist/es2019/testing/polyfill.js +9 -6
  35. package/dist/esm/animations/presets.js +4 -4
  36. package/dist/esm/dialog/create-close-event.js +1 -2
  37. package/dist/esm/entry-points/dialog.js +1 -1
  38. package/dist/esm/entry-points/get-aria-for-trigger.js +1 -0
  39. package/dist/esm/entry-points/popover-surface.js +1 -0
  40. package/dist/esm/entry-points/popover.js +1 -1
  41. package/dist/esm/entry-points/use-popover-id.js +15 -0
  42. package/dist/esm/internal/get-aria-for-trigger.js +83 -0
  43. package/dist/esm/internal/role-types.js +58 -0
  44. package/dist/esm/internal/slot.js +1 -1
  45. package/dist/esm/internal/use-anchor-position.js +30 -14
  46. package/dist/esm/popover/create-close-event.js +1 -2
  47. package/dist/esm/popover/popover.js +79 -80
  48. package/dist/esm/popover-surface/index.js +1 -0
  49. package/dist/esm/popover-surface/popover-surface.js +26 -0
  50. package/dist/esm/testing/polyfill.js +9 -6
  51. package/dist/types/animations/presets.d.ts +4 -4
  52. package/dist/types/animations/types.d.ts +1 -9
  53. package/dist/types/dialog/create-close-event.d.ts +1 -2
  54. package/dist/types/dialog/types.d.ts +0 -8
  55. package/dist/types/entry-points/animations.d.ts +1 -1
  56. package/dist/types/entry-points/dialog.d.ts +1 -1
  57. package/dist/types/entry-points/get-aria-for-trigger.d.ts +1 -0
  58. package/dist/types/entry-points/popover-surface.d.ts +2 -0
  59. package/dist/types/entry-points/popover.d.ts +2 -2
  60. package/dist/types/entry-points/use-popover-id.d.ts +11 -0
  61. package/dist/types/internal/get-aria-for-trigger.d.ts +86 -0
  62. package/dist/types/internal/role-types.d.ts +7 -0
  63. package/dist/types/internal/slot.d.ts +2 -2
  64. package/dist/types/popover/create-close-event.d.ts +1 -2
  65. package/dist/types/popover/index.d.ts +1 -1
  66. package/dist/types/popover/popover.d.ts +6 -13
  67. package/dist/types/popover/types.d.ts +6 -5
  68. package/dist/types/popover-surface/index.d.ts +2 -0
  69. package/dist/types/popover-surface/popover-surface.d.ts +21 -0
  70. package/dist/types-ts4.5/animations/presets.d.ts +4 -4
  71. package/dist/types-ts4.5/animations/types.d.ts +1 -9
  72. package/dist/types-ts4.5/dialog/create-close-event.d.ts +1 -2
  73. package/dist/types-ts4.5/dialog/types.d.ts +0 -8
  74. package/dist/types-ts4.5/entry-points/animations.d.ts +1 -1
  75. package/dist/types-ts4.5/entry-points/dialog.d.ts +1 -1
  76. package/dist/types-ts4.5/entry-points/get-aria-for-trigger.d.ts +1 -0
  77. package/dist/types-ts4.5/entry-points/popover-surface.d.ts +2 -0
  78. package/dist/types-ts4.5/entry-points/popover.d.ts +2 -2
  79. package/dist/types-ts4.5/entry-points/use-popover-id.d.ts +11 -0
  80. package/dist/types-ts4.5/internal/get-aria-for-trigger.d.ts +86 -0
  81. package/dist/types-ts4.5/internal/role-types.d.ts +7 -0
  82. package/dist/types-ts4.5/internal/slot.d.ts +2 -2
  83. package/dist/types-ts4.5/popover/create-close-event.d.ts +1 -2
  84. package/dist/types-ts4.5/popover/index.d.ts +1 -1
  85. package/dist/types-ts4.5/popover/popover.d.ts +6 -13
  86. package/dist/types-ts4.5/popover/types.d.ts +6 -5
  87. package/dist/types-ts4.5/popover-surface/index.d.ts +2 -0
  88. package/dist/types-ts4.5/popover-surface/popover-surface.d.ts +21 -0
  89. package/get-aria-for-trigger/package.json +17 -0
  90. package/package.json +3 -3
  91. package/popover-surface/package.json +17 -0
  92. package/use-popover-id/package.json +17 -0
  93. package/create-close-event/package.json +0 -17
  94. package/dialog/create-close-event/package.json +0 -17
  95. package/dist/cjs/entry-points/create-close-event.js +0 -19
  96. package/dist/cjs/entry-points/dialog/create-close-event.js +0 -12
  97. package/dist/cjs/entry-points/popover/create-close-event.js +0 -12
  98. package/dist/cjs/entry-points/popup-surface.js +0 -12
  99. package/dist/cjs/entry-points/popup.js +0 -12
  100. package/dist/cjs/popup/index.js +0 -37
  101. package/dist/cjs/popup/popup-content.js +0 -215
  102. package/dist/cjs/popup/popup-context.js +0 -25
  103. package/dist/cjs/popup/popup-provider.js +0 -15
  104. package/dist/cjs/popup/popup-trigger-function.js +0 -80
  105. package/dist/cjs/popup/popup-trigger.js +0 -66
  106. package/dist/cjs/popup/popup.js +0 -81
  107. package/dist/cjs/popup/types.js +0 -5
  108. package/dist/cjs/popup/use-maybe-popup-context.js +0 -17
  109. package/dist/cjs/popup/use-popup-context.js +0 -20
  110. package/dist/es2019/entry-points/create-close-event.js +0 -9
  111. package/dist/es2019/entry-points/dialog/create-close-event.js +0 -1
  112. package/dist/es2019/entry-points/popover/create-close-event.js +0 -1
  113. package/dist/es2019/entry-points/popup-surface.js +0 -1
  114. package/dist/es2019/entry-points/popup.js +0 -1
  115. package/dist/es2019/popup/index.js +0 -33
  116. package/dist/es2019/popup/popup-content.js +0 -207
  117. package/dist/es2019/popup/popup-context.js +0 -20
  118. package/dist/es2019/popup/popup-provider.js +0 -10
  119. package/dist/es2019/popup/popup-surface.js +0 -30
  120. package/dist/es2019/popup/popup-trigger-function.js +0 -75
  121. package/dist/es2019/popup/popup-trigger.js +0 -61
  122. package/dist/es2019/popup/popup.js +0 -67
  123. package/dist/es2019/popup/types.js +0 -1
  124. package/dist/es2019/popup/use-maybe-popup-context.js +0 -12
  125. package/dist/es2019/popup/use-popup-context.js +0 -15
  126. package/dist/esm/entry-points/create-close-event.js +0 -9
  127. package/dist/esm/entry-points/dialog/create-close-event.js +0 -1
  128. package/dist/esm/entry-points/popover/create-close-event.js +0 -1
  129. package/dist/esm/entry-points/popup-surface.js +0 -1
  130. package/dist/esm/entry-points/popup.js +0 -1
  131. package/dist/esm/popup/index.js +0 -33
  132. package/dist/esm/popup/popup-content.js +0 -207
  133. package/dist/esm/popup/popup-context.js +0 -20
  134. package/dist/esm/popup/popup-provider.js +0 -10
  135. package/dist/esm/popup/popup-surface.js +0 -29
  136. package/dist/esm/popup/popup-trigger-function.js +0 -75
  137. package/dist/esm/popup/popup-trigger.js +0 -59
  138. package/dist/esm/popup/popup.js +0 -72
  139. package/dist/esm/popup/types.js +0 -1
  140. package/dist/esm/popup/use-maybe-popup-context.js +0 -12
  141. package/dist/esm/popup/use-popup-context.js +0 -15
  142. package/dist/types/entry-points/create-close-event.d.ts +0 -11
  143. package/dist/types/entry-points/dialog/create-close-event.d.ts +0 -2
  144. package/dist/types/entry-points/popover/create-close-event.d.ts +0 -2
  145. package/dist/types/entry-points/popup-surface.d.ts +0 -2
  146. package/dist/types/entry-points/popup.d.ts +0 -2
  147. package/dist/types/popup/index.d.ts +0 -34
  148. package/dist/types/popup/popup-content.d.ts +0 -14
  149. package/dist/types/popup/popup-context.d.ts +0 -96
  150. package/dist/types/popup/popup-provider.d.ts +0 -10
  151. package/dist/types/popup/popup-surface.d.ts +0 -22
  152. package/dist/types/popup/popup-trigger-function.d.ts +0 -112
  153. package/dist/types/popup/popup-trigger.d.ts +0 -14
  154. package/dist/types/popup/popup.d.ts +0 -14
  155. package/dist/types/popup/types.d.ts +0 -321
  156. package/dist/types/popup/use-maybe-popup-context.d.ts +0 -8
  157. package/dist/types/popup/use-popup-context.d.ts +0 -7
  158. package/dist/types-ts4.5/entry-points/create-close-event.d.ts +0 -11
  159. package/dist/types-ts4.5/entry-points/dialog/create-close-event.d.ts +0 -2
  160. package/dist/types-ts4.5/entry-points/popover/create-close-event.d.ts +0 -2
  161. package/dist/types-ts4.5/entry-points/popup-surface.d.ts +0 -2
  162. package/dist/types-ts4.5/entry-points/popup.d.ts +0 -2
  163. package/dist/types-ts4.5/popup/index.d.ts +0 -34
  164. package/dist/types-ts4.5/popup/popup-content.d.ts +0 -14
  165. package/dist/types-ts4.5/popup/popup-context.d.ts +0 -96
  166. package/dist/types-ts4.5/popup/popup-provider.d.ts +0 -10
  167. package/dist/types-ts4.5/popup/popup-surface.d.ts +0 -22
  168. package/dist/types-ts4.5/popup/popup-trigger-function.d.ts +0 -112
  169. package/dist/types-ts4.5/popup/popup-trigger.d.ts +0 -14
  170. package/dist/types-ts4.5/popup/popup.d.ts +0 -14
  171. package/dist/types-ts4.5/popup/types.d.ts +0 -321
  172. package/dist/types-ts4.5/popup/use-maybe-popup-context.d.ts +0 -8
  173. package/dist/types-ts4.5/popup/use-popup-context.d.ts +0 -7
  174. package/popover/create-close-event/package.json +0 -17
  175. package/popup/package.json +0 -17
  176. package/popup-surface/package.json +0 -17
  177. /package/dist/cjs/{popup/popup-surface.compiled.css → popover-surface/popover-surface.compiled.css} +0 -0
  178. /package/dist/es2019/{popup/popup-surface.compiled.css → popover-surface/popover-surface.compiled.css} +0 -0
  179. /package/dist/esm/{popup/popup-surface.compiled.css → popover-surface/popover-surface.compiled.css} +0 -0
@@ -28,13 +28,13 @@ import { setStyle } from './set-style';
28
28
  // from `./resolve-placement` so search-and-jump lands at the source of truth.
29
29
 
30
30
  /**
31
- * Module-scope no-op `ResizeObserver` stub used in non-DOM jest environments where
31
+ * Module-scope no-op `ResizeObserver` used in non-DOM jest environments where
32
32
  * `ResizeObserver` is `undefined`. Hoisted out of the effect to avoid
33
- * re-allocating on every run.
33
+ * allocating a new class declaration on every run.
34
34
  */
35
- const noopResizeObserver = {
36
- observe() {},
37
- unobserve() {},
35
+ const NoopResizeObserver = class {
36
+ observe() {}
37
+ unobserve() {}
38
38
  disconnect() {}
39
39
  };
40
40
 
@@ -197,6 +197,7 @@ function useStablePlacement(placement) {
197
197
  * comparison is used internally so re-runs are skipped when the resolved
198
198
  * placement shape is unchanged.
199
199
  */
200
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
200
201
  export function useAnchorPosition({
201
202
  anchorRef,
202
203
  popoverRef,
@@ -402,17 +403,20 @@ export function useAnchorPosition({
402
403
  // Self-disconnects after one valid measurement; ongoing
403
404
  // scroll/resize is handled by the window listeners below.
404
405
  // `ResizeObserver` is missing in some non-DOM jest environments
405
- // (e.g. post-office's `node` environment). Fall back to the
406
- // module-scope `noopResizeObserver` stub there. The scroll/resize
407
- // listeners below still keep the popover positioned in the rare
408
- // case the consumer also polyfilled `showPopover` but not
409
- // `ResizeObserver`. Real browsers always have it.
410
- const resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => {
406
+ // (e.g. post-office's `node` environment). Fall back to a no-op
407
+ // observer there. The scroll/resize listeners below still keep
408
+ // the popover positioned in the rare case the consumer also
409
+ // polyfilled `showPopover` but not `ResizeObserver`. Real
410
+ // browsers always have it. The `NoopResizeObserver` class is
411
+ // hoisted to module scope (see top of file) so we do not allocate
412
+ // a new constructor on every effect run.
413
+ const ResizeObserverImpl = typeof ResizeObserver !== 'undefined' ? ResizeObserver : NoopResizeObserver;
414
+ const resizeObserver = new ResizeObserverImpl(() => {
411
415
  if (popover.offsetWidth > 0 && popover.offsetHeight > 0) {
412
416
  update();
413
417
  resizeObserver.disconnect();
414
418
  }
415
- }) : noopResizeObserver;
419
+ });
416
420
 
417
421
  // If the popover is already open by the time this effect runs (for
418
422
  // example, the parent `useAnchorPosition` effect runs after the child
@@ -12,8 +12,7 @@
12
12
  *
13
13
  * @example
14
14
  * ```tsx
15
- * import { createPopoverCloseEvent } from '@atlaskit/top-layer/create-close-event';
16
- * import { type TPopoverCloseReason } from '@atlaskit/top-layer/popover';
15
+ * import { createPopoverCloseEvent, type TPopoverCloseReason } from '@atlaskit/top-layer/popover';
17
16
  *
18
17
  * function onPopoverClose({ reason }: { reason: TPopoverCloseReason }) {
19
18
  * legacyOnClose(createPopoverCloseEvent({ reason }));
@@ -7,13 +7,12 @@ 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/experimental/open-layer-observer';
10
+ import { shouldFocusIntoPopover } from '../internal/role-types';
10
11
  import { useAnimatedVisibility } from '../internal/use-animated-visibility';
11
12
  import { useFocusWrap } from '../internal/use-focus-wrap';
12
13
  import { useInitialFocus } from '../internal/use-initial-focus';
13
14
  /**
14
- * Detects whether the browser supports the `popover="hint"` attribute value.
15
- * Uses DOM reflection: unsupported browsers reflect the invalid value default ("manual").
16
- * Safe for SSR. Result is cached via `once`.
15
+ * Detects `popover="hint"` support via DOM reflection. SSR-safe, cached.
17
16
  */
18
17
  const supportsPopoverHint = once(() => {
19
18
  if (typeof document === 'undefined') {
@@ -30,33 +29,20 @@ const styles = {
30
29
  root: "_19itglyw _ca0qidpf _u5f3idpf _n3tdidpf _19bvidpf _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1r041wug _1reoewfl _18m9ewfl _bfhk1j28"
31
30
  };
32
31
  /**
33
- * ARIA roles on the Popover primitive that register with the open layer observer
34
- * as LayerType 'popup'. These are interactive overlays that should be tracked
35
- * and dismissed when closeLayers() is called (e.g. when the side nav resizes).
36
- *
37
- * Note on open layer observer types not included here:
38
- * - 'modal' only ever comes from the Dialog primitive, not Popover.
39
- * - Other layer types (e.g. 'tooltip') do not yet exist in LayerType because
40
- * there is no current use case for distinguishing them in the observer.
32
+ * Roles registered with the open layer observer as `popup`, so `closeLayers()`
33
+ * dismisses them. `modal` comes from Dialog only; other layer types are unused.
41
34
  */
42
35
  const POPUP_ROLES = new Set(['menu', 'listbox', 'dialog', 'alertdialog', 'tree', 'grid']);
43
36
 
44
37
  /**
45
- * Unopinionated top-layer primitive.
46
- *
47
- * Manages only visibility (isOpen) and animation (animate). Has no knowledge
48
- * of positioning - compose with `useAnchorPosition` for anchor positioning
49
- * and optionally `useWidthFromAnchor` for setting the width of a popover element
50
- * relative to its anchor trigger.
38
+ * Unopinionated top-layer primitive. Manages visibility and animation only;
39
+ * compose with `useAnchorPosition` / `useWidthFromAnchor` for positioning.
51
40
  *
52
- * Visibility is driven by the `isOpen` prop:
53
- * - `isOpen={true}` → `showPopover()`, entry animation via `@starting-style`
54
- * - `isOpen={false}` → `hidePopover()`, exit animation via `allow-discrete`
41
+ * `isOpen={true}` calls `showPopover()` (entry via `@starting-style`);
42
+ * `isOpen={false}` calls `hidePopover()` (exit via `allow-discrete`).
55
43
  *
56
- * For `mode="auto"`, the browser can dismiss the popover via light dismiss
57
- * (Escape, click outside). When that happens, `onClose` is called and the
58
- * consumer must set `isOpen` to `false`. The DOM owns the dismiss; React
59
- * must follow.
44
+ * In `mode="auto"`, the browser can light-dismiss (Escape, click outside).
45
+ * `onClose` fires; the consumer must then set `isOpen` to `false`.
60
46
  */
61
47
  export const Popover = /*#__PURE__*/forwardRef(function Popover({
62
48
  children,
@@ -79,11 +65,9 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
79
65
  const autoId = useId();
80
66
  const ownRef = useRef(null);
81
67
  const combinedRef = mergeRefs([ownRef, ref].filter(Boolean));
82
- // Regex is intentional: `replaceAll` is not available in our supported
83
- // browser matrix. In React 18, `useId()` returns IDs containing colons
84
- // (e.g. `:r1:`) which are invalid in CSS selectors and `popover` target
85
- // attributes. React 19.2.0 replaced colons with underscores, making this
86
- // stripping unnecessary once React 18 support is dropped.
68
+ // React 18 `useId()` returns IDs with colons (invalid in CSS selectors
69
+ // and popover target attributes). Strip them; remove once React 18 is
70
+ // dropped (React 19.2 uses underscores).
87
71
  const popoverId = idProp !== null && idProp !== void 0 ? idProp : `popover-${autoId.replace(/:/g, '')}`;
88
72
  const {
89
73
  showChildren,
@@ -96,12 +80,10 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
96
80
  onExitFinish
97
81
  });
98
82
 
99
- // Focus management
100
- // Initial focus: on open, moves focus into the popover (role-dependent).
101
- // Focus wrap: Tab/Shift+Tab cycle within the popover for dialog roles.
102
- // Focus restoration: handled natively by the Popover API. The browser
103
- // tracks `previouslyFocusedElement` on show and restores it on hide.
104
- // No custom restoration hook is needed. See: notes/architecture/focus-restoration.md
83
+ // Focus management: initial focus on open (role-dependent), Tab cycling
84
+ // for dialog roles. Restoration is native for outermost popovers; nested
85
+ // focus-capturing roles are handled below via `beforetoggle` snapshot.
86
+ // See: notes/architecture/focus-restoration.md
105
87
  useFocusWrap({
106
88
  elementRef: ownRef,
107
89
  role
@@ -112,13 +94,9 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
112
94
  role
113
95
  });
114
96
 
115
- // Open layer observer registration
116
- // Notifies the open layer observer so app-coordination features
117
- // (closeLayers(), open-count subscriptions) work with top-layer popovers.
118
- //
119
- // Only set type: 'popup' for roles that are interactive overlay popups.
120
- // Passive/informational roles (tooltip, note, status, alert, log) and
121
- // no-role cases do not map to LayerType 'popup', so type is left undefined.
97
+ // Register with open layer observer so `closeLayers()` and open-count
98
+ // subscriptions work. Only popup-like roles register as `popup`; passive
99
+ // roles (tooltip, status, etc.) leave `type` undefined.
122
100
  const handleObserverClose = useCallback(() => {
123
101
  onClose === null || onClose === void 0 ? void 0 : onClose({
124
102
  reason: 'programmatic'
@@ -130,25 +108,29 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
130
108
  onClose: handleObserverClose
131
109
  });
132
110
 
133
- // Hint mode fallback: "hint" downgrades to "auto" if unsupported.
134
- //
135
- // Decision (2026-03-17 audit): We keep "auto" as the fallback (not "manual")
136
- // because it provides the closest behavior to "hint" (light dismiss works).
137
- // However, there is a behavioral difference: "auto" participates in the
138
- // auto-dismiss stack and WILL close other "auto" popovers when it opens
139
- // (e.g. a tooltip opening will close an open dropdown). "hint" does not
140
- // have this side effect. This difference only affects browsers without
141
- // popover="hint" support, and that set is shrinking rapidly.
111
+ // `hint` falls back to `auto` when unsupported: closest behavior (light
112
+ // dismiss works), but `auto` participates in the auto-dismiss stack so it
113
+ // will close other `auto` popovers when opened. Acceptable trade-off given
114
+ // shrinking browser set without `hint` support.
142
115
  const mode = modeProp === 'hint' && !supportsPopoverHint() ? 'auto' : modeProp;
143
116
 
144
- // Distinguishes programmatic hidePopover() from browser-initiated dismiss.
145
- // When we call hidePopover() ourselves the beforetoggle event fires synchronously,
146
- // so this ref prevents the toggle handler from calling onClose redundantly.
117
+ // Prevents toggle handler from calling onClose for our own hidePopover() calls.
147
118
  const programmaticCloseRef = useRef(false);
148
119
 
149
- // Escape key tracking: set in a capture-phase keydown listener (fires before
150
- // the browser processes light dismiss), read in the toggle handler.
120
+ // Set in capture-phase keydown so the toggle handler knows close reason.
151
121
  const closeReasonRef = useRef('light-dismiss');
122
+
123
+ // Snapshot of pre-open focus, used to restore focus for nested popovers
124
+ // with focus-capturing roles (browser only restores the outermost).
125
+ const previouslyFocusedElementRef = useRef(null);
126
+ const handleBeforeToggle = useCallback(event => {
127
+ if (event.newState !== 'open') {
128
+ return;
129
+ }
130
+ // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- need active element snapshot
131
+ const active = document.activeElement;
132
+ previouslyFocusedElementRef.current = active instanceof HTMLElement ? active : null;
133
+ }, []);
152
134
  const handleToggle = useCallback(event => {
153
135
  const element = ownRef.current;
154
136
  if (event.newState === 'open') {
@@ -166,32 +148,52 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
166
148
  isOpen: false,
167
149
  element: element
168
150
  });
151
+
152
+ // Nested-popover focus restoration fallback (browser only
153
+ // restores outermost; Firefox skips nested entirely).
154
+ // Restore only when the role moved focus in on open AND the
155
+ // close was Escape/programmatic. For light dismiss, the
156
+ // click target keeps focus per HTML spec
157
+ // (`focusPreviousElement=false`).
158
+ const previouslyFocused = previouslyFocusedElementRef.current;
159
+ const reason = closeReasonRef.current;
160
+ const isProgrammatic = programmaticCloseRef.current;
161
+ const shouldRestore = previouslyFocused !== null && previouslyFocused.isConnected && shouldFocusIntoPopover({
162
+ role
163
+ }) && (reason === 'escape' || isProgrammatic);
164
+ if (shouldRestore && previouslyFocused) {
165
+ previouslyFocused.focus({
166
+ preventScroll: true
167
+ });
168
+ }
169
+ previouslyFocusedElementRef.current = null;
169
170
  }
170
171
 
171
- // Programmatic close (unmount or isOpen→false). Consumer already knows.
172
+ // Programmatic close: consumer already knows.
172
173
  if (programmaticCloseRef.current) {
174
+ // Reset reason so a stale 'escape' from a race with Escape keydown
175
+ // does not corrupt the next browser-dismiss cycle.
176
+ closeReasonRef.current = 'light-dismiss';
173
177
  return;
174
178
  }
175
179
 
176
- // Browser dismiss (Escape or click-outside). Notify consumer.
180
+ // Browser dismiss (Escape/click-outside).
177
181
  const reason = closeReasonRef.current;
178
- closeReasonRef.current = 'light-dismiss'; // reset for next dismiss
179
- // `onClose` is required for `auto`/`hint` and forbidden for
180
- // `manual`, but `TPopoverForwardedProps` is a flat type used
181
- // internally by `PopupContent` - the optional check guards
182
- // the manual-mode forwarded path where the field is undefined.
182
+ // reset for next dismiss
183
+ closeReasonRef.current = 'light-dismiss';
184
+ // Optional: `manual` mode has no `onClose` in the forwarded type.
183
185
  onClose === null || onClose === void 0 ? void 0 : onClose({
184
186
  reason
185
187
  });
186
188
  }
187
- }, [onClose, onOpenChange]);
189
+ }, [onClose, onOpenChange, role]);
188
190
  useEffect(() => {
189
191
  const element = ownRef.current;
190
192
  if (!element) {
191
193
  return;
192
194
  }
193
195
 
194
- // Capture-phase: tag Escape before the browser processes light dismiss.
196
+ // Tag Escape before the browser processes light dismiss.
195
197
  const unbindEscape = bind(element, {
196
198
  type: 'keydown',
197
199
  listener: event => {
@@ -207,15 +209,20 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
207
209
  type: 'toggle',
208
210
  listener: handleToggle
209
211
  });
212
+ // Snapshot `document.activeElement` before `useInitialFocus` moves it.
213
+ const unbindBeforeToggle = bind(element, {
214
+ type: 'beforetoggle',
215
+ listener: handleBeforeToggle
216
+ });
210
217
  return () => {
211
218
  unbindEscape();
212
219
  unbindToggle();
220
+ unbindBeforeToggle();
213
221
  };
214
- }, [handleToggle]);
222
+ }, [handleToggle, handleBeforeToggle]);
215
223
 
216
- // Placement-dependent animation CSS vars (e.g. slide direction).
217
- // Separate from show/hide so that preset reference changes
218
- // (e.g. inline `animate={slideAndFade()}`) do not re-trigger visibility.
224
+ // Placement-dependent animation CSS vars. Kept separate from show/hide
225
+ // so preset reference changes do not re-trigger visibility.
219
226
  useLayoutEffect(() => {
220
227
  const el = ownRef.current;
221
228
  if (!el || !(preset !== null && preset !== void 0 && preset.getProperties) || !placement) {
@@ -229,14 +236,9 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
229
236
  });
230
237
  }, [preset, placement]);
231
238
 
232
- // Show/hide based on isOpen.
233
- // showPopover() and hidePopover() are no-ops when the popover is already
234
- // in the target state (e.g. browser dismissed via light-dismiss before our
235
- // effect ran).
236
- // Defensive try/catch: these APIs throw InvalidStateError if the element
237
- // is disconnected or missing the popover attribute. While we do not expect
238
- // that to happen in normal usage, it can occur in edge cases (e.g. React
239
- // concurrent mode, StrictMode double-effects, or unmount races).
239
+ // Show/hide based on isOpen. `showPopover`/`hidePopover` are no-ops when
240
+ // already in the target state. Try/catch guards `InvalidStateError` on
241
+ // disconnected elements (StrictMode/concurrent/unmount edge cases).
240
242
  useLayoutEffect(() => {
241
243
  const el = ownRef.current;
242
244
  if (!el) {
@@ -244,20 +246,17 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
244
246
  }
245
247
  if (isOpen) {
246
248
  programmaticCloseRef.current = false;
247
- // Defensive: element may be disconnected or in an unexpected state.
248
249
  try {
249
250
  el.showPopover();
250
251
  } catch {}
251
252
  return () => {
252
253
  programmaticCloseRef.current = true;
253
- // Defensive: element may be disconnected or in an unexpected state.
254
254
  try {
255
255
  el.hidePopover();
256
256
  } catch {}
257
257
  };
258
258
  }
259
259
  programmaticCloseRef.current = true;
260
- // Defensive: element may be disconnected or in an unexpected state.
261
260
  try {
262
261
  el.hidePopover();
263
262
  } catch {}
@@ -0,0 +1 @@
1
+ export { PopoverSurface } from './popover-surface';
@@ -0,0 +1,27 @@
1
+ /* popover-surface.tsx generated by @compiled/babel-plugin v0.39.1 */
2
+ import "./popover-surface.compiled.css";
3
+ import { ax, ix } from "@compiled/react/runtime";
4
+ import React from 'react';
5
+ const styles = {
6
+ root: "_2rkofajl _1reo1wug _18m91wug _bfhk1bhr _16qs130s"
7
+ };
8
+
9
+ /**
10
+ * Optional styled wrapper for popover content.
11
+ *
12
+ * Adds default visual styling (background, border-radius, box-shadow)
13
+ * using design tokens.
14
+ *
15
+ * Presentational primitive - `children`-only by design. This component
16
+ * intentionally exposes no `style`, `className`, `xcss`, `id`, or `ref`
17
+ * props. If you need any of those, render your own wrapper element inside
18
+ * `Popover` instead. `PopoverSurface` is the no-config "give me the default
19
+ * ADS overlay look" surface, nothing more.
20
+ */
21
+ export function PopoverSurface({
22
+ children
23
+ }) {
24
+ return /*#__PURE__*/React.createElement("div", {
25
+ className: ax([styles.root])
26
+ }, children);
27
+ }
@@ -107,7 +107,7 @@ function createToggleScheduler() {
107
107
  newState
108
108
  });
109
109
 
110
- // Task queue (setTimeout) - browser fires toggle after microtasks drain, not during
110
+ // Task queue (setTimeout): browser fires toggle after microtasks drain, not during
111
111
  setTimeout(() => {
112
112
  const entry = pending.get(element);
113
113
  if (!entry) {
@@ -552,10 +552,11 @@ if (typeof HTMLElement !== 'undefined') {
552
552
  element: this
553
553
  })} is not connected to the document. Ensure the element has been added to the DOM (e.g. via render() or document.body.appendChild()) before calling showPopover().`, 'InvalidStateError');
554
554
  }
555
+ // No-op: already showing.
555
556
  if (isPopoverOpen({
556
557
  element: this
557
558
  })) {
558
- return; // already showing - no-op
559
+ return;
559
560
  }
560
561
 
561
562
  // sync, cancelable on show only
@@ -745,8 +746,9 @@ if (typeof HTMLDialogElement !== 'undefined') {
745
746
  })} is not connected to the document. Ensure the element has been added to the DOM (e.g. via render() or document.body.appendChild()) before calling showModal().`, 'InvalidStateError');
746
747
  }
747
748
  if (this.hasAttribute('open')) {
749
+ // No-op: already modal.
748
750
  if (this._isModal) {
749
- return; // already modal - no-op
751
+ return;
750
752
  }
751
753
  throw new DOMException(`Failed to execute 'showModal' on 'HTMLDialogElement': ${describeElement({
752
754
  element: this
@@ -770,7 +772,7 @@ if (typeof HTMLDialogElement !== 'undefined') {
770
772
  }
771
773
  if (typeof proto.show !== 'function') {
772
774
  proto.show = function show() {
773
- // Browser does not throw for disconnected elements on show - just no-op
775
+ // Browser does not throw for disconnected elements on show; no-op.
774
776
  if (!this.isConnected || this.hasAttribute('open')) {
775
777
  return;
776
778
  }
@@ -792,11 +794,12 @@ if (typeof HTMLDialogElement !== 'undefined') {
792
794
  }
793
795
  if (typeof proto.close !== 'function') {
794
796
  proto.close = function close(returnValue) {
797
+ // No-op: already closed.
795
798
  if (!this.hasAttribute('open')) {
796
- return; // already closed - no-op
799
+ return;
797
800
  }
798
801
 
799
- // Sync beforetoggle (not cancelable) - matches browser behavior
802
+ // Sync beforetoggle (not cancelable); matches browser behavior.
800
803
  fireToggleEvent({
801
804
  element: this,
802
805
  type: 'beforetoggle',
@@ -58,10 +58,10 @@ var SLIDE_AND_FADE_CSS = "\n[data-ds-popover-slide-and-fade] {\n opacity: 0;\n
58
58
  * import { slideAndFade } from '@atlaskit/top-layer/animations';
59
59
  *
60
60
  * // Default 4px slide (tooltip)
61
- * <Popup.Content animate={slideAndFade()} />
61
+ * <Popover animate={slideAndFade()} />
62
62
  *
63
63
  * // Custom 8px slide (popup / dropdown)
64
- * <Popup.Content animate={slideAndFade({ distance: 8 })} />
64
+ * <Popover animate={slideAndFade({ distance: 8 })} />
65
65
  * ```
66
66
  */
67
67
  export function slideAndFade(options) {
@@ -95,7 +95,7 @@ var FADE_CSS = "\n[data-ds-popover-fade] {\n opacity: 0;\n transition:\n op
95
95
  * ```tsx
96
96
  * import { fade } from '@atlaskit/top-layer/animations';
97
97
  *
98
- * <Popup.Content animate={fade()} />
98
+ * <Popover animate={fade()} />
99
99
  * ```
100
100
  */
101
101
  // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
@@ -121,7 +121,7 @@ var SCALE_AND_FADE_CSS = "\n[data-ds-popover-scale-and-fade] {\n opacity: 0;\n
121
121
  * ```tsx
122
122
  * import { scaleAndFade } from '@atlaskit/top-layer/animations';
123
123
  *
124
- * <Popup.Content animate={scaleAndFade()} />
124
+ * <Popover animate={scaleAndFade()} />
125
125
  * ```
126
126
  */
127
127
  // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
@@ -10,8 +10,7 @@
10
10
  *
11
11
  * @example
12
12
  * ```tsx
13
- * import { createCloseEvent } from '@atlaskit/top-layer/create-close-event';
14
- * import { type TDialogCloseReason } from '@atlaskit/top-layer/dialog';
13
+ * import { createCloseEvent, type TDialogCloseReason } from '@atlaskit/top-layer/dialog';
15
14
  *
16
15
  * function onDialogClose({ reason }: { reason: TDialogCloseReason }) {
17
16
  * legacyOnClose(createCloseEvent({ reason }));
@@ -1 +1 @@
1
- export { Dialog } from '../dialog';
1
+ export { createCloseEvent, Dialog } from '../dialog';
@@ -0,0 +1 @@
1
+ export { getAriaForTrigger } from '../internal/get-aria-for-trigger';
@@ -0,0 +1 @@
1
+ export { PopoverSurface } from '../popover-surface';
@@ -1 +1 @@
1
- export { Popover } from '../popover';
1
+ export { createPopoverCloseEvent, Popover } from '../popover';
@@ -0,0 +1,15 @@
1
+ import { useId } from '@atlaskit/ds-lib/use-id';
2
+ /**
3
+ * Returns a stable HTML `id` used to wire a trigger element to its popover
4
+ * via ARIA (`aria-controls`) and the Popover API (`popovertarget`).
5
+ *
6
+ * The returned value is intended for the `id` attribute on the popover and
7
+ * for the matching `aria-controls` / `popovertarget` references on the
8
+ * trigger. It is NOT a CSS `anchor-name` value: CSS anchor positioning is
9
+ * handled internally by `useAnchorPosition`, which mints and applies its
10
+ * own `anchor-name` on the trigger element.
11
+ */
12
+ export function usePopoverId() {
13
+ // prefixing for debug clarity in the DOM
14
+ return "popover-".concat(useId());
15
+ }
@@ -0,0 +1,83 @@
1
+ import { roleToAriaHasPopup } from './role-to-aria-has-popup';
2
+
3
+ /**
4
+ * Roles accepted by `getAriaForTrigger`.
5
+ *
6
+ * Excludes tooltip-family roles (`tooltip`, `note`, `status`, `alert`, `log`)
7
+ * because the correct trigger wiring for those is `aria-describedby`, not
8
+ * `aria-haspopup` / `aria-expanded` / `aria-controls`. Use `aria-describedby`
9
+ * manually for tooltip triggers.
10
+ */
11
+
12
+ /**
13
+ * Non-undefined subset of `TAriaHasPopupValue`. Every role accepted by
14
+ * `getAriaForTrigger` maps to a concrete `aria-haspopup` string, so the
15
+ * trigger always receives a defined attribute value.
16
+ */
17
+
18
+ /**
19
+ * Returns the ARIA attributes to spread onto a click-activated popover trigger.
20
+ *
21
+ * Centralises three attributes that are easy to forget or get wrong individually:
22
+ *
23
+ * - `aria-haspopup` - derived from the popover's role (e.g. `dialog → "dialog"`,
24
+ * `menu → "menu"`, `tooltip → undefined`). React omits the attribute when
25
+ * the value is `undefined`.
26
+ * - `aria-expanded` - reflects the current open state.
27
+ * - `aria-controls` - references the popover element by its id, even
28
+ * while the popover is closed. The relationship is stable; `aria-expanded`
29
+ * carries the current visibility state.
30
+ *
31
+ * **This function is for click/keyboard-activated popovers only.** For
32
+ * hover-driven tooltips, the trigger should use `aria-describedby` instead.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * import { getAriaForTrigger } from '@atlaskit/top-layer/get-aria-for-trigger';
37
+ * import { usePopoverId } from '@atlaskit/top-layer/use-popover-id';
38
+ *
39
+ * const popoverId = usePopoverId();
40
+ * const [isOpen, setIsOpen] = useState(false);
41
+ *
42
+ * return (
43
+ * <>
44
+ * <button
45
+ * ref={triggerRef}
46
+ * onClick={() => setIsOpen((previous) => !previous)}
47
+ * {...getAriaForTrigger({ role: 'dialog', isOpen, popoverId })}
48
+ * >
49
+ * Open
50
+ * </button>
51
+ * <Popover
52
+ * id={popoverId}
53
+ * role="dialog"
54
+ * label="My dialog"
55
+ * isOpen={isOpen}
56
+ * triggerRef={triggerRef}
57
+ * onClose={() => setIsOpen(false)}
58
+ * >
59
+ * …
60
+ * </Popover>
61
+ * </>
62
+ * );
63
+ * ```
64
+ */
65
+ export function getAriaForTrigger(_ref) {
66
+ var role = _ref.role,
67
+ isOpen = _ref.isOpen,
68
+ popoverId = _ref.popoverId;
69
+ // `roleToAriaHasPopup` accepts a wider role union (including tooltip-family
70
+ // roles that map to `undefined`). `TAriaForTriggerRole` excludes those, so
71
+ // the returned value is guaranteed to be a defined string. The cast narrows
72
+ // the wider helper signature to match.
73
+ var ariaHasPopup = roleToAriaHasPopup({
74
+ role: role
75
+ });
76
+ return {
77
+ 'aria-haspopup': ariaHasPopup,
78
+ 'aria-expanded': isOpen,
79
+ // Keep the trigger-to-popover relationship stable. `aria-expanded`
80
+ // tells assistive technology whether the controlled popover is visible.
81
+ 'aria-controls': popoverId
82
+ };
83
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Valid values for the `aria-haspopup` attribute.
3
+ *
4
+ * Derived from the HTML spec - maps popover roles to what the trigger
5
+ * announces. `undefined` means the attribute is omitted entirely (used for
6
+ * non-popup roles like `tooltip`/`status`/`alert`/`note`/`log` where
7
+ * `aria-haspopup` would be misleading).
8
+ *
9
+ * `true` is intentionally NOT in the union - the runtime only ever produces
10
+ * the explicit string forms, so widening the type would invite consumers to
11
+ * pass `true` and get an `aria-haspopup="true"` serialisation that the
12
+ * runtime no longer emits.
13
+ */
14
+
15
+ /**
16
+ * Roles that require an accessible name (`label` or `labelledBy`) per WCAG 4.1.2.
17
+ * Without a name, assistive technology cannot identify these landmarks.
18
+ */
19
+
20
+ /**
21
+ * Roles where the accessible name is derived from content or provided externally
22
+ * (e.g. a combobox labels its listbox, tooltip content IS the accessible name).
23
+ */
24
+
25
+ // Shared ARIA role discrimination types
26
+ //
27
+ // These encode the WCAG 4.1.2 constraint: roles like `dialog`, `alertdialog`,
28
+ // and `menu` MUST have an accessible name (either `label` or `labelledBy`).
29
+ // TypeScript enforces this at compile time via discriminated unions.
30
+
31
+ /**
32
+ * ARIA role props where a role is always required.
33
+ *
34
+ * Used by `TPopupContentProps`. Popup content must always declare
35
+ * its role so assistive technology can identify the layer.
36
+ */
37
+
38
+ /**
39
+ * ARIA role props where a role is optional.
40
+ *
41
+ * Used by `TPopoverProps`. The low-level primitive allows omitting
42
+ * the role for cases where it is set externally or not needed.
43
+ */
44
+
45
+ var rolesThatMoveFocus = new Set(['dialog', 'alertdialog', 'menu', 'listbox', 'tree', 'grid']);
46
+
47
+ /**
48
+ * Returns `true` when the given popover role moves focus into the popover
49
+ * on open, and `false` otherwise (including when `role` is `undefined`).
50
+ */
51
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
52
+ export function shouldFocusIntoPopover(_ref) {
53
+ var role = _ref.role;
54
+ if (!role) {
55
+ return false;
56
+ }
57
+ return rolesThatMoveFocus.has(role);
58
+ }