@blocknote/react
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
43 lines (39 loc) • 1.78 kB
text/typescript
import { RefObject, useEffect, useRef } from "react";
/**
* BlockNote's autofocus for form inputs: focuses the element when
* `autoFocus` is true, without scrolling.
*
* Why not the native `autofocus` attribute (or React's `autoFocus` prop,
* which is a bare `.focus()` at commit): these inputs live in popovers that
* floating-ui positions *after* mount, so the browser's scroll-into-view
* would run while the popover is still at its pre-positioned spot and yank
* the page (on mobile, right out from under the block being edited).
*
* The shape matches the official popover-autofocus implementations
* (Mantine: setTimeout + preventScroll; floating-ui: microtask + rAF +
* preventScroll), minus their deferral layers: there is nothing here to
* wait for, and added hops erode the user-gesture window in which iOS
* Safari lets a programmatic focus open the keyboard (validated on real
* iOS). `preventScroll` also makes the timing not load-bearing: no
* ordering relative to floating-ui's positioning can scroll the page.
*
* This runs at mount, so the popover content must mount when the popover
* opens: Mantine and Base UI do that by default, Ariakit needs
* `unmountOnHide` (see the ariakit Popover). The skins keep their
* libraries' own autofocus off; Ariakit's is a bare `focus()` plus a Safari
* `scrollIntoView`, exactly the scroll this hook avoids.
*
* Returns the ref to attach; merge it with a forwarded ref via
* `useMergeRefs`.
*/
export function useAutoFocus<T extends HTMLElement>(
autoFocus: boolean | undefined,
): RefObject<T | null> {
const elementRef = useRef<T | null>(null);
useEffect(() => {
if (autoFocus) {
elementRef.current?.focus({ preventScroll: true });
}
}, [autoFocus]);
return elementRef;
}