@sproutsocial/seeds-react-tabs 1.0.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/.eslintignore +6 -0
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +7 -0
- package/dist/esm/index.js +204 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +241 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +9 -0
- package/package.json +43 -0
- package/src/Tabs.stories.tsx +122 -0
- package/src/Tabs.tsx +171 -0
- package/src/TabsTypes.ts +31 -0
- package/src/__tests__/Tabs.typetest.tsx +20 -0
- package/src/__tests__/tabs.test.tsx +271 -0
- package/src/index.ts +5 -0
- package/src/styled.d.ts +7 -0
- package/src/styles.ts +80 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +12 -0
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sproutsocial/seeds-react-tabs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Seeds React Tabs",
|
|
5
|
+
"author": "Sprout Social, Inc.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/esm/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup --dts",
|
|
12
|
+
"build:debug": "tsup --dts --metafile",
|
|
13
|
+
"dev": "tsup --watch --dts",
|
|
14
|
+
"clean": "rm -rf .turbo dist",
|
|
15
|
+
"clean:modules": "rm -rf node_modules",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"test:watch": "jest --watch --coverage=false"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@sproutsocial/seeds-react-theme": "*",
|
|
22
|
+
"@sproutsocial/seeds-react-system-props": "*",
|
|
23
|
+
"@sproutsocial/seeds-react-button": "*"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^18.0.0",
|
|
27
|
+
"@types/styled-components": "^5.1.26",
|
|
28
|
+
"@sproutsocial/eslint-config-seeds": "*",
|
|
29
|
+
"react": "^18.0.0",
|
|
30
|
+
"styled-components": "^5.2.3",
|
|
31
|
+
"tsup": "^8.0.2",
|
|
32
|
+
"typescript": "^5.6.2",
|
|
33
|
+
"@sproutsocial/seeds-tsconfig": "*",
|
|
34
|
+
"@sproutsocial/seeds-testing": "*",
|
|
35
|
+
"@sproutsocial/seeds-react-testing-library": "*"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"styled-components": "^5.2.3"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Button } from "@sproutsocial/seeds-react-button";
|
|
3
|
+
import { Icon, type TypeIconName } from "@sproutsocial/seeds-react-icon";
|
|
4
|
+
import { Tabs, type TypeTabsProps } from "./";
|
|
5
|
+
import { Text } from "@sproutsocial/seeds-react-text";
|
|
6
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
7
|
+
|
|
8
|
+
interface TypeTabData {
|
|
9
|
+
id: string;
|
|
10
|
+
text: string;
|
|
11
|
+
icon: TypeIconName;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface TypeTabsStoryState {
|
|
15
|
+
selected: string;
|
|
16
|
+
tabs?: TypeTabData[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const meta: Meta<typeof Tabs> = {
|
|
20
|
+
title: "Components/Tabs",
|
|
21
|
+
component: Tabs,
|
|
22
|
+
args: {
|
|
23
|
+
fullWidth: false,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
|
|
29
|
+
type Story = StoryObj<typeof Tabs>;
|
|
30
|
+
|
|
31
|
+
export const Default: Story = {
|
|
32
|
+
render: (args) => {
|
|
33
|
+
const [selectedId, setSelectedId] = useState("notifications");
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Tabs
|
|
37
|
+
selectedId={selectedId}
|
|
38
|
+
onSelect={(selected) => setSelectedId(selected)}
|
|
39
|
+
fullWidth={args.fullWidth}
|
|
40
|
+
>
|
|
41
|
+
<Tabs.Button id="notifications">
|
|
42
|
+
<Icon name="bell-outline" mr="12px" aria-hidden />
|
|
43
|
+
<Text size="300" fontWeight="600">
|
|
44
|
+
Notifications
|
|
45
|
+
</Text>
|
|
46
|
+
</Tabs.Button>
|
|
47
|
+
<Tabs.Button id="issues">
|
|
48
|
+
<Icon name="triangle-exclamation-outline" mr="12px" aria-hidden />
|
|
49
|
+
<Text size="300" fontWeight="600">
|
|
50
|
+
Issues
|
|
51
|
+
</Text>
|
|
52
|
+
</Tabs.Button>
|
|
53
|
+
</Tabs>
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
name: "Default",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const RemoveATab: Story = {
|
|
60
|
+
render: (args) => {
|
|
61
|
+
const [state, setState] = useState<TypeTabsStoryState>({
|
|
62
|
+
selected: "notifications",
|
|
63
|
+
tabs: [
|
|
64
|
+
{
|
|
65
|
+
id: "notifications",
|
|
66
|
+
text: "Notifications",
|
|
67
|
+
icon: "bell-outline",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "issues",
|
|
71
|
+
text: "Issues",
|
|
72
|
+
icon: "triangle-exclamation-outline",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "fun",
|
|
76
|
+
text: "Fun Tab",
|
|
77
|
+
icon: "x-twitter",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "drafts",
|
|
81
|
+
text: "Drafts",
|
|
82
|
+
icon: "paper-outline",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<React.Fragment>
|
|
89
|
+
<Button
|
|
90
|
+
onClick={() =>
|
|
91
|
+
setState({
|
|
92
|
+
...state,
|
|
93
|
+
tabs: state.tabs?.slice(1),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
>
|
|
97
|
+
Remove a tab <Icon name="x-outline" aria-hidden />
|
|
98
|
+
</Button>
|
|
99
|
+
<Tabs
|
|
100
|
+
selectedId={state.selected}
|
|
101
|
+
onSelect={(selected) =>
|
|
102
|
+
setState({
|
|
103
|
+
...state,
|
|
104
|
+
selected,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
fullWidth={args.fullWidth}
|
|
108
|
+
>
|
|
109
|
+
{state.tabs?.map((tab) => (
|
|
110
|
+
<Tabs.Button key={tab.id} id={tab.id}>
|
|
111
|
+
<Icon name={tab.icon} mr="12px" aria-hidden />
|
|
112
|
+
<Text size="300" fontWeight="600">
|
|
113
|
+
{tab.text}
|
|
114
|
+
</Text>
|
|
115
|
+
</Tabs.Button>
|
|
116
|
+
))}
|
|
117
|
+
</Tabs>
|
|
118
|
+
</React.Fragment>
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
name: "Remove a tab",
|
|
122
|
+
};
|
package/src/Tabs.tsx
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Container, { TabItem, TabButton } from "./styles";
|
|
3
|
+
import type { TypeTabButtonsProps, TypeTabsProps } from "./TabsTypes";
|
|
4
|
+
|
|
5
|
+
interface TypeTabButtonRef {
|
|
6
|
+
current: null | React.ElementRef<"button">;
|
|
7
|
+
}
|
|
8
|
+
interface TypeTabButtonContext {
|
|
9
|
+
onActivate: (arg0: string) => void;
|
|
10
|
+
onTabMount: (id: string, ref: TypeTabButtonRef) => void;
|
|
11
|
+
onTabUpdate: (previousId: string, nextId: string) => void;
|
|
12
|
+
onTabUnmount: (id: string) => void;
|
|
13
|
+
selectedId: string;
|
|
14
|
+
fullWidth?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TabButtonContext = React.createContext<Partial<TypeTabButtonContext>>({});
|
|
18
|
+
|
|
19
|
+
class TabItemButton extends React.Component<TypeTabButtonsProps> {
|
|
20
|
+
static override contextType = TabButtonContext;
|
|
21
|
+
// @ts-notes - using the legacy syntax here because `declare` does not play well with babel
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
23
|
+
//@ts-ignore
|
|
24
|
+
context!: React.ContextType<typeof TabButtonContext>;
|
|
25
|
+
|
|
26
|
+
buttonRef: TypeTabButtonRef = React.createRef<React.ElementRef<"button">>();
|
|
27
|
+
|
|
28
|
+
override componentDidMount() {
|
|
29
|
+
this.context.onTabMount?.(this.props.id, this.buttonRef);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override componentDidUpdate(prevProps: TypeTabButtonsProps) {
|
|
33
|
+
if (prevProps.id !== this.props.id) {
|
|
34
|
+
this.context.onTabUpdate?.(prevProps.id, this.props.id);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override componentWillUnmount() {
|
|
39
|
+
this.context.onTabUnmount?.(this.props.id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override render() {
|
|
43
|
+
const { children, id, ...rest } = this.props;
|
|
44
|
+
const { selectedId, onActivate, fullWidth } = this.context;
|
|
45
|
+
const isSelected = selectedId === id;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<TabItem key={id} fullWidth={fullWidth} isSelected={isSelected}>
|
|
49
|
+
<TabButton
|
|
50
|
+
innerRef={this.buttonRef}
|
|
51
|
+
id={id}
|
|
52
|
+
onClick={() => onActivate?.(id)}
|
|
53
|
+
isSelected={isSelected}
|
|
54
|
+
tabIndex={isSelected ? 0 : -1}
|
|
55
|
+
fullWidth={fullWidth}
|
|
56
|
+
data-qa-tab-button={id}
|
|
57
|
+
data-qa-tab-button-state={isSelected}
|
|
58
|
+
aria-selected={isSelected}
|
|
59
|
+
role="tab"
|
|
60
|
+
// TODO: Add a TabPanel subcomponent for use with tabs
|
|
61
|
+
// aria-controls={tabPanelId}
|
|
62
|
+
{...rest}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
</TabButton>
|
|
66
|
+
</TabItem>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Render a group of buttons in a tab-heading style
|
|
73
|
+
*/
|
|
74
|
+
class Tabs extends React.Component<TypeTabsProps> {
|
|
75
|
+
static Button = TabItemButton;
|
|
76
|
+
buttonRefs: Record<string, TypeTabButtonRef | null | undefined> = {};
|
|
77
|
+
tabList: string[] = [];
|
|
78
|
+
|
|
79
|
+
getSelectedId = () => this.props.selectedId;
|
|
80
|
+
onActivate = (id: string) => this.props.onSelect(id);
|
|
81
|
+
onTabMount = (id: string, ref: TypeTabButtonRef) => {
|
|
82
|
+
if (!this.tabList.includes(id)) {
|
|
83
|
+
this.tabList.push(id);
|
|
84
|
+
this.buttonRefs[id] = ref;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
onTabUpdate = (previousId: string, newId: string) => {
|
|
89
|
+
if (this.tabList.includes(previousId)) {
|
|
90
|
+
this.tabList = this.tabList.map((id) => (id === previousId ? newId : id));
|
|
91
|
+
this.buttonRefs[newId] = this.buttonRefs[previousId];
|
|
92
|
+
this.buttonRefs[previousId] = undefined;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
onTabUnmount = (id: string) => {
|
|
97
|
+
if (this.tabList.includes(id)) {
|
|
98
|
+
this.tabList = this.tabList.filter((currentId) => currentId !== id);
|
|
99
|
+
this.buttonRefs[id] = undefined;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
selectNextTab = (id: string) => {
|
|
104
|
+
const buttonRef = this.buttonRefs[id];
|
|
105
|
+
this.props.onSelect(id);
|
|
106
|
+
buttonRef &&
|
|
107
|
+
buttonRef.current &&
|
|
108
|
+
buttonRef.current.focus &&
|
|
109
|
+
buttonRef.current.focus();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
keyHandler = ({ keyCode }: React.KeyboardEvent<HTMLUListElement>) => {
|
|
113
|
+
switch (keyCode) {
|
|
114
|
+
// left arrow
|
|
115
|
+
case 37:
|
|
116
|
+
this.tabList.forEach((id, index) => {
|
|
117
|
+
if (id === this.getSelectedId()) {
|
|
118
|
+
const count = this.tabList.length;
|
|
119
|
+
const nextIndex = index - 1 >= 0 ? index - 1 : count - 1;
|
|
120
|
+
const nextId = this.tabList[nextIndex];
|
|
121
|
+
this.selectNextTab(nextId ? nextId : "");
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
// right arrow
|
|
127
|
+
case 39:
|
|
128
|
+
this.tabList.forEach((id, index) => {
|
|
129
|
+
if (id === this.getSelectedId()) {
|
|
130
|
+
const count = this.tabList.length;
|
|
131
|
+
const nextIndex = index + 1 < count ? index + 1 : 0;
|
|
132
|
+
const nextId = this.tabList[nextIndex];
|
|
133
|
+
this.selectNextTab(nextId ? nextId : "");
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
default:
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
override render() {
|
|
144
|
+
const { children, qa, onSelect, ...rest } = this.props;
|
|
145
|
+
return (
|
|
146
|
+
<Container
|
|
147
|
+
data-qa-tabs=""
|
|
148
|
+
onKeyUp={this.keyHandler}
|
|
149
|
+
onSelect={onSelect}
|
|
150
|
+
role="tablist"
|
|
151
|
+
{...qa}
|
|
152
|
+
{...rest}
|
|
153
|
+
>
|
|
154
|
+
<TabButtonContext.Provider
|
|
155
|
+
value={{
|
|
156
|
+
selectedId: this.getSelectedId(),
|
|
157
|
+
fullWidth: this.props.fullWidth,
|
|
158
|
+
onActivate: this.onActivate,
|
|
159
|
+
onTabMount: this.onTabMount,
|
|
160
|
+
onTabUpdate: this.onTabUpdate,
|
|
161
|
+
onTabUnmount: this.onTabUnmount,
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{children}
|
|
165
|
+
</TabButtonContext.Provider>
|
|
166
|
+
</Container>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default Tabs;
|
package/src/TabsTypes.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type {
|
|
3
|
+
TypeStyledComponentsCommonProps,
|
|
4
|
+
TypeSystemCommonProps,
|
|
5
|
+
} from "@sproutsocial/seeds-react-system-props";
|
|
6
|
+
import type { TypeButtonProps } from "@sproutsocial/seeds-react-button";
|
|
7
|
+
|
|
8
|
+
export interface TypeTabButtonsProps extends TypeButtonProps {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
|
|
11
|
+
/** Should be unique among sibling elements */
|
|
12
|
+
id: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TypeTabsProps
|
|
16
|
+
extends TypeStyledComponentsCommonProps,
|
|
17
|
+
TypeSystemCommonProps,
|
|
18
|
+
Omit<
|
|
19
|
+
React.ComponentPropsWithoutRef<"ul">,
|
|
20
|
+
keyof TypeSystemCommonProps | "onSelect"
|
|
21
|
+
> {
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
onSelect: (arg0: string) => void;
|
|
24
|
+
|
|
25
|
+
/** Whether or not the tabs should stretch to fill the width of their container */
|
|
26
|
+
fullWidth?: boolean;
|
|
27
|
+
qa?: object;
|
|
28
|
+
|
|
29
|
+
/** ID of the selected tab */
|
|
30
|
+
selectedId: string;
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Tabs from "../";
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5
|
+
function TabsTypes() {
|
|
6
|
+
const tabOne = "Tab one";
|
|
7
|
+
const mockHandler = () => {};
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<Tabs selectedId={tabOne} onSelect={mockHandler}>
|
|
12
|
+
<Tabs.Button id={tabOne}>{tabOne}</Tabs.Button>
|
|
13
|
+
</Tabs>
|
|
14
|
+
{/* @ts-expect-error - test that invalid onSelect is rejected */}
|
|
15
|
+
<Tabs selectedId={3} onSelect="invalid">
|
|
16
|
+
<Tabs.Button id={tabOne}>{tabOne}</Tabs.Button>
|
|
17
|
+
</Tabs>
|
|
18
|
+
</>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
fireEvent,
|
|
5
|
+
screen,
|
|
6
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
7
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
8
|
+
import { Tabs, type TypeTabsProps } from "../";
|
|
9
|
+
|
|
10
|
+
const leftArrowKey = {
|
|
11
|
+
key: "ArrowLeft",
|
|
12
|
+
code: 37,
|
|
13
|
+
keyCode: 37,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const rightArrowKey = {
|
|
17
|
+
key: "ArrowRight",
|
|
18
|
+
code: 39,
|
|
19
|
+
keyCode: 39,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
interface TypeButtonData {
|
|
23
|
+
id: string;
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const mapTabButton = (button: TypeButtonData) => (
|
|
28
|
+
<Tabs.Button key={button.id} id={button.id}>
|
|
29
|
+
{button.text}
|
|
30
|
+
</Tabs.Button>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
interface TypeTestStatefulTabsProps {
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
selectedId?: string;
|
|
36
|
+
onSelect?: (id: string) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const TestStatefulTabs = ({
|
|
40
|
+
children,
|
|
41
|
+
selectedId = "1",
|
|
42
|
+
onSelect = jest.fn(),
|
|
43
|
+
}: TypeTestStatefulTabsProps) => {
|
|
44
|
+
const [state, setState] = React.useState({
|
|
45
|
+
selectedId,
|
|
46
|
+
});
|
|
47
|
+
return (
|
|
48
|
+
<Tabs
|
|
49
|
+
selectedId={state.selectedId}
|
|
50
|
+
onSelect={(id) => {
|
|
51
|
+
onSelect(id);
|
|
52
|
+
setState({
|
|
53
|
+
selectedId: id,
|
|
54
|
+
});
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</Tabs>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe("Tabs", () => {
|
|
63
|
+
it("should render selected tab", () => {
|
|
64
|
+
render(
|
|
65
|
+
<TestStatefulTabs>
|
|
66
|
+
<Tabs.Button id="1">First</Tabs.Button>
|
|
67
|
+
<Tabs.Button id="2">Second</Tabs.Button>
|
|
68
|
+
</TestStatefulTabs>
|
|
69
|
+
);
|
|
70
|
+
const firstTab = screen.getByText(/first/i);
|
|
71
|
+
const secondTab = screen.getByText(/second/i);
|
|
72
|
+
expect(firstTab).toHaveAttribute("tabindex", "0");
|
|
73
|
+
expect(secondTab).toHaveAttribute("tabindex", "-1");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should work when tab buttons aren't direct children", () => {
|
|
77
|
+
render(
|
|
78
|
+
<TestStatefulTabs>
|
|
79
|
+
<Box border="2px red solid">
|
|
80
|
+
<Tabs.Button id="1">First</Tabs.Button>
|
|
81
|
+
</Box>
|
|
82
|
+
<Box border="2px green solid">
|
|
83
|
+
<Tabs.Button id="2">Second</Tabs.Button>
|
|
84
|
+
</Box>
|
|
85
|
+
</TestStatefulTabs>
|
|
86
|
+
);
|
|
87
|
+
const tabs = screen.getByDataQaLabel({
|
|
88
|
+
tabs: "",
|
|
89
|
+
});
|
|
90
|
+
const secondTab = screen.getByText(/second/i);
|
|
91
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
92
|
+
expect(secondTab).toHaveFocus();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should handle left arrow key", () => {
|
|
96
|
+
const onSelect = jest.fn();
|
|
97
|
+
render(
|
|
98
|
+
<TestStatefulTabs onSelect={onSelect}>
|
|
99
|
+
<Tabs.Button id="1">First</Tabs.Button>
|
|
100
|
+
<Tabs.Button id="2">Second</Tabs.Button>
|
|
101
|
+
<Tabs.Button id="3">Third</Tabs.Button>
|
|
102
|
+
</TestStatefulTabs>
|
|
103
|
+
);
|
|
104
|
+
const tabs = screen.getByDataQaLabel({
|
|
105
|
+
tabs: "",
|
|
106
|
+
});
|
|
107
|
+
const secondTab = screen.getByText(/second/i);
|
|
108
|
+
const thirdTab = screen.getByText(/third/i);
|
|
109
|
+
fireEvent.keyUp(tabs, leftArrowKey);
|
|
110
|
+
expect(onSelect).toHaveBeenCalledWith("3");
|
|
111
|
+
expect(thirdTab).toHaveFocus();
|
|
112
|
+
onSelect.mockReset();
|
|
113
|
+
fireEvent.keyUp(tabs, leftArrowKey);
|
|
114
|
+
expect(onSelect).toHaveBeenCalledWith("2");
|
|
115
|
+
expect(secondTab).toHaveFocus();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should handle right arrow key", () => {
|
|
119
|
+
const onSelect = jest.fn();
|
|
120
|
+
render(
|
|
121
|
+
<TestStatefulTabs onSelect={onSelect}>
|
|
122
|
+
<Tabs.Button id="1">First</Tabs.Button>
|
|
123
|
+
<Tabs.Button id="2">Second</Tabs.Button>
|
|
124
|
+
<Tabs.Button id="3">Third</Tabs.Button>
|
|
125
|
+
</TestStatefulTabs>
|
|
126
|
+
);
|
|
127
|
+
const tabs = screen.getByDataQaLabel({
|
|
128
|
+
tabs: "",
|
|
129
|
+
});
|
|
130
|
+
const firstTab = screen.getByText(/first/i);
|
|
131
|
+
const secondTab = screen.getByText(/second/i);
|
|
132
|
+
const thirdTab = screen.getByText(/third/i);
|
|
133
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
134
|
+
expect(onSelect).toHaveBeenCalledWith("2");
|
|
135
|
+
expect(secondTab).toHaveFocus();
|
|
136
|
+
onSelect.mockReset();
|
|
137
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
138
|
+
expect(onSelect).toHaveBeenCalledWith("3");
|
|
139
|
+
expect(thirdTab).toHaveFocus();
|
|
140
|
+
onSelect.mockReset();
|
|
141
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
142
|
+
expect(onSelect).toHaveBeenCalledWith("1");
|
|
143
|
+
expect(firstTab).toHaveFocus();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should handle dynamically adding tabs", () => {
|
|
147
|
+
const onSelect = jest.fn();
|
|
148
|
+
const initialButtons = [
|
|
149
|
+
{
|
|
150
|
+
id: "1",
|
|
151
|
+
text: "First",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "2",
|
|
155
|
+
text: "Second",
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
const addButton = initialButtons.concat({
|
|
159
|
+
id: "3",
|
|
160
|
+
text: "Third",
|
|
161
|
+
});
|
|
162
|
+
const { rerender } = render(
|
|
163
|
+
<TestStatefulTabs onSelect={onSelect}>
|
|
164
|
+
{initialButtons.map(mapTabButton)}
|
|
165
|
+
</TestStatefulTabs>
|
|
166
|
+
);
|
|
167
|
+
const tabs = screen.getByDataQaLabel({
|
|
168
|
+
tabs: "",
|
|
169
|
+
});
|
|
170
|
+
const secondTab = screen.getByText(/second/i);
|
|
171
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
172
|
+
expect(onSelect).toHaveBeenCalledWith("2");
|
|
173
|
+
expect(secondTab).toHaveFocus();
|
|
174
|
+
rerender(
|
|
175
|
+
<TestStatefulTabs onSelect={onSelect}>
|
|
176
|
+
{addButton.map(mapTabButton)}
|
|
177
|
+
</TestStatefulTabs>
|
|
178
|
+
);
|
|
179
|
+
onSelect.mockReset();
|
|
180
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
181
|
+
const thirdTab = screen.getByText(/third/i);
|
|
182
|
+
expect(onSelect).toHaveBeenCalledWith("3");
|
|
183
|
+
expect(thirdTab).toHaveFocus();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should handle dynamically removing tabs", () => {
|
|
187
|
+
const onSelect = jest.fn();
|
|
188
|
+
const initialButtons = [
|
|
189
|
+
{
|
|
190
|
+
id: "1",
|
|
191
|
+
text: "First",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "2",
|
|
195
|
+
text: "Second",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "3",
|
|
199
|
+
text: "Third",
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
const removeButton = initialButtons.slice(1);
|
|
203
|
+
const { rerender } = render(
|
|
204
|
+
<TestStatefulTabs onSelect={onSelect}>
|
|
205
|
+
{initialButtons.map(mapTabButton)}
|
|
206
|
+
</TestStatefulTabs>
|
|
207
|
+
);
|
|
208
|
+
const tabs = screen.getByDataQaLabel({
|
|
209
|
+
tabs: "",
|
|
210
|
+
});
|
|
211
|
+
const secondTab = screen.getByText(/second/i);
|
|
212
|
+
const thirdTab = screen.getByText(/third/i);
|
|
213
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
214
|
+
expect(onSelect).toHaveBeenCalledWith("2");
|
|
215
|
+
expect(secondTab).toHaveFocus();
|
|
216
|
+
rerender(
|
|
217
|
+
<TestStatefulTabs onSelect={onSelect}>
|
|
218
|
+
{removeButton.map(mapTabButton)}
|
|
219
|
+
</TestStatefulTabs>
|
|
220
|
+
);
|
|
221
|
+
onSelect.mockReset();
|
|
222
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
223
|
+
expect(onSelect).toHaveBeenCalledWith("3");
|
|
224
|
+
expect(thirdTab).toHaveFocus();
|
|
225
|
+
onSelect.mockReset();
|
|
226
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
227
|
+
expect(onSelect).toHaveBeenCalledWith("2");
|
|
228
|
+
expect(secondTab).toHaveFocus();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should handle dynamically changing tab ids", () => {
|
|
232
|
+
const onSelect = jest.fn();
|
|
233
|
+
const { rerender } = render(
|
|
234
|
+
<TestStatefulTabs onSelect={onSelect}>
|
|
235
|
+
<Tabs.Button id="1">First</Tabs.Button>
|
|
236
|
+
<Tabs.Button id="2">Second</Tabs.Button>
|
|
237
|
+
<Tabs.Button id="3">Third</Tabs.Button>
|
|
238
|
+
<Tabs.Button id="4">Fourth</Tabs.Button>
|
|
239
|
+
</TestStatefulTabs>
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
rerender(
|
|
243
|
+
<TestStatefulTabs onSelect={onSelect}>
|
|
244
|
+
<Tabs.Button id="1">First</Tabs.Button>
|
|
245
|
+
<Tabs.Button id="6">Second</Tabs.Button>
|
|
246
|
+
<Tabs.Button id="5">Third</Tabs.Button>
|
|
247
|
+
<Tabs.Button id="4">Fourth</Tabs.Button>
|
|
248
|
+
</TestStatefulTabs>
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const tabs = screen.getByDataQaLabel({
|
|
252
|
+
tabs: "",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const secondTab = screen.getByText(/second/i);
|
|
256
|
+
const thirdTab = screen.getByText(/third/i);
|
|
257
|
+
const fourthTab = screen.getByText(/fourth/i);
|
|
258
|
+
|
|
259
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
260
|
+
expect(onSelect).toHaveBeenCalledWith("6");
|
|
261
|
+
expect(secondTab).toHaveFocus();
|
|
262
|
+
onSelect.mockReset();
|
|
263
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
264
|
+
expect(onSelect).toHaveBeenCalledWith("5");
|
|
265
|
+
expect(thirdTab).toHaveFocus();
|
|
266
|
+
onSelect.mockReset();
|
|
267
|
+
fireEvent.keyUp(tabs, rightArrowKey);
|
|
268
|
+
expect(onSelect).toHaveBeenCalledWith("4");
|
|
269
|
+
expect(fourthTab).toHaveFocus();
|
|
270
|
+
});
|
|
271
|
+
});
|
package/src/index.ts
ADDED
package/src/styled.d.ts
ADDED