diff --git a/package.json b/package.json index 1da0a90b01..d2e7786bed 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "filesize": "3.5.6", "flux": "2.1.1", "focus-trap-react": "^3.0.5", + "focus-visible": "^5.0.2", "fuse.js": "^2.2.0", "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", "gfm.css": "^1.1.1", diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 0c081ec0d5..f3afef284f 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -18,7 +18,7 @@ limitations under the License. cursor: pointer; } -.mx_AccessibleButton:focus { +.mx_AccessibleButton:focus:not(.focus-visible) { outline: 0; } diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index 0f4d0b38d4..04314e5a4e 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -21,6 +21,7 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {focusCapturedRef} from "../../utils/Accessibility"; +import {KeyCode} from "../../Keyboard"; // Shamelessly ripped off Modal.js. There's probably a better way // 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 windowResize: PropTypes.func, // method to close menu - closeMenu: PropTypes.func, + closeMenu: PropTypes.func.isRequired, }; 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() { const position = {}; let chevronFace = null; @@ -210,7 +219,7 @@ export default class ContextualMenu extends React.Component { // FIXME: If a menu uses getDefaultProps it clobbers the onFinished // property set here so you can't close the menu from a button click! - return
+ return
{ chevron } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a185664038..6a03941bd9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -24,6 +24,9 @@ import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; 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 { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import MatrixClientPeg from "../../MatrixClientPeg"; @@ -41,7 +44,7 @@ import * as Rooms from '../../Rooms'; import linkifyMatrix from "../../linkify-matrix"; import * as Lifecycle from '../../Lifecycle'; // LifecycleStore is not used but does listen to and dispatch actions -require('../../stores/LifecycleStore'); +import '../../stores/LifecycleStore'; import PageTypes from '../../PageTypes'; import { getHomePageUrl } from '../../utils/pages'; diff --git a/src/components/structures/TabbedView.js b/src/components/structures/TabbedView.js index 6ececbb329..01c68fad62 100644 --- a/src/components/structures/TabbedView.js +++ b/src/components/structures/TabbedView.js @@ -1,6 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2019 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. @@ -18,6 +19,7 @@ limitations under the License. import * as React from "react"; import {_t} from '../../languageHandler'; import PropTypes from "prop-types"; +import sdk from "../../index"; /** * Represents a tab for the TabbedView. @@ -70,6 +72,8 @@ export class TabbedView extends React.Component { } _renderTabLabel(tab) { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + let classes = "mx_TabbedView_tabLabel "; const idx = this.props.tabs.indexOf(tab); @@ -84,19 +88,12 @@ export class TabbedView extends React.Component { const label = _t(tab.label); return ( - + {tabIcon} { label } - + ); } diff --git a/src/components/structures/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js index cf3dda077c..42b8623e56 100644 --- a/src/components/structures/TopLeftMenuButton.js +++ b/src/components/structures/TopLeftMenuButton.js @@ -109,10 +109,11 @@ export default class TopLeftMenuButton extends React.Component { return ( this._buttonRef = r} aria-label={_t("Your profile")} + aria-haspopup={true} + aria-expanded={this.state.menuDisplayed} > -
+ { _t('Reject') } -
+
; } } diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 66005eb730..a832b2fbb2 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd 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"); you may not use this file except in compliance with the License. @@ -288,6 +289,8 @@ module.exports = createReactClass({ }, render: function() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const cli = MatrixClientPeg.get(); const me = cli.getUserId(); const mxEvent = this.props.mxEvent; @@ -319,89 +322,89 @@ module.exports = createReactClass({ if (!mxEvent.isRedacted()) { if (eventStatus === EventStatus.NOT_SENT) { resendButton = ( -
+ { _t('Resend') } -
+ ); } if (editStatus === EventStatus.NOT_SENT) { resendEditButton = ( -
+ { _t('Resend edit') } -
+ ); } if (unsentReactionsCount !== 0) { resendReactionsButton = ( -
+ { _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) } -
+ ); } } if (redactStatus === EventStatus.NOT_SENT) { resendRedactionButton = ( -
+ { _t('Resend removal') } -
+ ); } if (isSent && this.state.canRedact) { redactButton = ( -
+ { _t('Remove') } -
+ ); } if (allowCancel) { cancelButton = ( -
+ { _t('Cancel Sending') } -
+ ); } if (isContentActionable(mxEvent)) { forwardButton = ( -
+ { _t('Forward Message') } -
+ ); if (this.state.canPin) { pinButton = ( -
+ { this._isPinned() ? _t('Unpin Message') : _t('Pin Message') } -
+ ); } } const viewSourceButton = ( -
+ { _t('View Source') } -
+ ); if (mxEvent.getType() !== mxEvent.getWireType()) { viewClearSourceButton = ( -
+ { _t('View Decrypted Source') } -
+ ); } if (this.props.eventTileOps) { if (this.props.eventTileOps.isWidgetHidden()) { unhidePreviewButton = ( -
+ { _t('Unhide Preview') } -
+ ); } } @@ -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) const permalinkButton = ( -
- + + { mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message' ? _t('Share Permalink') : _t('Share Message') } -
+ ); if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) { quoteButton = ( -
+ { _t('Quote') } -
+ ); } @@ -435,34 +437,43 @@ module.exports = createReactClass({ isUrlPermitted(mxEvent.event.content.external_url) ) { externalURLButton = ( -
- { _t('Source URL') } -
+ + + { _t('Source URL') } + + ); } if (this.props.collapseReplyThread) { collapseReplyThread = ( -
+ { _t('Collapse Reply Thread') } -
+ ); } let e2eInfo; if (this.props.e2eInfoCallback) { - e2eInfo =
+ e2eInfo = ( + { _t('End-to-end encryption information') } -
; + + ); } let reportEventButton; if (mxEvent.getSender() !== me) { reportEventButton = ( -
+ { _t('Report Content') } -
+ ); } diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index eba8254c03..9bb573026f 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd 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"); you may not use this file except in compliance with the License. @@ -227,6 +228,8 @@ module.exports = createReactClass({ }, _renderNotifMenu: function() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const alertMeClasses = classNames({ 'mx_RoomTileContextMenu_notif_field': true, 'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD, @@ -249,29 +252,29 @@ module.exports = createReactClass({ return (
-
+
-
+ { _t('All messages (noisy)') } -
-
+ + { _t('All messages') } -
-
+ + { _t('Mentions only') } -
-
+ + { _t('Mute') } -
+
); }, @@ -287,12 +290,13 @@ module.exports = createReactClass({ }, _renderSettingsMenu: function() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return (
-
+ { _t('Settings') } -
+
); }, @@ -302,6 +306,8 @@ module.exports = createReactClass({ return null; } + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + let leaveClickHandler = null; let leaveText = null; @@ -323,15 +329,17 @@ module.exports = createReactClass({ return (
-
+ { leaveText } -
+
); }, _renderRoomTagMenu: function() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const favouriteClasses = classNames({ 'mx_RoomTileContextMenu_tag_field': true, 'mx_RoomTileContextMenu_tag_fieldSet': this.state.isFavourite, @@ -352,21 +360,21 @@ module.exports = createReactClass({ return (
-
+ { _t('Favourite') } -
-
+ + { _t('Low Priority') } -
-
+ + { _t('Direct Chat') } -
+
); }, diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js index 09e9142201..4080ebf5bf 100644 --- a/src/components/views/context_menus/TopLeftMenu.js +++ b/src/components/views/context_menus/TopLeftMenu.js @@ -24,6 +24,7 @@ import Modal from "../../../Modal"; import SdkConfig from '../../../SdkConfig'; import { getHostingLink } from '../../../utils/HostingLink'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from "../../../index"; export class TopLeftMenu extends React.Component { static propTypes = { @@ -57,6 +58,8 @@ export class TopLeftMenu extends React.Component { } render() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const isGuest = MatrixClientPeg.get().isGuest(); const hostingSignupLink = getHostingLink('user-context-menu'); @@ -77,25 +80,33 @@ export class TopLeftMenu extends React.Component { let homePageItem = null; if (this.hasHomePage()) { - homePageItem =
  • - {_t("Home")} -
  • ; + homePageItem = ( + + {_t("Home")} + + ); } let signInOutItem; if (isGuest) { - signInOutItem =
  • - {_t("Sign in")} -
  • ; + signInOutItem = ( + + {_t("Sign in")} + + ); } else { - signInOutItem =
  • - {_t("Sign out")} -
  • ; + signInOutItem = ( + + {_t("Sign out")} + + ); } - const settingsItem =
  • - {_t("Settings")} -
  • ; + const settingsItem = ( + + {_t("Settings")} + + ); return
    diff --git a/src/components/views/elements/ToggleSwitch.js b/src/components/views/elements/ToggleSwitch.js index e8e870edd8..05986e4e99 100644 --- a/src/components/views/elements/ToggleSwitch.js +++ b/src/components/views/elements/ToggleSwitch.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import classNames from "classnames"; +import {KeyCode} from "../../../Keyboard"; export default class ToggleSwitch extends React.Component { static propTypes = { @@ -44,10 +45,7 @@ export default class ToggleSwitch extends React.Component { } } - _onClick = (e) => { - e.stopPropagation(); - e.preventDefault(); - + _toggle = () => { if (this.props.disabled) return; 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() { // eslint-disable-next-line no-unused-vars const {checked, disabled, onChange, ...props} = this.props; @@ -71,6 +85,7 @@ export default class ToggleSwitch extends React.Component { {...props} className={classes} onClick={this._onClick} + onKeyDown={this._onKeyDown} role="checkbox" aria-checked={this.state.checked} aria-disabled={disabled} diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 8db268076c..2b43c5fe2a 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -140,6 +140,8 @@ export default class MessageActionBar extends React.PureComponent { } render() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + let reactButton; let replyButton; let editButton; @@ -149,14 +151,16 @@ export default class MessageActionBar extends React.PureComponent { reactButton = this.renderReactButton(); } if (this.context.room.canReply) { - replyButton = ; } } if (canEditContent(this.props.mxEvent)) { - editButton = ; @@ -166,9 +170,11 @@ export default class MessageActionBar extends React.PureComponent { {reactButton} {replyButton} {editButton} -
    ; } diff --git a/yarn.lock b/yarn.lock index 9e50f38082..4985cd0700 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3367,6 +3367,11 @@ focus-trap@^2.0.1: dependencies: 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: version "1.7.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76"