@wordpress/ui 0.13.1-next.v.202605131006.0 → 0.14.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 (282) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/CONTRIBUTING.md +34 -0
  3. package/README.md +15 -0
  4. package/build/alert-dialog/portal.cjs.map +2 -2
  5. package/build/alert-dialog/types.cjs.map +1 -1
  6. package/build/button/button.cjs +1 -1
  7. package/build/button/button.cjs.map +2 -2
  8. package/build/card/content.cjs +1 -1
  9. package/build/card/content.cjs.map +2 -2
  10. package/build/card/full-bleed.cjs +1 -1
  11. package/build/card/full-bleed.cjs.map +2 -2
  12. package/build/card/header.cjs +1 -1
  13. package/build/card/header.cjs.map +2 -2
  14. package/build/card/root.cjs +1 -1
  15. package/build/card/root.cjs.map +2 -2
  16. package/build/collapsible-card/header.cjs.map +2 -2
  17. package/build/dialog/portal.cjs.map +2 -2
  18. package/build/dialog/types.cjs.map +1 -1
  19. package/build/drawer/portal.cjs.map +2 -2
  20. package/build/drawer/types.cjs.map +1 -1
  21. package/build/form/primitives/autocomplete/clear.cjs +4 -1
  22. package/build/form/primitives/autocomplete/clear.cjs.map +2 -2
  23. package/build/form/primitives/autocomplete/empty.cjs +1 -1
  24. package/build/form/primitives/autocomplete/empty.cjs.map +2 -2
  25. package/build/form/primitives/autocomplete/index.cjs +4 -1
  26. package/build/form/primitives/autocomplete/index.cjs.map +2 -2
  27. package/build/form/primitives/autocomplete/item.cjs +1 -1
  28. package/build/form/primitives/autocomplete/item.cjs.map +2 -2
  29. package/build/form/primitives/autocomplete/list-body.cjs +1 -1
  30. package/build/form/primitives/autocomplete/list-body.cjs.map +2 -2
  31. package/build/form/primitives/autocomplete/list.cjs +1 -1
  32. package/build/form/primitives/autocomplete/list.cjs.map +2 -2
  33. package/build/form/primitives/autocomplete/popup.cjs +14 -31
  34. package/build/form/primitives/autocomplete/popup.cjs.map +3 -3
  35. package/build/form/primitives/autocomplete/portal.cjs +10 -2
  36. package/build/form/primitives/autocomplete/portal.cjs.map +2 -2
  37. package/build/form/primitives/autocomplete/positioner.cjs +158 -0
  38. package/build/form/primitives/autocomplete/positioner.cjs.map +7 -0
  39. package/build/form/primitives/autocomplete/types.cjs.map +1 -1
  40. package/build/form/primitives/constants.cjs.map +2 -2
  41. package/build/form/primitives/select/index.cjs +4 -1
  42. package/build/form/primitives/select/index.cjs.map +2 -2
  43. package/build/form/primitives/select/item.cjs +1 -1
  44. package/build/form/primitives/select/item.cjs.map +2 -2
  45. package/build/form/primitives/select/popup.cjs +18 -36
  46. package/build/form/primitives/select/popup.cjs.map +3 -3
  47. package/build/form/primitives/select/portal.cjs +11 -5
  48. package/build/form/primitives/select/portal.cjs.map +2 -2
  49. package/build/form/primitives/select/positioner.cjs +159 -0
  50. package/build/form/primitives/select/positioner.cjs.map +7 -0
  51. package/build/form/primitives/select/types.cjs.map +1 -1
  52. package/build/icon-button/icon-button.cjs +1 -1
  53. package/build/icon-button/icon-button.cjs.map +2 -2
  54. package/build/index.cjs +7 -1
  55. package/build/index.cjs.map +2 -2
  56. package/build/popover/index.cjs +3 -0
  57. package/build/popover/index.cjs.map +2 -2
  58. package/build/popover/popup.cjs +23 -51
  59. package/build/popover/popup.cjs.map +3 -3
  60. package/build/popover/portal.cjs.map +2 -2
  61. package/build/popover/positioner.cjs +168 -0
  62. package/build/popover/positioner.cjs.map +7 -0
  63. package/build/popover/root.cjs.map +2 -2
  64. package/build/popover/types.cjs.map +1 -1
  65. package/build/tooltip/portal.cjs +10 -2
  66. package/build/tooltip/portal.cjs.map +2 -2
  67. package/build/tooltip/positioner.cjs.map +2 -2
  68. package/build/tooltip/types.cjs.map +1 -1
  69. package/build/utils/create-overlay-title-validation.cjs.map +2 -2
  70. package/build/utils/render-slot-with-children.cjs +4 -1
  71. package/build/utils/render-slot-with-children.cjs.map +2 -2
  72. package/build/utils/use-enable-wp-compat-overlay-slot.cjs +39 -0
  73. package/build/utils/use-enable-wp-compat-overlay-slot.cjs.map +7 -0
  74. package/build/utils/wp-compat-overlay-slot.cjs +177 -0
  75. package/build/utils/wp-compat-overlay-slot.cjs.map +7 -0
  76. package/build-module/alert-dialog/portal.mjs.map +2 -2
  77. package/build-module/button/button.mjs +1 -1
  78. package/build-module/button/button.mjs.map +2 -2
  79. package/build-module/card/content.mjs +1 -1
  80. package/build-module/card/content.mjs.map +2 -2
  81. package/build-module/card/full-bleed.mjs +1 -1
  82. package/build-module/card/full-bleed.mjs.map +2 -2
  83. package/build-module/card/header.mjs +1 -1
  84. package/build-module/card/header.mjs.map +2 -2
  85. package/build-module/card/root.mjs +1 -1
  86. package/build-module/card/root.mjs.map +2 -2
  87. package/build-module/collapsible-card/header.mjs.map +2 -2
  88. package/build-module/dialog/portal.mjs.map +2 -2
  89. package/build-module/drawer/portal.mjs.map +2 -2
  90. package/build-module/form/primitives/autocomplete/clear.mjs +4 -1
  91. package/build-module/form/primitives/autocomplete/clear.mjs.map +2 -2
  92. package/build-module/form/primitives/autocomplete/empty.mjs +1 -1
  93. package/build-module/form/primitives/autocomplete/empty.mjs.map +2 -2
  94. package/build-module/form/primitives/autocomplete/index.mjs +3 -1
  95. package/build-module/form/primitives/autocomplete/index.mjs.map +2 -2
  96. package/build-module/form/primitives/autocomplete/item.mjs +1 -1
  97. package/build-module/form/primitives/autocomplete/item.mjs.map +2 -2
  98. package/build-module/form/primitives/autocomplete/list-body.mjs +1 -1
  99. package/build-module/form/primitives/autocomplete/list-body.mjs.map +2 -2
  100. package/build-module/form/primitives/autocomplete/list.mjs +1 -1
  101. package/build-module/form/primitives/autocomplete/list.mjs.map +2 -2
  102. package/build-module/form/primitives/autocomplete/popup.mjs +14 -31
  103. package/build-module/form/primitives/autocomplete/popup.mjs.map +3 -3
  104. package/build-module/form/primitives/autocomplete/portal.mjs +10 -2
  105. package/build-module/form/primitives/autocomplete/portal.mjs.map +2 -2
  106. package/build-module/form/primitives/autocomplete/positioner.mjs +123 -0
  107. package/build-module/form/primitives/autocomplete/positioner.mjs.map +7 -0
  108. package/build-module/form/primitives/constants.mjs.map +2 -2
  109. package/build-module/form/primitives/select/index.mjs +3 -1
  110. package/build-module/form/primitives/select/index.mjs.map +2 -2
  111. package/build-module/form/primitives/select/item.mjs +1 -1
  112. package/build-module/form/primitives/select/item.mjs.map +2 -2
  113. package/build-module/form/primitives/select/popup.mjs +18 -36
  114. package/build-module/form/primitives/select/popup.mjs.map +3 -3
  115. package/build-module/form/primitives/select/portal.mjs +11 -5
  116. package/build-module/form/primitives/select/portal.mjs.map +2 -2
  117. package/build-module/form/primitives/select/positioner.mjs +124 -0
  118. package/build-module/form/primitives/select/positioner.mjs.map +7 -0
  119. package/build-module/icon-button/icon-button.mjs +1 -1
  120. package/build-module/icon-button/icon-button.mjs.map +2 -2
  121. package/build-module/index.mjs +5 -1
  122. package/build-module/index.mjs.map +2 -2
  123. package/build-module/popover/index.mjs +2 -0
  124. package/build-module/popover/index.mjs.map +2 -2
  125. package/build-module/popover/popup.mjs +23 -51
  126. package/build-module/popover/popup.mjs.map +3 -3
  127. package/build-module/popover/portal.mjs.map +2 -2
  128. package/build-module/popover/positioner.mjs +133 -0
  129. package/build-module/popover/positioner.mjs.map +7 -0
  130. package/build-module/popover/root.mjs.map +2 -2
  131. package/build-module/tooltip/portal.mjs +10 -2
  132. package/build-module/tooltip/portal.mjs.map +2 -2
  133. package/build-module/tooltip/positioner.mjs.map +2 -2
  134. package/build-module/utils/create-overlay-title-validation.mjs.map +2 -2
  135. package/build-module/utils/render-slot-with-children.mjs +4 -1
  136. package/build-module/utils/render-slot-with-children.mjs.map +2 -2
  137. package/build-module/utils/use-enable-wp-compat-overlay-slot.mjs +14 -0
  138. package/build-module/utils/use-enable-wp-compat-overlay-slot.mjs.map +7 -0
  139. package/build-module/utils/wp-compat-overlay-slot.mjs +148 -0
  140. package/build-module/utils/wp-compat-overlay-slot.mjs.map +7 -0
  141. package/build-types/alert-dialog/portal.d.ts +8 -5
  142. package/build-types/alert-dialog/portal.d.ts.map +1 -1
  143. package/build-types/alert-dialog/types.d.ts +2 -2
  144. package/build-types/alert-dialog/types.d.ts.map +1 -1
  145. package/build-types/badge/stories/e2e/index.story.d.ts +7 -0
  146. package/build-types/badge/stories/e2e/index.story.d.ts.map +1 -0
  147. package/build-types/button/stories/e2e/index.story.d.ts +8 -0
  148. package/build-types/button/stories/e2e/index.story.d.ts.map +1 -0
  149. package/build-types/card/full-bleed.d.ts +18 -0
  150. package/build-types/card/full-bleed.d.ts.map +1 -1
  151. package/build-types/card/stories/index.story.d.ts +23 -0
  152. package/build-types/card/stories/index.story.d.ts.map +1 -1
  153. package/build-types/collapsible-card/header.d.ts +2 -0
  154. package/build-types/collapsible-card/header.d.ts.map +1 -1
  155. package/build-types/collapsible-card/stories/index.story.d.ts +14 -0
  156. package/build-types/collapsible-card/stories/index.story.d.ts.map +1 -1
  157. package/build-types/dialog/portal.d.ts +8 -6
  158. package/build-types/dialog/portal.d.ts.map +1 -1
  159. package/build-types/dialog/types.d.ts +2 -2
  160. package/build-types/dialog/types.d.ts.map +1 -1
  161. package/build-types/drawer/portal.d.ts +8 -6
  162. package/build-types/drawer/portal.d.ts.map +1 -1
  163. package/build-types/drawer/types.d.ts +2 -2
  164. package/build-types/drawer/types.d.ts.map +1 -1
  165. package/build-types/form/primitives/autocomplete/clear.d.ts.map +1 -1
  166. package/build-types/form/primitives/autocomplete/index.d.ts +2 -1
  167. package/build-types/form/primitives/autocomplete/index.d.ts.map +1 -1
  168. package/build-types/form/primitives/autocomplete/popup.d.ts +1 -0
  169. package/build-types/form/primitives/autocomplete/popup.d.ts.map +1 -1
  170. package/build-types/form/primitives/autocomplete/portal.d.ts +9 -4
  171. package/build-types/form/primitives/autocomplete/portal.d.ts.map +1 -1
  172. package/build-types/form/primitives/autocomplete/positioner.d.ts +12 -0
  173. package/build-types/form/primitives/autocomplete/positioner.d.ts.map +1 -0
  174. package/build-types/form/primitives/autocomplete/stories/index.story.d.ts.map +1 -1
  175. package/build-types/form/primitives/autocomplete/types.d.ts +11 -9
  176. package/build-types/form/primitives/autocomplete/types.d.ts.map +1 -1
  177. package/build-types/form/primitives/constants.d.ts +9 -4
  178. package/build-types/form/primitives/constants.d.ts.map +1 -1
  179. package/build-types/form/primitives/select/index.d.ts +2 -1
  180. package/build-types/form/primitives/select/index.d.ts.map +1 -1
  181. package/build-types/form/primitives/select/popup.d.ts +1 -0
  182. package/build-types/form/primitives/select/popup.d.ts.map +1 -1
  183. package/build-types/form/primitives/select/portal.d.ts +9 -4
  184. package/build-types/form/primitives/select/portal.d.ts.map +1 -1
  185. package/build-types/form/primitives/select/positioner.d.ts +12 -0
  186. package/build-types/form/primitives/select/positioner.d.ts.map +1 -0
  187. package/build-types/form/primitives/select/stories/index.story.d.ts.map +1 -1
  188. package/build-types/form/primitives/select/types.d.ts +11 -2
  189. package/build-types/form/primitives/select/types.d.ts.map +1 -1
  190. package/build-types/icon/stories/index.story.d.ts.map +1 -1
  191. package/build-types/index.d.ts +2 -0
  192. package/build-types/index.d.ts.map +1 -1
  193. package/build-types/popover/index.d.ts +2 -1
  194. package/build-types/popover/index.d.ts.map +1 -1
  195. package/build-types/popover/popup.d.ts +2 -1
  196. package/build-types/popover/popup.d.ts.map +1 -1
  197. package/build-types/popover/portal.d.ts +8 -5
  198. package/build-types/popover/portal.d.ts.map +1 -1
  199. package/build-types/popover/positioner.d.ts +12 -0
  200. package/build-types/popover/positioner.d.ts.map +1 -0
  201. package/build-types/popover/root.d.ts +5 -2
  202. package/build-types/popover/root.d.ts.map +1 -1
  203. package/build-types/popover/stories/index.story.d.ts +10 -21
  204. package/build-types/popover/stories/index.story.d.ts.map +1 -1
  205. package/build-types/popover/types.d.ts +12 -3
  206. package/build-types/popover/types.d.ts.map +1 -1
  207. package/build-types/tooltip/portal.d.ts +9 -4
  208. package/build-types/tooltip/portal.d.ts.map +1 -1
  209. package/build-types/tooltip/positioner.d.ts +8 -5
  210. package/build-types/tooltip/positioner.d.ts.map +1 -1
  211. package/build-types/tooltip/types.d.ts +3 -3
  212. package/build-types/tooltip/types.d.ts.map +1 -1
  213. package/build-types/utils/render-slot-with-children.d.ts.map +1 -1
  214. package/build-types/utils/test/use-enable-wp-compat-overlay-slot.test.d.ts +2 -0
  215. package/build-types/utils/test/use-enable-wp-compat-overlay-slot.test.d.ts.map +1 -0
  216. package/build-types/utils/test/wp-compat-overlay-slot.test.d.ts +2 -0
  217. package/build-types/utils/test/wp-compat-overlay-slot.test.d.ts.map +1 -0
  218. package/build-types/utils/use-deprioritized-initial-focus.d.ts +1 -1
  219. package/build-types/utils/use-enable-wp-compat-overlay-slot.d.ts +17 -0
  220. package/build-types/utils/use-enable-wp-compat-overlay-slot.d.ts.map +1 -0
  221. package/build-types/utils/wp-compat-overlay-slot.d.ts +30 -0
  222. package/build-types/utils/wp-compat-overlay-slot.d.ts.map +1 -0
  223. package/package.json +14 -14
  224. package/src/alert-dialog/portal.tsx +1 -4
  225. package/src/alert-dialog/types.ts +2 -4
  226. package/src/badge/stories/e2e/index.story.tsx +20 -0
  227. package/src/button/stories/e2e/index.story.tsx +130 -0
  228. package/src/button/stories/index.story.tsx +1 -1
  229. package/src/button/style.module.css +17 -12
  230. package/src/card/full-bleed.tsx +18 -0
  231. package/src/card/stories/index.story.tsx +115 -1
  232. package/src/card/style.module.css +16 -0
  233. package/src/card/test/index.test.tsx +18 -1
  234. package/src/collapsible-card/header.tsx +2 -0
  235. package/src/collapsible-card/stories/index.story.tsx +66 -0
  236. package/src/dialog/portal.tsx +1 -5
  237. package/src/dialog/types.ts +2 -2
  238. package/src/drawer/portal.tsx +1 -5
  239. package/src/drawer/types.ts +2 -2
  240. package/src/form/primitives/autocomplete/clear.tsx +10 -4
  241. package/src/form/primitives/autocomplete/index.ts +2 -1
  242. package/src/form/primitives/autocomplete/popup.tsx +17 -21
  243. package/src/form/primitives/autocomplete/portal.tsx +11 -5
  244. package/src/form/primitives/autocomplete/positioner.tsx +29 -0
  245. package/src/form/primitives/autocomplete/stories/index.story.tsx +1 -0
  246. package/src/form/primitives/autocomplete/test/index.test.tsx +219 -0
  247. package/src/form/primitives/autocomplete/types.ts +15 -15
  248. package/src/form/primitives/constants.ts +7 -4
  249. package/src/form/primitives/select/index.ts +2 -1
  250. package/src/form/primitives/select/popup.tsx +30 -34
  251. package/src/form/primitives/select/portal.tsx +15 -8
  252. package/src/form/primitives/select/positioner.tsx +33 -0
  253. package/src/form/primitives/select/stories/index.story.tsx +1 -0
  254. package/src/form/primitives/select/test/index.test.tsx +134 -0
  255. package/src/form/primitives/select/types.ts +12 -2
  256. package/src/form/select-control/test/index.test.tsx +64 -0
  257. package/src/icon/stories/index.story.tsx +3 -2
  258. package/src/icon-button/icon-button.tsx +1 -1
  259. package/src/icon-button/test/index.test.tsx +10 -10
  260. package/src/index.ts +2 -0
  261. package/src/popover/index.ts +12 -1
  262. package/src/popover/popup.tsx +28 -45
  263. package/src/popover/portal.tsx +1 -4
  264. package/src/popover/positioner.tsx +42 -0
  265. package/src/popover/root.tsx +5 -2
  266. package/src/popover/stories/index.story.tsx +85 -138
  267. package/src/popover/test/index.test.tsx +36 -1
  268. package/src/popover/types.ts +13 -15
  269. package/src/tabs/stories/index.story.tsx +2 -2
  270. package/src/tooltip/portal.tsx +11 -5
  271. package/src/tooltip/positioner.tsx +1 -4
  272. package/src/tooltip/style.module.css +1 -1
  273. package/src/tooltip/test/index.test.tsx +110 -0
  274. package/src/tooltip/types.ts +3 -5
  275. package/src/utils/create-overlay-title-validation.tsx +1 -1
  276. package/src/utils/css/item-popup.module.css +7 -4
  277. package/src/utils/css/wp-compat-overlay-slot.module.css +35 -0
  278. package/src/utils/render-slot-with-children.ts +4 -1
  279. package/src/utils/test/use-enable-wp-compat-overlay-slot.test.tsx +74 -0
  280. package/src/utils/test/wp-compat-overlay-slot.test.ts +300 -0
  281. package/src/utils/use-enable-wp-compat-overlay-slot.ts +32 -0
  282. package/src/utils/wp-compat-overlay-slot.ts +129 -0
