HTML 82.1%
Python 14.6%
TypeScript 1.9%
CSS 1%
JavaScript 0.5%
1// -----------------------------------------------------------------------------2// Job·Ka — Agrégateur d'offres d'emploi (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 lieux, stations, assistant Ka.9// -----------------------------------------------------------------------------10import { ReactNode, useEffect, useRef, useState } from "react";11import { createPortal } from "react-dom";12import { Ico } from "../components/Icons";1314export default function BottomSheet({ open, onClose, title, sub, children, footer, tall = false }: {15 open: boolean; onClose: () => void; title: ReactNode; sub?: ReactNode;16 children: ReactNode; footer?: ReactNode; tall?: boolean;17}) {18 const [full, setFull] = useState(tall);19 const panel = useRef<HTMLDivElement>(null);20 const drag = useRef<{ y0: number; t0: number } | null>(null);2122 useEffect(() => {23 if (!open) return;24 setFull(tall);25 document.documentElement.classList.add("ka-scroll-lock");26 const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };27 window.addEventListener("keydown", onKey);28 const t = setTimeout(() => panel.current?.focus(), 30);29 return () => {30 document.documentElement.classList.remove("ka-scroll-lock");31 window.removeEventListener("keydown", onKey);32 clearTimeout(t);33 };34 }, [open, onClose, tall]);3536 if (!open) return null;3738 const onDown = (e: React.PointerEvent) => { drag.current = { y0: e.clientY, t0: Date.now() }; };39 const onUp = (e: React.PointerEvent) => {40 if (!drag.current) return;41 const dy = e.clientY - drag.current.y0;42 drag.current = null;43 if (dy > 90) onClose();44 else if (dy < -60) setFull(true);45 };4647 return createPortal(48 <>49 <div className="jk-sheet-backdrop" onClick={onClose} aria-hidden="true" />50 <div className={`jk-sheet ${full ? "full" : ""}`} role="dialog" aria-modal="true"51 aria-label={typeof title === "string" ? title : undefined} ref={panel} tabIndex={-1}>52 <div className="jk-sheet-handle" onPointerDown={onDown} onPointerUp={onUp} onPointerCancel={onUp} />53 <div className="jk-sheet-head" onPointerDown={onDown} onPointerUp={onUp} onPointerCancel={onUp}>54 <div style={{ minWidth: 0 }}>55 <h3 className="jk-sheet-title">{title}</h3>56 {sub && <p className="jk-sheet-sub">{sub}</p>}57 </div>58 <button type="button" className="jk-sheet-x" onClick={onClose} aria-label="Fermer">59 <Ico name="close" size={18} />60 </button>61 </div>62 <div className="jk-sheet-body">{children}</div>63 {footer && <div className="jk-sheet-foot">{footer}</div>}64 </div>65 </>,66 document.body,67 );68}69