You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>

控制台日志

  1. 首次点击打开下拉框:
openDropdown
  1. 下拉框展开后点击选择框:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 01:22:06