From 1efb4b56fafff576f3fca1aa5b653a41990bcd3e Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Tue, 23 Jun 2020 09:40:57 +0200 Subject: [PATCH] Move presentational role conflict resolution down to Role.from (#273) * Move presentational role conflict resolution down to Role.from --- packages/alfa-aria/src/feature.ts | 13 ++-- packages/alfa-aria/src/node.ts | 121 ++++++++++-------------------- packages/alfa-aria/src/role.ts | 39 +++++++++- 3 files changed, 80 insertions(+), 93 deletions(-) diff --git a/packages/alfa-aria/src/feature.ts b/packages/alfa-aria/src/feature.ts index 1decca197a..95b7c46abd 100644 --- a/packages/alfa-aria/src/feature.ts +++ b/packages/alfa-aria/src/feature.ts @@ -57,11 +57,7 @@ export class Feature { } export namespace Feature { - export type Aspect = []> = Mapper< - Element, - T, - A - >; + export type Aspect = []> = Mapper; export interface Status { readonly obsolete: boolean; @@ -71,7 +67,7 @@ export namespace Feature { /** * @internal */ - readonly allowPresentational?: boolean; + readonly allowPresentational: boolean; } const features = Cache.empty>(); @@ -309,9 +305,10 @@ Feature.register( Feature.register( Namespace.HTML, - Feature.of("img", (element, { allowPresentational = true }) => + Feature.of("img", (element, { allowPresentational }) => Option.of( - allowPresentational && element.attribute("alt").some((alt) => alt.value === "") + allowPresentational && + element.attribute("alt").some((alt) => alt.value === "") ? "presentation" : "img" ) diff --git a/packages/alfa-aria/src/node.ts b/packages/alfa-aria/src/node.ts index 4229eb0a1b..252f32adec 100644 --- a/packages/alfa-aria/src/node.ts +++ b/packages/alfa-aria/src/node.ts @@ -231,75 +231,58 @@ export namespace Node { if (style.computed("visibility").value.value !== "visible") { accessibleNode = Branched.of(Container.of(node)); } else { - accessibleNode = Role.from(node) - .flatMap((role) => { - // If the element has a presentational role, but is not allowed to - // be presentational, we fall back to its implicit role by not - // considering its explicit role. - if ( - role.some(isPresentational) && - !isAllowedPresentational(node) - ) { - return Role.from(node, { - explicit: false, - allowPresentational: false, - }); - } - - return Branched.of(role); - }) - .flatMap((role) => { - if (role.some(isPresentational)) { - return Branched.of(Container.of(node)); - } + accessibleNode = Role.from(node).flatMap((role) => { + if (role.some(Role.isPresentational)) { + return Branched.of(Container.of(node)); + } - let attributes = Map.empty(); + let attributes = Map.empty(); - // First pass: Look up implicit attributes on the role. - if (role.isSome()) { - const queue = [role.get()]; + // First pass: Look up implicit attributes on the role. + if (role.isSome()) { + const queue = [role.get()]; - while (queue.length > 0) { - const role = queue.pop()!; + while (queue.length > 0) { + const role = queue.pop()!; - for (const [name, value] of role.characteristics.implicits) { - attributes = attributes.set(name, value); - } + for (const [name, value] of role.characteristics.implicits) { + attributes = attributes.set(name, value); + } - for (const name of role.characteristics.inherits) { - for (const role of Role.lookup(name)) { - queue.push(role); - } + for (const name of role.characteristics.inherits) { + for (const role of Role.lookup(name)) { + queue.push(role); } } } + } - // Second pass: Look up implicit attributes on the feature mapping. - for (const namespace of node.namespace) { - for (const feature of Feature.lookup(namespace, node.name)) { - attributes = attributes.concat(feature.attributes(node)); - } + // Second pass: Look up implicit attributes on the feature mapping. + for (const namespace of node.namespace) { + for (const feature of Feature.lookup(namespace, node.name)) { + attributes = attributes.concat(feature.attributes(node)); } + } - // Third pass: Look up explicit `aria-*` attributes and set the - // ones that are allowed by the role. - for (const attribute of node.attributes) { - if ( - attribute.name.startsWith("aria-") && - role - .orElse(() => Role.lookup("roletype")) - .some((role) => - role.isAllowed(property("name", equals(attribute.name))) - ) - ) { - attributes = attributes.set(attribute.name, attribute.value); - } + // Third pass: Look up explicit `aria-*` attributes and set the + // ones that are allowed by the role. + for (const attribute of node.attributes) { + if ( + attribute.name.startsWith("aria-") && + role + .orElse(() => Role.lookup("roletype")) + .some((role) => + role.isAllowed(property("name", equals(attribute.name))) + ) + ) { + attributes = attributes.set(attribute.name, attribute.value); } + } - return getName(node, device).map((name) => - Element.of(node, role, name, attributes) - ); - }); + return getName(node, device).map((name) => + Element.of(node, role, name, attributes) + ); + }); } } @@ -323,29 +306,3 @@ export namespace Node { }); } } - -const isPresentational: Predicate = property( - "name", - equals("presentation", "none") -); - -/** - * Determine if an element is allowed to be presentational. - * - * @see https://w3c.github.io/aria/#conflict_resolution_presentation_none - */ -const isAllowedPresentational: Predicate = (element) => { - if (element.tabIndex().isSome()) { - return false; - } - - return Role.lookup("roletype").some((role) => { - for (const attribute of role.characteristics.supports) { - if (element.attribute(attribute).isSome()) { - return false; - } - } - - return true; - }); -}; diff --git a/packages/alfa-aria/src/role.ts b/packages/alfa-aria/src/role.ts index 99ca303c6d..2d1cd5d45b 100644 --- a/packages/alfa-aria/src/role.ts +++ b/packages/alfa-aria/src/role.ts @@ -233,6 +233,9 @@ export namespace Role { ): Branched, Browser> { const role = element.attribute("role").map((attr) => attr.value.trim()); + const allowedPresentational = + options.allowPresentational ?? isAllowedPresentational(element); + return ( Branched.of, Browser>( role.map((role) => role.toLowerCase()) @@ -251,15 +254,22 @@ export namespace Role { const role = Role.lookup(name); if ( + // If the role is not abstract... role.some( (role) => role.category !== Role.Category.Abstract + ) && + // ...and it's not a presentational role in a forbidden context... + !( + role.some(Role.isPresentational) && + !isAllowedPresentational ) ) { + // ...then we got ourselves a valid explicit role... return role; } } } - + // ...otherwise, default to implicit role computation. return None; }) .orElse(() => { @@ -270,7 +280,7 @@ export namespace Role { return feature.flatMap((feature) => feature .role(element, { - allowPresentational: options.allowPresentational, + allowPresentational: allowedPresentational, }) .flatMap(Role.lookup) ); @@ -284,12 +294,14 @@ export namespace Role { } export namespace from { - export interface Options extends Feature.RoleOptions { + export interface Options extends Partial { readonly explicit?: boolean; readonly implicit?: boolean; } } + export const isPresentational = hasName("presentation", "none"); + export function hasName(predicate: Predicate): Predicate; export function hasName( @@ -313,6 +325,27 @@ export namespace Role { } } +/** + * Determine if an element is allowed to be presentational. + * + * @see https://w3c.github.io/aria/#conflict_resolution_presentation_none + */ +const isAllowedPresentational: Predicate = (element) => { + if (element.tabIndex().isSome()) { + return false; + } + + return Role.lookup("roletype").some((role) => { + for (const attribute of role.characteristics.supports) { + if (element.attribute(attribute).isSome()) { + return false; + } + } + + return true; + }); +}; + import "./role/separator"; import "./role/abstract/command";