refactor: Breadcrumb (#4175)

pull/4188/head
tangjinzhou 2021-06-08 21:54:26 +08:00 committed by GitHub
parent 7e1301ad7f
commit 6cff37bb03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 197 additions and 123 deletions

View File

@ -1,11 +1,11 @@
import { inject, cloneVNode, defineComponent, PropType } from 'vue'; import { cloneVNode, defineComponent, PropType, ExtractPropTypes } from 'vue';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { filterEmpty, getComponent, getSlot } from '../_util/props-util'; import { flattenChildren, getPropsSlot } from '../_util/props-util';
import warning from '../_util/warning'; import warning from '../_util/warning';
import { defaultConfigProvider } from '../config-provider';
import BreadcrumbItem from './BreadcrumbItem'; import BreadcrumbItem from './BreadcrumbItem';
import Menu from '../menu'; import Menu from '../menu';
import { Omit, VueNode } from '../_util/type'; import { Omit, VueNode } from '../_util/type';
import useConfigInject from '../_util/hooks/useConfigInject';
export interface Route { export interface Route {
path: string; path: string;
@ -13,11 +13,11 @@ export interface Route {
children?: Omit<Route, 'children'>[]; children?: Omit<Route, 'children'>[];
} }
const BreadcrumbProps = { const breadcrumbProps = {
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
routes: { type: Array as PropType<Route[]> }, routes: { type: Array as PropType<Route[]> },
params: PropTypes.any, params: PropTypes.any,
separator: PropTypes.VNodeChild, separator: PropTypes.any,
itemRender: { itemRender: {
type: Function as PropType< type: Function as PropType<
(opt: { route: Route; params: unknown; routes: Route[]; paths: string[] }) => VueNode (opt: { route: Route; params: unknown; routes: Route[]; paths: string[] }) => VueNode
@ -25,6 +25,8 @@ const BreadcrumbProps = {
}, },
}; };
export type BreadcrumbProps = Partial<ExtractPropTypes<typeof breadcrumbProps>>;
function getBreadcrumbName(route: Route, params: unknown) { function getBreadcrumbName(route: Route, params: unknown) {
if (!route.breadcrumbName) { if (!route.breadcrumbName) {
return null; return null;
@ -50,34 +52,37 @@ function defaultItemRender(opt: {
export default defineComponent({ export default defineComponent({
name: 'ABreadcrumb', name: 'ABreadcrumb',
props: BreadcrumbProps, props: breadcrumbProps,
setup() { slots: ['separator', 'itemRender'],
return { setup(props, { slots }) {
configProvider: inject('configProvider', defaultConfigProvider), const { prefixCls, direction } = useConfigInject('breadcrumb', props);
};
}, const getPath = (path: string, params: unknown) => {
methods: {
getPath(path: string, params: unknown) {
path = (path || '').replace(/^\//, ''); path = (path || '').replace(/^\//, '');
Object.keys(params).forEach(key => { Object.keys(params).forEach(key => {
path = path.replace(`:${key}`, params[key]); path = path.replace(`:${key}`, params[key]);
}); });
return path; return path;
}, };
addChildPath(paths: string[], childPath = '', params: unknown) { const addChildPath = (paths: string[], childPath = '', params: unknown) => {
const originalPaths = [...paths]; const originalPaths = [...paths];
const path = this.getPath(childPath, params); const path = getPath(childPath, params);
if (path) { if (path) {
originalPaths.push(path); originalPaths.push(path);
} }
return originalPaths; return originalPaths;
}, };
genForRoutes({ routes = [], params = {}, separator, itemRender = defaultItemRender }: any) { const genForRoutes = ({
routes = [],
params = {},
separator,
itemRender = defaultItemRender,
}: any) => {
const paths = []; const paths = [];
return routes.map((route: Route) => { return routes.map((route: Route) => {
const path = this.getPath(route.path, params); const path = getPath(route.path, params);
if (path) { if (path) {
paths.push(path); paths.push(path);
@ -94,7 +99,7 @@ export default defineComponent({
route: child, route: child,
params, params,
routes, routes,
paths: this.addChildPath(tempPaths, child.path, params), paths: addChildPath(tempPaths, child.path, params),
})} })}
</Menu.Item> </Menu.Item>
))} ))}
@ -112,36 +117,41 @@ export default defineComponent({
</BreadcrumbItem> </BreadcrumbItem>
); );
}); });
}, };
}, return () => {
render() { let crumbs: VueNode[];
let crumbs: VueNode[];
const { prefixCls: customizePrefixCls, routes, params = {}, $slots } = this;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
const children = filterEmpty(getSlot(this)); const { routes, params = {} } = props;
const separator = getComponent(this, 'separator');
const itemRender = this.itemRender || $slots.itemRender || defaultItemRender; const children = flattenChildren(getPropsSlot(slots, props));
if (routes && routes.length > 0) { const separator = getPropsSlot(slots, props, 'separator') ?? '/';
// generated by route
crumbs = this.genForRoutes({ const itemRender = props.itemRender || slots.itemRender || defaultItemRender;
routes, if (routes && routes.length > 0) {
params, // generated by route
separator, crumbs = genForRoutes({
itemRender, routes,
}); params,
} else if (children.length) { separator,
crumbs = children.map((element, index) => { itemRender,
warning( });
typeof element.type === 'object' && } else if (children.length) {
(element.type.__ANT_BREADCRUMB_ITEM || element.type.__ANT_BREADCRUMB_SEPARATOR), crumbs = children.map((element, index) => {
'Breadcrumb', warning(
"Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children", typeof element.type === 'object' &&
); (element.type.__ANT_BREADCRUMB_ITEM || element.type.__ANT_BREADCRUMB_SEPARATOR),
return cloneVNode(element, { separator, key: index }); 'Breadcrumb',
}); "Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children",
} );
return <div class={prefixCls}>{crumbs}</div>; return cloneVNode(element, { separator, key: index });
});
}
const breadcrumbClassName = {
[prefixCls.value]: true,
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
};
return <div class={breadcrumbClassName}>{crumbs}</div>;
};
}, },
}); });

View File

@ -1,31 +1,31 @@
import { defineComponent, inject } from 'vue'; import { defineComponent, ExtractPropTypes } from 'vue';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { hasProp, getComponent, getSlot } from '../_util/props-util'; import { getPropsSlot } from '../_util/props-util';
import { defaultConfigProvider } from '../config-provider';
import DropDown from '../dropdown/dropdown'; import DropDown from '../dropdown/dropdown';
import DownOutlined from '@ant-design/icons-vue/DownOutlined'; import DownOutlined from '@ant-design/icons-vue/DownOutlined';
import useConfigInject from '../_util/hooks/useConfigInject';
const breadcrumbItemProps = {
prefixCls: PropTypes.string,
href: PropTypes.string,
separator: PropTypes.any,
overlay: PropTypes.any,
};
export type BreadcrumbItemProps = Partial<ExtractPropTypes<typeof breadcrumbItemProps>>;
export default defineComponent({ export default defineComponent({
name: 'ABreadcrumbItem', name: 'ABreadcrumbItem',
__ANT_BREADCRUMB_ITEM: true, __ANT_BREADCRUMB_ITEM: true,
props: { props: breadcrumbItemProps,
prefixCls: PropTypes.string, slots: ['separator', 'overlay'],
href: PropTypes.string, setup(props, { slots }) {
separator: PropTypes.VNodeChild.def('/'), const { prefixCls } = useConfigInject('breadcrumb', props);
overlay: PropTypes.VNodeChild,
},
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
};
},
methods: {
/** /**
* if overlay is have * if overlay is have
* Wrap a DropDown * Wrap a DropDown
*/ */
renderBreadcrumbNode(breadcrumbItem: JSX.Element, prefixCls: string) { const renderBreadcrumbNode = (breadcrumbItem: JSX.Element, prefixCls: string) => {
const overlay = getComponent(this, 'overlay'); const overlay = getPropsSlot(slots, props, 'overlay');
if (overlay) { if (overlay) {
return ( return (
<DropDown overlay={overlay} placement="bottomCenter"> <DropDown overlay={overlay} placement="bottomCenter">
@ -37,32 +37,29 @@ export default defineComponent({
); );
} }
return breadcrumbItem; return breadcrumbItem;
}, };
},
render() { return () => {
const { prefixCls: customizePrefixCls } = this; const separator = getPropsSlot(slots, props, 'separator') ?? '/';
const getPrefixCls = this.configProvider.getPrefixCls; const children = getPropsSlot(slots, props);
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls); let link: JSX.Element;
const separator = getComponent(this, 'separator');
const children = getSlot(this); if (props.href !== undefined) {
let link: JSX.Element; link = <a class={`${prefixCls.value}-link`}>{children}</a>;
if (hasProp(this, 'href')) { } else {
link = <a class={`${prefixCls}-link`}>{children}</a>; link = <span class={`${prefixCls.value}-link`}>{children}</span>;
} else { }
link = <span class={`${prefixCls}-link`}>{children}</span>; // wrap to dropDown
} link = renderBreadcrumbNode(link, prefixCls.value);
// wrap to dropDown if (children) {
link = this.renderBreadcrumbNode(link, prefixCls); return (
if (children) { <span>
return ( {link}
<span> {separator && <span class={`${prefixCls.value}-separator`}>{separator}</span>}
{link} </span>
{separator && separator !== '' && ( );
<span class={`${prefixCls}-separator`}>{separator}</span> }
)} return null;
</span> };
);
}
return null;
}, },
}); });

