60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* FAQ accordion — accessible disclosure pattern.
|
|
* - Real <button aria-expanded aria-controls> toggles each panel.
|
|
* - Panel height animates via Motion (height auto), with a reduced-motion guard.
|
|
* - Single-open behaviour; arrow rotates; keyboard + screen-reader friendly.
|
|
* The full Q&A text is always in the DOM (good for AEO / FAQPage schema).
|
|
*/
|
|
import { useState } from "react";
|
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
import { faqs } from "../content";
|
|
|
|
export default function Faq() {
|
|
const [open, setOpen] = useState<number | null>(0);
|
|
const reduce = useReducedMotion();
|
|
|
|
return (
|
|
<ul className="faq">
|
|
{faqs.map((f, i) => {
|
|
const isOpen = open === i;
|
|
return (
|
|
<li className={`faq__item ${isOpen ? "is-open" : ""}`} key={f.q}>
|
|
<h3 className="faq__q">
|
|
<button
|
|
className="faq__btn"
|
|
aria-expanded={isOpen}
|
|
aria-controls={`faq-panel-${i}`}
|
|
id={`faq-btn-${i}`}
|
|
onClick={() => setOpen(isOpen ? null : i)}
|
|
>
|
|
<span>{f.q}</span>
|
|
<span className="faq__icon" aria-hidden="true">
|
|
<span />
|
|
<span />
|
|
</span>
|
|
</button>
|
|
</h3>
|
|
<AnimatePresence initial={false}>
|
|
{isOpen && (
|
|
<motion.div
|
|
id={`faq-panel-${i}`}
|
|
role="region"
|
|
aria-labelledby={`faq-btn-${i}`}
|
|
className="faq__panel"
|
|
initial={reduce ? { height: "auto", opacity: 1 } : { height: 0, opacity: 0 }}
|
|
animate={{ height: "auto", opacity: 1 }}
|
|
exit={reduce ? { height: "auto", opacity: 1 } : { height: 0, opacity: 0 }}
|
|
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
|
|
>
|
|
<p className="faq__a">{f.a}</p>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
);
|
|
}
|