简体   繁体   English

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

[英]VueJS - Click event is not triggered as parent event prevaults

I am building a search input that fetchs data from my API and lists it in a dropdown list.我正在构建一个搜索输入,它从我的 API 中获取数据并将其列在下拉列表中。

Here is the behavior I want my component to have:这是我希望我的组件具有的行为:

  • If I start typing and my API founds data, it opens the dropdown menu and lists it.如果我开始键入并且我的 API 找到数据,它会打开下拉菜单并列出它。
    • If I click on one of the elements from the list, it is set as 'activeItem' and the dropdown list closes如果我单击列表中的一个元素,它会被设置为“activeItem”并且下拉列表会关闭
    • Else, I can click out of the component (input and dropdown list) and the dropdown list closes否则,我可以点击组件(输入和下拉列表)并关闭下拉列表
  • Else, no dropdown list appears and my input works like a regular text input否则,不会出现下拉列表,我的输入就像常规文本输入一样

My issue has to do with Event Bubbling.我的问题与事件冒泡有关。

  • My list items (from API) have a @click input that set the clicked element as the 'activeItem'.我的列表项(来自 API)有一个 @click 输入,它将被点击的元素设置为“activeItem”。
  • My input has both @focusin and @focusout events, that allow me to display or hide the dropdown list.我的输入有@focusin 和@focusout 事件,它们允许我显示或隐藏下拉列表。

I can't click the elements in the dropdown list as the @focusout event from the input is being triggered first and closes the list.我无法单击下拉列表中的元素,因为输入中的 @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"
                        target="_blank"
                    >
                      {{ 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>

Here is a look at the input:下面是输入:

With API results (can't click the elements):结果为 API(无法单击元素):

在此处输入图像描述

When no data is found:当没有找到数据时:

在此处输入图像描述

I tried:我试过:

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)"
>

在此处输入图像描述

The solution:解决方案:

relatedTarget will be null if the element you click on is not focusable.如果您单击的元素不可聚焦,则 relatedTarget 将为 null。 by adding the tabindex attribute it should make the element focusable and allow it to be set as relatedTarget.通过添加 tabindex 属性,它应该使元素可聚焦并允许将其设置为 relatedTarget。 if you actually happen to be clicking on some container or overlay element make sure the element being clicked on has that tabindex="0" added to it so you can maintain isFocus = true如果您确实碰巧点击了某个容器或覆盖元素,请确保被点击的元素添加了 tabindex="0" 以便您可以维护 isFocus = true

Thanks to @yoduh for the solution感谢@yoduh 的解决方案

The root issue looks to be how the dropdown list is being removed from the DOM as soon as the input loses focus because of the v-if on it.根本问题似乎是一旦输入因为v-if而失去焦点,下拉列表如何从 DOM 中删除。

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

This is ok to have, but you'll need to work around it by coming up with a solution that keeps isFocus true whether the focus is on the input or the dropdown.这没关系,但您需要通过提出一个解决方案来解决这个问题,无论焦点是在输入还是下拉列表上,该解决方案都能使isFocus保持为真。 I would suggest your input's @focusout to execute a method that only sets isFocus = false if the focus event's relatedTarget is not any of the dropdown items (can be determined via classname or other attribute).如果焦点事件的relatedTarget不是任何下拉项(可以通过类名或其他属性确定),我建议您输入的@focusout执行仅设置isFocus = false的方法。 One roadblock to implementing this is that some elements aren't natively focusable, like <li> items, so they won't be set as the relatedTarget, but you can make them focusable by adding the tabindex attribute.实现这一点的一个障碍是某些元素本身不是可聚焦的,例如<li>项,因此它们不会被设置为 relatedTarget,但您可以通过添加tabindex属性使它们可聚焦。 Putting it all together should look something like this:把它们放在一起应该是这个样子:

<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);
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM