Skip to content

CUK-69 feat(tabs): Tabs Component v1 #205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@react-aria/separator": "3.1.6",
"@react-aria/ssr": "^3.2.0",
"@react-aria/switch": "^3.1.3",
"@react-aria/tabs": "3.3.1",
"@react-aria/textfield": "^3.5.0",
"@react-aria/tooltip": "^3.1.3",
"@react-aria/utils": "^3.11.0",
Expand All @@ -84,15 +85,16 @@
"@react-stately/collections": "^3.3.4",
"@react-stately/combobox": "3.0.1",
"@react-stately/list": "^3.5.1",
"@react-stately/menu": "^3.3.1",
"@react-stately/numberfield": "^3.0.2",
"@react-stately/overlays": "^3.1.6",
"@react-stately/radio": "^3.3.2",
"@react-stately/searchfield": "^3.1.3",
"@react-stately/select": "^3.2.1",
"@react-stately/tabs": "3.2.1",
"@react-stately/toggle": "^3.2.3",
"@react-stately/tooltip": "^3.0.8",
"@react-stately/tree": "^3.3.1",
"@react-stately/menu": "^3.3.1",
"@react-stately/utils": "^3.5.0",
"@react-types/button": "^3.4.1",
"@react-types/checkbox": "^3.2.5",
Expand All @@ -103,8 +105,9 @@
"@react-types/overlays": "^3.5.5",
"@react-types/radio": "^3.1.2",
"@react-types/select": "^3.6.1",
"@react-types/shared": "^3.10.1",
"@react-types/shared": "3.14.1",
"@react-types/switch": "^3.1.2",
"@react-types/tabs": "3.1.3",
"@react-types/textfield": "^3.3.0",
"@react-types/tooltip": "^3.1.5",
"clipboard-copy": "^4.0.1",
Expand Down Expand Up @@ -134,9 +137,9 @@
"@storybook/addon-interactions": "6.5.9",
"@storybook/addon-links": "6.5.9",
"@storybook/builder-webpack5": "6.5.9",
"@storybook/jest": "0.0.10",
"@storybook/manager-webpack5": "6.5.9",
"@storybook/react": "6.5.9",
"@storybook/jest": "0.0.10",
"@storybook/test-runner": "0.3.0",
"@storybook/testing-library": "0.0.13",
"@swc/core": "1.2.148",
Expand All @@ -146,8 +149,8 @@
"@testing-library/react-hooks": "^8.0.0",
"@testing-library/user-event": "14.2.0",
"@types/react": "^17.0.38",
"@types/react-is": "17.0.3",
"@types/react-dom": "^17.0.11",
"@types/react-is": "17.0.3",
"@types/react-test-renderer": "17.0.1",
"@types/react-transition-group": "^4.4.2",
"@typescript-eslint/eslint-plugin": "^5.8.1",
Expand Down Expand Up @@ -178,10 +181,10 @@
"react-test-renderer": "^17.0.2",
"rimraf": "^3.0.2",
"size-limit": "^7.0.5",
"styled-components": "5.3.0",
"typescript": "^4.5.4",
"storybook-addon-turbo-build": "1.1.0",
"swc-loader": "0.2.3"
"styled-components": "5.3.0",
"swc-loader": "0.2.3",
"typescript": "^4.5.4"
},
"resolutions": {
"es5-ext": "0.10.53",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useState } from 'react';
import { LegacyTabs } from './LegacyTabs';

export default {
title: 'Navigation/Tabs',
title: 'Navigation/LegacyTabs',
component: LegacyTabs,
argTypes: {},
};
Expand Down
24 changes: 24 additions & 0 deletions src/components/navigation/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { TeamOutlined, PlusOutlined } from '@ant-design/icons';

import { Tabs } from './Tabs';

export default {
title: 'Navigation/Tabs',
component: Tabs,
argTypes: {},
};

const Template = () => {
return (
<Tabs>
<Tabs.Item title="Tab 1" icon={<TeamOutlined />}>
One Tab Content
</Tabs.Item>
<Tabs.Item title="Tab 2">Two Tab Content</Tabs.Item>
<Tabs.Item textValue="Add Tab" icon={<PlusOutlined />} />
</Tabs>
);
};

export const Default = Template.bind({});
Default.args = {};
183 changes: 183 additions & 0 deletions src/components/navigation/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import React, { ReactNode, ReactElement, Children } from 'react';
import { useTabList, useTab, useTabPanel } from '@react-aria/tabs';
import { TabListState, useTabListState } from '@react-stately/tabs';
import {
DOMRef,
ItemProps,
Node,
Orientation,
CollectionElement,
} from '@react-types/shared';
import { Item as BaseItem } from '@react-stately/collections';
import { useHover } from '@react-aria/interactions';
import { AriaTabListProps, TabListProps } from '@react-types/tabs';
import { useDOMRef } from '@react-spectrum/utils';
import { useButton } from '@react-aria/button';
import { AriaButtonProps } from '@react-types/button';

import { mergeProps } from '../../../utils/react';
import { useFocus } from '../../../utils/react/interactions';

import {
StyledTabsContainer,
StyledTabPanes,
StyledTabItem,
StyledTabBody,
// ACTION_BUTTON,
} from './styled';

type CubeTabButtonProps = {
icon?: ReactElement;
isDisabled?: boolean;
children?: ReactNode;
} & AriaButtonProps<'button'>;

