Skip to content

Instantly share code, notes, and snippets.

@silvester-pari
Last active February 6, 2024 13:18
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 silvester-pari/2912bb049fa906671807415eb0e87188 to your computer and use it in GitHub Desktop.
Save silvester-pari/2912bb049fa906671807415eb0e87188 to your computer and use it in GitHub Desktop.
Animated Details

Animated Details

Tiny web component to animate opening/closing of HTML details element.

📦 Install

npm i -S gist:2912bb049fa906671807415eb0e87188

Usage

import 'animated-details'
<details is="animated-details">
  <summary>...</summary>
  <div>...</div>
</details>
class AnimatedDetails extends HTMLDetailsElement {
// this is an adaptation of // https://css-tricks.com/how-to-animate-the-details-element-using-waapi/
// which turns the class into a custom element
// additionally, the "exclusive" attribute has been introduced
// for accordion-style exclusiv eopening/closing
constructor() {
super();
}
connectedCallback() {
// Store the <details> element
this.el = this;
// Store the <summary> element
this.summary = this.querySelector("summary");
// Store the <div class="content"> element
this.content = this.lastElementChild;
// Store the animation object (so we can cancel it if needed)
this.animation = null;
// Store if the element is closing
this.isClosing = false;
// Store if the element is expanding
this.isExpanding = false;
// Detect user clicks on the summary element
this.summary.addEventListener("click", (e) => this.onClick(e));
}
onClick(e) {
// Stop default behaviour from the browser
e?.preventDefault();
// Add an overflow on the <details> to avoid content overflowing
this.el.style.overflow = "hidden";
// Check if the element is being closed or is already closed
if (this.isClosing || !this.el.open) {
this.doOpen();
// Check if the element is being openned or is already open
} else if (this.isExpanding || this.el.open) {
this.shrink();
}
}
shrink() {
// Set the element as "being closed"
this.isClosing = true;
// Store the current height of the element
const startHeight = `${this.el.offsetHeight}px`;
// Calculate the height of the summary
const endHeight = `${this.summary.offsetHeight}px`;
// If there is already an animation running
if (this.animation) {
// Cancel the current animation
this.animation.cancel();
}
// Start a WAAPI animation
this.animation = this.el.animate(
{
// Set the keyframes from the startHeight to endHeight
height: [startHeight, endHeight],
},
{
duration: 300,
easing: "ease-out",
},
);
// When the animation is complete, call onAnimationFinish()
this.animation.onfinish = () => this.onAnimationFinish(false);
// If the animation is cancelled, isClosing variable is set to false
this.animation.oncancel = () => (this.isClosing = false);
}
doOpen() {
// Apply a fixed height on the element
this.el.style.height = `${this.el.offsetHeight}px`;
// Force the [open] attribute on the details element
this.el.open = true;
// Wait for the next frame to call the expand function
window.requestAnimationFrame(() => this.expand());
//console.log(this.hasAttribute("exclusive"))
const siblings = this.parentElement.querySelectorAll(
"details[open][exclusive]",
);
siblings.forEach((s) => {
if (s !== this) {
s.onClick();
}
});
}
expand() {
// Set the element as "being expanding"
this.isExpanding = true;
// Get the current fixed height of the element
const startHeight = `${this.el.offsetHeight}px`;
// Calculate the open height of the element (summary height + content height)
const endHeight = `${
this.summary.offsetHeight + this.content.offsetHeight
}px`;
// If there is already an animation running
if (this.animation) {
// Cancel the current animation
this.animation.cancel();
}
// Start a WAAPI animation
this.animation = this.el.animate(
{
// Set the keyframes from the startHeight to endHeight
height: [startHeight, endHeight],
},
{
duration: 300,
easing: "ease-out",
},
);
// When the animation is complete, call onAnimationFinish()
this.animation.onfinish = () => this.onAnimationFinish(true);
// If the animation is cancelled, isExpanding variable is set to false
this.animation.oncancel = () => (this.isExpanding = false);
}
onAnimationFinish(open) {
// Set the open attribute based on the parameter
this.el.open = open;
// Clear the stored animation
this.animation = null;
// Reset isClosing & isExpanding
this.isClosing = false;
this.isExpanding = false;
// Remove the overflow hidden and the fixed height
this.el.style.height = this.el.style.overflow = "";
}
}
customElements.define("animated-details", AnimatedDetails, {
extends: "details",
});
{
"version": "0.0.1",
"name": "animated-details",
"main": "animatedDetails.js"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment