Skip to content

Commit ad8e7fc

Browse files
authored
feat(breadcrumb): add breadcrumb collapse API (#3487)
re #3391
1 parent 8f6d7c6 commit ad8e7fc

File tree

10 files changed

+931
-8
lines changed

10 files changed

+931
-8
lines changed

packages/components/breadcrumb/Breadcrumb.tsx

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import React from 'react';
1+
import React, { Children, useCallback, useMemo } from 'react';
22
import classNames from 'classnames';
3+
import { EllipsisIcon } from 'tdesign-icons-react';
4+
import { isFunction } from 'lodash-es';
5+
import log from '@tdesign/common-js/log/index';
36
import useConfig from '../hooks/useConfig';
47
import forwardRefWithStatics from '../_util/forwardRefWithStatics';
58
import BreadcrumbItem from './BreadcrumbItem';
@@ -11,15 +14,18 @@ import { TdBreadcrumbItemProps } from './type';
1114

1215
const Breadcrumb = forwardRefWithStatics(
1316
(props: BreadcrumbProps, ref: React.Ref<HTMLDivElement>) => {
14-
const { children, options, separator, maxItemWidth, className, ...restProps } = useDefaultProps(
17+
const { children, options, separator, maxItemWidth, className, maxItems, itemsAfterCollapse, itemsBeforeCollapse, ellipsis, ...restProps } = useDefaultProps(
1518
props,
1619
breadcrumbDefaultProps,
1720
);
1821
const { classPrefix } = useConfig();
1922

20-
let content = children;
21-
if (options && options.length) {
22-
content = options.map((option, index) => {
23+
const renderOptionsToItems = useCallback((options: TdBreadcrumbItemProps[]) => {
24+
if (!options || !options.length) {
25+
return undefined;
26+
}
27+
28+
return options.map((option, index) => {
2329
const targetProps: TdBreadcrumbItemProps = {};
2430
if (option.target) {
2531
targetProps.target = option.target;
@@ -37,12 +43,74 @@ const Breadcrumb = forwardRefWithStatics(
3743
separator={separator}
3844
maxItemWidth={maxItemWidth}
3945
icon={option.icon}
46+
content={option.content}
4047
>
4148
{option.content || option.children}
4249
</BreadcrumbItem>
4350
);
4451
});
45-
}
52+
}, [maxItemWidth, separator]);
53+
54+
const renderEllipsis = useCallback((items: TdBreadcrumbItemProps[]) => {
55+
if (!ellipsis) {
56+
return <EllipsisIcon />;
57+
}
58+
59+
if (isFunction(ellipsis)) {
60+
return ellipsis({items, separator});
61+
}
62+
63+
return ellipsis;
64+
}, [ellipsis, separator]);
65+
66+
const content = useMemo(() => {
67+
let items = renderOptionsToItems(options) || Children.toArray(children);
68+
69+
const itemsCount = items?.length || 0;
70+
71+
if (!items || !maxItems) {
72+
return items;
73+
}
74+
75+
if (maxItems >= itemsCount) {
76+
return items;
77+
}
78+
79+
// 配置有误的情况,不显示省略并告警
80+
if (
81+
maxItems &&
82+
(!itemsBeforeCollapse || !itemsAfterCollapse)
83+
) {
84+
log.error('Breadcrumb', '需要设置 itemsBeforeCollapse 和 itemsAfterCollapse 属性来控制省略号前后的显示项数。');
85+
return items;
86+
}
87+
88+
if (
89+
maxItems &&
90+
maxItems < (itemsAfterCollapse || 0) + (itemsBeforeCollapse || 0)
91+
) {
92+
log.error('Breadcrumb', `maxItems={${maxItems}} 必须大于或等于 itemsBeforeCollapse={${itemsBeforeCollapse}} + itemsAfterCollapse={${itemsAfterCollapse}}`);
93+
return items;
94+
}
95+
96+
const startCount = itemsBeforeCollapse;
97+
const endCount = itemsAfterCollapse;
98+
99+
const collapsedItems = items.slice(startCount, itemsCount - endCount);
100+
101+
const beforeItems = items.slice(0, startCount);
102+
const afterItems = items.slice(itemsCount - endCount);
103+
104+
const ellipsis = renderEllipsis(collapsedItems.map((item) => item.props));
105+
106+
items = [
107+
...beforeItems,
108+
<BreadcrumbItem key="ellipsis" readOnly>{ellipsis}</BreadcrumbItem>,
109+
...afterItems,
110+
];
111+
112+
return items;
113+
}, [renderOptionsToItems, options, children, maxItems, itemsBeforeCollapse, itemsAfterCollapse, renderEllipsis]);
46114

47115
return (
48116
<BreadcrumbContext.Provider

packages/components/breadcrumb/BreadcrumbItem.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const BreadcrumbItem = forwardRef<HTMLDivElement, BreadcrumbItemProps>((props, r
3434
content,
3535
onClick,
3636
tooltipProps,
37+
readOnly: readonly,
3738
...restProps
3839
} = useDefaultProps<BreadcrumbItemProps>(props, breadcrumbItemDefaultProps);
3940

@@ -45,7 +46,8 @@ const BreadcrumbItem = forwardRef<HTMLDivElement, BreadcrumbItemProps>((props, r
4546
const textWrapperClassName = `${classPrefix}-breadcrumb__inner`;
4647
const textClassName = `${classPrefix}-breadcrumb__inner-text`;
4748

48-
const textClassNames = classNames(`${classPrefix}-breadcrumb--text-overflow`, {
49+
const textClassNames = classNames({
50+
[`${classPrefix}-breadcrumb--text-overflow`]: !readonly,
4951
[commonClassNames.STATUS.disabled]: disabled,
5052
});
5153
const separatorClassName = `${classPrefix}-breadcrumb__separator`;

packages/components/breadcrumb/BreadcrumbProps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export interface BreadcrumbProps extends StyledProps, React.PropsWithChildren<Td
77
export interface BreadcrumbItemProps extends StyledProps, React.PropsWithChildren<TdBreadcrumbItemProps> {
88
separator?: TdBreadcrumbProps['separator'];
99
maxItemWidth?: TdBreadcrumbProps['maxItemWidth'];
10+
readOnly?: boolean;
1011
}

packages/components/breadcrumb/__tests__/breadcrumb.test.tsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { vi, render } from '@test/utils';
33
import Breadcrumb from '../Breadcrumb';
4+
import { TdBreadcrumbItemProps } from '../type';
45

56
const { BreadcrumbItem } = Breadcrumb;
67

@@ -57,4 +58,120 @@ describe('Breadcrumb', () => {
5758
expect(node).toHaveTextContent(options[index]?.content);
5859
});
5960
});
61+
62+
test('render ellipsis breadcrumbItem correctly (components)', () => {
63+
const options = [
64+
{ content: '页面1' },
65+
{ content: '页面2' },
66+
{ content: '页面3' },
67+
{ content: '页面4' },
68+
{ content: '页面5' },
69+
{ content: '页面6' },
70+
{ content: '页面7' },
71+
{ content: '页面8' },
72+
];
73+
74+
const el = (
75+
<Breadcrumb maxItems={5} itemsBeforeCollapse={2} itemsAfterCollapse={1} data-testid={rootTestID}>
76+
{options.map((option) => (
77+
<BreadcrumbItem key={option.content}>{option.content}</BreadcrumbItem>
78+
))}
79+
</Breadcrumb>
80+
);
81+
82+
const wrapper = render(el);
83+
84+
const root = wrapper.getByTestId(rootTestID);
85+
86+
expect(root.childNodes).toHaveLength(4);
87+
expect(root.childNodes[0]).toHaveTextContent('页面1');
88+
expect(root.childNodes[1]).toHaveTextContent('页面2');
89+
expect(root.childNodes[3]).toHaveTextContent('页面8');
90+
});
91+
92+
test('render ellipsis breadcrumbItem correctly (options)', () => {
93+
const options = [
94+
{ content: '页面1' },
95+
{ content: '页面2' },
96+
{ content: '页面3' },
97+
{ content: '页面4' },
98+
{ content: '页面5' },
99+
{ content: '页面6' },
100+
{ content: '页面7' },
101+
{ content: '页面8' },
102+
];
103+
104+
const el = (
105+
<Breadcrumb maxItems={5} itemsBeforeCollapse={2} itemsAfterCollapse={1} data-testid={rootTestID} options={options} />
106+
);
107+
108+
const wrapper = render(el);
109+
110+
const root = wrapper.getByTestId(rootTestID);
111+
112+
expect(root.childNodes).toHaveLength(4);
113+
expect(root.childNodes[0]).toHaveTextContent('页面1');
114+
expect(root.childNodes[1]).toHaveTextContent('页面2');
115+
expect(root.childNodes[3]).toHaveTextContent('页面8');
116+
});
117+
118+
test('render custom ellipsis breadcrumbItem correctly (string)', () => {
119+
const options = [
120+
{ content: '页面1' },
121+
{ content: '页面2' },
122+
{ content: '页面3' },
123+
{ content: '页面4' },
124+
{ content: '页面5' },
125+
{ content: '页面6' },
126+
{ content: '页面7' },
127+
{ content: '页面8' },
128+
];
129+
130+
const el = (
131+
<Breadcrumb maxItems={5} itemsBeforeCollapse={2} itemsAfterCollapse={1} data-testid={rootTestID} options={options} ellipsis="..." />
132+
);
133+
134+
const wrapper = render(el);
135+
136+
const root = wrapper.getByTestId(rootTestID);
137+
138+
expect(root.childNodes).toHaveLength(4);
139+
expect(root.childNodes[2]).toHaveTextContent('...');
140+
});
141+
142+
test('render custom ellipsis breadcrumbItem correctly (function)', () => {
143+
const options = [
144+
{ content: '页面1' },
145+
{ content: '页面2' },
146+
{ content: '页面3' },
147+
{ content: '页面4' },
148+
{ content: '页面5' },
149+
{ content: '页面6' },
150+
{ content: '页面7' },
151+
{ content: '页面8' },
152+
];
153+
154+
const separator = '>';
155+
156+
const getEllipsis = (items: TdBreadcrumbItemProps[]) => items.map((item: any) => item.content).join(separator);
157+
158+
const el = (
159+
<Breadcrumb
160+
maxItems={5}
161+
itemsBeforeCollapse={2}
162+
itemsAfterCollapse={1}
163+
data-testid={rootTestID}
164+
separator={separator}
165+
options={options}
166+
ellipsis={({items}) => getEllipsis(items)}
167+
/>
168+
);
169+
170+
const wrapper = render(el);
171+
172+
const root = wrapper.getByTestId(rootTestID);
173+
174+
expect(root.childNodes).toHaveLength(4);
175+
expect(root.childNodes[2]).toHaveTextContent(getEllipsis(options.slice(2, 7)));
176+
});
60177
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import { Breadcrumb, Button, Dropdown } from 'tdesign-react';
3+
import { EllipsisIcon } from 'tdesign-icons-react';
4+
5+
const { BreadcrumbItem } = Breadcrumb;
6+
7+
const options = [
8+
{ content: '页面1'},
9+
{ content: '页面2'},
10+
{ content: '页面3'},
11+
{ content: '页面4'},
12+
{ content: '页面5'},
13+
];
14+
15+
export default function BreadcrumbExample() {
16+
return (
17+
<>
18+
<Breadcrumb maxItems={4} itemsBeforeCollapse={2} itemsAfterCollapse={1} ellipsis={(props) => (
19+
<Dropdown>
20+
<Button icon={<EllipsisIcon />} shape='square' variant='text' />
21+
<Dropdown.DropdownMenu>
22+
{props.items.map((item) => (
23+
<Dropdown.DropdownItem key={String(item.content)}>{item.content}</Dropdown.DropdownItem>
24+
))}
25+
</Dropdown.DropdownMenu>
26+
</Dropdown>
27+
)}>
28+
{options.map((option) => (
29+
<BreadcrumbItem key={option.content} content={option.content} />
30+
))}
31+
</Breadcrumb>
32+
33+
<Breadcrumb maxItems={4} itemsBeforeCollapse={2} itemsAfterCollapse={1} options={options} ellipsis={(props) => (
34+
<Dropdown>
35+
<Button icon={<EllipsisIcon />} shape='square' variant='text' />
36+
<Dropdown.DropdownMenu>
37+
{props.items.map((item) => (
38+
<Dropdown.DropdownItem key={String(item.content)}>{item.content}</Dropdown.DropdownItem>
39+
))}
40+
</Dropdown.DropdownMenu>
41+
</Dropdown>
42+
)} />
43+
</>
44+
);
45+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import { Breadcrumb } from 'tdesign-react';
3+
4+
const { BreadcrumbItem } = Breadcrumb;
5+
6+
const options = [
7+
{ content: '页面1'},
8+
{ content: '页面2'},
9+
{ content: '页面3'},
10+
{ content: '页面4'},
11+
{ content: '页面5'},
12+
];
13+
14+
export default function BreadcrumbExample() {
15+
return (
16+
<>
17+
<Breadcrumb maxItems={3} itemsBeforeCollapse={2} itemsAfterCollapse={1}>
18+
{options.map((option) => (
19+
<BreadcrumbItem key={option.content} content={option.content} />
20+
))}
21+
</Breadcrumb>
22+
23+
<Breadcrumb maxItems={3} itemsBeforeCollapse={2} itemsAfterCollapse={1} options={options} />
24+
</>
25+
);
26+
}

packages/components/breadcrumb/type.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,26 @@ import { TNode, TElement } from '../common';
99
import { MouseEvent } from 'react';
1010

1111
export interface TdBreadcrumbProps {
12+
/**
13+
* 自定义折叠时省略号的内容
14+
*/
15+
ellipsis?: string | TNode<{ items: Array<TdBreadcrumbItemProps>; separator: TdBreadcrumbProps['separator'] }>;
16+
/**
17+
* 超过面包屑最大显示数量时,省略号后显示几项。`maxItems > 0`时有效
18+
*/
19+
itemsAfterCollapse?: number;
20+
/**
21+
* 超过面包屑最大显示数量时,省略号前显示几项。`maxItems > 0`时有效
22+
*/
23+
itemsBeforeCollapse?: number;
1224
/**
1325
* 单项最大宽度,超出后会以省略号形式呈现
1426
*/
1527
maxItemWidth?: string;
28+
/**
29+
* 显示的面包屑的最大数量,超出该值后中间的面包屑内容将会显示为省略号。值`<= 0`代表不限制
30+
*/
31+
maxItems?: number;
1632
/**
1733
* 面包屑项,功能同 BreadcrumbItem
1834
*/

0 commit comments

Comments
 (0)