Skip to content

Commit

Permalink
Merge pull request #1944 from IBMa/dev-1623
Browse files Browse the repository at this point in the history
fixrule(`input_label_visible, input_placeholder_label_visible, input_checkboxes_grouped`): tight the scope of the rules and merge help
  • Loading branch information
ErickRenteria authored Jul 29, 2024
2 parents a5fedb2 + 554ab0e commit 4c92a58
Show file tree
Hide file tree
Showing 15 changed files with 1,198 additions and 161 deletions.
48 changes: 38 additions & 10 deletions accessibility-checker-engine/help-v4/en-US/input_label_visible.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,45 @@ <h3 id="ruleMessage"></h3>

### Why is this important?

Visible labels are essential so that people using voice control know what to say.
This allows them to easily navigate to interactive elements on the screen.
Visible labels are essential, so every user can know what information to enter:
- People with cognitive, language, and learning disabilities, older adults, and all users will easily learn what information is expected.
- People using voice control will see what to speak. This allows them to easily jump to interactive elements and form fields.

Placeholder labels when used as the only visible label can reduce the accessibility for a wide range of users. Avoid placeholder labels for the following reasons:
- May not be persistent: the placeholder label disappears when the user starts typing in the input field
- Can be mistaken for a pre-filled value: this can be more of an issue in AI applications
- Hard to see: the text color commonly fails a minimum contrast ratio of 4.5 to 1
- May be too small: the lack of a label reduces the hit region for setting focus on the input field in touch devices

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

### What to do

* If there is already a visible label for the `<input>` element, use the `for` attribute on the label to reference the `<input>` element's `id` value
* **Or**, add a visible label, with the `for` attribute linking it to the `<input>`
The intent of labels, including expected formats and required fields,
on interactive elements is not to clutter the page with unnecessary information but to provide important cues that will benefit all users.
Too much information can be just as harmful as too little.

For example:
**If** there is already a visible label for the element, use an attribute on the label to reference the element's id
- **Or**, add a visible label with an attribute or coding construct associating it to the element.

**If** using the placeholder attribute, ensure it is a short hint to aid the user with data entry,
- **And**, use a valid label method that is both visible and programmatically determined.

**If** a visible label cannot be added,
- **Either**, make a label or instructions available to users when the individual control has focus (both keyboard and pointer),
- **Or**, verify the input field and interactive element is understood within the context or instructions.

Examples:

```
<label for="test1">License Number:</label>
<input type="text" id="test1" aria-label="Enter driver license number"/>
<input type="text" id="test1" aria-label="Enter driver license number"/>
<label for="test2">First name:</label>
<input type="text" id="test2"/>
<input type="text" id="test2"/>
<label>Email Address:
<input type="email" name="address1" placeholder="[email protected]">
</label>
```

</script></mark-down>
Expand All @@ -77,13 +98,20 @@ <h3 id="ruleMessage"></h3>
### About this requirement

