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

Shadow inputs in Form-Associated Custom Elements should be allowed to be hidden from AOM #196

Open
bennypowers opened this issue Feb 27, 2023 · 0 comments

Comments

@bennypowers
Copy link

bennypowers commented Feb 27, 2023

In this demo, a form-associated custom element provides a range slider. The FACE' shadow root contains a private input. The developer intends for the shadow input to be transparent to assistive technology, since the FACE exposes it's state to AT via ElementInternals.

We expect AT to report a single interactive control here, but instead find that the Chrome dev tools accessibility panel lists two nested controls. AXE and Lighthouse audits also flag the shadow input as either being unlabeled, or having an illegal aria-hidden attr.

The developer's intent here is to use the native input functionality without reimplementing everything, while making the custom element accessible via ElementInternals. Given this bug, the developer will either have to reimplement the element without using <input> in shadow root (contenteditable, <canvas>, animated SVG, etc), or will have to apply one of several workarounds which are emerging, like moving aria-attributes from light to shadow DOM. All of which seem to run counter to the "intent" of the ElementInternals APIs.

Screenshot from 2023-02-27 09-41-28

Screenshot from 2023-02-27 09-41-21

Please see the reproduction of this bug on my website

Source
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>AOM Repro</title>
  </head>
  <body>
    <header>
      <h1>Shadow Inputs should be allowed to be aria-hidden</h1>
    </header>

    <main>
      <p>In this demo, a form-associated custom element provides a range slider.
        The <abbr title="form-associated custom element">FACE</abbr>' shadow root 
        contains a private input. The private input is meant to be transparent to
        assistive technology, since the FACE exposes it's state to
        <abbr title="assistive technology">AT</abbr> via <code>ElementInternals</code>.</p>
      <p>We expect AT to report a single interactive control here, but instead find that the 
        Chrome dev tools accessibility panel lists two nested controls.</p>

      <form>
        <fieldset>
          <legend>Using <code>aria-hidden="true"</code></legend>
          <label>Range <x-range template-type="aria-hidden"></x-range> </label>
        </fieldset>
        <fieldset>
          <legend>Using <code>role="presentation"</code></legend>
          <label>Range <x-range template-type="presentation"></x-range> </label>
        </fieldset>
      </form>
    </main>

    <template id="aria-hidden">
      <input type="range" aria-hidden="true" min="0" max="100" step="1">
    </template>

    <template id="presentation">
      <input type="range" role="presentation" min="0" max="100" step="1">
    </template>

    <script>
      customElements.define('x-range', class extends HTMLElement {
        static formAssociated = true;

        #internals = this.attachInternals();

        #input;

        constructor() {
          super();
          this.#internals.role = 'slider';
          const type = this.getAttribute('template-type');
          this.attachShadow({ mode: 'open' })
            .append(document.getElementById(type)
              .content.cloneNode(true));
          this.#input = this.shadowRoot.querySelector('input');
          this.#input.addEventListener('change', e => this.#onChange(e))
          if (this.hasAttribute('value')) this.#input.value = this.getAttribute('value');
          this.#update();
        }

        #onChange(event) {
          event.stopPropagation();
          this.#update()
        }

        #update(value = this.#input.value) {
          this.#input.value = value;
          this.#input.step = this.getAttribute('step') ?? '1'
          this.#internals.ariaValueMin = this.getAttribute('min') ?? '0';
          this.#internals.ariaValueMax = this.getAttribute('max') ?? '100';
          this.#internals.ariaValueNow = this.#input.value;
        }
      });
    </script>
  </body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant