diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 722093522b..8ba4e7bea9 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -4,6 +4,7 @@ on: [pull_request] jobs: prettier: + if: ${{ false }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/semantic-pr-title.yml b/.github/workflows/semantic-pr-title.yml index 181cd23986..8ae2854010 100644 --- a/.github/workflows/semantic-pr-title.yml +++ b/.github/workflows/semantic-pr-title.yml @@ -10,6 +10,7 @@ on: jobs: semantic-pr-title: + if: ${{ false }} runs-on: ubuntu-latest steps: - uses: dequelabs/semantic-pr-title@v1 diff --git a/.gitignore b/.gitignore index 3331c6fdbb..338db02b52 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ typings/axe-core/axe-core-tests.js # doc doc/rule-descriptions.*.md + +.history diff --git a/bower.json b/bower.json index 87018d7ffa..5eba3dd57b 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "axe-core", - "version": "4.10.0", + "version": "4.10.0-canary.1", "deprecated": true, "contributors": [ { diff --git a/build/configure.js b/build/configure.js index ca3241fbd3..fab2406b86 100644 --- a/build/configure.js +++ b/build/configure.js @@ -10,10 +10,8 @@ var { encode } = require('html-entities'); var packageJSON = require('../package.json'); var doTRegex = /\{\{.+?\}\}/g; -var axeVersion = packageJSON.version.substring( - 0, - packageJSON.version.lastIndexOf('.') -); +var _v = packageJSON.version.replace(/-\w+\.\w+$/, ''); +var axeVersion = _v.substring(0, _v.lastIndexOf('.')); var descriptionTableHeader = '| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules |\n| :------- | :------- | :------- | :------- | :------- | :------- |\n'; diff --git a/build/tasks/update-help.js b/build/tasks/update-help.js index 417a94048c..0e947549fb 100644 --- a/build/tasks/update-help.js +++ b/build/tasks/update-help.js @@ -9,7 +9,7 @@ module.exports = function (grunt) { var options = this.options({ version: '1.0.0' }); - var v = options.version.split('.'); + var v = options.version.replace(/-\w+\.\w+$/, '').split('.'); v.pop(); var baseUrl = 'https://dequeuniversity.com/rules/axe/' + v.join('.') + '/'; diff --git a/build/tasks/validate.js b/build/tasks/validate.js index 8eb5a2eba5..e063bdf6fe 100644 --- a/build/tasks/validate.js +++ b/build/tasks/validate.js @@ -310,6 +310,7 @@ function validateRule({ tags, metadata }) { const miscTags = ['ACT', 'experimental', 'review-item', 'deprecated']; const categories = [ + 'epub', 'aria', 'color', 'forms', diff --git a/doc/examples/qunit/Gruntfile.js b/doc/examples/qunit/Gruntfile.js index bc50188c67..b582446b26 100644 --- a/doc/examples/qunit/Gruntfile.js +++ b/doc/examples/qunit/Gruntfile.js @@ -5,7 +5,7 @@ module.exports = function (grunt) { grunt.initConfig({ qunit: { - all: ['test/**/*.html'], + all: ['test/**/*.html', 'test/**/*__.xhtml'], options: { puppeteer: { ignoreDefaultArgs: true, diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index cd10e730b6..cb57e0fdd5 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -87,9 +87,10 @@ These rules are disabled by default, until WCAG 2.2 is more widely adopted and required. -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :------------------------------------------------------------------------------------------------ | :-------------------------------------------------- | :------ | :--------------------------------------------- | :------------------------- | :-------- | -| [target-size](https://dequeuniversity.com/rules/axe/4.10/target-size?application=RuleDescription) | Ensure touch targets have sufficient size and space | Serious | cat.sensory-and-visual-cues, wcag22aa, wcag258 | failure, needs review | | +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :-------------------------------------------------------------------------------------------------------- | :-------------------------------------------------- | :------- | :--------------------------------------------- | :------------------------- | :-------- | +| [pagebreak-label](https://dequeuniversity.com/rules/axe/4.10/pagebreak-label?application=RuleDescription) | Ensure page markers have an accessible label | Moderate | cat.epub | failure | | +| [target-size](https://dequeuniversity.com/rules/axe/4.10/target-size?application=RuleDescription) | Ensure touch targets have sufficient size and space | Serious | cat.sensory-and-visual-cues, wcag22aa, wcag258 | failure, needs review | | ## Best Practices Rules @@ -104,6 +105,7 @@ Rules that do not necessarily conform to WCAG success criterion but are industry | [aria-treeitem-name](https://dequeuniversity.com/rules/axe/4.10/aria-treeitem-name?application=RuleDescription) | Ensure every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | | [empty-heading](https://dequeuniversity.com/rules/axe/4.10/empty-heading?application=RuleDescription) | Ensure headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | [ffd0e9](https://act-rules.github.io/rules/ffd0e9) | | [empty-table-header](https://dequeuniversity.com/rules/axe/4.10/empty-table-header?application=RuleDescription) | Ensure table headers have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | +| [epub-type-has-matching-role](https://dequeuniversity.com/rules/axe/4.10/epub-type-has-matching-role?application=RuleDescription) | Ensure the element has an ARIA role matching its epub:type | Moderate | cat.aria, best-practice | failure | | | [frame-tested](https://dequeuniversity.com/rules/axe/4.10/frame-tested?application=RuleDescription) | Ensure <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, best-practice, review-item | failure, needs review | | | [heading-order](https://dequeuniversity.com/rules/axe/4.10/heading-order?application=RuleDescription) | Ensure the order of headings is semantically correct | Moderate | cat.semantics, best-practice | failure, needs review | | | [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.10/image-redundant-alt?application=RuleDescription) | Ensure image alternative is not repeated as text | Minor | cat.text-alternatives, best-practice | failure | | diff --git a/lib/checks/aria/aria-prohibited-attr-evaluate.js b/lib/checks/aria/aria-prohibited-attr-evaluate.js index cc9cefd5f8..2ef62b6a71 100644 --- a/lib/checks/aria/aria-prohibited-attr-evaluate.js +++ b/lib/checks/aria/aria-prohibited-attr-evaluate.js @@ -34,7 +34,7 @@ export default function ariaProhibitedAttrEvaluate( ) { const elementsAllowedAriaLabel = options?.elementsAllowedAriaLabel || []; const { nodeName } = virtualNode.props; - const role = getRole(virtualNode, { chromium: true }); + const role = getRole(virtualNode, { dpub: true, chromium: true }); const prohibitedList = listProhibitedAttrs( virtualNode, diff --git a/lib/checks/aria/aria-required-children-evaluate.js b/lib/checks/aria/aria-required-children-evaluate.js index 1e7512d960..f2662ec75c 100644 --- a/lib/checks/aria/aria-required-children-evaluate.js +++ b/lib/checks/aria/aria-required-children-evaluate.js @@ -75,6 +75,8 @@ export default function ariaRequiredChildrenEvaluate( */ function getOwnedRoles(virtualNode, required) { let vNode; + const parentRole = getRole(virtualNode, { dpub: true }); + const ownedRoles = []; const ownedVirtual = getOwnedVirtual(virtualNode); while ((vNode = ownedVirtual.shift())) { @@ -85,7 +87,8 @@ function getOwnedRoles(virtualNode, required) { continue; } - const role = getRole(vNode, { noPresentational: true }); + const role = getRole(vNode, { noPresentational: true, dpub: true }); + const globalAriaAttr = getGlobalAriaAttr(vNode); const hasGlobalAriaOrFocusable = !!globalAriaAttr || isFocusable(vNode); @@ -96,7 +99,9 @@ function getOwnedRoles(virtualNode, required) { if ( (!role && !hasGlobalAriaOrFocusable) || (['group', 'rowgroup'].includes(role) && - required.some(requiredRole => requiredRole === role)) + required.some(requiredRole => requiredRole === role)) || + (['list'].includes(role) && + ['doc-bibliography', 'doc-endnotes'].includes(parentRole)) ) { ownedVirtual.push(...vNode.children); } else if (role || hasGlobalAriaOrFocusable) { diff --git a/lib/checks/aria/matching-aria-role-evaluate.js b/lib/checks/aria/matching-aria-role-evaluate.js new file mode 100644 index 0000000000..ca1d7316bc --- /dev/null +++ b/lib/checks/aria/matching-aria-role-evaluate.js @@ -0,0 +1,190 @@ +import { tokenList } from '../../core/utils'; +import standards from '../../standards'; +import { getRole } from '../../commons/aria'; +import matchesSelector from '../../core/utils/element-matches'; + +function matchingAriaRoleEvaluate(node) { + // https://idpf.github.io/epub-guides/epub-aria-authoring/#sec-mappings + // https://www.w3.org/TR/dpub-aam-1.0/#mapping_role_table + // https://w3c.github.io/publ-cg/guides/aria-mapping.html#mapping-table + const mappings = new Map([ + ['abstract', 'doc-abstract'], + ['acknowledgments', 'doc-acknowledgments'], + ['afterword', 'doc-afterword'], + // ['answer', '??'], + // ['answers', '??'], + ['appendix', 'doc-appendix'], + // ['assessment', '??'], + // ['assessments', '??'], + // ['backmatter', '??'], + // ['balloon', '??'], + // ['backlink', 'doc-backlink'], // ?? + ['biblioentry', 'doc-biblioentry'], + ['bibliography', 'doc-bibliography'], + ['biblioref', 'doc-biblioref'], + // ['bodymatter', '??'], + // ['bridgehead', '??'], + // ['case-study', '??'], + ['chapter', 'doc-chapter'], + ['colophon', 'doc-colophon'], + // ['concluding-sentence', '??'], + ['conclusion', 'doc-conclusion'], + // ['contributors', '??'], + // ['copyright-page', '??'], + // ['cover', '??'], + // ['cover-image', 'doc-cover'], // ?? + // ['covertitle', '??'], + ['credit', 'doc-credit'], + ['credits', 'doc-credits'], + ['dedication', 'doc-dedication'], + // ['division', '??'], + ['endnote', 'doc-endnote'], + ['endnotes', 'doc-endnotes'], + ['epigraph', 'doc-epigraph'], + ['epilogue', 'doc-epilogue'], + ['errata', 'doc-errata'], + // ['example', 'doc-example'], + // ['feedback', '??'], + ['figure', 'figure'], // ARIA + // ['fill-in-the-blank-problem', '??'], + ['footnote', 'doc-footnote'], + // ['footnotes', '??'], + ['foreword', 'doc-foreword'], + // ['frontmatter', '??'], + // ['fulltitle', '??'], + // ['general-problem', '??'], + ['glossary', 'doc-glossary'], + ['glossdef', 'definition'], // ARIA + ['glossref', 'doc-glossref'], + ['glossterm', 'term'], // ARIA + // ['halftitle', '??'], + // ['halftitlepage', '??'], + // ['imprimatur', '??'], + // ['imprint', '??'], + ['help', 'doc-tip'], // ?? + ['index', 'doc-index'], + // ['index-editor-note', '??'], + // ['index-entry', '??'], + // ['index-entry-list', '??'], + // ['index-group', '??'], + // ['index-headnotes', '??'], + // ['index-legend', '??'], + // ['index-locator', '??'], + // ['index-locator-list', '??'], + // ['index-locator-range', '??'], + // ['index-term', '??'], + // ['index-term-categories', '??'], + // ['index-term-category', '??'], + // ['index-xref-preferred', '??'], + // ['index-xref-related', '??'], + ['introduction', 'doc-introduction'], + // ['keyword', '??'], + // ['keywords', '??'], + // ['label', '??'], + // ['landmarks', 'directory'], // ARIA (SKIPPED! NavDoc) + // ['learning-objective', '??'], + // ['learning-objectives', '??'], + // ['learning-outcome', '??'], + // ['learning-outcomes', '??'], + // ['learning-resource', '??'], + // ['learning-resources', '??'], + // ['learning-standard', '??'], + // ['learning-standards', '??'], + ['list', 'list'], // ARIA + ['list-item', 'listitem'], // ARIA + // ['loa', '??'], + // ['loi', '??'], + // ['lot', '??'], + // ['lov', '??'], + // ['match-problem', '??'], + // ['multiple-choice-problem', '??'], + ['noteref', 'doc-noteref'], + ['notice', 'doc-notice'], + // ['ordinal', '??'], + // ['other-credits', '??'], + ['page-list', 'doc-pagelist'], + ['pagebreak', 'doc-pagebreak'], + // ['panel', '??'], + // ['panel-group', '??'], + ['part', 'doc-part'], + // ['practice', '??'], + // ['practices', '??'], + // ['preamble', '??'], + ['preface', 'doc-preface'], + ['prologue', 'doc-prologue'], + ['pullquote', 'doc-pullquote'], + ['qna', 'doc-qna'], + // ['question', '??'], + ['referrer', 'doc-backlink'], + // ['revision-history', '??'], + // ['seriespage', '??'], + // ['sound-area', '??'], + // ['subchapter', '??'], + ['subtitle', 'doc-subtitle'], + ['table', 'table'], + ['table-cell', 'cell'], + ['table-row', 'row'], + // ['text-area', '??'], + ['tip', 'doc-tip'], + // ['title', '??'], + // ['titlepage', '??'], + ['toc', 'doc-toc'] + // ['toc-brief', '??'], + // ['topic-sentence', '??'], + // ['true-false-problem', '??'], + // ['volume', '??'], + ]); + + const hasXmlEpubType = node.hasAttributeNS( + 'http://www.idpf.org/2007/ops', + 'type' + ); + if ( + hasXmlEpubType || + node.hasAttribute('epub:type') // for unit tests that are not XML-aware due to fixture.innerHTML + ) { + // abort if descendant of landmarks nav (nav with epub:type=landmarks) + if ( + (hasXmlEpubType && matchesSelector(node, 'nav[*|type~="landmarks"] *')) || + matchesSelector(node, 'nav[epub\\:type~="landmarks"] *') + ) { + // console.log('BREAKPOINT'); + // throw new Error('BREAKPOINT'); + return true; + } + + // iterate for each epub:type value + var types = tokenList( + hasXmlEpubType + ? node.getAttributeNS('http://www.idpf.org/2007/ops', 'type') + : node.getAttribute('epub:type') + ); + for (const type of types) { + // If there is a 1-1 mapping, check that the role is set (best practice) + if (mappings.has(type)) { + // Note: using axe’s `getRole` util returns the effective role of the element + // (either explicitly set with the role attribute or implicit) + // So this works for types mapping to core ARIA roles (eg. glossref/glossterm). + const mappedRole = mappings.get(type); + const role = getRole(node, { dpub: true }); + const roleDefinition = standards.ariaRoles[mappedRole]; + if (!roleDefinition || roleDefinition.deprecated) { + return true; + } + // if (mappedRole !== role) { + // console.log('BREAKPOINT: ', type, mappedRole, role); + // // throw new Error('BREAKPOINT'); + // } + return mappedRole === role; + } else { + // e.g. cover, landmarks + // console.log('BREAKPOINT: ', type); + // throw new Error('BREAKPOINT'); + } + } + } + + return true; +} + +export default matchingAriaRoleEvaluate; diff --git a/lib/checks/aria/matching-aria-role.json b/lib/checks/aria/matching-aria-role.json new file mode 100644 index 0000000000..fb78acbe75 --- /dev/null +++ b/lib/checks/aria/matching-aria-role.json @@ -0,0 +1,11 @@ +{ + "id": "matching-aria-role", + "evaluate": "matching-aria-role-evaluate", + "metadata": { + "impact": "minor", + "messages": { + "pass": "Element has an ARIA role matching its epub:type", + "fail": "Element has no ARIA role matching its epub:type" + } + } +} diff --git a/lib/checks/landmarks/landmark-is-unique-after.js b/lib/checks/landmarks/landmark-is-unique-after.js index fd4eb2ec1f..71628c861c 100644 --- a/lib/checks/landmarks/landmark-is-unique-after.js +++ b/lib/checks/landmarks/landmark-is-unique-after.js @@ -1,11 +1,20 @@ function landmarkIsUniqueAfter(results) { const uniqueLandmarks = []; + // console.log("landmarkIsUniqueAfter results: ", JSON.stringify(results, null, 4)); + // filter out landmark elements that share the same role and accessible text // so every non-unique landmark isn't reported as a failure (just the first) - return results.filter(currentResult => { - const findMatch = someResult => { + var filtered = results.filter(currentResult => { + if (!currentResult.data) { + // console.log('landmarkIsUniqueAfterlandmarkIsUniqueAfter NO DATA???!!!'); + return false; + } + + var findMatch = someResult => { return ( + // currentResult.data.isLandmark && + // someResult.data.isLandmark && currentResult.data.role === someResult.data.role && currentResult.data.accessibleText === someResult.data.accessibleText ); @@ -22,6 +31,9 @@ function landmarkIsUniqueAfter(results) { currentResult.relatedNodes = []; return true; }); + + // console.log("landmarkIsUniqueAfter filtered: ", JSON.stringify(filtered, null, 4)); + return filtered; } export default landmarkIsUniqueAfter; diff --git a/lib/checks/landmarks/landmark-is-unique-evaluate.js b/lib/checks/landmarks/landmark-is-unique-evaluate.js index f8375fed5f..0a6e9151d7 100644 --- a/lib/checks/landmarks/landmark-is-unique-evaluate.js +++ b/lib/checks/landmarks/landmark-is-unique-evaluate.js @@ -1,11 +1,40 @@ -import { getRole } from '../../commons/aria'; +import { getRole } from '../../commons/aria'; // getRoleType import { accessibleTextVirtual } from '../../commons/text'; +// import { getAriaRolesByType } from '../../commons/standards'; function landmarkIsUniqueEvaluate(node, options, virtualNode) { - const role = getRole(node); - let accessibleText = accessibleTextVirtual(virtualNode); + var role = getRole(node, { dpub: true }); // fallback: true + if (!role) { + // this.data({ role: '', accessibleText: '', isLandmark: null }); + // console.log('landmarkIsUniqueEvaluate landmarkIsUniqueEvaluate landmarkIsUniqueEvaluate NO ROLE???!!!'); + return false; + } + + // var landmarks = getAriaRolesByType('landmark'); + // var roleType = getRoleType(role); + // var isLandmark = + // roleType === 'landmark' || + // landmarks.includes(roleType) || + // landmarks.includes(role); + + // if (!isLandmark) { + // // this.data({ role: '', accessibleText: '', isLandmark: null }); + // return false; + // } + // throw new Error('BREAK'); + + var accessibleText = accessibleTextVirtual(virtualNode); + + // console.log('\n\n ))))) ', virtualNode.props ? virtualNode.props.nodeName : '!virtualNode.props', role, roleType, JSON.stringify(landmarks), isLandmark, " [[" + accessibleText + "]]") + accessibleText = accessibleText ? accessibleText.toLowerCase() : null; - this.data({ role: role, accessibleText: accessibleText }); + + this.data({ + role: role, + accessibleText: accessibleText + // isLandmark: isLandmark + }); + this.relatedNodes([node]); return true; diff --git a/lib/checks/lists/listitem-evaluate.js b/lib/checks/lists/listitem-evaluate.js index dde5dcb4f4..13bb2c6278 100644 --- a/lib/checks/lists/listitem-evaluate.js +++ b/lib/checks/lists/listitem-evaluate.js @@ -1,4 +1,8 @@ -import { isValidRole, getExplicitRole } from '../../commons/aria'; +import { + getExplicitRole, + getSuperClassRole, + isValidRole +} from '../../commons/aria'; export default function listitemEvaluate(node, options, virtualNode) { const { parent } = virtualNode; @@ -15,6 +19,11 @@ export default function listitemEvaluate(node, options, virtualNode) { } if (parentRole && isValidRole(parentRole)) { + const sup = getSuperClassRole(parentRole); + if (sup && sup.includes('list')) { + return true; + } + this.data({ messageKey: 'roleNotValid' }); diff --git a/lib/checks/lists/only-listitems-evaluate.js b/lib/checks/lists/only-listitems-evaluate.js index 93ab74adfa..ff9d27d728 100644 --- a/lib/checks/lists/only-listitems-evaluate.js +++ b/lib/checks/lists/only-listitems-evaluate.js @@ -1,5 +1,6 @@ import { isVisibleToScreenReaders } from '../../commons/dom'; -import { getRole } from '../../commons/aria'; + +import { getRole, getSuperClassRole } from '../../commons/aria'; /** * @deprecated @@ -27,7 +28,11 @@ function onlyListitemsEvaluate(node, options, virtualNode) { isEmpty = false; const isLi = actualNode.nodeName.toUpperCase() === 'LI'; const role = getRole(vNode); - const isListItemRole = role === 'listitem'; + + const sup = getSuperClassRole(role); + const isListItemRole = + role === 'listitem' || (sup && sup.includes('listitem')); + // const isListItemRole = role === 'listitem'; if (!isLi && !isListItemRole) { badNodes.push(actualNode); diff --git a/lib/commons/aria/get-element-unallowed-roles.js b/lib/commons/aria/get-element-unallowed-roles.js index 92544d47dd..037a8b0092 100644 --- a/lib/commons/aria/get-element-unallowed-roles.js +++ b/lib/commons/aria/get-element-unallowed-roles.js @@ -8,10 +8,10 @@ import { tokenList, isHtmlElement, nodeLookup } from '../../core/utils'; // HTML elements (img, link, etc.) const dpubRoles = [ 'doc-backlink', - 'doc-biblioentry', + // 'doc-biblioentry', 'doc-biblioref', 'doc-cover', - 'doc-endnote', + // 'doc-endnote', 'doc-glossref', 'doc-noteref' ]; diff --git a/lib/commons/aria/get-super-class-role.js b/lib/commons/aria/get-super-class-role.js new file mode 100644 index 0000000000..52544dd100 --- /dev/null +++ b/lib/commons/aria/get-super-class-role.js @@ -0,0 +1,21 @@ +import standards from '../../standards'; + +/** + * Get the "superclassRole" of role + * @method getSuperClassRole + * @memberof axe.commons.aria + * @instance + * @param {String} role The role to check + * @return {Mixed} String if a matching role and its superclassRole are found, otherwise `null` + */ +function getSuperClassRole(role) { + const roleDef = standards.ariaRoles[role]; + + if (!roleDef) { + return null; + } + + return roleDef.superclassRole; +} + +export default getSuperClassRole; diff --git a/lib/commons/aria/index.js b/lib/commons/aria/index.js index 9eb5bb3a88..c41443ebfd 100644 --- a/lib/commons/aria/index.js +++ b/lib/commons/aria/index.js @@ -12,6 +12,7 @@ export { default as getExplicitRole } from './get-explicit-role'; export { default as getImplicitRole } from './implicit-role'; export { default as getOwnedVirtual } from './get-owned-virtual'; export { default as getRoleType } from './get-role-type'; +export { default as getSuperClassRole } from './get-super-class-role'; export { default as getRole } from './get-role'; export { default as getRolesByType } from './get-roles-by-type'; export { default as getRolesWithNameFromContents } from './get-roles-with-name-from-contents'; diff --git a/lib/commons/aria/lookup-table.js b/lib/commons/aria/lookup-table.js index 84700f8f1c..11d3aa10d6 100644 --- a/lib/commons/aria/lookup-table.js +++ b/lib/commons/aria/lookup-table.js @@ -489,7 +489,7 @@ lookupTable.role = { owned: null, nameFrom: ['author'], context: null, - implicit: ['dd', 'dfn'], + implicit: ['dd'], // DAISY-AXE: remove 'dfn' which has implicit 'term' role, see https://www.w3.org/TR/html-aria/#docconformance unsupported: false }, dialog: { @@ -590,7 +590,7 @@ lookupTable.role = { ] }, 'doc-biblioentry': { - type: 'listitem', + type: 'structure', attributes: { allowed: [ 'aria-expanded', @@ -611,9 +611,11 @@ lookupTable.role = { attributes: { allowed: ['aria-expanded', 'aria-errormessage'] }, - owned: { - one: ['doc-biblioentry'] - }, + owned: null, + // owned: { + // // one: ['doc-biblioentry'] + // one: ['list'] + // }, nameFrom: ['author'], context: null, unsupported: false, @@ -714,7 +716,7 @@ lookupTable.role = { allowedElements: ['section'] }, 'doc-endnote': { - type: 'listitem', + type: 'structure', attributes: { allowed: [ 'aria-expanded', @@ -735,9 +737,11 @@ lookupTable.role = { attributes: { allowed: ['aria-expanded', 'aria-errormessage'] }, - owned: { - one: ['doc-endnote'] - }, + owned: null, + // owned: { + // // one: ['doc-endnote'] + // one: ['list'] + // }, namefrom: ['author'], context: null, unsupported: false, @@ -775,8 +779,11 @@ lookupTable.role = { unsupported: false, allowedElements: ['section'] }, + // https://www.w3.org/TR/dpub-aria-1.0/#doc-example + // ==> (was 'section' now 'figure') + // https://www.w3.org/TR/dpub-aria-1.1/#doc-example 'doc-example': { - type: 'section', + type: 'structure', attributes: { allowed: ['aria-expanded', 'aria-errormessage'] }, @@ -813,7 +820,8 @@ lookupTable.role = { attributes: { allowed: ['aria-expanded', 'aria-errormessage'] }, - owned: ['term', 'definition'], + owned: null, + // owned: ['term', 'definition'], namefrom: ['author'], context: null, unsupported: false, @@ -895,6 +903,7 @@ lookupTable.role = { }, owned: null, namefrom: ['author'], + nameFromContent: true, context: null, unsupported: false, allowedElements: ['hr'] @@ -943,8 +952,11 @@ lookupTable.role = { unsupported: false, allowedElements: ['section'] }, + // https://www.w3.org/TR/dpub-aria-1.0/#doc-pullquote + // ==> (was 'none' now 'section') + // https://www.w3.org/TR/dpub-aria-1.1/#doc-pullquote 'doc-pullquote': { - type: 'none', + type: 'section', attributes: { allowed: ['aria-expanded'] }, @@ -972,6 +984,7 @@ lookupTable.role = { }, owned: null, namefrom: ['author'], + nameFromContent: true, context: null, unsupported: false, allowedElements: { @@ -1163,7 +1176,7 @@ lookupTable.role = { }, nameFrom: ['author'], context: null, - implicit: ['ol', 'ul', 'dl'], + implicit: ['ol', 'ul'], // DAISY-AXE: remove 'dl' which has no implicit role, see https://www.w3.org/TR/html-aria/#docconformance unsupported: false }, listbox: { @@ -1202,7 +1215,7 @@ lookupTable.role = { owned: null, nameFrom: ['author', 'contents'], context: ['list'], - implicit: ['li', 'dt'], + implicit: ['li'], // DAISY-AXE: remove 'dt' which has implicit 'term' role, see https://www.w3.org/TR/html-aria/#docconformance unsupported: false }, log: { @@ -1944,7 +1957,7 @@ lookupTable.role = { owned: null, nameFrom: ['author', 'contents'], context: null, - implicit: ['dt'], + implicit: ['dt', 'dfn'], // DAISY-AXE: add 'dfn' which has implicit 'term' role, see https://www.w3.org/TR/html-aria/#docconformance unsupported: false }, textbox: { diff --git a/lib/core/base/audit.js b/lib/core/base/audit.js index 36bcb325ff..a687c21f7b 100644 --- a/lib/core/base/audit.js +++ b/lib/core/base/audit.js @@ -766,11 +766,12 @@ function getDefferedRule(rule, context, options) { * For all the rules, create the helpUrl and add it to the data for that rule */ function getHelpUrl({ brand, application, lang }, ruleId, version) { + var _v = version ? version : axe.version.replace(/-\w+\.\w+$/, ''); return ( constants.helpUrlBase + brand + '/' + - (version || axe.version.substring(0, axe.version.lastIndexOf('.'))) + + (version || _v.substring(0, _v.lastIndexOf('.'))) + '/' + ruleId + '?application=' + diff --git a/lib/core/public/configure.js b/lib/core/public/configure.js index afe73582b1..899d2b8d27 100644 --- a/lib/core/public/configure.js +++ b/lib/core/public/configure.js @@ -3,6 +3,7 @@ import { configureStandards } from '../../standards'; import constants from '../constants'; function configure(spec) { + // throw new Error("DAISY-AXE BREAKPOINT AXE CONFIGURE"); const audit = axe._audit; if (!audit) { @@ -52,7 +53,7 @@ function configure(spec) { spec.checks.forEach(check => { if (!check.id) { throw new TypeError( - // eslint-disable-next-line max-len + `Configured check ${JSON.stringify( check )} is invalid. Checks must be an object with at least an id property` @@ -72,7 +73,7 @@ function configure(spec) { spec.rules.forEach(rule => { if (!rule.id) { throw new TypeError( - // eslint-disable-next-line max-len + `Configured rule ${JSON.stringify( rule )} is invalid. Rules must be an object with at least an id property` diff --git a/lib/rules/epub-type-has-matching-role-matches.js b/lib/rules/epub-type-has-matching-role-matches.js new file mode 100644 index 0000000000..21d542169f --- /dev/null +++ b/lib/rules/epub-type-has-matching-role-matches.js @@ -0,0 +1,19 @@ +function epubTypeHasMatchingRoleMatches(node) { + // selector: '[*|type]', + return ( + node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type') || + node.hasAttribute('epub:type') // for unit tests that are not XML-aware due to fixture.innerHTML + ); + + // console.log('node.nodeName: ', node.nodeName); + // const attrs = Array.from(getNodeAttributes(node)); + // console.log(attrs.length); + // attrs.forEach((attr) => { + // console.log('\n====='); + // console.log(JSON.stringify(attr)); + // console.log('attr.nodeName: ', attr.nodeName); + // console.log('attr.namespaceURI: ', attr.namespaceURI); + // }); +} + +export default epubTypeHasMatchingRoleMatches; diff --git a/lib/rules/epub-type-has-matching-role.json b/lib/rules/epub-type-has-matching-role.json new file mode 100644 index 0000000000..f0f260a906 --- /dev/null +++ b/lib/rules/epub-type-has-matching-role.json @@ -0,0 +1,13 @@ +{ + "id": "epub-type-has-matching-role", + "impact": "moderate", + "matches": "epub-type-has-matching-role-matches", + "tags": ["cat.aria", "best-practice"], + "metadata": { + "description": "Ensure the element has an ARIA role matching its epub:type", + "help": "ARIA role should be used in addition to epub:type" + }, + "all": [], + "any": ["matching-aria-role"], + "none": [] +} diff --git a/lib/rules/landmark-one-main.json b/lib/rules/landmark-one-main.json index feecb55332..771b5436a1 100644 --- a/lib/rules/landmark-one-main.json +++ b/lib/rules/landmark-one-main.json @@ -7,7 +7,7 @@ "description": "Ensure the document has a main landmark", "help": "Document should have one main landmark" }, - "all": ["page-has-main"], + "all": ["page-no-duplicate-main"], "any": [], "none": [] } diff --git a/lib/rules/landmark-unique-matches.js b/lib/rules/landmark-unique-matches.js index d651949764..ec213912a9 100644 --- a/lib/rules/landmark-unique-matches.js +++ b/lib/rules/landmark-unique-matches.js @@ -1,7 +1,8 @@ import { isVisibleToScreenReaders } from '../commons/dom'; -import { getRole } from '../commons/aria'; +import { getRole, getRoleType } from '../commons/aria'; import { getAriaRolesByType } from '../commons/standards'; import { accessibleTextVirtual } from '../commons/text'; +// import { closest } from '../core/utils'; export default function landmarkUniqueMatches(node, virtualNode) { return ( @@ -11,7 +12,7 @@ export default function landmarkUniqueMatches(node, virtualNode) { function isLandmarkVirtual(vNode) { const landmarkRoles = getAriaRolesByType('landmark'); - const role = getRole(vNode); + const role = getRole(vNode, { dpub: true }); if (!role) { return false; } @@ -23,5 +24,11 @@ function isLandmarkVirtual(vNode) { return !!accessibleText; } - return landmarkRoles.indexOf(role) >= 0 || role === 'region'; + var roleType = getRoleType(role); + return ( + role === 'region' || + roleType === 'landmark' || + landmarkRoles.includes(roleType) || + landmarkRoles.indexOf(role) >= 0 + ); } diff --git a/lib/rules/pagebreak-label-matches.js b/lib/rules/pagebreak-label-matches.js new file mode 100644 index 0000000000..85775c4049 --- /dev/null +++ b/lib/rules/pagebreak-label-matches.js @@ -0,0 +1,16 @@ +function pagebreakLabelMatches(node) { + // selector: '[*|type~="pagebreak"], [role~="doc-pagebreak"]', + return ( + (node.hasAttribute('role') && + node.getAttribute('role').match(/\S+/g).includes('doc-pagebreak')) || + (node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type') && + node + .getAttributeNS('http://www.idpf.org/2007/ops', 'type') + .match(/\S+/g) + .includes('pagebreak')) + ); + + return false; +} + +export default pagebreakLabelMatches; diff --git a/lib/rules/pagebreak-label.json b/lib/rules/pagebreak-label.json new file mode 100644 index 0000000000..33fdb11444 --- /dev/null +++ b/lib/rules/pagebreak-label.json @@ -0,0 +1,13 @@ +{ + "id": "pagebreak-label", + "impact": "moderate", + "matches": "pagebreak-label-matches", + "tags": ["cat.epub"], + "metadata": { + "description": "Ensure page markers have an accessible label", + "help": "Page markers must have an accessible label" + }, + "all": [], + "any": ["aria-label", "non-empty-title"], + "none": [] +} diff --git a/lib/standards/dpub-roles.js b/lib/standards/dpub-roles.js index c2f8d33b95..bd1b88ecbe 100644 --- a/lib/standards/dpub-roles.js +++ b/lib/standards/dpub-roles.js @@ -27,7 +27,7 @@ const dpubRoles = { superclassRole: ['link'] }, 'doc-biblioentry': { - type: 'listitem', + type: 'structure', allowedAttrs: [ 'aria-expanded', 'aria-level', @@ -36,11 +36,15 @@ const dpubRoles = { ], superclassRole: ['listitem'], deprecated: true + // requiredContext: ['doc-bibliography'] + // requiredContext: ['list', 'listitem'] }, 'doc-bibliography': { type: 'landmark', allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] + // requiredOwned: ['doc-biblioentry'] + // requiredOwned: ['list'] }, 'doc-biblioref': { type: 'link', @@ -84,20 +88,24 @@ const dpubRoles = { superclassRole: ['section'] }, 'doc-endnote': { - type: 'listitem', + type: 'structure', allowedAttrs: [ 'aria-expanded', 'aria-level', 'aria-posinset', 'aria-setsize' ], - superclassRole: ['listitem'], + superclassRole: ['none'], deprecated: true + // requiredContext: ['doc-endnotes'] + // requiredContext: ['list', 'listitem'] }, 'doc-endnotes': { type: 'landmark', allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] + // requiredOwned: ['doc-endnote'] + // requiredOwned: ['list'] }, 'doc-epigraph': { type: 'section', @@ -114,10 +122,13 @@ const dpubRoles = { allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, + // https://www.w3.org/TR/dpub-aria-1.0/#doc-example + // ==> (was 'section' now 'figure') + // https://www.w3.org/TR/dpub-aria-1.1/#doc-example 'doc-example': { - type: 'section', + type: 'structure', allowedAttrs: ['aria-expanded'], - superclassRole: ['section'] + superclassRole: ['figure'] }, 'doc-footnote': { type: 'section', @@ -187,8 +198,11 @@ const dpubRoles = { allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, + // https://www.w3.org/TR/dpub-aria-1.0/#doc-pullquote + // ==> (was 'none' now 'section') + // https://www.w3.org/TR/dpub-aria-1.1/#doc-pullquote 'doc-pullquote': { - type: 'none', + type: 'section', superclassRole: ['none'] }, 'doc-qna': { diff --git a/locales/_template.json b/locales/_template.json index b6315ed671..48318f50cd 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -173,6 +173,10 @@ "description": "Ensure table headers have discernible text", "help": "Table header text should not be empty" }, + "epub-type-has-matching-role": { + "description": "Ensure the element has an ARIA role matching its epub:type", + "help": "ARIA role should be used in addition to epub:type" + }, "focus-order-semantics": { "description": "Ensure elements in the focus order have a role appropriate for interactive content", "help": "Elements in the focus order should have an appropriate role" @@ -341,6 +345,10 @@ "description": "Ensure that the page, or at least one of its frames contains a level-one heading", "help": "Page should contain a level-one heading" }, + "pagebreak-label": { + "description": "Ensure page markers have an accessible label", + "help": "Page markers must have an accessible label" + }, "presentation-role-conflict": { "description": "Elements marked as presentational should not have global ARIA or tabindex to ensure all screen readers ignore them", "help": "Ensure elements marked as presentational are consistently ignored" @@ -596,6 +604,10 @@ "pass": "Element is focusable.", "fail": "Element is not focusable." }, + "matching-aria-role": { + "pass": "Element has an ARIA role matching its epub:type", + "fail": "Element has no ARIA role matching its epub:type" + }, "no-implicit-explicit-label": { "pass": "There is no mismatch between a