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:
parent
437a440bdf
commit
5ccbcf02e2
8 changed files with 60 additions and 65 deletions
|
@ -78,6 +78,7 @@
|
||||||
"react": "^15.4.0",
|
"react": "^15.4.0",
|
||||||
"react-addons-css-transition-group": "15.3.2",
|
"react-addons-css-transition-group": "15.3.2",
|
||||||
"react-dom": "^15.4.0",
|
"react-dom": "^15.4.0",
|
||||||
|
"react-focus-trap": "^2.5.0",
|
||||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
||||||
"sanitize-html": "^1.14.1",
|
"sanitize-html": "^1.14.1",
|
||||||
"text-encoding-utf-8": "^1.0.1",
|
"text-encoding-utf-8": "^1.0.1",
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
|
import FocusTrap from 'react-focus-trap';
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
|
|
||||||
|
@ -164,6 +165,7 @@ class ModalManager {
|
||||||
);
|
);
|
||||||
modal.onFinished = props ? props.onFinished : null;
|
modal.onFinished = props ? props.onFinished : null;
|
||||||
modal.className = className;
|
modal.className = className;
|
||||||
|
modal.closeDialog = closeDialog;
|
||||||
|
|
||||||
this._modals.unshift(modal);
|
this._modals.unshift(modal);
|
||||||
|
|
||||||
|
@ -194,9 +196,9 @@ class ModalManager {
|
||||||
const modal = this._modals[0];
|
const modal = this._modals[0];
|
||||||
const dialog = (
|
const dialog = (
|
||||||
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
|
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
|
||||||
<div className="mx_Dialog">
|
<FocusTrap className="mx_Dialog" onExit={modal.closeDialog}>
|
||||||
{ modal.elem }
|
{ modal.elem }
|
||||||
</div>
|
</FocusTrap>
|
||||||
<div className="mx_Dialog_background" onClick={this.closeAll}></div>
|
<div className="mx_Dialog_background" onClick={this.closeAll}></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -33,9 +33,6 @@ export default React.createClass({
|
||||||
// onFinished callback to call when Escape is pressed
|
// onFinished callback to call when Escape is pressed
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
|
||||||
// callback to call when Enter is pressed
|
|
||||||
onEnterPressed: React.PropTypes.func,
|
|
||||||
|
|
||||||
// CSS class to apply to dialog div
|
// CSS class to apply to dialog div
|
||||||
className: React.PropTypes.string,
|
className: React.PropTypes.string,
|
||||||
|
|
||||||
|
@ -51,17 +48,16 @@ export default React.createClass({
|
||||||
contentId: React.PropTypes.string
|
contentId: React.PropTypes.string
|
||||||
},
|
},
|
||||||
|
|
||||||
_onKeyDown: function(e) {
|
componentDidMount: function() {
|
||||||
if (e.keyCode === KeyCode.ESCAPE) {
|
this.applicationNode = document.getElementById('matrixchat');
|
||||||
e.stopPropagation();
|
if (this.applicationNode) {
|
||||||
e.preventDefault();
|
this.applicationNode.setAttribute('aria-hidden', 'true');
|
||||||
this.props.onFinished();
|
|
||||||
} else if (e.keyCode === KeyCode.ENTER) {
|
|
||||||
if (this.props.onEnterPressed) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onEnterPressed(e);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
return (
|
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}
|
<AccessibleButton onClick={this._onCancelClick}
|
||||||
className="mx_Dialog_cancelButton"
|
className="mx_Dialog_cancelButton"
|
||||||
>
|
>
|
||||||
|
|
|
@ -116,10 +116,10 @@ export default React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this.onOk}
|
|
||||||
title={this.props.title}
|
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">
|
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||||
{ avatar }
|
{ avatar }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -116,7 +116,6 @@ export default React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this._onFormSubmit}
|
|
||||||
title={_t('Create Community')}
|
title={_t('Create Community')}
|
||||||
>
|
>
|
||||||
<form onSubmit={this._onFormSubmit}>
|
<form onSubmit={this._onFormSubmit}>
|
||||||
|
|
|
@ -43,9 +43,9 @@ export default React.createClass({
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this.onOk}
|
|
||||||
title={_t('Create Room')}
|
title={_t('Create Room')}
|
||||||
>
|
>
|
||||||
|
<form onSubmit={this.onOk}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_CreateRoomDialog_label">
|
<div className="mx_CreateRoomDialog_label">
|
||||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||||
|
@ -71,10 +71,9 @@ export default React.createClass({
|
||||||
<button onClick={this.onCancel}>
|
<button onClick={this.onCancel}>
|
||||||
{ _t('Cancel') }
|
{ _t('Cancel') }
|
||||||
</button>
|
</button>
|
||||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
<input type="submit" className="mx_Dialog_primary" value={ _t('Create Room') }/>
|
||||||
{ _t('Create Room') }
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -64,7 +64,6 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this.onOk}
|
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
|
|
|
@ -60,9 +60,9 @@ export default React.createClass({
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this.onOk}
|
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
>
|
>
|
||||||
|
<form onSubmit={this.onOk}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_TextInputDialog_label">
|
<div className="mx_TextInputDialog_label">
|
||||||
<label htmlFor="textinput"> { this.props.description } </label>
|
<label htmlFor="textinput"> { this.props.description } </label>
|
||||||
|
@ -75,10 +75,9 @@ export default React.createClass({
|
||||||
<button onClick={this.onCancel}>
|
<button onClick={this.onCancel}>
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
<input type="submit" className="mx_Dialog_primary" value={ this.props.button }/>
|
||||||
{ this.props.button }
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue