@joomla/joomla-a11y-checker
Version:
ooa11y is an accessibility and quality assurance tool that visually highlights common accessibility and usability issues.
1,064 lines (950 loc) • 136 kB
JavaScript
import tippy from 'tippy.js';
// import 'tippy.js/dist/tippy.css';
/**
* Utility methods
*/
// Determine element visibility
const isElementHidden = ($el) => {
if ($el.getAttribute('hidden') || ($el.offsetWidth === 0 && $el.offsetHeight === 0)) {
return true;
} else {
const compStyles = getComputedStyle($el);
return compStyles.getPropertyValue('display') === 'none';
}
};
// Escape HTML, encode HTML symbols
const escapeHTML = (text) => {
const $div = document.createElement('div');
$div.textContent = text;
return $div.innerHTML.replaceAll('"', '"').replaceAll("'", ''').replaceAll("`", '`');
}
/**
* Jooa11y Translation object
*/
const Lang = {
langStrings: {},
addI18n: function (strings) {
this.langStrings = strings;
},
_: function (string) {
return this.translate(string);
},
sprintf: function (string, ...args) {
let transString = this._(string);
if (args && args.length) {
args.forEach((arg) => {
transString = transString.replace(/%\([a-zA-z]+\)/, arg);
});
}
return transString;
},
translate: function (string) {
return this.langStrings[string] || string;
},
};
/**
* Jooa11y default options
*/
const defaultOptions = {
langCode: 'en',
// Target area to scan.
checkRoot: 'main', // A content container
// Readability configuration.
readabilityRoot: 'main',
readabilityLang: 'en',
// Inclusions and exclusions. Use commas to seperate classes or elements.
containerIgnore: '.jooa11y-ignore', // Ignore specific regions.
outlineIgnore: '', // Exclude headings from outline panel.
headerIgnore: '', // Ignore specific headings. E.g. "h1.jumbotron-heading"
imageIgnore: '', // Ignore specific images.
linkIgnore: '', // Ignore specific links.
linkIgnoreSpan: 'noscript, span.sr-only-example', // Ignore specific classes within links. Example: <a href="#">learn more <span class="sr-only-example">(opens new tab)</span></a>.
linksToFlag: '', // Links you don't want your content editors pointing to (e.g. development environments).
// Embedded content.
videoContent: "video, [src*='youtube.com'], [src*='vimeo.com'], [src*='yuja.com'], [src*='panopto.com']",
audioContent: "audio, [src*='soundcloud.com'], [src*='simplecast.com'], [src*='podbean.com'], [src*='buzzsprout.com'], [src*='blubrry.com'], [src*='transistor.fm'], [src*='fusebox.fm'], [src*='libsyn.com']",
embeddedContent: '',
// Alt Text stop words.
suspiciousAltWords: ['image', 'graphic', 'picture', 'photo'],
placeholderAltStopWords: [
'alt',
'image',
'photo',
'decorative',
'photo',
'placeholder',
'placeholder image',
'spacer',
'.',
],
// Link Text stop words
partialAltStopWords: [
'click',
'click here',
'click here for more',
'click here to learn more',
'click here to learn more.',
'check out',
'download',
'download here',
'download here.',
'find out',
'find out more',
'find out more.',
'form',
'here',
'here.',
'info',
'information',
'link',
'learn',
'learn more',
'learn more.',
'learn to',
'more',
'page',
'paper',
'read more',
'read',
'read this',
'this',
'this page',
'this page.',
'this website',
'this website.',
'view',
'view our',
'website',
'.',
],
warningAltWords: [
'< ',
' >',
'click here',
],
// Link Text (Advanced)
newWindowPhrases: [
'external',
'new tab',
'new window',
'pop-up',
'pop up',
],
// Link Text (Advanced). Only some items in list would need to be translated.
fileTypePhrases: [
'document',
'pdf',
'doc',
'docx',
'word',
'mp3',
'ppt',
'text',
'pptx',
'powerpoint',
'txt',
'exe',
'dmg',
'rtf',
'install',
'windows',
'macos',
'spreadsheet',
'worksheet',
'csv',
'xls',
'xlsx',
'video',
'mp4',
'mov',
'avi',
],
};
defaultOptions.embeddedContent = `${defaultOptions.videoContent}, ${defaultOptions.audioContent}`;
/**
* Load and validate options
*
* @param {Jooa11y} instance
* @param {Object} customOptions
* @returns {Object}
*/
const loadOptions = (instance, customOptions) => {
const options = customOptions ? Object.assign(defaultOptions, customOptions) : defaultOptions;
// Check required options
['langCode', 'checkRoot'].forEach((option) => {
if (!options[option]) {
throw new Error(`Option [${option}] is required`);
}
});
if (!options.readabilityRoot) {
options.readabilityRoot = options.checkRoot;
}
// Container ignores apply to self and children.
if (options.containerIgnore) {
let containerSelectors = options.containerIgnore.split(',').map((el) => {
return `${el} *, ${el}`
});
options.containerIgnore = '[aria-hidden="true"], #jooa11y-container *, .jooa11y-instance *, ' + containerSelectors.join(', ')
} else {
options.containerIgnore = '[aria-hidden="true"], #jooa11y-container *, .jooa11y-instance *';
}
instance.containerIgnore = options.containerIgnore;
// Images ignore
instance.imageIgnore = instance.containerIgnore + ', [role="presentation"], [src^="https://trck.youvisit.com"]';
if (options.imageIgnore) {
instance.imageIgnore = options.imageIgnore + ',' + instance.imageIgnore;
}
// Ignore specific headings
instance.headerIgnore = options.containerIgnore;
if (options.headerIgnore) {
instance.headerIgnore = options.headerIgnore + ',' + instance.headerIgnore;
}
// Links ignore defaults plus jooa11y links.
instance.linkIgnore = instance.containerIgnore + ', [aria-hidden="true"], .anchorjs-link';
if (options.linkIgnore) {
instance.linkIgnore = options.linkIgnore + ',' + instance.linkIgnore;
}
return options;
};
/**
* Jooa11y class
*/
class Jooa11y {
constructor(options) {
this.containerIgnore = '';
this.imageIgnore = '';
this.headerIgnore = '';
this.linkIgnore = '';
// Load options
this.options = loadOptions(this, options);
//Icon on the main toggle. Easy to replace.
const MainToggleIcon =
"<svg role='img' focusable='false' width='35px' height='35px' aria-hidden='true' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path fill='#ffffff' d='M256 48c114.953 0 208 93.029 208 208 0 114.953-93.029 208-208 208-114.953 0-208-93.029-208-208 0-114.953 93.029-208 208-208m0-40C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 56C149.961 64 64 149.961 64 256s85.961 192 192 192 192-85.961 192-192S362.039 64 256 64zm0 44c19.882 0 36 16.118 36 36s-16.118 36-36 36-36-16.118-36-36 16.118-36 36-36zm117.741 98.023c-28.712 6.779-55.511 12.748-82.14 15.807.851 101.023 12.306 123.052 25.037 155.621 3.617 9.26-.957 19.698-10.217 23.315-9.261 3.617-19.699-.957-23.316-10.217-8.705-22.308-17.086-40.636-22.261-78.549h-9.686c-5.167 37.851-13.534 56.208-22.262 78.549-3.615 9.255-14.05 13.836-23.315 10.217-9.26-3.617-13.834-14.056-10.217-23.315 12.713-32.541 24.185-54.541 25.037-155.621-26.629-3.058-53.428-9.027-82.141-15.807-8.6-2.031-13.926-10.648-11.895-19.249s10.647-13.926 19.249-11.895c96.686 22.829 124.283 22.783 220.775 0 8.599-2.03 17.218 3.294 19.249 11.895 2.029 8.601-3.297 17.219-11.897 19.249z'/></svg>";
const jooa11ycontainer = document.createElement("div");
jooa11ycontainer.setAttribute("id", "jooa11y-container");
jooa11ycontainer.setAttribute("role", "region");
jooa11ycontainer.setAttribute("lang", this.options.langCode);
jooa11ycontainer.setAttribute("aria-label", Lang._('CONTAINER_LABEL'));
let loadContrastPreference =
localStorage.getItem("jooa11y-remember-contrast") === "On";
let loadLabelsPreference =
localStorage.getItem("jooa11y-remember-labels") === "On";
let loadChangeRequestPreference =
localStorage.getItem("jooa11y-remember-links-advanced") === "On";
let loadReadabilityPreference =
localStorage.getItem("jooa11y-remember-readability") === "On";
jooa11ycontainer.innerHTML =
//Main toggle button.
`<button type="button" aria-expanded="false" id="jooa11y-toggle" aria-describedby="jooa11y-notification-badge" aria-label="${Lang._('MAIN_TOGGLE_LABEL')}">
${MainToggleIcon}
<div id="jooa11y-notification-badge">
<span id="jooa11y-notification-count"></span>
</div>
</button>` +
//Start of main container.
`<div id="jooa11y-panel">` +
//Page Outline tab.
`<div id="jooa11y-outline-panel" role="tabpanel" aria-labelledby="jooa11y-outline-header">
<div id="jooa11y-outline-header" class="jooa11y-header-text">
<h2 tabindex="-1">${Lang._('PAGE_OUTLINE')}</h2>
</div>
<div id="jooa11y-outline-content">
<ul id="jooa11y-outline-list"></ul>
</div>` +
//Readability tab.
`<div id="jooa11y-readability-panel">
<div id="jooa11y-readability-content">
<h2 class="jooa11y-header-text-inline">${Lang._('READABILITY')}</h2>
<p id="jooa11y-readability-info"></p>
<ul id="jooa11y-readability-details"></ul>
</div>
</div>
</div>` + //End of Page Outline tab.
//Settings tab.
`<div id="jooa11y-settings-panel" role="tabpanel" aria-labelledby="jooa11y-settings-header">
<div id="jooa11y-settings-header" class="jooa11y-header-text">
<h2 tabindex="-1">${Lang._('SETTINGS')}</h2>
</div>
<div id="jooa11y-settings-content">
<ul id="jooa11y-settings-options">
<li>
<label id="check-contrast" for="jooa11y-contrast-toggle">${Lang._('CONTRAST')}</label>
<button id="jooa11y-contrast-toggle"
aria-labelledby="check-contrast"
class="jooa11y-settings-switch"
aria-pressed="${
loadContrastPreference ? "true" : "false"
}">${loadContrastPreference ? Lang._('ON') : Lang._('OFF')}</button>
</li>
<li>
<label id="check-labels" for="jooa11y-labels-toggle">${Lang._('FORM_LABELS')}</label>
<button id="jooa11y-labels-toggle" aria-labelledby="check-labels" class="jooa11y-settings-switch"
aria-pressed="${
loadLabelsPreference ? "true" : "false"
}">${loadLabelsPreference ? Lang._('ON') : Lang._('OFF')}</button>
</li>
<li>
<label id="check-changerequest" for="jooa11y-links-advanced-toggle">${Lang._('LINKS_ADVANCED')}<span class="jooa11y-badge">AAA</span></label>
<button id="jooa11y-links-advanced-toggle" aria-labelledby="check-changerequest" class="jooa11y-settings-switch"
aria-pressed="${
loadChangeRequestPreference ? "true" : "false"
}">${loadChangeRequestPreference ? Lang._('ON') : Lang._('OFF')}</button>
</li>
<li>
<label id="check-readability" for="jooa11y-readability-toggle">${Lang._('READABILITY')}<span class="jooa11y-badge">AAA</span></label>
<button id="jooa11y-readability-toggle" aria-labelledby="check-readability" class="jooa11y-settings-switch"
aria-pressed="${
loadReadabilityPreference ? "true" : "false"
}">${loadReadabilityPreference ? Lang._('ON') : Lang._('OFF')}</button>
</li>
<li>
<label id="dark-mode" for="jooa11y-theme-toggle">${Lang._('DARK_MODE')}</label>
<button id="jooa11y-theme-toggle" aria-labelledby="dark-mode" class="jooa11y-settings-switch"></button>
</li>
</ul>
</div>
</div>` +
//Console warning messages.
`<div id="jooa11y-panel-alert">
<div class="jooa11y-header-text">
<button id="jooa11y-close-alert" class="jooa11y-close-btn" aria-label="${Lang._('ALERT_CLOSE')}" aria-describedby="jooa11y-alert-heading jooa11y-panel-alert-text"></button>
<h2 id="jooa11y-alert-heading">${Lang._('ALERT_TEXT')}</h2>
</div>
<p id="jooa11y-panel-alert-text"></p>
<div id="jooa11y-panel-alert-preview"></div>
</div>` +
//Main panel that conveys state of page.
`<div id="jooa11y-panel-content">
<button id="jooa11y-cycle-toggle" type="button" aria-label="${Lang._('SHORTCUT_SR')}">
<div class="jooa11y-panel-icon"></div>
</button>
<div id="jooa11y-panel-text"><p id="jooa11y-status" aria-live="polite"></p></div>
</div>` +
//Show Outline & Show Settings button.
`<div id="jooa11y-panel-controls" role="tablist" aria-orientation="horizontal">
<button type="button" role="tab" aria-expanded="false" id="jooa11y-outline-toggle" aria-controls="jooa11y-outline-panel">
${Lang._('SHOW_OUTLINE')}
</button>
<button type="button" role="tab" aria-expanded="false" id="jooa11y-settings-toggle" aria-controls="jooa11y-settings-panel">
${Lang._('SHOW_SETTINGS')}
</button>
<div style="width:35px"></div>
</div>` +
//End of main container.
`</div>`;
document.body.append(jooa11ycontainer);
//Put before document.ready because of CSS flicker when dark mode is enabled.
this.settingPanelToggles();
// Preload before CheckAll function.
this.jooa11yMainToggle();
this.sanitizeHTMLandComputeARIA();
this.initializeJumpToIssueTooltip();
}
//----------------------------------------------------------------------
// Main toggle button
//----------------------------------------------------------------------
jooa11yMainToggle() {
//Keeps checker active when navigating between pages until it is toggled off.
const jooa11yToggle = document.getElementById("jooa11y-toggle");
jooa11yToggle.addEventListener('click', (e) => {
if (localStorage.getItem("jooa11y-remember-panel") === "Opened") {
localStorage.setItem("jooa11y-remember-panel", "Closed");
jooa11yToggle.classList.remove("jooa11y-on")
jooa11yToggle.setAttribute("aria-expanded", "false");
this.resetAll();
this.updateBadge();
e.preventDefault();
} else {
localStorage.setItem("jooa11y-remember-panel", "Opened");
jooa11yToggle.classList.add("jooa11y-on");
jooa11yToggle.setAttribute("aria-expanded", "true");
this.checkAll();
//Don't show badge when panel is opened.
document.getElementById("jooa11y-notification-badge").style.display = 'none';
e.preventDefault();
}
});
//Remember to leave it open
if (localStorage.getItem("jooa11y-remember-panel") === "Opened") {
jooa11yToggle.classList.add("jooa11y-on");
jooa11yToggle.setAttribute("aria-expanded", "true");
}
//Crudely give a little time to load any other content or slow post-rendered JS, iFrames, etc.
if (jooa11yToggle.classList.contains("jooa11y-on")) {
jooa11yToggle.classList.toggle("loading-jooa11y");
jooa11yToggle.setAttribute("aria-expanded", "true");
setTimeout(this.checkAll, 800);
}
//Keyboard commands
document.onkeydown = (evt) => {
evt = evt || window.event;
//Escape key to close accessibility checker panel
var isEscape = false;
if ("key" in evt) {
isEscape = (evt.key === "Escape" || evt.key === "Esc");
} else {
isEscape = (evt.keyCode === 27);
}
if (isEscape && document.getElementById("jooa11y-panel").classList.contains("jooa11y-active")) {
jooa11yToggle.setAttribute("aria-expanded", "false");
jooa11yToggle.classList.remove("jooa11y-on");
jooa11yToggle.click();
this.resetAll();
}
//Alt + A to open accessibility checker panel
if (evt.altKey && evt.code == "KeyA") {
const jooa11yToggle = document.getElementById("jooa11y-toggle");
jooa11yToggle.click();
jooa11yToggle.focus();
evt.preventDefault();
}
}
}
// ============================================================
// Helpers: Sanitize HTML and compute ARIA for hyperlinks
// ============================================================
sanitizeHTMLandComputeARIA() {
//Helper: Compute alt text on images within a text node.
this.computeTextNodeWithImage = function ($el) {
const imgArray = Array.from($el.querySelectorAll("img"));
let returnText = "";
//No image, has text.
if (imgArray.length === 0 && $el.textContent.trim().length > 1) {
returnText = $el.textContent.trim();
}
//Has image, no text.
else if (imgArray.length && $el.textContent.trim().length === 0) {
let imgalt = imgArray[0].getAttribute("alt");
if (!imgalt || imgalt === " ") {
returnText = " ";
} else if (imgalt !== undefined) {
returnText = imgalt;
}
}
//Has image and text.
//To-do: This is a hack? Any way to do this better?
else if (imgArray.length && $el.textContent.trim().length) {
imgArray.forEach(element => {
element.insertAdjacentHTML("afterend", " <span class='jooa11y-clone-image-text' aria-hidden='true'>" + imgArray[0].getAttribute("alt") + "</span> ")
});
returnText = $el.textContent.trim();
}
return returnText;
}
//Helper: Handle ARIA labels for Link Text module.
this.computeAriaLabel = function (el) {
if (el.matches("[aria-label]")) {
return el.getAttribute("aria-label");
}
else if (el.matches("[aria-labelledby]")) {
let target = el.getAttribute("aria-labelledby").split(/\s+/);
if (target.length > 0) {
let returnText = "";
target.forEach((x) => {
if (document.querySelector("#" + x) === null) {
returnText += " ";
} else {
returnText += document.querySelector("#" + x).firstChild.nodeValue + " ";
}
})
return returnText;
} else {
return "";
}
}
//Children of element.
else if (Array.from(el.children).filter(x => x.matches("[aria-label]")).length > 0) {
return Array.from(el.children)[0].getAttribute("aria-label");
}
else if (Array.from(el.children).filter(x => x.matches("[title]")).length > 0) {
return Array.from(el.children)[0].getAttribute("title");
}
else if (Array.from(el.children).filter(x => x.matches("[aria-labelledby]")).length > 0) {
let target = Array.from(el.children)[0].getAttribute("aria-labelledby").split(/\s+/);
if (target.length > 0) {
let returnText = "";
target.forEach((x) => {
if (document.querySelector("#" + x) === null) {
returnText += " ";
} else {
returnText += document.querySelector("#" + x).firstChild.nodeValue + " ";
}
})
return returnText;
} else {
return "";
}
}
else {
return "noAria";
}
};
}
//----------------------------------------------------------------------
// Setting's panel: Additional ruleset toggles.
//----------------------------------------------------------------------
settingPanelToggles() {
//Toggle: Contrast
const $jooa11yContrastCheck = document.getElementById("jooa11y-contrast-toggle");
$jooa11yContrastCheck.onclick = async () => {
if (localStorage.getItem("jooa11y-remember-contrast") === "On") {
localStorage.setItem("jooa11y-remember-contrast", "Off");
$jooa11yContrastCheck.textContent = Lang._('OFF');
$jooa11yContrastCheck.setAttribute("aria-pressed", "false");
this.resetAll(false);
await this.checkAll();
} else {
localStorage.setItem("jooa11y-remember-contrast", "On");
$jooa11yContrastCheck.textContent = Lang._('ON');
$jooa11yContrastCheck.setAttribute("aria-pressed", "true");
this.resetAll(false);
await this.checkAll();
}
};
//Toggle: Form labels
const $jooa11yLabelsCheck = document.getElementById("jooa11y-labels-toggle");
$jooa11yLabelsCheck.onclick = async () => {
if (localStorage.getItem("jooa11y-remember-labels") === "On") {
localStorage.setItem("jooa11y-remember-labels", "Off");
$jooa11yLabelsCheck.textContent = Lang._('OFF');
$jooa11yLabelsCheck.setAttribute("aria-pressed", "false");
this.resetAll(false);
await this.checkAll();
} else {
localStorage.setItem("jooa11y-remember-labels", "On");
$jooa11yLabelsCheck.textContent = Lang._('ON');
$jooa11yLabelsCheck.setAttribute("aria-pressed", "true");
this.resetAll(false);
await this.checkAll();
}
};
//Toggle: Links (Advanced)
const $jooa11yChangeRequestCheck = document.getElementById("jooa11y-links-advanced-toggle");
$jooa11yChangeRequestCheck.onclick = async () => {
if (localStorage.getItem("jooa11y-remember-links-advanced") === "On") {
localStorage.setItem("jooa11y-remember-links-advanced", "Off");
$jooa11yChangeRequestCheck.textContent = Lang._('OFF');
$jooa11yChangeRequestCheck.setAttribute("aria-pressed", "false");
this.resetAll(false);
await this.checkAll();
} else {
localStorage.setItem("jooa11y-remember-links-advanced", "On");
$jooa11yChangeRequestCheck.textContent = Lang._('ON');
$jooa11yChangeRequestCheck.setAttribute("aria-pressed", "true");
this.resetAll(false);
await this.checkAll();
}
};
//Toggle: Readability
const $jooa11yReadabilityCheck = document.getElementById("jooa11y-readability-toggle");
$jooa11yReadabilityCheck.onclick = async () => {
if (localStorage.getItem("jooa11y-remember-readability") === "On") {
localStorage.setItem("jooa11y-remember-readability", "Off");
$jooa11yReadabilityCheck.textContent = Lang._('OFF');
$jooa11yReadabilityCheck.setAttribute("aria-pressed", "false");
document.getElementById("jooa11y-readability-panel").classList.remove("jooa11y-active");
this.resetAll(false);
await this.checkAll();
} else {
localStorage.setItem("jooa11y-remember-readability", "On");
$jooa11yReadabilityCheck.textContent = Lang._('ON');
$jooa11yReadabilityCheck.setAttribute("aria-pressed", "true");
document.getElementById("jooa11y-readability-panel").classList.add("jooa11y-active");
this.resetAll(false);
await this.checkAll();
}
};
if (localStorage.getItem("jooa11y-remember-readability") === "On") {
document.getElementById("jooa11y-readability-panel").classList.add("jooa11y-active");
}
//Toggle: Dark mode. (Credits: https://derekkedziora.com/blog/dark-mode-revisited)
let systemInitiatedDark = window.matchMedia(
"(prefers-color-scheme: dark)"
);
const $jooa11yTheme = document.getElementById("jooa11y-theme-toggle");
const html = document.querySelector("html");
const theme = localStorage.getItem("jooa11y-remember-theme");
if (systemInitiatedDark.matches) {
$jooa11yTheme.textContent = Lang._('ON');
$jooa11yTheme.setAttribute("aria-pressed", "true");
} else {
$jooa11yTheme.textContent = Lang._('OFF');
$jooa11yTheme.setAttribute("aria-pressed", "false");
}
function prefersColorTest(systemInitiatedDark) {
if (systemInitiatedDark.matches) {
html.setAttribute("data-jooa11y-theme", "dark");
$jooa11yTheme.textContent = Lang._('ON');
$jooa11yTheme.setAttribute("aria-pressed", "true");
localStorage.setItem("jooa11y-remember-theme", "");
} else {
html.setAttribute("data-jooa11y-theme", "light");
$jooa11yTheme.textContent = Lang._('OFF');
$jooa11yTheme.setAttribute("aria-pressed", "false");
localStorage.setItem("jooa11y-remember-theme", "");
}
}
systemInitiatedDark.addEventListener('change', prefersColorTest);
$jooa11yTheme.onclick = async () => {
const theme = localStorage.getItem("jooa11y-remember-theme");
if (theme === "dark") {
html.setAttribute("data-jooa11y-theme", "light");
localStorage.setItem("jooa11y-remember-theme", "light");
$jooa11yTheme.textContent = Lang._('OFF');
$jooa11yTheme.setAttribute("aria-pressed", "false");
} else if (theme === "light") {
html.setAttribute("data-jooa11y-theme", "dark");
localStorage.setItem("jooa11y-remember-theme", "dark");
$jooa11yTheme.textContent = Lang._('ON');
$jooa11yTheme.setAttribute("aria-pressed", "true");
} else if (systemInitiatedDark.matches) {
html.setAttribute("data-jooa11y-theme", "light");
localStorage.setItem("jooa11y-remember-theme", "light");
$jooa11yTheme.textContent = Lang._('OFF');
$jooa11yTheme.setAttribute("aria-pressed", "false");
} else {
html.setAttribute("data-jooa11y-theme", "dark");
localStorage.setItem("jooa11y-remember-theme", "dark");
$jooa11yTheme.textContent = Lang._('OFF');
$jooa11yTheme.setAttribute("aria-pressed", "true");
}
};
if (theme === "dark") {
html.setAttribute("data-jooa11y-theme", "dark");
localStorage.setItem("jooa11y-remember-theme", "dark");
$jooa11yTheme.textContent = Lang._('ON');
$jooa11yTheme.setAttribute("aria-pressed", "true");
} else if (theme === "light") {
html.setAttribute("data-jooa11y-theme", "light");
localStorage.setItem("jooa11y-remember-theme", "light");
$jooa11yTheme.textContent = Lang._('OFF');
$jooa11yTheme.setAttribute("aria-pressed", "false");
}
}
//----------------------------------------------------------------------
// Tooltip for Jump-to-Issue button.
//----------------------------------------------------------------------
initializeJumpToIssueTooltip() {
tippy('#jooa11y-cycle-toggle', {
content: `<div style="text-align:center">${Lang._('SHORTCUT_TOOLTIP')} »<br><span class="jooa11y-shortcut-icon"></span></div>`,
allowHTML: true,
delay: [900, 0],
trigger: "mouseenter focusin",
arrow: true,
placement: 'top',
theme: "jooa11y-theme",
aria: {
content: null,
expanded: false,
},
appendTo: document.body,
});
}
// ----------------------------------------------------------------------
// Do Initial check
// ----------------------------------------------------------------------
doInitialCheck() {
if (localStorage.getItem("jooa11y-remember-panel") === "Closed" || !localStorage.getItem("jooa11y-remember-panel")) {
this.panelActive = true; // Prevent panel popping up after initial check
this.checkAll();
}
}
// ----------------------------------------------------------------------
// Check all
// ----------------------------------------------------------------------
checkAll = async () => {
this.errorCount = 0;
this.warningCount = 0;
this.$root = document.querySelector(this.options.checkRoot);
this.findElements();
//Ruleset checks
this.checkHeaders();
this.checkLinkText();
this.checkUnderline();
this.checkAltText();
if (localStorage.getItem("jooa11y-remember-contrast") === "On") {
this.checkContrast();
}
if (localStorage.getItem("jooa11y-remember-labels") === "On") {
this.checkLabels();
}
if (localStorage.getItem("jooa11y-remember-links-advanced") === "On") {
this.checkLinksAdvanced();
}
if (localStorage.getItem("jooa11y-remember-readability") === "On") {
this.checkReadability();
}
this.checkEmbeddedContent();
this.checkQA();
//Update panel
if (this.panelActive) {
this.resetAll();
} else {
this.updatePanel();
}
this.initializeTooltips();
this.detectOverflow();
this.nudge();
//Don't show badge when panel is opened.
if (!document.getElementsByClassName('jooa11y-on').length) {
this.updateBadge();
}
};
// ============================================================
// Reset all
// ============================================================
resetAll (restartPanel = true) {
this.panelActive = false;
this.clearEverything();
//Remove eventListeners on the Show Outline and Show Panel toggles.
const $outlineToggle = document.getElementById("jooa11y-outline-toggle");
const resetOutline = $outlineToggle.cloneNode(true);
$outlineToggle.parentNode.replaceChild(resetOutline, $outlineToggle);
const $settingsToggle = document.getElementById("jooa11y-settings-toggle");
const resetSettings = $settingsToggle.cloneNode(true);
$settingsToggle.parentNode.replaceChild(resetSettings, $settingsToggle);
//Errors
document.querySelectorAll('.jooa11y-error-border').forEach((el) => el.classList.remove('jooa11y-error-border'));
document.querySelectorAll('.jooa11y-error-text').forEach((el) => el.classList.remove('jooa11y-error-text'));
//Warnings
document.querySelectorAll('.jooa11y-warning-border').forEach((el) => el.classList.remove('jooa11y-warning-border'));
document.querySelectorAll('.jooa11y-warning-text').forEach((el) => el.classList.remove('jooa11y-warning-text'));
document.querySelectorAll('p').forEach((el) => el.classList.remove('jooa11y-fake-list'));
let allcaps = document.querySelectorAll('.jooa11y-warning-uppercase');
allcaps.forEach(el => el.outerHTML = el.innerHTML);
//Good
document.querySelectorAll('.jooa11y-good-border').forEach((el) => el.classList.remove('jooa11y-good-border'));
document.querySelectorAll('.jooa11y-good-text').forEach((el) => el.classList.remove('jooa11y-good-text'));
//Remove
document.querySelectorAll(`
.jooa11y-instance,
.jooa11y-instance-inline,
.jooa11y-heading-label,
#jooa11y-outline-list li,
.jooa11y-readability-period,
#jooa11y-readability-info span,
#jooa11y-readability-details li,
.jooa11y-clone-image-text
`).forEach(el => el.parentNode.removeChild(el));
//Etc
document.querySelectorAll('.jooa11y-overflow').forEach((el) => el.classList.remove('jooa11y-overflow'));
document.querySelectorAll('.jooa11y-fake-heading').forEach((el) => el.classList.remove('jooa11y-fake-heading'));
document.querySelectorAll('.jooa11y-pulse-border').forEach((el) => el.classList.remove('jooa11y-pulse-border'));
document.querySelector('#jooa11y-panel-alert').classList.remove("jooa11y-active")
var empty = document.querySelector('#jooa11y-panel-alert-text');
while(empty.firstChild) empty.removeChild(empty.firstChild);
var clearStatus = document.querySelector('#jooa11y-status');
while(clearStatus.firstChild) clearStatus.removeChild(clearStatus.firstChild)
if (restartPanel) {
document.querySelector('#jooa11y-panel').classList.remove("jooa11y-active");
}
};
clearEverything () {};
// ============================================================
// Initialize tooltips for error/warning/pass buttons: (Tippy.js)
// Although you can also swap this with Bootstrap's tooltip library for example.
// ============================================================
initializeTooltips () {
tippy(".jooa11y-btn", {
interactive: true,
trigger: "mouseenter click focusin", //Focusin trigger to ensure "Jump to issue" button displays tooltip.
arrow: true,
delay: [200, 0], //Slight delay to ensure mouse doesn't quickly trigger and hide tooltip.
theme: "jooa11y-theme",
placement: 'bottom',
allowHTML: true,
aria: {
content: 'describedby',
},
appendTo: document.body,
});
}
// ============================================================
// Detect parent containers that have hidden overflow.
// ============================================================
detectOverflow () {
const findParentWithOverflow = ($el, property, value) => {
while($el !== null) {
const style = window.getComputedStyle($el);
const propValue = style.getPropertyValue(property);
if (propValue === value) {
return $el;
}
$el = $el.parentElement;
}
return null;
};
const $findButtons = document.querySelectorAll('.jooa11y-btn');
$findButtons.forEach(function ($el) {
const overflowing = findParentWithOverflow($el, 'overflow', 'hidden');
if (overflowing !== null) {
overflowing.classList.add('jooa11y-overflow');
}
});
}
// ============================================================
// Nudge buttons if they overlap.
// ============================================================
nudge = () => {
const jooa11yInstance = document.querySelectorAll('.jooa11y-instance, .jooa11y-instance-inline');
jooa11yInstance.forEach(($el) => {
const sibling = $el.nextElementSibling;
if (sibling !== null && (sibling.classList.contains("jooa11y-instance") ||
sibling.classList.contains("jooa11y-instance-inline"))) {
sibling.querySelector("button").setAttribute("style", "margin: -10px -20px !important;");
}
});
}
// ============================================================
// Update iOS style notification badge on icon.
// ============================================================
updateBadge () {
let totalCount = this.errorCount + this.warningCount;
const notifBadge = document.getElementById("jooa11y-notification-badge");
if (totalCount === 0) {
notifBadge.style.display = "none";
} else {
notifBadge.style.display = "flex";
document.getElementById('jooa11y-notification-count').innerHTML = Lang.sprintf('PANEL_STATUS_ICON', totalCount);
}
}
// ----------------------------------------------------------------------
// Main panel: Display and update panel.
// ----------------------------------------------------------------------
updatePanel () {
this.panelActive = true;
let totalCount = this.errorCount + this.warningCount;
this.buildPanel();
this.skipToIssue();
const $jooa11ySkipBtn = document.getElementById("jooa11y-cycle-toggle");
$jooa11ySkipBtn.disabled = false;
$jooa11ySkipBtn.setAttribute("style", "cursor: pointer !important;");
const $jooa11yPanel = document.getElementById("jooa11y-panel");
$jooa11yPanel.classList.add("jooa11y-active");
const $panelContent = document.getElementById("jooa11y-panel-content");
const $jooa11yStatus = document.getElementById("jooa11y-status");
const $findButtons = document.querySelectorAll('.jooa11y-btn');
if (this.errorCount > 0 && this.warningCount > 0) {
$panelContent.setAttribute("class", "jooa11y-errors");
$jooa11yStatus.textContent = Lang.sprintf('PANEL_STATUS_BOTH', this.errorCount, this.warningCount);
}
else if (this.errorCount > 0) {
$panelContent.setAttribute("class", "jooa11y-errors");
$jooa11yStatus.textContent = Lang.sprintf('PANEL_STATUS_ERRORS', this.errorCount);
}
else if (this.warningCount > 0) {
$panelContent.setAttribute("class", "jooa11y-warnings");
$jooa11yStatus.textContent = Lang.sprintf('PANEL_STATUS_WARNINGS', this.warningCount);
}
else {
$panelContent.setAttribute("class", "jooa11y-good");
$jooa11yStatus.textContent = Lang._('PANEL_STATUS_NONE');
if ($findButtons.length === 0) {
$jooa11ySkipBtn.disabled = true;
$jooa11ySkipBtn.setAttribute("style", "cursor: default !important;");
}
}
};
// ----------------------------------------------------------------------
// Main panel: Build Show Outline and Settings tabs.
// ----------------------------------------------------------------------
buildPanel = () => {
const $outlineToggle = document.getElementById("jooa11y-outline-toggle");
const $outlinePanel = document.getElementById("jooa11y-outline-panel");
const $outlineList = document.getElementById("jooa11y-outline-list");
const $settingsToggle = document.getElementById("jooa11y-settings-toggle");
const $settingsPanel = document.getElementById("jooa11y-settings-panel");
const $settingsContent = document.getElementById("jooa11y-settings-content");
const $headingAnnotations = document.querySelectorAll(".jooa11y-heading-label");
//Show outline panel
$outlineToggle.addEventListener('click', () => {
if ($outlineToggle.getAttribute("aria-expanded") === "true") {
$outlineToggle.classList.remove("jooa11y-outline-active");
$outlinePanel.classList.remove("jooa11y-active");
$outlineToggle.textContent = Lang._('SHOW_OUTLINE');
$outlineToggle.setAttribute("aria-expanded", "false");
localStorage.setItem("jooa11y-remember-outline", "Closed");
} else {
$outlineToggle.classList.add("jooa11y-outline-active");
$outlinePanel.classList.add("jooa11y-active");
$outlineToggle.textContent = Lang._('HIDE_OUTLINE');
$outlineToggle.setAttribute("aria-expanded", "true");
localStorage.setItem("jooa11y-remember-outline", "Opened");
}
//Set focus on Page Outline heading for accessibility.
document.querySelector("#jooa11y-outline-header > h2").focus();
//Show heading level annotations.
$headingAnnotations.forEach(($el) => $el.classList.toggle("jooa11y-label-visible"));
//Close Settings panel when Show Outline is active.
$settingsPanel.classList.remove("jooa11y-active");
$settingsToggle.classList.remove("jooa11y-settings-active");
$settingsToggle.setAttribute("aria-expanded", "false");
$settingsToggle.textContent = Lang._('SHOW_SETTINGS');
//Keyboard accessibility fix for scrollable panel content.
if ($outlineList.clientHeight > 250) {
$outlineList.setAttribute("tabindex", "0");
}
});
//Remember to leave outline open
if (localStorage.getItem("jooa11y-remember-outline") === "Opened") {
$outlineToggle.classList.add("jooa11y-outline-active");
$outlinePanel.classList.add("jooa11y-active");
$outlineToggle.textContent = Lang._('HIDE_OUTLINE');
$outlineToggle.setAttribute("aria-expanded", "true");
$headingAnnotations.forEach(($el) => $el.classList.toggle("jooa11y-label-visible"));
//Keyboard accessibility fix for scrollable panel content.
if ($outlineList.clientHeight > 250) {
$outlineList.setAttribute("tabindex", "0");
}
}
//Show settings panel
$settingsToggle.addEventListener('click', () => {
if ($settingsToggle.getAttribute("aria-expanded") === "true") {
$settingsToggle.classList.remove("jooa11y-settings-active");
$settingsPanel.classList.remove("jooa11y-active");
$settingsToggle.textContent = Lang._('SHOW_SETTINGS');
$settingsToggle.setAttribute("aria-expanded", "false");
} else {
$settingsToggle.classList.add("jooa11y-settings-active");
$settingsPanel.classList.add("jooa11y-active");
$settingsToggle.textContent = Lang._('HIDE_SETTINGS');
$settingsToggle.setAttribute("aria-expanded", "true");
}
//Set focus on Settings heading for accessibility.
document.querySelector("#jooa11y-settings-header > h2").focus();
//Close Show Outline panel when Settings is active.
$outlinePanel.classList.remove("jooa11y-active");
$outlineToggle.classList.remove("jooa11y-outline-active");
$outlineToggle.setAttribute("aria-expanded", "false");
$outlineToggle.textContent = Lang._('SHOW_OUTLINE');
$headingAnnotations.forEach(($el) => $el.classList.remove("jooa11y-label-visible"));
localStorage.setItem("jooa11y-remember-outline", "Closed");
//Keyboard accessibility fix for scrollable panel content.
if ($settingsContent.clientHeight > 350) {
$settingsContent.setAttribute("tabindex", "0");
}
});
//Enhanced keyboard accessibility for panel.
document.getElementById('jooa11y-panel-controls').addEventListener('keydown', function(e) {
const $tab = document.querySelectorAll('#jooa11y-outline-toggle[role=tab], #jooa11y-settings-toggle[role=tab]');
if (e.key === 'ArrowRight') {
for (let i = 0; i < $tab.length; i++) {
if ($tab[i].getAttribute('aria-expanded') === "true" || $tab[i].getAttribute('aria-expanded') === "false") {
$tab[i+1].focus();
e.preventDefault();
break;
}
}
}
if (e.key === 'ArrowDown') {
for (let i = 0; i < $tab.length; i++) {
if ($tab[i].getAttribute('aria-expanded') === "true" || $tab[i].getAttribute('aria-expanded') === "false") {
$tab[i+1].focus();
e.preventDefault();
break;
}
}
}
if (e.key === 'ArrowLeft') {
for (let i = $tab.length-1; i > 0; i--) {
if ($tab[i].getAttribute('aria-expanded') === "true" || $tab[i].getAttr