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/react
、antd
这些库直接用pnpm 、npm 、yarm
直接安装即可
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;
如果你有更多问题,欢迎随时问我!😊