Skip to content
File

Blob: worker/src/client/ui/anchored-dialog.tsx

typescript101 lines
1import type { ReactNode } from 'react';
2import {
3 autoUpdate,
4 flip,
5 FloatingFocusManager,
6 FloatingPortal,
7 offset,
8 shift,
9 size,
10 useDismiss,
11 useFloating,
12 useInteractions,
13 useRole,
14} from '@floating-ui/react';
15 
16export function AnchoredDialog({
17 id,
18 anchor,
19 title,
20 children,
21 onClose,
22 closeLabel = 'Close explanation',
23}: {
24 id: string;
25 anchor: HTMLElement | null;
26 title: ReactNode;
27 children: ReactNode;
28 onClose: () => void;
29 closeLabel?: string;
30}) {
31 const { refs, floatingStyles, context } = useFloating({
32 open: anchor !== null,
33 onOpenChange: (open) => {
34 if (!open) onClose();
35 },
36 elements: { reference: anchor },
37 placement: 'bottom-start',
38 strategy: 'fixed',
39 middleware: [
40 offset(10),
41 flip({ padding: 12, fallbackPlacements: ['top-start', 'right-start', 'left-start'] }),
42 shift({ padding: 12 }),
43 size({
44 padding: 12,
45 apply({ availableHeight, elements }) {
46 elements.floating.style.maxHeight = `${Math.max(120, availableHeight)}px`;
47 },
48 }),
49 ],
50 whileElementsMounted: autoUpdate,
51 });
52 const dismiss = useDismiss(context);
53 const role = useRole(context, { role: 'dialog' });
54 const { getFloatingProps } = useInteractions([dismiss, role]);
55 if (!anchor) return null;
56 return (
57 <FloatingPortal>
58 <FloatingFocusManager
59 context={context}
60 modal={false}
61 returnFocus
62 initialFocus={refs.floating}
63 >
64 <section
65 ref={refs.setFloating}
66 style={floatingStyles}
67 {...getFloatingProps()}
68 id={id}
69 aria-labelledby={`${id}-title`}
70 aria-describedby={`${id}-body`}
71 tabIndex={-1}
72 className="z-30 w-[min(390px,calc(100vw-24px))] overflow-y-auto overscroll-contain rounded-xl border border-line bg-leaf p-4 text-ink shadow-lg outline-none"
73 >
74 <h2
75 id={`${id}-title`}
76 className="pr-10 text-xl leading-7 font-medium tracking-tight wrap-anywhere"
77 >
78 {title}
79 </h2>
80 <button
81 type="button"
82 className="absolute top-2 right-2 inline-flex size-11 items-center justify-center rounded-md text-muted hover:bg-panel hover:text-ink active:bg-core"
83 onClick={onClose}
84 aria-label={closeLabel}
85 >
86 <span aria-hidden="true" className="text-2xl leading-none">
87 ×
88 </span>
89 </button>
90 <div
91 id={`${id}-body`}
92 className="mt-2 text-sm leading-relaxed wrap-anywhere text-muted empty:mt-0"
93 >
94 {children}
95 </div>
96 </section>
97 </FloatingFocusManager>
98 </FloatingPortal>
99 );
100}