* [IBM 3.3.2 Label or Instruction](https://www.ibm.com/able/requirements/requirements/#3_3_2)
* [IBM 2.5.3 Label in Name](https://www.ibm.com/able/requirements/requirements/#2_5_3)
* [H44: Associate text labels with form controls](https://www.w3.org/WAI/WCAG22/Techniques/html/H44)
* [ARIA1: Use aria-describedby to label a user interface control](https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA1)
* [ARIA9: Use aria-labelledby to concatenate several text nodes](https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA9)
* [W3C Placeholder Research](https://www.w3.org/WAI/GL/low-vision-a11y-tf/wiki/Placeholder_Research)

### Who does this affect?


* People with cognitive, language, and learning disabilities
* People with dexterity impairments using voice control

* People with low vision using screen magnification
* People with vision impairments needing contrast enhancement
* People with tremors or other movement disorders using touch devices
* Many older adults

</script></mark-down>
<!-- End side panel -->
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1929,6 +1929,33 @@ export class RPTUtil {

return descendant;
}

/**
* This function is responsible for getting All descendant elements with the specified roles, under
* the element that was provided. This function aslo finds elements with implicit roles.
*
* @parm {element} element - parent element for which we will be checking descendants for
* @parm {string[]} roleNames - The roles to look for on the descendant's elements
* @parm {bool} considerHiddenSetting - true or false based on if hidden setting should be considered.
* @parm {bool} considerImplicitRoles - true or false based on if implicit roles setting should be considered.
*
* @return {node[]} - all descendant elements that match the roles specified
*
* @memberOf RPTUtil
*/
public static getAllDescendantsWithRoles(element, roleNames:string[], considerHiddenSetting, considerImplicitRoles) {
if (!roleNames || roleNames.length ===0) return;
// Variable Decleration
let descendants = [];

roleNames.forEach(roleName => {
let kids = RPTUtil.getAllDescendantsWithRoleHidden(element, roleName, considerHiddenSetting, considerImplicitRoles);
if (kids && kids.length > 0)
descendants = descendants.concat(kids);
});
return descendants;
}

/**
* This function is responsible for getting All descendant elements with the specified role, under
* the element that was provided. This function aslo finds elements with implicit roles.
Expand All @@ -1938,11 +1965,11 @@ export class RPTUtil {
* @parm {bool} considerHiddenSetting - true or false based on if hidden setting should be considered.
* @parm {bool} considerImplicitRoles - true or false based on if implicit roles setting should be considered.
*
* @return {node} - The descendant element that matches the role specified (only one)
* @return {node[]} - The descendant elements that match the role specified
*
* @memberOf RPTUtil
*/
public static getAllDescendantsWithRoleHidden(element, roleName, considerHiddenSetting, considerImplicitRoles) {
public static getAllDescendantsWithRoleHidden(element, roleName:string, considerHiddenSetting, considerImplicitRoles) {
// Variable Decleration
let descendants = [];
let nw = new NodeWalker(element);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,46 +20,46 @@ import { VisUtil } from "../../v2/dom/VisUtil";

export let input_checkboxes_grouped: Rule = {
id: "input_checkboxes_grouped",
context: "dom:input",
context: "dom:input[type=radio], dom:input[type=checkbox]",
refactor: {
"WCAG20_Input_RadioChkInFieldSet": {
"Pass_LoneNogroup": "Pass_LoneNogroup",
"Pass_Grouped": "Pass_Grouped",
"Pass_RadioNoName": "Pass_RadioNoName",
"Fail_ControlNameMismatch": "Fail_ControlNameMismatch",
"Fail_NotGroupedOtherGrouped": "Fail_NotGroupedOtherGrouped",
"Fail_NotGroupedOtherNotGrouped": "Fail_NotGroupedOtherNotGrouped",
"Fail_NotSameGroup": "Fail_NotSameGroup",
"Potential_LoneCheckbox": "Potential_LoneCheckbox",
"Potential_UnnamedCheckbox": "Potential_UnnamedCheckbox"
"Pass_LoneNogroup": "pass_lonenogroup",
"Pass_Grouped": "pass_grouped",
"Pass_RadioNoName": "pass_radioNoName",
"Fail_ControlNameMismatch": "fail_controlnamemismatch",
"Fail_NotGroupedOtherGrouped": "fail_notgroupedothergrouped",
"Fail_NotGroupedOtherNotGrouped": "fail_notgroupedothernotgrouped",
"Fail_NotSameGroup": "fail_notsamegroup",
"Potential_LoneCheckbox": "potential_lonecheckbox",
"Potential_UnnamedCheckbox": "potential_unnamedcheckbox"
}
},
help: {
"en-US": {
"group": "input_checkboxes_grouped.html",
"Pass_LoneNogroup": "input_checkboxes_grouped.html",
"Pass_Grouped": "input_checkboxes_grouped.html",
"Pass_RadioNoName": "input_checkboxes_grouped.html",
"Fail_ControlNameMismatch": "input_checkboxes_grouped.html",
"Fail_NotGroupedOtherGrouped": "input_checkboxes_grouped.html",
"Fail_NotGroupedOtherNotGrouped": "input_checkboxes_grouped.html",
"Fail_NotSameGroup": "input_checkboxes_grouped.html",
"Potential_LoneCheckbox": "input_checkboxes_grouped.html",
"Potential_UnnamedCheckbox": "input_checkboxes_grouped.html"
"pass_lonenogroup": "input_checkboxes_grouped.html",
"pass_grouped": "input_checkboxes_grouped.html",
"pass_radiononame": "input_checkboxes_grouped.html",
"fail_controlnamemismatch": "input_checkboxes_grouped.html",
"fail_notgroupedothergrouped": "input_checkboxes_grouped.html",
"fail_notgroupedothernotgrouped": "input_checkboxes_grouped.html",
"fail_notsamegroup": "input_checkboxes_grouped.html",
"potential_lonecheckbox": "input_checkboxes_grouped.html",
"potential_unnamedcheckbox": "input_checkboxes_grouped.html"
}
},
messages: {
"en-US": {
"group": "Related sets of radio buttons or checkboxes should be programmatically grouped",
"Pass_LoneNogroup": "{0} grouping not required for a control of this type",
"Pass_Grouped": "{0} input is grouped with other related controls with the same name",
"Pass_RadioNoName": "Radio input is not grouped, but passes because it has no name to group with other radio inputs",
"Fail_ControlNameMismatch": "{0} input found that has the same name, \"{2}\" as a {1} input",
"Fail_NotGroupedOtherGrouped": "{0} input is not in the group with another {0} with the name \"{1}\"",
"Fail_NotGroupedOtherNotGrouped": "{0} input and others with the name \"{1}\" are not grouped together",
"Fail_NotSameGroup": "{0} input is in a different group than another {0} with the name \"{1}\"",
"Potential_LoneCheckbox": "Verify that this ungrouped checkbox input is not related to other checkboxes",
"Potential_UnnamedCheckbox": "Verify that this un-named, ungrouped checkbox input is not related to other checkboxes"
"pass_lonenogroup": "{0} grouping not required for a control of this type",
"pass_grouped": "{0} input is grouped with other related controls with the same name",
"pass_radiononame": "Radio input is not grouped, but passes because it has no name to group with other radio inputs",
"fail_controlnamemismatch": "{0} input found that has the same name, \"{2}\" as a {1} input",
"fail_notgroupedothergrouped": "{0} input is not in the group with another {0} with the name \"{1}\"",
"fail_notgroupedothernotgrouped": "{0} input and others with the name \"{1}\" are not grouped together",
"fail_notsamegroup": "{0} input is in a different group than another {0} with the name \"{1}\"",
"potential_lonecheckbox": "Verify that this ungrouped checkbox input is not related to other checkboxes",
"potential_unnamedcheckbox": "Verify that this un-named, ungrouped checkbox input is not related to other checkboxes"
}
},
rulesets: [{
Expand All @@ -71,7 +71,9 @@ export let input_checkboxes_grouped: Rule = {
act: [],
run: (context: RuleContext, options?: {}, contextHierarchies?: RuleContextHierarchy): RuleResult | RuleResult[] => {
const ruleContext = context["dom"].node as Element;
if (context["aria"].role === 'none' || context["aria"].role === 'presentation') return null;

//skip the rule
if (VisUtil.isNodeHiddenFromAT(ruleContext)) return null;

const getGroup = (e: Element) => {
let retVal = RPTUtil.getAncestor(e, "fieldset")
Expand All @@ -89,11 +91,8 @@ export let input_checkboxes_grouped: Rule = {
}

// Only radio buttons and checkboxes are in scope
let ctxType = ruleContext.hasAttribute("type") ? ruleContext.getAttribute("type").toLowerCase() : "text";
if (ctxType !== "checkbox" && ctxType !== "radio") {
return null;
}

let ctxType = ruleContext.getAttribute("type").toLowerCase();

// Determine which form we're in (if any) to determine our scope
let ctxForm = RPTUtil.getAncestorWithRole(ruleContext, "form")
|| RPTUtil.getAncestor(ruleContext, "html")
Expand Down Expand Up @@ -172,20 +171,20 @@ export let input_checkboxes_grouped: Rule = {
if (ctxType === "Radio") {
// Radios without names don't act like groups, so don't enforce grouping
if (ctxGroup === null) {
return RulePass("Pass_RadioNoName", [ctxType]);
return RulePass("pass_radiononame", [ctxType]);
} else {
return RulePass("Pass_Grouped", [ctxType]);
return RulePass("pass_grouped", [ctxType]);
}
} else {
// Must be an unnamed checkbox
if (ctxGroup === null) {
if ((formCache.checkboxByName[""] || []).length > 1) {
return RulePotential("Potential_UnnamedCheckbox", [ctxType]);
return RulePotential("potential_unnamedcheckbox", [ctxType]);
} else {
return RulePass("Pass_LoneNogroup", [ctxType]);
return RulePass("pass_lonenogroup", [ctxType]);
}
} else {
return RulePass("Pass_Grouped", [ctxType]);
return RulePass("pass_grouped", [ctxType]);
}
}
} else {
Expand All @@ -195,40 +194,40 @@ export let input_checkboxes_grouped: Rule = {
// Capitalize the input type for messages
if (numRadiosWithName > 0 && numCheckboxesWithName > 0) {
// We have a naming mismatch between different controls
return RuleFail("Fail_ControlNameMismatch", [ctxType, ctxType === "checkbox" ? "radio" : "checkbox", ctxName]);
return RuleFail("fail_controlnamemismatch", [ctxType, ctxType === "checkbox" ? "radio" : "checkbox", ctxName]);
} else if (ctxType === "Radio" && (formCache.numRadios === 1 || numRadiosWithName === 1)
|| ctxType === "Checkbox" && formCache.numCheckboxes === 1) {
// This is a lone control (either only control of this type on the page, or a radio button without any others by that name)
// We pass this control in all cases
if (ctxGroup === null) {
return RulePass("Pass_LoneNogroup", [ctxType]);
return RulePass("pass_lonenogroup", [ctxType]);
} else {
return RulePass("Pass_Grouped", [ctxType]);
return RulePass("pass_grouped", [ctxType]);
}
} else if (ctxType === "Checkbox" && formCache.numCheckboxes > 1 && numCheckboxesWithName === 1) {
// We have only one checkbox with this name, but there are other checkboxes in the form.
// If we're not grouped, ask them to examine it
if (ctxGroup === null) {
return RulePotential("Potential_LoneCheckbox", [ctxType]);
return RulePotential("potential_lonecheckbox", [ctxType]);
} else {
return RulePass("Pass_Grouped", [ctxType]);
return RulePass("pass_grouped", [ctxType]);
}
} else {
// We share a name with another similar control. Are we grouped together?
if (ctxGroup === null) {
if (formCache.nameToGroup[ctxName] !== null) {
// We're not grouped, but some control with the same name is in a group
return RuleFail("Fail_NotGroupedOtherGrouped", [ctxType, ctxName]);
return RuleFail("fail_notgroupedothergrouped", [ctxType, ctxName]);
} else {
// None of us are grouped
return RuleFail("Fail_NotGroupedOtherNotGrouped", [ctxType, ctxName])
return RuleFail("fail_notgroupedothernotgrouped", [ctxType, ctxName])
}
} else if (formCache.nameToGroup[ctxName] !== ctxGroup) {
// We're not in the main group with the others
return RuleFail("Fail_NotSameGroup", [ctxType, ctxName]);
return RuleFail("fail_notsamegroup", [ctxType, ctxName]);
} else {
// We're all grouped up!
return RulePass("Pass_Grouped", [ctxType]);
return RulePass("pass_grouped", [ctxType]);
}
}
}
Expand Down
Loading

0 comments on commit 4c92a58

Please sign in to comment.