Repository: Siteimprove/alfa (ts)

Issue:
`isVisible` reports content of closed `<details>`  as visible
The [isVisible](https://github.com/Siteimprove/alfa/blob/ee0c470dab051834d0024ba3868b9c7806a24193/packages/alfa-style/src/node/predicate/is-visible.ts#L29) predicate does not detect content inside a closed `<details>` element as invisible.

Minimal reproducible example:
```
<!doctype html>
<html>
  <head>
    <title>Details Visibility MRE</title>
  </head>
  <body>
    <details>
      <summary>Minimal Reproducible Example</summary>
      <div>I'm invisible</div>
    </details>
  </body>
</html>
```

Comparing with browser dev tools, the `<div>` element has computed styles `visibility: visible` and `display: block` so it's unclear what exactly is the mechanism that makes the element not visible/not rendered.

`<details>` is not added to [user-agent.ts](https://github.com/Siteimprove/alfa/blob/ee0c470dab051834d0024ba3868b9c7806a24193/packages/alfa-cascade/src/user-agent.ts#L4) according to [rendering.html](https://html.spec.whatwg.org/multipage/rendering.html#the-details-and-summary-elements), but it's unclear if adding that will be enough to correctly detect the visibility of the content.

Do we need to add special handling of `<details>` elements in the predicate?

Relates to [II-8780](https://siteimprove-wgs.atlassian.net/browse/II-8780).

[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details).

[II-8780]: https://siteimprove-wgs.atlassian.net/browse/II-8780?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Pull request description:
Update user agent sheet for `<details>` to fix visibility of content
Resolves #1776 

This solution doesn't actually follow the standard, unfortunately, in stead it tries to approximate the observed behavior seen in the browser.

As far as I could find, it's going to be difficult to implement it correctly with what we currently have as it involves a style rule `content-visibility: hidden` of pseudo element `::details-content`.

Can we allow this discrepancy or will it bite us?


Patch:
diff --git a/.changeset/tall-wolves-allow.md b/.changeset/tall-wolves-allow.md
new file mode 100644
index 000000000..7e45a5cd7
--- /dev/null
+++ b/.changeset/tall-wolves-allow.md
@@ -0,0 +1,7 @@
+---
+"@siteimprove/alfa-cascade": patch
+---
+
+**Fixed:** The user agent sheet now includes `details` and `summary`.
+
+This fixes the visibility behavior of the content after the `<summary>` element, when the `<details>` is closed.
diff --git a/packages/alfa-cascade/src/user-agent.ts b/packages/alfa-cascade/src/user-agent.ts
index 2c2863a61..c78be11fd 100644
--- a/packages/alfa-cascade/src/user-agent.ts
+++ b/packages/alfa-cascade/src/user-agent.ts
@@ -234,4 +234,26 @@ export const UserAgent = h.sheet([
   h.rule.style("fieldset", {
     display: "block",
   }),
+
+  /**
+   * {@link https://html.spec.whatwg.org/multipage/rendering.html#the-details-and-summary-elements}
+   */
+
+  h.rule.style("details, summary", { display: "block" }),
+  h.rule.style("details > summary:first-of-type", {
+    display: "list-item",
+    counterIncrement: "list-item 0",
+    listStyle: "disclosure-closed inside",
+  }),
+  h.rule.style("details[open] > summary:first-of-type", {
+    listStyleType: "disclosure-open",
+  }),
+
+  // This is not totally accurate. Browsers hide <details> content using `content-visibility: hidden`
+  // on an internal shadow pseudo-element `::details-content`. We approximate this behavior by
+  // directly applying `display: none` to the actual DOM content. This ensures proper visibility
+  // detection while acknowledging it's a simplified modeling choice.
+  h.rule.style("details:not([open]) > :not(summary:first-of-type)", {
+    display: "none",
+  }),
 ]);


Test patch:
diff --git a/packages/alfa-cascade/test/cascade.spec.tsx b/packages/alfa-cascade/test/cascade.spec.tsx
index dc1f36caf..999779bc3 100644
--- a/packages/alfa-cascade/test/cascade.spec.tsx
+++ b/packages/alfa-cascade/test/cascade.spec.tsx
@@ -7,7 +7,7 @@ import { Cascade } from "../dist/index.js";
 import { Block } from "../dist/block.js";
 import { Layer, Origin } from "../dist/index.js";
 
-import { layer, getBlock, authorOrder } from "./common/common.js";
+import { authorOrder, getBlock, layer } from "./common/common.js";
 
 const device = Device.standard();
 
@@ -17,8 +17,8 @@ test(".from() builds a cascade with the User Agent style sheet", (t) => {
 
   // Even the "empty" cascade is pretty big due to the UA sheet.
   // We only superficially check the selector map
-  t.deepEqual(cascade.toJSON().selectors.types.length, 96);
-  t.deepEqual(cascade.toJSON().selectors.other.length, 8);
+  t.deepEqual(cascade.toJSON().selectors.types.length, 98);
+  t.deepEqual(cascade.toJSON().selectors.other.length, 9);
 
   t.deepEqual(cascade.toJSON().selectors.ids.length, 0);
   t.deepEqual(cascade.toJSON().selectors.classes.length, 0);
diff --git a/packages/alfa-style/test/node/predicate/is-visible.spec.tsx b/packages/alfa-style/test/node/predicate/is-visible.spec.tsx
index 45d0f8026..cb04328e9 100644
--- a/packages/alfa-style/test/node/predicate/is-visible.spec.tsx
+++ b/packages/alfa-style/test/node/predicate/is-visible.spec.tsx
@@ -363,3 +363,59 @@ test("isVisible() returns true for single line `<select>`", (t) => {
   // The `<select>` is nonetheless visible, showing the value of the first `<option>`.
   t(isVisible(select));
 });
+
+test("isVisible() returns true for content of open `<details>`", (t) => {
+  const content = <div>World</div>;
+  const details = (
+    <details open>
+      <summary>Hello</summary>
+      {content}
+    </details>
+  );
+
+  h.document([details]);
+
+  t(isVisible(content));
+});
+
+test("isVisible() returns false for content of closed `<details>`", (t) => {
+  const content = <div>World</div>;
+  const details = (
+    <details>
+      <summary>Hello</summary>
+      {content}
+    </details>
+  );
+
+  h.document([details]);
+
+  t(!isVisible(content));
+});
+
+test("isVisible() returns true for summary of open `<details>`", (t) => {
+  const summary = <summary>Hello</summary>;
+  const details = (
+    <details open>
+      {summary}
+      <div>World</div>
+    </details>
+  );
+
+  h.document([details]);
+
+  t(isVisible(summary));
+});
+
+test("isVisible() returns true for summary of closed `<details>`", (t) => {
+  const summary = <summary>Hello</summary>;
+  const details = (
+    <details>
+      {summary}
+      <div>World</div>
+    </details>
+  );
+
+  h.document([details]);
+
+  t(isVisible(summary));
+});