diff --git a/packages/sanity/src/desk/comments/src/components/list/CommentsListItem.tsx b/packages/sanity/src/desk/comments/src/components/list/CommentsListItem.tsx index e7ad15b21142..242f518f3acb 100644 --- a/packages/sanity/src/desk/comments/src/components/list/CommentsListItem.tsx +++ b/packages/sanity/src/desk/comments/src/components/list/CommentsListItem.tsx @@ -123,8 +123,6 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm onReply?.(nextComment) setValue(EMPTY_ARRAY) - - replyInputRef.current?.focus() }, [ onReply, parentComment._id, @@ -149,7 +147,9 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm const confirmDiscard = useCallback(() => { replyInputRef.current?.discardDialogController.close() + replyInputRef.current?.reset() setValue(EMPTY_ARRAY) + replyInputRef.current?.focus() }, []) const handleThreadRootClick = useCallback(() => { diff --git a/packages/sanity/src/desk/comments/src/components/list/CreateNewThreadInput.tsx b/packages/sanity/src/desk/comments/src/components/list/CreateNewThreadInput.tsx index be680801be77..5c7ae7888531 100644 --- a/packages/sanity/src/desk/comments/src/components/list/CreateNewThreadInput.tsx +++ b/packages/sanity/src/desk/comments/src/components/list/CreateNewThreadInput.tsx @@ -34,7 +34,6 @@ export function CreateNewThreadInput(props: CreateNewThreadInputProps) { const handleSubmit = useCallback(() => { onNewThreadCreate?.(value) setValue(EMPTY_ARRAY) - commentInputHandle.current?.focus() }, [onNewThreadCreate, value]) const hasValue = useMemo(() => hasCommentMessageValue(value), [value]) @@ -42,7 +41,6 @@ export function CreateNewThreadInput(props: CreateNewThreadInputProps) { const startDiscard = useCallback(() => { if (!hasValue) { onEditDiscard?.() - commentInputHandle.current?.focus() return } @@ -50,15 +48,15 @@ export function CreateNewThreadInput(props: CreateNewThreadInputProps) { }, [hasValue, onEditDiscard]) const confirmDiscard = useCallback(() => { - commentInputHandle.current?.discardDialogController.close() setValue(EMPTY_ARRAY) - onEditDiscard?.() + commentInputHandle.current?.reset() + commentInputHandle.current?.discardDialogController.close() commentInputHandle.current?.focus() + onEditDiscard?.() }, [onEditDiscard]) const cancelDiscard = useCallback(() => { commentInputHandle.current?.discardDialogController.close() - commentInputHandle.current?.focus() }, []) const placeholder = ( diff --git a/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInput.tsx b/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInput.tsx index de78064160d0..844d6e0a1d4d 100644 --- a/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInput.tsx +++ b/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInput.tsx @@ -1,5 +1,5 @@ import React, {forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react' -import {EditorChange, PortableTextEditor} from '@sanity/portable-text-editor' +import {EditorChange, PortableTextEditor, keyGenerator} from '@sanity/portable-text-editor' import {CurrentUser, PortableTextBlock} from '@sanity/types' import FocusLock from 'react-focus-lock' import {Stack} from '@sanity/ui' @@ -47,6 +47,7 @@ export interface CommentInputHandle { discardDialogController: CommentDiscardDialogController focus: () => void scrollTo: () => void + reset: () => void } /** @@ -79,15 +80,26 @@ export const CommentInput = forwardRef( const editorContainerRef = useRef(null) const [showDiscardDialog, setShowDiscardDialog] = useState(false) + // A unique (React) key for the editor instance. + const [editorInstanceKey, setEditorInstanceKey] = useState(keyGenerator()) + + const requestFocus = useCallback(() => { + requestAnimationFrame(() => { + if (!editorRef.current) return + PortableTextEditor.focus(editorRef.current) + }) + }, []) + + const resetEditorInstance = useCallback(() => { + setEditorInstanceKey(keyGenerator()) + }, []) + const handleChange = useCallback( (change: EditorChange) => { // Focus the editor when ready if focusOnMount is true if (change.type === 'ready') { - if (focusOnMount && editorRef.current) { - requestAnimationFrame(() => { - if (!editorRef.current) return - PortableTextEditor.focus(editorRef.current) - }) + if (focusOnMount) { + requestFocus() } } if (change.type === 'focus') { @@ -101,60 +113,57 @@ export const CommentInput = forwardRef( // Update the comment value whenever the comment is edited by the user. if (change.type === 'patch' && editorRef.current) { const editorStateValue = PortableTextEditor.getValue(editorRef.current) - if (editorStateValue) { - onChange(editorStateValue) - } + onChange(editorStateValue || EMPTY_ARRAY) } }, - [focusOnMount, onChange], + [focusOnMount, onChange, requestFocus], ) const scrollToEditor = useCallback(() => { editorContainerRef.current?.scrollIntoView(SCROLL_INTO_VIEW_OPTIONS) }, []) + const handleSubmit = useCallback(() => { + onSubmit() + resetEditorInstance() + requestFocus() + scrollToEditor() + }, [onSubmit, requestFocus, resetEditorInstance, scrollToEditor]) + // The way a user a comment can be discarded varies from the context it is used in. // This controller is used to take care of the main logic of the discard process, while // specific behavior is handled by the consumer. const discardDialogController = useMemo(() => { return { open: () => { - if (editorRef?.current) { - PortableTextEditor.blur(editorRef.current) - } - setShowDiscardDialog(true) }, close: () => { setShowDiscardDialog(false) - - if (editorRef?.current) { - PortableTextEditor.focus(editorRef.current) - } + requestFocus() }, } satisfies CommentDiscardDialogController - }, []) + }, [requestFocus]) useImperativeHandle( ref, () => { return { - focus() { - if (editorRef?.current) { - PortableTextEditor.focus(editorRef.current) - } - }, + focus: requestFocus, blur() { - if (editorRef?.current) { + if (editorRef.current) { PortableTextEditor.blur(editorRef.current) } }, scrollTo: scrollToEditor, + reset: () => { + setEditorInstanceKey(keyGenerator()) + }, discardDialogController, } }, - [discardDialogController, scrollToEditor], + [discardDialogController, requestFocus, scrollToEditor], ) return ( @@ -165,6 +174,7 @@ export const CommentInput = forwardRef( ( onBlur={onBlur} onEscapeKeyDown={onEscapeKeyDown} onFocus={onFocus} - onSubmit={onSubmit} + onSubmit={handleSubmit} placeholder={placeholder} withAvatar={withAvatar} /> diff --git a/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInputInner.tsx b/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInputInner.tsx index 9e3f34f609fa..7ccc344e926f 100644 --- a/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInputInner.tsx +++ b/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInputInner.tsx @@ -150,6 +150,7 @@ export function CommentInputInner(props: CommentInputInnerProps) { focusLock={focusLock} onBlur={onBlur} onFocus={onFocus} + onSubmit={onSubmit} placeholder={placeholder} /> diff --git a/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInputProvider.tsx b/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInputProvider.tsx index cfa65bf039f2..f2e7b4f7c1e1 100644 --- a/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInputProvider.tsx +++ b/packages/sanity/src/desk/comments/src/components/pte/comment-input/CommentInputProvider.tsx @@ -200,10 +200,8 @@ export function CommentInputProvider(props: CommentInputProviderProps) { } } } - - closeMentions() }, - [closeMentions, editor, selectionAtMentionInsert], + [editor, selectionAtMentionInsert], ) const ctxValue = useMemo( diff --git a/packages/sanity/src/desk/comments/src/components/pte/comment-input/Editable.tsx b/packages/sanity/src/desk/comments/src/components/pte/comment-input/Editable.tsx index f1724db8f403..d62cb0a22b2d 100644 --- a/packages/sanity/src/desk/comments/src/components/pte/comment-input/Editable.tsx +++ b/packages/sanity/src/desk/comments/src/components/pte/comment-input/Editable.tsx @@ -53,6 +53,7 @@ interface EditableProps { focusLock?: boolean onBlur?: (e: React.FormEvent) => void onFocus?: (e: React.FormEvent) => void + onSubmit?: () => void placeholder?: React.ReactNode } @@ -61,7 +62,7 @@ export interface EditableHandle { } export function Editable(props: EditableProps) { - const {focusLock, placeholder = 'Create a new comment', onFocus, onBlur} = props + const {focusLock, placeholder = 'Create a new comment', onFocus, onBlur, onSubmit} = props const [popoverElement, setPopoverElement] = useState(null) const rootElementRef = useRef(null) const editableRef = useRef(null) @@ -69,6 +70,7 @@ export function Editable(props: EditableProps) { const selection = usePortableTextEditorSelection() const { + canSubmit, closeMentions, focusEditor, insertMention, @@ -118,10 +120,25 @@ export function Editable(props: EditableProps) { (event: KeyboardEvent) => { switch (event.code) { case 'Enter': + // Shift enter is used to insert a new line, + // keep the default behavior + if (event.shiftKey) { + break + } + // Enter is being used both to select something from the mentionsMenu + // or to submit the comment. Prevent the default behavior. + event.preventDefault() + event.stopPropagation() + + // If the mention menu is open close it, but don't submit. if (mentionsMenuOpen) { - // Stop the event from creating a new block in the editor here - event.preventDefault() - event.stopPropagation() + closeMentions() + break + } + + // Submit the comment if eligible for submission + if (onSubmit && canSubmit) { + onSubmit() } break case 'Escape': @@ -135,7 +152,7 @@ export function Editable(props: EditableProps) { default: } }, - [closeMentions, focusEditor, mentionsMenuOpen], + [canSubmit, closeMentions, focusEditor, mentionsMenuOpen, onSubmit], ) const initialSelectionAtEndOfContent: EditorSelection | undefined = useMemo(() => {