Copilot commented on code in PR #36417:
URL: https://github.com/apache/superset/pull/36417#discussion_r2590135229


##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx:
##########
@@ -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) || '';

Review Comment:
   The `validateSearch` function receives `props` parameter but never uses it 
correctly. The function accesses `props.value`, but in React component 
callbacks, the component's current value should come from the component's own 
state or props, not from a passed-in props object. This could lead to stale or 
incorrect validation results.
   
   The Ant Design Mentions component's `validateSearch` callback signature may 
not actually pass props as the second parameter. You should verify the actual 
API and either remove this unused parameter or properly integrate with the 
Mentions component's validation mechanism.



##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/EmojiTextArea.stories.tsx:
##########
@@ -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&apos;s behavior. Try these examples:

Review Comment:
   HTML entity `&apos;` can be replaced with a regular apostrophe `'` in JSX 
strings for better readability. JSX automatically handles apostrophes in 
strings.
   ```suggestion
             The emoji picker mimics Slack's behavior. Try these examples:
   ```



##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx:
##########
@@ -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;

Review Comment:
   The emoji regex is quite limited and described as "simplified". It misses 
many emoji ranges including:
   - Emoji modifiers and skin tones (U+1F3FB-1F3FF)
   - Combined emojis with ZWJ (Zero Width Joiner)
   - Emoji presentation selectors
   - Regional indicators beyond U+1F1E0-1F1FF
   
   Consider using a more comprehensive emoji detection library or extending 
this regex to cover more Unicode emoji ranges for better compatibility.
   ```suggestion
   import emojiRegex from 'emoji-regex';
   
   const MIN_CHARS_BEFORE_POPUP = 2;
   
   // Comprehensive regex to match all emoji characters, including modifiers 
and ZWJ sequences
   const EMOJI_REGEX = emojiRegex();
   ```



##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx:
##########
@@ -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}`);
+

Review Comment:
   Magic number `-1` should be documented or extracted to a constant. While 
this is a common pattern for indexOf/lastIndexOf, adding a comment explaining 
what -1 represents (not found) would improve readability.
   ```suggestion
   
           // If lastIndexOf returns -1, the substring was not found
   ```



##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx:
##########
@@ -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,

Review Comment:
   The comment mentions storing the full item for onSelect callback, but 
TypeScript doesn't recognize the `data` property on the option type. Consider 
explicitly typing the options array to include the `data` property, or document 
this as an extended property pattern.



##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/emojiData.ts:
##########
@@ -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'] },

Review Comment:
   The shortcode `wave_emoji` is confusing because there's already a `wave` 
emoji (๐Ÿ‘‹) on line 120. This ocean wave emoji should have a more distinct name 
like `ocean_wave` or `water_wave` to avoid confusion and to match common emoji 
naming conventions (e.g., Slack uses `ocean` for this emoji).
   ```suggestion
     { shortcode: 'ocean_wave', emoji: '๐ŸŒŠ', keywords: ['ocean', 'water'] },
   ```



##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx:
##########
@@ -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}`);
+

Review Comment:
   Using `lastIndexOf` to find the colon position could be inefficient and may 
find the wrong colon if there are multiple colons in the text. For example, if 
the text is `:smile: :heart:`, this will find the second colon, not necessarily 
the one being actively typed. Consider using a more precise method to locate 
the current cursor position and the colon being typed.
   ```suggestion
           // Find the colon that starts the current search, based on cursor 
position
           // Use selectionStart if available, otherwise fallback to end of text
           const cursorPos =
             typeof (props as any).selectionStart === 'number'
               ? (props as any).selectionStart
               : fullValue.length;
   
           // Search backward from the cursor for the nearest colon
           let colonIndex = -1;
           for (let i = cursorPos - text.length - 1; i >= 0; i--) {
             if (fullValue[i] === ':') {
               // Check if the substring after this colon matches the search 
text
               if (fullValue.slice(i + 1, i + 1 + text.length) === text) {
                 colonIndex = i;
                 break;
               }
             }
           }
   ```



##########
superset-frontend/packages/superset-ui-core/src/components/index.ts:
##########
@@ -102,6 +102,13 @@ export {
   type DynamicEditableTitleProps,
 } from './DynamicEditableTitle';
 export { EditableTitle, type EditableTitleProps } from './EditableTitle';
+export {
+  EmojiTextArea,
+  type EmojiTextAreaProps,
+  type EmojiItem,
+  filterEmojis,
+  EMOJI_DATA,

Review Comment:
   [nitpick] Exporting `filterEmojis` and `EMOJI_DATA` from the main components 
index may expose internal implementation details. Consider whether these 
utilities need to be part of the public API. If they're only meant for internal 
use within the EmojiTextArea component, they could remain private. If external 
consumers need them, ensure they're properly documented as part of the public 
API contract.
   ```suggestion
   
   ```



##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx:
##########
@@ -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

Review Comment:
   Magic number `100` (milliseconds) should be extracted to a named constant 
like `RAPID_TYPING_THRESHOLD_MS = 100` to make the code more maintainable and 
the intent clearer.



##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/EmojiTextArea.test.tsx:
##########
@@ -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);
+});

Review Comment:
   Missing test coverage for the core functionality:
   - No tests for the colon trigger validation (e.g., `:sm` should trigger, 
`hello:sm` should not)
   - No tests for emoji popup visibility behavior
   - No tests for the onEmojiSelect callback
   - No tests for the Enter key handling logic that prevents accidental 
selection
   - No tests verifying emoji insertion into the textarea
   
   The tests only cover basic rendering and the utility functions, but don't 
test the main Slack-like autocomplete behavior that this component is designed 
for.



##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx:
##########
@@ -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)' }}>

Review Comment:
   The inline styles use CSS variable references 
(`var(--ant-color-text-secondary)`) which may not be defined in all contexts 
where this component is used. Consider using themed colors from the component 
library or documenting this dependency on Ant Design CSS variables.
   ```suggestion
                 <span style={{ color: '#8c8c8c' }}>
   ```



##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx:
##########
@@ -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 }>
+    >([]);

Review Comment:
   The options type could be extracted to an interface or type alias for better 
maintainability and reusability. The inline type `Array<{ value: string; label: 
React.ReactNode }>` is used in multiple places and should be centralized. 
Consider creating an `EmojiOption` type that includes the optional `data` 
property mentioned in comments.



##########
superset-frontend/packages/superset-ui-core/src/components/EmojiTextArea/index.tsx:
##########
@@ -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) {

Review Comment:
   The Enter key handling logic appears to have inverted logic. The condition 
checks if `timeSinceLastKey < 100` AND `lastSearchRef.current.length === 0`, 
which means it only prevents selection when there's NO search text. This seems 
backwards - you'd want to prevent accidental selection when the user IS typing 
(has search text), not when they aren't.
   
   The comment says "If typed very quickly (< 100ms since last keypress) and 
there's meaningful search text", but the code checks for `length === 0` (no 
search text). This will allow accidental selection when typing `:sm[Enter]` 
quickly, which is the exact scenario you're trying to prevent.
   ```suggestion
             if (timeSinceLastKey < 100 && lastSearchRef.current.length > 0) {
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to