diff --git a/.changeset/curly-ligers-lay.md b/.changeset/curly-ligers-lay.md new file mode 100644 index 0000000000..9311e653b9 --- /dev/null +++ b/.changeset/curly-ligers-lay.md @@ -0,0 +1,5 @@ +--- +'slate-react': minor +--- + +Add `onSelectionChange` and `onValueChange` in Slate React component diff --git a/packages/slate-react/src/components/slate.tsx b/packages/slate-react/src/components/slate.tsx index 2e1d69d730..e7d176385b 100644 --- a/packages/slate-react/src/components/slate.tsx +++ b/packages/slate-react/src/components/slate.tsx @@ -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' @@ -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(() => { if (!Node.isNodeList(initialValue)) { @@ -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) diff --git a/packages/slate-react/src/plugin/with-react.ts b/packages/slate-react/src/plugin/with-react.ts index 21d0ff10b7..395f208ddb 100644 --- a/packages/slate-react/src/plugin/with-react.ts +++ b/packages/slate-react/src/plugin/with-react.ts @@ -363,7 +363,7 @@ export const withReact = ( const onContextChange = EDITOR_TO_ON_CHANGE.get(e) if (onContextChange) { - onContextChange() + onContextChange(options) } onChange(options) diff --git a/packages/slate-react/src/utils/weak-maps.ts b/packages/slate-react/src/utils/weak-maps.ts index acad54176f..92ff09c544 100644 --- a/packages/slate-react/src/utils/weak-maps.ts +++ b/packages/slate-react/src/utils/weak-maps.ts @@ -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' @@ -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 void>() +export const EDITOR_TO_ON_CHANGE = new WeakMap< + Editor, + (options?: { operation?: Operation }) => void +>() /** * Weak maps for saving pending state on composition stage. diff --git a/packages/slate-react/test/index.spec.tsx b/packages/slate-react/test/index.spec.tsx index 3839788360..47de4135b0 100644 --- a/packages/slate-react/test/index.spec.tsx +++ b/packages/slate-react/test/index.spec.tsx @@ -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' @@ -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( + + + , + { 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( + + + , + { 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( + + + , + { 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() + }) })