From 58718dab37d145566c70b8f2731acd35ed798cd6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 23:51:12 +0100 Subject: [PATCH 01/72] Convert ContextMenu to TypeScript Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/@types/common.ts | 1 + .../{ContextMenu.js => ContextMenu.tsx} | 259 ++++++++++-------- .../views/elements/AccessibleButton.tsx | 2 +- 3 files changed, 148 insertions(+), 114 deletions(-) rename src/components/structures/{ContextMenu.js => ContextMenu.tsx} (71%) diff --git a/src/@types/common.ts b/src/@types/common.ts index 9109993541..a24d47ac9e 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -17,3 +17,4 @@ limitations under the License. // Based on https://stackoverflow.com/a/53229857/3532235 export type Without = {[P in Exclude] ? : never}; export type XOR = (T | U) extends object ? (Without & U) | (Without & T) : T | U; +export type Writeable = { -readonly [P in keyof T]: T[P] }; diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.tsx similarity index 71% rename from src/components/structures/ContextMenu.js rename to src/components/structures/ContextMenu.tsx index 5ba2662796..f07c12f0b3 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.tsx @@ -1,28 +1,28 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. + * + * Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under 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 obtain a copy of the License at +import React, {CSSProperties, useRef, useState} from "react"; +import ReactDOM from "react-dom"; +import classNames from "classnames"; - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React, {useRef, useState} from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; import {Key} from "../../Keyboard"; -import * as sdk from "../../index"; -import AccessibleButton from "../views/elements/AccessibleButton"; +import AccessibleButton, { IAccessibleButtonProps } from "../views/elements/AccessibleButton"; +import {Writeable} from "../../@types/common"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -30,8 +30,8 @@ import AccessibleButton from "../views/elements/AccessibleButton"; const ContextualMenuContainerId = "mx_ContextualMenu_Container"; -function getOrCreateContainer() { - let container = document.getElementById(ContextualMenuContainerId); +function getOrCreateContainer(): HTMLDivElement { + let container = document.getElementById(ContextualMenuContainerId) as HTMLDivElement; if (!container) { container = document.createElement("div"); @@ -43,50 +43,70 @@ function getOrCreateContainer() { } const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]); + +interface IPosition { + top?: number; + bottom?: number; + left?: number; + right?: number; +} + +export enum ChevronFace { + Top = "top", + Bottom = "bottom", + Left = "left", + Right = "right", + None = "none", +} + +interface IProps extends IPosition { + menuWidth?: number; + menuHeight?: number; + + chevronOffset?: number; + chevronFace?: ChevronFace; + + menuPaddingTop?: number; + menuPaddingBottom?: number; + menuPaddingLeft?: number; + menuPaddingRight?: number; + + zIndex?: number; + + // If true, insert an invisible screen-sized element behind the menu that when clicked will close it. + hasBackground?: boolean; + // whether this context menu should be focus managed. If false it must handle itself + managed?: boolean; + + // Function to be called on menu close + onFinished(); + // on resize callback + windowResize(); +} + +interface IState { + contextMenuElem: HTMLDivElement; +} + // Generic ContextMenu Portal wrapper // all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1} // this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines. -export class ContextMenu extends React.Component { - static propTypes = { - top: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - menuWidth: PropTypes.number, - menuHeight: PropTypes.number, - chevronOffset: PropTypes.number, - chevronFace: PropTypes.string, // top, bottom, left, right or none - // Function to be called on menu close - onFinished: PropTypes.func.isRequired, - menuPaddingTop: PropTypes.number, - menuPaddingRight: PropTypes.number, - menuPaddingBottom: PropTypes.number, - menuPaddingLeft: PropTypes.number, - zIndex: PropTypes.number, - - // If true, insert an invisible screen-sized element behind the - // menu that when clicked will close it. - hasBackground: PropTypes.bool, - - // on resize callback - windowResize: PropTypes.func, - - managed: PropTypes.bool, // whether this context menu should be focus managed. If false it must handle itself - }; +export class ContextMenu extends React.PureComponent { + private initialFocus: HTMLElement; static defaultProps = { hasBackground: true, managed: true, }; - constructor() { - super(); + constructor(props, context) { + super(props, context); this.state = { contextMenuElem: null, }; // persist what had focus when we got initialized so we can return it after - this.initialFocus = document.activeElement; + this.initialFocus = document.activeElement as HTMLElement; } componentWillUnmount() { @@ -232,9 +252,8 @@ export class ContextMenu extends React.Component { } }; - renderMenu(hasBackground=this.props.hasBackground) { - const position = {}; - let chevronFace = null; + renderMenu(hasBackground = this.props.hasBackground) { + const position: Partial> = {}; const props = this.props; if (props.top) { @@ -243,23 +262,24 @@ export class ContextMenu extends React.Component { position.bottom = props.bottom; } + let chevronFace: IProps["chevronFace"]; if (props.left) { position.left = props.left; - chevronFace = 'left'; + chevronFace = ChevronFace.Left; } else { position.right = props.right; - chevronFace = 'right'; + chevronFace = ChevronFace.Right; } const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null; - const chevronOffset = {}; + const chevronOffset: CSSProperties = {}; if (props.chevronFace) { chevronFace = props.chevronFace; } - const hasChevron = chevronFace && chevronFace !== "none"; + const hasChevron = chevronFace && chevronFace !== ChevronFace.None; - if (chevronFace === 'top' || chevronFace === 'bottom') { + if (chevronFace === ChevronFace.Top || chevronFace === ChevronFace.Bottom) { chevronOffset.left = props.chevronOffset; } else if (position.top !== undefined) { const target = position.top; @@ -289,13 +309,13 @@ export class ContextMenu extends React.Component { 'mx_ContextualMenu_right': !hasChevron && position.right, 'mx_ContextualMenu_top': !hasChevron && position.top, 'mx_ContextualMenu_bottom': !hasChevron && position.bottom, - 'mx_ContextualMenu_withChevron_left': chevronFace === 'left', - 'mx_ContextualMenu_withChevron_right': chevronFace === 'right', - 'mx_ContextualMenu_withChevron_top': chevronFace === 'top', - 'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom', + 'mx_ContextualMenu_withChevron_left': chevronFace === ChevronFace.Left, + 'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right, + 'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top, + 'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom, }); - const menuStyle = {}; + const menuStyle: CSSProperties = {}; if (props.menuWidth) { menuStyle.width = props.menuWidth; } @@ -326,13 +346,28 @@ export class ContextMenu extends React.Component { let background; if (hasBackground) { background = ( -
+
); } return ( -
-
+
+
{ chevron } { props.children }
@@ -341,14 +376,19 @@ export class ContextMenu extends React.Component { ); } - render() { + render(): React.ReactChild { return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer()); } } +interface IContextMenuButtonProps extends IAccessibleButtonProps { + label?: string; + // whether or not the context menu is currently open + isExpanded: boolean; +} + // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuButton = ({ label, isExpanded, children, onClick, onContextMenu, ...props }) => { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); +export const ContextMenuButton: React.FC = ({ label, isExpanded, children, onClick, onContextMenu, ...props }) => { return ( ); }; -ContextMenuButton.propTypes = { - ...AccessibleButton.propTypes, - label: PropTypes.string, - isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open -}; + +interface IMenuItemProps extends IAccessibleButtonProps { + label?: string; + className?: string; + onClick(); +} // Semantic component for representing a role=menuitem -export const MenuItem = ({children, label, ...props}) => { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); +export const MenuItem: React.FC = ({children, label, ...props}) => { return ( { children } ); }; -MenuItem.propTypes = { - ...AccessibleButton.propTypes, - label: PropTypes.string, // optional - className: PropTypes.string, // optional - onClick: PropTypes.func.isRequired, -}; + +interface IMenuGroupProps extends React.HTMLAttributes { + label: string; + className?: string; +} // Semantic component for representing a role=group for grouping menu radios/checkboxes -export const MenuGroup = ({children, label, ...props}) => { +export const MenuGroup: React.FC = ({children, label, ...props}) => { return
{ children }
; }; -MenuGroup.propTypes = { - label: PropTypes.string.isRequired, - className: PropTypes.string, // optional -}; + +interface IMenuItemCheckboxProps extends IAccessibleButtonProps { + label?: string; + active: boolean; + disabled?: boolean; + className?: string; + onClick(); +} // Semantic component for representing a role=menuitemcheckbox -export const MenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); +export const MenuItemCheckbox: React.FC = ({children, label, active = false, disabled = false, ...props}) => { return ( { children } ); }; -MenuItemCheckbox.propTypes = { - ...AccessibleButton.propTypes, - label: PropTypes.string, // optional - active: PropTypes.bool.isRequired, - disabled: PropTypes.bool, // optional - className: PropTypes.string, // optional - onClick: PropTypes.func.isRequired, -}; + +interface IMenuItemRadioProps extends IAccessibleButtonProps { + label?: string; + active: boolean; + disabled?: boolean; + className?: string; + onClick(); +} // Semantic component for representing a role=menuitemradio -export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); +export const MenuItemRadio: React.FC = ({children, label, active = false, disabled = false, ...props}) => { return ( { children } ); }; -MenuItemRadio.propTypes = { - ...AccessibleButton.propTypes, - label: PropTypes.string, // optional - active: PropTypes.bool.isRequired, - disabled: PropTypes.bool, // optional - className: PropTypes.string, // optional - onClick: PropTypes.func.isRequired, -}; // Placement method for to position context menu to right of elementRect with chevronOffset -export const toRightOf = (elementRect, chevronOffset=12) => { +export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => { const left = elementRect.right + window.pageXOffset + 3; let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset; top -= chevronOffset + 8; // where 8 is half the height of the chevron @@ -441,8 +474,8 @@ export const toRightOf = (elementRect, chevronOffset=12) => { }; // Placement method for to position context menu right-aligned and flowing to the left of elementRect -export const aboveLeftOf = (elementRect, chevronFace="none") => { - const menuOptions = { chevronFace }; +export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None) => { + const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; const buttonRight = elementRect.right + window.pageXOffset; const buttonBottom = elementRect.bottom + window.pageYOffset; diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 01a27d9522..489f11699a 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -42,7 +42,7 @@ interface IProps extends React.InputHTMLAttributes { onClick?(e?: ButtonEvent): void; } -interface IAccessibleButtonProps extends React.InputHTMLAttributes { +export interface IAccessibleButtonProps extends React.InputHTMLAttributes { ref?: React.Ref; } From 6802f9b4dfc237a61e465e5b445fe28604331a45 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 23:52:49 +0100 Subject: [PATCH 02/72] unbreak copyright Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/ContextMenu.tsx | 32 +++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index f07c12f0b3..3e38a18d98 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -1,20 +1,20 @@ /* - * - * Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * / - */ +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ import React, {CSSProperties, useRef, useState} from "react"; import ReactDOM from "react-dom"; From 07e0a017e7885692e308b02a63c947d147c68cc2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 23:56:57 +0100 Subject: [PATCH 03/72] fix types Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/ContextMenu.tsx | 10 ++++----- src/components/structures/UserMenu.tsx | 22 ++++++++++---------- src/components/views/rooms/RoomSublist2.tsx | 23 ++++++++++----------- src/components/views/rooms/RoomTile2.tsx | 8 +++---- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 3e38a18d98..be41ec1569 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -21,7 +21,7 @@ import ReactDOM from "react-dom"; import classNames from "classnames"; import {Key} from "../../Keyboard"; -import AccessibleButton, { IAccessibleButtonProps } from "../views/elements/AccessibleButton"; +import AccessibleButton, { IAccessibleButtonProps, ButtonEvent } from "../views/elements/AccessibleButton"; import {Writeable} from "../../@types/common"; // Shamelessly ripped off Modal.js. There's probably a better way @@ -81,7 +81,7 @@ interface IProps extends IPosition { // Function to be called on menu close onFinished(); // on resize callback - windowResize(); + windowResize?(); } interface IState { @@ -407,7 +407,7 @@ export const ContextMenuButton: React.FC = ({ label, is interface IMenuItemProps extends IAccessibleButtonProps { label?: string; className?: string; - onClick(); + onClick(ev: ButtonEvent); } // Semantic component for representing a role=menuitem @@ -436,7 +436,7 @@ interface IMenuItemCheckboxProps extends IAccessibleButtonProps { active: boolean; disabled?: boolean; className?: string; - onClick(); + onClick(ev: ButtonEvent); } // Semantic component for representing a role=menuitemcheckbox @@ -453,7 +453,7 @@ interface IMenuItemRadioProps extends IAccessibleButtonProps { active: boolean; disabled?: boolean; className?: string; - onClick(); + onClick(ev: ButtonEvent); } // Semantic component for representing a role=menuitemradio diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 1cfe244845..aba0a3f053 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -15,15 +15,15 @@ limitations under the License. */ import * as React from "react"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; +import {createRef} from "react"; +import {MatrixClientPeg} from "../../MatrixClientPeg"; import defaultDispatcher from "../../dispatcher/dispatcher"; -import { ActionPayload } from "../../dispatcher/payloads"; -import { Action } from "../../dispatcher/actions"; -import { createRef } from "react"; -import { _t } from "../../languageHandler"; -import {ContextMenu, ContextMenuButton} from "./ContextMenu"; +import {ActionPayload} from "../../dispatcher/payloads"; +import {Action} from "../../dispatcher/actions"; +import {_t} from "../../languageHandler"; +import {ChevronFace, ContextMenu, ContextMenuButton} from "./ContextMenu"; import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog"; -import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; +import {OpenToTabPayload} from "../../dispatcher/payloads/OpenToTabPayload"; import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog"; import Modal from "../../Modal"; import LogoutDialog from "../views/dialogs/LogoutDialog"; @@ -33,8 +33,8 @@ import {getHostingLink} from "../../utils/HostingLink"; import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; import SdkConfig from "../../SdkConfig"; import {getHomePageUrl} from "../../utils/pages"; -import { OwnProfileStore } from "../../stores/OwnProfileStore"; -import { UPDATE_EVENT } from "../../stores/AsyncStore"; +import {OwnProfileStore} from "../../stores/OwnProfileStore"; +import {UPDATE_EVENT} from "../../stores/AsyncStore"; import BaseAvatar from '../views/avatars/BaseAvatar'; import classNames from "classnames"; @@ -105,7 +105,7 @@ export default class UserMenu extends React.Component { if (this.buttonRef.current) this.buttonRef.current.click(); }; - private onOpenMenuClick = (ev: InputEvent) => { + private onOpenMenuClick = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); const target = ev.target as HTMLButtonElement; @@ -214,7 +214,7 @@ export default class UserMenu extends React.Component { return ( { this.forceUpdate(); // because the layout doesn't trigger a re-render }; - private onOpenMenuClick = (ev: InputEvent) => { + private onOpenMenuClick = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); const target = ev.target as HTMLButtonElement; @@ -219,7 +218,7 @@ export default class RoomSublist2 extends React.Component { const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; contextMenu = ( { // align the context menu's icons with the icon which opened the context menu const left = elementRect.left + window.pageXOffset - 9; const top = elementRect.bottom + window.pageYOffset + 17; - const chevronFace = "none"; + const chevronFace = ChevronFace.None; return {left, top, chevronFace}; }; @@ -151,7 +151,7 @@ export default class RoomTile2 extends React.Component { this.setState({selected: isActive}); }; - private onNotificationsMenuOpenClick = (ev: InputEvent) => { + private onNotificationsMenuOpenClick = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); const target = ev.target as HTMLButtonElement; @@ -162,7 +162,7 @@ export default class RoomTile2 extends React.Component { this.setState({notificationsMenuPosition: null}); }; - private onGeneralMenuOpenClick = (ev: InputEvent) => { + private onGeneralMenuOpenClick = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); const target = ev.target as HTMLButtonElement; From 9fcc2ced3d3ed7af84169d2a80c61733033aac22 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 23:59:06 +0100 Subject: [PATCH 04/72] fix types some more Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/ContextMenu.tsx | 2 +- src/components/views/elements/AccessibleButton.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index be41ec1569..cec924c022 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -21,7 +21,7 @@ import ReactDOM from "react-dom"; import classNames from "classnames"; import {Key} from "../../Keyboard"; -import AccessibleButton, { IAccessibleButtonProps, ButtonEvent } from "../views/elements/AccessibleButton"; +import AccessibleButton, { IProps as IAccessibleButtonProps, ButtonEvent } from "../views/elements/AccessibleButton"; import {Writeable} from "../../@types/common"; // Shamelessly ripped off Modal.js. There's probably a better way diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 489f11699a..34481601f7 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -27,7 +27,7 @@ export type ButtonEvent = React.MouseEvent | React.KeyboardEvent { +export interface IProps extends React.InputHTMLAttributes { inputRef?: React.Ref; element?: string; // The kind of button, similar to how Bootstrap works. @@ -42,7 +42,7 @@ interface IProps extends React.InputHTMLAttributes { onClick?(e?: ButtonEvent): void; } -export interface IAccessibleButtonProps extends React.InputHTMLAttributes { +interface IAccessibleButtonProps extends React.InputHTMLAttributes { ref?: React.Ref; } @@ -64,7 +64,6 @@ export default function AccessibleButton({ className, ...restProps }: IProps) { - const newProps: IAccessibleButtonProps = restProps; if (!disabled) { newProps.onClick = onClick; From 0854924b8dd3b84850649cf5c257293e4330ef60 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2020 23:51:02 +0100 Subject: [PATCH 05/72] iterate some more Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/ContextMenu.tsx | 34 +++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index c76bbe6133..e45d846bd0 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -114,7 +114,7 @@ export class ContextMenu extends React.PureComponent { this.initialFocus.focus(); } - collectContextMenuRect = (element) => { + private collectContextMenuRect = (element) => { // We don't need to clean up when unmounting, so ignore if (!element) return; @@ -131,7 +131,7 @@ export class ContextMenu extends React.PureComponent { }); }; - onContextMenu = (e) => { + private onContextMenu = (e) => { if (this.props.onFinished) { this.props.onFinished(); @@ -154,20 +154,20 @@ export class ContextMenu extends React.PureComponent { } }; - onContextMenuPreventBubbling = (e) => { + private onContextMenuPreventBubbling = (e) => { // stop propagation so that any context menu handlers don't leak out of this context menu // but do not inhibit the default browser menu e.stopPropagation(); }; // Prevent clicks on the background from going through to the component which opened the menu. - _onFinished = (ev: InputEvent) => { + private onFinished = (ev: React.MouseEvent) => { ev.stopPropagation(); ev.preventDefault(); if (this.props.onFinished) this.props.onFinished(); }; - _onMoveFocus = (element, up) => { + private onMoveFocus = (element: Element, up: boolean) => { let descending = false; // are we currently descending or ascending through the DOM tree? do { @@ -201,25 +201,25 @@ export class ContextMenu extends React.PureComponent { } while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role"))); if (element) { - element.focus(); + (element as HTMLElement).focus(); } }; - _onMoveFocusHomeEnd = (element, up) => { + private onMoveFocusHomeEnd = (element: Element, up: boolean) => { let results = element.querySelectorAll('[role^="menuitem"]'); if (!results) { results = element.querySelectorAll('[tab-index]'); } if (results && results.length) { if (up) { - results[0].focus(); + (results[0] as HTMLElement).focus(); } else { - results[results.length - 1].focus(); + (results[results.length - 1] as HTMLElement).focus(); } } }; - _onKeyDown = (ev) => { + private onKeyDown = (ev: React.KeyboardEvent) => { if (!this.props.managed) { if (ev.key === Key.ESCAPE) { this.props.onFinished(); @@ -237,16 +237,16 @@ export class ContextMenu extends React.PureComponent { this.props.onFinished(); break; case Key.ARROW_UP: - this._onMoveFocus(ev.target, true); + this.onMoveFocus(ev.target as Element, true); break; case Key.ARROW_DOWN: - this._onMoveFocus(ev.target, false); + this.onMoveFocus(ev.target as Element, false); break; case Key.HOME: - this._onMoveFocusHomeEnd(this.state.contextMenuElem, true); + this.onMoveFocusHomeEnd(this.state.contextMenuElem, true); break; case Key.END: - this._onMoveFocusHomeEnd(this.state.contextMenuElem, false); + this.onMoveFocusHomeEnd(this.state.contextMenuElem, false); break; default: handled = false; @@ -259,7 +259,7 @@ export class ContextMenu extends React.PureComponent { } }; - renderMenu(hasBackground = this.props.hasBackground) { + protected renderMenu(hasBackground = this.props.hasBackground) { const position: Partial> = {}; const props = this.props; @@ -356,7 +356,7 @@ export class ContextMenu extends React.PureComponent {
); @@ -366,7 +366,7 @@ export class ContextMenu extends React.PureComponent {
Date: Sun, 5 Jul 2020 01:06:36 +0100 Subject: [PATCH 06/72] Improve a11y of default BaseAvatar Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/avatars/BaseAvatar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 508691e5fd..53e8d0072b 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -132,7 +132,7 @@ const BaseAvatar = (props) => { ); } else { return ( - + { textNode } { imgNode } From 1620feb55eb4419dd660439f2d91d4434a468c09 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 5 Jul 2020 01:07:46 +0100 Subject: [PATCH 07/72] Sprinkle in some better ARIA props Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanel2.tsx | 9 ++++-- src/components/structures/RoomSearch.tsx | 7 +++-- src/components/views/rooms/RoomList2.tsx | 3 -- src/components/views/rooms/RoomSublist2.tsx | 22 +++++++++----- src/components/views/rooms/RoomTile2.tsx | 33 ++++++++++++++++++--- 5 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 23a9e74646..0b3fa2f7f6 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -235,7 +235,12 @@ export default class LeftPanel2 extends React.Component { private renderSearchExplore(): React.ReactNode { return ( -
+
{ // TODO fix the accessibility of this: https://github.com/vector-im/riot-web/issues/14180 className="mx_LeftPanel2_exploreButton" onClick={this.onExplore} - alt={_t("Explore rooms")} + title={_t("Explore rooms")} />
); diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 7ed2acf276..15f3bd5b54 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -149,7 +149,8 @@ export default class RoomSearch extends React.PureComponent { let clearButton = ( ); @@ -157,8 +158,8 @@ export default class RoomSearch extends React.PureComponent { if (this.props.isMinimized) { icon = ( ); diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index b0bb70c9a0..06708931a2 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -276,9 +276,6 @@ export default class RoomList2 extends React.Component { className="mx_RoomList2" role="tree" aria-label={_t("Rooms")} - // Firefox sometimes makes this element focusable due to - // overflow:scroll;, so force it out of tab order. - tabIndex={-1} >{sublists}
)} diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 21e7c581f0..69125ca88f 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -63,7 +63,7 @@ interface IProps { onAddRoom?: () => void; addRoomLabel: string; isInvite: boolean; - layout: ListLayout; + layout?: ListLayout; isMinimized: boolean; tagId: TagID; @@ -203,6 +203,7 @@ export default class RoomSublist2 extends React.Component { dis.dispatch({ action: 'view_room', room_id: room.roomId, + show_room_tile: true, // to make sure the room gets scrolled into view }); } }; @@ -383,16 +384,22 @@ export default class RoomSublist2 extends React.Component { private renderHeader(): React.ReactElement { return ( - + {({onFocus, isActive, ref}) => { const tabIndex = isActive ? 0 : -1; + let ariaLabel = _t("Jump to first unread room."); + if (this.props.tagId === DefaultTagID.Invite) { + ariaLabel = _t("Jump to first invite."); + } + const badge = ( ); @@ -433,7 +440,7 @@ export default class RoomSublist2 extends React.Component { // doesn't become sticky. // The same applies to the notification badge. return ( -
+
{ tabIndex={tabIndex} className="mx_RoomSublist2_headerText" role="treeitem" + aria-expanded={!this.props.layout || !this.props.layout.isCollapsed} aria-level={1} onClick={this.onHeaderClick} onContextMenu={this.onContextMenu} @@ -496,12 +504,12 @@ export default class RoomSublist2 extends React.Component { ); if (this.props.isMinimized) showMoreText = null; showNButton = ( -
+ {/* set by CSS masking */} {showMoreText} -
+
); } else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) { // we have all tiles visible - add a button to show less @@ -512,12 +520,12 @@ export default class RoomSublist2 extends React.Component { ); if (this.props.isMinimized) showLessText = null; showNButton = ( -
+ {/* set by CSS masking */} {showLessText} -
+ ); } diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 8a9712b5a4..46b6d57501 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -30,9 +30,15 @@ import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/ import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; -import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs"; +import { + getRoomNotifsState, + setRoomNotifsState, + ALL_MESSAGES, + ALL_MESSAGES_LOUD, + MENTIONS_ONLY, + MUTE, +} from "../../../RoomNotifs"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { setRoomNotifsState } from "../../../RoomNotifs"; import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState"; import { INotificationState } from "../../../stores/notifications/INotificationState"; import NotificationBadge from "./NotificationBadge"; @@ -406,10 +412,11 @@ export default class RoomTile2 extends React.Component { } } + const notificationColor = this.state.notificationState.color; const nameClasses = classNames({ "mx_RoomTile2_name": true, "mx_RoomTile2_nameWithPreview": !!messagePreview, - "mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.color >= NotificationColor.Bold, + "mx_RoomTile2_nameHasUnreadEvents": notificationColor >= NotificationColor.Bold, }); let nameContainer = ( @@ -422,6 +429,22 @@ export default class RoomTile2 extends React.Component { ); if (this.props.isMinimized) nameContainer = null; + let ariaLabel = name; + // The following labels are written in such a fashion to increase screen reader efficiency (speed). + if (this.props.tag === DefaultTagID.Invite) { + // append nothing + } else if (notificationColor >= NotificationColor.Red) { + ariaLabel += " " + _t("%(count)s unread messages including mentions.", { + count: this.state.notificationState.count, + }); + } else if (notificationColor >= NotificationColor.Grey) { + ariaLabel += " " + _t("%(count)s unread messages.", { + count: this.state.notificationState.count, + }); + } else if (notificationColor >= NotificationColor.Bold) { + ariaLabel += " " + _t("Unread messages."); + } + return ( @@ -434,8 +457,10 @@ export default class RoomTile2 extends React.Component { onMouseEnter={this.onTileMouseEnter} onMouseLeave={this.onTileMouseLeave} onClick={this.onTileClick} - role="treeitem" onContextMenu={this.onContextMenu} + role="treeitem" + aria-label={ariaLabel} + aria-selected={this.state.selected} > {roomAvatar} {nameContainer} From 4f1cd82b665e2d481cae53d3ae0a9f1e337aff8a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 5 Jul 2020 01:24:22 +0100 Subject: [PATCH 08/72] i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b23264a297..3794816a27 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1208,6 +1208,8 @@ "Show": "Show", "Message preview": "Message preview", "List options": "List options", + "Jump to first unread room.": "Jump to first unread room.", + "Jump to first invite.": "Jump to first invite.", "Add room": "Add room", "Show %(count)s more|other": "Show %(count)s more", "Show %(count)s more|one": "Show %(count)s more", @@ -2089,6 +2091,8 @@ "Find a room…": "Find a room…", "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "If you can't find the room you're looking for, ask for an invite or Create a new room.", + "Clear filter": "Clear filter", + "Search rooms": "Search rooms", "You can't send any messages until you review and agree to our terms and conditions.": "You can't send any messages until you review and agree to our terms and conditions.", "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.", @@ -2100,8 +2104,6 @@ "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Active call": "Active call", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", - "Jump to first unread room.": "Jump to first unread room.", - "Jump to first invite.": "Jump to first invite.", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", "Search failed": "Search failed", @@ -2116,7 +2118,6 @@ "Click to mute video": "Click to mute video", "Click to unmute audio": "Click to unmute audio", "Click to mute audio": "Click to mute audio", - "Clear filter": "Clear filter", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", From 83cfdd9c07e647ee7ed9c5f728e421771bf9c625 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 5 Jul 2020 01:30:22 +0100 Subject: [PATCH 09/72] Fix accessibility of the Explore button Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/_LeftPanel2.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index bdaada0d15..a73658d916 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -86,11 +86,15 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations .mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton { // Cheaty way to return the occupied space to the filter input + flex-basis: 0; margin: 0; width: 0; - // Don't forget to hide the masked ::before icon - visibility: hidden; + // Don't forget to hide the masked ::before icon, + // using display:none or visibility:hidden would break accessibility + &::before { + content: none; + } } .mx_LeftPanel2_exploreButton { From 069cdf3ce01fa29181b53dc1d336bf699d7b709b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 5 Jul 2020 18:23:57 +0100 Subject: [PATCH 10/72] Fix room list v2 context menus to be aria menus Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/ContextMenu.js | 36 +++++++++++++++++++++ src/components/views/rooms/RoomSublist2.tsx | 26 ++++++++------- src/components/views/rooms/RoomTile2.tsx | 24 ++++++++++---- 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js index e43b0d1431..038208e49f 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.js @@ -23,6 +23,8 @@ import classNames from 'classnames'; import {Key} from "../../Keyboard"; import * as sdk from "../../index"; import AccessibleButton from "../views/elements/AccessibleButton"; +import StyledCheckbox from "../views/elements/StyledCheckbox"; +import StyledRadioButton from "../views/elements/StyledRadioButton"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -421,6 +423,23 @@ MenuItemCheckbox.propTypes = { onClick: PropTypes.func.isRequired, }; +// Semantic component for representing a styled role=menuitemcheckbox +export const StyledMenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => { + return ( + + { children } + + ); +}; +StyledMenuItemCheckbox.propTypes = { + ...AccessibleButton.propTypes, + label: PropTypes.string, // optional + active: PropTypes.bool.isRequired, + disabled: PropTypes.bool, // optional + className: PropTypes.string, // optional + onClick: PropTypes.func.isRequired, +}; + // Semantic component for representing a role=menuitemradio export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -439,6 +458,23 @@ MenuItemRadio.propTypes = { onClick: PropTypes.func.isRequired, }; +// Semantic component for representing a styled role=menuitemradio +export const StyledMenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => { + return ( + + { children } + + ); +}; +StyledMenuItemRadio.propTypes = { + ...StyledMenuItemRadio.propTypes, + label: PropTypes.string, // optional + active: PropTypes.bool.isRequired, + disabled: PropTypes.bool, // optional + className: PropTypes.string, // optional + onClick: PropTypes.func.isRequired, +}; + // Placement method for to position context menu to right of elementRect with chevronOffset export const toRightOf = (elementRect, chevronOffset=12) => { const left = elementRect.right + window.pageXOffset + 3; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 69125ca88f..1b1e2ed66d 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -26,16 +26,18 @@ import AccessibleButton from "../../views/elements/AccessibleButton"; import RoomTile2 from "./RoomTile2"; import { ResizableBox, ResizeCallbackData } from "react-resizable"; import { ListLayout } from "../../../stores/room-list/ListLayout"; -import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; -import StyledCheckbox from "../elements/StyledCheckbox"; -import StyledRadioButton from "../elements/StyledRadioButton"; +import { + ContextMenu, + ContextMenuButton, + StyledMenuItemCheckbox, + StyledMenuItemRadio, +} from "../../structures/ContextMenu"; import RoomListStore from "../../../stores/room-list/RoomListStore2"; import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import dis from "../../../dispatcher/dispatcher"; import NotificationBadge from "./NotificationBadge"; import { ListNotificationState } from "../../../stores/notifications/ListNotificationState"; -import Tooltip from "../elements/Tooltip"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { Key } from "../../../Keyboard"; @@ -329,40 +331,40 @@ export default class RoomSublist2 extends React.Component {
{_t("Sort by")}
- this.onTagSortChanged(SortAlgorithm.Recent)} checked={!isAlphabetical} name={`mx_${this.props.tagId}_sortBy`} > {_t("Activity")} - - + this.onTagSortChanged(SortAlgorithm.Alphabetic)} checked={isAlphabetical} name={`mx_${this.props.tagId}_sortBy`} > {_t("A-Z")} - +

{_t("Unread rooms")}
- {_t("Always show first")} - +

{_t("Show")}
- {_t("Message preview")} - +
diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 46b6d57501..dbaed0d819 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -26,7 +26,13 @@ import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import ActiveRoomObserver from "../../../ActiveRoomObserver"; import { _t } from "../../../languageHandler"; -import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/ContextMenu"; +import { + ContextMenu, + ContextMenuButton, + MenuItemRadio, + MenuItemCheckbox, + MenuItem, +} from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; @@ -328,20 +334,24 @@ export default class RoomTile2 extends React.Component {
- this.onTagRoom(e, DefaultTagID.Favourite)}> + this.onTagRoom(e, DefaultTagID.Favourite)} + active={false} // TODO: https://github.com/vector-im/riot-web/issues/14283 + label={_t("Favourite")} + > {_t("Favourite")} - - + + {_t("Settings")} - +
- + {_t("Leave Room")} - +
From 3cebfc8072b5f84de38c205b5e9be52788d81673 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 5 Jul 2020 19:31:24 +0100 Subject: [PATCH 11/72] Fix StyledMenuItemCheckbox and StyledMenuItemRadio Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/ContextMenu.js | 28 +++++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js index 038208e49f..c4825ca1da 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.js @@ -424,9 +424,17 @@ MenuItemCheckbox.propTypes = { }; // Semantic component for representing a styled role=menuitemcheckbox -export const StyledMenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => { +export const StyledMenuItemCheckbox = ({children, label, checked=false, disabled=false, ...props}) => { return ( - + { children } ); @@ -434,7 +442,7 @@ export const StyledMenuItemCheckbox = ({children, label, active=false, disabled= StyledMenuItemCheckbox.propTypes = { ...AccessibleButton.propTypes, label: PropTypes.string, // optional - active: PropTypes.bool.isRequired, + checked: PropTypes.bool.isRequired, disabled: PropTypes.bool, // optional className: PropTypes.string, // optional onClick: PropTypes.func.isRequired, @@ -459,9 +467,17 @@ MenuItemRadio.propTypes = { }; // Semantic component for representing a styled role=menuitemradio -export const StyledMenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => { +export const StyledMenuItemRadio = ({children, label, checked=false, disabled=false, ...props}) => { return ( - + { children } ); @@ -469,7 +485,7 @@ export const StyledMenuItemRadio = ({children, label, active=false, disabled=fal StyledMenuItemRadio.propTypes = { ...StyledMenuItemRadio.propTypes, label: PropTypes.string, // optional - active: PropTypes.bool.isRequired, + checked: PropTypes.bool.isRequired, disabled: PropTypes.bool, // optional className: PropTypes.string, // optional onClick: PropTypes.func.isRequired, From a68e23c9e089a66894f85b8cdd24ae0cc2be54b4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 5 Jul 2020 19:38:45 +0100 Subject: [PATCH 12/72] Make message previews accessible via describedby Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTile2.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index dbaed0d819..3d9a4b5aca 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -80,6 +80,8 @@ interface IState { generalMenuPosition: PartialDOMRect; } +const messagePreviewId = (roomId: string) => `mx_RoomTile2_messagePreview_${roomId}`; + const contextMenuBelow = (elementRect: PartialDOMRect) => { // align the context menu's icons with the icon which opened the context menu const left = elementRect.left + window.pageXOffset - 9; @@ -135,6 +137,10 @@ export default class RoomTile2 extends React.Component { return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite; } + private get showMessagePreview(): boolean { + return !this.props.isMinimized && this.props.showMessagePreview; + } + public componentWillUnmount() { if (this.props.room) { ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); @@ -408,14 +414,14 @@ export default class RoomTile2 extends React.Component { name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon let messagePreview = null; - if (this.props.showMessagePreview && !this.props.isMinimized) { + if (this.showMessagePreview) { // The preview store heavily caches this info, so should be safe to hammer. const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag); // Only show the preview if there is one to show. if (text) { messagePreview = ( -
+
{text}
); @@ -455,6 +461,11 @@ export default class RoomTile2 extends React.Component { ariaLabel += " " + _t("Unread messages."); } + let ariaDescribedBy: string; + if (this.showMessagePreview) { + ariaDescribedBy = messagePreviewId(this.props.room.roomId); + } + return ( @@ -471,6 +482,7 @@ export default class RoomTile2 extends React.Component { role="treeitem" aria-label={ariaLabel} aria-selected={this.state.selected} + aria-describedby={ariaDescribedBy} > {roomAvatar} {nameContainer} From 7c29a53ebdca7cc06dbd51e34565371c9b04561e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 5 Jul 2020 19:59:29 +0100 Subject: [PATCH 13/72] aria-hide the notifications badge on room tiles as we have manual labels here Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTile2.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 3d9a4b5aca..f3b22c3a64 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -397,8 +397,9 @@ export default class RoomTile2 extends React.Component { let badge: React.ReactNode; if (!this.props.isMinimized) { + // aria-hidden because we summarise the unread count/highlight status in a manual aria-label below badge = ( -
+