203 lines
6.1 KiB
Vue
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>
|
|
);
|
|
};
|
|
},
|
|
});
|