emoji-picker-react 4.13.2 → 4.14.0-rc.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.
- package/README.md +15 -15
- 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 +540 -486
- 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 +541 -487
- package/dist/emoji-picker-react.esm.js.map +1 -1
- package/dist/hooks/useCategoryHeight.d.ts +5 -0
- package/dist/hooks/useOnScroll.d.ts +1 -1
- package/package.json +1 -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 +126 -35
- 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 +1 -8
- package/src/components/main/PickerMain.tsx +8 -5
- package/src/dataUtils/emojiSelectors.ts +29 -3
- package/src/hooks/useCategoryHeight.ts +92 -0
- package/src/hooks/useOnScroll.ts +5 -1
- package/dist/components/body/Suggested.d.ts +0 -7
- package/dist/components/header/Search/CssSearch.d.ts +0 -4
- package/src/components/body/Suggested.tsx +0 -69
- package/src/components/header/Search/CssSearch.tsx +0 -45
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { ElementRef } from '../components/context/ElementRefContext';
|
|
2
|
-
export declare function useOnScroll(BodyRef: ElementRef):
|
|
2
|
+
export declare function useOnScroll(BodyRef: ElementRef): number;
|
package/package.json
CHANGED
|
@@ -16,131 +16,6 @@ export function elementCountInRow(
|
|
|
16
16
|
return Math.floor(parentWidth / elementWidth);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function elementIndexInRow(
|
|
20
|
-
parent: NullableElement,
|
|
21
|
-
element: NullableElement
|
|
22
|
-
): number {
|
|
23
|
-
if (!parent || !element) {
|
|
24
|
-
return 0;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const elementWidth = element.getBoundingClientRect().width;
|
|
28
|
-
const elementLeft = element.getBoundingClientRect().left;
|
|
29
|
-
const parentLeft = parent.getBoundingClientRect().left;
|
|
30
|
-
|
|
31
|
-
return Math.floor((elementLeft - parentLeft) / elementWidth);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function rowNumber(
|
|
35
|
-
parent: NullableElement,
|
|
36
|
-
element: NullableElement
|
|
37
|
-
): number {
|
|
38
|
-
if (!parent || !element) {
|
|
39
|
-
return 0;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const elementHeight = element.getBoundingClientRect().height;
|
|
43
|
-
const elementTop = element.getBoundingClientRect().top;
|
|
44
|
-
const parentTop = parent.getBoundingClientRect().top;
|
|
45
|
-
return Math.round((elementTop - parentTop) / elementHeight);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function hasNextRow(
|
|
49
|
-
parent: NullableElement,
|
|
50
|
-
element: NullableElement
|
|
51
|
-
): boolean {
|
|
52
|
-
if (!parent || !element) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const elementHeight = element.getBoundingClientRect().height;
|
|
57
|
-
const elementTop = element.getBoundingClientRect().top;
|
|
58
|
-
const parentTop = parent.getBoundingClientRect().top;
|
|
59
|
-
const parentHeight = parent.getBoundingClientRect().height;
|
|
60
|
-
|
|
61
|
-
return Math.round(elementTop - parentTop + elementHeight) < parentHeight;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function getRowElements(
|
|
65
|
-
elements: HTMLElement[],
|
|
66
|
-
row: number,
|
|
67
|
-
elementsInRow: number
|
|
68
|
-
): HTMLElement[] {
|
|
69
|
-
if (row === -1) {
|
|
70
|
-
const lastRow = Math.floor((elements.length - 1) / elementsInRow);
|
|
71
|
-
const firstElementIndex = lastRow * elementsInRow;
|
|
72
|
-
const lastElementIndex = elements.length - 1;
|
|
73
|
-
return elements.slice(firstElementIndex, lastElementIndex + 1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return elements.slice(row * elementsInRow, (row + 1) * elementsInRow);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function getNextRowElements(
|
|
80
|
-
allElements: HTMLElement[],
|
|
81
|
-
currentRow: number,
|
|
82
|
-
elementsInRow: number
|
|
83
|
-
): HTMLElement[] {
|
|
84
|
-
const nextRow = currentRow + 1;
|
|
85
|
-
|
|
86
|
-
if (nextRow * elementsInRow > allElements.length) {
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return getRowElements(allElements, nextRow, elementsInRow);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function getElementInRow(
|
|
94
|
-
elements: HTMLElement[],
|
|
95
|
-
row: number,
|
|
96
|
-
elementsInRow: number,
|
|
97
|
-
indexInRow: number
|
|
98
|
-
): NullableElement {
|
|
99
|
-
const rowElements = getRowElements(elements, row, elementsInRow);
|
|
100
|
-
// get element, default to last
|
|
101
|
-
return rowElements[indexInRow] || rowElements[rowElements.length - 1] || null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function getElementInNextRow(
|
|
105
|
-
allElements: HTMLElement[],
|
|
106
|
-
currentRow: number,
|
|
107
|
-
elementsInRow: number,
|
|
108
|
-
index: number
|
|
109
|
-
): NullableElement {
|
|
110
|
-
const nextRowElements = getNextRowElements(
|
|
111
|
-
allElements,
|
|
112
|
-
currentRow,
|
|
113
|
-
elementsInRow
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
// return item in index, or last item in row
|
|
117
|
-
return (
|
|
118
|
-
nextRowElements[index] ||
|
|
119
|
-
nextRowElements[nextRowElements.length - 1] ||
|
|
120
|
-
null
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function getElementInPrevRow(
|
|
125
|
-
allElements: HTMLElement[],
|
|
126
|
-
currentRow: number,
|
|
127
|
-
elementsInRow: number,
|
|
128
|
-
index: number
|
|
129
|
-
): NullableElement {
|
|
130
|
-
const prevRowElements = getRowElements(
|
|
131
|
-
allElements,
|
|
132
|
-
currentRow - 1,
|
|
133
|
-
elementsInRow
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// default to last
|
|
137
|
-
return (
|
|
138
|
-
prevRowElements[index] ||
|
|
139
|
-
prevRowElements[prevRowElements.length - 1] ||
|
|
140
|
-
null
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
19
|
export function firstVisibleElementInContainer(
|
|
145
20
|
parent: NullableElement,
|
|
146
21
|
elements: HTMLElement[],
|
|
@@ -183,18 +58,20 @@ export function hasNextElementSibling(element: HTMLElement) {
|
|
|
183
58
|
return !!element.nextElementSibling;
|
|
184
59
|
}
|
|
185
60
|
|
|
186
|
-
function getLabelHeight(parentNode:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
61
|
+
export function getLabelHeight(parentNode: NullableElement) {
|
|
62
|
+
if (!parentNode) {
|
|
63
|
+
return DEFAULT_LABEL_HEIGHT;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const label = parentNode.querySelector(asSelectors(ClassNames.label));
|
|
190
67
|
|
|
191
|
-
|
|
68
|
+
if (label) {
|
|
192
69
|
const height = label.getBoundingClientRect().height;
|
|
193
|
-
// return height if label is not hidden
|
|
194
70
|
if (height > 0) {
|
|
195
71
|
return height;
|
|
196
72
|
}
|
|
197
73
|
}
|
|
198
74
|
|
|
75
|
+
// fallback to default
|
|
199
76
|
return DEFAULT_LABEL_HEIGHT;
|
|
200
77
|
}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
elementCountInRow,
|
|
3
|
-
elementIndexInRow,
|
|
4
|
-
getElementInNextRow,
|
|
5
|
-
getElementInPrevRow,
|
|
6
|
-
getElementInRow,
|
|
7
|
-
hasNextRow,
|
|
8
|
-
rowNumber
|
|
9
|
-
} from './elementPositionInRow';
|
|
1
|
+
import { elementCountInRow } from './elementPositionInRow';
|
|
10
2
|
import { focusElement } from './focusElement';
|
|
11
3
|
import { scrollEmojiAboveLabel } from './scrollTo';
|
|
12
4
|
import {
|
|
@@ -104,31 +96,47 @@ function visibleEmojiOneRowUp(element: HTMLElement) {
|
|
|
104
96
|
|
|
105
97
|
const categoryContent = closestCategoryContent(element);
|
|
106
98
|
const category = closestCategory(categoryContent);
|
|
107
|
-
const indexInRow = elementIndexInRow(categoryContent, element);
|
|
108
|
-
const row = rowNumber(categoryContent, element);
|
|
109
99
|
const countInRow = elementCountInRow(categoryContent, element);
|
|
110
100
|
|
|
111
|
-
|
|
112
|
-
|
|
101
|
+
const emojisInCurrentCategory = allVisibleEmojis(category);
|
|
102
|
+
const currentEmojiIndex = emojisInCurrentCategory.indexOf(element);
|
|
103
|
+
const indexInRow = currentEmojiIndex % countInRow;
|
|
113
104
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
if (currentEmojiIndex === -1) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (emojisInCurrentCategory[currentEmojiIndex - countInRow]) {
|
|
110
|
+
return emojisInCurrentCategory[currentEmojiIndex - countInRow];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const prevVisibleCategory = prevCategory(category);
|
|
114
|
+
|
|
115
|
+
if (!prevVisibleCategory) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const allPrevEmojis = allVisibleEmojis(prevVisibleCategory);
|
|
120
|
+
|
|
121
|
+
// if there is an emoji in the same index of `indexInRow` in the previous category, return it
|
|
122
|
+
// for this we need to find the last emoji in the previous category that shares the same indexInRow
|
|
123
|
+
// this is by using the % operator to find the last emoji that matches
|
|
124
|
+
|
|
125
|
+
const lastIndexInRow = (allPrevEmojis.length % countInRow) - 1;
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
-1, // last row
|
|
121
|
-
countInRow,
|
|
122
|
-
indexInRow
|
|
123
|
-
);
|
|
127
|
+
if (indexInRow > lastIndexInRow) {
|
|
128
|
+
return allPrevEmojis.at(-1);
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
countInRow
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
// otherwise, return the last emoji that shares the same indexInRow
|
|
132
|
+
|
|
133
|
+
for (let i = allPrevEmojis.length - 1; i >= 0; i--) {
|
|
134
|
+
if (i % countInRow === indexInRow) {
|
|
135
|
+
return allPrevEmojis[i];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return allPrevEmojis.at(-1);
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
function visibleEmojiOneRowDown(element: HTMLElement) {
|
|
@@ -138,30 +146,36 @@ function visibleEmojiOneRowDown(element: HTMLElement) {
|
|
|
138
146
|
|
|
139
147
|
const categoryContent = closestCategoryContent(element);
|
|
140
148
|
const category = closestCategory(categoryContent);
|
|
141
|
-
const indexInRow = elementIndexInRow(categoryContent, element);
|
|
142
|
-
const row = rowNumber(categoryContent, element);
|
|
143
149
|
const countInRow = elementCountInRow(categoryContent, element);
|
|
144
|
-
if (!hasNextRow(categoryContent, element)) {
|
|
145
|
-
const nextVisibleCategory = nextCategory(category);
|
|
146
150
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
151
|
+
const emojisInCurrentCategory = allVisibleEmojis(category);
|
|
152
|
+
const currentEmojiIndex = emojisInCurrentCategory.indexOf(element);
|
|
150
153
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
0,
|
|
154
|
-
countInRow,
|
|
155
|
-
indexInRow
|
|
156
|
-
);
|
|
154
|
+
if (currentEmojiIndex === -1) {
|
|
155
|
+
return null;
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
158
|
+
// the remainder until the end of the row
|
|
159
|
+
const remainder = countInRow - (currentEmojiIndex % countInRow) - 1;
|
|
160
|
+
const firstInNextRow = currentEmojiIndex + remainder + 1;
|
|
161
|
+
|
|
162
|
+
if (emojisInCurrentCategory[firstInNextRow]) {
|
|
163
|
+
// if we have a next row, search in the next row for the last available emoji
|
|
164
|
+
for (let p = currentEmojiIndex + countInRow; p % countInRow >= 0; p--) {
|
|
165
|
+
if (emojisInCurrentCategory[p]) {
|
|
166
|
+
return emojisInCurrentCategory[p];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const indexInRow = currentEmojiIndex % countInRow;
|
|
172
|
+
|
|
173
|
+
const nextVisibleCategory = nextCategory(category);
|
|
174
|
+
const emojisInNextCategory = allVisibleEmojis(nextVisibleCategory);
|
|
175
|
+
|
|
176
|
+
if (emojisInNextCategory[indexInRow]) {
|
|
177
|
+
return emojisInNextCategory[indexInRow];
|
|
178
|
+
}
|
|
165
179
|
|
|
166
|
-
return
|
|
180
|
+
return emojisInNextCategory.at(0) ?? null;
|
|
167
181
|
}
|
|
@@ -17,7 +17,7 @@ import { EmojiVariationPicker } from './EmojiVariationPicker';
|
|
|
17
17
|
|
|
18
18
|
export function Body() {
|
|
19
19
|
const BodyRef = useBodyRef();
|
|
20
|
-
useOnScroll(BodyRef);
|
|
20
|
+
const scrollTop = useOnScroll(BodyRef);
|
|
21
21
|
useMouseDownHandlers(BodyRef, MOUSE_EVENT_SOURCE.PICKER);
|
|
22
22
|
useOnMouseMove();
|
|
23
23
|
|
|
@@ -27,7 +27,7 @@ export function Body() {
|
|
|
27
27
|
ref={BodyRef}
|
|
28
28
|
>
|
|
29
29
|
<EmojiVariationPicker />
|
|
30
|
-
<EmojiList />
|
|
30
|
+
<EmojiList scrollTop={scrollTop} />
|
|
31
31
|
</div>
|
|
32
32
|
);
|
|
33
33
|
}
|
|
@@ -18,13 +18,15 @@ type Props = Readonly<{
|
|
|
18
18
|
children?: React.ReactNode;
|
|
19
19
|
hidden?: boolean;
|
|
20
20
|
hiddenOnSearch?: boolean;
|
|
21
|
+
height?: number;
|
|
21
22
|
}>;
|
|
22
23
|
|
|
23
24
|
export function EmojiCategory({
|
|
24
25
|
categoryConfig,
|
|
25
26
|
children,
|
|
26
27
|
hidden,
|
|
27
|
-
hiddenOnSearch
|
|
28
|
+
hiddenOnSearch,
|
|
29
|
+
height
|
|
28
30
|
}: Props) {
|
|
29
31
|
const category = categoryFromCategoryConfig(categoryConfig);
|
|
30
32
|
const categoryName = categoryNameFromCategoryConfig(categoryConfig);
|
|
@@ -40,7 +42,9 @@ export function EmojiCategory({
|
|
|
40
42
|
aria-label={categoryName}
|
|
41
43
|
>
|
|
42
44
|
<h2 className={cx(styles.label)}>{categoryName}</h2>
|
|
43
|
-
<div className={cx(styles.categoryContent)}
|
|
45
|
+
<div className={cx(styles.categoryContent)} style={{ height }}>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
44
48
|
</li>
|
|
45
49
|
);
|
|
46
50
|
}
|
|
@@ -48,9 +52,7 @@ export function EmojiCategory({
|
|
|
48
52
|
const styles = stylesheet.create({
|
|
49
53
|
category: {
|
|
50
54
|
'.': ClassNames.category,
|
|
51
|
-
|
|
52
|
-
display: 'none'
|
|
53
|
-
}
|
|
55
|
+
position: 'relative'
|
|
54
56
|
},
|
|
55
57
|
categoryContent: {
|
|
56
58
|
'.': ClassNames.categoryContent,
|
|
@@ -2,9 +2,9 @@ import { cx } from 'flairup';
|
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
|
|
4
4
|
import { ClassNames } from '../../DomUtils/classNames';
|
|
5
|
+
import { getLabelHeight } from '../../DomUtils/elementPositionInRow';
|
|
5
6
|
import { stylesheet } from '../../Stylesheet/stylesheet';
|
|
6
7
|
import {
|
|
7
|
-
Categories,
|
|
8
8
|
CategoryConfig,
|
|
9
9
|
categoryFromCategoryConfig
|
|
10
10
|
} from '../../config/categoryConfig';
|
|
@@ -15,37 +15,54 @@ import {
|
|
|
15
15
|
useLazyLoadEmojisConfig,
|
|
16
16
|
useSkinTonesDisabledConfig
|
|
17
17
|
} from '../../config/useConfig';
|
|
18
|
-
import {
|
|
18
|
+
import { DataEmojis } from '../../dataUtils/DataTypes';
|
|
19
|
+
import {
|
|
20
|
+
emojiUnified,
|
|
21
|
+
useGetEmojisByCategory
|
|
22
|
+
} from '../../dataUtils/emojiSelectors';
|
|
23
|
+
import { useCategoryHeight } from '../../hooks/useCategoryHeight';
|
|
19
24
|
import { useIsEmojiDisallowed } from '../../hooks/useDisallowedEmojis';
|
|
20
25
|
import { useIsEmojiHidden } from '../../hooks/useIsEmojiHidden';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
useIsPastInitialLoad
|
|
24
|
-
} from '../context/PickerContext';
|
|
26
|
+
import { useBodyRef, useEmojiListRef } from '../context/ElementRefContext';
|
|
27
|
+
import { useActiveSkinToneState } from '../context/PickerContext';
|
|
25
28
|
import { ClickableEmoji } from '../emoji/Emoji';
|
|
26
29
|
|
|
27
30
|
import { EmojiCategory } from './EmojiCategory';
|
|
28
|
-
import { Suggested } from './Suggested';
|
|
29
31
|
|
|
30
|
-
export function EmojiList() {
|
|
32
|
+
export function EmojiList({ scrollTop }: { scrollTop: number }) {
|
|
31
33
|
const categories = useCategoriesConfig();
|
|
32
|
-
const
|
|
34
|
+
const [categoryHeights, setCategoryHeights] = React.useState<{
|
|
35
|
+
[key: string]: number;
|
|
36
|
+
}>({});
|
|
37
|
+
const EmojiListRef = useEmojiListRef();
|
|
38
|
+
const getEmojisByCategory = useGetEmojisByCategory();
|
|
33
39
|
|
|
40
|
+
const labelHeight = getLabelHeight(EmojiListRef.current);
|
|
41
|
+
|
|
42
|
+
let topOffset = 0;
|
|
34
43
|
return (
|
|
35
|
-
<ul className={cx(styles.emojiList)}>
|
|
44
|
+
<ul className={cx(styles.emojiList)} ref={EmojiListRef}>
|
|
36
45
|
{categories.map(categoryConfig => {
|
|
37
46
|
const category = categoryFromCategoryConfig(categoryConfig);
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
const currentOffset = topOffset;
|
|
49
|
+
const categoryHeight = categoryHeights[category];
|
|
50
|
+
if (categoryHeight) {
|
|
51
|
+
topOffset += categoryHeight + labelHeight;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
return (
|
|
44
55
|
<React.Suspense key={category}>
|
|
45
56
|
<RenderCategory
|
|
46
|
-
|
|
57
|
+
categoryEmojis={getEmojisByCategory(category)}
|
|
47
58
|
categoryConfig={categoryConfig}
|
|
48
|
-
|
|
59
|
+
topOffset={currentOffset}
|
|
60
|
+
onHeightReady={height => {
|
|
61
|
+
if (categoryHeights[category] !== height) {
|
|
62
|
+
setCategoryHeights(prev => ({ ...prev, [category]: height }));
|
|
63
|
+
}
|
|
64
|
+
}}
|
|
65
|
+
scrollTop={scrollTop}
|
|
49
66
|
/>
|
|
50
67
|
</React.Suspense>
|
|
51
68
|
);
|
|
@@ -55,37 +72,45 @@ export function EmojiList() {
|
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
function RenderCategory({
|
|
58
|
-
|
|
75
|
+
categoryEmojis,
|
|
59
76
|
categoryConfig,
|
|
60
|
-
|
|
77
|
+
topOffset,
|
|
78
|
+
onHeightReady,
|
|
79
|
+
scrollTop
|
|
61
80
|
}: {
|
|
62
|
-
|
|
81
|
+
categoryEmojis: DataEmojis;
|
|
63
82
|
categoryConfig: CategoryConfig;
|
|
64
|
-
|
|
83
|
+
topOffset: number;
|
|
84
|
+
onHeightReady: (height: number) => void;
|
|
85
|
+
scrollTop: number;
|
|
65
86
|
}) {
|
|
66
87
|
const isEmojiHidden = useIsEmojiHidden();
|
|
67
88
|
const lazyLoadEmojis = useLazyLoadEmojisConfig();
|
|
68
89
|
const emojiStyle = useEmojiStyleConfig();
|
|
69
|
-
const isPastInitialLoad = useIsPastInitialLoad();
|
|
70
90
|
const [activeSkinTone] = useActiveSkinToneState();
|
|
71
91
|
const isEmojiDisallowed = useIsEmojiDisallowed();
|
|
72
92
|
const getEmojiUrl = useGetEmojiUrlConfig();
|
|
73
93
|
const showVariations = !useSkinTonesDisabledConfig();
|
|
94
|
+
const BodyRef = useBodyRef();
|
|
74
95
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const emojisToPush =
|
|
78
|
-
!isPastInitialLoad && renderdCategoriesCountRef.current > 0
|
|
79
|
-
? []
|
|
80
|
-
: emojisByCategory(category);
|
|
96
|
+
let hiddenCounter = 0;
|
|
97
|
+
let virtualizedCounter = 0;
|
|
81
98
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
99
|
+
const emojisToPush = categoryEmojis.filter(emoji => {
|
|
100
|
+
const { failedToLoad, filteredOut, hidden } = isEmojiHidden(emoji);
|
|
85
101
|
|
|
86
|
-
|
|
102
|
+
return !failedToLoad && !filteredOut && !hidden;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const dimensions = useCategoryHeight(emojisToPush.length);
|
|
106
|
+
|
|
107
|
+
React.useEffect(() => {
|
|
108
|
+
if (dimensions) {
|
|
109
|
+
onHeightReady(dimensions.categoryHeight);
|
|
110
|
+
}
|
|
111
|
+
}, [dimensions, onHeightReady, emojisToPush.length]);
|
|
87
112
|
|
|
88
|
-
const emojis = emojisToPush.
|
|
113
|
+
const emojis = emojisToPush.reduce((accumulator, emoji, index) => {
|
|
89
114
|
const unified = emojiUnified(emoji, activeSkinTone);
|
|
90
115
|
const { failedToLoad, filteredOut, hidden } = isEmojiHidden(emoji);
|
|
91
116
|
|
|
@@ -95,11 +120,26 @@ function RenderCategory({
|
|
|
95
120
|
hiddenCounter++;
|
|
96
121
|
}
|
|
97
122
|
|
|
98
|
-
if (isDisallowed) {
|
|
99
|
-
return
|
|
123
|
+
if (isDisallowed || failedToLoad || filteredOut) {
|
|
124
|
+
return accumulator;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const style = getEmojiPositionStyle(dimensions, index - hiddenCounter);
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
shouldVirtualize({
|
|
131
|
+
scrollTop,
|
|
132
|
+
clientHeight: BodyRef.current?.clientHeight ?? 0,
|
|
133
|
+
topOffset,
|
|
134
|
+
style,
|
|
135
|
+
dimensions
|
|
136
|
+
})
|
|
137
|
+
) {
|
|
138
|
+
virtualizedCounter++;
|
|
139
|
+
return accumulator;
|
|
100
140
|
}
|
|
101
141
|
|
|
102
|
-
|
|
142
|
+
accumulator.push(
|
|
103
143
|
<ClickableEmoji
|
|
104
144
|
showVariations={showVariations}
|
|
105
145
|
key={unified}
|
|
@@ -110,16 +150,23 @@ function RenderCategory({
|
|
|
110
150
|
emojiStyle={emojiStyle}
|
|
111
151
|
lazyLoad={lazyLoadEmojis}
|
|
112
152
|
getEmojiUrl={getEmojiUrl}
|
|
153
|
+
style={{
|
|
154
|
+
...style,
|
|
155
|
+
position: 'absolute'
|
|
156
|
+
}}
|
|
113
157
|
/>
|
|
114
158
|
);
|
|
115
|
-
|
|
159
|
+
|
|
160
|
+
return accumulator;
|
|
161
|
+
}, [] as any);
|
|
116
162
|
|
|
117
163
|
return (
|
|
118
164
|
<EmojiCategory
|
|
119
165
|
categoryConfig={categoryConfig}
|
|
166
|
+
height={dimensions?.categoryHeight}
|
|
120
167
|
// Indicates that there are no visible emojis
|
|
121
168
|
// Hence, the category should be hidden
|
|
122
|
-
hidden={hiddenCounter === emojis.length}
|
|
169
|
+
hidden={hiddenCounter === emojis.length && virtualizedCounter === 0}
|
|
123
170
|
>
|
|
124
171
|
{emojis}
|
|
125
172
|
</EmojiCategory>
|
|
@@ -134,3 +181,47 @@ const styles = stylesheet.create({
|
|
|
134
181
|
padding: '0'
|
|
135
182
|
}
|
|
136
183
|
});
|
|
184
|
+
|
|
185
|
+
type Dimensions =
|
|
186
|
+
| {
|
|
187
|
+
emojiSize: number;
|
|
188
|
+
emojisPerRow: number;
|
|
189
|
+
categoryHeight: number;
|
|
190
|
+
}
|
|
191
|
+
| undefined;
|
|
192
|
+
|
|
193
|
+
function getEmojiPositionStyle(dimensions: Dimensions, index: number) {
|
|
194
|
+
return dimensions
|
|
195
|
+
? {
|
|
196
|
+
top: Math.floor(index / dimensions.emojisPerRow) * dimensions.emojiSize,
|
|
197
|
+
left: (index % dimensions.emojisPerRow) * dimensions.emojiSize
|
|
198
|
+
}
|
|
199
|
+
: undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function shouldVirtualize({
|
|
203
|
+
scrollTop,
|
|
204
|
+
clientHeight,
|
|
205
|
+
topOffset,
|
|
206
|
+
style,
|
|
207
|
+
dimensions
|
|
208
|
+
}: {
|
|
209
|
+
scrollTop: number;
|
|
210
|
+
clientHeight: number;
|
|
211
|
+
topOffset: number;
|
|
212
|
+
style: { top: number } | undefined;
|
|
213
|
+
dimensions: Dimensions;
|
|
214
|
+
}): boolean {
|
|
215
|
+
if (!style || !dimensions) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const emojiTop = topOffset + style.top;
|
|
220
|
+
const emojiBottom = emojiTop + dimensions.emojiSize;
|
|
221
|
+
|
|
222
|
+
const isVisible =
|
|
223
|
+
emojiBottom + dimensions.emojiSize * 2 >= scrollTop &&
|
|
224
|
+
emojiTop <= scrollTop + clientHeight + dimensions.emojiSize;
|
|
225
|
+
|
|
226
|
+
return !isVisible;
|
|
227
|
+
}
|
|
@@ -11,6 +11,7 @@ export function ElementRefContextProvider({
|
|
|
11
11
|
const PickerMainRef = React.useRef<HTMLElement>(null);
|
|
12
12
|
const AnchoredEmojiRef = React.useRef<HTMLElement>(null);
|
|
13
13
|
const BodyRef = React.useRef<HTMLDivElement>(null);
|
|
14
|
+
const EmojiListRef = React.useRef<HTMLUListElement>(null);
|
|
14
15
|
const SearchInputRef = React.useRef<HTMLInputElement>(null);
|
|
15
16
|
const SkinTonePickerRef = React.useRef<HTMLDivElement>(null);
|
|
16
17
|
const CategoryNavigationRef = React.useRef<HTMLDivElement>(null);
|
|
@@ -22,6 +23,7 @@ export function ElementRefContextProvider({
|
|
|
22
23
|
value={{
|
|
23
24
|
AnchoredEmojiRef,
|
|
24
25
|
BodyRef,
|
|
26
|
+
EmojiListRef,
|
|
25
27
|
CategoryNavigationRef,
|
|
26
28
|
PickerMainRef,
|
|
27
29
|
SearchInputRef,
|
|
@@ -42,6 +44,7 @@ export type ElementRef<
|
|
|
42
44
|
type ElementRefs = {
|
|
43
45
|
PickerMainRef: ElementRef;
|
|
44
46
|
AnchoredEmojiRef: ElementRef;
|
|
47
|
+
EmojiListRef: ElementRef<HTMLUListElement>;
|
|
45
48
|
SkinTonePickerRef: ElementRef<HTMLDivElement>;
|
|
46
49
|
SearchInputRef: ElementRef<HTMLInputElement>;
|
|
47
50
|
BodyRef: ElementRef<HTMLDivElement>;
|
|
@@ -54,6 +57,7 @@ const ElementRefContext = React.createContext<ElementRefs>({
|
|
|
54
57
|
AnchoredEmojiRef: React.createRef(),
|
|
55
58
|
BodyRef: React.createRef(),
|
|
56
59
|
CategoryNavigationRef: React.createRef(),
|
|
60
|
+
EmojiListRef: React.createRef(),
|
|
57
61
|
PickerMainRef: React.createRef(),
|
|
58
62
|
SearchInputRef: React.createRef(),
|
|
59
63
|
SkinTonePickerRef: React.createRef(),
|
|
@@ -65,6 +69,10 @@ function useElementRef() {
|
|
|
65
69
|
return React.useContext(ElementRefContext);
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
export function useEmojiListRef() {
|
|
73
|
+
return useElementRef()['EmojiListRef'];
|
|
74
|
+
}
|
|
75
|
+
|
|
68
76
|
export function usePickerMainRef() {
|
|
69
77
|
return useElementRef()['PickerMainRef'];
|
|
70
78
|
}
|
|
@@ -19,6 +19,7 @@ type ClickableEmojiButtonProps = Readonly<{
|
|
|
19
19
|
unified?: string;
|
|
20
20
|
noBackground?: boolean;
|
|
21
21
|
className?: string;
|
|
22
|
+
style?: React.CSSProperties;
|
|
22
23
|
}>;
|
|
23
24
|
|
|
24
25
|
export function ClickableEmojiButton({
|
|
@@ -30,7 +31,8 @@ export function ClickableEmojiButton({
|
|
|
30
31
|
hasVariations,
|
|
31
32
|
children,
|
|
32
33
|
className,
|
|
33
|
-
noBackground = false
|
|
34
|
+
noBackground = false,
|
|
35
|
+
style
|
|
34
36
|
}: ClickableEmojiButtonProps) {
|
|
35
37
|
return (
|
|
36
38
|
<Button
|
|
@@ -48,6 +50,7 @@ export function ClickableEmojiButton({
|
|
|
48
50
|
data-unified={unified}
|
|
49
51
|
aria-label={getAriaLabel(emojiNames)}
|
|
50
52
|
data-full-name={emojiNames}
|
|
53
|
+
style={style}
|
|
51
54
|
>
|
|
52
55
|
{children}
|
|
53
56
|
</Button>
|