Skip to content

Commit

Permalink
refactor(comments): inline text filtering for mentions menu
Browse files Browse the repository at this point in the history
  • Loading branch information
skogsmaskin committed Sep 27, 2023
1 parent 28d35f0 commit 540486e
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 137 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {Box, Flex, Spinner, Stack, Text, TextInput} from '@sanity/ui'
import {Box, Flex, Spinner, Stack, Text} from '@sanity/ui'
import styled from 'styled-components'
import React, {useCallback, useEffect, useMemo, useState} from 'react'
import {SearchIcon} from '@sanity/icons'
import React, {useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'
import {MentionOptionUser} from '../../types'
import {CommandList} from '../../../components'
import {CommandList, CommandListHandle} from '../../../components'
import {MentionsMenuItem} from './MentionsMenuItem'

const EMPTY_ARRAY: MentionOptionUser[] = []
Expand All @@ -12,11 +11,6 @@ const Root = styled(Stack)({
maxWidth: '220px', // todo: improve
})

const HeaderBox = styled(Box)({
borderBottom: '1px solid var(--card-border-color)',
minHeight: 'max-content',
})

const ITEM_HEIGHT = 41
const LIST_PADDING = 4
const MAX_ITEMS = 7
Expand All @@ -25,23 +19,35 @@ const FlexWrap = styled(Flex)({
maxHeight: ITEM_HEIGHT * MAX_ITEMS + LIST_PADDING * 2 + ITEM_HEIGHT / 2,
})

export interface MentionsMenuHandle {
setSearchTerm: (term: string) => void
}
interface MentionsMenuProps {
loading: boolean
inputElement?: HTMLDivElement | null
onSelect: (userId: string) => void
options: MentionOptionUser[] | null
}

export const MentionsMenu = React.forwardRef(function MentionsMenu(
props: MentionsMenuProps,
ref: React.Ref<HTMLDivElement>,
ref: React.Ref<MentionsMenuHandle>,
) {
const {loading, onSelect, options = []} = props
const [inputElement, setInputElement] = useState<HTMLInputElement | null>(null)
const {loading, onSelect, options = [], inputElement} = props
const [searchTerm, setSearchTerm] = useState<string>('')

const handleInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value)
}, [])
const commandListRef = useRef<CommandListHandle>(null)

useImperativeHandle(
ref,
() => {
return {
setSearchTerm(term: string) {
setSearchTerm(term)
},
}
},
[],
)

const renderItem = useCallback(
(itemProps: MentionOptionUser) => {
Expand All @@ -67,14 +73,6 @@ export const MentionsMenu = React.forwardRef(function MentionsMenu(
return filtered || EMPTY_ARRAY
}, [options, searchTerm])

useEffect(() => {
const timeout = setTimeout(() => inputElement?.focus(), 0)

return () => {
clearTimeout(timeout)
}
}, [inputElement])

if (loading) {
return (
<Root>
Expand All @@ -85,19 +83,12 @@ export const MentionsMenu = React.forwardRef(function MentionsMenu(
)
}

return (
<Flex direction="column" height="fill" ref={ref}>
<HeaderBox padding={1}>
<TextInput
fontSize={1}
icon={SearchIcon}
onChange={handleInputChange}
placeholder="Search for a user"
radius={2}
ref={setInputElement}
/>
</HeaderBox>
// In this case the input element is the actual content editable HTMLDivElement from the PTE.
// Typecast it to an input element to make the CommandList component happy.
const _inputElement = inputElement ? (inputElement as HTMLInputElement) : undefined

return (
<Flex direction="column" height="fill">
{filteredOptions.length === 0 && (
<Box padding={5}>
<Text align="center" size={1} muted>
Expand All @@ -111,13 +102,13 @@ export const MentionsMenu = React.forwardRef(function MentionsMenu(
<CommandList
activeItemDataAttr="data-hovered"
ariaLabel="List of users to mention"
autoFocus="input"
fixedHeight
getItemDisabled={getItemDisabled}
inputElement={inputElement}
inputElement={_inputElement}
itemHeight={41}
items={filteredOptions}
padding={1}
ref={commandListRef}
renderItem={renderItem}
/>
</FlexWrap>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ export function CommentInputInner(props: CommentInputInnerProps) {
const [discardButtonElement, setDiscardButtonElement] = useState<HTMLButtonElement | null>(null)

const [user] = useUser(currentUser.id)
const {openMentions, focused, expandOnFocus, canSubmit, hasChanges} = useCommentInput()
const {openMentions, focused, expandOnFocus, canSubmit, hasChanges, insertAtChar} =
useCommentInput()

const avatar = withAvatar ? <CommentsAvatar user={user} /> : null

Expand All @@ -96,6 +97,11 @@ export function CommentInputInner(props: CommentInputInnerProps) {
discardButtonElement?.blur()
}, [discardButtonElement, onEditDiscard])

const handleMentionButtonClicked = useCallback(() => {
insertAtChar()
openMentions()
}, [insertAtChar, openMentions])

return (
<Flex align="flex-start" gap={2}>
{avatar}
Expand All @@ -121,7 +127,7 @@ export function CommentInputInner(props: CommentInputInnerProps) {
aria-label="Mention user"
icon={MentionIcon}
mode="bleed"
onClick={openMentions}
onClick={handleMentionButtonClicked}
/>

<ButtonDivider />
Expand Down
Loading

0 comments on commit 540486e

Please sign in to comment.