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
|
@@ -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
|
|
|
@@ -1633,10 +1633,11 @@ Existing `reactionsDefaultOpen`, `allowExpandReactions`, `reactions`, `onReactio
|
|
|
1633
1633
|
|
|
1634
1634
|
For standard emoji IDs:
|
|
1635
1635
|
- matching is case-insensitive;
|
|
1636
|
+
- matching ignores where U+FE0F appears: `'2764'` and `'2764-fe0f'` both resolve to the dataset's own spelling, the same rule `reactions` and `hiddenEmojis` use;
|
|
1636
1637
|
- a valid variation remains that exact variation for rendering rather than collapsing to the neutral base emoji;
|
|
1637
1638
|
- duplicates are removed by canonical normalized ID, first occurrence wins.
|
|
1638
1639
|
|
|
1639
|
-
Custom emoji IDs use the same case-insensitive rule
|
|
1640
|
+
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
1641
|
|
|
1641
1642
|
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
1643
|
|
|
@@ -1891,6 +1892,8 @@ Styling is opt-in in both directions:
|
|
|
1891
1892
|
|
|
1892
1893
|
`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
1894
|
|
|
1895
|
+
The intrinsic grid width remains present during an empty search, data loading or a load error.
|
|
1896
|
+
|
|
1894
1897
|
## 12. Data API
|
|
1895
1898
|
|
|
1896
1899
|
The data entry point is defined in [DATA_API.md](./DATA_API.md):
|
|
@@ -2362,11 +2365,13 @@ The following variables are already documented public customization surface and
|
|
|
2362
2365
|
- `--epr-preview-text-color`
|
|
2363
2366
|
|
|
2364
2367
|
### Native emoji font
|
|
2368
|
+
- `--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
2369
|
- `--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
2370
|
|
|
2367
2371
|
### Skin tone
|
|
2368
2372
|
- `--epr-skin-tone-picker-menu-color`
|
|
2369
2373
|
- `--epr-skin-tone-size`
|
|
2374
|
+
- `--epr-skin-tone-neutral-color`, `--epr-skin-tone-light-color`, `--epr-skin-tone-medium-light-color`, `--epr-skin-tone-medium-color`, `--epr-skin-tone-medium-dark-color`, `--epr-skin-tone-dark-color` — the tone swatch colors (defaults `#ffd225`, `#ffdfbd`, `#e9c197`, `#c88e62`, `#a86637`, `#60463a`).
|
|
2370
2375
|
|
|
2371
2376
|
### Dark mode
|
|
2372
2377
|
- `--epr-dark-bg-color`
|
|
@@ -3025,6 +3030,7 @@ No other data helpers are part of v5.
|
|
|
3025
3030
|
- trims surrounding whitespace;
|
|
3026
3031
|
- normalizes hexadecimal unified code to lowercase;
|
|
3027
3032
|
- accepts base or variation unified code;
|
|
3033
|
+
- accepts either U+FE0F spelling (`2764` or `2764-fe0f`); an exact match wins;
|
|
3028
3034
|
- variation lookup returns the canonical/base EmojiInfo whose `variations` includes the requested variation;
|
|
3029
3035
|
- returns `undefined` for unknown/empty input;
|
|
3030
3036
|
- does not mutate the dataset;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "5.0.
|
|
2
|
+
"version": "5.0.2",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"typings": "dist/index.d.ts",
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
"path": "dist/esm/index.mjs",
|
|
186
|
-
"limit": "
|
|
186
|
+
"limit": "76 KB"
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"path": "dist/esm/primitives/index.mjs",
|
|
@@ -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({
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
useSuggestedEmojisModeConfig,
|
|
6
6
|
} from '../../config/useConfig';
|
|
7
7
|
import { getPickerDataSnapshot } from '../../data-core/pickerData';
|
|
8
|
-
import { normalizeQuery } from '../../data-core/prepare';
|
|
8
|
+
import { normalizeQuery, withoutFe0f } from '../../data-core/prepare';
|
|
9
9
|
import { searchEmojis } from '../../data-core/search';
|
|
10
10
|
import {
|
|
11
11
|
DataEmoji,
|
|
@@ -79,6 +79,7 @@ export function PickerDataProvider({
|
|
|
79
79
|
|
|
80
80
|
const result =
|
|
81
81
|
data.allEmojisByUnified[unified] ??
|
|
82
|
+
data.allEmojisByUnified[withoutFe0f(unified)] ??
|
|
82
83
|
data.allEmojisByUnified[unifiedWithoutSkinTone(unified)];
|
|
83
84
|
return result;
|
|
84
85
|
},
|
|
@@ -109,7 +110,11 @@ export function PickerDataProvider({
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
for (const custom of allCustomEmojis) {
|
|
112
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
emojiNames(custom).some((name) =>
|
|
115
|
+
name.toLowerCase().includes(normalized),
|
|
116
|
+
)
|
|
117
|
+
) {
|
|
113
118
|
dict[custom[Keys.unified]] = custom;
|
|
114
119
|
}
|
|
115
120
|
}
|
|
@@ -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
|
}))();
|
|
@@ -108,23 +108,24 @@ const styles = /* @__PURE__ */ (() =>
|
|
|
108
108
|
':focus': {
|
|
109
109
|
boxShadow: '0 0 0 3px var(--epr-focus-bg-color)',
|
|
110
110
|
},
|
|
111
|
+
// Swatch colors are themeable; the fallbacks are the defaults.
|
|
111
112
|
'&.epr-tone-neutral': {
|
|
112
|
-
backgroundColor: '#ffd225',
|
|
113
|
+
backgroundColor: 'var(--epr-skin-tone-neutral-color, #ffd225)',
|
|
113
114
|
},
|
|
114
115
|
'&.epr-tone-1f3fb': {
|
|
115
|
-
backgroundColor: '#ffdfbd',
|
|
116
|
+
backgroundColor: 'var(--epr-skin-tone-light-color, #ffdfbd)',
|
|
116
117
|
},
|
|
117
118
|
'&.epr-tone-1f3fc': {
|
|
118
|
-
backgroundColor: '#e9c197',
|
|
119
|
+
backgroundColor: 'var(--epr-skin-tone-medium-light-color, #e9c197)',
|
|
119
120
|
},
|
|
120
121
|
'&.epr-tone-1f3fd': {
|
|
121
|
-
backgroundColor: '#c88e62',
|
|
122
|
+
backgroundColor: 'var(--epr-skin-tone-medium-color, #c88e62)',
|
|
122
123
|
},
|
|
123
124
|
'&.epr-tone-1f3fe': {
|
|
124
|
-
backgroundColor: '#a86637',
|
|
125
|
+
backgroundColor: 'var(--epr-skin-tone-medium-dark-color, #a86637)',
|
|
125
126
|
},
|
|
126
127
|
'&.epr-tone-1f3ff': {
|
|
127
|
-
backgroundColor: '#60463a',
|
|
128
|
+
backgroundColor: 'var(--epr-skin-tone-dark-color, #60463a)',
|
|
128
129
|
},
|
|
129
130
|
},
|
|
130
131
|
}))();
|
|
@@ -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: {
|
package/src/config/config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { DEFAULT_REACTIONS } from '../components/Reactions/DEFAULT_REACTIONS';
|
|
4
4
|
import { GetEmojiUrl } from '../components/emoji/BaseEmojiProps';
|
|
5
|
+
import { withoutFe0f } from '../data-core/prepare';
|
|
5
6
|
import { emojiUrlByUnified } from '../dataUtils/emojiUtils';
|
|
6
7
|
import type { EmojiDataInput } from '../hooks/useResolvedEmojiData';
|
|
7
8
|
import {
|
|
@@ -27,7 +28,8 @@ import {
|
|
|
27
28
|
} from './categoryConfig';
|
|
28
29
|
import { CustomEmoji } from './customEmojiConfig';
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
// Stored without U+FE0F, like every hiddenEmojis entry.
|
|
32
|
+
const KNOWN_FAILING_EMOJIS = ['2640', '2642', '2695'];
|
|
31
33
|
|
|
32
34
|
export const DEFAULT_SEARCH_PLACEHOLDER = 'Search';
|
|
33
35
|
export const DEFAULT_PICKER_WIDTH = 350;
|
|
@@ -171,9 +173,10 @@ export function mergeConfig(
|
|
|
171
173
|
|
|
172
174
|
// Dataset ids are lowercase; accept the uppercase spelling consumers
|
|
173
175
|
// copy from docs and tools (and stray whitespace), like `reactions` and
|
|
174
|
-
// `suggestedEmojis` already do.
|
|
176
|
+
// `suggestedEmojis` already do. Kept without U+FE0F, so either spelling
|
|
177
|
+
// of an ID hides the emoji (see useIsEmojiDisallowed).
|
|
175
178
|
config.hiddenEmojis.forEach((emoji) => {
|
|
176
|
-
const id = String(emoji).trim().toLowerCase();
|
|
179
|
+
const id = withoutFe0f(String(emoji).trim().toLowerCase());
|
|
177
180
|
if (id) config.unicodeToHide.add(id);
|
|
178
181
|
});
|
|
179
182
|
|
|
@@ -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(
|