refactor(breadcrumb): rewrite with hook (#24512)

* refactor(breadcrumb): rewrite with hook

* Update BreadcrumbItem.tsx
This commit is contained in:
Tom Xu 2020-05-28 15:22:00 +08:00 committed by GitHub
parent 81bde547cd
commit 56c42e496b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 132 additions and 165 deletions

View File

@ -1,11 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import toArray from 'rc-util/lib/Children/toArray'; import toArray from 'rc-util/lib/Children/toArray';
import omit from 'omit.js';
import BreadcrumbItem from './BreadcrumbItem'; import BreadcrumbItem from './BreadcrumbItem';
import BreadcrumbSeparator from './BreadcrumbSeparator'; import BreadcrumbSeparator from './BreadcrumbSeparator';
import Menu from '../menu'; import Menu from '../menu';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider';
import devWarning from '../_util/devWarning'; import devWarning from '../_util/devWarning';
import { Omit } from '../_util/type'; import { Omit } from '../_util/type';
import { cloneElement } from '../_util/reactNode'; import { cloneElement } from '../_util/reactNode';
@ -49,16 +48,7 @@ function defaultItemRender(route: Route, params: any, routes: Route[], paths: st
return isLastItem ? <span>{name}</span> : <a href={`#/${paths.join('/')}`}>{name}</a>; return isLastItem ? <span>{name}</span> : <a href={`#/${paths.join('/')}`}>{name}</a>;
} }
export default class Breadcrumb extends React.Component<BreadcrumbProps, any> { const getPath = (path: string, params: any) => {
static Item: typeof BreadcrumbItem;
static Separator: typeof BreadcrumbSeparator;
static defaultProps = {
separator: '/',
};
getPath = (path: string, params: any) => {
path = (path || '').replace(/^\//, ''); path = (path || '').replace(/^\//, '');
Object.keys(params).forEach(key => { Object.keys(params).forEach(key => {
path = path.replace(`:${key}`, params[key]); path = path.replace(`:${key}`, params[key]);
@ -66,24 +56,40 @@ export default class Breadcrumb extends React.Component<BreadcrumbProps, any> {
return path; return path;
}; };
addChildPath = (paths: string[], childPath: string = '', params: any) => { const addChildPath = (paths: string[], childPath: string = '', params: any) => {
const originalPaths = [...paths]; const originalPaths = [...paths];
const path = this.getPath(childPath, params); const path = getPath(childPath, params);
if (path) { if (path) {
originalPaths.push(path); originalPaths.push(path);
} }
return originalPaths; return originalPaths;
}; };
genForRoutes = ({ interface BreadcrumbInterface extends React.FC<BreadcrumbProps> {
routes = [], Item: typeof BreadcrumbItem;
params = {}, Separator: typeof BreadcrumbSeparator;
separator, }
const Breadcrumb: BreadcrumbInterface = ({
prefixCls: customizePrefixCls,
separator = '/',
style,
className,
routes,
children,
itemRender = defaultItemRender, itemRender = defaultItemRender,
}: BreadcrumbProps) => { params = {},
...restProps
}) => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
let crumbs;
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
if (routes && routes.length > 0) {
// generated by route
const paths: string[] = []; const paths: string[] = [];
return routes.map(route => { crumbs = routes.map(route => {
const path = this.getPath(route.path, params); const path = getPath(route.path, params);
if (path) { if (path) {
paths.push(path); paths.push(path);
@ -95,7 +101,7 @@ export default class Breadcrumb extends React.Component<BreadcrumbProps, any> {
<Menu> <Menu>
{route.children.map(child => ( {route.children.map(child => (
<Menu.Item key={child.path || child.breadcrumbName}> <Menu.Item key={child.path || child.breadcrumbName}>
{itemRender(child, params, routes, this.addChildPath(paths, child.path, params))} {itemRender(child, params, routes, addChildPath(paths, child.path, params))}
</Menu.Item> </Menu.Item>
))} ))}
</Menu> </Menu>
@ -108,23 +114,6 @@ export default class Breadcrumb extends React.Component<BreadcrumbProps, any> {
</BreadcrumbItem> </BreadcrumbItem>
); );
}); });
};
renderBreadcrumb = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
let crumbs;
const {
prefixCls: customizePrefixCls,
separator,
style,
className,
routes,
children,
...restProps
} = this.props;
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
if (routes && routes.length > 0) {
// generated by route
crumbs = this.genForRoutes(this.props);
} else if (children) { } else if (children) {
crumbs = toArray(children).map((element: any, index) => { crumbs = toArray(children).map((element: any, index) => {
if (!element) { if (!element) {
@ -145,21 +134,20 @@ export default class Breadcrumb extends React.Component<BreadcrumbProps, any> {
}); });
}); });
} }
const breadcrumbClassName = classNames(className, prefixCls, { const breadcrumbClassName = classNames(className, prefixCls, {
[`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-rtl`]: direction === 'rtl',
}); });
return ( return (
<div <div className={breadcrumbClassName} style={style} {...restProps}>
className={breadcrumbClassName}
style={style}
{...omit(restProps, ['itemRender', 'linkRender', 'nameRender', 'params'])}
>
{crumbs} {crumbs}
</div> </div>
); );
}; };
render() { Breadcrumb.Item = BreadcrumbItem;
return <ConfigConsumer>{this.renderBreadcrumb}</ConfigConsumer>;
} Breadcrumb.Separator = BreadcrumbSeparator;
}
export default Breadcrumb;

View File

@ -1,9 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import DownOutlined from '@ant-design/icons/DownOutlined'; import DownOutlined from '@ant-design/icons/DownOutlined';
import omit from 'omit.js';
import DropDown, { DropDownProps } from '../dropdown/dropdown'; import DropDown, { DropDownProps } from '../dropdown/dropdown';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider';
export interface BreadcrumbItemProps { export interface BreadcrumbItemProps {
prefixCls?: string; prefixCls?: string;
@ -13,53 +12,24 @@ export interface BreadcrumbItemProps {
dropdownProps?: DropDownProps; dropdownProps?: DropDownProps;
onClick?: React.MouseEventHandler<HTMLAnchorElement | HTMLSpanElement>; onClick?: React.MouseEventHandler<HTMLAnchorElement | HTMLSpanElement>;
} }
interface BreadcrumbItemInterface extends React.FC<BreadcrumbItemProps> {
export default class BreadcrumbItem extends React.Component<BreadcrumbItemProps, any> { __ANT_BREADCRUMB_ITEM: boolean;
static __ANT_BREADCRUMB_ITEM = true; }
const BreadcrumbItem: BreadcrumbItemInterface = ({
static defaultProps = { prefixCls: customizePrefixCls,
separator: '/', separator,
}; children,
overlay,
renderBreadcrumbItem = ({ getPrefixCls }: ConfigConsumerProps) => { dropdownProps,
const { prefixCls: customizePrefixCls, separator, children, ...restProps } = this.props; ...restProps
}) => {
const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls); const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
let link;
if ('href' in this.props) {
link = (
<a className={`${prefixCls}-link`} {...omit(restProps, ['overlay'])}>
{children}
</a>
);
} else {
link = (
<span className={`${prefixCls}-link`} {...omit(restProps, ['overlay'])}>
{children}
</span>
);
}
// wrap to dropDown
link = this.renderBreadcrumbNode(link, prefixCls);
if (children) {
return (
<span>
{link}
{separator && separator !== '' && (
<span className={`${prefixCls}-separator`}>{separator}</span>
)}
</span>
);
}
return null;
};
/** /**
* if overlay is have * if overlay is have
* Wrap a DropDown * Wrap a DropDown
*/ */
renderBreadcrumbNode = (breadcrumbItem: React.ReactNode, prefixCls: string) => { const renderBreadcrumbNode = (breadcrumbItem: React.ReactNode) => {
const { overlay, dropdownProps } = this.props;
if (overlay) { if (overlay) {
return ( return (
<DropDown overlay={overlay} placement="bottomCenter" {...dropdownProps}> <DropDown overlay={overlay} placement="bottomCenter" {...dropdownProps}>
@ -73,7 +43,36 @@ export default class BreadcrumbItem extends React.Component<BreadcrumbItemProps,
return breadcrumbItem; return breadcrumbItem;
}; };
render() { let link;
return <ConfigConsumer>{this.renderBreadcrumbItem}</ConfigConsumer>; if ('href' in restProps) {
link = (
<a className={`${prefixCls}-link`} {...restProps}>
{children}
</a>
);
} else {
link = (
<span className={`${prefixCls}-link`} {...restProps}>
{children}
</span>
);
} }
// wrap to dropDown
link = renderBreadcrumbNode(link);
if (children) {
return (
<span>
{link}
{separator && separator !== '' && (
<span className={`${prefixCls}-separator`}>{separator}</span>
)}
</span>
);
} }
return null;
};
BreadcrumbItem.__ANT_BREADCRUMB_ITEM = true;
export default BreadcrumbItem;

View File

@ -1,17 +1,17 @@
import * as React from 'react'; import * as React from 'react';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider';
export default class BreadcrumbSeparator extends React.Component<any> { interface BreadcrumbSeparatorInterface extends React.FC {
static __ANT_BREADCRUMB_SEPARATOR = true; __ANT_BREADCRUMB_SEPARATOR: boolean;
}
renderSeparator = ({ getPrefixCls }: ConfigConsumerProps) => { const BreadcrumbSeparator: BreadcrumbSeparatorInterface = ({ children }) => {
const { children } = this.props; const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('breadcrumb'); const prefixCls = getPrefixCls('breadcrumb');
return <span className={`${prefixCls}-separator`}>{children || '/'}</span>; return <span className={`${prefixCls}-separator`}>{children || '/'}</span>;
}; };
render() { BreadcrumbSeparator.__ANT_BREADCRUMB_SEPARATOR = true;
return <ConfigConsumer>{this.renderSeparator}</ConfigConsumer>;
} export default BreadcrumbSeparator;
}

View File

@ -111,21 +111,6 @@ describe('Breadcrumb', () => {
it('should accept undefined routes', () => { it('should accept undefined routes', () => {
const wrapper = render(<Breadcrumb routes={undefined} />); const wrapper = render(<Breadcrumb routes={undefined} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
})
it('props#linkRender and props#nameRender do not warn anymore', () => {
const linkRender = jest.fn();
const nameRender = jest.fn();
mount(
<Breadcrumb linkRender={linkRender} nameRender={nameRender}>
<Breadcrumb.Item />
<Breadcrumb.Item>xxx</Breadcrumb.Item>
</Breadcrumb>,
);
expect(errorSpy.mock.calls.length).toBe(0);
expect(linkRender).not.toHaveBeenCalled();
expect(nameRender).not.toHaveBeenCalled();
}); });
it('should support custom attribute', () => { it('should support custom attribute', () => {

View File

@ -73,7 +73,6 @@ exports[`react router react router 3 1`] = `
}, },
] ]
} }
separator="/"
> >
<div <div
className="ant-breadcrumb" className="ant-breadcrumb"

View File

@ -1,10 +1,6 @@
import Breadcrumb from './Breadcrumb'; import Breadcrumb from './Breadcrumb';
import BreadcrumbItem from './BreadcrumbItem';
import BreadcrumbSeparator from './BreadcrumbSeparator';
export { BreadcrumbProps } from './Breadcrumb'; export { BreadcrumbProps } from './Breadcrumb';
export { BreadcrumbItemProps } from './BreadcrumbItem'; export { BreadcrumbItemProps } from './BreadcrumbItem';
Breadcrumb.Item = BreadcrumbItem;
Breadcrumb.Separator = BreadcrumbSeparator;
export default Breadcrumb; export default Breadcrumb;