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.
- package/README.md +176 -43
- package/dist/DomUtils/classNames.d.ts +9 -1
- package/dist/DomUtils/elementPositionInRow.d.ts +10 -0
- package/dist/DomUtils/focusElement.d.ts +5 -0
- package/dist/DomUtils/getActiveElement.d.ts +2 -0
- package/dist/DomUtils/isDarkTheme.d.ts +1 -0
- package/dist/DomUtils/keyboardNavigation.d.ts +8 -0
- package/dist/DomUtils/scrollTo.d.ts +3 -1
- package/dist/DomUtils/selectors.d.ts +24 -11
- package/dist/EmojiPickerReact.d.ts +6 -0
- package/dist/components/ErrorBoundary.d.ts +15 -0
- package/dist/components/body/{RecentlyUsed.d.ts → Suggested.d.ts} +1 -1
- package/dist/components/context/ElementRefContext.d.ts +10 -13
- package/dist/components/context/PickerContext.d.ts +3 -3
- package/dist/components/emoji/Emoji.d.ts +11 -7
- package/dist/components/emoji/ExportedEmoji.d.ts +8 -0
- package/dist/components/footer/Preview.d.ts +1 -1
- package/dist/config/categoryConfig.d.ts +7 -3
- package/dist/config/config.d.ts +18 -15
- package/dist/config/useConfig.d.ts +11 -4
- package/dist/data/skinToneVariations.d.ts +2 -0
- package/dist/dataUtils/emojiSelectors.d.ts +3 -2
- package/dist/dataUtils/suggested.d.ts +11 -0
- package/dist/emoji-picker-react.cjs.development.css +9 -9
- package/dist/emoji-picker-react.cjs.development.js +3771 -3068
- package/dist/emoji-picker-react.cjs.development.js.map +1 -1
- package/dist/emoji-picker-react.cjs.production.min.js +1 -1
- package/dist/emoji-picker-react.cjs.production.min.js.map +1 -1
- package/dist/emoji-picker-react.esm.js +3781 -3078
- package/dist/emoji-picker-react.esm.js.map +1 -1
- package/dist/hooks/preloadEmoji.d.ts +1 -1
- package/dist/hooks/useActiveCategoryScrollDetection.d.ts +1 -2
- package/dist/hooks/useCloseAllOpenToggles.d.ts +2 -4
- package/dist/hooks/useDebouncedState.d.ts +1 -1
- package/dist/hooks/useDisallowMouseMove.d.ts +4 -0
- package/dist/hooks/useEmojiPreviewEvents.d.ts +3 -0
- package/dist/hooks/useFilter.d.ts +3 -3
- package/dist/hooks/useFocus.d.ts +3 -0
- package/dist/hooks/useInitialLoad.d.ts +1 -1
- package/dist/hooks/useIsEmojiHidden.d.ts +7 -1
- package/dist/hooks/useKeyboardNavigation.d.ts +1 -0
- package/dist/hooks/useMouseDownHandlers.d.ts +3 -2
- package/dist/hooks/useOnFocus.d.ts +1 -0
- package/dist/hooks/useScrollCategoryIntoView.d.ts +1 -0
- package/dist/hooks/useSetVariationPicker.d.ts +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/types/exposedTypes.d.ts +5 -1
- package/package.json +13 -5
- package/src/DomUtils/classNames.ts +12 -1
- package/src/DomUtils/elementPositionInRow.ts +167 -0
- package/src/DomUtils/focusElement.ts +35 -0
- package/src/DomUtils/getActiveElement.ts +5 -0
- package/src/DomUtils/isDarkTheme.ts +7 -0
- package/src/DomUtils/keyboardNavigation.ts +161 -0
- package/src/DomUtils/scrollTo.ts +22 -1
- package/src/DomUtils/selectors.ts +200 -41
- package/src/EmojiPickerReact.css +99 -48
- package/src/EmojiPickerReact.tsx +26 -0
- package/src/components/ErrorBoundary.tsx +28 -0
- package/src/components/body/Body.tsx +3 -2
- package/src/components/body/EmojiCategory.css +14 -28
- package/src/components/body/EmojiCategory.tsx +5 -4
- package/src/components/body/EmojiList.tsx +13 -13
- package/src/components/body/EmojiVariationPicker.css +13 -5
- package/src/components/body/EmojiVariationPicker.tsx +25 -17
- package/src/components/body/Suggested.tsx +54 -0
- package/src/components/context/ElementRefContext.tsx +54 -19
- package/src/components/context/PickerContext.tsx +36 -33
- package/src/components/emoji/Emoji.css +42 -12
- package/src/components/emoji/Emoji.tsx +73 -34
- package/src/components/emoji/ExportedEmoji.tsx +30 -0
- package/src/components/footer/Preview.css +1 -1
- package/src/components/footer/Preview.tsx +16 -23
- package/src/components/header/Search.css +7 -7
- package/src/components/header/Search.tsx +31 -7
- package/src/components/header/SkinTonePicker.css +15 -8
- package/src/components/header/SkinTonePicker.tsx +23 -5
- package/src/components/main/PickerMain.css +0 -2
- package/src/components/main/PickerMain.tsx +14 -1
- package/src/components/navigation/CategoryNavigation.css +41 -10
- package/src/components/navigation/CategoryNavigation.tsx +22 -10
- package/src/components/navigation/svg/CategoryNav.svg +343 -379
- package/src/config/categoryConfig.ts +40 -14
- package/src/config/config.ts +59 -13
- package/src/config/useConfig.ts +35 -7
- package/src/data/emojis.json +1 -1
- package/src/data/skinToneVariations.ts +19 -0
- package/src/dataUtils/emojiSelectors.ts +24 -4
- package/src/dataUtils/{recentlyUsed.ts → suggested.ts} +20 -18
- package/src/hooks/preloadEmoji.ts +13 -2
- package/src/hooks/useActiveCategoryScrollDetection.ts +10 -8
- package/src/hooks/useCloseAllOpenToggles.ts +15 -4
- package/src/hooks/useDebouncedState.ts +10 -7
- package/src/hooks/useDisallowMouseMove.ts +47 -0
- package/src/hooks/useEmojiPreviewEvents.ts +103 -0
- package/src/hooks/useFilter.ts +46 -47
- package/src/hooks/useFocus.ts +40 -0
- package/src/hooks/useInitialLoad.ts +1 -0
- package/src/hooks/useIsEmojiHidden.ts +16 -3
- package/src/hooks/useKeyboardNavigation.ts +438 -0
- package/src/hooks/useMouseDownHandlers.ts +97 -89
- package/src/hooks/useOnFocus.ts +46 -0
- package/src/hooks/useOnScroll.ts +2 -2
- package/src/hooks/useScrollCategoryIntoView.ts +28 -0
- package/src/hooks/useSetVariationPicker.ts +28 -0
- package/src/index.tsx +9 -17
- package/src/types/exposedTypes.ts +6 -1
- package/dist/DomUtils/emojiElementRef.d.ts +0 -10
- package/dist/DomUtils/scrollCategoryIntoView.d.ts +0 -1
- package/dist/dataUtils/recentlyUsed.d.ts +0 -11
- package/dist/hooks/useEmojiElements.d.ts +0 -1
- package/dist/hooks/useEmojiMouseEnter.d.ts +0 -3
- package/src/DomUtils/emojiElementRef.ts +0 -42
- package/src/DomUtils/scrollCategoryIntoView.ts +0 -18
- package/src/components/body/RecentlyUsed.tsx +0 -50
- package/src/hooks/useEmojiElements.ts +0 -7
- 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 '
|
|
1
|
+
import { Categories } from '../config/categoryConfig';
|
|
2
2
|
import { cdnUrl } from '../config/cdnUrls';
|
|
3
3
|
import emojis from '../data/emojis';
|
|
4
|
-
import
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
6
|
+
const SUGGESTED_LS_KEY = 'epr_suggested';
|
|
6
7
|
|
|
7
|
-
type
|
|
8
|
+
type SuggestedItem = {
|
|
8
9
|
unified: string;
|
|
9
10
|
original: string;
|
|
10
11
|
count: number;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
type
|
|
14
|
+
type Suggested = SuggestedItem[];
|
|
14
15
|
|
|
15
|
-
export function
|
|
16
|
+
export function getsuggested(mode?: SuggestionMode): Suggested {
|
|
16
17
|
if (!window?.localStorage) {
|
|
17
18
|
return [];
|
|
18
19
|
}
|
|
19
20
|
try {
|
|
20
|
-
|
|
21
|
-
window?.localStorage.getItem(
|
|
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
|
|
29
|
-
const recent =
|
|
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:
|
|
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
|
-
|
|
55
|
-
|
|
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(
|
|
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 {
|
|
5
|
-
import {
|
|
4
|
+
import { asSelectors, ClassNames } from '../DomUtils/classNames';
|
|
5
|
+
import { useBodyRef } from '../components/context/ElementRefContext';
|
|
6
6
|
|
|
7
|
-
export function useActiveCategoryScrollDetection(
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
44
|
+
bodyRef?.querySelectorAll(asSelectors(ClassNames.category)).forEach(el => {
|
|
43
45
|
observer.observe(el);
|
|
44
46
|
});
|
|
45
|
-
}, [
|
|
47
|
+
}, [BodyRef, setActiveCategory]);
|
|
46
48
|
}
|
|
@@ -17,10 +17,21 @@ export function useCloseAllOpenToggles() {
|
|
|
17
17
|
if (skinToneFanOpen) {
|
|
18
18
|
setSkinToneFanOpen(false);
|
|
19
19
|
}
|
|
20
|
-
}, [
|
|
20
|
+
}, [
|
|
21
|
+
variationPicker,
|
|
22
|
+
skinToneFanOpen,
|
|
23
|
+
setVariationPicker,
|
|
24
|
+
setSkinToneFanOpen
|
|
25
|
+
]);
|
|
21
26
|
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
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) =>
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
return new Promise<T>(resolve => {
|
|
12
|
+
if (timer.current) {
|
|
13
|
+
clearTimeout(timer.current);
|
|
14
|
+
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
}
|
package/src/hooks/useFilter.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { scrollTo } from '../DomUtils/scrollTo';
|
|
2
|
-
import {
|
|
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 =
|
|
56
|
+
const SearchInputRef = useSearchInputRef();
|
|
35
57
|
const filterRef = useFilterRef();
|
|
36
58
|
const setFilterRef = useSetFilterRef();
|
|
59
|
+
const applySearch = useApplySearch();
|
|
37
60
|
|
|
38
|
-
const [searchTerm
|
|
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
|
|
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
|
|
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
|
-
|
|
89
|
+
applySearch(nextValue);
|
|
78
90
|
}
|
|
91
|
+
}
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
}
|
|
@@ -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) =>
|
|
7
|
+
export function useIsEmojiHidden(): (emoji: DataEmoji) => IsHiddenReturn {
|
|
8
8
|
const [emojisThatFailedToLoad] = useEmojisThatFailedToLoadState();
|
|
9
9
|
const isEmojiFiltered = useIsEmojiFiltered();
|
|
10
10
|
|
|
11
|
-
return (emoji: DataEmoji):
|
|
11
|
+
return (emoji: DataEmoji): IsHiddenReturn => {
|
|
12
12
|
const unified = emojiUnified(emoji);
|
|
13
13
|
|
|
14
|
-
|
|
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
|
+
};
|