Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement step 2H of the accessible name computation #430

Merged
merged 1 commit into from
Oct 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions packages/alfa-aria/src/name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@ export namespace Name {
return fromNode(node, device, State.empty());
}

const names = Cache.empty<Node, Branched<Option<Name>, Browser>>();

/**
* @internal
*/
Expand All @@ -469,7 +471,27 @@ export namespace Name {
device: Device,
state: State
): Branched<Option<Name>, Browser> {
return isElement(node) ? fromElement(node, device, state) : fromText(node);
// Construct a thunk with the computed name of the node. We first need to
// decide whether or not we can pull the name of the node from the cache and
// so the actual computation of the name must be delayed.
const name = () =>
isElement(node) ? fromElement(node, device, state) : fromText(node);

if (isElement(node)) {
// As chained references are not allowed, we cannot make use of the cache
// when computing a referenced name. If, for example, `foo` references
// `bar` and `bar` references `baz`, the reference from `bar` to `baz` is
// only allowed to be followed when computing a name for `bar`. When
// computing the name for `foo`, however, the second reference must be
// ignored and the name for `bar` computed as if though the reference does
// not exist. This of course means that we cannot make use of whatever is
// in the cache for `bar`.
if (state.isReferencing) {
return name();
}
}

return names.get(node, name);
}

/**
Expand Down Expand Up @@ -576,25 +598,31 @@ export namespace Name {
return empty;
}

return fromDescendants(element, device, state);
},

// Step 2H: Use the subtree content, if descending.
// https://w3c.github.io/accname/#step2H
() => {
// Unless we're already descending then this step produces an empty
// name.
if (!state.isDescending) {
return empty;
}

return fromDescendants(element, device, state);
}
);
});
}

/**
* Accessible names for text nodes are abundant and not impacted by the
* computation state and so are easily cached.
*/
const textNames = Cache.empty<Text, Branched<Option<Name>, Browser>>();

/**
* @internal
*/
export function fromText(text: Text): Branched<Option<Name>, Browser> {
// Step 2G: Use the data of the text node.
// https://w3c.github.io/accname/#step2G
return textNames.get(text, () => fromData(text));
return fromData(text);
}

/**
Expand Down
85 changes: 81 additions & 4 deletions packages/alfa-aria/test/name.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,59 @@ test(`.from() determines the name of an <img> element with an alt attribute`, (t
]);
});

test(`.from() determines the name of an <a> element with a <img> child element
with an alt attribute`, (t) => {
const img = <img alt="Hello world" />;

const a = <a href="#">{img}</a>;

t.deepEqual(Name.from(a, device).toArray(), [
[
Option.of(
Name.of("Hello world", [
Name.Source.descendant(
a,
Name.of("Hello world", [
Name.Source.label(img.attribute("alt").get()),
])
),
])
),
[],
],
]);
});

test(`.from() determines the name of an <a> element with a <figure> child element
with a <img> child element with an alt attribute`, (t) => {
const img = <img alt="Hello world" />;

const figure = <figure>{img}</figure>;

const a = <a href="#">{figure}</a>;

t.deepEqual(Name.from(a, device).toArray(), [
[
Option.of(
Name.of("Hello world", [
Name.Source.descendant(
a,
Name.of("Hello world", [
Name.Source.descendant(
figure,
Name.of("Hello world", [
Name.Source.label(img.attribute("alt").get()),
])
),
])
),
])
),
[],
],
]);
});

test(`.from() determines the name of an <area> element with an alt attribute`, (t) => {
const area = <area alt="Hello world" />;

Expand Down Expand Up @@ -843,7 +896,8 @@ test(`.from() correctly handles circular aria-labelledby references`, (t) => {
});

test(`.from() correctly handles chained aria-labelledby references`, (t) => {
const text = h.text("Bar");
const text1 = h.text("Bar");
const text2 = h.text("Baz");

const foo = (
<div id="foo" aria-labelledby="bar">
Expand All @@ -853,18 +907,20 @@ test(`.from() correctly handles chained aria-labelledby references`, (t) => {

const bar = (
<div id="bar" aria-labelledby="baz">
{text}
{text1}
</div>
);

const baz = <div id="baz">Baz</div>;
const baz = <div id="baz">{text2}</div>;

<div>
{foo}
{bar}
{baz}
</div>;

// From the perspective of `foo`, `bar` has a name of "Bar" as the second
// `aria-labelledby` reference isn't followed.
t.deepEqual(Name.from(foo, device).toArray(), [
[
Option.of(
Expand All @@ -874,7 +930,28 @@ test(`.from() correctly handles chained aria-labelledby references`, (t) => {
Name.of("Bar", [
Name.Source.descendant(
bar,
Name.of("Bar", [Name.Source.data(text)])
Name.of("Bar", [Name.Source.data(text1)])
),
])
),
])
),
[],
],
]);

// From the perspective of `bar`, it has a name of "Baz" as `bar` doesn't care
// about `foo` and therefore only sees a single `aria-labelledby` reference.
t.deepEqual(Name.from(bar, device).toArray(), [
[
Option.of(
Name.of("Baz", [
Name.Source.reference(
bar.attribute("aria-labelledby").get(),
Name.of("Baz", [
Name.Source.descendant(
baz,
Name.of("Baz", [Name.Source.data(text2)])
),
])
),
Expand Down