const smSelect = document.createElement('template') smSelect.innerHTML = `
`; customElements.define('sm-select', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smSelect.content.cloneNode(true)) this.reset = this.reset.bind(this) this.open = this.open.bind(this) this.collapse = this.collapse.bind(this) this.toggle = this.toggle.bind(this) this.handleSelectKeyDown = this.handleSelectKeyDown.bind(this) this.handleOptionsKeyDown = this.handleOptionsKeyDown.bind(this) this.handleOptionsKeyDown = this.handleOptionsKeyDown.bind(this) this.availableOptions this.optionList = this.shadowRoot.querySelector('.options') this.chevron = this.shadowRoot.querySelector('.toggle') this.selection = this.shadowRoot.querySelector('.selection'), this.previousOption this.isOpen = false; this.slideDown = [{ transform: `translateY(-0.5rem)`, opacity: 0 }, { transform: `translateY(0)`, opacity: 1 } ] this.slideUp = [{ transform: `translateY(0)`, opacity: 1 }, { transform: `translateY(-0.5rem)`, opacity: 0 } ] this.animationOptions = { duration: 300, fill: "forwards", easing: 'ease' } } static get observedAttributes() { return ['value', 'disabled'] } get value() { return this.getAttribute('value') } set value(val) { this.setAttribute('value', val) } reset() { } open() { this.optionList.classList.remove('hide') this.optionList.animate(this.slideDown, this.animationOptions) this.chevron.classList.add('rotate') this.isOpen = true } collapse() { this.chevron.classList.remove('rotate') this.optionList.animate(this.slideUp, this.animationOptions) .onfinish = () => { this.optionList.classList.add('hide') this.isOpen = false } } toggle() { if (!this.isOpen && !this.hasAttribute('disabled')) { this.open() } else { this.collapse() } } handleSelectKeyDown(e) { if (e.code === 'ArrowDown' || e.code === 'ArrowRight') { e.preventDefault() this.availableOptions[0].focus() } else if (e.code === 'Enter' || e.code === 'Space') { if (!this.isOpen) { this.optionList.classList.remove('hide') this.optionList.animate(this.slideDown, this.animationOptions) this.chevron.classList.add('rotate') this.isOpen = true } else { this.collapse() } } } handleOptionsKeyDown(e) { if (e.code === 'ArrowUp' || e.code === 'ArrowRight') { e.preventDefault() if (document.activeElement.previousElementSibling) { document.activeElement.previousElementSibling.focus() } else { this.availableOptions[this.availableOptions.length - 1].focus() } } else if (e.code === 'ArrowDown' || e.code === 'ArrowLeft') { e.preventDefault() if (document.activeElement.nextElementSibling) { document.activeElement.nextElementSibling.focus() } else { this.availableOptions[0].focus() } } } connectedCallback() { this.setAttribute('role', 'listbox') if (!this.hasAttribute('disabled')) { this.selection.setAttribute('tabindex', '0') } let slot = this.shadowRoot.querySelector('slot') slot.addEventListener('slotchange', e => { this.availableOptions = slot.assignedElements() if (this.availableOptions[0]) { let firstElement = this.availableOptions[0]; this.previousOption = firstElement; firstElement.classList.add('check-selected') this.setAttribute('value', firstElement.getAttribute('value')) this.shadowRoot.querySelector('.option-text').textContent = firstElement.textContent this.availableOptions.forEach((element, index) => { element.setAttribute('tabindex', "0"); }) } }); this.selection.addEventListener('click', this.toggle) this.selection.addEventListener('keydown', this.handleSelectKeyDown) this.optionList.addEventListener('keydown', this.handleOptionsKeyDown) this.addEventListener('optionSelected', e => { if (this.previousOption !== e.target) { this.setAttribute('value', e.detail.value) this.shadowRoot.querySelector('.option-text').textContent = e.detail.text; this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true, detail: { value: e.detail.value } })) if (this.previousOption) { this.previousOption.classList.remove('check-selected') } this.previousOption = e.target; } if (!e.detail.switching) this.collapse() e.target.classList.add('check-selected') }) document.addEventListener('mousedown', e => { if (this.isOpen && !this.contains(e.target)) { this.collapse() } }) } attributeChangedCallback(name, oldVal, newVal) { if (name === "disabled") { if (this.hasAttribute('disabled')) { this.selection.removeAttribute('tabindex') }else { this.selection.setAttribute('tabindex', '0') } } } disconnectedCallback() { this.selection.removeEventListener('click', this.toggle) this.selection.removeEventListener('keydown', this.handleSelectKeyDown) this.optionList.removeEventListener('keydown', this.handleOptionsKeyDown) } }) // option const smOption = document.createElement('template') smOption.innerHTML = `
`; customElements.define('sm-option', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smOption.content.cloneNode(true)) this.sendDetails = this.sendDetails.bind(this) } sendDetails(switching) { let optionSelected = new CustomEvent('optionSelected', { bubbles: true, composed: true, detail: { text: this.textContent, value: this.getAttribute('value'), switching: switching } }) this.dispatchEvent(optionSelected) } connectedCallback() { this.setAttribute('role', 'option') let validKey = [ 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight' ] this.addEventListener('click', this.sendDetails) this.addEventListener('keyup', e => { if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() this.sendDetails(false) } if (validKey.includes(e.code)) { e.preventDefault() this.sendDetails(true) } }) if (this.hasAttribute('default')) { setTimeout(() => { this.sendDetails() }, 0); } } })