Skip to content

Commit

Permalink
Merge pull request #1719 from IBMa/dev-1674
Browse files Browse the repository at this point in the history
newrule(`target_spacing_sufficient`): WCAG 2.2: 2.5.8 Target size (minimum)
  • Loading branch information
ErickRenteria authored Nov 9, 2023
2 parents 6dc7dbc + c75a765 commit e0b4359
Show file tree
Hide file tree
Showing 28 changed files with 3,145 additions and 27 deletions.
18 changes: 11 additions & 7 deletions accessibility-checker-engine/README-RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,14 @@ Help integrates the following:
* Element location
* What to do
* Examples
* About the requirement
* Who does this affects?
* About this requirement
* Who does this affect?

Mappings of the latest rules to the standards, the individual failure messages, and `links to the Help files` are listed in the published [Checker rule sets](https://www.ibm.com/able/requirements/checker-rule-sets).
### Rule sets and Mappings

* Rule sets such as `IBM Accessibility v7.2`, `WCAG 2.2 (A & AA)`, etc. and mappings of the latest rules to the standards (Requirement and Rule IDs), the individual failure messages (by Reasons ID), and links to the Help files are listed in the published [Checker rule sets](https://www.ibm.com/able/requirements/checker-rule-sets)
* `npm run build:help` in the `.../accessibility-checker-engine` directory creates `dist/help/rules.html` that can be reviewed
* Each build creates the `Rules listing` artifact in **Actions** that can be reviewed prior to deployment.

## Test cases

Expand Down Expand Up @@ -143,16 +147,16 @@ Note: Rule changes are not automatically rebuilt. You will have to kill the rule

## Summary of steps to implement/update and test a new rule

* Create a rule id for a new rule.
* Create a rule id for a new rule using the 3-word format with underscores: `type_property_test`.
* Create the help file in [help-v4](help-v4).
* Create the rule implementation in [src/v4/rules](src/v4/rules). The rule implementation includes the rule context, message, help, ruleset mappings, logic and outcome.
* Create the rule implementation in [src/v4/rules](src/v4/rules). The rule implementation includes the rule context, message, help, ruleset mappings, logic, and outcome.
* Create test cases for the rule in [test/v2/checker/accessibility/rules](test/v2/checker/accessibility/rules).
* Test the rules with the test cases. You may run the test cases locally, or run with the local rule server.

## Feedback and reporting bugs

If you think you've found a bug, have questions or suggestions, open a [GitHub Issue](https://github.com/IBMa/equal-access/issues). If you are an IBM employee, feel free to ask questions in the IBM internal Slack channel `#accessibility-at-ibm`.
If you think you've found a bug or have questions or suggestions, open a [GitHub Issue](https://github.com/IBMa/equal-access/issues). If you are an IBM employee, feel free to ask questions in the IBM internal Slack channel `#accessibility-at-ibm`.

## License

[![IBM Equal Access Toolkit is released under the Apache-2.0 license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
[![IBM Equal Access Toolkit is released under the Apache-2.0 license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,16 @@ <h3 id="ruleMessage"></h3>
* Using only the keyboard, make sure you can access all the functionality provided by the mouse event handlers.
* **And**, if there is no equivalent keyboard access, follow the event handler table below to provide the corresponding keyboard event handlers:

| Use | with |
| :------------ | :------------ |
| onmousedown | onkeydown |
| onmouseup | onkeyup |
| onclick | onkeypress |
| onmouseover | onfocus |
| onmouseout | onblur |
<!-- To add a table, use three or more hyphens (---) to create each column’s header, and use pipes (|) to separate each column, add a pipe on either end of the row. -->
<!-- Cell widths can vary, the rendered output should look the same. -->

| Use | with |
| -------------- | ------------- |
| onmousedown | onkeydown |
| onmouseup | onkeyup |
| onclick | onkeypress |
| onmouseover | onfocus |
| onmouseout | onblur |

</script></mark-down>
<!-- End main panel -->
Expand All @@ -74,12 +77,12 @@ <h3 id="ruleMessage"></h3>

### About this requirement

* [IBM 2.1.1 Keyboard](https://www.ibm.com/able/requirements/requirements/#2_1_1))
* [WCAG technique SCR20](https://www.w3.org/WAI/WCAG21/Techniques/client-side-script/SCR20)
* [IBM 2.1.1 Keyboard](https://www.ibm.com/able/requirements/requirements/#2_1_1)
* [WCAG technique SCR20: Using both keyboard and other device-specific functions](https://www.w3.org/WAI/WCAG21/Techniques/client-side-script/SCR20)

### Who does this affect?

* People using a screen reader, including blind, low vision and neurodivergent people
* People using a screen reader, including blind, low vision, and neurodivergent people
* People with low vision
* People with tremor or other movement disorders
* People who rely on keyboard control
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<!--
/******************************************************************************
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.
*****************************************************************************/
-->
<!-- Title and messages generated at build time -->
<link rel="icon" href="https://ibm.com/able/favicon-32x32.png" type="image/png">
<link rel="icon" href="https://ibm.com/able/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="../common/help.css" />
<script type="module">
import "https://1.www.s81c.com/common/carbon/web-components/tag/latest/code-snippet.min.js";
import "https://1.www.s81c.com/common/carbon/web-components/tag/latest/list.min.js";
</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="../common/help.js"></script>
</head>
<body>
<div class="bx--grid toolHelp">
<div class="bx--row">
<div class="bx--col-sm-4 bx--col-md-8 bx--col-lg-16 toolHead">
<!-- Group message injected here -->
<h3 id="ruleMessage"></h3>
<!-- Severity level injected here -->
<div id="locLevel"></div>
<!-- Rule specific message injected here -->
<p id="groupLabel"></p>
</div>
</div>
<div class="bx--row">
<div class="bx--col-sm-4 bx--col-md-5 bx--col-lg-8 toolMain">
<!-- Start main panel -->
<mark-down><script type="text/plain">

### Why is this important?

Some people with physical impairments cannot click or touch small buttons that are close together, especially on mobile touch screens.

The intent of the requirement is to provide adequate clearance from any adjacent elements to prevent accidental activation of adjacent elements.
The requirement also intends to provide a larger region of the display that will accept a touch or pointer action when elements overlap and in other situations where the target size may be smaller than that of the element.

Even though exceptions such as **_inline link text_** are listed in [2.5.8 Target Size (Minimum AA)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum), it is recommended to increase the size and spacing where possible.

**Note**: When a _minimum size_ cannot be met, then a _minimum spacing_ is required.
Therefore, it is still possible to have small elements and meet the requirement, provided that the elements do not have any adjacent elements that are too close.
It is recommended to meet the minimum size requirement regardless of spacing.
For important links and controls, consider aiming for the stricter [2.5.5 Target Size (Enhanced AAA)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced) of at least 44 by 44 CSS pixels.

<!-- This is where the code snippet is injected -->
<div id="locSnippet"></div>

### What to do

- Increase the size of the element's target
- **Or**: increase the spacing around the target
- **Or**: ensure the function can be achieved through a different control on the same page that meets the minimum size and spacing requirement

</script></mark-down>
<!-- End main panel -->
<!-- This is where the rule id is injected -->
<div id="ruleInfo"></div>
</div>
<div class="bx--col-sm-4 bx--col-md-3 bx--col-lg-4 toolSide">
<!-- Start side panel -->
<mark-down><script type="text/plain">

### About this requirement

- [WCAG 2.5.8 Target Size (Minimum AA)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum)
- [WCAG technique C42: Using min-height and min-width to ensure sufficient target spacing](https://www.w3.org/WAI/WCAG22/Techniques/css/C42)

### Who does this affect?

- People who use a mobile device where the touch screen is the primary mode of interaction
- People with tremors or other movement disorders using a mouse, stylus, or touch input
- People using a device in environments where they are exposed to shaking such as public transportation
- People with large fingers or who are operating the device with only a part of their finger or knuckle
- People using a device with only one hand
- Many older adults or other users who have difficulty with fine motor movements

</script></mark-down>
<!-- End side panel -->
</div>
</div>
</div>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import { ARIAMapper } from "../../../aria/ARIAMapper";
import { DOMWalker } from "../../../dom/DOMWalker";
import { VisUtil } from "../../../dom/VisUtil";
import { FragmentUtil } from "./fragment";
import { getDefinedStyles } from "../../../../v4/util/CSSUtil";
import { getDefinedStyles, getComputedStyle } from "../../../../v4/util/CSSUtil";
import { DOMUtil } from "../../../dom/DOMUtil";
import { DOMMapper } from "../../../dom/DOMMapper";

export class RPTUtil {

Expand Down Expand Up @@ -419,6 +420,165 @@ export class RPTUtil {
}
}

/**
* a target is en element that accept a pointer action (click or touch)
*
*/
public static isTarget(element) {
if (!element) return false;

if (element.hasAttribute("tabindex") || RPTUtil.isTabbable(element)) return true;

const roles = RPTUtil.getRoles(element, true);
if (!roles && roles.length === 0)
return false;

let tagProperty = RPTUtil.getElementAriaProperty(element);
let allowedRoles = RPTUtil.getAllowedAriaRoles(element, tagProperty);
if (!allowedRoles || allowedRoles.length === 0)
return false;

let parent = element.parentElement;
// datalist, fieldset, optgroup, etc. may be just used for grouping purpose, so go up to the parent
while (parent && roles.some(role => role === 'group'))
parent = parent.parentElement;

if (parent && (parent.hasAttribute("tabindex") || RPTUtil.isTabbable(parent))) {
const target_roles =["listitem", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "switch", "treeitem"];
if (allowedRoles.includes('any') || roles.some(role => target_roles.includes(role)))
return true;
}
return false;
}

/**
* a target is en element that accept a pointer action (click or touch)
* a target is a browser default if it's a native widget (no user defined role) without user style
*/
public static isTargetBrowserDefault(element) {
if (!element) return false;

// user defained widget
const roles = RPTUtil.getRoles(element, false);
if (roles && roles.length > 0)
return false;

// no user style to space control size, including use of font
const styles = getDefinedStyles(element);
if (styles['line-height'] || styles['height'] || styles['width'] || styles['min-height'] || styles['min-width']
|| styles['font-size'] || styles['margin-top'] || styles['margin-bottom'] || styles['margin-left'] || styles['margin-right'])
return false;

return true;
}

/**
* an "inline" CSS display property tells the element to fit itself on the same line. An 'inline' element's width and height are ignored.
* some element has default inline property, such as <span>, <a>
* most formatting elements inherent inline property, such as <em>, <strong>, <i>, <small>
* other inline elements: <abbr> <acronym> <b> <bdo> <big> <br> <cite> <code> <dfn> <em> <i> <input> <kbd> <label>
* <map> <object> <output> <q> <samp> <script> <select> <small> <span> <strong> <sub> <sup> <textarea> <time> <tt> <var>
* an "inline-block" element still place element in the same line without breaking the line, but the element's width and height are applied.
* inline-block elements: img, button, select, meter, progress, marguee, also in Chrome: textarea, input
* A block-level element always starts on a new line, and the browsers automatically add some space (a margin) before and after the element.
* block-level elements: <address> <article> <aside> <blockquote> <canvas> <dd> <div> <dl> <dt> <fieldset> <figcaption> <figure> <footer> <form>
* <h1>-<h6> <header> <hr> <li> <main> <nav> <noscript> <ol> <p> <pre> <section> <table> <tfoot> <ul> <video>
*
* return: if it's inline element and { inline: true | false, text: true | false, violation: null | {node} }
*/
public static getInlineStatus(element) {
if (!element) return null;

const style = getComputedStyle(element);
if (!style) return null;

let status = { "inline": false, "text": false, "violation": null };
const udisplay = style.getPropertyValue("display");
// inline element only
if (udisplay !== 'inline')
return status;

status.inline = true;
const parent = element.parentElement;
if (parent) {
const mapper : DOMMapper = new DOMMapper();
const bounds = mapper.getUnadjustedBounds(element);
const style = getComputedStyle(parent);
const display = style.getPropertyValue("display");
// an inline element is inside a block. note <body> is a block element too
if (display === 'block' || display === 'inline-block') {
let containText = false;
// one or more inline elements with text in the same line: <target>, text<target>, <target>text, <inline>+text<target>, <target><inline>+text, text<target><inline>+
let walkNode = element.nextSibling;
let last = true;
while (walkNode) {
// note browsers insert Text nodes to represent whitespaces.
if (!containText && walkNode.nodeType === Node.TEXT_NODE && walkNode.nodeValue && walkNode.nodeValue.trim().length > 0) {
containText = true;
} else if (walkNode.nodeType === Node.ELEMENT_NODE) {
// special case: <br> is styled 'inline' by default, but change the line
if (status.violation === null && walkNode.nodeName.toLowerCase() !== 'br') {
const cStyle = getComputedStyle(walkNode);
const cDisplay = cStyle.getPropertyValue("display");
if (cDisplay === 'inline') {
last = false;
if (RPTUtil.isTarget(walkNode) && bounds.width < 24) {
// check if the horizontal spacing is sufficient
const bnds = mapper.getUnadjustedBounds(walkNode);
if (Math.round(bounds.width/2) + bnds.left - (bounds.left + bounds.width) < 24)
status.violation = walkNode.nodeName.toLowerCase();
}
} else
break;
}
}
walkNode = walkNode.nextSibling;
}

walkNode = element.previousSibling;
let first = true;
let checked = false;
while (walkNode) {
// note browsers insert Text nodes to represent whitespaces.
if (!containText && walkNode.nodeType === Node.TEXT_NODE && walkNode.nodeValue && walkNode.nodeValue.trim().length > 0) {
containText = true;
} else if (walkNode.nodeType === Node.ELEMENT_NODE) {
// special case: <br> is styled 'inline' by default, but change the line
if (!checked && walkNode.nodeName.toLowerCase() !== 'br') {
const cStyle = getComputedStyle(walkNode);
const cDisplay = cStyle.getPropertyValue("display");
if (cDisplay === 'inline') {
first = false;
checked = true;
if (RPTUtil.isTarget(walkNode) && bounds.width < 24) {
// check if the horizontal spacing is sufficient
const bnds = mapper.getUnadjustedBounds(walkNode);
if (Math.round(bounds.width/2) + bounds.left - (bnds.left + bnds.width) < 24) {
status.violation = status.violation === null ? walkNode.nodeName.toLowerCase() : status.violation + ", " + walkNode.nodeName.toLowerCase();
}
}
} else
break;
}
}
walkNode = walkNode.previousSibling;
}

// one or more inline elements are in the same line with text
if (containText)
status.text = true;

return status;
} else {
//parent is inline element
if (!RPTUtil.isInnerTextOnlyEmpty(parent))
status.text = true;
}
}
// all other cases
return status;
}

public static tabIndexLEZero(elem) {
if (RPTUtil.hasAttribute(elem, "tabindex")) {
if (elem.getAttribute("tabindex").match(/^-?\d+$/)) {
Expand Down
Loading

0 comments on commit e0b4359

Please sign in to comment.