one-click-accessibility/modules/scanner/assets/js/rules/heading-hierarchy-check.js
VasylD ad2174ffe3
[APP-1792] Add headings structure (#406)
* New: Add the heading structure components [APP-1792] (#391)

* New: Add the new rules [APP-1792]

* New: Add the BE integration [APP-1792]

* Fix: Fix QA issues [APP-1792]

* merge latest dev

* merge latest dev

* merge latest dev

* merge latest dev

* merge latest dev

* merge latest dev

* add reset to initial

---------

Co-authored-by: Pavlo Kniazevych <139438463+pkniazevych@users.noreply.github.com>
Co-authored-by: Kniazevych <pavlok@elementor.red>
2025-09-24 09:47:57 +02:00

149 lines
3.4 KiB
JavaScript

/**
* Heading Hierarchy Check Rule
*
* This rule checks for proper heading hierarchy.
* Skipping heading levels can confuse screen readers and break document outline.
*
* Compatible with ACE (accessibility-checker-engine)
*/
import {
getHeadingLevel,
getHeadingXpath,
getPageHeadings,
} from '../utils/page-headings';
const headingHierarchyCheck = {
id: 'heading_hierarchy_check',
context: 'dom:html',
dependencies: [],
help: {
'en-US': {
group: 'Heading Structure',
pass: 'Heading hierarchy follows proper sequence',
fail_skipped_level:
'Heading levels are skipped in the document. Maintain proper heading hierarchy to help screen readers understand the content structure.',
},
},
messages: {
'en-US': {
group: 'Heading Structure',
pass: 'Heading hierarchy follows proper sequence',
fail_skipped_level:
'Heading levels are skipped. Ensure headings follow proper hierarchy (h1 -> h2 -> h3).',
},
},
rulesets: [
{
id: ['IBM_Accessibility'],
num: '1.3.1',
level: 'recommendation',
toolkitLevel: 'LEVEL_ONE',
},
],
run: (context) => {
const document = context.dom.node;
const headings = getPageHeadings(document);
if (headings.length === 0) {
return {
ruleId: 'heading_hierarchy_check',
reasonId: 'pass',
value: ['RECOMMENDATION', 'PASS'],
path: {
dom: '/html[1]',
aria: '/document[1]',
},
ruleTime: 0,
message: 'No headings found',
messageArgs: [],
apiArgs: [],
bounds: {
left: 0,
top: 0,
height: 0,
width: 0,
},
snippet: '<html>',
category: 'Accessibility',
ignored: false,
level: 'pass',
};
}
// Extract heading levels and check for skips
let previousLevel = 0;
let violatingElement = null;
const skippedLevels = [];
for (const heading of headings) {
const currentLevel = getHeadingLevel(heading);
if (previousLevel > 0 && currentLevel > previousLevel + 1) {
violatingElement = heading;
for (let i = previousLevel + 1; i < currentLevel; i++) {
skippedLevels.push(`h${i}`);
}
break;
}
previousLevel = currentLevel;
}
if (violatingElement) {
const rect = violatingElement.getBoundingClientRect();
return {
ruleId: 'heading_hierarchy_check',
reasonId: 'fail_skipped_level',
value: ['RECOMMENDATION', 'FAIL'],
path: {
dom: getHeadingXpath(violatingElement),
aria: '/document[1]',
},
node: violatingElement,
ruleTime: 0,
message: `Heading levels are skipped. Ensure headings follow proper hierarchy. Missing: ${skippedLevels.join(', ')}.`,
messageArgs: [skippedLevels.join(', ')],
apiArgs: [],
bounds: {
left: Math.round(rect.left),
top: Math.round(rect.top),
height: Math.round(rect.height),
width: Math.round(rect.width),
},
snippet: violatingElement.outerHTML,
category: 'Accessibility',
ignored: false,
level: 'recommendation',
};
}
return {
ruleId: 'heading_hierarchy_check',
reasonId: 'pass',
value: ['RECOMMENDATION', 'PASS'],
path: {
dom: '/html[1]',
aria: '/document[1]',
},
ruleTime: 0,
message: 'Heading hierarchy follows proper sequence',
messageArgs: [],
apiArgs: [],
bounds: {
left: 0,
top: 0,
height: 0,
width: 0,
},
snippet: headings[0]?.outerHTML || '<html>',
category: 'Accessibility',
ignored: false,
level: 'pass',
};
},
};
export default headingHierarchyCheck;