@wordpress/components 25.12.0 → 25.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/build/angle-picker-control/index.js +0 -1
  3. package/build/angle-picker-control/index.js.map +1 -1
  4. package/build/border-control/border-control-dropdown/component.js +4 -2
  5. package/build/border-control/border-control-dropdown/component.js.map +1 -1
  6. package/build/border-control/border-control-dropdown/hook.js +3 -2
  7. package/build/border-control/border-control-dropdown/hook.js.map +1 -1
  8. package/build/border-control/styles.js +17 -17
  9. package/build/border-control/styles.js.map +1 -1
  10. package/build/checkbox-control/index.js +1 -1
  11. package/build/checkbox-control/index.js.map +1 -1
  12. package/build/checkbox-control/types.js.map +1 -1
  13. package/build/custom-select-control-v2/index.js +87 -0
  14. package/build/custom-select-control-v2/index.js.map +1 -0
  15. package/build/custom-select-control-v2/styles.js +85 -0
  16. package/build/custom-select-control-v2/styles.js.map +1 -0
  17. package/build/custom-select-control-v2/types.js +6 -0
  18. package/build/custom-select-control-v2/types.js.map +1 -0
  19. package/build/date-time/time/timezone.js +11 -2
  20. package/build/date-time/time/timezone.js.map +1 -1
  21. package/build/dimension-control/index.js +2 -0
  22. package/build/dimension-control/index.js.map +1 -1
  23. package/build/dimension-control/types.js.map +1 -1
  24. package/build/dropdown-menu-v2-ariakit/index.js +49 -20
  25. package/build/dropdown-menu-v2-ariakit/index.js.map +1 -1
  26. package/build/dropdown-menu-v2-ariakit/styles.js +82 -59
  27. package/build/dropdown-menu-v2-ariakit/styles.js.map +1 -1
  28. package/build/dropdown-menu-v2-ariakit/types.js.map +1 -1
  29. package/build/focal-point-picker/controls.js +5 -1
  30. package/build/focal-point-picker/controls.js.map +1 -1
  31. package/build/focal-point-picker/index.js +2 -0
  32. package/build/focal-point-picker/index.js.map +1 -1
  33. package/build/focal-point-picker/styles/focal-point-picker-style.js +15 -15
  34. package/build/focal-point-picker/styles/focal-point-picker-style.js.map +1 -1
  35. package/build/focal-point-picker/types.js.map +1 -1
  36. package/build/font-size-picker/font-size-picker-select.js +2 -0
  37. package/build/font-size-picker/font-size-picker-select.js.map +1 -1
  38. package/build/font-size-picker/font-size-picker-toggle-group.js +2 -0
  39. package/build/font-size-picker/font-size-picker-toggle-group.js.map +1 -1
  40. package/build/font-size-picker/index.js +6 -1
  41. package/build/font-size-picker/index.js.map +1 -1
  42. package/build/font-size-picker/types.js.map +1 -1
  43. package/build/form-token-field/index.js +6 -2
  44. package/build/form-token-field/index.js.map +1 -1
  45. package/build/form-token-field/token-input.js.map +1 -1
  46. package/build/form-token-field/types.js.map +1 -1
  47. package/build/heading/hook.js +6 -3
  48. package/build/heading/hook.js.map +1 -1
  49. package/build/heading/types.js.map +1 -1
  50. package/build/index.native.js +0 -16
  51. package/build/index.native.js.map +1 -1
  52. package/build/mobile/global-styles-context/utils.native.js +13 -0
  53. package/build/mobile/global-styles-context/utils.native.js.map +1 -1
  54. package/build/mobile/utils/alignments.native.js +1 -1
  55. package/build/mobile/utils/alignments.native.js.map +1 -1
  56. package/build/palette-edit/index.js +21 -1
  57. package/build/palette-edit/index.js.map +1 -1
  58. package/build/private-apis.js +3 -2
  59. package/build/private-apis.js.map +1 -1
  60. package/build/query-controls/author-select.js +3 -1
  61. package/build/query-controls/author-select.js.map +1 -1
  62. package/build/query-controls/category-select.js +3 -1
  63. package/build/query-controls/category-select.js.map +1 -1
  64. package/build/query-controls/index.js +6 -1
  65. package/build/query-controls/index.js.map +1 -1
  66. package/build/query-controls/types.js.map +1 -1
  67. package/build/slot-fill/types.js.map +1 -1
  68. package/build/tabs/index.js +24 -5
  69. package/build/tabs/index.js.map +1 -1
  70. package/build/tabs/tab.js +4 -4
  71. package/build/tabs/tab.js.map +1 -1
  72. package/build/tabs/tabpanel.js +4 -3
  73. package/build/tabs/tabpanel.js.map +1 -1
  74. package/build/tabs/types.js.map +1 -1
  75. package/build/text/types.js.map +1 -1
  76. package/build/toggle-group-control/toggle-group-control/utils.js +17 -17
  77. package/build/toggle-group-control/toggle-group-control/utils.js.map +1 -1
  78. package/build/tools-panel/tools-panel-item/hook.js +16 -12
  79. package/build/tools-panel/tools-panel-item/hook.js.map +1 -1
  80. package/build-module/angle-picker-control/index.js +0 -1
  81. package/build-module/angle-picker-control/index.js.map +1 -1
  82. package/build-module/border-control/border-control-dropdown/component.js +4 -2
  83. package/build-module/border-control/border-control-dropdown/component.js.map +1 -1
  84. package/build-module/border-control/border-control-dropdown/hook.js +3 -2
  85. package/build-module/border-control/border-control-dropdown/hook.js.map +1 -1
  86. package/build-module/border-control/styles.js +17 -17
  87. package/build-module/border-control/styles.js.map +1 -1
  88. package/build-module/checkbox-control/index.js +1 -1
  89. package/build-module/checkbox-control/index.js.map +1 -1
  90. package/build-module/checkbox-control/types.js.map +1 -1
  91. package/build-module/custom-select-control-v2/index.js +74 -0
  92. package/build-module/custom-select-control-v2/index.js.map +1 -0
  93. package/build-module/custom-select-control-v2/styles.js +71 -0
  94. package/build-module/custom-select-control-v2/styles.js.map +1 -0
  95. package/build-module/custom-select-control-v2/types.js +2 -0
  96. package/build-module/custom-select-control-v2/types.js.map +1 -0
  97. package/build-module/date-time/time/timezone.js +11 -2
  98. package/build-module/date-time/time/timezone.js.map +1 -1
  99. package/build-module/dimension-control/index.js +2 -0
  100. package/build-module/dimension-control/index.js.map +1 -1
  101. package/build-module/dimension-control/types.js.map +1 -1
  102. package/build-module/dropdown-menu-v2-ariakit/index.js +46 -18
  103. package/build-module/dropdown-menu-v2-ariakit/index.js.map +1 -1
  104. package/build-module/dropdown-menu-v2-ariakit/styles.js +69 -40
  105. package/build-module/dropdown-menu-v2-ariakit/styles.js.map +1 -1
  106. package/build-module/dropdown-menu-v2-ariakit/types.js.map +1 -1
  107. package/build-module/focal-point-picker/controls.js +5 -1
  108. package/build-module/focal-point-picker/controls.js.map +1 -1
  109. package/build-module/focal-point-picker/index.js +2 -0
  110. package/build-module/focal-point-picker/index.js.map +1 -1
  111. package/build-module/focal-point-picker/styles/focal-point-picker-style.js +15 -15
  112. package/build-module/focal-point-picker/styles/focal-point-picker-style.js.map +1 -1
  113. package/build-module/focal-point-picker/types.js.map +1 -1
  114. package/build-module/font-size-picker/font-size-picker-select.js +2 -0
  115. package/build-module/font-size-picker/font-size-picker-select.js.map +1 -1
  116. package/build-module/font-size-picker/font-size-picker-toggle-group.js +2 -0
  117. package/build-module/font-size-picker/font-size-picker-toggle-group.js.map +1 -1
  118. package/build-module/font-size-picker/index.js +6 -1
  119. package/build-module/font-size-picker/index.js.map +1 -1
  120. package/build-module/font-size-picker/types.js.map +1 -1
  121. package/build-module/form-token-field/index.js +6 -2
  122. package/build-module/form-token-field/index.js.map +1 -1
  123. package/build-module/form-token-field/token-input.js.map +1 -1
  124. package/build-module/form-token-field/types.js.map +1 -1
  125. package/build-module/heading/hook.js +6 -3
  126. package/build-module/heading/hook.js.map +1 -1
  127. package/build-module/heading/types.js.map +1 -1
  128. package/build-module/index.native.js +0 -2
  129. package/build-module/index.native.js.map +1 -1
  130. package/build-module/mobile/global-styles-context/utils.native.js +13 -0
  131. package/build-module/mobile/global-styles-context/utils.native.js.map +1 -1
  132. package/build-module/mobile/utils/alignments.native.js +1 -1
  133. package/build-module/mobile/utils/alignments.native.js.map +1 -1
  134. package/build-module/palette-edit/index.js +20 -3
  135. package/build-module/palette-edit/index.js.map +1 -1
  136. package/build-module/private-apis.js +4 -3
  137. package/build-module/private-apis.js.map +1 -1
  138. package/build-module/query-controls/author-select.js +3 -1
  139. package/build-module/query-controls/author-select.js.map +1 -1
  140. package/build-module/query-controls/category-select.js +3 -1
  141. package/build-module/query-controls/category-select.js.map +1 -1
  142. package/build-module/query-controls/index.js +6 -1
  143. package/build-module/query-controls/index.js.map +1 -1
  144. package/build-module/query-controls/types.js.map +1 -1
  145. package/build-module/slot-fill/types.js.map +1 -1
  146. package/build-module/tabs/index.js +25 -6
  147. package/build-module/tabs/index.js.map +1 -1
  148. package/build-module/tabs/tab.js +6 -6
  149. package/build-module/tabs/tab.js.map +1 -1
  150. package/build-module/tabs/tabpanel.js +6 -5
  151. package/build-module/tabs/tabpanel.js.map +1 -1
  152. package/build-module/tabs/types.js.map +1 -1
  153. package/build-module/text/types.js.map +1 -1
  154. package/build-module/toggle-group-control/toggle-group-control/utils.js +17 -17
  155. package/build-module/toggle-group-control/toggle-group-control/utils.js.map +1 -1
  156. package/build-module/tools-panel/tools-panel-item/hook.js +17 -13
  157. package/build-module/tools-panel/tools-panel-item/hook.js.map +1 -1
  158. package/build-style/style-rtl.css +32 -6
  159. package/build-style/style.css +32 -6
  160. package/build-types/angle-picker-control/index.d.ts.map +1 -1
  161. package/build-types/border-control/border-control-dropdown/component.d.ts.map +1 -1
  162. package/build-types/border-control/border-control-dropdown/hook.d.ts +1 -0
  163. package/build-types/border-control/border-control-dropdown/hook.d.ts.map +1 -1
  164. package/build-types/border-control/styles.d.ts +1 -1
  165. package/build-types/border-control/styles.d.ts.map +1 -1
  166. package/build-types/box-control/stories/index.story.d.ts +1944 -0
  167. package/build-types/box-control/stories/index.story.d.ts.map +1 -0
  168. package/build-types/checkbox-control/index.d.ts.map +1 -1
  169. package/build-types/checkbox-control/types.d.ts +3 -2
  170. package/build-types/checkbox-control/types.d.ts.map +1 -1
  171. package/build-types/color-palette/styles.d.ts +4 -1
  172. package/build-types/color-palette/styles.d.ts.map +1 -1
  173. package/build-types/custom-select-control-v2/index.d.ts +6 -0
  174. package/build-types/custom-select-control-v2/index.d.ts.map +1 -0
  175. package/build-types/custom-select-control-v2/stories/index.story.d.ts +19 -0
  176. package/build-types/custom-select-control-v2/stories/index.story.d.ts.map +1 -0
  177. package/build-types/custom-select-control-v2/styles.d.ts +47 -0
  178. package/build-types/custom-select-control-v2/styles.d.ts.map +1 -0
  179. package/build-types/custom-select-control-v2/types.d.ts +57 -0
  180. package/build-types/custom-select-control-v2/types.d.ts.map +1 -0
  181. package/build-types/date-time/date/styles.d.ts +4 -1
  182. package/build-types/date-time/date/styles.d.ts.map +1 -1
  183. package/build-types/date-time/time/timezone.d.ts.map +1 -1
  184. package/build-types/dimension-control/index.d.ts.map +1 -1
  185. package/build-types/dimension-control/types.d.ts +6 -0
  186. package/build-types/dimension-control/types.d.ts.map +1 -1
  187. package/build-types/dropdown-menu-v2-ariakit/index.d.ts +11 -2
  188. package/build-types/dropdown-menu-v2-ariakit/index.d.ts.map +1 -1
  189. package/build-types/dropdown-menu-v2-ariakit/stories/index.story.d.ts.map +1 -1
  190. package/build-types/dropdown-menu-v2-ariakit/styles.d.ts +26 -18
  191. package/build-types/dropdown-menu-v2-ariakit/styles.d.ts.map +1 -1
  192. package/build-types/dropdown-menu-v2-ariakit/types.d.ts +0 -6
  193. package/build-types/dropdown-menu-v2-ariakit/types.d.ts.map +1 -1
  194. package/build-types/focal-point-picker/controls.d.ts +1 -1
  195. package/build-types/focal-point-picker/controls.d.ts.map +1 -1
  196. package/build-types/focal-point-picker/index.d.ts +1 -1
  197. package/build-types/focal-point-picker/index.d.ts.map +1 -1
  198. package/build-types/focal-point-picker/stories/index.story.d.ts +8 -4
  199. package/build-types/focal-point-picker/stories/index.story.d.ts.map +1 -1
  200. package/build-types/focal-point-picker/types.d.ts +7 -0
  201. package/build-types/focal-point-picker/types.d.ts.map +1 -1
  202. package/build-types/font-size-picker/font-size-picker-select.d.ts.map +1 -1
  203. package/build-types/font-size-picker/font-size-picker-toggle-group.d.ts.map +1 -1
  204. package/build-types/font-size-picker/index.d.ts.map +1 -1
  205. package/build-types/font-size-picker/types.d.ts +8 -1
  206. package/build-types/font-size-picker/types.d.ts.map +1 -1
  207. package/build-types/form-token-field/index.d.ts.map +1 -1
  208. package/build-types/form-token-field/token-input.d.ts.map +1 -1
  209. package/build-types/form-token-field/types.d.ts +1 -1
  210. package/build-types/form-token-field/types.d.ts.map +1 -1
  211. package/build-types/heading/component.d.ts +4 -1
  212. package/build-types/heading/component.d.ts.map +1 -1
  213. package/build-types/heading/hook.d.ts.map +1 -1
  214. package/build-types/heading/types.d.ts +20 -1
  215. package/build-types/heading/types.d.ts.map +1 -1
  216. package/build-types/navigation/styles/navigation-styles.d.ts +4 -1
  217. package/build-types/navigation/styles/navigation-styles.d.ts.map +1 -1
  218. package/build-types/palette-edit/index.d.ts +6 -1
  219. package/build-types/palette-edit/index.d.ts.map +1 -1
  220. package/build-types/palette-edit/styles.d.ts +4 -1
  221. package/build-types/palette-edit/styles.d.ts.map +1 -1
  222. package/build-types/private-apis.d.ts.map +1 -1
  223. package/build-types/query-controls/author-select.d.ts +1 -1
  224. package/build-types/query-controls/author-select.d.ts.map +1 -1
  225. package/build-types/query-controls/category-select.d.ts +1 -1
  226. package/build-types/query-controls/category-select.d.ts.map +1 -1
  227. package/build-types/query-controls/index.d.ts +1 -1
  228. package/build-types/query-controls/index.d.ts.map +1 -1
  229. package/build-types/query-controls/types.d.ts +9 -0
  230. package/build-types/query-controls/types.d.ts.map +1 -1
  231. package/build-types/slot-fill/bubbles-virtually/slot.d.ts +1 -1
  232. package/build-types/slot-fill/types.d.ts +16 -6
  233. package/build-types/slot-fill/types.d.ts.map +1 -1
  234. package/build-types/tabs/index.d.ts +3 -2
  235. package/build-types/tabs/index.d.ts.map +1 -1
  236. package/build-types/tabs/stories/index.story.d.ts.map +1 -1
  237. package/build-types/tabs/tab.d.ts +2 -1
  238. package/build-types/tabs/tab.d.ts.map +1 -1
  239. package/build-types/tabs/tabpanel.d.ts +2 -1
  240. package/build-types/tabs/tabpanel.d.ts.map +1 -1
  241. package/build-types/tabs/types.d.ts +8 -3
  242. package/build-types/tabs/types.d.ts.map +1 -1
  243. package/build-types/text/types.d.ts +15 -2
  244. package/build-types/text/types.d.ts.map +1 -1
  245. package/build-types/toggle-group-control/toggle-group-control/utils.d.ts.map +1 -1
  246. package/build-types/tools-panel/tools-panel-item/hook.d.ts.map +1 -1
  247. package/package.json +19 -19
  248. package/src/angle-picker-control/index.tsx +0 -1
  249. package/src/border-control/border-control-dropdown/component.tsx +3 -1
  250. package/src/border-control/border-control-dropdown/hook.ts +3 -2
  251. package/src/border-control/styles.ts +2 -9
  252. package/src/box-control/stories/index.story.tsx +82 -0
  253. package/src/button/style.scss +10 -2
  254. package/src/checkbox-control/README.md +2 -1
  255. package/src/checkbox-control/index.tsx +8 -6
  256. package/src/checkbox-control/test/__snapshots__/index.tsx.snap +3 -8
  257. package/src/checkbox-control/test/index.tsx +7 -0
  258. package/src/checkbox-control/types.ts +3 -2
  259. package/src/combobox-control/README.md +1 -3
  260. package/src/custom-select-control/test/index.js +367 -35
  261. package/src/custom-select-control-v2/README.md +73 -0
  262. package/src/custom-select-control-v2/index.tsx +99 -0
  263. package/src/custom-select-control-v2/stories/index.story.tsx +149 -0
  264. package/src/custom-select-control-v2/styles.ts +76 -0
  265. package/src/custom-select-control-v2/types.ts +63 -0
  266. package/src/date-time/time/timezone.tsx +15 -3
  267. package/src/dimension-control/index.tsx +2 -0
  268. package/src/dimension-control/test/__snapshots__/index.test.js.snap +2 -2
  269. package/src/dimension-control/types.ts +6 -0
  270. package/src/dropdown-menu-v2-ariakit/README.md +19 -5
  271. package/src/dropdown-menu-v2-ariakit/index.tsx +85 -36
  272. package/src/dropdown-menu-v2-ariakit/stories/index.story.tsx +204 -90
  273. package/src/dropdown-menu-v2-ariakit/styles.ts +165 -117
  274. package/src/dropdown-menu-v2-ariakit/test/index.tsx +5 -10
  275. package/src/dropdown-menu-v2-ariakit/types.ts +0 -7
  276. package/src/focal-point-picker/controls.tsx +4 -0
  277. package/src/focal-point-picker/index.tsx +2 -0
  278. package/src/focal-point-picker/styles/focal-point-picker-style.ts +1 -1
  279. package/src/focal-point-picker/types.ts +7 -0
  280. package/src/font-size-picker/font-size-picker-select.tsx +2 -0
  281. package/src/font-size-picker/font-size-picker-toggle-group.tsx +9 -1
  282. package/src/font-size-picker/index.tsx +11 -3
  283. package/src/font-size-picker/types.ts +8 -1
  284. package/src/form-toggle/style.scss +40 -8
  285. package/src/form-token-field/index.tsx +11 -3
  286. package/src/form-token-field/token-input.tsx +1 -3
  287. package/src/form-token-field/types.ts +1 -0
  288. package/src/heading/README.md +6 -1
  289. package/src/heading/hook.ts +6 -3
  290. package/src/heading/types.ts +23 -1
  291. package/src/index.native.js +0 -2
  292. package/src/mobile/global-styles-context/test/utils.native.js +22 -0
  293. package/src/mobile/global-styles-context/utils.native.js +14 -0
  294. package/src/mobile/link-settings/style.native.scss +0 -17
  295. package/src/mobile/utils/alignments.native.js +1 -0
  296. package/src/navigable-container/README.md +1 -1
  297. package/src/palette-edit/index.tsx +22 -8
  298. package/src/palette-edit/style.scss +2 -2
  299. package/src/palette-edit/test/index.tsx +75 -1
  300. package/src/private-apis.ts +4 -2
  301. package/src/query-controls/author-select.tsx +2 -0
  302. package/src/query-controls/category-select.tsx +2 -0
  303. package/src/query-controls/index.tsx +6 -1
  304. package/src/query-controls/types.ts +9 -0
  305. package/src/search-control/README.md +2 -0
  306. package/src/slot-fill/README.md +1 -1
  307. package/src/slot-fill/types.ts +18 -6
  308. package/src/spinner/README.md +2 -0
  309. package/src/tabs/README.md +4 -4
  310. package/src/tabs/index.tsx +34 -3
  311. package/src/tabs/stories/index.story.tsx +56 -48
  312. package/src/tabs/tab.tsx +7 -7
  313. package/src/tabs/tabpanel.tsx +10 -6
  314. package/src/tabs/test/index.tsx +180 -87
  315. package/src/tabs/types.ts +8 -3
  316. package/src/text/README.md +5 -1
  317. package/src/text/types.ts +15 -2
  318. package/src/toggle-control/README.md +2 -2
  319. package/src/toggle-group-control/test/index.tsx +54 -1
  320. package/src/toggle-group-control/toggle-group-control/utils.ts +15 -20
  321. package/src/tools-panel/tools-panel-item/hook.ts +21 -23
  322. package/tsconfig.tsbuildinfo +1 -1
  323. package/build/mobile/inserter-button/index.native.js +0 -98
  324. package/build/mobile/inserter-button/index.native.js.map +0 -1
  325. package/build/mobile/inserter-button/sparkles.js +0 -23
  326. package/build/mobile/inserter-button/sparkles.js.map +0 -1
  327. package/build/mobile/link-settings/image-link-destinations-screen.native.js +0 -119
  328. package/build/mobile/link-settings/image-link-destinations-screen.native.js.map +0 -1
  329. package/build-module/mobile/inserter-button/index.native.js +0 -89
  330. package/build-module/mobile/inserter-button/index.native.js.map +0 -1
  331. package/build-module/mobile/inserter-button/sparkles.js +0 -15
  332. package/build-module/mobile/inserter-button/sparkles.js.map +0 -1
  333. package/build-module/mobile/link-settings/image-link-destinations-screen.native.js +0 -110
  334. package/build-module/mobile/link-settings/image-link-destinations-screen.native.js.map +0 -1
  335. package/build-types/mobile/inserter-button/sparkles.d.ts +0 -3
  336. package/build-types/mobile/inserter-button/sparkles.d.ts.map +0 -1
  337. package/src/box-control/stories/index.story.js +0 -75
  338. package/src/mobile/inserter-button/README.md +0 -62
  339. package/src/mobile/inserter-button/index.native.js +0 -116
  340. package/src/mobile/inserter-button/sparkles.js +0 -15
  341. package/src/mobile/inserter-button/style.native.scss +0 -72
  342. package/src/mobile/link-settings/image-link-destinations-screen.native.js +0 -152
