@atlaskit/top-layer 0.1.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 (300) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +151 -0
  3. package/animations/package.json +17 -0
  4. package/arrow/package.json +17 -0
  5. package/create-close-event/package.json +17 -0
  6. package/dialog/package.json +17 -0
  7. package/dialog-scroll-lock/package.json +17 -0
  8. package/dist/cjs/animations/presets.js +199 -0
  9. package/dist/cjs/animations/types.js +5 -0
  10. package/dist/cjs/arrow/index.js +130 -0
  11. package/dist/cjs/arrow/types.js +5 -0
  12. package/dist/cjs/dialog/create-close-event.js +39 -0
  13. package/dist/cjs/dialog/dialog-content.compiled.css +14 -0
  14. package/dist/cjs/dialog/dialog-content.js +144 -0
  15. package/dist/cjs/dialog/index.js +19 -0
  16. package/dist/cjs/dialog/types.js +5 -0
  17. package/dist/cjs/dialog-scroll-lock/index.js +46 -0
  18. package/dist/cjs/entry-points/animations.js +36 -0
  19. package/dist/cjs/entry-points/arrow.js +12 -0
  20. package/dist/cjs/entry-points/create-close-event.js +19 -0
  21. package/dist/cjs/entry-points/dialog-scroll-lock.js +12 -0
  22. package/dist/cjs/entry-points/dialog.js +12 -0
  23. package/dist/cjs/entry-points/focus.js +24 -0
  24. package/dist/cjs/entry-points/placement-map.js +18 -0
  25. package/dist/cjs/entry-points/popover.js +12 -0
  26. package/dist/cjs/entry-points/popup-surface.js +12 -0
  27. package/dist/cjs/entry-points/popup.js +12 -0
  28. package/dist/cjs/entry-points/use-anchor-position.js +12 -0
  29. package/dist/cjs/entry-points/use-arrow-navigation.js +18 -0
  30. package/dist/cjs/entry-points/use-simple-light-dismiss.js +12 -0
  31. package/dist/cjs/focus/focus.js +115 -0
  32. package/dist/cjs/focus/index.js +24 -0
  33. package/dist/cjs/index.js +8 -0
  34. package/dist/cjs/internal/anchor-positioning-fallback.js +154 -0
  35. package/dist/cjs/internal/combine.js +29 -0
  36. package/dist/cjs/internal/ensure-preset-styles.js +34 -0
  37. package/dist/cjs/internal/reduced-motion.js +27 -0
  38. package/dist/cjs/internal/resolve-placement.js +24 -0
  39. package/dist/cjs/internal/role-types.js +66 -0
  40. package/dist/cjs/internal/set-style.js +42 -0
  41. package/dist/cjs/internal/slot.js +65 -0
  42. package/dist/cjs/internal/use-anchor-positioning.js +336 -0
  43. package/dist/cjs/internal/use-animated-visibility.js +169 -0
  44. package/dist/cjs/internal/use-animation-preset.js +24 -0
  45. package/dist/cjs/internal/use-focus-wrap.js +94 -0
  46. package/dist/cjs/internal/use-initial-focus.js +112 -0
  47. package/dist/cjs/internal/use-preset-styles.js +27 -0
  48. package/dist/cjs/placement-map/index.js +147 -0
  49. package/dist/cjs/popover/create-close-event.js +44 -0
  50. package/dist/cjs/popover/index.js +19 -0
  51. package/dist/cjs/popover/popover.compiled.css +13 -0
  52. package/dist/cjs/popover/popover.js +251 -0
  53. package/dist/cjs/popover/types.js +5 -0
  54. package/dist/cjs/popup/index.js +37 -0
  55. package/dist/cjs/popup/popup-content.js +201 -0
  56. package/dist/cjs/popup/popup-context.js +42 -0
  57. package/dist/cjs/popup/popup-surface.compiled.css +5 -0
  58. package/dist/cjs/popup/popup-surface.js +30 -0
  59. package/dist/cjs/popup/popup-trigger-function.js +74 -0
  60. package/dist/cjs/popup/popup-trigger.js +82 -0
  61. package/dist/cjs/popup/popup.js +70 -0
  62. package/dist/cjs/popup/types.js +5 -0
  63. package/dist/cjs/use-arrow-navigation/index.js +18 -0
  64. package/dist/cjs/use-arrow-navigation/use-arrow-navigation.js +245 -0
  65. package/dist/cjs/use-simple-light-dismiss/index.js +84 -0
  66. package/dist/es2019/animations/presets.js +375 -0
  67. package/dist/es2019/animations/types.js +1 -0
  68. package/dist/es2019/arrow/index.js +249 -0
  69. package/dist/es2019/arrow/types.js +1 -0
  70. package/dist/es2019/dialog/create-close-event.js +34 -0
  71. package/dist/es2019/dialog/dialog-content.compiled.css +14 -0
  72. package/dist/es2019/dialog/dialog-content.js +138 -0
  73. package/dist/es2019/dialog/index.js +2 -0
  74. package/dist/es2019/dialog/types.js +1 -0
  75. package/dist/es2019/dialog-scroll-lock/index.js +41 -0
  76. package/dist/es2019/entry-points/animations.js +1 -0
  77. package/dist/es2019/entry-points/arrow.js +1 -0
  78. package/dist/es2019/entry-points/create-close-event.js +2 -0
  79. package/dist/es2019/entry-points/dialog-scroll-lock.js +1 -0
  80. package/dist/es2019/entry-points/dialog.js +1 -0
  81. package/dist/es2019/entry-points/focus.js +1 -0
  82. package/dist/es2019/entry-points/placement-map.js +1 -0
  83. package/dist/es2019/entry-points/popover.js +1 -0
  84. package/dist/es2019/entry-points/popup-surface.js +1 -0
  85. package/dist/es2019/entry-points/popup.js +1 -0
  86. package/dist/es2019/entry-points/use-anchor-position.js +1 -0
  87. package/dist/es2019/entry-points/use-arrow-navigation.js +1 -0
  88. package/dist/es2019/entry-points/use-simple-light-dismiss.js +1 -0
  89. package/dist/es2019/focus/focus.js +107 -0
  90. package/dist/es2019/focus/index.js +1 -0
  91. package/dist/es2019/index.js +7 -0
  92. package/dist/es2019/internal/anchor-positioning-fallback.js +153 -0
  93. package/dist/es2019/internal/combine.js +18 -0
  94. package/dist/es2019/internal/ensure-preset-styles.js +29 -0
  95. package/dist/es2019/internal/reduced-motion.js +21 -0
  96. package/dist/es2019/internal/resolve-placement.js +19 -0
  97. package/dist/es2019/internal/role-types.js +61 -0
  98. package/dist/es2019/internal/set-style.js +39 -0
  99. package/dist/es2019/internal/slot.js +57 -0
  100. package/dist/es2019/internal/use-anchor-positioning.js +330 -0
  101. package/dist/es2019/internal/use-animated-visibility.js +159 -0
  102. package/dist/es2019/internal/use-animation-preset.js +18 -0
  103. package/dist/es2019/internal/use-focus-wrap.js +90 -0
  104. package/dist/es2019/internal/use-initial-focus.js +109 -0
  105. package/dist/es2019/internal/use-preset-styles.js +22 -0
  106. package/dist/es2019/placement-map/index.js +142 -0
  107. package/dist/es2019/popover/create-close-event.js +39 -0
  108. package/dist/es2019/popover/index.js +2 -0
  109. package/dist/es2019/popover/popover.compiled.css +13 -0
  110. package/dist/es2019/popover/popover.js +240 -0
  111. package/dist/es2019/popover/types.js +1 -0
  112. package/dist/es2019/popup/index.js +33 -0
  113. package/dist/es2019/popup/popup-content.js +192 -0
  114. package/dist/es2019/popup/popup-context.js +34 -0
  115. package/dist/es2019/popup/popup-surface.compiled.css +5 -0
  116. package/dist/es2019/popup/popup-surface.js +24 -0
  117. package/dist/es2019/popup/popup-trigger-function.js +69 -0
  118. package/dist/es2019/popup/popup-trigger.js +76 -0
  119. package/dist/es2019/popup/popup.js +56 -0
  120. package/dist/es2019/popup/types.js +1 -0
  121. package/dist/es2019/use-arrow-navigation/index.js +1 -0
  122. package/dist/es2019/use-arrow-navigation/use-arrow-navigation.js +238 -0
  123. package/dist/es2019/use-simple-light-dismiss/index.js +79 -0
  124. package/dist/esm/animations/presets.js +190 -0
  125. package/dist/esm/animations/types.js +1 -0
  126. package/dist/esm/arrow/index.js +123 -0
  127. package/dist/esm/arrow/types.js +1 -0
  128. package/dist/esm/dialog/create-close-event.js +33 -0
  129. package/dist/esm/dialog/dialog-content.compiled.css +14 -0
  130. package/dist/esm/dialog/dialog-content.js +135 -0
  131. package/dist/esm/dialog/index.js +2 -0
  132. package/dist/esm/dialog/types.js +1 -0
  133. package/dist/esm/dialog-scroll-lock/index.js +41 -0
  134. package/dist/esm/entry-points/animations.js +1 -0
  135. package/dist/esm/entry-points/arrow.js +1 -0
  136. package/dist/esm/entry-points/create-close-event.js +2 -0
  137. package/dist/esm/entry-points/dialog-scroll-lock.js +1 -0
  138. package/dist/esm/entry-points/dialog.js +1 -0
  139. package/dist/esm/entry-points/focus.js +1 -0
  140. package/dist/esm/entry-points/placement-map.js +1 -0
  141. package/dist/esm/entry-points/popover.js +1 -0
  142. package/dist/esm/entry-points/popup-surface.js +1 -0
  143. package/dist/esm/entry-points/popup.js +1 -0
  144. package/dist/esm/entry-points/use-anchor-position.js +1 -0
  145. package/dist/esm/entry-points/use-arrow-navigation.js +1 -0
  146. package/dist/esm/entry-points/use-simple-light-dismiss.js +1 -0
  147. package/dist/esm/focus/focus.js +107 -0
  148. package/dist/esm/focus/index.js +1 -0
  149. package/dist/esm/index.js +7 -0
  150. package/dist/esm/internal/anchor-positioning-fallback.js +147 -0
  151. package/dist/esm/internal/combine.js +23 -0
  152. package/dist/esm/internal/ensure-preset-styles.js +28 -0
  153. package/dist/esm/internal/reduced-motion.js +21 -0
  154. package/dist/esm/internal/resolve-placement.js +18 -0
  155. package/dist/esm/internal/role-types.js +60 -0
  156. package/dist/esm/internal/set-style.js +36 -0
  157. package/dist/esm/internal/slot.js +59 -0
  158. package/dist/esm/internal/use-anchor-positioning.js +323 -0
  159. package/dist/esm/internal/use-animated-visibility.js +162 -0
  160. package/dist/esm/internal/use-animation-preset.js +18 -0
  161. package/dist/esm/internal/use-focus-wrap.js +89 -0
  162. package/dist/esm/internal/use-initial-focus.js +107 -0
  163. package/dist/esm/internal/use-preset-styles.js +21 -0
  164. package/dist/esm/placement-map/index.js +141 -0
  165. package/dist/esm/popover/create-close-event.js +38 -0
  166. package/dist/esm/popover/index.js +2 -0
  167. package/dist/esm/popover/popover.compiled.css +13 -0
  168. package/dist/esm/popover/popover.js +242 -0
  169. package/dist/esm/popover/types.js +1 -0
  170. package/dist/esm/popup/index.js +33 -0
  171. package/dist/esm/popup/popup-content.js +192 -0
  172. package/dist/esm/popup/popup-context.js +34 -0
  173. package/dist/esm/popup/popup-surface.compiled.css +5 -0
  174. package/dist/esm/popup/popup-surface.js +23 -0
  175. package/dist/esm/popup/popup-trigger-function.js +69 -0
  176. package/dist/esm/popup/popup-trigger.js +74 -0
  177. package/dist/esm/popup/popup.js +61 -0
  178. package/dist/esm/popup/types.js +1 -0
  179. package/dist/esm/use-arrow-navigation/index.js +1 -0
  180. package/dist/esm/use-arrow-navigation/use-arrow-navigation.js +238 -0
  181. package/dist/esm/use-simple-light-dismiss/index.js +78 -0
  182. package/dist/types/animations/presets.d.ts +94 -0
  183. package/dist/types/animations/types.d.ts +42 -0
  184. package/dist/types/arrow/index.d.ts +24 -0
  185. package/dist/types/arrow/types.d.ts +29 -0
  186. package/dist/types/dialog/create-close-event.d.ts +24 -0
  187. package/dist/types/dialog/dialog-content.d.ts +20 -0
  188. package/dist/types/dialog/index.d.ts +3 -0
  189. package/dist/types/dialog/types.d.ts +92 -0
  190. package/dist/types/dialog-scroll-lock/index.d.ts +24 -0
  191. package/dist/types/entry-points/animations.d.ts +2 -0
  192. package/dist/types/entry-points/arrow.d.ts +2 -0
  193. package/dist/types/entry-points/create-close-event.d.ts +4 -0
  194. package/dist/types/entry-points/dialog-scroll-lock.d.ts +1 -0
  195. package/dist/types/entry-points/dialog.d.ts +2 -0
  196. package/dist/types/entry-points/focus.d.ts +2 -0
  197. package/dist/types/entry-points/placement-map.d.ts +2 -0
  198. package/dist/types/entry-points/popover.d.ts +2 -0
  199. package/dist/types/entry-points/popup-surface.d.ts +2 -0
  200. package/dist/types/entry-points/popup.d.ts +2 -0
  201. package/dist/types/entry-points/use-anchor-position.d.ts +1 -0
  202. package/dist/types/entry-points/use-arrow-navigation.d.ts +2 -0
  203. package/dist/types/entry-points/use-simple-light-dismiss.d.ts +2 -0
  204. package/dist/types/focus/focus.d.ts +42 -0
  205. package/dist/types/focus/index.d.ts +2 -0
  206. package/dist/types/index.d.ts +0 -0
  207. package/dist/types/internal/anchor-positioning-fallback.d.ts +24 -0
  208. package/dist/types/internal/combine.d.ts +14 -0
  209. package/dist/types/internal/ensure-preset-styles.d.ts +6 -0
  210. package/dist/types/internal/reduced-motion.d.ts +14 -0
  211. package/dist/types/internal/resolve-placement.d.ts +16 -0
  212. package/dist/types/internal/role-types.d.ts +62 -0
  213. package/dist/types/internal/set-style.d.ts +28 -0
  214. package/dist/types/internal/slot.d.ts +29 -0
  215. package/dist/types/internal/use-anchor-positioning.d.ts +95 -0
  216. package/dist/types/internal/use-animated-visibility.d.ts +74 -0
  217. package/dist/types/internal/use-animation-preset.d.ts +11 -0
  218. package/dist/types/internal/use-focus-wrap.d.ts +20 -0
  219. package/dist/types/internal/use-initial-focus.d.ts +21 -0
  220. package/dist/types/internal/use-preset-styles.d.ts +15 -0
  221. package/dist/types/placement-map/index.d.ts +55 -0
  222. package/dist/types/popover/create-close-event.d.ts +26 -0
  223. package/dist/types/popover/index.d.ts +3 -0
  224. package/dist/types/popover/popover.d.ts +22 -0
  225. package/dist/types/popover/types.d.ts +153 -0
  226. package/dist/types/popup/index.d.ts +34 -0
  227. package/dist/types/popup/popup-content.d.ts +14 -0
  228. package/dist/types/popup/popup-context.d.ts +96 -0
  229. package/dist/types/popup/popup-surface.d.ts +16 -0
  230. package/dist/types/popup/popup-trigger-function.d.ts +108 -0
  231. package/dist/types/popup/popup-trigger.d.ts +23 -0
  232. package/dist/types/popup/popup.d.ts +14 -0
  233. package/dist/types/popup/types.d.ts +289 -0
  234. package/dist/types/use-arrow-navigation/index.d.ts +2 -0
  235. package/dist/types/use-arrow-navigation/use-arrow-navigation.d.ts +136 -0
  236. package/dist/types/use-simple-light-dismiss/index.d.ts +45 -0
  237. package/dist/types-ts4.5/animations/presets.d.ts +94 -0
  238. package/dist/types-ts4.5/animations/types.d.ts +42 -0
  239. package/dist/types-ts4.5/arrow/index.d.ts +24 -0
  240. package/dist/types-ts4.5/arrow/types.d.ts +29 -0
  241. package/dist/types-ts4.5/dialog/create-close-event.d.ts +24 -0
  242. package/dist/types-ts4.5/dialog/dialog-content.d.ts +20 -0
  243. package/dist/types-ts4.5/dialog/index.d.ts +3 -0
  244. package/dist/types-ts4.5/dialog/types.d.ts +92 -0
  245. package/dist/types-ts4.5/dialog-scroll-lock/index.d.ts +24 -0
  246. package/dist/types-ts4.5/entry-points/animations.d.ts +2 -0
  247. package/dist/types-ts4.5/entry-points/arrow.d.ts +2 -0
  248. package/dist/types-ts4.5/entry-points/create-close-event.d.ts +4 -0
  249. package/dist/types-ts4.5/entry-points/dialog-scroll-lock.d.ts +1 -0
  250. package/dist/types-ts4.5/entry-points/dialog.d.ts +2 -0
  251. package/dist/types-ts4.5/entry-points/focus.d.ts +2 -0
  252. package/dist/types-ts4.5/entry-points/placement-map.d.ts +2 -0
  253. package/dist/types-ts4.5/entry-points/popover.d.ts +2 -0
  254. package/dist/types-ts4.5/entry-points/popup-surface.d.ts +2 -0
  255. package/dist/types-ts4.5/entry-points/popup.d.ts +2 -0
  256. package/dist/types-ts4.5/entry-points/use-anchor-position.d.ts +1 -0
  257. package/dist/types-ts4.5/entry-points/use-arrow-navigation.d.ts +2 -0
  258. package/dist/types-ts4.5/entry-points/use-simple-light-dismiss.d.ts +2 -0
  259. package/dist/types-ts4.5/focus/focus.d.ts +42 -0
  260. package/dist/types-ts4.5/focus/index.d.ts +2 -0
  261. package/dist/types-ts4.5/index.d.ts +0 -0
  262. package/dist/types-ts4.5/internal/anchor-positioning-fallback.d.ts +24 -0
  263. package/dist/types-ts4.5/internal/combine.d.ts +14 -0
  264. package/dist/types-ts4.5/internal/ensure-preset-styles.d.ts +6 -0
  265. package/dist/types-ts4.5/internal/reduced-motion.d.ts +14 -0
  266. package/dist/types-ts4.5/internal/resolve-placement.d.ts +16 -0
  267. package/dist/types-ts4.5/internal/role-types.d.ts +62 -0
  268. package/dist/types-ts4.5/internal/set-style.d.ts +28 -0
  269. package/dist/types-ts4.5/internal/slot.d.ts +29 -0
  270. package/dist/types-ts4.5/internal/use-anchor-positioning.d.ts +95 -0
  271. package/dist/types-ts4.5/internal/use-animated-visibility.d.ts +74 -0
  272. package/dist/types-ts4.5/internal/use-animation-preset.d.ts +11 -0
  273. package/dist/types-ts4.5/internal/use-focus-wrap.d.ts +20 -0
  274. package/dist/types-ts4.5/internal/use-initial-focus.d.ts +21 -0
  275. package/dist/types-ts4.5/internal/use-preset-styles.d.ts +15 -0
  276. package/dist/types-ts4.5/placement-map/index.d.ts +55 -0
  277. package/dist/types-ts4.5/popover/create-close-event.d.ts +26 -0
  278. package/dist/types-ts4.5/popover/index.d.ts +3 -0
  279. package/dist/types-ts4.5/popover/popover.d.ts +22 -0
  280. package/dist/types-ts4.5/popover/types.d.ts +153 -0
  281. package/dist/types-ts4.5/popup/index.d.ts +34 -0
  282. package/dist/types-ts4.5/popup/popup-content.d.ts +14 -0
  283. package/dist/types-ts4.5/popup/popup-context.d.ts +96 -0
  284. package/dist/types-ts4.5/popup/popup-surface.d.ts +16 -0
  285. package/dist/types-ts4.5/popup/popup-trigger-function.d.ts +108 -0
  286. package/dist/types-ts4.5/popup/popup-trigger.d.ts +23 -0
  287. package/dist/types-ts4.5/popup/popup.d.ts +14 -0
  288. package/dist/types-ts4.5/popup/types.d.ts +289 -0
  289. package/dist/types-ts4.5/use-arrow-navigation/index.d.ts +2 -0
  290. package/dist/types-ts4.5/use-arrow-navigation/use-arrow-navigation.d.ts +136 -0
  291. package/dist/types-ts4.5/use-simple-light-dismiss/index.d.ts +45 -0
  292. package/focus/package.json +17 -0
  293. package/package.json +95 -0
  294. package/placement-map/package.json +17 -0
  295. package/popover/package.json +17 -0
  296. package/popup/package.json +17 -0
  297. package/popup-surface/package.json +17 -0
  298. package/use-anchor-position/package.json +17 -0
  299. package/use-arrow-navigation/package.json +17 -0
  300. package/use-simple-light-dismiss/package.json +17 -0
