@@ -4,6 +4,7 @@ import * as Switch from '@radix-ui/react-switch'
44import { matchSorter } from 'match-sorter'
55import * as React from 'react'
66import { useDetectClickOutside } from 'react-detect-click-outside'
7+ import { type ItemOptions , useItemList } from 'use-item-list'
78
89import TextareaAutosize , {
910 type TextareaAutosizeProps ,
@@ -22,9 +23,9 @@ import { HtmlView } from './html-view'
2223import { type SuggestionType , getSuggestionData } from '~/utils/suggestion'
2324import { env } from '~/env'
2425import { api } from '~/trpc/react'
25- import { SuggestionListSelect } from './suggestion-list'
2626import { uploadImageCommandHandler } from '~/server/cloudinary'
2727import { markdownToHtml } from '~/utils/text'
28+ import { useEffect , useRef } from 'react'
2829
2930type 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+ }
0 commit comments