@wordpress/ui 0.11.0 → 0.12.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 (660) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +4 -4
  3. package/build/alert-dialog/index.cjs +3 -0
  4. package/build/alert-dialog/index.cjs.map +2 -2
  5. package/build/alert-dialog/popup.cjs +120 -55
  6. package/build/alert-dialog/popup.cjs.map +3 -3
  7. package/build/alert-dialog/portal.cjs +38 -0
  8. package/build/alert-dialog/portal.cjs.map +7 -0
  9. package/build/alert-dialog/types.cjs.map +1 -1
  10. package/build/collapsible-card/content.cjs +9 -5
  11. package/build/collapsible-card/content.cjs.map +2 -2
  12. package/build/collapsible-card/header.cjs +14 -4
  13. package/build/collapsible-card/header.cjs.map +3 -3
  14. package/build/dialog/content.cjs +85 -0
  15. package/build/dialog/content.cjs.map +7 -0
  16. package/build/dialog/context.cjs +12 -44
  17. package/build/dialog/context.cjs.map +2 -2
  18. package/build/dialog/description.cjs +59 -0
  19. package/build/dialog/description.cjs.map +7 -0
  20. package/build/dialog/footer.cjs +5 -4
  21. package/build/dialog/footer.cjs.map +2 -2
  22. package/build/dialog/header.cjs +5 -4
  23. package/build/dialog/header.cjs.map +2 -2
  24. package/build/dialog/index.cjs +9 -0
  25. package/build/dialog/index.cjs.map +2 -2
  26. package/build/dialog/popup.cjs +21 -9
  27. package/build/dialog/popup.cjs.map +2 -2
  28. package/build/dialog/portal.cjs +38 -0
  29. package/build/dialog/portal.cjs.map +7 -0
  30. package/build/dialog/root.cjs +3 -2
  31. package/build/dialog/root.cjs.map +2 -2
  32. package/build/dialog/title.cjs +9 -6
  33. package/build/dialog/title.cjs.map +2 -2
  34. package/build/dialog/types.cjs.map +1 -1
  35. package/build/drawer/action.cjs +48 -0
  36. package/build/drawer/action.cjs.map +7 -0
  37. package/build/drawer/close-icon.cjs +58 -0
  38. package/build/drawer/close-icon.cjs.map +7 -0
  39. package/build/drawer/content.cjs +86 -0
  40. package/build/drawer/content.cjs.map +7 -0
  41. package/build/drawer/context.cjs +44 -0
  42. package/build/drawer/context.cjs.map +7 -0
  43. package/build/drawer/description.cjs +47 -0
  44. package/build/drawer/description.cjs.map +7 -0
  45. package/build/drawer/footer.cjs +65 -0
  46. package/build/drawer/footer.cjs.map +7 -0
  47. package/build/drawer/header.cjs +65 -0
  48. package/build/drawer/header.cjs.map +7 -0
  49. package/build/drawer/index.cjs +61 -0
  50. package/build/drawer/index.cjs.map +7 -0
  51. package/build/drawer/popup.cjs +103 -0
  52. package/build/drawer/popup.cjs.map +7 -0
  53. package/build/drawer/portal.cjs +38 -0
  54. package/build/drawer/portal.cjs.map +7 -0
  55. package/build/drawer/root.cjs +49 -0
  56. package/build/drawer/root.cjs.map +7 -0
  57. package/build/drawer/title.cjs +70 -0
  58. package/build/drawer/title.cjs.map +7 -0
  59. package/build/drawer/trigger.cjs +38 -0
  60. package/build/drawer/trigger.cjs.map +7 -0
  61. package/build/drawer/types.cjs +19 -0
  62. package/build/drawer/types.cjs.map +7 -0
  63. package/build/form/primitives/autocomplete/clear.cjs +62 -0
  64. package/build/form/primitives/autocomplete/clear.cjs.map +7 -0
  65. package/build/form/primitives/autocomplete/collection.cjs +38 -0
  66. package/build/form/primitives/autocomplete/collection.cjs.map +7 -0
  67. package/build/form/primitives/autocomplete/empty.cjs +67 -0
  68. package/build/form/primitives/autocomplete/empty.cjs.map +7 -0
  69. package/build/form/primitives/autocomplete/index.cjs +64 -0
  70. package/build/form/primitives/autocomplete/index.cjs.map +7 -0
  71. package/build/form/primitives/autocomplete/input-group.cjs +36 -0
  72. package/build/form/primitives/autocomplete/input-group.cjs.map +7 -0
  73. package/build/form/primitives/autocomplete/input.cjs +47 -0
  74. package/build/form/primitives/autocomplete/input.cjs.map +7 -0
  75. package/build/form/primitives/autocomplete/item.cjs +81 -0
  76. package/build/form/primitives/autocomplete/item.cjs.map +7 -0
  77. package/build/form/primitives/autocomplete/list-body.cjs +57 -0
  78. package/build/form/primitives/autocomplete/list-body.cjs.map +7 -0
  79. package/build/form/primitives/autocomplete/list.cjs +67 -0
  80. package/build/form/primitives/autocomplete/list.cjs.map +7 -0
  81. package/build/form/primitives/autocomplete/popup.cjs +102 -0
  82. package/build/form/primitives/autocomplete/popup.cjs.map +7 -0
  83. package/build/form/primitives/autocomplete/portal.cjs +38 -0
  84. package/build/form/primitives/autocomplete/portal.cjs.map +7 -0
  85. package/build/form/primitives/autocomplete/root.cjs +35 -0
  86. package/build/form/primitives/autocomplete/root.cjs.map +7 -0
  87. package/build/form/primitives/autocomplete/types.cjs +19 -0
  88. package/build/form/primitives/autocomplete/types.cjs.map +7 -0
  89. package/build/form/primitives/autocomplete/value.cjs +35 -0
  90. package/build/form/primitives/autocomplete/value.cjs.map +7 -0
  91. package/build/form/primitives/index.cjs +3 -0
  92. package/build/form/primitives/index.cjs.map +2 -2
  93. package/build/form/primitives/select/index.cjs +3 -0
  94. package/build/form/primitives/select/index.cjs.map +2 -2
  95. package/build/form/primitives/select/item.cjs +4 -5
  96. package/build/form/primitives/select/item.cjs.map +2 -2
  97. package/build/form/primitives/select/popup.cjs +12 -11
  98. package/build/form/primitives/select/popup.cjs.map +2 -2
  99. package/build/form/primitives/select/portal.cjs +38 -0
  100. package/build/form/primitives/select/portal.cjs.map +7 -0
  101. package/build/form/primitives/select/types.cjs.map +1 -1
  102. package/build/index.cjs +3 -0
  103. package/build/index.cjs.map +2 -2
  104. package/build/link/link.cjs +8 -18
  105. package/build/link/link.cjs.map +2 -2
  106. package/build/link/types.cjs.map +1 -1
  107. package/build/notice/action-button.cjs +3 -3
  108. package/build/notice/action-button.cjs.map +2 -2
  109. package/build/notice/action-link.cjs +8 -7
  110. package/build/notice/action-link.cjs.map +2 -2
  111. package/build/notice/actions.cjs +3 -3
  112. package/build/notice/actions.cjs.map +2 -2
  113. package/build/notice/close-icon.cjs +3 -3
  114. package/build/notice/close-icon.cjs.map +2 -2
  115. package/build/notice/description.cjs +3 -3
  116. package/build/notice/description.cjs.map +2 -2
  117. package/build/notice/root.cjs +3 -3
  118. package/build/notice/root.cjs.map +2 -2
  119. package/build/notice/title.cjs +3 -3
  120. package/build/notice/title.cjs.map +2 -2
  121. package/build/popover/arrow.cjs +4 -4
  122. package/build/popover/arrow.cjs.map +2 -2
  123. package/build/popover/context.cjs +4 -44
  124. package/build/popover/context.cjs.map +2 -2
  125. package/build/popover/description.cjs +1 -24
  126. package/build/popover/description.cjs.map +4 -4
  127. package/build/popover/index.cjs +3 -0
  128. package/build/popover/index.cjs.map +2 -2
  129. package/build/popover/popup.cjs +15 -15
  130. package/build/popover/popup.cjs.map +2 -2
  131. package/build/popover/portal.cjs +38 -0
  132. package/build/popover/portal.cjs.map +7 -0
  133. package/build/popover/root.cjs.map +1 -1
  134. package/build/popover/title.cjs +18 -4
  135. package/build/popover/title.cjs.map +3 -3
  136. package/build/popover/types.cjs.map +1 -1
  137. package/build/tabs/context.cjs +9 -22
  138. package/build/tabs/context.cjs.map +2 -2
  139. package/build/tabs/list.cjs +4 -4
  140. package/build/tabs/list.cjs.map +2 -2
  141. package/build/tabs/panel.cjs +19 -6
  142. package/build/tabs/panel.cjs.map +3 -3
  143. package/build/tabs/tab.cjs +4 -4
  144. package/build/tabs/tab.cjs.map +2 -2
  145. package/build/text/text.cjs +2 -2
  146. package/build/text/text.cjs.map +2 -2
  147. package/build/tooltip/index.cjs +3 -0
  148. package/build/tooltip/index.cjs.map +2 -2
  149. package/build/tooltip/popup.cjs +11 -14
  150. package/build/tooltip/popup.cjs.map +3 -3
  151. package/build/tooltip/portal.cjs +38 -0
  152. package/build/tooltip/portal.cjs.map +7 -0
  153. package/build/tooltip/provider.cjs +2 -2
  154. package/build/tooltip/provider.cjs.map +3 -3
  155. package/build/tooltip/root.cjs.map +3 -3
  156. package/build/tooltip/trigger.cjs +2 -2
  157. package/build/tooltip/trigger.cjs.map +3 -3
  158. package/build/tooltip/types.cjs.map +1 -1
  159. package/build/utils/create-overlay-modal-context.cjs +48 -0
  160. package/build/utils/create-overlay-modal-context.cjs.map +7 -0
  161. package/build/utils/create-overlay-title-validation.cjs +93 -0
  162. package/build/utils/create-overlay-title-validation.cjs.map +7 -0
  163. package/build/utils/render-portal-with-children.cjs +37 -0
  164. package/build/utils/render-portal-with-children.cjs.map +7 -0
  165. package/build/utils/use-deprioritized-initial-focus.cjs +8 -8
  166. package/build/utils/use-deprioritized-initial-focus.cjs.map +2 -2
  167. package/build/utils/use-overlay-scroll-state-attributes.cjs +140 -0
  168. package/build/utils/use-overlay-scroll-state-attributes.cjs.map +7 -0
  169. package/build/utils/use-schedule-validation.cjs +59 -0
  170. package/build/utils/use-schedule-validation.cjs.map +7 -0
  171. package/build/visually-hidden/visually-hidden.cjs +5 -1
  172. package/build/visually-hidden/visually-hidden.cjs.map +2 -2
  173. package/build-module/alert-dialog/index.mjs +2 -0
  174. package/build-module/alert-dialog/index.mjs.map +2 -2
  175. package/build-module/alert-dialog/popup.mjs +124 -56
  176. package/build-module/alert-dialog/popup.mjs.map +3 -3
  177. package/build-module/alert-dialog/portal.mjs +13 -0
  178. package/build-module/alert-dialog/portal.mjs.map +7 -0
  179. package/build-module/collapsible-card/content.mjs +9 -5
  180. package/build-module/collapsible-card/content.mjs.map +2 -2
  181. package/build-module/collapsible-card/header.mjs +14 -4
  182. package/build-module/collapsible-card/header.mjs.map +3 -3
  183. package/build-module/dialog/content.mjs +50 -0
  184. package/build-module/dialog/content.mjs.map +7 -0
  185. package/build-module/dialog/context.mjs +10 -51
  186. package/build-module/dialog/context.mjs.map +2 -2
  187. package/build-module/dialog/description.mjs +34 -0
  188. package/build-module/dialog/description.mjs.map +7 -0
  189. package/build-module/dialog/footer.mjs +5 -4
  190. package/build-module/dialog/footer.mjs.map +2 -2
  191. package/build-module/dialog/header.mjs +5 -4
  192. package/build-module/dialog/header.mjs.map +2 -2
  193. package/build-module/dialog/index.mjs +6 -0
  194. package/build-module/dialog/index.mjs.map +2 -2
  195. package/build-module/dialog/popup.mjs +23 -11
  196. package/build-module/dialog/popup.mjs.map +2 -2
  197. package/build-module/dialog/portal.mjs +13 -0
  198. package/build-module/dialog/portal.mjs.map +7 -0
  199. package/build-module/dialog/root.mjs +3 -2
  200. package/build-module/dialog/root.mjs.map +2 -2
  201. package/build-module/dialog/title.mjs +10 -7
  202. package/build-module/dialog/title.mjs.map +2 -2
  203. package/build-module/drawer/action.mjs +23 -0
  204. package/build-module/drawer/action.mjs.map +7 -0
  205. package/build-module/drawer/close-icon.mjs +33 -0
  206. package/build-module/drawer/close-icon.mjs.map +7 -0
  207. package/build-module/drawer/content.mjs +51 -0
  208. package/build-module/drawer/content.mjs.map +7 -0
  209. package/build-module/drawer/context.mjs +16 -0
  210. package/build-module/drawer/context.mjs.map +7 -0
  211. package/build-module/drawer/description.mjs +22 -0
  212. package/build-module/drawer/description.mjs.map +7 -0
  213. package/build-module/drawer/footer.mjs +30 -0
  214. package/build-module/drawer/footer.mjs.map +7 -0
  215. package/build-module/drawer/header.mjs +30 -0
  216. package/build-module/drawer/header.mjs.map +7 -0
  217. package/build-module/drawer/index.mjs +26 -0
  218. package/build-module/drawer/index.mjs.map +7 -0
  219. package/build-module/drawer/popup.mjs +70 -0
  220. package/build-module/drawer/popup.mjs.map +7 -0
  221. package/build-module/drawer/portal.mjs +13 -0
  222. package/build-module/drawer/portal.mjs.map +7 -0
  223. package/build-module/drawer/root.mjs +24 -0
  224. package/build-module/drawer/root.mjs.map +7 -0
  225. package/build-module/drawer/title.mjs +45 -0
  226. package/build-module/drawer/title.mjs.map +7 -0
  227. package/build-module/drawer/trigger.mjs +13 -0
  228. package/build-module/drawer/trigger.mjs.map +7 -0
  229. package/build-module/drawer/types.mjs +1 -0
  230. package/build-module/form/primitives/autocomplete/clear.mjs +37 -0
  231. package/build-module/form/primitives/autocomplete/clear.mjs.map +7 -0
  232. package/build-module/form/primitives/autocomplete/collection.mjs +13 -0
  233. package/build-module/form/primitives/autocomplete/collection.mjs.map +7 -0
  234. package/build-module/form/primitives/autocomplete/empty.mjs +32 -0
  235. package/build-module/form/primitives/autocomplete/empty.mjs.map +7 -0
  236. package/build-module/form/primitives/autocomplete/index.mjs +28 -0
  237. package/build-module/form/primitives/autocomplete/index.mjs.map +7 -0
  238. package/build-module/form/primitives/autocomplete/input-group.mjs +11 -0
  239. package/build-module/form/primitives/autocomplete/input-group.mjs.map +7 -0
  240. package/build-module/form/primitives/autocomplete/input.mjs +22 -0
  241. package/build-module/form/primitives/autocomplete/input.mjs.map +7 -0
  242. package/build-module/form/primitives/autocomplete/item.mjs +46 -0
  243. package/build-module/form/primitives/autocomplete/item.mjs.map +7 -0
  244. package/build-module/form/primitives/autocomplete/list-body.mjs +32 -0
  245. package/build-module/form/primitives/autocomplete/list-body.mjs.map +7 -0
  246. package/build-module/form/primitives/autocomplete/list.mjs +32 -0
  247. package/build-module/form/primitives/autocomplete/list.mjs.map +7 -0
  248. package/build-module/form/primitives/autocomplete/popup.mjs +69 -0
  249. package/build-module/form/primitives/autocomplete/popup.mjs.map +7 -0
  250. package/build-module/form/primitives/autocomplete/portal.mjs +13 -0
  251. package/build-module/form/primitives/autocomplete/portal.mjs.map +7 -0
  252. package/build-module/form/primitives/autocomplete/root.mjs +10 -0
  253. package/build-module/form/primitives/autocomplete/root.mjs.map +7 -0
  254. package/build-module/form/primitives/autocomplete/types.mjs +1 -0
  255. package/build-module/form/primitives/autocomplete/value.mjs +10 -0
  256. package/build-module/form/primitives/autocomplete/value.mjs.map +7 -0
  257. package/build-module/form/primitives/index.mjs +2 -0
  258. package/build-module/form/primitives/index.mjs.map +2 -2
  259. package/build-module/form/primitives/select/index.mjs +2 -0
  260. package/build-module/form/primitives/select/index.mjs.map +2 -2
  261. package/build-module/form/primitives/select/item.mjs +4 -5
  262. package/build-module/form/primitives/select/item.mjs.map +2 -2
  263. package/build-module/form/primitives/select/popup.mjs +12 -11
  264. package/build-module/form/primitives/select/popup.mjs.map +2 -2
  265. package/build-module/form/primitives/select/portal.mjs +13 -0
  266. package/build-module/form/primitives/select/portal.mjs.map +7 -0
  267. package/build-module/index.mjs +2 -0
  268. package/build-module/index.mjs.map +2 -2
  269. package/build-module/link/link.mjs +8 -18
  270. package/build-module/link/link.mjs.map +2 -2
  271. package/build-module/notice/action-button.mjs +3 -3
  272. package/build-module/notice/action-button.mjs.map +2 -2
  273. package/build-module/notice/action-link.mjs +8 -7
  274. package/build-module/notice/action-link.mjs.map +2 -2
  275. package/build-module/notice/actions.mjs +3 -3
  276. package/build-module/notice/actions.mjs.map +2 -2
  277. package/build-module/notice/close-icon.mjs +3 -3
  278. package/build-module/notice/close-icon.mjs.map +2 -2
  279. package/build-module/notice/description.mjs +3 -3
  280. package/build-module/notice/description.mjs.map +2 -2
  281. package/build-module/notice/root.mjs +3 -3
  282. package/build-module/notice/root.mjs.map +2 -2
  283. package/build-module/notice/title.mjs +3 -3
  284. package/build-module/notice/title.mjs.map +2 -2
  285. package/build-module/popover/arrow.mjs +4 -4
  286. package/build-module/popover/arrow.mjs.map +2 -2
  287. package/build-module/popover/context.mjs +4 -51
  288. package/build-module/popover/context.mjs.map +2 -2
  289. package/build-module/popover/description.mjs +1 -14
  290. package/build-module/popover/description.mjs.map +3 -3
  291. package/build-module/popover/index.mjs +2 -0
  292. package/build-module/popover/index.mjs.map +2 -2
  293. package/build-module/popover/popup.mjs +16 -16
  294. package/build-module/popover/popup.mjs.map +2 -2
  295. package/build-module/popover/portal.mjs +13 -0
  296. package/build-module/popover/portal.mjs.map +7 -0
  297. package/build-module/popover/root.mjs.map +1 -1
  298. package/build-module/popover/title.mjs +19 -5
  299. package/build-module/popover/title.mjs.map +3 -3
  300. package/build-module/tabs/context.mjs +11 -24
  301. package/build-module/tabs/context.mjs.map +2 -2
  302. package/build-module/tabs/list.mjs +4 -4
  303. package/build-module/tabs/list.mjs.map +2 -2
  304. package/build-module/tabs/panel.mjs +19 -6
  305. package/build-module/tabs/panel.mjs.map +3 -3
  306. package/build-module/tabs/tab.mjs +4 -4
  307. package/build-module/tabs/tab.mjs.map +2 -2
  308. package/build-module/text/text.mjs +2 -2
  309. package/build-module/text/text.mjs.map +2 -2
  310. package/build-module/tooltip/index.mjs +2 -0
  311. package/build-module/tooltip/index.mjs.map +2 -2
  312. package/build-module/tooltip/popup.mjs +14 -17
  313. package/build-module/tooltip/popup.mjs.map +2 -2
  314. package/build-module/tooltip/portal.mjs +13 -0
  315. package/build-module/tooltip/portal.mjs.map +7 -0
  316. package/build-module/tooltip/provider.mjs +3 -3
  317. package/build-module/tooltip/provider.mjs.map +2 -2
  318. package/build-module/tooltip/root.mjs +2 -2
  319. package/build-module/tooltip/root.mjs.map +2 -2
  320. package/build-module/tooltip/trigger.mjs +3 -3
  321. package/build-module/tooltip/trigger.mjs.map +2 -2
  322. package/build-module/utils/create-overlay-modal-context.mjs +23 -0
  323. package/build-module/utils/create-overlay-modal-context.mjs.map +7 -0
  324. package/build-module/utils/create-overlay-title-validation.mjs +75 -0
  325. package/build-module/utils/create-overlay-title-validation.mjs.map +7 -0
  326. package/build-module/utils/render-portal-with-children.mjs +12 -0
  327. package/build-module/utils/render-portal-with-children.mjs.map +7 -0
  328. package/build-module/utils/use-deprioritized-initial-focus.mjs +9 -9
  329. package/build-module/utils/use-deprioritized-initial-focus.mjs.map +2 -2
  330. package/build-module/utils/use-overlay-scroll-state-attributes.mjs +114 -0
  331. package/build-module/utils/use-overlay-scroll-state-attributes.mjs.map +7 -0
  332. package/build-module/utils/use-schedule-validation.mjs +34 -0
  333. package/build-module/utils/use-schedule-validation.mjs.map +7 -0
  334. package/build-module/visually-hidden/visually-hidden.mjs +5 -1
  335. package/build-module/visually-hidden/visually-hidden.mjs.map +2 -2
  336. package/build-types/alert-dialog/index.d.ts +1 -0
  337. package/build-types/alert-dialog/index.d.ts.map +1 -1
  338. package/build-types/alert-dialog/popup.d.ts.map +1 -1
  339. package/build-types/alert-dialog/portal.d.ts +9 -0
  340. package/build-types/alert-dialog/portal.d.ts.map +1 -0
  341. package/build-types/alert-dialog/stories/index.story.d.ts +29 -1
  342. package/build-types/alert-dialog/stories/index.story.d.ts.map +1 -1
  343. package/build-types/alert-dialog/types.d.ts +25 -3
  344. package/build-types/alert-dialog/types.d.ts.map +1 -1
  345. package/build-types/badge/stories/index.story.d.ts.map +1 -1
  346. package/build-types/card/stories/index.story.d.ts.map +1 -1
  347. package/build-types/collapsible/stories/index.story.d.ts.map +1 -1
  348. package/build-types/collapsible-card/content.d.ts.map +1 -1
  349. package/build-types/collapsible-card/header.d.ts.map +1 -1
  350. package/build-types/collapsible-card/stories/index.story.d.ts.map +1 -1
  351. package/build-types/dialog/content.d.ts +17 -0
  352. package/build-types/dialog/content.d.ts.map +1 -0
  353. package/build-types/dialog/context.d.ts +11 -16
  354. package/build-types/dialog/context.d.ts.map +1 -1
  355. package/build-types/dialog/description.d.ts +9 -0
  356. package/build-types/dialog/description.d.ts.map +1 -0
  357. package/build-types/dialog/footer.d.ts +8 -1
  358. package/build-types/dialog/footer.d.ts.map +1 -1
  359. package/build-types/dialog/header.d.ts +8 -1
  360. package/build-types/dialog/header.d.ts.map +1 -1
  361. package/build-types/dialog/index.d.ts +4 -1
  362. package/build-types/dialog/index.d.ts.map +1 -1
  363. package/build-types/dialog/popup.d.ts +3 -0
  364. package/build-types/dialog/popup.d.ts.map +1 -1
  365. package/build-types/dialog/portal.d.ts +10 -0
  366. package/build-types/dialog/portal.d.ts.map +1 -0
  367. package/build-types/dialog/root.d.ts +14 -4
  368. package/build-types/dialog/root.d.ts.map +1 -1
  369. package/build-types/dialog/stories/index.story.d.ts +29 -6
  370. package/build-types/dialog/stories/index.story.d.ts.map +1 -1
  371. package/build-types/dialog/title.d.ts.map +1 -1
  372. package/build-types/dialog/types.d.ts +60 -7
  373. package/build-types/dialog/types.d.ts.map +1 -1
  374. package/build-types/drawer/action.d.ts +8 -0
  375. package/build-types/drawer/action.d.ts.map +1 -0
  376. package/build-types/drawer/close-icon.d.ts +8 -0
  377. package/build-types/drawer/close-icon.d.ts.map +1 -0
  378. package/build-types/drawer/content.d.ts +21 -0
  379. package/build-types/drawer/content.d.ts.map +1 -0
  380. package/build-types/drawer/context.d.ts +20 -0
  381. package/build-types/drawer/context.d.ts.map +1 -0
  382. package/build-types/drawer/description.d.ts +9 -0
  383. package/build-types/drawer/description.d.ts.map +1 -0
  384. package/build-types/drawer/footer.d.ts +15 -0
  385. package/build-types/drawer/footer.d.ts.map +1 -0
  386. package/build-types/drawer/header.d.ts +15 -0
  387. package/build-types/drawer/header.d.ts.map +1 -0
  388. package/build-types/drawer/index.d.ts +13 -0
  389. package/build-types/drawer/index.d.ts.map +1 -0
  390. package/build-types/drawer/popup.d.ts +16 -0
  391. package/build-types/drawer/popup.d.ts.map +1 -0
  392. package/build-types/drawer/portal.d.ts +10 -0
  393. package/build-types/drawer/portal.d.ts.map +1 -0
  394. package/build-types/drawer/root.d.ts +21 -0
  395. package/build-types/drawer/root.d.ts.map +1 -0
  396. package/build-types/drawer/stories/index.story.d.ts +63 -0
  397. package/build-types/drawer/stories/index.story.d.ts.map +1 -0
  398. package/build-types/drawer/test/index.test.d.ts +2 -0
  399. package/build-types/drawer/test/index.test.d.ts.map +1 -0
  400. package/build-types/drawer/title.d.ts +22 -0
  401. package/build-types/drawer/title.d.ts.map +1 -0
  402. package/build-types/drawer/trigger.d.ts +7 -0
  403. package/build-types/drawer/trigger.d.ts.map +1 -0
  404. package/build-types/drawer/types.d.ts +146 -0
  405. package/build-types/drawer/types.d.ts.map +1 -0
  406. package/build-types/empty-state/stories/index.story.d.ts +1 -1
  407. package/build-types/empty-state/stories/index.story.d.ts.map +1 -1
  408. package/build-types/form/input-control/stories/index.story.d.ts +1 -1
  409. package/build-types/form/input-control/stories/index.story.d.ts.map +1 -1
  410. package/build-types/form/primitives/autocomplete/clear.d.ts +13 -0
  411. package/build-types/form/primitives/autocomplete/clear.d.ts.map +1 -0
  412. package/build-types/form/primitives/autocomplete/collection.d.ts +3 -0
  413. package/build-types/form/primitives/autocomplete/collection.d.ts.map +1 -0
  414. package/build-types/form/primitives/autocomplete/empty.d.ts +10 -0
  415. package/build-types/form/primitives/autocomplete/empty.d.ts.map +1 -0
  416. package/build-types/form/primitives/autocomplete/index.d.ts +13 -0
  417. package/build-types/form/primitives/autocomplete/index.d.ts.map +1 -0
  418. package/build-types/form/primitives/autocomplete/input-group.d.ts +16 -0
  419. package/build-types/form/primitives/autocomplete/input-group.d.ts.map +1 -0
  420. package/build-types/form/primitives/autocomplete/input.d.ts +3 -0
  421. package/build-types/form/primitives/autocomplete/input.d.ts.map +1 -0
  422. package/build-types/form/primitives/autocomplete/item.d.ts +10 -0
  423. package/build-types/form/primitives/autocomplete/item.d.ts.map +1 -0
  424. package/build-types/form/primitives/autocomplete/list-body.d.ts +13 -0
  425. package/build-types/form/primitives/autocomplete/list-body.d.ts.map +1 -0
  426. package/build-types/form/primitives/autocomplete/list.d.ts +11 -0
  427. package/build-types/form/primitives/autocomplete/list.d.ts.map +1 -0
  428. package/build-types/form/primitives/autocomplete/popup.d.ts +11 -0
  429. package/build-types/form/primitives/autocomplete/popup.d.ts.map +1 -0
  430. package/build-types/form/primitives/autocomplete/portal.d.ts +8 -0
  431. package/build-types/form/primitives/autocomplete/portal.d.ts.map +1 -0
  432. package/build-types/form/primitives/autocomplete/root.d.ts +8 -0
  433. package/build-types/form/primitives/autocomplete/root.d.ts.map +1 -0
  434. package/build-types/form/primitives/autocomplete/stories/fixtures.d.ts +8 -0
  435. package/build-types/form/primitives/autocomplete/stories/fixtures.d.ts.map +1 -0
  436. package/build-types/form/primitives/autocomplete/stories/index.story.d.ts +41 -0
  437. package/build-types/form/primitives/autocomplete/stories/index.story.d.ts.map +1 -0
  438. package/build-types/form/primitives/autocomplete/test/index.test.d.ts +2 -0
  439. package/build-types/form/primitives/autocomplete/test/index.test.d.ts.map +1 -0
  440. package/build-types/form/primitives/autocomplete/types.d.ts +44 -0
  441. package/build-types/form/primitives/autocomplete/types.d.ts.map +1 -0
  442. package/build-types/form/primitives/autocomplete/value.d.ts +3 -0
  443. package/build-types/form/primitives/autocomplete/value.d.ts.map +1 -0
  444. package/build-types/form/primitives/field/stories/index.story.d.ts +1 -1
  445. package/build-types/form/primitives/field/stories/index.story.d.ts.map +1 -1
  446. package/build-types/form/primitives/fieldset/stories/index.story.d.ts +1 -1
  447. package/build-types/form/primitives/fieldset/stories/index.story.d.ts.map +1 -1
  448. package/build-types/form/primitives/index.d.ts +1 -0
  449. package/build-types/form/primitives/index.d.ts.map +1 -1
  450. package/build-types/form/primitives/input/stories/index.story.d.ts +1 -1
  451. package/build-types/form/primitives/input/stories/index.story.d.ts.map +1 -1
  452. package/build-types/form/primitives/input-layout/stories/index.story.d.ts +1 -1
  453. package/build-types/form/primitives/input-layout/stories/index.story.d.ts.map +1 -1
  454. package/build-types/form/primitives/select/index.d.ts +1 -0
  455. package/build-types/form/primitives/select/index.d.ts.map +1 -1
  456. package/build-types/form/primitives/select/item.d.ts.map +1 -1
  457. package/build-types/form/primitives/select/popup.d.ts +1 -2
  458. package/build-types/form/primitives/select/popup.d.ts.map +1 -1
  459. package/build-types/form/primitives/select/portal.d.ts +8 -0
  460. package/build-types/form/primitives/select/portal.d.ts.map +1 -0
  461. package/build-types/form/primitives/select/stories/index.story.d.ts +14 -6
  462. package/build-types/form/primitives/select/stories/index.story.d.ts.map +1 -1
  463. package/build-types/form/primitives/select/types.d.ts +7 -2
  464. package/build-types/form/primitives/select/types.d.ts.map +1 -1
  465. package/build-types/index.d.ts +1 -0
  466. package/build-types/index.d.ts.map +1 -1
  467. package/build-types/link/link.d.ts.map +1 -1
  468. package/build-types/link/stories/index.story.d.ts +2 -3
  469. package/build-types/link/stories/index.story.d.ts.map +1 -1
  470. package/build-types/link/types.d.ts +1 -2
  471. package/build-types/link/types.d.ts.map +1 -1
  472. package/build-types/notice/action-link.d.ts.map +1 -1
  473. package/build-types/popover/context.d.ts +6 -13
  474. package/build-types/popover/context.d.ts.map +1 -1
  475. package/build-types/popover/description.d.ts +0 -1
  476. package/build-types/popover/description.d.ts.map +1 -1
  477. package/build-types/popover/index.d.ts +2 -1
  478. package/build-types/popover/index.d.ts.map +1 -1
  479. package/build-types/popover/popup.d.ts +3 -2
  480. package/build-types/popover/popup.d.ts.map +1 -1
  481. package/build-types/popover/portal.d.ts +9 -0
  482. package/build-types/popover/portal.d.ts.map +1 -0
  483. package/build-types/popover/root.d.ts +2 -2
  484. package/build-types/popover/stories/index.story.d.ts +23 -15
  485. package/build-types/popover/stories/index.story.d.ts.map +1 -1
  486. package/build-types/popover/title.d.ts.map +1 -1
  487. package/build-types/popover/types.d.ts +8 -15
  488. package/build-types/popover/types.d.ts.map +1 -1
  489. package/build-types/stack/stories/index.story.d.ts.map +1 -1
  490. package/build-types/tabs/context.d.ts.map +1 -1
  491. package/build-types/tabs/panel.d.ts.map +1 -1
  492. package/build-types/tabs/stories/index.story.d.ts +1 -1
  493. package/build-types/tabs/stories/index.story.d.ts.map +1 -1
  494. package/build-types/text/stories/index.story.d.ts.map +1 -1
  495. package/build-types/tooltip/index.d.ts +2 -1
  496. package/build-types/tooltip/index.d.ts.map +1 -1
  497. package/build-types/tooltip/popup.d.ts.map +1 -1
  498. package/build-types/tooltip/portal.d.ts +8 -0
  499. package/build-types/tooltip/portal.d.ts.map +1 -0
  500. package/build-types/tooltip/provider.d.ts +1 -1
  501. package/build-types/tooltip/provider.d.ts.map +1 -1
  502. package/build-types/tooltip/stories/index.story.d.ts +18 -1
  503. package/build-types/tooltip/stories/index.story.d.ts.map +1 -1
  504. package/build-types/tooltip/stories/usage-guidelines.story.d.ts.map +1 -1
  505. package/build-types/tooltip/trigger.d.ts.map +1 -1
  506. package/build-types/tooltip/types.d.ts +11 -7
  507. package/build-types/tooltip/types.d.ts.map +1 -1
  508. package/build-types/utils/create-overlay-modal-context.d.ts +14 -0
  509. package/build-types/utils/create-overlay-modal-context.d.ts.map +1 -0
  510. package/build-types/utils/create-overlay-title-validation.d.ts +15 -0
  511. package/build-types/utils/create-overlay-title-validation.d.ts.map +1 -0
  512. package/build-types/utils/render-portal-with-children.d.ts +16 -0
  513. package/build-types/utils/render-portal-with-children.d.ts.map +1 -0
  514. package/build-types/utils/use-deprioritized-initial-focus.d.ts +9 -8
  515. package/build-types/utils/use-deprioritized-initial-focus.d.ts.map +1 -1
  516. package/build-types/utils/use-overlay-scroll-state-attributes.d.ts +85 -0
  517. package/build-types/utils/use-overlay-scroll-state-attributes.d.ts.map +1 -0
  518. package/build-types/utils/use-schedule-validation.d.ts +13 -0
  519. package/build-types/utils/use-schedule-validation.d.ts.map +1 -0
  520. package/build-types/visually-hidden/stories/index.story.d.ts.map +1 -1
  521. package/build-types/visually-hidden/visually-hidden.d.ts +4 -20
  522. package/build-types/visually-hidden/visually-hidden.d.ts.map +1 -1
  523. package/package.json +12 -12
  524. package/src/alert-dialog/index.ts +1 -0
  525. package/src/alert-dialog/popup.tsx +114 -45
  526. package/src/alert-dialog/portal.tsx +17 -0
  527. package/src/alert-dialog/stories/index.story.tsx +123 -3
  528. package/src/alert-dialog/style.module.css +13 -4
  529. package/src/alert-dialog/test/index.test.tsx +329 -3
  530. package/src/alert-dialog/types.ts +30 -3
  531. package/src/badge/stories/choosing-intent.story.tsx +1 -1
  532. package/src/badge/stories/index.story.tsx +1 -0
  533. package/src/card/stories/index.story.tsx +1 -0
  534. package/src/collapsible/stories/index.story.tsx +1 -0
  535. package/src/collapsible-card/content.tsx +12 -1
  536. package/src/collapsible-card/header.tsx +2 -0
  537. package/src/collapsible-card/stories/index.story.tsx +1 -0
  538. package/src/collapsible-card/style.module.css +16 -4
  539. package/src/dialog/content.tsx +47 -0
  540. package/src/dialog/context.tsx +14 -98
  541. package/src/dialog/description.tsx +27 -0
  542. package/src/dialog/footer.tsx +10 -2
  543. package/src/dialog/header.tsx +10 -2
  544. package/src/dialog/index.ts +16 -1
  545. package/src/dialog/popup.tsx +28 -8
  546. package/src/dialog/portal.tsx +18 -0
  547. package/src/dialog/root.tsx +22 -5
  548. package/src/dialog/stories/index.story.tsx +195 -51
  549. package/src/dialog/style.module.css +73 -23
  550. package/src/dialog/test/index.test.tsx +849 -149
  551. package/src/dialog/title.tsx +6 -4
  552. package/src/dialog/types.ts +64 -6
  553. package/src/drawer/action.tsx +28 -0
  554. package/src/drawer/close-icon.tsx +33 -0
  555. package/src/drawer/content.tsx +50 -0
  556. package/src/drawer/context.tsx +29 -0
  557. package/src/drawer/description.tsx +25 -0
  558. package/src/drawer/footer.tsx +34 -0
  559. package/src/drawer/header.tsx +34 -0
  560. package/src/drawer/index.ts +25 -0
  561. package/src/drawer/popup.tsx +100 -0
  562. package/src/drawer/portal.tsx +18 -0
  563. package/src/drawer/root.tsx +41 -0
  564. package/src/drawer/stories/index.story.tsx +543 -0
  565. package/src/drawer/style.module.css +324 -0
  566. package/src/drawer/test/index.test.tsx +1097 -0
  567. package/src/drawer/title.tsx +53 -0
  568. package/src/drawer/trigger.tsx +14 -0
  569. package/src/drawer/types.ts +174 -0
  570. package/src/empty-state/stories/index.story.tsx +2 -1
  571. package/src/form/input-control/stories/index.story.tsx +4 -1
  572. package/src/form/primitives/autocomplete/clear.tsx +35 -0
  573. package/src/form/primitives/autocomplete/collection.tsx +13 -0
  574. package/src/form/primitives/autocomplete/empty.tsx +17 -0
  575. package/src/form/primitives/autocomplete/index.ts +12 -0
  576. package/src/form/primitives/autocomplete/input-group.tsx +16 -0
  577. package/src/form/primitives/autocomplete/input.tsx +20 -0
  578. package/src/form/primitives/autocomplete/item.tsx +24 -0
  579. package/src/form/primitives/autocomplete/list-body.tsx +23 -0
  580. package/src/form/primitives/autocomplete/list.tsx +17 -0
  581. package/src/form/primitives/autocomplete/popup.tsx +42 -0
  582. package/src/form/primitives/autocomplete/portal.tsx +16 -0
  583. package/src/form/primitives/autocomplete/root.tsx +11 -0
  584. package/src/form/primitives/autocomplete/stories/fixtures.ts +35 -0
  585. package/src/form/primitives/autocomplete/stories/index.story.tsx +437 -0
  586. package/src/form/primitives/autocomplete/style.module.css +7 -0
  587. package/src/form/primitives/autocomplete/test/index.test.tsx +162 -0
  588. package/src/form/primitives/autocomplete/types.ts +74 -0
  589. package/src/form/primitives/autocomplete/value.tsx +6 -0
  590. package/src/form/primitives/field/stories/index.story.tsx +1 -1
  591. package/src/form/primitives/fieldset/stories/index.story.tsx +1 -1
  592. package/src/form/primitives/index.ts +1 -0
  593. package/src/form/primitives/input/stories/index.story.tsx +2 -1
  594. package/src/form/primitives/input-layout/stories/index.story.tsx +2 -1
  595. package/src/form/primitives/select/index.ts +1 -0
  596. package/src/form/primitives/select/item.tsx +0 -1
  597. package/src/form/primitives/select/popup.tsx +34 -37
  598. package/src/form/primitives/select/portal.tsx +16 -0
  599. package/src/form/primitives/select/stories/index.story.tsx +21 -7
  600. package/src/form/primitives/select/test/index.test.tsx +7 -3
  601. package/src/form/primitives/select/types.ts +9 -2
  602. package/src/index.ts +1 -0
  603. package/src/link/link.tsx +12 -26
  604. package/src/link/stories/index.story.tsx +6 -11
  605. package/src/link/style.module.css +5 -17
  606. package/src/link/test/index.test.tsx +31 -27
  607. package/src/link/types.ts +1 -2
  608. package/src/notice/action-link.tsx +7 -4
  609. package/src/notice/style.module.css +5 -5
  610. package/src/popover/context.tsx +6 -89
  611. package/src/popover/description.tsx +1 -5
  612. package/src/popover/index.ts +2 -1
  613. package/src/popover/popup.tsx +17 -15
  614. package/src/popover/portal.tsx +17 -0
  615. package/src/popover/root.tsx +2 -2
  616. package/src/popover/stories/index.story.tsx +56 -25
  617. package/src/popover/style.module.css +33 -4
  618. package/src/popover/test/index.test.tsx +189 -74
  619. package/src/popover/title.tsx +9 -5
  620. package/src/popover/types.ts +10 -15
  621. package/src/stack/stories/index.story.tsx +1 -0
  622. package/src/tabs/context.tsx +14 -34
  623. package/src/tabs/panel.tsx +7 -2
  624. package/src/tabs/stories/index.story.tsx +2 -1
  625. package/src/tabs/style.module.css +0 -17
  626. package/src/tabs/test/index.test.tsx +7 -3
  627. package/src/text/stories/index.story.tsx +1 -0
  628. package/src/text/text.tsx +2 -2
  629. package/src/tooltip/index.ts +2 -1
  630. package/src/tooltip/popup.tsx +24 -28
  631. package/src/tooltip/portal.tsx +16 -0
  632. package/src/tooltip/provider.tsx +3 -3
  633. package/src/tooltip/root.tsx +2 -2
  634. package/src/tooltip/stories/index.story.tsx +39 -1
  635. package/src/tooltip/stories/usage-guidelines.story.tsx +5 -1
  636. package/src/tooltip/style.module.css +12 -0
  637. package/src/tooltip/test/index.test.tsx +9 -3
  638. package/src/tooltip/trigger.tsx +3 -7
  639. package/src/tooltip/types.ts +13 -7
  640. package/src/utils/create-overlay-modal-context.tsx +34 -0
  641. package/src/utils/create-overlay-title-validation.tsx +116 -0
  642. package/src/utils/css/item-popup.module.css +9 -11
  643. package/src/utils/css/overlay-chrome.module.css +222 -0
  644. package/src/utils/render-portal-with-children.ts +27 -0
  645. package/src/utils/test/use-deprioritized-initial-focus.test.tsx +3 -3
  646. package/src/utils/use-deprioritized-initial-focus.ts +23 -17
  647. package/src/utils/use-overlay-scroll-state-attributes.ts +272 -0
  648. package/src/utils/use-schedule-validation.ts +45 -0
  649. package/src/visually-hidden/stories/index.story.tsx +1 -0
  650. package/src/visually-hidden/visually-hidden.tsx +9 -21
  651. package/build/types/css-modules.d.cjs +0 -2
  652. package/build/types/react.d.cjs +0 -5
  653. package/build/types/react.d.cjs.map +0 -7
  654. package/build-module/types/css-modules.d.mjs +0 -1
  655. package/build-module/types/react.d.mjs +0 -3
  656. package/build-module/types/react.d.mjs.map +0 -7
  657. package/src/types/css-modules.d.ts +0 -4
  658. package/src/types/react.d.ts +0 -7
  659. /package/build-module/{types/css-modules.d.mjs.map → drawer/types.mjs.map} +0 -0
  660. /package/{build/types/css-modules.d.cjs.map → build-module/form/primitives/autocomplete/types.mjs.map} +0 -0
