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:
5
.changeset/rare-baboons-camp.md
Normal file
5
.changeset/rare-baboons-camp.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'slate-react': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Delay rendering of placeholder to avoid IME hiding
|
@@ -29,7 +29,6 @@ import { useSlate } from '../hooks/use-slate'
|
|||||||
import { TRIPLE_CLICK } from '../utils/constants'
|
import { TRIPLE_CLICK } from '../utils/constants'
|
||||||
import {
|
import {
|
||||||
DOMElement,
|
DOMElement,
|
||||||
DOMNode,
|
|
||||||
DOMRange,
|
DOMRange,
|
||||||
DOMText,
|
DOMText,
|
||||||
getDefaultView,
|
getDefaultView,
|
||||||
@@ -154,6 +153,9 @@ export const Editable = (props: EditableProps) => {
|
|||||||
const [isComposing, setIsComposing] = useState(false)
|
const [isComposing, setIsComposing] = useState(false)
|
||||||
const ref = useRef<HTMLDivElement | null>(null)
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
const deferredOperations = useRef<DeferredOperation[]>([])
|
const deferredOperations = useRef<DeferredOperation[]>([])
|
||||||
|
const [placeholderHeight, setPlaceholderHeight] = useState<
|
||||||
|
number | undefined
|
||||||
|
>()
|
||||||
|
|
||||||
const { onUserInput, receivedUserInput } = useTrackUserInput()
|
const { onUserInput, receivedUserInput } = useTrackUserInput()
|
||||||
|
|
||||||
@@ -780,17 +782,30 @@ export const Editable = (props: EditableProps) => {
|
|||||||
|
|
||||||
const decorations = decorate([editor, []])
|
const decorations = decorate([editor, []])
|
||||||
|
|
||||||
if (
|
const showPlaceholder =
|
||||||
placeholder &&
|
placeholder &&
|
||||||
editor.children.length === 1 &&
|
editor.children.length === 1 &&
|
||||||
Array.from(Node.texts(editor)).length === 1 &&
|
Array.from(Node.texts(editor)).length === 1 &&
|
||||||
Node.string(editor) === '' &&
|
Node.string(editor) === '' &&
|
||||||
!isComposing
|
!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, [])
|
const start = Editor.start(editor, [])
|
||||||
decorations.push({
|
decorations.push({
|
||||||
[PLACEHOLDER_SYMBOL]: true,
|
[PLACEHOLDER_SYMBOL]: true,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
onPlaceholderResize: placeHolderResizeHandler,
|
||||||
anchor: start,
|
anchor: start,
|
||||||
focus: start,
|
focus: start,
|
||||||
})
|
})
|
||||||
@@ -845,10 +860,6 @@ export const Editable = (props: EditableProps) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const placeholderHeight = EDITOR_TO_PLACEHOLDER_ELEMENT.get(
|
|
||||||
editor
|
|
||||||
)?.getBoundingClientRect()?.height
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReadOnlyContext.Provider value={readOnly}>
|
<ReadOnlyContext.Provider value={readOnly}>
|
||||||
<DecorateContext.Provider value={decorate}>
|
<DecorateContext.Provider value={decorate}>
|
||||||
@@ -1696,7 +1707,7 @@ export type RenderPlaceholderProps = {
|
|||||||
'data-slate-placeholder': boolean
|
'data-slate-placeholder': boolean
|
||||||
dir?: 'rtl'
|
dir?: 'rtl'
|
||||||
contentEditable: boolean
|
contentEditable: boolean
|
||||||
ref: React.RefObject<any>
|
ref: React.RefCallback<any>
|
||||||
style: React.CSSProperties
|
style: React.CSSProperties
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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 { Element, Text } from 'slate'
|
||||||
import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer'
|
import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer'
|
||||||
import String from './string'
|
import String from './string'
|
||||||
@@ -10,10 +16,30 @@ import {
|
|||||||
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
|
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
|
||||||
import { useSlateStatic } from '../hooks/use-slate-static'
|
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.
|
* Individual leaves in a text node with unique formatting.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Leaf = (props: {
|
const Leaf = (props: {
|
||||||
isLast: boolean
|
isLast: boolean
|
||||||
leaf: Text
|
leaf: Text
|
||||||
@@ -31,65 +57,61 @@ const Leaf = (props: {
|
|||||||
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
|
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const lastPlaceholderRef = useRef<HTMLSpanElement | null>(null)
|
|
||||||
const placeholderRef = useRef<HTMLSpanElement | null>(null)
|
|
||||||
const editor = useSlateStatic()
|
const editor = useSlateStatic()
|
||||||
|
|
||||||
const placeholderResizeObserver = useRef<ResizeObserver | null>(null)
|
const placeholderResizeObserver = useRef<ResizeObserver | null>(null)
|
||||||
|
const placeholderRef = useRef<HTMLElement | null>(null)
|
||||||
|
const [showPlaceholder, setShowPlaceholder] = useState(false)
|
||||||
|
const showPlaceholderTimeoutRef = useRef<TimerId>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
const callbackPlaceholderRef = useCallback(
|
||||||
return () => {
|
(placeholderEl: HTMLElement | null) => {
|
||||||
if (placeholderResizeObserver.current) {
|
disconnectPlaceholderResizeObserver(
|
||||||
placeholderResizeObserver.current.disconnect()
|
placeholderResizeObserver,
|
||||||
}
|
placeholderEl == null
|
||||||
}
|
)
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (placeholderEl == null) {
|
||||||
const placeholderEl = placeholderRef?.current
|
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor)
|
||||||
|
leaf.onPlaceholderResize?.(null)
|
||||||
|
} else {
|
||||||
|
EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl)
|
||||||
|
|
||||||
if (placeholderEl) {
|
if (!placeholderResizeObserver.current) {
|
||||||
EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl)
|
// Create a new observer and observe the placeholder element.
|
||||||
} else {
|
const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill
|
||||||
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor)
|
placeholderResizeObserver.current = new ResizeObserver(() => {
|
||||||
}
|
leaf.onPlaceholderResize?.(placeholderEl)
|
||||||
|
})
|
||||||
if (placeholderResizeObserver.current) {
|
}
|
||||||
// Update existing observer.
|
|
||||||
placeholderResizeObserver.current.disconnect()
|
|
||||||
if (placeholderEl)
|
|
||||||
placeholderResizeObserver.current.observe(placeholderEl)
|
placeholderResizeObserver.current.observe(placeholderEl)
|
||||||
} else if (placeholderEl) {
|
placeholderRef.current = placeholderEl
|
||||||
// Create a new observer and observe the placeholder element.
|
}
|
||||||
const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill
|
},
|
||||||
placeholderResizeObserver.current = new ResizeObserver(() => {
|
[placeholderRef, leaf, editor]
|
||||||
// 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])
|
|
||||||
|
|
||||||
let children = (
|
let children = (
|
||||||
<String isLast={isLast} leaf={leaf} parent={parent} text={text} />
|
<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 = {
|
const placeholderProps: RenderPlaceholderProps = {
|
||||||
children: leaf.placeholder,
|
children: leaf.placeholder,
|
||||||
attributes: {
|
attributes: {
|
||||||
@@ -105,7 +127,7 @@ const Leaf = (props: {
|
|||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
},
|
},
|
||||||
contentEditable: false,
|
contentEditable: false,
|
||||||
ref: placeholderRef,
|
ref: callbackPlaceholderRef,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,9 +6,11 @@ declare module 'slate' {
|
|||||||
Editor: ReactEditor
|
Editor: ReactEditor
|
||||||
Text: BaseText & {
|
Text: BaseText & {
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
onPlaceholderResize?: (node: HTMLElement | null) => void
|
||||||
}
|
}
|
||||||
Range: BaseRange & {
|
Range: BaseRange & {
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
onPlaceholderResize?: (node: HTMLElement | null) => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,8 @@ test.describe('placeholder example', () => {
|
|||||||
const slateEditor = page.locator('[data-slate-editor=true]')
|
const slateEditor = page.locator('[data-slate-editor=true]')
|
||||||
const placeholderElement = page.locator('[data-slate-placeholder=true]')
|
const placeholderElement = page.locator('[data-slate-placeholder=true]')
|
||||||
|
|
||||||
|
await expect(placeholderElement).toBeVisible()
|
||||||
|
|
||||||
const editorBoundingBox = await slateEditor.boundingBox()
|
const editorBoundingBox = await slateEditor.boundingBox()
|
||||||
const placeholderBoundingBox = await placeholderElement.boundingBox()
|
const placeholderBoundingBox = await placeholderElement.boundingBox()
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user