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(commons/standards): create the commons/standards object for helper functions against the standards table #2358

Merged
merged 4 commits into from
Jul 10, 2020
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
4 changes: 3 additions & 1 deletion lib/commons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as color from './color';
import * as dom from './dom';
import * as forms from './forms';
import matches from './matches';
import * as standards from './standards';
import * as table from './table';
import * as text from './text';
import * as utils from '../core/utils';
Expand All @@ -21,9 +22,10 @@ var commons = {
dom,
forms,
matches,
standards,
table,
text,
utils
};

export { aria, color, dom, forms, matches, table, text, utils };
export { aria, color, dom, forms, matches, standards, table, text, utils };
14 changes: 14 additions & 0 deletions lib/commons/standards/get-aria-roles-by-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import standards from '../../standards';

/**
* Return a list of aria roles whose type matches the provided value.
* @param {String} type The desired role type
* @return {String[]} List of all roles matching the type
*/
function getAriaRolesByType(type) {
return Object.keys(standards.ariaRoles).filter(roleName => {
return standards.ariaRoles[roleName].type === type;
});
}

export default getAriaRolesByType;
49 changes: 49 additions & 0 deletions lib/commons/standards/get-element-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import standards from '../../standards';
import matchesFn from '../../commons/matches';

/**
* Return the spec for an HTML element from the standards object. Since the spec is determined by the node and what attributes it has, a node is required.
* @param {VirtualNode} vNode The VirtualNode to get the spec for.
* @return {Object} The standard spec object
*/
function getElementSpec(vNode) {
const standard = standards.htmlElms[vNode.props.nodeName];

if (!standard.variant) {
return standard;
}

// start with the information at the top level
const { variant, ...spec } = standard;

// loop through all variants (excluding default) finding anything
// that matches
for (const variantName in variant) {
if (!variant.hasOwnProperty(variantName) || variantName === 'default') {
continue;
}

const { matches, ...props } = variant[variantName];
if (matchesFn(vNode, matches)) {
for (const propName in props) {
if (props.hasOwnProperty(propName)) {
spec[propName] = props[propName];
}
}
}
}

// apply defaults if properties were not found
for (const propName in variant.default) {
if (
variant.default.hasOwnProperty(propName) &&
typeof spec[propName] === 'undefined'
) {
spec[propName] = variant.default[propName];
}
}

return spec;
}

export default getElementSpec;
13 changes: 13 additions & 0 deletions lib/commons/standards/get-global-aria-attrs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import standards from '../../standards';

/**
* Return a list of global aria attributes.
* @return {String[]} List of all global aria attributes
*/
function getGlobalAriaAttrs() {
return Object.keys(standards.ariaAttrs).filter(attrName => {
return standards.ariaAttrs[attrName].global;
});
}

export default getGlobalAriaAttrs;
8 changes: 8 additions & 0 deletions lib/commons/standards/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Namespace for standards-related utilities.
* @namespace commons.standards
* @memberof axe
*/
export { default as getAriaRolesByType } from './get-aria-roles-by-type';
export { default as getGlobalAriaAttrs } from './get-global-aria-attrs';
export { default as getElementSpec } from './get-element-spec';
29 changes: 22 additions & 7 deletions lib/standards/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
import ariaAttrs from './aria-attrs';
import { clone, deepMerge } from '../core/utils';
import ariaRoles from './aria-roles';
import dpubRoles from './dpub-roles';
import htmlElms from './html-elms';
import { deepMerge } from '../core/utils';

const origAriaAttrs = clone(ariaAttrs);
const originals = {
ariaAttrs,
ariaRoles: {
...ariaRoles,
...dpubRoles
},
htmlElms
};
const standards = {
ariaAttrs
...originals
};

export function configureStandards(config) {
if (config.ariaAttrs) {
standards.ariaAttrs = deepMerge(standards.ariaAttrs, config.ariaAttrs);
}
Object.keys(standards).forEach(propName => {
if (config[propName]) {
standards[propName] = deepMerge(standards[propName], config[propName]);
}
});
}

export function resetStandards() {
standards.ariaAttrs = origAriaAttrs;
Object.keys(standards).forEach(propName => {
standards[propName] = originals[propName];
});
}

export default standards;
1 change: 1 addition & 0 deletions test/commons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('axe.commons', function() {
assert.exists(axe.commons.dom);
assert.exists(axe.commons.forms);
assert.exists(axe.commons.matches);
assert.exists(axe.commons.standards);
assert.exists(axe.commons.table);
assert.exists(axe.commons.text);
assert.exists(axe.commons.utils);
Expand Down
72 changes: 72 additions & 0 deletions test/commons/standards/get-aria-roles-by-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
describe('standards.getAriaRolesByType', function() {
var getAriaRolesByType = axe.commons.standards.getAriaRolesByType;

before(function() {
axe._load({});
});

after(function() {
axe.reset();
});

it('should return a list of role names by type', function() {
// Source: https://www.w3.org/TR/wai-aria-1.1/#document_structure_roles
var structureRoles = getAriaRolesByType('structure');
assert.deepEqual(structureRoles, [
'article',
'cell',
'columnheader',
'definition',
'directory',
'document',
'feed',
'figure',
'group',
'heading',
'img',
'list',
'listitem',
'math',
'note',
'presentation',
'row',
'rowgroup',
'rowheader',
'separator',
'table',
'term',
'toolbar',
'tooltip'
]);
});

it('should return configured roles', function() {
axe.configure({
standards: {
ariaRoles: {
myRole: {
type: 'structure'
}
}
}
});

var structureRoles = getAriaRolesByType('structure');
assert.include(structureRoles, 'myRole');
});

it('should not return role that is configured to not be of the type', function() {
axe.configure({
standards: {
ariaRoles: {
article: {
type: 'notstructure'
}
}
}
});

var structureRoles = getAriaRolesByType('structure');
assert.notInclude(structureRoles, 'article');
});
});
99 changes: 99 additions & 0 deletions test/commons/standards/get-element-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
describe('standards.getElementSpec', function() {
var getElementSpec = axe.commons.standards.getElementSpec;
var queryFixture = axe.testUtils.queryFixture;
var fixture = document.querySelector('#fixture');

before(function() {
axe._load({});
});

afterEach(function() {
fixture.innerHTML = '';
});

after(function() {
axe.reset();
});

it('should return a spec for an element without variants', function() {
axe.configure({
standards: {
htmlElms: {
abbr: {
contentTypes: ['phrasing', 'flow'],
allowedRoles: true
}
}
}
});

var vNode = queryFixture('<abbr id="target"></abbr>');
assert.deepEqual(getElementSpec(vNode), {
contentTypes: ['phrasing', 'flow'],
allowedRoles: true
});
});

describe('variants', function() {
before(function() {
axe.configure({
standards: {
htmlElms: {
abbr: {
variant: {
controls: {
matches: '[controls]',
customProp: 'controls'
},
label: {
matches: '[aria-label]',
anotherProp: 'label'
},
default: {
customProp: 'default',
anotherProp: 'default'
}
},
allowedRoles: false
}
}
}
});
});

it('should return top level properties', function() {
var vNode = queryFixture('<abbr id="target" controls></abbr>');
var spec = getElementSpec(vNode);
assert.equal(spec.allowedRoles, false);
});

it('should return properties from matching variant', function() {
var vNode = queryFixture('<abbr id="target" controls></abbr>');
var spec = getElementSpec(vNode);
assert.equal(spec.customProp, 'controls');
});

it('should return all properties from matching variants', function() {
var vNode = queryFixture(
'<abbr id="target" controls aria-label="foo"></abbr>'
);
var spec = getElementSpec(vNode);
assert.equal(spec.customProp, 'controls');
assert.equal(spec.anotherProp, 'label');
});

it('should return default props in no variants match', function() {
var vNode = queryFixture('<abbr id="target"></abbr>');
var spec = getElementSpec(vNode);
assert.equal(spec.customProp, 'default');
assert.equal(spec.anotherProp, 'default');
});

it('should return default props that were not part of other matches', function() {
var vNode = queryFixture('<abbr id="target" controls></abbr>');
var spec = getElementSpec(vNode);
assert.equal(spec.customProp, 'controls');
assert.equal(spec.anotherProp, 'default');
});
});
});
69 changes: 69 additions & 0 deletions test/commons/standards/get-global-aria-attrs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
describe('standards.getGlobalAriaAttrs', function() {
var getGlobalAriaAttrs = axe.commons.standards.getGlobalAriaAttrs;

before(function() {
axe._load({});
});

after(function() {
axe.reset();
});

it('should return global attrs', function() {
// Source: https://www.w3.org/TR/wai-aria-1.1/#global_states
var globalAttrs = getGlobalAriaAttrs();
assert.deepEqual(globalAttrs, [
'aria-atomic',
'aria-busy',
'aria-controls',
'aria-current',
'aria-describedby',
'aria-details',
'aria-disabled',
'aria-dropeffect',
'aria-errormessage',
'aria-flowto',
'aria-grabbed',
'aria-haspopup',
'aria-hidden',
'aria-invalid',
'aria-keyshortcuts',
'aria-label',
'aria-labelledby',
'aria-live',
'aria-owns',
'aria-relevant',
'aria-roledescription'
]);
});

it('should return configured global attrs', function() {
axe.configure({
standards: {
ariaAttrs: {
myAttr: {
global: true
}
}
}
});

var globalAttrs = getGlobalAriaAttrs();
assert.include(globalAttrs, 'myAttr');
});

it('should not return global attr that is configured to not be global', function() {
axe.configure({
standards: {
ariaAttrs: {
'aria-atomic': {
global: false
}
}
}
});

var globalAttrs = getGlobalAriaAttrs();
assert.notInclude(globalAttrs, 'aria-atomic');
});
});