@github/auto-complete-element
Advanced tools
Comparing version 0.1.2 to 0.2.0
@@ -54,2 +54,16 @@ function debounce(callback, wait) { | ||
function scrollTo(container, target) { | ||
if (!inViewport(container, target)) { | ||
container.scrollTop = target.offsetTop; | ||
} | ||
} | ||
function inViewport(container, element) { | ||
var scrollTop = container.scrollTop; | ||
var containerBottom = scrollTop + container.clientHeight; | ||
var top = element.offsetTop; | ||
var bottom = top + element.clientHeight; | ||
return top >= scrollTop && bottom <= containerBottom; | ||
} | ||
var classCallCheck = function (instance, Constructor) { | ||
@@ -103,4 +117,6 @@ if (!(instance instanceof Constructor)) { | ||
var ctrlBindings = navigator.userAgent.match(/Macintosh/); | ||
var Autocomplete = function () { | ||
function Autocomplete(container, input, results, list) { | ||
function Autocomplete(container, input, results) { | ||
classCallCheck(this, Autocomplete); | ||
@@ -111,3 +127,2 @@ | ||
this.results = results; | ||
this.list = list; | ||
@@ -121,2 +136,3 @@ this.results.hidden = true; | ||
this.onInputChange = debounce(this.onInputChange.bind(this), 300); | ||
this.onResultsClick = this.onResultsClick.bind(this); | ||
this.onResultsMouseDown = this.onResultsMouseDown.bind(this); | ||
@@ -132,2 +148,3 @@ this.onInputBlur = this.onInputBlur.bind(this); | ||
this.results.addEventListener('mousedown', this.onResultsMouseDown); | ||
this.results.addEventListener('click', this.onResultsClick); | ||
} | ||
@@ -143,8 +160,83 @@ | ||
this.results.removeEventListener('mousedown', this.onResultsMouseDown); | ||
this.results.removeEventListener('click', this.onResultsClick); | ||
} | ||
}, { | ||
key: 'sibling', | ||
value: function sibling(next) { | ||
var options = Array.from(this.results.querySelectorAll('[role="option"]')); | ||
var selected = this.results.querySelector('[aria-selected="true"]'); | ||
var index = options.indexOf(selected); | ||
var sibling = next ? options[index + 1] : options[index - 1]; | ||
var def = next ? options[0] : options[options.length - 1]; | ||
return sibling || def; | ||
} | ||
}, { | ||
key: 'select', | ||
value: function select(target) { | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
try { | ||
for (var _iterator = this.results.querySelectorAll('[aria-selected="true"]')[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var el = _step.value; | ||
el.removeAttribute('aria-selected'); | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
} | ||
target.setAttribute('aria-selected', 'true'); | ||
this.input.setAttribute('aria-activedescendant', target.id); | ||
scrollTo(this.results, target); | ||
} | ||
}, { | ||
key: 'onKeydown', | ||
value: function onKeydown(event) { | ||
if (event.key === 'Escape') { | ||
this.container.open = false; | ||
switch (event.key) { | ||
case 'Escape': | ||
this.container.open = false; | ||
event.preventDefault(); | ||
break; | ||
case 'ArrowDown': | ||
this.select(this.sibling(true)); | ||
event.preventDefault(); | ||
break; | ||
case 'ArrowUp': | ||
this.select(this.sibling(false)); | ||
event.preventDefault(); | ||
break; | ||
case 'n': | ||
if (ctrlBindings && event.ctrlKey) { | ||
this.select(this.sibling(true)); | ||
event.preventDefault(); | ||
} | ||
break; | ||
case 'p': | ||
if (ctrlBindings && event.ctrlKey) { | ||
this.select(this.sibling(false)); | ||
event.preventDefault(); | ||
} | ||
break; | ||
case 'Enter': | ||
{ | ||
var selected = this.results.querySelector('[aria-selected="true"]'); | ||
if (selected) { | ||
this.commit(selected); | ||
event.preventDefault(); | ||
} | ||
} | ||
break; | ||
} | ||
@@ -164,2 +256,17 @@ } | ||
}, { | ||
key: 'commit', | ||
value: function commit(selected) { | ||
if (selected.getAttribute('aria-disabled') === 'true') return; | ||
var value = selected.getAttribute('data-autocomplete-value') || selected.textContent; | ||
this.container.value = value; | ||
this.container.open = false; | ||
} | ||
}, { | ||
key: 'onResultsClick', | ||
value: function onResultsClick(event) { | ||
if (!(event.target instanceof Element)) return; | ||
var selected = event.target.closest('[role="option"]'); | ||
if (selected) this.commit(selected); | ||
} | ||
}, { | ||
key: 'onResultsMouseDown', | ||
@@ -181,2 +288,31 @@ value: function onResultsMouseDown() { | ||
}, { | ||
key: 'identifyOptions', | ||
value: function identifyOptions() { | ||
var id = 0; | ||
var _iteratorNormalCompletion2 = true; | ||
var _didIteratorError2 = false; | ||
var _iteratorError2 = undefined; | ||
try { | ||
for (var _iterator2 = this.results.querySelectorAll('[role="option"]:not([id])')[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { | ||
var el = _step2.value; | ||
el.id = this.results.id + '-option-' + id++; | ||
} | ||
} catch (err) { | ||
_didIteratorError2 = true; | ||
_iteratorError2 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion2 && _iterator2.return) { | ||
_iterator2.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError2) { | ||
throw _iteratorError2; | ||
} | ||
} | ||
} | ||
} | ||
}, { | ||
key: 'fetchResults', | ||
@@ -202,3 +338,4 @@ value: function fetchResults() { | ||
fragment(this.input, url.toString()).then(function (html) { | ||
_this2.list.innerHTML = html; | ||
_this2.results.innerHTML = html; | ||
_this2.identifyOptions(); | ||
var hasResults = !!_this2.results.querySelector('[data-autocomplete-value]'); | ||
@@ -218,2 +355,3 @@ _this2.container.open = hasResults; | ||
positionBelow(this.input, this.results); | ||
this.container.setAttribute('aria-expanded', 'true'); | ||
this.container.dispatchEvent(new CustomEvent('toggle', { detail: { input: this.input, results: this.results } })); | ||
@@ -226,2 +364,4 @@ } | ||
this.results.hidden = true; | ||
this.input.removeAttribute('aria-activedescendant'); | ||
this.container.setAttribute('aria-expanded', 'false'); | ||
this.container.dispatchEvent(new CustomEvent('toggle', { detail: { input: this.input, results: this.results } })); | ||
@@ -277,7 +417,18 @@ } | ||
value: function connectedCallback() { | ||
var input = this.querySelector('input[slot="field"]'); | ||
var results = this.querySelector('[slot="popup"]'); | ||
var list = this.querySelector('[slot="results"]'); | ||
if (!(input instanceof HTMLInputElement) || !results || !list) return; | ||
state.set(this, new Autocomplete(this, input, results, list)); | ||
var owns = this.getAttribute('aria-owns'); | ||
if (!owns) return; | ||
var input = this.querySelector('input'); | ||
var results = document.getElementById(owns); | ||
if (!(input instanceof HTMLInputElement) || !results) return; | ||
state.set(this, new Autocomplete(this, input, results)); | ||
this.setAttribute('role', 'combobox'); | ||
this.setAttribute('aria-haspopup', 'listbox'); | ||
this.setAttribute('aria-expanded', 'false'); | ||
input.setAttribute('aria-autocomplete', 'list'); | ||
input.setAttribute('aria-controls', owns); | ||
results.setAttribute('role', 'listbox'); | ||
} | ||
@@ -284,0 +435,0 @@ }, { |
@@ -60,2 +60,16 @@ (function (global, factory) { | ||
function scrollTo(container, target) { | ||
if (!inViewport(container, target)) { | ||
container.scrollTop = target.offsetTop; | ||
} | ||
} | ||
function inViewport(container, element) { | ||
var scrollTop = container.scrollTop; | ||
var containerBottom = scrollTop + container.clientHeight; | ||
var top = element.offsetTop; | ||
var bottom = top + element.clientHeight; | ||
return top >= scrollTop && bottom <= containerBottom; | ||
} | ||
var classCallCheck = function (instance, Constructor) { | ||
@@ -109,4 +123,6 @@ if (!(instance instanceof Constructor)) { | ||
var ctrlBindings = navigator.userAgent.match(/Macintosh/); | ||
var Autocomplete = function () { | ||
function Autocomplete(container, input, results, list) { | ||
function Autocomplete(container, input, results) { | ||
classCallCheck(this, Autocomplete); | ||
@@ -117,3 +133,2 @@ | ||
this.results = results; | ||
this.list = list; | ||
@@ -127,2 +142,3 @@ this.results.hidden = true; | ||
this.onInputChange = debounce(this.onInputChange.bind(this), 300); | ||
this.onResultsClick = this.onResultsClick.bind(this); | ||
this.onResultsMouseDown = this.onResultsMouseDown.bind(this); | ||
@@ -138,2 +154,3 @@ this.onInputBlur = this.onInputBlur.bind(this); | ||
this.results.addEventListener('mousedown', this.onResultsMouseDown); | ||
this.results.addEventListener('click', this.onResultsClick); | ||
} | ||
@@ -149,8 +166,83 @@ | ||
this.results.removeEventListener('mousedown', this.onResultsMouseDown); | ||
this.results.removeEventListener('click', this.onResultsClick); | ||
} | ||
}, { | ||
key: 'sibling', | ||
value: function sibling(next) { | ||
var options = Array.from(this.results.querySelectorAll('[role="option"]')); | ||
var selected = this.results.querySelector('[aria-selected="true"]'); | ||
var index = options.indexOf(selected); | ||
var sibling = next ? options[index + 1] : options[index - 1]; | ||
var def = next ? options[0] : options[options.length - 1]; | ||
return sibling || def; | ||
} | ||
}, { | ||
key: 'select', | ||
value: function select(target) { | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
try { | ||
for (var _iterator = this.results.querySelectorAll('[aria-selected="true"]')[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var el = _step.value; | ||
el.removeAttribute('aria-selected'); | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
} | ||
target.setAttribute('aria-selected', 'true'); | ||
this.input.setAttribute('aria-activedescendant', target.id); | ||
scrollTo(this.results, target); | ||
} | ||
}, { | ||
key: 'onKeydown', | ||
value: function onKeydown(event) { | ||
if (event.key === 'Escape') { | ||
this.container.open = false; | ||
switch (event.key) { | ||
case 'Escape': | ||
this.container.open = false; | ||
event.preventDefault(); | ||
break; | ||
case 'ArrowDown': | ||
this.select(this.sibling(true)); | ||
event.preventDefault(); | ||
break; | ||
case 'ArrowUp': | ||
this.select(this.sibling(false)); | ||
event.preventDefault(); | ||
break; | ||
case 'n': | ||
if (ctrlBindings && event.ctrlKey) { | ||
this.select(this.sibling(true)); | ||
event.preventDefault(); | ||
} | ||
break; | ||
case 'p': | ||
if (ctrlBindings && event.ctrlKey) { | ||
this.select(this.sibling(false)); | ||
event.preventDefault(); | ||
} | ||
break; | ||
case 'Enter': | ||
{ | ||
var selected = this.results.querySelector('[aria-selected="true"]'); | ||
if (selected) { | ||
this.commit(selected); | ||
event.preventDefault(); | ||
} | ||
} | ||
break; | ||
} | ||
@@ -170,2 +262,17 @@ } | ||
}, { | ||
key: 'commit', | ||
value: function commit(selected) { | ||
if (selected.getAttribute('aria-disabled') === 'true') return; | ||
var value = selected.getAttribute('data-autocomplete-value') || selected.textContent; | ||
this.container.value = value; | ||
this.container.open = false; | ||
} | ||
}, { | ||
key: 'onResultsClick', | ||
value: function onResultsClick(event) { | ||
if (!(event.target instanceof Element)) return; | ||
var selected = event.target.closest('[role="option"]'); | ||
if (selected) this.commit(selected); | ||
} | ||
}, { | ||
key: 'onResultsMouseDown', | ||
@@ -187,2 +294,31 @@ value: function onResultsMouseDown() { | ||
}, { | ||
key: 'identifyOptions', | ||
value: function identifyOptions() { | ||
var id = 0; | ||
var _iteratorNormalCompletion2 = true; | ||
var _didIteratorError2 = false; | ||
var _iteratorError2 = undefined; | ||
try { | ||
for (var _iterator2 = this.results.querySelectorAll('[role="option"]:not([id])')[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { | ||
var el = _step2.value; | ||
el.id = this.results.id + '-option-' + id++; | ||
} | ||
} catch (err) { | ||
_didIteratorError2 = true; | ||
_iteratorError2 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion2 && _iterator2.return) { | ||
_iterator2.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError2) { | ||
throw _iteratorError2; | ||
} | ||
} | ||
} | ||
} | ||
}, { | ||
key: 'fetchResults', | ||
@@ -208,3 +344,4 @@ value: function fetchResults() { | ||
fragment(this.input, url.toString()).then(function (html) { | ||
_this2.list.innerHTML = html; | ||
_this2.results.innerHTML = html; | ||
_this2.identifyOptions(); | ||
var hasResults = !!_this2.results.querySelector('[data-autocomplete-value]'); | ||
@@ -224,2 +361,3 @@ _this2.container.open = hasResults; | ||
positionBelow(this.input, this.results); | ||
this.container.setAttribute('aria-expanded', 'true'); | ||
this.container.dispatchEvent(new CustomEvent('toggle', { detail: { input: this.input, results: this.results } })); | ||
@@ -232,2 +370,4 @@ } | ||
this.results.hidden = true; | ||
this.input.removeAttribute('aria-activedescendant'); | ||
this.container.setAttribute('aria-expanded', 'false'); | ||
this.container.dispatchEvent(new CustomEvent('toggle', { detail: { input: this.input, results: this.results } })); | ||
@@ -283,7 +423,18 @@ } | ||
value: function connectedCallback() { | ||
var input = this.querySelector('input[slot="field"]'); | ||
var results = this.querySelector('[slot="popup"]'); | ||
var list = this.querySelector('[slot="results"]'); | ||
if (!(input instanceof HTMLInputElement) || !results || !list) return; | ||
state.set(this, new Autocomplete(this, input, results, list)); | ||
var owns = this.getAttribute('aria-owns'); | ||
if (!owns) return; | ||
var input = this.querySelector('input'); | ||
var results = document.getElementById(owns); | ||
if (!(input instanceof HTMLInputElement) || !results) return; | ||
state.set(this, new Autocomplete(this, input, results)); | ||
this.setAttribute('role', 'combobox'); | ||
this.setAttribute('aria-haspopup', 'listbox'); | ||
this.setAttribute('aria-expanded', 'false'); | ||
input.setAttribute('aria-autocomplete', 'list'); | ||
input.setAttribute('aria-controls', owns); | ||
results.setAttribute('role', 'listbox'); | ||
} | ||
@@ -290,0 +441,0 @@ }, { |
{ | ||
"name": "@github/auto-complete-element", | ||
"version": "0.1.2", | ||
"version": "0.2.0", | ||
"description": "Auto-complete input values from server results", | ||
@@ -5,0 +5,0 @@ "repository": "github/auto-complete-element", |
@@ -18,10 +18,17 @@ # <auto-complete> element | ||
```html | ||
<auto-complete src="/users/search"> | ||
<input slot="field" type="text" data-autocomplete-autofocus> | ||
<div slot="popup"> | ||
<ul slot="results"></ul> | ||
</div> | ||
<auto-complete src="/users/search" aria-owns="users-popup"> | ||
<input type="text" data-autocomplete-autofocus> | ||
<ul id="users-popup"></ul> | ||
</auto-complete> | ||
``` | ||
The server response should include the items that matched the search query. | ||
```html | ||
<li role="option" data-autocomplete-value="@hubot">Hubot</li> | ||
<li role="option" data-autocomplete-value="@bender">Bender</li> | ||
<li role="option" data-autocomplete-value="@bb-8">BB-8</li> | ||
<li role="option" data-autocomplete-value="@r2d2" aria-disabled="true">R2-D2 (powered down)</li> | ||
``` | ||
## Browser support | ||
@@ -28,0 +35,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
35468
890
51