Convert emojipicker to typescript
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
559414f97d
commit
6873402666
8 changed files with 211 additions and 160 deletions
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {RefObject} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPicker";
|
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;
|
const OVERFLOW_ROWS = 3;
|
||||||
|
|
||||||
class Category extends React.PureComponent {
|
export type CategoryKey = (keyof typeof DATA_BY_CATEGORY) | "recent";
|
||||||
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),
|
|
||||||
};
|
|
||||||
|
|
||||||
_renderEmojiRow = (rowIndex) => {
|
export interface ICategory {
|
||||||
|
id: CategoryKey;
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
visible: boolean;
|
||||||
|
ref: RefObject<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
emojis: IEmoji[];
|
||||||
|
selectedEmojis: Set<string>;
|
||||||
|
heightBefore: number;
|
||||||
|
viewportHeight: number;
|
||||||
|
scrollTop: number;
|
||||||
|
onClick(emoji: IEmoji): void;
|
||||||
|
onMouseEnter(emoji: IEmoji): void;
|
||||||
|
onMouseLeave(emoji: IEmoji): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Category extends React.PureComponent<IProps> {
|
||||||
|
private renderEmojiRow = (rowIndex: number) => {
|
||||||
const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props;
|
const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props;
|
||||||
const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8);
|
const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8);
|
||||||
const Emoji = sdk.getComponent("emojipicker.Emoji");
|
|
||||||
return (<div key={rowIndex}>{
|
return (<div key={rowIndex}>{
|
||||||
emojisForRow.map(emoji =>
|
emojisForRow.map(emoji => ((
|
||||||
<Emoji key={emoji.hexcode} emoji={emoji} selectedEmojis={selectedEmojis}
|
<Emoji
|
||||||
onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />)
|
key={emoji.hexcode}
|
||||||
|
emoji={emoji}
|
||||||
|
selectedEmojis={selectedEmojis}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
/>
|
||||||
|
)))
|
||||||
}</div>);
|
}</div>);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -52,7 +74,6 @@ class Category extends React.PureComponent {
|
||||||
for (let counter = 0; counter < rows.length; ++counter) {
|
for (let counter = 0; counter < rows.length; ++counter) {
|
||||||
rows[counter] = counter;
|
rows[counter] = counter;
|
||||||
}
|
}
|
||||||
const LazyRenderList = sdk.getComponent('elements.LazyRenderList');
|
|
||||||
|
|
||||||
const viewportTop = scrollTop;
|
const viewportTop = scrollTop;
|
||||||
const viewportBottom = viewportTop + viewportHeight;
|
const viewportBottom = viewportTop + viewportHeight;
|
||||||
|
@ -84,7 +105,7 @@ class Category extends React.PureComponent {
|
||||||
height={localHeight}
|
height={localHeight}
|
||||||
overflowItems={OVERFLOW_ROWS}
|
overflowItems={OVERFLOW_ROWS}
|
||||||
overflowMargin={0}
|
overflowMargin={0}
|
||||||
renderItem={this._renderEmojiRow}>
|
renderItem={this.renderEmojiRow}>
|
||||||
</LazyRenderList>
|
</LazyRenderList>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {MenuItem} from "../../structures/ContextMenu";
|
import {MenuItem} from "../../structures/ContextMenu";
|
||||||
|
import {IEmoji} from "../../../emoji";
|
||||||
|
|
||||||
class Emoji extends React.PureComponent {
|
interface IProps {
|
||||||
static propTypes = {
|
emoji: IEmoji;
|
||||||
onClick: PropTypes.func,
|
selectedEmojis?: Set<string>;
|
||||||
onMouseEnter: PropTypes.func,
|
onClick(emoji: IEmoji): void;
|
||||||
onMouseLeave: PropTypes.func,
|
onMouseEnter(emoji: IEmoji): void;
|
||||||
emoji: PropTypes.object.isRequired,
|
onMouseLeave(emoji: IEmoji): void;
|
||||||
selectedEmojis: PropTypes.instanceOf(Set),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
class Emoji extends React.PureComponent<IProps> {
|
||||||
render() {
|
render() {
|
||||||
const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
|
const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
|
||||||
const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);
|
const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
import * as recent from '../../../emojipicker/recent';
|
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 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 CATEGORY_HEADER_HEIGHT = 22;
|
||||||
export const EMOJI_HEIGHT = 37;
|
export const EMOJI_HEIGHT = 37;
|
||||||
export const EMOJIS_PER_ROW = 8;
|
export const EMOJIS_PER_ROW = 8;
|
||||||
|
|
||||||
class EmojiPicker extends React.Component {
|
interface IProps {
|
||||||
static propTypes = {
|
selectedEmojis: Set<string>;
|
||||||
onChoose: PropTypes.func.isRequired,
|
showQuickReactions?: boolean;
|
||||||
selectedEmojis: PropTypes.instanceOf(Set),
|
onChoose(unicode: string): boolean;
|
||||||
showQuickReactions: PropTypes.bool,
|
}
|
||||||
};
|
|
||||||
|
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<IProps, IState> {
|
||||||
|
private readonly recentlyUsed: IEmoji[];
|
||||||
|
private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
|
||||||
|
private readonly categories: ICategory[];
|
||||||
|
|
||||||
|
private bodyRef = React.createRef<HTMLElement>();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -42,9 +61,6 @@ class EmojiPicker extends React.Component {
|
||||||
filter: "",
|
filter: "",
|
||||||
previewEmoji: null,
|
previewEmoji: null,
|
||||||
scrollTop: 0,
|
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,
|
viewportHeight: 280,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -110,18 +126,9 @@ class EmojiPicker extends React.Component {
|
||||||
visible: false,
|
visible: false,
|
||||||
ref: React.createRef(),
|
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;
|
const body = this.bodyRef.current;
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollTop: body.scrollTop,
|
scrollTop: body.scrollTop,
|
||||||
|
@ -130,7 +137,7 @@ class EmojiPicker extends React.Component {
|
||||||
this.updateVisibility();
|
this.updateVisibility();
|
||||||
};
|
};
|
||||||
|
|
||||||
updateVisibility() {
|
private updateVisibility = () => {
|
||||||
const body = this.bodyRef.current;
|
const body = this.bodyRef.current;
|
||||||
const rect = body.getBoundingClientRect();
|
const rect = body.getBoundingClientRect();
|
||||||
for (const cat of this.categories) {
|
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.
|
// We update this here instead of through React to avoid re-render on scroll.
|
||||||
if (cat.visible) {
|
if (cat.visible) {
|
||||||
cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
|
cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
|
||||||
cat.ref.current.setAttribute("aria-selected", true);
|
cat.ref.current.setAttribute("aria-selected", "true");
|
||||||
cat.ref.current.setAttribute("tabindex", 0);
|
cat.ref.current.setAttribute("tabindex", "0");
|
||||||
} else {
|
} else {
|
||||||
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
|
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
|
||||||
cat.ref.current.setAttribute("aria-selected", false);
|
cat.ref.current.setAttribute("aria-selected", "false");
|
||||||
cat.ref.current.setAttribute("tabindex", -1);
|
cat.ref.current.setAttribute("tabindex", "-1");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
scrollToCategory(category) {
|
private scrollToCategory = (category: string) => {
|
||||||
this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView();
|
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
|
filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
|
||||||
for (const cat of this.categories) {
|
for (const cat of this.categories) {
|
||||||
let emojis;
|
let emojis;
|
||||||
|
@ -181,27 +188,27 @@ class EmojiPicker extends React.Component {
|
||||||
// Header underlines need to be updated, but updating requires knowing
|
// Header underlines need to be updated, but updating requires knowing
|
||||||
// where the categories are, so we wait for a tick.
|
// where the categories are, so we wait for a tick.
|
||||||
setTimeout(this.updateVisibility, 0);
|
setTimeout(this.updateVisibility, 0);
|
||||||
}
|
};
|
||||||
|
|
||||||
onHoverEmoji(emoji) {
|
private onHoverEmoji = (emoji: IEmoji) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
previewEmoji: emoji,
|
previewEmoji: emoji,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onHoverEmojiEnd(emoji) {
|
private onHoverEmojiEnd = (emoji: IEmoji) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
previewEmoji: null,
|
previewEmoji: null,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onClickEmoji(emoji) {
|
private onClickEmoji = (emoji: IEmoji) => {
|
||||||
if (this.props.onChoose(emoji.unicode) !== false) {
|
if (this.props.onChoose(emoji.unicode) !== false) {
|
||||||
recent.add(emoji.unicode);
|
recent.add(emoji.unicode);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_categoryHeightForEmojiCount(count) {
|
private static categoryHeightForEmojiCount(count: number) {
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -209,25 +216,30 @@ class EmojiPicker extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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;
|
let heightBefore = 0;
|
||||||
return (
|
return (
|
||||||
<div className="mx_EmojiPicker">
|
<div className="mx_EmojiPicker">
|
||||||
<Header categories={this.categories} defaultCategory="recent" onAnchorClick={this.scrollToCategory} />
|
<Header categories={this.categories} onAnchorClick={this.scrollToCategory} />
|
||||||
<Search query={this.state.filter} onChange={this.onChangeFilter} />
|
<Search query={this.state.filter} onChange={this.onChangeFilter} />
|
||||||
<AutoHideScrollbar className="mx_EmojiPicker_body" wrappedRef={e => this.bodyRef.current = e} onScroll={this.onScroll}>
|
<AutoHideScrollbar className="mx_EmojiPicker_body" wrappedRef={this.bodyRef} onScroll={this.onScroll}>
|
||||||
{this.categories.map(category => {
|
{this.categories.map(category => {
|
||||||
const emojis = this.memoizedDataByCategory[category.id];
|
const emojis = this.memoizedDataByCategory[category.id];
|
||||||
const categoryElement = (<Category key={category.id} id={category.id} name={category.name}
|
const categoryElement = ((
|
||||||
heightBefore={heightBefore} viewportHeight={this.state.viewportHeight}
|
<Category
|
||||||
scrollTop={this.state.scrollTop} emojis={emojis} onClick={this.onClickEmoji}
|
key={category.id}
|
||||||
onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd}
|
id={category.id}
|
||||||
selectedEmojis={this.props.selectedEmojis} />);
|
name={category.name}
|
||||||
const height = this._categoryHeightForEmojiCount(emojis.length);
|
heightBefore={heightBefore}
|
||||||
|
viewportHeight={this.state.viewportHeight}
|
||||||
|
scrollTop={this.state.scrollTop}
|
||||||
|
emojis={emojis}
|
||||||
|
onClick={this.onClickEmoji}
|
||||||
|
onMouseEnter={this.onHoverEmoji}
|
||||||
|
onMouseLeave={this.onHoverEmojiEnd}
|
||||||
|
selectedEmojis={this.props.selectedEmojis}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length);
|
||||||
heightBefore += height;
|
heightBefore += height;
|
||||||
return categoryElement;
|
return categoryElement;
|
||||||
})}
|
})}
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
|
import {CategoryKey, ICategory} from "./Category";
|
||||||
|
|
||||||
class Header extends React.PureComponent {
|
interface IProps {
|
||||||
static propTypes = {
|
categories: ICategory[];
|
||||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
onAnchorClick(id: CategoryKey): void
|
||||||
onAnchorClick: PropTypes.func.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
findNearestEnabled(index, delta) {
|
class Header extends React.PureComponent<IProps> {
|
||||||
|
private findNearestEnabled(index: number, delta: number) {
|
||||||
index += this.props.categories.length;
|
index += this.props.categories.length;
|
||||||
const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories];
|
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);
|
const current = this.props.categories.findIndex(c => c.visible);
|
||||||
this.changeCategoryAbsolute(current + delta, delta);
|
this.changeCategoryAbsolute(current + delta, delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeCategoryAbsolute(index, delta=1) {
|
private changeCategoryAbsolute(index: number, delta=1) {
|
||||||
const category = this.props.categories[this.findNearestEnabled(index, delta)];
|
const category = this.props.categories[this.findNearestEnabled(index, delta)];
|
||||||
if (category) {
|
if (category) {
|
||||||
this.props.onAnchorClick(category.id);
|
this.props.onAnchorClick(category.id);
|
||||||
|
@ -52,7 +53,7 @@ class Header extends React.PureComponent {
|
||||||
|
|
||||||
// Implements ARIA Tabs with Automatic Activation pattern
|
// Implements ARIA Tabs with Automatic Activation pattern
|
||||||
// https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html
|
// https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html
|
||||||
onKeyDown = (ev) => {
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
let handled = true;
|
let handled = true;
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.ARROW_LEFT:
|
case Key.ARROW_LEFT:
|
||||||
|
@ -80,7 +81,12 @@ class Header extends React.PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<nav className="mx_EmojiPicker_header" role="tablist" aria-label={_t("Categories")} onKeyDown={this.onKeyDown}>
|
<nav
|
||||||
|
className="mx_EmojiPicker_header"
|
||||||
|
role="tablist"
|
||||||
|
aria-label={_t("Categories")}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
>
|
||||||
{this.props.categories.map(category => {
|
{this.props.categories.map(category => {
|
||||||
const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, {
|
const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, {
|
||||||
mx_EmojiPicker_anchor_visible: category.visible,
|
mx_EmojiPicker_anchor_visible: category.visible,
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,19 +16,21 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
class Preview extends React.PureComponent {
|
import {IEmoji} from "../../../emoji";
|
||||||
static propTypes = {
|
|
||||||
emoji: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
emoji: IEmoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Preview extends React.PureComponent<IProps> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
unicode = "",
|
unicode = "",
|
||||||
annotation = "",
|
annotation = "",
|
||||||
shortcodes: [shortcode = ""],
|
shortcodes: [shortcode = ""],
|
||||||
} = this.props.emoji || {};
|
} = this.props.emoji || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_EmojiPicker_footer mx_EmojiPicker_preview">
|
<div className="mx_EmojiPicker_footer mx_EmojiPicker_preview">
|
||||||
<div className="mx_EmojiPicker_preview_emoji">
|
<div className="mx_EmojiPicker_preview_emoji">
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,11 +16,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {getEmojiFromUnicode} from "../../../emoji";
|
import {getEmojiFromUnicode, IEmoji} from "../../../emoji";
|
||||||
|
import Emoji from "./Emoji";
|
||||||
|
|
||||||
// We use the variation-selector Heart in Quick Reactions for some reason
|
// We use the variation-selector Heart in Quick Reactions for some reason
|
||||||
const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => {
|
const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => {
|
||||||
|
@ -30,36 +30,36 @@ const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
class QuickReactions extends React.Component {
|
interface IProps {
|
||||||
static propTypes = {
|
selectedEmojis?: Set<string>;
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick(emoji: IEmoji): void;
|
||||||
selectedEmojis: PropTypes.instanceOf(Set),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
hover?: IEmoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuickReactions extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
hover: null,
|
hover: null,
|
||||||
};
|
};
|
||||||
this.onMouseEnter = this.onMouseEnter.bind(this);
|
|
||||||
this.onMouseLeave = this.onMouseLeave.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseEnter(emoji) {
|
private onMouseEnter = (emoji: IEmoji) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
hover: emoji,
|
hover: emoji,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onMouseLeave() {
|
private onMouseLeave = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
hover: null,
|
hover: null,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const Emoji = sdk.getComponent("emojipicker.Emoji");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mx_EmojiPicker_footer mx_EmojiPicker_quick mx_EmojiPicker_category">
|
<section className="mx_EmojiPicker_footer mx_EmojiPicker_quick mx_EmojiPicker_category">
|
||||||
<h2 className="mx_EmojiPicker_quick_header mx_EmojiPicker_category_label">
|
<h2 className="mx_EmojiPicker_quick_header mx_EmojiPicker_category_label">
|
||||||
|
@ -72,10 +72,16 @@ class QuickReactions extends React.Component {
|
||||||
}
|
}
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}>
|
<ul className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}>
|
||||||
{QUICK_REACTIONS.map(emoji => <Emoji
|
{QUICK_REACTIONS.map(emoji => ((
|
||||||
key={emoji.hexcode} emoji={emoji} onClick={this.props.onClick}
|
<Emoji
|
||||||
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
|
key={emoji.hexcode}
|
||||||
selectedEmojis={this.props.selectedEmojis} />)}
|
emoji={emoji}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
onMouseEnter={this.onMouseEnter}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
selectedEmojis={this.props.selectedEmojis}
|
||||||
|
/>
|
||||||
|
)))}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,26 +16,29 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import EmojiPicker from "./EmojiPicker";
|
import EmojiPicker from "./EmojiPicker";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
|
||||||
class ReactionPicker extends React.Component {
|
interface IProps {
|
||||||
static propTypes = {
|
mxEvent: MatrixEvent;
|
||||||
mxEvent: PropTypes.object.isRequired,
|
reactions: any; // TODO type this once js-sdk is more typescripted
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished(): void;
|
||||||
reactions: PropTypes.object,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
selectedEmojis: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReactionPicker extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
selectedEmojis: new Set(Object.keys(this.getReactions())),
|
selectedEmojis: new Set(Object.keys(this.getReactions())),
|
||||||
};
|
};
|
||||||
this.onChoose = this.onChoose.bind(this);
|
|
||||||
this.onReactionsChange = this.onReactionsChange.bind(this);
|
|
||||||
this.addListeners();
|
this.addListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +49,7 @@ class ReactionPicker extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addListeners() {
|
private addListeners() {
|
||||||
if (this.props.reactions) {
|
if (this.props.reactions) {
|
||||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||||
|
@ -55,22 +59,13 @@ class ReactionPicker extends React.Component {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this.props.reactions) {
|
if (this.props.reactions) {
|
||||||
this.props.reactions.removeListener(
|
this.props.reactions.removeListener("Relations.add", this.onReactionsChange);
|
||||||
"Relations.add",
|
this.props.reactions.removeListener("Relations.remove", this.onReactionsChange);
|
||||||
this.onReactionsChange,
|
this.props.reactions.removeListener("Relations.redaction", this.onReactionsChange);
|
||||||
);
|
|
||||||
this.props.reactions.removeListener(
|
|
||||||
"Relations.remove",
|
|
||||||
this.onReactionsChange,
|
|
||||||
);
|
|
||||||
this.props.reactions.removeListener(
|
|
||||||
"Relations.redaction",
|
|
||||||
this.onReactionsChange,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getReactions() {
|
private getReactions() {
|
||||||
if (!this.props.reactions) {
|
if (!this.props.reactions) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -81,13 +76,13 @@ class ReactionPicker extends React.Component {
|
||||||
.map(event => [event.getRelation().key, event.getId()]));
|
.map(event => [event.getRelation().key, event.getId()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
onReactionsChange() {
|
private onReactionsChange = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedEmojis: new Set(Object.keys(this.getReactions())),
|
selectedEmojis: new Set(Object.keys(this.getReactions())),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onChoose(reaction) {
|
onChoose = (reaction: string) => {
|
||||||
this.componentWillUnmount();
|
this.componentWillUnmount();
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
const myReactions = this.getReactions();
|
const myReactions = this.getReactions();
|
||||||
|
@ -109,7 +104,7 @@ class ReactionPicker extends React.Component {
|
||||||
dis.dispatch({action: "message_sent"});
|
dis.dispatch({action: "message_sent"});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <EmojiPicker
|
return <EmojiPicker
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,19 +16,16 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
class Search extends React.PureComponent {
|
interface IProps {
|
||||||
static propTypes = {
|
query: string;
|
||||||
query: PropTypes.string.isRequired,
|
onChange(value: string): void;
|
||||||
onChange: PropTypes.func.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
class Search extends React.PureComponent<IProps> {
|
||||||
super(props);
|
private inputRef = React.createRef<HTMLInputElement>();
|
||||||
this.inputRef = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a setTimeout
|
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a setTimeout
|
||||||
|
@ -38,9 +36,11 @@ class Search extends React.PureComponent {
|
||||||
let rightButton;
|
let rightButton;
|
||||||
if (this.props.query) {
|
if (this.props.query) {
|
||||||
rightButton = (
|
rightButton = (
|
||||||
<button onClick={() => this.props.onChange("")}
|
<button
|
||||||
className="mx_EmojiPicker_search_icon mx_EmojiPicker_search_clear"
|
onClick={() => this.props.onChange("")}
|
||||||
title={_t("Cancel search")} />
|
className="mx_EmojiPicker_search_icon mx_EmojiPicker_search_clear"
|
||||||
|
title={_t("Cancel search")}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
rightButton = <span className="mx_EmojiPicker_search_icon" />;
|
rightButton = <span className="mx_EmojiPicker_search_icon" />;
|
||||||
|
@ -48,8 +48,14 @@ class Search extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_EmojiPicker_search">
|
<div className="mx_EmojiPicker_search">
|
||||||
<input autoFocus type="text" placeholder="Search" value={this.props.query}
|
<input
|
||||||
onChange={ev => this.props.onChange(ev.target.value)} ref={this.inputRef} />
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
placeholder="Search"
|
||||||
|
value={this.props.query}
|
||||||
|
onChange={ev => this.props.onChange(ev.target.value)}
|
||||||
|
ref={this.inputRef}
|
||||||
|
/>
|
||||||
{rightButton}
|
{rightButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
Loading…
Reference in a new issue