@wordpress/components 29.12.0 → 30.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 (337) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/build/autocomplete/index.js +4 -0
  3. package/build/autocomplete/index.js.map +1 -1
  4. package/build/box-control/input-control.js +2 -2
  5. package/build/box-control/input-control.js.map +1 -1
  6. package/build/calendar/date-calendar/index.js +69 -0
  7. package/build/calendar/date-calendar/index.js.map +1 -0
  8. package/build/calendar/date-range-calendar/index.js +172 -0
  9. package/build/calendar/date-range-calendar/index.js.map +1 -0
  10. package/build/calendar/index.js +27 -0
  11. package/build/calendar/index.js.map +1 -0
  12. package/build/calendar/types.js +6 -0
  13. package/build/calendar/types.js.map +1 -0
  14. package/build/calendar/utils/constants.js +68 -0
  15. package/build/calendar/utils/constants.js.map +1 -0
  16. package/build/calendar/utils/day-cell.js +137 -0
  17. package/build/calendar/utils/day-cell.js.map +1 -0
  18. package/build/calendar/utils/misc.js +10 -0
  19. package/build/calendar/utils/misc.js.map +1 -0
  20. package/build/calendar/utils/use-localization-props.js +162 -0
  21. package/build/calendar/utils/use-localization-props.js.map +1 -0
  22. package/build/custom-gradient-picker/gradient-bar/control-points.js +1 -1
  23. package/build/custom-gradient-picker/gradient-bar/control-points.js.map +1 -1
  24. package/build/custom-select-control-v2/custom-select.js +3 -3
  25. package/build/custom-select-control-v2/custom-select.js.map +1 -1
  26. package/build/date-time/date/index.js +1 -1
  27. package/build/date-time/date/index.js.map +1 -1
  28. package/build/form-token-field/index.js +11 -1
  29. package/build/form-token-field/index.js.map +1 -1
  30. package/build/form-token-field/token.js +1 -1
  31. package/build/form-token-field/token.js.map +1 -1
  32. package/build/icon/index.js +2 -0
  33. package/build/icon/index.js.map +1 -1
  34. package/build/mobile/bottom-sheet/cell.native.js +2 -2
  35. package/build/mobile/bottom-sheet/cell.native.js.map +1 -1
  36. package/build/mobile/link-picker/index.native.js +1 -1
  37. package/build/mobile/link-picker/index.native.js.map +1 -1
  38. package/build/navigation/menu/menu-title-search.js +1 -1
  39. package/build/navigation/menu/menu-title-search.js.map +1 -1
  40. package/build/palette-edit/index.js +4 -4
  41. package/build/palette-edit/index.js.map +1 -1
  42. package/build/private-apis.js +5 -1
  43. package/build/private-apis.js.map +1 -1
  44. package/build/select-control/index.js +1 -1
  45. package/build/select-control/index.js.map +1 -1
  46. package/build/toggle-group-control/toggle-group-control/as-button-group.js.map +1 -1
  47. package/build/utils/hooks/use-controlled-value.js +8 -4
  48. package/build/utils/hooks/use-controlled-value.js.map +1 -1
  49. package/build/validated-form-controls/components/checkbox-control.js +52 -0
  50. package/build/validated-form-controls/components/checkbox-control.js.map +1 -0
  51. package/build/validated-form-controls/components/combobox-control.js +64 -0
  52. package/build/validated-form-controls/components/combobox-control.js.map +1 -0
  53. package/build/validated-form-controls/components/custom-select-control.js +71 -0
  54. package/build/validated-form-controls/components/custom-select-control.js.map +1 -0
  55. package/build/validated-form-controls/components/index.js +138 -0
  56. package/build/validated-form-controls/components/index.js.map +1 -0
  57. package/build/validated-form-controls/components/input-control.js +50 -0
  58. package/build/validated-form-controls/components/input-control.js.map +1 -0
  59. package/build/validated-form-controls/components/number-control.js +53 -0
  60. package/build/validated-form-controls/components/number-control.js.map +1 -0
  61. package/build/validated-form-controls/components/radio-control.js +51 -0
  62. package/build/validated-form-controls/components/radio-control.js.map +1 -0
  63. package/build/validated-form-controls/components/range-control.js +51 -0
  64. package/build/validated-form-controls/components/range-control.js.map +1 -0
  65. package/build/validated-form-controls/components/select-control.js +53 -0
  66. package/build/validated-form-controls/components/select-control.js.map +1 -0
  67. package/build/validated-form-controls/components/text-control.js +51 -0
  68. package/build/validated-form-controls/components/text-control.js.map +1 -0
  69. package/build/validated-form-controls/components/textarea-control.js +50 -0
  70. package/build/validated-form-controls/components/textarea-control.js.map +1 -0
  71. package/build/validated-form-controls/components/toggle-control.js +60 -0
  72. package/build/validated-form-controls/components/toggle-control.js.map +1 -0
  73. package/build/validated-form-controls/components/toggle-group-control.js +69 -0
  74. package/build/validated-form-controls/components/toggle-group-control.js.map +1 -0
  75. package/build/validated-form-controls/components/types.js +6 -0
  76. package/build/validated-form-controls/components/types.js.map +1 -0
  77. package/build/validated-form-controls/control-with-error.js +137 -0
  78. package/build/validated-form-controls/control-with-error.js.map +1 -0
  79. package/build/validated-form-controls/index.js +28 -0
  80. package/build/validated-form-controls/index.js.map +1 -0
  81. package/build-module/autocomplete/index.js +4 -0
  82. package/build-module/autocomplete/index.js.map +1 -1
  83. package/build-module/box-control/input-control.js +2 -2
  84. package/build-module/box-control/input-control.js.map +1 -1
  85. package/build-module/calendar/date-calendar/index.js +59 -0
  86. package/build-module/calendar/date-calendar/index.js.map +1 -0
  87. package/build-module/calendar/date-range-calendar/index.js +161 -0
  88. package/build-module/calendar/date-range-calendar/index.js.map +1 -0
  89. package/build-module/calendar/index.js +4 -0
  90. package/build-module/calendar/index.js.map +1 -0
  91. package/build-module/calendar/types.js +2 -0
  92. package/build-module/calendar/types.js.map +1 -0
  93. package/build-module/calendar/utils/constants.js +61 -0
  94. package/build-module/calendar/utils/constants.js.map +1 -0
  95. package/build-module/calendar/utils/day-cell.js +131 -0
  96. package/build-module/calendar/utils/day-cell.js.map +1 -0
  97. package/build-module/calendar/utils/misc.js +4 -0
  98. package/build-module/calendar/utils/misc.js.map +1 -0
  99. package/build-module/calendar/utils/use-localization-props.js +154 -0
  100. package/build-module/calendar/utils/use-localization-props.js.map +1 -0
  101. package/build-module/custom-gradient-picker/gradient-bar/control-points.js +1 -1
  102. package/build-module/custom-gradient-picker/gradient-bar/control-points.js.map +1 -1
  103. package/build-module/custom-select-control-v2/custom-select.js +4 -4
  104. package/build-module/custom-select-control-v2/custom-select.js.map +1 -1
  105. package/build-module/date-time/date/index.js +1 -1
  106. package/build-module/date-time/date/index.js.map +1 -1
  107. package/build-module/form-token-field/index.js +11 -1
  108. package/build-module/form-token-field/index.js.map +1 -1
  109. package/build-module/form-token-field/token.js +1 -1
  110. package/build-module/form-token-field/token.js.map +1 -1
  111. package/build-module/icon/index.js +2 -0
  112. package/build-module/icon/index.js.map +1 -1
  113. package/build-module/mobile/bottom-sheet/cell.native.js +2 -2
  114. package/build-module/mobile/bottom-sheet/cell.native.js.map +1 -1
  115. package/build-module/mobile/link-picker/index.native.js +1 -1
  116. package/build-module/mobile/link-picker/index.native.js.map +1 -1
  117. package/build-module/navigation/menu/menu-title-search.js +1 -1
  118. package/build-module/navigation/menu/menu-title-search.js.map +1 -1
  119. package/build-module/palette-edit/index.js +4 -4
  120. package/build-module/palette-edit/index.js.map +1 -1
  121. package/build-module/private-apis.js +5 -1
  122. package/build-module/private-apis.js.map +1 -1
  123. package/build-module/select-control/index.js +1 -1
  124. package/build-module/select-control/index.js.map +1 -1
  125. package/build-module/toggle-group-control/toggle-group-control/as-button-group.js.map +1 -1
  126. package/build-module/utils/hooks/use-controlled-value.js +9 -5
  127. package/build-module/utils/hooks/use-controlled-value.js.map +1 -1
  128. package/build-module/validated-form-controls/components/checkbox-control.js +44 -0
  129. package/build-module/validated-form-controls/components/checkbox-control.js.map +1 -0
  130. package/build-module/validated-form-controls/components/combobox-control.js +56 -0
  131. package/build-module/validated-form-controls/components/combobox-control.js.map +1 -0
  132. package/build-module/validated-form-controls/components/custom-select-control.js +63 -0
  133. package/build-module/validated-form-controls/components/custom-select-control.js.map +1 -0
  134. package/build-module/validated-form-controls/components/index.js +13 -0
  135. package/build-module/validated-form-controls/components/index.js.map +1 -0
  136. package/build-module/validated-form-controls/components/input-control.js +42 -0
  137. package/build-module/validated-form-controls/components/input-control.js.map +1 -0
  138. package/build-module/validated-form-controls/components/number-control.js +45 -0
  139. package/build-module/validated-form-controls/components/number-control.js.map +1 -0
  140. package/build-module/validated-form-controls/components/radio-control.js +43 -0
  141. package/build-module/validated-form-controls/components/radio-control.js.map +1 -0
  142. package/build-module/validated-form-controls/components/range-control.js +43 -0
  143. package/build-module/validated-form-controls/components/range-control.js.map +1 -0
  144. package/build-module/validated-form-controls/components/select-control.js +45 -0
  145. package/build-module/validated-form-controls/components/select-control.js.map +1 -0
  146. package/build-module/validated-form-controls/components/text-control.js +43 -0
  147. package/build-module/validated-form-controls/components/text-control.js.map +1 -0
  148. package/build-module/validated-form-controls/components/textarea-control.js +42 -0
  149. package/build-module/validated-form-controls/components/textarea-control.js.map +1 -0
  150. package/build-module/validated-form-controls/components/toggle-control.js +52 -0
  151. package/build-module/validated-form-controls/components/toggle-control.js.map +1 -0
  152. package/build-module/validated-form-controls/components/toggle-group-control.js +62 -0
  153. package/build-module/validated-form-controls/components/toggle-group-control.js.map +1 -0
  154. package/build-module/validated-form-controls/components/types.js +2 -0
  155. package/build-module/validated-form-controls/components/types.js.map +1 -0
  156. package/build-module/validated-form-controls/control-with-error.js +129 -0
  157. package/build-module/validated-form-controls/control-with-error.js.map +1 -0
  158. package/build-module/validated-form-controls/index.js +3 -0
  159. package/build-module/validated-form-controls/index.js.map +1 -0
  160. package/build-style/style-rtl.css +418 -22
  161. package/build-style/style.css +418 -22
  162. package/build-types/autocomplete/index.d.ts.map +1 -1
  163. package/build-types/box-control/input-control.d.ts.map +1 -1
  164. package/build-types/box-control/utils.d.ts +7 -7
  165. package/build-types/calendar/date-calendar/index.d.ts +11 -0
  166. package/build-types/calendar/date-calendar/index.d.ts.map +1 -0
  167. package/build-types/calendar/date-range-calendar/index.d.ts +14 -0
  168. package/build-types/calendar/date-range-calendar/index.d.ts.map +1 -0
  169. package/build-types/calendar/index.d.ts +4 -0
  170. package/build-types/calendar/index.d.ts.map +1 -0
  171. package/build-types/calendar/stories/date-calendar.story.d.ts +16 -0
  172. package/build-types/calendar/stories/date-calendar.story.d.ts.map +1 -0
  173. package/build-types/calendar/stories/date-range-calendar.story.d.ts +16 -0
  174. package/build-types/calendar/stories/date-range-calendar.story.d.ts.map +1 -0
  175. package/build-types/calendar/test/__utils__/index.d.ts +10 -0
  176. package/build-types/calendar/test/__utils__/index.d.ts.map +1 -0
  177. package/build-types/calendar/test/date-calendar.d.ts +2 -0
  178. package/build-types/calendar/test/date-calendar.d.ts.map +1 -0
  179. package/build-types/calendar/test/date-range-calendar.d.ts +2 -0
  180. package/build-types/calendar/test/date-range-calendar.d.ts.map +1 -0
  181. package/build-types/calendar/types.d.ts +317 -0
  182. package/build-types/calendar/types.d.ts.map +1 -0
  183. package/build-types/calendar/utils/constants.d.ts +52 -0
  184. package/build-types/calendar/utils/constants.d.ts.map +1 -0
  185. package/build-types/calendar/utils/day-cell.d.ts +21 -0
  186. package/build-types/calendar/utils/day-cell.d.ts.map +1 -0
  187. package/build-types/calendar/utils/misc.d.ts +2 -0
  188. package/build-types/calendar/utils/misc.d.ts.map +1 -0
  189. package/build-types/calendar/utils/use-localization-props.d.ts +64 -0
  190. package/build-types/calendar/utils/use-localization-props.d.ts.map +1 -0
  191. package/build-types/color-picker/styles.d.ts.map +1 -1
  192. package/build-types/custom-gradient-picker/constants.d.ts +6 -3
  193. package/build-types/custom-gradient-picker/constants.d.ts.map +1 -1
  194. package/build-types/custom-select-control-v2/custom-select.d.ts.map +1 -1
  195. package/build-types/dimension-control/sizes.d.ts +15 -3
  196. package/build-types/dimension-control/sizes.d.ts.map +1 -1
  197. package/build-types/font-size-picker/constants.d.ts +2 -2
  198. package/build-types/font-size-picker/constants.d.ts.map +1 -1
  199. package/build-types/form-token-field/index.d.ts.map +1 -1
  200. package/build-types/icon/index.d.ts.map +1 -1
  201. package/build-types/popover/overlay-middlewares.d.ts +6 -1
  202. package/build-types/popover/overlay-middlewares.d.ts.map +1 -1
  203. package/build-types/private-apis.d.ts.map +1 -1
  204. package/build-types/select-control/stories/index.story.d.ts.map +1 -1
  205. package/build-types/toggle-group-control/toggle-group-control/as-button-group.d.ts.map +1 -1
  206. package/build-types/utils/hooks/use-controlled-value.d.ts +2 -2
  207. package/build-types/utils/hooks/use-controlled-value.d.ts.map +1 -1
  208. package/build-types/validated-form-controls/components/checkbox-control.d.ts +9 -0
  209. package/build-types/validated-form-controls/components/checkbox-control.d.ts.map +1 -0
  210. package/build-types/validated-form-controls/components/combobox-control.d.ts +21 -0
  211. package/build-types/validated-form-controls/components/combobox-control.d.ts.map +1 -0
  212. package/build-types/validated-form-controls/components/custom-select-control.d.ts +4 -0
  213. package/build-types/validated-form-controls/components/custom-select-control.d.ts.map +1 -0
  214. package/build-types/validated-form-controls/components/index.d.ts +13 -0
  215. package/build-types/validated-form-controls/components/index.d.ts.map +1 -0
  216. package/build-types/validated-form-controls/components/input-control.d.ts +4 -0
  217. package/build-types/validated-form-controls/components/input-control.d.ts.map +1 -0
  218. package/build-types/validated-form-controls/components/number-control.d.ts +17 -0
  219. package/build-types/validated-form-controls/components/number-control.d.ts.map +1 -0
  220. package/build-types/validated-form-controls/components/radio-control.d.ts +11 -0
  221. package/build-types/validated-form-controls/components/radio-control.d.ts.map +1 -0
  222. package/build-types/validated-form-controls/components/range-control.d.ts +36 -0
  223. package/build-types/validated-form-controls/components/range-control.d.ts.map +1 -0
  224. package/build-types/validated-form-controls/components/select-control.d.ts +9 -0
  225. package/build-types/validated-form-controls/components/select-control.d.ts.map +1 -0
  226. package/build-types/validated-form-controls/components/stories/checkbox-control.story.d.ts +12 -0
  227. package/build-types/validated-form-controls/components/stories/checkbox-control.story.d.ts.map +1 -0
  228. package/build-types/validated-form-controls/components/stories/combobox-control.story.d.ts +12 -0
  229. package/build-types/validated-form-controls/components/stories/combobox-control.story.d.ts.map +1 -0
  230. package/build-types/validated-form-controls/components/stories/custom-select-control.story.d.ts +12 -0
  231. package/build-types/validated-form-controls/components/stories/custom-select-control.story.d.ts.map +1 -0
  232. package/build-types/validated-form-controls/components/stories/input-control.story.d.ts +18 -0
  233. package/build-types/validated-form-controls/components/stories/input-control.story.d.ts.map +1 -0
  234. package/build-types/validated-form-controls/components/stories/number-control.story.d.ts +12 -0
  235. package/build-types/validated-form-controls/components/stories/number-control.story.d.ts.map +1 -0
  236. package/build-types/validated-form-controls/components/stories/overview.story.d.ts +19 -0
  237. package/build-types/validated-form-controls/components/stories/overview.story.d.ts.map +1 -0
  238. package/build-types/validated-form-controls/components/stories/radio-control.story.d.ts +12 -0
  239. package/build-types/validated-form-controls/components/stories/radio-control.story.d.ts.map +1 -0
  240. package/build-types/validated-form-controls/components/stories/range-control.story.d.ts +9 -0
  241. package/build-types/validated-form-controls/components/stories/range-control.story.d.ts.map +1 -0
  242. package/build-types/validated-form-controls/components/stories/select-control.story.d.ts +12 -0
  243. package/build-types/validated-form-controls/components/stories/select-control.story.d.ts.map +1 -0
  244. package/build-types/validated-form-controls/components/stories/story-utils.d.ts +9 -0
  245. package/build-types/validated-form-controls/components/stories/story-utils.d.ts.map +1 -0
  246. package/build-types/validated-form-controls/components/stories/text-control.story.d.ts +9 -0
  247. package/build-types/validated-form-controls/components/stories/text-control.story.d.ts.map +1 -0
  248. package/build-types/validated-form-controls/components/stories/textarea-control.story.d.ts +9 -0
  249. package/build-types/validated-form-controls/components/stories/textarea-control.story.d.ts.map +1 -0
  250. package/build-types/validated-form-controls/components/stories/toggle-control.story.d.ts +9 -0
  251. package/build-types/validated-form-controls/components/stories/toggle-control.story.d.ts.map +1 -0
  252. package/build-types/validated-form-controls/components/stories/toggle-group-control.story.d.ts +9 -0
  253. package/build-types/validated-form-controls/components/stories/toggle-group-control.story.d.ts.map +1 -0
  254. package/build-types/validated-form-controls/components/text-control.d.ts +8 -0
  255. package/build-types/validated-form-controls/components/text-control.d.ts.map +1 -0
  256. package/build-types/validated-form-controls/components/textarea-control.d.ts +7 -0
  257. package/build-types/validated-form-controls/components/textarea-control.d.ts.map +1 -0
  258. package/build-types/validated-form-controls/components/toggle-control.d.ts +7 -0
  259. package/build-types/validated-form-controls/components/toggle-control.d.ts.map +1 -0
  260. package/build-types/validated-form-controls/components/toggle-group-control.d.ts +15 -0
  261. package/build-types/validated-form-controls/components/toggle-group-control.d.ts.map +1 -0
  262. package/build-types/validated-form-controls/components/types.d.ts +27 -0
  263. package/build-types/validated-form-controls/components/types.d.ts.map +1 -0
  264. package/build-types/validated-form-controls/control-with-error.d.ts +36 -0
  265. package/build-types/validated-form-controls/control-with-error.d.ts.map +1 -0
  266. package/build-types/validated-form-controls/index.d.ts +3 -0
  267. package/build-types/validated-form-controls/index.d.ts.map +1 -0
  268. package/package.json +21 -20
  269. package/src/autocomplete/index.tsx +4 -0
  270. package/src/box-control/input-control.tsx +14 -5
  271. package/src/calendar/date-calendar/README.md +261 -0
  272. package/src/calendar/date-calendar/index.tsx +69 -0
  273. package/src/calendar/date-range-calendar/README.md +298 -0
  274. package/src/calendar/date-range-calendar/index.tsx +215 -0
  275. package/src/calendar/index.tsx +3 -0
  276. package/src/calendar/stories/date-calendar.story.tsx +221 -0
  277. package/src/calendar/stories/date-range-calendar.story.tsx +230 -0
  278. package/src/calendar/style.scss +431 -0
  279. package/src/calendar/test/__utils__/index.ts +56 -0
  280. package/src/calendar/test/date-calendar.tsx +975 -0
  281. package/src/calendar/test/date-range-calendar.tsx +1701 -0
  282. package/src/calendar/types.ts +342 -0
  283. package/src/calendar/utils/constants.ts +62 -0
  284. package/src/calendar/utils/day-cell.tsx +133 -0
  285. package/src/calendar/utils/misc.ts +3 -0
  286. package/src/calendar/utils/use-localization-props.ts +169 -0
  287. package/src/custom-gradient-picker/gradient-bar/control-points.tsx +1 -1
  288. package/src/custom-select-control-v2/custom-select.tsx +6 -3
  289. package/src/date-time/date/index.tsx +1 -1
  290. package/src/dimension-control/test/__snapshots__/index.test.js.snap +8 -8
  291. package/src/form-token-field/index.tsx +12 -1
  292. package/src/form-token-field/token.tsx +1 -1
  293. package/src/icon/index.tsx +2 -0
  294. package/src/mobile/bottom-sheet/cell.native.js +2 -2
  295. package/src/mobile/link-picker/index.native.js +1 -1
  296. package/src/navigation/menu/menu-title-search.tsx +1 -1
  297. package/src/palette-edit/index.tsx +4 -4
  298. package/src/private-apis.ts +5 -0
  299. package/src/select-control/index.tsx +1 -1
  300. package/src/select-control/style.scss +0 -6
  301. package/src/style.scss +3 -2
  302. package/src/toggle-group-control/toggle-group-control/as-button-group.tsx +3 -1
  303. package/src/utils/hooks/use-controlled-value.ts +16 -8
  304. package/src/validated-form-controls/components/checkbox-control.tsx +64 -0
  305. package/src/validated-form-controls/components/combobox-control.tsx +77 -0
  306. package/src/validated-form-controls/components/custom-select-control.tsx +86 -0
  307. package/src/validated-form-controls/components/index.ts +12 -0
  308. package/src/validated-form-controls/components/input-control.tsx +59 -0
  309. package/src/validated-form-controls/components/number-control.tsx +61 -0
  310. package/src/validated-form-controls/components/radio-control.tsx +60 -0
  311. package/src/validated-form-controls/components/range-control.tsx +60 -0
  312. package/src/validated-form-controls/components/select-control.tsx +75 -0
  313. package/src/validated-form-controls/components/stories/checkbox-control.story.tsx +57 -0
  314. package/src/validated-form-controls/components/stories/combobox-control.story.tsx +64 -0
  315. package/src/validated-form-controls/components/stories/custom-select-control.story.tsx +64 -0
  316. package/src/validated-form-controls/components/stories/input-control.story.tsx +132 -0
  317. package/src/validated-form-controls/components/stories/number-control.story.tsx +62 -0
  318. package/src/validated-form-controls/components/stories/overview.mdx +52 -0
  319. package/src/validated-form-controls/components/stories/overview.story.tsx +100 -0
  320. package/src/validated-form-controls/components/stories/radio-control.story.tsx +64 -0
  321. package/src/validated-form-controls/components/stories/range-control.story.tsx +60 -0
  322. package/src/validated-form-controls/components/stories/select-control.story.tsx +60 -0
  323. package/src/validated-form-controls/components/stories/story-utils.tsx +46 -0
  324. package/src/validated-form-controls/components/stories/text-control.story.tsx +55 -0
  325. package/src/validated-form-controls/components/stories/textarea-control.story.tsx +52 -0
  326. package/src/validated-form-controls/components/stories/toggle-control.story.tsx +55 -0
  327. package/src/validated-form-controls/components/stories/toggle-group-control.story.tsx +66 -0
  328. package/src/validated-form-controls/components/text-control.tsx +60 -0
  329. package/src/validated-form-controls/components/textarea-control.tsx +59 -0
  330. package/src/validated-form-controls/components/toggle-control.tsx +69 -0
  331. package/src/validated-form-controls/components/toggle-group-control.tsx +82 -0
  332. package/src/validated-form-controls/components/types.ts +28 -0
  333. package/src/validated-form-controls/control-with-error.tsx +198 -0
  334. package/src/validated-form-controls/index.ts +2 -0
  335. package/src/validated-form-controls/style.scss +75 -0
  336. package/tsconfig.tsbuildinfo +1 -1
  337. package/src/dimension-control/style.scss +0 -22
