refactor: Breadcrumb

pull/6213/head
tangjinzhou 2023-01-26 16:52:06 +08:00
parent 1e4e3cb3b4
commit 287a8d0c4e
9 changed files with 173 additions and 132 deletions

View File

@ -46,3 +46,7 @@ export type MaybeRef<T> = T | Ref<T>;
export function eventType<T>() { export function eventType<T>() {
return { type: [Function, Array] as PropType<T | T[]> }; return { type: [Function, Array] as PropType<T | T[]> };
} }
export function objectType<T>(defaultVal?: any) {
return { type: Object as PropType<T>, default: defaultVal as T };
}

View File

@ -3,11 +3,12 @@ import { cloneVNode, defineComponent } from 'vue';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { flattenChildren, getPropsSlot } from '../_util/props-util'; import { flattenChildren, getPropsSlot } from '../_util/props-util';
import warning from '../_util/warning'; import warning from '../_util/warning';
import type { BreadcrumbItemProps } from './BreadcrumbItem';
import BreadcrumbItem from './BreadcrumbItem'; import BreadcrumbItem from './BreadcrumbItem';
import Menu from '../menu'; import Menu from '../menu';
import type { VueNode } from '../_util/type'; import type { VueNode } from '../_util/type';
import useConfigInject from '../_util/hooks/useConfigInject'; import useConfigInject from '../_util/hooks/useConfigInject';
import useStyle from './style';
export interface Route { export interface Route {
path: string; path: string;
breadcrumbName: string; breadcrumbName: string;
@ -54,11 +55,12 @@ function defaultItemRender(opt: {
export default defineComponent({ export default defineComponent({
compatConfig: { MODE: 3 }, compatConfig: { MODE: 3 },
name: 'ABreadcrumb', name: 'ABreadcrumb',
inheritAttrs: false,
props: breadcrumbProps(), props: breadcrumbProps(),
slots: ['separator', 'itemRender'], slots: ['separator', 'itemRender'],
setup(props, { slots }) { setup(props, { slots, attrs }) {
const { prefixCls, direction } = useConfigInject('breadcrumb', props); const { prefixCls, direction } = useConfigInject('breadcrumb', props);
const [wrapSSR, hashId] = useStyle(prefixCls);
const getPath = (path: string, params: unknown) => { const getPath = (path: string, params: unknown) => {
path = (path || '').replace(/^\//, ''); path = (path || '').replace(/^\//, '');
Object.keys(params).forEach(key => { Object.keys(params).forEach(key => {
@ -94,27 +96,25 @@ export default defineComponent({
let overlay = null; let overlay = null;
if (route.children && route.children.length) { if (route.children && route.children.length) {
overlay = ( overlay = (
<Menu> <Menu
{route.children.map(child => ( items={route.children.map(child => ({
<Menu.Item key={child.path || child.breadcrumbName}> key: child.path || child.breadcrumbName,
{itemRender({ label: itemRender({
route: child, route: child,
params, params,
routes, routes,
paths: addChildPath(tempPaths, child.path, params), paths: addChildPath(tempPaths, child.path, params),
})} }),
</Menu.Item> }))}
))} ></Menu>
</Menu>
); );
} }
const itemProps: BreadcrumbItemProps = { separator };
if (overlay) {
itemProps.overlay = overlay;
}
return ( return (
<BreadcrumbItem <BreadcrumbItem {...itemProps} key={path || route.breadcrumbName}>
overlay={overlay}
separator={separator}
key={path || route.breadcrumbName}
>
{itemRender({ route, params, routes, paths: tempPaths })} {itemRender({ route, params, routes, paths: tempPaths })}
</BreadcrumbItem> </BreadcrumbItem>
); );
@ -152,11 +152,12 @@ export default defineComponent({
const breadcrumbClassName = { const breadcrumbClassName = {
[prefixCls.value]: true, [prefixCls.value]: true,
[`${prefixCls.value}-rtl`]: direction.value === 'rtl', [`${prefixCls.value}-rtl`]: direction.value === 'rtl',
[hashId.value]: true,
}; };
return ( return wrapSSR(
<nav class={breadcrumbClassName}> <nav {...attrs} class={breadcrumbClassName}>
<ol>{crumbs}</ol> <ol>{crumbs}</ol>
</nav> </nav>,
); );
}; };
}, },

View File

@ -1,18 +1,21 @@
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'; import type { CSSProperties, ExtractPropTypes } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { getPropsSlot } from '../_util/props-util'; import { getPropsSlot } from '../_util/props-util';
import type { DropdownProps } from '../dropdown/dropdown';
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'; import useConfigInject from '../_util/hooks/useConfigInject';
import type { MouseEventHandler } from '../_util/EventInterface'; import type { MouseEventHandler } from '../_util/EventInterface';
import { eventType, objectType } from '../_util/type';
export const breadcrumbItemProps = () => ({ export const breadcrumbItemProps = () => ({
prefixCls: String, prefixCls: String,
href: String, href: String,
separator: PropTypes.any, separator: PropTypes.any,
dropdownProps: objectType<DropdownProps>(),
overlay: PropTypes.any, overlay: PropTypes.any,
onClick: Function as PropType<MouseEventHandler>, onClick: eventType<MouseEventHandler>(),
}); });
export type BreadcrumbItemProps = Partial<ExtractPropTypes<ReturnType<typeof breadcrumbItemProps>>>; export type BreadcrumbItemProps = Partial<ExtractPropTypes<ReturnType<typeof breadcrumbItemProps>>>;
@ -24,7 +27,7 @@ export default defineComponent({
props: breadcrumbItemProps(), props: breadcrumbItemProps(),
// emits: ['click'], // emits: ['click'],
slots: ['separator', 'overlay'], slots: ['separator', 'overlay'],
setup(props, { slots, attrs }) { setup(props, { slots, attrs, emit }) {
const { prefixCls } = useConfigInject('breadcrumb', props); const { prefixCls } = useConfigInject('breadcrumb', props);
/** /**
* if overlay is have * if overlay is have
@ -34,7 +37,7 @@ export default defineComponent({
const overlay = getPropsSlot(slots, props, 'overlay'); const overlay = getPropsSlot(slots, props, 'overlay');
if (overlay) { if (overlay) {
return ( return (
<Dropdown overlay={overlay} placement="bottom"> <Dropdown {...props.dropdownProps} overlay={overlay} placement="bottom">
<span class={`${prefixCls}-overlay-link`}> <span class={`${prefixCls}-overlay-link`}>
{breadcrumbItem} {breadcrumbItem}
<DownOutlined /> <DownOutlined />
@ -44,7 +47,9 @@ export default defineComponent({
} }
return breadcrumbItem; return breadcrumbItem;
}; };
const handleClick = (e: MouseEvent) => {
emit('click', e);
};
return () => { return () => {
const separator = getPropsSlot(slots, props, 'separator') ?? '/'; const separator = getPropsSlot(slots, props, 'separator') ?? '/';
const children = getPropsSlot(slots, props); const children = getPropsSlot(slots, props);
@ -52,20 +57,20 @@ export default defineComponent({
let link: JSX.Element; let link: JSX.Element;
if (props.href !== undefined) { if (props.href !== undefined) {
link = ( link = (
<a class={`${prefixCls.value}-link`} onClick={props.onClick} {...restAttrs}> <a class={`${prefixCls.value}-link`} onClick={handleClick} {...restAttrs}>
{children} {children}
</a> </a>
); );
} else { } else {
link = ( link = (
<span class={`${prefixCls.value}-link`} onClick={props.onClick} {...restAttrs}> <span class={`${prefixCls.value}-link`} onClick={handleClick} {...restAttrs}>
{children} {children}
</span> </span>
); );
} }
// wrap to dropDown // wrap to dropDown
link = renderBreadcrumbNode(link, prefixCls.value); link = renderBreadcrumbNode(link, prefixCls.value);
if (children) { if (children !== undefined && children !== null) {
return ( return (
<li class={cls} style={style as CSSProperties}> <li class={cls} style={style as CSSProperties}>
{link} {link}

View File

@ -1,64 +0,0 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@breadcrumb-prefix-cls: ~'@{ant-prefix}-breadcrumb';
.@{breadcrumb-prefix-cls} {
.reset-component();
color: @breadcrumb-base-color;
font-size: @breadcrumb-font-size;
.@{iconfont-css-prefix} {
font-size: @breadcrumb-icon-font-size;
}
ol {
display: flex;
flex-wrap: wrap;
margin: 0;
padding: 0;
list-style: none;
}
a {
color: @breadcrumb-link-color;
transition: color 0.3s;
&:hover {
color: @breadcrumb-link-color-hover;
}
}
li:last-child {
color: @breadcrumb-last-item-color;
a {
color: @breadcrumb-last-item-color;
}
}
li:last-child &-separator {
display: none;
}
&-separator {
margin: @breadcrumb-separator-margin;
color: @breadcrumb-separator-color;
}
&-link {
> .@{iconfont-css-prefix} + span,
> .@{iconfont-css-prefix} + a {
margin-left: 4px;
}
}
&-overlay-link {
> .@{iconfont-css-prefix} {
margin-left: 4px;
}
}
}
@import './rtl';

View File

@ -0,0 +1,127 @@
import type { CSSObject } from '../../_util/cssinjs';
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { genFocusStyle, resetComponent } from '../../_style';
interface BreadcrumbToken extends FullToken<'Breadcrumb'> {
breadcrumbBaseColor: string;
breadcrumbFontSize: number;
breadcrumbIconFontSize: number;
breadcrumbLinkColor: string;
breadcrumbLinkColorHover: string;
breadcrumbLastItemColor: string;
breadcrumbSeparatorMargin: number;
breadcrumbSeparatorColor: string;
}
const genBreadcrumbStyle: GenerateStyle<BreadcrumbToken, CSSObject> = token => {
const { componentCls, iconCls } = token;
return {
[componentCls]: {
...resetComponent(token),
color: token.breadcrumbBaseColor,
fontSize: token.breadcrumbFontSize,
[iconCls]: {
fontSize: token.breadcrumbIconFontSize,
},
ol: {
display: 'flex',
flexWrap: 'wrap',
margin: 0,
padding: 0,
listStyle: 'none',
},
a: {
color: token.breadcrumbLinkColor,
transition: `color ${token.motionDurationMid}`,
padding: `0 ${token.paddingXXS}px`,
borderRadius: token.borderRadiusSM,
height: token.lineHeight * token.fontSize,
display: 'inline-block',
marginInline: -token.marginXXS,
'&:hover': {
color: token.breadcrumbLinkColorHover,
backgroundColor: token.colorBgTextHover,
},
...genFocusStyle(token),
},
[`li:last-child`]: {
color: token.breadcrumbLastItemColor,
[`& > ${componentCls}-separator`]: {
display: 'none',
},
},
[`${componentCls}-separator`]: {
marginInline: token.breadcrumbSeparatorMargin,
color: token.breadcrumbSeparatorColor,
},
[`${componentCls}-link`]: {
[`
> ${iconCls} + span,
> ${iconCls} + a
`]: {
marginInlineStart: token.marginXXS,
},
},
[`${componentCls}-overlay-link`]: {
borderRadius: token.borderRadiusSM,
height: token.lineHeight * token.fontSize,
display: 'inline-block',
padding: `0 ${token.paddingXXS}px`,
marginInline: -token.marginXXS,
[`> ${iconCls}`]: {
marginInlineStart: token.marginXXS,
fontSize: token.fontSizeIcon,
},
'&:hover': {
color: token.breadcrumbLinkColorHover,
backgroundColor: token.colorBgTextHover,
a: {
color: token.breadcrumbLinkColorHover,
},
},
a: {
'&:hover': {
backgroundColor: 'transparent',
},
},
},
// rtl style
[`&${token.componentCls}-rtl`]: {
direction: 'rtl',
},
},
};
};
// ============================== Export ==============================
export default genComponentStyleHook('Breadcrumb', token => {
const BreadcrumbToken = mergeToken<BreadcrumbToken>(token, {
breadcrumbBaseColor: token.colorTextDescription,
breadcrumbFontSize: token.fontSize,
breadcrumbIconFontSize: token.fontSize,
breadcrumbLinkColor: token.colorTextDescription,
breadcrumbLinkColorHover: token.colorText,
breadcrumbLastItemColor: token.colorText,
breadcrumbSeparatorMargin: token.marginXS,
breadcrumbSeparatorColor: token.colorTextDescription,
});
return [genBreadcrumbStyle(BreadcrumbToken)];
});

View File

@ -1,6 +0,0 @@
import '../../style/index.less';
import './index.less';
// style dependencies
import '../../menu/style';
import '../../dropdown/style';

View File

@ -1,29 +0,0 @@
.@{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

@ -3,6 +3,8 @@ import PropTypes from '../_util/vue-types';
import buttonTypes from '../button/buttonTypes'; import buttonTypes from '../button/buttonTypes';
import type { MouseEventHandler } from '../_util/EventInterface'; import type { MouseEventHandler } from '../_util/EventInterface';
import type { MenuProps } from '../menu';
import { objectType } from '../_util/type';
export type Align = { export type Align = {
points?: [string, string]; points?: [string, string];
@ -30,6 +32,7 @@ const dropdownProps = () => ({
trigger: { trigger: {
type: [Array, String] as PropType<Trigger[] | Trigger>, type: [Array, String] as PropType<Trigger[] | Trigger>,
}, },
menu: objectType<MenuProps>(),
overlay: PropTypes.any, overlay: PropTypes.any,
visible: { type: Boolean, default: undefined }, visible: { type: Boolean, default: undefined },
disabled: { type: Boolean, default: undefined }, disabled: { type: Boolean, default: undefined },

View File

@ -33,7 +33,7 @@ import './modal/style';
// import './alert/style'; // import './alert/style';
import './time-picker/style'; import './time-picker/style';
import './steps/style'; import './steps/style';
import './breadcrumb/style'; // import './breadcrumb/style';
import './calendar/style'; import './calendar/style';
import './date-picker/style'; import './date-picker/style';
import './slider/style'; import './slider/style';