Skip to content

Commit f94410a

Browse files
fix suggestion list
1 parent 827ad06 commit f94410a

File tree

4 files changed

+147
-64
lines changed

4 files changed

+147
-64
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"textarea-caret": "^3.1.0",
6262
"textarea-markdown-editor": "^1.0.4",
6363
"use-debounce": "^10.0.0",
64+
"use-item-list": "^0.1.2",
6465
"zod": "^3.22.4",
6566
"zustand": "^4.4.7"
6667
},

pnpm-lock.yaml

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/_components/markdown-editor.tsx

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as Switch from '@radix-ui/react-switch'
44
import { matchSorter } from 'match-sorter'
55
import * as React from 'react'
66
import { useDetectClickOutside } from 'react-detect-click-outside'
7+
import { type ItemOptions, useItemList } from 'use-item-list'
78

89
import TextareaAutosize, {
910
type TextareaAutosizeProps,
@@ -22,9 +23,9 @@ import { HtmlView } from './html-view'
2223
import { type SuggestionType, getSuggestionData } from '~/utils/suggestion'
2324
import { env } from '~/env'
2425
import { api } from '~/trpc/react'
25-
import { SuggestionListSelect } from './suggestion-list'
2626
import { uploadImageCommandHandler } from '~/server/cloudinary'
2727
import { markdownToHtml } from '~/utils/text'
28+
import { useEffect, useRef } from 'react'
2829

2930
type SuggestionResult = {
3031
label: string
@@ -359,11 +360,9 @@ const Suggestions = ({
359360
onClose,
360361
}: {
361362
state: SuggestionState
362-
onSelect: (suggestionResult: SuggestionResult) => void
363+
onSelect: (suggestionResult: string) => void
363364
onClose: () => void
364365
}) => {
365-
const ref = useDetectClickOutside({ onTriggered: onClose })
366-
367366
const isMentionType = state.type === 'mention'
368367
const isEmojiType = state.type === 'emoji'
369368

@@ -403,21 +402,130 @@ const Suggestions = ({
403402
return null
404403
}
405404

405+
return (
406+
<SuggestionList
407+
suggestionList={suggestionList}
408+
position={state.position}
409+
onSuggestionSelect={onSelect}
410+
onClose={onClose}
411+
/>
412+
)
413+
}
414+
415+
const SuggestionList = ({
416+
suggestionList,
417+
position,
418+
onSuggestionSelect,
419+
onClose,
420+
}: {
421+
suggestionList: SuggestionResult[]
422+
position: SuggestionPosition
423+
onSuggestionSelect: (suggestionResult: string) => void
424+
onClose: () => void
425+
}) => {
426+
const ref = useDetectClickOutside({ onTriggered: onClose })
427+
428+
const { moveHighlightedItem, selectHighlightedItem, useItem } = useItemList({
429+
onSelect: (item: SuggestionResult) => {
430+
onSuggestionSelect(item.value)
431+
},
432+
})
433+
434+
useEffect(() => {
435+
function handleKeydownEvent(event: KeyboardEvent) {
436+
const { code } = event
437+
438+
const preventDefaultCodes = ['ArrowUp', 'ArrowDown', 'Enter', 'Tab']
439+
440+
if (preventDefaultCodes.includes(code)) {
441+
event.preventDefault()
442+
}
443+
444+
if (code === 'ArrowUp') {
445+
moveHighlightedItem(-1)
446+
}
447+
448+
if (code === 'ArrowDown') {
449+
moveHighlightedItem(1)
450+
}
451+
452+
if (code === 'Enter' || code === 'Tab') {
453+
selectHighlightedItem()
454+
}
455+
}
456+
457+
document.addEventListener('keydown', handleKeydownEvent)
458+
return () => {
459+
document.removeEventListener('keydown', handleKeydownEvent)
460+
}
461+
}, [moveHighlightedItem, selectHighlightedItem])
462+
406463
return (
407464
<div
408-
className="absolute"
409465
ref={ref}
466+
className="absolute w-56 max-h-[286px] border rounded shadow-lg bg-primary overflow-y-auto"
410467
style={{
411-
top: state.position.top,
412-
left: state.position.left,
468+
top: position.top,
469+
left: position.left,
413470
}}
414471
>
415-
<SuggestionListSelect
416-
name={state.type === 'mention' ? 'Mention' : 'Emoji'}
417-
onValueChange={onSelect}
418-
open={state.isOpen}
419-
suggestions={suggestionList}
420-
/>
472+
<ul role="listbox" className="divide-y divide-primary">
473+
{suggestionList.map((suggestionResult) => (
474+
<SuggestionResult
475+
key={suggestionResult.value}
476+
useItem={
477+
useItem as ({ ref, text, value, disabled }: ItemOptions) => {
478+
id: string
479+
index: number
480+
highlight: () => void
481+
select: () => void
482+
selected: boolean
483+
useHighlighted: () => boolean
484+
}
485+
}
486+
suggestionResult={suggestionResult}
487+
/>
488+
))}
489+
</ul>
421490
</div>
422491
)
423492
}
493+
494+
const SuggestionResult = ({
495+
useItem,
496+
suggestionResult,
497+
}: {
498+
useItem: ({ ref, text, value, disabled }: ItemOptions) => {
499+
id: string
500+
index: number
501+
highlight: () => void
502+
select: () => void
503+
selected: boolean
504+
useHighlighted: () => boolean
505+
}
506+
suggestionResult: SuggestionResult
507+
}) => {
508+
const ref = useRef<HTMLLIElement>(null)
509+
const { id, highlight, select, useHighlighted } = useItem({
510+
ref,
511+
value: suggestionResult,
512+
})
513+
const highlighted = useHighlighted()
514+
515+
return (
516+
<li
517+
ref={ref}
518+
id={id}
519+
onMouseEnter={highlight}
520+
onClick={select}
521+
role="option"
522+
aria-selected={highlighted ? 'true' : 'false'}
523+
className={classNames(
524+
'px-4 py-2 text-sm text-left transition-colors cursor-pointer ',
525+
highlighted ? 'bg-blue-600 text-white' : 'text-primary',
526+
)}
527+
>
528+
{suggestionResult.label}
529+
</li>
530+
)
531+
}

src/app/_components/suggestion-list.tsx

Lines changed: 0 additions & 51 deletions
This file was deleted.

0 commit comments

Comments
 (0)