gameflow-deck/src/mainview/components/ContextDialog.tsx

131 lines
No EOL
5.1 KiB
TypeScript

import { FocusContext, FocusDetails, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import classNames from "classnames";
import { createContext, JSX, useContext, useEffect } from "react";
import { twMerge } from "tailwind-merge";
import { X } from "lucide-react";
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
const ContextDialogContext = createContext({} as {
close: () => void,
id: string;
});
export function ContextList (data: { options?: DialogEntry[]; className?: string; showCloseButton?: boolean; })
{
const context = useContext(ContextDialogContext);
return <ul className={twMerge("list", data.className)}>
{data.options?.map(o => <OptionElement className="list-row" key={o.id} {...o} />)}
{data.showCloseButton !== false && <OptionElement className="list-row" type='accent' icon={<X />} action={() => context.close()} id="close-context-dialog" content="Close" />}
</ul>;
}
export function OptionElement (data: DialogEntry & { onFocus?: () => void; className?: string; })
{
const context = useContext(ContextDialogContext);
const handleFocus = () =>
{
(ref.current as HTMLElement).scrollIntoView({ block: 'nearest', behavior: 'smooth' });
data.onFocus?.();
};
const handleAction = data.action ? () => data.action?.({ close: context.close, focus: focusSelf }) : undefined;
const { ref, focused, focusSelf, focusKey, hasFocusedChild } = useFocusable({
focusKey: `${context.id}-list-option-${data.id}`,
onEnterPress: data.shortcuts ? undefined : handleAction,
onFocus: handleFocus,
trackChildren: typeof data.content !== 'string'
});
const colors = {
primary: classNames("hover:bg-primary/40", { "bg-primary text-primary-content": focused || hasFocusedChild }),
secondary: classNames("hover:bg-secondary/40", { "bg-secondary text-secondary-content": focused || hasFocusedChild }),
accent: classNames("hover:bg-accent/40", { "bg-accent text-accent-content": focused || hasFocusedChild }),
info: classNames("hover:bg-info/40", { "bg-info text-info-content": focused || hasFocusedChild }),
warning: classNames("hover:bg-warning/40", { "bg-warning text-warning-content": focused || hasFocusedChild }),
error: classNames("hover:bg-error/40", { "bg-error text-error-content": focused || hasFocusedChild })
};
if (data.shortcuts)
{
useShortcuts(focusKey, () => data.shortcuts!, [data.shortcuts]);
}
return <li ref={ref}
onClick={handleAction}
className={
twMerge("flex cursor-pointer sm:text-sm md:text-base")}>
<FocusContext value={focusKey}>
<div className={twMerge("flex w-full sm:h-12 md:h-14 items-center px-4 rounded-2xl transition-all gap-2",
classNames({ "font-semibold": focused || hasFocusedChild }),
data.className,
colors[data.type])}>
{data.icon}
{data.content}
</div>
</FocusContext>
</li>;
}
export interface DialogEntry
{
id: string,
content: string | JSX.Element;
icon?: string | JSX.Element;
type: 'primary' | 'secondary' | 'accent' | 'info' | 'warning' | 'error';
action?: (ctx: { close: () => void, focus: (focusDetails?: FocusDetails | undefined) => void; }) => void;
shortcuts?: Shortcut[];
}
export function ContextDialog (data: {
id: string,
children: any | any[],
open: boolean,
close: () => void;
className?: string;
preferredChildFocusKey?: string;
})
{
const { ref, focusKey, focusSelf } = useFocusable({
focusable: data.open,
focusKey: `${data.id}-context-dialog`,
isFocusBoundary: true,
preferredChildFocusKey: data.preferredChildFocusKey
});
useEffect(() =>
{
if (data.open)
{
focusSelf();
}
}, [data.open]);
useShortcuts(focusKey, () => data.open ? [{
label: "Close",
button: GamePadButtonCode.B,
action: () =>
{
data.close();
}
}] : [], [data.open]);
return <dialog ref={ref} open={data.open} closedby="any" className={
twMerge("absolute modal cursor-pointer bg-base-300/80 backdrop-brightness-50 duration-300 ease-in-out transition-all text-base-content",
classNames({ "opacity-0": !data.open }))
}
onClick={() =>
{
if (data.open) data.close();
}}>
<FocusContext value={focusKey}>
<ContextDialogContext value={{ id: data.id, close: data.close }} >
<div
className={twMerge(
"bg-base-100/80 delay-200 rounded-4xl sm:p-4 md:p-6 sm:min-w-[80vw] md:min-w-[20vw] cursor-auto",
data.open ? "animate-scale-delayed" : "opacity-0",
data.className)
}
style={{ backdropFilter: 'md:blur(24px)' }}
onClick={(e) => e.stopPropagation()}
>
{data.children}
</div>
</ContextDialogContext>
</FocusContext>
</dialog>;
}