Skip to content

Commit

Permalink
feat(tabs): add overflow w. arrows feature
Browse files Browse the repository at this point in the history
  • Loading branch information
soykje committed May 5, 2023
1 parent b70b1d8 commit 5fbfaa8
Show file tree
Hide file tree
Showing 12 changed files with 340 additions and 151 deletions.
6 changes: 6 additions & 0 deletions packages/components/tabs/src/Tabs.doc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,9 @@ Use `icon` prop on item to add it to it. Icons can be used to improve labels cla
Use `orientation` prop to set `horizontal|vertical` orientation.

<Canvas of={stories.Orientation} />

## Overflow

For `horizontal` orientation only, navigation arrows are automatically displayed to help user browse tabs on content overflow. Use then `loop` prop on list to allow navigation loop from last tab to first, and vice versa.

<Canvas of={stories.Overflow} />
256 changes: 152 additions & 104 deletions packages/components/tabs/src/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Meta, StoryFn } from '@storybook/react'

import { Tabs } from '.'
import type { TabsRootProps } from './TabsRoot'
import type { TabsTriggerProps } from './TabsTrigger'

const meta: Meta<typeof Tabs> = {
title: 'Components/Tabs',
Expand All @@ -11,161 +12,208 @@ const meta: Meta<typeof Tabs> = {

export default meta

const tabs = [
interface TabItem {
title?: string
value: string
disabled?: boolean
icon?: TabsTriggerProps['icon']
content: string
}

const defaultTabs = [
{
title: 'Inbox',
value: 'tab1',
icon: MailFill,
disabled: false,
content: 'Your inbox is empty',
},
{
title: 'Today',
value: 'tab2',
icon: ConversationFill,
disabled: false,
content: 'Make some coffee',
},

{
title: 'Upcoming',
value: 'tab3',
icon: HolidayFill,
disabled: false,
content: 'Order more coffee',
},
]

const invokeTabs = (customProps: TabsRootProps = {}) => {
const invokeTabs = (customProps: TabsRootProps = {}, tabs: TabItem[] = defaultTabs) => {
return (
<Tabs defaultValue="tab1" {...customProps}>
<Tabs.List>
{tabs.map(({ title, value }) => (
<Tabs.Trigger key={value} value={value} label={title} />
{tabs.map(({ title, value, icon, disabled }) => (
<Tabs.Trigger key={value} value={value} label={title} icon={icon} disabled={disabled} />
))}
</Tabs.List>

{tabs.map(({ value }) => (
{tabs.map(({ content, value }) => (
<Tabs.Content key={value} value={value}>
<span>
{
{
tab1: 'Your inbox is empty',
tab2: 'Make some coffee',
tab3: 'Order more coffee',
}[value]
}
</span>
<p>{content}</p>
</Tabs.Content>
))}
</Tabs>
)
}

export const Default: StoryFn = _args => invokeTabs({ size: 'xs' })
export const Default: StoryFn = _args => (
<div className="gap-lg flex flex-row">
<div className="shrink basis-auto overflow-auto">{invokeTabs({ size: 'xs' })}</div>
</div>
)

export const Intent: StoryFn = _args => (
<div className="gap-lg flex flex-row">
{invokeTabs()}
{invokeTabs({ intent: 'secondary' })}
<div className="shrink basis-auto overflow-auto">{invokeTabs()}</div>
<div className="shrink basis-auto overflow-auto">{invokeTabs({ intent: 'secondary' })}</div>
</div>
)

export const Size: StoryFn = _args => (
<div className="gap-lg flex flex-row">
{invokeTabs({ size: 'xs' })}
<div className="shrink basis-auto overflow-auto">{invokeTabs({ size: 'xs' })}</div>

{invokeTabs({ size: 'sm' })}
<div className="shrink basis-auto overflow-auto">{invokeTabs({ size: 'sm' })}</div>

{invokeTabs({ size: 'md' })}
<div className="shrink basis-auto overflow-auto">{invokeTabs({ size: 'md' })}</div>
</div>
)

export const State: StoryFn = _args => (
<div className="gap-lg flex flex-row">
<Tabs defaultValue="tab2">
<Tabs.List>
{tabs.map(({ title, value }) => (
<Tabs.Trigger key={value} value={value} label={title} disabled={value === 'tab1'} />
))}
</Tabs.List>

{tabs.map(({ value }) => (
<Tabs.Content key={value} value={value}>
<span>
{
{
tab1: 'Your inbox is empty',
tab2: 'Make some coffee',
tab3: 'Order more coffee',
}[value]
}
</span>
</Tabs.Content>
))}
</Tabs>
<div className="shrink basis-auto overflow-auto">
{invokeTabs({ defaultValue: 'tab2' }, [
{
title: 'Inbox',
value: 'tab1',
content: 'Your inbox is empty',
disabled: true,
},
{
title: 'Today',
value: 'tab2',
content: 'Make some coffee',
disabled: false,
},
{
title: 'Upcoming',
value: 'tab3',
content: 'Order more coffee',
disabled: false,
},
])}
</div>
</div>
)

export const Iconed: StoryFn = _args => (
<div className="gap-lg flex flex-row">
<Tabs defaultValue="tab3">
<Tabs.List>
{tabs.map(({ title, value, icon }) => {
const Icon = icon

return (
<Tabs.Trigger
key={value}
value={value}
label={title}
icon={<Icon />}
disabled={value === 'tab1'}
/>
)
})}
</Tabs.List>

{tabs.map(({ value }) => (
<Tabs.Content key={value} value={value}>
<span>
{
{
tab1: 'Your inbox is empty',
tab2: 'Make some coffee',
tab3: 'Order more coffee',
}[value]
}
</span>
</Tabs.Content>
))}
</Tabs>

<Tabs defaultValue="tab3">
<Tabs.List>
{tabs.map(({ value, icon }) => {
const Icon = icon

return (
<Tabs.Trigger key={value} value={value} icon={<Icon />} disabled={value === 'tab1'} />
)
})}
</Tabs.List>

{tabs.map(({ value }) => (
<Tabs.Content key={value} value={value}>
<span>
{
{
tab1: 'Your inbox is empty',
tab2: 'Make some coffee',
tab3: 'Order more coffee',
}[value]
}
</span>
</Tabs.Content>
))}
</Tabs>
<div className="shrink basis-auto overflow-auto">
{invokeTabs({ defaultValue: 'tab2' }, [
{
title: 'Inbox',
value: 'tab1',
icon: <MailFill />,
content: 'Your inbox is empty',
disabled: true,
},
{
title: 'Today',
value: 'tab2',
icon: <ConversationFill />,
content: 'Make some coffee',
disabled: false,
},
{
title: 'Upcoming',
value: 'tab3',
icon: <HolidayFill />,
content: 'Order more coffee',
disabled: false,
},
])}
</div>

<div className="shrink basis-auto overflow-auto">
{invokeTabs({ defaultValue: 'tab2' }, [
{
value: 'tab1',
icon: <MailFill />,
content: 'Your inbox is empty',
disabled: true,
},
{
value: 'tab2',
icon: <ConversationFill />,
content: 'Make some coffee',
disabled: false,
},
{
value: 'tab3',
icon: <HolidayFill />,
content: 'Order more coffee',
disabled: false,
},
])}
</div>
</div>
)

export const Orientation: StoryFn = _args => (
<div className="gap-lg flex flex-row">
{invokeTabs({ orientation: 'horizontal' })}
{invokeTabs({ orientation: 'vertical' })}
<div className="shrink basis-auto overflow-auto">
{invokeTabs({ orientation: 'horizontal' })}
</div>

<div className="shrink basis-auto overflow-auto">{invokeTabs({ orientation: 'vertical' })}</div>
</div>
)

export const Overflow: StoryFn = _args => {
const overflowTabs = [
...defaultTabs,
{
title: 'Pending',
value: 'tab4',
disabled: false,
content: 'Wait for your coffee',
},
{
title: 'Blocked',
value: 'tab5',
disabled: false,
content: 'Something went wrong',
},
{
title: 'Sandbox',
value: 'tab6',
disabled: false,
content: 'Imagine your coffee',
},
]

return (
<div className="gap-lg flex flex-row">
<div className="shrink basis-auto overflow-auto">{invokeTabs({}, overflowTabs)}</div>

<div className="shrink basis-auto overflow-auto">
<Tabs defaultValue="tab1">
<Tabs.List loop={false}>
{overflowTabs.map(({ title, value, disabled }) => (
<Tabs.Trigger key={value} value={value} label={title} disabled={disabled} />
))}
</Tabs.List>

{overflowTabs.map(({ content, value }) => (
<Tabs.Content key={value} value={value}>
<p>{content}</p>
</Tabs.Content>
))}
</Tabs>
</div>
</div>
)
}
6 changes: 6 additions & 0 deletions packages/components/tabs/src/TabsContent.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { cva } from 'class-variance-authority'

export const contentStyles = cva([
'spark-orientation-horizontal:mt-lg',
'spark-orientation-vertical:ml-lg',
])
4 changes: 2 additions & 2 deletions packages/components/tabs/src/TabsContent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as RadixTabs from '@radix-ui/react-tabs'
import { forwardRef, type PropsWithChildren } from 'react'

import { tabsContentStyles } from './Tabs.styles'
import { contentStyles } from './TabsContent.styles'

export type TabsContentProps = PropsWithChildren<RadixTabs.TabsContentProps>

Expand All @@ -19,7 +19,7 @@ export const TabsContent = forwardRef<HTMLDivElement, TabsContentProps>(
ref
) => {
return (
<RadixTabs.Content ref={ref} className={tabsContentStyles()} asChild={asChild} {...rest}>
<RadixTabs.Content ref={ref} className={contentStyles()} asChild={asChild} {...rest}>
{children}
</RadixTabs.Content>
)
Expand Down
13 changes: 9 additions & 4 deletions packages/components/tabs/src/TabsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { createContext, useContext } from 'react'
import type { TabsProps } from '@radix-ui/react-tabs'
import { createContext, type Dispatch, useContext } from 'react'

import { TabsTriggerVariantsProps } from './Tabs.styles'
import { TabsTriggerVariantsProps } from './TabsTrigger.styles'

export type TabsContextInterface = TabsTriggerVariantsProps
export type TabsContextInterface = TabsTriggerVariantsProps &
Pick<TabsProps, 'orientation'> & {
selectedTab?: string
setSelectedTab: Dispatch<string>
}

export const TabsContext = createContext<TabsContextInterface>({})
export const TabsContext = createContext<TabsContextInterface>({} as TabsContextInterface)

export const useTabsContext = () => {
const context = useContext(TabsContext)
Expand Down
17 changes: 17 additions & 0 deletions packages/components/tabs/src/TabsList.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable tailwindcss/no-custom-classname */
import { cva } from 'class-variance-authority'

export const wrapperStyles = cva(['relative flex'])

export const listStyles = cva([
'flex',
'spark-orientation-horizontal:flex-row',
'spark-orientation-vertical:flex-col',
'no-scrollbar overflow-x-auto overflow-y-hidden',
])

export const navigationArrowStyles = cva([
'border-outline border-b-sm',
'outline-none',
'focus-visible:ring-outline-high focus-visible:bg-surface-hovered ring-inset focus-visible:border-none focus-visible:ring-2',
])
Loading

0 comments on commit 5fbfaa8

Please sign in to comment.