From cb9a9310c2fcc2c0eb806b4e4ab67db65f1dd72a Mon Sep 17 00:00:00 2001 From: unix Date: Mon, 23 Mar 2020 01:21:54 +0800 Subject: [PATCH] feat(tabs): add component --- components/index.ts | 2 + components/tabs/index.ts | 7 ++ components/tabs/tabs-context.ts | 21 +++++ components/tabs/tabs-item.tsx | 31 +++++++ components/tabs/tabs.tsx | 150 ++++++++++++++++++++++++++++++++ components/tabs/use-tabs.ts | 28 ++++++ pages/docs/components/tabs.mdx | 128 +++++++++++++++++++++++++++ 7 files changed, 367 insertions(+) create mode 100644 components/tabs/index.ts create mode 100644 components/tabs/tabs-context.ts create mode 100644 components/tabs/tabs-item.tsx create mode 100644 components/tabs/tabs.tsx create mode 100644 components/tabs/use-tabs.ts create mode 100644 pages/docs/components/tabs.mdx diff --git a/components/index.ts b/components/index.ts index 6719b1b..82c68bf 100644 --- a/components/index.ts +++ b/components/index.ts @@ -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' diff --git a/components/tabs/index.ts b/components/tabs/index.ts new file mode 100644 index 0000000..238319d --- /dev/null +++ b/components/tabs/index.ts @@ -0,0 +1,7 @@ +import Tabs from './tabs' +import TabsItem from './tabs-item' + +Tabs.Item = TabsItem +Tabs.Tab = TabsItem + +export default Tabs diff --git a/components/tabs/tabs-context.ts b/components/tabs/tabs-context.ts new file mode 100644 index 0000000..19a9fd8 --- /dev/null +++ b/components/tabs/tabs-context.ts @@ -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(defaultContext) + +export const useTabsContext = (): TabsConfig => React.useContext(TabsContext) diff --git a/components/tabs/tabs-item.tsx b/components/tabs/tabs-item.tsx new file mode 100644 index 0000000..d98d24c --- /dev/null +++ b/components/tabs/tabs-item.tsx @@ -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> = ({ + 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) diff --git a/components/tabs/tabs.tsx b/components/tabs/tabs.tsx new file mode 100644 index 0000000..b176108 --- /dev/null +++ b/components/tabs/tabs.tsx @@ -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 + +const Tabs: React.FC> = ({ + initialValue: userCustomInitialValue, value, children, onChange, + className, ...props +}) => { + const theme = useTheme() + const [selfValue, setSelfValue] = useState(userCustomInitialValue) + const [tabs, setTabs, tabsRef] = useCurrentState>([]) + + 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(() => ({ + 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 ( + +
+
+ {tabs.map((item, index) => ( +
clickHandler(item)}> + {item.label} +
+ ))} +
+
+ {children} +
+ +
+
+ ) +} + +Tabs.defaultProps = defaultProps + +type TabsComponent

= React.FC

& { + Item: typeof TabsItem + Tab: typeof TabsItem +} + +type ComponentProps = Partial & Omit + +export default Tabs as TabsComponent diff --git a/components/tabs/use-tabs.ts b/components/tabs/use-tabs.ts new file mode 100644 index 0000000..db36302 --- /dev/null +++ b/components/tabs/use-tabs.ts @@ -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> + currentRef: MutableRefObject + bindings: { + value: string + onChange: (val: string) => void + } +} => { + const [state, setState, currentRef] = useCurrentState(initialValue) + + return { + state, + setState, + currentRef, + bindings: { + value: state, + onChange: (val: string) => { + setState(val) + }, + }, + } +} + +export default useTabs diff --git a/pages/docs/components/tabs.mdx b/pages/docs/components/tabs.mdx new file mode 100644 index 0000000..8e99051 --- /dev/null +++ b/pages/docs/components/tabs.mdx @@ -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. + + + The Evil Rabbit Jumped over the Fence. + The Fence Jumped over The Evil Rabbit. + +`} /> + + + The Evil Rabbit Jumped over the Fence. + + +`} /> + + + ZEIT UI} value="1"> + Hello, this is the unofficial ZEIT UI Library. + Click here to visit GitHub repo. + + React Components } value="2"> + The Components of React looks very cool. + + +`} /> + + { + const [value, setValue] = useState('1') + const switchHandler = () => setValue('2') + const changeHandler = val => setValue(val) + return ( + <> + + + + The Evil Rabbit Jumped over the Fence. + The Fence Jumped over The Evil Rabbit. + + + ) +} +`} /> + +with useTabs} + scope={{ Tabs, Button, Spacer, Code, Text, useTabs }} + code={` +() => { + const { setState, bindings } = useTabs('1') + return ( + <> + + + + The Evil Rabbit Jumped over the Fence. + The Fence Jumped over The Evil Rabbit. + + + ) +} +`} /> + + + +Tabs.Props + +| 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', ...` | - | + +Tabs.Item.Props + +| Attribute | Description | Type | Accepted values | Default +| ---------- | ---------- | ---- | -------------- | ------ | +| **label**(required) | display tab's label | `string` | - | - | +| **value** | unique ident value | `string` | - | - | +| **disabled** | disable current tab | `boolean` | - | `false` | + +useTabs + +```ts +type useTabs = (initialValue: string) => { + state: string + setState: Dispatch> + currentRef: MutableRefObject + bindings: { + value: string + onChange: (val: string) => void + } +} +``` + + + +export default ({ children }) => {children}