diff --git a/packages/design/src/Popover.vue b/packages/design/src/Popover.vue index 40d2b3d2..8b9bf6ef 100644 --- a/packages/design/src/Popover.vue +++ b/packages/design/src/Popover.vue @@ -43,8 +43,16 @@ const props = withDefaults(defineProps(), { 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(); }); diff --git a/packages/design/src/types.ts b/packages/design/src/types.ts index 7c5572a3..7672a0fe 100644 --- a/packages/design/src/types.ts +++ b/packages/design/src/types.ts @@ -258,6 +258,13 @@ export interface PopoverProps { popperClass?: string; tabindex?: number; destroyOnClose?: boolean; + /** 点击 popover 及其衍生浮层以外的区域时收起,默认开启。 */ + closeOnClickOutside?: boolean; + /** + * 追加的「点击不关闭」选择器,会与内置的弹窗 / 确认框 / 下拉等浮层选择器合并。 + * 用于 popover 内部触发、挂载到 body 之外的浮层不应顺带关闭 popover 的场景。 + */ + clickOutsideIgnore?: string; } export interface RadioProps { diff --git a/packages/editor/src/layouts/history-list/HistoryListPanel.vue b/packages/editor/src/layouts/history-list/HistoryListPanel.vue index f9eaf535..b4a64339 100644 --- a/packages/editor/src/layouts/history-list/HistoryListPanel.vue +++ b/packages/editor/src/layouts/history-list/HistoryListPanel.vue @@ -3,7 +3,7 @@ popper-class="m-editor-history-list-popover" placement="bottom" trigger="click" - :visible="visible" + v-model:visible="visible" :width="660" >
@@ -163,7 +163,10 @@ const ClockIcon = markRaw(Clock); const CloseIcon = markRaw(Close); const activeTab = ref('page'); -/** 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。 */ +/** + * 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。 + * 点击面板以外区域的自动收起由 TMagicPopover 通过 v-model:visible 回写完成。 + */ const visible = ref(false); const tabPaneComponent = getDesignConfig('components')?.tabPane;