@wordpress/ui 0.9.1-next.v.202603161435.0 → 0.11.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 (655) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/CONTRIBUTING.md +180 -0
  3. package/README.md +34 -6
  4. package/build/alert-dialog/context.cjs +39 -0
  5. package/build/alert-dialog/context.cjs.map +7 -0
  6. package/build/alert-dialog/index.cjs +37 -0
  7. package/build/alert-dialog/index.cjs.map +7 -0
  8. package/build/alert-dialog/popup.cjs +165 -0
  9. package/build/alert-dialog/popup.cjs.map +7 -0
  10. package/build/alert-dialog/root.cjs +152 -0
  11. package/build/alert-dialog/root.cjs.map +7 -0
  12. package/build/alert-dialog/trigger.cjs +38 -0
  13. package/build/alert-dialog/trigger.cjs.map +7 -0
  14. package/build/alert-dialog/types.cjs +19 -0
  15. package/build/alert-dialog/types.cjs.map +7 -0
  16. package/build/badge/badge.cjs +14 -14
  17. package/build/badge/badge.cjs.map +2 -2
  18. package/build/button/button.cjs +18 -8
  19. package/build/button/button.cjs.map +3 -3
  20. package/build/card/content.cjs +4 -4
  21. package/build/card/content.cjs.map +2 -2
  22. package/build/card/full-bleed.cjs +4 -4
  23. package/build/card/full-bleed.cjs.map +2 -2
  24. package/build/card/header.cjs +4 -4
  25. package/build/card/header.cjs.map +2 -2
  26. package/build/card/root.cjs +6 -6
  27. package/build/card/root.cjs.map +2 -2
  28. package/build/card/title.cjs +14 -21
  29. package/build/card/title.cjs.map +3 -3
  30. package/build/collapsible-card/content.cjs +24 -3
  31. package/build/collapsible-card/content.cjs.map +4 -4
  32. package/build/collapsible-card/context.cjs +35 -0
  33. package/build/collapsible-card/context.cjs.map +7 -0
  34. package/build/collapsible-card/header-description.cjs +52 -0
  35. package/build/collapsible-card/header-description.cjs.map +7 -0
  36. package/build/collapsible-card/header.cjs +39 -18
  37. package/build/collapsible-card/header.cjs.map +2 -2
  38. package/build/collapsible-card/index.cjs +3 -0
  39. package/build/collapsible-card/index.cjs.map +2 -2
  40. package/build/collapsible-card/types.cjs.map +1 -1
  41. package/build/dialog/action.cjs +4 -2
  42. package/build/dialog/action.cjs.map +2 -2
  43. package/build/dialog/close-icon.cjs +2 -1
  44. package/build/dialog/close-icon.cjs.map +2 -2
  45. package/build/dialog/footer.cjs +3 -3
  46. package/build/dialog/footer.cjs.map +2 -2
  47. package/build/dialog/header.cjs +3 -3
  48. package/build/dialog/header.cjs.map +2 -2
  49. package/build/dialog/popup.cjs +24 -6
  50. package/build/dialog/popup.cjs.map +2 -2
  51. package/build/dialog/title.cjs +10 -19
  52. package/build/dialog/title.cjs.map +3 -3
  53. package/build/dialog/types.cjs.map +1 -1
  54. package/build/empty-state/actions.cjs +66 -0
  55. package/build/empty-state/actions.cjs.map +7 -0
  56. package/build/empty-state/description.cjs +69 -0
  57. package/build/empty-state/description.cjs.map +7 -0
  58. package/build/empty-state/icon.cjs +69 -0
  59. package/build/empty-state/icon.cjs.map +7 -0
  60. package/build/empty-state/index.cjs +46 -0
  61. package/build/empty-state/index.cjs.map +7 -0
  62. package/build/empty-state/root.cjs +66 -0
  63. package/build/empty-state/root.cjs.map +7 -0
  64. package/build/empty-state/title.cjs +71 -0
  65. package/build/empty-state/title.cjs.map +7 -0
  66. package/build/empty-state/types.cjs +19 -0
  67. package/build/empty-state/types.cjs.map +7 -0
  68. package/build/empty-state/visual.cjs +66 -0
  69. package/build/empty-state/visual.cjs.map +7 -0
  70. package/build/form/index.cjs +27 -0
  71. package/build/form/index.cjs.map +7 -0
  72. package/build/form/input-control/index.cjs +31 -0
  73. package/build/form/input-control/index.cjs.map +7 -0
  74. package/build/form/input-control/input-control.cjs +50 -0
  75. package/build/form/input-control/input-control.cjs.map +7 -0
  76. package/build/form/input-control/types.cjs +19 -0
  77. package/build/form/input-control/types.cjs.map +7 -0
  78. package/build/form/primitives/field/description.cjs +17 -4
  79. package/build/form/primitives/field/description.cjs.map +3 -3
  80. package/build/form/primitives/field/details.cjs +4 -4
  81. package/build/form/primitives/field/details.cjs.map +2 -2
  82. package/build/form/primitives/field/label.cjs +8 -8
  83. package/build/form/primitives/field/label.cjs.map +2 -2
  84. package/build/form/primitives/field/root.cjs +2 -2
  85. package/build/form/primitives/field/root.cjs.map +1 -1
  86. package/build/form/primitives/fieldset/description.cjs +20 -4
  87. package/build/form/primitives/fieldset/description.cjs.map +3 -3
  88. package/build/form/primitives/fieldset/details.cjs +3 -3
  89. package/build/form/primitives/fieldset/details.cjs.map +2 -2
  90. package/build/form/primitives/fieldset/legend.cjs +8 -7
  91. package/build/form/primitives/fieldset/legend.cjs.map +2 -2
  92. package/build/form/primitives/fieldset/root.cjs +2 -2
  93. package/build/form/primitives/fieldset/root.cjs.map +1 -1
  94. package/build/form/primitives/input/input.cjs +23 -7
  95. package/build/form/primitives/input/input.cjs.map +3 -3
  96. package/build/form/primitives/input-layout/input-layout.cjs +15 -5
  97. package/build/form/primitives/input-layout/input-layout.cjs.map +3 -3
  98. package/build/form/primitives/input-layout/slot.cjs +6 -5
  99. package/build/form/primitives/input-layout/slot.cjs.map +2 -2
  100. package/build/form/primitives/select/item.cjs +5 -5
  101. package/build/form/primitives/select/item.cjs.map +2 -2
  102. package/build/form/primitives/select/popup.cjs +9 -9
  103. package/build/form/primitives/select/popup.cjs.map +2 -2
  104. package/build/form/primitives/select/trigger.cjs +6 -6
  105. package/build/form/primitives/select/trigger.cjs.map +2 -2
  106. package/build/form/primitives/select/types.cjs.map +1 -1
  107. package/build/form/primitives/textarea/textarea.cjs +23 -4
  108. package/build/form/primitives/textarea/textarea.cjs.map +3 -3
  109. package/build/form/types.cjs +19 -0
  110. package/build/form/types.cjs.map +7 -0
  111. package/build/icon-button/icon-button.cjs +2 -2
  112. package/build/icon-button/icon-button.cjs.map +1 -1
  113. package/build/index.cjs +11 -2
  114. package/build/index.cjs.map +2 -2
  115. package/build/link/link.cjs +18 -8
  116. package/build/link/link.cjs.map +3 -3
  117. package/build/notice/action-button.cjs +3 -3
  118. package/build/notice/action-button.cjs.map +2 -2
  119. package/build/notice/action-link.cjs +3 -3
  120. package/build/notice/action-link.cjs.map +2 -2
  121. package/build/notice/actions.cjs +3 -3
  122. package/build/notice/actions.cjs.map +2 -2
  123. package/build/notice/close-icon.cjs +3 -3
  124. package/build/notice/close-icon.cjs.map +2 -2
  125. package/build/notice/description.cjs +3 -3
  126. package/build/notice/description.cjs.map +2 -2
  127. package/build/notice/index.cjs.map +1 -1
  128. package/build/notice/root.cjs +5 -5
  129. package/build/notice/root.cjs.map +2 -2
  130. package/build/notice/title.cjs +3 -3
  131. package/build/notice/title.cjs.map +2 -2
  132. package/build/popover/arrow.cjs +94 -0
  133. package/build/popover/arrow.cjs.map +7 -0
  134. package/build/popover/close.cjs +45 -0
  135. package/build/popover/close.cjs.map +7 -0
  136. package/build/popover/context.cjs +76 -0
  137. package/build/popover/context.cjs.map +7 -0
  138. package/build/popover/description.cjs +70 -0
  139. package/build/popover/description.cjs.map +7 -0
  140. package/build/popover/index.cjs +49 -0
  141. package/build/popover/index.cjs.map +7 -0
  142. package/build/popover/popup.cjs +138 -0
  143. package/build/popover/popup.cjs.map +7 -0
  144. package/build/popover/root.cjs +35 -0
  145. package/build/popover/root.cjs.map +7 -0
  146. package/build/popover/title.cjs +56 -0
  147. package/build/popover/title.cjs.map +7 -0
  148. package/build/popover/trigger.cjs +38 -0
  149. package/build/popover/trigger.cjs.map +7 -0
  150. package/build/popover/types.cjs +19 -0
  151. package/build/popover/types.cjs.map +7 -0
  152. package/build/stack/stack.cjs +2 -2
  153. package/build/stack/stack.cjs.map +1 -1
  154. package/build/tabs/context.cjs +121 -0
  155. package/build/tabs/context.cjs.map +7 -0
  156. package/build/tabs/list.cjs +3 -4
  157. package/build/tabs/list.cjs.map +2 -2
  158. package/build/tabs/panel.cjs +5 -3
  159. package/build/tabs/panel.cjs.map +2 -2
  160. package/build/tabs/root.cjs +2 -1
  161. package/build/tabs/root.cjs.map +2 -2
  162. package/build/tabs/tab.cjs +5 -3
  163. package/build/tabs/tab.cjs.map +2 -2
  164. package/build/text/text.cjs +20 -5
  165. package/build/text/text.cjs.map +3 -3
  166. package/build/tooltip/popup.cjs +7 -6
  167. package/build/tooltip/popup.cjs.map +2 -2
  168. package/build/tooltip/root.cjs.map +2 -2
  169. package/build/tooltip/types.cjs.map +1 -1
  170. package/build/utils/types.cjs.map +1 -1
  171. package/build/utils/use-deprioritized-initial-focus.cjs +64 -0
  172. package/build/utils/use-deprioritized-initial-focus.cjs.map +7 -0
  173. package/build/visually-hidden/visually-hidden.cjs +2 -2
  174. package/build/visually-hidden/visually-hidden.cjs.map +2 -2
  175. package/build-module/alert-dialog/context.mjs +14 -0
  176. package/build-module/alert-dialog/context.mjs.map +7 -0
  177. package/build-module/alert-dialog/index.mjs +10 -0
  178. package/build-module/alert-dialog/index.mjs.map +7 -0
  179. package/build-module/alert-dialog/popup.mjs +132 -0
  180. package/build-module/alert-dialog/popup.mjs.map +7 -0
  181. package/build-module/alert-dialog/root.mjs +133 -0
  182. package/build-module/alert-dialog/root.mjs.map +7 -0
  183. package/build-module/alert-dialog/trigger.mjs +13 -0
  184. package/build-module/alert-dialog/trigger.mjs.map +7 -0
  185. package/build-module/alert-dialog/types.mjs +1 -0
  186. package/build-module/alert-dialog/types.mjs.map +7 -0
  187. package/build-module/badge/badge.mjs +14 -14
  188. package/build-module/badge/badge.mjs.map +2 -2
  189. package/build-module/button/button.mjs +18 -8
  190. package/build-module/button/button.mjs.map +3 -3
  191. package/build-module/card/content.mjs +4 -4
  192. package/build-module/card/content.mjs.map +2 -2
  193. package/build-module/card/full-bleed.mjs +4 -4
  194. package/build-module/card/full-bleed.mjs.map +2 -2
  195. package/build-module/card/header.mjs +4 -4
  196. package/build-module/card/header.mjs.map +2 -2
  197. package/build-module/card/root.mjs +6 -6
  198. package/build-module/card/root.mjs.map +2 -2
  199. package/build-module/card/title.mjs +14 -21
  200. package/build-module/card/title.mjs.map +3 -3
  201. package/build-module/collapsible-card/content.mjs +24 -3
  202. package/build-module/collapsible-card/content.mjs.map +3 -3
  203. package/build-module/collapsible-card/context.mjs +10 -0
  204. package/build-module/collapsible-card/context.mjs.map +7 -0
  205. package/build-module/collapsible-card/header-description.mjs +27 -0
  206. package/build-module/collapsible-card/header-description.mjs.map +7 -0
  207. package/build-module/collapsible-card/header.mjs +40 -19
  208. package/build-module/collapsible-card/header.mjs.map +2 -2
  209. package/build-module/collapsible-card/index.mjs +2 -0
  210. package/build-module/collapsible-card/index.mjs.map +2 -2
  211. package/build-module/dialog/action.mjs +4 -2
  212. package/build-module/dialog/action.mjs.map +2 -2
  213. package/build-module/dialog/close-icon.mjs +2 -1
  214. package/build-module/dialog/close-icon.mjs.map +2 -2
  215. package/build-module/dialog/footer.mjs +3 -3
  216. package/build-module/dialog/footer.mjs.map +2 -2
  217. package/build-module/dialog/header.mjs +3 -3
  218. package/build-module/dialog/header.mjs.map +2 -2
  219. package/build-module/dialog/popup.mjs +24 -6
  220. package/build-module/dialog/popup.mjs.map +2 -2
  221. package/build-module/dialog/title.mjs +10 -9
  222. package/build-module/dialog/title.mjs.map +2 -2
  223. package/build-module/empty-state/actions.mjs +31 -0
  224. package/build-module/empty-state/actions.mjs.map +7 -0
  225. package/build-module/empty-state/description.mjs +34 -0
  226. package/build-module/empty-state/description.mjs.map +7 -0
  227. package/build-module/empty-state/icon.mjs +34 -0
  228. package/build-module/empty-state/icon.mjs.map +7 -0
  229. package/build-module/empty-state/index.mjs +16 -0
  230. package/build-module/empty-state/index.mjs.map +7 -0
  231. package/build-module/empty-state/root.mjs +31 -0
  232. package/build-module/empty-state/root.mjs.map +7 -0
  233. package/build-module/empty-state/title.mjs +36 -0
  234. package/build-module/empty-state/title.mjs.map +7 -0
  235. package/build-module/empty-state/types.mjs +1 -0
  236. package/build-module/empty-state/types.mjs.map +7 -0
  237. package/build-module/empty-state/visual.mjs +31 -0
  238. package/build-module/empty-state/visual.mjs.map +7 -0
  239. package/build-module/form/index.mjs +4 -0
  240. package/build-module/form/index.mjs.map +7 -0
  241. package/build-module/form/input-control/index.mjs +6 -0
  242. package/build-module/form/input-control/index.mjs.map +7 -0
  243. package/build-module/form/input-control/input-control.mjs +25 -0
  244. package/build-module/form/input-control/input-control.mjs.map +7 -0
  245. package/build-module/form/input-control/types.mjs +1 -0
  246. package/build-module/form/input-control/types.mjs.map +7 -0
  247. package/build-module/form/primitives/field/description.mjs +17 -4
  248. package/build-module/form/primitives/field/description.mjs.map +3 -3
  249. package/build-module/form/primitives/field/details.mjs +4 -4
  250. package/build-module/form/primitives/field/details.mjs.map +2 -2
  251. package/build-module/form/primitives/field/label.mjs +8 -8
  252. package/build-module/form/primitives/field/label.mjs.map +2 -2
  253. package/build-module/form/primitives/field/root.mjs +2 -2
  254. package/build-module/form/primitives/field/root.mjs.map +1 -1
  255. package/build-module/form/primitives/fieldset/description.mjs +20 -4
  256. package/build-module/form/primitives/fieldset/description.mjs.map +3 -3
  257. package/build-module/form/primitives/fieldset/details.mjs +3 -3
  258. package/build-module/form/primitives/fieldset/details.mjs.map +2 -2
  259. package/build-module/form/primitives/fieldset/legend.mjs +8 -7
  260. package/build-module/form/primitives/fieldset/legend.mjs.map +2 -2
  261. package/build-module/form/primitives/fieldset/root.mjs +2 -2
  262. package/build-module/form/primitives/fieldset/root.mjs.map +1 -1
  263. package/build-module/form/primitives/input/input.mjs +23 -7
  264. package/build-module/form/primitives/input/input.mjs.map +3 -3
  265. package/build-module/form/primitives/input-layout/input-layout.mjs +15 -5
  266. package/build-module/form/primitives/input-layout/input-layout.mjs.map +3 -3
  267. package/build-module/form/primitives/input-layout/slot.mjs +6 -5
  268. package/build-module/form/primitives/input-layout/slot.mjs.map +2 -2
  269. package/build-module/form/primitives/select/item.mjs +5 -5
  270. package/build-module/form/primitives/select/item.mjs.map +2 -2
  271. package/build-module/form/primitives/select/popup.mjs +9 -9
  272. package/build-module/form/primitives/select/popup.mjs.map +2 -2
  273. package/build-module/form/primitives/select/trigger.mjs +6 -6
  274. package/build-module/form/primitives/select/trigger.mjs.map +2 -2
  275. package/build-module/form/primitives/textarea/textarea.mjs +23 -4
  276. package/build-module/form/primitives/textarea/textarea.mjs.map +3 -3
  277. package/build-module/form/types.mjs +1 -0
  278. package/build-module/form/types.mjs.map +7 -0
  279. package/build-module/icon-button/icon-button.mjs +2 -2
  280. package/build-module/icon-button/icon-button.mjs.map +1 -1
  281. package/build-module/index.mjs +7 -1
  282. package/build-module/index.mjs.map +2 -2
  283. package/build-module/link/link.mjs +18 -8
  284. package/build-module/link/link.mjs.map +3 -3
  285. package/build-module/notice/action-button.mjs +3 -3
  286. package/build-module/notice/action-button.mjs.map +2 -2
  287. package/build-module/notice/action-link.mjs +3 -3
  288. package/build-module/notice/action-link.mjs.map +2 -2
  289. package/build-module/notice/actions.mjs +3 -3
  290. package/build-module/notice/actions.mjs.map +2 -2
  291. package/build-module/notice/close-icon.mjs +3 -3
  292. package/build-module/notice/close-icon.mjs.map +2 -2
  293. package/build-module/notice/description.mjs +3 -3
  294. package/build-module/notice/description.mjs.map +2 -2
  295. package/build-module/notice/index.mjs.map +1 -1
  296. package/build-module/notice/root.mjs +5 -5
  297. package/build-module/notice/root.mjs.map +2 -2
  298. package/build-module/notice/title.mjs +3 -3
  299. package/build-module/notice/title.mjs.map +2 -2
  300. package/build-module/popover/arrow.mjs +59 -0
  301. package/build-module/popover/arrow.mjs.map +7 -0
  302. package/build-module/popover/close.mjs +20 -0
  303. package/build-module/popover/close.mjs.map +7 -0
  304. package/build-module/popover/context.mjs +57 -0
  305. package/build-module/popover/context.mjs.map +7 -0
  306. package/build-module/popover/description.mjs +35 -0
  307. package/build-module/popover/description.mjs.map +7 -0
  308. package/build-module/popover/index.mjs +18 -0
  309. package/build-module/popover/index.mjs.map +7 -0
  310. package/build-module/popover/popup.mjs +105 -0
  311. package/build-module/popover/popup.mjs.map +7 -0
  312. package/build-module/popover/root.mjs +10 -0
  313. package/build-module/popover/root.mjs.map +7 -0
  314. package/build-module/popover/title.mjs +31 -0
  315. package/build-module/popover/title.mjs.map +7 -0
  316. package/build-module/popover/trigger.mjs +13 -0
  317. package/build-module/popover/trigger.mjs.map +7 -0
  318. package/build-module/popover/types.mjs +1 -0
  319. package/build-module/popover/types.mjs.map +7 -0
  320. package/build-module/stack/stack.mjs +2 -2
  321. package/build-module/stack/stack.mjs.map +1 -1
  322. package/build-module/tabs/context.mjs +101 -0
  323. package/build-module/tabs/context.mjs.map +7 -0
  324. package/build-module/tabs/list.mjs +3 -4
  325. package/build-module/tabs/list.mjs.map +2 -2
  326. package/build-module/tabs/panel.mjs +5 -3
  327. package/build-module/tabs/panel.mjs.map +2 -2
  328. package/build-module/tabs/root.mjs +2 -1
  329. package/build-module/tabs/root.mjs.map +2 -2
  330. package/build-module/tabs/tab.mjs +5 -3
  331. package/build-module/tabs/tab.mjs.map +2 -2
  332. package/build-module/text/text.mjs +20 -5
  333. package/build-module/text/text.mjs.map +3 -3
  334. package/build-module/tooltip/popup.mjs +7 -6
  335. package/build-module/tooltip/popup.mjs.map +2 -2
  336. package/build-module/tooltip/root.mjs.map +2 -2
  337. package/build-module/utils/use-deprioritized-initial-focus.mjs +39 -0
  338. package/build-module/utils/use-deprioritized-initial-focus.mjs.map +7 -0
  339. package/build-module/visually-hidden/visually-hidden.mjs +2 -2
  340. package/build-module/visually-hidden/visually-hidden.mjs.map +2 -2
  341. package/build-types/alert-dialog/context.d.ts +11 -0
  342. package/build-types/alert-dialog/context.d.ts.map +1 -0
  343. package/build-types/alert-dialog/index.d.ts +4 -0
  344. package/build-types/alert-dialog/index.d.ts.map +1 -0
  345. package/build-types/alert-dialog/popup.d.ts +4 -0
  346. package/build-types/alert-dialog/popup.d.ts.map +1 -0
  347. package/build-types/alert-dialog/root.d.ts +18 -0
  348. package/build-types/alert-dialog/root.d.ts.map +1 -0
  349. package/build-types/alert-dialog/stories/index.story.d.ts +56 -0
  350. package/build-types/alert-dialog/stories/index.story.d.ts.map +1 -0
  351. package/build-types/alert-dialog/test/index.test.d.ts +2 -0
  352. package/build-types/alert-dialog/test/index.test.d.ts.map +1 -0
  353. package/build-types/alert-dialog/trigger.d.ts +7 -0
  354. package/build-types/alert-dialog/trigger.d.ts.map +1 -0
  355. package/build-types/alert-dialog/types.d.ts +105 -0
  356. package/build-types/alert-dialog/types.d.ts.map +1 -0
  357. package/build-types/badge/badge.d.ts.map +1 -1
  358. package/build-types/button/button.d.ts.map +1 -1
  359. package/build-types/card/stories/index.story.d.ts.map +1 -1
  360. package/build-types/card/title.d.ts.map +1 -1
  361. package/build-types/collapsible/panel.d.ts +2 -1
  362. package/build-types/collapsible/panel.d.ts.map +1 -1
  363. package/build-types/collapsible/root.d.ts +2 -1
  364. package/build-types/collapsible/root.d.ts.map +1 -1
  365. package/build-types/collapsible/trigger.d.ts +2 -1
  366. package/build-types/collapsible/trigger.d.ts.map +1 -1
  367. package/build-types/collapsible-card/content.d.ts.map +1 -1
  368. package/build-types/collapsible-card/context.d.ts +4 -0
  369. package/build-types/collapsible-card/context.d.ts.map +1 -0
  370. package/build-types/collapsible-card/header-description.d.ts +15 -0
  371. package/build-types/collapsible-card/header-description.d.ts.map +1 -0
  372. package/build-types/collapsible-card/header.d.ts.map +1 -1
  373. package/build-types/collapsible-card/index.d.ts +2 -1
  374. package/build-types/collapsible-card/index.d.ts.map +1 -1
  375. package/build-types/collapsible-card/stories/index.story.d.ts +10 -0
  376. package/build-types/collapsible-card/stories/index.story.d.ts.map +1 -1
  377. package/build-types/collapsible-card/types.d.ts +21 -0
  378. package/build-types/collapsible-card/types.d.ts.map +1 -1
  379. package/build-types/dialog/action.d.ts.map +1 -1
  380. package/build-types/dialog/close-icon.d.ts.map +1 -1
  381. package/build-types/dialog/popup.d.ts.map +1 -1
  382. package/build-types/dialog/stories/index.story.d.ts +8 -6
  383. package/build-types/dialog/stories/index.story.d.ts.map +1 -1
  384. package/build-types/dialog/title.d.ts +12 -2
  385. package/build-types/dialog/title.d.ts.map +1 -1
  386. package/build-types/dialog/types.d.ts +13 -6
  387. package/build-types/dialog/types.d.ts.map +1 -1
  388. package/build-types/empty-state/actions.d.ts +7 -0
  389. package/build-types/empty-state/actions.d.ts.map +1 -0
  390. package/build-types/empty-state/description.d.ts +7 -0
  391. package/build-types/empty-state/description.d.ts.map +1 -0
  392. package/build-types/empty-state/icon.d.ts +7 -0
  393. package/build-types/empty-state/icon.d.ts.map +1 -0
  394. package/build-types/empty-state/index.d.ts +8 -0
  395. package/build-types/empty-state/index.d.ts.map +1 -0
  396. package/build-types/empty-state/root.d.ts +6 -0
  397. package/build-types/empty-state/root.d.ts.map +1 -0
  398. package/build-types/empty-state/stories/index.story.d.ts +8 -0
  399. package/build-types/empty-state/stories/index.story.d.ts.map +1 -0
  400. package/build-types/empty-state/test/actions.test.d.ts +2 -0
  401. package/build-types/empty-state/test/actions.test.d.ts.map +1 -0
  402. package/build-types/empty-state/test/description.test.d.ts +2 -0
  403. package/build-types/empty-state/test/description.test.d.ts.map +1 -0
  404. package/build-types/empty-state/test/icon.test.d.ts +2 -0
  405. package/build-types/empty-state/test/icon.test.d.ts.map +1 -0
  406. package/build-types/empty-state/test/root.test.d.ts +2 -0
  407. package/build-types/empty-state/test/root.test.d.ts.map +1 -0
  408. package/build-types/empty-state/test/title.test.d.ts +2 -0
  409. package/build-types/empty-state/test/title.test.d.ts.map +1 -0
  410. package/build-types/empty-state/test/visual.test.d.ts +2 -0
  411. package/build-types/empty-state/test/visual.test.d.ts.map +1 -0
  412. package/build-types/empty-state/title.d.ts +6 -0
  413. package/build-types/empty-state/title.d.ts.map +1 -0
  414. package/build-types/empty-state/types.d.ts +40 -0
  415. package/build-types/empty-state/types.d.ts.map +1 -0
  416. package/build-types/empty-state/visual.d.ts +7 -0
  417. package/build-types/empty-state/visual.d.ts.map +1 -0
  418. package/build-types/form/index.d.ts +3 -0
  419. package/build-types/form/index.d.ts.map +1 -0
  420. package/build-types/form/input-control/index.d.ts +2 -0
  421. package/build-types/form/input-control/index.d.ts.map +1 -0
  422. package/build-types/form/input-control/input-control.d.ts +6 -0
  423. package/build-types/form/input-control/input-control.d.ts.map +1 -0
  424. package/build-types/form/input-control/stories/index.story.d.ts +16 -0
  425. package/build-types/form/input-control/stories/index.story.d.ts.map +1 -0
  426. package/build-types/form/input-control/test/index.test.d.ts +2 -0
  427. package/build-types/form/input-control/test/index.test.d.ts.map +1 -0
  428. package/build-types/form/input-control/types.d.ts +4 -0
  429. package/build-types/form/input-control/types.d.ts.map +1 -0
  430. package/build-types/form/primitives/field/description.d.ts +2 -1
  431. package/build-types/form/primitives/field/description.d.ts.map +1 -1
  432. package/build-types/form/primitives/field/details.d.ts +2 -1
  433. package/build-types/form/primitives/field/details.d.ts.map +1 -1
  434. package/build-types/form/primitives/field/label.d.ts +2 -1
  435. package/build-types/form/primitives/field/label.d.ts.map +1 -1
  436. package/build-types/form/primitives/field/stories/index.story.d.ts.map +1 -1
  437. package/build-types/form/primitives/fieldset/description.d.ts +2 -1
  438. package/build-types/form/primitives/fieldset/description.d.ts.map +1 -1
  439. package/build-types/form/primitives/fieldset/details.d.ts +2 -1
  440. package/build-types/form/primitives/fieldset/details.d.ts.map +1 -1
  441. package/build-types/form/primitives/fieldset/legend.d.ts +2 -1
  442. package/build-types/form/primitives/fieldset/legend.d.ts.map +1 -1
  443. package/build-types/form/primitives/fieldset/root.d.ts +2 -1
  444. package/build-types/form/primitives/fieldset/root.d.ts.map +1 -1
  445. package/build-types/form/primitives/fieldset/stories/index.story.d.ts.map +1 -1
  446. package/build-types/form/primitives/input/input.d.ts.map +1 -1
  447. package/build-types/form/primitives/input/stories/index.story.d.ts +2 -0
  448. package/build-types/form/primitives/input/stories/index.story.d.ts.map +1 -1
  449. package/build-types/form/primitives/input-layout/input-layout.d.ts.map +1 -1
  450. package/build-types/form/primitives/input-layout/slot.d.ts.map +1 -1
  451. package/build-types/form/primitives/input-layout/stories/index.story.d.ts +5 -0
  452. package/build-types/form/primitives/input-layout/stories/index.story.d.ts.map +1 -1
  453. package/build-types/form/primitives/select/item.d.ts +6 -2
  454. package/build-types/form/primitives/select/item.d.ts.map +1 -1
  455. package/build-types/form/primitives/select/popup.d.ts +11 -1
  456. package/build-types/form/primitives/select/popup.d.ts.map +1 -1
  457. package/build-types/form/primitives/select/trigger.d.ts +12 -2
  458. package/build-types/form/primitives/select/trigger.d.ts.map +1 -1
  459. package/build-types/form/primitives/select/types.d.ts +13 -3
  460. package/build-types/form/primitives/select/types.d.ts.map +1 -1
  461. package/build-types/form/primitives/textarea/textarea.d.ts.map +1 -1
  462. package/build-types/form/stories/shared.d.ts +3 -0
  463. package/build-types/form/stories/shared.d.ts.map +1 -0
  464. package/build-types/form/types.d.ts +30 -0
  465. package/build-types/form/types.d.ts.map +1 -0
  466. package/build-types/index.d.ts +4 -1
  467. package/build-types/index.d.ts.map +1 -1
  468. package/build-types/link/link.d.ts.map +1 -1
  469. package/build-types/notice/index.d.ts +0 -1
  470. package/build-types/notice/index.d.ts.map +1 -1
  471. package/build-types/popover/arrow.d.ts +10 -0
  472. package/build-types/popover/arrow.d.ts.map +1 -0
  473. package/build-types/popover/close.d.ts +11 -0
  474. package/build-types/popover/close.d.ts.map +1 -0
  475. package/build-types/popover/context.d.ts +22 -0
  476. package/build-types/popover/context.d.ts.map +1 -0
  477. package/build-types/popover/description.d.ts +10 -0
  478. package/build-types/popover/description.d.ts.map +1 -0
  479. package/build-types/popover/index.d.ts +9 -0
  480. package/build-types/popover/index.d.ts.map +1 -0
  481. package/build-types/popover/popup.d.ts +11 -0
  482. package/build-types/popover/popup.d.ts.map +1 -0
  483. package/build-types/popover/root.d.ts +37 -0
  484. package/build-types/popover/root.d.ts.map +1 -0
  485. package/build-types/popover/stories/index.story.d.ts +211 -0
  486. package/build-types/popover/stories/index.story.d.ts.map +1 -0
  487. package/build-types/popover/stories/utils.d.ts +25 -0
  488. package/build-types/popover/stories/utils.d.ts.map +1 -0
  489. package/build-types/popover/test/index.test.d.ts +2 -0
  490. package/build-types/popover/test/index.test.d.ts.map +1 -0
  491. package/build-types/popover/title.d.ts +20 -0
  492. package/build-types/popover/title.d.ts.map +1 -0
  493. package/build-types/popover/trigger.d.ts +10 -0
  494. package/build-types/popover/trigger.d.ts.map +1 -0
  495. package/build-types/popover/types.d.ts +83 -0
  496. package/build-types/popover/types.d.ts.map +1 -0
  497. package/build-types/tabs/context.d.ts +26 -0
  498. package/build-types/tabs/context.d.ts.map +1 -0
  499. package/build-types/tabs/list.d.ts +2 -1
  500. package/build-types/tabs/list.d.ts.map +1 -1
  501. package/build-types/tabs/panel.d.ts +2 -1
  502. package/build-types/tabs/panel.d.ts.map +1 -1
  503. package/build-types/tabs/root.d.ts +2 -1
  504. package/build-types/tabs/root.d.ts.map +1 -1
  505. package/build-types/tabs/tab.d.ts +2 -1
  506. package/build-types/tabs/tab.d.ts.map +1 -1
  507. package/build-types/text/stories/index.story.d.ts +4 -0
  508. package/build-types/text/stories/index.story.d.ts.map +1 -1
  509. package/build-types/text/text.d.ts.map +1 -1
  510. package/build-types/tooltip/popup.d.ts.map +1 -1
  511. package/build-types/tooltip/root.d.ts +13 -0
  512. package/build-types/tooltip/root.d.ts.map +1 -1
  513. package/build-types/tooltip/stories/index.story.d.ts.map +1 -1
  514. package/build-types/tooltip/stories/usage-guidelines.story.d.ts +21 -0
  515. package/build-types/tooltip/stories/usage-guidelines.story.d.ts.map +1 -0
  516. package/build-types/tooltip/types.d.ts +4 -0
  517. package/build-types/tooltip/types.d.ts.map +1 -1
  518. package/build-types/utils/test/use-deprioritized-initial-focus.test.d.ts +2 -0
  519. package/build-types/utils/test/use-deprioritized-initial-focus.test.d.ts.map +1 -0
  520. package/build-types/utils/types.d.ts +6 -2
  521. package/build-types/utils/types.d.ts.map +1 -1
  522. package/build-types/utils/use-deprioritized-initial-focus.d.ts +36 -0
  523. package/build-types/utils/use-deprioritized-initial-focus.d.ts.map +1 -0
  524. package/build-types/visually-hidden/stories/index.story.d.ts +7 -0
  525. package/build-types/visually-hidden/stories/index.story.d.ts.map +1 -1
  526. package/build-types/visually-hidden/visually-hidden.d.ts +34 -0
  527. package/build-types/visually-hidden/visually-hidden.d.ts.map +1 -1
  528. package/package.json +17 -16
  529. package/src/alert-dialog/context.tsx +22 -0
  530. package/src/alert-dialog/index.ts +3 -0
  531. package/src/alert-dialog/popup.tsx +116 -0
  532. package/src/alert-dialog/root.tsx +226 -0
  533. package/src/alert-dialog/stories/index.story.tsx +305 -0
  534. package/src/alert-dialog/style.module.css +21 -0
  535. package/src/alert-dialog/test/index.test.tsx +1509 -0
  536. package/src/alert-dialog/trigger.tsx +15 -0
  537. package/src/alert-dialog/types.ts +119 -0
  538. package/src/badge/badge.tsx +11 -14
  539. package/src/badge/style.module.css +0 -4
  540. package/src/button/button.tsx +2 -0
  541. package/src/button/style.module.css +9 -3
  542. package/src/card/stories/index.story.tsx +4 -5
  543. package/src/card/style.module.css +4 -10
  544. package/src/card/test/index.test.tsx +17 -1
  545. package/src/card/title.tsx +14 -12
  546. package/src/collapsible-card/content.tsx +16 -3
  547. package/src/collapsible-card/context.ts +7 -0
  548. package/src/collapsible-card/header-description.tsx +43 -0
  549. package/src/collapsible-card/header.tsx +47 -24
  550. package/src/collapsible-card/index.ts +2 -1
  551. package/src/collapsible-card/stories/index.story.tsx +102 -4
  552. package/src/collapsible-card/style.module.css +34 -2
  553. package/src/collapsible-card/test/index.test.tsx +96 -9
  554. package/src/collapsible-card/types.ts +22 -0
  555. package/src/dialog/action.tsx +8 -2
  556. package/src/dialog/close-icon.tsx +1 -0
  557. package/src/dialog/popup.tsx +23 -3
  558. package/src/dialog/stories/index.story.tsx +33 -28
  559. package/src/dialog/style.module.css +18 -14
  560. package/src/dialog/test/index.test.tsx +180 -4
  561. package/src/dialog/title.tsx +21 -9
  562. package/src/dialog/types.ts +20 -6
  563. package/src/empty-state/actions.tsx +24 -0
  564. package/src/empty-state/description.tsx +31 -0
  565. package/src/empty-state/icon.tsx +24 -0
  566. package/src/empty-state/index.ts +8 -0
  567. package/src/empty-state/root.tsx +23 -0
  568. package/src/empty-state/stories/index.story.tsx +64 -0
  569. package/src/empty-state/style.module.css +53 -0
  570. package/src/empty-state/test/actions.test.tsx +18 -0
  571. package/src/empty-state/test/description.test.tsx +26 -0
  572. package/src/empty-state/test/icon.test.tsx +13 -0
  573. package/src/empty-state/test/root.test.tsx +13 -0
  574. package/src/empty-state/test/title.test.tsx +26 -0
  575. package/src/empty-state/test/visual.test.tsx +17 -0
  576. package/src/empty-state/title.tsx +29 -0
  577. package/src/empty-state/types.ts +45 -0
  578. package/src/empty-state/visual.tsx +24 -0
  579. package/src/form/index.ts +3 -0
  580. package/src/form/input-control/index.ts +1 -0
  581. package/src/form/input-control/input-control.tsx +33 -0
  582. package/src/form/input-control/stories/index.story.tsx +163 -0
  583. package/src/form/input-control/test/index.test.tsx +53 -0
  584. package/src/form/input-control/types.ts +5 -0
  585. package/src/form/primitives/field/description.tsx +6 -1
  586. package/src/form/primitives/field/details.tsx +4 -2
  587. package/src/form/primitives/field/label.tsx +9 -5
  588. package/src/form/primitives/field/stories/index.story.tsx +2 -7
  589. package/src/form/primitives/field/test/index.test.tsx +11 -0
  590. package/src/form/primitives/fieldset/description.tsx +9 -1
  591. package/src/form/primitives/fieldset/legend.tsx +9 -4
  592. package/src/form/primitives/fieldset/stories/index.story.tsx +2 -7
  593. package/src/form/primitives/fieldset/test/index.test.tsx +22 -0
  594. package/src/form/primitives/input/input.tsx +6 -1
  595. package/src/form/primitives/input/stories/index.story.tsx +7 -0
  596. package/src/form/primitives/input/style.module.css +4 -0
  597. package/src/form/primitives/input-layout/input-layout.tsx +2 -0
  598. package/src/form/primitives/input-layout/slot.tsx +6 -2
  599. package/src/form/primitives/input-layout/stories/index.story.tsx +22 -1
  600. package/src/form/primitives/input-layout/style.module.css +3 -3
  601. package/src/form/primitives/select/popup.tsx +5 -2
  602. package/src/form/primitives/select/test/index.test.tsx +60 -1
  603. package/src/form/primitives/select/types.ts +14 -4
  604. package/src/form/primitives/stories/overview.mdx +15 -0
  605. package/src/form/primitives/textarea/textarea.tsx +11 -2
  606. package/src/form/stories/shared.tsx +21 -0
  607. package/src/form/types.ts +34 -0
  608. package/src/index.ts +4 -1
  609. package/src/link/link.tsx +2 -0
  610. package/src/link/style.module.css +11 -1
  611. package/src/notice/index.ts +0 -2
  612. package/src/notice/style.module.css +6 -6
  613. package/src/popover/arrow.tsx +49 -0
  614. package/src/popover/close.tsx +24 -0
  615. package/src/popover/context.tsx +100 -0
  616. package/src/popover/description.tsx +29 -0
  617. package/src/popover/index.ts +9 -0
  618. package/src/popover/popup.tsx +106 -0
  619. package/src/popover/root.tsx +41 -0
  620. package/src/popover/stories/index.story.tsx +1315 -0
  621. package/src/popover/stories/utils.tsx +91 -0
  622. package/src/popover/style.module.css +64 -0
  623. package/src/popover/test/index.test.tsx +727 -0
  624. package/src/popover/title.tsx +47 -0
  625. package/src/popover/trigger.tsx +17 -0
  626. package/src/popover/types.ts +113 -0
  627. package/src/tabs/context.tsx +170 -0
  628. package/src/tabs/list.tsx +0 -1
  629. package/src/tabs/panel.tsx +3 -0
  630. package/src/tabs/root.tsx +6 -1
  631. package/src/tabs/style.module.css +3 -3
  632. package/src/tabs/tab.tsx +3 -0
  633. package/src/tabs/test/index.test.tsx +162 -0
  634. package/src/text/stories/index.story.tsx +4 -2
  635. package/src/text/style.module.css +62 -36
  636. package/src/text/test/index.test.tsx +1 -4
  637. package/src/text/text.tsx +8 -1
  638. package/src/tooltip/popup.tsx +2 -1
  639. package/src/tooltip/root.tsx +13 -0
  640. package/src/tooltip/stories/index.story.tsx +20 -15
  641. package/src/tooltip/stories/usage-guidelines.mdx +91 -0
  642. package/src/tooltip/stories/usage-guidelines.story.tsx +119 -0
  643. package/src/tooltip/style.module.css +2 -2
  644. package/src/tooltip/test/index.test.tsx +61 -0
  645. package/src/tooltip/types.ts +5 -0
  646. package/src/utils/css/field.module.css +12 -9
  647. package/src/utils/css/focus.module.css +7 -5
  648. package/src/utils/css/global-css-defense.module.css +117 -0
  649. package/src/utils/css/item-popup.module.css +3 -2
  650. package/src/utils/css/select-trigger.module.css +1 -0
  651. package/src/utils/test/use-deprioritized-initial-focus.test.tsx +230 -0
  652. package/src/utils/types.ts +7 -2
  653. package/src/utils/use-deprioritized-initial-focus.ts +84 -0
  654. package/src/visually-hidden/stories/index.story.tsx +25 -0
  655. package/src/visually-hidden/visually-hidden.tsx +34 -0
@@ -0,0 +1,1509 @@
1
+ import { speak } from '@wordpress/a11y';
2
+ import { act, render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { createRef } from '@wordpress/element';
5
+
6
+ import * as AlertDialog from '..';
7
+ import type { ConfirmResult } from '../types';
8
+
9
+ jest.mock( '@wordpress/a11y', () => ( {
10
+ speak: jest.fn(),
11
+ } ) );
12
+
13
+ function createDeferred() {
14
+ let resolve!: ( value?: ConfirmResult ) => void;
15
+ let reject!: ( reason?: unknown ) => void;
16
+ const promise = new Promise< ConfirmResult >( ( res, rej ) => {
17
+ resolve = res;
18
+ reject = rej;
19
+ } );
20
+ return { promise, resolve, reject };
21
+ }
22
+
23
+ describe( 'AlertDialog', () => {
24
+ it( 'forwards ref', () => {
25
+ const triggerRef = createRef< HTMLButtonElement >();
26
+ const popupRef = createRef< HTMLDivElement >();
27
+
28
+ render(
29
+ <AlertDialog.Root defaultOpen>
30
+ <AlertDialog.Trigger ref={ triggerRef }>
31
+ Open
32
+ </AlertDialog.Trigger>
33
+ <AlertDialog.Popup ref={ popupRef } title="Test Title">
34
+ Content
35
+ </AlertDialog.Popup>
36
+ </AlertDialog.Root>
37
+ );
38
+
39
+ expect( triggerRef.current ).toBeInstanceOf( HTMLButtonElement );
40
+ expect( popupRef.current ).toBeInstanceOf( HTMLDivElement );
41
+ } );
42
+
43
+ it( 'renders with title, children, and default buttons', async () => {
44
+ render(
45
+ <AlertDialog.Root open onOpenChange={ jest.fn() }>
46
+ <AlertDialog.Popup title="Test Title">
47
+ Test message content
48
+ </AlertDialog.Popup>
49
+ </AlertDialog.Root>
50
+ );
51
+
52
+ await waitFor( () => {
53
+ expect( screen.getByText( 'Test Title' ) ).toBeVisible();
54
+ } );
55
+
56
+ expect( screen.getByText( 'Test message content' ) ).toBeVisible();
57
+ expect(
58
+ screen.queryByRole( 'button', { name: 'Close' } )
59
+ ).not.toBeInTheDocument();
60
+ expect( screen.getByRole( 'button', { name: 'OK' } ) ).toBeVisible();
61
+ expect(
62
+ screen.getByRole( 'button', { name: 'Cancel' } )
63
+ ).toBeVisible();
64
+ } );
65
+
66
+ it( 'renders description when provided', async () => {
67
+ render(
68
+ <AlertDialog.Root open onOpenChange={ jest.fn() }>
69
+ <AlertDialog.Popup
70
+ title="Test Title"
71
+ description="This is a description"
72
+ >
73
+ Body content
74
+ </AlertDialog.Popup>
75
+ </AlertDialog.Root>
76
+ );
77
+
78
+ await waitFor( () => {
79
+ expect( screen.getByText( 'This is a description' ) ).toBeVisible();
80
+ } );
81
+ } );
82
+
83
+ it( 'renders with role="alertdialog" for default intent', async () => {
84
+ render(
85
+ <AlertDialog.Root open onOpenChange={ jest.fn() }>
86
+ <AlertDialog.Popup title="Default Dialog">
87
+ Content
88
+ </AlertDialog.Popup>
89
+ </AlertDialog.Root>
90
+ );
91
+
92
+ await waitFor( () => {
93
+ expect( screen.getByRole( 'alertdialog' ) ).toBeVisible();
94
+ } );
95
+ } );
96
+
97
+ it( 'renders with role="alertdialog" for irreversible intent', async () => {
98
+ render(
99
+ <AlertDialog.Root open onOpenChange={ jest.fn() }>
100
+ <AlertDialog.Popup
101
+ intent="irreversible"
102
+ title="Irreversible Dialog"
103
+ >
104
+ Content
105
+ </AlertDialog.Popup>
106
+ </AlertDialog.Root>
107
+ );
108
+
109
+ await waitFor( () => {
110
+ expect( screen.getByRole( 'alertdialog' ) ).toBeVisible();
111
+ } );
112
+ } );
113
+
114
+ it( 'uses custom button labels', async () => {
115
+ render(
116
+ <AlertDialog.Root open onOpenChange={ jest.fn() }>
117
+ <AlertDialog.Popup
118
+ title="Custom Labels"
119
+ confirmButtonText="Yes, do it"
120
+ cancelButtonText="No, go back"
121
+ >
122
+ Content
123
+ </AlertDialog.Popup>
124
+ </AlertDialog.Root>
125
+ );
126
+
127
+ await waitFor( () => {
128
+ expect(
129
+ screen.getByRole( 'button', { name: 'Yes, do it' } )
130
+ ).toBeVisible();
131
+ } );
132
+
133
+ expect(
134
+ screen.getByRole( 'button', { name: 'No, go back' } )
135
+ ).toBeVisible();
136
+ } );
137
+
138
+ it( 'opens dialog when Trigger is clicked', async () => {
139
+ render(
140
+ <AlertDialog.Root>
141
+ <AlertDialog.Trigger>Open</AlertDialog.Trigger>
142
+ <AlertDialog.Popup title="Trigger Test">
143
+ Dialog content
144
+ </AlertDialog.Popup>
145
+ </AlertDialog.Root>
146
+ );
147
+
148
+ expect(
149
+ screen.queryByText( 'Dialog content' )
150
+ ).not.toBeInTheDocument();
151
+
152
+ await userEvent.click( screen.getByRole( 'button', { name: 'Open' } ) );
153
+
154
+ await waitFor( () => {
155
+ expect( screen.getByText( 'Trigger Test' ) ).toBeVisible();
156
+ } );
157
+
158
+ expect( screen.getByText( 'Dialog content' ) ).toBeVisible();
159
+ } );
160
+
161
+ describe( 'sync confirm flow', () => {
162
+ it( 'calls onConfirm and closes on confirm click', async () => {
163
+ const onConfirm = jest.fn();
164
+ const onOpenChange = jest.fn();
165
+
166
+ render(
167
+ <AlertDialog.Root
168
+ open
169
+ onOpenChange={ onOpenChange }
170
+ onConfirm={ onConfirm }
171
+ >
172
+ <AlertDialog.Popup title="Sync Test">
173
+ Content
174
+ </AlertDialog.Popup>
175
+ </AlertDialog.Root>
176
+ );
177
+
178
+ await waitFor( () => {
179
+ expect(
180
+ screen.getByRole( 'button', { name: 'OK' } )
181
+ ).toBeVisible();
182
+ } );
183
+
184
+ await userEvent.click(
185
+ screen.getByRole( 'button', { name: 'OK' } )
186
+ );
187
+
188
+ expect( onConfirm ).toHaveBeenCalledTimes( 1 );
189
+ await waitFor( () => {
190
+ expect( onOpenChange ).toHaveBeenCalledWith(
191
+ false,
192
+ expect.objectContaining( {
193
+ reason: 'imperative-action',
194
+ } )
195
+ );
196
+ } );
197
+ } );
198
+
199
+ it( 'provides well-formed event details on confirm close', async () => {
200
+ const onOpenChange = jest.fn();
201
+
202
+ render(
203
+ <AlertDialog.Root
204
+ open
205
+ onOpenChange={ onOpenChange }
206
+ onConfirm={ jest.fn() }
207
+ >
208
+ <AlertDialog.Popup title="Details Test">
209
+ Content
210
+ </AlertDialog.Popup>
211
+ </AlertDialog.Root>
212
+ );
213
+
214
+ await waitFor( () => {
215
+ expect(
216
+ screen.getByRole( 'button', { name: 'OK' } )
217
+ ).toBeVisible();
218
+ } );
219
+
220
+ await userEvent.click(
221
+ screen.getByRole( 'button', { name: 'OK' } )
222
+ );
223
+
224
+ await waitFor( () => {
225
+ expect( onOpenChange ).toHaveBeenCalledWith(
226
+ false,
227
+ expect.objectContaining( {
228
+ reason: 'imperative-action',
229
+ } )
230
+ );
231
+ } );
232
+
233
+ const details = onOpenChange.mock.calls.find(
234
+ ( [ open ]: [ boolean ] ) => ! open
235
+ )?.[ 1 ];
236
+
237
+ expect( details ).toBeDefined();
238
+ expect( typeof details.cancel ).toBe( 'function' );
239
+ expect( typeof details.allowPropagation ).toBe( 'function' );
240
+ expect( typeof details.preventUnmountOnClose ).toBe( 'function' );
241
+ expect( details.event ).toBeInstanceOf( Event );
242
+ } );
243
+
244
+ it( 'closes without onConfirm when no handler is provided', async () => {
245
+ const onOpenChange = jest.fn();
246
+
247
+ render(
248
+ <AlertDialog.Root open onOpenChange={ onOpenChange }>
249
+ <AlertDialog.Popup title="No Handler">
250
+ Content
251
+ </AlertDialog.Popup>
252
+ </AlertDialog.Root>
253
+ );
254
+
255
+ await waitFor( () => {
256
+ expect(
257
+ screen.getByRole( 'button', { name: 'OK' } )
258
+ ).toBeVisible();
259
+ } );
260
+
261
+ await userEvent.click(
262
+ screen.getByRole( 'button', { name: 'OK' } )
263
+ );
264
+
265
+ await waitFor( () => {
266
+ expect( onOpenChange ).toHaveBeenCalledWith(
267
+ false,
268
+ expect.objectContaining( {
269
+ reason: 'imperative-action',
270
+ } )
271
+ );
272
+ } );
273
+ } );
274
+ } );
275
+
276
+ describe( 'cancel and dismiss', () => {
277
+ it( 'closes on cancel click without calling onConfirm', async () => {
278
+ const onConfirm = jest.fn();
279
+ const onOpenChange = jest.fn();
280
+
281
+ render(
282
+ <AlertDialog.Root
283
+ open
284
+ onOpenChange={ onOpenChange }
285
+ onConfirm={ onConfirm }
286
+ >
287
+ <AlertDialog.Popup title="Cancel Test">
288
+ Content
289
+ </AlertDialog.Popup>
290
+ </AlertDialog.Root>
291
+ );
292
+
293
+ await waitFor( () => {
294
+ expect(
295
+ screen.getByRole( 'button', { name: 'Cancel' } )
296
+ ).toBeVisible();
297
+ } );
298
+
299
+ await userEvent.click(
300
+ screen.getByRole( 'button', { name: 'Cancel' } )
301
+ );
302
+
303
+ expect( onOpenChange ).toHaveBeenCalledWith(
304
+ false,
305
+ expect.objectContaining( { reason: 'close-press' } )
306
+ );
307
+ expect( onConfirm ).not.toHaveBeenCalled();
308
+ } );
309
+
310
+ it( 'closes on escape key', async () => {
311
+ const onOpenChange = jest.fn();
312
+
313
+ render(
314
+ <AlertDialog.Root open onOpenChange={ onOpenChange }>
315
+ <AlertDialog.Popup title="Escape Test">
316
+ Content
317
+ </AlertDialog.Popup>
318
+ </AlertDialog.Root>
319
+ );
320
+
321
+ await waitFor( () => {
322
+ expect( screen.getByText( 'Escape Test' ) ).toBeVisible();
323
+ } );
324
+
325
+ await userEvent.keyboard( '{Escape}' );
326
+
327
+ expect( onOpenChange ).toHaveBeenCalledWith(
328
+ false,
329
+ expect.objectContaining( { reason: 'escape-key' } )
330
+ );
331
+ } );
332
+
333
+ it( 'does not close on backdrop click', async () => {
334
+ const onOpenChange = jest.fn();
335
+
336
+ render(
337
+ <AlertDialog.Root open onOpenChange={ onOpenChange }>
338
+ <AlertDialog.Popup title="Backdrop Test">
339
+ Content
340
+ </AlertDialog.Popup>
341
+ </AlertDialog.Root>
342
+ );
343
+
344
+ await waitFor( () => {
345
+ expect( screen.getByText( 'Backdrop Test' ) ).toBeVisible();
346
+ } );
347
+
348
+ await userEvent.click( document.body );
349
+
350
+ expect( onOpenChange ).not.toHaveBeenCalled();
351
+ } );
352
+ } );
353
+
354
+ describe( 'irreversible intent', () => {
355
+ it( 'renders title and buttons', async () => {
356
+ render(
357
+ <AlertDialog.Root open onOpenChange={ jest.fn() }>
358
+ <AlertDialog.Popup
359
+ intent="irreversible"
360
+ title="Irreversible Dialog"
361
+ >
362
+ Irreversible message content
363
+ </AlertDialog.Popup>
364
+ </AlertDialog.Root>
365
+ );
366
+
367
+ await waitFor( () => {
368
+ expect(
369
+ screen.getByText( 'Irreversible Dialog' )
370
+ ).toBeVisible();
371
+ } );
372
+
373
+ expect(
374
+ screen.getByText( 'Irreversible message content' )
375
+ ).toBeVisible();
376
+ expect(
377
+ screen.getByRole( 'button', { name: 'OK' } )
378
+ ).toBeVisible();
379
+ expect(
380
+ screen.getByRole( 'button', { name: 'Cancel' } )
381
+ ).toBeVisible();
382
+ } );
383
+
384
+ it( 'closes on escape key', async () => {
385
+ const onOpenChange = jest.fn();
386
+
387
+ render(
388
+ <AlertDialog.Root open onOpenChange={ onOpenChange }>
389
+ <AlertDialog.Popup
390
+ intent="irreversible"
391
+ title="Irreversible Dialog"
392
+ >
393
+ Content
394
+ </AlertDialog.Popup>
395
+ </AlertDialog.Root>
396
+ );
397
+
398
+ await waitFor( () => {
399
+ expect(
400
+ screen.getByText( 'Irreversible Dialog' )
401
+ ).toBeVisible();
402
+ } );
403
+
404
+ await userEvent.keyboard( '{Escape}' );
405
+
406
+ expect( onOpenChange ).toHaveBeenCalledWith(
407
+ false,
408
+ expect.objectContaining( { reason: 'escape-key' } )
409
+ );
410
+ } );
411
+
412
+ it( 'does not close on backdrop click', async () => {
413
+ const onOpenChange = jest.fn();
414
+
415
+ render(
416
+ <AlertDialog.Root open onOpenChange={ onOpenChange }>
417
+ <AlertDialog.Popup
418
+ intent="irreversible"
419
+ title="Irreversible Dialog"
420
+ >
421
+ Content
422
+ </AlertDialog.Popup>
423
+ </AlertDialog.Root>
424
+ );
425
+
426
+ await waitFor( () => {
427
+ expect(
428
+ screen.getByText( 'Irreversible Dialog' )
429
+ ).toBeVisible();
430
+ } );
431
+
432
+ await userEvent.click( document.body );
433
+
434
+ expect( onOpenChange ).not.toHaveBeenCalled();
435
+ } );
436
+ } );
437
+
438
+ describe( 'async confirm flow', () => {
439
+ it( 'disables buttons while confirm is pending', async () => {
440
+ const deferred = createDeferred();
441
+
442
+ render(
443
+ <AlertDialog.Root
444
+ open
445
+ onOpenChange={ jest.fn() }
446
+ onConfirm={ () => deferred.promise }
447
+ >
448
+ <AlertDialog.Popup title="Async Test">
449
+ Content
450
+ </AlertDialog.Popup>
451
+ </AlertDialog.Root>
452
+ );
453
+
454
+ await waitFor( () => {
455
+ expect(
456
+ screen.getByRole( 'button', { name: 'OK' } )
457
+ ).toBeVisible();
458
+ } );
459
+
460
+ await userEvent.click(
461
+ screen.getByRole( 'button', { name: 'OK' } )
462
+ );
463
+
464
+ await waitFor( () => {
465
+ expect(
466
+ screen.getByRole( 'button', { name: 'OK' } )
467
+ ).toHaveAttribute( 'aria-disabled', 'true' );
468
+ } );
469
+
470
+ expect(
471
+ screen.getByRole( 'button', { name: 'Cancel' } )
472
+ ).toHaveAttribute( 'aria-disabled', 'true' );
473
+
474
+ await act( async () => {
475
+ deferred.resolve();
476
+ } );
477
+ } );
478
+
479
+ it( 'closes dialog when async confirm resolves', async () => {
480
+ const deferred = createDeferred();
481
+ const onOpenChange = jest.fn();
482
+
483
+ render(
484
+ <AlertDialog.Root
485
+ open
486
+ onOpenChange={ onOpenChange }
487
+ onConfirm={ () => deferred.promise }
488
+ >
489
+ <AlertDialog.Popup title="Async Resolve">
490
+ Content
491
+ </AlertDialog.Popup>
492
+ </AlertDialog.Root>
493
+ );
494
+
495
+ await waitFor( () => {
496
+ expect(
497
+ screen.getByRole( 'button', { name: 'OK' } )
498
+ ).toBeVisible();
499
+ } );
500
+
501
+ await userEvent.click(
502
+ screen.getByRole( 'button', { name: 'OK' } )
503
+ );
504
+
505
+ await act( async () => {
506
+ deferred.resolve();
507
+ } );
508
+
509
+ await waitFor( () => {
510
+ expect( onOpenChange ).toHaveBeenCalledWith(
511
+ false,
512
+ expect.objectContaining( {
513
+ reason: 'imperative-action',
514
+ } )
515
+ );
516
+ } );
517
+ } );
518
+
519
+ it( 're-enables buttons when async confirm rejects (task failure)', async () => {
520
+ const deferred = createDeferred();
521
+ const consoleSpy = jest
522
+ .spyOn( console, 'error' )
523
+ .mockImplementation( () => {} );
524
+
525
+ render(
526
+ <AlertDialog.Root
527
+ open
528
+ onOpenChange={ jest.fn() }
529
+ onConfirm={ () => deferred.promise }
530
+ >
531
+ <AlertDialog.Popup title="Async Reject">
532
+ Content
533
+ </AlertDialog.Popup>
534
+ </AlertDialog.Root>
535
+ );
536
+
537
+ await waitFor( () => {
538
+ expect(
539
+ screen.getByRole( 'button', { name: 'OK' } )
540
+ ).toBeVisible();
541
+ } );
542
+
543
+ await userEvent.click(
544
+ screen.getByRole( 'button', { name: 'OK' } )
545
+ );
546
+
547
+ await waitFor( () => {
548
+ expect(
549
+ screen.getByRole( 'button', { name: 'OK' } )
550
+ ).toHaveAttribute( 'aria-disabled', 'true' );
551
+ } );
552
+
553
+ await act( async () => {
554
+ deferred.reject( new Error( 'Task failed' ) );
555
+ } );
556
+
557
+ // The error is caught and logged via console.error in Root.
558
+ await waitFor( () => {
559
+ expect( consoleSpy ).toHaveBeenCalledWith(
560
+ expect.objectContaining( { message: 'Task failed' } )
561
+ );
562
+ } );
563
+
564
+ await waitFor( () => {
565
+ expect(
566
+ screen.getByRole( 'button', { name: 'OK' } )
567
+ ).not.toHaveAttribute( 'aria-disabled', 'true' );
568
+ } );
569
+
570
+ expect(
571
+ screen.getByRole( 'button', { name: 'Cancel' } )
572
+ ).not.toHaveAttribute( 'aria-disabled', 'true' );
573
+
574
+ expect( screen.getByText( 'Async Reject' ) ).toBeVisible();
575
+
576
+ // Throws do NOT render a visible error message.
577
+ expect(
578
+ screen.queryByText( 'Task failed' )
579
+ ).not.toBeInTheDocument();
580
+
581
+ consoleSpy.mockRestore();
582
+ } );
583
+
584
+ it( 'keeps dialog open when confirm returns { close: false }', async () => {
585
+ const onOpenChange = jest.fn();
586
+
587
+ render(
588
+ <AlertDialog.Root
589
+ open
590
+ onOpenChange={ onOpenChange }
591
+ onConfirm={ () => ( { close: false } ) }
592
+ >
593
+ <AlertDialog.Popup title="Keep Open">
594
+ Content
595
+ </AlertDialog.Popup>
596
+ </AlertDialog.Root>
597
+ );
598
+
599
+ await waitFor( () => {
600
+ expect(
601
+ screen.getByRole( 'button', { name: 'OK' } )
602
+ ).toBeVisible();
603
+ } );
604
+
605
+ await userEvent.click(
606
+ screen.getByRole( 'button', { name: 'OK' } )
607
+ );
608
+
609
+ await waitFor( () => {
610
+ expect(
611
+ screen.getByRole( 'button', { name: 'OK' } )
612
+ ).not.toHaveAttribute( 'aria-disabled', 'true' );
613
+ } );
614
+
615
+ expect( onOpenChange ).not.toHaveBeenCalledWith(
616
+ false,
617
+ expect.anything()
618
+ );
619
+ expect( screen.getByText( 'Keep Open' ) ).toBeVisible();
620
+ } );
621
+
622
+ it( 'keeps dialog open when async confirm returns { close: false }', async () => {
623
+ const deferred = createDeferred();
624
+ const onOpenChange = jest.fn();
625
+
626
+ render(
627
+ <AlertDialog.Root
628
+ open
629
+ onOpenChange={ onOpenChange }
630
+ onConfirm={ () => deferred.promise }
631
+ >
632
+ <AlertDialog.Popup title="Async Keep Open">
633
+ Content
634
+ </AlertDialog.Popup>
635
+ </AlertDialog.Root>
636
+ );
637
+
638
+ await waitFor( () => {
639
+ expect(
640
+ screen.getByRole( 'button', { name: 'OK' } )
641
+ ).toBeVisible();
642
+ } );
643
+
644
+ await userEvent.click(
645
+ screen.getByRole( 'button', { name: 'OK' } )
646
+ );
647
+
648
+ await waitFor( () => {
649
+ expect(
650
+ screen.getByRole( 'button', { name: 'OK' } )
651
+ ).toHaveAttribute( 'aria-disabled', 'true' );
652
+ } );
653
+
654
+ await act( async () => {
655
+ deferred.resolve( { close: false } );
656
+ } );
657
+
658
+ await waitFor( () => {
659
+ expect(
660
+ screen.getByRole( 'button', { name: 'OK' } )
661
+ ).not.toHaveAttribute( 'aria-disabled', 'true' );
662
+ } );
663
+
664
+ expect( onOpenChange ).not.toHaveBeenCalledWith(
665
+ false,
666
+ expect.anything()
667
+ );
668
+ } );
669
+
670
+ it( 'blocks dismiss while pending by default', async () => {
671
+ const deferred = createDeferred();
672
+ const onOpenChange = jest.fn();
673
+
674
+ render(
675
+ <AlertDialog.Root
676
+ open
677
+ onOpenChange={ onOpenChange }
678
+ onConfirm={ () => deferred.promise }
679
+ >
680
+ <AlertDialog.Popup title="Block Dismiss">
681
+ Content
682
+ </AlertDialog.Popup>
683
+ </AlertDialog.Root>
684
+ );
685
+
686
+ await waitFor( () => {
687
+ expect(
688
+ screen.getByRole( 'button', { name: 'OK' } )
689
+ ).toBeVisible();
690
+ } );
691
+
692
+ await userEvent.click(
693
+ screen.getByRole( 'button', { name: 'OK' } )
694
+ );
695
+
696
+ await waitFor( () => {
697
+ expect(
698
+ screen.getByRole( 'button', { name: 'OK' } )
699
+ ).toHaveAttribute( 'aria-disabled', 'true' );
700
+ } );
701
+
702
+ await userEvent.keyboard( '{Escape}' );
703
+
704
+ expect( onOpenChange ).not.toHaveBeenCalledWith(
705
+ false,
706
+ expect.anything()
707
+ );
708
+
709
+ await act( async () => {
710
+ deferred.resolve();
711
+ } );
712
+ } );
713
+
714
+ it( 'ignores duplicate confirm clicks while pending', async () => {
715
+ const onConfirm = jest.fn(
716
+ () =>
717
+ new Promise< void >( () => {
718
+ // Never resolves
719
+ } )
720
+ );
721
+
722
+ render(
723
+ <AlertDialog.Root
724
+ open
725
+ onOpenChange={ jest.fn() }
726
+ onConfirm={ onConfirm }
727
+ >
728
+ <AlertDialog.Popup title="Double Click">
729
+ Content
730
+ </AlertDialog.Popup>
731
+ </AlertDialog.Root>
732
+ );
733
+
734
+ await waitFor( () => {
735
+ expect(
736
+ screen.getByRole( 'button', { name: 'OK' } )
737
+ ).toBeVisible();
738
+ } );
739
+
740
+ await userEvent.click(
741
+ screen.getByRole( 'button', { name: 'OK' } )
742
+ );
743
+ await userEvent.click(
744
+ screen.getByRole( 'button', { name: 'OK' } )
745
+ );
746
+
747
+ expect( onConfirm ).toHaveBeenCalledTimes( 1 );
748
+ } );
749
+ } );
750
+
751
+ describe( 'uncontrolled mode', () => {
752
+ it( 'renders dialog open when defaultOpen is true', async () => {
753
+ render(
754
+ <AlertDialog.Root defaultOpen>
755
+ <AlertDialog.Trigger>Open</AlertDialog.Trigger>
756
+ <AlertDialog.Popup title="Default Open">
757
+ Dialog content
758
+ </AlertDialog.Popup>
759
+ </AlertDialog.Root>
760
+ );
761
+
762
+ await waitFor( () => {
763
+ expect( screen.getByRole( 'alertdialog' ) ).toBeVisible();
764
+ } );
765
+ expect( screen.getByText( 'Default Open' ) ).toBeVisible();
766
+ expect( screen.getByText( 'Dialog content' ) ).toBeVisible();
767
+ } );
768
+
769
+ it( 'allows closing and reopening after defaultOpen', async () => {
770
+ render(
771
+ <AlertDialog.Root defaultOpen>
772
+ <AlertDialog.Trigger>Open</AlertDialog.Trigger>
773
+ <AlertDialog.Popup title="Reopen Test">
774
+ Content
775
+ </AlertDialog.Popup>
776
+ </AlertDialog.Root>
777
+ );
778
+
779
+ await waitFor( () => {
780
+ expect( screen.getByRole( 'alertdialog' ) ).toBeVisible();
781
+ } );
782
+
783
+ await userEvent.click(
784
+ screen.getByRole( 'button', { name: 'Cancel' } )
785
+ );
786
+
787
+ await waitFor( () => {
788
+ expect(
789
+ screen.queryByRole( 'alertdialog' )
790
+ ).not.toBeInTheDocument();
791
+ } );
792
+
793
+ await userEvent.click(
794
+ screen.getByRole( 'button', { name: 'Open' } )
795
+ );
796
+
797
+ await waitFor( () => {
798
+ expect( screen.getByRole( 'alertdialog' ) ).toBeVisible();
799
+ } );
800
+ } );
801
+
802
+ it( 'opens and closes via cancel', async () => {
803
+ const onConfirm = jest.fn();
804
+
805
+ render(
806
+ <AlertDialog.Root onConfirm={ onConfirm }>
807
+ <AlertDialog.Trigger>Open</AlertDialog.Trigger>
808
+ <AlertDialog.Popup title="Uncontrolled">
809
+ Content
810
+ </AlertDialog.Popup>
811
+ </AlertDialog.Root>
812
+ );
813
+
814
+ expect( screen.queryByText( 'Content' ) ).not.toBeInTheDocument();
815
+
816
+ await userEvent.click(
817
+ screen.getByRole( 'button', { name: 'Open' } )
818
+ );
819
+
820
+ await waitFor( () => {
821
+ expect( screen.getByText( 'Uncontrolled' ) ).toBeVisible();
822
+ } );
823
+
824
+ await userEvent.click(
825
+ screen.getByRole( 'button', { name: 'Cancel' } )
826
+ );
827
+
828
+ await waitFor( () => {
829
+ expect(
830
+ screen.queryByText( 'Uncontrolled' )
831
+ ).not.toBeInTheDocument();
832
+ } );
833
+ } );
834
+
835
+ it( 'closes and unmounts dialog via confirm click', async () => {
836
+ const onConfirm = jest.fn();
837
+
838
+ render(
839
+ <AlertDialog.Root onConfirm={ onConfirm }>
840
+ <AlertDialog.Trigger>Open</AlertDialog.Trigger>
841
+ <AlertDialog.Popup title="Uncontrolled Confirm">
842
+ Content
843
+ </AlertDialog.Popup>
844
+ </AlertDialog.Root>
845
+ );
846
+
847
+ await userEvent.click(
848
+ screen.getByRole( 'button', { name: 'Open' } )
849
+ );
850
+
851
+ await waitFor( () => {
852
+ expect(
853
+ screen.getByText( 'Uncontrolled Confirm' )
854
+ ).toBeVisible();
855
+ } );
856
+
857
+ await userEvent.click(
858
+ screen.getByRole( 'button', { name: 'OK' } )
859
+ );
860
+
861
+ expect( onConfirm ).toHaveBeenCalledTimes( 1 );
862
+
863
+ await waitFor( () => {
864
+ expect(
865
+ screen.queryByRole( 'alertdialog' )
866
+ ).not.toBeInTheDocument();
867
+ } );
868
+ } );
869
+ } );
870
+
871
+ describe( 'edge cases', () => {
872
+ it( 'does not error when unmounted during pending', async () => {
873
+ const deferred = createDeferred();
874
+
875
+ const { unmount } = render(
876
+ <AlertDialog.Root
877
+ open
878
+ onOpenChange={ jest.fn() }
879
+ onConfirm={ () => deferred.promise }
880
+ >
881
+ <AlertDialog.Popup title="Unmount Test">
882
+ Content
883
+ </AlertDialog.Popup>
884
+ </AlertDialog.Root>
885
+ );
886
+
887
+ await waitFor( () => {
888
+ expect(
889
+ screen.getByRole( 'button', { name: 'OK' } )
890
+ ).toBeVisible();
891
+ } );
892
+
893
+ await userEvent.click(
894
+ screen.getByRole( 'button', { name: 'OK' } )
895
+ );
896
+
897
+ await waitFor( () => {
898
+ expect(
899
+ screen.getByRole( 'button', { name: 'OK' } )
900
+ ).toHaveAttribute( 'aria-disabled', 'true' );
901
+ } );
902
+
903
+ // Unmount while pending — should not throw
904
+ unmount();
905
+
906
+ // Resolve the deferred — should be a no-op after unmount
907
+ await act( async () => {
908
+ deferred.resolve();
909
+ } );
910
+ } );
911
+
912
+ it( 'controlled mode: recovers to idle when consumer keeps dialog open after confirm', async () => {
913
+ const onConfirm = jest.fn();
914
+
915
+ render(
916
+ <AlertDialog.Root
917
+ open
918
+ onOpenChange={ jest.fn() }
919
+ onConfirm={ onConfirm }
920
+ >
921
+ <AlertDialog.Popup title="Deadlock Test">
922
+ Content
923
+ </AlertDialog.Popup>
924
+ </AlertDialog.Root>
925
+ );
926
+
927
+ await waitFor( () => {
928
+ expect(
929
+ screen.getByRole( 'button', { name: 'OK' } )
930
+ ).toBeVisible();
931
+ } );
932
+
933
+ await userEvent.click(
934
+ screen.getByRole( 'button', { name: 'OK' } )
935
+ );
936
+
937
+ expect( onConfirm ).toHaveBeenCalledTimes( 1 );
938
+
939
+ // Consumer passes open={true} and does NOT update it in
940
+ // onOpenChange, so phase would be stuck at 'closing'.
941
+ // The safety-net useEffect should recover phase to 'idle'.
942
+ await waitFor( () => {
943
+ expect(
944
+ screen.getByRole( 'button', { name: 'OK' } )
945
+ ).not.toHaveAttribute( 'aria-disabled', 'true' );
946
+ } );
947
+
948
+ expect(
949
+ screen.getByRole( 'button', { name: 'Cancel' } )
950
+ ).not.toHaveAttribute( 'aria-disabled', 'true' );
951
+ } );
952
+
953
+ it( 'recovers when onConfirm throws synchronously', async () => {
954
+ const onConfirm = jest.fn( () => {
955
+ throw new Error( 'Sync error' );
956
+ } );
957
+ const onOpenChange = jest.fn();
958
+ const consoleSpy = jest
959
+ .spyOn( console, 'error' )
960
+ .mockImplementation( () => {} );
961
+
962
+ render(
963
+ <AlertDialog.Root
964
+ open
965
+ onOpenChange={ onOpenChange }
966
+ onConfirm={ onConfirm }
967
+ >
968
+ <AlertDialog.Popup title="Throw Test">
969
+ Content
970
+ </AlertDialog.Popup>
971
+ </AlertDialog.Root>
972
+ );
973
+
974
+ await waitFor( () => {
975
+ expect(
976
+ screen.getByRole( 'button', { name: 'OK' } )
977
+ ).toBeVisible();
978
+ } );
979
+
980
+ await userEvent.click(
981
+ screen.getByRole( 'button', { name: 'OK' } )
982
+ );
983
+
984
+ // The error is caught and logged via console.error in Root.
985
+ await waitFor( () => {
986
+ expect( consoleSpy ).toHaveBeenCalledWith(
987
+ expect.objectContaining( { message: 'Sync error' } )
988
+ );
989
+ } );
990
+
991
+ // Dialog stays open and buttons return to idle
992
+ await waitFor( () => {
993
+ expect(
994
+ screen.getByRole( 'button', { name: 'OK' } )
995
+ ).not.toHaveAttribute( 'aria-disabled', 'true' );
996
+ } );
997
+
998
+ expect( onOpenChange ).not.toHaveBeenCalledWith(
999
+ false,
1000
+ expect.anything()
1001
+ );
1002
+ expect( screen.getByText( 'Throw Test' ) ).toBeVisible();
1003
+
1004
+ // Throws do NOT render a visible error message.
1005
+ expect(
1006
+ screen.queryByText( 'Sync error' )
1007
+ ).not.toBeInTheDocument();
1008
+
1009
+ consoleSpy.mockRestore();
1010
+ } );
1011
+
1012
+ it( 'sets aria-describedby when description is provided', async () => {
1013
+ render(
1014
+ <AlertDialog.Root open onOpenChange={ jest.fn() }>
1015
+ <AlertDialog.Popup
1016
+ title="Describedby Test"
1017
+ description="A helpful description"
1018
+ >
1019
+ Content
1020
+ </AlertDialog.Popup>
1021
+ </AlertDialog.Root>
1022
+ );
1023
+
1024
+ await waitFor( () => {
1025
+ expect( screen.getByRole( 'alertdialog' ) ).toBeVisible();
1026
+ } );
1027
+
1028
+ const dialog = screen.getByRole( 'alertdialog' );
1029
+ expect( dialog ).toHaveAccessibleDescription(
1030
+ 'A helpful description'
1031
+ );
1032
+ } );
1033
+
1034
+ it( 'allows re-confirm after { close: false, error }', async () => {
1035
+ const deferred = createDeferred();
1036
+ const onOpenChange = jest.fn();
1037
+
1038
+ render(
1039
+ <AlertDialog.Root
1040
+ open
1041
+ onOpenChange={ onOpenChange }
1042
+ onConfirm={ () => deferred.promise }
1043
+ >
1044
+ <AlertDialog.Popup title="Error Retry">
1045
+ Content
1046
+ </AlertDialog.Popup>
1047
+ </AlertDialog.Root>
1048
+ );
1049
+
1050
+ await waitFor( () => {
1051
+ expect(
1052
+ screen.getByRole( 'button', { name: 'OK' } )
1053
+ ).toBeVisible();
1054
+ } );
1055
+
1056
+ // First confirm — returns error
1057
+ await userEvent.click(
1058
+ screen.getByRole( 'button', { name: 'OK' } )
1059
+ );
1060
+
1061
+ await act( async () => {
1062
+ deferred.resolve( {
1063
+ close: false,
1064
+ error: 'Validation failed',
1065
+ } );
1066
+ } );
1067
+
1068
+ await waitFor( () => {
1069
+ expect( screen.getByText( 'Validation failed' ) ).toBeVisible();
1070
+ } );
1071
+
1072
+ // Buttons are re-enabled, user can retry
1073
+ expect(
1074
+ screen.getByRole( 'button', { name: 'OK' } )
1075
+ ).not.toHaveAttribute( 'aria-disabled', 'true' );
1076
+ } );
1077
+
1078
+ it( 'allows re-confirm after { close: false }', async () => {
1079
+ let callCount = 0;
1080
+ const onConfirm = jest.fn( (): { close: boolean } | undefined => {
1081
+ callCount++;
1082
+ if ( callCount === 1 ) {
1083
+ return { close: false };
1084
+ }
1085
+ return undefined;
1086
+ } );
1087
+ const onOpenChange = jest.fn();
1088
+
1089
+ render(
1090
+ <AlertDialog.Root
1091
+ open
1092
+ onOpenChange={ onOpenChange }
1093
+ onConfirm={ onConfirm }
1094
+ >
1095
+ <AlertDialog.Popup title="Retry Test">
1096
+ Content
1097
+ </AlertDialog.Popup>
1098
+ </AlertDialog.Root>
1099
+ );
1100
+
1101
+ await waitFor( () => {
1102
+ expect(
1103
+ screen.getByRole( 'button', { name: 'OK' } )
1104
+ ).toBeVisible();
1105
+ } );
1106
+
1107
+ // First confirm — returns { close: false }
1108
+ await userEvent.click(
1109
+ screen.getByRole( 'button', { name: 'OK' } )
1110
+ );
1111
+
1112
+ await waitFor( () => {
1113
+ expect(
1114
+ screen.getByRole( 'button', { name: 'OK' } )
1115
+ ).not.toHaveAttribute( 'aria-disabled', 'true' );
1116
+ } );
1117
+
1118
+ expect( onOpenChange ).not.toHaveBeenCalledWith(
1119
+ false,
1120
+ expect.anything()
1121
+ );
1122
+
1123
+ // Second confirm — returns void → should close
1124
+ await userEvent.click(
1125
+ screen.getByRole( 'button', { name: 'OK' } )
1126
+ );
1127
+
1128
+ await waitFor( () => {
1129
+ expect( onOpenChange ).toHaveBeenCalledWith(
1130
+ false,
1131
+ expect.objectContaining( {
1132
+ reason: 'imperative-action',
1133
+ } )
1134
+ );
1135
+ } );
1136
+
1137
+ expect( onConfirm ).toHaveBeenCalledTimes( 2 );
1138
+ } );
1139
+ } );
1140
+
1141
+ describe( 'error handling', () => {
1142
+ beforeEach( () => {
1143
+ ( speak as jest.Mock ).mockClear();
1144
+ } );
1145
+
1146
+ it( 'displays error message when onConfirm returns { close: false, error }', async () => {
1147
+ render(
1148
+ <AlertDialog.Root
1149
+ open
1150
+ onOpenChange={ jest.fn() }
1151
+ onConfirm={ () => ( {
1152
+ close: false,
1153
+ error: 'Something went wrong.',
1154
+ } ) }
1155
+ >
1156
+ <AlertDialog.Popup title="Error Test">
1157
+ Content
1158
+ </AlertDialog.Popup>
1159
+ </AlertDialog.Root>
1160
+ );
1161
+
1162
+ await waitFor( () => {
1163
+ expect(
1164
+ screen.getByRole( 'button', { name: 'OK' } )
1165
+ ).toBeVisible();
1166
+ } );
1167
+
1168
+ await userEvent.click(
1169
+ screen.getByRole( 'button', { name: 'OK' } )
1170
+ );
1171
+
1172
+ await waitFor( () => {
1173
+ expect(
1174
+ screen.getByText( 'Something went wrong.' )
1175
+ ).toBeVisible();
1176
+ } );
1177
+ } );
1178
+
1179
+ it( 'displays error message from async onConfirm', async () => {
1180
+ const deferred = createDeferred();
1181
+
1182
+ render(
1183
+ <AlertDialog.Root
1184
+ open
1185
+ onOpenChange={ jest.fn() }
1186
+ onConfirm={ () => deferred.promise }
1187
+ >
1188
+ <AlertDialog.Popup title="Async Error">
1189
+ Content
1190
+ </AlertDialog.Popup>
1191
+ </AlertDialog.Root>
1192
+ );
1193
+
1194
+ await waitFor( () => {
1195
+ expect(
1196
+ screen.getByRole( 'button', { name: 'OK' } )
1197
+ ).toBeVisible();
1198
+ } );
1199
+
1200
+ await userEvent.click(
1201
+ screen.getByRole( 'button', { name: 'OK' } )
1202
+ );
1203
+
1204
+ await act( async () => {
1205
+ deferred.resolve( {
1206
+ close: false,
1207
+ error: 'Server error occurred.',
1208
+ } );
1209
+ } );
1210
+
1211
+ await waitFor( () => {
1212
+ expect(
1213
+ screen.getByText( 'Server error occurred.' )
1214
+ ).toBeVisible();
1215
+ } );
1216
+
1217
+ // Buttons return to idle
1218
+ expect(
1219
+ screen.getByRole( 'button', { name: 'OK' } )
1220
+ ).not.toHaveAttribute( 'aria-disabled', 'true' );
1221
+ } );
1222
+
1223
+ it( 'stays open when error is returned without explicit close: false', async () => {
1224
+ const onOpenChange = jest.fn();
1225
+
1226
+ render(
1227
+ <AlertDialog.Root
1228
+ open
1229
+ onOpenChange={ onOpenChange }
1230
+ onConfirm={ () => ( { error: 'Implicit stay open.' } ) }
1231
+ >
1232
+ <AlertDialog.Popup title="Implicit Close">
1233
+ Content
1234
+ </AlertDialog.Popup>
1235
+ </AlertDialog.Root>
1236
+ );
1237
+
1238
+ await waitFor( () => {
1239
+ expect(
1240
+ screen.getByRole( 'button', { name: 'OK' } )
1241
+ ).toBeVisible();
1242
+ } );
1243
+
1244
+ await userEvent.click(
1245
+ screen.getByRole( 'button', { name: 'OK' } )
1246
+ );
1247
+
1248
+ await waitFor( () => {
1249
+ expect(
1250
+ screen.getByText( 'Implicit stay open.' )
1251
+ ).toBeVisible();
1252
+ } );
1253
+
1254
+ // Dialog stays open — onOpenChange(false) was not called
1255
+ expect( onOpenChange ).not.toHaveBeenCalledWith(
1256
+ false,
1257
+ expect.anything()
1258
+ );
1259
+ } );
1260
+
1261
+ it( 'clears error message on next confirm attempt', async () => {
1262
+ let callCount = 0;
1263
+ const onConfirm = jest.fn( (): ConfirmResult => {
1264
+ callCount++;
1265
+ if ( callCount === 1 ) {
1266
+ return {
1267
+ close: false,
1268
+ error: 'First attempt failed.',
1269
+ };
1270
+ }
1271
+ return undefined;
1272
+ } );
1273
+
1274
+ render(
1275
+ <AlertDialog.Root
1276
+ open
1277
+ onOpenChange={ jest.fn() }
1278
+ onConfirm={ onConfirm }
1279
+ >
1280
+ <AlertDialog.Popup title="Clear Error">
1281
+ Content
1282
+ </AlertDialog.Popup>
1283
+ </AlertDialog.Root>
1284
+ );
1285
+
1286
+ await waitFor( () => {
1287
+ expect(
1288
+ screen.getByRole( 'button', { name: 'OK' } )
1289
+ ).toBeVisible();
1290
+ } );
1291
+
1292
+ // First confirm — shows error
1293
+ await userEvent.click(
1294
+ screen.getByRole( 'button', { name: 'OK' } )
1295
+ );
1296
+
1297
+ await waitFor( () => {
1298
+ expect(
1299
+ screen.getByText( 'First attempt failed.' )
1300
+ ).toBeVisible();
1301
+ } );
1302
+
1303
+ // Second confirm — error should be cleared
1304
+ await userEvent.click(
1305
+ screen.getByRole( 'button', { name: 'OK' } )
1306
+ );
1307
+
1308
+ await waitFor( () => {
1309
+ expect(
1310
+ screen.queryByText( 'First attempt failed.' )
1311
+ ).not.toBeInTheDocument();
1312
+ } );
1313
+ } );
1314
+
1315
+ it( 'clears error message when dialog reopens', async () => {
1316
+ render(
1317
+ <AlertDialog.Root
1318
+ onConfirm={ () => ( {
1319
+ close: false,
1320
+ error: 'Persistent error.',
1321
+ } ) }
1322
+ >
1323
+ <AlertDialog.Trigger>Open</AlertDialog.Trigger>
1324
+ <AlertDialog.Popup title="Reopen Clear">
1325
+ Content
1326
+ </AlertDialog.Popup>
1327
+ </AlertDialog.Root>
1328
+ );
1329
+
1330
+ // Open dialog
1331
+ await userEvent.click(
1332
+ screen.getByRole( 'button', { name: 'Open' } )
1333
+ );
1334
+
1335
+ await waitFor( () => {
1336
+ expect(
1337
+ screen.getByRole( 'button', { name: 'OK' } )
1338
+ ).toBeVisible();
1339
+ } );
1340
+
1341
+ // Trigger error
1342
+ await userEvent.click(
1343
+ screen.getByRole( 'button', { name: 'OK' } )
1344
+ );
1345
+
1346
+ await waitFor( () => {
1347
+ expect( screen.getByText( 'Persistent error.' ) ).toBeVisible();
1348
+ } );
1349
+
1350
+ // Close via cancel
1351
+ await userEvent.click(
1352
+ screen.getByRole( 'button', { name: 'Cancel' } )
1353
+ );
1354
+
1355
+ await waitFor( () => {
1356
+ expect(
1357
+ screen.queryByRole( 'alertdialog' )
1358
+ ).not.toBeInTheDocument();
1359
+ } );
1360
+
1361
+ // Reopen — error should be gone
1362
+ await userEvent.click(
1363
+ screen.getByRole( 'button', { name: 'Open' } )
1364
+ );
1365
+
1366
+ await waitFor( () => {
1367
+ expect(
1368
+ screen.getByRole( 'button', { name: 'OK' } )
1369
+ ).toBeVisible();
1370
+ } );
1371
+
1372
+ expect(
1373
+ screen.queryByText( 'Persistent error.' )
1374
+ ).not.toBeInTheDocument();
1375
+ } );
1376
+
1377
+ it( 'announces error message to screen readers via speak()', async () => {
1378
+ render(
1379
+ <AlertDialog.Root
1380
+ open
1381
+ onOpenChange={ jest.fn() }
1382
+ onConfirm={ () => ( {
1383
+ close: false,
1384
+ error: 'Announced error.',
1385
+ } ) }
1386
+ >
1387
+ <AlertDialog.Popup title="Speak Test">
1388
+ Content
1389
+ </AlertDialog.Popup>
1390
+ </AlertDialog.Root>
1391
+ );
1392
+
1393
+ await waitFor( () => {
1394
+ expect(
1395
+ screen.getByRole( 'button', { name: 'OK' } )
1396
+ ).toBeVisible();
1397
+ } );
1398
+
1399
+ await userEvent.click(
1400
+ screen.getByRole( 'button', { name: 'OK' } )
1401
+ );
1402
+
1403
+ await waitFor( () => {
1404
+ expect( speak ).toHaveBeenCalledWith(
1405
+ 'Announced error.',
1406
+ 'assertive'
1407
+ );
1408
+ } );
1409
+ } );
1410
+
1411
+ it( 'does not show error message when onConfirm throws', async () => {
1412
+ const consoleSpy = jest
1413
+ .spyOn( console, 'error' )
1414
+ .mockImplementation( () => {} );
1415
+
1416
+ render(
1417
+ <AlertDialog.Root
1418
+ open
1419
+ onOpenChange={ jest.fn() }
1420
+ onConfirm={ () => {
1421
+ throw new Error( 'Unhandled throw' );
1422
+ } }
1423
+ >
1424
+ <AlertDialog.Popup title="No Error Display">
1425
+ Content
1426
+ </AlertDialog.Popup>
1427
+ </AlertDialog.Root>
1428
+ );
1429
+
1430
+ await waitFor( () => {
1431
+ expect(
1432
+ screen.getByRole( 'button', { name: 'OK' } )
1433
+ ).toBeVisible();
1434
+ } );
1435
+
1436
+ await userEvent.click(
1437
+ screen.getByRole( 'button', { name: 'OK' } )
1438
+ );
1439
+
1440
+ await waitFor( () => {
1441
+ expect( consoleSpy ).toHaveBeenCalledWith(
1442
+ expect.objectContaining( { message: 'Unhandled throw' } )
1443
+ );
1444
+ } );
1445
+
1446
+ // No error message rendered — throws don't trigger the error UI
1447
+ expect(
1448
+ screen.queryByText( 'Unhandled throw' )
1449
+ ).not.toBeInTheDocument();
1450
+ expect( speak ).not.toHaveBeenCalled();
1451
+
1452
+ consoleSpy.mockRestore();
1453
+ } );
1454
+ } );
1455
+
1456
+ describe( 'container', () => {
1457
+ it( 'should render inside the container when provided', async () => {
1458
+ const user = userEvent.setup();
1459
+ const containerRef = createRef< HTMLDivElement >();
1460
+
1461
+ render(
1462
+ <div data-testid="wrapper">
1463
+ <AlertDialog.Root>
1464
+ <AlertDialog.Trigger>Open</AlertDialog.Trigger>
1465
+ <div
1466
+ ref={ containerRef }
1467
+ data-testid="custom-container"
1468
+ />
1469
+ <AlertDialog.Popup
1470
+ title="Confirm"
1471
+ container={ containerRef }
1472
+ />
1473
+ </AlertDialog.Root>
1474
+ </div>
1475
+ );
1476
+
1477
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
1478
+
1479
+ const dialog = await screen.findByRole( 'alertdialog' );
1480
+ expect( dialog ).toBeVisible();
1481
+
1482
+ expect( screen.getByTestId( 'custom-container' ) ).toContainElement(
1483
+ dialog
1484
+ );
1485
+ } );
1486
+
1487
+ it( 'should render with a portal by default', async () => {
1488
+ const user = userEvent.setup();
1489
+
1490
+ render(
1491
+ <div data-testid="wrapper">
1492
+ <AlertDialog.Root>
1493
+ <AlertDialog.Trigger>Open</AlertDialog.Trigger>
1494
+ <AlertDialog.Popup title="Confirm" />
1495
+ </AlertDialog.Root>
1496
+ </div>
1497
+ );
1498
+
1499
+ await user.click( screen.getByRole( 'button', { name: 'Open' } ) );
1500
+
1501
+ const dialog = await screen.findByRole( 'alertdialog' );
1502
+ expect( dialog ).toBeVisible();
1503
+
1504
+ expect( screen.getByTestId( 'wrapper' ) ).not.toContainElement(
1505
+ dialog
1506
+ );
1507
+ } );
1508
+ } );
1509
+ } );