Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@github/auto-complete-element

Package Overview
Dependencies
Maintainers
19
Versions
35
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@github/auto-complete-element - npm Package Compare versions

Comparing version 3.0.2 to 3.1.0

validator.js

9

dist/auto-complete-element.d.ts
export default class AutocompleteElement extends HTMLElement {
constructor();
connectedCallback(): void;

@@ -14,1 +13,9 @@ disconnectedCallback(): void;

}
declare global {
interface Window {
AutocompleteElement: typeof AutocompleteElement;
}
interface HTMLElementTagNameMap {
'auto-complete': AutocompleteElement;
}
}

10

dist/auto-complete-element.js

@@ -5,5 +5,2 @@ import AutocompleteEvent from './auto-complete-event';

export default class AutocompleteElement extends HTMLElement {
constructor() {
super();
}
connectedCallback() {

@@ -17,3 +14,4 @@ const listId = this.getAttribute('for');

return;
state.set(this, new Autocomplete(this, input, results));
const autoselectEnabled = this.getAttribute('data-autoselect') === 'true';
state.set(this, new Autocomplete(this, input, results, autoselectEnabled));
results.setAttribute('role', 'listbox');

@@ -76,1 +74,5 @@ }

}
if (!window.customElements.get('auto-complete')) {
window.AutocompleteElement = AutocompleteElement;
window.customElements.define('auto-complete', AutocompleteElement);
}

@@ -8,12 +8,18 @@ import type AutocompleteElement from './auto-complete-element';

combobox: Combobox;
feedback: HTMLElement | null;
autoselectEnabled: boolean;
clientOptions: NodeListOf<HTMLElement> | null;
clearButton: HTMLElement | null;
interactingWithList: boolean;
constructor(container: AutocompleteElement, input: HTMLInputElement, results: HTMLElement);
constructor(container: AutocompleteElement, input: HTMLInputElement, results: HTMLElement, autoselectEnabled?: boolean);
destroy(): void;
handleClear(event: Event): void;
onKeydown(event: KeyboardEvent): void;
onInputFocus(): void;
onInputBlur(): void;
onCommit({ target }: Event): void;
onCommit({ target }: Pick<Event, 'target'>): void;
onResultsMouseDown(): void;
onInputChange(): void;
identifyOptions(): void;
updateFeedbackForScreenReaders(inputString: string): void;
fetchResults(): void;

@@ -20,0 +26,0 @@ open(): void;

