feat(tabs): sync the label and set value to required (#334)

* feat(tabs): sync the label and set value to required

* test(tabs): add testcase for label sync

* docs(tabs): update value to required
This commit is contained in:
witt
2020-07-15 18:02:07 +08:00
committed by unix
parent c931ae3ca9
commit 98f701ec35
6 changed files with 40 additions and 55 deletions

View File

@@ -87,36 +87,26 @@ describe('Tabs', () => {
expect(active.text()).toContain('label2')
})
it('should warning when label duplicated', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
mount(
<Tabs>
<Tabs.Item label="label1" value="1">
test-1
</Tabs.Item>
<Tabs.Item label="label2" value="1">
test-2
</Tabs.Item>
</Tabs>,
)
expect(errorSpy).toHaveBeenCalled()
errorSpy.mockRestore()
})
it('should re-render when items updated', () => {
const Mock = ({ label = 'label1' }) => {
return (
<Tabs value="1">
<Tabs.Item label={label} value="1">
test-1
</Tabs.Item>
<Tabs.Item label="label-fixed" value="2">
test-label-fixed
</Tabs.Item>
</Tabs>
)
}
const wrapper = mount(<Mock />)
let active = wrapper.find('header').find('.active')
expect(active.text()).toContain('label1')
it('should use label as key when value is missing', async () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
const wrapper = mount(
<Tabs>
<Tabs.Item label="label1">test-1</Tabs.Item>
<Tabs.Item label="label2">test-2</Tabs.Item>
</Tabs>,
)
expect(errorSpy).not.toHaveBeenCalled()
wrapper.setProps({ value: 'label2' })
await updateWrapper(wrapper, 350)
const active = wrapper.find('header').find('.active')
wrapper.setProps({ label: 'label2' })
active = wrapper.find('header').find('.active')
expect(active.text()).toContain('label2')
errorSpy.mockRestore()
expect(() => wrapper.unmount()).not.toThrow()
})
})

View File

@@ -8,7 +8,6 @@ export interface TabsLabelItem {
export interface TabsConfig {
register?: (item: TabsLabelItem) => void
unregister?: (item: TabsLabelItem) => void
currentValue?: string
inGroup: boolean
}

View File

@@ -4,7 +4,7 @@ import { useTabsContext } from './tabs-context'
interface Props {
label: string | React.ReactNode
value?: string
value: string
disabled?: boolean
}
@@ -16,20 +16,16 @@ export type TabsItemProps = Props & typeof defaultProps
const TabsItem: React.FC<React.PropsWithChildren<TabsItemProps>> = ({
children,
value: userCustomValue,
value,
label,
disabled,
}) => {
const value = useMemo(() => userCustomValue || `${label}`, [userCustomValue, label])
const { register, unregister, currentValue } = useTabsContext()
const { register, currentValue } = useTabsContext()
const isActive = useMemo(() => currentValue === value, [currentValue, value])
useEffect(() => {
register && register({ value, label, disabled })
return () => {
unregister && unregister({ value, label, disabled })
}
}, [])
}, [value, label, disabled])
/* eslint-disable react/jsx-no-useless-fragment */
return isActive ? <>{children}</> : null

View File

@@ -2,8 +2,6 @@ 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'
import useWarning from '../utils/use-warning'
interface Props {
initialValue?: string
@@ -32,24 +30,26 @@ const Tabs: React.FC<React.PropsWithChildren<TabsProps>> = ({
}) => {
const theme = useTheme()
const [selfValue, setSelfValue] = useState<string | undefined>(userCustomInitialValue)
const [tabs, setTabs, tabsRef] = useCurrentState<Array<TabsLabelItem>>([])
const [tabs, setTabs] = useState<Array<TabsLabelItem>>([])
const register = (next: TabsLabelItem) => {
const hasItem = tabsRef.current.find(item => item.value === next.value)
if (hasItem) {
useWarning('The "value" of each "Tabs.Item" must be unique.', 'Tabs')
}
setTabs([...tabsRef.current, next])
}
const unregister = (next: TabsLabelItem) => {
const nextTabs = tabsRef.current.filter(item => item.value !== next.value)
setTabs([...nextTabs])
setTabs(last => {
const hasItem = last.find(item => item.value === next.value)
if (!hasItem) return [...last, next]
return last.map(item => {
if (item.value !== next.value) return item
return {
...item,
label: next.label,
disabled: next.disabled,
}
})
})
}
const initialValue = useMemo<TabsConfig>(
() => ({
register,
unregister,
currentValue: selfValue,
inGroup: true,
}),
@@ -71,13 +71,13 @@ const Tabs: React.FC<React.PropsWithChildren<TabsProps>> = ({
<TabsContext.Provider value={initialValue}>
<div className={`tabs ${className}`} {...props}>
<header className={hideDivider ? 'hide-divider' : ''}>
{tabs.map((item, index) => (
{tabs.map(item => (
<div
className={`tab ${selfValue === item.value ? 'active' : ''} ${
item.disabled ? 'disabled' : ''
}`}
role="button"
key={item.value + index}
key={item.value}
onClick={() => clickHandler(item)}>
{item.label}
</div>

View File

@@ -134,7 +134,7 @@ Display tab content.
| Attribute | Description | Type | Accepted values | Default |
| ------------------- | ------------------- | --------- | --------------- | ------- |
| **label**(required) | display tab's label | `string` | - | - |
| **value** | unique ident value | `string` | - | - |
| **value**(required) | unique ident value | `string` | - | - |
| **disabled** | disable current tab | `boolean` | - | `false` |
<Attributes.Title>useTabs</Attributes.Title>

View File

@@ -131,7 +131,7 @@ export const meta = {
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
| ----------------- | -------------- | --------- | ------ | ------- |
| **label**(必须的) | 选项卡标签文字 | `string` | - | - |
| **value** | 唯一鉴别值 | `string` | - | - |
| **value**(必须的) | 唯一鉴别值 | `string` | - | - |
| **disabled** | 禁用当前选项卡 | `boolean` | - | `false` |
<Attributes.Title>useTabs</Attributes.Title>