Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CLOSING > See: #1081] feat(rule): css-orientation-lock new wcag21 rule #971

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions build/tasks/test-webdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ module.exports = function(grunt) {
.timeouts()
.setScriptTimeout(!isMobile ? 60000 * 5 : 60000 * 10);

driver
.manage()
.timeouts()
.implicitlyWait(60000);

return (
driver
.get(url)
Expand Down Expand Up @@ -87,6 +92,9 @@ module.exports = function(grunt) {
return Promise.resolve(errors);
}
})
.catch(function(err) {
console.error('Selenium Webdriver Error: ', err);
})
);
}

Expand Down
1 change: 1 addition & 0 deletions doc/rule-descriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
| bypass | Ensures each page has at least one mechanism for a user to bypass navigation and jump straight to the content | Serious | cat.keyboard, wcag2a, wcag241, section508, section508.22.o | true |
| checkboxgroup | Ensures related <input type="checkbox"> elements have a group and that the group designation is consistent | Critical | cat.forms, best-practice | true |
| color-contrast | Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds | Serious | cat.color, wcag2aa, wcag143 | true |
| css-orientation-lock | Ensures content is not locked to any specific display orientation, and the content is operable in all display orientations | Serious | cat.structure, wcag262, wcag21aa, experimental | true |
| definition-list | Ensures <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131 | true |
| dlitem | Ensures <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131 | true |
| document-title | Ensures each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242 | true |
Expand Down
129 changes: 129 additions & 0 deletions lib/checks/mobile/css-orientation-lock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/* global context */

// extract asset of type `cssom` from context
const { cssom = undefined } = context || {};
const checkPass = true;
const checkFail = false;
const checkIncomplete = undefined;

// if there is no cssom <- return incomplete
if (!cssom || !cssom.length) {
return checkIncomplete;
}

// combine all rules from each sheet into one array
const rulesGroupByDocumentFragment = cssom.reduce(
(out, { sheet, root, shadowId }) => {
// construct key based on shadowId or top level document
const key = shadowId ? shadowId : 'topDocument';
// init property if does not exist
if (!out[key]) {
out[key] = {
root,
rules: []
};
}
// check if sheet and rules exist
if (!sheet || !sheet.rules) {
//return
return out;
}
const rules = Array.from(sheet.rules);
// add rules into same document fragment
out[key].rules = out[key].rules.concat(rules);

//return
return out;
},
{}
);

// Note:
// Some of these functions can be extracted to utils, but best to do it when other cssom rules are authored.

// extract styles for each orientation rule to verify transform is applied
let isLocked = false;
let relatedElements = [];

Object.keys(rulesGroupByDocumentFragment).forEach(key => {
const { root, rules } = rulesGroupByDocumentFragment[key];

// filter media rules from all rules
const mediaRules = rules.filter(r => {
// doc: https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule
// type value of 4 (CSSRule.MEDIA_RULE) pertains to media rules
return r.type === 4;
});

// narrow down to media rules with `orientation` as a keyword
const orientationRules = mediaRules.filter(r => {
// conditionText exists on media rules, which contains only the @media condition
// eg: screen and (max-width: 767px) and (min-width: 320px) and (orientation: landscape)
const cssText = r.cssText;
return (
/orientation:\s+landscape/i.test(cssText) ||
/orientation:\s+portrait/i.test(cssText)
);
});

orientationRules.forEach(r => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not take priority into consideration:

div {
  transform: rotate(90deg)
}
html div {
  transform: rotate(0)
}

The second rule has higher priority, and so it shouldn't fail. We need to take priority into consideration in order to avoid false positives.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue created: #1078

// r.cssRules is a RULEList and not an array
if (!r.cssRules.length) {
return;
}
// cssRules ia a list of rules
// a media query has framents of css styles applied to various selectors
// iteration through cssRules and see if orientation lock has been applied
Array.from(r.cssRules).forEach(cssRule => {
/* eslint max-statements: ["error", 20], complexity: ["error", 15] */

// ensure selectorText exists
if (!cssRule.selectorText) {
return;
}
// ensure the given selector has styles declared (non empty selector)
if (cssRule.styleMap && cssRule.styleMap.size <= 0) {
return;
}

// check if transform style exists
const transformStyleValue = cssRule.style.transform || false;
// transformStyleValue -> is the value applied to property
// eg: "rotate(-90deg)"
if (!transformStyleValue) {
return;
}

const rotate = transformStyleValue.match(/rotate\(([^)]+)deg\)/);
const deg = parseInt((rotate && rotate[1]) || 0);
const locked = deg % 90 === 0 && deg % 180 !== 0;

// if locked
// and not root HTML
// preserve as relatedNodes
if (locked && cssRule.selectorText.toUpperCase() !== 'HTML') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for this HTML selector thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To exclude the matches html, so that it does not get added to relatedNodes.

const selector = cssRule.selectorText;
const elms = Array.from(root.querySelectorAll(selector));
if (elms && elms.length) {
relatedElements = relatedElements.concat(elms);
}
}

// set locked boolean
isLocked = locked;
});
});
});

if (!isLocked) {
// return
return checkPass;
}

// set relatedNodes
if (relatedElements.length) {
this.relatedNodes(relatedElements);
}

// return
return checkFail;
11 changes: 11 additions & 0 deletions lib/checks/mobile/css-orientation-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "css-orientation-lock",
"evaluate": "css-orientation-lock.js",
"metadata": {
"impact": "serious",
"messages": {
"pass": "Display is operable, and orientation lock does not exist",
"fail": "CSS Orientation lock is applied, and makes display inoperable"
}
}
}
12 changes: 8 additions & 4 deletions lib/core/utils/preload-cssom.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ function loadCssom({ root, shadowId }, timeout, convertTextToStylesheetFn) {
const sheet = convertTextToStylesheetFn({
data,
isExternal: true,
shadowId
shadowId,
root
});
resolve(sheet);
})
Expand Down Expand Up @@ -60,7 +61,8 @@ function loadCssom({ root, shadowId }, timeout, convertTextToStylesheetFn) {
resolve({
sheet,
isExternal: false,
shadowId
shadowId,
root
})
);
return;
Expand Down Expand Up @@ -89,6 +91,7 @@ function loadCssom({ root, shadowId }, timeout, convertTextToStylesheetFn) {
convertTextToStylesheetFn({
data: inlineRulesCssText,
shadowId,
root,
isExternal: false
})
)
Expand Down Expand Up @@ -166,15 +169,16 @@ axe.utils.preloadCssom = function preloadCssom({
* @property {Object} param.doc implementation document to create style elements
* @property {String} param.shadowId (Optional) shadowId if shadowDOM
*/
function convertTextToStylesheet({ data, isExternal, shadowId }) {
function convertTextToStylesheet({ data, isExternal, shadowId, root }) {
const style = dynamicDoc.createElement('style');
style.type = 'text/css';
style.appendChild(dynamicDoc.createTextNode(data));
dynamicDoc.head.appendChild(style);
return {
sheet: style.sheet,
isExternal,
shadowId
shadowId,
root
};
}

Expand Down
20 changes: 20 additions & 0 deletions lib/rules/css-orientation-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"id": "css-orientation-lock",
"selector": "html",
"tags": [
"cat.structure",
"wcag262",
"wcag21aa",
"experimental"
],
"metadata": {
"description": "Ensures content is not locked to any specific display orientation, and the content is operable in all display orientations",
"help": "CSS Media queries are not used to lock display orientation"
},
"all": [
"css-orientation-lock"
],
"any": [],
"none": [],
"preload": true
}
Loading