one-click-accessibility/modules/scanner/assets/js/utils/validate-headings.js
VasylD f0dce2793e
[APP-1923][APP-1924] manage remediations, global remediations (#424)
* [APP-1923] change remediation management (#411)

* [APP-1988] Add tabs

* [APP-1988][APP-1989] add tabs, remove menu item

* [APP-1988][APP-1989] add tabs, remove menu item

* [APP-1992] add empty state and tab navigation

* [APP-1993][APP-1994] Change view for manage AI fixes, change delete confirmation modal

* [APP-1996] add view for alt text fixes

* [APP-1996] add edit for alt text fixes

* [APP-1996] add edit for alt text fixes

* [APP-1996] add edit for alt text fixes

* [APP-1996] add edit for alt text fixes

* [APP-1996] add edit for alt text fixes

* [APP-1995] Color contrast form

* [APP-2001] add global remediation logic (#415)

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2001] add global remediation logic

* [APP-2003] add disable across scans (#418)

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* [APP-2003] add disable across scans

* add logs

* add logs

* add logs

* remove logs

* remove logs

* remove logs

* remove logs

* remove logs

* remove logs

* remove logs

* remove logs

* remove logs

* remove logs

* change to theme value

* change to theme value

* change to theme value

* change to theme value

* add edit mode

* add edit mode

* add edit mode
2025-11-10 16:25:45 +07:00

102 lines
2.7 KiB
JavaScript

import {
getHeadingXpath,
toFlatTree,
} from '@ea11y-apps/scanner/utils/page-headings';
import { EA11Y_RULES } from '../rules';
import { HEADING_STATUS } from '../types/heading';
/**
* Validates the heading tree and mutates it to show notices in the UI.
*
* @param {import('../types/heading').Ea11yHeading[]} headingTree
* @param {string[]} dismissedHeadingIssues
* @return {import('../types/heading').Ea11yHeading[]} Validated heading tree.
*/
export const validateHeadings = (headingTree, dismissedHeadingIssues) => {
if (!headingTree.length) {
return headingTree;
}
const clone = [...headingTree];
const flatHeadings = toFlatTree(clone).filter(
(heading) =>
!['none', 'presentation'].includes(heading.node.getAttribute('role')),
);
const h1Titles = flatHeadings.filter((heading) => 1 === heading.level);
if (h1Titles.length > 1) {
h1Titles.forEach((heading) => {
heading.status = HEADING_STATUS.ERROR;
heading.violationCode = EA11Y_RULES.REDUNDANT_H1_TAGS;
});
}
if (!h1Titles.length) {
clone[0].status = HEADING_STATUS.ERROR;
clone[0].violationCode = EA11Y_RULES.MISSING_H1_TAG;
}
validateHierarchy(clone, dismissedHeadingIssues);
return clone;
};
/**
* @param {import('../types/heading').Ea11yHeading[]} headings
* @param {string[]} dismissedHeadingIssues
* @param {number} parentLevel
*/
const validateHierarchy = (
headings,
dismissedHeadingIssues = [],
parentLevel = 0,
) => {
let previousLevel = parentLevel;
headings.forEach((heading) => {
const currentLevel = heading.level;
if (previousLevel > 0 && currentLevel > previousLevel + 1) {
const xpath = getHeadingXpath(heading.node);
if (
// Important to not overwrite HEADING_STATUS.ERROR
heading.status === HEADING_STATUS.SUCCESS &&
!dismissedHeadingIssues.includes(xpath)
) {
heading.status = HEADING_STATUS.WARNING;
heading.violationCode = EA11Y_RULES.INCORRECT_HEADING_HIERARCHY;
}
}
previousLevel = currentLevel;
if (heading.children && heading.children.length > 0) {
validateHierarchy(heading.children, dismissedHeadingIssues, currentLevel);
}
});
};
/**
* Returns heading validation stats.
*
* @param {import('../types/heading').Ea11yHeading[]} headings Validated heading tree.
* @return {import('../types/heading').Ea11yHeadingStats} Validation stats.
*/
export const calculateStats = (headings) => {
const flatHeadings = toFlatTree(headings);
const output = {
total: flatHeadings.length,
[HEADING_STATUS.SUCCESS]: 0,
[HEADING_STATUS.WARNING]: 0,
[HEADING_STATUS.ERROR]: 0,
};
flatHeadings.forEach((heading) => {
const status = heading.status;
output[status]++;
});
return output;
};