@wordpress/ui 0.6.1-next.v.0 → 0.7.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 (297) hide show
  1. package/AGENTS.md +9 -0
  2. package/CHANGELOG.md +32 -1
  3. package/CLAUDE.md +1 -0
  4. package/README.md +13 -12
  5. package/build/badge/badge.cjs +37 -62
  6. package/build/badge/badge.cjs.map +4 -4
  7. package/build/button/button.cjs +3 -3
  8. package/build/button/button.cjs.map +2 -2
  9. package/build/dialog/action.cjs +46 -0
  10. package/build/dialog/action.cjs.map +7 -0
  11. package/build/dialog/close-icon.cjs +57 -0
  12. package/build/dialog/close-icon.cjs.map +7 -0
  13. package/build/dialog/context.cjs +76 -0
  14. package/build/dialog/context.cjs.map +7 -0
  15. package/build/dialog/footer.cjs +64 -0
  16. package/build/dialog/footer.cjs.map +7 -0
  17. package/build/dialog/header.cjs +64 -0
  18. package/build/dialog/header.cjs.map +7 -0
  19. package/build/dialog/index.cjs +52 -0
  20. package/build/dialog/index.cjs.map +7 -0
  21. package/build/dialog/popup.cjs +77 -0
  22. package/build/dialog/popup.cjs.map +7 -0
  23. package/build/dialog/root.cjs +35 -0
  24. package/build/dialog/root.cjs.map +7 -0
  25. package/build/dialog/title.cjs +76 -0
  26. package/build/dialog/title.cjs.map +7 -0
  27. package/build/dialog/trigger.cjs +38 -0
  28. package/build/dialog/trigger.cjs.map +7 -0
  29. package/build/dialog/types.cjs +19 -0
  30. package/build/dialog/types.cjs.map +7 -0
  31. package/build/form/primitives/field/root.cjs +1 -1
  32. package/build/form/primitives/field/root.cjs.map +1 -1
  33. package/build/form/primitives/fieldset/root.cjs +3 -3
  34. package/build/form/primitives/fieldset/root.cjs.map +2 -2
  35. package/build/form/primitives/index.cjs +5 -2
  36. package/build/form/primitives/index.cjs.map +2 -2
  37. package/build/form/primitives/input-layout/input-layout.cjs +3 -3
  38. package/build/form/primitives/input-layout/input-layout.cjs.map +2 -2
  39. package/build/form/primitives/input-layout/slot.cjs +3 -3
  40. package/build/form/primitives/input-layout/slot.cjs.map +2 -2
  41. package/build/form/primitives/select/item.cjs +3 -3
  42. package/build/form/primitives/select/item.cjs.map +2 -2
  43. package/build/form/primitives/select/popup.cjs +3 -3
  44. package/build/form/primitives/select/popup.cjs.map +2 -2
  45. package/build/form/primitives/select/trigger.cjs +3 -3
  46. package/build/form/primitives/select/trigger.cjs.map +2 -2
  47. package/build/{box → form/primitives/textarea}/index.cjs +7 -7
  48. package/build/form/primitives/textarea/index.cjs.map +7 -0
  49. package/build/form/primitives/textarea/textarea.cjs +90 -0
  50. package/build/form/primitives/textarea/textarea.cjs.map +7 -0
  51. package/build/form/primitives/textarea/types.cjs +19 -0
  52. package/build/form/primitives/textarea/types.cjs.map +7 -0
  53. package/build/icon-button/icon-button.cjs +104 -0
  54. package/build/icon-button/icon-button.cjs.map +7 -0
  55. package/build/icon-button/index.cjs +31 -0
  56. package/build/icon-button/index.cjs.map +7 -0
  57. package/build/icon-button/types.cjs +19 -0
  58. package/build/icon-button/types.cjs.map +7 -0
  59. package/build/index.cjs +8 -2
  60. package/build/index.cjs.map +2 -2
  61. package/build/tabs/index.cjs +40 -0
  62. package/build/tabs/index.cjs.map +7 -0
  63. package/build/tabs/list.cjs +145 -0
  64. package/build/tabs/list.cjs.map +7 -0
  65. package/build/tabs/panel.cjs +67 -0
  66. package/build/tabs/panel.cjs.map +7 -0
  67. package/build/tabs/root.cjs +38 -0
  68. package/build/tabs/root.cjs.map +7 -0
  69. package/build/tabs/tab.cjs +71 -0
  70. package/build/tabs/tab.cjs.map +7 -0
  71. package/build/{box → tabs}/types.cjs +1 -1
  72. package/build/tabs/types.cjs.map +7 -0
  73. package/build/tooltip/popup.cjs +3 -3
  74. package/build/tooltip/popup.cjs.map +2 -2
  75. package/build-module/badge/badge.mjs +27 -62
  76. package/build-module/badge/badge.mjs.map +3 -3
  77. package/build-module/button/button.mjs +3 -3
  78. package/build-module/button/button.mjs.map +2 -2
  79. package/build-module/dialog/action.mjs +21 -0
  80. package/build-module/dialog/action.mjs.map +7 -0
  81. package/build-module/dialog/close-icon.mjs +32 -0
  82. package/build-module/dialog/close-icon.mjs.map +7 -0
  83. package/build-module/dialog/context.mjs +57 -0
  84. package/build-module/dialog/context.mjs.map +7 -0
  85. package/build-module/dialog/footer.mjs +29 -0
  86. package/build-module/dialog/footer.mjs.map +7 -0
  87. package/build-module/dialog/header.mjs +29 -0
  88. package/build-module/dialog/header.mjs.map +7 -0
  89. package/build-module/dialog/index.mjs +20 -0
  90. package/build-module/dialog/index.mjs.map +7 -0
  91. package/build-module/dialog/popup.mjs +44 -0
  92. package/build-module/dialog/popup.mjs.map +7 -0
  93. package/build-module/dialog/root.mjs +10 -0
  94. package/build-module/dialog/root.mjs.map +7 -0
  95. package/build-module/dialog/title.mjs +41 -0
  96. package/build-module/dialog/title.mjs.map +7 -0
  97. package/build-module/dialog/trigger.mjs +13 -0
  98. package/build-module/dialog/trigger.mjs.map +7 -0
  99. package/build-module/form/primitives/field/root.mjs +1 -1
  100. package/build-module/form/primitives/field/root.mjs.map +1 -1
  101. package/build-module/form/primitives/fieldset/root.mjs +3 -3
  102. package/build-module/form/primitives/fieldset/root.mjs.map +2 -2
  103. package/build-module/form/primitives/index.mjs +3 -1
  104. package/build-module/form/primitives/index.mjs.map +2 -2
  105. package/build-module/form/primitives/input-layout/input-layout.mjs +3 -3
  106. package/build-module/form/primitives/input-layout/input-layout.mjs.map +2 -2
  107. package/build-module/form/primitives/input-layout/slot.mjs +3 -3
  108. package/build-module/form/primitives/input-layout/slot.mjs.map +2 -2
  109. package/build-module/form/primitives/select/item.mjs +3 -3
  110. package/build-module/form/primitives/select/item.mjs.map +2 -2
  111. package/build-module/form/primitives/select/popup.mjs +3 -3
  112. package/build-module/form/primitives/select/popup.mjs.map +2 -2
  113. package/build-module/form/primitives/select/trigger.mjs +3 -3
  114. package/build-module/form/primitives/select/trigger.mjs.map +2 -2
  115. package/build-module/form/primitives/textarea/index.mjs +6 -0
  116. package/build-module/form/primitives/textarea/index.mjs.map +7 -0
  117. package/build-module/form/primitives/textarea/textarea.mjs +55 -0
  118. package/build-module/form/primitives/textarea/textarea.mjs.map +7 -0
  119. package/build-module/form/primitives/textarea/types.mjs +1 -0
  120. package/build-module/form/primitives/textarea/types.mjs.map +7 -0
  121. package/build-module/icon-button/icon-button.mjs +69 -0
  122. package/build-module/icon-button/icon-button.mjs.map +7 -0
  123. package/build-module/icon-button/index.mjs +6 -0
  124. package/build-module/icon-button/index.mjs.map +7 -0
  125. package/build-module/icon-button/types.mjs +1 -0
  126. package/build-module/icon-button/types.mjs.map +7 -0
  127. package/build-module/index.mjs +5 -1
  128. package/build-module/index.mjs.map +2 -2
  129. package/build-module/tabs/index.mjs +12 -0
  130. package/build-module/tabs/index.mjs.map +7 -0
  131. package/build-module/tabs/list.mjs +110 -0
  132. package/build-module/tabs/list.mjs.map +7 -0
  133. package/build-module/tabs/panel.mjs +32 -0
  134. package/build-module/tabs/panel.mjs.map +7 -0
  135. package/build-module/tabs/root.mjs +13 -0
  136. package/build-module/tabs/root.mjs.map +7 -0
  137. package/build-module/tabs/tab.mjs +36 -0
  138. package/build-module/tabs/tab.mjs.map +7 -0
  139. package/build-module/tabs/types.mjs +1 -0
  140. package/build-module/tabs/types.mjs.map +7 -0
  141. package/build-module/tooltip/popup.mjs +3 -3
  142. package/build-module/tooltip/popup.mjs.map +2 -2
  143. package/build-types/badge/badge.d.ts +1 -2
  144. package/build-types/badge/badge.d.ts.map +1 -1
  145. package/build-types/button/stories/index.story.d.ts +1 -2
  146. package/build-types/button/stories/index.story.d.ts.map +1 -1
  147. package/build-types/dialog/action.d.ts +8 -0
  148. package/build-types/dialog/action.d.ts.map +1 -0
  149. package/build-types/dialog/close-icon.d.ts +8 -0
  150. package/build-types/dialog/close-icon.d.ts.map +1 -0
  151. package/build-types/dialog/context.d.ts +25 -0
  152. package/build-types/dialog/context.d.ts.map +1 -0
  153. package/build-types/dialog/footer.d.ts +8 -0
  154. package/build-types/dialog/footer.d.ts.map +1 -0
  155. package/build-types/dialog/header.d.ts +8 -0
  156. package/build-types/dialog/header.d.ts.map +1 -0
  157. package/build-types/dialog/index.d.ts +10 -0
  158. package/build-types/dialog/index.d.ts.map +1 -0
  159. package/build-types/dialog/popup.d.ts +8 -0
  160. package/build-types/dialog/popup.d.ts.map +1 -0
  161. package/build-types/dialog/root.d.ts +10 -0
  162. package/build-types/dialog/root.d.ts.map +1 -0
  163. package/build-types/dialog/stories/index.story.d.ts +18 -0
  164. package/build-types/dialog/stories/index.story.d.ts.map +1 -0
  165. package/build-types/dialog/test/index.test.d.ts +2 -0
  166. package/build-types/dialog/test/index.test.d.ts.map +1 -0
  167. package/build-types/dialog/title.d.ts +12 -0
  168. package/build-types/dialog/title.d.ts.map +1 -0
  169. package/build-types/dialog/trigger.d.ts +7 -0
  170. package/build-types/dialog/trigger.d.ts.map +1 -0
  171. package/build-types/dialog/types.d.ts +77 -0
  172. package/build-types/dialog/types.d.ts.map +1 -0
  173. package/build-types/form/primitives/field/stories/index.story.d.ts +0 -1
  174. package/build-types/form/primitives/field/stories/index.story.d.ts.map +1 -1
  175. package/build-types/form/primitives/index.d.ts +1 -0
  176. package/build-types/form/primitives/index.d.ts.map +1 -1
  177. package/build-types/form/primitives/input/input.d.ts +1 -1
  178. package/build-types/form/primitives/select/stories/index.story.d.ts +0 -1
  179. package/build-types/form/primitives/select/stories/index.story.d.ts.map +1 -1
  180. package/build-types/form/primitives/textarea/index.d.ts +2 -0
  181. package/build-types/form/primitives/textarea/index.d.ts.map +1 -0
  182. package/build-types/form/primitives/textarea/stories/index.story.d.ts +13 -0
  183. package/build-types/form/primitives/textarea/stories/index.story.d.ts.map +1 -0
  184. package/build-types/form/primitives/textarea/test/index.test.d.ts +2 -0
  185. package/build-types/form/primitives/textarea/test/index.test.d.ts.map +1 -0
  186. package/build-types/form/primitives/textarea/textarea.d.ts +4 -0
  187. package/build-types/form/primitives/textarea/textarea.d.ts.map +1 -0
  188. package/build-types/form/primitives/textarea/types.d.ts +11 -0
  189. package/build-types/form/primitives/textarea/types.d.ts.map +1 -0
  190. package/build-types/icon-button/icon-button.d.ts +13 -0
  191. package/build-types/icon-button/icon-button.d.ts.map +1 -0
  192. package/build-types/icon-button/index.d.ts +2 -0
  193. package/build-types/icon-button/index.d.ts.map +1 -0
  194. package/build-types/icon-button/stories/index.story.d.ts +19 -0
  195. package/build-types/icon-button/stories/index.story.d.ts.map +1 -0
  196. package/build-types/icon-button/test/index.test.d.ts +2 -0
  197. package/build-types/icon-button/test/index.test.d.ts.map +1 -0
  198. package/build-types/icon-button/types.d.ts +36 -0
  199. package/build-types/icon-button/types.d.ts.map +1 -0
  200. package/build-types/index.d.ts +3 -1
  201. package/build-types/index.d.ts.map +1 -1
  202. package/build-types/stack/stories/index.story.d.ts.map +1 -1
  203. package/build-types/tabs/index.d.ts +6 -0
  204. package/build-types/tabs/index.d.ts.map +1 -0
  205. package/build-types/tabs/list.d.ts +16 -0
  206. package/build-types/tabs/list.d.ts.map +1 -0
  207. package/build-types/tabs/panel.d.ts +15 -0
  208. package/build-types/tabs/panel.d.ts.map +1 -0
  209. package/build-types/tabs/root.d.ts +15 -0
  210. package/build-types/tabs/root.d.ts.map +1 -0
  211. package/build-types/tabs/stories/index.story.d.ts +13 -0
  212. package/build-types/tabs/stories/index.story.d.ts.map +1 -0
  213. package/build-types/tabs/tab.d.ts +15 -0
  214. package/build-types/tabs/tab.d.ts.map +1 -0
  215. package/build-types/tabs/test/index.test.d.ts +2 -0
  216. package/build-types/tabs/test/index.test.d.ts.map +1 -0
  217. package/build-types/tabs/types.d.ts +33 -0
  218. package/build-types/tabs/types.d.ts.map +1 -0
  219. package/package.json +12 -10
  220. package/src/badge/badge.tsx +19 -78
  221. package/src/badge/stories/choosing-intent.story.tsx +1 -1
  222. package/src/badge/style.module.css +48 -0
  223. package/src/button/stories/index.story.tsx +3 -16
  224. package/src/button/style.module.css +23 -12
  225. package/src/dialog/action.tsx +22 -0
  226. package/src/dialog/close-icon.tsx +32 -0
  227. package/src/dialog/context.tsx +113 -0
  228. package/src/dialog/footer.tsx +26 -0
  229. package/src/dialog/header.tsx +26 -0
  230. package/src/dialog/index.ts +10 -0
  231. package/src/dialog/popup.tsx +46 -0
  232. package/src/dialog/root.tsx +14 -0
  233. package/src/dialog/stories/index.story.tsx +177 -0
  234. package/src/dialog/style.module.css +114 -0
  235. package/src/dialog/test/index.test.tsx +309 -0
  236. package/src/dialog/title.tsx +39 -0
  237. package/src/dialog/trigger.tsx +14 -0
  238. package/src/dialog/types.ts +93 -0
  239. package/src/form/primitives/field/root.tsx +1 -1
  240. package/src/form/primitives/field/stories/index.story.tsx +0 -1
  241. package/src/form/primitives/fieldset/style.module.css +1 -1
  242. package/src/form/primitives/index.ts +1 -0
  243. package/src/form/primitives/input-layout/style.module.css +5 -8
  244. package/src/form/primitives/select/stories/index.story.tsx +0 -1
  245. package/src/form/primitives/select/test/index.test.tsx +0 -2
  246. package/src/form/primitives/textarea/index.ts +1 -0
  247. package/src/form/primitives/textarea/stories/index.story.tsx +40 -0
  248. package/src/form/primitives/textarea/style.module.css +22 -0
  249. package/src/form/primitives/textarea/test/index.test.tsx +143 -0
  250. package/src/form/primitives/textarea/textarea.tsx +51 -0
  251. package/src/form/primitives/textarea/types.ts +18 -0
  252. package/src/icon-button/icon-button.tsx +65 -0
  253. package/src/icon-button/index.ts +1 -0
  254. package/src/icon-button/stories/index.story.tsx +128 -0
  255. package/src/icon-button/style.module.css +16 -0
  256. package/src/icon-button/test/index.test.tsx +86 -0
  257. package/src/icon-button/types.ts +38 -0
  258. package/src/index.ts +3 -1
  259. package/src/stack/stories/index.story.tsx +4 -5
  260. package/src/tabs/index.ts +6 -0
  261. package/src/tabs/list.tsx +130 -0
  262. package/src/tabs/panel.tsx +23 -0
  263. package/src/tabs/root.tsx +15 -0
  264. package/src/tabs/stories/best-practices.mdx +85 -0
  265. package/src/tabs/stories/index.story.tsx +363 -0
  266. package/src/tabs/style.module.css +269 -0
  267. package/src/tabs/tab.tsx +29 -0
  268. package/src/tabs/test/index.test.tsx +2260 -0
  269. package/src/tabs/types.ts +36 -0
  270. package/src/tooltip/style.module.css +3 -3
  271. package/src/utils/css/item-popup.module.css +2 -2
  272. package/src/utils/css/select-trigger.module.css +1 -1
  273. package/build/box/box.cjs +0 -88
  274. package/build/box/box.cjs.map +0 -7
  275. package/build/box/index.cjs.map +0 -7
  276. package/build/box/types.cjs.map +0 -7
  277. package/build-module/box/box.mjs +0 -63
  278. package/build-module/box/box.mjs.map +0 -7
  279. package/build-module/box/index.mjs +0 -6
  280. package/build-module/box/index.mjs.map +0 -7
  281. package/build-types/box/box.d.ts +0 -7
  282. package/build-types/box/box.d.ts.map +0 -1
  283. package/build-types/box/index.d.ts +0 -2
  284. package/build-types/box/index.d.ts.map +0 -1
  285. package/build-types/box/stories/index.story.d.ts +0 -8
  286. package/build-types/box/stories/index.story.d.ts.map +0 -1
  287. package/build-types/box/test/box.test.d.ts +0 -2
  288. package/build-types/box/test/box.test.d.ts.map +0 -1
  289. package/build-types/box/types.d.ts +0 -46
  290. package/build-types/box/types.d.ts.map +0 -1
  291. package/src/box/box.tsx +0 -118
  292. package/src/box/index.ts +0 -1
  293. package/src/box/stories/index.story.tsx +0 -41
  294. package/src/box/test/box.test.tsx +0 -29
  295. package/src/box/types.ts +0 -61
  296. /package/build-module/{box → dialog}/types.mjs +0 -0
  297. /package/build-module/{box → dialog}/types.mjs.map +0 -0