import debounce from './debounce';
import { fragment } from './send';
import Combobox from '@github/combobox-nav';
const SCREEN_READER_DELAY = window.testScreenReaderDelay || 100;
export default class Autocomplete {
constructor(container, input, results) {
constructor(container, input, results, autoselectEnabled = false) {
var _a;
this.container = container;

@@ -10,3 +12,20 @@ this.input = input;

this.combobox = new Combobox(input, results);
this.feedback = document.getElementById(`${this.results.id}-feedback`);
this.autoselectEnabled = autoselectEnabled;
this.clearButton = document.getElementById(`${this.input.id || this.input.name}-clear`);
this.clientOptions = results.querySelectorAll('[role=option]');
if (this.feedback) {
this.feedback.setAttribute('aria-live', 'polite');
this.feedback.setAttribute('aria-atomic', 'true');
}
if (this.clearButton && !this.clearButton.getAttribute('aria-label')) {
const labelElem = document.querySelector(`label[for="${this.input.name}"]`);
this.clearButton.setAttribute('aria-label', `clear:`);
this.clearButton.setAttribute('aria-labelledby', `${this.clearButton.id} ${(labelElem === null || labelElem === void 0 ? void 0 : labelElem.id) || ''}`);
}
if (!this.input.getAttribute('aria-expanded')) {
this.input.setAttribute('aria-expanded', 'false');
}
this.results.hidden = true;
this.results.setAttribute('aria-label', 'results');
this.input.setAttribute('autocomplete', 'off');

@@ -21,2 +40,3 @@ this.input.setAttribute('spellcheck', 'false');

this.onCommit = this.onCommit.bind(this);
this.handleClear = this.handleClear.bind(this);
this.input.addEventListener('keydown', this.onKeydown);

@@ -28,2 +48,3 @@ this.input.addEventListener('focus', this.onInputFocus);

this.results.addEventListener('combobox-commit', this.onCommit);
(_a = this.clearButton) === null || _a === void 0 ? void 0 : _a.addEventListener('click', this.handleClear);
}

@@ -38,3 +59,23 @@ destroy() {

}
handleClear(event) {
event.preventDefault();
if (this.input.getAttribute('aria-expanded') === 'true') {
this.input.setAttribute('aria-expanded', 'false');
this.updateFeedbackForScreenReaders('Results hidden.');
}
this.input.value = '';
this.container.value = '';
this.input.focus();
this.input.dispatchEvent(new Event('change'));
this.container.open = false;
}
onKeydown(event) {
if (event.key === 'Enter' && this.container.open && this.autoselectEnabled) {
const firstOption = this.results.children[0];
if (firstOption) {
event.stopPropagation();
event.preventDefault();
this.onCommit({ target: firstOption });
}
}
if (event.key === 'Escape' && this.container.open) {

@@ -76,3 +117,7 @@ this.container.open = false;

const value = selected.getAttribute('data-autocomplete-value') || selected.textContent;
this.updateFeedbackForScreenReaders(`${selected.textContent || ''} selected.`);
this.container.value = value;
if (!value) {
this.updateFeedbackForScreenReaders(`Results hidden.`);
}
}

@@ -83,2 +128,5 @@ onResultsMouseDown() {

onInputChange() {
if (this.feedback && this.feedback.innerHTML) {
this.feedback.innerHTML = '';
}
this.container.removeAttribute('value');

@@ -93,2 +141,9 @@ this.fetchResults();

}
updateFeedbackForScreenReaders(inputString) {
setTimeout(() => {
if (this.feedback) {
this.feedback.innerHTML = inputString;
}
}, SCREEN_READER_DELAY);
}
fetchResults() {

@@ -112,3 +167,13 @@ const query = this.input.value.trim();

this.identifyOptions();
const hasResults = !!this.results.querySelector('[role="option"]');
const allNewOptions = this.results.querySelectorAll('[role="option"]');
const hasResults = !!allNewOptions.length;
const numOptions = allNewOptions.length;
const [firstOption] = allNewOptions;
const firstOptionValue = firstOption === null || firstOption === void 0 ? void 0 : firstOption.textContent;
if (this.autoselectEnabled && firstOptionValue) {
this.updateFeedbackForScreenReaders(`${numOptions} results. ${firstOptionValue} is the top result: Press Enter to activate.`);
}
else {
this.updateFeedbackForScreenReaders(`${numOptions || 'No'} results.`);
}
this.container.open = hasResults;

@@ -115,0 +180,0 @@ this.container.dispatchEvent(new CustomEvent('load'));

@@ -223,4 +223,6 @@ const ctrlBindings = !!navigator.userAgent.match(/Macintosh/);

const SCREEN_READER_DELAY = window.testScreenReaderDelay || 100;
class Autocomplete {
constructor(container, input, results) {
constructor(container, input, results, autoselectEnabled = false) {
var _a;
this.container = container;

@@ -230,3 +232,20 @@ this.input = input;

this.combobox = new Combobox(input, results);
this.feedback = document.getElementById(`${this.results.id}-feedback`);
this.autoselectEnabled = autoselectEnabled;
this.clearButton = document.getElementById(`${this.input.id || this.input.name}-clear`);
this.clientOptions = results.querySelectorAll('[role=option]');
if (this.feedback) {
this.feedback.setAttribute('aria-live', 'polite');
this.feedback.setAttribute('aria-atomic', 'true');
}
if (this.clearButton && !this.clearButton.getAttribute('aria-label')) {
const labelElem = document.querySelector(`label[for="${this.input.name}"]`);
this.clearButton.setAttribute('aria-label', `clear:`);
this.clearButton.setAttribute('aria-labelledby', `${this.clearButton.id} ${(labelElem === null || labelElem === void 0 ? void 0 : labelElem.id) || ''}`);
}
if (!this.input.getAttribute('aria-expanded')) {
this.input.setAttribute('aria-expanded', 'false');
}
this.results.hidden = true;
this.results.setAttribute('aria-label', 'results');
this.input.setAttribute('autocomplete', 'off');

@@ -241,2 +260,3 @@ this.input.setAttribute('spellcheck', 'false');

this.onCommit = this.onCommit.bind(this);
this.handleClear = this.handleClear.bind(this);
this.input.addEventListener('keydown', this.onKeydown);

@@ -248,2 +268,3 @@ this.input.addEventListener('focus', this.onInputFocus);

this.results.addEventListener('combobox-commit', this.onCommit);
(_a = this.clearButton) === null || _a === void 0 ? void 0 : _a.addEventListener('click', this.handleClear);
}

@@ -258,3 +279,23 @@ destroy() {

}
handleClear(event) {
event.preventDefault();
if (this.input.getAttribute('aria-expanded') === 'true') {
this.input.setAttribute('aria-expanded', 'false');
this.updateFeedbackForScreenReaders('Results hidden.');
}
this.input.value = '';
this.container.value = '';
this.input.focus();
this.input.dispatchEvent(new Event('change'));
this.container.open = false;
}
onKeydown(event) {
if (event.key === 'Enter' && this.container.open && this.autoselectEnabled) {
const firstOption = this.results.children[0];
if (firstOption) {
event.stopPropagation();
event.preventDefault();
this.onCommit({ target: firstOption });
}
}
if (event.key === 'Escape' && this.container.open) {

@@ -296,3 +337,7 @@ this.container.open = false;

const value = selected.getAttribute('data-autocomplete-value') || selected.textContent;
this.updateFeedbackForScreenReaders(`${selected.textContent || ''} selected.`);
this.container.value = value;
if (!value) {
this.updateFeedbackForScreenReaders(`Results hidden.`);
}
}

@@ -303,2 +348,5 @@ onResultsMouseDown() {

onInputChange() {
if (this.feedback && this.feedback.innerHTML) {
this.feedback.innerHTML = '';
}
this.container.removeAttribute('value');

@@ -313,2 +361,9 @@ this.fetchResults();

}
updateFeedbackForScreenReaders(inputString) {
setTimeout(() => {
if (this.feedback) {
this.feedback.innerHTML = inputString;
}
}, SCREEN_READER_DELAY);
}
fetchResults() {

@@ -332,3 +387,13 @@ const query = this.input.value.trim();

this.identifyOptions();
const hasResults = !!this.results.querySelector('[role="option"]');
const allNewOptions = this.results.querySelectorAll('[role="option"]');
const hasResults = !!allNewOptions.length;
const numOptions = allNewOptions.length;
const [firstOption] = allNewOptions;
const firstOptionValue = firstOption === null || firstOption === void 0 ? void 0 : firstOption.textContent;
if (this.autoselectEnabled && firstOptionValue) {
this.updateFeedbackForScreenReaders(`${numOptions} results. ${firstOptionValue} is the top result: Press Enter to activate.`);
}
else {
this.updateFeedbackForScreenReaders(`${numOptions || 'No'} results.`);
}
this.container.open = hasResults;

@@ -359,5 +424,2 @@ this.container.dispatchEvent(new CustomEvent('load'));

class AutocompleteElement extends HTMLElement {
constructor() {
super();
}
connectedCallback() {

@@ -371,3 +433,4 @@ const listId = this.getAttribute('for');

return;
state.set(this, new Autocomplete(this, input, results));
const autoselectEnabled = this.getAttribute('data-autoselect') === 'true';
state.set(this, new Autocomplete(this, input, results, autoselectEnabled));
results.setAttribute('role', 'listbox');

@@ -430,3 +493,2 @@ }

}
if (!window.customElements.get('auto-complete')) {

@@ -433,0 +495,0 @@ window.AutocompleteElement = AutocompleteElement;

import AutocompleteElement from './auto-complete-element';
export { AutocompleteElement as default };
export { default as AutocompleteEvent } from './auto-complete-event';
declare global {
interface Window {
AutocompleteElement: typeof AutocompleteElement;
}
interface HTMLElementTagNameMap {
'auto-complete': AutocompleteElement;
}
}

@@ -53,4 +53,6 @@ import Combobox from '@github/combobox-nav';

const SCREEN_READER_DELAY = window.testScreenReaderDelay || 100;
class Autocomplete {
constructor(container, input, results) {
constructor(container, input, results, autoselectEnabled = false) {
var _a;
this.container = container;

@@ -60,3 +62,20 @@ this.input = input;

this.combobox = new Combobox(input, results);
this.feedback = document.getElementById(`${this.results.id}-feedback`);
this.autoselectEnabled = autoselectEnabled;
this.clearButton = document.getElementById(`${this.input.id || this.input.name}-clear`);
this.clientOptions = results.querySelectorAll('[role=option]');
if (this.feedback) {
this.feedback.setAttribute('aria-live', 'polite');
this.feedback.setAttribute('aria-atomic', 'true');
}
if (this.clearButton && !this.clearButton.getAttribute('aria-label')) {
const labelElem = document.querySelector(`label[for="${this.input.name}"]`);
this.clearButton.setAttribute('aria-label', `clear:`);
this.clearButton.setAttribute('aria-labelledby', `${this.clearButton.id} ${(labelElem === null || labelElem === void 0 ? void 0 : labelElem.id) || ''}`);
}
if (!this.input.getAttribute('aria-expanded')) {
this.input.setAttribute('aria-expanded', 'false');
}
this.results.hidden = true;
this.results.setAttribute('aria-label', 'results');
this.input.setAttribute('autocomplete', 'off');

@@ -71,2 +90,3 @@ this.input.setAttribute('spellcheck', 'false');

this.onCommit = this.onCommit.bind(this);
this.handleClear = this.handleClear.bind(this);
this.input.addEventListener('keydown', this.onKeydown);

@@ -78,2 +98,3 @@ this.input.addEventListener('focus', this.onInputFocus);

this.results.addEventListener('combobox-commit', this.onCommit);
(_a = this.clearButton) === null || _a === void 0 ? void 0 : _a.addEventListener('click', this.handleClear);
}

@@ -88,3 +109,23 @@ destroy() {

}
handleClear(event) {
event.preventDefault();
if (this.input.getAttribute('aria-expanded') === 'true') {
this.input.setAttribute('aria-expanded', 'false');
this.updateFeedbackForScreenReaders('Results hidden.');
}
this.input.value = '';
this.container.value = '';
this.input.focus();
this.input.dispatchEvent(new Event('change'));
this.container.open = false;
}
onKeydown(event) {
if (event.key === 'Enter' && this.container.open && this.autoselectEnabled) {
const firstOption = this.results.children[0];
if (firstOption) {
event.stopPropagation();
event.preventDefault();
this.onCommit({ target: firstOption });
}
}
if (event.key === 'Escape' && this.container.open) {

@@ -126,3 +167,7 @@ this.container.open = false;

const value = selected.getAttribute('data-autocomplete-value') || selected.textContent;
this.updateFeedbackForScreenReaders(`${selected.textContent || ''} selected.`);
this.container.value = value;
if (!value) {
this.updateFeedbackForScreenReaders(`Results hidden.`);
}
}

@@ -133,2 +178,5 @@ onResultsMouseDown() {

onInputChange() {
if (this.feedback && this.feedback.innerHTML) {
this.feedback.innerHTML = '';
}
this.container.removeAttribute('value');

@@ -143,2 +191,9 @@ this.fetchResults();

}
updateFeedbackForScreenReaders(inputString) {
setTimeout(() => {
if (this.feedback) {
this.feedback.innerHTML = inputString;
}
}, SCREEN_READER_DELAY);
}
fetchResults() {

@@ -162,3 +217,13 @@ const query = this.input.value.trim();

this.identifyOptions();
const hasResults = !!this.results.querySelector('[role="option"]');
const allNewOptions = this.results.querySelectorAll('[role="option"]');
const hasResults = !!allNewOptions.length;
const numOptions = allNewOptions.length;
const [firstOption] = allNewOptions;
const firstOptionValue = firstOption === null || firstOption === void 0 ? void 0 : firstOption.textContent;
if (this.autoselectEnabled && firstOptionValue) {
this.updateFeedbackForScreenReaders(`${numOptions} results. ${firstOptionValue} is the top result: Press Enter to activate.`);
}
else {
this.updateFeedbackForScreenReaders(`${numOptions || 'No'} results.`);
}
this.container.open = hasResults;

@@ -189,5 +254,2 @@ this.container.dispatchEvent(new CustomEvent('load'));

class AutocompleteElement extends HTMLElement {
constructor() {
super();
}
connectedCallback() {

@@ -201,3 +263,4 @@ const listId = this.getAttribute('for');

return;
state.set(this, new Autocomplete(this, input, results));
const autoselectEnabled = this.getAttribute('data-autoselect') === 'true';
state.set(this, new Autocomplete(this, input, results, autoselectEnabled));
results.setAttribute('role', 'listbox');

@@ -260,3 +323,2 @@ }

}
if (!window.customElements.get('auto-complete')) {

@@ -263,0 +325,0 @@ window.AutocompleteElement = AutocompleteElement;

{
"name": "@github/auto-complete-element",
"version": "3.0.2",
"version": "3.1.0",
"description": "Auto-complete input values from server results",

@@ -12,2 +12,4 @@ "repository": "github/auto-complete-element",

"clean": "rm -rf dist",
"example": "http-server -c-1 ./examples",
"preexample": "mkdir -p ./examples/dist && npm run build && cp ./dist/bundle.js ./examples/dist/bundle.js",
"lint": "eslint . --ext .ts,.js && tsc --noEmit",

@@ -17,3 +19,3 @@ "prebuild": "npm run clean && npm run lint",

"pretest": "npm run build",
"test": "karma start test/karma.config.cjs",
"test": "karma start karma.config.cjs",
"prepublishOnly": "npm run build",

@@ -28,3 +30,4 @@ "postpublish": "npm publish --ignore-scripts --@github:registry='https://npm.pkg.github.com'"

"files": [
"dist"
"dist",
"validator.js"
],

@@ -36,6 +39,10 @@ "dependencies": {

"@github/prettier-config": "0.0.4",
"chai": "^4.2.0",
"eslint": "^6.6.0",
"eslint-plugin-github": "^4.0.1",
"karma": "^5.0.4",
"axe-core": "^4.4.0",
"chai": "^4.3.6",
"chromium": "^3.0.3",
"eslint": "^7.25.0",
"eslint-plugin-custom-elements": "^0.0.2",
"eslint-plugin-github": "^4.1.3",
"http-server": "^14.0.0",
"karma": "^6.3.2",
"karma-chai": "^0.1.0",

@@ -45,10 +52,7 @@ "karma-chrome-launcher": "^3.1.0",

"karma-mocha-reporter": "^2.2.5",
"mocha": "^7.1.2",
"rollup": "^2.12.0",
"mocha": "^8.3.2",
"rollup": "^2.45.2",
"rollup-plugin-node-resolve": "^5.2.0",
"typescript": "^3.9.3"
},
"eslintIgnore": [
"dist/"
]
"typescript": "^4.2.4"
}
}

@@ -31,7 +31,27 @@ # &lt;auto-complete&gt; element

<auto-complete src="/users/search" for="users-popup">
<input type="text">
<input type="text" name="users">
<!--
Optional clear button:
- id must match the id of the input or the name of the input plus "-clear"
- recommended to be *before* UL elements to avoid conflicting with their blur logic
Please see Note below on this button for more details
-->
<button id="users-clear">X</button>
<ul id="users-popup"></ul>
<!--
Optional div for screen reader feedback. Note the ID matches the ul, but with -feedback appended.
Recommended: Use a "Screen Reader Only" class to position the element off the visual boundary of the page.
-->
<div id="users-popup-feedback" class="sr-only"></div>
</auto-complete>
```
If you want to enable auto-select (pressing Enter in the input will select the first option), using the above example:
```html
<auto-complete data-autoselect="true" src="/users/search" for="users-popup">
...
</auto-complete>
```
The server response should include the items that matched the search query.

@@ -53,2 +73,9 @@

### A Note on Clear button
While `input type="search"` comes with an `x` that clears the content of the field and refocuses it on many browsers, the implementation for this control is not keyboard accessible, and so we've opted to enable a customizable clear button so that your keyboard users will be able to interact with it.
As an example:
> In Chrome, this 'x' isn't a button but a div with a pseudo="-webkit-search-cancel-button". It doesn't have a tab index or a way to navigate to it without a mouse. Additionally, this control is only visible on mouse hover.
## Attributes

@@ -113,4 +140,39 @@

To view changes locally, run `npm run examples`.
In `examples/index.html`, uncomment `<!--<script type="module" src="./dist/bundle.js"></script>-->` and comment out the script referencing the `unpkg` version. This allows you to use the `src` code in this repo. Otherwise, you will be pulling the latest published code, which will not reflect the local changes you are making.
## Accessibility Testing
We have included some custom rules that assist in providing guardrails to confirm this component is being used accessibly.
If you are using the `axe-core` library in your project,
```js
import axe from 'axe-core'
import autoCompleteRulesBuilder from '@github/auto-complete-element/validator'
const autoCompleteRules = autoCompleteRulesBuilder() // optionally, pass in your app's custom rules object, it will build and return the full object
axe.configure(autoCompleteRules)
axe.run(document)
```
## Validate usage in your project
To confirm your usage is working as designed,
```js
import {validate} from '@github/auto-complete-element/validator'
validate(document)
```
Passes and failures may be determined by the length of the `passes` and `violations` arrays on the returned object:
```js
{
passes: [],
violations: []
}
```
## License
Distributed under the MIT license. See LICENSE for details.
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc