feat(design): popover 支持点击外部关闭

新增 closeOnClickOutside 与 clickOutsideIgnore 配置,
兼容 element-plus / tdesign 衍生浮层。
历史列表面板改用 v-model:visible 配合自动收起。
This commit is contained in:
roymondchen 2026-06-11 16:49:54 +08:00
parent fd652b0d13
commit 846f05e04d
3 changed files with 85 additions and 2 deletions

View File

@ -43,8 +43,16 @@ const props = withDefaults(defineProps<PopoverProps>(), {
visible: undefined,
tabindex: 0,
destroyOnClose: false,
closeOnClickOutside: true,
});
const emit = defineEmits<{
/** 受控模式(传入了 visible)下点击外部收起时触发,便于配合 v-model:visible。 */
'update:visible': [_visible: boolean];
/** 点击 popover 及其衍生浮层以外的区域时触发。 */
clickoutside: [_event: MouseEvent];
}>();
const popoverVisible = ref(false);
const visibleWatch = watch(
@ -179,6 +187,70 @@ if (props.trigger === 'hover' && typeof props.visible === 'undefined') {
});
}
/**
* popover 内部触发、却挂载到 body(在 popper 之外)的浮层:弹窗、二次确认框、tooltip、
* 下拉 / 日期选择等。点击它们属于 popover 内部交互,不应顺带把 popover 关闭。
*
* 由于 @tmagic/design 通过适配器支持 element-plus、tdesign 等多套 UI 库,这里同时列出
* 两套库的浮层 class(class 名互不冲突,未命中的选择器无副作用),避免切换适配器后失效。
*/
const DEFAULT_CLICK_OUTSIDE_IGNORE = [
// @tmagic/design 自身(与适配器无关)
'.tmagic-design-dialog',
// element-plus
'.el-overlay',
'.el-message-box',
'.el-popper',
'.el-select-dropdown',
'.el-picker__popper',
'.el-dropdown__popper',
'.el-cascader__dropdown',
// tdesign:弹窗 / 消息确认(DialogPlugin / MessagePlugin)与各类浮层(tooltip / select / dropdown / 日期选择等均挂在 .t-popup 内)
'.t-dialog__ctx',
'.t-dialog',
'.t-message',
'.t-popup',
].join(',');
const clickOutsideIgnoreSelector = computed(() =>
[DEFAULT_CLICK_OUTSIDE_IGNORE, props.clickOutsideIgnore].filter(Boolean).join(','),
);
const handleClickOutside = (e: MouseEvent) => {
if (props.disabled) return;
const target = e.target as HTMLElement | null;
if (!target) return;
// 点击 reference、popper 自身或衍生浮层时保持打开
if (referenceElementRef.value?.contains(target)) return;
if (popperElementRef.value?.contains(target)) return;
if (target.closest(clickOutsideIgnoreSelector.value)) return;
emit('clickoutside', e);
// 非受控:直接收起;受控:通过 update:visible 通知父级(可配合 v-model:visible)
if (typeof props.visible === 'undefined') {
popoverVisible.value = false;
} else {
emit('update:visible', false);
}
};
const bindClickOutside = () => globalThis.document?.addEventListener('click', handleClickOutside);
const unbindClickOutside = () => globalThis.document?.removeEventListener('click', handleClickOutside);
watch(popoverVisible, (visible) => {
if (!props.closeOnClickOutside) return;
if (visible) {
// 延后到「打开 popover 的这一次点击」冒泡结束后再监听,避免刚打开就被立即关闭
nextTick(bindClickOutside);
} else {
unbindClickOutside();
}
});
const destroy = () => {
if (!instanceRef.value) return;
@ -188,5 +260,6 @@ const destroy = () => {
onBeforeUnmount(() => {
destroy();
unbindClickOutside();
});
</script>

View File

@ -258,6 +258,13 @@ export interface PopoverProps {
popperClass?: string;
tabindex?: number;
destroyOnClose?: boolean;
/** 点击 popover 及其衍生浮层以外的区域时收起,默认开启。 */
closeOnClickOutside?: boolean;
/**
* 追加的「点击不关闭」选择器,会与内置的弹窗 / 确认框 / 下拉等浮层选择器合并。
* 用于 popover 内部触发、挂载到 body 之外的浮层不应顺带关闭 popover 的场景。
*/
clickOutsideIgnore?: string;
}
export interface RadioProps {

View File

@ -3,7 +3,7 @@
popper-class="m-editor-history-list-popover"
placement="bottom"
trigger="click"
:visible="visible"
v-model:visible="visible"
:width="660"
>
<div class="m-editor-history-list">
@ -163,7 +163,10 @@ const ClockIcon = markRaw(Clock);
const CloseIcon = markRaw(Close);
const activeTab = ref<string>('page');
/** 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。 */
/**
* 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。
* 点击面板以外区域的自动收起由 TMagicPopover 通过 v-model:visible 回写完成。
*/
const visible = ref(false);
const tabPaneComponent = getDesignConfig('components')?.tabPane;