Merge branch 't3chguy/accesibility' into release-v1.6.0

This commit is contained in:
J. Ryan Stinnett 2019-09-27 10:17:30 +01:00
commit 5a8943353f
13 changed files with 161 additions and 92 deletions

View file

@ -75,6 +75,7 @@
"filesize": "3.5.6", "filesize": "3.5.6",
"flux": "2.1.1", "flux": "2.1.1",
"focus-trap-react": "^3.0.5", "focus-trap-react": "^3.0.5",
"focus-visible": "^5.0.2",
"fuse.js": "^2.2.0", "fuse.js": "^2.2.0",
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566",
"gfm.css": "^1.1.1", "gfm.css": "^1.1.1",

View file

@ -18,7 +18,7 @@ limitations under the License.
cursor: pointer; cursor: pointer;
} }
.mx_AccessibleButton:focus { .mx_AccessibleButton:focus:not(.focus-visible) {
outline: 0; outline: 0;
} }

View file

@ -21,6 +21,7 @@ import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import {focusCapturedRef} from "../../utils/Accessibility"; import {focusCapturedRef} from "../../utils/Accessibility";
import {KeyCode} from "../../Keyboard";
// Shamelessly ripped off Modal.js. There's probably a better way // Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and // of doing reusable widgets like dialog boxes & menus where we go and
@ -67,7 +68,7 @@ export default class ContextualMenu extends React.Component {
// on resize callback // on resize callback
windowResize: PropTypes.func, windowResize: PropTypes.func,
// method to close menu // method to close menu
closeMenu: PropTypes.func, closeMenu: PropTypes.func.isRequired,
}; };
constructor() { constructor() {
@ -114,6 +115,14 @@ export default class ContextualMenu extends React.Component {
} }
} }
_onKeyDown = (ev) => {
if (ev.keyCode === KeyCode.ESCAPE) {
ev.stopPropagation();
ev.preventDefault();
this.props.closeMenu();
}
};
render() { render() {
const position = {}; const position = {};
let chevronFace = null; let chevronFace = null;
@ -210,7 +219,7 @@ export default class ContextualMenu extends React.Component {
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished // FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click! // property set here so you can't close the menu from a button click!
return <div className={className} style={{...position, ...wrapperStyle}}> return <div className={className} style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} tabIndex={0}> <div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} tabIndex={0}>
{ chevron } { chevron }
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} /> <ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />

View file

@ -24,6 +24,9 @@ import createReactClass from 'create-react-class';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Matrix from "matrix-js-sdk"; import Matrix from "matrix-js-sdk";
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
import 'focus-visible';
import Analytics from "../../Analytics"; import Analytics from "../../Analytics";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import MatrixClientPeg from "../../MatrixClientPeg"; import MatrixClientPeg from "../../MatrixClientPeg";
@ -41,7 +44,7 @@ import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix"; import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle'; import * as Lifecycle from '../../Lifecycle';
// LifecycleStore is not used but does listen to and dispatch actions // LifecycleStore is not used but does listen to and dispatch actions
require('../../stores/LifecycleStore'); import '../../stores/LifecycleStore';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import { getHomePageUrl } from '../../utils/pages'; import { getHomePageUrl } from '../../utils/pages';

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2017 Travis Ralston Copyright 2017 Travis Ralston
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -18,6 +19,7 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import {_t} from '../../languageHandler'; import {_t} from '../../languageHandler';
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import sdk from "../../index";
/** /**
* Represents a tab for the TabbedView. * Represents a tab for the TabbedView.
@ -70,6 +72,8 @@ export class TabbedView extends React.Component {
} }
_renderTabLabel(tab) { _renderTabLabel(tab) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let classes = "mx_TabbedView_tabLabel "; let classes = "mx_TabbedView_tabLabel ";
const idx = this.props.tabs.indexOf(tab); const idx = this.props.tabs.indexOf(tab);
@ -84,19 +88,12 @@ export class TabbedView extends React.Component {
const label = _t(tab.label); const label = _t(tab.label);
return ( return (
<span <AccessibleButton className={classes} key={"tab_label_" + tab.label} onClick={onClickHandler}>
className={classes}
key={"tab_label_" + tab.label}
onClick={onClickHandler}
role="button"
aria-label={label}
tabIndex={0}
>
{tabIcon} {tabIcon}
<span className="mx_TabbedView_tabLabel_text"> <span className="mx_TabbedView_tabLabel_text">
{ label } { label }
</span> </span>
</span> </AccessibleButton>
); );
} }

View file

@ -109,10 +109,11 @@ export default class TopLeftMenuButton extends React.Component {
return ( return (
<AccessibleButton <AccessibleButton
className="mx_TopLeftMenuButton" className="mx_TopLeftMenuButton"
role="button"
onClick={this.onToggleMenu} onClick={this.onToggleMenu}
inputRef={(r) => this._buttonRef = r} inputRef={(r) => this._buttonRef = r}
aria-label={_t("Your profile")} aria-label={_t("Your profile")}
aria-haspopup={true}
aria-expanded={this.state.menuDisplayed}
> >
<BaseAvatar <BaseAvatar
idName={MatrixClientPeg.get().getUserId()} idName={MatrixClientPeg.get().getUserId()}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2018 Vector Creations Ltd Copyright 2018 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -77,11 +78,12 @@ export default class GroupInviteTileContextMenu extends React.Component {
} }
render() { render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <div> return <div>
<div className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} > <AccessibleButton className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" /> <img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
{ _t('Reject') } { _t('Reject') }
</div> </AccessibleButton>
</div>; </div>;
} }
} }

View file

@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -288,6 +289,8 @@ module.exports = createReactClass({
}, },
render: function() { render: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const me = cli.getUserId(); const me = cli.getUserId();
const mxEvent = this.props.mxEvent; const mxEvent = this.props.mxEvent;
@ -319,89 +322,89 @@ module.exports = createReactClass({
if (!mxEvent.isRedacted()) { if (!mxEvent.isRedacted()) {
if (eventStatus === EventStatus.NOT_SENT) { if (eventStatus === EventStatus.NOT_SENT) {
resendButton = ( resendButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onResendClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
{ _t('Resend') } { _t('Resend') }
</div> </AccessibleButton>
); );
} }
if (editStatus === EventStatus.NOT_SENT) { if (editStatus === EventStatus.NOT_SENT) {
resendEditButton = ( resendEditButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onResendEditClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendEditClick}>
{ _t('Resend edit') } { _t('Resend edit') }
</div> </AccessibleButton>
); );
} }
if (unsentReactionsCount !== 0) { if (unsentReactionsCount !== 0) {
resendReactionsButton = ( resendReactionsButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
{ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) } { _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
</div> </AccessibleButton>
); );
} }
} }
if (redactStatus === EventStatus.NOT_SENT) { if (redactStatus === EventStatus.NOT_SENT) {
resendRedactionButton = ( resendRedactionButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onResendRedactionClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendRedactionClick}>
{ _t('Resend removal') } { _t('Resend removal') }
</div> </AccessibleButton>
); );
} }
if (isSent && this.state.canRedact) { if (isSent && this.state.canRedact) {
redactButton = ( redactButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onRedactClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
{ _t('Remove') } { _t('Remove') }
</div> </AccessibleButton>
); );
} }
if (allowCancel) { if (allowCancel) {
cancelButton = ( cancelButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
{ _t('Cancel Sending') } { _t('Cancel Sending') }
</div> </AccessibleButton>
); );
} }
if (isContentActionable(mxEvent)) { if (isContentActionable(mxEvent)) {
forwardButton = ( forwardButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onForwardClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
{ _t('Forward Message') } { _t('Forward Message') }
</div> </AccessibleButton>
); );
if (this.state.canPin) { if (this.state.canPin) {
pinButton = ( pinButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onPinClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') } { this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
</div> </AccessibleButton>
); );
} }
} }
const viewSourceButton = ( const viewSourceButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
{ _t('View Source') } { _t('View Source') }
</div> </AccessibleButton>
); );
if (mxEvent.getType() !== mxEvent.getWireType()) { if (mxEvent.getType() !== mxEvent.getWireType()) {
viewClearSourceButton = ( viewClearSourceButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
{ _t('View Decrypted Source') } { _t('View Decrypted Source') }
</div> </AccessibleButton>
); );
} }
if (this.props.eventTileOps) { if (this.props.eventTileOps) {
if (this.props.eventTileOps.isWidgetHidden()) { if (this.props.eventTileOps.isWidgetHidden()) {
unhidePreviewButton = ( unhidePreviewButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
{ _t('Unhide Preview') } { _t('Unhide Preview') }
</div> </AccessibleButton>
); );
} }
} }
@ -412,20 +415,19 @@ module.exports = createReactClass({
} }
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID) // XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
const permalinkButton = ( const permalinkButton = (
<div className="mx_MessageContextMenu_field"> <AccessibleButton className="mx_MessageContextMenu_field">
<a href={permalink} <a href={permalink} target="_blank" rel="noopener" onClick={this.onPermalinkClick} tabIndex={-1}>
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message' { mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
? _t('Share Permalink') : _t('Share Message') } ? _t('Share Permalink') : _t('Share Message') }
</a> </a>
</div> </AccessibleButton>
); );
if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) { if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) {
quoteButton = ( quoteButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
{ _t('Quote') } { _t('Quote') }
</div> </AccessibleButton>
); );
} }
@ -435,34 +437,43 @@ module.exports = createReactClass({
isUrlPermitted(mxEvent.event.content.external_url) isUrlPermitted(mxEvent.event.content.external_url)
) { ) {
externalURLButton = ( externalURLButton = (
<div className="mx_MessageContextMenu_field"> <AccessibleButton className="mx_MessageContextMenu_field">
<a href={mxEvent.event.content.external_url} <a
rel="noopener" target="_blank" onClick={this.closeMenu}>{ _t('Source URL') }</a> href={mxEvent.event.content.external_url}
</div> target="_blank"
rel="noopener"
onClick={this.closeMenu}
tabIndex={-1}
>
{ _t('Source URL') }
</a>
</AccessibleButton>
); );
} }
if (this.props.collapseReplyThread) { if (this.props.collapseReplyThread) {
collapseReplyThread = ( collapseReplyThread = (
<div className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
{ _t('Collapse Reply Thread') } { _t('Collapse Reply Thread') }
</div> </AccessibleButton>
); );
} }
let e2eInfo; let e2eInfo;
if (this.props.e2eInfoCallback) { if (this.props.e2eInfoCallback) {
e2eInfo = <div className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}> e2eInfo = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
{ _t('End-to-end encryption information') } { _t('End-to-end encryption information') }
</div>; </AccessibleButton>
);
} }
let reportEventButton; let reportEventButton;
if (mxEvent.getSender() !== me) { if (mxEvent.getSender() !== me) {
reportEventButton = ( reportEventButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}> <AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
{ _t('Report Content') } { _t('Report Content') }
</div> </AccessibleButton>
); );
} }

View file

@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -227,6 +228,8 @@ module.exports = createReactClass({
}, },
_renderNotifMenu: function() { _renderNotifMenu: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const alertMeClasses = classNames({ const alertMeClasses = classNames({
'mx_RoomTileContextMenu_notif_field': true, 'mx_RoomTileContextMenu_notif_field': true,
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD, 'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD,
@ -249,29 +252,29 @@ module.exports = createReactClass({
return ( return (
<div className="mx_RoomTileContextMenu"> <div className="mx_RoomTileContextMenu">
<div className="mx_RoomTileContextMenu_notif_picker" > <div className="mx_RoomTileContextMenu_notif_picker">
<img src={require("../../../../res/img/notif-slider.svg")} width="20" height="107" /> <img src={require("../../../../res/img/notif-slider.svg")} width="20" height="107" />
</div> </div>
<div className={alertMeClasses} onClick={this._onClickAlertMe} > <AccessibleButton className={alertMeClasses} onClick={this._onClickAlertMe}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" /> <img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off-copy.svg")} width="16" height="12" /> <img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off-copy.svg")} width="16" height="12" />
{ _t('All messages (noisy)') } { _t('All messages (noisy)') }
</div> </AccessibleButton>
<div className={allNotifsClasses} onClick={this._onClickAllNotifs} > <AccessibleButton className={allNotifsClasses} onClick={this._onClickAllNotifs}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" /> <img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off.svg")} width="16" height="12" /> <img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off.svg")} width="16" height="12" />
{ _t('All messages') } { _t('All messages') }
</div> </AccessibleButton>
<div className={mentionsClasses} onClick={this._onClickMentions} > <AccessibleButton className={mentionsClasses} onClick={this._onClickMentions}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" /> <img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-mentions.svg")} width="16" height="12" /> <img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-mentions.svg")} width="16" height="12" />
{ _t('Mentions only') } { _t('Mentions only') }
</div> </AccessibleButton>
<div className={muteNotifsClasses} onClick={this._onClickMute} > <AccessibleButton className={muteNotifsClasses} onClick={this._onClickMute}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" /> <img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute.svg")} width="16" height="12" /> <img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute.svg")} width="16" height="12" />
{ _t('Mute') } { _t('Mute') }
</div> </AccessibleButton>
</div> </div>
); );
}, },
@ -287,12 +290,13 @@ module.exports = createReactClass({
}, },
_renderSettingsMenu: function() { _renderSettingsMenu: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return ( return (
<div> <div>
<div className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings} > <AccessibleButton className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icons-settings-room.svg")} width="15" height="15" /> <img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icons-settings-room.svg")} width="15" height="15" />
{ _t('Settings') } { _t('Settings') }
</div> </AccessibleButton>
</div> </div>
); );
}, },
@ -302,6 +306,8 @@ module.exports = createReactClass({
return null; return null;
} }
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let leaveClickHandler = null; let leaveClickHandler = null;
let leaveText = null; let leaveText = null;
@ -323,15 +329,17 @@ module.exports = createReactClass({
return ( return (
<div> <div>
<div className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler} > <AccessibleButton className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" /> <img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
{ leaveText } { leaveText }
</div> </AccessibleButton>
</div> </div>
); );
}, },
_renderRoomTagMenu: function() { _renderRoomTagMenu: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const favouriteClasses = classNames({ const favouriteClasses = classNames({
'mx_RoomTileContextMenu_tag_field': true, 'mx_RoomTileContextMenu_tag_field': true,
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isFavourite, 'mx_RoomTileContextMenu_tag_fieldSet': this.state.isFavourite,
@ -352,21 +360,21 @@ module.exports = createReactClass({
return ( return (
<div> <div>
<div className={favouriteClasses} onClick={this._onClickFavourite} > <AccessibleButton className={favouriteClasses} onClick={this._onClickFavourite} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_fave.svg")} width="15" height="15" /> <img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_fave.svg")} width="15" height="15" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_fave_on.svg")} width="15" height="15" /> <img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_fave_on.svg")} width="15" height="15" />
{ _t('Favourite') } { _t('Favourite') }
</div> </AccessibleButton>
<div className={lowPriorityClasses} onClick={this._onClickLowPriority} > <AccessibleButton className={lowPriorityClasses} onClick={this._onClickLowPriority} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_low.svg")} width="15" height="15" /> <img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_low.svg")} width="15" height="15" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_low_on.svg")} width="15" height="15" /> <img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_low_on.svg")} width="15" height="15" />
{ _t('Low Priority') } { _t('Low Priority') }
</div> </AccessibleButton>
<div className={dmClasses} onClick={this._onClickDM} > <AccessibleButton className={dmClasses} onClick={this._onClickDM} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_person.svg")} width="15" height="15" /> <img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_person.svg")} width="15" height="15" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_person_on.svg")} width="15" height="15" /> <img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_person_on.svg")} width="15" height="15" />
{ _t('Direct Chat') } { _t('Direct Chat') }
</div> </AccessibleButton>
</div> </div>
); );
}, },

View file

@ -24,6 +24,7 @@ import Modal from "../../../Modal";
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { getHostingLink } from '../../../utils/HostingLink'; import { getHostingLink } from '../../../utils/HostingLink';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from "../../../index";
export class TopLeftMenu extends React.Component { export class TopLeftMenu extends React.Component {
static propTypes = { static propTypes = {
@ -57,6 +58,8 @@ export class TopLeftMenu extends React.Component {
} }
render() { render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const isGuest = MatrixClientPeg.get().isGuest(); const isGuest = MatrixClientPeg.get().isGuest();
const hostingSignupLink = getHostingLink('user-context-menu'); const hostingSignupLink = getHostingLink('user-context-menu');
@ -77,25 +80,33 @@ export class TopLeftMenu extends React.Component {
let homePageItem = null; let homePageItem = null;
if (this.hasHomePage()) { if (this.hasHomePage()) {
homePageItem = <li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage} tabIndex={0}> homePageItem = (
{_t("Home")} <AccessibleButton element="li" className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>
</li>; {_t("Home")}
</AccessibleButton>
);
} }
let signInOutItem; let signInOutItem;
if (isGuest) { if (isGuest) {
signInOutItem = <li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn} tabIndex={0}> signInOutItem = (
{_t("Sign in")} <AccessibleButton element="li" className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>
</li>; {_t("Sign in")}
</AccessibleButton>
);
} else { } else {
signInOutItem = <li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut} tabIndex={0}> signInOutItem = (
{_t("Sign out")} <AccessibleButton element="li" className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>
</li>; {_t("Sign out")}
</AccessibleButton>
);
} }
const settingsItem = <li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings} tabIndex={0}> const settingsItem = (
{_t("Settings")} <AccessibleButton element="li" className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
</li>; {_t("Settings")}
</AccessibleButton>
);
return <div className="mx_TopLeftMenu mx_HiddenFocusable" tabIndex={0} ref={this.props.containerRef}> return <div className="mx_TopLeftMenu mx_HiddenFocusable" tabIndex={0} ref={this.props.containerRef}>
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true}> <div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true}>

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from "classnames"; import classNames from "classnames";
import {KeyCode} from "../../../Keyboard";
export default class ToggleSwitch extends React.Component { export default class ToggleSwitch extends React.Component {
static propTypes = { static propTypes = {
@ -44,10 +45,7 @@ export default class ToggleSwitch extends React.Component {
} }
} }
_onClick = (e) => { _toggle = () => {
e.stopPropagation();
e.preventDefault();
if (this.props.disabled) return; if (this.props.disabled) return;
const newState = !this.state.checked; const newState = !this.state.checked;
@ -57,6 +55,22 @@ export default class ToggleSwitch extends React.Component {
} }
}; };
_onClick = (e) => {
e.stopPropagation();
e.preventDefault();
this._toggle();
};
_onKeyDown = (e) => {
e.stopPropagation();
e.preventDefault();
if (e.keyCode === KeyCode.ENTER || e.keyCode === KeyCode.SPACE) {
this._toggle();
}
};
render() { render() {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const {checked, disabled, onChange, ...props} = this.props; const {checked, disabled, onChange, ...props} = this.props;
@ -71,6 +85,7 @@ export default class ToggleSwitch extends React.Component {
{...props} {...props}
className={classes} className={classes}
onClick={this._onClick} onClick={this._onClick}
onKeyDown={this._onKeyDown}
role="checkbox" role="checkbox"
aria-checked={this.state.checked} aria-checked={this.state.checked}
aria-disabled={disabled} aria-disabled={disabled}

View file

@ -140,6 +140,8 @@ export default class MessageActionBar extends React.PureComponent {
} }
render() { render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let reactButton; let reactButton;
let replyButton; let replyButton;
let editButton; let editButton;
@ -149,14 +151,16 @@ export default class MessageActionBar extends React.PureComponent {
reactButton = this.renderReactButton(); reactButton = this.renderReactButton();
} }
if (this.context.room.canReply) { if (this.context.room.canReply) {
replyButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" replyButton = <AccessibleButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
title={_t("Reply")} title={_t("Reply")}
onClick={this.onReplyClick} onClick={this.onReplyClick}
/>; />;
} }
} }
if (canEditContent(this.props.mxEvent)) { if (canEditContent(this.props.mxEvent)) {
editButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton" editButton = <AccessibleButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
title={_t("Edit")} title={_t("Edit")}
onClick={this.onEditClick} onClick={this.onEditClick}
/>; />;
@ -166,9 +170,11 @@ export default class MessageActionBar extends React.PureComponent {
{reactButton} {reactButton}
{replyButton} {replyButton}
{editButton} {editButton}
<span className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton" <AccessibleButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
title={_t("Options")} title={_t("Options")}
onClick={this.onOptionsClick} onClick={this.onOptionsClick}
aria-haspopup={true}
/> />
</div>; </div>;
} }

View file

@ -3367,6 +3367,11 @@ focus-trap@^2.0.1:
dependencies: dependencies:
tabbable "^1.0.3" tabbable "^1.0.3"
focus-visible@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.0.2.tgz#4fae9cf40458b73c10701c9774c462e3ccd53caf"
integrity sha512-zT2fj/bmOgEBjqGbURGlowTmCwsIs3bRDMr/sFZz8Ly7VkEiwuCn9swNTL3pPuf8Oua2de7CLuKdnuNajWdDsQ==
follow-redirects@^1.0.0: follow-redirects@^1.0.0:
version "1.7.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76"