Skip to content

Commit

Permalink
refactor(comments): refactor key event handling in the comment input …
Browse files Browse the repository at this point in the history
…and consumers

This will let comment input key events propagate to the parent components for
handling spesific functionality for that parent.

This also reduces the need for additional key handlers in the parent components,
and makes it easier to compose functionality as there is only one originating source
of those events.
  • Loading branch information
skogsmaskin committed Oct 26, 2023
1 parent 2c773dc commit 9a2390b
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface CommentFieldButtonProps {
onClick?: () => void
onCommentAdd: () => void
onDiscard: () => void
onInputKeyDown?: (event: React.KeyboardEvent<Element>) => void
open: boolean
setOpen: (open: boolean) => void
value: CommentMessage
Expand All @@ -74,11 +75,11 @@ export function CommentFieldButton(props: CommentFieldButtonProps) {
onClick,
onCommentAdd,
onDiscard,
onInputKeyDown,
open,
setOpen,
value,
} = props
const [mentionMenuOpen, setMentionMenuOpen] = useState<boolean>(false)
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
const commentInputHandle = useRef<CommentInputHandle | null>(null)
const hasComments = Boolean(count > 0)
Expand All @@ -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<Element>) => {
// 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()
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ interface CommentsListItemProps {
onCreateRetry: (id: string) => void
onDelete: (id: string) => void
onEdit: (id: string, payload: CommentEditPayload) => void
onKeyDown?: (event: React.KeyboardEvent<Element>) => void
onPathSelect?: (path: Path) => void
onReply: (payload: CommentCreatePayload) => void
onStatusChange?: (id: string, status: CommentStatus) => void
Expand All @@ -95,6 +96,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
onCreateRetry,
onDelete,
onEdit,
onKeyDown,
onPathSelect,
onReply,
onStatusChange,
Expand Down Expand Up @@ -141,14 +143,37 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
replyInputRef.current?.discardDialogController.open()
}, [hasValue])

const handleInputKeyDown = useCallback(
(event: React.KeyboardEvent<Element>) => {
// 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()
}, [])

Expand Down Expand Up @@ -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}
Expand All @@ -207,6 +233,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
)),
[
currentUser,
handleInputKeyDown,
mentionOptions,
onCopyLink,
onCreateRetry,
Expand Down Expand Up @@ -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}
/>
Expand All @@ -269,7 +297,6 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
)}

{renderedReplies}

{canReply && (
<CommentInput
currentUser={currentUser}
Expand All @@ -278,7 +305,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
onChange={setValue}
onDiscardCancel={cancelDiscard}
onDiscardConfirm={confirmDiscard}
onEscapeKeyDown={startDiscard}
onKeyDown={handleInputKeyDown}
onSubmit={handleReplySubmit}
placeholder="Reply"
readOnly={readOnly}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ interface CommentsListItemLayoutProps {
onCreateRetry?: (id: string) => void
onDelete: (id: string) => void
onEdit: (id: string, message: CommentEditPayload) => void
onInputKeyDown?: (event: React.KeyboardEvent<Element>) => void
onStatusChange?: (id: string, status: CommentStatus) => void
readOnly?: boolean
}
Expand All @@ -137,6 +138,7 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) {
onCreateRetry,
onDelete,
onEdit,
onInputKeyDown,
onStatusChange,
readOnly,
} = props
Expand All @@ -146,7 +148,6 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) {
const [value, setValue] = useState<CommentMessage>(message)
const [isEditing, setIsEditing] = useState<boolean>(false)
const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
const [mentionMenuOpen, setMentionMenuOpen] = useState<boolean>(false)
const startMessage = useRef<CommentMessage>(message)
const [menuOpen, setMenuOpen] = useState<boolean>(false)

Expand Down Expand Up @@ -184,10 +185,27 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) {
cancelEdit()
return
}

commentInputRef.current?.discardDialogController.open()
}, [cancelEdit, hasChanges, hasValue])

const handleInputKeyDown = useCallback(
(event: React.KeyboardEvent<Element>) => {
// 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()
}, [])
Expand Down Expand Up @@ -215,15 +233,14 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) {
})

useGlobalKeyDown((event) => {
if (event.key === 'Escape' && !mentionMenuOpen && !hasChanges) {
if (event.key === 'Escape' && !hasChanges) {
cancelEdit()
}
})

useClickOutside(() => {
if (!hasChanges) {
cancelEdit()
commentInputRef.current?.blur()
}
}, [rootElement])

Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ interface CreateNewThreadInputProps {
fieldName: string
mentionOptions: MentionOptionsHookValue
onBlur?: CommentInputProps['onBlur']
onEditDiscard?: () => void
onFocus?: CommentInputProps['onFocus']
onKeyDown?: (event: React.KeyboardEvent<Element>) => void
onNewThreadCreate: (payload: CommentMessage) => void
readOnly?: boolean
}
Expand All @@ -22,8 +22,8 @@ export function CreateNewThreadInput(props: CreateNewThreadInputProps) {
fieldName,
mentionOptions,
onBlur,
onEditDiscard,
onFocus,
onKeyDown,
onNewThreadCreate,
readOnly,
} = props
Expand All @@ -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<Element>) => {
// 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()
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export interface CommentInputProps {
onChange: (value: PortableTextBlock[]) => void
onDiscardCancel: () => void
onDiscardConfirm: () => void
onEscapeKeyDown?: () => void
onFocus?: (e: React.FormEvent<HTMLDivElement>) => void
onKeyDown?: (e: React.KeyboardEvent<Element>) => void
onMentionMenuOpenChange?: (open: boolean) => void
onSubmit: () => void
placeholder?: React.ReactNode
Expand Down Expand Up @@ -66,8 +66,8 @@ export const CommentInput = forwardRef<CommentInputHandle, CommentInputProps>(
onChange,
onDiscardCancel,
onDiscardConfirm,
onEscapeKeyDown,
onFocus,
onKeyDown,
onMentionMenuOpenChange,
onSubmit,
placeholder,
Expand Down Expand Up @@ -130,6 +130,11 @@ export const CommentInput = forwardRef<CommentInputHandle, CommentInputProps>(
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.
Expand All @@ -156,20 +161,18 @@ export const CommentInput = forwardRef<CommentInputHandle, CommentInputProps>(
}
},
scrollTo: scrollToEditor,
reset: () => {
setEditorInstanceKey(keyGenerator())
},
reset: resetEditorInstance,

discardDialogController,
}
},
[discardDialogController, requestFocus, scrollToEditor],
[discardDialogController, requestFocus, resetEditorInstance, scrollToEditor],
)

return (
<>
{showDiscardDialog && (
<CommentInputDiscardDialog onClose={onDiscardCancel} onConfirm={onDiscardConfirm} />
<CommentInputDiscardDialog onClose={onDiscardCancel} onConfirm={handleDiscardConfirm} />
)}

<Stack ref={editorContainerRef} data-testid="comment-input">
Expand Down Expand Up @@ -199,8 +202,8 @@ export const CommentInput = forwardRef<CommentInputHandle, CommentInputProps>(
currentUser={currentUser}
focusLock={focusLock}
onBlur={onBlur}
onEscapeKeyDown={onEscapeKeyDown}
onFocus={onFocus}
onKeyDown={onKeyDown}
onSubmit={handleSubmit}
placeholder={placeholder}
withAvatar={withAvatar}
Expand Down
Loading

0 comments on commit 9a2390b

Please sign in to comment.