@blocknote/react
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
79 lines (66 loc) • 2.39 kB
text/typescript
// Copied from https://github.com/mantinedev/mantine/blob/90900efc7f107933ba027007cf240fea61d9c9f2/packages/%40mantine/hooks/src/use-focus-within/use-focus-within.ts#L16
import { useEffect, useRef, useState } from "react";
export interface UseFocusWithinOptions {
onFocus?: (event: FocusEvent) => void;
onBlur?: (event: FocusEvent) => void;
}
function containsRelatedTarget(event: FocusEvent) {
if (
event.currentTarget instanceof HTMLElement &&
event.relatedTarget instanceof HTMLElement
) {
return event.currentTarget.contains(event.relatedTarget);
}
return false;
}
/**
* Tracks whether focus is within a referenced DOM element. Returns a ref to
* attach to the target element and a `focused` boolean that is `true` whenever
* the element or any of its descendants has focus.
*
* Adapted from Mantine's `useFocusWithin` hook.
*
* @param options - Optional `onFocus` and `onBlur` callbacks fired when focus
* enters or leaves the element.
* @returns An object with a `ref` to attach to the target element and a
* `focused` boolean indicating current focus-within state.
*/
export function useFocusWithin<T extends HTMLElement = any>({
onBlur,
onFocus,
}: UseFocusWithinOptions = {}): { ref: React.RefObject<T>; focused: boolean } {
const ref = useRef<T>(null);
const [focused, setFocused] = useState(false);
const focusedRef = useRef(false);
const _setFocused = (value: boolean) => {
setFocused(value);
focusedRef.current = value;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleFocusIn = (event: FocusEvent) => {
if (!focusedRef.current) {
_setFocused(true);
onFocus?.(event);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleFocusOut = (event: FocusEvent) => {
if (focusedRef.current && !containsRelatedTarget(event)) {
_setFocused(false);
onBlur?.(event);
}
};
useEffect(() => {
const node = ref.current;
if (node) {
node.addEventListener("focusin", handleFocusIn);
node.addEventListener("focusout", handleFocusOut);
return () => {
node?.removeEventListener("focusin", handleFocusIn);
node?.removeEventListener("focusout", handleFocusOut);
};
}
return undefined;
}, [handleFocusIn, handleFocusOut]);
return { ref: ref as React.RefObject<T>, focused };
}