@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.
Files changed (65) hide show
  1. package/package.json +4 -3
  2. package/src/antd/styles.ts +21 -4
  3. package/src/designSystem.ts +18 -28
  4. package/src/emoji-picker/app-icon-picker.tsx +31 -26
  5. package/src/emoji-picker/emoji-picker-content-with-color.tsx +107 -0
  6. package/src/emoji-picker/emoji-picker.tsx +34 -37
  7. package/src/emoji-picker/emoji.tsx +4 -7
  8. package/src/emoji-picker/icon-emoji-picker.tsx +17 -8
  9. package/src/emoji-picker/primitives/category.tsx +95 -25
  10. package/src/emoji-picker/primitives/emoji.tsx +37 -78
  11. package/src/emoji-picker/primitives/footer.tsx +3 -1
  12. package/src/emoji-picker/primitives/grid.tsx +53 -23
  13. package/src/emoji-picker/primitives/root.tsx +13 -3
  14. package/src/emoji-picker/primitives/search.tsx +38 -20
  15. package/src/emoji-picker/primitives/skin-tone.tsx +21 -11
  16. package/src/emoji-picker/stores/emoji-data-store.tsx +0 -1
  17. package/src/emoji-picker/stores/lazy-icon-data-store.tsx +44 -0
  18. package/src/emoji-picker/stores/static-emoji-data-store.tsx +16 -0
  19. package/src/emoji-picker/utils/emoji-set.ts +1 -1
  20. package/src/icons/ast/AiAssistant.ts +1 -1
  21. package/src/icons/ast/AiAvatar.ts +1 -1
  22. package/src/icons/ast/AppTemplates.ts +1 -1
  23. package/src/icons/ast/BellCircle.ts +8 -0
  24. package/src/icons/ast/BellX.ts +8 -0
  25. package/src/icons/ast/Bolt.ts +8 -0
  26. package/src/icons/ast/CheckCircle.ts +8 -0
  27. package/src/icons/ast/ClockForward.ts +8 -0
  28. package/src/icons/ast/ColorCodingFilled.ts +8 -0
  29. package/src/icons/ast/Delete.ts +1 -1
  30. package/src/icons/ast/EmojiDelete.ts +8 -0
  31. package/src/icons/ast/ExtensionAssignments.ts +1 -1
  32. package/src/icons/ast/Fields.ts +1 -1
  33. package/src/icons/ast/Figma.ts +8 -0
  34. package/src/icons/ast/GoogleDrive.ts +8 -0
  35. package/src/icons/ast/GoogleMap.ts +8 -0
  36. package/src/icons/ast/InvitePeople.ts +1 -1
  37. package/src/icons/ast/LineDivider.ts +8 -0
  38. package/src/icons/ast/Loom.ts +8 -0
  39. package/src/icons/ast/Miro.ts +8 -0
  40. package/src/icons/ast/Mixpanel.ts +8 -0
  41. package/src/icons/ast/Shared.ts +1 -1
  42. package/src/icons/ast/Twitter.ts +8 -0
  43. package/src/icons/ast/Youtube.ts +8 -0
  44. package/src/icons/ast/index.tsx +16 -0
  45. package/src/icons/react/BellCircle.tsx +12 -0
  46. package/src/icons/react/BellX.tsx +12 -0
  47. package/src/icons/react/Bolt.tsx +12 -0
  48. package/src/icons/react/CheckCircle.tsx +12 -0
  49. package/src/icons/react/ClockForward.tsx +12 -0
  50. package/src/icons/react/ColorCodingFilled.tsx +12 -0
  51. package/src/icons/react/EmojiDelete.tsx +12 -0
  52. package/src/icons/react/Figma.tsx +12 -0
  53. package/src/icons/react/GoogleDrive.tsx +12 -0
  54. package/src/icons/react/GoogleMap.tsx +12 -0
  55. package/src/icons/react/LineDivider.tsx +12 -0
  56. package/src/icons/react/Loom.tsx +12 -0
  57. package/src/icons/react/Miro.tsx +12 -0
  58. package/src/icons/react/Mixpanel.tsx +12 -0
  59. package/src/icons/react/Twitter.tsx +12 -0
  60. package/src/icons/react/Youtube.tsx +12 -0
  61. package/src/icons/react/index.tsx +16 -0
  62. package/src/Button/icon-button-with-tooltip.tsx +0 -15
  63. package/src/emoji-picker/index.tsx +0 -5
  64. package/src/emoji-picker/lazy-icon-data-store.tsx +0 -55
  65. /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 {CSSProperties, css} from "@linaria/core";
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
- /* is required only because contentVisibility lifts emoji up */
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 EmojiPickerCategoryProps = {
59
- category: Category;
66
+ type CategoryItemProps = {
67
+ id: string;
68
+ /** Custom renderer for emoji items */
69
+ renderEmoji?: (args: {emoji: EmojiItem}) => React.ReactNode;
60
70
  };
61
- export const EmojiPickerCategory: React.FC<EmojiPickerCategoryProps> = ({category}) => {
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 skin = useEmojiSkin();
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
- <span aria-hidden={true /* already labeled by the section aria-label */}>{label}</span>
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
- <Emoji
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 {border, themeVars} from "../../designSystem";
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 clickableCss = css`
15
- padding: ${emojiPadding}px;
16
- cursor: pointer;
17
- margin: 0;
18
- border: none;
19
- background: none;
20
- box-shadow: none;
21
- border-radius: ${border.radius8}px;
22
-
23
- &:hover {
24
- background-color: var(--fibery-emoji-hover-color, ${themeVars.badgeBgColor});
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
- const getSizeStyle = (size: number) => ({
29
- display: "inline-block",
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
- forceSize?: boolean;
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
- <span
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
- <span
101
- style={{
102
- ...getSizeStyle(size),
103
- backgroundImage: `url(${emoji.imageUrl})`,
104
- backgroundSize: "contain",
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
- // TODO: use fibery's tooltip?
113
- const title = tooltip ? emoji.id : undefined;
82
+ const style = size ? {fontSize: size, width: size, height: size} : {};
114
83
 
115
- return onClick ? (
116
- <button
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: 0 ${space.s}px;
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, useRef, useState} from "react";
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: 238px;
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
- const AllCategories = () => {
18
+ type AllCategoriesProps = Pick<EmojiPickerCategoryProps, "renderEmoji" | "scrollParent">;
19
+ const AllCategories: React.FC<AllCategoriesProps> = ({renderEmoji, scrollParent}) => {
19
20
  const contentSettings = useEmojiPickerContentSettings();
20
21
 
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
-
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 ? <EmojiPickerCategory category={recentCategory} /> : null}
33
- {categories.map((category) => {
34
- return <EmojiPickerCategory key={category.id} category={category} />;
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 ref = useRef<HTMLDivElement>(null);
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 (ref.current) {
58
- ref.current.scrollTop = 0;
83
+ if (gridElement) {
84
+ gridElement.scrollTop = 0;
59
85
  }
60
- }, [foundEmojis]);
86
+ }, [foundEmojis, gridElement]);
61
87
 
62
88
  return (
63
- <div ref={ref} className={gridWrapperCss}>
64
- {searchCategory ? <EmojiPickerCategory category={searchCategory} /> : <AllCategories />}
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
- onEmojiSelect(emoji);
70
+ onEmojiSelectCb(emoji);
61
71
  addToFrequentlyUsed(emoji.id);
62
72
  },
63
73
  }),
64
- [onEmojiSelect]
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 Search from "../../icons/react/Search";
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 {IconButton} from "../../Button/icon-button";
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
- // imroves responsiveness from input.
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={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
- {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
- )}
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
- <span data-skin={skinTone}>
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
- </span>
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<