@wordpress/components 19.17.0 → 20.0.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 (773) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/CONTRIBUTING.md +10 -10
  3. package/build/alignment-matrix-control/index.js +1 -1
  4. package/build/alignment-matrix-control/index.js.map +1 -1
  5. package/build/alignment-matrix-control/styles/alignment-matrix-control-styles.js +9 -9
  6. package/build/alignment-matrix-control/styles/alignment-matrix-control-styles.js.map +1 -1
  7. package/build/angle-picker-control/index.js +3 -0
  8. package/build/angle-picker-control/index.js.map +1 -1
  9. package/build/angle-picker-control/styles/angle-picker-control-styles.js +14 -4
  10. package/build/angle-picker-control/styles/angle-picker-control-styles.js.map +1 -1
  11. package/build/autocomplete/index.js +9 -11
  12. package/build/autocomplete/index.js.map +1 -1
  13. package/build/base-control/styles/base-control-styles.js +8 -8
  14. package/build/base-control/styles/base-control-styles.js.map +1 -1
  15. package/build/base-field/styles.js +5 -5
  16. package/build/base-field/styles.js.map +1 -1
  17. package/build/box-control/styles/box-control-visualizer-styles.js +8 -8
  18. package/build/box-control/styles/box-control-visualizer-styles.js.map +1 -1
  19. package/build/card/card/component.js +6 -11
  20. package/build/card/card/component.js.map +1 -1
  21. package/build/card/card/hook.js +0 -10
  22. package/build/card/card/hook.js.map +1 -1
  23. package/build/card/card/index.js.map +1 -1
  24. package/build/card/card-body/component.js +7 -8
  25. package/build/card/card-body/component.js.map +1 -1
  26. package/build/card/card-body/hook.js +0 -4
  27. package/build/card/card-body/hook.js.map +1 -1
  28. package/build/card/card-body/index.js.map +1 -1
  29. package/build/card/card-divider/component.js +7 -8
  30. package/build/card/card-divider/component.js.map +1 -1
  31. package/build/card/card-divider/hook.js +0 -4
  32. package/build/card/card-divider/hook.js.map +1 -1
  33. package/build/card/card-divider/index.js.map +1 -1
  34. package/build/card/card-footer/component.js +7 -8
  35. package/build/card/card-footer/component.js.map +1 -1
  36. package/build/card/card-footer/hook.js +0 -4
  37. package/build/card/card-footer/hook.js.map +1 -1
  38. package/build/card/card-footer/index.js.map +1 -1
  39. package/build/card/card-header/component.js +7 -8
  40. package/build/card/card-header/component.js.map +1 -1
  41. package/build/card/card-header/hook.js +0 -4
  42. package/build/card/card-header/hook.js.map +1 -1
  43. package/build/card/card-header/index.js.map +1 -1
  44. package/build/card/card-media/component.js +7 -7
  45. package/build/card/card-media/component.js.map +1 -1
  46. package/build/card/card-media/hook.js +0 -4
  47. package/build/card/card-media/hook.js.map +1 -1
  48. package/build/card/card-media/index.js.map +1 -1
  49. package/build/card/context.js.map +1 -1
  50. package/build/card/index.js.map +1 -1
  51. package/build/card/styles.js +17 -17
  52. package/build/card/styles.js.map +1 -1
  53. package/build/color-list-picker/index.js +16 -5
  54. package/build/color-list-picker/index.js.map +1 -1
  55. package/build/color-palette/index.js +22 -12
  56. package/build/color-palette/index.js.map +1 -1
  57. package/build/color-palette/index.native.js +3 -3
  58. package/build/color-palette/index.native.js.map +1 -1
  59. package/build/combobox-control/index.js +7 -4
  60. package/build/combobox-control/index.js.map +1 -1
  61. package/build/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/constants.js +0 -0
  62. package/build/custom-gradient-picker/gradient-bar/constants.js.map +1 -0
  63. package/build/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/control-points.js +34 -41
  64. package/build/custom-gradient-picker/gradient-bar/control-points.js.map +1 -0
  65. package/build/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/index.js +0 -1
  66. package/build/custom-gradient-picker/gradient-bar/index.js.map +1 -0
  67. package/build/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/utils.js +0 -0
  68. package/build/custom-gradient-picker/gradient-bar/utils.js.map +1 -0
  69. package/build/custom-gradient-picker/index.js +19 -4
  70. package/build/custom-gradient-picker/index.js.map +1 -1
  71. package/build/custom-select-control/index.js +21 -11
  72. package/build/custom-select-control/index.js.map +1 -1
  73. package/build/custom-select-control/styles.js +34 -0
  74. package/build/custom-select-control/styles.js.map +1 -0
  75. package/build/date-time/constants.js +9 -0
  76. package/build/date-time/constants.js.map +1 -0
  77. package/build/date-time/date/index.js +175 -186
  78. package/build/date-time/date/index.js.map +1 -1
  79. package/build/date-time/date/styles.js +59 -40
  80. package/build/date-time/date/styles.js.map +1 -1
  81. package/build/date-time/date-time/index.js +5 -2
  82. package/build/date-time/date-time/index.js.map +1 -1
  83. package/build/date-time/time/index.js +22 -17
  84. package/build/date-time/time/index.js.map +1 -1
  85. package/build/date-time/utils.js +27 -0
  86. package/build/date-time/utils.js.map +1 -0
  87. package/build/dimension-control/index.js +2 -6
  88. package/build/dimension-control/index.js.map +1 -1
  89. package/build/disabled/index.js +28 -11
  90. package/build/disabled/index.js.map +1 -1
  91. package/build/disabled/styles/disabled-styles.js +3 -3
  92. package/build/disabled/styles/disabled-styles.js.map +1 -1
  93. package/build/{swatch → disabled}/types.js +0 -0
  94. package/build/{swatch → disabled}/types.js.map +0 -0
  95. package/build/drop-zone/index.js +2 -4
  96. package/build/drop-zone/index.js.map +1 -1
  97. package/build/dropdown/index.js +5 -6
  98. package/build/dropdown/index.js.map +1 -1
  99. package/build/duotone-picker/custom-duotone-bar.js +2 -2
  100. package/build/duotone-picker/custom-duotone-bar.js.map +1 -1
  101. package/build/duotone-picker/duotone-picker.js +9 -1
  102. package/build/duotone-picker/duotone-picker.js.map +1 -1
  103. package/build/duotone-picker/duotone-swatch.js +13 -3
  104. package/build/duotone-picker/duotone-swatch.js.map +1 -1
  105. package/build/focal-point-picker/index.js +4 -6
  106. package/build/focal-point-picker/index.js.map +1 -1
  107. package/build/focal-point-picker/styles/focal-point-style.js +4 -4
  108. package/build/focal-point-picker/styles/focal-point-style.js.map +1 -1
  109. package/build/font-size-picker/index.js +43 -14
  110. package/build/font-size-picker/index.js.map +1 -1
  111. package/build/font-size-picker/utils.js +32 -22
  112. package/build/font-size-picker/utils.js.map +1 -1
  113. package/build/form-token-field/index.js +29 -20
  114. package/build/form-token-field/index.js.map +1 -1
  115. package/build/gradient-picker/index.js +15 -3
  116. package/build/gradient-picker/index.js.map +1 -1
  117. package/build/guide/index.js +5 -3
  118. package/build/guide/index.js.map +1 -1
  119. package/build/guide/page-control.js +3 -7
  120. package/build/guide/page-control.js.map +1 -1
  121. package/build/item-group/styles.js +10 -10
  122. package/build/item-group/styles.js.map +1 -1
  123. package/build/mobile/bottom-sheet/index.native.js +3 -6
  124. package/build/mobile/bottom-sheet/index.native.js.map +1 -1
  125. package/build/mobile/global-styles-context/utils.native.js +3 -1
  126. package/build/mobile/global-styles-context/utils.native.js.map +1 -1
  127. package/build/mobile/image/index.native.js +6 -26
  128. package/build/mobile/image/index.native.js.map +1 -1
  129. package/build/mobile/picker/index.android.js +3 -3
  130. package/build/mobile/picker/index.android.js.map +1 -1
  131. package/build/modal/index.js +1 -3
  132. package/build/modal/index.js.map +1 -1
  133. package/build/navigable-container/container.js +9 -8
  134. package/build/navigable-container/container.js.map +1 -1
  135. package/build/navigable-container/menu.js +3 -9
  136. package/build/navigable-container/menu.js.map +1 -1
  137. package/build/navigator/navigator-back-button/hook.js +0 -4
  138. package/build/navigator/navigator-back-button/hook.js.map +1 -1
  139. package/build/navigator/navigator-button/hook.js +0 -4
  140. package/build/navigator/navigator-button/hook.js.map +1 -1
  141. package/build/notice/list.js +10 -6
  142. package/build/notice/list.js.map +1 -1
  143. package/build/palette-edit/index.js +1 -0
  144. package/build/palette-edit/index.js.map +1 -1
  145. package/build/palette-edit/styles.js +10 -10
  146. package/build/palette-edit/styles.js.map +1 -1
  147. package/build/popover/index.js +100 -97
  148. package/build/popover/index.js.map +1 -1
  149. package/build/popover/utils.js +178 -0
  150. package/build/popover/utils.js.map +1 -0
  151. package/build/range-control/index.js +3 -0
  152. package/build/range-control/index.js.map +1 -1
  153. package/build/range-control/styles/range-control-styles.js +43 -37
  154. package/build/range-control/styles/range-control-styles.js.map +1 -1
  155. package/build/select-control/chevron-down.js +30 -0
  156. package/build/select-control/chevron-down.js.map +1 -0
  157. package/build/select-control/index.js +7 -7
  158. package/build/select-control/index.js.map +1 -1
  159. package/build/select-control/styles/select-control-styles.js +30 -15
  160. package/build/select-control/styles/select-control-styles.js.map +1 -1
  161. package/build/snackbar/list.js +5 -3
  162. package/build/snackbar/list.js.map +1 -1
  163. package/build/spinner/index.js +2 -0
  164. package/build/spinner/index.js.map +1 -1
  165. package/build/text/styles.js +7 -7
  166. package/build/text/styles.js.map +1 -1
  167. package/build/toggle-group-control/toggle-group-control/component.js +16 -8
  168. package/build/toggle-group-control/toggle-group-control/component.js.map +1 -1
  169. package/build/toggle-group-control/toggle-group-control/styles.js +7 -5
  170. package/build/toggle-group-control/toggle-group-control/styles.js.map +1 -1
  171. package/build/toggle-group-control/toggle-group-control-option/component.js +15 -5
  172. package/build/toggle-group-control/toggle-group-control-option/component.js.map +1 -1
  173. package/build/toggle-group-control/toggle-group-control-option-base/component.js +5 -1
  174. package/build/toggle-group-control/toggle-group-control-option-base/component.js.map +1 -1
  175. package/build/toggle-group-control/toggle-group-control-option-base/styles.js +20 -7
  176. package/build/toggle-group-control/toggle-group-control-option-base/styles.js.map +1 -1
  177. package/build/toggle-group-control/toggle-group-control-option-icon/component.js +26 -7
  178. package/build/toggle-group-control/toggle-group-control-option-icon/component.js.map +1 -1
  179. package/build/tools-panel/styles.js +11 -11
  180. package/build/tools-panel/styles.js.map +1 -1
  181. package/build/tooltip/index.js +1 -7
  182. package/build/tooltip/index.js.map +1 -1
  183. package/build/tree-grid/index.js +4 -10
  184. package/build/tree-grid/index.js.map +1 -1
  185. package/build/ui/context/context-connect.js +1 -3
  186. package/build/ui/context/context-connect.js.map +1 -1
  187. package/build/unit-control/index.js +2 -3
  188. package/build/unit-control/index.js.map +1 -1
  189. package/build/utils/colors-values.js +52 -142
  190. package/build/utils/colors-values.js.map +1 -1
  191. package/build/utils/config-values.js +1 -1
  192. package/build/utils/config-values.js.map +1 -1
  193. package/build/utils/input/input-control.js +1 -1
  194. package/build/utils/input/input-control.js.map +1 -1
  195. package/build/utils/rtl.js +6 -5
  196. package/build/utils/rtl.js.map +1 -1
  197. package/build/utils/strings.js +50 -0
  198. package/build/utils/strings.js.map +1 -0
  199. package/build-module/alignment-matrix-control/index.js +1 -1
  200. package/build-module/alignment-matrix-control/index.js.map +1 -1
  201. package/build-module/alignment-matrix-control/styles/alignment-matrix-control-styles.js +9 -9
  202. package/build-module/alignment-matrix-control/styles/alignment-matrix-control-styles.js.map +1 -1
  203. package/build-module/angle-picker-control/index.js +3 -0
  204. package/build-module/angle-picker-control/index.js.map +1 -1
  205. package/build-module/angle-picker-control/styles/angle-picker-control-styles.js +13 -4
  206. package/build-module/angle-picker-control/styles/angle-picker-control-styles.js.map +1 -1
  207. package/build-module/autocomplete/index.js +9 -10
  208. package/build-module/autocomplete/index.js.map +1 -1
  209. package/build-module/base-control/styles/base-control-styles.js +8 -8
  210. package/build-module/base-control/styles/base-control-styles.js.map +1 -1
  211. package/build-module/base-field/styles.js +5 -5
  212. package/build-module/base-field/styles.js.map +1 -1
  213. package/build-module/box-control/styles/box-control-visualizer-styles.js +8 -8
  214. package/build-module/box-control/styles/box-control-visualizer-styles.js.map +1 -1
  215. package/build-module/card/card/component.js +5 -10
  216. package/build-module/card/card/component.js.map +1 -1
  217. package/build-module/card/card/hook.js +0 -9
  218. package/build-module/card/card/hook.js.map +1 -1
  219. package/build-module/card/card/index.js.map +1 -1
  220. package/build-module/card/card-body/component.js +7 -8
  221. package/build-module/card/card-body/component.js.map +1 -1
  222. package/build-module/card/card-body/hook.js +0 -4
  223. package/build-module/card/card-body/hook.js.map +1 -1
  224. package/build-module/card/card-body/index.js.map +1 -1
  225. package/build-module/card/card-divider/component.js +7 -8
  226. package/build-module/card/card-divider/component.js.map +1 -1
  227. package/build-module/card/card-divider/hook.js +0 -4
  228. package/build-module/card/card-divider/hook.js.map +1 -1
  229. package/build-module/card/card-divider/index.js.map +1 -1
  230. package/build-module/card/card-footer/component.js +7 -8
  231. package/build-module/card/card-footer/component.js.map +1 -1
  232. package/build-module/card/card-footer/hook.js +0 -4
  233. package/build-module/card/card-footer/hook.js.map +1 -1
  234. package/build-module/card/card-footer/index.js.map +1 -1
  235. package/build-module/card/card-header/component.js +7 -8
  236. package/build-module/card/card-header/component.js.map +1 -1
  237. package/build-module/card/card-header/hook.js +0 -4
  238. package/build-module/card/card-header/hook.js.map +1 -1
  239. package/build-module/card/card-header/index.js.map +1 -1
  240. package/build-module/card/card-media/component.js +7 -7
  241. package/build-module/card/card-media/component.js.map +1 -1
  242. package/build-module/card/card-media/hook.js +0 -4
  243. package/build-module/card/card-media/hook.js.map +1 -1
  244. package/build-module/card/card-media/index.js.map +1 -1
  245. package/build-module/card/context.js.map +1 -1
  246. package/build-module/card/index.js.map +1 -1
  247. package/build-module/card/styles.js +17 -17
  248. package/build-module/card/styles.js.map +1 -1
  249. package/build-module/color-list-picker/index.js +13 -5
  250. package/build-module/color-list-picker/index.js.map +1 -1
  251. package/build-module/color-palette/index.js +22 -11
  252. package/build-module/color-palette/index.js.map +1 -1
  253. package/build-module/color-palette/index.native.js +4 -4
  254. package/build-module/color-palette/index.native.js.map +1 -1
  255. package/build-module/combobox-control/index.js +6 -3
  256. package/build-module/combobox-control/index.js.map +1 -1
  257. package/build-module/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/constants.js +0 -0
  258. package/build-module/custom-gradient-picker/gradient-bar/constants.js.map +1 -0
  259. package/build-module/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/control-points.js +34 -41
  260. package/build-module/custom-gradient-picker/gradient-bar/control-points.js.map +1 -0
  261. package/build-module/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/index.js +0 -1
  262. package/build-module/custom-gradient-picker/gradient-bar/index.js.map +1 -0
  263. package/build-module/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/utils.js +0 -0
  264. package/build-module/custom-gradient-picker/gradient-bar/utils.js.map +1 -0
  265. package/build-module/custom-gradient-picker/index.js +16 -3
  266. package/build-module/custom-gradient-picker/index.js.map +1 -1
  267. package/build-module/custom-select-control/index.js +20 -12
  268. package/build-module/custom-select-control/index.js.map +1 -1
  269. package/build-module/custom-select-control/styles.js +22 -0
  270. package/build-module/custom-select-control/styles.js.map +1 -0
  271. package/build-module/date-time/constants.js +2 -0
  272. package/build-module/date-time/constants.js.map +1 -0
  273. package/build-module/date-time/date/index.js +178 -184
  274. package/build-module/date-time/date/index.js.map +1 -1
  275. package/build-module/date-time/date/styles.js +47 -38
  276. package/build-module/date-time/date/styles.js.map +1 -1
  277. package/build-module/date-time/date-time/index.js +4 -2
  278. package/build-module/date-time/date-time/index.js.map +1 -1
  279. package/build-module/date-time/time/index.js +20 -16
  280. package/build-module/date-time/time/index.js.map +1 -1
  281. package/build-module/date-time/utils.js +19 -0
  282. package/build-module/date-time/utils.js.map +1 -0
  283. package/build-module/dimension-control/index.js +1 -5
  284. package/build-module/dimension-control/index.js.map +1 -1
  285. package/build-module/disabled/index.js +28 -11
  286. package/build-module/disabled/index.js.map +1 -1
  287. package/build-module/disabled/styles/disabled-styles.js +3 -3
  288. package/build-module/disabled/styles/disabled-styles.js.map +1 -1
  289. package/build-module/{swatch → disabled}/types.js +0 -0
  290. package/build-module/{swatch → disabled}/types.js.map +0 -0
  291. package/build-module/drop-zone/index.js +2 -3
  292. package/build-module/drop-zone/index.js.map +1 -1
  293. package/build-module/dropdown/index.js +5 -6
  294. package/build-module/dropdown/index.js.map +1 -1
  295. package/build-module/duotone-picker/custom-duotone-bar.js +1 -1
  296. package/build-module/duotone-picker/custom-duotone-bar.js.map +1 -1
  297. package/build-module/duotone-picker/duotone-picker.js +7 -1
  298. package/build-module/duotone-picker/duotone-picker.js.map +1 -1
  299. package/build-module/duotone-picker/duotone-swatch.js +11 -3
  300. package/build-module/duotone-picker/duotone-swatch.js.map +1 -1
  301. package/build-module/focal-point-picker/index.js +4 -5
  302. package/build-module/focal-point-picker/index.js.map +1 -1
  303. package/build-module/focal-point-picker/styles/focal-point-style.js +4 -4
  304. package/build-module/focal-point-picker/styles/focal-point-style.js.map +1 -1
  305. package/build-module/font-size-picker/index.js +39 -14
  306. package/build-module/font-size-picker/index.js.map +1 -1
  307. package/build-module/font-size-picker/utils.js +31 -23
  308. package/build-module/font-size-picker/utils.js.map +1 -1
  309. package/build-module/form-token-field/index.js +30 -20
  310. package/build-module/form-token-field/index.js.map +1 -1
  311. package/build-module/gradient-picker/index.js +14 -3
  312. package/build-module/gradient-picker/index.js.map +1 -1
  313. package/build-module/guide/index.js +5 -3
  314. package/build-module/guide/index.js.map +1 -1
  315. package/build-module/guide/page-control.js +3 -6
  316. package/build-module/guide/page-control.js.map +1 -1
  317. package/build-module/item-group/styles.js +10 -10
  318. package/build-module/item-group/styles.js.map +1 -1
  319. package/build-module/mobile/bottom-sheet/index.native.js +3 -5
  320. package/build-module/mobile/bottom-sheet/index.native.js.map +1 -1
  321. package/build-module/mobile/global-styles-context/utils.native.js +2 -1
  322. package/build-module/mobile/global-styles-context/utils.native.js.map +1 -1
  323. package/build-module/mobile/image/index.native.js +8 -28
  324. package/build-module/mobile/image/index.native.js.map +1 -1
  325. package/build-module/mobile/picker/index.android.js +3 -3
  326. package/build-module/mobile/picker/index.android.js.map +1 -1
  327. package/build-module/modal/index.js +1 -2
  328. package/build-module/modal/index.js.map +1 -1
  329. package/build-module/navigable-container/container.js +9 -7
  330. package/build-module/navigable-container/container.js.map +1 -1
  331. package/build-module/navigable-container/menu.js +3 -8
  332. package/build-module/navigable-container/menu.js.map +1 -1
  333. package/build-module/navigator/navigator-back-button/hook.js +0 -4
  334. package/build-module/navigator/navigator-back-button/hook.js.map +1 -1
  335. package/build-module/navigator/navigator-button/hook.js +0 -4
  336. package/build-module/navigator/navigator-button/hook.js.map +1 -1
  337. package/build-module/notice/list.js +10 -5
  338. package/build-module/notice/list.js.map +1 -1
  339. package/build-module/palette-edit/index.js +1 -0
  340. package/build-module/palette-edit/index.js.map +1 -1
  341. package/build-module/palette-edit/styles.js +10 -10
  342. package/build-module/palette-edit/styles.js.map +1 -1
  343. package/build-module/popover/index.js +100 -98
  344. package/build-module/popover/index.js.map +1 -1
  345. package/build-module/popover/utils.js +164 -0
  346. package/build-module/popover/utils.js.map +1 -0
  347. package/build-module/range-control/index.js +3 -0
  348. package/build-module/range-control/index.js.map +1 -1
  349. package/build-module/range-control/styles/range-control-styles.js +43 -37
  350. package/build-module/range-control/styles/range-control-styles.js.map +1 -1
  351. package/build-module/select-control/chevron-down.js +21 -0
  352. package/build-module/select-control/chevron-down.js.map +1 -0
  353. package/build-module/select-control/index.js +7 -7
  354. package/build-module/select-control/index.js.map +1 -1
  355. package/build-module/select-control/styles/select-control-styles.js +24 -13
  356. package/build-module/select-control/styles/select-control-styles.js.map +1 -1
  357. package/build-module/snackbar/list.js +5 -2
  358. package/build-module/snackbar/list.js.map +1 -1
  359. package/build-module/spinner/index.js +2 -0
  360. package/build-module/spinner/index.js.map +1 -1
  361. package/build-module/text/styles.js +7 -7
  362. package/build-module/text/styles.js.map +1 -1
  363. package/build-module/toggle-group-control/toggle-group-control/component.js +14 -7
  364. package/build-module/toggle-group-control/toggle-group-control/component.js.map +1 -1
  365. package/build-module/toggle-group-control/toggle-group-control/styles.js +5 -4
  366. package/build-module/toggle-group-control/toggle-group-control/styles.js.map +1 -1
  367. package/build-module/toggle-group-control/toggle-group-control-option/component.js +17 -3
  368. package/build-module/toggle-group-control/toggle-group-control-option/component.js.map +1 -1
  369. package/build-module/toggle-group-control/toggle-group-control-option-base/component.js +5 -1
  370. package/build-module/toggle-group-control/toggle-group-control-option-base/component.js.map +1 -1
  371. package/build-module/toggle-group-control/toggle-group-control-option-base/styles.js +16 -6
  372. package/build-module/toggle-group-control/toggle-group-control-option-base/styles.js.map +1 -1
  373. package/build-module/toggle-group-control/toggle-group-control-option-icon/component.js +27 -5
  374. package/build-module/toggle-group-control/toggle-group-control-option-icon/component.js.map +1 -1
  375. package/build-module/tools-panel/styles.js +11 -11
  376. package/build-module/tools-panel/styles.js.map +1 -1
  377. package/build-module/tooltip/index.js +1 -6
  378. package/build-module/tooltip/index.js.map +1 -1
  379. package/build-module/tree-grid/index.js +4 -9
  380. package/build-module/tree-grid/index.js.map +1 -1
  381. package/build-module/ui/context/context-connect.js +1 -2
  382. package/build-module/ui/context/context-connect.js.map +1 -1
  383. package/build-module/unit-control/index.js +2 -2
  384. package/build-module/unit-control/index.js.map +1 -1
  385. package/build-module/utils/colors-values.js +52 -141
  386. package/build-module/utils/colors-values.js.map +1 -1
  387. package/build-module/utils/config-values.js +1 -1
  388. package/build-module/utils/config-values.js.map +1 -1
  389. package/build-module/utils/input/input-control.js +1 -1
  390. package/build-module/utils/input/input-control.js.map +1 -1
  391. package/build-module/utils/rtl.js +6 -4
  392. package/build-module/utils/rtl.js.map +1 -1
  393. package/build-module/utils/strings.js +37 -0
  394. package/build-module/utils/strings.js.map +1 -0
  395. package/build-style/style-rtl.css +176 -1227
  396. package/build-style/style.css +171 -1225
  397. package/build-types/card/card/component.d.ts +3 -3
  398. package/build-types/card/card/component.d.ts.map +1 -1
  399. package/build-types/card/card/hook.d.ts +7 -2
  400. package/build-types/card/card/hook.d.ts.map +1 -1
  401. package/build-types/card/card/index.d.ts +2 -2
  402. package/build-types/card/card/index.d.ts.map +1 -1
  403. package/build-types/card/card-body/component.d.ts +3 -3
  404. package/build-types/card/card-body/component.d.ts.map +1 -1
  405. package/build-types/card/card-body/hook.d.ts +5 -2
  406. package/build-types/card/card-body/hook.d.ts.map +1 -1
  407. package/build-types/card/card-body/index.d.ts +2 -2
  408. package/build-types/card/card-body/index.d.ts.map +1 -1
  409. package/build-types/card/card-divider/component.d.ts +3 -3
  410. package/build-types/card/card-divider/component.d.ts.map +1 -1
  411. package/build-types/card/card-divider/hook.d.ts +5 -2
  412. package/build-types/card/card-divider/hook.d.ts.map +1 -1
  413. package/build-types/card/card-divider/index.d.ts +2 -2
  414. package/build-types/card/card-divider/index.d.ts.map +1 -1
  415. package/build-types/card/card-footer/component.d.ts +3 -3
  416. package/build-types/card/card-footer/component.d.ts.map +1 -1
  417. package/build-types/card/card-footer/hook.d.ts +5 -2
  418. package/build-types/card/card-footer/hook.d.ts.map +1 -1
  419. package/build-types/card/card-footer/index.d.ts +2 -2
  420. package/build-types/card/card-footer/index.d.ts.map +1 -1
  421. package/build-types/card/card-header/component.d.ts +3 -3
  422. package/build-types/card/card-header/component.d.ts.map +1 -1
  423. package/build-types/card/card-header/hook.d.ts +5 -2
  424. package/build-types/card/card-header/hook.d.ts.map +1 -1
  425. package/build-types/card/card-header/index.d.ts +2 -2
  426. package/build-types/card/card-header/index.d.ts.map +1 -1
  427. package/build-types/card/card-media/component.d.ts +3 -4
  428. package/build-types/card/card-media/component.d.ts.map +1 -1
  429. package/build-types/card/card-media/hook.d.ts +6 -5
  430. package/build-types/card/card-media/hook.d.ts.map +1 -1
  431. package/build-types/card/card-media/index.d.ts +2 -2
  432. package/build-types/card/card-media/index.d.ts.map +1 -1
  433. package/build-types/card/context.d.ts +3 -2
  434. package/build-types/card/context.d.ts.map +1 -1
  435. package/build-types/card/index.d.ts +6 -6
  436. package/build-types/card/index.d.ts.map +1 -1
  437. package/build-types/card/stories/index.d.ts +12 -0
  438. package/build-types/card/stories/index.d.ts.map +1 -0
  439. package/build-types/card/styles.d.ts +20 -22
  440. package/build-types/card/styles.d.ts.map +1 -1
  441. package/build-types/card/test/index.d.ts +2 -0
  442. package/build-types/{flyout → card/test}/index.d.ts.map +1 -1
  443. package/build-types/card/types.d.ts +7 -1
  444. package/build-types/card/types.d.ts.map +1 -1
  445. package/build-types/color-palette/index.d.ts +2 -1
  446. package/build-types/color-palette/index.d.ts.map +1 -1
  447. package/build-types/color-picker/styles.d.ts +2 -2
  448. package/build-types/date-time/constants.d.ts +2 -0
  449. package/build-types/date-time/constants.d.ts.map +1 -0
  450. package/build-types/date-time/date/index.d.ts +3 -1
  451. package/build-types/date-time/date/index.d.ts.map +1 -1
  452. package/build-types/date-time/date/styles.d.ts +22 -8
  453. package/build-types/date-time/date/styles.d.ts.map +1 -1
  454. package/build-types/date-time/date/test/index.d.ts +1 -1
  455. package/build-types/date-time/date/test/index.d.ts.map +1 -1
  456. package/build-types/date-time/date-time/index.d.ts.map +1 -1
  457. package/build-types/date-time/time/index.d.ts.map +1 -1
  458. package/build-types/date-time/utils.d.ts +8 -0
  459. package/build-types/date-time/utils.d.ts.map +1 -0
  460. package/build-types/disabled/index.d.ts +35 -28
  461. package/build-types/disabled/index.d.ts.map +1 -1
  462. package/build-types/disabled/stories/index.d.ts +13 -0
  463. package/build-types/disabled/stories/index.d.ts.map +1 -0
  464. package/build-types/disabled/styles/disabled-styles.d.ts +2 -1
  465. package/build-types/disabled/styles/disabled-styles.d.ts.map +1 -1
  466. package/build-types/disabled/test/index.d.ts +2 -0
  467. package/build-types/{flyout/flyout → disabled/test}/index.d.ts.map +1 -1
  468. package/build-types/disabled/types.d.ts +14 -0
  469. package/build-types/disabled/types.d.ts.map +1 -0
  470. package/build-types/dropdown/index.d.ts.map +1 -1
  471. package/build-types/form-token-field/index.d.ts.map +1 -1
  472. package/build-types/form-token-field/stories/index.d.ts +1 -0
  473. package/build-types/form-token-field/stories/index.d.ts.map +1 -1
  474. package/build-types/form-token-field/test/index.d.ts +2 -0
  475. package/build-types/form-token-field/test/index.d.ts.map +1 -0
  476. package/build-types/form-token-field/types.d.ts +7 -0
  477. package/build-types/form-token-field/types.d.ts.map +1 -1
  478. package/build-types/navigable-container/menu.d.ts.map +1 -1
  479. package/build-types/navigator/navigator-back-button/hook.d.ts +0 -3
  480. package/build-types/navigator/navigator-back-button/hook.d.ts.map +1 -1
  481. package/build-types/navigator/navigator-button/hook.d.ts +0 -3
  482. package/build-types/navigator/navigator-button/hook.d.ts.map +1 -1
  483. package/build-types/placeholder/test/index.d.ts +2 -0
  484. package/build-types/placeholder/test/index.d.ts.map +1 -0
  485. package/build-types/popover/index.d.ts +1 -1
  486. package/build-types/popover/index.d.ts.map +1 -1
  487. package/build-types/popover/utils.d.ts +26 -69
  488. package/build-types/popover/utils.d.ts.map +1 -1
  489. package/build-types/range-control/index.d.ts +2 -2
  490. package/build-types/range-control/index.d.ts.map +1 -1
  491. package/build-types/range-control/styles/range-control-styles.d.ts +5 -2
  492. package/build-types/range-control/styles/range-control-styles.d.ts.map +1 -1
  493. package/build-types/range-control/types.d.ts +2 -2
  494. package/build-types/range-control/types.d.ts.map +1 -1
  495. package/build-types/select-control/chevron-down.d.ts +4 -0
  496. package/build-types/select-control/chevron-down.d.ts.map +1 -0
  497. package/build-types/select-control/index.d.ts.map +1 -1
  498. package/build-types/select-control/styles/select-control-styles.d.ts +8 -0
  499. package/build-types/select-control/styles/select-control-styles.d.ts.map +1 -1
  500. package/build-types/spinner/index.d.ts.map +1 -1
  501. package/build-types/toggle-group-control/stories/index.d.ts +25 -0
  502. package/build-types/toggle-group-control/stories/index.d.ts.map +1 -0
  503. package/build-types/toggle-group-control/test/index.d.ts +2 -0
  504. package/build-types/toggle-group-control/test/index.d.ts.map +1 -0
  505. package/build-types/toggle-group-control/toggle-group-control/component.d.ts +6 -3
  506. package/build-types/toggle-group-control/toggle-group-control/component.d.ts.map +1 -1
  507. package/build-types/toggle-group-control/toggle-group-control/styles.d.ts +1 -0
  508. package/build-types/toggle-group-control/toggle-group-control/styles.d.ts.map +1 -1
  509. package/build-types/toggle-group-control/toggle-group-control-option/component.d.ts +6 -7
  510. package/build-types/toggle-group-control/toggle-group-control-option/component.d.ts.map +1 -1
  511. package/build-types/toggle-group-control/toggle-group-control-option-base/component.d.ts.map +1 -1
  512. package/build-types/toggle-group-control/toggle-group-control-option-base/styles.d.ts +4 -0
  513. package/build-types/toggle-group-control/toggle-group-control-option-base/styles.d.ts.map +1 -1
  514. package/build-types/toggle-group-control/toggle-group-control-option-icon/component.d.ts +11 -9
  515. package/build-types/toggle-group-control/toggle-group-control-option-icon/component.d.ts.map +1 -1
  516. package/build-types/toggle-group-control/types.d.ts +23 -10
  517. package/build-types/toggle-group-control/types.d.ts.map +1 -1
  518. package/build-types/tooltip/index.d.ts.map +1 -1
  519. package/build-types/ui/context/context-connect.d.ts +3 -0
  520. package/build-types/ui/context/context-connect.d.ts.map +1 -1
  521. package/build-types/ui/form-group/use-form-group.d.ts +2 -2
  522. package/build-types/unit-control/index.d.ts +2 -1
  523. package/build-types/unit-control/index.d.ts.map +1 -1
  524. package/build-types/unit-control/types.d.ts +5 -1
  525. package/build-types/unit-control/types.d.ts.map +1 -1
  526. package/build-types/utils/colors-values.d.ts +18 -97
  527. package/build-types/utils/colors-values.d.ts.map +1 -1
  528. package/build-types/utils/rtl.d.ts.map +1 -1
  529. package/build-types/utils/strings.d.ts +2 -0
  530. package/build-types/utils/strings.d.ts.map +1 -0
  531. package/package.json +22 -21
  532. package/src/alignment-matrix-control/index.js +1 -1
  533. package/src/alignment-matrix-control/stories/index.js +49 -24
  534. package/src/alignment-matrix-control/styles/alignment-matrix-control-styles.js +1 -3
  535. package/src/angle-picker-control/index.js +7 -1
  536. package/src/angle-picker-control/stories/index.js +8 -5
  537. package/src/angle-picker-control/styles/angle-picker-control-styles.js +12 -3
  538. package/src/autocomplete/index.js +9 -18
  539. package/src/base-control/styles/base-control-styles.ts +1 -1
  540. package/src/base-field/styles.js +1 -1
  541. package/src/base-field/test/__snapshots__/index.js.snap +2 -2
  542. package/src/box-control/styles/box-control-visualizer-styles.js +0 -1
  543. package/src/box-control/test/index.js +8 -8
  544. package/src/button/test/index.js +188 -146
  545. package/src/card/card/{component.js → component.tsx} +13 -9
  546. package/src/card/card/{hook.js → hook.ts} +11 -11
  547. package/src/card/card/{index.js → index.ts} +0 -0
  548. package/src/card/card-body/{component.js → component.tsx} +13 -9
  549. package/src/card/card-body/{hook.js → hook.ts} +5 -5
  550. package/src/card/card-body/{index.js → index.ts} +0 -0
  551. package/src/card/card-divider/{component.js → component.tsx} +16 -10
  552. package/src/card/card-divider/{hook.js → hook.ts} +5 -5
  553. package/src/card/card-divider/{index.js → index.ts} +0 -0
  554. package/src/card/card-footer/{component.js → component.tsx} +13 -9
  555. package/src/card/card-footer/{hook.js → hook.ts} +5 -5
  556. package/src/card/card-footer/{index.js → index.ts} +0 -0
  557. package/src/card/card-header/{component.js → component.tsx} +13 -9
  558. package/src/card/card-header/{hook.js → hook.ts} +5 -5
  559. package/src/card/card-header/{index.js → index.ts} +0 -0
  560. package/src/card/card-media/{component.js → component.tsx} +13 -8
  561. package/src/card/card-media/{hook.js → hook.ts} +5 -5
  562. package/src/card/card-media/{index.js → index.ts} +0 -0
  563. package/src/card/{context.js → context.ts} +0 -0
  564. package/src/card/{index.js → index.ts} +0 -0
  565. package/src/card/stories/index.tsx +75 -0
  566. package/src/card/{styles.js → styles.ts} +0 -0
  567. package/src/card/test/__snapshots__/{index.js.snap → index.tsx.snap} +0 -0
  568. package/src/card/test/{index.js → index.tsx} +3 -3
  569. package/src/card/types.ts +8 -1
  570. package/src/color-list-picker/index.js +15 -3
  571. package/src/color-list-picker/style.scss +11 -0
  572. package/src/color-palette/index.js +32 -16
  573. package/src/color-palette/index.native.js +10 -6
  574. package/src/color-palette/stories/index.js +1 -1
  575. package/src/color-palette/test/__snapshots__/index.js.snap +17 -3
  576. package/src/combobox-control/index.js +6 -5
  577. package/src/combobox-control/stories/index.js +9 -3
  578. package/src/combobox-control/style.scss +0 -1
  579. package/src/confirm-dialog/test/index.js +85 -62
  580. package/src/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/constants.js +0 -0
  581. package/src/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/control-points.js +54 -50
  582. package/src/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/index.js +0 -3
  583. package/src/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/test/utils.js +0 -0
  584. package/src/{custom-gradient-bar → custom-gradient-picker/gradient-bar}/utils.js +0 -0
  585. package/src/custom-gradient-picker/index.js +18 -3
  586. package/src/custom-gradient-picker/stories/index.js +4 -3
  587. package/src/custom-gradient-picker/style.scss +30 -27
  588. package/src/custom-select-control/README.md +10 -0
  589. package/src/custom-select-control/index.js +22 -16
  590. package/src/custom-select-control/stories/index.js +1 -1
  591. package/src/custom-select-control/style.scss +0 -12
  592. package/src/custom-select-control/styles.ts +28 -0
  593. package/src/custom-select-control/test/index.js +4 -1
  594. package/src/date-time/constants.ts +1 -0
  595. package/src/date-time/date/index.tsx +289 -184
  596. package/src/date-time/date/styles.ts +86 -27
  597. package/src/date-time/date/test/index.tsx +18 -28
  598. package/src/date-time/date-time/index.tsx +3 -2
  599. package/src/date-time/time/index.tsx +23 -19
  600. package/src/date-time/utils.ts +17 -0
  601. package/src/dimension-control/index.js +1 -5
  602. package/src/disabled/index.tsx +80 -0
  603. package/src/disabled/stories/index.tsx +87 -0
  604. package/src/disabled/styles/{disabled-styles.js → disabled-styles.tsx} +0 -0
  605. package/src/disabled/test/index.tsx +174 -0
  606. package/src/disabled/types.ts +13 -0
  607. package/src/drop-zone/index.js +2 -3
  608. package/src/dropdown/index.js +3 -5
  609. package/src/dropdown-menu/stories/index.js +13 -2
  610. package/src/duotone-picker/custom-duotone-bar.js +1 -1
  611. package/src/duotone-picker/duotone-picker.js +34 -26
  612. package/src/duotone-picker/duotone-swatch.js +12 -5
  613. package/src/duotone-picker/stories/duotone-picker.js +66 -0
  614. package/src/duotone-picker/stories/duotone-swatch.js +32 -0
  615. package/src/focal-point-picker/index.js +10 -5
  616. package/src/focal-point-picker/styles/focal-point-style.js +0 -1
  617. package/src/focal-point-picker/test/index.js +78 -45
  618. package/src/font-size-picker/index.js +161 -144
  619. package/src/font-size-picker/stories/index.js +1 -0
  620. package/src/font-size-picker/style.scss +8 -5
  621. package/src/font-size-picker/test/index.js +13 -6
  622. package/src/font-size-picker/test/{util.js → utils.js} +77 -1
  623. package/src/font-size-picker/utils.js +38 -23
  624. package/src/form-token-field/README.md +1 -0
  625. package/src/form-token-field/index.tsx +41 -33
  626. package/src/form-token-field/stories/index.tsx +8 -0
  627. package/src/form-token-field/test/index.tsx +2106 -0
  628. package/src/form-token-field/types.ts +7 -0
  629. package/src/gradient-picker/index.js +22 -7
  630. package/src/gradient-picker/stories/index.js +56 -81
  631. package/src/guide/index.js +3 -1
  632. package/src/guide/page-control.js +1 -6
  633. package/src/guide/stories/index.js +12 -11
  634. package/src/guide/test/index.js +54 -37
  635. package/src/guide/test/page-control.js +14 -17
  636. package/src/higher-order/with-notices/test/index.js +4 -2
  637. package/src/higher-order/with-spoken-messages/test/index.js +1 -1
  638. package/src/item-group/styles.ts +3 -3
  639. package/src/menu-group/test/index.js +18 -7
  640. package/src/mobile/bottom-sheet/index.native.js +2 -4
  641. package/src/mobile/global-styles-context/utils.native.js +2 -1
  642. package/src/mobile/image/index.native.js +9 -30
  643. package/src/mobile/picker/index.android.js +10 -4
  644. package/src/mobile/picker/styles.native.scss +4 -4
  645. package/src/modal/index.js +1 -2
  646. package/src/modal/test/index.js +11 -9
  647. package/src/navigable-container/container.js +12 -18
  648. package/src/navigable-container/menu.js +3 -7
  649. package/src/navigable-container/test/menu.js +14 -11
  650. package/src/navigator/navigator-back-button/hook.ts +0 -3
  651. package/src/navigator/navigator-button/hook.ts +0 -3
  652. package/src/notice/list.js +12 -10
  653. package/src/palette-edit/index.js +1 -0
  654. package/src/palette-edit/styles.js +6 -5
  655. package/src/placeholder/README.md +1 -1
  656. package/src/placeholder/style.scss +65 -20
  657. package/src/placeholder/test/index.tsx +174 -0
  658. package/src/popover/index.js +133 -112
  659. package/src/popover/stories/index.js +17 -3
  660. package/src/popover/test/__snapshots__/index.js.snap +6 -6
  661. package/src/popover/test/index.js +129 -36
  662. package/src/popover/utils.js +107 -0
  663. package/src/range-control/index.tsx +3 -0
  664. package/src/range-control/styles/range-control-styles.ts +9 -4
  665. package/src/range-control/types.ts +5 -2
  666. package/src/select-control/chevron-down.tsx +25 -0
  667. package/src/select-control/index.tsx +6 -9
  668. package/src/select-control/styles/select-control-styles.ts +23 -14
  669. package/src/slot-fill/test/slot.js +5 -5
  670. package/src/snackbar/list.js +3 -2
  671. package/src/spinner/index.tsx +2 -0
  672. package/src/style.scss +0 -2
  673. package/src/text/styles.js +1 -1
  674. package/src/text/test/index.tsx +1 -1
  675. package/src/toggle-group-control/stories/index.tsx +127 -0
  676. package/src/toggle-group-control/test/__snapshots__/{index.js.snap → index.tsx.snap} +28 -22
  677. package/src/toggle-group-control/test/{index.js → index.tsx} +2 -4
  678. package/src/toggle-group-control/toggle-group-control/README.md +2 -0
  679. package/src/toggle-group-control/toggle-group-control/component.tsx +18 -9
  680. package/src/toggle-group-control/toggle-group-control/styles.ts +9 -6
  681. package/src/toggle-group-control/toggle-group-control-option/component.tsx +19 -3
  682. package/src/toggle-group-control/toggle-group-control-option-base/component.tsx +3 -0
  683. package/src/toggle-group-control/toggle-group-control-option-base/styles.ts +18 -0
  684. package/src/toggle-group-control/toggle-group-control-option-icon/README.md +6 -8
  685. package/src/toggle-group-control/toggle-group-control-option-icon/component.tsx +30 -7
  686. package/src/toggle-group-control/types.ts +74 -59
  687. package/src/toolbar/test/index.js +4 -4
  688. package/src/toolbar-group/test/index.js +9 -15
  689. package/src/tools-panel/styles.ts +2 -2
  690. package/src/tools-panel/test/index.js +3 -2
  691. package/src/tooltip/index.js +1 -5
  692. package/src/tooltip/test/index.js +188 -172
  693. package/src/tree-grid/index.js +4 -9
  694. package/src/ui/context/context-connect.ts +3 -2
  695. package/src/unit-control/index.tsx +2 -2
  696. package/src/unit-control/types.ts +10 -1
  697. package/src/utils/colors-values.js +42 -137
  698. package/src/utils/config-values.js +1 -1
  699. package/src/utils/input/input-control.js +6 -6
  700. package/src/utils/rtl.js +6 -2
  701. package/src/utils/strings.ts +72 -0
  702. package/src/utils/test/strings.js +15 -0
  703. package/tsconfig.json +0 -1
  704. package/tsconfig.tsbuildinfo +1 -1
  705. package/build/custom-gradient-bar/constants.js.map +0 -1
  706. package/build/custom-gradient-bar/control-points.js.map +0 -1
  707. package/build/custom-gradient-bar/index.js.map +0 -1
  708. package/build/custom-gradient-bar/utils.js.map +0 -1
  709. package/build/date-time/date/utils.js +0 -34
  710. package/build/date-time/date/utils.js.map +0 -1
  711. package/build/swatch/index.js +0 -41
  712. package/build/swatch/index.js.map +0 -1
  713. package/build-module/custom-gradient-bar/constants.js.map +0 -1
  714. package/build-module/custom-gradient-bar/control-points.js.map +0 -1
  715. package/build-module/custom-gradient-bar/index.js.map +0 -1
  716. package/build-module/custom-gradient-bar/utils.js.map +0 -1
  717. package/build-module/date-time/date/utils.js +0 -22
  718. package/build-module/date-time/date/utils.js.map +0 -1
  719. package/build-module/swatch/index.js +0 -30
  720. package/build-module/swatch/index.js.map +0 -1
  721. package/build-types/color-picker/color-display.d.ts +0 -13
  722. package/build-types/color-picker/color-display.d.ts.map +0 -1
  723. package/build-types/date-time/date/test/utils.d.ts +0 -2
  724. package/build-types/date-time/date/test/utils.d.ts.map +0 -1
  725. package/build-types/date-time/date/utils.d.ts +0 -15
  726. package/build-types/date-time/date/utils.d.ts.map +0 -1
  727. package/build-types/flyout/context.d.ts +0 -6
  728. package/build-types/flyout/context.d.ts.map +0 -1
  729. package/build-types/flyout/flyout/component.d.ts +0 -21
  730. package/build-types/flyout/flyout/component.d.ts.map +0 -1
  731. package/build-types/flyout/flyout/hook.d.ts +0 -270
  732. package/build-types/flyout/flyout/hook.d.ts.map +0 -1
  733. package/build-types/flyout/flyout/index.d.ts +0 -3
  734. package/build-types/flyout/flyout-content/component.d.ts +0 -3
  735. package/build-types/flyout/flyout-content/component.d.ts.map +0 -1
  736. package/build-types/flyout/flyout-content/index.d.ts +0 -2
  737. package/build-types/flyout/flyout-content/index.d.ts.map +0 -1
  738. package/build-types/flyout/index.d.ts +0 -2
  739. package/build-types/flyout/styles.d.ts +0 -22
  740. package/build-types/flyout/styles.d.ts.map +0 -1
  741. package/build-types/flyout/types.d.ts +0 -80
  742. package/build-types/flyout/types.d.ts.map +0 -1
  743. package/build-types/flyout/utils.d.ts +0 -8
  744. package/build-types/flyout/utils.d.ts.map +0 -1
  745. package/build-types/form-token-field/test/lib/fixtures.d.ts +0 -26
  746. package/build-types/form-token-field/test/lib/fixtures.d.ts.map +0 -1
  747. package/build-types/form-token-field/test/lib/token-field-wrapper.d.ts +0 -21
  748. package/build-types/form-token-field/test/lib/token-field-wrapper.d.ts.map +0 -1
  749. package/build-types/swatch/index.d.ts +0 -5
  750. package/build-types/swatch/index.d.ts.map +0 -1
  751. package/build-types/swatch/types.d.ts +0 -11
  752. package/build-types/swatch/types.d.ts.map +0 -1
  753. package/build-types/utils/hooks/use-combined-ref.d.ts +0 -8
  754. package/build-types/utils/hooks/use-combined-ref.d.ts.map +0 -1
  755. package/src/card/stories/index.js +0 -209
  756. package/src/date-time/date/datepicker.scss +0 -863
  757. package/src/date-time/date/style.scss +0 -85
  758. package/src/date-time/date/test/utils.ts +0 -32
  759. package/src/date-time/date/utils.ts +0 -20
  760. package/src/date-time/style.scss +0 -1
  761. package/src/disabled/index.js +0 -55
  762. package/src/disabled/stories/index.js +0 -61
  763. package/src/disabled/test/index.js +0 -240
  764. package/src/form-token-field/test/index.js +0 -411
  765. package/src/form-token-field/test/lib/fixtures.js +0 -89
  766. package/src/form-token-field/test/lib/token-field-wrapper.tsx +0 -71
  767. package/src/menu-group/test/__snapshots__/index.js.snap +0 -23
  768. package/src/placeholder/test/index.js +0 -163
  769. package/src/swatch/index.tsx +0 -22
  770. package/src/swatch/style.scss +0 -21
  771. package/src/swatch/types.ts +0 -11
  772. package/src/toggle-group-control/stories/index.js +0 -203
  773. package/src/tools-panel/test/__snapshots__/index.js.snap +0 -210
