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