SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
3.1 KB · 73 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/BottomSheet.tsx : panneau générique — bottom sheet sur mobile (hauteur5//   initiale 68 % du viewport, glisser vers le haut = plein écran, glisser6//   vers le bas = fermeture), modale centrée à partir de 900 px (CSS).7//   Portail à la racine, verrou du défilement (.ka-scroll-lock), Escape,8//   focus initial, aria-modal. Utilisé pour comparables, lieux, stations,9//   assistant Ka, méthodologies.10// -----------------------------------------------------------------------------11import { ReactNode, useEffect, useRef, useState } from "react";12import { createPortal } from "react-dom";13import { IcoClose } from "../components/Icons";1415export default function BottomSheet({ open, onClose, title, sub, children, footer, tall = false }: {16  open: boolean; onClose: () => void; title: ReactNode; sub?: ReactNode;17  children: ReactNode; footer?: ReactNode; tall?: boolean;18}) {19  const [full, setFull] = useState(tall);20  const panel = useRef<HTMLDivElement>(null);21  const drag = useRef<{ y0: number; t0: number } | null>(null);2223  useEffect(() => {24    if (!open) return;25    setFull(tall);26    document.documentElement.classList.add("ka-scroll-lock");27    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };28    window.addEventListener("keydown", onKey);29    // focus initial sur le panneau (lecteurs d'écran + clavier)30    const t = setTimeout(() => panel.current?.focus(), 30);31    return () => {32      document.documentElement.classList.remove("ka-scroll-lock");33      window.removeEventListener("keydown", onKey);34      clearTimeout(t);35    };36  }, [open, onClose, tall]);3738  if (!open) return null;3940  const onDown = (e: React.PointerEvent) => {41    drag.current = { y0: e.clientY, t0: Date.now() };42  };43  const onUp = (e: React.PointerEvent) => {44    if (!drag.current) return;45    const dy = e.clientY - drag.current.y0;46    drag.current = null;47    if (dy > 90) onClose();48    else if (dy < -60) setFull(true);49  };5051  return createPortal(52    <>53      <div className="lk-sheet-backdrop" onClick={onClose} aria-hidden="true" />54      <div className={`lk-sheet ${full ? "full" : ""}`} role="dialog" aria-modal="true"55           aria-label={typeof title === "string" ? title : undefined} ref={panel} tabIndex={-1}>56        <div className="lk-sheet-handle" onPointerDown={onDown} onPointerUp={onUp} onPointerCancel={onUp} />57        <div className="lk-sheet-head" onPointerDown={onDown} onPointerUp={onUp} onPointerCancel={onUp}>58          <div style={{ minWidth: 0 }}>59            <h3 className="lk-sheet-title">{title}</h3>60            {sub && <p className="lk-sheet-sub">{sub}</p>}61          </div>62          <button type="button" className="lk-sheet-x" onClick={onClose} aria-label="Fermer">63            <IcoClose size={18} />64          </button>65        </div>66        <div className="lk-sheet-body">{children}</div>67        {footer && <div className="lk-sheet-foot">{footer}</div>}68      </div>69    </>,70    document.body,71  );72}73