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.
Files changed (52) hide show
  1. package/dist/DomUtils/classNames.d.ts +0 -1
  2. package/dist/DomUtils/keyboardNavigation.d.ts +10 -2
  3. package/dist/DomUtils/selectors.d.ts +2 -2
  4. package/dist/data/index.js +1 -1
  5. package/dist/data/skinToneVariations.js +2 -2
  6. package/dist/data/skinToneVariations.ts +8 -7
  7. package/dist/esm/chunks/chunk-7ZGTUZFB.mjs +8021 -12
  8. package/dist/esm/chunks/chunk-ACXAQEKG.mjs +149 -0
  9. package/dist/esm/chunks/chunk-F2KS7DCA.mjs +6735 -0
  10. package/dist/esm/chunks/chunk-L5W4PFRJ.mjs +14 -0
  11. package/dist/esm/chunks/chunk-LO4TPPLH.mjs +32 -42
  12. package/dist/esm/chunks/emojis-6L3GRBAP.mjs +5 -7
  13. package/dist/esm/data.mjs +9 -16
  14. package/dist/esm/index.mjs +230 -340
  15. package/dist/esm/primitives/index.mjs +6 -65
  16. package/dist/hooks/preloadEmoji.d.ts +0 -4
  17. package/dist/hooks/useCategoryHeight.d.ts +2 -1
  18. package/dist/hooks/useEmojiVirtualization.d.ts +1 -0
  19. package/dist/index.js +1 -1
  20. package/dist/primitives/index.js +1 -1
  21. package/dist/virtualization/virtualizationHelpers.d.ts +2 -0
  22. package/llms-full.txt +6 -3
  23. package/package.json +1 -1
  24. package/src/DomUtils/classNames.ts +0 -4
  25. package/src/DomUtils/elementPositionInRow.ts +2 -2
  26. package/src/DomUtils/keyboardNavigation.ts +74 -10
  27. package/src/DomUtils/scrollTo.ts +2 -2
  28. package/src/DomUtils/selectors.ts +10 -16
  29. package/src/components/body/EmojiCategory.tsx +1 -1
  30. package/src/components/body/EmojiList.tsx +54 -23
  31. package/src/components/context/PickerDataContext.tsx +5 -1
  32. package/src/components/header/Search/Search.tsx +4 -2
  33. package/src/components/main/defaultAppearance.tsx +2 -1
  34. package/src/data/skinToneVariations.ts +8 -7
  35. package/src/data-core/pickerData.ts +1 -3
  36. package/src/data-core/prepare.ts +20 -35
  37. package/src/dataUtils/emojiSelectors.ts +2 -1
  38. package/src/dataUtils/parseNativeEmoji.ts +3 -4
  39. package/src/hooks/preloadEmoji.ts +1 -46
  40. package/src/hooks/useActiveCategoryScrollDetection.ts +22 -19
  41. package/src/hooks/useCategoryHeight.ts +38 -6
  42. package/src/hooks/useDisallowedEmojis.ts +18 -13
  43. package/src/hooks/useEmojiVirtualization.tsx +93 -67
  44. package/src/hooks/useFilter.ts +10 -8
  45. package/src/hooks/useIsEmojiHidden.ts +16 -12
  46. package/src/hooks/useKeyboardNavigation.ts +104 -17
  47. package/src/primitives/structuralStyles.tsx +3 -0
  48. package/src/primitives/tokens.ts +1 -0
  49. package/src/virtualization/virtualizationHelpers.ts +9 -2
  50. package/dist/esm/chunks/chunk-46BUDQLE.mjs +0 -228
  51. package/dist/esm/chunks/chunk-FQIJ4HBW.mjs +0 -25
  52. package/dist/esm/chunks/chunk-KQPOIRSC.mjs +0 -9305
@@ -9,12 +9,12 @@ import {
9
9
  import { getActiveElement } from '../DomUtils/getActiveElement';
10
10
  import { isRtl } from '../DomUtils/isRtl';
11
11
  import {
12
- focusAndClickFirstVisibleEmoji,
12
+ focusFirstEmoji,
13
13
  focusFirstVisibleEmoji,
14
14
  focusAdjacentEmoji,
15
15
  } from '../DomUtils/keyboardNavigation';
16
16
  import { useScrollTo } from '../DomUtils/scrollTo';
17
- import { buttonFromTarget } from '../DomUtils/selectors';
17
+ import { buttonFromTarget, firstVisibleEmoji } from '../DomUtils/selectors';
18
18
  import {
19
19
  ElementRef,
20
20
  useBodyRef,
@@ -27,9 +27,16 @@ import {
27
27
  import {
28
28
  useNavigationRegistry,
29
29
  useSearchComposingState,
30
+ useSearchDisplayState,
31
+ useSearchTermState,
30
32
  useSkinToneFanOpenState,
31
33
  } from '../components/context/PickerContext';
32
- import { useSearchDisabledConfig } from '../config/useConfig';
34
+ import { usePickerDataContext } from '../components/context/PickerDataContext';
35
+ import {
36
+ useSearchDisabledConfig,
37
+ useSuggestedEmojisConfig,
38
+ } from '../config/useConfig';
39
+ import { normalizeQuery } from '../data-core/prepare';
33
40
  import {
34
41
  NavigationRegionKind,
35
42
  RegisteredRegion,
@@ -47,7 +54,11 @@ import {
47
54
  useFocusSkinTonePicker,
48
55
  } from './useFocus';
49
56
  import useIsSearchMode from './useIsSearchMode';
50
- import { useClearSearchValue, useTypeToSearchKey } from './useSearchController';
57
+ import {
58
+ useAcceptedSearchValue,
59
+ useClearSearchValue,
60
+ useTypeToSearchKey,
61
+ } from './useSearchController';
51
62
  import useSetVariationPicker from './useSetVariationPicker';
52
63
 
53
64
  export function useKeyboardNavigation() {
@@ -76,6 +87,65 @@ export function useKeyboardNavigation() {
76
87
  const focusPrevRegionFromGrid = useFocusPrevRegionFrom('grid');
77
88
  const setVariationPicker = useSetVariationPicker();
78
89
  const registry = useNavigationRegistry();
90
+ const [searchTerm] = useSearchTermState();
91
+ const suggestions = JSON.stringify(useSuggestedEmojisConfig());
92
+ const { emojiData } = usePickerDataContext();
93
+ const searchValue = useAcceptedSearchValue();
94
+ // A controlled parent may not have accepted what the input shows yet.
95
+ const [displayed] = useSearchDisplayState();
96
+ // Enter pressed while the input is ahead of the debounced results: the
97
+ // query it was pressed for, selected once those results commit.
98
+ const pendingPick = useRef<string | null>(null);
99
+ const pickEmoji = (emoji: HTMLElement | null) => {
100
+ pendingPick.current = null;
101
+ focusElement(emoji);
102
+ emoji?.click();
103
+ };
104
+ const selectFirstResult = () => {
105
+ const query = pendingPick.current;
106
+ const generation = registry.currentGeneration();
107
+ if (
108
+ !focusFirstEmoji(BodyRef.current, {
109
+ shouldFocus: () =>
110
+ pendingPick.current === query &&
111
+ SearchInputRef.current?.value === query &&
112
+ registry.isCurrent(generation),
113
+ moveTo: pickEmoji,
114
+ })
115
+ )
116
+ pendingPick.current = null;
117
+ };
118
+ // Each committed result set settles a queued Enter for its query.
119
+ useEffect(() => {
120
+ const pending = pendingPick.current;
121
+ pendingPick.current = null;
122
+ if (
123
+ pending !== null &&
124
+ pending === displayed &&
125
+ normalizeQuery(pending) === searchTerm
126
+ ) {
127
+ pendingPick.current = pending;
128
+ selectFirstResult();
129
+ }
130
+ // Per committed result set; selection reads live refs.
131
+ // eslint-disable-next-line react-hooks/exhaustive-deps
132
+ }, [searchTerm, suggestions, emojiData]);
133
+ // Enter in search selects the first result, even one scrolled out of view.
134
+ const selectFromSearch = () => {
135
+ // Remember fast Enter while the input is ahead of the debounced grid.
136
+ if (
137
+ ![searchValue, displayed].every(
138
+ (value) => normalizeQuery(value) === searchTerm,
139
+ )
140
+ ) {
141
+ pendingPick.current = displayed;
142
+ return;
143
+ }
144
+ if (isSearchMode) {
145
+ pendingPick.current = displayed;
146
+ selectFirstResult();
147
+ } else pickEmoji(firstVisibleEmoji(BodyRef.current));
148
+ };
79
149
  const goDownFromSearchInput = () => {
80
150
  if (isSearchMode) return focusFirstVisibleEmoji(BodyRef.current);
81
151
  focusCategoryNavigation();
@@ -90,14 +160,21 @@ export function useKeyboardNavigation() {
90
160
  disallowMouseMove();
91
161
  switch (key) {
92
162
  case 'Escape':
93
- event.preventDefault();
94
163
  if (hasOpenToggles()) {
164
+ event.preventDefault();
95
165
  closeAllOpenToggles();
96
166
  // A nested variation/tone menu consumes Escape before the
97
167
  // host popover or autocomplete dismisses the whole picker.
98
168
  event.stopPropagation();
99
169
  return;
100
170
  }
171
+ // Clearing a search consumes the key. With nothing to clear, the
172
+ // event stays untouched (this runs in window capture, before host
173
+ // listeners that skip default-prevented keys, such as Radix).
174
+ if (searchValue || displayed) {
175
+ event.preventDefault();
176
+ event.stopPropagation();
177
+ }
101
178
  clearSearch();
102
179
  scrollTo(0);
103
180
  focusSearchInput();
@@ -129,7 +206,7 @@ export function useKeyboardNavigation() {
129
206
  break;
130
207
  case 'Enter':
131
208
  event.preventDefault();
132
- focusAndClickFirstVisibleEmoji(BodyRef.current);
209
+ selectFromSearch();
133
210
  break;
134
211
  }
135
212
  };
@@ -245,20 +322,15 @@ export function useKeyboardNavigation() {
245
322
  }
246
323
  };
247
324
  const [composing] = useSearchComposingState();
248
- const handler = useRef<(event: KeyboardEvent, menusOnly?: boolean) => void>(
325
+ const handler = useRef<(event: KeyboardEvent, escapeOnly?: boolean) => void>(
249
326
  () => {},
250
327
  );
251
328
  // One listener owns the native lifecycle; current callbacks supply state
252
329
  // and live region refs without reattaching on every picker render.
253
- handler.current = (event, menusOnly) => {
254
- if (
255
- composing ||
256
- event.isComposing ||
257
- event.keyCode === 229 ||
258
- (menusOnly && !hasOpenToggles())
259
- )
260
- return;
261
- if (!menusOnly) {
330
+ handler.current = (event, escapeOnly) => {
331
+ if (composing || event.isComposing || event.keyCode === 229) return;
332
+ if (event.key !== 'Enter') pendingPick.current = null;
333
+ if (!escapeOnly) {
262
334
  search(event);
263
335
  tone(event);
264
336
  categories(event);
@@ -272,16 +344,31 @@ export function useKeyboardNavigation() {
272
344
  // Descendant parts are read through live refs by the delegated handler.
273
345
  const current = ref.current;
274
346
  if (!current) return;
347
+ // Escape runs once, in the window capture phase, so a consumed key is
348
+ // stopped before host dismissal listeners (Radix captures on document).
349
+ let captured: KeyboardEvent | undefined;
275
350
  const scoped = (event: KeyboardEvent) => {
351
+ if (event === captured) return;
276
352
  if (eventBelongsToPicker(event, current)) handler.current(event);
277
353
  };
278
354
  const capture = (event: KeyboardEvent) => {
279
- if (event.key === 'Escape' && eventBelongsToPicker(event, current))
355
+ if (event.key === 'Escape' && eventBelongsToPicker(event, current)) {
356
+ captured = event;
280
357
  handler.current(event, true);
358
+ }
359
+ };
360
+ // Editing the query (paste, autocorrect) or leaving the picker cancels
361
+ // a queued Enter.
362
+ const cancelPendingPick = () => {
363
+ pendingPick.current = null;
281
364
  };
365
+ current.addEventListener('input', cancelPendingPick);
366
+ current.addEventListener('focusout', cancelPendingPick);
282
367
  current.addEventListener('keydown', scoped);
283
368
  window.addEventListener('keydown', capture, true);
284
369
  return () => {
370
+ current.removeEventListener('input', cancelPendingPick);
371
+ current.removeEventListener('focusout', cancelPendingPick);
285
372
  current.removeEventListener('keydown', scoped);
286
373
  window.removeEventListener('keydown', capture, true);
287
374
  };
@@ -46,6 +46,9 @@ const boxSizingReset =
46
46
  // vertical tab rail still adds its own width beside the grid.
47
47
  ':where([data-epr-columns] :is([data-epr-part=preview],[data-epr-part=search],[aria-orientation=horizontal])){contain:inline-size}' +
48
48
  ':where([data-epr-columns] [data-epr-part=viewport]){scrollbar-gutter:stable}' +
49
+ // A permanent sizing row keeps intrinsic width when all categories are
50
+ // hidden (no matches) or the dataset is still loading/has failed.
51
+ ':where([data-epr-columns] [data-epr-part=list])::before{content:"";display:block;height:0;width:calc(var(--epr-columns)*var(--epr-emoji-fullsize) + 2*var(--epr-horizontal-padding));max-width:100%}' +
49
52
  ':where([data-epr-columns] [data-epr-part=category-content]){width:calc(var(--epr-columns)*var(--epr-emoji-fullsize));min-width:calc(100% - 2*var(--epr-horizontal-padding));max-width:calc(100% - 2*var(--epr-horizontal-padding))}';
50
53
 
51
54
  export const structuralStyles = /* @__PURE__ */ (() =>
@@ -140,6 +140,7 @@ export const darkPickerTokens: Record<string, string> = {
140
140
 
141
141
  /** Full light preset: geometry plus light colors (v4 defaults). */
142
142
  export const defaultPickerTokens: Record<string, string> = {
143
+ '--epr-font-family': 'sans-serif',
143
144
  ...lightPickerTokens,
144
145
  ...structuralPickerTokens,
145
146
  };
@@ -1,3 +1,9 @@
1
+ export const OVERSCAN_ROWS = 2;
2
+
3
+ export function isJustBelowViewport(top: number, bottom: number, size: number) {
4
+ return top >= bottom && top < bottom + size * OVERSCAN_ROWS;
5
+ }
6
+
1
7
  export function shouldVirtualize({
2
8
  scrollTop,
3
9
  clientHeight,
@@ -19,8 +25,9 @@ export function shouldVirtualize({
19
25
  const emojiBottom = emojiTop + dimensions.emojiSize;
20
26
 
21
27
  const isVisible =
22
- emojiBottom + dimensions.emojiSize * 2 >= scrollTop &&
23
- emojiTop <= scrollTop + clientHeight + dimensions.emojiSize;
28
+ emojiBottom + dimensions.emojiSize * OVERSCAN_ROWS >= scrollTop &&
29
+ emojiTop <=
30
+ scrollTop + clientHeight + dimensions.emojiSize * (OVERSCAN_ROWS - 1);
24
31
 
25
32
  return !isVisible;
26
33
  }
@@ -1,228 +0,0 @@
1
- import {
2
- __esm
3
- } from "./chunk-LO4TPPLH.mjs";
4
-
5
- // src/data/defaultEmojiData.ts
6
- function registerDefaultEmojiData(data) {
7
- registered = data;
8
- }
9
- function getRegisteredDefaultEmojiData() {
10
- return registered;
11
- }
12
- function defaultEmojiDataOrEmpty() {
13
- return registered ?? EMPTY_EMOJI_DATA;
14
- }
15
- function loadDefaultEmojiData() {
16
- if (registered) {
17
- return Promise.resolve(registered);
18
- }
19
- if (!pending) {
20
- pending = import("./emojis-6L3GRBAP.mjs").then((module) => {
21
- const data = module.default ?? module;
22
- registered = registered ?? data;
23
- return registered;
24
- }).catch((error) => {
25
- pending = null;
26
- throw error;
27
- });
28
- }
29
- return pending;
30
- }
31
- var registered, EMPTY_EMOJI_DATA, pending;
32
- var init_defaultEmojiData = __esm({
33
- "src/data/defaultEmojiData.ts"() {
34
- "use strict";
35
- registered = null;
36
- EMPTY_EMOJI_DATA = Object.freeze({
37
- categories: {},
38
- emojis: {}
39
- });
40
- pending = null;
41
- }
42
- });
43
-
44
- // src/data-core/prepare.ts
45
- init_defaultEmojiData();
46
-
47
- // src/dataUtils/parseNativeEmoji.ts
48
- function parseNativeEmoji(unified) {
49
- return unified.split("-").map((hex) => String.fromCodePoint(parseInt(hex, 16))).join("");
50
- }
51
-
52
- // src/data-core/prepare.ts
53
- var coreCache = /* @__PURE__ */ new WeakMap();
54
- var prepareCount = 0;
55
- function normalizeUnified(input) {
56
- return input.trim().toLowerCase();
57
- }
58
- function normalizeQuery(input) {
59
- if (!input || typeof input !== "string") {
60
- return "";
61
- }
62
- return input.trim().toLowerCase();
63
- }
64
- function toEmojiInfo(unified, names, variations, addedIn) {
65
- return Object.freeze({
66
- unified,
67
- emoji: parseNativeEmoji(unified),
68
- name: names[names.length - 1] || "",
69
- names: Object.freeze([...names]),
70
- variations: Object.freeze([...variations ?? []]),
71
- addedIn
72
- });
73
- }
74
- function readStringArray(raw) {
75
- if (!Array.isArray(raw)) {
76
- return [];
77
- }
78
- return raw.filter((n) => typeof n === "string").map(
79
- (n) => n.toLowerCase()
80
- );
81
- }
82
- function appendEntry(byUnified, byCharMutable, records, entry) {
83
- const rawUnified = entry.u;
84
- if (typeof rawUnified !== "string" || rawUnified.length === 0) {
85
- return;
86
- }
87
- const unified = rawUnified.toLowerCase();
88
- if (byUnified.has(unified)) {
89
- return;
90
- }
91
- const names = readStringArray(entry.n);
92
- const variations = readStringArray(entry.v);
93
- const rawAddedIn = entry.a;
94
- const addedIn = typeof rawAddedIn === "string" ? rawAddedIn : String(rawAddedIn ?? "");
95
- const info = toEmojiInfo(unified, names, variations, addedIn);
96
- records.push(info);
97
- byUnified.set(unified, info);
98
- for (const variation of info.variations) {
99
- if (!byUnified.has(variation)) {
100
- byUnified.set(variation, info);
101
- }
102
- }
103
- indexCharBucket(byCharMutable, names, info);
104
- }
105
- function indexCharBucket(byCharMutable, names, info) {
106
- const distinctChars = Array.from(new Set(names.join("")));
107
- for (const char of distinctChars) {
108
- const bucket = byCharMutable.get(char);
109
- if (bucket) {
110
- bucket.push(info);
111
- } else {
112
- byCharMutable.set(char, [info]);
113
- }
114
- }
115
- }
116
- function getPreparedCore(emojiData) {
117
- const source = emojiData ?? defaultEmojiDataOrEmpty();
118
- const cached = coreCache.get(source);
119
- if (cached) {
120
- return cached;
121
- }
122
- prepareCount += 1;
123
- const byUnified = /* @__PURE__ */ new Map();
124
- const byCharMutable = /* @__PURE__ */ new Map();
125
- const records = [];
126
- const groups = source.emojis ?? {};
127
- for (const key of Object.keys(groups)) {
128
- const list = groups[key] ?? [];
129
- for (const entry of list) {
130
- appendEntry(byUnified, byCharMutable, records, entry);
131
- }
132
- }
133
- const byChar = /* @__PURE__ */ new Map();
134
- byCharMutable.forEach((bucket, char) => {
135
- byChar.set(char, Object.freeze(bucket));
136
- });
137
- const core = {
138
- records,
139
- byUnified,
140
- byChar,
141
- queryMemo: /* @__PURE__ */ new Map()
142
- };
143
- coreCache.set(source, core);
144
- return core;
145
- }
146
-
147
- // src/data-core/search.ts
148
- var EMPTY_RESULTS = Object.freeze([]);
149
- var MAX_QUERY_MEMO_SIZE = 500;
150
- function memoizeQuery(core, normalized, results) {
151
- const memo = core.queryMemo;
152
- if (memo.has(normalized)) {
153
- memo.delete(normalized);
154
- } else if (memo.size >= MAX_QUERY_MEMO_SIZE) {
155
- const oldest = memo.keys().next();
156
- if (!oldest.done) {
157
- memo.delete(oldest.value);
158
- }
159
- }
160
- memo.set(normalized, results);
161
- }
162
- function smallestCandidateBucket(core, normalized) {
163
- let smallest = core.records;
164
- for (const char of Array.from(normalized)) {
165
- const bucket = core.byChar.get(char);
166
- if (!bucket) {
167
- return EMPTY_RESULTS;
168
- }
169
- if (bucket.length < smallest.length) {
170
- smallest = bucket;
171
- }
172
- }
173
- return smallest;
174
- }
175
- function getEmojiByUnified(unified, options) {
176
- if (!unified || typeof unified !== "string") {
177
- return void 0;
178
- }
179
- const normalized = normalizeUnified(unified);
180
- if (!normalized) {
181
- return void 0;
182
- }
183
- const core = getPreparedCore(options?.emojiData);
184
- return core.byUnified.get(normalized);
185
- }
186
- function searchEmojis(query, options) {
187
- const normalized = normalizeQuery(query);
188
- if (!normalized) {
189
- return EMPTY_RESULTS;
190
- }
191
- const core = getPreparedCore(options?.emojiData);
192
- const memoized = core.queryMemo.get(normalized);
193
- if (memoized) {
194
- memoizeQuery(core, normalized, memoized);
195
- return Object.freeze([...memoized]);
196
- }
197
- if (normalized.length === 1) {
198
- const bucket = core.byChar.get(normalized) ?? EMPTY_RESULTS;
199
- memoizeQuery(core, normalized, bucket);
200
- return Object.freeze([...bucket]);
201
- }
202
- const matches = [];
203
- for (const record of smallestCandidateBucket(core, normalized)) {
204
- const names = record.names;
205
- for (let i = 0; i < names.length; i += 1) {
206
- if (names[i].includes(normalized)) {
207
- matches.push(record);
208
- break;
209
- }
210
- }
211
- }
212
- memoizeQuery(core, normalized, matches);
213
- return Object.freeze([...matches]);
214
- }
215
-
216
- export {
217
- registerDefaultEmojiData,
218
- getRegisteredDefaultEmojiData,
219
- EMPTY_EMOJI_DATA,
220
- defaultEmojiDataOrEmpty,
221
- loadDefaultEmojiData,
222
- init_defaultEmojiData,
223
- parseNativeEmoji,
224
- normalizeQuery,
225
- getPreparedCore,
226
- getEmojiByUnified,
227
- searchEmojis
228
- };
@@ -1,25 +0,0 @@
1
- import {
2
- init_defaultEmojiData,
3
- registerDefaultEmojiData
4
- } from "./chunk-46BUDQLE.mjs";
5
- import {
6
- emojis_default,
7
- init_emojis
8
- } from "./chunk-7ZGTUZFB.mjs";
9
- import {
10
- __commonJS
11
- } from "./chunk-LO4TPPLH.mjs";
12
-
13
- // src/data/registerDefaultEmojiData.ts
14
- var require_registerDefaultEmojiData = __commonJS({
15
- "src/data/registerDefaultEmojiData.ts"() {
16
- "use strict";
17
- init_defaultEmojiData();
18
- init_emojis();
19
- registerDefaultEmojiData(emojis_default);
20
- }
21
- });
22
-
23
- export {
24
- require_registerDefaultEmojiData
25
- };