113 lines
3.1 KiB
TypeScript
113 lines
3.1 KiB
TypeScript
import {
|
|
type CSSProperties,
|
|
type KeyboardEvent,
|
|
type Ref,
|
|
useEffect,
|
|
} from "react";
|
|
import {
|
|
EmojiPicker,
|
|
type Locale as EmojiLocale,
|
|
useSkinTone,
|
|
} from "frimousse";
|
|
import { t } from "@lingui/core/macro";
|
|
import { Trans } from "@lingui/react/macro";
|
|
import { useLingui } from "@lingui/react";
|
|
import { readSkinTone, SKIN_TONE_KEY } from "./skinTone.ts";
|
|
|
|
/** Remembers the skin tone picked in the selector for the next picker. */
|
|
function SkinTonePersist() {
|
|
const [skinTone] = useSkinTone();
|
|
useEffect(() => {
|
|
try {
|
|
localStorage.setItem(SKIN_TONE_KEY, skinTone);
|
|
} catch {
|
|
// Private mode: the tone lasts as long as the picker.
|
|
}
|
|
}, [skinTone]);
|
|
return null;
|
|
}
|
|
|
|
interface EmojiPickerPopupProps {
|
|
floatRef: Ref<HTMLDivElement>;
|
|
searchRef: Ref<HTMLInputElement>;
|
|
viewportRef: Ref<HTMLDivElement>;
|
|
style: CSSProperties;
|
|
/** Inline, the query is typed in the textarea and the search only shows it;
|
|
* browsing, the search field takes it. */
|
|
inline: boolean;
|
|
query: string;
|
|
onQueryChange: (query: string) => void;
|
|
onSelect: (emoji: string) => void;
|
|
onClose: () => void;
|
|
onKeyDownCapture: (e: KeyboardEvent<HTMLDivElement>) => void;
|
|
}
|
|
|
|
/** The floating emoji picker the editor opens from its button or a `:query`. */
|
|
export function EmojiPickerPopup(
|
|
{
|
|
floatRef,
|
|
searchRef,
|
|
viewportRef,
|
|
style,
|
|
inline,
|
|
query,
|
|
onQueryChange,
|
|
onSelect,
|
|
onClose,
|
|
onKeyDownCapture,
|
|
}: EmojiPickerPopupProps,
|
|
) {
|
|
const { i18n } = useLingui();
|
|
return (
|
|
<div
|
|
ref={floatRef}
|
|
className="emoji-picker-float"
|
|
style={style}
|
|
onKeyDownCapture={onKeyDownCapture}
|
|
>
|
|
<EmojiPicker.Root
|
|
onEmojiSelect={(e) => onSelect(e.emoji)}
|
|
locale={i18n.locale as EmojiLocale}
|
|
emojibaseUrl={import.meta.env.VITE_EMOJIBASE_URL}
|
|
skinTone={readSkinTone()}
|
|
>
|
|
<SkinTonePersist />
|
|
<div className="emoji-picker-search-row">
|
|
<EmojiPicker.Search
|
|
ref={searchRef}
|
|
value={query}
|
|
onChange={(e) => onQueryChange(e.target.value)}
|
|
placeholder={t`Search emoji…`}
|
|
aria-label={t`Search emoji`}
|
|
readOnly={inline}
|
|
tabIndex={inline ? -1 : undefined}
|
|
/>
|
|
<EmojiPicker.SkinToneSelector aria-label={t`Change skin tone`} />
|
|
<button
|
|
type="button"
|
|
className="emoji-picker-close-btn"
|
|
onClick={onClose}
|
|
aria-label={t`Close emoji picker`}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
<EmojiPicker.Viewport
|
|
ref={viewportRef}
|
|
// tabIndex={-1} makes the div programmatically focusable so
|
|
// frimousse's onFocusCapture can detect it and arm arrow-key nav
|
|
tabIndex={-1}
|
|
>
|
|
<EmojiPicker.Loading>
|
|
<Trans>Loading…</Trans>
|
|
</EmojiPicker.Loading>
|
|
<EmojiPicker.Empty>
|
|
<Trans>No emoji found.</Trans>
|
|
</EmojiPicker.Empty>
|
|
<EmojiPicker.List />
|
|
</EmojiPicker.Viewport>
|
|
</EmojiPicker.Root>
|
|
</div>
|
|
);
|
|
}
|