agency-web/app/components/Faq.tsx

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>
);
}