@@ -4,54 +4,205 @@
4
4
  import { render, screen } from '@testing-library/react';
5
5
  import userEvent from '@testing-library/user-event';
6
6
 
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { useState } from '@wordpress/element';
11
+
7
12
  /**
8
13
  * Internal dependencies
9
14
  */
10
15
  import CustomSelectControl from '..';
11
16
 
12
- describe( 'CustomSelectControl', () => {
13
- it( 'Captures the keypress event and does not let it propagate', async () => {
14
- const user = userEvent.setup();
15
- const onKeyDown = jest.fn();
16
- const options = [
17
- {
18
- key: 'one',
19
- name: 'Option one',
20
- },
21
- {
22
- key: 'two',
23
- name: 'Option two',
24
- },
25
- {
26
- key: 'three',
27
- name: 'Option three',
17
+ const customClass = 'amber-skies';
18
+
19
+ const props = {
20
+ label: 'label!',
21
+ options: [
22
+ {
23
+ key: 'flower1',
24
+ name: 'violets',
25
+ },
26
+ {
27
+ key: 'flower2',
28
+ name: 'crimson clover',
29
+ className: customClass,
30
+ },
31
+ {
32
+ key: 'flower3',
33
+ name: 'poppy',
34
+ },
35
+ {
36
+ key: 'color1',
37
+ name: 'amber',
38
+ className: customClass,
39
+ },
40
+ {
41
+ key: 'color2',
42
+ name: 'aquamarine',
43
+ style: {
44
+ backgroundColor: 'rgb(127, 255, 212)',
45
+ rotate: '13deg',
28
46
  },
29
- ];
47
+ },
48
+ ],
49
+ __nextUnconstrainedWidth: true,
50
+ };
30
51
 
31
- render(
32
- <div
33
- // This role="none" is required to prevent an eslint warning about accessibility.
34
- role="none"
35
- onKeyDown={ onKeyDown }
36
- >
37
- <CustomSelectControl
38
- options={ options }
39
- __nextUnconstrainedWidth
40
- />
41
- </div>
52
+ const ControlledCustomSelectControl = ( { options } ) => {
53
+ const [ value, setValue ] = useState( options[ 0 ] );
54
+ return (
55
+ <CustomSelectControl
56
+ { ...props }
57
+ onChange={ ( { selectedItem } ) => setValue( selectedItem ) }
58
+ value={ options.find( ( option ) => option.key === value.key ) }
59
+ />
60
+ );
61
+ };
62
+
63
+ describe.each( [
64
+ [ 'uncontrolled', CustomSelectControl ],
65
+ [ 'controlled', ControlledCustomSelectControl ],
66
+ ] )( 'CustomSelectControl %s', ( ...modeAndComponent ) => {
67
+ const [ , Component ] = modeAndComponent;
68
+
69
+ it( 'Should replace the initial selection when a new item is selected', async () => {
70
+ const user = userEvent.setup();
71
+
72
+ render( <Component { ...props } /> );
73
+
74
+ const currentSelectedItem = screen.getByRole( 'button', {
75
+ expanded: false,
76
+ } );
77
+
78
+ await user.click( currentSelectedItem );
79
+
80
+ await user.click(
81
+ screen.getByRole( 'option', {
82
+ name: 'crimson clover',
83
+ } )
84
+ );
85
+
86
+ expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' );
87
+
88
+ await user.click( currentSelectedItem );
89
+
90
+ await user.click(
91
+ screen.getByRole( 'option', {
92
+ name: 'poppy',
93
+ } )
94
+ );
95
+
96
+ expect( currentSelectedItem ).toHaveTextContent( 'poppy' );
97
+ } );
98
+
99
+ it( 'Should keep current selection if dropdown is closed without changing selection', async () => {
100
+ const user = userEvent.setup();
101
+
102
+ render( <CustomSelectControl { ...props } /> );
103
+
104
+ const currentSelectedItem = screen.getByRole( 'button', {
105
+ expanded: false,
106
+ } );
107
+
108
+ await user.tab();
109
+ await user.keyboard( '{enter}' );
110
+ expect(
111
+ screen.getByRole( 'listbox', {
112
+ name: 'label!',
113
+ } )
114
+ ).toBeVisible();
115
+
116
+ await user.keyboard( '{escape}' );
117
+ expect(
118
+ screen.queryByRole( 'listbox', {
119
+ name: 'label!',
120
+ } )
121
+ ).not.toBeInTheDocument();
122
+
123
+ expect( currentSelectedItem ).toHaveTextContent(
124
+ props.options[ 0 ].name
125
+ );
126
+ } );
127
+
128
+ it( 'Should apply class only to options that have a className defined', async () => {
129
+ const user = userEvent.setup();
130
+
131
+ render( <CustomSelectControl { ...props } /> );
132
+
133
+ await user.click(
134
+ screen.getByRole( 'button', {
135
+ expanded: false,
136
+ } )
137
+ );
138
+
139
+ // return an array of items _with_ a className added
140
+ const itemsWithClass = props.options.filter(
141
+ ( option ) => option.className !== undefined
142
+ );
143
+
144
+ // assert against filtered array
145
+ itemsWithClass.map( ( { name } ) =>
146
+ expect( screen.getByRole( 'option', { name } ) ).toHaveClass(
147
+ customClass
148
+ )
149
+ );
150
+
151
+ // return an array of items _without_ a className added
152
+ const itemsWithoutClass = props.options.filter(
153
+ ( option ) => option.className === undefined
154
+ );
155
+
156
+ // assert against filtered array
157
+ itemsWithoutClass.map( ( { name } ) =>
158
+ expect( screen.getByRole( 'option', { name } ) ).not.toHaveClass(
159
+ customClass
160
+ )
161
+ );
162
+ } );
163
+
164
+ it( 'Should apply styles only to options that have styles defined', async () => {
165
+ const user = userEvent.setup();
166
+ const customStyles =
167
+ 'background-color: rgb(127, 255, 212); rotate: 13deg;';
168
+
169
+ render( <CustomSelectControl { ...props } /> );
170
+
171
+ await user.click(
172
+ screen.getByRole( 'button', {
173
+ expanded: false,
174
+ } )
175
+ );
176
+
177
+ // return an array of items _with_ styles added
178
+ const styledItems = props.options.filter(
179
+ ( option ) => option.style !== undefined
42
180
  );
43
- const toggleButton = screen.getByRole( 'button' );
44
- await user.click( toggleButton );
45
181
 
46
- const customSelect = screen.getByRole( 'listbox' );
47
- await user.type( customSelect, '{enter}' );
182
+ // assert against filtered array
183
+ styledItems.map( ( { name } ) =>
184
+ expect( screen.getByRole( 'option', { name } ) ).toHaveStyle(
185
+ customStyles
186
+ )
187
+ );
188
+
189
+ // return an array of items _without_ styles added
190
+ const unstyledItems = props.options.filter(
191
+ ( option ) => option.style === undefined
192
+ );
48
193
 
49
- expect( onKeyDown ).toHaveBeenCalledTimes( 0 );
194
+ // assert against filtered array
195
+ unstyledItems.map( ( { name } ) =>
196
+ expect( screen.getByRole( 'option', { name } ) ).not.toHaveStyle(
197
+ customStyles
198
+ )
199
+ );
50
200
  } );
51
201
 
52
202
  it( 'does not show selected hint by default', () => {
53
203
  render(
54
204
  <CustomSelectControl
205
+ { ...props }
55
206
  label="Custom select"
56
207
  options={ [
57
208
  {
@@ -60,7 +211,6 @@ describe( 'CustomSelectControl', () => {
60
211
  __experimentalHint: 'Hint',
61
212
  },
62
213
  ] }
63
- __nextUnconstrainedWidth
64
214
  />
65
215
  );
66
216
  expect(
@@ -71,6 +221,7 @@ describe( 'CustomSelectControl', () => {
71
221
  it( 'shows selected hint when __experimentalShowSelectedHint is set', () => {
72
222
  render(
73
223
  <CustomSelectControl
224
+ { ...props }
74
225
  label="Custom select"
75
226
  options={ [
76
227
  {
@@ -80,11 +231,192 @@ describe( 'CustomSelectControl', () => {
80
231
  },
81
232
  ] }
82
233
  __experimentalShowSelectedHint
83
- __nextUnconstrainedWidth
84
234
  />
85
235
  );
86
236
  expect(
87
237
  screen.getByRole( 'button', { name: 'Custom select' } )
88
238
  ).toHaveTextContent( 'Hint' );
89
239
  } );
240
+
241
+ describe( 'Keyboard behavior and accessibility', () => {
242
+ it( 'Captures the keypress event and does not let it propagate', async () => {
243
+ const user = userEvent.setup();
244
+ const onKeyDown = jest.fn();
245
+
246
+ render(
247
+ <div
248
+ // This role="none" is required to prevent an eslint warning about accessibility.
249
+ role="none"
250
+ onKeyDown={ onKeyDown }
251
+ >
252
+ <CustomSelectControl { ...props } />
253
+ </div>
254
+ );
255
+ const currentSelectedItem = screen.getByRole( 'button', {
256
+ expanded: false,
257
+ } );
258
+ await user.click( currentSelectedItem );
259
+
260
+ const customSelect = screen.getByRole( 'listbox', {
261
+ name: 'label!',
262
+ } );
263
+ await user.type( customSelect, '{enter}' );
264
+
265
+ expect( onKeyDown ).toHaveBeenCalledTimes( 0 );
266
+ } );
267
+
268
+ it( 'Should be able to change selection using keyboard', async () => {
269
+ const user = userEvent.setup();
270
+
271
+ render( <CustomSelectControl { ...props } /> );
272
+
273
+ const currentSelectedItem = screen.getByRole( 'button', {
274
+ expanded: false,
275
+ } );
276
+
277
+ await user.tab();
278
+ expect( currentSelectedItem ).toHaveFocus();
279
+
280
+ await user.keyboard( '{enter}' );
281
+ expect(
282
+ screen.getByRole( 'listbox', {
283
+ name: 'label!',
284
+ } )
285
+ ).toHaveFocus();
286
+
287
+ await user.keyboard( '{arrowdown}' );
288
+ await user.keyboard( '{enter}' );
289
+
290
+ expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' );
291
+ } );
292
+
293
+ it( 'Should be able to type characters to select matching options', async () => {
294
+ const user = userEvent.setup();
295
+
296
+ render( <CustomSelectControl { ...props } /> );
297
+
298
+ const currentSelectedItem = screen.getByRole( 'button', {
299
+ expanded: false,
300
+ } );
301
+
302
+ await user.tab();
303
+ await user.keyboard( '{enter}' );
304
+ expect(
305
+ screen.getByRole( 'listbox', {
306
+ name: 'label!',
307
+ } )
308
+ ).toHaveFocus();
309
+
310
+ await user.keyboard( '{a}' );
311
+ await user.keyboard( '{enter}' );
312
+ expect( currentSelectedItem ).toHaveTextContent( 'amber' );
313
+ } );
314
+
315
+ it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => {
316
+ const user = userEvent.setup();
317
+
318
+ render( <CustomSelectControl { ...props } /> );
319
+
320
+ const currentSelectedItem = screen.getByRole( 'button', {
321
+ expanded: false,
322
+ } );
323
+
324
+ await user.tab();
325
+ expect( currentSelectedItem ).toHaveFocus();
326
+
327
+ await user.keyboard( '{a}' );
328
+ await user.keyboard( '{q}' );
329
+
330
+ expect(
331
+ screen.queryByRole( 'listbox', {
332
+ name: 'label!',
333
+ hidden: true,
334
+ } )
335
+ ).not.toBeInTheDocument();
336
+
337
+ await user.keyboard( '{enter}' );
338
+ expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' );
339
+ } );
340
+
341
+ it( 'Should have correct aria-selected value for selections', async () => {
342
+ const user = userEvent.setup();
343
+
344
+ render( <CustomSelectControl { ...props } /> );
345
+
346
+ const currentSelectedItem = screen.getByRole( 'button', {
347
+ expanded: false,
348
+ } );
349
+
350
+ await user.click( currentSelectedItem );
351
+
352
+ // get all items except for first option
353
+ const unselectedItems = props.options.filter(
354
+ ( { name } ) => name !== props.options[ 0 ].name
355
+ );
356
+
357
+ // assert that all other items have aria-selected="false"
358
+ unselectedItems.map( ( { name } ) =>
359
+ expect(
360
+ screen.getByRole( 'option', { name, selected: false } )
361
+ ).toBeVisible()
362
+ );
363
+
364
+ // assert that first item has aria-selected="true"
365
+ expect(
366
+ screen.getByRole( 'option', {
367
+ name: props.options[ 0 ].name,
368
+ selected: true,
369
+ } )
370
+ ).toBeVisible();
371
+
372
+ // change the current selection
373
+ await user.click( screen.getByRole( 'option', { name: 'poppy' } ) );
374
+
375
+ // click button to mount listbox with options again
376
+ await user.click( currentSelectedItem );
377
+
378
+ // check that first item is has aria-selected="false" after new selection
379
+ expect(
380
+ screen.getByRole( 'option', {
381
+ name: props.options[ 0 ].name,
382
+ selected: false,
383
+ } )
384
+ ).toBeVisible();
385
+
386
+ // check that new selected item now has aria-selected="true"
387
+ expect(
388
+ screen.getByRole( 'option', {
389
+ name: 'poppy',
390
+ selected: true,
391
+ } )
392
+ ).toBeVisible();
393
+ } );
394
+
395
+ it( 'Should call custom event handlers', async () => {
396
+ const user = userEvent.setup();
397
+ const onFocusMock = jest.fn();
398
+ const onBlurMock = jest.fn();
399
+
400
+ render(
401
+ <CustomSelectControl
402
+ { ...props }
403
+ onFocus={ onFocusMock }
404
+ onBlur={ onBlurMock }
405
+ />
406
+ );
407
+
408
+ const currentSelectedItem = screen.getByRole( 'button', {
409
+ expanded: false,
410
+ } );
411
+
412
+ await user.tab();
413
+
414
+ expect( currentSelectedItem ).toHaveFocus();
415
+ expect( onFocusMock ).toHaveBeenCalledTimes( 1 );
416
+
417
+ await user.tab();
418
+ expect( currentSelectedItem ).not.toHaveFocus();
419
+ expect( onBlurMock ).toHaveBeenCalledTimes( 1 );
420
+ } );
421
+ } );
90
422
  } );
@@ -0,0 +1,73 @@
1
+ <div class="callout callout-alert">
2
+ This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
3
+ </div>
4
+
5
+ ### `CustomSelect`
6
+
7
+ Used to render a customizable select control component.
8
+
9
+ #### Props
10
+
11
+ The component accepts the following props:
12
+
13
+ ##### `children`: `React.ReactNode`
14
+
15
+ The child elements. This should be composed of CustomSelect.Item components.
16
+
17
+ - Required: yes
18
+
19
+ ##### `defaultValue`: `string`
20
+
21
+ An optional default value for the control. If left `undefined`, the first non-disabled item will be used.
22
+
23
+ - Required: no
24
+
25
+ ##### `label`: `string`
26
+
27
+ Label for the control.
28
+
29
+ - Required: yes
30
+
31
+ ##### `onChange`: `( newValue: string ) => void`
32
+
33
+ A function that receives the new value of the input.
34
+
35
+ - Required: no
36
+
37
+ ##### `renderSelectedValue`: `( selectValue: string ) => React.ReactNode`
38
+
39
+ Can be used to render select UI with custom styled values.
40
+
41
+ - Required: no
42
+
43
+ ##### `size`: `'default' | 'large'`
44
+
45
+ The size of the control.
46
+
47
+ - Required: no
48
+
49
+ ##### `value`: `string`
50
+
51
+ Can be used to externally control the value of the control.
52
+
53
+ - Required: no
54
+
55
+ ### `CustomSelectItem`
56
+
57
+ Used to render a select item.
58
+
59
+ #### Props
60
+
61
+ The component accepts the following props:
62
+
63
+ ##### `value`: `string`
64
+
65
+ The value of the select item. This will be used as the children if children are left `undefined`.
66
+
67
+ - Required: yes
68
+
69
+ ##### `children`: `React.ReactNode`
70
+
71
+ The children to display for each select item. The `value` will be used if left `undefined`.
72
+
73
+ - Required: no
@@ -0,0 +1,99 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ // eslint-disable-next-line no-restricted-imports
5
+ import * as Ariakit from '@ariakit/react';
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { createContext, useContext } from '@wordpress/element';
10
+ import { __, sprintf } from '@wordpress/i18n';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import * as Styled from './styles';
16
+ import type {
17
+ CustomSelectProps,
18
+ CustomSelectItemProps,
19
+ CustomSelectContext as CustomSelectContextType,
20
+ } from './types';
21
+
22
+ export const CustomSelectContext =
23
+ createContext< CustomSelectContextType >( undefined );
24
+
25
+ function defaultRenderSelectedValue( value: CustomSelectProps[ 'value' ] ) {
26
+ const isValueEmpty = Array.isArray( value )
27
+ ? value.length === 0
28
+ : value === undefined || value === null;
29
+
30
+ if ( isValueEmpty ) {
31
+ return __( 'Select an item' );
32
+ }
33
+
34
+ if ( Array.isArray( value ) ) {
35
+ return value.length === 1
36
+ ? value[ 0 ]
37
+ : // translators: %s: number of items selected (it will always be 2 or more items)
38
+ sprintf( __( '%s items selected' ), value.length );
39
+ }
40
+
41
+ return value;
42
+ }
43
+
44
+ export function CustomSelect( props: CustomSelectProps ) {
45
+ const {
46
+ children,
47
+ defaultValue,
48
+ label,
49
+ onChange,
50
+ size = 'default',
51
+ value,
52
+ renderSelectedValue = defaultRenderSelectedValue,
53
+ } = props;
54
+
55
+ const store = Ariakit.useSelectStore( {
56
+ setValue: ( nextValue ) => onChange?.( nextValue ),
57
+ defaultValue,
58
+ value,
59
+ } );
60
+
61
+ const { value: currentValue } = store.useState();
62
+
63
+ return (
64
+ <>
65
+ <Styled.CustomSelectLabel store={ store }>
66
+ { label }
67
+ </Styled.CustomSelectLabel>
68
+ <Styled.CustomSelectButton
69
+ size={ size }
70
+ hasCustomRenderProp={ !! renderSelectedValue }
71
+ store={ store }
72
+ >
73
+ { renderSelectedValue( currentValue ) }
74
+ <Ariakit.SelectArrow />
75
+ </Styled.CustomSelectButton>
76
+ <Styled.CustomSelectPopover gutter={ 12 } store={ store } sameWidth>
77
+ <CustomSelectContext.Provider value={ { store } }>
78
+ { children }
79
+ </CustomSelectContext.Provider>
80
+ </Styled.CustomSelectPopover>
81
+ </>
82
+ );
83
+ }
84
+
85
+ export function CustomSelectItem( {
86
+ children,
87
+ ...props
88
+ }: CustomSelectItemProps ) {
89
+ const customSelectContext = useContext( CustomSelectContext );
90
+ return (
91
+ <Styled.CustomSelectItem
92
+ store={ customSelectContext?.store }
93
+ { ...props }
94
+ >
95
+ { children ?? props.value }
96
+ <Ariakit.SelectItemCheck />
97
+ </Styled.CustomSelectItem>
98
+ );
99
+ }