Skip to content

Commit

Permalink
Fix tree traversal of flat tree (#1547)
Browse files Browse the repository at this point in the history
* Fix tree traversal of flat tree

* Remove unused import

* Extract API

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
Jym77 and github-actions[bot] authored Jan 10, 2024
1 parent 1bb0273 commit 3b683ee
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/dirty-lizards-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siteimprove/alfa-dom": patch
---

**Fixed:** Parents of `Comment` inside a shadow tree now correctly skip over the shadow root when traversing the flat tree.
4 changes: 0 additions & 4 deletions docs/review/api/alfa-dom.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,6 @@ export class Element<N extends string = string> extends Node<"element"> implemen
// (undocumented)
static of<N extends string = string>(namespace: Option<Namespace>, prefix: Option<string>, name: N, attributes?: Iterable_2<Attribute>, children?: Iterable_2<Node>, style?: Option<Block>, box?: Option<Rectangle>, device?: Option<Device>, externalId?: string, extraData?: any): Element<N>;
// (undocumented)
parent(options?: Node.Traversal): Option<Node>;
// (undocumented)
get prefix(): Option<string>;
// (undocumented)
get qualifiedName(): string;
Expand Down Expand Up @@ -1149,8 +1147,6 @@ export class Text extends Node<"text"> implements Slotable {
// (undocumented)
static of(data: string, externalId?: string, extraData?: any): Text;
// (undocumented)
parent(options?: Node.Traversal): Option<Node>;
// (undocumented)
toJSON(): Text.JSON;
// (undocumented)
toString(): string;
Expand Down
28 changes: 28 additions & 0 deletions packages/alfa-dom/src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
Fragment,
Shadow,
Slot,
Slotable,
Text,
Type,
} from ".";
Expand Down Expand Up @@ -135,6 +136,33 @@ export abstract class Node<T extends string = string>
});
}

public parent(options: Node.Traversal = Node.Traversal.empty): Option<Node> {
const parent = this._parent as Option<Node>;

// If we traverse the flat tree, we want to jump over shadow roots.
if (options.isSet(Node.Traversal.flattened)) {
return parent.flatMap((parent) => {
if (Shadow.isShadow(parent)) {
return parent.host;
}

// Additionally, if this is a slottable light child of a shadow host, we want
// to search for where it is slotted, and return that parent instead.
if (
Element.isElement(parent) &&
parent.shadow.isSome() &&
Slotable.isSlotable(this)
) {
return this.assignedSlot().flatMap((slot) => slot.parent(options));
}

return Option.of(parent);
});
}

return parent;
}

private _path: Array<string> = [];

