Tabbable
Ensure a smooth tab navigation experience within your editor with the Tabbable plugin.
Properly handle tab orders for void nodes, allowing for seamless navigation and interaction. Without this plugin, DOM elements inside void nodes come after the editor in the tab order.
This is a void element.
This is a void element.
Place your cursor here and try pressing tab or shift+tab.
- List item 1
- List item 2
- List item 3
In this example, the plugin is disabled when the cursor is inside a list or a code block. You can customise this using the
query option.This is a void element.
When you press tab at the end of the editor, the focus should go to the button below.
1
100%
Files
'use client';
import * as React from 'react';
import type { PlateElementProps } from 'platejs/react';
import { TabbablePlugin } from '@platejs/tabbable/react';
import {
Plate,
PlateElement,
useFocused,
usePlateEditor,
useSelected,
} from 'platejs/react';
import { cn } from '@/lib/utils';
import { EditorKit } from '@/components/editor/editor-kit';
import { tabbableValue } from '@/registry/examples/values/tabbable-value';
import { Editor, EditorContainer } from '@/components/ui/editor';
export default function TabbableDemo() {
const editor = usePlateEditor({
plugins: [
...EditorKit,
TabbablePlugin.configure({
node: { component: TabbableElement, isElement: true, isVoid: true },
}),
],
value: tabbableValue,
});
return (
<Plate editor={editor}>
<EditorContainer variant="demo">
<Editor />
</EditorContainer>
</Plate>
);
}
export function TabbableElement({ children, ...props }: PlateElementProps) {
const selected = useSelected();
const focused = useFocused();
return (
<PlateElement {...props}>
<div
className={cn(
'mb-2 p-2',
selected && focused
? 'border-2 border-blue-500'
: 'border border-gray-200'
)}
contentEditable={false}
>
<p>This is a void element.</p>
<button type="button">Button 1</button>{' '}
<button type="button">Button 2</button>
</div>
{children}
</PlateElement>
);
}
tabbable-demo


功能特性
- 确保编辑器中可聚焦元素之间的标签顺序一致
- 管理空元素与外部DOM元素之间的焦点切换
套件使用
安装
最快捷的方式是使用 TabbableKit,它包含预配置的 TabbablePlugin 和智能查询逻辑,可避免与其他插件冲突。
'use client';
import { TabbablePlugin } from '@platejs/tabbable/react';
import { KEYS } from 'platejs';
export const TabbableKit = TabbablePlugin.configure(({ editor }) => ({
node: {
isElement: true,
},
options: {
query: () => {
if (editor.api.isAt({ start: true }) || editor.api.isAt({ end: true }))
return false;
return !editor.api.some({
match: (n) => {
return !!(
(n.type &&
[
KEYS.codeBlock,
KEYS.li,
KEYS.listTodoClassic,
KEYS.table,
].includes(n.type as any)) ||
n.listStyleType
);
},
});
},
},
override: {
enabled: {
indent: false,
},
},
}));
添加套件
import { createPlateEditor } from 'platejs/react';
import { TabbableKit } from '@/components/editor/plugins/tabbable-kit';
const editor = createPlateEditor({
plugins: [
// ...其他插件,
...TabbableKit,
],
});手动配置
安装
pnpm add @platejs/tabbable
添加插件
import { TabbablePlugin } from '@platejs/tabbable/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...其他插件,
TabbablePlugin,
],
});配置插件
import { TabbablePlugin } from '@platejs/tabbable/react';
import { createPlateEditor } from 'platejs/react';
import { KEYS } from 'platejs';
const editor = createPlateEditor({
plugins: [
// ...其他插件,
TabbablePlugin.configure({
options: {
query: (event) => {
// 在列表或代码块中禁用
const inList = editor.api.some({ match: { type: KEYS.li } });
const inCodeBlock = editor.api.some({ match: { type: KEYS.codeBlock } });
return !inList && !inCodeBlock;
},
globalEventListener: true,
isTabbable: (tabbableEntry) =>
editor.api.isVoid(tabbableEntry.slateNode),
},
}),
],
});options.query: 根据编辑器状态动态启用/禁用插件的函数options.globalEventListener: 为true时,将事件监听器添加到document而非编辑器options.isTabbable: 判断哪些元素应包含在标签顺序中的函数
高级用法
与其他插件的冲突
Tabbable插件可能会与处理Tab键的其他插件产生冲突,例如:
- 列表插件
- 代码块插件
- 缩进插件
使用query选项在Tab键应由其他插件处理时禁用Tabbable插件:
query: (event) => {
const inList = editor.api.some({ match: { type: KEYS.li } });
const inCodeBlock = editor.api.some({ match: { type: KEYS.codeBlock } });
return !inList && !inCodeBlock;
},如果使用缩进插件,可以仅在选中特定类型节点(如void节点)时启用Tabbable插件:
query: (event) => !!editor.api.some({
match: (node) => editor.api.isVoid(node),
}),非空Slate节点
将为编辑器中的每个可聚焦DOM元素创建一个TabbableEntry,使用tabbable NPM包确定。然后使用isTabbable过滤可聚焦列表。
默认情况下,isTabbable仅对void Slate节点内的entry返回true。可以覆盖isTabbable以支持其他类型Slate节点中包含的DOM元素:
// 启用CUSTOM_ELEMENT内的可聚焦DOM元素
isTabbable: (tabbableEntry) => (
tabbableEntry.slateNode.type === CUSTOM_ELEMENT ||
editor.api.isVoid(tabbableEntry.slateNode)
),编辑器外部的DOM元素
某些情况下,可能需要允许用户从编辑器切换到外部渲染的DOM元素(如交互式弹出框)。
为此,覆盖insertTabbableEntries返回TabbableEntry对象数组,每个对象对应一个要包含在可聚焦列表中的外部DOM元素。TabbableEntry的slateNode和path应引用当DOM元素可聚焦时用户光标所在的Slate节点。
将globalEventListener选项设为true以确保Tabbable插件能将用户焦点返回到编辑器。
例如,如果DOM元素在选中链接时出现,slateNode和path应为该链接的节点。
// 将.my-popover内的按钮添加到可聚焦列表
globalEventListener: true,
insertTabbableEntries: (event) => {
const [selectedNode, selectedNodePath] = editor.api.node(editor.selection);
return [
...document.querySelectorAll('.my-popover > button'),
].map((domNode) => ({
domNode,
slateNode: selectedNode,
path: selectedNodePath,
}));
},插件
TabbablePlugin
管理可聚焦元素间标签顺序的插件。
类型
TabbableEntry
定义可聚焦entry的属性。