@@ -0,0 +1,2260 @@
1
+ /* eslint-disable jest/no-conditional-expect */
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { DirectionProvider } from '@base-ui/react/direction-provider';
5
+ import { useEffect, useState, createRef } from '@wordpress/element';
6
+ import { Tabs } from '../..';
7
+ import type { TabRootProps } from '../types';
8
+
9
+ type Tab = {
10
+ value: string;
11
+ title: string;
12
+ content: React.ReactNode;
13
+ tab: {
14
+ className?: string;
15
+ disabled?: boolean;
16
+ };
17
+ tabpanel?: {
18
+ tabIndex?: number;
19
+ };
20
+ };
21
+
22
+ const TABS: Tab[] = [
23
+ {
24
+ value: 'alpha',
25
+ title: 'Alpha',
26
+ content: 'Selected tab: Alpha',
27
+ tab: { className: 'alpha-class' },
28
+ },
29
+ {
30
+ value: 'beta',
31
+ title: 'Beta',
32
+ content: 'Selected tab: Beta',
33
+ tab: { className: 'beta-class' },
34
+ },
35
+ {
36
+ value: 'gamma',
37
+ title: 'Gamma',
38
+ content: 'Selected tab: Gamma',
39
+ tab: { className: 'gamma-class' },
40
+ },
41
+ ];
42
+
43
+ const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) =>
44
+ tabObj.value === 'alpha'
45
+ ? {
46
+ ...tabObj,
47
+ tab: {
48
+ ...tabObj.tab,
49
+ disabled: true,
50
+ },
51
+ }
52
+ : tabObj
53
+ );
54
+
55
+ const TABS_WITH_BETA_DISABLED = TABS.map( ( tabObj ) =>
56
+ tabObj.value === 'beta'
57
+ ? {
58
+ ...tabObj,
59
+ tab: {
60
+ ...tabObj.tab,
61
+ disabled: true,
62
+ },
63
+ }
64
+ : tabObj
65
+ );
66
+
67
+ const TABS_WITH_DELTA: Tab[] = [
68
+ ...TABS,
69
+ {
70
+ value: 'delta',
71
+ title: 'Delta',
72
+ content: 'Selected tab: Delta',
73
+ tab: { className: 'delta-class' },
74
+ },
75
+ ];
76
+
77
+ const UncontrolledTabs = ( {
78
+ tabs,
79
+ selectOnMove,
80
+ ...props
81
+ }: Omit< TabRootProps, 'children' | 'tabs' > & {
82
+ tabs: Tab[];
83
+ selectOnMove?: boolean;
84
+ } ) => {
85
+ return (
86
+ <Tabs.Root { ...props }>
87
+ <Tabs.List activateOnFocus={ selectOnMove }>
88
+ { tabs.map( ( tabObj, index ) => (
89
+ <Tabs.Tab
90
+ key={ `${ tabObj.title }-${ index }` }
91
+ value={ tabObj.value }
92
+ className={ tabObj.tab.className }
93
+ disabled={ tabObj.tab.disabled }
94
+ >
95
+ { tabObj.title }
96
+ </Tabs.Tab>
97
+ ) ) }
98
+ </Tabs.List>
99
+ { tabs.map( ( tabObj, index ) => (
100
+ <Tabs.Panel
101
+ key={ `${ tabObj.title }-${ index }` }
102
+ value={ tabObj.value }
103
+ // Only apply tabIndex if defined, otherwise fallback
104
+ // to default internal implementation
105
+ { ...( tabObj.tabpanel?.tabIndex !== undefined && {
106
+ tabIndex: tabObj.tabpanel.tabIndex,
107
+ } ) }
108
+ >
109
+ { tabObj.content }
110
+ </Tabs.Panel>
111
+ ) ) }
112
+ </Tabs.Root>
113
+ );
114
+ };
115
+
116
+ const ControlledTabs = ( {
117
+ tabs,
118
+ selectOnMove,
119
+ ...props
120
+ }: Omit< TabRootProps, 'children' | 'tabs' > & {
121
+ tabs: Tab[];
122
+ selectOnMove?: boolean;
123
+ } ) => {
124
+ const [ value, setValue ] = useState( props.value ?? null );
125
+
126
+ useEffect( () => {
127
+ setValue( props.value ?? null );
128
+ }, [ props.value ] );
129
+
130
+ return (
131
+ <Tabs.Root
132
+ { ...props }
133
+ value={ value }
134
+ onValueChange={ ( selectedId, event ) => {
135
+ setValue( selectedId );
136
+ props.onValueChange?.( selectedId, event );
137
+ } }
138
+ >
139
+ <Tabs.List activateOnFocus={ selectOnMove }>
140
+ { tabs.map( ( tabObj, index ) => (
141
+ <Tabs.Tab
142
+ key={ `${ tabObj.title }-${ index }` }
143
+ value={ tabObj.value }
144
+ className={ tabObj.tab.className }
145
+ disabled={ tabObj.tab.disabled }
146
+ >
147
+ { tabObj.title }
148
+ </Tabs.Tab>
149
+ ) ) }
150
+ </Tabs.List>
151
+ { tabs.map( ( tabObj, index ) => (
152
+ <Tabs.Panel
153
+ key={ `${ tabObj.title }-${ index }` }
154
+ value={ tabObj.value }
155
+ // Only apply tabIndex if defined, otherwise fallback
156
+ // to default internal implementation
157
+ { ...( tabObj.tabpanel?.tabIndex !== undefined && {
158
+ tabIndex: tabObj.tabpanel.tabIndex,
159
+ } ) }
160
+ >
161
+ { tabObj.content }
162
+ </Tabs.Panel>
163
+ ) ) }
164
+ </Tabs.Root>
165
+ );
166
+ };
167
+
168
+ async function waitForComponentToBeInitializedWithSelectedTab(
169
+ selectedTabName: string | undefined
170
+ ) {
171
+ if ( ! selectedTabName ) {
172
+ // No initially selected tabs or tabpanels.
173
+ await waitFor( () =>
174
+ expect(
175
+ screen.queryByRole( 'tab', { selected: true } )
176
+ ).not.toBeInTheDocument()
177
+ );
178
+ await waitFor( () =>
179
+ expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument()
180
+ );
181
+ } else {
182
+ // Waiting for a tab to be selected is a sign that the component
183
+ // has fully initialized.
184
+ expect(
185
+ await screen.findByRole( 'tab', {
186
+ selected: true,
187
+ name: selectedTabName,
188
+ } )
189
+ ).toBeVisible();
190
+ // The corresponding tabpanel is also shown.
191
+ expect(
192
+ screen.getByRole( 'tabpanel', {
193
+ name: selectedTabName,
194
+ } )
195
+ ).toBeVisible();
196
+ }
197
+ }
198
+
199
+ describe( 'Tabs', () => {
200
+ describe( 'Adherence to spec and basic behavior', () => {
201
+ it( 'should apply the correct roles, semantics and attributes', async () => {
202
+ render(
203
+ <Tabs.Root>
204
+ <Tabs.List>
205
+ <Tabs.Tab value="one">One</Tabs.Tab>
206
+ <Tabs.Tab value="two">Two</Tabs.Tab>
207
+ <Tabs.Tab value="three">Three</Tabs.Tab>
208
+ </Tabs.List>
209
+ <Tabs.Panel value="one">First panel</Tabs.Panel>
210
+ <Tabs.Panel value="two">Second panel</Tabs.Panel>
211
+ <Tabs.Panel value="three">Third panel</Tabs.Panel>
212
+ </Tabs.Root>
213
+ );
214
+
215
+ await waitForComponentToBeInitializedWithSelectedTab( 'One' );
216
+
217
+ const tabList = screen.getByRole( 'tablist' );
218
+ const allTabs = screen.getAllByRole( 'tab' );
219
+ const allTabpanels = screen.getAllByRole( 'tabpanel' );
220
+
221
+ expect( tabList ).toBeVisible();
222
+ // Since 'horizontal' is the default orientation, no need to set it.
223
+ expect( tabList ).not.toHaveAttribute( 'aria-orientation' );
224
+
225
+ expect( allTabs ).toHaveLength( TABS.length );
226
+
227
+ // Only 1 tab panel is accessible — the one associated with the
228
+ // selected tab. The selected `tab` aria-controls the active
229
+ // `tabpanel`, which is `aria-labelledby` the selected `tab`.
230
+ expect( allTabpanels ).toHaveLength( 1 );
231
+
232
+ expect( allTabpanels[ 0 ] ).toBeVisible();
233
+
234
+ expect( allTabs[ 0 ] ).toHaveAttribute(
235
+ 'aria-controls',
236
+ allTabpanels[ 0 ].getAttribute( 'id' )
237
+ );
238
+ expect( allTabpanels[ 0 ] ).toHaveAttribute(
239
+ 'aria-labelledby',
240
+ allTabs[ 0 ].getAttribute( 'id' )
241
+ );
242
+ } );
243
+
244
+ it( 'should associate each `tab` with the correct `tabpanel`, even if they are not rendered in the same order', async () => {
245
+ const TABS_WITH_DELTA_REVERSED = [ ...TABS_WITH_DELTA ].reverse();
246
+
247
+ const user = userEvent.setup();
248
+
249
+ render(
250
+ <Tabs.Root defaultValue="alpha">
251
+ <Tabs.List>
252
+ { TABS_WITH_DELTA.map( ( tabObj, index ) => (
253
+ <Tabs.Tab
254
+ key={ `${ tabObj.title }-${ index }` }
255
+ value={ tabObj.value }
256
+ className={ tabObj.tab.className }
257
+ disabled={ tabObj.tab.disabled }
258
+ >
259
+ { tabObj.title }
260
+ </Tabs.Tab>
261
+ ) ) }
262
+ </Tabs.List>
263
+ { TABS_WITH_DELTA_REVERSED.map( ( tabObj, index ) => (
264
+ <Tabs.Panel
265
+ key={ `${ tabObj.title }-${ index }` }
266
+ value={ tabObj.value }
267
+ // Only apply tabIndex if defined, otherwise fallback
268
+ // to default internal implementation
269
+ { ...( tabObj.tabpanel?.tabIndex !== undefined && {
270
+ tabIndex: tabObj.tabpanel.tabIndex,
271
+ } ) }
272
+ >
273
+ { tabObj.content }
274
+ </Tabs.Panel>
275
+ ) ) }
276
+ </Tabs.Root>
277
+ );
278
+
279
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
280
+
281
+ // Select Beta, make sure the correct tabpanel is rendered
282
+ await user.click( screen.getByRole( 'tab', { name: 'Beta' } ) );
283
+ expect(
284
+ screen.getByRole( 'tab', {
285
+ selected: true,
286
+ name: 'Beta',
287
+ } )
288
+ ).toBeVisible();
289
+ expect(
290
+ screen.getByRole( 'tabpanel', {
291
+ name: 'Beta',
292
+ } )
293
+ ).toBeVisible();
294
+
295
+ // Select Gamma, make sure the correct tabpanel is rendered
296
+ await user.click( screen.getByRole( 'tab', { name: 'Gamma' } ) );
297
+ expect(
298
+ screen.getByRole( 'tab', {
299
+ selected: true,
300
+ name: 'Gamma',
301
+ } )
302
+ ).toBeVisible();
303
+ expect(
304
+ screen.getByRole( 'tabpanel', {
305
+ name: 'Gamma',
306
+ } )
307
+ ).toBeVisible();
308
+
309
+ // Select Delta, make sure the correct tabpanel is rendered
310
+ await user.click( screen.getByRole( 'tab', { name: 'Delta' } ) );
311
+ expect(
312
+ screen.getByRole( 'tab', {
313
+ selected: true,
314
+ name: 'Delta',
315
+ } )
316
+ ).toBeVisible();
317
+ expect(
318
+ screen.getByRole( 'tabpanel', {
319
+ name: 'Delta',
320
+ } )
321
+ ).toBeVisible();
322
+ } );
323
+
324
+ it( "should apply the tab's `className` to the tab button", async () => {
325
+ render( <UncontrolledTabs tabs={ TABS } /> );
326
+
327
+ // Alpha is automatically selected as the selected tab.
328
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
329
+
330
+ expect(
331
+ await screen.findByRole( 'tab', { name: 'Alpha' } )
332
+ ).toHaveClass( 'alpha-class' );
333
+ expect( screen.getByRole( 'tab', { name: 'Beta' } ) ).toHaveClass(
334
+ 'beta-class'
335
+ );
336
+ expect( screen.getByRole( 'tab', { name: 'Gamma' } ) ).toHaveClass(
337
+ 'gamma-class'
338
+ );
339
+ } );
340
+
341
+ it( 'should forward refs', () => {
342
+ const rootRef = createRef< HTMLDivElement >();
343
+ const listRef = createRef< HTMLDivElement >();
344
+ const tabRef = createRef< HTMLButtonElement >();
345
+ const panelRef = createRef< HTMLDivElement >();
346
+
347
+ render(
348
+ <Tabs.Root ref={ rootRef } defaultValue="tab1">
349
+ <Tabs.List ref={ listRef }>
350
+ <Tabs.Tab ref={ tabRef } value="tab1">
351
+ Tab 1
352
+ </Tabs.Tab>
353
+ <Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
354
+ </Tabs.List>
355
+ <Tabs.Panel ref={ panelRef } value="tab1">
356
+ Panel 1 content
357
+ </Tabs.Panel>
358
+ <Tabs.Panel value="tab2">Panel 2 content</Tabs.Panel>
359
+ </Tabs.Root>
360
+ );
361
+
362
+ expect( rootRef.current ).toBeInstanceOf( HTMLDivElement );
363
+ expect( listRef.current ).toBeInstanceOf( HTMLDivElement );
364
+ expect( tabRef.current ).toBeInstanceOf( HTMLButtonElement );
365
+ expect( panelRef.current ).toBeInstanceOf( HTMLDivElement );
366
+ } );
367
+ } );
368
+
369
+ describe( 'pointer interactions', () => {
370
+ it( 'should select a tab when clicked', async () => {
371
+ const mockOnValueChange = jest.fn();
372
+
373
+ const user = userEvent.setup();
374
+
375
+ render(
376
+ <UncontrolledTabs
377
+ tabs={ TABS }
378
+ onValueChange={ mockOnValueChange }
379
+ defaultValue="alpha"
380
+ />
381
+ );
382
+
383
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
384
+
385
+ // Click on Beta, make sure beta is the selected tab
386
+ await user.click( screen.getByRole( 'tab', { name: 'Beta' } ) );
387
+
388
+ expect(
389
+ screen.getByRole( 'tab', {
390
+ selected: true,
391
+ name: 'Beta',
392
+ } )
393
+ ).toBeVisible();
394
+ expect(
395
+ screen.getByRole( 'tabpanel', {
396
+ name: 'Beta',
397
+ } )
398
+ ).toBeVisible();
399
+
400
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
401
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
402
+ 'beta',
403
+ expect.anything()
404
+ );
405
+
406
+ // Click on Alpha, make sure alpha is the selected tab
407
+ await user.click( screen.getByRole( 'tab', { name: 'Alpha' } ) );
408
+
409
+ expect(
410
+ screen.getByRole( 'tab', {
411
+ selected: true,
412
+ name: 'Alpha',
413
+ } )
414
+ ).toBeVisible();
415
+ expect(
416
+ screen.getByRole( 'tabpanel', {
417
+ name: 'Alpha',
418
+ } )
419
+ ).toBeVisible();
420
+
421
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 2 );
422
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
423
+ 'alpha',
424
+ expect.anything()
425
+ );
426
+ } );
427
+
428
+ it( 'should not select a disabled tab when clicked', async () => {
429
+ const mockOnValueChange = jest.fn();
430
+
431
+ const user = userEvent.setup();
432
+
433
+ render(
434
+ <UncontrolledTabs
435
+ tabs={ TABS_WITH_BETA_DISABLED }
436
+ onValueChange={ mockOnValueChange }
437
+ defaultValue="alpha"
438
+ />
439
+ );
440
+
441
+ // Alpha is automatically selected as the selected tab.
442
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
443
+
444
+ // Clicking on Beta does not result in beta being selected
445
+ // because the tab is disabled.
446
+ await user.click( screen.getByRole( 'tab', { name: 'Beta' } ) );
447
+
448
+ expect(
449
+ screen.getByRole( 'tab', {
450
+ selected: true,
451
+ name: 'Alpha',
452
+ } )
453
+ ).toBeVisible();
454
+ expect(
455
+ screen.getByRole( 'tabpanel', {
456
+ name: 'Alpha',
457
+ } )
458
+ ).toBeVisible();
459
+
460
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 0 );
461
+ } );
462
+ } );
463
+
464
+ describe( 'initial tab selection', () => {
465
+ describe( 'when a selected tab id is not specified', () => {
466
+ describe( 'when left `undefined` [Uncontrolled]', () => {
467
+ it( 'should choose the first tab as selected', async () => {
468
+ const user = userEvent.setup();
469
+
470
+ render( <UncontrolledTabs tabs={ TABS } /> );
471
+
472
+ // Alpha is automatically selected as the selected tab.
473
+ await waitForComponentToBeInitializedWithSelectedTab(
474
+ 'Alpha'
475
+ );
476
+
477
+ // Press tab. The selected tab (alpha) received focus.
478
+ await user.keyboard( '{Tab}' );
479
+ expect(
480
+ await screen.findByRole( 'tab', {
481
+ selected: true,
482
+ name: 'Alpha',
483
+ } )
484
+ ).toHaveFocus();
485
+
486
+ // TODO: check that `onValueChange` fired
487
+ // once https://github.com/mui/base-ui/issues/2097 is fixed
488
+ } );
489
+
490
+ it( 'should choose the first non-disabled tab if the first tab is disabled', async () => {
491
+ const user = userEvent.setup();
492
+
493
+ render(
494
+ <UncontrolledTabs tabs={ TABS_WITH_ALPHA_DISABLED } />
495
+ );
496
+
497
+ // Beta is automatically selected as the selected tab, since alpha is
498
+ // disabled.
499
+ await waitForComponentToBeInitializedWithSelectedTab(
500
+ 'Beta'
501
+ );
502
+
503
+ // Press tab. The selected tab (beta) received focus. The corresponding
504
+ // tabpanel is shown.
505
+ await user.keyboard( '{Tab}' );
506
+ expect(
507
+ await screen.findByRole( 'tab', {
508
+ selected: true,
509
+ name: 'Beta',
510
+ } )
511
+ ).toHaveFocus();
512
+
513
+ // TODO: check that `onValueChange` fired
514
+ // once https://github.com/mui/base-ui/issues/2097 is fixed
515
+ } );
516
+ } );
517
+ describe( 'when `null` [Controlled]', () => {
518
+ it( 'should not have a selected tab nor show any tabpanels, make the tablist tabbable and still allow selecting tabs', async () => {
519
+ const user = userEvent.setup();
520
+
521
+ render( <ControlledTabs tabs={ TABS } value={ null } /> );
522
+
523
+ // No initially selected tabs or tabpanels.
524
+ await waitForComponentToBeInitializedWithSelectedTab(
525
+ undefined
526
+ );
527
+
528
+ // Press tab to focus and select the first tab (alpha) and
529
+ // show the related tabpanel.
530
+ await user.keyboard( '{Tab}' );
531
+ await user.keyboard( '{Enter}' );
532
+ expect(
533
+ await screen.findByRole( 'tab', {
534
+ selected: true,
535
+ name: 'Alpha',
536
+ } )
537
+ ).toHaveFocus();
538
+ expect(
539
+ await screen.findByRole( 'tabpanel', {
540
+ name: 'Alpha',
541
+ } )
542
+ ).toBeVisible();
543
+ } );
544
+ } );
545
+ } );
546
+
547
+ describe( 'when a selected tab id is specified', () => {
548
+ describe( 'through the `defaultValue` prop [Uncontrolled]', () => {
549
+ it( 'should select the initial tab matching the `defaultValue` prop', async () => {
550
+ const user = userEvent.setup();
551
+
552
+ render(
553
+ <UncontrolledTabs tabs={ TABS } defaultValue="beta" />
554
+ );
555
+
556
+ // Beta is the initially selected tab
557
+ await waitForComponentToBeInitializedWithSelectedTab(
558
+ 'Beta'
559
+ );
560
+
561
+ // Press tab. The selected tab (beta) received focus. The corresponding
562
+ // tabpanel is shown.
563
+ await user.keyboard( '{Tab}' );
564
+ expect(
565
+ await screen.findByRole( 'tab', {
566
+ selected: true,
567
+ name: 'Beta',
568
+ } )
569
+ ).toHaveFocus();
570
+ } );
571
+
572
+ it( 'should select the initial tab matching the `defaultValue` prop even if the tab is disabled', async () => {
573
+ const user = userEvent.setup();
574
+ render(
575
+ <UncontrolledTabs
576
+ tabs={ TABS_WITH_BETA_DISABLED }
577
+ defaultValue="beta"
578
+ />
579
+ );
580
+
581
+ // Beta is automatically selected as the selected tab despite being
582
+ // disabled, respecting the `defaultValue` prop.
583
+ await waitForComponentToBeInitializedWithSelectedTab(
584
+ 'Beta'
585
+ );
586
+
587
+ // Press tab. The selected tab (beta) received focus, since it is
588
+ // accessible despite being disabled.
589
+ await user.keyboard( '{Tab}' );
590
+ expect(
591
+ await screen.findByRole( 'tab', {
592
+ selected: true,
593
+ name: 'Beta',
594
+ } )
595
+ ).toHaveFocus();
596
+ } );
597
+
598
+ it( 'should select the first tab and allow tabbing to it when `defaultValue` prop does not match any known tab', async () => {
599
+ const user = userEvent.setup();
600
+
601
+ render(
602
+ <UncontrolledTabs
603
+ tabs={ TABS }
604
+ defaultValue="non-existing-tab"
605
+ />
606
+ );
607
+
608
+ // No initially selected tabs or tabpanels, since the `defaultValue`
609
+ // prop is not matching any known tabs.
610
+ await waitForComponentToBeInitializedWithSelectedTab(
611
+ 'Alpha'
612
+ );
613
+
614
+ // Press tab. The first tab receives focus, but it's
615
+ // not selected.
616
+ await user.keyboard( '{Tab}' );
617
+ expect(
618
+ screen.getByRole( 'tab', { name: 'Alpha' } )
619
+ ).toHaveFocus();
620
+ await user.keyboard( '{Enter}' );
621
+ expect(
622
+ screen.queryByRole( 'tab', {
623
+ selected: true,
624
+ name: 'Alpha',
625
+ } )
626
+ ).toBeVisible();
627
+ expect(
628
+ await screen.findByRole( 'tabpanel', {
629
+ name: 'Alpha',
630
+ } )
631
+ ).toBeVisible();
632
+ } );
633
+
634
+ it( 'should select the first non-disabled tab and allow tabbing to it when `defaultValue` prop does not match any known tab', async () => {
635
+ const user = userEvent.setup();
636
+ render(
637
+ <UncontrolledTabs
638
+ tabs={ TABS_WITH_ALPHA_DISABLED }
639
+ defaultValue="non-existing-tab"
640
+ />
641
+ );
642
+
643
+ // No initially selected tabs or tabpanels, since the `defaultValue`
644
+ // prop is not matching any known tabs.
645
+ await waitForComponentToBeInitializedWithSelectedTab(
646
+ 'Beta'
647
+ );
648
+
649
+ // Press tab. The first non-disabled tab receives focus and is selected.
650
+ await user.keyboard( '{Tab}' );
651
+ expect(
652
+ await screen.findByRole( 'tab', {
653
+ selected: true,
654
+ name: 'Beta',
655
+ } )
656
+ ).toHaveFocus();
657
+ expect(
658
+ await screen.findByRole( 'tabpanel', {
659
+ name: 'Beta',
660
+ } )
661
+ ).toBeVisible();
662
+ } );
663
+
664
+ it( 'should ignore any changes to the `defaultValue` prop after the first render', async () => {
665
+ const mockOnValueChange = jest.fn();
666
+ const consoleErrorSpy = jest
667
+ .spyOn( console, 'error' )
668
+ .mockImplementation( () => {} );
669
+
670
+ const { rerender } = render(
671
+ <UncontrolledTabs
672
+ tabs={ TABS }
673
+ defaultValue="beta"
674
+ onValueChange={ mockOnValueChange }
675
+ />
676
+ );
677
+
678
+ // Beta is the initially selected tab
679
+ await waitForComponentToBeInitializedWithSelectedTab(
680
+ 'Beta'
681
+ );
682
+
683
+ // Changing the defaultValue prop to gamma should not have any effect.
684
+ rerender(
685
+ <UncontrolledTabs
686
+ tabs={ TABS }
687
+ defaultValue="gamma"
688
+ onValueChange={ mockOnValueChange }
689
+ />
690
+ );
691
+
692
+ expect(
693
+ await screen.findByRole( 'tab', {
694
+ selected: true,
695
+ name: 'Beta',
696
+ } )
697
+ ).toBeVisible();
698
+ expect(
699
+ screen.getByRole( 'tabpanel', {
700
+ name: 'Beta',
701
+ } )
702
+ ).toBeVisible();
703
+
704
+ expect( mockOnValueChange ).not.toHaveBeenCalled();
705
+
706
+ expect( consoleErrorSpy ).toHaveBeenCalled();
707
+ expect( consoleErrorSpy ).toHaveBeenCalledWith(
708
+ expect.stringContaining(
709
+ 'changing the default value state'
710
+ )
711
+ );
712
+
713
+ consoleErrorSpy.mockRestore();
714
+ } );
715
+ } );
716
+
717
+ describe( 'through the `value` prop [Controlled]', () => {
718
+ describe( 'when the `value` matches an existing tab', () => {
719
+ it( 'should choose the initial tab matching the `value`', async () => {
720
+ const user = userEvent.setup();
721
+
722
+ render( <ControlledTabs tabs={ TABS } value="beta" /> );
723
+
724
+ // Beta is the initially selected tab
725
+ await waitForComponentToBeInitializedWithSelectedTab(
726
+ 'Beta'
727
+ );
728
+
729
+ // Press tab. The selected tab (beta) received focus, since it is
730
+ // accessible despite being disabled.
731
+ await user.keyboard( '{Tab}' );
732
+ expect(
733
+ await screen.findByRole( 'tab', {
734
+ selected: true,
735
+ name: 'Beta',
736
+ } )
737
+ ).toHaveFocus();
738
+ } );
739
+
740
+ it( 'should choose the initial tab matching the `value` even if a `defaultValue` is passed', async () => {
741
+ const user = userEvent.setup();
742
+
743
+ render(
744
+ <ControlledTabs
745
+ tabs={ TABS }
746
+ defaultValue="beta"
747
+ value="gamma"
748
+ />
749
+ );
750
+
751
+ // Gamma is the initially selected tab
752
+ await waitForComponentToBeInitializedWithSelectedTab(
753
+ 'Gamma'
754
+ );
755
+
756
+ // Press tab. The selected tab (gamma) received focus.
757
+ await user.keyboard( '{Tab}' );
758
+ expect(
759
+ await screen.findByRole( 'tab', {
760
+ selected: true,
761
+ name: 'Gamma',
762
+ } )
763
+ ).toHaveFocus();
764
+ } );
765
+
766
+ it( 'should choose the initial tab matching the `value` even if the tab is disabled', async () => {
767
+ const user = userEvent.setup();
768
+
769
+ render(
770
+ <ControlledTabs
771
+ tabs={ TABS_WITH_BETA_DISABLED }
772
+ value="beta"
773
+ />
774
+ );
775
+
776
+ // Beta is the initially selected tab
777
+ await waitForComponentToBeInitializedWithSelectedTab(
778
+ 'Beta'
779
+ );
780
+
781
+ // Press tab. The selected tab (beta) received focus, since it is
782
+ // accessible despite being disabled.
783
+ await user.keyboard( '{Tab}' );
784
+ expect(
785
+ await screen.findByRole( 'tab', {
786
+ selected: true,
787
+ name: 'Beta',
788
+ } )
789
+ ).toHaveFocus();
790
+ } );
791
+ } );
792
+
793
+ describe( "when the `value` doesn't match an existing tab", () => {
794
+ it( 'should not have a selected tab nor show any tabpanels, but allow tabbing to the first tab', async () => {
795
+ const user = userEvent.setup();
796
+
797
+ render(
798
+ <ControlledTabs
799
+ tabs={ TABS }
800
+ value="non-existing-tab"
801
+ />
802
+ );
803
+
804
+ // No initially selected tabs or tabpanels, since the `value`
805
+ // prop is not matching any known tabs.
806
+ await waitForComponentToBeInitializedWithSelectedTab(
807
+ undefined
808
+ );
809
+
810
+ // Press tab. The first tab receives focus and gets selected.
811
+ await user.keyboard( '{Tab}' );
812
+ await user.keyboard( '{Enter}' );
813
+ expect(
814
+ await screen.findByRole( 'tab', {
815
+ selected: true,
816
+ name: 'Alpha',
817
+ } )
818
+ ).toHaveFocus();
819
+ expect(
820
+ await screen.findByRole( 'tabpanel', {
821
+ name: 'Alpha',
822
+ } )
823
+ ).toBeVisible();
824
+ } );
825
+
826
+ it( 'should not have a selected tab nor show any tabpanels, but allow tabbing to the first tab even when disabled', async () => {
827
+ const user = userEvent.setup();
828
+
829
+ render(
830
+ <ControlledTabs
831
+ tabs={ TABS_WITH_ALPHA_DISABLED }
832
+ value="non-existing-tab"
833
+ />
834
+ );
835
+
836
+ // No initially selected tabs or tabpanels, since the `value`
837
+ // prop is not matching any known tabs.
838
+ await waitForComponentToBeInitializedWithSelectedTab(
839
+ undefined
840
+ );
841
+
842
+ // Press tab. The first tab receives focus, but it's
843
+ // not selected since it's disabled.
844
+ await user.keyboard( '{Tab}' );
845
+ expect(
846
+ screen.getByRole( 'tab', { name: 'Alpha' } )
847
+ ).toHaveFocus();
848
+ await waitFor( () =>
849
+ expect(
850
+ screen.queryByRole( 'tab', { selected: true } )
851
+ ).not.toBeInTheDocument()
852
+ );
853
+ await waitFor( () =>
854
+ expect(
855
+ screen.queryByRole( 'tabpanel' )
856
+ ).not.toBeInTheDocument()
857
+ );
858
+
859
+ // Press right arrow to select the next tab (beta) and
860
+ // show the related tabpanel.
861
+ await user.keyboard( '{ArrowRight}' );
862
+ await user.keyboard( '{Enter}' );
863
+ expect(
864
+ await screen.findByRole( 'tab', {
865
+ selected: true,
866
+ name: 'Beta',
867
+ } )
868
+ ).toHaveFocus();
869
+ expect(
870
+ await screen.findByRole( 'tabpanel', {
871
+ name: 'Beta',
872
+ } )
873
+ ).toBeVisible();
874
+ } );
875
+ } );
876
+ } );
877
+ } );
878
+ } );
879
+
880
+ describe( 'keyboard interactions', () => {
881
+ describe.each( [
882
+ [ 'Uncontrolled', UncontrolledTabs ],
883
+ [ 'Controlled', ControlledTabs ],
884
+ ] )( '[`%s`]', ( _mode, Component ) => {
885
+ it( 'should handle the tablist as one tab stop', async () => {
886
+ const user = userEvent.setup();
887
+
888
+ const valueProps =
889
+ _mode === 'Uncontrolled'
890
+ ? { defaultValue: 'alpha' }
891
+ : { value: 'alpha' };
892
+
893
+ render( <Component tabs={ TABS } { ...valueProps } /> );
894
+
895
+ // Alpha is automatically selected as the selected tab.
896
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
897
+
898
+ // Press tab. The selected tab (alpha) received focus.
899
+ await user.keyboard( '{Tab}' );
900
+ expect(
901
+ await screen.findByRole( 'tab', {
902
+ selected: true,
903
+ name: 'Alpha',
904
+ } )
905
+ ).toHaveFocus();
906
+
907
+ // By default the tabpanel should receive focus
908
+ await user.keyboard( '{Tab}' );
909
+ expect(
910
+ await screen.findByRole( 'tabpanel', {
911
+ name: 'Alpha',
912
+ } )
913
+ ).toHaveFocus();
914
+ } );
915
+
916
+ it( 'should not focus the tabpanel container when it is not tabbable', async () => {
917
+ const user = userEvent.setup();
918
+
919
+ const valueProps =
920
+ _mode === 'Uncontrolled'
921
+ ? { defaultValue: 'alpha' }
922
+ : { value: 'alpha' };
923
+
924
+ render(
925
+ <Component
926
+ tabs={ TABS.map( ( tabObj ) =>
927
+ tabObj.value === 'alpha'
928
+ ? {
929
+ ...tabObj,
930
+ content: (
931
+ <>
932
+ Selected Tab: Alpha
933
+ <button>Alpha Button</button>
934
+ </>
935
+ ),
936
+ tabpanel: { tabIndex: -1 },
937
+ }
938
+ : tabObj
939
+ ) }
940
+ { ...valueProps }
941
+ />
942
+ );
943
+
944
+ // Alpha is automatically selected as the selected tab.
945
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
946
+
947
+ // Tab should initially focus the first tab in the tablist, which
948
+ // is Alpha.
949
+ await user.keyboard( '{Tab}' );
950
+ expect(
951
+ await screen.findByRole( 'tab', {
952
+ selected: true,
953
+ name: 'Alpha',
954
+ } )
955
+ ).toHaveFocus();
956
+
957
+ // In this case, the tabpanel container is skipped and focus is
958
+ // moved directly to its contents
959
+ await user.keyboard( '{Tab}' );
960
+ expect(
961
+ await screen.findByRole( 'button', {
962
+ name: 'Alpha Button',
963
+ } )
964
+ ).toHaveFocus();
965
+ } );
966
+
967
+ it( 'should select tabs in the tablist when using the left and right arrow keys when automatic tab activation is enabled', async () => {
968
+ const mockOnValueChange = jest.fn();
969
+ const user = userEvent.setup();
970
+
971
+ const valueProps =
972
+ _mode === 'Uncontrolled'
973
+ ? { defaultValue: 'alpha' }
974
+ : { value: 'alpha' };
975
+
976
+ render(
977
+ <Component
978
+ tabs={ TABS }
979
+ onValueChange={ mockOnValueChange }
980
+ selectOnMove
981
+ { ...valueProps }
982
+ />
983
+ );
984
+
985
+ // Alpha is automatically selected as the selected tab.
986
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
987
+
988
+ // TODO: re-enable once https://github.com/mui/base-ui/issues/2097 is fixed
989
+ // expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
990
+ // expect( mockOnValueChange ).toHaveBeenLastCalledWith( 'alpha' );
991
+
992
+ // Focus the tablist (and the selected tab, alpha)
993
+ // Tab should initially focus the first tab in the tablist, which
994
+ // is Alpha.
995
+ await user.keyboard( '{Tab}' );
996
+ expect(
997
+ await screen.findByRole( 'tab', {
998
+ selected: true,
999
+ name: 'Alpha',
1000
+ } )
1001
+ ).toHaveFocus();
1002
+
1003
+ // Press the right arrow key to select the beta tab
1004
+ await user.keyboard( '{ArrowRight}' );
1005
+
1006
+ expect(
1007
+ screen.getByRole( 'tab', {
1008
+ selected: true,
1009
+ name: 'Beta',
1010
+ } )
1011
+ ).toHaveFocus();
1012
+ expect(
1013
+ screen.getByRole( 'tabpanel', {
1014
+ name: 'Beta',
1015
+ } )
1016
+ ).toBeVisible();
1017
+
1018
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1019
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1020
+ 'beta',
1021
+ expect.anything()
1022
+ );
1023
+
1024
+ // Press the right arrow key to select the gamma tab
1025
+ await user.keyboard( '{ArrowRight}' );
1026
+
1027
+ expect(
1028
+ screen.getByRole( 'tab', {
1029
+ selected: true,
1030
+ name: 'Gamma',
1031
+ } )
1032
+ ).toHaveFocus();
1033
+ expect(
1034
+ screen.getByRole( 'tabpanel', {
1035
+ name: 'Gamma',
1036
+ } )
1037
+ ).toBeVisible();
1038
+
1039
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 2 );
1040
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1041
+ 'gamma',
1042
+ expect.anything()
1043
+ );
1044
+
1045
+ // Press the left arrow key to select the beta tab
1046
+ await user.keyboard( '{ArrowLeft}' );
1047
+
1048
+ expect(
1049
+ screen.getByRole( 'tab', {
1050
+ selected: true,
1051
+ name: 'Beta',
1052
+ } )
1053
+ ).toHaveFocus();
1054
+ expect(
1055
+ screen.getByRole( 'tabpanel', {
1056
+ name: 'Beta',
1057
+ } )
1058
+ ).toBeVisible();
1059
+
1060
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 3 );
1061
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1062
+ 'beta',
1063
+ expect.anything()
1064
+ );
1065
+ } );
1066
+
1067
+ it( 'should not automatically select tabs in the tablist when pressing the left and right arrow keys by default (manual tab activation)', async () => {
1068
+ const mockOnValueChange = jest.fn();
1069
+
1070
+ const user = userEvent.setup();
1071
+
1072
+ const valueProps =
1073
+ _mode === 'Uncontrolled'
1074
+ ? { defaultValue: 'alpha' }
1075
+ : { value: 'alpha' };
1076
+
1077
+ render(
1078
+ <Component
1079
+ tabs={ TABS }
1080
+ onValueChange={ mockOnValueChange }
1081
+ { ...valueProps }
1082
+ />
1083
+ );
1084
+
1085
+ // Alpha is automatically selected as the selected tab.
1086
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
1087
+
1088
+ // TODO: re-enable once https://github.com/mui/base-ui/issues/2097 is fixed
1089
+ // expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1090
+ // expect( mockOnValueChange ).toHaveBeenLastCalledWith( 'alpha' );
1091
+
1092
+ // Focus the tablist (and the selected tab, alpha)
1093
+ // Tab should initially focus the first tab in the tablist, which
1094
+ // is Alpha.
1095
+ await user.keyboard( '{Tab}' );
1096
+ expect(
1097
+ await screen.findByRole( 'tab', {
1098
+ selected: true,
1099
+ name: 'Alpha',
1100
+ } )
1101
+ ).toHaveFocus();
1102
+
1103
+ // Press the right arrow key to move focus to the beta tab,
1104
+ // but without selecting it
1105
+ await user.keyboard( '{ArrowRight}' );
1106
+
1107
+ expect(
1108
+ screen.getByRole( 'tab', {
1109
+ selected: false,
1110
+ name: 'Beta',
1111
+ } )
1112
+ ).toHaveFocus();
1113
+ expect(
1114
+ await screen.findByRole( 'tab', {
1115
+ selected: true,
1116
+ name: 'Alpha',
1117
+ } )
1118
+ ).toBeVisible();
1119
+ expect(
1120
+ screen.getByRole( 'tabpanel', {
1121
+ name: 'Alpha',
1122
+ } )
1123
+ ).toBeVisible();
1124
+
1125
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 0 );
1126
+
1127
+ // Press the space key to click the beta tab, and select it.
1128
+ // The same should be true with any other mean of clicking the tab button
1129
+ // (ie. mouse click, enter key).
1130
+ await user.keyboard( '{ }' );
1131
+
1132
+ await waitFor( () =>
1133
+ expect(
1134
+ screen.getByRole( 'tab', {
1135
+ selected: true,
1136
+ name: 'Beta',
1137
+ } )
1138
+ ).toHaveFocus()
1139
+ );
1140
+ expect(
1141
+ screen.getByRole( 'tabpanel', {
1142
+ name: 'Beta',
1143
+ } )
1144
+ ).toBeVisible();
1145
+
1146
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1147
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1148
+ 'beta',
1149
+ expect.anything()
1150
+ );
1151
+ } );
1152
+
1153
+ 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 () => {
1154
+ const mockOnValueChange = jest.fn();
1155
+
1156
+ const user = userEvent.setup();
1157
+
1158
+ const valueProps =
1159
+ _mode === 'Uncontrolled'
1160
+ ? { defaultValue: 'alpha' }
1161
+ : { value: 'alpha' };
1162
+
1163
+ const { rerender } = render(
1164
+ <Component
1165
+ tabs={ TABS }
1166
+ onValueChange={ mockOnValueChange }
1167
+ { ...valueProps }
1168
+ />
1169
+ );
1170
+
1171
+ // Alpha is automatically selected as the selected tab.
1172
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
1173
+
1174
+ // TODO: re-enable once https://github.com/mui/base-ui/issues/2097 is fixed
1175
+ // expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1176
+ // expect( mockOnValueChange ).toHaveBeenLastCalledWith( 'alpha' );
1177
+
1178
+ // Focus the tablist (and the selected tab, alpha)
1179
+ // Tab should initially focus the first tab in the tablist, which
1180
+ // is Alpha.
1181
+ await user.keyboard( '{Tab}' );
1182
+ expect(
1183
+ await screen.findByRole( 'tab', {
1184
+ selected: true,
1185
+ name: 'Alpha',
1186
+ } )
1187
+ ).toHaveFocus();
1188
+
1189
+ // Press the up arrow key, but the focused/selected tab does not change.
1190
+ await user.keyboard( '{ArrowUp}' );
1191
+
1192
+ expect(
1193
+ screen.getByRole( 'tab', {
1194
+ selected: true,
1195
+ name: 'Alpha',
1196
+ } )
1197
+ ).toHaveFocus();
1198
+ expect(
1199
+ screen.getByRole( 'tabpanel', {
1200
+ name: 'Alpha',
1201
+ } )
1202
+ ).toBeVisible();
1203
+
1204
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 0 );
1205
+
1206
+ // Press the down arrow key, but the focused/selected tab does not change.
1207
+ await user.keyboard( '{ArrowDown}' );
1208
+
1209
+ expect(
1210
+ screen.getByRole( 'tab', {
1211
+ selected: true,
1212
+ name: 'Alpha',
1213
+ } )
1214
+ ).toHaveFocus();
1215
+ expect(
1216
+ screen.getByRole( 'tabpanel', {
1217
+ name: 'Alpha',
1218
+ } )
1219
+ ).toBeVisible();
1220
+
1221
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 0 );
1222
+
1223
+ // Change the orientation to "vertical" and rerender the component.
1224
+ rerender(
1225
+ <Component
1226
+ tabs={ TABS }
1227
+ onValueChange={ mockOnValueChange }
1228
+ orientation="vertical"
1229
+ { ...valueProps }
1230
+ />
1231
+ );
1232
+
1233
+ // Pressing the down arrow key now selects the next tab (beta).
1234
+ await user.keyboard( '{ArrowDown}' );
1235
+ await user.keyboard( '{Enter}' );
1236
+
1237
+ expect(
1238
+ screen.getByRole( 'tab', {
1239
+ selected: true,
1240
+ name: 'Beta',
1241
+ } )
1242
+ ).toHaveFocus();
1243
+ expect(
1244
+ screen.getByRole( 'tabpanel', {
1245
+ name: 'Beta',
1246
+ } )
1247
+ ).toBeVisible();
1248
+
1249
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1250
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1251
+ 'beta',
1252
+ expect.anything()
1253
+ );
1254
+
1255
+ // Pressing the up arrow key now selects the previous tab (alpha).
1256
+ await user.keyboard( '{ArrowUp}' );
1257
+ await user.keyboard( '{Enter}' );
1258
+
1259
+ expect(
1260
+ screen.getByRole( 'tab', {
1261
+ selected: true,
1262
+ name: 'Alpha',
1263
+ } )
1264
+ ).toHaveFocus();
1265
+ expect(
1266
+ screen.getByRole( 'tabpanel', {
1267
+ name: 'Alpha',
1268
+ } )
1269
+ ).toBeVisible();
1270
+
1271
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 2 );
1272
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1273
+ 'alpha',
1274
+ expect.anything()
1275
+ );
1276
+ } );
1277
+
1278
+ it( 'should loop tab focus at the end of the tablist when using arrow keys', async () => {
1279
+ const mockOnValueChange = jest.fn();
1280
+
1281
+ const user = userEvent.setup();
1282
+
1283
+ const valueProps =
1284
+ _mode === 'Uncontrolled'
1285
+ ? { defaultValue: 'alpha' }
1286
+ : { value: 'alpha' };
1287
+
1288
+ render(
1289
+ <Component
1290
+ tabs={ TABS }
1291
+ onValueChange={ mockOnValueChange }
1292
+ { ...valueProps }
1293
+ />
1294
+ );
1295
+
1296
+ // Alpha is automatically selected as the selected tab.
1297
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
1298
+
1299
+ // TODO: re-enable once https://github.com/mui/base-ui/issues/2097 is fixed
1300
+ // expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1301
+ // expect( mockOnValueChange ).toHaveBeenLastCalledWith( 'alpha' );
1302
+
1303
+ // Focus the tablist (and the selected tab, alpha)
1304
+ // Tab should initially focus the first tab in the tablist, which
1305
+ // is Alpha.
1306
+ await user.keyboard( '{Tab}' );
1307
+ expect(
1308
+ await screen.findByRole( 'tab', {
1309
+ selected: true,
1310
+ name: 'Alpha',
1311
+ } )
1312
+ ).toHaveFocus();
1313
+
1314
+ // Press the left arrow key to loop around and select the gamma tab
1315
+ await user.keyboard( '{ArrowLeft}' );
1316
+ await user.keyboard( '{Enter}' );
1317
+
1318
+ expect(
1319
+ screen.getByRole( 'tab', {
1320
+ selected: true,
1321
+ name: 'Gamma',
1322
+ } )
1323
+ ).toHaveFocus();
1324
+ expect(
1325
+ screen.getByRole( 'tabpanel', {
1326
+ name: 'Gamma',
1327
+ } )
1328
+ ).toBeVisible();
1329
+
1330
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1331
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1332
+ 'gamma',
1333
+ expect.anything()
1334
+ );
1335
+
1336
+ // Press the right arrow key to loop around and select the alpha tab
1337
+ await user.keyboard( '{ArrowRight}' );
1338
+ await user.keyboard( '{Enter}' );
1339
+
1340
+ expect(
1341
+ screen.getByRole( 'tab', {
1342
+ selected: true,
1343
+ name: 'Alpha',
1344
+ } )
1345
+ ).toHaveFocus();
1346
+ expect(
1347
+ screen.getByRole( 'tabpanel', {
1348
+ name: 'Alpha',
1349
+ } )
1350
+ ).toBeVisible();
1351
+
1352
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 2 );
1353
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1354
+ 'alpha',
1355
+ expect.anything()
1356
+ );
1357
+ } );
1358
+
1359
+ it( 'should swap the left and right arrow keys when selecting tabs if the writing direction is set to RTL', async () => {
1360
+ const mockOnValueChange = jest.fn();
1361
+
1362
+ const user = userEvent.setup();
1363
+
1364
+ const valueProps =
1365
+ _mode === 'Uncontrolled'
1366
+ ? { defaultValue: 'alpha' }
1367
+ : { value: 'alpha' };
1368
+
1369
+ render(
1370
+ <DirectionProvider direction="rtl">
1371
+ <Component
1372
+ tabs={ TABS }
1373
+ onValueChange={ mockOnValueChange }
1374
+ { ...valueProps }
1375
+ />
1376
+ </DirectionProvider>
1377
+ );
1378
+
1379
+ // Alpha is automatically selected as the selected tab.
1380
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
1381
+
1382
+ // TODO: re-enable once https://github.com/mui/base-ui/issues/2097 is fixed
1383
+ // expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1384
+ // expect( mockOnValueChange ).toHaveBeenLastCalledWith( 'alpha' );
1385
+
1386
+ // Focus the tablist (and the selected tab, alpha)
1387
+ // Tab should initially focus the first tab in the tablist, which
1388
+ // is Alpha.
1389
+ await user.keyboard( '{Tab}' );
1390
+ expect(
1391
+ await screen.findByRole( 'tab', {
1392
+ selected: true,
1393
+ name: 'Alpha',
1394
+ } )
1395
+ ).toHaveFocus();
1396
+
1397
+ // Press the left arrow key to select the beta tab
1398
+ await user.keyboard( '{ArrowLeft}' );
1399
+ await user.keyboard( '{Enter}' );
1400
+
1401
+ expect(
1402
+ screen.getByRole( 'tab', {
1403
+ selected: true,
1404
+ name: 'Beta',
1405
+ } )
1406
+ ).toHaveFocus();
1407
+ expect(
1408
+ screen.getByRole( 'tabpanel', {
1409
+ name: 'Beta',
1410
+ } )
1411
+ ).toBeVisible();
1412
+
1413
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1414
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1415
+ 'beta',
1416
+ expect.anything()
1417
+ );
1418
+
1419
+ // Press the left arrow key to select the gamma tab
1420
+ await user.keyboard( '{ArrowLeft}' );
1421
+ await user.keyboard( '{Enter}' );
1422
+
1423
+ expect(
1424
+ screen.getByRole( 'tab', {
1425
+ selected: true,
1426
+ name: 'Gamma',
1427
+ } )
1428
+ ).toHaveFocus();
1429
+ expect(
1430
+ screen.getByRole( 'tabpanel', {
1431
+ name: 'Gamma',
1432
+ } )
1433
+ ).toBeVisible();
1434
+
1435
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 2 );
1436
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1437
+ 'gamma',
1438
+ expect.anything()
1439
+ );
1440
+
1441
+ // Press the right arrow key to select the beta tab
1442
+ await user.keyboard( '{ArrowRight}' );
1443
+ await user.keyboard( '{Enter}' );
1444
+
1445
+ expect(
1446
+ screen.getByRole( 'tab', {
1447
+ selected: true,
1448
+ name: 'Beta',
1449
+ } )
1450
+ ).toHaveFocus();
1451
+ expect(
1452
+ screen.getByRole( 'tabpanel', {
1453
+ name: 'Beta',
1454
+ } )
1455
+ ).toBeVisible();
1456
+
1457
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 3 );
1458
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1459
+ 'beta',
1460
+ expect.anything()
1461
+ );
1462
+ } );
1463
+
1464
+ it( 'should focus tabs in the tablist even if disabled', async () => {
1465
+ const mockOnValueChange = jest.fn();
1466
+
1467
+ const user = userEvent.setup();
1468
+
1469
+ const valueProps =
1470
+ _mode === 'Uncontrolled'
1471
+ ? { defaultValue: 'alpha' }
1472
+ : { value: 'alpha' };
1473
+
1474
+ render(
1475
+ <Component
1476
+ tabs={ TABS_WITH_BETA_DISABLED }
1477
+ onValueChange={ mockOnValueChange }
1478
+ { ...valueProps }
1479
+ />
1480
+ );
1481
+
1482
+ // Alpha is automatically selected as the selected tab.
1483
+ await waitForComponentToBeInitializedWithSelectedTab( 'Alpha' );
1484
+
1485
+ // TODO: re-enable once https://github.com/mui/base-ui/issues/2097 is fixed
1486
+ // expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1487
+ // expect( mockOnValueChange ).toHaveBeenLastCalledWith( 'alpha' );
1488
+
1489
+ // Focus the tablist (and the selected tab, alpha)
1490
+ // Tab should initially focus the first tab in the tablist, which
1491
+ // is Alpha.
1492
+ await user.keyboard( '{Tab}' );
1493
+ expect(
1494
+ await screen.findByRole( 'tab', {
1495
+ selected: true,
1496
+ name: 'Alpha',
1497
+ } )
1498
+ ).toHaveFocus();
1499
+
1500
+ // Pressing the right arrow key moves focus to the beta tab, but alpha
1501
+ // remains the selected tab because beta is disabled.
1502
+ await user.keyboard( '{ArrowRight}' );
1503
+ await user.keyboard( '{Enter}' );
1504
+
1505
+ expect(
1506
+ screen.getByRole( 'tab', {
1507
+ selected: false,
1508
+ name: 'Beta',
1509
+ } )
1510
+ ).toHaveFocus();
1511
+ expect(
1512
+ screen.getByRole( 'tab', {
1513
+ selected: true,
1514
+ name: 'Alpha',
1515
+ } )
1516
+ ).toBeVisible();
1517
+ expect(
1518
+ screen.getByRole( 'tabpanel', {
1519
+ name: 'Alpha',
1520
+ } )
1521
+ ).toBeVisible();
1522
+
1523
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 0 );
1524
+
1525
+ // Press the right arrow key to select the gamma tab
1526
+ await user.keyboard( '{ArrowRight}' );
1527
+ await user.keyboard( '{Enter}' );
1528
+
1529
+ expect(
1530
+ screen.getByRole( 'tab', {
1531
+ selected: true,
1532
+ name: 'Gamma',
1533
+ } )
1534
+ ).toHaveFocus();
1535
+ expect(
1536
+ screen.getByRole( 'tabpanel', {
1537
+ name: 'Gamma',
1538
+ } )
1539
+ ).toBeVisible();
1540
+
1541
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1542
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1543
+ 'gamma',
1544
+ expect.anything()
1545
+ );
1546
+ } );
1547
+ } );
1548
+
1549
+ describe( 'When `selectedId` is changed by the controlling component [Controlled]', () => {
1550
+ describe.each( [ true, false ] )(
1551
+ 'and automatic tab activation is %s',
1552
+ ( selectOnMove ) => {
1553
+ it( 'should continue to handle arrow key navigation properly', async () => {
1554
+ const user = userEvent.setup();
1555
+
1556
+ const { rerender } = render(
1557
+ <ControlledTabs
1558
+ tabs={ TABS }
1559
+ value="beta"
1560
+ selectOnMove={ selectOnMove }
1561
+ />
1562
+ );
1563
+
1564
+ // Beta is the selected tab.
1565
+ await waitForComponentToBeInitializedWithSelectedTab(
1566
+ 'Beta'
1567
+ );
1568
+
1569
+ // Tab key should focus the currently first tab (if manual activation mode),
1570
+ // or the currently selected tab (if automatic activation mode).
1571
+ await user.keyboard( '{Tab}' );
1572
+ expect(
1573
+ screen.getByRole( 'tab', {
1574
+ selected: true,
1575
+ name: 'Beta',
1576
+ } )
1577
+ ).toHaveFocus();
1578
+
1579
+ rerender(
1580
+ <ControlledTabs
1581
+ tabs={ TABS }
1582
+ value="gamma"
1583
+ selectOnMove={ selectOnMove }
1584
+ />
1585
+ );
1586
+
1587
+ expect(
1588
+ screen.getByRole( 'tab', {
1589
+ selected: true,
1590
+ name: 'Gamma',
1591
+ } )
1592
+ ).toBeVisible();
1593
+ expect(
1594
+ screen.getByRole( 'tab', {
1595
+ selected: false,
1596
+ name: 'Beta',
1597
+ } )
1598
+ ).toHaveFocus();
1599
+
1600
+ // Arrow left should move focus to the previous tab.
1601
+ await user.keyboard( '{ArrowLeft}' );
1602
+
1603
+ await waitFor( () =>
1604
+ expect(
1605
+ screen.getByRole( 'tab', {
1606
+ selected: selectOnMove,
1607
+ name: 'Alpha',
1608
+ } )
1609
+ ).toHaveFocus()
1610
+ );
1611
+ } );
1612
+
1613
+ it( 'should focus the correct tab when tabbing out and back into the tablist', async () => {
1614
+ const user = userEvent.setup();
1615
+
1616
+ const { rerender } = render(
1617
+ <>
1618
+ <button>Focus me</button>
1619
+ <ControlledTabs
1620
+ tabs={ TABS }
1621
+ value="beta"
1622
+ selectOnMove={ selectOnMove }
1623
+ />
1624
+ </>
1625
+ );
1626
+
1627
+ // Beta is the selected tab.
1628
+ await waitForComponentToBeInitializedWithSelectedTab(
1629
+ 'Beta'
1630
+ );
1631
+
1632
+ // Tab key should focus the currently selected tab, which is Beta.
1633
+ await user.keyboard( '{Tab}' );
1634
+ await user.keyboard( '{Tab}' );
1635
+ expect(
1636
+ screen.getByRole( 'tab', {
1637
+ selected: true,
1638
+ name: 'Beta',
1639
+ } )
1640
+ ).toHaveFocus();
1641
+
1642
+ // Change the selected tab to gamma via a controlled update.
1643
+ rerender(
1644
+ <>
1645
+ <button>Focus me</button>
1646
+ <ControlledTabs
1647
+ tabs={ TABS }
1648
+ value="gamma"
1649
+ selectOnMove={ selectOnMove }
1650
+ />
1651
+ </>
1652
+ );
1653
+
1654
+ expect(
1655
+ screen.getByRole( 'tab', {
1656
+ selected: true,
1657
+ name: 'Gamma',
1658
+ } )
1659
+ ).toBeVisible();
1660
+ expect(
1661
+ screen.getByRole( 'tab', {
1662
+ selected: false,
1663
+ name: 'Beta',
1664
+ } )
1665
+ ).toHaveFocus();
1666
+
1667
+ // Press shift+tab, move focus to the button before Tabs
1668
+ await user.keyboard( '{Shift>}{Tab}{/Shift}' );
1669
+ expect(
1670
+ screen.getByRole( 'button', { name: 'Focus me' } )
1671
+ ).toHaveFocus();
1672
+
1673
+ // Press tab, move focus back to the tablist
1674
+ await user.keyboard( '{Tab}' );
1675
+
1676
+ expect(
1677
+ screen.getByRole( 'tab', {
1678
+ name: 'Beta',
1679
+ } )
1680
+ ).toHaveFocus();
1681
+ } );
1682
+ }
1683
+ );
1684
+ } );
1685
+ } );
1686
+
1687
+ describe( 'miscellaneous runtime changes', () => {
1688
+ describe( 'removing a tab', () => {
1689
+ describe( 'with no explicitly set initial tab', () => {
1690
+ it( 'should not select a new tab when the selected tab is removed', async () => {
1691
+ const mockOnValueChange = jest.fn();
1692
+
1693
+ const user = userEvent.setup();
1694
+
1695
+ const { rerender } = render(
1696
+ <UncontrolledTabs
1697
+ tabs={ TABS }
1698
+ onValueChange={ mockOnValueChange }
1699
+ defaultValue="alpha"
1700
+ />
1701
+ );
1702
+
1703
+ // Alpha is automatically selected as the selected tab.
1704
+ await waitForComponentToBeInitializedWithSelectedTab(
1705
+ 'Alpha'
1706
+ );
1707
+
1708
+ // TODO: re-enable once https://github.com/mui/base-ui/issues/2097 is fixed
1709
+ // expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1710
+ // expect( mockOnValueChange ).toHaveBeenLastCalledWith( 'alpha' );
1711
+
1712
+ // Select gamma
1713
+ await user.click(
1714
+ screen.getByRole( 'tab', { name: 'Gamma' } )
1715
+ );
1716
+
1717
+ expect(
1718
+ screen.getByRole( 'tab', {
1719
+ selected: true,
1720
+ name: 'Gamma',
1721
+ } )
1722
+ ).toHaveFocus();
1723
+ expect(
1724
+ screen.getByRole( 'tabpanel', {
1725
+ name: 'Gamma',
1726
+ } )
1727
+ ).toBeVisible();
1728
+
1729
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1730
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1731
+ 'gamma',
1732
+ expect.anything()
1733
+ );
1734
+
1735
+ // Remove gamma
1736
+ rerender(
1737
+ <UncontrolledTabs
1738
+ tabs={ TABS.slice( 0, 2 ) }
1739
+ onValueChange={ mockOnValueChange }
1740
+ defaultValue="alpha"
1741
+ />
1742
+ );
1743
+
1744
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength( 2 );
1745
+
1746
+ // Falls back to the first tab.
1747
+ expect(
1748
+ screen.getByRole( 'tab', {
1749
+ name: 'Alpha',
1750
+ selected: true,
1751
+ } )
1752
+ ).toBeVisible();
1753
+ expect(
1754
+ screen.getByRole( 'tabpanel', {
1755
+ name: 'Alpha',
1756
+ } )
1757
+ ).toBeVisible();
1758
+
1759
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1760
+ } );
1761
+ } );
1762
+
1763
+ describe.each( [
1764
+ [ 'defaultValue', 'Uncontrolled', UncontrolledTabs ],
1765
+ [ 'value', 'Controlled', ControlledTabs ],
1766
+ ] )(
1767
+ 'when using the `%s` prop [%s]',
1768
+ ( propName, mode, Component ) => {
1769
+ it( 'should handle the selected tab being removed', async () => {
1770
+ const mockOnValueChange = jest.fn();
1771
+
1772
+ const initialComponentProps = {
1773
+ tabs: TABS,
1774
+ [ propName ]: 'gamma',
1775
+ onValueChange: mockOnValueChange,
1776
+ };
1777
+
1778
+ const { rerender } = render(
1779
+ <Component { ...initialComponentProps } />
1780
+ );
1781
+
1782
+ // Gamma is the selected tab.
1783
+ await waitForComponentToBeInitializedWithSelectedTab(
1784
+ 'Gamma'
1785
+ );
1786
+
1787
+ // Remove gamma
1788
+ rerender(
1789
+ <Component
1790
+ { ...initialComponentProps }
1791
+ tabs={ TABS.slice( 0, 2 ) }
1792
+ />
1793
+ );
1794
+
1795
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength(
1796
+ 2
1797
+ );
1798
+
1799
+ if ( mode === 'Uncontrolled' ) {
1800
+ // Falls back to the first tab.
1801
+ expect(
1802
+ screen.getByRole( 'tab', {
1803
+ name: 'Alpha',
1804
+ selected: true,
1805
+ } )
1806
+ ).toBeVisible();
1807
+ expect(
1808
+ screen.getByRole( 'tabpanel', {
1809
+ name: 'Alpha',
1810
+ } )
1811
+ ).toBeVisible();
1812
+ }
1813
+
1814
+ if ( mode === 'Controlled' ) {
1815
+ // No tab should be selected i.e. it doesn't fall back to first tab.
1816
+ expect(
1817
+ screen.queryByRole( 'tab', { selected: true } )
1818
+ ).not.toBeInTheDocument();
1819
+ expect(
1820
+ screen.queryByRole( 'tabpanel' )
1821
+ ).not.toBeInTheDocument();
1822
+ }
1823
+
1824
+ // Re-add gamma.
1825
+ rerender( <Component { ...initialComponentProps } /> );
1826
+
1827
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength(
1828
+ TABS.length
1829
+ );
1830
+
1831
+ if ( mode === 'Uncontrolled' ) {
1832
+ // First tab stays selected.
1833
+ expect(
1834
+ screen.getByRole( 'tab', {
1835
+ selected: true,
1836
+ name: 'Alpha',
1837
+ } )
1838
+ ).toBeVisible();
1839
+ expect(
1840
+ screen.getByRole( 'tabpanel', {
1841
+ name: 'Alpha',
1842
+ } )
1843
+ ).toBeVisible();
1844
+ }
1845
+
1846
+ if ( mode === 'Controlled' ) {
1847
+ // Gamma becomes selected again.
1848
+ expect(
1849
+ screen.getByRole( 'tab', {
1850
+ selected: true,
1851
+ name: 'Gamma',
1852
+ } )
1853
+ ).toBeVisible();
1854
+ expect(
1855
+ screen.getByRole( 'tabpanel', {
1856
+ name: 'Gamma',
1857
+ } )
1858
+ ).toBeVisible();
1859
+ }
1860
+
1861
+ expect( mockOnValueChange ).not.toHaveBeenCalled();
1862
+ } );
1863
+
1864
+ it( `should not fall back to the tab matching the \`${ propName }\` prop when a different selected tab is removed`, async () => {
1865
+ const mockOnValueChange = jest.fn();
1866
+
1867
+ const initialComponentProps = {
1868
+ tabs: TABS,
1869
+ [ propName ]: 'gamma',
1870
+ onValueChange: mockOnValueChange,
1871
+ };
1872
+
1873
+ const user = userEvent.setup();
1874
+
1875
+ const { rerender } = render(
1876
+ <Component { ...initialComponentProps } />
1877
+ );
1878
+
1879
+ // Gamma is the selected tab.
1880
+ await waitForComponentToBeInitializedWithSelectedTab(
1881
+ 'Gamma'
1882
+ );
1883
+
1884
+ // Select alpha
1885
+ await user.click(
1886
+ screen.getByRole( 'tab', { name: 'Alpha' } )
1887
+ );
1888
+
1889
+ expect(
1890
+ screen.getByRole( 'tab', {
1891
+ selected: true,
1892
+ name: 'Alpha',
1893
+ } )
1894
+ ).toHaveFocus();
1895
+ expect(
1896
+ screen.getByRole( 'tabpanel', {
1897
+ name: 'Alpha',
1898
+ } )
1899
+ ).toBeVisible();
1900
+
1901
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1902
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
1903
+ 'alpha',
1904
+ expect.anything()
1905
+ );
1906
+
1907
+ // Remove alpha
1908
+ rerender(
1909
+ <Component
1910
+ { ...initialComponentProps }
1911
+ tabs={ TABS.slice( 1 ) }
1912
+ />
1913
+ );
1914
+
1915
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength(
1916
+ 2
1917
+ );
1918
+
1919
+ if ( mode === 'Uncontrolled' ) {
1920
+ // Falls back to the first available tab.
1921
+ expect(
1922
+ screen.getByRole( 'tab', {
1923
+ name: 'Beta',
1924
+ selected: true,
1925
+ } )
1926
+ ).toBeVisible();
1927
+ expect(
1928
+ screen.getByRole( 'tabpanel', {
1929
+ name: 'Beta',
1930
+ } )
1931
+ ).toBeVisible();
1932
+ }
1933
+
1934
+ if ( mode === 'Controlled' ) {
1935
+ // No tab should be selected i.e. it doesn't fall back to gamma,
1936
+ // even if it matches the `defaultValue` prop.
1937
+ expect(
1938
+ screen.queryByRole( 'tab', { selected: true } )
1939
+ ).not.toBeInTheDocument();
1940
+ // No tabpanel should be rendered either
1941
+ expect(
1942
+ screen.queryByRole( 'tabpanel' )
1943
+ ).not.toBeInTheDocument();
1944
+ }
1945
+
1946
+ // Re-add alpha. Alpha becomes selected again.
1947
+ rerender( <Component { ...initialComponentProps } /> );
1948
+
1949
+ expect( screen.getAllByRole( 'tab' ) ).toHaveLength(
1950
+ TABS.length
1951
+ );
1952
+
1953
+ if ( mode === 'Uncontrolled' ) {
1954
+ // Beta stays selected.
1955
+ expect(
1956
+ screen.getByRole( 'tab', {
1957
+ selected: true,
1958
+ name: 'Beta',
1959
+ } )
1960
+ ).toBeVisible();
1961
+ expect(
1962
+ screen.getByRole( 'tabpanel', {
1963
+ name: 'Beta',
1964
+ } )
1965
+ ).toBeVisible();
1966
+ }
1967
+
1968
+ if ( mode === 'Controlled' ) {
1969
+ expect(
1970
+ screen.getByRole( 'tab', {
1971
+ selected: true,
1972
+ name: 'Alpha',
1973
+ } )
1974
+ ).toBeVisible();
1975
+ expect(
1976
+ screen.getByRole( 'tabpanel', {
1977
+ name: 'Alpha',
1978
+ } )
1979
+ ).toBeVisible();
1980
+ }
1981
+
1982
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
1983
+ } );
1984
+ }
1985
+ );
1986
+ } );
1987
+
1988
+ describe( 'adding a tab', () => {
1989
+ describe.each( [
1990
+ [ 'defaultValue', 'Uncontrolled', UncontrolledTabs ],
1991
+ [ 'value', 'Controlled', ControlledTabs ],
1992
+ ] )(
1993
+ 'when using the `%s` prop [%s]',
1994
+ ( propName, mode, Component ) => {
1995
+ it( `should select a newly added tab if it matches the \`${ propName }\` prop`, async () => {
1996
+ const mockOnValueChange = jest.fn();
1997
+
1998
+ const initialComponentProps = {
1999
+ tabs: TABS,
2000
+ [ propName ]: 'delta',
2001
+ onValueChange: mockOnValueChange,
2002
+ };
2003
+
2004
+ const { rerender } = render(
2005
+ <Component { ...initialComponentProps } />
2006
+ );
2007
+
2008
+ if ( mode === 'Uncontrolled' ) {
2009
+ // Falls back to the first tab.
2010
+ await waitForComponentToBeInitializedWithSelectedTab(
2011
+ 'Alpha'
2012
+ );
2013
+ }
2014
+
2015
+ if ( mode === 'Controlled' ) {
2016
+ // No initially selected tabs or tabpanels, since the `value`
2017
+ // prop is not matching any known tabs.
2018
+ await waitForComponentToBeInitializedWithSelectedTab(
2019
+ undefined
2020
+ );
2021
+ }
2022
+
2023
+ expect( mockOnValueChange ).not.toHaveBeenCalled();
2024
+
2025
+ // Re-render with delta added.
2026
+ rerender(
2027
+ <Component
2028
+ { ...initialComponentProps }
2029
+ tabs={ TABS_WITH_DELTA }
2030
+ />
2031
+ );
2032
+
2033
+ if ( mode === 'Uncontrolled' ) {
2034
+ // Alpha stays selected.
2035
+ expect(
2036
+ screen.getByRole( 'tab', {
2037
+ selected: true,
2038
+ name: 'Alpha',
2039
+ } )
2040
+ ).toBeVisible();
2041
+ expect(
2042
+ screen.getByRole( 'tabpanel', {
2043
+ name: 'Alpha',
2044
+ } )
2045
+ ).toBeVisible();
2046
+ }
2047
+
2048
+ if ( mode === 'Controlled' ) {
2049
+ // Delta becomes selected
2050
+ expect(
2051
+ screen.getByRole( 'tab', {
2052
+ selected: true,
2053
+ name: 'Delta',
2054
+ } )
2055
+ ).toBeVisible();
2056
+ expect(
2057
+ screen.getByRole( 'tabpanel', {
2058
+ name: 'Delta',
2059
+ } )
2060
+ ).toBeVisible();
2061
+ }
2062
+
2063
+ expect( mockOnValueChange ).not.toHaveBeenCalled();
2064
+ } );
2065
+ }
2066
+ );
2067
+ } );
2068
+ describe( 'a tab becomes disabled', () => {
2069
+ describe.each( [
2070
+ [ 'defaultValue', 'Uncontrolled', UncontrolledTabs ],
2071
+ [ 'value', 'Controlled', ControlledTabs ],
2072
+ ] )(
2073
+ 'when using the `%s` prop [%s]',
2074
+ ( propName, mode, Component ) => {
2075
+ it( `should keep the initial tab matching the \`${ propName }\` prop as selected even if it becomes disabled`, async () => {
2076
+ const mockOnValueChange = jest.fn();
2077
+
2078
+ const initialComponentProps = {
2079
+ tabs: TABS,
2080
+ [ propName ]: 'beta',
2081
+ onValueChange: mockOnValueChange,
2082
+ };
2083
+
2084
+ const { rerender } = render(
2085
+ <Component { ...initialComponentProps } />
2086
+ );
2087
+
2088
+ // Beta is the selected tab.
2089
+ await waitForComponentToBeInitializedWithSelectedTab(
2090
+ 'Beta'
2091
+ );
2092
+
2093
+ expect( mockOnValueChange ).not.toHaveBeenCalled();
2094
+
2095
+ // Re-render with beta disabled.
2096
+ rerender(
2097
+ <Component
2098
+ { ...initialComponentProps }
2099
+ tabs={ TABS_WITH_BETA_DISABLED }
2100
+ />
2101
+ );
2102
+
2103
+ // Beta continues to be selected and focused, even if it is disabled.
2104
+ expect(
2105
+ screen.getByRole( 'tab', {
2106
+ selected: true,
2107
+ name: 'Beta',
2108
+ } )
2109
+ ).toBeVisible();
2110
+ expect(
2111
+ screen.getByRole( 'tabpanel', {
2112
+ name: 'Beta',
2113
+ } )
2114
+ ).toBeVisible();
2115
+
2116
+ // Re-enable beta.
2117
+ rerender( <Component { ...initialComponentProps } /> );
2118
+
2119
+ // Beta continues to be selected and focused.
2120
+ expect(
2121
+ screen.getByRole( 'tab', {
2122
+ selected: true,
2123
+ name: 'Beta',
2124
+ } )
2125
+ ).toBeVisible();
2126
+ expect(
2127
+ screen.getByRole( 'tabpanel', {
2128
+ name: 'Beta',
2129
+ } )
2130
+ ).toBeVisible();
2131
+
2132
+ expect( mockOnValueChange ).not.toHaveBeenCalled();
2133
+ } );
2134
+
2135
+ it( 'should handle the user-selected tab becoming disabled', async () => {
2136
+ const mockOnValueChange = jest.fn();
2137
+
2138
+ const user = userEvent.setup();
2139
+
2140
+ const initialComponentProps = {
2141
+ tabs: TABS,
2142
+ [ propName ]: 'alpha',
2143
+ onValueChange: mockOnValueChange,
2144
+ };
2145
+
2146
+ const { rerender } = render(
2147
+ <Component { ...initialComponentProps } />
2148
+ );
2149
+
2150
+ // Alpha is automatically selected as the selected tab.
2151
+ await waitForComponentToBeInitializedWithSelectedTab(
2152
+ 'Alpha'
2153
+ );
2154
+
2155
+ // TODO: re-enable once https://github.com/mui/base-ui/issues/2097 is fixed
2156
+ // expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
2157
+ // expect( mockOnValueChange ).toHaveBeenLastCalledWith( 'alpha' );
2158
+
2159
+ // Click on beta tab, beta becomes selected.
2160
+ await user.click(
2161
+ screen.getByRole( 'tab', { name: 'Beta' } )
2162
+ );
2163
+
2164
+ expect(
2165
+ screen.getByRole( 'tab', {
2166
+ selected: true,
2167
+ name: 'Beta',
2168
+ } )
2169
+ ).toBeVisible();
2170
+ expect(
2171
+ screen.getByRole( 'tabpanel', {
2172
+ name: 'Beta',
2173
+ } )
2174
+ ).toBeVisible();
2175
+
2176
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
2177
+ expect( mockOnValueChange ).toHaveBeenLastCalledWith(
2178
+ 'beta',
2179
+ expect.anything()
2180
+ );
2181
+
2182
+ // Re-render with beta disabled.
2183
+ rerender(
2184
+ <Component
2185
+ { ...initialComponentProps }
2186
+ tabs={ TABS_WITH_BETA_DISABLED }
2187
+ />
2188
+ );
2189
+
2190
+ if ( mode === 'Uncontrolled' ) {
2191
+ // Alpha becomes the selected tab.
2192
+ expect(
2193
+ screen.getByRole( 'tab', {
2194
+ selected: true,
2195
+ name: 'Alpha',
2196
+ } )
2197
+ ).toBeVisible();
2198
+ expect(
2199
+ screen.getByRole( 'tabpanel', {
2200
+ name: 'Alpha',
2201
+ } )
2202
+ ).toBeVisible();
2203
+ }
2204
+
2205
+ if ( mode === 'Controlled' ) {
2206
+ // Beta continues to be selected, even if it is disabled.
2207
+ expect(
2208
+ screen.getByRole( 'tab', {
2209
+ selected: true,
2210
+ name: 'Beta',
2211
+ } )
2212
+ ).toHaveFocus();
2213
+ expect(
2214
+ screen.getByRole( 'tabpanel', {
2215
+ name: 'Beta',
2216
+ } )
2217
+ ).toBeVisible();
2218
+ }
2219
+
2220
+ // Re-enable beta.
2221
+ rerender( <Component { ...initialComponentProps } /> );
2222
+
2223
+ if ( mode === 'Uncontrolled' ) {
2224
+ // Alpha stays selected.
2225
+ expect(
2226
+ screen.getByRole( 'tab', {
2227
+ selected: true,
2228
+ name: 'Alpha',
2229
+ } )
2230
+ ).toBeVisible();
2231
+ expect(
2232
+ screen.getByRole( 'tabpanel', {
2233
+ name: 'Alpha',
2234
+ } )
2235
+ ).toBeVisible();
2236
+ }
2237
+
2238
+ if ( mode === 'Controlled' ) {
2239
+ // Beta continues to be selected and focused.
2240
+ expect(
2241
+ screen.getByRole( 'tab', {
2242
+ selected: true,
2243
+ name: 'Beta',
2244
+ } )
2245
+ ).toBeVisible();
2246
+ expect(
2247
+ screen.getByRole( 'tabpanel', {
2248
+ name: 'Beta',
2249
+ } )
2250
+ ).toBeVisible();
2251
+ }
2252
+
2253
+ expect( mockOnValueChange ).toHaveBeenCalledTimes( 1 );
2254
+ } );
2255
+ }
2256
+ );
2257
+ } );
2258
+ } );
2259
+ } );
2260
+ /* eslint-enable jest/no-conditional-expect */