From 6c8ed2ad658634446952f5865fe2716be1e7dbb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 16 Nov 2022 23:40:56 +0100 Subject: [PATCH] Fix promo panel icon picker, use TS, FC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../components/icon-picker-dropdown.js | 103 ------------ .../components/icon-picker-dropdown.tsx | 85 ++++++++++ .../components/icon-picker-menu.js | 153 ------------------ .../components/icon-picker-menu.tsx | 131 +++++++++++++++ 4 files changed, 216 insertions(+), 256 deletions(-) delete mode 100644 app/soapbox/features/soapbox-config/components/icon-picker-dropdown.js create mode 100644 app/soapbox/features/soapbox-config/components/icon-picker-dropdown.tsx delete mode 100644 app/soapbox/features/soapbox-config/components/icon-picker-menu.js create mode 100644 app/soapbox/features/soapbox-config/components/icon-picker-menu.tsx diff --git a/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.js b/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.js deleted file mode 100644 index 5393f0dc8..000000000 --- a/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.js +++ /dev/null @@ -1,103 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; -import Overlay from 'react-overlays/lib/Overlay'; - -import Icon from 'soapbox/components/icon'; - -import IconPickerMenu from './icon-picker-menu'; - -const messages = defineMessages({ - emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' }, -}); - -class IconPickerDropdown extends React.PureComponent { - - static propTypes = { - frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), - intl: PropTypes.object.isRequired, - onPickEmoji: PropTypes.func.isRequired, - value: PropTypes.string, - }; - - state = { - active: false, - loading: false, - }; - - setRef = (c) => { - this.dropdown = c; - } - - onShowDropdown = ({ target }) => { - this.setState({ active: true }); - - const { top } = target.getBoundingClientRect(); - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); - } - - onHideDropdown = () => { - this.setState({ active: false }); - } - - onToggle = (e) => { - if (!this.state.loading && (!e.key || e.key === 'Enter')) { - if (this.state.active) { - this.onHideDropdown(); - } else { - this.onShowDropdown(e); - } - } - } - - handleKeyDown = e => { - if (e.key === 'Escape') { - this.onHideDropdown(); - } - } - - setTargetRef = c => { - this.target = c; - } - - findTarget = () => { - return this.target; - } - - render() { - const { intl, onPickEmoji, value } = this.props; - const title = intl.formatMessage(messages.emoji); - const { active, loading, placement } = this.state; - const forkAwesomeIcons = require('../forkawesome.json'); - - return ( -
-
- -
- - - - -
- ); - } - -} - -export default injectIntl(IconPickerDropdown); \ No newline at end of file diff --git a/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.tsx b/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.tsx new file mode 100644 index 000000000..d23e9f004 --- /dev/null +++ b/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.tsx @@ -0,0 +1,85 @@ +import React, { useRef, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +// @ts-ignore +import Overlay from 'react-overlays/lib/Overlay'; + +import Icon from 'soapbox/components/icon'; + +import IconPickerMenu from './icon-picker-menu'; + +const messages = defineMessages({ + emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' }, +}); + +interface IIconPickerDropdown { + value: string, + onPickEmoji: React.ChangeEventHandler, +} + +const IconPickerDropdown: React.FC = ({ value, onPickEmoji }) => { + const intl = useIntl(); + + const [active, setActive] = useState(false); + const [placement, setPlacement] = useState<'bottom' | 'top'>(); + + const target = useRef(null); + + const onShowDropdown: React.KeyboardEventHandler = ({ target }) => { + setActive(true); + + const { top } = (target as any).getBoundingClientRect(); + setPlacement(top * 2 < innerHeight ? 'bottom' : 'top'); + }; + + const onHideDropdown = () => { + setActive(false); + }; + + const onToggle: React.KeyboardEventHandler = (e) => { + e.stopPropagation(); + if (!e.key || e.key === 'Enter') { + if (active) { + onHideDropdown(); + } else { + onShowDropdown(e); + } + } + }; + + const handleKeyDown: React.KeyboardEventHandler = (e) => { + if (e.key === 'Escape') { + onHideDropdown(); + } + }; + + const title = intl.formatMessage(messages.emoji); + const forkAwesomeIcons = require('../forkawesome.json'); + + return ( +
+
} + onKeyDown={onToggle} + tabIndex={0} + > + +
+ + + + +
+ ); +}; + +export default IconPickerDropdown; diff --git a/app/soapbox/features/soapbox-config/components/icon-picker-menu.js b/app/soapbox/features/soapbox-config/components/icon-picker-menu.js deleted file mode 100644 index a9b6be2f2..000000000 --- a/app/soapbox/features/soapbox-config/components/icon-picker-menu.js +++ /dev/null @@ -1,153 +0,0 @@ -import classNames from 'clsx'; -import { supportsPassiveEvents } from 'detect-passive-events'; -import Picker from 'emoji-mart/dist-es/components/picker/picker'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' }, - emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' }, - emoji_not_found: { id: 'icon_button.not_found', defaultMessage: 'No icons!! (╯°□°)╯︵ ┻━┻' }, - custom: { id: 'icon_button.icons', defaultMessage: 'Icons' }, - search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' }, -}); - -const backgroundImageFn = () => ''; -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - -const categoriesSort = ['custom']; - -class IconPickerMenu extends React.PureComponent { - - static propTypes = { - custom_emojis: PropTypes.object, - loading: PropTypes.bool, - onClose: PropTypes.func.isRequired, - onPick: PropTypes.func.isRequired, - style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, - intl: PropTypes.object.isRequired, - }; - - static defaultProps = { - style: {}, - loading: true, - }; - - state = { - modifierOpen: false, - placement: null, - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - componentDidMount() { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - - if (!c) return; - - // Nice and dirty hack to display the icons - c.querySelectorAll('button.emoji-mart-emoji > img').forEach(elem => { - const newIcon = document.createElement('span'); - newIcon.innerHTML = ``; - elem.parentNode.replaceChild(newIcon, elem); - }); - } - - getI18n = () => { - const { intl } = this.props; - - return { - search: intl.formatMessage(messages.emoji_search), - notfound: intl.formatMessage(messages.emoji_not_found), - categories: { - search: intl.formatMessage(messages.search_results), - custom: intl.formatMessage(messages.custom), - }, - }; - } - - handleClick = emoji => { - emoji.native = emoji.colons; - - this.props.onClose(); - this.props.onPick(emoji); - } - - buildIcons = (customEmojis, autoplay = false) => { - const emojis = []; - - Object.values(customEmojis).forEach(category => { - category.forEach(function(icon) { - const name = icon.replace('fa fa-', ''); - if (icon !== 'email' && icon !== 'memo') { - emojis.push({ - id: name, - name, - short_names: [name], - emoticons: [], - keywords: [name], - imageUrl: '', - }); - } - }); - }); - - return emojis; - }; - - render() { - const { loading, style, intl, custom_emojis } = this.props; - - if (loading) { - return
; - } - - const data = { compressed: true, categories: [], aliases: [], emojis: [] }; - const title = intl.formatMessage(messages.emoji); - const { modifierOpen } = this.state; - - return ( -
- -
- ); - } - -} - -export default injectIntl(IconPickerMenu); diff --git a/app/soapbox/features/soapbox-config/components/icon-picker-menu.tsx b/app/soapbox/features/soapbox-config/components/icon-picker-menu.tsx new file mode 100644 index 000000000..c88c1afd7 --- /dev/null +++ b/app/soapbox/features/soapbox-config/components/icon-picker-menu.tsx @@ -0,0 +1,131 @@ +import classNames from 'clsx'; +import { supportsPassiveEvents } from 'detect-passive-events'; +// @ts-ignore +import Picker from 'emoji-mart/dist-es/components/picker/picker'; +import React, { useCallback, useEffect, useRef } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +const messages = defineMessages({ + emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' }, + emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' }, + emoji_not_found: { id: 'icon_button.not_found', defaultMessage: 'No icons!! (╯°□°)╯︵ ┻━┻' }, + custom: { id: 'icon_button.icons', defaultMessage: 'Icons' }, + search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' }, +}); + +const backgroundImageFn = () => ''; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +const categoriesSort = ['custom']; + +interface IIconPickerMenu { + customEmojis: Record>, + onClose: () => void, + onPick: any, + style?: React.CSSProperties, +} + +const IconPickerMenu: React.FC = ({ customEmojis, onClose, onPick, style }) => { + const intl = useIntl(); + + const node = useRef(null); + + const handleDocumentClick = useCallback((e: MouseEvent | TouchEvent) => { + if (node.current && !node.current.contains(e.target as Node)) { + onClose(); + } + }, []); + + useEffect(() => { + document.addEventListener('click', handleDocumentClick, false); + document.addEventListener('touchend', handleDocumentClick, listenerOptions); + + return () => { + document.removeEventListener('click', handleDocumentClick, false); + document.removeEventListener('touchend', handleDocumentClick, listenerOptions as any); + + }; + }, []); + + const setRef = (c: HTMLDivElement) => { + node.current = c; + + if (!c) return; + + // Nice and dirty hack to display the icons + c.querySelectorAll('button.emoji-mart-emoji > img').forEach(elem => { + const newIcon = document.createElement('span'); + newIcon.innerHTML = ``; + (elem.parentNode as any).replaceChild(newIcon, elem); + }); + }; + + const getI18n = () => { + + return { + search: intl.formatMessage(messages.emoji_search), + notfound: intl.formatMessage(messages.emoji_not_found), + categories: { + search: intl.formatMessage(messages.search_results), + custom: intl.formatMessage(messages.custom), + }, + }; + }; + + const handleClick = (emoji: Record) => { + emoji.native = emoji.colons; + + onClose(); + onPick(emoji); + }; + + const buildIcons = () => { + const emojis: Record = []; + + Object.values(customEmojis).forEach((category) => { + category.forEach((icon) => { + const name = icon.replace('fa fa-', ''); + if (icon !== 'email' && icon !== 'memo') { + emojis.push({ + id: name, + name, + short_names: [name], + emoticons: [], + keywords: [name], + imageUrl: '', + }); + } + }); + }); + + return emojis; + }; + + const data = { compressed: true, categories: [], aliases: [], emojis: [] }; + const title = intl.formatMessage(messages.emoji); + + return ( +
+ +
+ ); +}; + +export default IconPickerMenu;