Files
site-portfolio/public/js/modules/ui/skills-accordion.js
T

163 lines
5.0 KiB
JavaScript

import { prefersReducedMotion } from "../shared/media.js";
export function initSkillsAccordion() {
const root = document.querySelector("#skills");
if (!root) return;
const toggles = Array.from(root.querySelectorAll("[data-skills-accordion-toggle]"));
if (!toggles.length) return;
const toggleToPanel = new Map();
toggles.forEach((toggle) => {
const panelId = toggle.getAttribute("aria-controls");
if (!panelId) return;
const panel = root.querySelector(`#${panelId}`);
if (!panel) return;
toggleToPanel.set(toggle, panel);
});
if (!toggleToPanel.size) return;
const reducedMotion = prefersReducedMotion();
let locked = false;
function getHeaderOffsetPx() {
const raw = getComputedStyle(document.documentElement).getPropertyValue("--header-offset").trim();
const parsed = Number.parseFloat(raw);
if (!Number.isFinite(parsed)) return 52;
return Math.max(parsed, 52);
}
function ensureToggleVisible(toggle) {
const rect = toggle.getBoundingClientRect();
const viewportH = window.innerHeight || document.documentElement.clientHeight || 0;
const topSafe = getHeaderOffsetPx() + 12;
const bottomSafe = viewportH - 12;
const targetTop = window.scrollY + rect.top - topSafe;
if (rect.top < topSafe || rect.bottom > bottomSafe) {
window.scrollTo({
top: Math.max(targetTop, 0),
behavior: reducedMotion ? "auto" : "smooth",
});
}
}
function clearPanelAnimation(panel) {
panel.style.height = "";
panel.style.opacity = "";
panel.style.transition = "";
panel.style.overflow = "";
panel.style.willChange = "auto";
}
function expandPanel(panel) {
if (reducedMotion) {
panel.hidden = false;
clearPanelAnimation(panel);
return Promise.resolve();
}
panel.hidden = false;
panel.style.opacity = "0";
panel.style.willChange = "opacity";
const duration = 220;
return new Promise((resolve) => {
requestAnimationFrame(() => {
panel.style.transition = `opacity ${duration}ms ease`;
panel.style.opacity = "1";
});
window.setTimeout(() => {
clearPanelAnimation(panel);
resolve();
}, duration + 40);
});
}
function collapsePanel(panel) {
if (panel.hidden) return Promise.resolve();
if (reducedMotion) {
panel.hidden = true;
clearPanelAnimation(panel);
return Promise.resolve();
}
const startHeight = panel.scrollHeight;
const duration = 240;
panel.style.height = `${startHeight}px`;
panel.style.opacity = "1";
panel.style.overflow = "hidden";
panel.style.willChange = "height, opacity";
return new Promise((resolve) => {
requestAnimationFrame(() => {
panel.style.transition = `height ${duration}ms cubic-bezier(0.2, 0.65, 0.25, 1), opacity ${duration}ms ease`;
panel.style.height = "0px";
panel.style.opacity = "0";
});
window.setTimeout(() => {
panel.hidden = true;
clearPanelAnimation(panel);
resolve();
}, duration + 40);
});
}
function setExpanded(toggle, expanded) {
const panel = toggleToPanel.get(toggle);
if (!panel) return;
toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
if (expanded) {
panel.hidden = false;
} else {
panel.hidden = true;
}
}
toggleToPanel.forEach((_, toggle) => {
const isExpanded = toggle.getAttribute("aria-expanded") === "true";
setExpanded(toggle, isExpanded);
});
toggleToPanel.forEach((_, toggle) => {
toggle.addEventListener("click", async () => {
if (locked) return;
locked = true;
const isExpanded = toggle.getAttribute("aria-expanded") === "true";
const jobs = [];
toggleToPanel.forEach((panel, otherToggle) => {
const otherExpanded = otherToggle.getAttribute("aria-expanded") === "true";
if (!otherExpanded) return;
otherToggle.setAttribute("aria-expanded", "false");
jobs.push(collapsePanel(panel));
});
if (!isExpanded) {
toggle.setAttribute("aria-expanded", "true");
const panel = toggleToPanel.get(toggle);
if (panel) jobs.push(expandPanel(panel));
}
await Promise.all(jobs);
requestAnimationFrame(() => ensureToggleVisible(toggle));
locked = false;
});
});
const categories = Array.from(root.querySelectorAll(".skills-category"));
categories.forEach((category) => {
category.addEventListener("click", (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
if (target.closest(".skills-category__panel")) return;
const toggle = category.querySelector("[data-skills-accordion-toggle]");
if (!(toggle instanceof HTMLElement)) return;
if (target.closest("[data-skills-accordion-toggle]")) return;
toggle.click();
});
});
}