ant-design-vue/components/vc-dialog/Dialog.tsx

203 lines
6.1 KiB
Vue

import type { PropType } from 'vue';
import { defineComponent, onBeforeUnmount, shallowRef, watch, watchEffect } from 'vue';
import contains from '../vc-util/Dom/contains';
import type ScrollLocker from '../vc-util/Dom/scrollLocker';
import classNames from '../_util/classNames';
import type { MouseEventHandler } from '../_util/EventInterface';
import KeyCode from '../_util/KeyCode';
import omit from '../_util/omit';
import pickAttrs from '../_util/pickAttrs';
import { initDefaultProps } from '../_util/props-util';
import type { ContentRef } from './Content';
import Content from './Content';
import dialogPropTypes from './IDialogPropTypes';
import Mask from './Mask';
import { getMotionName, getUUID } from './util';
export default defineComponent({
compatConfig: { MODE: 3 },
name: 'Dialog',
inheritAttrs: false,
props: initDefaultProps(
{
...dialogPropTypes(),
getOpenCount: Function as PropType<() => number>,
scrollLocker: Object as PropType<ScrollLocker>,
},
{
mask: true,
visible: false,
keyboard: true,
closable: true,
maskClosable: true,
destroyOnClose: false,
prefixCls: 'rc-dialog',
getOpenCount: () => null,
focusTriggerAfterClose: true,
},
),
setup(props, { attrs, slots }) {
const lastOutSideActiveElementRef = shallowRef<HTMLElement>();
const wrapperRef = shallowRef<HTMLDivElement>();
const contentRef = shallowRef<ContentRef>();
const animatedVisible = shallowRef(props.visible);
const ariaIdRef = shallowRef<string>(`vcDialogTitle${getUUID()}`);
// ========================= Events =========================
const onDialogVisibleChanged = (newVisible: boolean) => {
if (newVisible) {
// Try to focus
if (!contains(wrapperRef.value, document.activeElement as HTMLElement)) {
lastOutSideActiveElementRef.value = document.activeElement as HTMLElement;
contentRef.value?.focus();
}
} else {
const preAnimatedVisible = animatedVisible.value;
// Clean up scroll bar & focus back
animatedVisible.value = false;
if (props.mask && lastOutSideActiveElementRef.value && props.focusTriggerAfterClose) {
try {
lastOutSideActiveElementRef.value.focus({ preventScroll: true });
} catch (e) {
// Do nothing
}
lastOutSideActiveElementRef.value = null;
}
// Trigger afterClose only when change visible from true to false
if (preAnimatedVisible) {
props.afterClose?.();
}
}
};
const onInternalClose = (e: MouseEvent | KeyboardEvent) => {
props.onClose?.(e);
};
// >>> Content
const contentClickRef = shallowRef(false);
const contentTimeoutRef = shallowRef<any>();
// We need record content click incase content popup out of dialog
const onContentMouseDown: MouseEventHandler = () => {
clearTimeout(contentTimeoutRef.value);
contentClickRef.value = true;
};
const onContentMouseUp: MouseEventHandler = () => {
contentTimeoutRef.value = setTimeout(() => {
contentClickRef.value = false;
});
};
const onWrapperClick = (e: MouseEvent) => {
if (!props.maskClosable) return null;
if (contentClickRef.value) {
contentClickRef.value = false;
} else if (wrapperRef.value === e.target) {
onInternalClose(e);
}
};
const onWrapperKeyDown = (e: KeyboardEvent) => {
if (props.keyboard && e.keyCode === KeyCode.ESC) {
e.stopPropagation();
onInternalClose(e);
return;
}
// keep focus inside dialog
if (props.visible) {
if (e.keyCode === KeyCode.TAB) {
contentRef.value.changeActive(!e.shiftKey);
}
}
};
watch(
() => props.visible,
() => {
if (props.visible) {
animatedVisible.value = true;
}
},
{ flush: 'post' },
);
onBeforeUnmount(() => {
clearTimeout(contentTimeoutRef.value);
props.scrollLocker?.unLock();
});
watchEffect(() => {
props.scrollLocker?.unLock();
if (animatedVisible.value) {
props.scrollLocker?.lock();
}
});
return () => {
const {
prefixCls,
mask,
visible,
maskTransitionName,
maskAnimation,
zIndex,
wrapClassName,
rootClassName,
wrapStyle,
closable,
maskProps,
maskStyle,
transitionName,
animation,
wrapProps,
title = slots.title,
} = props;
const { style, class: className } = attrs;
return (
<div class={[`${prefixCls}-root`, rootClassName]} {...pickAttrs(props, { data: true })}>
<Mask
prefixCls={prefixCls}
visible={mask && visible}
motionName={getMotionName(prefixCls, maskTransitionName, maskAnimation)}
style={{
zIndex,
...maskStyle,
}}
maskProps={maskProps}
/>
<div
tabIndex={-1}
onKeydown={onWrapperKeyDown}
class={classNames(`${prefixCls}-wrap`, wrapClassName)}
ref={wrapperRef}
onClick={onWrapperClick}
role="dialog"
aria-labelledby={title ? ariaIdRef.value : null}
style={{ zIndex, ...wrapStyle, display: !animatedVisible.value ? 'none' : null }}
{...wrapProps}
>
<Content
{...omit(props, ['scrollLocker'])}
style={style}
class={className}
v-slots={slots}
onMousedown={onContentMouseDown}
onMouseup={onContentMouseUp}
ref={contentRef}
closable={closable}
ariaId={ariaIdRef.value}
prefixCls={prefixCls}
visible={visible}
onClose={onInternalClose}
onVisibleChanged={onDialogVisibleChanged}
motionName={getMotionName(prefixCls, transitionName, animation)}
/>
</div>
</div>
);
};
},
});