Skip to content

Commit c37eb73

Browse files
committed
feat: tabs initial
1 parent a52828d commit c37eb73

File tree

6 files changed

+513
-8
lines changed

6 files changed

+513
-8
lines changed

package.json

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"@react-aria/separator": "3.1.6",
7676
"@react-aria/ssr": "^3.2.0",
7777
"@react-aria/switch": "^3.1.3",
78+
"@react-aria/tabs": "3.3.1",
7879
"@react-aria/textfield": "^3.5.0",
7980
"@react-aria/tooltip": "^3.1.3",
8081
"@react-aria/utils": "^3.11.0",
@@ -84,15 +85,16 @@
8485
"@react-stately/collections": "^3.3.4",
8586
"@react-stately/combobox": "3.0.1",
8687
"@react-stately/list": "^3.5.1",
88+
"@react-stately/menu": "^3.3.1",
8789
"@react-stately/numberfield": "^3.0.2",
8890
"@react-stately/overlays": "^3.1.6",
8991
"@react-stately/radio": "^3.3.2",
9092
"@react-stately/searchfield": "^3.1.3",
9193
"@react-stately/select": "^3.2.1",
94+
"@react-stately/tabs": "3.2.1",
9295
"@react-stately/toggle": "^3.2.3",
9396
"@react-stately/tooltip": "^3.0.8",
9497
"@react-stately/tree": "^3.3.1",
95-
"@react-stately/menu": "^3.3.1",
9698
"@react-stately/utils": "^3.5.0",
9799
"@react-types/button": "^3.4.1",
98100
"@react-types/checkbox": "^3.2.5",
@@ -103,8 +105,9 @@
103105
"@react-types/overlays": "^3.5.5",
104106
"@react-types/radio": "^3.1.2",
105107
"@react-types/select": "^3.6.1",
106-
"@react-types/shared": "^3.10.1",
108+
"@react-types/shared": "3.14.1",
107109
"@react-types/switch": "^3.1.2",
110+
"@react-types/tabs": "3.1.3",
108111
"@react-types/textfield": "^3.3.0",
109112
"@react-types/tooltip": "^3.1.5",
110113
"clipboard-copy": "^4.0.1",
@@ -134,9 +137,9 @@
134137
"@storybook/addon-interactions": "6.5.9",
135138
"@storybook/addon-links": "6.5.9",
136139
"@storybook/builder-webpack5": "6.5.9",
140+
"@storybook/jest": "0.0.10",
137141
"@storybook/manager-webpack5": "6.5.9",
138142
"@storybook/react": "6.5.9",
139-
"@storybook/jest": "0.0.10",
140143
"@storybook/test-runner": "0.3.0",
141144
"@storybook/testing-library": "0.0.13",
142145
"@swc/core": "1.2.148",
@@ -146,8 +149,8 @@
146149
"@testing-library/react-hooks": "^8.0.0",
147150
"@testing-library/user-event": "14.2.0",
148151
"@types/react": "^17.0.38",
149-
"@types/react-is": "17.0.3",
150152
"@types/react-dom": "^17.0.11",
153+
"@types/react-is": "17.0.3",
151154
"@types/react-test-renderer": "17.0.1",
152155
"@types/react-transition-group": "^4.4.2",
153156
"@typescript-eslint/eslint-plugin": "^5.8.1",
@@ -178,10 +181,10 @@
178181
"react-test-renderer": "^17.0.2",
179182
"rimraf": "^3.0.2",
180183
"size-limit": "^7.0.5",
181-
"styled-components": "5.3.0",
182-
"typescript": "^4.5.4",
183184
"storybook-addon-turbo-build": "1.1.0",
184-
"swc-loader": "0.2.3"
185+
"styled-components": "5.3.0",
186+
"swc-loader": "0.2.3",
187+
"typescript": "^4.5.4"
185188
},
186189
"resolutions": {
187190
"es5-ext": "0.10.53",

src/components/navigation/LegacyTabs/LegacyTabs.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useState } from 'react';
33
import { LegacyTabs } from './LegacyTabs';
44

