Pre-alpha: React with any emoji, behind flag
This commit is contained in:
parent
d13c3d3350
commit
0865a5481c
31 changed files with 572 additions and 234 deletions
|
@ -3,7 +3,7 @@ import classNames from 'classnames';
|
|||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { Avatar, Props as AvatarProps } from './Avatar';
|
||||
import { useRestoreFocus } from './hooks';
|
||||
import { useRestoreFocus } from '../util/hooks';
|
||||
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useRestoreFocus } from './hooks';
|
||||
import { useRestoreFocus } from '../util/hooks';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type Props = {
|
||||
|
|
|
@ -8,7 +8,14 @@ import { setup as setupI18n } from '../../../js/modules/i18n';
|
|||
// @ts-ignore
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import { Message, PropsActions, PropsData, PropsHousekeeping } from './Message';
|
||||
import {
|
||||
Message,
|
||||
Props as AllProps,
|
||||
PropsActions,
|
||||
PropsData,
|
||||
PropsHousekeeping,
|
||||
} from './Message';
|
||||
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||
|
||||
const book = storiesOf('Components/Conversation/Message', module);
|
||||
|
||||
|
@ -1267,6 +1274,21 @@ const stories: Array<MessageStory> = [
|
|||
],
|
||||
];
|
||||
|
||||
const renderEmojiPicker: AllProps['renderEmojiPicker'] = ({
|
||||
onClose,
|
||||
onPickEmoji,
|
||||
ref,
|
||||
}) => (
|
||||
<EmojiPicker
|
||||
i18n={setupI18n('en', enMessages)}
|
||||
skinTone={0}
|
||||
onSetSkinTone={action('EmojiPicker::onSetSkinTone')}
|
||||
ref={ref}
|
||||
onClose={onClose}
|
||||
onPickEmoji={onPickEmoji}
|
||||
/>
|
||||
);
|
||||
|
||||
stories.forEach(([chapterTitle, propsArr]) =>
|
||||
book.add(chapterTitle, () =>
|
||||
propsArr.map(
|
||||
|
@ -1294,6 +1316,7 @@ stories.forEach(([chapterTitle, propsArr]) =>
|
|||
{...dataProps}
|
||||
{...makeActionProps()}
|
||||
{...makeHouseKeepingProps()}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
OwnProps as ReactionViewerProps,
|
||||
ReactionViewer,
|
||||
} from './ReactionViewer';
|
||||
import { ReactionPicker } from './ReactionPicker';
|
||||
import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker';
|
||||
import { Emoji } from '../emoji/Emoji';
|
||||
|
||||
import {
|
||||
|
@ -155,7 +155,10 @@ export type PropsActions = {
|
|||
showExpiredOutgoingTapToViewToast: () => unknown;
|
||||
};
|
||||
|
||||
export type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||
export type Props = PropsData &
|
||||
PropsHousekeeping &
|
||||
PropsActions &
|
||||
Pick<ReactionPickerProps, 'renderEmojiPicker'>;
|
||||
|
||||
interface State {
|
||||
expiring: boolean;
|
||||
|
@ -1001,9 +1004,11 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
canReply,
|
||||
direction,
|
||||
disableMenu,
|
||||
i18n,
|
||||
id,
|
||||
isSticker,
|
||||
isTapToView,
|
||||
renderEmojiPicker,
|
||||
replyToMessage,
|
||||
} = this.props;
|
||||
|
||||
|
@ -1121,6 +1126,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
<Popper placement="top">
|
||||
{({ ref, style }) => (
|
||||
<ReactionPicker
|
||||
i18n={i18n}
|
||||
ref={ref}
|
||||
style={style}
|
||||
selected={this.props.selectedReaction}
|
||||
|
@ -1132,6 +1138,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
remove: emoji === this.props.selectedReaction,
|
||||
});
|
||||
}}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
/>
|
||||
)}
|
||||
</Popper>,
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
### Reaction Picker
|
||||
|
||||
#### Base
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
|
||||
<ReactionPicker onPick={e => console.log(`Picked reaction: ${e}`)} />
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
#### Selected
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
|
||||
{['❤️', '👍', '👎', '😂', '😮', '😢', '😡'].map(e => (
|
||||
<div key={e} style={{ height: '100px' }}>
|
||||
<ReactionPicker
|
||||
selected={e}
|
||||
onPick={e => console.log(`Picked reaction: ${e}`)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</util.ConversationContext>
|
||||
```
|
51
ts/components/conversation/ReactionPicker.stories.tsx
Normal file
51
ts/components/conversation/ReactionPicker.stories.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker';
|
||||
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const renderEmojiPicker: ReactionPickerProps['renderEmojiPicker'] = ({
|
||||
onClose,
|
||||
onPickEmoji,
|
||||
ref,
|
||||
}) => (
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
skinTone={0}
|
||||
onSetSkinTone={action('EmojiPicker::onSetSkinTone')}
|
||||
ref={ref}
|
||||
onClose={onClose}
|
||||
onPickEmoji={onPickEmoji}
|
||||
/>
|
||||
);
|
||||
|
||||
storiesOf('Components/Conversation/ReactionPicker', module)
|
||||
.add('Base', () => {
|
||||
return (
|
||||
<ReactionPicker
|
||||
i18n={i18n}
|
||||
onPick={action('onPick')}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Selected Reaction', () => {
|
||||
return ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'].map(e => (
|
||||
<div key={e} style={{ height: '100px' }}>
|
||||
<ReactionPicker
|
||||
i18n={i18n}
|
||||
selected={e}
|
||||
onPick={action('onPick')}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
});
|
|
@ -1,20 +1,34 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Emoji } from '../emoji/Emoji';
|
||||
import { useRestoreFocus } from '../hooks';
|
||||
import { convertShortName } from '../emoji/lib';
|
||||
import { Props as EmojiPickerProps } from '../emoji/EmojiPicker';
|
||||
import { useRestoreFocus } from '../../util/hooks';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type RenderEmojiPickerProps = Pick<Props, 'onClose' | 'style'> &
|
||||
Pick<EmojiPickerProps, 'onPickEmoji'> & {
|
||||
ref: React.Ref<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export type OwnProps = {
|
||||
i18n: LocalizerType;
|
||||
selected?: string;
|
||||
onClose?: () => unknown;
|
||||
onPick: (emoji: string) => unknown;
|
||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement;
|
||||
};
|
||||
|
||||
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
||||
|
||||
const emojis = ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'];
|
||||
|
||||
const getEmojis = () =>
|
||||
emojis.slice(0, window.REACT_ANY_EMOJI ? emojis.length - 1 : emojis.length);
|
||||
|
||||
export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||
({ selected, onClose, onPick, ...rest }, ref) => {
|
||||
({ i18n, selected, onClose, onPick, renderEmojiPicker, style }, ref) => {
|
||||
const [pickingOther, setPickingOther] = React.useState(false);
|
||||
const focusRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Handle escape key
|
||||
|
@ -32,12 +46,24 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
|||
};
|
||||
}, [onClose]);
|
||||
|
||||
// Handle EmojiPicker::onPickEmoji
|
||||
const onPickEmoji: EmojiPickerProps['onPickEmoji'] = React.useCallback(
|
||||
({ shortName, skinTone }) => {
|
||||
onPick(convertShortName(shortName, skinTone));
|
||||
},
|
||||
[onPick]
|
||||
);
|
||||
|
||||
// Focus first button and restore focus on unmount
|
||||
useRestoreFocus(focusRef);
|
||||
|
||||
return (
|
||||
<div {...rest} ref={ref} className="module-reaction-picker">
|
||||
{emojis.map((emoji, index) => {
|
||||
const otherSelected = selected && !getEmojis().includes(selected);
|
||||
|
||||
return pickingOther ? (
|
||||
renderEmojiPicker({ onPickEmoji, onClose, style, ref })
|
||||
) : (
|
||||
<div ref={ref} style={style} className="module-reaction-picker">
|
||||
{getEmojis().map((emoji, index) => {
|
||||
const maybeFocusRef = index === 0 ? focusRef : undefined;
|
||||
|
||||
return (
|
||||
|
@ -55,6 +81,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
|||
e.stopPropagation();
|
||||
onPick(emoji);
|
||||
}}
|
||||
title={emoji}
|
||||
>
|
||||
<div className="module-reaction-picker__emoji-btn__emoji">
|
||||
<Emoji size={48} emoji={emoji} />
|
||||
|
@ -62,6 +89,31 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
|||
</button>
|
||||
);
|
||||
})}
|
||||
{window.REACT_ANY_EMOJI ? (
|
||||
<button
|
||||
className={classNames(
|
||||
'module-reaction-picker__emoji-btn',
|
||||
otherSelected
|
||||
? 'module-reaction-picker__emoji-btn--selected'
|
||||
: 'module-reaction-picker__emoji-btn--more'
|
||||
)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (otherSelected && selected) {
|
||||
onPick(selected);
|
||||
} else {
|
||||
setPickingOther(true);
|
||||
}
|
||||
}}
|
||||
title={i18n('ReactionsViewer--more')}
|
||||
>
|
||||
{otherSelected ? (
|
||||
<div className="module-reaction-picker__emoji-btn__emoji">
|
||||
<Emoji size={48} emoji={selected} />
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
|||
import { ContactName } from './ContactName';
|
||||
import { Avatar, Props as AvatarProps } from '../Avatar';
|
||||
import { Emoji } from '../emoji/Emoji';
|
||||
import { useRestoreFocus } from '../hooks';
|
||||
import { useRestoreFocus } from '../../util/hooks';
|
||||
|
||||
export type Reaction = {
|
||||
emoji: string;
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
### A plain message
|
||||
|
||||
```jsx
|
||||
const item = {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-1',
|
||||
direction: 'incoming',
|
||||
timestamp: Date.now(),
|
||||
authorPhoneNumber: '(202) 555-2001',
|
||||
authorColor: 'green',
|
||||
text: '🔥',
|
||||
},
|
||||
};
|
||||
|
||||
<TimelineItem item={item} i18n={util.i18n} />;
|
||||
```
|
||||
|
||||
### A notification
|
||||
|
||||
```jsx
|
||||
const item = {
|
||||
type: 'timerNotification',
|
||||
data: {
|
||||
type: 'fromOther',
|
||||
phoneNumber: '(202) 555-0000',
|
||||
timespan: '1 hour',
|
||||
},
|
||||
};
|
||||
|
||||
<TimelineItem item={item} i18n={util.i18n} />;
|
||||
```
|
||||
|
||||
### Unknown type
|
||||
|
||||
```jsx
|
||||
const item = {
|
||||
type: 'random',
|
||||
data: {
|
||||
somethin: 'somethin',
|
||||
},
|
||||
};
|
||||
|
||||
<TimelineItem item={item} i18n={util.i18n} />;
|
||||
```
|
||||
|
||||
### Missing itme
|
||||
|
||||
```jsx
|
||||
<TimelineItem item={null} i18n={util.i18n} />
|
||||
```
|
104
ts/components/conversation/TimelineItem.stories.tsx
Normal file
104
ts/components/conversation/TimelineItem.stories.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const renderEmojiPicker: TimelineItemProps['renderEmojiPicker'] = ({
|
||||
onClose,
|
||||
onPickEmoji,
|
||||
ref,
|
||||
}) => (
|
||||
<EmojiPicker
|
||||
i18n={setupI18n('en', enMessages)}
|
||||
skinTone={0}
|
||||
onSetSkinTone={action('EmojiPicker::onSetSkinTone')}
|
||||
ref={ref}
|
||||
onClose={onClose}
|
||||
onPickEmoji={onPickEmoji}
|
||||
/>
|
||||
);
|
||||
|
||||
const getDefaultProps = () => ({
|
||||
conversationId: 'conversation-id',
|
||||
id: 'asdf',
|
||||
isSelected: false,
|
||||
selectMessage: action('selectMessage'),
|
||||
reactToMessage: action('reactToMessage'),
|
||||
clearSelectedMessage: action('clearSelectedMessage'),
|
||||
replyToMessage: action('replyToMessage'),
|
||||
retrySend: action('retrySend'),
|
||||
deleteMessage: action('deleteMessage'),
|
||||
showMessageDetail: action('showMessageDetail'),
|
||||
openConversation: action('openConversation'),
|
||||
showContactDetail: action('showContactDetail'),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
downloadAttachment: action('downloadAttachment'),
|
||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
showExpiredOutgoingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
openLink: action('openLink'),
|
||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||
downloadNewVersion: action('downloadNewVersion'),
|
||||
showIdentity: action('showIdentity'),
|
||||
renderEmojiPicker,
|
||||
});
|
||||
|
||||
storiesOf('Components/Conversation/TimelineItem', module)
|
||||
.add('Plain Message', () => {
|
||||
const item = {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-1',
|
||||
direction: 'incoming',
|
||||
timestamp: Date.now(),
|
||||
authorPhoneNumber: '(202) 555-2001',
|
||||
authorColor: 'green',
|
||||
text: '🔥',
|
||||
},
|
||||
} as TimelineItemProps['item'];
|
||||
|
||||
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||
})
|
||||
.add('Notification', () => {
|
||||
const item = {
|
||||
type: 'timerNotification',
|
||||
data: {
|
||||
type: 'fromOther',
|
||||
phoneNumber: '(202) 555-0000',
|
||||
timespan: '1 hour',
|
||||
},
|
||||
} as TimelineItemProps['item'];
|
||||
|
||||
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||
})
|
||||
.add('Unknown Type', () => {
|
||||
// @ts-ignore: intentional
|
||||
const item = {
|
||||
type: 'random',
|
||||
data: {
|
||||
somethin: 'somethin',
|
||||
},
|
||||
} as TimelineItemProps['item'];
|
||||
|
||||
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||
})
|
||||
.add('Missing Item', () => {
|
||||
// @ts-ignore: intentional
|
||||
const item = null as TimelineItemProps['item'];
|
||||
|
||||
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||
});
|
|
@ -3,6 +3,7 @@ import { LocalizerType } from '../../types/Util';
|
|||
|
||||
import {
|
||||
Message,
|
||||
Props as AllMessageProps,
|
||||
PropsActions as MessageActionsType,
|
||||
PropsData as MessageProps,
|
||||
} from './Message';
|
||||
|
@ -87,7 +88,9 @@ type PropsActionsType = MessageActionsType &
|
|||
UnsupportedMessageActionsType &
|
||||
SafetyNumberActionsType;
|
||||
|
||||
type PropsType = PropsLocalType & PropsActionsType;
|
||||
export type PropsType = PropsLocalType &
|
||||
PropsActionsType &
|
||||
Pick<AllMessageProps, 'renderEmojiPicker'>;
|
||||
|
||||
export class TimelineItem extends React.PureComponent<PropsType> {
|
||||
public render() {
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
#### Default
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<div
|
||||
style={{
|
||||
height: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<EmojiButton
|
||||
i18n={util.i18n}
|
||||
onPickEmoji={e => console.log('onPickEmoji', e)}
|
||||
skinTone={0}
|
||||
onSetSkinTone={t => console.log('onSetSkinTone', t)}
|
||||
onClose={() => console.log('onClose')}
|
||||
recentEmojis={[
|
||||
'grinning',
|
||||
'grin',
|
||||
'joy',
|
||||
'rolling_on_the_floor_laughing',
|
||||
'smiley',
|
||||
'smile',
|
||||
'sweat_smile',
|
||||
'laughing',
|
||||
'wink',
|
||||
'blush',
|
||||
'yum',
|
||||
'sunglasses',
|
||||
'heart_eyes',
|
||||
'kissing_heart',
|
||||
'kissing',
|
||||
'kissing_smiling_eyes',
|
||||
'kissing_closed_eyes',
|
||||
'relaxed',
|
||||
'slightly_smiling_face',
|
||||
'hugging_face',
|
||||
'grinning_face_with_star_eyes',
|
||||
'thinking_face',
|
||||
'face_with_one_eyebrow_raised',
|
||||
'neutral_face',
|
||||
'expressionless',
|
||||
'no_mouth',
|
||||
'face_with_rolling_eyes',
|
||||
'smirk',
|
||||
'persevere',
|
||||
'disappointed_relieved',
|
||||
'open_mouth',
|
||||
'zipper_mouth_face',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</util.ConversationContext>
|
||||
```
|
66
ts/components/emoji/EmojiButton.stories.tsx
Normal file
66
ts/components/emoji/EmojiButton.stories.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { EmojiButton } from './EmojiButton';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
storiesOf('Components/emojis/EmojiButton', module).add('Base', () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<EmojiButton
|
||||
i18n={i18n}
|
||||
onPickEmoji={action('onPickEmoji')}
|
||||
skinTone={0}
|
||||
onSetSkinTone={action('onSetSkinTone')}
|
||||
recentEmojis={[
|
||||
'grinning',
|
||||
'grin',
|
||||
'joy',
|
||||
'rolling_on_the_floor_laughing',
|
||||
'smiley',
|
||||
'smile',
|
||||
'sweat_smile',
|
||||
'laughing',
|
||||
'wink',
|
||||
'blush',
|
||||
'yum',
|
||||
'sunglasses',
|
||||
'heart_eyes',
|
||||
'kissing_heart',
|
||||
'kissing',
|
||||
'kissing_smiling_eyes',
|
||||
'kissing_closed_eyes',
|
||||
'relaxed',
|
||||
'slightly_smiling_face',
|
||||
'hugging_face',
|
||||
'grinning_face_with_star_eyes',
|
||||
'thinking_face',
|
||||
'face_with_one_eyebrow_raised',
|
||||
'neutral_face',
|
||||
'expressionless',
|
||||
'no_mouth',
|
||||
'face_with_rolling_eyes',
|
||||
'smirk',
|
||||
'persevere',
|
||||
'disappointed_relieved',
|
||||
'open_mouth',
|
||||
'zipper_mouth_face',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -1,60 +0,0 @@
|
|||
#### Default
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<EmojiPicker
|
||||
i18n={util.i18n}
|
||||
onPickEmoji={e => console.log('onPickEmoji', e)}
|
||||
onSetSkinTone={t => console.log('onSetSkinTone', t)}
|
||||
onClose={() => console.log('onClose')}
|
||||
recentEmojis={[
|
||||
'grinning',
|
||||
'grin',
|
||||
'joy',
|
||||
'rolling_on_the_floor_laughing',
|
||||
'smiley',
|
||||
'smile',
|
||||
'sweat_smile',
|
||||
'laughing',
|
||||
'wink',
|
||||
'blush',
|
||||
'yum',
|
||||
'sunglasses',
|
||||
'heart_eyes',
|
||||
'kissing_heart',
|
||||
'kissing',
|
||||
'kissing_smiling_eyes',
|
||||
'kissing_closed_eyes',
|
||||
'relaxed',
|
||||
'slightly_smiling_face',
|
||||
'hugging_face',
|
||||
'grinning_face_with_star_eyes',
|
||||
'thinking_face',
|
||||
'face_with_one_eyebrow_raised',
|
||||
'neutral_face',
|
||||
'expressionless',
|
||||
'no_mouth',
|
||||
'face_with_rolling_eyes',
|
||||
'smirk',
|
||||
'persevere',
|
||||
'disappointed_relieved',
|
||||
'open_mouth',
|
||||
'zipper_mouth_face',
|
||||
]}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
#### No Recents
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<EmojiPicker
|
||||
i18n={util.i18n}
|
||||
onPickEmoji={e => console.log('onPickEmoji', e)}
|
||||
onSetSkinTone={t => console.log('onSetSkinTone', t)}
|
||||
onClose={() => console.log('onClose')}
|
||||
recentEmojis={[]}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
71
ts/components/emoji/EmojiPicker.stories.tsx
Normal file
71
ts/components/emoji/EmojiPicker.stories.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { EmojiPicker } from './EmojiPicker';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
storiesOf('Components/emojis/EmojiPicker', module)
|
||||
.add('Base', () => {
|
||||
return (
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
onPickEmoji={action('onPickEmoji')}
|
||||
onSetSkinTone={action('onSetSkinTone')}
|
||||
onClose={action('onClose')}
|
||||
skinTone={0}
|
||||
recentEmojis={[
|
||||
'grinning',
|
||||
'grin',
|
||||
'joy',
|
||||
'rolling_on_the_floor_laughing',
|
||||
'smiley',
|
||||
'smile',
|
||||
'sweat_smile',
|
||||
'laughing',
|
||||
'wink',
|
||||
'blush',
|
||||
'yum',
|
||||
'sunglasses',
|
||||
'heart_eyes',
|
||||
'kissing_heart',
|
||||
'kissing',
|
||||
'kissing_smiling_eyes',
|
||||
'kissing_closed_eyes',
|
||||
'relaxed',
|
||||
'slightly_smiling_face',
|
||||
'hugging_face',
|
||||
'grinning_face_with_star_eyes',
|
||||
'thinking_face',
|
||||
'face_with_one_eyebrow_raised',
|
||||
'neutral_face',
|
||||
'expressionless',
|
||||
'no_mouth',
|
||||
'face_with_rolling_eyes',
|
||||
'smirk',
|
||||
'persevere',
|
||||
'disappointed_relieved',
|
||||
'open_mouth',
|
||||
'zipper_mouth_face',
|
||||
]}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('No Recents', () => {
|
||||
return (
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
onPickEmoji={action('onPickEmoji')}
|
||||
onSetSkinTone={action('onSetSkinTone')}
|
||||
onClose={action('onClose')}
|
||||
skinTone={0}
|
||||
recentEmojis={[]}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from 'lodash';
|
||||
import { Emoji } from './Emoji';
|
||||
import { dataByCategory, search } from './lib';
|
||||
import { useRestoreFocus } from '../hooks';
|
||||
import { useRestoreFocus } from '../../util/hooks';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type EmojiPickDataType = { skinTone?: number; shortName: string };
|
||||
|
@ -29,7 +29,7 @@ export type OwnProps = {
|
|||
readonly skinTone: number;
|
||||
readonly onSetSkinTone: (tone: number) => unknown;
|
||||
readonly recentEmojis?: Array<string>;
|
||||
readonly onClose: () => unknown;
|
||||
readonly onClose?: () => unknown;
|
||||
};
|
||||
|
||||
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
||||
|
@ -83,11 +83,15 @@ export const EmojiPicker = React.memo(
|
|||
const [scrollToRow, setScrollToRow] = React.useState(0);
|
||||
const [selectedTone, setSelectedTone] = React.useState(skinTone);
|
||||
|
||||
const handleToggleSearch = React.useCallback(() => {
|
||||
setSearchText('');
|
||||
setSelectedCategory(categories[0]);
|
||||
setSearchMode(m => !m);
|
||||
}, [setSearchText, setSearchMode]);
|
||||
const handleToggleSearch = React.useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setSearchText('');
|
||||
setSelectedCategory(categories[0]);
|
||||
setSearchMode(m => !m);
|
||||
},
|
||||
[setSearchText, setSearchMode]
|
||||
);
|
||||
|
||||
const debounceSearchChange = React.useMemo(
|
||||
() =>
|
||||
|
@ -122,15 +126,16 @@ export const EmojiPicker = React.memo(
|
|||
| React.KeyboardEvent<HTMLButtonElement>
|
||||
) => {
|
||||
if ('key' in e) {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === 'Enter' && doSend) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (doSend) {
|
||||
doSend();
|
||||
}
|
||||
doSend();
|
||||
}
|
||||
} else {
|
||||
const { shortName } = e.currentTarget.dataset;
|
||||
if (shortName) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onPickEmoji({ skinTone: selectedTone, shortName });
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +165,9 @@ export const EmojiPicker = React.memo(
|
|||
' ', // Space
|
||||
].includes(event.key)
|
||||
) {
|
||||
onClose();
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -224,8 +231,9 @@ export const EmojiPicker = React.memo(
|
|||
);
|
||||
|
||||
const handleSelectCategory = React.useCallback(
|
||||
({ currentTarget }: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const { category } = currentTarget.dataset;
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
const { category } = e.currentTarget.dataset;
|
||||
if (category) {
|
||||
setSelectedCategory(category);
|
||||
setScrollToRow(catToRowOffsets[category]);
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Restore focus on teardown
|
||||
export const useRestoreFocus = (
|
||||
// The ref for the element to receive initial focus
|
||||
focusRef: React.RefObject<any>,
|
||||
// Allow for an optional root element that must exist
|
||||
root: boolean | HTMLElement | null = true
|
||||
) => {
|
||||
React.useEffect(() => {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastFocused = document.activeElement as any;
|
||||
if (focusRef.current) {
|
||||
focusRef.current.focus();
|
||||
}
|
||||
|
||||
return () => {
|
||||
// This ensures that the focus is returned to
|
||||
// previous element
|
||||
setTimeout(() => {
|
||||
if (lastFocused && lastFocused.focus) {
|
||||
lastFocused.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [focusRef, root]);
|
||||
};
|
|
@ -2,7 +2,7 @@
|
|||
/* tslint:disable:cyclomatic-complexity */
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useRestoreFocus } from '../hooks';
|
||||
import { useRestoreFocus } from '../../util/hooks';
|
||||
import { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { ConfirmationDialog } from '../ConfirmationDialog';
|
|||
import { LocalizerType } from '../../types/Util';
|
||||
import { StickerPackType } from '../../state/ducks/stickers';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { useRestoreFocus } from '../hooks';
|
||||
import { useRestoreFocus } from '../../util/hooks';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly onClose: () => unknown;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue