emoji-picker-react 4.13.3 → 4.14.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/dist/DomUtils/elementPositionInRow.d.ts +1 -6
- package/dist/components/body/EmojiCategory.d.ts +2 -1
- package/dist/components/body/EmojiList.d.ts +3 -1
- package/dist/components/context/ElementRefContext.d.ts +1 -0
- package/dist/components/emoji/ClickableEmojiButton.d.ts +2 -1
- package/dist/components/emoji/Emoji.d.ts +3 -2
- package/dist/dataUtils/emojiSelectors.d.ts +1 -1
- package/dist/emoji-picker-react.cjs.development.js +615 -544
- 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 +616 -545
- package/dist/emoji-picker-react.esm.js.map +1 -1
- package/dist/hooks/preloadEmoji.d.ts +4 -0
- package/dist/hooks/useCategoryHeight.d.ts +5 -0
- package/dist/hooks/useEmojiVirtualization.d.ts +16 -0
- package/dist/hooks/useOnScroll.d.ts +1 -1
- package/dist/virtualization/virtualizationHelpers.d.ts +18 -0
- package/package.json +2 -1
- package/src/DomUtils/elementPositionInRow.ts +8 -131
- package/src/DomUtils/keyboardNavigation.ts +62 -48
- package/src/components/body/Body.tsx +2 -2
- package/src/components/body/EmojiCategory.tsx +7 -5
- package/src/components/body/EmojiList.tsx +43 -78
- package/src/components/context/ElementRefContext.tsx +8 -0
- package/src/components/emoji/ClickableEmojiButton.tsx +4 -1
- package/src/components/emoji/Emoji.tsx +4 -1
- package/src/components/header/Search/Search.tsx +0 -5
- package/src/components/main/PickerMain.tsx +8 -5
- package/src/dataUtils/emojiSelectors.ts +29 -3
- package/src/hooks/preloadEmoji.ts +49 -7
- package/src/hooks/useCategoryHeight.ts +92 -0
- package/src/hooks/useEmojiVirtualization.tsx +112 -0
- package/src/hooks/useFilter.ts +17 -24
- package/src/hooks/useOnScroll.ts +5 -1
- package/src/virtualization/virtualizationHelpers.ts +45 -0
- package/dist/components/body/Suggested.d.ts +0 -7
- package/dist/components/header/Search/CssSearch.d.ts +0 -4
- package/dist/globals/doesBrowserSupportCssHas.d.ts +0 -2
- package/src/components/body/Suggested.tsx +0 -69
- package/src/components/header/Search/CssSearch.tsx +0 -45
- package/src/globals/doesBrowserSupportCssHas.ts +0 -16
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { cx } from 'flairup';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { useState } from 'react';
|
|
4
3
|
|
|
5
4
|
import { darkMode, stylesheet } from '../../../Stylesheet/stylesheet';
|
|
6
5
|
import {
|
|
@@ -17,7 +16,6 @@ import { useSearchInputRef } from '../../context/ElementRefContext';
|
|
|
17
16
|
import { SkinTonePicker } from '../SkinTonePicker/SkinTonePicker';
|
|
18
17
|
|
|
19
18
|
import { BtnClearSearch } from './BtnClearSearch';
|
|
20
|
-
import { CssSearch } from './CssSearch';
|
|
21
19
|
import { IcnSearch } from './IcnSearch';
|
|
22
20
|
import SVGTimes from './svg/times.svg';
|
|
23
21
|
|
|
@@ -40,7 +38,6 @@ export function SearchContainer() {
|
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
export function Search() {
|
|
43
|
-
const [inc, setInc] = useState(0);
|
|
44
41
|
const closeAllOpenToggles = useCloseAllOpenToggles();
|
|
45
42
|
const SearchInputRef = useSearchInputRef();
|
|
46
43
|
const placeholder = useSearchPlaceHolderConfig();
|
|
@@ -52,7 +49,6 @@ export function Search() {
|
|
|
52
49
|
|
|
53
50
|
return (
|
|
54
51
|
<Relative className={cx(styles.searchContainer)}>
|
|
55
|
-
<CssSearch value={value} />
|
|
56
52
|
<input
|
|
57
53
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
58
54
|
autoFocus={autoFocus}
|
|
@@ -63,7 +59,6 @@ export function Search() {
|
|
|
63
59
|
aria-controls="epr-search-id"
|
|
64
60
|
placeholder={placeholder}
|
|
65
61
|
onChange={event => {
|
|
66
|
-
setInc(inc + 1);
|
|
67
62
|
onChange(event?.target?.value ?? value);
|
|
68
63
|
}}
|
|
69
64
|
ref={SearchInputRef}
|
|
@@ -97,8 +97,10 @@ const DarkTheme = {
|
|
|
97
97
|
'var(--epr-dark-category-icon-active-color)',
|
|
98
98
|
'--epr-skin-tone-picker-menu-color':
|
|
99
99
|
'var(--epr-dark-skin-tone-picker-menu-color)',
|
|
100
|
-
'--epr-skin-tone-outer-border-color':
|
|
101
|
-
|
|
100
|
+
'--epr-skin-tone-outer-border-color':
|
|
101
|
+
'var(--epr-dark-skin-tone-outer-border-color)',
|
|
102
|
+
'--epr-skin-tone-inner-border-color':
|
|
103
|
+
'var(--epr-dark-skin-tone-inner-border-color)'
|
|
102
104
|
};
|
|
103
105
|
|
|
104
106
|
const styles = stylesheet.create({
|
|
@@ -113,7 +115,7 @@ const styles = stylesheet.create({
|
|
|
113
115
|
borderColor: 'var(--epr-picker-border-color)',
|
|
114
116
|
backgroundColor: 'var(--epr-bg-color)',
|
|
115
117
|
overflow: 'hidden',
|
|
116
|
-
transition: '
|
|
118
|
+
transition: 'height 0.3s ease-in-out, background-color 0.1s ease-in-out',
|
|
117
119
|
'*': {
|
|
118
120
|
boxSizing: 'border-box',
|
|
119
121
|
fontFamily: 'sans-serif'
|
|
@@ -213,8 +215,9 @@ const styles = stylesheet.create({
|
|
|
213
215
|
'--epr-dark-emoji-variation-indicator-color': '#444',
|
|
214
216
|
'--epr-dark-category-icon-active-color': '#3271b7',
|
|
215
217
|
'--epr-dark-skin-tone-picker-menu-color': '#22222295',
|
|
216
|
-
'--epr-dark-skin-tone-outer-border-color':
|
|
217
|
-
|
|
218
|
+
'--epr-dark-skin-tone-outer-border-color':
|
|
219
|
+
'var(--epr-dark-picker-border-color)',
|
|
220
|
+
'--epr-dark-skin-tone-inner-border-color': '#00000000'
|
|
218
221
|
}
|
|
219
222
|
},
|
|
220
223
|
autoThemeDark: {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { useUpdateSuggested } from '../components/context/PickerContext';
|
|
1
4
|
import { Categories } from '../config/categoryConfig';
|
|
2
5
|
import { cdnUrl } from '../config/cdnUrls';
|
|
3
6
|
import { CustomEmoji } from '../config/customEmojiConfig';
|
|
7
|
+
import { useSuggestedEmojisModeConfig } from '../config/useConfig';
|
|
4
8
|
import emojis from '../data/emojis';
|
|
5
9
|
import skinToneVariations, {
|
|
6
10
|
skinTonesMapped
|
|
@@ -9,6 +13,7 @@ import { EmojiStyle, SkinTones } from '../types/exposedTypes';
|
|
|
9
13
|
|
|
10
14
|
import { DataEmoji, DataEmojis, EmojiProperties, WithName } from './DataTypes';
|
|
11
15
|
import { indexEmoji } from './alphaNumericEmojiIndex';
|
|
16
|
+
import { getSuggested } from './suggested';
|
|
12
17
|
|
|
13
18
|
export function emojiNames(emoji: WithName): string[] {
|
|
14
19
|
return emoji[EmojiProperties.name] ?? [];
|
|
@@ -47,9 +52,30 @@ export function emojiUnified(emoji: DataEmoji, skinTone?: string): string {
|
|
|
47
52
|
return emojiVariationUnified(emoji, skinTone) ?? unified;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
export function
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
export function useGetEmojisByCategory(): (Category: Categories) => DataEmojis {
|
|
56
|
+
const suggestedEmojisModeConfig = useSuggestedEmojisModeConfig();
|
|
57
|
+
const [suggestedUpdated] = useUpdateSuggested();
|
|
58
|
+
|
|
59
|
+
const suggested = React.useMemo(
|
|
60
|
+
() => {
|
|
61
|
+
const suggested = getSuggested(suggestedEmojisModeConfig) ?? [];
|
|
62
|
+
|
|
63
|
+
return suggested
|
|
64
|
+
.map(s => emojiByUnified(s.unified))
|
|
65
|
+
.filter(Boolean) as DataEmojis;
|
|
66
|
+
},
|
|
67
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
+
[suggestedUpdated, suggestedEmojisModeConfig]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return function getEmojisByCategory(category: Categories): DataEmojis {
|
|
72
|
+
if (category === Categories.SUGGESTED) {
|
|
73
|
+
return suggested;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// @ts-ignore
|
|
77
|
+
return emojis[category] ?? [];
|
|
78
|
+
};
|
|
53
79
|
}
|
|
54
80
|
|
|
55
81
|
// WARNING: DO NOT USE DIRECTLY
|
|
@@ -2,17 +2,24 @@ import { GetEmojiUrl } from '../components/emoji/BaseEmojiProps';
|
|
|
2
2
|
import { DataEmoji } from '../dataUtils/DataTypes';
|
|
3
3
|
import { emojiUnified, emojiVariations } from '../dataUtils/emojiSelectors';
|
|
4
4
|
import { EmojiStyle } from '../types/exposedTypes';
|
|
5
|
+
import { Dimensions } from '../virtualization/virtualizationHelpers';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
// eslint-disable-next-line max-params
|
|
8
|
+
export function preloaEmojidIfNeeded(
|
|
8
9
|
emoji: undefined | DataEmoji,
|
|
9
|
-
|
|
10
|
+
emojiStyles: EmojiStyle,
|
|
11
|
+
scrollTop: number,
|
|
12
|
+
clientHeight: number,
|
|
13
|
+
topOffset: number,
|
|
14
|
+
style: { top: number } | undefined,
|
|
15
|
+
dimensions: Dimensions,
|
|
16
|
+
getEmojiUrl: GetEmojiUrl
|
|
10
17
|
): void {
|
|
11
18
|
if (!emoji) {
|
|
12
19
|
return;
|
|
13
20
|
}
|
|
14
21
|
|
|
15
|
-
if (
|
|
22
|
+
if (emojiStyles === EmojiStyle.NATIVE) {
|
|
16
23
|
return;
|
|
17
24
|
}
|
|
18
25
|
|
|
@@ -22,12 +29,47 @@ export function preloadEmoji(
|
|
|
22
29
|
return;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
// preload only if the emoji is a few rows below the viewport
|
|
33
|
+
if (!style || !dimensions) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
const emojiTop = topOffset + style.top;
|
|
39
|
+
const viewportBottom = scrollTop + clientHeight;
|
|
40
|
+
|
|
41
|
+
const isJustBelowViewport =
|
|
42
|
+
emojiTop >= viewportBottom &&
|
|
43
|
+
emojiTop < viewportBottom + dimensions.emojiSize * 2;
|
|
44
|
+
|
|
45
|
+
if (isJustBelowViewport) {
|
|
46
|
+
preloadEmoji(getEmojiUrl, emoji, emojiStyles);
|
|
47
|
+
}
|
|
28
48
|
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function preloadEmoji(
|
|
52
|
+
getEmojiUrl: GetEmojiUrl,
|
|
53
|
+
emoji: undefined | DataEmoji,
|
|
54
|
+
emojiStyle: EmojiStyle
|
|
55
|
+
): void {
|
|
56
|
+
if (!emoji) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
29
59
|
|
|
60
|
+
const unified = emojiUnified(emoji);
|
|
61
|
+
|
|
62
|
+
if (preloadedEmojs.has(unified)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
30
65
|
preloadedEmojs.add(unified);
|
|
66
|
+
|
|
67
|
+
emojiVariations(emoji)
|
|
68
|
+
.concat(unified)
|
|
69
|
+
.forEach(variation => {
|
|
70
|
+
const emojiUrl = getEmojiUrl(variation, emojiStyle);
|
|
71
|
+
preloadImage(emojiUrl);
|
|
72
|
+
});
|
|
31
73
|
}
|
|
32
74
|
|
|
33
75
|
export const preloadedEmojs: Set<string> = new Set();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { EmojiButtonSelector } from '../DomUtils/selectors';
|
|
4
|
+
import {
|
|
5
|
+
useEmojiListRef,
|
|
6
|
+
usePickerMainRef
|
|
7
|
+
} from '../components/context/ElementRefContext';
|
|
8
|
+
import { useReactionsModeState } from '../components/context/PickerContext';
|
|
9
|
+
|
|
10
|
+
const EMOJI_SIZE_DEFAULT = 32;
|
|
11
|
+
|
|
12
|
+
export function useCategoryHeight(
|
|
13
|
+
emojiCount: number
|
|
14
|
+
):
|
|
15
|
+
| {
|
|
16
|
+
categoryHeight: number;
|
|
17
|
+
emojisPerRow: number;
|
|
18
|
+
emojiSize: number;
|
|
19
|
+
}
|
|
20
|
+
| undefined {
|
|
21
|
+
const EmojiListRef = useEmojiListRef();
|
|
22
|
+
const [isReactionsMode] = useReactionsModeState();
|
|
23
|
+
const PickerMainRef = usePickerMainRef();
|
|
24
|
+
const emojiSizeRef = React.useRef<number | undefined>();
|
|
25
|
+
const [dimensions, setDimensions] = React.useState<{
|
|
26
|
+
categoryHeight: number;
|
|
27
|
+
emojisPerRow: number;
|
|
28
|
+
emojiSize: number;
|
|
29
|
+
}>();
|
|
30
|
+
|
|
31
|
+
// Helper to compute and store dimensions based on current DOM
|
|
32
|
+
const computeAndSetDimensions = React.useCallback(() => {
|
|
33
|
+
const listEl = EmojiListRef.current;
|
|
34
|
+
if (!listEl) return;
|
|
35
|
+
|
|
36
|
+
const emojiElement = listEl.querySelector(
|
|
37
|
+
EmojiButtonSelector
|
|
38
|
+
) as HTMLElement | null;
|
|
39
|
+
|
|
40
|
+
const measured = emojiElement?.clientHeight;
|
|
41
|
+
const emojiSize = measured ?? emojiSizeRef.current ?? EMOJI_SIZE_DEFAULT;
|
|
42
|
+
emojiSizeRef.current = emojiSize;
|
|
43
|
+
const pickerWidth = listEl.clientWidth;
|
|
44
|
+
|
|
45
|
+
if (pickerWidth === 0 || emojiSize === 0) return;
|
|
46
|
+
|
|
47
|
+
const emojisPerRow = Math.max(1, Math.floor(pickerWidth / emojiSize));
|
|
48
|
+
const rowCount = Math.ceil(emojiCount / emojisPerRow);
|
|
49
|
+
const categoryHeight = rowCount * emojiSize;
|
|
50
|
+
|
|
51
|
+
setDimensions({ categoryHeight, emojisPerRow, emojiSize });
|
|
52
|
+
}, [EmojiListRef, emojiCount]);
|
|
53
|
+
|
|
54
|
+
// Recompute on data-count changes and when reactions mode toggles
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
computeAndSetDimensions();
|
|
57
|
+
}, [emojiCount, isReactionsMode, computeAndSetDimensions]);
|
|
58
|
+
|
|
59
|
+
// Listen to transitionend on the picker root (where height transition occurs)
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
const rootEl = PickerMainRef.current;
|
|
62
|
+
if (!rootEl) return;
|
|
63
|
+
|
|
64
|
+
const handler = (e: Event) => {
|
|
65
|
+
const te = e as TransitionEvent;
|
|
66
|
+
const prop = te.propertyName;
|
|
67
|
+
if (
|
|
68
|
+
prop === 'width' ||
|
|
69
|
+
prop === 'max-width' ||
|
|
70
|
+
prop === 'min-width' ||
|
|
71
|
+
prop === 'height' ||
|
|
72
|
+
prop === 'max-height' ||
|
|
73
|
+
prop === 'min-height'
|
|
74
|
+
) {
|
|
75
|
+
if (typeof queueMicrotask === 'function') {
|
|
76
|
+
queueMicrotask(() => computeAndSetDimensions());
|
|
77
|
+
} else {
|
|
78
|
+
requestAnimationFrame(() => computeAndSetDimensions());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
rootEl.addEventListener('transitionend', handler, {
|
|
84
|
+
passive: true
|
|
85
|
+
});
|
|
86
|
+
return () => {
|
|
87
|
+
rootEl.removeEventListener('transitionend', handler);
|
|
88
|
+
};
|
|
89
|
+
}, [PickerMainRef, computeAndSetDimensions]);
|
|
90
|
+
|
|
91
|
+
return dimensions;
|
|
92
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ReactNode, useEffect } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
import { useBodyRef } from '../components/context/ElementRefContext';
|
|
5
|
+
import { useActiveSkinToneState } from '../components/context/PickerContext';
|
|
6
|
+
import { ClickableEmoji } from '../components/emoji/Emoji';
|
|
7
|
+
import {
|
|
8
|
+
useEmojiStyleConfig,
|
|
9
|
+
useGetEmojiUrlConfig,
|
|
10
|
+
useLazyLoadEmojisConfig,
|
|
11
|
+
useSkinTonesDisabledConfig
|
|
12
|
+
} from '../config/useConfig';
|
|
13
|
+
import { DataEmojis } from '../dataUtils/DataTypes';
|
|
14
|
+
import { emojiUnified } from '../dataUtils/emojiSelectors';
|
|
15
|
+
import {
|
|
16
|
+
getEmojiPositionStyle,
|
|
17
|
+
shouldVirtualize
|
|
18
|
+
} from '../virtualization/virtualizationHelpers';
|
|
19
|
+
|
|
20
|
+
import { preloaEmojidIfNeeded } from './preloadEmoji';
|
|
21
|
+
import { useCategoryHeight } from './useCategoryHeight';
|
|
22
|
+
import { useIsEmojiDisallowed } from './useDisallowedEmojis';
|
|
23
|
+
import { useIsEmojiHidden } from './useIsEmojiHidden';
|
|
24
|
+
|
|
25
|
+
export function useEmojiVirtualization({
|
|
26
|
+
categoryEmojis,
|
|
27
|
+
topOffset,
|
|
28
|
+
onHeightReady,
|
|
29
|
+
scrollTop
|
|
30
|
+
}: {
|
|
31
|
+
categoryEmojis: DataEmojis;
|
|
32
|
+
topOffset: number;
|
|
33
|
+
onHeightReady: (height: number) => void;
|
|
34
|
+
scrollTop: number;
|
|
35
|
+
}) {
|
|
36
|
+
const isEmojiHidden = useIsEmojiHidden();
|
|
37
|
+
const lazyLoadEmojis = useLazyLoadEmojisConfig();
|
|
38
|
+
const emojiStyle = useEmojiStyleConfig();
|
|
39
|
+
const [activeSkinTone] = useActiveSkinToneState();
|
|
40
|
+
const isEmojiDisallowed = useIsEmojiDisallowed();
|
|
41
|
+
const getEmojiUrl = useGetEmojiUrlConfig();
|
|
42
|
+
const showVariations = !useSkinTonesDisabledConfig();
|
|
43
|
+
const BodyRef = useBodyRef();
|
|
44
|
+
|
|
45
|
+
let virtualizedCounter = 0;
|
|
46
|
+
|
|
47
|
+
const emojisToPush = categoryEmojis.filter(emoji => {
|
|
48
|
+
const isDisallowed = isEmojiDisallowed(emoji);
|
|
49
|
+
const { failedToLoad, filteredOut, hidden } = isEmojiHidden(emoji);
|
|
50
|
+
|
|
51
|
+
return !failedToLoad && !filteredOut && !hidden && !isDisallowed;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const dimensions = useCategoryHeight(emojisToPush.length);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (dimensions) {
|
|
58
|
+
onHeightReady(dimensions.categoryHeight);
|
|
59
|
+
}
|
|
60
|
+
}, [dimensions, onHeightReady, emojisToPush.length]);
|
|
61
|
+
|
|
62
|
+
const emojis = emojisToPush.reduce((accumulator, emoji, index) => {
|
|
63
|
+
const unified = emojiUnified(emoji, activeSkinTone);
|
|
64
|
+
const style = getEmojiPositionStyle(dimensions, index);
|
|
65
|
+
if (
|
|
66
|
+
dimensions &&
|
|
67
|
+
BodyRef.current &&
|
|
68
|
+
shouldVirtualize({
|
|
69
|
+
scrollTop,
|
|
70
|
+
clientHeight: BodyRef.current?.clientHeight ?? 0,
|
|
71
|
+
topOffset,
|
|
72
|
+
style,
|
|
73
|
+
dimensions
|
|
74
|
+
})
|
|
75
|
+
) {
|
|
76
|
+
virtualizedCounter++;
|
|
77
|
+
preloaEmojidIfNeeded(
|
|
78
|
+
emoji,
|
|
79
|
+
emojiStyle,
|
|
80
|
+
scrollTop,
|
|
81
|
+
BodyRef.current?.clientHeight ?? 0,
|
|
82
|
+
topOffset,
|
|
83
|
+
style,
|
|
84
|
+
dimensions,
|
|
85
|
+
getEmojiUrl
|
|
86
|
+
);
|
|
87
|
+
return accumulator;
|
|
88
|
+
}
|
|
89
|
+
accumulator.push(
|
|
90
|
+
<ClickableEmoji
|
|
91
|
+
showVariations={showVariations}
|
|
92
|
+
key={unified}
|
|
93
|
+
emoji={emoji}
|
|
94
|
+
unified={unified}
|
|
95
|
+
emojiStyle={emojiStyle}
|
|
96
|
+
lazyLoad={lazyLoadEmojis}
|
|
97
|
+
getEmojiUrl={getEmojiUrl}
|
|
98
|
+
style={{
|
|
99
|
+
...style,
|
|
100
|
+
position: 'absolute'
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
return accumulator;
|
|
105
|
+
}, [] as ReactNode[]);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
virtualizedCounter,
|
|
109
|
+
emojis,
|
|
110
|
+
dimensions
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/hooks/useFilter.ts
CHANGED
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
import { useSearchResultsConfig } from '../config/useConfig';
|
|
12
12
|
import { DataEmoji } from '../dataUtils/DataTypes';
|
|
13
13
|
import { emojiNames } from '../dataUtils/emojiSelectors';
|
|
14
|
-
import doesBrowserSupportCssHas from '../globals/doesBrowserSupportCssHas';
|
|
15
14
|
|
|
16
15
|
import { useFocusSearchInput } from './useFocus';
|
|
17
16
|
|
|
@@ -78,34 +77,28 @@ export function useFilter() {
|
|
|
78
77
|
};
|
|
79
78
|
|
|
80
79
|
function onChange(inputValue: string) {
|
|
81
|
-
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
setTimeout(() => {
|
|
86
|
-
const filter = filterRef.current;
|
|
80
|
+
const filter = filterRef.current;
|
|
87
81
|
|
|
88
|
-
|
|
82
|
+
const nextValue = inputValue.toLowerCase();
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
if (filter?.[nextValue] || nextValue.length <= 1) {
|
|
85
|
+
return applySearch(nextValue);
|
|
86
|
+
}
|
|
93
87
|
|
|
94
|
-
|
|
88
|
+
const longestMatch = findLongestMatch(nextValue, filter);
|
|
95
89
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
if (!longestMatch) {
|
|
91
|
+
// Can we even get here?
|
|
92
|
+
// If so, we need to search among all emojis
|
|
93
|
+
return applySearch(nextValue);
|
|
94
|
+
}
|
|
101
95
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
96
|
+
setFilterRef(current =>
|
|
97
|
+
Object.assign(current, {
|
|
98
|
+
[nextValue]: filterEmojiObjectByKeyword(longestMatch, nextValue)
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
applySearch(nextValue);
|
|
109
102
|
}
|
|
110
103
|
}
|
|
111
104
|
|
package/src/hooks/useOnScroll.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { ElementRef } from '../components/context/ElementRefContext';
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@ import { useCloseAllOpenToggles } from './useCloseAllOpenToggles';
|
|
|
6
6
|
|
|
7
7
|
export function useOnScroll(BodyRef: ElementRef) {
|
|
8
8
|
const closeAllOpenToggles = useCloseAllOpenToggles();
|
|
9
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
9
10
|
|
|
10
11
|
useEffect(() => {
|
|
11
12
|
const bodyRef = BodyRef.current;
|
|
@@ -18,6 +19,7 @@ export function useOnScroll(BodyRef: ElementRef) {
|
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
function onScroll() {
|
|
22
|
+
setScrollTop(bodyRef?.scrollTop ?? 0);
|
|
21
23
|
closeAllOpenToggles();
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -25,4 +27,6 @@ export function useOnScroll(BodyRef: ElementRef) {
|
|
|
25
27
|
bodyRef?.removeEventListener('scroll', onScroll);
|
|
26
28
|
};
|
|
27
29
|
}, [BodyRef, closeAllOpenToggles]);
|
|
30
|
+
|
|
31
|
+
return scrollTop;
|
|
28
32
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function shouldVirtualize({
|
|
2
|
+
scrollTop,
|
|
3
|
+
clientHeight,
|
|
4
|
+
topOffset,
|
|
5
|
+
style,
|
|
6
|
+
dimensions
|
|
7
|
+
}: {
|
|
8
|
+
scrollTop: number;
|
|
9
|
+
clientHeight: number;
|
|
10
|
+
topOffset: number;
|
|
11
|
+
style: { top: number } | undefined;
|
|
12
|
+
dimensions: Dimensions;
|
|
13
|
+
}): boolean {
|
|
14
|
+
if (!style || !dimensions) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const emojiTop = topOffset + style.top;
|
|
19
|
+
const emojiBottom = emojiTop + dimensions.emojiSize;
|
|
20
|
+
|
|
21
|
+
const isVisible =
|
|
22
|
+
emojiBottom + dimensions.emojiSize * 2 >= scrollTop &&
|
|
23
|
+
emojiTop <= scrollTop + clientHeight + dimensions.emojiSize;
|
|
24
|
+
|
|
25
|
+
return !isVisible;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getEmojiPositionStyle(dimensions: Dimensions, index: number) {
|
|
29
|
+
return dimensions
|
|
30
|
+
? {
|
|
31
|
+
top: Math.floor(index / dimensions.emojisPerRow) * dimensions.emojiSize,
|
|
32
|
+
left: (index % dimensions.emojisPerRow) * dimensions.emojiSize
|
|
33
|
+
}
|
|
34
|
+
: undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// preload emoji if it is one row below viewport
|
|
38
|
+
|
|
39
|
+
export type Dimensions =
|
|
40
|
+
| {
|
|
41
|
+
emojiSize: number;
|
|
42
|
+
emojisPerRow: number;
|
|
43
|
+
categoryHeight: number;
|
|
44
|
+
}
|
|
45
|
+
| undefined;
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
import { CategoryConfig } from '../../config/categoryConfig';
|
|
4
|
-
import {
|
|
5
|
-
useEmojiStyleConfig,
|
|
6
|
-
useGetEmojiUrlConfig,
|
|
7
|
-
useSuggestedEmojisModeConfig
|
|
8
|
-
} from '../../config/useConfig';
|
|
9
|
-
import { emojiByUnified } from '../../dataUtils/emojiSelectors';
|
|
10
|
-
import { getSuggested } from '../../dataUtils/suggested';
|
|
11
|
-
import { useIsEmojiDisallowed } from '../../hooks/useDisallowedEmojis';
|
|
12
|
-
import { useIsEverMounted } from '../../hooks/useIsEverMounted';
|
|
13
|
-
import { useUpdateSuggested } from '../context/PickerContext';
|
|
14
|
-
import { ClickableEmoji } from '../emoji/Emoji';
|
|
15
|
-
|
|
16
|
-
import { EmojiCategory } from './EmojiCategory';
|
|
17
|
-
|
|
18
|
-
type Props = Readonly<{
|
|
19
|
-
categoryConfig: CategoryConfig;
|
|
20
|
-
}>;
|
|
21
|
-
|
|
22
|
-
export function Suggested({ categoryConfig }: Props) {
|
|
23
|
-
const [suggestedUpdated] = useUpdateSuggested();
|
|
24
|
-
const isMounted = useIsEverMounted();
|
|
25
|
-
const suggestedEmojisModeConfig = useSuggestedEmojisModeConfig();
|
|
26
|
-
const getEmojiUrl = useGetEmojiUrlConfig();
|
|
27
|
-
const suggested = React.useMemo(
|
|
28
|
-
() => getSuggested(suggestedEmojisModeConfig) ?? [],
|
|
29
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
30
|
-
[suggestedUpdated, suggestedEmojisModeConfig]
|
|
31
|
-
);
|
|
32
|
-
const emojiStyle = useEmojiStyleConfig();
|
|
33
|
-
const isEmojiDisallowed = useIsEmojiDisallowed();
|
|
34
|
-
|
|
35
|
-
if (!isMounted) {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<EmojiCategory
|
|
41
|
-
categoryConfig={categoryConfig}
|
|
42
|
-
hiddenOnSearch
|
|
43
|
-
hidden={suggested.length === 0}
|
|
44
|
-
>
|
|
45
|
-
{suggested.map(suggestedItem => {
|
|
46
|
-
const emoji = emojiByUnified(suggestedItem.original);
|
|
47
|
-
|
|
48
|
-
if (!emoji) {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (isEmojiDisallowed(emoji)) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<ClickableEmoji
|
|
58
|
-
showVariations={false}
|
|
59
|
-
unified={suggestedItem.unified}
|
|
60
|
-
emojiStyle={emojiStyle}
|
|
61
|
-
emoji={emoji}
|
|
62
|
-
key={suggestedItem.unified}
|
|
63
|
-
getEmojiUrl={getEmojiUrl}
|
|
64
|
-
/>
|
|
65
|
-
);
|
|
66
|
-
})}
|
|
67
|
-
</EmojiCategory>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
import { ClassNames, asSelectors } from '../../../DomUtils/classNames';
|
|
4
|
-
import { getNormalizedSearchTerm } from '../../../hooks/useFilter';
|
|
5
|
-
|
|
6
|
-
const SCOPE = `${asSelectors(ClassNames.emojiPicker)} ${asSelectors(
|
|
7
|
-
ClassNames.emojiList
|
|
8
|
-
)}`;
|
|
9
|
-
|
|
10
|
-
const EMOJI_BUTTON = ['button', asSelectors(ClassNames.emoji)].join('');
|
|
11
|
-
const CATEGORY = asSelectors(ClassNames.category);
|
|
12
|
-
|
|
13
|
-
export function CssSearch({ value }: { value: undefined | string }) {
|
|
14
|
-
if (!value) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const q = genQuery(value);
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<style>{`
|
|
22
|
-
${SCOPE} ${EMOJI_BUTTON} {
|
|
23
|
-
display: none;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
${SCOPE} ${q} {
|
|
28
|
-
display: flex;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
${SCOPE} ${CATEGORY}:not(:has(${q})) {
|
|
32
|
-
display: none;
|
|
33
|
-
}
|
|
34
|
-
`}</style>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function genQuery(value: string): string {
|
|
39
|
-
return [
|
|
40
|
-
EMOJI_BUTTON,
|
|
41
|
-
'[data-full-name*="',
|
|
42
|
-
getNormalizedSearchTerm(value),
|
|
43
|
-
'"]'
|
|
44
|
-
].join('');
|
|
45
|
-
}
|