Skip to content

Commit

Permalink
Expose ariaAllowedAttributes on ARIA Element (#1721)
Browse files Browse the repository at this point in the history
* Expose 'allowedAttributes' on alfa Element class

Move the logic from 'r18/rule.ts' into the alfa-aria package.

* Add changeset

* Code review fixes

Co-authored-by: Jean-Yves Moyen <[email protected]>

* Fix dependencies

* Yarn install

* Run yarn extract again

---------

Co-authored-by: Jean-Yves Moyen <[email protected]>
  • Loading branch information
srnjcbsn and Jym77 authored Nov 29, 2024
1 parent c9aff2e commit 53e9682
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 76 deletions.
8 changes: 8 additions & 0 deletions .changeset/twenty-boxes-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@siteimprove/alfa-rules": minor
"@siteimprove/alfa-aria": minor
---

**Added:** Expose `allowedAttributes` on ARIA Element type.

This function takes into account "implicit ARIA semantics" and "ARIA role allowances" from [ARIA in HTML](https://w3c.github.io/html-aria/#docconformance). The logic is moved from rule R18 implementation.
3 changes: 3 additions & 0 deletions docs/review/api/alfa-aria.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export namespace DOM {

// @public (undocumented)
export class Element extends Node<"element"> {
allowedAttributes(): ReadonlyArray<Attribute.Name>;
// (undocumented)
attribute<N extends Attribute.Name>(refinement: Refinement<Attribute, Attribute<N>>): Option<Attribute<N>>;
// (undocumented)
Expand All @@ -137,6 +138,8 @@ export class Element extends Node<"element"> {
// (undocumented)
clone(): Element;
// (undocumented)
isAttributeAllowed(attribute: Attribute.Name): boolean;
// (undocumented)
isIgnored(): boolean;
// (undocumented)
get name(): Option<Name>;
Expand Down
1 change: 1 addition & 0 deletions packages/alfa-aria/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@siteimprove/alfa-option": "workspace:^0.95.0",
"@siteimprove/alfa-predicate": "workspace:^0.95.0",
"@siteimprove/alfa-refinement": "workspace:^0.95.0",
"@siteimprove/alfa-selective": "workspace:^0.95.0",
"@siteimprove/alfa-selector": "workspace:^0.95.0",
"@siteimprove/alfa-sequence": "workspace:^0.95.0",
"@siteimprove/alfa-set": "workspace:^0.95.0",
Expand Down
65 changes: 64 additions & 1 deletion packages/alfa-aria/src/node/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import type * as dom from "@siteimprove/alfa-dom";
import type { Attribute } from "../attribute.js";
import type { Name } from "../name/index.js";
import { Node } from "../node.js";
import type { Role } from "../role.js";
import { Set } from "@siteimprove/alfa-set";
import { Role } from "../role.js";
import type { InputType } from "../../../alfa-dom/src/node/element/input-type.js";
import { Element as DomElement } from "@siteimprove/alfa-dom";
import { Selective } from "@siteimprove/alfa-selective";

/**
* @public
Expand Down Expand Up @@ -115,6 +119,65 @@ export class Element extends Node<"element"> {
...this._children.map((child) => String.indent(child.toString())),
].join("\n");
}

private static allowedAttributesForInputType(
inputType: InputType
): ReadonlyArray<Attribute.Name> {
switch (inputType) {
// https://www.w3.org/TR/html-aria/#el-input-color
case "color":
return ["aria-disabled"];
// https://www.w3.org/TR/html-aria/#el-input-date
case "date":
// https://www.w3.org/TR/html-aria/#el-input-datetime-local
case "datetime-local":
// https://www.w3.org/TR/html-aria/#el-input-email
case "email":
// https://www.w3.org/TR/html-aria/#el-input-month
case "month":
// https://www.w3.org/TR/html-aria/#el-input-password
case "password":
// https://www.w3.org/TR/html-aria/#el-input-time
case "time":
// https://www.w3.org/TR/html-aria/#el-input-week
case "week":
return Role.of("textbox").supportedAttributes;
// https://www.w3.org/TR/html-aria/#el-input-file
case "file":
return ["aria-disabled", "aria-invalid", "aria-required"];
default:
return [];
}
}

/**
* The attributes that are allowed on this element, taking into consideration ARIA in HTML conformance requirements.
* See {@link https://w3c.github.io/html-aria/#docconformance}
*/
public allowedAttributes(): ReadonlyArray<Attribute.Name> {
const global = Role.of("roletype").supportedAttributes;
const fromRole = this.role.map(role => role.supportedAttributes).getOr([]);
const additional = Selective.of(this.node)
.if(DomElement.hasName("input"), input =>
Element.allowedAttributesForInputType(input.inputType())
)
// https://www.w3.org/TR/html-aria/#el-select
.if(
DomElement.hasName("select"),
select =>
DomElement.hasDisplaySize((size: Number) => size !== 1)(select)
? Role.of("combobox").supportedAttributes
: Role.of("menu").supportedAttributes
)
.else(() => [])
.get();

return Array.from(Set.from([... global, ...fromRole, ...additional]));
}

public isAttributeAllowed(attribute: Attribute.Name): boolean {
return this.allowedAttributes().includes(attribute);
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/alfa-aria/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
{ "path": "../../alfa-option" },
{ "path": "../../alfa-predicate" },
{ "path": "../../alfa-refinement" },
{ "path": "../../alfa-selective" },
{ "path": "../../alfa-selector" },
{ "path": "../../alfa-sequence" },
{ "path": "../../alfa-set" },
Expand Down
1 change: 0 additions & 1 deletion packages/alfa-rules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
"@siteimprove/alfa-refinement": "workspace:^0.95.0",
"@siteimprove/alfa-result": "workspace:^0.95.0",
"@siteimprove/alfa-sarif": "workspace:^0.95.0",
"@siteimprove/alfa-selective": "workspace:^0.95.0",
"@siteimprove/alfa-selector": "workspace:^0.95.0",
"@siteimprove/alfa-sequence": "workspace:^0.95.0",
"@siteimprove/alfa-set": "workspace:^0.95.0",
Expand Down
77 changes: 5 additions & 72 deletions packages/alfa-rules/src/sia-r18/rule.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Diagnostic, Rule } from "@siteimprove/alfa-act";
import { DOM, Role } from "@siteimprove/alfa-aria";
import { DOM } from "@siteimprove/alfa-aria";
import type { Attribute } from "@siteimprove/alfa-dom";
import { Element, Node, Query } from "@siteimprove/alfa-dom";
import { Node, Query } from "@siteimprove/alfa-dom";
import { Predicate } from "@siteimprove/alfa-predicate";
import { Err, Ok } from "@siteimprove/alfa-result";
import { Selective } from "@siteimprove/alfa-selective";
import { Sequence } from "@siteimprove/alfa-sequence";
import { Set } from "@siteimprove/alfa-set";
import { Technique } from "@siteimprove/alfa-wcag";
import type { Page } from "@siteimprove/alfa-web";

Expand All @@ -18,7 +16,6 @@ import { ARIA } from "../requirements/index.js";
import { Scope, Stability, Version } from "../tags/index.js";

const { hasRole, isIncludedInTheAccessibilityTree } = DOM;
const { hasDisplaySize, hasInputType, hasName } = Element;
const { test, property } = Predicate;
const { getElementDescendants } = Query;

Expand All @@ -30,8 +27,6 @@ export default Rule.Atomic.of<Page, Attribute>({
],
tags: [Scope.Component, Stability.Stable, Version.of(2)],
evaluate({ device, document }) {
const global = Set.from(Role.of("roletype").supportedAttributes);

return {
applicability() {
return getElementDescendants(document, Node.fullTree)
Expand All @@ -44,19 +39,13 @@ export default Rule.Atomic.of<Page, Attribute>({
},

expectations(target) {
// Since the attribute was found on a element, it has a owner.
// Since the attribute was found on a element, it has an owner.
const owner = target.owner.getUnsafe();
const ariaNode = aria.Node.from(owner, device) as aria.Element;

return {
1: expectation(
global.has(target.name as aria.Attribute.Name) ||
test(
hasRole(device, (role) =>
role.isAttributeSupported(target.name as aria.Attribute.Name),
),
owner,
) ||
ariaHtmlAllowed(target),
ariaNode.isAttributeAllowed(target.name as aria.Attribute.Name),
() => Outcomes.IsAllowed,
() => Outcomes.IsNotAllowed,
),
Expand All @@ -76,62 +65,6 @@ export default Rule.Atomic.of<Page, Attribute>({
},
});

function allowedForInputType(
attributeName: aria.Attribute.Name,
): Predicate<Element> {
return hasInputType((inputType) => {
switch (inputType) {
// https://www.w3.org/TR/html-aria/#el-input-color
case "color":
return attributeName === "aria-disabled";
// https://www.w3.org/TR/html-aria/#el-input-date
case "date":
// https://www.w3.org/TR/html-aria/#el-input-datetime-local
case "datetime-local":
// https://www.w3.org/TR/html-aria/#el-input-email
case "email":
// https://www.w3.org/TR/html-aria/#el-input-month
case "month":
// https://www.w3.org/TR/html-aria/#el-input-password
case "password":
// https://www.w3.org/TR/html-aria/#el-input-time
case "time":
// https://www.w3.org/TR/html-aria/#el-input-week
case "week":
return Role.of("textbox").isAttributeSupported(attributeName);
// https://www.w3.org/TR/html-aria/#el-input-file
case "file":
return (
attributeName === "aria-disabled" ||
attributeName === "aria-invalid" ||
attributeName === "aria-required"
);
default:
return false;
}
});
}

function ariaHtmlAllowed(target: Attribute): boolean {
const attributeName = target.name as aria.Attribute.Name;
return target.owner
.map((element) =>
Selective.of(element)
.if(hasName("input"), allowedForInputType(attributeName))
// https://www.w3.org/TR/html-aria/#el-select
.if(
hasName("select"),
(select) =>
(hasDisplaySize((size: Number) => size !== 1)(select) &&
Role.of("combobox").isAttributeSupported(attributeName)) ||
Role.of("menu").isAttributeSupported(attributeName),
)
.else(() => false)
.get(),
)
.getOr(false);
}

/**
* @public
*/
Expand Down
1 change: 0 additions & 1 deletion packages/alfa-rules/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,6 @@
{ "path": "../../alfa-refinement" },
{ "path": "../../alfa-result" },
{ "path": "../../alfa-sarif" },
{ "path": "../../alfa-selective" },
{ "path": "../../alfa-selector" },
{ "path": "../../alfa-sequence" },
{ "path": "../../alfa-set" },
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,7 @@ __metadata:
"@siteimprove/alfa-option": "workspace:^0.95.0"
"@siteimprove/alfa-predicate": "workspace:^0.95.0"
"@siteimprove/alfa-refinement": "workspace:^0.95.0"
"@siteimprove/alfa-selective": "workspace:^0.95.0"
"@siteimprove/alfa-selector": "workspace:^0.95.0"
"@siteimprove/alfa-sequence": "workspace:^0.95.0"
"@siteimprove/alfa-set": "workspace:^0.95.0"
Expand Down Expand Up @@ -1742,7 +1743,6 @@ __metadata:
"@siteimprove/alfa-refinement": "workspace:^0.95.0"
"@siteimprove/alfa-result": "workspace:^0.95.0"
"@siteimprove/alfa-sarif": "workspace:^0.95.0"
"@siteimprove/alfa-selective": "workspace:^0.95.0"
"@siteimprove/alfa-selector": "workspace:^0.95.0"
"@siteimprove/alfa-sequence": "workspace:^0.95.0"
"@siteimprove/alfa-set": "workspace:^0.95.0"
Expand Down

0 comments on commit 53e9682

Please sign in to comment.