Skip to content

feat(emojis): add built-in StreamEmojiPicker and deprecate the emoji-mart EmojiPicker#3236

Open
oliverlaz wants to merge 39 commits into
masterfrom
feat/replace-emoji-mart
Open

feat(emojis): add built-in StreamEmojiPicker and deprecate the emoji-mart EmojiPicker#3236
oliverlaz wants to merge 39 commits into
masterfrom
feat/replace-emoji-mart

Conversation

@oliverlaz

@oliverlaz oliverlaz commented Jul 7, 2026

Copy link
Copy Markdown
Member

🎯 Goal

The SDK's emoji functionality is built on the unmaintained emoji-mart family (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 pickerStreamEmojiPicker — as the successor, and deprecates the emoji-mart-based EmojiPicker rather 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):

  • Native React panel replacing <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).
  • In-house search index porting emoji-mart's SearchIndex ranking verbatim.
  • Vendored @emoji-mart/data snapshot, loaded via a memoized dynamic import() so the ~340 KB JSON stays a separate async chunk (fetched only when the picker/search run).
  • Curated emoji-mart-compatible 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):

  • Still the emoji-mart engine, unchanged behavior; marked @deprecated and logs a one-time console warning pointing at StreamEmojiPicker. Its wide pickerProps type is preserved, so existing TypeScript integrations don't break.
  • @emoji-mart/data, @emoji-mart/react, emoji-mart return 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's SearchIndex still works.
  • Message reactions are unaffected. The extended "react with any emoji" set loads from the vendored dataset via loadDefaultExtendedReactionOptions() — no emoji-mart install required, code-split, and still opt-in (default reactions are unchanged). mapEmojiMartData is retained.
  • Core stream-chat-react (index) imports none of the emoji code — bundle optionality preserved (verified: the core bundle has zero @emoji-mart references).

Examples & docs:

  • Tutorial and vite examples use 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.md documents 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, swap EmojiPickerStreamEmojiPicker and drop the emoji-mart / @emoji-mart/react / @emoji-mart/data installs and any init({ data }).

Verification: yarn types + strict tsconfig.lib.json build, 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

StreamEmojiPicker is 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

    • Added a built-in emoji picker with category navigation, search, skin-tone selection, frequently used emojis, previews, keyboard controls, and loading/error recovery.
    • Added built-in shortcode autocomplete and expanded emoji reaction support.
    • Added an emoji picker settings tab with engine selection, autofocus, live preview, and reset controls.
    • Added localized emoji picker labels and accessibility text across supported languages.
  • Documentation

    • Updated integration guidance and examples to use the built-in picker.
  • Deprecation

    • Marked the legacy emoji-mart picker as deprecated and added migration guidance.

oliverlaz added 28 commits July 2, 2026 14:30
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).
…/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.
Comment thread src/i18n/de.json Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5ddc4bd and 9055a79.

📒 Files selected for processing (12)
  • src/i18n/de.json
  • src/i18n/en.json
  • src/i18n/es.json
  • src/i18n/fr.json
  • src/i18n/hi.json
  • src/i18n/it.json
  • src/i18n/ja.json
  • src/i18n/ko.json
  • src/i18n/nl.json
  • src/i18n/pt.json
  • src/i18n/ru.json
  • src/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

Comment thread src/i18n/en.json
Comment on lines +554 to +561
"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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
"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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Skip the "unknown category" warning for 'frequent'.

The categories prop JSDoc says "frequent always prepends," but if a developer includes 'frequent' in their categories list, byId.has('frequent') is false (it's not a dataset category), so the code emits a spurious console.warn about an unknown category id. The synthetic 'frequent' category is then prepended correctly by the categories memo 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5adf3af and d162162.

📒 Files selected for processing (11)
  • AI.md
  • examples/vite/src/App.tsx
  • examples/vite/src/AppSettings/state.ts
  • examples/vite/src/AppSettings/tabs/EmojiPicker/EmojiPickerTab.tsx
  • src/plugins/Emojis/StreamEmojiPicker.tsx
  • src/plugins/Emojis/__tests__/StreamEmojiPicker.test.tsx
  • src/plugins/Emojis/components/EmojiPickerPanel.tsx
  • src/plugins/Emojis/components/__tests__/EmojiPickerPanel.test.tsx
  • src/plugins/Emojis/data/__tests__/filterEmojiData.test.ts
  • src/plugins/Emojis/data/filterEmojiData.ts
  • src/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

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.87755% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.95%. Comparing base (c89366b) to head (ccb2772).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/plugins/Emojis/components/CategoryNav.tsx 48.00% 13 Missing ⚠️
src/plugins/Emojis/StreamEmojiPicker.tsx 92.98% 4 Missing ⚠️
src/plugins/Emojis/components/EmojiPickerRoot.tsx 94.93% 4 Missing ⚠️
src/plugins/Emojis/components/EmptyResults.tsx 20.00% 4 Missing ⚠️
src/plugins/Emojis/components/SkinToneSelector.tsx 93.87% 3 Missing ⚠️
src/plugins/Emojis/hooks/gridNavigation.ts 95.34% 2 Missing ⚠️
src/plugins/Emojis/components/EmojiGrid.tsx 97.22% 1 Missing ⚠️
src/plugins/Emojis/components/SearchInput.tsx 94.11% 1 Missing ⚠️
.../plugins/Emojis/context/EmojiPickerCellContext.tsx 85.71% 1 Missing ⚠️
src/plugins/Emojis/context/EmojiPickerContext.tsx 85.71% 1 Missing ⚠️
... and 2 more
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants