= ({ inputRef, children, endAdornment
tabIndex={-1}
aria-selected={isActive}
role="option"
+ element="li"
>
{children}
diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx
index f603a4b95709..305cee51967f 100644
--- a/src/components/views/elements/Dropdown.tsx
+++ b/src/components/views/elements/Dropdown.tsx
@@ -30,7 +30,7 @@ interface IMenuOptionProps {
highlighted?: boolean;
dropdownKey: string;
id?: string;
- inputRef?: Ref
;
+ inputRef?: Ref;
onClick(dropdownKey: string): void;
onMouseEnter(dropdownKey: string): void;
}
@@ -57,7 +57,7 @@ class MenuOption extends React.Component {
});
return (
- {
ref={this.props.inputRef}
>
{this.props.children}
-
+
);
}
}
@@ -78,6 +78,7 @@ export interface DropdownProps {
label: string;
value?: string;
className?: string;
+ autoComplete?: string;
children: NonEmptyArray;
// negative for consistency with HTML
disabled?: boolean;
@@ -318,21 +319,21 @@ export default class Dropdown extends React.Component {
});
if (!options?.length) {
return [
-
+
{_t("No results")}
- ,
+ ,
];
}
return options;
}
public render(): React.ReactNode {
- let currentValue;
+ let currentValue: JSX.Element | undefined;
const menuStyle: CSSProperties = {};
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
- let menu;
+ let menu: JSX.Element | undefined;
if (this.state.expanded) {
if (this.props.searchEnabled) {
currentValue = (
@@ -340,6 +341,7 @@ export default class Dropdown extends React.Component {
id={`${this.props.id}_input`}
type="text"
autoFocus={true}
+ autoComplete={this.props.autoComplete}
className="mx_Dropdown_option"
onChange={this.onInputChange}
value={this.state.searchQuery}
@@ -355,9 +357,9 @@ export default class Dropdown extends React.Component {
);
}
menu = (
-
+
{this.getMenuOptions()}
-
+
);
}
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index eaa41903f700..2648aac6a706 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -128,8 +128,8 @@ export default class EventTilePreview extends React.Component {
const event = this.fakeEvent(this.state);
return (
-
-
+
+
);
}
diff --git a/src/components/views/elements/LabelledToggleSwitch.tsx b/src/components/views/elements/LabelledToggleSwitch.tsx
index 4455b16a9f38..2a1540920b2b 100644
--- a/src/components/views/elements/LabelledToggleSwitch.tsx
+++ b/src/components/views/elements/LabelledToggleSwitch.tsx
@@ -16,6 +16,7 @@ limitations under the License.
import React from "react";
import classNames from "classnames";
+import { randomString } from "matrix-js-sdk/src/randomstring";
import ToggleSwitch from "./ToggleSwitch";
import { Caption } from "../typography/Caption";
@@ -43,18 +44,15 @@ interface IProps {
}
export default class LabelledToggleSwitch extends React.PureComponent
{
+ private readonly id = `mx_LabelledToggleSwitch_${randomString(12)}`;
+
public render(): React.ReactNode {
// This is a minimal version of a SettingsFlag
const { label, caption } = this.props;
let firstPart = (
- {label}
- {caption && (
- <>
-
- {caption}
- >
- )}
+ {label}
+ {caption && {caption} }
);
let secondPart = (
@@ -62,15 +60,14 @@ export default class LabelledToggleSwitch extends React.PureComponent {
checked={this.props.value}
disabled={this.props.disabled}
onChange={this.props.onChange}
- title={this.props.label}
tooltip={this.props.tooltip}
+ aria-labelledby={this.id}
+ aria-describedby={caption ? `${this.id}_caption` : undefined}
/>
);
if (this.props.toggleInFront) {
- const temp = firstPart;
- firstPart = secondPart;
- secondPart = temp;
+ [firstPart, secondPart] = [secondPart, firstPart];
}
const classes = classNames("mx_SettingsFlag", this.props.className, {
diff --git a/src/components/views/elements/LazyRenderList.tsx b/src/components/views/elements/LazyRenderList.tsx
index 0a041730339c..802e60ca1968 100644
--- a/src/components/views/elements/LazyRenderList.tsx
+++ b/src/components/views/elements/LazyRenderList.tsx
@@ -73,6 +73,7 @@ interface IProps {
element?: string;
className?: string;
+ role?: string;
}
interface IState {
@@ -128,6 +129,7 @@ export default class LazyRenderList extends React.Component,
const elementProps = {
style: { paddingTop: `${paddingTop}px`, paddingBottom: `${paddingBottom}px` },
className: this.props.className,
+ role: this.props.role,
};
return React.createElement(element, elementProps, renderedItems.map(renderItem));
}
diff --git a/src/components/views/elements/ToggleSwitch.tsx b/src/components/views/elements/ToggleSwitch.tsx
index f29405ba8d5f..588374d17b67 100644
--- a/src/components/views/elements/ToggleSwitch.tsx
+++ b/src/components/views/elements/ToggleSwitch.tsx
@@ -41,7 +41,7 @@ interface IProps {
}
// Controlled Toggle Switch element, written with Accessibility in mind
-export default ({ checked, disabled = false, title, tooltip, onChange, ...props }: IProps): JSX.Element => {
+export default ({ checked, disabled = false, onChange, ...props }: IProps): JSX.Element => {
const _onClick = (): void => {
if (disabled) return;
onChange(!checked);
@@ -61,8 +61,6 @@ export default ({ checked, disabled = false, title, tooltip, onChange, ...props
role="switch"
aria-checked={checked}
aria-disabled={disabled}
- title={title}
- tooltip={tooltip}
>
diff --git a/src/components/views/emojipicker/Category.tsx b/src/components/views/emojipicker/Category.tsx
index f4ffce911b5e..cf662feea39b 100644
--- a/src/components/views/emojipicker/Category.tsx
+++ b/src/components/views/emojipicker/Category.tsx
@@ -21,6 +21,7 @@ import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPic
import LazyRenderList from "../elements/LazyRenderList";
import { DATA_BY_CATEGORY, IEmoji } from "../../../emoji";
import Emoji from "./Emoji";
+import { ButtonEvent } from "../elements/AccessibleButton";
const OVERFLOW_ROWS = 3;
@@ -42,18 +43,31 @@ interface IProps {
heightBefore: number;
viewportHeight: number;
scrollTop: number;
- onClick(emoji: IEmoji): void;
+ onClick(ev: ButtonEvent, emoji: IEmoji): void;
onMouseEnter(emoji: IEmoji): void;
onMouseLeave(emoji: IEmoji): void;
isEmojiDisabled?: (unicode: string) => boolean;
}
+function hexEncode(str: string): string {
+ let hex: string;
+ let i: number;
+
+ let result = "";
+ for (i = 0; i < str.length; i++) {
+ hex = str.charCodeAt(i).toString(16);
+ result += ("000" + hex).slice(-4);
+ }
+
+ return result;
+}
+
class Category extends React.PureComponent {
private renderEmojiRow = (rowIndex: number): JSX.Element => {
const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props;
const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8);
return (
-
+
{emojisForRow.map((emoji) => (
{
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
disabled={this.props.isEmojiDisabled?.(emoji.unicode)}
+ id={`mx_EmojiPicker_item_${this.props.id}_${hexEncode(emoji.unicode)}`}
+ role="gridcell"
/>
))}
@@ -101,7 +117,6 @@ class Category extends React.PureComponent
{
>
{name}
{
overflowItems={OVERFLOW_ROWS}
overflowMargin={0}
renderItem={this.renderEmojiRow}
+ role="grid"
/>
);
diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx
index 022c29a94a6e..627988730344 100644
--- a/src/components/views/emojipicker/Emoji.tsx
+++ b/src/components/views/emojipicker/Emoji.tsx
@@ -17,36 +17,40 @@ limitations under the License.
import React from "react";
-import { MenuItem } from "../../structures/ContextMenu";
import { IEmoji } from "../../../emoji";
+import { ButtonEvent } from "../elements/AccessibleButton";
+import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
interface IProps {
emoji: IEmoji;
selectedEmojis?: Set;
- onClick(emoji: IEmoji): void;
+ onClick(ev: ButtonEvent, emoji: IEmoji): void;
onMouseEnter(emoji: IEmoji): void;
onMouseLeave(emoji: IEmoji): void;
disabled?: boolean;
+ id?: string;
+ role?: string;
}
class Emoji extends React.PureComponent {
public render(): React.ReactNode {
const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
- const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);
+ const isSelected = selectedEmojis?.has(emoji.unicode);
return (
- onClick(emoji)}
+ onClick(ev, emoji)}
onMouseEnter={() => onMouseEnter(emoji)}
onMouseLeave={() => onMouseLeave(emoji)}
className="mx_EmojiPicker_item_wrapper"
- label={emoji.unicode}
disabled={this.props.disabled}
+ role={this.props.role}
+ focusOnMouseOver
>
{emoji.unicode}
-
+
);
}
}
diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx
index b4a868f474d5..7a62c4dd079c 100644
--- a/src/components/views/emojipicker/EmojiPicker.tsx
+++ b/src/components/views/emojipicker/EmojiPicker.tsx
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { Dispatch } from "react";
import { _t } from "../../../languageHandler";
import * as recent from "../../../emojipicker/recent";
@@ -25,8 +25,18 @@ import Header from "./Header";
import Search from "./Search";
import Preview from "./Preview";
import QuickReactions from "./QuickReactions";
-import Category, { ICategory, CategoryKey } from "./Category";
+import Category, { CategoryKey, ICategory } from "./Category";
import { filterBoolean } from "../../../utils/arrays";
+import {
+ IAction as RovingAction,
+ IState as RovingState,
+ RovingTabIndexProvider,
+ Type,
+} from "../../../accessibility/RovingTabIndex";
+import { Key } from "../../../Keyboard";
+import { clamp } from "../../../utils/numbers";
+import { ButtonEvent } from "../elements/AccessibleButton";
+import { Ref } from "../../../accessibility/roving/types";
export const CATEGORY_HEADER_HEIGHT = 20;
export const EMOJI_HEIGHT = 35;
@@ -37,6 +47,7 @@ const ZERO_WIDTH_JOINER = "\u200D";
interface IProps {
selectedEmojis?: Set;
onChoose(unicode: string): boolean;
+ onFinished(): void;
isEmojiDisabled?: (unicode: string) => boolean;
}
@@ -150,6 +161,68 @@ class EmojiPicker extends React.Component {
this.updateVisibility();
};
+ private keyboardNavigation(ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch): void {
+ const node = state.activeRef.current;
+ const parent = node.parentElement;
+ if (!parent) return;
+ const rowIndex = Array.from(parent.children).indexOf(node);
+ const refIndex = state.refs.indexOf(state.activeRef);
+
+ let focusRef: Ref | undefined;
+ let newParent: HTMLElement | undefined;
+ switch (ev.key) {
+ case Key.ARROW_LEFT:
+ focusRef = state.refs[refIndex - 1];
+ newParent = focusRef?.current?.parentElement;
+ break;
+
+ case Key.ARROW_RIGHT:
+ focusRef = state.refs[refIndex + 1];
+ newParent = focusRef?.current?.parentElement;
+ break;
+
+ case Key.ARROW_UP:
+ case Key.ARROW_DOWN: {
+ // For up/down we find the prev/next parent by inspecting the refs either side of our row
+ const ref =
+ ev.key === Key.ARROW_UP
+ ? state.refs[refIndex - rowIndex - 1]
+ : state.refs[refIndex - rowIndex + EMOJIS_PER_ROW];
+ newParent = ref?.current?.parentElement;
+ const newTarget = newParent?.children[clamp(rowIndex, 0, newParent.children.length - 1)];
+ focusRef = state.refs.find((r) => r.current === newTarget);
+ break;
+ }
+ }
+
+ if (focusRef) {
+ dispatch({
+ type: Type.SetFocus,
+ payload: { ref: focusRef },
+ });
+
+ if (parent !== newParent) {
+ focusRef.current?.scrollIntoView({
+ behavior: "auto",
+ block: "center",
+ inline: "center",
+ });
+ }
+ }
+
+ ev.preventDefault();
+ ev.stopPropagation();
+ }
+
+ private onKeyDown = (ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch): void => {
+ if (
+ state.activeRef?.current &&
+ [Key.ARROW_DOWN, Key.ARROW_RIGHT, Key.ARROW_LEFT, Key.ARROW_UP].includes(ev.key)
+ ) {
+ this.keyboardNavigation(ev, state, dispatch);
+ }
+ };
+
private updateVisibility = (): void => {
const body = this.scrollRef.current?.containerRef.current;
if (!body) return;
@@ -239,11 +312,11 @@ class EmojiPicker extends React.Component {
};
private onEnterFilter = (): void => {
- const btn =
- this.scrollRef.current?.containerRef.current?.querySelector(".mx_EmojiPicker_item");
- if (btn) {
- btn.click();
- }
+ const btn = this.scrollRef.current?.containerRef.current?.querySelector(
+ '.mx_EmojiPicker_item_wrapper[tabindex="0"]',
+ );
+ btn?.click();
+ this.props.onFinished();
};
private onHoverEmoji = (emoji: IEmoji): void => {
@@ -258,10 +331,13 @@ class EmojiPicker extends React.Component {
});
};
- private onClickEmoji = (emoji: IEmoji): void => {
+ private onClickEmoji = (ev: ButtonEvent, emoji: IEmoji): void => {
if (this.props.onChoose(emoji.unicode) !== false) {
recent.add(emoji.unicode);
}
+ if ((ev as React.KeyboardEvent).key === Key.ENTER) {
+ this.props.onFinished();
+ }
};
private static categoryHeightForEmojiCount(count: number): number {
@@ -272,41 +348,60 @@ class EmojiPicker extends React.Component {
}
public render(): React.ReactNode {
- let heightBefore = 0;
return (
-
-
-
-
- {this.categories.map((category) => {
- const emojis = this.memoizedDataByCategory[category.id];
- const categoryElement = (
-
+ {({ onKeyDownHandler }) => {
+ let heightBefore = 0;
+ return (
+
+
+
- );
- const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length);
- heightBefore += height;
- return categoryElement;
- })}
-
- {this.state.previewEmoji ? (
-
- ) : (
-
- )}
-
+
+ {this.categories.map((category) => {
+ const emojis = this.memoizedDataByCategory[category.id];
+ const categoryElement = (
+
+ );
+ const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length);
+ heightBefore += height;
+ return categoryElement;
+ })}
+
+ {this.state.previewEmoji ? (
+
+ ) : (
+
+ )}
+
+ );
+ }}
+
);
}
}
diff --git a/src/components/views/emojipicker/Header.tsx b/src/components/views/emojipicker/Header.tsx
index 9a7005d63249..c3643f6e2a96 100644
--- a/src/components/views/emojipicker/Header.tsx
+++ b/src/components/views/emojipicker/Header.tsx
@@ -17,6 +17,7 @@ limitations under the License.
import React from "react";
import classNames from "classnames";
+import { findLastIndex } from "lodash";
import { _t } from "../../../languageHandler";
import { CategoryKey, ICategory } from "./Category";
@@ -40,7 +41,14 @@ class Header extends React.PureComponent {
}
private changeCategoryRelative(delta: number): void {
- const current = this.props.categories.findIndex((c) => c.visible);
+ let current: number;
+ // As multiple categories may be visible at once, we want to find the one closest to the relative direction
+ if (delta < 0) {
+ current = this.props.categories.findIndex((c) => c.visible);
+ } else {
+ // XXX: Switch to Array::findLastIndex once we enable ES2023
+ current = findLastIndex(this.props.categories, (c) => c.visible);
+ }
this.changeCategoryAbsolute(current + delta, delta);
}
diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx
index 6b149069481c..a58c6b875fd3 100644
--- a/src/components/views/emojipicker/QuickReactions.tsx
+++ b/src/components/views/emojipicker/QuickReactions.tsx
@@ -20,6 +20,8 @@ import React from "react";
import { _t } from "../../../languageHandler";
import { getEmojiFromUnicode, IEmoji } from "../../../emoji";
import Emoji from "./Emoji";
+import { ButtonEvent } from "../elements/AccessibleButton";
+import Toolbar from "../../../accessibility/Toolbar";
// We use the variation-selector Heart in Quick Reactions for some reason
const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map((emoji) => {
@@ -32,7 +34,7 @@ const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀
interface IProps {
selectedEmojis?: Set;
- onClick(emoji: IEmoji): void;
+ onClick(ev: ButtonEvent, emoji: IEmoji): void;
}
interface IState {
@@ -70,7 +72,7 @@ class QuickReactions extends React.Component {
)}
-
+
{QUICK_REACTIONS.map((emoji) => (
{
selectedEmojis={this.props.selectedEmojis}
/>
))}
-
+
);
}
diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx
index 6b13c7682315..97222740f888 100644
--- a/src/components/views/emojipicker/ReactionPicker.tsx
+++ b/src/components/views/emojipicker/ReactionPicker.tsx
@@ -135,6 +135,7 @@ class ReactionPicker extends React.Component {
);
diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx
index edd6b2c4fca8..a34a14cbafd6 100644
--- a/src/components/views/emojipicker/Search.tsx
+++ b/src/components/views/emojipicker/Search.tsx
@@ -20,14 +20,19 @@ import React from "react";
import { _t } from "../../../languageHandler";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
+import { RovingTabIndexContext } from "../../../accessibility/RovingTabIndex";
interface IProps {
query: string;
onChange(value: string): void;
onEnter(): void;
+ onKeyDown(event: React.KeyboardEvent): void;
}
class Search extends React.PureComponent {
+ public static contextType = RovingTabIndexContext;
+ public context!: React.ContextType;
+
private inputRef = React.createRef();
public componentDidMount(): void {
@@ -43,11 +48,14 @@ class Search extends React.PureComponent {
ev.stopPropagation();
ev.preventDefault();
break;
+
+ default:
+ this.props.onKeyDown(ev);
}
};
public render(): React.ReactNode {
- let rightButton;
+ let rightButton: JSX.Element;
if (this.props.query) {
rightButton = (
{
onChange={(ev) => this.props.onChange(ev.target.value)}
onKeyDown={this.onKeyDown}
ref={this.inputRef}
+ aria-activedescendant={this.context.state.activeRef?.current?.id}
+ aria-controls="mx_EmojiPicker_body"
+ aria-haspopup="grid"
+ aria-autocomplete="list"
/>
{rightButton}
diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx
index 615792057dd5..80e538c04879 100644
--- a/src/components/views/right_panel/BaseCard.tsx
+++ b/src/components/views/right_panel/BaseCard.tsx
@@ -47,7 +47,7 @@ interface IGroupProps {
export const Group: React.FC = ({ className, title, children }) => {
return (
-
{title}
+ {title}
{children}
);
diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx
index a32d8da047d3..860a58df024f 100644
--- a/src/components/views/right_panel/RoomSummaryCard.tsx
+++ b/src/components/views/right_panel/RoomSummaryCard.tsx
@@ -318,7 +318,7 @@ const RoomSummaryCard: React.FC = ({ room, permalinkCreator, onClose })
/>
- {(name) => {name} }
+ {(name) => {name} }
{alias}
diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index d7d25356101d..fa45e56d1996 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -798,7 +798,7 @@ export default class BasicMessageEditor extends React.Component
};
const { completionIndex } = this.state;
- const hasAutocomplete = Boolean(this.state.autoComplete);
+ const hasAutocomplete = !!this.state.autoComplete;
let activeDescendant: string | undefined;
if (hasAutocomplete && completionIndex! >= 0) {
activeDescendant = generateCompletionDomId(completionIndex!);
@@ -828,7 +828,7 @@ export default class BasicMessageEditor extends React.Component
aria-multiline="true"
aria-autocomplete="list"
aria-haspopup="listbox"
- aria-expanded={hasAutocomplete ? true : undefined}
+ aria-expanded={hasAutocomplete ? !this.autocompleteRef.current?.state.hide : undefined}
aria-owns={hasAutocomplete ? "mx_Autocomplete" : undefined}
aria-activedescendant={activeDescendant}
dir="auto"
diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx
index db7accb62c5d..b35aa2aef556 100644
--- a/src/components/views/rooms/EmojiButton.tsx
+++ b/src/components/views/rooms/EmojiButton.tsx
@@ -36,17 +36,14 @@ export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonP
let contextMenu: React.ReactElement | null = null;
if (menuDisplayed && button.current) {
const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect());
+ const onFinished = (): void => {
+ closeMenu();
+ overflowMenuCloser?.();
+ };
contextMenu = (
- {
- closeMenu();
- overflowMenuCloser?.();
- }}
- managed={false}
- >
-
+
+
);
}
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 30f262cf5916..a6393b7c8255 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -218,6 +218,10 @@ export interface EventTileProps {
// displayed to the current user either because they're
// the author or they are a moderator
isSeeingThroughMessageHiddenForModeration?: boolean;
+
+ // The following properties are used by EventTilePreview to disable tab indexes within the event tile
+ hideTimestamp?: boolean;
+ inhibitInteraction?: boolean;
}
interface IState {
@@ -1016,7 +1020,7 @@ export class UnwrappedEventTile extends React.Component
}
if (this.props.mxEvent.sender && avatarSize) {
- let member;
+ let member: RoomMember | null = null;
// set member to receiver (target) if it is a 3PID invite
// so that the correct avatar is shown as the text is
// `$target accepted the invitation for $email`
@@ -1026,9 +1030,11 @@ export class UnwrappedEventTile extends React.Component
member = this.props.mxEvent.sender;
}
// In the ThreadsList view we use the entire EventTile as a click target to open the thread instead
- const viewUserOnClick = ![TimelineRenderingType.ThreadsList, TimelineRenderingType.Notification].includes(
- this.context.timelineRenderingType,
- );
+ const viewUserOnClick =
+ !this.props.inhibitInteraction &&
+ ![TimelineRenderingType.ThreadsList, TimelineRenderingType.Notification].includes(
+ this.context.timelineRenderingType,
+ );
avatar = (
const showTimestamp =
this.props.mxEvent.getTs() &&
+ !this.props.hideTimestamp &&
(this.props.alwaysShowTimestamps ||
this.props.last ||
this.state.hover ||
@@ -1111,7 +1118,7 @@ export class UnwrappedEventTile extends React.Component
);
}
- const linkedTimestamp = (
+ const linkedTimestamp = !this.props.hideTimestamp ? (
>
{timestamp}
- );
+ ) : null;
const useIRCLayout = this.props.layout === Layout.IRC;
const groupTimestamp = !useIRCLayout ? linkedTimestamp : null;
diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx
index 17990d3aa66e..81491d35c260 100644
--- a/src/components/views/rooms/MessageComposerButtons.tsx
+++ b/src/components/views/rooms/MessageComposerButtons.tsx
@@ -65,11 +65,11 @@ export const OverflowMenuContext = createContext(null
const MessageComposerButtons: React.FC = (props: IProps) => {
const matrixClient = useContext(MatrixClientContext);
- const { room, roomId, narrow } = useContext(RoomContext);
+ const { room, narrow } = useContext(RoomContext);
const isWysiwygLabEnabled = useSettingValue("feature_wysiwyg_composer");
- if (props.haveRecording) {
+ if (!matrixClient || !room || props.haveRecording) {
return null;
}
@@ -93,7 +93,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => {
voiceRecordingButton(props, narrow),
startVoiceBroadcastButton(props),
props.showPollsButton ? pollButton(room, props.relation) : null,
- showLocationButton(props, room, roomId, matrixClient),
+ showLocationButton(props, room, matrixClient),
];
} else {
mainButtons = [
@@ -113,7 +113,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => {
voiceRecordingButton(props, narrow),
startVoiceBroadcastButton(props),
props.showPollsButton ? pollButton(room, props.relation) : null,
- showLocationButton(props, room, roomId, matrixClient),
+ showLocationButton(props, room, matrixClient),
];
}
@@ -127,7 +127,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => {
});
return (
-
+
{mainButtons}
{moreButtons.length > 0 && (
{
}
}
-function showLocationButton(
- props: IProps,
- room: Room,
- roomId: string,
- matrixClient: MatrixClient,
-): ReactElement | null {
- const sender = room.getMember(matrixClient.getUserId()!);
+function showLocationButton(props: IProps, room: Room, matrixClient: MatrixClient): ReactElement | null {
+ const sender = room.getMember(matrixClient.getSafeUserId());
return props.showLocationButton && sender ? (
{
const result = await MatrixClientPeg.get().lookupThreePid(
"email",
this.props.invitedEmail,
- identityAccessToken,
+ identityAccessToken!,
);
this.setState({ invitedEmailMxid: result.mxid });
} catch (err) {
@@ -243,8 +243,8 @@ export default class RoomPreviewBar extends React.Component {
if (!inviteEvent) {
return null;
}
- const inviterUserId = inviteEvent.events.member.getSender();
- return room.currentState.getMember(inviterUserId);
+ const inviterUserId = inviteEvent.events.member?.getSender();
+ return inviterUserId ? room.currentState.getMember(inviterUserId) : null;
}
private isDMInvite(): boolean {
@@ -252,8 +252,8 @@ export default class RoomPreviewBar extends React.Component {
if (!myMember) {
return false;
}
- const memberContent = myMember.events.member.getContent();
- return memberContent.membership === "invite" && memberContent.is_direct;
+ const memberContent = myMember.events.member?.getContent();
+ return memberContent?.membership === "invite" && memberContent.is_direct;
}
private makeScreenAfterLogin(): { screen: string; params: Record } {
diff --git a/src/components/views/rooms/SearchBar.tsx b/src/components/views/rooms/SearchBar.tsx
index c75b340c29a0..a4dbfe60ce64 100644
--- a/src/components/views/rooms/SearchBar.tsx
+++ b/src/components/views/rooms/SearchBar.tsx
@@ -121,6 +121,9 @@ export default class SearchBar extends React.Component {
type="text"
autoFocus={true}
placeholder={_t("Search…")}
+ aria-label={
+ this.state.scope === SearchScope.Room ? _t("Search this room") : _t("Search all rooms")
+ }
onKeyDown={this.onSearchChange}
/>
{
const eventId = resultEvent.getId();
const ts1 = resultEvent.getTs();
- const ret = [ ];
+ const ret = [ ];
const layout = SettingsStore.getValue("layout");
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx
index 24fbf5ccad12..700776d54cda 100644
--- a/src/components/views/rooms/SendMessageComposer.tsx
+++ b/src/components/views/rooms/SendMessageComposer.tsx
@@ -372,7 +372,10 @@ export class SendMessageComposer extends React.Component= 0; i--) {
@@ -443,8 +447,8 @@ export class SendMessageComposer extends React.Component(posthogEvent);
@@ -480,7 +484,7 @@ export class SendMessageComposer extends React.Component {
}
public render(): React.ReactNode {
- const usersTyping = this.state.usersTyping;
+ const usersTyping = [...this.state.usersTyping];
// append the users that have been reported not typing anymore
// but have a timeout timer running so they can disappear
// when a message comes in
diff --git a/src/components/views/settings/CrossSigningPanel.tsx b/src/components/views/settings/CrossSigningPanel.tsx
index e3f62d4ba263..d3926d954fa9 100644
--- a/src/components/views/settings/CrossSigningPanel.tsx
+++ b/src/components/views/settings/CrossSigningPanel.tsx
@@ -243,36 +243,34 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
{_t("Advanced")}
-
-
- {_t("Cross-signing public keys:")}
- {crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}
-
-
- {_t("Cross-signing private keys:")}
-
- {crossSigningPrivateKeysInStorage
- ? _t("in secret storage")
- : _t("not found in storage")}
-
-
-
- {_t("Master private key:")}
- {masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
-
-
- {_t("Self signing private key:")}
- {selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
-
-
- {_t("User signing private key:")}
- {userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
-
-
- {_t("Homeserver feature support:")}
- {homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}
-
-
+
+ {_t("Cross-signing public keys:")}
+ {crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}
+
+
+ {_t("Cross-signing private keys:")}
+
+ {crossSigningPrivateKeysInStorage
+ ? _t("in secret storage")
+ : _t("not found in storage")}
+
+
+
+ {_t("Master private key:")}
+ {masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
+
+
+ {_t("Self signing private key:")}
+ {selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
+
+
+ {_t("User signing private key:")}
+ {userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}
+
+
+ {_t("Homeserver feature support:")}
+ {homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}
+
{errorSection}
diff --git a/src/components/views/settings/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx
index 34b52e405ee9..79ddad2544ee 100644
--- a/src/components/views/settings/CryptographyPanel.tsx
+++ b/src/components/views/settings/CryptographyPanel.tsx
@@ -75,22 +75,20 @@ export default class CryptographyPanel extends React.Component {
{_t("Cryptography")}
-
-
- {_t("Session ID:")}
-
- {deviceId}
-
-
-
- {_t("Session key:")}
-
-
- {identityKey}
-
-
-
-
+
+ {_t("Session ID:")}
+
+ {deviceId}
+
+
+
+ {_t("Session key:")}
+
+
+ {identityKey}
+
+
+
{importExportButtons}
{noSendUnverifiedSetting}
diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx
index 747378684c08..2b19a8af583e 100644
--- a/src/components/views/settings/SecureBackupPanel.tsx
+++ b/src/components/views/settings/SecureBackupPanel.tsx
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { ReactNode } from "react";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
@@ -231,9 +231,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
sessionsRemaining,
} = this.state;
- let statusDescription;
- let extraDetailsTableRows;
- let extraDetails;
+ let statusDescription: JSX.Element;
+ let extraDetailsTableRows: JSX.Element | undefined;
+ let extraDetails: JSX.Element | undefined;
const actions: JSX.Element[] = [];
if (error) {
statusDescription =
{_t("Unable to load key backup status")}
;
@@ -267,7 +267,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
restoreButtonCaption = _t("Connect this session to Key Backup");
}
- let uploadStatus;
+ let uploadStatus: ReactNode;
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
// No upload status to show when backup disabled.
uploadStatus = "";
@@ -391,11 +391,11 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
extraDetailsTableRows = (
<>
- {_t("Backup version:")}
+ {_t("Backup version:")}
{backupInfo.version}
- {_t("Algorithm:")}
+ {_t("Algorithm:")}
{backupInfo.algorithm}
>
@@ -460,7 +460,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
}
}
- let actionRow;
+ let actionRow: JSX.Element | undefined;
if (actions.length) {
actionRow =
{actions}
;
}
@@ -478,28 +478,26 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
{_t("Advanced")}
-
-
- {_t("Backup key stored:")}
- {backupKeyStored === true ? _t("in secret storage") : _t("not stored")}
-
-
- {_t("Backup key cached:")}
-
- {backupKeyCached ? _t("cached locally") : _t("not found locally")}
- {backupKeyWellFormedText}
-
-
-
- {_t("Secret storage public key:")}
- {secretStorageKeyInAccount ? _t("in account data") : _t("not found")}
-
-
- {_t("Secret storage:")}
- {secretStorageReady ? _t("ready") : _t("not ready")}
-
- {extraDetailsTableRows}
-
+
+ {_t("Backup key stored:")}
+ {backupKeyStored === true ? _t("in secret storage") : _t("not stored")}
+
+
+ {_t("Backup key cached:")}
+
+ {backupKeyCached ? _t("cached locally") : _t("not found locally")}
+ {backupKeyWellFormedText}
+
+
+
+ {_t("Secret storage public key:")}
+ {secretStorageKeyInAccount ? _t("in account data") : _t("not found")}
+
+
+ {_t("Secret storage:")}
+ {secretStorageReady ? _t("ready") : _t("not ready")}
+
+ {extraDetailsTableRows}
{extraDetails}
diff --git a/src/components/views/settings/account/EmailAddresses.tsx b/src/components/views/settings/account/EmailAddresses.tsx
index 3d3cb8fb1870..c90e1c2768cf 100644
--- a/src/components/views/settings/account/EmailAddresses.tsx
+++ b/src/components/views/settings/account/EmailAddresses.tsx
@@ -280,7 +280,7 @@ export default class EmailAddresses extends React.Component
{
{
{
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
- Modal.createDialog(RoomUpgradeDialog, { room });
+ if (room) Modal.createDialog(RoomUpgradeDialog, { room });
};
private onOldRoomClicked = (e: ButtonEvent): void => {
diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
index 78bb1fe782a5..4f5124d7dc08 100644
--- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
@@ -443,6 +443,7 @@ export default class SecurityRoomSettingsTab extends React.Component
{this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced")}
diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
index 7a8157e38c75..f4b05b3631d5 100644
--- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
@@ -88,7 +88,11 @@ export default class AppearanceUserSettingsTab extends React.Component this.setState({ showAdvanced: !this.state.showAdvanced })}>
+ this.setState({ showAdvanced: !this.state.showAdvanced })}
+ aria-expanded={this.state.showAdvanced}
+ >
{this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")}
);
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx
index dc3bf9f408b9..0827065fac33 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx
@@ -66,13 +66,20 @@ interface IState {
haveIdServer: boolean;
serverSupportsSeparateAddAndBind?: boolean;
idServerHasUnsignedTerms: boolean;
- requiredPolicyInfo: {
- // This object is passed along to a component for handling
- hasTerms: boolean;
- policiesAndServices: ServicePolicyPair[] | null; // From the startTermsFlow callback
- agreedUrls: string[] | null; // From the startTermsFlow callback
- resolve: ((values: string[]) => void) | null; // Promise resolve function for startTermsFlow callback
- };
+ requiredPolicyInfo:
+ | {
+ // This object is passed along to a component for handling
+ hasTerms: false;
+ policiesAndServices: null; // From the startTermsFlow callback
+ agreedUrls: null; // From the startTermsFlow callback
+ resolve: null; // Promise resolve function for startTermsFlow callback
+ }
+ | {
+ hasTerms: boolean;
+ policiesAndServices: ServicePolicyPair[];
+ agreedUrls: string[];
+ resolve: (values: string[]) => void;
+ };
emails: IThreepid[];
msisdns: IThreepid[];
loading3pids: boolean; // whether or not the emails and msisdns have been loaded
@@ -191,19 +198,19 @@ export default class GeneralUserSettingsTab extends React.Component {
- if (!this.state.haveIdServer) {
+ // By starting the terms flow we get the logic for checking which terms the user has signed
+ // for free. So we might as well use that for our own purposes.
+ const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl();
+ if (!this.state.haveIdServer || !idServerUrl) {
this.setState({ idServerHasUnsignedTerms: false });
return;
}
- // By starting the terms flow we get the logic for checking which terms the user has signed
- // for free. So we might as well use that for our own purposes.
- const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl();
const authClient = new IdentityAuthClient();
try {
const idAccessToken = await authClient.getAccessToken({ check: false });
await startTermsFlow(
- [new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken)],
+ [new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken!)],
(policiesAndServices, agreedUrls, extraClassNames) => {
return new Promise((resolve, reject) => {
this.setState({
@@ -468,7 +475,7 @@ export default class GeneralUserSettingsTab extends React.Component
+
{_t("Account management")}
{_t("Deactivating your account is a permanent action — be careful!")}
@@ -528,8 +535,10 @@ export default class GeneralUserSettingsTab extends React.Component
- {_t("General")}
+
+
+ {_t("General")}
+
{this.renderProfileSection()}
{this.renderAccountSection()}
{this.renderLanguageSection()}
diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
index b5c1e8086934..f939cba64cc8 100644
--- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
@@ -137,57 +137,97 @@ export default class HelpUserSettingsTab extends React.Component
{_t("Credits")}
- The{" "}
-
- default cover photo
- {" "}
- is ©
-
- Jesús Roncero
- {" "}
- used under the terms of
-
- CC-BY-SA 4.0
-
- .
+ {_t(
+ "The default cover photo is © " +
+ "Jesús Roncero used under the terms of CC-BY-SA 4.0 .",
+ {},
+ {
+ photo: (sub) => (
+
+ {sub}
+
+ ),
+ author: (sub) => (
+
+ {sub}
+
+ ),
+ terms: (sub) => (
+
+ {sub}
+
+ ),
+ },
+ )}
- The{" "}
-
- twemoji-colr
- {" "}
- font is ©
-
- Mozilla Foundation
- {" "}
- used under the terms of
-
- Apache 2.0
-
- .
+ {_t(
+ "The twemoji-colr font is © Mozilla Foundation " +
+ "used under the terms of Apache 2.0 .",
+ {},
+ {
+ colr: (sub) => (
+
+ {sub}
+
+ ),
+ author: (sub) => (
+
+ {sub}
+
+ ),
+ terms: (sub) => (
+
+ {sub}
+
+ ),
+ },
+ )}
- The{" "}
-
- Twemoji
- {" "}
- emoji art is ©
-
- Twitter, Inc and other contributors
- {" "}
- used under the terms of
-
- CC-BY 4.0
-
- .
+ {_t(
+ "The Twemoji emoji art is © " +
+ "Twitter, Inc and other contributors used under the terms of " +
+ "CC-BY 4.0 .",
+ {},
+ {
+ twemoji: (sub) => (
+
+ {sub}
+
+ ),
+ author: (sub) => (
+
+ {sub}
+
+ ),
+ terms: (sub) => (
+
+ {sub}
+
+ ),
+ },
+ )}
diff --git a/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx b/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx
index cf9a41a55409..ef79b98d4837 100644
--- a/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx
@@ -41,10 +41,10 @@ const KeyboardShortcutRow: React.FC = ({ name }) => {
if (!displayName || !value) return null;
return (
-
+
{displayName}
-
+
);
};
@@ -59,12 +59,12 @@ const KeyboardShortcutSection: React.FC = ({ cate
return (
{_t(category.categoryLabel)}
-
+
{" "}
{category.settingNames.map((shortcutName) => {
return ;
})}{" "}
-
+
);
};
diff --git a/src/components/views/spaces/QuickSettingsButton.tsx b/src/components/views/spaces/QuickSettingsButton.tsx
index 1adcd419600c..4f9529466b0a 100644
--- a/src/components/views/spaces/QuickSettingsButton.tsx
+++ b/src/components/views/spaces/QuickSettingsButton.tsx
@@ -134,6 +134,7 @@ const QuickSettingsButton: React.FC<{
title={_t("Quick settings")}
inputRef={handle}
forceHide={!isPanelCollapsed}
+ aria-expanded={!isPanelCollapsed}
>
{!isPanelCollapsed ? _t("Settings") : null}
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 64fc408b7745..ded069778d5b 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -89,8 +89,8 @@ const SpaceCreateMenuType: React.FC<{
}> = ({ title, description, className, onClick }) => {
return (
- {title}
- {description}
+ {title}
+ {description}
);
};
diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx
index 85446ab25175..68bf940831a8 100644
--- a/src/components/views/spaces/SpacePublicShare.tsx
+++ b/src/components/views/spaces/SpacePublicShare.tsx
@@ -52,7 +52,7 @@ const SpacePublicShare: React.FC = ({ space, onFinished }) => {
}
}}
>
- {_t("Share invite link")}
+ {_t("Share invite link")}
{copiedText}
{space.canInvite(MatrixClientPeg.get()?.getUserId()) && shouldShowComponent(UIComponent.InviteUsers) ? (
@@ -63,8 +63,8 @@ const SpacePublicShare: React.FC = ({ space, onFinished }) => {
showRoomInviteDialog(space.roomId);
}}
>
- {_t("Invite people")}
- {_t("Invite with email or username")}
+ {_t("Invite people")}
+ {_t("Invite with email or username")}
) : null}
diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
index 7993a2af642b..368d6c96fc05 100644
--- a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
+++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
@@ -97,6 +97,7 @@ const SpaceSettingsVisibilityTab: React.FC = ({ matrixClient: cli, space
onClick={toggleAdvancedSection}
kind="link"
className="mx_SettingsTab_showAdvanced"
+ aria-expanded={showAdvancedSection}
>
{showAdvancedSection ? _t("Hide advanced") : _t("Show advanced")}
diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx
index d9a36af6ccfe..e079360b4735 100644
--- a/src/components/views/voip/AudioFeed.tsx
+++ b/src/components/views/voip/AudioFeed.tsx
@@ -62,7 +62,7 @@ export default class AudioFeed extends React.Component {
// it fails.
// It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID
// back to the default after the call is over - Dave
- element.setSinkId(audioOutput);
+ element!.setSinkId(audioOutput);
} catch (e) {
logger.error("Couldn't set requested audio output device: using default", e);
logger.warn("Couldn't set requested audio output device: using default", e);
@@ -103,7 +103,7 @@ export default class AudioFeed extends React.Component {
if (!element) return;
element.pause();
- element.src = null;
+ element.removeAttribute("src");
// As per comment in componentDidMount, setting the sink ID back to the
// default once the call is over makes setSinkId work reliably. - Dave
diff --git a/src/components/views/voip/LegacyCallView.tsx b/src/components/views/voip/LegacyCallView.tsx
index 86be87608d59..5978acb316fd 100644
--- a/src/components/views/voip/LegacyCallView.tsx
+++ b/src/components/views/voip/LegacyCallView.tsx
@@ -339,16 +339,17 @@ export default class LegacyCallView extends React.Component {
private onCallResumeClick = (): void => {
const userFacingRoomId = LegacyCallHandler.instance.roomIdForCall(this.props.call);
- LegacyCallHandler.instance.setActiveCallRoomId(userFacingRoomId);
+ if (userFacingRoomId) LegacyCallHandler.instance.setActiveCallRoomId(userFacingRoomId);
};
private onTransferClick = (): void => {
const transfereeCall = LegacyCallHandler.instance.getTransfereeForCallId(this.props.call.callId);
- this.props.call.transferToCall(transfereeCall);
+ if (transfereeCall) this.props.call.transferToCall(transfereeCall);
};
private onHangupClick = (): void => {
- LegacyCallHandler.instance.hangupOrReject(LegacyCallHandler.instance.roomIdForCall(this.props.call));
+ const roomId = LegacyCallHandler.instance.roomIdForCall(this.props.call);
+ if (roomId) LegacyCallHandler.instance.hangupOrReject(roomId);
};
private onToggleSidebar = (): void => {
@@ -451,13 +452,12 @@ export default class LegacyCallView extends React.Component {
let holdTransferContent: React.ReactNode;
if (transfereeCall) {
- const transferTargetRoom = MatrixClientPeg.get().getRoom(
- LegacyCallHandler.instance.roomIdForCall(call),
- );
+ const cli = MatrixClientPeg.get();
+ const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
+ const transferTargetRoom = callRoomId ? cli.getRoom(callRoomId) : null;
const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
- const transfereeRoom = MatrixClientPeg.get().getRoom(
- LegacyCallHandler.instance.roomIdForCall(transfereeCall),
- );
+ const transfereeCallRoomId = LegacyCallHandler.instance.roomIdForCall(transfereeCall);
+ const transfereeRoom = transfereeCallRoomId ? cli.getRoom(transfereeCallRoomId) : null;
const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
holdTransferContent = (
@@ -579,6 +579,8 @@ export default class LegacyCallView extends React.Component {
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
const secondaryCallRoomId = LegacyCallHandler.instance.roomIdForCall(secondaryCall);
const callRoom = callRoomId ? client.getRoom(callRoomId) : null;
+ if (!callRoom) return null;
+
const secCallRoom = secondaryCallRoomId ? client.getRoom(secondaryCallRoomId) : null;
const callViewClasses = classNames({
diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx
index 503c53ec66e9..c02154936f45 100644
--- a/src/components/views/voip/VideoFeed.tsx
+++ b/src/components/views/voip/VideoFeed.tsx
@@ -150,7 +150,7 @@ export default class VideoFeed extends React.PureComponent {
if (!element) return;
element.pause();
- element.src = null;
+ element.removeAttribute("src");
// As per comment in componentDidMount, setting the sink ID back to the
// default once the call is over makes setSinkId work reliably. - Dave
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index bbd71d70f33c..caca153edb0d 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1568,6 +1568,9 @@
"Olm version:": "Olm version:",
"Legal": "Legal",
"Credits": "Credits",
+ "The default cover photo is © Jesús Roncero used under the terms of CC-BY-SA 4.0 .": "The default cover photo is © Jesús Roncero used under the terms of CC-BY-SA 4.0 .",
+ "The twemoji-colr font is © Mozilla Foundation used under the terms of Apache 2.0 .": "The twemoji-colr font is © Mozilla Foundation used under the terms of Apache 2.0 .",
+ "The Twemoji emoji art is © Twitter, Inc and other contributors used under the terms of CC-BY 4.0 .": "The Twemoji emoji art is © Twitter, Inc and other contributors used under the terms of CC-BY 4.0 .",
"For help with using %(brand)s, click here .": "For help with using %(brand)s, click here .",
"For help with using %(brand)s, click here or start a chat with our bot using the button below.": "For help with using %(brand)s, click here or start a chat with our bot using the button below.",
"Chat with %(brand)s Bot": "Chat with %(brand)s Bot",
@@ -2138,6 +2141,8 @@
"This Room": "This Room",
"All Rooms": "All Rooms",
"Search…": "Search…",
+ "Search this room": "Search this room",
+ "Search all rooms": "Search all rooms",
"Failed to connect to integration manager": "Failed to connect to integration manager",
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
"Add some now": "Add some now",
diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts
index bca7547b6ced..84c0a3ec2042 100644
--- a/src/indexing/EventIndex.ts
+++ b/src/indexing/EventIndex.ts
@@ -235,8 +235,11 @@ export default class EventIndex extends EventEmitter {
const indexManager = PlatformPeg.get()?.getEventIndexingManager();
if (!indexManager) return;
+ const associatedId = ev.getAssociatedId();
+ if (!associatedId) return;
+
try {
- await indexManager.deleteEvent(ev.getAssociatedId());
+ await indexManager.deleteEvent(associatedId);
} catch (e) {
logger.log("EventIndex: Error deleting event from index", e);
}
@@ -519,10 +522,10 @@ export default class EventIndex extends EventEmitter {
const profiles: Record = {};
stateEvents.forEach((ev) => {
- if (ev.event.content && ev.event.content.membership === "join") {
- profiles[ev.event.sender] = {
- displayname: ev.event.content.displayname,
- avatar_url: ev.event.content.avatar_url,
+ if (ev.getContent().membership === "join") {
+ profiles[ev.getSender()!] = {
+ displayname: ev.getContent().displayname,
+ avatar_url: ev.getContent().avatar_url,
};
}
});
@@ -733,7 +736,7 @@ export default class EventIndex extends EventEmitter {
const matrixEvents = events.map((e) => {
const matrixEvent = eventMapper(e.event);
- const member = new RoomMember(room.roomId, matrixEvent.getSender());
+ const member = new RoomMember(room.roomId, matrixEvent.getSender()!);
// We can't really reconstruct the whole room state from our
// EventIndex to calculate the correct display name. Use the
diff --git a/src/models/Call.ts b/src/models/Call.ts
index 6f96e9d887cf..d3b99db28437 100644
--- a/src/models/Call.ts
+++ b/src/models/Call.ts
@@ -213,7 +213,7 @@ export abstract class Call extends TypedEventEmitter {
await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) });
}
} else if (payload.action === Action.ViewRoom) {
- if (payload.auto_join && !this.matrixClient.getRoom(payload.room_id)) {
+ if (payload.auto_join && payload.room_id && !this.matrixClient.getRoom(payload.room_id)) {
// Queue the room instead of pushing it immediately. We're probably just
// waiting for a room join to complete.
this.waitingRooms.push({ roomId: payload.room_id, addedTs: Date.now() });
diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts
index db9e57f46c21..2509dc92a320 100644
--- a/src/stores/OwnBeaconStore.ts
+++ b/src/stores/OwnBeaconStore.ts
@@ -426,6 +426,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient {
roomId: Room["roomId"],
beaconInfoContent: MBeaconInfoEventContent,
): Promise => {
+ if (!this.matrixClient) return;
// explicitly stop any live beacons this user has
// to ensure they remain stopped
// if the new replacing beacon is redacted
@@ -435,7 +436,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient {
// eslint-disable-next-line camelcase
const { event_id } = await doMaybeLocalRoomAction(
roomId,
- (actualRoomId: string) => this.matrixClient.unstable_createLiveBeacon(actualRoomId, beaconInfoContent),
+ (actualRoomId: string) => this.matrixClient!.unstable_createLiveBeacon(actualRoomId, beaconInfoContent),
this.matrixClient,
);
@@ -552,7 +553,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient {
const updateContent = makeBeaconInfoContent(timeout, live, description, assetType, timestamp);
try {
- await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, updateContent);
+ await this.matrixClient!.unstable_setLiveBeacon(beacon.roomId, updateContent);
// cleanup any errors
const hadError = this.beaconUpdateErrors.has(beacon.identifier);
if (hadError) {
@@ -576,7 +577,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient {
this.lastPublishedPositionTimestamp = Date.now();
await Promise.all(
this.healthyLiveBeaconIds.map((beaconId) =>
- this.sendLocationToBeacon(this.beacons.get(beaconId), position),
+ this.beacons.has(beaconId) ? this.sendLocationToBeacon(this.beacons.get(beaconId)!, position) : null,
),
);
};
@@ -589,7 +590,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient {
private sendLocationToBeacon = async (beacon: Beacon, { geoUri, timestamp }: TimedGeoUri): Promise => {
const content = makeBeaconContent(geoUri, timestamp, beacon.beaconInfoId);
try {
- await this.matrixClient.sendEvent(beacon.roomId, M_BEACON.name, content);
+ await this.matrixClient!.sendEvent(beacon.roomId, M_BEACON.name, content);
this.incrementBeaconLocationPublishErrorCount(beacon.identifier, false);
} catch (error) {
logger.error(error);
diff --git a/src/stores/local-echo/RoomEchoChamber.ts b/src/stores/local-echo/RoomEchoChamber.ts
index 15a3affdda42..2eac117451b3 100644
--- a/src/stores/local-echo/RoomEchoChamber.ts
+++ b/src/stores/local-echo/RoomEchoChamber.ts
@@ -27,7 +27,7 @@ export enum CachedRoomKey {
NotificationVolume,
}
-export class RoomEchoChamber extends GenericEchoChamber {
+export class RoomEchoChamber extends GenericEchoChamber {
private properties = new Map();
public constructor(context: RoomEchoContext) {
@@ -67,11 +67,12 @@ export class RoomEchoChamber extends GenericEchoChamber {
public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS): Promise => {
try {
- const { rooms } = await this.matrixClient.getRoomHierarchy(space.roomId, limit, 1, true);
+ const { rooms } = await this.matrixClient!.getRoomHierarchy(space.roomId, limit, 1, true);
const viaMap = new EnhancedMap>();
rooms.forEach((room) => {
@@ -979,7 +979,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
private onRoomState = (ev: MatrixEvent): void => {
const room = this.matrixClient?.getRoom(ev.getRoomId());
- if (!room) return;
+ if (!this.matrixClient || !room) return;
switch (ev.getType()) {
case EventType.SpaceChild: {
diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts
index 4c37ffd84c5c..9e24126b15dc 100644
--- a/src/stores/widgets/StopGapWidgetDriver.ts
+++ b/src/stores/widgets/StopGapWidgetDriver.ts
@@ -274,19 +274,22 @@ export class StopGapWidgetDriver extends WidgetDriver {
await Promise.all(
Object.entries(contentMap).flatMap(([userId, userContentMap]) =>
Object.entries(userContentMap).map(async ([deviceId, content]): Promise => {
+ const devices = deviceInfoMap.get(userId);
+ if (!devices) return;
+
if (deviceId === "*") {
// Send the message to all devices we have keys for
await client.encryptAndSendToDevices(
- Array.from(deviceInfoMap.get(userId).values()).map((deviceInfo) => ({
+ Array.from(devices.values()).map((deviceInfo) => ({
userId,
deviceInfo,
})),
content,
);
- } else {
+ } else if (devices.has(deviceId)) {
// Send the message to a specific device
await client.encryptAndSendToDevices(
- [{ userId, deviceInfo: deviceInfoMap.get(userId).get(deviceId) }],
+ [{ userId, deviceInfo: devices.get(deviceId)! }],
content,
);
}
diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts
index f260895c30a2..2bfd555ea30f 100644
--- a/src/stores/widgets/WidgetLayoutStore.ts
+++ b/src/stores/widgets/WidgetLayoutStore.ts
@@ -363,7 +363,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
}
public getContainerWidgets(room: Optional, container: Container): IApp[] {
- return this.byRoom.get(room?.roomId)?.get(container)?.ordered || [];
+ return (room && this.byRoom.get(room.roomId)?.get(container)?.ordered) || [];
}
public isInContainer(room: Room, widget: IApp, container: Container): boolean {
diff --git a/src/stores/widgets/WidgetPermissionStore.ts b/src/stores/widgets/WidgetPermissionStore.ts
index 244a95f06cba..b6ea52c162ce 100644
--- a/src/stores/widgets/WidgetPermissionStore.ts
+++ b/src/stores/widgets/WidgetPermissionStore.ts
@@ -58,7 +58,7 @@ export class WidgetPermissionStore {
return OIDCState.Unknown;
}
- public setOIDCState(widget: Widget, kind: WidgetKind, roomId: string, newState: OIDCState): void {
+ public setOIDCState(widget: Widget, kind: WidgetKind, roomId: string | undefined, newState: OIDCState): void {
const settingsKey = this.packSettingKey(widget, kind, roomId);
let currentValues = SettingsStore.getValue<{
diff --git a/src/utils/SortMembers.ts b/src/utils/SortMembers.ts
index d19b461b1e96..8be4e8a9399d 100644
--- a/src/utils/SortMembers.ts
+++ b/src/utils/SortMembers.ts
@@ -67,7 +67,7 @@ interface IActivityScore {
// We do this by checking every room to see who has sent a message in the last few hours, and giving them
// a score which correlates to the freshness of their message. In theory, this results in suggestions
// which are closer to "continue this conversation" rather than "this person exists".
-export function buildActivityScores(cli: MatrixClient): { [key: string]: IActivityScore | undefined } {
+export function buildActivityScores(cli: MatrixClient): { [userId: string]: IActivityScore } {
const now = new Date().getTime();
const earliestAgeConsidered = now - 60 * 60 * 1000; // 1 hour ago
const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic
@@ -75,6 +75,7 @@ export function buildActivityScores(cli: MatrixClient): { [key: string]: IActivi
.flatMap((room) => takeRight(room.getLiveTimeline().getEvents(), maxMessagesConsidered))
.filter((ev) => ev.getTs() > earliestAgeConsidered);
const senderEvents = groupBy(events, (ev) => ev.getSender());
+ // If the iteratee in mapValues returns undefined that key will be removed from the resultant object
return mapValues(senderEvents, (events) => {
if (!events.length) return;
const lastEvent = maxBy(events, (ev) => ev.getTs())!;
@@ -87,7 +88,7 @@ export function buildActivityScores(cli: MatrixClient): { [key: string]: IActivi
// an approximate maximum for being selected.
score: Math.max(1, inverseTime / (15 * 60 * 1000)), // 15min segments to keep scores sane
};
- });
+ }) as { [key: string]: IActivityScore };
}
interface IMemberScore {
@@ -96,13 +97,14 @@ interface IMemberScore {
numRooms: number;
}
-export function buildMemberScores(cli: MatrixClient): { [key: string]: IMemberScore | undefined } {
+export function buildMemberScores(cli: MatrixClient): { [userId: string]: IMemberScore } {
const maxConsideredMembers = 200;
const consideredRooms = joinedRooms(cli).filter((room) => room.getJoinedMemberCount() < maxConsideredMembers);
const memberPeerEntries = consideredRooms.flatMap((room) =>
room.getJoinedMembers().map((member) => ({ member, roomSize: room.getJoinedMemberCount() })),
);
const userMeta = groupBy(memberPeerEntries, ({ member }) => member.userId);
+ // If the iteratee in mapValues returns undefined that key will be removed from the resultant object
return mapValues(userMeta, (roomMemberships) => {
if (!roomMemberships.length) return;
const maximumPeers = maxConsideredMembers * roomMemberships.length;
@@ -112,5 +114,5 @@ export function buildMemberScores(cli: MatrixClient): { [key: string]: IMemberSc
numRooms: roomMemberships.length,
score: Math.max(0, Math.pow(1 - totalPeers / maximumPeers, 5)),
};
- });
+ }) as { [userId: string]: IMemberScore };
}
diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts
index eda75bb17a44..45ae7566048f 100644
--- a/src/utils/notifications.ts
+++ b/src/utils/notifications.ts
@@ -28,7 +28,7 @@ export const deviceNotificationSettingsKeys = [
"audioNotificationsEnabled",
];
-export function getLocalNotificationAccountDataEventType(deviceId: string): string {
+export function getLocalNotificationAccountDataEventType(deviceId: string | null): string {
return `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
}
diff --git a/test/components/structures/SpaceHierarchy-test.tsx b/test/components/structures/SpaceHierarchy-test.tsx
index 5f248140c89a..b81a84facaa5 100644
--- a/test/components/structures/SpaceHierarchy-test.tsx
+++ b/test/components/structures/SpaceHierarchy-test.tsx
@@ -32,11 +32,9 @@ import DMRoomMap from "../../../src/utils/DMRoomMap";
import SettingsStore from "../../../src/settings/SettingsStore";
// Fake random strings to give a predictable snapshot for checkbox IDs
-jest.mock("matrix-js-sdk/src/randomstring", () => {
- return {
- randomString: () => "abdefghi",
- };
-});
+jest.mock("matrix-js-sdk/src/randomstring", () => ({
+ randomString: () => "abdefghi",
+}));
describe("SpaceHierarchy", () => {
describe("showRoom", () => {
diff --git a/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx b/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx
index 28f1aa47658f..c220e5a0f4a4 100644
--- a/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx
+++ b/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx
@@ -43,7 +43,7 @@ describe(" ", () => {
return result;
}
- function mockEdits(...edits: { msg: string; ts: number | undefined }[]) {
+ function mockEdits(...edits: { msg: string; ts?: number }[]) {
client.relations.mockImplementation(() =>
Promise.resolve({
events: edits.map(
diff --git a/test/components/views/dialogs/SpotlightDialog-test.tsx b/test/components/views/dialogs/SpotlightDialog-test.tsx
index e587ffbe5cd1..fb1fc1e65ffe 100644
--- a/test/components/views/dialogs/SpotlightDialog-test.tsx
+++ b/test/components/views/dialogs/SpotlightDialog-test.tsx
@@ -174,7 +174,7 @@ describe("Spotlight Dialog", () => {
expect(filterChip.innerHTML).toContain("Public rooms");
const content = document.querySelector("#mx_SpotlightDialog_content")!;
- const options = content.querySelectorAll("div.mx_SpotlightDialog_option");
+ const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBe(1);
expect(options[0].innerHTML).toContain(testPublicRoom.name);
});
@@ -196,7 +196,7 @@ describe("Spotlight Dialog", () => {
expect(filterChip.innerHTML).toContain("People");
const content = document.querySelector("#mx_SpotlightDialog_content")!;
- const options = content.querySelectorAll("div.mx_SpotlightDialog_option");
+ const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBeGreaterThanOrEqual(1);
expect(options[0]!.innerHTML).toContain(testPerson.display_name);
});
@@ -242,7 +242,7 @@ describe("Spotlight Dialog", () => {
expect(filterChip.innerHTML).toContain("Public rooms");
const content = document.querySelector("#mx_SpotlightDialog_content")!;
- const options = content.querySelectorAll("div.mx_SpotlightDialog_option");
+ const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBe(1);
expect(options[0]!.innerHTML).toContain(testPublicRoom.name);
@@ -265,7 +265,7 @@ describe("Spotlight Dialog", () => {
expect(filterChip.innerHTML).toContain("People");
const content = document.querySelector("#mx_SpotlightDialog_content")!;
- const options = content.querySelectorAll("div.mx_SpotlightDialog_option");
+ const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBeGreaterThanOrEqual(1);
expect(options[0]!.innerHTML).toContain(testPerson.display_name);
});
@@ -324,7 +324,7 @@ describe("Spotlight Dialog", () => {
await flushPromisesWithFakeTimers();
const content = document.querySelector("#mx_SpotlightDialog_content")!;
- options = content.querySelectorAll("div.mx_SpotlightDialog_option");
+ options = content.querySelectorAll("li.mx_SpotlightDialog_option");
});
it("should find Rooms", () => {
@@ -350,7 +350,7 @@ describe("Spotlight Dialog", () => {
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
- const options = document.querySelectorAll("div.mx_SpotlightDialog_option");
+ const options = document.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBeGreaterThanOrEqual(1);
expect(options[0]!.innerHTML).toContain(testPerson.display_name);
@@ -372,7 +372,7 @@ describe("Spotlight Dialog", () => {
await flushPromisesWithFakeTimers();
const content = document.querySelector("#mx_SpotlightDialog_content")!;
- const options = content.querySelectorAll("div.mx_SpotlightDialog_option");
+ const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
expect(options.length).toBe(1);
expect(options[0].innerHTML).toContain(testPublicRoom.name);
diff --git a/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap b/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap
index d82ee64cefa4..1f1c5dff73c8 100644
--- a/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap
+++ b/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap
@@ -1,12 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` renders dropdown options in menu 1`] = `
-
-
-
+
renders dropdown options in menu 1`] = `
with description
-
-
+
+
`;
exports[` renders selected option 1`] = `
diff --git a/test/components/views/emojipicker/EmojiPicker-test.tsx b/test/components/views/emojipicker/EmojiPicker-test.tsx
index efd09825a9ce..4f8c091bb7ca 100644
--- a/test/components/views/emojipicker/EmojiPicker-test.tsx
+++ b/test/components/views/emojipicker/EmojiPicker-test.tsx
@@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import React from "react";
+import { render } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
import EmojiPicker from "../../../../src/components/views/emojipicker/EmojiPicker";
import { stubClient } from "../../../test-utils";
@@ -21,7 +25,7 @@ describe("EmojiPicker", function () {
stubClient();
it("sort emojis by shortcode and size", function () {
- const ep = new EmojiPicker({ onChoose: (str: String) => false });
+ const ep = new EmojiPicker({ onChoose: (str: string) => false, onFinished: jest.fn() });
//@ts-ignore private access
ep.onChangeFilter("heart");
@@ -31,4 +35,47 @@ describe("EmojiPicker", function () {
//@ts-ignore private access
expect(ep.memoizedDataByCategory["people"][1].shortcodes[0]).toEqual("heartbeat");
});
+
+ it("should allow keyboard navigation using arrow keys", async () => {
+ // mock offsetParent
+ Object.defineProperty(HTMLElement.prototype, "offsetParent", {
+ get() {
+ return this.parentNode;
+ },
+ });
+
+ const onChoose = jest.fn();
+ const onFinished = jest.fn();
+ const { container } = render( );
+
+ const input = container.querySelector("input")!;
+ expect(input).toHaveFocus();
+
+ function getEmoji(): string {
+ const activeDescendant = input.getAttribute("aria-activedescendant");
+ return container.querySelector("#" + activeDescendant)!.textContent!;
+ }
+
+ expect(getEmoji()).toEqual("😀");
+ await userEvent.keyboard("[ArrowDown]");
+ expect(getEmoji()).toEqual("🙂");
+ await userEvent.keyboard("[ArrowUp]");
+ expect(getEmoji()).toEqual("😀");
+ await userEvent.keyboard("Flag");
+ await userEvent.keyboard("[ArrowRight]");
+ await userEvent.keyboard("[ArrowRight]");
+ expect(getEmoji()).toEqual("📫️");
+ await userEvent.keyboard("[ArrowDown]");
+ expect(getEmoji()).toEqual("🇦🇨");
+ await userEvent.keyboard("[ArrowLeft]");
+ expect(getEmoji()).toEqual("📭️");
+ await userEvent.keyboard("[ArrowUp]");
+ expect(getEmoji()).toEqual("⛳️");
+ await userEvent.keyboard("[ArrowRight]");
+ expect(getEmoji()).toEqual("📫️");
+ await userEvent.keyboard("[Enter]");
+
+ expect(onChoose).toHaveBeenCalledWith("📫️");
+ expect(onFinished).toHaveBeenCalled();
+ });
});
diff --git a/test/components/views/location/LocationShareMenu-test.tsx b/test/components/views/location/LocationShareMenu-test.tsx
index 9ee667f319e9..8ab7b46cbd0c 100644
--- a/test/components/views/location/LocationShareMenu-test.tsx
+++ b/test/components/views/location/LocationShareMenu-test.tsx
@@ -72,6 +72,11 @@ jest.mock("../../../../src/Modal", () => ({
ModalManagerEvent: { Opened: "opened" },
}));
+// Fake random strings to give a predictable snapshot for IDs
+jest.mock("matrix-js-sdk/src/randomstring", () => ({
+ randomString: () => "abdefghi",
+}));
+
describe(" ", () => {
const userId = "@ernie:server.org";
const mockClient = getMockClientWithEventEmitter({
diff --git a/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap b/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap
index cd492db7a2ee..e83d959d5cdb 100644
--- a/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap
+++ b/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap
@@ -25,12 +25,16 @@ exports[` with live location disabled goes to labs flag scr
- Enable live location sharing
+
+ Enable live location sharing
+
renders the room summary 1`] = `
tabindex="0"
/>
-
!room:domain.org
-
+
renders the room summary 1`] = `
-
+
About
-
+
renders the room summary 1`] = `
-
+
Widgets
-
+
-
+
Toggle Bold
@@ -73,8 +73,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Toggle Italics
@@ -93,8 +93,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Toggle Quote
@@ -119,8 +119,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Toggle Link
@@ -145,8 +145,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Toggle Code Block
@@ -165,8 +165,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Undo edit
@@ -185,8 +185,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Redo edit
@@ -205,8 +205,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Jump to start of the composer
@@ -225,8 +225,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Jump to end of the composer
@@ -245,8 +245,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Cancel replying to a message
@@ -259,8 +259,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Navigate to next message to edit
@@ -273,8 +273,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Navigate to previous message to edit
@@ -287,8 +287,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Navigate to next message in composer history
@@ -313,8 +313,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Navigate to previous message in composer history
@@ -339,8 +339,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Send a sticker
@@ -359,9 +359,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
+
-
+
Calls
-
+
-
Toggle microphone mute
@@ -392,8 +392,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Toggle webcam on/off
@@ -412,9 +412,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
+
-
+
Room
-
+
-
Search (must be enabled)
@@ -445,8 +445,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Upload a file
@@ -471,8 +471,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Dismiss read marker and jump to bottom
@@ -485,8 +485,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Jump to oldest unread message
@@ -505,8 +505,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Scroll up in the timeline
@@ -519,8 +519,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Scroll down in the timeline
@@ -533,8 +533,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Jump to first message
@@ -553,8 +553,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Jump to last message
@@ -573,9 +573,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
+
-
+
Room List
-
+
-
Select room from the room list
@@ -600,8 +600,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Collapse room list section
@@ -614,8 +614,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Expand room list section
@@ -628,8 +628,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Navigate down in the room list
@@ -642,8 +642,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Navigate up in the room list
@@ -656,9 +656,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
+
-
+
Accessibility
-
+
-
Close dialog or context menu
@@ -683,8 +683,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Activate selected button
@@ -697,9 +697,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
+
-
+
Navigation
-
+
-
Toggle the top left menu
@@ -730,8 +730,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Toggle right panel
@@ -750,8 +750,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Toggle space panel
@@ -776,8 +776,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Open this settings tab
@@ -796,8 +796,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Go to Home View
@@ -822,8 +822,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Jump to room search
@@ -842,8 +842,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Next unread room or DM
@@ -868,8 +868,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Previous unread room or DM
@@ -894,8 +894,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Next room or DM
@@ -914,8 +914,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Previous room or DM
@@ -934,9 +934,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
+
-
+
Autocomplete
-
+
-
Cancel autocomplete
@@ -961,8 +961,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Next autocomplete suggestion
@@ -975,8 +975,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Previous autocomplete suggestion
@@ -989,8 +989,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Complete
@@ -1003,8 +1003,8 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
-
+
Force complete
@@ -1017,9 +1017,9 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
-
+
-
+
diff --git a/test/components/views/spaces/SpaceSettingsVisibilityTab-test.tsx b/test/components/views/spaces/SpaceSettingsVisibilityTab-test.tsx
index 1d2ee066f982..a7d507e01146 100644
--- a/test/components/views/spaces/SpaceSettingsVisibilityTab-test.tsx
+++ b/test/components/views/spaces/SpaceSettingsVisibilityTab-test.tsx
@@ -16,6 +16,7 @@ limitations under the License.
import React from "react";
import { mocked } from "jest-mock";
+import { randomString } from "matrix-js-sdk/src/randomstring";
import { act, fireEvent, render, RenderResult } from "@testing-library/react";
import { EventType, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
@@ -27,6 +28,11 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
const SpaceSettingsVisibilityTab = wrapInMatrixClientContext(_SpaceSettingsVisibilityTab);
+// Fake random strings to give a predictable snapshot for IDs
+jest.mock("matrix-js-sdk/src/randomstring", () => ({
+ randomString: jest.fn(),
+}));
+
jest.useFakeTimers();
describe(" ", () => {
@@ -89,13 +95,16 @@ describe(" ", () => {
const toggleButton = getByTestId("toggle-guest-access-btn")!;
fireEvent.click(toggleButton);
};
- const getGuestAccessToggle = ({ container }: RenderResult) =>
- container.querySelector('[aria-label="Enable guest access"]');
- const getHistoryVisibilityToggle = ({ container }: RenderResult) =>
- container.querySelector('[aria-label="Preview Space"]');
+ const getGuestAccessToggle = ({ getByLabelText }: RenderResult) => getByLabelText("Enable guest access");
+ const getHistoryVisibilityToggle = ({ getByLabelText }: RenderResult) => getByLabelText("Preview Space");
const getErrorMessage = ({ getByTestId }: RenderResult) => getByTestId("space-settings-error")?.textContent;
beforeEach(() => {
+ let i = 0;
+ mocked(randomString).mockImplementation(() => {
+ return "testid_" + i++;
+ });
+
(mockMatrixClient.sendStateEvent as jest.Mock).mockClear().mockResolvedValue({});
MatrixClientPeg.get = jest.fn().mockReturnValue(mockMatrixClient);
});
diff --git a/test/components/views/spaces/__snapshots__/SpaceSettingsVisibilityTab-test.tsx.snap b/test/components/views/spaces/__snapshots__/SpaceSettingsVisibilityTab-test.tsx.snap
index 8de0ae2c153e..a93fda9d6a56 100644
--- a/test/components/views/spaces/__snapshots__/SpaceSettingsVisibilityTab-test.tsx.snap
+++ b/test/components/views/spaces/__snapshots__/SpaceSettingsVisibilityTab-test.tsx.snap
@@ -4,7 +4,7 @@ exports[` for a public space Access renders guest
renders container 1`] = `
- Preview Space
+
+ Preview Space
+
=> {
// of removing the same modal because the promises don't flush otherwise.
//
// XXX: Maybe in the future with Jest 29.5.0+, we could use `runAllTimersAsync` instead.
- await flushPromisesWithFakeTimers();
+
+ // this is called in some places where timers are not faked
+ // which causes a lot of noise in the console
+ // to make a hack even hackier check if timers are faked using a weird trick from github
+ // then call the appropriate promise flusher
+ // https://github.com/facebook/jest/issues/10555#issuecomment-1136466942
+ const jestTimersFaked = setTimeout.name === "setTimeout";
+ if (jestTimersFaked) {
+ await flushPromisesWithFakeTimers();
+ } else {
+ await flushPromises();
+ }
}
};