diff --git a/build/tasks/aria-supported.js b/build/tasks/aria-supported.js index 24e81b8c1c..12ff0ab2df 100644 --- a/build/tasks/aria-supported.js +++ b/build/tasks/aria-supported.js @@ -10,108 +10,188 @@ module.exports = function(grunt) { 'aria-supported', 'Task for generating a diff of supported aria roles and properties.', function() { - const entry = this.data.entry; - const destFile = this.data.destFile; - const listType = this.data.listType.toLowerCase(); - /** - * `axe` has to be dynamically required at this stage, as `axe` does not exist until grunt task `build:uglify` is complete, and hence cannot be required at the top of the file. + * NOTE: + * `axe` has to be dynamically required at this stage, + * as `axe` does not exist until grunt task `build:uglify` is complete, + * hence cannot be required at the top of the file. */ const axe = require('../../axe'); + const listType = this.data.listType.toLowerCase(); + const headings = { + main: + `# ARIA Roles and Attributes ${ + listType === 'all' ? 'available' : listType + } in axe-core.\n\n` + + 'It can be difficult to know which features of web technologies are accessible across ' + + 'different platforms, and with different screen readers and other assistive technologies. ' + + 'Axe-core does some of this work for you, by raising issues when accessibility features are ' + + 'used that are known to cause problems.\n\n' + + 'This page contains a list of ARIA 1.1 features that axe-core raises as unsupported. ' + + 'For more information, read [We’ve got your back with “Accessibility Supported” in axe]' + + '(https://www.deque.com/blog/weve-got-your-back-with-accessibility-supported-in-axe/).\n\n' + + 'For a detailed description about how accessibility support is decided, see [How we make ' + + 'decisions on rules](accessibility-supported.md).', + rolesMdTableHeader: ['aria-role', 'axe-core support'], + attributesMdTableHeader: ['aria-attribute', 'axe-core support'] + }; + + const { diff: rolesTable, notes: rolesFootnotes } = getDiff( + roles, + axe.commons.aria.lookupTable.role, + listType + ); + const rolesTableMarkdown = mdTable([ + headings.rolesMdTableHeader, + ...rolesTable + ]); + + const ariaQueryAriaAttributes = getAriaQueryAttributes(); + const { diff: attributesTable, notes: attributesFootnotes } = getDiff( + ariaQueryAriaAttributes, + axe.commons.aria.lookupTable.attributes, + listType + ); + const attributesTableMarkdown = mdTable([ + headings.attributesMdTableHeader, + ...attributesTable + ]); + + const footnotes = [...rolesFootnotes, ...attributesFootnotes].map( + (footnote, index) => `[^${index + 1}]: ${footnote}` + ); + + const content = `${ + headings.main + }\n\n## Roles\n\n${rolesTableMarkdown}\n\n## Attributes\n\n${attributesTableMarkdown}\n\n${footnotes}`; + + const destFile = this.data.destFile; + // Format the content so Prettier doesn't create a diff after running. + // See https://github.com/dequelabs/axe-core/issues/1310. + const formattedContent = format(content, destFile); + + // write `aria supported` file contents + grunt.file.write(destFile, formattedContent); /** - * As `aria-query` roles map, does not list all aria attributes in its props, - * the below reduce function aims to concatanate and unique the below two, - * - list from props with in roles map - * - list from aria map - * - * @return {Map} `aQaria` - This gives a composite list of aria attributes, which is later used to diff against axe-core supported attributes. + * Get list of aria attributes, from `aria-query` + * @returns {Set|Object} collection of aria attributes from `aria-query` module */ - const ariaKeys = Array.from(props).map(([key]) => key); - const roleAriaKeys = Array.from(roles).reduce((out, [name, rule]) => { - return [...out, ...Object.keys(rule.props)]; - }, []); - const aQaria = new Set(axe.utils.uniqueArray(roleAriaKeys, ariaKeys)); + function getAriaQueryAttributes() { + const ariaKeys = Array.from(props).map(([key]) => key); + const roleAriaKeys = Array.from(roles).reduce((out, [name, rule]) => { + return [...out, ...Object.keys(rule.props)]; + }, []); + return new Set(axe.utils.uniqueArray(roleAriaKeys, ariaKeys)); + } /** * Given a `base` Map and `subject` Map object, * The function converts the `base` Map entries to an array which is sorted then enumerated to compare each entry against the `subject` Map - * The function constructs a `string` to represent a `markdown table` to + * The function constructs a `string` to represent a `markdown table`, as well as returns notes to append to footnote * @param {Map} base Base Map Object * @param {Map} subject Subject Map Object - * @return {Array[]} Example Output: [ [ 'alert', 'No' ], [ 'figure', 'Yes' ] ] + * @param {String} type type to compare + * @returns {Array[]} + * @example Example Output: [ [ 'alert', 'No' ], [ 'figure', 'Yes' ] ] */ - const getDiff = (base, subject) => { - return Array.from(base.entries()) - .sort() - .reduce((out, [key] = item) => { - switch (listType) { - case 'supported': - if ( - subject.hasOwnProperty(key) && - subject[key].unsupported === false - ) { - out.push([`${key}`, 'Yes']); - } - break; - case 'unsupported': - if ( - (subject[key] && subject[key].unsupported === true) || - !subject.hasOwnProperty(key) - ) { - out.push([`${key}`, 'No']); + function getDiff(base, subject, type) { + const diff = []; + const notes = []; + + const sortedBase = Array.from(base.entries()).sort(); + + sortedBase.forEach(([key] = item) => { + switch (type) { + case 'supported': + if ( + subject.hasOwnProperty(key) && + subject[key].unsupported === false + ) { + diff.push([`${key}`, 'Yes']); + } + break; + case 'unsupported': + if ( + (subject[key] && subject[key].unsupported === true) || + !subject.hasOwnProperty(key) + ) { + diff.push([`${key}`, 'No']); + } else if ( + subject[key] && + subject[key].unsupported && + subject[key].unsupported.exceptions + ) { + diff.push([`${key}`, `Mixed[^${notes.length + 1}]`]); + notes.push( + getSupportedElementsAsFootnote( + subject[key].unsupported.exceptions + ) + ); + } + break; + case 'all': + default: + diff.push([ + `${key}`, + subject.hasOwnProperty(key) && + subject[key].unsupported === false + ? 'Yes' + : 'No' + ]); + break; + } + }); + + return { + diff, + notes + }; + } + + /** + * Parse a list of unsupported exception elements and add a footnote + * detailing which HTML elements are supported. + * + * @param {Array} elements List of supported elements + * @returns {Array} notes + */ + function getSupportedElementsAsFootnote(elements) { + const notes = []; + + const supportedElements = elements.map(element => { + if (typeof element === 'string') { + return `\`<${element}>\``; + } + + /** + * if element is not a string it will be an object with structure: + { + nodeName: string, + properties: { + type: {string|string[]} } - break; - case 'all': - default: - out.push([ - `${key}`, - subject.hasOwnProperty(key) && - subject[key].unsupported === false - ? 'Yes' - : 'No' - ]); - break; + } + */ + return Object.keys(element.properties).map(prop => { + const value = element.properties[prop]; + + // the 'type' property can be a string or an array + if (typeof value === 'string') { + return `\`<${element.nodeName} ${prop}="${value}">\``; } - return out; - }, []); - }; - const getMdContent = (heading, rolesTable, attributesTable) => { - return `${heading}\n\n## Roles\n\n${rolesTable}\n\n## Attributes\n\n${attributesTable}`; - }; + // output format for an array of types: + // + const values = value.map(v => `"${v}"`).join(' | '); + return `\`<${element.nodeName} ${prop}=${values}>\``; + }); + }); - const generateDoc = () => { - const content = getMdContent( - `# ARIA Roles and Attributes ${ - listType === 'all' ? 'available' : listType - } in axe-core.\n\n` + - 'It can be difficult to know which features of web technologies are accessible across ' + - 'different platforms, and with different screen readers and other assistive technologies. ' + - 'Axe-core does some of this work for you, by raising issues when accessibility features are ' + - 'used that are known to cause problems.\n\n' + - 'This page contains a list of ARIA 1.1 features that axe-core raises as unsupported. ' + - 'For more information, read [We’ve got your back with “Accessibility Supported” in axe]' + - '(https://www.deque.com/blog/weve-got-your-back-with-accessibility-supported-in-axe/).\n\n' + - 'For a detailed description about how accessibility support is decided, see [How we make ' + - 'decisions on rules](accessibility-supported.md).', - mdTable([ - ['aria-role', 'axe-core support'], - ...getDiff(roles, axe.commons.aria.lookupTable.role) - ]), - mdTable([ - ['aria-attribute', 'axe-core support'], - ...getDiff(aQaria, axe.commons.aria.lookupTable.attributes) - ]) - ); - - // Format the content so Prettier doesn't create a diff after running. - // See https://github.com/dequelabs/axe-core/issues/1310. - const formattedContent = format(content, destFile); - grunt.file.write(destFile, formattedContent); - }; + notes.push('Supported on elements: ' + supportedElements.join(', ')); - generateDoc(); + return notes; + } } ); }; diff --git a/doc/aria-supported.md b/doc/aria-supported.md index 285d5b1a6f..e568c0db0f 100644 --- a/doc/aria-supported.md +++ b/doc/aria-supported.md @@ -18,4 +18,6 @@ For a detailed description about how accessibility support is decided, see [How | -------------------- | ---------------- | | aria-describedat | No | | aria-details | No | -| aria-roledescription | No | +| aria-roledescription | Mixed[^1] | + +[^1]: Supported on elements: ` + diff --git a/test/integration/rules/aria-allowed-attr/passes.json b/test/integration/rules/aria-allowed-attr/passes.json index 1b34b38f30..8c4b057b05 100644 --- a/test/integration/rules/aria-allowed-attr/passes.json +++ b/test/integration/rules/aria-allowed-attr/passes.json @@ -74,6 +74,8 @@ ["#pass69"], ["#pass70"], ["#pass71"], - ["#pass72"] + ["#pass72"], + ["#pass73"], + ["#pass74"] ] }