SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
1 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
3.0 KB · 69 lines tsx
Raw Blame History
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