diff --git a/.changeset/many-houses-occur.md b/.changeset/many-houses-occur.md new file mode 100644 index 0000000000..b9da4b217c --- /dev/null +++ b/.changeset/many-houses-occur.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": minor +--- + +Add configurable tabindex option to allow customizing the tabindex attribute on the editor element via coreExtensionOptions diff --git a/packages/core/__tests__/tabindex.spec.ts b/packages/core/__tests__/tabindex.spec.ts new file mode 100644 index 0000000000..39bfe674c9 --- /dev/null +++ b/packages/core/__tests__/tabindex.spec.ts @@ -0,0 +1,74 @@ +import { Editor } from '@tiptap/core' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import Text from '@tiptap/extension-text' +import { describe, expect, it } from 'vitest' + +describe('tabindex extension', () => { + it('should set tabindex="0" on editable editor by default', () => { + const editor = new Editor({ + extensions: [Document, Paragraph, Text], + }) + + expect(editor.view.dom.getAttribute('tabindex')).toBe('0') + + editor.destroy() + }) + + it('should not set tabindex on non-editable editor by default', () => { + const editor = new Editor({ + extensions: [Document, Paragraph, Text], + editable: false, + }) + + expect(editor.view.dom.getAttribute('tabindex')).toBeNull() + + editor.destroy() + }) + + it('should set custom tabindex on editable editor when configured', () => { + const editor = new Editor({ + extensions: [Document, Paragraph, Text], + coreExtensionOptions: { + tabindex: { + value: '4', + }, + }, + }) + + expect(editor.view.dom.getAttribute('tabindex')).toBe('4') + + editor.destroy() + }) + + it('should set custom tabindex on non-editable editor when configured', () => { + const editor = new Editor({ + extensions: [Document, Paragraph, Text], + editable: false, + coreExtensionOptions: { + tabindex: { + value: '-1', + }, + }, + }) + + expect(editor.view.dom.getAttribute('tabindex')).toBe('-1') + + editor.destroy() + }) + + it('should set tabindex="0" when value is explicitly undefined', () => { + const editor = new Editor({ + extensions: [Document, Paragraph, Text], + coreExtensionOptions: { + tabindex: { + value: undefined, + }, + }, + }) + + expect(editor.view.dom.getAttribute('tabindex')).toBe('0') + + editor.destroy() + }) +}) diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts index c9f83e641b..415c034d20 100644 --- a/packages/core/src/Editor.ts +++ b/packages/core/src/Editor.ts @@ -433,7 +433,9 @@ export class Editor extends EventEmitter { Commands, FocusEvents, Keymap, - Tabindex, + Tabindex.configure({ + value: this.options.coreExtensionOptions?.tabindex?.value, + }), Drop, Paste, Delete, diff --git a/packages/core/src/extensions/tabindex.ts b/packages/core/src/extensions/tabindex.ts index da14d02afe..aa886c8be2 100644 --- a/packages/core/src/extensions/tabindex.ts +++ b/packages/core/src/extensions/tabindex.ts @@ -2,15 +2,55 @@ import { Plugin, PluginKey } from '@tiptap/pm/state' import { Extension } from '../Extension.js' -export const Tabindex = Extension.create({ +/** + * Options for the Tabindex extension. + */ +export type TabindexOptions = { + /** + * The value for the `tabindex` attribute on the editor element. + * When undefined, editable editors default to `0` and non-editable editors get no tabindex. + */ + value?: string +} + +/** + * The Tabindex extension adds a configurable tabindex attribute to the editor. + * + * By default, the editor gets tabindex="0" when editable. This can be customized + * via coreExtensionOptions to support specific focus ordering requirements in forms + * or to enable focusing on non-editable editors. + * + * @example + * ```ts + * new Editor({ + * coreExtensionOptions: { + * tabindex: { + * value: '-1', + * }, + * }, + * }) + * ``` + */ +export const Tabindex = Extension.create({ name: 'tabindex', + addOptions() { + return { + value: undefined, + } + }, + addProseMirrorPlugins() { return [ new Plugin({ key: new PluginKey('tabindex'), props: { - attributes: (): { [name: string]: string } => (this.editor.isEditable ? { tabindex: '0' } : {}), + attributes: (): { [name: string]: string } => { + if (!this.editor.isEditable && this.options.value === undefined) { + return {} + } + return { tabindex: this.options.value ?? '0' } + }, }, }), ] diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index f8046174f1..de1bd45b99 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -339,6 +339,15 @@ export interface EditorOptions { clipboardTextSerializer?: { blockSeparator?: string } + /** + * Options for the `tabindex` core extension. + */ + tabindex?: { + /** + * The value for the `tabindex` attribute on the editor element. + */ + value?: string + } delete?: { /** * Whether the `delete` extension should be called asynchronously to avoid blocking the editor while processing deletions