【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:

  • 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))returnonSelect(selectedEmoji!, selectedBackground!);clear();setIsEmojiModalOpen(false);};const clear = () => {setSelectedEmoji('')setSelectedBackground('')};return (<><Button onClick={() => setIsEmojiModalOpen(true)}> Emoj 表情</Button><Modaltitle="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: stringname: stringkeywords: string[]skins: Skins[]version: numberemoticons?: string[]
};type IEmojiPickerInnerProps = {emoji?: stringbackground?: stringonSelect?: (emoji: string, background: string) => voidclassName?: 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 iconsconst 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};// useEffectuseEffect(() => {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><inputtype="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 <divkey={`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 <divkey={`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 <divkey={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?: booleaniconType?: AppIconType | nullicon?: stringbackground?: string | nullimageUrl?: string | null | undefinedclassName?: stringinnerIcon?: React.ReactNodeonClick?: () => 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 <spanclassName={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.mzph.cn/pingmian/74136.shtml

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

相关文章

Qt中绘制不规则控件

在Qt中绘制不规则控件可通过设置遮罩&#xff08;Mask&#xff09;实现。以下是详细步骤: ‌继承目标控件‌&#xff1a;如QPushButton或QWidget。‌重写resizeEvent‌&#xff1a;当控件大小变化时&#xff0c;更新遮罩形状。‌创建遮罩区域‌&#xff1a;使用QRegion或QPain…

Parallel_Scheduling_of_DAGs_under_Memory_Constraints论文阅读

内存约束下的 DAG 并行调度 点击阅读原文语雀链接更清晰 摘要 科学工作流通常被建模为任务的有向无环图&#xff08;DAG&#xff09;&#xff0c;这些任务代表计算模块及其依赖关系&#xff0c;依赖关系表现为任务生成的数据被其他任务使用。这种形式化方法允许使用运行时系统&…

探索MVC、MVP、MVVM和DDD架构在不同编程语言中的实现差异

MVC与MVP/MVVM/DDD架构对比&#xff0c;不同语言实现 MVC 分层架构设计概述 模型-视图-控制器&#xff08;Model-View-Controller&#xff0c;简称 MVC&#xff09;是一种经典软件架构设计&#xff0c;通过分层解耦&#xff0c;使得系统结构清晰和易于维护&#xff0c;具有良…

一文读懂 UML:基础概念与体系框架

UML 图是一种标准化的建模语言&#xff0c;在软件开发和系统设计等领域有着广泛的应用。以下是对 UML 图各类图的详细介绍&#xff1a; 1.用例图 定义&#xff1a;用例图是从用户角度描述系统功能的模型图&#xff0c;展现了系统的参与者与用例之间的关系。作用&#xff1a;帮…

Spring 及 Spring Boot 条件化注解(15个)完整列表及示例

Spring 及 Spring Boot 条件化注解完整列表及示例 1. 所有条件化注解列表 Spring 和 Spring Boot 提供了以下条件化注解&#xff08;共 15 个&#xff09;&#xff0c;用于在配置类或方法上实现条件化注册 Bean 或配置&#xff1a; 注解名称作用来源框架Conditional自定义条件…

【Kafka】深入探讨 Kafka 如何保证一致性

文章目录 Kafka 基本概念回顾​副本角色​ 数据写入一致性​同步副本&#xff08;ISR&#xff09;集合​数据读取一致性​故障处理与一致性恢复​总结​ 在分布式系统领域&#xff0c;数据一致性是至关重要的一环。作为一款高性能的分布式消息队列系统&#xff0c;Kafka 在设计…

从入门到精通:SQL注入防御与攻防实战——红队如何突破,蓝队如何应对!

引言&#xff1a;为什么SQL注入攻击依然如此强大&#xff1f; SQL注入&#xff08;SQL Injection&#xff09;是最古老且最常见的Web应用漏洞之一。尽管很多公司和组织都已经采取了WAF、防火墙、数据库隔离等防护措施&#xff0c;但SQL注入依然在许多情况下能够突破防线&#…

【算法day27】有效的数独——请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。

36. 有效的数独 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例…

leetcode 2360. 图中的最长环 困难

给你一个 n 个节点的 有向图 &#xff0c;节点编号为 0 到 n - 1 &#xff0c;其中每个节点 至多 有一条出边。 图用一个大小为 n 下标从 0 开始的数组 edges 表示&#xff0c;节点 i 到节点 edges[i] 之间有一条有向边。如果节点 i 没有出边&#xff0c;那么 edges[i] -1 。…

