mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 06:14:59 +00:00
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>
147 lines
3.9 KiB
TypeScript
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,
|
|
}
|
|
}
|