diff --git a/packages/sanity/src/desk/comments/plugin/field/CommentFieldButton.tsx b/packages/sanity/src/desk/comments/plugin/field/CommentFieldButton.tsx index 078d3e66cd6..74b078d546f 100644 --- a/packages/sanity/src/desk/comments/plugin/field/CommentFieldButton.tsx +++ b/packages/sanity/src/desk/comments/plugin/field/CommentFieldButton.tsx @@ -58,6 +58,7 @@ interface CommentFieldButtonProps { onClick?: () => void onCommentAdd: () => void onDiscard: () => void + onInputKeyDown?: (event: React.KeyboardEvent) => void open: boolean setOpen: (open: boolean) => void value: CommentMessage @@ -74,11 +75,11 @@ export function CommentFieldButton(props: CommentFieldButtonProps) { onClick, onCommentAdd, onDiscard, + onInputKeyDown, open, setOpen, value, } = props - const [mentionMenuOpen, setMentionMenuOpen] = useState(false) const [popoverElement, setPopoverElement] = useState(null) const commentInputHandle = useRef(null) const hasComments = Boolean(count > 0) @@ -93,15 +94,31 @@ export function CommentFieldButton(props: CommentFieldButtonProps) { const hasValue = useMemo(() => hasCommentMessageValue(value), [value]) const startDiscard = useCallback(() => { - if (mentionMenuOpen) return - if (!hasValue) { closePopover() return } commentInputHandle.current?.discardDialogController.open() - }, [closePopover, hasValue, mentionMenuOpen]) + }, [closePopover, hasValue]) + + const handleInputKeyDown = useCallback( + (event: React.KeyboardEvent) => { + // Don't act if the input already prevented this event + if (event.isDefaultPrevented()) { + return + } + // Discard the input text + if (event.key === 'Escape') { + event.preventDefault() + event.stopPropagation() + startDiscard() + } + // Call parent handler + if (onInputKeyDown) onInputKeyDown(event) + }, + [onInputKeyDown, startDiscard], + ) const handleDiscardCancel = useCallback(() => { commentInputHandle.current?.discardDialogController.close() @@ -132,8 +149,7 @@ export function CommentFieldButton(props: CommentFieldButtonProps) { onChange={onChange} onDiscardCancel={handleDiscardCancel} onDiscardConfirm={handleDiscardConfirm} - onEscapeKeyDown={startDiscard} - onMentionMenuOpenChange={setMentionMenuOpen} + onKeyDown={handleInputKeyDown} onSubmit={handleSubmit} placeholder={placeholder} readOnly={isRunningSetup} 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 8f78bafd829..5a40d7f9416 100644 --- a/packages/sanity/src/desk/comments/src/components/list/CommentsListItem.tsx +++ b/packages/sanity/src/desk/comments/src/components/list/CommentsListItem.tsx @@ -77,6 +77,7 @@ interface CommentsListItemProps { onCreateRetry: (id: string) => void onDelete: (id: string) => void onEdit: (id: string, payload: CommentEditPayload) => void + onKeyDown?: (event: React.KeyboardEvent) => void onPathSelect?: (path: Path) => void onReply: (payload: CommentCreatePayload) => void onStatusChange?: (id: string, status: CommentStatus) => void @@ -95,6 +96,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm onCreateRetry, onDelete, onEdit, + onKeyDown, onPathSelect, onReply, onStatusChange, @@ -141,14 +143,37 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm replyInputRef.current?.discardDialogController.open() }, [hasValue]) + const handleInputKeyDown = useCallback( + (event: React.KeyboardEvent) => { + // Don't act if the input already prevented this event + if (event.isDefaultPrevented()) { + return + } + // Discard input text with Escape + if (event.key === 'Escape') { + event.preventDefault() + event.stopPropagation() + startDiscard() + } + // TODO: this would be cool + // Edit last comment if current user is the owner and pressing arrowUp + // if (event.key === 'ArrowUp') { + // const lastReply = replies.splice(-1)[0] + // if (lastReply?.authorId === currentUser.id && !hasValue) { + // + // } + // } + }, + [startDiscard], + ) + const cancelDiscard = useCallback(() => { replyInputRef.current?.discardDialogController.close() }, []) const confirmDiscard = useCallback(() => { - replyInputRef.current?.discardDialogController.close() - replyInputRef.current?.reset() setValue(EMPTY_ARRAY) + replyInputRef.current?.discardDialogController.close() replyInputRef.current?.focus() }, []) @@ -197,6 +222,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm hasError={reply._state?.type === 'createError'} isRetrying={reply._state?.type === 'createRetrying'} mentionOptions={mentionOptions} + onInputKeyDown={handleInputKeyDown} onCopyLink={onCopyLink} onCreateRetry={onCreateRetry} onDelete={onDelete} @@ -207,6 +233,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm )), [ currentUser, + handleInputKeyDown, mentionOptions, onCopyLink, onCreateRetry, @@ -247,6 +274,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm onCreateRetry={onCreateRetry} onDelete={onDelete} onEdit={onEdit} + onInputKeyDown={onKeyDown} onStatusChange={onStatusChange} readOnly={readOnly} /> @@ -269,7 +297,6 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm )} {renderedReplies} - {canReply && ( void onDelete: (id: string) => void onEdit: (id: string, message: CommentEditPayload) => void + onInputKeyDown?: (event: React.KeyboardEvent) => void onStatusChange?: (id: string, status: CommentStatus) => void readOnly?: boolean } @@ -137,6 +138,7 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) { onCreateRetry, onDelete, onEdit, + onInputKeyDown, onStatusChange, readOnly, } = props @@ -146,7 +148,6 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) { const [value, setValue] = useState(message) const [isEditing, setIsEditing] = useState(false) const [rootElement, setRootElement] = useState(null) - const [mentionMenuOpen, setMentionMenuOpen] = useState(false) const startMessage = useRef(message) const [menuOpen, setMenuOpen] = useState(false) @@ -184,10 +185,27 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) { cancelEdit() return } - commentInputRef.current?.discardDialogController.open() }, [cancelEdit, hasChanges, hasValue]) + const handleInputKeyDown = useCallback( + (event: React.KeyboardEvent) => { + // Don't act if the input already prevented this event + if (event.isDefaultPrevented()) { + return + } + // Discard the input text + if (event.key === 'Escape') { + event.preventDefault() + event.stopPropagation() + startDiscard() + } + // Call parent handler + if (onInputKeyDown) onInputKeyDown(event) + }, + [onInputKeyDown, startDiscard], + ) + const cancelDiscard = useCallback(() => { commentInputRef.current?.discardDialogController.close() }, []) @@ -215,7 +233,7 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) { }) useGlobalKeyDown((event) => { - if (event.key === 'Escape' && !mentionMenuOpen && !hasChanges) { + if (event.key === 'Escape' && !hasChanges) { cancelEdit() } }) @@ -223,7 +241,6 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) { useClickOutside(() => { if (!hasChanges) { cancelEdit() - commentInputRef.current?.blur() } }, [rootElement]) @@ -294,8 +311,7 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) { onChange={setValue} onDiscardCancel={cancelDiscard} onDiscardConfirm={confirmDiscard} - onEscapeKeyDown={startDiscard} - onMentionMenuOpenChange={setMentionMenuOpen} + onKeyDown={handleInputKeyDown} onSubmit={handleEditSubmit} readOnly={readOnly} ref={commentInputRef} 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 5c7ae788853..44cb74133e0 100644 --- a/packages/sanity/src/desk/comments/src/components/list/CreateNewThreadInput.tsx +++ b/packages/sanity/src/desk/comments/src/components/list/CreateNewThreadInput.tsx @@ -10,8 +10,8 @@ interface CreateNewThreadInputProps { fieldName: string mentionOptions: MentionOptionsHookValue onBlur?: CommentInputProps['onBlur'] - onEditDiscard?: () => void onFocus?: CommentInputProps['onFocus'] + onKeyDown?: (event: React.KeyboardEvent) => void onNewThreadCreate: (payload: CommentMessage) => void readOnly?: boolean } @@ -22,8 +22,8 @@ export function CreateNewThreadInput(props: CreateNewThreadInputProps) { fieldName, mentionOptions, onBlur, - onEditDiscard, onFocus, + onKeyDown, onNewThreadCreate, readOnly, } = props @@ -40,20 +40,34 @@ export function CreateNewThreadInput(props: CreateNewThreadInputProps) { const startDiscard = useCallback(() => { if (!hasValue) { - onEditDiscard?.() return } - commentInputHandle.current?.discardDialogController.open() - }, [hasValue, onEditDiscard]) + }, [hasValue]) + + const handleInputKeyDown = useCallback( + (event: React.KeyboardEvent) => { + // Don't act if the input already prevented this event + if (event.isDefaultPrevented()) { + return + } + // Discard the input text + if (event.key === 'Escape') { + event.preventDefault() + event.stopPropagation() + startDiscard() + } + // Call parent handler + if (onKeyDown) onKeyDown(event) + }, + [onKeyDown, startDiscard], + ) const confirmDiscard = useCallback(() => { setValue(EMPTY_ARRAY) - commentInputHandle.current?.reset() commentInputHandle.current?.discardDialogController.close() commentInputHandle.current?.focus() - onEditDiscard?.() - }, [onEditDiscard]) + }, []) const cancelDiscard = useCallback(() => { commentInputHandle.current?.discardDialogController.close() @@ -74,7 +88,7 @@ export function CreateNewThreadInput(props: CreateNewThreadInputProps) { onChange={setValue} onDiscardCancel={cancelDiscard} onDiscardConfirm={confirmDiscard} - onEscapeKeyDown={startDiscard} + onKeyDown={handleInputKeyDown} onFocus={onFocus} onSubmit={handleSubmit} placeholder={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 844d6e0a1d4..6714fc76134 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 @@ -27,8 +27,8 @@ export interface CommentInputProps { onChange: (value: PortableTextBlock[]) => void onDiscardCancel: () => void onDiscardConfirm: () => void - onEscapeKeyDown?: () => void onFocus?: (e: React.FormEvent) => void + onKeyDown?: (e: React.KeyboardEvent) => void onMentionMenuOpenChange?: (open: boolean) => void onSubmit: () => void placeholder?: React.ReactNode @@ -66,8 +66,8 @@ export const CommentInput = forwardRef( onChange, onDiscardCancel, onDiscardConfirm, - onEscapeKeyDown, onFocus, + onKeyDown, onMentionMenuOpenChange, onSubmit, placeholder, @@ -130,6 +130,11 @@ export const CommentInput = forwardRef( scrollToEditor() }, [onSubmit, requestFocus, resetEditorInstance, scrollToEditor]) + const handleDiscardConfirm = useCallback(() => { + onDiscardConfirm() + resetEditorInstance() + }, [onDiscardConfirm, resetEditorInstance]) + // 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. @@ -156,20 +161,18 @@ export const CommentInput = forwardRef( } }, scrollTo: scrollToEditor, - reset: () => { - setEditorInstanceKey(keyGenerator()) - }, + reset: resetEditorInstance, discardDialogController, } }, - [discardDialogController, requestFocus, scrollToEditor], + [discardDialogController, requestFocus, resetEditorInstance, scrollToEditor], ) return ( <> {showDiscardDialog && ( - + )} @@ -199,8 +202,8 @@ export const CommentInput = forwardRef( currentUser={currentUser} focusLock={focusLock} onBlur={onBlur} - onEscapeKeyDown={onEscapeKeyDown} onFocus={onFocus} + onKeyDown={onKeyDown} 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 7ccc344e926..04fcd29288b 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 @@ -77,36 +77,20 @@ interface CommentInputInnerProps { currentUser: CurrentUser focusLock?: boolean onBlur?: (e: React.FormEvent) => void - onEscapeKeyDown?: () => void onFocus?: (e: React.FormEvent) => void + onKeyDown?: (e: React.KeyboardEvent) => void onSubmit: () => void placeholder?: React.ReactNode withAvatar?: boolean } export function CommentInputInner(props: CommentInputInnerProps) { - const { - currentUser, - focusLock, - onBlur, - onEscapeKeyDown, - onFocus, - onSubmit, - placeholder, - withAvatar, - } = props + const {currentUser, focusLock, onBlur, onFocus, onKeyDown, onSubmit, placeholder, withAvatar} = + props const [user] = useUser(currentUser.id) - const { - canSubmit, - expandOnFocus, - focused, - hasChanges, - insertAtChar, - mentionsMenuOpen, - openMentions, - readOnly, - } = useCommentInput() + const {canSubmit, expandOnFocus, focused, hasChanges, insertAtChar, openMentions, readOnly} = + useCommentInput() const avatar = withAvatar ? : null @@ -119,21 +103,8 @@ export function CommentInputInner(props: CommentInputInnerProps) { [insertAtChar, openMentions], ) - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - e.stopPropagation() - e.preventDefault() - if (mentionsMenuOpen) return - - onEscapeKeyDown?.() - } - }, - [mentionsMenuOpen, onEscapeKeyDown], - ) - return ( - + {avatar} 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 f2e7b4f7c1e..a150b45754e 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 @@ -73,16 +73,14 @@ export function CommentInputProvider(props: CommentInputProviderProps) { setMentionsMenuOpen(false) setMentionsSearchTerm('') setSelectionAtMentionInsert(null) - focusEditor() - }, [focusEditor]) + }, []) const openMentions = useCallback(() => { setMentionsMenuOpen(true) setMentionsSearchTerm('') setMentionsMenuOpen(true) setSelectionAtMentionInsert(PortableTextEditor.getSelection(editor)) - focusEditor() - }, [focusEditor, editor]) + }, [editor]) // This function activates or deactivates the mentions menu and updates // the mention search term when the user types into the Portable Text Editor. 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 3f587eab15c..0688f4c4f44 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 + onKeyDown?: (e: React.KeyboardEvent) => void onSubmit?: () => void placeholder?: React.ReactNode } @@ -62,7 +63,14 @@ export interface EditableHandle { } export function Editable(props: EditableProps) { - const {focusLock, placeholder = 'Create a new comment', onFocus, onBlur, onSubmit} = props + const { + focusLock, + placeholder = 'Create a new comment', + onFocus, + onBlur, + onKeyDown, + onSubmit, + } = props const [popoverElement, setPopoverElement] = useState(null) const rootElementRef = useRef(null) const editableRef = useRef(null) @@ -72,7 +80,6 @@ export function Editable(props: EditableProps) { const { canSubmit, closeMentions, - focusEditor, insertMention, mentionOptions, mentionsMenuOpen, @@ -145,14 +152,18 @@ export function Editable(props: EditableProps) { case 'ArrowLeft': case 'ArrowRight': if (mentionsMenuOpen) { + // stop these events if the menu is open + event.preventDefault() + event.stopPropagation() closeMentions() - focusEditor() } break default: } + // Call parent key handler + if (onKeyDown) onKeyDown(event) }, - [canSubmit, closeMentions, focusEditor, mentionsMenuOpen, onSubmit], + [canSubmit, closeMentions, mentionsMenuOpen, onKeyDown, onSubmit], ) const initialSelectionAtEndOfContent: EditorSelection | undefined = useMemo(() => {