Everything is accessed through a single import:
import {
EditorFocusScope,
EditorFocusScopeProvider,
} from '@react-email/editor/ui';
Use EditorFocusScopeProvider to keep the editor’s focus and blur behavior in
sync across registered surfaces, then wrap each portaled surface that should
still count as being inside the editor UI with EditorFocusScope.
Inspector.Root already renders EditorFocusScopeProvider and wraps itself
in EditorFocusScope by default, so the Inspector handles the editor’s focus
idiomatically and reliably. Inside a custom inspector, you usually only add
EditorFocusScope around extra portaled content.
EditorFocusScopeProvider
Wrap your editor UI with EditorFocusScopeProvider when you need focus-aware
portals outside the built-in Inspector. The provider must be rendered inside
the current editor context so it can access the active editor instance.
Example
import {
EditorFocusScope,
EditorFocusScopeProvider,
} from '@react-email/editor/ui';
import { EditorContent, EditorContext, useEditor } from '@tiptap/react';
import * as Popover from '@radix-ui/react-popover';
export function MyEditor() {
const editor = useEditor({
extensions,
content,
});
return (
<EditorContext.Provider value={{ editor }}>
<EditorFocusScopeProvider clearSelectionOnBlur={false}>
<EditorContent editor={editor} />
<Popover.Root>
<Popover.Trigger type="button">Theme presets</Popover.Trigger>
<Popover.Portal>
<EditorFocusScope>
<Popover.Content sideOffset={8}>
<button type="button">Apply preset</button>
</Popover.Content>
</EditorFocusScope>
</Popover.Portal>
</Popover.Root>
</EditorFocusScopeProvider>
</EditorContext.Provider>
);
}
Use EditorFocusScope for each portaled surface that should still count as
being inside the editor UI.
Props
The editor UI subtree that should share focus tracking.
When focus leaves every registered scope, clear the current selection as part
of blur handling. Set this to false if you want to preserve the selection
when the editor truly loses focus.
EditorFocusScope
Use EditorFocusScope around portaled UI like Radix Select.Content,
Popover.Content, or dialog content so moving focus into that portal is still
treated as staying inside the editor UI.
EditorFocusScope must be used inside
EditorFocusScopeProvider,
which tracks registered focus scopes and updates the editor’s focus state for
them.
Example
import { EditorFocusScope, Inspector } from '@react-email/editor/ui';
import * as Select from '@radix-ui/react-select';
<Inspector.Node>
{({ getAttr, setAttr }) => (
<Select.Root
value={String(getAttr('alignment') ?? 'left')}
onValueChange={(value) => setAttr('alignment', value)}
>
<Select.Trigger>
<Select.Value />
</Select.Trigger>
<Select.Portal>
<EditorFocusScope>
<Select.Content>
<Select.Viewport>
<Select.Item value="left">
<Select.ItemText>Left</Select.ItemText>
</Select.Item>
<Select.Item value="center">
<Select.ItemText>Center</Select.ItemText>
</Select.Item>
<Select.Item value="right">
<Select.ItemText>Right</Select.ItemText>
</Select.Item>
</Select.Viewport>
</Select.Content>
</EditorFocusScope>
</Select.Portal>
</Select.Root>
)}
</Inspector.Node>
If you are building a focus-aware editor shell outside Inspector.Root, wrap
the editor with
EditorFocusScopeProvider
and then use EditorFocusScope for each portaled surface.
Props
The element or subtree to register as an additional editor focus scope. Wrap
the portaled container that receives focus.