Conversation
- Changed to Map<nodeId, Map<registrationId, BindingRegistration>> - Merge all registrations for a node with later keys overriding earlier - Only activate bindings from capture-mode registrations when capture is enabled - This allows different handlers for the same key in different modes
- Use useId() to generate unique registration IDs - Register bindings in component body instead of useEffect - Only cleanup in useEffect to prevent re-registration issues - Enables multiple useKeybindings calls to compose properly
- Check capture mode BEFORE checking named bindings - Keys not in passthrough go to onKeypress immediately - Keys in passthrough continue to named bindings - Makes text input more intuitive - capture intercepts first
- Three text input fields with navigation and edit modes - Shows different bindings for same keys in different modes - enter: starts editing in nav mode, moves to next field in edit mode - Demonstrates clean multi-registration pattern
file-list.tsx: - Expanded from 3 to 7 files with metadata - Added search mode with live filtering - Added delete and open file operations - Shows multi-registration pattern with search capture mode focus-trap.tsx: - Enhanced with confirmation dialogs - Added action tracking and visual feedback - Added black background to modal for better visibility - Better demonstrates input isolation
input.mdx: - Document multi-registration behavior with unique IDs - Clarify that capture-mode bindings only activate when capture is enabled - Update capture mode priority explanation - Add new capture mode contact form example core-concepts.mdx: - Update input routing priority order (capture first) - Remove unnecessary internal implementation details
Pure TypeScript store with no React dependency. Implements the full focus tree: registerNode (with reverse-scan orphan adoption and pending focusFirstChild fulfillment), unregisterNode (with ancestor refocus via persistent parentMap), focusNode (with passive scope transitions), focusFirstChild (with deferred queue), navigateSibling (with shallow parameter), isFocused (ancestor walk), isPassive, makePassive, getActiveBranchPath, setTrap/clearTrap, subscribe/notify.
…ocusScope) Implements the React integration for the external focus store: - StoreContext: React context for the FocusStore instance, plus ScopeIdContext for implicit parent discovery and a useStore() accessor - useFocusScope: hook that registers a scope node in the store and returns a reactive handle (hasFocus, isPassive) via useSyncExternalStore - FocusScope: component that sets ScopeIdContext so child components can discover their parent scope without explicit prop threading - GigglesProvider: creates a FocusStore instance (once, via useRef) and provides it via StoreContext alongside the existing legacy providers The old FocusContext/FocusProvider remains in place; UI components will be migrated in a later chunk.
- FocusStore gains registerKeybindings, unregisterKeybindings, getNodeBindings, and getAllBindings. The two-level nodeId → registrationId map and the per-registration merge logic are ported from InputContext. - useKeybindings now calls store.registerKeybindings directly, synchronously during render, so handlers always reflect the latest closed-over values. - InputRouter and useKeybindingRegistry read node bindings, trap ID, and all bindings from the store. The active branch path still comes from FocusContext until the focus tree itself moves in the next chunk. - FocusTrap calls store.setTrap/clearTrap instead of InputContext. - InputProvider and InputContext are deleted; GigglesProvider no longer wraps children in InputProvider.
FocusStore gains store.dispatch(input, key) — the full dispatch algorithm: passive-scope skipping (the key new capability), capture mode, trap boundary, and mounted-binding fallback. normalizeKey is called inside the store. FocusContext is updated to dual-register every node in the store and to call store.focusNode whenever it moves focus. This keeps the store's active branch path and focusedId in sync with FocusContext's tree so that store.dispatch walks the correct nodes while old UI components still register through FocusContext. The bridge is removed in chunk #6. InputRouter is reduced to a single line: useInput((input, key) => store.dispatch(input, key)) The inline dispatch loop and its useFocusContext/useStore calls are gone.
useFocusScope now accepts a keybindings option (plain object or callback).
When a callback is provided it receives six navigation helpers:
next/prev — navigate siblings, auto-drill into first leaf
nextShallow/prevShallow — navigate siblings, land on scope node
escape — focus own scope node, mark passive
drillIn — focus first child of this scope, clears passive
All helpers are stable references (useCallback with stable deps). Bindings
register synchronously during render so closures are always fresh.
useKeybindings now accepts { id: string } instead of FocusHandle, making it
compatible with FocusScopeHandle and any future handle type.
FocusScopeHelpers is exported from the focus package.
useFocusNode is now store-based: registers in FocusStore via useEffect, reads hasFocus reactively via useSyncExternalStore. The old focused/focus() shape is gone. UI components (Select, TextInput, Autocomplete, MultiSelect, Confirm) updated to use focus.hasFocus. Viewport, Modal, and CommandPalette needed no changes. FocusTrap now uses the store directly for setTrap/clearTrap/focusFirstChild/ focusNode/getFocusedId. Children are scoped via ScopeIdContext.Provider. ScreenEntry (router) migrated from FocusContext to useFocusNode + store. useKeybindingRegistry now reads the active branch path from the store instead of FocusContext. GigglesProvider no longer wraps children in FocusProvider — the store is the only focus system. Deleted: FocusContext, FocusGroup, useFocus, FocusBindContext, types (FocusHandle), useFocusState. The dual-registration bridge from chunk #4 is gone. Public API: FocusGroup/useFocus/FocusHandle/FocusGroupHelpers removed. New exports: useFocusScope, FocusScope, useFocusNode (new), FocusScopeHandle, FocusScopeHelpers, FocusScopeOptions, FocusNodeHandle.
Both components now support controlled and uncontrolled modes. Omitting value/onChange lets the component manage selection state internally; passing them opts into the controlled pattern. MultiSelect uses internal state for toggle logic when uncontrolled.
… API docs Replace hand-rolled PanelItem, FileItem, and FileList components with Select in the focus examples. Passive mode and shallow navigation remain the teaching focus; Select handles internal list navigation. Update Select and MultiSelect API reference to document controlled vs uncontrolled modes and mark value/onChange as optional.
Add concurrently so both processes share a terminal and are torn down together on exit. Initial build still runs first to ensure dist/ exists before Next.js starts.
Spreads BoxProps (excluding height, overflow, flexDirection) onto the outer container, allowing callers to set borderStyle, borderColor, padding, width, etc. directly on Viewport.
…rops Replace artificial line array with paragraph text to demonstrate automatic wrap-height measurement. Add rounded border using theme borderColor to the live demo. Remove stale FocusGroup callout. Add footer and ...boxProps to API reference table.
… editing keys Add borderStyle to GigglesTheme (optional, defaults to 'round'). CodeBlock, Panel, and Modal now read border style from the theme instead of hardcoding it. Fix TextInput keybinding bug where capture mode intercepted backspace, delete, and cursor movement keys before named handlers could fire. Add these keys to the passthrough list so they reach their handlers correctly. Add placeholder to TextInputRenderProps so custom renders can implement cursor-on-placeholder behaviour. Update default render to show cursor at position 0 of placeholder text when focused and empty.
…ferences Add live demo showing a vim-style search box using the render prop, with cursor-on-placeholder behaviour and theme-aware border style. Fix FocusGroup reference in Form with Submit example (deleted API). Update placeholder description to reflect new focused cursor behaviour. Add placeholder to TextInputRenderProps table.
…ove unexported AlternateScreen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
FocusStoreclass, subscribed viauseSyncExternalStoreuseFocusScope/<FocusScope>/useFocusNodeas the new public API, replacingFocusGroup,useFocus, and the oldFocusHandletypeFocusStore, slimmingInputRouterto a thin bridgeescape()from keybindings yields input back to the parent scope without unmounting, enabling same-key navigation at multiple nesting levelsSelect,MultiSelect,Autocomplete,TextInput,Confirm) and the router'sScreenEntryto the new API; removes all legacy focus filesfocus.mdx,input.mdx,core-concepts.mdx) and all live demo components to reflect the new APITest plan
pnpm devand verify the playground renders correctly and keyboard navigation worksnpx tsc --noEmitpasses with zero errors