@fibery/ui-kit 1.22.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/src/design-system.ts +2 -0
- package/src/emoji-picker/app-icon-picker.tsx +21 -15
- package/src/emoji-picker/emoji-picker-content-with-color.tsx +9 -11
- package/src/emoji-picker/emoji-picker-preview-footer.tsx +94 -0
- package/src/emoji-picker/emoji-picker.tsx +3 -28
- package/src/emoji-picker/icon-emoji-picker.tsx +1 -1
- package/src/emoji-picker/primitives/category-label.tsx +42 -0
- package/src/emoji-picker/primitives/content.tsx +15 -3
- package/src/emoji-picker/primitives/footer.tsx +4 -2
- package/src/emoji-picker/primitives/grid-item.tsx +73 -0
- package/src/emoji-picker/primitives/grid.tsx +332 -76
- package/src/emoji-picker/primitives/preview-provider.tsx +27 -0
- package/src/emoji-picker/primitives/root.tsx +8 -5
- package/src/emoji-picker/primitives/search.tsx +50 -6
- package/src/emoji-picker/stores/emoji-data-store.tsx +13 -10
- package/src/emoji-picker/stores/lazy-icon-data-store.tsx +4 -2
- package/src/emoji-picker/stores/static-emoji-data-store.tsx +11 -6
- package/src/lists/list-row-surface.tsx +8 -0
- package/src/select/index.tsx +5 -0
- package/src/emoji-picker/primitives/category.tsx +0 -177
- package/src/emoji-picker/stores/lazy-emoji-data-store.tsx +0 -29
|
@@ -1,99 +1,355 @@
|
|
|
1
1
|
import {css, cx} from "@linaria/core";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import {useMemo, useRef, forwardRef, useImperativeHandle} from "react";
|
|
3
|
+
import {GroupedVirtuoso, GroupedVirtuosoHandle, CalculateViewLocation} from "react-virtuoso";
|
|
4
|
+
import {textStyles, themeVars} from "../../design-system";
|
|
5
|
+
import {EmojiDataStore, useEmojiDataStore, useEmojiDataStoreSelector} from "../stores/emoji-data-store";
|
|
4
6
|
import {get as getFrequentlyUsed} from "../utils/frequently";
|
|
5
|
-
import {
|
|
7
|
+
import {EmojiPickerGridItem, EmojiPickerGridItemProps} from "./grid-item";
|
|
8
|
+
import {EmojiPickerCategoryLabel} from "./category-label";
|
|
6
9
|
import {useEmojiPickerContentSettings} from "./content";
|
|
10
|
+
import {Emoji} from "./emoji";
|
|
7
11
|
import {contentHorizontalPadding} from "./layout";
|
|
12
|
+
import {useEmojiPickerI18N} from "./root";
|
|
8
13
|
import {useFoundEmojis} from "./search-provider";
|
|
14
|
+
import {useEmojiPreview, useSetEmojiPreview} from "./preview-provider";
|
|
15
|
+
import type {Category, EmojiItem} from "@fibery/emoji-data";
|
|
9
16
|
|
|
10
17
|
const gridWrapperCss = css`
|
|
11
|
-
height: 258px;
|
|
12
|
-
overflow-y: scroll;
|
|
13
18
|
overflow-x: hidden;
|
|
14
19
|
|
|
15
20
|
padding: 0 ${contentHorizontalPadding}px;
|
|
16
21
|
`;
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
const disableScrollCss = css`
|
|
24
|
+
overflow-y: hidden !important;
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const makeSearchCategory = ({emojis}: {emojis: string[]}) => ({
|
|
28
|
+
id: "search",
|
|
29
|
+
name: "Search",
|
|
30
|
+
emojis,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const makeRecentCategory = ({emojis}: {emojis: string[]}) => ({
|
|
34
|
+
id: "recent",
|
|
35
|
+
name: "Recent",
|
|
36
|
+
emojis,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const notFoundCss = css`
|
|
40
|
+
padding-top: 102px;
|
|
41
|
+
text-align: center;
|
|
21
42
|
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
${textStyles.regular};
|
|
44
|
+
color: ${themeVars.accentTextColor};
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const NotFound = () => {
|
|
48
|
+
const i18n = useEmojiPickerI18N();
|
|
24
49
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
return (
|
|
51
|
+
<div className={notFoundCss}>
|
|
52
|
+
<Emoji emoji="sleuth_or_spy" size={38} />
|
|
53
|
+
<div>{i18n.notfound}</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type ListItem = {
|
|
59
|
+
categoryId: string;
|
|
60
|
+
emojis: string[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const makeData = ({
|
|
64
|
+
emojiData,
|
|
65
|
+
foundEmojis,
|
|
66
|
+
categories,
|
|
67
|
+
perLine,
|
|
68
|
+
}: {
|
|
69
|
+
emojiData: EmojiDataStore;
|
|
70
|
+
foundEmojis: EmojiItem[] | null;
|
|
71
|
+
categories: readonly Category[];
|
|
72
|
+
perLine: number;
|
|
73
|
+
}) => {
|
|
74
|
+
let allCategories = [];
|
|
75
|
+
|
|
76
|
+
if (foundEmojis) {
|
|
77
|
+
allCategories = [makeSearchCategory({emojis: foundEmojis.map(({id}) => id)})];
|
|
78
|
+
} else {
|
|
79
|
+
const recentCategory = makeRecentCategory({
|
|
80
|
+
emojis: getFrequentlyUsed(perLine).filter((id) => {
|
|
30
81
|
const emoji = emojiData.get(id);
|
|
31
82
|
const hidden = emoji && "hidden" in emoji ? emoji.hidden : false;
|
|
32
83
|
return emoji && !hidden;
|
|
33
84
|
}),
|
|
34
|
-
})
|
|
35
|
-
[contentSettings, emojiData]
|
|
36
|
-
);
|
|
85
|
+
});
|
|
37
86
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
scrollParent={scrollParent}
|
|
45
|
-
renderEmoji={renderEmoji}
|
|
46
|
-
/>
|
|
47
|
-
) : null}
|
|
48
|
-
{categories
|
|
49
|
-
.filter((category) => category.emojis.length !== 0)
|
|
50
|
-
.map((category, idx) => {
|
|
51
|
-
return (
|
|
52
|
-
<EmojiPickerCategory
|
|
53
|
-
preRender={idx < 2}
|
|
54
|
-
key={category.id}
|
|
55
|
-
category={category}
|
|
56
|
-
scrollParent={scrollParent}
|
|
57
|
-
renderEmoji={renderEmoji}
|
|
58
|
-
/>
|
|
59
|
-
);
|
|
60
|
-
})}
|
|
61
|
-
</>
|
|
62
|
-
);
|
|
63
|
-
};
|
|
64
|
-
export type EmojiPickerGridProps = {
|
|
65
|
-
className?: string;
|
|
66
|
-
} & Pick<EmojiPickerCategoryProps, "renderEmoji">;
|
|
67
|
-
|
|
68
|
-
export const EmojiPickerGrid: React.FC<EmojiPickerGridProps> = ({className, renderEmoji}) => {
|
|
69
|
-
const foundEmojis = useFoundEmojis();
|
|
70
|
-
const [gridElement, setGridElement] = useState<HTMLDivElement | null>(null);
|
|
71
|
-
|
|
72
|
-
const searchCategory = useMemo(
|
|
73
|
-
() =>
|
|
74
|
-
foundEmojis
|
|
75
|
-
? {
|
|
76
|
-
id: "search",
|
|
77
|
-
name: "Search",
|
|
78
|
-
emojis: foundEmojis.map(({id}) => id),
|
|
79
|
-
}
|
|
80
|
-
: null,
|
|
81
|
-
[foundEmojis]
|
|
82
|
-
);
|
|
87
|
+
allCategories = [recentCategory, ...categories].filter((c) => c.emojis.length > 0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const counts = [];
|
|
91
|
+
|
|
92
|
+
const dataByRows: Array<ListItem> = [];
|
|
83
93
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
for (const {emojis, id} of allCategories) {
|
|
95
|
+
const rowsForCategory = Math.ceil(emojis.length / perLine);
|
|
96
|
+
|
|
97
|
+
let tmp: string[] = [];
|
|
98
|
+
emojis.forEach((em) => {
|
|
99
|
+
tmp.push(em);
|
|
100
|
+
|
|
101
|
+
if (tmp.length === perLine) {
|
|
102
|
+
dataByRows.push({categoryId: id, emojis: tmp});
|
|
103
|
+
tmp = [];
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (tmp.length > 0) {
|
|
107
|
+
dataByRows.push({categoryId: id, emojis: tmp});
|
|
87
108
|
}
|
|
88
|
-
}, [foundEmojis, gridElement]);
|
|
89
109
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
counts.push(rowsForCategory);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {allCategories, counts, dataByRows};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// workaround for https://github.com/petyosi/react-virtuoso/issues/1019
|
|
117
|
+
const calculateViewLocation: CalculateViewLocation = ({
|
|
118
|
+
itemTop,
|
|
119
|
+
itemBottom,
|
|
120
|
+
viewportTop,
|
|
121
|
+
viewportBottom,
|
|
122
|
+
locationParams: {behavior, ...rest},
|
|
123
|
+
}) => {
|
|
124
|
+
if (itemTop - 32 < viewportTop) {
|
|
125
|
+
return {...rest, behavior, align: "start"};
|
|
126
|
+
}
|
|
127
|
+
if (itemBottom > viewportBottom) {
|
|
128
|
+
return {...rest, behavior, align: "end"};
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type EmojiPickerGridHandle = {
|
|
134
|
+
navigate: (direction: "up" | "down" | "left" | "right") => boolean;
|
|
135
|
+
scrollToTop: () => void;
|
|
99
136
|
};
|
|
137
|
+
|
|
138
|
+
export type EmojiPickerGridProps = {
|
|
139
|
+
disableScroll?: boolean;
|
|
140
|
+
} & Pick<EmojiPickerGridItemProps, "renderEmoji">;
|
|
141
|
+
|
|
142
|
+
export const EmojiPickerGrid = forwardRef<EmojiPickerGridHandle, EmojiPickerGridProps>(
|
|
143
|
+
({disableScroll, renderEmoji}, ref) => {
|
|
144
|
+
const setEmojiPreview = useSetEmojiPreview();
|
|
145
|
+
const foundEmojis = useFoundEmojis();
|
|
146
|
+
const virtuosoRef = useRef<GroupedVirtuosoHandle>(null);
|
|
147
|
+
// const [gridElement, setGridElement] = useState<HTMLDivElement | null>(null);
|
|
148
|
+
|
|
149
|
+
const contentSettings = useEmojiPickerContentSettings();
|
|
150
|
+
|
|
151
|
+
// TODO: accessing emojiData inside useMemo is not reactive so it won't be re-rendered if emojis are modified
|
|
152
|
+
const emojiData = useEmojiDataStore();
|
|
153
|
+
|
|
154
|
+
const categories = useEmojiDataStoreSelector((store) => store.getCategories());
|
|
155
|
+
|
|
156
|
+
const {counts, allCategories, dataByRows} = useMemo(
|
|
157
|
+
() => makeData({categories, emojiData, foundEmojis, perLine: contentSettings.perLine}),
|
|
158
|
+
[categories, contentSettings.perLine, emojiData, foundEmojis]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const emojiPreview = useEmojiPreview();
|
|
162
|
+
|
|
163
|
+
useImperativeHandle(
|
|
164
|
+
ref,
|
|
165
|
+
() => {
|
|
166
|
+
return {
|
|
167
|
+
scrollToTop: () => {
|
|
168
|
+
virtuosoRef.current?.scrollToIndex(0);
|
|
169
|
+
},
|
|
170
|
+
navigate(direction) {
|
|
171
|
+
const virtuoso = virtuosoRef.current;
|
|
172
|
+
if (!virtuoso) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// TODO: not optimal. Probably better to lift up in context and store focused index separately instead of calculating it every time
|
|
177
|
+
let colIdx = -1;
|
|
178
|
+
let rowIdx = emojiPreview
|
|
179
|
+
? dataByRows.findIndex(
|
|
180
|
+
({categoryId, emojis}) =>
|
|
181
|
+
categoryId === emojiPreview.categoryId &&
|
|
182
|
+
(colIdx = emojis.findIndex((id) => id === emojiPreview.emoji.id)) > -1
|
|
183
|
+
)
|
|
184
|
+
: -1;
|
|
185
|
+
|
|
186
|
+
switch (direction) {
|
|
187
|
+
case "down": {
|
|
188
|
+
rowIdx++;
|
|
189
|
+
const nextRow = dataByRows[rowIdx];
|
|
190
|
+
if (nextRow) {
|
|
191
|
+
colIdx = Math.min(Math.max(colIdx, 0), nextRow.emojis.length - 1);
|
|
192
|
+
|
|
193
|
+
virtuoso.scrollIntoView({
|
|
194
|
+
index: rowIdx,
|
|
195
|
+
done: () =>
|
|
196
|
+
setEmojiPreview({
|
|
197
|
+
categoryId: nextRow.categoryId,
|
|
198
|
+
emoji: emojiData.get(nextRow.emojis[colIdx]) as EmojiItem,
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case "up": {
|
|
207
|
+
rowIdx--;
|
|
208
|
+
const nextRow = dataByRows[rowIdx];
|
|
209
|
+
if (nextRow) {
|
|
210
|
+
colIdx = Math.min(Math.max(colIdx, 0), nextRow.emojis.length - 1);
|
|
211
|
+
|
|
212
|
+
virtuoso.scrollIntoView({
|
|
213
|
+
index: rowIdx,
|
|
214
|
+
calculateViewLocation: calculateViewLocation,
|
|
215
|
+
done: () =>
|
|
216
|
+
setEmojiPreview({
|
|
217
|
+
categoryId: nextRow.categoryId,
|
|
218
|
+
emoji: emojiData.get(nextRow.emojis[colIdx]) as EmojiItem,
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case "right": {
|
|
228
|
+
let nextRow = dataByRows[rowIdx];
|
|
229
|
+
colIdx++;
|
|
230
|
+
|
|
231
|
+
if (!nextRow?.emojis[colIdx]) {
|
|
232
|
+
rowIdx++;
|
|
233
|
+
nextRow = dataByRows[rowIdx];
|
|
234
|
+
|
|
235
|
+
if (nextRow) {
|
|
236
|
+
colIdx = 0;
|
|
237
|
+
virtuoso.scrollIntoView({
|
|
238
|
+
index: rowIdx,
|
|
239
|
+
done: () =>
|
|
240
|
+
setEmojiPreview({
|
|
241
|
+
categoryId: nextRow.categoryId,
|
|
242
|
+
emoji: emojiData.get(nextRow.emojis[colIdx]) as EmojiItem,
|
|
243
|
+
}),
|
|
244
|
+
});
|
|
245
|
+
return true;
|
|
246
|
+
} else {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
setEmojiPreview({
|
|
252
|
+
categoryId: nextRow.categoryId,
|
|
253
|
+
emoji: emojiData.get(nextRow.emojis[colIdx]) as EmojiItem,
|
|
254
|
+
});
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
case "left": {
|
|
258
|
+
const row = dataByRows[rowIdx];
|
|
259
|
+
colIdx--;
|
|
260
|
+
|
|
261
|
+
if (!row?.emojis[colIdx]) {
|
|
262
|
+
rowIdx--;
|
|
263
|
+
const nextRow = dataByRows[rowIdx];
|
|
264
|
+
|
|
265
|
+
if (nextRow) {
|
|
266
|
+
colIdx = nextRow.emojis.length - 1;
|
|
267
|
+
virtuoso.scrollIntoView({
|
|
268
|
+
index: rowIdx,
|
|
269
|
+
calculateViewLocation,
|
|
270
|
+
done: () =>
|
|
271
|
+
setEmojiPreview({
|
|
272
|
+
categoryId: nextRow.categoryId,
|
|
273
|
+
emoji: emojiData.get(nextRow.emojis[colIdx]) as EmojiItem,
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
return true;
|
|
277
|
+
} else {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
setEmojiPreview({
|
|
283
|
+
categoryId: row.categoryId,
|
|
284
|
+
emoji: emojiData.get(row.emojis[colIdx]) as EmojiItem,
|
|
285
|
+
});
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
default:
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return false;
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
[dataByRows, emojiData, emojiPreview, setEmojiPreview]
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div
|
|
301
|
+
className={css`
|
|
302
|
+
height: 258px;
|
|
303
|
+
position: relative;
|
|
304
|
+
`}
|
|
305
|
+
>
|
|
306
|
+
<GroupedVirtuoso
|
|
307
|
+
ref={virtuosoRef}
|
|
308
|
+
className={cx(gridWrapperCss, disableScroll && disableScrollCss)}
|
|
309
|
+
defaultItemHeight={contentSettings.emojiContainerSize}
|
|
310
|
+
fixedItemHeight={contentSettings.emojiContainerSize}
|
|
311
|
+
increaseViewportBy={130}
|
|
312
|
+
groupCounts={counts}
|
|
313
|
+
groupContent={(i) => <EmojiPickerCategoryLabel category={allCategories[i]} />}
|
|
314
|
+
itemContent={(rowIdx) => {
|
|
315
|
+
const row = dataByRows[rowIdx];
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<div
|
|
319
|
+
className={css`
|
|
320
|
+
display: flex;
|
|
321
|
+
`}
|
|
322
|
+
>
|
|
323
|
+
{row.emojis.map((id) => {
|
|
324
|
+
return (
|
|
325
|
+
<EmojiPickerGridItem
|
|
326
|
+
key={`${row.categoryId}-${id}`}
|
|
327
|
+
id={id}
|
|
328
|
+
categoryId={row.categoryId}
|
|
329
|
+
renderEmoji={renderEmoji}
|
|
330
|
+
highlighted={Boolean(
|
|
331
|
+
emojiPreview && emojiPreview.emoji.id === id && emojiPreview.categoryId === row.categoryId
|
|
332
|
+
)}
|
|
333
|
+
/>
|
|
334
|
+
);
|
|
335
|
+
})}
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}}
|
|
339
|
+
/>
|
|
340
|
+
|
|
341
|
+
{/* don't want to remount GroupedVirtuoso that's why I positioned NotFound as absolute */}
|
|
342
|
+
{foundEmojis && foundEmojis.length === 0 ? (
|
|
343
|
+
<div
|
|
344
|
+
className={css`
|
|
345
|
+
position: absolute;
|
|
346
|
+
inset: 0;
|
|
347
|
+
`}
|
|
348
|
+
>
|
|
349
|
+
<NotFound />
|
|
350
|
+
</div>
|
|
351
|
+
) : null}
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {useCallback, useState} from "react";
|
|
2
|
+
import {createContext} from "@fibery/react/src/create-context";
|
|
3
|
+
import type {EmojiItem} from "@fibery/emoji-data";
|
|
4
|
+
|
|
5
|
+
type EmojiPreview = {emoji: EmojiItem; categoryId: string} | null;
|
|
6
|
+
|
|
7
|
+
type Setter = (v: EmojiPreview) => void;
|
|
8
|
+
|
|
9
|
+
const [SetterProvider, useSetterCtx] = createContext<Setter>("EmojiPreviewSetterProvider");
|
|
10
|
+
const [ValueProvider, useValueCtx] = createContext<EmojiPreview>("EmojiPreviewProvider");
|
|
11
|
+
|
|
12
|
+
export const EmojiPreviewProvider: React.FC<React.PropsWithChildren> = ({children}) => {
|
|
13
|
+
const [preview, setPreview] = useState<EmojiPreview>(null);
|
|
14
|
+
|
|
15
|
+
const setter = useCallback<Setter>((v) => {
|
|
16
|
+
setPreview(v);
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<SetterProvider value={setter}>
|
|
21
|
+
<ValueProvider value={preview}>{children}</ValueProvider>
|
|
22
|
+
</SetterProvider>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const useSetEmojiPreview = useSetterCtx;
|
|
27
|
+
export const useEmojiPreview = useValueCtx;
|
|
@@ -7,6 +7,7 @@ import {EmojiSkinProvider} from "./skin-provider";
|
|
|
7
7
|
import {add as addToFrequentlyUsed} from "../utils/frequently";
|
|
8
8
|
import {isApple} from "../utils/emoji-set";
|
|
9
9
|
import {useCallbackRef} from "@fibery/react/src/use-callback-ref";
|
|
10
|
+
import {EmojiPreviewProvider} from "./preview-provider";
|
|
10
11
|
|
|
11
12
|
type EmojiPickerCtx = {
|
|
12
13
|
version: number;
|
|
@@ -76,11 +77,13 @@ export const EmojiPickerRoot: React.FC<
|
|
|
76
77
|
|
|
77
78
|
return (
|
|
78
79
|
<Provider value={emojiPickerCtx}>
|
|
79
|
-
<
|
|
80
|
-
<
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
<EmojiPreviewProvider>
|
|
81
|
+
<EmojiSkinProvider>
|
|
82
|
+
<EmojiPickerSearchProvider>
|
|
83
|
+
<I18NProvider value={mergedI18N}>{children}</I18NProvider>
|
|
84
|
+
</EmojiPickerSearchProvider>
|
|
85
|
+
</EmojiSkinProvider>
|
|
86
|
+
</EmojiPreviewProvider>
|
|
84
87
|
</Provider>
|
|
85
88
|
);
|
|
86
89
|
};
|
|
@@ -10,6 +10,8 @@ import {useEmojiDataStore} from "../stores/emoji-data-store";
|
|
|
10
10
|
import {useEmojiPickerCtx, useEmojiPickerI18N} from "./root";
|
|
11
11
|
import {useFoundEmojis, useSetFoundEmojis} from "./search-provider";
|
|
12
12
|
import {useEmojiSkin} from "./skin-provider";
|
|
13
|
+
import {useEmojiPreview, useSetEmojiPreview} from "./preview-provider";
|
|
14
|
+
import type {EmojiPickerGridHandle} from "./grid";
|
|
13
15
|
|
|
14
16
|
const searchCss = css`
|
|
15
17
|
${textStyles.regular}
|
|
@@ -72,10 +74,19 @@ export type EmojiPickerSearchProps = {
|
|
|
72
74
|
|
|
73
75
|
searchRef?: React.RefObject<HTMLInputElement>;
|
|
74
76
|
|
|
77
|
+
// TODO: it's better to pass it via context
|
|
78
|
+
gridRef?: React.RefObject<EmojiPickerGridHandle>;
|
|
79
|
+
|
|
75
80
|
extraAction?: React.ReactNode;
|
|
76
81
|
};
|
|
77
82
|
|
|
78
|
-
export const EmojiPickerSearch: React.FC<EmojiPickerSearchProps> = ({
|
|
83
|
+
export const EmojiPickerSearch: React.FC<EmojiPickerSearchProps> = ({
|
|
84
|
+
autoFocus,
|
|
85
|
+
className,
|
|
86
|
+
searchRef,
|
|
87
|
+
gridRef,
|
|
88
|
+
extraAction,
|
|
89
|
+
}) => {
|
|
79
90
|
const ref = useRef<HTMLInputElement>(null);
|
|
80
91
|
|
|
81
92
|
const setRefs = useComposedRefs(ref, searchRef);
|
|
@@ -86,11 +97,16 @@ export const EmojiPickerSearch: React.FC<EmojiPickerSearchProps> = ({autoFocus,
|
|
|
86
97
|
const foundEmojis = useFoundEmojis();
|
|
87
98
|
const skin = useEmojiSkin();
|
|
88
99
|
const _setFoundEmojis = useSetFoundEmojis();
|
|
100
|
+
const setEmojiPreview = useSetEmojiPreview();
|
|
101
|
+
const emojiPreview = useEmojiPreview();
|
|
89
102
|
|
|
90
103
|
const setFoundEmojis = (emojis: EmojiItem[] | null) => {
|
|
104
|
+
gridRef?.current?.scrollToTop();
|
|
91
105
|
// improves responsiveness of input.
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
startTransition(() => {
|
|
107
|
+
_setFoundEmojis(emojis);
|
|
108
|
+
setEmojiPreview(emojis?.[0] ? {emoji: emojis[0], categoryId: "search"} : null);
|
|
109
|
+
});
|
|
94
110
|
};
|
|
95
111
|
|
|
96
112
|
const inputId = `emoji-picker-search-${useId()}`;
|
|
@@ -104,10 +120,38 @@ export const EmojiPickerSearch: React.FC<EmojiPickerSearchProps> = ({autoFocus,
|
|
|
104
120
|
};
|
|
105
121
|
|
|
106
122
|
const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
|
|
107
|
-
if (e.key === "Enter" &&
|
|
108
|
-
emojiPicker.onEmojiSelect(foundEmojis[0]);
|
|
109
|
-
|
|
123
|
+
if (e.key === "Enter" && emojiPreview) {
|
|
110
124
|
e.preventDefault();
|
|
125
|
+
emojiPicker.onEmojiSelect(emojiPreview.emoji);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const cursorAtEnd = ref.current && ref.current.selectionStart === ref.current.value.length;
|
|
129
|
+
|
|
130
|
+
if (gridRef?.current && cursorAtEnd) {
|
|
131
|
+
let handled = false;
|
|
132
|
+
if (e.key === "ArrowDown") {
|
|
133
|
+
handled = gridRef.current.navigate("down");
|
|
134
|
+
}
|
|
135
|
+
if (e.key === "ArrowUp" && emojiPreview) {
|
|
136
|
+
handled = gridRef.current.navigate("up");
|
|
137
|
+
if (!handled) {
|
|
138
|
+
setEmojiPreview(null);
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (e.key === "ArrowLeft" && emojiPreview) {
|
|
143
|
+
handled = gridRef.current.navigate("left");
|
|
144
|
+
if (!handled) {
|
|
145
|
+
setEmojiPreview(null);
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (e.key === "ArrowRight") {
|
|
150
|
+
handled = gridRef.current.navigate("right");
|
|
151
|
+
}
|
|
152
|
+
if (handled) {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
}
|
|
111
155
|
}
|
|
112
156
|
};
|
|
113
157
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {EmojiData} from "@fibery/emoji-data";
|
|
2
2
|
import {PubSub, makePubSub} from "@fibery/helpers/utils/pub-sub";
|
|
3
3
|
import {createContext} from "@fibery/react/src/create-context";
|
|
4
|
-
import {
|
|
4
|
+
import {useSyncExternalStore} from "react";
|
|
5
5
|
|
|
6
6
|
export type EmojiDataStore = EmojiData & {pubSub: PubSub};
|
|
7
7
|
|
|
@@ -11,6 +11,10 @@ export const makeEmojiDataStore = (emojiData: EmojiData): EmojiDataStore => {
|
|
|
11
11
|
return {
|
|
12
12
|
...emojiData,
|
|
13
13
|
|
|
14
|
+
setData(...args: Parameters<EmojiData["setData"]>) {
|
|
15
|
+
emojiData.setData(...args);
|
|
16
|
+
pubSub.publish();
|
|
17
|
+
},
|
|
14
18
|
setCustom(...args: Parameters<EmojiData["setCustom"]>) {
|
|
15
19
|
emojiData.setCustom(...args);
|
|
16
20
|
pubSub.publish();
|
|
@@ -30,20 +34,19 @@ export const makeEmojiDataStore = (emojiData: EmojiData): EmojiDataStore => {
|
|
|
30
34
|
|
|
31
35
|
const [DataStoreProvider, useDataStoreCtx] = createContext<EmojiDataStore>("EmojiDataStore");
|
|
32
36
|
|
|
33
|
-
export const EmojiDataStoreProvider
|
|
34
|
-
React.PropsWithChildren<{
|
|
35
|
-
data: EmojiData;
|
|
36
|
-
}>
|
|
37
|
-
> = ({data, children}) => {
|
|
38
|
-
const emojiDataStore = useMemo(() => makeEmojiDataStore(data), [data]);
|
|
39
|
-
|
|
40
|
-
return <DataStoreProvider value={emojiDataStore}>{children}</DataStoreProvider>;
|
|
41
|
-
};
|
|
37
|
+
export const EmojiDataStoreProvider = DataStoreProvider;
|
|
42
38
|
|
|
43
39
|
export const useEmojiDataStore = useDataStoreCtx;
|
|
44
40
|
|
|
41
|
+
// TODO: this https://github.com/facebook/react/blob/main/packages/use-sync-external-store/with-selector.js can be used
|
|
42
|
+
// for more flexible selections (e.g. getManyEmojis(ids: string[]){})
|
|
43
|
+
// but it's not documented by react yet. Even though react-redux uses it in useSelector
|
|
45
44
|
export const useEmojiDataStoreSelector = <Snapshot,>(getter: (emojiDataStore: EmojiDataStore) => Snapshot) => {
|
|
46
45
|
const dataStore = useDataStoreCtx();
|
|
47
46
|
|
|
48
47
|
return useSyncExternalStore(dataStore.pubSub.subscribe, () => getter(dataStore));
|
|
49
48
|
};
|
|
49
|
+
|
|
50
|
+
const getIsInitialized = (store: EmojiDataStore) => store.getIsInitialized();
|
|
51
|
+
|
|
52
|
+
export const useIsInitializedEmojiDataStore = () => useEmojiDataStoreSelector(getIsInitialized);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {RawCustomEmoji, makeEmojiData} from "@fibery/emoji-data";
|
|
2
2
|
import {Suspense, lazy} from "react";
|
|
3
|
-
import {EmojiDataStoreProvider} from "./emoji-data-store";
|
|
3
|
+
import {EmojiDataStoreProvider, makeEmojiDataStore} from "./emoji-data-store";
|
|
4
4
|
import {getImageUrl} from "../../app-icon-with-fallback";
|
|
5
5
|
|
|
6
6
|
const EMPTY_DATA = {
|
|
@@ -24,9 +24,11 @@ const LazyAppIcons = lazy(async () => {
|
|
|
24
24
|
|
|
25
25
|
appIconsEmojiData.setCustom(custom);
|
|
26
26
|
|
|
27
|
+
const appIconsEmojiStore = makeEmojiDataStore(appIconsEmojiData);
|
|
28
|
+
|
|
27
29
|
return {
|
|
28
30
|
default: function EmojiDataStore({children}: React.PropsWithChildren) {
|
|
29
|
-
return <EmojiDataStoreProvider
|
|
31
|
+
return <EmojiDataStoreProvider value={appIconsEmojiStore}>{children}</EmojiDataStoreProvider>;
|
|
30
32
|
},
|
|
31
33
|
};
|
|
32
34
|
});
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import {FC, ReactNode, useMemo} from "react";
|
|
2
1
|
import {makeEmojiData} from "@fibery/emoji-data";
|
|
3
|
-
import {getEmojiSet} from "../utils/emoji-set";
|
|
4
|
-
import {EmojiDataStoreProvider} from "./emoji-data-store";
|
|
5
2
|
import rawData from "@fibery/emoji-data/data/all.json";
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import {FC, ReactNode, useMemo} from "react";
|
|
4
|
+
import {getEmojiSet} from "../utils/emoji-set";
|
|
5
|
+
import {EmojiDataStoreProvider, makeEmojiDataStore} from "./emoji-data-store";
|
|
8
6
|
|
|
9
7
|
interface EmojiDataProviderProps {
|
|
10
8
|
children?: ReactNode;
|
|
11
9
|
}
|
|
12
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated
|
|
13
|
+
* Static means "preloaded", "non-lazy" here
|
|
14
|
+
* Should Not outside of tests
|
|
15
|
+
*/
|
|
13
16
|
export const StaticEmojiDataStore: FC<EmojiDataProviderProps> = ({children}) => {
|
|
14
17
|
const emojiData = useMemo(() => makeEmojiData(rawData, {set: getEmojiSet()}), []);
|
|
15
|
-
|
|
18
|
+
const emojiDataStore = useMemo(() => makeEmojiDataStore(emojiData), [emojiData]);
|
|
19
|
+
|
|
20
|
+
return <EmojiDataStoreProvider value={emojiDataStore}>{children}</EmojiDataStoreProvider>;
|
|
16
21
|
};
|