@@ -1,6 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import { useId, useRef, useState } from '@wordpress/element';
3
- import { SlotFillProvider, Slot } from '@wordpress/components';
4
3
  import { close, info } from '@wordpress/icons';
5
4
  import * as Popover from '../';
6
5
  import { VisuallyHidden } from '../../visually-hidden';
@@ -14,6 +13,7 @@ const meta: Meta< typeof Popover.Root > = {
14
13
  subcomponents: {
15
14
  'Popover.Trigger': Popover.Trigger,
16
15
  'Popover.Portal': Popover.Portal,
16
+ 'Popover.Positioner': Popover.Positioner,
17
17
  'Popover.Popup': Popover.Popup,
18
18
  'Popover.Arrow': Popover.Arrow,
19
19
  'Popover.Title': Popover.Title,
@@ -88,7 +88,7 @@ export const NoArrow: Story = {
88
88
  };
89
89
 
90
90
  /**
91
- * All combinations of `side` and `align` props on `Popover.Popup`.
91
+ * All combinations of `side` and `align` props on `Popover.Positioner`.
92
92
  *
93
93
  * Each row shows a side (`top`, `right`, `bottom`, `left`), and each column
94
94
  * shows an alignment (`start`, `center`, `end`).
@@ -116,12 +116,16 @@ export const Positioning: Story = {
116
116
  { side } / { align }
117
117
  </Popover.Trigger>
118
118
  <Popover.Popup
119
- side={ side }
120
- align={ align }
121
- collisionAvoidance={ {
122
- side: 'none',
123
- align: 'none',
124
- } }
119
+ positioner={
120
+ <Popover.Positioner
121
+ side={ side }
122
+ align={ align }
123
+ collisionAvoidance={ {
124
+ side: 'none',
125
+ align: 'none',
126
+ } }
127
+ />
128
+ }
125
129
  >
126
130
  <VisuallyHidden render={ <Popover.Title /> }>
127
131
  { side } / { align }
@@ -416,16 +420,21 @@ export const OverlayPlacement: Story = {
416
420
  </Popover.Trigger>
417
421
  <Popover.Popup
418
422
  ref={ popupRef }
419
- side="bottom"
420
- align="center"
421
- sideOffset={
422
- -1 *
423
- ( popupSize.height / 2 + triggerSize.height / 2 )
423
+ positioner={
424
+ <Popover.Positioner
425
+ side="bottom"
426
+ align="center"
427
+ sideOffset={
428
+ -1 *
429
+ ( popupSize.height / 2 +
430
+ triggerSize.height / 2 )
431
+ }
432
+ collisionAvoidance={ {
433
+ side: 'none',
434
+ align: 'none',
435
+ } }
436
+ />
424
437
  }
425
- collisionAvoidance={ {
426
- side: 'none',
427
- align: 'none',
428
- } }
429
438
  >
430
439
  <Popover.Title
431
440
  style={ {
@@ -538,8 +547,12 @@ export const CollisionAvoidance: Story = {
538
547
  <Popover.Root defaultOpen>
539
548
  <Popover.Trigger>Flip (default)</Popover.Trigger>
540
549
  <Popover.Popup
541
- side="top"
542
- collisionBoundary={ boundary ?? undefined }
550
+ positioner={
551
+ <Popover.Positioner
552
+ side="top"
553
+ collisionBoundary={ boundary ?? undefined }
554
+ />
555
+ }
543
556
  >
544
557
  <Popover.Title
545
558
  style={ {
@@ -558,12 +571,16 @@ export const CollisionAvoidance: Story = {
558
571
  <Popover.Root defaultOpen>
559
572
  <Popover.Trigger>No collision</Popover.Trigger>
560
573
  <Popover.Popup
561
- side="top"
562
- collisionBoundary={ boundary ?? undefined }
563
- collisionAvoidance={ {
564
- side: 'none',
565
- align: 'none',
566
- } }
574
+ positioner={
575
+ <Popover.Positioner
576
+ side="top"
577
+ collisionBoundary={ boundary ?? undefined }
578
+ collisionAvoidance={ {
579
+ side: 'none',
580
+ align: 'none',
581
+ } }
582
+ />
583
+ }
567
584
  >
568
585
  <Popover.Title
569
586
  style={ {
@@ -646,8 +663,12 @@ export const CrossIframe: Story = {
646
663
  }
647
664
  />
648
665
  }
649
- collisionBoundary={
650
- iframeBoundary ?? undefined
666
+ positioner={
667
+ <Popover.Positioner
668
+ collisionBoundary={
669
+ iframeBoundary ?? undefined
670
+ }
671
+ />
651
672
  }
652
673
  >
653
674
  <Popover.Arrow />
@@ -675,98 +696,6 @@ export const CrossIframe: Story = {
675
696
  },
676
697
  };
677
698
 
678
- /**
679
- * Same cross-iframe scenario, but using `SlotFillProvider` and `Slot` from
680
- * `@wordpress/components` as the render target.
681
- *
682
- * The `Slot` renders a `div` in the parent document, and its forwarded ref
683
- * is passed to `Popover.Portal`'s `container` prop (via `Popover.Popup`'s
684
- * `portal` prop) so the popup portals into the slot element. This mirrors the
685
- * legacy Popover's `WithSlotOutsideIframe` pattern.
686
- */
687
- export const CrossIframeWithSlotFill: Story = {
688
- name: 'Cross-Iframe (SlotFill)',
689
- args: { defaultOpen: true },
690
- argTypes: { defaultOpen: { control: false } },
691
- render: function Render( { children: _children, ...args } ) {
692
- const slotRef = useRef< HTMLDivElement >( null );
693
- const [ iframeBoundary, setIframeBoundary ] =
694
- useState< HTMLIFrameElement | null >( null );
695
-
696
- return (
697
- <SlotFillProvider>
698
- <Slot
699
- name="popover-container"
700
- bubblesVirtually
701
- ref={ slotRef }
702
- />
703
- <GenericIframe
704
- ref={ setIframeBoundary }
705
- style={ {
706
- width: '100%',
707
- height: 400,
708
- border: 0,
709
- outline: '1px solid purple',
710
- } }
711
- >
712
- <div
713
- style={ {
714
- height: '200vh',
715
- paddingTop: '10vh',
716
- } }
717
- >
718
- <div
719
- style={ {
720
- maxWidth: 200,
721
- marginTop: 100,
722
- marginInline: 'auto',
723
- } }
724
- >
725
- <Popover.Root { ...args }>
726
- <Popover.Trigger
727
- style={ {
728
- padding: 8,
729
- background: 'salmon',
730
- } }
731
- >
732
- Popover&apos;s anchor (inside iframe)
733
- </Popover.Trigger>
734
- <Popover.Popup
735
- portal={
736
- <Popover.Portal
737
- container={
738
- slotRef as React.RefObject< HTMLElement >
739
- }
740
- />
741
- }
742
- collisionBoundary={
743
- iframeBoundary ?? undefined
744
- }
745
- >
746
- <Popover.Arrow />
747
- <Popover.Title
748
- style={ {
749
- marginBottom:
750
- 'var(--wpds-dimension-gap-xs)',
751
- } }
752
- >
753
- Cross-Iframe (SlotFill)
754
- </Popover.Title>
755
- <Popover.Description>
756
- This popup renders in the parent
757
- document via a `Slot` from
758
- `@wordpress/components`.
759
- </Popover.Description>
760
- </Popover.Popup>
761
- </Popover.Root>
762
- </div>
763
- </div>
764
- </GenericIframe>
765
- </SlotFillProvider>
766
- );
767
- },
768
- };
769
-
770
699
  /**
771
700
  * Popovers in Gutenberg are managed with explicit z-index values, which can
772
701
  * create situations where a popover renders below another popover when you
@@ -816,9 +745,9 @@ export const WithCustomZIndex: Story = {
816
745
  };
817
746
 
818
747
  /**
819
- * Use the `anchor` prop on `Popover.Popup` to position the popover against an
820
- * arbitrary element instead of the built-in trigger. Base UI accepts four
821
- * anchor types:
748
+ * Pass an `anchor` to `Popover.Positioner` (via `Popover.Popup`'s `positioner`
749
+ * slot) to position the popover against an arbitrary element instead of the
750
+ * built-in trigger. `anchor` accepts four types:
822
751
  *
823
752
  * 1. **Element** — a direct DOM element reference.
824
753
  * 2. **VirtualElement** — an object with a `getBoundingClientRect()` method.
@@ -852,7 +781,7 @@ export const Anchor: Story = {
852
781
  textAlign: 'center' as const,
853
782
  };
854
783
 
855
- const popupProps = {
784
+ const sharedPositionerProps = {
856
785
  collisionAvoidance: {
857
786
  side: 'none' as const,
858
787
  align: 'none' as const,
@@ -875,8 +804,12 @@ export const Anchor: Story = {
875
804
  </div>
876
805
  <Popover.Root open>
877
806
  <Popover.Popup
878
- anchor={ elementAnchor ?? undefined }
879
- { ...popupProps }
807
+ positioner={
808
+ <Popover.Positioner
809
+ anchor={ elementAnchor ?? undefined }
810
+ { ...sharedPositionerProps }
811
+ />
812
+ }
880
813
  >
881
814
  <VisuallyHidden render={ <Popover.Title /> }>
882
815
  Element anchor
@@ -896,8 +829,12 @@ export const Anchor: Story = {
896
829
  </div>
897
830
  <Popover.Root open>
898
831
  <Popover.Popup
899
- anchor={ virtualAnchor }
900
- { ...popupProps }
832
+ positioner={
833
+ <Popover.Positioner
834
+ anchor={ virtualAnchor }
835
+ { ...sharedPositionerProps }
836
+ />
837
+ }
901
838
  >
902
839
  <VisuallyHidden render={ <Popover.Title /> }>
903
840
  Virtual anchor
@@ -916,7 +853,14 @@ export const Anchor: Story = {
916
853
  RefObject anchor
917
854
  </div>
918
855
  <Popover.Root open>
919
- <Popover.Popup anchor={ refAnchor } { ...popupProps }>
856
+ <Popover.Popup
857
+ positioner={
858
+ <Popover.Positioner
859
+ anchor={ refAnchor }
860
+ { ...sharedPositionerProps }
861
+ />
862
+ }
863
+ >
920
864
  <VisuallyHidden render={ <Popover.Title /> }>
921
865
  Ref anchor
922
866
  </VisuallyHidden>
@@ -935,8 +879,12 @@ export const Anchor: Story = {
935
879
  </div>
936
880
  <Popover.Root open>
937
881
  <Popover.Popup
938
- anchor={ () => callbackTarget.current }
939
- { ...popupProps }
882
+ positioner={
883
+ <Popover.Positioner
884
+ anchor={ () => callbackTarget.current }
885
+ { ...sharedPositionerProps }
886
+ />
887
+ }
940
888
  >
941
889
  <VisuallyHidden render={ <Popover.Title /> }>
942
890
  Callback anchor
@@ -991,13 +939,12 @@ export const ToolbarVariant: Story = {
991
939
  };
992
940
 
993
941
  /**
994
- * Base UI's Positioner exposes `--available-height` and
995
- * `--available-width` CSS variables representing the space
996
- * between the anchor and the viewport edge. Apply them as `max-height` /
997
- * `max-width` via the `style` prop (which targets the positioner) to
998
- * constrain the popup size. Then add `overflow: auto` on an inner wrapper
999
- * so scrolling happens inside the popup content area — this replaces the
1000
- * legacy Popover's `resize` prop.
942
+ * `Popover.Positioner` exposes `--available-height` and `--available-width`
943
+ * CSS variables representing the space between the anchor and the viewport
944
+ * edge. These cascade down to `Popover.Popup`, where applying them as
945
+ * `max-height` / `max-width` via the `style` prop constrains the popup size.
946
+ * Then add `overflow: auto` on an inner wrapper so scrolling happens inside
947
+ * the popup content area — this replaces the legacy Popover's `resize` prop.
1001
948
  *
1002
949
  * Open the popover and resize or scroll the container to see the popup shrink
1003
950
  * to fit.
@@ -1019,7 +966,7 @@ export const ViewportConstrainedSize: Story = {
1019
966
  <Popover.Root { ...args }>
1020
967
  <Popover.Trigger>Show Content</Popover.Trigger>
1021
968
  <Popover.Popup
1022
- side="bottom"
969
+ positioner={ <Popover.Positioner side="bottom" /> }
1023
970
  style={ {
1024
971
  maxHeight: 'var(--available-height, 300px)',
1025
972
  maxWidth: 'var(--available-width, 300px)',
@@ -528,6 +528,35 @@ describe( 'Popover', () => {
528
528
  } );
529
529
  } );
530
530
 
531
+ describe( 'positioner', () => {
532
+ it( 'should render the custom positioner element wrapping the popup content', async () => {
533
+ const user = userEvent.setup();
534
+
535
+ render(
536
+ <Popover.Root>
537
+ <Popover.Trigger>Open</Popover.Trigger>
538
+ <Popover.Popup
539
+ positioner={
540
+ <Popover.Positioner data-testid="custom-positioner" />
541
+ }
542
+ >
543
+ <Popover.Title>Title</Popover.Title>
544
+ Positioner slot content
545
+ </Popover.Popup>
546
+ </Popover.Root>
547
+ );
548
+
549
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
550
+
551
+ const content = await screen.findByText(
552
+ 'Positioner slot content'
553
+ );
554
+ const positioner = screen.getByTestId( 'custom-positioner' );
555
+
556
+ expect( positioner ).toContainElement( content );
557
+ } );
558
+ } );
559
+
531
560
  describe( 'anchor', () => {
532
561
  it( 'should render the popup when an anchor element is provided without a trigger', async () => {
533
562
  function AnchorTest() {
@@ -539,7 +568,13 @@ describe( 'Popover', () => {
539
568
  Anchor element
540
569
  </div>
541
570
  <Popover.Root defaultOpen>
542
- <Popover.Popup anchor={ anchorEl ?? undefined }>
571
+ <Popover.Popup
572
+ positioner={
573
+ <Popover.Positioner
574
+ anchor={ anchorEl ?? undefined }
575
+ />
576
+ }
577
+ >
543
578
  <Popover.Title>Title</Popover.Title>
544
579
  Anchored content
545
580
  </Popover.Popup>
@@ -1,9 +1,11 @@
1
- import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from 'react';
1
+ import type { ReactElement, ReactNode } from 'react';
2
2
  import type { Popover as _Popover } from '@base-ui/react/popover';
3
3
 
4
4
  import type { ComponentProps } from '../utils/types';
5
5
 
6
- export type PortalProps = ComponentPropsWithoutRef< typeof _Popover.Portal >;
6
+ export type PortalProps = ComponentProps< typeof _Popover.Portal >;
7
+
8
+ export type PositionerProps = ComponentProps< typeof _Popover.Positioner >;
7
9
 
8
10
  export interface RootProps
9
11
  extends Pick<
@@ -27,19 +29,6 @@ export interface TriggerProps
27
29
 
28
30
  export interface PopupProps
29
31
  extends ComponentProps< 'div' >,
30
- Pick<
31
- _Popover.Positioner.Props,
32
- | 'align'
33
- | 'alignOffset'
34
- | 'anchor'
35
- | 'arrowPadding'
36
- | 'collisionAvoidance'
37
- | 'collisionBoundary'
38
- | 'collisionPadding'
39
- | 'side'
40
- | 'sideOffset'
41
- | 'sticky'
42
- >,
43
32
  Pick< _Popover.Popup.Props, 'initialFocus' | 'finalFocus' > {
44
33
  /**
45
34
  * Whether to render a backdrop overlay behind the popover.
@@ -66,6 +55,15 @@ export interface PopupProps
66
55
  */
67
56
  portal?: ReactElement< Omit< PortalProps, 'children' > >;
68
57
 
58
+ /**
59
+ * Optional positioner element, typically `<Popover.Positioner />` with
60
+ * custom positioning props (`side`, `align`, `sideOffset`, collision
61
+ * settings, anchor, etc.). When omitted, `Popover.Popup` uses
62
+ * `Popover.Positioner` with default props. Do not pass `children` on
63
+ * the positioner element; they would be ignored.
64
+ */
65
+ positioner?: ReactElement< Omit< PositionerProps, 'children' > >;
66
+
69
67
  /**
70
68
  * The visual style variant of the popup.
71
69
  *
@@ -7,6 +7,7 @@ import * as Tooltip from '../../tooltip';
7
7
  const meta: Meta< typeof Tabs.Root > = {
8
8
  title: 'Design System/Components/Tabs',
9
9
  component: Tabs.Root,
10
+ tags: [ 'manifest' ],
10
11
  subcomponents: {
11
12
  'Tabs.List': Tabs.List,
12
13
  'Tabs.Tab': Tabs.Tab,
@@ -14,9 +15,8 @@ const meta: Meta< typeof Tabs.Root > = {
14
15
  },
15
16
  parameters: {
16
17
  componentStatus: {
17
- status: 'use-with-caution',
18
+ status: 'recommended',
18
19
  whereUsed: 'global',
19
- notes: 'Not yet recommended for use alongside components from `@wordpress/components`, pending review of color consistency with `@wordpress/components`. See [WordPress/gutenberg#76135](https://github.com/WordPress/gutenberg/issues/76135).',
20
20
  },
21
21
  },
22
22
  };
@@ -1,15 +1,21 @@
1
1
  import { Tooltip as _Tooltip } from '@base-ui/react/tooltip';
2
2
  import { forwardRef } from '@wordpress/element';
3
3
  import type { PortalProps } from './types';
4
+ import { getWpCompatOverlaySlot } from '../utils/wp-compat-overlay-slot';
4
5
 
5
6
  /**
6
- * Root element that portals `Tooltip` floating content. Pass to
7
- * `Tooltip.Popup`'s `portal` prop. When `portal` is omitted, `Tooltip.Popup`
8
- * uses this component with default props.
7
+ * Used to apply custom portal behavior to `Tooltip`'s floating content.
8
+ * `container` defaults to the `@wordpress/ui` compat overlay slot.
9
9
  */
10
10
  const Portal = forwardRef< HTMLDivElement, PortalProps >(
11
- function TooltipPortal( props, ref ) {
12
- return <_Tooltip.Portal ref={ ref } { ...props } />;
11
+ function TooltipPortal( { container, ...restProps }, ref ) {
12
+ return (
13
+ <_Tooltip.Portal
14
+ container={ container ?? getWpCompatOverlaySlot() }
15
+ { ...restProps }
16
+ ref={ ref }
17
+ />
18
+ );
13
19
  }
14
20
  );
15
21
 
@@ -6,10 +6,7 @@ import resetStyles from '../utils/css/resets.module.css';
6
6
  import styles from './style.module.css';
7
7
 
8
8
  /**
9
- * Positions the floating tooltip content relative to the trigger. Pass to
10
- * `Tooltip.Popup`'s `positioner` prop to customize `side`, `align`,
11
- * `sideOffset`, collision behavior, etc. When `positioner` is omitted,
12
- * `Tooltip.Popup` uses this component with default props.
9
+ * Used to apply custom positioning to `Tooltip`'s floating content.
13
10
  */
14
11
  const Positioner = forwardRef< HTMLDivElement, PositionerProps >(
15
12
  function TooltipPositioner(
@@ -25,7 +25,7 @@
25
25
  * positioner (via the shared `.box-sizing` reset utility), so
26
26
  * the 1px is absorbed and non-FC layout is unchanged.
27
27
  */
28
- @media (forced-colors: active) {
28
+ @media ( forced-colors: active ) {
29
29
  border: 1px solid CanvasText;
30
30
  }
31
31
  }
@@ -1,7 +1,9 @@
1
1
  import { render, screen, waitFor } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { createRef } from '@wordpress/element';
4
+ import type { ReactNode } from 'react';
4
5
  import * as Tooltip from '../index';
6
+ import { useEnableWpCompatOverlaySlot } from '../../utils/use-enable-wp-compat-overlay-slot';
5
7
  import type { ProviderProps } from '../types';
6
8
 
7
9
  // Test wrapper that sets delay={0} to avoid real-time delays in tests.
@@ -151,4 +153,112 @@ describe( 'Tooltip', () => {
151
153
  );
152
154
  } );
153
155
  } );
156
+
157
+ // Slot is identified by a data attribute, not a user-facing role/text.
158
+ /* eslint-disable testing-library/no-node-access */
159
+ describe( 'wp compat overlay slot', () => {
160
+ const SLOT_SELECTOR = '[data-wp-compat-overlay-slot]';
161
+
162
+ // Exercises the public opt-in path rather than poking the flag.
163
+ function WithSlotEnabled( { children }: { children: ReactNode } ) {
164
+ useEnableWpCompatOverlaySlot();
165
+ return <>{ children }</>;
166
+ }
167
+
168
+ afterEach( () => {
169
+ // The hook is one-way at runtime; reset explicitly between tests.
170
+ delete ( window as { __wpUiCompatOverlaySlotEnabled?: boolean } )
171
+ .__wpUiCompatOverlaySlotEnabled;
172
+ document
173
+ .querySelectorAll( SLOT_SELECTOR )
174
+ .forEach( ( el ) => el.remove() );
175
+ } );
176
+
177
+ it( 'portals the popup into the slot when the consumer opts in', async () => {
178
+ const user = userEvent.setup();
179
+
180
+ render(
181
+ <WithSlotEnabled>
182
+ <TestProvider>
183
+ <Tooltip.Root>
184
+ <Tooltip.Trigger>Hover me</Tooltip.Trigger>
185
+ <Tooltip.Popup>Tooltip content</Tooltip.Popup>
186
+ </Tooltip.Root>
187
+ </TestProvider>
188
+ </WithSlotEnabled>
189
+ );
190
+
191
+ await user.hover(
192
+ screen.getByRole( 'button', { name: 'Hover me' } )
193
+ );
194
+
195
+ const content = await screen.findByText( 'Tooltip content' );
196
+ expect( content ).toBeVisible();
197
+
198
+ const slot = document.querySelector( SLOT_SELECTOR );
199
+ expect( slot ).not.toBeNull();
200
+ expect( slot ).toContainElement( content );
201
+ } );
202
+
203
+ it( 'does not create a slot when the consumer has not opted in (dormant default)', async () => {
204
+ const user = userEvent.setup();
205
+
206
+ render(
207
+ <TestProvider>
208
+ <Tooltip.Root>
209
+ <Tooltip.Trigger>Hover me</Tooltip.Trigger>
210
+ <Tooltip.Popup>Tooltip content</Tooltip.Popup>
211
+ </Tooltip.Root>
212
+ </TestProvider>
213
+ );
214
+
215
+ await user.hover(
216
+ screen.getByRole( 'button', { name: 'Hover me' } )
217
+ );
218
+
219
+ const content = await screen.findByText( 'Tooltip content' );
220
+ expect( content ).toBeVisible();
221
+
222
+ expect( document.querySelector( SLOT_SELECTOR ) ).toBeNull();
223
+ } );
224
+
225
+ it( 'lets a caller-supplied portal container override the slot', async () => {
226
+ const user = userEvent.setup();
227
+ const containerRef = createRef< HTMLDivElement >();
228
+
229
+ render(
230
+ <WithSlotEnabled>
231
+ <TestProvider>
232
+ <Tooltip.Root>
233
+ <Tooltip.Trigger>Hover me</Tooltip.Trigger>
234
+ <div
235
+ ref={ containerRef }
236
+ data-testid="custom-container"
237
+ />
238
+ <Tooltip.Popup
239
+ portal={
240
+ <Tooltip.Portal
241
+ container={ containerRef }
242
+ />
243
+ }
244
+ >
245
+ Tooltip content
246
+ </Tooltip.Popup>
247
+ </Tooltip.Root>
248
+ </TestProvider>
249
+ </WithSlotEnabled>
250
+ );
251
+
252
+ await user.hover(
253
+ screen.getByRole( 'button', { name: 'Hover me' } )
254
+ );
255
+
256
+ const content = await screen.findByText( 'Tooltip content' );
257
+ expect( content ).toBeVisible();
258
+ expect( screen.getByTestId( 'custom-container' ) ).toContainElement(
259
+ content
260
+ );
261
+ } );
262
+ } );
263
+ /* eslint-enable testing-library/no-node-access */
154
264
  } );
@@ -1,13 +1,11 @@
1
- import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from 'react';
1
+ import type { ReactElement, ReactNode } from 'react';
2
2
  import type { Tooltip as _Tooltip } from '@base-ui/react/tooltip';
3
3
 
4
4
  import type { ComponentProps } from '../utils/types';
5
5
 
6
- export type PortalProps = ComponentPropsWithoutRef< typeof _Tooltip.Portal >;
6
+ export type PortalProps = ComponentProps< typeof _Tooltip.Portal >;
7
7
 
8
- export type PositionerProps = ComponentPropsWithoutRef<
9
- typeof _Tooltip.Positioner
10
- >;
8
+ export type PositionerProps = ComponentProps< typeof _Tooltip.Positioner >;
11
9
 
12
10
  export type RootProps = Pick< _Tooltip.Root.Props, 'disabled' | 'children' >;
13
11
 
@@ -46,7 +46,7 @@ export function createOverlayTitleValidation( componentName: string ) {
46
46
  function ValidationProviderDev( {
47
47
  children,
48
48
  }: OverlayValidationProviderProps ) {
49
- const titleElementRef = useRef< HTMLElement | null >( null );
49
+ const titleElementRef = useRef< HTMLElement >( null );
50
50
 
51
51
  const scheduleValidation = useScheduleValidation( () => {
52
52
  const titleElement = titleElementRef.current;
@@ -16,10 +16,6 @@
16
16
  border: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral);
17
17
  box-shadow: var(--wpds-elevation-md);
18
18
  background-color: var(--wpds-color-bg-surface-neutral-strong);
19
- font-family: var(--wpds-typography-font-family-body);
20
- font-size: var(--wpds-typography-font-size-md);
21
- line-height: 1.4;
22
- color: var(--wpds-color-fg-interactive-neutral);
23
19
  }
24
20
 
25
21
  .list {
@@ -27,6 +23,10 @@
27
23
  grid-area: main;
28
24
  grid-template-rows: minmax(0, 1fr) auto;
29
25
  grid-template-areas: "scrollable" "footer";
26
+ font-family: var(--wpds-typography-font-family-body);
27
+ font-size: var(--wpds-typography-font-size-md);
28
+ line-height: var(--wpds-typography-line-height-sm);
29
+ color: var(--wpds-color-fg-interactive-neutral);
30
30
  }
31
31
 
32
32
  .list-scrollable-container {
@@ -124,6 +124,9 @@
124
124
  align-items: center;
125
125
  min-height: var(--wp-ui-popup-empty-min-height);
126
126
  padding-inline: var(--wp-ui-popup-empty-padding-inline);
127
+ font-family: var(--wpds-typography-font-family-body);
128
+ font-size: var(--wpds-typography-font-size-md);
129
+ line-height: var(--wpds-typography-line-height-sm);
127
130
  color: var(--wpds-color-fg-content-neutral-weak);
128
131
  }
129
132