VueJS - Click 事件不会作为父事件 prevault 触发

VueJS - Click event is not triggered as parent event prevaults

提问人:Gaël Duval 提问时间:2/1/2023 最后编辑:Gaël Duval 更新时间:2/1/2023 访问量:258

问:

我正在构建一个搜索输入,该输入从我的 API 获取数据并将其列在下拉列表中。

以下是我希望我的组件具有的行为:

  • 如果我开始输入并且我的 API 找到数据,它会打开下拉菜单并列出它。
    • 如果我单击列表中的某个元素,它将设置为“activeItem”,并且下拉列表将关闭
    • 否则,我可以单击组件(输入和下拉列表)并关闭下拉列表
  • 否则,不会出现下拉列表,我的输入就像常规文本输入一样工作

我的问题与事件冒泡有关。

  • 我的列表项(来自 API)有一个@click输入,将单击的元素设置为“activeItem”。
  • 我的输入同时具有@focusin和@focusout事件,这些事件允许我显示或隐藏下拉列表。

我无法单击下拉列表中的元素,因为首先触发了输入中的@focusout事件并关闭了列表。

import ...

export default {
  components: {
    ...
  },

  props: {
    ...
  },

  data() {
    return {
      results: [],
      activeItem: null,
      isFocus: false,
    }
  },

  watch: {
    modelValue: _.debounce(function (newSearchText) {
      ... API Call
    }, 350)
  },

  computed: {
    computedLabel() {
      return this.required ? this.label + '<span class="text-primary-600 font-bold ml-1">*</span>' : this.label;
    },

    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  },

  methods: {
    setActiveItem(item) {
      this.activeItem = item;

      this.$emit('selectItem', this.activeItem);
    },

    resetActiveItem() {
      this.activeItem = null;

      this.isFocus = false;
      this.results = [];

      this.$emit('selectItem', null);
    },
  },

  emits: [
    'selectItem',
    'update:modelValue',
  ],
}
</script>

<template>
  <div class="relative">
    <label
        v-if="label.length"
        class="block text-tiny font-bold tracking-wide font-medium text-black/75 mb-1 uppercase"
        v-html="computedLabel"
    ></label>

    <div :class="widthCssClass">
      <div class="relative" v-if="!activeItem">
        <div class="flex items-center text-secondary-800">
          <svg
              xmlns="http://www.w3.org/2000/svg"
              class="h-3.5 w-3.5 ml-4 absolute"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
              stroke-width="2"
          >
            <path
                stroke-linecap="round"
                stroke-linejoin="round"
                d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
            />
          </svg>

          <!-- The input that triggers the API call -->
          <input
              class="text-black py-2.5 pr-3.5 pl-10 text-black focus:ring-primary-800 focus:border-primary-800 block w-full rounded sm:text-sm border-gray-300"
              placeholder="Search for anything..."
              type="text"
              @input="$emit('update:modelValue', $event.target.value)"
              @focusin="isFocus = true"
              @focusout="isFocus = false"
          >
        </div>

       <!-- The Dropdown list -->
        <Card
            class="rounded-t-none shadow-2xl absolute w-full z-10 mt-1 overflow-y-auto max-h-48 px-0 py-0"
            v-if="isFocus && results.length"
        >
          <div class="flow-root">
            <ul role="list" class="divide-y divide-gray-200">

              <!-- API results are displayed here -->
              <li
                  v-for="(result, index) in results"
                  :key="index"
                  @click="setActiveItem(result)" <!-- The event I can't trigger -->
              >
                <div class="flex items-center space-x-4 cursor-pointer px-4 py-3">
                  <div class="flex-shrink-0">
                    <img
                        class="h-8 w-8 rounded-md ring-2 ring-lighter shadow-lg"
                        :src="result.image ?? this.$page.props.page.defaultImage.url"
                        :alt="result.title"
                    />
                  </div>
                  <div class="min-w-0 flex-1">
                    <p
                        class="truncate text-sm font-medium text-black"
                        :class="{
                          'text-primary-900 font-bold': result.id === activeItem?.id
                        }"
                    >
                      {{ result.title }}
                    </p>
                    <p class="truncate text-sm text-black/75">
                      {{ result.description }}
                    </p>
                  </div>
                  <div v-if="result.action">
                    <Link
                        :href="result.action?.url"
                        class="inline-flex items-center rounded-full border border-gray-300 bg-white px-2.5 py-0.5 text-sm font-medium leading-5 text-black/75 shadow-sm hover:bg-primary-50"
                       
                    >
                      {{ result.action?.text }}
                    </Link>
                  </div>
                </div>
              </li>
            </ul>
          </div>
        </Card>
      </div>

      <!-- Display the active element, can be ignored for this example -->
      <div v-else>
        <article class="bg-primary-50 border-2 border-primary-800 rounded-md">
          <div class="flex items-center space-x-4 px-4 py-3">
            <div class="flex-shrink-0">
              <img
                  class="h-8 w-8 rounded-md ring-2 ring-lighter shadow-lg"
                  :src="activeItem.image ?? this.$page.props.page.defaultImage.url"
                  :alt="activeItem.title"
              />
            </div>
            <div class="min-w-0 flex-1">
              <p class="truncate text-sm font-medium text-black font-bold">
                {{ activeItem.title }}
              </p>
              <p class="truncate text-sm text-black/75 whitespace-pre-wrap">
                {{ activeItem.description }}
              </p>
            </div>
            <div class="flex">
              <AppButton @click.stop="resetActiveItem();" @focusout.stop>
                <svg
                    class="w-5 h-5 text-primary-800"
                    fill="none"
                    stroke="currentColor"
                    viewBox="0 0 24 24"
                    xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                      stroke-linecap="round"
                      stroke-linejoin="round"
                      stroke-width="2"
                      d="M6 18L18 6M6 6l12 12"
                  ></path>
                </svg>
              </AppButton>
            </div>
          </div>
        </article>
      </div>
    </div>
  </div>
