π File detail
ink/hooks/use-declared-cursor.ts
π§© .tsπ 74 linesπΎ 2,996 bytesπ text
β Back to All Filesπ― Use case
This file lives under βink/β, which covers Ink terminal UI (layouts, TTY IO, keyboard, renderer components). On the API surface it exposes useDeclaredCursor β mainly functions, hooks, or classes. Dependencies touch React UI. It composes internal code from components and dom (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { useCallback, useContext, useLayoutEffect, useRef } from 'react' import CursorDeclarationContext from '../components/CursorDeclarationContext.js' import type { DOMElement } from '../dom.js' /**
π€ Exports (heuristic)
useDeclaredCursor
π External import roots
Package roots from from "β¦" (relative paths omitted).
react
π₯οΈ Source preview
import { useCallback, useContext, useLayoutEffect, useRef } from 'react'
import CursorDeclarationContext from '../components/CursorDeclarationContext.js'
import type { DOMElement } from '../dom.js'
/**
* Declares where the terminal cursor should be parked after each frame.
*
* Terminal emulators render IME preedit text at the physical cursor
* position, and screen readers / screen magnifiers track the native
* cursor β so parking it at the text input's caret makes CJK input
* appear inline and lets accessibility tools follow the input.
*
* Returns a ref callback to attach to the Box that contains the input.
* The declared (line, column) is interpreted relative to that Box's
* nodeCache rect (populated by renderNodeToOutput).
*
* Timing: Both ref attach and useLayoutEffect fire in React's layout
* phase β after resetAfterCommit calls scheduleRender. scheduleRender
* defers onRender via queueMicrotask, so onRender runs AFTER layout
* effects commit and reads the fresh declaration on the first frame
* (no one-keystroke lag). Test env uses onImmediateRender (synchronous,
* no microtask), so tests compensate by calling ink.onRender()
* explicitly after render.
*/
export function useDeclaredCursor({
line,
column,
active,
}: {
line: number
column: number
active: boolean
}): (element: DOMElement | null) => void {
const setCursorDeclaration = useContext(CursorDeclarationContext)
const nodeRef = useRef<DOMElement | null>(null)
const setNode = useCallback((node: DOMElement | null) => {
nodeRef.current = node
}, [])
// When active, set unconditionally. When inactive, clear conditionally
// (only if the currently-declared node is ours). The node-identity check
// handles two hazards:
// 1. A memo()ized active instance elsewhere (e.g. the search input in
// a memo'd Footer) doesn't re-render this commit β an inactive
// instance re-rendering here must not clobber it.
// 2. Sibling handoff (menu focus moving between list items) β when
// focus moves opposite to sibling order, the newly-inactive item's
// effect runs AFTER the newly-active item's set. Without the node
// check it would clobber.
// No dep array: must re-declare every commit so the active instance
// re-claims the declaration after another instance's unmount-cleanup or
// sibling handoff nulls it.
useLayoutEffect(() => {
const node = nodeRef.current
if (active && node) {
setCursorDeclaration({ relativeX: column, relativeY: line, node })
} else {
setCursorDeclaration(null, node)
}
})
// Clear on unmount (conditionally β another instance may own by then).
// Separate effect with empty deps so cleanup only fires once β not on
// every line/column change, which would transiently null between commits.
useLayoutEffect(() => {
return () => {
setCursorDeclaration(null, nodeRef.current)
}
}, [setCursorDeclaration])
return setNode
}