emoji-picker-react 4.0.0-rc.9 → 4.0.0

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 (117) hide show
  1. package/README.md +176 -43
  2. package/dist/DomUtils/classNames.d.ts +9 -1
  3. package/dist/DomUtils/elementPositionInRow.d.ts +10 -0
  4. package/dist/DomUtils/focusElement.d.ts +5 -0
  5. package/dist/DomUtils/getActiveElement.d.ts +2 -0
  6. package/dist/DomUtils/isDarkTheme.d.ts +1 -0
  7. package/dist/DomUtils/keyboardNavigation.d.ts +8 -0
  8. package/dist/DomUtils/scrollTo.d.ts +3 -1
  9. package/dist/DomUtils/selectors.d.ts +24 -11
  10. package/dist/EmojiPickerReact.d.ts +6 -0
  11. package/dist/components/ErrorBoundary.d.ts +15 -0
  12. package/dist/components/body/{RecentlyUsed.d.ts → Suggested.d.ts} +1 -1
  13. package/dist/components/context/ElementRefContext.d.ts +10 -13
  14. package/dist/components/context/PickerContext.d.ts +3 -3
  15. package/dist/components/emoji/Emoji.d.ts +11 -7
  16. package/dist/components/emoji/ExportedEmoji.d.ts +8 -0
  17. package/dist/components/footer/Preview.d.ts +1 -1
  18. package/dist/config/categoryConfig.d.ts +7 -3
  19. package/dist/config/config.d.ts +18 -15
  20. package/dist/config/useConfig.d.ts +11 -4
  21. package/dist/data/skinToneVariations.d.ts +2 -0
  22. package/dist/dataUtils/emojiSelectors.d.ts +3 -2
  23. package/dist/dataUtils/suggested.d.ts +11 -0
  24. package/dist/emoji-picker-react.cjs.development.css +9 -9
  25. package/dist/emoji-picker-react.cjs.development.js +3771 -3068
  26. package/dist/emoji-picker-react.cjs.development.js.map +1 -1
  27. package/dist/emoji-picker-react.cjs.production.min.js +1 -1
  28. package/dist/emoji-picker-react.cjs.production.min.js.map +1 -1
  29. package/dist/emoji-picker-react.esm.js +3781 -3078
  30. package/dist/emoji-picker-react.esm.js.map +1 -1
  31. package/dist/hooks/preloadEmoji.d.ts +1 -1
  32. package/dist/hooks/useActiveCategoryScrollDetection.d.ts +1 -2
  33. package/dist/hooks/useCloseAllOpenToggles.d.ts +2 -4
  34. package/dist/hooks/useDebouncedState.d.ts +1 -1
  35. package/dist/hooks/useDisallowMouseMove.d.ts +4 -0
  36. package/dist/hooks/useEmojiPreviewEvents.d.ts +3 -0
  37. package/dist/hooks/useFilter.d.ts +3 -3
  38. package/dist/hooks/useFocus.d.ts +3 -0
  39. package/dist/hooks/useInitialLoad.d.ts +1 -1
  40. package/dist/hooks/useIsEmojiHidden.d.ts +7 -1
  41. package/dist/hooks/useKeyboardNavigation.d.ts +1 -0
  42. package/dist/hooks/useMouseDownHandlers.d.ts +3 -2
  43. package/dist/hooks/useOnFocus.d.ts +1 -0
  44. package/dist/hooks/useScrollCategoryIntoView.d.ts +1 -0
  45. package/dist/hooks/useSetVariationPicker.d.ts +2 -0
  46. package/dist/index.d.ts +2 -2
  47. package/dist/types/exposedTypes.d.ts +5 -1
  48. package/package.json +13 -5
  49. package/src/DomUtils/classNames.ts +12 -1
  50. package/src/DomUtils/elementPositionInRow.ts +167 -0
  51. package/src/DomUtils/focusElement.ts +35 -0
  52. package/src/DomUtils/getActiveElement.ts +5 -0
  53. package/src/DomUtils/isDarkTheme.ts +7 -0
  54. package/src/DomUtils/keyboardNavigation.ts +161 -0
  55. package/src/DomUtils/scrollTo.ts +22 -1
  56. package/src/DomUtils/selectors.ts +200 -41
  57. package/src/EmojiPickerReact.css +99 -48
  58. package/src/EmojiPickerReact.tsx +26 -0
  59. package/src/components/ErrorBoundary.tsx +28 -0
  60. package/src/components/body/Body.tsx +3 -2
  61. package/src/components/body/EmojiCategory.css +14 -28
  62. package/src/components/body/EmojiCategory.tsx +5 -4
  63. package/src/components/body/EmojiList.tsx +13 -13
  64. package/src/components/body/EmojiVariationPicker.css +13 -5
  65. package/src/components/body/EmojiVariationPicker.tsx +25 -17
  66. package/src/components/body/Suggested.tsx +54 -0
  67. package/src/components/context/ElementRefContext.tsx +54 -19
  68. package/src/components/context/PickerContext.tsx +36 -33
  69. package/src/components/emoji/Emoji.css +42 -12
  70. package/src/components/emoji/Emoji.tsx +73 -34
  71. package/src/components/emoji/ExportedEmoji.tsx +30 -0
  72. package/src/components/footer/Preview.css +1 -1
  73. package/src/components/footer/Preview.tsx +16 -23
  74. package/src/components/header/Search.css +7 -7
  75. package/src/components/header/Search.tsx +31 -7
  76. package/src/components/header/SkinTonePicker.css +15 -8
  77. package/src/components/header/SkinTonePicker.tsx +23 -5
  78. package/src/components/main/PickerMain.css +0 -2
  79. package/src/components/main/PickerMain.tsx +14 -1
  80. package/src/components/navigation/CategoryNavigation.css +41 -10
  81. package/src/components/navigation/CategoryNavigation.tsx +22 -10
  82. package/src/components/navigation/svg/CategoryNav.svg +343 -379
  83. package/src/config/categoryConfig.ts +40 -14
  84. package/src/config/config.ts +59 -13
  85. package/src/config/useConfig.ts +35 -7
  86. package/src/data/emojis.json +1 -1
  87. package/src/data/skinToneVariations.ts +19 -0
  88. package/src/dataUtils/emojiSelectors.ts +24 -4
  89. package/src/dataUtils/{recentlyUsed.ts → suggested.ts} +20 -18
  90. package/src/hooks/preloadEmoji.ts +13 -2
  91. package/src/hooks/useActiveCategoryScrollDetection.ts +10 -8
  92. package/src/hooks/useCloseAllOpenToggles.ts +15 -4
  93. package/src/hooks/useDebouncedState.ts +10 -7
  94. package/src/hooks/useDisallowMouseMove.ts +47 -0
  95. package/src/hooks/useEmojiPreviewEvents.ts +103 -0
  96. package/src/hooks/useFilter.ts +46 -47
  97. package/src/hooks/useFocus.ts +40 -0
  98. package/src/hooks/useInitialLoad.ts +1 -0
  99. package/src/hooks/useIsEmojiHidden.ts +16 -3
  100. package/src/hooks/useKeyboardNavigation.ts +438 -0
  101. package/src/hooks/useMouseDownHandlers.ts +97 -89
  102. package/src/hooks/useOnFocus.ts +46 -0
  103. package/src/hooks/useOnScroll.ts +2 -2
  104. package/src/hooks/useScrollCategoryIntoView.ts +28 -0
  105. package/src/hooks/useSetVariationPicker.ts +28 -0
  106. package/src/index.tsx +9 -17
  107. package/src/types/exposedTypes.ts +6 -1
  108. package/dist/DomUtils/emojiElementRef.d.ts +0 -10
  109. package/dist/DomUtils/scrollCategoryIntoView.d.ts +0 -1
  110. package/dist/dataUtils/recentlyUsed.d.ts +0 -11
  111. package/dist/hooks/useEmojiElements.d.ts +0 -1
  112. package/dist/hooks/useEmojiMouseEnter.d.ts +0 -3
  113. package/src/DomUtils/emojiElementRef.ts +0 -42
  114. package/src/DomUtils/scrollCategoryIntoView.ts +0 -18
  115. package/src/components/body/RecentlyUsed.tsx +0 -50
  116. package/src/hooks/useEmojiElements.ts +0 -7
  117. package/src/hooks/useEmojiMouseEnter.ts +0 -59
@@ -9,4 +9,23 @@ const skinToneVariations = [
9
9
  SkinTones.DARK
10
10
  ];
11
11
 
12
+ export const skinTonesNamed = Object.entries(SkinTones).reduce(
13
+ (acc, [key, value]) => {
14
+ acc[value] = key;
15
+ return acc;
16
+ },
17
+ {} as Record<string, string>
18
+ );
19
+
20
+ export const skinTonesMapped: Record<
21
+ string,
22
+ string
23
+ > = skinToneVariations.reduce(
24
+ (mapped, skinTone) =>
25
+ Object.assign(mapped, {
26
+ [skinTone]: skinTone
27
+ }),
28
+ {}
29
+ );
30
+
12
31
  export default skinToneVariations;
@@ -1,10 +1,13 @@
1
- import { Categories } from './../config/categoryConfig';
1
+ import { Categories } from '../config/categoryConfig';
2
2
  import { cdnUrl } from '../config/cdnUrls';
3
3
  import emojis from '../data/emojis';
4
- import { DataEmoji, DataEmojis } from './DataTypes';
5
- import skinToneVariations from '../data/skinToneVariations';
4
+ import skinToneVariations, {
5
+ skinTonesMapped
6
+ } from '../data/skinToneVariations';
6
7
  import { EmojiStyle, SkinTones } from '../types/exposedTypes';
7
8
 
9
+ import { DataEmoji, DataEmojis } from './DataTypes';
10
+
8
11
  enum EmojiProperties {
9
12
  name = 'n',
10
13
  unified = 'u',
@@ -19,6 +22,17 @@ export function emojiName(emoji: DataEmoji): string {
19
22
  return emojiNames(emoji)[0];
20
23
  }
21
24
 
25
+ export function unifiedWithoutSkinTone(unified: string): string {
26
+ const splat = unified.split('-');
27
+ const [skinTone] = splat.splice(1, 1);
28
+
29
+ if (skinTonesMapped[skinTone]) {
30
+ return splat.join('-');
31
+ }
32
+
33
+ return unified;
34
+ }
35
+
22
36
  export function emojiUrl(
23
37
  emojiStyle: EmojiStyle,
24
38
  emoji: DataEmoji,
@@ -70,7 +84,13 @@ export function emojiByUnified(unified?: string): DataEmoji | undefined {
70
84
  if (!unified) {
71
85
  return;
72
86
  }
73
- return allEmojisByUnified[unified];
87
+
88
+ if (allEmojisByUnified[unified]) {
89
+ return allEmojisByUnified[unified];
90
+ }
91
+
92
+ const withoutSkinTone = unifiedWithoutSkinTone(unified);
93
+ return allEmojisByUnified[withoutSkinTone];
74
94
  }
75
95
 
76
96
  export const allEmojis: DataEmojis = Object.values(emojis).flat();
@@ -1,39 +1,46 @@
1
- import { SkinTones } from '../types/exposedTypes';
1
+ import { SkinTones, SuggestionMode } from '../types/exposedTypes';
2
+
2
3
  import { DataEmoji } from './DataTypes';
3
4
  import { emojiUnified } from './emojiSelectors';
4
5
 
5
- const RECENTLY_USED_LS_KEY = 'epr_recentlyUsed';
6
+ const SUGGESTED_LS_KEY = 'epr_suggested';
6
7
 
7
- type RecentlyUsedItem = {
8
+ type SuggestedItem = {
8
9
  unified: string;
9
10
  original: string;
10
11
  count: number;
11
12
  };
12
13
 
13
- type RecentlyUsed = RecentlyUsedItem[];
14
+ type Suggested = SuggestedItem[];
14
15
 
15
- export function getRecentlyUsed(): RecentlyUsed {
16
+ export function getsuggested(mode?: SuggestionMode): Suggested {
16
17
  if (!window?.localStorage) {
17
18
  return [];
18
19
  }
19
20
  try {
20
- return JSON.parse(
21
- window?.localStorage.getItem(RECENTLY_USED_LS_KEY) ?? '[]'
22
- );
21
+ const recent = JSON.parse(
22
+ window?.localStorage.getItem(SUGGESTED_LS_KEY) ?? '[]'
23
+ ) as Suggested;
24
+
25
+ if (mode === SuggestionMode.FREQUENT) {
26
+ return recent.sort((a, b) => b.count - a.count);
27
+ }
28
+
29
+ return recent;
23
30
  } catch {
24
31
  return [];
25
32
  }
26
33
  }
27
34
 
28
- export function setRecentlyUsed(emoji: DataEmoji, skinTone: SkinTones) {
29
- const recent = getRecentlyUsed();
35
+ export function setsuggested(emoji: DataEmoji, skinTone: SkinTones) {
36
+ const recent = getsuggested();
30
37
 
31
38
  const unified = emojiUnified(emoji, skinTone);
32
39
  const originalUnified = emojiUnified(emoji);
33
40
 
34
41
  let existing = recent.find(({ unified: u }) => u === unified);
35
42
 
36
- let nextList: RecentlyUsedItem[];
43
+ let nextList: SuggestedItem[];
37
44
 
38
45
  if (existing) {
39
46
  nextList = [existing].concat(recent.filter(i => i !== existing));
@@ -51,13 +58,8 @@ export function setRecentlyUsed(emoji: DataEmoji, skinTone: SkinTones) {
51
58
  nextList.length = Math.min(nextList.length, 14);
52
59
 
53
60
  try {
54
- setTimeout(() => {
55
- window?.localStorage.setItem(
56
- RECENTLY_USED_LS_KEY,
57
- JSON.stringify(nextList)
58
- );
59
- // Prevents the change from being seen immediately.
60
- }, 10);
61
+ window?.localStorage.setItem(SUGGESTED_LS_KEY, JSON.stringify(nextList));
62
+ // Prevents the change from being seen immediately.
61
63
  } catch {
62
64
  // ignore
63
65
  }
@@ -1,12 +1,23 @@
1
+ import { DataEmoji } from '../dataUtils/DataTypes';
1
2
  import {
2
3
  emojiUnified,
3
4
  emojiUrlByUnified,
4
5
  emojiVariations
5
6
  } from '../dataUtils/emojiSelectors';
6
- import { DataEmoji } from '../dataUtils/DataTypes';
7
7
  import { EmojiStyle } from '../types/exposedTypes';
8
8
 
9
- export function preloadEmoji(emoji: DataEmoji, emojiStyle: EmojiStyle): void {
9
+ export function preloadEmoji(
10
+ emoji: undefined | DataEmoji,
11
+ emojiStyle: EmojiStyle
12
+ ): void {
13
+ if (!emoji) {
14
+ return;
15
+ }
16
+
17
+ if (emojiStyle === EmojiStyle.NATIVE) {
18
+ return;
19
+ }
20
+
10
21
  const unified = emojiUnified(emoji);
11
22
 
12
23
  if (preloadedEmojs.has(unified)) {
@@ -1,18 +1,20 @@
1
1
  import { useEffect } from 'react';
2
2
 
3
3
  import { categoryNameFromDom } from '../DomUtils/categoryNameFromDom';
4
- import { ElementRef } from '../components/context/ElementRefContext';
5
- import { useActiveCategoryState } from '../components/context/PickerContext';
4
+ import { asSelectors, ClassNames } from '../DomUtils/classNames';
5
+ import { useBodyRef } from '../components/context/ElementRefContext';
6
6
 
7
- export function useActiveCategoryScrollDetection(bodyRef: ElementRef) {
8
- const [, setActiveCategory] = useActiveCategoryState();
7
+ export function useActiveCategoryScrollDetection(
8
+ setActiveCategory: (category: string) => void
9
+ ) {
10
+ const BodyRef = useBodyRef();
9
11
 
10
12
  useEffect(() => {
11
13
  const visibleCategories = new Map();
14
+ const bodyRef = BodyRef.current;
12
15
  const observer = new IntersectionObserver(
13
16
  entries => {
14
- const refCurrent = bodyRef.current;
15
- if (!refCurrent) {
17
+ if (!bodyRef) {
16
18
  return;
17
19
  }
18
20
 
@@ -39,8 +41,8 @@ export function useActiveCategoryScrollDetection(bodyRef: ElementRef) {
39
41
  threshold: [0, 1]
40
42
  }
41
43
  );
42
- bodyRef.current?.querySelectorAll('.epr-emoji-category').forEach(el => {
44
+ bodyRef?.querySelectorAll(asSelectors(ClassNames.category)).forEach(el => {
43
45
  observer.observe(el);
44
46
  });
45
- }, [bodyRef]);
47
+ }, [BodyRef, setActiveCategory]);
46
48
  }
@@ -17,10 +17,21 @@ export function useCloseAllOpenToggles() {
17
17
  if (skinToneFanOpen) {
18
18
  setSkinToneFanOpen(false);
19
19
  }
20
- }, [[variationPicker, skinToneFanOpen]]);
20
+ }, [
21
+ variationPicker,
22
+ skinToneFanOpen,
23
+ setVariationPicker,
24
+ setSkinToneFanOpen
25
+ ]);
21
26
 
22
- return {
23
- closeAllOpenToggles,
24
- dependencyArray: [variationPicker, skinToneFanOpen]
27
+ return closeAllOpenToggles;
28
+ }
29
+
30
+ export function useHasOpenToggles() {
31
+ const [variationPicker] = useEmojiVariationPickerState();
32
+ const [skinToneFanOpen] = useSkinToneFanOpenState();
33
+
34
+ return function hasOpenToggles() {
35
+ return !!variationPicker || skinToneFanOpen;
25
36
  };
26
37
  }
@@ -3,18 +3,21 @@ import { useRef, useState } from 'react';
3
3
  export function useDebouncedState<T>(
4
4
  initialValue: T,
5
5
  delay: number = 0
6
- ): [T, (value: T) => void] {
6
+ ): [T, (value: T) => Promise<T>] {
7
7
  const [state, setState] = useState<T>(initialValue);
8
8
  const timer = useRef<number | null>(null);
9
9
 
10
10
  function debouncedSetState(value: T) {
11
- if (timer.current) {
12
- clearTimeout(timer.current);
13
- }
11
+ return new Promise<T>(resolve => {
12
+ if (timer.current) {
13
+ clearTimeout(timer.current);
14
+ }
14
15
 
15
- timer.current = window?.setTimeout(() => {
16
- setState(value);
17
- }, delay);
16
+ timer.current = window?.setTimeout(() => {
17
+ setState(value);
18
+ resolve(value);
19
+ }, delay);
20
+ });
18
21
  }
19
22
 
20
23
  return [state, debouncedSetState];
@@ -0,0 +1,47 @@
1
+ import { useEffect } from 'react';
2
+
3
+ import { useBodyRef } from '../components/context/ElementRefContext';
4
+ import { useDisallowMouseRef } from '../components/context/PickerContext';
5
+
6
+ export function useDisallowMouseMove() {
7
+ const DisallowMouseRef = useDisallowMouseRef();
8
+ return function disallowMouseMove() {
9
+ DisallowMouseRef.current = true;
10
+ };
11
+ }
12
+
13
+ export function useAllowMouseMove() {
14
+ const DisallowMouseRef = useDisallowMouseRef();
15
+ return function allowMouseMove() {
16
+ DisallowMouseRef.current = false;
17
+ };
18
+ }
19
+
20
+ export function useIsMouseDisallowed() {
21
+ const DisallowMouseRef = useDisallowMouseRef();
22
+ return function isMouseDisallowed() {
23
+ return DisallowMouseRef.current;
24
+ };
25
+ }
26
+
27
+ export function useOnMouseMove() {
28
+ const BodyRef = useBodyRef();
29
+ const allowMouseMove = useAllowMouseMove();
30
+ const isMouseDisallowed = useIsMouseDisallowed();
31
+
32
+ useEffect(() => {
33
+ const bodyRef = BodyRef.current;
34
+ bodyRef?.addEventListener('mousemove', onMouseMove, {
35
+ passive: true
36
+ });
37
+
38
+ function onMouseMove() {
39
+ if (isMouseDisallowed()) {
40
+ allowMouseMove();
41
+ }
42
+ }
43
+ return () => {
44
+ bodyRef?.removeEventListener('mousemove', onMouseMove);
45
+ };
46
+ }, [BodyRef, allowMouseMove, isMouseDisallowed]);
47
+ }
@@ -0,0 +1,103 @@
1
+ import * as React from 'react';
2
+ import { useEffect } from 'react';
3
+
4
+ import { focusElement } from '../DomUtils/focusElement';
5
+ import {
6
+ buttonFromTarget,
7
+ originalUnifiedFromEmojiElement,
8
+ unifiedFromEmojiElement
9
+ } from '../DomUtils/selectors';
10
+ import { useBodyRef } from '../components/context/ElementRefContext';
11
+ import { PreviewEmoji } from '../components/footer/Preview';
12
+
13
+ import {
14
+ useAllowMouseMove,
15
+ useIsMouseDisallowed
16
+ } from './useDisallowMouseMove';
17
+
18
+ export function useEmojiPreviewEvents(
19
+ allow: boolean,
20
+ setPreviewEmoji: React.Dispatch<React.SetStateAction<PreviewEmoji>>
21
+ ) {
22
+ const BodyRef = useBodyRef();
23
+ const isMouseDisallowed = useIsMouseDisallowed();
24
+ const allowMouseMove = useAllowMouseMove();
25
+
26
+ useEffect(() => {
27
+ if (!allow) {
28
+ return;
29
+ }
30
+ const bodyRef = BodyRef.current;
31
+
32
+ bodyRef?.addEventListener('keydown', onEscape, {
33
+ passive: true
34
+ });
35
+
36
+ bodyRef?.addEventListener('mouseover', onMouseOver, true);
37
+
38
+ bodyRef?.addEventListener('focus', onEnter, true);
39
+
40
+ bodyRef?.addEventListener('mouseout', onLeave, {
41
+ passive: true
42
+ });
43
+ bodyRef?.addEventListener('blur', onLeave, true);
44
+
45
+
46
+ function onEnter(e: FocusEvent) {
47
+ const button = buttonFromTarget(e.target as HTMLElement);
48
+
49
+ if (!button) {
50
+ return onLeave();
51
+ }
52
+
53
+ const unified = unifiedFromEmojiElement(button);
54
+ const originalUnified = originalUnifiedFromEmojiElement(button);
55
+
56
+ if (!unified || !originalUnified) {
57
+ return onLeave();
58
+ }
59
+
60
+ setPreviewEmoji({
61
+ unified,
62
+ originalUnified
63
+ });
64
+ }
65
+ function onLeave(e?: FocusEvent | MouseEvent) {
66
+ if (e) {
67
+ const relatedTarget = e.relatedTarget as HTMLElement;
68
+
69
+ if (!buttonFromTarget(relatedTarget)) {
70
+ return setPreviewEmoji(null);
71
+ }
72
+ }
73
+
74
+ setPreviewEmoji(null);
75
+ }
76
+ function onEscape(e: KeyboardEvent) {
77
+ if (e.key === 'Escape') {
78
+ setPreviewEmoji(null);
79
+ }
80
+ }
81
+
82
+ function onMouseOver(e: MouseEvent) {
83
+ if (isMouseDisallowed()) {
84
+ return;
85
+ }
86
+
87
+ const button = buttonFromTarget(e.target as HTMLElement);
88
+
89
+ if (button) {
90
+ focusElement(button);
91
+ }
92
+ }
93
+
94
+
95
+ return () => {
96
+ bodyRef?.removeEventListener('mouseover', onMouseOver);
97
+ bodyRef?.removeEventListener('mouseout', onLeave);
98
+ bodyRef?.removeEventListener('focus', onEnter, true);
99
+ bodyRef?.removeEventListener('blur', onLeave, true);
100
+ bodyRef?.removeEventListener('keydown', onEscape);
101
+ };
102
+ }, [BodyRef, allow, setPreviewEmoji, isMouseDisallowed, allowMouseMove]);
103
+ }
@@ -1,5 +1,8 @@
1
1
  import { scrollTo } from '../DomUtils/scrollTo';
2
- import { usePickerMainRef } from '../components/context/ElementRefContext';
2
+ import {
3
+ usePickerMainRef,
4
+ useSearchInputRef
5
+ } from '../components/context/ElementRefContext';
3
6
  import {
4
7
  FilterState,
5
8
  useFilterRef,
@@ -7,14 +10,6 @@ import {
7
10
  } from '../components/context/PickerContext';
8
11
  import { DataEmoji } from '../dataUtils/DataTypes';
9
12
  import { emojiNames } from '../dataUtils/emojiSelectors';
10
- import {
11
- hideElement,
12
- hideEmoji,
13
- iterateEmojiRef,
14
- showElement
15
- } from '../DomUtils/emojiElementRef';
16
- import { useRef } from 'react';
17
- import { ClassNames } from '../DomUtils/classNames';
18
13
 
19
14
  function useSetFilterRef() {
20
15
  const filterRef = useFilterRef();
@@ -30,35 +25,52 @@ function useSetFilterRef() {
30
25
  };
31
26
  }
32
27
 
28
+ export function useClearSearch() {
29
+ const applySearch = useApplySearch();
30
+ const SearchInputRef = useSearchInputRef();
31
+
32
+ return function clearSearch() {
33
+ if (SearchInputRef.current) {
34
+ SearchInputRef.current.value = '';
35
+ }
36
+
37
+ applySearch('');
38
+ };
39
+ }
40
+
41
+ export function useAppendSearch() {
42
+ const SearchInputRef = useSearchInputRef();
43
+ const applySearch = useApplySearch();
44
+
45
+ return function appendSearch(str: string) {
46
+ if (SearchInputRef.current) {
47
+ SearchInputRef.current.value = SearchInputRef.current.value + str;
48
+ applySearch(SearchInputRef.current.value);
49
+ } else {
50
+ applySearch(str);
51
+ }
52
+ };
53
+ }
54
+
33
55
  export function useFilter() {
34
- const SearchInputRef = useRef<HTMLInputElement>(null);
56
+ const SearchInputRef = useSearchInputRef();
35
57
  const filterRef = useFilterRef();
36
58
  const setFilterRef = useSetFilterRef();
59
+ const applySearch = useApplySearch();
37
60
 
38
- const [searchTerm, setSearchTerm] = useSearchTermState();
39
- const PickerMainRef = usePickerMainRef();
61
+ const [searchTerm] = useSearchTermState();
40
62
 
41
63
  return {
42
64
  onChange,
43
65
  searchTerm,
44
- clearSearch,
45
66
  SearchInputRef
46
67
  };
47
68
 
48
- function clearSearch() {
49
- if (SearchInputRef.current) {
50
- SearchInputRef.current.value = '';
51
- }
52
- onChange('');
53
- }
54
-
55
69
  function onChange(nextValue: string) {
56
- scrollTo(PickerMainRef.current, 0);
57
-
58
70
  const filter = filterRef.current;
59
71
 
60
72
  if (filter?.[nextValue] || nextValue.length <= 1) {
61
- return applyFilterToDom(nextValue);
73
+ return applySearch(nextValue);
62
74
  }
63
75
 
64
76
  const longestMatch = findLongestMatch(nextValue, filter);
@@ -66,7 +78,7 @@ export function useFilter() {
66
78
  if (!longestMatch) {
67
79
  // Can we even get here?
68
80
  // If so, we need to search among all emojis
69
- return applyFilterToDom(nextValue);
81
+ return applySearch(nextValue);
70
82
  }
71
83
 
72
84
  setFilterRef(current =>
@@ -74,32 +86,21 @@ export function useFilter() {
74
86
  [nextValue]: filterEmojiObjectByKeyword(longestMatch, nextValue)
75
87
  })
76
88
  );
77
- applyFilterToDom(nextValue);
89
+ applySearch(nextValue);
78
90
  }
91
+ }
79
92
 
80
- // This function is the farthest from being "React" as it can be
81
- // It performs DOM manipulation and is not a pure function
82
- // But it is the most performant way to do it, at least 5 times faster than
83
- // react re-rendering. The reason I trust it to work correctly is because we
84
- // eventually do update the state, which keeps the DOM in sync - after the fact
85
- function applyFilterToDom(searchTerm: string): void {
86
- PickerMainRef.current?.classList.toggle(
87
- ClassNames.searchActive,
88
- !!searchTerm
89
- );
90
-
91
- iterateEmojiRef((element, unified) => {
92
- if (isEmojiFilteredBySearchTerm(unified, filterRef.current, searchTerm)) {
93
- hideElement(element);
94
- return;
95
- }
96
- showElement(element);
97
- });
93
+ function useApplySearch() {
94
+ const [, setSearchTerm] = useSearchTermState();
95
+ const PickerMainRef = usePickerMainRef();
98
96
 
97
+ return function applySearch(searchTerm: string) {
99
98
  requestAnimationFrame(() => {
100
- setSearchTerm(searchTerm);
99
+ setSearchTerm(searchTerm).then(() => {
100
+ scrollTo(PickerMainRef.current, 0);
101
+ });
101
102
  });
102
- }
103
+ };
103
104
  }
104
105
 
105
106
  function filterEmojiObjectByKeyword(
@@ -113,8 +114,6 @@ function filterEmojiObjectByKeyword(
113
114
 
114
115
  if (hasMatch(emoji, keyword)) {
115
116
  filtered[unified] = emoji;
116
- } else {
117
- hideEmoji(unified);
118
117
  }
119
118
  }
120
119
 
@@ -0,0 +1,40 @@
1
+ import { useCallback } from 'react';
2
+
3
+ import { focusElement, focusFirstElementChild } from '../DomUtils/focusElement';
4
+ import {
5
+ useCategoryNavigationRef,
6
+ useSearchInputRef,
7
+ useSkinTonePickerRef
8
+ } from '../components/context/ElementRefContext';
9
+
10
+ export function useFocusSearchInput() {
11
+ const SearchInputRef = useSearchInputRef();
12
+
13
+ return useCallback(() => {
14
+ focusElement(SearchInputRef.current);
15
+ }, [SearchInputRef]);
16
+ }
17
+
18
+ export function useFocusSkinTonePicker() {
19
+ const SkinTonePickerRef = useSkinTonePickerRef();
20
+
21
+ return useCallback(() => {
22
+ if (!SkinTonePickerRef.current) {
23
+ return;
24
+ }
25
+
26
+ focusFirstElementChild(SkinTonePickerRef.current);
27
+ }, [SkinTonePickerRef]);
28
+ }
29
+
30
+ export function useFocusCategoryNavigation() {
31
+ const CategoryNavigationRef = useCategoryNavigationRef();
32
+
33
+ return useCallback(() => {
34
+ if (!CategoryNavigationRef.current) {
35
+ return;
36
+ }
37
+
38
+ focusFirstElementChild(CategoryNavigationRef.current);
39
+ }, [CategoryNavigationRef]);
40
+ }
@@ -1,4 +1,5 @@
1
1
  import { useEffect } from 'react';
2
+ import * as React from 'react';
2
3
 
3
4
  export function useMarkInitialLoad(
4
5
  dispatch: React.Dispatch<React.SetStateAction<boolean>>
@@ -4,13 +4,26 @@ import { emojiUnified } from '../dataUtils/emojiSelectors';
4
4
 
5
5
  import { useIsEmojiFiltered } from './useFilter';
6
6
 
7
- export function useIsEmojiHidden(): (emoji: DataEmoji) => boolean {
7
+ export function useIsEmojiHidden(): (emoji: DataEmoji) => IsHiddenReturn {
8
8
  const [emojisThatFailedToLoad] = useEmojisThatFailedToLoadState();
9
9
  const isEmojiFiltered = useIsEmojiFiltered();
10
10
 
11
- return (emoji: DataEmoji): boolean => {
11
+ return (emoji: DataEmoji): IsHiddenReturn => {
12
12
  const unified = emojiUnified(emoji);
13
13
 
14
- return emojisThatFailedToLoad.has(unified) || isEmojiFiltered(unified);
14
+ const failedToLoad = emojisThatFailedToLoad.has(unified);
15
+ const filteredOut = isEmojiFiltered(unified);
16
+
17
+ return {
18
+ failedToLoad,
19
+ filteredOut,
20
+ hidden: failedToLoad || filteredOut
21
+ };
15
22
  };
16
23
  }
24
+
25
+ type IsHiddenReturn = {
26
+ failedToLoad: boolean;
27
+ filteredOut: boolean;
28
+ hidden: boolean;
29
+ };