89 lines
2.6 KiB
TypeScript
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;
|