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(standards): add html elems object #2325

Closed
wants to merge 15 commits into from
11 changes: 6 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
- checkout
- <<: *restore_dependency_cache_unix
- run: npm run test

# Run the test suite in IE in windows
test_win:
<<: *defaults
Expand All @@ -83,16 +83,16 @@ jobs:
- checkout
# npm i or restore cache
- <<: *restore_dependency_cache_win
# install selenium
# install selenium
- run: |
choco install selenium-ie-driver --version 3.141.5
export PATH=/c/tools/selenium:$PATH
echo $PATH
# build `axe`
- run: npm run build
# get fixtures ready for running tests
- run: npx grunt testconfig
- run: npx grunt fixture
- run: npx grunt testconfig
- run: npx grunt fixture
# run IE webdriver tests
- run: npx grunt connect test-webdriver:ie
# test examples
Expand All @@ -107,6 +107,7 @@ jobs:
steps:
- checkout
- <<: *restore_dependency_cache_unix
- run: npm run build
- run: npm run test:examples

# Test locale files
Expand Down Expand Up @@ -191,7 +192,7 @@ workflows:
requires:
- dependencies_unix
- lint
# Run IE/ Windows test on all commits
# Run IE/ Windows test on all commits
- test_win:
requires:
- dependencies_win
Expand Down
4 changes: 2 additions & 2 deletions axe.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// Definitions by: Marcy Sutton <https://github.com/marcysutton>

