diff --git a/src/sass/partials/virtual-select.scss b/src/sass/partials/virtual-select.scss index b5b8539..1fcbd56 100755 --- a/src/sass/partials/virtual-select.scss +++ b/src/sass/partials/virtual-select.scss @@ -272,7 +272,9 @@ } .vscomp-search-label, -.vscomp-live-region { +.vscomp-live-region, +.vscomp-dropbox-container-top, +.vscomp-dropbox-container-bottom { border: 0; clip: rect(0 0 0 0); height: 1px; diff --git a/src/utils/dom-utils.js b/src/utils/dom-utils.js index bc53b7e..7e20288 100644 --- a/src/utils/dom-utils.js +++ b/src/utils/dom-utils.js @@ -213,6 +213,24 @@ export class DomUtils { return $ele.forEach === undefined ? [$ele] : $ele; } + /** + * @static + * @param {string} [$selector=''] + * @param {*} [$parentEle=undefined] + * @return {*} + * @memberof DomUtils + */ + static getElementsBySelector($selector = '', $parentEle = undefined) { + let elements; + const parent = $parentEle !== undefined ? $parentEle : document; + + if ($selector !== '') { + elements = parent.querySelectorAll($selector); + } + + return elements !== undefined ? Array.from(elements) : []; + } + /** * @param {HTMLElement} $ele * @param {string} events diff --git a/src/virtual-select.js b/src/virtual-select.js index 34806ea..ea5f54d 100755 --- a/src/virtual-select.js +++ b/src/virtual-select.js @@ -10,6 +10,8 @@ const keyDownMethodMapping = { 13: 'onEnterPress', 38: 'onUpArrowPress', 40: 'onDownArrowPress', + 46: 'onBackspaceOrDeletePress', // Delete + 8: 'onBackspaceOrDeletePress', // Backspace }; const valueLessProps = ['autofocus', 'disabled', 'multiple', 'required']; @@ -175,11 +177,6 @@ export class VirtualSelect {
- - -
-

-
${this.renderDropbox({ wrapperClasses })} `; @@ -187,7 +184,6 @@ export class VirtualSelect { this.$ele.innerHTML = html; this.$body = document.querySelector('body'); this.$wrapper = this.$ele.querySelector('.vscomp-wrapper'); - this.$ariaLiveElem = this.$ele.querySelector('.vscomp-live-region-title'); if (this.hasDropboxWrapper) { this.$allWrappers = [this.$wrapper, this.$dropboxWrapper]; @@ -205,6 +201,8 @@ export class VirtualSelect { this.$hiddenInput = this.$ele.querySelector('.vscomp-hidden-input'); this.$dropbox = this.$dropboxContainer.querySelector('.vscomp-dropbox'); this.$dropboxCloseButton = this.$dropboxContainer.querySelector('.vscomp-dropbox-close-button'); + this.$dropboxContainerBottom = this.$dropboxContainer.querySelector('.vscomp-dropbox-container-bottom'); + this.$dropboxContainerTop = this.$dropboxContainer.querySelector('.vscomp-dropbox-container-top'); this.$search = this.$dropboxContainer.querySelector('.vscomp-search-wrapper'); this.$optionsContainer = this.$dropboxContainer.querySelector('.vscomp-options-container'); this.$optionsList = this.$dropboxContainer.querySelector('.vscomp-options-list'); @@ -219,6 +217,7 @@ export class VirtualSelect { const $wrapper = this.dropboxWrapper !== 'self' ? document.querySelector(this.dropboxWrapper) : null; const html = `
+
@@ -236,6 +235,7 @@ export class VirtualSelect {
+
`; @@ -289,6 +289,8 @@ export class VirtualSelect { let rightSection = ''; let description = ''; let groupIndexText = ''; + let ariaLabel = ''; + let tabIndexValue = '-1'; const isSelected = convertToBoolean(d.isSelected); let ariaDisabledText = ''; @@ -297,6 +299,7 @@ export class VirtualSelect { } if (d.isFocused) { + tabIndexValue = '0'; optionClasses += ' focused'; } @@ -320,6 +323,10 @@ export class VirtualSelect { if (d.isGroupOption) { optionClasses += ' group-option'; groupIndexText = `data-group-index="${d.groupIndex}"`; + + const groupName = d.customData.group_name !== undefined ? `${d.customData.group_name}, ` : ''; + const optionDesc = d.customData.description !== undefined ? ` ${d.customData.description},` : ''; + ariaLabel = `aria-label="${groupName}${d.label},${optionDesc}"`; } if (hasLabelRenderer) { @@ -341,7 +348,7 @@ export class VirtualSelect { html += `
${leftSection} @@ -398,6 +405,8 @@ export class VirtualSelect { this.addEvent(this.$searchInput, 'input', 'onSearch'); this.addEvent(this.$searchClear, 'click', 'onSearchClear'); this.addEvent(this.$toggleAllButton, 'click', 'onToggleAllOptions'); + this.addEvent(this.$dropboxContainerBottom, 'focus', 'onDropboxContainerTopOrBottomFocus'); + this.addEvent(this.$dropboxContainerTop, 'focus', 'onDropboxContainerTopOrBottomFocus'); } /** render methods - end */ @@ -448,8 +457,14 @@ export class VirtualSelect { const key = e.which || e.keyCode; const method = keyDownMethodMapping[key]; - if (document.activeElement === this.$searchInput && (key === 9 || (e.shiftKey && key === 9))) { - this.closeDropbox(); + if (document.activeElement === this.$searchInput && (e.shiftKey && key === 9)) { + e.preventDefault(); + this.$dropboxContainerTop.focus(); + return; + } + if (document.activeElement === this.$searchInput && key === 9) { + e.preventDefault(); + this.focusFirstVisibleOption(); return; } // Handle the Escape key when showing the dropdown as a popup, closing it @@ -467,7 +482,7 @@ export class VirtualSelect { if (this.isOpened()) { this.selectFocusedOption(); - } else { + } else if (this.$ele.disabled === false) { this.openDropbox(); } } @@ -492,6 +507,15 @@ export class VirtualSelect { } } + onBackspaceOrDeletePress(e) { + if (e.target === this.$wrapper) { + e.preventDefault(); + if (this.selectedValues.length > 0) { + this.reset(); + } + } + } + onToggleButtonClick(e) { const $target = e.target; @@ -573,6 +597,10 @@ export class VirtualSelect { this.toggleAllOptions(); } + onDropboxContainerTopOrBottomFocus() { + this.closeDropbox(); + } + onResize() { this.setOptionsContainerHeight(true); } @@ -672,13 +700,34 @@ export class VirtualSelect { if (!this.allowNewOption || this.hasServerSearch || this.showOptionsOnlyOnSearch) { DomUtils.toggleClass(this.$allWrappers, 'has-no-search-results', hasNoSearchResults); + if (hasNoSearchResults) { + DomUtils.setAttr(this.$noSearchResults, 'tabindex', '0'); + DomUtils.setAttr(this.$noSearchResults, 'aria-hidden', 'false'); + } else { + DomUtils.setAttr(this.$noSearchResults, 'tabindex', '-1'); + DomUtils.setAttr(this.$noSearchResults, 'aria-hidden', 'true'); + } } DomUtils.toggleClass(this.$allWrappers, 'has-no-options', hasNoOptions); + if (hasNoOptions) { + DomUtils.setAttr(this.$noOptions, 'tabindex', '0'); + DomUtils.setAttr(this.$noOptions, 'aria-hidden', 'false'); + } else { + DomUtils.setAttr(this.$noOptions, 'tabindex', '-1'); + DomUtils.setAttr(this.$noOptions, 'aria-hidden', 'true'); + } this.setOptionAttr(); this.setOptionsPosition(); this.setOptionsTooltip(); + + if (document.activeElement !== this.$searchInput) { + const focusedOption = DomUtils.getElementsBySelector('.focused', this.$dropboxContainer)[0]; + if (focusedOption !== undefined) { + focusedOption.focus(); + } + } } afterSetOptionsContainerHeight(reset) { @@ -2185,7 +2234,7 @@ export class VirtualSelect { DomUtils.addClass(this.$body, 'vscomp-popup-active'); this.isPopupActive = true; } else { - this.focusSearchInput(); + this.focusElementOnOpen(); } DomUtils.dispatchEvent(this.$ele, 'afterOpen'); @@ -2277,6 +2326,53 @@ export class VirtualSelect { } } + focusElementOnOpen() { + const $ele = this.$searchInput; + const hasNoOptions = !this.options.length && !this.hasServerSearch; + + if ($ele) { + if (hasNoOptions) { + DomUtils.setAttr($ele, 'disabled', ''); + this.$noOptions.focus(); + } else { + $ele.focus(); + } + } else { + const $focusableEle = this.$dropbox.querySelector('[tabindex="0"]'); + const optIndex = DomUtils.getData($focusableEle, 'index'); + + if (optIndex !== undefined) { + this.focusOption({ direction: 'next' }); + } else if ($focusableEle) { + $focusableEle.focus(); + } else { + this.focusFirstVisibleOption(); + } + } + } + + focusFirstVisibleOption() { + let $focusableEle = this.$optionsContainer.querySelector(`[data-index='${this.getFirstVisibleOptionIndex()}']`); + + if ($focusableEle) { + if (DomUtils.hasClass($focusableEle, 'group-title')) { + $focusableEle = this.getSibling($focusableEle, 'next'); + } + + DomUtils.setAttr($focusableEle, 'tabindex', '0'); + this.$optionsContainer.scrollTop = this.optionHeight * this.getFirstVisibleOptionIndex(); + this.focusOption({ + focusFirst: true, + }); + $focusableEle.focus(); + } else { + $focusableEle = this.$dropbox.querySelector('[tabindex="0"]'); + if ($focusableEle) { + $focusableEle.focus(); + } + } + } + focusOption({ direction, $option, focusFirst } = {}) { const $focusedEle = this.$dropboxContainer.querySelector('.vscomp-option.focused'); let $newFocusedEle; @@ -2302,10 +2398,6 @@ export class VirtualSelect { this.toggleOptionFocusedState($focusedEle, false); } - if (this.$ariaLiveElem) { - this.$ariaLiveElem.textContent = $newFocusedEle.textContent; - } - this.toggleOptionFocusedState($newFocusedEle, true); this.toggleFocusedProp(DomUtils.getData($newFocusedEle, 'index'), true); this.moveFocusedOptionToView($newFocusedEle); @@ -2890,6 +2982,7 @@ export class VirtualSelect { this.$ele.removeAttribute('disabled'); this.$hiddenInput.removeAttribute('disabled'); DomUtils.setAria(this.$wrapper, 'disabled', false); + DomUtils.changeTabIndex(this.$wrapper, 0); } disable() { @@ -2898,6 +2991,8 @@ export class VirtualSelect { this.$ele.setAttribute('disabled', ''); this.$hiddenInput.setAttribute('disabled', ''); DomUtils.setAria(this.$wrapper, 'disabled', true); + DomUtils.changeTabIndex(this.$wrapper, -1); + this.$wrapper.blur(); } validate() { @@ -2979,6 +3074,11 @@ export class VirtualSelect { } DomUtils.toggleClass($ele, 'focused', isFocused); + DomUtils.setAttr($ele, 'tabindex', isFocused ? '0' : '-1'); + + if (document.activeElement !== this.$searchInput) { + $ele.focus(); + } if (isFocused) { DomUtils.setAria(this.$wrapper, 'activedescendant', $ele.id);