diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.tsx similarity index 68% rename from src/components/views/emojipicker/Category.js rename to src/components/views/emojipicker/Category.tsx index eb3f83dcdf..c4feaac8ae 100644 --- a/src/components/views/emojipicker/Category.js +++ b/src/components/views/emojipicker/Category.tsx @@ -1,5 +1,6 @@ /* Copyright 2019 Tulir Asokan +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); 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 limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, {RefObject} from 'react'; + import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPicker"; -import * as sdk from '../../../index'; +import LazyRenderList from "../elements/LazyRenderList"; +import {DATA_BY_CATEGORY, IEmoji} from "../../../emoji"; +import Emoji from './Emoji'; const OVERFLOW_ROWS = 3; -class Category extends React.PureComponent { - static propTypes = { - emojis: PropTypes.arrayOf(PropTypes.object).isRequired, - name: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, - onMouseEnter: PropTypes.func.isRequired, - onMouseLeave: PropTypes.func.isRequired, - onClick: PropTypes.func.isRequired, - selectedEmojis: PropTypes.instanceOf(Set), - }; +export type CategoryKey = (keyof typeof DATA_BY_CATEGORY) | "recent"; - _renderEmojiRow = (rowIndex) => { +export interface ICategory { + id: CategoryKey; + name: string; + enabled: boolean; + visible: boolean; + ref: RefObject; +} + +interface IProps { + id: string; + name: string; + emojis: IEmoji[]; + selectedEmojis: Set; + heightBefore: number; + viewportHeight: number; + scrollTop: number; + onClick(emoji: IEmoji): void; + onMouseEnter(emoji: IEmoji): void; + onMouseLeave(emoji: IEmoji): void; +} + +class Category extends React.PureComponent { + private renderEmojiRow = (rowIndex: number) => { const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props; const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8); - const Emoji = sdk.getComponent("emojipicker.Emoji"); return (
{ - emojisForRow.map(emoji => - ) + emojisForRow.map(emoji => (( + + ))) }
); }; @@ -52,7 +74,6 @@ class Category extends React.PureComponent { for (let counter = 0; counter < rows.length; ++counter) { rows[counter] = counter; } - const LazyRenderList = sdk.getComponent('elements.LazyRenderList'); const viewportTop = scrollTop; const viewportBottom = viewportTop + viewportHeight; @@ -84,7 +105,7 @@ class Category extends React.PureComponent { height={localHeight} overflowItems={OVERFLOW_ROWS} overflowMargin={0} - renderItem={this._renderEmojiRow}> + renderItem={this.renderEmojiRow}> ); diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.tsx similarity index 81% rename from src/components/views/emojipicker/Emoji.js rename to src/components/views/emojipicker/Emoji.tsx index 36aa4ff782..5d715fb935 100644 --- a/src/components/views/emojipicker/Emoji.js +++ b/src/components/views/emojipicker/Emoji.tsx @@ -1,5 +1,6 @@ /* Copyright 2019 Tulir Asokan +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,18 +16,19 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; + import {MenuItem} from "../../structures/ContextMenu"; +import {IEmoji} from "../../../emoji"; -class Emoji extends React.PureComponent { - static propTypes = { - onClick: PropTypes.func, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, - emoji: PropTypes.object.isRequired, - selectedEmojis: PropTypes.instanceOf(Set), - }; +interface IProps { + emoji: IEmoji; + selectedEmojis?: Set; + onClick(emoji: IEmoji): void; + onMouseEnter(emoji: IEmoji): void; + onMouseLeave(emoji: IEmoji): void; +} +class Emoji extends React.PureComponent { render() { const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props; const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode); diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.tsx similarity index 65% rename from src/components/views/emojipicker/EmojiPicker.js rename to src/components/views/emojipicker/EmojiPicker.tsx index 16a0fc67e7..bf0481c51c 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -1,5 +1,6 @@ /* Copyright 2019 Tulir Asokan +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,25 +16,43 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; - import * as recent from '../../../emojipicker/recent'; -import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji"; +import {DATA_BY_CATEGORY, getEmojiFromUnicode, IEmoji} from "../../../emoji"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import Header from "./Header"; +import Search from "./Search"; +import Preview from "./Preview"; +import QuickReactions from "./QuickReactions"; +import Category, {ICategory, CategoryKey} from "./Category"; export const CATEGORY_HEADER_HEIGHT = 22; export const EMOJI_HEIGHT = 37; export const EMOJIS_PER_ROW = 8; -class EmojiPicker extends React.Component { - static propTypes = { - onChoose: PropTypes.func.isRequired, - selectedEmojis: PropTypes.instanceOf(Set), - showQuickReactions: PropTypes.bool, - }; +interface IProps { + selectedEmojis: Set; + showQuickReactions?: boolean; + onChoose(unicode: string): boolean; +} + +interface IState { + filter: string; + previewEmoji?: IEmoji; + scrollTop: number; + // initial estimation of height, dialog is hardcoded to 450px height. + // should be enough to never have blank rows of emojis as + // 3 rows of overflow are also rendered. The actual value is updated on scroll. + viewportHeight: number; +} + +class EmojiPicker extends React.Component { + private readonly recentlyUsed: IEmoji[]; + private readonly memoizedDataByCategory: Record; + private readonly categories: ICategory[]; + + private bodyRef = React.createRef(); constructor(props) { super(props); @@ -42,9 +61,6 @@ class EmojiPicker extends React.Component { filter: "", previewEmoji: null, scrollTop: 0, - // initial estimation of height, dialog is hardcoded to 450px height. - // should be enough to never have blank rows of emojis as - // 3 rows of overflow are also rendered. The actual value is updated on scroll. viewportHeight: 280, }; @@ -110,18 +126,9 @@ class EmojiPicker extends React.Component { visible: false, ref: React.createRef(), }]; - - this.bodyRef = React.createRef(); - - this.onChangeFilter = this.onChangeFilter.bind(this); - this.onHoverEmoji = this.onHoverEmoji.bind(this); - this.onHoverEmojiEnd = this.onHoverEmojiEnd.bind(this); - this.onClickEmoji = this.onClickEmoji.bind(this); - this.scrollToCategory = this.scrollToCategory.bind(this); - this.updateVisibility = this.updateVisibility.bind(this); } - onScroll = () => { + private onScroll = () => { const body = this.bodyRef.current; this.setState({ scrollTop: body.scrollTop, @@ -130,7 +137,7 @@ class EmojiPicker extends React.Component { this.updateVisibility(); }; - updateVisibility() { + private updateVisibility = () => { const body = this.bodyRef.current; const rect = body.getBoundingClientRect(); for (const cat of this.categories) { @@ -147,21 +154,21 @@ class EmojiPicker extends React.Component { // We update this here instead of through React to avoid re-render on scroll. if (cat.visible) { cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible"); - cat.ref.current.setAttribute("aria-selected", true); - cat.ref.current.setAttribute("tabindex", 0); + cat.ref.current.setAttribute("aria-selected", "true"); + cat.ref.current.setAttribute("tabindex", "0"); } else { cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible"); - cat.ref.current.setAttribute("aria-selected", false); - cat.ref.current.setAttribute("tabindex", -1); + cat.ref.current.setAttribute("aria-selected", "false"); + cat.ref.current.setAttribute("tabindex", "-1"); } } - } + }; - scrollToCategory(category) { + private scrollToCategory = (category: string) => { this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView(); - } + }; - onChangeFilter(filter) { + private onChangeFilter = (filter: string) => { filter = filter.toLowerCase(); // filter is case insensitive stored lower-case for (const cat of this.categories) { let emojis; @@ -181,27 +188,34 @@ class EmojiPicker extends React.Component { // Header underlines need to be updated, but updating requires knowing // where the categories are, so we wait for a tick. setTimeout(this.updateVisibility, 0); - } + }; - onHoverEmoji(emoji) { + private onEnterFilter = () => { + const btn = this.bodyRef.current.querySelector(".mx_EmojiPicker_item"); + if (btn) { + btn.click(); + } + }; + + private onHoverEmoji = (emoji: IEmoji) => { this.setState({ previewEmoji: emoji, }); - } + }; - onHoverEmojiEnd(emoji) { + private onHoverEmojiEnd = (emoji: IEmoji) => { this.setState({ previewEmoji: null, }); - } + }; - onClickEmoji(emoji) { + private onClickEmoji = (emoji: IEmoji) => { if (this.props.onChoose(emoji.unicode) !== false) { recent.add(emoji.unicode); } - } + }; - _categoryHeightForEmojiCount(count) { + private static categoryHeightForEmojiCount(count: number) { if (count === 0) { return 0; } @@ -209,25 +223,37 @@ class EmojiPicker extends React.Component { } render() { - const Header = sdk.getComponent("emojipicker.Header"); - const Search = sdk.getComponent("emojipicker.Search"); - const Category = sdk.getComponent("emojipicker.Category"); - const Preview = sdk.getComponent("emojipicker.Preview"); - const QuickReactions = sdk.getComponent("emojipicker.QuickReactions"); let heightBefore = 0; return (
-
- - this.bodyRef.current = e} onScroll={this.onScroll}> +
+ + { + // @ts-ignore - AutoHideScrollbar should accept a RefObject or fall back to its own instead + this.bodyRef.current = ref + }} + onScroll={this.onScroll} + > {this.categories.map(category => { const emojis = this.memoizedDataByCategory[category.id]; - const categoryElement = (); - const height = this._categoryHeightForEmojiCount(emojis.length); + const categoryElement = (( + + )); + const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length); heightBefore += height; return categoryElement; })} diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.tsx similarity index 83% rename from src/components/views/emojipicker/Header.js rename to src/components/views/emojipicker/Header.tsx index c53437e02d..9a93722483 100644 --- a/src/components/views/emojipicker/Header.js +++ b/src/components/views/emojipicker/Header.tsx @@ -1,5 +1,6 @@ /* Copyright 2019 Tulir Asokan +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,19 +16,19 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from "classnames"; import {_t} from "../../../languageHandler"; import {Key} from "../../../Keyboard"; +import {CategoryKey, ICategory} from "./Category"; -class Header extends React.PureComponent { - static propTypes = { - categories: PropTypes.arrayOf(PropTypes.object).isRequired, - onAnchorClick: PropTypes.func.isRequired, - }; +interface IProps { + categories: ICategory[]; + onAnchorClick(id: CategoryKey): void +} - findNearestEnabled(index, delta) { +class Header extends React.PureComponent { + private findNearestEnabled(index: number, delta: number) { index += this.props.categories.length; const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories]; @@ -37,12 +38,12 @@ class Header extends React.PureComponent { } } - changeCategoryRelative(delta) { + private changeCategoryRelative(delta: number) { const current = this.props.categories.findIndex(c => c.visible); this.changeCategoryAbsolute(current + delta, delta); } - changeCategoryAbsolute(index, delta=1) { + private changeCategoryAbsolute(index: number, delta=1) { const category = this.props.categories[this.findNearestEnabled(index, delta)]; if (category) { this.props.onAnchorClick(category.id); @@ -52,7 +53,7 @@ class Header extends React.PureComponent { // Implements ARIA Tabs with Automatic Activation pattern // https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html - onKeyDown = (ev) => { + private onKeyDown = (ev: React.KeyboardEvent) => { let handled = true; switch (ev.key) { case Key.ARROW_LEFT: @@ -80,7 +81,12 @@ class Header extends React.PureComponent { render() { return ( -