@atlaskit/top-layer 0.9.0 → 0.10.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 (127) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +5 -1
  3. package/dialog/create-close-event/package.json +17 -0
  4. package/dist/cjs/animations/presets.js +7 -4
  5. package/dist/cjs/arrow/index.js +131 -0
  6. package/dist/cjs/arrow/types.js +5 -0
  7. package/dist/cjs/dialog/dialog-content.js +11 -6
  8. package/dist/cjs/entry-points/dialog/create-close-event.js +12 -0
  9. package/dist/cjs/entry-points/placement-map.js +6 -5
  10. package/dist/cjs/entry-points/popover/create-close-event.js +12 -0
  11. package/dist/cjs/focus/focus.js +71 -19
  12. package/dist/cjs/index.js +3 -4
  13. package/dist/cjs/internal/resolve-css-length-to-pixels.js +61 -10
  14. package/dist/cjs/internal/role-types.js +40 -9
  15. package/dist/cjs/internal/set-style.js +23 -25
  16. package/dist/cjs/internal/use-anchor-position.js +107 -68
  17. package/dist/cjs/internal/use-focus-wrap.js +13 -2
  18. package/dist/cjs/internal/use-initial-focus.js +31 -10
  19. package/dist/cjs/placement-map/index.js +9 -111
  20. package/dist/cjs/placement-map/legacy-placements.js +113 -0
  21. package/dist/cjs/popover/popover.js +14 -12
  22. package/dist/cjs/popup/popup-content.js +19 -13
  23. package/dist/cjs/popup/popup-surface.js +6 -0
  24. package/dist/cjs/popup/popup-trigger-function.js +1 -1
  25. package/dist/cjs/popup/popup-trigger.js +17 -37
  26. package/dist/cjs/popup/popup.js +21 -10
  27. package/dist/cjs/use-arrow-navigation/use-arrow-navigation.js +26 -10
  28. package/dist/es2019/animations/presets.js +6 -3
  29. package/dist/es2019/arrow/index.js +269 -0
  30. package/dist/es2019/arrow/types.js +1 -0
  31. package/dist/es2019/dialog/dialog-content.js +11 -6
  32. package/dist/es2019/entry-points/create-close-event.js +7 -0
  33. package/dist/es2019/entry-points/dialog/create-close-event.js +1 -0
  34. package/dist/es2019/entry-points/placement-map.js +2 -1
  35. package/dist/es2019/entry-points/popover/create-close-event.js +1 -0
  36. package/dist/es2019/focus/focus.js +56 -7
  37. package/dist/es2019/index.js +3 -4
  38. package/dist/es2019/internal/resolve-css-length-to-pixels.js +60 -7
  39. package/dist/es2019/internal/role-types.js +40 -9
  40. package/dist/es2019/internal/set-style.js +19 -22
  41. package/dist/es2019/internal/use-anchor-position.js +95 -54
  42. package/dist/es2019/internal/use-focus-wrap.js +14 -3
  43. package/dist/es2019/internal/use-initial-focus.js +32 -11
  44. package/dist/es2019/placement-map/index.js +8 -109
  45. package/dist/es2019/placement-map/legacy-placements.js +107 -0
  46. package/dist/es2019/popover/popover.js +14 -11
  47. package/dist/es2019/popup/popup-content.js +20 -14
  48. package/dist/es2019/popup/popup-surface.js +6 -0
  49. package/dist/es2019/popup/popup-trigger-function.js +1 -1
  50. package/dist/es2019/popup/popup-trigger.js +17 -37
  51. package/dist/es2019/popup/popup.js +20 -9
  52. package/dist/es2019/use-arrow-navigation/use-arrow-navigation.js +26 -10
  53. package/dist/esm/animations/presets.js +6 -3
  54. package/dist/esm/arrow/index.js +124 -0
  55. package/dist/esm/arrow/types.js +1 -0
  56. package/dist/esm/dialog/dialog-content.js +11 -6
  57. package/dist/esm/entry-points/create-close-event.js +7 -0
  58. package/dist/esm/entry-points/dialog/create-close-event.js +1 -0
  59. package/dist/esm/entry-points/placement-map.js +2 -1
  60. package/dist/esm/entry-points/popover/create-close-event.js +1 -0
  61. package/dist/esm/focus/focus.js +70 -19
  62. package/dist/esm/index.js +3 -4
  63. package/dist/esm/internal/resolve-css-length-to-pixels.js +61 -10
  64. package/dist/esm/internal/role-types.js +39 -9
  65. package/dist/esm/internal/set-style.js +23 -25
  66. package/dist/esm/internal/use-anchor-position.js +108 -64
  67. package/dist/esm/internal/use-focus-wrap.js +14 -3
  68. package/dist/esm/internal/use-initial-focus.js +32 -11
  69. package/dist/esm/placement-map/index.js +8 -109
  70. package/dist/esm/placement-map/legacy-placements.js +107 -0
  71. package/dist/esm/popover/popover.js +14 -12
  72. package/dist/esm/popup/popup-content.js +20 -14
  73. package/dist/esm/popup/popup-surface.js +6 -0
  74. package/dist/esm/popup/popup-trigger-function.js +1 -1
  75. package/dist/esm/popup/popup-trigger.js +17 -37
  76. package/dist/esm/popup/popup.js +21 -10
  77. package/dist/esm/use-arrow-navigation/use-arrow-navigation.js +26 -10
  78. package/dist/types/arrow/index.d.ts +24 -0
  79. package/dist/types/arrow/types.d.ts +29 -0
  80. package/dist/types/dialog/dialog-content.d.ts +5 -1
  81. package/dist/types/dialog/types.d.ts +50 -24
  82. package/dist/types/entry-points/create-close-event.d.ts +7 -0
  83. package/dist/types/entry-points/dialog/create-close-event.d.ts +2 -0
  84. package/dist/types/entry-points/placement-map.d.ts +2 -1
  85. package/dist/types/entry-points/popover/create-close-event.d.ts +2 -0
  86. package/dist/types/focus/focus.d.ts +8 -0
  87. package/dist/types/internal/resolve-css-length-to-pixels.d.ts +4 -6
  88. package/dist/types/internal/role-types.d.ts +25 -4
  89. package/dist/types/internal/set-style.d.ts +2 -19
  90. package/dist/types/internal/use-anchor-position.d.ts +2 -3
  91. package/dist/types/internal/use-initial-focus.d.ts +5 -4
  92. package/dist/types/placement-map/index.d.ts +8 -21
  93. package/dist/types/placement-map/legacy-placements.d.ts +21 -0
  94. package/dist/types/popover/popover.d.ts +1 -1
  95. package/dist/types/popover/types.d.ts +17 -17
  96. package/dist/types/popup/popup-surface.d.ts +6 -0
  97. package/dist/types/popup/popup-trigger-function.d.ts +10 -6
  98. package/dist/types/popup/popup-trigger.d.ts +8 -17
  99. package/dist/types/popup/popup.d.ts +2 -2
  100. package/dist/types/popup/types.d.ts +68 -22
  101. package/dist/types/use-arrow-navigation/use-arrow-navigation.d.ts +19 -5
  102. package/dist/types-ts4.5/arrow/index.d.ts +24 -0
  103. package/dist/types-ts4.5/arrow/types.d.ts +29 -0
  104. package/dist/types-ts4.5/dialog/dialog-content.d.ts +5 -1
  105. package/dist/types-ts4.5/dialog/types.d.ts +50 -24
  106. package/dist/types-ts4.5/entry-points/create-close-event.d.ts +7 -0
  107. package/dist/types-ts4.5/entry-points/dialog/create-close-event.d.ts +2 -0
  108. package/dist/types-ts4.5/entry-points/placement-map.d.ts +2 -1
  109. package/dist/types-ts4.5/entry-points/popover/create-close-event.d.ts +2 -0
  110. package/dist/types-ts4.5/focus/focus.d.ts +8 -0
  111. package/dist/types-ts4.5/internal/resolve-css-length-to-pixels.d.ts +4 -6
  112. package/dist/types-ts4.5/internal/role-types.d.ts +25 -4
  113. package/dist/types-ts4.5/internal/set-style.d.ts +2 -19
  114. package/dist/types-ts4.5/internal/use-anchor-position.d.ts +2 -3
  115. package/dist/types-ts4.5/internal/use-initial-focus.d.ts +5 -4
  116. package/dist/types-ts4.5/placement-map/index.d.ts +8 -21
  117. package/dist/types-ts4.5/placement-map/legacy-placements.d.ts +39 -0
  118. package/dist/types-ts4.5/popover/popover.d.ts +1 -1
  119. package/dist/types-ts4.5/popover/types.d.ts +17 -17
  120. package/dist/types-ts4.5/popup/popup-surface.d.ts +6 -0
  121. package/dist/types-ts4.5/popup/popup-trigger-function.d.ts +10 -6
  122. package/dist/types-ts4.5/popup/popup-trigger.d.ts +8 -17
  123. package/dist/types-ts4.5/popup/popup.d.ts +2 -2
  124. package/dist/types-ts4.5/popup/types.d.ts +68 -22
  125. package/dist/types-ts4.5/use-arrow-navigation/use-arrow-navigation.d.ts +19 -5
  126. package/package.json +1 -1
  127. package/popover/create-close-event/package.json +17 -0