@@ -0,0 +1,2106 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import {
5
+ render,
6
+ screen,
7
+ within,
8
+ getDefaultNormalizer,
9
+ waitFor,
10
+ } from '@testing-library/react';
11
+ import userEvent from '@testing-library/user-event';
12
+ import type { ComponentProps } from 'react';
13
+
14
+ /**
15
+ * WordPress dependencies
16
+ */
17
+ import { useState } from '@wordpress/element';
18
+
19
+ /**
20
+ * Internal dependencies
21
+ */
22
+ import FormTokenField from '../';
23
+
24
+ const FormTokenFieldWithState = ( {
25
+ onChange,
26
+ value,
27
+ initialValue = [],
28
+ ...props
29
+ }: ComponentProps< typeof FormTokenField > & {
30
+ initialValue?: ComponentProps< typeof FormTokenField >[ 'value' ];
31
+ } ) => {
32
+ const [ selectedValue, setSelectedValue ] =
33
+ useState< ComponentProps< typeof FormTokenField >[ 'value' ] >(
34
+ initialValue
35
+ );
36
+
37
+ return (
38
+ <FormTokenField
39
+ { ...props }
40
+ value={ selectedValue }
41
+ onChange={ ( tokens ) => {
42
+ setSelectedValue( tokens );
43
+ onChange?.( tokens );
44
+ } }
45
+ />
46
+ );
47
+ };
48
+
49
+ const expectTokensToBeInTheDocument = ( tokensText: string[] ) => {
50
+ tokensText.forEach( ( tokenText, tokenIndex, tokensArray ) => {
51
+ // Each token has 2 tags rendered in the DOM:
52
+ // - one with the format "takenName (X of Y)", which is visibly hidden,
53
+ // and is used for assistive technology;
54
+ // - one with the format "tokenName", which is visible but hidden to
55
+ // assistive technology.
56
+ const assistiveTechnologyToken = screen.getByText(
57
+ `${ tokenText } (${ tokenIndex + 1 } of ${ tokensArray.length })`,
58
+ {
59
+ normalizer: getDefaultNormalizer( {
60
+ collapseWhitespace: false,
61
+ trim: false,
62
+ } ),
63
+ }
64
+ );
65
+ // The "exact" flag is necessary in order no to match the element
66
+ // used for assistive technology.
67
+ const visibleToken = screen.getByText( tokenText, {
68
+ exact: true,
69
+ normalizer: getDefaultNormalizer( {
70
+ collapseWhitespace: false,
71
+ trim: false,
72
+ } ),
73
+ } );
74
+
75
+ expect( assistiveTechnologyToken ).toBeInTheDocument();
76
+ expect( visibleToken ).toBeVisible();
77
+ expect( visibleToken ).toHaveAttribute( 'aria-hidden', 'true' );
78
+ } );
79
+ };
80
+ const expectTokensNotToBeInTheDocument = ( tokensText: string[] ) => {
81
+ tokensText.forEach( ( tokenText ) =>
82
+ expect( screen.queryByText( tokenText ) ).not.toBeInTheDocument()
83
+ );
84
+ };
85
+
86
+ const expectVisibleSuggestionsToBe = (
87
+ listElement: HTMLElement,
88
+ suggestionsText: string[]
89
+ ) => {
90
+ const allVisibleOptions = within( listElement ).queryAllByRole( 'option' );
91
+
92
+ expect( allVisibleOptions ).toHaveLength( suggestionsText.length );
93
+
94
+ allVisibleOptions.forEach( ( matchedOption, index ) => {
95
+ expect( matchedOption ).toHaveAccessibleName(
96
+ suggestionsText[ index ]
97
+ );
98
+ } );
99
+ };
100
+
101
+ function unescapeAndFormatSpaces( str: string ) {
102
+ const nbsp = String.fromCharCode( 160 );
103
+ const escaped = new DOMParser().parseFromString( str, 'text/html' );
104
+ return escaped.documentElement.textContent?.replace( / /g, nbsp ) ?? '';
105
+ }
106
+
107
+ describe( 'FormTokenField', () => {
108
+ describe( 'basic usage', () => {
109
+ it( "should add tokens with the input's value when pressing the enter key", async () => {
110
+ const user = userEvent.setup( {
111
+ advanceTimers: jest.advanceTimersByTime,
112
+ } );
113
+
114
+ const onChangeSpy = jest.fn();
115
+
116
+ render( <FormTokenFieldWithState onChange={ onChangeSpy } /> );
117
+
118
+ const input = screen.getByRole( 'combobox' );
119
+
120
+ // Add 'apple' token by typing it and pressing enter to tokenize it.
121
+ await user.type( input, 'apple[Enter]' );
122
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
123
+ expect( onChangeSpy ).toHaveBeenCalledWith( [ 'apple' ] );
124
+ expectTokensToBeInTheDocument( [ 'apple' ] );
125
+
126
+ // Add 'pear' token by typing it and pressing enter to tokenize it.
127
+ await user.type( input, 'pear[Enter]' );
128
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
129
+ expect( onChangeSpy ).toHaveBeenLastCalledWith( [
130
+ 'apple',
131
+ 'pear',
132
+ ] );
133
+ expectTokensToBeInTheDocument( [ 'apple', 'pear' ] );
134
+ } );
135
+
136
+ it( "should add a token with the input's value when pressing the comma key", async () => {
137
+ const user = userEvent.setup( {
138
+ advanceTimers: jest.advanceTimersByTime,
139
+ } );
140
+
141
+ const onChangeSpy = jest.fn();
142
+
143
+ render( <FormTokenFieldWithState onChange={ onChangeSpy } /> );
144
+
145
+ const input = screen.getByRole( 'combobox' );
146
+
147
+ // Add 'orange' token by typing it and pressing enter to tokenize it.
148
+ await user.type( input, 'orange,' );
149
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
150
+ expect( onChangeSpy ).toHaveBeenCalledWith( [ 'orange' ] );
151
+ expectTokensToBeInTheDocument( [ 'orange' ] );
152
+ } );
153
+
154
+ it( 'should add a token with the input value when pressing the space key and the `tokenizeOnSpace` prop is `true`', async () => {
155
+ const user = userEvent.setup( {
156
+ advanceTimers: jest.advanceTimersByTime,
157
+ } );
158
+
159
+ const onChangeSpy = jest.fn();
160
+
161
+ const { rerender } = render(
162
+ <FormTokenFieldWithState onChange={ onChangeSpy } />
163
+ );
164
+
165
+ const input = screen.getByRole( 'combobox' );
166
+
167
+ // Add 'dragon fruit' token by typing it and pressing enter to tokenize it.
168
+ await user.type( input, 'dragon fruit[Enter]' );
169
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
170
+ expect( onChangeSpy ).toHaveBeenCalledWith( [ 'dragon fruit' ] );
171
+ expectTokensToBeInTheDocument( [ 'dragon fruit' ] );
172
+
173
+ rerender(
174
+ <FormTokenFieldWithState
175
+ onChange={ onChangeSpy }
176
+ tokenizeOnSpace
177
+ />
178
+ );
179
+
180
+ // Add 'dragon fruit' token by typing it and pressing enter to tokenize it,
181
+ // this time two separate tokens should be added
182
+ await user.type( input, 'dragon fruit[Enter]' );
183
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
184
+ expect( onChangeSpy ).toHaveBeenNthCalledWith( 2, [
185
+ 'dragon fruit',
186
+ 'dragon',
187
+ ] );
188
+ expect( onChangeSpy ).toHaveBeenNthCalledWith( 3, [
189
+ 'dragon fruit',
190
+ 'dragon',
191
+ 'fruit',
192
+ ] );
193
+ expectTokensToBeInTheDocument( [
194
+ 'dragon fruit',
195
+ 'dragon',
196
+ 'fruit',
197
+ ] );
198
+ } );
199
+
200
+ it( "should not add a token with the input's value when pressing the tab key", async () => {
201
+ const user = userEvent.setup( {
202
+ advanceTimers: jest.advanceTimersByTime,
203
+ } );
204
+
205
+ const onChangeSpy = jest.fn();
206
+
207
+ render( <FormTokenFieldWithState onChange={ onChangeSpy } /> );
208
+
209
+ const input = screen.getByRole( 'combobox' );
210
+
211
+ // Add 'orange' token by typing it and pressing enter to tokenize it.
212
+ await user.type( input, 'grapefruit' );
213
+ await user.tab();
214
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 0 );
215
+ expectTokensNotToBeInTheDocument( [ 'grapefruit' ] );
216
+ } );
217
+
218
+ it( 'should remove the last token when pressing the backspace key', async () => {
219
+ const user = userEvent.setup( {
220
+ advanceTimers: jest.advanceTimersByTime,
221
+ } );
222
+
223
+ const onChangeSpy = jest.fn();
224
+
225
+ render(
226
+ <FormTokenFieldWithState
227
+ onChange={ onChangeSpy }
228
+ initialValue={ [ 'banana', 'mango' ] }
229
+ />
230
+ );
231
+
232
+ const input = screen.getByRole( 'combobox' );
233
+
234
+ // Press backspace to remove the last token ("mango")
235
+ await user.type( input, '[Backspace]' );
236
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
237
+ expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'banana' ] );
238
+ expectTokensToBeInTheDocument( [ 'banana' ] );
239
+ expectTokensNotToBeInTheDocument( [ 'mango' ] );
240
+
241
+ // Press backspace to remove the last token ("banana")
242
+ await user.type( input, '[Backspace]' );
243
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
244
+ expect( onChangeSpy ).toHaveBeenLastCalledWith( [] );
245
+ expectTokensNotToBeInTheDocument( [ 'banana', 'mango' ] );
246
+ } );
247
+
248
+ it( 'should remove a token when clicking the token\'s "remove" button', async () => {
249
+ const user = userEvent.setup( {
250
+ advanceTimers: jest.advanceTimersByTime,
251
+ } );
252
+
253
+ const onChangeSpy = jest.fn();
254
+
255
+ render(
256
+ <FormTokenFieldWithState
257
+ initialValue={ [ 'lemon', 'bergamot' ] }
258
+ onChange={ onChangeSpy }
259
+ />
260
+ );
261
+
262
+ expectTokensToBeInTheDocument( [ 'lemon', 'bergamot' ] );
263
+
264
+ // There should be 2 "remove item" buttons, one per token
265
+ expect(
266
+ screen.getAllByRole( 'button', { name: 'Remove item' } )
267
+ ).toHaveLength( 2 );
268
+
269
+ // Click the "X" button for the "lemon" token (the first one)
270
+ await user.click(
271
+ screen.getAllByRole( 'button', { name: 'Remove item' } )[ 0 ]
272
+ );
273
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
274
+ expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'bergamot' ] );
275
+ expectTokensToBeInTheDocument( [ 'bergamot' ] );
276
+ expectTokensNotToBeInTheDocument( [ 'lemon' ] );
277
+
278
+ // There should be 1 "remove item" button for the "bergamot" token
279
+ expect(
280
+ screen.getAllByRole( 'button', { name: 'Remove item' } )
281
+ ).toHaveLength( 1 );
282
+
283
+ // Click the "X" button for the "bergamot" token (the only one)
284
+ await user.click(
285
+ screen.getAllByRole( 'button', { name: 'Remove item' } )[ 0 ]
286
+ );
287
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
288
+ expect( onChangeSpy ).toHaveBeenLastCalledWith( [] );
289
+ expectTokensNotToBeInTheDocument( [ 'lemon', 'bergamot' ] );
290
+ } );
291
+
292
+ it( 'should remove a token when by focusing on the token\'s "remove" button and pressing space bar', async () => {
293
+ const user = userEvent.setup( {
294
+ advanceTimers: jest.advanceTimersByTime,
295
+ } );
296
+
297
+ const onChangeSpy = jest.fn();
298
+
299
+ render(
300
+ <FormTokenFieldWithState
301
+ onChange={ onChangeSpy }
302
+ initialValue={ [ 'persimmon', 'plum' ] }
303
+ />
304
+ );
305
+
306
+ const input = screen.getByRole( 'combobox' );
307
+ await user.click( input );
308
+
309
+ // Currently the focus in on the input. Pressing shift+tab twice should
310
+ // move focus on the "remove item" button of the first token ("persimmon")
311
+ await user.tab( { shift: true } );
312
+ await user.tab( { shift: true } );
313
+
314
+ expect(
315
+ screen.getAllByRole( 'button', { name: 'Remove item' } )
316
+ ).toHaveLength( 2 );
317
+ expect(
318
+ screen.getAllByRole( 'button', { name: 'Remove item' } )[ 0 ]
319
+ ).toHaveFocus();
320
+
321
+ // Pressing the "space" key on the button should remove the "persimmon" item
322
+ await user.keyboard( '[Space]' );
323
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
324
+ expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'plum' ] );
325
+ expectTokensToBeInTheDocument( [ 'plum' ] );
326
+ expectTokensNotToBeInTheDocument( [ 'persimmon' ] );
327
+ } );
328
+
329
+ it( 'should not add a new token if a token with the same value already exists', async () => {
330
+ const user = userEvent.setup( {
331
+ advanceTimers: jest.advanceTimersByTime,
332
+ } );
333
+
334
+ const onChangeSpy = jest.fn();
335
+
336
+ render(
337
+ <FormTokenFieldWithState
338
+ initialValue={ [ 'papaya' ] }
339
+ onChange={ onChangeSpy }
340
+ />
341
+ );
342
+
343
+ const input = screen.getByRole( 'combobox' );
344
+
345
+ // Add 'guava' token by typing it and pressing enter to tokenize it.
346
+ await user.type( input, 'guava[Enter]' );
347
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
348
+ expect( onChangeSpy ).toHaveBeenCalledWith( [ 'papaya', 'guava' ] );
349
+ expectTokensToBeInTheDocument( [ 'papaya', 'guava' ] );
350
+
351
+ // Try to add a 'papaya' token by typing it and pressing enter to tokenize it,
352
+ // but the token won't be added because it already exists.
353
+ await user.type( input, 'papaya[Enter]' );
354
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
355
+ expectTokensToBeInTheDocument( [ 'papaya', 'guava' ] );
356
+ } );
357
+
358
+ it( 'should not add a new token if the text input is blank', async () => {
359
+ const user = userEvent.setup( {
360
+ advanceTimers: jest.advanceTimersByTime,
361
+ } );
362
+
363
+ const onChangeSpy = jest.fn();
364
+
365
+ render(
366
+ <FormTokenFieldWithState
367
+ initialValue={ [ 'melon' ] }
368
+ onChange={ onChangeSpy }
369
+ />
370
+ );
371
+
372
+ const input = screen.getByRole( 'combobox' );
373
+
374
+ // Press enter on an empty input, no token gets added
375
+ await user.type( input, '[Enter]' );
376
+ expect( onChangeSpy ).not.toHaveBeenCalled();
377
+ expectTokensToBeInTheDocument( [ 'melon' ] );
378
+ } );
379
+
380
+ it( 'should allow moving the cursor through the tokens when pressing the arrow keys, and should remove the token in front of the cursor when pressing the delete key', async () => {
381
+ const user = userEvent.setup( {
382
+ advanceTimers: jest.advanceTimersByTime,
383
+ } );
384
+
385
+ const onChangeSpy = jest.fn();
386
+
387
+ render(
388
+ <FormTokenFieldWithState
389
+ initialValue={ [ 'kiwi', 'peach', 'nectarine', 'coconut' ] }
390
+ onChange={ onChangeSpy }
391
+ />
392
+ );
393
+
394
+ expectTokensToBeInTheDocument( [
395
+ 'kiwi',
396
+ 'peach',
397
+ 'nectarine',
398
+ 'coconut',
399
+ ] );
400
+
401
+ const input = screen.getByRole( 'combobox' );
402
+
403
+ // Press "delete" to delete the token in front of the cursor, but since
404
+ // there's no token in front of the cursor, nothing happens
405
+ await user.type( input, '[Delete]' );
406
+
407
+ // Pressing the right arrow doesn't move the cursor because there are no
408
+ // tokens in front of it, and therefore pressing "delete" yields the same
409
+ // result as before — no tokens are deleted.
410
+ await user.type( input, '[ArrowRight][Delete]' );
411
+
412
+ // Proof that so far, all keyboard interactions didn't delete any tokens.
413
+ expect( onChangeSpy ).not.toHaveBeenCalled();
414
+ expectTokensToBeInTheDocument( [
415
+ 'kiwi',
416
+ 'peach',
417
+ 'nectarine',
418
+ 'coconut',
419
+ ] );
420
+
421
+ // Press the left arrow 4 times, moving cursor between the "kiwi" and
422
+ // "peach" tokens. Pressing the "delete" key will delete the "peach"
423
+ // token, since it's in front of the cursor.
424
+ await user.type(
425
+ input,
426
+ '[ArrowLeft][ArrowLeft][ArrowLeft][ArrowLeft][Delete]'
427
+ );
428
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
429
+ expect( onChangeSpy ).toHaveBeenCalledWith( [
430
+ 'peach',
431
+ 'nectarine',
432
+ 'coconut',
433
+ ] );
434
+ expectTokensToBeInTheDocument( [
435
+ 'peach',
436
+ 'nectarine',
437
+ 'coconut',
438
+ ] );
439
+ expectTokensNotToBeInTheDocument( [ 'kiwi' ] );
440
+
441
+ // Press backspace to delete the token before the cursor, but since
442
+ // there's no token before the cursor, nothing happens
443
+ await user.type( input, '[Backspace]' );
444
+
445
+ // Pressing the left arrow doesn't move the cursor because there are no
446
+ // tokens before it, and therefore pressing backspace yields the same
447
+ // result as before — no tokens are deleted.
448
+ await user.type( input, '[ArrowLeft][Backspace]' );
449
+
450
+ // Proof that pressing backspace hasn't caused any further token deletion.
451
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
452
+
453
+ // Press the right arrow, moving cursor between the "kiwi" and
454
+ // "nectarine" tokens. Pressing the "delete" key will delete the "nectarine"
455
+ // token, since it's in front of the cursor.
456
+ await user.type( input, '[ArrowRight][Delete]' );
457
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
458
+ expect( onChangeSpy ).toHaveBeenCalledWith( [
459
+ 'peach',
460
+ 'coconut',
461
+ ] );
462
+ expectTokensToBeInTheDocument( [ 'peach', 'coconut' ] );
463
+ expectTokensNotToBeInTheDocument( [ 'kiwi', 'nectarine' ] );
464
+
465
+ // Add 'starfruit' token while the cursor is in between the "peach" and
466
+ // "coconut" tokens.
467
+ await user.type( input, 'starfruit[Enter]' );
468
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
469
+ expect( onChangeSpy ).toHaveBeenCalledWith( [
470
+ 'peach',
471
+ // Notice that starfruit is added in between "peach" and "coconut"
472
+ 'starfruit',
473
+ 'coconut',
474
+ ] );
475
+ expectTokensToBeInTheDocument( [
476
+ 'peach',
477
+ 'starfruit',
478
+ 'coconut',
479
+ ] );
480
+ } );
481
+
482
+ it( "should add additional classnames passed via the `className` prop to the input element's 2nd level wrapper", () => {
483
+ render( <FormTokenFieldWithState className="test-classname" /> );
484
+
485
+ const input = screen.getByRole( 'combobox' );
486
+
487
+ // This is testing implementation details, but I'm not sure there's
488
+ // a better way.
489
+ expect( input.parentElement?.parentElement ).toHaveClass(
490
+ 'test-classname'
491
+ );
492
+ } );
493
+
494
+ it( 'should label the input correctly via the `label` prop', () => {
495
+ const { rerender } = render( <FormTokenFieldWithState /> );
496
+
497
+ expect(
498
+ screen.getByRole( 'combobox', { name: 'Add item' } )
499
+ ).toBeVisible();
500
+
501
+ rerender( <FormTokenFieldWithState label="Test label" /> );
502
+
503
+ expect(
504
+ screen.getByRole( 'combobox', { name: 'Test label' } )
505
+ ).toBeVisible();
506
+ } );
507
+
508
+ it( 'should fire the `onFocus` callback when the input is focused', async () => {
509
+ const user = userEvent.setup( {
510
+ advanceTimers: jest.advanceTimersByTime,
511
+ } );
512
+
513
+ const onFocusSpy = jest.fn();
514
+
515
+ render( <FormTokenFieldWithState onFocus={ onFocusSpy } /> );
516
+
517
+ const input = screen.getByRole( 'combobox' );
518
+
519
+ await user.click( input );
520
+
521
+ expect( onFocusSpy ).toHaveBeenCalledTimes( 1 );
522
+ expect( onFocusSpy ).toHaveBeenCalledWith(
523
+ expect.objectContaining( {
524
+ type: 'focus',
525
+ target: input,
526
+ } )
527
+ );
528
+
529
+ expect( input ).toHaveFocus();
530
+ } );
531
+
532
+ it( "should fire the `onInputChange` callback when the input's value changes", async () => {
533
+ const user = userEvent.setup( {
534
+ advanceTimers: jest.advanceTimersByTime,
535
+ } );
536
+
537
+ const onInputChangeSpy = jest.fn();
538
+
539
+ render(
540
+ <FormTokenFieldWithState onInputChange={ onInputChangeSpy } />
541
+ );
542
+
543
+ const input = screen.getByRole( 'combobox' );
544
+
545
+ await user.type( input, 'strawberry[Enter]' );
546
+
547
+ expect( onInputChangeSpy ).toHaveBeenCalledTimes(
548
+ 'strawberry'.length
549
+ );
550
+ expect( onInputChangeSpy ).toHaveBeenNthCalledWith(
551
+ 5,
552
+ 'strawberry'.slice( 0, 5 )
553
+ );
554
+ } );
555
+
556
+ it( 'should show extra instructions when the `__experimentalShowHowTo` prop is set to `true`', () => {
557
+ const instructionsTokenizeSpace =
558
+ 'Separate with commas, spaces, or the Enter key.';
559
+ const instructionsDefault =
560
+ 'Separate with commas or the Enter key.';
561
+
562
+ // The __experimentalShowHowTo prop is `true` by default
563
+ const { rerender } = render( <FormTokenFieldWithState /> );
564
+
565
+ expect( screen.getByText( instructionsDefault ) ).toBeVisible();
566
+
567
+ // The "show how to" text is used to aria-describedby the input
568
+ expect(
569
+ screen.getByRole( 'combobox' )
570
+ ).toHaveAccessibleDescription( instructionsDefault );
571
+
572
+ rerender( <FormTokenFieldWithState tokenizeOnSpace /> );
573
+
574
+ expect(
575
+ screen.getByText( instructionsTokenizeSpace )
576
+ ).toBeVisible();
577
+
578
+ // The "show how to" text is used to aria-describedby the input
579
+ expect(
580
+ screen.getByRole( 'combobox' )
581
+ ).toHaveAccessibleDescription( instructionsTokenizeSpace );
582
+
583
+ rerender(
584
+ <FormTokenFieldWithState
585
+ tokenizeOnSpace
586
+ __experimentalShowHowTo={ false }
587
+ />
588
+ );
589
+
590
+ expect(
591
+ screen.queryByText( instructionsDefault )
592
+ ).not.toBeInTheDocument();
593
+ expect(
594
+ screen.queryByText( instructionsTokenizeSpace )
595
+ ).not.toBeInTheDocument();
596
+ expect(
597
+ screen.getByRole( 'combobox' )
598
+ ).not.toHaveAccessibleDescription();
599
+ } );
600
+
601
+ it( "should use the value of the `placeholder` prop as the input's placeholder only when there are no tokens", async () => {
602
+ const user = userEvent.setup( {
603
+ advanceTimers: jest.advanceTimersByTime,
604
+ } );
605
+
606
+ const onChangeSpy = jest.fn();
607
+
608
+ render(
609
+ <FormTokenFieldWithState
610
+ onChange={ onChangeSpy }
611
+ placeholder="Test placeholder"
612
+ />
613
+ );
614
+
615
+ expect(
616
+ screen.getByPlaceholderText( 'Test placeholder' )
617
+ ).toBeVisible();
618
+
619
+ const input = screen.getByRole( 'combobox' );
620
+
621
+ // Add 'blueberry' token. The placeholder text should not be shown anymore
622
+ await user.type( input, 'blueberry[Enter]' );
623
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
624
+ expect( onChangeSpy ).toHaveBeenCalledWith( [ 'blueberry' ] );
625
+ expectTokensToBeInTheDocument( [ 'blueberry' ] );
626
+
627
+ expect(
628
+ screen.queryByPlaceholderText( 'Test placeholder' )
629
+ ).not.toBeInTheDocument();
630
+ } );
631
+
632
+ it( 'should handle accents and special characters in tokens and input value', async () => {
633
+ const user = userEvent.setup( {
634
+ advanceTimers: jest.advanceTimersByTime,
635
+ } );
636
+
637
+ const onChangeSpy = jest.fn();
638
+
639
+ render(
640
+ <FormTokenFieldWithState
641
+ onChange={ onChangeSpy }
642
+ initialValue={ [ 'français', 'español', '日本', 'עברית' ] }
643
+ />
644
+ );
645
+
646
+ const input = screen.getByRole( 'combobox' );
647
+
648
+ // Add 'عربى' token by typing it and pressing enter to tokenize it.
649
+ await user.type( input, 'عربى[Enter]' );
650
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
651
+ expect( onChangeSpy ).toHaveBeenCalledWith( [
652
+ 'français',
653
+ 'español',
654
+ '日本',
655
+ 'עברית',
656
+ 'عربى',
657
+ ] );
658
+ expectTokensToBeInTheDocument( [
659
+ 'français',
660
+ 'español',
661
+ '日本',
662
+ 'עברית',
663
+ 'عربى',
664
+ ] );
665
+ } );
666
+ } );
667
+
668
+ describe( 'suggestions', () => {
669
+ it( 'should not render suggestions in its default state', () => {
670
+ render(
671
+ <FormTokenFieldWithState
672
+ suggestions={ [ 'Red', 'Magenta', 'Vermilion' ] }
673
+ />
674
+ );
675
+
676
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
677
+ } );
678
+
679
+ it( 'should render suggestions when receiving focus if the `__experimentalExpandOnFocus` prop is set to `true`', async () => {
680
+ const user = userEvent.setup( {
681
+ advanceTimers: jest.advanceTimersByTime,
682
+ } );
683
+
684
+ const onFocusSpy = jest.fn();
685
+
686
+ const suggestions = [ 'Cobalt', 'Blue', 'Octane' ];
687
+
688
+ render(
689
+ <>
690
+ <FormTokenFieldWithState
691
+ onFocus={ onFocusSpy }
692
+ suggestions={ suggestions }
693
+ __experimentalExpandOnFocus
694
+ />
695
+ </>
696
+ );
697
+
698
+ const input = screen.getByRole( 'combobox' );
699
+
700
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
701
+
702
+ // Click the input, focusing it.
703
+ await user.click( input );
704
+
705
+ const suggestionList = screen.getByRole( 'listbox' );
706
+
707
+ expect( onFocusSpy ).toHaveBeenCalledTimes( 1 );
708
+ expect( suggestionList ).toBeVisible();
709
+
710
+ expectVisibleSuggestionsToBe(
711
+ screen.getByRole( 'listbox' ),
712
+ suggestions
713
+ );
714
+
715
+ // Minimum length limitations don't affect the search text when the
716
+ // `__experimentalExpandOnFocus` is `true`
717
+ await user.keyboard( 'c' );
718
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
719
+ 'Cobalt',
720
+ 'Octane',
721
+ ] );
722
+ } );
723
+
724
+ it( 'should not render suggestions if the text input is not matching any of the suggestions', async () => {
725
+ const user = userEvent.setup( {
726
+ advanceTimers: jest.advanceTimersByTime,
727
+ } );
728
+
729
+ const suggestions = [ 'White', 'Pearl', 'Alabaster' ];
730
+
731
+ render( <FormTokenFieldWithState suggestions={ suggestions } /> );
732
+
733
+ const input = screen.getByRole( 'combobox' );
734
+
735
+ // Type 'Snow' which doesn't match any of the suggestions
736
+ await user.type( input, 'Snow' );
737
+
738
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
739
+ } );
740
+
741
+ it( 'should render the matching suggestions only if the text input has the minimum length', async () => {
742
+ const user = userEvent.setup( {
743
+ advanceTimers: jest.advanceTimersByTime,
744
+ } );
745
+
746
+ const suggestions = [ 'Yellow', 'Canary', 'Gold', 'Blonde' ];
747
+
748
+ render( <FormTokenFieldWithState suggestions={ suggestions } /> );
749
+
750
+ const input = screen.getByRole( 'combobox' );
751
+
752
+ // Despite 'l' matches some suggestions, the search text needs to be
753
+ // at least 2 characters
754
+ await user.type( input, ' l ' );
755
+
756
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
757
+
758
+ // The trimmed search text is now 2 characters long (`lo`), which is
759
+ // enough to show matching suggestions ('Yellow' and 'Blonde')
760
+ await user.type( input, '[ArrowLeft][ArrowLeft][ArrowLeft]o' );
761
+
762
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
763
+ 'Yellow',
764
+ 'Blonde',
765
+ ] );
766
+ } );
767
+
768
+ it( 'should not render a matching suggestion if a token with the same value has already been added', async () => {
769
+ const user = userEvent.setup( {
770
+ advanceTimers: jest.advanceTimersByTime,
771
+ } );
772
+
773
+ const suggestions = [ 'Green', 'Emerald', 'Seaweed' ];
774
+
775
+ render(
776
+ <FormTokenFieldWithState
777
+ suggestions={ suggestions }
778
+ initialValue={ [ 'Green' ] }
779
+ />
780
+ );
781
+
782
+ const input = screen.getByRole( 'combobox' );
783
+
784
+ // Despite 'ee' matches both the "Green" and "Seaweed", "Green" won't be
785
+ // displayed because there's already a token with the same value
786
+ await user.type( input, 'ee' );
787
+
788
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
789
+ 'Seaweed',
790
+ ] );
791
+ } );
792
+
793
+ it( 'should allow the user to use the keyboard to navigate and select suggestions (which are marked with the `aria-selected` attribute)', async () => {
794
+ const user = userEvent.setup( {
795
+ advanceTimers: jest.advanceTimersByTime,
796
+ } );
797
+
798
+ const onChangeSpy = jest.fn();
799
+
800
+ const suggestions = [
801
+ 'Pink',
802
+ 'Salmon',
803
+ 'Flamingo',
804
+ 'Carnation',
805
+ 'Neon',
806
+ ];
807
+
808
+ render(
809
+ <FormTokenFieldWithState
810
+ onChange={ onChangeSpy }
811
+ suggestions={ suggestions }
812
+ />
813
+ );
814
+
815
+ const input = screen.getByRole( 'combobox' );
816
+
817
+ // Typing "on" will show the "Salmon", "Carnation" and "Neon" suggestions
818
+ await user.type( input, 'on' );
819
+
820
+ const suggestionList = screen.getByRole( 'listbox' );
821
+
822
+ expectVisibleSuggestionsToBe( suggestionList, [
823
+ 'Salmon',
824
+ 'Carnation',
825
+ 'Neon',
826
+ ] );
827
+
828
+ // Currently, none of the suggestions are selected
829
+ expect(
830
+ within( suggestionList ).queryAllByRole( 'option', {
831
+ selected: true,
832
+ } )
833
+ ).toHaveLength( 0 );
834
+
835
+ // Pressing the down arrow will select "Salmon"
836
+ await user.keyboard( '[ArrowDown]' );
837
+
838
+ expect(
839
+ within( suggestionList ).getByRole( 'option', {
840
+ selected: true,
841
+ } )
842
+ ).toHaveAccessibleName( 'Salmon' );
843
+
844
+ // Pressing the up arrow will select "Neon" (the selection wraps around
845
+ // the list)
846
+ await user.keyboard( '[ArrowUp]' );
847
+
848
+ expect(
849
+ within( suggestionList ).getByRole( 'option', {
850
+ selected: true,
851
+ } )
852
+ ).toHaveAccessibleName( 'Neon' );
853
+
854
+ // Pressing the down arrow twice will select "Carnation" (the selection
855
+ // wraps around the list)
856
+ await user.keyboard( '[ArrowDown][ArrowDown]' );
857
+
858
+ expect(
859
+ within( suggestionList ).getByRole( 'option', {
860
+ selected: true,
861
+ } )
862
+ ).toHaveAccessibleName( 'Carnation' );
863
+
864
+ // Pressing enter will add "Carnation" as a token and close the suggestion list
865
+ await user.keyboard( '[Enter]' );
866
+
867
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
868
+ expect( onChangeSpy ).toHaveBeenCalledWith( [ 'Carnation' ] );
869
+ expectTokensToBeInTheDocument( [ 'Carnation' ] );
870
+
871
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
872
+ } );
873
+
874
+ it( 'should allow the user to use the mouse to navigate and select suggestions (which are marked with the `aria-selected` attribute)', async () => {
875
+ const user = userEvent.setup( {
876
+ advanceTimers: jest.advanceTimersByTime,
877
+ } );
878
+
879
+ const onChangeSpy = jest.fn();
880
+
881
+ const suggestions = [ 'Tiger', 'Tangerine', 'Orange' ];
882
+
883
+ render(
884
+ <FormTokenFieldWithState
885
+ onChange={ onChangeSpy }
886
+ suggestions={ suggestions }
887
+ />
888
+ );
889
+
890
+ const input = screen.getByRole( 'combobox' );
891
+
892
+ // Typing "er" will show the "Tiger" and "Tangerine" suggestions
893
+ await user.type( input, 'er' );
894
+
895
+ const suggestionList = screen.getByRole( 'listbox' );
896
+ expectVisibleSuggestionsToBe( suggestionList, [
897
+ 'Tiger',
898
+ 'Tangerine',
899
+ ] );
900
+
901
+ // Currently, none of the suggestions are selected
902
+ expect(
903
+ within( suggestionList ).queryAllByRole( 'option', {
904
+ selected: true,
905
+ } )
906
+ ).toHaveLength( 0 );
907
+
908
+ const tigerOption = within( suggestionList ).getByRole( 'option', {
909
+ name: 'Tiger',
910
+ } );
911
+ const tangerineOption = within( suggestionList ).getByRole(
912
+ 'option',
913
+ {
914
+ name: 'Tangerine',
915
+ }
916
+ );
917
+
918
+ // Hovering over each option will mark it as selected (via the
919
+ // `aria-selected` attribute)
920
+ await user.hover( tigerOption );
921
+
922
+ expect( tigerOption ).toHaveAttribute( 'aria-selected', 'true' );
923
+ expect( tangerineOption ).toHaveAttribute(
924
+ 'aria-selected',
925
+ 'false'
926
+ );
927
+
928
+ await user.hover( tangerineOption );
929
+
930
+ expect( tigerOption ).toHaveAttribute( 'aria-selected', 'false' );
931
+ expect( tangerineOption ).toHaveAttribute(
932
+ 'aria-selected',
933
+ 'true'
934
+ );
935
+
936
+ // Clicking an option will add it as a token and close the list
937
+ await user.click( tangerineOption );
938
+
939
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
940
+ expect( onChangeSpy ).toHaveBeenCalledWith( [ 'Tangerine' ] );
941
+ expectTokensToBeInTheDocument( [ 'Tangerine' ] );
942
+
943
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
944
+ } );
945
+
946
+ it( 'should hide the suggestion list when the Escape key is pressed', async () => {
947
+ const user = userEvent.setup( {
948
+ advanceTimers: jest.advanceTimersByTime,
949
+ } );
950
+
951
+ const onChangeSpy = jest.fn();
952
+
953
+ const suggestions = [ 'Black', 'Ash', 'Onyx', 'Ebony' ];
954
+
955
+ render(
956
+ <FormTokenFieldWithState
957
+ onChange={ onChangeSpy }
958
+ suggestions={ suggestions }
959
+ />
960
+ );
961
+
962
+ const input = screen.getByRole( 'combobox' );
963
+
964
+ // Typing "ony" will show the "Onyx" and "Ebony" suggestions
965
+ await user.type( input, 'ony' );
966
+
967
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
968
+ 'Onyx',
969
+ 'Ebony',
970
+ ] );
971
+
972
+ expect( screen.getByRole( 'listbox' ) ).toBeVisible();
973
+
974
+ // Pressing the ESC key will close the suggestion list
975
+ await user.keyboard( '[Escape]' );
976
+
977
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
978
+ expect( onChangeSpy ).not.toHaveBeenCalled();
979
+ } );
980
+
981
+ it( 'matches the search text with the suggestions in a case-insensitive way', async () => {
982
+ const user = userEvent.setup( {
983
+ advanceTimers: jest.advanceTimersByTime,
984
+ } );
985
+
986
+ const suggestions = [ 'Cinnamon', 'Tawny', 'Mocha' ];
987
+
988
+ render( <FormTokenFieldWithState suggestions={ suggestions } /> );
989
+
990
+ const input = screen.getByRole( 'combobox' );
991
+
992
+ // Because text-matching is case-insensitive, "mo" matches both
993
+ // "Mocha" and "Cinnamon"
994
+ await user.type( input, 'mo' );
995
+
996
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
997
+ 'Mocha',
998
+ 'Cinnamon',
999
+ ] );
1000
+ } );
1001
+
1002
+ it( 'should show, at most, a number of suggestions equals to the value of the `maxSuggestions` prop', async () => {
1003
+ const user = userEvent.setup( {
1004
+ advanceTimers: jest.advanceTimersByTime,
1005
+ } );
1006
+
1007
+ const suggestions = [
1008
+ 'Ablaze',
1009
+ 'Ability',
1010
+ 'Abandon',
1011
+ 'Abdomen',
1012
+ 'Abdicate',
1013
+ 'Abortive',
1014
+ 'Abundance',
1015
+ 'Abashedly',
1016
+ 'Abominable',
1017
+ 'Absolutely',
1018
+ 'Absorption',
1019
+ 'Abnormality',
1020
+ ];
1021
+
1022
+ const { rerender } = render(
1023
+ <FormTokenFieldWithState suggestions={ suggestions } />
1024
+ );
1025
+
1026
+ const input = screen.getByRole( 'combobox' );
1027
+
1028
+ // Because text-matching is case-insensitive, "Ab" matches all suggestions
1029
+ await user.type( input, 'Ab' );
1030
+
1031
+ // By default, `maxSuggestions` has a value of 100, which means that
1032
+ // all matching suggestions will be shown.
1033
+ expectVisibleSuggestionsToBe(
1034
+ screen.getByRole( 'listbox' ),
1035
+ suggestions
1036
+ );
1037
+
1038
+ rerender(
1039
+ <FormTokenFieldWithState
1040
+ suggestions={ suggestions }
1041
+ maxSuggestions={ 3 }
1042
+ />
1043
+ );
1044
+
1045
+ // Only the first 3 suggestions are shown, as per the `maxSuggestions` prop
1046
+ expectVisibleSuggestionsToBe(
1047
+ screen.getByRole( 'listbox' ),
1048
+ suggestions.slice( 0, 3 )
1049
+ );
1050
+ } );
1051
+
1052
+ it( 'should match the search text against the unescaped values of suggestions with special characters (including spaces)', async () => {
1053
+ const user = userEvent.setup( {
1054
+ advanceTimers: jest.advanceTimersByTime,
1055
+ } );
1056
+
1057
+ render(
1058
+ <FormTokenFieldWithState
1059
+ displayTransform={ unescapeAndFormatSpaces }
1060
+ suggestions={ [
1061
+ '<3',
1062
+ 'Stuff & Things',
1063
+ 'Tags & Stuff',
1064
+ 'Tags & Stuff 2',
1065
+ ] }
1066
+ />
1067
+ );
1068
+
1069
+ const input = screen.getByRole( 'combobox' );
1070
+
1071
+ // Should match against the escaped value
1072
+ await user.type( input, '& S' );
1073
+
1074
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
1075
+ 'Tags & Stuff',
1076
+ 'Tags & Stuff 2',
1077
+ ] );
1078
+
1079
+ // Should match against the escaped value
1080
+ await user.clear( input );
1081
+ await user.type( input, 's &' );
1082
+
1083
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
1084
+ 'Tags & Stuff',
1085
+ 'Tags & Stuff 2',
1086
+ ] );
1087
+
1088
+ // Should not match against the escaped value
1089
+ await user.clear( input );
1090
+ await user.type( input, 'amp' );
1091
+
1092
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
1093
+ } );
1094
+
1095
+ it( 'should re-render if suggestions change', async () => {
1096
+ const user = userEvent.setup( {
1097
+ advanceTimers: jest.advanceTimersByTime,
1098
+ } );
1099
+
1100
+ const suggestions = [ 'Aluminum', 'Silver', 'Bronze' ];
1101
+
1102
+ const { rerender } = render( <FormTokenFieldWithState /> );
1103
+
1104
+ // Type "sil", but there are no suggestions.
1105
+ await user.type( screen.getByRole( 'combobox' ), 'sil' );
1106
+
1107
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
1108
+
1109
+ // When suggestions change, the "sil" text is matched against the new
1110
+ // suggestions, which results in the "Silver" suggestion being shown.
1111
+ rerender( <FormTokenFieldWithState suggestions={ suggestions } /> );
1112
+
1113
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
1114
+ 'Silver',
1115
+ ] );
1116
+ } );
1117
+
1118
+ it( 'should automatically select the first matching suggestions when the `__experimentalAutoSelectFirstMatch` prop is set to `true`', async () => {
1119
+ const user = userEvent.setup( {
1120
+ advanceTimers: jest.advanceTimersByTime,
1121
+ } );
1122
+
1123
+ const suggestions = [ 'Walnut', 'Hazelnut', 'Pecan' ];
1124
+
1125
+ const { rerender } = render(
1126
+ <FormTokenFieldWithState suggestions={ suggestions } />
1127
+ );
1128
+
1129
+ const input = screen.getByRole( 'combobox' );
1130
+
1131
+ // Type "nut", which will match "Walnut" and "Hazelnut".
1132
+ await user.type( input, 'nut' );
1133
+
1134
+ const suggestionList = screen.getByRole( 'listbox' );
1135
+
1136
+ expectVisibleSuggestionsToBe( suggestionList, [
1137
+ 'Walnut',
1138
+ 'Hazelnut',
1139
+ ] );
1140
+
1141
+ expect(
1142
+ within( suggestionList ).queryByRole( 'option', {
1143
+ selected: true,
1144
+ } )
1145
+ ).not.toBeInTheDocument();
1146
+
1147
+ rerender(
1148
+ <FormTokenFieldWithState
1149
+ __experimentalAutoSelectFirstMatch
1150
+ suggestions={ suggestions }
1151
+ />
1152
+ );
1153
+
1154
+ expect(
1155
+ within( suggestionList ).getByRole( 'option', {
1156
+ selected: true,
1157
+ } )
1158
+ ).toHaveAccessibleName( 'Walnut' );
1159
+ } );
1160
+
1161
+ it( 'should allow to render custom suggestion items via the `__experimentalRenderItem` prop', async () => {
1162
+ const user = userEvent.setup( {
1163
+ advanceTimers: jest.advanceTimersByTime,
1164
+ } );
1165
+
1166
+ const suggestions = [ 'Wood', 'Stone', 'Metal' ];
1167
+
1168
+ render(
1169
+ <FormTokenFieldWithState
1170
+ suggestions={ suggestions }
1171
+ __experimentalRenderItem={ ( { item } ) => (
1172
+ <>Suggestion: { item }</>
1173
+ ) }
1174
+ />
1175
+ );
1176
+
1177
+ // Type "woo". Matching suggestion will be "Wood"
1178
+ await user.type( screen.getByRole( 'combobox' ), 'woo' );
1179
+
1180
+ // The `__experimentalRenderItem` only affects the rendered suggestion,
1181
+ // but doesn't change the underlying data `value`, nor the value
1182
+ // displayed in the added token.
1183
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
1184
+ 'Suggestion: Wood',
1185
+ ] );
1186
+
1187
+ await user.keyboard( '[ArrowDown][Enter]' );
1188
+
1189
+ expectTokensToBeInTheDocument( [ 'Wood' ] );
1190
+ } );
1191
+ } );
1192
+
1193
+ describe( 'tokens as objects', () => {
1194
+ it( 'should accept tokens in their object format', async () => {
1195
+ const user = userEvent.setup( {
1196
+ advanceTimers: jest.advanceTimersByTime,
1197
+ } );
1198
+
1199
+ const onChangeSpy = jest.fn();
1200
+
1201
+ const { rerender } = render(
1202
+ <FormTokenFieldWithState
1203
+ onChange={ onChangeSpy }
1204
+ __experimentalExpandOnFocus
1205
+ initialValue={ [
1206
+ { value: 'Italy' },
1207
+ { value: 'Switzerland' },
1208
+ ] }
1209
+ />
1210
+ );
1211
+
1212
+ expectTokensToBeInTheDocument( [ 'Italy', 'Switzerland' ] );
1213
+
1214
+ const input = screen.getByRole( 'combobox' );
1215
+
1216
+ await user.type( input, 'Italy[Enter]' );
1217
+
1218
+ expect( onChangeSpy ).not.toHaveBeenCalled();
1219
+
1220
+ rerender(
1221
+ <FormTokenFieldWithState
1222
+ onChange={ onChangeSpy }
1223
+ __experimentalExpandOnFocus
1224
+ initialValue={ [
1225
+ { value: 'Italy' },
1226
+ { value: 'Switzerland' },
1227
+ ] }
1228
+ suggestions={ [ 'Italy', 'Switzerland', 'Sweden' ] }
1229
+ />
1230
+ );
1231
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
1232
+ 'Sweden',
1233
+ ] );
1234
+ } );
1235
+
1236
+ it( 'should trigger mouse callbacks if the `onMouseEnter` and/or the `onMouseLeave` properties are set on a token data object', async () => {
1237
+ const user = userEvent.setup( {
1238
+ advanceTimers: jest.advanceTimersByTime,
1239
+ } );
1240
+
1241
+ const onMouseEnterSpy = jest.fn();
1242
+ const onMouseLeaveSpy = jest.fn();
1243
+
1244
+ render(
1245
+ <FormTokenFieldWithState
1246
+ initialValue={ [
1247
+ {
1248
+ value: 'Germany',
1249
+ onMouseEnter: onMouseEnterSpy,
1250
+ onMouseLeave: onMouseLeaveSpy,
1251
+ },
1252
+ { value: 'Liechtenstein' },
1253
+ { value: 'Austria' },
1254
+ ] }
1255
+ />
1256
+ );
1257
+
1258
+ // Move mouse over the 'Germany' token, then over 'Austria', then over
1259
+ // 'Liechtenstein'. The mouse-related callbacks should fire only for
1260
+ // the 'Germany' token, since they are not defined for other tokens.
1261
+ await user.hover( screen.getByText( 'Germany', { exact: true } ) );
1262
+
1263
+ expect( onMouseEnterSpy ).toHaveBeenCalledTimes( 1 );
1264
+ expect( onMouseLeaveSpy ).not.toHaveBeenCalled();
1265
+
1266
+ await user.hover( screen.getByText( 'Austria', { exact: true } ) );
1267
+
1268
+ expect( onMouseEnterSpy ).toHaveBeenCalledTimes( 1 );
1269
+ expect( onMouseLeaveSpy ).toHaveBeenCalledTimes( 1 );
1270
+
1271
+ await user.hover(
1272
+ screen.getByText( 'Liechtenstein', { exact: true } )
1273
+ );
1274
+
1275
+ expect( onMouseEnterSpy ).toHaveBeenCalledTimes( 1 );
1276
+ expect( onMouseLeaveSpy ).toHaveBeenCalledTimes( 1 );
1277
+ } );
1278
+
1279
+ it( 'should add an accessible `title` to a token when specified', () => {
1280
+ render(
1281
+ <FormTokenFieldWithState
1282
+ initialValue={ [
1283
+ { value: 'France' },
1284
+ { value: 'Spain', title: 'España' },
1285
+ ] }
1286
+ />
1287
+ );
1288
+
1289
+ expect( screen.queryByTitle( 'France' ) ).not.toBeInTheDocument();
1290
+ expect( screen.getByTitle( 'España' ) ).toBeVisible();
1291
+ } );
1292
+
1293
+ it( 'should be still used to filter out duplicate suggestions', () => {
1294
+ render(
1295
+ <FormTokenFieldWithState
1296
+ __experimentalExpandOnFocus
1297
+ initialValue={ [ { value: 'France' }, { value: 'Spain' } ] }
1298
+ />
1299
+ );
1300
+ } );
1301
+ } );
1302
+
1303
+ describe( 'saveTransform', () => {
1304
+ it( "by default, it should trim the input's value from extra white spaces before attempting to add it as a token", async () => {
1305
+ const user = userEvent.setup( {
1306
+ advanceTimers: jest.advanceTimersByTime,
1307
+ } );
1308
+
1309
+ const onChangeSpy = jest.fn();
1310
+
1311
+ const { rerender } = render(
1312
+ <FormTokenFieldWithState
1313
+ initialValue={ [ 'potato' ] }
1314
+ onChange={ onChangeSpy }
1315
+ />
1316
+ );
1317
+
1318
+ const input = screen.getByRole( 'combobox' );
1319
+
1320
+ // Press enter on an empty input, no token gets added
1321
+ await user.type( input, '[Enter]' );
1322
+ expect( onChangeSpy ).not.toHaveBeenCalled();
1323
+ expectTokensToBeInTheDocument( [ 'potato' ] );
1324
+
1325
+ // Add the "carrot" token - white space gets trimmed
1326
+ await user.type( input, ' carrot [Enter]' );
1327
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1328
+ expect( onChangeSpy ).toHaveBeenCalledWith( [
1329
+ 'potato',
1330
+ 'carrot',
1331
+ ] );
1332
+ expectTokensToBeInTheDocument( [ 'potato', 'carrot' ] );
1333
+
1334
+ // Press enter on an input containing a duplicate token but surrounded by
1335
+ // white space, no token gets added
1336
+ await user.type( input, ' potato [Enter]' );
1337
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1338
+ expectTokensToBeInTheDocument( [ 'potato', 'carrot' ] );
1339
+
1340
+ // Press enter on an input containing only spaces, no token gets added
1341
+ await user.type( input, ' [Enter]' );
1342
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1343
+ expectTokensToBeInTheDocument( [ 'potato', 'carrot' ] );
1344
+
1345
+ rerender(
1346
+ <FormTokenFieldWithState
1347
+ initialValue={ [ 'potato' ] }
1348
+ onChange={ onChangeSpy }
1349
+ saveTransform={ ( text: string ) => text }
1350
+ />
1351
+ );
1352
+
1353
+ // If a custom `saveTransform` function is passed, it will be the new
1354
+ // function's duty to trim the whitespace if necessary.
1355
+ await user.clear( input );
1356
+ await user.type( input, ' parnsnip [Enter]' );
1357
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
1358
+ expect( onChangeSpy ).toHaveBeenCalledWith( [
1359
+ 'potato',
1360
+ 'carrot',
1361
+ ' parnsnip ',
1362
+ ] );
1363
+ expectTokensToBeInTheDocument( [
1364
+ 'potato',
1365
+ 'carrot',
1366
+ ' parnsnip ',
1367
+ ] );
1368
+ } );
1369
+
1370
+ it( "should allow to modify the input's value when saving it as a token", async () => {
1371
+ const user = userEvent.setup( {
1372
+ advanceTimers: jest.advanceTimersByTime,
1373
+ } );
1374
+
1375
+ const onChangeSpy = jest.fn();
1376
+
1377
+ const { rerender } = render(
1378
+ <FormTokenFieldWithState
1379
+ onFocus={ onChangeSpy }
1380
+ initialValue={ [ 'small trousers', 'small shirt' ] }
1381
+ />
1382
+ );
1383
+
1384
+ expectTokensToBeInTheDocument( [
1385
+ 'small trousers',
1386
+ 'small shirt',
1387
+ ] );
1388
+
1389
+ rerender(
1390
+ <FormTokenFieldWithState
1391
+ onChange={ onChangeSpy }
1392
+ initialValue={ [ 'small trousers', 'small shirt' ] }
1393
+ saveTransform={ ( tokenText: string ) =>
1394
+ tokenText.replace( /small/g, 'medium' )
1395
+ }
1396
+ />
1397
+ );
1398
+
1399
+ // The `saveTransform` prop doesn't apply to existing tokens.
1400
+ expectTokensToBeInTheDocument( [
1401
+ 'small trousers',
1402
+ 'small shirt',
1403
+ ] );
1404
+ expectTokensNotToBeInTheDocument( [
1405
+ 'medium trousers',
1406
+ 'medium shirt',
1407
+ ] );
1408
+ expect( onChangeSpy ).not.toHaveBeenCalled();
1409
+
1410
+ const input = screen.getByRole( 'combobox' );
1411
+
1412
+ // Add 'small jacket' token by typing it and pressing enter to tokenize it.
1413
+ // The saveTransform function will change its value to "medium jacket"
1414
+ // when tokenizing it, thus affecting both the onChange callback and
1415
+ // the text rendered in the document.
1416
+ await user.type( input, 'small jacket[Enter]' );
1417
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1418
+ expect( onChangeSpy ).toHaveBeenCalledWith( [
1419
+ 'small trousers',
1420
+ 'small shirt',
1421
+ 'medium jacket',
1422
+ ] );
1423
+ expectTokensToBeInTheDocument( [
1424
+ 'small trousers',
1425
+ 'small shirt',
1426
+ 'medium jacket',
1427
+ ] );
1428
+ expectTokensNotToBeInTheDocument( [
1429
+ 'medium trousers',
1430
+ 'medium shirt',
1431
+ 'small jacket',
1432
+ ] );
1433
+ } );
1434
+
1435
+ it( 'is applied to the search value when matching it against the list of suggestions', async () => {
1436
+ const user = userEvent.setup( {
1437
+ advanceTimers: jest.advanceTimersByTime,
1438
+ } );
1439
+
1440
+ const onChangeSpy = jest.fn();
1441
+
1442
+ const suggestions = [ 'Expensive food', 'Free food' ];
1443
+
1444
+ render(
1445
+ <FormTokenFieldWithState
1446
+ onChange={ onChangeSpy }
1447
+ suggestions={ suggestions }
1448
+ saveTransform={ ( text: string ) =>
1449
+ text.replace( /cheap/gi, 'free' )
1450
+ }
1451
+ />
1452
+ );
1453
+
1454
+ const input = screen.getByRole( 'combobox' );
1455
+
1456
+ // "cheap" matches the "Free food" option, since the `saveTransform`
1457
+ // function transform "cheap" to "free"
1458
+ await user.type( input, 'cheap' );
1459
+
1460
+ // But the value shown in the suggestion is still "Cheap food"
1461
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
1462
+ 'Free food',
1463
+ ] );
1464
+
1465
+ // Selecting the suggestion will add the transformed value as a token,
1466
+ // since the `saveTransform` function will be applied before tokenizing.
1467
+ await user.keyboard( '[ArrowDown][Enter]' );
1468
+
1469
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1470
+ expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'Free food' ] );
1471
+ } );
1472
+ } );
1473
+
1474
+ describe( 'displayTransform', () => {
1475
+ it( 'should allow to modify the text rendered in the browser for each token', async () => {
1476
+ const user = userEvent.setup( {
1477
+ advanceTimers: jest.advanceTimersByTime,
1478
+ } );
1479
+
1480
+ const onChangeSpy = jest.fn();
1481
+
1482
+ const { rerender } = render(
1483
+ <FormTokenFieldWithState
1484
+ onChange={ onChangeSpy }
1485
+ initialValue={ [ 'dark blue', 'dark green' ] }
1486
+ />
1487
+ );
1488
+
1489
+ expectTokensToBeInTheDocument( [ 'dark blue', 'dark green' ] );
1490
+
1491
+ rerender(
1492
+ <FormTokenFieldWithState
1493
+ onChange={ onChangeSpy }
1494
+ initialValue={ [ 'dark blue', 'dark green' ] }
1495
+ displayTransform={ ( tokenText: string ) =>
1496
+ tokenText.replace( /dark/g, 'light' )
1497
+ }
1498
+ />
1499
+ );
1500
+
1501
+ // The `displayTransform` prop applies also to the displayed text
1502
+ // of existing tokens
1503
+ expectTokensToBeInTheDocument( [ 'light blue', 'light green' ] );
1504
+ expectTokensNotToBeInTheDocument( [ 'dark blue', 'dark green' ] );
1505
+
1506
+ expect( onChangeSpy ).not.toHaveBeenCalled();
1507
+
1508
+ const input = screen.getByRole( 'combobox' );
1509
+
1510
+ // Add 'dark red' token by typing it and pressing enter to tokenize it.
1511
+ // The displayTransform function will change its displayed value to
1512
+ // "light red", but the onChange callback will still receive "dark red" as
1513
+ // part of the component's new value.
1514
+ await user.type( input, 'dark red[Enter]' );
1515
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1516
+ expect( onChangeSpy ).toHaveBeenCalledWith( [
1517
+ 'dark blue',
1518
+ 'dark green',
1519
+ 'dark red',
1520
+ ] );
1521
+ expectTokensToBeInTheDocument( [
1522
+ 'light blue',
1523
+ 'light green',
1524
+ 'light red',
1525
+ ] );
1526
+ expectTokensNotToBeInTheDocument( [
1527
+ 'dark blue',
1528
+ 'dark green',
1529
+ 'dark red',
1530
+ ] );
1531
+ } );
1532
+
1533
+ it( "is applied to each suggestions, but doesn't influence the matching against the search value", async () => {
1534
+ const user = userEvent.setup( {
1535
+ advanceTimers: jest.advanceTimersByTime,
1536
+ } );
1537
+
1538
+ const onChangeSpy = jest.fn();
1539
+
1540
+ const suggestions = [ 'Hot coffee', 'Hot tea' ];
1541
+
1542
+ render(
1543
+ <FormTokenFieldWithState
1544
+ onChange={ onChangeSpy }
1545
+ suggestions={ suggestions }
1546
+ displayTransform={ ( text: string ) =>
1547
+ text.replace( /hot/gi, 'cold' )
1548
+ }
1549
+ />
1550
+ );
1551
+
1552
+ const input = screen.getByRole( 'combobox' );
1553
+
1554
+ // The `displayTransform` function is only applied to the value
1555
+ // rendered in the DOM, while the data behind is not modified.
1556
+ await user.type( input, 'hot' );
1557
+
1558
+ expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
1559
+ 'cold coffee',
1560
+ 'cold tea',
1561
+ ] );
1562
+
1563
+ await user.keyboard( '[ArrowDown][Enter]' );
1564
+
1565
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1566
+ expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'Hot coffee' ] );
1567
+ } );
1568
+
1569
+ it( 'should allow to pass a function that renders tokens with escaped special characters correctly', async () => {
1570
+ render(
1571
+ <FormTokenFieldWithState
1572
+ initialValue={ [
1573
+ 'a b',
1574
+ 'i &lt;3 tags',
1575
+ '1&amp;2&amp;3&amp;4',
1576
+ ] }
1577
+ displayTransform={ unescapeAndFormatSpaces }
1578
+ />
1579
+ );
1580
+
1581
+ // This is hacky, but it's a way we can check exactly the output HTML
1582
+ [
1583
+ 'a&nbsp;&nbsp;&nbsp;b',
1584
+ 'i&nbsp;&lt;3&nbsp;tags',
1585
+ '1&amp;2&amp;3&amp;4',
1586
+ ].forEach( ( tokenHtml ) => {
1587
+ screen.getByText( ( _, node: Element | null ) => {
1588
+ if ( node === null ) {
1589
+ return false;
1590
+ }
1591
+
1592
+ return node.innerHTML === tokenHtml;
1593
+ } );
1594
+ } );
1595
+ } );
1596
+
1597
+ it( 'should allow to pass a function that renders tokens with special characters correctly', async () => {
1598
+ // This test is not as realistic as the previous one: if a WP site
1599
+ // contains tag names with special characters, the API will always
1600
+ // return the tag names already escaped. However, this is still
1601
+ // worth testing, so we can be sure that token values with
1602
+ // dangerous characters in them don't have these characters carried
1603
+ // through unescaped to the HTML.
1604
+ render(
1605
+ <FormTokenFieldWithState
1606
+ initialValue={ [ 'a b', 'i <3 tags', '1&2&3&4' ] }
1607
+ displayTransform={ unescapeAndFormatSpaces }
1608
+ />
1609
+ );
1610
+
1611
+ // This is hacky, but it's a way we can check exactly the output HTML
1612
+ [
1613
+ 'a&nbsp;&nbsp;&nbsp;b',
1614
+ 'i&nbsp;&lt;3&nbsp;tags',
1615
+ '1&amp;2&amp;3&amp;4',
1616
+ ].forEach( ( tokenHtml ) => {
1617
+ screen.getByText( ( _, node: Element | null ) => {
1618
+ if ( node === null ) {
1619
+ return false;
1620
+ }
1621
+
1622
+ return node.innerHTML === tokenHtml;
1623
+ } );
1624
+ } );
1625
+ } );
1626
+ } );
1627
+
1628
+ describe( 'validation', () => {
1629
+ it( 'should add a token only if it passes the validation set via `__experimentalValidateInput`', async () => {
1630
+ const user = userEvent.setup( {
1631
+ advanceTimers: jest.advanceTimersByTime,
1632
+ } );
1633
+
1634
+ const onChangeSpy = jest.fn();
1635
+ const startsWithCapitalLetter = ( tokenText: string ) =>
1636
+ /^[A-Z]/.test( tokenText );
1637
+
1638
+ const { rerender } = render(
1639
+ <FormTokenFieldWithState onChange={ onChangeSpy } />
1640
+ );
1641
+
1642
+ const input = screen.getByRole( 'combobox' );
1643
+
1644
+ // Add 'cherry' token by typing it and pressing enter to tokenize it.
1645
+ await user.type( input, 'cherry[Enter]' );
1646
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1647
+ expect( onChangeSpy ).toHaveBeenCalledWith( [ 'cherry' ] );
1648
+ expectTokensToBeInTheDocument( [ 'cherry' ] );
1649
+
1650
+ rerender(
1651
+ <FormTokenFieldWithState
1652
+ onChange={ onChangeSpy }
1653
+ __experimentalValidateInput={ startsWithCapitalLetter }
1654
+ />
1655
+ );
1656
+
1657
+ // Add 'cranberry' token by typing it and pressing enter to tokenize it.
1658
+ // The validation function won't allow the value from being tokenized.
1659
+ // Note that the any token added before is still around, even if it
1660
+ // wouldn't pass the newly added validation — this is because the
1661
+ // validation happens when the input\'s value gets tokenized.
1662
+ await user.type( input, 'cranberry[Enter]' );
1663
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1664
+ expectTokensToBeInTheDocument( [ 'cherry' ] );
1665
+ expectTokensNotToBeInTheDocument( [ 'cranberry' ] );
1666
+
1667
+ // Retry, this time with capital letter. The value should be added.
1668
+ await user.clear( input );
1669
+ await user.type( input, 'Cranberry[Enter]' );
1670
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
1671
+ expectTokensToBeInTheDocument( [ 'cherry', 'Cranberry' ] );
1672
+ } );
1673
+ } );
1674
+
1675
+ describe( 'maxLength', () => {
1676
+ it( 'should not allow adding new tokens beyond the value defined by the `maxLength` prop', async () => {
1677
+ const user = userEvent.setup( {
1678
+ advanceTimers: jest.advanceTimersByTime,
1679
+ } );
1680
+
1681
+ const onChangeSpy = jest.fn();
1682
+
1683
+ render(
1684
+ <FormTokenFieldWithState
1685
+ onChange={ onChangeSpy }
1686
+ initialValue={ [ 'square', 'triangle', 'circle' ] }
1687
+ maxLength={ 3 }
1688
+ />
1689
+ );
1690
+
1691
+ expectTokensToBeInTheDocument( [ 'square', 'triangle', 'circle' ] );
1692
+
1693
+ const input = screen.getByRole( 'combobox' );
1694
+
1695
+ // Try to add the 'hexagon' token, but because the number of tokens already
1696
+ // matches `maxLength`, the token won't be added.
1697
+ await user.type( input, 'hexagon[Enter]' );
1698
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 0 );
1699
+ expectTokensToBeInTheDocument( [ 'square', 'triangle', 'circle' ] );
1700
+ expectTokensNotToBeInTheDocument( [ 'hexagon' ] );
1701
+
1702
+ // Delete the last token ("circle"), in order to make space for the
1703
+ // hexagon token
1704
+ await user.clear( input );
1705
+ await user.keyboard( '[Backspace]' );
1706
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1707
+ expect( onChangeSpy ).toHaveBeenLastCalledWith( [
1708
+ 'square',
1709
+ 'triangle',
1710
+ ] );
1711
+ expectTokensToBeInTheDocument( [ 'square', 'triangle' ] );
1712
+ expectTokensNotToBeInTheDocument( [ 'circle' ] );
1713
+
1714
+ // Try to add the 'hexagon' token again. This time, the token will be
1715
+ // added because the current number of tokens is below the `maxLength`
1716
+ // threshold.
1717
+ await user.type( input, 'hexagon[Enter]' );
1718
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
1719
+ expect( onChangeSpy ).toHaveBeenLastCalledWith( [
1720
+ 'square',
1721
+ 'triangle',
1722
+ 'hexagon',
1723
+ ] );
1724
+ expectTokensToBeInTheDocument( [
1725
+ 'square',
1726
+ 'triangle',
1727
+ 'hexagon',
1728
+ ] );
1729
+ } );
1730
+
1731
+ it( "should not affect the number of tokens set via the `value` prop (ie. not caused by tokenizing the user's input)", () => {
1732
+ render(
1733
+ <FormTokenFieldWithState
1734
+ initialValue={ [ 'rectangle', 'ellipse', 'pentagon' ] }
1735
+ maxLength={ 2 }
1736
+ />
1737
+ );
1738
+
1739
+ expectTokensToBeInTheDocument( [
1740
+ 'rectangle',
1741
+ 'ellipse',
1742
+ 'pentagon',
1743
+ ] );
1744
+ } );
1745
+
1746
+ it( 'should not affect tokens that were added before the limit was imposed', async () => {
1747
+ const user = userEvent.setup( {
1748
+ advanceTimers: jest.advanceTimersByTime,
1749
+ } );
1750
+
1751
+ const onChangeSpy = jest.fn();
1752
+
1753
+ const { rerender } = render(
1754
+ <FormTokenFieldWithState onChange={ onChangeSpy } />
1755
+ );
1756
+
1757
+ const input = screen.getByRole( 'combobox' );
1758
+
1759
+ await user.type( input, 'cube[Enter]sphere[Enter]cylinder[Enter]' );
1760
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
1761
+ expect( onChangeSpy ).toHaveBeenLastCalledWith( [
1762
+ 'cube',
1763
+ 'sphere',
1764
+ 'cylinder',
1765
+ ] );
1766
+ expectTokensToBeInTheDocument( [ 'cube', 'sphere', 'cylinder' ] );
1767
+
1768
+ // Add a `maxLength` after some tokens have already been added.
1769
+ rerender(
1770
+ <FormTokenFieldWithState
1771
+ onChange={ onChangeSpy }
1772
+ maxLength={ 1 }
1773
+ />
1774
+ );
1775
+
1776
+ // Changing `maxLength` doesn't affect existing tokens, even if their
1777
+ // number exceeds the new limit.
1778
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
1779
+ expectTokensToBeInTheDocument( [ 'cube', 'sphere', 'cylinder' ] );
1780
+
1781
+ // Try to add the 'pyramid' token, but because the number of tokens already
1782
+ // exceeds `maxLength`, the token won't be added.
1783
+ await user.type( input, 'pyramid[Enter]' );
1784
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
1785
+ expectTokensToBeInTheDocument( [ 'cube', 'sphere', 'cylinder' ] );
1786
+ expectTokensNotToBeInTheDocument( [ 'pyramid' ] );
1787
+ } );
1788
+ } );
1789
+
1790
+ describe( 'disabled', () => {
1791
+ it( 'should not allow adding tokens when the `disabled` prop is `true`', async () => {
1792
+ const user = userEvent.setup( {
1793
+ advanceTimers: jest.advanceTimersByTime,
1794
+ } );
1795
+
1796
+ const onChangeSpy = jest.fn();
1797
+
1798
+ const { rerender } = render(
1799
+ <FormTokenFieldWithState onChange={ onChangeSpy } />
1800
+ );
1801
+
1802
+ const input = screen.getByRole( 'combobox' );
1803
+
1804
+ // Add 'sun' token by typing it and pressing enter to tokenize it.
1805
+ await user.type( input, 'sun[Enter]' );
1806
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1807
+ expect( onChangeSpy ).toHaveBeenCalledWith( [ 'sun' ] );
1808
+ expectTokensToBeInTheDocument( [ 'sun' ] );
1809
+
1810
+ rerender(
1811
+ <FormTokenFieldWithState onChange={ onChangeSpy } disabled />
1812
+ );
1813
+
1814
+ // Try to add 'moon' token. The token is not added because of the `disabled`
1815
+ // prop.
1816
+ await user.type( input, 'moon[Enter]' );
1817
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
1818
+ expectTokensNotToBeInTheDocument( [ 'moon' ] );
1819
+ } );
1820
+
1821
+ it( 'should not allow removing tokens when the `disable` prop is `true`', async () => {
1822
+ const user = userEvent.setup( {
1823
+ advanceTimers: jest.advanceTimersByTime,
1824
+ } );
1825
+
1826
+ const onChangeSpy = jest.fn();
1827
+
1828
+ render(
1829
+ <FormTokenFieldWithState
1830
+ onChange={ onChangeSpy }
1831
+ initialValue={ [ 'sea', 'ocean' ] }
1832
+ disabled
1833
+ />
1834
+ );
1835
+
1836
+ const input = screen.getByRole( 'combobox' );
1837
+
1838
+ // Try to delete the last token with the keyboard. The token won't be
1839
+ // deleted, because of the `disabled` prop.
1840
+ await user.type( input, '[Backspace]' );
1841
+ expect( onChangeSpy ).not.toHaveBeenCalled();
1842
+ expectTokensToBeInTheDocument( [ 'sea', 'ocean' ] );
1843
+
1844
+ // Try to delete the last token with the mouse. The token won't be
1845
+ // deleted, because of the `disabled` prop.
1846
+ await user.click(
1847
+ screen.getAllByRole( 'button', { name: 'Remove item' } )[ 0 ]
1848
+ );
1849
+ expect( onChangeSpy ).not.toHaveBeenCalled();
1850
+ expectTokensToBeInTheDocument( [ 'sea', 'ocean' ] );
1851
+ } );
1852
+ } );
1853
+
1854
+ describe( 'messages', () => {
1855
+ const defaultMessages = {
1856
+ added: 'Item added.',
1857
+ removed: 'Item removed.',
1858
+ remove: 'Remove item',
1859
+ __experimentalInvalid: 'Invalid item',
1860
+ };
1861
+ const customMessages = {
1862
+ added: 'Test message for new item.',
1863
+ removed: 'Test message for item delete.',
1864
+ remove: 'Test label for item delete button.',
1865
+ __experimentalInvalid:
1866
+ 'Test message for when an item fails validation.',
1867
+ };
1868
+
1869
+ it( 'should announce to assistive technology the addition of a new token', async () => {
1870
+ const user = userEvent.setup( {
1871
+ advanceTimers: jest.advanceTimersByTime,
1872
+ } );
1873
+
1874
+ render( <FormTokenFieldWithState /> );
1875
+
1876
+ const input = screen.getByRole( 'combobox' );
1877
+
1878
+ // Add 'cat' token, check that the aria-live region has been updated.
1879
+ await user.type( input, 'cat[Enter]' );
1880
+
1881
+ expect( screen.getByText( defaultMessages.added ) ).toHaveAttribute(
1882
+ 'aria-live',
1883
+ 'assertive'
1884
+ );
1885
+ } );
1886
+
1887
+ it( 'should announce to assistive technology the addition of a new token with a custom message', async () => {
1888
+ const user = userEvent.setup( {
1889
+ advanceTimers: jest.advanceTimersByTime,
1890
+ } );
1891
+
1892
+ render( <FormTokenFieldWithState messages={ customMessages } /> );
1893
+
1894
+ const input = screen.getByRole( 'combobox' );
1895
+
1896
+ // Add 'dog' token, check that the aria-live region has been updated.
1897
+ await user.type( input, 'dog[Enter]' );
1898
+
1899
+ expect( screen.getByText( customMessages.added ) ).toHaveAttribute(
1900
+ 'aria-live',
1901
+ 'assertive'
1902
+ );
1903
+ } );
1904
+
1905
+ it( 'should announce to assistive technology the removal of a token', async () => {
1906
+ const user = userEvent.setup( {
1907
+ advanceTimers: jest.advanceTimersByTime,
1908
+ } );
1909
+
1910
+ render( <FormTokenFieldWithState initialValue={ [ 'horse' ] } /> );
1911
+
1912
+ const input = screen.getByRole( 'combobox' );
1913
+
1914
+ // Delete "horse" token
1915
+ await user.type( input, '[Backspace]' );
1916
+
1917
+ expect(
1918
+ screen.getByText( defaultMessages.removed )
1919
+ ).toHaveAttribute( 'aria-live', 'assertive' );
1920
+ } );
1921
+
1922
+ it( 'should announce to assistive technology the removal of a token with a custom message', async () => {
1923
+ const user = userEvent.setup( {
1924
+ advanceTimers: jest.advanceTimersByTime,
1925
+ } );
1926
+
1927
+ render(
1928
+ <FormTokenFieldWithState
1929
+ initialValue={ [ 'donkey' ] }
1930
+ messages={ customMessages }
1931
+ />
1932
+ );
1933
+
1934
+ const input = screen.getByRole( 'combobox' );
1935
+
1936
+ // Delete "donkey" token
1937
+ await user.type( input, '[Backspace]' );
1938
+
1939
+ expect(
1940
+ screen.getByText( customMessages.removed )
1941
+ ).toHaveAttribute( 'aria-live', 'assertive' );
1942
+ } );
1943
+
1944
+ it( 'should announce to assistive technology the failure of a potential token to pass validation', async () => {
1945
+ const user = userEvent.setup( {
1946
+ advanceTimers: jest.advanceTimersByTime,
1947
+ } );
1948
+
1949
+ render(
1950
+ <FormTokenFieldWithState
1951
+ __experimentalValidateInput={ () => false }
1952
+ />
1953
+ );
1954
+
1955
+ const input = screen.getByRole( 'combobox' );
1956
+
1957
+ // Try to add "eagle" token, which won't be added because of the
1958
+ // __experimentalValidateInput prop.
1959
+ await user.type( input, 'eagle[Enter]' );
1960
+
1961
+ expect(
1962
+ screen.getByText( defaultMessages.__experimentalInvalid )
1963
+ ).toHaveAttribute( 'aria-live', 'assertive' );
1964
+ } );
1965
+
1966
+ it( 'should announce to assistive technology the failure of a potential token to pass validation with a custom message', async () => {
1967
+ const user = userEvent.setup( {
1968
+ advanceTimers: jest.advanceTimersByTime,
1969
+ } );
1970
+
1971
+ render(
1972
+ <FormTokenFieldWithState
1973
+ __experimentalValidateInput={ () => false }
1974
+ messages={ customMessages }
1975
+ />
1976
+ );
1977
+
1978
+ const input = screen.getByRole( 'combobox' );
1979
+
1980
+ // Try to add "crocodile" token, which won't be added because of the
1981
+ // __experimentalValidateInput prop.
1982
+ await user.type( input, 'crocodile[Enter]' );
1983
+
1984
+ expect(
1985
+ screen.getByText( customMessages.__experimentalInvalid )
1986
+ ).toHaveAttribute( 'aria-live', 'assertive' );
1987
+ } );
1988
+
1989
+ it( 'should announce to assistive technology the result of the matching of the search text against the list of suggestions', async () => {
1990
+ const user = userEvent.setup( {
1991
+ advanceTimers: jest.advanceTimersByTime,
1992
+ } );
1993
+
1994
+ render(
1995
+ <FormTokenFieldWithState
1996
+ suggestions={ [ 'Donkey', 'Horse', 'Dog' ] }
1997
+ />
1998
+ );
1999
+
2000
+ const input = screen.getByRole( 'combobox' );
2001
+
2002
+ // No matching suggestions.
2003
+ await user.type( input, 'cat' );
2004
+
2005
+ await waitFor( () =>
2006
+ expect( screen.getByText( 'No results.' ) ).toHaveAttribute(
2007
+ 'aria-live',
2008
+ 'assertive'
2009
+ )
2010
+ );
2011
+
2012
+ // "Donkey" and "Dog" matching
2013
+ await user.clear( input );
2014
+ await user.type( input, 'do' );
2015
+
2016
+ await waitFor( () =>
2017
+ expect(
2018
+ screen.getByText(
2019
+ '2 results found, use up and down arrow keys to navigate.'
2020
+ )
2021
+ ).toHaveAttribute( 'aria-live', 'assertive' )
2022
+ );
2023
+
2024
+ // Only "Donkey" matches
2025
+ await user.type( input, 'nk' );
2026
+
2027
+ await waitFor( () =>
2028
+ expect(
2029
+ screen.getByText(
2030
+ '1 result found, use up and down arrow keys to navigate.'
2031
+ )
2032
+ ).toHaveAttribute( 'aria-live', 'assertive' )
2033
+ );
2034
+ } );
2035
+
2036
+ it( 'should update the label for the "delete" button of a token', async () => {
2037
+ render(
2038
+ <FormTokenFieldWithState
2039
+ initialValue={ [ 'bear', 'panda' ] }
2040
+ messages={ customMessages }
2041
+ />
2042
+ );
2043
+
2044
+ expect(
2045
+ screen.getAllByRole( 'button', { name: customMessages.remove } )
2046
+ ).toHaveLength( 2 );
2047
+ } );
2048
+ } );
2049
+
2050
+ // This section is definitely testing things in a non-user centric way,
2051
+ // but I wasn't sure if there was a better way.
2052
+ describe( 'aria attributes', () => {
2053
+ it( 'should add the correct aria attributes to the input as the user interacts with it', async () => {
2054
+ const user = userEvent.setup( {
2055
+ advanceTimers: jest.advanceTimersByTime,
2056
+ } );
2057
+
2058
+ const suggestions = [ 'Pine', 'Pistachio', 'Sage' ];
2059
+
2060
+ render( <FormTokenFieldWithState suggestions={ suggestions } /> );
2061
+
2062
+ // No suggestions visible
2063
+ const input = screen.getByRole( 'combobox' );
2064
+
2065
+ expect( input ).toHaveAttribute( 'autoComplete', 'off' );
2066
+ expect( input ).toHaveAttribute( 'aria-autocomplete', 'list' );
2067
+ expect( input ).toHaveAttribute( 'aria-expanded', 'false' );
2068
+ expect( input ).not.toHaveAttribute( 'aria-owns' );
2069
+ expect( input ).not.toHaveAttribute( 'aria-activedescendant' );
2070
+
2071
+ // Typing "Pi" will show the "Pistachio" and "Pine" suggestions.
2072
+ await user.type( input, 'Pi' );
2073
+
2074
+ const suggestionList = screen.getByRole( 'listbox' );
2075
+ expect( suggestionList ).toBeVisible();
2076
+
2077
+ expect( input ).toHaveAttribute( 'aria-expanded', 'true' );
2078
+ expect( input ).toHaveAttribute( 'aria-owns', suggestionList.id );
2079
+ expect( input ).not.toHaveAttribute( 'aria-activedescendant' );
2080
+
2081
+ // Select the "Pine" suggestion
2082
+ await user.click( input );
2083
+ await user.keyboard( '[ArrowDown]' );
2084
+
2085
+ const pineSuggestion = within( suggestionList ).getByRole(
2086
+ 'option',
2087
+ { name: 'Pine', selected: true }
2088
+ );
2089
+ expect( input ).toHaveAttribute( 'aria-expanded', 'true' );
2090
+ expect( input ).toHaveAttribute( 'aria-owns', suggestionList.id );
2091
+ expect( input ).toHaveAttribute(
2092
+ 'aria-activedescendant',
2093
+ pineSuggestion.id
2094
+ );
2095
+
2096
+ // Add the suggestion, which hides the list
2097
+ await user.keyboard( '[Enter]' );
2098
+
2099
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
2100
+
2101
+ expect( input ).toHaveAttribute( 'aria-expanded', 'false' );
2102
+ expect( input ).not.toHaveAttribute( 'aria-owns' );
2103
+ expect( input ).not.toHaveAttribute( 'aria-activedescendant' );
2104
+ } );
2105
+ } );
2106
+ } );