Search code examples
vue.jssassnavigationscrollbararrow-keys

Disable scrolling when navigating with arrows up and down


I have a serach bar where users can filter a list of products and navigate through this list using arrows up and down.The filtering and navigation work ok, but there is a side effect of using the arrows that I haven't been able to surpress: using them makes all the page scroll (if it's large enough to have a scrollbar), and if the page hasn't got the scrollbar, the list won't scroll properly either (as shown on second and third images).

My search bar looks like this:

The expanded search bar list

In the two images below you can see that when using arrow down first, the element is partially hidden; and when using the arrow up first (the list starts being navigated bottom to top, then the element is hidden entirelly from the screen.

Here you can see the first time I navigate with arrow down, the list goes up making the content disapear a little

Here, I am navigating starting from the bottom, and the content is out of the view

My .vue file looks like this:

<template>
  <form class="ui-search-bar" role="search" aria-label="Produtos à venda">
    <input
      aria-label="Pesquisar por produto"
      class="ui-input"
      @focus="enableArrowNavigation"
      placeholder="Pesquisar"
      type="text"
      v-model="search"
      @keydown.up="navigateUp"
      @keydown.down="navigateDown"
    />
    <div class="ui-search-bar__wrapper">
      <div>
        <ul v-if="copied_parents.length > 0">
          <li class="ui-search-bar__grandpa" :key="i" v-for="(parent, i) in copied_parents">
            <p>{{ parent.title }}</p>
            <ul class="ui-search-bar__parent" :key="j" v-for="(child, j) in parent[children_key]">
              <li
                class="ui-search-bar__children"
                :data-category="child.title"
                :key="k"
                v-for="(grandchild, k) in child[grandchildren_key]"
              >
                <a
                  :href="grandchild.route"
                  @keydown.up="navigateUp"
                  @keydown.down="navigateDown"
                  tabindex="-1"
                  v-if="grandchild.quantity > 0"
                >
                  {{ grandchild.title }}
                </a>
                <p v-else>
                  {{ grandchild.title }} -
                  <span> Esgotado </span>
                </p>
              </li>
            </ul>
          </li>
        </ul>
        <p v-else-if="parents.length <= 0">Não há produtos disponíveis para compra.</p>
        <p v-else>Não há resultados para esta pesquisa.</p>
      </div>
    </div>
  </form>
</template>

<script>
export default {
  props: ["children_key", "grandchildren_key", "parents"],
  data() {
    return {
      amount_of_children: 0,
      copied_parents: this.makeParentCopy(),
      search: "",
      selected_child: 0,
    };
  },
  watch: {
    search(value) {
      if (value.length >= 3) {
        this.copied_parents = this.filterItems(value);
      } else {
        this.copied_parents = this.makeParentCopy();
      }
    },
  },
  mounted() {
    this.amount_of_children = document.querySelectorAll(".ui-search-bar__children a").length;
  },
  methods: {
    makeParentCopy() {
      return JSON.parse(JSON.stringify(this.parents));
    },
    filterItems(needle) {
      this.copied_parents = this.makeParentCopy();

      const filtered_items = this.copied_parents.filter((parent) => {
        const children = parent[this.children_key].filter((child) => {
          const grandchildren = child[this.grandchildren_key].filter((product) => {
            return product.title.toLowerCase().includes(needle.toLowerCase());
          });
          child[this.grandchildren_key] = grandchildren;
          if (grandchildren.length > 0) {
            return child;
          }
        });
        parent[this.children_key] = children;
        if (children.length > 0) {
          return children;
        }
      });

      return filtered_items;
    },
    enableArrowNavigation() {
      this.selected_child = 0;
    },
    navigate() {
      document.querySelectorAll(".ui-search-bar__children a")[this.selected_child - 1]?.focus();
    },
    navigateDown() {
      if (this.selected_child === this.amount_of_children) {
        this.selected_child = 1;
      } else {
        this.selected_child++;
      }
      this.navigate();
    },
    navigateUp() {
      if (this.selected_child > 1) {
        this.selected_child--;
      } else if (this.selected_child === 1) {
        this.selected_child = this.amount_of_children;
      }
      this.navigate();
    },
  },
};
</script>

And the .sass looks like this:

@use "../abstracts/colours" as c;
@use "../abstracts/shorthands" as s;
@use "../abstracts/media" as m;

.ui-search-bar {
  padding: 10px 10px 0;
  position: relative;
  width: 70%;
  z-index: 100;
  & input {
    position: relative;
    width: 100%;
    z-index: 110;
  }
  &:focus-within > .ui-search-bar__wrapper {
    display: flex;
    flex-direction: column;
  }
  &__children {
    position: relative;
    width: 100%;
    & a,
    & p {
      border-radius: 6px;
      color: c.$darker-gray;
      display: inline-block;
      @include s.font(14);
      padding: 1px 5px;
      text-decoration: none;
      width: 100%;
    }
    & p {
      color: c.$dark-gray;
      & > span {
        color: c.$red;
        @include s.font(16);
        font-variant-caps: all-petite-caps;
      }
    }
    a:focus,
    a:hover {
      @include m.desktop-up {
        background-color: c.$gray;
        outline-color: c.$gray;
        outline-style: solid;
        outline-width: 2px;
      }
    }
    &:first-of-type::before {
      color: c.$dark-gray;
      content: attr(data-category);
      @include s.font(14);
      position: absolute;
      right: 0;
      top: 0;
    }
  }
  &__grandpa {
    & > p {
      color: c.$lead;
      @include s.font(18);
    }
    &:not(:first-of-type) {
      margin-top: 15px;
    }
    &:last-of-type() {
      margin-bottom: 5px;
    }
  }
  &__parent {
    border-top: 1px solid c.$gray;
    padding: 5px 0;
  }
  &__wrapper {
    background-color: white;
    border-radius: 8px;
    box-shadow: 0px 0px 12px -5px #1d283a;
    display: none;
    left: 0;
    max-height: 500px;
    padding-top: 60px;
    position: absolute;
    top: 0;
    width: 100%;
    & > div {
      overflow-y: auto;
      padding-inline: 10px;
      width: 100%;
    }
    & > div > ul {
      width: 100%;
    }
    & > div > p {
      color: c.$dark-gray;
      @include s.font(18, 500);
      padding: 10px 20px;
      text-align: center;
    }
  }
}

I haven't really found anything that could help me with this. I guess it's because I don't really know if this is a css or js problem, and that's why I come to you: maybe someone will have a hint what could be causing this and hopefully a solution too.


Solution

  • You need to prevent the default browser behavior by calling event.preventDefault().

    In Vue.js you can do this directly in the template like so:

    <input
        ...
        @keydown.up.prevent="navigateUp"
        @keydown.down.prevent="navigateDown"
    />