mirror of
https://github.com/apache/superset.git
synced 2026-04-28 12:34:23 +00:00
Compare commits
1 Commits
feat/use-l
...
emojis-one
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9965abdb9 |
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { EmojiTextArea, type EmojiItem } from '.';
|
||||
|
||||
const meta: Meta<typeof EmojiTextArea> = {
|
||||
title: 'Components/EmojiTextArea',
|
||||
component: EmojiTextArea,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
A TextArea component with Slack-like emoji autocomplete.
|
||||
|
||||
## Features
|
||||
|
||||
- **Colon prefix trigger**: Type \`:sm\` to see smile emoji suggestions
|
||||
- **Minimum 2 characters**: Popup only shows after typing 2+ characters (configurable)
|
||||
- **Smart trigger detection**: Colon must be preceded by whitespace, start of line, or another emoji
|
||||
- **Prevents accidental selection**: Quick Enter keypress creates newline instead of selecting
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`tsx
|
||||
import { EmojiTextArea } from '@superset-ui/core/components';
|
||||
|
||||
<EmojiTextArea
|
||||
placeholder="Type :smile: to add emojis..."
|
||||
onChange={(text) => console.log(text)}
|
||||
onEmojiSelect={(emoji) => console.log('Selected:', emoji)}
|
||||
/>
|
||||
\`\`\`
|
||||
|
||||
## Trigger Behavior (Slack-like)
|
||||
|
||||
The emoji picker triggers in these scenarios:
|
||||
- \`:sm\` - at the start of text
|
||||
- \`hello :sm\` - after a space
|
||||
- \`😀:sm\` - after another emoji
|
||||
|
||||
It does NOT trigger in:
|
||||
- \`hello:sm\` - no space before colon
|
||||
- \`http://example.com\` - colon preceded by letter
|
||||
|
||||
Try it out below!
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
minCharsBeforePopup: {
|
||||
control: { type: 'number', min: 1, max: 5 },
|
||||
description: 'Minimum characters after colon before showing popup',
|
||||
defaultValue: 2,
|
||||
},
|
||||
maxSuggestions: {
|
||||
control: { type: 'number', min: 1, max: 20 },
|
||||
description: 'Maximum number of emoji suggestions to show',
|
||||
defaultValue: 10,
|
||||
},
|
||||
placeholder: {
|
||||
control: 'text',
|
||||
description: 'Placeholder text',
|
||||
},
|
||||
rows: {
|
||||
control: { type: 'number', min: 1, max: 20 },
|
||||
description: 'Number of visible rows',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EmojiTextArea>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
placeholder: 'Type :smile: or :thumbsup: to add emojis...',
|
||||
rows: 4,
|
||||
style: { width: '100%', maxWidth: 500 },
|
||||
},
|
||||
};
|
||||
|
||||
export const WithMinChars: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
minCharsBeforePopup: 3,
|
||||
placeholder: 'Requires 3 characters after colon (e.g., :smi)',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithMaxSuggestions: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
maxSuggestions: 5,
|
||||
placeholder: 'Shows max 5 suggestions',
|
||||
},
|
||||
};
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: function ControlledStory() {
|
||||
const [value, setValue] = useState('');
|
||||
const [selectedEmojis, setSelectedEmojis] = useState<EmojiItem[]>([]);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 500 }}>
|
||||
<EmojiTextArea
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onEmojiSelect={emoji => setSelectedEmojis(prev => [...prev, emoji])}
|
||||
placeholder="Type :smile: or :heart: to add emojis..."
|
||||
rows={4}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<strong>Current value:</strong>
|
||||
<pre
|
||||
style={{
|
||||
background: 'var(--ant-color-bg-container)',
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
border: '1px solid var(--ant-color-border)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{value || '(empty)'}
|
||||
</pre>
|
||||
</div>
|
||||
{selectedEmojis.length > 0 && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<strong>Selected emojis:</strong>
|
||||
<div style={{ fontSize: 24, marginTop: 8 }}>
|
||||
{selectedEmojis.map((e, i) => (
|
||||
<span key={i} title={`:${e.shortcode}:`}>
|
||||
{e.emoji}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const SlackBehaviorDemo: Story = {
|
||||
render: function SlackBehaviorDemoStory() {
|
||||
const examples = [
|
||||
{ input: ':sm', works: true, desc: 'Start of text' },
|
||||
{ input: 'hello :sm', works: true, desc: 'After space' },
|
||||
{
|
||||
input: '😀:sm',
|
||||
works: true,
|
||||
desc: 'After emoji',
|
||||
needsEmoji: true,
|
||||
},
|
||||
{ input: 'hello:sm', works: false, desc: 'No space before colon' },
|
||||
{ input: ':s', works: false, desc: 'Only 1 character' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 600 }}>
|
||||
<h3>Slack-like Trigger Behavior</h3>
|
||||
<p style={{ color: 'var(--ant-color-text-secondary)' }}>
|
||||
The emoji picker mimics Slack's behavior. Try these examples:
|
||||
</p>
|
||||
|
||||
<table
|
||||
style={{
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
padding: 8,
|
||||
borderBottom: '1px solid var(--ant-color-border)',
|
||||
}}
|
||||
>
|
||||
Input
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
padding: 8,
|
||||
borderBottom: '1px solid var(--ant-color-border)',
|
||||
}}
|
||||
>
|
||||
Shows Popup?
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
padding: 8,
|
||||
borderBottom: '1px solid var(--ant-color-border)',
|
||||
}}
|
||||
>
|
||||
Reason
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{examples.map((ex, i) => (
|
||||
<tr key={i}>
|
||||
<td
|
||||
style={{
|
||||
padding: 8,
|
||||
borderBottom: '1px solid var(--ant-color-border)',
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
>
|
||||
{ex.input}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: 8,
|
||||
borderBottom: '1px solid var(--ant-color-border)',
|
||||
}}
|
||||
>
|
||||
{ex.works ? '✅ Yes' : '❌ No'}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: 8,
|
||||
borderBottom: '1px solid var(--ant-color-border)',
|
||||
}}
|
||||
>
|
||||
{ex.desc}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<EmojiTextArea
|
||||
placeholder="Try the examples above..."
|
||||
rows={4}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const InForm: Story = {
|
||||
render: function InFormStory() {
|
||||
const [description, setDescription] = useState('');
|
||||
const [title, setTitle] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(`Title: ${title}\nDescription: ${description}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} style={{ maxWidth: 500 }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label htmlFor="title" style={{ display: 'block', marginBottom: 4 }}>
|
||||
Title
|
||||
</label>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
placeholder="Enter a title"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
border: '1px solid var(--ant-color-border)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label
|
||||
htmlFor="description"
|
||||
style={{ display: 'block', marginBottom: 4 }}
|
||||
>
|
||||
Description (with emoji support)
|
||||
</label>
|
||||
<EmojiTextArea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
placeholder="Add a description... use :smile: for emojis!"
|
||||
rows={4}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: 'var(--ant-color-primary)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, userEvent } from '@superset-ui/core/spec';
|
||||
import { EmojiTextArea } from '.';
|
||||
import { filterEmojis, EMOJI_DATA } from './emojiData';
|
||||
|
||||
test('renders EmojiTextArea with placeholder', () => {
|
||||
render(<EmojiTextArea placeholder="Type something..." />);
|
||||
expect(screen.getByPlaceholderText('Type something...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders EmojiTextArea as textarea element', () => {
|
||||
render(<EmojiTextArea placeholder="Type here" />);
|
||||
const textarea = screen.getByPlaceholderText('Type here');
|
||||
expect(textarea.tagName.toLowerCase()).toBe('textarea');
|
||||
});
|
||||
|
||||
test('allows typing in the textarea', async () => {
|
||||
render(<EmojiTextArea placeholder="Type here" />);
|
||||
const textarea = screen.getByPlaceholderText('Type here');
|
||||
await userEvent.type(textarea, 'Hello world');
|
||||
expect(textarea).toHaveValue('Hello world');
|
||||
});
|
||||
|
||||
test('calls onChange when typing', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(<EmojiTextArea placeholder="Type here" onChange={onChange} />);
|
||||
const textarea = screen.getByPlaceholderText('Type here');
|
||||
await userEvent.type(textarea, 'Hi');
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('passes through rows prop', () => {
|
||||
render(<EmojiTextArea placeholder="Type here" rows={5} />);
|
||||
const textarea = screen.getByPlaceholderText('Type here');
|
||||
expect(textarea).toHaveAttribute('rows', '5');
|
||||
});
|
||||
|
||||
test('forwards ref to underlying component', () => {
|
||||
const ref = { current: null };
|
||||
render(<EmojiTextArea ref={ref} placeholder="Type here" />);
|
||||
expect(ref.current).not.toBeNull();
|
||||
});
|
||||
|
||||
test('renders controlled component with value prop', () => {
|
||||
render(<EmojiTextArea value="Hello" onChange={() => {}} />);
|
||||
expect(screen.getByDisplayValue('Hello')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Unit tests for filterEmojis utility function
|
||||
// ============================================
|
||||
|
||||
test('filterEmojis returns matching emojis by shortcode', () => {
|
||||
const results = filterEmojis('smile');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].shortcode).toBe('smile');
|
||||
});
|
||||
|
||||
test('filterEmojis returns matching emojis by partial shortcode', () => {
|
||||
const results = filterEmojis('sm');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
// Should include smile, smirk, etc.
|
||||
expect(results.some(e => e.shortcode.includes('sm'))).toBe(true);
|
||||
});
|
||||
|
||||
test('filterEmojis returns matching emojis by keyword', () => {
|
||||
const results = filterEmojis('happy');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
// Should include emojis with 'happy' keyword
|
||||
expect(results.some(e => e.keywords?.includes('happy'))).toBe(true);
|
||||
});
|
||||
|
||||
test('filterEmojis is case insensitive', () => {
|
||||
const results1 = filterEmojis('SMILE');
|
||||
const results2 = filterEmojis('smile');
|
||||
expect(results1.length).toBe(results2.length);
|
||||
expect(results1[0].shortcode).toBe(results2[0].shortcode);
|
||||
});
|
||||
|
||||
test('filterEmojis respects limit parameter', () => {
|
||||
const results = filterEmojis('a', 5);
|
||||
expect(results.length).toBeLessThanOrEqual(5);
|
||||
});
|
||||
|
||||
test('filterEmojis returns empty array for empty search', () => {
|
||||
const results = filterEmojis('');
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
test('filterEmojis returns empty array for no matches', () => {
|
||||
const results = filterEmojis('zzzznotanemoji');
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Unit tests for EMOJI_DATA
|
||||
// ============================================
|
||||
|
||||
test('EMOJI_DATA contains expected smileys', () => {
|
||||
const smile = EMOJI_DATA.find(e => e.shortcode === 'smile');
|
||||
expect(smile).toBeDefined();
|
||||
expect(smile?.emoji).toBe('😄');
|
||||
|
||||
const joy = EMOJI_DATA.find(e => e.shortcode === 'joy');
|
||||
expect(joy).toBeDefined();
|
||||
expect(joy?.emoji).toBe('😂');
|
||||
});
|
||||
|
||||
test('EMOJI_DATA contains expected gestures', () => {
|
||||
const thumbsup = EMOJI_DATA.find(e => e.shortcode === 'thumbsup');
|
||||
expect(thumbsup).toBeDefined();
|
||||
expect(thumbsup?.emoji).toBe('👍');
|
||||
|
||||
const clap = EMOJI_DATA.find(e => e.shortcode === 'clap');
|
||||
expect(clap).toBeDefined();
|
||||
expect(clap?.emoji).toBe('👏');
|
||||
});
|
||||
|
||||
test('EMOJI_DATA contains expected symbols', () => {
|
||||
const heart = EMOJI_DATA.find(e => e.shortcode === 'heart');
|
||||
expect(heart).toBeDefined();
|
||||
expect(heart?.emoji).toBe('❤️');
|
||||
|
||||
const fire = EMOJI_DATA.find(e => e.shortcode === 'fire');
|
||||
expect(fire).toBeDefined();
|
||||
expect(fire?.emoji).toBe('🔥');
|
||||
|
||||
const checkmark = EMOJI_DATA.find(e => e.shortcode === 'white_check_mark');
|
||||
expect(checkmark).toBeDefined();
|
||||
expect(checkmark?.emoji).toBe('✅');
|
||||
});
|
||||
|
||||
test('EMOJI_DATA items have required properties', () => {
|
||||
EMOJI_DATA.forEach(item => {
|
||||
expect(item).toHaveProperty('shortcode');
|
||||
expect(item).toHaveProperty('emoji');
|
||||
expect(typeof item.shortcode).toBe('string');
|
||||
expect(typeof item.emoji).toBe('string');
|
||||
expect(item.shortcode.length).toBeGreaterThan(0);
|
||||
expect(item.emoji.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('EMOJI_DATA shortcodes are unique', () => {
|
||||
const shortcodes = EMOJI_DATA.map(e => e.shortcode);
|
||||
const uniqueShortcodes = new Set(shortcodes);
|
||||
expect(uniqueShortcodes.size).toBe(shortcodes.length);
|
||||
});
|
||||
|
||||
test('EMOJI_DATA has a reasonable number of emojis', () => {
|
||||
// Ensure we have a substantial emoji set
|
||||
expect(EMOJI_DATA.length).toBeGreaterThan(100);
|
||||
});
|
||||
@@ -0,0 +1,569 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export interface EmojiItem {
|
||||
shortcode: string;
|
||||
emoji: string;
|
||||
keywords?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Common emoji data with shortcodes.
|
||||
* This is a curated subset of emojis commonly used in Slack-like applications.
|
||||
* Can be extended or replaced with a more comprehensive emoji library.
|
||||
*/
|
||||
export const EMOJI_DATA: EmojiItem[] = [
|
||||
// Smileys & Emotion
|
||||
{ shortcode: 'smile', emoji: '😄', keywords: ['happy', 'joy', 'glad'] },
|
||||
{ shortcode: 'smiley', emoji: '😃', keywords: ['happy', 'joy'] },
|
||||
{ shortcode: 'grinning', emoji: '😀', keywords: ['happy', 'smile'] },
|
||||
{ shortcode: 'blush', emoji: '😊', keywords: ['happy', 'shy', 'smile'] },
|
||||
{ shortcode: 'wink', emoji: '😉', keywords: ['flirt'] },
|
||||
{
|
||||
shortcode: 'heart_eyes',
|
||||
emoji: '😍',
|
||||
keywords: ['love', 'crush', 'adore'],
|
||||
},
|
||||
{ shortcode: 'kissing_heart', emoji: '😘', keywords: ['love', 'kiss'] },
|
||||
{ shortcode: 'laughing', emoji: '😆', keywords: ['happy', 'haha', 'lol'] },
|
||||
{ shortcode: 'sweat_smile', emoji: '😅', keywords: ['nervous', 'phew'] },
|
||||
{ shortcode: 'joy', emoji: '😂', keywords: ['tears', 'laugh', 'lol', 'lmao'] },
|
||||
{
|
||||
shortcode: 'rofl',
|
||||
emoji: '🤣',
|
||||
keywords: ['rolling', 'laugh', 'lol', 'lmao'],
|
||||
},
|
||||
{ shortcode: 'relaxed', emoji: '☺️', keywords: ['calm', 'peace'] },
|
||||
{ shortcode: 'yum', emoji: '😋', keywords: ['tasty', 'delicious'] },
|
||||
{ shortcode: 'relieved', emoji: '😌', keywords: ['calm', 'peaceful'] },
|
||||
{ shortcode: 'sunglasses', emoji: '😎', keywords: ['cool', 'awesome'] },
|
||||
{ shortcode: 'smirk', emoji: '😏', keywords: ['sly', 'confident'] },
|
||||
{ shortcode: 'neutral_face', emoji: '😐', keywords: ['meh', 'blank'] },
|
||||
{ shortcode: 'expressionless', emoji: '😑', keywords: ['blank', 'meh'] },
|
||||
{ shortcode: 'unamused', emoji: '😒', keywords: ['bored', 'meh'] },
|
||||
{ shortcode: 'sweat', emoji: '😓', keywords: ['nervous', 'worried'] },
|
||||
{ shortcode: 'pensive', emoji: '😔', keywords: ['sad', 'thoughtful'] },
|
||||
{ shortcode: 'confused', emoji: '😕', keywords: ['puzzled', 'unsure'] },
|
||||
{ shortcode: 'upside_down', emoji: '🙃', keywords: ['silly', 'sarcasm'] },
|
||||
{ shortcode: 'thinking', emoji: '🤔', keywords: ['ponder', 'hmm'] },
|
||||
{ shortcode: 'zipper_mouth', emoji: '🤐', keywords: ['secret', 'quiet'] },
|
||||
{ shortcode: 'raised_eyebrow', emoji: '🤨', keywords: ['skeptical', 'doubt'] },
|
||||
{ shortcode: 'rolling_eyes', emoji: '🙄', keywords: ['annoyed', 'whatever'] },
|
||||
{ shortcode: 'grimacing', emoji: '😬', keywords: ['awkward', 'nervous'] },
|
||||
{ shortcode: 'lying_face', emoji: '🤥', keywords: ['liar', 'pinocchio'] },
|
||||
{ shortcode: 'shushing', emoji: '🤫', keywords: ['quiet', 'secret'] },
|
||||
{ shortcode: 'hand_over_mouth', emoji: '🤭', keywords: ['oops', 'giggle'] },
|
||||
{ shortcode: 'face_vomiting', emoji: '🤮', keywords: ['sick', 'gross'] },
|
||||
{ shortcode: 'exploding_head', emoji: '🤯', keywords: ['mind', 'blown'] },
|
||||
{ shortcode: 'cowboy', emoji: '🤠', keywords: ['western', 'yeehaw'] },
|
||||
{ shortcode: 'partying', emoji: '🥳', keywords: ['party', 'celebration'] },
|
||||
{ shortcode: 'star_struck', emoji: '🤩', keywords: ['excited', 'amazed'] },
|
||||
{ shortcode: 'sleeping', emoji: '😴', keywords: ['zzz', 'tired'] },
|
||||
{ shortcode: 'drooling', emoji: '🤤', keywords: ['hungry', 'want'] },
|
||||
{ shortcode: 'sleepy', emoji: '😪', keywords: ['tired', 'zzz'] },
|
||||
{ shortcode: 'mask', emoji: '😷', keywords: ['sick', 'covid'] },
|
||||
{ shortcode: 'nerd', emoji: '🤓', keywords: ['geek', 'smart'] },
|
||||
{ shortcode: 'monocle', emoji: '🧐', keywords: ['curious', 'inspect'] },
|
||||
{ shortcode: 'worried', emoji: '😟', keywords: ['concerned', 'anxious'] },
|
||||
{ shortcode: 'frowning', emoji: '🙁', keywords: ['sad', 'unhappy'] },
|
||||
{ shortcode: 'open_mouth', emoji: '😮', keywords: ['surprised', 'wow'] },
|
||||
{ shortcode: 'hushed', emoji: '😯', keywords: ['surprised', 'quiet'] },
|
||||
{ shortcode: 'astonished', emoji: '😲', keywords: ['shocked', 'wow'] },
|
||||
{ shortcode: 'flushed', emoji: '😳', keywords: ['embarrassed', 'shy'] },
|
||||
{ shortcode: 'pleading', emoji: '🥺', keywords: ['puppy', 'please'] },
|
||||
{ shortcode: 'cry', emoji: '😢', keywords: ['sad', 'tear'] },
|
||||
{ shortcode: 'sob', emoji: '😭', keywords: ['crying', 'sad', 'tears'] },
|
||||
{ shortcode: 'scream', emoji: '😱', keywords: ['scared', 'horror'] },
|
||||
{ shortcode: 'confounded', emoji: '😖', keywords: ['frustrated'] },
|
||||
{ shortcode: 'persevere', emoji: '😣', keywords: ['struggling'] },
|
||||
{ shortcode: 'disappointed', emoji: '😞', keywords: ['sad', 'let down'] },
|
||||
{ shortcode: 'fearful', emoji: '😨', keywords: ['scared', 'afraid'] },
|
||||
{ shortcode: 'cold_sweat', emoji: '😰', keywords: ['nervous', 'anxious'] },
|
||||
{ shortcode: 'weary', emoji: '😩', keywords: ['tired', 'exhausted'] },
|
||||
{ shortcode: 'tired_face', emoji: '😫', keywords: ['exhausted'] },
|
||||
{ shortcode: 'angry', emoji: '😠', keywords: ['mad', 'grumpy'] },
|
||||
{ shortcode: 'rage', emoji: '😡', keywords: ['angry', 'furious'] },
|
||||
{ shortcode: 'triumph', emoji: '😤', keywords: ['proud', 'huffing'] },
|
||||
{ shortcode: 'skull', emoji: '💀', keywords: ['dead', 'death'] },
|
||||
{ shortcode: 'poop', emoji: '💩', keywords: ['crap', 'shit'] },
|
||||
{ shortcode: 'clown', emoji: '🤡', keywords: ['funny', 'circus'] },
|
||||
{ shortcode: 'imp', emoji: '👿', keywords: ['devil', 'evil'] },
|
||||
{ shortcode: 'ghost', emoji: '👻', keywords: ['boo', 'spooky'] },
|
||||
{ shortcode: 'alien', emoji: '👽', keywords: ['ufo', 'space'] },
|
||||
{ shortcode: 'robot', emoji: '🤖', keywords: ['bot', 'machine'] },
|
||||
{ shortcode: 'cat', emoji: '😺', keywords: ['kitty', 'meow'] },
|
||||
{ shortcode: 'heart_eyes_cat', emoji: '😻', keywords: ['love', 'cat'] },
|
||||
{ shortcode: 'joy_cat', emoji: '😹', keywords: ['laugh', 'cat'] },
|
||||
{ shortcode: 'crying_cat', emoji: '😿', keywords: ['sad', 'cat'] },
|
||||
{ shortcode: 'pouting_cat', emoji: '😾', keywords: ['angry', 'cat'] },
|
||||
{ shortcode: 'see_no_evil', emoji: '🙈', keywords: ['monkey', 'shy'] },
|
||||
{ shortcode: 'hear_no_evil', emoji: '🙉', keywords: ['monkey'] },
|
||||
{ shortcode: 'speak_no_evil', emoji: '🙊', keywords: ['monkey', 'secret'] },
|
||||
|
||||
// Gestures & Body
|
||||
{ shortcode: 'wave', emoji: '👋', keywords: ['hello', 'bye', 'hi'] },
|
||||
{ shortcode: 'raised_hand', emoji: '✋', keywords: ['stop', 'high five'] },
|
||||
{ shortcode: 'ok_hand', emoji: '👌', keywords: ['perfect', 'nice'] },
|
||||
{ shortcode: 'pinching_hand', emoji: '🤏', keywords: ['small', 'tiny'] },
|
||||
{ shortcode: 'v', emoji: '✌️', keywords: ['peace', 'victory'] },
|
||||
{ shortcode: 'crossed_fingers', emoji: '🤞', keywords: ['luck', 'hope'] },
|
||||
{ shortcode: 'love_you', emoji: '🤟', keywords: ['ily', 'sign'] },
|
||||
{ shortcode: 'metal', emoji: '🤘', keywords: ['rock', 'horns'] },
|
||||
{ shortcode: 'call_me', emoji: '🤙', keywords: ['phone', 'shaka'] },
|
||||
{ shortcode: 'point_left', emoji: '👈', keywords: ['direction'] },
|
||||
{ shortcode: 'point_right', emoji: '👉', keywords: ['direction'] },
|
||||
{ shortcode: 'point_up', emoji: '👆', keywords: ['direction'] },
|
||||
{ shortcode: 'point_down', emoji: '👇', keywords: ['direction'] },
|
||||
{ shortcode: 'middle_finger', emoji: '🖕', keywords: ['flip', 'rude'] },
|
||||
{ shortcode: 'thumbsup', emoji: '👍', keywords: ['yes', 'good', '+1'] },
|
||||
{ shortcode: 'thumbsdown', emoji: '👎', keywords: ['no', 'bad', '-1'] },
|
||||
{ shortcode: 'fist', emoji: '✊', keywords: ['power', 'punch'] },
|
||||
{ shortcode: 'punch', emoji: '👊', keywords: ['fist', 'bump'] },
|
||||
{ shortcode: 'clap', emoji: '👏', keywords: ['applause', 'bravo'] },
|
||||
{ shortcode: 'raised_hands', emoji: '🙌', keywords: ['celebration', 'yay'] },
|
||||
{ shortcode: 'open_hands', emoji: '👐', keywords: ['hug', 'open'] },
|
||||
{ shortcode: 'palms_up', emoji: '🤲', keywords: ['prayer', 'request'] },
|
||||
{ shortcode: 'handshake', emoji: '🤝', keywords: ['deal', 'agreement'] },
|
||||
{ shortcode: 'pray', emoji: '🙏', keywords: ['please', 'thanks', 'namaste'] },
|
||||
{ shortcode: 'writing', emoji: '✍️', keywords: ['write', 'pen'] },
|
||||
{ shortcode: 'nail_care', emoji: '💅', keywords: ['nails', 'fabulous'] },
|
||||
{ shortcode: 'selfie', emoji: '🤳', keywords: ['photo', 'camera'] },
|
||||
{ shortcode: 'muscle', emoji: '💪', keywords: ['strong', 'flex', 'bicep'] },
|
||||
{ shortcode: 'leg', emoji: '🦵', keywords: ['kick'] },
|
||||
{ shortcode: 'foot', emoji: '🦶', keywords: ['kick', 'step'] },
|
||||
{ shortcode: 'ear', emoji: '👂', keywords: ['listen', 'hear'] },
|
||||
{ shortcode: 'nose', emoji: '👃', keywords: ['smell', 'sniff'] },
|
||||
{ shortcode: 'brain', emoji: '🧠', keywords: ['think', 'smart'] },
|
||||
{ shortcode: 'eyes', emoji: '👀', keywords: ['look', 'see', 'watch'] },
|
||||
{ shortcode: 'eye', emoji: '👁️', keywords: ['look', 'see'] },
|
||||
{ shortcode: 'tongue', emoji: '👅', keywords: ['taste', 'lick'] },
|
||||
{ shortcode: 'lips', emoji: '👄', keywords: ['mouth', 'kiss'] },
|
||||
{ shortcode: 'baby', emoji: '👶', keywords: ['child', 'infant'] },
|
||||
{ shortcode: 'person', emoji: '🧑', keywords: ['human', 'adult'] },
|
||||
{ shortcode: 'man', emoji: '👨', keywords: ['male', 'guy'] },
|
||||
{ shortcode: 'woman', emoji: '👩', keywords: ['female', 'lady'] },
|
||||
{ shortcode: 'older_person', emoji: '🧓', keywords: ['senior', 'elderly'] },
|
||||
|
||||
// Hearts & Love
|
||||
{ shortcode: 'heart', emoji: '❤️', keywords: ['love', 'red'] },
|
||||
{ shortcode: 'orange_heart', emoji: '🧡', keywords: ['love'] },
|
||||
{ shortcode: 'yellow_heart', emoji: '💛', keywords: ['love'] },
|
||||
{ shortcode: 'green_heart', emoji: '💚', keywords: ['love'] },
|
||||
{ shortcode: 'blue_heart', emoji: '💙', keywords: ['love'] },
|
||||
{ shortcode: 'purple_heart', emoji: '💜', keywords: ['love'] },
|
||||
{ shortcode: 'black_heart', emoji: '🖤', keywords: ['love', 'dark'] },
|
||||
{ shortcode: 'white_heart', emoji: '🤍', keywords: ['love', 'pure'] },
|
||||
{ shortcode: 'brown_heart', emoji: '🤎', keywords: ['love'] },
|
||||
{ shortcode: 'broken_heart', emoji: '💔', keywords: ['sad', 'heartbreak'] },
|
||||
{ shortcode: 'heartbeat', emoji: '💓', keywords: ['love', 'pulse'] },
|
||||
{ shortcode: 'heartpulse', emoji: '💗', keywords: ['love', 'growing'] },
|
||||
{ shortcode: 'two_hearts', emoji: '💕', keywords: ['love', 'romance'] },
|
||||
{ shortcode: 'revolving_hearts', emoji: '💞', keywords: ['love'] },
|
||||
{ shortcode: 'cupid', emoji: '💘', keywords: ['love', 'arrow'] },
|
||||
{ shortcode: 'sparkling_heart', emoji: '💖', keywords: ['love', 'sparkle'] },
|
||||
{ shortcode: 'gift_heart', emoji: '💝', keywords: ['love', 'valentine'] },
|
||||
{ shortcode: 'heart_decoration', emoji: '💟', keywords: ['love'] },
|
||||
{ shortcode: 'kiss', emoji: '💋', keywords: ['love', 'lips'] },
|
||||
{ shortcode: 'love_letter', emoji: '💌', keywords: ['email', 'message'] },
|
||||
|
||||
// Symbols & Objects
|
||||
{ shortcode: 'fire', emoji: '🔥', keywords: ['hot', 'lit', 'flame'] },
|
||||
{ shortcode: 'star', emoji: '⭐', keywords: ['favorite', 'rating'] },
|
||||
{ shortcode: 'sparkles', emoji: '✨', keywords: ['shiny', 'new', 'magic'] },
|
||||
{ shortcode: 'zap', emoji: '⚡', keywords: ['lightning', 'power'] },
|
||||
{ shortcode: 'boom', emoji: '💥', keywords: ['explosion', 'collision'] },
|
||||
{ shortcode: 'dizzy', emoji: '💫', keywords: ['star', 'dazed'] },
|
||||
{ shortcode: 'speech_balloon', emoji: '💬', keywords: ['talk', 'chat'] },
|
||||
{ shortcode: 'thought_balloon', emoji: '💭', keywords: ['think', 'idea'] },
|
||||
{ shortcode: 'zzz', emoji: '💤', keywords: ['sleep', 'tired'] },
|
||||
{ shortcode: 'wave_emoji', emoji: '🌊', keywords: ['ocean', 'water'] },
|
||||
{ shortcode: 'droplet', emoji: '💧', keywords: ['water', 'sweat'] },
|
||||
{ shortcode: 'sweat_drops', emoji: '💦', keywords: ['water', 'splash'] },
|
||||
{ shortcode: 'dash', emoji: '💨', keywords: ['wind', 'running'] },
|
||||
{ shortcode: 'hole', emoji: '🕳️', keywords: ['empty', 'void'] },
|
||||
{ shortcode: 'bomb', emoji: '💣', keywords: ['explosive', 'danger'] },
|
||||
{ shortcode: 'money', emoji: '💰', keywords: ['bag', 'cash', 'dollar'] },
|
||||
{ shortcode: 'dollar', emoji: '💵', keywords: ['money', 'cash'] },
|
||||
{ shortcode: 'gem', emoji: '💎', keywords: ['diamond', 'jewel'] },
|
||||
{ shortcode: 'bulb', emoji: '💡', keywords: ['idea', 'light'] },
|
||||
{ shortcode: 'bell', emoji: '🔔', keywords: ['notification', 'alert'] },
|
||||
{ shortcode: 'loudspeaker', emoji: '📢', keywords: ['announce'] },
|
||||
{ shortcode: 'mega', emoji: '📣', keywords: ['megaphone', 'announce'] },
|
||||
{ shortcode: 'lock', emoji: '🔒', keywords: ['secure', 'closed'] },
|
||||
{ shortcode: 'unlock', emoji: '🔓', keywords: ['open', 'access'] },
|
||||
{ shortcode: 'key', emoji: '🔑', keywords: ['password', 'access'] },
|
||||
{ shortcode: 'magnifying_glass', emoji: '🔍', keywords: ['search', 'find'] },
|
||||
{ shortcode: 'link', emoji: '🔗', keywords: ['chain', 'url'] },
|
||||
{ shortcode: 'paperclip', emoji: '📎', keywords: ['attach'] },
|
||||
{ shortcode: 'scissors', emoji: '✂️', keywords: ['cut', 'snip'] },
|
||||
{ shortcode: 'hammer', emoji: '🔨', keywords: ['tool', 'build'] },
|
||||
{ shortcode: 'wrench', emoji: '🔧', keywords: ['tool', 'fix'] },
|
||||
{ shortcode: 'gear', emoji: '⚙️', keywords: ['settings', 'cog'] },
|
||||
{ shortcode: 'shield', emoji: '🛡️', keywords: ['protect', 'security'] },
|
||||
{ shortcode: 'trophy', emoji: '🏆', keywords: ['win', 'first', 'award'] },
|
||||
{ shortcode: 'medal', emoji: '🏅', keywords: ['award', 'sports'] },
|
||||
{ shortcode: 'first_place', emoji: '🥇', keywords: ['gold', 'winner'] },
|
||||
{ shortcode: 'second_place', emoji: '🥈', keywords: ['silver'] },
|
||||
{ shortcode: 'third_place', emoji: '🥉', keywords: ['bronze'] },
|
||||
{ shortcode: 'soccer', emoji: '⚽', keywords: ['football', 'sports'] },
|
||||
{ shortcode: 'basketball', emoji: '🏀', keywords: ['sports', 'ball'] },
|
||||
{ shortcode: 'football', emoji: '🏈', keywords: ['sports', 'american'] },
|
||||
{ shortcode: 'baseball', emoji: '⚾', keywords: ['sports', 'ball'] },
|
||||
{ shortcode: 'tennis', emoji: '🎾', keywords: ['sports', 'ball'] },
|
||||
{ shortcode: 'dart', emoji: '🎯', keywords: ['target', 'bullseye'] },
|
||||
{ shortcode: 'video_game', emoji: '🎮', keywords: ['gaming', 'controller'] },
|
||||
{ shortcode: 'slot_machine', emoji: '🎰', keywords: ['gambling', 'casino'] },
|
||||
{ shortcode: 'game_die', emoji: '🎲', keywords: ['dice', 'random'] },
|
||||
{ shortcode: 'jigsaw', emoji: '🧩', keywords: ['puzzle', 'piece'] },
|
||||
{ shortcode: 'art', emoji: '🎨', keywords: ['palette', 'paint'] },
|
||||
{ shortcode: 'performing_arts', emoji: '🎭', keywords: ['theater', 'drama'] },
|
||||
{ shortcode: 'microphone', emoji: '🎤', keywords: ['sing', 'karaoke'] },
|
||||
{ shortcode: 'headphones', emoji: '🎧', keywords: ['music', 'audio'] },
|
||||
{ shortcode: 'musical_note', emoji: '🎵', keywords: ['music', 'song'] },
|
||||
{ shortcode: 'notes', emoji: '🎶', keywords: ['music', 'melody'] },
|
||||
{ shortcode: 'guitar', emoji: '🎸', keywords: ['music', 'rock'] },
|
||||
{ shortcode: 'piano', emoji: '🎹', keywords: ['music', 'keys'] },
|
||||
{ shortcode: 'drum', emoji: '🥁', keywords: ['music', 'beat'] },
|
||||
{ shortcode: 'trumpet', emoji: '🎺', keywords: ['music', 'brass'] },
|
||||
{ shortcode: 'violin', emoji: '🎻', keywords: ['music', 'string'] },
|
||||
{ shortcode: 'movie_camera', emoji: '🎥', keywords: ['film', 'video'] },
|
||||
{ shortcode: 'camera', emoji: '📷', keywords: ['photo', 'picture'] },
|
||||
{ shortcode: 'tv', emoji: '📺', keywords: ['television', 'watch'] },
|
||||
{ shortcode: 'computer', emoji: '💻', keywords: ['laptop', 'pc'] },
|
||||
{ shortcode: 'keyboard', emoji: '⌨️', keywords: ['type', 'computer'] },
|
||||
{ shortcode: 'phone', emoji: '📱', keywords: ['mobile', 'cell'] },
|
||||
{ shortcode: 'email', emoji: '📧', keywords: ['mail', 'message'] },
|
||||
{ shortcode: 'inbox', emoji: '📥', keywords: ['mail', 'receive'] },
|
||||
{ shortcode: 'outbox', emoji: '📤', keywords: ['mail', 'send'] },
|
||||
{ shortcode: 'package', emoji: '📦', keywords: ['box', 'delivery'] },
|
||||
{ shortcode: 'memo', emoji: '📝', keywords: ['note', 'write'] },
|
||||
{ shortcode: 'page', emoji: '📄', keywords: ['document', 'file'] },
|
||||
{ shortcode: 'bookmark', emoji: '🔖', keywords: ['save', 'tag'] },
|
||||
{ shortcode: 'book', emoji: '📖', keywords: ['read', 'open'] },
|
||||
{ shortcode: 'books', emoji: '📚', keywords: ['library', 'study'] },
|
||||
{ shortcode: 'newspaper', emoji: '📰', keywords: ['news', 'article'] },
|
||||
{ shortcode: 'calendar', emoji: '📅', keywords: ['date', 'schedule'] },
|
||||
{ shortcode: 'chart', emoji: '📈', keywords: ['graph', 'increase'] },
|
||||
{ shortcode: 'chart_down', emoji: '📉', keywords: ['graph', 'decrease'] },
|
||||
{ shortcode: 'bar_chart', emoji: '📊', keywords: ['graph', 'stats'] },
|
||||
{ shortcode: 'clipboard', emoji: '📋', keywords: ['list', 'todo'] },
|
||||
{ shortcode: 'pushpin', emoji: '📌', keywords: ['pin', 'location'] },
|
||||
{ shortcode: 'round_pushpin', emoji: '📍', keywords: ['pin', 'location'] },
|
||||
{ shortcode: 'triangular_ruler', emoji: '📐', keywords: ['math', 'measure'] },
|
||||
{ shortcode: 'straight_ruler', emoji: '📏', keywords: ['math', 'measure'] },
|
||||
{ shortcode: 'pencil', emoji: '✏️', keywords: ['write', 'draw'] },
|
||||
{ shortcode: 'pen', emoji: '🖊️', keywords: ['write', 'sign'] },
|
||||
{ shortcode: 'crayon', emoji: '🖍️', keywords: ['draw', 'color'] },
|
||||
{ shortcode: 'paintbrush', emoji: '🖌️', keywords: ['art', 'paint'] },
|
||||
{ shortcode: 'folder', emoji: '📁', keywords: ['file', 'directory'] },
|
||||
{ shortcode: 'open_folder', emoji: '📂', keywords: ['file', 'directory'] },
|
||||
|
||||
// Nature & Animals
|
||||
{ shortcode: 'dog', emoji: '🐶', keywords: ['puppy', 'pet', 'woof'] },
|
||||
{ shortcode: 'cat_face', emoji: '🐱', keywords: ['kitty', 'pet', 'meow'] },
|
||||
{ shortcode: 'mouse', emoji: '🐭', keywords: ['rodent'] },
|
||||
{ shortcode: 'hamster', emoji: '🐹', keywords: ['pet', 'rodent'] },
|
||||
{ shortcode: 'rabbit', emoji: '🐰', keywords: ['bunny', 'pet'] },
|
||||
{ shortcode: 'fox', emoji: '🦊', keywords: ['animal'] },
|
||||
{ shortcode: 'bear', emoji: '🐻', keywords: ['animal'] },
|
||||
{ shortcode: 'panda', emoji: '🐼', keywords: ['animal', 'cute'] },
|
||||
{ shortcode: 'koala', emoji: '🐨', keywords: ['animal', 'australia'] },
|
||||
{ shortcode: 'tiger', emoji: '🐯', keywords: ['animal', 'cat'] },
|
||||
{ shortcode: 'lion', emoji: '🦁', keywords: ['animal', 'king'] },
|
||||
{ shortcode: 'cow', emoji: '🐮', keywords: ['animal', 'farm'] },
|
||||
{ shortcode: 'pig', emoji: '🐷', keywords: ['animal', 'farm'] },
|
||||
{ shortcode: 'frog', emoji: '🐸', keywords: ['animal', 'toad'] },
|
||||
{ shortcode: 'monkey_face', emoji: '🐵', keywords: ['animal', 'ape'] },
|
||||
{ shortcode: 'chicken', emoji: '🐔', keywords: ['animal', 'farm', 'hen'] },
|
||||
{ shortcode: 'penguin', emoji: '🐧', keywords: ['animal', 'bird'] },
|
||||
{ shortcode: 'bird', emoji: '🐦', keywords: ['animal', 'fly'] },
|
||||
{ shortcode: 'eagle', emoji: '🦅', keywords: ['animal', 'bird'] },
|
||||
{ shortcode: 'duck', emoji: '🦆', keywords: ['animal', 'bird', 'quack'] },
|
||||
{ shortcode: 'owl', emoji: '🦉', keywords: ['animal', 'bird', 'night'] },
|
||||
{ shortcode: 'bat', emoji: '🦇', keywords: ['animal', 'night', 'vampire'] },
|
||||
{ shortcode: 'wolf', emoji: '🐺', keywords: ['animal'] },
|
||||
{ shortcode: 'horse', emoji: '🐴', keywords: ['animal'] },
|
||||
{ shortcode: 'unicorn', emoji: '🦄', keywords: ['animal', 'magic'] },
|
||||
{ shortcode: 'bee', emoji: '🐝', keywords: ['insect', 'honey'] },
|
||||
{ shortcode: 'bug', emoji: '🐛', keywords: ['insect', 'caterpillar'] },
|
||||
{ shortcode: 'butterfly', emoji: '🦋', keywords: ['insect', 'pretty'] },
|
||||
{ shortcode: 'snail', emoji: '🐌', keywords: ['slow'] },
|
||||
{ shortcode: 'lady_beetle', emoji: '🐞', keywords: ['insect', 'bug'] },
|
||||
{ shortcode: 'ant', emoji: '🐜', keywords: ['insect', 'bug'] },
|
||||
{ shortcode: 'spider', emoji: '🕷️', keywords: ['insect', 'scary'] },
|
||||
{ shortcode: 'turtle', emoji: '🐢', keywords: ['animal', 'slow'] },
|
||||
{ shortcode: 'snake', emoji: '🐍', keywords: ['animal', 'reptile'] },
|
||||
{ shortcode: 'dragon', emoji: '🐲', keywords: ['animal', 'mythical'] },
|
||||
{ shortcode: 'dinosaur', emoji: '🦕', keywords: ['animal', 'extinct'] },
|
||||
{ shortcode: 't_rex', emoji: '🦖', keywords: ['animal', 'dinosaur'] },
|
||||
{ shortcode: 'whale', emoji: '🐳', keywords: ['animal', 'ocean'] },
|
||||
{ shortcode: 'dolphin', emoji: '🐬', keywords: ['animal', 'ocean'] },
|
||||
{ shortcode: 'fish', emoji: '🐟', keywords: ['animal', 'ocean'] },
|
||||
{ shortcode: 'tropical_fish', emoji: '🐠', keywords: ['animal', 'ocean'] },
|
||||
{ shortcode: 'shark', emoji: '🦈', keywords: ['animal', 'ocean'] },
|
||||
{ shortcode: 'octopus', emoji: '🐙', keywords: ['animal', 'ocean'] },
|
||||
{ shortcode: 'crab', emoji: '🦀', keywords: ['animal', 'ocean'] },
|
||||
{ shortcode: 'lobster', emoji: '🦞', keywords: ['animal', 'ocean'] },
|
||||
{ shortcode: 'shrimp', emoji: '🦐', keywords: ['animal', 'ocean'] },
|
||||
|
||||
// Plants & Nature
|
||||
{ shortcode: 'bouquet', emoji: '💐', keywords: ['flowers', 'gift'] },
|
||||
{ shortcode: 'cherry_blossom', emoji: '🌸', keywords: ['flower', 'spring'] },
|
||||
{ shortcode: 'rose', emoji: '🌹', keywords: ['flower', 'love'] },
|
||||
{ shortcode: 'tulip', emoji: '🌷', keywords: ['flower', 'spring'] },
|
||||
{ shortcode: 'sunflower', emoji: '🌻', keywords: ['flower', 'summer'] },
|
||||
{ shortcode: 'hibiscus', emoji: '🌺', keywords: ['flower', 'tropical'] },
|
||||
{ shortcode: 'seedling', emoji: '🌱', keywords: ['plant', 'grow'] },
|
||||
{ shortcode: 'evergreen_tree', emoji: '🌲', keywords: ['tree', 'pine'] },
|
||||
{ shortcode: 'deciduous_tree', emoji: '🌳', keywords: ['tree'] },
|
||||
{ shortcode: 'palm_tree', emoji: '🌴', keywords: ['tree', 'tropical'] },
|
||||
{ shortcode: 'cactus', emoji: '🌵', keywords: ['plant', 'desert'] },
|
||||
{ shortcode: 'herb', emoji: '🌿', keywords: ['plant', 'leaf'] },
|
||||
{ shortcode: 'shamrock', emoji: '☘️', keywords: ['clover', 'irish'] },
|
||||
{ shortcode: 'four_leaf_clover', emoji: '🍀', keywords: ['luck', 'irish'] },
|
||||
{ shortcode: 'maple_leaf', emoji: '🍁', keywords: ['fall', 'autumn'] },
|
||||
{ shortcode: 'fallen_leaf', emoji: '🍂', keywords: ['fall', 'autumn'] },
|
||||
{ shortcode: 'leaves', emoji: '🍃', keywords: ['leaf', 'wind'] },
|
||||
{ shortcode: 'mushroom', emoji: '🍄', keywords: ['fungus'] },
|
||||
|
||||
// Food & Drink
|
||||
{ shortcode: 'apple', emoji: '🍎', keywords: ['fruit', 'red'] },
|
||||
{ shortcode: 'green_apple', emoji: '🍏', keywords: ['fruit'] },
|
||||
{ shortcode: 'pear', emoji: '🍐', keywords: ['fruit'] },
|
||||
{ shortcode: 'orange', emoji: '🍊', keywords: ['fruit', 'citrus'] },
|
||||
{ shortcode: 'lemon', emoji: '🍋', keywords: ['fruit', 'citrus'] },
|
||||
{ shortcode: 'banana', emoji: '🍌', keywords: ['fruit'] },
|
||||
{ shortcode: 'watermelon', emoji: '🍉', keywords: ['fruit', 'summer'] },
|
||||
{ shortcode: 'grapes', emoji: '🍇', keywords: ['fruit', 'wine'] },
|
||||
{ shortcode: 'strawberry', emoji: '🍓', keywords: ['fruit', 'berry'] },
|
||||
{ shortcode: 'cherries', emoji: '🍒', keywords: ['fruit'] },
|
||||
{ shortcode: 'peach', emoji: '🍑', keywords: ['fruit'] },
|
||||
{ shortcode: 'mango', emoji: '🥭', keywords: ['fruit', 'tropical'] },
|
||||
{ shortcode: 'pineapple', emoji: '🍍', keywords: ['fruit', 'tropical'] },
|
||||
{ shortcode: 'coconut', emoji: '🥥', keywords: ['fruit', 'tropical'] },
|
||||
{ shortcode: 'avocado', emoji: '🥑', keywords: ['fruit', 'guacamole'] },
|
||||
{ shortcode: 'tomato', emoji: '🍅', keywords: ['vegetable', 'red'] },
|
||||
{ shortcode: 'eggplant', emoji: '🍆', keywords: ['vegetable', 'purple'] },
|
||||
{ shortcode: 'potato', emoji: '🥔', keywords: ['vegetable', 'spud'] },
|
||||
{ shortcode: 'carrot', emoji: '🥕', keywords: ['vegetable', 'orange'] },
|
||||
{ shortcode: 'corn', emoji: '🌽', keywords: ['vegetable', 'maize'] },
|
||||
{ shortcode: 'hot_pepper', emoji: '🌶️', keywords: ['spicy', 'chili'] },
|
||||
{ shortcode: 'broccoli', emoji: '🥦', keywords: ['vegetable', 'green'] },
|
||||
{ shortcode: 'bread', emoji: '🍞', keywords: ['food', 'toast'] },
|
||||
{ shortcode: 'croissant', emoji: '🥐', keywords: ['food', 'french'] },
|
||||
{ shortcode: 'pretzel', emoji: '🥨', keywords: ['food', 'snack'] },
|
||||
{ shortcode: 'bagel', emoji: '🥯', keywords: ['food', 'breakfast'] },
|
||||
{ shortcode: 'cheese', emoji: '🧀', keywords: ['food', 'dairy'] },
|
||||
{ shortcode: 'egg', emoji: '🥚', keywords: ['food', 'breakfast'] },
|
||||
{ shortcode: 'bacon', emoji: '🥓', keywords: ['food', 'breakfast'] },
|
||||
{ shortcode: 'pancakes', emoji: '🥞', keywords: ['food', 'breakfast'] },
|
||||
{ shortcode: 'waffle', emoji: '🧇', keywords: ['food', 'breakfast'] },
|
||||
{ shortcode: 'steak', emoji: '🥩', keywords: ['food', 'meat'] },
|
||||
{ shortcode: 'poultry_leg', emoji: '🍗', keywords: ['food', 'chicken'] },
|
||||
{ shortcode: 'hamburger', emoji: '🍔', keywords: ['food', 'burger'] },
|
||||
{ shortcode: 'fries', emoji: '🍟', keywords: ['food', 'fast'] },
|
||||
{ shortcode: 'pizza', emoji: '🍕', keywords: ['food', 'italian'] },
|
||||
{ shortcode: 'hot_dog', emoji: '🌭', keywords: ['food', 'fast'] },
|
||||
{ shortcode: 'sandwich', emoji: '🥪', keywords: ['food', 'lunch'] },
|
||||
{ shortcode: 'taco', emoji: '🌮', keywords: ['food', 'mexican'] },
|
||||
{ shortcode: 'burrito', emoji: '🌯', keywords: ['food', 'mexican'] },
|
||||
{ shortcode: 'sushi', emoji: '🍣', keywords: ['food', 'japanese'] },
|
||||
{ shortcode: 'ramen', emoji: '🍜', keywords: ['food', 'noodles'] },
|
||||
{ shortcode: 'spaghetti', emoji: '🍝', keywords: ['food', 'pasta'] },
|
||||
{ shortcode: 'curry', emoji: '🍛', keywords: ['food', 'rice'] },
|
||||
{ shortcode: 'rice', emoji: '🍚', keywords: ['food', 'white'] },
|
||||
{ shortcode: 'salad', emoji: '🥗', keywords: ['food', 'healthy'] },
|
||||
{ shortcode: 'popcorn', emoji: '🍿', keywords: ['food', 'movie'] },
|
||||
{ shortcode: 'cake', emoji: '🎂', keywords: ['food', 'birthday'] },
|
||||
{ shortcode: 'cupcake', emoji: '🧁', keywords: ['food', 'sweet'] },
|
||||
{ shortcode: 'pie', emoji: '🥧', keywords: ['food', 'dessert'] },
|
||||
{ shortcode: 'cookie', emoji: '🍪', keywords: ['food', 'sweet'] },
|
||||
{ shortcode: 'chocolate', emoji: '🍫', keywords: ['food', 'sweet'] },
|
||||
{ shortcode: 'candy', emoji: '🍬', keywords: ['food', 'sweet'] },
|
||||
{ shortcode: 'lollipop', emoji: '🍭', keywords: ['food', 'sweet'] },
|
||||
{ shortcode: 'donut', emoji: '🍩', keywords: ['food', 'sweet'] },
|
||||
{ shortcode: 'ice_cream', emoji: '🍨', keywords: ['food', 'dessert'] },
|
||||
{ shortcode: 'icecream', emoji: '🍦', keywords: ['food', 'dessert', 'cone'] },
|
||||
{ shortcode: 'coffee', emoji: '☕', keywords: ['drink', 'caffeine'] },
|
||||
{ shortcode: 'tea', emoji: '🍵', keywords: ['drink', 'green'] },
|
||||
{ shortcode: 'beer', emoji: '🍺', keywords: ['drink', 'alcohol'] },
|
||||
{ shortcode: 'beers', emoji: '🍻', keywords: ['drink', 'cheers'] },
|
||||
{ shortcode: 'wine_glass', emoji: '🍷', keywords: ['drink', 'alcohol'] },
|
||||
{ shortcode: 'cocktail', emoji: '🍸', keywords: ['drink', 'alcohol'] },
|
||||
{ shortcode: 'tropical_drink', emoji: '🍹', keywords: ['drink', 'vacation'] },
|
||||
{ shortcode: 'champagne', emoji: '🍾', keywords: ['drink', 'celebrate'] },
|
||||
{ shortcode: 'milk', emoji: '🥛', keywords: ['drink', 'dairy'] },
|
||||
{ shortcode: 'baby_bottle', emoji: '🍼', keywords: ['drink', 'infant'] },
|
||||
{ shortcode: 'juice', emoji: '🧃', keywords: ['drink', 'box'] },
|
||||
{ shortcode: 'cup_with_straw', emoji: '🥤', keywords: ['drink', 'soda'] },
|
||||
|
||||
// Weather & Nature
|
||||
{ shortcode: 'sun', emoji: '☀️', keywords: ['weather', 'sunny', 'bright'] },
|
||||
{ shortcode: 'moon', emoji: '🌙', keywords: ['night', 'sleep'] },
|
||||
{ shortcode: 'full_moon', emoji: '🌕', keywords: ['night', 'lunar'] },
|
||||
{ shortcode: 'new_moon', emoji: '🌑', keywords: ['night', 'dark'] },
|
||||
{ shortcode: 'star2', emoji: '🌟', keywords: ['glow', 'sparkle'] },
|
||||
{ shortcode: 'milky_way', emoji: '🌌', keywords: ['galaxy', 'space'] },
|
||||
{ shortcode: 'cloud', emoji: '☁️', keywords: ['weather', 'sky'] },
|
||||
{ shortcode: 'sun_behind_cloud', emoji: '⛅', keywords: ['weather'] },
|
||||
{ shortcode: 'cloud_with_rain', emoji: '🌧️', keywords: ['weather', 'rainy'] },
|
||||
{ shortcode: 'thunder', emoji: '⛈️', keywords: ['weather', 'storm'] },
|
||||
{ shortcode: 'snowflake', emoji: '❄️', keywords: ['weather', 'cold'] },
|
||||
{ shortcode: 'snowman', emoji: '☃️', keywords: ['winter', 'snow'] },
|
||||
{ shortcode: 'wind_blowing', emoji: '🌬️', keywords: ['weather', 'air'] },
|
||||
{ shortcode: 'tornado', emoji: '🌪️', keywords: ['weather', 'storm'] },
|
||||
{ shortcode: 'fog', emoji: '🌫️', keywords: ['weather', 'mist'] },
|
||||
{ shortcode: 'umbrella', emoji: '☂️', keywords: ['rain', 'weather'] },
|
||||
{ shortcode: 'rainbow', emoji: '🌈', keywords: ['weather', 'pride'] },
|
||||
{ shortcode: 'earth', emoji: '🌍', keywords: ['world', 'planet'] },
|
||||
{ shortcode: 'earth_americas', emoji: '🌎', keywords: ['world', 'planet'] },
|
||||
{ shortcode: 'earth_asia', emoji: '🌏', keywords: ['world', 'planet'] },
|
||||
{ shortcode: 'rocket', emoji: '🚀', keywords: ['space', 'launch'] },
|
||||
{ shortcode: 'satellite', emoji: '🛰️', keywords: ['space', 'orbit'] },
|
||||
{ shortcode: 'ufo', emoji: '🛸', keywords: ['alien', 'space'] },
|
||||
|
||||
// Checkmarks & Common Symbols
|
||||
{ shortcode: 'white_check_mark', emoji: '✅', keywords: ['done', 'yes', 'ok'] },
|
||||
{ shortcode: 'check', emoji: '✔️', keywords: ['done', 'yes'] },
|
||||
{ shortcode: 'x', emoji: '❌', keywords: ['no', 'wrong', 'cancel'] },
|
||||
{ shortcode: 'cross_mark', emoji: '❎', keywords: ['no', 'wrong'] },
|
||||
{ shortcode: 'plus', emoji: '➕', keywords: ['add', 'math'] },
|
||||
{ shortcode: 'minus', emoji: '➖', keywords: ['subtract', 'math'] },
|
||||
{ shortcode: 'divide', emoji: '➗', keywords: ['math', 'division'] },
|
||||
{ shortcode: 'multiply', emoji: '✖️', keywords: ['math', 'times'] },
|
||||
{ shortcode: 'infinity', emoji: '♾️', keywords: ['forever', 'endless'] },
|
||||
{ shortcode: 'question', emoji: '❓', keywords: ['ask', 'what'] },
|
||||
{ shortcode: 'grey_question', emoji: '❔', keywords: ['ask', 'what'] },
|
||||
{ shortcode: 'exclamation', emoji: '❗', keywords: ['alert', 'important'] },
|
||||
{ shortcode: 'grey_exclamation', emoji: '❕', keywords: ['alert'] },
|
||||
{ shortcode: 'warning', emoji: '⚠️', keywords: ['alert', 'caution'] },
|
||||
{ shortcode: 'no_entry', emoji: '⛔', keywords: ['stop', 'forbidden'] },
|
||||
{ shortcode: 'prohibited', emoji: '🚫', keywords: ['stop', 'banned'] },
|
||||
{ shortcode: 'recycle', emoji: '♻️', keywords: ['environment', 'green'] },
|
||||
{ shortcode: 'arrow_up', emoji: '⬆️', keywords: ['direction', 'north'] },
|
||||
{ shortcode: 'arrow_down', emoji: '⬇️', keywords: ['direction', 'south'] },
|
||||
{ shortcode: 'arrow_left', emoji: '⬅️', keywords: ['direction', 'west'] },
|
||||
{ shortcode: 'arrow_right', emoji: '➡️', keywords: ['direction', 'east'] },
|
||||
{
|
||||
shortcode: 'arrow_upper_right',
|
||||
emoji: '↗️',
|
||||
keywords: ['direction', 'northeast'],
|
||||
},
|
||||
{
|
||||
shortcode: 'arrow_lower_right',
|
||||
emoji: '↘️',
|
||||
keywords: ['direction', 'southeast'],
|
||||
},
|
||||
{
|
||||
shortcode: 'arrow_lower_left',
|
||||
emoji: '↙️',
|
||||
keywords: ['direction', 'southwest'],
|
||||
},
|
||||
{
|
||||
shortcode: 'arrow_upper_left',
|
||||
emoji: '↖️',
|
||||
keywords: ['direction', 'northwest'],
|
||||
},
|
||||
{
|
||||
shortcode: 'left_right_arrow',
|
||||
emoji: '↔️',
|
||||
keywords: ['direction', 'horizontal'],
|
||||
},
|
||||
{
|
||||
shortcode: 'up_down_arrow',
|
||||
emoji: '↕️',
|
||||
keywords: ['direction', 'vertical'],
|
||||
},
|
||||
{ shortcode: 'arrows_clockwise', emoji: '🔃', keywords: ['refresh', 'sync'] },
|
||||
{
|
||||
shortcode: 'arrows_counterclockwise',
|
||||
emoji: '🔄',
|
||||
keywords: ['refresh', 'sync'],
|
||||
},
|
||||
{ shortcode: 'back', emoji: '🔙', keywords: ['return', 'previous'] },
|
||||
{ shortcode: 'end', emoji: '🔚', keywords: ['finish', 'last'] },
|
||||
{ shortcode: 'on', emoji: '🔛', keywords: ['active'] },
|
||||
{ shortcode: 'soon', emoji: '🔜', keywords: ['coming', 'future'] },
|
||||
{ shortcode: 'top', emoji: '🔝', keywords: ['best', 'first'] },
|
||||
{ shortcode: 'new', emoji: '🆕', keywords: ['fresh', 'latest'] },
|
||||
{ shortcode: 'free', emoji: '🆓', keywords: ['gratis', 'cost'] },
|
||||
{ shortcode: 'up', emoji: '🆙', keywords: ['increase', 'level'] },
|
||||
{ shortcode: 'cool', emoji: '🆒', keywords: ['nice', 'awesome'] },
|
||||
{ shortcode: 'ok', emoji: '🆗', keywords: ['yes', 'approve'] },
|
||||
{ shortcode: 'sos', emoji: '🆘', keywords: ['help', 'emergency'] },
|
||||
{ shortcode: 'stop_sign', emoji: '🛑', keywords: ['halt', 'cease'] },
|
||||
{ shortcode: 'a', emoji: '🅰️', keywords: ['letter', 'blood'] },
|
||||
{ shortcode: 'b', emoji: '🅱️', keywords: ['letter', 'blood'] },
|
||||
{ shortcode: 'o', emoji: '🅾️', keywords: ['letter', 'blood'] },
|
||||
{ shortcode: 'information', emoji: 'ℹ️', keywords: ['info', 'help'] },
|
||||
{ shortcode: 'copyright', emoji: '©️', keywords: ['legal', 'ip'] },
|
||||
{ shortcode: 'registered', emoji: '®️', keywords: ['legal', 'brand'] },
|
||||
{ shortcode: 'tm', emoji: '™️', keywords: ['legal', 'trademark'] },
|
||||
{ shortcode: 'one', emoji: '1️⃣', keywords: ['number', 'first'] },
|
||||
{ shortcode: 'two', emoji: '2️⃣', keywords: ['number', 'second'] },
|
||||
{ shortcode: 'three', emoji: '3️⃣', keywords: ['number', 'third'] },
|
||||
{ shortcode: 'four', emoji: '4️⃣', keywords: ['number'] },
|
||||
{ shortcode: 'five', emoji: '5️⃣', keywords: ['number'] },
|
||||
{ shortcode: 'six', emoji: '6️⃣', keywords: ['number'] },
|
||||
{ shortcode: 'seven', emoji: '7️⃣', keywords: ['number'] },
|
||||
{ shortcode: 'eight', emoji: '8️⃣', keywords: ['number'] },
|
||||
{ shortcode: 'nine', emoji: '9️⃣', keywords: ['number'] },
|
||||
{ shortcode: 'zero', emoji: '0️⃣', keywords: ['number'] },
|
||||
{ shortcode: 'keycap_ten', emoji: '🔟', keywords: ['number', 'ten'] },
|
||||
{ shortcode: 'hash', emoji: '#️⃣', keywords: ['number', 'pound', 'hashtag'] },
|
||||
{ shortcode: 'asterisk', emoji: '*️⃣', keywords: ['star', 'symbol'] },
|
||||
{ shortcode: 'eject', emoji: '⏏️', keywords: ['media', 'remove'] },
|
||||
{ shortcode: 'play', emoji: '▶️', keywords: ['media', 'start'] },
|
||||
{ shortcode: 'pause', emoji: '⏸️', keywords: ['media', 'wait'] },
|
||||
{ shortcode: 'stop', emoji: '⏹️', keywords: ['media', 'end'] },
|
||||
{ shortcode: 'record', emoji: '⏺️', keywords: ['media', 'red'] },
|
||||
{ shortcode: 'fast_forward', emoji: '⏩', keywords: ['media', 'skip'] },
|
||||
{ shortcode: 'rewind', emoji: '⏪', keywords: ['media', 'back'] },
|
||||
{ shortcode: 'next_track', emoji: '⏭️', keywords: ['media', 'skip'] },
|
||||
{ shortcode: 'previous_track', emoji: '⏮️', keywords: ['media', 'back'] },
|
||||
{ shortcode: 'cinema', emoji: '🎦', keywords: ['movie', 'film'] },
|
||||
{ shortcode: 'low_brightness', emoji: '🔅', keywords: ['dim', 'light'] },
|
||||
{ shortcode: 'high_brightness', emoji: '🔆', keywords: ['bright', 'light'] },
|
||||
{ shortcode: 'signal_strength', emoji: '📶', keywords: ['wifi', 'bars'] },
|
||||
{ shortcode: 'vibration', emoji: '📳', keywords: ['phone', 'mode'] },
|
||||
{ shortcode: 'mobile_off', emoji: '📴', keywords: ['phone', 'silent'] },
|
||||
{ shortcode: 'female', emoji: '♀️', keywords: ['woman', 'gender'] },
|
||||
{ shortcode: 'male', emoji: '♂️', keywords: ['man', 'gender'] },
|
||||
{ shortcode: 'medical', emoji: '⚕️', keywords: ['health', 'doctor'] },
|
||||
{ shortcode: 'atom', emoji: '⚛️', keywords: ['science', 'physics'] },
|
||||
];
|
||||
|
||||
/**
|
||||
* Filter emojis by search text (checks shortcode and keywords)
|
||||
*/
|
||||
export function filterEmojis(
|
||||
searchText: string,
|
||||
limit: number = 10,
|
||||
): EmojiItem[] {
|
||||
if (!searchText) return [];
|
||||
|
||||
const lowerSearch = searchText.toLowerCase();
|
||||
return EMOJI_DATA.filter(
|
||||
item =>
|
||||
item.shortcode.toLowerCase().includes(lowerSearch) ||
|
||||
item.keywords?.some(keyword =>
|
||||
keyword.toLowerCase().includes(lowerSearch),
|
||||
),
|
||||
).slice(0, limit);
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef, useCallback, useMemo, useState, useRef } from 'react';
|
||||
import { Mentions } from 'antd';
|
||||
import type { MentionsRef, MentionsProps } from 'antd/es/mentions';
|
||||
import { filterEmojis, type EmojiItem } from './emojiData';
|
||||
|
||||
const MIN_CHARS_BEFORE_POPUP = 2;
|
||||
|
||||
// Regex to match emoji characters (simplified, covers most common emojis)
|
||||
const EMOJI_REGEX =
|
||||
/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]/u;
|
||||
|
||||
export interface EmojiTextAreaProps
|
||||
extends Omit<MentionsProps, 'prefix' | 'options' | 'onSelect'> {
|
||||
/**
|
||||
* Minimum characters after colon before showing popup.
|
||||
* @default 2 (Slack-like behavior)
|
||||
*/
|
||||
minCharsBeforePopup?: number;
|
||||
/**
|
||||
* Maximum number of emoji suggestions to show.
|
||||
* @default 10
|
||||
*/
|
||||
maxSuggestions?: number;
|
||||
/**
|
||||
* Called when an emoji is selected from the popup.
|
||||
*/
|
||||
onEmojiSelect?: (emoji: EmojiItem) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A TextArea component with Slack-like emoji autocomplete.
|
||||
*
|
||||
* Features:
|
||||
* - Triggers on `:` prefix (like Slack)
|
||||
* - Only shows popup after 2+ characters are typed (configurable)
|
||||
* - Colon must be preceded by a space, start of line, or another emoji
|
||||
* - Prevents accidental Enter key selection when typing quickly
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <EmojiTextArea
|
||||
* placeholder="Type :sm to see emoji suggestions..."
|
||||
* onChange={(text) => console.log(text)}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const EmojiTextArea = forwardRef<MentionsRef, EmojiTextAreaProps>(
|
||||
(
|
||||
{
|
||||
minCharsBeforePopup = MIN_CHARS_BEFORE_POPUP,
|
||||
maxSuggestions = 10,
|
||||
onEmojiSelect,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
...restProps
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [options, setOptions] = useState<
|
||||
Array<{ value: string; label: React.ReactNode }>
|
||||
>([]);
|
||||
const [isPopupVisible, setIsPopupVisible] = useState(false);
|
||||
const lastSearchRef = useRef<string>('');
|
||||
const lastKeyPressTimeRef = useRef<number>(0);
|
||||
|
||||
/**
|
||||
* Validates whether the colon trigger should activate the popup.
|
||||
* Implements Slack-like behavior:
|
||||
* - Colon must be preceded by whitespace, start of text, or emoji
|
||||
* - At least minCharsBeforePopup characters must be typed after colon
|
||||
*/
|
||||
const validateSearch = useCallback(
|
||||
(text: string, props: MentionsProps): boolean => {
|
||||
// Get the full value to check what precedes the colon
|
||||
const fullValue = (props.value as string) || '';
|
||||
|
||||
// Find where this search text starts in the full value
|
||||
// The search text is what comes after the `:` prefix
|
||||
const colonIndex = fullValue.lastIndexOf(`:${text}`);
|
||||
|
||||
if (colonIndex === -1) {
|
||||
setIsPopupVisible(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check what precedes the colon
|
||||
if (colonIndex > 0) {
|
||||
const charBefore = fullValue[colonIndex - 1];
|
||||
|
||||
// Must be preceded by whitespace, newline, or emoji
|
||||
const isWhitespace = /\s/.test(charBefore);
|
||||
const isEmoji = EMOJI_REGEX.test(charBefore);
|
||||
|
||||
if (!isWhitespace && !isEmoji) {
|
||||
setIsPopupVisible(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check minimum character requirement
|
||||
if (text.length < minCharsBeforePopup) {
|
||||
setIsPopupVisible(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsPopupVisible(true);
|
||||
return true;
|
||||
},
|
||||
[minCharsBeforePopup],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles search and filters emoji suggestions.
|
||||
*/
|
||||
const handleSearch = useCallback(
|
||||
(searchText: string) => {
|
||||
lastSearchRef.current = searchText;
|
||||
|
||||
if (searchText.length < minCharsBeforePopup) {
|
||||
setOptions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredEmojis = filterEmojis(searchText, maxSuggestions);
|
||||
|
||||
const newOptions = filteredEmojis.map(item => ({
|
||||
value: item.emoji,
|
||||
label: (
|
||||
<span>
|
||||
<span style={{ marginRight: 8 }}>{item.emoji}</span>
|
||||
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
|
||||
:{item.shortcode}:
|
||||
</span>
|
||||
</span>
|
||||
),
|
||||
// Store the full item for onSelect callback
|
||||
data: item,
|
||||
}));
|
||||
|
||||
setOptions(newOptions);
|
||||
},
|
||||
[minCharsBeforePopup, maxSuggestions],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles emoji selection from the popup.
|
||||
*/
|
||||
const handleSelect = useCallback(
|
||||
(option: { value: string; data?: EmojiItem }) => {
|
||||
if (option.data && onEmojiSelect) {
|
||||
onEmojiSelect(option.data);
|
||||
}
|
||||
setIsPopupVisible(false);
|
||||
},
|
||||
[onEmojiSelect],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles key down events to prevent accidental selection on Enter.
|
||||
* If the user presses Enter very quickly after typing (< 100ms),
|
||||
* we treat it as a newline intent rather than selection.
|
||||
*/
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const now = Date.now();
|
||||
const timeSinceLastKey = now - lastKeyPressTimeRef.current;
|
||||
|
||||
// If Enter is pressed and popup is visible
|
||||
if (e.key === 'Enter' && isPopupVisible) {
|
||||
// If typed very quickly (< 100ms since last keypress) and
|
||||
// there's meaningful search text, allow the Enter to create newline
|
||||
// This prevents accidental selection when typing something like:
|
||||
// "let me show you an example:[Enter]"
|
||||
if (timeSinceLastKey < 100 && lastSearchRef.current.length === 0) {
|
||||
// Let the default behavior (newline) happen
|
||||
setIsPopupVisible(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
lastKeyPressTimeRef.current = now;
|
||||
|
||||
// Call original onKeyDown if provided
|
||||
onKeyDown?.(e);
|
||||
},
|
||||
[isPopupVisible, onKeyDown],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(text: string) => {
|
||||
lastKeyPressTimeRef.current = Date.now();
|
||||
onChange?.(text);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
// Memoize the Mentions component props
|
||||
const mentionsProps = useMemo(
|
||||
() => ({
|
||||
prefix: ':',
|
||||
split: '',
|
||||
options,
|
||||
validateSearch,
|
||||
onSearch: handleSearch,
|
||||
onSelect: handleSelect,
|
||||
onKeyDown: handleKeyDown,
|
||||
onChange: handleChange,
|
||||
notFoundContent: null, // Don't show "Not Found" message
|
||||
...restProps,
|
||||
}),
|
||||
[
|
||||
options,
|
||||
validateSearch,
|
||||
handleSearch,
|
||||
handleSelect,
|
||||
handleKeyDown,
|
||||
handleChange,
|
||||
restProps,
|
||||
],
|
||||
);
|
||||
|
||||
return <Mentions ref={ref} {...mentionsProps} />;
|
||||
},
|
||||
);
|
||||
|
||||
EmojiTextArea.displayName = 'EmojiTextArea';
|
||||
|
||||
export type { EmojiItem };
|
||||
export { filterEmojis, EMOJI_DATA } from './emojiData';
|
||||
@@ -102,6 +102,13 @@ export {
|
||||
type DynamicEditableTitleProps,
|
||||
} from './DynamicEditableTitle';
|
||||
export { EditableTitle, type EditableTitleProps } from './EditableTitle';
|
||||
export {
|
||||
EmojiTextArea,
|
||||
type EmojiTextAreaProps,
|
||||
type EmojiItem,
|
||||
filterEmojis,
|
||||
EMOJI_DATA,
|
||||
} from './EmojiTextArea';
|
||||
export { EmptyState, type EmptyStateProps } from './EmptyState';
|
||||
export { Empty, type EmptyProps } from './EmptyState/Empty';
|
||||
export { FaveStar, type FaveStarProps } from './FaveStar';
|
||||
|
||||
Reference in New Issue
Block a user