Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 01031e3

Browse files
authored
Merge pull request #5257 from matrix-org/t3chguy/fix/14112
Choose first result on enter in the emoji picker
2 parents 489c5b9 + abd7bf3 commit 01031e3

File tree

8 files changed

+237
-161
lines changed

8 files changed

+237
-161
lines changed

src/components/views/emojipicker/Category.js renamed to src/components/views/emojipicker/Category.tsx

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
Copyright 2019 Tulir Asokan <[email protected]>
3+
Copyright 2020 The Matrix.org Foundation C.I.C.
34
45
Licensed under the Apache License, Version 2.0 (the "License");
56
you may not use this file except in compliance with the License.
@@ -14,32 +15,53 @@ See the License for the specific language governing permissions and
1415
limitations under the License.
1516
*/
1617

17-
import React from 'react';
18-
import PropTypes from 'prop-types';
18+
import React, {RefObject} from 'react';
19+
1920
import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPicker";
20-
import * as sdk from '../../../index';
21+
import LazyRenderList from "../elements/LazyRenderList";
22+
import {DATA_BY_CATEGORY, IEmoji} from "../../../emoji";
23+
import Emoji from './Emoji';
2124

2225
const OVERFLOW_ROWS = 3;
2326

24-
class Category extends React.PureComponent {
25-
static propTypes = {
26-
emojis: PropTypes.arrayOf(PropTypes.object).isRequired,
27-
name: PropTypes.string.isRequired,
28-
id: PropTypes.string.isRequired,
29-
onMouseEnter: PropTypes.func.isRequired,
30-
onMouseLeave: PropTypes.func.isRequired,
31-
onClick: PropTypes.func.isRequired,
32-
selectedEmojis: PropTypes.instanceOf(Set),
33-
};
27+
export type CategoryKey = (keyof typeof DATA_BY_CATEGORY) | "recent";
28+
29+
export interface ICategory {
30+
id: CategoryKey;
31+
name: string;
32+
enabled: boolean;
33+
visible: boolean;
34+
ref: RefObject<HTMLButtonElement>;
35+
}
36+
37+
interface IProps {
38+
id: string;
39+
name: string;
40+
emojis: IEmoji[];
41+
selectedEmojis: Set<string>;
42+
heightBefore: number;
43+
viewportHeight: number;
44+
scrollTop: number;
45+
onClick(emoji: IEmoji): void;
46+
onMouseEnter(emoji: IEmoji): void;
47+
onMouseLeave(emoji: IEmoji): void;
48+
}
3449

35-
_renderEmojiRow = (rowIndex) => {
50+
class Category extends React.PureComponent<IProps> {
51+
private renderEmojiRow = (rowIndex: number) => {
3652
const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props;
3753
const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8);
38-
const Emoji = sdk.getComponent("emojipicker.Emoji");
3954
return (<div key={rowIndex}>{
40-
emojisForRow.map(emoji =>
41-
<Emoji key={emoji.hexcode} emoji={emoji} selectedEmojis={selectedEmojis}
42-
onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />)
55+
emojisForRow.map(emoji => ((
56+
<Emoji
57+
key={emoji.hexcode}
58+
emoji={emoji}
59+
selectedEmojis={selectedEmojis}
60+
onClick={onClick}
61+
onMouseEnter={onMouseEnter}
62+
onMouseLeave={onMouseLeave}
63+
/>
64+
)))
4365
}</div>);
4466
};
4567

@@ -52,7 +74,6 @@ class Category extends React.PureComponent {
5274
for (let counter = 0; counter < rows.length; ++counter) {
5375
rows[counter] = counter;
5476
}
55-
const LazyRenderList = sdk.getComponent('elements.LazyRenderList');
5677

5778
const viewportTop = scrollTop;
5879
const viewportBottom = viewportTop + viewportHeight;
@@ -84,7 +105,7 @@ class Category extends React.PureComponent {
84105
height={localHeight}
85106
overflowItems={OVERFLOW_ROWS}
86107
overflowMargin={0}
87-
renderItem={this._renderEmojiRow}>
108+
renderItem={this.renderEmojiRow}>
88109
</LazyRenderList>
89110
</section>
90111
);

src/components/views/emojipicker/Emoji.js renamed to src/components/views/emojipicker/Emoji.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
Copyright 2019 Tulir Asokan <[email protected]>
3+
Copyright 2020 The Matrix.org Foundation C.I.C.
34
45
Licensed under the Apache License, Version 2.0 (the "License");
56
you may not use this file except in compliance with the License.
@@ -15,18 +16,19 @@ limitations under the License.
1516
*/
1617

1718
import React from 'react';
18-
import PropTypes from 'prop-types';
19+
1920
import {MenuItem} from "../../structures/ContextMenu";
21+
import {IEmoji} from "../../../emoji";
2022

21-
class Emoji extends React.PureComponent {
22-
static propTypes = {
23-
onClick: PropTypes.func,
24-
onMouseEnter: PropTypes.func,
25-
onMouseLeave: PropTypes.func,
26-
emoji: PropTypes.object.isRequired,
27-
selectedEmojis: PropTypes.instanceOf(Set),
28-
};
23+
interface IProps {
24+
emoji: IEmoji;
25+
selectedEmojis?: Set<string>;
26+
onClick(emoji: IEmoji): void;
27+
onMouseEnter(emoji: IEmoji): void;
28+
onMouseLeave(emoji: IEmoji): void;
29+
}
2930

31+
class Emoji extends React.PureComponent<IProps> {
3032
render() {
3133
const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
3234
const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);

src/components/views/emojipicker/EmojiPicker.js renamed to src/components/views/emojipicker/EmojiPicker.tsx

Lines changed: 80 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
Copyright 2019 Tulir Asokan <[email protected]>
3+
Copyright 2020 The Matrix.org Foundation C.I.C.
34
45
Licensed under the Apache License, Version 2.0 (the "License");
56
you may not use this file except in compliance with the License.
@@ -15,25 +16,43 @@ limitations under the License.
1516
*/
1617

1718
import React from 'react';
18-
import PropTypes from 'prop-types';
1919

20-
import * as sdk from '../../../index';
2120
import { _t } from '../../../languageHandler';
22-
2321
import * as recent from '../../../emojipicker/recent';
24-
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";
22+
import {DATA_BY_CATEGORY, getEmojiFromUnicode, IEmoji} from "../../../emoji";
2523
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
24+
import Header from "./Header";
25+
import Search from "./Search";
26+
import Preview from "./Preview";
27+
import QuickReactions from "./QuickReactions";
28+
import Category, {ICategory, CategoryKey} from "./Category";
2629

2730
export const CATEGORY_HEADER_HEIGHT = 22;
2831
export const EMOJI_HEIGHT = 37;
2932
export const EMOJIS_PER_ROW = 8;
3033

31-
class EmojiPicker extends React.Component {
32-
static propTypes = {
33-
onChoose: PropTypes.func.isRequired,
34-
selectedEmojis: PropTypes.instanceOf(Set),
35-
showQuickReactions: PropTypes.bool,
36-
};
34+
interface IProps {
35+
selectedEmojis: Set<string>;
36+
showQuickReactions?: boolean;
37+
onChoose(unicode: string): boolean;
38+
}
39+
40+
interface IState {
41+
filter: string;
42+
previewEmoji?: IEmoji;
43+
scrollTop: number;
44+
// initial estimation of height, dialog is hardcoded to 450px height.
45+
// should be enough to never have blank rows of emojis as
46+
// 3 rows of overflow are also rendered. The actual value is updated on scroll.
47+
viewportHeight: number;
48+
}
49+
50+
class EmojiPicker extends React.Component<IProps, IState> {
51+
private readonly recentlyUsed: IEmoji[];
52+
private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
53+
private readonly categories: ICategory[];
54+
55+
private bodyRef = React.createRef<HTMLDivElement>();
3756

3857
constructor(props) {
3958
super(props);
@@ -42,9 +61,6 @@ class EmojiPicker extends React.Component {
4261
filter: "",
4362
previewEmoji: null,
4463
scrollTop: 0,
45-
// initial estimation of height, dialog is hardcoded to 450px height.
46-
// should be enough to never have blank rows of emojis as
47-
// 3 rows of overflow are also rendered. The actual value is updated on scroll.
4864
viewportHeight: 280,
4965
};
5066

@@ -110,18 +126,9 @@ class EmojiPicker extends React.Component {
110126
visible: false,
111127
ref: React.createRef(),
112128
}];
113-
114-
this.bodyRef = React.createRef();
115-
116-
this.onChangeFilter = this.onChangeFilter.bind(this);
117-
this.onHoverEmoji = this.onHoverEmoji.bind(this);
118-
this.onHoverEmojiEnd = this.onHoverEmojiEnd.bind(this);
119-
this.onClickEmoji = this.onClickEmoji.bind(this);
120-
this.scrollToCategory = this.scrollToCategory.bind(this);
121-
this.updateVisibility = this.updateVisibility.bind(this);
122129
}
123130

124-
onScroll = () => {
131+
private onScroll = () => {
125132
const body = this.bodyRef.current;
126133
this.setState({
127134
scrollTop: body.scrollTop,
@@ -130,7 +137,7 @@ class EmojiPicker extends React.Component {
130137
this.updateVisibility();
131138
};
132139

133-
updateVisibility() {
140+
private updateVisibility = () => {
134141
const body = this.bodyRef.current;
135142
const rect = body.getBoundingClientRect();
136143
for (const cat of this.categories) {
@@ -147,21 +154,21 @@ class EmojiPicker extends React.Component {
147154
// We update this here instead of through React to avoid re-render on scroll.
148155
if (cat.visible) {
149156
cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
150-
cat.ref.current.setAttribute("aria-selected", true);
151-
cat.ref.current.setAttribute("tabindex", 0);
157+
cat.ref.current.setAttribute("aria-selected", "true");
158+
cat.ref.current.setAttribute("tabindex", "0");
152159
} else {
153160
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
154-
cat.ref.current.setAttribute("aria-selected", false);
155-
cat.ref.current.setAttribute("tabindex", -1);
161+
cat.ref.current.setAttribute("aria-selected", "false");
162+
cat.ref.current.setAttribute("tabindex", "-1");
156163
}
157164
}
158-
}
165+
};
159166

160-
scrollToCategory(category) {
167+
private scrollToCategory = (category: string) => {
161168
this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView();
162-
}
169+
};
163170

164-
onChangeFilter(filter) {
171+
private onChangeFilter = (filter: string) => {
165172
filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
166173
for (const cat of this.categories) {
167174
let emojis;
@@ -181,53 +188,72 @@ class EmojiPicker extends React.Component {
181188
// Header underlines need to be updated, but updating requires knowing
182189
// where the categories are, so we wait for a tick.
183190
setTimeout(this.updateVisibility, 0);
184-
}
191+
};
192+
193+
private onEnterFilter = () => {
194+
const btn = this.bodyRef.current.querySelector<HTMLButtonElement>(".mx_EmojiPicker_item");
195+
if (btn) {
196+
btn.click();
197+
}
198+
};
185199

186-
onHoverEmoji(emoji) {
200+
private onHoverEmoji = (emoji: IEmoji) => {
187201
this.setState({
188202
previewEmoji: emoji,
189203
});
190-
}
204+
};
191205

192-
onHoverEmojiEnd(emoji) {
206+
private onHoverEmojiEnd = (emoji: IEmoji) => {
193207
this.setState({
194208
previewEmoji: null,
195209
});
196-
}
210+
};
197211

198-
onClickEmoji(emoji) {
212+
private onClickEmoji = (emoji: IEmoji) => {
199213
if (this.props.onChoose(emoji.unicode) !== false) {
200214
recent.add(emoji.unicode);
201215
}
202-
}
216+
};
203217

204-
_categoryHeightForEmojiCount(count) {
218+
private static categoryHeightForEmojiCount(count: number) {
205219
if (count === 0) {
206220
return 0;
207221
}
208222
return CATEGORY_HEADER_HEIGHT + (Math.ceil(count / EMOJIS_PER_ROW) * EMOJI_HEIGHT);
209223
}
210224

211225
render() {
212-
const Header = sdk.getComponent("emojipicker.Header");
213-
const Search = sdk.getComponent("emojipicker.Search");
214-
const Category = sdk.getComponent("emojipicker.Category");
215-
const Preview = sdk.getComponent("emojipicker.Preview");
216-
const QuickReactions = sdk.getComponent("emojipicker.QuickReactions");
217226
let heightBefore = 0;
218227
return (
219228
<div className="mx_EmojiPicker">
220-
<Header categories={this.categories} defaultCategory="recent" onAnchorClick={this.scrollToCategory} />
221-
<Search query={this.state.filter} onChange={this.onChangeFilter} />
222-
<AutoHideScrollbar className="mx_EmojiPicker_body" wrappedRef={e => this.bodyRef.current = e} onScroll={this.onScroll}>
229+
<Header categories={this.categories} onAnchorClick={this.scrollToCategory} />
230+
<Search query={this.state.filter} onChange={this.onChangeFilter} onEnter={this.onEnterFilter} />
231+
<AutoHideScrollbar
232+
className="mx_EmojiPicker_body"
233+
wrappedRef={ref => {
234+
// @ts-ignore - AutoHideScrollbar should accept a RefObject or fall back to its own instead
235+
this.bodyRef.current = ref
236+
}}
237+
onScroll={this.onScroll}
238+
>
223239
{this.categories.map(category => {
224240
const emojis = this.memoizedDataByCategory[category.id];
225-
const categoryElement = (<Category key={category.id} id={category.id} name={category.name}
226-
heightBefore={heightBefore} viewportHeight={this.state.viewportHeight}
227-
scrollTop={this.state.scrollTop} emojis={emojis} onClick={this.onClickEmoji}
228-
onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd}
229-
selectedEmojis={this.props.selectedEmojis} />);
230-
const height = this._categoryHeightForEmojiCount(emojis.length);
241+
const categoryElement = ((
242+
<Category
243+
key={category.id}
244+
id={category.id}
245+
name={category.name}
246+
heightBefore={heightBefore}
247+
viewportHeight={this.state.viewportHeight}
248+
scrollTop={this.state.scrollTop}
249+
emojis={emojis}
250+
onClick={this.onClickEmoji}
251+
onMouseEnter={this.onHoverEmoji}
252+
onMouseLeave={this.onHoverEmojiEnd}
253+
selectedEmojis={this.props.selectedEmojis}
254+
/>
255+
));
256+
const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length);
231257
heightBefore += height;
232258
return categoryElement;
233259
})}

0 commit comments

Comments
 (0)