Last active
May 24, 2023 15:27
-
-
Save rodrigolira/b5554e28f1f4ce8b90df15d8916d75c6 to your computer and use it in GitHub Desktop.
Animated dropdown native Web Component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Dropdown extends HTMLElement { | |
constructor() { | |
super(); | |
this._documentClick = this._closeOnDocumentClick.bind(this); | |
this._dropdownAlignment = "left"; | |
this._open = false; | |
this.attachShadow({ mode: 'open' }); | |
this.shadowRoot.innerHTML = /*html*/` | |
<style> | |
:host { | |
position: relative; | |
display: inline-block; | |
} | |
.tks-dropdown-trigger { | |
cursor: pointer; | |
} | |
.tks-dropdown { | |
position: absolute; | |
z-index: var(--tks-dropdown-elevation, 100); | |
transition-property: opacity,transform; | |
} | |
.tks-dropdown-hidden { | |
display: none; | |
} | |
.tks-dropdown-left { | |
left: 0; | |
transform-origin: top left; | |
} | |
.tks-dropdown-right { | |
right: 0; | |
transform-origin: top right; | |
} | |
.opening-dropdown { | |
transition-duration: .1s; | |
transition-timing-function: cubic-bezier(0,0,.2,1); | |
} | |
.closing-dropdown { | |
transition-duration: 75ms; | |
transition-timing-function: cubic-bezier(.4,0,1,1); | |
} | |
.opening-dropdown-start, | |
.closing-dropdown-end { | |
opacity: 0; | |
transform: scaleX(.95) scaleY(.95); | |
} | |
.opening-dropdown-end, | |
.closing-dropdown-start { | |
opacity: 1; | |
transform: scaleX(1) scaleY(1); | |
} | |
</style> | |
<div class="tks-dropdown-trigger"> | |
<slot name="dropdown-trigger"></slot> | |
</div> | |
<div class="tks-dropdown tks-dropdown-hidden ${this._dropdownAlignment === "right" ? "tks-dropdown-right" : "tks-dropdown-left"}"> | |
<slot name="dropdown-content"></slot> | |
</div> | |
`; | |
} | |
connectedCallback() { | |
document.addEventListener('click', this._documentClick); | |
} | |
disconnectedCallback() { | |
document.removeEventListener('click', this._documentClick); | |
} | |
_nextFrame() { | |
return new Promise(resolve => { | |
requestAnimationFrame(() => { | |
requestAnimationFrame(resolve); | |
}); | |
}); | |
} | |
_afterTransition(element) { | |
return new Promise(resolve => { | |
const duration = Number(getComputedStyle(element).transitionDuration.replace('s', '')) * 1000; | |
setTimeout(() => { | |
resolve(); | |
}, duration); | |
}); | |
} | |
static get observedAttributes() { | |
return ['align']; | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
if (newValue === oldValue) { | |
return; | |
} | |
if (name === "align") { | |
this._dropdownAlignment = newValue === "right" ? "right" : "left"; | |
this._render(); | |
} | |
} | |
_render() { | |
const dropdownElement = this.shadowRoot.querySelector(".tks-dropdown"); | |
dropdownElement.classList.remove("tks-dropdown-right", "tks-dropdown-left"); | |
dropdownElement.classList.add(`tks-dropdown-${this._dropdownAlignment}`); | |
} | |
async _closeOnDocumentClick(event) { | |
if (!event.composedPath().includes(this)) { | |
this.close(); | |
} | |
} | |
async _openDropdown() { | |
const dropdownElement = this.shadowRoot.querySelector(".tks-dropdown"); | |
dropdownElement.classList.remove('tks-dropdown-hidden'); | |
dropdownElement.classList.add('opening-dropdown'); | |
dropdownElement.classList.add('opening-dropdown-start'); | |
await this._nextFrame(); | |
dropdownElement.classList.remove('opening-dropdown-start'); | |
dropdownElement.classList.add('opening-dropdown-end'); | |
await this._afterTransition(dropdownElement); | |
dropdownElement.classList.remove('opening-dropdown'); | |
dropdownElement.classList.remove('opening-dropdown-end'); | |
} | |
async _closeDropdown() { | |
const dropdownElement = this.shadowRoot.querySelector(".tks-dropdown"); | |
dropdownElement.classList.add('closing-dropdown'); | |
dropdownElement.classList.add('closing-dropdown-start'); | |
await this._nextFrame(); | |
dropdownElement.classList.remove('closing-dropdown-start'); | |
dropdownElement.classList.add('closing-dropdown-end'); | |
await this._afterTransition(dropdownElement); | |
dropdownElement.classList.remove('closing-dropdown'); | |
dropdownElement.classList.remove('closing-dropdown-end'); | |
dropdownElement.classList.add('tks-dropdown-hidden'); | |
} | |
async open() { | |
if (!this.isOpen) { | |
this._open = true; | |
await this._openDropdown(); | |
} | |
} | |
async close() { | |
if (this.isOpen) { | |
this._open = false; | |
await this._closeDropdown(); | |
} | |
} | |
async toggle() { | |
if (this.isOpen) { | |
this._open = false; | |
await this._closeDropdown(); | |
} | |
else { | |
this._open = true; | |
await this._openDropdown(); | |
} | |
} | |
get isOpen() { | |
return this._open; | |
} | |
} | |
customElements.define("tks-dropdown", Dropdown); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="Dropdown.js"></script> | |
<tks-dropdown align="right"> | |
<button id="btn-dropdown-trigger" slot="dropdown-trigger" class="btn btn-primary">Menu</button> | |
<ul slot="dropdown-content" class="list-group"> | |
<li class="list-group-item">Cras justo odio</li> | |
<li class="list-group-item">Dapibus ac facilisis in</li> | |
<li class="list-group-item">Morbi leo risus</li> | |
<li class="list-group-item">Porta ac consectetur ac</li> | |
<li class="list-group-item">Vestibulum at eros</li> | |
</ul> | |
</tks-dropdown> | |
<script> | |
const tksDropdown = document.querySelector("tks-dropdown"); | |
document.querySelector("#btn-dropdown-trigger").addEventListener("click", () => tksDropdown.toggle()); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment