mirror of
https://github.com/ant-design/ant-design.git
synced 2025-01-18 06:03:38 +08:00
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
This commit is contained in:
parent
4ecac35a29
commit
78e9d58ef1
@ -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[];
|
||||
routes?: ItemType[];
|
||||
|
||||
itemRender?: (route: Route, params: any, routes: Route[], paths: string[]) => React.ReactNode;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -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;
|
||||
|
@ -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
Normal file
29
components/breadcrumb/__tests__/itemRender.test.tsx
Normal file
@ -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();
|
||||
});
|
||||
});
|
@ -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 | - | |
|
||||
|
@ -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
Normal file
73
components/breadcrumb/useItemRender.tsx
Normal file
@ -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;
|
||||
}
|
@ -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…
Reference in New Issue
Block a user