@wordpress/components 25.9.1 → 25.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (369) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/build/alignment-matrix-control/cell.js +8 -5
  3. package/build/alignment-matrix-control/cell.js.map +1 -1
  4. package/build/alignment-matrix-control/index.js +27 -43
  5. package/build/alignment-matrix-control/index.js.map +1 -1
  6. package/build/alignment-matrix-control/utils.js +29 -9
  7. package/build/alignment-matrix-control/utils.js.map +1 -1
  8. package/build/autocomplete/index.js +104 -52
  9. package/build/autocomplete/index.js.map +1 -1
  10. package/build/circular-option-picker/circular-option-picker-option.js +20 -39
  11. package/build/circular-option-picker/circular-option-picker-option.js.map +1 -1
  12. package/build/circular-option-picker/circular-option-picker.js +11 -32
  13. package/build/circular-option-picker/circular-option-picker.js.map +1 -1
  14. package/build/circular-option-picker/types.js.map +1 -1
  15. package/build/color-palette/index.js +7 -2
  16. package/build/color-palette/index.js.map +1 -1
  17. package/build/color-picker/component.js +12 -2
  18. package/build/color-picker/component.js.map +1 -1
  19. package/build/color-picker/picker.js +77 -1
  20. package/build/color-picker/picker.js.map +1 -1
  21. package/build/color-picker/styles.js +8 -8
  22. package/build/color-picker/styles.js.map +1 -1
  23. package/build/color-picker/types.js.map +1 -1
  24. package/build/composite/v2.js +43 -0
  25. package/build/composite/v2.js.map +1 -0
  26. package/build/confirm-dialog/component.js +74 -8
  27. package/build/confirm-dialog/component.js.map +1 -1
  28. package/build/confirm-dialog/types.js.map +1 -1
  29. package/build/custom-gradient-picker/gradient-bar/control-points.js +13 -4
  30. package/build/custom-gradient-picker/gradient-bar/control-points.js.map +1 -1
  31. package/build/dropdown-menu-v2-ariakit/index.js +217 -0
  32. package/build/dropdown-menu-v2-ariakit/index.js.map +1 -0
  33. package/build/dropdown-menu-v2-ariakit/styles.js +157 -0
  34. package/build/dropdown-menu-v2-ariakit/styles.js.map +1 -0
  35. package/build/dropdown-menu-v2-ariakit/types.js +6 -0
  36. package/build/dropdown-menu-v2-ariakit/types.js.map +1 -0
  37. package/build/font-size-picker/utils.js +1 -1
  38. package/build/font-size-picker/utils.js.map +1 -1
  39. package/build/input-control/styles/input-control-styles.js +23 -23
  40. package/build/input-control/styles/input-control-styles.js.map +1 -1
  41. package/build/mobile/global-styles-context/utils.native.js +1 -1
  42. package/build/mobile/global-styles-context/utils.native.js.map +1 -1
  43. package/build/modal/index.js +45 -16
  44. package/build/modal/index.js.map +1 -1
  45. package/build/palette-edit/index.js +4 -0
  46. package/build/palette-edit/index.js.map +1 -1
  47. package/build/popover/index.js +34 -6
  48. package/build/popover/index.js.map +1 -1
  49. package/build/private-apis.js +18 -2
  50. package/build/private-apis.js.map +1 -1
  51. package/build/progress-bar/styles.js +5 -5
  52. package/build/progress-bar/styles.js.map +1 -1
  53. package/build/sandbox/index.js +1 -1
  54. package/build/sandbox/index.js.map +1 -1
  55. package/build/sandbox/index.native.js +1 -1
  56. package/build/sandbox/index.native.js.map +1 -1
  57. package/build/select-control/styles/select-control-styles.js +8 -8
  58. package/build/select-control/styles/select-control-styles.js.map +1 -1
  59. package/build/slot-fill/bubbles-virtually/slot-fill-provider.js +1 -1
  60. package/build/slot-fill/bubbles-virtually/slot-fill-provider.js.map +1 -1
  61. package/build/tabs/context.js +16 -0
  62. package/build/tabs/context.js.map +1 -0
  63. package/build/tabs/index.js +147 -0
  64. package/build/tabs/index.js.map +1 -0
  65. package/build/tabs/styles.js +38 -0
  66. package/build/tabs/styles.js.map +1 -0
  67. package/build/tabs/tab.js +46 -0
  68. package/build/tabs/tab.js.map +1 -0
  69. package/build/tabs/tablist.js +47 -0
  70. package/build/tabs/tablist.js.map +1 -0
  71. package/build/tabs/tabpanel.js +48 -0
  72. package/build/tabs/tabpanel.js.map +1 -0
  73. package/build/tabs/types.js +6 -0
  74. package/build/tabs/types.js.map +1 -0
  75. package/build/text/component.js +7 -6
  76. package/build/text/component.js.map +1 -1
  77. package/build/text/hook.js +6 -15
  78. package/build/text/hook.js.map +1 -1
  79. package/build/text/index.js.map +1 -1
  80. package/build/text/styles.js +7 -7
  81. package/build/text/styles.js.map +1 -1
  82. package/build/text/types.js.map +1 -1
  83. package/build/text/utils.js +17 -33
  84. package/build/text/utils.js.map +1 -1
  85. package/build/toggle-group-control/toggle-group-control-option-base/component.js +1 -0
  86. package/build/toggle-group-control/toggle-group-control-option-base/component.js.map +1 -1
  87. package/build/toolbar/toolbar/index.js +17 -10
  88. package/build/toolbar/toolbar/index.js.map +1 -1
  89. package/build/toolbar/toolbar/types.js.map +1 -1
  90. package/build/tools-panel/tools-panel-item/hook.js +2 -2
  91. package/build/tools-panel/tools-panel-item/hook.js.map +1 -1
  92. package/build/tools-panel/types.js.map +1 -1
  93. package/build/tooltip/index.js +2 -2
  94. package/build/tooltip/index.js.map +1 -1
  95. package/build/unit-control/utils.js +108 -0
  96. package/build/unit-control/utils.js.map +1 -1
  97. package/build/utils/unit-values.js +1 -1
  98. package/build/utils/unit-values.js.map +1 -1
  99. package/build-module/alignment-matrix-control/cell.js +7 -4
  100. package/build-module/alignment-matrix-control/cell.js.map +1 -1
  101. package/build-module/alignment-matrix-control/index.js +27 -43
  102. package/build-module/alignment-matrix-control/index.js.map +1 -1
  103. package/build-module/alignment-matrix-control/utils.js +29 -8
  104. package/build-module/alignment-matrix-control/utils.js.map +1 -1
  105. package/build-module/autocomplete/index.js +104 -52
  106. package/build-module/autocomplete/index.js.map +1 -1
  107. package/build-module/circular-option-picker/circular-option-picker-option.js +20 -39
  108. package/build-module/circular-option-picker/circular-option-picker-option.js.map +1 -1
  109. package/build-module/circular-option-picker/circular-option-picker.js +10 -31
  110. package/build-module/circular-option-picker/circular-option-picker.js.map +1 -1
  111. package/build-module/circular-option-picker/types.js.map +1 -1
  112. package/build-module/color-palette/index.js +7 -2
  113. package/build-module/color-palette/index.js.map +1 -1
  114. package/build-module/color-picker/component.js +13 -3
  115. package/build-module/color-picker/component.js.map +1 -1
  116. package/build-module/color-picker/picker.js +78 -2
  117. package/build-module/color-picker/picker.js.map +1 -1
  118. package/build-module/color-picker/styles.js +8 -8
  119. package/build-module/color-picker/styles.js.map +1 -1
  120. package/build-module/color-picker/types.js.map +1 -1
  121. package/build-module/composite/v2.js +15 -0
  122. package/build-module/composite/v2.js.map +1 -0
  123. package/build-module/confirm-dialog/component.js +72 -7
  124. package/build-module/confirm-dialog/component.js.map +1 -1
  125. package/build-module/confirm-dialog/types.js.map +1 -1
  126. package/build-module/custom-gradient-picker/gradient-bar/control-points.js +13 -4
  127. package/build-module/custom-gradient-picker/gradient-bar/control-points.js.map +1 -1
  128. package/build-module/dropdown-menu-v2-ariakit/index.js +199 -0
  129. package/build-module/dropdown-menu-v2-ariakit/index.js.map +1 -0
  130. package/build-module/dropdown-menu-v2-ariakit/styles.js +136 -0
  131. package/build-module/dropdown-menu-v2-ariakit/styles.js.map +1 -0
  132. package/build-module/dropdown-menu-v2-ariakit/types.js +2 -0
  133. package/build-module/dropdown-menu-v2-ariakit/types.js.map +1 -0
  134. package/build-module/font-size-picker/utils.js +1 -1
  135. package/build-module/font-size-picker/utils.js.map +1 -1
  136. package/build-module/input-control/styles/input-control-styles.js +23 -23
  137. package/build-module/input-control/styles/input-control-styles.js.map +1 -1
  138. package/build-module/mobile/global-styles-context/utils.native.js +2 -2
  139. package/build-module/mobile/global-styles-context/utils.native.js.map +1 -1
  140. package/build-module/modal/index.js +47 -18
  141. package/build-module/modal/index.js.map +1 -1
  142. package/build-module/palette-edit/index.js +4 -0
  143. package/build-module/palette-edit/index.js.map +1 -1
  144. package/build-module/popover/index.js +34 -6
  145. package/build-module/popover/index.js.map +1 -1
  146. package/build-module/private-apis.js +18 -2
  147. package/build-module/private-apis.js.map +1 -1
  148. package/build-module/progress-bar/styles.js +5 -5
  149. package/build-module/progress-bar/styles.js.map +1 -1
  150. package/build-module/sandbox/index.js +1 -1
  151. package/build-module/sandbox/index.js.map +1 -1
  152. package/build-module/sandbox/index.native.js +1 -1
  153. package/build-module/sandbox/index.native.js.map +1 -1
  154. package/build-module/select-control/styles/select-control-styles.js +8 -8
  155. package/build-module/select-control/styles/select-control-styles.js.map +1 -1
  156. package/build-module/slot-fill/bubbles-virtually/slot-fill-provider.js +1 -1
  157. package/build-module/slot-fill/bubbles-virtually/slot-fill-provider.js.map +1 -1
  158. package/build-module/tabs/context.js +12 -0
  159. package/build-module/tabs/context.js.map +1 -0
  160. package/build-module/tabs/index.js +142 -0
  161. package/build-module/tabs/index.js.map +1 -0
  162. package/build-module/tabs/styles.js +36 -0
  163. package/build-module/tabs/styles.js.map +1 -0
  164. package/build-module/tabs/tab.js +43 -0
  165. package/build-module/tabs/tab.js.map +1 -0
  166. package/build-module/tabs/tablist.js +41 -0
  167. package/build-module/tabs/tablist.js.map +1 -0
  168. package/build-module/tabs/tabpanel.js +43 -0
  169. package/build-module/tabs/tabpanel.js.map +1 -0
  170. package/build-module/tabs/types.js +2 -0
  171. package/build-module/tabs/types.js.map +1 -0
  172. package/build-module/text/component.js +6 -6
  173. package/build-module/text/component.js.map +1 -1
  174. package/build-module/text/hook.js +11 -19
  175. package/build-module/text/hook.js.map +1 -1
  176. package/build-module/text/index.js.map +1 -1
  177. package/build-module/text/styles.js +7 -7
  178. package/build-module/text/styles.js.map +1 -1
  179. package/build-module/text/types.js.map +1 -1
  180. package/build-module/text/utils.js +17 -10
  181. package/build-module/text/utils.js.map +1 -1
  182. package/build-module/toggle-group-control/toggle-group-control-option-base/component.js +1 -0
  183. package/build-module/toggle-group-control/toggle-group-control-option-base/component.js.map +1 -1
  184. package/build-module/toolbar/toolbar/index.js +18 -11
  185. package/build-module/toolbar/toolbar/index.js.map +1 -1
  186. package/build-module/toolbar/toolbar/types.js.map +1 -1
  187. package/build-module/tools-panel/tools-panel-item/hook.js +2 -2
  188. package/build-module/tools-panel/tools-panel-item/hook.js.map +1 -1
  189. package/build-module/tools-panel/types.js.map +1 -1
  190. package/build-module/tooltip/index.js +2 -2
  191. package/build-module/tooltip/index.js.map +1 -1
  192. package/build-module/unit-control/utils.js +108 -0
  193. package/build-module/unit-control/utils.js.map +1 -1
  194. package/build-module/utils/unit-values.js +1 -1
  195. package/build-module/utils/unit-values.js.map +1 -1
  196. package/build-style/style-rtl.css +17 -5
  197. package/build-style/style.css +17 -5
  198. package/build-types/alignment-matrix-control/cell.d.ts +1 -1
  199. package/build-types/alignment-matrix-control/cell.d.ts.map +1 -1
  200. package/build-types/alignment-matrix-control/index.d.ts.map +1 -1
  201. package/build-types/alignment-matrix-control/stories/index.story.d.ts.map +1 -1
  202. package/build-types/alignment-matrix-control/utils.d.ts +9 -9
  203. package/build-types/alignment-matrix-control/utils.d.ts.map +1 -1
  204. package/build-types/autocomplete/index.d.ts.map +1 -1
  205. package/build-types/circular-option-picker/circular-option-picker-option.d.ts.map +1 -1
  206. package/build-types/circular-option-picker/circular-option-picker.d.ts.map +1 -1
  207. package/build-types/circular-option-picker/types.d.ts +4 -6
  208. package/build-types/circular-option-picker/types.d.ts.map +1 -1
  209. package/build-types/color-palette/index.d.ts.map +1 -1
  210. package/build-types/color-picker/component.d.ts.map +1 -1
  211. package/build-types/color-picker/picker.d.ts +1 -1
  212. package/build-types/color-picker/picker.d.ts.map +1 -1
  213. package/build-types/color-picker/styles.d.ts.map +1 -1
  214. package/build-types/color-picker/types.d.ts +3 -0
  215. package/build-types/color-picker/types.d.ts.map +1 -1
  216. package/build-types/composite/v2.d.ts +12 -0
  217. package/build-types/composite/v2.d.ts.map +1 -0
  218. package/build-types/confirm-dialog/component.d.ts +70 -29
  219. package/build-types/confirm-dialog/component.d.ts.map +1 -1
  220. package/build-types/confirm-dialog/stories/index.story.d.ts +11 -0
  221. package/build-types/confirm-dialog/stories/index.story.d.ts.map +1 -0
  222. package/build-types/confirm-dialog/test/index.d.ts +2 -0
  223. package/build-types/confirm-dialog/test/index.d.ts.map +1 -0
  224. package/build-types/confirm-dialog/types.d.ts +32 -10
  225. package/build-types/confirm-dialog/types.d.ts.map +1 -1
  226. package/build-types/custom-gradient-picker/gradient-bar/control-points.d.ts.map +1 -1
  227. package/build-types/dropdown-menu-v2-ariakit/index.d.ts +11 -0
  228. package/build-types/dropdown-menu-v2-ariakit/index.d.ts.map +1 -0
  229. package/build-types/dropdown-menu-v2-ariakit/stories/index.story.d.ts +16 -0
  230. package/build-types/dropdown-menu-v2-ariakit/stories/index.story.d.ts.map +1 -0
  231. package/build-types/dropdown-menu-v2-ariakit/styles.d.ts +88 -0
  232. package/build-types/dropdown-menu-v2-ariakit/styles.d.ts.map +1 -0
  233. package/build-types/dropdown-menu-v2-ariakit/test/index.d.ts +2 -0
  234. package/build-types/dropdown-menu-v2-ariakit/test/index.d.ts.map +1 -0
  235. package/build-types/dropdown-menu-v2-ariakit/types.d.ts +174 -0
  236. package/build-types/dropdown-menu-v2-ariakit/types.d.ts.map +1 -0
  237. package/build-types/font-size-picker/utils.d.ts.map +1 -1
  238. package/build-types/heading/stories/index.story.d.ts.map +1 -1
  239. package/build-types/modal/index.d.ts.map +1 -1
  240. package/build-types/palette-edit/index.d.ts.map +1 -1
  241. package/build-types/popover/index.d.ts +1 -1
  242. package/build-types/popover/index.d.ts.map +1 -1
  243. package/build-types/popover/stories/e2e/index.story.d.ts +1 -1
  244. package/build-types/private-apis.d.ts.map +1 -1
  245. package/build-types/progress-bar/styles.d.ts.map +1 -1
  246. package/build-types/sandbox/index.d.ts.map +1 -1
  247. package/build-types/tabs/context.d.ts +8 -0
  248. package/build-types/tabs/context.d.ts.map +1 -0
  249. package/build-types/tabs/index.d.ts +13 -0
  250. package/build-types/tabs/index.d.ts.map +1 -0
  251. package/build-types/tabs/stories/index.story.d.ts +20 -0
  252. package/build-types/tabs/stories/index.story.d.ts.map +1 -0
  253. package/build-types/tabs/styles.d.ts +17 -0
  254. package/build-types/tabs/styles.d.ts.map +1 -0
  255. package/build-types/tabs/tab.d.ts +10 -0
  256. package/build-types/tabs/tab.d.ts.map +1 -0
  257. package/build-types/tabs/tablist.d.ts +7 -0
  258. package/build-types/tabs/tablist.d.ts.map +1 -0
  259. package/build-types/tabs/tabpanel.d.ts +7 -0
  260. package/build-types/tabs/tabpanel.d.ts.map +1 -0
  261. package/build-types/tabs/test/index.d.ts +2 -0
  262. package/build-types/tabs/test/index.d.ts.map +1 -0
  263. package/build-types/tabs/types.d.ts +134 -0
  264. package/build-types/tabs/types.d.ts.map +1 -0
  265. package/build-types/text/component.d.ts +4 -2
  266. package/build-types/text/component.d.ts.map +1 -1
  267. package/build-types/text/hook.d.ts +171 -165
  268. package/build-types/text/hook.d.ts.map +1 -1
  269. package/build-types/text/index.d.ts +2 -2
  270. package/build-types/text/index.d.ts.map +1 -1
  271. package/build-types/text/stories/index.story.d.ts +21 -0
  272. package/build-types/text/stories/index.story.d.ts.map +1 -0
  273. package/build-types/text/styles.d.ts +7 -7
  274. package/build-types/text/styles.d.ts.map +1 -1
  275. package/build-types/text/types.d.ts +1 -1
  276. package/build-types/text/types.d.ts.map +1 -1
  277. package/build-types/text/utils.d.ts +56 -61
  278. package/build-types/text/utils.d.ts.map +1 -1
  279. package/build-types/toggle-group-control/toggle-group-control-option-base/component.d.ts.map +1 -1
  280. package/build-types/toolbar/stories/index.story.d.ts +5 -0
  281. package/build-types/toolbar/stories/index.story.d.ts.map +1 -1
  282. package/build-types/toolbar/toolbar/index.d.ts.map +1 -1
  283. package/build-types/toolbar/toolbar/types.d.ts +10 -0
  284. package/build-types/toolbar/toolbar/types.d.ts.map +1 -1
  285. package/build-types/tools-panel/tools-panel-item/hook.d.ts.map +1 -1
  286. package/build-types/tools-panel/types.d.ts +2 -0
  287. package/build-types/tools-panel/types.d.ts.map +1 -1
  288. package/build-types/tooltip/index.d.ts.map +1 -1
  289. package/build-types/unit-control/utils.d.ts.map +1 -1
  290. package/package.json +21 -20
  291. package/src/alignment-matrix-control/cell.tsx +6 -2
  292. package/src/alignment-matrix-control/index.tsx +31 -54
  293. package/src/alignment-matrix-control/stories/index.story.tsx +3 -7
  294. package/src/alignment-matrix-control/test/index.tsx +117 -18
  295. package/src/alignment-matrix-control/utils.tsx +33 -9
  296. package/src/autocomplete/index.tsx +136 -77
  297. package/src/button/style.scss +1 -2
  298. package/src/circular-option-picker/circular-option-picker-option.tsx +24 -38
  299. package/src/circular-option-picker/circular-option-picker.tsx +11 -28
  300. package/src/circular-option-picker/types.ts +6 -5
  301. package/src/color-palette/index.tsx +6 -1
  302. package/src/color-picker/component.tsx +25 -3
  303. package/src/color-picker/picker.tsx +96 -2
  304. package/src/color-picker/styles.ts +0 -1
  305. package/src/color-picker/types.ts +3 -0
  306. package/src/composite/v2.ts +22 -0
  307. package/src/confirm-dialog/README.md +1 -1
  308. package/src/confirm-dialog/component.tsx +79 -13
  309. package/src/confirm-dialog/stories/{index.story.js → index.story.tsx} +26 -24
  310. package/src/confirm-dialog/test/{index.js → index.tsx} +3 -3
  311. package/src/confirm-dialog/types.ts +32 -12
  312. package/src/custom-gradient-picker/gradient-bar/control-points.tsx +32 -25
  313. package/src/dimension-control/test/__snapshots__/index.test.js.snap +8 -8
  314. package/src/dropdown-menu-v2-ariakit/README.md +324 -0
  315. package/src/dropdown-menu-v2-ariakit/index.tsx +318 -0
  316. package/src/dropdown-menu-v2-ariakit/stories/index.story.tsx +506 -0
  317. package/src/dropdown-menu-v2-ariakit/styles.ts +297 -0
  318. package/src/dropdown-menu-v2-ariakit/test/index.tsx +1139 -0
  319. package/src/dropdown-menu-v2-ariakit/types.ts +186 -0
  320. package/src/font-size-picker/utils.ts +2 -1
  321. package/src/heading/stories/index.story.tsx +2 -4
  322. package/src/input-control/styles/input-control-styles.tsx +2 -2
  323. package/src/mobile/global-styles-context/utils.native.js +2 -2
  324. package/src/modal/index.tsx +58 -22
  325. package/src/modal/test/index.tsx +29 -0
  326. package/src/notice/style.scss +0 -1
  327. package/src/palette-edit/index.tsx +4 -0
  328. package/src/popover/index.tsx +99 -57
  329. package/src/popover/style.scss +9 -0
  330. package/src/private-apis.ts +31 -1
  331. package/src/progress-bar/styles.ts +19 -4
  332. package/src/sandbox/index.native.js +1 -1
  333. package/src/sandbox/index.tsx +3 -1
  334. package/src/select-control/styles/select-control-styles.ts +2 -2
  335. package/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx +1 -1
  336. package/src/tabs/README.md +242 -0
  337. package/src/tabs/context.ts +13 -0
  338. package/src/tabs/index.tsx +167 -0
  339. package/src/tabs/stories/index.story.tsx +352 -0
  340. package/src/tabs/styles.ts +103 -0
  341. package/src/tabs/tab.tsx +39 -0
  342. package/src/tabs/tablist.tsx +40 -0
  343. package/src/tabs/tabpanel.tsx +42 -0
  344. package/src/tabs/test/index.tsx +1133 -0
  345. package/src/tabs/types.ts +142 -0
  346. package/src/text/README.md +2 -2
  347. package/src/text/{component.js → component.tsx} +10 -6
  348. package/src/text/{hook.js → hook.ts} +12 -15
  349. package/src/text/stories/index.story.tsx +80 -0
  350. package/src/text/types.ts +1 -6
  351. package/src/text/{utils.js → utils.ts} +40 -14
  352. package/src/toggle-group-control/test/__snapshots__/index.tsx.snap +16 -0
  353. package/src/toggle-group-control/toggle-group-control-option-base/component.tsx +1 -0
  354. package/src/toolbar/stories/index.story.tsx +15 -0
  355. package/src/toolbar/test/index.tsx +8 -0
  356. package/src/toolbar/toolbar/README.md +9 -0
  357. package/src/toolbar/toolbar/index.tsx +21 -12
  358. package/src/toolbar/toolbar/style.scss +9 -0
  359. package/src/toolbar/toolbar/types.ts +10 -0
  360. package/src/tools-panel/tools-panel/README.md +3 -0
  361. package/src/tools-panel/tools-panel-item/hook.ts +4 -6
  362. package/src/tools-panel/types.ts +2 -0
  363. package/src/tooltip/index.tsx +2 -3
  364. package/src/unit-control/utils.ts +124 -0
  365. package/src/utils/unit-values.ts +1 -1
  366. package/tsconfig.tsbuildinfo +1 -1
  367. package/src/text/stories/index.story.js +0 -53
  368. /package/src/text/{index.js → index.ts} +0 -0
  369. /package/src/text/{styles.js → styles.ts} +0 -0
