From 2dfbf6fe0b5e8b3750c5fdf4e3584bdea000fa18 Mon Sep 17 00:00:00 2001 From: yoshi6jp Date: Wed, 29 Nov 2023 05:19:00 +0900 Subject: [PATCH] fix(Tabs): support v4 (#433) * fix(Tabs): support v4 * remove comment --- .storybook/StoryLayout.tsx | 84 +++++++++++---------- src/Tabs/RadioTab.tsx | 58 +++++++++++++++ src/Tabs/Tab.tsx | 79 +++++--------------- src/Tabs/Tabs.stories.tsx | 146 +++++++++++++++++++++++++++---------- src/Tabs/Tabs.test.tsx | 45 ++++++++---- src/Tabs/Tabs.tsx | 88 +++++++--------------- src/Tabs/index.tsx | 2 +- 7 files changed, 288 insertions(+), 214 deletions(-) create mode 100644 src/Tabs/RadioTab.tsx diff --git a/.storybook/StoryLayout.tsx b/.storybook/StoryLayout.tsx index 35ab12b6..159c13a9 100644 --- a/.storybook/StoryLayout.tsx +++ b/.storybook/StoryLayout.tsx @@ -1,6 +1,6 @@ import React, { ReactNode, useEffect, useMemo, useState } from 'react' -import Highlight, { defaultProps } from "prism-react-renderer" -import theme from "prism-react-renderer/themes/vsDark" +import Highlight, { defaultProps } from 'prism-react-renderer' +import theme from 'prism-react-renderer/themes/vsDark' import { useGlobalTheme } from './theming' @@ -17,28 +17,33 @@ type Props = { } const StoryLayout = ({ children, title, description, source }: Props) => { - const [tab, setTab] = useState('preview') const globalTheme = useGlobalTheme() useEffect(() => { - document.getElementsByTagName('html')[0].setAttribute('data-theme', globalTheme) + document + .getElementsByTagName('html')[0] + .setAttribute('data-theme', globalTheme) }, [globalTheme]) - const Code = () => useMemo(() => ( - - {({ tokens, getLineProps, getTokenProps }) => ( -
-          {tokens.map((line, i) => (
-            
- {line.map((token, key) => ( - + const Code = () => + useMemo( + () => ( + + {({ tokens, getLineProps, getTokenProps }) => ( +
+              {tokens.map((line, i) => (
+                
+ {line.map((token, key) => ( + + ))} +
))} -
- ))} -
- )} -
- ), [theme, source]) + + )} + + ), + [theme, source] + ) return ( @@ -53,7 +58,7 @@ const StoryLayout = ({ children, title, description, source }: Props) => {

{description}

{/* Mobile view */} -
+
{children} @@ -61,23 +66,16 @@ const StoryLayout = ({ children, title, description, source }: Props) => {
{/* Desktop view */} -
- setTab(tab === 'fullWidthClick' ? 'html' : tab)} - > - - Preview - - - HTML - - - -
- {tab === 'preview' ? ( +
+ +
- )} -
+
+
diff --git a/src/Tabs/RadioTab.tsx b/src/Tabs/RadioTab.tsx new file mode 100644 index 00000000..06a2b299 --- /dev/null +++ b/src/Tabs/RadioTab.tsx @@ -0,0 +1,58 @@ +import React, { forwardRef, ReactNode } from 'react' +import clsx from 'clsx' +import { twMerge } from 'tailwind-merge' + +export type RadioTabProps = Omit< + React.InputHTMLAttributes, + 'type' +> & { + active?: boolean + disabled?: boolean + label: string + name: string + contentClassName?: string +} + +const RadioTab = forwardRef( + ( + { + children, + className, + active, + label, + disabled, + name, + contentClassName, + ...props + }, + ref + ): JSX.Element => { + const classes = twMerge( + 'tab', + className, + clsx({ + 'tab-active': active, + 'tab-disabled': disabled, + }) + ) + const contentClasses = twMerge('tab-content', contentClassName) + + return ( + <> + +
{children}
+ + ) + } +) + +export default RadioTab diff --git a/src/Tabs/Tab.tsx b/src/Tabs/Tab.tsx index 6a572764..6646f864 100644 --- a/src/Tabs/Tab.tsx +++ b/src/Tabs/Tab.tsx @@ -1,67 +1,28 @@ -import React from 'react' +import React, { forwardRef } from 'react' import clsx from 'clsx' import { twMerge } from 'tailwind-merge' -import { ComponentSize } from '../types' - -export type TabProps = Omit< - React.AnchorHTMLAttributes, - 'onClick' -> & { - value: T - activeValue?: T - onClick?: (value: T) => void - size?: ComponentSize - variant?: 'boxed' | 'bordered' | 'lifted' +export type TabProps = React.AnchorHTMLAttributes & { + active?: boolean disabled?: boolean } -const TabInner = ( - { - children, - value, - activeValue, - onClick, - size, - variant, - disabled, - className, - style, - ...props - }: TabProps, - ref?: React.ForwardedRef -): JSX.Element => { - const classes = twMerge( - 'tab', - className, - clsx({ - 'tab-active': value != null && value === activeValue, - 'tab-disabled': disabled, - 'tab-lg': size === 'lg', - 'tab-md': size === 'md', - 'tab-sm': size === 'sm', - 'tab-xs': size === 'xs', - 'tab-bordered': variant === 'bordered', - 'tab-lifted': variant === 'lifted', - }) - ) - - return ( - onClick && onClick(value)} - > - {children} - - ) -} - -// Make forwardRef work with generic component -const Tab = React.forwardRef(TabInner) as ( - props: TabProps & { ref?: React.ForwardedRef } -) => ReturnType +const Tab = forwardRef( + ({ children, className, active, disabled, ...props }, ref): JSX.Element => { + const classes = twMerge( + 'tab', + className, + clsx({ + 'tab-active': active, + 'tab-disabled': disabled, + }) + ) + return ( + + {children} + + ) + } +) export default Tab diff --git a/src/Tabs/Tabs.stories.tsx b/src/Tabs/Tabs.stories.tsx index 137acf60..9522a830 100644 --- a/src/Tabs/Tabs.stories.tsx +++ b/src/Tabs/Tabs.stories.tsx @@ -3,66 +3,132 @@ import { StoryFn as Story, Meta } from '@storybook/react' import Tabs, { TabsProps } from '.' -const { Tab } = Tabs +const { Tab, RadioTab } = Tabs -export default { +const meta: Meta = { title: 'Navigation/Tabs', component: Tabs, - parameters: { - controls: { exclude: ['ref'] }, - }, - args: { - value: 1, - }, -} as Meta +} -const Template: Story> = (args) => { - const [tabValue, setTabValue] = React.useState(0) +export default meta + +const Template: Story = (args) => { return ( - - Tab 1 - Tab 2 - Tab 3 + + Tab 1 + Tab 2 + Tab 3 ) } -export const Default = Template.bind({}) -Default.args = {} - -export const Bordered = Template.bind({}) -Bordered.args = { variant: 'bordered' } - -export const Lifted = Template.bind({}) -Lifted.args = { variant: 'lifted' } +export const Default: Story = Template.bind({}) +export const Bordered: Story = Template.bind({}) +Bordered.args = { + variant: 'bordered', +} +export const Lifted: Story = Template.bind({}) +Lifted.args = { + variant: 'lifted', +} -export const Boxed = Template.bind({}) -Boxed.args = { boxed: true } +export const Boxed: Story = Template.bind({}) +Boxed.args = { + variant: 'boxed', +} -export const Sizes: Story> = ({ size, ...args }) => { +export const Sizes: Story = ({ size, ...args }) => { return ( -
+
+ {/*xs*/} - Tiny - Tiny - Tiny + Tiny + Tiny + Tiny + {/*sm*/} - Small - Small - Small + Small + Small + Small + {/*md*/} - Normal - Normal - Normal + Normal + Normal + Normal + {/*lg*/} - Large - Large - Large + Large + Large + Large
) } -Sizes.args = { variant: 'lifted' } +Sizes.argTypes = { + size: { + control: false, + }, +} +Sizes.args = { + variant: 'lifted', +} + +export const RadioTabBordered: Story = (args) => { + return ( + + + Tab content 1 + + + Tab content 2 + + + Tab content 3 + + + ) +} +RadioTabBordered.args = { + variant: 'bordered', +} + +export const RadioTabLifted: Story = (args) => { + return ( + + + Tab content 1 + + + Tab content 2 + + + Tab content 3 + + + ) +} +RadioTabLifted.args = { + className: 'w-full my-10 lg:mx-10', + variant: 'lifted', +} diff --git a/src/Tabs/Tabs.test.tsx b/src/Tabs/Tabs.test.tsx index 78f6df69..09337c0c 100644 --- a/src/Tabs/Tabs.test.tsx +++ b/src/Tabs/Tabs.test.tsx @@ -6,14 +6,14 @@ import Tabs from './' describe('Tabs', () => { const tabLabel1 = 'one' const tabLabel2 = 'two' - const tabValue1 = 1 - const tabValue2 = 2 + const tabContent1 = 'content one' + const tabContent2 = 'content two' it('Should render tabs', () => { render( - {tabLabel1} - {tabLabel2} + {tabLabel1} + {tabLabel2} ) expect(screen.getByRole('tablist')).toBeInTheDocument() @@ -22,25 +22,40 @@ describe('Tabs', () => { it('Should render tab labels', () => { render( - {tabLabel1} - {tabLabel2} + {tabLabel1} + {tabLabel2} ) expect(screen.getByText(tabLabel1)).toBeInTheDocument() expect(screen.getByText(tabLabel2)).toBeInTheDocument() }) - it('Should call handler on tab click', async () => { - const mockHandler = jest.fn() + it('Should render radio tabs', () => { render( - - {tabLabel1} - {tabLabel2} + + + {tabContent1} + + + {tabContent2} + + + ) + expect(screen.getByRole('tablist')).toBeInTheDocument() + }) + + it('Should render tab content', () => { + render( + + + {tabContent1} + + + {tabContent2} + ) - const tab = screen.getByText(tabLabel1) - await userEvent.click(tab) - expect(mockHandler).toHaveBeenCalled() - expect(mockHandler).toBeCalledWith(tabValue1) + expect(screen.getByText(tabContent1)).toBeInTheDocument() + expect(screen.getByText(tabContent2)).toBeInTheDocument() }) }) diff --git a/src/Tabs/Tabs.tsx b/src/Tabs/Tabs.tsx index 5aa5f44b..1bcaffb5 100644 --- a/src/Tabs/Tabs.tsx +++ b/src/Tabs/Tabs.tsx @@ -1,72 +1,40 @@ -import React, { cloneElement, ReactElement } from 'react' +import React, { cloneElement, forwardRef, ReactElement } from 'react' import clsx from 'clsx' import { twMerge } from 'tailwind-merge' import { IComponentBaseProps, ComponentSize } from '../types' -import Tab, { TabProps } from './Tab' +import Tab from './Tab' +import RadioTab from './RadioTab' -export type TabsProps = Omit< - React.HTMLAttributes, - 'onChange' -> & +export type TabsProps = React.HTMLAttributes & IComponentBaseProps & { - children: ReactElement>[] - value?: T - onChange?: (value: T) => void - variant?: 'bordered' | 'lifted' + variant?: 'bordered' | 'lifted' | 'boxed' size?: ComponentSize - boxed?: boolean } -const TabsInner = ( - { - children, - value, - onChange, - variant, - size, - boxed, - dataTheme, - className, - ...props - }: TabsProps, - ref?: React.ForwardedRef -): JSX.Element => { - const classes = twMerge( - 'tabs', - className, - clsx({ - 'tabs-boxed': boxed, - }) - ) +const Tabs = forwardRef( + ({ children, className, variant, size }, ref): JSX.Element => { + const classes = twMerge( + 'tabs', + className, + clsx({ + 'tabs-boxed': variant === 'boxed', + 'tabs-bordered': variant === 'bordered', + 'tabs-lifted': variant === 'lifted', + 'tabs-lg': size === 'lg', + 'tabs-md': size === 'md', + 'tabs-sm': size === 'sm', + 'tabs-xs': size === 'xs', + }) + ) - return ( -
} - role="tablist" - {...props} - data-theme={dataTheme} - className={classes} - > - {children.map((child, index) => { - return cloneElement(child, { - key: child.props.value, - variant, - size, - activeValue: value, - onClick: (value: T) => { - onChange && onChange(value) - }, - }) - })} -
- ) -} - -// Make forwardRef work with generic component -const Tabs = React.forwardRef(TabsInner) as ( - props: TabsProps & { ref?: React.ForwardedRef } -) => ReturnType + return ( +
+ {children} +
+ ) + } +) -export default Object.assign(Tabs, { Tab }) +export default Object.assign(Tabs, { Tab, RadioTab }) diff --git a/src/Tabs/index.tsx b/src/Tabs/index.tsx index 5c5d81a8..3e0577be 100644 --- a/src/Tabs/index.tsx +++ b/src/Tabs/index.tsx @@ -1,3 +1,3 @@ import Tabs, { TabsProps as TTabsProps } from './Tabs' -export type TabsProps = TTabsProps +export type TabsProps = TTabsProps export default Tabs