49 lines
3.4 KiB
TypeScript
49 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { useI18n } from "@/components/i18n";
|
|
import { useEffect, useRef } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { useBackdropDismiss } from "@/components/backdrop-dismiss";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
export function ConnectionRow({ label, action, actionVariant, disabled, onAction, children, testid, actionTestid, labelClassName }: { label: string; action: string; actionVariant?: "outline" | "ghost"; disabled: boolean; onAction: () => void; children?: React.ReactNode; testid?: string; actionTestid?: string; labelClassName?: string }) {
|
|
const variant = actionVariant ?? (action === "Connect" ? "outline" : "ghost");
|
|
return <div className="flex flex-wrap items-center gap-3" data-testid={testid}>
|
|
<span className={labelClassName ?? "font-ui text-xs text-foreground"}>{label}</span>
|
|
{children}
|
|
<Button type="button" size="xs" variant={variant} className={`cursor-pointer disabled:cursor-not-allowed ${variant === "outline" ? "" : "text-muted-foreground hover:text-foreground"}`} data-testid={actionTestid} disabled={disabled} onClick={onAction}>{action}</Button>
|
|
</div>;
|
|
}
|
|
|
|
export function ConnectionFooter({ leading, children, testid }: { leading?: React.ReactNode; children: React.ReactNode; testid?: string }) {
|
|
return <div className="mt-4 flex flex-wrap items-center justify-between gap-3" data-testid={testid}>
|
|
{leading}
|
|
<div className="ml-auto flex flex-wrap items-center justify-end gap-2">{children}</div>
|
|
</div>;
|
|
}
|
|
|
|
export function ConnectionDialog({ children, onClose, busy, label, testid, backdropTestid }: { children: React.ReactNode; onClose: () => void; busy: boolean; label: string; testid: string; backdropTestid: string }) {
|
|
const ref = useRef<HTMLDivElement>(null), closeRef = useRef(onClose);
|
|
closeRef.current = onClose;
|
|
const backdropDismiss = useBackdropDismiss(() => { if (!busy) onClose(); });
|
|
useEffect(() => {
|
|
const previous = document.activeElement as HTMLElement | null;
|
|
const focusable = () => [...(ref.current?.querySelectorAll<HTMLElement>('button:not([disabled]), a[href], input:not([disabled]), select:not([disabled])') ?? [])];
|
|
focusable()[0]?.focus();
|
|
const key = (event: KeyboardEvent) => {
|
|
if (event.key === "Escape") { event.preventDefault(); event.stopImmediatePropagation(); closeRef.current(); }
|
|
else if (event.key === "Tab") {
|
|
const items = focusable(), first = items[0], last = items.at(-1);
|
|
if (!first) { event.preventDefault(); return; }
|
|
if (event.shiftKey && (document.activeElement === first || !ref.current?.contains(document.activeElement))) { event.preventDefault(); last?.focus(); }
|
|
else if (!event.shiftKey && (document.activeElement === last || !ref.current?.contains(document.activeElement))) { event.preventDefault(); first.focus(); }
|
|
}
|
|
};
|
|
document.addEventListener("keydown", key, true);
|
|
return () => { document.removeEventListener("keydown", key, true); if (previous?.isConnected) previous.focus(); };
|
|
}, []);
|
|
return createPortal(<div className="fixed inset-0 z-[60] flex items-center justify-center bg-background/80" data-testid={backdropTestid} {...backdropDismiss}>
|
|
<div ref={ref} role="dialog" aria-modal="true" aria-label={label} aria-busy={busy} data-testid={testid} onClick={(event) => event.stopPropagation()} className="settings-dialog-pop mx-4 max-h-[85vh] w-full max-w-lg overflow-y-auto rounded-md border border-border bg-card p-5 shadow-xl">{children}</div>
|
|
</div>, document.body);
|
|
}
|