From 5a0d3974d6cb2c099dff4c0976e9390d24c345ad Mon Sep 17 00:00:00 2001 From: Ed Hager Date: Mon, 20 Mar 2023 11:23:54 -0500 Subject: [PATCH] Fix/delete all closes keyboard (#5368) * fix: clean up ref handling in leaf * fix: delay rendering of placeholder to allow selections to settle * fix: move placeholder height calculation into layout effect * fix: add change set * fix: handle placeholder resizing in a better way * fix: fix placeholder integration test --- .changeset/rare-baboons-camp.md | 5 + .../slate-react/src/components/editable.tsx | 27 ++-- packages/slate-react/src/components/leaf.tsx | 124 +++++++++++------- packages/slate-react/src/custom-types.ts | 2 + .../integration/examples/placeholder.test.ts | 2 + 5 files changed, 101 insertions(+), 59 deletions(-) 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 000000000..7ef769894 --- /dev/null +++ b/.changeset/rare-baboons-camp.md @@ -0,0 +1,5 @@ +--- +'slate-react': patch +--- + +Delay rendering of placeholder to avoid IME hiding diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index dabd025c2..334189714 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, @@ -154,6 +153,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,17 +782,30 @@ 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 - ) { + + 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, }) @@ -845,10 +860,6 @@ export const Editable = (props: EditableProps) => { }) }) - const placeholderHeight = EDITOR_TO_PLACEHOLDER_ELEMENT.get( - editor - )?.getBoundingClientRect()?.height - return ( @@ -1696,7 +1707,7 @@ export type RenderPlaceholderProps = { 'data-slate-placeholder': boolean dir?: 'rtl' contentEditable: boolean - ref: React.RefObject + 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 09e74b9ec..1dedcbe20 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -1,4 +1,10 @@ -import React, { useRef, useEffect } 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' @@ -10,10 +16,30 @@ 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 + } + } +} + +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. */ - const Leaf = (props: { isLast: boolean leaf: Text @@ -31,65 +57,61 @@ const Leaf = (props: { renderLeaf = (props: RenderLeafProps) => , } = props - const lastPlaceholderRef = useRef(null) - const placeholderRef = useRef(null) const editor = useSlateStatic() - const placeholderResizeObserver = useRef(null) + const placeholderRef = useRef(null) + const [showPlaceholder, setShowPlaceholder] = useState(false) + const showPlaceholderTimeoutRef = useRef(null) - useEffect(() => { - return () => { - if (placeholderResizeObserver.current) { - placeholderResizeObserver.current.disconnect() - } - } - }, []) + const callbackPlaceholderRef = useCallback( + (placeholderEl: HTMLElement | null) => { + disconnectPlaceholderResizeObserver( + placeholderResizeObserver, + placeholderEl == null + ) - useEffect(() => { - const placeholderEl = placeholderRef?.current + if (placeholderEl == null) { + EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) + leaf.onPlaceholderResize?.(null) + } else { + EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl) - 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) + if (!placeholderResizeObserver.current) { + // Create a new observer and observe the placeholder element. + const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill + placeholderResizeObserver.current = new ResizeObserver(() => { + leaf.onPlaceholderResize?.(placeholderEl) + }) + } 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, leaf, editor] + ) let children = ( ) - 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 + }, 300) + } + } else { + clearTimeoutRef(showPlaceholderTimeoutRef) + setShowPlaceholder(false) + } + return () => clearTimeoutRef(showPlaceholderTimeoutRef) + }, [leafIsPlaceholder, setShowPlaceholder]) + + if (leafIsPlaceholder && showPlaceholder) { const placeholderProps: RenderPlaceholderProps = { children: leaf.placeholder, attributes: { @@ -105,7 +127,7 @@ const Leaf = (props: { textDecoration: 'none', }, contentEditable: false, - ref: placeholderRef, + ref: callbackPlaceholderRef, }, } diff --git a/packages/slate-react/src/custom-types.ts b/packages/slate-react/src/custom-types.ts index 9c91b64c1..b56188a8f 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 } } } diff --git a/playwright/integration/examples/placeholder.test.ts b/playwright/integration/examples/placeholder.test.ts index ede5de359..b8f5654f1 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()