-
-
Save Thanood/a2c9282dafa4b3fb5c98b31004765996 to your computer and use it in GitHub Desktop.
Aurelia Accessible Autocomplete with Filtering
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<require from="./autocomplete"></require> | |
<form> | |
<label class="form-component"> | |
Country:<br/> | |
<autocomplete service.bind="suggestionService.country" | |
value.bind="model.country" | |
placeholder="Enter country..." | |
change.delegate="model.city = null"> | |
</autocomplete> | |
</label> | |
<label class="form-component"> | |
City${model.country ? ' (' + countryIndex[model.country].length + ' choices)' : ''}:<br/> | |
<autocomplete service.bind="suggestionService.city" | |
value.bind="model.city" | |
placeholder="Enter city..."> | |
<template replace-part="suggestion"> | |
<span style="font-style: italic">${suggestion}</span> | |
</template> | |
</autocomplete> | |
</label> | |
</form> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class App { | |
model = { | |
country: null, | |
city: null | |
}; | |
suggestionService = null; | |
countryIndex = null; | |
activate() { | |
return fetch('https://rawgit.com/jdanyow/97c8bdfaf7c7e0a2b8920e8789b46309/raw/d68abc0a90a6eb1a7e16362cdb7bf8832738665f/countries.json') | |
.then(response => response.json()) | |
.then(countries => { | |
this.countryIndex = countries; | |
this.suggestionService = new SuggestionService(this.model, countries); | |
}); | |
} | |
} | |
export class SuggestionService { | |
constructor(model, countries) { | |
this.model = model; | |
this.countryIndex = countries; | |
this.countries = Object.keys(countries); | |
} | |
country = { | |
suggest: value => { | |
if (value === '') { | |
return Promise.resolve([]); | |
} | |
value = value.toLowerCase(); | |
const suggestions = this.countries | |
.filter(x => x.toLowerCase().indexOf(value) === 0) | |
.sort(); | |
return Promise.resolve(suggestions); | |
}, | |
getName: suggestion => suggestion | |
}; | |
city = { | |
suggest: value => { | |
if (value === '' || this.model.country === null) { | |
return Promise.resolve([]); | |
} | |
value = value.toLowerCase(); | |
let suggestions = this.countryIndex[this.model.country] | |
.filter(x => x.toLowerCase().indexOf(value) === 0); | |
suggestions = suggestions.filter((x, i) => suggestions.indexOf(x) === i) | |
.sort(); | |
return Promise.resolve(suggestions); | |
}, | |
getName: suggestion => suggestion | |
}; | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
autocomplete { | |
display: inline-block; | |
} | |
autocomplete .suggestions { | |
list-style-type: none; | |
cursor: default; | |
padding: 0; | |
margin: 0; | |
border: 1px solid #ccc; | |
background: #fff; | |
box-shadow: -1px 1px 3px rgba(0,0,0,.1); | |
position: absolute; | |
z-index: 9999; | |
max-height: 15rem; | |
overflow: hidden; | |
overflow-y: auto; | |
box-sizing: border-box; | |
} | |
autocomplete .suggestion { | |
padding: 0 .3rem; | |
line-height: 1.5rem; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
color: #333; | |
} | |
autocomplete .suggestion:hover, | |
autocomplete .suggestion.selected { | |
background: #f0f0f0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<require from="./autocomplete.css"></require> | |
<input type="text" autocomplete="off" | |
aria-autocomplete="list" | |
aria-expanded.bind="expanded" | |
aria-owns.one-time="'au-autocomplate-' + id + '-suggestions'" | |
aria-activedescendant.bind="index >= 0 ? 'au-autocomplate-' + id + '-suggestion-' + index : ''" | |
id.one-time="'au-autocomplete-' + id" | |
placeholder.bind="placeholder" | |
value.bind="inputValue & debounce:delay" | |
keydown.delegate="keydown($event.which)" | |
blur.trigger="blur()"> | |
<ul class="suggestions" role="listbox" | |
if.bind="expanded" | |
id.one-time="'au-autocomplate-' + id + '-suggestions'" | |
ref="suggestionsUL"> | |
<li repeat.for="suggestion of suggestions" | |
id.one-time="'au-autocomplate-' + id + '-suggestion-' + $index" | |
role="option" | |
class-name.bind="($index === index ? 'selected' : '') + ' suggestion'" | |
mousedown.delegate="suggestionClicked(suggestion)"> | |
<template replaceable part="suggestion"> | |
${suggestion} | |
</template> | |
</li> | |
</ul> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {bindingMode, observable} from 'aurelia-binding'; | |
import {bindable} from 'aurelia-templating'; | |
import {inject} from 'aurelia-dependency-injection'; | |
import {DOM} from 'aurelia-pal'; | |
let nextID = 0; | |
@inject(Element) | |
export class Autocomplete { | |
@bindable service; | |
@bindable({ defaultBindingMode: bindingMode.twoWay }) value; | |
@bindable placeholder = ''; | |
@bindable delay = 300; | |
id = nextID++; | |
expanded = false; | |
@observable inputValue = ''; | |
updatingInput = false; | |
suggestions = []; | |
index = -1; | |
suggestionsUL = null; | |
userInput = ''; | |
constructor(element) { | |
this.element = element; | |
} | |
display(name) { | |
this.updatingInput = true; | |
this.inputValue = name; | |
this.updatingInput = false; | |
} | |
getName(suggestion) { | |
if (suggestion == null) { | |
return ''; | |
} | |
return this.service.getName(suggestion); | |
} | |
collapse() { | |
this.expanded = false; | |
this.index = -1; | |
} | |
select(suggestion, notify) { | |
this.value = suggestion; | |
const name = this.getName(this.value); | |
this.userInput = name; | |
this.display(name); | |
this.collapse(); | |
if (notify) { | |
const event = DOM.createCustomEvent('change', { bubbles: true }); | |
event.value = suggestion; | |
event.autocomplete = this; | |
this.element.dispatchEvent(event); | |
} | |
} | |
valueChanged() { | |
this.select(this.value, false); | |
} | |
inputValueChanged(value) { | |
if (this.updatingInput) { | |
return; | |
} | |
this.userInput = value; | |
if (value === '') { | |
this.value = null; | |
this.collapse(); | |
return; | |
} | |
this.service.suggest(value) | |
.then(suggestions => { | |
this.index = -1; | |
this.suggestions.splice(0, this.suggestions.length, ...suggestions); | |
if (suggestions.length === 1 && suggestions[0] !== this.value) { | |
this.select(suggestions[0], true); | |
} else if (suggestions.length === 0) { | |
this.collapse(); | |
} else { | |
this.expanded = true; | |
} | |
}); | |
} | |
scroll() { | |
const ul = this.suggestionsUL; | |
const li = ul.children.item(this.index === -1 ? 0 : this.index); | |
if (li.offsetTop + li.offsetHeight > ul.offsetHeight) { | |
ul.scrollTop += li.offsetHeight; | |
} else if(li.offsetTop < ul.scrollTop) { | |
ul.scrollTop = li.offsetTop; | |
} | |
} | |
keydown(key) { | |
if (!this.expanded) { | |
return true; | |
} | |
// down | |
if (key === 40) { | |
if (this.index < this.suggestions.length - 1) { | |
this.index++; | |
this.display(this.getName(this.suggestions[this.index])); | |
} else { | |
this.index = -1; | |
this.display(this.userInput); | |
} | |
this.scroll(); | |
return; | |
} | |
// up | |
if (key === 38) { | |
if (this.index === -1) { | |
this.index = this.suggestions.length - 1; | |
this.display(this.getName(this.suggestions[this.index])); | |
} else if (this.index > 0) { | |
this.index--; | |
this.display(this.getName(this.suggestions[this.index])); | |
} else { | |
this.index = -1; | |
this.display(this.userInput); | |
} | |
this.scroll(); | |
return; | |
} | |
// escape | |
if (key === 27) { | |
this.display(this.userInput); | |
this.collapse(); | |
return; | |
} | |
// enter | |
if (key === 13) { | |
if (this.index >= 0) { | |
this.select(this.suggestions[this.index], true); | |
} | |
return; | |
} | |
return true; | |
} | |
blur() { | |
this.select(this.value, false); | |
this.element.dispatchEvent(DOM.createCustomEvent('blur')); | |
} | |
suggestionClicked(suggestion) { | |
this.select(suggestion, true); | |
} | |
focus() { | |
this.element.firstElementChild.focus(); | |
} | |
} | |
// aria-activedescendant | |
// https://webaccessibility.withgoogle.com/unit?unit=6&lesson=13 | |
// https://www.w3.org/TR/wai-aria/states_and_properties#aria-autocomplete | |
// https://www.w3.org/TR/wai-aria/roles#combobox |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<title>Aurelia</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css"> | |
<style> | |
body { | |
padding: 20px; | |
} | |
.form-component { | |
display: block; | |
margin-bottom: 20px; | |
} | |
</style> | |
</head> | |
<body aurelia-app> | |
<h1>Loading...</h1> | |
<script src="https://jdanyow.github.io/rjs-bundle/node_modules/requirejs/require.js"></script> | |
<script src="https://jdanyow.github.io/rjs-bundle/config.js"></script> | |
<script src="https://jdanyow.github.io/rjs-bundle/bundles/aurelia.js"></script> | |
<script src="https://jdanyow.github.io/rjs-bundle/bundles/babel.js"></script> | |
<script> | |
require(['aurelia-bootstrapper']); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment