From 78e9d58ef18adb4455b3aefb8f6ecb82485efae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Thu, 27 Apr 2023 22:43:42 +0800 Subject: [PATCH] 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 --- components/breadcrumb/Breadcrumb.tsx | 74 ++++++------------- components/breadcrumb/BreadcrumbItem.tsx | 51 +++++-------- .../__snapshots__/itemRender.test.tsx.snap | 34 +++++++++ .../breadcrumb/__tests__/itemRender.test.tsx | 29 ++++++++ components/breadcrumb/index.en-US.md | 3 +- components/breadcrumb/index.zh-CN.md | 3 +- components/breadcrumb/useItemRender.tsx | 73 ++++++++++++++++++ components/breadcrumb/useItems.ts | 8 +- 8 files changed, 183 insertions(+), 92 deletions(-) create mode 100644 components/breadcrumb/__tests__/__snapshots__/itemRender.test.tsx.snap create mode 100644 components/breadcrumb/__tests__/itemRender.test.tsx create mode 100644 components/breadcrumb/useItemRender.tsx diff --git a/components/breadcrumb/Breadcrumb.tsx b/components/breadcrumb/Breadcrumb.tsx index aad0be2c56..bffc0eddbd 100755 --- a/components/breadcrumb/Breadcrumb.tsx +++ b/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[]; -} - 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; + + /** @deprecated Please use `menu` instead */ + children?: Omit[]; } export interface BreadcrumbSeparatorType { type: 'separator'; separator?: React.ReactNode; } -export type ItemType = BreadcrumbItemType | BreadcrumbSeparatorType; +export type ItemType = Partial; -type InternalRouteType = Partial; +export type InternalRouteType = Partial; -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 & { - 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 ( - { href={href} separator={isLastItem ? '' : separator} onClick={onClick} + prefixCls={prefixCls} > - {mergedItemRender(item as BreadcrumbItemType, params, itemRenderRoutes, paths)} - + {mergedItemRender(item as BreadcrumbItemType, params, itemRenderRoutes, paths, href)} + ); }); } else if (children) { diff --git a/components/breadcrumb/BreadcrumbItem.tsx b/components/breadcrumb/BreadcrumbItem.tsx index 3fc4bbacf9..0cb432e1b8 100644 --- a/components/breadcrumb/BreadcrumbItem.tsx +++ b/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 & { - __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 = ( - - {children} - - ); - } else { - link = ( - - {children} - - ); - } - // wrap to dropDown - link = renderBreadcrumbNode(link); - if (children !== undefined && children !== null) { + const link = renderBreadcrumbNode(children); + if (link !== undefined && link !== null) { return ( <>
  • {link}
  • @@ -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 ( + + {renderItem(prefixCls, restProps as ItemType, children, href)} + + ); +}; + BreadcrumbItem.__ANT_BREADCRUMB_ITEM = true; export default BreadcrumbItem; diff --git a/components/breadcrumb/__tests__/__snapshots__/itemRender.test.tsx.snap b/components/breadcrumb/__tests__/__snapshots__/itemRender.test.tsx.snap new file mode 100644 index 0000000000..00e7cc1d8a --- /dev/null +++ b/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`] = ` +
    + +
    +`; diff --git a/components/breadcrumb/__tests__/itemRender.test.tsx b/components/breadcrumb/__tests__/itemRender.test.tsx new file mode 100644 index 0000000000..d476dede19 --- /dev/null +++ b/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( + ( + + {route.title} + + )} + />, + ); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/components/breadcrumb/index.en-US.md b/components/breadcrumb/index.en-US.md index f1516a4e31..2f03017f0a 100644 --- a/components/breadcrumb/index.en-US.md +++ b/components/breadcrumb/index.en-US.md @@ -64,7 +64,8 @@ return ; | --- | --- | --- | --- | --- | | 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 | - | | diff --git a/components/breadcrumb/index.zh-CN.md b/components/breadcrumb/index.zh-CN.md index 7b94a3fc56..6ca4e9b69a 100644 --- a/components/breadcrumb/index.zh-CN.md +++ b/components/breadcrumb/index.zh-CN.md @@ -65,7 +65,8 @@ return ; | --- | --- | --- | --- | --- | | 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 | diff --git a/components/breadcrumb/useItemRender.tsx b/components/breadcrumb/useItemRender.tsx new file mode 100644 index 0000000000..11d10f3b29 --- /dev/null +++ b/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 any, TParameters extends [...args: any]> = ( + ...args: [...Parameters, ...TParameters] +) => ReturnType; + +type ItemRender = NonNullable; +type InternalItemRenderParams = AddParameters; + +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 ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +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; +} diff --git a/components/breadcrumb/useItems.ts b/components/breadcrumb/useItems.ts index cb50627183..ac0e74abfe 100644 --- a/components/breadcrumb/useItems.ts +++ b/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[] | null { return useMemo(() => { if (items) {