From f544edee4f42706341a5530f9f3e79b92e1d7517 Mon Sep 17 00:00:00 2001 From: Robin Pyon Date: Fri, 15 Sep 2023 10:43:17 +0100 Subject: [PATCH] fix(comments): only calculate current caret element on demand (#4935) --- .../comment-input/CommentInputProvider.tsx | 13 ++- .../components/pte/comment-input/Editable.tsx | 11 +-- .../pte/comment-input/getCaretElement.ts | 29 +++++++ .../pte/comment-input/useCursorElement.ts | 86 ------------------- 4 files changed, 43 insertions(+), 96 deletions(-) create mode 100644 packages/sanity/src/core/comments/components/pte/comment-input/getCaretElement.ts delete mode 100644 packages/sanity/src/core/comments/components/pte/comment-input/useCursorElement.ts diff --git a/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputProvider.tsx b/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputProvider.tsx index aa2f715bcba..cb14124ed13 100644 --- a/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputProvider.tsx +++ b/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputProvider.tsx @@ -4,16 +4,18 @@ import { PortableTextEditor, usePortableTextEditor, } from '@sanity/portable-text-editor' -import React, {FormEventHandler, useCallback, useMemo, useState} from 'react' +import React, {FormEventHandler, startTransition, useCallback, useMemo, useState} from 'react' import {Path, isPortableTextSpan, isPortableTextTextBlock} from '@sanity/types' import {CommentMessage} from '../../../types' import {useDidUpdate} from '../../../../form' import {useCommentHasChanged} from '../../../helpers' import {MentionOptionsHookValue} from '../../../hooks' +import {getCaretElement} from './getCaretElement' type FIXME = any export interface CommentInputContextValue { + activeCaretElement?: HTMLElement | null canSubmit?: boolean closeMentions: () => void editor: PortableTextEditor @@ -58,6 +60,7 @@ export function CommentInputProvider(props: CommentInputProviderProps) { const editor = usePortableTextEditor() + const [activeCaretElement, setActiveCaretElement] = useState(null) const [mentionsMenuOpen, setMentionsMenuOpen] = useState(false) const [selectionAtMentionInsert, setSelectionAtMentionInsert] = useState(null) @@ -100,7 +103,10 @@ export function CommentInputProvider(props: CommentInputProviderProps) { const onBeforeInput = useCallback( (event: FIXME): void => { if (event.inputType === 'insertText' && event.data === '@') { - setMentionsMenuOpen(true) + const element = getCaretElement(event.target) + setActiveCaretElement(element) + startTransition(() => setMentionsMenuOpen(true)) + setSelectionAtMentionInsert(PortableTextEditor.getSelection(editor)) } }, @@ -109,6 +115,7 @@ export function CommentInputProvider(props: CommentInputProviderProps) { const closeMentions = useCallback(() => { if (!mentionsMenuOpen) return + setActiveCaretElement(null) setMentionsMenuOpen(false) focusEditor() setSelectionAtMentionInsert(null) @@ -181,6 +188,7 @@ export function CommentInputProvider(props: CommentInputProviderProps) { const ctxValue = useMemo( () => ({ + activeCaretElement, canSubmit, closeMentions, editor, @@ -198,6 +206,7 @@ export function CommentInputProvider(props: CommentInputProviderProps) { mentionOptions, }) satisfies CommentInputContextValue, [ + activeCaretElement, canSubmit, closeMentions, editor, diff --git a/packages/sanity/src/core/comments/components/pte/comment-input/Editable.tsx b/packages/sanity/src/core/comments/components/pte/comment-input/Editable.tsx index f042b4fe0ea..0458dc6dbc9 100644 --- a/packages/sanity/src/core/comments/components/pte/comment-input/Editable.tsx +++ b/packages/sanity/src/core/comments/components/pte/comment-input/Editable.tsx @@ -6,7 +6,6 @@ import {MentionsMenu} from '../../mentions' import {useDidUpdate} from '../../../../form' import {renderBlock, renderChild} from '../render' import {useCommentInput} from './useCommentInput' -import {useCursorElement} from './useCursorElement' const INLINE_STYLE: React.CSSProperties = {outline: 'none'} @@ -53,6 +52,7 @@ export function Editable(props: EditableProps) { const editableRef = useRef(null) const { + activeCaretElement, closeMentions, expanded, focusEditor, @@ -63,11 +63,6 @@ export function Editable(props: EditableProps) { onBeforeInput, } = useCommentInput() - const cursorElement = useCursorElement({ - disabled: mentionsMenuOpen, - rootElement: editableRef.current, - }) - const renderPlaceholder = useCallback(() => {placeholder}, [placeholder]) useGlobalKeyDown( @@ -100,7 +95,7 @@ export function Editable(props: EditableProps) { diff --git a/packages/sanity/src/core/comments/components/pte/comment-input/getCaretElement.ts b/packages/sanity/src/core/comments/components/pte/comment-input/getCaretElement.ts new file mode 100644 index 00000000000..de6179f5987 --- /dev/null +++ b/packages/sanity/src/core/comments/components/pte/comment-input/getCaretElement.ts @@ -0,0 +1,29 @@ +export function getCaretElement(rootElement: HTMLElement | null): HTMLElement | null { + const selection = window.getSelection() + + if (!selection || selection.type !== 'Caret') return null + + const selectionRange = selection.getRangeAt(0) + const isWithinRoot = rootElement?.contains(selectionRange.commonAncestorContainer) + + if (!isWithinRoot) return null + + const {anchorNode, focusNode} = selection + + if (rootElement?.contains(anchorNode) && anchorNode === focusNode) { + try { + const range = window.getSelection()?.getRangeAt(0) + const rect = range?.getBoundingClientRect() + if (rect) { + const element = { + getBoundingClientRect: () => rect, + } as HTMLElement + return element + } + } catch (_) { + return null + } + } + + return null +} diff --git a/packages/sanity/src/core/comments/components/pte/comment-input/useCursorElement.ts b/packages/sanity/src/core/comments/components/pte/comment-input/useCursorElement.ts deleted file mode 100644 index 8999c93f380..00000000000 --- a/packages/sanity/src/core/comments/components/pte/comment-input/useCursorElement.ts +++ /dev/null @@ -1,86 +0,0 @@ -import {useState, useMemo, useEffect, useCallback} from 'react' - -const EVENT_LISTENER_OPTIONS: AddEventListenerOptions = {passive: true} - -interface CursorElementHookOptions { - disabled: boolean - rootElement: HTMLElement | null -} - -export function useCursorElement(opts: CursorElementHookOptions): HTMLElement | null { - const {disabled, rootElement} = opts - const [cursorRect, setCursorRect] = useState(null) - const [selection, setSelection] = useState<{ - anchorNode: Node | null - anchorOffset: number - focusNode: Node | null - focusOffset: number - } | null>(null) - - const cursorElement = useMemo(() => { - if (!cursorRect) { - return null - } - return { - getBoundingClientRect: () => { - return cursorRect - }, - } as HTMLElement - }, [cursorRect]) - - const handleSelectionChange = useCallback(() => { - if (disabled) { - setSelection(null) - return - } - - const sel = window.getSelection() - - if (!sel || !sel.isCollapsed) return - - const range = sel.getRangeAt(0) - const isWithinRoot = rootElement?.contains(range.commonAncestorContainer) - - if (!isWithinRoot) return - - const {anchorNode, anchorOffset, focusNode, focusOffset} = sel - setSelection({ - anchorNode, - anchorOffset, - focusNode, - focusOffset, - }) - }, [disabled, rootElement]) - - useEffect(() => { - if (!selection || disabled) { - setCursorRect(null) - return - } - - const {anchorNode, focusNode} = selection - - if (rootElement?.contains(anchorNode) && anchorNode === focusNode) { - try { - const range = window.getSelection()?.getRangeAt(0) - const rect = range?.getBoundingClientRect() - - if (rect) { - setCursorRect(rect) - } - } catch (_) { - setCursorRect(null) - } - } - }, [disabled, rootElement, selection]) - - useEffect(() => { - document.addEventListener('selectionchange', handleSelectionChange, EVENT_LISTENER_OPTIONS) - - return () => { - document.removeEventListener('selectionchange', handleSelectionChange) - } - }, [handleSelectionChange]) - - return cursorElement -}