@@ -1,36 +1,19 @@
1
- import { render, screen, waitFor } from '@testing-library/react';
1
+ import { act, render, screen, waitFor } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
- import { Component, createRef } from '@wordpress/element';
4
- import type { ReactNode } from 'react';
3
+ import { createRef, useState } from '@wordpress/element';
5
4
  import * as Dialog from '../index';
6
5
 
7
- class TestErrorBoundary extends Component<
8
- { children: ReactNode; onError: ( error: Error ) => void },
9
- { hasError: boolean }
10
- > {
11
- constructor( props: {
12
- children: ReactNode;
13
- onError: ( error: Error ) => void;
14
- } ) {
15
- super( props );
16
- this.state = { hasError: false };
17
- }
18
-
19
- static getDerivedStateFromError() {
20
- return { hasError: true };
21
- }
22
-
23
- componentDidCatch( error: Error ) {
24
- this.props.onError( error );
25
- }
26
-
27
- render() {
28
- if ( this.state.hasError ) {
29
- return null;
30
- }
31
-
32
- return this.props.children;
33
- }
6
+ function collectUncaughtErrors() {
7
+ const errors: Error[] = [];
8
+ const handler = ( event: ErrorEvent ) => {
9
+ event.preventDefault();
10
+ errors.push( event.error );
11
+ };
12
+ window.addEventListener( 'error', handler );
13
+ return {
14
+ errors,
15
+ cleanup: () => window.removeEventListener( 'error', handler ),
16
+ };
34
17
  }
35
18
 
36
19
  describe( 'Dialog', () => {
@@ -39,10 +22,12 @@ describe( 'Dialog', () => {
39
22
  const triggerRef = createRef< HTMLButtonElement >();
40
23
  const popupRef = createRef< HTMLDivElement >();
41
24
  const actionRef = createRef< HTMLButtonElement >();
42
- const headerRef = createRef< HTMLDivElement >();
25
+ const headerRef = createRef< HTMLElement >();
43
26
  const titleRef = createRef< HTMLHeadingElement >();
27
+ const descriptionRef = createRef< HTMLParagraphElement >();
44
28
  const closeIconRef = createRef< HTMLButtonElement >();
45
- const footerRef = createRef< HTMLDivElement >();
29
+ const footerRef = createRef< HTMLElement >();
30
+ const contentRef = createRef< HTMLDivElement >();
46
31
 
47
32
  render(
48
33
  <Dialog.Root>
@@ -54,6 +39,11 @@ describe( 'Dialog', () => {
54
39
  </Dialog.Title>
55
40
  <Dialog.CloseIcon ref={ closeIconRef } />
56
41
  </Dialog.Header>
42
+ <Dialog.Content ref={ contentRef }>
43
+ <Dialog.Description ref={ descriptionRef }>
44
+ A test description
45
+ </Dialog.Description>
46
+ </Dialog.Content>
57
47
  <Dialog.Footer ref={ footerRef }>
58
48
  <Dialog.Action ref={ actionRef }>Close</Dialog.Action>
59
49
  </Dialog.Footer>
@@ -73,15 +63,221 @@ describe( 'Dialog', () => {
73
63
  } );
74
64
 
75
65
  // Now that the dialog is open, verify all inner refs
76
- expect( headerRef.current ).toBeInstanceOf( HTMLDivElement );
66
+ expect( headerRef.current ).toBeInstanceOf( HTMLElement );
67
+ expect( headerRef.current?.tagName ).toBe( 'HEADER' );
77
68
  expect( titleRef.current ).toBeInstanceOf( HTMLHeadingElement );
69
+ expect( descriptionRef.current ).toBeInstanceOf( HTMLParagraphElement );
78
70
  expect( closeIconRef.current ).toBeInstanceOf( HTMLButtonElement );
79
71
  expect( actionRef.current ).toBeInstanceOf( HTMLButtonElement );
80
- expect( footerRef.current ).toBeInstanceOf( HTMLDivElement );
72
+ expect( footerRef.current ).toBeInstanceOf( HTMLElement );
73
+ expect( footerRef.current?.tagName ).toBe( 'FOOTER' );
74
+ expect( contentRef.current ).toBeInstanceOf( HTMLDivElement );
75
+ } );
76
+
77
+ it( 'merges user `className` on Dialog.Title with the internal one', async () => {
78
+ // Regression test for the shared `useRender` class-name merge
79
+ // that also covers Popover.Title, Dialog.Description and
80
+ // Popover.Description.
81
+ const user = userEvent.setup();
82
+
83
+ render(
84
+ <Dialog.Root>
85
+ <Dialog.Trigger>Open</Dialog.Trigger>
86
+ <Dialog.Popup>
87
+ <Dialog.Title className="custom-title">Title</Dialog.Title>
88
+ </Dialog.Popup>
89
+ </Dialog.Root>
90
+ );
91
+
92
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
93
+
94
+ const heading = await screen.findByRole( 'heading', { name: 'Title' } );
95
+ // The regression this guards against: `useRender` must still forward
96
+ // the user-supplied className to the underlying DOM node. CSS module
97
+ // classes are stubbed in the Jest environment, so we can only assert
98
+ // the user class end-to-end.
99
+ expect( heading ).toHaveClass( 'custom-title' );
100
+ } );
101
+
102
+ it( 'associates Dialog.Description with the popup via aria-describedby', async () => {
103
+ const user = userEvent.setup();
104
+ const popupRef = createRef< HTMLDivElement >();
105
+
106
+ render(
107
+ <Dialog.Root>
108
+ <Dialog.Trigger>Open</Dialog.Trigger>
109
+ <Dialog.Popup ref={ popupRef }>
110
+ <Dialog.Title>Title</Dialog.Title>
111
+ <Dialog.Description>My description</Dialog.Description>
112
+ </Dialog.Popup>
113
+ </Dialog.Root>
114
+ );
115
+
116
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
117
+
118
+ await waitFor( () => {
119
+ expect( popupRef.current ).toHaveAccessibleDescription(
120
+ 'My description'
121
+ );
122
+ } );
123
+ } );
124
+
125
+ it( 'renders Dialog.Footer and supports render/className props', async () => {
126
+ const user = userEvent.setup();
127
+
128
+ render(
129
+ <Dialog.Root>
130
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
131
+ <Dialog.Popup>
132
+ <Dialog.Title>Test Dialog</Dialog.Title>
133
+ <Dialog.Footer
134
+ render={ <section data-testid="dialog-footer" /> }
135
+ className="custom-footer"
136
+ >
137
+ <Dialog.Action>Close</Dialog.Action>
138
+ </Dialog.Footer>
139
+ </Dialog.Popup>
140
+ </Dialog.Root>
141
+ );
142
+
143
+ await user.click(
144
+ screen.getByRole( 'button', { name: 'Open Dialog' } )
145
+ );
146
+
147
+ const footer = await screen.findByTestId( 'dialog-footer' );
148
+ expect( footer.tagName ).toBe( 'SECTION' );
149
+ expect( footer ).toHaveClass( 'custom-footer' );
150
+ expect(
151
+ screen.getByRole( 'button', { name: 'Close' } )
152
+ ).toBeInTheDocument();
153
+ } );
154
+
155
+ it( 'renders backdrop only when modal is true', async () => {
156
+ const getBackdrops = () => screen.queryAllByTestId( 'dialog-backdrop' );
157
+
158
+ const view = render(
159
+ <Dialog.Root open modal>
160
+ <Dialog.Popup>
161
+ <Dialog.Title>Modal dialog</Dialog.Title>
162
+ </Dialog.Popup>
163
+ </Dialog.Root>
164
+ );
165
+
166
+ expect( await screen.findByRole( 'dialog' ) ).toBeInTheDocument();
167
+ expect( getBackdrops() ).toHaveLength( 1 );
168
+
169
+ view.rerender(
170
+ <Dialog.Root open modal={ false }>
171
+ <Dialog.Popup>
172
+ <Dialog.Title>Non modal dialog</Dialog.Title>
173
+ </Dialog.Popup>
174
+ </Dialog.Root>
175
+ );
176
+ expect( await screen.findByRole( 'dialog' ) ).toBeInTheDocument();
177
+ expect( getBackdrops() ).toHaveLength( 0 );
178
+
179
+ view.rerender(
180
+ <Dialog.Root open modal="trap-focus">
181
+ <Dialog.Popup>
182
+ <Dialog.Title>Trap focus dialog</Dialog.Title>
183
+ </Dialog.Popup>
184
+ </Dialog.Root>
185
+ );
186
+ expect( await screen.findByRole( 'dialog' ) ).toBeInTheDocument();
187
+ expect( getBackdrops() ).toHaveLength( 0 );
188
+ } );
189
+
190
+ it( 'renders the popup across default and explicit size values', async () => {
191
+ const view = render(
192
+ <Dialog.Root open>
193
+ <Dialog.Popup>
194
+ <Dialog.Title>Default size dialog</Dialog.Title>
195
+ </Dialog.Popup>
196
+ </Dialog.Root>
197
+ );
198
+
199
+ expect( await screen.findByRole( 'dialog' ) ).toBeInTheDocument();
200
+
201
+ for ( const size of [
202
+ 'small',
203
+ 'medium',
204
+ 'large',
205
+ 'stretch',
206
+ 'full',
207
+ ] as const ) {
208
+ view.rerender(
209
+ <Dialog.Root open>
210
+ <Dialog.Popup size={ size }>
211
+ <Dialog.Title>{ size } dialog</Dialog.Title>
212
+ </Dialog.Popup>
213
+ </Dialog.Root>
214
+ );
215
+ expect( await screen.findByRole( 'dialog' ) ).toBeInTheDocument();
216
+ }
217
+ } );
218
+
219
+ it( 'marks Dialog.Action as disabled when loading is true', async () => {
220
+ render(
221
+ <Dialog.Root open>
222
+ <Dialog.Popup>
223
+ <Dialog.Title>Action states</Dialog.Title>
224
+ <Dialog.Footer>
225
+ <Dialog.Action loading>Loading action</Dialog.Action>
226
+ </Dialog.Footer>
227
+ </Dialog.Popup>
228
+ </Dialog.Root>
229
+ );
230
+
231
+ const action = await screen.findByRole( 'button', {
232
+ name: 'Loading action',
233
+ } );
234
+ expect( action ).toHaveAttribute( 'aria-disabled', 'true' );
235
+ } );
236
+
237
+ it( 'marks Dialog.Action as disabled when disabled is true', async () => {
238
+ render(
239
+ <Dialog.Root open>
240
+ <Dialog.Popup>
241
+ <Dialog.Title>Action states</Dialog.Title>
242
+ <Dialog.Footer>
243
+ <Dialog.Action disabled>Disabled action</Dialog.Action>
244
+ </Dialog.Footer>
245
+ </Dialog.Popup>
246
+ </Dialog.Root>
247
+ );
248
+
249
+ const action = await screen.findByRole( 'button', {
250
+ name: 'Disabled action',
251
+ } );
252
+ expect( action ).toHaveAttribute( 'aria-disabled', 'true' );
253
+ } );
254
+
255
+ it( 'lets explicit disabled={ false } override loading on Dialog.Action', async () => {
256
+ // `Dialog.Action` uses `disabled ?? loading`, so an explicit
257
+ // `disabled={ false }` wins over an active loading state.
258
+ render(
259
+ <Dialog.Root open>
260
+ <Dialog.Popup>
261
+ <Dialog.Title>Action states</Dialog.Title>
262
+ <Dialog.Footer>
263
+ <Dialog.Action disabled={ false } loading>
264
+ Explicit not-disabled
265
+ </Dialog.Action>
266
+ </Dialog.Footer>
267
+ </Dialog.Popup>
268
+ </Dialog.Root>
269
+ );
270
+
271
+ const action = await screen.findByRole( 'button', {
272
+ name: 'Explicit not-disabled',
273
+ } );
274
+ expect( action ).not.toHaveAttribute( 'aria-disabled', 'true' );
81
275
  } );
82
276
 
83
277
  describe( 'Development mode validation', () => {
84
- // Suppress React's error boundary logging for these tests.
278
+ // Suppress console.error from React act() warnings and jsdom
279
+ // unhandled-error logging. Validation errors are caught via
280
+ // collectUncaughtErrors (window 'error' event) instead.
85
281
  let originalConsoleError: typeof console.error;
86
282
 
87
283
  beforeEach( () => {
@@ -98,210 +294,313 @@ describe( 'Dialog', () => {
98
294
 
99
295
  it( 'should throw when Dialog.Title is missing', async () => {
100
296
  const user = userEvent.setup();
101
- const onError = jest.fn();
297
+ const { errors, cleanup } = collectUncaughtErrors();
102
298
 
103
299
  render(
104
- <TestErrorBoundary onError={ onError }>
105
- <Dialog.Root>
106
- <Dialog.Trigger>Open Dialog</Dialog.Trigger>
107
- <Dialog.Popup>
108
- <Dialog.Header>
109
- { /* Missing Dialog.Title */ }
110
- </Dialog.Header>
111
- <p>Content without a title</p>
112
- <Dialog.Footer>
113
- <Dialog.Action>Close</Dialog.Action>
114
- </Dialog.Footer>
115
- </Dialog.Popup>
116
- </Dialog.Root>
117
- </TestErrorBoundary>
300
+ <Dialog.Root>
301
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
302
+ <Dialog.Popup>
303
+ <Dialog.Header>
304
+ { /* Missing Dialog.Title */ }
305
+ </Dialog.Header>
306
+ <p>Content without a title</p>
307
+ <Dialog.Footer>
308
+ <Dialog.Action>Close</Dialog.Action>
309
+ </Dialog.Footer>
310
+ </Dialog.Popup>
311
+ </Dialog.Root>
118
312
  );
119
313
 
120
- // Open the dialog - this will trigger the error in useEffect
121
314
  await user.click(
122
315
  screen.getByRole( 'button', { name: 'Open Dialog' } )
123
316
  );
124
317
 
125
318
  await waitFor( () => {
126
- expect( onError ).toHaveBeenCalled();
319
+ expect( errors.length ).toBeGreaterThan( 0 );
127
320
  } );
128
321
 
129
- expect( onError.mock.calls[ 0 ][ 0 ] ).toBeInstanceOf( Error );
130
- expect( ( onError.mock.calls[ 0 ][ 0 ] as Error ).message ).toBe(
322
+ expect( errors[ 0 ].message ).toBe(
131
323
  'Dialog: Missing <Dialog.Title>. ' +
132
324
  'For accessibility, every dialog requires a title. ' +
133
325
  'If needed, the title can be visually hidden but must not be omitted.'
134
326
  );
327
+
328
+ cleanup();
135
329
  } );
136
330
 
137
331
  it( 'should not throw before opening the dialog', async () => {
138
- const onError = jest.fn();
332
+ const { errors, cleanup } = collectUncaughtErrors();
139
333
 
140
334
  render(
141
- <TestErrorBoundary onError={ onError }>
142
- <Dialog.Root>
143
- <Dialog.Trigger>Open Dialog</Dialog.Trigger>
144
- <Dialog.Popup>
145
- <Dialog.Header>
146
- <Dialog.Title>My Title</Dialog.Title>
147
- </Dialog.Header>
148
- <p>Content with a title</p>
149
- <Dialog.Footer>
150
- <Dialog.Action>Close</Dialog.Action>
151
- </Dialog.Footer>
152
- </Dialog.Popup>
153
- </Dialog.Root>
154
- </TestErrorBoundary>
335
+ <Dialog.Root>
336
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
337
+ <Dialog.Popup>
338
+ <Dialog.Header>
339
+ <Dialog.Title>My Title</Dialog.Title>
340
+ </Dialog.Header>
341
+ <p>Content with a title</p>
342
+ <Dialog.Footer>
343
+ <Dialog.Action>Close</Dialog.Action>
344
+ </Dialog.Footer>
345
+ </Dialog.Popup>
346
+ </Dialog.Root>
155
347
  );
156
348
 
157
- // Check that the dialog itself hasn't been rendered in the DOM.
158
349
  await expect( screen.findByRole( 'dialog' ) ).rejects.toThrow();
350
+ expect( errors ).toHaveLength( 0 );
159
351
 
160
- expect( onError ).not.toHaveBeenCalled();
352
+ cleanup();
161
353
  } );
162
354
 
163
355
  it( 'should not throw when Dialog.Title is present', async () => {
164
356
  const user = userEvent.setup();
165
- const onError = jest.fn();
357
+ const { errors, cleanup } = collectUncaughtErrors();
166
358
 
167
359
  render(
168
- <TestErrorBoundary onError={ onError }>
169
- <Dialog.Root>
170
- <Dialog.Trigger>Open Dialog</Dialog.Trigger>
171
- <Dialog.Popup>
172
- <Dialog.Header>
173
- <Dialog.Title>My Title</Dialog.Title>
174
- </Dialog.Header>
175
- <p>Content with a title</p>
176
- <Dialog.Footer>
177
- <Dialog.Action>Close</Dialog.Action>
178
- </Dialog.Footer>
179
- </Dialog.Popup>
180
- </Dialog.Root>
181
- </TestErrorBoundary>
360
+ <Dialog.Root>
361
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
362
+ <Dialog.Popup>
363
+ <Dialog.Header>
364
+ <Dialog.Title>My Title</Dialog.Title>
365
+ </Dialog.Header>
366
+ <p>Content with a title</p>
367
+ <Dialog.Footer>
368
+ <Dialog.Action>Close</Dialog.Action>
369
+ </Dialog.Footer>
370
+ </Dialog.Popup>
371
+ </Dialog.Root>
182
372
  );
183
373
 
184
- // Open the dialog - should not throw
185
374
  await user.click(
186
375
  screen.getByRole( 'button', { name: 'Open Dialog' } )
187
376
  );
188
377
 
189
- // Wait for the dialog to appear and ensure validation does not trigger errors
190
378
  await waitFor( () => {
191
379
  expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
192
380
  } );
193
- expect( onError ).not.toHaveBeenCalled();
381
+
382
+ // Allow deferred validation to settle.
383
+ await act(
384
+ () => new Promise( ( resolve ) => setTimeout( resolve, 50 ) )
385
+ );
386
+ expect( errors ).toHaveLength( 0 );
387
+
388
+ cleanup();
194
389
  } );
195
390
 
196
391
  it( 'should throw when Dialog.Title is empty', async () => {
197
392
  const user = userEvent.setup();
198
- const onError = jest.fn();
393
+ const { errors, cleanup } = collectUncaughtErrors();
199
394
 
200
395
  render(
201
- <TestErrorBoundary onError={ onError }>
202
- <Dialog.Root>
203
- <Dialog.Trigger>Open Dialog</Dialog.Trigger>
204
- <Dialog.Popup>
205
- <Dialog.Header>
206
- { /* Empty title */ }
207
- <Dialog.Title />
208
- </Dialog.Header>
209
- <p>Content with empty title</p>
210
- <Dialog.Footer>
211
- <Dialog.Action>Close</Dialog.Action>
212
- </Dialog.Footer>
213
- </Dialog.Popup>
214
- </Dialog.Root>
215
- </TestErrorBoundary>
396
+ <Dialog.Root>
397
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
398
+ <Dialog.Popup>
399
+ <Dialog.Header>
400
+ <Dialog.Title />
401
+ </Dialog.Header>
402
+ <p>Content with empty title</p>
403
+ <Dialog.Footer>
404
+ <Dialog.Action>Close</Dialog.Action>
405
+ </Dialog.Footer>
406
+ </Dialog.Popup>
407
+ </Dialog.Root>
216
408
  );
217
409
 
218
- // Open the dialog - this will trigger the error
219
410
  await user.click(
220
411
  screen.getByRole( 'button', { name: 'Open Dialog' } )
221
412
  );
222
413
 
223
414
  await waitFor( () => {
224
- expect( onError ).toHaveBeenCalled();
415
+ expect( errors.length ).toBeGreaterThan( 0 );
225
416
  } );
226
417
 
227
- expect( onError.mock.calls[ 0 ][ 0 ] ).toBeInstanceOf( Error );
228
- expect( ( onError.mock.calls[ 0 ][ 0 ] as Error ).message ).toBe(
418
+ expect( errors[ 0 ].message ).toBe(
229
419
  'Dialog: <Dialog.Title> cannot be empty. ' +
230
420
  'Provide meaningful text content for the dialog title.'
231
421
  );
422
+
423
+ cleanup();
232
424
  } );
233
425
 
234
426
  it( 'should throw when Dialog.Title contains only whitespace', async () => {
235
427
  const user = userEvent.setup();
236
- const onError = jest.fn();
428
+ const { errors, cleanup } = collectUncaughtErrors();
237
429
 
238
430
  render(
239
- <TestErrorBoundary onError={ onError }>
240
- <Dialog.Root>
241
- <Dialog.Trigger>Open Dialog</Dialog.Trigger>
242
- <Dialog.Popup>
243
- <Dialog.Header>
244
- <Dialog.Title> </Dialog.Title>
245
- </Dialog.Header>
246
- <p>Content with whitespace-only title</p>
247
- <Dialog.Footer>
248
- <Dialog.Action>Close</Dialog.Action>
249
- </Dialog.Footer>
250
- </Dialog.Popup>
251
- </Dialog.Root>
252
- </TestErrorBoundary>
431
+ <Dialog.Root>
432
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
433
+ <Dialog.Popup>
434
+ <Dialog.Header>
435
+ <Dialog.Title> </Dialog.Title>
436
+ </Dialog.Header>
437
+ <p>Content with whitespace-only title</p>
438
+ <Dialog.Footer>
439
+ <Dialog.Action>Close</Dialog.Action>
440
+ </Dialog.Footer>
441
+ </Dialog.Popup>
442
+ </Dialog.Root>
253
443
  );
254
444
 
255
- // Open the dialog - this will trigger the error
256
445
  await user.click(
257
446
  screen.getByRole( 'button', { name: 'Open Dialog' } )
258
447
  );
259
448
 
260
449
  await waitFor( () => {
261
- expect( onError ).toHaveBeenCalled();
450
+ expect( errors.length ).toBeGreaterThan( 0 );
262
451
  } );
263
452
 
264
- expect( onError.mock.calls[ 0 ][ 0 ] ).toBeInstanceOf( Error );
265
- expect( ( onError.mock.calls[ 0 ][ 0 ] as Error ).message ).toBe(
453
+ expect( errors[ 0 ].message ).toBe(
266
454
  'Dialog: <Dialog.Title> cannot be empty. ' +
267
455
  'Provide meaningful text content for the dialog title.'
268
456
  );
457
+
458
+ cleanup();
269
459
  } );
270
460
 
271
461
  it( 'should not throw when Dialog.Title contains mixed content with text', async () => {
272
462
  const user = userEvent.setup();
273
- const onError = jest.fn();
463
+ const { errors, cleanup } = collectUncaughtErrors();
274
464
 
275
465
  render(
276
- <TestErrorBoundary onError={ onError }>
466
+ <Dialog.Root>
467
+ <Dialog.Trigger>Open Dialog</Dialog.Trigger>
468
+ <Dialog.Popup>
469
+ <Dialog.Header>
470
+ <Dialog.Title>
471
+ <span aria-hidden="true">🎉</span>
472
+ Settings
473
+ </Dialog.Title>
474
+ </Dialog.Header>
475
+ <p>Content with icon and text title</p>
476
+ <Dialog.Footer>
477
+ <Dialog.Action>Close</Dialog.Action>
478
+ </Dialog.Footer>
479
+ </Dialog.Popup>
480
+ </Dialog.Root>
481
+ );
482
+
483
+ await user.click(
484
+ screen.getByRole( 'button', { name: 'Open Dialog' } )
485
+ );
486
+
487
+ await waitFor( () => {
488
+ expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
489
+ } );
490
+
491
+ await act(
492
+ () => new Promise( ( resolve ) => setTimeout( resolve, 50 ) )
493
+ );
494
+ expect( errors ).toHaveLength( 0 );
495
+
496
+ cleanup();
497
+ } );
498
+
499
+ it( 'should throw when title is removed after mount', async () => {
500
+ const user = userEvent.setup();
501
+ const { errors, cleanup } = collectUncaughtErrors();
502
+
503
+ function Test() {
504
+ const [ showTitle, setShowTitle ] = useState( true );
505
+ return (
277
506
  <Dialog.Root>
278
- <Dialog.Trigger>Open Dialog</Dialog.Trigger>
507
+ <Dialog.Trigger>Open</Dialog.Trigger>
279
508
  <Dialog.Popup>
280
- <Dialog.Header>
281
- <Dialog.Title>
282
- <span aria-hidden="true">🎉</span>
283
- Settings
284
- </Dialog.Title>
285
- </Dialog.Header>
286
- <p>Content with icon and text title</p>
287
- <Dialog.Footer>
288
- <Dialog.Action>Close</Dialog.Action>
289
- </Dialog.Footer>
509
+ { showTitle && (
510
+ <Dialog.Title>My Title</Dialog.Title>
511
+ ) }
512
+ <button onClick={ () => setShowTitle( false ) }>
513
+ Remove Title
514
+ </button>
290
515
  </Dialog.Popup>
291
516
  </Dialog.Root>
292
- </TestErrorBoundary>
517
+ );
518
+ }
519
+
520
+ render( <Test /> );
521
+
522
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
523
+
524
+ await waitFor( () => {
525
+ expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
526
+ } );
527
+
528
+ // Let initial validation settle — no errors expected.
529
+ await act(
530
+ () => new Promise( ( resolve ) => setTimeout( resolve, 50 ) )
293
531
  );
532
+ expect( errors ).toHaveLength( 0 );
294
533
 
295
- // Open the dialog - should not throw
534
+ // Remove the title.
296
535
  await user.click(
297
- screen.getByRole( 'button', { name: 'Open Dialog' } )
536
+ screen.getByRole( 'button', { name: 'Remove Title' } )
298
537
  );
299
538
 
300
- // Wait for the dialog to appear and ensure validation does not trigger errors
539
+ await waitFor( () => {
540
+ expect( errors.length ).toBeGreaterThan( 0 );
541
+ } );
542
+
543
+ expect( errors[ 0 ].message ).toBe(
544
+ 'Dialog: Missing <Dialog.Title>. ' +
545
+ 'For accessibility, every dialog requires a title. ' +
546
+ 'If needed, the title can be visually hidden but must not be omitted.'
547
+ );
548
+
549
+ cleanup();
550
+ } );
551
+
552
+ it( 'should recover when title is added back', async () => {
553
+ const user = userEvent.setup();
554
+ const { errors, cleanup } = collectUncaughtErrors();
555
+
556
+ function Test() {
557
+ const [ showTitle, setShowTitle ] = useState( false );
558
+ return (
559
+ <Dialog.Root>
560
+ <Dialog.Trigger>Open</Dialog.Trigger>
561
+ <Dialog.Popup>
562
+ { showTitle && (
563
+ <Dialog.Title>My Title</Dialog.Title>
564
+ ) }
565
+ <button
566
+ onClick={ () => setShowTitle( ( s ) => ! s ) }
567
+ >
568
+ Toggle Title
569
+ </button>
570
+ </Dialog.Popup>
571
+ </Dialog.Root>
572
+ );
573
+ }
574
+
575
+ render( <Test /> );
576
+
577
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
578
+
301
579
  await waitFor( () => {
302
580
  expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
303
581
  } );
304
- expect( onError ).not.toHaveBeenCalled();
582
+
583
+ // Initially no title — should error.
584
+ await waitFor( () => {
585
+ expect( errors.length ).toBeGreaterThan( 0 );
586
+ } );
587
+
588
+ const errorCountAfterInitial = errors.length;
589
+
590
+ // Add the title back.
591
+ await user.click(
592
+ screen.getByRole( 'button', { name: 'Toggle Title' } )
593
+ );
594
+
595
+ // Wait for deferred validation to settle.
596
+ await act(
597
+ () => new Promise( ( resolve ) => setTimeout( resolve, 50 ) )
598
+ );
599
+
600
+ // No new errors should have been thrown.
601
+ expect( errors ).toHaveLength( errorCountAfterInitial );
602
+
603
+ cleanup();
305
604
  } );
306
605
  } );
307
606
 
@@ -422,8 +721,8 @@ describe( 'Dialog', () => {
422
721
  } );
423
722
  } );
424
723
 
425
- describe( 'container', () => {
426
- it( 'should render inside the container when provided', async () => {
724
+ describe( 'portal', () => {
725
+ it( 'should render inside the portal container when a custom target is provided', async () => {
427
726
  const user = userEvent.setup();
428
727
  const containerRef = createRef< HTMLDivElement >();
429
728
 
@@ -435,7 +734,11 @@ describe( 'Dialog', () => {
435
734
  ref={ containerRef }
436
735
  data-testid="custom-container"
437
736
  />
438
- <Dialog.Popup container={ containerRef }>
737
+ <Dialog.Popup
738
+ portal={
739
+ <Dialog.Portal container={ containerRef } />
740
+ }
741
+ >
439
742
  <Dialog.Header>
440
743
  <Dialog.Title>Title</Dialog.Title>
441
744
  </Dialog.Header>
@@ -482,4 +785,401 @@ describe( 'Dialog', () => {
482
785
  );
483
786
  } );
484
787
  } );
788
+
789
+ describe( 'overlay scroll container', () => {
790
+ it( 'marks Dialog.Content with data-wp-ui-overlay-scroll-container', async () => {
791
+ const user = userEvent.setup();
792
+ const contentRef = createRef< HTMLDivElement >();
793
+
794
+ render(
795
+ <Dialog.Root>
796
+ <Dialog.Trigger>Open</Dialog.Trigger>
797
+ <Dialog.Popup>
798
+ <Dialog.Title>Title</Dialog.Title>
799
+ <Dialog.Content ref={ contentRef }>
800
+ <p>Body</p>
801
+ </Dialog.Content>
802
+ </Dialog.Popup>
803
+ </Dialog.Root>
804
+ );
805
+
806
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
807
+ await waitFor( () => {
808
+ expect( contentRef.current ).toBeInstanceOf( HTMLDivElement );
809
+ } );
810
+
811
+ expect( contentRef.current ).toHaveAttribute(
812
+ 'data-wp-ui-overlay-scroll-container'
813
+ );
814
+ } );
815
+
816
+ it( 'sets data-wp-ui-overlay-modal on the popup when modal is true', async () => {
817
+ const user = userEvent.setup();
818
+ const popupRef = createRef< HTMLDivElement >();
819
+
820
+ render(
821
+ <Dialog.Root modal>
822
+ <Dialog.Trigger>Open</Dialog.Trigger>
823
+ <Dialog.Popup ref={ popupRef }>
824
+ <Dialog.Title>Title</Dialog.Title>
825
+ </Dialog.Popup>
826
+ </Dialog.Root>
827
+ );
828
+
829
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
830
+ await waitFor( () => {
831
+ expect( popupRef.current ).toBeInstanceOf( HTMLDivElement );
832
+ } );
833
+
834
+ expect( popupRef.current ).toHaveAttribute(
835
+ 'data-wp-ui-overlay-modal'
836
+ );
837
+ } );
838
+
839
+ it.each( [
840
+ [ 'false', false as const ],
841
+ [ 'trap-focus', 'trap-focus' as const ],
842
+ ] )(
843
+ 'omits data-wp-ui-overlay-modal on the popup when modal is %s',
844
+ async ( _label, modal ) => {
845
+ const user = userEvent.setup();
846
+ const popupRef = createRef< HTMLDivElement >();
847
+
848
+ render(
849
+ <Dialog.Root modal={ modal }>
850
+ <Dialog.Trigger>Open</Dialog.Trigger>
851
+ <Dialog.Popup ref={ popupRef }>
852
+ <Dialog.Title>Title</Dialog.Title>
853
+ </Dialog.Popup>
854
+ </Dialog.Root>
855
+ );
856
+
857
+ await user.click(
858
+ screen.getByRole( 'button', { name: 'Open' } )
859
+ );
860
+ await waitFor( () => {
861
+ expect( popupRef.current ).toBeInstanceOf( HTMLDivElement );
862
+ } );
863
+
864
+ expect( popupRef.current ).not.toHaveAttribute(
865
+ 'data-wp-ui-overlay-modal'
866
+ );
867
+ }
868
+ );
869
+
870
+ it( 'pins Dialog.Header when rendered as a sibling of Dialog.Content', async () => {
871
+ const user = userEvent.setup();
872
+ const popupRef = createRef< HTMLDivElement >();
873
+ const headerRef = createRef< HTMLElement >();
874
+ const contentRef = createRef< HTMLDivElement >();
875
+
876
+ render(
877
+ <Dialog.Root>
878
+ <Dialog.Trigger>Open</Dialog.Trigger>
879
+ <Dialog.Popup ref={ popupRef }>
880
+ <Dialog.Header ref={ headerRef }>
881
+ <Dialog.Title>Title</Dialog.Title>
882
+ </Dialog.Header>
883
+ <Dialog.Content ref={ contentRef }>
884
+ <p>Body</p>
885
+ </Dialog.Content>
886
+ </Dialog.Popup>
887
+ </Dialog.Root>
888
+ );
889
+
890
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
891
+ await waitFor( () => {
892
+ expect( headerRef.current ).toBeInstanceOf( HTMLElement );
893
+ } );
894
+
895
+ // The header is inside the popup but NOT inside the scroll
896
+ // container — it sits outside the scrolling region as a
897
+ // pinned flex sibling of `Content`.
898
+ expect( popupRef.current ).toContainElement( headerRef.current );
899
+ expect( popupRef.current ).toContainElement( contentRef.current );
900
+ expect( contentRef.current ).not.toContainElement(
901
+ headerRef.current
902
+ );
903
+ // And it sits *before* the scroll container — the CSS
904
+ // sticky-separator selectors rely on that DOM order.
905
+ const position = headerRef.current!.compareDocumentPosition(
906
+ contentRef.current!
907
+ );
908
+ expect(
909
+ // eslint-disable-next-line no-bitwise
910
+ position & Node.DOCUMENT_POSITION_FOLLOWING
911
+ ).toBeTruthy();
912
+ } );
913
+
914
+ it( 'scrolls Dialog.Header with the body when nested inside Dialog.Content', async () => {
915
+ const user = userEvent.setup();
916
+ const headerRef = createRef< HTMLElement >();
917
+ const contentRef = createRef< HTMLDivElement >();
918
+
919
+ render(
920
+ <Dialog.Root>
921
+ <Dialog.Trigger>Open</Dialog.Trigger>
922
+ <Dialog.Popup>
923
+ <Dialog.Content ref={ contentRef }>
924
+ <Dialog.Header ref={ headerRef }>
925
+ <Dialog.Title>Title</Dialog.Title>
926
+ </Dialog.Header>
927
+ <p>Body</p>
928
+ </Dialog.Content>
929
+ </Dialog.Popup>
930
+ </Dialog.Root>
931
+ );
932
+
933
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
934
+ await waitFor( () => {
935
+ expect( headerRef.current ).toBeInstanceOf( HTMLElement );
936
+ } );
937
+
938
+ expect( contentRef.current ).toContainElement( headerRef.current );
939
+ } );
940
+
941
+ it( 'invokes a consumer-supplied onScroll on Dialog.Content', async () => {
942
+ const user = userEvent.setup();
943
+ const onScroll = jest.fn();
944
+ const contentRef = createRef< HTMLDivElement >();
945
+
946
+ render(
947
+ <Dialog.Root>
948
+ <Dialog.Trigger>Open</Dialog.Trigger>
949
+ <Dialog.Popup>
950
+ <Dialog.Title>Title</Dialog.Title>
951
+ <Dialog.Content
952
+ ref={ contentRef }
953
+ onScroll={ onScroll }
954
+ >
955
+ <p>Body</p>
956
+ </Dialog.Content>
957
+ </Dialog.Popup>
958
+ </Dialog.Root>
959
+ );
960
+
961
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
962
+ await waitFor( () => {
963
+ expect( contentRef.current ).toBeInstanceOf( HTMLDivElement );
964
+ } );
965
+
966
+ act( () => {
967
+ contentRef.current?.dispatchEvent(
968
+ new Event( 'scroll', { bubbles: true } )
969
+ );
970
+ } );
971
+
972
+ expect( onScroll ).toHaveBeenCalledTimes( 1 );
973
+ } );
974
+
975
+ it( 'toggles tabindex="0" on Dialog.Content based on overflow', async () => {
976
+ const user = userEvent.setup();
977
+ const contentRef = createRef< HTMLDivElement >();
978
+
979
+ render(
980
+ <Dialog.Root>
981
+ <Dialog.Trigger>Open</Dialog.Trigger>
982
+ <Dialog.Popup>
983
+ <Dialog.Title>Title</Dialog.Title>
984
+ <Dialog.Content ref={ contentRef }>
985
+ <p>Body</p>
986
+ </Dialog.Content>
987
+ </Dialog.Popup>
988
+ </Dialog.Root>
989
+ );
990
+
991
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
992
+ await waitFor( () => {
993
+ expect( contentRef.current ).toBeInstanceOf( HTMLDivElement );
994
+ } );
995
+
996
+ // JSDOM reports 0/0 dimensions by default, so the initial
997
+ // mount sees no overflow and installs no tabindex. Stub
998
+ // layout metrics, dispatch a scroll to re-run the update,
999
+ // and verify the tabindex is installed.
1000
+ const content = contentRef.current!;
1001
+ Object.defineProperty( content, 'scrollHeight', {
1002
+ configurable: true,
1003
+ value: 500,
1004
+ } );
1005
+ Object.defineProperty( content, 'clientHeight', {
1006
+ configurable: true,
1007
+ value: 100,
1008
+ } );
1009
+ Object.defineProperty( content, 'scrollTop', {
1010
+ configurable: true,
1011
+ value: 0,
1012
+ } );
1013
+
1014
+ act( () => {
1015
+ content.dispatchEvent(
1016
+ new Event( 'scroll', { bubbles: true } )
1017
+ );
1018
+ } );
1019
+
1020
+ expect( content ).toHaveAttribute( 'tabindex', '0' );
1021
+
1022
+ // Shrink content so it no longer overflows and verify the
1023
+ // hook removes its managed tabindex.
1024
+ Object.defineProperty( content, 'scrollHeight', {
1025
+ configurable: true,
1026
+ value: 100,
1027
+ } );
1028
+
1029
+ act( () => {
1030
+ content.dispatchEvent(
1031
+ new Event( 'scroll', { bubbles: true } )
1032
+ );
1033
+ } );
1034
+
1035
+ expect( content ).not.toHaveAttribute( 'tabindex' );
1036
+ } );
1037
+
1038
+ // This test exercises the `updateScrollAttributes` path for
1039
+ // consumer takeover (overflow flips off while the override is
1040
+ // in place). The matching `cleanupScrollAttributes` path —
1041
+ // popup unmounts while the override is in place — is covered
1042
+ // transitively because both paths share a single
1043
+ // `reconcileTabbableFlag` helper inside the hook. If that
1044
+ // shared helper is ever inlined or split, add an explicit
1045
+ // unmount-after-takeover test to keep both paths regressions-
1046
+ // guarded.
1047
+ it( 'preserves a consumer-supplied tabindex set after the hook installed its own', async () => {
1048
+ const user = userEvent.setup();
1049
+ const contentRef = createRef< HTMLDivElement >();
1050
+
1051
+ render(
1052
+ <Dialog.Root>
1053
+ <Dialog.Trigger>Open</Dialog.Trigger>
1054
+ <Dialog.Popup>
1055
+ <Dialog.Title>Title</Dialog.Title>
1056
+ <Dialog.Content ref={ contentRef }>
1057
+ <p>Body</p>
1058
+ </Dialog.Content>
1059
+ </Dialog.Popup>
1060
+ </Dialog.Root>
1061
+ );
1062
+
1063
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
1064
+ await waitFor( () => {
1065
+ expect( contentRef.current ).toBeInstanceOf( HTMLDivElement );
1066
+ } );
1067
+
1068
+ const content = contentRef.current!;
1069
+ Object.defineProperty( content, 'scrollHeight', {
1070
+ configurable: true,
1071
+ value: 500,
1072
+ } );
1073
+ Object.defineProperty( content, 'clientHeight', {
1074
+ configurable: true,
1075
+ value: 100,
1076
+ } );
1077
+ Object.defineProperty( content, 'scrollTop', {
1078
+ configurable: true,
1079
+ value: 0,
1080
+ } );
1081
+
1082
+ act( () => {
1083
+ content.dispatchEvent(
1084
+ new Event( 'scroll', { bubbles: true } )
1085
+ );
1086
+ } );
1087
+
1088
+ expect( content ).toHaveAttribute( 'tabindex', '0' );
1089
+
1090
+ // Simulate the consumer taking over the tabindex after the
1091
+ // hook installed its own (e.g. a re-render with an explicit
1092
+ // `tabIndex={ -1 }` prop). The hook should detect the
1093
+ // consumer takeover and not clobber that value on the next
1094
+ // non-overflow tick.
1095
+ content.setAttribute( 'tabindex', '-1' );
1096
+
1097
+ Object.defineProperty( content, 'scrollHeight', {
1098
+ configurable: true,
1099
+ value: 100,
1100
+ } );
1101
+
1102
+ act( () => {
1103
+ content.dispatchEvent(
1104
+ new Event( 'scroll', { bubbles: true } )
1105
+ );
1106
+ } );
1107
+
1108
+ expect( content ).toHaveAttribute( 'tabindex', '-1' );
1109
+ } );
1110
+
1111
+ it( 'toggles data-wp-ui-overlay-scrolled-from-* based on scroll position', async () => {
1112
+ const user = userEvent.setup();
1113
+ const contentRef = createRef< HTMLDivElement >();
1114
+
1115
+ render(
1116
+ <Dialog.Root>
1117
+ <Dialog.Trigger>Open</Dialog.Trigger>
1118
+ <Dialog.Popup>
1119
+ <Dialog.Title>Title</Dialog.Title>
1120
+ <Dialog.Content ref={ contentRef }>
1121
+ <p>Body</p>
1122
+ </Dialog.Content>
1123
+ </Dialog.Popup>
1124
+ </Dialog.Root>
1125
+ );
1126
+
1127
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
1128
+ await waitFor( () => {
1129
+ expect( contentRef.current ).toBeInstanceOf( HTMLDivElement );
1130
+ } );
1131
+
1132
+ // JSDOM doesn't lay out elements, so we simulate an
1133
+ // overflowing scroll container by stubbing layout metrics
1134
+ // per scenario and dispatching a scroll event.
1135
+ const content = contentRef.current!;
1136
+ Object.defineProperty( content, 'scrollHeight', {
1137
+ configurable: true,
1138
+ value: 500,
1139
+ } );
1140
+ Object.defineProperty( content, 'clientHeight', {
1141
+ configurable: true,
1142
+ value: 100,
1143
+ } );
1144
+
1145
+ const setScrollTop = ( value: number ) => {
1146
+ Object.defineProperty( content, 'scrollTop', {
1147
+ configurable: true,
1148
+ value,
1149
+ } );
1150
+ act( () => {
1151
+ content.dispatchEvent(
1152
+ new Event( 'scroll', { bubbles: true } )
1153
+ );
1154
+ } );
1155
+ };
1156
+
1157
+ // At the top: only "from-bottom" is set (content below).
1158
+ setScrollTop( 0 );
1159
+ expect( content ).not.toHaveAttribute(
1160
+ 'data-wp-ui-overlay-scrolled-from-top'
1161
+ );
1162
+ expect( content ).toHaveAttribute(
1163
+ 'data-wp-ui-overlay-scrolled-from-bottom'
1164
+ );
1165
+
1166
+ // In the middle: both are set.
1167
+ setScrollTop( 200 );
1168
+ expect( content ).toHaveAttribute(
1169
+ 'data-wp-ui-overlay-scrolled-from-top'
1170
+ );
1171
+ expect( content ).toHaveAttribute(
1172
+ 'data-wp-ui-overlay-scrolled-from-bottom'
1173
+ );
1174
+
1175
+ // At the bottom: only "from-top" is set (content above).
1176
+ setScrollTop( 400 );
1177
+ expect( content ).toHaveAttribute(
1178
+ 'data-wp-ui-overlay-scrolled-from-top'
1179
+ );
1180
+ expect( content ).not.toHaveAttribute(
1181
+ 'data-wp-ui-overlay-scrolled-from-bottom'
1182
+ );
1183
+ } );
1184
+ } );
485
1185
  } );