refactor(webui): share outside-dismiss behavior
This commit is contained in:
@@ -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>);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
30
webui/src/hooks/useOutsideDismiss.ts
Normal file
30
webui/src/hooks/useOutsideDismiss.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user