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

fix(engine): Update the engine to fix ARIA definition and other issues #1936

Merged
merged 14 commits into from
Jun 27, 2024
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
91 changes: 48 additions & 43 deletions accessibility-checker-engine/src/v2/aria/ARIADefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2385,7 +2385,7 @@ export class ARIADefinitions {
"checkbox-with-aria-pressed": {
implicitRole: ["checkbox"],
//roleCondition: " with type=checkbox and aria-pressed attribute is present",
validRoles: ["button"],
validRoles: ["menuitemcheckbox", "option", "switch", "button"],
globalAriaAttributesValid: true,
otherAllowedAriaAttributes: ["aria-required"],
otherDisallowedAriaAttributes: ["aria-checked"]
Expand Down Expand Up @@ -2715,132 +2715,137 @@ export class ARIADefinitions {
ariaAttributeValue: string | null,
htmlAttributeNames: string[],
htmlAttributeValues: string[] | null
},
}[],
overlapping?: {
ariaAttributeValue: string | null,
htmlAttributeNames: string[],
htmlAttributeValues: string[] | null
}
}[]
}
} = {
"aria-checked": {
conflict: {
conflict: [{
ariaAttributeValue: "false",
htmlAttributeNames: ["checked"],
htmlAttributeValues: null
},
overlapping: {
}],
overlapping: [{
ariaAttributeValue: "true",
htmlAttributeNames: ["checked"],
htmlAttributeValues: null
}
}]
},
"aria-disabled": {
conflict: {
conflict: [{
ariaAttributeValue: "false",
htmlAttributeNames: ["disabled"],
htmlAttributeValues: null
},
overlapping: {
}],
overlapping: [{
ariaAttributeValue: "true",
htmlAttributeNames: ["disabled"],
htmlAttributeValues: null
}
}]
},
"aria-hidden": {
conflict: {
conflict: [{
ariaAttributeValue: "false",
htmlAttributeNames: ["hidden"],
htmlAttributeValues: null
htmlAttributeValues: ["hidden,null"]
},
overlapping: {
{
ariaAttributeValue: "true",
htmlAttributeNames: ["hidden"],
htmlAttributeValues: null
}
htmlAttributeValues: ["until-found"]
}],
overlapping: [{
ariaAttributeValue: "true",
htmlAttributeNames: ["hidden"],
htmlAttributeValues: ["hidden,null"]
}]
},
"aria-placeholder": {
conflict: {
conflict: [{
ariaAttributeValue: null,
htmlAttributeNames: ["placeholder"],
htmlAttributeValues: null
}
}]
},
"aria-valuemax": {
conflict: {
conflict: [{
ariaAttributeValue: null,
htmlAttributeNames: ["max"],
htmlAttributeValues: null
}
}]
//overlap case covered in the role definition: Authors SHOULD NOT use aria-valuemax on any element which allows the max attribute. Use the max attribute instead.
},
"aria-valuemin": {
conflict: {
conflict: [{
ariaAttributeValue: null,
htmlAttributeNames: ["min"],
htmlAttributeValues: null
}
}]
////overlap case covered in the role definition:Authors SHOULD NOT use aria-valuemin on any element which allows the min attribute. Use the min attribute instead.
},
"aria-readonly": {
conflict: {
conflict: [{
ariaAttributeValue: "false",
htmlAttributeNames: ["readonly", "contenteditable", "iscontenteditable"],
htmlAttributeValues: [null, "false", "false"]
},
overlapping: {
}],
overlapping: [{
ariaAttributeValue: "true",
htmlAttributeNames: ["readonly", "contenteditable", "iscontenteditable"],
htmlAttributeValues: [null, "true", "true"]
}
}]
},
"aria-required": {
conflict: {
conflict: [{
ariaAttributeValue: "false",
htmlAttributeNames: ["required"],
htmlAttributeValues: null
},
overlapping: {
}],
overlapping: [{
ariaAttributeValue: "true",
htmlAttributeNames: ["required"],
htmlAttributeValues: null
}
}]
},
"aria-colspan": {
conflict: {
conflict: [{
// conflict occurs if both values are different
ariaAttributeValue: "VALUE",
htmlAttributeNames: ["colspan"],
htmlAttributeValues: ["VALUE"]
},
overlapping: {
}],
overlapping: [{
// overlap occurs if both exists
ariaAttributeValue: null,
htmlAttributeNames: ["colspan"],
htmlAttributeValues: null
}
}]
},
"aria-rowspan": {
conflict: {
conflict: [{
// conflict occurs if both values are different
ariaAttributeValue: "VALUE",
htmlAttributeNames: ["rowspan"],
htmlAttributeValues: ["VALUE"]
},
overlapping: {
}],
overlapping: [{
// overlap occurs if both exists
ariaAttributeValue: null,
htmlAttributeNames: ["rowspan"],
htmlAttributeValues: null
}
}]
},
"aria-autocomplete": {
conflict: {
// conflict occurs if both values are conflict
ariaAttributeValue: "none",
conflict: [{
// conflict occurs if both exists, aria value is only for custom widget, rather than native
ariaAttributeValue: null,
htmlAttributeNames: ["autocomplete"],
htmlAttributeValues: ["on"]
}
htmlAttributeValues: null
}]
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3212,41 +3212,61 @@ export class RPTUtil {
* or null where ariaAttr won't cause conflict
*/
public static getConflictOrOverlappingHtmlAttribute(ariaAttr, htmlAttrs, type): any[] | null {
let exist = ARIADefinitions.relatedAriaHtmlAttributes[ariaAttr['name']];
let exist = ARIADefinitions.relatedAriaHtmlAttributes[ariaAttr['name']];
if (exist) {
if (!ariaAttr || ariaAttr.length ==0 || !htmlAttrs || htmlAttrs.length == 0) return [];

let examinedHtmlAtrNames = [];
let ariaAttrValue = '';
let concernTypes = null;
if (type === 'conflict') {
if (!exist.conflict) return null;
ariaAttrValue = exist.conflict.ariaAttributeValue;
if (!exist.conflict || Object.keys(exist.conflict).length === 0) return null;
concernTypes = exist.conflict;
} else if (type === 'overlapping') {
if (!exist.overlapping) return null;
ariaAttrValue = exist.overlapping.ariaAttributeValue;
if (!exist.overlapping || Object.keys(exist.overlapping).length === 0) return null;
concernTypes = exist.overlapping;
} else
return null;
if (ariaAttrValue === null || ariaAttrValue === 'VALUE' || ariaAttrValue === ariaAttr['value']) {
let htmlAttrNames = [];
let htmlAttrValues = [];
if (type === 'conflict') {
htmlAttrNames = exist.conflict.htmlAttributeNames;
htmlAttrValues = exist.conflict.htmlAttributeValues;
} else {
htmlAttrNames = exist.overlapping.htmlAttributeNames;
htmlAttrValues = exist.overlapping.htmlAttributeValues;
}
return null;

let applicable = false;
let fail = false;
for (let k=0; k < concernTypes.length; k++) {
let concernAriaValue = concernTypes[k].ariaAttributeValue;
let concernHtmlNames = concernTypes[k].htmlAttributeNames;
let concernHtmlValues = concernTypes[k].htmlAttributeValues;

for (let i = 0; i < htmlAttrs.length; i++) {
let index = htmlAttrNames.indexOf(htmlAttrs[i]['name']);
let index = concernHtmlNames.indexOf(htmlAttrs[i]['name']);
if (index !== -1) {
if (htmlAttrValues === null
|| (ariaAttrValue === 'VALUE' && htmlAttrValues[index] === 'VALUE' && htmlAttrs[i]['value'] !== ariaAttr['value'])
|| htmlAttrs[i]['value'] === htmlAttrValues[index]) {
examinedHtmlAtrNames.push({result: 'Failed', 'attr': htmlAttrs[i]['name']});
continue;
} else
examinedHtmlAtrNames.push({result: 'Pass', 'attr': htmlAttrs[i]['name']});
}
}
applicable = true;
let htmlValuesInConcern = (concernHtmlValues===null || concernHtmlValues[index]===null) ? null : concernHtmlValues[index].split(",");

if (concernAriaValue === null) {
if (htmlValuesInConcern === null) {
examinedHtmlAtrNames.push({result: 'Failed', 'attr': htmlAttrs[i]['name']});
fail = true;
} else if (htmlValuesInConcern.includes(htmlAttrs[i]['value'])) {
examinedHtmlAtrNames.push({result: 'Failed', 'attr': htmlAttrs[i]['name']});
fail = true;
}
} else if (htmlValuesInConcern === null) {
if (concernAriaValue === ariaAttr['value']) {
examinedHtmlAtrNames.push({result: 'Failed', 'attr': htmlAttrs[i]['name']});
fail = true;
}
} else if (concernAriaValue === 'VALUE' && htmlValuesInConcern.includes('VALUE') && htmlValuesInConcern[0] !== ariaAttr['value']) {
examinedHtmlAtrNames.push({result: 'Failed', 'attr': htmlAttrs[i]['name']});
fail = true;
} else if (concernAriaValue === ariaAttr['value'] && htmlValuesInConcern.includes(htmlAttrs[i]['value'])) {
examinedHtmlAtrNames.push({result: 'Failed', 'attr': htmlAttrs[i]['name']});
fail = true;
}
}
}
}

if (applicable && !fail)
examinedHtmlAtrNames.push({result: 'Pass', 'attr': ''});

return examinedHtmlAtrNames;
} else
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export let aria_attribute_conflict: Rule = {
},
messages: {
"en-US": {
"pass": "Rule Passed",
"pass": "The ARIA attribute is not conflict with the corresponding HTML attribute",
"fail_conflict": "The ARIA attribute \"{0}\" is in conflict with the corresponding HTML attribute \"{1}\"",
"group": "An ARIA attribute must not conflict with the corresponding HTML attribute"
}
Expand Down Expand Up @@ -62,8 +62,8 @@ export let aria_attribute_conflict: Rule = {
RPTUtil.reduceArrayItemList([conflictAttributes[i]['ariaAttr']], ariaAttributes);
}

for (let i = 0; i < ariaAttributes.length; i++)
ret.push(RulePass("pass"));
//for (let i = 0; i < ariaAttributes.length; i++)
// ret.push(RulePass("pass"));

if (ret.length > 0)
return ret;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
limitations under the License.
*****************************************************************************/

import { Rule, RuleResult, RuleFail, RuleContext, RulePass, RuleContextHierarchy, RulePotential } from "../api/IRule";
import { Rule, RuleResult, RuleFail, RuleContext, RulePass, RuleContextHierarchy } from "../api/IRule";
import { eRulePolicy, eToolkitLevel } from "../api/IRule";
import { RPTUtil } from "../../v2/checker/accessibility/util/legacy";
import { getCache } from "../util/CacheUtil";
import { getInvalidAriaAttributes, getConflictAriaAndHtmlAttributes } from "../util/CommonUtil";

export let aria_attribute_redundant: Rule = {
Expand All @@ -31,9 +30,9 @@ export let aria_attribute_redundant: Rule = {
},
messages: {
"en-US": {
"pass": "Rule Passed",
"pass": "The ARIA attribute is not redundant with a corresponding HTML attribute",
"fail_redundant": "The ARIA attribute \"{0}\" is redundant with the HTML attribute \"{1}\"",
"group": "An ARIA attribute should not be used when there is a corresponding HTML attribute"
"group": "An ARIA attribute should not be redundant with a corresponding HTML attribute"
}
},
rulesets: [{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
*****************************************************************************/

import { ARIADefinitions } from "../../v2/aria/ARIADefinitions";
import { ARIAMapper } from "../../v2/aria/ARIAMapper";
import { RPTUtil } from "../../v2/checker/accessibility/util/legacy";
import { Rule, RuleResult, RuleFail, RuleContext, RulePotential, RuleManual, RulePass, RuleContextHierarchy } from "../api/IRule";
import { Rule, RuleResult, RuleFail, RuleContext, RulePass, RuleContextHierarchy } from "../api/IRule";
import { eRulePolicy, eToolkitLevel } from "../api/IRule";

export let aria_attribute_required: Rule = {
Expand All @@ -23,22 +22,22 @@ export let aria_attribute_required: Rule = {
dependencies: ["aria_role_allowed"],
refactor: {
"Rpt_Aria_RequiredProperties": {
"Pass_0": "Pass_0",
"Fail_1": "Fail_1"
"Pass_0": "pass",
"Fail_1": "fail_missing"
}
},
help: {
"en-US": {
"group": `aria_attribute_required.html`,
"Pass_0": `aria_attribute_required.html`,
"Fail_1": `aria_attribute_required.html`
"pass": `aria_attribute_required.html`,
"fail_missing": `aria_attribute_required.html`
}
},
messages: {
"en-US": {
"group": "When using an ARIA role on an element, the required attributes for that role must be defined",
"Pass_0": "Rule Passed",
"Fail_1": "An element with ARIA role '{0}' does not have the required ARIA attribute(s): '{1}'"
"group": "The required attributes for the element with a role must be defined",
"pass": "The required attributes for the element with the role are defined",
"fail_missing": "An element with ARIA role '{0}' does not have the required ARIA attribute(s): '{1}'"
}
},
rulesets: [{
Expand All @@ -59,13 +58,16 @@ export let aria_attribute_required: Rule = {
let hasAttribute = RPTUtil.hasAttribute;
let testedRoles = 0;

let tagProperty = RPTUtil.getElementAriaProperty(ruleContext);
for (let j = 0, rolesLength = roles.length; j < rolesLength; ++j) {
if (implicitRole.length > 0 && implicitRole.includes(roles[j])) continue;
if (designPatterns[roles[j]] && RPTUtil.getRoleRequiredProperties(roles[j], ruleContext) != null) {
let requiredRoleProps = RPTUtil.getRoleRequiredProperties(roles[j], ruleContext);
let allowedRoleProps = RPTUtil.getAllowedAriaAttributes(ruleContext, roles[j], tagProperty);
let roleMissingReqProp = false;
testedRoles++;
for (let i = 0, propertiesLength = requiredRoleProps.length; i < propertiesLength; i++) {
if (!allowedRoleProps.includes(requiredRoleProps[i])) continue;
if (!hasAttribute(ruleContext, requiredRoleProps[i])) {
// If an aria-labelledby isn't present, an aria-label will meet the requirement.
if (requiredRoleProps[i] == "aria-labelledby") {
Expand Down Expand Up @@ -99,9 +101,9 @@ export let aria_attribute_required: Rule = {
if (testedRoles === 0) {
return null;
} else if (!passed) {
return RuleFail("Fail_1", retToken);
return RuleFail("fail_missing", retToken);
} else {
return RulePass("Pass_0");
return RulePass("pass");
}
}
}
1 change: 1 addition & 0 deletions accessibility-checker-engine/src/v4/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export * from "./style_focus_visible"
export * from "./style_highcontrast_visible"
export * from "./style_hover_persistent"
export * from "./style_viewport_resizable"
export * from "./svg_graphics_labelled"
export * from "./table_aria_descendants"
export * from "./table_caption_empty"
export * from "./table_caption_nested"
Expand Down
Loading
Loading