Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Weblate 2018-03-20 11:36:04 +00:00
commit fb0269f6f9
21 changed files with 205 additions and 94 deletions

View file

@ -65,6 +65,7 @@
"file-saver": "^1.3.3", "file-saver": "^1.3.3",
"filesize": "3.5.6", "filesize": "3.5.6",
"flux": "2.1.1", "flux": "2.1.1",
"focus-trap-react": "^3.0.5",
"fuse.js": "^2.2.0", "fuse.js": "^2.2.0",
"glob": "^5.0.14", "glob": "^5.0.14",
"highlight.js": "^8.9.1", "highlight.js": "^8.9.1",

View file

@ -22,6 +22,7 @@ const ReactDOM = require('react-dom');
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Analytics from './Analytics'; import Analytics from './Analytics';
import sdk from './index'; import sdk from './index';
import dis from './dispatcher';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
@ -188,10 +189,22 @@ class ModalManager {
_reRender() { _reRender() {
if (this._modals.length == 0) { 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()); ReactDOM.unmountComponentAtNode(this.getOrCreateContainer());
return; 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 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 : '')}>

View file

@ -374,7 +374,7 @@ const LoggedInView = React.createClass({
} }
return ( return (
<div className='mx_MatrixChat_wrapper'> <div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers}>
{ topBar } { topBar }
<DragDropContext onDragEnd={this._onDragEnd}> <DragDropContext onDragEnd={this._onDragEnd}>
<div className={bodyClasses}> <div className={bodyClasses}>

View file

@ -171,6 +171,10 @@ export default React.createClass({
register_hs_url: null, register_hs_url: null,
register_is_url: null, register_is_url: null,
register_id_sid: 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; return s;
}, },
@ -608,6 +612,16 @@ export default React.createClass({
case 'send_event': case 'send_event':
this.onSendEvent(payload.room_id, payload.event); this.onSendEvent(payload.room_id, payload.event);
break; break;
case 'aria_hide_main_app':
this.setState({
hideToSRUsers: true,
});
break;
case 'aria_unhide_main_app':
this.setState({
hideToSRUsers: false,
});
break;
} }
}, },

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import FocusTrap from 'focus-trap-react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk';
@ -37,9 +38,6 @@ export default React.createClass({
// onFinished callback to call when Escape is pressed // onFinished callback to call when Escape is pressed
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
// callback to call when Enter is pressed
onEnterPressed: PropTypes.func,
// called when a key is pressed // called when a key is pressed
onKeyDown: PropTypes.func, onKeyDown: PropTypes.func,
@ -52,6 +50,10 @@ export default React.createClass({
// children should be the content of the dialog // children should be the content of the dialog
children: PropTypes.node, children: PropTypes.node,
// Id of content element
// If provided, this is used to add a aria-describedby attribute
contentId: React.PropTypes.string,
}, },
childContextTypes: { childContextTypes: {
@ -76,12 +78,6 @@ export default React.createClass({
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.props.onFinished(); 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"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
return ( return (
<div onKeyDown={this._onKeyDown} className={this.props.className}> <FocusTrap onKeyDown={this._onKeyDown}
className={this.props.className}
role="dialog"
aria-labelledby='mx_BaseDialog_title'
// This should point to a node describing the dialog.
// If we were about to completelly follow this recommendation we'd need to
// make all the components relying on BaseDialog to be aware of it.
// So instead we will use the whole content as the description.
// Description comes first and if the content contains more text,
// AT users can skip its presentation.
aria-describedby={this.props.contentId}
>
<AccessibleButton onClick={this._onCancelClick} <AccessibleButton onClick={this._onCancelClick}
className="mx_Dialog_cancelButton" className="mx_Dialog_cancelButton"
> >
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" /> <TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</AccessibleButton> </AccessibleButton>
<div className={'mx_Dialog_title ' + this.props.titleClass}> <div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
{ this.props.title } { this.props.title }
</div> </div>
{ this.props.children } { this.props.children }
</div> </FocusTrap>
); );
}, },
}); });

View file

