优雅解决el-popover内有select时在选择后会自动关闭 2025-01-21

1 问题描述

在使用 element-plus 框架的 el-popover 组件的时候,如果这个组件的内部有 el-select组件时,当选中了其中一项后 el-popover会自动关闭。
如下示例:
https://zhonghuitech.github.io/vfg/#/popover

image.png

代码:

<el-popover placement="bottom" :width="500" trigger="click">
        <template #default>
            <el-card style="max-width: 500px;border:0px solid red;--el-card-border-color:#ffffff" shadow="never">
                <template #header>
                    <div class="card-header">
                        <span style="font-weight: bold;font-size: large;">筛选</span>
                    </div>
                </template>

                <el-form ref="taskRef">
                    <el-row>
                        <el-col :span="12">
                            <el-form-item prop="itemCode" style="margin-left: 10px;">
                                <el-select v-model="form.type" placeholder="" :teleported="true" placement="bottom">
                                    <el-option v-for="dict in type_dic" :key="dict.value" :label="dict.label"
                                        :value="dict.value"></el-option>
                                </el-select>
                            </el-form-item>
                        </el-col>
                    </el-row>
                </el-form>

                <template #footer>
                    <div style="display: flex;justify-content:space-between">
                        <span>
                            <el-button @click="cancel" link>取 消</el-button>
                            <el-button type="primary" @click="submitFilterForm">确 定</el-button>
                        </span>
                    </div>
                </template>
            </el-card>
        </template>
        <template #reference>
            <el-button type="primary" @click="proBtnClick">问题示例</el-button>
        </template>
    </el-popover>

2 问题原因

官方文档上有相关的问题issue,原因如下:

image.png

https://github.com/element-plus/element-plus/issues/1266
https://github.com/element-plus/element-plus/issues/9404

给出的解决方式是将 el-popoverteleported 设置为 false

3 解法一:将 teleported 设置为 false

网上通用的作法是将 el-popoverteleported 属性设置为 false,问题是得到了解决。但又出现了新的问题:
https://zhonghuitech.github.io/vfg/#/popover

image.png

新的问题el-popoveroptions 部分很多导致高度不够,多余的部分显示不出来。

4 解法二:优雅解决

通过将teleported的属性设置为false显然不是我们想要的解决方案。那只剩下最后一条路,那就是自己手动控制el-popover的显示与隐藏。

<el-popover placement="bottom" :width="500" trigger="click" :visible="popoverVisible">

设置变量 popoverVisible

const popoverVisible = ref(false)
const data = reactive({
    form: { type: '' }
})
const { form } = toRefs(data);

function proBtnClick() {
    popoverVisible.value = true
}

但这里要解决一个关键问题:当鼠标点击发生在 el-popover 区域外时,需要关闭 popover 。怎么解决这个问题?

4.1 解决鼠标在区域外点击时关闭的问题

解决这个问题的思路是监听全局的点击事件,然后判断这个点击事件的发生是否在 popover的区域内。
先上源码:

<el-popover placement="bottom" :width="500" trigger="click" :visible="popoverVisible">
        <template #default>
            <div ref="elPopoverFilterCardWrapDivRef">
                <el-card style="max-width: 500px;border:0px solid red;--el-card-border-color:#ffffff" shadow="never">
                    <template #header>
                        <div class="card-header">
                            <span style="font-weight: bold;font-size: large;">筛选</span>
                        </div>
                    </template>

                    <el-form ref="taskRef">
                        <el-row>
                            <el-col :span="12">
                                <el-form-item prop="itemCode" style="margin-left: 10px;">
                                    <el-select v-model="form.type" placeholder="" :teleported="true" placement="bottom">
                                        <el-option v-for="dict in type_dic" :key="dict.value" :label="dict.label"
                                            :value="dict.value"></el-option>
                                    </el-select>
                                </el-form-item>
                            </el-col>
                        </el-row>
                    </el-form>

                    <template #footer>
                        <div style="display: flex;justify-content:space-between">
                            <span>
                                <el-button @click="cancel" link>取 消</el-button>
                                <el-button type="primary" @click="submitFilterForm">确 定</el-button>
                            </span>
                        </div>
                    </template>
                </el-card>
            </div>
        </template>
        <template #reference>
            <el-button type="primary" @click="proBtnClick">优雅解决</el-button>
        </template>
    </el-popover>

JS部分代码如下:

const popoverVisible = ref(false)
const data = reactive({
    form: { type: '' }
})
const { form } = toRefs(data);

function proBtnClick(e) {
    popoverVisible.value = true
    e.stopPropagation();    
}
const elPopoverFilterCardWrapDivRef = ref(null)

const isPopoverChildComponent = (element, parent) => {
    if (element === parent) {
        // ref 只能作用于原生 html 组件上,不能在 el-card 之上,所以只能在 el-card 上包一层。
        console.log(element)
        return true;
    }
    if (element.parentElement) {
        return isPopoverChildComponent(element.parentElement, parent);
    }
    return false;
};

const handleGlobalClick = (event) => {
    nextTick(() => {
        // debugger
        if (isPopoverChildComponent(event.target, elPopoverFilterCardWrapDivRef.value)) {
            console.log('点击发生在 Popover 或其子组件上');
        } else {
            console.log('点击发生在 Popover 之外');
            popoverVisible.value = false
        }
    });
};

const registerGlobalClick = () => {
    window.addEventListener('click', handleGlobalClick);
};

const unregisterGlobalClick = () => {
    window.removeEventListener('click', handleGlobalClick);
};

onMounted(() => {
    // 组件挂载时注册全局点击事件
    registerGlobalClick();
});

onUnmounted(() => {
    // 组件卸载时取消注册全局点击事件
    unregisterGlobalClick();
});

几点说明:
1)elPopoverFilterCardWrapDivRef 用来关联popover内容部分的组件(注意:这个一定要原生的 html 组件才行,如 div)
2)handleGlobalClick 处理全局的点击事件
3)isPopoverChildComponent 判断点击事件是否来自popover之内

3.4 遇到新问题

el-select组件通过上面的方法解决了,但是,如果添加 el-date-picker 这个组件,仍然会有问题。

image.png

看起来上面的机制失效了。手工 debug 看,发现找父组件,最后找到了 body 上,没有找到 elPopoverFilterCardWrapDivNewRef

image.png

3.4 新问题解决

无论是 el-popover 还是 el-date-picker 都是基于 popper 这个开源库来的。el-date-picker 里有一个 popper-class,我们可以设置一个特殊的 class类,然后往上找的时候,如果找到的就说明点击事件是发生在 popover的内部。

popper-class="ItemValueDataPickerPopoverVitualClass"

JS部分代码稍做修改即可:

const isPopoverChildComponent = (element, parent) => {
    if (element && element.classList) {
        if (element.classList.contains('ItemValueDataPickerPopoverVitualClass')) {
            console.log('ItemValueDataPickerPopoverVitualClass')
            return true;
        }
    }

    if (element === parent) {
        // ref 只能作用于原生 html 组件上,不能在 el-card 之上,所以只能在 el-card 上包一层。
        console.log(element)
        return true;
    }
    if (element.parentElement) {
        return isPopoverChildComponent(element.parentElement, parent);
    }
    return false;
};

DEMO 地址:https://zhonghuitech.github.io/vfg/#/popover
源码:https://github.com/zhonghuitech/vfg/blob/main/playground/src/pages/PopoverDemo.vue

image.png
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容