signal-desktop/ts/components/emoji/EmojiButton.tsx

183 lines
4.9 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2019 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2019-05-24 23:58:27 +00:00
import * as React from 'react';
import type { MutableRefObject } from 'react';
2019-05-24 23:58:27 +00:00
import classNames from 'classnames';
import { get, noop } from 'lodash';
2019-05-24 23:58:27 +00:00
import { Manager, Popper, Reference } from 'react-popper';
2021-07-19 19:26:06 +00:00
import { Emoji } from './Emoji';
import type { Props as EmojiPickerProps } from './EmojiPicker';
import { EmojiPicker } from './EmojiPicker';
import type { LocalizerType } from '../../types/Util';
2022-06-15 17:53:08 +00:00
import { useRefMerger } from '../../hooks/useRefMerger';
import { handleOutsideClick } from '../../util/handleOutsideClick';
import * as KeyboardLayout from '../../services/keyboardLayout';
2019-05-24 23:58:27 +00:00
2022-10-18 17:12:02 +00:00
export enum EmojiButtonVariant {
Normal,
ProfileEditor,
}
export type OwnProps = Readonly<{
className?: string;
closeOnPick?: boolean;
emoji?: string;
i18n: LocalizerType;
onClose?: () => unknown;
emojiButtonApi?: MutableRefObject<EmojiButtonAPI | undefined>;
2022-10-18 17:12:02 +00:00
variant?: EmojiButtonVariant;
}>;
2019-05-24 23:58:27 +00:00
export type Props = OwnProps &
Pick<
EmojiPickerProps,
2019-11-07 21:36:16 +00:00
'doSend' | 'onPickEmoji' | 'onSetSkinTone' | 'recentEmojis' | 'skinTone'
2019-05-24 23:58:27 +00:00
>;
export type EmojiButtonAPI = Readonly<{
close: () => void;
}>;
2022-11-18 00:45:19 +00:00
export const EmojiButton = React.memo(function EmojiButtonInner({
className,
closeOnPick,
emoji,
emojiButtonApi,
i18n,
doSend,
onClose,
onPickEmoji,
skinTone,
onSetSkinTone,
recentEmojis,
variant = EmojiButtonVariant.Normal,
}: Props) {
const [open, setOpen] = React.useState(false);
const buttonRef = React.useRef<HTMLButtonElement | null>(null);
const popperRef = React.useRef<HTMLDivElement | null>(null);
2022-11-18 00:45:19 +00:00
const refMerger = useRefMerger();
const handleClickButton = React.useCallback(() => {
if (open) {
2020-01-08 17:44:54 +00:00
setOpen(false);
2022-11-18 00:45:19 +00:00
} else {
setOpen(true);
}
}, [open, setOpen]);
2019-05-24 23:58:27 +00:00
2022-11-18 00:45:19 +00:00
const handleClose = React.useCallback(() => {
setOpen(false);
if (onClose) {
onClose();
}
}, [setOpen, onClose]);
const api = React.useMemo(
() => ({
close: () => setOpen(false),
}),
[setOpen]
);
if (emojiButtonApi) {
// Using a React.MutableRefObject, so we need to reassign this prop.
// eslint-disable-next-line no-param-reassign
emojiButtonApi.current = api;
}
2022-11-18 00:45:19 +00:00
React.useEffect(() => {
if (!open) {
2020-01-08 17:44:54 +00:00
return noop;
2022-11-18 00:45:19 +00:00
}
2019-05-24 23:58:27 +00:00
2022-11-18 00:45:19 +00:00
return handleOutsideClick(
() => {
handleClose();
return true;
},
{
containerElements: [popperRef, buttonRef],
name: 'EmojiButton',
}
2022-11-18 00:45:19 +00:00
);
}, [open, handleClose]);
2022-11-18 00:45:19 +00:00
// Install keyboard shortcut to open emoji picker
React.useEffect(() => {
const handleKeydown = (event: KeyboardEvent) => {
const { ctrlKey, metaKey, shiftKey } = event;
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && ctrlKey;
const commandOrCtrl = commandKey || controlKey;
const key = KeyboardLayout.lookup(event);
// We don't want to open up if the conversation has any panels open
const panels = document.querySelectorAll('.conversation .panel');
if (panels && panels.length > 1) {
return;
}
2022-11-18 00:45:19 +00:00
if (commandOrCtrl && shiftKey && (key === 'j' || key === 'J')) {
event.stopPropagation();
event.preventDefault();
2019-11-07 21:36:16 +00:00
2022-11-18 00:45:19 +00:00
setOpen(!open);
}
};
document.addEventListener('keydown', handleKeydown);
return () => {
document.removeEventListener('keydown', handleKeydown);
};
}, [open, setOpen]);
return (
<Manager>
<Reference>
{({ ref }) => (
<button
type="button"
ref={refMerger(buttonRef, ref)}
onClick={handleClickButton}
className={classNames(className, {
'module-emoji-button__button': true,
'module-emoji-button__button--active': open,
'module-emoji-button__button--has-emoji': Boolean(emoji),
'module-emoji-button__button--profile-editor':
variant === EmojiButtonVariant.ProfileEditor,
})}
2023-03-30 00:03:25 +00:00
aria-label={i18n('icu:EmojiButton__label')}
2022-11-18 00:45:19 +00:00
>
{emoji && <Emoji emoji={emoji} size={24} />}
</button>
)}
</Reference>
{open ? (
<div ref={popperRef}>
<Popper placement="top-start" strategy="fixed">
{({ ref, style }) => (
<EmojiPicker
ref={ref}
i18n={i18n}
style={style}
onPickEmoji={ev => {
onPickEmoji(ev);
if (closeOnPick) {
handleClose();
}
}}
doSend={doSend}
onClose={handleClose}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
recentEmojis={recentEmojis}
/>
)}
</Popper>
</div>
) : null}
2022-11-18 00:45:19 +00:00
</Manager>
);
});