@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.
@@ -1,99 +1,355 @@
1
1
  import {css, cx} from "@linaria/core";
2
- import {useLayoutEffect, useMemo, useState} from "react";
3
- import {useEmojiDataStore, useEmojiDataStoreSelector} from "../stores/emoji-data-store";
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 {EmojiPickerCategory, EmojiPickerCategoryProps} from "./category";
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
- type AllCategoriesProps = Pick<EmojiPickerCategoryProps, "renderEmoji" | "scrollParent">;
19
- const AllCategories: React.FC<AllCategoriesProps> = ({renderEmoji, scrollParent}) => {
20
- const contentSettings = useEmojiPickerContentSettings();
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
- const emojiData = useEmojiDataStore();
23
- const categories = useEmojiDataStoreSelector((store) => store.getCategories());
43
+ ${textStyles.regular};
44
+ color: ${themeVars.accentTextColor};
45
+ `;
46
+
47
+ const NotFound = () => {
48
+ const i18n = useEmojiPickerI18N();
24
49
 
25
- const recentCategory = useMemo(
26
- () => ({
27
- id: "recent",
28
- name: "Recent",
29
- emojis: getFrequentlyUsed(contentSettings.perLine).filter((id) => {
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
- return (
39
- <>
40
- {recentCategory.emojis.length > 0 ? (
41
- <EmojiPickerCategory
42
- preRender
43
- category={recentCategory}
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
- useLayoutEffect(() => {
85
- if (gridElement) {
86
- gridElement.scrollTop = 0;
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
- return (
91
- <div ref={setGridElement} className={cx(gridWrapperCss, className)}>
92
- {searchCategory ? (
93
- <EmojiPickerCategory preRender scrollParent={gridElement} category={searchCategory} renderEmoji={renderEmoji} />
94
- ) : (
95
- <AllCategories scrollParent={gridElement} renderEmoji={renderEmoji} />
96
- )}
97
- </div>
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
- <EmojiSkinProvider>
80
- <EmojiPickerSearchProvider>
81
- <I18NProvider value={mergedI18N}>{children}</I18NProvider>
82
- </EmojiPickerSearchProvider>
83
- </EmojiSkinProvider>
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> = ({autoFocus, className, searchRef, extraAction}) => {
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
- // TODO: better just implement a proper virtualization
93
- startTransition(() => _setFoundEmojis(emojis));
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" && foundEmojis && foundEmojis.length > 0) {
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 {useMemo, useSyncExternalStore} from "react";
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: React.FC<
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 data={appIconsEmojiData}>{children}</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
- // Static means "preloaded", "non-lazy" here
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
- return <EmojiDataStoreProvider data={emojiData}>{children}</EmojiDataStoreProvider>;
18
+ const emojiDataStore = useMemo(() => makeEmojiDataStore(emojiData), [emojiData]);
19
+
20
+ return <EmojiDataStoreProvider value={emojiDataStore}>{children}</EmojiDataStoreProvider>;
16
21
  };