@@ -1,7 +1,4 @@
1
- /**
2
- * Legacy Popper.js placement values (used by `@atlaskit/popper`, `@atlaskit/popup`,
3
- * `@atlaskit/tooltip`, etc.).
4
- */
1
+ import { placementMapping } from './legacy-placements';
5
2
 
6
3
  /**
7
4
  * Maps legacy Popper.js placement values to the new object-based
@@ -13,9 +10,11 @@
13
10
  * | ------------------- | --------------------------------------------------- |
14
11
  * | `top` | `{ axis: 'block', edge: 'start', align: 'center' }` |
15
12
  * | `top-start` | `{ axis: 'block', edge: 'start', align: 'start' }` |
13
+ * | `top-center` | `{ axis: 'block', edge: 'start', align: 'center' }` |
16
14
  * | `top-end` | `{ axis: 'block', edge: 'start', align: 'end' }` |
17
15
  * | `bottom` | `{ axis: 'block', edge: 'end', align: 'center' }` |
18
16
  * | `bottom-start` | `{ axis: 'block', edge: 'end', align: 'start' }` |
17
+ * | `bottom-center` | `{ axis: 'block', edge: 'end', align: 'center' }` |
19
18
  * | `bottom-end` | `{ axis: 'block', edge: 'end', align: 'end' }` |
20
19
  * | `right` | `{ axis: 'inline', edge: 'end', align: 'center' }` |
21
20
  * | `right-start` | `{ axis: 'inline', edge: 'end', align: 'start' }` |
@@ -27,7 +26,10 @@
27
26
  * | `auto-start` | `{ axis: 'block', edge: 'end', align: 'start' }` |
28
27
  * | `auto-end` | `{ axis: 'block', edge: 'end', align: 'end' }` |
29
28
  *
30
- * The `position-try-fallbacks` CSS property handles flipping automatically.
29
+ * `auto*` is mapped to `block-end` (a reduction in expressiveness vs Popper's
30
+ * runtime auto-flip). CSS `position-try-fallbacks` provides flipping at the
31
+ * edge - but the initial placement is fixed. If migrating away from Popper's
32
+ * dynamic auto, prefer an explicit placement.
31
33
  *
32
34
  * @example
33
35
  * ```ts
@@ -39,103 +41,6 @@
39
41
  * // newPlacement === { axis: 'block', edge: 'end', align: 'start' }
40
42
  * ```
41
43
  */