@@ -0,0 +1,1133 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen, waitFor } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { wordpress, category, media } from '@wordpress/icons';
11
+ import { useState } from '@wordpress/element';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import Tabs from '..';
17
+ import type { TabsProps } from '../types';
18
+ import type { IconType } from '../../icon';
19
+
20
+ type Tab = {
21
+ id: string;
22
+ title: string;
23
+ content: React.ReactNode;
24
+ tab: {
25
+ className?: string;
26
+ icon?: IconType;
27
+ disabled?: boolean;
28
+ };
29
+ };
30
+
31
+ const TABS: Tab[] = [
32
+ {
33
+ id: 'alpha',
34
+ title: 'Alpha',
35
+ content: 'Selected tab: Alpha',
36
+ tab: { className: 'alpha-class', icon: wordpress },
37
+ },
38
+ {
39
+ id: 'beta',
40
+ title: 'Beta',
41
+ content: 'Selected tab: Beta',
42
+ tab: { className: 'beta-class', icon: category },
43
+ },
44
+ {
45
+ id: 'gamma',
46
+ title: 'Gamma',
47
+ content: 'Selected tab: Gamma',
48
+ tab: { className: 'gamma-class', icon: media },
49
+ },
50
+ ];
51
+
52
+ const TABS_WITH_DELTA: Tab[] = [
53
+ ...TABS,
54
+ {
55
+ id: 'delta',
56
+ title: 'Delta',
57
+ content: 'Selected tab: Delta',
58
+ tab: { className: 'delta-class', icon: media },
59
+ },
60
+ ];
61
+
62
+ const UncontrolledTabs = ( {
63
+ tabs,
64
+ showTabIcons = false,
65
+ ...props
66
+ }: Omit< TabsProps, 'children' > & {
67
+ tabs: Tab[];
68
+ showTabIcons?: boolean;
69
+ } ) => {
70
+ return (
71
+ <Tabs { ...props }>
72
+ <Tabs.TabList>
73
+ { tabs.map( ( tabObj ) => (
74
+ <Tabs.Tab
75
+ key={ tabObj.id }
76
+ id={ tabObj.id }
77
+ className={ tabObj.tab.className }
78
+ disabled={ tabObj.tab.disabled }
79
+ icon={ showTabIcons ? tabObj.tab.icon : undefined }
80
+ >
81
+ { showTabIcons ? null : tabObj.title }
82
+ </Tabs.Tab>
83
+ ) ) }
84
+ </Tabs.TabList>
85
+ { tabs.map( ( tabObj ) => (
86
+ <Tabs.TabPanel key={ tabObj.id } id={ tabObj.id }>
87
+ { tabObj.content }
88
+ </Tabs.TabPanel>
89
+ ) ) }
90
+ </Tabs>
91
+ );
92
+ };
93
+
94
+ const ControlledTabs = ( {
95
+ tabs,
96
+ showTabIcons = false,
97
+ ...props
98
+ }: Omit< TabsProps, 'children' > & {
99
+ tabs: Tab[];
100
+ showTabIcons?: boolean;
101
+ } ) => {
102
+ const [ selectedTabId, setSelectedTabId ] = useState<
103
+ string | undefined | null
104
+ >( props.selectedTabId );
105
+
106
+ return (
107
+ <Tabs
108
+ { ...props }
109
+ selectedTabId={ selectedTabId }
110
+ onSelect={ ( selectedId ) => {
111
+ setSelectedTabId( selectedId );
112
+ props.onSelect?.( selectedId );
113
+ } }
114
+ >
115
+ <Tabs.TabList>
116
+ { tabs.map( ( tabObj ) => (
117
+ <Tabs.Tab
118
+ key={ tabObj.id }
119
+ id={ tabObj.id }
120
+ className={ tabObj.tab.className }
121
+ disabled={ tabObj.tab.disabled }
122
+ icon={ showTabIcons ? tabObj.tab.icon : undefined }
123
+ >
124
+ { showTabIcons ? null : tabObj.title }
125
+ </Tabs.Tab>
126
+ ) ) }
127
+ </Tabs.TabList>
128
+ { tabs.map( ( tabObj ) => (
129
+ <Tabs.TabPanel key={ tabObj.id } id={ tabObj.id }>
130
+ { tabObj.content }
131
+ </Tabs.TabPanel>
132
+ ) ) }
133
+ </Tabs>
134
+ );
135
+ };
136
+
137
+ const getSelectedTab = async () =>
138
+ await screen.findByRole( 'tab', { selected: true } );
139
+
140
+ let originalGetClientRects: () => DOMRectList;
141
+
142
+ describe( 'Tabs', () => {
143
+ beforeAll( () => {
144
+ originalGetClientRects = window.HTMLElement.prototype.getClientRects;
145
+ // Mocking `getClientRects()` is necessary to pass a check performed by
146
+ // the `focus.tabbable.find()` and by the `focus.focusable.find()` functions
147
+ // from the `@wordpress/dom` package.
148
+ // @ts-expect-error We're not trying to comply to the DOM spec, only mocking
149
+ window.HTMLElement.prototype.getClientRects = function () {
150
+ return [ 'trick-jsdom-into-having-size-for-element-rect' ];
151
+ };
152
+ } );
153
+
154
+ afterAll( () => {
155
+ window.HTMLElement.prototype.getClientRects = originalGetClientRects;
156
+ } );
157
+
158
+ describe( 'Accessibility and semantics', () => {
159
+ it( 'should use the correct aria attributes', async () => {
160
+ render( <UncontrolledTabs tabs={ TABS } /> );
161
+
162
+ const tabList = screen.getByRole( 'tablist' );
163
+ const allTabs = screen.getAllByRole( 'tab' );
164
+ const selectedTabPanel = await screen.findByRole( 'tabpanel' );
165
+
166
+ expect( tabList ).toBeVisible();
167
+ expect( tabList ).toHaveAttribute(
168
+ 'aria-orientation',
169
+ 'horizontal'
170
+ );
171
+
172
+ expect( allTabs ).toHaveLength( TABS.length );
173
+
174
+ // The selected `tab` aria-controls the active `tabpanel`,
175
+ // which is `aria-labelledby` the selected `tab`.
176
+ expect( selectedTabPanel ).toBeVisible();
177
+ expect( allTabs[ 0 ] ).toHaveAttribute(
178
+ 'aria-controls',
179
+ selectedTabPanel.getAttribute( 'id' )
180
+ );
181
+ expect( selectedTabPanel ).toHaveAttribute(
182
+ 'aria-labelledby',
183
+ allTabs[ 0 ].getAttribute( 'id' )
184
+ );
185
+ } );
186
+ } );
187
+
188
+ describe( 'Tab Attributes', () => {
189
+ it( "should apply the tab's `className` to the tab button", async () => {
190
+ render( <UncontrolledTabs tabs={ TABS } /> );
191
+
192
+ expect(
193
+ await screen.findByRole( 'tab', { name: 'Alpha' } )
194
+ ).toHaveClass( 'alpha-class' );
195
+ expect( screen.getByRole( 'tab', { name: 'Beta' } ) ).toHaveClass(
196
+ 'beta-class'
197
+ );
198
+ expect( screen.getByRole( 'tab', { name: 'Gamma' } ) ).toHaveClass(
199
+ 'gamma-class'
200
+ );
201
+ } );
202
+ } );
203
+
204
+ describe( 'Tab Activation', () => {
205
+ it( 'defaults to automatic tab activation (pointer clicks)', async () => {
206
+ const user = userEvent.setup();
207
+ const mockOnSelect = jest.fn();
208
+
209
+ render(
210
+ <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
211
+ );
212
+
213
+ // Alpha is the initially selected tab
214
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
215
+ expect(
216
+ await screen.findByRole( 'tabpanel', { name: 'Alpha' } )
217
+ ).toBeInTheDocument();
218
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
219
+
220
+ // Click on Beta, make sure beta is the selected tab
221
+ await user.click( screen.getByRole( 'tab', { name: 'Beta' } ) );
222
+
223
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
224
+ expect(
225
+ screen.getByRole( 'tabpanel', { name: 'Beta' } )
226
+ ).toBeInTheDocument();
227
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
228
+
229
+ // Click on Alpha, make sure beta is the selected tab
230
+ await user.click( screen.getByRole( 'tab', { name: 'Alpha' } ) );
231
+
232
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
233
+ expect(
234
+ screen.getByRole( 'tabpanel', { name: 'Alpha' } )
235
+ ).toBeInTheDocument();
236
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
237
+ } );
238
+
239
+ it( 'defaults to automatic tab activation (arrow keys)', async () => {
240
+ const user = userEvent.setup();
241
+ const mockOnSelect = jest.fn();
242
+
243
+ render(
244
+ <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
245
+ );
246
+
247
+ // onSelect gets called on the initial render. It should be called
248
+ // with the first enabled tab, which is alpha.
249
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
250
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
251
+
252
+ // Tab to focus the tablist. Make sure alpha is focused.
253
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
254
+ expect( await getSelectedTab() ).not.toHaveFocus();
255
+ await user.keyboard( '[Tab]' );
256
+ expect( await getSelectedTab() ).toHaveFocus();
257
+
258
+ // Navigate forward with arrow keys and make sure the Beta tab is
259
+ // selected automatically.
260
+ await user.keyboard( '[ArrowRight]' );
261
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
262
+ expect( await getSelectedTab() ).toHaveFocus();
263
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
264
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
265
+
266
+ // Navigate backwards with arrow keys. Make sure alpha is
267
+ // selected automatically.
268
+ await user.keyboard( '[ArrowLeft]' );
269
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
270
+ expect( await getSelectedTab() ).toHaveFocus();
271
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
272
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
273
+ } );
274
+
275
+ it( 'wraps around the last/first tab when using arrow keys', async () => {
276
+ const user = userEvent.setup();
277
+ const mockOnSelect = jest.fn();
278
+
279
+ render(
280
+ <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
281
+ );
282
+
283
+ // onSelect gets called on the initial render.
284
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
285
+
286
+ // Tab to focus the tablist. Make sure Alpha is focused.
287
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
288
+ expect( await getSelectedTab() ).not.toHaveFocus();
289
+ await user.keyboard( '[Tab]' );
290
+ expect( await getSelectedTab() ).toHaveFocus();
291
+
292
+ // Navigate backwards with arrow keys and make sure that the Gamma tab
293
+ // (the last tab) is selected automatically.
294
+ await user.keyboard( '[ArrowLeft]' );
295
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
296
+ expect( await getSelectedTab() ).toHaveFocus();
297
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
298
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
299
+
300
+ // Navigate forward with arrow keys. Make sure alpha (the first tab) is
301
+ // selected automatically.
302
+ await user.keyboard( '[ArrowRight]' );
303
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
304
+ expect( await getSelectedTab() ).toHaveFocus();
305
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
306
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
307
+ } );
308
+
309
+ it( 'should not move tab selection when pressing the up/down arrow keys, unless the orientation is changed to `vertical`', async () => {
310
+ const user = userEvent.setup();
311
+ const mockOnSelect = jest.fn();
312
+
313
+ const { rerender } = render(
314
+ <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
315
+ );
316
+
317
+ // onSelect gets called on the initial render. It should be called
318
+ // with the first enabled tab, which is alpha.
319
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
320
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
321
+
322
+ // Tab to focus the tablist. Make sure alpha is focused.
323
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
324
+ expect( await getSelectedTab() ).not.toHaveFocus();
325
+ await user.keyboard( '[Tab]' );
326
+ expect( await getSelectedTab() ).toHaveFocus();
327
+
328
+ // Press the arrow up key, nothing happens.
329
+ await user.keyboard( '[ArrowUp]' );
330
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
331
+ expect( await getSelectedTab() ).toHaveFocus();
332
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
333
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
334
+
335
+ // Press the arrow down key, nothing happens
336
+ await user.keyboard( '[ArrowDown]' );
337
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
338
+ expect( await getSelectedTab() ).toHaveFocus();
339
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
340
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
341
+
342
+ // Change orientation to `vertical`. When the orientation is vertical,
343
+ // left/right arrow keys are replaced by up/down arrow keys.
344
+ rerender(
345
+ <UncontrolledTabs
346
+ tabs={ TABS }
347
+ onSelect={ mockOnSelect }
348
+ orientation="vertical"
349
+ />
350
+ );
351
+
352
+ expect( screen.getByRole( 'tablist' ) ).toHaveAttribute(
353
+ 'aria-orientation',
354
+ 'vertical'
355
+ );
356
+
357
+ // Make sure alpha is still focused.
358
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
359
+ expect( await getSelectedTab() ).toHaveFocus();
360
+
361
+ // Navigate forward with arrow keys and make sure the Beta tab is
362
+ // selected automatically.
363
+ await user.keyboard( '[ArrowDown]' );
364
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
365
+ expect( await getSelectedTab() ).toHaveFocus();
366
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
367
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
368
+
369
+ // Navigate backwards with arrow keys. Make sure alpha is
370
+ // selected automatically.
371
+ await user.keyboard( '[ArrowUp]' );
372
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
373
+ expect( await getSelectedTab() ).toHaveFocus();
374
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
375
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
376
+
377
+ // Navigate backwards with arrow keys. Make sure alpha is
378
+ // selected automatically.
379
+ await user.keyboard( '[ArrowUp]' );
380
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
381
+ expect( await getSelectedTab() ).toHaveFocus();
382
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 4 );
383
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
384
+
385
+ // Navigate backwards with arrow keys. Make sure alpha is
386
+ // selected automatically.
387
+ await user.keyboard( '[ArrowDown]' );
388
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
389
+ expect( await getSelectedTab() ).toHaveFocus();
390
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 5 );
391
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
392
+ } );
393
+
394
+ it( 'should move focus on a tab even if disabled with arrow key, but not with pointer clicks', async () => {
395
+ const user = userEvent.setup();
396
+ const mockOnSelect = jest.fn();
397
+
398
+ const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( ( tabObj ) =>
399
+ tabObj.id === 'delta'
400
+ ? {
401
+ ...tabObj,
402
+ tab: {
403
+ ...tabObj.tab,
404
+ disabled: true,
405
+ },
406
+ }
407
+ : tabObj
408
+ );
409
+
410
+ render(
411
+ <UncontrolledTabs
412
+ tabs={ TABS_WITH_DELTA_DISABLED }
413
+ onSelect={ mockOnSelect }
414
+ />
415
+ );
416
+
417
+ // onSelect gets called on the initial render. It should be called
418
+ // with the first enabled tab, which is alpha.
419
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
420
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
421
+
422
+ // Tab to focus the tablist. Make sure Alpha is focused.
423
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
424
+ expect( await getSelectedTab() ).not.toHaveFocus();
425
+ await user.keyboard( '[Tab]' );
426
+ expect( await getSelectedTab() ).toHaveFocus();
427
+ // Confirm onSelect has not been re-called
428
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
429
+
430
+ // Press the right arrow key three times. Since the delta tab is disabled:
431
+ // - it won't be selected. The gamma tab will be selected instead, since
432
+ // it was the tab that was last selected before delta. Therefore, the
433
+ // `mockOnSelect` function gets called only twice (and not three times)
434
+ // - it will receive focus, when using arrow keys
435
+ await user.keyboard( '[ArrowRight][ArrowRight][ArrowRight]' );
436
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
437
+ expect(
438
+ screen.getByRole( 'tab', { name: 'Delta' } )
439
+ ).toHaveFocus();
440
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
441
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
442
+
443
+ // Navigate backwards with arrow keys. The gamma tab receives focus.
444
+ // The `mockOnSelect` callback doesn't fire, since the gamma tab was
445
+ // already selected.
446
+ await user.keyboard( '[ArrowLeft]' );
447
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
448
+ expect( await getSelectedTab() ).toHaveFocus();
449
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
450
+
451
+ // Click on the disabled tab. Compared to using arrow keys to move the
452
+ // focus, disabled tabs ignore pointer clicks — and therefore, they don't
453
+ // receive focus, nor they cause the `mockOnSelect` function to fire.
454
+ await user.click( screen.getByRole( 'tab', { name: 'Delta' } ) );
455
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
456
+ expect( await getSelectedTab() ).toHaveFocus();
457
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
458
+ } );
459
+
460
+ it( 'should not focus the next tab when the Tab key is pressed', async () => {
461
+ const user = userEvent.setup();
462
+
463
+ render( <UncontrolledTabs tabs={ TABS } /> );
464
+
465
+ // Tab should initially focus the first tab in the tablist, which
466
+ // is Alpha.
467
+ await user.keyboard( '[Tab]' );
468
+ expect(
469
+ await screen.findByRole( 'tab', { name: 'Alpha' } )
470
+ ).toHaveFocus();
471
+
472
+ // Because all other tabs should have `tabindex=-1`, pressing Tab
473
+ // should NOT move the focus to the next tab, which is Beta.
474
+ // Instead, focus should go to the currently selected tabpanel (alpha).
475
+ await user.keyboard( '[Tab]' );
476
+ expect(
477
+ await screen.findByRole( 'tabpanel', {
478
+ name: 'Alpha',
479
+ } )
480
+ ).toHaveFocus();
481
+ } );
482
+
483
+ it( 'switches to manual tab activation when the `selectOnMove` prop is set to `false`', async () => {
484
+ const user = userEvent.setup();
485
+ const mockOnSelect = jest.fn();
486
+
487
+ render(
488
+ <UncontrolledTabs
489
+ tabs={ TABS }
490
+ onSelect={ mockOnSelect }
491
+ selectOnMove={ false }
492
+ />
493
+ );
494
+
495
+ // onSelect gets called on the initial render. It should be called
496
+ // with the first enabled tab, which is alpha.
497
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
498
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
499
+
500
+ // Click on Alpha and make sure it is selected.
501
+ // onSelect shouldn't fire since the selected tab didn't change.
502
+ await user.click( screen.getByRole( 'tab', { name: 'Alpha' } ) );
503
+ expect(
504
+ await screen.findByRole( 'tab', { name: 'Alpha' } )
505
+ ).toHaveFocus();
506
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
507
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
508
+
509
+ // Navigate forward with arrow keys. Make sure Beta is focused, but
510
+ // that the tab selection happens only when pressing the spacebar
511
+ // or enter key. onSelect shouldn't fire since the selected tab
512
+ // didn't change.
513
+ await user.keyboard( '[ArrowRight]' );
514
+ expect(
515
+ await screen.findByRole( 'tab', { name: 'Beta' } )
516
+ ).toHaveFocus();
517
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
518
+
519
+ await user.keyboard( '[Enter]' );
520
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
521
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
522
+
523
+ // Navigate forward with arrow keys. Make sure Gamma (last tab) is
524
+ // focused, but that tab selection happens only when pressing the
525
+ // spacebar or enter key. onSelect shouldn't fire since the selected
526
+ // tab didn't change.
527
+ await user.keyboard( '[ArrowRight]' );
528
+ expect(
529
+ await screen.findByRole( 'tab', { name: 'Gamma' } )
530
+ ).toHaveFocus();
531
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
532
+ expect(
533
+ screen.getByRole( 'tab', { name: 'Gamma' } )
534
+ ).toHaveFocus();
535
+
536
+ await user.keyboard( '[Space]' );
537
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
538
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
539
+ } );
540
+ } );
541
+ describe( 'Uncontrolled mode', () => {
542
+ describe( 'Without `initialTabId` prop', () => {
543
+ it( 'should render first tab', async () => {
544
+ render( <UncontrolledTabs tabs={ TABS } /> );
545
+
546
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
547
+ expect(
548
+ await screen.findByRole( 'tabpanel', { name: 'Alpha' } )
549
+ ).toBeInTheDocument();
550
+ } );
551
+ it( 'should fall back to first enabled tab if the active tab is removed', async () => {
552
+ const { rerender } = render(
553
+ <UncontrolledTabs tabs={ TABS } />
554
+ );
555
+
556
+ // Remove first item from `TABS` array
557
+ rerender( <UncontrolledTabs tabs={ TABS.slice( 1 ) } /> );
558
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
559
+ } );
560
+ it( 'should not load any tab if the active tab is removed and there are no enabled tabs', async () => {
561
+ const TABS_WITH_BETA_GAMMA_DISABLED = TABS.map( ( tabObj ) =>
562
+ tabObj.id !== 'alpha'
563
+ ? {
564
+ ...tabObj,
565
+ tab: {
566
+ ...tabObj.tab,
567
+ disabled: true,
568
+ },
569
+ }
570
+ : tabObj
571
+ );
572
+
573
+ const { rerender } = render(
574
+ <UncontrolledTabs tabs={ TABS_WITH_BETA_GAMMA_DISABLED } />
575
+ );
576
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
577
+
578
+ // Remove alpha
579
+ rerender(
580
+ <UncontrolledTabs
581
+ tabs={ TABS_WITH_BETA_GAMMA_DISABLED.slice( 1 ) }
582
+ />
583
+ );
584
+
585
+ // No tab should be selected i.e. it doesn't fall back to first tab.
586
+ await waitFor( () =>
587
+ expect(
588
+ screen.queryByRole( 'tab', { selected: true } )
589
+ ).not.toBeInTheDocument()
590
+ );
591
+
592
+ // No tabpanel should be rendered either
593
+ expect(
594
+ screen.queryByRole( 'tabpanel' )
595
+ ).not.toBeInTheDocument();
596
+ } );
597
+ } );
598
+
599
+ describe( 'With `initialTabId`', () => {
600
+ it( 'should render the tab set by `initialTabId` prop', async () => {
601
+ render(
602
+ <UncontrolledTabs tabs={ TABS } initialTabId="beta" />
603
+ );
604
+
605
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
606
+ } );
607
+
608
+ it( 'should not select a tab when `initialTabId` does not match any known tab', () => {
609
+ render(
610
+ <UncontrolledTabs
611
+ tabs={ TABS }
612
+ initialTabId="does-not-exist"
613
+ />
614
+ );
615
+
616
+ // No tab should be selected i.e. it doesn't fall back to first tab.
617
+ expect(
618
+ screen.queryByRole( 'tab', { selected: true } )
619
+ ).not.toBeInTheDocument();
620
+
621
+ // No tabpanel should be rendered either
622
+ expect(
623
+ screen.queryByRole( 'tabpanel' )
624
+ ).not.toBeInTheDocument();
625
+ } );
626
+ it( 'should not change tabs when initialTabId is changed', async () => {
627
+ const { rerender } = render(
628
+ <UncontrolledTabs tabs={ TABS } initialTabId="beta" />
629
+ );
630
+
631
+ rerender(
632
+ <UncontrolledTabs tabs={ TABS } initialTabId="alpha" />
633
+ );
634
+
635
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
636
+ } );
637
+
638
+ it( 'should fall back to the tab associated to `initialTabId` if the currently active tab is removed', async () => {
639
+ const user = userEvent.setup();
640
+ const mockOnSelect = jest.fn();
641
+
642
+ const { rerender } = render(
643
+ <UncontrolledTabs
644
+ tabs={ TABS }
645
+ initialTabId="gamma"
646
+ onSelect={ mockOnSelect }
647
+ />
648
+ );
649
+
650
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
651
+
652
+ await user.click(
653
+ screen.getByRole( 'tab', { name: 'Alpha' } )
654
+ );
655
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
656
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
657
+
658
+ rerender(
659
+ <UncontrolledTabs
660
+ tabs={ TABS.slice( 1 ) }
661
+ initialTabId="gamma"
662
+ onSelect={ mockOnSelect }
663
+ />
664
+ );
665
+
666
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
667
+ } );
668
+
669
+ it( 'should fall back to the tab associated to `initialTabId` if the currently active tab becomes disabled', async () => {
670
+ const user = userEvent.setup();
671
+ const mockOnSelect = jest.fn();
672
+
673
+ const { rerender } = render(
674
+ <UncontrolledTabs
675
+ tabs={ TABS }
676
+ initialTabId="gamma"
677
+ onSelect={ mockOnSelect }
678
+ />
679
+ );
680
+
681
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
682
+
683
+ await user.click(
684
+ screen.getByRole( 'tab', { name: 'Alpha' } )
685
+ );
686
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
687
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
688
+
689
+ const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) =>
690
+ tabObj.id === 'alpha'
691
+ ? {
692
+ ...tabObj,
693
+ tab: {
694
+ ...tabObj.tab,
695
+ disabled: true,
696
+ },
697
+ }
698
+ : tabObj
699
+ );
700
+
701
+ rerender(
702
+ <UncontrolledTabs
703
+ tabs={ TABS_WITH_ALPHA_DISABLED }
704
+ initialTabId="gamma"
705
+ onSelect={ mockOnSelect }
706
+ />
707
+ );
708
+
709
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
710
+ } );
711
+
712
+ it( 'should have no active tabs when the tab associated to `initialTabId` is removed while being the active tab', async () => {
713
+ const { rerender } = render(
714
+ <UncontrolledTabs tabs={ TABS } initialTabId="gamma" />
715
+ );
716
+
717
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
718
+
719
+ // Remove gamma
720
+ rerender(
721
+ <UncontrolledTabs
722
+ tabs={ TABS.slice( 0, 2 ) }
723
+ initialTabId="gamma"
724
+ />
725
+ );
726
+
727
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength( 2 );
728
+ // No tab should be selected i.e. it doesn't fall back to first tab.
729
+ expect(
730
+ screen.queryByRole( 'tab', { selected: true } )
731
+ ).not.toBeInTheDocument();
732
+ // No tabpanel should be rendered either
733
+ expect(
734
+ screen.queryByRole( 'tabpanel' )
735
+ ).not.toBeInTheDocument();
736
+ } );
737
+
738
+ it( 'waits for the tab with the `initialTabId` to be present in the `tabs` array before selecting it', async () => {
739
+ const { rerender } = render(
740
+ <UncontrolledTabs tabs={ TABS } initialTabId="delta" />
741
+ );
742
+
743
+ // There should be no selected tab yet.
744
+ expect(
745
+ screen.queryByRole( 'tab', { selected: true } )
746
+ ).not.toBeInTheDocument();
747
+
748
+ rerender(
749
+ <UncontrolledTabs
750
+ tabs={ TABS_WITH_DELTA }
751
+ initialTabId="delta"
752
+ />
753
+ );
754
+
755
+ expect( await getSelectedTab() ).toHaveTextContent( 'Delta' );
756
+ } );
757
+ } );
758
+
759
+ describe( 'Disabled tab', () => {
760
+ it( 'should disable the tab when `disabled` is `true`', async () => {
761
+ const user = userEvent.setup();
762
+ const mockOnSelect = jest.fn();
763
+
764
+ const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map(
765
+ ( tabObj ) =>
766
+ tabObj.id === 'delta'
767
+ ? {
768
+ ...tabObj,
769
+ tab: {
770
+ ...tabObj.tab,
771
+ disabled: true,
772
+ },
773
+ }
774
+ : tabObj
775
+ );
776
+
777
+ render(
778
+ <UncontrolledTabs
779
+ tabs={ TABS_WITH_DELTA_DISABLED }
780
+ onSelect={ mockOnSelect }
781
+ />
782
+ );
783
+
784
+ expect(
785
+ screen.getByRole( 'tab', { name: 'Delta' } )
786
+ ).toHaveAttribute( 'aria-disabled', 'true' );
787
+
788
+ // onSelect gets called on the initial render.
789
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
790
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
791
+
792
+ // onSelect should not be called since the disabled tab is
793
+ // highlighted, but not selected.
794
+ await user.keyboard( '[Tab]' );
795
+ await user.keyboard( '[ArrowLeft]' );
796
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
797
+
798
+ // Delta (which is disabled) has focus
799
+ expect(
800
+ screen.getByRole( 'tab', { name: 'Delta' } )
801
+ ).toHaveFocus();
802
+
803
+ // Alpha retains the selection, even if it's not focused.
804
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
805
+ } );
806
+
807
+ it( 'should select first enabled tab when the initial tab is disabled', async () => {
808
+ const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) =>
809
+ tabObj.id === 'alpha'
810
+ ? {
811
+ ...tabObj,
812
+ tab: {
813
+ ...tabObj.tab,
814
+ disabled: true,
815
+ },
816
+ }
817
+ : tabObj
818
+ );
819
+
820
+ const { rerender } = render(
821
+ <UncontrolledTabs tabs={ TABS_WITH_ALPHA_DISABLED } />
822
+ );
823
+
824
+ // As alpha (first tab) is disabled,
825
+ // the first enabled tab should be beta.
826
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
827
+
828
+ // Re-enable all tabs
829
+ rerender( <UncontrolledTabs tabs={ TABS } /> );
830
+
831
+ // Even if the initial tab becomes enabled again, the selected
832
+ // tab doesn't change.
833
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
834
+ } );
835
+
836
+ it( 'should select first enabled tab when the tab associated to `initialTabId` is disabled', async () => {
837
+ const TABS_ONLY_GAMMA_ENABLED = TABS.map( ( tabObj ) =>
838
+ tabObj.id !== 'gamma'
839
+ ? {
840
+ ...tabObj,
841
+ tab: {
842
+ ...tabObj.tab,
843
+ disabled: true,
844
+ },
845
+ }
846
+ : tabObj
847
+ );
848
+ const { rerender } = render(
849
+ <UncontrolledTabs
850
+ tabs={ TABS_ONLY_GAMMA_ENABLED }
851
+ initialTabId="beta"
852
+ />
853
+ );
854
+
855
+ // As alpha (first tab), and beta (the initial tab), are both
856
+ // disabled the first enabled tab should be gamma.
857
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
858
+
859
+ // Re-enable all tabs
860
+ rerender(
861
+ <UncontrolledTabs tabs={ TABS } initialTabId="beta" />
862
+ );
863
+
864
+ // Even if the initial tab becomes enabled again, the selected tab doesn't
865
+ // change.
866
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
867
+ } );
868
+
869
+ it( 'should select the first enabled tab when the selected tab becomes disabled', async () => {
870
+ const mockOnSelect = jest.fn();
871
+ const { rerender } = render(
872
+ <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
873
+ );
874
+
875
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
876
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
877
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
878
+
879
+ const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) =>
880
+ tabObj.id === 'alpha'
881
+ ? {
882
+ ...tabObj,
883
+ tab: {
884
+ ...tabObj.tab,
885
+ disabled: true,
886
+ },
887
+ }
888
+ : tabObj
889
+ );
890
+
891
+ // Disable alpha
892
+ rerender(
893
+ <UncontrolledTabs
894
+ tabs={ TABS_WITH_ALPHA_DISABLED }
895
+ onSelect={ mockOnSelect }
896
+ />
897
+ );
898
+
899
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
900
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
901
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
902
+
903
+ // Re-enable all tabs
904
+ rerender(
905
+ <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
906
+ );
907
+
908
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
909
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
910
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
911
+ } );
912
+
913
+ it( 'should select the first enabled tab when the tab associated to `initialTabId` becomes disabled while being the active tab', async () => {
914
+ const mockOnSelect = jest.fn();
915
+
916
+ const { rerender } = render(
917
+ <UncontrolledTabs
918
+ tabs={ TABS }
919
+ onSelect={ mockOnSelect }
920
+ initialTabId="gamma"
921
+ />
922
+ );
923
+
924
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
925
+
926
+ const TABS_WITH_GAMMA_DISABLED = TABS.map( ( tabObj ) =>
927
+ tabObj.id === 'gamma'
928
+ ? {
929
+ ...tabObj,
930
+ tab: {
931
+ ...tabObj.tab,
932
+ disabled: true,
933
+ },
934
+ }
935
+ : tabObj
936
+ );
937
+
938
+ // Disable gamma
939
+ rerender(
940
+ <UncontrolledTabs
941
+ tabs={ TABS_WITH_GAMMA_DISABLED }
942
+ onSelect={ mockOnSelect }
943
+ initialTabId="gamma"
944
+ />
945
+ );
946
+
947
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
948
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
949
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
950
+
951
+ // Re-enable all tabs
952
+ rerender(
953
+ <UncontrolledTabs
954
+ tabs={ TABS }
955
+ onSelect={ mockOnSelect }
956
+ initialTabId="gamma"
957
+ />
958
+ );
959
+
960
+ // Confirm that alpha is still selected, and that onSelect has
961
+ // not been called again.
962
+ expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
963
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
964
+ } );
965
+ } );
966
+ } );
967
+
968
+ describe( 'Controlled mode', () => {
969
+ it( 'should render the tab specified by the `selectedTabId` prop', async () => {
970
+ render( <ControlledTabs tabs={ TABS } selectedTabId="beta" /> );
971
+
972
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
973
+ expect(
974
+ await screen.findByRole( 'tabpanel', { name: 'Beta' } )
975
+ ).toBeInTheDocument();
976
+ } );
977
+ it( 'should render the specified `selectedTabId`, and ignore the `initialTabId` prop', async () => {
978
+ render(
979
+ <ControlledTabs
980
+ tabs={ TABS }
981
+ selectedTabId="gamma"
982
+ initialTabId="beta"
983
+ />
984
+ );
985
+
986
+ expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
987
+ } );
988
+ it( 'should not render any tab if `selectedTabId` does not match any known tab', async () => {
989
+ render(
990
+ <ControlledTabs
991
+ tabs={ TABS_WITH_DELTA }
992
+ selectedTabId="does-not-exist"
993
+ />
994
+ );
995
+
996
+ // No tab should be selected i.e. it doesn't fall back to first tab.
997
+ // `waitFor` is needed here to prevent testing library from
998
+ // throwing a 'not wrapped in `act()`' error.
999
+ await waitFor( () =>
1000
+ expect(
1001
+ screen.queryByRole( 'tab', { selected: true } )
1002
+ ).not.toBeInTheDocument()
1003
+ );
1004
+ // No tabpanel should be rendered either
1005
+ expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument();
1006
+ } );
1007
+ it( 'should not render any tab if the active tab is removed', async () => {
1008
+ const { rerender } = render(
1009
+ <ControlledTabs tabs={ TABS } selectedTabId="beta" />
1010
+ );
1011
+
1012
+ // Remove beta
1013
+ rerender(
1014
+ <ControlledTabs
1015
+ tabs={ TABS.filter( ( tab ) => tab.id !== 'beta' ) }
1016
+ selectedTabId="beta"
1017
+ />
1018
+ );
1019
+
1020
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength( 2 );
1021
+
1022
+ // No tab should be selected i.e. it doesn't fall back to first tab.
1023
+ // `waitFor` is needed here to prevent testing library from
1024
+ // throwing a 'not wrapped in `act()`' error.
1025
+ await waitFor( () =>
1026
+ expect(
1027
+ screen.queryByRole( 'tab', { selected: true } )
1028
+ ).not.toBeInTheDocument()
1029
+ );
1030
+ // No tabpanel should be rendered either
1031
+ expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument();
1032
+
1033
+ // Restore beta
1034
+ rerender( <ControlledTabs tabs={ TABS } selectedTabId="beta" /> );
1035
+
1036
+ // No tab should be selected i.e. it doesn't reselect the previously
1037
+ // removed tab.
1038
+ expect(
1039
+ screen.queryByRole( 'tab', { selected: true } )
1040
+ ).not.toBeInTheDocument();
1041
+ // No tabpanel should be rendered either
1042
+ expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument();
1043
+ } );
1044
+
1045
+ describe( 'Disabled tab', () => {
1046
+ it( 'should not render any tab if `selectedTabId` refers to a disabled tab', async () => {
1047
+ const TABS_WITH_DELTA_WITH_BETA_DISABLED = TABS_WITH_DELTA.map(
1048
+ ( tabObj ) =>
1049
+ tabObj.id === 'beta'
1050
+ ? {
1051
+ ...tabObj,
1052
+ tab: {
1053
+ ...tabObj.tab,
1054
+ disabled: true,
1055
+ },
1056
+ }
1057
+ : tabObj
1058
+ );
1059
+
1060
+ render(
1061
+ <ControlledTabs
1062
+ tabs={ TABS_WITH_DELTA_WITH_BETA_DISABLED }
1063
+ selectedTabId="beta"
1064
+ />
1065
+ );
1066
+
1067
+ // No tab should be selected i.e. it doesn't fall back to first tab.
1068
+ await waitFor( () => {
1069
+ expect(
1070
+ screen.queryByRole( 'tab', { selected: true } )
1071
+ ).not.toBeInTheDocument();
1072
+ } );
1073
+ // No tabpanel should be rendered either
1074
+ expect(
1075
+ screen.queryByRole( 'tabpanel' )
1076
+ ).not.toBeInTheDocument();
1077
+ } );
1078
+ it( 'should not render any tab when the selected tab becomes disabled', async () => {
1079
+ const { rerender } = render(
1080
+ <ControlledTabs tabs={ TABS } selectedTabId="beta" />
1081
+ );
1082
+
1083
+ expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1084
+
1085
+ const TABS_WITH_BETA_DISABLED = TABS.map( ( tabObj ) =>
1086
+ tabObj.id === 'beta'
1087
+ ? {
1088
+ ...tabObj,
1089
+ tab: {
1090
+ ...tabObj.tab,
1091
+ disabled: true,
1092
+ },
1093
+ }
1094
+ : tabObj
1095
+ );
1096
+
1097
+ rerender(
1098
+ <ControlledTabs
1099
+ tabs={ TABS_WITH_BETA_DISABLED }
1100
+ selectedTabId="beta"
1101
+ />
1102
+ );
1103
+ // No tab should be selected i.e. it doesn't fall back to first tab.
1104
+ // `waitFor` is needed here to prevent testing library from
1105
+ // throwing a 'not wrapped in `act()`' error.
1106
+ await waitFor( () => {
1107
+ expect(
1108
+ screen.queryByRole( 'tab', { selected: true } )
1109
+ ).not.toBeInTheDocument();
1110
+ } );
1111
+ // No tabpanel should be rendered either
1112
+ expect(
1113
+ screen.queryByRole( 'tabpanel' )
1114
+ ).not.toBeInTheDocument();
1115
+
1116
+ // re-enable all tabs
1117
+ rerender(
1118
+ <ControlledTabs tabs={ TABS } selectedTabId="beta" />
1119
+ );
1120
+
1121
+ // If the previously selected tab is reenabled, it should not
1122
+ // be reselected.
1123
+ expect(
1124
+ screen.queryByRole( 'tab', { selected: true } )
1125
+ ).not.toBeInTheDocument();
1126
+ // No tabpanel should be rendered either
1127
+ expect(
1128
+ screen.queryByRole( 'tabpanel' )
1129
+ ).not.toBeInTheDocument();
1130
+ } );
1131
+ } );
1132
+ } );
1133
+ } );