163 lines
5.0 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
}
|