aboutsummaryrefslogtreecommitdiff
path: root/src/routes/+layout.svelte
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-05-19 02:19:44 +0000
committerFuwn <[email protected]>2026-05-19 02:19:44 +0000
commit383823a48e7e5a5b3d13a87b3b81475bf06d4994 (patch)
treef55b5d468909d07ec346681e8600ae7b777323a5 /src/routes/+layout.svelte
parentfeat(nav): collapse hamburger menu on scroll (diff)
downloaddue.moe-383823a48e7e5a5b3d13a87b3b81475bf06d4994.tar.xz
due.moe-383823a48e7e5a5b3d13a87b3b81475bf06d4994.zip
feat(details): animate details open/close via Web Animations API
A document-level click delegate intercepts <summary> clicks and animates the parent <details> height instead of relying on the browser's instant snap. Initial attempt used the CSS pseudo-element approach (::details-content + interpolate-size: allow-keywords). Both features have shipped at different times across browsers (Safari < 18.2 has neither, Safari 18.2 to 25 only has the pseudo) and degrade in distinct broken ways. JS via WAAPI works in every browser that has shipped Web Animations. Closed height is computed from the summary's offsetHeight plus the details element's vertical padding and border (read from getComputedStyle), so the animation end-state matches the natural collapsed height regardless of per-element padding tweaks (details-unstyled, card variants, etc). Earlier draft animated to summary.offsetHeight only, which undershot by 2 * padding and caused the element to clip text before snapping back to its resting height. Respects prefers-reduced-motion (bypass to native toggle). Uses a WeakMap so rapid toggles cancel the in-flight animation cleanly. Duration 240ms / cubic-bezier(0.22,1,0.36,1) matches the panel-class motion token used elsewhere.
Diffstat (limited to 'src/routes/+layout.svelte')
-rw-r--r--src/routes/+layout.svelte51
1 files changed, 51 insertions, 0 deletions
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 625f58b1..8c36bb0f 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -101,6 +101,55 @@ const handleScroll = () => {
if (isMenuOpen) isMenuOpen = false;
};
+const detailsAnimations = new WeakMap<HTMLDetailsElement, Animation>();
+
+const animateDetails = (e: MouseEvent) => {
+ const summary = (e.target as HTMLElement | null)?.closest("summary");
+ if (!summary) return;
+ const details = summary.parentElement as HTMLDetailsElement | null;
+ if (!details || details.tagName !== "DETAILS") return;
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
+
+ e.preventDefault();
+ detailsAnimations.get(details)?.cancel();
+
+ const detailsStyle = getComputedStyle(details);
+ const closedHeight =
+ summary.offsetHeight +
+ parseFloat(detailsStyle.paddingTop) +
+ parseFloat(detailsStyle.paddingBottom) +
+ parseFloat(detailsStyle.borderTopWidth) +
+ parseFloat(detailsStyle.borderBottomWidth);
+ const startHeight = details.offsetHeight;
+
+ if (details.open) {
+ details.style.overflow = "hidden";
+ const animation = details.animate(
+ { height: [`${startHeight}px`, `${closedHeight}px`] },
+ { duration: 240, easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
+ );
+ detailsAnimations.set(details, animation);
+ animation.onfinish = () => {
+ details.open = false;
+ details.style.overflow = "";
+ detailsAnimations.delete(details);
+ };
+ } else {
+ details.style.overflow = "hidden";
+ details.open = true;
+ const fullHeight = details.offsetHeight;
+ const animation = details.animate(
+ { height: [`${closedHeight}px`, `${fullHeight}px`] },
+ { duration: 240, easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
+ );
+ detailsAnimations.set(details, animation);
+ animation.onfinish = () => {
+ details.style.overflow = "";
+ detailsAnimations.delete(details);
+ };
+ }
+};
+
onMount(async () => {
if (browser) {
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
@@ -223,6 +272,8 @@ $: {
on:keydown={(e) => { if (e.key === 'Escape' && isMenuOpen) isMenuOpen = false; }}
on:click={(e) => {
if (isMenuOpen && !(e.target as HTMLElement).closest('.header')) isMenuOpen = false;
+
+ animateDetails(e);
}}
/>