Skip to content

Commit

Permalink
Implement step 2H of the name computation (#430)
Browse files Browse the repository at this point in the history
  • Loading branch information
kasperisager authored Oct 6, 2020
1 parent 82d73a7 commit 7dcb689
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 12 deletions.
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

0 comments on commit 7dcb689

Please sign in to comment.