Skip to content

Instantly share code, notes, and snippets.

@rodrigolira
Last active May 24, 2023 15:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rodrigolira/b5554e28f1f4ce8b90df15d8916d75c6 to your computer and use it in GitHub Desktop.
Save rodrigolira/b5554e28f1f4ce8b90df15d8916d75c6 to your computer and use it in GitHub Desktop.
Animated dropdown native Web Component
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);
<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