Skip to content

Instantly share code, notes, and snippets.

@hannesl
Last active May 4, 2018 11:32
Show Gist options
  • Save hannesl/033027eaccc9c033428c371b65e2769b to your computer and use it in GitHub Desktop.
Save hannesl/033027eaccc9c033428c371b65e2769b to your computer and use it in GitHub Desktop.
A few fixes to the Vue.js autocomplete component published here: https://alligator.io/vuejs/vue-a11y-autocomplete/
<script>
export default {
name: 'autocomplete',
props: {
items: {
type: Array,
required: false,
default: () => [],
},
isAsync: {
type: Boolean,
required: false,
default: false,
},
ariaLabelledBy: {
type: String,
required: true
}
},
data() {
return {
isOpen: false,
results: [],
search: '',
isLoading: false,
arrowCounter: 0,
activedescendant: ''
};
},
methods: {
onChange() {
this.$emit('input', this.search);
if (this.isAsync) {
this.isLoading = true;
this.arrowCounter = -1; // Reset selected row.
} else {
this.filterResults();
}
},
filterResults() {
this.results = this.items.filter((item) => {
return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
});
},
setResult(result) {
this.search = result;
this.isOpen = false;
},
onArrowDown(evt) {
if (this.isOpen && !this.isLoading) {
if (this.arrowCounter < this.results.length - 1) {
this.arrowCounter = this.arrowCounter + 1;
this.setActiveDescendant();
}
}
},
onArrowUp(e) {
e.preventDefault(); // Don't move the text cursor.
if (this.isOpen) {
if (this.arrowCounter > 0) {
this.arrowCounter = this.arrowCounter -1;
this.setActiveDescendant();
}
}
},
onEnter(e) {
e.preventDefault(); // Don't submit the form.
this.search = this.results[this.arrowCounter];
this.arrowCounter = -1;
},
handleClickOutside(evt) {
if (!this.$el.contains(evt.target)) {
this.isOpen = false;
this.arrowCounter = -1;
}
},
setActiveDescendant() {
this.activedescendant = this.getId(this.arrowCounter);
},
getId(index) {
return `result-item-${index}`;
},
isSelected(i) {
return i === this.arrowCounter;
},
},
watch: {
items: function (val, oldValue) {
// Here, I removed the if statement for comparing the old an new value.
// It's probably more efficient to always update, unless I'm missing something.
this.results = val;
this.isLoading = false;
},
},
mounted() {
document.addEventListener('click', this.handleClickOutside)
},
destroyed() {
document.removeEventListener('click', this.handleClickOutside)
}
};
</script>
</script>
<template>
<div
class="autocomplete"
role="combobox"
aria-haspopup="listbox"
aria-owns="autocomplete-results"
:aria-expanded="isOpen"
>
<input
type="text"
@input="onChange"
v-model="search"
@keydown.down="onArrowDown"
@keydown.up="onArrowUp"
@keydown.enter="onEnter"
role="searchbox"
aria-autocomplete="list"
aria-controls="autocomplete-results"
:aria-labelledby="ariaLabelledBy"
:aria-activedescendant="activedescendant"
/>
<!-- Removed @focus since the onFocus method was missing. -->
<ul
id="autocomplete-results"
v-show="isOpen"
class="autocomplete-results"
role="listbox"
>
<li
class="loading"
v-if="isLoading"
>
Loading results...
</li>
<li
v-else
v-for="(result, i) in results"
:key="i"
@click="setResult(result)"
class="autocomplete-result"
:class="{ 'is-active': isSelected(i) }"
role="option"
:id="getId(i)"
:aria-selected="isSelected(i)"
>
{{ result }}
</li>
</ul>
</div>
</template>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment