@fibery/ui-kit 1.13.0 → 1.14.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 +5 -7
- package/src/designSystem.ts +2 -0
- 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 +75 -0
- package/src/emoji-picker/emoji.tsx +11 -0
- package/src/{emoji-mart → emoji-picker}/icon-emoji-picker.tsx +13 -11
- package/src/emoji-picker/index.tsx +5 -0
- package/src/emoji-picker/lazy-emoji-data-store.tsx +29 -0
- package/src/emoji-picker/lazy-icon-data-store.tsx +55 -0
- package/src/emoji-picker/primitives/category.tsx +107 -0
- package/src/emoji-picker/primitives/content.tsx +48 -0
- package/src/emoji-picker/primitives/emoji.tsx +130 -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 +79 -0
- package/src/emoji-picker/primitives/search-provider.tsx +22 -0
- package/src/emoji-picker/primitives/search.tsx +133 -0
- package/src/emoji-picker/primitives/skin-provider.tsx +27 -0
- package/src/emoji-picker/primitives/skin-tone.tsx +99 -0
- package/src/emoji-picker/stores/emoji-data-store.tsx +50 -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/index.tsx +6 -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/index.tsx +6 -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,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,79 @@
|
|
|
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
|
+
|
|
9
|
+
type EmojiPickerCtx = {
|
|
10
|
+
version: number;
|
|
11
|
+
onEmojiSelect: (emoji: EmojiItem) => void;
|
|
12
|
+
};
|
|
13
|
+
const [Provider, useCtx] = createContext<EmojiPickerCtx>("EmojiPickerRoot");
|
|
14
|
+
|
|
15
|
+
// TODO: replace with simple props to atomic components???
|
|
16
|
+
const defaultI18N = {
|
|
17
|
+
search: "Search",
|
|
18
|
+
clear: "Clear", // Accessible label on "clear" button
|
|
19
|
+
notfound: "No Emoji Found",
|
|
20
|
+
skintext: "Select emoji skin tone",
|
|
21
|
+
categories: {
|
|
22
|
+
search: "Search Results",
|
|
23
|
+
recent: "Frequently Used",
|
|
24
|
+
people: "Smileys & People",
|
|
25
|
+
nature: "Animals & Nature",
|
|
26
|
+
foods: "Food & Drink",
|
|
27
|
+
activity: "Activity",
|
|
28
|
+
places: "Travel & Places",
|
|
29
|
+
objects: "Objects",
|
|
30
|
+
symbols: "Symbols",
|
|
31
|
+
flags: "Flags",
|
|
32
|
+
custom: "Custom",
|
|
33
|
+
},
|
|
34
|
+
categorieslabel: "Emoji categories", // Accessible title for the list of categories
|
|
35
|
+
skintones: {
|
|
36
|
+
1: "Default Skin Tone",
|
|
37
|
+
2: "Light Skin Tone",
|
|
38
|
+
3: "Medium-Light Skin Tone",
|
|
39
|
+
4: "Medium Skin Tone",
|
|
40
|
+
5: "Medium-Dark Skin Tone",
|
|
41
|
+
6: "Dark Skin Tone",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
type I18N = typeof defaultI18N;
|
|
45
|
+
|
|
46
|
+
const [I18NProvider, useI18N] = createContext<I18N>("EmojiPickerI18N");
|
|
47
|
+
|
|
48
|
+
export const EmojiPickerRoot: React.FC<
|
|
49
|
+
React.PropsWithChildren<{
|
|
50
|
+
i18n?: Partial<I18N>;
|
|
51
|
+
onEmojiSelect: (emoji: EmojiItem) => void;
|
|
52
|
+
}>
|
|
53
|
+
> = ({i18n, onEmojiSelect, children}) => {
|
|
54
|
+
const mergedI18N = useMemo(() => ({...defaultI18N, ...i18n}), [i18n]);
|
|
55
|
+
|
|
56
|
+
const emojiPickerCtx = useMemo(
|
|
57
|
+
() => ({
|
|
58
|
+
version: detectEmojiSupportLevel(),
|
|
59
|
+
onEmojiSelect(emoji: EmojiItem) {
|
|
60
|
+
onEmojiSelect(emoji);
|
|
61
|
+
addToFrequentlyUsed(emoji.id);
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
[onEmojiSelect]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Provider value={emojiPickerCtx}>
|
|
69
|
+
<EmojiSkinProvider>
|
|
70
|
+
<EmojiPickerSearchProvider>
|
|
71
|
+
<I18NProvider value={mergedI18N}>{children}</I18NProvider>
|
|
72
|
+
</EmojiPickerSearchProvider>
|
|
73
|
+
</EmojiSkinProvider>
|
|
74
|
+
</Provider>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const useEmojiPickerCtx = useCtx;
|
|
79
|
+
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,133 @@
|
|
|
1
|
+
import {css, cx} from "@linaria/core";
|
|
2
|
+
import {useEmojiPickerCtx, useEmojiPickerI18N} from "./root";
|
|
3
|
+
import {startTransition, useId, useRef} from "react";
|
|
4
|
+
import Search from "../../icons/react/Search";
|
|
5
|
+
import Close from "../../icons/react/Close";
|
|
6
|
+
import {border, space, textStyles, themeVars} from "../../designSystem";
|
|
7
|
+
import {useEmojiDataStore} from "../stores/emoji-data-store";
|
|
8
|
+
import {useFoundEmojis, useSetFoundEmojis} from "./search-provider";
|
|
9
|
+
import {IconButton} from "../../Button/icon-button";
|
|
10
|
+
import {EmojiItem} from "@fibery/emoji-data";
|
|
11
|
+
|
|
12
|
+
const searchCss = css`
|
|
13
|
+
${textStyles.regular}
|
|
14
|
+
width: 100%;
|
|
15
|
+
background-color: ${themeVars.inputBgColor};
|
|
16
|
+
box-shadow: ${themeVars.inputBorderColor};
|
|
17
|
+
height: 32px;
|
|
18
|
+
border-radius: ${border.radius6}px;
|
|
19
|
+
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
padding-left: ${space.l}px;
|
|
23
|
+
padding-right: ${space.m}px;
|
|
24
|
+
gap: ${space.xxs}px;
|
|
25
|
+
|
|
26
|
+
/* For screenreaders only, via https://stackoverflow.com/a/19758620 */
|
|
27
|
+
& .sr-only {
|
|
28
|
+
position: absolute;
|
|
29
|
+
width: 1px;
|
|
30
|
+
height: 1px;
|
|
31
|
+
padding: 0;
|
|
32
|
+
margin: -1px;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
clip: rect(0, 0, 0, 0);
|
|
35
|
+
border: 0;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const searchInputCss = css`
|
|
40
|
+
background-color: ${themeVars.inputBgColor};
|
|
41
|
+
border: 0;
|
|
42
|
+
outline: none;
|
|
43
|
+
width: 100%;
|
|
44
|
+
|
|
45
|
+
&,
|
|
46
|
+
&::-webkit-search-decoration,
|
|
47
|
+
&::-webkit-search-cancel-button,
|
|
48
|
+
&::-webkit-search-results-button,
|
|
49
|
+
&::-webkit-search-results-decoration {
|
|
50
|
+
/* remove webkit/blink styles for <input type="search">
|
|
51
|
+
* via https://stackoverflow.com/a/9422689 */
|
|
52
|
+
-webkit-appearance: none;
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const searchIconCss = css`
|
|
57
|
+
padding: ${space.xxs}px;
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
export type EmojiPickerSearchProps = {
|
|
61
|
+
autoFocus?: boolean;
|
|
62
|
+
|
|
63
|
+
className?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const EmojiPickerSearch: React.FC<EmojiPickerSearchProps> = ({autoFocus, className}) => {
|
|
67
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
68
|
+
const i18n = useEmojiPickerI18N();
|
|
69
|
+
const emojiPicker = useEmojiPickerCtx();
|
|
70
|
+
const emojiData = useEmojiDataStore();
|
|
71
|
+
const foundEmojis = useFoundEmojis();
|
|
72
|
+
const _setFoundEmojis = useSetFoundEmojis();
|
|
73
|
+
|
|
74
|
+
const setFoundEmojis = (emojis: EmojiItem[] | null) => {
|
|
75
|
+
// imroves responsiveness from input.
|
|
76
|
+
// TODO: better just implement a proper virtualization
|
|
77
|
+
startTransition(() => _setFoundEmojis(emojis));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const inputId = `emoji-picker-search-${useId()}`;
|
|
81
|
+
|
|
82
|
+
const clearSearch = () => {
|
|
83
|
+
setFoundEmojis(null);
|
|
84
|
+
if (ref.current) {
|
|
85
|
+
ref.current.value = "";
|
|
86
|
+
ref.current.focus();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
|
|
91
|
+
if (e.key === "Enter" && foundEmojis && foundEmojis.length > 0) {
|
|
92
|
+
emojiPicker.onEmojiSelect(foundEmojis[0]);
|
|
93
|
+
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<section className={cx(searchCss, className)} aria-label={i18n.search}>
|
|
100
|
+
<input
|
|
101
|
+
ref={ref}
|
|
102
|
+
id={inputId}
|
|
103
|
+
className={searchInputCss}
|
|
104
|
+
type="search"
|
|
105
|
+
placeholder={i18n.search}
|
|
106
|
+
autoFocus={autoFocus}
|
|
107
|
+
onKeyDown={onKeyDown}
|
|
108
|
+
onChange={(e) => {
|
|
109
|
+
const value = e.target.value;
|
|
110
|
+
|
|
111
|
+
setFoundEmojis(value ? emojiData.search(value) : null);
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
{/*
|
|
115
|
+
* Use a <label> in addition to the placeholder for accessibility, but place it off-screen
|
|
116
|
+
* http://www.maxability.co.in/2016/01/placeholder-attribute-and-why-it-is-not-accessible/
|
|
117
|
+
*/}
|
|
118
|
+
<label className="sr-only" htmlFor={inputId}>
|
|
119
|
+
{i18n.search}
|
|
120
|
+
</label>
|
|
121
|
+
|
|
122
|
+
{foundEmojis ? (
|
|
123
|
+
<IconButton size="small" onClick={clearSearch}>
|
|
124
|
+
<Close iconSize={16} />
|
|
125
|
+
</IconButton>
|
|
126
|
+
) : (
|
|
127
|
+
<span className={searchIconCss}>
|
|
128
|
+
<Search iconSize={16} />
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
131
|
+
</section>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
@@ -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,99 @@
|
|
|
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
|
+
background-color: ${themeVars.actionMenuBg};
|
|
11
|
+
font-size: 0;
|
|
12
|
+
line-height: 1.15;
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const skinToneSwatchCss = css`
|
|
16
|
+
display: inline-block;
|
|
17
|
+
vertical-align: middle;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
width: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
transition-property: width, padding;
|
|
22
|
+
transition-duration: 0.125s;
|
|
23
|
+
transition-timing-function: ease-out;
|
|
24
|
+
|
|
25
|
+
&:nth-child(1) {
|
|
26
|
+
transition-delay: 0s;
|
|
27
|
+
}
|
|
28
|
+
&:nth-child(2) {
|
|
29
|
+
transition-delay: 0.03s;
|
|
30
|
+
}
|
|
31
|
+
&:nth-child(3) {
|
|
32
|
+
transition-delay: 0.06s;
|
|
33
|
+
}
|
|
34
|
+
&:nth-child(4) {
|
|
35
|
+
transition-delay: 0.09s;
|
|
36
|
+
}
|
|
37
|
+
&:nth-child(5) {
|
|
38
|
+
transition-delay: 0.12s;
|
|
39
|
+
}
|
|
40
|
+
&:nth-child(6) {
|
|
41
|
+
transition-delay: 0.15s;
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const expandedCss = css`
|
|
46
|
+
width: 32px;
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const openedSkinTextCss = css`
|
|
50
|
+
display: inline-block;
|
|
51
|
+
vertical-align: middle;
|
|
52
|
+
${textStyles.small};
|
|
53
|
+
|
|
54
|
+
width: 98px;
|
|
55
|
+
color: ${themeVars.accentTextColor};
|
|
56
|
+
padding-left: 8px;
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
export type EmojiPickerSkinToneProps = {
|
|
60
|
+
className?: string;
|
|
61
|
+
};
|
|
62
|
+
export const EmojiPickerSkinTone: React.FC<EmojiPickerSkinToneProps> = ({className}) => {
|
|
63
|
+
const [opened, setOpened] = useState(false);
|
|
64
|
+
const emojiPickerCtx = useEmojiPickerCtx();
|
|
65
|
+
const i18n = useEmojiPickerI18N();
|
|
66
|
+
const setSkin = useSetEmojiSkin();
|
|
67
|
+
const skin = useEmojiSkin();
|
|
68
|
+
|
|
69
|
+
const skinToneNodes = [];
|
|
70
|
+
|
|
71
|
+
for (let skinTone = 1; skinTone <= 6; skinTone++) {
|
|
72
|
+
const selected = skinTone === skin;
|
|
73
|
+
|
|
74
|
+
skinToneNodes.push(
|
|
75
|
+
<span key={`skin-tone-${skinTone}`} className={cx(skinToneSwatchCss, (selected || opened) && expandedCss)}>
|
|
76
|
+
<span data-skin={skinTone}>
|
|
77
|
+
<Emoji
|
|
78
|
+
onClick={() => {
|
|
79
|
+
if (opened) {
|
|
80
|
+
setSkin(skinTone as EmojiSkin);
|
|
81
|
+
}
|
|
82
|
+
setOpened(!opened);
|
|
83
|
+
}}
|
|
84
|
+
emoji={`:hand::skin-tone-${skinTone}:`}
|
|
85
|
+
version={emojiPickerCtx.version}
|
|
86
|
+
size={20}
|
|
87
|
+
/>
|
|
88
|
+
</span>
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className={cx(skinToneWrapperCss, className)}>
|
|
95
|
+
<div className={cx(opened && openedSkinTextCss)}>{i18n.skintext}</div>
|
|
96
|
+
{skinToneNodes}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// "hard-forked" from https://github.com/nolanlawson/emoji-picker-element/
|
|
2
2
|
const versionsAndTestEmoji = {
|
|
3
3
|
"🫸🏻": 15,
|
|
4
4
|
"🫠": 14,
|
|
@@ -16,7 +16,6 @@ const versionsAndTestEmoji = {
|
|
|
16
16
|
const FONT_FAMILY =
|
|
17
17
|
'"Twemoji Mozilla","Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol",' +
|
|
18
18
|
'"Noto Color Emoji","EmojiOne Color","Android Emoji",sans-serif';
|
|
19
|
-
const rIC = typeof requestIdleCallback === "function" ? requestIdleCallback : setTimeout;
|
|
20
19
|
|
|
21
20
|
// only used in jest tests
|
|
22
21
|
const simulateCanvasError = false;
|
|
@@ -89,14 +88,11 @@ function determineEmojiSupportLevel() {
|
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
// Check which emojis we know for sure aren't supported, based on Unicode version level
|
|
92
|
-
let
|
|
91
|
+
let version: number;
|
|
92
|
+
|
|
93
93
|
export const detectEmojiSupportLevel = () => {
|
|
94
|
-
if (!
|
|
95
|
-
|
|
96
|
-
rIC(
|
|
97
|
-
() => resolve(determineEmojiSupportLevel()) // delay so ideally this can run while IDB is first populating
|
|
98
|
-
);
|
|
99
|
-
});
|
|
94
|
+
if (!version) {
|
|
95
|
+
version = determineEmojiSupportLevel();
|
|
100
96
|
}
|
|
101
|
-
return
|
|
97
|
+
return version;
|
|
102
98
|
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as store from "./store";
|
|
2
|
+
|
|
3
|
+
/** "hard-forked" as is from https://github.dev/missive/emoji-mart/blob/v3.0.1/src/utils/frequently.js */
|
|
4
|
+
|
|
5
|
+
const DEFAULTS = [
|
|
6
|
+
"+1",
|
|
7
|
+
"grinning",
|
|
8
|
+
"kissing_heart",
|
|
9
|
+
"heart_eyes",
|
|
10
|
+
"laughing",
|
|
11
|
+
"stuck_out_tongue_winking_eye",
|
|
12
|
+
"sweat_smile",
|
|
13
|
+
"joy",
|
|
14
|
+
"scream",
|
|
15
|
+
"disappointed",
|
|
16
|
+
"unamused",
|
|
17
|
+
"weary",
|
|
18
|
+
"sob",
|
|
19
|
+
"sunglasses",
|
|
20
|
+
"heart",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
let frequently: Record<string, number>;
|
|
24
|
+
let defaults: Record<string, number> = {};
|
|
25
|
+
let initialized = false;
|
|
26
|
+
|
|
27
|
+
function init() {
|
|
28
|
+
initialized = true;
|
|
29
|
+
frequently = store.get("frequently");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function add(emojiId: string) {
|
|
33
|
+
if (!initialized) {
|
|
34
|
+
init();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!frequently) {
|
|
38
|
+
frequently = defaults;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!frequently[emojiId]) {
|
|
42
|
+
frequently[emojiId] = 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
frequently[emojiId] += 1;
|
|
46
|
+
|
|
47
|
+
store.set("last", emojiId);
|
|
48
|
+
store.set("frequently", frequently);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function get(perLine: number) {
|
|
52
|
+
if (!initialized) {
|
|
53
|
+
init();
|
|
54
|
+
}
|
|
55
|
+
if (!frequently) {
|
|
56
|
+
defaults = {};
|
|
57
|
+
|
|
58
|
+
const result = [];
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < perLine; i++) {
|
|
61
|
+
defaults[DEFAULTS[i]] = perLine - i;
|
|
62
|
+
result.push(DEFAULTS[i]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const quantity = perLine * 4;
|
|
69
|
+
const frequentlyKeys = Object.keys(frequently);
|
|
70
|
+
|
|
71
|
+
const sorted = frequentlyKeys.sort((a, b) => frequently[a] - frequently[b]).reverse();
|
|
72
|
+
const sliced = sorted.slice(0, quantity);
|
|
73
|
+
|
|
74
|
+
const last = store.get("last");
|
|
75
|
+
|
|
76
|
+
if (last && sliced.indexOf(last) === -1) {
|
|
77
|
+
sliced.pop();
|
|
78
|
+
sliced.push(last);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return sliced;
|
|
82
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** "hard-forked" as is from https://github.dev/missive/emoji-mart/blob/v3.0.1/src/utils/store.js */
|
|
2
|
+
|
|
3
|
+
const NAMESPACE = "emoji-mart";
|
|
4
|
+
|
|
5
|
+
const isLocalStorageSupported = typeof window !== "undefined" && "localStorage" in window;
|
|
6
|
+
|
|
7
|
+
export function update(state: {[key: string]: unknown}) {
|
|
8
|
+
for (const key in state) {
|
|
9
|
+
const value = state[key];
|
|
10
|
+
set(key, value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function set(key: string, value: unknown) {
|
|
15
|
+
if (!isLocalStorageSupported) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
window.localStorage[`${NAMESPACE}.${key}`] = JSON.stringify(value);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
/* empty */
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function get(key: string) {
|
|
26
|
+
if (!isLocalStorageSupported) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const value = window.localStorage[`${NAMESPACE}.${key}`];
|
|
31
|
+
|
|
32
|
+
if (value) {
|
|
33
|
+
return JSON.parse(value);
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
|
|
2
|
+
// This icon file is generated automatically.
|
|
3
|
+
|
|
4
|
+
import { IconDefinition } from '../types';
|
|
5
|
+
|
|
6
|
+
const AddAfter: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"viewBox":"0 0 20 20"},"children":[{"type":"element","tagName":"path","properties":{"fillRule":"evenodd","clipRule":"evenodd","d":"M4.25 3A2.25 2.25 0 0 0 2 5.25v9.5A2.25 2.25 0 0 0 4.25 17h3.5A2.25 2.25 0 0 0 10 14.75v-9.5A2.25 2.25 0 0 0 7.75 3h-3.5ZM3.5 5.25a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 .75.75v9.5a.75.75 0 0 1-.75.75h-3.5a.75.75 0 0 1-.75-.75v-9.5Z"},"children":[]},{"type":"element","tagName":"path","properties":{"d":"M15 7a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0v-1.5h-1.5a.75.75 0 0 1 0-1.5h1.5v-1.5A.75.75 0 0 1 15 7Z"},"children":[]}],"metadata":""}]},"name":"add-after"};
|
|
7
|
+
|
|
8
|
+
export default AddAfter;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
|
|
2
|
+
// This icon file is generated automatically.
|
|
3
|
+
|
|
4
|
+
import { IconDefinition } from '../types';
|
|
5
|
+
|
|
6
|
+
const AddBefore: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"viewBox":"0 0 20 20"},"children":[{"type":"element","tagName":"path","properties":{"fillRule":"evenodd","clipRule":"evenodd","d":"M12.25 3A2.25 2.25 0 0 0 10 5.25v9.5A2.25 2.25 0 0 0 12.25 17h3.5A2.25 2.25 0 0 0 18 14.75v-9.5A2.25 2.25 0 0 0 15.75 3h-3.5Zm-.75 2.25a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 .75.75v9.5a.75.75 0 0 1-.75.75h-3.5a.75.75 0 0 1-.75-.75v-9.5Z"},"children":[]},{"type":"element","tagName":"path","properties":{"d":"M5 7a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0v-1.5h-1.5a.75.75 0 0 1 0-1.5h1.5v-1.5A.75.75 0 0 1 5 7Z"},"children":[]}],"metadata":""}]},"name":"add-before"};
|
|
7
|
+
|
|
8
|
+
export default AddBefore;
|