Files
metabuilder/hooks/useClickOutside.ts
johndoe6345789 5aabff44cd refactor(fakemui): flatten QML components directory structure and update documentation
Directory Restructuring:
- qml/qml-components/qml-components/* → qml/components/ (flattens nesting)
- All 104 QML files moved with git history preserved
- Eliminates redundant qml-components nesting

Documentation Updates:
- ARCHITECTURE.md: Updated qml/components references (2 locations)
- GETTING_STARTED.md: Updated qml/components path (1 location, end of file)
- README.md: Updated qml/components references (3 locations)
- CODE_REVIEW.md: Updated qml/components file paths (4 locations)
- docs/ARCHITECTURE.md: Complete refactor with qml/components paths

Verification:
-  No remaining qml-components/ references in documentation
-  All 104 QML files present in flattened structure
-  Directory structure verified (12 component categories)
-  First-class directory naming convention

Structure Post-Refactor:
qml/
├── components/
│   ├── atoms/ (16 files)
│   ├── core/ (11 files)
│   ├── data-display/ (10 files)
│   ├── feedback/ (11 files)
│   ├── form/ (19 files)
│   ├── lab/ (11 files)
│   ├── layout/ (12 files)
│   ├── navigation/ (12 files)
│   ├── surfaces/ (7 files)
│   ├── theming/ (4 files)
│   └── utils/ (13 files)
├── hybrid/
└── widgets/

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-23 19:54:21 +00:00

147 lines
3.9 KiB
TypeScript

/**
* useClickOutside Hook
* Detect clicks outside a referenced element and manage open state
*
* Features:
* - Track element reference with useRef
* - Detect clicks outside element
* - Support for multiple target nodes
* - Optional callback on outside click
* - Automatic cleanup
* - Support for both mouse and touch events
* - Respects disabled state
*
* @example
* // Basic usage with dialog
* const { ref, isOpen, setIsOpen } = useClickOutside()
*
* return (
* <div>
* <Button onClick={() => setIsOpen(true)}>Open Menu</Button>
* {isOpen && (
* <Menu ref={ref}>
* <MenuItem>Item 1</MenuItem>
* <MenuItem>Item 2</MenuItem>
* </Menu>
* )}
* </div>
* )
*
* @example
* // With callback
* const { ref, isOpen, setIsOpen } = useClickOutside({
* onClickOutside: () => console.log('Closed from outside click'),
* excludeRefs: [triggerButtonRef]
* })
*
* @example
* // Close dropdown when clicking outside
* const { ref, isOpen, setIsOpen } = useClickOutside()
*
* useEffect(() => {
* if (!isOpen) return
* const handleClick = () => setIsOpen(false)
* // Dropdown closes when clicking outside
* }, [isOpen])
*/
import { useRef, useState, useCallback, useEffect } from 'react'
export interface UseClickOutsideOptions {
/** Callback when click occurs outside element */
onClickOutside?: () => void
/** Additional refs to exclude from outside click detection */
excludeRefs?: React.RefObject<HTMLElement>[]
/** Include touch events in addition to mouse events */
includeTouch?: boolean
/** Delay before detecting outside click (ms) */
delayMs?: number
}
export interface UseClickOutsideReturn<T extends HTMLElement = HTMLDivElement> {
/** Ref to attach to the target element */
ref: React.RefObject<T>
/** Whether the element is currently open/visible */
isOpen: boolean
/** Set the open state */
setIsOpen: (open: boolean) => void
/** Toggle the open state */
toggle: () => void
}
export function useClickOutside<T extends HTMLElement = HTMLDivElement>(
options: UseClickOutsideOptions = {}
): UseClickOutsideReturn<T> {
const {
onClickOutside,
excludeRefs = [],
includeTouch = true,
delayMs = 0,
} = options
const ref = useRef<T>(null)
const [isOpen, setIsOpen] = useState(false)
const delayTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
const toggle = useCallback(() => {
setIsOpen((prev) => !prev)
}, [])
useEffect(() => {
if (!isOpen) return
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
// Check if click is outside the main ref
if (ref.current?.contains(event.target as Node)) {
return
}
// Check if click is within any excluded refs
const isWithinExcluded = excludeRefs.some((excludeRef) =>
excludeRef.current?.contains(event.target as Node)
)
if (isWithinExcluded) {
return
}
// Handle delay
if (delayMs > 0) {
if (delayTimeoutRef.current) {
clearTimeout(delayTimeoutRef.current)
}
delayTimeoutRef.current = setTimeout(() => {
setIsOpen(false)
onClickOutside?.()
}, delayMs)
} else {
setIsOpen(false)
onClickOutside?.()
}
}
// Add event listeners
document.addEventListener('mousedown', handleClickOutside)
if (includeTouch) {
document.addEventListener('touchstart', handleClickOutside)
}
// Cleanup
return () => {
document.removeEventListener('mousedown', handleClickOutside)
if (includeTouch) {
document.removeEventListener('touchstart', handleClickOutside)
}
if (delayTimeoutRef.current) {
clearTimeout(delayTimeoutRef.current)
}
}
}, [isOpen, excludeRefs, delayMs, includeTouch, onClickOutside])
return {
ref,
isOpen,
setIsOpen,
toggle,
}
}