/**
Expand Down
20 changes: 0 additions & 20 deletions packages/alfa-dom/src/node/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,26 +161,6 @@ export class Element<N extends string = string>
return this._boxes.get(device);
}

public parent(options: Node.Traversal = Node.Traversal.empty): Option<Node> {
const parent = this._parent as Option<Node>;

if (options.isSet(Node.Traversal.flattened)) {
return parent.flatMap((parent) => {
if (Shadow.isShadow(parent)) {
return parent.host;
}

if (Element.isElement(parent) && parent.shadow.isSome()) {
return this.assignedSlot().flatMap((slot) => slot.parent(options));
}

return Option.of(parent);
});
}

return parent;
}

public children(
options: Node.Traversal = Node.Traversal.empty,
): Sequence<Node> {
Expand Down
2 changes: 2 additions & 0 deletions packages/alfa-dom/src/node/shadow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class Shadow extends Node<"shadow"> {
}

public parent(options: Node.Traversal = Node.Traversal.empty): Option<Node> {
// We only "land" on Shadow roots when traversing the composed tree.
// Notably, flattening the tree "jumps" over them.
if (options.isSet(Node.Traversal.composed)) {
return this._host;
}
Expand Down
22 changes: 0 additions & 22 deletions packages/alfa-dom/src/node/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { Option } from "@siteimprove/alfa-option";
import { Trampoline } from "@siteimprove/alfa-trampoline";

import { Node } from "../node";
import { Element } from "./element";
import { Shadow } from "./shadow";
import { Slot } from "./slot";
import { Slotable } from "./slotable";

Expand Down Expand Up @@ -31,26 +29,6 @@ export class Text extends Node<"text"> implements Slotable {
return this._data;
}

public parent(options: Node.Traversal = Node.Traversal.empty): Option<Node> {
const parent = this._parent as Option<Node>;

if (options.isSet(Node.Traversal.flattened)) {
return parent.flatMap((parent) => {
if (Shadow.isShadow(parent)) {
return parent.host;
}

if (Element.isElement(parent) && parent.shadow.isSome()) {
return this.assignedSlot().flatMap((slot) => slot.parent(options));
}

return Option.of(parent);
});
}

return parent;
}

public assignedSlot(): Option<Slot> {
return Slotable.findSlot(this);
}
Expand Down
5 changes: 2 additions & 3 deletions packages/alfa-dom/src/node/traversal/get-nodes-between.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Node } from "../..";
import { Predicate } from "@siteimprove/alfa-predicate";
import { Sequence } from "@siteimprove/alfa-sequence";
import { Node } from "../..";

import { lowestCommonAncestor } from "./lowest-common-ancestor";

Expand Down Expand Up @@ -43,7 +43,7 @@ export function getNodesBetween(
// of the closest ancestor having one.
between = first
// Closest ancestor with a next sibling.
.closest((ancestor) => ancestor.next(treeOptions).isSome())
.closest((ancestor) => ancestor.next(treeOptions).isSome(), treeOptions)
// Get that sibling.
.flatMap((node) => node.next(treeOptions))
// Skip everything until next.
Expand Down Expand Up @@ -71,7 +71,6 @@ function getNodesInclusivelyBetween(
): Sequence<Node> {
const isFrontier = or(equals(node1), equals(node2));

// Get descendants of the LCA, and skip everything before and after both nodes.
return lowestCommonAncestor(node1, node2, treeOptions)
.map((context) =>
context
Expand Down
66 changes: 66 additions & 0 deletions packages/alfa-dom/test/traversal.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { None } from "@siteimprove/alfa-option";
import { test } from "@siteimprove/alfa-test";

import { h, Comment, Node } from "../src";

const targets = [<span />, h.text("hello"), Comment.of("world")];
const shadow = h.shadow(targets);
const div = <div>{shadow}</div>;

// Start with no flag.
// For each of the three flags, duplicate existing list and add the flag to one side.
// This ends up giving the eight possible options.
const flags = [
Node.Traversal.composed,
Node.Traversal.flattened,
Node.Traversal.nested,
].reduce(
(old, cur) => old.flatMap((flag) => [flag, flag.add(cur)]),
[Node.Traversal.of(Node.Traversal.none)],
);

test(".parent() of a shadow host returns None in composed traversal, the shadow root otherwise", (t) => {
for (const traversal of flags) {
if (traversal.has(Node.Traversal.composed)) {
t.deepEqual(shadow.parent(traversal).getUnsafe(), div);
} else {
t.deepEqual(shadow.parent(traversal), None);
}
}
});

test(".parent() of the children of a shadow root returns the shadow host in flat traversal, the shadow root otherwise", (t) => {
for (const target of targets) {
for (const traversal of flags) {
if (traversal.has(Node.Traversal.flattened)) {
t.deepEqual(target.parent(traversal).getUnsafe(), div);
} else {
t.deepEqual(target.parent(traversal).getUnsafe(), shadow);
}
}
}
});

test(".parent() of a slottable child of a shadow host returns the slot's parent in flat traversal, the light parent otherwise", (t) => {
const target = <span slot="foo" />;
const shadowDiv = (
<div>
<slot name="foo"></slot>
</div>
);
const shadow = h.shadow([shadowDiv]);
const lightDiv = (
<div>
{shadow}
{target}
</div>
);

for (const traversal of flags) {
if (traversal.has(Node.Traversal.flattened)) {
t.deepEqual(target.parent(traversal).getUnsafe(), shadowDiv);
} else {
t.deepEqual(target.parent(traversal).getUnsafe(), lightDiv);
}
}
});
5 changes: 3 additions & 2 deletions packages/alfa-dom/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@
"test/node/traversal/get-nodes-between.spec.tsx",
"test/node/traversal/lowest-common-ancestor.spec.tsx",
"test/node/query/element-descendants.spec.tsx",
"test/node/query/element-id-map.spec.tsx"
"test/node/query/element-id-map.spec.tsx",
"test/traversal.spec.tsx"
],
"references": [
{ "path": "../alfa-array" },
Expand All @@ -103,7 +104,7 @@
{ "path": "../alfa-map" },
{ "path": "../alfa-option" },
{ "path": "../alfa-predicate" },
{ "path": "../alfa-rectangle"},
{ "path": "../alfa-rectangle" },
{ "path": "../alfa-refinement" },
{ "path": "../alfa-sarif" },
{ "path": "../alfa-selective" },
Expand Down

0 comments on commit 3b683ee

Please sign in to comment.