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.focusIn = this.focusIn.bind(this) 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.selectOption = this.selectOption.bind(this) this.debounce = this.debounce.bind(this) this.availableOptions = [] this.previousOption this.isOpen = false; this.label = '' this.defaultSelected = '' this.isUnderViewport = false this.animationOptions = { duration: 300, fill: "forwards", easing: 'ease' } this.optionList = this.shadowRoot.querySelector('.options') this.selection = this.shadowRoot.querySelector('.selection') this.selectedOptionText = this.shadowRoot.querySelector('.selected-option-text') } static get observedAttributes() { return ['disabled', 'label'] } get value() { return this.getAttribute('value') } set value(val) { const selectedOption = this.shadowRoot.querySelector('slot').assignedElements().find(option => option.getAttribute('value') === val) if (selectedOption) { this.setAttribute('value', val) this.selectOption(selectedOption) } else { console.warn(`There is no option with ${val} as value`) } } debounce(callback, wait) { let timeoutId = null; return (...args) => { window.clearTimeout(timeoutId); timeoutId = window.setTimeout(() => { callback.apply(null, args); }, wait); }; } reset(fire = true) { if (this.availableOptions[0] && this.previousOption !== this.availableOptions[0]) { const selectedOption = this.availableOptions.find(option => option.hasAttribute('selected')) || this.availableOptions[0]; this.value = selectedOption.getAttribute('value') if (fire) { this.fireEvent() } } } selectOption(selectedOption) { if (this.previousOption !== selectedOption) { this.querySelectorAll('[selected]').forEach(option => option.removeAttribute('selected')) this.selectedOptionText.textContent = `${this.label}${selectedOption.textContent}`; selectedOption.setAttribute('selected', '') this.previousOption = selectedOption } } focusIn() { this.selection.focus() } open() { this.availableOptions.forEach(option => option.setAttribute('tabindex', 0)) this.optionList.classList.remove('hidden') this.isUnderViewport = this.getBoundingClientRect().bottom + this.optionList.getBoundingClientRect().height > window.innerHeight; if (this.isUnderViewport) { this.setAttribute('isUnder', '') } else { this.removeAttribute('isUnder') } this.optionList.animate([ { transform: `translateY(${this.isUnderViewport ? '' : '-'}0.5rem)`, opacity: 0 }, { transform: `translateY(0)`, opacity: 1 } ], this.animationOptions) this.setAttribute('open', ''); this.style.zIndex = 1000; (this.availableOptions.find(option => option.hasAttribute('selected')) || this.availableOptions[0]).focus() document.addEventListener('mousedown', this.handleClickOutside) this.isOpen = true } collapse() { this.removeAttribute('open') this.optionList.animate([ { transform: `translateY(0)`, opacity: 1 }, { transform: `translateY(${this.isUnderViewport ? '' : '-'}0.5rem)`, opacity: 0 }, ], this.animationOptions) .onfinish = () => { this.availableOptions.forEach(option => option.removeAttribute('tabindex')) document.removeEventListener('mousedown', this.handleClickOutside) this.optionList.classList.add('hidden') this.isOpen = false this.style.zIndex = 'auto'; } } 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.key === 'ArrowUp') { e.preventDefault() if (document.activeElement.previousElementSibling) { document.activeElement.previousElementSibling.focus() } else { this.availableOptions[this.availableOptions.length - 1].focus() } } else if (e.key === '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.fireEvent() } } handleClick(e) { if (e.target === this) { this.toggle() } else { this.handleOptionSelection() this.collapse() } } handleKeydown(e) { if (e.target === this) { if (this.isOpen && e.key === 'ArrowDown') { e.preventDefault(); (this.availableOptions.find(option => option.hasAttribute('selected')) || this.availableOptions[0]).focus() this.handleOptionSelection(e) } else if (e.key === ' ') { e.preventDefault() this.toggle() } } else { this.handleOptionsNavigation(e) this.handleOptionSelection(e) if (['Enter', ' ', 'Escape', 'Tab'].includes(e.key)) { e.preventDefault() this.collapse() this.focusIn() } } } 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', this.debounce(e => { this.availableOptions = slot.assignedElements() this.reset(false) this.defaultSelected = this.value }, 100)); new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const offsetLeft = this.selection.getBoundingClientRect().left if (offsetLeft < window.innerWidth / 2) { this.setAttribute('align-select', 'left') } else { this.setAttribute('align-select', 'right') } } }) }).observe(this) this.addEventListener('click', this.handleClick) this.addEventListener('keydown', this.handleKeydown) } disconnectedCallback() { this.removeEventListener('click', this.handleClick) this.removeEventListener('click', this.toggle) this.removeEventListener('keydown', this.handleKeydown) } attributeChangedCallback(name) { if (name === "disabled") { if (this.hasAttribute('disabled')) { this.selection.removeAttribute('tabindex') } else { this.selection.setAttribute('tabindex', '0') } } else if (name === 'label') { this.label = this.hasAttribute('label') ? `${this.getAttribute('label')} ` : '' } } }) // 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') } })