diff --git a/packages/slate-react/src/components/string.tsx b/packages/slate-react/src/components/string.tsx index 1f7fde5889..67edcaf9f3 100644 --- a/packages/slate-react/src/components/string.tsx +++ b/packages/slate-react/src/components/string.tsx @@ -25,7 +25,7 @@ const String = (props: { // COMPAT: Render text inside void nodes with a zero-width space. // So the node can contain selection but the text is not visible. if (editor.isVoid(parent)) { - return + return } // COMPAT: If this is the last text node in an empty block, render a zero- @@ -37,14 +37,18 @@ const String = (props: { !editor.isInline(parent) && Editor.string(editor, parentPath) === '' ) { - return + return ( + + ) } // COMPAT: If the text is empty, it's because it's on the edge of an inline // node, so we render a zero-width space so that the selection can be // inserted next to it still. if (leaf.text === '') { - return + return ( + + ) } // COMPAT: Browsers will collapse trailing new lines at the end of blocks, @@ -59,14 +63,52 @@ const String = (props: { /** * Leaf strings with text in them. */ -const TextString = (props: { text: string; isTrailing?: boolean }) => { - const { text, isTrailing = false } = props +const TextString = (props: { + text: string + isTrailing?: boolean + zeroWidth?: boolean + length?: number + isLineBreak?: boolean + isMarkPlaceholder?: boolean +}) => { + const { + text, + isTrailing = false, + zeroWidth, + length, + isLineBreak, + isMarkPlaceholder, + } = props const ref = useRef(null) const getTextContent = () => { + if (zeroWidth) { + return '\uFEFF' + } + return `${text ?? ''}${isTrailing ? '\n' : ''}` } const [initialText] = useState(getTextContent) + let attributes: Record = { + 'data-slate-string': true, + } + let children: React.ReactNode = initialText + + if (zeroWidth || isLineBreak) { + attributes = { + 'data-slate-zero-width': isLineBreak ? 'n' : 'z', + 'data-slate-length': length ?? 0, + } + + if (isLineBreak) { + children =
+ } + } + + if (isMarkPlaceholder) { + attributes['data-slate-mark-placeholder'] = true + } + // 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 @@ -77,6 +119,10 @@ const TextString = (props: { text: string; isTrailing?: boolean }) => { // useLayoutEffect: updating our span before browser paint useIsomorphicLayoutEffect(() => { + if (isLineBreak) { + return + } + // 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 = getTextContent() @@ -90,45 +136,23 @@ const TextString = (props: { text: string; isTrailing?: boolean }) => { // We intentionally render a memoized that only receives the initial text content when the component is mounted. // We defer to the layout effect above to update the `textContent` of the span element when needed. - return {initialText} + return ( + + {children} + + ) } const MemoizedText = memo( - forwardRef((props, ref) => { - return ( - - {props.children} - - ) - }) -) - -/** - * Leaf strings without text, render as zero-width strings. - */ - -export const ZeroWidthString = (props: { - length?: number - isLineBreak?: boolean - isMarkPlaceholder?: boolean -}) => { - const { length = 0, isLineBreak = false, isMarkPlaceholder = false } = props - - const attributes = { - 'data-slate-zero-width': isLineBreak ? 'n' : 'z', - 'data-slate-length': length, - } - - if (isMarkPlaceholder) { - attributes['data-slate-mark-placeholder'] = true - } - - return ( - - {!IS_ANDROID || !isLineBreak ? '\uFEFF' : null} - {isLineBreak ?
: null} -
+ forwardRef>( + ({ children, ...props }, ref) => { + return ( + + {children} + + ) + } ) -} +) export default String