Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
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