Skip to content

Commit d162162

Browse files
committed
refactor(emojis): slim down the StreamEmojiPicker public API
`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.
1 parent 5adf3af commit d162162

15 files changed

Lines changed: 112 additions & 713 deletions

File tree

AI.md

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -274,27 +274,16 @@ import 'stream-chat-react/css/emoji-picker.css';
274274
- On `StreamEmojiPicker`, skin tone and "frequently used" are integrator-managed props
275275
(`skinTone`/`onSkinToneChange`, `frequentlyUsedEmoji`/`onFrequentlyUsedChange`);
276276
the SDK does not persist them. See `examples/vite/` for a localStorage example.
277-
- On `StreamEmojiPicker`, `pickerProps` accepts a curated set of emoji-mart-compatible `Picker` options (plus
278-
`theme` and `style`):
279-
- **Layout**: `navPosition` / `previewPosition` (`'top' | 'bottom' | 'none'`),
280-
`searchPosition` (`'sticky' | 'static' | 'none'`), `skinTonePosition`
281-
(`'preview' | 'search' | 'none'`).
282-
- **Grid & content**: `perLine` (default `9`), `categories` (filter + reorder;
283-
`'frequent'` always prepends), `maxFrequentRows` (default `1`).
284-
- **Filtering & polish**: `exceptEmojis`, `emojiVersion`, `noCountryFlags`,
285-
`previewEmoji`, `noResultsEmoji`, `autoFocus`, `onClickOutside`.
286-
287-
Divergences from emoji-mart's defaults: `autoFocus` defaults to `true`; `emojiVersion`
288-
is unfiltered by default (the bundled set 15); `previewEmoji` / `noResultsEmoji` default
289-
to the SDK's placeholder / empty state; `noCountryFlags` is opt-in (no Windows
290-
auto-detect); `categories` cannot reposition `'frequent'`.
291-
292-
Not supported (rejected by the type; ignored with a console warning at runtime): image
293-
sets (`set`, `getSpritesheetURL`), `custom` emoji, `data`, `i18n` / `locale`,
294-
`dynamicWidth`, `icons`, `categoryIcons`. Sizing knobs (`emojiSize`, `emojiButtonSize`,
295-
…) are `--str-chat__emoji-picker-*` CSS variables, not props. Skin tone uses the
296-
first-class `skinTone` / `defaultSkinTone` props (not emoji-mart's `skin`). Try these
297-
live in the "Emoji Picker" settings tab of `examples/vite/`.
277+
- `StreamEmojiPicker` exposes a small, flat set of props (no emoji-mart-style
278+
`pickerProps` bag): `theme` (`'auto'` — the default, inherits the ancestor SDK theme —
279+
`'light'`, or `'dark'`), `style`, `categories` (filter + reorder; `'frequent'` always
280+
prepends and cannot be repositioned), `exceptEmojis`, `autoFocus` (default `true`), and
281+
`onClickOutside`. Layout and sizing are CSS, not props: set
282+
`--str-chat__emoji-picker-per-line` (default `9`) for the column count and the other
283+
`--str-chat__emoji-picker-*` tokens for sizing/colors. Emoji-mart-only features (image
284+
`set`s, `custom` emoji, `data`, `i18n` / `locale`, …) aren't available on the built-in
285+
picker — keep using the deprecated `EmojiPicker` if you still need them. Try it live in
286+
the "Emoji Picker" settings tab of `examples/vite/`.
298287

299288
- To let users **react with any emoji** (the reaction selector's `+` button), fill
300289
`reactionOptions.extended` with the full emoji set. It also gates display — a reaction
@@ -321,9 +310,11 @@ import 'stream-chat-react/css/emoji-picker.css';
321310
- **Migrating from emoji-mart** (the deprecated `EmojiPicker`): swap `EmojiPicker`
322311
`StreamEmojiPicker`, then remove the `emoji-mart` / `@emoji-mart/react` /
323312
`@emoji-mart/data` installs and any `init({ data })` call. The deprecated `EmojiPicker`
324-
still accepts raw emoji-mart `pickerProps` (e.g. `set`, `emojiSize`); `StreamEmojiPicker`
325-
uses the curated `pickerProps` above. Autocomplete (`createTextComposerEmojiMiddleware`)
326-
and reactions (`mapEmojiMartData`) are engine-agnostic and need no changes.
313+
takes a raw emoji-mart `pickerProps` bag (e.g. `set`, `emojiSize`); `StreamEmojiPicker`
314+
instead exposes flat props (`theme`, `categories`, `exceptEmojis`, `autoFocus`, …) and
315+
moves layout/sizing to `--str-chat__emoji-picker-*` CSS tokens. Autocomplete
316+
(`createTextComposerEmojiMiddleware`) and reactions (`mapEmojiMartData`) are
317+
engine-agnostic and need no changes.
327318

328319
**Reference**: See `examples/tutorial/src/6-emoji-picker/`
329320

examples/vite/src/App.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ const EmojiPickerWithCustomOptions = (
226226
return (
227227
<StreamEmojiPicker
228228
{...props}
229+
autoFocus={pickerOptions.autoFocus}
229230
frequentlyUsedEmoji={frequentlyUsedEmoji}
230231
onFrequentlyUsedChange={(ids) => {
231232
setFrequentlyUsedEmoji(ids);
@@ -235,12 +236,8 @@ const EmojiPickerWithCustomOptions = (
235236
setSkinTone(tone);
236237
writeStored(EMOJI_SKIN_TONE_KEY, tone);
237238
}}
238-
pickerProps={{
239-
...props.pickerProps,
240-
...pickerOptions,
241-
theme: mode,
242-
}}
243239
skinTone={skinTone}
240+
theme={mode}
244241
/>
245242
);
246243
};

examples/vite/src/AppSettings/state.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,12 @@ export type ChatViewSettingsState = {
1414
export type EmojiPickerSettingsState = {
1515
autoFocus: boolean;
1616
engine: 'emoji-mart' | 'stream';
17-
maxFrequentRows: number;
18-
navPosition: 'top' | 'bottom' | 'none';
19-
noCountryFlags: boolean;
20-
perLine: number;
21-
previewPosition: 'top' | 'bottom' | 'none';
22-
searchPosition: 'sticky' | 'static' | 'none';
23-
skinTonePosition: 'preview' | 'search' | 'none';
2417
};
2518

2619
// Mirrors the SDK's EmojiPicker defaults; the settings tab resets to this.
2720
export const DEFAULT_EMOJI_PICKER_SETTINGS: EmojiPickerSettingsState = {
2821
autoFocus: true,
2922
engine: 'stream',
30-
maxFrequentRows: 1,
31-
navPosition: 'top',
32-
noCountryFlags: false,
33-
perLine: 9,
34-
previewPosition: 'bottom',
35-
searchPosition: 'sticky',
36-
skinTonePosition: 'preview',
3723
};
3824

3925
export type ThemeSettingsState = {

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

Lines changed: 9 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -62,27 +62,23 @@ function Field<T extends string | number | boolean>({
6262
);
6363
}
6464

65-
const numberOptions = (values: number[]) =>
66-
values.map((value) => ({ label: String(value), value }));
67-
6865
/**
6966
* Always-open picker wired to the current settings, so tweaks show instantly without
7067
* opening the composer. Skin tone and frequently-used are local to the preview —
71-
* selecting an emoji here feeds the "frequently used" row so `maxFrequentRows` can be
72-
* exercised too.
68+
* selecting an emoji here feeds the "frequently used" row.
7369
*/
7470
const EmojiPickerPreview = ({ options }: { options: EmojiPickerSettingsState }) => {
7571
const { mode } = useAppSettingsSelector((state) => state.theme);
76-
const { engine, ...pickerOptions } = options;
72+
const { autoFocus, engine } = options;
7773
const [skinTone, setSkinTone] = useState(0);
7874
const [frequentlyUsedIds, setFrequentlyUsedIds] = useState<string[]>([]);
7975

80-
// The deprecated emoji-mart picker renders inline too and honors the same
81-
// emoji-mart-compatible option names, so the same controls drive both engines.
76+
// The deprecated emoji-mart picker renders inline too and honors the same option
77+
// names, so the shared controls drive both engines.
8278
if (engine === 'emoji-mart') {
8379
return (
8480
<EmojiMart
85-
{...pickerOptions}
81+
autoFocus={autoFocus}
8682
data={async () => (await import('@emoji-mart/data')).default}
8783
onEmojiSelect={() => undefined}
8884
theme={mode}
@@ -92,20 +88,20 @@ const EmojiPickerPreview = ({ options }: { options: EmojiPickerSettingsState })
9288

9389
return (
9490
<EmojiPickerPanel
91+
autoFocus={autoFocus}
9592
frequentlyUsedIds={frequentlyUsedIds}
9693
onEmojiSelect={(emoji: EmojiSelection) =>
9794
setFrequentlyUsedIds((ids) => [emoji.id, ...ids.filter((id) => id !== emoji.id)])
9895
}
9996
onSkinToneChange={setSkinTone}
100-
options={{ ...pickerOptions, exceptEmojis: [] }}
10197
skinToneIndex={skinTone}
10298
/>
10399
);
104100
};
105101

106102
/**
107-
* Playground for the built-in EmojiPicker's `pickerProps`. Each control writes to the
108-
* app settings store; the live preview (and the composer's picker via
103+
* Playground for the built-in `StreamEmojiPicker`. Each control writes to the app
104+
* settings store; the live preview (and the composer's picker via
109105
* `EmojiPickerWithCustomOptions`) reflect the change instantly.
110106
*/
111107
export const EmojiPickerTab = ({ close }: EmojiPickerTabProps) => {
@@ -118,7 +114,7 @@ export const EmojiPickerTab = ({ close }: EmojiPickerTabProps) => {
118114
<div className='app__settings-modal__content-stack'>
119115
<SettingsTabLayoutHeader
120116
close={close}
121-
description='Switch between the Stream (native) and deprecated emoji-mart pickers, and configure pickerProps. The live preview and the composer’s picker both update instantly.'
117+
description='Switch between the Stream (native) and deprecated emoji-mart pickers. The live preview and the composer’s picker both update instantly.'
122118
title='Emoji Picker'
123119
/>
124120

@@ -147,58 +143,6 @@ export const EmojiPickerTab = ({ close }: EmojiPickerTabProps) => {
147143
]}
148144
value={emojiPicker.engine}
149145
/>
150-
<Field<EmojiPickerSettingsState['navPosition']>
151-
label='Navigation position'
152-
onSelect={(navPosition) => update({ navPosition })}
153-
options={[
154-
{ label: 'Top', value: 'top' },
155-
{ label: 'Bottom', value: 'bottom' },
156-
{ label: 'None', value: 'none' },
157-
]}
158-
value={emojiPicker.navPosition}
159-
/>
160-
<Field<EmojiPickerSettingsState['previewPosition']>
161-
label='Preview position'
162-
onSelect={(previewPosition) => update({ previewPosition })}
163-
options={[
164-
{ label: 'Top', value: 'top' },
165-
{ label: 'Bottom', value: 'bottom' },
166-
{ label: 'None', value: 'none' },
167-
]}
168-
value={emojiPicker.previewPosition}
169-
/>
170-
<Field<EmojiPickerSettingsState['searchPosition']>
171-
label='Search position'
172-
onSelect={(searchPosition) => update({ searchPosition })}
173-
options={[
174-
{ label: 'Sticky', value: 'sticky' },
175-
{ label: 'Static', value: 'static' },
176-
{ label: 'None', value: 'none' },
177-
]}
178-
value={emojiPicker.searchPosition}
179-
/>
180-
<Field<EmojiPickerSettingsState['skinTonePosition']>
181-
label='Skin-tone position'
182-
onSelect={(skinTonePosition) => update({ skinTonePosition })}
183-
options={[
184-
{ label: 'Preview', value: 'preview' },
185-
{ label: 'Search', value: 'search' },
186-
{ label: 'None', value: 'none' },
187-
]}
188-
value={emojiPicker.skinTonePosition}
189-
/>
190-
<Field<number>
191-
label='Emoji per line'
192-
onSelect={(perLine) => update({ perLine })}
193-
options={numberOptions([7, 8, 9, 10])}
194-
value={emojiPicker.perLine}
195-
/>
196-
<Field<number>
197-
label='Frequently-used rows'
198-
onSelect={(maxFrequentRows) => update({ maxFrequentRows })}
199-
options={numberOptions([1, 2, 3, 4])}
200-
value={emojiPicker.maxFrequentRows}
201-
/>
202146
<Field<boolean>
203147
label='Auto-focus search'
204148
onSelect={(autoFocus) => update({ autoFocus })}
@@ -208,15 +152,6 @@ export const EmojiPickerTab = ({ close }: EmojiPickerTabProps) => {
208152
]}
209153
value={emojiPicker.autoFocus}
210154
/>
211-
<Field<boolean>
212-
label='Country flags'
213-
onSelect={(noCountryFlags) => update({ noCountryFlags })}
214-
options={[
215-
{ label: 'Show', value: false },
216-
{ label: 'Hide', value: true },
217-
]}
218-
value={emojiPicker.noCountryFlags}
219-
/>
220155
</div>
221156
<div className='app__emoji-settings__preview'>
222157
<div className='app__settings-modal__field-label'>Live preview</div>

src/plugins/Emojis/StreamEmojiPicker.tsx

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef, useState } from 'react';
1+
import React, { type CSSProperties, useEffect, useRef, useState } from 'react';
22

33
import { useMessageComposerContext, useTranslationContext } from '../../context';
44
import {
@@ -12,46 +12,29 @@ import { useIsCooldownActive } from '../../components/MessageComposer/hooks/useI
1212
import { EmojiPickerPanel } from './components';
1313
import { useFrequentlyUsedEmoji } from './hooks/useFrequentlyUsedEmoji';
1414
import { useSkinTone } from './hooks/useSkinTone';
15-
import {
16-
type EmojiPickerPassthroughProps,
17-
resolveEmojiPickerOptions,
18-
warnUnsupportedPickerProps,
19-
} from './options';
2015

2116
const isShadowRoot = (node: Node): node is ShadowRoot => !!(node as ShadowRoot).host;
2217

23-
export type {
24-
EmojiPickerNavPosition,
25-
EmojiPickerPassthroughProps,
26-
EmojiPickerPreviewPosition,
27-
EmojiPickerSearchPosition,
28-
EmojiPickerSkinTonePosition,
29-
} from './options';
30-
3118
export type StreamEmojiPickerProps = {
3219
ButtonIconComponent?: React.ComponentType;
3320
buttonClassName?: string;
3421
pickerContainerClassName?: string;
3522
wrapperClassName?: string;
3623
closeOnEmojiSelect?: boolean;
37-
/**
38-
* Presentation + curated layout/behavior options for the picker panel, using
39-
* emoji-mart-compatible names (`theme`, `style`, `perLine`, `navPosition`,
40-
* `previewPosition`, `searchPosition`, `skinTonePosition`, `categories`,
41-
* `exceptEmojis`, `emojiVersion`, `maxFrequentRows`, `noCountryFlags`,
42-
* `previewEmoji`, `noResultsEmoji`, `autoFocus`, `onClickOutside`).
43-
*
44-
* Not every emoji-mart `Picker` option is supported: image sets (`set`,
45-
* `getSpritesheetURL`), `custom` emoji, `data`, `i18n`/`locale`, `dynamicWidth`,
46-
* `icons`, and `categoryIcons` are rejected by the type and ignored (with a console
47-
* warning) at runtime; sizing knobs (`emojiSize`, …) are CSS tokens instead. See the
48-
* emoji migration notes in `AI.md`.
49-
*/
50-
pickerProps?: EmojiPickerPassthroughProps;
51-
/**
52-
* Floating UI placement (default: 'top-end') for the picker popover
53-
*/
24+
/** Floating UI placement (default: 'top-end') for the picker popover. */
5425
placement?: PopperLikePlacement;
26+
/** Focus the search input when the picker opens (default `true`). */
27+
autoFocus?: boolean;
28+
/** Category ids to show, in order. Defaults to the dataset order. `frequent` always prepends. */
29+
categories?: string[];
30+
/** Emoji ids to exclude from the grid and search. */
31+
exceptEmojis?: string[];
32+
/** Called when a pointer press lands outside the open picker. */
33+
onClickOutside?: () => void;
34+
/** Inline styles applied to the picker panel root. */
35+
style?: CSSProperties;
36+
/** Color theme. 'auto' (default) inherits the ancestor SDK theme; 'light'/'dark' force it. */
37+
theme?: 'auto' | 'light' | 'dark';
5538
/** Uncontrolled initial skin tone index (0 = default, 1–5 = light → dark). */
5639
defaultSkinTone?: number;
5740
/**
@@ -114,19 +97,10 @@ export const StreamEmojiPicker = (props: StreamEmojiPickerProps) => {
11497
const { pickerContainerClassName, wrapperClassName } = classNames;
11598

11699
const { ButtonIconComponent = IconEmoji } = props;
117-
const pickerStyle = props.pickerProps?.style;
118-
const options = resolveEmojiPickerOptions(props.pickerProps);
119100
// Latest-ref so the click-outside listener isn't re-attached when the callback identity
120101
// changes between renders.
121-
const onClickOutsideRef = useRef(props.pickerProps?.onClickOutside);
122-
onClickOutsideRef.current = props.pickerProps?.onClickOutside;
123-
124-
const pickerPropsKeys = Object.keys(props.pickerProps ?? {});
125-
useEffect(() => {
126-
warnUnsupportedPickerProps(props.pickerProps);
127-
// Re-check only when the set of provided keys changes.
128-
// eslint-disable-next-line react-hooks/exhaustive-deps
129-
}, [pickerPropsKeys.join(',')]);
102+
const onClickOutsideRef = useRef(props.onClickOutside);
103+
onClickOutsideRef.current = props.onClickOutside;
130104

131105
useEffect(() => {
132106
if (!popperElement || !referenceElement) return;
@@ -160,6 +134,9 @@ export const StreamEmojiPicker = (props: StreamEmojiPickerProps) => {
160134
style={{ left: x ?? 0, position: strategy, top: y ?? 0 }}
161135
>
162136
<EmojiPickerPanel
137+
autoFocus={props.autoFocus}
138+
categories={props.categories}
139+
exceptEmojis={props.exceptEmojis}
163140
frequentlyUsedIds={frequentlyUsedIds}
164141
onClose={() => {
165142
setDisplayPicker(false);
@@ -177,10 +154,9 @@ export const StreamEmojiPicker = (props: StreamEmojiPickerProps) => {
177154
}
178155
}}
179156
onSkinToneChange={setSkinTone}
180-
options={options}
181157
skinToneIndex={skinTone}
182-
style={pickerStyle}
183-
theme={props.pickerProps?.theme}
158+
style={props.style}
159+
theme={props.theme}
184160
/>
185161
</div>
186162
)}

src/plugins/Emojis/__tests__/StreamEmojiPicker.test.tsx

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -132,40 +132,10 @@ describe('StreamEmojiPicker session state', () => {
132132
});
133133
});
134134

135-
describe('StreamEmojiPicker pickerProps', () => {
136-
it('warns about unsupported (emoji-mart) pickerProps, but not about theme/style', () => {
137-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
138-
139-
const { unmount } = render(
140-
<StreamEmojiPicker
141-
// @ts-expect-error emoji-mart-only Picker options (image sets) are not supported
142-
pickerProps={{ set: 'apple', theme: 'dark' }}
143-
/>,
144-
);
145-
expect(warn).toHaveBeenCalledWith(expect.stringContaining('set'));
146-
unmount();
147-
148-
warn.mockClear();
149-
render(<StreamEmojiPicker pickerProps={{ style: { width: 320 }, theme: 'light' }} />);
150-
expect(warn).not.toHaveBeenCalled();
151-
152-
warn.mockRestore();
153-
});
154-
155-
it('does not warn when only supported (curated) picker options are passed', () => {
156-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
157-
render(
158-
<StreamEmojiPicker
159-
pickerProps={{ navPosition: 'bottom', perLine: 8, theme: 'light' }}
160-
/>,
161-
);
162-
expect(warn).not.toHaveBeenCalled();
163-
warn.mockRestore();
164-
});
165-
135+
describe('StreamEmojiPicker props', () => {
166136
it('calls onClickOutside when a pointer press lands outside the open picker', () => {
167137
const onClickOutside = vi.fn();
168-
render(<StreamEmojiPicker pickerProps={{ onClickOutside }} />);
138+
render(<StreamEmojiPicker onClickOutside={onClickOutside} />);
169139
openPicker();
170140
fireEvent.pointerDown(document.body);
171141
expect(onClickOutside).toHaveBeenCalledTimes(1);

0 commit comments

Comments
 (0)