@wordpress/components 25.9.1 → 25.11.1-next.f8d8eceb.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 (364) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/build/alignment-matrix-control/cell.js +8 -5
  3. package/build/alignment-matrix-control/cell.js.map +1 -1
  4. package/build/alignment-matrix-control/index.js +27 -43
  5. package/build/alignment-matrix-control/index.js.map +1 -1
  6. package/build/alignment-matrix-control/utils.js +29 -9
  7. package/build/alignment-matrix-control/utils.js.map +1 -1
  8. package/build/autocomplete/index.js +104 -52
  9. package/build/autocomplete/index.js.map +1 -1
  10. package/build/circular-option-picker/circular-option-picker-option.js +20 -39
  11. package/build/circular-option-picker/circular-option-picker-option.js.map +1 -1
  12. package/build/circular-option-picker/circular-option-picker.js +11 -32
  13. package/build/circular-option-picker/circular-option-picker.js.map +1 -1
  14. package/build/circular-option-picker/types.js.map +1 -1
  15. package/build/color-palette/index.js +7 -2
  16. package/build/color-palette/index.js.map +1 -1
  17. package/build/color-picker/component.js +12 -2
  18. package/build/color-picker/component.js.map +1 -1
  19. package/build/color-picker/picker.js +77 -1
  20. package/build/color-picker/picker.js.map +1 -1
  21. package/build/color-picker/styles.js +8 -8
  22. package/build/color-picker/styles.js.map +1 -1
  23. package/build/color-picker/types.js.map +1 -1
  24. package/build/composite/v2.js +43 -0
  25. package/build/composite/v2.js.map +1 -0
  26. package/build/confirm-dialog/component.js +74 -8
  27. package/build/confirm-dialog/component.js.map +1 -1
  28. package/build/confirm-dialog/types.js.map +1 -1
  29. package/build/custom-gradient-picker/gradient-bar/control-points.js +13 -4
  30. package/build/custom-gradient-picker/gradient-bar/control-points.js.map +1 -1
  31. package/build/dropdown-menu-v2-ariakit/index.js +217 -0
  32. package/build/dropdown-menu-v2-ariakit/index.js.map +1 -0
  33. package/build/dropdown-menu-v2-ariakit/styles.js +157 -0
  34. package/build/dropdown-menu-v2-ariakit/styles.js.map +1 -0
  35. package/build/dropdown-menu-v2-ariakit/types.js +6 -0
  36. package/build/dropdown-menu-v2-ariakit/types.js.map +1 -0
  37. package/build/font-size-picker/utils.js +1 -1
  38. package/build/font-size-picker/utils.js.map +1 -1
  39. package/build/input-control/styles/input-control-styles.js +23 -23
  40. package/build/input-control/styles/input-control-styles.js.map +1 -1
  41. package/build/mobile/global-styles-context/utils.native.js +1 -1
  42. package/build/mobile/global-styles-context/utils.native.js.map +1 -1
  43. package/build/modal/index.js +45 -16
  44. package/build/modal/index.js.map +1 -1
  45. package/build/palette-edit/index.js +4 -0
  46. package/build/palette-edit/index.js.map +1 -1
  47. package/build/popover/index.js +34 -6
  48. package/build/popover/index.js.map +1 -1
  49. package/build/private-apis.js +18 -2
  50. package/build/private-apis.js.map +1 -1
  51. package/build/progress-bar/styles.js +5 -5
  52. package/build/progress-bar/styles.js.map +1 -1
  53. package/build/sandbox/index.js +1 -1
  54. package/build/sandbox/index.js.map +1 -1
  55. package/build/sandbox/index.native.js +1 -1
  56. package/build/sandbox/index.native.js.map +1 -1
  57. package/build/select-control/styles/select-control-styles.js +8 -8
  58. package/build/select-control/styles/select-control-styles.js.map +1 -1
  59. package/build/tabs/context.js +16 -0
  60. package/build/tabs/context.js.map +1 -0
  61. package/build/tabs/index.js +147 -0
  62. package/build/tabs/index.js.map +1 -0
  63. package/build/tabs/styles.js +38 -0
  64. package/build/tabs/styles.js.map +1 -0
  65. package/build/tabs/tab.js +46 -0
  66. package/build/tabs/tab.js.map +1 -0
  67. package/build/tabs/tablist.js +47 -0
  68. package/build/tabs/tablist.js.map +1 -0
  69. package/build/tabs/tabpanel.js +48 -0
  70. package/build/tabs/tabpanel.js.map +1 -0
  71. package/build/tabs/types.js +6 -0
  72. package/build/tabs/types.js.map +1 -0
  73. package/build/text/component.js +7 -6
  74. package/build/text/component.js.map +1 -1
  75. package/build/text/hook.js +6 -15
  76. package/build/text/hook.js.map +1 -1
  77. package/build/text/index.js.map +1 -1
  78. package/build/text/styles.js +7 -7
  79. package/build/text/styles.js.map +1 -1
  80. package/build/text/types.js.map +1 -1
  81. package/build/text/utils.js +17 -33
  82. package/build/text/utils.js.map +1 -1
  83. package/build/toggle-group-control/toggle-group-control-option-base/component.js +1 -0
  84. package/build/toggle-group-control/toggle-group-control-option-base/component.js.map +1 -1
  85. package/build/toolbar/toolbar/index.js +17 -10
  86. package/build/toolbar/toolbar/index.js.map +1 -1
  87. package/build/toolbar/toolbar/types.js.map +1 -1
  88. package/build/tools-panel/tools-panel-item/hook.js +2 -2
  89. package/build/tools-panel/tools-panel-item/hook.js.map +1 -1
  90. package/build/tools-panel/types.js.map +1 -1
  91. package/build/tooltip/index.js +2 -2
  92. package/build/tooltip/index.js.map +1 -1
  93. package/build/unit-control/utils.js +108 -0
  94. package/build/unit-control/utils.js.map +1 -1
  95. package/build/utils/unit-values.js +1 -1
  96. package/build/utils/unit-values.js.map +1 -1
  97. package/build-module/alignment-matrix-control/cell.js +7 -4
  98. package/build-module/alignment-matrix-control/cell.js.map +1 -1
  99. package/build-module/alignment-matrix-control/index.js +27 -43
  100. package/build-module/alignment-matrix-control/index.js.map +1 -1
  101. package/build-module/alignment-matrix-control/utils.js +29 -8
  102. package/build-module/alignment-matrix-control/utils.js.map +1 -1
  103. package/build-module/autocomplete/index.js +104 -52
  104. package/build-module/autocomplete/index.js.map +1 -1
  105. package/build-module/circular-option-picker/circular-option-picker-option.js +20 -39
  106. package/build-module/circular-option-picker/circular-option-picker-option.js.map +1 -1
  107. package/build-module/circular-option-picker/circular-option-picker.js +10 -31
  108. package/build-module/circular-option-picker/circular-option-picker.js.map +1 -1
  109. package/build-module/circular-option-picker/types.js.map +1 -1
  110. package/build-module/color-palette/index.js +7 -2
  111. package/build-module/color-palette/index.js.map +1 -1
  112. package/build-module/color-picker/component.js +13 -3
  113. package/build-module/color-picker/component.js.map +1 -1
  114. package/build-module/color-picker/picker.js +78 -2
  115. package/build-module/color-picker/picker.js.map +1 -1
  116. package/build-module/color-picker/styles.js +8 -8
  117. package/build-module/color-picker/styles.js.map +1 -1
  118. package/build-module/color-picker/types.js.map +1 -1
  119. package/build-module/composite/v2.js +15 -0
  120. package/build-module/composite/v2.js.map +1 -0
  121. package/build-module/confirm-dialog/component.js +72 -7
  122. package/build-module/confirm-dialog/component.js.map +1 -1
  123. package/build-module/confirm-dialog/types.js.map +1 -1
  124. package/build-module/custom-gradient-picker/gradient-bar/control-points.js +13 -4
  125. package/build-module/custom-gradient-picker/gradient-bar/control-points.js.map +1 -1
  126. package/build-module/dropdown-menu-v2-ariakit/index.js +199 -0
  127. package/build-module/dropdown-menu-v2-ariakit/index.js.map +1 -0
  128. package/build-module/dropdown-menu-v2-ariakit/styles.js +136 -0
  129. package/build-module/dropdown-menu-v2-ariakit/styles.js.map +1 -0
  130. package/build-module/dropdown-menu-v2-ariakit/types.js +2 -0
  131. package/build-module/dropdown-menu-v2-ariakit/types.js.map +1 -0
  132. package/build-module/font-size-picker/utils.js +1 -1
  133. package/build-module/font-size-picker/utils.js.map +1 -1
  134. package/build-module/input-control/styles/input-control-styles.js +23 -23
  135. package/build-module/input-control/styles/input-control-styles.js.map +1 -1
  136. package/build-module/mobile/global-styles-context/utils.native.js +2 -2
  137. package/build-module/mobile/global-styles-context/utils.native.js.map +1 -1
  138. package/build-module/modal/index.js +47 -18
  139. package/build-module/modal/index.js.map +1 -1
  140. package/build-module/palette-edit/index.js +4 -0
  141. package/build-module/palette-edit/index.js.map +1 -1
  142. package/build-module/popover/index.js +34 -6
  143. package/build-module/popover/index.js.map +1 -1
  144. package/build-module/private-apis.js +18 -2
  145. package/build-module/private-apis.js.map +1 -1
  146. package/build-module/progress-bar/styles.js +5 -5
  147. package/build-module/progress-bar/styles.js.map +1 -1
  148. package/build-module/sandbox/index.js +1 -1
  149. package/build-module/sandbox/index.js.map +1 -1
  150. package/build-module/sandbox/index.native.js +1 -1
  151. package/build-module/sandbox/index.native.js.map +1 -1
  152. package/build-module/select-control/styles/select-control-styles.js +8 -8
  153. package/build-module/select-control/styles/select-control-styles.js.map +1 -1
  154. package/build-module/tabs/context.js +12 -0
  155. package/build-module/tabs/context.js.map +1 -0
  156. package/build-module/tabs/index.js +142 -0
  157. package/build-module/tabs/index.js.map +1 -0
  158. package/build-module/tabs/styles.js +36 -0
  159. package/build-module/tabs/styles.js.map +1 -0
  160. package/build-module/tabs/tab.js +43 -0
  161. package/build-module/tabs/tab.js.map +1 -0
  162. package/build-module/tabs/tablist.js +41 -0
  163. package/build-module/tabs/tablist.js.map +1 -0
  164. package/build-module/tabs/tabpanel.js +43 -0
  165. package/build-module/tabs/tabpanel.js.map +1 -0
  166. package/build-module/tabs/types.js +2 -0
  167. package/build-module/tabs/types.js.map +1 -0
  168. package/build-module/text/component.js +6 -6
  169. package/build-module/text/component.js.map +1 -1
  170. package/build-module/text/hook.js +11 -19
  171. package/build-module/text/hook.js.map +1 -1
  172. package/build-module/text/index.js.map +1 -1
  173. package/build-module/text/styles.js +7 -7
  174. package/build-module/text/styles.js.map +1 -1
  175. package/build-module/text/types.js.map +1 -1
  176. package/build-module/text/utils.js +17 -10
  177. package/build-module/text/utils.js.map +1 -1
  178. package/build-module/toggle-group-control/toggle-group-control-option-base/component.js +1 -0
  179. package/build-module/toggle-group-control/toggle-group-control-option-base/component.js.map +1 -1
  180. package/build-module/toolbar/toolbar/index.js +18 -11
  181. package/build-module/toolbar/toolbar/index.js.map +1 -1
  182. package/build-module/toolbar/toolbar/types.js.map +1 -1
  183. package/build-module/tools-panel/tools-panel-item/hook.js +2 -2
  184. package/build-module/tools-panel/tools-panel-item/hook.js.map +1 -1
  185. package/build-module/tools-panel/types.js.map +1 -1
  186. package/build-module/tooltip/index.js +2 -2
  187. package/build-module/tooltip/index.js.map +1 -1
  188. package/build-module/unit-control/utils.js +108 -0
  189. package/build-module/unit-control/utils.js.map +1 -1
  190. package/build-module/utils/unit-values.js +1 -1
  191. package/build-module/utils/unit-values.js.map +1 -1
  192. package/build-style/style-rtl.css +17 -5
  193. package/build-style/style.css +17 -5
  194. package/build-types/alignment-matrix-control/cell.d.ts +1 -1
  195. package/build-types/alignment-matrix-control/cell.d.ts.map +1 -1
  196. package/build-types/alignment-matrix-control/index.d.ts.map +1 -1
  197. package/build-types/alignment-matrix-control/stories/index.story.d.ts.map +1 -1
  198. package/build-types/alignment-matrix-control/utils.d.ts +9 -9
  199. package/build-types/alignment-matrix-control/utils.d.ts.map +1 -1
  200. package/build-types/autocomplete/index.d.ts.map +1 -1
  201. package/build-types/circular-option-picker/circular-option-picker-option.d.ts.map +1 -1
  202. package/build-types/circular-option-picker/circular-option-picker.d.ts.map +1 -1
  203. package/build-types/circular-option-picker/types.d.ts +4 -6
  204. package/build-types/circular-option-picker/types.d.ts.map +1 -1
  205. package/build-types/color-palette/index.d.ts.map +1 -1
  206. package/build-types/color-picker/component.d.ts.map +1 -1
  207. package/build-types/color-picker/picker.d.ts +1 -1
  208. package/build-types/color-picker/picker.d.ts.map +1 -1
  209. package/build-types/color-picker/styles.d.ts.map +1 -1
  210. package/build-types/color-picker/types.d.ts +3 -0
  211. package/build-types/color-picker/types.d.ts.map +1 -1
  212. package/build-types/composite/v2.d.ts +12 -0
  213. package/build-types/composite/v2.d.ts.map +1 -0
  214. package/build-types/confirm-dialog/component.d.ts +70 -29
  215. package/build-types/confirm-dialog/component.d.ts.map +1 -1
  216. package/build-types/confirm-dialog/stories/index.story.d.ts +11 -0
  217. package/build-types/confirm-dialog/stories/index.story.d.ts.map +1 -0
  218. package/build-types/confirm-dialog/test/index.d.ts +2 -0
  219. package/build-types/confirm-dialog/test/index.d.ts.map +1 -0
  220. package/build-types/confirm-dialog/types.d.ts +32 -10
  221. package/build-types/confirm-dialog/types.d.ts.map +1 -1
  222. package/build-types/custom-gradient-picker/gradient-bar/control-points.d.ts.map +1 -1
  223. package/build-types/dropdown-menu-v2-ariakit/index.d.ts +11 -0
  224. package/build-types/dropdown-menu-v2-ariakit/index.d.ts.map +1 -0
  225. package/build-types/dropdown-menu-v2-ariakit/stories/index.story.d.ts +16 -0
  226. package/build-types/dropdown-menu-v2-ariakit/stories/index.story.d.ts.map +1 -0
  227. package/build-types/dropdown-menu-v2-ariakit/styles.d.ts +88 -0
  228. package/build-types/dropdown-menu-v2-ariakit/styles.d.ts.map +1 -0
  229. package/build-types/dropdown-menu-v2-ariakit/test/index.d.ts +2 -0
  230. package/build-types/dropdown-menu-v2-ariakit/test/index.d.ts.map +1 -0
  231. package/build-types/dropdown-menu-v2-ariakit/types.d.ts +174 -0
  232. package/build-types/dropdown-menu-v2-ariakit/types.d.ts.map +1 -0
  233. package/build-types/font-size-picker/utils.d.ts.map +1 -1
  234. package/build-types/heading/stories/index.story.d.ts.map +1 -1
  235. package/build-types/modal/index.d.ts.map +1 -1
  236. package/build-types/palette-edit/index.d.ts.map +1 -1
  237. package/build-types/popover/index.d.ts +1 -1
  238. package/build-types/popover/index.d.ts.map +1 -1
  239. package/build-types/popover/stories/e2e/index.story.d.ts +1 -1
  240. package/build-types/private-apis.d.ts.map +1 -1
  241. package/build-types/progress-bar/styles.d.ts.map +1 -1
  242. package/build-types/sandbox/index.d.ts.map +1 -1
  243. package/build-types/tabs/context.d.ts +8 -0
  244. package/build-types/tabs/context.d.ts.map +1 -0
  245. package/build-types/tabs/index.d.ts +13 -0
  246. package/build-types/tabs/index.d.ts.map +1 -0
  247. package/build-types/tabs/stories/index.story.d.ts +20 -0
  248. package/build-types/tabs/stories/index.story.d.ts.map +1 -0
  249. package/build-types/tabs/styles.d.ts +17 -0
  250. package/build-types/tabs/styles.d.ts.map +1 -0
  251. package/build-types/tabs/tab.d.ts +10 -0
  252. package/build-types/tabs/tab.d.ts.map +1 -0
  253. package/build-types/tabs/tablist.d.ts +7 -0
  254. package/build-types/tabs/tablist.d.ts.map +1 -0
  255. package/build-types/tabs/tabpanel.d.ts +7 -0
  256. package/build-types/tabs/tabpanel.d.ts.map +1 -0
  257. package/build-types/tabs/test/index.d.ts +2 -0
  258. package/build-types/tabs/test/index.d.ts.map +1 -0
  259. package/build-types/tabs/types.d.ts +134 -0
  260. package/build-types/tabs/types.d.ts.map +1 -0
  261. package/build-types/text/component.d.ts +4 -2
  262. package/build-types/text/component.d.ts.map +1 -1
  263. package/build-types/text/hook.d.ts +171 -165
  264. package/build-types/text/hook.d.ts.map +1 -1
  265. package/build-types/text/index.d.ts +2 -2
  266. package/build-types/text/index.d.ts.map +1 -1
  267. package/build-types/text/stories/index.story.d.ts +21 -0
  268. package/build-types/text/stories/index.story.d.ts.map +1 -0
  269. package/build-types/text/styles.d.ts +7 -7
  270. package/build-types/text/styles.d.ts.map +1 -1
  271. package/build-types/text/types.d.ts +1 -1
  272. package/build-types/text/types.d.ts.map +1 -1
  273. package/build-types/text/utils.d.ts +56 -61
  274. package/build-types/text/utils.d.ts.map +1 -1
  275. package/build-types/toggle-group-control/toggle-group-control-option-base/component.d.ts.map +1 -1
  276. package/build-types/toolbar/stories/index.story.d.ts +5 -0
  277. package/build-types/toolbar/stories/index.story.d.ts.map +1 -1
  278. package/build-types/toolbar/toolbar/index.d.ts.map +1 -1
  279. package/build-types/toolbar/toolbar/types.d.ts +10 -0
  280. package/build-types/toolbar/toolbar/types.d.ts.map +1 -1
  281. package/build-types/tools-panel/tools-panel-item/hook.d.ts.map +1 -1
  282. package/build-types/tools-panel/types.d.ts +2 -0
  283. package/build-types/tools-panel/types.d.ts.map +1 -1
  284. package/build-types/tooltip/index.d.ts.map +1 -1
  285. package/build-types/unit-control/utils.d.ts.map +1 -1
  286. package/package.json +21 -20
  287. package/src/alignment-matrix-control/cell.tsx +6 -2
  288. package/src/alignment-matrix-control/index.tsx +31 -54
  289. package/src/alignment-matrix-control/stories/index.story.tsx +3 -7
  290. package/src/alignment-matrix-control/test/index.tsx +117 -18
  291. package/src/alignment-matrix-control/utils.tsx +33 -9
  292. package/src/autocomplete/index.tsx +136 -77
  293. package/src/button/style.scss +1 -2
  294. package/src/circular-option-picker/circular-option-picker-option.tsx +24 -38
  295. package/src/circular-option-picker/circular-option-picker.tsx +11 -28
  296. package/src/circular-option-picker/types.ts +6 -5
  297. package/src/color-palette/index.tsx +6 -1
  298. package/src/color-picker/component.tsx +25 -3
  299. package/src/color-picker/picker.tsx +96 -2
  300. package/src/color-picker/styles.ts +0 -1
  301. package/src/color-picker/types.ts +3 -0
  302. package/src/composite/v2.ts +22 -0
  303. package/src/confirm-dialog/README.md +1 -1
  304. package/src/confirm-dialog/component.tsx +79 -13
  305. package/src/confirm-dialog/stories/{index.story.js → index.story.tsx} +26 -24
  306. package/src/confirm-dialog/test/{index.js → index.tsx} +3 -3
  307. package/src/confirm-dialog/types.ts +32 -12
  308. package/src/custom-gradient-picker/gradient-bar/control-points.tsx +32 -25
  309. package/src/dimension-control/test/__snapshots__/index.test.js.snap +8 -8
  310. package/src/dropdown-menu-v2-ariakit/README.md +324 -0
  311. package/src/dropdown-menu-v2-ariakit/index.tsx +318 -0
  312. package/src/dropdown-menu-v2-ariakit/stories/index.story.tsx +506 -0
  313. package/src/dropdown-menu-v2-ariakit/styles.ts +297 -0
  314. package/src/dropdown-menu-v2-ariakit/test/index.tsx +1139 -0
  315. package/src/dropdown-menu-v2-ariakit/types.ts +186 -0
  316. package/src/font-size-picker/utils.ts +2 -1
  317. package/src/heading/stories/index.story.tsx +2 -4
  318. package/src/input-control/styles/input-control-styles.tsx +2 -2
  319. package/src/mobile/global-styles-context/utils.native.js +2 -2
  320. package/src/modal/index.tsx +58 -22
  321. package/src/modal/test/index.tsx +29 -0
  322. package/src/notice/style.scss +0 -1
  323. package/src/palette-edit/index.tsx +4 -0
  324. package/src/popover/index.tsx +99 -57
  325. package/src/popover/style.scss +9 -0
  326. package/src/private-apis.ts +31 -1
  327. package/src/progress-bar/styles.ts +19 -4
  328. package/src/sandbox/index.native.js +1 -1
  329. package/src/sandbox/index.tsx +3 -1
  330. package/src/select-control/styles/select-control-styles.ts +2 -2
  331. package/src/tabs/README.md +242 -0
  332. package/src/tabs/context.ts +13 -0
  333. package/src/tabs/index.tsx +167 -0
  334. package/src/tabs/stories/index.story.tsx +352 -0
  335. package/src/tabs/styles.ts +103 -0
  336. package/src/tabs/tab.tsx +39 -0
  337. package/src/tabs/tablist.tsx +40 -0
  338. package/src/tabs/tabpanel.tsx +42 -0
  339. package/src/tabs/test/index.tsx +1133 -0
  340. package/src/tabs/types.ts +142 -0
  341. package/src/text/README.md +2 -2
  342. package/src/text/{component.js → component.tsx} +10 -6
  343. package/src/text/{hook.js → hook.ts} +12 -15
  344. package/src/text/stories/index.story.tsx +80 -0
  345. package/src/text/types.ts +1 -6
  346. package/src/text/{utils.js → utils.ts} +40 -14
  347. package/src/toggle-group-control/test/__snapshots__/index.tsx.snap +16 -0
  348. package/src/toggle-group-control/toggle-group-control-option-base/component.tsx +1 -0
  349. package/src/toolbar/stories/index.story.tsx +15 -0
  350. package/src/toolbar/test/index.tsx +8 -0
  351. package/src/toolbar/toolbar/README.md +9 -0
  352. package/src/toolbar/toolbar/index.tsx +21 -12
  353. package/src/toolbar/toolbar/style.scss +9 -0
  354. package/src/toolbar/toolbar/types.ts +10 -0
  355. package/src/tools-panel/tools-panel/README.md +3 -0
  356. package/src/tools-panel/tools-panel-item/hook.ts +4 -6
  357. package/src/tools-panel/types.ts +2 -0
  358. package/src/tooltip/index.tsx +2 -3
  359. package/src/unit-control/utils.ts +124 -0
  360. package/src/utils/unit-values.ts +1 -1
  361. package/tsconfig.tsbuildinfo +1 -1
  362. package/src/text/stories/index.story.js +0 -53
  363. /package/src/text/{index.js → index.ts} +0 -0
  364. /package/src/text/{styles.js → styles.ts} +0 -0
@@ -0,0 +1,1139 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen, waitFor } from '@testing-library/react';
5
+ import { press, click, hover, type } from '@ariakit/test';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { useState } from '@wordpress/element';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import {
16
+ DropdownMenu,
17
+ DropdownMenuCheckboxItem,
18
+ DropdownMenuItem,
19
+ DropdownMenuGroupLabel,
20
+ DropdownMenuRadioItem,
21
+ DropdownMenuSeparator,
22
+ DropdownMenuGroup,
23
+ } from '..';
24
+
25
+ const delay = ( delayInMs: number ) => {
26
+ return new Promise( ( resolve ) => setTimeout( resolve, delayInMs ) );
27
+ };
28
+
29
+ describe( 'DropdownMenu', () => {
30
+ // See https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/
31
+ it( 'should follow the WAI-ARIA spec', async () => {
32
+ render(
33
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
34
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
35
+ <DropdownMenuSeparator />
36
+ <DropdownMenu
37
+ trigger={
38
+ <DropdownMenuItem>Dropdown submenu</DropdownMenuItem>
39
+ }
40
+ >
41
+ <DropdownMenuItem>Dropdown submenu item 1</DropdownMenuItem>
42
+ <DropdownMenuItem>Dropdown submenu item 2</DropdownMenuItem>
43
+ </DropdownMenu>
44
+ </DropdownMenu>
45
+ );
46
+
47
+ const toggleButton = screen.getByRole( 'button', {
48
+ name: 'Open dropdown',
49
+ } );
50
+
51
+ expect( toggleButton ).toHaveAttribute( 'aria-haspopup', 'menu' );
52
+ expect( toggleButton ).toHaveAttribute( 'aria-expanded', 'false' );
53
+
54
+ await click( toggleButton );
55
+
56
+ expect( toggleButton ).toHaveAttribute( 'aria-expanded', 'true' );
57
+
58
+ expect(
59
+ screen.getByRole( 'menu', { name: toggleButton.textContent ?? '' } )
60
+ ).toHaveFocus();
61
+ expect( screen.getByRole( 'separator' ) ).toHaveAttribute(
62
+ 'aria-orientation',
63
+ 'horizontal'
64
+ );
65
+ expect( screen.getAllByRole( 'menuitem' ) ).toHaveLength( 2 );
66
+
67
+ const submenuTrigger = screen.getByRole( 'menuitem', {
68
+ name: 'Dropdown submenu',
69
+ } );
70
+ expect( submenuTrigger ).toHaveAttribute( 'aria-haspopup', 'menu' );
71
+ expect( submenuTrigger ).toHaveAttribute( 'aria-expanded', 'false' );
72
+
73
+ await hover( submenuTrigger );
74
+
75
+ // Wait for the open animation after hovering
76
+ await waitFor( () =>
77
+ expect(
78
+ screen.getByRole( 'menu', {
79
+ name: submenuTrigger.textContent ?? '',
80
+ } )
81
+ ).toBeVisible()
82
+ );
83
+
84
+ expect( submenuTrigger ).toHaveAttribute( 'aria-expanded', 'true' );
85
+ expect( submenuTrigger ).toHaveAttribute(
86
+ 'aria-controls',
87
+ screen.getAllByRole( 'menu' )[ 1 ].id
88
+ );
89
+ } );
90
+
91
+ describe( 'pointer and keyboard interactions', () => {
92
+ it( 'should open and focus the menu when clicking the trigger', async () => {
93
+ render(
94
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
95
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
96
+ </DropdownMenu>
97
+ );
98
+
99
+ const toggleButton = screen.getByRole( 'button', {
100
+ name: 'Open dropdown',
101
+ } );
102
+
103
+ // DropdownMenu closed
104
+ expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument();
105
+
106
+ // Click to open the menu
107
+ await click( toggleButton );
108
+
109
+ // DropdownMenu open, focus is on the menu wrapper
110
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
111
+ } );
112
+
113
+ it( 'should open and focus the first item when pressing the arrow down key on the trigger', async () => {
114
+ render(
115
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
116
+ <DropdownMenuItem disabled>First item</DropdownMenuItem>
117
+ <DropdownMenuItem>Second item</DropdownMenuItem>
118
+ <DropdownMenuItem>Third item</DropdownMenuItem>
119
+ </DropdownMenu>
120
+ );
121
+
122
+ const toggleButton = screen.getByRole( 'button', {
123
+ name: 'Open dropdown',
124
+ } );
125
+
126
+ // Move focus on the toggle
127
+ await press.Tab();
128
+
129
+ expect( toggleButton ).toHaveFocus();
130
+
131
+ // DropdownMenu closed
132
+ expect( screen.queryByRole( 'menuitem' ) ).not.toBeInTheDocument();
133
+
134
+ await press.ArrowDown();
135
+
136
+ // DropdownMenu open, focus is on the first focusable item
137
+ expect(
138
+ screen.getByRole( 'menuitem', { name: 'Second item' } )
139
+ ).toHaveFocus();
140
+ } );
141
+
142
+ it( 'should open and focus the first item when pressing the space key on the trigger', async () => {
143
+ render(
144
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
145
+ <DropdownMenuItem disabled>First item</DropdownMenuItem>
146
+ <DropdownMenuItem>Second item</DropdownMenuItem>
147
+ <DropdownMenuItem>Third item</DropdownMenuItem>
148
+ </DropdownMenu>
149
+ );
150
+
151
+ const toggleButton = screen.getByRole( 'button', {
152
+ name: 'Open dropdown',
153
+ } );
154
+
155
+ // Move focus on the toggle
156
+ await press.Tab();
157
+
158
+ expect( toggleButton ).toHaveFocus();
159
+
160
+ // DropdownMenu closed
161
+ expect( screen.queryByRole( 'menuitem' ) ).not.toBeInTheDocument();
162
+
163
+ await press.Space();
164
+
165
+ // DropdownMenu open, focus is on the first focusable item
166
+ expect(
167
+ screen.getByRole( 'menuitem', { name: 'Second item' } )
168
+ ).toHaveFocus();
169
+ } );
170
+
171
+ it( 'should close when pressing the escape key', async () => {
172
+ render(
173
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
174
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
175
+ </DropdownMenu>
176
+ );
177
+
178
+ const trigger = screen.getByRole( 'button', {
179
+ name: 'Open dropdown',
180
+ } );
181
+
182
+ await click( trigger );
183
+
184
+ // Focuses menu on mouse click, focuses first item on keyboard press
185
+ // Can be changed with a custom useEffect
186
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
187
+
188
+ // Pressing esc will close the menu and move focus to the toggle
189
+ await press.Escape();
190
+
191
+ expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument();
192
+
193
+ await waitFor( () =>
194
+ expect(
195
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
196
+ ).toHaveFocus()
197
+ );
198
+ } );
199
+
200
+ it( 'should not close when pressing the escape key if the `hideOnEscape` prop is set to `false`', async () => {
201
+ render(
202
+ <DropdownMenu
203
+ trigger={ <button>Open dropdown</button> }
204
+ hideOnEscape={ false }
205
+ >
206
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
207
+ </DropdownMenu>
208
+ );
209
+
210
+ const trigger = screen.getByRole( 'button', {
211
+ name: 'Open dropdown',
212
+ } );
213
+
214
+ await click( trigger );
215
+
216
+ // Focuses menu on mouse click, focuses first item on keyboard press
217
+ // Can be changed with a custom useEffect
218
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
219
+
220
+ // Pressing esc will close the menu and move focus to the toggle
221
+ await press.Escape();
222
+
223
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
224
+ } );
225
+
226
+ it( 'should close when clicking outside of the content', async () => {
227
+ render(
228
+ <DropdownMenu
229
+ defaultOpen
230
+ trigger={ <button>Open dropdown</button> }
231
+ >
232
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
233
+ </DropdownMenu>
234
+ );
235
+
236
+ expect( screen.getByRole( 'menu' ) ).toBeInTheDocument();
237
+
238
+ // Click on the body (ie. outside of the dropdown menu)
239
+ await click( document.body );
240
+
241
+ expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument();
242
+ } );
243
+
244
+ it( 'should close when clicking on a menu item', async () => {
245
+ render(
246
+ <DropdownMenu
247
+ defaultOpen
248
+ trigger={ <button>Open dropdown</button> }
249
+ >
250
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
251
+ </DropdownMenu>
252
+ );
253
+
254
+ expect( screen.getByRole( 'menu' ) ).toBeInTheDocument();
255
+
256
+ // Clicking a menu item will close the menu
257
+ await click( screen.getByRole( 'menuitem' ) );
258
+
259
+ expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument();
260
+ } );
261
+
262
+ it( 'should not close when clicking on a menu item when the `hideOnClick` prop is set to `false`', async () => {
263
+ render(
264
+ <DropdownMenu
265
+ defaultOpen
266
+ trigger={ <button>Open dropdown</button> }
267
+ >
268
+ <DropdownMenuItem hideOnClick={ false }>
269
+ Dropdown menu item
270
+ </DropdownMenuItem>
271
+ </DropdownMenu>
272
+ );
273
+
274
+ expect( screen.getByRole( 'menu' ) ).toBeVisible();
275
+
276
+ // Clicking a menu item will close the menu
277
+ await click( screen.getByRole( 'menuitem' ) );
278
+
279
+ expect( screen.getByRole( 'menu' ) ).toBeVisible();
280
+ } );
281
+
282
+ it( 'should not close when clicking on a disabled menu item', async () => {
283
+ render(
284
+ <DropdownMenu
285
+ defaultOpen
286
+ trigger={ <button>Open dropdown</button> }
287
+ >
288
+ <DropdownMenuItem disabled>
289
+ Dropdown menu item
290
+ </DropdownMenuItem>
291
+ </DropdownMenu>
292
+ );
293
+
294
+ expect( screen.getByRole( 'menu' ) ).toBeInTheDocument();
295
+
296
+ // Clicking a disabled menu item won't close the menu
297
+ await click( screen.getByRole( 'menuitem' ) );
298
+
299
+ expect( screen.getByRole( 'menu' ) ).toBeInTheDocument();
300
+ } );
301
+
302
+ it( 'should reveal submenu content when hovering over the submenu trigger', async () => {
303
+ render(
304
+ <DropdownMenu
305
+ defaultOpen
306
+ trigger={ <button>Open dropdown</button> }
307
+ >
308
+ <DropdownMenuItem>Dropdown menu item 1</DropdownMenuItem>
309
+ <DropdownMenuItem>Dropdown menu item 2</DropdownMenuItem>
310
+ <DropdownMenu
311
+ trigger={
312
+ <DropdownMenuItem>
313
+ Dropdown submenu
314
+ </DropdownMenuItem>
315
+ }
316
+ >
317
+ <DropdownMenuItem>
318
+ Dropdown submenu item 1
319
+ </DropdownMenuItem>
320
+ <DropdownMenuItem>
321
+ Dropdown submenu item 2
322
+ </DropdownMenuItem>
323
+ </DropdownMenu>
324
+ <DropdownMenuItem>Dropdown menu item 3</DropdownMenuItem>
325
+ </DropdownMenu>
326
+ );
327
+
328
+ // Before hover, submenu items are not rendered
329
+ expect(
330
+ screen.queryByRole( 'menuitem', {
331
+ name: 'Dropdown submenu item 1',
332
+ } )
333
+ ).not.toBeInTheDocument();
334
+
335
+ await hover(
336
+ screen.getByRole( 'menuitem', { name: 'Dropdown submenu' } )
337
+ );
338
+
339
+ // After hover, submenu items are rendered
340
+ // Reason for `findByRole`: due to the animation, we've got to wait
341
+ // a short amount of time for the submenu to appear
342
+ await screen.findByRole( 'menuitem', {
343
+ name: 'Dropdown submenu item 1',
344
+ } );
345
+ } );
346
+
347
+ it( 'should navigate menu items and subitems using the arrow, spacebar and enter keys', async () => {
348
+ render(
349
+ <DropdownMenu
350
+ defaultOpen
351
+ trigger={ <button>Open dropdown</button> }
352
+ >
353
+ <DropdownMenuItem>Dropdown menu item 1</DropdownMenuItem>
354
+ <DropdownMenuItem>Dropdown menu item 2</DropdownMenuItem>
355
+ <DropdownMenu
356
+ trigger={
357
+ <DropdownMenuItem>
358
+ Dropdown submenu
359
+ </DropdownMenuItem>
360
+ }
361
+ >
362
+ <DropdownMenuItem>
363
+ Dropdown submenu item 1
364
+ </DropdownMenuItem>
365
+ <DropdownMenuItem>
366
+ Dropdown submenu item 2
367
+ </DropdownMenuItem>
368
+ </DropdownMenu>
369
+ <DropdownMenuItem>Dropdown menu item 3</DropdownMenuItem>
370
+ </DropdownMenu>
371
+ );
372
+
373
+ // The menu is focused automatically when `defaultOpen` is set.
374
+ await waitFor( () =>
375
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus()
376
+ );
377
+
378
+ // Arrow up/down selects menu items
379
+ // The selection wraps around from last to first and viceversa
380
+ await press.ArrowDown();
381
+ expect(
382
+ screen.getByRole( 'menuitem', { name: 'Dropdown menu item 1' } )
383
+ ).toHaveFocus();
384
+
385
+ await press.ArrowDown();
386
+ expect(
387
+ screen.getByRole( 'menuitem', { name: 'Dropdown menu item 2' } )
388
+ ).toHaveFocus();
389
+
390
+ await press.ArrowDown();
391
+ expect(
392
+ screen.getByRole( 'menuitem', { name: 'Dropdown submenu' } )
393
+ ).toHaveFocus();
394
+
395
+ await press.ArrowDown();
396
+ expect(
397
+ screen.getByRole( 'menuitem', { name: 'Dropdown menu item 3' } )
398
+ ).toHaveFocus();
399
+
400
+ await press.ArrowDown();
401
+ expect(
402
+ screen.getByRole( 'menuitem', { name: 'Dropdown menu item 1' } )
403
+ ).toHaveFocus();
404
+
405
+ await press.ArrowUp();
406
+ expect(
407
+ screen.getByRole( 'menuitem', { name: 'Dropdown menu item 3' } )
408
+ ).toHaveFocus();
409
+
410
+ await press.ArrowUp();
411
+ expect(
412
+ screen.getByRole( 'menuitem', { name: 'Dropdown submenu' } )
413
+ ).toHaveFocus();
414
+
415
+ // Arrow right/left can be used to enter/leave submenus
416
+ await press.ArrowRight();
417
+ expect(
418
+ screen.getByRole( 'menuitem', {
419
+ name: 'Dropdown submenu item 1',
420
+ } )
421
+ ).toHaveFocus();
422
+
423
+ await press.ArrowDown();
424
+ expect(
425
+ screen.getByRole( 'menuitem', {
426
+ name: 'Dropdown submenu item 2',
427
+ } )
428
+ ).toHaveFocus();
429
+
430
+ await press.ArrowLeft();
431
+ expect(
432
+ screen.getByRole( 'menuitem', {
433
+ name: 'Dropdown submenu',
434
+ } )
435
+ ).toHaveFocus();
436
+
437
+ // Spacebar or enter key can also be used to enter a submenu
438
+ await press.Enter();
439
+ expect(
440
+ screen.getByRole( 'menuitem', {
441
+ name: 'Dropdown submenu item 1',
442
+ } )
443
+ ).toHaveFocus();
444
+
445
+ await press.ArrowLeft();
446
+ expect(
447
+ screen.getByRole( 'menuitem', {
448
+ name: 'Dropdown submenu',
449
+ } )
450
+ ).toHaveFocus();
451
+
452
+ await press.Space();
453
+ expect(
454
+ screen.getByRole( 'menuitem', {
455
+ name: 'Dropdown submenu item 1',
456
+ } )
457
+ ).toHaveFocus();
458
+
459
+ await press.ArrowLeft();
460
+ expect(
461
+ screen.getByRole( 'menuitem', {
462
+ name: 'Dropdown submenu',
463
+ } )
464
+ ).toHaveFocus();
465
+ } );
466
+
467
+ it( 'should check radio items and keep the menu open when clicking (controlled)', async () => {
468
+ const onRadioValueChangeSpy = jest.fn();
469
+
470
+ const ControlledRadioGroup = () => {
471
+ const [ radioValue, setRadioValue ] = useState( 'two' );
472
+ const onRadioChange: React.ComponentProps<
473
+ typeof DropdownMenuRadioItem
474
+ >[ 'onChange' ] = ( e ) => {
475
+ onRadioValueChangeSpy( e.target.value );
476
+ setRadioValue( e.target.value );
477
+ };
478
+ return (
479
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
480
+ <DropdownMenuGroup>
481
+ <DropdownMenuGroupLabel>
482
+ Radio group label
483
+ </DropdownMenuGroupLabel>
484
+ <DropdownMenuRadioItem
485
+ name="radio-test"
486
+ value="radio-one"
487
+ checked={ radioValue === 'radio-one' }
488
+ onChange={ onRadioChange }
489
+ >
490
+ Radio item one
491
+ </DropdownMenuRadioItem>
492
+ <DropdownMenuRadioItem
493
+ name="radio-test"
494
+ value="radio-two"
495
+ checked={ radioValue === 'radio-two' }
496
+ onChange={ onRadioChange }
497
+ >
498
+ Radio item two
499
+ </DropdownMenuRadioItem>
500
+ </DropdownMenuGroup>
501
+ </DropdownMenu>
502
+ );
503
+ };
504
+
505
+ render( <ControlledRadioGroup /> );
506
+
507
+ // Open dropdown
508
+ await click(
509
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
510
+ );
511
+
512
+ // No radios should be checked at this point
513
+ expect( screen.getAllByRole( 'menuitemradio' ) ).toHaveLength( 2 );
514
+ expect(
515
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
516
+ ).not.toBeChecked();
517
+ expect(
518
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
519
+ ).not.toBeChecked();
520
+
521
+ // Click first radio item, make sure that the callback fires
522
+ await click(
523
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
524
+ );
525
+ expect( onRadioValueChangeSpy ).toHaveBeenCalledTimes( 1 );
526
+ expect( onRadioValueChangeSpy ).toHaveBeenLastCalledWith(
527
+ 'radio-one'
528
+ );
529
+
530
+ // Make sure that first radio is checked
531
+ expect(
532
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
533
+ ).toBeChecked();
534
+ expect(
535
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
536
+ ).not.toBeChecked();
537
+
538
+ // Click second radio item, make sure that the callback fires
539
+ await click(
540
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
541
+ );
542
+ expect( onRadioValueChangeSpy ).toHaveBeenCalledTimes( 2 );
543
+ expect( onRadioValueChangeSpy ).toHaveBeenLastCalledWith(
544
+ 'radio-two'
545
+ );
546
+
547
+ // Make sure that second radio is selected
548
+ expect(
549
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
550
+ ).not.toBeChecked();
551
+ expect(
552
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
553
+ ).toBeChecked();
554
+ } );
555
+
556
+ it( 'should check radio items and keep the menu open when clicking (uncontrolled)', async () => {
557
+ const onRadioValueChangeSpy = jest.fn();
558
+ render(
559
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
560
+ <DropdownMenuGroup>
561
+ <DropdownMenuGroupLabel>
562
+ Radio group label
563
+ </DropdownMenuGroupLabel>
564
+ <DropdownMenuRadioItem
565
+ name="radio-test"
566
+ value="radio-one"
567
+ onChange={ ( e ) =>
568
+ onRadioValueChangeSpy( e.target.value )
569
+ }
570
+ >
571
+ Radio item one
572
+ </DropdownMenuRadioItem>
573
+ <DropdownMenuRadioItem
574
+ name="radio-test"
575
+ value="radio-two"
576
+ defaultChecked
577
+ onChange={ ( e ) =>
578
+ onRadioValueChangeSpy( e.target.value )
579
+ }
580
+ >
581
+ Radio item two
582
+ </DropdownMenuRadioItem>
583
+ </DropdownMenuGroup>
584
+ </DropdownMenu>
585
+ );
586
+
587
+ // Open dropdown
588
+ await click(
589
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
590
+ );
591
+
592
+ // Radio item two should be checked (`defaultChecked` prop)
593
+ expect( screen.getAllByRole( 'menuitemradio' ) ).toHaveLength( 2 );
594
+ expect(
595
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
596
+ ).not.toBeChecked();
597
+ expect(
598
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
599
+ ).toBeChecked();
600
+
601
+ // Click first radio item, make sure that the callback fires
602
+ await click(
603
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
604
+ );
605
+ expect( onRadioValueChangeSpy ).toHaveBeenCalledTimes( 1 );
606
+ expect( onRadioValueChangeSpy ).toHaveBeenLastCalledWith(
607
+ 'radio-one'
608
+ );
609
+
610
+ // Make sure that first radio is checked
611
+ expect(
612
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
613
+ ).toBeChecked();
614
+ expect(
615
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
616
+ ).not.toBeChecked();
617
+
618
+ // Click second radio item, make sure that the callback fires
619
+ await click(
620
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
621
+ );
622
+ expect( onRadioValueChangeSpy ).toHaveBeenCalledTimes( 2 );
623
+ expect( onRadioValueChangeSpy ).toHaveBeenLastCalledWith(
624
+ 'radio-two'
625
+ );
626
+
627
+ // Make sure that second radio is selected
628
+ expect(
629
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
630
+ ).not.toBeChecked();
631
+ expect(
632
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
633
+ ).toBeChecked();
634
+ } );
635
+
636
+ it( 'should check checkbox items and keep the menu open when clicking (controlled)', async () => {
637
+ const onCheckboxValueChangeSpy = jest.fn();
638
+
639
+ const ControlledRadioGroup = () => {
640
+ const [ itemOneChecked, setItemOneChecked ] =
641
+ useState< boolean >();
642
+ const [ itemTwoChecked, setItemTwoChecked ] =
643
+ useState< boolean >();
644
+
645
+ return (
646
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
647
+ <DropdownMenuCheckboxItem
648
+ name="item-one"
649
+ value="item-one-value"
650
+ checked={ itemOneChecked }
651
+ onChange={ ( e ) => {
652
+ onCheckboxValueChangeSpy(
653
+ e.target.name,
654
+ e.target.value,
655
+ e.target.checked
656
+ );
657
+ setItemOneChecked( e.target.checked );
658
+ } }
659
+ >
660
+ Checkbox item one
661
+ </DropdownMenuCheckboxItem>
662
+
663
+ <DropdownMenuCheckboxItem
664
+ name="item-two"
665
+ value="item-two-value"
666
+ checked={ itemTwoChecked }
667
+ onChange={ ( e ) => {
668
+ onCheckboxValueChangeSpy(
669
+ e.target.name,
670
+ e.target.value,
671
+ e.target.checked
672
+ );
673
+ setItemTwoChecked( e.target.checked );
674
+ } }
675
+ >
676
+ Checkbox item two
677
+ </DropdownMenuCheckboxItem>
678
+ </DropdownMenu>
679
+ );
680
+ };
681
+
682
+ render( <ControlledRadioGroup /> );
683
+
684
+ // Open dropdown
685
+ await click(
686
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
687
+ );
688
+
689
+ // No checkboxes should be checked at this point
690
+ expect( screen.getAllByRole( 'menuitemcheckbox' ) ).toHaveLength(
691
+ 2
692
+ );
693
+ expect(
694
+ screen.getByRole( 'menuitemcheckbox', {
695
+ name: 'Checkbox item one',
696
+ } )
697
+ ).not.toBeChecked();
698
+ expect(
699
+ screen.getByRole( 'menuitemcheckbox', {
700
+ name: 'Checkbox item two',
701
+ } )
702
+ ).not.toBeChecked();
703
+
704
+ // Click first checkbox item, make sure that the callback fires
705
+ await click(
706
+ screen.getByRole( 'menuitemcheckbox', {
707
+ name: 'Checkbox item one',
708
+ } )
709
+ );
710
+ expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 1 );
711
+ expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith(
712
+ 'item-one',
713
+ 'item-one-value',
714
+ true
715
+ );
716
+
717
+ // Make sure that first checkbox is checked
718
+ expect(
719
+ screen.getByRole( 'menuitemcheckbox', {
720
+ name: 'Checkbox item one',
721
+ } )
722
+ ).toBeChecked();
723
+
724
+ // Click second checkbox item, make sure that the callback fires
725
+ await click(
726
+ screen.getByRole( 'menuitemcheckbox', {
727
+ name: 'Checkbox item two',
728
+ } )
729
+ );
730
+ expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 2 );
731
+ expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith(
732
+ 'item-two',
733
+ 'item-two-value',
734
+ true
735
+ );
736
+
737
+ // Make sure that second checkbox is selected
738
+ expect(
739
+ screen.getByRole( 'menuitemcheckbox', {
740
+ name: 'Checkbox item two',
741
+ } )
742
+ ).toBeChecked();
743
+
744
+ // Click second checkbox item, make sure that the callback fires
745
+ await click(
746
+ screen.getByRole( 'menuitemcheckbox', {
747
+ name: 'Checkbox item two',
748
+ } )
749
+ );
750
+ expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 3 );
751
+ expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith(
752
+ 'item-two',
753
+ 'item-two-value',
754
+ false
755
+ );
756
+
757
+ // Make sure that second checkbox is unselected
758
+ expect(
759
+ screen.getByRole( 'menuitemcheckbox', {
760
+ name: 'Checkbox item two',
761
+ } )
762
+ ).not.toBeChecked();
763
+ } );
764
+
765
+ it( 'should check checkbox items and keep the menu open when clicking (uncontrolled)', async () => {
766
+ const onCheckboxValueChangeSpy = jest.fn();
767
+
768
+ render(
769
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
770
+ <DropdownMenuCheckboxItem
771
+ name="item-one"
772
+ value="item-one-value"
773
+ onChange={ ( e ) => {
774
+ onCheckboxValueChangeSpy(
775
+ e.target.name,
776
+ e.target.value,
777
+ e.target.checked
778
+ );
779
+ } }
780
+ >
781
+ Checkbox item one
782
+ </DropdownMenuCheckboxItem>
783
+
784
+ <DropdownMenuCheckboxItem
785
+ name="item-two"
786
+ value="item-two-value"
787
+ defaultChecked
788
+ onChange={ ( e ) => {
789
+ onCheckboxValueChangeSpy(
790
+ e.target.name,
791
+ e.target.value,
792
+ e.target.checked
793
+ );
794
+ } }
795
+ >
796
+ Checkbox item two
797
+ </DropdownMenuCheckboxItem>
798
+ </DropdownMenu>
799
+ );
800
+
801
+ // Open dropdown
802
+ await click(
803
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
804
+ );
805
+
806
+ // Checkbox item two should be checked (`defaultChecked`)
807
+ expect( screen.getAllByRole( 'menuitemcheckbox' ) ).toHaveLength(
808
+ 2
809
+ );
810
+ expect(
811
+ screen.getByRole( 'menuitemcheckbox', {
812
+ name: 'Checkbox item one',
813
+ } )
814
+ ).not.toBeChecked();
815
+ expect(
816
+ screen.getByRole( 'menuitemcheckbox', {
817
+ name: 'Checkbox item two',
818
+ } )
819
+ ).toBeChecked();
820
+
821
+ // Click first checkbox item, make sure that the callback fires
822
+ await click(
823
+ screen.getByRole( 'menuitemcheckbox', {
824
+ name: 'Checkbox item one',
825
+ } )
826
+ );
827
+ expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 1 );
828
+ expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith(
829
+ 'item-one',
830
+ 'item-one-value',
831
+ true
832
+ );
833
+
834
+ // Make sure that first checkbox is checked
835
+ expect(
836
+ screen.getByRole( 'menuitemcheckbox', {
837
+ name: 'Checkbox item one',
838
+ } )
839
+ ).toBeChecked();
840
+
841
+ // Click second checkbox item, make sure that the callback fires
842
+ await click(
843
+ screen.getByRole( 'menuitemcheckbox', {
844
+ name: 'Checkbox item two',
845
+ } )
846
+ );
847
+ expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 2 );
848
+ expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith(
849
+ 'item-two',
850
+ 'item-two-value',
851
+ false
852
+ );
853
+
854
+ // Make sure that second checkbox is unchecked
855
+ expect(
856
+ screen.getByRole( 'menuitemcheckbox', {
857
+ name: 'Checkbox item two',
858
+ } )
859
+ ).not.toBeChecked();
860
+
861
+ // Click second checkbox item, make sure that the callback fires
862
+ await click(
863
+ screen.getByRole( 'menuitemcheckbox', {
864
+ name: 'Checkbox item two',
865
+ } )
866
+ );
867
+ expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 3 );
868
+ expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith(
869
+ 'item-two',
870
+ 'item-two-value',
871
+ true
872
+ );
873
+
874
+ // Make sure that second checkbox is unselected
875
+ expect(
876
+ screen.getByRole( 'menuitemcheckbox', {
877
+ name: 'Checkbox item two',
878
+ } )
879
+ ).toBeChecked();
880
+ } );
881
+ } );
882
+
883
+ describe( 'modality', () => {
884
+ it( 'should be modal by default', async () => {
885
+ render(
886
+ <>
887
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
888
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
889
+ </DropdownMenu>
890
+ <button>Button outside of dropdown</button>
891
+ </>
892
+ );
893
+
894
+ // Click to open the menu
895
+ await click(
896
+ screen.getByRole( 'button', {
897
+ name: 'Open dropdown',
898
+ } )
899
+ );
900
+
901
+ // DropdownMenu open, focus is on the menu wrapper
902
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
903
+
904
+ expect(
905
+ screen.queryByRole( 'button', {
906
+ name: 'Button outside of dropdown',
907
+ } )
908
+ ).not.toBeInTheDocument();
909
+ } );
910
+
911
+ it( 'should not be modal when the `modal` prop is set to `false`', async () => {
912
+ render(
913
+ <>
914
+ <DropdownMenu
915
+ trigger={ <button>Open dropdown</button> }
916
+ modal={ false }
917
+ >
918
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
919
+ </DropdownMenu>
920
+ <button>Button outside of dropdown</button>
921
+ </>
922
+ );
923
+
924
+ // Click to open the menu
925
+ await click(
926
+ screen.getByRole( 'button', {
927
+ name: 'Open dropdown',
928
+ } )
929
+ );
930
+
931
+ // DropdownMenu open, focus is on the menu wrapper
932
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
933
+
934
+ // DropdownMenu is not modal, therefore the outer button is part of the
935
+ // accessibility tree and can be found.
936
+ const outerButton = screen.getByRole( 'button', {
937
+ name: 'Button outside of dropdown',
938
+ } );
939
+
940
+ // The outer button can be focused by pressing tab. Doing so will cause
941
+ // the DropdownMenu to close.
942
+ await press.Tab();
943
+ expect( outerButton ).toBeInTheDocument();
944
+ expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument();
945
+ } );
946
+ } );
947
+
948
+ describe( 'items prefix and suffix', () => {
949
+ it( 'should display a prefix on regular items', async () => {
950
+ render(
951
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
952
+ <DropdownMenuItem prefix={ <>Item prefix</> }>
953
+ Dropdown menu item
954
+ </DropdownMenuItem>
955
+ </DropdownMenu>
956
+ );
957
+
958
+ // Click to open the menu
959
+ await click(
960
+ screen.getByRole( 'button', {
961
+ name: 'Open dropdown',
962
+ } )
963
+ );
964
+
965
+ // The contents of the prefix are rendered before the item's children
966
+ expect(
967
+ screen.getByRole( 'menuitem', {
968
+ name: 'Item prefix Dropdown menu item',
969
+ } )
970
+ ).toBeInTheDocument();
971
+ } );
972
+
973
+ it( 'should display a suffix on regular items', async () => {
974
+ render(
975
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
976
+ <DropdownMenuItem suffix={ <>Item suffix</> }>
977
+ Dropdown menu item
978
+ </DropdownMenuItem>
979
+ </DropdownMenu>
980
+ );
981
+
982
+ // Click to open the menu
983
+ await click(
984
+ screen.getByRole( 'button', {
985
+ name: 'Open dropdown',
986
+ } )
987
+ );
988
+
989
+ // The contents of the suffix are rendered after the item's children
990
+ expect(
991
+ screen.getByRole( 'menuitem', {
992
+ name: 'Dropdown menu item Item suffix',
993
+ } )
994
+ ).toBeInTheDocument();
995
+ } );
996
+
997
+ it( 'should display a suffix on radio items', async () => {
998
+ render(
999
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
1000
+ <DropdownMenuRadioItem
1001
+ name="radio-test"
1002
+ value="radio-one"
1003
+ suffix="Radio suffix"
1004
+ >
1005
+ Radio item one
1006
+ </DropdownMenuRadioItem>
1007
+ </DropdownMenu>
1008
+ );
1009
+
1010
+ // Click to open the menu
1011
+ await click(
1012
+ screen.getByRole( 'button', {
1013
+ name: 'Open dropdown',
1014
+ } )
1015
+ );
1016
+
1017
+ // The contents of the suffix are rendered after the item's children
1018
+ expect(
1019
+ screen.getByRole( 'menuitemradio', {
1020
+ name: 'Radio item oneRadio suffix',
1021
+ } )
1022
+ ).toBeInTheDocument();
1023
+ } );
1024
+
1025
+ it( 'should display a suffix on checkbox items', async () => {
1026
+ render(
1027
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
1028
+ <DropdownMenuCheckboxItem
1029
+ name="checkbox-test"
1030
+ value="checkbox-one"
1031
+ suffix="Checkbox suffix"
1032
+ >
1033
+ Checkbox item one
1034
+ </DropdownMenuCheckboxItem>
1035
+ </DropdownMenu>
1036
+ );
1037
+
1038
+ // Click to open the menu
1039
+ await click(
1040
+ screen.getByRole( 'button', {
1041
+ name: 'Open dropdown',
1042
+ } )
1043
+ );
1044
+
1045
+ // The contents of the suffix are rendered after the item's children
1046
+ expect(
1047
+ screen.getByRole( 'menuitemcheckbox', {
1048
+ name: 'Checkbox item one Checkbox suffix',
1049
+ } )
1050
+ ).toBeInTheDocument();
1051
+ } );
1052
+ } );
1053
+
1054
+ describe( 'typeahead', () => {
1055
+ it( 'should highlight matching item', async () => {
1056
+ render(
1057
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
1058
+ <DropdownMenuItem>One</DropdownMenuItem>
1059
+ <DropdownMenuItem>Two</DropdownMenuItem>
1060
+ </DropdownMenu>
1061
+ );
1062
+
1063
+ // Click to open the menu
1064
+ await click(
1065
+ screen.getByRole( 'button', {
1066
+ name: 'Open dropdown',
1067
+ } )
1068
+ );
1069
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
1070
+
1071
+ // Type "tw", it should match and focus the item with content "Two"
1072
+ await type( 'tw' );
1073
+ expect(
1074
+ screen.getByRole( 'menuitem', { name: 'Two' } )
1075
+ ).toHaveFocus();
1076
+
1077
+ // Wait for the typeahead timer to reset and interpret
1078
+ // the next keystrokes as a new search
1079
+ await delay( 500 );
1080
+
1081
+ // Type "on", it should match and focus the item with content "One"
1082
+ await type( 'on' );
1083
+ expect(
1084
+ screen.getByRole( 'menuitem', { name: 'One' } )
1085
+ ).toHaveFocus();
1086
+ } );
1087
+
1088
+ it( 'should keep previous focus when no matches are found', async () => {
1089
+ render(
1090
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
1091
+ <DropdownMenuItem>One</DropdownMenuItem>
1092
+ <DropdownMenuItem>Two</DropdownMenuItem>
1093
+ </DropdownMenu>
1094
+ );
1095
+
1096
+ // Click to open the menu
1097
+ await click(
1098
+ screen.getByRole( 'button', {
1099
+ name: 'Open dropdown',
1100
+ } )
1101
+ );
1102
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
1103
+
1104
+ // Type a string that doesn't match any items. Focus shouldn't move.
1105
+ await type( 'abc' );
1106
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
1107
+
1108
+ // Wait for the typeahead timer to reset and interpret
1109
+ // the next keystrokes as a new search
1110
+ await delay( 500 );
1111
+
1112
+ // Type "on", it should match and focus the item with content "One"
1113
+ await type( 'on' );
1114
+ expect(
1115
+ screen.getByRole( 'menuitem', { name: 'One' } )
1116
+ ).toHaveFocus();
1117
+
1118
+ // Wait for the typeahead timer to reset and interpret
1119
+ // the next keystrokes as a new search
1120
+ await delay( 500 );
1121
+
1122
+ // Type a string that doesn't match any items. Focus shouldn't move.
1123
+ await type( 'abc' );
1124
+ expect(
1125
+ screen.getByRole( 'menuitem', { name: 'One' } )
1126
+ ).toHaveFocus();
1127
+
1128
+ // Wait for the typeahead timer to reset and interpret
1129
+ // the next keystrokes as a new search
1130
+ await delay( 500 );
1131
+
1132
+ // Type "tw", it should match and focus the item with content "Two"
1133
+ await type( 'tw' );
1134
+ expect(
1135
+ screen.getByRole( 'menuitem', { name: 'Two' } )
1136
+ ).toHaveFocus();
1137
+ } );
1138
+ } );
1139
+ } );