SPB Git

Repository Public

TestimonialSlider.tsx 3.8 KB 107 lines tsx

1"use client";23import { useCallback, useEffect, useId, useState, useSyncExternalStore } from "react";4import { QuoteMark } from "@/components/ui/Icons";56type Item = { quote: string; author: string; role: string };78const INTERVAL_MS = 8000;9const RM_QUERY = "(prefers-reduced-motion: reduce)";10function subscribeReducedMotion(cb: () => void) {11  const mq = window.matchMedia(RM_QUERY);12  mq.addEventListener("change", cb);13  return () => mq.removeEventListener("change", cb);14}15function getReducedMotion() {16  return window.matchMedia(RM_QUERY).matches;17}1819export function TestimonialSlider({ items }: { items: Item[] }) {20  const [index, setIndex] = useState(0);21  const [paused, setPaused] = useState(false);22  const reduced = useSyncExternalStore(subscribeReducedMotion, getReducedMotion, () => false);23  const regionId = useId();2425  const go = useCallback(26    (n: number) => setIndex((i) => (i + n + items.length) % items.length),27    [items.length],28  );2930  useEffect(() => {31    if (items.length < 2 || paused || reduced) return;32    const t = setInterval(() => go(1), INTERVAL_MS);33    return () => clearInterval(t);34  }, [go, items.length, paused, reduced]);3536  const current = items[index];37  const multiple = items.length > 1;3839  return (40    <section41      aria-roledescription="carrousel"42      aria-label="Témoignages de clients"43      className="bg-pale-200 relative rounded-[var(--radius-xl)] px-6 py-8 sm:px-10 md:px-14 md:py-10"44      onMouseEnter={() => setPaused(true)}45      onMouseLeave={() => setPaused(false)}46      onFocusCapture={() => setPaused(true)}47      onBlurCapture={(e) => {48        if (!e.currentTarget.contains(e.relatedTarget as Node)) setPaused(false);49      }}50      onKeyDown={(e) => {51        if (!multiple) return;52        if (e.key === "ArrowRight") go(1);53        if (e.key === "ArrowLeft") go(-1);54      }}55    >56      <div className="flex flex-col gap-6 md:flex-row md:items-start md:gap-8">57        <QuoteMark className="text-gold-400 h-9 w-9 shrink-0 md:h-11 md:w-11" />58        <div id={regionId} aria-live="polite" aria-atomic="true" className="flex-1">59          <blockquote>60            <p className="text-navy-900 font-serif text-xl leading-relaxed italic md:text-[1.45rem]">61              {current.quote}62            </p>63            <footer className="text-ink-700 mt-5 text-sm">64              <span aria-hidden="true">— </span>65              <cite className="text-ink-900 font-semibold not-italic">{current.author}</cite>66              {current.role ? <span>, {current.role}</span> : null}67            </footer>68          </blockquote>69        </div>7071        {multiple ? (72          <div73            className="flex items-center gap-3 self-end md:self-center"74            role="group"75            aria-label="Pagination des témoignages"76          >77            <button78              type="button"79              onClick={() => setPaused((p) => !p)}80              aria-pressed={paused}81              className="text-ink-700 rounded-md px-2 py-1 text-xs hover:bg-white/50"82            >83              {paused ? "Reprendre" : "Pause"}84            </button>85            <ul className="flex items-center gap-2.5">86              {items.map((_, i) => (87                <li key={i}>88                  <button89                    type="button"90                    aria-label={`Témoignage ${i + 1} sur ${items.length}`}91                    aria-current={i === index ? "true" : undefined}92                    aria-controls={regionId}93                    onClick={() => setIndex(i)}94                    className={`block h-2.5 w-2.5 rounded-full transition-colors ${95                      i === index ? "bg-navy-900" : "bg-pale-400 hover:bg-slate-500"96                    }`}97                  />98                </li>99              ))}100            </ul>101          </div>102        ) : null}103      </div>104    </section>105  );106}107
Files main
Menu

Theme