39948-vm/frontend/src/hooks/useOutsideClick.ts
2026-03-29 16:03:25 +04:00

89 lines
2.6 KiB
TypeScript

/**
* useOutsideClick Hook
*
* Detects clicks outside specified elements to clear selection.
* Used in constructor.tsx to deselect elements when clicking outside.
*/
import { useEffect, useCallback, RefObject } from 'react';
interface UseOutsideClickOptions {
/** Ref to the element whose outside clicks should be detected */
containerRef: RefObject<HTMLElement | null>;
/** Additional refs to ignore (clicking these won't trigger onOutsideClick) */
ignoreRefs?: RefObject<HTMLElement | null>[];
/** Data attribute to check on clicked elements (if present, won't trigger) */
ignoreDataAttribute?: string;
/** Current selected value to check (e.g., element ID) */
selectedValue?: string;
/** Callback when click outside is detected */
onOutsideClick: () => void;
/** Whether the hook is active */
enabled?: boolean;
}
/**
* Hook to detect clicks outside a container element.
* Useful for closing panels, deselecting elements, etc.
*
* @example
* useOutsideClick({
* containerRef: panelRef,
* ignoreRefs: [buttonRef],
* onOutsideClick: () => setSelectedId(''),
* enabled: !!selectedId,
* });
*/
export function useOutsideClick({
containerRef,
ignoreRefs = [],
ignoreDataAttribute,
selectedValue,
onOutsideClick,
enabled = true,
}: UseOutsideClickOptions): void {
const handleMouseDown = useCallback(
(event: MouseEvent) => {
const target = event.target as HTMLElement | null;
if (!target) return;
// Check if click is inside the container
if (containerRef.current?.contains(target)) return;
// Check if click is inside any ignored refs
for (const ref of ignoreRefs) {
if (ref.current?.contains(target)) return;
}
// Check for data attribute on clicked element or ancestors
if (ignoreDataAttribute) {
const clickedElement = target.closest(`[${ignoreDataAttribute}]`);
if (clickedElement) {
const attributeValue =
clickedElement.getAttribute(ignoreDataAttribute);
// If selected value matches clicked element's attribute, don't trigger
if (selectedValue && attributeValue === selectedValue) return;
}
}
onOutsideClick();
},
[
containerRef,
ignoreRefs,
ignoreDataAttribute,
selectedValue,
onOutsideClick,
],
);
useEffect(() => {
if (!enabled) return;
window.addEventListener('mousedown', handleMouseDown);
return () => window.removeEventListener('mousedown', handleMouseDown);
}, [enabled, handleMouseDown]);
}
export default useOutsideClick;