mirror of
https://github.com/zhigang1992/react.git
synced 2026-02-11 17:21:06 +08:00
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:
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,6 @@ export interface TabsLabelItem {
|
||||
|
||||
export interface TabsConfig {
|
||||
register?: (item: TabsLabelItem) => void
|
||||
unregister?: (item: TabsLabelItem) => void
|
||||
currentValue?: string
|
||||
inGroup: boolean
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -131,7 +131,7 @@ export const meta = {
|
||||
| 属性 | 描述 | 类型 | 推荐值 | 默认 |
|
||||
| ----------------- | -------------- | --------- | ------ | ------- |
|
||||
| **label**(必须的) | 选项卡标签文字 | `string` | - | - |
|
||||
| **value** | 唯一鉴别值 | `string` | - | - |
|
||||
| **value**(必须的) | 唯一鉴别值 | `string` | - | - |
|
||||
| **disabled** | 禁用当前选项卡 | `boolean` | - | `false` |
|
||||
|
||||
<Attributes.Title>useTabs</Attributes.Title>
|
||||
|
||||
Reference in New Issue
Block a user