@github/auto-complete-element
Advanced tools
+96
| const SELECTOR = 'auto-complete' | ||
| const INPUT_RULE_ID = 'required-input-element-child' | ||
| const INPUT_HELP_TEXT = 'This component requires an input field to be provided.' | ||
| const CLEAR_BUTTON_RULE_ID = 'optional-clear-must-be-button' | ||
| const CLEAR_BUTTON_HELP_TEXT = 'If provided with clear button, it must be a button element.' | ||
| function checkForInput(autoCompleteElement) { | ||
| return autoCompleteElement.querySelectorAll('input').length === 1 | ||
| } | ||
| function checkForOptionalClearButton(autoCompleteElement) { | ||
| const [input] = autoCompleteElement.querySelectorAll('input') | ||
| if (!input) { | ||
| return true | ||
| } | ||
| const clearButtonId = `${input.id || input.getAttribute('name')}-clear` | ||
| const clearButton = autoCompleteElement.ownerDocument.getElementById(clearButtonId) | ||
| if (!clearButton) { | ||
| return true | ||
| } | ||
| if (clearButton instanceof HTMLButtonElement) { | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
| const rules = [ | ||
| { | ||
| id: INPUT_RULE_ID, | ||
| excludeHidden: true, | ||
| selector: SELECTOR, | ||
| metadata: { | ||
| help: INPUT_HELP_TEXT, | ||
| helpUrl: '' | ||
| }, | ||
| all: [`${INPUT_RULE_ID}_0`] | ||
| }, | ||
| { | ||
| id: CLEAR_BUTTON_RULE_ID, | ||
| excludeHidden: true, | ||
| selector: SELECTOR, | ||
| metadata: { | ||
| help: CLEAR_BUTTON_HELP_TEXT, | ||
| helpUrl: '' | ||
| }, | ||
| all: [`${CLEAR_BUTTON_RULE_ID}_0`] | ||
| } | ||
| ] | ||
| const checks = [ | ||
| { | ||
| id: `${INPUT_RULE_ID}_0`, | ||
| evaluate: checkForInput, | ||
| metadata: {impact: 'critical'} | ||
| }, | ||
| { | ||
| id: `${CLEAR_BUTTON_RULE_ID}_0`, | ||
| evaluate: checkForOptionalClearButton, | ||
| metadata: {impact: 'critical'} | ||
| } | ||
| ] | ||
| export function validator(domNode) { | ||
| const result = { | ||
| passes: [], | ||
| violations: [] | ||
| } | ||
| for (const element of domNode.getElementsByTagName(SELECTOR)) { | ||
| for (const rule of rules) { | ||
| for (const checkId of rule.all) { | ||
| const thisCheck = checks.find(check => check.id === checkId) | ||
| const checkResult = thisCheck.evaluate(element) | ||
| result[checkResult ? 'passes' : 'violations'].push({ | ||
| id: rule.id, | ||
| help: rule.metadata.help, | ||
| helpUrl: rule.metadata.helpUrl, | ||
| nodes: [element] | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| return result | ||
| } | ||
| /** | ||
| * | ||
| * @param {import('axe-core').Spec} ruleset | ||
| * @returns {import('axe-core').Spec} | ||
| */ | ||
| export default function combineRules(ruleset = {}) { | ||
| return Object.assign({}, ruleset, { | ||
| checks: (ruleset.checks || []).concat(checks), | ||
| rules: (ruleset.rules || []).concat(rules) | ||
| }) | ||
| } |
| 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; | ||
| } | ||
| } |
@@ -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; |
+67
-2
| 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')); |
+69
-7
@@ -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; |
+0
-8
| 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; | ||
| } | ||
| } |
+69
-7
@@ -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; |
+18
-14
| { | ||
| "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" | ||
| } | ||
| } |
+63
-1
@@ -31,7 +31,27 @@ # <auto-complete> 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. |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
54334
40.98%17
6.25%1258
29.56%176
54.39%17
30.77%1
Infinity%