Skip to main content

Tabbable

Programmatically corrects the tab order for tabbable elements inside the editor

Installation#

npm install @udecode/plate
# or
npm install @udecode/plate-tabbable

Usage#

import React from 'react';
import {
  createListPlugin,
  createTabbablePlugin,
  ELEMENT_CODE_BLOCK,
  ELEMENT_LI,
  findNode,
  Plate,
} from '@udecode/plate';
import { basicNodesPlugins } from './basic-nodes/basicNodesPlugins';
import { editableProps } from './common/editableProps';
import { plateUI } from './common/plateUI';
import { createTabbableElementPlugin } from './tabbable/createTabbableElementPlugin';
import { TabbableElement } from './tabbable/TabbableElement';
import { tabbableValue } from './tabbable/tabbableValue';
import { createMyPlugins, MyValue } from './typescript/plateTypes';

const plugins = createMyPlugins(
  [
    ...basicNodesPlugins,
    createListPlugin(),
    createTabbablePlugin({
      options: {
        query: (editor) => {
          const inList = findNode(editor, { match: { type: ELEMENT_LI } });
          const inCodeBlock = findNode(editor, {
            match: { type: ELEMENT_CODE_BLOCK },
          });
          return !inList && !inCodeBlock;
        },
      },
    }),
    createTabbableElementPlugin({
      component: TabbableElement,
    }),
  ],
  {
    components: plateUI,
  }
);

export default () => (
  <>
    <button type="button">Button before editor</button>
    <Plate<MyValue>
      editableProps={editableProps}
      plugins={plugins}
      initialValue={tabbableValue}
    />
    <button type="button">Button after editor</button>
  </>

Conflicts with other plugins#

The Tabbable plugin may cause issues with other plugins that handle the Tab key, such as:

  • Lists
  • Code blocks
  • Indent plugin

Use the query option to disable the Tabbable plugin when the Tab key should be handled by another plugin:

query: (editor) => {
const inList = findNode(editor, { match: { type: ELEMENT_LI } });
const inCodeBlock = findNode(editor, { match: { type: ELEMENT_CODE_BLOCK } });
return !inList && !inCodeBlock;
},

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: (editor) => !!findNode(editor, {
match: (node) => isVoid(editor, node),
}),

Non-void Slate nodes#

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 Slate nodes. You can override isTabbable to add support for DOM elements contained in other types of Slate node.

// Enable tabbable DOM elements inside CUSTOM_ELEMENT
isTabbable: (editor, tabbableEntry) => (
tabbableEntry.slateNode.type === CUSTOM_ELEMENT ||
isVoid(editor, tabbableEntry.slateNode)
),

DOM elements outside the editor#

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 Slate node the user's cursor will be inside when the DOM element should be tabbable to. For example, if the DOM element appears when a link is selected, the slateNode and path should be that of the link.

Set the globalEventListener option to true to make sure the Tabbable plugin is able to return the user's focus to the editor.

// Add buttons inside .my-popover to the list of tabbables
globalEventListener: true,
insertTabbableEntries: (editor) => {
const [selectedNode, selectedNodePath] = getNodeEntry(editor, editor.selection);
return [
...document.querySelectorAll('.my-popover > button'),
].map((domNode) => ({
domNode,
slateNode: selectedNode,
path: selectedNodePath,
}));
},

Options#

export type TabbableEntry = {
domNode: HTMLElement;
slateNode: TNode;
path: TPath;
};
export interface TabbablePlugin<V extends Value = Value> {
/**
* Dynamically enable or disable the plugin.
* @default: () => true
*/
query?: (editor: PlateEditor<V>, event: KeyboardEvent) => boolean;
/**
* When true, the plugin will add its event listener to the document instead
* of the editor, allowing it to capture events from outside the editor.
* @default: false
*/
globalEventListener?: boolean;
/**
* Add additional tabbables to the list of tabbables. Useful for adding
* tabbables that are not contained within the editor. Ignores `isTabbable`.
* @default: () => []
*/
insertTabbableEntries?: (
editor: PlateEditor<V>,
event: KeyboardEvent
) => TabbableEntry[];
/**
* Determine whether an element should be included in the tabbable list.
* @default: (editor, tabbableEntry) => isVoid(editor, tabbableEntry.slateNode)
*/
isTabbable?: (editor: PlateEditor<V>, tabbableEntry: TabbableEntry) => boolean;
}

Source Code#