Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add onSelectionChange and onValueChange in Slate React component #5526

Merged
merged 5 commits into from
Oct 20, 2023
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/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()
})
jkcs marked this conversation as resolved.
Show resolved Hide resolved

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()
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add

expect(onChange).toHaveBeenCalled()
expect(onSelectionChange).toHaveBeenCalled()

And another test using Transforms.setNodes that should not change selection?

Copy link
Contributor Author

@jkcs jkcs Oct 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a unit test case using Transforms.setNodes to set the text as bold. Is that okay?

})
Loading