Search code examples
javascripthtmlaccessibilityreact-typescriptwcag

How to achieve HTML accessibility for content inside shadow-root in a React web app?


I have one main react webapp which takes content from another and has it injected. The content of an injected component is whole inside a shadow-root. In short it looks like:

<main-app>
...content
   <web-component>
   #shadow-root(open)
      <div>
      ...content
      </div>
   </web-component>
</main-app>

I gotta make content from injected component accessible from the main app. Aria attributes don't work. Writing custom HTMLElements is also not an option, as I gotta stick to some strict rules. Both - main app and injected component are written in react. Is there a common way, to make content inside shadow-root accessible?

I've tried appending children in the component I'm injecting, but obviously it worked outisde shadow-root and inside it did not. I have also tried putting outsie shadow-root and connecting it with desired element via ariaLabeledBy, but it also did not work.

The web component code that I'm injecting looks like:

import { Button } from "@mui/material";
import ReactDOM from "react-dom";

export class WebComponent extends HTMLElement {
    connectedCallback() {
        const appContainer = document.createElement("div");
        const mountPoint = document.createElement("div");
        mountPoint.appendChild(appContainer);
        this.attachShadow({ mode: "open" }).appendChild(mountPoint);

        ReactDOM.render(
            <MainApp/>, appContainer
        )
    }
}

const MainApp = () => {
    // Aria-labels will be skipped by screen readers.
    // Text content also will be skipped by screen readers.
    return (
        <Button aria-label="TEST">TestBtn</Button>
    )
}

customElements.define("web-component", WebComponent);

The component is being rendered by calling connectedCallback() method in my main web app. Unfortunately, it's not possible for me to paste the actual code, but I hope it can show the context of the problem.


Solution

  • I've managed to find a solution.

    For the browser's built-in content reader, the only working solution was to dynamically add label elements just outside shadow-root content using hooks. It was not visible, but readers were available to read the content aloud.

    For screen readers like NVDA buttons, inputs etc worked without any workaround. The only problem was that text content like headers and paragraphs was not read immediately when the page loaded. It wasn't a bug, nor it was related to shadow-root, but in my case, I needed it to be read right after the page loaded. Adding the role="alert" attribute solved it in my specific case.

    Hope, that someone finds it useful.