@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.
- package/AGENTS.md +9 -0
- package/CHANGELOG.md +32 -1
- package/CLAUDE.md +1 -0
- package/README.md +13 -12
- package/build/badge/badge.cjs +37 -62
- package/build/badge/badge.cjs.map +4 -4
- package/build/button/button.cjs +3 -3
- package/build/button/button.cjs.map +2 -2
- package/build/dialog/action.cjs +46 -0
- package/build/dialog/action.cjs.map +7 -0
- package/build/dialog/close-icon.cjs +57 -0
- package/build/dialog/close-icon.cjs.map +7 -0
- package/build/dialog/context.cjs +76 -0
- package/build/dialog/context.cjs.map +7 -0
- package/build/dialog/footer.cjs +64 -0
- package/build/dialog/footer.cjs.map +7 -0
- package/build/dialog/header.cjs +64 -0
- package/build/dialog/header.cjs.map +7 -0
- package/build/dialog/index.cjs +52 -0
- package/build/dialog/index.cjs.map +7 -0
- package/build/dialog/popup.cjs +77 -0
- package/build/dialog/popup.cjs.map +7 -0
- package/build/dialog/root.cjs +35 -0
- package/build/dialog/root.cjs.map +7 -0
- package/build/dialog/title.cjs +76 -0
- package/build/dialog/title.cjs.map +7 -0
- package/build/dialog/trigger.cjs +38 -0
- package/build/dialog/trigger.cjs.map +7 -0
- package/build/dialog/types.cjs +19 -0
- package/build/dialog/types.cjs.map +7 -0
- package/build/form/primitives/field/root.cjs +1 -1
- package/build/form/primitives/field/root.cjs.map +1 -1
- package/build/form/primitives/fieldset/root.cjs +3 -3
- package/build/form/primitives/fieldset/root.cjs.map +2 -2
- package/build/form/primitives/index.cjs +5 -2
- package/build/form/primitives/index.cjs.map +2 -2
- package/build/form/primitives/input-layout/input-layout.cjs +3 -3
- package/build/form/primitives/input-layout/input-layout.cjs.map +2 -2
- package/build/form/primitives/input-layout/slot.cjs +3 -3
- package/build/form/primitives/input-layout/slot.cjs.map +2 -2
- package/build/form/primitives/select/item.cjs +3 -3
- package/build/form/primitives/select/item.cjs.map +2 -2
- package/build/form/primitives/select/popup.cjs +3 -3
- package/build/form/primitives/select/popup.cjs.map +2 -2
- package/build/form/primitives/select/trigger.cjs +3 -3
- package/build/form/primitives/select/trigger.cjs.map +2 -2
- package/build/{box → form/primitives/textarea}/index.cjs +7 -7
- package/build/form/primitives/textarea/index.cjs.map +7 -0
- package/build/form/primitives/textarea/textarea.cjs +90 -0
- package/build/form/primitives/textarea/textarea.cjs.map +7 -0
- package/build/form/primitives/textarea/types.cjs +19 -0
- package/build/form/primitives/textarea/types.cjs.map +7 -0
- package/build/icon-button/icon-button.cjs +104 -0
- package/build/icon-button/icon-button.cjs.map +7 -0
- package/build/icon-button/index.cjs +31 -0
- package/build/icon-button/index.cjs.map +7 -0
- package/build/icon-button/types.cjs +19 -0
- package/build/icon-button/types.cjs.map +7 -0
- package/build/index.cjs +8 -2
- package/build/index.cjs.map +2 -2
- package/build/tabs/index.cjs +40 -0
- package/build/tabs/index.cjs.map +7 -0
- package/build/tabs/list.cjs +145 -0
- package/build/tabs/list.cjs.map +7 -0
- package/build/tabs/panel.cjs +67 -0
- package/build/tabs/panel.cjs.map +7 -0
- package/build/tabs/root.cjs +38 -0
- package/build/tabs/root.cjs.map +7 -0
- package/build/tabs/tab.cjs +71 -0
- package/build/tabs/tab.cjs.map +7 -0
- package/build/{box → tabs}/types.cjs +1 -1
- package/build/tabs/types.cjs.map +7 -0
- package/build/tooltip/popup.cjs +3 -3
- package/build/tooltip/popup.cjs.map +2 -2
- package/build-module/badge/badge.mjs +27 -62
- package/build-module/badge/badge.mjs.map +3 -3
- package/build-module/button/button.mjs +3 -3
- package/build-module/button/button.mjs.map +2 -2
- package/build-module/dialog/action.mjs +21 -0
- package/build-module/dialog/action.mjs.map +7 -0
- package/build-module/dialog/close-icon.mjs +32 -0
- package/build-module/dialog/close-icon.mjs.map +7 -0
- package/build-module/dialog/context.mjs +57 -0
- package/build-module/dialog/context.mjs.map +7 -0
- package/build-module/dialog/footer.mjs +29 -0
- package/build-module/dialog/footer.mjs.map +7 -0
- package/build-module/dialog/header.mjs +29 -0
- package/build-module/dialog/header.mjs.map +7 -0
- package/build-module/dialog/index.mjs +20 -0
- package/build-module/dialog/index.mjs.map +7 -0
- package/build-module/dialog/popup.mjs +44 -0
- package/build-module/dialog/popup.mjs.map +7 -0
- package/build-module/dialog/root.mjs +10 -0
- package/build-module/dialog/root.mjs.map +7 -0
- package/build-module/dialog/title.mjs +41 -0
- package/build-module/dialog/title.mjs.map +7 -0
- package/build-module/dialog/trigger.mjs +13 -0
- package/build-module/dialog/trigger.mjs.map +7 -0
- package/build-module/form/primitives/field/root.mjs +1 -1
- package/build-module/form/primitives/field/root.mjs.map +1 -1
- package/build-module/form/primitives/fieldset/root.mjs +3 -3
- package/build-module/form/primitives/fieldset/root.mjs.map +2 -2
- package/build-module/form/primitives/index.mjs +3 -1
- package/build-module/form/primitives/index.mjs.map +2 -2
- package/build-module/form/primitives/input-layout/input-layout.mjs +3 -3
- package/build-module/form/primitives/input-layout/input-layout.mjs.map +2 -2
- package/build-module/form/primitives/input-layout/slot.mjs +3 -3
- package/build-module/form/primitives/input-layout/slot.mjs.map +2 -2
- package/build-module/form/primitives/select/item.mjs +3 -3
- package/build-module/form/primitives/select/item.mjs.map +2 -2
- package/build-module/form/primitives/select/popup.mjs +3 -3
- package/build-module/form/primitives/select/popup.mjs.map +2 -2
- package/build-module/form/primitives/select/trigger.mjs +3 -3
- package/build-module/form/primitives/select/trigger.mjs.map +2 -2
- package/build-module/form/primitives/textarea/index.mjs +6 -0
- package/build-module/form/primitives/textarea/index.mjs.map +7 -0
- package/build-module/form/primitives/textarea/textarea.mjs +55 -0
- package/build-module/form/primitives/textarea/textarea.mjs.map +7 -0
- package/build-module/form/primitives/textarea/types.mjs +1 -0
- package/build-module/form/primitives/textarea/types.mjs.map +7 -0
- package/build-module/icon-button/icon-button.mjs +69 -0
- package/build-module/icon-button/icon-button.mjs.map +7 -0
- package/build-module/icon-button/index.mjs +6 -0
- package/build-module/icon-button/index.mjs.map +7 -0
- package/build-module/icon-button/types.mjs +1 -0
- package/build-module/icon-button/types.mjs.map +7 -0
- package/build-module/index.mjs +5 -1
- package/build-module/index.mjs.map +2 -2
- package/build-module/tabs/index.mjs +12 -0
- package/build-module/tabs/index.mjs.map +7 -0
- package/build-module/tabs/list.mjs +110 -0
- package/build-module/tabs/list.mjs.map +7 -0
- package/build-module/tabs/panel.mjs +32 -0
- package/build-module/tabs/panel.mjs.map +7 -0
- package/build-module/tabs/root.mjs +13 -0
- package/build-module/tabs/root.mjs.map +7 -0
- package/build-module/tabs/tab.mjs +36 -0
- package/build-module/tabs/tab.mjs.map +7 -0
- package/build-module/tabs/types.mjs +1 -0
- package/build-module/tabs/types.mjs.map +7 -0
- package/build-module/tooltip/popup.mjs +3 -3
- package/build-module/tooltip/popup.mjs.map +2 -2
- package/build-types/badge/badge.d.ts +1 -2
- package/build-types/badge/badge.d.ts.map +1 -1
- package/build-types/button/stories/index.story.d.ts +1 -2
- package/build-types/button/stories/index.story.d.ts.map +1 -1
- package/build-types/dialog/action.d.ts +8 -0
- package/build-types/dialog/action.d.ts.map +1 -0
- package/build-types/dialog/close-icon.d.ts +8 -0
- package/build-types/dialog/close-icon.d.ts.map +1 -0
- package/build-types/dialog/context.d.ts +25 -0
- package/build-types/dialog/context.d.ts.map +1 -0
- package/build-types/dialog/footer.d.ts +8 -0
- package/build-types/dialog/footer.d.ts.map +1 -0
- package/build-types/dialog/header.d.ts +8 -0
- package/build-types/dialog/header.d.ts.map +1 -0
- package/build-types/dialog/index.d.ts +10 -0
- package/build-types/dialog/index.d.ts.map +1 -0
- package/build-types/dialog/popup.d.ts +8 -0
- package/build-types/dialog/popup.d.ts.map +1 -0
- package/build-types/dialog/root.d.ts +10 -0
- package/build-types/dialog/root.d.ts.map +1 -0
- package/build-types/dialog/stories/index.story.d.ts +18 -0
- package/build-types/dialog/stories/index.story.d.ts.map +1 -0
- package/build-types/dialog/test/index.test.d.ts +2 -0
- package/build-types/dialog/test/index.test.d.ts.map +1 -0
- package/build-types/dialog/title.d.ts +12 -0
- package/build-types/dialog/title.d.ts.map +1 -0
- package/build-types/dialog/trigger.d.ts +7 -0
- package/build-types/dialog/trigger.d.ts.map +1 -0
- package/build-types/dialog/types.d.ts +77 -0
- package/build-types/dialog/types.d.ts.map +1 -0
- package/build-types/form/primitives/field/stories/index.story.d.ts +0 -1
- package/build-types/form/primitives/field/stories/index.story.d.ts.map +1 -1
- package/build-types/form/primitives/index.d.ts +1 -0
- package/build-types/form/primitives/index.d.ts.map +1 -1
- package/build-types/form/primitives/input/input.d.ts +1 -1
- package/build-types/form/primitives/select/stories/index.story.d.ts +0 -1
- package/build-types/form/primitives/select/stories/index.story.d.ts.map +1 -1
- package/build-types/form/primitives/textarea/index.d.ts +2 -0
- package/build-types/form/primitives/textarea/index.d.ts.map +1 -0
- package/build-types/form/primitives/textarea/stories/index.story.d.ts +13 -0
- package/build-types/form/primitives/textarea/stories/index.story.d.ts.map +1 -0
- package/build-types/form/primitives/textarea/test/index.test.d.ts +2 -0
- package/build-types/form/primitives/textarea/test/index.test.d.ts.map +1 -0
- package/build-types/form/primitives/textarea/textarea.d.ts +4 -0
- package/build-types/form/primitives/textarea/textarea.d.ts.map +1 -0
- package/build-types/form/primitives/textarea/types.d.ts +11 -0
- package/build-types/form/primitives/textarea/types.d.ts.map +1 -0
- package/build-types/icon-button/icon-button.d.ts +13 -0
- package/build-types/icon-button/icon-button.d.ts.map +1 -0
- package/build-types/icon-button/index.d.ts +2 -0
- package/build-types/icon-button/index.d.ts.map +1 -0
- package/build-types/icon-button/stories/index.story.d.ts +19 -0
- package/build-types/icon-button/stories/index.story.d.ts.map +1 -0
- package/build-types/icon-button/test/index.test.d.ts +2 -0
- package/build-types/icon-button/test/index.test.d.ts.map +1 -0
- package/build-types/icon-button/types.d.ts +36 -0
- package/build-types/icon-button/types.d.ts.map +1 -0
- package/build-types/index.d.ts +3 -1
- package/build-types/index.d.ts.map +1 -1
- package/build-types/stack/stories/index.story.d.ts.map +1 -1
- package/build-types/tabs/index.d.ts +6 -0
- package/build-types/tabs/index.d.ts.map +1 -0
- package/build-types/tabs/list.d.ts +16 -0
- package/build-types/tabs/list.d.ts.map +1 -0
- package/build-types/tabs/panel.d.ts +15 -0
- package/build-types/tabs/panel.d.ts.map +1 -0
- package/build-types/tabs/root.d.ts +15 -0
- package/build-types/tabs/root.d.ts.map +1 -0
- package/build-types/tabs/stories/index.story.d.ts +13 -0
- package/build-types/tabs/stories/index.story.d.ts.map +1 -0
- package/build-types/tabs/tab.d.ts +15 -0
- package/build-types/tabs/tab.d.ts.map +1 -0
- package/build-types/tabs/test/index.test.d.ts +2 -0
- package/build-types/tabs/test/index.test.d.ts.map +1 -0
- package/build-types/tabs/types.d.ts +33 -0
- package/build-types/tabs/types.d.ts.map +1 -0
- package/package.json +12 -10
- package/src/badge/badge.tsx +19 -78
- package/src/badge/stories/choosing-intent.story.tsx +1 -1
- package/src/badge/style.module.css +48 -0
- package/src/button/stories/index.story.tsx +3 -16
- package/src/button/style.module.css +23 -12
- package/src/dialog/action.tsx +22 -0
- package/src/dialog/close-icon.tsx +32 -0
- package/src/dialog/context.tsx +113 -0
- package/src/dialog/footer.tsx +26 -0
- package/src/dialog/header.tsx +26 -0
- package/src/dialog/index.ts +10 -0
- package/src/dialog/popup.tsx +46 -0
- package/src/dialog/root.tsx +14 -0
- package/src/dialog/stories/index.story.tsx +177 -0
- package/src/dialog/style.module.css +114 -0
- package/src/dialog/test/index.test.tsx +309 -0
- package/src/dialog/title.tsx +39 -0
- package/src/dialog/trigger.tsx +14 -0
- package/src/dialog/types.ts +93 -0
- package/src/form/primitives/field/root.tsx +1 -1
- package/src/form/primitives/field/stories/index.story.tsx +0 -1
- package/src/form/primitives/fieldset/style.module.css +1 -1
- package/src/form/primitives/index.ts +1 -0
- package/src/form/primitives/input-layout/style.module.css +5 -8
- package/src/form/primitives/select/stories/index.story.tsx +0 -1
- package/src/form/primitives/select/test/index.test.tsx +0 -2
- package/src/form/primitives/textarea/index.ts +1 -0
- package/src/form/primitives/textarea/stories/index.story.tsx +40 -0
- package/src/form/primitives/textarea/style.module.css +22 -0
- package/src/form/primitives/textarea/test/index.test.tsx +143 -0
- package/src/form/primitives/textarea/textarea.tsx +51 -0
- package/src/form/primitives/textarea/types.ts +18 -0
- package/src/icon-button/icon-button.tsx +65 -0
- package/src/icon-button/index.ts +1 -0
- package/src/icon-button/stories/index.story.tsx +128 -0
- package/src/icon-button/style.module.css +16 -0
- package/src/icon-button/test/index.test.tsx +86 -0
- package/src/icon-button/types.ts +38 -0
- package/src/index.ts +3 -1
- package/src/stack/stories/index.story.tsx +4 -5
- package/src/tabs/index.ts +6 -0
- package/src/tabs/list.tsx +130 -0
- package/src/tabs/panel.tsx +23 -0
- package/src/tabs/root.tsx +15 -0
- package/src/tabs/stories/best-practices.mdx +85 -0
- package/src/tabs/stories/index.story.tsx +363 -0
- package/src/tabs/style.module.css +269 -0
- package/src/tabs/tab.tsx +29 -0
- package/src/tabs/test/index.test.tsx +2260 -0
- package/src/tabs/types.ts +36 -0
- package/src/tooltip/style.module.css +3 -3
- package/src/utils/css/item-popup.module.css +2 -2
- package/src/utils/css/select-trigger.module.css +1 -1
- package/build/box/box.cjs +0 -88
- package/build/box/box.cjs.map +0 -7
- package/build/box/index.cjs.map +0 -7
- package/build/box/types.cjs.map +0 -7
- package/build-module/box/box.mjs +0 -63
- package/build-module/box/box.mjs.map +0 -7
- package/build-module/box/index.mjs +0 -6
- package/build-module/box/index.mjs.map +0 -7
- package/build-types/box/box.d.ts +0 -7
- package/build-types/box/box.d.ts.map +0 -1
- package/build-types/box/index.d.ts +0 -2
- package/build-types/box/index.d.ts.map +0 -1
- package/build-types/box/stories/index.story.d.ts +0 -8
- package/build-types/box/stories/index.story.d.ts.map +0 -1
- package/build-types/box/test/box.test.d.ts +0 -2
- package/build-types/box/test/box.test.d.ts.map +0 -1
- package/build-types/box/types.d.ts +0 -46
- package/build-types/box/types.d.ts.map +0 -1
- package/src/box/box.tsx +0 -118
- package/src/box/index.ts +0 -1
- package/src/box/stories/index.story.tsx +0 -41
- package/src/box/test/box.test.tsx +0 -29
- package/src/box/types.ts +0 -61
- /package/build-module/{box → dialog}/types.mjs +0 -0
- /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 */
|