View File

@ -1,31 +1,29 @@
import { defineComponent, inject } from 'vue'; import { defineComponent, ExtractPropTypes } from 'vue';
import { defaultConfigProvider } from '../config-provider';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { getSlot } from '../_util/props-util'; import { flattenChildren } from '../_util/props-util';
import useConfigInject from '../_util/hooks/useConfigInject';
const breadcrumbSeparator = {
prefixCls: PropTypes.string,
};
export type BreadcrumbSeparator = Partial<ExtractPropTypes<typeof breadcrumbSeparator>>;
export default defineComponent({ export default defineComponent({
name: 'ABreadcrumbSeparator', name: 'ABreadcrumbSeparator',
__ANT_BREADCRUMB_SEPARATOR: true, __ANT_BREADCRUMB_SEPARATOR: true,
inheritAttrs: false, inheritAttrs: false,
props: { props: breadcrumbSeparator,
prefixCls: PropTypes.string, setup(props, { slots, attrs }) {
}, const { prefixCls } = useConfigInject('breadcrumb', props);
setup() {
return { return () => {
configProvider: inject('configProvider', defaultConfigProvider), const { separator, class: className, ...restAttrs } = attrs;
const children = flattenChildren(slots.default?.());
return (
<span class={[`${prefixCls.value}-separator`, className]} {...restAttrs}>
{children.length > 0 ? children : '/'}
</span>
);
}; };
}, },
render() {
const { prefixCls: customizePrefixCls } = this;
const { separator, class: className, ...restAttrs } = this.$attrs;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
const children = getSlot(this);
return (
<span class={[`${prefixCls}-separator`, className]} {...restAttrs}>
{children.length > 0 ? children : '/'}
</span>
);
},
}); });

View File

@ -107,4 +107,25 @@ describe('Breadcrumb', () => {
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
// https://github.com/ant-design/ant-design/issues/25975
it('should support Breadcrumb.Item default separator', () => {
const MockComponent = () => (
<span>
<Breadcrumb.Item>Mock Node</Breadcrumb.Item>
</span>
);
const wrapper = mount({
render() {
return (
<Breadcrumb>
<Breadcrumb.Item>Location</Breadcrumb.Item>
<MockComponent />
<Breadcrumb.Item>Application Center</Breadcrumb.Item>
</Breadcrumb>
);
},
});
expect(wrapper.html()).toMatchSnapshot();
});
}); });

View File

@ -2,8 +2,14 @@
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>`; 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>`;
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>`; exports[`Breadcrumb should not display Breadcrumb Item when its children is falsy 1`] = `
<div class="ant-breadcrumb">
<!----><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>
`;
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>`; 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>`;
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>`;
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>`; 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>`;

View File

@ -3,6 +3,10 @@ import Breadcrumb from './Breadcrumb';
import BreadcrumbItem from './BreadcrumbItem'; import BreadcrumbItem from './BreadcrumbItem';
import BreadcrumbSeparator from './BreadcrumbSeparator'; import BreadcrumbSeparator from './BreadcrumbSeparator';
export { BreadcrumbProps } from './Breadcrumb';
export { BreadcrumbItemProps } from './BreadcrumbItem';
export { BreadcrumbSeparator } from './BreadcrumbSeparator';
Breadcrumb.Item = BreadcrumbItem; Breadcrumb.Item = BreadcrumbItem;
Breadcrumb.Separator = BreadcrumbSeparator; Breadcrumb.Separator = BreadcrumbSeparator;

