From 413e44a170001690a03997d7ec910797f02103a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=91=E9=9B=A8?= Date: Sun, 5 Mar 2023 20:57:49 +0800 Subject: [PATCH 1/2] feat: breadcrumb support items (#40543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: breadCrumb support items * feat: update snap * feat: 删除部分不支持的test case * feat: update snap * feat: update snap * feat: update snap * feat: update ts * feat: update ts * feat: update ts * feat: update for reviewer * doc: update for doc * doc: update for doc * doc: replace breadcrumbName to title * doc: replace breadcrumbName to title * doc: replace breadcrumbName to title * chore: adjust separator logic * refactor: use items * chore: update logic * chore: fix routes logic * chore: fix logic * chore: clean up * chore: adjust warning info * doc: edit test case * feat: update snap --------- Co-authored-by: 二货机器人 --- components/breadcrumb/Breadcrumb.tsx | 211 ++++++++++++------ components/breadcrumb/BreadcrumbItem.tsx | 54 ++++- .../breadcrumb/__tests__/Breadcrumb.test.tsx | 202 +++++++++++++---- .../__snapshots__/Breadcrumb.test.tsx.snap | 74 +++--- .../__snapshots__/demo-extend.test.ts.snap | 171 ++++++++++++++ .../__tests__/__snapshots__/demo.test.ts.snap | 54 +++++ .../__snapshots__/router.test.tsx.snap | 48 ++-- components/breadcrumb/demo/basic.tsx | 26 ++- components/breadcrumb/demo/debug-routes.md | 7 + components/breadcrumb/demo/debug-routes.tsx | 27 +++ components/breadcrumb/demo/overlay.tsx | 29 ++- components/breadcrumb/demo/react-router.tsx | 18 +- .../breadcrumb/demo/separator-component.md | 4 +- .../breadcrumb/demo/separator-component.tsx | 40 +++- components/breadcrumb/demo/separator.tsx | 25 ++- components/breadcrumb/demo/withIcon.tsx | 32 ++- components/breadcrumb/index.en-US.md | 79 ++++--- components/breadcrumb/index.ts | 2 +- components/breadcrumb/index.zh-CN.md | 80 ++++--- components/breadcrumb/useItems.ts | 43 ++++ 20 files changed, 905 insertions(+), 321 deletions(-) create mode 100644 components/breadcrumb/demo/debug-routes.md create mode 100644 components/breadcrumb/demo/debug-routes.tsx create mode 100644 components/breadcrumb/useItems.ts diff --git a/components/breadcrumb/Breadcrumb.tsx b/components/breadcrumb/Breadcrumb.tsx index 9d7d5413fe..41ccb65166 100755 --- a/components/breadcrumb/Breadcrumb.tsx +++ b/components/breadcrumb/Breadcrumb.tsx @@ -1,9 +1,8 @@ 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 type { DropdownProps } from '../dropdown'; -import Menu from '../menu'; import { cloneElement } from '../_util/reactNode'; import warning from '../_util/warning'; import type { BreadcrumbItemProps } from './BreadcrumbItem'; @@ -11,63 +10,92 @@ import BreadcrumbItem from './BreadcrumbItem'; import BreadcrumbSeparator from './BreadcrumbSeparator'; import useStyle from './style'; +import useItems from './useItems'; +/** @deprecated New of Breadcrumb using `items` instead of `routes` */ export interface Route { path: string; breadcrumbName: string; children?: Omit[]; } -export interface BreadcrumbProps { +export interface BreadcrumbItemType { + key?: React.Key; + /** + * Different with `path`. Directly set the link of this item. + */ + href?: string; + /** + * Different with `href`. It will concat all prev `path` to the current one. + */ + path?: string; + title: React.ReactNode; + menu?: BreadcrumbItemProps['menu']; + /** @deprecated Please use `menu` instead */ + overlay?: React.ReactNode; +} +export interface BreadcrumbSeparatorType { + type: 'separator'; + separator?: React.ReactNode; +} + +export type ItemType = BreadcrumbItemType | BreadcrumbSeparatorType; + +type InternalRouteType = Partial; + +export interface BaseBreadcrumbProps { prefixCls?: string; - routes?: Route[]; params?: any; separator?: React.ReactNode; - itemRender?: ( - route: Route, - params: any, - routes: Array, - paths: Array, - ) => React.ReactNode; style?: React.CSSProperties; className?: string; rootClassName?: string; children?: React.ReactNode; } -function getBreadcrumbName(route: Route, params: any) { - if (!route.breadcrumbName) { +export interface LegacyBreadcrumbProps extends BaseBreadcrumbProps { + /** @deprecated Please use `items` instead */ + routes: Route[]; + + itemRender?: (route: Route, params: any, routes: Route[], paths: string[]) => React.ReactNode; +} + +export interface NewBreadcrumbProps extends BaseBreadcrumbProps { + items: ItemType[]; + + itemRender?: ( + route: ItemType, + params: any, + routes: ItemType[], + paths: string[], + ) => 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('|'); - const name = route.breadcrumbName.replace( - new RegExp(`:(${paramsKeys})`, 'g'), - (replacement, key) => params[key] || replacement, - ); - return name; + return typeof route.title === 'object' + ? route.title + : String(route.title).replace( + new RegExp(`:(${paramsKeys})`, 'g'), + (replacement, key) => params[key] || replacement, + ); } -function defaultItemRender(route: Route, params: any, routes: Route[], paths: string[]) { - const isLastItem = routes.indexOf(route) === routes.length - 1; - const name = getBreadcrumbName(route, params); - return isLastItem ? {name} : {name}; -} - -const getPath = (path: string, params: any) => { - path = (path || '').replace(/^\//, ''); - Object.keys(params).forEach((key) => { - path = path.replace(`:${key}`, params[key]); - }); - return path; -}; - -const addChildPath = (paths: string[], childPath: string, params: any) => { - const originalPaths = [...paths]; - const path = getPath(childPath || '', params); - if (path) { - originalPaths.push(path); +const getPath = (params: any, path?: string) => { + if (path === undefined) { + return path; } - return originalPaths; + + let mergedPath = (path || '').replace(/^\//, ''); + Object.keys(params).forEach((key) => { + mergedPath = mergedPath.replace(`:${key}`, params[key]!); + }); + return mergedPath; }; type CompoundedComponent = React.FC & { @@ -75,55 +103,87 @@ type CompoundedComponent = React.FC & { Separator: typeof BreadcrumbSeparator; }; -const Breadcrumb: CompoundedComponent = ({ - prefixCls: customizePrefixCls, - separator = '/', - style, - className, - rootClassName, - routes, - children, - itemRender = defaultItemRender, - params = {}, - ...restProps -}) => { +const Breadcrumb: CompoundedComponent = (props) => { + const { + prefixCls: customizePrefixCls, + separator = '/', + style, + className, + rootClassName, + routes: legacyRoutes, + items, + children, + itemRender, + params = {}, + ...restProps + } = props as LegacyBreadcrumbProps & NewBreadcrumbProps; + const { getPrefixCls, direction } = React.useContext(ConfigContext); let crumbs: React.ReactNode; const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls); const [wrapSSR, hashId] = useStyle(prefixCls); - if (routes && routes.length > 0) { + const mergedItems = useItems(items, legacyRoutes); + + if (process.env.NODE_ENV !== 'production') { + warning(!legacyRoutes, 'Breadcrumb', '`routes` is deprecated. Please use `items` instead.'); + } + + const mergedItemRender = + itemRender || + ((route: BreadcrumbItemType) => { + const name = getBreadcrumbName(route, params); + + return name; + }); + + if (mergedItems && mergedItems.length > 0) { // generated by route const paths: string[] = []; - crumbs = routes.map((route) => { - const path = getPath(route.path, params); - if (path) { - paths.push(path); - } - // generated overlay by route.children - let overlay: DropdownProps['overlay']; - if (route.children && route.children.length) { - overlay = ( - ({ - key: child.path || child.breadcrumbName, - label: itemRender(child, params, routes, addChildPath(paths, child.path, params)), - }))} - /> - ); + const itemRenderRoutes: any = items || legacyRoutes; + + crumbs = mergedItems.map((item, index) => { + const { path, key, type, menu, overlay, separator: itemSeparator } = item; + const mergedPath = getPath(params, path); + + if (mergedPath !== undefined) { + paths.push(mergedPath); } - const itemProps: BreadcrumbItemProps = { separator }; + const mergedKey = key ?? index; - if (overlay) { - itemProps.overlay = overlay; + if (type === 'separator') { + return {itemSeparator}; + } + + const itemProps: BreadcrumbItemProps = {}; + const isLastItem = index === mergedItems.length - 1; + + if (menu) { + itemProps.menu = menu; + } else if (overlay) { + itemProps.overlay = overlay as any; + } + + let { href } = item; + if (paths.length && mergedPath !== undefined) { + href = `#/${paths.join('/')}`; } return ( - - {itemRender(route, params, routes, paths)} + + {mergedItemRender(item as BreadcrumbItemType, params, itemRenderRoutes, paths)} ); }); @@ -133,7 +193,14 @@ const Breadcrumb: CompoundedComponent = ({ if (!element) { return element; } - + // =================== Warning ===================== + if (process.env.NODE_ENV !== 'production') { + warning( + !element, + 'Breadcrumb', + '`Breadcrumb.Item and Breadcrumb.Separator` is deprecated. Please use `items` instead.', + ); + } warning( element.type && (element.type.__ANT_BREADCRUMB_ITEM === true || diff --git a/components/breadcrumb/BreadcrumbItem.tsx b/components/breadcrumb/BreadcrumbItem.tsx index 8cc33f3670..1ec40badf5 100644 --- a/components/breadcrumb/BreadcrumbItem.tsx +++ b/components/breadcrumb/BreadcrumbItem.tsx @@ -1,21 +1,34 @@ -import * as React from 'react'; import DownOutlined from '@ant-design/icons/DownOutlined'; -import warning from '../_util/warning'; +import * as React from 'react'; import { ConfigContext } from '../config-provider'; import type { DropdownProps } from '../dropdown/dropdown'; import Dropdown from '../dropdown/dropdown'; +import warning from '../_util/warning'; import BreadcrumbSeparator from './BreadcrumbSeparator'; -export interface BreadcrumbItemProps { - prefixCls?: string; +export interface SeparatorType { separator?: React.ReactNode; + key?: React.Key; +} + +type MenuType = DropdownProps['menu']; +interface MenuItem { + title?: React.ReactNode; + label?: React.ReactNode; + path?: string; href?: string; - menu?: DropdownProps['menu']; +} + +export interface BreadcrumbItemProps extends SeparatorType { + prefixCls?: string; + href?: string; + menu?: Omit & { + items?: MenuItem[]; + }; dropdownProps?: DropdownProps; onClick?: React.MouseEventHandler; className?: string; children?: React.ReactNode; - // Deprecated /** @deprecated Please use `menu` instead */ overlay?: DropdownProps['overlay']; @@ -31,6 +44,7 @@ const BreadcrumbItem: CompoundedComponent = (props: BreadcrumbItemProps) => { menu, overlay, dropdownProps, + href, ...restProps } = props; @@ -52,11 +66,31 @@ const BreadcrumbItem: CompoundedComponent = (props: BreadcrumbItemProps) => { const mergeDropDownProps: DropdownProps = { ...dropdownProps, }; - if ('overlay' in props) { + + if (menu) { + const { items, ...menuProps } = menu! || {}; + mergeDropDownProps.menu = { + ...menuProps, + items: items?.map(({ title, label, path, ...itemProps }, index) => { + let mergedLabel: React.ReactNode = label ?? title; + + if (path) { + mergedLabel = {mergedLabel}; + } + + return { + ...itemProps, + key: index, + label: mergedLabel as string, + }; + }), + }; + } else if (overlay) { mergeDropDownProps.overlay = overlay; } + return ( - + {breadcrumbItem} @@ -68,9 +102,9 @@ const BreadcrumbItem: CompoundedComponent = (props: BreadcrumbItemProps) => { }; let link: React.ReactNode; - if ('href' in restProps) { + if (href !== undefined) { link = ( - + {children} ); diff --git a/components/breadcrumb/__tests__/Breadcrumb.test.tsx b/components/breadcrumb/__tests__/Breadcrumb.test.tsx index b7b88845cc..af702de28d 100644 --- a/components/breadcrumb/__tests__/Breadcrumb.test.tsx +++ b/components/breadcrumb/__tests__/Breadcrumb.test.tsx @@ -3,7 +3,8 @@ import accessibilityTest from '../../../tests/shared/accessibilityTest'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; import { render } from '../../../tests/utils'; -import type { Route } from '../Breadcrumb'; +import { resetWarned } from '../../_util/warning'; +import type { ItemType } from '../Breadcrumb'; import Breadcrumb from '../index'; describe('Breadcrumb', () => { @@ -33,22 +34,96 @@ describe('Breadcrumb', () => { ); }); - it('overlay deprecation warning', () => { + it('warns on routes', () => { + render( + , + ); + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: [antd: Breadcrumb] `routes` is deprecated. Please use `items` instead.', + ); + }); + + it('should render correct', () => { + const { asFragment } = render( + xxx, + }, + { + title: 'yyy', + }, + ]} + />, + ); + expect(asFragment().firstChild).toMatchSnapshot(); + }); + + describe('overlay deprecation warning set', () => { + it('legacy jsx', () => { + resetWarned(); + render( + + menu}> + General + + , + ); + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: [antd: Breadcrumb.Item] `overlay` is deprecated. Please use `menu` instead.', + ); + }); + + it('items', () => { + resetWarned(); + render( + menu, + title: 'General', + }, + ]} + />, + ); + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: [antd: Breadcrumb.Item] `overlay` is deprecated. Please use `menu` instead.', + ); + }); + }); + + it('Breadcrumb.Item deprecation warning', () => { render( - menu}> - General - + Location , ); expect(errorSpy).toHaveBeenCalledWith( - 'Warning: [antd: Breadcrumb.Item] `overlay` is deprecated. Please use `menu` instead.', + 'Warning: [antd: Breadcrumb] `Breadcrumb.Item and Breadcrumb.Separator` is deprecated. Please use `items` instead.', + ); + }); + + it('Breadcrumb.separator deprecation warning', () => { + render( + + : + , + ); + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: [antd: Breadcrumb] `Breadcrumb.Item and Breadcrumb.Separator` is deprecated. Please use `items` instead.', ); }); // https://github.com/ant-design/ant-design/issues/40204 it('wrong overlay deprecation warning in Dropdown', () => { - const items = [ + const menuItems = [ { key: '1', label: ( @@ -59,11 +134,14 @@ describe('Breadcrumb', () => { }, ]; render( - - - General - - , + General, + }, + ]} + />, ); expect(errorSpy).not.toHaveBeenCalledWith( 'Warning: [antd: Dropdown] `overlay` is deprecated. Please use `menu` instead.', @@ -79,18 +157,23 @@ describe('Breadcrumb', () => { {undefined} , ); - expect(errorSpy).not.toHaveBeenCalled(); expect(asFragment().firstChild).toMatchSnapshot(); }); // https://github.com/ant-design/ant-design/issues/5542 it('should not display Breadcrumb Item when its children is falsy', () => { const { asFragment } = render( - - - xxx - yyy - , + , ); expect(asFragment().firstChild).toMatchSnapshot(); }); @@ -111,53 +194,66 @@ describe('Breadcrumb', () => { }); it('should render a menu', () => { - const routes: Route[] = [ + const items: ItemType[] = [ { path: 'index', - breadcrumbName: 'home', + title: 'home', }, { path: 'first', - breadcrumbName: 'first', - children: [ - { - path: '/general', - breadcrumbName: 'General', - }, - { - path: '/layout', - breadcrumbName: 'Layout', - }, - { - path: '/navigation', - breadcrumbName: 'Navigation', - }, - ], + title: 'first', + menu: { + items: [ + { + path: '/general', + title: 'General', + }, + { + path: '/layout', + title: 'Layout', + }, + { + path: '/navigation', + title: 'Navigation', + }, + ], + }, }, { path: 'second', - breadcrumbName: 'second', + title: 'second', }, { path: 'third', - breadcrumbName: '', + title: '', }, ]; - const { asFragment } = render(); + const { asFragment } = render(); expect(asFragment().firstChild).toMatchSnapshot(); }); - it('should accept undefined routes', () => { - const { asFragment } = render(); + it('should accept undefined items', () => { + const { asFragment } = render(); expect(asFragment().firstChild).toMatchSnapshot(); }); it('should support custom attribute', () => { const { asFragment } = render( - - xxx - yyy - , + ( + + ) as React.ReactElement>, ); expect(asFragment().firstChild).toMatchSnapshot(); }); @@ -194,12 +290,19 @@ describe('Breadcrumb', () => { ); expect(asFragment().firstChild).toMatchSnapshot(); }); + it('should support string `0` and number `0`', () => { const { container } = render( - - {0} - 0 - , + , ); expect(container.querySelectorAll('.ant-breadcrumb-link')[0].textContent).toBe('0'); expect(container.querySelectorAll('.ant-breadcrumb-link')[1].textContent).toBe('0'); @@ -207,6 +310,7 @@ describe('Breadcrumb', () => { }); it('should console Error when `overlay` in props', () => { + resetWarned(); const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); render( @@ -221,7 +325,7 @@ describe('Breadcrumb', () => { it('should not console Error when `overlay` not in props', () => { const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - render(); + render(); expect(errSpy).not.toHaveBeenCalled(); errSpy.mockRestore(); }); diff --git a/components/breadcrumb/__tests__/__snapshots__/Breadcrumb.test.tsx.snap b/components/breadcrumb/__tests__/__snapshots__/Breadcrumb.test.tsx.snap index 2cf89626c9..2af8918a6b 100644 --- a/components/breadcrumb/__tests__/__snapshots__/Breadcrumb.test.tsx.snap +++ b/components/breadcrumb/__tests__/__snapshots__/Breadcrumb.test.tsx.snap @@ -44,7 +44,7 @@ exports[`Breadcrumb rtl render component should be rendered correctly in RTL dir `; -exports[`Breadcrumb should accept undefined routes 1`] = ` +exports[`Breadcrumb should accept undefined items 1`] = ` +`; + +exports[`Breadcrumb should render correct 1`] = ` + `; diff --git a/components/breadcrumb/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/breadcrumb/__tests__/__snapshots__/demo-extend.test.ts.snap index 2600a7ec94..036ccd5c5f 100644 --- a/components/breadcrumb/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/breadcrumb/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -63,6 +63,177 @@ exports[`renders ./components/breadcrumb/demo/basic.tsx extend context correctly `; +exports[`renders ./components/breadcrumb/demo/debug-routes.tsx extend context correctly 1`] = ` + +`; + exports[`renders ./components/breadcrumb/demo/overlay.tsx extend context correctly 1`] = `