Skip to content

Commit

Permalink
Add onSelectionChange and onValueChange in Slate React component (#…
Browse files Browse the repository at this point in the history
…5526)

* Add `onSelectorChange` and `onValueChange` in Slate React component

* docs: add changeset

* fix: fixed lint error

* Rename Slate React component `onSelectorChange` to `onSelectionChange`.
Add more unit tests.

* docs: update changeset

---------

Co-authored-by: willliu <[email protected]>
  • Loading branch information
jkcs and willliu authored Oct 20, 2023
1 parent 0bdff51 commit 623f445
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-ligers-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'slate-react': minor
---

Add `onSelectionChange` and `onValueChange` in Slate React component
45 changes: 33 additions & 12 deletions packages/slate-react/src/components/slate.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react'
import { Descendant, Editor, Node, Scrubber } from 'slate'
import { Descendant, Editor, Node, Operation, Scrubber, Selection } from 'slate'
import { FocusedContext } from '../hooks/use-focused'
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
import { SlateContext, SlateContextValue } from '../hooks/use-slate'
Expand All @@ -22,8 +22,18 @@ export const Slate = (props: {
initialValue: Descendant[]
children: React.ReactNode
onChange?: (value: Descendant[]) => void
onSelectionChange?: (selection: Selection) => void
onValueChange?: (value: Descendant[]) => void
}) => {
const { editor, children, onChange, initialValue, ...rest } = props
const {
editor,
children,
onChange,
onSelectionChange,
onValueChange,
initialValue,
...rest
} = props

const [context, setContext] = React.useState<SlateContextValue>(() => {
if (!Node.isNodeList(initialValue)) {
Expand All @@ -48,17 +58,28 @@ export const Slate = (props: {
onChange: handleSelectorChange,
} = useSelectorContext(editor)

const onContextChange = useCallback(() => {
if (onChange) {
onChange(editor.children)
}
const onContextChange = useCallback(
(options?: { operation?: Operation }) => {
if (onChange) {
onChange(editor.children)
}

setContext(prevContext => ({
v: prevContext.v + 1,
editor,
}))
handleSelectorChange(editor)
}, [editor, handleSelectorChange, onChange])
switch (options?.operation?.type) {
case 'set_selection':
onSelectionChange?.(editor.selection)
break
default:
onValueChange?.(editor.children)
}

setContext(prevContext => ({
v: prevContext.v + 1,
editor,
}))
handleSelectorChange(editor)
},
[editor, handleSelectorChange, onChange, onSelectionChange, onValueChange]
)

useEffect(() => {
EDITOR_TO_ON_CHANGE.set(editor, onContextChange)
Expand Down
2 changes: 1 addition & 1 deletion packages/slate-react/src/plugin/with-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ export const withReact = <T extends BaseEditor>(
const onContextChange = EDITOR_TO_ON_CHANGE.get(e)

if (onContextChange) {
onContextChange()
onContextChange(options)
}

onChange(options)
Expand Down
7 changes: 5 additions & 2 deletions packages/slate-react/src/utils/weak-maps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Ancestor, Editor, Node, Range, RangeRef, Text } from 'slate'
import { Ancestor, Editor, Node, Operation, Range, RangeRef, Text } from 'slate'
import { Action } from '../hooks/android-input-manager/android-input-manager'
import { TextDiff } from './diff-text'
import { Key } from './key'
Expand Down Expand Up @@ -47,7 +47,10 @@ export const EDITOR_TO_USER_SELECTION: WeakMap<
* Weak map for associating the context `onChange` context with the plugin.
*/

export const EDITOR_TO_ON_CHANGE = new WeakMap<Editor, () => void>()
export const EDITOR_TO_ON_CHANGE = new WeakMap<
Editor,
(options?: { operation?: Operation }) => void
>()

/**
* Weak maps for saving pending state on composition stage.
Expand Down
105 changes: 104 additions & 1 deletion packages/slate-react/test/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react'
import { createEditor, Element, Transforms } from 'slate'
import { createEditor, Text, Transforms } from 'slate'
import { create, act, ReactTestRenderer } from 'react-test-renderer'
import { Slate, withReact, Editable } from '../src'

Expand Down Expand Up @@ -95,4 +95,107 @@ describe('slate-react', () => {
})
})
})

test('calls onSelectionChange when editor select change', async () => {
const editor = withReact(createEditor())
const initialValue = [
{ type: 'block', children: [{ text: 'te' }] },
{ type: 'block', children: [{ text: 'st' }] },
]
const onChange = jest.fn()
const onValueChange = jest.fn()
const onSelectionChange = jest.fn()

act(() => {
create(
<Slate
editor={editor}
initialValue={initialValue}
onChange={onChange}
onValueChange={onValueChange}
onSelectionChange={onSelectionChange}
>
<Editable />
</Slate>,
{ createNodeMock }
)
})

await act(async () =>
Transforms.select(editor, { path: [0, 0], offset: 2 })
)

expect(onSelectionChange).toHaveBeenCalled()
expect(onChange).toHaveBeenCalled()
expect(onValueChange).not.toHaveBeenCalled()
})

test('calls onValueChange when editor children change', async () => {
const editor = withReact(createEditor())
const initialValue = [{ type: 'block', children: [{ text: 'test' }] }]
const onChange = jest.fn()
const onValueChange = jest.fn()
const onSelectionChange = jest.fn()

act(() => {
create(
<Slate
editor={editor}
initialValue={initialValue}
onChange={onChange}
onValueChange={onValueChange}
onSelectionChange={onSelectionChange}
>
<Editable />
</Slate>,
{ createNodeMock }
)
})

await act(async () => Transforms.insertText(editor, 'Hello word!'))

expect(onValueChange).toHaveBeenCalled()
expect(onChange).toHaveBeenCalled()
expect(onSelectionChange).not.toHaveBeenCalled()
})

test('calls onValueChange when editor setNodes', async () => {
const editor = withReact(createEditor())
const initialValue = [{ type: 'block', children: [{ text: 'test' }] }]
const onChange = jest.fn()
const onValueChange = jest.fn()
const onSelectionChange = jest.fn()

act(() => {
create(
<Slate
editor={editor}
initialValue={initialValue}
onChange={onChange}
onValueChange={onValueChange}
onSelectionChange={onSelectionChange}
>
<Editable />
</Slate>,
{ createNodeMock }
)
})

await act(async () =>
Transforms.setNodes(
editor,
// @ts-ignore
{ bold: true },
{
at: { path: [0, 0], offset: 2 },
match: Text.isText,
split: true,
}
)
)

expect(onChange).toHaveBeenCalled()
expect(onValueChange).toHaveBeenCalled()
expect(onSelectionChange).not.toHaveBeenCalled()
})
})

0 comments on commit 623f445

Please sign in to comment.