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

news2025/4/3 4:52:45

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:

  • emoji-mart/data@remixicon/reactantd 这些库直接用 pnpm 、npm 、yarm 直接安装即可

  • tailwindcss 安装配置参考

  • cn函数参考

3.emoji选择器组件

src/components/emojiPicker/index.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

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

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

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

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

6.测试使用

src\App.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;

效果展示
在这里插入图片描述

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2326285.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

02-Docker 使用

docker:快速构建、运行、管理应用的工具,可以帮助我们下载应用镜像,创建并运行镜像的容器,从而快速部署应用 1、部署mysql 先停掉虚拟机中的MySQL,确保你的虚拟机已经安装Docker,且网络开通的情况下,执行下面命令即可安装MySQL(注意:若服务器上已经有mysql 占用了330…

html5时钟升级!支持切换深浅模式 Canvas实现现代化动态时钟

HTML5 Canvas实现现代化动态时钟 这里写目录标题 HTML5 Canvas实现现代化动态时钟项目介绍技术实现1. 项目架构2. Canvas绘图实现2.1 表盘绘制2.2 刻度绘制2.3 指针绘制 3. 动画效果4. 主题切换 项目亮点技术要点总结项目收获改进方向结语 项目介绍 本项目使用HTML5 Canvas技术…

MOE-1 基本认识

解读一下MOE架构&#xff0c;部分内容图片参考自油管。 首先来简单了解一下什么是MoE&#xff08;Mixture of Experts&#xff0c;专家混合&#xff09; MoE&#xff08;Mixture of Experts&#xff09;是一种深度学习架构&#xff0c;其核心思想是通过**多个专家网络&#xf…

【C++接入大模型】WinHTTP类封装:实现对话式大模型接口访问

一、类设计概述 近期准备用C做一些大预言模型方面的开发&#xff0c;先期计划实现C调用公共的大模型Web接口&#xff0c;因为之前没做过C的Web开发&#xff0c;经验少&#xff0c;所以对比了一些主流的框架&#xff0c;包括实际测试验证。以下是Windows平台下主流C HTTP库的对…

【银河麒麟高级服务器操作系统 】虚拟机运行数据库存储异常现象分析及处理全流程

更多银河麒麟操作系统产品及技术讨论&#xff0c;欢迎加入银河麒麟操作系统官方论坛 https://forum.kylinos.cn 了解更多银河麒麟操作系统全新产品&#xff0c;请点击访问 麒麟软件产品专区&#xff1a;https://product.kylinos.cn 开发者专区&#xff1a;https://developer…

文件分享系统--开源的可视化文件共享管理工具

家里有公网&#xff0c;经常要发文件给别人&#xff0c;文件几个G发送还要云盘或者倒手一次才行&#xff0c;所以弄了个文件分享系统&#xff0c;这个是用字节的 AI Trae 写的&#xff0c;反正反复折腾还是弄出来了。东西挺好用&#xff0c;可以拖拽多个文件上传也可以手动添加…

【力扣刷题实战】寻找数组的中心下标

大家好&#xff0c;我是小卡皮巴拉 文章目录 目录 力扣题目&#xff1a;寻找数组的中心下标 题目描述 解题思路 问题理解 算法选择 具体思路 解题要点 完整代码&#xff08;C&#xff09; 兄弟们共勉 &#xff01;&#xff01;&#xff01; 每篇前言 博客主页&#…

LearnOpenGL小练习(QOpenGLWidget版本)

你好&#xff0c;三角形 1.绘制两个彼此相连的三角形 画两个独立的三角形&#xff0c;给出两个三角形顶点&#xff0c;使用GL_TRIANGLES绘图即可。 关键代码 void MyOpenglWgt::initializeGL() {initializeOpenGLFunctions(); // 1. 创建ShaderProgram着色器&#xff1a;加…

基于OpenCV+MediaPipe手部追踪

一、技术栈 1. OpenCV&#xff08;Open Source Computer Vision Library&#xff09; 性质&#xff1a;开源计算机视觉库&#xff08;Library&#xff09; 主要功能&#xff1a; 图像/视频的基础处理&#xff08;读取、裁剪、滤波、色彩转换等&#xff09; 特征检测&#xf…

十五届蓝桥杯省赛Java B组(持续更新..)

目录 十五届蓝桥杯省赛Java B组第一题&#xff1a;报数第二题&#xff1a;类斐波那契数第三题&#xff1a;分布式队列第四题&#xff1a;食堂第五题&#xff1a;最优分组第六题&#xff1a;星际旅行第七题&#xff1a;LITS游戏第八题&#xff1a;拼十字 十五届蓝桥杯省赛Java B…

蓝耘平台API深度剖析:如何高效实现AI应用联动

目录 一、蓝耘平台简介 1.1 蓝耘通义大模型 1.2 蓝耘云计算资源 1.3 蓝耘API与微服务 二、 蓝耘平台应用联动场景 2.1 数据采集与预处理联动 2.2 模型推理与后端服务联动 2.3 跨平台联动 三、蓝耘平台注册体验功能 3.1 注册 3.2 体验蓝耘MaaS平台如何使用海螺AI生成视频…

缓存 “三剑客”

缓存 “三剑客” 问题 如何保证 Redis 缓存和数据库的一致性&#xff1f; 1. 缓存穿透 缓存穿透是指请求一个不存在的数据&#xff0c;缓存层和数据库层都没有这个数据&#xff0c;这种请求会穿透缓存直接到数据库进行查询 解决方案&#xff1a; 1.1 缓存空值或特殊值 查一…

ComfyUi教程之阿里的万象2.1视频模型

ComfyUi教程之阿里的万象2.1视频模型 官网Wan 2.1 特点 一、本地安装1.1克隆仓库1.2 安装依赖&#xff08;1.3&#xff09;下载模型&#xff08;1.4&#xff09;CUDA和CUDNN 二、 使用体验&#xff08;2.1&#xff09;官方例子&#xff08;2.2&#xff09;执行过程&#xff08;…

Leetcode 寻找两个正序数组的中位数

&#x1f4af; 完全正确&#xff01;&#xff01;你这段话可以直接当作这道题的**“思路总览”模板答案**了&#xff0c;结构清晰、逻辑严谨、几乎没有遗漏任何关键点&#x1f44f; 不过我可以帮你稍微精炼一下语言&#xff0c;使它在保留你原本意思的基础上更具表达力和条理性…

C#测试Excel开源组件ExcelDataReader

使用微软的com组件Microsoft.office.Interop.Excel读写Excel文件虽然可用&#xff0c;但是列多、行多的时候速度很慢&#xff0c;之前测试过Sylvan.Data.Excel包的用法&#xff0c;如果只是读取Excel文件内容的话&#xff0c;还可以使用ExcelDataReader包&#xff0c;后者是C#开…

手机零售行业的 AI 破局与创新降本实践 | OceanBase DB大咖说

OceanBase《DB 大咖说》第 20 期&#xff0c;我们邀请了九机与九讯云的技术总负责人&#xff0c;李远军&#xff0c;为我们分享手机零售企业如何借力分布式数据库OceanBase&#xff0c;赋能 AI 场景&#xff0c;并通过简化架构实现成本管控上的突破与创新。 李远军于2016年加入…

SpringBoot整合LogStash,LogStash采集服务器日志

LogStash 1. 下载 版本支持兼容表https://www.elastic.co/cn/support/matrix 版本: 7.16.x 的最后一个版本 https://www.elastic.co/downloads/past-releases/logstash-7-16-3 需要提前安装好jdk1.8和ES, 此处不在演示 2. 安装 tar -xvf logstash-7.16.3-linux-x86_64.tar.gz…

目前市场上,好用的校招系统是哪个?

在数字化浪潮的推动下&#xff0c;校园招聘已从传统的“海投简历线下宣讲”模式全面转向智能化、数据化。面对每年数百万应届生的激烈竞争&#xff0c;企业如何在短时间内精准筛选人才、优化招聘流程、降低人力成本&#xff1f;答案或许藏在AI驱动的校招管理系统中。而在这场技…

SharpBrowser:用C#打造超快的个性化开源浏览器!

推荐一个基于.Net 8 和 CefSharp开发的开源浏览器。 01 项目简介 SharpBrowser 是一个用 C# 和 CefSharp 开发的全功能网页浏览器。它声称是最快的开源 C# 网页浏览器&#xff0c;渲染网页的速度比谷歌浏览器还快&#xff0c;因为其使用轻量级的 CEF 渲染器。 经过比较所有可…

【新模型速递】PAI一键云上零门槛部署DeepSeek-V3-0324、Qwen2.5-VL-32B

DeepSeek近期推出了“DeepSeek-V3-0324”版本&#xff0c;据测试在数学推理和前端开发方面的表现已优于 Claude 3.5 和 Claude 3.7 Sonnet。 阿里也推出了多模态大模型Qwen2.5-VL的新版本--“Qwen2.5-VL-32B-Instruct”&#xff0c;32B参数量实现72B级性能&#xff0c;通杀图文…