mirror of
https://github.com/zhigang1992/react.git
synced 2026-05-10 13:05:04 +08:00
feat(tabs): add component
This commit is contained in:
@@ -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
7
components/tabs/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Tabs from './tabs'
|
||||
import TabsItem from './tabs-item'
|
||||
|
||||
Tabs.Item = TabsItem
|
||||
Tabs.Tab = TabsItem
|
||||
|
||||
export default Tabs
|
||||
21
components/tabs/tabs-context.ts
Normal file
21
components/tabs/tabs-context.ts
Normal 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)
|
||||
31
components/tabs/tabs-item.tsx
Normal file
31
components/tabs/tabs-item.tsx
Normal 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
150
components/tabs/tabs.tsx
Normal 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>
|
||||
28
components/tabs/use-tabs.ts
Normal file
28
components/tabs/use-tabs.ts
Normal 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
|
||||
128
pages/docs/components/tabs.mdx
Normal file
128
pages/docs/components/tabs.mdx
Normal 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>
|
||||
Reference in New Issue
Block a user