diff --git a/package.json b/package.json index bb8db64d28..b32e5f0501 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "file-saver": "^1.3.3", "filesize": "3.5.6", "flux": "2.1.1", + "focus-trap-react": "^3.0.5", "fuse.js": "^2.2.0", "glob": "^5.0.14", "highlight.js": "^8.9.1", diff --git a/src/Modal.js b/src/Modal.js index c9f08772e7..2565d5c73b 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -22,6 +22,7 @@ const ReactDOM = require('react-dom'); import PropTypes from 'prop-types'; import Analytics from './Analytics'; import sdk from './index'; +import dis from './dispatcher'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; @@ -188,10 +189,22 @@ class ModalManager { _reRender() { if (this._modals.length == 0) { + // If there is no modal to render, make all of Riot available + // to screen reader users again + dis.dispatch({ + action: 'aria_unhide_main_app', + }); ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); return; } + // Hide the content outside the modal to screen reader users + // so they won't be able to navigate into it and act on it using + // screen reader specific features + dis.dispatch({ + action: 'aria_hide_main_app', + }); + const modal = this._modals[0]; const dialog = (
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index f6bbfd247b..d9ac9de693 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -374,7 +374,7 @@ const LoggedInView = React.createClass({ } return ( -
+
{ topBar }
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4bfe5d0b34..1312abda09 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -171,6 +171,10 @@ export default React.createClass({ register_hs_url: null, register_is_url: null, register_id_sid: null, + + // When showing Modal dialogs we need to set aria-hidden on the root app element + // and disable it when there are no dialogs + hideToSRUsers: false, }; return s; }, @@ -608,6 +612,16 @@ export default React.createClass({ case 'send_event': this.onSendEvent(payload.room_id, payload.event); break; + case 'aria_hide_main_app': + this.setState({ + hideToSRUsers: true, + }); + break; + case 'aria_unhide_main_app': + this.setState({ + hideToSRUsers: false, + }); + break; } }, diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 66e5fcb0c0..21a2477c37 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import FocusTrap from 'focus-trap-react'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; @@ -37,9 +38,6 @@ export default React.createClass({ // onFinished callback to call when Escape is pressed onFinished: PropTypes.func.isRequired, - // callback to call when Enter is pressed - onEnterPressed: PropTypes.func, - // called when a key is pressed onKeyDown: PropTypes.func, @@ -52,6 +50,10 @@ export default React.createClass({ // children should be the content of the dialog children: PropTypes.node, + + // Id of content element + // If provided, this is used to add a aria-describedby attribute + contentId: React.PropTypes.string, }, childContextTypes: { @@ -76,12 +78,6 @@ export default React.createClass({ e.stopPropagation(); e.preventDefault(); this.props.onFinished(); - } else if (e.keyCode === KeyCode.ENTER) { - if (this.props.onEnterPressed) { - e.stopPropagation(); - e.preventDefault(); - this.props.onEnterPressed(e); - } } }, @@ -93,17 +89,28 @@ export default React.createClass({ const TintableSvg = sdk.getComponent("elements.TintableSvg"); return ( -
+ -
+
{ this.props.title }
{ this.props.children } -
+
); }, }); diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 0a909d9906..e2387064cf 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -129,7 +129,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
{ _t("Start new chat") }
; - content =
+ content =
{ _t('You already have existing direct chats with this user:') }
{ this.state.tiles } @@ -147,7 +147,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { if (this.state.busyProfile) { profile = ; } else if (this.state.profileError) { - profile =
+ profile =
Unable to load profile information for { this.props.userId }
; } else { @@ -163,14 +163,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
; } content =
-
+

{ _t('Click on the button below to start chatting!') }

{ profile }
+ onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
; } @@ -179,6 +179,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { { content } diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index f347261470..b65d98d78d 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -114,10 +114,10 @@ export default React.createClass({ return ( -
+
{ avatar }
diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 86a2b2498c..04f99a0e15 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -112,7 +112,7 @@ export default React.createClass({ // XXX: We should catch errcodes and give sensible i18ned messages for them, // rather than displaying what the server gives us, but synapse doesn't give // any yet. - createErrorNode =
+ createErrorNode =
{ _t('Something went wrong whilst creating your community') }
{ this.state.createError.message }
; @@ -120,7 +120,6 @@ export default React.createClass({ return (
diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index d9287d23da..51693a19c9 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -45,30 +45,31 @@ export default React.createClass({ const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( -
-
- -
-
- -
-
- -
- { _t('Advanced options') } -
- - + +
+
+
-
-
+
+ +
+
+ +
+ { _t('Advanced options') } +
+ + +
+
+
+ diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index 2af2d6214f..a055f07629 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -52,22 +52,18 @@ export default React.createClass({ }; }, - componentDidMount: function() { - if (this.props.focus) { - this.refs.button.focus(); - } - }, - render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( -
+ title={this.props.title || _t('Error')} + contentId='mx_Dialog_content' + > +
{ this.props.description || _t('An error has occurred.') }
-
diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index a47702305c..b682156072 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -73,11 +73,12 @@ export default React.createClass({ let content; if (this.state.authError) { content = ( -
-
{ this.state.authError.message || this.state.authError.toString() }
+
+
{ this.state.authError.message || this.state.authError.toString() }

{ _t("Dismiss") } @@ -85,7 +86,7 @@ export default React.createClass({ ); } else { content = ( -
+
{ content } diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index 00bcc942a1..b9b64a69d2 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -126,11 +126,11 @@ export default React.createClass({ text = _t(text, {displayName: displayName}); return ( -
+

{ text }

-