Skip to content

Add programmatic search and imperative API #353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function App() {

# Props

The following props are accepted by them picker:
The following props are accepted by the picker:

| Prop | Type | Default | Description |
| ---------------------- | ----------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
Expand All @@ -70,6 +70,9 @@ The following props are accepted by them picker:
| emojiVersion | `string` | - | Allows displaying emojis up to a certain version for compatibility. |
| `height` | `number`/`string` | `450` | Controls the height of the picker. You can provide a number that will be treated as pixel size, or your any accepted css height as string. |
| getEmojiUrl | `Function` | - | Allows to customize the emoji url and provide your own image host. |
| search | string | - | Programmatically set the emoji search query. |
| onReturnFocus | function | - | When `searchDisabled` is set, this function will be called when focus would have been returned to the search input by keyboard navigation. |
| api | `RefObject<EmojiPickerApi>`| - | Pass in a ref that gives you access to an imperative API. See below. |

## Full details

Expand Down Expand Up @@ -186,6 +189,24 @@ import { SkinTones } from 'emoji-picker-react';

* `getEmojiUrl`: `(unified: string, emojiStyle: EmojiStyle) => string` - Allows to customize the emoji url and provide your own image host. The function receives the emoji unified and the emoji style as parameters. The function should return the url of the emoji image.

- `search`: `string` - Sets the current search query used to filter the emoji list. This works regardless of whether `searchDisabled` is set.

- `onReturnFocus`: `() => void` - If `searchDisabled` is set, this function will be called when user keyboard navigation would have returned focus to the search input field. Use this if building a custom search UI.

- `api`: `RefObject<EmojiPickerApi>` - Provides you access to an imperative API that your component can use. These methods are available:
- `takeFocus()` - sets focus to the first interactive element in the picker UI. Similar to pressing <kbd>↓</kbd> when focus is in the picker's search input.
- `activate()` - selects the first visible emoji. Similar to pressing <kbd>Enter</kbd> when focus is in the search input.

For example:
```tsx
function MyComponent() {
const picker = useRef<EmojiPickerApi>(null)
return <div>
<button onClick={() => picker.current?.takeFocus()}>Set focus to picker</button>
<EmojiPicker api={picker} />
</div>;
}
```
# Customization

