mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-11 05:05:07 -05:00
Strict improvement because we avoid the flash on clientside navigation. One practical bug was scroll restoration between tournament teams list and user page. When user pressed back they ended up at the bottom of the page because the placeholder (smaller height than actual content) rendered. With useHydrated this placeholder is no longer rendered for client side navigations.
103 lines
2.7 KiB
TypeScript
103 lines
2.7 KiB
TypeScript
import * as React from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import { type FetcherWithComponents, useFetcher } from "react-router";
|
|
import type { SendouButtonProps } from "~/components/elements/Button";
|
|
import { SendouDialog } from "~/components/elements/Dialog";
|
|
import { useHydrated } from "~/hooks/useHydrated";
|
|
import invariant from "~/utils/invariant";
|
|
import { SubmitButton } from "./SubmitButton";
|
|
|
|
interface ChildProps {
|
|
onPress?: () => void;
|
|
type?: "button";
|
|
}
|
|
|
|
export function FormWithConfirm({
|
|
fields,
|
|
children,
|
|
dialogHeading,
|
|
submitButtonText,
|
|
action,
|
|
submitButtonTestId = "submit-button",
|
|
submitButtonVariant = "destructive",
|
|
fetcher: _fetcher,
|
|
}: {
|
|
fields?: (
|
|
| [name: string, value: string | number]
|
|
| readonly [name: string, value: string | number]
|
|
)[];
|
|
children: React.ReactElement<ChildProps>;
|
|
dialogHeading: string;
|
|
submitButtonText?: string;
|
|
action?: string;
|
|
submitButtonTestId?: string;
|
|
submitButtonVariant?: SendouButtonProps["variant"];
|
|
fetcher?: FetcherWithComponents<any>;
|
|
}) {
|
|
const componentsFetcher = useFetcher();
|
|
const fetcher = _fetcher ?? componentsFetcher;
|
|
|
|
const isHydrated = useHydrated();
|
|
const { t } = useTranslation(["common"]);
|
|
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
const formRef = React.useRef<HTMLFormElement>(null);
|
|
const id = React.useId();
|
|
|
|
const openDialog = React.useCallback(() => setDialogOpen(true), []);
|
|
const closeDialog = React.useCallback(() => setDialogOpen(false), []);
|
|
|
|
invariant(React.isValidElement(children));
|
|
|
|
React.useEffect(() => {
|
|
if (fetcher.state === "loading") {
|
|
closeDialog();
|
|
}
|
|
}, [fetcher.state, closeDialog]);
|
|
|
|
return (
|
|
<>
|
|
{isHydrated
|
|
? // using portal here makes nesting this component in another form work
|
|
createPortal(
|
|
<fetcher.Form
|
|
id={id}
|
|
className="hidden"
|
|
ref={formRef}
|
|
method="post"
|
|
action={action}
|
|
>
|
|
{fields?.map(([name, value]) => (
|
|
<input type="hidden" key={name} name={name} value={value} />
|
|
))}
|
|
</fetcher.Form>,
|
|
document.body,
|
|
)
|
|
: null}
|
|
<SendouDialog
|
|
isOpen={dialogOpen}
|
|
onClose={closeDialog}
|
|
onOpenChange={closeDialog}
|
|
isDismissable
|
|
>
|
|
<div className="stack md">
|
|
<h2 className="text-md text-center">{dialogHeading}</h2>
|
|
<div className="stack horizontal md justify-center mt-2">
|
|
<SubmitButton
|
|
form={id}
|
|
variant={submitButtonVariant}
|
|
testId={dialogOpen ? "confirm-button" : submitButtonTestId}
|
|
>
|
|
{submitButtonText ?? t("common:actions.delete")}
|
|
</SubmitButton>
|
|
</div>
|
|
</div>
|
|
</SendouDialog>
|
|
{React.cloneElement(children, {
|
|
onPress: openDialog,
|
|
type: "button",
|
|
})}
|
|
</>
|
|
);
|
|
}
|