mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +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>
13 KiB
13 KiB
Keyboard & Event Hooks
Comprehensive keyboard shortcut and event listener hooks for MetaBuilder applications.
Quick Reference
| Hook | Purpose | Returns |
|---|---|---|
| useKeyboardShortcuts | Unified keyboard shortcut handling with meta keys | { registerShortcut, unregister, clearAll } |
| useClickOutside | Detect clicks outside element | { ref, isOpen, setIsOpen, toggle } |
| useHotkeys | Global hotkey registration with combo support | { register, unregister, unregisterAll } |
| useEventListener | Generic event listener with cleanup | { add, remove, removeAll } |
useKeyboardShortcuts
Unified keyboard shortcut handling with automatic platform detection (macOS vs Windows/Linux).
Features
- Register shortcuts with meta keys (cmd/ctrl, shift, alt)
- Platform detection (Mac vs Windows)
- Prevent default browser shortcuts
- Debounce support
- Clean unregistration
Basic Usage
import { useKeyboardShortcuts } from '@metabuilder/hooks'
function SaveDialog() {
const { registerShortcut, unregister } = useKeyboardShortcuts()
useEffect(() => {
// Register Ctrl+S (or Cmd+S on Mac)
const id = registerShortcut({
key: 's',
ctrl: true,
onPress: () => handleSave(),
preventDefault: true
})
return () => unregister(id)
}, [])
return <div>Save with Ctrl+S</div>
}
Meta Key Handling
const { registerShortcut } = useKeyboardShortcuts()
// Automatic cross-platform:
// - On Mac: uses Cmd key
// - On Windows: uses Ctrl key
registerShortcut({
key: 's',
ctrl: true, // Triggers Cmd on Mac, Ctrl on Windows
onPress: save
})
// macOS-specific
registerShortcut({
key: 's',
cmd: true, // Only Cmd on Mac
onPress: save
})
Complex Shortcuts
const { registerShortcut } = useKeyboardShortcuts()
// Ctrl+Shift+Enter
registerShortcut({
key: 'Enter',
ctrl: true,
shift: true,
onPress: submitForm
})
// Ctrl+Alt+K (without preventDefault)
registerShortcut({
key: 'k',
ctrl: true,
alt: true,
onPress: openCommandPalette
})
// Escape key
registerShortcut({
key: 'Escape',
onPress: closeDialog
})
With Debounce
const { registerShortcut } = useKeyboardShortcuts()
// Prevent rapid successive triggers
registerShortcut({
key: 's',
ctrl: true,
onPress: autoSave,
debounce: 500 // Wait 500ms before processing
})
API
interface KeyboardShortcut {
key: string // Key name or character
ctrl?: boolean // Ctrl key (Windows/Linux)
cmd?: boolean // Cmd key (macOS)
shift?: boolean // Shift key
alt?: boolean // Alt key
onPress: () => void // Callback
preventDefault?: boolean // Prevent default action
debounce?: number // Debounce in ms
}
// Return types
registerShortcut(shortcut): string // Returns ID
unregister(id: string): void // Remove by ID
clearAll(): void // Remove all
useClickOutside
Detect clicks outside a referenced element for modals, dropdowns, menus.
Features
- Element reference tracking
- Open state management
- Support for excluded refs
- Touch event support
- Delay option
Basic Usage
import { useClickOutside } from '@metabuilder/hooks'
function Dropdown() {
const { ref, isOpen, setIsOpen } = useClickOutside()
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Menu</button>
{isOpen && (
<div ref={ref}>
<div>Item 1</div>
<div>Item 2</div>
</div>
)}
</div>
)
}
With Callback
const { ref, isOpen, setIsOpen } = useClickOutside({
onClickOutside: () => {
console.log('Closed by outside click')
}
})
Exclude Refs
const triggerRef = useRef<HTMLButtonElement>(null)
const dropdownRef = useClickOutside({
excludeRefs: [triggerRef] // Don't close when clicking trigger
})
return (
<div>
<button ref={triggerRef} onClick={() => setOpen(true)}>
Open
</button>
{isOpen && (
<div ref={dropdownRef.ref}>Menu</div>
)}
</div>
)
With Delay
const { ref, isOpen, setIsOpen } = useClickOutside({
delayMs: 200 // Wait 200ms before closing
})
API
interface UseClickOutsideOptions {
onClickOutside?: () => void // Callback on outside click
excludeRefs?: React.RefObject<HTMLElement>[] // Refs to exclude
includeTouch?: boolean // Include touch events (default: true)
delayMs?: number // Delay before closing (ms)
}
// Returns
{
ref: React.RefObject<T> // Attach to element
isOpen: boolean // Is currently open
setIsOpen: (open: boolean) => void // Set state
toggle: () => void // Toggle state
}
useHotkeys
Global hotkey registration with combo key support (ctrl+shift+k style).
Features
- Combo key parsing (ctrl+shift+enter)
- Platform-aware (cmd on Mac, ctrl on Windows)
- Handler registration
- Debounce support
- Enable/disable per hotkey
Basic Usage
import { useHotkeys } from '@metabuilder/hooks'
function SearchBox() {
const hotkeys = useHotkeys()
useEffect(() => {
// Command palette: Ctrl+K
hotkeys.register('ctrl+k', () => {
openCommandPalette()
})
// Escape to close
hotkeys.register('Escape', () => {
closeCommandPalette()
})
}, [hotkeys])
return <SearchBox />
}
Combo Key Formats
const hotkeys = useHotkeys()
// Supported format: modifiers+key
hotkeys.register('ctrl+s', save) // Ctrl+S
hotkeys.register('ctrl+shift+k', search) // Ctrl+Shift+K
hotkeys.register('cmd+opt+i', inspector) // Cmd+Opt+I (Mac: opt=alt)
hotkeys.register('shift+enter', submitForm) // Shift+Enter
hotkeys.register('alt+1', switchTab1) // Alt+1
// Single key
hotkeys.register('Escape', closeMenu)
With Options
const hotkeys = useHotkeys()
const id = hotkeys.register('ctrl+shift+k', openPalette, {
preventDefault: true, // Prevent browser defaults
enabled: true, // Enable/disable this hotkey
debounceMs: 100 // Debounce rapid presses
})
// Later unregister
hotkeys.unregister(id)
Platform-Specific
const hotkeys = useHotkeys()
// Automatically uses Cmd on Mac, Ctrl on Windows
hotkeys.register('ctrl+s', save)
// You can also manually handle it
const saveKey = navigator.platform.includes('Mac') ? 'cmd+s' : 'ctrl+s'
hotkeys.register(saveKey, save)
API
interface HotkeysOptions {
preventDefault?: boolean // Prevent default action
enabled?: boolean // Enable/disable (default: true)
debounceMs?: number // Debounce delay
}
// Returns
{
register(combo, handler, options?): string // Returns ID
unregister(id: string): void // Remove by ID
unregisterAll(): void // Remove all
}
useEventListener
Generic event listener with proper cleanup for any event type.
Features
- Generic typing for any event
- Passive listener support (scroll/touch performance)
- Works with window, document, element
- Automatic cleanup
- Capture phase support
- Type-safe handlers
Basic Usage
import { useEventListener } from '@metabuilder/hooks'
function ResizeHandler() {
const { add } = useEventListener()
useEffect(() => {
const remove = add(window, 'resize', (e: UIEvent) => {
console.log('Window resized')
})
return remove
}, [add])
return <div>Listen for resizes</div>
}
Element Events
const { add } = useEventListener()
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (!inputRef.current) return
const remove = add(
inputRef.current,
'input',
(e: Event) => {
const target = e.target as HTMLInputElement
console.log('Value:', target.value)
}
)
return remove
}, [add])
Passive Listeners
const { add } = useEventListener()
// Passive listeners for better scroll performance
add(window, 'scroll', (e: Event) => {
console.log('Scrolling')
}, {
passive: true // Won't call preventDefault
})
add(window, 'touchmove', (e: TouchEvent) => {
console.log('Touch moving')
}, {
passive: true // Better mobile performance
})
Capture Phase
const { add } = useEventListener()
// Capture phase (runs before bubble phase)
add(document, 'click', (e: MouseEvent) => {
console.log('Clicked in capture phase')
}, {
capture: true
})
Document Events
const { add } = useEventListener()
add(document, 'click', (e: MouseEvent) => {
console.log('Clicked at', e.clientX, e.clientY)
})
add(document, 'keydown', (e: KeyboardEvent) => {
console.log('Key:', e.key)
})
Multiple Listeners
const { add, removeAll } = useEventListener()
useEffect(() => {
// Add multiple listeners
add(window, 'resize', handleResize)
add(window, 'scroll', handleScroll)
add(document, 'click', handleClick)
// Clean up all at once
return removeAll
}, [add, removeAll])
API
interface EventListenerOptions extends AddEventListenerOptions {
passive?: boolean // Better performance (default: false)
capture?: boolean // Use capture phase (default: false)
once?: boolean // Auto-remove after first trigger
}
// Returns
{
add(target, event, handler, options?): () => void // Returns cleanup fn
remove(target, event, handler, options?): void // Manual removal
removeAll(): void // Remove all listeners
}
// Types
type EventHandler<T extends Event = Event> = (event: T) => void
Best Practices
1. Always Clean Up
// ✅ Good - cleanup on unmount
useEffect(() => {
const id = registerShortcut(...)
return () => unregister(id)
}, [])
// ❌ Bad - leaves listeners
const { registerShortcut } = useKeyboardShortcuts()
registerShortcut(...) // Never cleaned up
2. Use TypeScript
// ✅ Good - typed events
const { add } = useEventListener()
add(window, 'resize', (e: UIEvent) => {
// e is properly typed
}, { passive: true })
// ❌ Avoid - any types
add(window, 'resize', (e: any) => {})
3. Respect Passive Listeners
// ✅ Good - passive for scroll
add(window, 'scroll', handleScroll, { passive: true })
// ⚠️ Can't preventDefault with passive
add(window, 'touchstart', (e: TouchEvent) => {
// e.preventDefault() won't work here
}, { passive: true })
4. Platform Detection
// ✅ Good - useKeyboardShortcuts handles it
const { registerShortcut } = useKeyboardShortcuts()
registerShortcut({
key: 's',
ctrl: true, // Automatic: Cmd on Mac, Ctrl elsewhere
onPress: save
})
// ✅ Also good - useHotkeys combo parsing
const hotkeys = useHotkeys()
hotkeys.register('ctrl+s', save) // Auto cross-platform
5. Avoid Memory Leaks
// ✅ Good
useEffect(() => {
const { ref, isOpen, setIsOpen } = useClickOutside()
return () => {
// useClickOutside cleans up automatically
}
}, [])
// ✅ Also good
const { add, removeAll } = useEventListener()
useEffect(() => {
add(window, 'resize', handleResize)
return removeAll // Clean everything
}, [])
Performance Tips
- Use passive listeners for scroll/touch - prevents jank
- Debounce rapid keyboard shortcuts - avoid processing storms
- Use capture phase rarely - default bubble is usually better
- Remove unused hotkeys - don't let them pile up
- Group related listeners - use one useEventListener for related events
Migration Guide
From Native Event Listeners
// Before
useEffect(() => {
const handler = () => console.log('Resized')
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [])
// After
const { add } = useEventListener()
useEffect(() => {
return add(window, 'resize', () => console.log('Resized'))
}, [add])
From Custom Shortcut Handlers
// Before
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault()
save()
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [])
// After
const { registerShortcut } = useKeyboardShortcuts()
useEffect(() => {
const id = registerShortcut({
key: 's',
ctrl: true,
onPress: save,
preventDefault: true
})
return () => unregister(id)
}, [])
Troubleshooting
Shortcuts not triggering
- Check key name matches
event.keyexactly - Verify platform (Mac uses cmd, not ctrl)
- Ensure preventDefault isn't conflicting
Click outside not closing
- Add element ref to the target element
- Verify trigger button isn't in excludeRefs unintentionally
- Check for nested refs with event propagation
Memory leaks
- Always return cleanup function from useEffect
- Call removeAll() or unregister() for cleanup
- Don't create multiple hook instances unnecessarily
Event handler not firing
- Verify event name is correct (e.g., 'input' vs 'change')
- Check passive listeners can't preventDefault
- Ensure target element exists in DOM