Login-page a11y improvements
Closes #27190 Signed-off-by: Peter Keuter <github@peterkeuter.nl>
This commit is contained in:
parent
788d146bf2
commit
9cced05049
4 changed files with 347 additions and 15 deletions
|
@ -11,7 +11,7 @@
|
|||
<div class="${properties.kcFormGroupClass!}">
|
||||
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
|
||||
|
||||
<input tabindex="1" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')}" type="text" autofocus autocomplete="off"
|
||||
<input tabindex="2" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')}" type="text" autofocus autocomplete="username"
|
||||
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
|
||||
/>
|
||||
|
||||
|
@ -28,11 +28,11 @@
|
|||
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
|
||||
|
||||
<div class="${properties.kcInputGroup!}">
|
||||
<input tabindex="2" id="password" class="${properties.kcInputClass!}" name="password" type="password" autocomplete="off"
|
||||
<input tabindex="3" id="password" class="${properties.kcInputClass!}" name="password" type="password" autocomplete="current-password"
|
||||
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
|
||||
/>
|
||||
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg("showPassword")}"
|
||||
aria-controls="password" data-password-toggle
|
||||
aria-controls="password" data-password-toggle tabindex="4"
|
||||
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
|
||||
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
|
||||
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
|
||||
|
@ -53,9 +53,9 @@
|
|||
<div class="checkbox">
|
||||
<label>
|
||||
<#if login.rememberMe??>
|
||||
<input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox" checked> ${msg("rememberMe")}
|
||||
<input tabindex="5" id="rememberMe" name="rememberMe" type="checkbox" checked> ${msg("rememberMe")}
|
||||
<#else>
|
||||
<input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox"> ${msg("rememberMe")}
|
||||
<input tabindex="5" id="rememberMe" name="rememberMe" type="checkbox"> ${msg("rememberMe")}
|
||||
</#if>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -63,7 +63,7 @@
|
|||
</div>
|
||||
<div class="${properties.kcFormOptionsWrapperClass!}">
|
||||
<#if realm.resetPasswordAllowed>
|
||||
<span><a tabindex="5" href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</a></span>
|
||||
<span><a tabindex="6" href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</a></span>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
|
@ -71,7 +71,7 @@
|
|||
|
||||
<div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
|
||||
<input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
|
||||
<input tabindex="4" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
|
||||
<input tabindex="7" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
|
||||
</div>
|
||||
</form>
|
||||
</#if>
|
||||
|
@ -82,7 +82,7 @@
|
|||
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
|
||||
<div id="kc-registration-container">
|
||||
<div id="kc-registration">
|
||||
<span>${msg("noAccount")} <a tabindex="6"
|
||||
<span>${msg("noAccount")} <a tabindex="8"
|
||||
href="${url.registrationUrl}">${msg("doRegister")}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -91,7 +91,7 @@
|
|||
<#if realm.password && social.providers??>
|
||||
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
|
||||
<hr/>
|
||||
<h4>${msg("identity-provider-login-label")}</h4>
|
||||
<h2>${msg("identity-provider-login-label")}</h2>
|
||||
|
||||
<ul class="${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>">
|
||||
<#list social.providers as p>
|
||||
|
|
|
@ -0,0 +1,315 @@
|
|||
// @ts-check
|
||||
/*
|
||||
* This content is licensed according to the W3C Software License at
|
||||
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
|
||||
*
|
||||
* File: menu-button-links.js
|
||||
*
|
||||
* Desc: Creates a menu button that opens a menu of links
|
||||
*
|
||||
* Modified by Peter Keuter to adhere to the coding standards of Keycloak
|
||||
* Original file: https://www.w3.org/WAI/content-assets/wai-aria-practices/patterns/menu-button/examples/js/menu-button-links.js
|
||||
* Source: https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-links.html
|
||||
*/
|
||||
|
||||
class MenuButtonLinks {
|
||||
constructor(domNode) {
|
||||
this.domNode = domNode;
|
||||
this.buttonNode = domNode.querySelector("button");
|
||||
this.menuNode = domNode.querySelector('[role="menu"]');
|
||||
this.menuitemNodes = [];
|
||||
this.firstMenuitem = false;
|
||||
this.lastMenuitem = false;
|
||||
this.firstChars = [];
|
||||
|
||||
this.buttonNode.addEventListener("keydown", (e) => this.onButtonKeydown(e));
|
||||
this.buttonNode.addEventListener("click", (e) => this.onButtonClick(e));
|
||||
|
||||
const nodes = domNode.querySelectorAll('[role="menuitem"]');
|
||||
|
||||
for (const menuitem of nodes) {
|
||||
this.menuitemNodes.push(menuitem);
|
||||
menuitem.tabIndex = -1;
|
||||
this.firstChars.push(menuitem.textContent.trim()[0].toLowerCase());
|
||||
|
||||
menuitem.addEventListener("keydown", (e) => this.onMenuitemKeydown(e));
|
||||
|
||||
menuitem.addEventListener("mouseover", (e) =>
|
||||
this.onMenuitemMouseover(e)
|
||||
);
|
||||
|
||||
if (!this.firstMenuitem) {
|
||||
this.firstMenuitem = menuitem;
|
||||
}
|
||||
this.lastMenuitem = menuitem;
|
||||
}
|
||||
|
||||
domNode.addEventListener("focusin", () => this.onFocusin());
|
||||
domNode.addEventListener("focusout", () => this.onFocusout());
|
||||
|
||||
window.addEventListener(
|
||||
"mousedown",
|
||||
(e) => this.onBackgroundMousedown(e),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
setFocusToMenuitem = (newMenuitem) =>
|
||||
this.menuitemNodes.forEach((item) => {
|
||||
if (item === newMenuitem) {
|
||||
item.tabIndex = 0;
|
||||
newMenuitem.focus();
|
||||
} else {
|
||||
item.tabIndex = -1;
|
||||
}
|
||||
});
|
||||
|
||||
setFocusToFirstMenuitem = () => this.setFocusToMenuitem(this.firstMenuitem);
|
||||
|
||||
setFocusToLastMenuitem = () => this.setFocusToMenuitem(this.lastMenuitem);
|
||||
|
||||
setFocusToPreviousMenuitem = (currentMenuitem) => {
|
||||
let newMenuitem, index;
|
||||
|
||||
if (currentMenuitem === this.firstMenuitem) {
|
||||
newMenuitem = this.lastMenuitem;
|
||||
} else {
|
||||
index = this.menuitemNodes.indexOf(currentMenuitem);
|
||||
newMenuitem = this.menuitemNodes[index - 1];
|
||||
}
|
||||
|
||||
this.setFocusToMenuitem(newMenuitem);
|
||||
|
||||
return newMenuitem;
|
||||
};
|
||||
|
||||
setFocusToNextMenuitem = (currentMenuitem) => {
|
||||
let newMenuitem, index;
|
||||
|
||||
if (currentMenuitem === this.lastMenuitem) {
|
||||
newMenuitem = this.firstMenuitem;
|
||||
} else {
|
||||
index = this.menuitemNodes.indexOf(currentMenuitem);
|
||||
newMenuitem = this.menuitemNodes[index + 1];
|
||||
}
|
||||
this.setFocusToMenuitem(newMenuitem);
|
||||
|
||||
return newMenuitem;
|
||||
};
|
||||
|
||||
setFocusByFirstCharacter = (currentMenuitem, char) => {
|
||||
let start, index;
|
||||
|
||||
if (char.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
char = char.toLowerCase();
|
||||
|
||||
// Get start index for search based on position of currentItem
|
||||
start = this.menuitemNodes.indexOf(currentMenuitem) + 1;
|
||||
if (start >= this.menuitemNodes.length) {
|
||||
start = 0;
|
||||
}
|
||||
|
||||
// Check remaining slots in the menu
|
||||
index = this.firstChars.indexOf(char, start);
|
||||
|
||||
// If not found in remaining slots, check from beginning
|
||||
if (index === -1) {
|
||||
index = this.firstChars.indexOf(char, 0);
|
||||
}
|
||||
|
||||
// If match was found...
|
||||
if (index > -1) {
|
||||
this.setFocusToMenuitem(this.menuitemNodes[index]);
|
||||
}
|
||||
};
|
||||
|
||||
// Utilities
|
||||
|
||||
getIndexFirstChars = (startIndex, char) => {
|
||||
for (let i = startIndex; i < this.firstChars.length; i++) {
|
||||
if (char === this.firstChars[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
// Popup menu methods
|
||||
|
||||
openPopup = () => {
|
||||
this.menuNode.style.display = "block";
|
||||
this.buttonNode.setAttribute("aria-expanded", "true");
|
||||
};
|
||||
|
||||
closePopup = () => {
|
||||
if (this.isOpen()) {
|
||||
this.buttonNode.setAttribute("aria-expanded", "false");
|
||||
this.menuNode.style.removeProperty("display");
|
||||
}
|
||||
};
|
||||
|
||||
isOpen = () => {
|
||||
return this.buttonNode.getAttribute("aria-expanded") === "true";
|
||||
};
|
||||
|
||||
// Menu event handlers
|
||||
|
||||
onFocusin = () => {
|
||||
this.domNode.classList.add("focus");
|
||||
};
|
||||
|
||||
onFocusout = () => {
|
||||
this.domNode.classList.remove("focus");
|
||||
};
|
||||
|
||||
onButtonKeydown = (event) => {
|
||||
const key = event.key;
|
||||
let flag = false;
|
||||
|
||||
switch (key) {
|
||||
case " ":
|
||||
case "Enter":
|
||||
case "ArrowDown":
|
||||
case "Down":
|
||||
this.openPopup();
|
||||
this.setFocusToFirstMenuitem();
|
||||
flag = true;
|
||||
break;
|
||||
|
||||
case "Esc":
|
||||
case "Escape":
|
||||
this.closePopup();
|
||||
this.buttonNode.focus();
|
||||
flag = true;
|
||||
break;
|
||||
|
||||
case "Up":
|
||||
case "ArrowUp":
|
||||
this.openPopup();
|
||||
this.setFocusToLastMenuitem();
|
||||
flag = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (flag) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
onButtonClick(event) {
|
||||
if (this.isOpen()) {
|
||||
this.closePopup();
|
||||
this.buttonNode.focus();
|
||||
} else {
|
||||
this.openPopup();
|
||||
this.setFocusToFirstMenuitem();
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
onMenuitemKeydown(event) {
|
||||
const tgt = event.currentTarget;
|
||||
const key = event.key;
|
||||
let flag = false;
|
||||
|
||||
const isPrintableCharacter = (str) => str.length === 1 && str.match(/\S/);
|
||||
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (isPrintableCharacter(key)) {
|
||||
this.setFocusByFirstCharacter(tgt, key);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
if (event.key === "Tab") {
|
||||
this.buttonNode.focus();
|
||||
this.closePopup();
|
||||
flag = true;
|
||||
}
|
||||
} else {
|
||||
switch (key) {
|
||||
case " ":
|
||||
window.location.href = tgt.href;
|
||||
break;
|
||||
|
||||
case "Esc":
|
||||
case "Escape":
|
||||
this.closePopup();
|
||||
this.buttonNode.focus();
|
||||
flag = true;
|
||||
break;
|
||||
|
||||
case "Up":
|
||||
case "ArrowUp":
|
||||
this.setFocusToPreviousMenuitem(tgt);
|
||||
flag = true;
|
||||
break;
|
||||
|
||||
case "ArrowDown":
|
||||
case "Down":
|
||||
this.setFocusToNextMenuitem(tgt);
|
||||
flag = true;
|
||||
break;
|
||||
|
||||
case "Home":
|
||||
case "PageUp":
|
||||
this.setFocusToFirstMenuitem();
|
||||
flag = true;
|
||||
break;
|
||||
|
||||
case "End":
|
||||
case "PageDown":
|
||||
this.setFocusToLastMenuitem();
|
||||
flag = true;
|
||||
break;
|
||||
|
||||
case "Tab":
|
||||
this.closePopup();
|
||||
break;
|
||||
|
||||
default:
|
||||
if (isPrintableCharacter(key)) {
|
||||
this.setFocusByFirstCharacter(tgt, key);
|
||||
flag = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onMenuitemMouseover(event) {
|
||||
const tgt = event.currentTarget;
|
||||
tgt.focus();
|
||||
}
|
||||
|
||||
onBackgroundMousedown(event) {
|
||||
if (!this.domNode.contains(event.target)) {
|
||||
if (this.isOpen()) {
|
||||
this.closePopup();
|
||||
this.buttonNode.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const menuButtons = document.querySelectorAll(".menu-button-links");
|
||||
for (const button of menuButtons) {
|
||||
new MenuButtonLinks(button);
|
||||
}
|
|
@ -29,6 +29,7 @@
|
|||
<script src="${url.resourcesPath}/${script}" type="text/javascript"></script>
|
||||
</#list>
|
||||
</#if>
|
||||
<script src="${url.resourcesPath}/js/menu-button-links.js" type="module"></script>
|
||||
<#if scripts??>
|
||||
<#list scripts as script>
|
||||
<script src="${script}" type="text/javascript"></script>
|
||||
|
@ -58,13 +59,15 @@
|
|||
<#if realm.internationalizationEnabled && locale.supported?size gt 1>
|
||||
<div class="${properties.kcLocaleMainClass!}" id="kc-locale">
|
||||
<div id="kc-locale-wrapper" class="${properties.kcLocaleWrapperClass!}">
|
||||
<div id="kc-locale-dropdown" class="${properties.kcLocaleDropDownClass!}">
|
||||
<a href="#" id="kc-current-locale-link">${locale.current}</a>
|
||||
<ul class="${properties.kcLocaleListClass!}">
|
||||
<div id="kc-locale-dropdown" class="menu-button-links ${properties.kcLocaleDropDownClass!}">
|
||||
<button tabindex="1" id="kc-current-locale-link" aria-label="${msg("languages")}" aria-haspopup="true" aria-expanded="false" aria-controls="language-switch1">${locale.current}</button>
|
||||
<ul role="menu" tabindex="-1" aria-labelledby="kc-current-locale-link" aria-activedescendant="" id="language-switch1" class="${properties.kcLocaleListClass!}">
|
||||
<#assign i = 1>
|
||||
<#list locale.supported as l>
|
||||
<li class="${properties.kcLocaleListItemClass!}">
|
||||
<a class="${properties.kcLocaleItemClass!}" href="${l.url}">${l.label}</a>
|
||||
<li class="${properties.kcLocaleListItemClass!}" role="none">
|
||||
<a role="menuitem" id="language-${i}" class="${properties.kcLocaleItemClass!}" href="${l.url}">${l.label}</a>
|
||||
</li>
|
||||
<#assign i++>
|
||||
</#list>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -53,7 +53,17 @@ h1#kc-page-title {
|
|||
font-size: var(--pf-global--FontSize--sm);
|
||||
}
|
||||
|
||||
a#kc-current-locale-link::after {
|
||||
#kc-locale-dropdown button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--pf-global--Color--200);
|
||||
text-align: right;
|
||||
font-size: var(--pf-global--FontSize--sm);
|
||||
}
|
||||
|
||||
button#kc-current-locale-link::after {
|
||||
content: "\2c5";
|
||||
margin-left: var(--pf-global--spacer--xs)
|
||||
}
|
||||
|
@ -274,6 +284,10 @@ ul#kc-totp-supported-apps {
|
|||
color: var(--pf-global--Color--200);
|
||||
}
|
||||
|
||||
.kc-social-gray h2 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.kc-social-item {
|
||||
margin-bottom: var(--pf-global--spacer--sm);
|
||||
font-size: 15px;
|
||||
|
|
Loading…
Reference in a new issue