declare namespace axe {
type ImpactValue = 'minor' | 'moderate' | 'serious' | 'critical';
type ImpactValue = 'minor' | 'moderate' | 'serious' | 'critical' | null;

type TagValue = 'wcag2a' | 'wcag2aa' | 'section508' | 'best-practice' | 'wcag21a' | 'wcag21aa';
type TagValue = string;

type ReporterVersion = 'v1' | 'v2' | 'raw' | 'raw-env' | 'no-passes';

Expand Down
32 changes: 26 additions & 6 deletions build/rule-generator/get-files-metadata.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
const directories = require('./directories');

/**
* Helper to convert a given string to camel case (split by hyphens if any)
* @param {String} str given string to be camel cased
*/
const camelCase = str => {
return str.replace(/-([a-z])/g, g => {
return g[1].toUpperCase();
});
};

/**
* Get meta data for the file to be created as RULE Specification
* @method getRuleSpecFileMeta
Expand All @@ -16,7 +26,7 @@ const getRuleSpecFileMeta = (ruleName, ruleHasMatches, ruleChecks) => {
id: `${ruleName}`,
selector: '',
...(ruleHasMatches && {
matches: `${ruleName}-matches.js`
matches: `${ruleName}-matches`
}),
tags: [],
metadata: {
Expand Down Expand Up @@ -57,11 +67,17 @@ const getRuleMatchesFileMeta = (
let files = [];

if (ruleHasMatches) {
const fnName = `${camelCase(ruleName)}Matches`;
const ruleMatchesJs = {
name: `${ruleName}-matches.js`,
content: `
// TODO: Filter node(s)
return node;
// TODO: Filter node(s)

function ${fnName}(node, virtualNode) {
return node
}

export default ${fnName}
`,
dir: directories.rules
};
Expand Down Expand Up @@ -98,7 +114,7 @@ const getCheckSpecFileMeta = (name, dir) => {
content: JSON.stringify(
{
id: `${name}`,
evaluate: `${name}.js`,
evaluate: `${name}-evaluate`,
metadata: {
impact: '',
messages: {
Expand All @@ -123,11 +139,15 @@ const getCheckSpecFileMeta = (name, dir) => {
* @returns {Object} meta data of file
*/
const getCheckJsFileMeta = (name, dir) => {
const fnName = `${camelCase(name)}Evaluate`;
return {
name: `${name}.js`,
name: `${name}-evaluate.js`,
content: `
// TODO: Logic for check
return true;
function ${fnName}(node, options, virtualNode) {
return true
}
export default ${fnName};
`,
dir
};
Expand Down
14 changes: 0 additions & 14 deletions doc/examples/jsdom/test/a11y.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,6 @@ describe('axe', () => {
</body>
</html>`);

global.document = window.document;
global.window = window;

// needed by axios lib/helpers/isURLSameOrigin.js
global.navigator = window.navigator;

// needed by axe /lib/core/public/run.js
global.Node = window.Node;
global.NodeList = window.NodeList;

// needed by axe /lib/core/base/context.js
global.Element = window.Element;
global.Document = window.Document;

const axe = require('axe-core');
const config = {
rules: {
Expand Down
12 changes: 10 additions & 2 deletions doc/examples/test-examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ const exampleDirs = readdirSync(__dirname)
const config = { stdio: 'inherit', shell: true };

// run npm install in parallel
function install(dir) {
return execa('npm install', { cwd: dir, ...config });
async function install(dir) {
await execa('npm install', { cwd: dir, ...config });

// override the package version of axe-core with the local version.
// this allows the examples to stay examples while allowing us to
// test them against our changes
return await execa('npm install --no-save file:..\\/..\\/..\\/', {
cwd: dir,
...config
});
}

// run tests synchronously so we can see which one threw an error
Expand Down
133 changes: 126 additions & 7 deletions lib/commons/aria/get-role.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,118 @@
import getExplicitRole from './get-explicit-role';
import getImplicitRole from './implicit-role';
import lookupTable from './lookup-table';
import isFocusable from '../dom/is-focusable';
import { getNodeFromTree } from '../../core/utils';
import AbstractVirtuaNode from '../../core/base/virtual-node/abstract-virtual-node';

// when an element inherits the presentational role from a parent
// is not defined in the spec, but through testing it seems to be
// when a specific HTML parent relationship is required and that
// parent has `role=presentation`, then the child inherits the
// role (i.e. table, ul, dl). Further testing has shown that
// intermediate elements (such as divs) break this chain only in
// Chrome.
//
// Also, any nested structure chains reset the role (so two nested
// lists with the topmost list role=none will not cause the nested
// list to inherit the role=none).
//
// from Scott O'Hara:
//
// "the expectation for me, in standard html is that element
// structures that require specific parent/child relationships,
// if the parent is set to presentational that should set the
// children to presentational. ala, tables and lists."
// "but outside of those specific constructs, i would not expect
// role=presentation to do anything to child element roles"
const inheritsPresentationChain = {
// valid parent elements, any other element will prevent any
// children from inheriting a presentational role from a valid
// ancestor
td: ['tr'],
th: ['tr'],
tr: ['thead', 'tbody', 'tfoot', 'table'],
thead: ['table'],
tbody: ['table'],
tfoot: ['table'],
li: ['ol', 'ul'],
// dts and dds can be wrapped in divs and the div will pass through
// the presentation role
dt: ['dl', 'div'],
dd: ['dl', 'div'],
div: ['dl']
};

// role presentation inheritance.
// Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none
function getInheritedRole(vNode, explicitRoleOptions) {
const parentNodeNames = inheritsPresentationChain[vNode.props.nodeName];
if (!parentNodeNames) {
return null;
}

// if we can't look at the parent then we can't know if the node
// inherits the presentational role or not
if (!vNode.parent) {
throw new ReferenceError(
'Cannot determine role presentational inheritance of a required parent outside the current scope.'
);
}

// parent is not a valid ancestor that can inherit presentation
if (!parentNodeNames.includes(vNode.parent.props.nodeName)) {
return null;
}

const parentRole = getExplicitRole(vNode.parent, explicitRoleOptions);
if (
['none', 'presentation'].includes(parentRole) &&
!hasConflictResolution(vNode.parent)
) {
return parentRole;
}

// an explicit role of anything other than presentational will
// prevent any children from inheriting a presentational role
// from a valid ancestor
if (parentRole) {
return null;
}

return getInheritedRole(vNode.parent, explicitRoleOptions);
}

function resolveImplicitRole(vNode, explicitRoleOptions) {
const implicitRole = getImplicitRole(vNode);

if (!implicitRole) {
return null;
}

const presentationalRole = getInheritedRole(vNode, explicitRoleOptions);
if (presentationalRole) {
return presentationalRole;
}

return implicitRole;
}

// role conflict resolution
// note: Chrome returns a list with resolved role as "generic"
// instead of as a list
// (e.g. <ul role="none" aria-label><li>hello</li></ul>)
// we will return it as a list as that is the best option.
// Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none
// See also: https://github.com/w3c/aria/issues/1270
function hasConflictResolution(vNode) {
const hasGlobalAria = lookupTable.globalAttributes.some(attr =>
vNode.hasAttr(attr)
);
return hasGlobalAria || isFocusable(vNode.actualNode);
}

/**
* Return the semantic role of an element
* Return the semantic role of an element.
*
* @method getRole
* @memberof axe.commons.aria
Expand All @@ -19,20 +127,31 @@ import AbstractVirtuaNode from '../../core/base/virtual-node/abstract-virtual-no
*
* @deprecated noImplicit option is deprecated. Use aria.getExplicitRole instead.
*/
function getRole(node, { noImplicit, fallback, abstracts, dpub } = {}) {
function getRole(node, { noImplicit, ...explicitRoleOptions } = {}) {
const vNode =
node instanceof AbstractVirtuaNode ? node : getNodeFromTree(node);
if (vNode.props.nodeType !== 1) {
return null;
}
const explicitRole = getExplicitRole(vNode, { fallback, abstracts, dpub });

// Get the implicit role, if permitted
if (!explicitRole && !noImplicit) {
return getImplicitRole(vNode);
const explicitRole = getExplicitRole(vNode, explicitRoleOptions);

if (!explicitRole) {
return noImplicit ? null : resolveImplicitRole(vNode, explicitRoleOptions);
}

if (!['presentation', 'none'].includes(explicitRole)) {
return explicitRole;
}

if (hasConflictResolution(vNode)) {
// return null if there is a conflict resolution but no implicit
// has been set as the explicit role is not the true role
return noImplicit ? null : resolveImplicitRole(vNode, explicitRoleOptions);
}

return explicitRole || null;
// role presentation or none and no conflict resolution
return explicitRole;
}

export default getRole;
23 changes: 20 additions & 3 deletions lib/commons/aria/lookup-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import idrefs from '../dom/idrefs';
import isColumnHeader from '../table/is-column-header';
import isRowHeader from '../table/is-row-header';
import sanitize from '../text/sanitize';
import isFocusable from '../dom/is-focusable';
import { closest } from '../../core/utils';

const isNull = value => value === null;
Expand Down Expand Up @@ -1587,7 +1588,8 @@ lookupTable.role = {
'aria-required',
'aria-expanded',
'aria-readonly',
'aria-errormessage'
'aria-errormessage',
'aria-orientation'
]
},
owned: {
Expand All @@ -1597,7 +1599,7 @@ lookupTable.role = {
context: null,
unsupported: false,
allowedElements: {
nodeName: ['ol', 'ul']
nodeName: ['ol', 'ul', 'fieldset']
}
},
range: {
Expand Down Expand Up @@ -2170,7 +2172,19 @@ lookupTable.implicitHtmlRole = {
},
hr: 'separator',
img: vNode => {
return vNode.hasAttr('alt') && !vNode.attr('alt') ? null : 'img';
// an images role is considered implicitly presentation if the
// alt attribute is empty. But that shouldn't be the case if it
// has global aria attributes or is focusable, so we need to
// override the role back to `img`
// e.g. <img alt="" aria-label="foo"></img>
const emptyAlt = vNode.hasAttr('alt') && !vNode.attr('alt');
const hasGlobalAria = lookupTable.globalAttributes.find(attr =>
vNode.hasAttr(attr)
);

return emptyAlt && !hasGlobalAria && !isFocusable(vNode.actualNode)
? 'presentation'
: 'img';
},
input: vNode => {
// Source: https://www.w3.org/TR/html52/sec-forms.html#suggestions-source-element
Expand Down Expand Up @@ -2204,6 +2218,9 @@ lookupTable.implicitHtmlRole = {
return !suggestionsSourceElement ? 'searchbox' : 'combobox';
}
},
// Note: if an li (or some other elms) do not have a required
// parent, Firefox ignores the implicit semantic role and treats
// it as a generic text.
li: 'listitem',
main: 'main',
math: 'math',
Expand Down
Loading