View File

@ -38,7 +38,8 @@
} }
&-link { &-link {
> .@{iconfont-css-prefix} + span { > .@{iconfont-css-prefix} + span,
> .@{iconfont-css-prefix} + a {
margin-left: 4px; margin-left: 4px;
} }
} }
@ -49,3 +50,5 @@
} }
} }
} }
@import './rtl';

View File

@ -0,0 +1,29 @@
.@{breadcrumb-prefix-cls} {
&-rtl {
.clearfix();
direction: rtl;
> span {
float: right;
}
}
&-link {
> .@{iconfont-css-prefix} + span,
> .@{iconfont-css-prefix} + a {
.@{breadcrumb-prefix-cls}-rtl & {
margin-right: 4px;
margin-left: 0;
}
}
}
&-overlay-link {
> .@{iconfont-css-prefix} {
.@{breadcrumb-prefix-cls}-rtl & {
margin-right: 4px;
margin-left: 0;
}
}
}
}

View File

@ -1,10 +1,8 @@
import Menu from '../../menu'; import Menu, { Item as MenuItem } from '../../menu';
import PropTypes from '../../_util/vue-types'; import PropTypes from '../../_util/vue-types';
import { OptionProps } from './Option'; import { OptionProps } from './Option';
import { inject } from 'vue'; import { inject } from 'vue';
const MenuItem = Menu.Item;
function noop() {} function noop() {}
export default { export default {
name: 'DropdownMenu', name: 'DropdownMenu',

View File

@ -1,6 +1,7 @@
import '@babel/polyfill'; import '@babel/polyfill';
import 'ant-design-vue/style'; import 'ant-design-vue/style';
import { createApp, version } from 'vue'; import { createApp, version } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue'; import App from './App.vue';
import antd from 'ant-design-vue/index.ts'; import antd from 'ant-design-vue/index.ts';
@ -9,9 +10,16 @@ console.log('Vue version: ', version);
const basic = (_, { slots }) => { const basic = (_, { slots }) => {
return slots && slots.default && slots.default(); return slots && slots.default && slots.default();
}; };
const router = createRouter({
history: createWebHistory(),
fallback: false,
routes: [],
});
const app = createApp(App); const app = createApp(App);
app.use(router);
app app
.component('demo-sort', basic) .component('DemoSort', basic)
.component('md', basic) .component('md', basic)
.component('api', basic) .component('api', basic)
.component('CN', basic) .component('CN', basic)

2
v2-doc

@ -1 +1 @@
Subproject commit 6819090fbcc94b248bc761d5f26162f29c04b2ef Subproject commit 0f6d531d088d5283250c8cec1c7e8be0e0d36a36