From 12a8f46338da304b71990fa368f01aef723b9931 Mon Sep 17 00:00:00 2001 From: Shunguo Date: Wed, 4 Oct 2023 11:37:44 -0500 Subject: [PATCH] add initial test case and rule #1676 --- .../v4/rules/element_tabbable_unobscured.ts | 88 ++++---- .../src/v4/rules/index.ts | 1 + .../unobscured.html | 198 ++++++++++++++++++ 3 files changed, 237 insertions(+), 50 deletions(-) create mode 100755 accessibility-checker-engine/test/v2/checker/accessibility/rules/element_tabbable_unobscured_ruleunit/unobscured.html diff --git a/accessibility-checker-engine/src/v4/rules/element_tabbable_unobscured.ts b/accessibility-checker-engine/src/v4/rules/element_tabbable_unobscured.ts index f12fed433..d4d8e8f5d 100644 --- a/accessibility-checker-engine/src/v4/rules/element_tabbable_unobscured.ts +++ b/accessibility-checker-engine/src/v4/rules/element_tabbable_unobscured.ts @@ -12,9 +12,10 @@ *****************************************************************************/ import { RPTUtil } from "../../v2/checker/accessibility/util/legacy"; -import { getDefinedStyles, getComputedStyle } from "../util/CSSUtil"; -import { Rule, RuleResult, RuleFail, RuleContext, RulePass, RuleContextHierarchy, RulePotential } from "../api/IRule"; +import { Rule, RuleResult, RuleContext, RulePass, RuleContextHierarchy, RulePotential } from "../api/IRule"; import { eRulePolicy, eToolkitLevel } from "../api/IRule"; +import { VisUtil } from "../../v2/dom/VisUtil"; +import { DOMMapper } from "../../v2/dom/DOMMapper"; export let element_tabbable_unobscured: Rule = { id: "element_tabbable_unobscured", @@ -24,77 +25,64 @@ export let element_tabbable_unobscured: Rule = { "en-US": { "group": "element_tabbable_unobscured.html", "pass": "element_tabbable_unobscured.html", - "potential_visible": "element_tabbable_unobscured.html" + "potential_obscured": "element_tabbable_unobscured.html" } }, messages: { "en-US": { - "group": "A tabbable element should be visible on the screen when it has keyboard focus", - "pass": "The tabbable element is visible on the screen", - "potential_visible": "Confirm the element should be tabbable, and is visible on the screen when it has keyboard focus" + "group": "When an element receives focus, it is not entirely covered by other content", + "pass": "The element is not entirely covered by other content", + "potential_obscured": "Confirm that when the element receives focus, it is not covered or, if covered by user action, can be uncovered without moving focus" } }, rulesets: [{ id: ["WCAG_2_2"], num: ["2.4.11"], - level: eRulePolicy.RECOMMENDATION, + level: eRulePolicy.VIOLATION, toolkitLevel: eToolkitLevel.LEVEL_THREE }], act: [], run: (context: RuleContext, options?: {}, contextHierarchies?: RuleContextHierarchy): RuleResult | RuleResult[] => { const ruleContext = context["dom"].node as HTMLElement; - if (!RPTUtil.isTabbable(ruleContext)) + if (!VisUtil.isNodeVisible(ruleContext) || (!RPTUtil.isTabbable(ruleContext) && (!ruleContext .hasAttribute("tabindex")|| parseInt(ruleContext.getAttribute("tabindex")) < 0))) return null; const nodeName = ruleContext.nodeName.toLocaleLowerCase(); - const bounds = context["dom"].bounds; + + //ignore certain elements + if (RPTUtil.getAncestor(ruleContext, ["pre", "code", "script", "meta"]) !== null + || nodeName === "body" || nodeName === "html" ) + return null; + + const bounds = context["dom"].bounds; + //in case the bounds not available if (!bounds) return null; - // defined styles only give the styles that changed - const defined_styles = getDefinedStyles(ruleContext); - const onfocus_styles = getDefinedStyles(ruleContext, ":focus"); - - if (bounds['height'] === 0 || bounds['width'] === 0 - || (defined_styles['position']==='absolute' && defined_styles['clip'] && defined_styles['clip'].replaceAll(' ', '')==='rect(0px,0px,0px,0px)' - && !onfocus_styles['clip'])) - return RulePotential("potential_visible", []); + //ignore if offscreen + if (bounds['height'] === 0 || bounds['width'] === 0 || bounds['top'] < 0 || bounds['left'] < 0) + return null; - if (bounds['top'] >= 0 && bounds['left'] >= 0) - return RulePass("pass"); - - const default_styles = getComputedStyle(ruleContext); - - let top = bounds['top']; - let left = bounds['left']; - - if (Object.keys(onfocus_styles).length === 0 ) { - // no onfocus position change, but could be changed from js - return RulePotential("potential_visible", []); - } else { - // with onfocus position change - var positions = ['absolute', 'fixed']; - if (typeof onfocus_styles['top'] !== 'undefined') { - if (positions.includes(onfocus_styles['position']) || (typeof onfocus_styles['position'] === 'undefined' && positions.includes(default_styles['position']))) { - top = onfocus_styles['top'].replace(/\D/g,''); - } else { - // the position is undefined and the parent's position is 'relative' - top = Number.MIN_VALUE; - } - } - if (typeof onfocus_styles['left'] !== 'undefined') { - if (positions.includes(onfocus_styles['position']) || (typeof onfocus_styles['position'] === 'undefined' && positions.includes(default_styles['position']))) { - left = onfocus_styles['left'].replace(/\D/g,''); - } else { - // the position is undefined and the parent's position is 'relative' - left = Number.MIN_VALUE; - } + let passed = true; + var elems = document.querySelectorAll('body *:not(' + nodeName +' *, ' + nodeName +')'); + if (!elems || elems.length == 0) + return; + + const mapper : DOMMapper = new DOMMapper(); + let violations = []; + elems.forEach(elem => { + // Skip hidden + if (VisUtil.isNodeVisible(elem)) { + const bnds = mapper.getBounds(elem); + if (bnds.top <= bounds.top && bnds.left <= bounds.left && bnds.top + bnds.height >= bounds.top + bounds.height + && bnds.top + bnds.height >= bounds.left + bounds.width) + violations.push(elem); } - } + }); - if (top >= 0 && left >= 0) - return RulePass("pass"); - else + if (violations.length > 0) return RulePotential("potential_visible", []); + + return RulePass("pass"); } } diff --git a/accessibility-checker-engine/src/v4/rules/index.ts b/accessibility-checker-engine/src/v4/rules/index.ts index c368346e3..f9430b5e7 100644 --- a/accessibility-checker-engine/src/v4/rules/index.ts +++ b/accessibility-checker-engine/src/v4/rules/index.ts @@ -89,6 +89,7 @@ export * from "./element_mouseevent_keyboard" export * from "./element_orientation_unlocked" export * from "./element_scrollable_tabbable" export * from "./element_tabbable_role_valid" +export * from "./element_tabbable_unobscured" export * from "./element_tabbable_visible" export * from "./embed_alt_exists" export * from "./embed_noembed_exists" diff --git a/accessibility-checker-engine/test/v2/checker/accessibility/rules/element_tabbable_unobscured_ruleunit/unobscured.html b/accessibility-checker-engine/test/v2/checker/accessibility/rules/element_tabbable_unobscured_ruleunit/unobscured.html new file mode 100755 index 000000000..03176d997 --- /dev/null +++ b/accessibility-checker-engine/test/v2/checker/accessibility/rules/element_tabbable_unobscured_ruleunit/unobscured.html @@ -0,0 +1,198 @@ + + + + + + Using CSS margin and scroll-margin to un-obscure content + + + + +

Fixed-Position Banner

+
+
+
+
+

Header Content

+
+
+

Main Content

+
+

+ +

+ +

+ +

+ +

+ +

+
+ + +
+ + + + + +