@@ -0,0 +1,330 @@
1
+ import { useId, useLayoutEffect } from 'react';
2
+ import { bind } from 'bind-event-listener';
3
+ import rafSchedule from 'raf-schd';
4
+ import once from '@atlaskit/ds-lib/once';
5
+ import { computeFallbackPosition } from './anchor-positioning-fallback';
6
+ import { combine } from './combine';
7
+ import { getPlacement } from './resolve-placement';
8
+ import { setStyle } from './set-style';
9
+ export { getPlacement } from './resolve-placement';
10
+
11
+ /**
12
+ * Resolves placement options to CSS `position-area` syntax.
13
+ *
14
+ * The `align` field represents the *visual* alignment:
15
+ * - `align: 'start'` means "aligned to the trigger's start edge" (left in LTR for block axis)
16
+ * - `align: 'end'` means "aligned to the trigger's end edge"
17
+ * - `align: 'center'` means centered (single-axis position-area)
18
+ *
19
+ * CSS `position-area` uses `span-*` keywords which indicate the *expansion direction*.
20
+ * Visual start-alignment requires `span-{cross-axis}-end` (expand toward end = start-aligned).
21
+ * This function handles the inversion internally.
22
+ *
23
+ * @example
24
+ * placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'center' } }) // → 'block-end'
25
+ * placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'start' } }) // → 'block-end span-inline-end'
26
+ * placementToPositionArea({ placement: { axis: 'block', edge: 'end', align: 'end' } }) // → 'block-end span-inline-start'
27
+ */
28
+ export function placementToPositionArea({
29
+ placement
30
+ }) {
31
+ const {
32
+ axis,
33
+ edge,
34
+ align
35
+ } = getPlacement({
36
+ placement
37
+ });
38
+ const edgeValue = `${axis}-${edge}`;
39
+ if (align === 'center') {
40
+ return edgeValue;
41
+ }
42
+
43
+ // Cross-axis name: block ↔ inline
44
+ const crossAxis = axis === 'block' ? 'inline' : 'block';
45
+ // Invert align → CSS span direction: visual 'start' → span toward 'end'
46
+ const spanDir = align === 'start' ? 'end' : 'start';
47
+ return `${edgeValue} span-${crossAxis}-${spanDir}`;
48
+ }
49
+
50
+ /**
51
+ * Returns `position-try-fallbacks` value based on placement.
52
+ *
53
+ * For centered placements, includes cross-axis aligned position-area
54
+ * values so the browser can shift the popover along the cross-axis
55
+ * when the centered position would overflow the viewport edge.
56
+ *
57
+ * Fallback order for centered placements (e.g. `block-end`):
58
+ * 1. Same edge, start-aligned (shift for near-edge overflow)
59
+ * 2. Same edge, end-aligned (shift for far-edge overflow)
60
+ * 3. Flipped edge, centered
61
+ * 4. Flipped edge, start-aligned
62
+ * 5. Flipped edge, end-aligned
63
+ *
64
+ * For aligned placements (start/end), includes the same-edge centered
65
+ * position as fallbacks so the browser can try centering when the aligned
66
+ * position would overflow, before flipping to the opposite edge.
67
+ */
68
+ export function placementToTryFallbacks({
69
+ placement
70
+ }) {
71
+ const {
72
+ axis,
73
+ edge,
74
+ align
75
+ } = getPlacement({
76
+ placement
77
+ });
78
+ const crossAxis = axis === 'block' ? 'inline' : 'block';
79
+ const flippedEdge = edge === 'start' ? 'end' : 'start';
80
+ if (align !== 'center') {
81
+ const sameEdge = `${axis}-${edge}`;
82
+ const oppositeEdge = `${axis}-${flippedEdge}`;
83
+ const flipKeyword = axis === 'block' ? 'flip-block' : 'flip-inline';
84
+ // Legacy Popper fallbacks for e.g. bottom-start: [bottom, bottom-end, top-start, top, top-end, auto].
85
+ // Include same-edge centered so the browser can try center when start/end would overflow.
86
+ return [`${sameEdge} span-${crossAxis}-${align === 'start' ? 'end' : 'start'}`, sameEdge, `${sameEdge} span-${crossAxis}-${align === 'start' ? 'start' : 'end'}`, flipKeyword, `${oppositeEdge} span-${crossAxis}-${align === 'start' ? 'end' : 'start'}`, oppositeEdge, `${oppositeEdge} span-${crossAxis}-${align === 'start' ? 'start' : 'end'}`].join(', ');
87
+ }
88
+ const sameEdge = `${axis}-${edge}`;
89
+ const oppositeEdge = `${axis}-${flippedEdge}`;
90
+ const flipKeyword = axis === 'block' ? 'flip-block' : 'flip-inline';
91
+ return [`${sameEdge} span-${crossAxis}-end`, `${sameEdge} span-${crossAxis}-start`, flipKeyword, `${oppositeEdge} span-${crossAxis}-end`, `${oppositeEdge} span-${crossAxis}-start`].join(', ');
92
+ }
93
+
94
+ /**
95
+ * Detects whether the browser supports CSS Anchor Positioning.
96
+ * Uses `once()` for lazy evaluation — safe for SSR and avoids hydration mismatches.
97
+ */
98
+ const supportsAnchorPositioning = once(() => {
99
+ if (typeof window === 'undefined' || typeof CSS === 'undefined' || typeof CSS.supports !== 'function') {
100
+ return false;
101
+ }
102
+ return CSS.supports('anchor-name', '--a');
103
+ });
104
+
105
+ /**
106
+ * Default gap between the popover and its trigger (in pixels).
107
+ * Maps to `space.100` (8px) in the design token scale.
108
+ */
109
+ const DEFAULT_OFFSET = 8;
110
+
111
+ /**
112
+ * Returns the CSS margin declaration that creates a gap between the
113
+ * popover and its trigger on the side facing the anchor.
114
+ *
115
+ * For example, a `block-end` placement (popover below) gets
116
+ * `margin-block-start: 8px` to push it away from the trigger's bottom edge.
117
+ */
118
+ function edgeMargin({
119
+ placement,
120
+ offset
121
+ }) {
122
+ const {
123
+ axis,
124
+ edge
125
+ } = placement;
126
+ const value = `${offset}px`;
127
+ if (axis === 'block' && edge === 'end') {
128
+ return {
129
+ property: 'margin-block-start',
130
+ value
131
+ };
132
+ }
133
+ if (axis === 'block' && edge === 'start') {
134
+ return {
135
+ property: 'margin-block-end',
136
+ value
137
+ };
138
+ }
139
+ if (axis === 'inline' && edge === 'end') {
140
+ return {
141
+ property: 'margin-inline-start',
142
+ value
143
+ };
144
+ }
145
+ if (axis === 'inline' && edge === 'start') {
146
+ return {
147
+ property: 'margin-inline-end',
148
+ value
149
+ };
150
+ }
151
+ return {
152
+ property: 'margin-block-start',
153
+ value
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Hook that positions an element relative to an anchor element using
159
+ * CSS Anchor Positioning (with a JS fallback for older browsers).
160
+ *
161
+ * This hook is the positioning primitive — it has no knowledge of popovers,
162
+ * visibility, or animation. Compose it with `Popover` for anchor-positioned
163
+ * top-layer content.
164
+ *
165
+ * When CSS Anchor Positioning is supported, it sets CSS properties
166
+ * (`anchor-name`, `position-anchor`, `position-area`, `position-try-fallbacks`)
167
+ * directly on the elements via `el.style.setProperty()`.
168
+ *
169
+ * When not supported, it falls back to JavaScript-based positioning using
170
+ * `position: fixed` with measured coordinates, re-running on scroll (capture)
171
+ * and resize events.
172
+ */
173
+ export function useAnchorPosition({
174
+ anchorRef,
175
+ popoverRef,
176
+ placement,
177
+ offset = DEFAULT_OFFSET,
178
+ forceFallbackPositioning = false,
179
+ arrow
180
+ }) {
181
+ const id = useId();
182
+ const anchorName = `--anchor-${id.replace(/:/g, '')}`;
183
+ useLayoutEffect(() => {
184
+ const trigger = anchorRef.current;
185
+ const popover = popoverRef.current;
186
+ if (!trigger || !popover) {
187
+ return;
188
+ }
189
+ const resolvedPlacement = getPlacement({
190
+ placement
191
+ });
192
+ const shouldForceFallback = Boolean(forceFallbackPositioning);
193
+ if (supportsAnchorPositioning() && !shouldForceFallback) {
194
+ const gap = edgeMargin({
195
+ placement: resolvedPlacement,
196
+ offset
197
+ });
198
+ const popoverStyles = [{
199
+ property: 'position-anchor',
200
+ value: anchorName
201
+ }, {
202
+ property: 'position-area',
203
+ value: placementToPositionArea({
204
+ placement: resolvedPlacement
205
+ })
206
+ }, {
207
+ property: 'position-try-fallbacks',
208
+ value: arrow ? arrow.getTryFallbacks({
209
+ placement: resolvedPlacement
210
+ }) : placementToTryFallbacks({
211
+ placement: resolvedPlacement
212
+ })
213
+ },
214
+ // Reset browser default popover positioning that conflicts
215
+ // with anchor positioning (UA: `inset: 0; margin: auto;`)
216
+ {
217
+ property: 'margin',
218
+ value: '0'
219
+ }, {
220
+ property: 'inset',
221
+ value: 'auto'
222
+ },
223
+ // Gap between the popover and its trigger
224
+ {
225
+ property: gap.property,
226
+ value: gap.value
227
+ }];
228
+ if (arrow) {
229
+ // Set the arrow size variable used by the injected arrow CSS
230
+ // and the @position-try rules for margin values.
231
+ popoverStyles.push({
232
+ property: '--ds-arrow-size',
233
+ value: `${offset}px`
234
+ });
235
+ }
236
+ const undoPositioning = combine(setStyle({
237
+ el: trigger,
238
+ styles: [{
239
+ property: 'anchor-name',
240
+ value: anchorName
241
+ }]
242
+ }), setStyle({
243
+ el: popover,
244
+ styles: popoverStyles
245
+ }));
246
+ return undoPositioning;
247
+ }
248
+
249
+ // ── JS fallback ──
250
+ // The popover is already in the browser's top layer via popover="auto",
251
+ // so it does not need position:fixed. We only reset the UA defaults
252
+ // (inset: 0; margin: auto) and set top/left based on measurements.
253
+ const cleanupBaseStyles = setStyle({
254
+ el: popover,
255
+ styles: [{
256
+ property: 'margin',
257
+ value: '0'
258
+ }, {
259
+ property: 'inset',
260
+ value: 'auto'
261
+ }]
262
+ });
263
+ function update() {
264
+ if (!trigger || !popover) {
265
+ return;
266
+ }
267
+ const triggerRect = trigger.getBoundingClientRect();
268
+ const viewport = {
269
+ width: window.innerWidth,
270
+ height: window.innerHeight
271
+ };
272
+ const {
273
+ top,
274
+ left
275
+ } = computeFallbackPosition({
276
+ triggerRect,
277
+ popoverEl: popover,
278
+ placement: resolvedPlacement,
279
+ viewport,
280
+ offset
281
+ });
282
+ popover.style.setProperty('top', `${top}px`);
283
+ popover.style.setProperty('left', `${left}px`);
284
+ }
285
+
286
+ // Throttle scroll/resize updates to one per animation frame
287
+ const scheduledUpdate = rafSchedule(update);
288
+ let pendingFrame = null;
289
+ const undoPositioning = combine(cleanupBaseStyles,
290
+ // Position when the popover transitions to visible.
291
+ // The popover starts hidden (`display: none` per the popover
292
+ // UA stylesheet), so `offsetWidth`/`offsetHeight` are 0 until
293
+ // `showPopover()` is called. We defer positioning to the next
294
+ // animation frame because some browsers (Firefox) fire the
295
+ // `toggle` event synchronously during `showPopover()`, before
296
+ // the element has been laid out — so dimensions are still 0.
297
+ // Waiting one frame guarantees layout has run and measurements
298
+ // are accurate.
299
+ bind(popover, {
300
+ type: 'toggle',
301
+ listener: event => {
302
+ const toggleEvent = event;
303
+ if (toggleEvent.newState === 'open') {
304
+ pendingFrame = requestAnimationFrame(update);
305
+ }
306
+ }
307
+ }), bind(window, {
308
+ type: 'scroll',
309
+ listener: scheduledUpdate,
310
+ options: {
311
+ capture: true,
312
+ passive: true
313
+ }
314
+ }), bind(window, {
315
+ type: 'resize',
316
+ listener: scheduledUpdate,
317
+ options: {
318
+ passive: true
319
+ }
320
+ }), () => {
321
+ scheduledUpdate.cancel();
322
+ if (pendingFrame !== null) {
323
+ cancelAnimationFrame(pendingFrame);
324
+ }
325
+ popover.style.removeProperty('top');
326
+ popover.style.removeProperty('left');
327
+ });
328
+ return undoPositioning;
329
+ }, [anchorRef, popoverRef, placement, anchorName, offset, forceFallbackPositioning, arrow]);
330
+ }
@@ -0,0 +1,159 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { bind } from 'bind-event-listener';
3
+ import { prefersReducedMotion } from './reduced-motion';
4
+ import { usePresetStyles } from './use-preset-styles';
5
+ /**
6
+ * Manages the children mount/unmount lifecycle around CSS exit transitions.
7
+ *
8
+ * Used by both `Popover` and `Dialog` to share the same animation lifecycle
9
+ * logic. Those components own their own show/hide mechanisms (showPopover /
10
+ * hidePopover vs showModal / close) and event handling — this hook only
11
+ * manages the relationship between `isOpen` and when children are rendered.
12
+ *
13
+ * ## Problem
14
+ *
15
+ * We want to delay the unmount of children until a CSS exit transition has
16
+ * finished playing. If we unmount children the moment `isOpen` becomes
17
+ * `false`, the exit animation is never visible — the content just disappears.
18
+ *
19
+ * ## How it works
20
+ *
21
+ * `showChildren` tracks whether the consumer's children should be in the DOM.
22
+ * It does not mirror `isOpen` exactly — it intentionally lags behind during
23
+ * exit animations:
24
+ *
25
+ * ```
26
+ * isOpen: true ──────────────── false
27
+ * showChildren: true ──────────────── true ─── (exit animation) ─── false
28
+ * ```
29
+ *
30
+ * Two close paths exist:
31
+ *
32
+ * 1. **Animated close** (`willAnimate === true`):
33
+ * `isOpen` goes `false` → `showChildren` stays `true` → the CSS exit
34
+ * transition plays → `transitionend` fires → `showChildren` → `false`
35
+ * → `onExitFinish` fires. A timeout fallback ensures we still unmount
36
+ * if `transitionend` never fires (e.g. browser quirks, zero-duration).
37
+ *
38
+ * 2. **Non-animated close** (`willAnimate === false`):
39
+ * `isOpen` goes `false` → `showChildren` → `false` synchronously during
40
+ * the same render → `onExitFinish` fires in a follow-up effect.
41
+ */
42
+ export function useAnimatedVisibility({
43
+ isOpen,
44
+ animate,
45
+ elementRef,
46
+ onExitFinish
47
+ }) {
48
+ // Resolve the animation preset and inject its CSS (idempotent).
49
+ // Returns `null` when `animate` is falsy.
50
+ const preset = usePresetStyles({
51
+ preset: animate
52
+ });
53
+
54
+ // Whether we should play CSS transitions. False when there is no preset
55
+ // or the user has enabled the "prefers reduced motion" OS setting.
56
+ const willAnimate = Boolean(animate) && !prefersReducedMotion();
57
+
58
+ // ── showChildren state ──
59
+ //
60
+ // This is the core of the hook. `showChildren` controls whether the
61
+ // consumer's children are in the DOM. The two `if` blocks below are
62
+ // intentionally written as synchronous state updates during render
63
+ // (React supports this pattern — setState during render is treated as
64
+ // a synchronous re-render before the browser paints).
65
+ //
66
+ // Mount path: isOpen went true but showChildren is still false → mount.
67
+ // Unmount path: isOpen went false, showChildren is still true, and we
68
+ // are NOT animating → unmount immediately (no transition
69
+ // to wait for). If we ARE animating, showChildren stays
70
+ // true here and the effect below will set it to false
71
+ // after `transitionend`.
72
+ const [showChildren, setShowChildren] = useState(isOpen);
73
+ if (isOpen && !showChildren) {
74
+ setShowChildren(true);
75
+ }
76
+ if (!isOpen && showChildren && !willAnimate) {
77
+ setShowChildren(false);
78
+ }
79
+
80
+ // Keep a stable ref to the latest `onExitFinish` so the effect and
81
+ // timeout closures below always call the current callback without
82
+ // needing `onExitFinish` in dependency arrays (which would reset
83
+ // the timeout on every new closure).
84
+ const onExitFinishRef = useRef(onExitFinish);
85
+ onExitFinishRef.current = onExitFinish;
86
+
87
+ // ── Animated exit effect ──
88
+ //
89
+ // Runs when all three conditions are true:
90
+ // • `isOpen` is false (consumer wants to close)
91
+ // • `showChildren` is true (children are still mounted for exit anim)
92
+ // • `willAnimate` is true (we have a preset and reduced motion is off)
93
+ //
94
+ // Listens for `transitionend` on the element to know when the CSS exit
95
+ // transition has finished, then unmounts children and fires onExitFinish.
96
+ //
97
+ // Fallback timeout: `transitionend` can fail to fire in edge cases
98
+ // (e.g. element removed from top layer by browser, zero-duration
99
+ // transition, interrupted animation). The timeout is set to the
100
+ // preset's exit duration + 50ms buffer to guarantee we always unmount.
101
+ useEffect(() => {
102
+ var _preset$exitDurationM;
103
+ // Guard: only run during the "exiting" window
104
+ if (isOpen || !showChildren || !willAnimate) {
105
+ return;
106
+ }
107
+ const el = elementRef.current;
108
+ if (!el) {
109
+ return;
110
+ }
111
+ function finish() {
112
+ var _onExitFinishRef$curr;
113
+ clearTimeout(fallbackId);
114
+ setShowChildren(false);
115
+ (_onExitFinishRef$curr = onExitFinishRef.current) === null || _onExitFinishRef$curr === void 0 ? void 0 : _onExitFinishRef$curr.call(onExitFinishRef);
116
+ }
117
+
118
+ // Fallback: unmount even if transitionend never fires
119
+ const fallbackId = setTimeout(finish, ((_preset$exitDurationM = preset === null || preset === void 0 ? void 0 : preset.exitDurationMs) !== null && _preset$exitDurationM !== void 0 ? _preset$exitDurationM : 0) + 50);
120
+
121
+ // Primary: listen for the CSS transition to complete
122
+ const unbind = bind(el, {
123
+ type: 'transitionend',
124
+ listener: finish,
125
+ options: {
126
+ once: true
127
+ }
128
+ });
129
+
130
+ // Cleanup on re-render or unmount: cancel pending timeout and listener
131
+ return () => {
132
+ clearTimeout(fallbackId);
133
+ unbind();
134
+ };
135
+ }, [isOpen, showChildren, willAnimate, preset === null || preset === void 0 ? void 0 : preset.exitDurationMs, elementRef]);
136
+
137
+ // ── Non-animated exit callback ──
138
+ //
139
+ // When there is no animation, children unmount synchronously (handled
140
+ // by the `if` block above). But `onExitFinish` still needs to fire —
141
+ // we do that in a follow-up effect so the consumer can react to it.
142
+ //
143
+ // Uses a previous-value ref (`wasOpenRef`) to detect the exact
144
+ // true→false transition of `isOpen`, avoiding false positives on
145
+ // mount or re-renders where `isOpen` was already false.
146
+ const wasOpenRef = useRef(isOpen);
147
+ useEffect(() => {
148
+ const wasOpen = wasOpenRef.current;
149
+ wasOpenRef.current = isOpen;
150
+ if (wasOpen && !isOpen && !willAnimate) {
151
+ var _onExitFinishRef$curr2;
152
+ (_onExitFinishRef$curr2 = onExitFinishRef.current) === null || _onExitFinishRef$curr2 === void 0 ? void 0 : _onExitFinishRef$curr2.call(onExitFinishRef);
153
+ }
154
+ }, [isOpen, willAnimate]);
155
+ return {
156
+ showChildren,
157
+ preset
158
+ };
159
+ }
@@ -0,0 +1,18 @@
1
+ import { useInsertionEffect } from 'react';
2
+ import { ensurePresetStyles } from './ensure-preset-styles';
3
+ /**
4
+ * Normalizes the `animate` prop to a preset (or null) and ensures its CSS
5
+ * is injected once into document.head. Used by PopupContent, Popover, and
6
+ * DialogContent so they don't duplicate this logic.
7
+ */
8
+ export function useAnimationPreset(animate) {
9
+ const preset = animate != null && animate !== false ? animate : null;
10
+ useInsertionEffect(() => {
11
+ if (preset) {
12
+ ensurePresetStyles({
13
+ preset
14
+ });
15
+ }
16
+ }, [preset]);
17
+ return preset;
18
+ }
@@ -0,0 +1,90 @@
1
+ import { useEffect } from 'react';
2
+ import { bind } from 'bind-event-listener';
3
+ import { getFirstFocusable, getLastFocusable, getNextFocusable } from '../focus/focus';
4
+
5
+ /**
6
+ * Roles that require focus wrapping per WAI-ARIA APG.
7
+ *
8
+ * - `dialog`: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
9
+ * - `alertdialog`: same keyboard pattern as dialog
10
+ *
11
+ * `menu` is intentionally excluded. Menus are composite widgets
12
+ * (https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_general_within)
13
+ * that manage their own internal keyboard navigation via arrow keys,
14
+ * not Tab. Tab/Shift+Tab should move focus OUT of a menu entirely.
15
+ *
16
+ * Menu keyboard behavior (arrow keys, Home/End, type-ahead, Enter/Space)
17
+ * is context-dependent — it varies by orientation, nesting depth, item
18
+ * type, and control pattern (menu button vs menubar). This makes it
19
+ * impossible to implement generically here. The consumer component
20
+ * (e.g. `dropdown-menu`) owns all menu keyboard navigation.
21
+ *
22
+ * See: notes/outputs/menu-keyboard-decision.md
23
+ */
24
+ function roleRequiresFocusWrap(role) {
25
+ return role === 'dialog' || role === 'alertdialog';
26
+ }
27
+
28
+ /**
29
+ * Wraps Tab/Shift+Tab focus within a popover element when its role
30
+ * semantically requires it (dialog, alertdialog).
31
+ *
32
+ * While the popover is open, Tab and Shift+Tab are intercepted
33
+ * (via `preventDefault`) and remapped to cycle through focusable
34
+ * elements within the container using wrapping navigation.
35
+ *
36
+ * Light dismiss (Escape, click outside) continues to work natively
37
+ * via `popover="auto"` — this hook only intercepts Tab.
38
+ *
39
+ * Always call this hook unconditionally. The listener is only
40
+ * attached when the role requires focus wrapping, and is cleaned
41
+ * up and re-evaluated when the role changes.
42
+ */
43
+ export function useFocusWrap({
44
+ elementRef,
45
+ role
46
+ }) {
47
+ useEffect(() => {
48
+ const el = elementRef.current;
49
+ if (!el || !roleRequiresFocusWrap(role)) {
50
+ return;
51
+ }
52
+ const unbind = bind(el, {
53
+ type: 'keydown',
54
+ listener: event => {
55
+ if (event.key !== 'Tab') {
56
+ return;
57
+ }
58
+ event.preventDefault();
59
+ const direction = event.shiftKey ? 'backwards' : 'forwards';
60
+
61
+ // Try to move to the next/previous focusable element relative
62
+ // to the currently focused element within the container.
63
+ const next = getNextFocusable({
64
+ container: el,
65
+ direction
66
+ });
67
+ if (next) {
68
+ next.focus();
69
+ return;
70
+ }
71
+
72
+ // Nothing is currently focused inside the container (or the
73
+ // focused element isn't in the focusable list). Fall back to
74
+ // the first or last focusable element depending on direction.
75
+ const fallback = direction === 'forwards' ? getFirstFocusable({
76
+ container: el
77
+ }) : getLastFocusable({
78
+ container: el
79
+ });
80
+ if (fallback) {
81
+ fallback.focus();
82
+ }
83
+ },
84
+ options: {
85
+ capture: true
86
+ }
87
+ });
88
+ return unbind;
89
+ }, [elementRef, role]);
90
+ }