Skip to content

Commit

Permalink
add initial test case and rule #1676
Browse files Browse the repository at this point in the history
  • Loading branch information
shunguoy committed Oct 4, 2023
1 parent a61b5aa commit 12a8f46
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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");
}
}
1 change: 1 addition & 0 deletions accessibility-checker-engine/src/v4/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Using CSS margin and scroll-margin to un-obscure content</title>
<style>
.wrapper {
display:grid;
gap:1rem;
grid-template-columns:repeat(9, 1fr);
grid-template-rows:8rem auto minmax(10rem, max-content);
height:100vh;
}

.wrapper > * {
border:1px solid var(--black);
padding:1rem;
}

header {
grid-column:1 / -1;
grid-row:1;
}

main {
grid-column:1 / 8;
}

aside {
grid-column:8 / 10;
}

/**footer {
grid-column:1 / -1;
}*/

@media (max-width:70rem) {
main {
grid-column:1 / -1;
}

aside {
grid-column:1 / -1;
}
}

.fixed-position-banner {
background:var(--banner-background);
border:1px solid var(--banner-border);
height:11rem;
inset-block-end:0;
margin-block-end:1rem;
padding:1rem;
position:relative;
width:calc(100vw - 1rem);
}

@media (min-width:70rem), (min-height:60rem) {
.fixed-position-banner {
margin-block-end:0;
position:fixed;
}

.banner-open {
margin-block-end:14rem;
}

.banner-open a {
scroll-margin-block-end:15.5rem;
}
}

.footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background-color: lightgray;
color: black;
text-align: center;
}
</style>
</head>
<body>
<dialog class="fixed-position-banner">
<h2 tabindex="-1">Fixed-Position Banner</h2>
<div><button aria-label="close fixed-position banner" class="close-banner" type="button">
Close
</button></div>
</dialog>
<div class="wrapper">
<header>
<h1>Header Content</h1>
</header>
<main>
<h1>Main Content</h1>
<div ></div>
<h2 style="margin-top: 5rem;">
<a
href="https://seirdy.one/notes/2023/07/22/polygot-xhtml5/"
itemprop="url"
class="u-url url"
rel="bookmark"
>
Test link 1
</a>
</h2>

<h2 style="margin-top: 20rem;">
<a
href="https://seirdy.one/notes/2023/07/22/polygot-xhtml5/"
itemprop="url"
class="u-url url"
rel="bookmark"
>
Test link 2
</a>
</h2>

<h2 style="margin-top: 10rem;">
<a
href="https://seirdy.one/notes/2023/07/22/polygot-xhtml5/"
itemprop="url"
class="u-url url"
rel="bookmark"
>
Test link 3
</a>
</h2>
</main>

<footer class="footer">
<h2>Footer Content</h2>
<p><a href="https://example.com">Here's an example link in the footer</a>.</p>
</footer>
</div>
<script>
document.addEventListener("DOMContentLoaded", function(e){
const cookieBanner = document.querySelector(".fixed-position-banner");
const closeBannerBtn = document.querySelector(".close-banner");
const pageBody = document.querySelector("body");

cookieBanner.show();

if(cookieBanner.hasAttribute("open")){
pageBody.classList.add("banner-open");
}

closeBannerBtn.addEventListener("click", function(e){
cookieBanner.close();
pageBody.classList.remove("banner-open");
}, false);
});
</script>

<script>
UnitTest = {
ruleIds: ["element_tabbable_unobscured"],
results: [
{
"ruleId": "element_tabbable_visible",
"value": [
"INFORMATION",
"PASS"
],
"path": {
"dom": "/html[1]/body[1]/button[1]",
"aria": "/document[1]/button[1]"
},
"reasonId": "pass",
"message": "The tabbable element is visible on the screen",
"messageArgs": [],
"apiArgs": [],
"category": "Accessibility"
},
{
"ruleId": "element_tabbable_visible",
"value": [
"INFORMATION",
"PASS"
],
"path": {
"dom": "/html[1]/body[1]/a[1]",
"aria": "/document[1]/link[1]"
},
"reasonId": "pass",
"message": "The tabbable element is visible on the screen",
"messageArgs": [],
"apiArgs": [],
"category": "Accessibility"
}
]
}
</script>
</body>
</html>

0 comments on commit 12a8f46

Please sign in to comment.