feat: update breadcrumb

pull/1790/head
tangjinzhou 2020-02-09 15:09:15 +08:00
parent 7b2fdd9051
commit a1cfa8962d
17 changed files with 370 additions and 43 deletions

View File

@ -1,5 +1,5 @@
module.exports = {
dev: {
componentName: 'button', // dev components
componentName: 'breadcrumb', // dev components
},
};

View File

@ -4,10 +4,12 @@ import { filterEmpty, getComponentFromProp, getSlotOptions } from '../_util/prop
import warning from '../_util/warning';
import { ConfigConsumerProps } from '../config-provider';
import BreadcrumbItem from './BreadcrumbItem';
import Menu from '../menu';
const Route = PropTypes.shape({
path: PropTypes.string,
breadcrumbName: PropTypes.string,
children: PropTypes.array,
}).loose;
const BreadcrumbProps = {
@ -42,6 +44,62 @@ export default {
const name = getBreadcrumbName(route, params);
return isLastItem ? <span>{name}</span> : <a href={`#/${paths.join('/')}`}>{name}</a>;
},
getPath(path, params) {
path = (path || '').replace(/^\//, '');
Object.keys(params).forEach(key => {
path = path.replace(`:${key}`, params[key]);
});
return path;
},
addChildPath(paths, childPath, params) {
const originalPaths = [...paths];
const path = this.getPath(childPath, params);
if (path) {
originalPaths.push(path);
}
return originalPaths;
},
genForRoutes({ routes = [], params = {}, separator, itemRender = defaultItemRender }) {
const paths = [];
return routes.map(route => {
const path = this.getPath(route.path, params);
if (path) {
paths.push(path);
}
// generated overlay by route.children
let overlay = null;
if (route.children && route.children.length) {
overlay = (
<Menu>
{route.children.map(child => (
<Menu.Item key={child.breadcrumbName || child.path}>
{itemRender({
route: child,
params,
routes,
paths: this.addChildPath(paths, child.path, params),
h: this.$createElement,
})}
</Menu.Item>
))}
</Menu>
);
}
return (
<BreadcrumbItem
overlay={overlay}
separator={separator}
key={route.breadcrumbName || path}
>
{itemRender({ route, params, routes, paths, h: this.$createElement })}
</BreadcrumbItem>
);
});
},
},
render() {
let crumbs;
@ -51,29 +109,22 @@ export default {
const children = filterEmpty($slots.default);
const separator = getComponentFromProp(this, 'separator');
const itemRender = this.itemRender || $scopedSlots.itemRender || this.defaultItemRender;
if (routes && routes.length > 0) {
const paths = [];
const itemRender = this.itemRender || $scopedSlots.itemRender || this.defaultItemRender;
crumbs = routes.map(route => {
route.path = route.path || '';
let path = route.path.replace(/^\//, '');
Object.keys(params).forEach(key => {
path = path.replace(`:${key}`, params[key]);
});
if (path) {
paths.push(path);
}
return (
<BreadcrumbItem separator={separator} key={route.breadcrumbName || path}>
{itemRender({ route, params, routes, paths, h })}
</BreadcrumbItem>
);
// generated by route
crumbs = this.genForRoutes({
routes,
params,
separator,
itemRender,
});
} else if (children.length) {
crumbs = children.map((element, index) => {
warning(
getSlotOptions(element).__ANT_BREADCRUMB_ITEM,
"Breadcrumb only accepts Breadcrumb.Item as it's children",
getSlotOptions(element).__ANT_BREADCRUMB_ITEM ||
getSlotOptions(element).__ANT_BREADCRUMB_SEPARATOR,
'Breadcrumb',
"Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children",
);
return cloneElement(element, {
props: { separator },

View File

@ -1,6 +1,8 @@
import PropTypes from '../_util/vue-types';
import { hasProp, getComponentFromProp } from '../_util/props-util';
import { ConfigConsumerProps } from '../config-provider';
import DropDown from '../dropdown/dropdown';
import Icon from '../icon';
export default {
name: 'ABreadcrumbItem',
@ -8,16 +10,37 @@ export default {
props: {
prefixCls: PropTypes.string,
href: PropTypes.string,
separator: PropTypes.any,
separator: PropTypes.any.def('/'),
overlay: PropTypes.any,
},
inject: {
configProvider: { default: () => ConfigConsumerProps },
},
methods: {
/**
* if overlay is have
* Wrap a DropDown
*/
renderBreadcrumbNode(breadcrumbItem, prefixCls) {
const overlay = getComponentFromProp(this, 'overlay');
if (overlay) {
return (
<DropDown overlay={overlay} placement="bottomCenter">
<span class={`${prefixCls}-overlay-link`}>
{breadcrumbItem}
<Icon type="down" />
</span>
</DropDown>
);
}
return breadcrumbItem;
},
},
render() {
const { prefixCls: customizePrefixCls, $slots } = this;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
const separator = getComponentFromProp(this, 'separator');
const children = $slots.default;
let link;
if (hasProp(this, 'href')) {
@ -25,13 +48,15 @@ export default {
} else {
link = <span class={`${prefixCls}-link`}>{children}</span>;
}
// wrap to dropDown
link = this.renderBreadcrumbNode(link, prefixCls);
if (children) {
return (
<span>
{link}
<span class={`${prefixCls}-separator`}>
{getComponentFromProp(this, 'separator') || '/'}
</span>
{separator && separator !== '' && (
<span class={`${prefixCls}-separator`}>{separator}</span>
)}
</span>
);
}

View File

@ -0,0 +1,21 @@
import { ConfigConsumerProps } from '../config-provider';
import PropTypes from '../_util/vue-types';
export default {
name: 'ABreadcrumbSeparator',
__ANT_BREADCRUMB_SEPARATOR: true,
props: {
prefixCls: PropTypes.string,
},
inject: {
configProvider: { default: () => ConfigConsumerProps },
},
render() {
const { prefixCls: customizePrefixCls, $slots } = this;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
const children = $slots.default;
return <span class={`${prefixCls}-separator`}>{children || '/'}</span>;
},
};

View File

@ -13,7 +13,7 @@ describe('Breadcrumb', () => {
});
// // https://github.com/airbnb/enzyme/issues/875
it('warns on non-Breadcrumb.Item children', () => {
it('warns on non-Breadcrumb.Item and non-Breadcrumb.Separator children', () => {
mount({
render() {
return (
@ -25,7 +25,7 @@ describe('Breadcrumb', () => {
});
expect(errorSpy.mock.calls).toHaveLength(1);
expect(errorSpy.mock.calls[0][0]).toMatch(
"Breadcrumb only accepts Breadcrumb.Item as it's children",
"Warning: [antdv: Breadcrumb] Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children",
);
});
@ -61,4 +61,50 @@ describe('Breadcrumb', () => {
});
expect(wrapper.html()).toMatchSnapshot();
});
it('should render a menu', () => {
const routes = [
{
path: 'index',
breadcrumbName: 'home',
},
{
path: 'first',
breadcrumbName: 'first',
children: [
{
path: '/general',
breadcrumbName: 'General',
},
{
path: '/layout',
breadcrumbName: 'Layout',
},
{
path: '/navigation',
breadcrumbName: 'Navigation',
},
],
},
{
path: 'second',
breadcrumbName: 'second',
},
];
const wrapper = mount(Breadcrumb, { propsData: { routes } });
expect(wrapper.html()).toMatchSnapshot();
});
it('should support custom attribute', () => {
const wrapper = mount({
render() {
return (
<Breadcrumb data-custom="custom">
<Breadcrumb.Item data-custom="custom-item">xxx</Breadcrumb.Item>
<Breadcrumb.Item>yyy</Breadcrumb.Item>
</Breadcrumb>
);
},
});
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@ -6,3 +6,11 @@ exports[`Breadcrumb should not display Breadcrumb Item when its children is fals
<div class="ant-breadcrumb">
<!----><span class=""><span class="ant-breadcrumb-link">xxx</span><span class="ant-breadcrumb-separator">/</span></span><span class=""><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><i aria-label="icon: down" class="anticon anticon-down"><svg viewBox="64 64 896 896" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class="">
<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></i></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 custom attribute 1`] = `<div class="ant-breadcrumb" data-custom="custom"><span data-custom="custom-item" class=""><span class="ant-breadcrumb-link">xxx</span><span class="ant-breadcrumb-separator">/</span></span><span class=""><span class="ant-breadcrumb-link">yyy</span><span class="ant-breadcrumb-separator">/</span></span></div>`;

View File

@ -2,6 +2,12 @@
exports[`renders ./components/breadcrumb/demo/basic.md correctly 1`] = `<div class="ant-breadcrumb"><span class=""><span class="ant-breadcrumb-link">Home</span><span class="ant-breadcrumb-separator">/</span></span><span class=""><span class="ant-breadcrumb-link"><a href="">Application Center</a></span><span class="ant-breadcrumb-separator">/</span></span><span class=""><span class="ant-breadcrumb-link"><a href="">Application List</a></span><span class="ant-breadcrumb-separator">/</span></span><span class=""><span class="ant-breadcrumb-link">An Application</span><span class="ant-breadcrumb-separator">/</span></span></div>`;
exports[`renders ./components/breadcrumb/demo/overlay.md correctly 1`] = `
<div class="ant-breadcrumb"><span class=""><span class="ant-breadcrumb-link">Ant Design Vue</span><span class="ant-breadcrumb-separator">/</span></span><span class=""><span class="ant-breadcrumb-link"><a href="">Component</a></span><span class="ant-breadcrumb-separator">/</span></span><span class=""><span class="ant-breadcrumb-overlay-link ant-dropdown-trigger"><span class="ant-breadcrumb-link"><a href="">General</a> </span><i aria-label="icon: down" class="anticon anticon-down"><svg viewBox="64 64 896 896" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class="">
<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></i></span><span class="ant-breadcrumb-separator">/</span></span><span class=""><span class="ant-breadcrumb-link">Button</span><span class="ant-breadcrumb-separator">/</span></span></div>
`;
exports[`renders ./components/breadcrumb/demo/router.md correctly 1`] = `<!---->`;
exports[`renders ./components/breadcrumb/demo/separator.md correctly 1`] = `
@ -11,6 +17,8 @@ exports[`renders ./components/breadcrumb/demo/separator.md correctly 1`] = `
</div>
`;
exports[`renders ./components/breadcrumb/demo/separator-indepent.md correctly 1`] = `<div class="ant-breadcrumb"><span class=""><span class="ant-breadcrumb-link">Location</span></span><span class="ant-breadcrumb-separator">:</span><span class=""><a class="ant-breadcrumb-link">Application Center</a></span><span class="ant-breadcrumb-separator">/</span><span class=""><a class="ant-breadcrumb-link">Application List</a></span><span class="ant-breadcrumb-separator">/</span><span class=""><span class="ant-breadcrumb-link">An Application</span></span></div>`;
exports[`renders ./components/breadcrumb/demo/withIcon.md correctly 1`] = `
<div class="ant-breadcrumb"><span class=""><a class="ant-breadcrumb-link"><i aria-label="icon: home" class="anticon anticon-home"><svg viewBox="64 64 896 896" data-icon="home" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M946.5 505L560.1 118.8l-25.9-25.9a31.5 31.5 0 0 0-44.4 0L77.5 505a63.9 63.9 0 0 0-18.8 46c.4 35.2 29.7 63.3 64.9 63.3h42.5V940h691.8V614.3h43.4c17.1 0 33.2-6.7 45.3-18.8a63.6 63.6 0 0 0 18.7-45.3c0-17-6.7-33.1-18.8-45.2zM568 868H456V664h112v204zm217.9-325.7V868H632V640c0-22.1-17.9-40-40-40H432c-22.1 0-40 17.9-40 40v228H238.1V542.3h-96l370-369.7 23.1 23.1L882 542.3h-96.1z"></path></svg></i></a><span class="ant-breadcrumb-separator">/</span></span><span class=""><a class="ant-breadcrumb-link"><i aria-label="icon: user" class="anticon anticon-user"><svg viewBox="64 64 896 896" data-icon="user" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M858.5 763.6a374 374 0 0 0-80.6-119.5 375.63 375.63 0 0 0-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 0 0-80.6 119.5A371.7 371.7 0 0 0 136 901.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 0 0 8-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"></path></svg></i> <span>Application List</span></a><span class="ant-breadcrumb-separator">/</span></span><span class=""><span class="ant-breadcrumb-link">
Application

View File

@ -3,6 +3,8 @@ import Basic from './basic.md';
import WithIcon from './withIcon.md';
import Separator from './separator.md';
import Router from './router';
import Overlay from './Overlay';
import SeparatorIndepent from './separator-indepent';
import US from './../index.en-US.md';
import CN from './../index.zh-CN.md';
@ -41,6 +43,8 @@ export default {
<WithIcon />
<Separator />
<Router />
<SeparatorIndepent />
<Overlay />
<api>
<CN slot="cn" />
<US />

View File

@ -0,0 +1,39 @@
<cn>
#### 带下拉菜单的面包屑
面包屑支持下拉菜单。
</cn>
<us>
#### Bread crumbs with drop down menu
Breadcrumbs support drop down menu.
</us>
```tpl
<template>
<a-breadcrumb>
<a-breadcrumb-item>Ant Design Vue</a-breadcrumb-item>
<a-breadcrumb-item><a href="">Component</a></a-breadcrumb-item>
<a-breadcrumb-item>
<a href="">General</a>
<a-menu slot="overlay">
<a-menu-item>
<a target="_blank" rel="noopener noreferrer" href="http://www.alipay.com/">
General
</a>
</a-menu-item>
<a-menu-item>
<a target="_blank" rel="noopener noreferrer" href="http://www.taobao.com/">
Layout
</a>
</a-menu-item>
<a-menu-item>
<a target="_blank" rel="noopener noreferrer" href="http://www.tmall.com/">
Navigation
</a>
</a-menu-item>
</a-menu>
</a-breadcrumb-item>
<a-breadcrumb-item>Button</a-breadcrumb-item>
</a-breadcrumb>
</template>
```

View File

@ -30,19 +30,33 @@ Used together with `vue-router`
data() {
const { lang } = this.$route.params;
return {
basePath: `/${lang}/components/breadcrumb`,
basePath: '/components/breadcrumb',
routes: [
{
path: 'index',
breadcrumbName: '首页',
breadcrumbName: 'home',
},
{
path: 'first',
breadcrumbName: '一级面包屑',
breadcrumbName: 'first',
children: [
{
path: '/general',
breadcrumbName: 'General',
},
{
path: '/layout',
breadcrumbName: 'Layout',
},
{
path: '/navigation',
breadcrumbName: 'Navigation',
},
],
},
{
path: 'second',
breadcrumbName: '当前页面',
breadcrumbName: 'second',
},
],
};

View File

@ -0,0 +1,23 @@
<cn>
#### 分隔符
使用 `Breadcrumb.Separator` 可以自定义分隔符。
</cn>
<us>
#### Configuring the Separator
The separator can be customized by setting the separator property: `Breadcrumb.Separator`
</us>
```tpl
<template>
<a-breadcrumb separator="">
<a-breadcrumb-item>Location</a-breadcrumb-item>
<a-breadcrumb-separator>:</a-breadcrumb-separator>
<a-breadcrumb-item href="">Application Center</a-breadcrumb-item>
<a-breadcrumb-separator />
<a-breadcrumb-item href="">Application List</a-breadcrumb-item>
<a-breadcrumb-separator />
<a-breadcrumb-item>An Application</a-breadcrumb-item>
</a-breadcrumb>
</template>
```

View File

@ -1,11 +1,24 @@
## API
| Property | Description | Type | Optional | Default |
| --- | --- | --- | --- | --- |
| itemRender | Custom item renderer, slot="itemRender" and slot-scope="{route, params, routes, paths}" | ({route, params, routes, paths, h}) => vNode | | - |
| params | Routing parameters | object | | - |
| routes | The routing stack information of router | object\[] | | - |
| separator | Custom separator | string\|slot | | `/` |
| Property | Description | Type | Optional | Default | Version |
| --- | --- | --- | --- | --- | --- |
| itemRender | Custom item renderer, slot="itemRender" and slot-scope="{route, params, routes, paths}" | ({route, params, routes, paths, h}) => vNode | | - | |
| params | Routing parameters | object | | - | |
| routes | The routing stack information of router | [routes\[\]](#routes) | | - | |
| separator | Custom separator | string\|slot | | `/` | |
### routes
```ts
interface Route {
path: string;
breadcrumbName: string;
children: Array<{
path: string;
breadcrumbName: string;
}>;
}
```
### Use with browserHistory
@ -31,15 +44,29 @@ The link of Breadcrumb item targets `#` by default, you can use `itemRender` to
routes: [
{
path: 'index',
breadcrumbName: '首页',
breadcrumbName: 'home',
},
{
path: 'first',
breadcrumbName: '一级面包屑',
breadcrumbName: 'first',
children: [
{
path: '/general',
breadcrumbName: 'General',
},
{
path: '/layout',
breadcrumbName: 'Layout',
},
{
path: '/navigation',
breadcrumbName: 'Navigation',
},
],
},
{
path: 'second',
breadcrumbName: '当前页面',
breadcrumbName: 'second',
},
],
};

View File

@ -1,14 +1,17 @@
import Breadcrumb from './Breadcrumb';
import BreadcrumbItem from './BreadcrumbItem';
import BreadcrumbSeparator from './BreadcrumbSeparator';
import Base from '../base';
Breadcrumb.Item = BreadcrumbItem;
Breadcrumb.Separator = BreadcrumbSeparator;
/* istanbul ignore next */
Breadcrumb.install = function(Vue) {
Vue.use(Base);
Vue.component(Breadcrumb.name, Breadcrumb);
Vue.component(BreadcrumbItem.name, BreadcrumbItem);
Vue.component(BreadcrumbSeparator.name, BreadcrumbSeparator);
};
export default Breadcrumb;

View File

@ -4,9 +4,43 @@
| --- | --- | --- | --- | --- |
| itemRender | 自定义链接函数,和 vue-router 配置使用, 也可使用 slot="itemRender" 和 slot-scope="props" | ({route, params, routes, paths, h}) => vNode | | - |
| params | 路由的参数 | object | | - |
| routes | router 的路由栈信息 | object\[] | | - |
| routes | router 的路由栈信息 | [routes\[\]](#routes) | | - |
| separator | 分隔符自定义 | string\|slot | | '/' |
### Breadcrumb.Item
| 参数 | 参数 | 类型 | 默认值 | 版本 |
| ------- | -------------- | -------------------------------------- | ------ | ----- |
| href | 链接的目的地 | string | - | 1.5.0 |
| overlay | 下拉菜单的内容 | [Menu](/components/menu) \| () => Menu | - | 1.5.0 |
#### 事件
| 事件名称 | 说明 | 回调参数 | 版本 |
| -------- | -------- | -------------------- | ---- |
| click | 单击事件 | (e:MouseEvent)=>void | - | 1.5.0 |
### Breadcrumb.Separator `3.21.0`
| 参数 | 参数 | 类型 | 默认值 | 版本 |
| ---- | ---- | ---- | ------ | ---- |
> 注意:在使用 `Breadcrumb.Separator` 时,其父组件的分隔符必须设置为 `separator=""`,否则会出现父组件默认的分隔符。
### routes
```ts
interface Route {
path: string;
breadcrumbName: string;
children: Array<{
path: string;
breadcrumbName: string;
}>;
}
```
### 和 browserHistory 配合
和 vue-router 一起使用时,默认生成的 url 路径是带有 `#` 的,如果和 browserHistory 一起使用的话,你可以使用 `itemRender` 属性定义面包屑链接。
@ -31,15 +65,29 @@
routes: [
{
path: 'index',
breadcrumbName: '首页',
breadcrumbName: 'home',
},
{
path: 'first',
breadcrumbName: '一级面包屑',
breadcrumbName: 'first',
children: [
{
path: '/general',
breadcrumbName: 'General',
},
{
path: '/layout',
breadcrumbName: 'Layout',
},
{
path: '/navigation',
breadcrumbName: 'Navigation',
},
],
},
{
path: 'second',
breadcrumbName: '当前页面',
breadcrumbName: 'second',
},
],
};

View File

@ -11,4 +11,5 @@ export declare class BreadcrumbItem extends AntdComponent {
* @type string
*/
href?: String;
overlay?: any;
}

View File

@ -0,0 +1,7 @@
// Project: https://github.com/vueComponent/ant-design-vue
// Definitions by: akki-jat <https://github.com/akki-jat>
// Definitions: https://github.com/vueComponent/ant-design-vue/types
import { AntdComponent } from '../component';
export declare class BreadcrumbSeparator extends AntdComponent {}

View File

@ -5,6 +5,7 @@
import { AntdComponent } from '../component';
import { VNode } from 'vue';
import { BreadcrumbItem } from './breadcrumb-item';
import { BreadcrumbSeparator } from './breadcrumb-separator';
export interface Route {
path?: String;
@ -13,6 +14,7 @@ export interface Route {
export declare class Breadcrumb extends AntdComponent {
static Item: typeof BreadcrumbItem;
static Separator: typeof BreadcrumbSeparator;
/**
* The routing stack information of router
* @type Route[]