From 2ac1e64daa71d2340f13147c838b2dc63caefb74 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 21 Jul 2026 13:35:09 +0200 Subject: [PATCH] refactor(webui): share outside-dismiss behavior --- webui/src/components/ColorPickerField.tsx | 29 ++++------------------ webui/src/components/DateTimeField.tsx | 26 +++----------------- webui/src/hooks/useOutsideDismiss.ts | 30 +++++++++++++++++++++++ 3 files changed, 38 insertions(+), 47 deletions(-) create mode 100644 webui/src/hooks/useOutsideDismiss.ts diff --git a/webui/src/components/ColorPickerField.tsx b/webui/src/components/ColorPickerField.tsx index 793a38f..780092d 100644 --- a/webui/src/components/ColorPickerField.tsx +++ b/webui/src/components/ColorPickerField.tsx @@ -1,5 +1,7 @@ -import { i18nMessage } from "../i18n/LanguageContext";import { Palette } from "lucide-react"; +import { Palette } from "lucide-react"; import { useEffect, useRef, useState, type InputHTMLAttributes } from "react"; +import useOutsideDismiss from "../hooks/useOutsideDismiss"; +import { i18nMessage } from "../i18n/LanguageContext"; type ColorPickerFieldProps = Omit, "type" | "value" | "onChange"> & { value: string; @@ -20,27 +22,6 @@ function normalizeColor(value: string): string { return trimmed; } -function useOutsideClose(open: boolean, onClose: () => void) { - const ref = useRef(null); - useEffect(() => { - if (!open) return; - function handlePointerDown(event: PointerEvent) { - if (!ref.current || ref.current.contains(event.target as Node)) return; - onClose(); - } - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") onClose(); - } - document.addEventListener("pointerdown", handlePointerDown); - document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("pointerdown", handlePointerDown); - document.removeEventListener("keydown", handleKeyDown); - }; - }, [open, onClose]); - return ref; -} - export default function ColorPickerField({ value, onChange, @@ -51,7 +32,7 @@ export default function ColorPickerField({ ...props }: ColorPickerFieldProps) { const [open, setOpen] = useState(false); - const rootRef = useOutsideClose(open, () => setOpen(false)); + const rootRef = useOutsideDismiss(open, () => setOpen(false)); const inputRef = useRef(null); const normalizedValue = normalizeColor(value || ""); const previewColor = HEX_PATTERN.test(normalizedValue) ? normalizedValue : "transparent"; @@ -102,4 +83,4 @@ export default function ColorPickerField({ } ); -} \ No newline at end of file +} diff --git a/webui/src/components/DateTimeField.tsx b/webui/src/components/DateTimeField.tsx index 953e992..835c8e2 100644 --- a/webui/src/components/DateTimeField.tsx +++ b/webui/src/components/DateTimeField.tsx @@ -1,5 +1,6 @@ import { CalendarDays, ChevronLeft, ChevronRight, Clock } from "lucide-react"; import { useEffect, useMemo, useRef, useState, type InputHTMLAttributes } from "react"; +import useOutsideDismiss from "../hooks/useOutsideDismiss"; type BaseProps = Omit, "type" | "value" | "onChange" | "min" | "max"> & { value: string; @@ -48,32 +49,11 @@ function combineDateTime(date: string, time: string): string { return `${date || dateString(new Date())}T${time || "00:00"}`; } -function useOutsideClose(open: boolean, onClose: () => void) { - const ref = useRef(null); - useEffect(() => { - if (!open) return; - function handlePointerDown(event: PointerEvent) { - if (!ref.current || ref.current.contains(event.target as Node)) return; - onClose(); - } - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") onClose(); - } - document.addEventListener("pointerdown", handlePointerDown); - document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("pointerdown", handlePointerDown); - document.removeEventListener("keydown", handleKeyDown); - }; - }, [open, onClose]); - return ref; -} - export function DateField({ value, onChange, min, max, disabled, className = "", placeholder = "i18n:govoplan-core.yyyy_mm_dd.d3f8f7b8", ...props }: BaseProps) { const selectedDate = parseDate(value); const [open, setOpen] = useState(false); const [visibleMonth, setVisibleMonth] = useState(() => selectedDate ?? new Date()); - const rootRef = useOutsideClose(open, () => setOpen(false)); + const rootRef = useOutsideDismiss(open, () => setOpen(false)); const inputRef = useRef(null); useEffect(() => { @@ -228,4 +208,4 @@ export function DateTimeField({ value, onChange, min, max, disabled, className = } -export default DateField; \ No newline at end of file +export default DateField; diff --git a/webui/src/hooks/useOutsideDismiss.ts b/webui/src/hooks/useOutsideDismiss.ts new file mode 100644 index 0000000..c19ac2a --- /dev/null +++ b/webui/src/hooks/useOutsideDismiss.ts @@ -0,0 +1,30 @@ +import { useEffect, useRef } from "react"; + +export default function useOutsideDismiss( + active: boolean, + onDismiss: () => void, +) { + const rootRef = useRef(null); + + useEffect(() => { + if (!active) return; + + function handlePointerDown(event: PointerEvent) { + if (!rootRef.current || rootRef.current.contains(event.target as Node)) return; + onDismiss(); + } + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") onDismiss(); + } + + document.addEventListener("pointerdown", handlePointerDown); + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("pointerdown", handlePointerDown); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [active, onDismiss]); + + return rootRef; +}