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.handleOptionsNavigation = this.handleOptionsNavigation.bind(this) this.handleOptionSelection = this.handleOptionSelection.bind(this) this.handleKeydown = this.handleKeydown.bind(this) this.handleClickOutside = this.handleClickOutside.bind(this) this.availableOptions 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' } this.optionList = this.shadowRoot.querySelector('.options') this.chevron = this.shadowRoot.querySelector('.toggle') this.selection = this.shadowRoot.querySelector('.selection') this.selectedOptionText = this.shadowRoot.querySelector('.selected-option-text') } static get observedAttributes() { return ['value', 'disabled'] } get value() { return this.getAttribute('value') } set value(val) { this.setAttribute('value', val) } reset(fire = true) { if (this.availableOptions[0] && this.previousOption !== this.availableOptions[0]) { const firstElement = this.availableOptions[0]; if (this.previousOption) { this.previousOption.classList.remove('check-selected') } firstElement.classList.add('check-selected') this.value = firstElement.getAttribute('value') this.selectedOptionText.textContent = firstElement.textContent this.previousOption = firstElement; if (fire) { this.fireEvent() } } } 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() } } fireEvent() { this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true, detail: { value: this.value } })) } handleOptionsNavigation(e) { if (e.code === 'ArrowUp') { e.preventDefault() if (document.activeElement.previousElementSibling) { document.activeElement.previousElementSibling.focus() } else { this.availableOptions[this.availableOptions.length - 1].focus() } } else if (e.code === 'ArrowDown') { e.preventDefault() if (document.activeElement.nextElementSibling) { document.activeElement.nextElementSibling.focus() } else { this.availableOptions[0].focus() } } } handleOptionSelection(e) { if (this.previousOption !== document.activeElement) { this.value = document.activeElement.getAttribute('value') this.selectedOptionText.textContent = document.activeElement.textContent; this.fireEvent() if (this.previousOption) { this.previousOption.classList.remove('check-selected') } document.activeElement.classList.add('check-selected') this.previousOption = document.activeElement } } handleClick(e) { if (e.target === this) { this.toggle() } else { this.handleOptionSelection() this.collapse() } } handleKeydown(e) { if (e.target === this) { if (this.isOpen && e.code === 'ArrowDown') { e.preventDefault() this.availableOptions[0].focus() this.handleOptionSelection(e) } else if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() this.toggle() } } else { this.handleOptionsNavigation(e) this.handleOptionSelection(e) if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() this.collapse() } } } handleClickOutside(e) { if (this.isOpen && !this.contains(e.target)) { this.collapse() } } 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() this.reset(false) }); this.addEventListener('click', this.handleClick) this.addEventListener('keydown', this.handleKeydown) document.addEventListener('mousedown', this.handleClickOutside) } disconnectedCallback() { this.removeEventListener('click', this.toggle) this.removeEventListener('keydown', this.handleKeydown) document.removeEventListener('mousedown', this.handleClickOutside) } attributeChangedCallback(name) { if (name === "disabled") { if (this.hasAttribute('disabled')) { this.selection.removeAttribute('tabindex') } else { this.selection.setAttribute('tabindex', '0') } } } }) // 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)) } connectedCallback() { this.setAttribute('role', 'option') this.setAttribute('tabindex', '0') } })