You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ant-design-vue/components/drawer/index.tsx

435 lines
13 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import type { CSSProperties, ExtractPropTypes, PropType } from 'vue';
import {
inject,
nextTick,
defineComponent,
shallowRef,
onMounted,
provide,
onUnmounted,
watch,
computed,
} from 'vue';
import { getPropsSlot, initDefaultProps } from '../_util/props-util';
import classnames from '../_util/classNames';
import VcDrawer from '../vc-drawer';
import PropTypes from '../_util/vue-types';
import CloseOutlined from '@ant-design/icons-vue/CloseOutlined';
import useConfigInject from '../config-provider/hooks/useConfigInject';
import { objectType, withInstall } from '../_util/type';
import type { CustomSlotsType } from '../_util/type';
import omit from '../_util/omit';
import devWarning from '../vc-util/devWarning';
import type { KeyboardEventHandler, MouseEventHandler } from '../_util/EventInterface';
import useStyle from './style';
import { NoCompactStyle } from '../space/Compact';
import isNumeric from '../_util/isNumeric';
import { getTransitionName, getTransitionProps } from '../_util/transition';
type ILevelMove = number | [number, number];
const PlacementTypes = ['top', 'right', 'bottom', 'left'] as const;
export type placementType = (typeof PlacementTypes)[number];
const SizeTypes = ['default', 'large'] as const;
export type sizeType = (typeof SizeTypes)[number];
export interface PushState {
distance: string | number;
}
const defaultPushState: PushState = { distance: 180 };
type getContainerFunc = () => HTMLElement;
export const drawerProps = () => ({
autofocus: { type: Boolean, default: undefined },
closable: { type: Boolean, default: undefined },
closeIcon: PropTypes.any,
destroyOnClose: { type: Boolean, default: undefined },
forceRender: { type: Boolean, default: undefined },
getContainer: {
type: [String, Function, Boolean, Object] as PropType<
string | HTMLElement | getContainerFunc | false
>,
default: undefined as string | HTMLElement | getContainerFunc | false,
},
maskClosable: { type: Boolean, default: undefined },
mask: { type: Boolean, default: undefined },
maskStyle: objectType<CSSProperties>(),
rootClassName: String,
rootStyle: objectType<CSSProperties>(),
size: {
type: String as PropType<sizeType>,
},
drawerStyle: objectType<CSSProperties>(),
headerStyle: objectType<CSSProperties>(),
bodyStyle: objectType<CSSProperties>(),
contentWrapperStyle: {
type: Object as PropType<CSSProperties>,
default: undefined as CSSProperties,
},
title: PropTypes.any,
/** @deprecated Please use `open` instead */
visible: { type: Boolean, default: undefined },
open: { type: Boolean, default: undefined },
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
zIndex: Number,
prefixCls: String,
push: PropTypes.oneOfType([PropTypes.looseBool, { type: Object as PropType<PushState> }]),
placement: PropTypes.oneOf(PlacementTypes),
keyboard: { type: Boolean, default: undefined },
extra: PropTypes.any,
footer: PropTypes.any,
footerStyle: objectType<CSSProperties>(),
level: PropTypes.any,
levelMove: {
type: [Number, Array, Function] as PropType<
ILevelMove | ((e: { target: HTMLElement; open: boolean }) => ILevelMove)
>,
},
handle: PropTypes.any,
/** @deprecated Use `@afterVisibleChange` instead */
afterVisibleChange: Function as PropType<(visible: boolean) => void>,
/** @deprecated Please use `@afterOpenChange` instead */
onAfterVisibleChange: Function as PropType<(visible: boolean) => void>,
onAfterOpenChange: Function as PropType<(open: boolean) => void>,
/** @deprecated Please use `onUpdate:open` instead */
'onUpdate:visible': Function as PropType<(visible: boolean) => void>,
'onUpdate:open': Function as PropType<(open: boolean) => void>,
onClose: Function as PropType<MouseEventHandler | KeyboardEventHandler>,
});
export type DrawerProps = Partial<ExtractPropTypes<ReturnType<typeof drawerProps>>>;
const Drawer = defineComponent({
compatConfig: { MODE: 3 },
name: 'ADrawer',
inheritAttrs: false,
props: initDefaultProps(drawerProps(), {
closable: true,
placement: 'right',
maskClosable: true,
mask: true,
level: null,
keyboard: true,
push: defaultPushState,
}),
slots: Object as CustomSlotsType<{
closeIcon: any;
title: any;
extra: any;
footer: any;
handle: any;
default: any;
}>,
// emits: ['update:visible', 'close', 'afterVisibleChange'],
setup(props, { emit, slots, attrs }) {
const sPush = shallowRef(false);
const destroyClose = shallowRef(false);
const vcDrawer = shallowRef(null);
const load = shallowRef(false);
const visible = shallowRef(false);
const mergedOpen = computed(() => props.open ?? props.visible);
watch(
mergedOpen,
() => {
if (mergedOpen.value) {
load.value = true;
} else {
visible.value = false;
}
},
{ immediate: true },
);
watch(
[mergedOpen, load],
() => {
if (mergedOpen.value && load.value) {
visible.value = true;
}
},
{ immediate: true },
);
const parentDrawerOpts = inject('parentDrawerOpts', null);
const { prefixCls, getPopupContainer, direction } = useConfigInject('drawer', props);
const [wrapSSR, hashId] = useStyle(prefixCls);
const getContainer = computed(() =>
// 有可能为 false所以不能直接判断
props.getContainer === undefined && getPopupContainer?.value
? () => getPopupContainer.value(document.body)
: props.getContainer,
);
devWarning(
!props.afterVisibleChange,
'Drawer',
'`afterVisibleChange` prop is deprecated, please use `@afterVisibleChange` event instead',
);
// ========================== Warning ===========================
if (process.env.NODE_ENV !== 'production') {
[
['visible', 'open'],
['onUpdate:visible', 'onUpdate:open'],
['onAfterVisibleChange', 'onAfterOpenChange'],
].forEach(([deprecatedName, newName]) => {
devWarning(
!props[deprecatedName],
'Drawer',
`\`${deprecatedName}\` is deprecated, please use \`${newName}\` instead.`,
);
});
}
const setPush = () => {
sPush.value = true;
};
const setPull = () => {
sPush.value = false;
nextTick(() => {
domFocus();
});
};
provide('parentDrawerOpts', {
setPush,
setPull,
});
onMounted(() => {
if (mergedOpen.value && parentDrawerOpts) {
parentDrawerOpts.setPush();
}
});
onUnmounted(() => {
if (parentDrawerOpts) {
parentDrawerOpts.setPull();
}
});
watch(
visible,
() => {
if (parentDrawerOpts) {
if (visible.value) {
parentDrawerOpts.setPush();
} else {
parentDrawerOpts.setPull();
}
}
},
{ flush: 'post' },
);
const domFocus = () => {
vcDrawer.value?.domFocus?.();
};
const close = (e: Event) => {
emit('update:visible', false);
emit('update:open', false);
emit('close', e);
};
const afterVisibleChange = (open: boolean) => {
if (!open) {
if (destroyClose.value === false) {
// set true only once
destroyClose.value = true;
}
if (props.destroyOnClose) {
load.value = false;
}
}
props.afterVisibleChange?.(open);
emit('afterVisibleChange', open);
emit('afterOpenChange', open);
};
const pushTransform = computed(() => {
const { push, placement } = props;
let distance: number | string;
if (typeof push === 'boolean') {
distance = push ? defaultPushState.distance : 0;
} else {
distance = push!.distance;
}
distance = parseFloat(String(distance || 0));
if (placement === 'left' || placement === 'right') {
return `translateX(${placement === 'left' ? distance : -distance}px)`;
}
if (placement === 'top' || placement === 'bottom') {
return `translateY(${placement === 'top' ? distance : -distance}px)`;
}
return null;
});
// ============================ Size ============================
const mergedWidth = computed(() => props.width ?? (props.size === 'large' ? 736 : 378));
const mergedHeight = computed(() => props.height ?? (props.size === 'large' ? 736 : 378));
const offsetStyle = computed(() => {
// https://github.com/ant-design/ant-design/issues/24287
const { mask, placement } = props;
if (!visible.value && !mask) {
return {};
}
const val: CSSProperties = {};
if (placement === 'left' || placement === 'right') {
val.width = isNumeric(mergedWidth.value) ? `${mergedWidth.value}px` : mergedWidth.value;
} else {
val.height = isNumeric(mergedHeight.value) ? `${mergedHeight.value}px` : mergedHeight.value;
}
return val;
});
const wrapperStyle = computed(() => {
const { zIndex } = props;
const val = offsetStyle.value;
return [{ zIndex, transform: sPush.value ? pushTransform.value : undefined }, val];
});
const renderHeader = (prefixCls: string) => {
const { closable, headerStyle } = props;
const extra = getPropsSlot(slots, props, 'extra');
const title = getPropsSlot(slots, props, 'title');
if (!title && !closable) {
return null;
}
return (
<div
class={classnames(`${prefixCls}-header`, {
[`${prefixCls}-header-close-only`]: closable && !title && !extra,
})}
style={headerStyle}
>
<div class={`${prefixCls}-header-title`}>
{renderCloseIcon(prefixCls)}
{title && <div class={`${prefixCls}-title`}>{title}</div>}
</div>
{extra && <div class={`${prefixCls}-extra`}>{extra}</div>}
</div>
);
};
const renderCloseIcon = (prefixCls: string) => {
const { closable } = props;
const $closeIcon = slots.closeIcon ? slots.closeIcon?.() : props.closeIcon;
return (
closable && (
<button key="closer" onClick={close} aria-label="Close" class={`${prefixCls}-close`}>
{$closeIcon === undefined ? <CloseOutlined></CloseOutlined> : $closeIcon}
</button>
)
);
};
const renderBody = (prefixCls: string) => {
if (destroyClose.value && !props.forceRender && !load.value) {
return null;
}
const { bodyStyle, drawerStyle } = props;
return (
<div class={`${prefixCls}-wrapper-body`} style={drawerStyle}>
{renderHeader(prefixCls)}
<div key="body" class={`${prefixCls}-body`} style={bodyStyle}>
{slots.default?.()}
</div>
{renderFooter(prefixCls)}
</div>
);
};
const renderFooter = (prefixCls: string) => {
const footer = getPropsSlot(slots, props, 'footer');
if (!footer) {
return null;
}
const footerClassName = `${prefixCls}-footer`;
return (
<div class={footerClassName} style={props.footerStyle}>
{footer}
</div>
);
};
const drawerClassName = computed(() =>
classnames(
{
'no-mask': !props.mask,
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
},
props.rootClassName,
hashId.value,
),
);
// =========================== Motion ===========================
const maskMotion = computed(() => {
return getTransitionProps(getTransitionName(prefixCls.value, 'mask-motion'));
});
const panelMotion = (motionPlacement: string) => {
return getTransitionProps(
getTransitionName(prefixCls.value, `panel-motion-${motionPlacement}`),
);
};
return () => {
const { width, height, placement, mask, forceRender, ...rest } = props;
const vcDrawerProps: any = {
...attrs,
...omit(rest, [
'size',
'closeIcon',
'closable',
'destroyOnClose',
'drawerStyle',
'headerStyle',
'bodyStyle',
'title',
'push',
'onAfterVisibleChange',
'onClose',
'onUpdate:visible',
'onUpdate:open',
'visible',
]),
forceRender,
onClose: close,
afterVisibleChange,
handler: false,
prefixCls: prefixCls.value,
open: visible.value,
showMask: mask,
placement,
ref: vcDrawer,
};
return wrapSSR(
<NoCompactStyle>
<VcDrawer
{...vcDrawerProps}
maskMotion={maskMotion.value}
motion={panelMotion}
width={mergedWidth.value}
height={mergedHeight.value}
getContainer={getContainer.value}
rootClassName={drawerClassName.value}
rootStyle={props.rootStyle}
contentWrapperStyle={wrapperStyle.value}
v-slots={{
handler: props.handle ? () => props.handle : slots.handle,
default: () => renderBody(prefixCls.value),
}}
></VcDrawer>
</NoCompactStyle>,
);
};
},
});
export default withInstall(Drawer);