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,41 +48,48 @@ 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; path = (path || '').replace(/^\//, '');
Object.keys(params).forEach(key => {
path = path.replace(`:${key}`, params[key]);
});
return path;
};
static Separator: typeof BreadcrumbSeparator; const addChildPath = (paths: string[], childPath: string = '', params: any) => {
const originalPaths = [...paths];
const path = getPath(childPath, params);
if (path) {
originalPaths.push(path);
}
return originalPaths;
};
static defaultProps = { interface BreadcrumbInterface extends React.FC<BreadcrumbProps> {
separator: '/', Item: typeof BreadcrumbItem;
}; Separator: typeof BreadcrumbSeparator;
}
getPath = (path: string, params: any) => { const Breadcrumb: BreadcrumbInterface = ({
path = (path || '').replace(/^\//, ''); prefixCls: customizePrefixCls,
Object.keys(params).forEach(key => { separator = '/',
path = path.replace(`:${key}`, params[key]); style,
}); className,
return path; routes,
}; children,
itemRender = defaultItemRender,
params = {},
...restProps
}) => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
addChildPath = (paths: string[], childPath: string = '', params: any) => { let crumbs;
const originalPaths = [...paths]; const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
const path = this.getPath(childPath, params); if (routes && routes.length > 0) {
if (path) { // generated by route
originalPaths.push(path);
}
return originalPaths;
};
genForRoutes = ({
routes = [],
params = {},
separator,
itemRender = defaultItemRender,
}: BreadcrumbProps) => {
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,58 +114,40 @@ export default class Breadcrumb extends React.Component<BreadcrumbProps, any> {
</BreadcrumbItem> </BreadcrumbItem>
); );
}); });
}; } else if (children) {
crumbs = toArray(children).map((element: any, index) => {
if (!element) {
return element;
}
renderBreadcrumb = ({ getPrefixCls, direction }: ConfigConsumerProps) => { devWarning(
let crumbs; element.type &&
const { (element.type.__ANT_BREADCRUMB_ITEM === true ||
prefixCls: customizePrefixCls, element.type.__ANT_BREADCRUMB_SEPARATOR === true),
separator, 'Breadcrumb',
style, "Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children",
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) {
crumbs = toArray(children).map((element: any, index) => {
if (!element) {
return element;
}
devWarning( return cloneElement(element, {
element.type && separator,
(element.type.__ANT_BREADCRUMB_ITEM === true || key: index,
element.type.__ANT_BREADCRUMB_SEPARATOR === true),
'Breadcrumb',
"Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children",
);
return cloneElement(element, {
separator,
key: index,
});
}); });
}
const breadcrumbClassName = classNames(className, prefixCls, {
[`${prefixCls}-rtl`]: direction === 'rtl',
}); });
return (
<div
className={breadcrumbClassName}
style={style}
{...omit(restProps, ['itemRender', 'linkRender', 'nameRender', 'params'])}
>
{crumbs}
</div>
);
};
render() {
return <ConfigConsumer>{this.renderBreadcrumb}</ConfigConsumer>;
} }
}
const breadcrumbClassName = classNames(className, prefixCls, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
return (
<div className={breadcrumbClassName} style={style} {...restProps}>
{crumbs}
</div>
);
};
Breadcrumb.Item = BreadcrumbItem;
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 prefixCls = getPrefixCls('breadcrumb', customizePrefixCls); }) => {
let link; const { getPrefixCls } = React.useContext(ConfigContext);
if ('href' in this.props) { const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
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 { children } = this.props;
const prefixCls = getPrefixCls('breadcrumb');
return <span className={`${prefixCls}-separator`}>{children || '/'}</span>;
};
render() {
return <ConfigConsumer>{this.renderSeparator}</ConfigConsumer>;
}
} }
const BreadcrumbSeparator: BreadcrumbSeparatorInterface = ({ children }) => {
const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('breadcrumb');
return <span className={`${prefixCls}-separator`}>{children || '/'}</span>;
};
BreadcrumbSeparator.__ANT_BREADCRUMB_SEPARATOR = true;
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;