97 lines
2.5 KiB
TypeScript
97 lines
2.5 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "react"
|
|
import { createPortal } from "react-dom"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
export interface AnchorRect {
|
|
left: number
|
|
top: number
|
|
width: number
|
|
height: number
|
|
}
|
|
|
|
/**
|
|
* Carte flottante ancrée à un rectangle (popover détails / création rapide),
|
|
* positionnée en `fixed` et recadrée dans la fenêtre.
|
|
*/
|
|
export function AgendaFloatingCard({
|
|
anchor,
|
|
onClose,
|
|
children,
|
|
className,
|
|
width = 416,
|
|
}: {
|
|
anchor: AnchorRect
|
|
onClose: () => void
|
|
children: ReactNode
|
|
className?: string
|
|
width?: number
|
|
}) {
|
|
const cardRef = useRef<HTMLDivElement>(null)
|
|
const [pos, setPos] = useState<{ left: number; top: number } | null>(null)
|
|
|
|
useLayoutEffect(() => {
|
|
const card = cardRef.current
|
|
if (!card) return
|
|
const margin = 8
|
|
const vw = window.innerWidth
|
|
const vh = window.innerHeight
|
|
const rect = card.getBoundingClientRect()
|
|
const w = Math.min(width, vw - margin * 2)
|
|
const h = rect.height
|
|
|
|
// Préférence : à droite de l'ancre, sinon à gauche, sinon en dessous.
|
|
let left = anchor.left + anchor.width + margin
|
|
if (left + w > vw - margin) left = anchor.left - w - margin
|
|
if (left < margin) left = Math.min(Math.max(margin, anchor.left), vw - w - margin)
|
|
|
|
let top = anchor.top
|
|
if (top + h > vh - margin) top = vh - h - margin
|
|
if (top < margin) top = margin
|
|
|
|
setPos({ left, top })
|
|
}, [anchor, width, children])
|
|
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
e.stopPropagation()
|
|
onClose()
|
|
}
|
|
}
|
|
window.addEventListener("keydown", onKey, true)
|
|
return () => window.removeEventListener("keydown", onKey, true)
|
|
}, [onClose])
|
|
|
|
if (typeof document === "undefined") return null
|
|
|
|
return createPortal(
|
|
<div className="fixed inset-0 z-[60]">
|
|
<button
|
|
type="button"
|
|
aria-label="Fermer"
|
|
className="absolute inset-0 cursor-default"
|
|
onClick={onClose}
|
|
/>
|
|
<div
|
|
ref={cardRef}
|
|
role="dialog"
|
|
className={cn(
|
|
"absolute flex max-h-[85vh] flex-col overflow-hidden rounded-xl border border-border/60 bg-popover text-popover-foreground shadow-2xl",
|
|
className,
|
|
)}
|
|
style={{
|
|
width: Math.min(width, window.innerWidth - 16),
|
|
left: pos?.left ?? -9999,
|
|
top: pos?.top ?? -9999,
|
|
visibility: pos ? "visible" : "hidden",
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
)
|
|
}
|