const smMenu = document.createElement('template')
smMenu.innerHTML = `
`;
customElements.define('sm-menu', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smMenu.content.cloneNode(true))
this.isOpen = false;
this.availableOptions
this.containerDimensions
this.animOptions = {
duration: 200,
easing: 'ease'
}
this.optionList = this.shadowRoot.querySelector('.options')
this.menu = this.shadowRoot.querySelector('.menu')
this.icon = this.shadowRoot.querySelector('.icon')
this.expand = this.expand.bind(this)
this.collapse = this.collapse.bind(this)
this.toggle = this.toggle.bind(this)
this.handleKeyDown = this.handleKeyDown.bind(this)
this.handleClick = this.handleClick.bind(this)
this.handleClickOutside = this.handleClickOutside.bind(this)
}
static get observedAttributes() {
return ['value']
}
get value() {
return this.getAttribute('value')
}
set value(val) {
this.setAttribute('value', val)
}
expand() {
if (!this.isOpen) {
this.optionList.classList.remove('hide')
this.optionList.animate([
{
transform: 'translateY(-1rem)',
opacity: '0'
},
{
transform: 'none',
opacity: '1'
},
], this.animOptions)
.onfinish = () => {
this.isOpen = true
this.icon.classList.add('focused')
document.addEventListener('mousedown', this.handleClickOutside)
const firstElement = this.optionList.firstElementChild.assignedElements().find(el => el.tagName === 'MENU-OPTION')
if (firstElement) {
firstElement.focus()
}
}
}
}
collapse() {
if (this.isOpen) {
this.optionList.animate([
{
transform: 'none',
opacity: '1'
},
{
transform: 'translateY(-1rem)',
opacity: '0'
},
], this.animOptions)
.onfinish = () => {
this.isOpen = false
this.icon.classList.remove('focused')
this.optionList.classList.add('hide')
document.removeEventListener('mousedown', this.handleClickOutside)
}
}
}
toggle() {
if (!this.isOpen) {
this.expand()
} else {
this.collapse()
}
}
handleClick(e) {
if (e.target === this) {
this.toggle()
} else {
this.collapse()
}
}
handleKeyDown(e) {
// If key is pressed on menu button
if (e.target === this) {
if (e.key === 'ArrowDown') {
e.preventDefault()
this.availableOptions[0].focus()
}
else if (e.key === ' ') {
e.preventDefault()
this.toggle()
}
} else { // If key is pressed over menu options
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()
}
} else if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
e.target.click()
this.collapse()
this.menu.focus()
}
}
}
handleClickOutside(e) {
if (!this.contains(e.target) && e.button !== 2) {
this.collapse()
}
}
connectedCallback() {
this.setAttribute('role', 'listbox')
this.setAttribute('aria-label', 'dropdown menu')
const slot = this.shadowRoot.querySelector('.options slot')
slot.addEventListener('slotchange', e => {
this.availableOptions = e.target.assignedElements()
this.containerDimensions = this.optionList.getBoundingClientRect()
});
this.addEventListener('click', this.handleClick)
this.addEventListener('keydown', this.handleKeyDown)
}
disconnectedCallback() {
this.removeEventListener('click', this.handleClick)
this.removeEventListener('keydown', this.handleKeyDown)
document.removeEventListener('mousedown', this.handleClickOutside)
}
})
// option
const menuOption = document.createElement('template')
menuOption.innerHTML = `
`;
customElements.define('menu-option', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(menuOption.content.cloneNode(true))
}
connectedCallback() {
this.setAttribute('role', 'option')
this.setAttribute('tabindex', '0')
this.addEventListener('keyup', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
this.click()
}
})
}
})