feat(tabs): add component

This commit is contained in:
unix
2020-03-23 01:21:54 +08:00
parent adfe15190c
commit cb9a9310c2
7 changed files with 367 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ export { default as ZEITUIProvider } from './providers/zeit-ui-provider'
export { default as useToasts } from './toast'
export { default as useInput } from './input/use-input'
export { default as useModal } from './modal/use-modal'
export { default as useTabs } from './tabs/use-tabs'
export { default as CSSBaseline } from './styles/css-baseline'
export { default as Avatar } from './avatar'
export { default as Text } from './text'
@@ -32,3 +33,4 @@ export { default as Capacity } from './capacity'
export { default as Input } from './input'
export { default as Radio } from './radio'
export { default as Select } from './select'
export { default as Tabs } from './tabs'

7
components/tabs/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import Tabs from './tabs'
import TabsItem from './tabs-item'
Tabs.Item = TabsItem
Tabs.Tab = TabsItem
export default Tabs

View File

@@ -0,0 +1,21 @@
import React from 'react'
export interface TabsLabelItem {
value: string
label: string | React.ReactNode
disabled: boolean
}
export interface TabsConfig {
register?: (item: TabsLabelItem) => void
currentValue?: string
inGroup: boolean
}
const defaultContext = {
inGroup: false,
}
export const TabsContext = React.createContext<TabsConfig>(defaultContext)
export const useTabsContext = (): TabsConfig => React.useContext<TabsConfig>(TabsContext)

View File

@@ -0,0 +1,31 @@
import React, { useEffect, useMemo } from 'react'
import withDefaults from '../utils/with-defaults'
import { useTabsContext } from './tabs-context'
interface Props {
label: string | React.ReactNode
value?: string
disabled?: boolean
}
const defaultProps = {
disabled: false,
}
export type TabsItemProps = Props & typeof defaultProps
const TabsItem: React.FC<React.PropsWithChildren<TabsItemProps>> = ({
children, value: userCustomValue, label, disabled,
}) => {
const value = useMemo(() => userCustomValue || `${label}`, [userCustomValue, label])
const { register, currentValue } = useTabsContext()
const isActive = useMemo(() => currentValue === value, [currentValue, value])
useEffect(() => {
register && register({ value, label, disabled })
}, [])
return isActive ? <>{children}</> : null
}
export default withDefaults(TabsItem, defaultProps)

150
components/tabs/tabs.tsx Normal file
View File

@@ -0,0 +1,150 @@
import React, { useEffect, useMemo, useState } from 'react'
import TabsItem from './tabs-item'
import useTheme from '../styles/use-theme'
import { TabsLabelItem, TabsConfig, TabsContext } from './tabs-context'
import useCurrentState from '../utils/use-current-state'
interface Props {
initialValue?: string
value?: string
onChange?: (val: string) => void
className?: string
}
const defaultProps = {
className: '',
}
export type TabsProps = Props & typeof defaultProps & React.HTMLAttributes<any>
const Tabs: React.FC<React.PropsWithChildren<TabsProps>> = ({
initialValue: userCustomInitialValue, value, children, onChange,
className, ...props
}) => {
const theme = useTheme()
const [selfValue, setSelfValue] = useState<string | undefined>(userCustomInitialValue)
const [tabs, setTabs, tabsRef] = useCurrentState<Array<TabsLabelItem>>([])
const register = (next: TabsLabelItem) => {
const hasItem = tabsRef.current.find(item => item.value === next.value)
if (hasItem) {
console.error('[Tabs]: The "value" of each "Tabs.Item" must be unique.')
}
setTabs([...tabsRef.current, next])
}
const initialValue = useMemo<TabsConfig>(() => ({
register,
currentValue: selfValue,
inGroup: true,
}), [selfValue])
useEffect(() => {
if (value === undefined) return
setSelfValue(value)
}, [value])
const clickHandler = (item: TabsLabelItem) => {
if (item.disabled) return
setSelfValue(item.value)
onChange && onChange(item.value)
}
return (
<TabsContext.Provider value={initialValue}>
<div className={`tabs ${className}`} {...props}>
<header>
{tabs.map((item, index) => (
<div className={`tab ${selfValue === item.value ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`}
key={item.value + index} onClick={() => clickHandler(item)}>
{item.label}
</div>
))}
</header>
<div className="content">
{children}
</div>
<style jsx>{`
.tabs {
width: initial;
}
header {
display: flex;
flex-wrap: wrap;
align-items: center;
border-bottom: 1px solid ${theme.palette.border};
}
.content {
padding-top: .625rem;
}
.tab {
padding: ${theme.layout.gapQuarter} calc(0.65 * ${theme.layout.gapQuarter});
cursor: pointer;
outline: 0;
transition: all 200ms ease;
text-transform: capitalize;
font-size: 1rem;
margin: 0 calc(.8 * ${theme.layout.gapHalf});
color: ${theme.palette.accents_6};
user-select: none;
display: flex;
align-items: center;
line-height: 1.25rem;
position: relative;
}
.tab:after {
position: absolute;
content: '';
bottom: -1px;
left: 0;
right: 0;
width: 100%;
height: 2px;
transform: scaleX(.85);
background-color: transparent;
transition: all 200ms ease;
}
.tab.active:after {
background-color: ${theme.palette.foreground};
transform: scaleX(1);
}
.tab :global(svg) {
max-width: .9em;
max-height: .9em;
margin-right: 5px;
}
.tab:first-of-type {
margin-left: 0;
}
.tab.active {
color: ${theme.palette.foreground};
}
.tab.disabled {
color: ${theme.palette.accents_3};
cursor: not-allowed;
}
`}</style>
</div>
</TabsContext.Provider>
)
}
Tabs.defaultProps = defaultProps
type TabsComponent<P = {}> = React.FC<P> & {
Item: typeof TabsItem
Tab: typeof TabsItem
}
type ComponentProps = Partial<typeof defaultProps> & Omit<Props, keyof typeof defaultProps>
export default Tabs as TabsComponent<ComponentProps>

