@wordpress/components 28.13.1-next.082ed6819.0 → 29.0.1-next.a9f418477.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 (586) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/CONTRIBUTING.md +16 -16
  3. package/build/angle-picker-control/index.js +1 -1
  4. package/build/angle-picker-control/index.js.map +1 -1
  5. package/build/animation/index.js +0 -6
  6. package/build/animation/index.js.map +1 -1
  7. package/build/autocomplete/index.js +0 -1
  8. package/build/autocomplete/index.js.map +1 -1
  9. package/build/border-box-control/border-box-control-split-controls/component.js +1 -1
  10. package/build/border-box-control/border-box-control-split-controls/component.js.map +1 -1
  11. package/build/border-control/border-control/component.js +2 -0
  12. package/build/border-control/border-control/component.js.map +1 -1
  13. package/build/box-control/all-input-control.js +1 -0
  14. package/build/box-control/all-input-control.js.map +1 -1
  15. package/build/box-control/axial-input-controls.js +1 -0
  16. package/build/box-control/axial-input-controls.js.map +1 -1
  17. package/build/box-control/index.js +22 -15
  18. package/build/box-control/index.js.map +1 -1
  19. package/build/box-control/input-controls.js +1 -0
  20. package/build/box-control/input-controls.js.map +1 -1
  21. package/build/box-control/types.js.map +1 -1
  22. package/build/circular-option-picker/circular-option-picker-actions.js +1 -0
  23. package/build/circular-option-picker/circular-option-picker-actions.js.map +1 -1
  24. package/build/circular-option-picker/circular-option-picker-option.js +1 -0
  25. package/build/circular-option-picker/circular-option-picker-option.js.map +1 -1
  26. package/build/clipboard-button/index.js +5 -3
  27. package/build/clipboard-button/index.js.map +1 -1
  28. package/build/color-palette/index.native.js +0 -1
  29. package/build/color-palette/index.native.js.map +1 -1
  30. package/build/color-picker/input-with-slider.js +2 -2
  31. package/build/color-picker/input-with-slider.js.map +1 -1
  32. package/build/composite/item.js +0 -9
  33. package/build/composite/item.js.map +1 -1
  34. package/build/context/context-connect.js +0 -1
  35. package/build/context/context-connect.js.map +1 -1
  36. package/build/custom-gradient-picker/gradient-bar/control-points.js +2 -0
  37. package/build/custom-gradient-picker/gradient-bar/control-points.js.map +1 -1
  38. package/build/custom-gradient-picker/index.js +2 -0
  39. package/build/custom-gradient-picker/index.js.map +1 -1
  40. package/build/custom-gradient-picker/types.js.map +1 -1
  41. package/build/custom-select-control/index.js +8 -0
  42. package/build/custom-select-control/index.js.map +1 -1
  43. package/build/custom-select-control/types.js.map +1 -1
  44. package/build/drop-zone/index.js +19 -13
  45. package/build/drop-zone/index.js.map +1 -1
  46. package/build/drop-zone/types.js.map +1 -1
  47. package/build/dropdown-menu/index.js +1 -0
  48. package/build/dropdown-menu/index.js.map +1 -1
  49. package/build/duotone-picker/color-list-picker/index.js +9 -14
  50. package/build/duotone-picker/color-list-picker/index.js.map +1 -1
  51. package/build/external-link/index.js +0 -1
  52. package/build/external-link/index.js.map +1 -1
  53. package/build/font-size-picker/font-size-picker-select.js +1 -0
  54. package/build/font-size-picker/font-size-picker-select.js.map +1 -1
  55. package/build/font-size-picker/index.js +1 -0
  56. package/build/font-size-picker/index.js.map +1 -1
  57. package/build/form-file-upload/index.js +11 -1
  58. package/build/form-file-upload/index.js.map +1 -1
  59. package/build/form-file-upload/types.js.map +1 -1
  60. package/build/form-token-field/index.js +6 -1
  61. package/build/form-token-field/index.js.map +1 -1
  62. package/build/gradient-picker/index.js +2 -0
  63. package/build/gradient-picker/index.js.map +1 -1
  64. package/build/gradient-picker/types.js.map +1 -1
  65. package/build/icon/index.js +9 -0
  66. package/build/icon/index.js.map +1 -1
  67. package/build/index.js +0 -6
  68. package/build/index.js.map +1 -1
  69. package/build/menu/checkbox-item.js +5 -9
  70. package/build/menu/checkbox-item.js.map +1 -1
  71. package/build/menu/group-label.js +4 -1
  72. package/build/menu/group-label.js.map +1 -1
  73. package/build/menu/group.js +4 -1
  74. package/build/menu/group.js.map +1 -1
  75. package/build/menu/item-help-text.js +5 -0
  76. package/build/menu/item-help-text.js.map +1 -1
  77. package/build/menu/item-label.js +5 -0
  78. package/build/menu/item-label.js.map +1 -1
  79. package/build/menu/item.js +4 -8
  80. package/build/menu/item.js.map +1 -1
  81. package/build/menu/radio-item.js +5 -9
  82. package/build/menu/radio-item.js.map +1 -1
  83. package/build/menu/separator.js +5 -2
  84. package/build/menu/separator.js.map +1 -1
  85. package/build/modal/aria-helper.js +0 -1
  86. package/build/modal/aria-helper.js.map +1 -1
  87. package/build/modal/index.js +0 -1
  88. package/build/modal/index.js.map +1 -1
  89. package/build/number-control/index.js +8 -0
  90. package/build/number-control/index.js.map +1 -1
  91. package/build/number-control/types.js.map +1 -1
  92. package/build/range-control/index.js +2 -1
  93. package/build/range-control/index.js.map +1 -1
  94. package/build/range-control/mark.js +0 -1
  95. package/build/range-control/mark.js.map +1 -1
  96. package/build/range-control/styles/range-control-styles.js +33 -45
  97. package/build/range-control/styles/range-control-styles.js.map +1 -1
  98. package/build/resizable-box/index.js +9 -1
  99. package/build/resizable-box/index.js.map +1 -1
  100. package/build/slot-fill/bubbles-virtually/fill.js +8 -12
  101. package/build/slot-fill/bubbles-virtually/fill.js.map +1 -1
  102. package/build/slot-fill/bubbles-virtually/slot-fill-provider.js +6 -10
  103. package/build/slot-fill/bubbles-virtually/slot-fill-provider.js.map +1 -1
  104. package/build/slot-fill/bubbles-virtually/slot.js +4 -10
  105. package/build/slot-fill/bubbles-virtually/slot.js.map +1 -1
  106. package/build/slot-fill/types.js.map +1 -1
  107. package/build/style-provider/index.js +0 -1
  108. package/build/style-provider/index.js.map +1 -1
  109. package/build/tabs/tab.js +0 -17
  110. package/build/tabs/tab.js.map +1 -1
  111. package/build/toolbar/toolbar-button/index.js +2 -0
  112. package/build/toolbar/toolbar-button/index.js.map +1 -1
  113. package/build/tools-panel/tools-panel/component.js +2 -0
  114. package/build/tools-panel/tools-panel/component.js.map +1 -1
  115. package/build/tree-grid/cell.js +4 -1
  116. package/build/tree-grid/cell.js.map +1 -1
  117. package/build/tree-grid/types.js.map +1 -1
  118. package/build/unit-control/index.js +10 -1
  119. package/build/unit-control/index.js.map +1 -1
  120. package/build/unit-control/types.js.map +1 -1
  121. package/build-module/angle-picker-control/index.js +1 -1
  122. package/build-module/angle-picker-control/index.js.map +1 -1
  123. package/build-module/animation/index.js +1 -1
  124. package/build-module/animation/index.js.map +1 -1
  125. package/build-module/autocomplete/index.js +0 -1
  126. package/build-module/autocomplete/index.js.map +1 -1
  127. package/build-module/border-box-control/border-box-control-split-controls/component.js +1 -1
  128. package/build-module/border-box-control/border-box-control-split-controls/component.js.map +1 -1
  129. package/build-module/border-control/border-control/component.js +2 -0
  130. package/build-module/border-control/border-control/component.js.map +1 -1
  131. package/build-module/box-control/all-input-control.js +1 -0
  132. package/build-module/box-control/all-input-control.js.map +1 -1
  133. package/build-module/box-control/axial-input-controls.js +1 -0
  134. package/build-module/box-control/axial-input-controls.js.map +1 -1
  135. package/build-module/box-control/index.js +22 -15
  136. package/build-module/box-control/index.js.map +1 -1
  137. package/build-module/box-control/input-controls.js +1 -0
  138. package/build-module/box-control/input-controls.js.map +1 -1
  139. package/build-module/box-control/types.js.map +1 -1
  140. package/build-module/circular-option-picker/circular-option-picker-actions.js +1 -0
  141. package/build-module/circular-option-picker/circular-option-picker-actions.js.map +1 -1
  142. package/build-module/circular-option-picker/circular-option-picker-option.js +1 -0
  143. package/build-module/circular-option-picker/circular-option-picker-option.js.map +1 -1
  144. package/build-module/clipboard-button/index.js +5 -3
  145. package/build-module/clipboard-button/index.js.map +1 -1
  146. package/build-module/color-palette/index.native.js +0 -1
  147. package/build-module/color-palette/index.native.js.map +1 -1
  148. package/build-module/color-picker/input-with-slider.js +2 -2
  149. package/build-module/color-picker/input-with-slider.js.map +1 -1
  150. package/build-module/composite/item.js +0 -9
  151. package/build-module/composite/item.js.map +1 -1
  152. package/build-module/context/context-connect.js +0 -1
  153. package/build-module/context/context-connect.js.map +1 -1
  154. package/build-module/custom-gradient-picker/gradient-bar/control-points.js +2 -0
  155. package/build-module/custom-gradient-picker/gradient-bar/control-points.js.map +1 -1
  156. package/build-module/custom-gradient-picker/index.js +2 -0
  157. package/build-module/custom-gradient-picker/index.js.map +1 -1
  158. package/build-module/custom-gradient-picker/types.js.map +1 -1
  159. package/build-module/custom-select-control/index.js +8 -0
  160. package/build-module/custom-select-control/index.js.map +1 -1
  161. package/build-module/custom-select-control/types.js.map +1 -1
  162. package/build-module/drop-zone/index.js +19 -13
  163. package/build-module/drop-zone/index.js.map +1 -1
  164. package/build-module/drop-zone/types.js.map +1 -1
  165. package/build-module/dropdown-menu/index.js +1 -0
  166. package/build-module/dropdown-menu/index.js.map +1 -1
  167. package/build-module/duotone-picker/color-list-picker/index.js +10 -15
  168. package/build-module/duotone-picker/color-list-picker/index.js.map +1 -1
  169. package/build-module/external-link/index.js +0 -1
  170. package/build-module/external-link/index.js.map +1 -1
  171. package/build-module/font-size-picker/font-size-picker-select.js +1 -0
  172. package/build-module/font-size-picker/font-size-picker-select.js.map +1 -1
  173. package/build-module/font-size-picker/index.js +1 -0
  174. package/build-module/font-size-picker/index.js.map +1 -1
  175. package/build-module/form-file-upload/index.js +13 -2
  176. package/build-module/form-file-upload/index.js.map +1 -1
  177. package/build-module/form-file-upload/types.js.map +1 -1
  178. package/build-module/form-token-field/index.js +6 -1
  179. package/build-module/form-token-field/index.js.map +1 -1
  180. package/build-module/gradient-picker/index.js +2 -0
  181. package/build-module/gradient-picker/index.js.map +1 -1
  182. package/build-module/gradient-picker/types.js.map +1 -1
  183. package/build-module/icon/index.js +9 -0
  184. package/build-module/icon/index.js.map +1 -1
  185. package/build-module/index.js +1 -1
  186. package/build-module/index.js.map +1 -1
  187. package/build-module/menu/checkbox-item.js +5 -9
  188. package/build-module/menu/checkbox-item.js.map +1 -1
  189. package/build-module/menu/group-label.js +4 -1
  190. package/build-module/menu/group-label.js.map +1 -1
  191. package/build-module/menu/group.js +4 -1
  192. package/build-module/menu/group.js.map +1 -1
  193. package/build-module/menu/item-help-text.js +6 -1
  194. package/build-module/menu/item-help-text.js.map +1 -1
  195. package/build-module/menu/item-label.js +6 -1
  196. package/build-module/menu/item-label.js.map +1 -1
  197. package/build-module/menu/item.js +4 -8
  198. package/build-module/menu/item.js.map +1 -1
  199. package/build-module/menu/radio-item.js +5 -9
  200. package/build-module/menu/radio-item.js.map +1 -1
  201. package/build-module/menu/separator.js +5 -2
  202. package/build-module/menu/separator.js.map +1 -1
  203. package/build-module/modal/aria-helper.js +0 -1
  204. package/build-module/modal/aria-helper.js.map +1 -1
  205. package/build-module/modal/index.js +0 -1
  206. package/build-module/modal/index.js.map +1 -1
  207. package/build-module/number-control/index.js +8 -0
  208. package/build-module/number-control/index.js.map +1 -1
  209. package/build-module/number-control/types.js.map +1 -1
  210. package/build-module/range-control/index.js +2 -1
  211. package/build-module/range-control/index.js.map +1 -1
  212. package/build-module/range-control/mark.js +0 -1
  213. package/build-module/range-control/mark.js.map +1 -1
  214. package/build-module/range-control/styles/range-control-styles.js +33 -45
  215. package/build-module/range-control/styles/range-control-styles.js.map +1 -1
  216. package/build-module/resizable-box/index.js +9 -1
  217. package/build-module/resizable-box/index.js.map +1 -1
  218. package/build-module/slot-fill/bubbles-virtually/fill.js +9 -13
  219. package/build-module/slot-fill/bubbles-virtually/fill.js.map +1 -1
  220. package/build-module/slot-fill/bubbles-virtually/slot-fill-provider.js +6 -10
  221. package/build-module/slot-fill/bubbles-virtually/slot-fill-provider.js.map +1 -1
  222. package/build-module/slot-fill/bubbles-virtually/slot.js +4 -10
  223. package/build-module/slot-fill/bubbles-virtually/slot.js.map +1 -1
  224. package/build-module/slot-fill/types.js.map +1 -1
  225. package/build-module/style-provider/index.js +0 -1
  226. package/build-module/style-provider/index.js.map +1 -1
  227. package/build-module/tabs/tab.js +0 -15
  228. package/build-module/tabs/tab.js.map +1 -1
  229. package/build-module/toolbar/toolbar-button/index.js +2 -0
  230. package/build-module/toolbar/toolbar-button/index.js.map +1 -1
  231. package/build-module/tools-panel/tools-panel/component.js +2 -0
  232. package/build-module/tools-panel/tools-panel/component.js.map +1 -1
  233. package/build-module/tree-grid/cell.js +4 -1
  234. package/build-module/tree-grid/cell.js.map +1 -1
  235. package/build-module/tree-grid/types.js.map +1 -1
  236. package/build-module/unit-control/index.js +10 -1
  237. package/build-module/unit-control/index.js.map +1 -1
  238. package/build-module/unit-control/types.js.map +1 -1
  239. package/build-style/style-rtl.css +15 -11
  240. package/build-style/style.css +15 -11
  241. package/build-types/alignment-matrix-control/styles.d.ts.map +1 -1
  242. package/build-types/angle-picker-control/styles/angle-picker-control-styles.d.ts.map +1 -1
  243. package/build-types/animation/index.d.ts +1 -1
  244. package/build-types/animation/index.d.ts.map +1 -1
  245. package/build-types/base-control/hooks.d.ts +4 -4
  246. package/build-types/base-control/styles/base-control-styles.d.ts.map +1 -1
  247. package/build-types/border-box-control/border-box-control/hook.d.ts +83 -83
  248. package/build-types/border-box-control/border-box-control-linked-button/hook.d.ts +93 -93
  249. package/build-types/border-box-control/border-box-control-split-controls/hook.d.ts +83 -83
  250. package/build-types/border-box-control/border-box-control-visualizer/hook.d.ts +83 -83
  251. package/build-types/border-control/border-control/component.d.ts.map +1 -1
  252. package/build-types/border-control/border-control/hook.d.ts +83 -83
  253. package/build-types/border-control/border-control-dropdown/hook.d.ts +83 -83
  254. package/build-types/border-control/stories/index.story.d.ts.map +1 -1
  255. package/build-types/box-control/all-input-control.d.ts.map +1 -1
  256. package/build-types/box-control/axial-input-controls.d.ts.map +1 -1
  257. package/build-types/box-control/index.d.ts +14 -13
  258. package/build-types/box-control/index.d.ts.map +1 -1
  259. package/build-types/box-control/input-controls.d.ts.map +1 -1
  260. package/build-types/box-control/stories/index.story.d.ts +852 -816
  261. package/build-types/box-control/stories/index.story.d.ts.map +1 -1
  262. package/build-types/box-control/styles/box-control-icon-styles.d.ts.map +1 -1
  263. package/build-types/box-control/styles/box-control-styles.d.ts +3 -2
  264. package/build-types/box-control/styles/box-control-styles.d.ts.map +1 -1
  265. package/build-types/box-control/types.d.ts +16 -2
  266. package/build-types/box-control/types.d.ts.map +1 -1
  267. package/build-types/card/card/hook.d.ts +83 -83
  268. package/build-types/card/card-body/hook.d.ts +83 -83
  269. package/build-types/card/card-divider/hook.d.ts +84 -84
  270. package/build-types/card/card-footer/hook.d.ts +83 -83
  271. package/build-types/card/card-header/hook.d.ts +83 -83
  272. package/build-types/card/card-media/hook.d.ts +83 -83
  273. package/build-types/circular-option-picker/circular-option-picker-actions.d.ts.map +1 -1
  274. package/build-types/circular-option-picker/circular-option-picker-option.d.ts.map +1 -1
  275. package/build-types/clipboard-button/index.d.ts.map +1 -1
  276. package/build-types/color-picker/styles.d.ts +3 -2
  277. package/build-types/color-picker/styles.d.ts.map +1 -1
  278. package/build-types/combobox-control/stories/index.story.d.ts.map +1 -1
  279. package/build-types/composite/index.d.ts.map +1 -1
  280. package/build-types/composite/item.d.ts.map +1 -1
  281. package/build-types/context/constants.d.ts.map +1 -1
  282. package/build-types/context/get-styled-class-name-from-key.d.ts +1 -9
  283. package/build-types/context/get-styled-class-name-from-key.d.ts.map +1 -1
  284. package/build-types/custom-gradient-picker/gradient-bar/control-points.d.ts.map +1 -1
  285. package/build-types/custom-gradient-picker/index.d.ts +1 -1
  286. package/build-types/custom-gradient-picker/index.d.ts.map +1 -1
  287. package/build-types/custom-gradient-picker/types.d.ts +6 -0
  288. package/build-types/custom-gradient-picker/types.d.ts.map +1 -1
  289. package/build-types/custom-select-control/index.d.ts.map +1 -1
  290. package/build-types/custom-select-control/stories/index.story.d.ts.map +1 -1
  291. package/build-types/custom-select-control/types.d.ts +7 -0
  292. package/build-types/custom-select-control/types.d.ts.map +1 -1
  293. package/build-types/custom-select-control-v2/stories/index.story.d.ts.map +1 -1
  294. package/build-types/custom-select-control-v2/styles.d.ts.map +1 -1
  295. package/build-types/date-time/date/styles.d.ts.map +1 -1
  296. package/build-types/date-time/stories/date-time.story.d.ts.map +1 -1
  297. package/build-types/date-time/stories/date.story.d.ts.map +1 -1
  298. package/build-types/date-time/stories/time.story.d.ts.map +1 -1
  299. package/build-types/date-time/time/styles.d.ts +8 -4
  300. package/build-types/date-time/time/styles.d.ts.map +1 -1
  301. package/build-types/dimension-control/stories/index.story.d.ts.map +1 -1
  302. package/build-types/drop-zone/index.d.ts +1 -1
  303. package/build-types/drop-zone/index.d.ts.map +1 -1
  304. package/build-types/drop-zone/types.d.ts +5 -0
  305. package/build-types/drop-zone/types.d.ts.map +1 -1
  306. package/build-types/dropdown-menu/index.d.ts.map +1 -1
  307. package/build-types/duotone-picker/color-list-picker/index.d.ts.map +1 -1
  308. package/build-types/elevation/hook.d.ts +83 -83
  309. package/build-types/flex/flex/hook.d.ts +83 -83
  310. package/build-types/flex/flex-block/hook.d.ts +83 -83
  311. package/build-types/flex/flex-item/hook.d.ts +83 -83
  312. package/build-types/focal-point-picker/stories/index.story.d.ts.map +1 -1
  313. package/build-types/focal-point-picker/styles/focal-point-picker-style.d.ts +2 -1
  314. package/build-types/focal-point-picker/styles/focal-point-picker-style.d.ts.map +1 -1
  315. package/build-types/font-size-picker/font-size-picker-select.d.ts.map +1 -1
  316. package/build-types/font-size-picker/index.d.ts.map +1 -1
  317. package/build-types/font-size-picker/styles.d.ts.map +1 -1
  318. package/build-types/form-file-upload/index.d.ts +2 -1
  319. package/build-types/form-file-upload/index.d.ts.map +1 -1
  320. package/build-types/form-file-upload/stories/index.story.d.ts.map +1 -1
  321. package/build-types/form-file-upload/types.d.ts +10 -8
  322. package/build-types/form-file-upload/types.d.ts.map +1 -1
  323. package/build-types/form-token-field/index.d.ts.map +1 -1
  324. package/build-types/form-token-field/stories/index.story.d.ts.map +1 -1
  325. package/build-types/gradient-picker/index.d.ts +1 -1
  326. package/build-types/gradient-picker/index.d.ts.map +1 -1
  327. package/build-types/gradient-picker/types.d.ts +6 -0
  328. package/build-types/gradient-picker/types.d.ts.map +1 -1
  329. package/build-types/grid/hook.d.ts +83 -83
  330. package/build-types/h-stack/hook.d.ts +83 -83
  331. package/build-types/heading/hook.d.ts +82 -82
  332. package/build-types/higher-order/with-fallback-styles/index.d.ts +2 -2
  333. package/build-types/higher-order/with-filters/index.d.ts +4 -4
  334. package/build-types/icon/index.d.ts +23 -7
  335. package/build-types/icon/index.d.ts.map +1 -1
  336. package/build-types/icon/stories/index.story.d.ts +7 -1
  337. package/build-types/icon/stories/index.story.d.ts.map +1 -1
  338. package/build-types/index.d.ts +1 -1
  339. package/build-types/index.d.ts.map +1 -1
  340. package/build-types/input-control/styles/input-control-styles.d.ts.map +1 -1
  341. package/build-types/item-group/item/hook.d.ts +83 -83
  342. package/build-types/item-group/item-group/hook.d.ts +83 -83
  343. package/build-types/lock-unlock.d.ts +2 -2
  344. package/build-types/menu/checkbox-item.d.ts.map +1 -1
  345. package/build-types/menu/group-label.d.ts.map +1 -1
  346. package/build-types/menu/group.d.ts.map +1 -1
  347. package/build-types/menu/index.d.ts.map +1 -1
  348. package/build-types/menu/item-help-text.d.ts.map +1 -1
  349. package/build-types/menu/item-label.d.ts.map +1 -1
  350. package/build-types/menu/item.d.ts.map +1 -1
  351. package/build-types/menu/radio-item.d.ts.map +1 -1
  352. package/build-types/menu/separator.d.ts.map +1 -1
  353. package/build-types/menu/styles.d.ts.map +1 -1
  354. package/build-types/menu-item/stories/index.story.d.ts.map +1 -1
  355. package/build-types/navigation/styles/navigation-styles.d.ts.map +1 -1
  356. package/build-types/navigator/navigator-back-button/hook.d.ts +92 -92
  357. package/build-types/navigator/navigator-button/hook.d.ts +92 -92
  358. package/build-types/number-control/index.d.ts +2 -1
  359. package/build-types/number-control/index.d.ts.map +1 -1
  360. package/build-types/number-control/stories/index.story.d.ts +2 -1
  361. package/build-types/number-control/stories/index.story.d.ts.map +1 -1
  362. package/build-types/number-control/types.d.ts +7 -0
  363. package/build-types/number-control/types.d.ts.map +1 -1
  364. package/build-types/popover/stories/index.story.d.ts.map +1 -1
  365. package/build-types/progress-bar/styles.d.ts.map +1 -1
  366. package/build-types/range-control/index.d.ts +1 -1
  367. package/build-types/range-control/index.d.ts.map +1 -1
  368. package/build-types/range-control/mark.d.ts.map +1 -1
  369. package/build-types/range-control/styles/range-control-styles.d.ts +4 -3
  370. package/build-types/range-control/styles/range-control-styles.d.ts.map +1 -1
  371. package/build-types/resizable-box/index.d.ts.map +1 -1
  372. package/build-types/resizable-box/resize-tooltip/styles/resize-tooltip.styles.d.ts.map +1 -1
  373. package/build-types/resizable-box/stories/index.story.d.ts.map +1 -1
  374. package/build-types/scrollable/hook.d.ts +83 -83
  375. package/build-types/scrollable/stories/index.story.d.ts.map +1 -1
  376. package/build-types/select-control/index.d.ts.map +1 -1
  377. package/build-types/select-control/stories/index.story.d.ts.map +1 -1
  378. package/build-types/select-control/styles/select-control-styles.d.ts.map +1 -1
  379. package/build-types/slot-fill/bubbles-virtually/fill.d.ts.map +1 -1
  380. package/build-types/slot-fill/bubbles-virtually/slot-fill-provider.d.ts.map +1 -1
  381. package/build-types/slot-fill/bubbles-virtually/slot.d.ts.map +1 -1
  382. package/build-types/slot-fill/bubbles-virtually/use-slot-fills.d.ts +1 -1
  383. package/build-types/slot-fill/types.d.ts +4 -3
  384. package/build-types/slot-fill/types.d.ts.map +1 -1
  385. package/build-types/spacer/hook.d.ts +83 -83
  386. package/build-types/spinner/styles.d.ts.map +1 -1
  387. package/build-types/surface/hook.d.ts +83 -83
  388. package/build-types/tabs/tab.d.ts +3 -0
  389. package/build-types/tabs/tab.d.ts.map +1 -1
  390. package/build-types/text/hook.d.ts +83 -83
  391. package/build-types/toggle-group-control/toggle-group-control-option-base/styles.d.ts.map +1 -1
  392. package/build-types/toolbar/toolbar-button/index.d.ts.map +1 -1
  393. package/build-types/tools-panel/stories/index.story.d.ts.map +1 -1
  394. package/build-types/tools-panel/tools-panel/component.d.ts +2 -0
  395. package/build-types/tools-panel/tools-panel/component.d.ts.map +1 -1
  396. package/build-types/tools-panel/tools-panel/hook.d.ts +83 -83
  397. package/build-types/tools-panel/tools-panel-header/hook.d.ts +83 -83
  398. package/build-types/tools-panel/tools-panel-item/hook.d.ts +83 -83
  399. package/build-types/tree-grid/cell.d.ts.map +1 -1
  400. package/build-types/tree-grid/types.d.ts +1 -1
  401. package/build-types/tree-grid/types.d.ts.map +1 -1
  402. package/build-types/truncate/hook.d.ts +83 -83
  403. package/build-types/unit-control/index.d.ts +3 -2
  404. package/build-types/unit-control/index.d.ts.map +1 -1
  405. package/build-types/unit-control/stories/index.story.d.ts.map +1 -1
  406. package/build-types/unit-control/styles/unit-control-styles.d.ts +2 -1
  407. package/build-types/unit-control/styles/unit-control-styles.d.ts.map +1 -1
  408. package/build-types/unit-control/types.d.ts +7 -0
  409. package/build-types/unit-control/types.d.ts.map +1 -1
  410. package/build-types/utils/hooks/use-controlled-state.d.ts +1 -1
  411. package/build-types/utils/hooks/use-controlled-state.d.ts.map +1 -1
  412. package/build-types/utils/rtl.d.ts +1 -1
  413. package/build-types/utils/rtl.d.ts.map +1 -1
  414. package/build-types/v-stack/hook.d.ts +83 -83
  415. package/build-types/z-stack/styles.d.ts.map +1 -1
  416. package/package.json +20 -20
  417. package/src/alignment-matrix-control/stories/index.story.tsx +2 -2
  418. package/src/angle-picker-control/index.tsx +1 -1
  419. package/src/angle-picker-control/stories/index.story.tsx +2 -2
  420. package/src/animation/index.tsx +0 -1
  421. package/src/base-control/stories/index.story.tsx +1 -1
  422. package/src/border-box-control/border-box-control-split-controls/component.tsx +1 -1
  423. package/src/border-box-control/stories/index.story.tsx +1 -1
  424. package/src/border-control/border-control/component.tsx +2 -0
  425. package/src/border-control/stories/index.story.tsx +1 -1
  426. package/src/box-control/README.md +80 -60
  427. package/src/box-control/all-input-control.tsx +1 -0
  428. package/src/box-control/axial-input-controls.tsx +1 -0
  429. package/src/box-control/docs-manifest.json +5 -0
  430. package/src/box-control/index.tsx +23 -15
  431. package/src/box-control/input-controls.tsx +1 -0
  432. package/src/box-control/stories/index.story.tsx +2 -1
  433. package/src/box-control/test/index.tsx +33 -26
  434. package/src/box-control/types.ts +71 -54
  435. package/src/button-group/stories/index.story.tsx +1 -1
  436. package/src/card/stories/index.story.tsx +2 -2
  437. package/src/checkbox-control/stories/index.story.tsx +1 -1
  438. package/src/circular-option-picker/circular-option-picker-actions.tsx +1 -0
  439. package/src/circular-option-picker/circular-option-picker-option.tsx +1 -0
  440. package/src/circular-option-picker/stories/index.story.tsx +2 -2
  441. package/src/circular-option-picker/style.scss +2 -2
  442. package/src/clipboard-button/index.tsx +5 -3
  443. package/src/color-palette/stories/index.story.tsx +3 -3
  444. package/src/color-picker/input-with-slider.tsx +1 -1
  445. package/src/color-picker/stories/index.story.tsx +2 -2
  446. package/src/combobox-control/stories/index.story.tsx +1 -1
  447. package/src/composite/item.tsx +1 -19
  448. package/src/composite/stories/index.story.tsx +3 -3
  449. package/src/confirm-dialog/stories/index.story.tsx +1 -1
  450. package/src/custom-gradient-picker/gradient-bar/control-points.tsx +2 -0
  451. package/src/custom-gradient-picker/index.tsx +2 -0
  452. package/src/custom-gradient-picker/style.scss +1 -1
  453. package/src/custom-gradient-picker/types.ts +6 -0
  454. package/src/custom-select-control/README.md +2 -0
  455. package/src/custom-select-control/index.tsx +9 -0
  456. package/src/custom-select-control/stories/index.story.tsx +3 -2
  457. package/src/custom-select-control/test/index.tsx +13 -9
  458. package/src/custom-select-control/types.ts +7 -0
  459. package/src/custom-select-control-v2/stories/index.story.tsx +2 -2
  460. package/src/date-time/stories/date-time.story.tsx +4 -1
  461. package/src/date-time/stories/date.story.tsx +4 -1
  462. package/src/date-time/stories/time.story.tsx +4 -1
  463. package/src/dimension-control/stories/index.story.tsx +1 -1
  464. package/src/disabled/stories/index.story.tsx +3 -3
  465. package/src/divider/stories/index.story.tsx +1 -1
  466. package/src/draggable/stories/index.story.tsx +2 -2
  467. package/src/drop-zone/index.tsx +21 -24
  468. package/src/drop-zone/types.ts +5 -0
  469. package/src/dropdown/stories/index.story.tsx +7 -7
  470. package/src/dropdown-menu/index.tsx +4 -1
  471. package/src/dropdown-menu/stories/index.story.tsx +3 -3
  472. package/src/dropdown-menu/style.scss +1 -1
  473. package/src/duotone-picker/color-list-picker/index.tsx +8 -8
  474. package/src/duotone-picker/color-list-picker/style.scss +0 -6
  475. package/src/duotone-picker/stories/duotone-picker.story.tsx +1 -1
  476. package/src/flex/stories/index.story.tsx +1 -1
  477. package/src/font-size-picker/font-size-picker-select.tsx +1 -0
  478. package/src/font-size-picker/index.tsx +1 -0
  479. package/src/font-size-picker/stories/index.story.tsx +1 -1
  480. package/src/form-file-upload/README.md +58 -48
  481. package/src/form-file-upload/docs-manifest.json +5 -0
  482. package/src/form-file-upload/index.tsx +12 -1
  483. package/src/form-file-upload/stories/index.story.tsx +4 -3
  484. package/src/form-file-upload/test/index.tsx +5 -1
  485. package/src/form-file-upload/types.ts +10 -8
  486. package/src/form-token-field/README.md +1 -0
  487. package/src/form-token-field/index.tsx +7 -0
  488. package/src/form-token-field/stories/index.story.tsx +4 -2
  489. package/src/form-token-field/test/index.tsx +5 -1
  490. package/src/gradient-picker/README.md +8 -0
  491. package/src/gradient-picker/index.tsx +2 -0
  492. package/src/gradient-picker/stories/index.story.tsx +1 -1
  493. package/src/gradient-picker/types.ts +6 -0
  494. package/src/grid/stories/index.story.tsx +1 -1
  495. package/src/h-stack/stories/index.story.tsx +2 -2
  496. package/src/icon/README.md +22 -65
  497. package/src/icon/docs-manifest.json +5 -0
  498. package/src/icon/index.tsx +28 -13
  499. package/src/icon/stories/index.story.tsx +50 -8
  500. package/src/index.ts +1 -5
  501. package/src/input-control/stories/index.story.tsx +4 -4
  502. package/src/item-group/stories/index.story.tsx +2 -2
  503. package/src/menu/checkbox-item.tsx +9 -7
  504. package/src/menu/group-label.tsx +8 -1
  505. package/src/menu/group.tsx +8 -1
  506. package/src/menu/item-help-text.tsx +10 -1
  507. package/src/menu/item-label.tsx +10 -1
  508. package/src/menu/item.tsx +8 -6
  509. package/src/menu/radio-item.tsx +9 -7
  510. package/src/menu/separator.tsx +9 -2
  511. package/src/menu/stories/index.story.tsx +2 -2
  512. package/src/menu-group/stories/index.story.tsx +1 -1
  513. package/src/menu-item/stories/index.story.tsx +1 -1
  514. package/src/menu-items-choice/stories/index.story.tsx +1 -1
  515. package/src/menu-items-choice/style.scss +1 -0
  516. package/src/modal/stories/index.story.tsx +2 -2
  517. package/src/modal/test/index.tsx +2 -1
  518. package/src/navigable-container/stories/navigable-menu.story.tsx +1 -1
  519. package/src/navigable-container/stories/tabbable-container.story.tsx +1 -1
  520. package/src/navigation/stories/index.story.tsx +4 -4
  521. package/src/navigator/stories/index.story.tsx +3 -3
  522. package/src/number-control/README.md +2 -1
  523. package/src/number-control/index.tsx +9 -0
  524. package/src/number-control/stories/index.story.tsx +2 -1
  525. package/src/number-control/test/index.tsx +5 -1
  526. package/src/number-control/types.ts +7 -0
  527. package/src/panel/stories/index.story.tsx +1 -1
  528. package/src/placeholder/stories/index.story.tsx +3 -3
  529. package/src/popover/stories/index.story.tsx +11 -9
  530. package/src/query-controls/stories/index.story.tsx +6 -6
  531. package/src/radio-control/stories/index.story.tsx +1 -1
  532. package/src/radio-group/stories/index.story.tsx +3 -3
  533. package/src/range-control/index.tsx +1 -0
  534. package/src/range-control/mark.tsx +0 -1
  535. package/src/range-control/stories/index.story.tsx +7 -7
  536. package/src/range-control/styles/range-control-styles.ts +18 -19
  537. package/src/resizable-box/index.tsx +10 -0
  538. package/src/resizable-box/stories/index.story.tsx +1 -1
  539. package/src/resizable-box/style.scss +8 -0
  540. package/src/responsive-wrapper/stories/index.story.tsx +1 -1
  541. package/src/sandbox/stories/index.story.tsx +1 -1
  542. package/src/scrollable/stories/index.story.tsx +2 -1
  543. package/src/search-control/stories/index.story.tsx +1 -1
  544. package/src/select-control/stories/index.story.tsx +1 -1
  545. package/src/slot-fill/bubbles-virtually/fill.tsx +7 -11
  546. package/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx +2 -13
  547. package/src/slot-fill/bubbles-virtually/slot.tsx +4 -7
  548. package/src/slot-fill/stories/index.story.tsx +2 -2
  549. package/src/slot-fill/types.ts +4 -3
  550. package/src/snackbar/stories/index.story.tsx +4 -4
  551. package/src/snackbar/stories/list.story.tsx +2 -2
  552. package/src/surface/stories/index.story.tsx +1 -1
  553. package/src/tabs/tab.tsx +0 -18
  554. package/src/tabs/test/index.tsx +1492 -947
  555. package/src/text-control/stories/index.story.tsx +1 -1
  556. package/src/textarea-control/stories/index.story.tsx +1 -1
  557. package/src/theme/stories/index.story.tsx +1 -1
  558. package/src/toggle-control/stories/index.story.tsx +1 -1
  559. package/src/toggle-group-control/stories/index.story.tsx +1 -1
  560. package/src/toolbar/stories/index.story.tsx +1 -1
  561. package/src/toolbar/toolbar-button/index.tsx +2 -0
  562. package/src/tools-panel/stories/index.story.tsx +15 -3
  563. package/src/tools-panel/test/index.tsx +0 -17
  564. package/src/tools-panel/tools-panel/README.md +4 -0
  565. package/src/tools-panel/tools-panel/component.tsx +2 -0
  566. package/src/tooltip/stories/index.story.tsx +1 -1
  567. package/src/tree-grid/cell.tsx +5 -1
  568. package/src/tree-grid/stories/index.story.tsx +1 -1
  569. package/src/tree-grid/types.ts +1 -1
  570. package/src/tree-select/stories/index.story.tsx +1 -1
  571. package/src/unit-control/README.md +3 -3
  572. package/src/unit-control/index.tsx +11 -1
  573. package/src/unit-control/stories/index.story.tsx +5 -4
  574. package/src/unit-control/test/index.tsx +5 -1
  575. package/src/unit-control/types.ts +7 -0
  576. package/src/view/stories/index.story.tsx +1 -1
  577. package/src/visually-hidden/stories/index.story.tsx +1 -1
  578. package/src/z-stack/stories/index.story.tsx +1 -1
  579. package/tsconfig.tsbuildinfo +1 -1
  580. package/build/menu/use-temporary-focus-visible-fix.js +0 -27
  581. package/build/menu/use-temporary-focus-visible-fix.js.map +0 -1
  582. package/build-module/menu/use-temporary-focus-visible-fix.js +0 -20
  583. package/build-module/menu/use-temporary-focus-visible-fix.js.map +0 -1
  584. package/build-types/menu/use-temporary-focus-visible-fix.d.ts +0 -8
  585. package/build-types/menu/use-temporary-focus-visible-fix.d.ts.map +0 -1
  586. package/src/menu/use-temporary-focus-visible-fix.ts +0 -22
