【React】基于 React+Tailwind 的 EmojiPicker 选择器组件

1.背景

复制代码
React 写一个 EmojiPicker 组件,基于 emoji-mart 组件二次封装。支持添加自定义背景 、Emoji 图标选择!并在页面上展示! 

2.技术栈

@emoji-mart/data 、emoji-mart : emoji 图标库、元数据

tailwindcss: 原子化 CSS 样式库

antd : 组件库

复制代码
"@emoji-mart/data": "^1.2.1",
"@remixicon/react": "^4.6.0",
"antd": "^5.24.3",
"emoji-mart": "^5.6.0",
"react": "^19.0.0",
"tailwindcss": "^3.4.17",

PS:

3.emoji选择器组件

src/components/emojiPicker/index.tsx

tsx 复制代码
import type { FC } from 'react';
import { useCallback, useState } from 'react';
import EmojiPickerInner from './emojiCom';
import { Button, Modal } from 'antd';

type IEmojiPickerProps = {
  onSelect: (emoji: string, background: string) => void
};
const EmojiPicker: FC<IEmojiPickerProps> = ({
  onSelect
}) => {
  const [isEmojiModalOpen, setIsEmojiModalOpen] = useState<boolean>(false);
  const [selectedBackground, setSelectedBackground] = useState<string>();
  const [selectedEmoji, setSelectedEmoji] = useState<string>();

  const selectEmoji = useCallback((emoji: string, background: string) => {
    setSelectedEmoji(emoji)
    setSelectedBackground(background)
  }, [setSelectedEmoji, setSelectedBackground]);

  const onModalSelectEmojOk = () => {

    if (!(selectedEmoji && selectedBackground))
      return

    onSelect(selectedEmoji!, selectedBackground!);
    clear();
    setIsEmojiModalOpen(false);
  };

  const clear = () => {
    setSelectedEmoji('')
    setSelectedBackground('')
  };

  return (
    <>
      <Button onClick={() => setIsEmojiModalOpen(true)}> Emoj 表情</Button>

      <Modal
        title="Emoj 表情选择"
        open={isEmojiModalOpen}
        onOk={onModalSelectEmojOk}
        okText="确定"
        cancelText="取消"
        okButtonProps={{ disabled: !(Boolean(selectedEmoji) && Boolean(selectedBackground)) }}
        onCancel={() => { setIsEmojiModalOpen(false) }}
      >
        {
          isEmojiModalOpen && (<EmojiPickerInner onSelect={selectEmoji} />)
        }

      </Modal >
    </>
  )
}
export default EmojiPicker

src/components/emojiPicker/emojiCom.tsx

tsx 复制代码
import type { ChangeEvent, FC } from 'react';
import { useState, useEffect } from 'react';

// components
import data from '@emoji-mart/data';
import type { EmojiMartData } from '@emoji-mart/data';
import { init, SearchIndex } from 'emoji-mart';

// icons
import { RiSearch2Line } from '@remixicon/react';

// utils
import { cn } from '@/lib/utils';

export interface Skins {
  native: string
};

export interface Emoji {
  id: string
  name: string
  keywords: string[]
  skins: Skins[]
  version: number
  emoticons?: string[]
};

type IEmojiPickerInnerProps = {
  emoji?: string
  background?: string
  onSelect?: (emoji: string, background: string) => void
  className?: string
};

init({ data });

const backgroundColors = [
  '#FFEAD5',
  '#E4FBCC',
  '#D3F8DF',
  '#E0F2FE',

  '#E0EAFF',
  '#EFF1F5',
  '#FBE8FF',
  '#FCE7F6',

  '#FEF7C3',
  '#E6F4D7',
  '#D5F5F6',
  '#D1E9FF',

  '#D1E0FF',
  '#D5D9EB',
  '#ECE9FE',
  '#FFE4E8',
];

const classNameComm = 'cursor-pointer w-8 h-8 p-1 flex items-center justify-center rounded-lg hover:ring-1 ring-offset-1 ring-gray-300'

