-
Notifications
You must be signed in to change notification settings - Fork 39
feat: add custom emoji category support #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mjcampagna
wants to merge
23
commits into
liveblocks:main
Choose a base branch
from
gluegroups:feat/custom-emoji-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,150
−13
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
51ef08a
feat: add custom emoji category support
mjcampagna ac54fd0
Merge pull request #1 from gluegroups/feat/custom-emoji-support
mjcampagna ba1c7ad
fix: use id comparison for custom emoji isActive check
mjcampagna 704b6bc
feat: add frequently used emojis support
mjcampagna f931012
docs: add frequently used emojis and changelog to CUSTOM-EMOJIS.md
mjcampagna ee35112
feat: add configurable frequentlyLabel prop, bump to 0.3.4
mjcampagna 69e817f
fix: revert package.json name, version, and repository to upstream va…
mjcampagna 410b9bb
fix: use CSS style for img sizing in docs example
mjcampagna 0fc0c4c
refactor: isolate custom emoji logic into dedicated files
mjcampagna 074c060
docs: update AGENTS.md with current architecture and reversal guide
mjcampagna ecb609f
docs: note searchCustomEmojis mirrors upstream searchEmojis scoring
mjcampagna 57d2d4f
feat: add searchLabel prop for unified cross-category search results
mjcampagna 3d52acc
feat: replace searchLabel gate with explicit unifiedSearch prop
mjcampagna c1f6411
feat: export scoreEmoji for use by consumers
mjcampagna c88ec05
test: add coverage for custom emoji additions
mjcampagna 5e9093d
fix: update sameEmojiPickerEmoji to compare custom emojis by id
mjcampagna 61f7221
fix: guard undefined in sameEmojiPickerEmoji before id/emoji checks
mjcampagna ab889c1
docs: update CUSTOM-EMOJIS.md with unifiedSearch, searchLabel, scoreE…
mjcampagna 63df20c
chore: remove AGENTS.md (fork-internal documentation)
mjcampagna e535648
feat: remove scoreEmoji export
mjcampagna 6590663
chore: remove PR-32-DESCRIPTION.md (local reference only)
mjcampagna 19862a2
feat: add shortcode search support to unified emoji search
mjcampagna 9e5ea9e
docs: fix package name reference in CUSTOM-EMOJIS.md
mjcampagna File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| # Custom Emoji Support | ||
|
|
||
| ## Overview | ||
|
|
||
| The `custom` prop on `<EmojiPicker.Root>` lets you inject image-based emoji categories into the picker. Custom categories appear after the standard Unicode emoji categories and are searchable by label and tags. The `frequently` prop adds a "Frequently Used" category at the top of the picker, supporting both native and custom emojis. | ||
|
|
||
| ## Usage | ||
|
|
||
| Pass custom categories via the `custom` prop on `<EmojiPicker.Root>` and provide a custom `Emoji` component via `<EmojiPicker.List>` to render images: | ||
|
|
||
| ```tsx | ||
| const customCategories = [ | ||
| { | ||
| id: "team", | ||
| label: "Team", | ||
| emojis: [ | ||
| { id: "shipit", label: "Ship It", url: "/emojis/shipit.png", tags: ["ship", "deploy"] }, | ||
| { id: "lgtm", label: "Looks Good To Me", url: "/emojis/lgtm.png", tags: ["approve"] }, | ||
| ], | ||
| }, | ||
| ]; | ||
|
|
||
| function MyEmojiPicker() { | ||
| return ( | ||
| <EmojiPicker.Root | ||
| custom={customCategories} | ||
| onEmojiSelect={(emoji) => { | ||
| if (emoji.url) { | ||
| // Handle custom image emoji | ||
| console.log("Custom emoji:", emoji.id, emoji.url); | ||
| } else { | ||
| // Handle standard Unicode emoji | ||
| console.log("Emoji:", emoji.emoji); | ||
| } | ||
| }} | ||
| > | ||
| <EmojiPicker.Search /> | ||
| <EmojiPicker.Viewport> | ||
| <EmojiPicker.List | ||
| components={{ | ||
| Emoji: ({ emoji, ...props }) => ( | ||
| <button {...props}> | ||
| {emoji.url ? ( | ||
| <img src={emoji.url} alt={emoji.label} style={{ width: "1em", height: "1em" }} /> | ||
| ) : ( | ||
| emoji.emoji | ||
| )} | ||
| </button> | ||
| ), | ||
| }} | ||
| /> | ||
| </EmojiPicker.Viewport> | ||
| </EmojiPicker.Root> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ## Search | ||
|
|
||
| Custom emojis are searchable using the same scoring as standard emojis: | ||
|
|
||
| - **Label match**: +10 points | ||
| - **Tag match**: +1 point per tag | ||
|
|
||
| Results are sorted by score descending and filtered when using `<EmojiPicker.Search>`. | ||
|
|
||
| ### Unified Search | ||
|
|
||
| By default, search results are displayed within their original categories (standard Unicode categories and custom categories separately). Enable `unifiedSearch` to merge all results — native and custom — into a single ranked list sorted by relevance score: | ||
|
|
||
| ```tsx | ||
| <EmojiPicker.Root | ||
| custom={customCategories} | ||
| unifiedSearch | ||
| searchLabel="Results" | ||
| onEmojiSelect={handleSelect} | ||
| > | ||
| {/* ... */} | ||
| </EmojiPicker.Root> | ||
| ``` | ||
|
|
||
| - **`unifiedSearch`** (`boolean`, default `false`): When `true`, all search results are combined into one category ranked by score. Has no effect when there is no active search query. | ||
| - **`searchLabel`** (`string`, optional): Label for the unified results category header. Defaults to `""` if omitted. | ||
|
|
||
| ## `onEmojiSelect` Handling | ||
|
|
||
| The `emoji` object passed to `onEmojiSelect` differs for custom vs standard emojis: | ||
|
|
||
| | Field | Standard Emoji | Custom Emoji | | ||
| | ------- | -------------- | ------------ | | ||
| | `emoji` | `"😀"` | `undefined` | | ||
| | `label` | `"Grinning"` | `"Ship It"` | | ||
| | `url` | `undefined` | `"/emojis/shipit.png"` | | ||
| | `id` | `undefined` | `"shipit"` | | ||
|
|
||
| Check for `emoji.url` to distinguish between the two. | ||
|
|
||
| ## Frequently Used Emojis | ||
|
|
||
| Pass an array of `EmojiPickerEmoji` objects via the `frequently` prop to display a "Frequently Used" category at the top of the picker. Supports both native and custom emojis. The category is hidden during search. | ||
|
|
||
| ```tsx | ||
| <EmojiPicker.Root | ||
| frequently={[ | ||
| { emoji: "👍", label: "Thumbs Up" }, | ||
| { emoji: "❤️", label: "Red Heart" }, | ||
| { id: "shipit", label: "Ship It", url: "/emojis/shipit.png" }, | ||
| ]} | ||
| frequentlyLabel="Favorites" | ||
| onEmojiSelect={handleSelect} | ||
| > | ||
| {/* ... */} | ||
| </EmojiPicker.Root> | ||
| ``` | ||
|
|
||
| The consumer is responsible for tracking and persisting frequency data — frimousse does not manage localStorage or usage counts internally. | ||
|
|
||
| ## Prop Reference | ||
|
|
||
| All custom emoji props are added to `<EmojiPicker.Root>`: | ||
|
|
||
| | Prop | Type | Default | Description | | ||
| | ----------------- | --------------------- | ------------------- | ----------- | | ||
| | `custom` | `CustomCategory[]` | — | Custom image-based emoji categories, appended after standard categories. | | ||
| | `frequently` | `EmojiPickerEmoji[]` | — | Emojis to show in a "Frequently Used" category at the top. Hidden during search. | | ||
| | `frequentlyLabel` | `string` | `"Frequently Used"` | Label for the frequently used category header. | | ||
| | `unifiedSearch` | `boolean` | `false` | When `true`, merges all search results into one ranked list. | | ||
| | `searchLabel` | `string` | `""` | Category header label when `unifiedSearch` is active. | | ||
|
|
||
| ## Types | ||
|
|
||
| ```ts | ||
| type CustomEmoji = { | ||
| id: string; | ||
| label: string; | ||
| url: string; | ||
| tags?: string[]; | ||
| }; | ||
|
|
||
| type CustomCategory = { | ||
| id: string; | ||
| label: string; | ||
| emojis: CustomEmoji[]; | ||
| }; | ||
| ``` | ||
|
|
||
| Both are exported from `frimousse`. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import type { EmojiPickerEmoji, EmojiPickerRootProps } from "./types"; | ||
|
|
||
| export type CustomEmoji = { | ||
| id: string; | ||
| label: string; | ||
| url: string; | ||
| tags?: string[]; | ||
| }; | ||
|
|
||
| export type CustomCategory = { | ||
| id: string; | ||
| label: string; | ||
| emojis: CustomEmoji[]; | ||
| }; | ||
|
|
||
| /** | ||
| * Additional root props for custom emoji support. | ||
| * Intersected with the upstream EmojiPickerRootProps at the component level. | ||
| */ | ||
| export interface CustomEmojiRootProps { | ||
| /** | ||
| * Custom emoji categories to append to the picker. | ||
| */ | ||
| custom?: CustomCategory[]; | ||
|
|
||
| /** | ||
| * Frequently used emojis to display at the top of the picker. | ||
| * Supports both native emojis (with `emoji` field) and custom emojis (with `url` and `id` fields). | ||
| */ | ||
| frequently?: EmojiPickerEmoji[]; | ||
|
|
||
| /** | ||
| * The label for the frequently used category header. | ||
| * | ||
| * @default "Frequently Used" | ||
| */ | ||
| frequentlyLabel?: string; | ||
|
|
||
| /** | ||
| * When true, search results from both native and custom emojis are merged | ||
| * into a single flat category sorted by relevance, instead of being | ||
| * displayed within their original categories. | ||
| * | ||
| * @default false | ||
| */ | ||
| unifiedSearch?: boolean; | ||
|
|
||
| /** | ||
| * The label for the unified search results category header. | ||
| * Only used when `unifiedSearch` is true. | ||
| * | ||
| * @default "" | ||
| */ | ||
| searchLabel?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Full root props type, including custom emoji extensions. | ||
| * Re-exported from index.ts as EmojiPickerRootProps to shadow the upstream type. | ||
| */ | ||
| export type AugmentedEmojiPickerRootProps = EmojiPickerRootProps & CustomEmojiRootProps; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.