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,3 +1,5 @@
|
|
|
1
|
+
export declare const OVERSCAN_ROWS = 2;
|
|
2
|
+
export declare function isJustBelowViewport(top: number, bottom: number, size: number): boolean;
|
|
1
3
|
export declare function shouldVirtualize({ scrollTop, clientHeight, topOffset, style, dimensions, }: {
|
|
2
4
|
scrollTop: number;
|
|
3
5
|
clientHeight: number;
|
package/llms-full.txt
CHANGED
|
@@ -227,7 +227,7 @@ Per-library snippets (plain CSS, CSS Modules, Tailwind, shadcn/ui, Emotion, styl
|
|
|
227
227
|
|
|
228
228
|
Locale datasets translate emoji names and category names. `labels` translates controls and announcements; `previewConfig.defaultCaption` sets the preview caption. Complete localization covers every visible and announced string in your composition: see [INTERNATIONALIZATION.md](../../INTERNATIONALIZATION.md).
|
|
229
229
|
|
|
230
|
-
Keep the managed ARIA props. Test keyboard selection, search and IME input, focus restoration and nested Escape inside the real host popover or dialog: Escape closes an open variation or tone menu before reaching the host. Custom emojis with duplicate names share an accessible name; scope test queries to a category.
|
|
230
|
+
Keep the managed ARIA props. Test keyboard selection, search and IME input, focus restoration and nested Escape inside the real host popover or dialog: Escape closes an open variation or tone menu, or clears a non-empty search, before reaching the host. Custom emojis with duplicate names share an accessible name; scope test queries to a category.
|
|
231
231
|
|
|
232
232
|
<!-- source: docs/v5/PROMPTS.md -->
|
|
233
233
|
|
|
@@ -1406,7 +1406,7 @@ The registry item depends on `emoji-picker-react@^5.0.0`. The registry component
|
|
|
1406
1406
|
</Popover>
|
|
1407
1407
|
```
|
|
1408
1408
|
|
|
1409
|
-
The host owns insertion and dismissal. The Popover restores trigger focus. Escape first closes an open variation/tone menu, then a subsequent Escape reaches the host. The light/dark Storybook examples import the registry component itself; browser checks exercise search, keyboard insertion, dismissal, focus restoration and WCAG-tagged axe rules.
|
|
1409
|
+
The host owns insertion and dismissal. The Popover restores trigger focus. Escape first closes an open variation/tone menu or clears a non-empty search, then a subsequent Escape reaches the host. The light/dark Storybook examples import the registry component itself; browser checks exercise search, keyboard insertion, dismissal, focus restoration and WCAG-tagged axe rules.
|
|
1410
1410
|
|
|
1411
1411
|
## Working starter sources
|
|
1412
1412
|
|
|
@@ -1636,7 +1636,7 @@ For standard emoji IDs:
|
|
|
1636
1636
|
- a valid variation remains that exact variation for rendering rather than collapsing to the neutral base emoji;
|
|
1637
1637
|
- duplicates are removed by canonical normalized ID, first occurrence wins.
|
|
1638
1638
|
|
|
1639
|
-
Custom emoji IDs use the same case-insensitive rule
|
|
1639
|
+
Custom emoji IDs use the same case-insensitive rule: IDs are lowercased when indexed. Custom display names retain the supplied capitalization; search still matches them case-insensitively.
|
|
1640
1640
|
|
|
1641
1641
|
Entries may be unified IDs, custom emoji IDs, or the emoji characters themselves (`'🧠'`, `'❤️'`), so recents stored as inserted text can be passed through unchanged; unknown entries are ignored.
|
|
1642
1642
|
|
|
@@ -1891,6 +1891,8 @@ Styling is opt-in in both directions:
|
|
|
1891
1891
|
|
|
1892
1892
|
`columns` sets the emojis per row on EmojiPicker and Root. The picker's width then fits those columns plus a stable scrollbar gutter, unless the consumer sets a width (`width`, `style` or any class). A container narrower than the columns renders fewer columns instead of clipping. Without `columns`, the width decides the column count, as in v4.
|
|
1893
1893
|
|
|
1894
|
+
The intrinsic grid width remains present during an empty search, data loading or a load error.
|
|
1895
|
+
|
|
1894
1896
|
## 12. Data API
|
|
1895
1897
|
|
|
1896
1898
|
The data entry point is defined in [DATA_API.md](./DATA_API.md):
|
|
@@ -2362,6 +2364,7 @@ The following variables are already documented public customization surface and
|
|
|
2362
2364
|
- `--epr-preview-text-color`
|
|
2363
2365
|
|
|
2364
2366
|
### Native emoji font
|
|
2367
|
+
- `--epr-font-family` — font for picker text (`sans-serif` by default). Set it to `inherit` on the picker to use the surrounding application's font. Native emoji glyphs keep their separate font stack.
|
|
2365
2368
|
- `--epr-emoji-font-family` (v5) — font stack for native emojis, e.g. a country-flag polyfill font; native support detection measures this same font.
|
|
2366
2369
|
|
|
2367
2370
|
### Skin tone
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_LABEL_HEIGHT } from '../components/main/labelHeight';
|
|
2
2
|
|
|
3
|
-
import { ClassNames
|
|
3
|
+
import { ClassNames } from './classNames';
|
|
4
4
|
import { NullableElement } from './selectors';
|
|
5
5
|
|
|
6
6
|
export function elementCountInRow(
|
|
@@ -74,7 +74,7 @@ export function getLabelHeight(parentNode: NullableElement) {
|
|
|
74
74
|
return DEFAULT_LABEL_HEIGHT;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const label = parentNode.querySelector(
|
|
77
|
+
const label = parentNode.querySelector(`.${ClassNames.label}`);
|
|
78
78
|
|
|
79
79
|
if (label) {
|
|
80
80
|
const height = label.getBoundingClientRect().height;
|
|
@@ -38,12 +38,45 @@ function categoryGeometry(category: HTMLElement) {
|
|
|
38
38
|
return { content, count, columns };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export type MoveOptions = {
|
|
42
|
+
shouldFocus?: () => boolean;
|
|
43
|
+
/** Called when an upward move leaves the grid. */
|
|
44
|
+
exitUp?: () => void;
|
|
45
|
+
/** Receives the destination cell; focuses it by default. */
|
|
46
|
+
moveTo?: (emoji: HTMLElement) => void;
|
|
47
|
+
/** Source cell while a virtualized destination is being materialized. */
|
|
48
|
+
origin?: HTMLElement;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Move to the first filtered result, including a cell outside the viewport. */
|
|
52
|
+
export function focusFirstEmoji(
|
|
53
|
+
body: NullableElement,
|
|
54
|
+
options: MoveOptions,
|
|
55
|
+
): boolean {
|
|
56
|
+
const category = body?.querySelector<HTMLElement>(
|
|
57
|
+
'[data-epr-part="category-content"]:not([data-epr-emoji-count="0"])',
|
|
58
|
+
)?.parentElement;
|
|
59
|
+
if (category) focusLogicalEmoji(category, 0, options);
|
|
60
|
+
return !!category;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pendingMoves = new WeakMap<
|
|
64
|
+
HTMLElement,
|
|
65
|
+
{
|
|
66
|
+
category: HTMLElement;
|
|
67
|
+
index: number;
|
|
68
|
+
origin?: HTMLElement;
|
|
69
|
+
valid: () => boolean;
|
|
70
|
+
}
|
|
71
|
+
>();
|
|
72
|
+
|
|
41
73
|
export function focusAdjacentEmoji(
|
|
42
74
|
element: NullableElement,
|
|
43
75
|
direction: number,
|
|
44
76
|
row: boolean,
|
|
45
|
-
|
|
77
|
+
options: MoveOptions,
|
|
46
78
|
) {
|
|
79
|
+
const { shouldFocus, exitUp } = options;
|
|
47
80
|
if (!element || (shouldFocus && !shouldFocus())) return;
|
|
48
81
|
const category = closestCategory(element);
|
|
49
82
|
if (!category || element.dataset.eprIndex === undefined) {
|
|
@@ -55,15 +88,34 @@ export function focusAdjacentEmoji(
|
|
|
55
88
|
);
|
|
56
89
|
return;
|
|
57
90
|
}
|
|
58
|
-
const
|
|
91
|
+
const cursor = gridCursor(category, element);
|
|
92
|
+
const destination = logicalDestination(
|
|
93
|
+
cursor.category,
|
|
94
|
+
cursor.index,
|
|
95
|
+
direction,
|
|
96
|
+
row,
|
|
97
|
+
);
|
|
59
98
|
if (destination)
|
|
60
|
-
focusLogicalEmoji(destination.category, destination.index,
|
|
99
|
+
focusLogicalEmoji(destination.category, destination.index, {
|
|
100
|
+
...options,
|
|
101
|
+
origin: element,
|
|
102
|
+
});
|
|
61
103
|
else if (direction < 0) exitUp?.();
|
|
62
104
|
}
|
|
63
105
|
|
|
106
|
+
// A key pressed before the previous move mounted its destination continues
|
|
107
|
+
// from that destination, not from the still-focused source cell.
|
|
108
|
+
function gridCursor(category: HTMLElement, element: HTMLElement) {
|
|
109
|
+
const body = closestScrollBody(category);
|
|
110
|
+
const pending = body && pendingMoves.get(body);
|
|
111
|
+
return pending?.origin === element && pending.valid()
|
|
112
|
+
? pending
|
|
113
|
+
: { category, index: Number(element.dataset.eprIndex) };
|
|
114
|
+
}
|
|
115
|
+
|
|
64
116
|
function logicalDestination(
|
|
65
117
|
category: HTMLElement,
|
|
66
|
-
|
|
118
|
+
currentIndex: number,
|
|
67
119
|
direction: number,
|
|
68
120
|
row: boolean,
|
|
69
121
|
) {
|
|
@@ -71,7 +123,6 @@ function logicalDestination(
|
|
|
71
123
|
const columns = geometry.columns;
|
|
72
124
|
let count = geometry.count;
|
|
73
125
|
if (count <= 0) return null;
|
|
74
|
-
const currentIndex = Number(element.dataset.eprIndex);
|
|
75
126
|
const column = row ? currentIndex % columns : 0;
|
|
76
127
|
let index = row
|
|
77
128
|
? (Math.floor(currentIndex / columns) + direction) * columns
|
|
@@ -105,23 +156,30 @@ function adjacentDestination(
|
|
|
105
156
|
function focusLogicalEmoji(
|
|
106
157
|
category: HTMLElement,
|
|
107
158
|
index: number,
|
|
108
|
-
shouldFocus
|
|
159
|
+
{ shouldFocus, moveTo = (emoji) => emoji.focus(), origin }: MoveOptions,
|
|
109
160
|
) {
|
|
110
161
|
const { content, columns, count } = categoryGeometry(category);
|
|
111
162
|
const body = closestScrollBody(category);
|
|
112
163
|
if (!body || !content || !columns) return;
|
|
113
164
|
const size = content.clientHeight / Math.ceil(count / columns);
|
|
114
165
|
let frames = 60;
|
|
115
|
-
const
|
|
116
|
-
|
|
166
|
+
const pending = { category, index, origin, valid: () => valid() };
|
|
167
|
+
pendingMoves.set(body, pending);
|
|
168
|
+
const valid = (): boolean =>
|
|
169
|
+
pendingMoves.get(body) === pending &&
|
|
170
|
+
(!shouldFocus || shouldFocus()) &&
|
|
171
|
+
body.contains(category);
|
|
117
172
|
const attempt = () => {
|
|
118
173
|
if (!valid()) return;
|
|
119
174
|
const emoji = category.querySelector<HTMLElement>(
|
|
120
175
|
`${VisibleEmojiSelector}[data-epr-index="${index}"]`,
|
|
121
176
|
);
|
|
122
177
|
if (emoji) {
|
|
123
|
-
emoji
|
|
178
|
+
moveTo(emoji);
|
|
124
179
|
scrollEmojiAboveLabel(emoji, valid);
|
|
180
|
+
// Keep the scroll guard valid until a later navigation replaces it,
|
|
181
|
+
// but only unfinished moves supply a logical cursor to another key.
|
|
182
|
+
pending.origin = undefined;
|
|
125
183
|
return;
|
|
126
184
|
}
|
|
127
185
|
if (!size || --frames <= 0) return;
|
|
@@ -133,5 +191,11 @@ function focusLogicalEmoji(
|
|
|
133
191
|
categoryLabelHeight(category);
|
|
134
192
|
requestAnimationFrame(attempt);
|
|
135
193
|
};
|
|
136
|
-
|
|
194
|
+
// Mounted cells can move immediately, so a second key in this frame
|
|
195
|
+
// reads the new position. Only unmounted destinations need a frame.
|
|
196
|
+
if (
|
|
197
|
+
category.querySelector(`${VisibleEmojiSelector}[data-epr-index="${index}"]`)
|
|
198
|
+
)
|
|
199
|
+
attempt();
|
|
200
|
+
else requestAnimationFrame(attempt);
|
|
137
201
|
}
|
package/src/DomUtils/scrollTo.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { useBodyRef } from '../components/context/ElementRefContext';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { ClassNames } from './classNames';
|
|
6
6
|
import {
|
|
7
7
|
categoryLabelHeight,
|
|
8
8
|
closestCategory,
|
|
@@ -53,7 +53,7 @@ export function scrollEmojiAboveLabel(
|
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
if (emoji.closest(
|
|
56
|
+
if (emoji.closest(`.${ClassNames.variationPicker}`)) {
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { unifiedWithoutSkinTone } from '../dataUtils/emojiSelectors';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ClassNames } from './classNames';
|
|
4
4
|
import { firstVisibleElementInContainer } from './elementPositionInRow';
|
|
5
5
|
|
|
6
6
|
export type NullableElement = HTMLElement | null;
|
|
7
7
|
|
|
8
|
-
export const EmojiButtonSelector = `button
|
|
9
|
-
export const VisibleEmojiSelector =
|
|
10
|
-
EmojiButtonSelector,
|
|
11
|
-
asSelectors(ClassNames.visible),
|
|
12
|
-
`:not(${asSelectors(ClassNames.hidden)})`,
|
|
13
|
-
].join('');
|
|
8
|
+
export const EmojiButtonSelector = `button.${ClassNames.emoji}`;
|
|
9
|
+
export const VisibleEmojiSelector = `${EmojiButtonSelector}.${ClassNames.visible}:not(.${ClassNames.hidden})`;
|
|
14
10
|
|
|
15
11
|
export function buttonFromTarget(
|
|
16
12
|
emojiElement: NullableElement,
|
|
@@ -35,7 +31,7 @@ export function isEmojiElement(element: NullableElement): boolean {
|
|
|
35
31
|
export function categoryLabelFromCategory(
|
|
36
32
|
category: NullableElement,
|
|
37
33
|
): NullableElement {
|
|
38
|
-
return category?.querySelector(
|
|
34
|
+
return category?.querySelector(`.${ClassNames.label}`) ?? null;
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
export function closestCategoryLabel(
|
|
@@ -74,7 +70,7 @@ export function categoryLabelHeight(category: NullableElement): number {
|
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
const categoryWithoutLabel = category.querySelector(
|
|
77
|
-
|
|
73
|
+
`.${ClassNames.categoryContent}`,
|
|
78
74
|
);
|
|
79
75
|
|
|
80
76
|
return (
|
|
@@ -96,9 +92,9 @@ export function isEmojiBehindLabel(emoji: NullableElement): boolean {
|
|
|
96
92
|
export function queryScrollBody(root: NullableElement): NullableElement {
|
|
97
93
|
if (!root) return null;
|
|
98
94
|
|
|
99
|
-
return root.matches(
|
|
95
|
+
return root.matches(`.${ClassNames.scrollBody}`)
|
|
100
96
|
? root
|
|
101
|
-
: root.querySelector(
|
|
97
|
+
: root.querySelector(`.${ClassNames.scrollBody}`);
|
|
102
98
|
}
|
|
103
99
|
|
|
104
100
|
export function emojiDistanceFromScrollTop(emoji: NullableElement): number {
|
|
@@ -114,7 +110,7 @@ export function closestScrollBody(element: NullableElement): NullableElement {
|
|
|
114
110
|
return null;
|
|
115
111
|
}
|
|
116
112
|
|
|
117
|
-
return element.closest(
|
|
113
|
+
return element.closest(`.${ClassNames.scrollBody}`) ?? null;
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
export function emojiTruOffsetLeft(element: NullableElement): number {
|
|
@@ -221,14 +217,12 @@ export function closestCategory(element: NullableElement) {
|
|
|
221
217
|
if (!element) {
|
|
222
218
|
return null;
|
|
223
219
|
}
|
|
224
|
-
return element.closest(
|
|
220
|
+
return element.closest(`.${ClassNames.category}`) as HTMLElement;
|
|
225
221
|
}
|
|
226
222
|
|
|
227
223
|
export function closestCategoryContent(element: NullableElement) {
|
|
228
224
|
if (!element) {
|
|
229
225
|
return null;
|
|
230
226
|
}
|
|
231
|
-
return element.closest(
|
|
232
|
-
asSelectors(ClassNames.categoryContent),
|
|
233
|
-
) as HTMLElement;
|
|
227
|
+
return element.closest(`.${ClassNames.categoryContent}`) as HTMLElement;
|
|
234
228
|
}
|
|
@@ -55,7 +55,7 @@ export function EmojiCategory({
|
|
|
55
55
|
>
|
|
56
56
|
{CategoryHeader ? (
|
|
57
57
|
<CategoryHeader
|
|
58
|
-
className={cx(styles.label)}
|
|
58
|
+
className={cx(styles.label, appearance && styles.labelAppearance)}
|
|
59
59
|
aria-hidden
|
|
60
60
|
data-epr-part="category-label"
|
|
61
61
|
category={{ id: categoryId, name: categoryName }}
|
|
@@ -15,8 +15,12 @@ import { useEmojiVirtualization } from '../../hooks/useEmojiVirtualization';
|
|
|
15
15
|
import { useRegisterRegion } from '../../hooks/useRegisterRegion';
|
|
16
16
|
import { useMergedRefs } from '../../primitives/nativeProps';
|
|
17
17
|
import { CategoryConfig } from '../../types/exposedTypes';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { OVERSCAN_ROWS } from '../../virtualization/virtualizationHelpers';
|
|
19
|
+
import { useBodyRef, useEmojiListRef } from '../context/ElementRefContext';
|
|
20
|
+
import {
|
|
21
|
+
useEmojiSizeState,
|
|
22
|
+
useVisibleCategoriesState,
|
|
23
|
+
} from '../context/PickerContext';
|
|
20
24
|
import { useGetEmojisByCategory } from '../context/PickerDataContext';
|
|
21
25
|
|
|
22
26
|
import { EmojiCategory } from './EmojiCategory';
|
|
@@ -41,6 +45,22 @@ export function EmojiList({
|
|
|
41
45
|
[key: string]: number;
|
|
42
46
|
}>({});
|
|
43
47
|
const EmojiListRef = useEmojiListRef();
|
|
48
|
+
const BodyRef = useBodyRef();
|
|
49
|
+
const [emojiSize] = useEmojiSizeState();
|
|
50
|
+
const [visibleCategories] = useVisibleCategoriesState();
|
|
51
|
+
const onHeightReady = React.useCallback(
|
|
52
|
+
(categoryId: string, height: number) => {
|
|
53
|
+
setCategoryHeights((previous) =>
|
|
54
|
+
previous[categoryId] === height
|
|
55
|
+
? previous
|
|
56
|
+
: {
|
|
57
|
+
...previous,
|
|
58
|
+
[categoryId]: height,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
[],
|
|
63
|
+
);
|
|
44
64
|
const mergedRef = useMergedRefs(EmojiListRef, outerRef);
|
|
45
65
|
const getEmojisByCategory = useGetEmojisByCategory();
|
|
46
66
|
useRegisterRegion('grid', EmojiListRef);
|
|
@@ -87,17 +107,28 @@ export function EmojiList({
|
|
|
87
107
|
categoryEmojis={getEmojisByCategory(category, group)}
|
|
88
108
|
categoryConfig={categoryConfig}
|
|
89
109
|
topOffset={currentOffset}
|
|
90
|
-
|
|
110
|
+
isCategoryVisible={
|
|
111
|
+
index === 0 || visibleCategories.includes(categoryId)
|
|
112
|
+
}
|
|
91
113
|
positioned={isPositioned}
|
|
92
|
-
onHeightReady={
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
onHeightReady={onHeightReady}
|
|
115
|
+
// Outside a section's rendering/preload margin, scrolling
|
|
116
|
+
// cannot change its cells. Keep its prop stable so it can bail.
|
|
117
|
+
scrollTop={
|
|
118
|
+
categoryHeight === undefined
|
|
119
|
+
? scrollTop
|
|
120
|
+
: Math.max(
|
|
121
|
+
currentOffset -
|
|
122
|
+
(BodyRef.current?.clientHeight ?? 0) -
|
|
123
|
+
(emojiSize || 40) * (OVERSCAN_ROWS + 1),
|
|
124
|
+
Math.min(
|
|
125
|
+
scrollTop,
|
|
126
|
+
currentOffset +
|
|
127
|
+
categoryHeight +
|
|
128
|
+
(emojiSize || 40) * (OVERSCAN_ROWS + 1),
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
101
132
|
/>
|
|
102
133
|
);
|
|
103
134
|
})}
|
|
@@ -105,24 +136,28 @@ export function EmojiList({
|
|
|
105
136
|
);
|
|
106
137
|
}
|
|
107
138
|
|
|
108
|
-
function RenderCategory({
|
|
139
|
+
const RenderCategory = /* @__PURE__ */ React.memo(function RenderCategory({
|
|
109
140
|
categoryEmojis,
|
|
110
141
|
categoryConfig,
|
|
111
142
|
topOffset,
|
|
112
143
|
onHeightReady,
|
|
113
144
|
scrollTop,
|
|
114
|
-
|
|
145
|
+
isCategoryVisible,
|
|
115
146
|
positioned,
|
|
116
147
|
}: {
|
|
117
148
|
categoryEmojis: DataEmojis;
|
|
118
149
|
categoryConfig: CategoryConfig;
|
|
119
150
|
topOffset: number;
|
|
120
|
-
onHeightReady: (height: number) => void;
|
|
151
|
+
onHeightReady: (categoryId: string, height: number) => void;
|
|
121
152
|
scrollTop: number;
|
|
122
|
-
|
|
153
|
+
isCategoryVisible: boolean;
|
|
123
154
|
positioned: boolean;
|
|
124
155
|
}) {
|
|
125
|
-
const
|
|
156
|
+
const categoryId = categoryIdFromCategoryConfig(categoryConfig);
|
|
157
|
+
const reportHeight = React.useCallback(
|
|
158
|
+
(height: number) => onHeightReady(categoryId, height),
|
|
159
|
+
[categoryId, onHeightReady],
|
|
160
|
+
);
|
|
126
161
|
|
|
127
162
|
// The observer is the only writer of visibleCategories. When it never
|
|
128
163
|
// fires (zero-height container at mount, hidden modal), no category
|
|
@@ -130,15 +165,11 @@ function RenderCategory({
|
|
|
130
165
|
// the deadlock; the observer takes over from there.
|
|
131
166
|
// https://github.com/ealush/emoji-picker-react/issues/469
|
|
132
167
|
// https://github.com/ealush/emoji-picker-react/issues/475
|
|
133
|
-
const isCategoryVisible =
|
|
134
|
-
isFirstCategory ||
|
|
135
|
-
visibleCategories.includes(categoryIdFromCategoryConfig(categoryConfig));
|
|
136
|
-
|
|
137
168
|
const { virtualizedCounter, emojis, dimensions, emojiCount } =
|
|
138
169
|
useEmojiVirtualization({
|
|
139
170
|
categoryEmojis,
|
|
140
171
|
topOffset,
|
|
141
|
-
onHeightReady,
|
|
172
|
+
onHeightReady: reportHeight,
|
|
142
173
|
scrollTop,
|
|
143
174
|
isCategoryVisible,
|
|
144
175
|
positioned,
|
|
@@ -157,7 +188,7 @@ function RenderCategory({
|
|
|
157
188
|
{emojis}
|
|
158
189
|
</EmojiCategory>
|
|
159
190
|
);
|
|
160
|
-
}
|
|
191
|
+
});
|
|
161
192
|
|
|
162
193
|
const styles = /* @__PURE__ */ (() =>
|
|
163
194
|
stylesheet.create({
|
|
@@ -109,7 +109,11 @@ export function PickerDataProvider({
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
for (const custom of allCustomEmojis) {
|
|
112
|
-
if (
|
|
112
|
+
if (
|
|
113
|
+
emojiNames(custom).some((name) =>
|
|
114
|
+
name.toLowerCase().includes(normalized),
|
|
115
|
+
)
|
|
116
|
+
) {
|
|
113
117
|
dict[custom[Keys.unified]] = custom;
|
|
114
118
|
}
|
|
115
119
|
}
|
|
@@ -96,10 +96,12 @@ const styles = /* @__PURE__ */ (() =>
|
|
|
96
96
|
border: '1px solid var(--epr-search-border-color)',
|
|
97
97
|
':focus': {
|
|
98
98
|
backgroundColor: 'var(--epr-search-input-bg-color-active)',
|
|
99
|
-
border:
|
|
99
|
+
border:
|
|
100
|
+
'1px solid var(--epr-search-border-color-active, var(--epr-highlight-color, Highlight))',
|
|
100
101
|
},
|
|
101
102
|
'::placeholder': {
|
|
102
|
-
color:
|
|
103
|
+
color:
|
|
104
|
+
'var(--epr-search-input-placeholder-color, var(--epr-text-color, GrayText))',
|
|
103
105
|
},
|
|
104
106
|
},
|
|
105
107
|
}))();
|
|
@@ -157,10 +157,11 @@ const styles = /* @__PURE__ */ (() =>
|
|
|
157
157
|
backgroundColor: 'var(--epr-bg-color)',
|
|
158
158
|
overflow: 'hidden',
|
|
159
159
|
transition: 'height 0.3s ease-in-out, background-color 0.1s ease-in-out',
|
|
160
|
+
fontFamily: 'var(--epr-font-family, inherit)',
|
|
160
161
|
'*': {
|
|
161
162
|
boxSizing: 'border-box',
|
|
162
163
|
// Overridable without a specificity fight (v4 hard-coded sans-serif).
|
|
163
|
-
fontFamily: '
|
|
164
|
+
fontFamily: 'inherit',
|
|
164
165
|
},
|
|
165
166
|
},
|
|
166
167
|
baseVariables: {
|
|
@@ -9,13 +9,14 @@ const skinToneVariations = [
|
|
|
9
9
|
SkinTones.DARK,
|
|
10
10
|
];
|
|
11
11
|
|
|
12
|
-
export const skinTonesNamed =
|
|
13
|
-
(
|
|
14
|
-
acc[value]
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
export const skinTonesNamed = /* @__PURE__ */ (() =>
|
|
13
|
+
Object.entries(SkinTones).reduce(
|
|
14
|
+
(acc, [key, value]) => {
|
|
15
|
+
acc[value] = key;
|
|
16
|
+
return acc;
|
|
17
|
+
},
|
|
18
|
+
{} as Record<string, string>,
|
|
19
|
+
))();
|
|
19
20
|
|
|
20
21
|
export const skinTonesMapped: Record<string, string> =
|
|
21
22
|
skinToneVariations.reduce(
|
|
@@ -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,
|
package/src/data-core/prepare.ts
CHANGED
|
@@ -62,28 +62,10 @@ export function normalizeQuery(input: string): string {
|
|
|
62
62
|
return input.trim().toLowerCase();
|
|
63
63
|
}
|
|
64
64
|
|
|
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
65
|
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(),
|
|
66
|
+
if (!Array.isArray(raw)) return [];
|
|
67
|
+
return (raw.filter((value) => typeof value === 'string') as string[]).map(
|
|
68
|
+
(value) => value.toLowerCase(),
|
|
87
69
|
);
|
|
88
70
|
}
|
|
89
71
|
|
|
@@ -106,11 +88,18 @@ function appendEntry(
|
|
|
106
88
|
const names = readStringArray((entry as { n?: unknown }).n);
|
|
107
89
|
const variations = readStringArray((entry as { v?: unknown }).v);
|
|
108
90
|
|
|
109
|
-
const
|
|
110
|
-
const addedIn =
|
|
111
|
-
typeof rawAddedIn === 'string' ? rawAddedIn : String(rawAddedIn ?? '');
|
|
91
|
+
const addedIn = String((entry as { a?: unknown }).a ?? '');
|
|
112
92
|
|
|
113
|
-
|
|
93
|
+
// Freeze the arrays readStringArray owns directly, without copying them
|
|
94
|
+
// a second time. Caller arrays are never mutated or frozen.
|
|
95
|
+
const info: EmojiInfo = Object.freeze({
|
|
96
|
+
unified,
|
|
97
|
+
emoji: parseNativeEmoji(unified),
|
|
98
|
+
name: names[names.length - 1] || '',
|
|
99
|
+
names: Object.freeze(names),
|
|
100
|
+
variations: Object.freeze(variations),
|
|
101
|
+
addedIn,
|
|
102
|
+
});
|
|
114
103
|
records.push(info);
|
|
115
104
|
byUnified.set(unified, info);
|
|
116
105
|
for (const variation of info.variations) {
|
|
@@ -121,18 +110,17 @@ function appendEntry(
|
|
|
121
110
|
indexCharBucket(byCharMutable, names, info);
|
|
122
111
|
}
|
|
123
112
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
113
|
+
// Records are indexed in order. A bucket already ending in this record
|
|
114
|
+
// has seen the character, avoiding a temporary Set for every emoji.
|
|
126
115
|
function indexCharBucket(
|
|
127
116
|
byCharMutable: Map<string, EmojiInfo[]>,
|
|
128
117
|
names: readonly string[],
|
|
129
118
|
info: EmojiInfo,
|
|
130
119
|
): void {
|
|
131
|
-
const
|
|
132
|
-
for (const char of distinctChars) {
|
|
120
|
+
for (const char of names.join('')) {
|
|
133
121
|
const bucket = byCharMutable.get(char);
|
|
134
122
|
if (bucket) {
|
|
135
|
-
bucket.push(info);
|
|
123
|
+
if (bucket[bucket.length - 1] !== info) bucket.push(info);
|
|
136
124
|
} else {
|
|
137
125
|
byCharMutable.set(char, [info]);
|
|
138
126
|
}
|
|
@@ -161,15 +149,12 @@ export function getPreparedCore(emojiData?: EmojiData): PreparedCore {
|
|
|
161
149
|
}
|
|
162
150
|
}
|
|
163
151
|
|
|
164
|
-
|
|
165
|
-
byCharMutable.forEach((bucket, char) => {
|
|
166
|
-
byChar.set(char, Object.freeze(bucket));
|
|
167
|
-
});
|
|
152
|
+
byCharMutable.forEach(Object.freeze);
|
|
168
153
|
|
|
169
154
|
const core: PreparedCore = {
|
|
170
155
|
records,
|
|
171
156
|
byUnified,
|
|
172
|
-
byChar,
|
|
157
|
+
byChar: byCharMutable,
|
|
173
158
|
queryMemo: new Map<string, readonly EmojiInfo[]>(),
|
|
174
159
|
};
|
|
175
160
|
|
|
@@ -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();
|