55
export default {
6-
title: 'Navigation/Tabs',
6+
title: 'Navigation/LegacyTabs',
77
component: LegacyTabs,
88
argTypes: {},
99
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { TeamOutlined, PlusOutlined } from '@ant-design/icons';
2+
3+
import { Tabs } from './Tabs';
4+
5+
export default {
6+
title: 'Navigation/Tabs',
7+
component: Tabs,
8+
argTypes: {},
9+
};
10+
11+
const Template = () => {
12+
return (
13+
<Tabs>
14+
<Tabs.Item title="Tab 1" icon={<TeamOutlined />}>
15+
One Tab Content
16+
</Tabs.Item>
17+
<Tabs.Item title="Tab 2">Two Tab Content</Tabs.Item>
18+
<Tabs.Item textValue="Add Tab" icon={<PlusOutlined />} />
19+
</Tabs>
20+
);
21+
};
22+
23+
export const Default = Template.bind({});
24+
Default.args = {};
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import React, { ReactNode, ReactElement, Children } from 'react';
2+
import { useTabList, useTab, useTabPanel } from '@react-aria/tabs';
3+
import { TabListState, useTabListState } from '@react-stately/tabs';
4+
import {
5+
DOMRef,
6+
ItemProps,
7+
Node,
8+
Orientation,
9+
CollectionElement,
10+
} from '@react-types/shared';
11+
import { Item as BaseItem } from '@react-stately/collections';
12+
import { useHover } from '@react-aria/interactions';
13+
import { AriaTabListProps, TabListProps } from '@react-types/tabs';
14+
import { useDOMRef } from '@react-spectrum/utils';
15+
import { useButton } from '@react-aria/button';
16+
import { AriaButtonProps } from '@react-types/button';
17+
18+
import { mergeProps } from '../../../utils/react';
19+
import { useFocus } from '../../../utils/react/interactions';
20+
21+
import {
22+
StyledTabsContainer,
23+
StyledTabPanes,
24+
StyledTabItem,
25+
StyledTabBody,
26+
// ACTION_BUTTON,
27+
} from './styled';
28+
29+
type CubeTabButtonProps = {
30+
icon?: ReactElement;
31+
isDisabled?: boolean;
32+
children?: ReactNode;
33+
} & AriaButtonProps<'button'>;
34+
35+
function TabButton(props: CubeTabButtonProps) {
36+
const { isDisabled, icon } = props;
37+
const ref = React.useRef(null);
38+
const { hoverProps, isHovered } = useHover({ isDisabled });
39+
const { focusProps, isFocused } = useFocus({ isDisabled }, true);
40+
41+
const { buttonProps, isPressed } = useButton(props, ref);
42+
43+
const mods = {
44+
hovered: isHovered,
45+
focused: isFocused,
46+
pressed: isPressed,
47+
};
48+
49+
return (
50+
<StyledTabItem
51+
{...mergeProps(buttonProps, hoverProps, focusProps, props)}
52+
ref={ref}
53+
as="button"
54+
mods={mods}
55+
>
56+
{icon}
57+
</StyledTabItem>
58+
);
59+
}
60+
61+
type CubeTabProps<T> = {
62+
item: Node<T>;
63+
state: TabListState<T>;
64+
orientation?: Orientation;
65+
};
66+
67+
function Tab<T extends object>({ item, state, orientation }: CubeTabProps<T>) {
68+
const { key, rendered, props: itemProps } = item;
69+
const ref = React.useRef(null);
70+
const { tabProps, isSelected, isDisabled } = useTab({ key }, state, ref);
71+
const { hoverProps, isHovered } = useHover({ isDisabled });
72+
const { focusProps, isFocused } = useFocus({ isDisabled }, true);
73+
74+
const icon = itemProps.icon;
75+
76+
const mods = {
77+
...itemProps.mods,
78+
selected: isSelected,
79+
disabled: isDisabled,
80+
hovered: isHovered,
81+
focused: isFocused,
82+
horizontal: orientation === 'horizontal',
83+
vertical: orientation === 'vertical',
84+
};
85+
86+
return (
87+
<StyledTabItem
88+
{...mergeProps(tabProps, hoverProps, focusProps, itemProps)}
89+
ref={ref}
90+
mods={mods}
91+
>
92+
{icon}
93+
{rendered}
94+
</StyledTabItem>
95+
);
96+
}
97+
98+
type CubeTabPanelProps<T> = {
99+
state: TabListState<T>;
100+
};
101+
102+
function TabPanel<T>({ state, ...props }: CubeTabPanelProps<T>) {
103+
const ref = React.useRef<Element>(null);
104+
const { tabPanelProps } = useTabPanel(props, state, ref);
105+
return (
106+
<StyledTabBody {...tabPanelProps} ref={ref}>
107+
{state.selectedItem?.props.children}
108+
</StyledTabBody>
109+
);
110+
}
111+
112+
export type CubeTabsProps<T> = TabListProps<T> & AriaTabListProps<T>;
113+
114+
function Tabs<T extends object>(
115+
props: CubeTabsProps<T>,
116+
ref: DOMRef<HTMLDivElement>,
117+
) {
118+
const domRef = useDOMRef(ref);
119+
const children = Children.toArray(props.children).filter(
120+
(el) => (el as ReactElement)?.props?.children,
121+
);
122+
const tabButtons = Children.toArray(props.children).filter(
123+
(el) => !(el as ReactElement)?.props?.children,
124+
) as ReactElement[];
125+
const state = useTabListState({
126+
...props,
127+
children: children as CollectionElement<T>[],
128+
});
129+
const { tabListProps } = useTabList(
130+
{
131+
...props,
132+
children: children as CollectionElement<T>[],
133+
},
134+
state,
135+
domRef,
136+
);
137+
138+
return (
139+
<StyledTabsContainer>
140+
<StyledTabPanes {...tabListProps} ref={domRef}>
141+
{[...state.collection].map((item) => (
142+
<Tab
143+
key={item.key}
144+
item={item}
145+
state={state}
146+
orientation={props.orientation}
147+
/>
148+
))}
149+
{tabButtons.map((el) => (
150+
<TabButton {...el.props} key={el.key}></TabButton>
151+
))}
152+
</StyledTabPanes>
153+
<TabPanel key={state.selectedItem?.key} state={state} />
154+
</StyledTabsContainer>
155+
);
156+
}
157+
158+
// forwardRef doesn't support generic parameters, so cast the result to the correct type
159+
// https://stackoverflow.com/questions/58469229/react-with-typescript-generics-while-using-react-forwardref
160+
const _Tabs = React.forwardRef(Tabs) as <T extends object>(
161+
props: CubeTabsProps<T> & { ref?: DOMRef<HTMLDivElement> },
162+
) => ReactElement;
163+
164+
type ItemComponent = <T>(
165+
props: Omit<ItemProps<T>, 'children'> & CubeTabButtonProps,
166+
) => JSX.Element;
167+
168+
const Item = Object.assign(BaseItem, {
169+
displayName: 'Item',
170+
}) as ItemComponent;
171+
172+
type __TabsComponent = typeof _Tabs & {
173+
Item: typeof Item;
174+
};
175+
176+
const __Tabs = Object.assign(_Tabs as __TabsComponent, {
177+
Item,
178+
displayName: 'Tabs',
179+
});
180+
181+
__Tabs.displayName = 'Tabs';
182+
183+
export { __Tabs as Tabs };
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Styles, tasty } from '../../../tasty';
2+
3+
export const StyledTabsContainer = tasty({});
4+
5+
export const StyledTabPanes = tasty({
6+
styles: {
7+
display: 'flex',
8+
flow: 'row',
9+
gap: '3x',
10+
},
11+
});
12+
13+
export const StyledTabItem = tasty({
14+
styles: {
15+
preset: 'h5s',
16+
display: 'flex',
17+
placeItems: 'center stretch',
18+
placeContent: 'center',
19+
flow: 'row',
20+
gap: '1x',
21+
fill: '#clear',
22+
color: {
23+
'': '#dark',
24+
selected: '#purple-text',
25+
hovered: '#purple-text',
26+
},
27+
outline: {
28+
'': '#purple-03.0',
29+
focused: '#purple-03',
30+
},
31+
border: {
32+
'': 0,
33+
selected: 'bottom 3bw #purple-text',
34+
},
35+
cursor: {
36+
'': 'pointer',
37+
disabled: 'default',
38+
},
39+
padding: {
40+
'': '1.5x 0',
41+
},
42+
radius: {
43+
'': '1r',
44+
selected: '1r 1r 0 0',
45+
},
46+
},
47+
});
48+
49+
export const StyledTabBody = tasty({});
50+
51+
export const ACTION_BUTTON: Styles = {
52+
border: {
53+
'': '#clear',
54+
pressed: '#clear',
55+
},
56+
fill: {
57+
'': '#clear',
58+
hovered: '#clear',
59+
'pressed | selected': '#clear',
60+
disabled: '#clear',
61+
},
62+
color: {
63+
'': '#dark-02',
64+
hovered: '#dark-02',
65+
'pressed | selected': '#purple-text',
66+
disabled: '#dark-04',
67+
},
68+
padding: {
69+
'': '1.5x 0',
70+
},
71+
cursor: {
72+
'': 'pointer',
73+
disabled: 'default',
74+
},
75+
shadow: '#clear',
76+
display: 'flex',
77+
flow: 'row',
78+
justifyContent: 'start',
79+
};

0 commit comments

Comments
 (0)