Skip to content

Commit

Permalink
Merge pull request #1865 from IBMa/dev-1638
Browse files Browse the repository at this point in the history
fixrule(`text_block_heading`): Reduce false positives on block headings
  • Loading branch information
ErickRenteria committed Apr 9, 2024
2 parents 902fd9c + bdb1bf2 commit 040a3f7
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 261 deletions.
113 changes: 79 additions & 34 deletions accessibility-checker-engine/src/v4/rules/text_block_heading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,33 @@
limitations under the License.
*****************************************************************************/

import { Rule, RuleResult, RuleFail, RuleContext, RulePotential, RuleManual, RulePass, RuleContextHierarchy } from "../api/IRule";
import { Rule, RuleResult, RuleContext, RulePotential, RuleContextHierarchy } from "../api/IRule";
import { eRulePolicy, eToolkitLevel } from "../api/IRule";
import { NodeWalker, RPTUtil } from "../../v2/checker/accessibility/util/legacy";
import { DOMWalker } from "../../v2/dom/DOMWalker";
import { getComputedStyle, getPixelsFromStyle } from "../util/CSSUtil";
import { VisUtil } from "../../v2/dom/VisUtil";

export let text_block_heading: Rule = {
id: "text_block_heading",
context: "dom:p, dom:div, dom:br",
refactor: {
"RPT_Block_ShouldBeHeading": {
"Pass_0": "Pass_0",
"Potential_1": "Potential_1"}
"Pass_0": "pass",
"Potential_1": "potential_heading"}
},
help: {
"en-US": {
"Pass_0": "text_block_heading.html",
"Potential_1": "text_block_heading.html",
"pass": "text_block_heading.html",
"potential_heading": "text_block_heading.html",
"group": "text_block_heading.html"
}
},
messages: {
"en-US": {
"Pass_0": "Rule Passed",
"Potential_1": "Check if this text should be marked up as a heading: {0}",
"group": "Heading text must use a heading element"
"pass": "Heading text uses a heading element or role",
"potential_heading": "Confirm this text '{0}' is used as a heading and if so, modify to use a heading element or role",
"group": "Heading text should use a heading element or role"
}
},
rulesets: [{
Expand All @@ -46,57 +48,100 @@ export let text_block_heading: Rule = {
}],
act: [],
run: (context: RuleContext, options?: {}, contextHierarchies?: RuleContextHierarchy): RuleResult | RuleResult[] => {
const ruleContext = context["dom"].node as HTMLElement;
//skip the check if the element is hidden or disabled
if (RPTUtil.isNodeDisabled(ruleContext) || !VisUtil.isNodeVisible(ruleContext))
return null;

// Don't trigger if we're not in the body or if we're in a script
if (RPTUtil.getAncestor(ruleContext, ["body"]) === null || RPTUtil.getAncestor(ruleContext, ["script"]) !== null)
return null;

const validateParams = {
numWords: {
value: 10,
type: "integer"
}
}

const ruleContext = context["dom"].node as Element;
let bodyFont = 0;
let body = ruleContext.ownerDocument.getElementsByTagName("body");
if (body != null) {
let bodyStyle = getComputedStyle(body[0]);
if (bodyStyle) bodyFont = getPixelsFromStyle(bodyStyle['font-size'], body);
}
let numWords = validateParams.numWords.value;
let wordsSeen = 0;
let wordStr: string[] = [];
let emphasizedText = false;
let nw = new NodeWalker(ruleContext);

let passed = false;
while (!passed &&
nw.nextNode() &&
nw.nextNode() &&
nw.node !== ruleContext &&
nw.node !== DOMWalker.parentNode(ruleContext) &&
!["br", "div", "p"].includes(nw.node.nodeName.toLowerCase())) // Don't report twice
{
if (RPTUtil.shouldNodeBeSkippedHidden(nw.node))
continue;

let nwName = nw.node.nodeName.toLowerCase();
if ((nwName == "b" || nwName == "em" || nwName == "i" ||
nwName == "strong" || nwName == "u" || nwName == "font") && !RPTUtil.shouldNodeBeSkippedHidden(nw.node)) {
let nextStr = RPTUtil.getInnerText(nw.node);
let wc = RPTUtil.wordCount(nextStr);
if (wc > 0) {
wordStr.push(nextStr);
emphasizedText = true;
wordsSeen += wc;
if (nw.node.nodeType === 3) {
// for text child
if (nw.node.nodeValue.trim().length > 0 && nw.node.parentElement) {
// check it's style if the target element contains text, e.g., <p> fake heading</p>
let style = getComputedStyle(nw.node.parentElement);
if (style && (style['font-weight'] === 'bold' || style['font-weight'] >= 700
|| (style['font-size'] && style['font-size'].includes("large"))
|| (style['font-size'] && bodyFont !== 0 && getPixelsFromStyle(style['font-size'],nw.node.parentElement) > bodyFont))) {
let nextStr = nw.node.nodeValue.trim();

let wc = RPTUtil.wordCount(nextStr);
if (wc > 0) {
wordStr.push(nextStr);
emphasizedText = true;
wordsSeen += wc;
}
passed = wordsSeen > numWords;
// Skip this node because it's emphasized
nw.bEndTag = true;
} else {
// the node contain regular text
passed = true;
}
}
} else if (nw.node.nodeType === 1) {
// for element child
if (nwName === "b" || nwName === "strong" || nwName === "u" || nwName === "font") {
// if the target element contains emphasis child, e.g., <p><strong>fake heading</strong></p>
let nextStr = RPTUtil.getInnerText(nw.node);

let wc = RPTUtil.wordCount(nextStr);
if (wc > 0) {
wordStr.push(nextStr);
emphasizedText = true;
wordsSeen += wc;
}
passed = wordsSeen > numWords;
// Skip this node because it's emphasized
nw.bEndTag = true;
} else {
// ignore the element which has a role except 'generic', 'paragraph' or 'strong'
// ignore applet element that is deprecated anyway
let role = RPTUtil.getResolvedRole(nw.node as HTMLElement);
passed = (role !== null && role !== 'generic' && role !== 'paragraph' && role !== 'strong') || nwName === "applet";
}
passed = wordsSeen > numWords;
// Skip this node because it's emphasized
nw.bEndTag = true;
} else {
passed =
(nw.node.nodeType == 1 && RPTUtil.attributeNonEmpty(nw.node, "alt") &&
(nwName == "applet" || nwName == "embed" || nwName == "img" ||
(nwName === "input" && nw.elem().hasAttribute("type") && nw.elem().getAttribute("type") == "image")
)
)
|| (nwName === "#text" && nw.node.nodeValue.trim().length > 0)
// Give them the benefit of the doubt if there's a link
|| (nwName === "a" && nw.elem().hasAttribute("href") && RPTUtil.attributeNonEmpty(nw.node, "href"));
}
}
if (wordsSeen == 0) passed = true;

//ignore if the string ends with “:” “,” “-” “;” or “.”
if (!passed) passed = /[:,;\-\.]$/.test(wordStr.join(" ").trim());

if (passed) {
return RulePass("Pass_0");
return null;
} else {
return RulePotential("Potential_1", [wordStr.join(" ")]);
return RulePotential("potential_heading", [wordStr.join(" ")]);
}
}
}
5 changes: 4 additions & 1 deletion accessibility-checker-engine/src/v4/util/CSSUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function selectorMatchesElem(element, selector) {
* @param elem
*/
export function getComputedStyle(elem: HTMLElement, pseudoElt?: PseudoClass) {
if (!elem) return null;
const doc = elem.ownerDocument;
const win = doc.defaultView;
return win.getComputedStyle(elem, pseudoElt);
Expand All @@ -74,9 +75,11 @@ export function getComputedStyle(elem: HTMLElement, pseudoElt?: PseudoClass) {
*/
export function getDefinedStyles(elem: HTMLElement, pseudoClass?: string ) {
// console.log("Function: getDefinedStyles");
if (!elem) return null;

let definedStyles = {};
let definedStylePseudo = {};

function fillStyle(maps, style) {
for (let sIndex=0; sIndex < style.length; ++sIndex) {
if (style[sIndex] === "all" && style[style[sIndex]]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,22 +136,16 @@ <h3>Headings Tests</h3>
],
failedXpaths: [
"/html/body/p[1]",
"/html/body/p[2]",
"/html/body/p[3]",
"/html/body/p[4]",
"/html/body/p[5]",
"/html/body/p[6]",
"/html/body/p[7]",
"/html/body/br[1]",
"/html/body/br[2]",
"/html/body/br[3]",
"/html/body/br[4]",
"/html/body/br[5]",
"/html/body/br[6]",
"/html/body/br[7]",
"/html/body/div[1]",
"/html/body/div[2]",
"/html/body/div[3]",
"/html/body/div[4]",
"/html/body/div[5]",
"/html/body/div[6]",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<!--
/******************************************************************************
Copyright:: 2020- IBM, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*****************************************************************************/
-->

<html lang="en">

<head>
<title>RPT Test Suite</title>
</head>

<body>
<main>
<div>
<p id='11' style="font-weight: bold;">Fake Heading 1:</p>
</div>
<div>
<p id='21' style="font-weight: 400;">not a heading</p>
</div>
<div>
<p id='31' style="font-weight: 800;">Fake Heading 2,</p>
</div>
<div>
<p id='41' style="font-size: 24px;">Fake Heading 3;</p>
</div>
<div>
<p id='51' style="font-size: 1.2rem;">Fake Heading 4.</p>
</div>
<div>
<p id='61' style="font-size: 0.8rem;">not a Heading</p>
</div>
<div>
<p id='71' style="font-size: large;">Fake Heading 5-</p>
</div>
</main>

<script type="text/javascript">
UnitTest = {
ruleIds: ["text_block_heading"],
results: [

]
};
//]]>
</script></body>

</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<!--
/******************************************************************************
Copyright:: 2020- IBM, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*****************************************************************************/
-->

<html lang="en">

<head>
<title>RPT Test Suite</title>
</head>

<body>
<main>
<div>
<ul>
<li><strong>test options</strong></li>
<li><u>test 0</u></li>
<li><i>test 1</i> </li>
<li><b>test 2</b></li>
</ul>
</div>
</main>

<script type="text/javascript">
UnitTest = {
ruleIds: ["text_block_heading"],
results: [

]
};
//]]>
</script></body>

</html>
Loading

0 comments on commit 040a3f7

Please sign in to comment.