@@ -9,6 +9,7 @@ import { render } from '@ariakit/test/react';
9
9
  * WordPress dependencies
10
10
  */
11
11
  import { useEffect, useState } from '@wordpress/element';
12
+ import { isRTL } from '@wordpress/i18n';
12
13
 
13
14
  /**
14
15
  * Internal dependencies
@@ -16,6 +17,16 @@ import { useEffect, useState } from '@wordpress/element';
16
17
  import { Tabs } from '..';
17
18
  import type { TabsProps } from '../types';
18
19
 
20
+ // Setup mocking the `isRTL` function to test arrow key navigation behavior.
21
+ jest.mock( '@wordpress/i18n', () => {
22
+ const original = jest.requireActual( '@wordpress/i18n' );
23
+ return {
24
+ ...original,
25
+ isRTL: jest.fn( () => false ),
26
+ };
27
+ } );
28
+ const mockedIsRTL = isRTL as jest.Mock;
29
+
19
30
  type Tab = {
20
31
  tabId: string;
21
32
  title: string;
@@ -50,6 +61,30 @@ const TABS: Tab[] = [
50
61
  },
51
62
  ];
52
63
 
64
+ const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) =>
65
+ tabObj.tabId === 'alpha'
66
+ ? {
67
+ ...tabObj,
68
+ tab: {
69
+ ...tabObj.tab,
70
+ disabled: true,
71
+ },
72
+ }
73
+ : tabObj
74
+ );
75
+
76
+ const TABS_WITH_BETA_DISABLED = TABS.map( ( tabObj ) =>
77
+ tabObj.tabId === 'beta'
78
+ ? {
79
+ ...tabObj,
80
+ tab: {
81
+ ...tabObj.tab,
82
+ disabled: true,
83
+ },
84
+ }
85
+ : tabObj
86
+ );
87
+
53
88
  const TABS_WITH_DELTA: Tab[] = [
54
89
  ...TABS,
55
90
  {
@@ -141,11 +176,47 @@ const ControlledTabs = ( {
141
176
  );
142
177
  };
143
178
 
144
- const getSelectedTab = async () =>
145
- await screen.findByRole( 'tab', { selected: true } );
146
-
147
179
  let originalGetClientRects: () => DOMRectList;
148
180
 
181
+ async function waitForComponentToBeInitializedWithSelectedTab(
182
+ selectedTabName: string | undefined
183
+ ) {
184
+ if ( ! selectedTabName ) {
185
+ // Wait for the tablist to be tabbable as a mean to know
186
+ // that ariakit has finished initializing.
187
+ await waitFor( () =>
188
+ expect( screen.getByRole( 'tablist' ) ).toHaveAttribute(
189
+ 'tabindex',
190
+ expect.stringMatching( /^(0|-1)$/ )
191
+ )
192
+ );
193
+ // No initially selected tabs or tabpanels.
194
+ await waitFor( () =>
195
+ expect(
196
+ screen.queryByRole( 'tab', { selected: true } )
197
+ ).not.toBeInTheDocument()
198
+ );
199
+ await waitFor( () =>
200
+ expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument()
201
+ );
202
+ } else {
203
+ // Waiting for a tab to be selected is a sign that the component
204
+ // has fully initialized.
205
+ expect(
206
+ await screen.findByRole( 'tab', {
207
+ selected: true,
208
+ name: selectedTabName,
209
+ } )
210
+ ).toBeVisible();
211
+ // The corresponding tabpanel is also shown.
212
+ expect(
213
+ screen.getByRole( 'tabpanel', {
214
+ name: selectedTabName,
215
+ } )
216
+ ).toBeVisible();
217
+ }
218
+ }
219
+
149
220
  describe( 'Tabs', () => {
150
221
  beforeAll( () => {
151
222
  originalGetClientRects = window.HTMLElement.prototype.getClientRects;
@@ -162,13 +233,16 @@ describe( 'Tabs', () => {
162
233
  window.HTMLElement.prototype.getClientRects = originalGetClientRects;
163
234
  } );
164
235
 
165
- describe( 'Accessibility and semantics', () => {
166
- it( 'should use the correct aria attributes', async () => {
236
+ describe( 'Adherence to spec and basic behavior', () => {
237
+ it( 'should apply the correct roles, semantics and attributes', async () => {
167
238
  await render( <UncontrolledTabs tabs={ TABS } /> );
168
239
 
240
+ // Alpha is automatically selected as the selected tab.
241
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
242
+
169
243
  const tabList = screen.getByRole( 'tablist' );
170
244
  const allTabs = screen.getAllByRole( 'tab' );
171
- const selectedTabPanel = await screen.findByRole( 'tabpanel' );
245
+ const allTabpanels = screen.getAllByRole( 'tabpanel' );
172
246
 
173
247
  expect( tabList ).toBeVisible();
174
248
  expect( tabList ).toHaveAttribute(
@@ -178,133 +252,103 @@ describe( 'Tabs', () => {
178
252
 
179
253
  expect( allTabs ).toHaveLength( TABS.length );
180
254
 
181
- // The selected `tab` aria-controls the active `tabpanel`,
182
- // which is `aria-labelledby` the selected `tab`.
183
- expect( selectedTabPanel ).toBeVisible();
255
+ // Only 1 tab panel is accessible — the one associated with the
256
+ // selected tab. The selected `tab` aria-controls the active
257
+ /// `tabpanel`, which is `aria-labelledby` the selected `tab`.
258
+ expect( allTabpanels ).toHaveLength( 1 );
259
+
260
+ expect( allTabpanels[ 0 ] ).toBeVisible();
184
261
  expect( allTabs[ 0 ] ).toHaveAttribute(
185
262
  'aria-controls',
186
- selectedTabPanel.getAttribute( 'id' )
263
+ allTabpanels[ 0 ].getAttribute( 'id' )
187
264
  );
188
- expect( selectedTabPanel ).toHaveAttribute(
265
+ expect( allTabpanels[ 0 ] ).toHaveAttribute(
189
266
  'aria-labelledby',
190
267
  allTabs[ 0 ].getAttribute( 'id' )
191
268
  );
192
269
  } );
193
- } );
194
- describe( 'Focus Behavior', () => {
195
- it( 'should focus on the related TabPanel when pressing the Tab key', async () => {
196
- await render( <UncontrolledTabs tabs={ TABS } /> );
197
270
 
198
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
199
-
200
- const selectedTabPanel = await screen.findByRole( 'tabpanel' );
201
-
202
- // Tab should initially focus the first tab in the tablist, which
203
- // is Alpha.
204
- await press.Tab();
205
- expect(
206
- await screen.findByRole( 'tab', { name: 'Alpha' } )
207
- ).toHaveFocus();
208
-
209
- // By default the tabpanel should receive focus
210
- await press.Tab();
211
- expect( selectedTabPanel ).toHaveFocus();
212
- } );
213
- it( 'should not focus on the related TabPanel when pressing the Tab key if `focusable: false` is set', async () => {
214
- const TABS_WITH_ALPHA_FOCUSABLE_FALSE = TABS.map( ( tabObj ) =>
215
- tabObj.tabId === 'alpha'
216
- ? {
217
- ...tabObj,
218
- content: (
219
- <>
220
- Selected Tab: Alpha
221
- <button>Alpha Button</button>
222
- </>
223
- ),
224
- tabpanel: { focusable: false },
225
- }
226
- : tabObj
227
- );
271
+ it( 'should associate each `tab` with the correct `tabpanel`, even if they are not rendered in the same order', async () => {
272
+ const TABS_WITH_DELTA_REVERSED = [ ...TABS_WITH_DELTA ].reverse();
228
273
 
229
274
  await render(
230
- <UncontrolledTabs tabs={ TABS_WITH_ALPHA_FOCUSABLE_FALSE } />
275
+ <Tabs>
276
+ <Tabs.TabList>
277
+ { TABS_WITH_DELTA.map( ( tabObj ) => (
278
+ <Tabs.Tab
279
+ key={ tabObj.tabId }
280
+ tabId={ tabObj.tabId }
281
+ className={ tabObj.tab.className }
282
+ disabled={ tabObj.tab.disabled }
283
+ >
284
+ { tabObj.title }
285
+ </Tabs.Tab>
286
+ ) ) }
287
+ </Tabs.TabList>
288
+ { TABS_WITH_DELTA_REVERSED.map( ( tabObj ) => (
289
+ <Tabs.TabPanel
290
+ key={ tabObj.tabId }
291
+ tabId={ tabObj.tabId }
292
+ focusable={ tabObj.tabpanel?.focusable }
293
+ >
294
+ { tabObj.content }
295
+ </Tabs.TabPanel>
296
+ ) ) }
297
+ </Tabs>
231
298
  );
232
299
 
233
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
234
-
235
- const alphaButton = await screen.findByRole( 'button', {
236
- name: /alpha button/i,
237
- } );
300
+ // Alpha is automatically selected as the selected tab.
301
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
238
302
 
239
- // Tab should initially focus the first tab in the tablist, which
240
- // is Alpha.
241
- await press.Tab();
303
+ // Select Beta, make sure the correct tabpanel is rendered
304
+ await click( screen.getByRole( 'tab', { name: 'Beta' } ) );
242
305
  expect(
243
- await screen.findByRole( 'tab', { name: 'Alpha' } )
244
- ).toHaveFocus();
245
- // Because the alpha tabpanel is set to `focusable: false`, pressing
246
- // the Tab key should focus the button, not the tabpanel
247
- await press.Tab();
248
- expect( alphaButton ).toHaveFocus();
249
- } );
250
-
251
- it( "should focus the first tab, even if disabled, when the current selected tab id doesn't match an existing one", async () => {
252
- const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) =>
253
- tabObj.tabId === 'alpha'
254
- ? {
255
- ...tabObj,
256
- tab: {
257
- ...tabObj.tab,
258
- disabled: true,
259
- },
260
- }
261
- : tabObj
262
- );
263
-
264
- await render(
265
- <ControlledTabs
266
- tabs={ TABS_WITH_ALPHA_DISABLED }
267
- selectedTabId="non-existing-tab"
268
- />
269
- );
270
-
271
- // No tab should be selected i.e. it doesn't fall back to first tab.
272
- await waitFor( () =>
273
- expect(
274
- screen.queryByRole( 'tab', { selected: true } )
275
- ).not.toBeInTheDocument()
276
- );
277
-
278
- // No tabpanel should be rendered either
279
- expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument();
280
-
281
- await press.Tab();
306
+ screen.getByRole( 'tab', {
307
+ selected: true,
308
+ name: 'Beta',
309
+ } )
310
+ ).toBeVisible();
282
311
  expect(
283
- await screen.findByRole( 'tab', { name: 'Alpha' } )
284
- ).toHaveFocus();
312
+ screen.getByRole( 'tabpanel', {
313
+ name: 'Beta',
314
+ } )
315
+ ).toBeVisible();
285
316
 
286
- await press.ArrowRight();
317
+ // Select Gamma, make sure the correct tabpanel is rendered
318
+ await click( screen.getByRole( 'tab', { name: 'Gamma' } ) );
287
319
  expect(
288
- await screen.findByRole( 'tab', { name: 'Beta' } )
289
- ).toHaveFocus();
290
-
291
- await press.ArrowRight();
320
+ screen.getByRole( 'tab', {
321
+ selected: true,
322
+ name: 'Gamma',
323
+ } )
324
+ ).toBeVisible();
292
325
  expect(
293
- await screen.findByRole( 'tab', { name: 'Gamma' } )
294
- ).toHaveFocus();
326
+ screen.getByRole( 'tabpanel', {
327
+ name: 'Gamma',
328
+ } )
329
+ ).toBeVisible();
295
330
 
296
- await press.Tab();
297
- await press.ShiftTab();
331
+ // Select Delta, make sure the correct tabpanel is rendered
332
+ await click( screen.getByRole( 'tab', { name: 'Delta' } ) );
333
+ expect(
334
+ screen.getByRole( 'tab', {
335
+ selected: true,
336
+ name: 'Delta',
337
+ } )
338
+ ).toBeVisible();
298
339
  expect(
299
- await screen.findByRole( 'tab', { name: 'Gamma' } )
300
- ).toHaveFocus();
340
+ screen.getByRole( 'tabpanel', {
341
+ name: 'Delta',
342
+ } )
343
+ ).toBeVisible();
301
344
  } );
302
- } );
303
345
 
304
- describe( 'Tab Attributes', () => {
305
346
  it( "should apply the tab's `className` to the tab button", async () => {
306
347
  await render( <UncontrolledTabs tabs={ TABS } /> );
307
348
 
349
+ // Alpha is automatically selected as the selected tab.
350
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
351
+
308
352
  expect(
309
353
  await screen.findByRole( 'tab', { name: 'Alpha' } )
310
354
  ).toHaveClass( 'alpha-class' );
@@ -317,908 +361,1076 @@ describe( 'Tabs', () => {
317
361
  } );
318
362
  } );
319
363
 
320
- describe( 'Tab Activation', () => {
321
- it( 'defaults to automatic tab activation (pointer clicks)', async () => {
364
+ describe( 'pointer interactions', () => {
365
+ it( 'should select a tab when clicked', async () => {
322
366
  const mockOnSelect = jest.fn();
323
367
 
324
368
  await render(
325
369
  <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
326
370
  );
327
371
 
328
- // Alpha is the initially selected tab
329
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
330
- expect(
331
- await screen.findByRole( 'tabpanel', { name: 'Alpha' } )
332
- ).toBeInTheDocument();
372
+ // Alpha is automatically selected as the selected tab.
373
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
374
+
375
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
333
376
  expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
334
377
 
335
378
  // Click on Beta, make sure beta is the selected tab
336
379
  await click( screen.getByRole( 'tab', { name: 'Beta' } ) );
337
380
 
338
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
339
381
  expect(
340
- screen.getByRole( 'tabpanel', { name: 'Beta' } )
341
- ).toBeInTheDocument();
382
+ screen.getByRole( 'tab', {
383
+ selected: true,
384
+ name: 'Beta',
385
+ } )
386
+ ).toBeVisible();
387
+ expect(
388
+ screen.getByRole( 'tabpanel', {
389
+ name: 'Beta',
390
+ } )
391
+ ).toBeVisible();
392
+
393
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
342
394
  expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
343
395
 
344
- // Click on Alpha, make sure beta is the selected tab
396
+ // Click on Alpha, make sure alpha is the selected tab
345
397
  await click( screen.getByRole( 'tab', { name: 'Alpha' } ) );
346
398
 
347
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
348
399
  expect(
349
- screen.getByRole( 'tabpanel', { name: 'Alpha' } )
350
- ).toBeInTheDocument();
351
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
352
- } );
353
-
354
- it( 'defaults to automatic tab activation (arrow keys)', async () => {
355
- const mockOnSelect = jest.fn();
356
-
357
- await render(
358
- <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
359
- );
360
-
361
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
362
-
363
- // onSelect gets called on the initial render. It should be called
364
- // with the first enabled tab, which is alpha.
365
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
366
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
367
-
368
- // Tab to focus the tablist. Make sure alpha is focused.
369
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
370
- expect( await getSelectedTab() ).not.toHaveFocus();
371
- await press.Tab();
372
- expect( await getSelectedTab() ).toHaveFocus();
373
-
374
- // Navigate forward with arrow keys and make sure the Beta tab is
375
- // selected automatically.
376
- await press.ArrowRight();
377
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
378
- expect( await getSelectedTab() ).toHaveFocus();
379
- expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
380
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
400
+ screen.getByRole( 'tab', {
401
+ selected: true,
402
+ name: 'Alpha',
403
+ } )
404
+ ).toBeVisible();
405
+ expect(
406
+ screen.getByRole( 'tabpanel', {
407
+ name: 'Alpha',
408
+ } )
409
+ ).toBeVisible();
381
410
 
382
- // Navigate backwards with arrow keys. Make sure alpha is
383
- // selected automatically.
384
- await press.ArrowLeft();
385
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
386
- expect( await getSelectedTab() ).toHaveFocus();
387
411
  expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
388
412
  expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
389
413
  } );
390
414
 
391
- it( 'wraps around the last/first tab when using arrow keys', async () => {
415
+ it( 'should not select a disabled tab when clicked', async () => {
392
416
  const mockOnSelect = jest.fn();
393
417
 
394
418
  await render(
395
- <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
419
+ <UncontrolledTabs
420
+ tabs={ TABS_WITH_BETA_DISABLED }
421
+ onSelect={ mockOnSelect }
422
+ />
396
423
  );
397
424
 
398
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
399
- expect( await getSelectedTab() ).not.toHaveFocus();
425
+ // Alpha is automatically selected as the selected tab.
426
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
400
427
 
401
- // onSelect gets called on the initial render.
402
428
  expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
403
429
  expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
404
430
 
405
- // Tab to focus the tablist. Make sure Alpha is focused.
406
- await press.Tab();
407
- expect( await getSelectedTab() ).toHaveFocus();
431
+ // Clicking on Beta does not result in beta being selected
432
+ // because the tab is disabled.
433
+ await click( screen.getByRole( 'tab', { name: 'Beta' } ) );
408
434
 
409
- // Navigate backwards with arrow keys and make sure that the Gamma tab
410
- // (the last tab) is selected automatically.
411
- await press.ArrowLeft();
412
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
413
- expect( await getSelectedTab() ).toHaveFocus();
414
- expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
415
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
435
+ expect(
436
+ screen.getByRole( 'tab', {
437
+ selected: true,
438
+ name: 'Alpha',
439
+ } )
440
+ ).toBeVisible();
441
+ expect(
442
+ screen.getByRole( 'tabpanel', {
443
+ name: 'Alpha',
444
+ } )
445
+ ).toBeVisible();
416
446
 
417
- // Navigate forward with arrow keys. Make sure alpha (the first tab) is
418
- // selected automatically.
419
- await press.ArrowRight();
420
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
421
- expect( await getSelectedTab() ).toHaveFocus();
422
- expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
423
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
447
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
424
448
  } );
449
+ } );
425
450
 
426
- it( 'should not move tab selection when pressing the up/down arrow keys, unless the orientation is changed to `vertical`', async () => {
427
- const mockOnSelect = jest.fn();
451
+ describe( 'initial tab selection', () => {
452
+ describe( 'when a selected tab id is not specified', () => {
453
+ describe( 'when left `undefined` [Uncontrolled]', () => {
454
+ it( 'should choose the first tab as selected', async () => {
455
+ await render( <UncontrolledTabs tabs={ TABS } /> );
428
456
 
429
- const { rerender } = await render(
430
- <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
431
- );
457
+ // Alpha is automatically selected as the selected tab.
458
+ await waitForComponentToBeInitializedWithSelectedTab(
459
+ 'Alpha'
460
+ );
432
461
 
433
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
434
- expect( await getSelectedTab() ).not.toHaveFocus();
462
+ // Press tab. The selected tab (alpha) received focus.
463
+ await press.Tab();
464
+ expect(
465
+ await screen.findByRole( 'tab', {
466
+ selected: true,
467
+ name: 'Alpha',
468
+ } )
469
+ ).toHaveFocus();
470
+ } );
471
+
472
+ it( 'should choose the first non-disabled tab if the first tab is disabled', async () => {
473
+ await render(
474
+ <UncontrolledTabs tabs={ TABS_WITH_ALPHA_DISABLED } />
475
+ );
476
+
477
+ // Beta is automatically selected as the selected tab, since alpha is
478
+ // disabled.
479
+ await waitForComponentToBeInitializedWithSelectedTab(
480
+ 'Beta'
481
+ );
482
+
483
+ // Press tab. The selected tab (beta) received focus. The corresponding
484
+ // tabpanel is shown.
485
+ await press.Tab();
486
+ expect(
487
+ await screen.findByRole( 'tab', {
488
+ selected: true,
489
+ name: 'Beta',
490
+ } )
491
+ ).toHaveFocus();
492
+ } );
493
+ } );
494
+ describe( 'when `null` [Controlled]', () => {
495
+ it( 'should not have a selected tab nor show any tabpanels, make the tablist tabbable and still allow selecting tabs', async () => {
496
+ await render(
497
+ <ControlledTabs tabs={ TABS } selectedTabId={ null } />
498
+ );
499
+
500
+ // No initially selected tabs or tabpanels.
501
+ await waitForComponentToBeInitializedWithSelectedTab(
502
+ undefined
503
+ );
504
+
505
+ // Press tab. The tablist receives focus
506
+ await press.Tab();
507
+ expect(
508
+ await screen.findByRole( 'tablist' )
509
+ ).toHaveFocus();
435
510
 
436
- // onSelect gets called on the initial render.
437
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
438
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
511
+ // Press right arrow to select the first tab (alpha) and
512
+ // show the related tabpanel.
513
+ await press.ArrowRight();
514
+ expect(
515
+ await screen.findByRole( 'tab', {
516
+ selected: true,
517
+ name: 'Alpha',
518
+ } )
519
+ ).toHaveFocus();
520
+ expect(
521
+ await screen.findByRole( 'tabpanel', {
522
+ name: 'Alpha',
523
+ } )
524
+ ).toBeVisible();
525
+ } );
526
+ } );
527
+ } );
439
528
 
440
- // Tab to focus the tablist. Make sure Alpha is focused.
441
- await press.Tab();
442
- expect( await getSelectedTab() ).toHaveFocus();
529
+ describe( 'when a selected tab id is specified', () => {
530
+ describe( 'through the `defaultTabId` prop [Uncontrolled]', () => {
531
+ it( 'should select the initial tab matching the `defaultTabId` prop', async () => {
532
+ await render(
533
+ <UncontrolledTabs tabs={ TABS } defaultTabId="beta" />
534
+ );
535
+
536
+ // Beta is the initially selected tab
537
+ await waitForComponentToBeInitializedWithSelectedTab(
538
+ 'Beta'
539
+ );
540
+
541
+ // Press tab. The selected tab (beta) received focus. The corresponding
542
+ // tabpanel is shown.
543
+ await press.Tab();
544
+ expect(
545
+ await screen.findByRole( 'tab', {
546
+ selected: true,
547
+ name: 'Beta',
548
+ } )
549
+ ).toHaveFocus();
550
+ } );
551
+
552
+ it( 'should select the initial tab matching the `defaultTabId` prop even if the tab is disabled', async () => {
553
+ await render(
554
+ <UncontrolledTabs
555
+ tabs={ TABS_WITH_BETA_DISABLED }
556
+ defaultTabId="beta"
557
+ />
558
+ );
559
+
560
+ // Beta is automatically selected as the selected tab despite being
561
+ // disabled, respecting the `defaultTabId` prop.
562
+ await waitForComponentToBeInitializedWithSelectedTab(
563
+ 'Beta'
564
+ );
565
+
566
+ // Press tab. The selected tab (beta) received focus, since it is
567
+ // accessible despite being disabled.
568
+ await press.Tab();
569
+ expect(
570
+ await screen.findByRole( 'tab', {
571
+ selected: true,
572
+ name: 'Beta',
573
+ } )
574
+ ).toHaveFocus();
575
+ } );
576
+
577
+ it( 'should not have a selected tab nor show any tabpanels, but allow tabbing to the first tab when `defaultTabId` prop does not match any known tab', async () => {
578
+ await render(
579
+ <UncontrolledTabs
580
+ tabs={ TABS }
581
+ defaultTabId="non-existing-tab"
582
+ />
583
+ );
584
+
585
+ // No initially selected tabs or tabpanels, since the `defaultTabId`
586
+ // prop is not matching any known tabs.
587
+ await waitForComponentToBeInitializedWithSelectedTab(
588
+ undefined
589
+ );
590
+
591
+ // Press tab. The first tab receives focus, but it's
592
+ // not selected.
593
+ await press.Tab();
594
+ expect(
595
+ screen.getByRole( 'tab', { name: 'Alpha' } )
596
+ ).toHaveFocus();
597
+ await waitFor( () =>
598
+ expect(
599
+ screen.queryByRole( 'tab', { selected: true } )
600
+ ).not.toBeInTheDocument()
601
+ );
602
+ await waitFor( () =>
603
+ expect(
604
+ screen.queryByRole( 'tabpanel' )
605
+ ).not.toBeInTheDocument()
606
+ );
443
607
 
444
- // Press the arrow up key, nothing happens.
445
- await press.ArrowUp();
446
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
447
- expect( await getSelectedTab() ).toHaveFocus();
448
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
449
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
608
+ // Press right arrow to select the next tab (beta) and
609
+ // show the related tabpanel.
610
+ await press.ArrowRight();
611
+ expect(
612
+ await screen.findByRole( 'tab', {
613
+ selected: true,
614
+ name: 'Beta',
615
+ } )
616
+ ).toHaveFocus();
617
+ expect(
618
+ await screen.findByRole( 'tabpanel', {
619
+ name: 'Beta',
620
+ } )
621
+ ).toBeVisible();
622
+ } );
623
+
624
+ it( 'should not have a selected tab nor show any tabpanels, but allow tabbing to the first tab, even when disabled, when `defaultTabId` prop does not match any known tab', async () => {
625
+ await render(
626
+ <UncontrolledTabs
627
+ tabs={ TABS_WITH_ALPHA_DISABLED }
628
+ defaultTabId="non-existing-tab"
629
+ />
630
+ );
631
+
632
+ // No initially selected tabs or tabpanels, since the `defaultTabId`
633
+ // prop is not matching any known tabs.
634
+ await waitForComponentToBeInitializedWithSelectedTab(
635
+ undefined
636
+ );
637
+
638
+ // Press tab. The first tab receives focus, but it's
639
+ // not selected.
640
+ await press.Tab();
641
+ expect(
642
+ screen.getByRole( 'tab', { name: 'Alpha' } )
643
+ ).toHaveFocus();
644
+ await waitFor( () =>
645
+ expect(
646
+ screen.queryByRole( 'tab', { selected: true } )
647
+ ).not.toBeInTheDocument()
648
+ );
649
+ await waitFor( () =>
650
+ expect(
651
+ screen.queryByRole( 'tabpanel' )
652
+ ).not.toBeInTheDocument()
653
+ );
450
654
 
451
- // Press the arrow down key, nothing happens
452
- await press.ArrowDown();
453
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
454
- expect( await getSelectedTab() ).toHaveFocus();
455
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
456
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
655
+ // Press right arrow to select the next tab (beta) and
656
+ // show the related tabpanel.
657
+ await press.ArrowRight();
658
+ expect(
659
+ await screen.findByRole( 'tab', {
660
+ selected: true,
661
+ name: 'Beta',
662
+ } )
663
+ ).toHaveFocus();
664
+ expect(
665
+ await screen.findByRole( 'tabpanel', {
666
+ name: 'Beta',
667
+ } )
668
+ ).toBeVisible();
669
+ } );
670
+
671
+ it( 'should ignore any changes to the `defaultTabId` prop after the first render', async () => {
672
+ const mockOnSelect = jest.fn();
673
+
674
+ const { rerender } = await render(
675
+ <UncontrolledTabs
676
+ tabs={ TABS }
677
+ defaultTabId="beta"
678
+ onSelect={ mockOnSelect }
679
+ />
680
+ );
681
+
682
+ // Beta is the initially selected tab
683
+ await waitForComponentToBeInitializedWithSelectedTab(
684
+ 'Beta'
685
+ );
686
+
687
+ // Changing the defaultTabId prop to gamma should not have any effect.
688
+ await rerender(
689
+ <UncontrolledTabs
690
+ tabs={ TABS }
691
+ defaultTabId="gamma"
692
+ onSelect={ mockOnSelect }
693
+ />
694
+ );
457
695
 
458
- // Change orientation to `vertical`. When the orientation is vertical,
459
- // left/right arrow keys are replaced by up/down arrow keys.
460
- await rerender(
461
- <UncontrolledTabs
462
- tabs={ TABS }
463
- onSelect={ mockOnSelect }
464
- orientation="vertical"
465
- />
466
- );
696
+ expect(
697
+ await screen.findByRole( 'tab', {
698
+ selected: true,
699
+ name: 'Beta',
700
+ } )
701
+ ).toBeVisible();
702
+ expect(
703
+ screen.getByRole( 'tabpanel', {
704
+ name: 'Beta',
705
+ } )
706
+ ).toBeVisible();
467
707
 
468
- expect( screen.getByRole( 'tablist' ) ).toHaveAttribute(
469
- 'aria-orientation',
470
- 'vertical'
471
- );
708
+ expect( mockOnSelect ).not.toHaveBeenCalled();
709
+ } );
710
+ } );
472
711
 
473
- // Make sure alpha is still focused.
474
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
475
- expect( await getSelectedTab() ).toHaveFocus();
712
+ describe( 'through the `selectedTabId` prop [Controlled]', () => {
713
+ describe( 'when the `selectedTabId` matches an existing tab', () => {
714
+ it( 'should choose the initial tab matching the `selectedTabId`', async () => {
715
+ await render(
716
+ <ControlledTabs
717
+ tabs={ TABS }
718
+ selectedTabId="beta"
719
+ />
720
+ );
476
721
 
477
- // Navigate forward with arrow keys and make sure the Beta tab is
478
- // selected automatically.
479
- await press.ArrowDown();
480
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
481
- expect( await getSelectedTab() ).toHaveFocus();
482
- expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
483
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
722
+ // Beta is the initially selected tab
723
+ await waitForComponentToBeInitializedWithSelectedTab(
724
+ 'Beta'
725
+ );
484
726
 
485
- // Navigate backwards with arrow keys. Make sure alpha is
486
- // selected automatically.
487
- await press.ArrowUp();
488
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
489
- expect( await getSelectedTab() ).toHaveFocus();
490
- expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
491
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
727
+ // Press tab. The selected tab (beta) received focus, since it is
728
+ // accessible despite being disabled.
729
+ await press.Tab();
730
+ expect(
731
+ await screen.findByRole( 'tab', {
732
+ selected: true,
733
+ name: 'Beta',
734
+ } )
735
+ ).toHaveFocus();
736
+ } );
492
737
 
493
- // Navigate backwards with arrow keys. Make sure alpha is
494
- // selected automatically.
495
- await press.ArrowUp();
496
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
497
- expect( await getSelectedTab() ).toHaveFocus();
498
- expect( mockOnSelect ).toHaveBeenCalledTimes( 4 );
499
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
500
-
501
- // Navigate backwards with arrow keys. Make sure alpha is
502
- // selected automatically.
503
- await press.ArrowDown();
504
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
505
- expect( await getSelectedTab() ).toHaveFocus();
506
- expect( mockOnSelect ).toHaveBeenCalledTimes( 5 );
507
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
508
- } );
738
+ it( 'should choose the initial tab matching the `selectedTabId` even if a `defaultTabId` is passed', async () => {
739
+ await render(
740
+ <ControlledTabs
741
+ tabs={ TABS }
742
+ defaultTabId="beta"
743
+ selectedTabId="gamma"
744
+ />
745
+ );
509
746
 
510
- it( 'should move focus on a tab even if disabled with arrow key, but not with pointer clicks', async () => {
511
- const mockOnSelect = jest.fn();
747
+ // Gamma is the initially selected tab
748
+ await waitForComponentToBeInitializedWithSelectedTab(
749
+ 'Gamma'
750
+ );
512
751
 
513
- const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( ( tabObj ) =>
514
- tabObj.tabId === 'delta'
515
- ? {
516
- ...tabObj,
517
- tab: {
518
- ...tabObj.tab,
519
- disabled: true,
520
- },
521
- }
522
- : tabObj
523
- );
752
+ // Press tab. The selected tab (gamma) received focus, since it is
753
+ // accessible despite being disabled.
754
+ await press.Tab();
755
+ expect(
756
+ await screen.findByRole( 'tab', {
757
+ selected: true,
758
+ name: 'Gamma',
759
+ } )
760
+ ).toHaveFocus();
761
+ } );
524
762
 
525
- await render(
526
- <UncontrolledTabs
527
- tabs={ TABS_WITH_DELTA_DISABLED }
528
- onSelect={ mockOnSelect }
529
- />
530
- );
763
+ it( 'should choose the initial tab matching the `selectedTabId` even if the tab is disabled', async () => {
764
+ await render(
765
+ <ControlledTabs
766
+ tabs={ TABS_WITH_BETA_DISABLED }
767
+ selectedTabId="beta"
768
+ />
769
+ );
531
770
 
532
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
533
- expect( await getSelectedTab() ).not.toHaveFocus();
771
+ // Beta is the initially selected tab
772
+ await waitForComponentToBeInitializedWithSelectedTab(
773
+ 'Beta'
774
+ );
534
775
 
535
- // onSelect gets called on the initial render.
536
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
537
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
776
+ // Press tab. The selected tab (beta) received focus, since it is
777
+ // accessible despite being disabled.
778
+ await press.Tab();
779
+ expect(
780
+ await screen.findByRole( 'tab', {
781
+ selected: true,
782
+ name: 'Beta',
783
+ } )
784
+ ).toHaveFocus();
785
+ } );
786
+ } );
538
787
 
539
- // Tab to focus the tablist. Make sure Alpha is focused.
540
- await press.Tab();
541
- expect( await getSelectedTab() ).toHaveFocus();
788
+ describe( "when the `selectedTabId` doesn't match an existing tab", () => {
789
+ it( 'should not have a selected tab nor show any tabpanels, but allow tabbing to the first tab', async () => {
790
+ await render(
791
+ <ControlledTabs
792
+ tabs={ TABS }
793
+ selectedTabId="non-existing-tab"
794
+ />
795
+ );
542
796
 
543
- // Confirm onSelect has not been re-called
544
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
797
+ // No initially selected tabs or tabpanels, since the `selectedTabId`
798
+ // prop is not matching any known tabs.
799
+ await waitForComponentToBeInitializedWithSelectedTab(
800
+ undefined
801
+ );
545
802
 
546
- // Press the right arrow key three times. Since the delta tab is disabled:
547
- // - it won't be selected. The gamma tab will be selected instead, since
548
- // it was the tab that was last selected before delta. Therefore, the
549
- // `mockOnSelect` function gets called only twice (and not three times)
550
- // - it will receive focus, when using arrow keys
551
- await press.ArrowRight();
552
- await press.ArrowRight();
553
- await press.ArrowRight();
554
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
555
- expect(
556
- screen.getByRole( 'tab', { name: 'Delta' } )
557
- ).toHaveFocus();
558
- expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
559
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
560
-
561
- // Navigate backwards with arrow keys. The gamma tab receives focus.
562
- // The `mockOnSelect` callback doesn't fire, since the gamma tab was
563
- // already selected.
564
- await press.ArrowLeft();
565
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
566
- expect( await getSelectedTab() ).toHaveFocus();
567
- expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
803
+ // Press tab. The first tab receives focus, but it's
804
+ // not selected.
805
+ await press.Tab();
806
+ expect(
807
+ screen.getByRole( 'tab', { name: 'Alpha' } )
808
+ ).toHaveFocus();
809
+ await waitFor( () =>
810
+ expect(
811
+ screen.queryByRole( 'tab', { selected: true } )
812
+ ).not.toBeInTheDocument()
813
+ );
814
+ await waitFor( () =>
815
+ expect(
816
+ screen.queryByRole( 'tabpanel' )
817
+ ).not.toBeInTheDocument()
818
+ );
568
819
 
569
- // Click on the disabled tab. Compared to using arrow keys to move the
570
- // focus, disabled tabs ignore pointer clicks — and therefore, they don't
571
- // receive focus, nor they cause the `mockOnSelect` function to fire.
572
- await click( screen.getByRole( 'tab', { name: 'Delta' } ) );
573
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
574
- expect( await getSelectedTab() ).toHaveFocus();
575
- expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
576
- } );
820
+ // Press right arrow to select the next tab (beta) and
821
+ // show the related tabpanel.
822
+ await press.ArrowRight();
823
+ expect(
824
+ await screen.findByRole( 'tab', {
825
+ selected: true,
826
+ name: 'Beta',
827
+ } )
828
+ ).toHaveFocus();
829
+ expect(
830
+ await screen.findByRole( 'tabpanel', {
831
+ name: 'Beta',
832
+ } )
833
+ ).toBeVisible();
834
+ } );
577
835
 
578
- it( 'should not focus the next tab when the Tab key is pressed', async () => {
579
- await render( <UncontrolledTabs tabs={ TABS } /> );
836
+ it( 'should not have a selected tab nor show any tabpanels, but allow tabbing to the first tab even when disabled', async () => {
837
+ await render(
838
+ <ControlledTabs
839
+ tabs={ TABS_WITH_ALPHA_DISABLED }
840
+ selectedTabId="non-existing-tab"
841
+ />
842
+ );
580
843
 
581
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
582
- expect( await getSelectedTab() ).not.toHaveFocus();
844
+ // No initially selected tabs or tabpanels, since the `selectedTabId`
845
+ // prop is not matching any known tabs.
846
+ await waitForComponentToBeInitializedWithSelectedTab(
847
+ undefined
848
+ );
583
849
 
584
- // Tab should initially focus the first tab in the tablist, which
585
- // is Alpha.
586
- await press.Tab();
587
- expect(
588
- await screen.findByRole( 'tab', { name: 'Alpha' } )
589
- ).toHaveFocus();
850
+ // Press tab. The first tab receives focus, but it's
851
+ // not selected.
852
+ await press.Tab();
853
+ expect(
854
+ screen.getByRole( 'tab', { name: 'Alpha' } )
855
+ ).toHaveFocus();
856
+ await waitFor( () =>
857
+ expect(
858
+ screen.queryByRole( 'tab', { selected: true } )
859
+ ).not.toBeInTheDocument()
860
+ );
861
+ await waitFor( () =>
862
+ expect(
863
+ screen.queryByRole( 'tabpanel' )
864
+ ).not.toBeInTheDocument()
865
+ );
590
866
 
591
- // Because all other tabs should have `tabindex=-1`, pressing Tab
592
- // should NOT move the focus to the next tab, which is Beta.
593
- // Instead, focus should go to the currently selected tabpanel (alpha).
594
- await press.Tab();
595
- expect(
596
- await screen.findByRole( 'tabpanel', {
597
- name: 'Alpha',
598
- } )
599
- ).toHaveFocus();
867
+ // Press right arrow to select the next tab (beta) and
868
+ // show the related tabpanel.
869
+ await press.ArrowRight();
870
+ expect(
871
+ await screen.findByRole( 'tab', {
872
+ selected: true,
873
+ name: 'Beta',
874
+ } )
875
+ ).toHaveFocus();
876
+ expect(
877
+ await screen.findByRole( 'tabpanel', {
878
+ name: 'Beta',
879
+ } )
880
+ ).toBeVisible();
881
+ } );
882
+ } );
883
+ } );
600
884
  } );
885
+ } );
601
886
 
602
- it( 'switches to manual tab activation when the `selectOnMove` prop is set to `false`', async () => {
603
- const mockOnSelect = jest.fn();
887
+ describe( 'keyboard interactions', () => {
888
+ describe.each( [
889
+ [ 'Uncontrolled', UncontrolledTabs ],
890
+ [ 'Controlled', ControlledTabs ],
891
+ ] )( '[`%s`]', ( _mode, Component ) => {
892
+ it( 'should handle the tablist as one tab stop', async () => {
893
+ await render( <Component tabs={ TABS } /> );
604
894
 
605
- await render(
606
- <UncontrolledTabs
607
- tabs={ TABS }
608
- onSelect={ mockOnSelect }
609
- selectOnMove={ false }
610
- />
611
- );
895
+ // Alpha is automatically selected as the selected tab.
896
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
612
897
 
613
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
614
- expect( await getSelectedTab() ).not.toHaveFocus();
898
+ // Press tab. The selected tab (alpha) received focus.
899
+ await press.Tab();
900
+ expect(
901
+ await screen.findByRole( 'tab', {
902
+ selected: true,
903
+ name: 'Alpha',
904
+ } )
905
+ ).toHaveFocus();
615
906
 
616
- // onSelect gets called on the initial render.
617
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
618
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
907
+ // By default the tabpanel should receive focus
908
+ await press.Tab();
909
+ expect(
910
+ await screen.findByRole( 'tabpanel', {
911
+ name: 'Alpha',
912
+ } )
913
+ ).toHaveFocus();
914
+ } );
619
915
 
620
- // Click on Alpha and make sure it is selected.
621
- // onSelect shouldn't fire since the selected tab didn't change.
622
- await click( screen.getByRole( 'tab', { name: 'Alpha' } ) );
623
- expect(
624
- await screen.findByRole( 'tab', { name: 'Alpha' } )
625
- ).toHaveFocus();
626
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
627
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
628
-
629
- // Navigate forward with arrow keys. Make sure Beta is focused, but
630
- // that the tab selection happens only when pressing the spacebar
631
- // or enter key. onSelect shouldn't fire since the selected tab
632
- // didn't change.
633
- await press.ArrowRight();
634
- expect(
635
- await screen.findByRole( 'tab', { name: 'Beta' } )
636
- ).toHaveFocus();
637
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
638
-
639
- await press.Enter();
640
- expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
641
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
916
+ it( 'should not focus the tabpanel container when its `focusable` property is set to `false`', async () => {
917
+ await render(
918
+ <Component
919
+ tabs={ TABS.map( ( tabObj ) =>
920
+ tabObj.tabId === 'alpha'
921
+ ? {
922
+ ...tabObj,
923
+ content: (
924
+ <>
925
+ Selected Tab: Alpha
926
+ <button>Alpha Button</button>
927
+ </>
928
+ ),
929
+ tabpanel: { focusable: false },
930
+ }
931
+ : tabObj
932
+ ) }
933
+ />
934
+ );
642
935
 
643
- // Navigate forward with arrow keys. Make sure Gamma (last tab) is
644
- // focused, but that tab selection happens only when pressing the
645
- // spacebar or enter key. onSelect shouldn't fire since the selected
646
- // tab didn't change.
647
- await press.ArrowRight();
648
- expect(
649
- await screen.findByRole( 'tab', { name: 'Gamma' } )
650
- ).toHaveFocus();
651
- expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
652
- expect(
653
- screen.getByRole( 'tab', { name: 'Gamma' } )
654
- ).toHaveFocus();
936
+ // Alpha is automatically selected as the selected tab.
937
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
655
938
 
656
- await press.Space();
657
- expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
658
- expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
659
- } );
660
- } );
661
- describe( 'Uncontrolled mode', () => {
662
- describe( 'Without `defaultTabId` prop', () => {
663
- it( 'should render first tab', async () => {
664
- await render( <UncontrolledTabs tabs={ TABS } /> );
939
+ // Tab should initially focus the first tab in the tablist, which
940
+ // is Alpha.
941
+ await press.Tab();
942
+ expect(
943
+ await screen.findByRole( 'tab', {
944
+ selected: true,
945
+ name: 'Alpha',
946
+ } )
947
+ ).toHaveFocus();
665
948
 
666
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
949
+ // In this case, the tabpanel container is skipped and focus is
950
+ // moved directly to its contents
951
+ await press.Tab();
667
952
  expect(
668
- await screen.findByRole( 'tabpanel', { name: 'Alpha' } )
669
- ).toBeInTheDocument();
953
+ await screen.findByRole( 'button', {
954
+ name: 'Alpha Button',
955
+ } )
956
+ ).toHaveFocus();
670
957
  } );
671
- it( 'should not have a selected tab if the currently selected tab is removed', async () => {
672
- const { rerender } = await render(
673
- <UncontrolledTabs tabs={ TABS } />
674
- );
675
958
 
676
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
677
- expect( await getSelectedTab() ).not.toHaveFocus();
959
+ it( 'should select tabs in the tablist when using the left and right arrow keys by default (automatic tab activation)', async () => {
960
+ const mockOnSelect = jest.fn();
678
961
 
679
- // Tab to focus the tablist. Make sure Alpha is focused.
680
- await press.Tab();
681
- expect( await getSelectedTab() ).toHaveFocus();
962
+ await render(
963
+ <Component tabs={ TABS } onSelect={ mockOnSelect } />
964
+ );
682
965
 
683
- // Remove first item from `TABS` array
684
- await rerender( <UncontrolledTabs tabs={ TABS.slice( 1 ) } /> );
966
+ // Alpha is automatically selected as the selected tab.
967
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
685
968
 
686
- // No tab should be selected i.e. it doesn't fall back to first tab.
687
- await waitFor( () =>
688
- expect(
689
- screen.queryByRole( 'tab', { selected: true } )
690
- ).not.toBeInTheDocument()
691
- );
969
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
970
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
692
971
 
693
- // No tabpanel should be rendered either
972
+ // Focus the tablist (and the selected tab, alpha)
973
+ // Tab should initially focus the first tab in the tablist, which
974
+ // is Alpha.
975
+ await press.Tab();
694
976
  expect(
695
- screen.queryByRole( 'tabpanel' )
696
- ).not.toBeInTheDocument();
697
- } );
698
- } );
977
+ await screen.findByRole( 'tab', {
978
+ selected: true,
979
+ name: 'Alpha',
980
+ } )
981
+ ).toHaveFocus();
699
982
 
700
- describe( 'With `defaultTabId`', () => {
701
- it( 'should render the tab set by `defaultTabId` prop', async () => {
702
- await render(
703
- <UncontrolledTabs tabs={ TABS } defaultTabId="beta" />
704
- );
983
+ // Press the right arrow key to select the beta tab
984
+ await press.ArrowRight();
705
985
 
706
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
707
- } );
986
+ expect(
987
+ screen.getByRole( 'tab', {
988
+ selected: true,
989
+ name: 'Beta',
990
+ } )
991
+ ).toHaveFocus();
992
+ expect(
993
+ screen.getByRole( 'tabpanel', {
994
+ name: 'Beta',
995
+ } )
996
+ ).toBeVisible();
708
997
 
709
- it( 'should not select a tab when `defaultTabId` does not match any known tab', async () => {
710
- await render(
711
- <UncontrolledTabs
712
- tabs={ TABS }
713
- defaultTabId="does-not-exist"
714
- />
715
- );
998
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
999
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
716
1000
 
717
- // No tab should be selected i.e. it doesn't fall back to first tab.
718
- expect(
719
- screen.queryByRole( 'tab', { selected: true } )
720
- ).not.toBeInTheDocument();
1001
+ // Press the right arrow key to select the gamma tab
1002
+ await press.ArrowRight();
721
1003
 
722
- // No tabpanel should be rendered either
723
1004
  expect(
724
- screen.queryByRole( 'tabpanel' )
725
- ).not.toBeInTheDocument();
726
- } );
727
- it( 'should not change tabs when defaultTabId is changed', async () => {
728
- const { rerender } = await render(
729
- <UncontrolledTabs tabs={ TABS } defaultTabId="beta" />
730
- );
1005
+ screen.getByRole( 'tab', {
1006
+ selected: true,
1007
+ name: 'Gamma',
1008
+ } )
1009
+ ).toHaveFocus();
1010
+ expect(
1011
+ screen.getByRole( 'tabpanel', {
1012
+ name: 'Gamma',
1013
+ } )
1014
+ ).toBeVisible();
731
1015
 
732
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1016
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
1017
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
733
1018
 
734
- await rerender(
735
- <UncontrolledTabs tabs={ TABS } defaultTabId="alpha" />
736
- );
1019
+ // Press the left arrow key to select the beta tab
1020
+ await press.ArrowLeft();
737
1021
 
738
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1022
+ expect(
1023
+ screen.getByRole( 'tab', {
1024
+ selected: true,
1025
+ name: 'Beta',
1026
+ } )
1027
+ ).toHaveFocus();
1028
+ expect(
1029
+ screen.getByRole( 'tabpanel', {
1030
+ name: 'Beta',
1031
+ } )
1032
+ ).toBeVisible();
1033
+
1034
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 4 );
1035
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
739
1036
  } );
740
1037
 
741
- it( 'should not have any selected tabs if the currently selected tab is removed, even if a tab is matching the defaultTabId', async () => {
1038
+ it( 'should not automatically select tabs in the tablist when pressing the left and right arrow keys if the `selectOnMove` prop is set to `false` (manual tab activation)', async () => {
742
1039
  const mockOnSelect = jest.fn();
743
1040
 
744
- const { rerender } = await render(
745
- <UncontrolledTabs
1041
+ await render(
1042
+ <Component
746
1043
  tabs={ TABS }
747
- defaultTabId="gamma"
748
1044
  onSelect={ mockOnSelect }
1045
+ selectOnMove={ false }
749
1046
  />
750
1047
  );
751
1048
 
752
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
1049
+ // Alpha is automatically selected as the selected tab.
1050
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
753
1051
 
754
- await click( screen.getByRole( 'tab', { name: 'Alpha' } ) );
755
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
1052
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
756
1053
  expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
757
1054
 
758
- await rerender(
759
- <UncontrolledTabs
760
- tabs={ TABS.slice( 1 ) }
761
- defaultTabId="gamma"
762
- onSelect={ mockOnSelect }
763
- />
764
- );
1055
+ // Focus the tablist (and the selected tab, alpha)
1056
+ // Tab should initially focus the first tab in the tablist, which
1057
+ // is Alpha.
1058
+ await press.Tab();
1059
+ expect(
1060
+ await screen.findByRole( 'tab', {
1061
+ selected: true,
1062
+ name: 'Alpha',
1063
+ } )
1064
+ ).toHaveFocus();
765
1065
 
766
- // No tab should be selected i.e. it doesn't fall back to first tab.
767
- await waitFor( () =>
768
- expect(
769
- screen.queryByRole( 'tab', { selected: true } )
770
- ).not.toBeInTheDocument()
771
- );
1066
+ // Press the right arrow key to move focus to the beta tab,
1067
+ // but without selecting it
1068
+ await press.ArrowRight();
1069
+
1070
+ expect(
1071
+ screen.getByRole( 'tab', {
1072
+ selected: false,
1073
+ name: 'Beta',
1074
+ } )
1075
+ ).toHaveFocus();
1076
+ expect(
1077
+ await screen.findByRole( 'tab', {
1078
+ selected: true,
1079
+ name: 'Alpha',
1080
+ } )
1081
+ ).toBeVisible();
1082
+ expect(
1083
+ screen.getByRole( 'tabpanel', {
1084
+ name: 'Alpha',
1085
+ } )
1086
+ ).toBeVisible();
1087
+
1088
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
772
1089
 
773
- // No tabpanel should be rendered either
1090
+ // Press the space key to click the beta tab, and select it.
1091
+ // The same should be true with any other mean of clicking the tab button
1092
+ // (ie. mouse click, enter key).
1093
+ await press.Space();
1094
+
1095
+ expect(
1096
+ screen.getByRole( 'tab', {
1097
+ selected: true,
1098
+ name: 'Beta',
1099
+ } )
1100
+ ).toHaveFocus();
774
1101
  expect(
775
- screen.queryByRole( 'tabpanel' )
776
- ).not.toBeInTheDocument();
1102
+ screen.getByRole( 'tabpanel', {
1103
+ name: 'Beta',
1104
+ } )
1105
+ ).toBeVisible();
1106
+
1107
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
1108
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
777
1109
  } );
778
1110
 
779
- it( 'should keep the currently selected tab even if it becomes disabled', async () => {
1111
+ it( 'should not select tabs in the tablist when using the up and down arrow keys, unless the `orientation` prop is set to `vertical`', async () => {
780
1112
  const mockOnSelect = jest.fn();
781
1113
 
782
1114
  const { rerender } = await render(
783
- <UncontrolledTabs
784
- tabs={ TABS }
785
- defaultTabId="gamma"
786
- onSelect={ mockOnSelect }
787
- />
1115
+ <Component tabs={ TABS } onSelect={ mockOnSelect } />
788
1116
  );
789
1117
 
790
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
791
-
792
- await click( screen.getByRole( 'tab', { name: 'Alpha' } ) );
793
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
1118
+ // Alpha is automatically selected as the selected tab.
1119
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
794
1120
 
795
1121
  expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
796
1122
  expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
797
1123
 
798
- const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) =>
799
- tabObj.tabId === 'alpha'
800
- ? {
801
- ...tabObj,
802
- tab: {
803
- ...tabObj.tab,
804
- disabled: true,
805
- },
806
- }
807
- : tabObj
808
- );
1124
+ // Focus the tablist (and the selected tab, alpha)
1125
+ // Tab should initially focus the first tab in the tablist, which
1126
+ // is Alpha.
1127
+ await press.Tab();
1128
+ expect(
1129
+ await screen.findByRole( 'tab', {
1130
+ selected: true,
1131
+ name: 'Alpha',
1132
+ } )
1133
+ ).toHaveFocus();
809
1134
 
810
- await rerender(
811
- <UncontrolledTabs
812
- tabs={ TABS_WITH_ALPHA_DISABLED }
813
- defaultTabId="gamma"
814
- onSelect={ mockOnSelect }
815
- />
816
- );
1135
+ // Press the up arrow key, but the focused/selected tab does not change.
1136
+ await press.ArrowUp();
1137
+
1138
+ expect(
1139
+ screen.getByRole( 'tab', {
1140
+ selected: true,
1141
+ name: 'Alpha',
1142
+ } )
1143
+ ).toHaveFocus();
1144
+ expect(
1145
+ screen.getByRole( 'tabpanel', {
1146
+ name: 'Alpha',
1147
+ } )
1148
+ ).toBeVisible();
817
1149
 
818
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
819
1150
  expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
820
- } );
821
1151
 
822
- it( 'should have no active tabs when the tab associated to `defaultTabId` is removed while being the active tab', async () => {
823
- const { rerender } = await render(
824
- <UncontrolledTabs tabs={ TABS } defaultTabId="gamma" />
825
- );
1152
+ // Press the down arrow key, but the focused/selected tab does not change.
1153
+ await press.ArrowDown();
1154
+
1155
+ expect(
1156
+ screen.getByRole( 'tab', {
1157
+ selected: true,
1158
+ name: 'Alpha',
1159
+ } )
1160
+ ).toHaveFocus();
1161
+ expect(
1162
+ screen.getByRole( 'tabpanel', {
1163
+ name: 'Alpha',
1164
+ } )
1165
+ ).toBeVisible();
826
1166
 
827
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
1167
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
828
1168
 
829
- // Remove gamma
1169
+ // Change the orientation to "vertical" and rerender the component.
830
1170
  await rerender(
831
- <UncontrolledTabs
832
- tabs={ TABS.slice( 0, 2 ) }
833
- defaultTabId="gamma"
1171
+ <Component
1172
+ tabs={ TABS }
1173
+ onSelect={ mockOnSelect }
1174
+ orientation="vertical"
834
1175
  />
835
1176
  );
836
1177
 
837
- expect( screen.getAllByRole( 'tab' ) ).toHaveLength( 2 );
838
- // No tab should be selected i.e. it doesn't fall back to first tab.
1178
+ // Pressing the down arrow key now selects the next tab (beta).
1179
+ await press.ArrowDown();
1180
+
839
1181
  expect(
840
- screen.queryByRole( 'tab', { selected: true } )
841
- ).not.toBeInTheDocument();
842
- // No tabpanel should be rendered either
1182
+ screen.getByRole( 'tab', {
1183
+ selected: true,
1184
+ name: 'Beta',
1185
+ } )
1186
+ ).toHaveFocus();
843
1187
  expect(
844
- screen.queryByRole( 'tabpanel' )
845
- ).not.toBeInTheDocument();
846
- } );
1188
+ screen.getByRole( 'tabpanel', {
1189
+ name: 'Beta',
1190
+ } )
1191
+ ).toBeVisible();
847
1192
 
848
- it( 'waits for the tab with the `defaultTabId` to be present in the `tabs` array before selecting it', async () => {
849
- const { rerender } = await render(
850
- <UncontrolledTabs tabs={ TABS } defaultTabId="delta" />
851
- );
1193
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
1194
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
852
1195
 
853
- // No tab should be selected i.e. it doesn't fall back to first tab.
854
- await waitFor( () =>
855
- expect(
856
- screen.queryByRole( 'tab', { selected: true } )
857
- ).not.toBeInTheDocument()
858
- );
1196
+ // Pressing the up arrow key now selects the previous tab (alpha).
1197
+ await press.ArrowUp();
859
1198
 
860
- // No tabpanel should be rendered either
861
1199
  expect(
862
- screen.queryByRole( 'tabpanel' )
863
- ).not.toBeInTheDocument();
864
-
865
- await rerender(
866
- <UncontrolledTabs
867
- tabs={ TABS_WITH_DELTA }
868
- defaultTabId="delta"
869
- />
870
- );
1200
+ screen.getByRole( 'tab', {
1201
+ selected: true,
1202
+ name: 'Alpha',
1203
+ } )
1204
+ ).toHaveFocus();
1205
+ expect(
1206
+ screen.getByRole( 'tabpanel', {
1207
+ name: 'Alpha',
1208
+ } )
1209
+ ).toBeVisible();
871
1210
 
872
- expect( await getSelectedTab() ).toHaveTextContent( 'Delta' );
1211
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
1212
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
873
1213
  } );
874
- } );
875
1214
 
876
- describe( 'Disabled tab', () => {
877
- it( 'should disable the tab when `disabled` is `true`', async () => {
1215
+ it( 'should loop tab focus at the end of the tablist when using arrow keys', async () => {
878
1216
  const mockOnSelect = jest.fn();
879
1217
 
880
- const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map(
881
- ( tabObj ) =>
882
- tabObj.tabId === 'delta'
883
- ? {
884
- ...tabObj,
885
- tab: {
886
- ...tabObj.tab,
887
- disabled: true,
888
- },
889
- }
890
- : tabObj
891
- );
892
-
893
1218
  await render(
894
- <UncontrolledTabs
895
- tabs={ TABS_WITH_DELTA_DISABLED }
896
- onSelect={ mockOnSelect }
897
- />
1219
+ <Component tabs={ TABS } onSelect={ mockOnSelect } />
898
1220
  );
899
1221
 
900
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
1222
+ // Alpha is automatically selected as the selected tab.
1223
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
901
1224
 
902
- expect(
903
- screen.getByRole( 'tab', { name: 'Delta' } )
904
- ).toHaveAttribute( 'aria-disabled', 'true' );
905
-
906
- // onSelect gets called on the initial render.
907
1225
  expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
908
1226
  expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
909
1227
 
910
- // Move focus to the tablist, make sure alpha is focused.
1228
+ // Focus the tablist (and the selected tab, alpha)
1229
+ // Tab should initially focus the first tab in the tablist, which
1230
+ // is Alpha.
911
1231
  await press.Tab();
912
1232
  expect(
913
- screen.getByRole( 'tab', { name: 'Alpha' } )
1233
+ await screen.findByRole( 'tab', {
1234
+ selected: true,
1235
+ name: 'Alpha',
1236
+ } )
914
1237
  ).toHaveFocus();
915
1238
 
916
- // onSelect should not be called since the disabled tab is
917
- // highlighted, but not selected.
1239
+ // Press the left arrow key to loop around and select the gamma tab
918
1240
  await press.ArrowLeft();
919
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
920
1241
 
921
- // Delta (which is disabled) has focus
922
1242
  expect(
923
- screen.getByRole( 'tab', { name: 'Delta' } )
1243
+ screen.getByRole( 'tab', {
1244
+ selected: true,
1245
+ name: 'Gamma',
1246
+ } )
924
1247
  ).toHaveFocus();
1248
+ expect(
1249
+ screen.getByRole( 'tabpanel', {
1250
+ name: 'Gamma',
1251
+ } )
1252
+ ).toBeVisible();
925
1253
 
926
- // Alpha retains the selection, even if it's not focused.
927
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
928
- } );
929
-
930
- it( 'should select first enabled tab when the initial tab is disabled', async () => {
931
- const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) =>
932
- tabObj.tabId === 'alpha'
933
- ? {
934
- ...tabObj,
935
- tab: {
936
- ...tabObj.tab,
937
- disabled: true,
938
- },
939
- }
940
- : tabObj
941
- );
942
-
943
- const { rerender } = await render(
944
- <UncontrolledTabs tabs={ TABS_WITH_ALPHA_DISABLED } />
945
- );
1254
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
1255
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
946
1256
 
947
- // As alpha (first tab) is disabled,
948
- // the first enabled tab should be beta.
949
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1257
+ // Press the right arrow key to loop around and select the alpha tab
1258
+ await press.ArrowRight();
950
1259
 
951
- // Re-enable all tabs
952
- await rerender( <UncontrolledTabs tabs={ TABS } /> );
1260
+ expect(
1261
+ screen.getByRole( 'tab', {
1262
+ selected: true,
1263
+ name: 'Alpha',
1264
+ } )
1265
+ ).toHaveFocus();
1266
+ expect(
1267
+ screen.getByRole( 'tabpanel', {
1268
+ name: 'Alpha',
1269
+ } )
1270
+ ).toBeVisible();
953
1271
 
954
- // Even if the initial tab becomes enabled again, the selected
955
- // tab doesn't change.
956
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1272
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
1273
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
957
1274
  } );
958
1275
 
959
- it( 'should select the tab associated to `defaultTabId` even if the tab is disabled', async () => {
960
- const TABS_ONLY_GAMMA_ENABLED = TABS.map( ( tabObj ) =>
961
- tabObj.tabId !== 'gamma'
962
- ? {
963
- ...tabObj,
964
- tab: {
965
- ...tabObj.tab,
966
- disabled: true,
967
- },
968
- }
969
- : tabObj
970
- );
971
- const { rerender } = await render(
972
- <UncontrolledTabs
973
- tabs={ TABS_ONLY_GAMMA_ENABLED }
974
- defaultTabId="beta"
975
- />
976
- );
1276
+ // TODO: mock writing direction to RTL
1277
+ it( 'should swap the left and right arrow keys when selecting tabs if the writing direction is set to RTL', async () => {
1278
+ // For this test only, mock the writing direction to RTL.
1279
+ mockedIsRTL.mockImplementation( () => true );
977
1280
 
978
- // As alpha (first tab), and beta (the initial tab), are both
979
- // disabled the first enabled tab should be gamma.
980
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1281
+ const mockOnSelect = jest.fn();
981
1282
 
982
- // Re-enable all tabs
983
- await rerender(
984
- <UncontrolledTabs tabs={ TABS } defaultTabId="beta" />
1283
+ await render(
1284
+ <Component tabs={ TABS } onSelect={ mockOnSelect } />
985
1285
  );
986
1286
 
987
- // Even if the initial tab becomes enabled again, the selected tab doesn't
988
- // change.
989
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
990
- } );
1287
+ // Alpha is automatically selected as the selected tab.
1288
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
991
1289
 
992
- it( 'should keep the currently tab as selected even when it becomes disabled', async () => {
993
- const mockOnSelect = jest.fn();
994
- const { rerender } = await render(
995
- <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
996
- );
997
-
998
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
999
1290
  expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
1000
1291
  expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
1001
1292
 
1002
- const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) =>
1003
- tabObj.tabId === 'alpha'
1004
- ? {
1005
- ...tabObj,
1006
- tab: {
1007
- ...tabObj.tab,
1008
- disabled: true,
1009
- },
1010
- }
1011
- : tabObj
1012
- );
1013
-
1014
- // Disable alpha
1015
- await rerender(
1016
- <UncontrolledTabs
1017
- tabs={ TABS_WITH_ALPHA_DISABLED }
1018
- onSelect={ mockOnSelect }
1019
- />
1020
- );
1293
+ // Focus the tablist (and the selected tab, alpha)
1294
+ // Tab should initially focus the first tab in the tablist, which
1295
+ // is Alpha.
1296
+ await press.Tab();
1297
+ expect(
1298
+ await screen.findByRole( 'tab', {
1299
+ selected: true,
1300
+ name: 'Alpha',
1301
+ } )
1302
+ ).toHaveFocus();
1021
1303
 
1022
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
1023
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
1304
+ // Press the left arrow key to select the beta tab
1305
+ await press.ArrowLeft();
1024
1306
 
1025
- // Re-enable all tabs
1026
- await rerender(
1027
- <UncontrolledTabs tabs={ TABS } onSelect={ mockOnSelect } />
1028
- );
1307
+ expect(
1308
+ screen.getByRole( 'tab', {
1309
+ selected: true,
1310
+ name: 'Beta',
1311
+ } )
1312
+ ).toHaveFocus();
1313
+ expect(
1314
+ screen.getByRole( 'tabpanel', {
1315
+ name: 'Beta',
1316
+ } )
1317
+ ).toBeVisible();
1029
1318
 
1030
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
1031
- expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
1032
- } );
1319
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
1320
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
1033
1321
 
1034
- it( 'should select the tab associated to `defaultTabId` even when disabled', async () => {
1035
- const mockOnSelect = jest.fn();
1322
+ // Press the left arrow key to select the gamma tab
1323
+ await press.ArrowLeft();
1036
1324
 
1037
- const { rerender } = await render(
1038
- <UncontrolledTabs
1039
- tabs={ TABS }
1040
- onSelect={ mockOnSelect }
1041
- defaultTabId="gamma"
1042
- />
1043
- );
1325
+ expect(
1326
+ screen.getByRole( 'tab', {
1327
+ selected: true,
1328
+ name: 'Gamma',
1329
+ } )
1330
+ ).toHaveFocus();
1331
+ expect(
1332
+ screen.getByRole( 'tabpanel', {
1333
+ name: 'Gamma',
1334
+ } )
1335
+ ).toBeVisible();
1044
1336
 
1045
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
1046
-
1047
- const TABS_WITH_GAMMA_DISABLED = TABS.map( ( tabObj ) =>
1048
- tabObj.tabId === 'gamma'
1049
- ? {
1050
- ...tabObj,
1051
- tab: {
1052
- ...tabObj.tab,
1053
- disabled: true,
1054
- },
1055
- }
1056
- : tabObj
1057
- );
1337
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 3 );
1338
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
1058
1339
 
1059
- // Disable gamma
1060
- await rerender(
1061
- <UncontrolledTabs
1062
- tabs={ TABS_WITH_GAMMA_DISABLED }
1063
- onSelect={ mockOnSelect }
1064
- defaultTabId="gamma"
1065
- />
1066
- );
1340
+ // Press the right arrow key to select the beta tab
1341
+ await press.ArrowRight();
1067
1342
 
1068
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
1343
+ expect(
1344
+ screen.getByRole( 'tab', {
1345
+ selected: true,
1346
+ name: 'Beta',
1347
+ } )
1348
+ ).toHaveFocus();
1349
+ expect(
1350
+ screen.getByRole( 'tabpanel', {
1351
+ name: 'Beta',
1352
+ } )
1353
+ ).toBeVisible();
1069
1354
 
1070
- // Re-enable all tabs
1071
- await rerender(
1072
- <UncontrolledTabs
1073
- tabs={ TABS }
1074
- onSelect={ mockOnSelect }
1075
- defaultTabId="gamma"
1076
- />
1077
- );
1355
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 4 );
1356
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' );
1078
1357
 
1079
- // Confirm that alpha is still selected, and that onSelect has
1080
- // not been called again.
1081
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
1082
- expect( mockOnSelect ).not.toHaveBeenCalled();
1358
+ // Restore the original implementation of the isRTL function.
1359
+ mockedIsRTL.mockRestore();
1083
1360
  } );
1084
- } );
1085
- } );
1086
-
1087
- describe( 'Controlled mode', () => {
1088
- it( 'should render the tab specified by the `selectedTabId` prop', async () => {
1089
- await render(
1090
- <ControlledTabs tabs={ TABS } selectedTabId="beta" />
1091
- );
1092
1361
 
1093
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1094
- expect(
1095
- await screen.findByRole( 'tabpanel', { name: 'Beta' } )
1096
- ).toBeInTheDocument();
1097
- } );
1098
- it( 'should render the specified `selectedTabId`, and ignore the `defaultTabId` prop', async () => {
1099
- await render(
1100
- <ControlledTabs
1101
- tabs={ TABS }
1102
- selectedTabId="gamma"
1103
- defaultTabId="beta"
1104
- />
1105
- );
1106
-
1107
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
1108
- } );
1109
- it( 'should not have a selected tab if `selectedTabId` does not match any known tab', async () => {
1110
- await render(
1111
- <ControlledTabs
1112
- tabs={ TABS_WITH_DELTA }
1113
- selectedTabId="does-not-exist"
1114
- />
1115
- );
1116
-
1117
- expect(
1118
- screen.queryByRole( 'tab', { selected: true } )
1119
- ).not.toBeInTheDocument();
1362
+ it( 'should focus tabs in the tablist even if disabled', async () => {
1363
+ const mockOnSelect = jest.fn();
1120
1364
 
1121
- // No tabpanel should be rendered either
1122
- expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument();
1123
- } );
1124
- it( 'should not have a selected tab if the active tab is removed, but should select a tab that gets added if it matches the selectedTabId', async () => {
1125
- const { rerender } = await render(
1126
- <ControlledTabs tabs={ TABS } selectedTabId="beta" />
1127
- );
1365
+ await render(
1366
+ <Component
1367
+ tabs={ TABS_WITH_BETA_DISABLED }
1368
+ onSelect={ mockOnSelect }
1369
+ />
1370
+ );
1128
1371
 
1129
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1372
+ // Alpha is automatically selected as the selected tab.
1373
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
1130
1374
 
1131
- // Remove beta
1132
- await rerender(
1133
- <ControlledTabs
1134
- tabs={ TABS.filter( ( tab ) => tab.tabId !== 'beta' ) }
1135
- selectedTabId="beta"
1136
- />
1137
- );
1138
-
1139
- expect( screen.getAllByRole( 'tab' ) ).toHaveLength( 2 );
1375
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
1376
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
1140
1377
 
1141
- // No tab should be selected i.e. it doesn't fall back to first tab.
1142
- // `waitFor` is needed here to prevent testing library from
1143
- // throwing a 'not wrapped in `act()`' error.
1144
- await waitFor( () =>
1378
+ // Focus the tablist (and the selected tab, alpha)
1379
+ // Tab should initially focus the first tab in the tablist, which
1380
+ // is Alpha.
1381
+ await press.Tab();
1145
1382
  expect(
1146
- screen.queryByRole( 'tab', { selected: true } )
1147
- ).not.toBeInTheDocument()
1148
- );
1149
-
1150
- // No tabpanel should be rendered either
1151
- expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument();
1152
-
1153
- // Restore beta
1154
- await rerender(
1155
- <ControlledTabs tabs={ TABS } selectedTabId="beta" />
1156
- );
1157
-
1158
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1159
- } );
1160
-
1161
- describe( 'Disabled tab', () => {
1162
- it( 'should `selectedTabId` refers to a disabled tab', async () => {
1163
- const TABS_WITH_DELTA_WITH_BETA_DISABLED = TABS_WITH_DELTA.map(
1164
- ( tabObj ) =>
1165
- tabObj.tabId === 'beta'
1166
- ? {
1167
- ...tabObj,
1168
- tab: {
1169
- ...tabObj.tab,
1170
- disabled: true,
1171
- },
1172
- }
1173
- : tabObj
1174
- );
1175
-
1176
- await render(
1177
- <ControlledTabs
1178
- tabs={ TABS_WITH_DELTA_WITH_BETA_DISABLED }
1179
- selectedTabId="beta"
1180
- />
1181
- );
1383
+ await screen.findByRole( 'tab', {
1384
+ selected: true,
1385
+ name: 'Alpha',
1386
+ } )
1387
+ ).toHaveFocus();
1182
1388
 
1183
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1184
- } );
1185
- it( 'should keep the currently selected tab as selected even when it becomes disabled', async () => {
1186
- const { rerender } = await render(
1187
- <ControlledTabs tabs={ TABS } selectedTabId="beta" />
1188
- );
1389
+ // Pressing the right arrow key moves focus to the beta tab, but alpha
1390
+ // remains the selected tab because beta is disabled.
1391
+ await press.ArrowRight();
1189
1392
 
1190
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1191
-
1192
- const TABS_WITH_BETA_DISABLED = TABS.map( ( tabObj ) =>
1193
- tabObj.tabId === 'beta'
1194
- ? {
1195
- ...tabObj,
1196
- tab: {
1197
- ...tabObj.tab,
1198
- disabled: true,
1199
- },
1200
- }
1201
- : tabObj
1202
- );
1393
+ expect(
1394
+ screen.getByRole( 'tab', {
1395
+ selected: false,
1396
+ name: 'Beta',
1397
+ } )
1398
+ ).toHaveFocus();
1399
+ expect(
1400
+ screen.getByRole( 'tab', {
1401
+ selected: true,
1402
+ name: 'Alpha',
1403
+ } )
1404
+ ).toBeVisible();
1405
+ expect(
1406
+ screen.getByRole( 'tabpanel', {
1407
+ name: 'Alpha',
1408
+ } )
1409
+ ).toBeVisible();
1203
1410
 
1204
- await rerender(
1205
- <ControlledTabs
1206
- tabs={ TABS_WITH_BETA_DISABLED }
1207
- selectedTabId="beta"
1208
- />
1209
- );
1411
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
1210
1412
 
1211
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1413
+ // Press the right arrow key to select the gamma tab
1414
+ await press.ArrowRight();
1212
1415
 
1213
- // re-enable all tabs
1214
- await rerender(
1215
- <ControlledTabs tabs={ TABS } selectedTabId="beta" />
1216
- );
1416
+ expect(
1417
+ screen.getByRole( 'tab', {
1418
+ selected: true,
1419
+ name: 'Gamma',
1420
+ } )
1421
+ ).toHaveFocus();
1422
+ expect(
1423
+ screen.getByRole( 'tabpanel', {
1424
+ name: 'Gamma',
1425
+ } )
1426
+ ).toBeVisible();
1217
1427
 
1218
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1428
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
1429
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
1219
1430
  } );
1220
1431
  } );
1221
- describe( 'When `selectedId` is changed by the controlling component', () => {
1432
+
1433
+ describe( 'When `selectedId` is changed by the controlling component [Controlled]', () => {
1222
1434
  describe.each( [ true, false ] )(
1223
1435
  'and `selectOnMove` is %s',
1224
1436
  ( selectOnMove ) => {
@@ -1231,17 +1443,18 @@ describe( 'Tabs', () => {
1231
1443
  />
1232
1444
  );
1233
1445
 
1234
- expect( await getSelectedTab() ).toHaveTextContent(
1446
+ // Beta is the selected tab.
1447
+ await waitForComponentToBeInitializedWithSelectedTab(
1235
1448
  'Beta'
1236
1449
  );
1237
1450
 
1238
1451
  // Tab key should focus the currently selected tab, which is Beta.
1239
1452
  await press.Tab();
1240
- expect( await getSelectedTab() ).toHaveTextContent(
1241
- 'Beta'
1242
- );
1243
1453
  expect(
1244
- screen.getByRole( 'tab', { name: 'Beta' } )
1454
+ screen.getByRole( 'tab', {
1455
+ selected: true,
1456
+ name: 'Beta',
1457
+ } )
1245
1458
  ).toHaveFocus();
1246
1459
 
1247
1460
  await rerender(
@@ -1253,17 +1466,28 @@ describe( 'Tabs', () => {
1253
1466
  );
1254
1467
 
1255
1468
  // When the selected tab is changed, focus should not be changed.
1256
- expect( await getSelectedTab() ).toHaveTextContent(
1257
- 'Gamma'
1258
- );
1259
1469
  expect(
1260
- screen.getByRole( 'tab', { name: 'Beta' } )
1470
+ screen.getByRole( 'tab', {
1471
+ selected: true,
1472
+ name: 'Gamma',
1473
+ } )
1474
+ ).toBeVisible();
1475
+ expect(
1476
+ screen.getByRole( 'tab', {
1477
+ selected: false,
1478
+ name: 'Beta',
1479
+ } )
1261
1480
  ).toHaveFocus();
1262
1481
 
1263
- // Arrow keys should move focus to the next tab, which is Gamma
1264
- await press.ArrowRight();
1482
+ // Arrow left should move focus to the previous tab (alpha).
1483
+ // The alpha tab should be always focused, and should be selected
1484
+ // when the `selectOnMove` prop is set to `true`.
1485
+ await press.ArrowLeft();
1265
1486
  expect(
1266
- screen.getByRole( 'tab', { name: 'Gamma' } )
1487
+ screen.getByRole( 'tab', {
1488
+ selected: selectOnMove,
1489
+ name: 'Alpha',
1490
+ } )
1267
1491
  ).toHaveFocus();
1268
1492
  } );
1269
1493
 
@@ -1279,20 +1503,22 @@ describe( 'Tabs', () => {
1279
1503
  </>
1280
1504
  );
1281
1505
 
1282
- expect( await getSelectedTab() ).toHaveTextContent(
1506
+ // Beta is the selected tab.
1507
+ await waitForComponentToBeInitializedWithSelectedTab(
1283
1508
  'Beta'
1284
1509
  );
1285
1510
 
1286
1511
  // Tab key should focus the currently selected tab, which is Beta.
1287
1512
  await press.Tab();
1288
1513
  await press.Tab();
1289
- expect( await getSelectedTab() ).toHaveTextContent(
1290
- 'Beta'
1291
- );
1292
1514
  expect(
1293
- screen.getByRole( 'tab', { name: 'Beta' } )
1515
+ screen.getByRole( 'tab', {
1516
+ selected: true,
1517
+ name: 'Beta',
1518
+ } )
1294
1519
  ).toHaveFocus();
1295
1520
 
1521
+ // Change the selected tab to gamma via a controlled update.
1296
1522
  await rerender(
1297
1523
  <>
1298
1524
  <button>Focus me</button>
@@ -1305,12 +1531,17 @@ describe( 'Tabs', () => {
1305
1531
  );
1306
1532
 
1307
1533
  // When the selected tab is changed, it should not automatically receive focus.
1308
- expect( await getSelectedTab() ).toHaveTextContent(
1309
- 'Gamma'
1310
- );
1311
-
1312
1534
  expect(
1313
- screen.getByRole( 'tab', { name: 'Beta' } )
1535
+ screen.getByRole( 'tab', {
1536
+ selected: true,
1537
+ name: 'Gamma',
1538
+ } )
1539
+ ).toBeVisible();
1540
+ expect(
1541
+ screen.getByRole( 'tab', {
1542
+ selected: false,
1543
+ name: 'Beta',
1544
+ } )
1314
1545
  ).toHaveFocus();
1315
1546
 
1316
1547
  // Press shift+tab, move focus to the button before Tabs
@@ -1336,125 +1567,439 @@ describe( 'Tabs', () => {
1336
1567
  }
1337
1568
  );
1338
1569
  } );
1570
+ } );
1339
1571
 
1340
- describe( 'When `selectOnMove` is `true`', () => {
1341
- it( 'should automatically select a newly focused tab', async () => {
1342
- await render(
1343
- <ControlledTabs tabs={ TABS } selectedTabId="beta" />
1344
- );
1572
+ describe( 'miscellaneous runtime changes', () => {
1573
+ describe( 'removing a tab', () => {
1574
+ describe( 'with no explicitly set initial tab', () => {
1575
+ it( 'should not select a new tab when the selected tab is removed', async () => {
1576
+ const mockOnSelect = jest.fn();
1345
1577
 
1346
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1578
+ const { rerender } = await render(
1579
+ <UncontrolledTabs
1580
+ tabs={ TABS }
1581
+ onSelect={ mockOnSelect }
1582
+ />
1583
+ );
1347
1584
 
1348
- await press.Tab();
1585
+ // Alpha is automatically selected as the selected tab.
1586
+ await waitForComponentToBeInitializedWithSelectedTab(
1587
+ 'Alpha'
1588
+ );
1349
1589
 
1350
- // Tab key should focus the currently selected tab, which is Beta.
1351
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1352
- expect( await getSelectedTab() ).toHaveFocus();
1590
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
1591
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
1353
1592
 
1354
- // Arrow keys should select and move focus to the next tab.
1355
- await press.ArrowRight();
1356
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
1357
- expect( await getSelectedTab() ).toHaveFocus();
1593
+ // Select gamma
1594
+ await click( screen.getByRole( 'tab', { name: 'Gamma' } ) );
1595
+
1596
+ expect(
1597
+ screen.getByRole( 'tab', {
1598
+ selected: true,
1599
+ name: 'Gamma',
1600
+ } )
1601
+ ).toHaveFocus();
1602
+ expect(
1603
+ screen.getByRole( 'tabpanel', {
1604
+ name: 'Gamma',
1605
+ } )
1606
+ ).toBeVisible();
1607
+
1608
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
1609
+ expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' );
1610
+
1611
+ // Remove gamma
1612
+ await rerender(
1613
+ <UncontrolledTabs
1614
+ tabs={ TABS.slice( 0, 2 ) }
1615
+ onSelect={ mockOnSelect }
1616
+ />
1617
+ );
1618
+
1619
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength( 2 );
1620
+
1621
+ // No tab should be selected i.e. it doesn't fall back to gamma,
1622
+ // even if it matches the `defaultTabId` prop.
1623
+ expect(
1624
+ screen.queryByRole( 'tab', { selected: true } )
1625
+ ).not.toBeInTheDocument();
1626
+ // No tabpanel should be rendered either
1627
+ expect(
1628
+ screen.queryByRole( 'tabpanel' )
1629
+ ).not.toBeInTheDocument();
1630
+
1631
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
1632
+ } );
1358
1633
  } );
1634
+
1635
+ describe.each( [
1636
+ [ 'defaultTabId', 'Uncontrolled', UncontrolledTabs ],
1637
+ [ 'selectedTabId', 'Controlled', ControlledTabs ],
1638
+ ] )(
1639
+ 'when using the `%s` prop [%s]',
1640
+ ( propName, _mode, Component ) => {
1641
+ it( 'should not select a new tab when the selected tab is removed', async () => {
1642
+ const mockOnSelect = jest.fn();
1643
+
1644
+ const initialComponentProps = {
1645
+ tabs: TABS,
1646
+ [ propName ]: 'gamma',
1647
+ onSelect: mockOnSelect,
1648
+ };
1649
+
1650
+ const { rerender } = await render(
1651
+ <Component { ...initialComponentProps } />
1652
+ );
1653
+
1654
+ // Gamma is the selected tab.
1655
+ await waitForComponentToBeInitializedWithSelectedTab(
1656
+ 'Gamma'
1657
+ );
1658
+
1659
+ // Remove gamma
1660
+ await rerender(
1661
+ <Component
1662
+ { ...initialComponentProps }
1663
+ tabs={ TABS.slice( 0, 2 ) }
1664
+ />
1665
+ );
1666
+
1667
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength(
1668
+ 2
1669
+ );
1670
+ // No tab should be selected i.e. it doesn't fall back to first tab.
1671
+ expect(
1672
+ screen.queryByRole( 'tab', { selected: true } )
1673
+ ).not.toBeInTheDocument();
1674
+ // No tabpanel should be rendered either
1675
+ expect(
1676
+ screen.queryByRole( 'tabpanel' )
1677
+ ).not.toBeInTheDocument();
1678
+
1679
+ // Re-add gamma. Gamma becomes selected again.
1680
+ await rerender(
1681
+ <Component { ...initialComponentProps } />
1682
+ );
1683
+
1684
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength(
1685
+ TABS.length
1686
+ );
1687
+
1688
+ expect(
1689
+ screen.getByRole( 'tab', {
1690
+ selected: true,
1691
+ name: 'Gamma',
1692
+ } )
1693
+ ).toBeVisible();
1694
+ expect(
1695
+ screen.getByRole( 'tabpanel', {
1696
+ name: 'Gamma',
1697
+ } )
1698
+ ).toBeVisible();
1699
+
1700
+ expect( mockOnSelect ).not.toHaveBeenCalled();
1701
+ } );
1702
+
1703
+ it( `should not select the tab matching the \`${ propName }\` prop as a fallback when the selected tab is removed`, async () => {
1704
+ const mockOnSelect = jest.fn();
1705
+
1706
+ const initialComponentProps = {
1707
+ tabs: TABS,
1708
+ [ propName ]: 'gamma',
1709
+ onSelect: mockOnSelect,
1710
+ };
1711
+
1712
+ const { rerender } = await render(
1713
+ <Component { ...initialComponentProps } />
1714
+ );
1715
+
1716
+ // Gamma is the selected tab.
1717
+ await waitForComponentToBeInitializedWithSelectedTab(
1718
+ 'Gamma'
1719
+ );
1720
+
1721
+ // Select alpha
1722
+ await click(
1723
+ screen.getByRole( 'tab', { name: 'Alpha' } )
1724
+ );
1725
+
1726
+ expect(
1727
+ screen.getByRole( 'tab', {
1728
+ selected: true,
1729
+ name: 'Alpha',
1730
+ } )
1731
+ ).toHaveFocus();
1732
+ expect(
1733
+ screen.getByRole( 'tabpanel', {
1734
+ name: 'Alpha',
1735
+ } )
1736
+ ).toBeVisible();
1737
+
1738
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
1739
+ expect( mockOnSelect ).toHaveBeenLastCalledWith(
1740
+ 'alpha'
1741
+ );
1742
+
1743
+ // Remove alpha
1744
+ await rerender(
1745
+ <Component
1746
+ { ...initialComponentProps }
1747
+ tabs={ TABS.slice( 1 ) }
1748
+ />
1749
+ );
1750
+
1751
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength(
1752
+ 2
1753
+ );
1754
+
1755
+ // No tab should be selected i.e. it doesn't fall back to gamma,
1756
+ // even if it matches the `defaultTabId` prop.
1757
+ expect(
1758
+ screen.queryByRole( 'tab', { selected: true } )
1759
+ ).not.toBeInTheDocument();
1760
+ // No tabpanel should be rendered either
1761
+ expect(
1762
+ screen.queryByRole( 'tabpanel' )
1763
+ ).not.toBeInTheDocument();
1764
+
1765
+ // Re-add alpha. Alpha becomes selected again.
1766
+ await rerender(
1767
+ <Component { ...initialComponentProps } />
1768
+ );
1769
+
1770
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength(
1771
+ TABS.length
1772
+ );
1773
+
1774
+ expect(
1775
+ screen.getByRole( 'tab', {
1776
+ selected: true,
1777
+ name: 'Alpha',
1778
+ } )
1779
+ ).toBeVisible();
1780
+ expect(
1781
+ screen.getByRole( 'tabpanel', {
1782
+ name: 'Alpha',
1783
+ } )
1784
+ ).toBeVisible();
1785
+
1786
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
1787
+ } );
1788
+ }
1789
+ );
1359
1790
  } );
1360
- describe( 'When `selectOnMove` is `false`', () => {
1361
- it( 'should apply focus without automatically changing the selected tab', async () => {
1362
- await render(
1363
- <ControlledTabs
1364
- tabs={ TABS }
1365
- selectedTabId="beta"
1366
- selectOnMove={ false }
1367
- />
1368
- );
1369
1791
 
1370
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1792
+ describe( 'adding a tab', () => {
1793
+ describe.each( [
1794
+ [ 'defaultTabId', 'Uncontrolled', UncontrolledTabs ],
1795
+ [ 'selectedTabId', 'Controlled', ControlledTabs ],
1796
+ ] )(
1797
+ 'when using the `%s` prop [%s]',
1798
+ ( propName, _mode, Component ) => {
1799
+ it( `should select a newly added tab if it matches the \`${ propName }\` prop`, async () => {
1800
+ const mockOnSelect = jest.fn();
1801
+
1802
+ const initialComponentProps = {
1803
+ tabs: TABS,
1804
+ [ propName ]: 'delta',
1805
+ onSelect: mockOnSelect,
1806
+ };
1371
1807
 
1372
- // Tab key should focus the currently selected tab, which is Beta.
1373
- await press.Tab();
1374
- await waitFor( async () =>
1375
- expect(
1376
- await screen.findByRole( 'tab', { name: 'Beta' } )
1377
- ).toHaveFocus()
1378
- );
1808
+ const { rerender } = await render(
1809
+ <Component { ...initialComponentProps } />
1810
+ );
1379
1811
 
1380
- // Arrow key should move focus but not automatically change the selected tab.
1381
- await press.ArrowRight();
1382
- expect(
1383
- screen.getByRole( 'tab', { name: 'Gamma' } )
1384
- ).toHaveFocus();
1385
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1812
+ // No initially selected tabs or tabpanels, since the `defaultTabId`
1813
+ // prop is not matching any known tabs.
1814
+ await waitForComponentToBeInitializedWithSelectedTab(
1815
+ undefined
1816
+ );
1386
1817
 
1387
- // Pressing the spacebar should select the focused tab.
1388
- await press.Space();
1389
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
1818
+ expect( mockOnSelect ).not.toHaveBeenCalled();
1390
1819
 
1391
- // Arrow key should move focus but not automatically change the selected tab.
1392
- await press.ArrowRight();
1393
- expect(
1394
- screen.getByRole( 'tab', { name: 'Alpha' } )
1395
- ).toHaveFocus();
1396
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
1820
+ // Re-render with beta disabled.
1821
+ await rerender(
1822
+ <Component
1823
+ { ...initialComponentProps }
1824
+ tabs={ TABS_WITH_DELTA }
1825
+ />
1826
+ );
1397
1827
 
1398
- // Pressing the enter/return should select the focused tab.
1399
- await press.Enter();
1400
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
1401
- } );
1828
+ // Delta becomes selected
1829
+ expect(
1830
+ screen.getByRole( 'tab', {
1831
+ selected: true,
1832
+ name: 'Delta',
1833
+ } )
1834
+ ).toBeVisible();
1835
+ expect(
1836
+ screen.getByRole( 'tabpanel', {
1837
+ name: 'Delta',
1838
+ } )
1839
+ ).toBeVisible();
1840
+
1841
+ expect( mockOnSelect ).not.toHaveBeenCalled();
1842
+ } );
1843
+ }
1844
+ );
1402
1845
  } );
1403
- } );
1404
- it( 'should associate each `Tab` with the correct `TabPanel`, even if they are not rendered in the same order', async () => {
1405
- const TABS_WITH_DELTA_REVERSED = [ ...TABS_WITH_DELTA ].reverse();
1406
-
1407
- await render(
1408
- <Tabs>
1409
- <Tabs.TabList>
1410
- { TABS_WITH_DELTA.map( ( tabObj ) => (
1411
- <Tabs.Tab
1412
- key={ tabObj.tabId }
1413
- tabId={ tabObj.tabId }
1414
- className={ tabObj.tab.className }
1415
- disabled={ tabObj.tab.disabled }
1416
- >
1417
- { tabObj.title }
1418
- </Tabs.Tab>
1419
- ) ) }
1420
- </Tabs.TabList>
1421
- { TABS_WITH_DELTA_REVERSED.map( ( tabObj ) => (
1422
- <Tabs.TabPanel
1423
- key={ tabObj.tabId }
1424
- tabId={ tabObj.tabId }
1425
- focusable={ tabObj.tabpanel?.focusable }
1426
- >
1427
- { tabObj.content }
1428
- </Tabs.TabPanel>
1429
- ) ) }
1430
- </Tabs>
1431
- );
1846
+ describe( 'a tab becomes disabled', () => {
1847
+ describe.each( [
1848
+ [ 'defaultTabId', 'Uncontrolled', UncontrolledTabs ],
1849
+ [ 'selectedTabId', 'Controlled', ControlledTabs ],
1850
+ ] )(
1851
+ 'when using the `%s` prop [%s]',
1852
+ ( propName, _mode, Component ) => {
1853
+ it( `should keep the initial tab matching the \`${ propName }\` prop as selected even if it becomes disabled`, async () => {
1854
+ const mockOnSelect = jest.fn();
1855
+
1856
+ const initialComponentProps = {
1857
+ tabs: TABS,
1858
+ [ propName ]: 'beta',
1859
+ onSelect: mockOnSelect,
1860
+ };
1432
1861
 
1433
- // Alpha is the initially selected tab,and should render the correct tabpanel
1434
- expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' );
1435
- expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent(
1436
- 'Selected tab: Alpha'
1437
- );
1862
+ const { rerender } = await render(
1863
+ <Component { ...initialComponentProps } />
1864
+ );
1438
1865
 
1439
- // Select Beta, make sure the correct tabpanel is rendered
1440
- await click( screen.getByRole( 'tab', { name: 'Beta' } ) );
1441
- expect( await getSelectedTab() ).toHaveTextContent( 'Beta' );
1442
- expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent(
1443
- 'Selected tab: Beta'
1444
- );
1866
+ // Beta is the selected tab.
1867
+ await waitForComponentToBeInitializedWithSelectedTab(
1868
+ 'Beta'
1869
+ );
1445
1870
 
1446
- // Select Gamma, make sure the correct tabpanel is rendered
1447
- await click( screen.getByRole( 'tab', { name: 'Gamma' } ) );
1448
- expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' );
1449
- expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent(
1450
- 'Selected tab: Gamma'
1451
- );
1871
+ expect( mockOnSelect ).not.toHaveBeenCalled();
1452
1872
 
1453
- // Select Delta, make sure the correct tabpanel is rendered
1454
- await click( screen.getByRole( 'tab', { name: 'Delta' } ) );
1455
- expect( await getSelectedTab() ).toHaveTextContent( 'Delta' );
1456
- expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent(
1457
- 'Selected tab: Delta'
1458
- );
1873
+ // Re-render with beta disabled.
1874
+ await rerender(
1875
+ <Component
1876
+ { ...initialComponentProps }
1877
+ tabs={ TABS_WITH_BETA_DISABLED }
1878
+ />
1879
+ );
1880
+
1881
+ // Beta continues to be selected and focused, even if it is disabled.
1882
+ expect(
1883
+ screen.getByRole( 'tab', {
1884
+ selected: true,
1885
+ name: 'Beta',
1886
+ } )
1887
+ ).toBeVisible();
1888
+ expect(
1889
+ screen.getByRole( 'tabpanel', {
1890
+ name: 'Beta',
1891
+ } )
1892
+ ).toBeVisible();
1893
+
1894
+ // Re-enable beta.
1895
+ await rerender(
1896
+ <Component { ...initialComponentProps } />
1897
+ );
1898
+
1899
+ // Beta continues to be selected and focused.
1900
+ expect(
1901
+ screen.getByRole( 'tab', {
1902
+ selected: true,
1903
+ name: 'Beta',
1904
+ } )
1905
+ ).toBeVisible();
1906
+ expect(
1907
+ screen.getByRole( 'tabpanel', {
1908
+ name: 'Beta',
1909
+ } )
1910
+ ).toBeVisible();
1911
+
1912
+ expect( mockOnSelect ).not.toHaveBeenCalled();
1913
+ } );
1914
+
1915
+ it( 'should keep the current tab selected by the user as selected even if it becomes disabled', async () => {
1916
+ const mockOnSelect = jest.fn();
1917
+
1918
+ const { rerender } = await render(
1919
+ <Component
1920
+ tabs={ TABS }
1921
+ onSelect={ mockOnSelect }
1922
+ />
1923
+ );
1924
+
1925
+ // Alpha is automatically selected as the selected tab.
1926
+ await waitForComponentToBeInitializedWithSelectedTab(
1927
+ 'Alpha'
1928
+ );
1929
+
1930
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 1 );
1931
+ expect( mockOnSelect ).toHaveBeenLastCalledWith(
1932
+ 'alpha'
1933
+ );
1934
+
1935
+ // Click on beta tab, beta becomes selected.
1936
+ await click(
1937
+ screen.getByRole( 'tab', { name: 'Beta' } )
1938
+ );
1939
+
1940
+ expect(
1941
+ screen.getByRole( 'tab', {
1942
+ selected: true,
1943
+ name: 'Beta',
1944
+ } )
1945
+ ).toBeVisible();
1946
+ expect(
1947
+ screen.getByRole( 'tabpanel', {
1948
+ name: 'Beta',
1949
+ } )
1950
+ ).toBeVisible();
1951
+
1952
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
1953
+ expect( mockOnSelect ).toHaveBeenLastCalledWith(
1954
+ 'beta'
1955
+ );
1956
+
1957
+ // Re-render with beta disabled.
1958
+ await rerender(
1959
+ <Component
1960
+ tabs={ TABS_WITH_BETA_DISABLED }
1961
+ onSelect={ mockOnSelect }
1962
+ />
1963
+ );
1964
+
1965
+ // Beta continues to be selected, even if it is disabled.
1966
+ expect(
1967
+ screen.getByRole( 'tab', {
1968
+ selected: true,
1969
+ name: 'Beta',
1970
+ } )
1971
+ ).toHaveFocus();
1972
+ expect(
1973
+ screen.getByRole( 'tabpanel', {
1974
+ name: 'Beta',
1975
+ } )
1976
+ ).toBeVisible();
1977
+
1978
+ // Re-enable beta.
1979
+ await rerender(
1980
+ <Component
1981
+ tabs={ TABS }
1982
+ onSelect={ mockOnSelect }
1983
+ />
1984
+ );
1985
+
1986
+ // Beta continues to be selected and focused.
1987
+ expect(
1988
+ screen.getByRole( 'tab', {
1989
+ selected: true,
1990
+ name: 'Beta',
1991
+ } )
1992
+ ).toBeVisible();
1993
+ expect(
1994
+ screen.getByRole( 'tabpanel', {
1995
+ name: 'Beta',
1996
+ } )
1997
+ ).toBeVisible();
1998
+
1999
+ expect( mockOnSelect ).toHaveBeenCalledTimes( 2 );
2000
+ } );
2001
+ }
2002
+ );
2003
+ } );
1459
2004
  } );
1460
2005
  } );