PySpur: AI 智能体可视化开发平台

GitHub&#xff1a;https://github.com/PySpur-Dev/pyspur 更多AI开源软件&#xff1a;发现分享好用的AI工具、AI开源软件、AI模型、AI变现 - 小众AI PySpur是一个开源的轻量级可视化AI智能体工作流构建器&#xff0c;旨在简化AI系统的开发流程。通过拖拽式界面&#xff0c;用户…

vcpkg安装及使用教程,以安装matio库解析mat文件为例

vcpkg安装及使用教程,以安装matio库解析mat文件为例 1. vcpkg安装2 安装matio三方库3 将三方库集成到VS中3.1 全局集成3.2 集成到特定工程4 结语Vcpkg 是微软开发的一款开源的 C/C++ 包管理工具,旨在简化 C/C++ 项目依赖库的安装和管理。它支持跨平台(Windows、Linux、macO…

LLM架构解析:NLP基础(第一部分)—— 模型、核心技术与发展历程全解析

本专栏深入探究从循环神经网络&#xff08;RNN&#xff09;到Transformer等自然语言处理&#xff08;NLP&#xff09;模型的架构&#xff0c;以及基于这些模型构建的应用程序。 本系列文章内容&#xff1a; NLP自然语言处理基础&#xff08;本文&#xff09;词嵌入&#xff0…

【Rtklib入门指南】2. 使用RTKLIB GUI进行观测数据分析

数据准备 下载2025年1月1日的香港CORS站数据和观测星历&#xff0c;详情参照如下博客&#xff1a; 使用GAMP_GOOD进行hk数据下载教程-CSDN博客 分析工具 RTKLIB 2.4.3 demo5&#xff08;也可以选用RTKLIB2.4.2&#xff0c;但不建议使用RTKLIB2.4.3&#xff09; 分析流程 …

suse15 sp1使用华为云软件源yum源zypper源

登录suse15终端&#xff0c; cd /etc/zypp/repos.d/进入目录后执行以下命令&#xff1a; zypper ar -fcg https://mirrors.huaweicloud.com/opensuse/distribution/leap/15.1/repo/oss HuaWeiCloud:15.1:OSS zypper ar -fcg https://mirrors.huaweicloud.com/opensuse/distribu…

首屏加载时间优化解决

&#x1f916; 作者简介&#xff1a;水煮白菜王&#xff08;juejin/csdn同名&#xff09; &#xff0c;一位前端劝退师 &#x1f47b; &#x1f440; 文章专栏&#xff1a; 高德AMap专栏 &#xff0c;记录一下平时学习在博客写作中记录&#xff0c;总结出的一些开发技巧✍。 感…

Sentinel[超详细讲解]-1

定义一系列 规则 &#x1f47a;&#xff0c;对资源进行 保护 &#x1f47a;&#xff0c; 如果违反的了规则&#xff0c;则抛出异常&#xff0c;看是否有fallback兜底处理&#xff0c;如果没有则直接返回异常信息&#x1f60e; 1. 快速入门 1.1 引入 Sentinel 依赖 <depend…

02-Docker 使用

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

@DeclareParents 注解实现接口功能增强:Spring中通过接口引入实现功能增强的完整示例

以下是Spring中通过接口引入实现功能增强的完整示例&#xff1a; // 1. 目标接口及实现类 package com.example;public interface Service {void doSomething(); }Component class ServiceImp implements Service {Overridepublic void doSomething() {System.out.println(&qu…

HTML中数字和字母不换行显示

HTML中数字和字母不换行显示的默认行为及如何通过CSS的word-wrap和word-break属性进行调整。 在HTML中标签中的数字和字母默认是不换行的&#xff0c;如果要将他们换行&#xff0c;在CSS中添加”word-wrap: break-word;” 即可解决 语法&#xff1a;word-wrap: normal|break-w…

Git团队开发命令总结

简易Git工作流 myname: 团队成员个人分支dev: 团队公共分支 个人独立分支开发 同步最新的【dev公共分支】到本地。【重要】基于最新的【dev公共分支】&#xff0c;创建【个人功能开发分支】。在此基础上开发。【个人功能开发分支】开发完成&#xff0c;推送到远程库。如果【…