Improve a11y:
+ Close context menu on escape + Use AccessibleButtons for more things (Context Menus and TabbedView) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>pull/21833/head
parent
8ec0ffea3a
commit
c37e27f03d
|
@ -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 <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}>
|
||||
{ chevron }
|
||||
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
|
||||
|
|
|
@ -17,9 +17,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import {_t} from '../../languageHandler';
|
||||
import {KeyCode} from "../../Keyboard";
|
||||
import {_t} from '../../languageHandler';``
|
||||
import PropTypes from "prop-types";
|
||||
import sdk from "../../index";
|
||||
|
||||
/**
|
||||
* Represents a tab for the TabbedView.
|
||||
|
@ -72,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);
|
||||
|
@ -83,30 +85,15 @@ export class TabbedView extends React.Component {
|
|||
}
|
||||
|
||||
const onClickHandler = () => this._setActiveTab(tab);
|
||||
const onKeyDownHandler = (e) => {
|
||||
if (e.keyCode === KeyCode.ENTER || e.keyCode === KeyCode.SPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this._setActiveTab(tab);
|
||||
}
|
||||
};
|
||||
|
||||
const label = _t(tab.label);
|
||||
return (
|
||||
<span
|
||||
className={classes}
|
||||
key={"tab_label_" + tab.label}
|
||||
onClick={onClickHandler}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
role="button"
|
||||
aria-label={label}
|
||||
tabIndex={0}
|
||||
>
|
||||
<AccessibleButton className={classes} key={"tab_label_" + tab.label} onClick={onClickHandler}>
|
||||
{tabIcon}
|
||||
<span className="mx_TabbedView_tabLabel_text">
|
||||
{ label }
|
||||
</span>
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -109,10 +109,11 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
return (
|
||||
<AccessibleButton
|
||||
className="mx_TopLeftMenuButton"
|
||||
role="button"
|
||||
onClick={this.onToggleMenu}
|
||||
inputRef={(r) => this._buttonRef = r}
|
||||
aria-label={_t("Your profile")}
|
||||
aria-haspopup={true}
|
||||
aria-expanded={this.state.menuDisplayed}
|
||||
>
|
||||
<BaseAvatar
|
||||
idName={MatrixClientPeg.get().getUserId()}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 Vector Creations 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.
|
||||
|
@ -77,11 +78,12 @@ export default class GroupInviteTileContextMenu extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
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" />
|
||||
{ _t('Reject') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,28 +31,11 @@ import Resend from '../../../Resend';
|
|||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import {KeyCode} from "../../../Keyboard";
|
||||
|
||||
function canCancel(eventStatus) {
|
||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||
}
|
||||
|
||||
const DropdownButton = ({children, onClick}) => {
|
||||
const onKeyDown = (e) => {
|
||||
if (e.keyCode === KeyCode.ENTER || e.keyCode === KeyCode.SPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="mx_MessageContextMenu_field" onClick={onClick} onKeyDown={onKeyDown} role="button" tabIndex={0}>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
module.exports = createReactClass({
|
||||
displayName: 'MessageContextMenu',
|
||||
|
||||
|
@ -306,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;
|
||||
|
@ -337,89 +322,89 @@ module.exports = createReactClass({
|
|||
if (!mxEvent.isRedacted()) {
|
||||
if (eventStatus === EventStatus.NOT_SENT) {
|
||||
resendButton = (
|
||||
<DropdownButton onClick={this.onResendClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
||||
{ _t('Resend') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (editStatus === EventStatus.NOT_SENT) {
|
||||
resendEditButton = (
|
||||
<DropdownButton onClick={this.onResendEditClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendEditClick}>
|
||||
{ _t('Resend edit') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (unsentReactionsCount !== 0) {
|
||||
resendReactionsButton = (
|
||||
<DropdownButton onClick={this.onResendReactionsClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
|
||||
{ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (redactStatus === EventStatus.NOT_SENT) {
|
||||
resendRedactionButton = (
|
||||
<DropdownButton onClick={this.onResendRedactionClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendRedactionClick}>
|
||||
{ _t('Resend removal') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<DropdownButton onClick={this.onRedactClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (allowCancel) {
|
||||
cancelButton = (
|
||||
<DropdownButton onClick={this.onCancelSendClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
||||
{ _t('Cancel Sending') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (isContentActionable(mxEvent)) {
|
||||
forwardButton = (
|
||||
<DropdownButton onClick={this.onForwardClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||
{ _t('Forward Message') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
<DropdownButton onClick={this.onPinClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
||||
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const viewSourceButton = (
|
||||
<DropdownButton onClick={this.onViewSourceClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
|
||||
{ _t('View Source') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
if (mxEvent.getType() !== mxEvent.getWireType()) {
|
||||
viewClearSourceButton = (
|
||||
<DropdownButton onClick={this.onViewClearSourceClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
|
||||
{ _t('View Decrypted Source') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.eventTileOps) {
|
||||
if (this.props.eventTileOps.isWidgetHidden()) {
|
||||
unhidePreviewButton = (
|
||||
<DropdownButton onClick={this.onUnhidePreviewClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
|
||||
{ _t('Unhide Preview') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -430,19 +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 = (
|
||||
<DropdownButton>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field">
|
||||
<a href={permalink} target="_blank" rel="noopener" onClick={this.onPermalinkClick} tabIndex={-1}>
|
||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||
? _t('Share Permalink') : _t('Share Message') }
|
||||
</a>
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) {
|
||||
quoteButton = (
|
||||
<DropdownButton onClick={this.onQuoteClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
||||
{ _t('Quote') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -452,7 +437,7 @@ module.exports = createReactClass({
|
|||
isUrlPermitted(mxEvent.event.content.external_url)
|
||||
) {
|
||||
externalURLButton = (
|
||||
<DropdownButton>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field">
|
||||
<a
|
||||
href={mxEvent.event.content.external_url}
|
||||
target="_blank"
|
||||
|
@ -462,33 +447,33 @@ module.exports = createReactClass({
|
|||
>
|
||||
{ _t('Source URL') }
|
||||
</a>
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.collapseReplyThread) {
|
||||
collapseReplyThread = (
|
||||
<DropdownButton onClick={this.onCollapseReplyThreadClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
|
||||
{ _t('Collapse Reply Thread') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let e2eInfo;
|
||||
if (this.props.e2eInfoCallback) {
|
||||
e2eInfo = (
|
||||
<DropdownButton onClick={this.e2eInfoClicked}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
|
||||
{ _t('End-to-end encryption information') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let reportEventButton;
|
||||
if (mxEvent.getSender() !== me) {
|
||||
reportEventButton = (
|
||||
<DropdownButton onClick={this.onReportEventClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
|
||||
{ _t('Report Content') }
|
||||
</DropdownButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<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" />
|
||||
</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_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off-copy.svg")} width="16" height="12" />
|
||||
{ _t('All messages (noisy)') }
|
||||
</div>
|
||||
<div className={allNotifsClasses} onClick={this._onClickAllNotifs} >
|
||||
</AccessibleButton>
|
||||
<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_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off.svg")} width="16" height="12" />
|
||||
{ _t('All messages') }
|
||||
</div>
|
||||
<div className={mentionsClasses} onClick={this._onClickMentions} >
|
||||
</AccessibleButton>
|
||||
<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_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-mentions.svg")} width="16" height="12" />
|
||||
{ _t('Mentions only') }
|
||||
</div>
|
||||
<div className={muteNotifsClasses} onClick={this._onClickMute} >
|
||||
</AccessibleButton>
|
||||
<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_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute.svg")} width="16" height="12" />
|
||||
{ _t('Mute') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -287,12 +290,13 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_renderSettingsMenu: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<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" />
|
||||
{ _t('Settings') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -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 (
|
||||
<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" />
|
||||
{ leaveText }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_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 (
|
||||
<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_set" src={require("../../../../res/img/icon_context_fave_on.svg")} width="15" height="15" />
|
||||
{ _t('Favourite') }
|
||||
</div>
|
||||
<div className={lowPriorityClasses} onClick={this._onClickLowPriority} >
|
||||
</AccessibleButton>
|
||||
<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_set" src={require("../../../../res/img/icon_context_low_on.svg")} width="15" height="15" />
|
||||
{ _t('Low Priority') }
|
||||
</div>
|
||||
<div className={dmClasses} onClick={this._onClickDM} >
|
||||
</AccessibleButton>
|
||||
<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_set" src={require("../../../../res/img/icon_context_person_on.svg")} width="15" height="15" />
|
||||
{ _t('Direct Chat') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue