EmojiList.tsx raw

   1  import Emoji from '@/components/Emoji'
   2  import { ScrollArea } from '@/components/ui/scroll-area'
   3  import { cn } from '@/lib/utils'
   4  import customEmojiService from '@/services/custom-emoji.service'
   5  import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
   6  
   7  export interface EmojiListProps {
   8    items: string[]
   9    command: (params: { name?: string }) => void
  10  }
  11  
  12  export interface EmojiListHandler {
  13    onKeyDown: (params: { event: KeyboardEvent }) => boolean
  14  }
  15  
  16  export const EmojiList = forwardRef<EmojiListHandler, EmojiListProps>((props, ref) => {
  17    const [selectedIndex, setSelectedIndex] = useState(0)
  18  
  19    const selectItem = (index: number): void => {
  20      const item = props.items[index]
  21  
  22      if (item) {
  23        props.command({ name: item })
  24      }
  25  
  26      customEmojiService.updateSuggested(item)
  27    }
  28  
  29    const upHandler = (): void => {
  30      setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length)
  31    }
  32  
  33    const downHandler = (): void => {
  34      setSelectedIndex((selectedIndex + 1) % props.items.length)
  35    }
  36  
  37    const enterHandler = (): void => {
  38      selectItem(selectedIndex)
  39    }
  40  
  41    useEffect(() => setSelectedIndex(props.items.length ? 0 : -1), [props.items])
  42  
  43    useImperativeHandle(ref, () => {
  44      return {
  45        onKeyDown: (x: { event: KeyboardEvent }): boolean => {
  46          if (x.event.key === 'ArrowUp') {
  47            upHandler()
  48            return true
  49          }
  50  
  51          if (x.event.key === 'ArrowDown') {
  52            downHandler()
  53            return true
  54          }
  55  
  56          if (x.event.key === 'Enter' && selectedIndex >= 0) {
  57            enterHandler()
  58            return true
  59          }
  60  
  61          return false
  62        }
  63      }
  64    }, [upHandler, downHandler, enterHandler])
  65  
  66    if (!props.items?.length) {
  67      return null
  68    }
  69  
  70    return (
  71      <ScrollArea
  72        className="border rounded-lg bg-background z-50 pointer-events-auto flex flex-col max-h-80 overflow-y-auto"
  73        onWheel={(e) => e.stopPropagation()}
  74        onTouchMove={(e) => e.stopPropagation()}
  75      >
  76        <div className="p-1">
  77          {props.items.map((item, index) => {
  78            return (
  79              <EmojiListItem
  80                key={item}
  81                id={item}
  82                selectedIndex={selectedIndex}
  83                index={index}
  84                selectItem={selectItem}
  85                setSelectedIndex={setSelectedIndex}
  86              />
  87            )
  88          })}
  89        </div>
  90      </ScrollArea>
  91    )
  92  })
  93  
  94  function EmojiListItem({
  95    id,
  96    selectedIndex,
  97    index,
  98    selectItem,
  99    setSelectedIndex
 100  }: {
 101    id: string
 102    selectedIndex: number
 103    index: number
 104    selectItem: (index: number) => void
 105    setSelectedIndex: (index: number) => void
 106  }) {
 107    const emoji = useMemo(() => customEmojiService.getEmojiById(id), [id])
 108    if (!emoji) return null
 109  
 110    return (
 111      <button
 112        className={cn(
 113          'cursor-pointer w-full p-1 rounded-lg transition-colors [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
 114          selectedIndex === index && 'bg-accent text-accent-foreground'
 115        )}
 116        onClick={() => selectItem(index)}
 117        onMouseEnter={() => setSelectedIndex(index)}
 118      >
 119        <div className="flex gap-2 items-center truncate pointer-events-none">
 120          <Emoji
 121            emoji={emoji}
 122            classNames={{
 123              img: 'size-8 shrink-0 rounded-md',
 124              text: 'w-8 text-center shrink-0'
 125            }}
 126          />
 127          <span className="truncate">:{emoji.shortcode}:</span>
 128        </div>
 129      </button>
 130    )
 131  }
 132