diff --git a/.gitignore b/.gitignore index deec545a3..c2abf9ca0 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ jspm_packages .gz .tar accessibility-checker-engine/karma.conf.js +karma-accessibility-checker/package.json diff --git a/accessibility-checker-engine/src/v2/aria/ARIADefinitions.ts b/accessibility-checker-engine/src/v2/aria/ARIADefinitions.ts index c8b040852..6469ef86e 100644 --- a/accessibility-checker-engine/src/v2/aria/ARIADefinitions.ts +++ b/accessibility-checker-engine/src/v2/aria/ARIADefinitions.ts @@ -1710,6 +1710,7 @@ export class ARIADefinitions { "body": { implicitRole: ["generic"], validRoles: null, + otherDisallowedAriaAttributes: ['aria-hidden'], globalAriaAttributesValid: true }, "br": { @@ -1720,7 +1721,7 @@ export class ARIADefinitions { }, "button": { implicitRole: ["button"], - validRoles: ["checkbox", "combobox", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "switch", "tab"], + validRoles: ["checkbox", "combobox", "gridcell", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "slider", "switch", "tab", "treeitem"], globalAriaAttributesValid: true }, "canvas": { @@ -1906,11 +1907,6 @@ export class ARIADefinitions { validRoles: null, globalAriaAttributesValid: true }, - "li": { - implicitRole: ["listitem"], - validRoles: ["menuitem", "menuitemcheckbox", "menuitemradio", "none", "option", "presentation", "radio", "separator", "tab", "treeitem"], - globalAriaAttributesValid: true - }, "link": { implicitRole: null, validRoles: null, @@ -2051,6 +2047,11 @@ export class ARIADefinitions { validRoles: null, globalAriaAttributesValid: false }, + "search": { + implicitRole: ['search'], + validRoles: ['search', 'form', 'group', 'none', 'presentation', 'region'], + globalAriaAttributesValid: true + }, "slot": { implicitRole: null, validRoles: null, @@ -2086,11 +2087,6 @@ export class ARIADefinitions { validRoles: ["any"], globalAriaAttributesValid: true }, - "summary": { - implicitRole: ["button"], - validRoles: null, - globalAriaAttributesValid: true - }, "sup": { implicitRole: ["superscript"], validRoles: ["any"], @@ -2294,7 +2290,7 @@ export class ARIADefinitions { "input": { "button": { implicitRole: ["button"], - validRoles: ["checkbox", "combobox", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "switch", "tab"], + validRoles: ["checkbox", "combobox", "gridcell", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "slider", "switch", "tab", "treeitem"], globalAriaAttributesValid: true }, "checkbox-with-aria-pressed": { @@ -2358,7 +2354,7 @@ export class ARIADefinitions { }, "image": { implicitRole: ["button"], - validRoles: ["link", "menuitem", "menuitemcheckbox", "menuitemradio", "radio", "switch"], + validRoles: ["checkbox", "gridcell", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "slider", "switch", "tab", "treeitem"], globalAriaAttributesValid: true }, "month": { @@ -2396,7 +2392,7 @@ export class ARIADefinitions { }, "reset": { implicitRole: ["button"], - validRoles: null, + validRoles: ["checkbox", "combobox", "gridcell", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "slider", "switch", "tab", "treeitem"], globalAriaAttributesValid: true }, "search-no-list": { @@ -2412,7 +2408,7 @@ export class ARIADefinitions { }, "submit": { implicitRole: ["button"], - validRoles: null, + validRoles: ["checkbox", "combobox", "gridcell", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "slider", "switch", "tab", "treeitem"], globalAriaAttributesValid: true }, "tel-no-list": { @@ -2476,6 +2472,18 @@ export class ARIADefinitions { globalAriaAttributesValid: true } }, + "li": { + "child-of-list-role": { + implicitRole: ['listitem'], + validRoles: null, + globalAriaAttributesValid: true + }, + "no-child-of-list-role": { + implicitRole: ['listitem'], + validRoles: ["any"], + globalAriaAttributesValid: true + } + }, "section": { "with-name": { implicitRole: ["region"], @@ -2504,6 +2512,19 @@ export class ARIADefinitions { otherDisallowedAriaAttributes: ["aria-multiselectable"] } }, + "summary": { + "first-summary-of-detail": { + implicitRole: null, + validRoles: null, + globalAriaAttributesValid: true, + otherAllowedAriaAttributes: ["aria-disabled", "aria-haspopup"] + }, + "no-first-summary-of-detail": { + implicitRole: null, + validRoles: ["any"], + globalAriaAttributesValid: true + } + }, "tbody": { "des-table": { implicitRole: ["rowgroup"], diff --git a/accessibility-checker-engine/src/v2/checker/accessibility/util/legacy.ts b/accessibility-checker-engine/src/v2/checker/accessibility/util/legacy.ts index 52cb64af6..9cf47a05e 100644 --- a/accessibility-checker-engine/src/v2/checker/accessibility/util/legacy.ts +++ b/accessibility-checker-engine/src/v2/checker/accessibility/util/legacy.ts @@ -21,6 +21,7 @@ import { DOMWalker } from "../../../dom/DOMWalker"; import { VisUtil } from "../../../dom/VisUtil"; import { FragmentUtil } from "./fragment"; import { getDefinedStyles } from "../../../../v4/util/CSSUtil"; +import { DOMUtil } from "../../../dom/DOMUtil"; export class RPTUtil { @@ -376,7 +377,15 @@ export class RPTUtil { "video": function (element) { return element.hasAttribute("controls"); }, - "summary": true + "summary": function (element) { + // first summary child of a details element is automatically focusable + return element.parentElement && element.parentElement.nodeName.toLowerCase() === 'details' + && DOMUtil.sameNode([...element.parentElement.children].filter(elem=>elem.nodeName.toLowerCase() === 'summary')[0], element); + }, + "details": function (element) { + //details element without a direct summary child is automatically focusable + return element.children && [...element.children].filter(elem=>elem.nodeName.toLowerCase() === 'summary').length === 0; + } } public static wordCount(str) : number { @@ -2543,6 +2552,13 @@ export class RPTUtil { RPTUtil.attributeNonEmpty(ruleContext, "list") ? tagProperty = specialTagProperties["text-with-list"] : tagProperty = specialTagProperties["text-no-list"]; } break; + case "li": + specialTagProperties = ARIADefinitions.documentConformanceRequirementSpecialTags["li"]; + if (ruleContext.parentElement && RPTUtil.hasRoleInSemantics(ruleContext.parentElement, "list")) + tagProperty = specialTagProperties["child-of-list-role"]; + else + tagProperty = specialTagProperties["no-child-of-list-role"]; + break; case "section": name = ARIAMapper.computeName(ruleContext); if (name && name.trim().length > 0) { @@ -2550,7 +2566,7 @@ export class RPTUtil { } else { tagProperty = specialTagProperties["without-name"]; } - break; + break; case "select": specialTagProperties = ARIADefinitions.documentConformanceRequirementSpecialTags["select"]; if (ruleContext.hasAttribute("multiple") || @@ -2559,6 +2575,14 @@ export class RPTUtil { else tagProperty = specialTagProperties["no-multiple-attr-size-gt1"]; break; + case "summary": + specialTagProperties = ARIADefinitions.documentConformanceRequirementSpecialTags["summary"]; + if (ruleContext.parentElement && ruleContext.parentElement.nodeName.toLowerCase() === 'details' + && DOMUtil.sameNode([...ruleContext.parentElement.children].filter(elem=>elem.nodeName.toLowerCase() === 'summary')[0], ruleContext)) + tagProperty = specialTagProperties["first-summary-of-detail"]; + else + tagProperty = specialTagProperties["no-first-summary-of-detail"]; + break; case "tbody": case "td": case "tr": diff --git a/accessibility-checker-engine/src/v2/dom/ColorUtil.ts b/accessibility-checker-engine/src/v2/dom/ColorUtil.ts index a0fbb5189..e4e8c7305 100644 --- a/accessibility-checker-engine/src/v2/dom/ColorUtil.ts +++ b/accessibility-checker-engine/src/v2/dom/ColorUtil.ts @@ -225,6 +225,7 @@ export class ColorUtil { var retVal = { "hasGradient": false, "hasBGImage": false, + "textShadow": false, "fg": null, "bg": null }; @@ -267,7 +268,7 @@ export class ColorUtil { overallWorst = worstColor; } } - return overallWorst; + return overallWorst; // return the darkest color } catch (e) { console.log(e); } @@ -285,7 +286,7 @@ export class ColorUtil { // cStyle is the computed style of this layer var cStyle = win.getComputedStyle(procNext); if (cStyle === null) continue; - + // thisBgColor is the color of this layer or null if the layer is transparent var thisBgColor = null; if (cStyle.backgroundColor && cStyle.backgroundColor != "transparent" && cStyle.backgroundColor != "rgba(0, 0, 0, 0)") { @@ -300,13 +301,19 @@ export class ColorUtil { if (!gradColors[i].length) { gradColors.splice(i--, 1); } else { - gradColorComp.push(ColorUtil.Color(gradColors[i])); + let colorComp = ColorUtil.Color(gradColors[i]); + if (colorComp.alpha !== undefined && colorComp.alpha < 1) { + // mix the grdient bg color wit parent bg if alpha < 1 + let compStackBg = thisStackBG || priorStackBG; + colorComp = colorComp.getOverlayColor(compStackBg); + } + gradColorComp.push(colorComp); } } thisBgColor = guessGradColor(gradColorComp, thisStackBG || priorStackBG, fg); } } - + // Handle non-solid opacity if (thisStackOpacity === null || (cStyle.opacity && cStyle.opacity.length > 0 && parseFloat(cStyle.opacity) < 1)) { // New stack, reset @@ -370,6 +377,10 @@ export class ColorUtil { } retVal.fg = fg; retVal.bg = priorStackBG; + + if (cStyle.textShadow && cStyle.textShadow !== 'none') + retVal.textShadow = true; + return retVal; } catch (err) { // something happened, then... @@ -413,10 +424,10 @@ export class ColorObj { contrastRatio(bgColor : ColorObj) { let fgColor: ColorObj = this; - + if (typeof (this.alpha) != "undefined") fgColor = this.getOverlayColor(bgColor); - + let lum1 = fgColor.relativeLuminance(); if (!bgColor.relativeLuminance) { let s = ""; diff --git a/accessibility-checker-engine/src/v4/rules/IBMA_Color_Contrast_WCAG2AA_PV.ts b/accessibility-checker-engine/src/v4/rules/IBMA_Color_Contrast_WCAG2AA_PV.ts deleted file mode 100644 index 919b0042a..000000000 --- a/accessibility-checker-engine/src/v4/rules/IBMA_Color_Contrast_WCAG2AA_PV.ts +++ /dev/null @@ -1,72 +0,0 @@ -/****************************************************************************** - Copyright:: 2022- IBM, Inc - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - *****************************************************************************/ - -import { RPTUtil } from "../../v2/checker/accessibility/util/legacy"; -import { VisUtil } from "../../v2/dom/VisUtil"; -import { Rule, RuleResult, RuleFail, RuleContext, RulePotential, RuleManual, RulePass, RuleContextHierarchy } from "../api/IRule"; -import { eRulePolicy, eToolkitLevel } from "../api/IRule"; -import { getCache } from "../util/CacheUtil"; - -export let IBMA_Color_Contrast_WCAG2AA_PV: Rule = { - id: "IBMA_Color_Contrast_WCAG2AA_PV", - // keeping old ruleID for archive purposes, functionality merged into new ruleID text_contrast_sufficient - context: "dom:*", - dependencies: ["text_contrast_sufficient"], - help: { - "en-US": { - "group": `IBMA_Color_Contrast_WCAG2AA_PV.html`, - "Pass_0": `IBMA_Color_Contrast_WCAG2AA_PV.html`, - "Potential_1": `IBMA_Color_Contrast_WCAG2AA_PV.html` - } - }, - messages: { - "en-US": { - "group": "The contrast ratio of text with its background (i.e. background with a color gradient or a background image) must meet WCAG 2.1 AA requirements", - "Pass_0": "Rule Passed", - "Potential_1": "Verify the contrast ratio of the text against the lightest and the darkest colors of the background meets the WCAG 2.1 AA minimum requirements for text of size {1}px and weight of {2}" - } - }, - rulesets: [{ - id: ["IBM_Accessibility", "WCAG_2_0", "WCAG_2_1"], - num: "1.4.3", // num: [ "2.4.4", "x.y.z" ] also allowed - level: eRulePolicy.VIOLATION, - toolkitLevel: eToolkitLevel.LEVEL_ONE - }], - act: ["afw4f7"], - run: (context: RuleContext, options?: {}, contextHierarchies?: RuleContextHierarchy): RuleResult | RuleResult[] => { - const ruleContext = context["dom"].node as Element; - let nodeName = ruleContext.nodeName.toLowerCase(); - // avoid diagnosing disabled nodes or those that are not visible. - if (RPTUtil.isNodeDisabled(ruleContext) || - !VisUtil.isNodeVisible(ruleContext) || - (VisUtil.hiddenByDefaultElements != null && - VisUtil.hiddenByDefaultElements != undefined && - VisUtil.hiddenByDefaultElements.indexOf(nodeName) > -1)) { - return null; - } - let precalc = getCache(ruleContext, "EXT_Color_Contrast_WCAG2AA", null); - if (!precalc) return RulePass("Pass_0"); - let passed = precalc.ratio >= 4.5 || (precalc.ratio >= 3 && precalc.isLargeScale); - - // If element or parent is disabled, this rule does not apply (but may be 3:1 in future) - if (!passed && precalc.isDisabled) { - passed = true; - } - - if (!passed) { - return RulePotential("Potential_1", [precalc.ratio.toFixed(2), precalc.size, precalc.weight]); - } else { - return RulePass("Pass_0", [precalc.ratio.toFixed(2), precalc.size, precalc.weight]); - } - } -} diff --git a/accessibility-checker-engine/src/v4/rules/index.ts b/accessibility-checker-engine/src/v4/rules/index.ts index 42aa4ffe2..e5fb3e799 100644 --- a/accessibility-checker-engine/src/v4/rules/index.ts +++ b/accessibility-checker-engine/src/v4/rules/index.ts @@ -107,7 +107,6 @@ export * from "./heading_content_exists" export * from "./heading_markup_misuse" export * from "./html_lang_exists" export * from "./html_skipnav_exists" -export * from "./IBMA_Color_Contrast_WCAG2AA_PV" export * from "./iframe_interactive_tabbable" export * from "./imagebutton_alt_exists" export * from "./imagemap_alt_exists" diff --git a/accessibility-checker-engine/src/v4/rules/text_contrast_sufficient.ts b/accessibility-checker-engine/src/v4/rules/text_contrast_sufficient.ts index dad3f4e6a..559612cc3 100644 --- a/accessibility-checker-engine/src/v4/rules/text_contrast_sufficient.ts +++ b/accessibility-checker-engine/src/v4/rules/text_contrast_sufficient.ts @@ -16,7 +16,7 @@ import { VisUtil } from "../../v2/dom/VisUtil"; import { ColorUtil } from "../../v2/dom/ColorUtil"; import { Rule, RuleResult, RuleFail, RuleContext, RulePotential, RulePass, RuleContextHierarchy } from "../api/IRule"; import { eRulePolicy, eToolkitLevel } from "../api/IRule"; -import { setCache } from "../util/CacheUtil"; +//import { setCache } from "../util/CacheUtil"; import { getWeightNumber, getFontInPixels } from "../util/CSSUtil"; export let text_contrast_sufficient: Rule = { @@ -26,7 +26,11 @@ export let text_contrast_sufficient: Rule = { "IBMA_Color_Contrast_WCAG2AA": { "Pass_0": "Pass_0", "Fail_1": "Fail_1", - "Potential_1": "Potential_1" + "Potential_1": "Potential_same_color" + }, + "IBMA_Color_Contrast_WCAG2AA_PV": { + "Pass_0": "Pass_0", + "Potential_1": "Potential_graphic_background" } }, help: { @@ -34,7 +38,9 @@ export let text_contrast_sufficient: Rule = { "group": `text_contrast_sufficient.html`, "Pass_0": `text_contrast_sufficient.html`, "Fail_1": `text_contrast_sufficient.html`, - "Potential_1": `text_contrast_sufficient.html` + "Potential_same_color": `text_contrast_sufficient.html`, + "Potential_graphic_background": `text_contrast_sufficient.html`, + "Potential_text_shadow": `text_contrast_sufficient.html` } }, messages: { @@ -42,7 +48,9 @@ export let text_contrast_sufficient: Rule = { "group": "The contrast ratio of text with its background must meet WCAG 2.1 AA requirements", "Pass_0": "Rule Passed", "Fail_1": "Text contrast of {0} with its background is less than the WCAG AA minimum requirements for text of size {1}px and weight of {2}", - "Potential_1": "The foreground text and its background color are both detected as {3}. Verify the text meets the WCAG 2.1 AA requirements for minimum contrast" + "Potential_same_color": "The foreground text and its background color are both detected as {3}. Verify the text meets the WCAG 2.1 AA requirements for minimum contrast", + "Potential_graphic_background": "Verify the contrast ratio of the text against the lightest and the darkest colors of the background meets the WCAG 2.1 AA minimum requirements for text of size {1}px and weight of {2}", + "Potential_text_shadow": "Verify the contrast ratio of the text with shadow meets the WCAG 2.1 AA minimum requirements for text of size {1}px and weight of {2}" } }, rulesets: [{ @@ -85,9 +93,23 @@ export let text_contrast_sufficient: Rule = { // Ensure that this element has children with actual text. let childStr = RPTUtil.getNodeText(ruleContext); - if (childStr.trim().length == 0 && (!RPTUtil.isShadowHostElement(ruleContext) || (RPTUtil.isShadowHostElement(ruleContext) && RPTUtil.getNodeText(ruleContext.shadowRoot) === ''))) - return null; - + if (!RPTUtil.isShadowHostElement(ruleContext) || (RPTUtil.isShadowHostElement(ruleContext) && RPTUtil.getNodeText(ruleContext.shadowRoot) === '')) { + if (childStr.trim().length == 0 ) + return null; + + // ignore if the text does not convey anything in human language + /** + * (1) ignore non-alphanumeric or special characters in ASCI: ^(a-zA-Z\d\s) + * (2) ignore non-printable unicode characters: \u0000-\u0008\u000B-\u001F\u007F-\u009F\u2000-\u200F\u2028-\u202F\u205F-\u206F\u3000\uFEFF + * see https://stackoverflow.com/questions/3770117/what-is-the-range-of-unicode-printable-characters + * (3) for now not consider unicode special characters that are different in different languages + */ + let regex = /[^(a-zA-Z\d\s)\u0000-\u0008\u000B-\u001F\u007F-\u009F\u2000-\u200F\u2028-\u202F\u205F-\u206F\u3000\uFEFF]+/g; + const removed = childStr.trim().replace(regex, ''); + if (removed.trim().length === 0) + return null; + } + let elem = ruleContext; // the child elements (rather than shadow root) of a shadow host is either re-assigned to the shadow slot if the slot exists // or not displayed, so shouldn't be checked from the light DOM, rather it should be checked as reassginged slot element(s) in the shadow DOM. @@ -104,7 +126,7 @@ export let text_contrast_sufficient: Rule = { } if (elem === null) return; } - + let style = win.getComputedStyle(elem); // JCH clip INFO: @@ -216,7 +238,7 @@ export let text_contrast_sufficient: Rule = { // Corner case where item is hidden (accessibility hiding technique) return null; } - + // First determine the color contrast ratio let colorCombo = ColorUtil.ColorCombo(elem); if (colorCombo === null) { @@ -232,6 +254,7 @@ export let text_contrast_sufficient: Rule = { let isLargeScale = size >= 24 || size >= 18.6 && weight >= 700; let passed = ratio >= 4.5 || (ratio >= 3 && isLargeScale); let hasBackground = colorCombo.hasBGImage || colorCombo.hasGradient; + let textShadow = colorCombo.textShadow; let isDisabled = RPTUtil.isNodeDisabled(elem); if (!isDisabled) { let control = RPTUtil.getControlOfLabel(elem); @@ -239,7 +262,7 @@ export let text_contrast_sufficient: Rule = { isDisabled = RPTUtil.isNodeDisabled(control); } } - + if (!isDisabled && nodeName === 'label' && RPTUtil.isDisabledByFirstChildFormElement(elem)) { isDisabled = true; } @@ -248,30 +271,33 @@ export let text_contrast_sufficient: Rule = { isDisabled = true; } - setCache(ruleContext, "EXT_Color_Contrast_WCAG2AA", { + /**setCache(ruleContext, "EXT_Color_Contrast_WCAG2AA", { "ratio": ratio, "isLargeScale": isLargeScale, "weight": weight, "size": size, "hasBackground": hasBackground, "isDisabled": isDisabled - }); - if (hasBackground) { - // Allow other color rule to fire if we have a background - return null; - } - + });*/ + // If element or parent is disabled, this rule does not apply (but may be 3:1 in future) if (!passed && isDisabled) { passed = true; } - //return new ValidationResult(passed, [ruleContext], '', '', [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]); if (!passed) { - if (fg.toHex() === bg.toHex()) { - return RulePotential("Potential_1", [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]); + if (hasBackground) { + // fire potential since a text on an image or gradient may be still viewable, depending on the text location on the gradient or image + return RulePotential("Potential_graphic_background", [ratio.toFixed(2), size, weight]);; + } else if (textShadow) { + // fire potential since a text with shadow may be still viewable, depending on the shadow efffects + return RulePotential("Potential_text_shadow", [ratio.toFixed(2), size, weight]);; } else { - return RuleFail("Fail_1", [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]); - } + if (fg.toHex() === bg.toHex()) { + return RulePotential("Potential_same_color", [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]); + } else { + return RuleFail("Fail_1", [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]); + } + } } else { return RulePass("Pass_0", [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]); } diff --git a/accessibility-checker-engine/test/v2/checker/accessibility/rules/IBMA_Color_Contrast_WCAG2AA_PV_ruleunit/Color-usingClass-BG.html b/accessibility-checker-engine/test/v2/checker/accessibility/rules/IBMA_Color_Contrast_WCAG2AA_PV_ruleunit/Color-usingClass-BG.html deleted file mode 100644 index 90823abc9..000000000 --- a/accessibility-checker-engine/test/v2/checker/accessibility/rules/IBMA_Color_Contrast_WCAG2AA_PV_ruleunit/Color-usingClass-BG.html +++ /dev/null @@ -1,358 +0,0 @@ - - - - - -
-