## Custom Picker Width and Height
Expand Down
5 changes: 4 additions & 1 deletion src/components/header/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { asSelectors, ClassNames } from '../../DomUtils/classNames';
import {
useAutoFocusSearchConfig,
useSearchDisabledConfig,
useSearchPlaceHolderConfig
useSearchPlaceHolderConfig,
useSearchQuery
} from '../../config/useConfig';
import { useCloseAllOpenToggles } from '../../hooks/useCloseAllOpenToggles';
import {
Expand Down Expand Up @@ -48,6 +49,7 @@ export function Search() {
const clearSearch = useClearSearch();
const placeholder = useSearchPlaceHolderConfig();
const autoFocus = useAutoFocusSearchConfig();
const searchQuery = useSearchQuery();
const { onChange } = useFilter();

const input = SearchInputRef?.current;
Expand All @@ -64,6 +66,7 @@ export function Search() {
className="epr-search"
type="text"
placeholder={placeholder}
defaultValue={searchQuery}
onChange={event => {
setInc(inc + 1);
setTimeout(() => {
Expand Down
9 changes: 8 additions & 1 deletion src/components/main/PickerMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import clsx from 'clsx';
import * as React from 'react';

import { ClassNames } from '../../DomUtils/classNames';
import { usePickerSizeConfig, useThemeConfig } from '../../config/useConfig';
import { usePickerSizeConfig, useSearchQuery, useThemeConfig } from '../../config/useConfig';
import useIsSearchMode from '../../hooks/useIsSearchMode';
import { useKeyboardNavigation } from '../../hooks/useKeyboardNavigation';
import { useOnFocus } from '../../hooks/useOnFocus';
import { Theme } from '../../types/exposedTypes';
import { usePickerMainRef } from '../context/ElementRefContext';
import { PickerContextProvider } from '../context/PickerContext';
import './PickerMain.css';
import { useFilter } from '../../hooks/useFilter';

type Props = Readonly<{
children: React.ReactNode;
Expand All @@ -33,6 +34,12 @@ function PickerRootElement({ children }: RootProps) {
const PickerMainRef = usePickerMainRef();
const { height, width } = usePickerSizeConfig();

const filter = useFilter();
const searchQuery = useSearchQuery();
React.useEffect(() => {
filter.onChange(searchQuery)
}, [searchQuery]);

useKeyboardNavigation();
useOnFocus();

Expand Down
6 changes: 6 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { RefObject } from 'react';
import { GetEmojiUrl } from '../components/emoji/Emoji';
import { emojiUrlByUnified } from '../dataUtils/emojiSelectors';
import {
EmojiClickData,
EmojiPickerApi,
EmojiStyle,
SkinTonePickerLocation,
SkinTones,
Expand Down Expand Up @@ -67,6 +69,7 @@ export function basePickerConfig(): PickerConfigInternal {
},
searchDisabled: false,
searchPlaceHolder: 'Search',
search: '',
skinTonePickerLocation: SkinTonePickerLocation.SEARCH,
skinTonesDisabled: false,
suggestedEmojisMode: SuggestionMode.FREQUENT,
Expand Down Expand Up @@ -95,6 +98,9 @@ export type PickerConfigInternal = {
searchDisabled: boolean;
skinTonePickerLocation: SkinTonePickerLocation;
unicodeToHide: Set<string>;
search: string;
api?: RefObject<EmojiPickerApi>;
onReturnFocus?: () => void;
};

export type PreviewConfig = {
Expand Down
15 changes: 15 additions & 0 deletions src/config/useConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,21 @@ export function useGetEmojiUrlConfig(): (
return getEmojiUrl;
}

export function useSearchQuery() {
const { search } = usePickerConfig();
return search;
}

export function useApi() {
const { api } = usePickerConfig();
return api;
}

export function useOnReturnFocus() {
const { onReturnFocus } = usePickerConfig();
return onReturnFocus;
}

function getDimension(dimensionConfig: PickerDimensions): PickerDimensions {
return typeof dimensionConfig === 'number'
? `${dimensionConfig}px`
Expand Down
8 changes: 6 additions & 2 deletions src/hooks/useFocus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import {
useSearchInputRef,
useSkinTonePickerRef
} from '../components/context/ElementRefContext';
import { useOnReturnFocus, useSearchDisabledConfig } from '../config/useConfig';

export function useFocusSearchInput() {
const SearchInputRef = useSearchInputRef();
const searchDisabled = useSearchDisabledConfig();
const onReturnFocus = useOnReturnFocus();

return useCallback(() => {
focusElement(SearchInputRef.current);
}, [SearchInputRef]);
if (searchDisabled && onReturnFocus) onReturnFocus();
else focusElement(SearchInputRef.current);
}, [SearchInputRef, searchDisabled, onReturnFocus]);
}

export function useFocusSkinTonePicker() {
Expand Down
20 changes: 16 additions & 4 deletions src/hooks/useKeyboardNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useImperativeHandle, useMemo } from 'react';

import { hasNextElementSibling } from '../DomUtils/elementPositionInRow';
import {
Expand Down Expand Up @@ -42,7 +42,7 @@ import {
useIsSkinToneInPreview,
useIsSkinToneInSearch
} from './useShouldShowSkinTonePicker';
import { useSearchDisabledConfig } from '../config/useConfig';
import { useApi, useOnReturnFocus, useSearchDisabledConfig } from '../config/useConfig';

enum KeyboardEvents {
ArrowDown = 'ArrowDown',
Expand Down Expand Up @@ -70,9 +70,19 @@ function usePickerMainKeyboardEvents() {
const focusSearchInput = useFocusSearchInput();
const hasOpenToggles = useHasOpenToggles();
const disallowMouseMove = useDisallowMouseMove();
const onReturnFocus = useOnReturnFocus();
const goDownFromSearchInput = useGoDownFromSearchInput();
const BodyRef = useBodyRef();

const closeAllOpenToggles = useCloseAllOpenToggles();

useImperativeHandle(useApi(), () => {
return {
takeFocus: goDownFromSearchInput,
activate: () => focusAndClickFirstVisibleEmoji(BodyRef.current)
}
}, [goDownFromSearchInput]);

const onKeyDown = useMemo(
() =>
function onKeyDown(event: KeyboardEvent) {
Expand All @@ -87,8 +97,10 @@ function usePickerMainKeyboardEvents() {
closeAllOpenToggles();
return;
}
clearSearch();
scrollTo(0);
if (!onReturnFocus) {
clearSearch();
scrollTo(0);
}
focusSearchInput();
break;
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export {
Categories,
EmojiClickData,
SuggestionMode,
SkinTonePickerLocation
SkinTonePickerLocation,
EmojiPickerApi
} from './types/exposedTypes';

export interface Props extends PickerConfig {}
Expand Down
5 changes: 5 additions & 0 deletions src/types/exposedTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,8 @@ export enum SkinTonePickerLocation {
SEARCH = 'SEARCH',
PREVIEW = 'PREVIEW'
}

export type EmojiPickerApi = {
takeFocus: () => void
activate: () => void
}