@fibery/ui-kit 1.13.0 → 1.15.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 +7 -9
- package/src/antd/styles.ts +20 -3
- package/src/designSystem.ts +14 -11
- package/src/emoji-picker/app-icon-picker.tsx +54 -0
- package/src/emoji-picker/emoji-index.ts +8 -0
- package/src/emoji-picker/emoji-picker.tsx +77 -0
- package/src/emoji-picker/emoji.tsx +8 -0
- package/src/{emoji-mart → emoji-picker}/icon-emoji-picker.tsx +39 -17
- package/src/emoji-picker/index.tsx +5 -0
- package/src/emoji-picker/primitives/category.tsx +120 -0
- package/src/emoji-picker/primitives/content.tsx +48 -0
- package/src/emoji-picker/primitives/emoji.tsx +131 -0
- package/src/emoji-picker/primitives/footer.tsx +12 -0
- package/src/emoji-picker/primitives/grid.tsx +67 -0
- package/src/emoji-picker/primitives/header.tsx +11 -0
- package/src/emoji-picker/primitives/layout.ts +5 -0
- package/src/emoji-picker/primitives/root.tsx +89 -0
- package/src/emoji-picker/primitives/search-provider.tsx +22 -0
- package/src/emoji-picker/primitives/search.tsx +141 -0
- package/src/emoji-picker/primitives/skin-provider.tsx +27 -0
- package/src/emoji-picker/primitives/skin-tone.tsx +103 -0
- package/src/emoji-picker/stores/emoji-data-store.tsx +50 -0
- package/src/emoji-picker/stores/lazy-emoji-data-store.tsx +29 -0
- package/src/emoji-picker/stores/lazy-icon-data-store.tsx +55 -0
- package/src/emoji-picker/stores/static-emoji-data-store.tsx +16 -0
- package/src/emoji-picker/utils/emoji-set.ts +5 -0
- package/src/{emoji-mart → emoji-picker/utils}/emoji-support.ts +6 -10
- package/src/emoji-picker/utils/frequently.ts +82 -0
- package/src/emoji-picker/utils/load-emoji-data.ts +4 -0
- package/src/emoji-picker/utils/store.ts +40 -0
- package/src/icons/ast/AddAfter.ts +8 -0
- package/src/icons/ast/AddBefore.ts +8 -0
- package/src/icons/ast/ArrowBarLeft.ts +8 -0
- package/src/icons/ast/ArrowBarRight.ts +8 -0
- package/src/icons/ast/Columns2.ts +8 -0
- package/src/icons/ast/Columns4.ts +8 -0
- package/src/icons/ast/LineDivider.ts +8 -0
- package/src/icons/ast/index.tsx +7 -0
- package/src/icons/react/AddAfter.tsx +12 -0
- package/src/icons/react/AddBefore.tsx +12 -0
- package/src/icons/react/ArrowBarLeft.tsx +12 -0
- package/src/icons/react/ArrowBarRight.tsx +12 -0
- package/src/icons/react/Columns2.tsx +12 -0
- package/src/icons/react/Columns4.tsx +12 -0
- package/src/icons/react/LineDivider.tsx +12 -0
- package/src/icons/react/index.tsx +7 -0
- package/src/emoji-mart/app-icon-picker.tsx +0 -68
- package/src/emoji-mart/data/get-data.tsx +0 -58
- package/src/emoji-mart/emoji/emoji.tsx +0 -26
- package/src/emoji-mart/emoji/index.tsx +0 -24
- package/src/emoji-mart/emoji-index.ts +0 -5
- package/src/emoji-mart/emoji-mart.tsx +0 -216
- package/src/emoji-mart/emoji-picker.tsx +0 -78
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {EmojiItem, EmojiSkin} from "@fibery/emoji-data";
|
|
2
|
+
import {css, cx} from "@linaria/core";
|
|
3
|
+
import {border, themeVars} from "../../designSystem";
|
|
4
|
+
import {useEmojiDataStoreSelector} from "../stores/emoji-data-store";
|
|
5
|
+
import {emojiPadding} from "./layout";
|
|
6
|
+
|
|
7
|
+
const emojiWrapperCss = css`
|
|
8
|
+
color: ${themeVars.textColor};
|
|
9
|
+
font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla",
|
|
10
|
+
"Noto Color Emoji", "Android Emoji";
|
|
11
|
+
`;
|
|
12
|
+
const emojiWrapperSizedCss = css`
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const emojiInlineImgCss = css`
|
|
19
|
+
display: inline-block;
|
|
20
|
+
width: 1em;
|
|
21
|
+
height: 1em;
|
|
22
|
+
vertical-align: -0.1em;
|
|
23
|
+
`;
|
|
24
|
+
const emojiSizedImgCss = css`
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: 100%;
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const clickableCss = css`
|
|
30
|
+
padding: ${emojiPadding}px;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
font-size: 0;
|
|
33
|
+
margin: 0;
|
|
34
|
+
border: none;
|
|
35
|
+
background: none;
|
|
36
|
+
box-shadow: none;
|
|
37
|
+
border-radius: ${border.radius8}px;
|
|
38
|
+
|
|
39
|
+
&:hover {
|
|
40
|
+
background-color: var(--fibery-emoji-hover-color, ${themeVars.badgeBgColor});
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
// 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
|
|
45
|
+
const emptyPixel = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
|
|
46
|
+
|
|
47
|
+
export type EmojiProps = {
|
|
48
|
+
size?: number;
|
|
49
|
+
/** id or colons */
|
|
50
|
+
emoji: string;
|
|
51
|
+
tooltip?: boolean;
|
|
52
|
+
/** max supported version of emojis */
|
|
53
|
+
version?: number;
|
|
54
|
+
onClick?: (emoji: EmojiItem, event: React.MouseEvent) => void;
|
|
55
|
+
skin?: EmojiSkin;
|
|
56
|
+
/** render emoji as native unicode if possible */
|
|
57
|
+
native?: boolean;
|
|
58
|
+
};
|
|
59
|
+
export const Emoji: React.FC<EmojiProps> = ({
|
|
60
|
+
emoji: idOrColons,
|
|
61
|
+
tooltip,
|
|
62
|
+
size,
|
|
63
|
+
onClick,
|
|
64
|
+
version = 14,
|
|
65
|
+
skin,
|
|
66
|
+
native,
|
|
67
|
+
}) => {
|
|
68
|
+
const emoji = useEmojiDataStoreSelector((store) => store.get(idOrColons, {skin}));
|
|
69
|
+
|
|
70
|
+
if (!emoji) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let emojiNode: React.ReactNode;
|
|
75
|
+
|
|
76
|
+
if ("native" in emoji) {
|
|
77
|
+
if (native && version >= emoji.version) {
|
|
78
|
+
emojiNode = emoji.native;
|
|
79
|
+
} else {
|
|
80
|
+
emojiNode = (
|
|
81
|
+
<img
|
|
82
|
+
alt={emoji.name}
|
|
83
|
+
className={cx(size ? emojiSizedImgCss : emojiInlineImgCss)}
|
|
84
|
+
src={emptyPixel}
|
|
85
|
+
style={{
|
|
86
|
+
backgroundImage: `url(${emoji.sprite.src})`,
|
|
87
|
+
backgroundSize: emoji.sprite.size,
|
|
88
|
+
backgroundPosition: emoji.sprite.position,
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
emojiNode = (
|
|
95
|
+
<img
|
|
96
|
+
alt={emoji.name}
|
|
97
|
+
loading="lazy"
|
|
98
|
+
className={cx(size ? emojiSizedImgCss : emojiInlineImgCss)}
|
|
99
|
+
src={emoji.imageUrl}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// TODO: use fibery's tooltip?
|
|
105
|
+
const title = tooltip ? emoji.shortNames[0] : undefined;
|
|
106
|
+
|
|
107
|
+
const style = size ? {fontSize: size, width: size, height: size} : {};
|
|
108
|
+
|
|
109
|
+
return onClick ? (
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
onClick={(e) => onClick(emoji, e)}
|
|
113
|
+
className={cx(clickableCss)}
|
|
114
|
+
title={title}
|
|
115
|
+
aria-label={emoji.name}
|
|
116
|
+
>
|
|
117
|
+
<span style={style} className={cx(emojiWrapperCss, size && emojiWrapperSizedCss)}>
|
|
118
|
+
{emojiNode}
|
|
119
|
+
</span>
|
|
120
|
+
</button>
|
|
121
|
+
) : (
|
|
122
|
+
<span
|
|
123
|
+
style={style}
|
|
124
|
+
className={cx(emojiWrapperCss, size && emojiWrapperSizedCss)}
|
|
125
|
+
title={title}
|
|
126
|
+
aria-label={emoji.name}
|
|
127
|
+
>
|
|
128
|
+
{emojiNode}
|
|
129
|
+
</span>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {css} from "@linaria/core";
|
|
2
|
+
import {space} from "../../designSystem";
|
|
3
|
+
|
|
4
|
+
const footerCss = css`
|
|
5
|
+
border-top: 1px solid var(--fibery-color-separatorColor);
|
|
6
|
+
padding: 0 ${space.s}px;
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
export const EmojiPickerFooter: React.FC<React.PropsWithChildren> = ({children}) => {
|
|
10
|
+
// TODO: Preview + ColorPicker + Skin Tone
|
|
11
|
+
return <div className={footerCss}>{children}</div>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {css} from "@linaria/core";
|
|
2
|
+
import {useLayoutEffect, useMemo, useRef, useState} from "react";
|
|
3
|
+
import {useEmojiDataStoreSelector} from "../stores/emoji-data-store";
|
|
4
|
+
import {get as getFrequentlyUsed} from "../utils/frequently";
|
|
5
|
+
import {EmojiPickerCategory} from "./category";
|
|
6
|
+
import {useEmojiPickerContentSettings} from "./content";
|
|
7
|
+
import {contentHorizontalPadding} from "./layout";
|
|
8
|
+
import {useFoundEmojis} from "./search-provider";
|
|
9
|
+
|
|
10
|
+
const gridWrapperCss = css`
|
|
11
|
+
height: 238px;
|
|
12
|
+
overflow-y: scroll;
|
|
13
|
+
overflow-x: hidden;
|
|
14
|
+
|
|
15
|
+
padding: 0 ${contentHorizontalPadding}px;
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const AllCategories = () => {
|
|
19
|
+
const contentSettings = useEmojiPickerContentSettings();
|
|
20
|
+
|
|
21
|
+
// TODO: filter frequently used to not have zombie dom nodes
|
|
22
|
+
const [recentCategory] = useState(() => ({
|
|
23
|
+
id: "recent",
|
|
24
|
+
name: "Recent",
|
|
25
|
+
emojis: getFrequentlyUsed(contentSettings.perLine),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const categories = useEmojiDataStoreSelector((store) => store.getCategories());
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
{recentCategory.emojis.length > 0 ? <EmojiPickerCategory category={recentCategory} /> : null}
|
|
33
|
+
{categories.map((category) => {
|
|
34
|
+
return <EmojiPickerCategory key={category.id} category={category} />;
|
|
35
|
+
})}
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const EmojiPickerGrid = () => {
|
|
41
|
+
const foundEmojis = useFoundEmojis();
|
|
42
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
43
|
+
|
|
44
|
+
const searchCategory = useMemo(
|
|
45
|
+
() =>
|
|
46
|
+
foundEmojis
|
|
47
|
+
? {
|
|
48
|
+
id: "search",
|
|
49
|
+
name: "Search",
|
|
50
|
+
emojis: foundEmojis.map(({id}) => id),
|
|
51
|
+
}
|
|
52
|
+
: null,
|
|
53
|
+
[foundEmojis]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
useLayoutEffect(() => {
|
|
57
|
+
if (ref.current) {
|
|
58
|
+
ref.current.scrollTop = 0;
|
|
59
|
+
}
|
|
60
|
+
}, [foundEmojis]);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div ref={ref} className={gridWrapperCss}>
|
|
64
|
+
{searchCategory ? <EmojiPickerCategory category={searchCategory} /> : <AllCategories />}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {css} from "@linaria/core";
|
|
2
|
+
import {space} from "../../designSystem";
|
|
3
|
+
import {contentHorizontalPadding} from "./layout";
|
|
4
|
+
|
|
5
|
+
const pickerHeader = css`
|
|
6
|
+
padding: ${space.s}px ${contentHorizontalPadding}px;
|
|
7
|
+
position: relative;
|
|
8
|
+
`;
|
|
9
|
+
export const EmojiPickerHeader: React.FC<React.PropsWithChildren> = ({children}) => {
|
|
10
|
+
return <div className={pickerHeader}>{children}</div>;
|
|
11
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {EmojiItem} from "@fibery/emoji-data";
|
|
2
|
+
import {createContext} from "@fibery/react/src/create-context";
|
|
3
|
+
import {useMemo} from "react";
|
|
4
|
+
import {EmojiPickerSearchProvider} from "./search-provider";
|
|
5
|
+
import {detectEmojiSupportLevel} from "../utils/emoji-support";
|
|
6
|
+
import {EmojiSkinProvider} from "./skin-provider";
|
|
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";
|
|
10
|
+
|
|
11
|
+
type EmojiPickerCtx = {
|
|
12
|
+
version: number;
|
|
13
|
+
onEmojiSelect: (emoji: EmojiItem) => void;
|
|
14
|
+
onCustomCategorySettings?: () => void;
|
|
15
|
+
native: boolean;
|
|
16
|
+
};
|
|
17
|
+
const [Provider, useCtx] = createContext<EmojiPickerCtx>("EmojiPickerRoot");
|
|
18
|
+
|
|
19
|
+
// TODO: replace with simple props to atomic components???
|
|
20
|
+
const defaultI18N = {
|
|
21
|
+
search: "Search",
|
|
22
|
+
clear: "Clear", // Accessible label on "clear" button
|
|
23
|
+
notfound: "No Emoji Found",
|
|
24
|
+
skintext: "Select emoji skin tone",
|
|
25
|
+
categories: {
|
|
26
|
+
search: "Search Results",
|
|
27
|
+
recent: "Frequently Used",
|
|
28
|
+
people: "Smileys & People",
|
|
29
|
+
nature: "Animals & Nature",
|
|
30
|
+
foods: "Food & Drink",
|
|
31
|
+
activity: "Activity",
|
|
32
|
+
places: "Travel & Places",
|
|
33
|
+
objects: "Objects",
|
|
34
|
+
symbols: "Symbols",
|
|
35
|
+
flags: "Flags",
|
|
36
|
+
custom: "Custom",
|
|
37
|
+
},
|
|
38
|
+
categorieslabel: "Emoji categories", // Accessible title for the list of categories
|
|
39
|
+
skintones: {
|
|
40
|
+
1: "Default Skin Tone",
|
|
41
|
+
2: "Light Skin Tone",
|
|
42
|
+
3: "Medium-Light Skin Tone",
|
|
43
|
+
4: "Medium Skin Tone",
|
|
44
|
+
5: "Medium-Dark Skin Tone",
|
|
45
|
+
6: "Dark Skin Tone",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
type I18N = typeof defaultI18N;
|
|
49
|
+
|
|
50
|
+
const [I18NProvider, useI18N] = createContext<I18N>("EmojiPickerI18N");
|
|
51
|
+
|
|
52
|
+
export const EmojiPickerRoot: React.FC<
|
|
53
|
+
React.PropsWithChildren<{
|
|
54
|
+
i18n?: Partial<I18N>;
|
|
55
|
+
onEmojiSelect: (emoji: EmojiItem) => void;
|
|
56
|
+
onCustomCategorySettings?: () => void;
|
|
57
|
+
}>
|
|
58
|
+
> = ({i18n, onEmojiSelect, onCustomCategorySettings, children}) => {
|
|
59
|
+
const mergedI18N = useMemo(() => ({...defaultI18N, ...i18n}), [i18n]);
|
|
60
|
+
|
|
61
|
+
const onEmojiSelectCb = useCallbackRef(onEmojiSelect);
|
|
62
|
+
const onCustomCategorySettingsCb = useCallbackRef(onCustomCategorySettings);
|
|
63
|
+
|
|
64
|
+
const emojiPickerCtx = useMemo(
|
|
65
|
+
() => ({
|
|
66
|
+
version: detectEmojiSupportLevel(),
|
|
67
|
+
native: isApple(),
|
|
68
|
+
onCustomCategorySettings: onCustomCategorySettingsCb,
|
|
69
|
+
onEmojiSelect(emoji: EmojiItem) {
|
|
70
|
+
onEmojiSelectCb(emoji);
|
|
71
|
+
addToFrequentlyUsed(emoji.id);
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
[onEmojiSelectCb, onCustomCategorySettingsCb]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Provider value={emojiPickerCtx}>
|
|
79
|
+
<EmojiSkinProvider>
|
|
80
|
+
<EmojiPickerSearchProvider>
|
|
81
|
+
<I18NProvider value={mergedI18N}>{children}</I18NProvider>
|
|
82
|
+
</EmojiPickerSearchProvider>
|
|
83
|
+
</EmojiSkinProvider>
|
|
84
|
+
</Provider>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const useEmojiPickerCtx = useCtx;
|
|
89
|
+
export const useEmojiPickerI18N = useI18N;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {EmojiItem} from "@fibery/emoji-data";
|
|
2
|
+
import {createContext} from "@fibery/react/src/create-context";
|
|
3
|
+
import {useState} from "react";
|
|
4
|
+
|
|
5
|
+
type SearchSetterCtx = (emojis: EmojiItem[] | null) => void;
|
|
6
|
+
type SearchValueCtx = EmojiItem[] | null;
|
|
7
|
+
|
|
8
|
+
const [SetterProvider, useSetterCtx] = createContext<SearchSetterCtx>("EmojiPickerSearchProvider");
|
|
9
|
+
const [ValueProvider, useValueCtx] = createContext<SearchValueCtx>("EmojiPickerSearchValueProvider");
|
|
10
|
+
|
|
11
|
+
export const EmojiPickerSearchProvider: React.FC<React.PropsWithChildren> = ({children}) => {
|
|
12
|
+
const [found, setFound] = useState<EmojiItem[] | null>(null);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<SetterProvider value={setFound}>
|
|
16
|
+
<ValueProvider value={found}>{children}</ValueProvider>
|
|
17
|
+
</SetterProvider>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const useSetFoundEmojis = useSetterCtx;
|
|
22
|
+
export const useFoundEmojis = useValueCtx;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {EmojiItem} from "@fibery/emoji-data";
|
|
2
|
+
import {useComposedRefs} from "@fibery/react/src/use-composed-refs";
|
|
3
|
+
import {css, cx} from "@linaria/core";
|
|
4
|
+
import {startTransition, useId, useRef} from "react";
|
|
5
|
+
import {IconButton} from "../../Button/icon-button";
|
|
6
|
+
import {border, space, textStyles, themeVars} from "../../designSystem";
|
|
7
|
+
import Close from "../../icons/react/Close";
|
|
8
|
+
import Search from "../../icons/react/Search";
|
|
9
|
+
import {useEmojiDataStore} from "../stores/emoji-data-store";
|
|
10
|
+
import {useEmojiPickerCtx, useEmojiPickerI18N} from "./root";
|
|
11
|
+
import {useFoundEmojis, useSetFoundEmojis} from "./search-provider";
|
|
12
|
+
import {useEmojiSkin} from "./skin-provider";
|
|
13
|
+
|
|
14
|
+
const searchCss = css`
|
|
15
|
+
${textStyles.regular}
|
|
16
|
+
width: 100%;
|
|
17
|
+
background-color: ${themeVars.inputBgColor};
|
|
18
|
+
box-shadow: ${themeVars.inputBorderColor};
|
|
19
|
+
height: 32px;
|
|
20
|
+
border-radius: ${border.radius6}px;
|
|
21
|
+
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
padding-left: ${space.l}px;
|
|
25
|
+
padding-right: ${space.m}px;
|
|
26
|
+
gap: ${space.xxs}px;
|
|
27
|
+
|
|
28
|
+
/* For screenreaders only, via https://stackoverflow.com/a/19758620 */
|
|
29
|
+
& .sr-only {
|
|
30
|
+
position: absolute;
|
|
31
|
+
width: 1px;
|
|
32
|
+
height: 1px;
|
|
33
|
+
padding: 0;
|
|
34
|
+
margin: -1px;
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
clip: rect(0, 0, 0, 0);
|
|
37
|
+
border: 0;
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const searchInputCss = css`
|
|
42
|
+
background-color: ${themeVars.inputBgColor};
|
|
43
|
+
border: 0;
|
|
44
|
+
outline: none;
|
|
45
|
+
width: 100%;
|
|
46
|
+
|
|
47
|
+
&,
|
|
48
|
+
&::-webkit-search-decoration,
|
|
49
|
+
&::-webkit-search-cancel-button,
|
|
50
|
+
&::-webkit-search-results-button,
|
|
51
|
+
&::-webkit-search-results-decoration {
|
|
52
|
+
/* remove webkit/blink styles for <input type="search">
|
|
53
|
+
* via https://stackoverflow.com/a/9422689 */
|
|
54
|
+
-webkit-appearance: none;
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const searchIconCss = css`
|
|
59
|
+
padding: ${space.xxs}px;
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
export type EmojiPickerSearchProps = {
|
|
63
|
+
autoFocus?: boolean;
|
|
64
|
+
|
|
65
|
+
className?: string;
|
|
66
|
+
|
|
67
|
+
searchRef?: React.RefObject<HTMLInputElement>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const EmojiPickerSearch: React.FC<EmojiPickerSearchProps> = ({autoFocus, className, searchRef}) => {
|
|
71
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
72
|
+
|
|
73
|
+
const setRefs = useComposedRefs(ref, searchRef);
|
|
74
|
+
|
|
75
|
+
const i18n = useEmojiPickerI18N();
|
|
76
|
+
const emojiPicker = useEmojiPickerCtx();
|
|
77
|
+
const emojiData = useEmojiDataStore();
|
|
78
|
+
const foundEmojis = useFoundEmojis();
|
|
79
|
+
const skin = useEmojiSkin();
|
|
80
|
+
const _setFoundEmojis = useSetFoundEmojis();
|
|
81
|
+
|
|
82
|
+
const setFoundEmojis = (emojis: EmojiItem[] | null) => {
|
|
83
|
+
// improves responsiveness of input.
|
|
84
|
+
// TODO: better just implement a proper virtualization
|
|
85
|
+
startTransition(() => _setFoundEmojis(emojis));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const inputId = `emoji-picker-search-${useId()}`;
|
|
89
|
+
|
|
90
|
+
const clearSearch = () => {
|
|
91
|
+
setFoundEmojis(null);
|
|
92
|
+
if (ref.current) {
|
|
93
|
+
ref.current.value = "";
|
|
94
|
+
ref.current.focus();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
|
|
99
|
+
if (e.key === "Enter" && foundEmojis && foundEmojis.length > 0) {
|
|
100
|
+
emojiPicker.onEmojiSelect(foundEmojis[0]);
|
|
101
|
+
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<section className={cx(searchCss, className)} aria-label={i18n.search}>
|
|
108
|
+
<input
|
|
109
|
+
ref={setRefs}
|
|
110
|
+
autoFocus={autoFocus}
|
|
111
|
+
id={inputId}
|
|
112
|
+
className={searchInputCss}
|
|
113
|
+
type="search"
|
|
114
|
+
placeholder={i18n.search}
|
|
115
|
+
onKeyDown={onKeyDown}
|
|
116
|
+
onChange={(e) => {
|
|
117
|
+
const value = e.target.value;
|
|
118
|
+
|
|
119
|
+
setFoundEmojis(value ? emojiData.search(value, {skin}) : null);
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
122
|
+
{/*
|
|
123
|
+
* Use a <label> in addition to the placeholder for accessibility, but place it off-screen
|
|
124
|
+
* http://www.maxability.co.in/2016/01/placeholder-attribute-and-why-it-is-not-accessible/
|
|
125
|
+
*/}
|
|
126
|
+
<label className="sr-only" htmlFor={inputId}>
|
|
127
|
+
{i18n.search}
|
|
128
|
+
</label>
|
|
129
|
+
|
|
130
|
+
{foundEmojis ? (
|
|
131
|
+
<IconButton size="small" onClick={clearSearch}>
|
|
132
|
+
<Close iconSize={16} />
|
|
133
|
+
</IconButton>
|
|
134
|
+
) : (
|
|
135
|
+
<span className={searchIconCss}>
|
|
136
|
+
<Search iconSize={16} />
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
</section>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {useCallback, useState} from "react";
|
|
2
|
+
import {createContext} from "@fibery/react/src/create-context";
|
|
3
|
+
import * as store from "../utils/store";
|
|
4
|
+
import {EmojiSkin} from "@fibery/emoji-data";
|
|
5
|
+
|
|
6
|
+
type Setter = (v: EmojiSkin) => void;
|
|
7
|
+
|
|
8
|
+
const [SetterProvider, useSetterCtx] = createContext<Setter>("EmojiSkinSetterProvider");
|
|
9
|
+
const [ValueProvider, useValueCtx] = createContext<EmojiSkin>("EmojiSkinProvider");
|
|
10
|
+
|
|
11
|
+
export const EmojiSkinProvider: React.FC<React.PropsWithChildren> = ({children}) => {
|
|
12
|
+
const [skin, setSkin] = useState(() => store.get("skin") || 1);
|
|
13
|
+
|
|
14
|
+
const setter = useCallback<Setter>((v) => {
|
|
15
|
+
store.update({skin: v});
|
|
16
|
+
setSkin(v);
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<SetterProvider value={setter}>
|
|
21
|
+
<ValueProvider value={skin}>{children}</ValueProvider>
|
|
22
|
+
</SetterProvider>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const useSetEmojiSkin = useSetterCtx;
|
|
27
|
+
export const useEmojiSkin = useValueCtx;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {EmojiSkin} from "@fibery/emoji-data";
|
|
2
|
+
import {css, cx} from "@linaria/core";
|
|
3
|
+
import {useState} from "react";
|
|
4
|
+
import {textStyles, themeVars} from "../../designSystem";
|
|
5
|
+
import {Emoji} from "./emoji";
|
|
6
|
+
import {useEmojiPickerCtx, useEmojiPickerI18N} from "./root";
|
|
7
|
+
import {useEmojiSkin, useSetEmojiSkin} from "./skin-provider";
|
|
8
|
+
|
|
9
|
+
const skinToneWrapperCss = css`
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
background-color: ${themeVars.actionMenuBg};
|
|
13
|
+
font-size: 0;
|
|
14
|
+
line-height: 1.15;
|
|
15
|
+
|
|
16
|
+
/* magic constant until skin tone is removed from picker search bar */
|
|
17
|
+
height: 36px;
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const skinToneSwatchCss = css`
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
width: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
transition-property: width, padding;
|
|
25
|
+
transition-duration: 0.125s;
|
|
26
|
+
transition-timing-function: ease-out;
|
|
27
|
+
|
|
28
|
+
&:nth-child(1) {
|
|
29
|
+
transition-delay: 0s;
|
|
30
|
+
}
|
|
31
|
+
&:nth-child(2) {
|
|
32
|
+
transition-delay: 0.03s;
|
|
33
|
+
}
|
|
34
|
+
&:nth-child(3) {
|
|
35
|
+
transition-delay: 0.06s;
|
|
36
|
+
}
|
|
37
|
+
&:nth-child(4) {
|
|
38
|
+
transition-delay: 0.09s;
|
|
39
|
+
}
|
|
40
|
+
&:nth-child(5) {
|
|
41
|
+
transition-delay: 0.12s;
|
|
42
|
+
}
|
|
43
|
+
&:nth-child(6) {
|
|
44
|
+
transition-delay: 0.15s;
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
const expandedCss = css`
|
|
49
|
+
width: 32px;
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const openedSkinTextCss = css`
|
|
53
|
+
display: inline-block;
|
|
54
|
+
vertical-align: middle;
|
|
55
|
+
${textStyles.small};
|
|
56
|
+
|
|
57
|
+
width: 98px;
|
|
58
|
+
color: ${themeVars.accentTextColor};
|
|
59
|
+
padding-left: 8px;
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
export type EmojiPickerSkinToneProps = {
|
|
63
|
+
className?: string;
|
|
64
|
+
};
|
|
65
|
+
export const EmojiPickerSkinTone: React.FC<EmojiPickerSkinToneProps> = ({className}) => {
|
|
66
|
+
const [opened, setOpened] = useState(false);
|
|
67
|
+
const emojiPickerCtx = useEmojiPickerCtx();
|
|
68
|
+
const i18n = useEmojiPickerI18N();
|
|
69
|
+
const setSkin = useSetEmojiSkin();
|
|
70
|
+
const skin = useEmojiSkin();
|
|
71
|
+
|
|
72
|
+
const skinToneNodes = [];
|
|
73
|
+
|
|
74
|
+
for (let skinTone = 1; skinTone <= 6; skinTone++) {
|
|
75
|
+
const selected = skinTone === skin;
|
|
76
|
+
|
|
77
|
+
skinToneNodes.push(
|
|
78
|
+
<span key={`skin-tone-${skinTone}`} className={cx(skinToneSwatchCss, (selected || opened) && expandedCss)}>
|
|
79
|
+
<span data-skin={skinTone}>
|
|
80
|
+
<Emoji
|
|
81
|
+
onClick={() => {
|
|
82
|
+
if (opened) {
|
|
83
|
+
setSkin(skinTone as EmojiSkin);
|
|
84
|
+
}
|
|
85
|
+
setOpened(!opened);
|
|
86
|
+
}}
|
|
87
|
+
emoji={`:hand::skin-tone-${skinTone}:`}
|
|
88
|
+
version={emojiPickerCtx.version}
|
|
89
|
+
native={emojiPickerCtx.native}
|
|
90
|
+
size={20}
|
|
91
|
+
/>
|
|
92
|
+
</span>
|
|
93
|
+
</span>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className={cx(skinToneWrapperCss, className)}>
|
|
99
|
+
<div className={cx(opened && openedSkinTextCss)}>{i18n.skintext}</div>
|
|
100
|
+
{skinToneNodes}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type {EmojiData} from "@fibery/emoji-data";
|
|
2
|
+
import {PubSub, makePubSub} from "@fibery/helpers/utils/pub-sub";
|
|
3
|
+
import {createContext} from "@fibery/react/src/create-context";
|
|
4
|
+
import {useMemo, useSyncExternalStore} from "react";
|
|
5
|
+
|
|
6
|
+
export type EmojiDataStore = EmojiData & {pubSub: PubSub};
|
|
7
|
+
|
|
8
|
+
export const makeEmojiDataStore = (emojiData: EmojiData): EmojiDataStore => {
|
|
9
|
+
const pubSub = makePubSub();
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
...emojiData,
|
|
13
|
+
|
|
14
|
+
setCustom(...args: Parameters<EmojiData["setCustom"]>) {
|
|
15
|
+
emojiData.setCustom(...args);
|
|
16
|
+
pubSub.publish();
|
|
17
|
+
},
|
|
18
|
+
addCustomEmoji(...args: Parameters<EmojiData["addCustomEmoji"]>) {
|
|
19
|
+
emojiData.addCustomEmoji(...args);
|
|
20
|
+
pubSub.publish();
|
|
21
|
+
},
|
|
22
|
+
removeCustomEmoji(...args: Parameters<EmojiData["removeCustomEmoji"]>) {
|
|
23
|
+
emojiData.removeCustomEmoji(...args);
|
|
24
|
+
pubSub.publish();
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
pubSub,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// TODO: this should probably wrap the whole app so every component shares the same data
|
|
32
|
+
const [DataStoreProvider, useDataStoreCtx] = createContext<EmojiDataStore>("EmojiDataStore");
|
|
33
|
+
|
|
34
|
+
export const EmojiDataStoreProvider: React.FC<
|
|
35
|
+
React.PropsWithChildren<{
|
|
36
|
+
data: EmojiData;
|
|
37
|
+
}>
|
|
38
|
+
> = ({data, children}) => {
|
|
39
|
+
const emojiDataStore = useMemo(() => makeEmojiDataStore(data), [data]);
|
|
40
|
+
|
|
41
|
+
return <DataStoreProvider value={emojiDataStore}>{children}</DataStoreProvider>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const useEmojiDataStore = useDataStoreCtx;
|
|
45
|
+
|
|
46
|
+
export const useEmojiDataStoreSelector = <Snapshot,>(getter: (emojiDataStore: EmojiDataStore) => Snapshot) => {
|
|
47
|
+
const dataStore = useDataStoreCtx();
|
|
48
|
+
|
|
49
|
+
return useSyncExternalStore(dataStore.pubSub.subscribe, () => getter(dataStore));
|
|
50
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {makeEmojiData} from "@fibery/emoji-data";
|
|
2
|
+
import {Suspense, lazy} from "react";
|
|
3
|
+
import {EmojiDataStoreProvider} from "./stores/emoji-data-store";
|
|
4
|
+
import {getEmojiSet} from "./utils/emoji-set";
|
|
5
|
+
import {loadEmojiData} from "./utils/load-emoji-data";
|
|
6
|
+
|
|
7
|
+
const LazyEmojiDataStoreComponent = lazy(async () => {
|
|
8
|
+
const rawData = await loadEmojiData();
|
|
9
|
+
|
|
10
|
+
const emojiData = makeEmojiData(rawData, {set: getEmojiSet()});
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
default: function EmojiDataStore({children}: React.PropsWithChildren) {
|
|
14
|
+
return <EmojiDataStoreProvider data={emojiData}>{children}</EmojiDataStoreProvider>;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const LazyEmojiDataStore: React.FC<
|
|
20
|
+
React.PropsWithChildren<{
|
|
21
|
+
fallback: React.ReactNode;
|
|
22
|
+
}>
|
|
23
|
+
> = ({fallback, children}) => {
|
|
24
|
+
return (
|
|
25
|
+
<Suspense fallback={fallback}>
|
|
26
|
+
<LazyEmojiDataStoreComponent>{children}</LazyEmojiDataStoreComponent>
|
|
27
|
+
</Suspense>
|
|
28
|
+
);
|
|
29
|
+
};
|