Browse Source

fix: Breadcrumb `itemRender` not remove link even path is provided (#42049)

* refactor: out of bread item render

* refactor: fix too filled

* fix: render logic

* test: add test case

* fix: ts

* fix: test
pull/42055/head
二货爱吃白萝卜 2 years ago
committed by GitHub
parent
commit
78e9d58ef1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 74
      components/breadcrumb/Breadcrumb.tsx
  2. 51
      components/breadcrumb/BreadcrumbItem.tsx
  3. 34
      components/breadcrumb/__tests__/__snapshots__/itemRender.test.tsx.snap
  4. 29
      components/breadcrumb/__tests__/itemRender.test.tsx
  5. 3
      components/breadcrumb/index.en-US.md
  6. 3
      components/breadcrumb/index.zh-CN.md
  7. 73
      components/breadcrumb/useItemRender.tsx
  8. 8
      components/breadcrumb/useItems.ts

74
components/breadcrumb/Breadcrumb.tsx

@ -2,23 +2,17 @@ import classNames from 'classnames';
import toArray from 'rc-util/lib/Children/toArray';
import pickAttrs from 'rc-util/lib/pickAttrs';
import * as React from 'react';
import { ConfigContext } from '../config-provider';
import { cloneElement } from '../_util/reactNode';
import warning from '../_util/warning';
import { ConfigContext } from '../config-provider';
import type { BreadcrumbItemProps } from './BreadcrumbItem';
import BreadcrumbItem from './BreadcrumbItem';
import BreadcrumbItem, { InternalBreadcrumbItem } from './BreadcrumbItem';
import BreadcrumbSeparator from './BreadcrumbSeparator';
import useStyle from './style';
import useItemRender from './useItemRender';
import useItems from './useItems';
/** @deprecated New of Breadcrumb using `items` instead of `routes` */
export interface Route {
path: string;
breadcrumbName: string;
children?: Omit<Route, 'children'>[];
}
export interface BreadcrumbItemType {
key?: React.Key;
/**
@ -29,23 +23,28 @@ export interface BreadcrumbItemType {
* Different with `href`. It will concat all prev `path` to the current one.
*/
path?: string;
title: React.ReactNode;
title?: React.ReactNode;
/* @deprecated Please use `title` instead */
breadcrumbName?: string;
menu?: BreadcrumbItemProps['menu'];
/** @deprecated Please use `menu` instead */
overlay?: React.ReactNode;
className?: string;
onClick?: React.MouseEventHandler<HTMLAnchorElement | HTMLSpanElement>;
/** @deprecated Please use `menu` instead */
children?: Omit<BreadcrumbItemType, 'children'>[];
}
export interface BreadcrumbSeparatorType {
type: 'separator';
separator?: React.ReactNode;
}
export type ItemType = BreadcrumbItemType | BreadcrumbSeparatorType;
export type ItemType = Partial<BreadcrumbItemType & BreadcrumbSeparatorType>;
type InternalRouteType = Partial<BreadcrumbItemType & BreadcrumbSeparatorType>;
export type InternalRouteType = Partial<BreadcrumbItemType & BreadcrumbSeparatorType>;
export interface BaseBreadcrumbProps {
export interface BreadcrumbProps {
prefixCls?: string;
params?: any;
separator?: React.ReactNode;
@ -53,17 +52,11 @@ export interface BaseBreadcrumbProps {
className?: string;
rootClassName?: string;
children?: React.ReactNode;
}
export interface LegacyBreadcrumbProps extends BaseBreadcrumbProps {
/** @deprecated Please use `items` instead */
routes: Route[];
itemRender?: (route: Route, params: any, routes: Route[], paths: string[]) => React.ReactNode;
}
routes?: ItemType[];
export interface NewBreadcrumbProps extends BaseBreadcrumbProps {
items: ItemType[];
items?: ItemType[];
itemRender?: (
route: ItemType,
@ -73,21 +66,6 @@ export interface NewBreadcrumbProps extends BaseBreadcrumbProps {
) => React.ReactNode;
}
export type BreadcrumbProps = BaseBreadcrumbProps | LegacyBreadcrumbProps | NewBreadcrumbProps;
function getBreadcrumbName(route: InternalRouteType, params: any) {
if (route.title === undefined) {
return null;
}
const paramsKeys = Object.keys(params).join('|');
return typeof route.title === 'object'
? route.title
: String(route.title).replace(
new RegExp(`:(${paramsKeys})`, 'g'),
(replacement, key) => params[key] || replacement,
);
}
const getPath = (params: any, path?: string) => {
if (path === undefined) {
return path;
@ -100,12 +78,7 @@ const getPath = (params: any, path?: string) => {
return mergedPath;
};
type CompoundedComponent = React.FC<BreadcrumbProps> & {
Item: typeof BreadcrumbItem;
Separator: typeof BreadcrumbSeparator;
};
const Breadcrumb: CompoundedComponent = (props) => {
const Breadcrumb = (props: BreadcrumbProps) => {
const {
prefixCls: customizePrefixCls,
separator = '/',
@ -118,7 +91,7 @@ const Breadcrumb: CompoundedComponent = (props) => {
itemRender,
params = {},
...restProps
} = props as LegacyBreadcrumbProps & NewBreadcrumbProps;
} = props;
const { getPrefixCls, direction } = React.useContext(ConfigContext);
@ -132,13 +105,7 @@ const Breadcrumb: CompoundedComponent = (props) => {
warning(!legacyRoutes, 'Breadcrumb', '`routes` is deprecated. Please use `items` instead.');
}
const mergedItemRender =
itemRender ||
((route: BreadcrumbItemType) => {
const name = getBreadcrumbName(route, params);
return name;
});
const mergedItemRender = useItemRender(prefixCls, itemRender);
if (mergedItems && mergedItems.length > 0) {
// generated by route
@ -188,7 +155,7 @@ const Breadcrumb: CompoundedComponent = (props) => {
}
return (
<BreadcrumbItem
<InternalBreadcrumbItem
key={mergedKey}
{...itemProps}
{...pickAttrs(item, {
@ -198,9 +165,10 @@ const Breadcrumb: CompoundedComponent = (props) => {
href={href}
separator={isLastItem ? '' : separator}
onClick={onClick}
prefixCls={prefixCls}
>
{mergedItemRender(item as BreadcrumbItemType, params, itemRenderRoutes, paths)}
</BreadcrumbItem>
{mergedItemRender(item as BreadcrumbItemType, params, itemRenderRoutes, paths, href)}
</InternalBreadcrumbItem>
);
});
} else if (children) {

51
components/breadcrumb/BreadcrumbItem.tsx

@ -4,7 +4,9 @@ import warning from '../_util/warning';
import { ConfigContext } from '../config-provider';
import type { DropdownProps } from '../dropdown/dropdown';
import Dropdown from '../dropdown/dropdown';
import type { ItemType } from './Breadcrumb';
import BreadcrumbSeparator from './BreadcrumbSeparator';
import { renderItem } from './useItemRender';
export interface SeparatorType {
separator?: React.ReactNode;
@ -34,23 +36,9 @@ export interface BreadcrumbItemProps extends SeparatorType {
/** @deprecated Please use `menu` instead */
overlay?: DropdownProps['overlay'];
}
type CompoundedComponent = React.FC<BreadcrumbItemProps> & {
__ANT_BREADCRUMB_ITEM: boolean;
};
const BreadcrumbItem: CompoundedComponent = (props: BreadcrumbItemProps) => {
const {
prefixCls: customizePrefixCls,
separator = '/',
children,
menu,
overlay,
dropdownProps,
href,
...restProps
} = props;
const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
export const InternalBreadcrumbItem = (props: BreadcrumbItemProps) => {
const { prefixCls, separator = '/', children, menu, overlay, dropdownProps, href } = props;
// Warning for deprecated usage
if (process.env.NODE_ENV !== 'production') {
@ -102,24 +90,9 @@ const BreadcrumbItem: CompoundedComponent = (props: BreadcrumbItemProps) => {
return breadcrumbItem;
};
let link: React.ReactNode;
if (href !== undefined) {
link = (
<a className={`${prefixCls}-link`} href={href} {...restProps}>
{children}
</a>
);
} else {
link = (
<span className={`${prefixCls}-link`} {...restProps}>
{children}
</span>
);
}
// wrap to dropDown
link = renderBreadcrumbNode(link);
if (children !== undefined && children !== null) {
const link = renderBreadcrumbNode(children);
if (link !== undefined && link !== null) {
return (
<>
<li>{link}</li>
@ -130,6 +103,18 @@ const BreadcrumbItem: CompoundedComponent = (props: BreadcrumbItemProps) => {
return null;
};
const BreadcrumbItem = (props: BreadcrumbItemProps) => {
const { prefixCls: customizePrefixCls, children, href, ...restProps } = props;
const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
return (
<InternalBreadcrumbItem {...restProps} prefixCls={prefixCls}>
{renderItem(prefixCls, restProps as ItemType, children, href)}
</InternalBreadcrumbItem>
);
};
BreadcrumbItem.__ANT_BREADCRUMB_ITEM = true;
export default BreadcrumbItem;

34
components/breadcrumb/__tests__/__snapshots__/itemRender.test.tsx.snap

@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Breadcrumb.ItemRender render as expect 1`] = `
<div>
<nav
class="ant-breadcrumb"
>
<ol>
<li>
<a
class="my-link"
data-path="/"
>
Home
</a>
</li>
<li
aria-hidden="true"
class="ant-breadcrumb-separator"
>
/
</li>
<li>
<a
class="my-link"
data-path="/bamboo"
>
Bamboo
</a>
</li>
</ol>
</nav>
</div>
`;

29
components/breadcrumb/__tests__/itemRender.test.tsx

@ -0,0 +1,29 @@
import React from 'react';
import { render } from '../../../tests/utils';
import Breadcrumb from '../index';
describe('Breadcrumb.ItemRender', () => {
it('render as expect', () => {
const { container } = render(
<Breadcrumb
items={[
{
path: '/',
title: 'Home',
},
{
path: '/bamboo',
title: 'Bamboo',
},
]}
itemRender={(route) => (
<a className="my-link" data-path={route.path}>
{route.title}
</a>
)}
/>,
);
expect(container).toMatchSnapshot();
});
});

3
components/breadcrumb/index.en-US.md

@ -64,7 +64,8 @@ return <Breadcrumb routes={[{ breadcrumbName: 'sample' }]} />;
| --- | --- | --- | --- | --- |
| className | The additional css class | string | - | |
| dropdownProps | The dropdown props | [Dropdown](/components/dropdown) | - | |
| href | Target of hyperlink | string | - | |
| href | Target of hyperlink. Can not work with `path` | string | - | |
| path | Connected path. Each path will connect with prev one. Can not work with `href` | string | - | |
| menu | The menu props | [MenuProps](/components/menu/#api) | - | 4.24.0 |
| onClick | Set the handler to handle click event | (e:MouseEvent) => void | - | |
| title | item name | ReactNode | - | |

3
components/breadcrumb/index.zh-CN.md

@ -65,7 +65,8 @@ return <Breadcrumb routes={[{ breadcrumbName: 'sample' }]} />;
| --- | --- | --- | --- | --- |
| className | 自定义类名 | string | - | |
| dropdownProps | 弹出下拉菜单的自定义配置 | [Dropdown](/components/dropdown-cn) | - | |
| href | 链接的目的地 | string | - | |
| href | 链接的目的地,不能和 `path` 共用 | string | - | |
| path | 拼接路径,每一层都会拼接前一个 `path` 信息。不能和 `href` 共用 | string | - | |
| menu | 菜单配置项 | [MenuProps](/components/menu-cn/#api) | - | 4.24.0 |
| onClick | 单击事件 | (e:MouseEvent) => void | - | |
| title | 名称 | ReactNode | - | 5.3.0 |

73
components/breadcrumb/useItemRender.tsx

@ -0,0 +1,73 @@
import classNames from 'classnames';
import pickAttrs from 'rc-util/lib/pickAttrs';
import * as React from 'react';
import type { BreadcrumbProps, InternalRouteType, ItemType } from './Breadcrumb';
type AddParameters<TFunction extends (...args: any) => any, TParameters extends [...args: any]> = (
...args: [...Parameters<TFunction>, ...TParameters]
) => ReturnType<TFunction>;
type ItemRender = NonNullable<BreadcrumbProps['itemRender']>;
type InternalItemRenderParams = AddParameters<ItemRender, [href?: string]>;
function getBreadcrumbName(route: InternalRouteType, params: any) {
if (route.title === undefined) {
return null;
}
const paramsKeys = Object.keys(params).join('|');
return typeof route.title === 'object'
? route.title
: String(route.title).replace(
new RegExp(`:(${paramsKeys})`, 'g'),
(replacement, key) => params[key] || replacement,
);
}
export function renderItem(
prefixCls: string,
item: ItemType,
children: React.ReactNode,
href?: string,
) {
if (children === null || children === undefined) {
return null;
}
const { className, onClick, ...restItem } = item;
const passedProps = {
...pickAttrs(restItem, {
data: true,
aria: true,
}),
onClick,
};
if (href !== undefined) {
return (
<a {...passedProps} className={classNames(`${prefixCls}-link`, className)} href={href}>
{children}
</a>
);
}
return (
<span {...passedProps} className={classNames(`${prefixCls}-link`, className)}>
{children}
</span>
);
}
export default function useItemRender(prefixCls: string, itemRender?: ItemRender) {
const mergedItemRender: InternalItemRenderParams = (item, params, routes, path, href) => {
if (itemRender) {
return itemRender(item, params, routes, path);
}
const name = getBreadcrumbName(item, params);
return renderItem(prefixCls, item, name, href);
};
return mergedItemRender;
}

8
components/breadcrumb/useItems.ts

@ -1,11 +1,11 @@
import { useMemo } from 'react';
import type { BreadcrumbItemType, BreadcrumbSeparatorType, ItemType, Route } from './Breadcrumb';
import type { BreadcrumbItemType, BreadcrumbSeparatorType, ItemType } from './Breadcrumb';
type MergedType = BreadcrumbItemType & {
children?: Route['children'];
children?: ItemType['children'];
};
function route2item(route: Route): MergedType {
function route2item(route: ItemType): MergedType {
const { breadcrumbName, children, ...rest } = route;
const clone: MergedType = {
@ -27,7 +27,7 @@ function route2item(route: Route): MergedType {
export default function useItems(
items?: ItemType[],
routes?: Route[],
routes?: ItemType[],
): Partial<MergedType & BreadcrumbSeparatorType>[] | null {
return useMemo<ItemType[] | null>(() => {
if (items) {

Loading…
Cancel
Save