Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(tabs, tab-nav): refactor tabs and tab-nav #601

Merged
merged 1 commit into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 36 additions & 43 deletions packages/components/src/components/tab-nav/TabNav.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import React, { useMemo, useState, useEffect, useRef } from 'react';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { useState, useRef, useMemo } from 'react';
import classNames from 'classnames';
import toArray from 'rc-util/lib/Children/toArray';
import { isNil } from 'lodash';
import { isNil, isUndefined, remove } from 'lodash';
import { useDeepCompareEffect } from 'react-use';
import { TabNavProps, TabNavItemProps } from './interface';
import useRefs from '../../utils/hooks/useRefs';
import composeRef from '../../utils/composeRef';
import usePrefixCls from '../../utils/hooks/use-prefix-cls';
import useControlledState from '../../utils/hooks/useControlledState';
import useDeepCompareMemo from '../../utils/hooks/useDeepCompareMemo';

const TabNav = (props: TabNavProps, ref?: React.RefObject<HTMLDivElement>) => {
const {
Expand All @@ -18,7 +23,7 @@ const TabNav = (props: TabNavProps, ref?: React.RefObject<HTMLDivElement>) => {
defaultActiveKey = '',
} = props;

const [localActiveKey, setLocalActiveKey] = useState<string | number>(activeKey || defaultActiveKey);
const [localActiveKey, setLocalActiveKey] = useControlledState<string>(activeKey, defaultActiveKey);
const [inkStyle, setInkStyle] = useState<{ left?: number; width?: number }>({});
const wrapperRefKey = useRef<symbol>(Symbol('tabNav'));
const [setRef, getRef] = useRefs<HTMLDivElement>();
Expand All @@ -31,65 +36,53 @@ const TabNav = (props: TabNavProps, ref?: React.RefObject<HTMLDivElement>) => {
[`${prefixCls}-xs`]: size === 'xs' && type === 'block',
});

const [tabNavKeys, tabNavChildren] = useMemo(() => {
const _tabNavKeys: (string | number)[] = [];
const [tabNavKeys, tabNavChildren] = useDeepCompareMemo(() => {
const _tabNavKeys: string[] = [];
const _tabNavChildren = toArray(children).map((node: React.ReactElement<TabNavItemProps>, index) => {
if (React.isValidElement(node) && node.type === TabNav.Item) {
const { className, disabled, onClick, ...rest } = node.props;
const _key = isNil(node.key) ? index : node.key;
const _key: string = isNil(node.key) ? index.toString() : node.key.toString();
_tabNavKeys.push(_key);
return (
<TabNav.Item
className={classNames(className, `${prefixCls}-item`, {
[`${prefixCls}-item-active`]: localActiveKey === _key,
[`${prefixCls}-item-disabled`]: disabled,
})}
prefixCls={prefixCls}
disabled={disabled}
key={_key}
ref={setRef(_key)}
onClick={(e) => {
if (!disabled) {
onClick?.(e);
onTabClick?.(_key);
if (isNil(activeKey)) {
setLocalActiveKey(_key);
}
if (localActiveKey !== _key) {
onChange?.(_key);
}
return React.cloneElement(node, {
className: classNames(className, `${prefixCls}-item`, {
[`${prefixCls}-item-active`]: localActiveKey === _key,
[`${prefixCls}-item-disabled`]: disabled,
}),
prefixCls,
disabled,
key: _key,
innerRef: setRef(_key),
onClick: (e: React.MouseEvent<HTMLDivElement>) => {
if (!disabled) {
onClick?.(e);
onTabClick?.(_key);
setLocalActiveKey(_key);
if (localActiveKey !== _key) {
onChange?.(_key);
}
}}
{...rest}
/>
);
}
},
...rest,
});
}
return null;
});
return [_tabNavKeys, _tabNavChildren];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [children, localActiveKey, onChange, onTabClick]);

useMemo(() => {
if (!tabNavKeys.includes(localActiveKey)) {
setLocalActiveKey(tabNavKeys[0]);
}
}, [localActiveKey, tabNavKeys]);
}, [tabNavKeys, localActiveKey, setLocalActiveKey]);

useMemo(() => {
if (!isNil(activeKey) && tabNavKeys.includes(activeKey)) {
setLocalActiveKey(activeKey);
}
}, [activeKey, tabNavKeys]);

useEffect(() => {
useDeepCompareEffect(() => {
if (!isNil(getRef(localActiveKey)?.current) && !isNil(getRef(wrapperRefKey.current)?.current)) {
const { left, width } = getRef(localActiveKey)!.current!.getBoundingClientRect();
const wrapperLeft = getRef(wrapperRefKey.current)!.current!.getBoundingClientRect().left;
setInkStyle({ left: left - wrapperLeft, width });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [localActiveKey, tabNavChildren]);
}, [localActiveKey, children]);

return (
<div className={classString} ref={setRef(wrapperRefKey.current, ref)}>
Expand All @@ -100,9 +93,9 @@ const TabNav = (props: TabNavProps, ref?: React.RefObject<HTMLDivElement>) => {
};

TabNav.Item = React.forwardRef(
({ prefixCls, children, ...rest }: TabNavItemProps, ref: React.RefObject<HTMLDivElement>) => (
({ prefixCls, children, innerRef, ...rest }: TabNavItemProps, ref: React.RefObject<HTMLDivElement>) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div ref={ref} {...rest}>
<div ref={composeRef(...remove([ref, innerRef], isUndefined))} {...rest}>
<div className={`${prefixCls}-item-btn`}>{children}</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('Testing TabNav', () => {

test('prop defaultActiveKey', () => {
const wrapper = mount(
<TabNav defaultActiveKey={1}>
<TabNav defaultActiveKey="1">
<TabNav.Item>111</TabNav.Item>
<TabNav.Item>222</TabNav.Item>
<TabNav.Item>333</TabNav.Item>
Expand All @@ -56,10 +56,10 @@ describe('Testing TabNav', () => {

test('prop activeKey', () => {
const wrapper = mount(getTabNav());
wrapper.setProps({ activeKey: 1 });
wrapper.setProps({ activeKey: '1' });
expect(wrapper.find('.gio-tabnav').childAt(0).exists('.gio-tabnav-item-active')).toBe(true);
wrapper.setProps({ activeKey: '2' });
expect(wrapper.find('.gio-tabnav').childAt(1).exists('.gio-tabnav-item-active')).toBe(true);
wrapper.setProps({ activeKey: 2 });
expect(wrapper.find('.gio-tabnav').childAt(2).exists('.gio-tabnav-item-active')).toBe(true);
wrapper.find('.gio-tabnav-item').at(1).simulate('click');
expect(wrapper.find('.gio-tabnav-item').at(1).exists('.gio-tabnav-item-active')).toBe(false);
});
Expand Down Expand Up @@ -92,7 +92,7 @@ describe('Testing TabNav', () => {

it('only render TabNav.Item', () => {
const wrapper = mount(
<TabNav defaultActiveKey={1}>
<TabNav defaultActiveKey="1">
<TabNav.Item>111</TabNav.Item>
<TabNav.Item>222</TabNav.Item>
<TabNav.Item>333</TabNav.Item>
Expand Down
10 changes: 6 additions & 4 deletions packages/components/src/components/tab-nav/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ export interface TabNavProps {
/**
* 标签被点击时的回调
*/
onTabClick?: (_key: string | number) => void;
onTabClick?: (_key: string) => void;
/**
* 标签激活改变时的回调
*/
onChange?: (_key: string | number) => void;
onChange?: (_key: string) => void;
/**
* 初始化选中面板的 key
*/
defaultActiveKey?: string | number;
defaultActiveKey?: string;
/**
* 开启受控模式,当前激活 tab 面板的 key
*/
activeKey?: string | number;
activeKey?: string;
}

export interface TabNavItemProps {
Expand All @@ -41,4 +41,6 @@ export interface TabNavItemProps {
className?: string;
disabled?: boolean;
onClick?: React.MouseEventHandler<HTMLDivElement>;
// 内部使用
innerRef?: React.RefObject<HTMLDivElement>;
}
46 changes: 19 additions & 27 deletions packages/components/src/components/tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/* eslint-disable no-underscore-dangle */
import React, { useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import classNames from 'classnames';
import toArray from 'rc-util/lib/Children/toArray';
import { isNil } from 'lodash';
import usePrefixCls from '../../utils/hooks/use-prefix-cls';
import TabNav from '../tab-nav';
import TabPane from './TabPane';
import { TabProps, TabPaneProps } from './interface';
import useControlledState from '../../utils/hooks/useControlledState';
import useDeepCompareMemo from '../../utils/hooks/useDeepCompareMemo';

const Tabs = (props: TabProps, ref: React.Ref<HTMLDivElement>) => {
const {
Expand All @@ -21,7 +22,7 @@ const Tabs = (props: TabProps, ref: React.Ref<HTMLDivElement>) => {
onTabClick,
onChange,
} = props;
const [localActiveKey, setLocalActiveKey] = useState<string | number>(activeKey || defaultActiveKey);
const [localActiveKey, setLocalActiveKey] = useControlledState<string>(activeKey, defaultActiveKey);
const prefixCls = usePrefixCls('tabs', customizePrefixCls);
const classString = classNames(prefixCls, className, {
[`${prefixCls}-${type}`]: true,
Expand All @@ -30,7 +31,7 @@ const Tabs = (props: TabProps, ref: React.Ref<HTMLDivElement>) => {
[`${prefixCls}-lg`]: size === 'large',
});

const [tabNav, tabPane] = useMemo(() => {
const [tabNav, tabPane] = useDeepCompareMemo(() => {
const _tabItem: JSX.Element[] = [];
const _tabPane = toArray(children).map((node: React.ReactElement<TabPaneProps>) => {
if (React.isValidElement(node) && node.type === TabPane) {
Expand All @@ -40,35 +41,28 @@ const Tabs = (props: TabProps, ref: React.Ref<HTMLDivElement>) => {
{tab}
</TabNav.Item>
);
return (
<TabPane
prefixCls={prefixCls}
className={classNames(paneClassName, {
[`${prefixCls}-tabpane-active`]: localActiveKey === node.key,
})}
key={node.key as string | number | undefined}
style={localActiveKey === node.key ? paneStyle : { ...paneStyle, display: 'none' }}
{...restProps}
/>
);
return React.cloneElement(node, {
prefixCls,
className: classNames(paneClassName, {
[`${prefixCls}-tabpane-active`]: localActiveKey === node.key,
}),
style: localActiveKey === node.key ? paneStyle : { ...paneStyle, display: 'none' },
...restProps,
});
}
return null;
});
return [_tabItem, _tabPane];
}, [children, localActiveKey, prefixCls]);

const tabNavKeys = useMemo(() => tabNav.map((item) => item.key!), [tabNav]);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const tabNavKeys = useDeepCompareMemo(() => tabNav.map((item) => item.key!.toString()), [tabNav]);

useMemo(() => {
if (!tabNavKeys.includes(localActiveKey)) {
setLocalActiveKey(tabNavKeys[0]);
}
}, [localActiveKey, tabNavKeys]);

useMemo(() => {
if (!isNil(activeKey) && tabNavKeys.includes(activeKey)) {
setLocalActiveKey(activeKey);
}
}, [activeKey, tabNavKeys]);
}, [localActiveKey, tabNavKeys, setLocalActiveKey]);

return (
<div className={classString} ref={ref} style={style}>
Expand All @@ -77,10 +71,8 @@ const Tabs = (props: TabProps, ref: React.Ref<HTMLDivElement>) => {
type={type}
activeKey={localActiveKey}
onTabClick={onTabClick}
onChange={(_key: string | number) => {
if (isNil(activeKey)) {
setLocalActiveKey(_key);
}
onChange={(_key: string) => {
setLocalActiveKey(_key);
onChange?.(_key);
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ describe('Testing Tabs', () => {
test('prop activeKey', () => {
const wrapper = mount(getTabs());
wrapper.setProps({ activeKey: '1' });
expect(wrapper.find('.gio-tabnav').childAt(1).exists('.gio-tabnav-item-active')).toBe(true);
expect(wrapper.find('.gio-tabnav').childAt(0).exists('.gio-tabnav-item-active')).toBe(true);
wrapper.setProps({ activeKey: '2' });
expect(wrapper.find('.gio-tabnav').childAt(2).exists('.gio-tabnav-item-active')).toBe(true);
expect(wrapper.find('.gio-tabnav').childAt(1).exists('.gio-tabnav-item-active')).toBe(true);
wrapper.find('.gio-tabnav-item').at(1).simulate('click');
expect(wrapper.find('.gio-tabnav').childAt(0).exists('.gio-tabnav-item-active')).toBe(false);
});
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/components/tabs/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export interface TabProps {
/**
`tab` 被点击的回调
*/
onTabClick?: (key: string | number) => void;
onTabClick?: (key: string) => void;
/**
切换面板的回调
*/
onChange?: (key: string | number) => void;
onChange?: (key: string) => void;
/**
开启受控模式,当前激活 `tab` 面板的 `key`
*/
Expand Down
14 changes: 14 additions & 0 deletions packages/components/src/utils/hooks/useDeepCompareMemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useRef, useMemo, DependencyList } from 'react';
import { isEqual } from 'lodash';

const useDeepCompareMemo = <T>(factory: () => T, deps: DependencyList | undefined): T => {
const ref = useRef<DependencyList | undefined>(undefined);
if (!ref.current || !isEqual(deps, ref.current)) {
ref.current = deps;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const value = useMemo<T>(factory, ref.current);
return value;
};

export default useDeepCompareMemo;
8 changes: 4 additions & 4 deletions packages/website/src/components/basic/tabnav/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ group:
| children | 子元素 | TabNav.Item[] | - |
| type | 标签导航按钮样式 | 'block' \| 'line' | 'block' |
| size | 标签导航尺寸 | 'large' \| 'middle' \| 'small' \| 'xs' | 'large' |
| onChange | 标签激活改变时的回调 | (\_key: string \| number) => void | - |
| onTabClick | 标签被点击时的回调 | (\_key: string \| number) => void | - |
| activeKey | 开启受控模式,当前激活 tab 面板的 key | string \| number | - |
| defaultActiveKey | 初始化选中面板的 key | string \| number | - |
| onChange | 标签激活改变时的回调 | (\_key: string) => void | - |
| onTabClick | 标签被点击时的回调 | (\_key: string) => void | - |
| activeKey | 开启受控模式,当前激活 tab 面板的 key | string | - |
| defaultActiveKey | 初始化选中面板的 key | string | - |

### TabNav.Item

Expand Down
37 changes: 20 additions & 17 deletions packages/website/src/components/basic/tabs/demo/block.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import React from 'react';
import React, { useState } from 'react';
import { Tabs, TabPane } from '@gio-design/components';
import '@gio-design/components/es/components/tabs/style/index.css';
import './index.less';

export default () => (
<Tabs>
<TabPane tab="我的" key="1">
1111
</TabPane>
<TabPane tab="全部" key="2">
2222
</TabPane>
<TabPane tab="共享" key="3">
3333
</TabPane>
<TabPane disabled tab="预置" key="4">
4444
</TabPane>
</Tabs>
);
export default () => {
const [key, setKey] = useState('1');
return (
<Tabs activeKey={key} onChange={setKey}>
<TabPane tab="我的" key="1">
1111
</TabPane>
<TabPane tab="全部" key="2">
2222
</TabPane>
<TabPane tab="共享" key="3">
3333
</TabPane>
<TabPane disabled tab="预置" key="4">
4444
</TabPane>
</Tabs>
);
};
4 changes: 2 additions & 2 deletions packages/website/src/components/basic/tabs/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ group:
| **onChange** | 切换面板的回调 | Function(activeKey) {} | |
| **onTabClick** | tab 被点击的回调 | Function | |
| **children** | 标签面板组件 | TabPane[] | |
| **activeKey** | 开启受控模式,当前激活 tab 面板的 key | string \| number | - |
| **defaultActiveKey** | 初始化选中面板的 key | string \| number | - |
| **activeKey** | 开启受控模式,当前激活 tab 面板的 key | string | - |
| **defaultActiveKey** | 初始化选中面板的 key | string | - |

### TabPane

Expand Down