refactor(webui): share outside-dismiss behavior

This commit is contained in:
2026-07-21 13:35:09 +02:00
parent 7526c5ebb2
commit 2ac1e64daa
3 changed files with 38 additions and 47 deletions

View File

@@ -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 { useEffect, useRef, useState, type InputHTMLAttributes } from "react";
import useOutsideDismiss from "../hooks/useOutsideDismiss";
import { i18nMessage } from "../i18n/LanguageContext";
type ColorPickerFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> & { type ColorPickerFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> & {
value: string; value: string;
@@ -20,27 +22,6 @@ function normalizeColor(value: string): string {
return trimmed; return trimmed;
} }
function useOutsideClose(open: boolean, onClose: () => void) {
const ref = useRef<HTMLDivElement | null>(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({ export default function ColorPickerField({
value, value,
onChange, onChange,
@@ -51,7 +32,7 @@ export default function ColorPickerField({
...props ...props
}: ColorPickerFieldProps) { }: ColorPickerFieldProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const rootRef = useOutsideClose(open, () => setOpen(false)); const rootRef = useOutsideDismiss(open, () => setOpen(false));
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
const normalizedValue = normalizeColor(value || ""); const normalizedValue = normalizeColor(value || "");
const previewColor = HEX_PATTERN.test(normalizedValue) ? normalizedValue : "transparent"; const previewColor = HEX_PATTERN.test(normalizedValue) ? normalizedValue : "transparent";
@@ -102,4 +83,4 @@ export default function ColorPickerField({
} }
</div>); </div>);
} }

View File

@@ -1,5 +1,6 @@
import { CalendarDays, ChevronLeft, ChevronRight, Clock } from "lucide-react"; import { CalendarDays, ChevronLeft, ChevronRight, Clock } from "lucide-react";
import { useEffect, useMemo, useRef, useState, type InputHTMLAttributes } from "react"; import { useEffect, useMemo, useRef, useState, type InputHTMLAttributes } from "react";
import useOutsideDismiss from "../hooks/useOutsideDismiss";
type BaseProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange" | "min" | "max"> & { type BaseProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange" | "min" | "max"> & {
value: string; value: string;
@@ -48,32 +49,11 @@ function combineDateTime(date: string, time: string): string {
return `${date || dateString(new Date())}T${time || "00:00"}`; return `${date || dateString(new Date())}T${time || "00:00"}`;
} }
function useOutsideClose(open: boolean, onClose: () => void) {
const ref = useRef<HTMLDivElement | null>(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) { export function DateField({ value, onChange, min, max, disabled, className = "", placeholder = "i18n:govoplan-core.yyyy_mm_dd.d3f8f7b8", ...props }: BaseProps) {
const selectedDate = parseDate(value); const selectedDate = parseDate(value);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [visibleMonth, setVisibleMonth] = useState<Date>(() => selectedDate ?? new Date()); const [visibleMonth, setVisibleMonth] = useState<Date>(() => selectedDate ?? new Date());
const rootRef = useOutsideClose(open, () => setOpen(false)); const rootRef = useOutsideDismiss(open, () => setOpen(false));
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => { useEffect(() => {
@@ -228,4 +208,4 @@ export function DateTimeField({ value, onChange, min, max, disabled, className =
} }
export default DateField; export default DateField;

View File

@@ -0,0 +1,30 @@
import { useEffect, useRef } from "react";
export default function useOutsideDismiss<TElement extends HTMLElement = HTMLDivElement>(
active: boolean,
onDismiss: () => void,
) {
const rootRef = useRef<TElement | null>(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;
}