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

feat(rule): css-orientation-lock (wcag21) #1081

Merged
merged 16 commits into from
Aug 21, 2018
Merged
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
17 changes: 11 additions & 6 deletions build/tasks/test-webdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,6 @@ module.exports = function(grunt) {
var url = urls.shift();
errors = errors || [];

// Give each page enough time
driver
.manage()
.timeouts()
.setScriptTimeout(!isMobile ? 60000 * 5 : 60000 * 10);

return (
driver
.get(url)
Expand Down Expand Up @@ -177,6 +171,17 @@ module.exports = function(grunt) {
return done();
}

// Give driver timeout options for scripts
driver
.manage()
.timeouts()
.setScriptTimeout(!isMobile ? 60000 * 5 : 60000 * 10);
// allow to wait for page load implicitly
driver
.manage()
.timeouts()
.implicitlyWait(50000);

// Test all pages
runTestUrls(driver, isMobile, options.urls)
.then(function(testErrors) {
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
132 changes: 132 additions & 0 deletions lib/checks/mobile/css-orientation-lock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/* global context */

// extract asset of type `cssom` from context
const { cssom = undefined } = context || {};

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

// 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.cssRules) {
//return
return out;
}
const rules = Array.from(sheet.cssRules);
// 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;
});
if (!mediaRules || !mediaRules.length) {
return;
}

// 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)
);
});
if (!orientationRules || !orientationRules.length) {
return;
}

orientationRules.forEach(r => {
// 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.style.length <= 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') {
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 true;
}

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

// return fail
return false;
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"
}
}
}
59 changes: 45 additions & 14 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 All @@ -37,12 +38,44 @@ function loadCssom({ root, shadowId }, timeout, convertTextToStylesheetFn) {

const q = axe.utils.queue();

// iterate to decipher multi-level nested sheets if any (this is essential to retrieve styles from shadowDOM)
Array.from(root.styleSheets).forEach(sheet => {
// ignore disabled sheets
if (sheet.disabled) {
return;
// handle .styleSheets non existent on certain shadowDOM root
const rootStyleSheets = root.styleSheets
? Array.from(root.styleSheets)
: null;
if (!rootStyleSheets) {
return q;
}

// convenience array fot help unique sheets if duplicated by same `href`
// both external and internal sheets
let sheetHrefs = [];

// filter out sheets, that should not be accounted for...
const sheets = rootStyleSheets.filter(sheet => {
// FILTER > sheets with the same href (if exists)
let sheetAlreadyExists = false;
if (sheet.href) {
if (!sheetHrefs.includes(sheet.href)) {
sheetHrefs.push(sheet.href);
} else {
sheetAlreadyExists = true;
}
}
// FILTER > media='print'
// Note:
// Chrome does this automagically, Firefox returns every sheet
// hence the need to filter
const isPrintMedia = Array.from(sheet.media).includes('print');
// FILTER > disabled
// Firefox does not respect `disabled` attribute on stylesheet
// Hence decided not to filter out disabled for the time being

// return
return !isPrintMedia && !sheetAlreadyExists;
});

// iterate to decipher multi-level nested sheets if any (this is essential to retrieve styles from shadowDOM)
sheets.forEach(sheet => {
// attempt to retrieve cssRules, or for external sheets make a XMLHttpRequest
try {
// accessing .cssRules throws for external (cross-domain) sheets, which is handled in the catch
Expand All @@ -60,7 +93,8 @@ function loadCssom({ root, shadowId }, timeout, convertTextToStylesheetFn) {
resolve({
sheet,
isExternal: false,
shadowId
shadowId,
root
})
);
return;
Expand Down Expand Up @@ -89,16 +123,12 @@ function loadCssom({ root, shadowId }, timeout, convertTextToStylesheetFn) {
convertTextToStylesheetFn({
data: inlineRulesCssText,
shadowId,
root,
isExternal: false
})
)
);
} catch (e) {
// if no href, do not attempt to make an XHR, but this is preventive check
// NOTE: as further enhancements to resolve nested @imports are done, a decision to throw an Error if necessary here will be made.
if (!sheet.href) {
return;
}
// external sheet -> make an xhr and q the response
q.defer((resolve, reject) => {
getExternalStylesheet({ resolve, reject, url: sheet.href });
Expand Down Expand Up @@ -166,15 +196,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