standard-ui/components/dist/menu.js
sairaj mote 13b9c16d59 feature improvements
-- Removed extra spaces and new line characters  from component string
2022-12-11 19:33:36 +05:30

309 lines
8.8 KiB
JavaScript

const smMenu = document.createElement('template')
smMenu.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
:host{
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
}
.menu{
display: -ms-grid;
display: grid;
place-items: center;
height: 2rem;
width: 2rem;
outline: none;
}
.icon {
position: absolute;
fill: rgba(var(--text-color), 0.7);
height: 2.4rem;
width: 2.4rem;
padding: 0.5rem;
border-radius: 2rem;
-webkit-transition: background 0.3s;
-o-transition: background 0.3s;
transition: background 0.3s;
}
.select{
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
cursor: pointer;
width: 100%;
-webkit-tap-highlight-color: transparent;
}
.menu:focus .icon,
.focused{
background: rgba(var(--text-color), 0.1);
}
:host([align-options="left"]) .options{
left: 0;
}
:host([align-options="right"]) .options{
right: 0;
}
.options{
top: 100%;
padding: 0.3rem;
overflow: hidden auto;
position: absolute;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
min-width: -webkit-max-content;
min-width: -moz-max-content;
min-width: max-content;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
background: var(--background, rgba(var(--background-color), 1));
border-radius: var(--border-radius, 0.5rem);
z-index: 1;
-webkit-box-shadow: 0 0.5rem 1.5rem -0.5rem rgba(0,0,0,0.3);
box-shadow: 0 0.5rem 1.5rem -0.5rem rgba(0,0,0,0.3);
bottom: auto;
}
.hide{
display: none;
}
@media (hover: hover){
.menu:hover .icon{
background: rgba(var(--text-color), 0.1);
}
}
</style>
<div class="select">
<div class="menu" tabindex="0">
<slot name="icon">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
</slot>
</div>
<div class="options hide">
<slot></slot>
</div>
</div>`;
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 = `
<style>
*{
padding: 0;
margin: 0;
box-sizing: border-box;
}
:host{
display: flex;
border-radius: var(--border-radius,0.3rem);
overflow: hidden;
}
.option{
display: flex;
min-width: 100%;
padding: var(--padding, 0.6rem 1rem);
cursor: pointer;
overflow-wrap: break-word;
white-space: nowrap;
outline: none;
user-select: none;
align-items: center;
}
:host(:focus){
outline: none;
background: rgba(var(--text-color), 0.1);
}
@media (any-hover: hover){
.option{
transition: background-color 0.2s;
}
.option:hover{
background: rgba(var(--text-color), 0.1);
}
}
</style>
<div class="option">
<slot></slot>
</div>`;
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()
}
})
}
})