Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/2026-09-29-keep-textbox-role-on-set-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/core': patch
---

The editor element keeps `role="textbox"` after `setOptions()` when `editorProps.attributes` is set. Before, a re-render in React replaced the default role with the user attributes. Attributes passed as a function now get the default role as well.
6 changes: 6 additions & 0 deletions .changeset/2026-09-29-restore-input-rule-markers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tiptap/core': patch
'@tiptap/extension-collaboration': patch
---

Undoing an input rule restores the Markdown characters that triggered it when using Collaboration.
5 changes: 5 additions & 0 deletions .changeset/2026-09-29-update-prosemirror.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/pm': patch
---

Update ProseMirror dependencies to their latest versions.
18 changes: 9 additions & 9 deletions demos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,24 @@
"lexical": "^0.36.2",
"linkifyjs": "^4.3.2",
"lowlight": "^3.3.0",
"prosemirror-changeset": "^2.4.1",
"prosemirror-changeset": "^2.4.4",
"prosemirror-collab": "^1.3.1",
"prosemirror-commands": "^1.7.1",
"prosemirror-dropcursor": "^1.8.2",
"prosemirror-commands": "^1.7.2",
"prosemirror-dropcursor": "^1.8.4",
"prosemirror-gapcursor": "^1.4.1",
"prosemirror-history": "^1.5.0",
"prosemirror-history": "^1.5.1",
"prosemirror-inputrules": "^1.5.1",
"prosemirror-keymap": "^1.2.3",
"prosemirror-markdown": "^1.13.4",
"prosemirror-markdown": "^1.13.8",
"prosemirror-menu": "^1.3.2",
"prosemirror-model": "^1.25.9",
"prosemirror-schema-basic": "^1.2.4",
"prosemirror-model": "^1.25.12",
"prosemirror-schema-basic": "^1.2.5",
"prosemirror-schema-list": "^1.5.1",
"prosemirror-state": "^1.4.4",
"prosemirror-tables": "^1.8.5",
"prosemirror-trailing-node": "^3.0.0",
"prosemirror-transform": "^1.12.0",
"prosemirror-view": "^1.42.3",
"prosemirror-transform": "^1.12.2",
"prosemirror-view": "^1.42.6",
"remixicon": "^2.5.0",
"shiki": "^1.25.1",
"y-webrtc": "10.3.0",
Expand Down
1 change: 0 additions & 1 deletion demos/src/Examples/Default/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ test.describe(`${demoPath}/${demoName}`, () => {
await expect(page.locator('.tiptap h1')).toBeVisible()

const editor = await getEditor(page)
await editor.click()
await editor.evaluate((el: any) => el.editor.commands.selectAll())
await editor.press(process.platform === 'darwin' ? 'Meta+Alt+0' : 'Control+Alt+0')

Expand Down
Empty file.
65 changes: 65 additions & 0 deletions demos/src/Experiments/InputRuleUndo/React/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Extension, InputRule } from '@tiptap/core'
import Collaboration from '@tiptap/extension-collaboration'
import Document from '@tiptap/extension-document'
import { BulletList, ListItem } from '@tiptap/extension-list'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { EditorContent, useEditor } from '@tiptap/react'
import * as Y from 'yjs'

const document = new Y.Doc()

const CustomListInputRule = Extension.create({
name: 'customListInputRule',

addInputRules() {
return [
new InputRule({
find: /^-\s$/,
handler: ({ chain, range, state }) => {
const $from = state.doc.resolve(range.from)

chain()
.setTextSelection({ from: $from.start(), to: $from.end() })
.toggleBulletList()
.command(({ tr }) => {
tr.delete(tr.selection.from, tr.selection.from + 2)
return true
})
.run()
},
}),
]
},
})

export default function InputRuleUndo() {
const editor = useEditor({
extensions: [
Document,
Paragraph,
Text,
BulletList,
ListItem,
CustomListInputRule,
Collaboration.configure({ document }),
],
})

return (
<>
<div className="control-group">
<p>Type "- " at the start of the paragraph. Undo should restore the marker.</p>
<div className="button-group">
<button type="button" disabled={!editor} onClick={() => editor?.commands.undo()}>
Undo
</button>
<button type="button" disabled={!editor} onClick={() => editor?.commands.redo()}>
Redo
</button>
</div>
</div>
<EditorContent editor={editor} />
</>
)
}
6 changes: 3 additions & 3 deletions demos/src/Marks/Link/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ async function paste(
await editor.evaluate((el: HTMLElement, text: string) => {
const dt = new DataTransfer()
dt.setData('text/plain', text)
el.dispatchEvent(
new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }),
)
const event = new ClipboardEvent('paste', { bubbles: true, cancelable: true })
Object.defineProperty(event, 'clipboardData', { value: dt })
el.dispatchEvent(event)
}, payload)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
},
"devDependencies": {
"@tiptap/pm": "workspace:^",
"prosemirror-model": "^1.25.11"
"prosemirror-model": "^1.25.12"
},
"peerDependencies": {
"@tiptap/pm": "workspace:*"
Expand Down
75 changes: 75 additions & 0 deletions packages/core/src/Editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,81 @@ describe('editorProps', () => {
})
})

