Skip to content

Commit c46e939

Browse files
committed
docs(emojis): document StreamEmojiPicker composition + add a composed-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.
1 parent 5912c64 commit c46e939

3 files changed

Lines changed: 128 additions & 6 deletions

File tree

AI.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,60 @@ import 'stream-chat-react/css/emoji-picker.css';
285285
picker — keep using the deprecated `EmojiPicker` if you still need them. Try it live in
286286
the "Emoji Picker" settings tab of `examples/vite/`.
287287

288+
- **Custom layouts (composition).** For a non-standard arrangement, or to replace a part,
289+
compose the picker from its slots instead of rearranging props. Rendered directly,
290+
`StreamEmojiPicker` is the preset; `StreamEmojiPicker.Root` + the slots let you assemble
291+
it yourself:
292+
293+
```tsx
294+
import { StreamEmojiPicker, useEmojiPickerContext } from 'stream-chat-react/emojis';
295+
296+
<StreamEmojiPicker.Root onEmojiSelect={insert}>
297+
<StreamEmojiPicker.Nav />
298+
<StreamEmojiPicker.Search />
299+
<StreamEmojiPicker.Grid />
300+
<StreamEmojiPicker.SkinTone />
301+
</StreamEmojiPicker.Root>;
302+
```
303+
304+
The rule: **the SDK owns the data + the selection; you own presentation + behavior.**
305+
`Root` exposes everything through `useEmojiPickerContext()` — read-only data
306+
(`categories`, `searchResults`, `status`, `skinTones`, `resolveNative`) and report-back
307+
setters (`selectEmoji`, `setQuery`, `setSkinTone`, `setActiveCategory` /
308+
`requestScrollToCategory`). A custom slot is just a component that calls the hook; when
309+
you replace a slot, that slot's mechanics (scrolling, scroll-spy, roving keyboard, ARIA,
310+
virtualization, hover-preview) become yours, but it keeps talking to the others through
311+
the shared state. Example — a paged grid driven by the built-in `Nav`:
312+
313+
```tsx
314+
function PagedGrid() {
315+
const {
316+
categories,
317+
activeCategoryId,
318+
searchResults,
319+
isSearching,
320+
selectEmoji,
321+
resolveNative,
322+
} = useEmojiPickerContext();
323+
const emojis = isSearching
324+
? (searchResults ?? [])
325+
: ((categories.find((c) => c.id === activeCategoryId) ?? categories[0])?.emojis ??
326+
[]);
327+
return (
328+
<div>
329+
{emojis.map((e) => (
330+
<button key={e.id} onClick={() => selectEmoji(e)}>
331+
{resolveNative(e)}
332+
</button>
333+
))}
334+
</div>
335+
);
336+
}
337+
```
338+
339+
`Root` always keeps the container-level a11y (dialog role, Escape-to-close,
340+
focus-return) and the loading/error states, regardless of how you recompose the inside.
341+
288342
- To let users **react with any emoji** (the reaction selector's `+` button), fill
289343
`reactionOptions.extended` with the full emoji set. It also gates display — a reaction
290344
whose type isn't in `quick`/`extended` is not rendered. Load it lazily from the emojis

examples/vite/src/AppSettings/state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type ChatViewSettingsState = {
1313

1414
export type EmojiPickerSettingsState = {
1515
autoFocus: boolean;
16-
engine: 'emoji-mart' | 'stream';
16+
engine: 'emoji-mart' | 'stream' | 'stream-composed';
1717
};
1818

1919
// Mirrors the SDK's EmojiPicker defaults; the settings tab resets to this.

examples/vite/src/AppSettings/tabs/EmojiPicker/EmojiPickerTab.tsx

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import EmojiMartPickerImport from '@emoji-mart/react';
22
import { type ComponentType, useState } from 'react';
33
import { Button } from 'stream-chat-react';
4-
import { EmojiPickerPanel, type EmojiSelection } from 'stream-chat-react/emojis';
4+
import {
5+
EmojiPickerPanel,
6+
type EmojiSelection,
7+
StreamEmojiPicker,
8+
useEmojiPickerContext,
9+
} from 'stream-chat-react/emojis';
510
import {
611
appSettingsStore,
712
DEFAULT_EMOJI_PICKER_SETTINGS,
@@ -62,17 +67,62 @@ function Field<T extends string | number | boolean>({
6267
);
6368
}
6469

70+
/**
71+
* A custom "paged" grid slot — renders only the active category (a layout the default
72+
* scrolling grid can't do), driven entirely by context: it reads `activeCategoryId`
73+
* (which the built-in Nav sets) and reports selection via `selectEmoji`. Demonstrates
74+
* that a replacement slot only needs the SDK's data + report-back APIs.
75+
*/
76+
const PagedGrid = () => {
77+
const {
78+
activeCategoryId,
79+
categories,
80+
isSearching,
81+
resolveNative,
82+
searchResults,
83+
selectEmoji,
84+
} = useEmojiPickerContext();
85+
const emojis = isSearching
86+
? (searchResults ?? [])
87+
: ((categories.find((category) => category.id === activeCategoryId) ?? categories[0])
88+
?.emojis ?? []);
89+
90+
return (
91+
<div className='str-chat__emoji-picker__body'>
92+
<div className='str-chat__emoji-picker__grid'>
93+
<div className='str-chat__emoji-picker__category-emojis'>
94+
{emojis.map((emoji) => (
95+
<button
96+
className='str-chat__emoji-picker__emoji'
97+
key={emoji.id}
98+
onClick={() => selectEmoji(emoji)}
99+
type='button'
100+
>
101+
{resolveNative(emoji)}
102+
</button>
103+
))}
104+
</div>
105+
</div>
106+
</div>
107+
);
108+
};
109+
65110
/**
66111
* Always-open picker wired to the current settings, so tweaks show instantly without
67112
* opening the composer. Skin tone and frequently-used are local to the preview —
68-
* selecting an emoji here feeds the "frequently used" row.
113+
* selecting an emoji here feeds the "frequently used" row. The `stream-composed` engine
114+
* shows the same picker assembled from `StreamEmojiPicker.Root` + slots, swapping in the
115+
* custom `PagedGrid` while keeping the built-in nav/search/preview/skin-tone.
69116
*/
70117
const EmojiPickerPreview = ({ options }: { options: EmojiPickerSettingsState }) => {
71118
const { mode } = useAppSettingsSelector((state) => state.theme);
72119
const { autoFocus, engine } = options;
73120
const [skinTone, setSkinTone] = useState(0);
74121
const [frequentlyUsedIds, setFrequentlyUsedIds] = useState<string[]>([]);
75122

123+
const recordUse = (emoji: EmojiSelection) =>
124+
setFrequentlyUsedIds((ids) => [emoji.id, ...ids.filter((id) => id !== emoji.id)]);
125+
76126
// The deprecated emoji-mart picker renders inline too and honors the same option
77127
// names, so the shared controls drive both engines.
78128
if (engine === 'emoji-mart') {
@@ -86,13 +136,30 @@ const EmojiPickerPreview = ({ options }: { options: EmojiPickerSettingsState })
86136
);
87137
}
88138

139+
if (engine === 'stream-composed') {
140+
return (
141+
<StreamEmojiPicker.Root
142+
frequentlyUsedIds={frequentlyUsedIds}
143+
onEmojiSelect={recordUse}
144+
onSkinToneChange={setSkinTone}
145+
skinToneIndex={skinTone}
146+
>
147+
<StreamEmojiPicker.Nav />
148+
<StreamEmojiPicker.Search autoFocus={autoFocus} />
149+
<PagedGrid />
150+
<div className='str-chat__emoji-picker__footer'>
151+
<StreamEmojiPicker.Preview />
152+
<StreamEmojiPicker.SkinTone />
153+
</div>
154+
</StreamEmojiPicker.Root>
155+
);
156+
}
157+
89158
return (
90159
<EmojiPickerPanel
91160
autoFocus={autoFocus}
92161
frequentlyUsedIds={frequentlyUsedIds}
93-
onEmojiSelect={(emoji: EmojiSelection) =>
94-
setFrequentlyUsedIds((ids) => [emoji.id, ...ids.filter((id) => id !== emoji.id)])
95-
}
162+
onEmojiSelect={recordUse}
96163
onSkinToneChange={setSkinTone}
97164
skinToneIndex={skinTone}
98165
/>
@@ -139,6 +206,7 @@ export const EmojiPickerTab = ({ close }: EmojiPickerTabProps) => {
139206
onSelect={(engine) => update({ engine })}
140207
options={[
141208
{ label: 'Stream (native)', value: 'stream' },
209+
{ label: 'Stream (composed)', value: 'stream-composed' },
142210
{ label: 'emoji-mart (deprecated)', value: 'emoji-mart' },
143211
]}
144212
value={emojiPicker.engine}

0 commit comments

Comments
 (0)