From dfcbd35500bf8aed49d3722909925b415ef4fc59 Mon Sep 17 00:00:00 2001 From: Avea-marina Date: Tue, 12 May 2026 01:37:47 +0300 Subject: [PATCH] fix(pkg/emojipicker): fix emojipicker --- .../src/stories/EmojiPicker.stories.tsx | 27 ++- package-lock.json | 2 +- packages/pkg.emoji.picker/EmojiCategory.tsx | 2 +- packages/pkg.emoji.picker/EmojiPicker.tsx | 186 +--------------- .../pkg.emoji.picker/EmojiPickerPopup.tsx | 200 ++++++++++++++++++ packages/pkg.emoji.picker/index.ts | 1 + packages/pkg.emoji.picker/package.json | 2 +- packages/pkg.emoji.picker/types.ts | 2 +- 8 files changed, 234 insertions(+), 188 deletions(-) create mode 100644 packages/pkg.emoji.picker/EmojiPickerPopup.tsx diff --git a/apps/xi.storybook/src/stories/EmojiPicker.stories.tsx b/apps/xi.storybook/src/stories/EmojiPicker.stories.tsx index 37a54617..a27ae0a7 100644 --- a/apps/xi.storybook/src/stories/EmojiPicker.stories.tsx +++ b/apps/xi.storybook/src/stories/EmojiPicker.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { EmojiPicker } from '@xipkg/emojipicker'; +import { EmojiPicker, EmojiPickerPopup } from '@xipkg/emojipicker'; import { useState } from 'react'; const meta = { @@ -30,11 +30,32 @@ export const Default: Story = { export const WithRecentEmojis: Story = { render: () => { const [selectedEmoji, setSelectedEmoji] = useState(''); - const recentEmojis = ['1f600', '1f601', '1f602', '1f603', '1f604']; + const [recentEmojis, setRecentEmojis] = useState(['πŸ˜‚', 'πŸ‘½']); + const onEmojiSelectHandler = (emoji: string) => { + setSelectedEmoji(emoji); + setRecentEmojis([emoji, ...recentEmojis]); + }; return (
- + + {selectedEmoji &&
Π’Ρ‹Π±Ρ€Π°Π½Π½Ρ‹ΠΉ эмодзи: {selectedEmoji}
} +
+ ); + }, +}; + +export const WithCustomTriggerComponent: Story = { + render: () => { + const [selectedEmoji, setSelectedEmoji] = useState(''); + const [open, setOpen] = useState(false); + + const setOpenHandle = () => setOpen(!open); + + return ( +
+ Trigger + {open && } {selectedEmoji &&
Π’Ρ‹Π±Ρ€Π°Π½Π½Ρ‹ΠΉ эмодзи: {selectedEmoji}
}
); diff --git a/package-lock.json b/package-lock.json index c053908b..e75c2b28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24964,7 +24964,7 @@ }, "packages/pkg.emoji.picker": { "name": "@xipkg/emojipicker", - "version": "1.0.10", + "version": "1.0.11", "license": "MIT", "dependencies": { "@xipkg/button": "^3.1.7", diff --git a/packages/pkg.emoji.picker/EmojiCategory.tsx b/packages/pkg.emoji.picker/EmojiCategory.tsx index 4a6e033c..f8ea6c66 100644 --- a/packages/pkg.emoji.picker/EmojiCategory.tsx +++ b/packages/pkg.emoji.picker/EmojiCategory.tsx @@ -86,7 +86,7 @@ export const EmojiCategory = memo( title={`:${emoji.name}:`} variant="ghost" className="hover:bg-gray-10 dark:hover:bg-gray-90 h-6 w-6 rounded-sm p-1" - onClick={() => handleEmojiClick(emoji.unicode)} + onClick={() => handleEmojiClick(emoji.char)} style={{ fontFamily: 'Apple Color Emoji, Twemoji Mozilla, Noto Color Emoji, Android Emoji', }} diff --git a/packages/pkg.emoji.picker/EmojiPicker.tsx b/packages/pkg.emoji.picker/EmojiPicker.tsx index 8db58e6c..f2d39f3b 100644 --- a/packages/pkg.emoji.picker/EmojiPicker.tsx +++ b/packages/pkg.emoji.picker/EmojiPicker.tsx @@ -1,117 +1,10 @@ -'use client'; - -import { useState, useRef, useMemo, useEffect, useCallback } from 'react'; import { Button } from '@xipkg/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@xipkg/dropdown'; -import { - Clock, - Emotions, - Search, - Nature, - Food, - Activity, - Places, - Objects, - Heart, - Flag, -} from '@xipkg/icons'; -import { Input } from '@xipkg/input'; -import { cn } from '@xipkg/utils'; -import { CategoryT, EmojiPickerPropsT, EmojiT } from './types'; -import { EmojiCategory } from './EmojiCategory'; - -import { unicodeToNative } from './utils/unicodeToNative'; - -import emojisData from './emojis.json'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@xipkg/tooltip'; - -const recentIcon = { icon: Clock, name: 'ПослСдниС' }; - -const categoryIcons = [ - { icon: Emotions, name: 'Π›ΠΈΡ†Π° ΠΈ эмоции' }, - { icon: Nature, name: 'ΠŸΡ€ΠΈΡ€ΠΎΠ΄Π°' }, - { icon: Food, name: 'Π•Π΄Π° ΠΈ Π½Π°ΠΏΠΈΡ‚ΠΊΠΈ' }, - { icon: Activity, name: 'Активности' }, - { icon: Places, name: 'ΠŸΡƒΡ‚Π΅ΡˆΠ΅ΡΡ‚Π²ΠΈΡ ΠΈ мСста' }, - { icon: Objects, name: 'ΠžΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹' }, - { icon: Heart, name: 'Π‘ΠΈΠΌΠ²ΠΎΠ»Ρ‹' }, - { icon: Flag, name: 'Π€Π»Π°Π³ΠΈ' }, -]; - -export const EmojiPicker = ({ recentEmojis, onEmojiSelect }: EmojiPickerPropsT) => { - const [activeCategoryIndex, setActiveCategoryIndex] = useState(0); - const scrollContainerRef = useRef(null); - const [searchQuery, setSearchQuery] = useState(''); - - const categories = emojisData; - - const handleSearchEmoji = (e: React.ChangeEvent) => { - setActiveCategoryIndex(0); - setSearchQuery(e.target.value); - }; - - const filteredCategories = useMemo(() => { - if (!categories) return { name: 'empty', emojis: [] }; - - return { - name: 'search', - emojis: categories - .map((category) => category.emojis) - .flat() - .filter((emoji: EmojiT) => emoji.name.toLowerCase().includes(searchQuery.toLowerCase())), - }; - }, [categories, searchQuery]); - - const matchedEmojis = useMemo(() => { - if (!categories || !recentEmojis) return []; - - return categories.flatMap((category) => - category.emojis.filter((emoji) => recentEmojis.includes(emoji.unicode)), - ); - }, [categories, recentEmojis]); - - const emojiCategoriesIcons = - matchedEmojis.length > 0 ? [recentIcon, ...categoryIcons] : categoryIcons; - - const emojiCategories: CategoryT[] = useMemo(() => { - if (!categories) { - return []; - } - return matchedEmojis.length > 0 - ? [{ emojis: matchedEmojis, name: 'recent' }, ...categories] - : categories; - }, [categories, matchedEmojis]); - - const scrollToCategory = (target: HTMLElement) => { - target.scrollIntoView({ - block: 'start', - inline: 'nearest', - }); - }; - - const selectCategory = (index: number) => { - setSearchQuery(''); - setTimeout(() => { - setActiveCategoryIndex(index); - }, 50); - - const container = scrollContainerRef.current; - if (!container) return; - - const categoryEl = container.querySelector(`#emoji-category-${index}`); - if (categoryEl) { - scrollToCategory(categoryEl); - } - }; - - const handleEmojiClick = useCallback( - (emoji: string) => { - const char = unicodeToNative(emoji); - onEmojiSelect(char); - }, - [onEmojiSelect], - ); +import { Emotions } from '@xipkg/icons'; +import { EmojiPickerPopup } from './EmojiPickerPopup'; +import { TEmojiPickerProps } from './types'; +export const EmojiPicker = ({ recentEmojis, onEmojiSelect }: TEmojiPickerProps) => { return ( @@ -120,76 +13,7 @@ export const EmojiPicker = ({ recentEmojis, onEmojiSelect }: EmojiPickerPropsT) -
-
- - {emojiCategoriesIcons.map(({ icon: Icon, name }, index) => { - return ( - - - - - -

{name}

-
-
- ); - })} -
-
-
- } - placeholder="Поиск" - className="border" - value={searchQuery} - onChange={handleSearchEmoji} - /> -
- {filteredCategories && searchQuery ? ( - - ) : ( - emojiCategories.length > 0 && - emojiCategories.map((emojis, index) => ( - - )) - )} -
-
-
+
); diff --git a/packages/pkg.emoji.picker/EmojiPickerPopup.tsx b/packages/pkg.emoji.picker/EmojiPickerPopup.tsx new file mode 100644 index 00000000..a0487d97 --- /dev/null +++ b/packages/pkg.emoji.picker/EmojiPickerPopup.tsx @@ -0,0 +1,200 @@ +'use client'; + +import { useState, useRef, useMemo, useCallback } from 'react'; +import { Button } from '@xipkg/button'; +import { + Clock, + Emotions, + Search, + Nature, + Food, + Activity, + Places, + Objects, + Heart, + Flag, +} from '@xipkg/icons'; +import { Input } from '@xipkg/input'; +import { cn } from '@xipkg/utils'; +import { CategoryT, TEmojiPickerProps, EmojiT } from './types'; +import { EmojiCategory } from './EmojiCategory'; +import emojisData from './emojis.json'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@xipkg/tooltip'; + +type TEmojiPopup = TEmojiPickerProps & { + className?: string; +}; + +const recentIcon = { icon: Clock, name: 'ПослСдниС' }; + +const categoryIcons = [ + { icon: Emotions, name: 'Π›ΠΈΡ†Π° ΠΈ эмоции' }, + { icon: Nature, name: 'ΠŸΡ€ΠΈΡ€ΠΎΠ΄Π°' }, + { icon: Food, name: 'Π•Π΄Π° ΠΈ Π½Π°ΠΏΠΈΡ‚ΠΊΠΈ' }, + { icon: Activity, name: 'Активности' }, + { icon: Places, name: 'ΠŸΡƒΡ‚Π΅ΡˆΠ΅ΡΡ‚Π²ΠΈΡ ΠΈ мСста' }, + { icon: Objects, name: 'ΠžΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹' }, + { icon: Heart, name: 'Π‘ΠΈΠΌΠ²ΠΎΠ»Ρ‹' }, + { icon: Flag, name: 'Π€Π»Π°Π³ΠΈ' }, +]; + +export const EmojiPickerPopup = ({ recentEmojis, onEmojiSelect }: TEmojiPopup) => { + const [activeCategoryIndex, setActiveCategoryIndex] = useState(0); + const scrollContainerRef = useRef(null); + const [searchQuery, setSearchQuery] = useState(''); + + const categories = emojisData; + + const handleSearchEmoji = (e: React.ChangeEvent) => { + setActiveCategoryIndex(0); + setSearchQuery(e.target.value); + }; + + const filteredCategories = useMemo(() => { + if (!categories) return { name: 'empty', emojis: [] }; + + return { + name: 'search', + emojis: categories + .map((category) => category.emojis) + .flat() + .filter((emoji: EmojiT) => emoji.name.toLowerCase().includes(searchQuery.toLowerCase())), + }; + }, [categories, searchQuery]); + + const matchedEmojis = useMemo(() => { + if (!categories || !recentEmojis) return []; + + const emojis = categories.reduce((acc, category) => { + acc.push(...category.emojis); + + return acc; + }, []); + const emojisMap = new Map(emojis.map((emoji) => [emoji.char, emoji])); + + const uniqRecentEmojis = [...new Set(recentEmojis)]; + + return uniqRecentEmojis.reduce((acc, recentEmoji) => { + const emoji = emojisMap.get(recentEmoji); + if (!emoji) return acc; + + acc.push(emoji); + + return acc; + }, []); + }, [categories, recentEmojis]); + + const emojiCategoriesIcons = + matchedEmojis.length > 0 ? [recentIcon, ...categoryIcons] : categoryIcons; + + const emojiCategories: CategoryT[] = useMemo(() => { + if (!categories) { + return []; + } + return matchedEmojis.length > 0 + ? [{ emojis: matchedEmojis, name: 'recent' }, ...categories] + : categories; + }, [categories, matchedEmojis]); + + const scrollToCategory = (target: HTMLElement) => { + target.scrollIntoView({ + block: 'start', + inline: 'nearest', + }); + }; + + const selectCategory = (index: number) => { + setSearchQuery(''); + setTimeout(() => { + setActiveCategoryIndex(index); + }, 50); + + const container = scrollContainerRef.current; + if (!container) return; + + const categoryEl = container.querySelector(`#emoji-category-${index}`); + if (categoryEl) { + scrollToCategory(categoryEl); + } + }; + + const handleEmojiClick = useCallback( + (char: string) => { + onEmojiSelect(char); + }, + [onEmojiSelect], + ); + + return ( +
+
+ + {emojiCategoriesIcons.map(({ icon: Icon, name }, index) => { + return ( + + + + + +

{name}

+
+
+ ); + })} +
+
+
+ } + placeholder="Поиск" + className="border" + value={searchQuery} + onChange={handleSearchEmoji} + /> +
+ {filteredCategories && searchQuery ? ( + + ) : ( + emojiCategories.length > 0 && + emojiCategories.map((emojis, index) => ( + + )) + )} +
+
+
+ ); +}; diff --git a/packages/pkg.emoji.picker/index.ts b/packages/pkg.emoji.picker/index.ts index f1cf0ebf..2ebd7dbf 100644 --- a/packages/pkg.emoji.picker/index.ts +++ b/packages/pkg.emoji.picker/index.ts @@ -1 +1,2 @@ export { EmojiPicker } from './EmojiPicker'; +export { EmojiPickerPopup } from './EmojiPickerPopup'; diff --git a/packages/pkg.emoji.picker/package.json b/packages/pkg.emoji.picker/package.json index 1a26722d..4681eedc 100644 --- a/packages/pkg.emoji.picker/package.json +++ b/packages/pkg.emoji.picker/package.json @@ -1,6 +1,6 @@ { "name": "@xipkg/emojipicker", - "version": "1.0.10", + "version": "1.0.11", "main": "./dist/index.mjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/pkg.emoji.picker/types.ts b/packages/pkg.emoji.picker/types.ts index cdf61cdf..7ca9f770 100644 --- a/packages/pkg.emoji.picker/types.ts +++ b/packages/pkg.emoji.picker/types.ts @@ -15,7 +15,7 @@ export type FormattedEmojiDataT = { emojis: EmojiT[]; }; -export type EmojiPickerPropsT = { +export type TEmojiPickerProps = { recentEmojis?: string[]; onEmojiSelect: (emoji: string) => void; };