Commit 210b2c0c authored by Martin Kutschker's avatar Martin Kutschker Committed by Christian Kuhn

[TASK] Improve accessibility of login

Colour contrast for footnote text meets WCAG 2.1 requirements.
Role status for the caps-lock notice belongs to the div
wrapping the image.
The HTML document has a heading structure (h1 to h3).
The content is placed in ARIA regions.
The first form field has the autofocus (interface selector comes
after password).
All password fields have a caps lock status notification.
The copyright notice implements the WAI-ARIA 1.1 disclosure pattern.
The message after successful changing the password contains
an accessible link.

Resolves: #92904
Releases: master
Change-Id: I67a2e61644711d20a20432b3eed4dca808ec591b
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/66818Tested-by: default avatarRichard Haeser <richard@richardhaeser.com>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: default avatarRichard Haeser <richard@richardhaeser.com>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent e9daa05b
......@@ -59,11 +59,11 @@ $login-input-padding-horizontal: $padding-large-horizontal;
.typo3-login-footnote {
margin-left: auto;
margin-right: auto;
font-size: 0.9em;
font-size: 0.95em;
text-align: center;
padding: 1em 1.5em;
display: block;
color: #999;
color: #666;
@media (min-width: $screen-sm-min) {
flex: none;
......@@ -114,7 +114,6 @@ $login-input-padding-horizontal: $padding-large-horizontal;
//
.typo3-login-logo {
padding-top: 1em;
margin-bottom: 2.5em;
img {
display: block;
......@@ -141,13 +140,13 @@ $login-input-padding-horizontal: $padding-large-horizontal;
border-radius: $login-border-radius;
.card-heading {
padding: 1.5em 2.5em;
padding: 2.5em 2.5em 0;
border-top-left-radius: $login-border-radius - 1;
border-top-right-radius: $login-border-radius - 1;
}
.card-body {
padding: 2.5em;
padding: 1.75em 2.5em 2.5em;
border-bottom: 3px solid $login-highlight;
}
......@@ -254,7 +253,7 @@ $login-input-padding-horizontal: $padding-large-horizontal;
}
.typo3-login-copyright-text {
font-size: 0.9em;
font-size: 0.95em;
margin-top: $line-height-computed;
color: $login-copyright-text;
......
......@@ -58,6 +58,7 @@ class UserPassLogin {
this.options = {
passwordField: '.t3js-login-password-field',
usernameField: '.t3js-login-username-field',
copyrightLink: 't3js-login-copyright-link',
};
// register submit handler
......@@ -65,9 +66,11 @@ class UserPassLogin {
const $usernameField = $(this.options.usernameField);
const $passwordField = $(this.options.passwordField);
const copyrightLink = document.getElementsByClassName(this.options.copyrightLink)[0];
$usernameField.on('keypress', this.showCapsLockWarning);
$passwordField.on('keypress', this.showCapsLockWarning);
copyrightLink.addEventListener('keydown', this.toggleCopyright);
// if the login screen is shown in the login_frameset window for re-login,
// then try to get the username of the current/former login from opening windows main frame:
......@@ -108,6 +111,12 @@ class UserPassLogin {
.find('.t3js-login-alert-capslock')
.toggleClass('hidden', !UserPassLogin.isCapslockEnabled(event));
}
public toggleCopyright = (event: KeyboardEvent): void => {
if (event.key === ' ') {
(<HTMLLinkElement>(event.target)).click();
}
}
}
export = new UserPassLogin();
......@@ -49,6 +49,12 @@ Have a nice day.</source>
<trans-unit id="foldertreeview.noFolders.message" resname="foldertreeview.noFolders.message">
<source>You do not have access to any folder. Please ask your administrator to fix access permissions for your account.</source>
</trans-unit>
<trans-unit id="login.header" resname="login.header">
<source>Login</source>
</trans-unit>
<trans-unit id="login.region.footnote" resname="login.region.footnote">
<source>Footnote</source>
</trans-unit>
<trans-unit id="login.link" resname="login.link">
<source>Login with username and password</source>
</trans-unit>
......@@ -64,6 +70,9 @@ Have a nice day.</source>
<trans-unit id="login.error.capslock" resname="login.error.capslock">
<source>Attention: Caps lock enabled!</source>
</trans-unit>
<trans-unit id="login.error.capslock" resname="login.error.capslockStatus">
<source>Caps lock enabled</source>
</trans-unit>
<trans-unit id="login.interface" resname="login.interface">
<source>Interface</source>
</trans-unit>
......@@ -73,6 +82,12 @@ Have a nice day.</source>
<trans-unit id="login.news.next" resname="login.news.next">
<source>Next</source>
</trans-unit>
<trans-unit id="login.navigation.loginProvider" resname="login.navigation.loginProvider">
<source>Login Provider</source>
</trans-unit>
<trans-unit id="login.navigation.typo3" resname="login.navigation.typo3">
<source>Further information about TYPO3</source>
</trans-unit>
<trans-unit id="login.error.message" resname="login.error.message">
<source>Your login attempt did not succeed</source>
</trans-unit>
......
......@@ -25,7 +25,7 @@
<source>You've successfully reset your password!</source>
</trans-unit>
<trans-unit id="reset_success.message" resname="reset_success.message">
<source><![CDATA[You can now log in to the TYPO3 backend with your new credentials <a href="%s">here</a>.]]></source>
<source><![CDATA[With your new credentials you can now log in to the <a href="%s">TYPO3 backend</a>.]]></source>
</trans-unit>
<trans-unit id="input.email" resname="input.email">
<source>Enter your email address</source>
......
......@@ -3,11 +3,14 @@
<div class="typo3-login-inner">
<div class="typo3-login-container">
<div class="typo3-login-wrap">
<div class="card card-lg card-login">
<div class="card-body">
<div class="card card-login">
<header class="card-heading">
<h1 class="sr-only"><f:translate key="login.header" /></h1>
<div class="typo3-login-logo">
<img src="{logo}" class="typo3-login-image" alt="" />
</div>
</header>
<main class="card-body">
<f:if condition="{formType} == 'LoginForm'">
<f:then>
<f:if condition="{hasLoginError}">
......@@ -34,29 +37,32 @@
<f:form.hidden name="redirect_url" value="{redirectUrl}" />
<f:form.hidden name="loginRefresh" value="{loginRefresh}" />
<f:render partial="Login/InterfaceSelector" arguments="{_all}" />
<f:render section="loginFormFields" />
<f:render partial="Login/InterfaceSelector" arguments="{_all}" />
<div class="form-group" id="t3-login-submit-section">
<button class="btn btn-block btn-login t3js-login-submit" id="t3-login-submit" type="submit" name="commandLI" data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i> {f:translate(key: 'login.process')}" autocomplete="off">
<f:translate key="login.submit" />
</button>
</div>
<ul class="list-unstyled typo3-login-links">
<f:for each="{loginProviders}" as="provider" key="providerKey">
<f:if condition="{provider.label}">
<f:if condition="{loginProviderIdentifier} != {providerKey}">
<li class="t3js-loginprovider-switch" data-providerkey="{providerKey}"><a href="?loginProvider={providerKey}"><i class="fa fa-fw {provider.icon-class}"></i> <span><f:translate key="{provider.label}" /></span></a></li>
</f:if>
</f:if>
</f:for>
</ul>
<f:if condition="{enablePasswordReset}">
<f:render section="ResetPassword" arguments="{_all}" optional="true" />
</f:if>
</form>
</div>
<nav aria-label="{f:translate(key: 'login.navigation.loginProvider')}">
<f:comment>role is for VoiceOver</f:comment>
<ul class="list-unstyled typo3-login-links" role="list">
<f:for each="{loginProviders}" as="provider" key="providerKey">
<f:if condition="{provider.label}">
<f:if condition="{loginProviderIdentifier} != {providerKey}">
<li class="t3js-loginprovider-switch" data-providerkey="{providerKey}"><a href="?loginProvider={providerKey}"><i class="fa fa-fw {provider.icon-class}"></i> <span><f:translate key="{provider.label}" /></span></a></li>
</f:if>
</f:if>
</f:for>
</ul>
</nav>
<f:if condition="{enablePasswordReset}">
<f:render section="ResetPassword" arguments="{_all}" optional="true" />
</f:if>
</f:then>
<f:else if="{enablePasswordReset}">
<f:render section="ResetPassword" arguments="{_all}" />
......@@ -96,34 +102,38 @@
</form>
</f:else>
</f:if>
</div>
</main>
<f:render partial="LoginNews" arguments="{_all}" />
<div class="card-footer">
<footer class="card-footer">
<div class="typo3-login-copyright-wrap">
<a href="#loginCopyright" class="typo3-login-copyright-link collapsed" data-bs-toggle="collapse" aria-expanded="false" aria-controls="loginCopyright">
<a href="#loginCopyright" class="typo3-login-copyright-link t3js-login-copyright-link collapsed" data-bs-toggle="collapse" role="button" aria-expanded="false" aria-controls="loginCopyright">
<span><f:translate key="login.copyrightLink" /></span>
<img src="{images.typo3}" alt="{f:translate(key: 'login.typo3Logo')}" width="70" height="20" />
<f:comment>the image within the button is only decorative</f:comment>
<img src="{images.typo3}" alt="" width="70" height="20" />
</a>
<div id="loginCopyright" class="collapse">
<div class="typo3-login-copyright-text">
<p>
<f:format.raw>{copyright}</f:format.raw>
</p>
<ul class="list-unstyled">
<li><a href="https://typo3.org" target="_blank" rel="noreferrer" class="t3-login-link-typo3"><i class="fa fa-external-link"></i> TYPO3.org</a></li>
<li><a href="https://typo3.org/donate/online-donation/" target="_blank" rel="noreferrer" class="t3-login-link-donate"><i class="fa fa-external-link"></i> <f:translate key="login.donate" /></a></li>
</ul>
<nav aria-label="{f:translate(key: 'login.navigation.typo3')}">
<f:comment>role is for VoiceOver</f:comment>
<ul class="list-unstyled" role="list">
<li><a href="https://typo3.org" target="_blank" rel="noreferrer" class="t3-login-link-typo3"><i class="fa fa-external-link"></i> TYPO3.org</a></li>
<li><a href="https://typo3.org/donate/online-donation/" target="_blank" rel="noreferrer" class="t3-login-link-donate"><i class="fa fa-external-link"></i> <f:translate key="login.donate" /></a></li>
</ul>
</nav>
</div>
</div>
</div>
</div>
</footer>
</div>
</div>
</div>
<f:if condition="{loginFootnote}">
<div class="typo3-login-footnote">
<aside class="typo3-login-footnote" aria-label="{f:translate(key: 'login.region.footnote')}">
<p>{loginFootnote}</p>
</div>
</aside>
</f:if>
</div>
<f:comment>This link is only used for protection of the backend.</f:comment>
......
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Login" />
<f:section name="ResetPassword">
......@@ -6,7 +7,7 @@
<f:if condition="{resetInitiated}">
<f:then>
<div class="callout callout-success">
<h4 class="callout-title"><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:email_sent.headline" /></h4>
<h3 class="callout-title"><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:email_sent.headline" /></h3>
<div class="callout-body"><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:email_sent.message" arguments="{0: email}" /></div>
</div>
<p class="pull-right">
......@@ -46,3 +47,4 @@
</f:if>
</div>
</f:section>
</html>
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Login" />
<f:section name="ResetPassword">
......@@ -14,7 +15,7 @@
<div class="callout callout-success">
<div class="media">
<div class="media-body">
<h4 class="callout-title"><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:reset_success.headline" /></h4>
<h3 class="callout-title"><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:reset_success.headline" /></h3>
<div class="callout-body"><f:format.raw><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:reset_success.message" arguments="{0: '{f:be.uri(route: \'login\')}'}"/></f:format.raw></div>
</div>
</div>
......@@ -30,14 +31,22 @@
<div class="form-group">
<div class="form-control-wrap">
<div class="form-control-holder">
<input type="password" name="password" placeholder="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:input.password')}" value="" class="form-control input-login t3js-clearable" autocomplete="off" autofocus="autofocus" required="required" />
<input type="password" name="password" placeholder="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:input.password')}" value="" class="form-control input-login t3js-clearable t3js-login-password-field" autocomplete="new-password" autofocus="autofocus" required="required" />
<div role="status" class="form-notice-capslock hidden t3js-login-alert-capslock">
<img aria-hidden="true" src="{images.capslock}" width="14" height="14" alt="" title="{f:translate(key: 'login.error.capslock')}" />
<span class="sr-only"><f:translate key="login.error.capslockStatus" /></span>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="form-control-wrap">
<div class="form-control-holder">
<input type="password" name="passwordrepeat" placeholder="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:input.passwordrepeat')}" value="" autocomplete="off" class="form-control input-login t3js-clearable" required="required" />
<input type="password" name="passwordrepeat" placeholder="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:input.passwordrepeat')}" value="" autocomplete="off" class="form-control input-login t3js-clearable t3js-login-password-field" required="required" />
<div role="status" class="form-notice-capslock hidden t3js-login-alert-capslock">
<img aria-hidden="true" src="{images.capslock}" width="14" height="14" alt="" title="{f:translate(key: 'login.error.capslock')}" />
<span class="sr-only"><f:translate key="login.error.capslockStatus" /></span>
</div>
</div>
</div>
</div>
......@@ -51,3 +60,4 @@
</f:if>
</div>
</f:section>
</html>
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Login" />
<f:section name="loginFormFields">
......@@ -5,8 +6,9 @@
<div class="form-control-wrap">
<div class="form-control-holder">
<input type="text" id="t3-username" name="username" value="{presetUsername}" aria-label="{f:translate(key: 'login.username')}" placeholder="{f:translate(key: 'login.username')}" class="form-control input-login t3js-clearable t3js-login-username-field" autofocus="autofocus" autocomplete="username" required="required" />
<div class="form-notice-capslock hidden t3js-login-alert-capslock">
<img role="status" src="{images.capslock}" width="14" height="14" alt="" title="{f:translate(key: 'login.error.capslock')}" />
<div role="status" class="form-notice-capslock hidden t3js-login-alert-capslock">
<img aria-hidden="true" src="{images.capslock}" width="14" height="14" alt="" title="{f:translate(key: 'login.error.capslock')}" />
<span class="sr-only"><f:translate key="login.error.capslockStatus" /></span>
</div>
</div>
</div>
......@@ -15,17 +17,20 @@
<div class="form-control-wrap">
<div class="form-control-holder">
<input type="password" id="t3-password" name="p_field" value="{presetPassword}" aria-label="{f:translate(key: 'login.password')}" placeholder="{f:translate(key: 'login.password')}" class="form-control input-login t3js-clearable t3js-login-password-field" autocomplete="current-password" required="required" data-rsa-encryption="t3-field-userident" />
<div class="form-notice-capslock hidden t3js-login-alert-capslock">
<img src="{images.capslock}" width="14" height="14" alt="{f:translate(key: 'login.error.capslock')}" title="{f:translate(key: 'login.error.capslock')}" />
<div role="status" class="form-notice-capslock hidden t3js-login-alert-capslock">
<img aria-hidden="true" src="{images.capslock}" width="14" height="14" alt="" title="{f:translate(key: 'login.error.capslock')}" />
<span class="sr-only"><f:translate key="login.error.capslockStatus" /></span>
</div>
</div>
</div>
</div>
</f:section>
<f:section name="ResetPassword">
<div class="forgot-password">
<div class="text-right">
<f:be.link route="password_forget" parameters="{loginProvider: loginProviderIdentifier}">{f:translate(key: 'login.password_forget')}</f:be.link>
<f:be.link route="password_forget" parameters="{loginProvider: loginProviderIdentifier}"><f:translate key="login.password_forget" /></f:be.link>
</div>
</div>
</f:section>
</html>
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};define(["require","exports","jquery","./Login"],(function(e,s,t,o){"use strict";t=__importDefault(t);class i{constructor(){this.resetPassword=()=>{const e=t.default(this.options.passwordField);e.val()&&(t.default(o.options.useridentField).val(e.val()),e.val(""))},this.showCapsLockWarning=e=>{t.default(e.target).parent().parent().find(".t3js-login-alert-capslock").toggleClass("hidden",!i.isCapslockEnabled(e))},this.options={passwordField:".t3js-login-password-field",usernameField:".t3js-login-username-field"},o.options.submitHandler=this.resetPassword;const e=t.default(this.options.usernameField),s=t.default(this.options.passwordField);e.on("keypress",this.showCapsLockWarning),s.on("keypress",this.showCapsLockWarning);try{parent.opener&&parent.opener.TYPO3&&parent.opener.TYPO3.configuration&&parent.opener.TYPO3.configuration.username&&e.val(parent.opener.TYPO3.configuration.username)}catch(e){}""===e.val()?e.focus():s.focus()}static isCapslockEnabled(e){const s=e||window.event;if(!s)return!1;let t=-1;s.which?t=s.which:s.keyCode&&(t=s.keyCode);let o=!1;return s.shiftKey?o=s.shiftKey:s.modifiers&&(o=!!(4&s.modifiers)),t>=65&&t<=90&&!o||t>=97&&t<=122&&o}}return new i}));
\ No newline at end of file
var __importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};define(["require","exports","jquery","./Login"],(function(e,t,s,i){"use strict";s=__importDefault(s);class o{constructor(){this.resetPassword=()=>{const e=s.default(this.options.passwordField);e.val()&&(s.default(i.options.useridentField).val(e.val()),e.val(""))},this.showCapsLockWarning=e=>{s.default(e.target).parent().parent().find(".t3js-login-alert-capslock").toggleClass("hidden",!o.isCapslockEnabled(e))},this.toggleCopyright=e=>{" "===e.key&&e.target.click()},this.options={passwordField:".t3js-login-password-field",usernameField:".t3js-login-username-field",copyrightLink:"t3js-login-copyright-link"},i.options.submitHandler=this.resetPassword;const e=s.default(this.options.usernameField),t=s.default(this.options.passwordField),n=document.getElementsByClassName(this.options.copyrightLink)[0];e.on("keypress",this.showCapsLockWarning),t.on("keypress",this.showCapsLockWarning),n.addEventListener("keydown",this.toggleCopyright);try{parent.opener&&parent.opener.TYPO3&&parent.opener.TYPO3.configuration&&parent.opener.TYPO3.configuration.username&&e.val(parent.opener.TYPO3.configuration.username)}catch(e){}""===e.val()?e.focus():t.focus()}static isCapslockEnabled(e){const t=e||window.event;if(!t)return!1;let s=-1;t.which?s=t.which:t.keyCode&&(s=t.keyCode);let i=!1;return t.shiftKey?i=t.shiftKey:t.modifiers&&(i=!!(4&t.modifiers)),s>=65&&s<=90&&!i||s>=97&&s<=122&&i}}return new o}));
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment