emoji-picker-react 5.0.0 → 5.0.2
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/data-core/prepare.d.ts +6 -0
- package/dist/esm/chunks/chunk-56D5WCOZ.mjs +6729 -0
- package/dist/esm/chunks/chunk-7ZGTUZFB.mjs +8021 -12
- package/dist/esm/chunks/chunk-IODYR4CR.mjs +14 -0
- package/dist/esm/chunks/chunk-LO4TPPLH.mjs +32 -42
- package/dist/esm/chunks/chunk-RU7G34WK.mjs +156 -0
- 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 +1 -5
- 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 +9 -3
- package/package.json +2 -2
- 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 +7 -2
- package/src/components/header/Search/Search.tsx +4 -2
- package/src/components/header/SkinTonePicker/BtnSkinToneVariation.tsx +7 -6
- package/src/components/main/defaultAppearance.tsx +2 -1
- package/src/config/config.ts +6 -3
- package/src/data/skinToneVariations.ts +8 -7
- package/src/data-core/pickerData.ts +8 -12
- package/src/data-core/prepare.ts +32 -39
- package/src/data-core/search.ts +10 -2
- package/src/dataUtils/emojiSelectors.ts +2 -1
- package/src/dataUtils/parseNativeEmoji.ts +3 -4
- package/src/dataUtils/suggestedEmojis.ts +25 -39
- package/src/hooks/preloadEmoji.ts +13 -52
- package/src/hooks/useActiveCategoryScrollDetection.ts +22 -19
- package/src/hooks/useCategoryHeight.ts +38 -6
- package/src/hooks/useDisallowedEmojis.ts +23 -13
- package/src/hooks/useEmojiVirtualization.tsx +102 -66
- 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
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from '../dataUtils/DataTypes';
|
|
22
22
|
import { Categories, type EmojiData } from '../types/exposedTypes';
|
|
23
23
|
|
|
24
|
-
import { getPreparedCore } from './prepare';
|
|
24
|
+
import { getPreparedCore, withoutFe0f } from './prepare';
|
|
25
25
|
|
|
26
26
|
export interface PickerDataSnapshot {
|
|
27
27
|
emojiData: EmojiData;
|
|
@@ -49,9 +49,7 @@ const NO_CUSTOM: object = {};
|
|
|
49
49
|
|
|
50
50
|
function customToRegularEmoji(emoji: CustomEmoji): DataEmoji {
|
|
51
51
|
return {
|
|
52
|
-
[EmojiProperties.name]: emoji.names
|
|
53
|
-
name.toLowerCase(),
|
|
54
|
-
),
|
|
52
|
+
[EmojiProperties.name]: [...emoji.names],
|
|
55
53
|
[EmojiProperties.unified]: emoji.id.toLowerCase(),
|
|
56
54
|
[EmojiProperties.added_in]: '0',
|
|
57
55
|
[EmojiProperties.imgUrl]: emoji.imgUrl,
|
|
@@ -136,15 +134,13 @@ export function getPickerDataSnapshot(
|
|
|
136
134
|
|
|
137
135
|
const allEmojisByUnified: Record<string, DataEmoji> = Object.create(null);
|
|
138
136
|
|
|
137
|
+
// The base unified and each variation, and either U+FE0F spelling of
|
|
138
|
+
// them; an exact ID always wins over another record's spelling.
|
|
139
139
|
allEmojis.forEach((emoji) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
emoji[Keys.variations]?.forEach((variation) => {
|
|
145
|
-
allEmojisByUnified[variation] = emoji;
|
|
146
|
-
});
|
|
147
|
-
}
|
|
140
|
+
[emoji[Keys.unified], ...(emoji[Keys.variations] || [])].forEach((id) => {
|
|
141
|
+
allEmojisByUnified[id] = emoji;
|
|
142
|
+
allEmojisByUnified[withoutFe0f(id)] ||= emoji;
|
|
143
|
+
});
|
|
148
144
|
});
|
|
149
145
|
|
|
150
146
|
const snapshot: PickerDataSnapshot = {
|
package/src/data-core/prepare.ts
CHANGED
|
@@ -55,6 +55,15 @@ export function normalizeUnified(input: string): string {
|
|
|
55
55
|
return input.trim().toLowerCase();
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Datasets disagree on where U+FE0F (emoji presentation) appears in an ID:
|
|
60
|
+
* `2764` and `2764-fe0f` are the same heart. IDs are also indexed under
|
|
61
|
+
* this form, so either spelling finds the dataset's own record.
|
|
62
|
+
*/
|
|
63
|
+
export function withoutFe0f(unified: string): string {
|
|
64
|
+
return unified.replace(/-fe0f/g, '');
|
|
65
|
+
}
|
|
66
|
+
|
|
58
67
|
export function normalizeQuery(input: string): string {
|
|
59
68
|
if (!input || typeof input !== 'string') {
|
|
60
69
|
return '';
|
|
@@ -62,28 +71,10 @@ export function normalizeQuery(input: string): string {
|
|
|
62
71
|
return input.trim().toLowerCase();
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
function toEmojiInfo(
|
|
66
|
-
unified: string,
|
|
67
|
-
names: readonly string[],
|
|
68
|
-
variations: readonly string[] | undefined,
|
|
69
|
-
addedIn: string,
|
|
70
|
-
): EmojiInfo {
|
|
71
|
-
return Object.freeze({
|
|
72
|
-
unified,
|
|
73
|
-
emoji: parseNativeEmoji(unified),
|
|
74
|
-
name: names[names.length - 1] || '',
|
|
75
|
-
names: Object.freeze([...names]),
|
|
76
|
-
variations: Object.freeze([...(variations ?? [])]),
|
|
77
|
-
addedIn,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
74
|
function readStringArray(raw: unknown): string[] {
|
|
82
|
-
if (!Array.isArray(raw))
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return (raw.filter((n) => typeof n === 'string') as string[]).map((n) =>
|
|
86
|
-
n.toLowerCase(),
|
|
75
|
+
if (!Array.isArray(raw)) return [];
|
|
76
|
+
return (raw.filter((value) => typeof value === 'string') as string[]).map(
|
|
77
|
+
(value) => value.toLowerCase(),
|
|
87
78
|
);
|
|
88
79
|
}
|
|
89
80
|
|
|
@@ -106,33 +97,38 @@ function appendEntry(
|
|
|
106
97
|
const names = readStringArray((entry as { n?: unknown }).n);
|
|
107
98
|
const variations = readStringArray((entry as { v?: unknown }).v);
|
|
108
99
|
|
|
109
|
-
const
|
|
110
|
-
const addedIn =
|
|
111
|
-
typeof rawAddedIn === 'string' ? rawAddedIn : String(rawAddedIn ?? '');
|
|
100
|
+
const addedIn = String((entry as { a?: unknown }).a ?? '');
|
|
112
101
|
|
|
113
|
-
|
|
102
|
+
// Freeze the arrays readStringArray owns directly, without copying them
|
|
103
|
+
// a second time. Caller arrays are never mutated or frozen.
|
|
104
|
+
const info: EmojiInfo = Object.freeze({
|
|
105
|
+
unified,
|
|
106
|
+
emoji: parseNativeEmoji(unified),
|
|
107
|
+
name: names[names.length - 1] || '',
|
|
108
|
+
names: Object.freeze(names),
|
|
109
|
+
variations: Object.freeze(variations),
|
|
110
|
+
addedIn,
|
|
111
|
+
});
|
|
114
112
|
records.push(info);
|
|
115
113
|
byUnified.set(unified, info);
|
|
116
|
-
for (const
|
|
117
|
-
if (!byUnified.has(
|
|
118
|
-
|
|
119
|
-
}
|
|
114
|
+
for (const id of [unified, ...info.variations]) {
|
|
115
|
+
if (!byUnified.has(id)) byUnified.set(id, info);
|
|
116
|
+
if (!byUnified.has(withoutFe0f(id))) byUnified.set(withoutFe0f(id), info);
|
|
120
117
|
}
|
|
121
118
|
indexCharBucket(byCharMutable, names, info);
|
|
122
119
|
}
|
|
123
120
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
121
|
+
// Records are indexed in order. A bucket already ending in this record
|
|
122
|
+
// has seen the character, avoiding a temporary Set for every emoji.
|
|
126
123
|
function indexCharBucket(
|
|
127
124
|
byCharMutable: Map<string, EmojiInfo[]>,
|
|
128
125
|
names: readonly string[],
|
|
129
126
|
info: EmojiInfo,
|
|
130
127
|
): void {
|
|
131
|
-
const
|
|
132
|
-
for (const char of distinctChars) {
|
|
128
|
+
for (const char of names.join('')) {
|
|
133
129
|
const bucket = byCharMutable.get(char);
|
|
134
130
|
if (bucket) {
|
|
135
|
-
bucket.push(info);
|
|
131
|
+
if (bucket[bucket.length - 1] !== info) bucket.push(info);
|
|
136
132
|
} else {
|
|
137
133
|
byCharMutable.set(char, [info]);
|
|
138
134
|
}
|
|
@@ -161,15 +157,12 @@ export function getPreparedCore(emojiData?: EmojiData): PreparedCore {
|
|
|
161
157
|
}
|
|
162
158
|
}
|
|
163
159
|
|
|
164
|
-
|
|
165
|
-
byCharMutable.forEach((bucket, char) => {
|
|
166
|
-
byChar.set(char, Object.freeze(bucket));
|
|
167
|
-
});
|
|
160
|
+
byCharMutable.forEach(Object.freeze);
|
|
168
161
|
|
|
169
162
|
const core: PreparedCore = {
|
|
170
163
|
records,
|
|
171
164
|
byUnified,
|
|
172
|
-
byChar,
|
|
165
|
+
byChar: byCharMutable,
|
|
173
166
|
queryMemo: new Map<string, readonly EmojiInfo[]>(),
|
|
174
167
|
};
|
|
175
168
|
|
package/src/data-core/search.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// Pure v5 data operations shared by the picker and `emoji-picker-react/data`.
|
|
2
2
|
// No React / ShipStyles imports allowed in this module graph.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getPreparedCore,
|
|
6
|
+
normalizeQuery,
|
|
7
|
+
normalizeUnified,
|
|
8
|
+
withoutFe0f,
|
|
9
|
+
} from './prepare';
|
|
5
10
|
import type { EmojiDataOptions, EmojiInfo } from './types';
|
|
6
11
|
|
|
7
12
|
export type { EmojiDataOptions, EmojiInfo };
|
|
@@ -74,7 +79,10 @@ export function getEmojiByUnified(
|
|
|
74
79
|
return undefined;
|
|
75
80
|
}
|
|
76
81
|
const core = getPreparedCore(options?.emojiData);
|
|
77
|
-
return
|
|
82
|
+
return (
|
|
83
|
+
core.byUnified.get(normalized) ??
|
|
84
|
+
core.byUnified.get(withoutFe0f(normalized))
|
|
85
|
+
);
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/**
|
|
@@ -82,7 +82,8 @@ export function emojiVariationUnified(
|
|
|
82
82
|
// only if the registered dataset changes, so importing this module costs
|
|
83
83
|
// nothing and does not pull the dataset into a bundle.
|
|
84
84
|
let indexedSource: EmojiData | null = null;
|
|
85
|
-
let allEmojisByUnified: Record<string, DataEmoji> =
|
|
85
|
+
let allEmojisByUnified: Record<string, DataEmoji> =
|
|
86
|
+
/* @__PURE__ */ Object.create(null);
|
|
86
87
|
|
|
87
88
|
function registryIndex(): Record<string, DataEmoji> {
|
|
88
89
|
const source = getRegisteredDefaultEmojiData();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withoutFe0f } from '../data-core/prepare';
|
|
2
|
+
|
|
1
3
|
import { DataEmoji, EmojiProperties } from './DataTypes';
|
|
2
4
|
|
|
3
5
|
// Caller-defined Suggested contents.
|
|
@@ -47,46 +49,30 @@ export function resolveEntryId(
|
|
|
47
49
|
if (!id) {
|
|
48
50
|
return undefined;
|
|
49
51
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
let unified = id;
|
|
53
|
+
let record = lookup(id);
|
|
54
|
+
// Unified IDs and custom IDs are ASCII; anything else may be the emoji
|
|
55
|
+
// character itself, read as its code points.
|
|
56
|
+
if (!record && /[^\0-\x7f]/.test(id)) {
|
|
57
|
+
unified = Array.from(id)
|
|
58
|
+
.map((char) =>
|
|
59
|
+
(char.codePointAt(0) as number).toString(16).padStart(4, '0'),
|
|
60
|
+
)
|
|
61
|
+
.join('-');
|
|
62
|
+
record = lookup(unified);
|
|
57
63
|
}
|
|
58
|
-
return nativeToUnifiedCandidates(id).find((candidate) =>
|
|
59
|
-
isKnownRenderId(candidate, lookup),
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Dataset IDs are zero-padded code points joined by "-", and carry U+FE0F
|
|
64
|
-
// where the emoji presentation needs it; typed or stored characters often
|
|
65
|
-
// omit or add it. Try the exact sequence, then without FE0F, then with
|
|
66
|
-
// FE0F after the first code point (e.g. "©" -> "00a9-fe0f").
|
|
67
|
-
function nativeToUnifiedCandidates(native: string): string[] {
|
|
68
|
-
const codePoints = Array.from(native).map((char) =>
|
|
69
|
-
(char.codePointAt(0) as number).toString(16).padStart(4, '0'),
|
|
70
|
-
);
|
|
71
|
-
const exact = codePoints.join('-');
|
|
72
|
-
const bare = codePoints.filter((codePoint) => codePoint !== 'fe0f');
|
|
73
|
-
const withPresentation = [bare[0], 'fe0f', ...bare.slice(1)];
|
|
74
|
-
return [exact, bare.join('-'), withPresentation.join('-')];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// A render identity is valid only as the base unified or a listed
|
|
78
|
-
// variation. Anything else — e.g. a nonexistent variation that fell back
|
|
79
|
-
// to the base record — is unknown and ignored.
|
|
80
|
-
function isKnownRenderId(
|
|
81
|
-
id: string,
|
|
82
|
-
lookup: (normalizedId: string) => DataEmoji | undefined,
|
|
83
|
-
): boolean {
|
|
84
|
-
const record = lookup(id);
|
|
85
64
|
if (!record) {
|
|
86
|
-
return
|
|
65
|
+
return undefined;
|
|
87
66
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
67
|
+
// A render identity is the base unified or a listed variation; a
|
|
68
|
+
// nonexistent variation that fell back to the base record is unknown.
|
|
69
|
+
// Typed, stored or other datasets' IDs often omit or add U+FE0F: resolve
|
|
70
|
+
// to the record's own spelling of that identity.
|
|
71
|
+
const known = [
|
|
72
|
+
record[EmojiProperties.unified],
|
|
73
|
+
...(record[EmojiProperties.variations] ?? []),
|
|
74
|
+
];
|
|
75
|
+
return known.includes(unified)
|
|
76
|
+
? unified
|
|
77
|
+
: known.find((each) => withoutFe0f(each) === withoutFe0f(unified));
|
|
92
78
|
}
|
|
@@ -1,75 +1,36 @@
|
|
|
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,
|
|
53
8
|
emoji: undefined | DataEmoji,
|
|
54
9
|
emojiStyle: EmojiStyleValue,
|
|
10
|
+
rendered?: string,
|
|
55
11
|
): void {
|
|
56
12
|
if (!emoji) {
|
|
57
13
|
return;
|
|
58
14
|
}
|
|
59
15
|
|
|
60
16
|
const unified = emojiUnified(emoji);
|
|
17
|
+
// With `rendered`, only the image the cell renders (the active tone's, or
|
|
18
|
+
// the base) is fetched. It is marked apart from a full preload, so a
|
|
19
|
+
// later full preload (on focus) still fetches every tone; a full one
|
|
20
|
+
// covers it.
|
|
21
|
+
const key = rendered ? rendered + '/only' : unified;
|
|
61
22
|
|
|
62
|
-
if (preloadedEmojs.has(unified)) {
|
|
23
|
+
if (preloadedEmojs.has(unified) || preloadedEmojs.has(key)) {
|
|
63
24
|
return;
|
|
64
25
|
}
|
|
65
|
-
preloadedEmojs.add(
|
|
26
|
+
preloadedEmojs.add(key);
|
|
66
27
|
|
|
67
|
-
emojiVariations(emoji)
|
|
68
|
-
|
|
69
|
-
.forEach((variation) => {
|
|
28
|
+
(rendered ? [rendered] : emojiVariations(emoji).concat(unified)).forEach(
|
|
29
|
+
(variation) => {
|
|
70
30
|
const emojiUrl = getEmojiUrl(variation, emojiStyle);
|
|
71
31
|
preloadImage(emojiUrl);
|
|
72
|
-
}
|
|
32
|
+
},
|
|
33
|
+
);
|
|
73
34
|
}
|
|
74
35
|
|
|
75
36
|
export const preloadedEmojs: Set<string> = new Set();
|
|
@@ -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,8 +1,11 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
useActiveSkinToneState,
|
|
3
5
|
useNativeEmojiSupport,
|
|
4
6
|
} from '../components/context/PickerContext';
|
|
5
7
|
import { useEmojiVersionConfig, useUnicodeToHide } from '../config/useConfig';
|
|
8
|
+
import { withoutFe0f } from '../data-core/prepare';
|
|
6
9
|
import { DataEmoji } from '../dataUtils/DataTypes';
|
|
7
10
|
import {
|
|
8
11
|
addedIn,
|
|
@@ -20,17 +23,24 @@ export function useIsEmojiDisallowed() {
|
|
|
20
23
|
const unicodeToHide = useUnicodeToHide();
|
|
21
24
|
const [activeSkinTone] = useActiveSkinToneState();
|
|
22
25
|
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
return useCallback(
|
|
27
|
+
function isEmojiDisallowed(
|
|
28
|
+
emoji: DataEmoji,
|
|
29
|
+
renderedUnified = emojiUnified(emoji, activeSkinTone),
|
|
30
|
+
) {
|
|
31
|
+
if (
|
|
32
|
+
unicodeToHide.has(
|
|
33
|
+
withoutFe0f(unifiedWithoutSkinTone(emojiUnified(emoji))),
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
return true;
|
|
37
|
+
if (isCustomEmoji(emoji)) return false;
|
|
38
|
+
const version = addedIn(emoji);
|
|
39
|
+
return (
|
|
40
|
+
version > cap ||
|
|
41
|
+
!isNativeEmojiSupported(nativeSupport, renderedUnified, version)
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
[activeSkinTone, unicodeToHide, cap, nativeSupport],
|
|
45
|
+
);
|
|
36
46
|
}
|