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);
}
}
})