</template>

以下是输入:

使用 API 结果(无法单击元素):

enter image description here

未找到数据时:

enter image description here

我试过了:

handleFocusOut(e) {
    console.log(e.relatedTarget, e.target, e.currentTarget)
    // No matter where I click:
    // e.relatedTarget = null
    // e.target = <input id="search" class="...
    // e.currentTarget = <input id="search" class="...
}
...

<input
    id="search"
    class="..."
    placeholder="Search for anything..."
    type="text"
    @input="$emit('update:modelValue', $event.target.value)"
    @focusin="isFocus = true"
    @focusout="handleFocusOut($event)"
>

enter image description here

解决方案:

如果单击的元素不是,则 relatedTarget 将为 null 可聚焦。通过添加 tabindex 属性,它应该使元素 focusable 并允许将其设置为 relatedTarget。如果你真的 碰巧点击一些容器或覆盖元素,确保 被单击的元素添加了 tabindex=“0”,因此您 可以保持 isFocus = true

感谢@yoduh的解决方案

JavaScript vue.js 焦点 事件冒泡

评论

0赞 Moritz Ringler 2/1/2023
你不能把和事件放在一个同时包含输入和下拉列表的容器上吗?@focusin@focusout
0赞 Gaël Duval 2/1/2023
试过了,也没用。还尝试在专用方法中处理焦点状态,但 event.target 和 event.currentTarget 返回输入和我放置事件的 div。每次。无法查看用户是否单击了组件外部的元素。我在这里遗漏了一些东西。

答:

2赞 yoduh 2/1/2023 #1

根本问题似乎是一旦输入因为 on 而失去焦点,下拉列表是如何从 DOM 中删除的。v-if

<Card
  v-if="isFocus && results.length"
>

这是可以的,但你需要通过提出一个解决方案来解决它,无论重点是输入还是下拉列表,都保持真实。我建议您的输入执行一个方法,该方法仅在焦点事件的 relatedTarget 不是任何下拉项(可以通过类名或其他属性确定)时设置。实现此目的的一个障碍是,某些元素(如 items)本身不可聚焦,因此它们不会设置为 relatedTarget,但可以通过添加 tabindex 属性使它们可聚焦。把它们放在一起应该看起来像这样:isFocus@focusoutisFocus = false<li>

<input
  type="text"
  @input="$emit('update:modelValue', $event.target.value)"
  @focusin="isFocus = true"
  @focusout="loseFocus($event)"
/>

...

<li
  v-for="(result, index) in results"
  :key="index"
  class="listResult"
  tabindex="0"
  @click="setActiveItem(result)"
>
loseFocus(event) {
  if (event.relatedTarget?.className !== 'listResult') {
    this.isFocus = false;
  }
}
setActiveItem(item) {
  this.activeItem = item;
  this.isFocus = false;
  this.$emit('selectItem', this.activeItem);
}

评论

0赞 Gaël Duval 2/1/2023
谢谢你的回答。event.relatedTarget 在我的情况下始终为 null。event.target 和 event.currentTarget 返回输入文本,无论我单击在哪里。
0赞 yoduh 2/1/2023
relatedTarget如果单击的元素不可聚焦,则为 null。通过添加该属性,它应该使元素可聚焦并允许将其设置为 。如果您碰巧单击了某个容器或覆盖元素,请确保被单击的元素已添加到其中,以便您可以维护tabindexrelatedTargettabindex="0"isFocus = true
0赞 Gaël Duval 2/1/2023
该死的,这个tabindex的东西太疯狂了。我不知道它会阻止我的元素被设置为 relatedTarget。谢谢伙计,它解决了我的问题。