Several changes improving accessibility of the dialogs

- Wrapped all the modals inside a react-focus-trap component disabling
keyboard navigation outside the modal dialogs
- Disabled our custom key handling at dialog level. Cancelling on esc
key is now handled via FocusTrap component.
- Removed onEnter prop from the BaseDialog component. Dialogs that
submit data all now embed a form with onSubmit handler. And since
keyboard focus is now managed better via FocusTrap it no longer makes
sense for the other dialog types. Fixes
https://github.com/vector-im/riot-web/issues/5736
- Set aria-hidden on the matrixChat outer node when showing dialogs to
disable navigating outside the modals by using screen reader specific
features.
This commit is contained in:
Peter Vágner 2017-12-03 21:38:21 +01:00
parent 437a440bdf
commit 5ccbcf02e2
8 changed files with 60 additions and 65 deletions

View file

@ -78,6 +78,7 @@
"react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2",
"react-dom": "^15.4.0",
"react-focus-trap": "^2.5.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.14.1",
"text-encoding-utf-8": "^1.0.1",

View file

@ -19,6 +19,7 @@ limitations under the License.
const React = require('react');
const ReactDOM = require('react-dom');
import FocusTrap from 'react-focus-trap';
import Analytics from './Analytics';
import sdk from './index';
@ -164,6 +165,7 @@ class ModalManager {
);
modal.onFinished = props ? props.onFinished : null;
modal.className = className;
modal.closeDialog = closeDialog;
this._modals.unshift(modal);
@ -194,9 +196,9 @@ class ModalManager {
const modal = this._modals[0];
const dialog = (
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
<div className="mx_Dialog">
<FocusTrap className="mx_Dialog" onExit={modal.closeDialog}>
{ modal.elem }
</div>
</FocusTrap>
<div className="mx_Dialog_background" onClick={this.closeAll}></div>
</div>
);

View file

@ -33,9 +33,6 @@ export default React.createClass({
// onFinished callback to call when Escape is pressed
onFinished: React.PropTypes.func.isRequired,
// callback to call when Enter is pressed
onEnterPressed: React.PropTypes.func,
// CSS class to apply to dialog div
className: React.PropTypes.string,
@ -51,17 +48,16 @@ export default React.createClass({
contentId: React.PropTypes.string
},
_onKeyDown: function(e) {
if (e.keyCode === KeyCode.ESCAPE) {
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);
}
componentDidMount: function() {
this.applicationNode = document.getElementById('matrixchat');
if (this.applicationNode) {
this.applicationNode.setAttribute('aria-hidden', 'true');
}
},
componentWillUnmount: function() {
if (this.applicationNode) {
this.applicationNode.setAttribute('aria-hidden', 'false');
}
},
@ -73,7 +69,7 @@ export default React.createClass({
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return (
<div onKeyDown={this._onKeyDown} className={this.props.className} role="dialog" aria-labelledby='mx_BaseDialog_title' aria-describedby={this.props.contentId}>
<div className={this.props.className} role="dialog" aria-labelledby='mx_BaseDialog_title' aria-describedby={this.props.contentId}>
<AccessibleButton onClick={this._onCancelClick}
className="mx_Dialog_cancelButton"
>

View file

@ -116,10 +116,10 @@ export default React.createClass({
return (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={this.props.title}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content">
<div id="mx_Dialog_content" className="mx_Dialog_content">
<div className="mx_ConfirmUserActionDialog_avatar">
{ avatar }
</div>

View file

@ -116,7 +116,6 @@ export default React.createClass({
return (
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
onEnterPressed={this._onFormSubmit}
title={_t('Create Community')}
>
<form onSubmit={this._onFormSubmit}>

View file

@ -43,38 +43,37 @@ export default React.createClass({
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={_t('Create Room')}
>
<div className="mx_Dialog_content">
<div className="mx_CreateRoomDialog_label">
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
</div>
<br />
<details className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
<div>
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
<label htmlFor="checkbox">
{ _t('Block users on other matrix homeservers from joining this room') }
<br />
({ _t('This setting cannot be changed later!') })
</label>
<form onSubmit={this.onOk}>
<div className="mx_Dialog_content">
<div className="mx_CreateRoomDialog_label">
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
</div>
</details>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
{ _t('Cancel') }
</button>
<button className="mx_Dialog_primary" onClick={this.onOk}>
{ _t('Create Room') }
</button>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
</div>
<br />
<details className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
<div>
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
<label htmlFor="checkbox">
{ _t('Block users on other matrix homeservers from joining this room') }
<br />
({ _t('This setting cannot be changed later!') })
</label>
</div>
</details>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
{ _t('Cancel') }
</button>
<input type="submit" className="mx_Dialog_primary" value={ _t('Create Room') }/>
</div>
</form>
</BaseDialog>
);
},

View file

@ -64,7 +64,6 @@ export default React.createClass({
});
return (
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={this.props.title}
contentId='mx_Dialog_content'
>

View file

@ -60,25 +60,24 @@ export default React.createClass({
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={this.props.title}
>
<div className="mx_Dialog_content">
<div className="mx_TextInputDialog_label">
<label htmlFor="textinput"> { this.props.description } </label>
<form onSubmit={this.onOk}>
<div className="mx_Dialog_content">
<div className="mx_TextInputDialog_label">
<label htmlFor="textinput"> { this.props.description } </label>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
</div>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
<input type="submit" className="mx_Dialog_primary" value={ this.props.button }/>
</div>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
<button className="mx_Dialog_primary" onClick={this.onOk}>
{ this.props.button }
</button>
</div>
</form>
</BaseDialog>
);
},