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

Prevent Slate errors by unifying the comment popups. #4804

Merged
merged 1 commit into from
Jan 30, 2024
Merged
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
294 changes: 103 additions & 191 deletions editor/src/components/canvas/controls/comment-popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,27 @@ const CommentThread = React.memo(({ comment }: CommentThreadProps) => {
}
}, [])

const getLiveblocksEditorElement = React.useCallback(() => {
if (composerRef.current == null) {
return null
}

const composerTextbox = getComposerTextbox()
if (composerTextbox == null) {
return null
}

scrollToBottom()

return composerTextbox
}, [scrollToBottom])

const triggerAutoFocus = React.useCallback(() => {
setTimeout(() => {
getLiveblocksEditorElement()?.focus()
}, 0)
}, [getLiveblocksEditorElement])

const onCreateThread = React.useCallback(
({ body }: ComposerSubmitComment, event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
Expand Down Expand Up @@ -207,32 +228,25 @@ const CommentThread = React.memo(({ comment }: CommentThreadProps) => {
switchEditorMode(EditorModes.commentMode(existingComment(newThread.id), 'not-dragging')),
setRightMenuTab(RightMenuTab.Comments),
])
triggerAutoFocus()
},
[createThread, comment, dispatch, remixSceneRoutes, scenes, createNewThreadReadStatus],
[
comment,
createNewThreadReadStatus,
dispatch,
triggerAutoFocus,
createThread,
scenes,
remixSceneRoutes,
],
)

const onSubmitComment = React.useCallback(() => {
if (threadId != null) {
createNewThreadReadStatus(threadId, 'read')
}
function getLiveblocksEditorElement(): HTMLDivElement | null {
if (composerRef.current == null) {
return null
}

const composerTextbox = getComposerTextbox()
if (composerTextbox == null) {
return null
}

scrollToBottom()

return composerTextbox
}
setTimeout(() => {
getLiveblocksEditorElement()?.focus()
}, 0)
}, [threadId, createNewThreadReadStatus, scrollToBottom])
triggerAutoFocus()
}, [threadId, triggerAutoFocus, createNewThreadReadStatus])

const onCommentDelete = React.useCallback(
(_deleted: CommentData) => {
Expand Down Expand Up @@ -278,8 +292,6 @@ const CommentThread = React.memo(({ comment }: CommentThreadProps) => {
setThreadReadStatus(thread.id, 'unread')
}, [thread?.id, setThreadReadStatus])

const collabs = useStorage((storage) => storage.collaborators)

const onScroll = () => {
const element = listRef.current
if (element == null) {
Expand Down Expand Up @@ -332,197 +344,97 @@ const CommentThread = React.memo(({ comment }: CommentThreadProps) => {
onKeyUp={stopPropagation}
onMouseUp={stopPropagation}
>
{thread == null ? (
<NewCommentPopup onComposerSubmit={onCreateThread} />
) : (
<>
<FlexRow
<>
<FlexRow
style={{
background: colorTheme.bg1.value,
justifyContent: 'flex-end',
padding: 6,
borderBottom: `1px solid ${colorTheme.bg3.value}`,
gap: 6,
}}
>
{when(
readByMe === 'read',
<Tooltip title='Mark As Unread' placement='top'>
<Button onClick={onClickMarkAsUnread}>
<Icn category='semantic' type='unread' width={18} height={18} color='main' />
</Button>
</Tooltip>,
)}
<Tooltip title='Resolve' placement='top'>
<Button onClick={onClickResolve} data-testid='resolve-thread-button'>
<Icn
category='semantic'
type={thread?.metadata.resolved ? 'resolved' : 'resolve'}
width={18}
height={18}
color='main'
/>
</Button>
</Tooltip>
<Button data-testid='close-comment' onClick={onClickClose}>
<Icn category='semantic' type='cross-large' width={16} height={16} color='main' />
</Button>
</FlexRow>
<div style={{ position: 'relative' }}>
<div
style={{
background: colorTheme.bg1.value,
justifyContent: 'flex-end',
padding: 6,
borderBottom: `1px solid ${colorTheme.bg3.value}`,
gap: 6,
maxHeight: PopupMaxHeight,
overflowY: 'scroll',
maxWidth: PopupMaxWidth,
wordWrap: 'break-word',
whiteSpace: 'normal',
}}
ref={listRef}
onScroll={onScroll}
>
{when(
readByMe === 'read',
<Tooltip title='Mark As Unread' placement='top'>
<Button onClick={onClickMarkAsUnread}>
<Icn category='semantic' type='unread' width={18} height={18} color='main' />
</Button>
</Tooltip>,
)}
<Tooltip title='Resolve' placement='top'>
<Button onClick={onClickResolve} data-testid='resolve-thread-button'>
<Icn
category='semantic'
type={thread?.metadata.resolved ? 'resolved' : 'resolve'}
width={18}
height={18}
color='main'
{(thread?.comments ?? []).map((c) => {
return (
<Comment
key={c.id}
comment={c}
onCommentDelete={onCommentDelete}
style={{ background: colorTheme.bg1.value }}
/>
</Button>
</Tooltip>
<Button data-testid='close-comment' onClick={onClickClose}>
<Icn category='semantic' type='cross-large' width={16} height={16} color='main' />
</Button>
</FlexRow>
<div style={{ position: 'relative' }}>
<div
style={{
maxHeight: PopupMaxHeight,
overflowY: 'scroll',
maxWidth: PopupMaxWidth,
wordWrap: 'break-word',
whiteSpace: 'normal',
}}
ref={listRef}
onScroll={onScroll}
>
{thread.comments.map((c) => {
return (
<Comment
key={c.id}
comment={c}
onCommentDelete={onCommentDelete}
style={{ background: colorTheme.bg1.value }}
/>
)
})}
</div>
<ListShadow position='top' enabled={showShadowTop} />
<ListShadow position='bottom' enabled={showShadowBottom} />
)
})}
</div>
<ListShadow position='top' enabled={showShadowTop} />
<ListShadow position='bottom' enabled={showShadowBottom} />
{thread == null ? null : (
<HeaderComment
enabled={thread.comments.length > 0 && showShadowTop}
comment={thread.comments[0]}
/>
</div>
)}
</div>
{thread == null ? (
<Composer
key={'comment-composer'}
ref={composerRef}
autoFocus
onComposerSubmit={onCreateThread}
style={ComposerStyle}
onKeyDown={onExistingCommentComposerKeyDown}
/>
) : (
<Composer
key={'comment-composer'}
ref={composerRef}
autoFocus
threadId={thread.id}
onComposerSubmit={onSubmitComment}
style={ComposerStyle}
onKeyDown={onExistingCommentComposerKeyDown}
/>
</>
)}
)}
</>
</div>
)
})
CommentThread.displayName = 'CommentThread'

type NewCommentPopupProps = {
onComposerSubmit: (
comment: ComposerSubmitComment,
event: React.FormEvent<HTMLFormElement>,
) => void
}

const NewCommentPopup = React.memo((props: NewCommentPopupProps) => {
const dispatch = useDispatch()

const onNewCommentComposerKeyDown = React.useCallback(
(e: React.KeyboardEvent) => switchToBasicCommentModeOnEscape(e, dispatch),
[dispatch],
)

const newCommentComposerAnimation = useAnimation()

const onClickOutsideNewComment = React.useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()

const composerTextbox = getComposerTextbox()
if (composerTextbox != null) {
function findPlaceholderChild(element: Element) {
if (element == null) {
return false
}
if (element.attributes.getNamedItem('data-placeholder') != null) {
return true
}
if (element.children.length < 1) {
return false
}
return findPlaceholderChild(element.children[0])
}

const isEmpty = composerTextbox.innerText.trim().length === 0
const isPlaceholder = !isEmpty && findPlaceholderChild(composerTextbox.children[0])

// if the contents of the new comment are empty...
if (isEmpty || isPlaceholder) {
// ...just close the popup
dispatch([switchEditorMode(EditorModes.commentMode(null, 'not-dragging'))])
} else {
// ...otherwise, shake the popup and re-focus its text box
const shakeDelta = 4 // px
void newCommentComposerAnimation.start({
x: [-shakeDelta, shakeDelta, -shakeDelta, shakeDelta, 0],
borderColor: [
colorTheme.error.cssValue,
colorTheme.error.cssValue,
colorTheme.error.cssValue,
colorTheme.error.cssValue,
'#00000000', // transparent, animatable
],
transition: { duration: 0.2 },
})
}

composerTextbox.focus()
}
},
[newCommentComposerAnimation, dispatch],
)

const onClickClose = React.useCallback(() => {
dispatch([switchEditorMode(EditorModes.commentMode(null, 'not-dragging'))])
}, [dispatch])

return (
<>
<div
style={{
background: 'transparent',
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
right: 0,
}}
onClick={onClickOutsideNewComment}
/>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
padding: 6,
height: 35,
background: colorTheme.bg1.value,
borderBottom: `1px solid ${colorTheme.bg3.value}`,
}}
>
<Button onClick={onClickClose}>
<Icn category='semantic' type='cross-large' width={16} height={16} color='main' />
</Button>
</div>
<motion.div animate={newCommentComposerAnimation}>
<Composer
autoFocus
onComposerSubmit={props.onComposerSubmit}
style={ComposerStyle}
onKeyDown={onNewCommentComposerKeyDown}
/>
</motion.div>
</>
)
})
NewCommentPopup.displayName = 'NewCommentPopup'

const ListShadow = React.memo(
({ enabled, position }: { enabled: boolean; position: 'top' | 'bottom' }) => {
return (
Expand Down
Loading