42
- const placementMapping = {
43
- top: {
44
- axis: 'block',
45
- edge: 'start',
46
- align: 'center'
47
- },
48
- 'top-start': {
49
- axis: 'block',
50
- edge: 'start',
51
- align: 'start'
52
- },
53
- 'top-center': {
54
- axis: 'block',
55
- edge: 'start',
56
- align: 'center'
57
- },
58
- 'top-end': {
59
- axis: 'block',
60
- edge: 'start',
61
- align: 'end'
62
- },
63
- bottom: {
64
- axis: 'block',
65
- edge: 'end',
66
- align: 'center'
67
- },
68
- 'bottom-start': {
69
- axis: 'block',
70
- edge: 'end',
71
- align: 'start'
72
- },
73
- 'bottom-center': {
74
- axis: 'block',
75
- edge: 'end',
76
- align: 'center'
77
- },
78
- 'bottom-end': {
79
- axis: 'block',
80
- edge: 'end',
81
- align: 'end'
82
- },
83
- right: {
84
- axis: 'inline',
85
- edge: 'end',
86
- align: 'center'
87
- },
88
- 'right-start': {
89
- axis: 'inline',
90
- edge: 'end',
91
- align: 'start'
92
- },
93
- 'right-end': {
94
- axis: 'inline',
95
- edge: 'end',
96
- align: 'end'
97
- },
98
- left: {
99
- axis: 'inline',
100
- edge: 'start',
101
- align: 'center'
102
- },
103
- 'left-start': {
104
- axis: 'inline',
105
- edge: 'start',
106
- align: 'start'
107
- },
108
- 'left-end': {
109
- axis: 'inline',
110
- edge: 'start',
111
- align: 'end'
112
- },
113
- auto: {
114
- axis: 'block',
115
- edge: 'end',
116
- align: 'center'
117
- },
118
- 'auto-start': {
119
- axis: 'block',
120
- edge: 'end',
121
- align: 'start'
122
- },
123
- 'auto-end': {
124
- axis: 'block',
125
- edge: 'end',
126
- align: 'end'
127
- }
128
- };
129
-
130
- /**
131
- * Convert a legacy Popper.js placement to the new `position-area`-based Placement.
132
- * Optional `offset` is the legacy Popper `[along, away]` tuple (also known as
133
- * `[skidding, distance]`); negative `along` becomes `direction: 'backwards'`.
134
- *
135
- * The tuple keeps the historical popper names so call sites that already pass
136
- * `[along, away]` migrate without thinking; the new internal field names
137
- * (`gap` / `crossAxisShift`) are produced inside this adapter.
138
- */
139
44
  export function fromLegacyPlacement({
140
45
  legacy,
141
46
  offset
@@ -156,10 +61,4 @@ export function fromLegacyPlacement({
156
61
  }
157
62
  }
158
63
  };
159
- }
160
-
161
- /**
162
- * The full mapping record, exposed for consumers that need to iterate or introspect.
163
- */
164
- // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
165
- export { placementMapping };
64
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Legacy Popper.js placement values (used by `@atlaskit/popper`,
3
+ * `@atlaskit/popup`, `@atlaskit/tooltip`, etc.). Re-exported from
4
+ * `@atlaskit/top-layer/placement-map` for migration consumers that need
5
+ * to enumerate every placement (e.g. for a settings dropdown). New code
6
+ * should use the object-based `TPlacementOptions` directly.
7
+ */
8
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
9
+ export const LEGACY_PLACEMENTS = ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-center', 'top-end', 'bottom', 'bottom-start', 'bottom-center', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'];
10
+ /**
11
+ * The full mapping record. Internal - use `fromLegacyPlacement` from the
12
+ * public entry-point so the offset-tuple translation is applied consistently.
13
+ *
14
+ * Note: `auto`, `auto-start`, `auto-end` map to `block-end` placements. This
15
+ * is a reduction in expressiveness vs Popper.js (which picked the best
16
+ * placement at runtime). CSS `position-try-fallbacks` flips at the edge but
17
+ * the initial placement is fixed. Migration consumers who relied on Popper's
18
+ * dynamic auto-flip should pick an explicit placement instead.
19
+ */
20
+ // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
21
+ export const placementMapping = {
22
+ top: {
23
+ axis: 'block',
24
+ edge: 'start',
25
+ align: 'center'
26
+ },
27
+ 'top-start': {
28
+ axis: 'block',
29
+ edge: 'start',
30
+ align: 'start'
31
+ },
32
+ 'top-center': {
33
+ axis: 'block',
34
+ edge: 'start',
35
+ align: 'center'
36
+ },
37
+ 'top-end': {
38
+ axis: 'block',
39
+ edge: 'start',
40
+ align: 'end'
41
+ },
42
+ bottom: {
43
+ axis: 'block',
44
+ edge: 'end',
45
+ align: 'center'
46
+ },
47
+ 'bottom-start': {
48
+ axis: 'block',
49
+ edge: 'end',
50
+ align: 'start'
51
+ },
52
+ 'bottom-center': {
53
+ axis: 'block',
54
+ edge: 'end',
55
+ align: 'center'
56
+ },
57
+ 'bottom-end': {
58
+ axis: 'block',
59
+ edge: 'end',
60
+ align: 'end'
61
+ },
62
+ right: {
63
+ axis: 'inline',
64
+ edge: 'end',
65
+ align: 'center'
66
+ },
67
+ 'right-start': {
68
+ axis: 'inline',
69
+ edge: 'end',
70
+ align: 'start'
71
+ },
72
+ 'right-end': {
73
+ axis: 'inline',
74
+ edge: 'end',
75
+ align: 'end'
76
+ },
77
+ left: {
78
+ axis: 'inline',
79
+ edge: 'start',
80
+ align: 'center'
81
+ },
82
+ 'left-start': {
83
+ axis: 'inline',
84
+ edge: 'start',
85
+ align: 'start'
86
+ },
87
+ 'left-end': {
88
+ axis: 'inline',
89
+ edge: 'start',
90
+ align: 'end'
91
+ },
92
+ auto: {
93
+ axis: 'block',
94
+ edge: 'end',
95
+ align: 'center'
96
+ },
97
+ 'auto-start': {
98
+ axis: 'block',
99
+ edge: 'end',
100
+ align: 'start'
101
+ },
102
+ 'auto-end': {
103
+ axis: 'block',
104
+ edge: 'end',
105
+ align: 'end'
106
+ }
107
+ };
@@ -5,7 +5,6 @@ import { ax, ix } from "@compiled/react/runtime";
5
5
  import React, { forwardRef, useCallback, useEffect, useId, useLayoutEffect, useRef } from 'react';
