emoji-picker-react 4.0.6 → 4.0.8

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.
@@ -2,9 +2,10 @@ import { DataEmoji } from '../dataUtils/DataTypes';
2
2
  export declare function useClearSearch(): () => void;
3
3
  export declare function useAppendSearch(): (str: string) => void;
4
4
  export declare function useFilter(): {
5
- onChange: (nextValue: string) => void;
5
+ onChange: (inputValue: string) => void;
6
6
  searchTerm: string;
7
7
  SearchInputRef: import("../components/context/ElementRefContext").ElementRef<HTMLInputElement>;
8
8
  };
9
9
  export declare function useIsEmojiFiltered(): (unified: string) => boolean;
10
10
  export declare type FilterDict = Record<string, DataEmoji>;
11
+ export declare function getNormalizedSearchTerm(str: string): string;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.0.6",
2
+ "version": "4.0.8",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -9,6 +9,7 @@ export enum ClassNames {
9
9
  label = 'epr-emoji-category-label',
10
10
  categoryContent = 'epr-emoji-category-content',
11
11
  emojiHasVariatios = 'epr-emoji-has-variations',
12
+ scrollBody = 'epr-body',
12
13
  external = '__EmojiPicker__'
13
14
  }
14
15
 
@@ -8,6 +8,7 @@ import {
8
8
  rowNumber
9
9
  } from './elementPositionInRow';
10
10
  import { focusElement } from './focusElement';
