Svelte自定义下拉框点击自身无法关闭的问题求助
自定义Svelte下拉框展开后点击容器无法关闭的问题解决
我是React工程师,首次接触Svelte维护公司遗留代码。自制的自定义下拉框,初始点击打开、handleSelection函数、clickOutside指令都正常工作,但当下拉框展开时,点击class="select"的容器无法关闭它。
问题代码(inputSelect.svelte)
<svelte:options tag="input-select" /> <script lang="ts"> import { onMount } from "svelte"; import { get_current_component } from "svelte/internal"; import Button from "./button.svelte"; export let options = []; let selectedOption = null; let isOpen = false; let parent: HTMLElement; const component = get_current_component(); function openDropdown(event: MouseEvent): void { console.log("openDropdown"); isOpen = !isOpen; console.log(""); } function handleSelection(value: string, event: Event): void { console.log("HANDLE SELECTION"); selectedOption = value; component?.dispatchEvent( new CustomEvent("selectedOptionChanged", { detail: value }) ); console.log("HANDLE SELECTION"); console.log(""); } function clickOutside(node: HTMLElement) { const handleClick = (event: MouseEvent) => { if ( node && !node.contains(event.target as Node) && isOpen === true ) { console.log("CLICK OUTSIDE"); isOpen = false; console.log(""); } }; document.addEventListener("click", handleClick, true); return { destroy() { document.removeEventListener("click", handleClick, true); }, }; } onMount(() => { clickOutside(parent); }); </script> <div> <div class="custom-select" style="border-color: {isOpen ? '#1677ff' : 'rgba(0, 0, 0, 0.25)'};" > <label for="entity-type" style="display: block; font-weight: 1000; margin-bottom: 4px;" > Entity Type {isOpen} </label> <div> <div class="select" class:select-arrow-active={isOpen} on:click={openDropdown} use:clickOutside bind:this={parent} > {#if selectedOption === null} <div>Select</div> {:else} <div> {options.find( (option) => option.value === selectedOption )?.label} </div> {/if} <div class="select-items" style={isOpen ? "display: block;" : "display: none;"} > {#each options as option (option.value)} <div on:click={(event) => { handleSelection(option.value, event); event.stopPropagation(); }} > {option.label} </div> {/each} </div> </div> </div> </div> </div> <style lang="scss"> .custom-select { position: relative; width: 200px; .select { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; border: 1px solid rgba(0, 0, 0, 0.25); cursor: pointer; user-select: none; border-radius: 4px; position: relative; &:after { content: ""; width: 0; height: 0; border-style: solid; border-width: 4px 4px 0 4px; border-color: rgba(0, 0, 0, 0.25) transparent transparent transparent; position: relative; transform: translateY(-50%) rotate(-180deg); margin-left: 8px; transition: transform 0.3s ease; } &.select-arrow-active::after, &.select-arrow-active::before { transform: translateY(-50%) rotate(0deg); } } .select-items { position: absolute; background-color: #fff; border: 1px solid #d9d9d9; border-radius: 4px; top: 100%; left: 0; right: 0; background-color: #ffffff; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); z-index: 100; } .select-items div:hover { background-color: #bae0ff; color: #222222; font-weight: 600; font-size: 12px; } .select-items div { padding: 8px 15px; cursor: pointer; } .select-items div:first-child { border-top-left-radius: 4px; border-top-right-radius: 4px; } .select-items div:last-child { border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } .select-items div:first-child:hover { border-top-left-radius: 4px; border-top-right-radius: 4px; } .select-items div:last-child:hover { border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } } </style>
控制台日志
- 首次点击打开下拉框:
openDropdown
- 下拉框展开后点击选择框:
CLICK OUTSIDE openDropdown
问题原因
从日志能看出,点击展开的容器时,clickOutside先触发把isOpen设为false,紧接着openDropdown执行又把isOpen变回true,导致视觉上没关闭。根源是:
clickOutside用了事件捕获模式(addEventListener第三个参数为true),事件会从文档根节点往下触发,先执行clickOutside的逻辑openDropdown是元素的on:click,属于事件冒泡阶段触发,晚于捕获阶段的clickOutside- 同时在
onMount里手动调用了clickOutside(parent),又在元素上用了use:clickOutside,导致事件监听被绑定了两次
解决步骤
1. 移除事件捕获模式
修改clickOutside函数,去掉addEventListener的第三个参数true,让事件在冒泡阶段触发:
function clickOutside(node: HTMLElement) { const handleClick = (event: MouseEvent) => { if ( node && !node.contains(event.target as Node) && isOpen === true ) { console.log("CLICK OUTSIDE"); isOpen = false; console.log(""); } }; // 去掉第三个参数true,默认是冒泡模式 document.addEventListener("click", handleClick); return { destroy() { document.removeEventListener("click", handleClick); }, }; }
2. 移除重复的指令调用
删掉onMount里手动调用clickOutside(parent)的代码,因为use:clickOutside已经会自动执行指令逻辑,重复调用会绑定两次事件:
// 删掉这段代码 onMount(() => { clickOutside(parent); });
验证效果
修改后,下拉框展开时点击容器,控制台只会输出openDropdown,isOpen从true切换为false,下拉框正常关闭;点击外部区域时,clickOutside正常触发关闭下拉框。
内容的提问来源于stack exchange,提问作者jefelewis
相关产品推荐
相关产品推荐