6
6
  import { bind } from 'bind-event-listener';
7
7
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
8
- import noop from '@atlaskit/ds-lib/noop';
9
8
  import once from '@atlaskit/ds-lib/once';
10
9
  import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
11
10
  import { useAnimatedVisibility } from '../internal/use-animated-visibility';
@@ -56,13 +55,13 @@ const POPUP_ROLES = new Set(['menu', 'listbox', 'dialog', 'alertdialog', 'tree',
56
55
  *
57
56
  * For `mode="auto"`, the browser can dismiss the popover via light dismiss
58
57
  * (Escape, click outside). When that happens, `onClose` is called and the
59
- * consumer must set `isOpen` to `false`. The DOM owns the dismiss — React
58
+ * consumer must set `isOpen` to `false`. The DOM owns the dismiss; React
60
59
  * must follow.
61
60
  */
62
61
  export const Popover = /*#__PURE__*/forwardRef(function Popover({
63
62
  children,
64
63
  mode: modeProp = 'auto',
65
- onClose = noop,
64
+ onClose,
66
65
  onOpenChange,
67
66
  onExitFinish,
68
67
  animate,
@@ -79,7 +78,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
79
78
  const autoId = useId();
80
79
  const ownRef = useRef(null);
81
80
  const combinedRef = mergeRefs([ownRef, ref].filter(Boolean));
82
- // Regex is intentional — `replaceAll` is not available in our supported
81
+ // Regex is intentional: `replaceAll` is not available in our supported
83
82
  // browser matrix. In React 18, `useId()` returns IDs containing colons
84
83
  // (e.g. `:r1:`) which are invalid in CSS selectors and `popover` target
85
84
  // attributes. React 19.2.0 replaced colons with underscores, making this
@@ -98,7 +97,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
98
97
  // ── Focus management ──
99
98
  // Initial focus: on open, moves focus into the popover (role-dependent).
100
99
  // Focus wrap: Tab/Shift+Tab cycle within the popover for dialog roles.
101
- // Focus restoration: handled natively by the Popover API — the browser
100
+ // Focus restoration: handled natively by the Popover API. The browser
102
101
  // tracks `previouslyFocusedElement` on show and restores it on hide.
103
102
  // No custom restoration hook is needed. See: notes/architecture/focus-restoration.md
104
103
  useFocusWrap({
@@ -119,7 +118,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
119
118
  // Passive/informational roles (tooltip, note, status, alert, log) and
120
119
  // no-role cases do not map to LayerType 'popup', so type is left undefined.
121
120
  const handleObserverClose = useCallback(() => {
122
- onClose({
121
+ onClose === null || onClose === void 0 ? void 0 : onClose({
123
122
  reason: 'programmatic'
124
123
  });
125
124
  }, [onClose]);
@@ -141,8 +140,8 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
141
140
  const mode = modeProp === 'hint' && !supportsPopoverHint() ? 'auto' : modeProp;
142
141
 
143
142
  // Distinguishes programmatic hidePopover() from browser-initiated dismiss.
144
- // When we call hidePopover() ourselves the beforetoggle event fires synchronously —
145
- // this ref prevents the toggle handler from calling onClose redundantly.
143
+ // When we call hidePopover() ourselves the beforetoggle event fires synchronously,
144
+ // so this ref prevents the toggle handler from calling onClose redundantly.
146
145
  const programmaticCloseRef = useRef(false);
147
146
 
148
147
  // Escape key tracking: set in a capture-phase keydown listener (fires before
@@ -175,7 +174,11 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
175
174
  // Browser dismiss (Escape or click-outside). Notify consumer.
176
175
  const reason = closeReasonRef.current;
177
176
  closeReasonRef.current = 'light-dismiss'; // reset for next dismiss
178
- onClose({
177
+ // `onClose` is required for `auto`/`hint` and forbidden for
178
+ // `manual`, but `TPopoverForwardedProps` is a flat type used
179
+ // internally by `PopupContent` - the optional check guards
180
+ // the manual-mode forwarded path where the field is undefined.
181
+ onClose === null || onClose === void 0 ? void 0 : onClose({
179
182
  reason
180
183
  });
181
184
  }
@@ -210,7 +213,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
210
213
 
211
214
  // Placement-dependent animation CSS vars (e.g. slide direction).
212
215
  // Separate from show/hide so that preset reference changes
213
- // (e.g. inline `animate={slideAndFade()}`) don't re-trigger visibility.
216
+ // (e.g. inline `animate={slideAndFade()}`) do not re-trigger visibility.
214
217
  useLayoutEffect(() => {
215
218
  const el = ownRef.current;
216
219
  if (!el || !(preset !== null && preset !== void 0 && preset.getProperties) || !placement) {
@@ -229,7 +232,7 @@ export const Popover = /*#__PURE__*/forwardRef(function Popover({
229
232
  // in the target state (e.g. browser dismissed via light-dismiss before our
230
233
  // effect ran).
231
234
  // Defensive try/catch: these APIs throw InvalidStateError if the element
232
- // is disconnected or missing the popover attribute. While we don't expect
235
+ // is disconnected or missing the popover attribute. While we do not expect
233
236
  // that to happen in normal usage, it can occur in edge cases (e.g. React
234
237
  // concurrent mode, StrictMode double-effects, or unmount races).
235
238
  useLayoutEffect(() => {
@@ -3,7 +3,7 @@ import _extends from "@babel/runtime/helpers/extends";
3
3
  import { ax, ix } from "@compiled/react/runtime";
4
4
  import React, { forwardRef, useCallback, useLayoutEffect, useRef } from 'react';
5
5
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
6
- import { roleToAriaHasPopup } from '../internal/role-types';
6
+ import { roleToAriaHasPopup, shouldFocusIntoPopover } from '../internal/role-types';
7
7
  import { useAnchorPosition } from '../internal/use-anchor-position';
8
8
  import { useWidthFromAnchor } from '../internal/use-width-from-anchor';
9
9
  import { Popover } from '../popover/popover';
@@ -29,7 +29,8 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
29
29
  placement: placementProp,
30
30
  onClose: onCloseProp,
31
31
  testId: testIdProp,
32
- forceFallbackPositioning: forceFallbackPositioningProp
32
+ forceFallbackPositioning: forceFallbackPositioningProp,
33
+ id: idProp
33
34
  }, ref) {
34
35
  var _ref, _ref2, _ctx$popoverRef;
35
36
  // When inside <Popup>, context provides defaults. Props always win.
@@ -74,7 +75,7 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
74
75
  // does not restore focus when they close.
75
76
  //
76
77
  // To handle this, we listen for the close transition via `onOpenChange` and
77
- // restore focus to the trigger if the browser didn't. This runs after the
78
+ // restore focus to the trigger if the browser did not. This runs after the
78
79
  // browser's native restoration (which happens synchronously during hidePopover),
79
80
  // so it only takes effect when the browser skipped restoration (nested case).
80
81
  //
@@ -91,7 +92,7 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
91
92
  // focus when they close.
92
93
  //
93
94
  // We detect this case by checking if focus is still inside the
94
- // closing popover element. If it is, the browser didn't restore,
95
+ // closing popover element. If it is, the browser did not restore,
95
96
  // and we need to move focus to the trigger ourselves.
96
97
  //
97
98
  // This does NOT fire for:
@@ -100,13 +101,15 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
100
101
  if (!args.isOpen) {
101
102
  const trigger = triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.current;
102
103
  const focusStillInPopover = trigger && args.element.contains(document.activeElement);
103
- if (focusStillInPopover) {
104
- const shouldRestore = role === 'dialog' || role === 'alertdialog' || role === 'menu' || role === 'listbox';
105
- if (shouldRestore) {
106
- trigger.focus({
107
- preventScroll: true
108
- });
109
- }
104
+ // Single source of truth for "this role moves focus on open
105
+ // and therefore needs to be restored on close" - see
106
+ // `shouldFocusIntoPopover` in `internal/role-types.tsx`.
107
+ if (focusStillInPopover && shouldFocusIntoPopover({
108
+ role
109
+ })) {
110
+ trigger.focus({
111
+ preventScroll: true
112
+ });
110
113
  }
111
114
  }
112
115
  }, [setIsOpen, consumerOnOpenChange, triggerRef, role]);
@@ -136,8 +139,8 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
136
139
  const combinedRef = mergeRefs([popoverRef, ref]);
137
140
 
138
141
  // TPopupContentProps already validates the discriminated union (role + label,
139
- // mode + onClose) at its API boundary. Popover accepts TPopoverInternalProps
140
- // internally so individual prop forwarding doesn't need to re-prove the union.
142
+ // mode + onClose) at its API boundary. Popover accepts TPopoverForwardedProps
143
+ // internally so individual prop forwarding does not need to re-prove the union.
141
144
  const popoverProps = {
142
145
  isOpen,
143
146
  animate,
@@ -150,7 +153,10 @@ export const PopupContent = /*#__PURE__*/forwardRef(function PopupContent({
150
153
  onOpenChange: handleOpenChange,
151
154
  testId,
152
155
  children,
153
- id: ctx === null || ctx === void 0 ? void 0 : ctx.popoverId,
156
+ // Standalone consumers can pass an explicit `id` - used by their own
157
+ // trigger to wire `aria-controls`. Inside the `<Popup>` compound, the
158
+ // id flows from context (`ctx.popoverId`).
159
+ id: idProp !== null && idProp !== void 0 ? idProp : ctx === null || ctx === void 0 ? void 0 : ctx.popoverId,
154
160
  xcss
155
161
  };
156
162
  return /*#__PURE__*/React.createElement(Popover, _extends({
@@ -14,6 +14,12 @@ const styles = {
14
14
  * should use `Popup.Content` without wrapping in `PopupSurface`.
15
15
  *
16
16
  * Exposed as `Popup.Surface` on the compound component.
17
+ *
18
+ * **Presentational primitive - `children`-only by design.** This component
19
+ * intentionally exposes no `style`, `className`, `xcss`, `id`, or `ref`
20
+ * props. If you need any of those, render your own wrapper element inside
21
+ * `Popup.Content` instead - `PopupSurface` is the no-config "give me the
22
+ * default ADS overlay look" surface, nothing more.
17
23
  */
18
24
  export function PopupSurface({
19
25
  children
@@ -8,7 +8,7 @@ import { usePopupContext } from './popup-context';
8
8
  /**
9
9
  * Render-prop alternative to `Popup.Trigger`.
10
10
  *
11
- * Use when you need full control over the trigger element — for example,
11
+ * Use when you need full control over the trigger element. For example,
12
12
  * to compose your own trigger with custom event handlers, wrap a third-party
13
13
  * component, or build a trigger from multiple elements.
14
14
  *
@@ -2,24 +2,15 @@ import React, { isValidElement, useCallback } from 'react';
2
2
  import { Slot } from '../internal/slot';
3
3
  import { usePopupContext } from './popup-context';
4
4
  /**
5
- * Wraps the trigger element. Attaches aria attributes and handles
6
- * click to toggle the popover. Sets the anchor-name on the trigger
7
- * for CSS Anchor Positioning.
5
+ * Wraps a single trigger element via `cloneElement`. Attaches aria
6
+ * attributes, the toggle handler, and the anchor-name. `aria-haspopup`
7
+ * is derived from `Popup.Content`'s role via context.
8
8
  *
9
- * `aria-haspopup` is derived from the content's role via context
10
- * (set by `Popup.Content`). This ensures the trigger always matches
11
- * the content's semantic role without the consumer specifying it
12
- * in two places.
13
- *
14
- * **Limitation:** Uses `cloneElement` internally (via `Slot`), which is
15
- * incompatible with Compiled's `css` prop on the direct child element.
16
- * The `@jsx jsx` pragma transforms the child before `cloneElement` can
17
- * inject `onClick` and `ref` onto the DOM node, preventing the popover
18
- * from opening.
19
- *
20
- * If you need Compiled `css` on the trigger, use `Popup.TriggerFunction`
21
- * instead - it hands you raw props via a render-prop and avoids
22
- * `cloneElement` entirely.
9
+ * Child constraints: must be a single ref-forwarding element. The child's
10
+ * `onClick` is composed (runs after); other event handlers are NOT.
11
+ * Children using Compiled's `css` prop swallow injected props - use
12
+ * `Popup.TriggerFunction` for these cases or any time you need handler
13
+ * composition.
23
14
  */
24
15
  export function PopupTrigger({
25
16
  children
@@ -33,35 +24,24 @@ export function PopupTrigger({
33
24
  } = usePopupContext();
34
25
  const childOnClick = /*#__PURE__*/isValidElement(children) ? children.props.onClick : undefined;
35
26
 
36
- // Use a ref to track the latest isOpen value so the click handler
37
- // does not need isOpen in its dependency array (which would recreate
38
- // the handler on every open/close, breaking cloneElement identity).
27
+ // Track latest isOpen via ref so the click handler keeps a stable
28
+ // identity (cloneElement relies on it).
39
29
  const isOpenRef = React.useRef(isOpen);
40
30
  isOpenRef.current = isOpen;
41
31
  const handleClick = useCallback(event => {
42
32
  const popoverEl = popoverRef.current;
43
33
  if (popoverEl) {
44
- // For popover="auto", clicking the trigger (which is outside
45
- // the popover) can cause the browser's built-in light-dismiss
46
- // to fire before our click handler runs. This means by the time
47
- // we get here, the DOM may have already closed the popover.
48
- //
49
- // We use React's `isOpen` state (via ref) rather than the DOM
50
- // state to determine intent:
51
- // - If React thinks it was open → the user wants to close.
52
- // The browser already did this via light-dismiss, so do nothing.
53
- // - If React thinks it was closed → the user wants to open.
54
- // Call showPopover().
34
+ // `popover="auto"` light-dismiss can close the popover BEFORE
35
+ // our click handler runs, so trust React's isOpen rather than
36
+ // DOM state. If React still thinks it is closed, open it; if
37
+ // it was open, the browser already closed it.
55
38
  if (!isOpenRef.current) {
56
- // Defensive: element may be disconnected or in an unexpected state.
57
39
  try {
58
40
  popoverEl.showPopover();
59
- } catch {}
41
+ } catch {
42
+ // Element may be disconnected; ignore.
43
+ }
60
44
  }
61
- // When isOpenRef.current is true, the browser's light-dismiss
62
- // already closed the popover and fired the toggle event, which
63
- // will update React state via handleToggle → setIsOpen(false).
64
- // No action needed here.
65
45
  }
66
46
  childOnClick === null || childOnClick === void 0 ? void 0 : childOnClick(event);
67
47
  }, [popoverRef, childOnClick]);
@@ -1,10 +1,10 @@
1
1
  import React, { useCallback, useId, useRef, useState } from 'react';
2
2
  import { PopupProvider } from './popup-context';
3
3
  /**
4
- * Popup compound — trigger + popover content.
4
+ * Popup compound: trigger + popover content.
5
5
  *
6
6
  * Use when a button opens anchored content (dropdown, menu, dialog).
7
- * The browser manages visibility via `togglePopover()` — the consumer
7
+ * The browser manages visibility via `togglePopover()`; the consumer
8
8
  * never manages `isOpen`. For custom trigger lifecycles (hover, timers,
9
9
  * external state), use `Popover` directly instead.
10
10
  *
@@ -12,25 +12,33 @@ import { PopupProvider } from './popup-context';
12
12
  * `Popup.Content` reads and forwards to the underlying `Popover`.
13
13
  */
14
14
  export function PopupRoot(props) {
15
- var _props$mode;
15
+ var _props$placement, _props$mode;
16
16
  const {
17
- placement,
18
17
  children,
19
18
  testId,
20
19
  forceFallbackPositioning,
21
20
  onOpenChange
22
21
  } = props;
22
+ // `{}` resolves to "below the trigger, centered, with `space.100` gap"
23
+ // inside `useAnchorPosition`, which is the common case.
24
+ const placement = (_props$placement = props.placement) !== null && _props$placement !== void 0 ? _props$placement : {};
23
25
  const mode = (_props$mode = props.mode) !== null && _props$mode !== void 0 ? _props$mode : 'auto';
24
- const onClose = mode === 'manual' ? null : props.onClose;
26
+
27
+ // `TPopupProps` discriminates on `mode`: `manual` types `onClose` as
28
+ // `never`, so for manual consumers `props.onClose` is always `undefined`
29
+ // here. No runtime guard needed; the type contract carries the rule.
30
+ const onClose = props.onClose;
25
31
  const id = useId();
26
- // Regex is intentional — `replaceAll` is not available in our supported
27
- // browser matrix. `useId()` returns IDs containing colons (e.g. `:r1:`)
28
- // which are invalid in CSS selectors and `popover` target attributes.
32
+ // `useId()` returns IDs containing colons (`:r1:`), which are invalid
33
+ // in CSS selectors and `popover` target attributes. `replaceAll` is
34
+ // not in our browser matrix.
29
35
  const sanitizedId = id.replace(/:/g, '');
30
36
  const popoverId = `popover-${sanitizedId}`;
31
37
  const triggerRef = useRef(null);
32
38
  const popoverRef = useRef(null);
33
39
  const [isOpen, setIsOpen] = useState(false);
40
+ // Initial seed before `Popup.Content`'s layout effect resolves the
41
+ // real role on first paint.
34
42
  const [ariaHasPopup, setAriaHasPopup] = useState('dialog');
35
43
  const handleClose = useCallback(args => {
36
44
  onClose === null || onClose === void 0 ? void 0 : onClose(args);
@@ -38,7 +46,10 @@ export function PopupRoot(props) {
38
46
  const contextValue = {
39
47
  popoverId,
40
48
  placement,
41
- onClose: mode === 'manual' ? null : handleClose,
49
+ // `null` when the consumer did not supply a handler (manual mode, or
50
+ // auto/hint without a light-dismiss handler). `Popup.Content` reads
51
+ // this to decide whether to forward an `onClose` to `Popover`.
52
+ onClose: onClose ? handleClose : null,
42
53
  triggerRef,
43
54
  popoverRef,
44
55
  isOpen,