const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
  onSelect, className,
}) => {
  const { categories } = data as EmojiMartData;
  const [selectedEmoji, setSelectedEmoji] = useState('');
  const [selectedBackground, setSelectedBackground] = useState(backgroundColors[0]);
  const [searchedEmojis, setSearchedEmojis] = useState<string[]>([]);
  const [isSearching, setIsSearching] = useState<boolean>(false);

  //  search icons
  const searchEmoji = async function searchEmoji(value: string) {
    const emojis: Emoji[] = await SearchIndex.search(value) || []

    const results = emojis.map((emoji) => {
      return emoji.skins[0]?.native
    })
    return results
  };

  // useEffect
  useEffect(() => {
    if (selectedEmoji && selectedBackground) onSelect?.(selectedEmoji, selectedBackground)
  }, [onSelect, selectedEmoji, selectedBackground]);

  return <div className={cn(className)}>
    <div className='flex flex-col items-center w-full px-3'>
      <div className="relative w-full">
        <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
          <RiSearch2Line className="w-5 h-5 text-gray-400" aria-hidden="true" />
        </div>
        <input
          type="search"
          id="search"
          className='block w-full h-10 px-3 pl-10 text-sm font-normal bg-gray-100 rounded-lg'
          placeholder="Search emojis..."
          onChange={async (e: ChangeEvent<HTMLInputElement>) => {
            if (e.target.value === '') {
              setIsSearching(false)
            } else {
              setIsSearching(true)
              const emojis = await searchEmoji(e.target.value)
              setSearchedEmojis(emojis)
            }
          }}
        />
      </div>
    </div>

    <div className="w-full max-h-[200px] overflow-x-hidden overflow-y-auto px-3">
      {isSearching && <>
        <div key={'category-search'} className='flex flex-col'>
          <p className='font-medium uppercase text-xs text-[#101828] mb-1'>Search</p>
          <div className='w-full h-full grid grid-cols-8 gap-1'>
            {searchedEmojis.map((emoji: string, index: number) => {
              return <div
                key={`emoji-search-${index}`}
                className='inline-flex w-10 h-10 rounded-lg items-center justify-center'
                onClick={() => {
                  setSelectedEmoji(emoji)
                }}
              >
                <div className={classNameComm}>
                  <em-emoji id={emoji} />
                </div>
              </div>
            })}
          </div>
        </div>
      </>}

      {categories.map((category, index: number) => {
        return <div key={`category-${index}`} className='flex flex-col'>
          <p className='font-medium uppercase text-xs text-[#101828] mb-1'>{category.id}</p>
          <div className='w-full h-full grid grid-cols-8 gap-1'>
            {category.emojis.map((emoji, index: number) => {
              return <div
                key={`emoji-${index}`}
                className='inline-flex w-10 h-10 rounded-lg items-center justify-center'
                onClick={() => {
                  setSelectedEmoji(emoji)
                }}
              >
                <div className={classNameComm}>
                  <em-emoji id={emoji} />
                </div>
              </div>
            })}

          </div>
        </div>
      })}
    </div>

    {/* Color Select */}
    <div className={cn('p-3 pb-0', selectedEmoji === '' ? 'opacity-25' : '')}>
      <p className='font-medium uppercase text-xs text-[#101828] mb-2'>Choose Style</p>
      <div className='w-full h-full grid grid-cols-8 gap-1'>
        {backgroundColors.map((color) => {
          return <div
            key={color}
            className={
              cn(
                'cursor-pointer',
                'hover:ring-1 ring-offset-1',
                'inline-flex w-10 h-10 rounded-lg items-center justify-center',
                color === selectedBackground ? 'ring-1 ring-gray-300' : '',
              )}
            onClick={() => {
              setSelectedBackground(color)
            }}
          >
            <div className={cn(
              'w-8 h-8 p-1 flex items-center justify-center rounded-lg',
            )
            } style={{ background: color }}>
              {selectedEmoji !== '' && <em-emoji id={selectedEmoji} />}
            </div>
          </div>
        })}
      </div>
    </div>
  </div>
};

export default EmojiPickerInner;

4. emoji 图标展示组件

src/components/appIcon/index.tsx

tsx 复制代码
import type { FC } from 'react';
import { init } from 'emoji-mart';;
import data from '@emoji-mart/data';
import { cn } from '@/lib/utils';

type AppIconType = 'image' | 'emoji';

init({ data });

export type AppIconProps = {
  size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large'
  rounded?: boolean
  iconType?: AppIconType | null
  icon?: string
  background?: string | null
  imageUrl?: string | null | undefined
  className?: string
  innerIcon?: React.ReactNode
  onClick?: () => void
};

// used for emojiPicker
const AppIcon: FC<AppIconProps> = ({
  size = 'medium',
  rounded = false,
  iconType,
  icon,
  background,
  imageUrl,
  className,
  innerIcon,
  onClick,
}) => {
  const wrapperClassName = cn(
    'flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0',
    size !== 'medium' && { large: 'w-10 h-10', small: 'w-8 h-8', tiny: 'w-6 h-6 text-base', xs: 'w-3 h-3 text-base' }[size],
    rounded && 'rounded-full',
    className ?? '',
    'overflow-hidden',
  );

  const isValidImageIcon = iconType === 'image' && imageUrl;

  return <span
    className={wrapperClassName}
    style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
    onClick={onClick}
  >
    {isValidImageIcon
      ? <img src={imageUrl} className="w-full h-full" alt="app icon" />
      : (innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />))
    }
  </span>
};

export default AppIcon;

5.cn 函数

src/lib/utils.ts

tsx 复制代码
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

6.测试使用

src\App.tsx

tsx 复制代码
import { useState } from 'react';
import EmojiPicker from '@/components/emojiPicker';
import AppIcon from '@/components/appIcon';


function App() {
  const [emoji, setEmoji] = useState('😀');
  const [background, setBackground] = useState('');

  const onSelect = (emoji: string, background: string) => {
    setEmoji(emoji)
    setBackground(background)
  };

  return (
    <div className="flex justify-center items-center h-screen gap-2">
      <AppIcon icon={emoji} background={background} />
      <EmojiPicker onSelect={onSelect} />
    </div>
  );
}

export default App;


如果你有更多问题,欢迎随时问我!😊

相关推荐
喝拿铁写前端28 分钟前
前端与 AI 结合的 10 个可能路径图谱
前端·人工智能
codingandsleeping32 分钟前
浏览器的缓存机制
前端·后端
灵感__idea2 小时前
JavaScript高级程序设计(第5版):扎实的基本功是唯一捷径
前端·javascript·程序员
摇滚侠2 小时前
Vue3 其它API toRow和markRow
前端·javascript
難釋懷2 小时前
JavaScript基础-history 对象
开发语言·前端·javascript
beibeibeiooo2 小时前
【CSS3】04-标准流 + 浮动 + flex布局
前端·html·css3
拉不动的猪2 小时前
刷刷题47(react常规面试题2)
前端·javascript·面试
浪遏2 小时前
场景题:大文件上传 ?| 过总字节一面😱
前端·javascript·面试
Bigger3 小时前
Tauri(十八)——如何开发 Tauri 插件
前端·rust·app
355984268550553 小时前
医保服务平台 Webpack逆向
前端·webpack·node.js