Skip to content

Commit

Permalink
fix(pte): preserve block key when pressing enter at start of block (#…
Browse files Browse the repository at this point in the history
…6521)

* fix(pte): preserve block key when pressing enter at start of block

* fix(pte): add withHotkeys test (wip)

* fix(pte): remove withHotkeys for new withInsertBreak plugin

* chore(pte): share createEmptyTextBlock function

* fix(pte): update e2e tests

* fix(pte): update writing together snapshot test
  • Loading branch information
pedrobonamin committed May 2, 2024
1 parent 790bc8f commit 7df5396
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ describe('selection adjustment', () => {
expect(await editorA.getValue()).toMatchInlineSnapshot(`
Array [
Object {
"_key": "someKey",
"_key": "B-7",
"_type": "block",
"children": Array [
Object {
"_key": "anotherKey",
"_key": "B-6",
"_type": "span",
"marks": Array [],
"text": "",
Expand All @@ -42,11 +42,11 @@ describe('selection adjustment', () => {
"style": "normal",
},
Object {
"_key": "B-6",
"_key": "someKey",
"_type": "block",
"children": Array [
Object {
"_key": "B-5",
"_key": "anotherKey",
"_type": "span",
"marks": Array [],
"text": "Hello",
Expand All @@ -61,7 +61,7 @@ describe('selection adjustment', () => {
expect(selectionA).toMatchInlineSnapshot(`
Object {
"anchor": Object {
"offset": 0,
"offset": 2,
"path": Array [
Object {
"_key": "someKey",
Expand All @@ -74,7 +74,7 @@ describe('selection adjustment', () => {
},
"backward": false,
"focus": Object {
"offset": 0,
"offset": 2,
"path": Array [
Object {
"_key": "someKey",
Expand Down Expand Up @@ -298,13 +298,13 @@ describe('selection adjustment', () => {
],
},
{
_key: 'B-6',
_key: 'B-7',
_type: 'block',
markDefs: [],
style: 'normal',
children: [
{
_key: 'B-5',
_key: 'B-6',
_type: 'span',
text: '',
marks: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,8 @@ describe('collaborate editing', () => {
}
`)
await editorA.pressKey('Enter')
expect(valA).toMatchInlineSnapshot(`
const valAAfterSecondEnter = await editorA.getValue()
expect(valAAfterSecondEnter).toMatchInlineSnapshot(`
Array [
Object {
"_key": "randomKey0",
Expand All @@ -637,6 +638,20 @@ describe('collaborate editing', () => {
"markDefs": Array [],
"style": "normal",
},
Object {
"_key": "A-9",
"_type": "block",
"children": Array [
Object {
"_key": "A-8",
"_type": "span",
"marks": Array [],
"text": "",
},
],
"markDefs": Array [],
"style": "normal",
},
Object {
"_key": "A-6",
"_type": "block",
Expand Down Expand Up @@ -669,8 +684,8 @@ describe('collaborate editing', () => {
`)
const selectionA = await editorA.getSelection()
expect(selectionA).toEqual({
anchor: {path: [{_key: 'A-8'}, 'children', {_key: 'A-7'}], offset: 0},
focus: {path: [{_key: 'A-8'}, 'children', {_key: 'A-7'}], offset: 0},
anchor: {path: [{_key: 'A-6'}, 'children', {_key: 'A-5'}], offset: 0},
focus: {path: [{_key: 'A-6'}, 'children', {_key: 'A-5'}], offset: 0},
backward: false,
})
const selectionB = await editorB.getSelection()
Expand Down
4 changes: 2 additions & 2 deletions packages/@sanity/portable-text-editor/src/editor/Editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable(
[change$, keyGenerator, schemaTypes],
)
const withHotKeys = useMemo(
() => createWithHotkeys(schemaTypes, keyGenerator, portableTextEditor, hotkeys),
[hotkeys, keyGenerator, portableTextEditor, schemaTypes],
() => createWithHotkeys(schemaTypes, portableTextEditor, hotkeys),
[hotkeys, portableTextEditor, schemaTypes],
)

// Output a minimal React editor inside Editable when in readOnly mode.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {describe, expect, it, jest} from '@jest/globals'
import {render, waitFor} from '@testing-library/react'
import {createRef, type RefObject} from 'react'

import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester'
import {PortableTextEditor} from '../../PortableTextEditor'

const initialValue = [
{
_key: 'a',
_type: 'myTestBlockType',
children: [
{
_key: 'a1',
_type: 'span',
marks: [],
text: 'Block A',
},
],
markDefs: [],
style: 'normal',
},
{
_key: 'b',
_type: 'myTestBlockType',
children: [
{
_key: 'b1',
_type: 'span',
marks: [],
text: 'Block B',
},
],
markDefs: [],
style: 'normal',
},
]

describe('plugin:withInsertBreak: "enter"', () => {
it('keeps text block key if enter is pressed at the start of the block, creating a new one in "before" position', async () => {
const initialSelection = {
focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 0},
anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 0},
}

const editorRef: RefObject<PortableTextEditor> = createRef()
const onChange = jest.fn()
render(
<PortableTextEditorTester
onChange={onChange}
ref={editorRef}
schemaType={schemaType}
value={initialValue}
/>,
)
const editor = editorRef.current
const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject')
await waitFor(async () => {
if (editor && inlineType) {
PortableTextEditor.focus(editor)
PortableTextEditor.select(editor, initialSelection)
PortableTextEditor.insertBreak(editor)

const value = PortableTextEditor.getValue(editor)
expect(value).toEqual([
initialValue[0],
{
_type: 'myTestBlockType',
_key: '3',
style: 'normal',
markDefs: [],
children: [
{
_type: 'span',
_key: '2',
text: '',
marks: [],
},
],
},
initialValue[1],
])
}
})
})
it('splits the text block key if enter is pressed at the middle of the block', async () => {
const initialSelection = {
focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 2},
anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 2},
}

const editorRef: RefObject<PortableTextEditor> = createRef()
const onChange = jest.fn()
render(
<PortableTextEditorTester
onChange={onChange}
ref={editorRef}
schemaType={schemaType}
value={initialValue}
/>,
)
const editor = editorRef.current
const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject')
await waitFor(async () => {
if (editor && inlineType) {
PortableTextEditor.focus(editor)
PortableTextEditor.select(editor, initialSelection)
PortableTextEditor.insertBreak(editor)

const value = PortableTextEditor.getValue(editor)
expect(value).toEqual([
initialValue[0],
{
_key: 'b',
_type: 'myTestBlockType',
children: [
{
_key: 'b1',
_type: 'span',
marks: [],
text: 'Bl',
},
],
markDefs: [],
style: 'normal',
},
{
_key: '2',
_type: 'myTestBlockType',
markDefs: [],
style: 'normal',
children: [
{
_key: '1',
_type: 'span',
marks: [],
text: 'ock B',
},
],
},
])
}
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -677,11 +677,11 @@ describe('plugin:withPortableTextMarksModel', () => {
"style": "normal",
},
Object {
"_key": "2f55670a03bb",
"_key": "3",
"_type": "myTestBlockType",
"children": Array [
Object {
"_key": "9f5ed7dee7ab",
"_key": "2",
"_type": "span",
"marks": Array [],
"text": "",
Expand All @@ -691,11 +691,11 @@ describe('plugin:withPortableTextMarksModel', () => {
"style": "normal",
},
Object {
"_key": "2",
"_key": "2f55670a03bb",
"_type": "myTestBlockType",
"children": Array [
Object {
"_key": "1",
"_key": "9f5ed7dee7ab",
"_type": "span",
"marks": Array [
"bab319ad3a9d",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '
import {type HotkeyOptions} from '../../types/options'
import {type SlateTextBlock, type VoidElement} from '../../types/slate'
import {debugWithName} from '../../utils/debug'
import {toSlateValue} from '../../utils/values'
import {type PortableTextEditor} from '../PortableTextEditor'

const debug = debugWithName('plugin:withHotKeys')
Expand All @@ -31,32 +30,11 @@ const DEFAULT_HOTKEYS: HotkeyOptions = {
*/
export function createWithHotkeys(
types: PortableTextMemberSchemaTypes,
keyGenerator: () => string,
portableTextEditor: PortableTextEditor,
hotkeysFromOptions?: HotkeyOptions,
): (editor: PortableTextSlateEditor & ReactEditor) => any {
const reservedHotkeys = ['enter', 'tab', 'shift', 'delete', 'end']
const activeHotkeys = hotkeysFromOptions || DEFAULT_HOTKEYS // TODO: Merge where possible? A union?
const createEmptyBlock = () =>
toSlateValue(
[
{
_type: types.block.name,
_key: keyGenerator(),
style: 'normal',
markDefs: [],
children: [
{
_type: 'span',
_key: keyGenerator(),
text: '',
marks: [],
},
],
},
],
portableTextEditor,
)[0]
return function withHotKeys(editor: PortableTextSlateEditor & ReactEditor) {
editor.pteWithHotKeys = (event: KeyboardEvent<HTMLDivElement>): void => {
// Wire up custom marks hotkeys
Expand Down Expand Up @@ -228,15 +206,15 @@ export function createWithHotkeys(
const [, end] = Range.edges(editor.selection)
const endAtEndOfNode = Editor.isEnd(editor, end, end.path)
if (endAtEndOfNode) {
Editor.insertNode(editor, createEmptyBlock())
Editor.insertNode(editor, editor.pteCreateEmptyBlock())
event.preventDefault()
editor.onChange()
return
}
}
// Block object enter key
if (focusBlock && Editor.isVoid(editor, focusBlock)) {
Editor.insertNode(editor, createEmptyBlock())
Editor.insertNode(editor, editor.pteCreateEmptyBlock())
event.preventDefault()
editor.onChange()
return
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {Editor, Node, Path, Range, Transforms} from 'slate'

import {type PortableTextSlateEditor} from '../../types/editor'
import {type SlateTextBlock, type VoidElement} from '../../types/slate'

/**
* Changes default behavior of insertBreak to insert a new block instead of splitting current when the cursor is at the
* start of the block.
*/
export function createWithInsertBreak(): (
editor: PortableTextSlateEditor,
) => PortableTextSlateEditor {
return function withInsertBreak(editor: PortableTextSlateEditor): PortableTextSlateEditor {
const {insertBreak} = editor

editor.insertBreak = () => {
if (editor.selection) {
const focusBlockPath = editor.selection.focus.path.slice(0, 1)
const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement

if (editor.isTextBlock(focusBlock)) {
// Enter from another style than the first (default one)
const [, end] = Range.edges(editor.selection)
// If it's at the start of block, we want to preserve the current block key and insert a new one in the current position instead of splitting the node.
const isEndAtStartOfNode = Editor.isStart(editor, end, end.path)
if (isEndAtStartOfNode) {
Editor.insertNode(editor, editor.pteCreateEmptyBlock())
const [nextBlockPath] = Path.next(focusBlockPath)
Transforms.select(editor, {
anchor: {path: [nextBlockPath, 0], offset: 0},
focus: {path: [nextBlockPath, 0], offset: 0},
})

editor.onChange()
return
}
}
}
insertBreak()
}
return editor
}
}

0 comments on commit 7df5396

Please sign in to comment.