View File

@@ -0,0 +1,28 @@
import { Dispatch, MutableRefObject, SetStateAction } from 'react'
import useCurrentState from '../utils/use-current-state'
const useTabs = (initialValue: string): {
state: string
setState: Dispatch<SetStateAction<string>>
currentRef: MutableRefObject<string>
bindings: {
value: string
onChange: (val: string) => void
}
} => {
const [state, setState, currentRef] = useCurrentState<string>(initialValue)
return {
state,
setState,
currentRef,
bindings: {
value: state,
onChange: (val: string) => {
setState(val)
},
},
}
}
export default useTabs

View File

@@ -0,0 +1,128 @@
import { Layout, Playground, Attributes } from 'lib/components'
import { Tabs, Spacer, Link, Text, Button, Code, useTabs } from 'components'
import { useState } from 'react'
import GithubIcon from 'lib/components/icons/github'
import ZeitIcon from 'lib/components/icons/zeit'
import ReactIcon from 'lib/components/icons/react'
export const meta = {
title: 'tabs',
description: 'tabs',
}
## Tabs
Display tab content.
<Playground
scope={{ Tabs }}
code={`
<Tabs initialValue="1">
<Tabs.Item label="evil rabbit" value="1">The Evil Rabbit Jumped over the Fence.</Tabs.Item>
<Tabs.Item label="jumped" value="2">The Fence Jumped over The Evil Rabbit.</Tabs.Item>
</Tabs>
`} />
<Playground
title="Disabled"
scope={{ Tabs }}
code={`
<Tabs initialValue="1">
<Tabs.Item label="evil rabbit" value="1">The Evil Rabbit Jumped over the Fence.</Tabs.Item>
<Tabs.Item label="jumped" value="2" disabled />
</Tabs>
`} />
<Playground
title="With Icon"
scope={{ Tabs, ReactIcon, ZeitIcon, Link, Text }}
code={`
<Tabs initialValue="1">
<Tabs.Item label={<><ZeitIcon /> ZEIT UI</>} value="1">
<Text>Hello, this is the unofficial ZEIT UI Library.</Text>
<Link href="https://github.com/zeit-ui/react" color rel="nofollow" target="_blank">Click here to visit GitHub repo.</Link>
</Tabs.Item>
<Tabs.Item label={<><ReactIcon/> React Components </>} value="2">
<Text>The Components of React looks very cool.</Text>
</Tabs.Item>
</Tabs>
`} />
<Playground
title="Imperatively"
scope={{ Tabs, Button, Spacer, Code, Text, useState }}
code={`
() => {
const [value, setValue] = useState('1')
const switchHandler = () => setValue('2')
const changeHandler = val => setValue(val)
return (
<>
<Button size="small" onClick={switchHandler}><Text>Select <Code>Jumped</Code></Text></Button>
<Spacer y={.5} />
<Tabs value={value} onChange={changeHandler}>
<Tabs.Item label="evil rabbit" value="1">The Evil Rabbit Jumped over the Fence.</Tabs.Item>
<Tabs.Item label="jumped" value="2">The Fence Jumped over The Evil Rabbit.</Tabs.Item>
</Tabs>
</>
)
}
`} />
<Playground
title={<>with <Code>useTabs</Code></>}
scope={{ Tabs, Button, Spacer, Code, Text, useTabs }}
code={`
() => {
const { setState, bindings } = useTabs('1')
return (
<>
<Button size="small" onClick={() => setState('2')}>
<Text>Select <Code>Jumped</Code></Text>
</Button>
<Spacer y={.5} />
<Tabs {...bindings}>
<Tabs.Item label="evil rabbit" value="1">The Evil Rabbit Jumped over the Fence.</Tabs.Item>
<Tabs.Item label="jumped" value="2">The Fence Jumped over The Evil Rabbit.</Tabs.Item>
</Tabs>
</>
)
}
`} />
<Attributes edit="/pages/docs/components/tabs.mdx">
<Attributes.Title>Tabs.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| **initialValue** | initial value | `string` | - | - |
| **value** | current selected value | `string` | - | - |
| **onChange** | change event | `(val: string) => void` | - | - |
| ... | native props | `HTMLAttributes` | `'alt', 'id', 'className', ...` | - |
<Attributes.Title alias="Tabs.Tab">Tabs.Item.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| **label**(required) | display tab's label | `string` | - | - |
| **value** | unique ident value | `string` | - | - |
| **disabled** | disable current tab | `boolean` | - | `false` |
<Attributes.Title>useTabs</Attributes.Title>
```ts
type useTabs = (initialValue: string) => {
state: string
setState: Dispatch<SetStateAction<string>>
currentRef: MutableRefObject<string>
bindings: {
value: string
onChange: (val: string) => void
}
}
```
</Attributes>
export default ({ children }) => <Layout meta={meta}>{children}</Layout>