1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-20 06:01:24 +02:00

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
This commit is contained in:
Ed Hager
2023-03-20 11:23:54 -05:00
committed by GitHub
parent 3c0abeb785
commit 5a0d3974d6
5 changed files with 101 additions and 59 deletions

View File

@@ -0,0 +1,5 @@
---
'slate-react': patch
---
Delay rendering of placeholder to avoid IME hiding

View File

@@ -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<HTMLDivElement | null>(null)
const deferredOperations = useRef<DeferredOperation[]>([])
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 (
<ReadOnlyContext.Provider value={readOnly}>
<DecorateContext.Provider value={decorate}>
@@ -1696,7 +1707,7 @@ export type RenderPlaceholderProps = {
'data-slate-placeholder': boolean
dir?: 'rtl'
contentEditable: boolean
ref: React.RefObject<any>
ref: React.RefCallback<any>
style: React.CSSProperties
}
}

View File

@@ -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<ResizeObserver | null>,
releaseObserver: boolean
) {
if (placeholderResizeObserver.current) {
placeholderResizeObserver.current.disconnect()
if (releaseObserver) {
placeholderResizeObserver.current = null
}
}
}
type TimerId = ReturnType<typeof setTimeout> | null
function clearTimeoutRef(timeoutRef: MutableRefObject<TimerId>) {
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) => <DefaultLeaf {...props} />,
} = props
const lastPlaceholderRef = useRef<HTMLSpanElement | null>(null)
const placeholderRef = useRef<HTMLSpanElement | null>(null)
const editor = useSlateStatic()
const placeholderResizeObserver = useRef<ResizeObserver | null>(null)
const placeholderRef = useRef<HTMLElement | null>(null)
const [showPlaceholder, setShowPlaceholder] = useState(false)
const showPlaceholderTimeoutRef = useRef<TimerId>(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) {
EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl)
} else {
if (placeholderEl == null) {
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor)
}
leaf.onPlaceholderResize?.(null)
} else {
EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl)
if (placeholderResizeObserver.current) {
// Update existing observer.
placeholderResizeObserver.current.disconnect()
if (placeholderEl)
placeholderResizeObserver.current.observe(placeholderEl)
} else if (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?.()
leaf.onPlaceholderResize?.(placeholderEl)
})
}
placeholderResizeObserver.current.observe(placeholderEl)
placeholderRef.current = 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, leaf, editor]
)
let children = (
<String isLast={isLast} leaf={leaf} parent={parent} text={text} />
)
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,
},
}

View File

@@ -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
}
}
}

View File

@@ -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()