feat(emojis): add built-in StreamEmojiPicker and deprecate the emoji-mart EmojiPicker#3236
feat(emojis): add built-in StreamEmojiPicker and deprecate the emoji-mart EmojiPicker#3236oliverlaz wants to merge 39 commits into
Conversation
Vendor a snapshot of @emoji-mart/data (native set 15, sheet stripped) into src/plugins/Emojis/data and add an in-house search index that replicates emoji-mart's SearchIndex.search ranking, so ":shortcode" autocomplete and emoticon replacement work without the emoji-mart packages. - scripts/vendor-emoji-data.mjs regenerates the vendored JSON + MIT LICENSE - loadEmojiData() lazily code-splits the dataset (never in the core bundle) - createTextComposerEmojiMiddleware() now defaults to the built-in index - deprecated no-op init() shim eases migration off emoji-mart
Replace emoji-mart's <em-emoji-picker> web component with an in-house React panel that loads the vendored dataset and renders category navigation, a grouped emoji grid, and a preview pane. The EmojiPicker shell keeps its popover, toggle button, click-outside and insert behaviour and public props unchanged. Removes the @emoji-mart/react import and its CJS interop shim; no runtime emoji-mart imports remain in src. Styling, in-picker search, skin tones, virtualization and full keyboard a11y follow in subsequent phases.
Rewrite EmojiPicker.scss with class-based, tokenized styles for the React panel (category nav, grid, emoji cells, preview) using component tokens resolved from semantic tokens, so light/dark theming comes for free via the inherited .str-chat__theme-* ancestor. Drop the emoji-mart <em-emoji-picker> rules. Update _emoji-replacement.scss to apply the Windows flag-font replacement to the new .str-chat__emoji-picker__emoji / __preview-emoji glyphs instead of the dead .emoji-mart-emoji-native selector.
Add a SearchInput (icon + labelled input + clear button, mirroring the SDK SearchBar) and an EmptyResults state. The panel builds an in-memory search index from the loaded dataset and swaps the grouped category grid for a flat results grid while a query is active; navigating a category exits search. Styling for the search box and empty state added to EmojiPicker.scss.
Add a footer skin-tone selector and a synthetic "frequently used" category. Both are integrator-managed via new optional props — skinTone/defaultSkinTone/ onSkinToneChange and frequentlyUsedEmoji/onFrequentlyUsedChange — with ephemeral in-memory defaults. The SDK never touches browser storage; integrators own any persistence (demonstrated in the vite example in a later phase). useSkinTone and useFrequentlyUsedEmoji implement the controlled-or-uncontrolled pattern and are unit-tested.
Render the category-grouped grid with react-virtuoso (already a dependency), virtualizing at the category level so only categories in/near the viewport mount — keeping picker open fast without giving up section headers, scroll-spy (rangeChanged) or per-category scrolling (scrollToIndex via an imperative scrollToCategory handle). Search results remain a small, non-virtualized grid.
- Escape closes the picker and returns focus to the toggle button (onClose) - the search input receives focus on open; ArrowDown moves into the grid - 2D roving-tabindex grid navigation (useGridKeyboardNav): Left/Right in reading order, Up/Down to the geometrically nearest cell on the adjacent row (robust across category headers and virtualization) - category tabs get roving Left/Right/Home/End navigation - roles/labels: dialog, tablist/tab, grid/row/gridcell, radiogroup skin tones, aria-live preview
Add translations for the picker's search placeholder, clear/skin-tone aria labels, empty state, the eight category labels + "Frequently used", and the six skin-tone labels across de/en/es/fr/hi/it/ja/ko/nl/pt/ru/tr. Category and skin-tone labels are looked up via t() with dynamic keys, so they are declared directly here rather than via extraction. validate-translations passes.
Emoji support is now fully built into the stream-chat-react/emojis entry point. Drop @emoji-mart/data, @emoji-mart/react and emoji-mart from peer/optional/dev dependencies (keeping @emoji-mart/data pinned as a devDependency for the data vendoring script) and sync the lockfile. Update AI.md and the tutorial + vite examples (the vite example now demonstrates persisting skin tone and frequently-used to localStorage via the new props) and refresh the emojiSearchIndex / pickerProps docs. Verified via the production build: the index bundle references no emoji picker code or the emoji-data chunk; the dataset is a separate async chunk loaded only by the emojis entry. Migration: emoji-mart, @emoji-mart/react and @emoji-mart/data are no longer peer dependencies and init() is no longer required. Import EmojiPicker from stream-chat-react/emojis and register createTextComposerEmojiMiddleware() (no argument) for autocomplete; passing emoji-mart's SearchIndex as emojiSearchIndex still works. pickerProps beyond theme/style and emoji-mart CSS variables are no longer honored; theme: 'auto' now inherits the ancestor .str-chat__theme-* rather than prefers-color-scheme; the SDK no longer persists skin tone or frequently-used (use the new props).
…mart # Conflicts: # yarn.lock
…/close The picker panel is mounted only while open, but it previously owned the uncontrolled skin-tone and frequently-used state, so closing the picker (and closeOnEmojiSelect in particular) discarded both and the frequently-used section never accumulated across openings. Hoist that state into the always-mounted EmojiPicker shell via useSkinTone/useFrequentlyUsedEmoji and pass the panel controlled skinToneIndex / frequentlyUsedIds; the shell records usage on select. Adds a regression test covering select -> close -> reopen. Addresses adversarial-review finding: frequently-used and skin-tone state discarded whenever the picker closes.
pickerProps used a Record<string, unknown> catch-all, so emoji-mart Picker options (data, set, custom, categories, perLine, emojiVersion, locale, ...) type-checked but were silently ignored — customers could lose branded emoji or platform filtering with no signal. Tighten pickerProps to the supported shape (theme + style) so unsupported options are now a compile error, and warn at runtime (matching the SDK's other misuse warnings) when unknown keys reach the picker via `as` casts. Document the supported surface + migration in AI.md and add a test asserting the type rejects emoji-mart options and the runtime warning fires. Addresses adversarial-review finding: legacy pickerProps customizations silently stop working. Migration: EmojiPicker pickerProps only accepts theme and style. emoji-mart Picker options are no longer accepted by the type and are ignored at runtime with a console warning.
The emoji picker ships its own stylesheet (dist/css/emoji-picker.css), kept out of index.css on purpose so apps that don't use the picker pay no emoji CSS cost — the same opt-in model as channel-detail.css. But the tutorial's emoji step and AI.md's Scenario 6 happy path never imported it, so anyone following them saw an unstyled picker panel and had to discover the fix in Troubleshooting. - examples/tutorial/src/6-emoji-picker: import emoji-picker.css in layout.css (the vite example already imports it via index.scss). - AI.md Scenario 6: add the stylesheet import as an explicit step and to the code snippet, with a note on why it's separate from index.css. The stylesheet stays a separate opt-in import rather than being folded into index.css, preserving the SDK's bundle/CSS optionality. Addresses adversarial-review finding: emoji picker unusable without the separate emoji-picker.css.
The SDK's global theme rules define light as the `.str-chat` default and apply
dark via `.str-chat__theme-dark`; there is no variable set bound to a
`.str-chat__theme-light` class. So when the picker was forced to `theme='light'`
inside a `.str-chat__theme-dark` subtree, its panel still inherited the ancestor's
dark tokens and the forced light theme was a no-op. (`theme='dark'` already worked
because the panel's `.str-chat__theme-dark` class matches the global dark rule.)
Re-assert the full light variable set on the forced-light panel
(`.str-chat__emoji-picker.str-chat__theme-light`), mirroring how variable-tokens.scss
already emits `light.variables` for the `.str-chat__theme-dark .str-chat__theme-inverse`
("light inside dark") case. This adds ~4KB gzipped to the opt-in emoji-picker.css,
which only loads with the picker. `theme='auto'` still inherits the ancestor theme.
Export `themeClassName` and add a unit test locking the theme-to-class contract the
CSS override depends on.
Addresses adversarial-review finding: theme='light' does not override a dark chat
ancestor.
…ARIA
Addresses the two medium findings from the adversarial review.
1. Recoverable dataset loading
loadEmojiData memoized its promise unconditionally, so a single chunk-load
failure (offline, or a stale chunk after a deploy) was cached forever: the
picker stayed permanently aria-busy across close/reopen and the shared search
index used by the composer middleware was poisoned too, with no way back short
of a page reload.
- Add memoizeAsyncWithReset: memoize the in-flight/resolved promise but drop it
on rejection, so the next call retries. Use it for both loadEmojiData and the
search index's getIndex — the middleware retries on the next keystroke.
- useEmojiPickerState now exposes { data, error, retry }; the panel renders an
announced error (role="alert") with a Retry button instead of a stuck spinner.
- New i18n keys "Failed to load emojis" and "Retry" across all 12 locales.
2. Valid emoji-grid accessibility
The virtualized category view rendered role="row"/role="gridcell" with no
owning role="grid" (Virtuoso's root has no grid role), so assistive tech saw
gridcells with no grid — invalid ARIA. A valid virtualized grid/row/gridcell
tree with correct rowindex/rowcount metadata isn't practical here, so use plain
native <button> semantics in labeled category regions instead. Keyboard
navigation is unaffected: useGridKeyboardNav selects cells by class, and roving
tabIndex still drives 2D arrow-key movement. Grid roles removed from the search
view too, for consistent semantics across both views.
Tests: memoizeAsyncWithReset (memoize/concurrent/reset), useEmojiPickerState
(error then retry), EmojiPickerPanel (error-with-retry vs loading), and an EmojiGrid
accessibility regression asserting no orphaned grid/row/gridcell roles in the
non-search view.
Restyle the EmojiPicker panel so the drop-in replacement looks like the emoji-mart picker it replaced: - Reorder to category nav on top, search below (emoji-mart layout). - Search: one filled grey field with the icon inside, smaller input text and a compact fixed height (was a bordered box with the icon outside). - Active category marked with an accent underline instead of a filled background. - Smaller, medium-weight, secondary-colour section headers. - Softer search/footer separators (a subtle hairline, not the strong border). - Larger panel (360x440) with a roomier ~9-per-row grid and real gaps. - Footer shows a "Pick an emoji…" placeholder (with a pointer glyph) when nothing is hovered. Adds the "Pick an emoji…" i18n key to all 12 locales.
The search field set `padding-inline` on top of the flex `gap` that already follows the (empty) visually-hidden label — the first flex child — so the search icon was inset twice (~16px) and looked misaligned with the content column below. Drop the inline padding; the icon's inset now comes solely from the gap (~8px). Follow-up to the emoji styling pass.
…d to one row Polish two details of the built-in emoji picker: - The footer preview now shows the emoji's `:shortcode:` beneath its name (the same token that drives `:` autocomplete), so the preview doubles as a hint. - The "frequently used" section is capped to a single row: its ids are sliced to one row's worth (resolveFrequentlyUsedEmoji) and the frequent grid is pinned to a fixed column count, so it no longer wraps to extra rows as more emoji are used. Also adds EmojiButton skin-tone tests documenting that skin tone applies only to emoji that have skin variants (hands/people) — not the faces shown by default — which is why changing the tone appears to have no effect there.
…ReactionOptions The reaction selector's "+" (react with any emoji) was driven by `reactionOptions.extended`, which the examples used to populate from the full `@emoji-mart/data` via `mapEmojiMartData`. Removing emoji-mart left `extended` empty, so only the handful of quick reactions showed — and, because `useProcessReactions` renders a reaction only when its type is a known option, arbitrary-emoji reactions could no longer be displayed either. Add `loadDefaultExtendedReactionOptions()` to the `stream-chat-react/emojis` entry: it builds the full extended map (every vendored emoji, keyed by unicode) from the lazily code-split dataset, so importing it costs nothing until called and the dataset never enters an app's initial bundle. Core stays emoji-free — the loader lives in the opt-in emojis entry and only depends on the pure `mapEmojiMartData` from core. Wire it into the vite example (load + merge into reactionOptions.extended) and document the pattern in AI.md.
…aries The emoji grid virtualizes at the category level, but useGridKeyboardNav only considered the cells currently mounted. At the edge of the mounted window ArrowRight clamped to the current cell and ArrowDown found nothing, with no way to scroll an unmounted category in — so keyboard users could not reach the full emoji set. Give the hook the ordered categories and a scrollToCategory callback. Within the mounted window it behaves as before; when a move leaves that window it scrolls the neighbouring category into view, waits (MutationObserver, with a timeout fallback) for its cells to mount, then focuses the target — first/last cell for Left/Right, same-column first/last row for Up/Down. Home/End now reach the absolute first/last emoji. Crossing is suppressed in the search view, whose flat results have no category and are all mounted.
- Text-composer middleware: spread options into a fresh object instead of mutating the shared DEFAULT_OPTIONS, so options no longer leak between createTextComposerEmojiMiddleware() calls — the no-arg default path kept the wrong trigger/minChars after any earlier customized call. - EmojiPicker: record a "frequently used" emoji only after confirming there is a textarea to insert into, so a no-op selection isn't tracked as used. - useFrequentlyUsedEmoji: build each update from a latest-value ref so two selections in one tick both survive instead of the second dropping the first.
Replace the DOM-geometry grid navigation (getBoundingClientRect + row-epsilon tolerance + center-x scoring) with a pure navigateGrid() over the known category model; only the current column count is read from layout (via offsetTop, once per keypress). This makes Up/Down navigation unit-testable (previously impossible in jsdom, so it was uncovered) and removes the duplicated row-extraction helpers. Also track the roving cell in a ref so moving focus flips two tabIndex values instead of sweeping every mounted cell, and reconcile the roving cell in the render effect only when it has actually detached.
The grid's scroll-spy only used Virtuoso's startIndex, so a short final category that never reaches the top of the viewport never lit its nav tab. Pin the last category active while at the bottom (and keep range changes from stealing it back). The Virtuoso mock previously rendered all items and never fired the scroll callbacks, so this was untested — the mock now captures them. Also harden two search tests that passed vacuously: the localeCompare tie-break fixture now actually ties (equal-length names put the shared keyword at the same offset), and the AND-semantics assertion checks comma-anchored token matches rather than a loose substring.
- Debounce the query that drives in-panel search (via a version-safe useDebouncedValue hook — useDeferredValue is React 18+), so a broad query no longer re-scans the ~1,900-entry index and re-renders up to 90 result cells on every keystroke; clearing the field still exits search immediately. - Memoize buildEmojiSearchData by its data object so the picker panel and the composer middleware share a single index build instead of computing it twice.
- Skin-tone selector: expose it as a proper WAI-ARIA radiogroup — focus moves onto the checked tone when it opens, a roving tabindex keeps one tone tab-reachable, arrow/Home/End keys move the selection (selection follows focus), and Escape collapses back to the toggle without closing the picker. - Mark the emoji toggle button with aria-haspopup="dialog".
Widen EmojiPicker's `pickerProps` (emoji-mart-compatible names) with layout positions (navPosition, previewPosition, searchPosition, skinTonePosition), grid/content (perLine, categories, maxFrequentRows), and filtering + polish (exceptEmojis, emojiVersion, noCountryFlags, previewEmoji, noResultsEmoji, autoFocus, onClickOutside). All optional and backwards-compatible — defaults reproduce today's picker, and unsupported emoji-mart options still warn. Add an "Emoji Picker" playground tab to the vite example — live preview, reset-to-defaults, and all options wired — and make the settings modal body scroll so tall tabs no longer clip.
Introduce `StreamEmojiPicker`, the built-in (emoji-mart-free) picker, as the recommended successor exported from `stream-chat-react/emojis`. The existing `EmojiPicker` keeps rendering the emoji-mart picker unchanged for backwards compatibility — now marked `@deprecated` with a one-time console warning. It and the optional emoji-mart peer dependencies are scheduled for removal in v15. - `EmojiPicker.tsx` remains the emoji-mart engine (history preserved) and warns once, pointing at `StreamEmojiPicker`. - `StreamEmojiPicker.tsx` is the built-in successor (new file): native React panel, in-house search index, vendored dataset, curated `pickerProps`. - Re-add `@emoji-mart/data`, `@emoji-mart/react`, `emoji-mart` as OPTIONAL peer dependencies (removed entirely in v15). - vite example: the settings pane gains a picker-engine toggle (Stream vs emoji-mart) that drives both the composer picker and the live preview; the shared option controls exercise both engines. - Examples and `AI.md` recommend `StreamEmojiPicker` and document the migration. Everything here is additive or a soft-deprecation — no breaking changes; the breaking removal lands in v15. Message reactions are unaffected: the extended set loads from the vendored dataset via `loadDefaultExtendedReactionOptions` (no emoji-mart install required), code-split and fetched on demand.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/i18n/en.json`:
- Around line 554-561: The `"Suggest an option"` entry in the English i18n map
has a capitalization mismatch between the key and value. Update the value in
`src/i18n/en.json` so it matches the key text exactly, consistent with the other
identity mappings in this section and the corresponding translations.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 75c58d8b-941e-4f08-b06c-38ed42a87f92
📒 Files selected for processing (12)
src/i18n/de.jsonsrc/i18n/en.jsonsrc/i18n/es.jsonsrc/i18n/fr.jsonsrc/i18n/hi.jsonsrc/i18n/it.jsonsrc/i18n/ja.jsonsrc/i18n/ko.jsonsrc/i18n/nl.jsonsrc/i18n/pt.jsonsrc/i18n/ru.jsonsrc/i18n/tr.json
✅ Files skipped from review due to trivial changes (7)
- src/i18n/ko.json
- src/i18n/nl.json
- src/i18n/hi.json
- src/i18n/fr.json
- src/i18n/it.json
- src/i18n/ru.json
- src/i18n/ja.json
🚧 Files skipped from review as they are similar to previous changes (2)
- src/i18n/pt.json
- src/i18n/tr.json
| "Smileys & People": "Smileys & People", | ||
| "Some of the files will not be accepted": "Some of the files will not be accepted", | ||
| "Start typing to search": "Start typing to search", | ||
| "Stop sharing": "Stop sharing", | ||
| "Submit": "Submit", | ||
| "Suggest a new option to add to this poll": "Suggest a new option to add to this poll", | ||
| "Suggest an option": "Suggest an Option", | ||
| "Symbols": "Symbols", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fix capitalization mismatch in "Suggest an option" value.
Every other new English entry here uses an identity mapping (key text === value text, e.g. "Activities": "Activities"), but this one capitalizes "Option" in the value while the key and the es/de translations use lowercase "option".
🐛 Proposed fix
- "Suggest an option": "Suggest an Option",
+ "Suggest an option": "Suggest an option",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "Smileys & People": "Smileys & People", | |
| "Some of the files will not be accepted": "Some of the files will not be accepted", | |
| "Start typing to search": "Start typing to search", | |
| "Stop sharing": "Stop sharing", | |
| "Submit": "Submit", | |
| "Suggest a new option to add to this poll": "Suggest a new option to add to this poll", | |
| "Suggest an option": "Suggest an Option", | |
| "Symbols": "Symbols", | |
| "Smileys & People": "Smileys & People", | |
| "Some of the files will not be accepted": "Some of the files will not be accepted", | |
| "Start typing to search": "Start typing to search", | |
| "Stop sharing": "Stop sharing", | |
| "Submit": "Submit", | |
| "Suggest a new option to add to this poll": "Suggest a new option to add to this poll", | |
| "Suggest an option": "Suggest an option", | |
| "Symbols": "Symbols", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/i18n/en.json` around lines 554 - 561, The `"Suggest an option"` entry in
the English i18n map has a capitalization mismatch between the key and value.
Update the value in `src/i18n/en.json` so it matches the key text exactly,
consistent with the other identity mappings in this section and the
corresponding translations.
`StreamEmojiPicker` is unreleased and `emoji-mart` is back as a deprecated engine, so its emoji-mart-shaped option surface is no longer needed. Replace the `pickerProps` bag with a small set of flat props and drop the low-value knobs. - Remove the `pickerProps` object and the `warnUnsupportedPickerProps` machinery (TypeScript now rejects unknown props); hoist the keepers to flat props: `theme`, `style`, `categories`, `exceptEmojis`, `autoFocus`, `onClickOutside`. - Drop the layout/cosmetic/niche knobs (`perLine`, `navPosition`, `previewPosition`, `searchPosition`, `skinTonePosition`, `maxFrequentRows`, `previewEmoji`, `noResultsEmoji`, `emojiVersion`, `noCountryFlags`). The panel now uses one fixed layout and the column count is driven by the `--str-chat__emoji-picker-per-line` CSS token (default 9). - Delete `options.ts` and `hooks/pickerLayout.ts`; simplify `filterEmojiData` to `exceptEmojis` only. - Update the vite example (engine + auto-focus controls only), remove the now-dead SCSS, and refresh the AI.md emoji docs. Skin-tone and frequently-used props and the shell customization props are unchanged.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/plugins/Emojis/components/EmojiPickerPanel.tsx (1)
126-137: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winSkip the "unknown category" warning for
'frequent'.The
categoriesprop JSDoc says "frequentalways prepends," but if a developer includes'frequent'in theircategorieslist,byId.has('frequent')is false (it's not a dataset category), so the code emits a spuriousconsole.warnabout an unknown category id. The synthetic'frequent'category is then prepended correctly by thecategoriesmemo regardless. Suppress the warning for this known synthetic id.🛠️ Proposed fix
.map((id) => { - if (!byId.has(id)) { + if (!byId.has(id) && id !== 'frequent') { console.warn( `[stream-chat-react] EmojiPicker: unknown category id "${id}" ignored.`, ); } return byId.get(id); })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/Emojis/components/EmojiPickerPanel.tsx` around lines 126 - 137, Update the unknown-category warning logic in the categoryFilter mapping to exclude the known synthetic id "frequent" from the byId.has(id) warning condition. Preserve the existing filtering behavior and allow the categories memo to continue prepending the synthetic frequent category.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/plugins/Emojis/components/EmojiPickerPanel.tsx`:
- Around line 126-137: Update the unknown-category warning logic in the
categoryFilter mapping to exclude the known synthetic id "frequent" from the
byId.has(id) warning condition. Preserve the existing filtering behavior and
allow the categories memo to continue prepending the synthetic frequent
category.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8feae476-2c8d-42bb-946d-e289e002b772
📒 Files selected for processing (11)
AI.mdexamples/vite/src/App.tsxexamples/vite/src/AppSettings/state.tsexamples/vite/src/AppSettings/tabs/EmojiPicker/EmojiPickerTab.tsxsrc/plugins/Emojis/StreamEmojiPicker.tsxsrc/plugins/Emojis/__tests__/StreamEmojiPicker.test.tsxsrc/plugins/Emojis/components/EmojiPickerPanel.tsxsrc/plugins/Emojis/components/__tests__/EmojiPickerPanel.test.tsxsrc/plugins/Emojis/data/__tests__/filterEmojiData.test.tssrc/plugins/Emojis/data/filterEmojiData.tssrc/plugins/Emojis/styling/EmojiPicker.scss
💤 Files with no reviewable changes (2)
- src/plugins/Emojis/styling/EmojiPicker.scss
- examples/vite/src/AppSettings/state.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- AI.md
- examples/vite/src/App.tsx
…kerContext Expose the built-in emoji picker as compound components so integrators can rearrange or replace parts, while `StreamEmojiPicker` (called directly) stays the unchanged zero-config preset built on top of them. - Add `StreamEmojiPicker.Root` (provider + dialog container owning dataset load, filtering, search, and container-level a11y — dialog role, Escape-to-close, theme class) and the slots `StreamEmojiPicker.Nav` / `.Search` / `.Grid` / `.Preview` / `.SkinTone`. - Expose `useEmojiPickerContext()` with the public contract: read-only data (`categories`, `searchResults`, `status`, `skinTones`, `resolveNative`) and report-back setters (`selectEmoji`, `setQuery`, `setSkinTone`, `setActiveCategory`, `requestScrollToCategory`). Hover-preview state lives in an internal, non-exported context so it doesn't re-render the grid and a custom grid doesn't implicitly drive the default preview. - Convert each slot from prop-driven to context-driven; `EmojiPickerPanel` becomes the default composition. Behavior, a11y, keyboard nav, virtualization, and bundle optionality are unchanged — all pre-existing tests stay green. - `autoFocus` moves to the panel's own props (it's a search behavior), so `Root` no longer advertises a prop it ignores.
…-grid example - AI.md: add a composition section (`StreamEmojiPicker.Root` + slots + `useEmojiPickerContext`), the "SDK owns data + selection; you own presentation + behavior" rule, and a paged-grid example. - vite example: add a "Stream (composed)" engine option that assembles the picker from `StreamEmojiPicker.Root` + the built-in nav/search/preview/skin-tone with a custom `PagedGrid`, demonstrating built-in-Nav + custom-Grid interop through shared state.
The vite example's custom `PagedGrid` used the bare `str-chat__emoji-picker__grid` class, which has no CSS rule of its own (it is only react-virtuoso's className), so its non-virtualized cells overflowed the fixed-height picker body and overlapped the preview footer. Use the SDK's scrollable `str-chat__emoji-picker__grid-container` class (`block-size: 100%` + `overflow-y`), the same one the built-in search-results view uses.
Remove the `componentName` label argument from the picker's context hook calls across the emoji plugin (`useEmojiPickerContext`, `useEmojiPickerPreviewContext`, and the core `useTranslationContext` / `useMessageComposer*` hooks). `useEmojiPickerContext` and `useEmojiPickerPreviewContext` are now zero-param and throw a single generic "must be rendered within a StreamEmojiPicker.Root" error.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3236 +/- ##
==========================================
+ Coverage 85.18% 85.95% +0.77%
==========================================
Files 505 535 +30
Lines 15784 16369 +585
Branches 5010 5178 +168
==========================================
+ Hits 13445 14070 +625
+ Misses 2339 2299 -40 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The per-slot conversion left many heavily-mocked tests that only asserted a slot forwards an interaction to a mocked context setter — wiring, not behavior. Replace them with real end-to-end coverage: EmojiPickerPanel.test now mocks only react-virtuoso and drives the real nav / search / grid / preview / skin-tone slots against real context. - Delete the CategoryNav / EmojiButton / PreviewPane / SearchInput slot tests; their behavior (category activation, selection, hover preview, search, ArrowDown → grid focus) is now asserted through the real component tree in EmojiPickerPanel.test. - Relocate the skin-tone resolution matrix to a resolveNative test in EmojiPickerContext, where that logic lives now (not in EmojiButton). - Merge duplicates (themeClassName, entry-exports, EmojiGrid a11y, SkinToneSelector keyboard) and drop a trivial aria-haspopup attribute check. Emoji suite: 104 → 92 tests, 26 → 22 files; behavior coverage preserved or improved.
The scroll-to-category effect depended on `scrollToCategory`, whose identity changes with `categories`. Selecting an emoji rebuilds the frequently-used category, so the effect re-fired and yanked the grid back to the last nav-clicked category. Key the effect on the `scrollTarget` nonce alone via a latest-ref, and defer a frame so a freshly-mounted (search → browse) grid has laid out before scrolling.
…ntext EmojiButton is memoized and mounts once per emoji (hundreds at a time), but it subscribed to the public EmojiPickerContext. memo guards prop changes, not context changes, so every mounted cell re-rendered whenever a hot field changed identity — activeCategoryId on each scroll-spy tick, query/searchResults on each keystroke, scrollTarget on each nav click — none of which a cell reads. Move the two cold values a cell needs (resolveNative, selectEmoji) into a dedicated internal EmojiPickerCellContext and subscribe the cell there, mirroring the existing preview-context split. Cells now re-render only when the skin tone or onEmojiSelect changes. Both values remain on the public context for custom grid slots, so there is no public API change.
🎯 Goal
The SDK's emoji functionality is built on the unmaintained
emoji-martfamily (emoji-mart,@emoji-mart/react,@emoji-mart/data) — which blocks React 19 without peer-dependency overrides and carries ongoing CJS/ESM interop pain.This introduces a built-in, dependency-free emoji picker —
StreamEmojiPicker— as the successor, and deprecates the emoji-mart-basedEmojiPickerrather than removing it. Existing integrations keep working with zero code changes; the deprecated picker and the emoji-mart peer dependencies are scheduled for removal in v15.Everything here is additive or a soft-deprecation → semver minor. No breaking changes.
🛠 Implementation details
StreamEmojiPicker— the new built-in picker (stream-chat-react/emojis):<em-emoji-picker>: category nav + scroll-spy, react-virtuoso grid, in-picker search + empty state, skin tones, frequently-used, preview pane, full keyboard a11y (2D roving focus, Escape, focus return).SearchIndexranking verbatim.@emoji-mart/datasnapshot, loaded via a memoized dynamicimport()so the ~340 KB JSON stays a separate async chunk (fetched only when the picker/search run).pickerProps:navPosition,previewPosition,searchPosition,skinTonePosition,perLine,categories,maxFrequentRows,exceptEmojis,emojiVersion,noCountryFlags,previewEmoji,noResultsEmoji,autoFocus,onClickOutside(+theme/style). Skin tone & frequently-used are integrator-managed props — the SDK persists nothing.EmojiPicker— kept, deprecated (removed in v15):@deprecatedand logs a one-time console warning pointing atStreamEmojiPicker. Its widepickerPropstype is preserved, so existing TypeScript integrations don't break.@emoji-mart/data,@emoji-mart/react,emoji-martreturn as optional peer dependencies (removed entirely in v15).Adjacent APIs (backwards compatible):
createTextComposerEmojiMiddleware()now defaults to a built-in search index (no argument, no emoji-mart needed); passing emoji-mart'sSearchIndexstill works.loadDefaultExtendedReactionOptions()— no emoji-mart install required, code-split, and still opt-in (default reactions are unchanged).mapEmojiMartDatais retained.stream-chat-react(index) imports none of the emoji code — bundle optionality preserved (verified: the core bundle has zero@emoji-martreferences).Examples & docs:
StreamEmojiPicker. The vite settings pane gains a picker-engine toggle (Stream ↔ emoji-mart) that drives both the composer picker and a live preview, plus an "Emoji Picker" options playground.AI.mddocuments the successor, the deprecation, and the migration.Migration (no-op for existing users): keep using
EmojiPicker— it works and warns. To move off emoji-mart, swapEmojiPicker→StreamEmojiPickerand drop theemoji-mart/@emoji-mart/react/@emoji-mart/datainstalls and anyinit({ data }).Verification:
yarn types+ stricttsconfig.lib.jsonbuild,yarn lint, full test suite (2705 passing), tutorial + vite example type-checks, and bundle-optionality checks (core entry emoji-free, dataset code-split) all pass.🎨 UI Changes
StreamEmojiPickeris visually matched to the emoji-mart picker — panel, category tabs with an active underline, search field, preview pane, and skin-tone selector — themed via the SDK's--str-chat__emoji-picker-*tokens (light/dark inherited from the ancestor.str-chat__theme-*). No change to the composer button or popover placement.The vite example's Emoji Picker settings tab adds a Picker engine toggle (Stream / emoji-mart) at the top; the live preview and the composer's picker both switch engines instantly, and the shared option controls exercise both.
Screenshots to attach: composer picker open (light + dark), search + empty state, skin-tone selector, and the settings-tab engine toggle with its live preview.
Summary by CodeRabbit
New Features
Documentation
Deprecation