11
+ import { scrollEmojiAboveLabel } from './scrollTo';
11
12
  import {
12
13
  allVisibleEmojis,
13
14
  closestCategory,
@@ -22,7 +23,9 @@ import {
22
23
  } from './selectors';
23
24
 
24
25
  export function focusFirstVisibleEmoji(parent: NullableElement) {
25
- focusElement(firstVisibleEmoji(parent));
26
+ const emoji = firstVisibleEmoji(parent);
27
+ focusElement(emoji);
28
+ scrollEmojiAboveLabel(emoji);
26
29
  }
27
30
 
28
31
  export function focusAndClickFirstVisibleEmoji(parent: NullableElement) {
@@ -48,6 +51,7 @@ export function focusNextVisibleEmoji(element: NullableElement) {
48
51
  }
49
52
 
50
53
  focusElement(next);
54
+ scrollEmojiAboveLabel(next);
51
55
  }
52
56
 
53
57
  export function focusPrevVisibleEmoji(element: NullableElement) {
@@ -62,6 +66,7 @@ export function focusPrevVisibleEmoji(element: NullableElement) {
62
66
  }
63
67
 
64
68
  focusElement(prev);
69
+ scrollEmojiAboveLabel(prev);
65
70
  }
66
71
 
67
72
  export function focusVisibleEmojiOneRowUp(
@@ -79,6 +84,7 @@ export function focusVisibleEmojiOneRowUp(
79
84
  }
80
85
 
81
86
  focusElement(prev);
87
+ scrollEmojiAboveLabel(prev);
82
88
  }
83
89
 
84
90
  export function focusVisibleEmojiOneRowDown(element: NullableElement) {
@@ -2,12 +2,20 @@ import { useCallback } from 'react';
2
2
 
3
3
  import { useBodyRef } from '../components/context/ElementRefContext';
4
4
 
5
- import { NullableElement } from './selectors';
5
+ import {
6
+ categoryLabelHeight,
7
+ closestCategory,
8
+ closestScrollBody,
9
+ emojiDistanceFromScrollTop,
10
+ isEmojiBehindLabel,
11
+ NullableElement,
12
+ queryScrollBody
13
+ } from './selectors';
6
14
 
7
15
  export function scrollTo(root: NullableElement, top: number = 0) {
8
- const $eprBody = root ? root.querySelector('.epr-body') : null;
16
+ const $eprBody = queryScrollBody(root);
9
17
 
10
- if (!root || !$eprBody) {
18
+ if (!$eprBody) {
11
19
  return;
12
20
  }
13
21
 
@@ -16,6 +24,18 @@ export function scrollTo(root: NullableElement, top: number = 0) {
16
24
  });
17
25
  }
18
26
 
27
+ export function scrollBy(root: NullableElement, by: number): void {
28
+ const $eprBody = queryScrollBody(root);
29
+
30
+ if (!$eprBody) {
31
+ return;
32
+ }
33
+
34
+ requestAnimationFrame(() => {
35
+ $eprBody.scrollTop = $eprBody.scrollTop + by;
36
+ });
37
+ }
38
+
19
39
  export function useScrollTo() {
20
40
  const BodyRef = useBodyRef();
21
41
 
@@ -30,3 +50,13 @@ export function useScrollTo() {
30
50
  [BodyRef]
31
51
  );
32
52
  }
53
+
54
+ export function scrollEmojiAboveLabel(emoji: NullableElement) {
55
+ if (!isEmojiBehindLabel(emoji)) {
56
+ return;
57
+ }
58
+
59
+ const scrollBody = closestScrollBody(emoji);
60
+ const by = emojiDistanceFromScrollTop(emoji);
61
+ scrollBy(scrollBody, -(categoryLabelHeight(closestCategory(emoji)) - by));
62
+ }
@@ -1,7 +1,7 @@
1
1
  import { DataEmoji } from '../dataUtils/DataTypes';
2
2
  import {
3
3
  emojiByUnified,
4
- unifiedWithoutSkinTone
4
+ unifiedWithoutSkinTone,
5
5
  } from '../dataUtils/emojiSelectors';
6
6
 
7
7
  import { asSelectors, ClassNames } from './classNames';
@@ -13,7 +13,7 @@ export const EmojiButtonSelector = `button${asSelectors(ClassNames.emoji)}`;
13
13
  export const VisibleEmojiSelector = [
14
14
  EmojiButtonSelector,
15
15
  asSelectors(ClassNames.visible),
16
- `:not(${asSelectors(ClassNames.hidden)})`
16
+ `:not(${asSelectors(ClassNames.hidden)})`,
17
17
  ].join('');
18
18
 
19
19
  export function buttonFromTarget(
@@ -22,6 +22,14 @@ export function buttonFromTarget(
22
22
  return emojiElement?.closest(EmojiButtonSelector) ?? null;
23
23
  }
24
24
 
25
+ export function isEmojiButton(element: NullableElement): boolean {
26
+ if (!element) {
27
+ return false;
28
+ }
29
+
30
+ return element.matches(EmojiButtonSelector);
31
+ }
32
+
25
33
  export function emojiFromElement(
26
34
  element: NullableElement
27
35
  ): [DataEmoji, string] | [] {
@@ -71,15 +79,63 @@ export function emojiTrueOffsetTop(element: NullableElement): number {
71
79
  }
72
80
 
73
81
  const button = buttonFromTarget(element);
74
- const categoryWithoutLabel = closestCategoryContent(button);
75
- const category = closestCategory(categoryWithoutLabel);
82
+ const category = closestCategory(button);
76
83
 
77
84
  // compensate for the label height
78
- const labelHeight =
79
- (category?.clientHeight ?? 0) - (categoryWithoutLabel?.clientHeight ?? 0);
85
+ const labelHeight = categoryLabelHeight(category);
80
86
 
81
87
  return elementOffsetTop(button) + elementOffsetTop(category) + labelHeight;
82
88
  }
89
+
90
+ export function categoryLabelHeight(category: NullableElement): number {
91
+ if (!category) {
92
+ return 0;
93
+ }
94
+
95
+ const categoryWithoutLabel = category.querySelector(
96
+ asSelectors(ClassNames.categoryContent)
97
+ );
98
+
99
+ return (
100
+ (category?.clientHeight ?? 0) - (categoryWithoutLabel?.clientHeight ?? 0)
101
+ );
102
+ }
103
+
104
+ export function isEmojiBehindLabel(emoji: NullableElement): boolean {
105
+ if (!emoji) {
106
+ return false;
107
+ }
108
+
109
+ return (
110
+ emojiDistanceFromScrollTop(emoji) <
111
+ categoryLabelHeight(closestCategory(emoji))
112
+ );
113
+ }
114
+
115
+ export function queryScrollBody(root: NullableElement): NullableElement {
116
+ return root
117
+ ? root.matches(asSelectors(ClassNames.scrollBody))
118
+ ? root
119
+ : root.querySelector(asSelectors(ClassNames.scrollBody))
120
+ : null;
121
+ }
122
+
123
+ export function emojiDistanceFromScrollTop(emoji: NullableElement): number {
124
+ if (!emoji) {
125
+ return 0;
126
+ }
127
+
128
+ return emojiTrueOffsetTop(emoji) - (closestScrollBody(emoji)?.scrollTop ?? 0);
129
+ }
130
+
131
+ export function closestScrollBody(element: NullableElement): NullableElement {
132
+ if (!element) {
133
+ return null;
134
+ }
135
+
136
+ return element.closest(asSelectors(ClassNames.scrollBody)) ?? null;
137
+ }
138
+
83
139
  export function emojiTruOffsetLeft(element: NullableElement): number {
84
140
  const button = buttonFromTarget(element);
85
141
  const category = closestCategory(button);
@@ -135,7 +135,7 @@ aside.EmojiPickerReact.epr-main:has(input:placeholder-shown)
135
135
  font-family: sans-serif;
136
136
  }
137
137
 
138
- .EmojiPickerReact button {
138
+ .EmojiPickerReact button.epr-btn {
139
139
  cursor: pointer;
140
140
  border: 0;
141
141
  background: none;
@@ -0,0 +1,22 @@
1
+ import clsx from 'clsx';
2
+ import * as React from 'react';
3
+
4
+ interface Props
5
+ extends React.DetailedHTMLProps<
6
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
7
+ HTMLButtonElement
8
+ > {
9
+ className?: string;
10
+ }
11
+
12
+ export function Button(props: Props) {
13
+ return (
14
+ <button
15
+ type="button"
16
+ {...props}
17
+ className={clsx('epr-btn', props.className)}
18
+ >
19
+ {props.children}
20
+ </button>
21
+ );
22
+ }
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
 
3
+ import { ClassNames } from '../../DomUtils/classNames';
3
4
  import { useOnMouseMove } from '../../hooks/useDisallowMouseMove';
4
5
  import { useMouseDownHandlers } from '../../hooks/useMouseDownHandlers';
5
6
  import { useOnScroll } from '../../hooks/useOnScroll';
@@ -17,7 +18,7 @@ export function Body() {
17
18
  useOnMouseMove();
18
19
 
19
20
  return (
20
- <div className="epr-body" ref={BodyRef}>
21
+ <div className={ClassNames.scrollBody} ref={BodyRef}>
21
22
  <EmojiVariationPicker />
22
23
  <EmojiList />
23
24
  </div>
@@ -13,6 +13,7 @@ import {
13
13
  } from '../../dataUtils/emojiSelectors';
14
14
  import { parseNativeEmoji } from '../../dataUtils/parseNativeEmoji';
15
15
  import { EmojiStyle } from '../../types/exposedTypes';
16
+ import { Button } from '../atoms/Button';
16
17
  import { useEmojisThatFailedToLoadState } from '../context/PickerContext';
17
18
  import './Emoji.css';
18
19
 
@@ -46,7 +47,7 @@ export function ClickableEmoji({
46
47
  const hasVariations = emojiHasVariations(emoji);
47
48
 
48
49
  return (
49
- <button
50
+ <Button
50
51
  className={clsx(ClassNames.emoji, {
51
52
  [ClassNames.hidden]: hidden,
52
53
  [ClassNames.hiddenOnSearch]: hiddenOnSearch,
@@ -57,7 +58,6 @@ export function ClickableEmoji({
57
58
  // @ts-ignore - let's ignore the fact this is not a real react ref, ok?
58
59
  aria-label={emojiName(emoji)}
59
60
  data-full-name={emojiNames(emoji)}
60
- type="button"
61
61
  >
62
62
  <ViewOnlyEmoji
63
63
  unified={unified}
@@ -66,7 +66,7 @@ export function ClickableEmoji({
66
66
  emojiStyle={emojiStyle}
67
67
  lazyLoad={lazyLoad}
68
68
  />
69
- </button>
69
+ </Button>
70
70
  );
71
71
  }
72
72
 
@@ -16,31 +16,59 @@
16
16
  width: 100%;
17
17
  }
18
18
 
19
- .EmojiPickerReact .epr-search-container button.epr-btn-clear-search {
19
+ .EmojiPickerReact
20
+ .epr-search-container
21
+ button.epr-btn-clear-search
22
+ .epr-icn-clear-search {
20
23
  background-image: svg-load(./svg/times.svg);
21
- height: 20px;
22
- width: 20px;
23
24
  background-color: transparent;
24
25
  background-repeat: no-repeat;
26
+ background-size: 20px;
27
+ height: 20px;
28
+ width: 20px;
29
+ }
30
+
31
+ .EmojiPickerReact .epr-search-container button.epr-btn-clear-search {
25
32
  position: absolute;
26
- top: 50%;
27
33
  right: var(--epr-search-bar-inner-padding);
34
+ height: 30px;
35
+ width: 30px;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ top: 50%;
28
40
  transform: translateY(-50%);
29
- background-size: 20px;
41
+ padding: 0;
42
+ border-radius: 50%;
30
43
  }
31
- .EmojiPickerReact .epr-search-container button.epr-btn-clear-search:hover {
44
+
45
+ .EmojiPickerReact
46
+ .epr-search-container
47
+ button.epr-btn-clear-search:hover
48
+ .epr-icn-clear-search,
49
+ .EmojiPickerReact
50
+ .epr-search-container
51
+ button.epr-btn-clear-search:focus
52
+ .epr-icn-clear-search {
32
53
  background-position-y: -20px;
33
54
  }
34
55
 
56
+ .EmojiPickerReact .epr-search-container button.epr-btn-clear-search:hover,
57
+ .EmojiPickerReact .epr-search-container button.epr-btn-clear-search:focus {
58
+ background: var(--epr-hover-bg-color);
59
+ }
60
+
35
61
  .EmojiPickerReact..epr-dark-theme
36
62
  .epr-search-container
37
- button.epr-btn-clear-search {
63
+ button.epr-btn-clear-search
64
+ .epr-icn-clear-search {
38
65
  background-position-y: -40px;
39
66
  }
40
67
 
41
68
  .EmojiPickerReact..epr-dark-theme
42
69
  .epr-search-container
43
- button.epr-btn-clear-search:hover {
70
+ button.epr-btn-clear-search:hover
71
+ .epr-icn-clear-search {
44
72
  background-position-y: -60px;
45
73
  }
46
74
 
@@ -7,8 +7,13 @@ import {
7
7
  useSearchPlaceHolderConfig
8
8
  } from '../../config/useConfig';
9
9
  import { useCloseAllOpenToggles } from '../../hooks/useCloseAllOpenToggles';
10
- import { useClearSearch, useFilter } from '../../hooks/useFilter';
10
+ import {
11
+ getNormalizedSearchTerm,
12
+ useClearSearch,
13
+ useFilter
14
+ } from '../../hooks/useFilter';
11
15
  import Relative from '../Layout/Relative';
16
+ import { Button } from '../atoms/Button';
12
17
  import { useSearchInputRef } from '../context/ElementRefContext';
13
18
 
14
19
  import './Search.css';
@@ -45,11 +50,12 @@ export function Search() {
45
50
  ref={SearchInputRef}
46
51
  />
47
52
  <div className="epr-icn-search" />
48
- <button
53
+ <Button
49
54
  className={clsx('epr-btn-clear-search', 'epr-visible-on-search-only')}
50
55
  onClick={clearSearch}
51
- type="button"
52
- />
56
+ >
57
+ <div className="epr-icn-clear-search" />
58
+ </Button>
53
59
  </Relative>
54
60
  );
55
61
  }
@@ -57,7 +63,9 @@ export function Search() {
57
63
  function CssSearch({ value }: { value: undefined | string }) {
58
64
  return value ? (
59
65
  <style>{`
60
- .EmojiPickerReact button.epr-emoji:not([data-full-name*="${value.toLowerCase()}"]) {
66
+ .EmojiPickerReact button.epr-emoji:not([data-full-name*="${getNormalizedSearchTerm(
67
+ value
68
+ )}"]) {
61
69
  display: none;
62
70
  }
63
71
  `}</style>
@@ -10,6 +10,7 @@ import { useCloseAllOpenToggles } from '../../hooks/useCloseAllOpenToggles';
10
10
  import { useFocusSearchInput } from '../../hooks/useFocus';
11
11
  import { SkinTones } from '../../types/exposedTypes';
12
12
  import Relative from '../Layout/Relative';
13
+ import { Button } from '../atoms/Button';
13
14
  import { useSkinTonePickerRef } from '../context/ElementRefContext';
14
15
  import {
15
16
  useActiveSkinToneState,
@@ -39,8 +40,7 @@ export function SkinTonePicker() {
39
40
  {skinToneVariations.map((skinToneVariation, i) => {
40
41
  const active = skinToneVariation === activeSkinTone;
41
42
  return (
42
- <button
43
- type="button"
43
+ <Button
44
44
  style={{
45
45
  transform: clsx(
46
46
  `translateX(-${i * (isOpen ? 28 : 0)}px)`,
@@ -65,7 +65,7 @@ export function SkinTonePicker() {
65
65
  aria-label={`Skin tone ${
66
66
  skinTonesNamed[skinToneVariation as SkinTones]
67
67
  }`}
68
- ></button>
68
+ ></Button>
69
69
  );
70
70
  })}
71
71
  </div>
@@ -10,13 +10,16 @@ import {
10
10
  } from '../../config/categoryConfig';
11
11
  import { useCategoriesConfig } from '../../config/useConfig';
12
12
  import { useActiveCategoryScrollDetection } from '../../hooks/useActiveCategoryScrollDetection';
13
+ import useIsSearchMode from '../../hooks/useIsSearchMode';
13
14
  import { useScrollCategoryIntoView } from '../../hooks/useScrollCategoryIntoView';
15
+ import { Button } from '../atoms/Button';
14
16
  import { useCategoryNavigationRef } from '../context/ElementRefContext';
15
17
 
16
18
  export function CategoryNavigation() {
17
19
  const [activeCategory, setActiveCategory] = useState<string | null>(null);
18
20
  const scrollCategoryIntoView = useScrollCategoryIntoView();
19
21
  useActiveCategoryScrollDetection(setActiveCategory);
22
+ const isSearchMode = useIsSearchMode();
20
23
 
21
24
  const categoriesConfig = useCategoriesConfig();
22
25
  const CategoryNavigationRef = useCategoryNavigationRef();
@@ -26,8 +29,8 @@ export function CategoryNavigation() {
26
29
  {categoriesConfig.map(categoryConfig => {
27
30
  const category = categoryFromCategoryConfig(categoryConfig);
28
31
  return (
29
- <button
30
- type="button"
32
+ <Button
33
+ tabIndex={isSearchMode ? -1 : 0}
31
34
  className={clsx('epr-cat-btn', `epr-icn-${category}`, {
32
35
  [ClassNames.active]: category === activeCategory
33
36
  })}
@@ -40,6 +40,7 @@ export function basePickerConfig(): PickerConfigInternal {
40
40
  categories: baseCategoriesConfig(),
41
41
  defaultSkinTone: SkinTones.NEUTRAL,
42
42
  emojiStyle: EmojiStyle.APPLE,
43
+ height: 450,
43
44
  lazyLoadEmojis: false,
44
45
  onEmojiClick: function defaultOnClickHandler(
45
46
  // @ts-ignore
@@ -49,15 +50,14 @@ export function basePickerConfig(): PickerConfigInternal {
49
50
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
50
51
  event: MouseEvent
51
52
  ) {},
52
- height: 450,
53
- width: 350,
54
53
  previewConfig: {
55
54
  ...basePreviewConfig
56
55
  },
57
56
  searchPlaceHolder: 'Search',
58
57
  skinTonesDisabled: false,
59
58
  suggestedEmojisMode: SuggestionMode.FREQUENT,
60
- theme: Theme.LIGHT
59
+ theme: Theme.LIGHT,
60
+ width: 350
61
61
  };
62
62
  }
63
63
 
@@ -11,6 +11,8 @@ import {
11
11
  import { DataEmoji } from '../dataUtils/DataTypes';
12
12
  import { emojiNames } from '../dataUtils/emojiSelectors';
13
13
 
14
+ import { useFocusSearchInput } from './useFocus';
15
+
14
16
  function useSetFilterRef() {
15
17
  const filterRef = useFilterRef();
16
18
 
@@ -28,6 +30,7 @@ function useSetFilterRef() {
28
30
  export function useClearSearch() {
29
31
  const applySearch = useApplySearch();
30
32
  const SearchInputRef = useSearchInputRef();
33
+ const focusSearchInput = useFocusSearchInput();
31
34
 
32
35
  return function clearSearch() {
33
36
  if (SearchInputRef.current) {
@@ -35,6 +38,7 @@ export function useClearSearch() {
35
38
  }
36
39
 
37
40
  applySearch('');
41
+ focusSearchInput();
38
42
  };
39
43
  }
40
44
 
@@ -44,10 +48,10 @@ export function useAppendSearch() {
44
48
 
45
49
  return function appendSearch(str: string) {
46
50
  if (SearchInputRef.current) {
47
- SearchInputRef.current.value = SearchInputRef.current.value + str;
48
- applySearch(SearchInputRef.current.value);
51
+ SearchInputRef.current.value = `${SearchInputRef.current.value}${str}`;
52
+ applySearch(getNormalizedSearchTerm(SearchInputRef.current.value));
49
53
  } else {
50
- applySearch(str);
54
+ applySearch(getNormalizedSearchTerm(str));
51
55
  }
52
56
  };
53
57
  }
@@ -66,9 +70,11 @@ export function useFilter() {
66
70
  SearchInputRef
67
71
  };
68
72
 
69
- function onChange(nextValue: string) {
73
+ function onChange(inputValue: string) {
70
74
  const filter = filterRef.current;
71
75
 
76
+ const nextValue = inputValue.toLowerCase();
77
+
72
78
  if (filter?.[nextValue] || nextValue.length <= 1) {
73
79
  return applySearch(nextValue);
74
80
  }
@@ -96,9 +102,11 @@ function useApplySearch() {
96
102
 
97
103
  return function applySearch(searchTerm: string) {
98
104
  requestAnimationFrame(() => {
99
- setSearchTerm(searchTerm.toLowerCase()).then(() => {
100
- scrollTo(PickerMainRef.current, 0);
101
- });
105
+ setSearchTerm(searchTerm ? searchTerm?.toLowerCase() : searchTerm).then(
106
+ () => {
107
+ scrollTo(PickerMainRef.current, 0);
108
+ }
109
+ );
102
110
  });
103
111
  };
104
112
  }
@@ -128,8 +136,7 @@ export function useIsEmojiFiltered(): (unified: string) => boolean {
128
136
  const { current: filter } = useFilterRef();
129
137
  const [searchTerm] = useSearchTermState();
130
138
 
131
- return unified =>
132
- isEmojiFilteredBySearchTerm(unified, filter, searchTerm.toLowerCase());
139
+ return unified => isEmojiFilteredBySearchTerm(unified, filter, searchTerm);
133
140
  }
134
141
 
135
142
  function isEmojiFilteredBySearchTerm(
@@ -168,3 +175,11 @@ function findLongestMatch(
168
175
 
169
176
  return null;
170
177
  }
178
+
179
+ export function getNormalizedSearchTerm(str: string): string {
180
+ if (!str || typeof str !== 'string') {
181
+ return '';
182
+ }
183
+
184
+ return str.trim().toLowerCase();
185
+ }
@@ -73,13 +73,8 @@ function usePickerMainKeyboardEvents() {
73
73
  function onKeyDown(event: KeyboardEvent) {
74
74
  const { key } = event;
75
75
 
76
+ disallowMouseMove();
76
77
  switch (key) {
77
- case KeyboardEvents.ArrowDown:
78
- case KeyboardEvents.ArrowUp:
79
- case KeyboardEvents.ArrowLeft:
80
- case KeyboardEvents.ArrowRight:
81
- disallowMouseMove();
82
- break;
83
78
  // eslint-disable-next-line no-fallthrough
84
79
  case KeyboardEvents.Escape:
85
80
  event.preventDefault();