emoji-picker-react 5.0.0 → 5.0.2
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/classNames.d.ts +0 -1
- package/dist/DomUtils/keyboardNavigation.d.ts +10 -2
- package/dist/DomUtils/selectors.d.ts +2 -2
- package/dist/data/index.js +1 -1
- package/dist/data/skinToneVariations.js +2 -2
- package/dist/data/skinToneVariations.ts +8 -7
- package/dist/data-core/prepare.d.ts +6 -0
- package/dist/esm/chunks/chunk-56D5WCOZ.mjs +6729 -0
- package/dist/esm/chunks/chunk-7ZGTUZFB.mjs +8021 -12
- package/dist/esm/chunks/chunk-IODYR4CR.mjs +14 -0
- package/dist/esm/chunks/chunk-LO4TPPLH.mjs +32 -42
- package/dist/esm/chunks/chunk-RU7G34WK.mjs +156 -0
- package/dist/esm/chunks/emojis-6L3GRBAP.mjs +5 -7
- package/dist/esm/data.mjs +9 -16
- package/dist/esm/index.mjs +230 -340
- package/dist/esm/primitives/index.mjs +6 -65
- package/dist/hooks/preloadEmoji.d.ts +1 -5
- package/dist/hooks/useCategoryHeight.d.ts +2 -1
- package/dist/hooks/useEmojiVirtualization.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/primitives/index.js +1 -1
- package/dist/virtualization/virtualizationHelpers.d.ts +2 -0
- package/llms-full.txt +9 -3
- package/package.json +2 -2
- package/src/DomUtils/classNames.ts +0 -4
- package/src/DomUtils/elementPositionInRow.ts +2 -2
- package/src/DomUtils/keyboardNavigation.ts +74 -10
- package/src/DomUtils/scrollTo.ts +2 -2
- package/src/DomUtils/selectors.ts +10 -16
- package/src/components/body/EmojiCategory.tsx +1 -1
- package/src/components/body/EmojiList.tsx +54 -23
- package/src/components/context/PickerDataContext.tsx +7 -2
- package/src/components/header/Search/Search.tsx +4 -2
- package/src/components/header/SkinTonePicker/BtnSkinToneVariation.tsx +7 -6
- package/src/components/main/defaultAppearance.tsx +2 -1
- package/src/config/config.ts +6 -3
- package/src/data/skinToneVariations.ts +8 -7
- package/src/data-core/pickerData.ts +8 -12
- package/src/data-core/prepare.ts +32 -39
- package/src/data-core/search.ts +10 -2
- package/src/dataUtils/emojiSelectors.ts +2 -1
- package/src/dataUtils/parseNativeEmoji.ts +3 -4
- package/src/dataUtils/suggestedEmojis.ts +25 -39
- package/src/hooks/preloadEmoji.ts +13 -52
- package/src/hooks/useActiveCategoryScrollDetection.ts +22 -19
- package/src/hooks/useCategoryHeight.ts +38 -6
- package/src/hooks/useDisallowedEmojis.ts +23 -13
- package/src/hooks/useEmojiVirtualization.tsx +102 -66
- package/src/hooks/useFilter.ts +10 -8
- package/src/hooks/useIsEmojiHidden.ts +16 -12
- package/src/hooks/useKeyboardNavigation.ts +104 -17
- package/src/primitives/structuralStyles.tsx +3 -0
- package/src/primitives/tokens.ts +1 -0
- package/src/virtualization/virtualizationHelpers.ts +9 -2
- package/dist/esm/chunks/chunk-46BUDQLE.mjs +0 -228
- package/dist/esm/chunks/chunk-FQIJ4HBW.mjs +0 -25
- package/dist/esm/chunks/chunk-KQPOIRSC.mjs +0 -9305
|
@@ -13,17 +13,20 @@ import {
|
|
|
13
13
|
useLazyLoadEmojisConfig,
|
|
14
14
|
useSkinTonesDisabledConfig,
|
|
15
15
|
} from '../config/useConfig';
|
|
16
|
-
import { DataEmojis } from '../dataUtils/DataTypes';
|
|
16
|
+
import { DataEmoji, DataEmojis } from '../dataUtils/DataTypes';
|
|
17
17
|
import { emojiUnified } from '../dataUtils/emojiUtils';
|
|
18
|
+
import { EmojiStyle } from '../types/exposedTypes';
|
|
18
19
|
import {
|
|
19
20
|
getEmojiPositionStyle,
|
|
20
21
|
shouldVirtualize,
|
|
22
|
+
isJustBelowViewport,
|
|
21
23
|
} from '../virtualization/virtualizationHelpers';
|
|
22
24
|
|
|
23
|
-
import {
|
|
25
|
+
import { preloadEmoji } from './preloadEmoji';
|
|
24
26
|
import { useCategoryHeight } from './useCategoryHeight';
|
|
25
27
|
import { useIsEmojiDisallowed } from './useDisallowedEmojis';
|
|
26
28
|
import { useIsEmojiHidden } from './useIsEmojiHidden';
|
|
29
|
+
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
|
|
27
30
|
|
|
28
31
|
export function useEmojiVirtualization({
|
|
29
32
|
categoryEmojis,
|
|
@@ -53,31 +56,42 @@ export function useEmojiVirtualization({
|
|
|
53
56
|
// Native glyphs checked exactly: the cells this pass renders (visible
|
|
54
57
|
// rows plus virtualization's margin rows), nothing else.
|
|
55
58
|
const probe: string[] = [];
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
const preloads: DataEmoji[] = [];
|
|
60
|
+
const [committed, setCommitted] = React.useState(false);
|
|
58
61
|
|
|
59
62
|
// A section shows each rendered emoji once. Recents can hold the same
|
|
60
63
|
// emoji under two tones (e.g. neutral and medium); the active tone
|
|
61
64
|
// renders both as one identity, which must not be listed or keyed twice.
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const emojisToPush = React.useMemo(() => {
|
|
66
|
+
const rendered = new Set<string>();
|
|
67
|
+
return categoryEmojis.filter((emoji) => {
|
|
68
|
+
const isDisallowed = isEmojiDisallowed(emoji);
|
|
69
|
+
const { hidden } = isEmojiHidden(emoji);
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
if (hidden || isDisallowed) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const unified = emojiUnified(emoji, activeSkinTone);
|
|
75
|
+
if (rendered.has(unified)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
rendered.add(unified);
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
}, [categoryEmojis, activeSkinTone, isEmojiHidden, isEmojiDisallowed]);
|
|
77
82
|
|
|
78
|
-
const dimensions = useCategoryHeight(
|
|
83
|
+
const dimensions = useCategoryHeight(
|
|
84
|
+
emojisToPush.length,
|
|
85
|
+
isCategoryVisible && positioned,
|
|
86
|
+
);
|
|
79
87
|
|
|
88
|
+
// Guarantee a render even when a hidden picker has no measurement cell.
|
|
89
|
+
// A passive update avoids flushing all observer effects before first paint.
|
|
80
90
|
useEffect(() => {
|
|
91
|
+
if (!dimensions) setCommitted(true);
|
|
92
|
+
}, [dimensions]);
|
|
93
|
+
|
|
94
|
+
useIsomorphicLayoutEffect(() => {
|
|
81
95
|
if (dimensions) {
|
|
82
96
|
onHeightReady(dimensions.categoryHeight);
|
|
83
97
|
}
|
|
@@ -94,61 +108,83 @@ export function useEmojiVirtualization({
|
|
|
94
108
|
dimensions,
|
|
95
109
|
});
|
|
96
110
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
const needsPreload = (style: { top: number } | undefined) =>
|
|
112
|
+
emojiStyle !== EmojiStyle.NATIVE &&
|
|
113
|
+
dimensions &&
|
|
114
|
+
style &&
|
|
115
|
+
isJustBelowViewport(
|
|
116
|
+
topOffset + style.top,
|
|
117
|
+
scrollTop + (BodyRef.current?.clientHeight ?? 0),
|
|
118
|
+
dimensions.emojiSize,
|
|
119
|
+
);
|
|
100
120
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
emoji,
|
|
105
|
-
emojiStyle,
|
|
106
|
-
scrollTop,
|
|
107
|
-
BodyRef.current?.clientHeight ?? 0,
|
|
108
|
-
topOffset,
|
|
109
|
-
style,
|
|
110
|
-
dimensions,
|
|
111
|
-
getEmojiUrl,
|
|
112
|
-
);
|
|
113
|
-
return accumulator;
|
|
114
|
-
}
|
|
121
|
+
const waitingForLayout =
|
|
122
|
+
(!committed && !dimensions) ||
|
|
123
|
+
((!dimensions || !positioned) && BodyRef.current?.clientWidth);
|
|
115
124
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
// MeasureEmoji supplies geometry before cells are built. Layout-free
|
|
126
|
+
// environments can render after the first commit without measurements.
|
|
127
|
+
const emojis = waitingForLayout
|
|
128
|
+
? []
|
|
129
|
+
: emojisToPush.reduce((accumulator, emoji, index) => {
|
|
130
|
+
const style = getEmojiPositionStyle(dimensions, index);
|
|
120
131
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
unified=
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
132
|
+
if (isVirtualized(style)) {
|
|
133
|
+
if (needsPreload(style)) preloads.push(emoji);
|
|
134
|
+
return accumulator;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!isCategoryVisible) {
|
|
138
|
+
return accumulator;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const unified = emojiUnified(emoji, activeSkinTone);
|
|
142
|
+
// Wait for layout: an unmeasured pass renders a whole category, and
|
|
143
|
+
// while sections above are unmeasured this one renders off-screen
|
|
144
|
+
// cells as if it were at the top.
|
|
145
|
+
if (dimensions && positioned) probe.push(unified);
|
|
146
|
+
accumulator.push(
|
|
147
|
+
<ClickableEmoji
|
|
148
|
+
showVariations={showVariations}
|
|
149
|
+
key={unified}
|
|
150
|
+
emoji={emoji}
|
|
151
|
+
unified={unified}
|
|
152
|
+
emojiStyle={emojiStyle}
|
|
153
|
+
lazyLoad={lazyLoadEmojis}
|
|
154
|
+
getEmojiUrl={getEmojiUrl}
|
|
155
|
+
style={{
|
|
156
|
+
...style,
|
|
157
|
+
position: 'absolute',
|
|
158
|
+
}}
|
|
159
|
+
index={index}
|
|
160
|
+
// Grid semantics (issue #508): each emoji is a cell of its
|
|
161
|
+
// category row; native button activation is kept.
|
|
162
|
+
role="gridcell"
|
|
163
|
+
/>,
|
|
164
|
+
);
|
|
165
|
+
return accumulator;
|
|
166
|
+
}, [] as ReactNode[]);
|
|
146
167
|
|
|
147
168
|
// Every commit: requests for already-checked glyphs are free.
|
|
148
169
|
useEffect(() => requestNativeProbe(probe));
|
|
170
|
+
// Image requests belong to committed work, never an abandoned render.
|
|
171
|
+
// The proximity and cache checks run synchronously, without per-cell timers.
|
|
172
|
+
// Only the image a nearby cell will render is fetched: every skin tone of
|
|
173
|
+
// every nearby cell is hundreds of requests per scroll and long frames on
|
|
174
|
+
// slow devices. Focusing a cell (useOnFocus) fetches its variations.
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
preloads.forEach((emoji) =>
|
|
177
|
+
preloadEmoji(
|
|
178
|
+
getEmojiUrl,
|
|
179
|
+
emoji,
|
|
180
|
+
emojiStyle,
|
|
181
|
+
emojiUnified(emoji, activeSkinTone),
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
});
|
|
149
185
|
|
|
150
186
|
return {
|
|
151
|
-
virtualizedCounter,
|
|
187
|
+
virtualizedCounter: emojisToPush.length - emojis.length,
|
|
152
188
|
emojis,
|
|
153
189
|
dimensions,
|
|
154
190
|
emojiCount: emojisToPush.length,
|
package/src/hooks/useFilter.ts
CHANGED
|
@@ -100,13 +100,11 @@ function useApplySearch() {
|
|
|
100
100
|
if (!mountedRef.current) {
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
|
-
setSearchTerm(searchTerm
|
|
104
|
-
()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
);
|
|
103
|
+
setSearchTerm(searchTerm).then(() => {
|
|
104
|
+
if (registry.isCurrent(token)) {
|
|
105
|
+
scrollTo(PickerMainRef.current, 0, () => registry.isCurrent(token));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
110
108
|
});
|
|
111
109
|
};
|
|
112
110
|
}
|
|
@@ -115,7 +113,11 @@ export function useIsEmojiFiltered(): (unified: string) => boolean {
|
|
|
115
113
|
const { current: filter } = useFilterRef();
|
|
116
114
|
const [searchTerm] = useSearchTermState();
|
|
117
115
|
|
|
118
|
-
return (
|
|
116
|
+
return React.useCallback(
|
|
117
|
+
(unified: string) =>
|
|
118
|
+
isEmojiFilteredBySearchTerm(unified, filter, searchTerm),
|
|
119
|
+
[filter, searchTerm],
|
|
120
|
+
);
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
export function isEmojiFilteredBySearchTerm(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
useActiveSkinToneState,
|
|
3
5
|
useEmojisThatFailedToLoadState,
|
|
@@ -9,21 +11,23 @@ import { useIsEmojiFiltered } from './useFilter';
|
|
|
9
11
|
|
|
10
12
|
export function useIsEmojiHidden(): (emoji: DataEmoji) => IsHiddenReturn {
|
|
11
13
|
const [activeSkinTone] = useActiveSkinToneState();
|
|
12
|
-
|
|
14
|
+
// The tuple changes when its Root-local Set is mutated by an image error.
|
|
15
|
+
const failures = useEmojisThatFailedToLoadState();
|
|
13
16
|
const isEmojiFiltered = useIsEmojiFiltered();
|
|
14
17
|
|
|
15
|
-
return (
|
|
16
|
-
|
|
17
|
-
emojiUnified(emoji, activeSkinTone)
|
|
18
|
-
|
|
19
|
-
const filteredOut = isEmojiFiltered(emojiCanonicalUnified(emoji));
|
|
18
|
+
return useCallback(
|
|
19
|
+
(emoji: DataEmoji): IsHiddenReturn => {
|
|
20
|
+
const failedToLoad = failures[0].has(emojiUnified(emoji, activeSkinTone));
|
|
21
|
+
const filteredOut = isEmojiFiltered(emojiCanonicalUnified(emoji));
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
return {
|
|
24
|
+
failedToLoad,
|
|
25
|
+
filteredOut,
|
|
26
|
+
hidden: failedToLoad || filteredOut,
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
[activeSkinTone, failures, isEmojiFiltered],
|
|
30
|
+
);
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
type IsHiddenReturn = {
|
|
@@ -9,12 +9,12 @@ import {
|
|
|
9
9
|
import { getActiveElement } from '../DomUtils/getActiveElement';
|
|
10
10
|
import { isRtl } from '../DomUtils/isRtl';
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
focusFirstEmoji,
|
|
13
13
|
focusFirstVisibleEmoji,
|
|
14
14
|
focusAdjacentEmoji,
|
|
15
15
|
} from '../DomUtils/keyboardNavigation';
|
|
16
16
|
import { useScrollTo } from '../DomUtils/scrollTo';
|
|
17
|
-
import { buttonFromTarget } from '../DomUtils/selectors';
|
|
17
|
+
import { buttonFromTarget, firstVisibleEmoji } from '../DomUtils/selectors';
|
|
18
18
|
import {
|
|
19
19
|
ElementRef,
|
|
20
20
|
useBodyRef,
|
|
@@ -27,9 +27,16 @@ import {
|
|
|
27
27
|
import {
|
|
28
28
|
useNavigationRegistry,
|
|
29
29
|
useSearchComposingState,
|
|
30
|
+
useSearchDisplayState,
|
|
31
|
+
useSearchTermState,
|
|
30
32
|
useSkinToneFanOpenState,
|
|
31
33
|
} from '../components/context/PickerContext';
|
|
32
|
-
import {
|
|
34
|
+
import { usePickerDataContext } from '../components/context/PickerDataContext';
|
|
35
|
+
import {
|
|
36
|
+
useSearchDisabledConfig,
|
|
37
|
+
useSuggestedEmojisConfig,
|
|
38
|
+
} from '../config/useConfig';
|
|
39
|
+
import { normalizeQuery } from '../data-core/prepare';
|
|
33
40
|
import {
|
|
34
41
|
NavigationRegionKind,
|
|
35
42
|
RegisteredRegion,
|
|
@@ -47,7 +54,11 @@ import {
|
|
|
47
54
|
useFocusSkinTonePicker,
|
|
48
55
|
} from './useFocus';
|
|
49
56
|
import useIsSearchMode from './useIsSearchMode';
|
|
50
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
useAcceptedSearchValue,
|
|
59
|
+
useClearSearchValue,
|
|
60
|
+
useTypeToSearchKey,
|
|
61
|
+
} from './useSearchController';
|
|
51
62
|
import useSetVariationPicker from './useSetVariationPicker';
|
|
52
63
|
|
|
53
64
|
export function useKeyboardNavigation() {
|
|
@@ -76,6 +87,65 @@ export function useKeyboardNavigation() {
|
|
|
76
87
|
const focusPrevRegionFromGrid = useFocusPrevRegionFrom('grid');
|
|
77
88
|
const setVariationPicker = useSetVariationPicker();
|
|
78
89
|
const registry = useNavigationRegistry();
|
|
90
|
+
const [searchTerm] = useSearchTermState();
|
|
91
|
+
const suggestions = JSON.stringify(useSuggestedEmojisConfig());
|
|
92
|
+
const { emojiData } = usePickerDataContext();
|
|
93
|
+
const searchValue = useAcceptedSearchValue();
|
|
94
|
+
// A controlled parent may not have accepted what the input shows yet.
|
|
95
|
+
const [displayed] = useSearchDisplayState();
|
|
96
|
+
// Enter pressed while the input is ahead of the debounced results: the
|
|
97
|
+
// query it was pressed for, selected once those results commit.
|
|
98
|
+
const pendingPick = useRef<string | null>(null);
|
|
99
|
+
const pickEmoji = (emoji: HTMLElement | null) => {
|
|
100
|
+
pendingPick.current = null;
|
|
101
|
+
focusElement(emoji);
|
|
102
|
+
emoji?.click();
|
|
103
|
+
};
|
|
104
|
+
const selectFirstResult = () => {
|
|
105
|
+
const query = pendingPick.current;
|
|
106
|
+
const generation = registry.currentGeneration();
|
|
107
|
+
if (
|
|
108
|
+
!focusFirstEmoji(BodyRef.current, {
|
|
109
|
+
shouldFocus: () =>
|
|
110
|
+
pendingPick.current === query &&
|
|
111
|
+
SearchInputRef.current?.value === query &&
|
|
112
|
+
registry.isCurrent(generation),
|
|
113
|
+
moveTo: pickEmoji,
|
|
114
|
+
})
|
|
115
|
+
)
|
|
116
|
+
pendingPick.current = null;
|
|
117
|
+
};
|
|
118
|
+
// Each committed result set settles a queued Enter for its query.
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const pending = pendingPick.current;
|
|
121
|
+
pendingPick.current = null;
|
|
122
|
+
if (
|
|
123
|
+
pending !== null &&
|
|
124
|
+
pending === displayed &&
|
|
125
|
+
normalizeQuery(pending) === searchTerm
|
|
126
|
+
) {
|
|
127
|
+
pendingPick.current = pending;
|
|
128
|
+
selectFirstResult();
|
|
129
|
+
}
|
|
130
|
+
// Per committed result set; selection reads live refs.
|
|
131
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
|
+
}, [searchTerm, suggestions, emojiData]);
|
|
133
|
+
// Enter in search selects the first result, even one scrolled out of view.
|
|
134
|
+
const selectFromSearch = () => {
|
|
135
|
+
// Remember fast Enter while the input is ahead of the debounced grid.
|
|
136
|
+
if (
|
|
137
|
+
![searchValue, displayed].every(
|
|
138
|
+
(value) => normalizeQuery(value) === searchTerm,
|
|
139
|
+
)
|
|
140
|
+
) {
|
|
141
|
+
pendingPick.current = displayed;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (isSearchMode) {
|
|
145
|
+
pendingPick.current = displayed;
|
|
146
|
+
selectFirstResult();
|
|
147
|
+
} else pickEmoji(firstVisibleEmoji(BodyRef.current));
|
|
148
|
+
};
|
|
79
149
|
const goDownFromSearchInput = () => {
|
|
80
150
|
if (isSearchMode) return focusFirstVisibleEmoji(BodyRef.current);
|
|
81
151
|
focusCategoryNavigation();
|
|
@@ -90,14 +160,21 @@ export function useKeyboardNavigation() {
|
|
|
90
160
|
disallowMouseMove();
|
|
91
161
|
switch (key) {
|
|
92
162
|
case 'Escape':
|
|
93
|
-
event.preventDefault();
|
|
94
163
|
if (hasOpenToggles()) {
|
|
164
|
+
event.preventDefault();
|
|
95
165
|
closeAllOpenToggles();
|
|
96
166
|
// A nested variation/tone menu consumes Escape before the
|
|
97
167
|
// host popover or autocomplete dismisses the whole picker.
|
|
98
168
|
event.stopPropagation();
|
|
99
169
|
return;
|
|
100
170
|
}
|
|
171
|
+
// Clearing a search consumes the key. With nothing to clear, the
|
|
172
|
+
// event stays untouched (this runs in window capture, before host
|
|
173
|
+
// listeners that skip default-prevented keys, such as Radix).
|
|
174
|
+
if (searchValue || displayed) {
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
event.stopPropagation();
|
|
177
|
+
}
|
|
101
178
|
clearSearch();
|
|
102
179
|
scrollTo(0);
|
|
103
180
|
focusSearchInput();
|
|
@@ -129,7 +206,7 @@ export function useKeyboardNavigation() {
|
|
|
129
206
|
break;
|
|
130
207
|
case 'Enter':
|
|
131
208
|
event.preventDefault();
|
|
132
|
-
|
|
209
|
+
selectFromSearch();
|
|
133
210
|
break;
|
|
134
211
|
}
|
|
135
212
|
};
|
|
@@ -245,20 +322,15 @@ export function useKeyboardNavigation() {
|
|
|
245
322
|
}
|
|
246
323
|
};
|
|
247
324
|
const [composing] = useSearchComposingState();
|
|
248
|
-
const handler = useRef<(event: KeyboardEvent,
|
|
325
|
+
const handler = useRef<(event: KeyboardEvent, escapeOnly?: boolean) => void>(
|
|
249
326
|
() => {},
|
|
250
327
|
);
|
|
251
328
|
// One listener owns the native lifecycle; current callbacks supply state
|
|
252
329
|
// and live region refs without reattaching on every picker render.
|
|
253
|
-
handler.current = (event,
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
event.keyCode === 229 ||
|
|
258
|
-
(menusOnly && !hasOpenToggles())
|
|
259
|
-
)
|
|
260
|
-
return;
|
|
261
|
-
if (!menusOnly) {
|
|
330
|
+
handler.current = (event, escapeOnly) => {
|
|
331
|
+
if (composing || event.isComposing || event.keyCode === 229) return;
|
|
332
|
+
if (event.key !== 'Enter') pendingPick.current = null;
|
|
333
|
+
if (!escapeOnly) {
|
|
262
334
|
search(event);
|
|
263
335
|
tone(event);
|
|
264
336
|
categories(event);
|
|
@@ -272,16 +344,31 @@ export function useKeyboardNavigation() {
|
|
|
272
344
|
// Descendant parts are read through live refs by the delegated handler.
|
|
273
345
|
const current = ref.current;
|
|
274
346
|
if (!current) return;
|
|
347
|
+
// Escape runs once, in the window capture phase, so a consumed key is
|
|
348
|
+
// stopped before host dismissal listeners (Radix captures on document).
|
|
349
|
+
let captured: KeyboardEvent | undefined;
|
|
275
350
|
const scoped = (event: KeyboardEvent) => {
|
|
351
|
+
if (event === captured) return;
|
|
276
352
|
if (eventBelongsToPicker(event, current)) handler.current(event);
|
|
277
353
|
};
|
|
278
354
|
const capture = (event: KeyboardEvent) => {
|
|
279
|
-
if (event.key === 'Escape' && eventBelongsToPicker(event, current))
|
|
355
|
+
if (event.key === 'Escape' && eventBelongsToPicker(event, current)) {
|
|
356
|
+
captured = event;
|
|
280
357
|
handler.current(event, true);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
// Editing the query (paste, autocorrect) or leaving the picker cancels
|
|
361
|
+
// a queued Enter.
|
|
362
|
+
const cancelPendingPick = () => {
|
|
363
|
+
pendingPick.current = null;
|
|
281
364
|
};
|
|
365
|
+
current.addEventListener('input', cancelPendingPick);
|
|
366
|
+
current.addEventListener('focusout', cancelPendingPick);
|
|
282
367
|
current.addEventListener('keydown', scoped);
|
|
283
368
|
window.addEventListener('keydown', capture, true);
|
|
284
369
|
return () => {
|
|
370
|
+
current.removeEventListener('input', cancelPendingPick);
|
|
371
|
+
current.removeEventListener('focusout', cancelPendingPick);
|
|
285
372
|
current.removeEventListener('keydown', scoped);
|
|
286
373
|
window.removeEventListener('keydown', capture, true);
|
|
287
374
|
};
|
|
@@ -46,6 +46,9 @@ const boxSizingReset =
|
|
|
46
46
|
// vertical tab rail still adds its own width beside the grid.
|
|
47
47
|
':where([data-epr-columns] :is([data-epr-part=preview],[data-epr-part=search],[aria-orientation=horizontal])){contain:inline-size}' +
|
|
48
48
|
':where([data-epr-columns] [data-epr-part=viewport]){scrollbar-gutter:stable}' +
|
|
49
|
+
// A permanent sizing row keeps intrinsic width when all categories are
|
|
50
|
+
// hidden (no matches) or the dataset is still loading/has failed.
|
|
51
|
+
':where([data-epr-columns] [data-epr-part=list])::before{content:"";display:block;height:0;width:calc(var(--epr-columns)*var(--epr-emoji-fullsize) + 2*var(--epr-horizontal-padding));max-width:100%}' +
|
|
49
52
|
':where([data-epr-columns] [data-epr-part=category-content]){width:calc(var(--epr-columns)*var(--epr-emoji-fullsize));min-width:calc(100% - 2*var(--epr-horizontal-padding));max-width:calc(100% - 2*var(--epr-horizontal-padding))}';
|
|
50
53
|
|
|
51
54
|
export const structuralStyles = /* @__PURE__ */ (() =>
|
package/src/primitives/tokens.ts
CHANGED
|
@@ -140,6 +140,7 @@ export const darkPickerTokens: Record<string, string> = {
|
|
|
140
140
|
|
|
141
141
|
/** Full light preset: geometry plus light colors (v4 defaults). */
|
|
142
142
|
export const defaultPickerTokens: Record<string, string> = {
|
|
143
|
+
'--epr-font-family': 'sans-serif',
|
|
143
144
|
...lightPickerTokens,
|
|
144
145
|
...structuralPickerTokens,
|
|
145
146
|
};
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export const OVERSCAN_ROWS = 2;
|
|
2
|
+
|
|
3
|
+
export function isJustBelowViewport(top: number, bottom: number, size: number) {
|
|
4
|
+
return top >= bottom && top < bottom + size * OVERSCAN_ROWS;
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
export function shouldVirtualize({
|
|
2
8
|
scrollTop,
|
|
3
9
|
clientHeight,
|
|
@@ -19,8 +25,9 @@ export function shouldVirtualize({
|
|
|
19
25
|
const emojiBottom = emojiTop + dimensions.emojiSize;
|
|
20
26
|
|
|
21
27
|
const isVisible =
|
|
22
|
-
emojiBottom + dimensions.emojiSize *
|
|
23
|
-
emojiTop <=
|
|
28
|
+
emojiBottom + dimensions.emojiSize * OVERSCAN_ROWS >= scrollTop &&
|
|
29
|
+
emojiTop <=
|
|
30
|
+
scrollTop + clientHeight + dimensions.emojiSize * (OVERSCAN_ROWS - 1);
|
|
24
31
|
|
|
25
32
|
return !isVisible;
|
|
26
33
|
}
|