refactor: Anchor、Alert、Avatar、Badge、BackTop、Col、Form、Layout、Menu、Space、Spin、Switch、Row、Result、Rate (#4171)

* chore: remove  resize-observer-polyfill

* refactor: align

* refactor(v3/avatar): refactor using composition api  (#4052)

* refactor(avatar): refactor using composition api

* refactor: update props define

* fix: avatar src scale not update

* refactor: resizeObserver

* refactor: divider

* refactor: localeProvider

* refactor(v3/back-top): use composition api (#4060)

* refactor: backtop

* refactor: empty

* refactor: transButton

* feat(v3/avatar): add avatar group (#4062)

* feat(avatar): add avatar group

* refactor: update

* refactor: update

Co-authored-by: tangjinzhou <415800467@qq.com>

* refactor: avatar

* refactor: avatar

* style: rename useProvide

* refactor:  menu (#4110)

* fix: menu

* refactor: menu

* refactor: remove rc-menu

* fix: menu rtl error

* style: lint

* refactor(Anchor): use composition api (#4054)

* refactor: anchor

* refactor: anchor

* refactor: anchor

* feat: update

* fix: icon class lose

* refactor(v3/badge): use composition api (#4076)

* refactor: badge

* fix: badge inheritAttrs

* refactor: grid

* refactor: layout

* fix: menu not close

* refactor: space

* refactor: result

* refactor: affix

* refactor: comment

* refactor: form

* feat: spin add rtl

* feat: export spin type

* refactor: pageHeader

* refactor: page-header

* refactor: skeleton

* refactor: typography

* refactor(v3/rate): use composition api

* fix: add useRef hook

* refactor: form

* fix: menu not update

* refactor: form

* refactor: form

* fix: slide animate not work

* fix: menu mode error

* fix: menu icon

* refactor: rate

* perf: remove rate

* feat: add vc-overflow

* refactor: menu

* fix: remove flex check (#4165)

* fix: dist locale file lose #3684

* release 2.2.0-beta.1

* dcos: update changelog

* chore: update type

* docs: update changelog

Co-authored-by: John <John60676@qq.com>
Co-authored-by: 言肆 <18x@loacg.com>
Co-authored-by: zkwolf <chenhao5866@gmail.com>
pull/4175/head 2.2.0-beta.1
tangjinzhou 2021-06-07 17:35:03 +08:00 committed by GitHub
parent b91659e4f7
commit 9e0df41a55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
211 changed files with 9409 additions and 7531 deletions

View File

@ -10,6 +10,37 @@
---
## 2.2.0-beta.1
`2021-06-17`
- 🔥🔥🔥 Virtual Table independent library released https://www.npmjs.com/package/@surely-vue/table, this component is an independent library, the document example is not yet complete, it is a completely ts-developed component , There are good type hints, there are API documents on npm, those who are in a hurry can explore and use it, here is an online experience example, https://store.antdv.com/pro/preview/list/big- table-list
- 🔥🔥🔥 Refactored a large number of components, the source code is more readable, the performance is better, and the ts type is more comprehensive -Refactored components in this version Anchor, Alert, Avatar, Badge, BackTop, Col, Form, Layout, Menu, Space, Spin, Switch, Row, Result, Rate
- 🎉 Menu
- Better performance [#3300](https://github.com/vueComponent/ant-design-vue/issues/3300)
- Fix the problem of incorrect highlighting [#4053](https://github.com/vueComponent/ant-design-vue/issues/4053)
- Fix console invalid warning [#4169](https://github.com/vueComponent/ant-design-vue/issues/4169)
- Easier to use, simpler to use single file recursion [#4133](https://github.com/vueComponent/ant-design-vue/issues/4133)
- 💄 icon icon needs to be passed through slot
- Skeleton
- 🌟 Support Skeleton.Avatar placeholder component.
- 🌟 Support Skeleton.Button placeholder component.
- 🌟 Support Skeleton.Input placeholder component.
- 💄 Destructive update
- The `a-menu-item` and `a-sub-menu` icons need to be passed through the slot, and the icon is not automatically obtained through the sub-node
- row gutter supports row-wrap, no need to use multiple rows to divide col
- `Menu` removes `defaultOpenKeys` and `defaultSelectedKeys`; `Switch` removes `defaultChecked`; `Rate` removes `defaultValue`; Please be cautious to use the defaultXxx-named attributes of other unrefactored components, and they will be removed in future versions.
- 🌟 Added Avatar.Group component
- 🐞 Fix AutoComplete filterOptions not taking effect [#4170](https://github.com/vueComponent/ant-design-vue/issues/4170)
- 🐞 Fix Select automatic width invalidation problem [#4118](https://github.com/vueComponent/ant-design-vue/issues/4118)
- 🐞 Fix the lack of internationalized files in dist [#3684](https://github.com/vueComponent/ant-design-vue/issues/3684)
## 2.1.6
`2021-05-13`

View File

@ -10,6 +10,38 @@
---
## 2.2.0-beta.1
`2021-06-17`
- 🔥🔥🔥 虚拟 Table 独立库发布 https://www.npmjs.com/package/@surely-vue/table , 该组件是一个独立的库,目前文档示例尚未完善,他是一个完全 ts 开发的组件有较好的类型提示npm 上已有 API 文档着急使用的的可以摸索着用起来了这里有个在线体验示例https://store.antdv.com/pro/preview/list/big-table-list
- 🔥🔥🔥 重构大量组件源码更加易读性能更优ts 类型更加全面
- 本版本重构组件 Anchor、Alert、Avatar、Badge、BackTop、Col、Form、Layout、Menu、Space、Spin、Switch、Row、Result、Rate
- 🎉 Menu
- 性能更优 [#3300](https://github.com/vueComponent/ant-design-vue/issues/3300)
- 修复高亮不正确问题 [#4053](https://github.com/vueComponent/ant-design-vue/issues/4053)
- 修复控制台无效 warning [#4169](https://github.com/vueComponent/ant-design-vue/issues/4169)
- 更加易用,更加简单的使用单文件递归 [#4133](https://github.com/vueComponent/ant-design-vue/issues/4133)
- 💄 图标 icon 需要通过 slot 传递
- Skeleton
- 🌟 支持 Skeleton.Avatar 占位组件。
- 🌟 支持 Skeleton.Button 占位组件。
- 🌟 支持 Skeleton.Input 占位组件。
- 💄 破坏性更新
- `a-menu-item`、`a-sub-menu` 图标需要通过 slot 传递,不在通过子节点自动获取图标
- row gutter 支持 row-wrap 无需使用多个 row 划分 col
- Menu 移除 defaultOpenKeys、defaultSelectedKeys; Switch 移除 defaultChecked; Rate 移除 defaultValue; 其它未重构组件的 defaultXxx 命名的属性请谨慎使用,在未来的版本中也会被移除。
- 🌟 新增 Avatar.Group 组件
- 🐞 修复 AutoComplete filterOptions 不生效问题 [#4170](https://github.com/vueComponent/ant-design-vue/issues/4170)
- 🐞 修复 Select 自动宽度失效问题 [#4118](https://github.com/vueComponent/ant-design-vue/issues/4118)
- 🐞 修复 dist 缺少国际化文件问题 [#3684](https://github.com/vueComponent/ant-design-vue/issues/3684)
## 2.1.6
`2021-05-13`

View File

@ -294,7 +294,7 @@ gulp.task(
function publish(tagString, done) {
let args = ['publish', '--with-antd-tools'];
args = args.concat(['--tag', 'next']);
// args = args.concat(['--tag', 'next']);
if (tagString) {
args = args.concat(['--tag', tagString]);
}

View File

@ -0,0 +1,5 @@
function canUseDom() {
return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
}
export default canUseDom;

View File

@ -1,17 +0,0 @@
export default function getScroll(target, top) {
if (typeof window === 'undefined') {
return 0;
}
const prop = top ? 'pageYOffset' : 'pageXOffset';
const method = top ? 'scrollTop' : 'scrollLeft';
const isWindow = target === window;
let ret = isWindow ? target[prop] : target[method];
// ie6,7,8 standard mode
if (isWindow && typeof ret !== 'number') {
ret = window.document.documentElement[method];
}
return ret;
}

View File

@ -0,0 +1,27 @@
export function isWindow(obj: any) {
return obj !== null && obj !== undefined && obj === obj.window;
}
export default function getScroll(
target: HTMLElement | Window | Document | null,
top: boolean,
): number {
if (typeof window === 'undefined') {
return 0;
}
const method = top ? 'scrollTop' : 'scrollLeft';
let result = 0;
if (isWindow(target)) {
result = (target as Window)[top ? 'pageYOffset' : 'pageXOffset'];
} else if (target instanceof Document) {
result = target.documentElement[method];
} else if (target) {
result = (target as HTMLElement)[method];
}
if (target && !isWindow(target) && typeof result !== 'number') {
result = ((target as HTMLElement).ownerDocument || (target as Document)).documentElement?.[
method
];
}
return result;
}

View File

@ -0,0 +1,21 @@
import { onMounted, onUnmounted, Ref, ref } from 'vue';
import ResponsiveObserve, { ScreenMap } from '../../_util/responsiveObserve';
function useBreakpoint(): Ref<ScreenMap> {
const screens = ref<ScreenMap>({});
let token = null;
onMounted(() => {
token = ResponsiveObserve.subscribe(supportScreens => {
screens.value = supportScreens;
});
});
onUnmounted(() => {
ResponsiveObserve.unsubscribe(token);
});
return screens;
}
export default useBreakpoint;

View File

@ -1,8 +1,46 @@
import { computed, inject } from 'vue';
import { defaultConfigProvider } from '../../config-provider';
import { RequiredMark } from '../../form/Form';
import { computed, ComputedRef, inject, UnwrapRef } from 'vue';
import {
ConfigProviderProps,
defaultConfigProvider,
Direction,
SizeType,
} from '../../config-provider';
export default (name: string, props: Record<any, any>) => {
const configProvider = inject('configProvider', defaultConfigProvider);
export default (
name: string,
props: Record<any, any>,
): {
configProvider: UnwrapRef<ConfigProviderProps>;
prefixCls: ComputedRef<string>;
direction: ComputedRef<Direction>;
size: ComputedRef<SizeType>;
getTargetContainer: ComputedRef<() => HTMLElement>;
space: ComputedRef<{ size: SizeType | number }>;
pageHeader: ComputedRef<{ ghost: boolean }>;
form?: ComputedRef<{
requiredMark?: RequiredMark;
}>;
} => {
const configProvider = inject<UnwrapRef<ConfigProviderProps>>(
'configProvider',
defaultConfigProvider,
);
const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls));
return { configProvider, prefixCls };
const direction = computed(() => configProvider.direction);
const space = computed(() => configProvider.space);
const pageHeader = computed(() => configProvider.pageHeader);
const form = computed(() => configProvider.form);
const size = computed(() => props.size || configProvider.componentSize);
const getTargetContainer = computed(() => props.getTargetContainer);
return {
configProvider,
prefixCls,
direction,
size,
getTargetContainer,
space,
pageHeader,
form,
};
};

View File

@ -0,0 +1,11 @@
import { onMounted, ref } from 'vue';
import { detectFlexGapSupported } from '../styleChecker';
export default () => {
const flexible = ref(false);
onMounted(() => {
flexible.value = detectFlexGapSupported();
});
return flexible;
};

View File

@ -0,0 +1,8 @@
import { computed, ComputedRef, inject } from 'vue';
import { defaultConfigProvider } from '../../config-provider';
export default (name: string, props: Record<any, any>): ComputedRef<string> => {
const configProvider = inject('configProvider', defaultConfigProvider);
const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls));
return prefixCls;
};

View File

@ -0,0 +1,14 @@
import { onBeforeUpdate, ref, Ref } from 'vue';
export type UseRef = [(el: any, key: string | number) => void, Ref<any>];
export const useRef = (): UseRef => {
const refs = ref<any>({});
const setRef = (el: any, key: string | number) => {
refs.value[key] = el;
};
onBeforeUpdate(() => {
refs.value = {};
});
return [setRef, refs];
};

View File

@ -0,0 +1,28 @@
import { computed, ComputedRef, inject, provide, UnwrapRef } from 'vue';
import { ConfigProviderProps, defaultConfigProvider, SizeType } from '../../config-provider';
const sizeProvider = Symbol('SizeProvider');
const useProvideSize = <T = SizeType>(props: Record<any, any>): ComputedRef<T> => {
const configProvider = inject<UnwrapRef<ConfigProviderProps>>(
'configProvider',
defaultConfigProvider,
);
const size = computed<T>(() => props.size || configProvider.componentSize);
provide(sizeProvider, size);
return size;
};
const useInjectSize = <T = SizeType>(props?: Record<any, any>): ComputedRef<T> => {
const size: ComputedRef<T> = props
? computed(() => props.size)
: inject(
sizeProvider,
computed(() => ('default' as unknown) as T),
);
return size;
};
export { useInjectSize, sizeProvider, useProvideSize };
export default useProvideSize;

View File

@ -116,7 +116,7 @@ const getSlotOptions = () => {
throw Error('使用 .type 直接取值');
};
const findDOMNode = instance => {
let node = instance && (instance.$el || instance);
let node = instance?.vnode?.el || (instance && (instance.$el || instance));
while (node && !node.tagName) {
node = node.nextSibling;
}
@ -394,7 +394,7 @@ function isValidElement(element) {
}
function getPropsSlot(slots, props, prop = 'default') {
return slots[prop]?.() ?? props[prop];
return props[prop] ?? slots[prop]?.();
}
export {

View File

@ -1,6 +1,7 @@
export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type BreakpointMap = Partial<Record<Breakpoint, string>>;
export type BreakpointMap = Record<Breakpoint, string>;
export type ScreenMap = Partial<Record<Breakpoint, boolean>>;
export type ScreenSizeMap = Partial<Record<Breakpoint, number>>;
export const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs'];
@ -43,7 +44,7 @@ const responsiveObserve = {
},
unregister() {
Object.keys(responsiveMap).forEach((screen: string) => {
const matchMediaQuery = responsiveMap[screen]!;
const matchMediaQuery = responsiveMap[screen];
const handler = this.matchHandlers[matchMediaQuery];
handler?.mql.removeListener(handler?.listener);
});
@ -51,7 +52,7 @@ const responsiveObserve = {
},
register() {
Object.keys(responsiveMap).forEach((screen: string) => {
const matchMediaQuery = responsiveMap[screen]!;
const matchMediaQuery = responsiveMap[screen];
const listener = ({ matches }: { matches: boolean }) => {
this.dispatch({
...screens,

View File

@ -1,9 +1,10 @@
import getScroll from './getScroll';
import raf from './raf';
import getScroll, { isWindow } from './getScroll';
import { easeInOutCubic } from './easings';
interface ScrollToOptions {
/** Scroll container, default as window */
getContainer?: () => HTMLElement | Window;
getContainer?: () => HTMLElement | Window | Document;
/** Scroll end callback */
callback?: () => any;
/** Animation duration, default as 450 */
@ -12,7 +13,6 @@ interface ScrollToOptions {
export default function scrollTo(y: number, options: ScrollToOptions = {}) {
const { getContainer = () => window, callback, duration = 450 } = options;
const container = getContainer();
const scrollTop = getScroll(container, true);
const startTime = Date.now();
@ -21,16 +21,18 @@ export default function scrollTo(y: number, options: ScrollToOptions = {}) {
const timestamp = Date.now();
const time = timestamp - startTime;
const nextScrollTop = easeInOutCubic(time > duration ? duration : time, scrollTop, y, duration);
if (container === window) {
window.scrollTo(window.pageXOffset, nextScrollTop);
if (isWindow(container)) {
(container as Window).scrollTo(window.pageXOffset, nextScrollTop);
} else if (container instanceof HTMLDocument || container.constructor.name === 'HTMLDocument') {
(container as HTMLDocument).documentElement.scrollTop = nextScrollTop;
} else {
(container as HTMLElement).scrollTop = nextScrollTop;
}
if (time < duration) {
requestAnimationFrame(frameFunc);
raf(frameFunc);
} else if (typeof callback === 'function') {
callback();
}
};
requestAnimationFrame(frameFunc);
raf(frameFunc);
}

View File

@ -1,5 +1,9 @@
const isStyleSupport = (styleName: string | Array<string>): boolean => {
if (typeof window !== 'undefined' && window.document && window.document.documentElement) {
import canUseDom from './canUseDom';
export const canUseDocElement = () => canUseDom() && window.document.documentElement;
export const isStyleSupport = (styleName: string | Array<string>): boolean => {
if (canUseDocElement()) {
const styleNameList = Array.isArray(styleName) ? styleName : [styleName];
const { documentElement } = window.document;
@ -8,6 +12,32 @@ const isStyleSupport = (styleName: string | Array<string>): boolean => {
return false;
};
export const isFlexSupported = isStyleSupport(['flex', 'webkitFlex', 'Flex', 'msFlex']);
let flexGapSupported: boolean | undefined;
export const detectFlexGapSupported = () => {
if (!canUseDocElement()) {
return false;
}
if (flexGapSupported !== undefined) {
return flexGapSupported;
}
// create flex container with row-gap set
const flex = document.createElement('div');
flex.style.display = 'flex';
flex.style.flexDirection = 'column';
flex.style.rowGap = '1px';
// create two, elements inside it
flex.appendChild(document.createElement('div'));
flex.appendChild(document.createElement('div'));
// append to the DOM (needed to obtain scrollHeight)
document.body.appendChild(flex);
flexGapSupported = flex.scrollHeight === 1; // flex container should be 1px high from the row-gap
document.body.removeChild(flex);
return flexGapSupported;
};
export default isStyleSupport;

View File

@ -1,77 +0,0 @@
import { defineComponent } from 'vue';
/**
* Wrap of sub component which need use as Button capacity (like Icon component).
* This helps accessibility reader to tread as a interactive button to operation.
*/
import KeyCode from './KeyCode';
import PropTypes from './vue-types';
const inlineStyle = {
border: 0,
background: 'transparent',
padding: 0,
lineHeight: 'inherit',
display: 'inline-block',
};
const TransButton = defineComponent({
name: 'TransButton',
inheritAttrs: false,
props: {
noStyle: PropTypes.looseBool,
onClick: PropTypes.func,
},
methods: {
onKeyDown(event) {
const { keyCode } = event;
if (keyCode === KeyCode.ENTER) {
event.preventDefault();
}
},
onKeyUp(event) {
const { keyCode } = event;
if (keyCode === KeyCode.ENTER) {
this.$emit('click', event);
}
},
setRef(btn) {
this.$refs.div = btn;
},
focus() {
if (this.$refs.div) {
this.$refs.div.focus();
}
},
blur() {
if (this.$refs.div) {
this.$refs.div.blur();
}
},
},
render() {
const { noStyle, onClick } = this.$props;
return (
<div
role="button"
tabindex={0}
ref="div"
{...this.$attrs}
onClick={onClick}
onKeydown={this.onKeyDown}
onKeyup={this.onKeyUp}
style={{ ...(!noStyle ? inlineStyle : null) }}
>
{this.$slots.default?.()}
</div>
);
},
});
export default TransButton;

View File

@ -0,0 +1,101 @@
import { defineComponent, CSSProperties, ref, onMounted } from 'vue';
/**
* Wrap of sub component which need use as Button capacity (like Icon component).
* This helps accessibility reader to tread as a interactive button to operation.
*/
import KeyCode from './KeyCode';
import PropTypes from './vue-types';
const inlineStyle = {
border: 0,
background: 'transparent',
padding: 0,
lineHeight: 'inherit',
display: 'inline-block',
};
const TransButton = defineComponent({
name: 'TransButton',
inheritAttrs: false,
props: {
noStyle: PropTypes.looseBool,
onClick: PropTypes.func,
disabled: PropTypes.looseBool,
autofocus: PropTypes.looseBool,
},
setup(props, { slots, emit, attrs, expose }) {
const domRef = ref();
const onKeyDown = (event: KeyboardEvent) => {
const { keyCode } = event;
if (keyCode === KeyCode.ENTER) {
event.preventDefault();
}
};
const onKeyUp = (event: KeyboardEvent) => {
const { keyCode } = event;
if (keyCode === KeyCode.ENTER) {
emit('click', event);
}
};
const onClick = (e: Event) => {
emit('click', e);
};
const focus = () => {
if (domRef.value) {
domRef.value.focus();
}
};
const blur = () => {
if (domRef.value) {
domRef.value.blur();
}
};
onMounted(() => {
if (props.autofocus) {
focus();
}
});
expose({
focus,
blur,
});
return () => {
const { noStyle, disabled, ...restProps } = props;
let mergedStyle: CSSProperties = {};
if (!noStyle) {
mergedStyle = {
...inlineStyle,
};
}
if (disabled) {
mergedStyle.pointerEvents = 'none';
}
return (
<div
role="button"
tabindex={0}
ref={domRef}
{...restProps}
{...attrs}
onClick={onClick}
onKeydown={onKeyDown}
onKeyup={onKeyUp}
style={{
...mergedStyle,
...((attrs.style as object) || {}),
}}
>
{slots.default?.()}
</div>
);
};
},
});
export default TransButton;

View File

@ -1,4 +1,12 @@
import { defineComponent, nextTick, Transition as T, TransitionGroup as TG } from 'vue';
import {
BaseTransitionProps,
CSSProperties,
defineComponent,
nextTick,
Ref,
Transition as T,
TransitionGroup as TG,
} from 'vue';
import { findDOMNode } from './props-util';
export const getTransitionProps = (transitionName: string, opt: object = {}) => {
@ -80,6 +88,63 @@ if (process.env.NODE_ENV === 'test') {
});
}
export { Transition, TransitionGroup };
export declare type MotionEvent = (TransitionEvent | AnimationEvent) & {
deadline?: boolean;
};
export declare type MotionEventHandler = (element: Element, done?: () => void) => CSSProperties;
export declare type MotionEndEventHandler = (element: Element, done?: () => void) => boolean | void;
// ================== Collapse Motion ==================
const getCollapsedHeight: MotionEventHandler = () => ({ height: 0, opacity: 0 });
const getRealHeight: MotionEventHandler = node => ({
height: `${node.scrollHeight}px`,
opacity: 1,
});
const getCurrentHeight: MotionEventHandler = (node: any) => ({ height: `${node.offsetHeight}px` });
// const skipOpacityTransition: MotionEndEventHandler = (_, event) =>
// (event as TransitionEvent).propertyName === 'height';
export interface CSSMotionProps extends Partial<BaseTransitionProps<Element>> {
name?: string;
css?: boolean;
}
const collapseMotion = (style: Ref<CSSProperties>, className: Ref<string>): CSSMotionProps => {
return {
name: 'ant-motion-collapse',
appear: true,
css: true,
onBeforeEnter: node => {
className.value = 'ant-motion-collapse';
style.value = getCollapsedHeight(node);
},
onEnter: node => {
nextTick(() => {
style.value = getRealHeight(node);
});
},
onAfterEnter: () => {
className.value = '';
style.value = {};
},
onBeforeLeave: node => {
className.value = 'ant-motion-collapse';
style.value = getCurrentHeight(node);
},
onLeave: node => {
window.setTimeout(() => {
style.value = getCollapsedHeight(node);
});
},
onAfterLeave: () => {
className.value = '';
style.value = {};
},
};
};
export { Transition, TransitionGroup, collapseMotion };
export default Transition;

View File

@ -1,7 +1,6 @@
import {
CSSProperties,
defineComponent,
inject,
ref,
reactive,
watch,
@ -10,13 +9,13 @@ import {
computed,
onUnmounted,
onUpdated,
ExtractPropTypes,
} from 'vue';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
import omit from 'omit.js';
import ResizeObserver from '../vc-resize-observer';
import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
import { defaultConfigProvider } from '../config-provider';
import { withInstall } from '../_util/type';
import {
addObserveTarget,
@ -25,6 +24,7 @@ import {
getFixedTop,
getFixedBottom,
} from './utils';
import useConfigInject from '../_util/hooks/useConfigInject';
function getDefaultTarget() {
return typeof window !== 'undefined' ? window : null;
@ -42,7 +42,7 @@ export interface AffixState {
}
// Affix
const AffixProps = {
const affixProps = {
/**
* 距离窗口顶部达到指定偏移量后触发
*/
@ -58,12 +58,14 @@ const AffixProps = {
onChange: PropTypes.func,
onTestUpdatePosition: PropTypes.func,
};
export type AffixProps = Partial<ExtractPropTypes<typeof affixProps>>;
const Affix = defineComponent({
name: 'AAffix',
props: AffixProps,
props: affixProps,
emits: ['change', 'testUpdatePosition'],
setup(props, { slots, emit, expose }) {
const configProvider = inject('configProvider', defaultConfigProvider);
const placeholderNode = ref();
const fixedNode = ref();
const state = reactive({
@ -218,12 +220,12 @@ const Affix = defineComponent({
(lazyUpdatePosition as any).cancel();
});
const { prefixCls } = useConfigInject('affix', props);
return () => {
const { prefixCls } = props;
const { affixStyle, placeholderStyle } = state;
const { getPrefixCls } = configProvider;
const className = classNames({
[getPrefixCls('affix', prefixCls)]: affixStyle,
[prefixCls.value]: affixStyle,
});
const restProps = omit(props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target']);
return (

View File

@ -1,23 +1,28 @@
import { defineComponent, inject, nextTick, provide } from 'vue';
import {
defineComponent,
nextTick,
onBeforeUnmount,
onMounted,
onUpdated,
reactive,
ref,
ExtractPropTypes,
computed,
} from 'vue';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
import addEventListener from '../vc-util/Dom/addEventListener';
import Affix from '../affix';
import scrollTo from '../_util/scrollTo';
import getScroll from '../_util/getScroll';
import { findDOMNode } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
import { defaultConfigProvider } from '../config-provider';
import useConfigInject from '../_util/hooks/useConfigInject';
import useProvideAnchor from './context';
function getDefaultContainer() {
return window;
}
function getOffsetTop(element: HTMLElement, container: AnchorContainer): number {
if (!element) {
return 0;
}
if (!element.getClientRects().length) {
return 0;
}
@ -26,7 +31,7 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
if (rect.width || rect.height) {
if (container === window) {
container = element.ownerDocument.documentElement;
container = element.ownerDocument!.documentElement!;
return rect.top - container.clientTop;
}
return rect.top - (container as HTMLElement).getBoundingClientRect().top;
@ -35,7 +40,7 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
return rect.top;
}
const sharpMatcherRegx = /#([^#]+)$/;
const sharpMatcherRegx = /#(\S+)$/;
type Section = {
link: string;
@ -44,7 +49,7 @@ type Section = {
export type AnchorContainer = HTMLElement | Window;
const AnchorProps = {
const anchorProps = {
prefixCls: PropTypes.string,
offsetTop: PropTypes.number,
bounds: PropTypes.number,
@ -59,107 +64,40 @@ const AnchorProps = {
onClick: PropTypes.func,
};
export interface AntAnchor {
registerLink: (link: string) => void;
unregisterLink: (link: string) => void;
$data: AnchorState;
scrollTo: (link: string) => void;
$emit?: Function;
}
export type AnchorProps = Partial<ExtractPropTypes<typeof anchorProps>>;
export interface AnchorState {
activeLink: null | string;
scrollContainer: HTMLElement | Window;
links: string[];
scrollEvent: any;
animating: boolean;
sPrefixCls?: string;
}
export default defineComponent({
name: 'AAnchor',
mixins: [BaseMixin],
inheritAttrs: false,
props: AnchorProps,
props: anchorProps,
emits: ['change', 'click'],
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
};
},
data() {
// this.links = [];
// this.sPrefixCls = '';
return {
activeLink: null,
setup(props, { emit, attrs, slots, expose }) {
const { prefixCls, getTargetContainer, direction } = useConfigInject('anchor', props);
const inkNodeRef = ref();
const anchorRef = ref();
const state = reactive<AnchorState>({
links: [],
sPrefixCls: '',
scrollContainer: null,
scrollEvent: null,
animating: false,
} as AnchorState;
},
created() {
provide('antAnchor', {
registerLink: (link: string) => {
if (!this.links.includes(link)) {
this.links.push(link);
}
},
unregisterLink: (link: string) => {
const index = this.links.indexOf(link);
if (index !== -1) {
this.links.splice(index, 1);
}
},
$data: this.$data,
scrollTo: this.handleScrollTo,
} as AntAnchor);
provide('antAnchorContext', this);
},
mounted() {
nextTick(() => {
const { getContainer } = this;
this.scrollContainer = getContainer();
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
this.handleScroll();
});
},
updated() {
nextTick(() => {
if (this.scrollEvent) {
const { getContainer } = this;
const currentContainer = getContainer();
if (this.scrollContainer !== currentContainer) {
this.scrollContainer = currentContainer;
this.scrollEvent.remove();
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
this.handleScroll();
}
}
this.updateInk();
const activeLink = ref();
const getContainer = computed(() => {
const { getContainer } = props;
return getContainer || getTargetContainer.value || getDefaultContainer;
});
},
beforeUnmount() {
if (this.scrollEvent) {
this.scrollEvent.remove();
}
},
methods: {
getCurrentActiveLink(offsetTop = 0, bounds = 5) {
const { getCurrentAnchor } = this;
if (typeof getCurrentAnchor === 'function') {
return getCurrentAnchor();
}
const activeLink = '';
if (typeof document === 'undefined') {
return activeLink;
}
// func...
const getCurrentAnchor = (offsetTop = 0, bounds = 5) => {
const linkSections: Array<Section> = [];
const { getContainer } = this;
const container = getContainer();
this.links.forEach(link => {
const container = getContainer.value();
state.links.forEach(link => {
const sharpLinkMatch = sharpMatcherRegx.exec(link.toString());
if (!sharpLinkMatch) {
return;
@ -181,12 +119,19 @@ export default defineComponent({
return maxSection.link;
}
return '';
},
};
const setCurrentActiveLink = (link: string) => {
const { getCurrentAnchor } = props;
if (activeLink.value !== link) {
return;
}
activeLink.value = typeof getCurrentAnchor === 'function' ? getCurrentAnchor() : link;
emit('change', link);
};
const handleScrollTo = (link: string) => {
const { offsetTop, getContainer, targetOffset } = props;
handleScrollTo(link: string) {
const { offsetTop, getContainer, targetOffset } = this;
this.setCurrentActiveLink(link);
setCurrentActiveLink(link);
const container = getContainer();
const scrollTop = getScroll(container, true);
const sharpLinkMatch = sharpMatcherRegx.exec(link);
@ -201,99 +146,123 @@ export default defineComponent({
const eleOffsetTop = getOffsetTop(targetElement, container);
let y = scrollTop + eleOffsetTop;
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
this.animating = true;
state.animating = true;
scrollTo(y, {
callback: () => {
this.animating = false;
state.animating = false;
},
getContainer,
});
},
setCurrentActiveLink(link: string) {
const { activeLink } = this;
if (activeLink !== link) {
this.setState({
activeLink: link,
});
this.$emit('change', link);
}
},
handleScroll() {
if (this.animating) {
};
expose({
scrollTo: handleScrollTo,
});
const handleScroll = () => {
if (state.animating) {
return;
}
const { offsetTop, bounds, targetOffset } = this;
const currentActiveLink = this.getCurrentActiveLink(
const { offsetTop, bounds, targetOffset } = props;
const currentActiveLink = getCurrentAnchor(
targetOffset !== undefined ? targetOffset : offsetTop || 0,
bounds,
);
this.setCurrentActiveLink(currentActiveLink);
},
setCurrentActiveLink(currentActiveLink);
};
updateInk() {
if (typeof document === 'undefined') {
return;
}
const { sPrefixCls } = this;
const linkNode = findDOMNode(this).getElementsByClassName(
`${sPrefixCls}-link-title-active`,
const updateInk = () => {
const linkNode = anchorRef.value.getElementsByClassName(
`${prefixCls.value}-link-title-active`,
)[0];
if (linkNode) {
(this.$refs.inkNode as HTMLElement).style.top = `${linkNode.offsetTop +
(inkNodeRef.value as HTMLElement).style.top = `${linkNode.offsetTop +
linkNode.clientHeight / 2 -
4.5}px`;
}
},
},
render() {
const {
prefixCls: customizePrefixCls,
offsetTop,
affix,
showInkInFixed,
activeLink,
$slots,
getContainer,
} = this;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
this.sPrefixCls = prefixCls;
const inkClass = classNames(`${prefixCls}-ink-ball`, {
visible: activeLink,
});
const wrapperClass = classNames(this.wrapperClass, `${prefixCls}-wrapper`);
const anchorClass = classNames(prefixCls, {
fixed: !affix && !showInkInFixed,
});
const wrapperStyle = {
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
...this.wrapperStyle,
};
const anchorContent = (
<div class={wrapperClass} style={wrapperStyle}>
<div class={anchorClass}>
<div class={`${prefixCls}-ink`}>
<span class={inkClass} ref="inkNode" />
</div>
{$slots.default?.()}
</div>
</div>
);
return !affix ? (
anchorContent
) : (
<Affix {...this.$attrs} offsetTop={offsetTop} target={getContainer}>
{anchorContent}
</Affix>
);
useProvideAnchor({
registerLink: (link: string) => {
if (!state.links.includes(link)) {
state.links.push(link);
}
},
unregisterLink: (link: string) => {
const index = state.links.indexOf(link);
if (index !== -1) {
state.links.splice(index, 1);
}
},
activeLink,
scrollTo: handleScrollTo,
handleClick: (e, info) => {
emit('click', e, info);
},
});
onMounted(() => {
nextTick(() => {
const container = getContainer.value();
state.scrollContainer = container;
state.scrollEvent = addEventListener(state.scrollContainer, 'scroll', handleScroll);
handleScroll();
});
});
onBeforeUnmount(() => {
if (state.scrollEvent) {
state.scrollEvent.remove();
}
});
onUpdated(() => {
if (state.scrollEvent) {
const currentContainer = getContainer.value();
if (state.scrollContainer !== currentContainer) {
state.scrollContainer = currentContainer;
state.scrollEvent.remove();
state.scrollEvent = addEventListener(state.scrollContainer, 'scroll', handleScroll);
handleScroll();
}
}
updateInk();
});
return () => {
const { offsetTop, affix, showInkInFixed } = props;
const pre = prefixCls.value;
const inkClass = classNames(`${pre}-ink-ball`, {
visible: activeLink.value,
});
const wrapperClass = classNames(props.wrapperClass, `${pre}-wrapper`, {
[`${pre}-rtl`]: direction.value === 'rtl',
});
const anchorClass = classNames(pre, {
fixed: !affix && !showInkInFixed,
});
const wrapperStyle = {
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
...props.wrapperStyle,
};
const anchorContent = (
<div class={wrapperClass} style={wrapperStyle} ref={anchorRef}>
<div class={anchorClass}>
<div class={`${pre}-ink`}>
<span class={inkClass} ref={inkNodeRef} />
</div>
{slots.default?.()}
</div>
</div>
);
return !affix ? (
anchorContent
) : (
<Affix {...attrs} offsetTop={offsetTop} target={getContainer.value}>
{anchorContent}
</Affix>
);
};
},
});

View File

@ -1,89 +1,91 @@
import { ComponentPublicInstance, defineComponent, inject, nextTick } from 'vue';
import {
defineComponent,
ExtractPropTypes,
nextTick,
onBeforeUnmount,
onMounted,
watch,
} from 'vue';
import PropTypes from '../_util/vue-types';
import { getComponent } from '../_util/props-util';
import { getPropsSlot } from '../_util/props-util';
import classNames from '../_util/classNames';
import { defaultConfigProvider } from '../config-provider';
import { AntAnchor } from './Anchor';
import useConfigInject from '../_util/hooks/useConfigInject';
import { useInjectAnchor } from './context';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function noop(..._any: any[]): any {}
const AnchorLinkProps = {
const anchorLinkProps = {
prefixCls: PropTypes.string,
href: PropTypes.string.def('#'),
title: PropTypes.VNodeChild,
target: PropTypes.string,
};
export type AnchorLinkProps = Partial<ExtractPropTypes<typeof anchorLinkProps>>;
export default defineComponent({
name: 'AAnchorLink',
props: AnchorLinkProps,
setup() {
return {
antAnchor: inject('antAnchor', {
registerLink: noop,
unregisterLink: noop,
scrollTo: noop,
$data: {},
} as AntAnchor),
antAnchorContext: inject('antAnchorContext', {}) as ComponentPublicInstance,
configProvider: inject('configProvider', defaultConfigProvider),
props: anchorLinkProps,
slots: ['title'],
setup(props, { slots }) {
let mergedTitle = null;
const {
handleClick: contextHandleClick,
scrollTo,
unregisterLink,
registerLink,
activeLink,
} = useInjectAnchor();
const { prefixCls } = useConfigInject('anchor', props);
const handleClick = (e: Event) => {
const { href } = props;
contextHandleClick(e, { title: mergedTitle, href });
scrollTo(href);
};
watch(
() => props.href,
(val, oldVal) => {
nextTick(() => {
unregisterLink(oldVal);
registerLink(val);
});
},
);
onMounted(() => {
registerLink(props.href);
});
onBeforeUnmount(() => {
unregisterLink(props.href);
});
return () => {
const { href, target } = props;
const pre = prefixCls.value;
const title = getPropsSlot(slots, props, 'title');
mergedTitle = title;
const active = activeLink.value === href;
const wrapperClassName = classNames(`${pre}-link`, {
[`${pre}-link-active`]: active,
});
const titleClassName = classNames(`${pre}-link-title`, {
[`${pre}-link-title-active`]: active,
});
return (
<div class={wrapperClassName}>
<a
class={titleClassName}
href={href}
title={typeof title === 'string' ? title : ''}
target={target}
onClick={handleClick}
>
{title}
</a>
{slots.default?.()}
</div>
);
};
},
watch: {
href(val, oldVal) {
nextTick(() => {
this.antAnchor.unregisterLink(oldVal);
this.antAnchor.registerLink(val);
});
},
},
mounted() {
this.antAnchor.registerLink(this.href);
},
beforeUnmount() {
this.antAnchor.unregisterLink(this.href);
},
methods: {
handleClick(e: Event) {
this.antAnchor.scrollTo(this.href);
const { scrollTo } = this.antAnchor;
const { href, title } = this.$props;
if (this.antAnchorContext.$emit) {
this.antAnchorContext.$emit('click', e, { title, href });
}
scrollTo(href);
},
},
render() {
const { prefixCls: customizePrefixCls, href, $slots, target } = this;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
const title = getComponent(this, 'title');
const active = this.antAnchor.$data.activeLink === href;
const wrapperClassName = classNames(`${prefixCls}-link`, {
[`${prefixCls}-link-active`]: active,
});
const titleClassName = classNames(`${prefixCls}-link-title`, {
[`${prefixCls}-link-title-active`]: active,
});
return (
<div class={wrapperClassName}>
<a
class={titleClassName}
href={href}
title={typeof title === 'string' ? title : ''}
target={target}
onClick={this.handleClick}
>
{title}
</a>
{$slots.default?.()}
</div>
);
},
});

View File

@ -1,6 +1,5 @@
import { mount } from '@vue/test-utils';
import * as Vue from 'vue';
import { asyncExpect } from '@/tests/utils';
import { ref } from 'vue';
import Anchor from '..';
const { Link } = Anchor;
@ -9,13 +8,20 @@ let idCounter = 0;
const getHashUrl = () => `Anchor-API-${idCounter++}`;
describe('Anchor Render', () => {
it('Anchor render perfectly', done => {
it('Anchor render perfectly', async done => {
const hash = getHashUrl();
const anchor = ref(null);
const activeLink = ref(null);
const wrapper = mount(
{
render() {
return (
<Anchor ref="anchor">
<Anchor
ref={anchor}
onChange={current => {
activeLink.value = current;
}}
>
<Link href={`#${hash}`} title={hash} />
</Anchor>
);
@ -23,22 +29,28 @@ describe('Anchor Render', () => {
},
{ sync: false },
);
Vue.nextTick(() => {
wrapper.vm.$nextTick(() => {
wrapper.find(`a[href="#${hash}`).trigger('click');
wrapper.vm.$refs.anchor.handleScroll();
setTimeout(() => {
expect(wrapper.vm.$refs.anchor.$data.activeLink).not.toBe(null);
expect(activeLink.value).not.toBe(hash);
done();
}, 1000);
});
});
it('Anchor render perfectly for complete href - click', done => {
it('Anchor render perfectly for complete href - click', async done => {
const currentActiveLink = ref(null);
const wrapper = mount(
{
render() {
return (
<Anchor ref="anchor">
<Anchor
ref="anchor"
onChange={current => {
currentActiveLink.value = current;
}}
>
<Link href="http://www.example.com/#API" title="API" />
</Anchor>
);
@ -46,160 +58,163 @@ describe('Anchor Render', () => {
},
{ sync: false },
);
Vue.nextTick(() => {
wrapper.vm.$nextTick(() => {
wrapper.find('a[href="http://www.example.com/#API"]').trigger('click');
expect(wrapper.vm.$refs.anchor.$data.activeLink).toBe('http://www.example.com/#API');
expect(currentActiveLink.value).toBe('http://www.example.com/#API');
done();
});
});
it('Anchor render perfectly for complete href - scroll', done => {
const wrapper = mount(
{
render() {
return (
<div>
<div id="API">Hello</div>
<Anchor ref="anchor">
<Link href="http://www.example.com/#API" title="API" />
</Anchor>
</div>
);
},
},
{ sync: false, attachTo: 'body' },
);
Vue.nextTick(() => {
wrapper.vm.$refs.anchor.handleScroll();
expect(wrapper.vm.$refs.anchor.$data.activeLink).toBe('http://www.example.com/#API');
done();
});
});
it('Anchor render perfectly for complete href - scrollTo', async () => {
const scrollToSpy = jest.spyOn(window, 'scrollTo');
const wrapper = mount(
{
render() {
return (
<div>
<div id="API">Hello</div>
<Anchor ref="anchor">
<Link href="##API" title="API" />
</Anchor>
</div>
);
},
},
{ sync: false, attachTo: 'body' },
);
await asyncExpect(() => {
wrapper.vm.$refs.anchor.handleScrollTo('##API');
expect(wrapper.vm.$refs.anchor.$data.activeLink).toBe('##API');
expect(scrollToSpy).not.toHaveBeenCalled();
});
await asyncExpect(() => {
expect(scrollToSpy).toHaveBeenCalled();
}, 1000);
});
it('should remove listener when unmount', async () => {
const wrapper = mount(
{
render() {
return (
<Anchor ref="anchor">
<Link href="#API" title="API" />
</Anchor>
);
},
},
{ sync: false, attachTo: 'body' },
);
await asyncExpect(() => {
const removeListenerSpy = jest.spyOn(wrapper.vm.$refs.anchor.scrollEvent, 'remove');
wrapper.unmount();
expect(removeListenerSpy).toHaveBeenCalled();
});
});
it('should unregister link when unmount children', async () => {
const wrapper = mount(
{
props: {
showLink: {
type: Boolean,
default: true,
/*
it('Anchor render perfectly for complete href - scroll', done => {
const wrapper = mount(
{
render() {
return (
<div>
<div id="API">Hello</div>
<Anchor ref="anchor">
<Link href="http://www.example.com/#API" title="API" />
</Anchor>
</div>
);
},
},
render() {
return (
<Anchor ref="anchor">{this.showLink ? <Link href="#API" title="API" /> : null}</Anchor>
);
},
},
{ sync: false, attachTo: 'body' },
);
await asyncExpect(() => {
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API']);
wrapper.setProps({ showLink: false });
{ sync: false, attachTo: 'body' },
);
wrapper.vm.$nextTick(() => {
wrapper.vm.$refs.anchor.handleScroll();
expect(wrapper.vm.$refs.anchor.$data.activeLink).toBe('http://www.example.com/#API');
done();
});
});
await asyncExpect(() => {
expect(wrapper.vm.$refs.anchor.links).toEqual([]);
});
});
it('should update links when link href update', async () => {
const wrapper = mount(
{
props: ['href'],
render() {
return (
<Anchor ref="anchor">
<Link href={this.href} title="API" />
</Anchor>
);
},
},
{
sync: false,
attachTo: 'body',
props: {
href: '#API',
},
},
);
await asyncExpect(() => {
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API']);
wrapper.setProps({ href: '#API_1' });
});
await asyncExpect(() => {
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API_1']);
});
});
it('Anchor onClick event', () => {
let event;
let link;
const handleClick = (...arg) => ([event, link] = arg);
const href = '#API';
const title = 'API';
const wrapper = mount({
render() {
return (
<Anchor ref="anchorRef" onClick={handleClick}>
<Link href={href} title={title} />
</Anchor>
it('Anchor render perfectly for complete href - scrollTo', async () => {
const scrollToSpy = jest.spyOn(window, 'scrollTo');
const wrapper = mount(
{
render() {
return (
<div>
<div id="API">Hello</div>
<Anchor ref="anchor">
<Link href="##API" title="API" />
</Anchor>
</div>
);
},
},
{ sync: false, attachTo: 'body' },
);
},
});
await asyncExpect(() => {
wrapper.vm.$refs.anchor.handleScrollTo('##API');
expect(wrapper.vm.$refs.anchor.$data.activeLink).toBe('##API');
expect(scrollToSpy).not.toHaveBeenCalled();
});
await asyncExpect(() => {
expect(scrollToSpy).toHaveBeenCalled();
}, 1000);
});
wrapper.find(`a[href="${href}"]`).trigger('click');
it('should remove listener when unmount', async () => {
const wrapper = mount(
{
render() {
return (
<Anchor ref="anchor">
<Link href="#API" title="API" />
</Anchor>
);
},
},
{ sync: false, attachTo: 'body' },
);
await asyncExpect(() => {
const removeListenerSpy = jest.spyOn(wrapper.vm.$refs.anchor.scrollEvent, 'remove');
wrapper.unmount();
expect(removeListenerSpy).toHaveBeenCalled();
});
});
wrapper.vm.$refs.anchorRef.handleScroll();
expect(event).not.toBe(undefined);
expect(link).toEqual({ href, title });
});
it('should unregister link when unmount children', async () => {
const wrapper = mount(
{
props: {
showLink: {
type: Boolean,
default: true,
},
},
render() {
return (
<Anchor ref="anchor">{this.showLink ? <Link href="#API" title="API" /> : null}</Anchor>
);
},
},
{ sync: false, attachTo: 'body' },
);
await asyncExpect(() => {
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API']);
wrapper.setProps({ showLink: false });
});
await asyncExpect(() => {
expect(wrapper.vm.$refs.anchor.links).toEqual([]);
});
});
it('should update links when link href update', async () => {
const wrapper = mount(
{
props: ['href'],
render() {
return (
<Anchor ref="anchor">
<Link href={this.href} title="API" />
</Anchor>
);
},
},
{
sync: false,
attachTo: 'body',
props: {
href: '#API',
},
},
);
await asyncExpect(() => {
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API']);
wrapper.setProps({ href: '#API_1' });
});
await asyncExpect(() => {
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API_1']);
});
});
it('Anchor onClick event', () => {
let event;
let link;
const handleClick = (...arg) => ([event, link] = arg);
const href = '#API';
const title = 'API';
const anchorRef = Vue.ref(null);
const wrapper = mount({
render() {
return (
<Anchor ref={anchorRef} onClick={handleClick}>
<Link href={href} title={title} />
</Anchor>
);
},
});
wrapper.find(`a[href="${href}"]`).trigger('click');
anchorRef.value.handleScroll();
expect(event).not.toBe(undefined);
expect(link).toEqual({ href, title });
}); */
});

View File

@ -0,0 +1,31 @@
import { computed, Ref, inject, InjectionKey, provide } from 'vue';
export interface AnchorContext {
registerLink: (link: string) => void;
unregisterLink: (link: string) => void;
activeLink: Ref<string>;
scrollTo: (link: string) => void;
handleClick: (e: Event, info: { title: any; href: string }) => void;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function noop(..._any: any[]): any {}
export const AnchorContextKey: InjectionKey<AnchorContext> = Symbol('anchorContextKey');
const useProvideAnchor = (state: AnchorContext) => {
provide(AnchorContextKey, state);
};
const useInjectAnchor = () => {
return inject(AnchorContextKey, {
registerLink: noop,
unregisterLink: noop,
scrollTo: noop,
activeLink: computed(() => ''),
handleClick: noop,
} as AnchorContext);
};
export { useInjectAnchor, useProvideAnchor };
export default useProvideAnchor;

View File

@ -1,6 +1,6 @@
import { App, Plugin } from 'vue';
import Anchor from './Anchor';
import AnchorLink from './AnchorLink';
import Anchor, { AnchorProps } from './Anchor';
import AnchorLink, { AnchorLinkProps } from './AnchorLink';
Anchor.Link = AnchorLink;
@ -11,6 +11,8 @@ Anchor.install = function(app: App) {
return app;
};
export { AnchorLinkProps, AnchorProps, AnchorLink, AnchorLink as Link };
export default Anchor as typeof Anchor &
Plugin & {
readonly Link: typeof AnchorLink;

View File

@ -13,7 +13,7 @@
margin-left: -4px;
padding-left: 4px;
overflow: auto;
background-color: @component-background;
background-color: @anchor-bg;
}
&-ink {
@ -52,7 +52,7 @@
}
&-link {
padding: 7px 0 7px 16px;
padding: @anchor-link-padding;
line-height: 1.143;
&-title {
@ -80,3 +80,5 @@
padding-bottom: 5px;
}
}
@import './rtl';

View File

@ -0,0 +1,35 @@
.@{ant-prefix}-anchor {
&-rtl {
direction: rtl;
}
&-wrapper {
.@{ant-prefix}-anchor-rtl& {
margin-right: -4px;
margin-left: 0;
padding-right: 4px;
padding-left: 0;
}
}
&-ink {
.@{ant-prefix}-anchor-rtl & {
right: 0;
left: auto;
}
&-ball {
.@{ant-prefix}-anchor-rtl & {
right: 50%;
left: 0;
transform: translateX(50%);
}
}
}
&-link {
.@{ant-prefix}-anchor-rtl & {
padding: @anchor-link-top @anchor-link-left @anchor-link-top 0;
}
}
}

View File

@ -1,134 +1,177 @@
import { tuple, VueNode } from '../_util/type';
import { CSSProperties, defineComponent, inject, nextTick, PropType } from 'vue';
import { defaultConfigProvider } from '../config-provider';
import { getComponent } from '../_util/props-util';
import {
computed,
CSSProperties,
defineComponent,
ExtractPropTypes,
nextTick,
onMounted,
PropType,
ref,
watch,
} from 'vue';
import { getPropsSlot } from '../_util/props-util';
import PropTypes from '../_util/vue-types';
import useBreakpoint from '../_util/hooks/useBreakpoint';
import { Breakpoint, responsiveArray, ScreenSizeMap } from '../_util/responsiveObserve';
import useConfigInject from '../_util/hooks/useConfigInject';
import ResizeObserver from '../vc-resize-observer';
import { useInjectSize } from '../_util/hooks/useSize';
export default defineComponent({
export type AvatarSize = 'large' | 'small' | 'default' | number | ScreenSizeMap;
export const avatarProps = {
prefixCls: PropTypes.string,
shape: PropTypes.oneOf(tuple('circle', 'square')).def('circle'),
size: {
type: [Number, String, Object] as PropType<AvatarSize>,
default: (): AvatarSize => 'default',
},
src: PropTypes.string,
/** Srcset of image avatar */
srcset: PropTypes.string,
icon: PropTypes.VNodeChild,
alt: PropTypes.string,
gap: PropTypes.number,
draggable: PropTypes.bool,
loadError: {
type: Function as PropType<() => boolean>,
},
};
export type AvatarProps = Partial<ExtractPropTypes<typeof avatarProps>>;
const Avatar = defineComponent({
name: 'AAvatar',
props: {
prefixCls: PropTypes.string,
shape: PropTypes.oneOf(tuple('circle', 'square')),
size: {
type: [Number, String] as PropType<'large' | 'small' | 'default' | number>,
default: 'default',
},
src: PropTypes.string,
/** Srcset of image avatar */
srcset: PropTypes.string,
/** @deprecated please use `srcset` instead `srcSet` */
srcSet: PropTypes.string,
icon: PropTypes.VNodeChild,
alt: PropTypes.string,
loadError: {
type: Function as PropType<() => boolean>,
},
},
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
};
},
data() {
return {
isImgExist: true,
isMounted: false,
scale: 1,
lastChildrenWidth: undefined,
lastNodeWidth: undefined,
};
},
watch: {
src() {
nextTick(() => {
this.isImgExist = true;
this.scale = 1;
// force uodate for position
this.$forceUpdate();
});
},
},
mounted() {
nextTick(() => {
this.setScale();
this.isMounted = true;
inheritAttrs: false,
props: avatarProps,
slots: ['icon'],
setup(props, { slots, attrs }) {
const isImgExist = ref(true);
const isMounted = ref(false);
const scale = ref(1);
const avatarChildrenRef = ref<HTMLElement>(null);
const avatarNodeRef = ref<HTMLElement>(null);
const { prefixCls } = useConfigInject('avatar', props);
const groupSize = useInjectSize();
const screens = useBreakpoint();
const responsiveSize = computed(() => {
if (typeof props.size !== 'object') {
return undefined;
}
const currentBreakpoint: Breakpoint = responsiveArray.find(screen => screens.value[screen])!;
const currentSize = props.size[currentBreakpoint];
return currentSize;
});
},
updated() {
nextTick(() => {
this.setScale();
});
},
methods: {
setScale() {
if (!this.$refs.avatarChildren || !this.$refs.avatarNode) {
const responsiveSizeStyle = (hasIcon: boolean) => {
if (responsiveSize.value) {
return {
width: `${responsiveSize.value}px`,
height: `${responsiveSize.value}px`,
lineHeight: `${responsiveSize.value}px`,
fontSize: `${hasIcon ? responsiveSize.value / 2 : 18}px`,
};
}
return {};
};
const setScaleParam = () => {
if (!avatarChildrenRef.value || !avatarNodeRef.value) {
return;
}
const childrenWidth = (this.$refs.avatarChildren as HTMLElement).offsetWidth; // offsetWidth avoid affecting be transform scale
const nodeWidth = (this.$refs.avatarNode as HTMLElement).offsetWidth;
const childrenWidth = avatarChildrenRef.value.offsetWidth; // offsetWidth avoid affecting be transform scale
const nodeWidth = avatarNodeRef.value.offsetWidth;
// denominator is 0 is no meaning
if (
childrenWidth === 0 ||
nodeWidth === 0 ||
(this.lastChildrenWidth === childrenWidth && this.lastNodeWidth === nodeWidth)
) {
return;
if (childrenWidth !== 0 && nodeWidth !== 0) {
const { gap = 4 } = props;
if (gap * 2 < nodeWidth) {
scale.value =
nodeWidth - gap * 2 < childrenWidth ? (nodeWidth - gap * 2) / childrenWidth : 1;
}
}
this.lastChildrenWidth = childrenWidth;
this.lastNodeWidth = nodeWidth;
// add 4px gap for each side to get better performance
this.scale = nodeWidth - 8 < childrenWidth ? (nodeWidth - 8) / childrenWidth : 1;
},
handleImgLoadError() {
const { loadError } = this.$props;
const errorFlag = loadError ? loadError() : undefined;
};
const handleImgLoadError = () => {
const { loadError } = props;
const errorFlag = loadError?.();
if (errorFlag !== false) {
this.isImgExist = false;
isImgExist.value = false;
}
},
},
render() {
const { prefixCls: customizePrefixCls, shape, size, src, alt, srcset, srcSet } = this.$props;
const icon = getComponent(this, 'icon');
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('avatar', customizePrefixCls);
const { isImgExist, scale, isMounted } = this.$data;
const sizeCls = {
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-sm`]: size === 'small',
};
const classString = {
[prefixCls]: true,
...sizeCls,
[`${prefixCls}-${shape}`]: shape,
[`${prefixCls}-image`]: src && isImgExist,
[`${prefixCls}-icon`]: icon,
};
watch(
() => props.src,
() => {
nextTick(() => {
isImgExist.value = true;
scale.value = 1;
});
},
);
const sizeStyle: CSSProperties =
typeof size === 'number'
? {
width: `${size}px`,
height: `${size}px`,
lineHeight: `${size}px`,
fontSize: icon ? `${size / 2}px` : '18px',
}
: {};
watch(
() => props.gap,
() => {
nextTick(() => {
setScaleParam();
});
},
);
let children: VueNode = this.$slots.default?.();
if (src && isImgExist) {
children = (
<img src={src} srcset={srcset || srcSet} onError={this.handleImgLoadError} alt={alt} />
);
} else if (icon) {
children = icon;
} else {
const childrenNode = this.$refs.avatarChildren;
if (childrenNode || scale !== 1) {
const transformString = `scale(${scale}) translateX(-50%)`;
onMounted(() => {
nextTick(() => {
setScaleParam();
isMounted.value = true;
});
});
return () => {
const { shape, size: customSize, src, alt, srcset, draggable } = props;
const icon = getPropsSlot(slots, props, 'icon');
const pre = prefixCls.value;
const size = customSize === 'default' ? groupSize.value : customSize;
const classString = {
[`${attrs.class}`]: !!attrs.class,
[pre]: true,
[`${pre}-lg`]: size === 'large',
[`${pre}-sm`]: size === 'small',
[`${pre}-${shape}`]: shape,
[`${pre}-image`]: src && isImgExist.value,
[`${pre}-icon`]: icon,
};
const sizeStyle: CSSProperties =
typeof size === 'number'
? {
width: `${size}px`,
height: `${size}px`,
lineHeight: `${size}px`,
fontSize: icon ? `${size / 2}px` : '18px',
}
: {};
const children: VueNode = slots.default?.();
let childrenToRender;
if (src && isImgExist.value) {
childrenToRender = (
<img
draggable={draggable}
src={src}
srcset={srcset}
onError={handleImgLoadError}
alt={alt}
/>
);
} else if (icon) {
childrenToRender = icon;
} else if (isMounted.value || scale.value !== 1) {
const transformString = `scale(${scale.value}) translateX(-50%)`;
const childrenStyle: CSSProperties = {
msTransform: transformString,
WebkitTransform: transformString,
@ -140,31 +183,40 @@ export default defineComponent({
lineHeight: `${size}px`,
}
: {};
children = (
<span
class={`${prefixCls}-string`}
ref="avatarChildren"
style={{ ...sizeChildrenStyle, ...childrenStyle }}
>
{children}
</span>
childrenToRender = (
<ResizeObserver onResize={setScaleParam}>
<span
class={`${pre}-string`}
ref={avatarChildrenRef}
style={{ ...sizeChildrenStyle, ...childrenStyle }}
>
{children}
</span>
</ResizeObserver>
);
} else {
const childrenStyle: CSSProperties = {};
if (!isMounted) {
childrenStyle.opacity = 0;
}
children = (
<span class={`${prefixCls}-string`} ref="avatarChildren" style={{ opacity: 0 }}>
childrenToRender = (
<span class={`${pre}-string`} ref={avatarChildrenRef} style={{ opacity: 0 }}>
{children}
</span>
);
}
}
return (
<span ref="avatarNode" class={classString} style={sizeStyle}>
{children}
</span>
);
return (
<span
{...attrs}
ref={avatarNodeRef}
class={classString}
style={{
...sizeStyle,
...responsiveSizeStyle(!!icon),
...(attrs.style as CSSProperties),
}}
>
{childrenToRender}
</span>
);
};
},
});
export default Avatar;

View File

@ -0,0 +1,85 @@
import { cloneElement } from '../_util/vnode';
import Avatar, { avatarProps, AvatarSize } from './Avatar';
import Popover from '../popover';
import { defineComponent, PropType, ExtractPropTypes, CSSProperties } from 'vue';
import PropTypes from '../_util/vue-types';
import { flattenChildren, getPropsSlot } from '../_util/props-util';
import { tuple } from '../_util/type';
import useConfigInject from '../_util/hooks/useConfigInject';
import useProvideSize from '../_util/hooks/useSize';
const groupProps = {
prefixCls: PropTypes.string,
maxCount: PropTypes.number,
maxStyle: {
type: Object as PropType<CSSProperties>,
default: () => ({} as CSSProperties),
},
maxPopoverPlacement: PropTypes.oneOf(tuple('top', 'bottom')).def('top'),
/*
* Size of avatar, options: `large`, `small`, `default`
* or a custom number size
* */
size: avatarProps.size,
};
export type AvatarGroupProps = Partial<ExtractPropTypes<typeof groupProps>> & {
size?: AvatarSize;
};
const Group = defineComponent({
name: 'AAvatarGroup',
inheritAttrs: false,
props: groupProps,
setup(props, { slots, attrs }) {
const { prefixCls, direction } = useConfigInject('avatar-group', props);
useProvideSize<AvatarSize>(props);
return () => {
const { maxPopoverPlacement = 'top', maxCount, maxStyle } = props;
const cls = {
[prefixCls.value]: true,
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
[`${attrs.class}`]: !!attrs.class,
};
const children = getPropsSlot(slots, props);
const childrenWithProps = flattenChildren(children).map((child, index) =>
cloneElement(child, {
key: `avatar-key-${index}`,
}),
);
const numOfChildren = childrenWithProps.length;
if (maxCount && maxCount < numOfChildren) {
const childrenShow = childrenWithProps.slice(0, maxCount);
const childrenHidden = childrenWithProps.slice(maxCount, numOfChildren);
childrenShow.push(
<Popover
key="avatar-popover-key"
content={childrenHidden}
trigger="hover"
placement={maxPopoverPlacement}
overlayClassName={`${prefixCls.value}-popover`}
>
<Avatar style={maxStyle}>{`+${numOfChildren - maxCount}`}</Avatar>
</Popover>,
);
return (
<div {...attrs} class={cls} style={attrs.style}>
{childrenShow}
</div>
);
}
return (
<div {...attrs} class={cls} style={attrs.style}>
{childrenWithProps}
</div>
);
};
},
});
export default Group;

View File

@ -1,9 +1,14 @@
import { mount } from '@vue/test-utils';
import { asyncExpect } from '@/tests/utils';
import Avatar from '..';
import useBreakpoint from '../../_util/hooks/useBreakpoint';
jest.mock('../../_util/hooks/useBreakpoint');
describe('Avatar Render', () => {
let originOffsetWidth;
const sizes = { xs: 24, sm: 32, md: 40, lg: 64, xl: 80, xxl: 100 };
beforeAll(() => {
// Mock offsetHeight
originOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth').get;
@ -41,16 +46,8 @@ describe('Avatar Render', () => {
props: {
src: 'http://error.url',
},
sync: false,
attachTo: 'body',
});
wrapper.vm.setScale = jest.fn(() => {
if (wrapper.vm.scale === 0.5) {
return;
}
wrapper.vm.scale = 0.5;
wrapper.vm.$forceUpdate();
});
await asyncExpect(() => {
wrapper.find('img').trigger('error');
}, 0);
@ -58,14 +55,7 @@ describe('Avatar Render', () => {
const children = wrapper.findAll('.ant-avatar-string');
expect(children.length).toBe(1);
expect(children[0].text()).toBe('Fallback');
expect(wrapper.vm.setScale).toHaveBeenCalled();
});
await asyncExpect(() => {
expect(global.document.body.querySelector('.ant-avatar-string').style.transform).toContain(
'scale(0.5)',
);
global.document.body.innerHTML = '';
}, 1000);
});
it('should handle onError correctly', async () => {
global.document.body.innerHTML = '';
@ -91,17 +81,17 @@ describe('Avatar Render', () => {
},
};
const wrapper = mount(Foo, { sync: false, attachTo: 'body' });
const wrapper = mount(Foo, { attachTo: 'body' });
await asyncExpect(() => {
// mock img load Error, since jsdom do not load resource by default
// https://github.com/jsdom/jsdom/issues/1816
wrapper.find('img').trigger('error');
}, 0);
await asyncExpect(() => {
expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(true);
expect(wrapper.find('img')).not.toBeNull();
}, 0);
await asyncExpect(() => {
expect(global.document.body.querySelector('img').getAttribute('src')).toBe(LOAD_SUCCESS_SRC);
expect(wrapper.find('img').attributes('src')).toBe(LOAD_SUCCESS_SRC);
}, 0);
});
@ -126,9 +116,8 @@ describe('Avatar Render', () => {
await asyncExpect(() => {
wrapper.find('img').trigger('error');
}, 0);
await asyncExpect(() => {
expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(false);
expect(wrapper.findComponent({ name: 'AAvatar' }).findAll('img').length).toBe(0);
expect(wrapper.findAll('.ant-avatar-string').length).toBe(1);
}, 0);
@ -136,8 +125,87 @@ describe('Avatar Render', () => {
wrapper.vm.src = LOAD_SUCCESS_SRC;
});
await asyncExpect(() => {
expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(true);
expect(wrapper.findComponent({ name: 'AAvatar' }).findAll('img').length).toBe(1);
expect(wrapper.findAll('.ant-avatar-image').length).toBe(1);
}, 0);
});
it('should calculate scale of avatar children correctly', async () => {
let wrapper = mount({
render() {
return <Avatar>Avatar</Avatar>;
},
});
await asyncExpect(() => {
expect(wrapper.find('.ant-avatar-string')).toMatchSnapshot();
}, 0);
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
get() {
if (this.className === 'ant-avatar-string') {
return 100;
}
return 40;
},
});
wrapper = mount({
render() {
return <Avatar>xx</Avatar>;
},
});
await asyncExpect(() => {
expect(wrapper.find('.ant-avatar-string')).toMatchSnapshot();
}, 0);
});
it('should calculate scale of avatar children correctly with gap', async () => {
const wrapper = mount({
render() {
return <Avatar gap={2}>Avatar</Avatar>;
},
});
await asyncExpect(() => {
expect(wrapper.html()).toMatchSnapshot();
}, 0);
});
Object.entries(sizes).forEach(([key, value]) => {
it(`adjusts component size to ${value} when window size is ${key}`, async () => {
useBreakpoint.mockReturnValue({ value: { [key]: true } });
const wrapper = mount({
render() {
return <Avatar size={sizes} />;
},
});
await asyncExpect(() => {
expect(wrapper.html()).toMatchSnapshot();
}, 0);
});
});
it('fallback', async () => {
const div = global.document.createElement('div');
global.document.body.appendChild(div);
const wrapper = mount(
{
render() {
return (
<Avatar shape="circle" src="http://error.url">
A
</Avatar>
);
},
},
{ attachTo: div },
);
await asyncExpect(async () => {
await wrapper.find('img').trigger('error');
expect(wrapper.html()).toMatchSnapshot();
wrapper.unmount();
global.document.body.removeChild(div);
}, 0);
});
});

View File

@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Avatar Render adjusts component size to 24 when window size is xs 1`] = `<span class="ant-avatar" style="width: 24px; height: 24px; line-height: 24px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
exports[`Avatar Render adjusts component size to 32 when window size is sm 1`] = `<span class="ant-avatar" style="width: 32px; height: 32px; line-height: 32px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
exports[`Avatar Render adjusts component size to 40 when window size is md 1`] = `<span class="ant-avatar" style="width: 40px; height: 40px; line-height: 40px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
exports[`Avatar Render adjusts component size to 64 when window size is lg 1`] = `<span class="ant-avatar" style="width: 64px; height: 64px; line-height: 64px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
exports[`Avatar Render adjusts component size to 80 when window size is xl 1`] = `<span class="ant-avatar" style="width: 80px; height: 80px; line-height: 80px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
exports[`Avatar Render adjusts component size to 100 when window size is xxl 1`] = `<span class="ant-avatar" style="width: 100px; height: 100px; line-height: 100px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
exports[`Avatar Render fallback 1`] = `<span class="ant-avatar ant-avatar-circle"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);">A</span></span>`;
exports[`Avatar Render should calculate scale of avatar children correctly 1`] = `
DOMWrapper {
"wrapperElement": <span
class="ant-avatar-string"
style="transform: scale(0.72) translateX(-50%);"
>
Avatar
</span>,
}
`;
exports[`Avatar Render should calculate scale of avatar children correctly 2`] = `
DOMWrapper {
"wrapperElement": <span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
>
xx
</span>,
}
`;
exports[`Avatar Render should calculate scale of avatar children correctly with gap 1`] = `<span class="ant-avatar"><span class="ant-avatar-string" style="transform: scale(0.36) translateX(-50%);">Avatar</span></span>`;

View File

@ -1,4 +1,20 @@
import { App, Plugin } from 'vue';
import Avatar from './Avatar';
import { withInstall } from '../_util/type';
import Group from './Group';
export default withInstall(Avatar);
export { AvatarProps, AvatarSize, avatarProps } from './Avatar';
export { AvatarGroupProps } from './Group';
Avatar.Group = Group;
/* istanbul ignore next */
Avatar.install = function(app: App) {
app.component(Avatar.name, Avatar);
app.component(Group.name, Group);
return app;
};
export default Avatar as typeof Avatar &
Plugin & {
readonly Group: typeof Group;
};

View File

@ -0,0 +1,17 @@
.@{avatar-prefix-cls}-group {
display: inline-flex;
.@{avatar-prefix-cls} {
border: 1px solid @avatar-group-border-color;
&:not(:first-child) {
margin-left: @avatar-group-overlapping;
}
}
&-popover {
.@{ant-prefix}-avatar + .@{ant-prefix}-avatar {
margin-left: @avatar-group-space;
}
}
}

View File

@ -7,19 +7,22 @@
.reset-component();
position: relative;
display: inline-flex;
display: inline-block;
overflow: hidden;
color: @avatar-color;
white-space: nowrap;
text-align: center;
vertical-align: middle;
background: @avatar-bg;
justify-content: center;
align-items: center;
&-image {
background: transparent;
}
.@{ant-prefix}-image-img {
display: block;
}
.avatar-size(@avatar-size-base, @avatar-font-size-base);
&-lg {
@ -45,6 +48,7 @@
.avatar-size(@size, @font-size) {
width: @size;
height: @size;
line-height: @size;
border-radius: 50%;
&-string {
@ -55,8 +59,12 @@
&.@{avatar-prefix-cls}-icon {
font-size: @font-size;
.@{iconfont-css-prefix} {
> .@{iconfont-css-prefix} {
margin: 0;
}
}
}
@import './group';
@import './rtl';

View File

@ -0,0 +1,15 @@
.@{avatar-prefix-cls}-group {
&-rtl {
.@{avatar-prefix-cls}:not(:first-child) {
margin-right: @avatar-group-overlapping;
margin-left: 0;
}
}
&-popover.@{ant-prefix}-popover-rtl {
.@{ant-prefix}-avatar + .@{ant-prefix}-avatar {
margin-right: @avatar-group-space;
margin-left: 0;
}
}
}

View File

@ -1,9 +0,0 @@
import PropTypes from '../_util/vue-types';
export default () => ({
visibilityHeight: PropTypes.number,
// onClick?: React.MouseEventHandler<any>;
target: PropTypes.func,
prefixCls: PropTypes.string,
onClick: PropTypes.func,
// visible: PropTypes.looseBool, // Only for test. Don't use it.
});

View File

@ -1,108 +1,147 @@
import { defineComponent, inject, nextTick } from 'vue';
import classNames from '../_util/classNames';
import {
defineComponent,
ExtractPropTypes,
inject,
nextTick,
onActivated,
onBeforeUnmount,
onMounted,
reactive,
PropType,
ref,
watch,
onDeactivated,
computed,
} from 'vue';
import VerticalAlignTopOutlined from '@ant-design/icons-vue/VerticalAlignTopOutlined';
import PropTypes from '../_util/vue-types';
import backTopTypes from './backTopTypes';
import addEventListener from '../vc-util/Dom/addEventListener';
import getScroll from '../_util/getScroll';
import BaseMixin from '../_util/BaseMixin';
import { getTransitionProps, Transition } from '../_util/transition';
import { defaultConfigProvider } from '../config-provider';
import scrollTo from '../_util/scrollTo';
import { withInstall } from '../_util/type';
import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
function getDefaultTarget() {
return window;
}
export const backTopProps = {
visibilityHeight: PropTypes.number.def(400),
duration: PropTypes.number.def(450),
target: Function as PropType<() => HTMLElement | Window | Document>,
prefixCls: PropTypes.string,
onClick: PropTypes.func,
// visible: PropTypes.looseBool, // Only for test. Don't use it.
};
const props = backTopTypes();
export type BackTopProps = Partial<ExtractPropTypes<typeof backTopProps>>;
const BackTop = defineComponent({
name: 'ABackTop',
mixins: [BaseMixin],
inheritAttrs: false,
props: {
...props,
visibilityHeight: PropTypes.number.def(400),
},
props: backTopProps,
emits: ['click'],
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
};
},
data() {
return {
setup(props, { slots, attrs, emit }) {
const configProvider = inject('configProvider', defaultConfigProvider);
const domRef = ref();
const state = reactive({
visible: false,
scrollEvent: null,
};
},
mounted() {
nextTick(() => {
const getTarget = this.target || getDefaultTarget;
this.scrollEvent = addEventListener(getTarget(), 'scroll', this.handleScroll);
this.handleScroll();
});
},
activated() {
nextTick(() => {
this.handleScroll();
});
},
beforeUnmount() {
if (this.scrollEvent) {
this.scrollEvent.remove();
}
},
methods: {
getCurrentScrollTop() {
const getTarget = this.target || getDefaultTarget;
const targetNode = getTarget();
if (targetNode === window) {
return window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
}
return targetNode.scrollTop;
},
const getDefaultTarget = () =>
domRef.value && domRef.value.ownerDocument ? domRef.value.ownerDocument : window;
scrollToTop(e: Event) {
const { target = getDefaultTarget } = this;
const scrollToTop = (e: Event) => {
const { target = getDefaultTarget, duration } = props;
scrollTo(0, {
getContainer: target,
duration,
});
this.$emit('click', e);
},
handleScroll() {
const { visibilityHeight, target = getDefaultTarget } = this;
const scrollTop = getScroll(target(), true);
this.setState({
visible: scrollTop > visibilityHeight,
});
},
},
render() {
const { prefixCls: customizePrefixCls, $slots } = this;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('back-top', customizePrefixCls);
const classString = classNames(prefixCls, this.$attrs.class);
const defaultElement = (
<div class={`${prefixCls}-content`}>
<div class={`${prefixCls}-icon`} />
</div>
);
const divProps = {
...this.$attrs,
onClick: this.scrollToTop,
class: classString,
emit('click', e);
};
const backTopBtn = this.visible ? (
<div {...divProps}>{$slots.default?.() || defaultElement}</div>
) : null;
const transitionProps = getTransitionProps('fade');
return <Transition {...transitionProps}>{backTopBtn}</Transition>;
const handleScroll = throttleByAnimationFrame((e: Event | { target: any }) => {
const { visibilityHeight } = props;
const scrollTop = getScroll(e.target, true);
state.visible = scrollTop > visibilityHeight;
});
const bindScrollEvent = () => {
const { target } = props;
const getTarget = target || getDefaultTarget;
const container = getTarget();
state.scrollEvent = addEventListener(container, 'scroll', (e: Event) => {
handleScroll(e);
});
handleScroll({
target: container,
});
};
const scrollRemove = () => {
if (state.scrollEvent) {
state.scrollEvent.remove();
}
(handleScroll as any).cancel();
};
watch(
() => props.target,
() => {
scrollRemove();
nextTick(() => {
bindScrollEvent();
});
},
);
onMounted(() => {
nextTick(() => {
bindScrollEvent();
});
});
onActivated(() => {
nextTick(() => {
bindScrollEvent();
});
});
onDeactivated(() => {
scrollRemove();
});
onBeforeUnmount(() => {
scrollRemove();
});
const prefixCls = computed(() => configProvider.getPrefixCls('back-top', props.prefixCls));
return () => {
const defaultElement = (
<div class={`${prefixCls.value}-content`}>
<div class={`${prefixCls.value}-icon`}>
<VerticalAlignTopOutlined />
</div>
</div>
);
const divProps = {
...attrs,
onClick: scrollToTop,
class: {
[`${prefixCls.value}`]: true,
[`${attrs.class}`]: attrs.class,
[`${prefixCls.value}-rtl`]: configProvider.direction === 'rtl',
},
};
const backTopBtn = state.visible ? (
<div {...divProps} ref={domRef}>
{slots.default?.() || defaultElement}
</div>
) : null;
const transitionProps = getTransitionProps('fade');
return <Transition {...transitionProps}>{backTopBtn}</Transition>;
};
},
});

View File

@ -14,6 +14,16 @@
height: 40px;
cursor: pointer;
&:empty {
display: none;
}
&-rtl {
right: auto;
left: 100px;
direction: rtl;
}
&-content {
width: 40px;
height: 40px;
@ -22,20 +32,17 @@
text-align: center;
background-color: @back-top-bg;
border-radius: 20px;
transition: all 0.3s @ease-in-out;
transition: all 0.3s;
&:hover {
background-color: @back-top-hover-bg;
transition: all 0.3s @ease-in-out;
transition: all 0.3s;
}
}
&-icon {
width: 14px;
height: 16px;
margin: 12px auto;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAoCAYAAACWwljjAAAABGdBTUEAALGPC/xhBQAAAbtJREFUWAntmMtKw0AUhhMvS5cuxILgQlRUpIggIoKIIoigG1eC+AA+jo+i6FIXBfeuXIgoeKVeitVWJX5HWhhDksnUpp3FDPyZk3Nm5nycmZKkXhAEOXSA3lG7muTeRzmfy6HneUvIhnYkQK+Q9NhAA0Opg0vBEhjBKHiyb8iGMyQMOYuK41BcBSypAL+MYXSKjtFAW7EAGEO3qN4uMQbbAkXiSfRQJ1H6a+yhlkKRcAoVFYiweYNjtCVQJJpBz2GCiPt7fBOZQpFgDpUikse5HgnkM4Fi4QX0Fpc5wf9EbLqpUCy4jMoJSXWhFwbMNgWKhVbRhy5jirhs9fy/oFhgHVVTJEs7RLZ8sSEoJm6iz7SZDMbJ+/OKERQTttCXQRLToRUmrKWCYuA2+jbN0MB4OQobYShfdTCgn/sL1K36M7TLrN3n+758aPy2rrpR6+/od5E8tf/A1uLS9aId5T7J3CNYihkQ4D9PiMdMC7mp4rjB9kjFjZp8BlnVHJBuO1yFXIV0FdDF3RlyFdJVQBdv5AxVdIsq8apiZ2PyYO1EVykesGfZEESsCkweyR8MUW+V8uJ1gkYipmpdP1pm2aJVPEGzAAAAAElFTkSuQmCC)
~'100%/100%' no-repeat;
font-size: 24px;
line-height: 40px;
}
}

View File

@ -1,27 +1,29 @@
import PropTypes from '../_util/vue-types';
import ScrollNumber from './ScrollNumber';
import classNames from '../_util/classNames';
import { initDefaultProps, getComponent, getSlot } from '../_util/props-util';
import { getPropsSlot, flattenChildren } from '../_util/props-util';
import { cloneElement } from '../_util/vnode';
import { getTransitionProps, Transition } from '../_util/transition';
import isNumeric from '../_util/isNumeric';
import { defaultConfigProvider } from '../config-provider';
import { inject, defineComponent, CSSProperties, VNode, App, Plugin } from 'vue';
import { defineComponent, ExtractPropTypes, CSSProperties, computed, ref, watch } from 'vue';
import { tuple } from '../_util/type';
import Ribbon from './Ribbon';
import { isPresetColor } from './utils';
import useConfigInject from '../_util/hooks/useConfigInject';
import isNumeric from '../_util/isNumeric';
const BadgeProps = {
export const badgeProps = {
/** Number to show in badge */
count: PropTypes.VNodeChild,
count: PropTypes.any,
showZero: PropTypes.looseBool,
/** Max count to show */
overflowCount: PropTypes.number,
overflowCount: PropTypes.number.def(99),
/** whether to show red dot without number */
dot: PropTypes.looseBool,
prefixCls: PropTypes.string,
scrollNumberPrefixCls: PropTypes.string,
status: PropTypes.oneOf(tuple('success', 'processing', 'default', 'error', 'warning')),
// sync antd@4.6.0
size: PropTypes.oneOf(tuple('default', 'small')).def('default'),
color: PropTypes.string,
text: PropTypes.VNodeChild,
offset: PropTypes.arrayOf(PropTypes.oneOfType([String, Number])),
@ -29,210 +31,192 @@ const BadgeProps = {
title: PropTypes.string,
};
const Badge = defineComponent({
export type BadgeProps = Partial<ExtractPropTypes<typeof badgeProps>>;
export default defineComponent({
name: 'ABadge',
Ribbon,
props: initDefaultProps(BadgeProps, {
showZero: false,
dot: false,
overflowCount: 99,
}) as typeof BadgeProps,
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
badgeCount: undefined,
};
},
methods: {
getNumberedDispayCount() {
const { overflowCount } = this.$props;
const count = this.badgeCount;
const displayCount = count > overflowCount ? `${overflowCount}+` : count;
return displayCount;
},
inheritAttrs: false,
props: badgeProps,
slots: ['text', 'count'],
setup(props, { slots, attrs }) {
const { prefixCls, direction } = useConfigInject('badge', props);
getDispayCount() {
const isDot = this.isDot();
// dot mode don't need count
if (isDot) {
return '';
// ================================ Misc ================================
const numberedDisplayCount = computed(() => {
return ((props.count as number) > (props.overflowCount as number)
? `${props.overflowCount}+`
: props.count) as string | number | null;
});
const hasStatus = computed(
() =>
(props.status !== null && props.status !== undefined) ||
(props.color !== null && props.color !== undefined),
);
const isZero = computed(
() => numberedDisplayCount.value === '0' || numberedDisplayCount.value === 0,
);
const showAsDot = computed(() => (props.dot && !isZero.value) || hasStatus.value);
const mergedCount = computed(() => (showAsDot.value ? '' : numberedDisplayCount.value));
const isHidden = computed(() => {
const isEmpty =
mergedCount.value === null || mergedCount.value === undefined || mergedCount.value === '';
return (isEmpty || (isZero.value && !props.showZero)) && !showAsDot.value;
});
// Count should be cache in case hidden change it
const livingCount = ref(props.count);
// We need cache count since remove motion should not change count display
const displayCount = ref(mergedCount.value);
// We will cache the dot status to avoid shaking on leaved motion
const isDotRef = ref(showAsDot.value);
watch(
[() => props.count, mergedCount, showAsDot],
() => {
if (!isHidden.value) {
livingCount.value = props.count;
displayCount.value = mergedCount.value;
isDotRef.value = showAsDot.value;
}
},
{ immediate: true },
);
// Shared styles
const statusCls = computed(() => ({
[`${prefixCls.value}-status-dot`]: hasStatus.value,
[`${prefixCls.value}-status-${props.status}`]: !!props.status,
[`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color),
}));
const statusStyle = computed(() => {
if (props.color && !isPresetColor(props.color)) {
return { background: props.color };
} else {
return {};
}
return this.getNumberedDispayCount();
},
});
getScrollNumberTitle() {
const { title } = this.$props;
const count = this.badgeCount;
if (title) {
return title;
}
return typeof count === 'string' || typeof count === 'number' ? count : undefined;
},
const scrollNumberCls = computed(() => ({
[`${prefixCls.value}-dot`]: isDotRef.value,
[`${prefixCls.value}-count`]: !isDotRef.value,
[`${prefixCls.value}-count-sm`]: props.size === 'small',
[`${prefixCls.value}-multiple-words`]:
!isDotRef.value && displayCount.value && displayCount.value.toString().length > 1,
[`${prefixCls.value}-status-${status}`]: !!status,
[`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color),
}));
getStyleWithOffset() {
const { offset, numberStyle } = this.$props;
return offset
? {
right: `${-parseInt(offset[0] as string, 10)}px`,
marginTop: isNumeric(offset[1]) ? `${offset[1]}px` : offset[1],
...numberStyle,
}
: { ...numberStyle };
},
getBadgeClassName(prefixCls: string, children: VNode[]) {
const hasStatus = this.hasStatus();
return classNames(prefixCls, {
[`${prefixCls}-status`]: hasStatus,
[`${prefixCls}-dot-status`]: hasStatus && this.dot && !this.isZero(),
[`${prefixCls}-not-a-wrapper`]: !children.length,
});
},
hasStatus() {
const { status, color } = this.$props;
return !!status || !!color;
},
isZero() {
const numberedDispayCount = this.getNumberedDispayCount();
return numberedDispayCount === '0' || numberedDispayCount === 0;
},
return () => {
const { offset, title, color } = props;
const style = attrs.style as CSSProperties;
const text = getPropsSlot(slots, props, 'text');
const pre = prefixCls.value;
const count = livingCount.value;
let children = flattenChildren(slots.default?.());
children = children.length ? children : null;
isDot() {
const { dot } = this.$props;
const isZero = this.isZero();
return (dot && !isZero) || this.hasStatus();
},
const visible = !!(!isHidden.value || slots.count);
isHidden() {
const { showZero } = this.$props;
const displayCount = this.getDispayCount();
const isZero = this.isZero();
const isDot = this.isDot();
const isEmpty = displayCount === null || displayCount === undefined || displayCount === '';
return (isEmpty || (isZero && !showZero)) && !isDot;
},
// =============================== Styles ===============================
const mergedStyle = (() => {
if (!offset) {
return { ...style };
}
renderStatusText(prefixCls: string) {
const text = getComponent(this, 'text');
const hidden = this.isHidden();
return hidden || !text ? null : <span class={`${prefixCls}-status-text`}>{text}</span>;
},
const offsetStyle: CSSProperties = {
marginTop: isNumeric(offset[1]) ? `${offset[1]}px` : offset[1],
};
if (direction.value === 'rtl') {
offsetStyle.left = `${parseInt(offset[0] as string, 10)}px`;
} else {
offsetStyle.right = `${-parseInt(offset[0] as string, 10)}px`;
}
renderDispayComponent() {
const count = this.badgeCount;
const customNode = count;
if (!customNode || typeof customNode !== 'object') {
return undefined;
}
return cloneElement(
customNode,
return {
...offsetStyle,
...style,
};
})();
// =============================== Render ===============================
// >>> Title
const titleNode =
title ?? (typeof count === 'string' || typeof count === 'number' ? count : undefined);
// >>> Status Text
const statusTextNode =
visible || !text ? null : <span class={`${pre}-status-text`}>{text}</span>;
// >>> Display Component
const displayNode = cloneElement(
slots.count?.(),
{
style: this.getStyleWithOffset(),
style: mergedStyle,
},
false,
);
},
renderBadgeNumber(prefixCls: string, scrollNumberPrefixCls: string) {
const { status, color } = this.$props;
const count = this.badgeCount;
const displayCount = this.getDispayCount();
const isDot = this.isDot();
const hidden = this.isHidden();
const badgeClassName = classNames(
pre,
{
[`${pre}-status`]: hasStatus.value,
[`${pre}-not-a-wrapper`]: !children,
[`${pre}-rtl`]: direction.value === 'rtl',
},
attrs.class,
);
const scrollNumberCls = {
[`${prefixCls}-dot`]: isDot,
[`${prefixCls}-count`]: !isDot,
[`${prefixCls}-multiple-words`]:
!isDot && count && count.toString && count.toString().length > 1,
[`${prefixCls}-status-${status}`]: !!status,
[`${prefixCls}-status-${color}`]: isPresetColor(color),
};
let statusStyle = this.getStyleWithOffset();
if (color && !isPresetColor(color)) {
statusStyle = statusStyle || {};
statusStyle.background = color;
// <Badge status="success" />
if (!children && hasStatus.value) {
const statusTextColor = mergedStyle.color;
return (
<span {...attrs} class={badgeClassName} style={mergedStyle}>
<span class={statusCls.value} style={statusStyle.value} />
<span style={{ color: statusTextColor }} class={`${pre}-status-text`}>
{text}
</span>
</span>
);
}
return hidden ? null : (
<ScrollNumber
prefixCls={scrollNumberPrefixCls}
data-show={!hidden}
v-show={!hidden}
class={scrollNumberCls}
count={displayCount}
displayComponent={this.renderDispayComponent()}
title={this.getScrollNumberTitle()}
style={statusStyle}
key="scrollNumber"
/>
);
},
},
const transitionProps = getTransitionProps(children ? `${pre}-zoom` : '', {
appear: false,
});
let scrollNumberStyle: CSSProperties = { ...mergedStyle, ...props.numberStyle };
if (color && !isPresetColor(color)) {
scrollNumberStyle = scrollNumberStyle || {};
scrollNumberStyle.background = color;
}
render() {
const {
prefixCls: customizePrefixCls,
scrollNumberPrefixCls: customizeScrollNumberPrefixCls,
status,
color,
} = this;
const text = getComponent(this, 'text');
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('badge', customizePrefixCls);
const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls);
const children = getSlot(this);
let count = getComponent(this, 'count');
if (Array.isArray(count)) {
count = count[0];
}
this.badgeCount = count;
const scrollNumber = this.renderBadgeNumber(prefixCls, scrollNumberPrefixCls);
const statusText = this.renderStatusText(prefixCls);
const statusCls = classNames({
[`${prefixCls}-status-dot`]: this.hasStatus(),
[`${prefixCls}-status-${status}`]: !!status,
[`${prefixCls}-status-${color}`]: isPresetColor(color),
});
const statusStyle: CSSProperties = {};
if (color && !isPresetColor(color)) {
statusStyle.background = color;
}
// <Badge status="success" />
if (!children.length && this.hasStatus()) {
const styleWithOffset = this.getStyleWithOffset();
const statusTextColor = styleWithOffset && styleWithOffset.color;
return (
<span class={this.getBadgeClassName(prefixCls, children)} style={styleWithOffset}>
<span class={statusCls} style={statusStyle} />
<span style={{ color: statusTextColor }} class={`${prefixCls}-status-text`}>
{text}
</span>
<span {...attrs} class={badgeClassName}>
{children}
<Transition {...transitionProps}>
<ScrollNumber
v-show={visible}
prefixCls={props.scrollNumberPrefixCls}
show={visible}
class={scrollNumberCls.value}
count={displayCount.value}
title={titleNode}
style={scrollNumberStyle}
key="scrollNumber"
>
{displayNode}
</ScrollNumber>
</Transition>
{statusTextNode}
</span>
);
}
const transitionProps = getTransitionProps(children.length ? `${prefixCls}-zoom` : '');
return (
<span class={this.getBadgeClassName(prefixCls, children)}>
{children}
<Transition {...transitionProps}>{scrollNumber}</Transition>
{statusText}
</span>
);
};
},
});
Badge.install = function(app: App) {
app.component(Badge.name, Badge);
app.component(Badge.Ribbon.displayName, Badge.Ribbon);
return app;
};
export default Badge as typeof Badge &
Plugin & {
readonly Ribbon: typeof Ribbon;
};

View File

@ -1,60 +1,55 @@
import { LiteralUnion, tuple } from '../_util/type';
import { PresetColorType } from '../_util/colors';
import { isPresetColor } from './utils';
import { defaultConfigProvider } from '../config-provider';
import { HTMLAttributes, FunctionalComponent, VNodeTypes, inject, CSSProperties } from 'vue';
import { CSSProperties, defineComponent, PropType, ExtractPropTypes, computed } from 'vue';
import PropTypes from '../_util/vue-types';
import useConfigInject from '../_util/hooks/useConfigInject';
type RibbonPlacement = 'start' | 'end';
export interface RibbonProps extends HTMLAttributes {
prefixCls?: string;
text?: VNodeTypes;
color?: LiteralUnion<PresetColorType, string>;
placement?: RibbonPlacement;
}
const Ribbon: FunctionalComponent<RibbonProps> = (props, { attrs, slots }) => {
const { prefixCls: customizePrefixCls, color, text = slots.text?.(), placement = 'end' } = props;
const { class: className, style } = attrs;
const children = slots.default?.();
const { getPrefixCls, direction } = inject('configProvider', defaultConfigProvider);
const prefixCls = getPrefixCls('ribbon', customizePrefixCls);
const colorInPreset = isPresetColor(color);
const ribbonCls = [
prefixCls,
`${prefixCls}-placement-${placement}`,
{
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-color-${color}`]: colorInPreset,
},
className,
];
const colorStyle: CSSProperties = {};
const cornerColorStyle: CSSProperties = {};
if (color && !colorInPreset) {
colorStyle.background = color;
cornerColorStyle.color = color;
}
return (
<div class={`${prefixCls}-wrapper`}>
{children}
<div class={ribbonCls} style={{ ...colorStyle, ...(style as CSSProperties) }}>
<span class={`${prefixCls}-text`}>{text}</span>
<div class={`${prefixCls}-corner`} style={cornerColorStyle} />
</div>
</div>
);
};
Ribbon.displayName = 'ABadgeRibbon';
Ribbon.inheritAttrs = false;
Ribbon.props = {
const ribbonProps = {
prefix: PropTypes.string,
color: PropTypes.string,
color: { type: String as PropType<LiteralUnion<PresetColorType, string>> },
text: PropTypes.any,
placement: PropTypes.oneOf(tuple('start', 'end')),
placement: PropTypes.oneOf(tuple('start', 'end')).def('end'),
};
export default Ribbon;
export type RibbonProps = Partial<ExtractPropTypes<typeof ribbonProps>>;
export default defineComponent({
name: 'ABadgeRibbon',
inheritAttrs: false,
props: ribbonProps,
slots: ['text'],
setup(props, { attrs, slots }) {
const { prefixCls, direction } = useConfigInject('ribbon', props);
const colorInPreset = computed(() => isPresetColor(props.color));
const ribbonCls = computed(() => [
prefixCls.value,
`${prefixCls.value}-placement-${props.placement}`,
{
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
[`${prefixCls.value}-color-${props.color}`]: colorInPreset.value,
},
]);
return () => {
const { class: className, style, ...restAttrs } = attrs;
const colorStyle: CSSProperties = {};
const cornerColorStyle: CSSProperties = {};
if (props.color && !colorInPreset.value) {
colorStyle.background = props.color;
cornerColorStyle.color = props.color;
}
return (
<div class={`${prefixCls.value}-wrapper`} {...restAttrs}>
{slots.default?.()}
<div
class={[ribbonCls.value, className]}
style={{ ...colorStyle, ...(style as CSSProperties) }}
>
<span class={`${prefixCls.value}-text`}>{props.text || slots.text?.()}</span>
<div class={`${prefixCls.value}-corner`} style={cornerColorStyle} />
</div>
</div>
);
};
},
});

View File

@ -1,203 +1,90 @@
import classNames from '../_util/classNames';
import PropTypes from '../_util/vue-types';
import BaseMixin from '../_util/BaseMixin';
import omit from 'omit.js';
import { cloneElement } from '../_util/vnode';
import { defaultConfigProvider } from '../config-provider';
import { CSSProperties, defineComponent, inject } from 'vue';
import {
defineComponent,
ExtractPropTypes,
CSSProperties,
DefineComponent,
HTMLAttributes,
} from 'vue';
import useConfigInject from '../_util/hooks/useConfigInject';
import SingleNumber from './SingleNumber';
import { filterEmpty } from '../_util/props-util';
function getNumberArray(num: string | number | undefined | null) {
return num
? num
.toString()
.split('')
.reverse()
.map(i => {
const current = Number(i);
return isNaN(current) ? i : current;
})
: [];
}
const ScrollNumberProps = {
export const scrollNumberProps = {
prefixCls: PropTypes.string,
count: PropTypes.any,
component: PropTypes.string,
title: PropTypes.oneOfType([PropTypes.number, PropTypes.string, null]),
displayComponent: PropTypes.any,
onAnimated: PropTypes.func,
show: Boolean,
};
export type ScrollNumberProps = Partial<ExtractPropTypes<typeof scrollNumberProps>>;
export default defineComponent({
name: 'ScrollNumber',
mixins: [BaseMixin],
inheritAttrs: false,
props: ScrollNumberProps,
emits: ['animated'],
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
lastCount: undefined,
timeout: undefined,
};
},
data() {
return {
animateStarted: true,
sCount: this.count,
};
},
watch: {
count() {
this.lastCount = this.sCount;
this.setState({
animateStarted: true,
});
},
},
updated() {
const { animateStarted, count } = this;
if (animateStarted) {
this.clearTimeout();
// Let browser has time to reset the scroller before actually
// performing the transition.
this.timeout = setTimeout(() => {
this.setState(
{
animateStarted: false,
sCount: count,
},
this.handleAnimated,
);
});
}
},
beforeUnmount() {
this.clearTimeout();
},
methods: {
clearTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = undefined;
}
},
getPositionByNum(num: number, i: number) {
const { sCount } = this;
const currentCount = Math.abs(Number(sCount));
const lastCount = Math.abs(Number(this.lastCount));
const currentDigit = Math.abs(getNumberArray(sCount)[i] as number);
const lastDigit = Math.abs(getNumberArray(this.lastCount)[i] as number);
props: scrollNumberProps,
setup(props, { attrs, slots }) {
const { prefixCls } = useConfigInject('scroll-number', props);
if (this.animateStarted) {
return 10 + num;
}
//
if (currentCount > lastCount) {
if (currentDigit >= lastDigit) {
return 10 + num;
}
return 20 + num;
}
if (currentDigit <= lastDigit) {
return 10 + num;
}
return num;
},
handleAnimated() {
this.$emit('animated');
},
return () => {
const {
prefixCls: customizePrefixCls,
count,
title,
show,
component: Tag = ('sup' as unknown) as DefineComponent,
class: className,
style,
...restProps
} = { ...props, ...attrs } as ScrollNumberProps & HTMLAttributes & { style: CSSProperties };
// ============================ Render ============================
const newProps = {
...restProps,
style,
'data-show': props.show,
class: classNames(prefixCls.value, className),
title: title as string,
};
renderNumberList(position: number, className: string) {
const childrenToReturn = [];
for (let i = 0; i < 30; i++) {
childrenToReturn.push(
<p
key={i.toString()}
class={classNames(className, {
current: position === i,
})}
>
{i % 10}
</p>,
);
// Only integer need motion
let numberNodes: any = count;
if (count && Number(count) % 1 === 0) {
const numberList = String(count).split('');
numberNodes = numberList.map((num, i) => (
<SingleNumber
prefixCls={prefixCls.value}
count={Number(count)}
value={num}
key={numberList.length - i}
/>
));
}
return childrenToReturn;
},
renderCurrentNumber(prefixCls: string, num: number | string, i: number) {
if (typeof num === 'number') {
const position = this.getPositionByNum(num, i);
const removeTransition =
this.animateStarted || getNumberArray(this.lastCount)[i] === undefined;
const style = {
transition: removeTransition ? 'none' : undefined,
msTransform: `translateY(${-position * 100}%)`,
WebkitTransform: `translateY(${-position * 100}%)`,
transform: `translateY(${-position * 100}%)`,
// allow specify the border
// mock border-color by box-shadow for compatible with old usage:
// <Badge count={4} style={{ backgroundColor: '#fff', color: '#999', borderColor: '#d9d9d9' }} />
if (style && style.borderColor) {
newProps.style = {
...(style as CSSProperties),
boxShadow: `0 0 0 1px ${style.borderColor} inset`,
};
return (
<span class={`${prefixCls}-only`} style={style} key={i}>
{this.renderNumberList(position, `${prefixCls}-only-unit`)}
</span>
}
const children = filterEmpty(slots.default?.());
if (children && children.length) {
return cloneElement(
children,
{
class: classNames(`${prefixCls.value}-custom-component`),
},
false,
);
}
return (
<span key="symbol" class={`${prefixCls}-symbol`}>
{num}
</span>
);
},
renderNumberElement(prefixCls: string) {
const { sCount } = this;
if (sCount && Number(sCount) % 1 === 0) {
return getNumberArray(sCount)
.map((num, i) => this.renderCurrentNumber(prefixCls, num, i))
.reverse();
}
return sCount;
},
},
render() {
const { prefixCls: customizePrefixCls, title, component: Tag = 'sup', displayComponent } = this;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('scroll-number', customizePrefixCls);
const { class: className, style = {} } = this.$attrs as {
class?: string;
style?: CSSProperties;
return <Tag {...newProps}>{numberNodes}</Tag>;
};
if (displayComponent) {
return cloneElement(displayComponent, {
class: classNames(
`${prefixCls}-custom-component`,
displayComponent.props && displayComponent.props.class,
),
});
}
// fix https://fb.me/react-unknown-prop
const restProps = omit({ ...this.$props, ...this.$attrs }, [
'count',
'onAnimated',
'component',
'prefixCls',
'displayComponent',
]);
const tempStyle = { ...style };
const newProps = {
...restProps,
title,
style: tempStyle,
class: classNames(prefixCls, className),
};
// allow specify the border
// mock border-color by box-shadow for compatible with old usage:
// <Badge count={4} style={{ backgroundColor: '#fff', color: '#999', borderColor: '#d9d9d9' }} />
if (style && style.borderColor) {
newProps.style.boxShadow = `0 0 0 1px ${style.borderColor} inset`;
}
return <Tag {...newProps}>{this.renderNumberElement(prefixCls)}</Tag>;
},
});

View File

@ -0,0 +1,131 @@
import { computed, CSSProperties, defineComponent, onUnmounted, reactive, ref, watch } from 'vue';
import classNames from '../_util/classNames';
export interface UnitNumberProps {
prefixCls: string;
value: string | number;
offset?: number;
current?: boolean;
}
function UnitNumber({ prefixCls, value, current, offset = 0 }: UnitNumberProps) {
let style: CSSProperties | undefined;
if (offset) {
style = {
position: 'absolute',
top: `${offset}00%`,
left: 0,
};
}
return (
<p
style={style}
class={classNames(`${prefixCls}-only-unit`, {
current,
})}
>
{value}
</p>
);
}
function getOffset(start: number, end: number, unit: -1 | 1) {
let index = start;
let offset = 0;
while ((index + 10) % 10 !== end) {
index += unit;
offset += unit;
}
return offset;
}
export default defineComponent({
name: 'SingleNumber',
props: {
prefixCls: String,
value: String,
count: Number,
},
setup(props) {
const originValue = computed(() => Number(props.value));
const originCount = computed(() => Math.abs(props.count));
const state = reactive({
prevValue: originValue.value,
prevCount: originCount.value,
});
// ============================= Events =============================
const onTransitionEnd = () => {
state.prevValue = originValue.value;
state.prevCount = originCount.value;
};
const timeout = ref();
// Fallback if transition event not support
watch(
originValue,
() => {
clearTimeout(timeout.value);
timeout.value = setTimeout(() => {
onTransitionEnd();
}, 1000);
},
{ flush: 'post' },
);
onUnmounted(() => {
clearTimeout(timeout.value);
});
return () => {
let unitNodes: any[];
let offsetStyle: CSSProperties = {};
const value = originValue.value;
if (state.prevValue === value || Number.isNaN(value) || Number.isNaN(state.prevValue)) {
// Nothing to change
unitNodes = [UnitNumber({ ...props, current: true } as UnitNumberProps)];
offsetStyle = {
transition: 'none',
};
} else {
unitNodes = [];
// Fill basic number units
const end = value + 10;
const unitNumberList: number[] = [];
for (let index = value; index <= end; index += 1) {
unitNumberList.push(index);
}
// Fill with number unit nodes
const prevIndex = unitNumberList.findIndex(n => n % 10 === state.prevValue);
unitNodes = unitNumberList.map((n, index) => {
const singleUnit = n % 10;
return UnitNumber({
...props,
value: singleUnit,
offset: index - prevIndex,
current: index === prevIndex,
} as UnitNumberProps);
});
// Calculate container offset value
const unit = state.prevCount < originCount.value ? 1 : -1;
offsetStyle = {
transform: `translateY(${-getOffset(state.prevValue, value, unit)}00%)`,
};
}
return (
<span
class={`${props.prefixCls}-only`}
style={offsetStyle}
onTransitionend={() => onTransitionEnd()}
>
{unitNodes}
</span>
);
};
},
});

View File

@ -1,3 +1,14 @@
import { App, Plugin } from 'vue';
import Badge from './Badge';
import Ribbon from './Ribbon';
export default Badge;
Badge.install = function(app: App) {
app.component(Badge.name, Badge);
app.component(Ribbon.name, Ribbon);
return app;
};
export default Badge as typeof Badge &
Plugin & {
readonly Ribbon: typeof Ribbon;
};

View File

@ -9,10 +9,10 @@
position: relative;
display: inline-block;
color: unset;
line-height: 1;
&-count {
z-index: @zindex-badge;
min-width: @badge-height;
height: @badge-height;
padding: 0 6px;
@ -22,7 +22,7 @@
line-height: @badge-height;
white-space: nowrap;
text-align: center;
background: @highlight-color;
background: @badge-color;
border-radius: (@badge-height / 2);
box-shadow: 0 0 0 1px @shadow-color-inverse;
a,
@ -31,12 +31,23 @@
}
}
&-count-sm {
min-width: @badge-height-sm;
height: @badge-height-sm;
padding: 0;
font-size: @badge-font-size-sm;
line-height: @badge-height-sm;
border-radius: (@badge-height-sm / 2);
}
&-multiple-words {
padding: 0 8px;
}
&-dot {
z-index: @zindex-badge;
width: @badge-dot-size;
min-width: @badge-dot-size;
height: @badge-dot-size;
background: @highlight-color;
border-radius: 100%;
@ -49,9 +60,12 @@
position: absolute;
top: 0;
right: 0;
z-index: @zindex-badge;
transform: translate(50%, -50%);
transform-origin: 100% 0%;
&.@{iconfont-css-prefix}-spin {
animation: antBadgeLoadingCircle 1s infinite linear;
}
}
&-status {
@ -115,24 +129,39 @@
&-zoom-appear,
&-zoom-enter {
animation: antZoomBadgeIn 0.3s @ease-out-back;
animation: antZoomBadgeIn @animation-duration-slow @ease-out-back;
animation-fill-mode: both;
}
&-zoom-leave {
animation: antZoomBadgeOut 0.3s @ease-in-back;
animation: antZoomBadgeOut @animation-duration-slow @ease-in-back;
animation-fill-mode: both;
}
&-not-a-wrapper {
.@{badge-prefix-cls}-zoom-appear,
.@{badge-prefix-cls}-zoom-enter {
animation: antNoWrapperZoomBadgeIn @animation-duration-slow @ease-out-back;
}
.@{badge-prefix-cls}-zoom-leave {
animation: antNoWrapperZoomBadgeOut @animation-duration-slow @ease-in-back;
}
&:not(.@{badge-prefix-cls}-status) {
vertical-align: middle;
}
.@{number-prefix-cls}-custom-component {
transform: none;
}
.@{number-prefix-cls}-custom-component,
.@{ant-prefix}-scroll-number {
position: relative;
top: auto;
display: block;
transform-origin: 50% 50%;
}
.@{badge-prefix-cls}-count {
@ -152,15 +181,25 @@
}
}
// Safari will blink with transform when inner element has absolute style.
.safari-fix-motion() {
-webkit-transform-style: preserve-3d;
-webkit-backface-visibility: hidden;
}
.@{number-prefix-cls} {
overflow: hidden;
&-only {
position: relative;
display: inline-block;
height: @badge-height;
transition: all 0.3s @ease-in-out;
transition: all @animation-duration-slow @ease-in-out;
.safari-fix-motion;
> p.@{number-prefix-cls}-only-unit {
height: @badge-height;
margin: 0;
.safari-fix-motion;
}
}
@ -189,4 +228,36 @@
}
}
@keyframes antNoWrapperZoomBadgeIn {
0% {
transform: scale(0);
opacity: 0;
}
100% {
transform: scale(1);
}
}
@keyframes antNoWrapperZoomBadgeOut {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
opacity: 0;
}
}
@keyframes antBadgeLoadingCircle {
0% {
transform-origin: 50%;
}
100% {
transform: translate(50%, -50%) rotate(360deg);
transform-origin: 50%;
}
}
@import './ribbon';
@import './rtl';

View File

@ -0,0 +1,104 @@
.@{badge-prefix-cls} {
&-rtl {
direction: rtl;
}
&-count,
&-dot,
.@{number-prefix-cls}-custom-component {
.@{badge-prefix-cls}-rtl & {
right: auto;
left: 0;
direction: ltr;
transform: translate(-50%, -50%);
transform-origin: 0% 0%;
}
}
.@{badge-prefix-cls}-rtl& .@{number-prefix-cls}-custom-component {
right: auto;
left: 0;
transform: translate(-50%, -50%);
transform-origin: 0% 0%;
}
&-status {
&-text {
.@{badge-prefix-cls}-rtl & {
margin-right: 8px;
margin-left: 0;
}
}
}
&-zoom-appear,
&-zoom-enter {
.@{badge-prefix-cls}-rtl & {
animation-name: antZoomBadgeInRtl;
}
}
&-zoom-leave {
.@{badge-prefix-cls}-rtl & {
animation-name: antZoomBadgeOutRtl;
}
}
&-not-a-wrapper {
.@{badge-prefix-cls}-count {
transform: none;
}
}
}
.@{ribbon-prefix-cls}-rtl {
direction: rtl;
&.@{ribbon-prefix-cls}-placement-end {
right: unset;
left: -8px;
border-bottom-right-radius: @border-radius-sm;
border-bottom-left-radius: 0;
.@{ribbon-prefix-cls}-corner {
right: unset;
left: 0;
border-color: currentColor currentColor transparent transparent;
&::after {
border-color: currentColor currentColor transparent transparent;
}
}
}
&.@{ribbon-prefix-cls}-placement-start {
right: -8px;
left: unset;
border-bottom-right-radius: 0;
border-bottom-left-radius: @border-radius-sm;
.@{ribbon-prefix-cls}-corner {
right: 0;
left: unset;
border-color: currentColor transparent transparent currentColor;
&::after {
border-color: currentColor transparent transparent currentColor;
}
}
}
}
@keyframes antZoomBadgeInRtl {
0% {
transform: scale(0) translate(-50%, -50%);
opacity: 0;
}
100% {
transform: scale(1) translate(-50%, -50%);
}
}
@keyframes antZoomBadgeOutRtl {
0% {
transform: scale(1) translate(-50%, -50%);
}
100% {
transform: scale(0) translate(-50%, -50%);
opacity: 0;
}
}

View File

@ -1,5 +1,5 @@
import { PresetColorTypes } from '../_util/colors';
export function isPresetColor(color?: string): boolean {
return (PresetColorTypes as string[]).indexOf(color) !== -1;
return (PresetColorTypes as any[]).indexOf(color) !== -1;
}

View File

@ -1,9 +1,9 @@
import { defineComponent, inject } from 'vue';
import { defineComponent, ExtractPropTypes } from 'vue';
import PropsTypes from '../_util/vue-types';
import { getComponent, getSlot } from '../_util/props-util';
import { defaultConfigProvider } from '../config-provider';
import { flattenChildren } from '../_util/props-util';
import { VueNode, withInstall } from '../_util/type';
export const CommentProps = {
import useConfigInject from '../_util/hooks/useConfigInject';
export const commentProps = {
actions: PropsTypes.array,
/** The element to display as the comment author. */
author: PropsTypes.VNodeChild,
@ -17,79 +17,79 @@ export const CommentProps = {
datetime: PropsTypes.VNodeChild,
};
export type CommentProps = Partial<ExtractPropTypes<typeof commentProps>>;
const Comment = defineComponent({
name: 'AComment',
props: CommentProps,
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
props: commentProps,
slots: ['actions', 'author', 'avatar', 'content', 'datetime'],
setup(props, { slots }) {
const { prefixCls, direction } = useConfigInject('comment', props);
const renderNested = (prefixCls: string, children: VueNode) => {
return <div class={`${prefixCls}-nested`}>{children}</div>;
};
},
methods: {
getAction(actions: VueNode[]) {
const getAction = (actions: VueNode[]) => {
if (!actions || !actions.length) {
return null;
}
const actionList = actions.map((action, index) => <li key={`action-${index}`}>{action}</li>);
return actionList;
},
renderNested(prefixCls: string, children: VueNode) {
return <div class={`${prefixCls}-nested`}>{children}</div>;
},
},
};
return () => {
const pre = prefixCls.value;
render() {
const { prefixCls: customizePrefixCls } = this.$props;
const actions = props.actions ?? slots.actions?.();
const author = props.author ?? slots.author?.();
const avatar = props.avatar ?? slots.avatar?.();
const content = props.content ?? slots.content?.();
const datetime = props.datetime ?? slots.datetime?.();
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('comment', customizePrefixCls);
const avatarDom = (
<div class={`${pre}-avatar`}>
{typeof avatar === 'string' ? <img src={avatar} alt="comment-avatar" /> : avatar}
</div>
);
const actions = getComponent(this, 'actions');
const author = getComponent(this, 'author');
const avatar = getComponent(this, 'avatar');
const content = getComponent(this, 'content');
const datetime = getComponent(this, 'datetime');
const actionDom = actions ? (
<ul class={`${pre}-actions`}>{getAction(Array.isArray(actions) ? actions : [actions])}</ul>
) : null;
const avatarDom = (
<div class={`${prefixCls}-avatar`}>
{typeof avatar === 'string' ? <img src={avatar} alt="comment-avatar" /> : avatar}
</div>
);
const authorContent = (
<div class={`${pre}-content-author`}>
{author && <span class={`${pre}-content-author-name`}>{author}</span>}
{datetime && <span class={`${pre}-content-author-time`}>{datetime}</span>}
</div>
);
const actionDom = actions ? (
<ul class={`${prefixCls}-actions`}>
{this.getAction(Array.isArray(actions) ? actions : [actions])}
</ul>
) : null;
const contentDom = (
<div class={`${pre}-content`}>
{authorContent}
<div class={`${pre}-content-detail`}>{content}</div>
{actionDom}
</div>
);
const authorContent = (
<div class={`${prefixCls}-content-author`}>
{author && <span class={`${prefixCls}-content-author-name`}>{author}</span>}
{datetime && <span class={`${prefixCls}-content-author-time`}>{datetime}</span>}
</div>
);
const contentDom = (
<div class={`${prefixCls}-content`}>
{authorContent}
<div class={`${prefixCls}-content-detail`}>{content}</div>
{actionDom}
</div>
);
const comment = (
<div class={`${prefixCls}-inner`}>
{avatarDom}
{contentDom}
</div>
);
const children = getSlot(this);
return (
<div class={prefixCls}>
{comment}
{children && children.length ? this.renderNested(prefixCls, children) : null}
</div>
);
const comment = (
<div class={`${pre}-inner`}>
{avatarDom}
{contentDom}
</div>
);
const children = flattenChildren(slots.default?.());
return (
<div
class={[
pre,
{
[`${pre}-rtl`]: direction.value === 'rtl',
},
]}
>
{comment}
{children && children.length ? renderNested(pre, children) : null}
</div>
);
};
},
});

View File

@ -15,8 +15,9 @@
&-avatar {
position: relative;
flex-shrink: 0;
margin-right: 12px;
margin-right: @margin-sm;
cursor: pointer;
img {
width: 32px;
height: 32px;
@ -35,11 +36,11 @@
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
margin-bottom: 4px;
margin-bottom: @margin-xss;
font-size: @comment-font-size-base;
& > a,
& > span {
padding-right: 8px;
padding-right: @padding-xs;
font-size: @comment-font-size-sm;
line-height: 18px;
}
@ -64,23 +65,27 @@
}
&-detail p {
margin-bottom: @comment-content-detail-p-margin-bottom;
white-space: pre-wrap;
}
}
&-actions {
margin-top: 12px;
margin-top: @comment-actions-margin-top;
margin-bottom: @comment-actions-margin-bottom;
padding-left: 0;
> li {
display: inline-block;
color: @comment-action-color;
> span {
padding-right: 10px;
margin-right: 10px;
color: @comment-action-color;
font-size: @comment-font-size-sm;
cursor: pointer;
transition: color 0.3s;
user-select: none;
&:hover {
color: @comment-action-hover-color;
}
@ -92,3 +97,5 @@
margin-left: @comment-nest-indent;
}
}
@import './rtl';

View File

@ -0,0 +1,50 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@comment-prefix-cls: ~'@{ant-prefix}-comment';
.@{comment-prefix-cls} {
&-rtl {
direction: rtl;
}
&-avatar {
.@{comment-prefix-cls}-rtl & {
margin-right: 0;
margin-left: 12px;
}
}
&-content {
&-author {
& > a,
& > span {
.@{comment-prefix-cls}-rtl & {
padding-right: 0;
padding-left: 8px;
}
}
}
}
&-actions {
.@{comment-prefix-cls}-rtl & {
padding-right: 0;
}
> li {
> span {
.@{comment-prefix-cls}-rtl & {
margin-right: 0;
margin-left: 10px;
}
}
}
}
&-nested {
.@{comment-prefix-cls}-rtl & {
margin-right: @comment-nest-indent;
margin-left: 0;
}
}
}

View File

@ -1,14 +0,0 @@
import { defineComponent, PropType, provide } from 'vue';
export type SizeType = 'small' | 'middle' | 'large' | undefined;
export const SizeContextProvider = defineComponent({
props: {
size: String as PropType<SizeType>,
},
setup(props, { slots }) {
provide('sizeProvider', props.size);
return () => slots.default?.();
},
});

View File

@ -1,10 +1,19 @@
import { reactive, provide, VNodeTypes, PropType, defineComponent, watch } from 'vue';
import {
reactive,
provide,
PropType,
defineComponent,
watch,
ExtractPropTypes,
UnwrapRef,
} from 'vue';
import PropTypes from '../_util/vue-types';
import defaultRenderEmpty, { RenderEmptyHandler } from './renderEmpty';
import LocaleProvider, { Locale, ANT_MARK } from '../locale-provider';
import { TransformCellTextProps } from '../table/interface';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import { withInstall } from '../_util/type';
import { RequiredMark } from '../form/Form';
export type SizeType = 'small' | 'middle' | 'large' | undefined;
@ -14,6 +23,8 @@ export interface CSPConfig {
export { RenderEmptyHandler };
export type Direction = 'ltr' | 'rtl';
export interface ConfigConsumerProps {
getTargetContainer?: () => HTMLElement;
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
@ -30,6 +41,7 @@ export interface ConfigConsumerProps {
pageHeader?: {
ghost: boolean;
};
componentSize?: SizeType;
direction?: 'ltr' | 'rtl';
space?: {
size?: SizeType | number;
@ -50,72 +62,54 @@ export const configConsumerProps = [
'pageHeader',
];
export interface ConfigProviderProps {
getTargetContainer?: () => HTMLElement;
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
prefixCls?: string;
children?: VNodeTypes;
renderEmpty?: RenderEmptyHandler;
transformCellText?: (tableProps: TransformCellTextProps) => any;
csp?: CSPConfig;
autoInsertSpaceInButton?: boolean;
input?: {
autoComplete?: string;
};
locale?: Locale;
pageHeader?: {
ghost: boolean;
};
componentSize?: SizeType;
direction?: 'ltr' | 'rtl';
space?: {
size?: SizeType | number;
};
virtual?: boolean;
dropdownMatchSelectWidth?: boolean;
}
export const configProviderProps = {
getTargetContainer: {
type: Function as PropType<() => HTMLElement>,
},
getPopupContainer: {
type: Function as PropType<(triggerNode: HTMLElement) => HTMLElement>,
},
prefixCls: String,
getPrefixCls: {
type: Function as PropType<(suffixCls?: string, customizePrefixCls?: string) => string>,
},
renderEmpty: {
type: Function as PropType<RenderEmptyHandler>,
},
transformCellText: {
type: Function as PropType<(tableProps: TransformCellTextProps) => any>,
},
csp: {
type: Object as PropType<CSPConfig>,
},
autoInsertSpaceInButton: PropTypes.looseBool,
locale: {
type: Object as PropType<Locale>,
},
pageHeader: {
type: Object as PropType<{ ghost: boolean }>,
},
componentSize: {
type: String as PropType<SizeType>,
},
direction: {
type: String as PropType<'ltr' | 'rtl'>,
},
space: {
type: Object as PropType<{ size: SizeType | number }>,
},
virtual: PropTypes.looseBool,
dropdownMatchSelectWidth: PropTypes.looseBool,
form: {
type: Object as PropType<{ requiredMark?: RequiredMark }>,
},
};
export type ConfigProviderProps = Partial<ExtractPropTypes<typeof configProviderProps>>;
const ConfigProvider = defineComponent({
name: 'AConfigProvider',
props: {
getTargetContainer: {
type: Function as PropType<() => HTMLElement>,
},
getPopupContainer: {
type: Function as PropType<(triggerNode: HTMLElement) => HTMLElement>,
},
prefixCls: String,
getPrefixCls: {
type: Function as PropType<(suffixCls?: string, customizePrefixCls?: string) => string>,
},
renderEmpty: {
type: Function as PropType<RenderEmptyHandler>,
},
transformCellText: {
type: Function as PropType<(tableProps: TransformCellTextProps) => any>,
},
csp: {
type: Object as PropType<CSPConfig>,
},
autoInsertSpaceInButton: PropTypes.looseBool,
locale: {
type: Object as PropType<Locale>,
},
pageHeader: {
type: Object as PropType<{ ghost: boolean }>,
},
componentSize: {
type: Object as PropType<SizeType>,
},
direction: {
type: String as PropType<'ltr' | 'rtl'>,
},
space: {
type: [String, Number] as PropType<SizeType | number>,
},
virtual: PropTypes.looseBool,
dropdownMatchSelectWidth: PropTypes.looseBool,
},
props: configProviderProps,
setup(props, { slots }) {
const getPrefixCls = (suffixCls?: string, customizePrefixCls?: string) => {
const { prefixCls = 'ant' } = props;
@ -166,12 +160,13 @@ const ConfigProvider = defineComponent({
},
});
export const defaultConfigProvider: ConfigConsumerProps = {
export const defaultConfigProvider: UnwrapRef<ConfigProviderProps> = reactive({
getPrefixCls: (suffixCls: string, customizePrefixCls?: string) => {
if (customizePrefixCls) return customizePrefixCls;
return `ant-${suffixCls}`;
return suffixCls ? `ant-${suffixCls}` : 'ant';
},
renderEmpty: defaultRenderEmpty,
};
direction: 'ltr',
});
export default withInstall(ConfigProvider);

View File

@ -1,46 +1,67 @@
import { flattenChildren } from '../_util/props-util';
import { computed, defineComponent, inject, PropType } from 'vue';
import { computed, defineComponent, ExtractPropTypes, inject, PropType } from 'vue';
import { defaultConfigProvider } from '../config-provider';
import { withInstall } from '../_util/type';
export const dividerProps = {
prefixCls: String,
type: {
type: String as PropType<'horizontal' | 'vertical' | ''>,
default: 'horizontal',
},
dashed: {
type: Boolean,
default: false,
},
orientation: {
type: String as PropType<'left' | 'right' | 'center'>,
default: 'center',
},
plain: {
type: Boolean,
default: false,
},
};
export type DividerProps = Partial<ExtractPropTypes<typeof dividerProps>>;
const Divider = defineComponent({
name: 'ADivider',
props: {
prefixCls: String,
type: {
type: String as PropType<'horizontal' | 'vertical' | ''>,
default: 'horizontal',
},
dashed: {
type: Boolean,
default: false,
},
orientation: {
type: String as PropType<'left' | 'right' | 'center'>,
default: 'center',
},
},
props: dividerProps,
setup(props, { slots }) {
const { getPrefixCls } = inject('configProvider', defaultConfigProvider);
const prefixCls = computed(() => getPrefixCls('divider', props.prefixCls));
const configProvider = inject('configProvider', defaultConfigProvider);
const prefixClsRef = computed(() => configProvider.getPrefixCls('divider', props.prefixCls));
const classString = computed(() => {
const { type, dashed, orientation } = props;
const orientationPrefix = orientation.length > 0 ? '-' + orientation : orientation;
const prefixClsRef = prefixCls.value;
const { type, dashed, plain } = props;
const prefixCls = prefixClsRef.value;
return {
[prefixClsRef]: true,
[`${prefixClsRef}-${type}`]: true,
[`${prefixClsRef}-with-text${orientationPrefix}`]: slots.default,
[`${prefixClsRef}-dashed`]: !!dashed,
[prefixCls]: true,
[`${prefixCls}-${type}`]: true,
[`${prefixCls}-dashed`]: !!dashed,
[`${prefixCls}-plain`]: !!plain,
[`${prefixCls}-rtl`]: configProvider.direction === 'rtl',
};
});
const orientationPrefix = computed(() =>
props.orientation.length > 0 ? '-' + props.orientation : props.orientation,
);
return () => {
const children = flattenChildren(slots.default?.());
return (
<div class={classString.value} role="separator">
{children.length ? <span class={`${prefixCls.value}-inner-text`}>{children}</span> : null}
<div
class={[
classString.value,
children.length
? `${prefixClsRef.value}-with-text ${prefixClsRef.value}-with-text${orientationPrefix.value}`
: '',
]}
role="separator"
>
{children.length ? (
<span class={`${prefixClsRef.value}-inner-text`}>{children}</span>
) : null}
</div>
);
};

View File

@ -6,59 +6,52 @@
.@{divider-prefix-cls} {
.reset-component();
background: @border-color-split;
border-top: @border-width-base solid @divider-color;
&, /* for compatiable */
&-vertical {
position: relative;
top: -0.06em;
display: inline-block;
width: 1px;
height: 0.9em;
margin: 0 8px;
vertical-align: middle;
border-top: 0;
border-left: @border-width-base solid @divider-color;
}
&-horizontal {
display: block;
display: flex;
clear: both;
width: 100%;
min-width: 100%; // Fix https://github.com/ant-design/ant-design/issues/10914
height: 1px;
margin: 24px 0;
}
&-horizontal&-with-text-center,
&-horizontal&-with-text-left,
&-horizontal&-with-text-right {
display: table;
&-horizontal&-with-text {
display: flex;
margin: 16px 0;
color: @heading-color;
font-weight: 500;
font-size: @font-size-lg;
white-space: nowrap;
text-align: center;
background: transparent;
border-top: 0;
border-top-color: @divider-color;
&::before,
&::after {
position: relative;
top: 50%;
display: table-cell;
width: 50%;
border-top: 1px solid @border-color-split;
border-top: @border-width-base solid transparent;
// Chrome not accept `inherit` in `border-top`
border-top-color: inherit;
border-bottom: 0;
transform: translateY(50%);
content: '';
}
}
&-horizontal&-with-text-left,
&-horizontal&-with-text-right {
.@{divider-prefix-cls}-inner-text {
display: inline-block;
padding: 0 10px;
}
}
&-horizontal&-with-text-left {
&::before {
top: 50%;
@ -90,12 +83,10 @@
background: none;
border-color: @divider-color;
border-style: dashed;
border-width: 1px 0 0;
border-width: @border-width-base 0 0;
}
&-horizontal&-with-text-center&-dashed,
&-horizontal&-with-text-left&-dashed,
&-horizontal&-with-text-right&-dashed {
&-horizontal&-with-text&-dashed {
border-top: 0;
&::before,
&::after {
@ -104,6 +95,14 @@
}
&-vertical&-dashed {
border-width: 0 0 0 1px;
border-width: 0 0 0 @border-width-base;
}
&-plain&-with-text {
color: @text-color;
font-weight: normal;
font-size: @font-size-base;
}
}
@import './rtl';

View File

@ -0,0 +1,36 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@divider-prefix-cls: ~'@{ant-prefix}-divider';
.@{divider-prefix-cls} {
&-rtl {
direction: rtl;
}
&-horizontal&-with-text-left {
&::before {
.@{divider-prefix-cls}-rtl& {
width: 100% - @divider-orientation-margin;
}
}
&::after {
.@{divider-prefix-cls}-rtl& {
width: @divider-orientation-margin;
}
}
}
&-horizontal&-with-text-right {
&::before {
.@{divider-prefix-cls}-rtl& {
width: @divider-orientation-margin;
}
}
&::after {
.@{divider-prefix-cls}-rtl& {
width: 100% - @divider-orientation-margin;
}
}
}
}

View File

@ -7,7 +7,7 @@
.@{empty-prefix-cls} {
margin: 0 8px;
font-size: @empty-font-size;
line-height: 22px;
line-height: @line-height-base;
text-align: center;
&-image {
@ -24,10 +24,6 @@
}
}
&-description {
margin: 0;
}
&-footer {
margin-top: 16px;
}
@ -56,8 +52,8 @@
// not support the definition because the less variables have no meaning
& when (@theme = dark) {
&-ellipse {
fill-opacity: 0.08;
fill: @white;
fill-opacity: 0.08;
}
&-path {
&-1 {
@ -82,8 +78,8 @@
}
& when not (@theme = dark) {
&-ellipse {
fill-opacity: 0.8;
fill: #f5f5f5;
fill-opacity: 0.8;
}
&-path {
&-1 {
@ -135,3 +131,5 @@
}
}
}
@import './rtl';

View File

@ -0,0 +1,10 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@empty-prefix-cls: ~'@{ant-prefix}-empty';
.@{empty-prefix-cls} {
&-rtl {
direction: rtl;
}
}

View File

@ -0,0 +1,87 @@
import { useInjectFormItemPrefix } from './context';
import { VueNode } from '../_util/type';
import { defineComponent, onBeforeUnmount, ref, watch } from '@vue/runtime-core';
import classNames from '../_util/classNames';
import Transition, { getTransitionProps } from '../_util/transition';
import useConfigInject from '../_util/hooks/useConfigInject';
export interface ErrorListProps {
errors?: VueNode[];
/** @private Internal Usage. Do not use in your production */
help?: VueNode;
/** @private Internal Usage. Do not use in your production */
onDomErrorVisibleChange?: (visible: boolean) => void;
}
export default defineComponent({
name: 'ErrorList',
props: ['errors', 'help', 'onDomErrorVisibleChange'],
setup(props) {
const { prefixCls: rootPrefixCls } = useConfigInject('', props);
const { prefixCls, status } = useInjectFormItemPrefix();
const visible = ref(!!(props.errors && props.errors.length));
const innerStatus = ref(status.value);
let timeout = ref();
const cacheErrors = ref([...props.errors]);
watch([() => [...props.errors], () => props.help], newValues => {
window.clearTimeout(timeout.value);
if (props.help) {
visible.value = !!(props.errors && props.errors.length);
if (visible.value) {
cacheErrors.value = newValues[0];
}
} else {
timeout.value = window.setTimeout(() => {
visible.value = !!(props.errors && props.errors.length);
if (visible.value) {
cacheErrors.value = newValues[0];
}
});
}
});
onBeforeUnmount(() => {
window.clearTimeout(timeout.value);
});
// Memo status in same visible
watch([visible, status], () => {
if (visible.value && status.value) {
innerStatus.value = status.value;
}
});
watch(
visible,
() => {
if (visible.value) {
props.onDomErrorVisibleChange?.(true);
}
},
{ immediate: true, flush: 'post' },
);
return () => {
const baseClassName = `${prefixCls.value}-item-explain`;
const transitionProps = getTransitionProps(`${rootPrefixCls.value}-show-help`, {
onAfterLeave: () => {
props.onDomErrorVisibleChange?.(false);
},
});
return (
<Transition {...transitionProps}>
{visible.value ? (
<div
class={classNames(baseClassName, {
[`${baseClassName}-${innerStatus.value}`]: innerStatus.value,
})}
key="help"
>
{cacheErrors.value?.map((error: any, index: number) => (
<div key={index} role="alert">
{error}
</div>
))}
</div>
) : null}
</Transition>
);
};
},
});

View File

@ -1,10 +1,16 @@
import { defineComponent, inject, provide, PropType, computed, ExtractPropTypes } from 'vue';
import {
defineComponent,
PropType,
computed,
ExtractPropTypes,
HTMLAttributes,
watch,
ref,
} from 'vue';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
import warning from '../_util/warning';
import FormItem from './FormItem';
import { getSlot } from '../_util/props-util';
import { defaultConfigProvider } from '../config-provider';
import FormItem, { FieldExpose } from './FormItem';
import { getNamePath, containsNamePath } from './utils/valueUtil';
import { defaultValidateMessages } from './utils/messages';
import { allPromiseFinish } from './utils/asyncUtil';
@ -15,6 +21,13 @@ import initDefaultProps from '../_util/props-util/initDefaultProps';
import { tuple, VueNode } from '../_util/type';
import { ColProps } from '../grid/Col';
import { InternalNamePath, NamePath, ValidateErrorEntity, ValidateOptions } from './interface';
import { useInjectSize } from '../_util/hooks/useSize';
import useConfigInject from '../_util/hooks/useConfigInject';
import { useProvideForm } from './context';
import { SizeType } from '../config-provider';
export type RequiredMark = boolean | 'optional';
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
export type ValidationRule = {
/** validation error message */
@ -45,11 +58,13 @@ export type ValidationRule = {
export const formProps = {
layout: PropTypes.oneOf(tuple('horizontal', 'inline', 'vertical')),
labelCol: { type: Object as PropType<ColProps> },
wrapperCol: { type: Object as PropType<ColProps> },
labelCol: { type: Object as PropType<ColProps & HTMLAttributes> },
wrapperCol: { type: Object as PropType<ColProps & HTMLAttributes> },
colon: PropTypes.looseBool,
labelAlign: PropTypes.oneOf(tuple('left', 'right')),
prefixCls: PropTypes.string,
requiredMark: { type: [String, Boolean] as PropType<RequiredMark | ''>, default: undefined },
/** @deprecated Will warning in future branch. Pls use `requiredMark` instead. */
hideRequiredMark: PropTypes.looseBool,
model: PropTypes.object,
rules: { type: Object as PropType<{ [k: string]: ValidationRule[] | ValidationRule }> },
@ -62,6 +77,7 @@ export const formProps = {
onFinishFailed: PropTypes.func,
name: PropTypes.string,
validateTrigger: { type: [String, Array] as PropType<string | string[]> },
size: { type: String as PropType<SizeType> },
};
export type FormProps = Partial<ExtractPropTypes<typeof formProps>>;
@ -79,92 +95,88 @@ const Form = defineComponent({
colon: true,
}),
Item: FormItem,
setup(props) {
return {
configProvider: inject('configProvider', defaultConfigProvider),
fields: [],
form: undefined,
lastValidatePromise: null,
vertical: computed(() => props.layout === 'vertical'),
emits: ['finishFailed', 'submit', 'finish'],
setup(props, { emit, slots, expose }) {
const size = useInjectSize(props);
const { prefixCls, direction, form: contextForm } = useConfigInject('form', props);
const requiredMark = computed(() => props.requiredMark === '' || props.requiredMark);
const mergedRequiredMark = computed(() => {
if (requiredMark.value !== undefined) {
return requiredMark.value;
}
if (contextForm && contextForm.value?.requiredMark !== undefined) {
return contextForm.value.requiredMark;
}
if (props.hideRequiredMark) {
return false;
}
return true;
});
const formClassName = computed(() =>
classNames(prefixCls.value, {
[`${prefixCls.value}-${props.layout}`]: true,
[`${prefixCls.value}-hide-required-mark`]: mergedRequiredMark.value === false,
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
[`${prefixCls.value}-${size.value}`]: size.value,
}),
);
const lastValidatePromise = ref();
const fields: Record<string, FieldExpose> = {};
const addField = (eventKey: string, field: FieldExpose) => {
fields[eventKey] = field;
};
},
watch: {
rules() {
if (this.validateOnRuleChange) {
this.validateFields();
}
},
},
created() {
provide('FormContext', this);
},
methods: {
addField(field: any) {
if (field) {
this.fields.push(field);
}
},
removeField(field: any) {
if (field.fieldName) {
this.fields.splice(this.fields.indexOf(field), 1);
}
},
handleSubmit(e: Event) {
e.preventDefault();
e.stopPropagation();
this.$emit('submit', e);
const res = this.validateFields();
res
.then(values => {
this.$emit('finish', values);
})
.catch(errors => {
this.handleFinishFailed(errors);
});
},
getFieldsByNameList(nameList: NamePath) {
const removeField = (eventKey: string) => {
delete fields[eventKey];
};
const getFieldsByNameList = (nameList: NamePath) => {
const provideNameList = !!nameList;
const namePathList = provideNameList ? toArray(nameList).map(getNamePath) : [];
if (!provideNameList) {
return this.fields;
return Object.values(fields);
} else {
return this.fields.filter(
field => namePathList.findIndex(namePath => isEqualName(namePath, field.fieldName)) > -1,
return Object.values(fields).filter(
field =>
namePathList.findIndex(namePath => isEqualName(namePath, field.fieldName.value)) > -1,
);
}
},
resetFields(name: NamePath) {
if (!this.model) {
};
const resetFields = (name: NamePath) => {
if (!props.model) {
warning(false, 'Form', 'model is required for resetFields to work.');
return;
}
this.getFieldsByNameList(name).forEach(field => {
getFieldsByNameList(name).forEach(field => {
field.resetField();
});
},
clearValidate(name: NamePath) {
this.getFieldsByNameList(name).forEach(field => {
};
const clearValidate = (name: NamePath) => {
getFieldsByNameList(name).forEach(field => {
field.clearValidate();
});
},
handleFinishFailed(errorInfo: ValidateErrorEntity) {
const { scrollToFirstError } = this;
this.$emit('finishFailed', errorInfo);
};
const handleFinishFailed = (errorInfo: ValidateErrorEntity) => {
const { scrollToFirstError } = props;
emit('finishFailed', errorInfo);
if (scrollToFirstError && errorInfo.errorFields.length) {
let scrollToFieldOptions: Options = {};
if (typeof scrollToFirstError === 'object') {
scrollToFieldOptions = scrollToFirstError;
}
this.scrollToField(errorInfo.errorFields[0].name, scrollToFieldOptions);
scrollToField(errorInfo.errorFields[0].name, scrollToFieldOptions);
}
},
validate(...args: any[]) {
return this.validateField(...args);
},
scrollToField(name: NamePath, options = {}) {
const fields = this.getFieldsByNameList(name);
};
const validate = (...args: any[]) => {
return validateField(...args);
};
const scrollToField = (name: NamePath, options = {}) => {
const fields = getFieldsByNameList(name);
if (fields.length) {
const fieldId = fields[0].fieldId;
const fieldId = fields[0].fieldId.value;
const node = fieldId ? document.getElementById(fieldId) : null;
if (node) {
@ -175,12 +187,12 @@ const Form = defineComponent({
});
}
}
},
};
// eslint-disable-next-line no-unused-vars
getFieldsValue(nameList: NamePath[] | true = true) {
const getFieldsValue = (nameList: NamePath[] | true = true) => {
const values: any = {};
this.fields.forEach(({ fieldName, fieldValue }) => {
values[fieldName] = fieldValue;
Object.values(fields).forEach(({ fieldName, fieldValue }) => {
values[fieldName.value] = fieldValue.value;
});
if (nameList === true) {
return values;
@ -191,14 +203,14 @@ const Form = defineComponent({
);
return res;
}
},
validateFields(nameList?: NamePath[], options?: ValidateOptions) {
};
const validateFields = (nameList?: NamePath[], options?: ValidateOptions) => {
warning(
!(nameList instanceof Function),
'Form',
'validateFields/validateField/validate not support callback, please use promise instead',
);
if (!this.model) {
if (!props.model) {
warning(false, 'Form', 'model is required for validateFields to work.');
return Promise.reject('Form `model` is required for validateFields to work.');
}
@ -213,25 +225,25 @@ const Form = defineComponent({
errors: string[];
}>[] = [];
this.fields.forEach(field => {
Object.values(fields).forEach(field => {
// Add field if not provide `nameList`
if (!provideNameList) {
namePathList.push(field.getNamePath());
namePathList.push(field.namePath.value);
}
// Skip if without rule
if (!field.getRules().length) {
if (!field.rules?.value.length) {
return;
}
const fieldNamePath = field.getNamePath();
const fieldNamePath = field.namePath.value;
// Add field validate rule in to promise list
if (!provideNameList || containsNamePath(namePathList, fieldNamePath)) {
const promise = field.validateRules({
validateMessages: {
...defaultValidateMessages,
...this.validateMessages,
...props.validateMessages,
},
...options,
});
@ -251,21 +263,21 @@ const Form = defineComponent({
});
const summaryPromise = allPromiseFinish(promiseList);
this.lastValidatePromise = summaryPromise;
lastValidatePromise.value = summaryPromise;
const returnPromise = summaryPromise
.then(() => {
if (this.lastValidatePromise === summaryPromise) {
return Promise.resolve(this.getFieldsValue(namePathList));
if (lastValidatePromise.value === summaryPromise) {
return Promise.resolve(getFieldsValue(namePathList));
}
return Promise.reject([]);
})
.catch(results => {
const errorList = results.filter(result => result && result.errors.length);
return Promise.reject({
values: this.getFieldsValue(namePathList),
values: getFieldsValue(namePathList),
errorFields: errorList,
outOfDate: this.lastValidatePromise !== summaryPromise,
outOfDate: lastValidatePromise.value !== summaryPromise,
});
});
@ -273,29 +285,65 @@ const Form = defineComponent({
returnPromise.catch(e => e);
return returnPromise;
},
validateField(...args: any[]) {
return this.validateFields(...args);
},
},
};
const validateField = (...args: any[]) => {
return validateFields(...args);
};
render() {
const { prefixCls: customizePrefixCls, hideRequiredMark, layout, handleSubmit } = this;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('form', customizePrefixCls);
const { class: className, ...restProps } = this.$attrs;
const handleSubmit = (e: Event) => {
e.preventDefault();
e.stopPropagation();
emit('submit', e);
const res = validateFields();
res
.then(values => {
emit('finish', values);
})
.catch(errors => {
handleFinishFailed(errors);
});
};
const formClassName = classNames(prefixCls, className, {
[`${prefixCls}-horizontal`]: layout === 'horizontal',
[`${prefixCls}-vertical`]: layout === 'vertical',
[`${prefixCls}-inline`]: layout === 'inline',
[`${prefixCls}-hide-required-mark`]: hideRequiredMark,
expose({
resetFields,
clearValidate,
validateFields,
getFieldsValue,
validate,
scrollToField,
});
return (
<form onSubmit={handleSubmit} class={formClassName} {...restProps}>
{getSlot(this)}
</form>
useProvideForm({
model: computed(() => props.model),
name: computed(() => props.name),
labelAlign: computed(() => props.labelAlign),
labelCol: computed(() => props.labelCol),
wrapperCol: computed(() => props.wrapperCol),
vertical: computed(() => props.layout === 'vertical'),
colon: computed(() => props.colon),
requiredMark: mergedRequiredMark,
validateTrigger: computed(() => props.validateTrigger),
rules: computed(() => props.rules),
addField,
removeField,
});
watch(
() => props.rules,
() => {
if (props.validateOnRuleChange) {
validateFields();
}
},
);
return () => {
return (
<form onSubmit={handleSubmit} class={formClassName.value}>
{slots.default?.()}
</form>
);
};
},
});

View File

@ -1,47 +1,47 @@
import {
inject,
provide,
PropType,
defineComponent,
computed,
nextTick,
ExtractPropTypes,
ref,
watchEffect,
onBeforeUnmount,
ComputedRef,
} from 'vue';
import cloneDeep from 'lodash-es/cloneDeep';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
import { getTransitionProps, Transition } from '../_util/transition';
import Row from '../grid/Row';
import Col, { ColProps } from '../grid/Col';
import hasProp, {
findDOMNode,
getComponent,
getOptionProps,
getEvents,
isValidElement,
getSlot,
} from '../_util/props-util';
import { ColProps } from '../grid/Col';
import { isValidElement, flattenChildren, filterEmpty } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
import { defaultConfigProvider } from '../config-provider';
import { cloneElement } from '../_util/vnode';
import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled';
import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled';
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
import { validateRules } from './utils/validateUtil';
import { validateRules as validateRulesUtil } from './utils/validateUtil';
import { getNamePath } from './utils/valueUtil';
import { toArray } from './utils/typeUtil';
import { warning } from '../vc-util/warning';
import find from 'lodash-es/find';
import { tuple, VueNode } from '../_util/type';
import { ValidateOptions } from './interface';
import { tuple } from '../_util/type';
import { InternalNamePath, RuleObject, ValidateOptions } from './interface';
import useConfigInject from '../_util/hooks/useConfigInject';
import { useInjectForm } from './context';
import FormItemLabel from './FormItemLabel';
import FormItemInput from './FormItemInput';
import { ValidationRule } from './Form';
const iconMap = {
success: CheckCircleFilled,
warning: ExclamationCircleFilled,
error: CloseCircleFilled,
validating: LoadingOutlined,
};
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
export type ValidateStatus = typeof ValidateStatuses[number];
export interface FieldExpose {
fieldValue: ComputedRef<any>;
fieldId: ComputedRef<any>;
fieldName: ComputedRef<any>;
resetField: () => void;
clearValidate: () => void;
namePath: ComputedRef<InternalNamePath>;
rules?: ComputedRef<ValidationRule[]>;
validateRules: (options: ValidateOptions) => Promise<void> | Promise<string[]>;
}
function getPropByPath(obj: any, namePathList: any, strict?: boolean) {
let tempObj = obj;
@ -95,19 +95,29 @@ export const formItemProps = {
validateStatus: PropTypes.oneOf(tuple('', 'success', 'warning', 'error', 'validating')),
validateTrigger: { type: [String, Array] as PropType<string | string[]> },
messageVariables: { type: Object as PropType<Record<string, string>> },
hidden: Boolean,
};
export type FormItemProps = Partial<ExtractPropTypes<typeof formItemProps>>;
let indexGuid = 0;
export default defineComponent({
name: 'AFormItem',
mixins: [BaseMixin],
inheritAttrs: false,
__ANT_NEW_FORM_ITEM: true,
props: formItemProps,
setup(props) {
const FormContext = inject('FormContext', {}) as any;
slots: ['help', 'label', 'extra'],
setup(props, { slots }) {
warning(props.prop === undefined, `\`prop\` is deprecated. Please use \`name\` instead.`);
const eventKey = `form-item-${++indexGuid}`;
const { prefixCls } = useConfigInject('form', props);
const formContext = useInjectForm();
const fieldName = computed(() => props.name || props.prop);
const errors = ref([]);
const validateDisabled = ref(false);
const domErrorVisible = ref(false);
const inputRef = ref();
const namePath = computed(() => {
const val = fieldName.value;
return getNamePath(val);
@ -119,26 +129,30 @@ export default defineComponent({
} else if (!namePath.value.length) {
return undefined;
} else {
const formName = FormContext.name;
const formName = formContext.name.value;
const mergedId = namePath.value.join('_');
return formName ? `${formName}_${mergedId}` : mergedId;
}
});
const fieldValue = computed(() => {
const model = FormContext.model;
const model = formContext.model.value;
if (!model || !fieldName.value) {
return;
}
return getPropByPath(model, namePath.value, true).v;
});
const initialValue = ref(cloneDeep(fieldValue.value));
const mergedValidateTrigger = computed(() => {
let validateTrigger =
props.validateTrigger !== undefined ? props.validateTrigger : FormContext.validateTrigger;
props.validateTrigger !== undefined
? props.validateTrigger
: formContext.validateTrigger.value;
validateTrigger = validateTrigger === undefined ? 'change' : validateTrigger;
return toArray(validateTrigger);
});
const getRules = () => {
let formRules = FormContext.rules;
const rulesRef = computed<ValidationRule[]>(() => {
let formRules = formContext.rules.value;
const selfRules = props.rules;
const requiredRule =
props.required !== undefined
@ -152,9 +166,9 @@ export default defineComponent({
} else {
return rules.concat(requiredRule);
}
};
});
const isRequired = computed(() => {
const rules = getRules();
const rules = rulesRef.value;
let isRequired = false;
if (rules && rules.length) {
rules.every(rule => {
@ -167,351 +181,233 @@ export default defineComponent({
}
return isRequired || props.required;
});
return {
isFormItemChildren: inject('isFormItemChildren', false),
configProvider: inject('configProvider', defaultConfigProvider),
FormContext,
fieldId,
fieldName,
namePath,
isRequired,
getRules,
fieldValue,
mergedValidateTrigger,
};
},
data() {
warning(!hasProp(this, 'prop'), `\`prop\` is deprecated. Please use \`name\` instead.`);
return {
validateState: this.validateStatus,
validateMessage: '',
validateDisabled: false,
validator: {},
helpShow: false,
errors: [],
initialValue: undefined,
};
},
watch: {
validateStatus(val) {
this.validateState = val;
},
},
created() {
provide('isFormItemChildren', true);
},
mounted() {
if (this.fieldName) {
const { addField } = this.FormContext;
addField && addField(this);
this.initialValue = cloneDeep(this.fieldValue);
}
},
beforeUnmount() {
const { removeField } = this.FormContext;
removeField && removeField(this);
},
methods: {
getNamePath() {
const { fieldName } = this;
const { prefixName = [] } = this.FormContext;
return fieldName !== undefined ? [...prefixName, ...this.namePath] : [];
},
validateRules(options: ValidateOptions) {
const { validateFirst = false, messageVariables } = this.$props;
const validateState = ref();
watchEffect(() => {
validateState.value = props.validateStatus;
});
const validateRules = (options: ValidateOptions) => {
const { validateFirst = false, messageVariables } = props;
const { triggerName } = options || {};
const namePath = this.getNamePath();
let filteredRules = this.getRules();
let filteredRules = rulesRef.value;
if (triggerName) {
filteredRules = filteredRules.filter(rule => {
const { trigger } = rule;
if (!trigger && !this.mergedValidateTrigger.length) {
if (!trigger && !mergedValidateTrigger.value.length) {
return true;
}
const triggerList = toArray(trigger || this.mergedValidateTrigger);
const triggerList = toArray(trigger || mergedValidateTrigger.value);
return triggerList.includes(triggerName);
});
}
if (!filteredRules.length) {
return Promise.resolve();
}
const promise = validateRules(
namePath,
this.fieldValue,
filteredRules,
const promise = validateRulesUtil(
namePath.value,
fieldValue.value,
filteredRules as RuleObject[],
options,
validateFirst,
messageVariables,
);
this.validateState = 'validating';
this.errors = [];
validateState.value = 'validating';
errors.value = [];
promise
.catch(e => e)
.then((errors = []) => {
if (this.validateState === 'validating') {
this.validateState = errors.length ? 'error' : 'success';
this.validateMessage = errors[0];
this.errors = errors;
.then((ers = []) => {
if (validateState.value === 'validating') {
validateState.value = ers.length ? 'error' : 'success';
errors.value = ers;
}
});
return promise;
},
onFieldBlur() {
this.validateRules({ triggerName: 'blur' });
},
onFieldChange() {
if (this.validateDisabled) {
this.validateDisabled = false;
};
const onFieldBlur = () => {
validateRules({ triggerName: 'blur' });
};
const onFieldChange = () => {
if (validateDisabled.value) {
validateDisabled.value = false;
return;
}
this.validateRules({ triggerName: 'change' });
},
clearValidate() {
this.validateState = '';
this.validateMessage = '';
this.validateDisabled = false;
},
resetField() {
this.validateState = '';
this.validateMessage = '';
const model = this.FormContext.model || {};
const value = this.fieldValue;
const prop = getPropByPath(model, this.namePath, true);
this.validateDisabled = true;
validateRules({ triggerName: 'change' });
};
const clearValidate = () => {
validateState.value = '';
validateDisabled.value = false;
errors.value = [];
};
const resetField = () => {
validateState.value = '';
validateDisabled.value = true;
errors.value = [];
const model = formContext.model.value || {};
const value = fieldValue.value;
const prop = getPropByPath(model, namePath.value, true);
if (Array.isArray(value)) {
prop.o[prop.k] = [].concat(this.initialValue);
prop.o[prop.k] = [].concat(initialValue.value);
} else {
prop.o[prop.k] = this.initialValue;
prop.o[prop.k] = initialValue.value;
}
// reset validateDisabled after onFieldChange triggered
nextTick(() => {
this.validateDisabled = false;
validateDisabled.value = false;
});
},
getHelpMessage() {
const help = getComponent(this, 'help');
};
return this.validateMessage || help;
},
onLabelClick() {
const id = this.fieldId;
if (!id) {
const onLabelClick = () => {
const id = fieldId.value;
if (!id || !inputRef.value) {
return;
}
const formItemNode = findDOMNode(this);
const control = formItemNode.querySelector(`[id="${id}"]`);
const control = inputRef.value.$el.querySelector(`[id="${id}"]`);
if (control && control.focus) {
control.focus();
}
},
};
formContext.addField(eventKey, {
fieldValue,
fieldId,
fieldName,
resetField,
clearValidate,
namePath,
validateRules,
rules: rulesRef,
});
onBeforeUnmount(() => {
formContext.removeField(eventKey);
});
// const onHelpAnimEnd = (_key: string, helpShow: boolean) => {
// this.helpShow = helpShow;
// if (!helpShow) {
// this.$forceUpdate();
// }
// };
const itemClassName = computed(() => ({
[`${prefixCls.value}-item`]: true,
onHelpAnimEnd(_key: string, helpShow: boolean) {
this.helpShow = helpShow;
if (!helpShow) {
this.$forceUpdate();
}
},
renderHelp(prefixCls: string) {
const help = this.getHelpMessage();
const children = help ? (
<div class={`${prefixCls}-explain`} key="help">
{help}
</div>
) : null;
if (children) {
this.helpShow = !!children;
}
const transitionProps = getTransitionProps('show-help', {
onAfterEnter: () => this.onHelpAnimEnd('help', true),
onAfterLeave: () => this.onHelpAnimEnd('help', false),
});
return (
<Transition {...transitionProps} key="help">
{children}
</Transition>
);
},
renderExtra(prefixCls: string) {
const extra = getComponent(this, 'extra');
return extra ? <div class={`${prefixCls}-extra`}>{extra}</div> : null;
},
renderValidateWrapper(prefixCls: string, c1: VueNode, c2: VueNode, c3: VueNode) {
const validateStatus = this.validateState;
let classes = `${prefixCls}-item-control`;
if (validateStatus) {
classes = classNames(`${prefixCls}-item-control`, {
'has-feedback': validateStatus && this.hasFeedback,
'has-success': validateStatus === 'success',
'has-warning': validateStatus === 'warning',
'has-error': validateStatus === 'error',
'is-validating': validateStatus === 'validating',
// Status
[`${prefixCls.value}-item-has-feedback`]: validateState.value && props.hasFeedback,
[`${prefixCls.value}-item-has-success`]: validateState.value === 'success',
[`${prefixCls.value}-item-has-warning`]: validateState.value === 'warning',
[`${prefixCls.value}-item-has-error`]: validateState.value === 'error',
[`${prefixCls.value}-item-is-validating`]: validateState.value === 'validating',
[`${prefixCls.value}-item-hidden`]: props.hidden,
}));
return () => {
const help = props.help ?? (slots.help ? filterEmpty(slots.help()) : null);
const children = flattenChildren(slots.default?.());
let firstChildren = children[0];
if (fieldName.value && props.autoLink && isValidElement(firstChildren)) {
const originalEvents = firstChildren.props;
const originalBlur = originalEvents.onBlur;
const originalChange = originalEvents.onChange;
firstChildren = cloneElement(firstChildren, {
...(fieldId.value ? { id: fieldId.value } : undefined),
onBlur: (...args: any[]) => {
if (Array.isArray(originalChange)) {
for (let i = 0, l = originalChange.length; i < l; i++) {
originalBlur[i](...args);
}
} else if (originalBlur) {
originalBlur(...args);
}
onFieldBlur();
},
onChange: (...args: any[]) => {
if (Array.isArray(originalChange)) {
for (let i = 0, l = originalChange.length; i < l; i++) {
originalChange[i](...args);
}
} else if (originalChange) {
originalChange(...args);
}
onFieldChange();
},
});
}
const IconNode = validateStatus && iconMap[validateStatus];
const icon =
this.hasFeedback && IconNode ? (
<span class={`${prefixCls}-item-children-icon`}>
<IconNode />
</span>
) : null;
return (
<div class={classes}>
<span class={`${prefixCls}-item-children`}>
{c1}
{icon}
</span>
{c2}
{c3}
</div>
);
},
renderWrapper(prefixCls: string, children: VueNode) {
const { wrapperCol: contextWrapperCol } = (this.isFormItemChildren
? {}
: this.FormContext) as any;
const { wrapperCol } = this;
const mergedWrapperCol = wrapperCol || contextWrapperCol || {};
const { style, id, ...restProps } = mergedWrapperCol;
const className = classNames(`${prefixCls}-item-control-wrapper`, mergedWrapperCol.class);
const colProps = {
...restProps,
class: className,
key: 'wrapper',
style,
id,
};
return <Col {...colProps}>{children}</Col>;
},
renderLabel(prefixCls: string) {
const {
vertical,
labelAlign: contextLabelAlign,
labelCol: contextLabelCol,
colon: contextColon,
} = this.FormContext;
const { labelAlign, labelCol, colon, fieldId, htmlFor } = this;
const label = getComponent(this, 'label');
const required = this.isRequired;
const mergedLabelCol = labelCol || contextLabelCol || {};
const mergedLabelAlign = labelAlign || contextLabelAlign;
const labelClsBasic = `${prefixCls}-item-label`;
const labelColClassName = classNames(
labelClsBasic,
mergedLabelAlign === 'left' && `${labelClsBasic}-left`,
mergedLabelCol.class,
);
const {
class: labelColClass,
style: labelColStyle,
id: labelColId,
...restProps
} = mergedLabelCol;
let labelChildren = label;
// Keep label is original where there should have no colon
const computedColon = colon === true || (contextColon !== false && colon !== false);
const haveColon = computedColon && !vertical;
// Remove duplicated user input colon
if (haveColon && typeof label === 'string' && label.trim() !== '') {
labelChildren = label.replace(/[:]\s*$/, '');
}
const labelClassName = classNames({
[`${prefixCls}-item-required`]: required,
[`${prefixCls}-item-no-colon`]: !computedColon,
});
const colProps = {
...restProps,
class: labelColClassName,
key: 'label',
style: labelColStyle,
id: labelColId,
};
return label ? (
<Col {...colProps}>
<label
for={htmlFor || fieldId}
class={labelClassName}
title={typeof label === 'string' ? label : ''}
onClick={this.onLabelClick}
<Row
class={[
itemClassName.value,
domErrorVisible.value || !!help ? `${prefixCls.value}-item-with-help` : '',
]}
key="row"
>
{/* Label */}
<FormItemLabel
{...props}
htmlFor={fieldId.value}
required={isRequired.value}
requiredMark={formContext.requiredMark.value}
prefixCls={prefixCls.value}
onClick={onLabelClick}
label={props.label ?? slots.label?.()}
/>
{/* Input Group */}
<FormItemInput
{...props}
errors={help !== undefined && help !== null ? toArray(help) : errors.value}
prefixCls={prefixCls.value}
status={validateState.value}
onDomErrorVisibleChange={(v: boolean) => (domErrorVisible.value = v)}
validateStatus={validateState.value}
ref={inputRef}
help={help}
extra={props.extra ?? slots.extra?.()}
>
{labelChildren}
</label>
</Col>
) : null;
},
renderChildren(prefixCls: string, child: VueNode) {
return [
this.renderLabel(prefixCls),
this.renderWrapper(
prefixCls,
this.renderValidateWrapper(
prefixCls,
child,
this.renderHelp(prefixCls),
this.renderExtra(prefixCls),
),
),
];
},
renderFormItem(child: any[]) {
const { prefixCls: customizePrefixCls } = this.$props;
const { class: className, ...restProps } = this.$attrs as any;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('form', customizePrefixCls);
const children = this.renderChildren(prefixCls, child);
const itemClassName = {
[className]: className,
[`${prefixCls}-item`]: true,
[`${prefixCls}-item-with-help`]: this.helpShow,
};
return (
<Row class={classNames(itemClassName)} key="row" {...restProps}>
{children}
{[firstChildren, children.slice(1)]}
</FormItemInput>
</Row>
);
},
},
render() {
const { autoLink } = getOptionProps(this);
const children = getSlot(this);
let firstChildren = children[0];
if (this.fieldName && autoLink && isValidElement(firstChildren)) {
const originalEvents = getEvents(firstChildren);
const originalBlur = originalEvents.onBlur;
const originalChange = originalEvents.onChange;
firstChildren = cloneElement(firstChildren, {
...(this.fieldId ? { id: this.fieldId } : undefined),
onBlur: (...args: any[]) => {
originalBlur && originalBlur(...args);
this.onFieldBlur();
},
onChange: (...args: any[]) => {
if (Array.isArray(originalChange)) {
for (let i = 0, l = originalChange.length; i < l; i++) {
originalChange[i](...args);
}
} else if (originalChange) {
originalChange(...args);
}
this.onFieldChange();
},
});
}
return this.renderFormItem([firstChildren, children.slice(1)]);
};
},
// data() {
// warning(!hasProp(this, 'prop'), `\`prop\` is deprecated. Please use \`name\` instead.`);
// return {
// validateState: this.validateStatus,
// validateMessage: '',
// validateDisabled: false,
// validator: {},
// helpShow: false,
// errors: [],
// initialValue: undefined,
// };
// },
// render() {
// const { autoLink } = getOptionProps(this);
// const children = getSlot(this);
// let firstChildren = children[0];
// if (this.fieldName && autoLink && isValidElement(firstChildren)) {
// const originalEvents = getEvents(firstChildren);
// const originalBlur = originalEvents.onBlur;
// const originalChange = originalEvents.onChange;
// firstChildren = cloneElement(firstChildren, {
// ...(this.fieldId ? { id: this.fieldId } : undefined),
// onBlur: (...args: any[]) => {
// originalBlur && originalBlur(...args);
// this.onFieldBlur();
// },
// onChange: (...args: any[]) => {
// if (Array.isArray(originalChange)) {
// for (let i = 0, l = originalChange.length; i < l; i++) {
// originalChange[i](...args);
// }
// } else if (originalChange) {
// originalChange(...args);
// }
// this.onFieldChange();
// },
// });
// }
// return this.renderFormItem([firstChildren, children.slice(1)]);
// },
});

View File

@ -0,0 +1,119 @@
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled';
import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled';
import Col, { ColProps } from '../grid/Col';
import { useProvideForm, useInjectForm, useProvideFormItemPrefix } from './context';
import ErrorList from './ErrorList';
import classNames from '../_util/classNames';
import { ValidateStatus } from './FormItem';
import { VueNode } from '../_util/type';
import { computed, defineComponent, HTMLAttributes, onUnmounted } from 'vue';
export interface FormItemInputMiscProps {
prefixCls: string;
errors: VueNode[];
hasFeedback?: boolean;
validateStatus?: ValidateStatus;
onDomErrorVisibleChange: (visible: boolean) => void;
}
export interface FormItemInputProps {
wrapperCol?: ColProps;
help?: VueNode;
extra?: VueNode;
status?: ValidateStatus;
}
const iconMap: { [key: string]: any } = {
success: CheckCircleFilled,
warning: ExclamationCircleFilled,
error: CloseCircleFilled,
validating: LoadingOutlined,
};
const FormItemInput = defineComponent({
slots: ['help', 'extra', 'errors'],
inheritAttrs: false,
props: [
'prefixCls',
'errors',
'hasFeedback',
'validateStatus',
'onDomErrorVisibleChange',
'wrapperCol',
'help',
'extra',
'status',
],
setup(props, { slots }) {
const formContext = useInjectForm();
const { wrapperCol: contextWrapperCol } = formContext;
// Pass to sub FormItem should not with col info
const subFormContext = { ...formContext };
delete subFormContext.labelCol;
delete subFormContext.wrapperCol;
useProvideForm(subFormContext);
useProvideFormItemPrefix({
prefixCls: computed(() => props.prefixCls),
status: computed(() => props.status),
});
onUnmounted(() => {
props.onDomErrorVisibleChange(false);
});
return () => {
const {
prefixCls,
wrapperCol,
help = slots.help?.(),
errors = slots.errors?.(),
onDomErrorVisibleChange,
hasFeedback,
validateStatus,
extra = slots.extra?.(),
} = props;
const baseClassName = `${prefixCls}-item`;
const mergedWrapperCol: ColProps & HTMLAttributes =
wrapperCol || contextWrapperCol?.value || {};
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.class);
// Should provides additional icon if `hasFeedback`
const IconNode = validateStatus && iconMap[validateStatus];
const icon =
hasFeedback && IconNode ? (
<span class={`${baseClassName}-children-icon`}>
<IconNode />
</span>
) : null;
const inputDom = (
<div class={`${baseClassName}-control-input`}>
<div class={`${baseClassName}-control-input-content`}>{slots.default?.()}</div>
{icon}
</div>
);
const errorListDom = (
<ErrorList errors={errors} help={help} onDomErrorVisibleChange={onDomErrorVisibleChange} />
);
// If extra = 0, && will goes wrong
// 0&&error -> 0
const extraDom = extra ? <div class={`${baseClassName}-extra`}>{extra}</div> : null;
return (
<Col {...mergedWrapperCol} class={className}>
{inputDom}
{errorListDom}
{extraDom}
</Col>
);
};
},
});
export default FormItemInput;

View File

@ -0,0 +1,97 @@
import Col, { ColProps } from '../grid/Col';
import { FormLabelAlign } from './interface';
import { useInjectForm } from './context';
import { RequiredMark } from './Form';
import { useLocaleReceiver } from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale/default';
import classNames from '../_util/classNames';
import { VueNode } from '../_util/type';
import { FunctionalComponent, HTMLAttributes } from 'vue';
export interface FormItemLabelProps {
colon?: boolean;
htmlFor?: string;
label?: VueNode;
labelAlign?: FormLabelAlign;
labelCol?: ColProps & HTMLAttributes;
requiredMark?: RequiredMark;
required?: boolean;
prefixCls: string;
onClick: Function;
}
const FormItemLabel: FunctionalComponent<FormItemLabelProps> = (props, { slots, emit, attrs }) => {
const { prefixCls, htmlFor, labelCol, labelAlign, colon, required, requiredMark } = {
...props,
...attrs,
};
const [formLocale] = useLocaleReceiver('Form');
const label = props.label ?? slots.label?.();
if (!label) return null;
const {
vertical,
labelAlign: contextLabelAlign,
labelCol: contextLabelCol,
colon: contextColon,
} = useInjectForm();
const mergedLabelCol: FormItemLabelProps['labelCol'] = labelCol || contextLabelCol?.value || {};
const mergedLabelAlign: FormLabelAlign | undefined = labelAlign || contextLabelAlign?.value;
const labelClsBasic = `${prefixCls}-item-label`;
const labelColClassName = classNames(
labelClsBasic,
mergedLabelAlign === 'left' && `${labelClsBasic}-left`,
mergedLabelCol.class,
);
let labelChildren = label;
// Keep label is original where there should have no colon
const computedColon = colon === true || (contextColon?.value !== false && colon !== false);
const haveColon = computedColon && !vertical.value;
// Remove duplicated user input colon
if (haveColon && typeof label === 'string' && (label as string).trim() !== '') {
labelChildren = (label as string).replace(/[:|]\s*$/, '');
}
labelChildren = (
<>
{labelChildren}
{slots.tooltip?.({ class: `${prefixCls}-item-tooltip` })}
</>
);
// Add required mark if optional
if (requiredMark === 'optional' && !required) {
labelChildren = (
<>
{labelChildren}
<span class={`${prefixCls}-item-optional`}>
{formLocale.value?.optional || defaultLocale.Form?.optional}
</span>
</>
);
}
const labelClassName = classNames({
[`${prefixCls}-item-required`]: required,
[`${prefixCls}-item-required-mark-optional`]: requiredMark === 'optional',
[`${prefixCls}-item-no-colon`]: !computedColon,
});
return (
<Col {...mergedLabelCol} class={labelColClassName}>
<label
html-for={htmlFor}
class={labelClassName}
title={typeof label === 'string' ? label : ''}
onClick={e => emit('click', e)}
>
{labelChildren}
</label>
</Col>
);
};
FormItemLabel.displayName = 'FormItemLabel';
FormItemLabel.inheritAttrs = false;
export default FormItemLabel;

View File

@ -0,0 +1,58 @@
import { inject, InjectionKey, provide, ComputedRef, computed } from 'vue';
import { ColProps } from '../grid';
import { RequiredMark, ValidationRule } from './Form';
import { ValidateStatus, FieldExpose } from './FormItem';
import { FormLabelAlign } from './interface';
export interface FormContextProps {
model?: ComputedRef<any>;
vertical: ComputedRef<boolean>;
name?: ComputedRef<string>;
colon?: ComputedRef<boolean>;
labelAlign?: ComputedRef<FormLabelAlign>;
labelCol?: ComputedRef<ColProps>;
wrapperCol?: ComputedRef<ColProps>;
requiredMark?: ComputedRef<RequiredMark>;
//itemRef: (name: (string | number)[]) => (node: React.ReactElement) => void;
addField: (eventKey: string, field: FieldExpose) => void;
removeField: (eventKey: string) => void;
validateTrigger?: ComputedRef<string | string[]>;
rules?: ComputedRef<{ [k: string]: ValidationRule[] | ValidationRule }>;
}
export const FormContextKey: InjectionKey<FormContextProps> = Symbol('formContextKey');
export const useProvideForm = (state: FormContextProps) => {
provide(FormContextKey, state);
};
export const useInjectForm = () => {
return inject(FormContextKey, {
labelAlign: computed(() => 'right' as FormLabelAlign),
vertical: computed(() => false),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
addField: (_eventKey: string, _field: FieldExpose) => {},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
removeField: (_eventKey: string) => {},
});
};
/** Used for ErrorList only */
export interface FormItemPrefixContextProps {
prefixCls: ComputedRef<string>;
status?: ComputedRef<ValidateStatus>;
}
export const FormItemPrefixContextKey: InjectionKey<FormItemPrefixContextProps> = Symbol(
'formItemPrefixContextKey',
);
export const useProvideFormItemPrefix = (state: FormItemPrefixContextProps) => {
provide(FormItemPrefixContextKey, state);
};
export const useInjectFormItemPrefix = () => {
return inject(FormItemPrefixContextKey, {
prefixCls: computed(() => ''),
});
};

View File

@ -1,5 +1,7 @@
import { VueNode } from '../_util/type';
export type FormLabelAlign = 'left' | 'right';
export type InternalNamePath = (string | number)[];
export type NamePath = string | number | InternalNamePath;

View File

@ -0,0 +1,71 @@
@import './index';
// ================================================================
// = Children Component =
// ================================================================
.@{form-item-prefix-cls} {
.@{ant-prefix}-mentions,
textarea.@{ant-prefix}-input {
height: auto;
}
// input[type=file]
.@{ant-prefix}-upload {
background: transparent;
}
.@{ant-prefix}-upload.@{ant-prefix}-upload-drag {
background: @background-color-light;
}
input[type='radio'],
input[type='checkbox'] {
width: 14px;
height: 14px;
}
// Radios and checkboxes on same line
.@{ant-prefix}-radio-inline,
.@{ant-prefix}-checkbox-inline {
display: inline-block;
margin-left: 8px;
font-weight: normal;
vertical-align: middle;
cursor: pointer;
&:first-child {
margin-left: 0;
}
}
.@{ant-prefix}-checkbox-vertical,
.@{ant-prefix}-radio-vertical {
display: block;
}
.@{ant-prefix}-checkbox-vertical + .@{ant-prefix}-checkbox-vertical,
.@{ant-prefix}-radio-vertical + .@{ant-prefix}-radio-vertical {
margin-left: 0;
}
.@{ant-prefix}-input-number {
+ .@{form-prefix-cls}-text {
margin-left: 8px;
}
&-handler-wrap {
z-index: 2; // https://github.com/ant-design/ant-design/issues/6289
}
}
.@{ant-prefix}-select,
.@{ant-prefix}-cascader-picker {
width: 100%;
}
// Don't impact select inside input group and calendar header select
.@{ant-prefix}-picker-calendar-year-select,
.@{ant-prefix}-picker-calendar-month-select,
.@{ant-prefix}-input-group .@{ant-prefix}-select,
.@{ant-prefix}-input-group .@{ant-prefix}-cascader-picker {
width: auto;
}
}

View File

@ -0,0 +1,10 @@
@import './index';
.@{form-prefix-cls}-horizontal {
.@{form-item-prefix-cls}-label {
flex-grow: 0;
}
.@{form-item-prefix-cls}-control {
flex: 1 1 0;
}
}

View File

@ -3,94 +3,82 @@
@import '../../input/style/mixin';
@import '../../button/style/mixin';
@import '../../grid/style/mixin';
@import './components';
@import './inline';
@import './horizontal';
@import './vertical';
@import './status';
@import './mixin';
@form-prefix-cls: ~'@{ant-prefix}-form';
@form-component-height: @input-height-base;
@form-component-max-height: @input-height-lg;
@form-feedback-icon-size: @font-size-base;
@form-help-margin-top: ((@form-component-height - @form-component-max-height) / 2) + 2px;
@form-explain-font-size: @font-size-base;
// Extends additional 1px to fix precision issue.
// https://github.com/ant-design/ant-design/issues/12803
// https://github.com/ant-design/ant-design/issues/8220
@form-explain-precision: 1px;
@form-explain-height: floor(@form-explain-font-size * @line-height-base);
@form-item-prefix-cls: ~'@{form-prefix-cls}-item';
@form-font-height: ceil(@font-size-base * @line-height-base);
.@{form-prefix-cls} {
.reset-component();
.reset-form();
}
.@{form-prefix-cls}-item-required::before {
display: inline-block;
margin-right: 4px;
color: @label-required-color;
font-size: @font-size-base;
font-family: SimSun, sans-serif;
line-height: 1;
content: '*';
.@{form-prefix-cls}-hide-required-mark & {
display: none;
.@{form-prefix-cls}-text {
display: inline-block;
padding-right: 8px;
}
}
.@{form-prefix-cls}-item-label > label {
color: @label-color;
&::after {
& when (@form-item-trailing-colon=true) {
content: ':';
}
& when not (@form-item-trailing-colon=true) {
content: ' ';
// ================================================================
// = Size =
// ================================================================
.formSize(@input-height) {
.@{form-item-prefix-cls}-label > label {
height: @input-height;
}
position: relative;
top: -0.5px;
margin: 0 @form-item-label-colon-margin-right 0 @form-item-label-colon-margin-left;
}
&.@{form-prefix-cls}-item-no-colon::after {
content: ' ';
}
}
// Form items
// You should wrap labels and controls in .@{form-prefix-cls}-item for optimum spacing
.@{form-prefix-cls}-item {
label {
position: relative;
> .@{iconfont-css-prefix} {
font-size: @font-size-base;
vertical-align: top;
.@{form-item-prefix-cls}-control-input {
min-height: @input-height;
}
}
&-small {
.formSize(@input-height-sm);
}
&-large {
.formSize(@input-height-lg);
}
}
.explainAndExtraDistance(@num) when (@num >= 0) {
padding-top: floor(@num);
}
.explainAndExtraDistance(@num) when (@num < 0) {
margin-top: ceil(@num);
margin-bottom: ceil(@num);
}
// ================================================================
// = Item =
// ================================================================
.@{form-item-prefix-cls} {
.reset-component();
margin-bottom: @form-item-margin-bottom;
vertical-align: top;
&-control {
position: relative;
line-height: @form-component-max-height;
.clearfix();
}
&-children {
position: relative;
}
&-with-help {
margin-bottom: max(0, @form-item-margin-bottom - @form-explain-height - @form-help-margin-top);
margin-bottom: 0;
}
&-hidden,
&-hidden.@{ant-prefix}-row {
// https://github.com/ant-design/ant-design/issues/26141
display: none;
}
// ==============================================================
// = Label =
// ==============================================================
&-label {
display: inline-block;
flex-grow: 0;
overflow: hidden;
line-height: @form-component-max-height - 0.0001px;
white-space: nowrap;
text-align: right;
vertical-align: middle;
@ -98,505 +86,126 @@
&-left {
text-align: left;
}
}
.@{ant-prefix}-switch {
margin: 2px 0 4px;
}
}
.@{form-prefix-cls}-explain,
.@{form-prefix-cls}-extra {
clear: both;
min-height: @form-explain-height + @form-explain-precision;
margin-top: @form-help-margin-top;
color: @text-color-secondary;
font-size: @form-explain-font-size;
line-height: @line-height-base;
transition: color 0.3s @ease-out; // sync input color transition
}
.@{form-prefix-cls}-explain {
margin-bottom: -@form-explain-precision;
}
.@{form-prefix-cls}-extra {
padding-top: 4px;
}
.@{form-prefix-cls}-text {
display: inline-block;
padding-right: 8px;
}
.@{form-prefix-cls}-split {
display: block;
text-align: center;
}
form {
.has-feedback {
// https://github.com/ant-design/ant-design/issues/19884
.@{ant-prefix}-input-affix-wrapper {
.@{ant-prefix}-input-suffix {
padding-right: 18px;
}
}
// Fix overlapping between feedback icon and <Select>'s arrow.
// https://github.com/ant-design/ant-design/issues/4431
> .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
> .@{ant-prefix}-select .@{ant-prefix}-select-selection__clear,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
:not(.@{ant-prefix}-input-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-selection__clear {
right: (@form-component-height / 2) + @form-feedback-icon-size - 2px;
}
> .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value,
:not(.@{ant-prefix}-input-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-selection-selected-value {
padding-right: 42px;
}
.@{ant-prefix}-cascader-picker {
&-arrow {
margin-right: (@form-component-height / 2) + @form-feedback-icon-size - 13px;
}
&-clear {
right: (@form-component-height / 2) + @form-feedback-icon-size - 2px;
}
}
// Fix issue: https://github.com/ant-design/ant-design/issues/7854
.@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) {
.@{ant-prefix}-input-suffix {
right: (@form-component-height / 2) + @form-feedback-icon-size - 2px;
}
}
// Fix issue: https://github.com/ant-design/ant-design/issues/4783
.@{ant-prefix}-calendar-picker,
.@{ant-prefix}-time-picker {
&-icon,
&-clear {
right: (@form-component-height / 2) + @form-feedback-icon-size - 2px;
}
}
}
.@{ant-prefix}-mentions,
textarea.@{ant-prefix}-input {
height: auto;
margin-bottom: 4px;
}
// input[type=file]
.@{ant-prefix}-upload {
background: transparent;
}
input[type='radio'],
input[type='checkbox'] {
width: 14px;
height: 14px;
}
// Radios and checkboxes on same line
.@{ant-prefix}-radio-inline,
.@{ant-prefix}-checkbox-inline {
display: inline-block;
margin-left: 8px;
font-weight: normal;
vertical-align: middle;
cursor: pointer;
&:first-child {
margin-left: 0;
}
}
.@{ant-prefix}-checkbox-vertical,
.@{ant-prefix}-radio-vertical {
display: block;
}
.@{ant-prefix}-checkbox-vertical + .@{ant-prefix}-checkbox-vertical,
.@{ant-prefix}-radio-vertical + .@{ant-prefix}-radio-vertical {
margin-left: 0;
}
.@{ant-prefix}-input-number {
+ .@{form-prefix-cls}-text {
margin-left: 8px;
}
&-handler-wrap {
z-index: 2; // https://github.com/ant-design/ant-design/issues/6289
}
}
.@{ant-prefix}-select,
.@{ant-prefix}-cascader-picker {
width: 100%;
}
// Don't impact select inside input group
.@{ant-prefix}-input-group .@{ant-prefix}-select,
.@{ant-prefix}-input-group .@{ant-prefix}-cascader-picker {
width: auto;
}
// fix input with addon position. https://github.com/ant-design/ant-design/issues/8243
:not(.@{ant-prefix}-input-group-wrapper) > .@{ant-prefix}-input-group,
.@{ant-prefix}-input-group-wrapper {
display: inline-block;
vertical-align: middle;
}
// https://github.com/ant-design/ant-design/issues/20616
&:not(.@{form-prefix-cls}-vertical) {
:not(.@{ant-prefix}-input-group-wrapper) > .@{ant-prefix}-input-group,
.@{ant-prefix}-input-group-wrapper {
> label {
position: relative;
top: -1px;
// display: inline;
display: inline-flex;
align-items: center;
height: @form-item-label-height;
color: @label-color;
font-size: @form-item-label-font-size;
> .@{iconfont-css-prefix} {
font-size: @form-item-label-font-size;
vertical-align: top;
}
// Required mark
&.@{form-item-prefix-cls}-required:not(.@{form-item-prefix-cls}-required-mark-optional)::before {
display: inline-block;
margin-right: 4px;
color: @label-required-color;
font-size: @form-item-label-font-size;
font-family: SimSun, sans-serif;
line-height: 1;
content: '*';
.@{form-prefix-cls}-hide-required-mark & {
display: none;
}
}
// Optional mark
.@{form-item-prefix-cls}-optional {
display: inline-block;
margin-left: @margin-xss;
color: @text-color-secondary;
.@{form-prefix-cls}-hide-required-mark & {
display: none;
}
}
// Optional mark
.@{form-item-prefix-cls}-tooltip {
color: @text-color-secondary;
cursor: help;
writing-mode: horizontal-tb;
margin-inline-start: @margin-xss;
}
&::after {
& when (@form-item-trailing-colon=true) {
content: ':';
}
& when not (@form-item-trailing-colon=true) {
content: ' ';
}
position: relative;
top: -0.5px;
margin: 0 @form-item-label-colon-margin-right 0 @form-item-label-colon-margin-left;
}
&.@{form-item-prefix-cls}-no-colon::after {
content: ' ';
}
}
}
}
// Form layout
//== Vertical Form
.make-vertical-layout-label() {
display: block;
margin: @form-vertical-label-margin;
padding: @form-vertical-label-padding;
line-height: @line-height-base;
white-space: initial;
text-align: left;
label::after {
display: none;
}
}
.make-vertical-layout() {
.@{form-prefix-cls}-item-label,
.@{form-prefix-cls}-item-control-wrapper {
display: block;
width: 100%;
}
.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
.@{form-prefix-cls}-vertical {
.@{form-prefix-cls}-item {
// ==============================================================
// = Input =
// ==============================================================
&-control {
display: flex;
flex-direction: column;
flex-grow: 1;
&-label > label {
height: auto;
&:first-child:not([class^=~"'@{ant-prefix}-col-'"]):not([class*=~"' @{ant-prefix}-col-'"]) {
width: 100%;
}
}
// fix https://github.com/vueComponent/ant-design-vue/issues/3319
.@{form-prefix-cls}-item-control-wrapper {
width: 100%;
}
}
&-control-input {
position: relative;
display: flex;
align-items: center;
min-height: @input-height-base;
.@{form-prefix-cls}-vertical .@{form-prefix-cls}-item-label,
// when labelCol is 24, it is a vertical form
.@{ant-prefix}-col-24.@{form-prefix-cls}-item-label,
.@{ant-prefix}-col-xl-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
.@{form-prefix-cls}-vertical {
.@{form-prefix-cls}-item {
padding-bottom: 8px;
&-content {
flex: auto;
max-width: 100%;
}
}
.@{form-prefix-cls}-item-control {
&-explain,
&-extra {
clear: both;
min-height: @form-item-margin-bottom;
color: @text-color-secondary;
font-size: @font-size-base;
line-height: @line-height-base;
}
.@{form-prefix-cls}-explain {
margin-top: 2px;
margin-bottom: -4px - @form-explain-precision;
}
.@{form-prefix-cls}-extra {
margin-top: 2px;
margin-bottom: -4px;
}
}
@media (max-width: @screen-xs-max) {
.make-vertical-layout();
.@{ant-prefix}-col-xs-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-sm-max) {
.@{ant-prefix}-col-sm-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-md-max) {
.@{ant-prefix}-col-md-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-lg-max) {
.@{ant-prefix}-col-lg-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-xl-max) {
.@{ant-prefix}-col-xl-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
//== Inline Form
.@{form-prefix-cls}-inline {
display: flex;
flex-wrap: wrap;
.@{form-prefix-cls}-item {
flex: none;
flex-wrap: nowrap;
margin-right: 16px;
margin-bottom: 0;
&-with-help {
margin-bottom: @form-item-margin-bottom;
}
> .@{form-prefix-cls}-item-control-wrapper,
> .@{form-prefix-cls}-item-label {
display: inline-block;
vertical-align: top;
}
> .@{form-prefix-cls}-item-label {
flex: none;
}
transition: color 0.3s @ease-out; // sync input color transition
.explainAndExtraDistance((@form-item-margin-bottom - @form-font-height) / 2);
}
.@{form-prefix-cls}-text {
display: inline-block;
}
.has-feedback {
display: inline-block;
}
}
// Validation state
.has-success,
.has-warning,
.has-error,
.is-validating {
&.has-feedback .@{form-prefix-cls}-item-children-icon {
position: absolute;
top: 50%;
right: 0;
z-index: 1;
width: @form-component-height;
height: 20px;
margin-top: -10px;
font-size: @form-feedback-icon-size;
line-height: 20px;
text-align: center;
visibility: visible;
animation: zoomIn 0.3s @ease-out-back;
pointer-events: none;
& svg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
}
}
.has-success {
&.has-feedback .@{form-prefix-cls}-item-children-icon {
color: @success-color;
animation-name: diffZoomIn1 !important;
}
}
.has-warning {
.form-control-validation(@warning-color; @warning-color; @form-warning-input-bg;);
&.has-feedback .@{form-prefix-cls}-item-children-icon {
color: @warning-color;
animation-name: diffZoomIn3 !important;
}
//select
.@{ant-prefix}-select {
&-selector {
border-color: @warning-color;
&:hover {
border-color: @warning-color;
}
}
&-open .@{ant-prefix}-select-selector,
&-focused .@{ant-prefix}-select-selector {
.active(@warning-color);
}
}
// arrow and icon
.@{ant-prefix}-calendar-picker-icon::after,
.@{ant-prefix}-time-picker-icon::after,
.@{ant-prefix}-picker-icon::after,
.@{ant-prefix}-select-arrow,
.@{ant-prefix}-cascader-picker-arrow {
color: @warning-color;
}
//input-number, timepicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-time-picker-input {
border-color: @warning-color;
&-focused,
&:focus {
.active(@warning-color);
}
&:not([disabled]):hover {
border-color: @warning-color;
}
}
.@{ant-prefix}-cascader-picker {
&:focus .@{ant-prefix}-cascader-input {
.active(@warning-color);
}
&:hover .@{ant-prefix}-cascader-input {
border-color: @warning-color;
}
}
}
.has-error,
&-has-error {
.form-control-validation(@error-color; @error-color; @form-error-input-bg;);
&.has-feedback .@{form-prefix-cls}-item-children-icon {
color: @error-color;
animation-name: diffZoomIn2 !important;
}
//select
.@{ant-prefix}-select:not(.@{ant-prefix}-select-borderless) {
.@{ant-prefix}-select-selector {
border-color: @error-color !important;
}
&.@{ant-prefix}-select-open .@{ant-prefix}-select-selector,
&.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector {
.active(@error-color);
}
}
.@{ant-prefix}-select.@{ant-prefix}-select-auto-complete {
.@{ant-prefix}-input:focus {
border-color: @error-color;
}
}
.@{ant-prefix}-input-group-addon .@{ant-prefix}-select {
&-selection {
border-color: transparent;
box-shadow: none;
}
}
//input-number, timepicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-time-picker-input {
border-color: @error-color;
&-focused,
&:focus {
.active(@error-color);
}
&:not([disabled]):hover {
border-color: @error-color;
}
}
.@{ant-prefix}-mention-wrapper {
.@{ant-prefix}-mention-editor {
&,
&:not([disabled]):hover {
border-color: @error-color;
}
}
&.@{ant-prefix}-mention-active:not([disabled]) .@{ant-prefix}-mention-editor,
.@{ant-prefix}-mention-editor:not([disabled]):focus {
.active(@error-color);
}
}
.@{ant-prefix}-cascader-picker {
&:focus .@{ant-prefix}-cascader-input {
.active(@error-color);
}
&:hover .@{ant-prefix}-cascader-input {
border-color: @error-color;
}
}
// transfer
.@{ant-prefix}-transfer {
&-list {
border-color: @error-color;
&-search:not([disabled]) {
border-color: @input-border-color;
&:hover {
.hover();
}
&:focus {
.active();
}
}
}
}
}
.is-validating {
&.has-feedback .@{form-prefix-cls}-item-children-icon {
display: inline-block;
color: @primary-color;
}
}
.@{ant-prefix}-advanced-search-form {
.@{form-prefix-cls}-item {
margin-bottom: @form-item-margin-bottom;
&-with-help {
margin-bottom: @form-item-margin-bottom - @form-explain-height - @form-help-margin-top;
.@{ant-prefix}-input-textarea-show-count {
&::after {
margin-bottom: -22px;
}
}
}
.show-help-motion(@className, @keyframeName, @duration: @animation-duration-slow) {
.make-motion(@className, @keyframeName, @duration);
.@{className}-enter,
.@{className}-appear {
@name: ~'@{ant-prefix}-@{className}';
.make-motion(@name, @keyframeName, @duration);
.@{name}-enter,
.@{name}-appear {
opacity: 0;
animation-timing-function: @ease-in-out;
}
.@{className}-leave {
.@{name}-leave {
animation-timing-function: @ease-in-out;
}
}
@ -649,3 +258,5 @@ form {
transform: scale(1);
}
}
@import './rtl';

View File

@ -3,3 +3,4 @@ import './index.less';
// style dependencies
import '../../grid/style';
import '../../tooltip/style';

View File

@ -0,0 +1,35 @@
@import './index';
.@{form-prefix-cls}-inline {
display: flex;
flex-wrap: wrap;
.@{form-prefix-cls}-item {
flex: none;
flex-wrap: nowrap;
margin-right: 16px;
margin-bottom: 0;
&-with-help {
margin-bottom: @form-item-margin-bottom;
}
> .@{form-item-prefix-cls}-label,
> .@{form-item-prefix-cls}-control {
display: inline-block;
vertical-align: top;
}
> .@{form-item-prefix-cls}-label {
flex: none;
}
.@{form-prefix-cls}-text {
display: inline-block;
}
.@{form-item-prefix-cls}-has-feedback {
display: inline-block;
}
}
}

View File

@ -1,8 +1,7 @@
@import '../../input/style/mixin';
.form-control-validation(@text-color: @input-color; @border-color: @input-border-color; @background-color: @input-bg) {
.@{ant-prefix}-form-explain,
.@{ant-prefix}-form-split {
.@{ant-prefix}-form-item-split {
color: @text-color;
}
// 输入框的不同校验状态
@ -10,6 +9,7 @@
.@{ant-prefix}-input-affix-wrapper {
&,
&:hover {
background-color: @background-color;
border-color: @border-color;
}
@ -19,18 +19,23 @@
}
}
.@{ant-prefix}-input {
&:not(&-disabled) {
background-color: @background-color;
.@{ant-prefix}-input-disabled {
&,
&:hover {
background-color: @input-disabled-bg;
border-color: @input-border-color;
}
}
.@{ant-prefix}-input-affix-wrapper {
&:not(&-disabled) {
background-color: @background-color;
}
input:focus {
box-shadow: none !important;
.@{ant-prefix}-input-affix-wrapper-disabled {
&,
&:hover {
background-color: @input-disabled-bg;
border-color: @input-border-color;
input:focus {
box-shadow: none !important;
}
}
}

View File

@ -0,0 +1,185 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import '../../input/style/mixin';
@import '../../button/style/mixin';
@import '../../grid/style/mixin';
@form-prefix-cls: ~'@{ant-prefix}-form';
@form-item-prefix-cls: ~'@{form-prefix-cls}-item';
.@{form-prefix-cls} {
&-rtl {
direction: rtl;
}
}
// ================================================================
// = Item =
// ================================================================
.@{form-item-prefix-cls} {
// ==============================================================
// = Label =
// ==============================================================
&-label {
.@{form-prefix-cls}-rtl & {
text-align: left;
}
> label {
&.@{form-item-prefix-cls}-required::before {
.@{form-prefix-cls}-rtl & {
margin-right: 0;
margin-left: 4px;
}
}
&::after {
.@{form-prefix-cls}-rtl & {
margin: 0 @form-item-label-colon-margin-left 0 @form-item-label-colon-margin-right;
}
}
.@{form-item-prefix-cls}-optional {
.@{form-prefix-cls}-rtl & {
margin-right: @margin-xss;
margin-left: 0;
}
}
}
}
// ==============================================================
// = Input =
// ==============================================================
&-control {
.@{ant-prefix}-col-rtl &:first-child {
width: 100%;
}
}
// status
&-has-feedback {
.@{ant-prefix}-input {
.@{form-prefix-cls}-rtl & {
padding-right: @input-padding-horizontal-base;
padding-left: 24px;
}
}
.@{ant-prefix}-input-affix-wrapper {
.@{ant-prefix}-input-suffix {
.@{form-prefix-cls}-rtl & {
padding-right: @input-padding-horizontal-base;
padding-left: 18px;
}
}
.@{ant-prefix}-input {
.@{form-prefix-cls}-rtl & {
padding: 0;
}
}
}
.@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) {
.@{ant-prefix}-input-suffix {
.@{form-prefix-cls}-rtl & {
right: auto;
left: 28px;
}
}
}
.@{ant-prefix}-input-number {
.@{form-prefix-cls}-rtl & {
padding-left: 18px;
}
}
> .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
> .@{ant-prefix}-select .@{ant-prefix}-select-clear,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-clear {
.@{form-prefix-cls}-rtl & {
right: auto;
left: 32px;
}
}
> .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value,
:not(.@{ant-prefix}-input-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-selection-selected-value {
.@{form-prefix-cls}-rtl & {
padding-right: 0;
padding-left: 42px;
}
}
.@{ant-prefix}-cascader-picker {
&-arrow {
.@{form-prefix-cls}-rtl & {
margin-right: 0;
margin-left: 19px;
}
}
&-clear {
.@{form-prefix-cls}-rtl & {
right: auto;
left: 32px;
}
}
}
.@{ant-prefix}-picker {
.@{form-prefix-cls}-rtl & {
padding-right: @input-padding-horizontal-base;
padding-left: @input-padding-horizontal-base + @font-size-base * 1.3;
}
&-large {
.@{form-prefix-cls}-rtl & {
padding-right: @input-padding-horizontal-lg;
padding-left: @input-padding-horizontal-lg + @font-size-base * 1.3;
}
}
&-small {
.@{form-prefix-cls}-rtl & {
padding-right: @input-padding-horizontal-sm;
padding-left: @input-padding-horizontal-sm + @font-size-base * 1.3;
}
}
}
&.@{form-item-prefix-cls} {
&-has-success,
&-has-warning,
&-has-error,
&-is-validating {
// ====================== Icon ======================
.@{form-item-prefix-cls}-children-icon {
.@{form-prefix-cls}-rtl & {
right: auto;
left: 0;
}
}
}
}
}
}
// inline
.@{form-prefix-cls}-inline {
.@{form-prefix-cls}-item {
.@{form-prefix-cls}-rtl& {
margin-right: 0;
margin-left: 16px;
}
}
}
// vertical
.make-vertical-layout-label() {
.@{form-prefix-cls}-rtl& {
text-align: right;
}
}

View File

@ -0,0 +1,278 @@
@import './index.less';
.@{form-item-prefix-cls} {
// ================================================================
// = Status =
// ================================================================
/* Some non-status related component style is in `components.less` */
// ========================= Explain =========================
/* To support leave along ErrorList. We add additional className to handle explain style */
&-explain {
&&-error {
color: @error-color;
}
&&-warning {
color: @warning-color;
}
}
&-has-feedback {
// ========================= Input =========================
.@{ant-prefix}-input {
padding-right: 24px;
}
// https://github.com/ant-design/ant-design/issues/19884
.@{ant-prefix}-input-affix-wrapper {
.@{ant-prefix}-input-suffix {
padding-right: 18px;
}
}
// Fix issue: https://github.com/ant-design/ant-design/issues/7854
.@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) {
.@{ant-prefix}-input-suffix {
right: 28px;
}
}
// ======================== Switch =========================
.@{ant-prefix}-switch {
margin: 2px 0 4px;
}
// ======================== Select =========================
// Fix overlapping between feedback icon and <Select>'s arrow.
// https://github.com/ant-design/ant-design/issues/4431
> .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
> .@{ant-prefix}-select .@{ant-prefix}-select-clear,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-clear {
right: 32px;
}
> .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value,
:not(.@{ant-prefix}-input-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-selection-selected-value {
padding-right: 42px;
}
// ======================= Cascader ========================
.@{ant-prefix}-cascader-picker {
&-arrow {
margin-right: 19px;
}
&-clear {
right: 32px;
}
}
// ======================== Picker =========================
// Fix issue: https://github.com/ant-design/ant-design/issues/4783
.@{ant-prefix}-picker {
padding-right: @input-padding-horizontal-base + @font-size-base * 1.3;
&-large {
padding-right: @input-padding-horizontal-lg + @font-size-base * 1.3;
}
&-small {
padding-right: @input-padding-horizontal-sm + @font-size-base * 1.3;
}
}
// ===================== Status Group ======================
&.@{form-item-prefix-cls} {
&-has-success,
&-has-warning,
&-has-error,
&-is-validating {
// ====================== Icon ======================
.@{form-item-prefix-cls}-children-icon {
position: absolute;
top: 50%;
right: 0;
z-index: 1;
width: @input-height-base;
height: 20px;
margin-top: -10px;
font-size: @font-size-base;
line-height: 20px;
text-align: center;
visibility: visible;
animation: zoomIn 0.3s @ease-out-back;
pointer-events: none;
}
}
}
}
// ======================== Success ========================
&-has-success {
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
color: @success-color;
animation-name: diffZoomIn1 !important;
}
}
// ======================== Warning ========================
&-has-warning {
.form-control-validation(@warning-color; @warning-color; @form-warning-input-bg);
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
color: @warning-color;
animation-name: diffZoomIn3 !important;
}
// Select
.@{ant-prefix}-select:not(.@{ant-prefix}-select-disabled):not(.@{ant-prefix}-select-customize-input) {
.@{ant-prefix}-select-selector {
background-color: @form-warning-input-bg;
border-color: @warning-color !important;
}
&.@{ant-prefix}-select-open .@{ant-prefix}-select-selector,
&.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector {
.active(@warning-color);
}
}
// InputNumber, TimePicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-picker {
background-color: @form-warning-input-bg;
border-color: @warning-color;
&-focused,
&:focus {
.active(@warning-color);
}
&:not([disabled]):hover {
background-color: @form-warning-input-bg;
border-color: @warning-color;
}
}
.@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input {
.active(@warning-color);
}
}
// ========================= Error =========================
&-has-error {
.form-control-validation(@error-color; @error-color; @form-error-input-bg);
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
color: @error-color;
animation-name: diffZoomIn2 !important;
}
// Select
.@{ant-prefix}-select:not(.@{ant-prefix}-select-disabled):not(.@{ant-prefix}-select-customize-input) {
.@{ant-prefix}-select-selector {
background-color: @form-error-input-bg;
border-color: @error-color !important;
}
&.@{ant-prefix}-select-open .@{ant-prefix}-select-selector,
&.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector {
.active(@error-color);
}
}
// fixes https://github.com/ant-design/ant-design/issues/20482
.@{ant-prefix}-input-group-addon .@{ant-prefix}-select {
&.@{ant-prefix}-select-single:not(.@{ant-prefix}-select-customize-input)
.@{ant-prefix}-select-selector {
background-color: inherit;
border: 0;
box-shadow: none;
}
}
.@{ant-prefix}-select.@{ant-prefix}-select-auto-complete {
.@{ant-prefix}-input:focus {
border-color: @error-color;
}
}
// InputNumber, TimePicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-picker {
background-color: @form-error-input-bg;
border-color: @error-color;
&-focused,
&:focus {
.active(@error-color);
}
&:not([disabled]):hover {
background-color: @form-error-input-bg;
border-color: @error-color;
}
}
.@{ant-prefix}-mention-wrapper {
.@{ant-prefix}-mention-editor {
&,
&:not([disabled]):hover {
background-color: @form-error-input-bg;
border-color: @error-color;
}
}
&.@{ant-prefix}-mention-active:not([disabled]) .@{ant-prefix}-mention-editor,
.@{ant-prefix}-mention-editor:not([disabled]):focus {
.active(@error-color);
}
}
// cascader
.@{ant-prefix}-cascader-picker {
&:hover
.@{ant-prefix}-cascader-picker-label:hover
+ .@{ant-prefix}-cascader-input.@{ant-prefix}-input {
border-color: @error-color;
}
&:focus .@{ant-prefix}-cascader-input {
background-color: @form-error-input-bg;
.active(@error-color);
}
}
// transfer
.@{ant-prefix}-transfer {
&-list {
border-color: @error-color;
&-search:not([disabled]) {
border-color: @input-border-color;
&:hover {
.hover();
}
&:focus {
.active();
}
}
}
}
// RadioGroup
.@{ant-prefix}-radio-button-wrapper {
border-color: @error-color !important;
&:not(:first-child) {
&::before {
background-color: @error-color;
}
}
}
}
// ====================== Validating =======================
&-is-validating {
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
display: inline-block;
color: @primary-color;
}
}
}

View File

@ -0,0 +1,84 @@
@import './index';
// ================== Label ==================
.make-vertical-layout-label() {
& when (@form-vertical-label-margin > 0) {
margin: @form-vertical-label-margin;
}
padding: @form-vertical-label-padding;
line-height: @line-height-base;
white-space: initial;
text-align: left;
> label {
margin: 0;
&::after {
display: none;
}
}
}
.make-vertical-layout() {
.@{form-prefix-cls}-item .@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
.@{form-prefix-cls} {
.@{form-prefix-cls}-item {
flex-wrap: wrap;
.@{form-prefix-cls}-item-label,
.@{form-prefix-cls}-item-control {
flex: 0 0 100%;
max-width: 100%;
}
}
}
}
.@{form-prefix-cls}-vertical {
.@{form-item-prefix-cls} {
flex-direction: column;
&-label > label {
height: auto;
}
}
}
.@{form-prefix-cls}-vertical .@{form-item-prefix-cls}-label,
// when labelCol is 24, it is a vertical form
.@{ant-prefix}-col-24.@{form-item-prefix-cls}-label,
.@{ant-prefix}-col-xl-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
@media (max-width: @screen-xs-max) {
.make-vertical-layout();
.@{ant-prefix}-col-xs-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-sm-max) {
.@{ant-prefix}-col-sm-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-md-max) {
.@{ant-prefix}-col-md-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-lg-max) {
.@{ant-prefix}-col-lg-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-xl-max) {
.@{ant-prefix}-col-xl-24.@{form-item-prefix-cls}-label {
.make-vertical-layout-label();
}
}

View File

@ -1,8 +1,8 @@
import { inject, defineComponent, HTMLAttributes, CSSProperties } from 'vue';
import { defineComponent, CSSProperties, ExtractPropTypes, computed } from 'vue';
import classNames from '../_util/classNames';
import PropTypes from '../_util/vue-types';
import { defaultConfigProvider } from '../config-provider';
import { rowContextState } from './Row';
import useConfigInject from '../_util/hooks/useConfigInject';
import { useInjectRow } from './context';
type ColSpanType = number | string;
@ -16,22 +16,6 @@ export interface ColSize {
pull?: ColSpanType;
}
export interface ColProps extends HTMLAttributes {
span?: ColSpanType;
order?: ColSpanType;
offset?: ColSpanType;
push?: ColSpanType;
pull?: ColSpanType;
xs?: ColSpanType | ColSize;
sm?: ColSpanType | ColSize;
md?: ColSpanType | ColSize;
lg?: ColSpanType | ColSize;
xl?: ColSpanType | ColSize;
xxl?: ColSpanType | ColSize;
prefixCls?: string;
flex?: FlexType;
}
function parseFlex(flex: FlexType): string {
if (typeof flex === 'number') {
return `${flex} ${flex} auto`;
@ -44,92 +28,17 @@ function parseFlex(flex: FlexType): string {
return flex;
}
const ACol = defineComponent<ColProps>({
name: 'ACol',
setup(props, { slots }) {
const configProvider = inject('configProvider', defaultConfigProvider);
const rowContext = inject<rowContextState>('rowContext', {});
return () => {
const { gutter } = rowContext;
const { prefixCls: customizePrefixCls, span, order, offset, push, pull, flex } = props;
const prefixCls = configProvider.getPrefixCls('col', customizePrefixCls);
let sizeClassObj = {};
['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].forEach(size => {
let sizeProps: ColSize = {};
const propSize = props[size];
if (typeof propSize === 'number') {
sizeProps.span = propSize;
} else if (typeof propSize === 'object') {
sizeProps = propSize || {};
}
sizeClassObj = {
...sizeClassObj,
[`${prefixCls}-${size}-${sizeProps.span}`]: sizeProps.span !== undefined,
[`${prefixCls}-${size}-order-${sizeProps.order}`]:
sizeProps.order || sizeProps.order === 0,
[`${prefixCls}-${size}-offset-${sizeProps.offset}`]:
sizeProps.offset || sizeProps.offset === 0,
[`${prefixCls}-${size}-push-${sizeProps.push}`]: sizeProps.push || sizeProps.push === 0,
[`${prefixCls}-${size}-pull-${sizeProps.pull}`]: sizeProps.pull || sizeProps.pull === 0,
};
});
const classes = classNames(
prefixCls,
{
[`${prefixCls}-${span}`]: span !== undefined,
[`${prefixCls}-order-${order}`]: order,
[`${prefixCls}-offset-${offset}`]: offset,
[`${prefixCls}-push-${push}`]: push,
[`${prefixCls}-pull-${pull}`]: pull,
},
sizeClassObj,
);
let mergedStyle: CSSProperties = {};
if (gutter) {
mergedStyle = {
...(gutter[0] > 0
? {
paddingLeft: `${gutter[0] / 2}px`,
paddingRight: `${gutter[0] / 2}px`,
}
: {}),
...(gutter[1] > 0
? {
paddingTop: `${gutter[1] / 2}px`,
paddingBottom: `${gutter[1] / 2}px`,
}
: {}),
...mergedStyle,
};
}
if (flex) {
mergedStyle.flex = parseFlex(flex);
}
return (
<div class={classes} style={mergedStyle}>
{slots.default?.()}
</div>
);
};
},
});
const stringOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
export const ColSize = PropTypes.shape({
export const colSize = PropTypes.shape({
span: stringOrNumber,
order: stringOrNumber,
offset: stringOrNumber,
push: stringOrNumber,
pull: stringOrNumber,
}).loose;
const objectOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number, colSize]);
const objectOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number, ColSize]);
ACol.props = {
const colProps = {
span: stringOrNumber,
order: stringOrNumber,
offset: stringOrNumber,
@ -145,4 +54,85 @@ ACol.props = {
flex: stringOrNumber,
};
export default ACol;
export type ColProps = Partial<ExtractPropTypes<typeof colProps>>;
export default defineComponent({
name: 'ACol',
props: colProps,
setup(props, { slots }) {
const { gutter, supportFlexGap, wrap } = useInjectRow();
const { prefixCls, direction } = useConfigInject('col', props);
const classes = computed(() => {
const { span, order, offset, push, pull } = props;
const pre = prefixCls.value;
let sizeClassObj = {};
['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].forEach(size => {
let sizeProps: ColSize = {};
const propSize = props[size];
if (typeof propSize === 'number') {
sizeProps.span = propSize;
} else if (typeof propSize === 'object') {
sizeProps = propSize || {};
}
sizeClassObj = {
...sizeClassObj,
[`${pre}-${size}-${sizeProps.span}`]: sizeProps.span !== undefined,
[`${pre}-${size}-order-${sizeProps.order}`]: sizeProps.order || sizeProps.order === 0,
[`${pre}-${size}-offset-${sizeProps.offset}`]: sizeProps.offset || sizeProps.offset === 0,
[`${pre}-${size}-push-${sizeProps.push}`]: sizeProps.push || sizeProps.push === 0,
[`${pre}-${size}-pull-${sizeProps.pull}`]: sizeProps.pull || sizeProps.pull === 0,
[`${pre}-rtl`]: direction.value === 'rtl',
};
});
return classNames(
pre,
{
[`${pre}-${span}`]: span !== undefined,
[`${pre}-order-${order}`]: order,
[`${pre}-offset-${offset}`]: offset,
[`${pre}-push-${push}`]: push,
[`${pre}-pull-${pull}`]: pull,
},
sizeClassObj,
);
});
const mergedStyle = computed(() => {
const { flex } = props;
const gutterVal = gutter.value;
const style: CSSProperties = {};
// Horizontal gutter use padding
if (gutterVal && gutterVal[0] > 0) {
const horizontalGutter = `${gutterVal[0] / 2}px`;
style.paddingLeft = horizontalGutter;
style.paddingRight = horizontalGutter;
}
// Vertical gutter use padding when gap not support
if (gutterVal && gutterVal[1] > 0 && !supportFlexGap.value) {
const verticalGutter = `${gutterVal[1] / 2}px`;
style.paddingTop = verticalGutter;
style.paddingBottom = verticalGutter;
}
if (flex) {
style.flex = parseFlex(flex);
// Hack for Firefox to avoid size issue
// https://github.com/ant-design/ant-design/pull/20023#issuecomment-564389553
if (flex === 'auto' && wrap.value === false && !style.minWidth) {
style.minWidth = 0;
}
}
return style;
});
return () => {
return (
<div class={classes.value} style={mergedStyle.value}>
{slots.default?.()}
</div>
);
};
},
});

View File

@ -1,22 +1,23 @@
import {
inject,
provide,
reactive,
defineComponent,
HTMLAttributes,
ref,
onMounted,
onBeforeUnmount,
ExtractPropTypes,
computed,
CSSProperties,
} from 'vue';
import classNames from '../_util/classNames';
import { tuple } from '../_util/type';
import PropTypes from '../_util/vue-types';
import { defaultConfigProvider } from '../config-provider';
import ResponsiveObserve, {
Breakpoint,
ScreenMap,
responsiveArray,
} from '../_util/responsiveObserve';
import useConfigInject from '../_util/hooks/useConfigInject';
import useFlexGapSupport from '../_util/hooks/useFlexGapSupport';
import useProvideRow from './context';
const RowAligns = tuple('top', 'middle', 'bottom', 'stretch');
const RowJustify = tuple('start', 'end', 'center', 'space-around', 'space-between');
@ -27,24 +28,36 @@ export interface rowContextState {
gutter?: [number, number];
}
export interface RowProps extends HTMLAttributes {
type?: 'flex';
gutter?: Gutter | [Gutter, Gutter];
align?: typeof RowAligns[number];
justify?: typeof RowJustify[number];
prefixCls?: string;
}
const rowProps = {
type: PropTypes.oneOf(['flex']),
align: PropTypes.oneOf(RowAligns),
justify: PropTypes.oneOf(RowJustify),
prefixCls: PropTypes.string,
gutter: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]).def(0),
wrap: PropTypes.looseBool,
};
const ARow = defineComponent<RowProps>({
export type RowProps = Partial<ExtractPropTypes<typeof rowProps>>;
const ARow = defineComponent({
name: 'ARow',
props: rowProps,
setup(props, { slots }) {
const rowContext = reactive<rowContextState>({
gutter: undefined,
});
provide('rowContext', rowContext);
const { prefixCls, direction } = useConfigInject('row', props);
let token: number;
const screens = ref<ScreenMap>({
xs: true,
sm: true,
md: true,
lg: true,
xl: true,
xxl: true,
});
const supportFlexGap = useFlexGapSupport();
onMounted(() => {
token = ResponsiveObserve.subscribe(screen => {
const currentGutter = props.gutter || 0;
@ -62,18 +75,7 @@ const ARow = defineComponent<RowProps>({
ResponsiveObserve.unsubscribe(token);
});
const screens = ref<ScreenMap>({
xs: true,
sm: true,
md: true,
lg: true,
xl: true,
xxl: true,
});
const { getPrefixCls } = inject('configProvider', defaultConfigProvider);
const getGutter = (): [number, number] => {
const gutter = computed(() => {
const results: [number, number] = [0, 0];
const { gutter = 0 } = props;
const normalizedGutter = Array.isArray(gutter) ? gutter : [gutter, 0];
@ -91,34 +93,48 @@ const ARow = defineComponent<RowProps>({
}
});
return results;
};
});
useProvideRow({
gutter,
supportFlexGap,
wrap: computed(() => props.wrap),
});
const classes = computed(() =>
classNames(prefixCls.value, {
[`${prefixCls.value}-no-wrap`]: props.wrap === false,
[`${prefixCls.value}-${props.justify}`]: props.justify,
[`${prefixCls.value}-${props.align}`]: props.align,
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
}),
);
const rowStyle = computed(() => {
const gt = gutter.value;
// Add gutter related style
const style: CSSProperties = {};
const horizontalGutter = gt[0] > 0 ? `${gt[0] / -2}px` : undefined;
const verticalGutter = gt[1] > 0 ? `${gt[1] / -2}px` : undefined;
if (horizontalGutter) {
style.marginLeft = horizontalGutter;
style.marginRight = horizontalGutter;
}
if (supportFlexGap.value) {
// Set gap direct if flex gap support
style.rowGap = `${gt[1]}px`;
} else if (verticalGutter) {
style.marginTop = verticalGutter;
style.marginBottom = verticalGutter;
}
return style;
});
return () => {
const { prefixCls: customizePrefixCls, justify, align } = props;
const prefixCls = getPrefixCls('row', customizePrefixCls);
const gutter = getGutter();
const classes = classNames(prefixCls, {
[`${prefixCls}-${justify}`]: justify,
[`${prefixCls}-${align}`]: align,
});
const rowStyle = {
...(gutter[0] > 0
? {
marginLeft: `${gutter[0] / -2}px`,
marginRight: `${gutter[0] / -2}px`,
}
: {}),
...(gutter[1] > 0
? {
marginTop: `${gutter[1] / -2}px`,
marginBottom: `${gutter[1] / -2}px`,
}
: {}),
};
rowContext.gutter = gutter;
return (
<div class={classes} style={rowStyle}>
<div class={classes.value} style={rowStyle.value}>
{slots.default?.()}
</div>
);
@ -126,12 +142,4 @@ const ARow = defineComponent<RowProps>({
},
});
ARow.props = {
type: PropTypes.oneOf(['flex']),
align: PropTypes.oneOf(RowAligns),
justify: PropTypes.oneOf(RowJustify),
prefixCls: PropTypes.string,
gutter: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]).def(0),
};
export default ARow;

View File

@ -0,0 +1,20 @@
import { Ref, inject, InjectionKey, provide, ComputedRef } from 'vue';
export interface RowContext {
gutter: ComputedRef<[number, number]>;
wrap: ComputedRef<boolean>;
supportFlexGap: Ref<boolean>;
}
export const RowContextKey: InjectionKey<RowContext> = Symbol('rowContextKey');
const useProvideRow = (state: RowContext) => {
provide(RowContextKey, state);
};
const useInjectRow = () => {
return inject(RowContextKey);
};
export { useInjectRow, useProvideRow };
export default useProvideRow;

View File

@ -1,4 +1,11 @@
import Row from './Row';
import Col from './Col';
import useBreakpoint from '../_util/hooks/useBreakpoint';
export { RowProps } from './Row';
export { ColProps, ColSize } from './Col';
export { Row, Col };
export default { useBreakpoint };

View File

@ -11,6 +11,11 @@
&::after {
display: flex;
}
// No wrap of flex
&-no-wrap {
flex-wrap: nowrap;
}
}
// x轴原点

View File

@ -15,7 +15,7 @@ const inputNumberProps = {
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
step: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).def(1),
defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
tabindex: PropTypes.number,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
disabled: PropTypes.looseBool,
size: PropTypes.oneOf(tuple('large', 'small', 'default')),
formatter: PropTypes.func,

View File

@ -1,6 +1,6 @@
import { defineComponent, inject } from 'vue';
import classNames from '../_util/classNames';
import isMobile from '../vc-menu/utils/isMobile';
import isMobile from '../_util/isMobile';
import Input from './Input';
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
import SearchOutlined from '@ant-design/icons-vue/SearchOutlined';

View File

@ -1,17 +1,25 @@
import classNames from '../_util/classNames';
import { inject, provide, PropType, defineComponent, nextTick } from 'vue';
import {
inject,
PropType,
defineComponent,
ExtractPropTypes,
ref,
watch,
onMounted,
onBeforeUnmount,
CSSProperties,
provide,
} from 'vue';
import PropTypes from '../_util/vue-types';
import { tuple } from '../_util/type';
import { getOptionProps, hasProp, getComponent, getSlot } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import BaseMixin from '../_util/BaseMixin';
import isNumeric from '../_util/isNumeric';
import { defaultConfigProvider } from '../config-provider';
import BarsOutlined from '@ant-design/icons-vue/BarsOutlined';
import RightOutlined from '@ant-design/icons-vue/RightOutlined';
import LeftOutlined from '@ant-design/icons-vue/LeftOutlined';
import omit from 'omit.js';
import { SiderHookProvider } from './layout';
import useConfigInject from '../_util/hooks/useConfigInject';
import { SiderCollapsedKey, SiderHookProviderKey } from './injectionKey';
const dimensionMaxMap = {
xs: '479.98px',
@ -24,7 +32,7 @@ const dimensionMaxMap = {
export type CollapseType = 'clickTrigger' | 'responsive';
export const SiderProps = {
export const siderProps = {
prefixCls: PropTypes.string,
collapsible: PropTypes.looseBool,
collapsed: PropTypes.looseBool,
@ -40,6 +48,7 @@ export const SiderProps = {
onCollapse: Function as PropType<(collapsed: boolean, type: CollapseType) => void>,
};
export type SiderProps = Partial<ExtractPropTypes<typeof siderProps>>;
// export interface SiderState {
// collapsed?: boolean;
// below: boolean;
@ -61,10 +70,8 @@ const generateId = (() => {
export default defineComponent({
name: 'ALayoutSider',
mixins: [BaseMixin],
inheritAttrs: false,
__ANT_LAYOUT_SIDER: true,
props: initDefaultProps(SiderProps, {
props: initDefaultProps(siderProps, {
collapsible: false,
defaultCollapsed: false,
reverseArrow: false,
@ -72,173 +79,141 @@ export default defineComponent({
collapsedWidth: 80,
}),
emits: ['breakpoint', 'update:collapsed', 'collapse'],
setup() {
return {
siderHook: inject<SiderHookProvider>('siderHook', {}),
configProvider: inject('configProvider', defaultConfigProvider),
};
},
data() {
const uniqueId = generateId('ant-sider-');
let matchMedia: typeof window.matchMedia;
if (typeof window !== 'undefined') {
matchMedia = window.matchMedia;
}
const props = getOptionProps(this) as any;
let mql: MediaQueryList;
if (matchMedia && props.breakpoint && props.breakpoint in dimensionMaxMap) {
mql = matchMedia(`(max-width: ${dimensionMaxMap[props.breakpoint]})`);
}
let sCollapsed: boolean;
if ('collapsed' in props) {
sCollapsed = props.collapsed;
} else {
sCollapsed = props.defaultCollapsed;
}
return {
sCollapsed,
below: false,
belowShow: false,
uniqueId,
mql,
};
},
watch: {
collapsed(val) {
this.setState({
sCollapsed: val,
});
},
},
created() {
provide('layoutSiderContext', this); // menu使
},
mounted() {
nextTick(() => {
if (this.mql) {
this.mql.addListener(this.responsiveHandler);
this.responsiveHandler(this.mql);
}
if (this.siderHook.addSider) {
this.siderHook.addSider(this.uniqueId);
}
});
},
beforeUnmount() {
if (this.mql) {
this.mql.removeListener(this.responsiveHandler);
}
if (this.siderHook.removeSider) {
this.siderHook.removeSider(this.uniqueId);
}
},
methods: {
responsiveHandler(mql: MediaQueryListEvent | MediaQueryList) {
this.setState({ below: mql.matches });
this.$emit('breakpoint', mql.matches);
if (this.sCollapsed !== mql.matches) {
this.setCollapsed(mql.matches, 'responsive');
}
},
setCollapsed(collapsed: boolean, type: CollapseType) {
if (!hasProp(this, 'collapsed')) {
this.setState({
sCollapsed: collapsed,
});
}
this.$emit('update:collapsed', collapsed);
this.$emit('collapse', collapsed, type);
},
toggle() {
const collapsed = !this.sCollapsed;
this.setCollapsed(collapsed, 'clickTrigger');
},
belowShowChange() {
this.setState({ belowShow: !this.belowShow });
},
},
render() {
const {
prefixCls: customizePrefixCls,
class: className,
theme,
collapsible,
reverseArrow,
style,
width,
collapsedWidth,
zeroWidthTriggerStyle,
...others
} = { ...getOptionProps(this), ...this.$attrs } as any;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('layout-sider', customizePrefixCls);
const divProps = omit(others, [
'collapsed',
'defaultCollapsed',
'onCollapse',
'breakpoint',
'onBreakpoint',
'siderHook',
'zeroWidthTriggerStyle',
'trigger',
]);
const trigger = getComponent(this, 'trigger');
const rawWidth = this.sCollapsed ? collapsedWidth : width;
// use "px" as fallback unit for width
const siderWidth = isNumeric(rawWidth) ? `${rawWidth}px` : String(rawWidth);
// special trigger when collapsedWidth == 0
const zeroWidthTrigger =
parseFloat(String(collapsedWidth || 0)) === 0 ? (
<span
onClick={this.toggle}
class={`${prefixCls}-zero-width-trigger ${prefixCls}-zero-width-trigger-${
reverseArrow ? 'right' : 'left'
}`}
style={zeroWidthTriggerStyle}
>
<BarsOutlined />
</span>
) : null;
const iconObj = {
expanded: reverseArrow ? <RightOutlined /> : <LeftOutlined />,
collapsed: reverseArrow ? <LeftOutlined /> : <RightOutlined />,
};
const status = this.sCollapsed ? 'collapsed' : 'expanded';
const defaultTrigger = iconObj[status];
const triggerDom =
trigger !== null
? zeroWidthTrigger || (
<div class={`${prefixCls}-trigger`} onClick={this.toggle} style={{ width: siderWidth }}>
{trigger || defaultTrigger}
</div>
)
: null;
const divStyle = {
...style,
flex: `0 0 ${siderWidth}`,
maxWidth: siderWidth, // Fix width transition bug in IE11
minWidth: siderWidth, // https://github.com/ant-design/ant-design/issues/6349
width: siderWidth,
};
const siderCls = classNames(className, prefixCls, `${prefixCls}-${theme}`, {
[`${prefixCls}-collapsed`]: !!this.sCollapsed,
[`${prefixCls}-has-trigger`]: collapsible && trigger !== null && !zeroWidthTrigger,
[`${prefixCls}-below`]: !!this.below,
[`${prefixCls}-zero-width`]: parseFloat(siderWidth) === 0,
});
return (
<aside class={siderCls} {...divProps} style={divStyle}>
<div class={`${prefixCls}-children`}>{getSlot(this)}</div>
{collapsible || (this.below && zeroWidthTrigger) ? triggerDom : null}
</aside>
setup(props, { emit, attrs, slots }) {
const { prefixCls } = useConfigInject('layout-sider', props);
const siderHook = inject(SiderHookProviderKey);
const collapsed = ref(
!!(props.collapsed !== undefined ? props.collapsed : props.defaultCollapsed),
);
const below = ref(false);
watch(
() => props.collapsed,
() => {
collapsed.value = !!props.collapsed;
},
);
provide(SiderCollapsedKey, collapsed);
const handleSetCollapsed = (value: boolean, type: CollapseType) => {
if (props.collapsed === undefined) {
collapsed.value = value;
}
emit('update:collapsed', value);
emit('collapse', value, type);
};
// ========================= Responsive =========================
const responsiveHandlerRef = ref<(mql: MediaQueryListEvent | MediaQueryList) => void>(
(mql: MediaQueryListEvent | MediaQueryList) => {
below.value = mql.matches;
emit('breakpoint', mql.matches);
if (collapsed.value !== mql.matches) {
handleSetCollapsed(mql.matches, 'responsive');
}
},
);
let mql: MediaQueryList;
function responsiveHandler(mql: MediaQueryListEvent | MediaQueryList) {
return responsiveHandlerRef.value!(mql);
}
const uniqueId = generateId('ant-sider-');
onMounted(() => {
if (typeof window !== 'undefined') {
const { matchMedia } = window;
if (matchMedia! && props.breakpoint && props.breakpoint in dimensionMaxMap) {
mql = matchMedia(`(max-width: ${dimensionMaxMap[props.breakpoint]})`);
try {
mql.addEventListener('change', responsiveHandler);
} catch (error) {
mql.addListener(responsiveHandler);
}
responsiveHandler(mql);
}
}
siderHook && siderHook.addSider(uniqueId);
});
onBeforeUnmount(() => {
try {
mql?.removeEventListener('change', responsiveHandler);
} catch (error) {
mql?.removeListener(responsiveHandler);
}
siderHook && siderHook.removeSider(uniqueId);
});
const toggle = () => {
handleSetCollapsed(!collapsed.value, 'clickTrigger');
};
return () => {
const pre = prefixCls.value;
const {
collapsedWidth,
width,
reverseArrow,
zeroWidthTriggerStyle,
trigger,
collapsible,
theme,
} = props;
const rawWidth = collapsed.value ? collapsedWidth : width;
// use "px" as fallback unit for width
const siderWidth = isNumeric(rawWidth) ? `${rawWidth}px` : String(rawWidth);
// special trigger when collapsedWidth == 0
const zeroWidthTrigger =
parseFloat(String(collapsedWidth || 0)) === 0 ? (
<span
onClick={toggle}
class={classNames(
`${pre}-zero-width-trigger`,
`${pre}-zero-width-trigger-${reverseArrow ? 'right' : 'left'}`,
)}
style={zeroWidthTriggerStyle}
>
{trigger || <BarsOutlined />}
</span>
) : null;
const iconObj = {
expanded: reverseArrow ? <RightOutlined /> : <LeftOutlined />,
collapsed: reverseArrow ? <LeftOutlined /> : <RightOutlined />,
};
const status = collapsed.value ? 'collapsed' : 'expanded';
const defaultTrigger = iconObj[status];
const triggerDom =
trigger !== null
? zeroWidthTrigger || (
<div class={`${pre}-trigger`} onClick={toggle} style={{ width: siderWidth }}>
{trigger || defaultTrigger}
</div>
)
: null;
const divStyle = {
...(attrs.style as CSSProperties),
flex: `0 0 ${siderWidth}`,
maxWidth: siderWidth, // Fix width transition bug in IE11
minWidth: siderWidth, // https://github.com/ant-design/ant-design/issues/6349
width: siderWidth,
};
const siderCls = classNames(
pre,
`${pre}-${theme}`,
{
[`${pre}-collapsed`]: !!collapsed.value,
[`${pre}-has-trigger`]: collapsible && trigger !== null && !zeroWidthTrigger,
[`${pre}-below`]: !!below.value,
[`${pre}-zero-width`]: parseFloat(siderWidth) === 0,
},
attrs.class,
);
return (
<aside {...attrs} class={siderCls} style={divStyle} ref={ref}>
<div class={`${pre}-children`}>{slots.default?.()}</div>
{collapsible || (below.value && zeroWidthTrigger) ? triggerDom : null}
</aside>
);
};
},
});

View File

@ -2,6 +2,9 @@ import { App, Plugin } from 'vue';
import Layout from './layout';
import Sider from './Sider';
export { BasicProps as LayoutProps } from './layout';
export { SiderProps } from './Sider';
Layout.Sider = Sider;
/* istanbul ignore next */

View File

@ -0,0 +1,12 @@
import { Ref, InjectionKey } from 'vue';
export type SiderCollapsed = Ref<boolean>;
export const SiderCollapsedKey: InjectionKey<SiderCollapsed> = Symbol('siderCollapsed');
export interface SiderHookProvider {
addSider?: (id: string) => void;
removeSider?: (id: string) => void;
}
export const SiderHookProviderKey: InjectionKey<SiderHookProvider> = Symbol('siderHookProvider');

View File

@ -1,17 +1,8 @@
import {
createVNode,
defineComponent,
inject,
provide,
toRefs,
ref,
ExtractPropTypes,
HTMLAttributes,
} from 'vue';
import { createVNode, defineComponent, provide, ref, ExtractPropTypes, HTMLAttributes } from 'vue';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
import { defaultConfigProvider } from '../config-provider';
import { flattenChildren } from '../_util/props-util';
import useConfigInject from '../_util/hooks/useConfigInject';
import { SiderHookProviderKey } from './injectionKey';
export const basicProps = {
prefixCls: PropTypes.string,
@ -21,40 +12,29 @@ export const basicProps = {
export type BasicProps = Partial<ExtractPropTypes<typeof basicProps>> & HTMLAttributes;
export interface SiderHookProvider {
addSider?: (id: string) => void;
removeSider?: (id: string) => void;
}
type GeneratorArgument = {
suffixCls: string;
tagName: string;
tagName: 'header' | 'footer' | 'main' | 'section';
name: string;
};
function generator({ suffixCls, tagName, name }: GeneratorArgument) {
return (BasicComponent: typeof Basic) => {
const Adapter = defineComponent<BasicProps>({
const Adapter = defineComponent({
name,
props: basicProps,
setup(props, { slots }) {
const { getPrefixCls } = inject('configProvider', defaultConfigProvider);
const { prefixCls } = useConfigInject(suffixCls, props);
return () => {
const { prefixCls: customizePrefixCls } = props;
const prefixCls = getPrefixCls(suffixCls, customizePrefixCls);
const basicComponentProps = {
prefixCls,
...props,
prefixCls: prefixCls.value,
tagName,
...props,
};
return (
<BasicComponent {...basicComponentProps}>
{flattenChildren(slots.default?.())}
</BasicComponent>
);
return <BasicComponent {...basicComponentProps}>{slots.default?.()}</BasicComponent>;
};
},
});
Adapter.props = basicProps;
return Adapter;
};
}
@ -62,30 +42,32 @@ function generator({ suffixCls, tagName, name }: GeneratorArgument) {
const Basic = defineComponent({
props: basicProps,
setup(props, { slots }) {
const { prefixCls, tagName } = toRefs(props);
return () => createVNode(tagName.value, { class: prefixCls.value }, slots.default?.());
return () => createVNode(props.tagName, { class: props.prefixCls }, slots.default?.());
},
});
const BasicLayout = defineComponent({
props: basicProps,
setup(props, { slots }) {
const { direction } = useConfigInject('', props);
const siders = ref<string[]>([]);
const siderHookProvider: SiderHookProvider = {
addSider: id => {
const siderHookProvider = {
addSider: (id: string) => {
siders.value = [...siders.value, id];
},
removeSider: id => {
removeSider: (id: string) => {
siders.value = siders.value.filter(currentId => currentId !== id);
},
};
provide('siderHook', siderHookProvider);
provide(SiderHookProviderKey, siderHookProvider);
return () => {
const { prefixCls, hasSider, tagName } = props;
const divCls = classNames(prefixCls, {
[`${prefixCls}-has-sider`]:
typeof hasSider === 'boolean' ? hasSider : siders.value.length > 0,
[`${prefixCls}-rtl`]: direction.value === 'rtl',
});
return createVNode(tagName, { class: divCls }, slots.default?.());
};

View File

@ -2,6 +2,7 @@
@import '../../style/mixins/index';
@layout-prefix-cls: ~'@{ant-prefix}-layout';
@layout-menu-prefix-cls: ~'@{ant-prefix}-menu';
.@{layout-prefix-cls} {
display: flex;
@ -18,9 +19,10 @@
&&-has-sider {
flex-direction: row;
> .@{layout-prefix-cls},
> .@{layout-prefix-cls}-content {
overflow-x: hidden;
width: 0; // https://segmentfault.com/a/1190000019498300
}
}
@ -32,6 +34,7 @@
&-header {
height: @layout-header-height;
padding: @layout-header-padding;
color: @layout-header-color;
line-height: @layout-header-height;
background: @layout-header-background;
}
@ -64,6 +67,10 @@
// https://github.com/ant-design/ant-design/issues/7967
// solution from https://stackoverflow.com/a/33132624/3040605
padding-top: 0.1px;
.@{layout-menu-prefix-cls}.@{layout-menu-prefix-cls}-inline-collapsed {
width: auto;
}
}
&-has-trigger {
@ -88,7 +95,7 @@
}
&-zero-width {
& > * {
> * {
overflow: hidden;
}
@ -108,8 +115,19 @@
cursor: pointer;
transition: background 0.3s ease;
&:hover {
background: tint(@layout-sider-background, 10%);
&::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: transparent;
transition: all 0.3s;
content: '';
}
&:hover::after {
background: rgba(255, 255, 255, 0.1);
}
&-right {
@ -122,3 +140,4 @@
}
@import './light';
@import './rtl';

View File

@ -1,15 +1,11 @@
.@{layout-prefix-cls} {
&-sider {
&-light {
background: @layout-sider-background-light;
}
&-light &-trigger {
color: @layout-trigger-color-light;
background: @layout-trigger-background-light;
}
&-light &-zero-width-trigger {
color: @layout-trigger-color-light;
background: @layout-trigger-background-light;
}
.@{layout-prefix-cls}-sider-light {
background: @layout-sider-background-light;
.@{layout-prefix-cls}-sider-trigger {
color: @layout-trigger-color-light;
background: @layout-trigger-background-light;
}
.@{layout-prefix-cls}-sider-zero-width-trigger {
color: @layout-trigger-color-light;
background: @layout-trigger-background-light;
}
}

View File

@ -0,0 +1,10 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@layout-prefix-cls: ~'@{ant-prefix}-layout';
.@{layout-prefix-cls} {
&-rtl {
direction: rtl;
}
}

View File

@ -1,4 +1,4 @@
import { inject, defineComponent, VNodeTypes, PropType } from 'vue';
import { inject, defineComponent, VNodeTypes, PropType, computed, ComputedRef } from 'vue';
import PropTypes from '../_util/vue-types';
import defaultLocaleData from './default';
import { Locale } from '.';
@ -30,39 +30,54 @@ export default defineComponent({
>,
},
},
setup() {
return {
localeData: inject<LocaleReceiverContext>('localeData', {}),
};
},
methods: {
getLocale() {
const { componentName = 'global', defaultLocale } = this;
setup(props, { slots }) {
const localeData = inject<LocaleReceiverContext>('localeData', {});
const locale = computed(() => {
const { componentName = 'global', defaultLocale } = props;
const locale =
defaultLocale || (defaultLocaleData as LocaleInterface)[componentName || 'global'];
const { antLocale } = this.localeData;
const { antLocale } = localeData;
const localeFromContext = componentName && antLocale ? antLocale[componentName] : {};
return {
...(typeof locale === 'function' ? locale() : locale),
...(localeFromContext || {}),
};
},
getLocaleCode() {
const { antLocale } = this.localeData;
});
const localeCode = computed(() => {
const { antLocale } = localeData;
const localeCode = antLocale && antLocale.locale;
// Had use LocaleProvide but didn't set locale
if (antLocale && antLocale.exist && !localeCode) {
return defaultLocaleData.locale;
}
return localeCode;
},
},
render() {
const { $slots } = this;
const children = this.children || $slots.default;
const { antLocale } = this.localeData;
return children?.(this.getLocale(), this.getLocaleCode(), antLocale);
});
return () => {
const children = props.children || slots.default;
const { antLocale } = localeData;
return children?.(locale.value, localeCode.value, antLocale);
};
},
});
type LocaleComponent = keyof Locale;
export function useLocaleReceiver<T extends LocaleComponent>(
componentName: T,
defaultLocale?: Locale[T] | Function,
): [ComputedRef<Locale[T]>] {
const localeData = inject<LocaleReceiverContext>('localeData', {} as LocaleReceiverContext);
const componentLocale = computed(() => {
const { antLocale } = localeData;
const locale =
defaultLocale || (defaultLocaleData as LocaleInterface)[componentName || 'global'];
const localeFromContext = componentName && antLocale ? antLocale[componentName] : {};
return {
...(typeof locale === 'function' ? (locale as Function)() : locale),
...(localeFromContext || {}),
};
});
return [componentLocale];
}

View File

@ -1,11 +1,11 @@
import { provide, App, defineComponent, VNode, PropType, reactive } from 'vue';
import { provide, App, defineComponent, VNode, PropType, reactive, watch, onUnmounted } from 'vue';
import PropTypes from '../_util/vue-types';
import moment from 'moment';
import interopDefault from '../_util/interopDefault';
import { ModalLocale, changeConfirmLocale } from '../modal/locale';
import warning from '../_util/warning';
import { getSlot } from '../_util/props-util';
import { withInstall } from '../_util/type';
import { ValidateMessages } from '../form/interface';
export interface Locale {
locale: string;
Pagination?: Object;
@ -18,6 +18,14 @@ export interface Locale {
Transfer?: Object;
Select?: Object;
Upload?: Object;
Form?: {
optional?: string;
defaultValidateMessages: ValidateMessages;
};
Image?: {
preview: string;
};
}
export interface LocaleProviderProps {
@ -44,7 +52,7 @@ const LocaleProvider = defineComponent({
},
ANT_MARK__: PropTypes.string,
},
setup(props) {
setup(props, { slots }) {
warning(
props.ANT_MARK__ === ANT_MARK,
'LocaleProvider',
@ -58,28 +66,25 @@ const LocaleProvider = defineComponent({
ANT_MARK__: ANT_MARK,
});
provide('localeData', state);
return { state };
},
watch: {
locale(val) {
this.state.antLocale = {
...val,
exist: true,
};
setMomentLocale(val);
changeConfirmLocale(val && val.Modal);
},
},
created() {
const { locale } = this;
setMomentLocale(locale);
changeConfirmLocale(locale && locale.Modal);
},
beforeUnmount() {
changeConfirmLocale();
},
render() {
return getSlot(this);
watch(
() => props.locale,
val => {
state.antLocale = {
...val,
exist: true,
};
setMomentLocale(val);
changeConfirmLocale(val && val.Modal);
},
{ immediate: true },
);
onUnmounted(() => {
changeConfirmLocale();
});
return () => {
return slots.default?.();
};
},
});

View File

@ -3,14 +3,13 @@ import DatePicker from '../date-picker/locale/en_US';
import TimePicker from '../time-picker/locale/en_US';
import Calendar from '../calendar/locale/en_US';
// import ColorPicker from '../color-picker/locale/en_US';
const typeTemplate = '${label} is not a valid ${type}';
export default {
locale: 'en',
Pagination,
DatePicker,
TimePicker,
Calendar,
// ColorPicker,
global: {
placeholder: 'Please select',
},
@ -18,11 +17,18 @@ export default {
filterTitle: 'Filter menu',
filterConfirm: 'OK',
filterReset: 'Reset',
filterEmptyText: 'No filters',
emptyText: 'No data',
selectAll: 'Select current page',
selectInvert: 'Invert current page',
selectNone: 'Clear all data',
selectionAll: 'Select all data',
sortTitle: 'Sort',
expand: 'Expand row',
collapse: 'Collapse row',
triggerDesc: 'Click to sort descending',
triggerAsc: 'Click to sort ascending',
cancelSort: 'Click to cancel sorting',
},
Modal: {
okText: 'OK',
@ -38,6 +44,12 @@ export default {
searchPlaceholder: 'Search here',
itemUnit: 'item',
itemsUnit: 'items',
remove: 'Remove',
selectCurrent: 'Select current page',
removeCurrent: 'Remove current page',
selectAll: 'Select all data',
removeAll: 'Remove all data',
selectInvert: 'Invert current page',
},
Upload: {
uploading: 'Uploading...',
@ -61,4 +73,57 @@ export default {
PageHeader: {
back: 'Back',
},
Form: {
optional: '(optional)',
defaultValidateMessages: {
default: 'Field validation error for ${label}',
required: 'Please enter ${label}',
enum: '${label} must be one of [${enum}]',
whitespace: '${label} cannot be a blank character',
date: {
format: '${label} date format is invalid',
parse: '${label} cannot be converted to a date',
invalid: '${label} is an invalid date',
},
types: {
string: typeTemplate,
method: typeTemplate,
array: typeTemplate,
object: typeTemplate,
number: typeTemplate,
date: typeTemplate,
boolean: typeTemplate,
integer: typeTemplate,
float: typeTemplate,
regexp: typeTemplate,
email: typeTemplate,
url: typeTemplate,
hex: typeTemplate,
},
string: {
len: '${label} must be ${len} characters',
min: '${label} must be at least ${min} characters',
max: '${label} must be up to ${max} characters',
range: '${label} must be between ${min}-${max} characters',
},
number: {
len: '${label} must be equal to ${len}',
min: '${label} must be minimum ${min}',
max: '${label} must be maximum ${max}',
range: '${label} must be between ${min}-${max}',
},
array: {
len: 'Must be ${len} ${label}',
min: 'At least ${min} ${label}',
max: 'At most ${max} ${label}',
range: 'The amount of ${label} must be between ${min}-${max}',
},
pattern: {
mismatch: '${label} does not match the pattern ${pattern}',
},
},
},
Image: {
preview: 'Preview',
},
};

View File

@ -1,66 +0,0 @@
import { defineComponent, inject } from 'vue';
import { Item, itemProps } from '../vc-menu';
import { getOptionProps, getSlot } from '../_util/props-util';
import Tooltip, { TooltipProps } from '../tooltip';
import { SiderContextProps } from '../layout/Sider';
import { injectExtraPropsKey } from '../vc-menu/FunctionProvider';
import PropTypes from '../_util/vue-types';
export default defineComponent({
name: 'MenuItem',
inheritAttrs: false,
props: {
...itemProps,
onClick: PropTypes.func,
},
isMenuItem: true,
setup() {
return {
getInlineCollapsed: inject<() => boolean>('getInlineCollapsed', () => false),
layoutSiderContext: inject<SiderContextProps>('layoutSiderContext', {}),
injectExtraProps: inject(injectExtraPropsKey, () => ({})),
};
},
methods: {
onKeyDown(e: HTMLElement) {
(this.$refs.menuItem as any).onKeyDown(e);
},
},
render() {
const props = getOptionProps(this);
const { level, title, rootPrefixCls } = { ...props, ...this.injectExtraProps } as any;
const { getInlineCollapsed, $attrs: attrs } = this;
const inlineCollapsed = getInlineCollapsed();
let tooltipTitle = title;
const children = getSlot(this);
if (typeof title === 'undefined') {
tooltipTitle = level === 1 ? children : '';
} else if (title === false) {
tooltipTitle = '';
}
const tooltipProps: TooltipProps = {
title: tooltipTitle,
};
const siderCollapsed = this.layoutSiderContext.sCollapsed;
if (!siderCollapsed && !inlineCollapsed) {
tooltipProps.title = null;
// Reset `visible` to fix control mode tooltip display not correct
// ref: https://github.com/ant-design/ant-design/issues/16742
tooltipProps.visible = false;
}
const itemProps = {
...props,
title,
...attrs,
ref: 'menuItem',
};
const toolTipProps: TooltipProps = {
...tooltipProps,
placement: 'right',
overlayClassName: `${rootPrefixCls}-inline-collapsed-tooltip`,
};
const item = <Item {...itemProps}>{children}</Item>;
return <Tooltip {...toolTipProps}>{item}</Tooltip>;
},
});

View File

@ -1,42 +0,0 @@
import { defineComponent, inject } from 'vue';
import { SubMenu as VcSubMenu } from '../vc-menu';
import classNames from '../_util/classNames';
import { injectExtraPropsKey } from '../vc-menu/FunctionProvider';
export type MenuTheme = 'light' | 'dark';
export interface MenuContextProps {
inlineCollapsed?: boolean;
theme?: MenuTheme;
}
export default defineComponent({
name: 'ASubMenu',
isSubMenu: true,
inheritAttrs: false,
props: { ...VcSubMenu.props },
setup() {
return {
menuPropsContext: inject<MenuContextProps>('menuPropsContext', {}),
injectExtraProps: inject(injectExtraPropsKey, () => ({})),
};
},
methods: {
onKeyDown(e: Event) {
(this.$refs.subMenu as any).onKeyDown(e);
},
},
render() {
const { $slots, $attrs } = this;
const { rootPrefixCls, popupClassName } = { ...this.$props, ...this.injectExtraProps } as any;
const { theme: antdMenuTheme } = this.menuPropsContext;
const props = {
...this.$props,
popupClassName: classNames(`${rootPrefixCls}-${antdMenuTheme}`, popupClassName),
ref: 'subMenu',
...$attrs,
} as any;
return <VcSubMenu {...props} v-slots={$slots}></VcSubMenu>;
},
});

View File

@ -1,327 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./antdv-demo/docs/menu/demo/horizontal.md correctly 1`] = `
<div>
<ul role="menu" class="ant-menu-light ant-menu-root ant-menu ant-menu-horizontal">
<li class="ant-menu-submenu ant-menu-submenu-horizontal ant-menu-overflowed-submenu" style="display: none;" role="menuitem">
<!---->
<!---->
<!---->
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span>···</span><i class="ant-menu-submenu-arrow"></i></div>
</li>
<!---->
<li role="menuitem" class="ant-menu-item ant-menu-item-selected"><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span>Navigation One
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-horizontal ant-menu-overflowed-submenu" style="display: none;" role="menuitem">
<!---->
<!---->
<!---->
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span>···</span><i class="ant-menu-submenu-arrow"></i></div>
</li>
<!---->
<li role="menuitem" aria-disabled="true" class="ant-menu-item ant-menu-item-disabled"><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span>Navigation Two
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-horizontal ant-menu-overflowed-submenu" style="display: none;" role="menuitem">
<!---->
<!---->
<!---->
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span>···</span><i class="ant-menu-submenu-arrow"></i></div>
</li>
<li class="ant-menu-submenu ant-menu-submenu-horizontal" role="menuitem">
<!---->
<!---->
<!---->
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span class="submenu-title-wrapper"><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span> Navigation Three - Submenu </span><i class="ant-menu-submenu-arrow"></i></div>
</li>
<li class="ant-menu-submenu ant-menu-submenu-horizontal ant-menu-overflowed-submenu" style="display: none;" role="menuitem">
<!---->
<!---->
<!---->
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span>···</span><i class="ant-menu-submenu-arrow"></i></div>
</li>
<!---->
<li role="menuitem" class="ant-menu-item"><a href="https://antdv.com" target="_blank" rel="noopener noreferrer"> Navigation Four - Link </a>
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-horizontal ant-menu-overflowed-submenu" style="visibility: hidden; position: absolute; display: none;" role="menuitem">
<!---->
<!---->
<!---->
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span>···</span><i class="ant-menu-submenu-arrow"></i></div>
</li>
</ul>
</div>
`;
exports[`renders ./antdv-demo/docs/menu/demo/inline.md correctly 1`] = `
<div>
<ul role="menu" class="ant-menu-light ant-menu-root ant-menu ant-menu-inline" style="width: 256px;" id="dddddd">
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open ant-menu-submenu-selected" role="menuitem">
<div aria-expanded="true" aria-owns="sub1$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span><span>Navigation One</span></span><i class="ant-menu-submenu-arrow"></i></div>
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="sub1$Menu">
<li class="ant-menu-item-group">
<div class="ant-menu-item-group-title"><span role="img" aria-label="qq" class="anticon anticon-qq"><svg class="" data-icon="qq" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M824.8 613.2c-16-51.4-34.4-94.6-62.7-165.3C766.5 262.2 689.3 112 511.5 112 331.7 112 256.2 265.2 261 447.9c-28.4 70.8-46.7 113.7-62.7 165.3-34 109.5-23 154.8-14.6 155.8 18 2.2 70.1-82.4 70.1-82.4 0 49 25.2 112.9 79.8 159-26.4 8.1-85.7 29.9-71.6 53.8 11.4 19.3 196.2 12.3 249.5 6.3 53.3 6 238.1 13 249.5-6.3 14.1-23.8-45.3-45.7-71.6-53.8 54.6-46.2 79.8-110.1 79.8-159 0 0 52.1 84.6 70.1 82.4 8.5-1.1 19.5-46.4-14.5-155.8z"></path></svg></span><span>Item 1</span></div>
<ul class="ant-menu-item-group-list">
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item ant-menu-item-selected">Option 1
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 2
<!---->
</li>
</ul>
</li>
<li class="ant-menu-item-group">
<div class="ant-menu-item-group-title" title="Item 2">Item 2</div>
<ul class="ant-menu-item-group-list">
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item"> Option 3
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item"> Option 4
<!---->
</li>
</ul>
</li>
</ul>
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Two</span></span><i class="ant-menu-submenu-arrow"></i></div>
<div></div>
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span><span>Navigation Three</span></span><i class="ant-menu-submenu-arrow"></i></div>
<div></div>
<!---->
</li>
</ul>
</div>
`;
exports[`renders ./antdv-demo/docs/menu/demo/inline-collapsed.md correctly 1`] = `
<div style="width: 256px;"><button style="margin-bottom: 16px;" class="ant-btn ant-btn-primary" type="button">
<!----><span role="img" aria-label="menu-fold" class="anticon anticon-menu-fold"><svg class="" data-icon="menu-fold" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 000 13.8z"></path></svg></span>
</button>
<ul role="menu" class="ant-menu-dark ant-menu-root ant-menu ant-menu-inline">
<!---->
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item ant-menu-item-selected"><span role="img" aria-label="pie-chart" class="anticon anticon-pie-chart"><svg class="" data-icon="pie-chart" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M864 518H506V160c0-4.4-3.6-8-8-8h-26a398.46 398.46 0 00-282.8 117.1 398.19 398.19 0 00-85.7 127.1A397.61 397.61 0 0072 552a398.46 398.46 0 00117.1 282.8c36.7 36.7 79.5 65.6 127.1 85.7A397.61 397.61 0 00472 952a398.46 398.46 0 00282.8-117.1c36.7-36.7 65.6-79.5 85.7-127.1A397.61 397.61 0 00872 552v-26c0-4.4-3.6-8-8-8zM705.7 787.8A331.59 331.59 0 01470.4 884c-88.1-.4-170.9-34.9-233.2-97.2C174.5 724.1 140 640.7 140 552c0-88.7 34.5-172.1 97.2-234.8 54.6-54.6 124.9-87.9 200.8-95.5V586h364.3c-7.7 76.3-41.3 147-96.6 201.8zM952 462.4l-2.6-28.2c-8.5-92.1-49.4-179-115.2-244.6A399.4 399.4 0 00589 74.6L560.7 72c-4.7-.4-8.7 3.2-8.7 7.9V464c0 4.4 3.6 8 8 8l384-1c4.7 0 8.4-4 8-8.6zm-332.2-58.2V147.6a332.24 332.24 0 01166.4 89.8c45.7 45.6 77 103.6 90 166.1l-256.4.7z"></path></svg></span><span>Option 1</span>
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item"><span role="img" aria-label="desktop" class="anticon anticon-desktop"><svg class="" data-icon="desktop" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 140H96c-17.7 0-32 14.3-32 32v496c0 17.7 14.3 32 32 32h380v112H304c-8.8 0-16 7.2-16 16v48c0 4.4 3.6 8 8 8h432c4.4 0 8-3.6 8-8v-48c0-8.8-7.2-16-16-16H548V700h380c17.7 0 32-14.3 32-32V172c0-17.7-14.3-32-32-32zm-40 488H136V212h752v416z"></path></svg></span><span>Option 2</span>
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item"><span role="img" aria-label="inbox" class="anticon anticon-inbox"><svg class="" data-icon="inbox" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="0 0 1024 1024" focusable="false"><path d="M885.2 446.3l-.2-.8-112.2-285.1c-5-16.1-19.9-27.2-36.8-27.2H281.2c-17 0-32.1 11.3-36.9 27.6L139.4 443l-.3.7-.2.8c-1.3 4.9-1.7 9.9-1 14.8-.1 1.6-.2 3.2-.2 4.8V830a60.9 60.9 0 0060.8 60.8h627.2c33.5 0 60.8-27.3 60.9-60.8V464.1c0-1.3 0-2.6-.1-3.7.4-4.9 0-9.6-1.3-14.1zm-295.8-43l-.3 15.7c-.8 44.9-31.8 75.1-77.1 75.1-22.1 0-41.1-7.1-54.8-20.6S436 441.2 435.6 419l-.3-15.7H229.5L309 210h399.2l81.7 193.3H589.4zm-375 76.8h157.3c24.3 57.1 76 90.8 140.4 90.8 33.7 0 65-9.4 90.3-27.2 22.2-15.6 39.5-37.4 50.7-63.6h156.5V814H214.4V480.1z"></path></svg></span><span>Option 3</span>
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open" role="menuitem">
<div aria-expanded="true" aria-owns="sub1$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span><span>Navigation One</span></span><i class="ant-menu-submenu-arrow"></i></div>
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="sub1$Menu">
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 5
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 6
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 7
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 8
<!---->
</li>
</ul>
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Two</span></span><i class="ant-menu-submenu-arrow"></i></div>
<div></div>
<!---->
</li>
</ul>
</div>
`;
exports[`renders ./antdv-demo/docs/menu/demo/sider-current.md correctly 1`] = `
<div>
<ul role="menu" class="ant-menu-light ant-menu-root ant-menu ant-menu-inline" style="width: 256px;">
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open" role="menuitem">
<div aria-expanded="true" aria-owns="sub1$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span><span>Navigation One</span></span><i class="ant-menu-submenu-arrow"></i></div>
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="sub1$Menu">
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 1
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 2
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 3
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 4
<!---->
</li>
</ul>
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Two</span></span><i class="ant-menu-submenu-arrow"></i></div>
<div></div>
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span><span>Navigation Three</span></span><i class="ant-menu-submenu-arrow"></i></div>
<div></div>
<!---->
</li>
</ul>
</div>
`;
exports[`renders ./antdv-demo/docs/menu/demo/switch-mode.md correctly 1`] = `
<div><button class="ant-switch" type="button" role="switch" aria-checked="false">
<!----><span class="ant-switch-inner"><!----></span>
</button> Change Mode <span class="ant-divider" style="margin: 0px 1em;"></span><button class="ant-switch" type="button" role="switch" aria-checked="false">
<!----><span class="ant-switch-inner"><!----></span>
</button> Change Theme <br><br>
<ul role="menu" class="ant-menu-light ant-menu-root ant-menu ant-menu-inline" style="width: 256px;">
<!---->
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item ant-menu-item-selected"><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span> Navigation One
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item"><span role="img" aria-label="calendar" class="anticon anticon-calendar"><svg class="" data-icon="calendar" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z"></path></svg></span> Navigation Two
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open" role="menuitem">
<div aria-expanded="true" aria-owns="sub1$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Three</span></span><i class="ant-menu-submenu-arrow"></i></div>
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="sub1$Menu">
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 3
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 4
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
<div aria-expanded="false" aria-haspopup="true" title="Submenu" style="padding-left: 48px;" class="ant-menu-submenu-title">Submenu<i class="ant-menu-submenu-arrow"></i></div>
<div></div>
<!---->
</li>
</ul>
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span><span>Navigation Four</span></span><i class="ant-menu-submenu-arrow"></i></div>
<div></div>
<!---->
</li>
</ul>
</div>
`;
exports[`renders ./antdv-demo/docs/menu/demo/template.md correctly 1`] = `
<div style="width: 256px;"><button style="margin-bottom: 16px;" class="ant-btn ant-btn-primary" type="button">
<!----><span role="img" aria-label="menu-fold" class="anticon anticon-menu-fold"><svg class="" data-icon="menu-fold" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 000 13.8z"></path></svg></span>
</button>
<ul role="menu" class="ant-menu-dark ant-menu-root ant-menu ant-menu-inline">
<!---->
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item ant-menu-item-selected"><span role="img" aria-label="pie-chart" class="anticon anticon-pie-chart"><svg class="" data-icon="pie-chart" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M864 518H506V160c0-4.4-3.6-8-8-8h-26a398.46 398.46 0 00-282.8 117.1 398.19 398.19 0 00-85.7 127.1A397.61 397.61 0 0072 552a398.46 398.46 0 00117.1 282.8c36.7 36.7 79.5 65.6 127.1 85.7A397.61 397.61 0 00472 952a398.46 398.46 0 00282.8-117.1c36.7-36.7 65.6-79.5 85.7-127.1A397.61 397.61 0 00872 552v-26c0-4.4-3.6-8-8-8zM705.7 787.8A331.59 331.59 0 01470.4 884c-88.1-.4-170.9-34.9-233.2-97.2C174.5 724.1 140 640.7 140 552c0-88.7 34.5-172.1 97.2-234.8 54.6-54.6 124.9-87.9 200.8-95.5V586h364.3c-7.7 76.3-41.3 147-96.6 201.8zM952 462.4l-2.6-28.2c-8.5-92.1-49.4-179-115.2-244.6A399.4 399.4 0 00589 74.6L560.7 72c-4.7-.4-8.7 3.2-8.7 7.9V464c0 4.4 3.6 8 8 8l384-1c4.7 0 8.4-4 8-8.6zm-332.2-58.2V147.6a332.24 332.24 0 01166.4 89.8c45.7 45.6 77 103.6 90 166.1l-256.4.7z"></path></svg></span><span>Option 1</span>
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open" role="menuitem">
<div aria-expanded="true" aria-owns="2$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span><span>Navigation 2</span></span><i class="ant-menu-submenu-arrow"></i></div>
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="2$Menu">
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 48px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span><span>Navigation 3</span></span><i class="ant-menu-submenu-arrow"></i></div>
<div></div>
<!---->
</li>
</ul>
<!---->
</li>
</ul>
</div>
`;
exports[`renders ./antdv-demo/docs/menu/demo/theme.md correctly 1`] = `
<div><button class="ant-switch ant-switch-checked" type="button" role="switch" aria-checked="true">
<!----><span class="ant-switch-inner">dark</span>
</button><br><br>
<ul role="menu" class="ant-menu-dark ant-menu-root ant-menu ant-menu-inline" style="width: 256px;">
<!---->
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item ant-menu-item-selected"><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span> Navigation One
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item"><span role="img" aria-label="calendar" class="anticon anticon-calendar"><svg class="" data-icon="calendar" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z"></path></svg></span> Navigation Two
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open" role="menuitem">
<div aria-expanded="true" aria-owns="sub1$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Three</span></span><i class="ant-menu-submenu-arrow"></i></div>
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="sub1$Menu">
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 3
<!---->
</li>
<!---->
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 4
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
<div aria-expanded="false" aria-haspopup="true" title="Submenu" style="padding-left: 48px;" class="ant-menu-submenu-title">Submenu<i class="ant-menu-submenu-arrow"></i></div>
<div></div>
<!---->
</li>
</ul>
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span><span>Navigation Four</span></span><i class="ant-menu-submenu-arrow"></i></div>
<div></div>
<!---->
</li>
</ul>
</div>
`;
exports[`renders ./antdv-demo/docs/menu/demo/vertical.md correctly 1`] = `
<div>
<ul role="menu" class="ant-menu-light ant-menu-root ant-menu ant-menu-vertical" style="width: 256px;">
<!---->
<li role="menuitem" class="ant-menu-item"><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span> Navigation One
<!---->
</li>
<!---->
<li role="menuitem" class="ant-menu-item"><span role="img" aria-label="calendar" class="anticon anticon-calendar"><svg class="" data-icon="calendar" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z"></path></svg></span> Navigation Two
<!---->
</li>
<li class="ant-menu-submenu ant-menu-submenu-vertical" role="menuitem">
<!---->
<!---->
<!---->
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Three</span></span><i class="ant-menu-submenu-arrow"></i></div>
</li>
<li class="ant-menu-submenu ant-menu-submenu-vertical" role="menuitem">
<!---->
<!---->
<!---->
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span><span>Navigation Four</span></span><i class="ant-menu-submenu-arrow"></i></div>
</li>
</ul>
</div>
`;

View File

@ -1,322 +1,39 @@
import { defineComponent, inject, provide, toRef, App, ExtractPropTypes, Plugin } from 'vue';
import omit from 'omit.js';
import VcMenu, { Divider, ItemGroup } from '../vc-menu';
import SubMenu from './SubMenu';
import PropTypes from '../_util/vue-types';
import animation from '../_util/openAnimation';
import warning from '../_util/warning';
import Item from './MenuItem';
import { hasProp, getOptionProps } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
import commonPropsType from '../vc-menu/commonPropsType';
import { defaultConfigProvider } from '../config-provider';
import { SiderContextProps } from '../layout/Sider';
import { tuple } from '../_util/type';
// import raf from '../_util/raf';
export const MenuMode = PropTypes.oneOf([
'vertical',
'vertical-left',
'vertical-right',
'horizontal',
'inline',
]);
export const menuProps = {
...commonPropsType,
theme: PropTypes.oneOf(tuple('light', 'dark')).def('light'),
mode: MenuMode.def('vertical'),
selectable: PropTypes.looseBool,
selectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
defaultSelectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
openKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
defaultOpenKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
openTransitionName: PropTypes.string,
prefixCls: PropTypes.string,
multiple: PropTypes.looseBool,
inlineIndent: PropTypes.number.def(24),
inlineCollapsed: PropTypes.looseBool,
isRootMenu: PropTypes.looseBool.def(true),
focusable: PropTypes.looseBool.def(false),
onOpenChange: PropTypes.func,
onSelect: PropTypes.func,
onDeselect: PropTypes.func,
onClick: PropTypes.func,
onMouseenter: PropTypes.func,
onSelectChange: PropTypes.func,
};
export type MenuProps = Partial<ExtractPropTypes<typeof menuProps>>;
const Menu = defineComponent({
name: 'AMenu',
mixins: [BaseMixin],
inheritAttrs: false,
props: menuProps,
Divider: { ...Divider, name: 'AMenuDivider' },
Item: { ...Item, name: 'AMenuItem' },
SubMenu: { ...SubMenu, name: 'ASubMenu' },
ItemGroup: { ...ItemGroup, name: 'AMenuItemGroup' },
emits: [
'update:selectedKeys',
'update:openKeys',
'mouseenter',
'openChange',
'click',
'selectChange',
'select',
'deselect',
],
setup() {
const layoutSiderContext = inject<SiderContextProps>('layoutSiderContext', {});
const layoutSiderCollapsed = toRef(layoutSiderContext, 'sCollapsed');
return {
configProvider: inject('configProvider', defaultConfigProvider),
layoutSiderContext,
layoutSiderCollapsed,
propsUpdating: false,
switchingModeFromInline: false,
leaveAnimationExecutedWhenInlineCollapsed: false,
inlineOpenKeys: [],
};
},
data() {
const props: MenuProps = getOptionProps(this);
warning(
!('inlineCollapsed' in props && props.mode !== 'inline'),
'Menu',
"`inlineCollapsed` should only be used when Menu's `mode` is inline.",
);
let sOpenKeys: (number | string)[];
if ('openKeys' in props) {
sOpenKeys = props.openKeys;
} else if ('defaultOpenKeys' in props) {
sOpenKeys = props.defaultOpenKeys;
}
return {
sOpenKeys,
};
},
// beforeUnmount() {
// raf.cancel(this.mountRafId);
// },
watch: {
mode(val, oldVal) {
if (oldVal === 'inline' && val !== 'inline') {
this.switchingModeFromInline = true;
}
},
openKeys(val) {
this.setState({ sOpenKeys: val });
},
inlineCollapsed(val) {
this.collapsedChange(val);
},
layoutSiderCollapsed(val) {
this.collapsedChange(val);
},
},
created() {
provide('getInlineCollapsed', this.getInlineCollapsed);
provide('menuPropsContext', this.$props);
},
updated() {
this.propsUpdating = false;
},
methods: {
collapsedChange(val: unknown) {
if (this.propsUpdating) {
return;
}
this.propsUpdating = true;
if (!hasProp(this, 'openKeys')) {
if (val) {
this.switchingModeFromInline = true;
this.inlineOpenKeys = this.sOpenKeys;
this.setState({ sOpenKeys: [] });
} else {
this.setState({ sOpenKeys: this.inlineOpenKeys });
this.inlineOpenKeys = [];
}
} else if (val) {
// openKeysreactopenKeysvue便openKeys
this.switchingModeFromInline = true;
}
},
restoreModeVerticalFromInline() {
if (this.switchingModeFromInline) {
this.switchingModeFromInline = false;
this.$forceUpdate();
}
},
// Restore vertical mode when menu is collapsed responsively when mounted
// https://github.com/ant-design/ant-design/issues/13104
// TODO: not a perfect solution, looking a new way to avoid setting switchingModeFromInline in this situation
handleMouseEnter(e: Event) {
this.restoreModeVerticalFromInline();
this.$emit('mouseenter', e);
},
handleTransitionEnd(e: TransitionEvent) {
// when inlineCollapsed menu width animation finished
// https://github.com/ant-design/ant-design/issues/12864
const widthCollapsed = e.propertyName === 'width' && e.target === e.currentTarget;
// Fix SVGElement e.target.className.indexOf is not a function
// https://github.com/ant-design/ant-design/issues/15699
const { className } = e.target as SVGAnimationElement | HTMLElement;
// SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during an animation.
const classNameValue =
Object.prototype.toString.call(className) === '[object SVGAnimatedString]'
? className.animVal
: className;
// Fix for <Menu style={{ width: '100%' }} />, the width transition won't trigger when menu is collapsed
// https://github.com/ant-design/ant-design-pro/issues/2783
const iconScaled = e.propertyName === 'font-size' && classNameValue.indexOf('anticon') >= 0;
if (widthCollapsed || iconScaled) {
this.restoreModeVerticalFromInline();
}
},
handleClick(e: Event) {
this.handleOpenChange([]);
this.$emit('click', e);
},
handleSelect(info) {
this.$emit('update:selectedKeys', info.selectedKeys);
this.$emit('select', info);
this.$emit('selectChange', info.selectedKeys);
},
handleDeselect(info) {
this.$emit('update:selectedKeys', info.selectedKeys);
this.$emit('deselect', info);
this.$emit('selectChange', info.selectedKeys);
},
handleOpenChange(openKeys: (number | string)[]) {
this.setOpenKeys(openKeys);
this.$emit('update:openKeys', openKeys);
this.$emit('openChange', openKeys);
},
setOpenKeys(openKeys: (number | string)[]) {
if (!hasProp(this, 'openKeys')) {
this.setState({ sOpenKeys: openKeys });
}
},
getRealMenuMode() {
const inlineCollapsed = this.getInlineCollapsed();
if (this.switchingModeFromInline && inlineCollapsed) {
return 'inline';
}
const { mode } = this.$props;
return inlineCollapsed ? 'vertical' : mode;
},
getInlineCollapsed() {
const { inlineCollapsed } = this.$props;
if (this.layoutSiderContext.sCollapsed !== undefined) {
return this.layoutSiderContext.sCollapsed;
}
return inlineCollapsed;
},
getMenuOpenAnimation(menuMode: string) {
const { openAnimation, openTransitionName } = this.$props;
let menuOpenAnimation = openAnimation || openTransitionName;
if (openAnimation === undefined && openTransitionName === undefined) {
if (menuMode === 'horizontal') {
menuOpenAnimation = 'slide-up';
} else if (menuMode === 'inline') {
menuOpenAnimation = animation;
} else {
// When mode switch from inline
// submenu should hide without animation
if (this.switchingModeFromInline) {
menuOpenAnimation = '';
this.switchingModeFromInline = false;
} else {
menuOpenAnimation = 'zoom-big';
}
}
}
return menuOpenAnimation;
},
},
render() {
const { layoutSiderContext } = this;
const { collapsedWidth } = layoutSiderContext;
const { getPopupContainer: getContextPopupContainer } = this.configProvider;
const props = getOptionProps(this);
const { prefixCls: customizePrefixCls, theme, getPopupContainer } = props;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('menu', customizePrefixCls);
const menuMode = this.getRealMenuMode();
const menuOpenAnimation = this.getMenuOpenAnimation(menuMode);
const { class: className, ...otherAttrs } = this.$attrs;
const menuClassName = {
[className as string]: className,
[`${prefixCls}-${theme}`]: true,
[`${prefixCls}-inline-collapsed`]: this.getInlineCollapsed(),
};
const menuProps = {
...omit(props, [
'inlineCollapsed',
'onUpdate:selectedKeys',
'onUpdate:openKeys',
'onSelectChange',
]),
getPopupContainer: getPopupContainer || getContextPopupContainer,
openKeys: this.sOpenKeys,
mode: menuMode,
prefixCls,
...otherAttrs,
onSelect: this.handleSelect,
onDeselect: this.handleDeselect,
onOpenChange: this.handleOpenChange,
onMouseenter: this.handleMouseEnter,
onTransitionend: this.handleTransitionEnd,
// children: getSlot(this),
};
if (!hasProp(this, 'selectedKeys')) {
delete menuProps.selectedKeys;
}
if (menuMode !== 'inline') {
// closing vertical popup submenu after click it
menuProps.onClick = this.handleClick;
menuProps.openTransitionName = menuOpenAnimation;
} else {
menuProps.onClick = (e: Event) => {
this.$emit('click', e);
};
menuProps.openAnimation = menuOpenAnimation;
}
// https://github.com/ant-design/ant-design/issues/8587
const hideMenu =
this.getInlineCollapsed() &&
(collapsedWidth === 0 || collapsedWidth === '0' || collapsedWidth === '0px');
if (hideMenu) {
menuProps.openKeys = [];
}
return <VcMenu {...menuProps} class={menuClassName} v-slots={this.$slots} />;
},
});
import Menu, { MenuProps } from './src/Menu';
import MenuItem, { MenuItemProps } from './src/MenuItem';
import SubMenu, { SubMenuProps } from './src/SubMenu';
import ItemGroup, { MenuItemGroupProps } from './src/ItemGroup';
import Divider from './src/Divider';
import { App, Plugin } from 'vue';
/* istanbul ignore next */
Menu.install = function(app: App) {
app.component(Menu.name, Menu);
app.component(Menu.Item.name, Menu.Item);
app.component(Menu.SubMenu.name, Menu.SubMenu);
app.component(Menu.Divider.name, Menu.Divider);
app.component(Menu.ItemGroup.name, Menu.ItemGroup);
app.component(MenuItem.name, MenuItem);
app.component(SubMenu.name, SubMenu);
app.component(Divider.name, Divider);
app.component(ItemGroup.name, ItemGroup);
return app;
};
Menu.Item = MenuItem;
Menu.Divider = Divider;
Menu.SubMenu = SubMenu;
Menu.ItemGroup = ItemGroup;
export {
SubMenu,
MenuItem as Item,
MenuItem,
ItemGroup,
Divider,
MenuProps,
SubMenuProps,
MenuItemProps,
MenuItemGroupProps,
};
export default Menu as typeof Menu &
Plugin & {
readonly Item: typeof Item;
readonly Item: typeof MenuItem;
readonly SubMenu: typeof SubMenu;
readonly Divider: typeof Divider;
readonly ItemGroup: typeof ItemGroup;

View File

@ -0,0 +1,13 @@
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Divider',
props: {
prefixCls: String,
},
setup(props) {
return () => {
return <li class={`${props.prefixCls}-item-divider`} />;
};
},
});

View File

@ -0,0 +1,66 @@
import { computed, defineComponent, ref, watch } from '@vue/runtime-core';
import Transition from '../../_util/transition';
import { useInjectMenu, MenuContextProvider } from './hooks/useMenuContext';
import SubMenuList from './SubMenuList';
export default defineComponent({
name: 'InlineSubMenuList',
inheritAttrs: false,
props: {
id: String,
open: Boolean,
keyPath: Array,
},
setup(props, { slots }) {
const fixedMode = computed(() => 'inline');
const { motion, mode, defaultMotions } = useInjectMenu();
const sameModeRef = computed(() => mode.value === fixedMode.value);
const destroy = ref(!sameModeRef.value);
const mergedOpen = computed(() => (sameModeRef.value ? props.open : false));
// ================================= Effect =================================
// Reset destroy state when mode change back
watch(
mode,
() => {
if (sameModeRef.value) {
destroy.value = false;
}
},
{ flush: 'post' },
);
const style = ref({});
const className = ref('');
const mergedMotion = computed(() => {
const m =
motion.value || defaultMotions.value?.[fixedMode.value] || defaultMotions.value?.other;
const res = typeof m === 'function' ? m(style, className) : m;
return { ...res, appear: props.keyPath.length <= 1 };
});
return () => {
if (destroy.value) {
return null;
}
return (
<MenuContextProvider
props={{
mode: fixedMode,
locked: !sameModeRef.value,
}}
>
<Transition {...mergedMotion.value}>
<SubMenuList
v-show={mergedOpen.value}
id={props.id}
style={style.value}
class={className.value}
>
{slots.default?.()}
</SubMenuList>
</Transition>
</MenuContextProvider>
);
};
},
});

View File

@ -0,0 +1,34 @@
import { getPropsSlot } from '../../_util/props-util';
import { computed, defineComponent, ExtractPropTypes } from 'vue';
import PropTypes from '../../_util/vue-types';
import { useInjectMenu } from './hooks/useMenuContext';
const menuItemGroupProps = {
title: PropTypes.VNodeChild,
};
export type MenuItemGroupProps = Partial<ExtractPropTypes<typeof menuItemGroupProps>>;
export default defineComponent({
name: 'AMenuItemGroup',
inheritAttrs: false,
props: menuItemGroupProps,
slots: ['title'],
setup(props, { slots, attrs }) {
const { prefixCls } = useInjectMenu();
const groupPrefixCls = computed(() => `${prefixCls.value}-item-group`);
return () => {
return (
<li {...attrs} onClick={e => e.stopPropagation()} class={groupPrefixCls.value}>
<div
title={typeof props.title === 'string' ? props.title : undefined}
class={`${groupPrefixCls.value}-title`}
>
{getPropsSlot(slots, props, 'title')}
</div>
<ul class={`${groupPrefixCls.value}-list`}>{slots.default?.()}</ul>
</li>
);
};
},
});

View File

@ -0,0 +1,448 @@
import { Key } from '../../_util/type';
import {
computed,
defineComponent,
ExtractPropTypes,
ref,
PropType,
inject,
watchEffect,
watch,
onMounted,
unref,
UnwrapRef,
} from 'vue';
import shallowEqual from '../../_util/shallowequal';
import useProvideMenu, {
MenuContextProvider,
StoreMenuInfo,
useProvideFirstLevel,
} from './hooks/useMenuContext';
import useConfigInject from '../../_util/hooks/useConfigInject';
import {
MenuTheme,
MenuMode,
BuiltinPlacements,
TriggerSubMenuAction,
MenuInfo,
SelectInfo,
} from './interface';
import devWarning from '../../vc-util/devWarning';
import { collapseMotion, CSSMotionProps } from '../../_util/transition';
import uniq from 'lodash-es/uniq';
import { SiderCollapsedKey } from '../../layout/injectionKey';
import { flattenChildren } from '../../_util/props-util';
import Overflow from '../../vc-overflow';
import MenuItem from './MenuItem';
import SubMenu from './SubMenu';
import EllipsisOutlined from '@ant-design/icons-vue/EllipsisOutlined';
export const menuProps = {
prefixCls: String,
disabled: Boolean,
inlineCollapsed: Boolean,
disabledOverflow: Boolean,
openKeys: Array,
selectedKeys: Array,
activeKey: String, // 使
selectable: { type: Boolean, default: true },
multiple: { type: Boolean, default: false },
motion: Object as PropType<CSSMotionProps>,
theme: { type: String as PropType<MenuTheme>, default: 'light' },
mode: { type: String as PropType<MenuMode>, default: 'vertical' },
inlineIndent: { type: Number, default: 24 },
subMenuOpenDelay: { type: Number, default: 0.1 },
subMenuCloseDelay: { type: Number, default: 0.1 },
builtinPlacements: { type: Object as PropType<BuiltinPlacements> },
triggerSubMenuAction: { type: String as PropType<TriggerSubMenuAction>, default: 'hover' },
getPopupContainer: Function as PropType<(node: HTMLElement) => HTMLElement>,
};
export type MenuProps = Partial<ExtractPropTypes<typeof menuProps>>;
const EMPTY_LIST: string[] = [];
export default defineComponent({
name: 'AMenu',
props: menuProps,
emits: [
'update:openKeys',
'openChange',
'select',
'deselect',
'update:selectedKeys',
'click',
'update:activeKey',
],
setup(props, { slots, emit }) {
const { prefixCls, direction } = useConfigInject('menu', props);
const store = ref<Record<string, StoreMenuInfo>>({});
const siderCollapsed = inject(SiderCollapsedKey, ref(undefined));
const inlineCollapsed = computed(() => {
if (siderCollapsed.value !== undefined) {
return siderCollapsed.value;
}
return props.inlineCollapsed;
});
const isMounted = ref(false);
onMounted(() => {
isMounted.value = true;
});
watchEffect(() => {
devWarning(
!(props.inlineCollapsed === true && props.mode !== 'inline'),
'Menu',
'`inlineCollapsed` should only be used when `mode` is inline.',
);
devWarning(
!(siderCollapsed.value !== undefined && props.inlineCollapsed === true),
'Menu',
'`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.',
);
});
const activeKeys = ref([]);
const mergedSelectedKeys = ref([]);
const keyMapStore = ref({});
watch(
store,
() => {
const newKeyMapStore = {};
for (const menuInfo of Object.values(store.value)) {
newKeyMapStore[menuInfo.key] = menuInfo;
}
keyMapStore.value = newKeyMapStore;
},
{ immediate: true },
);
watchEffect(() => {
if ('activeKey' in props) {
let keys = [];
const menuInfo = props.activeKey
? (keyMapStore.value[props.activeKey] as UnwrapRef<StoreMenuInfo>)
: undefined;
if (menuInfo && props.activeKey !== undefined) {
keys = [...menuInfo.parentKeys, props.activeKey];
} else {
keys = [];
}
if (!shallowEqual(activeKeys.value, keys)) {
activeKeys.value = keys;
}
}
});
watch(
() => props.selectedKeys,
selectedKeys => {
mergedSelectedKeys.value = selectedKeys || mergedSelectedKeys.value;
},
{ immediate: true },
);
const selectedSubMenuEventKeys = ref([]);
watch(
[keyMapStore, mergedSelectedKeys],
() => {
let subMenuParentEventKeys = [];
mergedSelectedKeys.value.forEach(key => {
const menuInfo = keyMapStore.value[key];
if (menuInfo) {
subMenuParentEventKeys.push(...unref(menuInfo.parentEventKeys));
}
});
subMenuParentEventKeys = uniq(subMenuParentEventKeys);
if (!shallowEqual(selectedSubMenuEventKeys.value, subMenuParentEventKeys)) {
selectedSubMenuEventKeys.value = subMenuParentEventKeys;
}
},
{ immediate: true },
);
// >>>>> Trigger select
const triggerSelection = (info: MenuInfo) => {
if (!props.selectable) {
return;
}
// Insert or Remove
const { key: targetKey } = info;
const exist = mergedSelectedKeys.value.includes(targetKey);
let newSelectedKeys: Key[];
if (props.multiple) {
if (exist) {
newSelectedKeys = mergedSelectedKeys.value.filter(key => key !== targetKey);
} else {
newSelectedKeys = [...mergedSelectedKeys.value, targetKey];
}
} else {
newSelectedKeys = [targetKey];
}
// Trigger event
const selectInfo: SelectInfo = {
...info,
selectedKeys: newSelectedKeys,
};
if (!shallowEqual(newSelectedKeys, mergedSelectedKeys.value)) {
if (!('selectedKeys' in props)) {
mergedSelectedKeys.value = newSelectedKeys;
}
emit('update:selectedKeys', newSelectedKeys);
if (exist && props.multiple) {
emit('deselect', selectInfo);
} else {
emit('select', selectInfo);
}
}
if (mergedMode.value !== 'inline' && !props.multiple && mergedOpenKeys.value.length) {
triggerOpenKeys(EMPTY_LIST);
}
};
const mergedOpenKeys = ref([]);
watch(
() => props.openKeys,
(openKeys = mergedOpenKeys.value) => {
if (!shallowEqual(mergedOpenKeys.value, openKeys)) {
mergedOpenKeys.value = openKeys;
}
},
{ immediate: true },
);
const changeActiveKeys = (keys: Key[]) => {
if ('activeKey' in props) {
emit('update:activeKey', keys[keys.length - 1]);
} else {
activeKeys.value = keys;
}
};
const disabled = computed(() => !!props.disabled);
const isRtl = computed(() => direction.value === 'rtl');
const mergedMode = ref<MenuMode>('vertical');
const mergedInlineCollapsed = ref(false);
watchEffect(() => {
if ((props.mode === 'inline' || props.mode === 'vertical') && inlineCollapsed.value) {
mergedMode.value = 'vertical';
mergedInlineCollapsed.value = inlineCollapsed.value;
} else {
mergedMode.value = props.mode;
mergedInlineCollapsed.value = false;
}
});
const isInlineMode = computed(() => mergedMode.value === 'inline');
const triggerOpenKeys = (keys: string[]) => {
mergedOpenKeys.value = keys;
emit('update:openKeys', keys);
emit('openChange', keys);
};
// >>>>> Cache & Reset open keys when inlineCollapsed changed
const inlineCacheOpenKeys = ref(mergedOpenKeys.value);
const mountRef = ref(false);
// Cache
watch(
mergedOpenKeys,
() => {
if (isInlineMode.value) {
inlineCacheOpenKeys.value = mergedOpenKeys.value;
}
},
{ immediate: true },
);
// Restore
watch(
isInlineMode,
() => {
if (!mountRef.value) {
mountRef.value = true;
return;
}
if (isInlineMode.value) {
mergedOpenKeys.value = inlineCacheOpenKeys.value;
} else {
// Trigger open event in case its in control
triggerOpenKeys(EMPTY_LIST);
}
},
{ immediate: true },
);
const className = computed(() => {
return {
[`${prefixCls.value}`]: true,
[`${prefixCls.value}-root`]: true,
[`${prefixCls.value}-${mergedMode.value}`]: true,
[`${prefixCls.value}-inline-collapsed`]: mergedInlineCollapsed.value,
[`${prefixCls.value}-rtl`]: isRtl.value,
[`${prefixCls.value}-${props.theme}`]: true,
};
});
const defaultMotions = {
horizontal: { name: `ant-slide-up` },
inline: collapseMotion,
other: { name: `ant-zoom-big` },
};
useProvideFirstLevel(true);
const getChildrenKeys = (eventKeys: string[] = []): Key[] => {
const keys = [];
const storeValue = store.value;
eventKeys.forEach(eventKey => {
const { key, childrenEventKeys } = storeValue[eventKey];
keys.push(key, ...getChildrenKeys(childrenEventKeys));
});
return keys;
};
// ========================= Open =========================
/**
* Click for item. SubMenu do not have selection status
*/
const onInternalClick = (info: MenuInfo) => {
emit('click', info);
triggerSelection(info);
};
const onInternalOpenChange = (eventKey: Key, open: boolean) => {
const { key, childrenEventKeys } = store.value[eventKey];
let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key);
if (open) {
newOpenKeys.push(key);
} else if (mergedMode.value !== 'inline') {
// We need find all related popup to close
const subPathKeys = getChildrenKeys(childrenEventKeys);
newOpenKeys = newOpenKeys.filter(k => !subPathKeys.includes(k));
}
if (!shallowEqual(mergedOpenKeys, newOpenKeys)) {
triggerOpenKeys(newOpenKeys);
}
};
const registerMenuInfo = (key: string, info: StoreMenuInfo) => {
store.value = { ...store.value, [key]: info as any };
};
const unRegisterMenuInfo = (key: string) => {
delete store.value[key];
store.value = { ...store.value };
};
const lastVisibleIndex = ref(0);
useProvideMenu({
store,
prefixCls,
activeKeys,
openKeys: mergedOpenKeys,
selectedKeys: mergedSelectedKeys,
changeActiveKeys,
disabled,
rtl: isRtl,
mode: mergedMode,
inlineIndent: computed(() => props.inlineIndent),
subMenuCloseDelay: computed(() => props.subMenuCloseDelay),
subMenuOpenDelay: computed(() => props.subMenuOpenDelay),
builtinPlacements: computed(() => props.builtinPlacements),
triggerSubMenuAction: computed(() => props.triggerSubMenuAction),
getPopupContainer: computed(() => props.getPopupContainer),
inlineCollapsed: mergedInlineCollapsed,
antdMenuTheme: computed(() => props.theme),
siderCollapsed,
defaultMotions: computed(() => (isMounted.value ? defaultMotions : null)),
motion: computed(() => (isMounted.value ? props.motion : null)),
overflowDisabled: computed(() => props.disabledOverflow),
onOpenChange: onInternalOpenChange,
onItemClick: onInternalClick,
registerMenuInfo,
unRegisterMenuInfo,
selectedSubMenuEventKeys,
isRootMenu: true,
});
return () => {
const childList = flattenChildren(slots.default?.());
const allVisible =
lastVisibleIndex.value >= childList.length - 1 ||
mergedMode.value !== 'horizontal' ||
props.disabledOverflow;
// >>>>> Children
const wrappedChildList =
mergedMode.value !== 'horizontal' || props.disabledOverflow
? childList
: // Need wrap for overflow dropdown that do not response for open
childList.map((child, index) => (
// Always wrap provider to avoid sub node re-mount
<MenuContextProvider
key={child.key}
props={{ overflowDisabled: computed(() => index > lastVisibleIndex.value) }}
>
{child}
</MenuContextProvider>
));
const overflowedIndicator = <EllipsisOutlined />;
// data-hack-store-update vue bughack
return (
<Overflow
data-hack-store-update={store.value}
prefixCls={`${prefixCls.value}-overflow`}
component="ul"
itemComponent={MenuItem}
class={className.value}
role="menu"
data={wrappedChildList}
renderRawItem={node => node}
renderRawRest={omitItems => {
// We use origin list since wrapped list use context to prevent open
const len = omitItems.length;
const originOmitItems = len ? childList.slice(-len) : null;
return (
<SubMenu
eventKey={Overflow.OVERFLOW_KEY}
title={overflowedIndicator}
disabled={allVisible}
internalPopupClose={len === 0}
>
{originOmitItems}
</SubMenu>
);
}}
maxCount={
mergedMode.value !== 'horizontal' || props.disabledOverflow
? Overflow.INVALIDATE
: Overflow.RESPONSIVE
}
ssr="full"
data-menu-list
onVisibleChange={newLastIndex => {
lastVisibleIndex.value = newLastIndex;
}}
/>
);
};
},
});

View File

@ -0,0 +1,237 @@
import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props-util';
import PropTypes from '../../_util/vue-types';
import {
computed,
defineComponent,
ExtractPropTypes,
getCurrentInstance,
onBeforeUnmount,
ref,
watch,
} from 'vue';
import { useInjectKeyPath } from './hooks/useKeyPath';
import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext';
import { cloneElement } from '../../_util/vnode';
import Tooltip from '../../tooltip';
import { MenuInfo } from './interface';
import KeyCode from '../../_util/KeyCode';
import useDirectionStyle from './hooks/useDirectionStyle';
import Overflow from '../../vc-overflow';
let indexGuid = 0;
const menuItemProps = {
role: String,
disabled: Boolean,
danger: Boolean,
title: { type: [String, Boolean], default: undefined },
icon: PropTypes.VNodeChild,
};
export type MenuItemProps = Partial<ExtractPropTypes<typeof menuItemProps>>;
export default defineComponent({
name: 'AMenuItem',
inheritAttrs: false,
props: menuItemProps,
emits: ['mouseenter', 'mouseleave', 'click', 'keydown', 'focus'],
slots: ['icon', 'title'],
setup(props, { slots, emit, attrs }) {
const instance = getCurrentInstance();
const key = instance.vnode.key;
const eventKey = `menu_item_${++indexGuid}_$$_${key}`;
const { parentEventKeys, parentKeys } = useInjectKeyPath();
const {
prefixCls,
activeKeys,
disabled,
changeActiveKeys,
rtl,
inlineCollapsed,
siderCollapsed,
onItemClick,
selectedKeys,
registerMenuInfo,
unRegisterMenuInfo,
} = useInjectMenu();
const firstLevel = useInjectFirstLevel();
const isActive = ref(false);
const keysPath = computed(() => {
return [...parentKeys.value, key];
});
// const keysPath = computed(() => [...parentEventKeys.value, eventKey]);
const menuInfo = {
eventKey,
key,
parentEventKeys,
parentKeys,
isLeaf: true,
};
registerMenuInfo(eventKey, menuInfo);
onBeforeUnmount(() => {
unRegisterMenuInfo(eventKey);
});
watch(
activeKeys,
() => {
isActive.value = !!activeKeys.value.find(val => val === key);
},
{ immediate: true },
);
const mergedDisabled = computed(() => disabled.value || props.disabled);
const selected = computed(() => selectedKeys.value.includes(key));
const classNames = computed(() => {
const itemCls = `${prefixCls.value}-item`;
return {
[`${itemCls}`]: true,
[`${itemCls}-danger`]: props.danger,
[`${itemCls}-active`]: isActive.value,
[`${itemCls}-selected`]: selected.value,
[`${itemCls}-disabled`]: mergedDisabled.value,
};
});
const getEventInfo = (e: MouseEvent | KeyboardEvent): MenuInfo => {
return {
key,
eventKey,
keyPath: keysPath.value,
eventKeyPath: [...parentEventKeys.value, eventKey],
domEvent: e,
};
};
// ============================ Events ============================
const onInternalClick = (e: MouseEvent) => {
if (mergedDisabled.value) {
return;
}
const info = getEventInfo(e);
emit('click', e);
onItemClick(info);
};
const onMouseEnter = (event: MouseEvent) => {
if (!mergedDisabled.value) {
changeActiveKeys(keysPath.value);
emit('mouseenter', event);
}
};
const onMouseLeave = (event: MouseEvent) => {
if (!mergedDisabled.value) {
changeActiveKeys([]);
emit('mouseleave', event);
}
};
const onInternalKeyDown = (e: KeyboardEvent) => {
emit('keydown', e);
if (e.which === KeyCode.ENTER) {
const info = getEventInfo(e);
// Legacy. Key will also trigger click event
emit('click', e);
onItemClick(info);
}
};
/**
* Used for accessibility. Helper will focus element without key board.
* We should manually trigger an active
*/
const onInternalFocus = (e: FocusEvent) => {
changeActiveKeys(keysPath.value);
emit('focus', e);
};
const renderItemChildren = (icon: any, children: any) => {
const wrapNode = <span class={`${prefixCls.value}-title-content`}>{children}</span>;
// inline-collapsed.md demo span , icon span
// ref: https://github.com/ant-design/ant-design/pull/23456
if (!icon || (isValidElement(children) && children.type === 'span')) {
if (children && inlineCollapsed.value && firstLevel && typeof children === 'string') {
return (
<div class={`${prefixCls.value}-inline-collapsed-noicon`}>{children.charAt(0)}</div>
);
}
}
return wrapNode;
};
// ========================== DirectionStyle ==========================
const directionStyle = useDirectionStyle(computed(() => keysPath.value.length));
return () => {
const title = props.title ?? slots.title?.();
const children = flattenChildren(slots.default?.());
const childrenLength = children.length;
let tooltipTitle: any = title;
if (typeof title === 'undefined') {
tooltipTitle = firstLevel ? children : '';
} else if (title === false) {
tooltipTitle = '';
}
const tooltipProps: any = {
title: tooltipTitle,
};
if (!siderCollapsed.value && !inlineCollapsed.value) {
tooltipProps.title = null;
// Reset `visible` to fix control mode tooltip display not correct
// ref: https://github.com/ant-design/ant-design/issues/16742
tooltipProps.visible = false;
}
// ============================ Render ============================
const optionRoleProps = {};
if (props.role === 'option') {
optionRoleProps['aria-selected'] = selected.value;
}
const icon = getPropsSlot(slots, props, 'icon');
return (
<Tooltip
{...tooltipProps}
placement={rtl.value ? 'left' : 'right'}
overlayClassName={`${prefixCls.value}-inline-collapsed-tooltip`}
>
<Overflow.Item
component="li"
{...attrs}
style={{ ...((attrs.style as any) || {}), ...directionStyle.value }}
class={[
classNames.value,
{
[`${attrs.class}`]: !!attrs.class,
[`${prefixCls.value}-item-only-child`]:
(icon ? childrenLength + 1 : childrenLength) === 1,
},
]}
role={props.role || 'menuitem'}
tabindex={props.disabled ? null : -1}
data-menu-id={key}
aria-disabled={props.disabled}
{...optionRoleProps}
onMouseenter={onMouseEnter}
onMouseleave={onMouseLeave}
onClick={onInternalClick}
onKeydown={onInternalKeyDown}
onFocus={onInternalFocus}
title={typeof title === 'string' ? title : undefined}
>
{cloneElement(icon, {
class: `${prefixCls.value}-item-icon`,
})}
{renderItemChildren(icon, children)}
</Overflow.Item>
</Tooltip>
);
};
},
});

View File

@ -0,0 +1,103 @@
import Trigger from '../../vc-trigger';
import { computed, defineComponent, onBeforeUnmount, PropType, ref, watch } from 'vue';
import { MenuMode } from './interface';
import { useInjectMenu } from './hooks/useMenuContext';
import { placements, placementsRtl } from './placements';
import raf from '../../_util/raf';
import classNames from '../../_util/classNames';
const popupPlacementMap = {
horizontal: 'bottomLeft',
vertical: 'rightTop',
'vertical-left': 'rightTop',
'vertical-right': 'leftTop',
};
export default defineComponent({
name: 'PopupTrigger',
inheritAttrs: false,
props: {
prefixCls: String,
mode: String as PropType<MenuMode>,
visible: Boolean,
// popup: React.ReactNode;
popupClassName: String,
popupOffset: Array as PropType<number[]>,
disabled: Boolean,
onVisibleChange: Function as PropType<(visible: boolean) => void>,
},
slots: ['popup'],
emits: ['visibleChange'],
setup(props, { slots, emit }) {
const innerVisible = ref(false);
const {
getPopupContainer,
rtl,
subMenuOpenDelay,
subMenuCloseDelay,
builtinPlacements,
triggerSubMenuAction,
isRootMenu,
} = useInjectMenu();
const placement = computed(() =>
rtl.value
? { ...placementsRtl, ...builtinPlacements.value }
: { ...placements, ...builtinPlacements.value },
);
const popupPlacement = computed(() => popupPlacementMap[props.mode]);
const visibleRef = ref<number>();
watch(
() => props.visible,
visible => {
raf.cancel(visibleRef.value);
visibleRef.value = raf(() => {
innerVisible.value = visible;
});
},
{ immediate: true },
);
onBeforeUnmount(() => {
raf.cancel(visibleRef.value);
});
const onVisibleChange = (visible: boolean) => {
emit('visibleChange', visible);
};
return () => {
const { prefixCls, popupClassName, mode, popupOffset, disabled } = props;
return (
<Trigger
prefixCls={prefixCls}
popupClassName={classNames(
`${prefixCls}-popup`,
{
[`${prefixCls}-rtl`]: rtl.value,
},
popupClassName,
)}
stretch={mode === 'horizontal' ? 'minWidth' : null}
getPopupContainer={
isRootMenu ? getPopupContainer.value : triggerNode => triggerNode.parentNode
}
builtinPlacements={placement.value}
popupPlacement={popupPlacement.value}
popupVisible={innerVisible.value}
popupAlign={popupOffset && { offset: popupOffset }}
action={disabled ? [] : [triggerSubMenuAction.value]}
mouseEnterDelay={subMenuOpenDelay.value}
mouseLeaveDelay={subMenuCloseDelay.value}
onPopupVisibleChange={onVisibleChange}
forceRender={true}
v-slots={{
popup: () => {
return slots.popup?.({ visible: innerVisible.value });
},
default: slots.default,
}}
></Trigger>
);
};
},
});

Some files were not shown because too many files have changed in this diff Show More