emoji-picker-react 5.0.0 → 5.0.1
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/dist/DomUtils/classNames.d.ts +0 -1
- package/dist/DomUtils/keyboardNavigation.d.ts +10 -2
- package/dist/DomUtils/selectors.d.ts +2 -2
- package/dist/data/index.js +1 -1
- package/dist/data/skinToneVariations.js +2 -2
- package/dist/data/skinToneVariations.ts +8 -7
- package/dist/esm/chunks/chunk-7ZGTUZFB.mjs +8021 -12
- package/dist/esm/chunks/chunk-ACXAQEKG.mjs +149 -0
- package/dist/esm/chunks/chunk-F2KS7DCA.mjs +6735 -0
- package/dist/esm/chunks/chunk-L5W4PFRJ.mjs +14 -0
- package/dist/esm/chunks/chunk-LO4TPPLH.mjs +32 -42
- package/dist/esm/chunks/emojis-6L3GRBAP.mjs +5 -7
- package/dist/esm/data.mjs +9 -16
- package/dist/esm/index.mjs +230 -340
- package/dist/esm/primitives/index.mjs +6 -65
- package/dist/hooks/preloadEmoji.d.ts +0 -4
- package/dist/hooks/useCategoryHeight.d.ts +2 -1
- package/dist/hooks/useEmojiVirtualization.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/primitives/index.js +1 -1
- package/dist/virtualization/virtualizationHelpers.d.ts +2 -0
- package/llms-full.txt +6 -3
- package/package.json +1 -1
- package/src/DomUtils/classNames.ts +0 -4
- package/src/DomUtils/elementPositionInRow.ts +2 -2
- package/src/DomUtils/keyboardNavigation.ts +74 -10
- package/src/DomUtils/scrollTo.ts +2 -2
- package/src/DomUtils/selectors.ts +10 -16
- package/src/components/body/EmojiCategory.tsx +1 -1
- package/src/components/body/EmojiList.tsx +54 -23
- package/src/components/context/PickerDataContext.tsx +5 -1
- package/src/components/header/Search/Search.tsx +4 -2
- package/src/components/main/defaultAppearance.tsx +2 -1
- package/src/data/skinToneVariations.ts +8 -7
- package/src/data-core/pickerData.ts +1 -3
- package/src/data-core/prepare.ts +20 -35
- package/src/dataUtils/emojiSelectors.ts +2 -1
- package/src/dataUtils/parseNativeEmoji.ts +3 -4
- package/src/hooks/preloadEmoji.ts +1 -46
- package/src/hooks/useActiveCategoryScrollDetection.ts +22 -19
- package/src/hooks/useCategoryHeight.ts +38 -6
- package/src/hooks/useDisallowedEmojis.ts +18 -13
- package/src/hooks/useEmojiVirtualization.tsx +93 -67
- package/src/hooks/useFilter.ts +10 -8
- package/src/hooks/useIsEmojiHidden.ts +16 -12
- package/src/hooks/useKeyboardNavigation.ts +104 -17
- package/src/primitives/structuralStyles.tsx +3 -0
- package/src/primitives/tokens.ts +1 -0
- package/src/virtualization/virtualizationHelpers.ts +9 -2
- package/dist/esm/chunks/chunk-46BUDQLE.mjs +0 -228
- package/dist/esm/chunks/chunk-FQIJ4HBW.mjs +0 -25
- package/dist/esm/chunks/chunk-KQPOIRSC.mjs +0 -9305
|
@@ -1,52 +1,7 @@
|
|
|
1
1
|
import { GetEmojiUrl } from '../components/emoji/BaseEmojiProps';
|
|
2
2
|
import { DataEmoji } from '../dataUtils/DataTypes';
|
|
3
3
|
import { emojiUnified, emojiVariations } from '../dataUtils/emojiUtils';
|
|
4
|
-
import {
|
|
5
|
-
import { Dimensions } from '../virtualization/virtualizationHelpers';
|
|
6
|
-
|
|
7
|
-
// eslint-disable-next-line max-params
|
|
8
|
-
export function preloadEmojiIfNeeded(
|
|
9
|
-
emoji: undefined | DataEmoji,
|
|
10
|
-
emojiStyles: EmojiStyleValue,
|
|
11
|
-
scrollTop: number,
|
|
12
|
-
clientHeight: number,
|
|
13
|
-
topOffset: number,
|
|
14
|
-
style: { top: number } | undefined,
|
|
15
|
-
dimensions: Dimensions,
|
|
16
|
-
getEmojiUrl: GetEmojiUrl,
|
|
17
|
-
): void {
|
|
18
|
-
if (!emoji) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (emojiStyles === EmojiStyle.NATIVE) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const unified = emojiUnified(emoji);
|
|
27
|
-
|
|
28
|
-
if (preloadedEmojs.has(unified)) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// preload only if the emoji is a few rows below the viewport
|
|
33
|
-
if (!style || !dimensions) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
setTimeout(() => {
|
|
38
|
-
const emojiTop = topOffset + style.top;
|
|
39
|
-
const viewportBottom = scrollTop + clientHeight;
|
|
40
|
-
|
|
41
|
-
const isJustBelowViewport =
|
|
42
|
-
emojiTop >= viewportBottom &&
|
|
43
|
-
emojiTop < viewportBottom + dimensions.emojiSize * 2;
|
|
44
|
-
|
|
45
|
-
if (isJustBelowViewport) {
|
|
46
|
-
preloadEmoji(getEmojiUrl, emoji, emojiStyles);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
}
|
|
4
|
+
import { EmojiStyleValue } from '../types/exposedTypes';
|
|
50
5
|
|
|
51
6
|
export function preloadEmoji(
|
|
52
7
|
getEmojiUrl: GetEmojiUrl,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
|
|
3
3
|
import { categoryNameFromDom } from '../DomUtils/categoryNameFromDom';
|
|
4
|
-
import {
|
|
4
|
+
import { ClassNames } from '../DomUtils/classNames';
|
|
5
5
|
import { useBodyRef } from '../components/context/ElementRefContext';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
CategoriesConfig,
|
|
8
|
+
categoryIdFromCategoryConfig,
|
|
9
|
+
} from '../config/categoryConfig';
|
|
7
10
|
|
|
8
11
|
export function useActiveCategoryScrollDetection({
|
|
9
12
|
setActiveCategory,
|
|
@@ -22,9 +25,11 @@ export function useActiveCategoryScrollDetection({
|
|
|
22
25
|
const BodyRef = useBodyRef();
|
|
23
26
|
|
|
24
27
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
29
|
+
setVisibleCategories(categories.map(categoryIdFromCategoryConfig));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const observed = new Map<string, IntersectionObserverEntry>();
|
|
28
33
|
const bodyRef = BodyRef.current;
|
|
29
34
|
const observer = new IntersectionObserver(
|
|
30
35
|
(entries) => {
|
|
@@ -39,31 +44,31 @@ export function useActiveCategoryScrollDetection({
|
|
|
39
44
|
continue;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
intersectingCategories.set(id, entry.isIntersecting);
|
|
44
|
-
targets.set(id, entry.target);
|
|
47
|
+
observed.set(id, entry);
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
const ratios = Array.from(
|
|
50
|
+
const ratios = Array.from(observed);
|
|
48
51
|
const visibleCats = ratios
|
|
49
|
-
.filter(
|
|
52
|
+
.filter(
|
|
53
|
+
([, entry]) => entry.intersectionRatio > 0 || entry.isIntersecting,
|
|
54
|
+
)
|
|
50
55
|
.map(([id]) => id);
|
|
51
56
|
|
|
52
57
|
setVisibleCategories(visibleCats);
|
|
53
58
|
const lastCategory = ratios[ratios.length - 1];
|
|
54
59
|
|
|
55
|
-
if (lastCategory?.[1]
|
|
60
|
+
if (lastCategory?.[1].intersectionRatio === 1) {
|
|
56
61
|
return setActiveCategory(lastCategory[0]);
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
const viewportTop = bodyRef.getBoundingClientRect().top;
|
|
60
|
-
for (const [id,
|
|
65
|
+
for (const [id, entry] of ratios) {
|
|
61
66
|
// Fractional offsets can leave less than a pixel of the preceding
|
|
62
67
|
// section intersecting after a jump. It must not steal the tab
|
|
63
68
|
// highlight from the section aligned with the viewport top.
|
|
64
69
|
if (
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
entry.intersectionRatio &&
|
|
71
|
+
entry.target.getBoundingClientRect().bottom > viewportTop + 1
|
|
67
72
|
) {
|
|
68
73
|
setActiveCategory(id);
|
|
69
74
|
break;
|
|
@@ -75,11 +80,9 @@ export function useActiveCategoryScrollDetection({
|
|
|
75
80
|
threshold: [0, 1],
|
|
76
81
|
},
|
|
77
82
|
);
|
|
78
|
-
bodyRef
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
observer.observe(el);
|
|
82
|
-
});
|
|
83
|
+
bodyRef?.querySelectorAll(`.${ClassNames.category}`).forEach((el) => {
|
|
84
|
+
observer.observe(el);
|
|
85
|
+
});
|
|
83
86
|
|
|
84
87
|
return () => {
|
|
85
88
|
observer.disconnect();
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ClassNames } from '../DomUtils/classNames';
|
|
4
4
|
import {
|
|
5
5
|
elementBorderBoxHeight,
|
|
6
6
|
EmojiButtonSelector,
|
|
7
7
|
} from '../DomUtils/selectors';
|
|
8
8
|
import {
|
|
9
|
+
useBodyRef,
|
|
9
10
|
useEmojiListRef,
|
|
10
11
|
usePickerMainRef,
|
|
11
12
|
} from '../components/context/ElementRefContext';
|
|
@@ -33,7 +34,7 @@ function firstVisibleContentWidth(
|
|
|
33
34
|
fallbackWidth: number,
|
|
34
35
|
): number {
|
|
35
36
|
const contentBoxes = listEl.querySelectorAll(
|
|
36
|
-
|
|
37
|
+
`.${ClassNames.categoryContent}`,
|
|
37
38
|
);
|
|
38
39
|
for (const box of Array.from(contentBoxes)) {
|
|
39
40
|
const width = (box as HTMLElement).clientWidth;
|
|
@@ -60,15 +61,20 @@ function trackColumnCount(
|
|
|
60
61
|
previousRef.current = emojisPerRow;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
export function useCategoryHeight(
|
|
64
|
+
export function useCategoryHeight(
|
|
65
|
+
emojiCount: number,
|
|
66
|
+
ready = true,
|
|
67
|
+
):
|
|
64
68
|
| {
|
|
65
69
|
categoryHeight: number;
|
|
66
70
|
emojisPerRow: number;
|
|
67
71
|
emojiSize: number;
|
|
68
72
|
rowWidth: number;
|
|
73
|
+
viewportHeight?: number;
|
|
69
74
|
}
|
|
70
75
|
| undefined {
|
|
71
76
|
const EmojiListRef = useEmojiListRef();
|
|
77
|
+
const BodyRef = useBodyRef();
|
|
72
78
|
const [isReactionsMode] = useReactionsModeState();
|
|
73
79
|
const PickerMainRef = usePickerMainRef();
|
|
74
80
|
const emojiSizeRef = React.useRef<number | undefined>(undefined);
|
|
@@ -82,6 +88,7 @@ export function useCategoryHeight(emojiCount: number):
|
|
|
82
88
|
emojisPerRow: number;
|
|
83
89
|
emojiSize: number;
|
|
84
90
|
rowWidth: number;
|
|
91
|
+
viewportHeight?: number;
|
|
85
92
|
}>();
|
|
86
93
|
|
|
87
94
|
// Helper to compute and store dimensions based on current DOM
|
|
@@ -99,8 +106,8 @@ export function useCategoryHeight(emojiCount: number):
|
|
|
99
106
|
emojiSizeRef.current = measured;
|
|
100
107
|
}
|
|
101
108
|
const emojiSize =
|
|
102
|
-
emojiSizeFromContext ||
|
|
103
109
|
measured ||
|
|
110
|
+
emojiSizeFromContext ||
|
|
104
111
|
emojiSizeRef.current ||
|
|
105
112
|
EMOJI_SIZE_DEFAULT;
|
|
106
113
|
const pickerWidth = listEl.clientWidth;
|
|
@@ -113,13 +120,36 @@ export function useCategoryHeight(emojiCount: number):
|
|
|
113
120
|
const rowCount = Math.ceil(emojiCount / emojisPerRow);
|
|
114
121
|
const categoryHeight = rowCount * emojiSize;
|
|
115
122
|
|
|
123
|
+
const next = {
|
|
124
|
+
categoryHeight,
|
|
125
|
+
emojisPerRow,
|
|
126
|
+
emojiSize,
|
|
127
|
+
rowWidth,
|
|
128
|
+
// Virtualization reads the viewport height during render, so a picker
|
|
129
|
+
// that mounts collapsed (reactions first) must re-render once it opens.
|
|
130
|
+
viewportHeight: BodyRef.current?.clientHeight,
|
|
131
|
+
};
|
|
132
|
+
|
|
116
133
|
trackColumnCount(emojisPerRowRef, emojisPerRow, registry);
|
|
117
|
-
|
|
118
|
-
|
|
134
|
+
// Returning `previous` when nothing changed is what stops the layout
|
|
135
|
+
// effect below, which depends on `dimensions`, from looping forever.
|
|
136
|
+
// Both objects come from the literal above, so their keys share an order.
|
|
137
|
+
setDimensions((previous) =>
|
|
138
|
+
JSON.stringify(previous) === JSON.stringify(next) ? previous : next,
|
|
139
|
+
);
|
|
140
|
+
}, [
|
|
141
|
+
EmojiListRef,
|
|
142
|
+
BodyRef,
|
|
143
|
+
emojiCount,
|
|
144
|
+
emojiSizeFromContext,
|
|
145
|
+
registry,
|
|
146
|
+
columns,
|
|
147
|
+
]);
|
|
119
148
|
|
|
120
149
|
// Restore measured row heights in the same commit as the filtered
|
|
121
150
|
// contents. A passive effect leaves one frame of stale offsets that
|
|
122
151
|
// category navigation can observe immediately after clearing search.
|
|
152
|
+
// Recheck an estimated size when positioned/visible cells first mount.
|
|
123
153
|
useIsomorphicLayoutEffect(() => {
|
|
124
154
|
computeAndSetDimensions();
|
|
125
155
|
}, [
|
|
@@ -127,6 +157,8 @@ export function useCategoryHeight(emojiCount: number):
|
|
|
127
157
|
isReactionsMode,
|
|
128
158
|
computeAndSetDimensions,
|
|
129
159
|
visibleCategories.length,
|
|
160
|
+
dimensions,
|
|
161
|
+
ready,
|
|
130
162
|
]);
|
|
131
163
|
|
|
132
164
|
// Fluid layouts (width: 100%, sheets, docks) resize without a CSS
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
useActiveSkinToneState,
|
|
3
5
|
useNativeEmojiSupport,
|
|
@@ -20,17 +22,20 @@ export function useIsEmojiDisallowed() {
|
|
|
20
22
|
const unicodeToHide = useUnicodeToHide();
|
|
21
23
|
const [activeSkinTone] = useActiveSkinToneState();
|
|
22
24
|
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
return useCallback(
|
|
26
|
+
function isEmojiDisallowed(
|
|
27
|
+
emoji: DataEmoji,
|
|
28
|
+
renderedUnified = emojiUnified(emoji, activeSkinTone),
|
|
29
|
+
) {
|
|
30
|
+
if (unicodeToHide.has(unifiedWithoutSkinTone(emojiUnified(emoji))))
|
|
31
|
+
return true;
|
|
32
|
+
if (isCustomEmoji(emoji)) return false;
|
|
33
|
+
const version = addedIn(emoji);
|
|
34
|
+
return (
|
|
35
|
+
version > cap ||
|
|
36
|
+
!isNativeEmojiSupported(nativeSupport, renderedUnified, version)
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
[activeSkinTone, unicodeToHide, cap, nativeSupport],
|
|
40
|
+
);
|
|
36
41
|
}
|
|
@@ -13,17 +13,20 @@ import {
|
|
|
13
13
|
useLazyLoadEmojisConfig,
|
|
14
14
|
useSkinTonesDisabledConfig,
|
|
15
15
|
} from '../config/useConfig';
|
|
16
|
-
import { DataEmojis } from '../dataUtils/DataTypes';
|
|
16
|
+
import { DataEmoji, DataEmojis } from '../dataUtils/DataTypes';
|
|
17
17
|
import { emojiUnified } from '../dataUtils/emojiUtils';
|
|
18
|
+
import { EmojiStyle } from '../types/exposedTypes';
|
|
18
19
|
import {
|
|
19
20
|
getEmojiPositionStyle,
|
|
20
21
|
shouldVirtualize,
|
|
22
|
+
isJustBelowViewport,
|
|
21
23
|
} from '../virtualization/virtualizationHelpers';
|
|
22
24
|
|
|
23
|
-
import {
|
|
25
|
+
import { preloadEmoji } from './preloadEmoji';
|
|
24
26
|
import { useCategoryHeight } from './useCategoryHeight';
|
|
25
27
|
import { useIsEmojiDisallowed } from './useDisallowedEmojis';
|
|
26
28
|
import { useIsEmojiHidden } from './useIsEmojiHidden';
|
|
29
|
+
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
|
|
27
30
|
|
|
28
31
|
export function useEmojiVirtualization({
|
|
29
32
|
categoryEmojis,
|
|
@@ -53,31 +56,42 @@ export function useEmojiVirtualization({
|
|
|
53
56
|
// Native glyphs checked exactly: the cells this pass renders (visible
|
|
54
57
|
// rows plus virtualization's margin rows), nothing else.
|
|
55
58
|
const probe: string[] = [];
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
const preloads: DataEmoji[] = [];
|
|
60
|
+
const [committed, setCommitted] = React.useState(false);
|
|
58
61
|
|
|
59
62
|
// A section shows each rendered emoji once. Recents can hold the same
|
|
60
63
|
// emoji under two tones (e.g. neutral and medium); the active tone
|
|
61
64
|
// renders both as one identity, which must not be listed or keyed twice.
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const emojisToPush = React.useMemo(() => {
|
|
66
|
+
const rendered = new Set<string>();
|
|
67
|
+
return categoryEmojis.filter((emoji) => {
|
|
68
|
+
const isDisallowed = isEmojiDisallowed(emoji);
|
|
69
|
+
const { hidden } = isEmojiHidden(emoji);
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
if (hidden || isDisallowed) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const unified = emojiUnified(emoji, activeSkinTone);
|
|
75
|
+
if (rendered.has(unified)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
rendered.add(unified);
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
}, [categoryEmojis, activeSkinTone, isEmojiHidden, isEmojiDisallowed]);
|
|
77
82
|
|
|
78
|
-
const dimensions = useCategoryHeight(
|
|
83
|
+
const dimensions = useCategoryHeight(
|
|
84
|
+
emojisToPush.length,
|
|
85
|
+
isCategoryVisible && positioned,
|
|
86
|
+
);
|
|
79
87
|
|
|
88
|
+
// Guarantee a render even when a hidden picker has no measurement cell.
|
|
89
|
+
// A passive update avoids flushing all observer effects before first paint.
|
|
80
90
|
useEffect(() => {
|
|
91
|
+
if (!dimensions) setCommitted(true);
|
|
92
|
+
}, [dimensions]);
|
|
93
|
+
|
|
94
|
+
useIsomorphicLayoutEffect(() => {
|
|
81
95
|
if (dimensions) {
|
|
82
96
|
onHeightReady(dimensions.categoryHeight);
|
|
83
97
|
}
|
|
@@ -94,61 +108,73 @@ export function useEmojiVirtualization({
|
|
|
94
108
|
dimensions,
|
|
95
109
|
});
|
|
96
110
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
scrollTop,
|
|
107
|
-
BodyRef.current?.clientHeight ?? 0,
|
|
108
|
-
topOffset,
|
|
109
|
-
style,
|
|
110
|
-
dimensions,
|
|
111
|
-
getEmojiUrl,
|
|
112
|
-
);
|
|
113
|
-
return accumulator;
|
|
114
|
-
}
|
|
111
|
+
const needsPreload = (style: { top: number } | undefined) =>
|
|
112
|
+
emojiStyle !== EmojiStyle.NATIVE &&
|
|
113
|
+
dimensions &&
|
|
114
|
+
style &&
|
|
115
|
+
isJustBelowViewport(
|
|
116
|
+
topOffset + style.top,
|
|
117
|
+
scrollTop + (BodyRef.current?.clientHeight ?? 0),
|
|
118
|
+
dimensions.emojiSize,
|
|
119
|
+
);
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
121
|
+
const waitingForLayout =
|
|
122
|
+
(!committed && !dimensions) ||
|
|
123
|
+
((!dimensions || !positioned) && BodyRef.current?.clientWidth);
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
125
|
+
// MeasureEmoji supplies geometry before cells are built. Layout-free
|
|
126
|
+
// environments can render after the first commit without measurements.
|
|
127
|
+
const emojis = waitingForLayout
|
|
128
|
+
? []
|
|
129
|
+
: emojisToPush.reduce((accumulator, emoji, index) => {
|
|
130
|
+
const style = getEmojiPositionStyle(dimensions, index);
|
|
131
|
+
|
|
132
|
+
if (isVirtualized(style)) {
|
|
133
|
+
if (needsPreload(style)) preloads.push(emoji);
|
|
134
|
+
return accumulator;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!isCategoryVisible) {
|
|
138
|
+
return accumulator;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const unified = emojiUnified(emoji, activeSkinTone);
|
|
142
|
+
// Wait for layout: an unmeasured pass renders a whole category, and
|
|
143
|
+
// while sections above are unmeasured this one renders off-screen
|
|
144
|
+
// cells as if it were at the top.
|
|
145
|
+
if (dimensions && positioned) probe.push(unified);
|
|
146
|
+
accumulator.push(
|
|
147
|
+
<ClickableEmoji
|
|
148
|
+
showVariations={showVariations}
|
|
149
|
+
key={unified}
|
|
150
|
+
emoji={emoji}
|
|
151
|
+
unified={unified}
|
|
152
|
+
emojiStyle={emojiStyle}
|
|
153
|
+
lazyLoad={lazyLoadEmojis}
|
|
154
|
+
getEmojiUrl={getEmojiUrl}
|
|
155
|
+
style={{
|
|
156
|
+
...style,
|
|
157
|
+
position: 'absolute',
|
|
158
|
+
}}
|
|
159
|
+
index={index}
|
|
160
|
+
// Grid semantics (issue #508): each emoji is a cell of its
|
|
161
|
+
// category row; native button activation is kept.
|
|
162
|
+
role="gridcell"
|
|
163
|
+
/>,
|
|
164
|
+
);
|
|
165
|
+
return accumulator;
|
|
166
|
+
}, [] as ReactNode[]);
|
|
146
167
|
|
|
147
168
|
// Every commit: requests for already-checked glyphs are free.
|
|
148
169
|
useEffect(() => requestNativeProbe(probe));
|
|
170
|
+
// Image requests belong to committed work, never an abandoned render.
|
|
171
|
+
// The proximity and cache checks run synchronously, without per-cell timers.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
preloads.forEach((emoji) => preloadEmoji(getEmojiUrl, emoji, emojiStyle));
|
|
174
|
+
});
|
|
149
175
|
|
|
150
176
|
return {
|
|
151
|
-
virtualizedCounter,
|
|
177
|
+
virtualizedCounter: emojisToPush.length - emojis.length,
|
|
152
178
|
emojis,
|
|
153
179
|
dimensions,
|
|
154
180
|
emojiCount: emojisToPush.length,
|
package/src/hooks/useFilter.ts
CHANGED
|
@@ -100,13 +100,11 @@ function useApplySearch() {
|
|
|
100
100
|
if (!mountedRef.current) {
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
|
-
setSearchTerm(searchTerm
|
|
104
|
-
()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
);
|
|
103
|
+
setSearchTerm(searchTerm).then(() => {
|
|
104
|
+
if (registry.isCurrent(token)) {
|
|
105
|
+
scrollTo(PickerMainRef.current, 0, () => registry.isCurrent(token));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
110
108
|
});
|
|
111
109
|
};
|
|
112
110
|
}
|
|
@@ -115,7 +113,11 @@ export function useIsEmojiFiltered(): (unified: string) => boolean {
|
|
|
115
113
|
const { current: filter } = useFilterRef();
|
|
116
114
|
const [searchTerm] = useSearchTermState();
|
|
117
115
|
|
|
118
|
-
return (
|
|
116
|
+
return React.useCallback(
|
|
117
|
+
(unified: string) =>
|
|
118
|
+
isEmojiFilteredBySearchTerm(unified, filter, searchTerm),
|
|
119
|
+
[filter, searchTerm],
|
|
120
|
+
);
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
export function isEmojiFilteredBySearchTerm(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
useActiveSkinToneState,
|
|
3
5
|
useEmojisThatFailedToLoadState,
|
|
@@ -9,21 +11,23 @@ import { useIsEmojiFiltered } from './useFilter';
|
|
|
9
11
|
|
|
10
12
|
export function useIsEmojiHidden(): (emoji: DataEmoji) => IsHiddenReturn {
|
|
11
13
|
const [activeSkinTone] = useActiveSkinToneState();
|
|
12
|
-
|
|
14
|
+
// The tuple changes when its Root-local Set is mutated by an image error.
|
|
15
|
+
const failures = useEmojisThatFailedToLoadState();
|
|
13
16
|
const isEmojiFiltered = useIsEmojiFiltered();
|
|
14
17
|
|
|
15
|
-
return (
|
|
16
|
-
|
|
17
|
-
emojiUnified(emoji, activeSkinTone)
|
|
18
|
-
|
|
19
|
-
const filteredOut = isEmojiFiltered(emojiCanonicalUnified(emoji));
|
|
18
|
+
return useCallback(
|
|
19
|
+
(emoji: DataEmoji): IsHiddenReturn => {
|
|
20
|
+
const failedToLoad = failures[0].has(emojiUnified(emoji, activeSkinTone));
|
|
21
|
+
const filteredOut = isEmojiFiltered(emojiCanonicalUnified(emoji));
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
return {
|
|
24
|
+
failedToLoad,
|
|
25
|
+
filteredOut,
|
|
26
|
+
hidden: failedToLoad || filteredOut,
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
[activeSkinTone, failures, isEmojiFiltered],
|
|
30
|
+
);
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
type IsHiddenReturn = {
|