@fibery/ui-kit 1.14.0 → 1.16.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/package.json +4 -3
- package/src/antd/styles.ts +21 -4
- package/src/designSystem.ts +18 -28
- package/src/emoji-picker/app-icon-picker.tsx +31 -26
- package/src/emoji-picker/emoji-picker-content-with-color.tsx +107 -0
- package/src/emoji-picker/emoji-picker.tsx +34 -37
- package/src/emoji-picker/emoji.tsx +4 -7
- package/src/emoji-picker/icon-emoji-picker.tsx +17 -8
- package/src/emoji-picker/primitives/category.tsx +95 -25
- package/src/emoji-picker/primitives/emoji.tsx +37 -78
- package/src/emoji-picker/primitives/footer.tsx +3 -1
- package/src/emoji-picker/primitives/grid.tsx +53 -23
- package/src/emoji-picker/primitives/root.tsx +13 -3
- package/src/emoji-picker/primitives/search.tsx +38 -20
- package/src/emoji-picker/primitives/skin-tone.tsx +21 -11
- package/src/emoji-picker/stores/emoji-data-store.tsx +0 -1
- package/src/emoji-picker/stores/lazy-icon-data-store.tsx +44 -0
- package/src/emoji-picker/stores/static-emoji-data-store.tsx +16 -0
- package/src/emoji-picker/utils/emoji-set.ts +1 -1
- package/src/icons/ast/AiAssistant.ts +1 -1
- package/src/icons/ast/AiAvatar.ts +1 -1
- package/src/icons/ast/AppTemplates.ts +1 -1
- package/src/icons/ast/BellCircle.ts +8 -0
- package/src/icons/ast/BellX.ts +8 -0
- package/src/icons/ast/Bolt.ts +8 -0
- package/src/icons/ast/CheckCircle.ts +8 -0
- package/src/icons/ast/ClockForward.ts +8 -0
- package/src/icons/ast/ColorCodingFilled.ts +8 -0
- package/src/icons/ast/Delete.ts +1 -1
- package/src/icons/ast/EmojiDelete.ts +8 -0
- package/src/icons/ast/ExtensionAssignments.ts +1 -1
- package/src/icons/ast/Fields.ts +1 -1
- package/src/icons/ast/Figma.ts +8 -0
- package/src/icons/ast/GoogleDrive.ts +8 -0
- package/src/icons/ast/GoogleMap.ts +8 -0
- package/src/icons/ast/InvitePeople.ts +1 -1
- package/src/icons/ast/LineDivider.ts +8 -0
- package/src/icons/ast/Loom.ts +8 -0
- package/src/icons/ast/Miro.ts +8 -0
- package/src/icons/ast/Mixpanel.ts +8 -0
- package/src/icons/ast/Shared.ts +1 -1
- package/src/icons/ast/Twitter.ts +8 -0
- package/src/icons/ast/Youtube.ts +8 -0
- package/src/icons/ast/index.tsx +16 -0
- package/src/icons/react/BellCircle.tsx +12 -0
- package/src/icons/react/BellX.tsx +12 -0
- package/src/icons/react/Bolt.tsx +12 -0
- package/src/icons/react/CheckCircle.tsx +12 -0
- package/src/icons/react/ClockForward.tsx +12 -0
- package/src/icons/react/ColorCodingFilled.tsx +12 -0
- package/src/icons/react/EmojiDelete.tsx +12 -0
- package/src/icons/react/Figma.tsx +12 -0
- package/src/icons/react/GoogleDrive.tsx +12 -0
- package/src/icons/react/GoogleMap.tsx +12 -0
- package/src/icons/react/LineDivider.tsx +12 -0
- package/src/icons/react/Loom.tsx +12 -0
- package/src/icons/react/Miro.tsx +12 -0
- package/src/icons/react/Mixpanel.tsx +12 -0
- package/src/icons/react/Twitter.tsx +12 -0
- package/src/icons/react/Youtube.tsx +12 -0
- package/src/icons/react/index.tsx +16 -0
- package/src/Button/icon-button-with-tooltip.tsx +0 -15
- package/src/emoji-picker/index.tsx +0 -5
- package/src/emoji-picker/lazy-icon-data-store.tsx +0 -55
- /package/src/emoji-picker/{lazy-emoji-data-store.tsx → stores/lazy-emoji-data-store.tsx} +0 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {Category} from "@fibery/emoji-data";
|
|
2
|
-
import {
|
|
1
|
+
import {Category, EmojiItem} from "@fibery/emoji-data";
|
|
2
|
+
import {css} from "@linaria/core";
|
|
3
|
+
import {memo} from "react";
|
|
4
|
+
import {useInView} from "react-intersection-observer";
|
|
5
|
+
import {IconButton} from "../../Button/icon-button";
|
|
3
6
|
import {space, textStyles, themeVars} from "../../designSystem";
|
|
7
|
+
import {Settings} from "../../icons/react";
|
|
8
|
+
import {useEmojiDataStoreSelector} from "../stores/emoji-data-store";
|
|
4
9
|
import {useEmojiPickerContentSettings} from "./content";
|
|
5
10
|
import {Emoji} from "./emoji";
|
|
6
11
|
import {emojiPadding} from "./layout";
|
|
@@ -17,7 +22,10 @@ const categoryLabelCss = css`
|
|
|
17
22
|
position: sticky;
|
|
18
23
|
top: 0;
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: ${space.xs}px;
|
|
28
|
+
|
|
21
29
|
z-index: 1;
|
|
22
30
|
`;
|
|
23
31
|
|
|
@@ -55,46 +63,108 @@ const NotFound = () => {
|
|
|
55
63
|
);
|
|
56
64
|
};
|
|
57
65
|
|
|
58
|
-
type
|
|
59
|
-
|
|
66
|
+
type CategoryItemProps = {
|
|
67
|
+
id: string;
|
|
68
|
+
/** Custom renderer for emoji items */
|
|
69
|
+
renderEmoji?: (args: {emoji: EmojiItem}) => React.ReactNode;
|
|
60
70
|
};
|
|
61
|
-
|
|
71
|
+
|
|
72
|
+
const CategoryItem: React.FC<CategoryItemProps> = ({id, renderEmoji}) => {
|
|
73
|
+
const skin = useEmojiSkin();
|
|
74
|
+
const emojiPickerCtx = useEmojiPickerCtx();
|
|
75
|
+
const emojiContentSettings = useEmojiPickerContentSettings();
|
|
76
|
+
|
|
77
|
+
const emoji = useEmojiDataStoreSelector((store) => store.get(id));
|
|
78
|
+
|
|
79
|
+
if (!emoji) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// TODO: title should be shown in preview in footer
|
|
84
|
+
const title = emoji.shortNames ? emoji.shortNames[0] : undefined;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<IconButton
|
|
88
|
+
size="big"
|
|
89
|
+
onClick={() => emojiPickerCtx.onEmojiSelect(emoji)}
|
|
90
|
+
className={css`
|
|
91
|
+
&:hover:not(:disabled),
|
|
92
|
+
&:focus-visible {
|
|
93
|
+
background-color: var(--fibery-emoji-hover-color, ${themeVars.badgeBgColor});
|
|
94
|
+
}
|
|
95
|
+
`}
|
|
96
|
+
title={title}
|
|
97
|
+
aria-label={emoji.name}
|
|
98
|
+
>
|
|
99
|
+
{renderEmoji ? (
|
|
100
|
+
renderEmoji({emoji})
|
|
101
|
+
) : (
|
|
102
|
+
<Emoji
|
|
103
|
+
emoji={id}
|
|
104
|
+
size={emojiContentSettings.emojiSize}
|
|
105
|
+
version={emojiPickerCtx.version}
|
|
106
|
+
skin={skin}
|
|
107
|
+
native={emojiPickerCtx.native}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
</IconButton>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type EmojiPickerCategoryProps = {
|
|
115
|
+
category: Category;
|
|
116
|
+
|
|
117
|
+
preRender?: boolean;
|
|
118
|
+
scrollParent: Element | null;
|
|
119
|
+
} & Pick<CategoryItemProps, "renderEmoji">;
|
|
120
|
+
|
|
121
|
+
export const EmojiPickerCategory = memo<EmojiPickerCategoryProps>(function EmojiPickerCategoryComponent({
|
|
122
|
+
category,
|
|
123
|
+
renderEmoji,
|
|
124
|
+
scrollParent,
|
|
125
|
+
preRender = false,
|
|
126
|
+
}) {
|
|
62
127
|
const i18n = useEmojiPickerI18N();
|
|
63
128
|
const emojiPickerCtx = useEmojiPickerCtx();
|
|
64
129
|
const emojiContentSettings = useEmojiPickerContentSettings();
|
|
65
130
|
|
|
131
|
+
const {ref, inView} = useInView({
|
|
132
|
+
root: scrollParent,
|
|
133
|
+
rootMargin: `129px 0px`, // grid height / 2
|
|
134
|
+
initialInView: preRender,
|
|
135
|
+
fallbackInView: true,
|
|
136
|
+
triggerOnce: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
66
139
|
const label = i18n.categories[category.id as keyof (typeof i18n)["categories"]] || category.name;
|
|
67
140
|
|
|
68
|
-
const
|
|
141
|
+
const emojiSize = emojiContentSettings.emojiSize + emojiPadding * 2;
|
|
69
142
|
|
|
70
143
|
return (
|
|
71
144
|
<section className={categoryCss} aria-label={label}>
|
|
72
145
|
<div className={categoryLabelCss}>
|
|
73
|
-
|
|
146
|
+
{/* already labeled by the section aria-label */}
|
|
147
|
+
<span aria-hidden={true}>{label}</span>
|
|
148
|
+
|
|
149
|
+
{category.id === "custom" && emojiPickerCtx.onCustomCategorySettings && (
|
|
150
|
+
<IconButton onClick={emojiPickerCtx.onCustomCategorySettings}>
|
|
151
|
+
<Settings iconSize={16} />
|
|
152
|
+
</IconButton>
|
|
153
|
+
)}
|
|
74
154
|
</div>
|
|
75
155
|
|
|
76
156
|
{category.emojis.length > 0 ? (
|
|
77
|
-
<ul className={categoryListCss}>
|
|
157
|
+
<ul ref={ref} className={categoryListCss}>
|
|
78
158
|
{category.emojis.map((id) => {
|
|
79
159
|
return (
|
|
80
160
|
<li
|
|
81
|
-
style={
|
|
82
|
-
// css-only virtualization until implementing the real one
|
|
83
|
-
{
|
|
84
|
-
contentVisibility: "auto",
|
|
85
|
-
containIntrinsicSize: `${emojiContentSettings.emojiSize + emojiPadding * 2}px`,
|
|
86
|
-
} as CSSProperties
|
|
87
|
-
}
|
|
88
161
|
key={id}
|
|
162
|
+
style={{
|
|
163
|
+
width: emojiSize,
|
|
164
|
+
height: emojiSize,
|
|
165
|
+
}}
|
|
89
166
|
>
|
|
90
|
-
<
|
|
91
|
-
onClick={emojiPickerCtx.onEmojiSelect}
|
|
92
|
-
emoji={id}
|
|
93
|
-
forceSize
|
|
94
|
-
version={emojiPickerCtx.version}
|
|
95
|
-
skin={skin}
|
|
96
|
-
tooltip
|
|
97
|
-
/>
|
|
167
|
+
{inView ? <CategoryItem id={id} renderEmoji={renderEmoji} /> : null}
|
|
98
168
|
</li>
|
|
99
169
|
);
|
|
100
170
|
})}
|
|
@@ -104,4 +174,4 @@ export const EmojiPickerCategory: React.FC<EmojiPickerCategoryProps> = ({categor
|
|
|
104
174
|
)}
|
|
105
175
|
</section>
|
|
106
176
|
);
|
|
107
|
-
};
|
|
177
|
+
});
|
|
@@ -1,57 +1,48 @@
|
|
|
1
1
|
import {EmojiItem, EmojiSkin} from "@fibery/emoji-data";
|
|
2
2
|
import {css, cx} from "@linaria/core";
|
|
3
|
-
import {
|
|
3
|
+
import {themeVars} from "../../designSystem";
|
|
4
4
|
import {useEmojiDataStoreSelector} from "../stores/emoji-data-store";
|
|
5
|
-
import {emojiPadding} from "./layout";
|
|
6
5
|
|
|
7
6
|
const emojiWrapperCss = css`
|
|
7
|
+
color: ${themeVars.textColor};
|
|
8
|
+
font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla",
|
|
9
|
+
"Noto Color Emoji", "Android Emoji";
|
|
10
|
+
line-height: 1;
|
|
11
|
+
`;
|
|
12
|
+
const emojiWrapperSizedCss = css`
|
|
8
13
|
display: inline-flex;
|
|
9
14
|
align-items: center;
|
|
10
15
|
justify-content: center;
|
|
11
|
-
color: ${themeVars.textColor};
|
|
12
16
|
`;
|
|
13
17
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
18
|
+
const emojiInlineImgCss = css`
|
|
19
|
+
display: inline-block;
|
|
20
|
+
width: 1em;
|
|
21
|
+
height: 1em;
|
|
22
|
+
vertical-align: -0.1em;
|
|
23
|
+
object-fit: contain;
|
|
24
|
+
`;
|
|
25
|
+
const emojiSizedImgCss = css`
|
|
26
|
+
width: 100%;
|
|
27
|
+
height: 100%;
|
|
28
|
+
object-fit: contain;
|
|
26
29
|
`;
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
width: size,
|
|
31
|
-
height: size,
|
|
32
|
-
});
|
|
31
|
+
// This is needed to keep text-selection working for image based emojis Details: https://the.fibery.io/SoftDev/bug/Improve-selection-text-with-emojis-8963
|
|
32
|
+
const emptyPixel = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
|
|
33
33
|
|
|
34
34
|
export type EmojiProps = {
|
|
35
35
|
size?: number;
|
|
36
36
|
/** id or colons */
|
|
37
37
|
emoji: string;
|
|
38
|
-
tooltip?: boolean;
|
|
39
38
|
/** max supported version of emojis */
|
|
40
39
|
version?: number;
|
|
41
40
|
onClick?: (emoji: EmojiItem, event: React.MouseEvent) => void;
|
|
42
41
|
skin?: EmojiSkin;
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
/** render emoji as native unicode if possible */
|
|
43
|
+
native?: boolean;
|
|
45
44
|
};
|
|
46
|
-
export const Emoji: React.FC<EmojiProps> = ({
|
|
47
|
-
emoji: idOrColons,
|
|
48
|
-
size = 20,
|
|
49
|
-
tooltip,
|
|
50
|
-
forceSize = false,
|
|
51
|
-
onClick,
|
|
52
|
-
version = 14,
|
|
53
|
-
skin,
|
|
54
|
-
}) => {
|
|
45
|
+
export const Emoji: React.FC<EmojiProps> = ({emoji: idOrColons, size, version = 14, skin, native}) => {
|
|
55
46
|
const emoji = useEmojiDataStoreSelector((store) => store.get(idOrColons, {skin}));
|
|
56
47
|
|
|
57
48
|
if (!emoji) {
|
|
@@ -61,33 +52,15 @@ export const Emoji: React.FC<EmojiProps> = ({
|
|
|
61
52
|
let emojiNode: React.ReactNode;
|
|
62
53
|
|
|
63
54
|
if ("native" in emoji) {
|
|
64
|
-
if (version >= emoji.version) {
|
|
65
|
-
emojiNode =
|
|
66
|
-
<span
|
|
67
|
-
style={{
|
|
68
|
-
fontSize: size,
|
|
69
|
-
fontFamily:
|
|
70
|
-
'"Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "Android Emoji"',
|
|
71
|
-
|
|
72
|
-
...(forceSize
|
|
73
|
-
? {
|
|
74
|
-
...getSizeStyle(size),
|
|
75
|
-
textAlign: "center",
|
|
76
|
-
lineHeight: 1,
|
|
77
|
-
wordBreak: "keep-all",
|
|
78
|
-
}
|
|
79
|
-
: {}),
|
|
80
|
-
}}
|
|
81
|
-
>
|
|
82
|
-
{emoji.native}
|
|
83
|
-
</span>
|
|
84
|
-
);
|
|
55
|
+
if (native && version >= emoji.version) {
|
|
56
|
+
emojiNode = emoji.native;
|
|
85
57
|
} else {
|
|
86
58
|
emojiNode = (
|
|
87
|
-
<
|
|
59
|
+
<img
|
|
60
|
+
alt={emoji.name}
|
|
61
|
+
className={cx(size ? emojiSizedImgCss : emojiInlineImgCss)}
|
|
62
|
+
src={emptyPixel}
|
|
88
63
|
style={{
|
|
89
|
-
...getSizeStyle(size),
|
|
90
|
-
|
|
91
64
|
backgroundImage: `url(${emoji.sprite.src})`,
|
|
92
65
|
backgroundSize: emoji.sprite.size,
|
|
93
66
|
backgroundPosition: emoji.sprite.position,
|
|
@@ -97,33 +70,19 @@ export const Emoji: React.FC<EmojiProps> = ({
|
|
|
97
70
|
}
|
|
98
71
|
} else {
|
|
99
72
|
emojiNode = (
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
backgroundRepeat: "no-repeat",
|
|
106
|
-
backgroundPosition: "center",
|
|
107
|
-
}}
|
|
73
|
+
<img
|
|
74
|
+
alt={emoji.name}
|
|
75
|
+
loading="lazy"
|
|
76
|
+
className={cx(size ? emojiSizedImgCss : emojiInlineImgCss)}
|
|
77
|
+
src={emoji.imageUrl}
|
|
108
78
|
/>
|
|
109
79
|
);
|
|
110
80
|
}
|
|
111
81
|
|
|
112
|
-
|
|
113
|
-
const title = tooltip ? emoji.id : undefined;
|
|
82
|
+
const style = size ? {fontSize: size, width: size, height: size} : {};
|
|
114
83
|
|
|
115
|
-
return
|
|
116
|
-
<
|
|
117
|
-
type="button"
|
|
118
|
-
onClick={(e) => onClick(emoji, e)}
|
|
119
|
-
className={cx(emojiWrapperCss, clickableCss)}
|
|
120
|
-
title={title}
|
|
121
|
-
aria-label={emoji.name}
|
|
122
|
-
>
|
|
123
|
-
{emojiNode}
|
|
124
|
-
</button>
|
|
125
|
-
) : (
|
|
126
|
-
<span className={emojiWrapperCss} title={title} aria-label={emoji.name}>
|
|
84
|
+
return (
|
|
85
|
+
<span style={style} className={cx(emojiWrapperCss, size && emojiWrapperSizedCss)} aria-label={emoji.name}>
|
|
127
86
|
{emojiNode}
|
|
128
87
|
</span>
|
|
129
88
|
);
|
|
@@ -2,8 +2,10 @@ import {css} from "@linaria/core";
|
|
|
2
2
|
import {space} from "../../designSystem";
|
|
3
3
|
|
|
4
4
|
const footerCss = css`
|
|
5
|
+
position: relative;
|
|
5
6
|
border-top: 1px solid var(--fibery-color-separatorColor);
|
|
6
|
-
padding:
|
|
7
|
+
padding: ${space.m}px ${space.l}px;
|
|
8
|
+
min-height: 40px;
|
|
7
9
|
`;
|
|
8
10
|
|
|
9
11
|
export const EmojiPickerFooter: React.FC<React.PropsWithChildren> = ({children}) => {
|
|
@@ -1,45 +1,71 @@
|
|
|
1
|
-
import {css} from "@linaria/core";
|
|
2
|
-
import {useLayoutEffect, useMemo,
|
|
3
|
-
import {useEmojiDataStoreSelector} from "../stores/emoji-data-store";
|
|
1
|
+
import {css, cx} from "@linaria/core";
|
|
2
|
+
import {useLayoutEffect, useMemo, useState} from "react";
|
|
3
|
+
import {useEmojiDataStore, useEmojiDataStoreSelector} from "../stores/emoji-data-store";
|
|
4
4
|
import {get as getFrequentlyUsed} from "../utils/frequently";
|
|
5
|
-
import {EmojiPickerCategory} from "./category";
|
|
5
|
+
import {EmojiPickerCategory, EmojiPickerCategoryProps} from "./category";
|
|
6
6
|
import {useEmojiPickerContentSettings} from "./content";
|
|
7
7
|
import {contentHorizontalPadding} from "./layout";
|
|
8
8
|
import {useFoundEmojis} from "./search-provider";
|
|
9
9
|
|
|
10
10
|
const gridWrapperCss = css`
|
|
11
|
-
height:
|
|
11
|
+
height: 258px;
|
|
12
12
|
overflow-y: scroll;
|
|
13
13
|
overflow-x: hidden;
|
|
14
14
|
|
|
15
15
|
padding: 0 ${contentHorizontalPadding}px;
|
|
16
16
|
`;
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
type AllCategoriesProps = Pick<EmojiPickerCategoryProps, "renderEmoji" | "scrollParent">;
|
|
19
|
+
const AllCategories: React.FC<AllCategoriesProps> = ({renderEmoji, scrollParent}) => {
|
|
19
20
|
const contentSettings = useEmojiPickerContentSettings();
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
const [recentCategory] = useState(() => ({
|
|
23
|
-
id: "recent",
|
|
24
|
-
name: "Recent",
|
|
25
|
-
emojis: getFrequentlyUsed(contentSettings.perLine),
|
|
26
|
-
}));
|
|
27
|
-
|
|
22
|
+
const emojiData = useEmojiDataStore();
|
|
28
23
|
const categories = useEmojiDataStoreSelector((store) => store.getCategories());
|
|
29
24
|
|
|
25
|
+
const recentCategory = useMemo(
|
|
26
|
+
() => ({
|
|
27
|
+
id: "recent",
|
|
28
|
+
name: "Recent",
|
|
29
|
+
emojis: getFrequentlyUsed(contentSettings.perLine).filter((id) => {
|
|
30
|
+
const emoji = emojiData.get(id);
|
|
31
|
+
const hidden = emoji && "hidden" in emoji ? emoji.hidden : false;
|
|
32
|
+
return emoji && !hidden;
|
|
33
|
+
}),
|
|
34
|
+
}),
|
|
35
|
+
[contentSettings, emojiData]
|
|
36
|
+
);
|
|
37
|
+
|
|
30
38
|
return (
|
|
31
39
|
<>
|
|
32
|
-
{recentCategory.emojis.length > 0 ?
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
{recentCategory.emojis.length > 0 ? (
|
|
41
|
+
<EmojiPickerCategory
|
|
42
|
+
preRender
|
|
43
|
+
category={recentCategory}
|
|
44
|
+
scrollParent={scrollParent}
|
|
45
|
+
renderEmoji={renderEmoji}
|
|
46
|
+
/>
|
|
47
|
+
) : null}
|
|
48
|
+
{categories.map((category, idx) => {
|
|
49
|
+
return (
|
|
50
|
+
<EmojiPickerCategory
|
|
51
|
+
preRender={idx < 2}
|
|
52
|
+
key={category.id}
|
|
53
|
+
category={category}
|
|
54
|
+
scrollParent={scrollParent}
|
|
55
|
+
renderEmoji={renderEmoji}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
35
58
|
})}
|
|
36
59
|
</>
|
|
37
60
|
);
|
|
38
61
|
};
|
|
62
|
+
export type EmojiPickerGridProps = {
|
|
63
|
+
className?: string;
|
|
64
|
+
} & Pick<EmojiPickerCategoryProps, "renderEmoji">;
|
|
39
65
|
|
|
40
|
-
export const EmojiPickerGrid = () => {
|
|
66
|
+
export const EmojiPickerGrid: React.FC<EmojiPickerGridProps> = ({className, renderEmoji}) => {
|
|
41
67
|
const foundEmojis = useFoundEmojis();
|
|
42
|
-
const
|
|
68
|
+
const [gridElement, setGridElement] = useState<HTMLDivElement | null>(null);
|
|
43
69
|
|
|
44
70
|
const searchCategory = useMemo(
|
|
45
71
|
() =>
|
|
@@ -54,14 +80,18 @@ export const EmojiPickerGrid = () => {
|
|
|
54
80
|
);
|
|
55
81
|
|
|
56
82
|
useLayoutEffect(() => {
|
|
57
|
-
if (
|
|
58
|
-
|
|
83
|
+
if (gridElement) {
|
|
84
|
+
gridElement.scrollTop = 0;
|
|
59
85
|
}
|
|
60
|
-
}, [foundEmojis]);
|
|
86
|
+
}, [foundEmojis, gridElement]);
|
|
61
87
|
|
|
62
88
|
return (
|
|
63
|
-
<div ref={
|
|
64
|
-
{searchCategory ?
|
|
89
|
+
<div ref={setGridElement} className={cx(gridWrapperCss, className)}>
|
|
90
|
+
{searchCategory ? (
|
|
91
|
+
<EmojiPickerCategory preRender scrollParent={gridElement} category={searchCategory} renderEmoji={renderEmoji} />
|
|
92
|
+
) : (
|
|
93
|
+
<AllCategories scrollParent={gridElement} renderEmoji={renderEmoji} />
|
|
94
|
+
)}
|
|
65
95
|
</div>
|
|
66
96
|
);
|
|
67
97
|
};
|
|
@@ -5,10 +5,14 @@ import {EmojiPickerSearchProvider} from "./search-provider";
|
|
|
5
5
|
import {detectEmojiSupportLevel} from "../utils/emoji-support";
|
|
6
6
|
import {EmojiSkinProvider} from "./skin-provider";
|
|
7
7
|
import {add as addToFrequentlyUsed} from "../utils/frequently";
|
|
8
|
+
import {isApple} from "../utils/emoji-set";
|
|
9
|
+
import {useCallbackRef} from "@fibery/react/src/use-callback-ref";
|
|
8
10
|
|
|
9
11
|
type EmojiPickerCtx = {
|
|
10
12
|
version: number;
|
|
11
13
|
onEmojiSelect: (emoji: EmojiItem) => void;
|
|
14
|
+
onCustomCategorySettings?: () => void;
|
|
15
|
+
native: boolean;
|
|
12
16
|
};
|
|
13
17
|
const [Provider, useCtx] = createContext<EmojiPickerCtx>("EmojiPickerRoot");
|
|
14
18
|
|
|
@@ -49,19 +53,25 @@ export const EmojiPickerRoot: React.FC<
|
|
|
49
53
|
React.PropsWithChildren<{
|
|
50
54
|
i18n?: Partial<I18N>;
|
|
51
55
|
onEmojiSelect: (emoji: EmojiItem) => void;
|
|
56
|
+
onCustomCategorySettings?: () => void;
|
|
52
57
|
}>
|
|
53
|
-
> = ({i18n, onEmojiSelect, children}) => {
|
|
58
|
+
> = ({i18n, onEmojiSelect, onCustomCategorySettings, children}) => {
|
|
54
59
|
const mergedI18N = useMemo(() => ({...defaultI18N, ...i18n}), [i18n]);
|
|
55
60
|
|
|
61
|
+
const onEmojiSelectCb = useCallbackRef(onEmojiSelect);
|
|
62
|
+
const onCustomCategorySettingsCb = useCallbackRef(onCustomCategorySettings);
|
|
63
|
+
|
|
56
64
|
const emojiPickerCtx = useMemo(
|
|
57
65
|
() => ({
|
|
58
66
|
version: detectEmojiSupportLevel(),
|
|
67
|
+
native: isApple(),
|
|
68
|
+
onCustomCategorySettings: onCustomCategorySettingsCb,
|
|
59
69
|
onEmojiSelect(emoji: EmojiItem) {
|
|
60
|
-
|
|
70
|
+
onEmojiSelectCb(emoji);
|
|
61
71
|
addToFrequentlyUsed(emoji.id);
|
|
62
72
|
},
|
|
63
73
|
}),
|
|
64
|
-
[
|
|
74
|
+
[onEmojiSelectCb, onCustomCategorySettingsCb]
|
|
65
75
|
);
|
|
66
76
|
|
|
67
77
|
return (
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import {EmojiItem} from "@fibery/emoji-data";
|
|
2
|
+
import {useComposedRefs} from "@fibery/react/src/use-composed-refs";
|
|
1
3
|
import {css, cx} from "@linaria/core";
|
|
2
|
-
import {useEmojiPickerCtx, useEmojiPickerI18N} from "./root";
|
|
3
4
|
import {startTransition, useId, useRef} from "react";
|
|
4
|
-
import
|
|
5
|
-
import Close from "../../icons/react/Close";
|
|
5
|
+
import {IconButton} from "../../Button/icon-button";
|
|
6
6
|
import {border, space, textStyles, themeVars} from "../../designSystem";
|
|
7
|
+
import Close from "../../icons/react/Close";
|
|
8
|
+
import Search from "../../icons/react/Search";
|
|
7
9
|
import {useEmojiDataStore} from "../stores/emoji-data-store";
|
|
10
|
+
import {useEmojiPickerCtx, useEmojiPickerI18N} from "./root";
|
|
8
11
|
import {useFoundEmojis, useSetFoundEmojis} from "./search-provider";
|
|
9
|
-
import {
|
|
10
|
-
import {EmojiItem} from "@fibery/emoji-data";
|
|
12
|
+
import {useEmojiSkin} from "./skin-provider";
|
|
11
13
|
|
|
12
14
|
const searchCss = css`
|
|
13
15
|
${textStyles.regular}
|
|
16
|
+
position: relative;
|
|
14
17
|
width: 100%;
|
|
15
18
|
background-color: ${themeVars.inputBgColor};
|
|
16
19
|
box-shadow: ${themeVars.inputBorderColor};
|
|
@@ -20,7 +23,6 @@ const searchCss = css`
|
|
|
20
23
|
display: flex;
|
|
21
24
|
align-items: center;
|
|
22
25
|
padding-left: ${space.l}px;
|
|
23
|
-
padding-right: ${space.m}px;
|
|
24
26
|
gap: ${space.xxs}px;
|
|
25
27
|
|
|
26
28
|
/* For screenreaders only, via https://stackoverflow.com/a/19758620 */
|
|
@@ -57,22 +59,36 @@ const searchIconCss = css`
|
|
|
57
59
|
padding: ${space.xxs}px;
|
|
58
60
|
`;
|
|
59
61
|
|
|
62
|
+
const extraActionSlotCss = css`
|
|
63
|
+
background-color: var(--fibery-emoji-hover-color, ${themeVars.inputCopyBgColor});
|
|
64
|
+
border-top-right-radius: ${space.s}px;
|
|
65
|
+
border-bottom-right-radius: ${space.s}px;
|
|
66
|
+
`;
|
|
67
|
+
|
|
60
68
|
export type EmojiPickerSearchProps = {
|
|
61
69
|
autoFocus?: boolean;
|
|
62
70
|
|
|
63
71
|
className?: string;
|
|
72
|
+
|
|
73
|
+
searchRef?: React.RefObject<HTMLInputElement>;
|
|
74
|
+
|
|
75
|
+
extraAction?: React.ReactNode;
|
|
64
76
|
};
|
|
65
77
|
|
|
66
|
-
export const EmojiPickerSearch: React.FC<EmojiPickerSearchProps> = ({autoFocus, className}) => {
|
|
78
|
+
export const EmojiPickerSearch: React.FC<EmojiPickerSearchProps> = ({autoFocus, className, searchRef, extraAction}) => {
|
|
67
79
|
const ref = useRef<HTMLInputElement>(null);
|
|
80
|
+
|
|
81
|
+
const setRefs = useComposedRefs(ref, searchRef);
|
|
82
|
+
|
|
68
83
|
const i18n = useEmojiPickerI18N();
|
|
69
84
|
const emojiPicker = useEmojiPickerCtx();
|
|
70
85
|
const emojiData = useEmojiDataStore();
|
|
71
86
|
const foundEmojis = useFoundEmojis();
|
|
87
|
+
const skin = useEmojiSkin();
|
|
72
88
|
const _setFoundEmojis = useSetFoundEmojis();
|
|
73
89
|
|
|
74
90
|
const setFoundEmojis = (emojis: EmojiItem[] | null) => {
|
|
75
|
-
//
|
|
91
|
+
// improves responsiveness of input.
|
|
76
92
|
// TODO: better just implement a proper virtualization
|
|
77
93
|
startTransition(() => _setFoundEmojis(emojis));
|
|
78
94
|
};
|
|
@@ -97,18 +113,28 @@ export const EmojiPickerSearch: React.FC<EmojiPickerSearchProps> = ({autoFocus,
|
|
|
97
113
|
|
|
98
114
|
return (
|
|
99
115
|
<section className={cx(searchCss, className)} aria-label={i18n.search}>
|
|
116
|
+
{foundEmojis ? (
|
|
117
|
+
<IconButton size="small" onClick={clearSearch}>
|
|
118
|
+
<Close iconSize={16} />
|
|
119
|
+
</IconButton>
|
|
120
|
+
) : (
|
|
121
|
+
<span className={searchIconCss}>
|
|
122
|
+
<Search iconSize={16} />
|
|
123
|
+
</span>
|
|
124
|
+
)}
|
|
125
|
+
|
|
100
126
|
<input
|
|
101
|
-
ref={
|
|
127
|
+
ref={setRefs}
|
|
128
|
+
autoFocus={autoFocus}
|
|
102
129
|
id={inputId}
|
|
103
130
|
className={searchInputCss}
|
|
104
131
|
type="search"
|
|
105
132
|
placeholder={i18n.search}
|
|
106
|
-
autoFocus={autoFocus}
|
|
107
133
|
onKeyDown={onKeyDown}
|
|
108
134
|
onChange={(e) => {
|
|
109
135
|
const value = e.target.value;
|
|
110
136
|
|
|
111
|
-
setFoundEmojis(value ? emojiData.search(value) : null);
|
|
137
|
+
setFoundEmojis(value ? emojiData.search(value, {skin}) : null);
|
|
112
138
|
}}
|
|
113
139
|
/>
|
|
114
140
|
{/*
|
|
@@ -119,15 +145,7 @@ export const EmojiPickerSearch: React.FC<EmojiPickerSearchProps> = ({autoFocus,
|
|
|
119
145
|
{i18n.search}
|
|
120
146
|
</label>
|
|
121
147
|
|
|
122
|
-
{
|
|
123
|
-
<IconButton size="small" onClick={clearSearch}>
|
|
124
|
-
<Close iconSize={16} />
|
|
125
|
-
</IconButton>
|
|
126
|
-
) : (
|
|
127
|
-
<span className={searchIconCss}>
|
|
128
|
-
<Search iconSize={16} />
|
|
129
|
-
</span>
|
|
130
|
-
)}
|
|
148
|
+
{extraAction ? <div className={extraActionSlotCss}>{extraAction}</div> : null}
|
|
131
149
|
</section>
|
|
132
150
|
);
|
|
133
151
|
};
|
|
@@ -5,16 +5,16 @@ import {textStyles, themeVars} from "../../designSystem";
|
|
|
5
5
|
import {Emoji} from "./emoji";
|
|
6
6
|
import {useEmojiPickerCtx, useEmojiPickerI18N} from "./root";
|
|
7
7
|
import {useEmojiSkin, useSetEmojiSkin} from "./skin-provider";
|
|
8
|
+
import {IconButton} from "../../Button/icon-button";
|
|
8
9
|
|
|
9
10
|
const skinToneWrapperCss = css`
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
10
13
|
background-color: ${themeVars.actionMenuBg};
|
|
11
14
|
font-size: 0;
|
|
12
|
-
line-height: 1.15;
|
|
13
15
|
`;
|
|
14
16
|
|
|
15
17
|
const skinToneSwatchCss = css`
|
|
16
|
-
display: inline-block;
|
|
17
|
-
vertical-align: middle;
|
|
18
18
|
overflow: hidden;
|
|
19
19
|
width: 0;
|
|
20
20
|
padding: 0;
|
|
@@ -50,6 +50,7 @@ const openedSkinTextCss = css`
|
|
|
50
50
|
display: inline-block;
|
|
51
51
|
vertical-align: middle;
|
|
52
52
|
${textStyles.small};
|
|
53
|
+
line-height: 1.3;
|
|
53
54
|
|
|
54
55
|
width: 98px;
|
|
55
56
|
color: ${themeVars.accentTextColor};
|
|
@@ -73,19 +74,28 @@ export const EmojiPickerSkinTone: React.FC<EmojiPickerSkinToneProps> = ({classNa
|
|
|
73
74
|
|
|
74
75
|
skinToneNodes.push(
|
|
75
76
|
<span key={`skin-tone-${skinTone}`} className={cx(skinToneSwatchCss, (selected || opened) && expandedCss)}>
|
|
76
|
-
<
|
|
77
|
+
<IconButton
|
|
78
|
+
size="big"
|
|
79
|
+
onClick={() => {
|
|
80
|
+
if (opened) {
|
|
81
|
+
setSkin(skinTone as EmojiSkin);
|
|
82
|
+
}
|
|
83
|
+
setOpened(!opened);
|
|
84
|
+
}}
|
|
85
|
+
className={css`
|
|
86
|
+
&:hover:not(:disabled),
|
|
87
|
+
&:focus-visible {
|
|
88
|
+
background-color: var(--fibery-emoji-hover-color, ${themeVars.badgeBgColor});
|
|
89
|
+
}
|
|
90
|
+
`}
|
|
91
|
+
>
|
|
77
92
|
<Emoji
|
|
78
|
-
onClick={() => {
|
|
79
|
-
if (opened) {
|
|
80
|
-
setSkin(skinTone as EmojiSkin);
|
|
81
|
-
}
|
|
82
|
-
setOpened(!opened);
|
|
83
|
-
}}
|
|
84
93
|
emoji={`:hand::skin-tone-${skinTone}:`}
|
|
85
94
|
version={emojiPickerCtx.version}
|
|
95
|
+
native={emojiPickerCtx.native}
|
|
86
96
|
size={20}
|
|
87
97
|
/>
|
|
88
|
-
</
|
|
98
|
+
</IconButton>
|
|
89
99
|
</span>
|
|
90
100
|
);
|
|
91
101
|
}
|
|
@@ -28,7 +28,6 @@ export const makeEmojiDataStore = (emojiData: EmojiData): EmojiDataStore => {
|
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
// TODO: this should probably wrap the whole app so every component shares the same data
|
|
32
31
|
const [DataStoreProvider, useDataStoreCtx] = createContext<EmojiDataStore>("EmojiDataStore");
|
|
33
32
|
|
|
34
33
|
export const EmojiDataStoreProvider: React.FC<
|