diff --git a/app/components/Modal.tsx b/app/components/Modal.tsx index c1d4aaef0..6ab08b2b9 100644 --- a/app/components/Modal.tsx +++ b/app/components/Modal.tsx @@ -1,5 +1,6 @@ import * as React from "react"; -import { Link } from "remix"; +import { Link, useNavigate } from "remix"; +import { useOnClickOutside } from "~/hooks/common"; export default function Modal({ closeUrl, @@ -10,9 +11,18 @@ export default function Modal({ title: React.ReactNode; children: React.ReactNode; }) { + const navigate = useNavigate(); + const ref = React.useRef(null); + + const useOnClickOutsideHandler = React.useCallback( + () => navigate(closeUrl), + [closeUrl] + ); + useOnClickOutside(ref, useOnClickOutsideHandler); + return (
-
+
Close diff --git a/app/hooks/common.ts b/app/hooks/common.ts index 993201c5d..ca16db7ef 100644 --- a/app/hooks/common.ts +++ b/app/hooks/common.ts @@ -76,3 +76,24 @@ export const useTimeoutState = ( ); return [state, setState]; }; + +/** @link https://usehooks.com/useOnClickOutside/ */ +export function useOnClickOutside( + ref: React.RefObject, + handler: (event: MouseEvent | TouchEvent) => void +) { + React.useEffect(() => { + const listener = (event: MouseEvent | TouchEvent) => { + if (!ref.current || ref.current.contains(event.target as Node)) { + return; + } + handler(event); + }; + document.addEventListener("mousedown", listener); + document.addEventListener("touchstart", listener); + return () => { + document.removeEventListener("mousedown", listener); + document.removeEventListener("touchstart", listener); + }; + }, [ref, handler]); +} diff --git a/app/utils/index.ts b/app/utils/index.ts index b870ecd63..a40b5087c 100644 --- a/app/utils/index.ts +++ b/app/utils/index.ts @@ -32,7 +32,7 @@ export function requireEvents(ctx: unknown) { try { const data = z.object({ events: z.unknown() }).parse(ctx); - // TODO: + // TODO: fix type assertion return data.events as EventTargetRecorder; } catch (e) { console.error(e); @@ -41,6 +41,7 @@ export function requireEvents(ctx: unknown) { } /** Asserts condition is truthy. Throws a new `Response` with status code 400 and given message if falsy. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- same format as TS docs: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions export function validate(condition: any, message: string): asserts condition { if (condition) return;