mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-22 23:12:52 +02:00
Optimize TextString rendering to support browser/OS text features, eg fix native spellcheck (#4733)
* slate-react: use a layout effect to render leaf text nodes instead of via virtual DOM, which implements diffing with real DOM avoiding interference with native TextNode behaviors for example spellcheck * lint * clarify and simplify extreme case of null text in TextString rendering * code style: use string interpolation in TextString Co-authored-by: Nemanja Tosic <netosic90@gmail.com> Co-authored-by: Peter Sipos <schipy@craft.do> Co-authored-by: Nemanja Tosic <netosic90@gmail.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useRef } from 'react'
|
||||
import React, { useRef, useLayoutEffect } from 'react'
|
||||
import { Editor, Text, Path, Element, Node } from 'slate'
|
||||
|
||||
import { ReactEditor, useSlateStatic } from '..'
|
||||
@@ -59,21 +59,30 @@ const TextString = (props: { text: string; isTrailing?: boolean }) => {
|
||||
const { text, isTrailing = false } = props
|
||||
|
||||
const ref = useRef<HTMLSpanElement>(null)
|
||||
const forceUpdateCount = useRef(0)
|
||||
|
||||
if (ref.current && ref.current.textContent !== text) {
|
||||
forceUpdateCount.current += 1
|
||||
}
|
||||
// This is the actual text rendering boundary where we interface with the DOM
|
||||
// The text is not rendered as part of the virtual DOM, as since we handle basic character insertions natively,
|
||||
// updating the DOM is not a one way dataflow anymore. What we need here is not reconciliation and diffing
|
||||
// with previous version of the virtual DOM, but rather diffing with the actual DOM element, and replace the DOM <span> content
|
||||
// exactly if and only if its current content does not match our current virtual DOM.
|
||||
// Otherwise the DOM TextNode would always be replaced by React as the user types, which interferes with native text features,
|
||||
// eg makes native spellcheck opt out from checking the text node.
|
||||
|
||||
// This component may have skipped rendering due to native operations being
|
||||
// applied. If an undo is performed React will see the old and new shadow DOM
|
||||
// match and not apply an update. Forces each render to actually reconcile.
|
||||
return (
|
||||
<span data-slate-string ref={ref} key={forceUpdateCount.current}>
|
||||
{text}
|
||||
{isTrailing ? '\n' : null}
|
||||
</span>
|
||||
)
|
||||
// useLayoutEffect: updating our span before browser paint
|
||||
useLayoutEffect(() => {
|
||||
// null coalescing text to make sure we're not outputing "null" as a string in the extreme case it is nullish at runtime
|
||||
const textWithTrailing = `${text ?? ''}${isTrailing ? '\n' : ''}`
|
||||
|
||||
if (ref.current && ref.current.textContent !== textWithTrailing) {
|
||||
ref.current.textContent = textWithTrailing
|
||||
}
|
||||
|
||||
// intentionally not specifying dependencies, so that this effect runs on every render
|
||||
// as this effectively replaces "specifying the text in the virtual DOM under the <span> below" on each render
|
||||
})
|
||||
|
||||
// the span is intentionally same on every render in virtual DOM, actual rendering happens in the layout effect above
|
||||
return <span data-slate-string ref={ref} />
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user