describe('editorProps.attributes', () => {
let editor: Editor

afterEach(() => {
editor.destroy()
})

it('adds role="textbox" when attributes are set while constructing Editor', () => {
editor = new Editor({
element: document.createElement('div'),
extensions: [StarterKit],
editorProps: { attributes: { class: 'my-editor' } },
})

expect(editor.view.dom.getAttribute('role')).toBe('textbox')
expect(editor.view.dom.classList.contains('my-editor')).toBe(true)
})

it('keeps role="textbox" when attributes are set through setOptions', () => {
editor = new Editor({
element: document.createElement('div'),
extensions: [StarterKit],
editorProps: { attributes: { class: 'my-editor' } },
})

editor.setOptions({ editorProps: { attributes: { class: 'my-editor is-updated' } } })

expect(editor.view.dom.getAttribute('role')).toBe('textbox')
expect(editor.view.dom.classList.contains('is-updated')).toBe(true)
})

it('keeps role="textbox" when setOptions is called with unrelated options', () => {
editor = new Editor({
element: document.createElement('div'),
extensions: [StarterKit],
editorProps: { attributes: { class: 'my-editor' } },
})

editor.setOptions({ editable: false })

expect(editor.view.dom.getAttribute('role')).toBe('textbox')
expect(editor.view.dom.classList.contains('my-editor')).toBe(true)
})

it('lets the user override the role', () => {
editor = new Editor({
element: document.createElement('div'),
extensions: [StarterKit],
editorProps: { attributes: { role: 'document' } },
})

expect(editor.view.dom.getAttribute('role')).toBe('document')

editor.setOptions({ editorProps: { attributes: { role: 'document', class: 'my-editor' } } })

expect(editor.view.dom.getAttribute('role')).toBe('document')
})

it('keeps role="textbox" when attributes are a function', () => {
editor = new Editor({
element: document.createElement('div'),
extensions: [StarterKit],
editorProps: { attributes: () => ({ class: 'my-editor' }) },
})

expect(editor.view.dom.getAttribute('role')).toBe('textbox')
expect(editor.view.dom.classList.contains('my-editor')).toBe(true)

editor.setOptions({ editorProps: { attributes: () => ({ class: 'is-updated' }) } })

expect(editor.view.dom.getAttribute('role')).toBe('textbox')
expect(editor.view.dom.classList.contains('is-updated')).toBe(true)
})
})

describe('editor.getHTML / editor.getJSON', () => {
let editor: Editor

Expand Down
32 changes: 26 additions & 6 deletions packages/core/src/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,10 @@ export class Editor extends EventEmitter<EditorEvents> {
}

if (this.options.editorProps) {
this.view.setProps(this.options.editorProps)
this.view.setProps({
...this.options.editorProps,
attributes: this.getEditorViewAttributes(),
})
}

this.view.updateState(this.state)
Expand Down Expand Up @@ -575,6 +578,27 @@ export class Editor extends EventEmitter<EditorEvents> {
return doc
}

/**
* Returns the attributes for the editor element.
* Merges the default `role="textbox"` with the attributes from `editorProps`,
* so the role is kept no matter when the props are applied.
*/
private getEditorViewAttributes(): NonNullable<DirectEditorProps['attributes']> {
const attributes = this.options.editorProps?.attributes

if (isFunction(attributes)) {
return state => ({
role: 'textbox',
...attributes(state),
})
}

return {
role: 'textbox',
...attributes,
}
}

/**
* Creates a ProseMirror view.
*/
Expand All @@ -595,11 +619,7 @@ export class Editor extends EventEmitter<EditorEvents> {

this.editorView = new EditorView(element, {
...editorProps,
attributes: {
// add `role="textbox"` to the editor element
role: 'textbox',
...editorProps?.attributes,
},
attributes: this.getEditorViewAttributes(),
dispatchTransaction: dispatch,
transformPastedHTML,
state: this.editorState,
Expand Down
Loading
Loading