From d754e9db7751e2628fc34074663999840c83ab34 Mon Sep 17 00:00:00 2001 From: Ed Hager Date: Wed, 15 Mar 2023 09:25:28 -0500 Subject: [PATCH 1/6] fix: clean up ref handling in leaf --- .../slate-react/src/components/editable.tsx | 2 +- packages/slate-react/src/components/leaf.tsx | 100 +++++++++--------- 2 files changed, 49 insertions(+), 53 deletions(-) diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index dabd025c2a..71e2a26466 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -1696,7 +1696,7 @@ export type RenderPlaceholderProps = { 'data-slate-placeholder': boolean dir?: 'rtl' contentEditable: boolean - ref: React.RefObject + ref: React.Ref style: React.CSSProperties } } diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index 09e74b9ec2..623723f58d 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect } from 'react' +import React, { useRef, useCallback, MutableRefObject } from 'react' import { Element, Text } from 'slate' import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer' import String from './string' @@ -10,10 +10,20 @@ import { import { RenderLeafProps, RenderPlaceholderProps } from './editable' import { useSlateStatic } from '../hooks/use-slate-static' +function disconnectPlaceholderResizeObserver( + placeholderResizeObserver: MutableRefObject, + releaseObserver: boolean +) { + if (placeholderResizeObserver.current) { + placeholderResizeObserver.current.disconnect() + if (releaseObserver) { + placeholderResizeObserver.current = null + } + } +} /** * Individual leaves in a text node with unique formatting. */ - const Leaf = (props: { isLast: boolean leaf: Text @@ -31,59 +41,45 @@ const Leaf = (props: { renderLeaf = (props: RenderLeafProps) => , } = props - const lastPlaceholderRef = useRef(null) - const placeholderRef = useRef(null) const editor = useSlateStatic() - const placeholderResizeObserver = useRef(null) - - useEffect(() => { - return () => { - if (placeholderResizeObserver.current) { - placeholderResizeObserver.current.disconnect() - } - } - }, []) - - useEffect(() => { - const placeholderEl = placeholderRef?.current - - if (placeholderEl) { - EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl) - } else { - EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) - } - - if (placeholderResizeObserver.current) { - // Update existing observer. - placeholderResizeObserver.current.disconnect() - if (placeholderEl) + const placeholderRef = useRef(null) + + const callbackPlaceholderRef = useCallback( + (placeholderEl: HTMLElement | null) => { + disconnectPlaceholderResizeObserver( + placeholderResizeObserver, + placeholderEl == null + ) + + if (placeholderEl == null) { + EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) + + if (placeholderRef.current) { + // Force a re-render of the editor so its min-height can be reset. + const forceRender = EDITOR_TO_FORCE_RENDER.get(editor) + forceRender?.() + } + } else { + EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl) + + if (!placeholderResizeObserver.current) { + // Create a new observer and observe the placeholder element. + const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill + placeholderResizeObserver.current = new ResizeObserver(() => { + // Force a re-render of the editor so its min-height can be updated + // to the new height of the placeholder. + const forceRender = EDITOR_TO_FORCE_RENDER.get(editor) + forceRender?.() + }) + } placeholderResizeObserver.current.observe(placeholderEl) - } else if (placeholderEl) { - // Create a new observer and observe the placeholder element. - const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill - placeholderResizeObserver.current = new ResizeObserver(() => { - // Force a re-render of the editor so its min-height can be updated - // to the new height of the placeholder. - const forceRender = EDITOR_TO_FORCE_RENDER.get(editor) - forceRender?.() - }) - placeholderResizeObserver.current.observe(placeholderEl) - } - - if (!placeholderEl && lastPlaceholderRef.current) { - // No placeholder element, so no need for a resize observer. - // Force a re-render of the editor so its min-height can be reset. - const forceRender = EDITOR_TO_FORCE_RENDER.get(editor) - forceRender?.() - } - lastPlaceholderRef.current = placeholderRef.current - - return () => { - EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) - } - }, [placeholderRef, leaf, editor]) + placeholderRef.current = placeholderEl + } + }, + [placeholderRef, editor] + ) let children = ( @@ -105,7 +101,7 @@ const Leaf = (props: { textDecoration: 'none', }, contentEditable: false, - ref: placeholderRef, + ref: callbackPlaceholderRef, }, } From 11336b22c851f2f8ceef5eb2e8c47c903a38a350 Mon Sep 17 00:00:00 2001 From: Ed Hager Date: Thu, 16 Mar 2023 18:33:27 -0500 Subject: [PATCH 2/6] fix: delay rendering of placeholder to allow selections to settle --- packages/slate-react/src/components/leaf.tsx | 41 ++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index 623723f58d..4e0a9ef9c3 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -1,4 +1,10 @@ -import React, { useRef, useCallback, MutableRefObject } from 'react' +import React, { + useRef, + useCallback, + MutableRefObject, + useState, + useEffect, +} from 'react' import { Element, Text } from 'slate' import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer' import String from './string' @@ -21,6 +27,16 @@ function disconnectPlaceholderResizeObserver( } } } + +type TimerId = ReturnType | null + +function clearTimeoutRef(timeoutRef: MutableRefObject) { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } +} + /** * Individual leaves in a text node with unique formatting. */ @@ -44,6 +60,8 @@ const Leaf = (props: { const editor = useSlateStatic() const placeholderResizeObserver = useRef(null) const placeholderRef = useRef(null) + const [showPlaceholder, setShowPlaceholder] = useState(false) + const showPlaceholderTimeoutRef = useRef(null) const callbackPlaceholderRef = useCallback( (placeholderEl: HTMLElement | null) => { @@ -85,7 +103,24 @@ const Leaf = (props: { ) - if (leaf[PLACEHOLDER_SYMBOL]) { + const leafIsPlaceholder = leaf[PLACEHOLDER_SYMBOL] + useEffect(() => { + if (leafIsPlaceholder) { + if (!showPlaceholderTimeoutRef.current) { + // Delay the placeholder so it will not render in a selection + showPlaceholderTimeoutRef.current = setTimeout(() => { + setShowPlaceholder(true) + showPlaceholderTimeoutRef.current = null + }, 30) + } + } else { + clearTimeoutRef(showPlaceholderTimeoutRef) + setShowPlaceholder(false) + } + return () => clearTimeoutRef(showPlaceholderTimeoutRef) + }, [leafIsPlaceholder, setShowPlaceholder]) + + if (leafIsPlaceholder) { const placeholderProps: RenderPlaceholderProps = { children: leaf.placeholder, attributes: { @@ -95,7 +130,7 @@ const Leaf = (props: { pointerEvents: 'none', width: '100%', maxWidth: '100%', - display: 'block', + display: showPlaceholder ? 'block' : 'none', opacity: '0.333', userSelect: 'none', textDecoration: 'none', From 97a9af3b2eef2a7227de8c75f1cf534491197b5c Mon Sep 17 00:00:00 2001 From: Ed Hager Date: Fri, 17 Mar 2023 09:18:52 -0500 Subject: [PATCH 3/6] fix: move placeholder height calculation into layout effect --- .../slate-react/src/components/editable.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 71e2a26466..307c1ebcc5 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -154,6 +154,9 @@ export const Editable = (props: EditableProps) => { const [isComposing, setIsComposing] = useState(false) const ref = useRef(null) const deferredOperations = useRef([]) + const [placeholderHeight, setPlaceholderHeight] = useState< + number | undefined + >() const { onUserInput, receivedUserInput } = useTrackUserInput() @@ -780,13 +783,13 @@ export const Editable = (props: EditableProps) => { const decorations = decorate([editor, []]) - if ( + const showPlaceholder = placeholder && editor.children.length === 1 && Array.from(Node.texts(editor)).length === 1 && Node.string(editor) === '' && !isComposing - ) { + if (showPlaceholder) { const start = Editor.start(editor, []) decorations.push({ [PLACEHOLDER_SYMBOL]: true, @@ -796,6 +799,15 @@ export const Editable = (props: EditableProps) => { }) } + useIsomorphicLayoutEffect(() => { + const placeholderEl = EDITOR_TO_PLACEHOLDER_ELEMENT.get(editor) + if (placeholderEl && showPlaceholder) { + setPlaceholderHeight(placeholderEl.getBoundingClientRect()?.height) + } else { + setPlaceholderHeight(undefined) + } + }, [showPlaceholder]) + const { marks } = editor state.hasMarkPlaceholder = false @@ -845,10 +857,6 @@ export const Editable = (props: EditableProps) => { }) }) - const placeholderHeight = EDITOR_TO_PLACEHOLDER_ELEMENT.get( - editor - )?.getBoundingClientRect()?.height - return ( From e0ebd224f5dceb82a5607ce533395e757c87288e Mon Sep 17 00:00:00 2001 From: Ed Hager Date: Fri, 17 Mar 2023 09:38:33 -0500 Subject: [PATCH 4/6] fix: add change set --- .changeset/rare-baboons-camp.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rare-baboons-camp.md diff --git a/.changeset/rare-baboons-camp.md b/.changeset/rare-baboons-camp.md new file mode 100644 index 0000000000..7ef7698942 --- /dev/null +++ b/.changeset/rare-baboons-camp.md @@ -0,0 +1,5 @@ +--- +'slate-react': patch +--- + +Delay rendering of placeholder to avoid IME hiding From e9e779dce3a437f960b5b8d31b7d712bcf974746 Mon Sep 17 00:00:00 2001 From: Ed Hager Date: Mon, 20 Mar 2023 09:36:40 -0500 Subject: [PATCH 5/6] fix: handle placeholder resizing in a better way --- .../slate-react/src/components/editable.tsx | 25 +++++++++++-------- packages/slate-react/src/components/leaf.tsx | 19 ++++---------- packages/slate-react/src/custom-types.ts | 2 ++ 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 307c1ebcc5..334189714c 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -29,7 +29,6 @@ import { useSlate } from '../hooks/use-slate' import { TRIPLE_CLICK } from '../utils/constants' import { DOMElement, - DOMNode, DOMRange, DOMText, getDefaultView, @@ -789,25 +788,29 @@ export const Editable = (props: EditableProps) => { Array.from(Node.texts(editor)).length === 1 && Node.string(editor) === '' && !isComposing + + const placeHolderResizeHandler = useCallback( + (placeholderEl: HTMLElement | null) => { + if (placeholderEl && showPlaceholder) { + setPlaceholderHeight(placeholderEl.getBoundingClientRect()?.height) + } else { + setPlaceholderHeight(undefined) + } + }, + [showPlaceholder] + ) + if (showPlaceholder) { const start = Editor.start(editor, []) decorations.push({ [PLACEHOLDER_SYMBOL]: true, placeholder, + onPlaceholderResize: placeHolderResizeHandler, anchor: start, focus: start, }) } - useIsomorphicLayoutEffect(() => { - const placeholderEl = EDITOR_TO_PLACEHOLDER_ELEMENT.get(editor) - if (placeholderEl && showPlaceholder) { - setPlaceholderHeight(placeholderEl.getBoundingClientRect()?.height) - } else { - setPlaceholderHeight(undefined) - } - }, [showPlaceholder]) - const { marks } = editor state.hasMarkPlaceholder = false @@ -1704,7 +1707,7 @@ export type RenderPlaceholderProps = { 'data-slate-placeholder': boolean dir?: 'rtl' contentEditable: boolean - ref: React.Ref + ref: React.RefCallback style: React.CSSProperties } } diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index 4e0a9ef9c3..4e759471b2 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -72,12 +72,7 @@ const Leaf = (props: { if (placeholderEl == null) { EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) - - if (placeholderRef.current) { - // Force a re-render of the editor so its min-height can be reset. - const forceRender = EDITOR_TO_FORCE_RENDER.get(editor) - forceRender?.() - } + leaf.onPlaceholderResize?.(null) } else { EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl) @@ -85,14 +80,10 @@ const Leaf = (props: { // Create a new observer and observe the placeholder element. const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill placeholderResizeObserver.current = new ResizeObserver(() => { - // Force a re-render of the editor so its min-height can be updated - // to the new height of the placeholder. - const forceRender = EDITOR_TO_FORCE_RENDER.get(editor) - forceRender?.() + leaf.onPlaceholderResize?.(placeholderEl) }) } placeholderResizeObserver.current.observe(placeholderEl) - placeholderRef.current = placeholderEl } }, @@ -111,7 +102,7 @@ const Leaf = (props: { showPlaceholderTimeoutRef.current = setTimeout(() => { setShowPlaceholder(true) showPlaceholderTimeoutRef.current = null - }, 30) + }, 300) } } else { clearTimeoutRef(showPlaceholderTimeoutRef) @@ -120,7 +111,7 @@ const Leaf = (props: { return () => clearTimeoutRef(showPlaceholderTimeoutRef) }, [leafIsPlaceholder, setShowPlaceholder]) - if (leafIsPlaceholder) { + if (leafIsPlaceholder && showPlaceholder) { const placeholderProps: RenderPlaceholderProps = { children: leaf.placeholder, attributes: { @@ -130,7 +121,7 @@ const Leaf = (props: { pointerEvents: 'none', width: '100%', maxWidth: '100%', - display: showPlaceholder ? 'block' : 'none', + display: 'block', opacity: '0.333', userSelect: 'none', textDecoration: 'none', diff --git a/packages/slate-react/src/custom-types.ts b/packages/slate-react/src/custom-types.ts index 9c91b64c1d..b56188a8fc 100644 --- a/packages/slate-react/src/custom-types.ts +++ b/packages/slate-react/src/custom-types.ts @@ -6,9 +6,11 @@ declare module 'slate' { Editor: ReactEditor Text: BaseText & { placeholder?: string + onPlaceholderResize?: (node: HTMLElement | null) => void } Range: BaseRange & { placeholder?: string + onPlaceholderResize?: (node: HTMLElement | null) => void } } } From 577ff45c8d5a280c01478d2096ac18e1e4fc3a64 Mon Sep 17 00:00:00 2001 From: Ed Hager Date: Mon, 20 Mar 2023 10:05:42 -0500 Subject: [PATCH 6/6] fix: fix placeholder integration test --- packages/slate-react/src/components/leaf.tsx | 2 +- playwright/integration/examples/placeholder.test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index 4e759471b2..1dedcbe201 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -87,7 +87,7 @@ const Leaf = (props: { placeholderRef.current = placeholderEl } }, - [placeholderRef, editor] + [placeholderRef, leaf, editor] ) let children = ( diff --git a/playwright/integration/examples/placeholder.test.ts b/playwright/integration/examples/placeholder.test.ts index ede5de359b..b8f5654f1f 100644 --- a/playwright/integration/examples/placeholder.test.ts +++ b/playwright/integration/examples/placeholder.test.ts @@ -19,6 +19,8 @@ test.describe('placeholder example', () => { const slateEditor = page.locator('[data-slate-editor=true]') const placeholderElement = page.locator('[data-slate-placeholder=true]') + await expect(placeholderElement).toBeVisible() + const editorBoundingBox = await slateEditor.boundingBox() const placeholderBoundingBox = await placeholderElement.boundingBox()