@@ -0,0 +1,975 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen, within } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
6
+ import {
7
+ startOfDay,
8
+ startOfWeek,
9
+ startOfMonth,
10
+ endOfWeek,
11
+ addDays,
12
+ subDays,
13
+ addWeeks,
14
+ addMonths,
15
+ subMonths,
16
+ subYears,
17
+ addHours,
18
+ } from 'date-fns';
19
+ import { ar } from 'date-fns/locale';
20
+ /**
21
+ * WordPress dependencies
22
+ */
23
+ import { useState } from '@wordpress/element';
24
+ /**
25
+ * Internal dependencies
26
+ */
27
+ import { DateCalendar, TZDate } from '..';
28
+ import {
29
+ getDateButton,
30
+ getDateCell,
31
+ queryDateCell,
32
+ monthNameFormatter,
33
+ } from './__utils__';
34
+ import type { DateCalendarProps } from '../types';
35
+
36
+ const UncontrolledDateCalendar = (
37
+ props: DateCalendarProps & {
38
+ initialSelected?: Date | undefined | null;
39
+ initialMonth?: Date | undefined;
40
+ }
41
+ ) => {
42
+ return (
43
+ <DateCalendar
44
+ { ...props }
45
+ defaultSelected={ props.initialSelected ?? undefined }
46
+ defaultMonth={ props.initialMonth }
47
+ />
48
+ );
49
+ };
50
+
51
+ const ControlledDateCalendar = (
52
+ props: DateCalendarProps & {
53
+ initialSelected?: Date | undefined | null;
54
+ initialMonth?: Date | undefined;
55
+ }
56
+ ) => {
57
+ const [ selected, setSelected ] = useState< Date | undefined | null >(
58
+ props.initialSelected
59
+ );
60
+ const [ month, setMonth ] = useState< Date | undefined >(
61
+ props.initialMonth
62
+ );
63
+ return (
64
+ <DateCalendar
65
+ { ...props }
66
+ selected={ selected ?? null }
67
+ onSelect={ ( ...args ) => {
68
+ setSelected( args[ 0 ] );
69
+ props.onSelect?.( ...args );
70
+ } }
71
+ month={ month }
72
+ onMonthChange={ ( newMonth ) => {
73
+ setMonth( newMonth );
74
+ props.onMonthChange?.( newMonth );
75
+ } }
76
+ />
77
+ );
78
+ };
79
+
80
+ function setupUserEvent() {
81
+ // The `advanceTimersByTime` is needed since we're using jest
82
+ // fake timers to simulate a fixed date for tests.
83
+ const user = userEvent.setup( { advanceTimers: jest.advanceTimersByTime } );
84
+ return user;
85
+ }
86
+
87
+ describe( 'DateCalendar', () => {
88
+ let today: Date;
89
+ let tomorrow: Date;
90
+ let yesterday: Date;
91
+ let currentMonth: Date;
92
+ let nextMonth: Date;
93
+ let nextNextMonth: Date;
94
+ let prevMonth: Date;
95
+ let prevPrevMonth: Date;
96
+
97
+ beforeAll( () => {
98
+ jest.useFakeTimers();
99
+ // For consistent tests, set the system time to a fixed date:
100
+ // Thursday, May 15, 2025, 20:00 UTC
101
+ jest.setSystemTime( 1747339200000 );
102
+ today = startOfDay( new Date() );
103
+ tomorrow = startOfDay( addDays( today, 1 ) );
104
+ yesterday = startOfDay( subDays( today, 1 ) );
105
+ currentMonth = startOfMonth( today );
106
+ nextMonth = startOfMonth( addMonths( today, 1 ) );
107
+ nextNextMonth = startOfMonth( addMonths( today, 2 ) );
108
+ prevMonth = startOfMonth( subMonths( today, 1 ) );
109
+ prevPrevMonth = startOfMonth( subMonths( today, 2 ) );
110
+ } );
111
+
112
+ afterAll( () => {
113
+ jest.useRealTimers();
114
+ } );
115
+
116
+ describe( 'Semantics and basic behavior', () => {
117
+ it( 'should apply the correct roles, semantics and attributes', async () => {
118
+ render( <DateCalendar /> );
119
+
120
+ expect(
121
+ screen.getByRole( 'application', { name: 'Date calendar' } )
122
+ ).toBeVisible();
123
+
124
+ const tableGrid = screen.getByRole( 'grid', {
125
+ name: monthNameFormatter( 'en-US' ).format( today ),
126
+ } );
127
+ expect( tableGrid ).toBeVisible();
128
+ expect( tableGrid ).toHaveAttribute(
129
+ 'aria-multiselectable',
130
+ 'false'
131
+ );
132
+
133
+ const todayButton = getDateButton( today );
134
+ expect( todayButton ).toBeVisible();
135
+ expect( todayButton ).toHaveAccessibleName( /today/i );
136
+ } );
137
+
138
+ it( 'should show multiple months at once via the `numberOfMonths` prop', () => {
139
+ render( <DateCalendar numberOfMonths={ 2 } /> );
140
+
141
+ const grids = screen.getAllByRole( 'grid' );
142
+ expect( grids ).toHaveLength( 2 );
143
+ expect( grids[ 0 ] ).toHaveAccessibleName(
144
+ monthNameFormatter( 'en-US' ).format( today )
145
+ );
146
+ expect( grids[ 1 ] ).toHaveAccessibleName(
147
+ monthNameFormatter( 'en-US' ).format( nextMonth )
148
+ );
149
+ } );
150
+ } );
151
+
152
+ describe( 'Date selection', () => {
153
+ it( 'should select an initial date in uncontrolled mode via the `defaultSelected` prop', () => {
154
+ render( <DateCalendar defaultSelected={ today } /> );
155
+
156
+ expect( getDateCell( today, { selected: true } ) ).toBeVisible();
157
+
158
+ const todayButton = getDateButton( today );
159
+ expect( todayButton ).toBeVisible();
160
+ expect( todayButton ).toHaveAccessibleName( /selected/i );
161
+ } );
162
+
163
+ it( 'should select an initial date in controlled mode via the `selected` prop', () => {
164
+ // Note: the `defaultSelected` prop is ignored when the `selected` prop is set.
165
+ render(
166
+ <DateCalendar defaultSelected={ tomorrow } selected={ today } />
167
+ );
168
+
169
+ expect( getDateCell( today, { selected: true } ) ).toBeVisible();
170
+
171
+ const todayButton = getDateButton( today );
172
+ expect( todayButton ).toBeVisible();
173
+ expect( todayButton ).toHaveAccessibleName( /selected/i );
174
+ } );
175
+
176
+ it( 'should have no date selected in uncontrolled mode when the `selected` and `defaultSelected` props are set to `undefined`', () => {
177
+ render( <DateCalendar /> );
178
+
179
+ expect(
180
+ screen.queryByRole( 'gridcell', { selected: true } )
181
+ ).not.toBeInTheDocument();
182
+ expect(
183
+ screen.queryByRole( 'button', { name: /selected/i } )
184
+ ).not.toBeInTheDocument();
185
+ } );
186
+
187
+ it( 'should have no date selected in controlled mode when the `selected` prop is set to `null`', () => {
188
+ // Note: the `defaultSelected` prop is ignored when the `selected` prop is set.
189
+ render(
190
+ <DateCalendar defaultSelected={ tomorrow } selected={ null } />
191
+ );
192
+
193
+ expect(
194
+ screen.queryByRole( 'gridcell', { selected: true } )
195
+ ).not.toBeInTheDocument();
196
+ expect(
197
+ screen.queryByRole( 'button', { name: /selected/i } )
198
+ ).not.toBeInTheDocument();
199
+ } );
200
+
201
+ it( 'should select a date in uncontrolled mode via the `defaultSelected` prop even if the date is disabled`', () => {
202
+ render(
203
+ <DateCalendar
204
+ defaultSelected={ tomorrow }
205
+ disabled={ tomorrow }
206
+ />
207
+ );
208
+
209
+ expect( getDateCell( tomorrow, { selected: true } ) ).toBeVisible();
210
+
211
+ const tomorrowButton = getDateButton( tomorrow );
212
+ expect( tomorrowButton ).toBeVisible();
213
+ expect( tomorrowButton ).toHaveAccessibleName( /selected/i );
214
+ expect( tomorrowButton ).toBeDisabled();
215
+ } );
216
+
217
+ it( 'should select a date in controlled mode via the `selected` prop even if the date is disabled`', () => {
218
+ render(
219
+ <DateCalendar selected={ tomorrow } disabled={ tomorrow } />
220
+ );
221
+
222
+ expect( getDateCell( tomorrow, { selected: true } ) ).toBeVisible();
223
+
224
+ const tomorrowButton = getDateButton( tomorrow );
225
+ expect( tomorrowButton ).toBeVisible();
226
+ expect( tomorrowButton ).toHaveAccessibleName( /selected/i );
227
+ expect( tomorrowButton ).toBeDisabled();
228
+ } );
229
+
230
+ describe.each( [
231
+ [ 'Uncontrolled', UncontrolledDateCalendar ],
232
+ [ 'Controlled', ControlledDateCalendar ],
233
+ ] )( '[`%s`]', ( _mode, Component ) => {
234
+ it( 'should select a date when a date button is clicked', async () => {
235
+ const user = setupUserEvent();
236
+ const onSelect = jest.fn();
237
+
238
+ render( <Component onSelect={ onSelect } /> );
239
+
240
+ const todayButton = getDateButton( today );
241
+ await user.click( todayButton );
242
+
243
+ expect( onSelect ).toHaveBeenCalledTimes( 1 );
244
+ expect( onSelect ).toHaveBeenCalledWith(
245
+ today,
246
+ today,
247
+ expect.objectContaining( { today: true } ),
248
+ expect.objectContaining( {
249
+ type: 'click',
250
+ target: todayButton,
251
+ } )
252
+ );
253
+
254
+ expect(
255
+ getDateCell( today, { selected: true } )
256
+ ).toBeVisible();
257
+ } );
258
+
259
+ it( 'should not select a disabled date when a date button is clicked', async () => {
260
+ const user = setupUserEvent();
261
+ const onSelect = jest.fn();
262
+
263
+ render(
264
+ <Component onSelect={ onSelect } disabled={ tomorrow } />
265
+ );
266
+
267
+ await user.click( getDateButton( tomorrow ) );
268
+
269
+ expect( onSelect ).not.toHaveBeenCalled();
270
+ expect(
271
+ screen.queryByRole( 'button', { name: /selected/i } )
272
+ ).not.toBeInTheDocument();
273
+ } );
274
+
275
+ it( 'should select a new date when a different date button is clicked', async () => {
276
+ const user = setupUserEvent();
277
+ const onSelect = jest.fn();
278
+
279
+ render(
280
+ <Component
281
+ initialSelected={ today }
282
+ onSelect={ onSelect }
283
+ />
284
+ );
285
+
286
+ const tomorrowButton = getDateButton( tomorrow );
287
+ await user.click( tomorrowButton );
288
+
289
+ expect( onSelect ).toHaveBeenCalledTimes( 1 );
290
+ expect( onSelect ).toHaveBeenCalledWith(
291
+ tomorrow,
292
+ tomorrow,
293
+ expect.objectContaining( { today: false } ),
294
+ expect.objectContaining( {
295
+ type: 'click',
296
+ target: tomorrowButton,
297
+ } )
298
+ );
299
+
300
+ expect(
301
+ getDateCell( tomorrow, { selected: true } )
302
+ ).toBeVisible();
303
+ } );
304
+
305
+ it( 'should de-select the selected date when the selected date button is clicked', async () => {
306
+ const user = setupUserEvent();
307
+ const onSelect = jest.fn();
308
+
309
+ render(
310
+ <Component
311
+ initialSelected={ today }
312
+ onSelect={ onSelect }
313
+ />
314
+ );
315
+
316
+ const todayButton = getDateButton( today );
317
+ await user.click( todayButton );
318
+
319
+ expect( onSelect ).toHaveBeenCalledTimes( 1 );
320
+ expect( onSelect ).toHaveBeenCalledWith(
321
+ undefined,
322
+ today,
323
+ expect.objectContaining( { today: true, selected: true } ),
324
+ expect.objectContaining( {
325
+ type: 'click',
326
+ target: todayButton,
327
+ } )
328
+ );
329
+
330
+ expect(
331
+ queryDateCell( today, { selected: true } )
332
+ ).not.toBeInTheDocument();
333
+ } );
334
+
335
+ it( 'should not de-select the selected date when the selected date button is clicked if the `required` prop is set to `true`', async () => {
336
+ const user = setupUserEvent();
337
+ const onSelect = jest.fn();
338
+
339
+ render(
340
+ <Component
341
+ initialSelected={ today }
342
+ onSelect={ onSelect }
343
+ required
344
+ />
345
+ );
346
+
347
+ const todayButton = getDateButton( today );
348
+ await user.click( todayButton );
349
+
350
+ expect( onSelect ).toHaveBeenCalledTimes( 1 );
351
+ expect( onSelect ).toHaveBeenCalledWith(
352
+ today,
353
+ today,
354
+ expect.objectContaining( { today: true, selected: true } ),
355
+ expect.objectContaining( {
356
+ type: 'click',
357
+ target: todayButton,
358
+ } )
359
+ );
360
+ expect(
361
+ queryDateCell( today, { selected: true } )
362
+ ).toBeVisible();
363
+ } );
364
+ } );
365
+ } );
366
+
367
+ describe( 'Month navigation', () => {
368
+ it( 'should select an initial month in uncontrolled mode via the `defaultMonth` prop', () => {
369
+ render( <DateCalendar defaultMonth={ nextMonth } /> );
370
+
371
+ expect(
372
+ screen.getByRole( 'grid', {
373
+ name: monthNameFormatter( 'en-US' ).format( nextMonth ),
374
+ } )
375
+ ).toBeVisible();
376
+ expect( getDateCell( nextMonth ) ).toBeVisible();
377
+ expect( getDateButton( nextMonth ) ).toBeVisible();
378
+ } );
379
+
380
+ it( 'should select an initial month in controlled mode via the `month` prop', () => {
381
+ render( <DateCalendar month={ nextMonth } /> );
382
+
383
+ expect(
384
+ screen.getByRole( 'grid', {
385
+ name: monthNameFormatter( 'en-US' ).format( nextMonth ),
386
+ } )
387
+ ).toBeVisible();
388
+ expect( getDateCell( nextMonth ) ).toBeVisible();
389
+ expect( getDateButton( nextMonth ) ).toBeVisible();
390
+ } );
391
+
392
+ describe.each( [
393
+ [ 'Uncontrolled', UncontrolledDateCalendar ],
394
+ [ 'Controlled', ControlledDateCalendar ],
395
+ ] )( '[`%s`]', ( _mode, Component ) => {
396
+ it( 'should navigate to the previous and next months when the previous and next month buttons are clicked', async () => {
397
+ const user = setupUserEvent();
398
+ const onMonthChange = jest.fn();
399
+
400
+ render( <Component onMonthChange={ onMonthChange } /> );
401
+
402
+ const prevButton = screen.getByRole( 'button', {
403
+ name: /previous month/i,
404
+ } );
405
+ const nextButton = screen.getByRole( 'button', {
406
+ name: /next month/i,
407
+ } );
408
+ await user.click( prevButton );
409
+
410
+ expect( onMonthChange ).toHaveBeenCalledTimes( 1 );
411
+ expect( onMonthChange ).toHaveBeenCalledWith( prevMonth );
412
+
413
+ expect(
414
+ screen.getByRole( 'grid', {
415
+ name: monthNameFormatter( 'en-US' ).format( prevMonth ),
416
+ } )
417
+ ).toBeVisible();
418
+ expect( getDateCell( prevMonth ) ).toBeVisible();
419
+ expect( getDateButton( prevMonth ) ).toBeVisible();
420
+
421
+ await user.click( nextButton );
422
+
423
+ expect( onMonthChange ).toHaveBeenCalledTimes( 2 );
424
+ expect( onMonthChange ).toHaveBeenCalledWith( currentMonth );
425
+
426
+ expect(
427
+ screen.getByRole( 'grid', {
428
+ name: monthNameFormatter( 'en-US' ).format(
429
+ currentMonth
430
+ ),
431
+ } )
432
+ ).toBeVisible();
433
+ expect( getDateCell( currentMonth ) ).toBeVisible();
434
+ expect( getDateButton( currentMonth ) ).toBeVisible();
435
+
436
+ await user.click( nextButton );
437
+
438
+ expect( onMonthChange ).toHaveBeenCalledTimes( 3 );
439
+ expect( onMonthChange ).toHaveBeenCalledWith( nextMonth );
440
+
441
+ expect(
442
+ screen.getByRole( 'grid', {
443
+ name: monthNameFormatter( 'en-US' ).format( nextMonth ),
444
+ } )
445
+ ).toBeVisible();
446
+ expect( getDateCell( nextMonth ) ).toBeVisible();
447
+ expect( getDateButton( nextMonth ) ).toBeVisible();
448
+ } );
449
+
450
+ it( 'should not navigate to a month that is before the `startMonth` prop', async () => {
451
+ const user = setupUserEvent();
452
+ const onMonthChange = jest.fn();
453
+
454
+ render(
455
+ <Component
456
+ startMonth={ nextMonth }
457
+ onMonthChange={ onMonthChange }
458
+ />
459
+ );
460
+
461
+ const prevButton = screen.getByRole( 'button', {
462
+ name: /previous month/i,
463
+ } );
464
+ const nextButton = screen.getByRole( 'button', {
465
+ name: /next month/i,
466
+ } );
467
+
468
+ expect(
469
+ screen.getByRole( 'grid', {
470
+ name: monthNameFormatter( 'en-US' ).format( nextMonth ),
471
+ } )
472
+ ).toBeVisible();
473
+ expect( getDateCell( nextMonth ) ).toBeVisible();
474
+ expect( getDateButton( nextMonth ) ).toBeVisible();
475
+
476
+ expect( prevButton ).toHaveAttribute( 'aria-disabled', 'true' );
477
+
478
+ await user.click( prevButton );
479
+
480
+ expect( onMonthChange ).not.toHaveBeenCalled();
481
+
482
+ await user.click( nextButton );
483
+
484
+ expect( onMonthChange ).toHaveBeenCalledTimes( 1 );
485
+ expect( onMonthChange ).toHaveBeenCalledWith( nextNextMonth );
486
+
487
+ expect(
488
+ screen.getByRole( 'grid', {
489
+ name: monthNameFormatter( 'en-US' ).format(
490
+ nextNextMonth
491
+ ),
492
+ } )
493
+ ).toBeVisible();
494
+ expect( getDateCell( nextNextMonth ) ).toBeVisible();
495
+ expect( getDateButton( nextNextMonth ) ).toBeVisible();
496
+
497
+ expect( prevButton ).not.toHaveAttribute( 'aria-disabled' );
498
+ } );
499
+
500
+ it( 'should not navigate to a month that is after the `endMonth` prop', async () => {
501
+ const user = setupUserEvent();
502
+ const onMonthChange = jest.fn();
503
+
504
+ render(
505
+ <Component
506
+ endMonth={ prevMonth }
507
+ onMonthChange={ onMonthChange }
508
+ />
509
+ );
510
+
511
+ const prevButton = screen.getByRole( 'button', {
512
+ name: /previous month/i,
513
+ } );
514
+ const nextButton = screen.getByRole( 'button', {
515
+ name: /next month/i,
516
+ } );
517
+
518
+ expect(
519
+ screen.getByRole( 'grid', {
520
+ name: monthNameFormatter( 'en-US' ).format( prevMonth ),
521
+ } )
522
+ ).toBeVisible();
523
+ expect( getDateCell( prevMonth ) ).toBeVisible();
524
+ expect( getDateButton( prevMonth ) ).toBeVisible();
525
+
526
+ expect( nextButton ).toHaveAttribute( 'aria-disabled', 'true' );
527
+
528
+ await user.click( nextButton );
529
+
530
+ expect( onMonthChange ).not.toHaveBeenCalled();
531
+
532
+ await user.click( prevButton );
533
+
534
+ expect( onMonthChange ).toHaveBeenCalledTimes( 1 );
535
+ expect( onMonthChange ).toHaveBeenCalledWith( prevPrevMonth );
536
+
537
+ expect(
538
+ screen.getByRole( 'grid', {
539
+ name: monthNameFormatter( 'en-US' ).format(
540
+ prevPrevMonth
541
+ ),
542
+ } )
543
+ ).toBeVisible();
544
+ expect( getDateCell( prevPrevMonth ) ).toBeVisible();
545
+ expect( getDateButton( prevPrevMonth ) ).toBeVisible();
546
+
547
+ expect( nextButton ).not.toHaveAttribute( 'aria-disabled' );
548
+ } );
549
+ } );
550
+ } );
551
+
552
+ describe( 'Keyboard focus and navigation', () => {
553
+ it( 'should auto-focus the selected day when the `autoFocus` prop is set to `true`', async () => {
554
+ // eslint-disable-next-line jsx-a11y/no-autofocus
555
+ render( <DateCalendar autoFocus defaultSelected={ tomorrow } /> );
556
+ expect( getDateButton( tomorrow ) ).toHaveFocus();
557
+ } );
558
+
559
+ it( "should auto-focus today's date if there is not selected date when the `autoFocus` prop is set to `true`", async () => {
560
+ // eslint-disable-next-line jsx-a11y/no-autofocus
561
+ render( <DateCalendar autoFocus /> );
562
+ expect( getDateButton( today ) ).toHaveFocus();
563
+ } );
564
+
565
+ it( 'should focus each arrow as a tab stop, but treat the grid as a 2d composite widget', async () => {
566
+ const user = setupUserEvent();
567
+ render( <DateCalendar /> );
568
+
569
+ // Focus previous month button
570
+ await user.tab();
571
+ expect(
572
+ screen.getByRole( 'button', { name: /previous month/i } )
573
+ ).toHaveFocus();
574
+
575
+ // Focus next month button
576
+ await user.tab();
577
+ expect(
578
+ screen.getByRole( 'button', { name: /next month/i } )
579
+ ).toHaveFocus();
580
+
581
+ // Focus today button
582
+ await user.tab();
583
+ expect( getDateButton( today ) ).toHaveFocus();
584
+
585
+ // Focus next day
586
+ await user.keyboard( '{ArrowRight}' );
587
+ expect( getDateButton( addDays( today, 1 ) ) ).toHaveFocus();
588
+
589
+ // Focus to next week
590
+ await user.keyboard( '{ArrowDown}' );
591
+ expect( getDateButton( addDays( today, 8 ) ) ).toHaveFocus();
592
+
593
+ // Focus previous day
594
+ await user.keyboard( '{ArrowLeft}' );
595
+ expect( getDateButton( addDays( today, 7 ) ) ).toHaveFocus();
596
+
597
+ // Focus previous week
598
+ await user.keyboard( '{ArrowUp}' );
599
+ expect( getDateButton( today ) ).toHaveFocus();
600
+
601
+ // Focus first day of week
602
+ await user.keyboard( '{Home}' );
603
+ expect( getDateButton( startOfWeek( today ) ) ).toHaveFocus();
604
+
605
+ // Focus last day of week
606
+ await user.keyboard( '{End}' );
607
+ expect( getDateButton( endOfWeek( today ) ) ).toHaveFocus();
608
+
609
+ // Focus previous month
610
+ await user.keyboard( '{PageUp}' );
611
+ expect(
612
+ getDateButton( subMonths( endOfWeek( today ), 1 ) )
613
+ ).toHaveFocus();
614
+
615
+ expect(
616
+ screen.getByRole( 'grid', {
617
+ name: monthNameFormatter( 'en-US' ).format(
618
+ subMonths( endOfWeek( today ), 1 )
619
+ ),
620
+ } )
621
+ ).toBeVisible();
622
+
623
+ // Navigate to next month
624
+ await user.keyboard( '{PageDown}' );
625
+ expect( getDateButton( endOfWeek( today ) ) ).toHaveFocus();
626
+ expect(
627
+ screen.getByRole( 'grid', {
628
+ name: monthNameFormatter( 'en-US' ).format(
629
+ endOfWeek( today )
630
+ ),
631
+ } )
632
+ ).toBeVisible();
633
+
634
+ // Focus previous year
635
+ await user.keyboard( '{Shift>}{PageUp}{/Shift}' );
636
+ expect(
637
+ getDateButton( subYears( endOfWeek( today ), 1 ) )
638
+ ).toHaveFocus();
639
+
640
+ expect(
641
+ screen.getByRole( 'grid', {
642
+ name: monthNameFormatter( 'en-US' ).format(
643
+ subYears( endOfWeek( today ), 1 )
644
+ ),
645
+ } )
646
+ ).toBeVisible();
647
+
648
+ // Focus next year
649
+ await user.keyboard( '{Shift>}{PageDown}{/Shift}' );
650
+ expect( getDateButton( endOfWeek( today ) ) ).toHaveFocus();
651
+
652
+ expect(
653
+ screen.getByRole( 'grid', {
654
+ name: monthNameFormatter( 'en-US' ).format(
655
+ endOfWeek( today )
656
+ ),
657
+ } )
658
+ ).toBeVisible();
659
+ } );
660
+
661
+ // Note: the following test is not testing advanced keyboard interactions
662
+ // (pageUp, pageDown, shift+pageUp, shift+pageDown, home, end)
663
+ it( 'should not focus disabled dates and skip over them when navigating using arrow keys', async () => {
664
+ const user = setupUserEvent();
665
+
666
+ render(
667
+ <DateCalendar
668
+ disabled={ [
669
+ tomorrow,
670
+ addWeeks( addDays( tomorrow, 1 ), 1 ),
671
+ addWeeks( today, 2 ),
672
+ addWeeks( tomorrow, 2 ),
673
+ ] }
674
+ />
675
+ );
676
+
677
+ await user.tab();
678
+ await user.tab();
679
+ await user.tab();
680
+ expect( getDateButton( today ) ).toHaveFocus();
681
+
682
+ await user.keyboard( '{ArrowRight}' );
683
+ expect( getDateButton( addDays( tomorrow, 1 ) ) ).toHaveFocus();
684
+
685
+ await user.keyboard( '{ArrowDown}' );
686
+ expect(
687
+ getDateButton( addWeeks( addDays( tomorrow, 1 ), 2 ) )
688
+ ).toHaveFocus();
689
+
690
+ await user.keyboard( '{ArrowLeft}' );
691
+ expect( getDateButton( addWeeks( yesterday, 2 ) ) ).toHaveFocus();
692
+
693
+ await user.keyboard( '{ArrowUp}' );
694
+ expect( getDateButton( addWeeks( yesterday, 1 ) ) ).toHaveFocus();
695
+ } );
696
+
697
+ it( 'should focus the selected date when tabbing into the calendar', async () => {
698
+ const user = setupUserEvent();
699
+ render( <DateCalendar selected={ tomorrow } /> );
700
+
701
+ // Tab to the calendar grid
702
+ await user.tab();
703
+ await user.tab();
704
+ await user.tab();
705
+
706
+ expect( getDateButton( tomorrow ) ).toHaveFocus();
707
+ } );
708
+ } );
709
+
710
+ describe( 'Disabled states', () => {
711
+ it( 'should support disabling all dates via the `disabled` prop', async () => {
712
+ const user = setupUserEvent();
713
+
714
+ render( <DateCalendar disabled /> );
715
+
716
+ within( screen.getByRole( 'grid' ) )
717
+ .getAllByRole( 'button' )
718
+ .forEach( ( button ) => {
719
+ expect( button ).toBeDisabled();
720
+ } );
721
+
722
+ await user.click(
723
+ screen.getByRole( 'button', { name: /previous/i } )
724
+ );
725
+
726
+ within( screen.getByRole( 'grid' ) )
727
+ .getAllByRole( 'button' )
728
+ .forEach( ( button ) => {
729
+ expect( button ).toBeDisabled();
730
+ } );
731
+
732
+ await user.click( screen.getByRole( 'button', { name: /next/i } ) );
733
+ await user.click( screen.getByRole( 'button', { name: /next/i } ) );
734
+
735
+ within( screen.getByRole( 'grid' ) )
736
+ .getAllByRole( 'button' )
737
+ .forEach( ( button ) => {
738
+ expect( button ).toBeDisabled();
739
+ } );
740
+ } );
741
+
742
+ it( 'should support disabling single dates via the `disabled` prop', async () => {
743
+ render( <DateCalendar disabled={ tomorrow } /> );
744
+
745
+ expect( getDateButton( tomorrow ) ).toBeDisabled();
746
+ } );
747
+
748
+ it( 'should support passing a custom function via the `disabled` prop', async () => {
749
+ const primeNumbers = [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 ];
750
+ render(
751
+ <DateCalendar
752
+ disabled={ ( date ) =>
753
+ primeNumbers.includes( date.getDate() )
754
+ }
755
+ />
756
+ );
757
+
758
+ for ( const date of primeNumbers ) {
759
+ expect(
760
+ getDateButton(
761
+ new Date( today.getFullYear(), today.getMonth(), date )
762
+ )
763
+ ).toBeDisabled();
764
+ }
765
+ } );
766
+
767
+ it( 'should support disabling all dates before a certain date via the `disabled` prop', async () => {
768
+ render( <DateCalendar disabled={ { before: today } } /> );
769
+
770
+ for ( let date = 1; date < today.getDate(); date++ ) {
771
+ expect(
772
+ getDateButton(
773
+ new Date( today.getFullYear(), today.getMonth(), date )
774
+ )
775
+ ).toBeDisabled();
776
+ }
777
+ expect( getDateButton( today ) ).toBeEnabled();
778
+ } );
779
+
780
+ it( 'should support disabling all dates after a certain date via the `disabled` prop', async () => {
781
+ render( <DateCalendar disabled={ { after: today } } /> );
782
+
783
+ for ( let date = today.getDate() + 1; date < 32; date++ ) {
784
+ expect(
785
+ getDateButton(
786
+ new Date( today.getFullYear(), today.getMonth(), date )
787
+ )
788
+ ).toBeDisabled();
789
+ }
790
+ expect( getDateButton( today ) ).toBeEnabled();
791
+ } );
792
+
793
+ it( 'should support disabling all dates before a certain date and after a certain date via the `disabled` prop', async () => {
794
+ render(
795
+ <DateCalendar
796
+ disabled={ {
797
+ before: yesterday,
798
+ after: addDays( today, 1 ),
799
+ } }
800
+ />
801
+ );
802
+
803
+ let date;
804
+
805
+ for ( date = 1; date < today.getDate() - 1; date++ ) {
806
+ expect(
807
+ getDateButton(
808
+ new Date( today.getFullYear(), today.getMonth(), date )
809
+ )
810
+ ).toBeDisabled();
811
+ }
812
+ expect( getDateButton( yesterday ) ).toBeEnabled();
813
+ expect( getDateButton( today ) ).toBeEnabled();
814
+ expect( getDateButton( addDays( today, 1 ) ) ).toBeEnabled();
815
+
816
+ for ( date = today.getDate() + 2; date < 32; date++ ) {
817
+ expect(
818
+ getDateButton(
819
+ new Date( today.getFullYear(), today.getMonth(), date )
820
+ )
821
+ ).toBeDisabled();
822
+ }
823
+ } );
824
+
825
+ it( 'should support disabling all dates within a certain date range via the `disabled` prop', async () => {
826
+ render(
827
+ <DateCalendar
828
+ disabled={ { from: yesterday, to: addDays( today, 1 ) } }
829
+ />
830
+ );
831
+
832
+ let date;
833
+
834
+ for ( date = 1; date < today.getDate() - 1; date++ ) {
835
+ expect(
836
+ getDateButton(
837
+ new Date( today.getFullYear(), today.getMonth(), date )
838
+ )
839
+ ).toBeEnabled();
840
+ }
841
+ expect( getDateButton( yesterday ) ).toBeDisabled();
842
+ expect( getDateButton( today ) ).toBeDisabled();
843
+ expect( getDateButton( addDays( today, 1 ) ) ).toBeDisabled();
844
+
845
+ for ( date = today.getDate() + 2; date < 32; date++ ) {
846
+ expect(
847
+ getDateButton(
848
+ new Date( today.getFullYear(), today.getMonth(), date )
849
+ )
850
+ ).toBeEnabled();
851
+ }
852
+ } );
853
+
854
+ it( 'should support disabling specific days of the week via the `disabled` prop', async () => {
855
+ const weekendsInMay = [ 3, 4, 10, 11, 17, 18, 24, 25, 31 ];
856
+ render( <DateCalendar disabled={ { dayOfWeek: [ 0, 6 ] } } /> );
857
+
858
+ for ( const date of weekendsInMay ) {
859
+ expect(
860
+ getDateButton(
861
+ new Date( today.getFullYear(), today.getMonth(), date )
862
+ )
863
+ ).toBeDisabled();
864
+ }
865
+ } );
866
+
867
+ it( 'should disable the previous and next months buttons if the `disableNavigation` is set to `true`', async () => {
868
+ const user = setupUserEvent();
869
+
870
+ render( <DateCalendar disableNavigation /> );
871
+
872
+ expect(
873
+ screen.getByRole( 'button', { name: /previous month/i } )
874
+ ).toHaveAttribute( 'aria-disabled', 'true' );
875
+ expect(
876
+ screen.getByRole( 'button', { name: /next month/i } )
877
+ ).toHaveAttribute( 'aria-disabled', 'true' );
878
+
879
+ await user.tab();
880
+ expect(
881
+ screen.getByRole( 'button', { name: /today/i } )
882
+ ).toHaveFocus();
883
+ } );
884
+ } );
885
+
886
+ // Note: we're not testing localization of strings. We're only testing
887
+ // that the date formatting, computed dir, and calendar format are correct.
888
+ describe( 'Localization', () => {
889
+ it( 'should localize the calendar based on the `locale` prop', async () => {
890
+ const user = setupUserEvent();
891
+
892
+ render( <DateCalendar locale={ ar } /> );
893
+
894
+ // Check computed writing direction
895
+ expect(
896
+ screen.getByRole( 'application', { name: 'Date calendar' } )
897
+ ).toHaveAttribute( 'dir', 'rtl' );
898
+
899
+ // Check month name
900
+ const grid = screen.getByRole( 'grid', {
901
+ name: monthNameFormatter( 'ar' ).format( today ),
902
+ } );
903
+ expect( grid ).toBeVisible();
904
+
905
+ // Check today button
906
+ expect( getDateButton( today, {}, 'ar' ) ).toHaveAccessibleName(
907
+ /today/i
908
+ );
909
+
910
+ await user.tab();
911
+ await user.tab();
912
+ await user.tab();
913
+ expect( getDateButton( today, {}, 'ar' ) ).toHaveFocus();
914
+
915
+ await user.keyboard( '{Home}' );
916
+ expect(
917
+ getDateButton( startOfWeek( today, { locale: ar } ), {}, 'ar' )
918
+ ).toHaveFocus();
919
+ } );
920
+
921
+ it( 'should support timezones according to the `timeZone` prop', async () => {
922
+ const user = setupUserEvent();
923
+ const onSelect = jest.fn();
924
+
925
+ render(
926
+ <DateCalendar timeZone="Asia/Tokyo" onSelect={ onSelect } />
927
+ );
928
+
929
+ // For someone in Tokyo, the current time simulated in the test
930
+ // (ie. 20:00 UTC) is the next day.
931
+ expect( getDateButton( tomorrow ) ).toHaveAccessibleName(
932
+ /today/i
933
+ );
934
+
935
+ // Select tomorrow's button (which is today in Tokyo)
936
+ const tomorrowButton = getDateButton( tomorrow );
937
+ await user.click( tomorrowButton );
938
+
939
+ const tomorrowFromTokyoTimezone = addHours(
940
+ tomorrow,
941
+ new TZDate( tomorrow, 'Asia/Tokyo' ).getTimezoneOffset() / 60
942
+ );
943
+
944
+ expect( onSelect ).toHaveBeenCalledWith(
945
+ tomorrowFromTokyoTimezone,
946
+ tomorrowFromTokyoTimezone,
947
+ expect.objectContaining( { today: true } ),
948
+ expect.objectContaining( {
949
+ type: 'click',
950
+ target: tomorrowButton,
951
+ } )
952
+ );
953
+ } );
954
+
955
+ it( 'should handle timezoned dates and convert them to the calendar timezone', async () => {
956
+ // Still the same time from UTC's POV, just expressed in Tokyo time.
957
+ const tomorrowAtMidnightInTokyo = new TZDate(
958
+ tomorrow,
959
+ 'Asia/Tokyo'
960
+ );
961
+
962
+ render(
963
+ <DateCalendar
964
+ defaultSelected={ tomorrowAtMidnightInTokyo }
965
+ // Note: using "Etc/GMT+2" instead of "-02:00" because support for raw offsets was introduced in Node v22 (while currently the repository still targets Node v20).
966
+ timeZone="Etc/GMT+2"
967
+ />
968
+ );
969
+
970
+ // Changing the calendar timezone to UTC-2 makes the dates become
971
+ // earlier by 1 day (from midnight to 10pm the previous day).
972
+ expect( getDateCell( today, { selected: true } ) ).toBeVisible();
973
+ } );
974
+ } );
975
+ } );