Merge branch 'icon-picker-ts' into 'develop'

Fix promo panel icon picker, use TS, FC

Closes #1184

See merge request soapbox-pub/soapbox!1906
environments/review-develop-3zknud/deployments/1429
marcin mikołajczak 2 years ago
commit 7e396ccd2f

@ -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 (
<div onKeyDown={this.handleKeyDown}>
<div
ref={this.setTargetRef}
className='h-[38px] w-[38px] text-lg flex items-center justify-center cursor-pointer'
title={title}
aria-label={title}
aria-expanded={active}
role='button'
onClick={this.onToggle}
onKeyDown={this.onToggle}
tabIndex={0}
>
<Icon id={value} />
</div>
<Overlay show={active} placement={placement} target={this.findTarget}>
<IconPickerMenu
custom_emojis={forkAwesomeIcons}
loading={loading}
onClose={this.onHideDropdown}
onPick={onPickEmoji}
/>
</Overlay>
</div>
);
}
}
export default injectIntl(IconPickerDropdown);

@ -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<IIconPickerDropdown> = ({ value, onPickEmoji }) => {
const intl = useIntl();
const [active, setActive] = useState(false);
const [placement, setPlacement] = useState<'bottom' | 'top'>();
const target = useRef(null);
const onShowDropdown: React.KeyboardEventHandler<HTMLDivElement> = ({ target }) => {
setActive(true);
const { top } = (target as any).getBoundingClientRect();
setPlacement(top * 2 < innerHeight ? 'bottom' : 'top');
};
const onHideDropdown = () => {
setActive(false);
};
const onToggle: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
e.stopPropagation();
if (!e.key || e.key === 'Enter') {
if (active) {
onHideDropdown();
} else {
onShowDropdown(e);
}
}
};
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
if (e.key === 'Escape') {
onHideDropdown();
}
};
const title = intl.formatMessage(messages.emoji);
const forkAwesomeIcons = require('../forkawesome.json');
return (
<div onKeyDown={handleKeyDown}>
<div
ref={target}
className='h-[38px] w-[38px] text-lg flex items-center justify-center cursor-pointer'
title={title}
aria-label={title}
aria-expanded={active}
role='button'
onClick={onToggle as any as React.MouseEventHandler<HTMLDivElement>}
onKeyDown={onToggle}
tabIndex={0}
>
<Icon id={value} />
</div>
<Overlay show={active} placement={placement} target={target.current}>
<IconPickerMenu
customEmojis={forkAwesomeIcons}
onClose={onHideDropdown}
onPick={onPickEmoji}
/>
</Overlay>
</div>
);
};
export default IconPickerDropdown;

@ -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 = `<i class="fa fa-${elem.parentNode.getAttribute('title')} fa-hack"></i>`;
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 <div style={{ width: 299 }} />;
}
const data = { compressed: true, categories: [], aliases: [], emojis: [] };
const title = intl.formatMessage(messages.emoji);
const { modifierOpen } = this.state;
return (
<div className={classNames('font-icon-picker emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
<Picker
perLine={8}
emojiSize={22}
include={categoriesSort}
sheetSize={32}
custom={this.buildIcons(custom_emojis)}
color=''
emoji=''
set=''
title={title}
i18n={this.getI18n()}
onClick={this.handleClick}
showPreview={false}
backgroundImageFn={backgroundImageFn}
emojiTooltip
noShowAnchors
data={data}
/>
</div>
);
}
}
export default injectIntl(IconPickerMenu);

@ -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<string, Array<string>>,
onClose: () => void,
onPick: any,
style?: React.CSSProperties,
}
const IconPickerMenu: React.FC<IIconPickerMenu> = ({ customEmojis, onClose, onPick, style }) => {
const intl = useIntl();
const node = useRef<HTMLDivElement | null>(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 = `<i class="fa fa-${(elem.parentNode as any).getAttribute('title')} fa-hack"></i>`;
(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<string, any>) => {
emoji.native = emoji.colons;
onClose();
onPick(emoji);
};
const buildIcons = () => {
const emojis: Record<string, any> = [];
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 (
<div className={classNames('font-icon-picker emoji-picker-dropdown__menu')} style={style} ref={setRef}>
<Picker
perLine={8}
emojiSize={22}
include={categoriesSort}
sheetSize={32}
custom={buildIcons()}
color=''
emoji=''
set=''
title={title}
i18n={getI18n()}
onClick={handleClick}
showPreview={false}
backgroundImageFn={backgroundImageFn}
emojiTooltip
noShowAnchors
data={data}
/>
</div>
);
};
export default IconPickerMenu;
Loading…
Cancel
Save