@ -129,7 +129,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
</div> </div>
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div> <div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
</AccessibleButton>; </AccessibleButton>;
content = <div className="mx_Dialog_content"> content = <div className="mx_Dialog_content" id='mx_Dialog_content'>
{ _t('You already have existing direct chats with this user:') } { _t('You already have existing direct chats with this user:') }
<div className="mx_ChatCreateOrReuseDialog_tiles"> <div className="mx_ChatCreateOrReuseDialog_tiles">
{ this.state.tiles } { this.state.tiles }
@ -147,7 +147,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
if (this.state.busyProfile) { if (this.state.busyProfile) {
profile = <Spinner />; profile = <Spinner />;
} else if (this.state.profileError) { } else if (this.state.profileError) {
profile = <div className="error"> profile = <div className="error" role="alert">
Unable to load profile information for { this.props.userId } Unable to load profile information for { this.props.userId }
</div>; </div>;
} else { } else {
@ -163,14 +163,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
</div>; </div>;
} }
content = <div> content = <div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content" id='mx_Dialog_content'>
<p> <p>
{ _t('Click on the button below to start chatting!') } { _t('Click on the button below to start chatting!') }
</p> </p>
{ profile } { profile }
</div> </div>
<DialogButtons primaryButton={_t('Start Chatting')} <DialogButtons primaryButton={_t('Start Chatting')}
onPrimaryButtonClick={this.props.onNewDMClick} /> onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
</div>; </div>;
} }
@ -179,6 +179,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
<BaseDialog className='mx_ChatCreateOrReuseDialog' <BaseDialog className='mx_ChatCreateOrReuseDialog'
onFinished={this.props.onFinished.bind(false)} onFinished={this.props.onFinished.bind(false)}
title={title} title={title}
contentId='mx_Dialog_content'
> >
{ content } { content }
</BaseDialog> </BaseDialog>

View file

@ -114,10 +114,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>

View file

@ -112,7 +112,7 @@ export default React.createClass({
// XXX: We should catch errcodes and give sensible i18ned messages for them, // 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 // rather than displaying what the server gives us, but synapse doesn't give
// any yet. // any yet.
createErrorNode = <div className="error"> createErrorNode = <div className="error" role="alert">
<div>{ _t('Something went wrong whilst creating your community') }</div> <div>{ _t('Something went wrong whilst creating your community') }</div>
<div>{ this.state.createError.message }</div> <div>{ this.state.createError.message }</div>
</div>; </div>;
@ -120,7 +120,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}>

View file

@ -45,30 +45,31 @@ export default React.createClass({
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
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')}
> >
<div className="mx_Dialog_content"> <form onSubmit={this.onOk}>
<div className="mx_CreateRoomDialog_label"> <div className="mx_Dialog_content">
<label htmlFor="textinput"> { _t('Room name (optional)') } </label> <div className="mx_CreateRoomDialog_label">
</div> <label htmlFor="textinput"> { _t('Room name (optional)') } </label>
<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> </div>
</details> <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>
</form>
<DialogButtons primaryButton={_t('Create Room')} <DialogButtons primaryButton={_t('Create Room')}
onPrimaryButtonClick={this.onOk} onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel} /> onCancel={this.onCancel} />

View file

@ -52,22 +52,18 @@ export default React.createClass({
}; };
}, },
componentDidMount: function() {
if (this.props.focus) {
this.refs.button.focus();
}
},
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={this.props.title || _t('Error')}> title={this.props.title || _t('Error')}
<div className="mx_Dialog_content"> contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ this.props.description || _t('An error has occurred.') } { this.props.description || _t('An error has occurred.') }
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}> <button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
{ this.props.button || _t('OK') } { this.props.button || _t('OK') }
</button> </button>
</div> </div>

View file

@ -73,11 +73,12 @@ export default React.createClass({
let content; let content;
if (this.state.authError) { if (this.state.authError) {
content = ( content = (
<div> <div id='mx_Dialog_content'>
<div>{ this.state.authError.message || this.state.authError.toString() }</div> <div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div>
<br /> <br />
<AccessibleButton onClick={this._onDismissClick} <AccessibleButton onClick={this._onDismissClick}
className="mx_UserSettings_button" className="mx_UserSettings_button"
autoFocus="true"
> >
{ _t("Dismiss") } { _t("Dismiss") }
</AccessibleButton> </AccessibleButton>
@ -85,7 +86,7 @@ export default React.createClass({
); );
} else { } else {
content = ( content = (
<div> <div id='mx_Dialog_content'>
<InteractiveAuth ref={this._collectInteractiveAuth} <InteractiveAuth ref={this._collectInteractiveAuth}
matrixClient={this.props.matrixClient} matrixClient={this.props.matrixClient}
authData={this.props.authData} authData={this.props.authData}
@ -100,6 +101,7 @@ export default React.createClass({
<BaseDialog className="mx_InteractiveAuthDialog" <BaseDialog className="mx_InteractiveAuthDialog"
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))} title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
contentId='mx_Dialog_content'
> >
{ content } { content }
</BaseDialog> </BaseDialog>

View file

@ -126,11 +126,11 @@ export default React.createClass({
text = _t(text, {displayName: displayName}); text = _t(text, {displayName: displayName});
return ( return (
<div> <div id='mx_Dialog_content'>
<p>{ text }</p> <p>{ text }</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked}> <button onClick={this._onVerifyClicked} autoFocus="true">
{ _t('Start verification') } { _t('Start verification') }
</button> </button>
<button onClick={this._onShareClicked}> <button onClick={this._onShareClicked}>
@ -154,7 +154,7 @@ export default React.createClass({
content = this._renderContent(); content = this._renderContent();
} else { } else {
content = ( content = (
<div> <div id='mx_Dialog_content'>
<p>{ _t('Loading device info...') }</p> <p>{ _t('Loading device info...') }</p>
<Spinner /> <Spinner />
</div> </div>
@ -165,6 +165,7 @@ export default React.createClass({
<BaseDialog className='mx_KeyShareRequestDialog' <BaseDialog className='mx_KeyShareRequestDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={_t('Encryption key request')} title={_t('Encryption key request')}
contentId='mx_Dialog_content'
> >
{ content } { content }
</BaseDialog> </BaseDialog>

View file

@ -60,10 +60,10 @@ 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'
> >
<div className="mx_Dialog_content"> <div className="mx_Dialog_content" id='mx_Dialog_content'>
{ this.props.description } { this.props.description }
</div> </div>
<DialogButtons primaryButton={this.props.button || _t('OK')} <DialogButtons primaryButton={this.props.button || _t('OK')}

View file

@ -30,6 +30,12 @@ export default React.createClass({
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
componentDidMount: function() {
if (this.refs.bugreportLink) {
this.refs.bugreportLink.focus();
}
},
_sendBugReport: function() { _sendBugReport: function() {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {}); Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
@ -50,16 +56,20 @@ export default React.createClass({
{ _t( { _t(
"Otherwise, <a>click here</a> to send a bug report.", "Otherwise, <a>click here</a> to send a bug report.",
{}, {},
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> }, { 'a': (sub) => <a ref="bugreportLink" onClick={this._sendBugReport}
key="bugreport" href='#'>{ sub }</a> },
) } ) }
</p> </p>
); );
} }
const shouldFocusContinueButton =!(bugreport==true);
return ( return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={_t('Unable to restore session')}> title={_t('Unable to restore session')}
<div className="mx_Dialog_content"> contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>{ _t("We encountered an error trying to restore your previous session. If " + <p>{ _t("We encountered an error trying to restore your previous session. If " +
"you continue, you will need to log in again, and encrypted chat " + "you continue, you will need to log in again, and encrypted chat " +
"history will be unreadable.") }</p> "history will be unreadable.") }</p>
@ -71,7 +81,7 @@ export default React.createClass({
{ bugreport } { bugreport }
</div> </div>
<DialogButtons primaryButton={_t("Continue anyway")} <DialogButtons primaryButton={_t("Continue anyway")}
onPrimaryButtonClick={this._continueClicked} onPrimaryButtonClick={this._continueClicked} focus={shouldFocusContinueButton}
onCancel={this.props.onFinished} /> onCancel={this.props.onFinished} />
</BaseDialog> </BaseDialog>
); );

View file

@ -41,9 +41,6 @@ export default React.createClass({
}; };
}, },
componentDidMount: function() {
},
onEmailAddressChanged: function(value) { onEmailAddressChanged: function(value) {
this.setState({ this.setState({
emailAddress: value, emailAddress: value,
@ -131,6 +128,7 @@ export default React.createClass({
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
className="mx_SetEmailDialog_email_input" className="mx_SetEmailDialog_email_input"
autoFocus="true"
placeholder={_t("Email address")} placeholder={_t("Email address")}
placeholderClassName="mx_SetEmailDialog_email_input_placeholder" placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
blurToCancel={false} blurToCancel={false}
@ -140,9 +138,10 @@ export default React.createClass({
<BaseDialog className="mx_SetEmailDialog" <BaseDialog className="mx_SetEmailDialog"
onFinished={this.onCancelled} onFinished={this.onCancelled}
title={this.props.title} title={this.props.title}
contentId='mx_Dialog_content'
> >
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p> <p id='mx_Dialog_content'>
{ _t('This will allow you to reset your password and receive notifications.') } { _t('This will allow you to reset your password and receive notifications.') }
</p> </p>
{ emailInput } { emailInput }

View file

@ -235,14 +235,14 @@ export default React.createClass({
"error": Boolean(this.state.usernameError), "error": Boolean(this.state.usernameError),
"success": usernameAvailable, "success": usernameAvailable,
}); });
usernameIndicator = <div className={usernameIndicatorClasses}> usernameIndicator = <div className={usernameIndicatorClasses} role="alert">
{ usernameAvailable ? _t('Username available') : this.state.usernameError } { usernameAvailable ? _t('Username available') : this.state.usernameError }
</div>; </div>;
} }
let authErrorIndicator = null; let authErrorIndicator = null;
if (this.state.authError) { if (this.state.authError) {
authErrorIndicator = <div className="error"> authErrorIndicator = <div className="error" role="alert">
{ this.state.authError } { this.state.authError }
</div>; </div>;
} }
@ -254,8 +254,9 @@ export default React.createClass({
<BaseDialog className="mx_SetMxIdDialog" <BaseDialog className="mx_SetMxIdDialog"
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={_t('To get started, please pick a username!')} title={_t('To get started, please pick a username!')}
contentId='mx_Dialog_content'
> >
<div className="mx_Dialog_content"> <div className="mx_Dialog_content" id='mx_Dialog_content'>
<div className="mx_SetMxIdDialog_input_group"> <div className="mx_SetMxIdDialog_input_group">
<input type="text" ref="input_value" value={this.state.username} <input type="text" ref="input_value" value={this.state.username}
autoFocus={true} autoFocus={true}

View file

@ -61,17 +61,18 @@ export default React.createClass({
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
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}
> >
<div className="mx_Dialog_content"> <form onSubmit={this.onOk}>
<div className="mx_TextInputDialog_label"> <div className="mx_Dialog_content">
<label htmlFor="textinput"> { this.props.description } </label> <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>
<div> </form>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
</div>
</div>
<DialogButtons primaryButton={this.props.button} <DialogButtons primaryButton={this.props.button}
onPrimaryButtonClick={this.onOk} onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel} /> onCancel={this.onCancel} />

View file

@ -189,8 +189,9 @@ export default React.createClass({
<BaseDialog className='mx_UnknownDeviceDialog' <BaseDialog className='mx_UnknownDeviceDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={_t('Room contains unknown devices')} title={_t('Room contains unknown devices')}
contentId='mx_Dialog_content'
> >
<GeminiScrollbar autoshow={false} className="mx_Dialog_content"> <GeminiScrollbar autoshow={false} className="mx_Dialog_content" id='mx_Dialog_content'>
<h4> <h4>
{ _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) } { _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }
</h4> </h4>

View file

@ -17,6 +17,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { KeyCode } from '../../../Keyboard';
/** /**
* AccessibleButton is a generic wrapper for any element that should be treated * AccessibleButton is a generic wrapper for any element that should be treated
* as a button. Identifies the element as a button, setting proper tab * as a button. Identifies the element as a button, setting proper tab
@ -28,8 +30,34 @@ import PropTypes from 'prop-types';
export default function AccessibleButton(props) { export default function AccessibleButton(props) {
const {element, onClick, children, ...restProps} = props; const {element, onClick, children, ...restProps} = props;
restProps.onClick = onClick; restProps.onClick = onClick;
// We need to consume enter onKeyDown and space onKeyUp
// otherwise we are risking also activating other keyboard focusable elements
// that might receive focus as a result of the AccessibleButtonClick action
// It's because we are using html buttons at a few places e.g. inside dialogs
// And divs which we report as role button to assistive technologies.
// Browsers handle space and enter keypresses differently and we are only adjusting to the
// inconsistencies here
restProps.onKeyDown = function(e) {
if (e.keyCode === KeyCode.ENTER) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
e.preventDefault();
}
};
restProps.onKeyUp = function(e) { restProps.onKeyUp = function(e) {
if (e.keyCode == 13 || e.keyCode == 32) return onClick(e); if (e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.ENTER) {
e.stopPropagation();
e.preventDefault();
}
}; };
restProps.tabIndex = restProps.tabIndex || "0"; restProps.tabIndex = restProps.tabIndex || "0";
restProps.role = "button"; restProps.role = "button";

View file

@ -128,12 +128,22 @@ export const PasswordAuthEntry = React.createClass({
); );
} }
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return ( return (
<div> <div>
<p>{ _t("To continue, please enter your password.") }</p> <p>{ _t("To continue, please enter your password.") }</p>
<p>{ _t("Password:") }</p>
<form onSubmit={this._onSubmit}> <form onSubmit={this._onSubmit}>
<label htmlFor="passwordField">{ _t("Password:") }</label>
<input <input
name="passwordField"
ref="passwordField" ref="passwordField"
className={passwordBoxClass} className={passwordBoxClass}
onChange={this._onPasswordFieldChange} onChange={this._onPasswordFieldChange}
@ -143,9 +153,7 @@ export const PasswordAuthEntry = React.createClass({
{ submitButtonOrSpinner } { submitButtonOrSpinner }
</div> </div>
</form> </form>
<div className="error"> { errorSection }
{ this.props.errorText }
</div>
</div> </div>
); );
}, },
@ -180,14 +188,22 @@ export const RecaptchaAuthEntry = React.createClass({
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm"); const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
const sitePublicKey = this.props.stageParams.public_key; const sitePublicKey = this.props.stageParams.public_key;
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return ( return (
<div> <div>
<CaptchaForm sitePublicKey={sitePublicKey} <CaptchaForm sitePublicKey={sitePublicKey}
onCaptchaResponse={this._onCaptchaResponse} onCaptchaResponse={this._onCaptchaResponse}
/> />
<div className="error"> { errorSection }
{ this.props.errorText }
</div>
</div> </div>
); );
}, },
@ -372,6 +388,14 @@ export const MsisdnAuthEntry = React.createClass({
mx_InteractiveAuthEntryComponents_msisdnSubmit: true, mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
mx_UserSettings_button: true, // XXX button classes mx_UserSettings_button: true, // XXX button classes
}); });
let errorSection;
if (this.state.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.state.errorText }
</div>
);
}
return ( return (
<div> <div>
<p>{ _t("A text message has been sent to %(msisdn)s", <p>{ _t("A text message has been sent to %(msisdn)s",
@ -385,6 +409,7 @@ export const MsisdnAuthEntry = React.createClass({
className="mx_InteractiveAuthEntryComponents_msisdnEntry" className="mx_InteractiveAuthEntryComponents_msisdnEntry"
value={this.state.token} value={this.state.token}
onChange={this._onTokenChange} onChange={this._onTokenChange}
aria-label={ _t("Code")}
/> />
<br /> <br />
<input type="submit" value={_t("Submit")} <input type="submit" value={_t("Submit")}
@ -392,9 +417,7 @@ export const MsisdnAuthEntry = React.createClass({
disabled={!enableSubmit} disabled={!enableSubmit}
/> />
</form> </form>
<div className="error"> {errorSection}
{ this.state.errorText }
</div>
</div> </div>
</div> </div>
); );
@ -427,6 +450,12 @@ export const FallbackAuthEntry = React.createClass({
} }
}, },
focus: function() {
if (this.refs.fallbackButton) {
this.refs.fallbackButton.focus();
}
},
_onShowFallbackClick: function() { _onShowFallbackClick: function() {
const url = this.props.matrixClient.getFallbackAuthUrl( const url = this.props.matrixClient.getFallbackAuthUrl(
this.props.loginType, this.props.loginType,
@ -445,12 +474,18 @@ export const FallbackAuthEntry = React.createClass({
}, },
render: function() { render: function() {
return ( let errorSection;
<div> if (this.props.errorText) {
<a onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a> errorSection = (
<div className="error"> <div className="error" role="alert">
{ this.props.errorText } { this.props.errorText }
</div> </div>
);
}
return (
<div>
<a ref="fallbackButton" onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
{errorSection}
</div> </div>
); );
}, },

View file

@ -531,6 +531,7 @@
"Token incorrect": "Token incorrect", "Token incorrect": "Token incorrect",
"A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s", "A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s",
"Please enter the code it contains:": "Please enter the code it contains:", "Please enter the code it contains:": "Please enter the code it contains:",
"Code": "Code",
"Start authentication": "Start authentication", "Start authentication": "Start authentication",
"powered by Matrix": "powered by Matrix", "powered by Matrix": "powered by Matrix",
"Username on %(hs)s": "Username on %(hs)s", "Username on %(hs)s": "Username on %(hs)s",