Skip to content

Commit 6cff37b

Browse files
authored
refactor: Breadcrumb (#4175)
1 parent 7e1301a commit 6cff37b

File tree

11 files changed

+197
-123
lines changed

11 files changed

+197
-123
lines changed

components/breadcrumb/Breadcrumb.tsx

+60-50
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
1-
import { inject, cloneVNode, defineComponent, PropType } from 'vue';
1+
import { cloneVNode, defineComponent, PropType, ExtractPropTypes } from 'vue';
22
import PropTypes from '../_util/vue-types';
3-
import { filterEmpty, getComponent, getSlot } from '../_util/props-util';
3+
import { flattenChildren, getPropsSlot } from '../_util/props-util';
44
import warning from '../_util/warning';
5-
import { defaultConfigProvider } from '../config-provider';
65
import BreadcrumbItem from './BreadcrumbItem';
76
import Menu from '../menu';
87
import { Omit, VueNode } from '../_util/type';
8+
import useConfigInject from '../_util/hooks/useConfigInject';
99

1010
export interface Route {
1111
path: string;
1212
breadcrumbName: string;
1313
children?: Omit<Route, 'children'>[];
1414
}
1515

16-
const BreadcrumbProps = {
16+
const breadcrumbProps = {
1717
prefixCls: PropTypes.string,
1818
routes: { type: Array as PropType<Route[]> },
1919
params: PropTypes.any,
20-
separator: PropTypes.VNodeChild,
20+
separator: PropTypes.any,
2121
itemRender: {
2222
type: Function as PropType<
2323
(opt: { route: Route; params: unknown; routes: Route[]; paths: string[] }) => VueNode
2424
>,
2525
},
2626
};
2727

28+
export type BreadcrumbProps = Partial<ExtractPropTypes<typeof breadcrumbProps>>;
29+
2830
function getBreadcrumbName(route: Route, params: unknown) {
2931
if (!route.breadcrumbName) {
3032
return null;
@@ -50,34 +52,37 @@ function defaultItemRender(opt: {
5052

5153
export default defineComponent({
5254
name: 'ABreadcrumb',
53-
props: BreadcrumbProps,
54-
setup() {
55-
return {
56-
configProvider: inject('configProvider', defaultConfigProvider),
57-
};
58-
},
59-
methods: {
60-
getPath(path: string, params: unknown) {
55+
props: breadcrumbProps,
56+
slots: ['separator', 'itemRender'],
57+
setup(props, { slots }) {
58+
const { prefixCls, direction } = useConfigInject('breadcrumb', props);
59+
60+
const getPath = (path: string, params: unknown) => {
6161
path = (path || '').replace(/^\//, '');
6262
Object.keys(params).forEach(key => {
6363
path = path.replace(`:${key}`, params[key]);
6464
});
6565
return path;
66-
},
66+
};
6767

68-
addChildPath(paths: string[], childPath = '', params: unknown) {
68+
const addChildPath = (paths: string[], childPath = '', params: unknown) => {
6969
const originalPaths = [...paths];
70-
const path = this.getPath(childPath, params);
70+
const path = getPath(childPath, params);
7171
if (path) {
7272
originalPaths.push(path);
7373
}
7474
return originalPaths;
75-
},
75+
};
7676

77-
genForRoutes({ routes = [], params = {}, separator, itemRender = defaultItemRender }: any) {
77+
const genForRoutes = ({
78+
routes = [],
79+
params = {},
80+
separator,
81+
itemRender = defaultItemRender,
82+
}: any) => {
7883
const paths = [];
7984
return routes.map((route: Route) => {
80-
const path = this.getPath(route.path, params);
85+
const path = getPath(route.path, params);
8186

8287
if (path) {
8388
paths.push(path);
@@ -94,7 +99,7 @@ export default defineComponent({
9499
route: child,
95100
params,
96101
routes,
97-
paths: this.addChildPath(tempPaths, child.path, params),
102+
paths: addChildPath(tempPaths, child.path, params),
98103
})}
99104
</Menu.Item>
100105
))}
@@ -112,36 +117,41 @@ export default defineComponent({
112117
</BreadcrumbItem>
113118
);
114119
});
115-
},
116-
},
117-
render() {
118-
let crumbs: VueNode[];
119-
const { prefixCls: customizePrefixCls, routes, params = {}, $slots } = this;
120-
const getPrefixCls = this.configProvider.getPrefixCls;
121-
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
120+
};
121+
return () => {
122+
let crumbs: VueNode[];
122123

123-
const children = filterEmpty(getSlot(this));
124-
const separator = getComponent(this, 'separator');
125-
const itemRender = this.itemRender || $slots.itemRender || defaultItemRender;
126-
if (routes && routes.length > 0) {
127-
// generated by route
128-
crumbs = this.genForRoutes({
129-
routes,
130-
params,
131-
separator,
132-
itemRender,
133-
});
134-
} else if (children.length) {
135-
crumbs = children.map((element, index) => {
136-
warning(
137-
typeof element.type === 'object' &&
138-
(element.type.__ANT_BREADCRUMB_ITEM || element.type.__ANT_BREADCRUMB_SEPARATOR),
139-
'Breadcrumb',
140-
"Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children",
141-
);
142-
return cloneVNode(element, { separator, key: index });
143-
});
144-
}
145-
return <div class={prefixCls}>{crumbs}</div>;
124+
const { routes, params = {} } = props;
125+
126+
const children = flattenChildren(getPropsSlot(slots, props));
127+
const separator = getPropsSlot(slots, props, 'separator') ?? '/';
128+
129+
const itemRender = props.itemRender || slots.itemRender || defaultItemRender;
130+
if (routes && routes.length > 0) {
131+
// generated by route
132+
crumbs = genForRoutes({
133+
routes,
134+
params,
135+
separator,
136+
itemRender,
137+
});
138+
} else if (children.length) {
139+
crumbs = children.map((element, index) => {
140+
warning(
141+
typeof element.type === 'object' &&
142+
(element.type.__ANT_BREADCRUMB_ITEM || element.type.__ANT_BREADCRUMB_SEPARATOR),
143+
'Breadcrumb',
144+
"Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children",
145+
);
146+
return cloneVNode(element, { separator, key: index });
147+
});
148+
}
149+
150+
const breadcrumbClassName = {
151+
[prefixCls.value]: true,
152+
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
153+
};
154+
return <div class={breadcrumbClassName}>{crumbs}</div>;
155+
};
146156
},
147157
});
+41-44
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
1-
import { defineComponent, inject } from 'vue';
1+
import { defineComponent, ExtractPropTypes } from 'vue';
22
import PropTypes from '../_util/vue-types';
3-
import { hasProp, getComponent, getSlot } from '../_util/props-util';
4-
import { defaultConfigProvider } from '../config-provider';
3+
import { getPropsSlot } from '../_util/props-util';
54
import DropDown from '../dropdown/dropdown';
65
import DownOutlined from '@ant-design/icons-vue/DownOutlined';
6+
import useConfigInject from '../_util/hooks/useConfigInject';
77

8+
const breadcrumbItemProps = {
9+
prefixCls: PropTypes.string,
10+
href: PropTypes.string,
11+
separator: PropTypes.any,
12+
overlay: PropTypes.any,
13+
};
14+
15+
export type BreadcrumbItemProps = Partial<ExtractPropTypes<typeof breadcrumbItemProps>>;
816
export default defineComponent({
917
name: 'ABreadcrumbItem',
1018
__ANT_BREADCRUMB_ITEM: true,
11-
props: {
12-
prefixCls: PropTypes.string,
13-
href: PropTypes.string,
14-
separator: PropTypes.VNodeChild.def('/'),
15-
overlay: PropTypes.VNodeChild,
16-
},
17-
setup() {
18-
return {
19-
configProvider: inject('configProvider', defaultConfigProvider),
20-
};
21-
},
22-
methods: {
19+
props: breadcrumbItemProps,
20+
slots: ['separator', 'overlay'],
21+
setup(props, { slots }) {
22+
const { prefixCls } = useConfigInject('breadcrumb', props);
2323
/**
2424
* if overlay is have
2525
* Wrap a DropDown
2626
*/
27-
renderBreadcrumbNode(breadcrumbItem: JSX.Element, prefixCls: string) {
28-
const overlay = getComponent(this, 'overlay');
27+
const renderBreadcrumbNode = (breadcrumbItem: JSX.Element, prefixCls: string) => {
28+
const overlay = getPropsSlot(slots, props, 'overlay');
2929
if (overlay) {
3030
return (
3131
<DropDown overlay={overlay} placement="bottomCenter">
@@ -37,32 +37,29 @@ export default defineComponent({
3737
);
3838
}
3939
return breadcrumbItem;
40-
},
41-
},
42-
render() {
43-
const { prefixCls: customizePrefixCls } = this;
44-
const getPrefixCls = this.configProvider.getPrefixCls;
45-
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
46-
const separator = getComponent(this, 'separator');
47-
const children = getSlot(this);
48-
let link: JSX.Element;
49-
if (hasProp(this, 'href')) {
50-
link = <a class={`${prefixCls}-link`}>{children}</a>;
51-
} else {
52-
link = <span class={`${prefixCls}-link`}>{children}</span>;
53-
}
54-
// wrap to dropDown
55-
link = this.renderBreadcrumbNode(link, prefixCls);
56-
if (children) {
57-
return (
58-
<span>
59-
{link}
60-
{separator && separator !== '' && (
61-
<span class={`${prefixCls}-separator`}>{separator}</span>
62-
)}
63-
</span>
64-
);
65-
}
66-
return null;
40+
};
41+
42+
return () => {
43+
const separator = getPropsSlot(slots, props, 'separator') ?? '/';
44+
const children = getPropsSlot(slots, props);
45+
let link: JSX.Element;
46+
47+
if (props.href !== undefined) {
48+
link = <a class={`${prefixCls.value}-link`}>{children}</a>;
49+
} else {
50+
link = <span class={`${prefixCls.value}-link`}>{children}</span>;
51+
}
52+
// wrap to dropDown
53+
link = renderBreadcrumbNode(link, prefixCls.value);
54+
if (children) {
55+
return (
56+
<span>
57+
{link}
58+
{separator && <span class={`${prefixCls.value}-separator`}>{separator}</span>}
59+
</span>
60+
);
61+
}
62+
return null;
63+
};
6764
},
6865
});
+20-22
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,29 @@
1-
import { defineComponent, inject } from 'vue';
2-
import { defaultConfigProvider } from '../config-provider';
1+
import { defineComponent, ExtractPropTypes } from 'vue';
32
import PropTypes from '../_util/vue-types';
4-
import { getSlot } from '../_util/props-util';
3+
import { flattenChildren } from '../_util/props-util';
4+
import useConfigInject from '../_util/hooks/useConfigInject';
5+
6+
const breadcrumbSeparator = {
7+
prefixCls: PropTypes.string,
8+
};
9+
export type BreadcrumbSeparator = Partial<ExtractPropTypes<typeof breadcrumbSeparator>>;
510

611
export default defineComponent({
712
name: 'ABreadcrumbSeparator',
813
__ANT_BREADCRUMB_SEPARATOR: true,
914
inheritAttrs: false,
10-
props: {
11-
prefixCls: PropTypes.string,
12-
},
13-
setup() {
14-
return {
15-
configProvider: inject('configProvider', defaultConfigProvider),
16-
};
17-
},
18-
render() {
19-
const { prefixCls: customizePrefixCls } = this;
20-
const { separator, class: className, ...restAttrs } = this.$attrs;
21-
const getPrefixCls = this.configProvider.getPrefixCls;
22-
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
15+
props: breadcrumbSeparator,
16+
setup(props, { slots, attrs }) {
17+
const { prefixCls } = useConfigInject('breadcrumb', props);
2318

24-
const children = getSlot(this);
25-
return (
26-
<span class={[`${prefixCls}-separator`, className]} {...restAttrs}>
27-
{children.length > 0 ? children : '/'}
28-
</span>
29-
);
19+
return () => {
20+
const { separator, class: className, ...restAttrs } = attrs;
21+
const children = flattenChildren(slots.default?.());
22+
return (
23+
<span class={[`${prefixCls.value}-separator`, className]} {...restAttrs}>
24+
{children.length > 0 ? children : '/'}
25+
</span>
26+
);
27+
};
3028
},
3129
});

components/breadcrumb/__tests__/Breadcrumb.test.js

+21
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,25 @@ describe('Breadcrumb', () => {
107107
});
108108
expect(wrapper.html()).toMatchSnapshot();
109109
});
110+
111+
// https://github.com/ant-design/ant-design/issues/25975
112+
it('should support Breadcrumb.Item default separator', () => {
113+
const MockComponent = () => (
114+
<span>
115+
<Breadcrumb.Item>Mock Node</Breadcrumb.Item>
116+
</span>
117+
);
118+
const wrapper = mount({
119+
render() {
120+
return (
121+
<Breadcrumb>
122+
<Breadcrumb.Item>Location</Breadcrumb.Item>
123+
<MockComponent />
124+
<Breadcrumb.Item>Application Center</Breadcrumb.Item>
125+
</Breadcrumb>
126+
);
127+
},
128+
});
129+
expect(wrapper.html()).toMatchSnapshot();
130+
});
110131
});

components/breadcrumb/__tests__/__snapshots__/Breadcrumb.test.js.snap

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

33
exports[`Breadcrumb should allow Breadcrumb.Item is null or undefined 1`] = `<div class="ant-breadcrumb"><span><span class="ant-breadcrumb-link">Home</span><span class="ant-breadcrumb-separator">/</span></span></div>`;
44

5-
exports[`Breadcrumb should not display Breadcrumb Item when its children is falsy 1`] = `<div class="ant-breadcrumb"><span><span class="ant-breadcrumb-link"></span><span class="ant-breadcrumb-separator">/</span></span><span><span class="ant-breadcrumb-link">xxx</span><span class="ant-breadcrumb-separator">/</span></span><span><span class="ant-breadcrumb-link">yyy</span><span class="ant-breadcrumb-separator">/</span></span></div>`;
5+
exports[`Breadcrumb should not display Breadcrumb Item when its children is falsy 1`] = `
6+
<div class="ant-breadcrumb">
7+
<!----><span><span class="ant-breadcrumb-link">xxx</span><span class="ant-breadcrumb-separator">/</span></span><span><span class="ant-breadcrumb-link">yyy</span><span class="ant-breadcrumb-separator">/</span></span>
8+
</div>
9+
`;
610

711
exports[`Breadcrumb should render a menu 1`] = `<div class="ant-breadcrumb"><span><span class="ant-breadcrumb-link"><a href="#/index">home</a></span><span class="ant-breadcrumb-separator">/</span></span><span><!----><span class="ant-breadcrumb-overlay-link ant-dropdown-trigger"><span class="ant-breadcrumb-link"><a href="#/index/first">first</a></span><span role="img" aria-label="down" class="anticon anticon-down"><svg class="" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></span></span><span class="ant-breadcrumb-separator">/</span></span><span><span class="ant-breadcrumb-link"><span>second</span></span><span class="ant-breadcrumb-separator">/</span></span></div>`;
812

13+
exports[`Breadcrumb should support Breadcrumb.Item default separator 1`] = `<div class="ant-breadcrumb"><span><span class="ant-breadcrumb-link">Location</span><span class="ant-breadcrumb-separator">/</span></span><span><span><span class="ant-breadcrumb-link">Mock Node</span><span class="ant-breadcrumb-separator">/</span></span></span><span><span class="ant-breadcrumb-link">Application Center</span><span class="ant-breadcrumb-separator">/</span></span></div>`;
14+
915
exports[`Breadcrumb should support custom attribute 1`] = `<div class="ant-breadcrumb" data-custom="custom"><span data-custom="custom-item"><span class="ant-breadcrumb-link">xxx</span><span class="ant-breadcrumb-separator">/</span></span><span><span class="ant-breadcrumb-link">yyy</span><span class="ant-breadcrumb-separator">/</span></span></div>`;

components/breadcrumb/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import Breadcrumb from './Breadcrumb';
33
import BreadcrumbItem from './BreadcrumbItem';
44
import BreadcrumbSeparator from './BreadcrumbSeparator';
55

6+
export { BreadcrumbProps } from './Breadcrumb';
7+
export { BreadcrumbItemProps } from './BreadcrumbItem';
8+
export { BreadcrumbSeparator } from './BreadcrumbSeparator';
9+
610
Breadcrumb.Item = BreadcrumbItem;
711
Breadcrumb.Separator = BreadcrumbSeparator;
812

components/breadcrumb/style/index.less

+4-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
}
3939

4040
&-link {
41-
> .@{iconfont-css-prefix} + span {
41+
> .@{iconfont-css-prefix} + span,
42+
> .@{iconfont-css-prefix} + a {
4243
margin-left: 4px;
4344
}
4445
}
@@ -49,3 +50,5 @@
4950
}
5051
}
5152
}
53+
54+
@import './rtl';

0 commit comments

Comments
 (0)