function TabButton(props: CubeTabButtonProps) {
const { isDisabled, icon } = props;
const ref = React.useRef(null);
const { hoverProps, isHovered } = useHover({ isDisabled });
const { focusProps, isFocused } = useFocus({ isDisabled }, true);

const { buttonProps, isPressed } = useButton(props, ref);

const mods = {
hovered: isHovered,
focused: isFocused,
pressed: isPressed,
};

return (
<StyledTabItem
{...mergeProps(buttonProps, hoverProps, focusProps, props)}
ref={ref}
as="button"
mods={mods}
>
{icon}
</StyledTabItem>
);
}

type CubeTabProps<T> = {
item: Node<T>;
state: TabListState<T>;
orientation?: Orientation;
};

function Tab<T extends object>({ item, state, orientation }: CubeTabProps<T>) {
const { key, rendered, props: itemProps } = item;
const ref = React.useRef(null);
const { tabProps, isSelected, isDisabled } = useTab({ key }, state, ref);
const { hoverProps, isHovered } = useHover({ isDisabled });
const { focusProps, isFocused } = useFocus({ isDisabled }, true);

const icon = itemProps.icon;

const mods = {
...itemProps.mods,
selected: isSelected,
disabled: isDisabled,
hovered: isHovered,
focused: isFocused,
horizontal: orientation === 'horizontal',
vertical: orientation === 'vertical',
};

return (
<StyledTabItem
{...mergeProps(tabProps, hoverProps, focusProps, itemProps)}
ref={ref}
mods={mods}
>
{icon}
{rendered}
</StyledTabItem>
);
}

type CubeTabPanelProps<T> = {
state: TabListState<T>;
};

function TabPanel<T>({ state, ...props }: CubeTabPanelProps<T>) {
const ref = React.useRef<Element>(null);
const { tabPanelProps } = useTabPanel(props, state, ref);
return (
<StyledTabBody {...tabPanelProps} ref={ref}>
{state.selectedItem?.props.children}
</StyledTabBody>
);
}

export type CubeTabsProps<T> = TabListProps<T> & AriaTabListProps<T>;

function Tabs<T extends object>(
props: CubeTabsProps<T>,
ref: DOMRef<HTMLDivElement>,
) {
const domRef = useDOMRef(ref);
const children = Children.toArray(props.children).filter(
(el) => (el as ReactElement)?.props?.children,
);
const tabButtons = Children.toArray(props.children).filter(
(el) => !(el as ReactElement)?.props?.children,
) as ReactElement[];
const state = useTabListState({
...props,
children: children as CollectionElement<T>[],
});
const { tabListProps } = useTabList(
{
...props,
children: children as CollectionElement<T>[],
},
state,
domRef,
);

return (
<StyledTabsContainer>
<StyledTabPanes {...tabListProps} ref={domRef}>
{[...state.collection].map((item) => (
<Tab
key={item.key}
item={item}
state={state}
orientation={props.orientation}
/>
))}
{tabButtons.map((el) => (
<TabButton {...el.props} key={el.key}></TabButton>
))}
</StyledTabPanes>
<TabPanel key={state.selectedItem?.key} state={state} />
</StyledTabsContainer>
);
}

// forwardRef doesn't support generic parameters, so cast the result to the correct type
// https://stackoverflow.com/questions/58469229/react-with-typescript-generics-while-using-react-forwardref
const _Tabs = React.forwardRef(Tabs) as <T extends object>(
props: CubeTabsProps<T> & { ref?: DOMRef<HTMLDivElement> },
) => ReactElement;

type ItemComponent = <T>(
props: Omit<ItemProps<T>, 'children'> & CubeTabButtonProps,
) => JSX.Element;

const Item = Object.assign(BaseItem, {
displayName: 'Item',
}) as ItemComponent;

type __TabsComponent = typeof _Tabs & {
Item: typeof Item;
};

const __Tabs = Object.assign(_Tabs as __TabsComponent, {
Item,
displayName: 'Tabs',
});

__Tabs.displayName = 'Tabs';

export { __Tabs as Tabs };
79 changes: 79 additions & 0 deletions src/components/navigation/Tabs/styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Styles, tasty } from '../../../tasty';

export const StyledTabsContainer = tasty({});

export const StyledTabPanes = tasty({
styles: {
display: 'flex',
flow: 'row',
gap: '3x',
},
});

export const StyledTabItem = tasty({
styles: {
preset: 'h5s',
display: 'flex',
placeItems: 'center stretch',
placeContent: 'center',
flow: 'row',
gap: '1x',
fill: '#clear',
color: {
'': '#dark',
selected: '#purple-text',
hovered: '#purple-text',
},
outline: {
'': '#purple-03.0',
focused: '#purple-03',
},
border: {
'': 0,
selected: 'bottom 3bw #purple-text',
},
cursor: {
'': 'pointer',
disabled: 'default',
},
padding: {
'': '1.5x 0',
},
radius: {
'': '1r',
selected: '1r 1r 0 0',
},
},
});

export const StyledTabBody = tasty({});

export const ACTION_BUTTON: Styles = {
border: {
'': '#clear',
pressed: '#clear',
},
fill: {
'': '#clear',
hovered: '#clear',
'pressed | selected': '#clear',
disabled: '#clear',
},
color: {
'': '#dark-02',
hovered: '#dark-02',
'pressed | selected': '#purple-text',
disabled: '#dark-04',
},
padding: {
'': '1.5x 0',
},
cursor: {
'': 'pointer',
disabled: 'default',
},
shadow: '#clear',
display: 'flex',
flow: 'row',
justifyContent: 'start',
};
Loading