|
@@ -1,7 +1,8 @@
|
|
|
-import axe, { ElementContext, NodeResult, Result, RunOptions } from 'axe-core';
|
|
|
+import axe from 'axe-core';
|
|
|
|
|
|
import A11yDialog from 'a11y-dialog';
|
|
|
import { Application } from '@hotwired/stimulus';
|
|
|
+import { getAxeConfiguration, renderA11yResults } from './a11y-result';
|
|
|
import { DialogController } from '../controllers/DialogController';
|
|
|
import { TeleportController } from '../controllers/TeleportController';
|
|
|
|
|
@@ -14,35 +15,6 @@ This component implements a roving tab index for keyboard navigation
|
|
|
Learn more about roving tabIndex: https:
|
|
|
*/
|
|
|
|
|
|
-
|
|
|
- * Wagtail's Axe configuration object. This should reflect what's returned by
|
|
|
- * `wagtail.admin.userbar.AccessibilityItem.get_axe_configuration()`.
|
|
|
- */
|
|
|
-interface WagtailAxeConfiguration {
|
|
|
- context: ElementContext;
|
|
|
- options: RunOptions;
|
|
|
- messages: Record<string, string>;
|
|
|
-}
|
|
|
-
|
|
|
-const sortAxeNodes = (nodeResultA?: NodeResult, nodeResultB?: NodeResult) => {
|
|
|
- if (!nodeResultA || !nodeResultB) return 0;
|
|
|
- const nodeA = document.querySelector<HTMLElement>(nodeResultA.target[0]);
|
|
|
- const nodeB = document.querySelector<HTMLElement>(nodeResultB.target[0]);
|
|
|
- if (!nodeA || !nodeB) return 0;
|
|
|
-
|
|
|
-
|
|
|
- return nodeA.compareDocumentPosition(nodeB) & Node.DOCUMENT_POSITION_PRECEDING
|
|
|
- ? 1
|
|
|
- : -1;
|
|
|
-};
|
|
|
-
|
|
|
-export const sortAxeViolations = (violations: Result[]) =>
|
|
|
- violations.sort((violationA, violationB) => {
|
|
|
- const earliestNodeA = violationA.nodes.sort(sortAxeNodes)[0];
|
|
|
- const earliestNodeB = violationB.nodes.sort(sortAxeNodes)[0];
|
|
|
- return sortAxeNodes(earliestNodeA, earliestNodeB);
|
|
|
- });
|
|
|
-
|
|
|
export class Userbar extends HTMLElement {
|
|
|
declare trigger: HTMLElement;
|
|
|
|
|
@@ -329,33 +301,13 @@ export class Userbar extends HTMLElement {
|
|
|
See documentation: https:
|
|
|
*/
|
|
|
|
|
|
- getAxeConfiguration(): WagtailAxeConfiguration | null {
|
|
|
- const script = this.shadowRoot?.querySelector<HTMLScriptElement>(
|
|
|
- '#accessibility-axe-configuration',
|
|
|
- );
|
|
|
-
|
|
|
- if (!script || !script.textContent) return null;
|
|
|
-
|
|
|
- try {
|
|
|
- return JSON.parse(script.textContent);
|
|
|
- } catch (err) {
|
|
|
-
|
|
|
- console.error('Error loading Axe config');
|
|
|
-
|
|
|
- console.error(err);
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
|
|
|
async initialiseAxe() {
|
|
|
const accessibilityTrigger = this.shadowRoot?.getElementById(
|
|
|
'accessibility-trigger',
|
|
|
);
|
|
|
|
|
|
- const config = this.getAxeConfiguration();
|
|
|
+ const config = getAxeConfiguration(this.shadowRoot);
|
|
|
|
|
|
if (!this.shadowRoot || !accessibilityTrigger || !config) return;
|
|
|
|
|
@@ -432,136 +384,83 @@ export class Userbar extends HTMLElement {
|
|
|
innerErrorBadges.forEach((badge) => {
|
|
|
|
|
|
badge.textContent = String(a11yErrorsNumber) || '0';
|
|
|
- if (results.violations.length) {
|
|
|
- badge.classList.add('has-errors');
|
|
|
- } else {
|
|
|
- badge.classList.remove('has-errors');
|
|
|
- }
|
|
|
+ badge.classList.toggle('has-errors', results.violations.length > 0);
|
|
|
});
|
|
|
|
|
|
- const showAxeResults = () => {
|
|
|
- modal.show();
|
|
|
-
|
|
|
- modalBody.innerHTML = '';
|
|
|
-
|
|
|
- if (results.violations.length) {
|
|
|
- const sortedViolations = sortAxeViolations(results.violations);
|
|
|
- sortedViolations.forEach((violation, violationIndex) => {
|
|
|
- modalBody.appendChild(a11yRowTemplate.content.cloneNode(true));
|
|
|
- const currentA11yRow = modalBody.querySelectorAll<HTMLDivElement>(
|
|
|
- '[data-a11y-result-row]',
|
|
|
- )[violationIndex];
|
|
|
-
|
|
|
- const a11yErrorName = currentA11yRow.querySelector(
|
|
|
- '[data-a11y-result-name]',
|
|
|
- ) as HTMLSpanElement;
|
|
|
- a11yErrorName.id = `w-a11y-result__name-${violationIndex}`;
|
|
|
-
|
|
|
-
|
|
|
- a11yErrorName.textContent =
|
|
|
- config.messages[violation.id] || violation.help;
|
|
|
- const a11yErrorCount = currentA11yRow.querySelector(
|
|
|
- '[data-a11y-result-count]',
|
|
|
- ) as HTMLSpanElement;
|
|
|
- a11yErrorCount.textContent = `${violation.nodes.length}`;
|
|
|
-
|
|
|
- const a11yErrorContainer = currentA11yRow.querySelector(
|
|
|
- '[data-a11y-result-container]',
|
|
|
- ) as HTMLDivElement;
|
|
|
-
|
|
|
- violation.nodes.forEach((node, nodeIndex) => {
|
|
|
- a11yErrorContainer.appendChild(
|
|
|
- a11ySelectorTemplate.content.cloneNode(true),
|
|
|
- );
|
|
|
- const currentA11ySelector =
|
|
|
- a11yErrorContainer.querySelectorAll<HTMLButtonElement>(
|
|
|
- '[data-a11y-result-selector]',
|
|
|
- )[nodeIndex];
|
|
|
-
|
|
|
- currentA11ySelector.setAttribute(
|
|
|
- 'aria-describedby',
|
|
|
- a11yErrorName.id,
|
|
|
- );
|
|
|
- const currentA11ySelectorText = currentA11ySelector.querySelector(
|
|
|
- '[data-a11y-result-selector-text]',
|
|
|
- ) as HTMLSpanElement;
|
|
|
- const selectorName = node.target[0];
|
|
|
-
|
|
|
- currentA11ySelectorText.textContent = selectorName.replace(
|
|
|
- /\[data-block-key="\w{5}"\]/,
|
|
|
- '',
|
|
|
- );
|
|
|
- currentA11ySelector.addEventListener('click', () => {
|
|
|
- const inaccessibleElement =
|
|
|
- document.querySelector<HTMLElement>(selectorName);
|
|
|
- const a11yOutlineContainer =
|
|
|
- this.shadowRoot?.querySelector<HTMLElement>(
|
|
|
- '[data-a11y-result-outline-container]',
|
|
|
- );
|
|
|
- if (a11yOutlineContainer?.firstElementChild) {
|
|
|
- a11yOutlineContainer.removeChild(
|
|
|
- a11yOutlineContainer.firstElementChild,
|
|
|
- );
|
|
|
- }
|
|
|
- a11yOutlineContainer?.appendChild(
|
|
|
- a11yOutlineTemplate.content.cloneNode(true),
|
|
|
- );
|
|
|
- const currentA11yOutline =
|
|
|
- this.shadowRoot?.querySelector<HTMLElement>(
|
|
|
- '[data-a11y-result-outline]',
|
|
|
- );
|
|
|
- if (
|
|
|
- !this.shadowRoot ||
|
|
|
- !inaccessibleElement ||
|
|
|
- !currentA11yOutline ||
|
|
|
- !a11yOutlineContainer
|
|
|
- )
|
|
|
- return;
|
|
|
-
|
|
|
- const styleA11yOutline = () => {
|
|
|
- const rect = inaccessibleElement.getBoundingClientRect();
|
|
|
- currentA11yOutline.style.cssText = `
|
|
|
- top: ${
|
|
|
- rect.height < 5
|
|
|
- ? `${rect.top + window.scrollY - 2.5}px`
|
|
|
- : `${rect.top + window.scrollY}px`
|
|
|
- };
|
|
|
- left: ${
|
|
|
- rect.width < 5
|
|
|
- ? `${rect.left + window.scrollX - 2.5}px`
|
|
|
- : `${rect.left + window.scrollX}px`
|
|
|
- };
|
|
|
- width: ${Math.max(rect.width, 5)}px;
|
|
|
- height: ${Math.max(rect.height, 5)}px;
|
|
|
- position: absolute;
|
|
|
- z-index: 129;
|
|
|
- outline: 1px solid #CD4444;
|
|
|
- box-shadow: 0px 0px 12px 1px #FF0000;
|
|
|
- `;
|
|
|
- };
|
|
|
-
|
|
|
- styleA11yOutline();
|
|
|
-
|
|
|
- window.addEventListener('resize', styleA11yOutline);
|
|
|
-
|
|
|
- inaccessibleElement.style.scrollMargin = '6.25rem';
|
|
|
- inaccessibleElement.scrollIntoView();
|
|
|
- inaccessibleElement.focus();
|
|
|
-
|
|
|
- accessibilityResultsBox.addEventListener('hide', () => {
|
|
|
- currentA11yOutline.style.cssText = '';
|
|
|
-
|
|
|
- window.removeEventListener('resize', styleA11yOutline);
|
|
|
- });
|
|
|
- });
|
|
|
- });
|
|
|
- });
|
|
|
+ const onClickSelector = (selectorName: string) => {
|
|
|
+ const inaccessibleElement =
|
|
|
+ document.querySelector<HTMLElement>(selectorName);
|
|
|
+ const a11yOutlineContainer = this.shadowRoot?.querySelector<HTMLElement>(
|
|
|
+ '[data-a11y-result-outline-container]',
|
|
|
+ );
|
|
|
+ if (a11yOutlineContainer?.firstElementChild) {
|
|
|
+ a11yOutlineContainer.removeChild(
|
|
|
+ a11yOutlineContainer.firstElementChild,
|
|
|
+ );
|
|
|
}
|
|
|
+ a11yOutlineContainer?.appendChild(
|
|
|
+ a11yOutlineTemplate.content.cloneNode(true),
|
|
|
+ );
|
|
|
+ const currentA11yOutline = this.shadowRoot?.querySelector<HTMLElement>(
|
|
|
+ '[data-a11y-result-outline]',
|
|
|
+ );
|
|
|
+ if (
|
|
|
+ !this.shadowRoot ||
|
|
|
+ !inaccessibleElement ||
|
|
|
+ !currentA11yOutline ||
|
|
|
+ !a11yOutlineContainer
|
|
|
+ )
|
|
|
+ return;
|
|
|
+
|
|
|
+ const styleA11yOutline = () => {
|
|
|
+ const rect = inaccessibleElement.getBoundingClientRect();
|
|
|
+ currentA11yOutline.style.cssText = `
|
|
|
+ top: ${
|
|
|
+ rect.height < 5
|
|
|
+ ? `${rect.top + window.scrollY - 2.5}px`
|
|
|
+ : `${rect.top + window.scrollY}px`
|
|
|
+ };
|
|
|
+ left: ${
|
|
|
+ rect.width < 5
|
|
|
+ ? `${rect.left + window.scrollX - 2.5}px`
|
|
|
+ : `${rect.left + window.scrollX}px`
|
|
|
+ };
|
|
|
+ width: ${Math.max(rect.width, 5)}px;
|
|
|
+ height: ${Math.max(rect.height, 5)}px;
|
|
|
+ position: absolute;
|
|
|
+ z-index: 129;
|
|
|
+ outline: 1px solid #CD4444;
|
|
|
+ box-shadow: 0px 0px 12px 1px #FF0000;
|
|
|
+ `;
|
|
|
+ };
|
|
|
+
|
|
|
+ styleA11yOutline();
|
|
|
+
|
|
|
+ window.addEventListener('resize', styleA11yOutline);
|
|
|
+
|
|
|
+ inaccessibleElement.style.scrollMargin = '6.25rem';
|
|
|
+ inaccessibleElement.scrollIntoView();
|
|
|
+ inaccessibleElement.focus();
|
|
|
+
|
|
|
+ accessibilityResultsBox.addEventListener('hide', () => {
|
|
|
+ currentA11yOutline.style.cssText = '';
|
|
|
+
|
|
|
+ window.removeEventListener('resize', styleA11yOutline);
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
const toggleAxeResults = () => {
|
|
|
if (accessibilityResultsBox.getAttribute('aria-hidden') === 'true') {
|
|
|
- showAxeResults();
|
|
|
+ modal.show();
|
|
|
+
|
|
|
+ renderA11yResults(
|
|
|
+ modalBody,
|
|
|
+ results,
|
|
|
+ config,
|
|
|
+ a11yRowTemplate,
|
|
|
+ a11ySelectorTemplate,
|
|
|
+ onClickSelector,
|
|
|
+ );
|
|
|
} else {
|
|
|
modal.hide();
|
|
|
}
|