Skip to content

Commit 8902804

Browse files
committed
patch: improve emoji measurement and layout stability
1 parent d600eb9 commit 8902804

File tree

7 files changed

+123
-27
lines changed

7 files changed

+123
-27
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "4.15.1",
2+
"version": "4.15.2",
33
"license": "MIT",
44
"main": "dist/index.js",
55
"homepage": "https://ealush.com/emoji-picker-react",

src/components/body/EmojiCategory.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export function EmojiCategory({
5252
const styles = stylesheet.create({
5353
category: {
5454
'.': ClassNames.category,
55+
minHeight:
56+
'calc(var(--epr-emoji-fullsize) + var(--epr-category-label-height))',
5557
position: 'relative'
5658
},
5759
categoryContent: {

src/components/body/EmojiList.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { useEmojiListRef } from '../context/ElementRefContext';
1616
import { useVisibleCategoriesState } from '../context/PickerContext';
1717

1818
import { EmojiCategory } from './EmojiCategory';
19+
import { MeasureEmoji } from './MeasureEmoji';
1920

2021
export function EmojiList({ scrollTop }: { scrollTop: number }) {
2122
const categories = useCategoriesConfig();
@@ -30,6 +31,7 @@ export function EmojiList({ scrollTop }: { scrollTop: number }) {
3031
let topOffset = 0;
3132
return (
3233
<ul className={cx(styles.emojiList)} ref={EmojiListRef}>
34+
<MeasureEmoji />
3335
{categories.map(categoryConfig => {
3436
const category = categoryFromCategoryConfig(categoryConfig);
3537

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as React from 'react';
2+
3+
import {
4+
categoryFromCategoryConfig
5+
} from '../../config/categoryConfig';
6+
import {
7+
useCategoriesConfig,
8+
useEmojiStyleConfig,
9+
useGetEmojiUrlConfig,
10+
useLazyLoadEmojisConfig
11+
} from '../../config/useConfig';
12+
import {
13+
useGetEmojisByCategory,
14+
emojiUnified
15+
} from '../../dataUtils/emojiSelectors';
16+
import { useActiveSkinToneState, useEmojiSizeState } from '../context/PickerContext';
17+
import { ClickableEmoji } from '../emoji/Emoji';
18+
19+
export function MeasureEmoji() {
20+
const categories = useCategoriesConfig();
21+
const getEmojisByCategory = useGetEmojisByCategory();
22+
const emojiStyle = useEmojiStyleConfig();
23+
const getEmojiUrl = useGetEmojiUrlConfig();
24+
const lazyLoadEmojis = useLazyLoadEmojisConfig();
25+
const [activeSkinTone] = useActiveSkinToneState();
26+
const [emojiSize, setEmojiSize] = useEmojiSizeState();
27+
const ref = React.useRef<HTMLDivElement>(null);
28+
29+
React.useLayoutEffect(() => {
30+
if (ref.current) {
31+
setEmojiSize(ref.current.clientHeight);
32+
}
33+
});
34+
35+
if (emojiSize) {
36+
return null;
37+
}
38+
39+
const firstCategory = categories[0];
40+
const dummyEmoji = getEmojisByCategory(
41+
categoryFromCategoryConfig(firstCategory)
42+
)[0];
43+
const unified = dummyEmoji
44+
? emojiUnified(dummyEmoji, activeSkinTone)
45+
: '';
46+
47+
if (!dummyEmoji) {
48+
return null;
49+
}
50+
51+
return (
52+
<div ref={ref}>
53+
<ClickableEmoji
54+
emoji={dummyEmoji}
55+
unified={unified}
56+
emojiStyle={emojiStyle}
57+
getEmojiUrl={getEmojiUrl}
58+
lazyLoad={lazyLoadEmojis}
59+
showVariations={false}
60+
hidden={false}
61+
style={{
62+
opacity: 0,
63+
pointerEvents: 'none',
64+
position: 'absolute',
65+
top: 0,
66+
left: 0,
67+
zIndex: -1,
68+
height: 'var(--epr-emoji-fullsize)',
69+
width: 'var(--epr-emoji-fullsize)'
70+
}}
71+
/>
72+
</div>
73+
);
74+
}

src/components/context/PickerContext.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function PickerContextProvider({ children }: Props) {
3636
const reactionsModeState = useState(reactionsDefaultOpen);
3737
const [isPastInitialLoad, setIsPastInitialLoad] = useState(false);
3838
const visibleCategoriesState = useState<string[]>([]);
39+
const emojiSizeState = useState<number | null>(null);
3940

4041
useMarkInitialLoad(setIsPastInitialLoad);
4142

@@ -55,7 +56,8 @@ export function PickerContextProvider({ children }: Props) {
5556
skinToneFanOpenState,
5657
suggestedUpdateState,
5758
reactionsModeState,
58-
visibleCategoriesState
59+
visibleCategoriesState,
60+
emojiSizeState
5961
}}
6062
>
6163
{children}
@@ -80,6 +82,7 @@ const PickerContext = React.createContext<{
8082
disallowedEmojisRef: React.MutableRefObject<Record<string, boolean>>;
8183
reactionsModeState: ReactState<boolean>;
8284
visibleCategoriesState: ReactState<Array<string>>;
85+
emojiSizeState: ReactState<number | null>;
8386
}>({
8487
activeCategoryState: [null, () => {}],
8588
activeSkinTone: [SkinTones.NEUTRAL, () => {}],
@@ -94,7 +97,8 @@ const PickerContext = React.createContext<{
9497
skinToneFanOpenState: [false, () => {}],
9598
suggestedUpdateState: [Date.now(), () => {}],
9699
reactionsModeState: [false, () => {}],
97-
visibleCategoriesState: [[], () => []]
100+
visibleCategoriesState: [[], () => []],
101+
emojiSizeState: [null, () => {}]
98102
});
99103

100104
type Props = Readonly<{
@@ -164,6 +168,11 @@ export function useVisibleCategoriesState() {
164168
return visibleCategoriesState;
165169
}
166170

171+
export function useEmojiSizeState() {
172+
const { emojiSizeState } = React.useContext(PickerContext);
173+
return emojiSizeState;
174+
}
175+
167176
export function useUpdateSuggested(): [number, () => void] {
168177
const { suggestedUpdateState } = React.useContext(PickerContext);
169178

src/hooks/useCategoryHeight.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@ import * as React from 'react';
33
import { EmojiButtonSelector } from '../DomUtils/selectors';
44
import {
55
useEmojiListRef,
6-
usePickerMainRef
6+
usePickerMainRef,
77
} from '../components/context/ElementRefContext';
88
import {
99
useReactionsModeState,
10-
useVisibleCategoriesState
10+
useVisibleCategoriesState,
11+
useEmojiSizeState,
1112
} from '../components/context/PickerContext';
1213

13-
const EMOJI_SIZE_DEFAULT = 32;
14+
const EMOJI_SIZE_DEFAULT = 40;
1415

15-
export function useCategoryHeight(
16-
emojiCount: number
17-
):
16+
export function useCategoryHeight(emojiCount: number):
1817
| {
1918
categoryHeight: number;
2019
emojisPerRow: number;
@@ -26,6 +25,7 @@ export function useCategoryHeight(
2625
const PickerMainRef = usePickerMainRef();
2726
const emojiSizeRef = React.useRef<number | undefined>();
2827
const [visibleCategories] = useVisibleCategoriesState();
28+
const [emojiSizeFromContext] = useEmojiSizeState();
2929
const [dimensions, setDimensions] = React.useState<{
3030
categoryHeight: number;
3131
emojisPerRow: number;
@@ -38,12 +38,18 @@ export function useCategoryHeight(
3838
if (!listEl) return;
3939

4040
const emojiElement = listEl.querySelector(
41-
EmojiButtonSelector
41+
EmojiButtonSelector,
4242
) as HTMLElement | null;
4343

4444
const measured = emojiElement?.clientHeight;
45-
const emojiSize = measured ?? emojiSizeRef.current ?? EMOJI_SIZE_DEFAULT;
46-
emojiSizeRef.current = emojiSize;
45+
if (measured) {
46+
emojiSizeRef.current = measured;
47+
}
48+
const emojiSize =
49+
emojiSizeFromContext ||
50+
measured ||
51+
emojiSizeRef.current ||
52+
EMOJI_SIZE_DEFAULT;
4753
const pickerWidth = listEl.clientWidth;
4854

4955
if (pickerWidth === 0 || emojiSize === 0) return;
@@ -53,7 +59,7 @@ export function useCategoryHeight(
5359
const categoryHeight = rowCount * emojiSize;
5460

5561
setDimensions({ categoryHeight, emojisPerRow, emojiSize });
56-
}, [EmojiListRef, emojiCount]);
62+
}, [EmojiListRef, emojiCount, emojiSizeFromContext]);
5763

5864
// Recompute on data-count changes and when reactions mode toggles
5965
React.useEffect(() => {
@@ -62,7 +68,7 @@ export function useCategoryHeight(
6268
emojiCount,
6369
isReactionsMode,
6470
computeAndSetDimensions,
65-
visibleCategories.length
71+
visibleCategories.length,
6672
]);
6773

6874
// Listen to transitionend on the picker root (where height transition occurs)
@@ -90,7 +96,7 @@ export function useCategoryHeight(
9096
};
9197

9298
rootEl.addEventListener('transitionend', handler, {
93-
passive: true
99+
passive: true,
94100
});
95101
return () => {
96102
rootEl.removeEventListener('transitionend', handler);

src/hooks/useEmojiVirtualization.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ReactNode, useEffect } from 'react';
1+
2+
import { ReactNode, useEffect } from 'react';
23
import * as React from 'react';
34

45
import { useBodyRef } from '../components/context/ElementRefContext';
@@ -61,20 +62,22 @@ export function useEmojiVirtualization({
6162
}
6263
}, [dimensions, onHeightReady, emojisToPush.length]);
6364

65+
const isVirtualized = (style: { top: number; left: number } | undefined) =>
66+
dimensions &&
67+
BodyRef.current &&
68+
shouldVirtualize({
69+
scrollTop,
70+
clientHeight: BodyRef.current?.clientHeight ?? 0,
71+
topOffset,
72+
style,
73+
dimensions
74+
});
75+
6476
const emojis = emojisToPush.reduce((accumulator, emoji, index) => {
6577
const unified = emojiUnified(emoji, activeSkinTone);
6678
const style = getEmojiPositionStyle(dimensions, index);
67-
if (
68-
dimensions &&
69-
BodyRef.current &&
70-
shouldVirtualize({
71-
scrollTop,
72-
clientHeight: BodyRef.current?.clientHeight ?? 0,
73-
topOffset,
74-
style,
75-
dimensions
76-
})
77-
) {
79+
80+
if (isVirtualized(style)) {
7881
virtualizedCounter++;
7982
preloadEmojiIfNeeded(
8083
emoji,

0 commit comments

Comments
 (0)