The fastest way to add the tabbable plugin is with TabbableKit. The kit keeps Indent installed and removes only its Tab and Shift+Tab shortcuts so focus navigation owns those keys.
'use client';
import type { TabbablePluginState } from '@platejs/tabbable';
import { TabbablePlugin } from '@platejs/tabbable/react';
import { PLUGINS, ElementApi } from 'platejs';
export type TabbableKitPluginState = Pick<TabbablePluginState, 'query'>;
export const TabbableKit = [
TabbablePlugin.extend({
override: {
plugins: {
[PLUGINS.indent]: {
shortcuts: {
tab: null,
untab: null,
},
},
},
},
}).extend(({ editor }): { initialState: TabbableKitPluginState } => ({
initialState: {
query: () => {
if (
editor.read.selection.isAtBlockStart() ||
editor.read.selection.isAtBlockEnd()
) {
return false;
}
const blockingTypes = new Set(
[
PLUGINS.codeBlock,
PLUGINS.listItem,
PLUGINS.todoList,
PLUGINS.table,
].flatMap((name) => {
const plugin = editor.plugin(name);
return plugin.installed ? [plugin.schema.type] : [];
})
);
return !editor.read.nodes.some({
match: (n) =>
!!(
(ElementApi.isElement(n) && blockingTypes.has(n.type)) ||
(ElementApi.isElement(n) && n.listType)
),
});
},
},
})),
];'use client';
import type { TabbablePluginState } from '@platejs/tabbable';
import { TabbablePlugin } from '@platejs/tabbable/react';
import { PLUGINS, ElementApi } from 'platejs';
export type TabbableKitPluginState = Pick<TabbablePluginState, 'query'>;
export const TabbableKit = [
TabbablePlugin.extend({
override: {
plugins: {
[PLUGINS.indent]: {
shortcuts: {
tab: null,
untab: null,
},
},
import { createPlateEditor } from 'platejs/react';
import { TabbableKit } from '@/components/editor/tabbable';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
...TabbableKit,
],
});import { createPlateEditor } from 'platejs/react';
import { TabbableKit } from '@/components/editor/tabbable';
const editor = createPlateEditor
import { TabbablePlugin } from '@platejs/tabbable/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
TabbablePlugin,
],
});import { TabbablePlugin } from '@platejs/tabbable/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
TabbablePlugin,
],
});import { TabbablePlugin } from '@platejs/tabbable/react';
import { PLUGINS } from 'platejs';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
TabbablePlugin.configure(({ editor }) => {
const listItem = editor.plugin(PLUGINS.listItem);
const codeBlock = editor.plugin(PLUGINS.codeBlock);
const blockedTypes = [listItem, codeBlock].flatMap((plugin) =>
plugin.installed
initialState.query: Function to dynamically enable/disable the plugin based on editor state.initialState.globalEventListener: When true, adds event listener to document instead of editor.initialState.isTabbable: Function to determine which elements should be included in tab order.The Tabbable plugin may cause issues with other plugins that handle the Tab key, such as:
Use the query option to disable the Tabbable plugin when the Tab key should be handled by another plugin:
TabbablePlugin.configure(({ editor }) => {
const listItem = editor.plugin(PLUGINS.listItem);
const codeBlock = editor.plugin(PLUGINS.codeBlock);
const blockedTypes = [listItem, codeBlock].flatMap((plugin) =>
plugin.installed ? [plugin.schema.type] : []
);
return {
initialState: {
query: () =>
blockedTypes.length === 0 ||
!editor.read.nodes.some({
type: blockedTypes,
}),
},
};
});TabbablePlugin.configure(({ editor }) => {
const listItem = editor.plugin(PLUGINS.listItem);
const codeBlock = editor.plugin(PLUGINS.codeBlock);
const blockedTypes = [listItem, codeBlock].flatMap((plugin) =>
plugin.installed ? [plugin.schema.type] : []
);
return {
initialState: {
query: () =>
blockedTypes.length === 0 ||
!editor.read.nodes.some({
type: blockedTypes,
}),
Alternatively, if you're using the Indent plugin, you can enable the Tabbable plugin only when a specific type of node is selected, such as voids:
query: (event) => !!editor.read.nodes.some({
match: (node) => editor.read.schema.isVoid(node),
}),query: (event) => !!editor.read.nodes.some({
match: (node) => editor.read.schema.isVoid(node),
}),One TabbableEntry will be created for each tabbable DOM element in the editor, as determined using the tabbable NPM package. The list of tabbables is then filtered using isTabbable.
By default, isTabbable only returns true for entries inside void Plite nodes. You can override isTabbable to add support for DOM elements contained in other types of Plite node:
// Enable tabbable DOM elements inside CUSTOM_ELEMENT
isTabbable: (tabbableEntry) => (
tabbableEntry.slateNode.type === CUSTOM_ELEMENT ||
editor.read.schema.isVoid(tabbableEntry.slateNode)
),// Enable tabbable DOM elements inside CUSTOM_ELEMENT
isTabbable: (tabbableEntry) => (
tabbableEntry.slateNode.type === CUSTOM_ELEMENT ||
editor.read.schema.isVoid(tabbableEntry.slateNode)
),In some circumstances, you may want to allow users to tab from the editor to a DOM element rendered outside the editor, such as an interactive popover.
To do this, override insertTabbableEntries to return an array of TabbableEntry objects, one for each DOM element outside the editor that you want to include in the tabbable list. The slateNode and path of the TabbableEntry should refer to the Plite node the user's cursor will be inside when the DOM element should be tabbable to.
Set the globalEventListener option to true to make sure the Tabbable plugin is able to return the user's focus to the editor.
For example, if the DOM element appears when a link is selected, the slateNode and path should be that of the link.
// Add buttons inside .my-popover to the list of tabbables
globalEventListener: true,
insertTabbableEntries: () => {
const selectedEntry = editor.read.nodes.above();
if (!selectedEntry) return [];
const [selectedNode, selectedNodePath] = selectedEntry;
return [
...document.querySelectorAll('.my-popover > button'),
].map((domNode) => ({
domNode,
slateNode: selectedNode,
path: selectedNodePath,
}));
},// Add buttons inside .my-popover to the list of tabbables
globalEventListener: true,
insertTabbableEntries: () => {
const selectedEntry = editor.read.nodes.above();
if (!selectedEntry) return [];
const [selectedNode, selectedNodePath] = selectedEntry;
return [
...document.querySelectorAll('.my-popover > button'),
].map((domNode) => ({
domNode,
slateNode: selectedNode,
path: selectedNodePath,
}));
},Plugin for managing tab order between tabbable elements.
Enable/disable plugin dynamically.
() => trueAdd event listener to document instead of editor.
falseAdd additional tabbable entries outside editor.
() => []Determine if element should be tabbable.
(tabbableEntry) => editor.read.schema.isVoid(tabbableEntry.slateNode)Finds the next DOM or document destination from the active snapshot.
Defines the properties of a tabbable entry.
import { TabbablePlugin } from '@platejs/tabbable/react';
import { PLUGINS } from 'platejs';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
TabbablePlugin.configure(({ editor }) => {
const listItem = editor.plugin(PLUGINS.listItem);
const codeBlock = editor.plugin(PLUGINS.codeBlock);
const blockedTypes = [listItem, codeBlock].flatMap((plugin) =>
plugin.installed ? [plugin.schema.type] : []
);
return {
initialState: {
query: () =>
blockedTypes.length === 0 ||
!editor.read.nodes.some({ type: blockedTypes }),
globalEventListener: true,
isTabbable: (tabbableEntry) =>
editor.read.schema.isVoid(tabbableEntry.slateNode),
},
};
}),
],
});