diff --git a/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx b/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx index 549031cf7..54a1080c2 100644 --- a/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx +++ b/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx @@ -1,16 +1,11 @@ import { Placement } from '@popperjs/core'; import clsx from 'clsx'; import React, { useEffect, useState } from 'react'; -import { useIntl } from 'react-intl'; import { usePopper } from 'react-popper'; -import { changeSetting } from 'soapbox/actions/settings'; import { Emoji, HStack, IconButton } from 'soapbox/components/ui'; -import { getFrequentlyUsedEmojis, messages } from 'soapbox/features/emoji/components/emoji-picker-dropdown'; -import { EmojiPicker as EmojiPickerAsync } from 'soapbox/features/ui/util/async-components'; -import { useAppDispatch, useAppSelector, useSettings, useSoapboxConfig } from 'soapbox/hooks'; - -let EmojiPicker: any; // load asynchronously +import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown'; +import { useSoapboxConfig } from 'soapbox/hooks'; interface IEmojiButton { /** Unicode emoji character. */ @@ -64,28 +59,15 @@ const EmojiSelector: React.FC = ({ offset = [-10, 0], all = true, }): JSX.Element => { - const intl = useIntl(); - const dispatch = useAppDispatch(); - - const frequentlyUsedEmojis = useAppSelector(state => getFrequentlyUsedEmojis(state)); - const settings = useSettings(); - const userTheme = settings.get('themeMode'); - const theme = (userTheme === 'dark' || userTheme === 'light') ? userTheme : 'auto'; const soapboxConfig = useSoapboxConfig(); - const title = intl.formatMessage(messages.emoji); const [expanded, setExpanded] = useState(false); - const [loading, setLoading] = useState(false); // `useRef` won't trigger a re-render, while `useState` does. // https://popper.js.org/react-popper/v2/ const [popperElement, setPopperElement] = useState(null); - const onSkinTone = (skinTone: string) => { - dispatch(changeSetting(['skinTone'], skinTone)); - }; - const handleClickOutside = (event: MouseEvent) => { if ([referenceElement, popperElement].some(el => el?.contains(event.target as Node))) { return; @@ -116,38 +98,6 @@ const EmojiSelector: React.FC = ({ setExpanded(true); }; - const getI18n = () => { - return { - search: intl.formatMessage(messages.emoji_search), - pick: intl.formatMessage(messages.emoji_pick), - search_no_results_1: intl.formatMessage(messages.emoji_oh_no), - search_no_results_2: intl.formatMessage(messages.emoji_not_found), - add_custom: intl.formatMessage(messages.emoji_add_custom), - categories: { - search: intl.formatMessage(messages.search_results), - frequent: intl.formatMessage(messages.recent), - people: intl.formatMessage(messages.people), - nature: intl.formatMessage(messages.nature), - foods: intl.formatMessage(messages.food), - activity: intl.formatMessage(messages.activity), - places: intl.formatMessage(messages.travel), - objects: intl.formatMessage(messages.objects), - symbols: intl.formatMessage(messages.symbols), - flags: intl.formatMessage(messages.flags), - custom: intl.formatMessage(messages.custom), - }, - skins: { - choose: intl.formatMessage(messages.skins_choose), - 1: intl.formatMessage(messages.skins_1), - 2: intl.formatMessage(messages.skins_2), - 3: intl.formatMessage(messages.skins_3), - 4: intl.formatMessage(messages.skins_4), - 5: intl.formatMessage(messages.skins_5), - 6: intl.formatMessage(messages.skins_6), - }, - }; - }; - useEffect(() => () => { document.body.style.overflow = ''; }, []); @@ -176,26 +126,6 @@ const EmojiSelector: React.FC = ({ } }, [expanded, update]); - useEffect(() => { - // fix scrolling focus issue - if (visible && expanded) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = ''; - } - - if (!EmojiPicker) { - setLoading(true); - - EmojiPickerAsync().then(EmojiMart => { - EmojiPicker = EmojiMart.Picker; - - setLoading(false); - }).catch(() => { - setLoading(false); - }); - } - }, [visible, expanded]); return (
= ({ {...attributes.popper} > {expanded ? ( - !loading && onReact(emoji.native)} - recent={frequentlyUsedEmojis} - perLine={8} - skin={onSkinTone} - emojiSize={22} - emojiButtonSize={34} - set='twitter' - theme={theme} - i18n={getI18n()} + ) : ( void condensed?: boolean - render: React.FC<{ - setPopperReference: React.Ref - title?: string - visible?: boolean - loading?: boolean - handleToggle: (e: Event) => void - }> + visible: boolean + setVisible: (value: boolean) => void + update: (() => Promise>) | null } const perLine = 8; @@ -132,9 +125,9 @@ const RenderAfter = ({ children, update }: any) => { return nextTick ? children : null; }; -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - -const EmojiPickerDropdown: React.FC = ({ onPickEmoji, condensed, render: Render }) => { +const EmojiPickerDropdown: React.FC = ({ + onPickEmoji, condensed, visible, setVisible, update, +}) => { const intl = useIntl(); const dispatch = useAppDispatch(); const settings = useSettings(); @@ -145,29 +138,8 @@ const EmojiPickerDropdown: React.FC = ({ onPickEmoji, cond const customEmojis = useAppSelector((state) => getCustomEmojis(state)); const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state)); - const [popperElement, setPopperElement] = useState(null); - const [popperReference, setPopperReference] = useState(null); - const [containerElement, setContainerElement] = useState(null); - - const [visible, setVisible] = useState(false); const [loading, setLoading] = useState(false); - const placement = condensed ? 'bottom-start' : 'top-start'; - const { styles, attributes, update } = usePopper(popperReference, popperElement, { - placement: isMobile(window.innerWidth) ? 'auto' : placement, - }); - - const handleToggle = (e: Event) => { - e.stopPropagation(); - setVisible(!visible); - }; - - const handleDocClick = (e: any) => { - if (!containerElement?.contains(e.target) && !popperElement?.contains(e.target)) { - setVisible(false); - } - }; - const handlePick = (emoji: any) => { setVisible(false); @@ -233,17 +205,6 @@ const EmojiPickerDropdown: React.FC = ({ onPickEmoji, cond }; }; - useEffect(() => { - document.addEventListener('click', handleDocClick, false); - document.addEventListener('touchend', handleDocClick, listenerOptions); - - return function cleanup() { - document.removeEventListener('click', handleDocClick, false); - // @ts-ignore - document.removeEventListener('touchend', handleDocClick, listenerOptions); - }; - }); - useEffect(() => { // fix scrolling focus issue if (visible) { @@ -265,51 +226,26 @@ const EmojiPickerDropdown: React.FC = ({ onPickEmoji, cond } }, [visible]); - // TODO: move to class - const style: React.CSSProperties = !isMobile(window.innerWidth) ? styles.popper : { - ...styles.popper, width: '100%', - }; - return ( -
- - - {createPortal( -
- {visible && ( - - {!loading && ( - - )} - - )} -
, - document.body, - )} -
+ visible ? ( + + {!loading && ( + + )} + + ) : null ); }; diff --git a/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx b/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx index cb9d509ed..f9b7150c4 100644 --- a/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx +++ b/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx @@ -1,35 +1,94 @@ import clsx from 'clsx'; -import React from 'react'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import React, { KeyboardEvent, useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { defineMessages, useIntl } from 'react-intl'; +import { usePopper } from 'react-popper'; import { IconButton } from 'soapbox/components/ui'; +import { isMobile } from 'soapbox/is-mobile'; import EmojiPickerDropdown, { IEmojiPickerDropdown } from '../components/emoji-picker-dropdown'; -const EmojiPickerDropdownWrapper = (props: Omit) => { - return ( - ( - - ) - } +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - {...props} - /> +export const messages = defineMessages({ + emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, +}); + +const EmojiPickerDropdownWrapper = ( + props: Pick, +) => { + const intl = useIntl(); + const title = intl.formatMessage(messages.emoji); + + const [popperElement, setPopperElement] = useState(null); + const [popperReference, setPopperReference] = useState(null); + const [containerElement, setContainerElement] = useState(null); + + const [visible, setVisible] = useState(false); + + const placement = props.condensed ? 'bottom-start' : 'top-start'; + const { styles, attributes, update } = usePopper(popperReference, popperElement, { + placement: isMobile(window.innerWidth) ? 'auto' : placement, + }); + + const handleDocClick = (e: any) => { + if (!containerElement?.contains(e.target) && !popperElement?.contains(e.target)) { + setVisible(false); + } + }; + + const handleToggle = (e: MouseEvent | KeyboardEvent) => { + e.stopPropagation(); + setVisible(!visible); + }; + + // TODO: move to class + const style: React.CSSProperties = !isMobile(window.innerWidth) ? styles.popper : { + ...styles.popper, width: '100%', + }; + + useEffect(() => { + document.addEventListener('click', handleDocClick, false); + document.addEventListener('touchend', handleDocClick, listenerOptions); + + return function cleanup() { + document.removeEventListener('click', handleDocClick, false); + // @ts-ignore + document.removeEventListener('touchend', handleDocClick, listenerOptions); + }; + }); + + return ( +
+ } + tabIndex={0} + /> + + {createPortal( +
+ +
, + document.body, + )} +
); }; diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index dc55b1f83..ddbc0ae9e 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -620,6 +620,7 @@ "email_verifilcation.exists": "This email has already been taken.", "embed.instructions": "Embed this post on your website by copying the code below.", "emoji_button.activity": "Activity", + "emoji_button.add_custom": "Add custom emoji", "emoji_button.custom": "Custom", "emoji_button.flags": "Flags", "emoji_button.food": "Food & Drink", @@ -627,7 +628,16 @@ "emoji_button.nature": "Nature", "emoji_button.not_found": "No emojis found.", "emoji_button.objects": "Objects", + "emoji_button.oh_no": "Oh no!", "emoji_button.people": "People", + "emoji_button.pick": "Pick an emoji…", + "emoji_button.skins_1": "Default", + "emoji_button.skins_2": "Light", + "emoji_button.skins_3": "Medium-Light", + "emoji_button.skins_4": "Medium", + "emoji_button.skins_5": "Medium-Dark", + "emoji_button.skins_6": "Dark", + "emoji_button.skins_choose": "Choose default skin tone", "emoji_button.recent": "Frequently used", "emoji_button.search": "Search…", "emoji_button.search_results": "Search results",