Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(comments): refactor how input and focus is handled #5068

Merged
merged 4 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions packages/sanity/playwright-ct/tests/comments/CommentInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,27 @@ test.describe('Comments', () => {
test('Should bring up mentions menu when typing @', async ({mount, page}) => {
await mount(<CommentsInputStory />)
const $editable = page.getByTestId('comment-input-editable')
await expect($editable).toBeEditable()
await page.keyboard.type(`@`)
await $editable.waitFor({state: 'visible'})
await page.keyboard.type('@')
await expect(page.getByTestId('comments-mentions-menu')).toBeVisible()
})

test('Should be able to submit', async ({mount, page}) => {
const {insertPortableText} = testHelpers({page})
let submitted = false
const onSubmit = () => {
submitted = true
}
await mount(<CommentsInputStory onSubmit={onSubmit} />)
const $editable = page.getByTestId('comment-input-editable')
await expect($editable).toBeEditable()
// Test that blank comments can't be submitted
await page.keyboard.press('Enter')
expect(submitted).toBe(false)
await insertPortableText('This is a comment!', $editable)
await expect($editable).toHaveText('This is a comment!')
await page.keyboard.press('Enter')
expect(submitted).toBe(true)
})
})
})
25 changes: 17 additions & 8 deletions packages/sanity/playwright-ct/tests/comments/CommentInputStory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,31 @@ const currentUser: CurrentUser = {

const SCHEMA_TYPES: [] = []

export function CommentsInputStory() {
const [value, setValue] = useState<PortableTextBlock[] | null>(null)

export function CommentsInputStory({
onDiscardCancel = noop,
onDiscardConfirm = noop,
onSubmit = noop,
value = null,
}: {
onDiscardCancel?: () => void
onDiscardConfirm?: () => void
onSubmit?: () => void
value?: PortableTextBlock[] | null
}) {
const [valueState, setValueState] = useState<PortableTextBlock[] | null>(value)
return (
<TestWrapper schemaTypes={SCHEMA_TYPES}>
<CommentInput
focusOnMount
placeholder="Your comment..."
focusLock
currentUser={currentUser}
onChange={setValue}
value={value}
onChange={setValueState}
value={valueState}
mentionOptions={{data: [], error: null, loading: false}}
onDiscardConfirm={noop}
onDiscardCancel={noop}
onSubmit={noop}
onDiscardConfirm={onDiscardConfirm}
onDiscardCancel={onDiscardCancel}
onSubmit={onSubmit}
/>
</TestWrapper>
)
Expand Down
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 @@ -123,8 +125,6 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm

onReply?.(nextComment)
setValue(EMPTY_ARRAY)

replyInputRef.current?.focus()
}, [
onReply,
parentComment._id,
Expand All @@ -143,13 +143,38 @@ 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()
setValue(EMPTY_ARRAY)
replyInputRef.current?.discardDialogController.close()
replyInputRef.current?.focus()
}, [])

const handleThreadRootClick = useCallback(() => {
Expand Down Expand Up @@ -185,6 +210,40 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
}
}, [replies])

const renderedReplies = useMemo(
() =>
splicedReplies.map((reply) => (
<Stack as="li" key={reply._id} data-comment-id={reply._id}>
<CommentsListItemLayout
canDelete={reply.authorId === currentUser.id}
canEdit={reply.authorId === currentUser.id}
comment={reply}
currentUser={currentUser}
hasError={reply._state?.type === 'createError'}
isRetrying={reply._state?.type === 'createRetrying'}
mentionOptions={mentionOptions}
onInputKeyDown={handleInputKeyDown}
onCopyLink={onCopyLink}
onCreateRetry={onCreateRetry}
onDelete={onDelete}
onEdit={onEdit}
readOnly={readOnly}
/>
</Stack>
)),
[
currentUser,
handleInputKeyDown,
mentionOptions,
onCopyLink,
onCreateRetry,
onDelete,
onEdit,
readOnly,
splicedReplies,
],
)

return (
<Stack space={2} ref={rootRef}>
<StyledThreadCard
Expand Down Expand Up @@ -215,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 @@ -236,25 +296,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
</Flex>
)}

{splicedReplies.map((reply) => (
<Stack as="li" key={reply._id} data-comment-id={reply._id}>
<CommentsListItemLayout
canDelete={reply.authorId === currentUser.id}
canEdit={reply.authorId === currentUser.id}
comment={reply}
currentUser={currentUser}
hasError={reply._state?.type === 'createError'}
isRetrying={reply._state?.type === 'createRetrying'}
mentionOptions={mentionOptions}
onCopyLink={onCopyLink}
onCreateRetry={onCreateRetry}
onDelete={onDelete}
onEdit={onEdit}
readOnly={readOnly}
/>
</Stack>
))}

{renderedReplies}
{canReply && (
<CommentInput
currentUser={currentUser}
Expand All @@ -263,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
Loading
Loading