diff --git a/lib/checks/aria/aria-valid-attr-value-evaluate.js b/lib/checks/aria/aria-valid-attr-value-evaluate.js index ad43d1393..900442368 100644 --- a/lib/checks/aria/aria-valid-attr-value-evaluate.js +++ b/lib/checks/aria/aria-valid-attr-value-evaluate.js @@ -140,7 +140,12 @@ export default function ariaValidAttrValueEvaluate(node, options, virtualNode) { } if (needsReview) { - this.data({ messageKey, needsReview }); + this.data({ + messageKey, + needsReview, + // a11y-rule-aria-valid-attr-value: emit reviewPayload + reviewPayload: { visualHelperData: { ariaAttribute: needsReview } } + }); return undefined; } diff --git a/lib/checks/keyboard/focusable-disabled-evaluate.js b/lib/checks/keyboard/focusable-disabled-evaluate.js index d58807158..683f26fa5 100644 --- a/lib/checks/keyboard/focusable-disabled-evaluate.js +++ b/lib/checks/keyboard/focusable-disabled-evaluate.js @@ -25,7 +25,7 @@ function focusableDisabledEvaluate(node, options, virtualNode) { return true; } - return relatedNodes.every(vNode => { + const verdict = relatedNodes.every(vNode => { const pointerEvents = vNode.getComputedStylePropertyValue('pointer-events'); const width = parseInt(vNode.getComputedStylePropertyValue('width')); const height = parseInt(vNode.getComputedStylePropertyValue('height')); @@ -37,6 +37,24 @@ function focusableDisabledEvaluate(node, options, virtualNode) { }) ? undefined : false; + + // a11y-rule-aria-hidden-focus: emit focusable descendants for bulk review; additive, guarded. + try { + this.data({ + reviewPayload: { + visualHelperData: { + focusableChildren: relatedNodes + .map(vNode => vNode.actualNode) + .filter(Boolean) + .map(actualNode => new axe.utils.DqElement(actualNode).selector) + } + } + }); + } catch { + // best-effort emit — preserve the scan result + } + + return verdict; } export default focusableDisabledEvaluate; diff --git a/lib/checks/keyboard/focusable-not-tabbable-evaluate.js b/lib/checks/keyboard/focusable-not-tabbable-evaluate.js index b5a5d2965..fbc2f6815 100644 --- a/lib/checks/keyboard/focusable-not-tabbable-evaluate.js +++ b/lib/checks/keyboard/focusable-not-tabbable-evaluate.js @@ -25,7 +25,7 @@ function focusableNotTabbableEvaluate(node, options, virtualNode) { return true; } - return relatedNodes.every(vNode => { + const verdict = relatedNodes.every(vNode => { const pointerEvents = vNode.getComputedStylePropertyValue('pointer-events'); const width = parseInt(vNode.getComputedStylePropertyValue('width')); const height = parseInt(vNode.getComputedStylePropertyValue('height')); @@ -37,6 +37,24 @@ function focusableNotTabbableEvaluate(node, options, virtualNode) { }) ? undefined : false; + + // a11y-rule-aria-hidden-focus: emit focusable descendants for bulk review; additive, guarded. + try { + this.data({ + reviewPayload: { + visualHelperData: { + focusableChildren: relatedNodes + .map(vNode => vNode.actualNode) + .filter(Boolean) + .map(actualNode => new axe.utils.DqElement(actualNode).selector) + } + } + }); + } catch { + // best-effort emit — preserve the scan result + } + + return verdict; } export default focusableNotTabbableEvaluate; diff --git a/lib/checks/navigation/identical-links-same-purpose-after.js b/lib/checks/navigation/identical-links-same-purpose-after.js index 2459be117..454232106 100644 --- a/lib/checks/navigation/identical-links-same-purpose-after.js +++ b/lib/checks/navigation/identical-links-same-purpose-after.js @@ -84,6 +84,9 @@ function identicalLinksSamePurposeAfter(results) { currentResult.result = undefined; } + // primary link's DqElement is at relatedNodes[0]; capture before rebuild. + const currentNode = currentResult.relatedNodes[0]; + /** * -> deduplicate results (for both `pass` or `incomplete`) and add `relatedNodes` if any */ @@ -92,6 +95,24 @@ function identicalLinksSamePurposeAfter(results) { ...sameNameResults.map(node => node.relatedNodes[0]) ); + if (sameNameResults.length) { + try { + const identicalLinks = [currentNode, ...currentResult.relatedNodes] + .filter(Boolean) + .map(dqElm => (dqElm ? dqElm.selector : undefined)) + .filter(Boolean); + const linkText = + (currentResult.data && currentResult.data.accessibleText) || name; + if (currentResult.data) { + currentResult.data.reviewPayload = { + visualHelperData: { linkText, identicalLinks } + }; + } + } catch { + // best-effort emit — preserve the reconciled results + } + } + /** * Update `nodeMap` with `sameNameResults` */ diff --git a/lib/checks/navigation/identical-links-same-purpose-evaluate.js b/lib/checks/navigation/identical-links-same-purpose-evaluate.js index b47400c91..6d5b4c23d 100644 --- a/lib/checks/navigation/identical-links-same-purpose-evaluate.js +++ b/lib/checks/navigation/identical-links-same-purpose-evaluate.js @@ -24,6 +24,7 @@ function identicalLinksSamePurposeEvaluate(node, options, virtualNode) { */ const afterData = { name, + accessibleText: text.sanitize(accText), urlProps: dom.urlPropsFromAttribute(node, 'href') }; this.data(afterData); diff --git a/lib/checks/parsing/duplicate-id-after.js b/lib/checks/parsing/duplicate-id-after.js index e6a587185..021dfffdd 100644 --- a/lib/checks/parsing/duplicate-id-after.js +++ b/lib/checks/parsing/duplicate-id-after.js @@ -1,8 +1,9 @@ function duplicateIdAfter(results) { const uniqueIds = []; return results.filter(r => { - if (uniqueIds.indexOf(r.data) === -1) { - uniqueIds.push(r.data); + const id = r.data && typeof r.data === 'object' ? r.data.id : r.data; + if (uniqueIds.indexOf(id) === -1) { + uniqueIds.push(id); return true; } return false; diff --git a/lib/checks/parsing/duplicate-id-aria.json b/lib/checks/parsing/duplicate-id-aria.json index 67cfa5cd9..56287276a 100644 --- a/lib/checks/parsing/duplicate-id-aria.json +++ b/lib/checks/parsing/duplicate-id-aria.json @@ -2,11 +2,12 @@ "id": "duplicate-id-aria", "evaluate": "duplicate-id-evaluate", "after": "duplicate-id-after", + "options": { "reviewPayload": true }, "metadata": { "impact": "critical", "messages": { "pass": "Document has no elements referenced with ARIA or labels that share the same id attribute", - "fail": "Document has multiple elements referenced with ARIA with the same id attribute: ${data}" + "fail": "Document has multiple elements referenced with ARIA with the same id attribute: ${data.id}" } } } diff --git a/lib/checks/parsing/duplicate-id-evaluate.js b/lib/checks/parsing/duplicate-id-evaluate.js index 55b520c09..8914fe8f9 100644 --- a/lib/checks/parsing/duplicate-id-evaluate.js +++ b/lib/checks/parsing/duplicate-id-evaluate.js @@ -1,7 +1,7 @@ import { getRootNode } from '../../commons/dom'; import { escapeSelector } from '../../core/utils'; -function duplicateIdEvaluate(node) { +function duplicateIdEvaluate(node, options = {}) { const id = node.getAttribute('id').trim(); // Since empty ID's are not meaningful and are ignored by Edge, we'll // let those pass. @@ -16,6 +16,29 @@ function duplicateIdEvaluate(node) { if (matchingNodes.length) { this.relatedNodes(matchingNodes); } + + // a11y-rule-duplicate-id-aria: emit duplicate id + sharing elements for bulk review when enabled. + if (options.reviewPayload) { + const verdict = matchingNodes.length === 0; + const data = { id }; + try { + if (matchingNodes.length) { + data.reviewPayload = { + visualHelperData: { + duplicateId: id, + elements: matchingNodes + .filter(Boolean) + .map(foundNode => new axe.utils.DqElement(foundNode).selector) + } + }; + } + } catch { + // best-effort emit — preserve the scan result + } + this.data(data); + return verdict; + } + this.data(id); return matchingNodes.length === 0; diff --git a/lib/rules/aria-hidden-focus.json b/lib/rules/aria-hidden-focus.json index afb1ad8fd..d149e7900 100755 --- a/lib/rules/aria-hidden-focus.json +++ b/lib/rules/aria-hidden-focus.json @@ -17,6 +17,7 @@ ], "actIds": ["6cfa84"], "metadata": { + "defaultCategory": "aria-hidden", "description": "Ensure aria-hidden elements are not focusable nor contain focusable elements", "help": "ARIA hidden element must not be focusable or contain focusable elements", "needsReviewConfidence": 49.99 diff --git a/lib/rules/aria-valid-attr-value.json b/lib/rules/aria-valid-attr-value.json index ac92598e2..dc39253c3 100644 --- a/lib/rules/aria-valid-attr-value.json +++ b/lib/rules/aria-valid-attr-value.json @@ -13,6 +13,7 @@ ], "actIds": ["6a7281"], "metadata": { + "defaultCategory": "aria-attributes", "description": "Ensure all ARIA attributes have valid values", "help": "ARIA attributes must conform to valid values", "needsReviewConfidence": 49.99 diff --git a/lib/rules/bypass.json b/lib/rules/bypass.json index 02666d9da..b05b74210 100644 --- a/lib/rules/bypass.json +++ b/lib/rules/bypass.json @@ -20,6 +20,7 @@ ], "actIds": ["cf77f2", "047fe0", "b40fd1", "3e12e1", "ye5d6e"], "metadata": { + "defaultCategory": "bypass-blocks", "description": "Ensure each page has at least one mechanism for a user to bypass navigation and jump straight to the content", "help": "Page must have means to bypass repeated blocks", "needsReviewConfidence": 49.99 diff --git a/lib/rules/css-orientation-lock.json b/lib/rules/css-orientation-lock.json index 06bc7d426..ed134b978 100644 --- a/lib/rules/css-orientation-lock.json +++ b/lib/rules/css-orientation-lock.json @@ -16,6 +16,7 @@ ], "actIds": ["b33eff"], "metadata": { + "defaultCategory": "orientation", "description": "Ensure content is not locked to any specific display orientation, and the content is operable in all display orientations", "help": "CSS Media queries must not lock display orientation", "violationConfidence": 90, diff --git a/lib/rules/duplicate-id-aria.json b/lib/rules/duplicate-id-aria.json index 1c900c3dd..d3921ff55 100644 --- a/lib/rules/duplicate-id-aria.json +++ b/lib/rules/duplicate-id-aria.json @@ -16,6 +16,7 @@ "reviewOnFail": true, "actIds": ["3ea0c8"], "metadata": { + "defaultCategory": "duplicate-ids", "description": "Ensure every id attribute value used in ARIA and in labels is unique", "help": "IDs used in ARIA and labels must be unique", "needsReviewConfidence": 49.99 diff --git a/lib/rules/identical-links-same-purpose.json b/lib/rules/identical-links-same-purpose.json index afde13c5e..cba1facec 100644 --- a/lib/rules/identical-links-same-purpose.json +++ b/lib/rules/identical-links-same-purpose.json @@ -8,6 +8,7 @@ "tags": ["cat.semantics", "wcag2aaa", "wcag249"], "actIds": ["b20e66"], "metadata": { + "defaultCategory": "link-consistency", "description": "Ensure that links with the same accessible name serve a similar purpose", "help": "Links with the same name must have a similar purpose", "needsReviewConfidence": 49.99 diff --git a/lib/rules/video-caption.json b/lib/rules/video-caption.json index b0297ddb0..98f23d092 100644 --- a/lib/rules/video-caption.json +++ b/lib/rules/video-caption.json @@ -17,6 +17,7 @@ ], "actIds": ["eac66b"], "metadata": { + "defaultCategory": "media-captions", "description": "Ensure