Search code examples
accessibilityweb-component

Can you have an `<li>` in a autonomous custom element with the parent `<ul>` not in the same ShadowDOM?


My understanding used to be that a list item <li> tag had to be the direct child of a <ul>, <ol> or <menu> tag.

But I've been using web components and autonomous custom elements recently. And I've read that for autonomous custom elements, the content model is transparent. See https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-core-concepts

I want to have an autonomous custom element <my-list> which has a <ul> in its shadowDOM (or one of the other valid parents, such as ol and menu), and a second autonomous custom element <my-list-item> with an <li> in its shadowDOM. (See "the example" below.)

Why? A few reasons:

  • css styling scoped to the shadowDOM
  • additional event handlers
  • some use cases might have more going on, like each <li> containing shadowDOM children besides just the <slot>
  • I'm using Lit 3, so its natural to write a web component

Is something like this example below valid or invalid, according to the HTML 5 spec, and according to accessibility guidelines?

Note: I'm using the <template shadowrootmode="open"> (see here https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template#shadowrootmode ) to make my example more self contained. In my real code, I'm using the Lit framework, which creates the shadowDOM programmatically.

The example:

<my-list role="presentation">
  <template shadowrootmode="open">
    <ul>
      <slot />
    </ul>
  </template>

  <my-list-item role="presentation">
    <template shadowrootmode="open">
      <li>
        <slot />
      </li>
    </template>

    My text here
  </my-list-item>
</my-list>

Another reasons I think this might be valid is that I see that axe-core has a unit test that looks similar:

https://github.com/dequelabs/axe-core/blob/develop/test/checks/lists/only-listitems.js#L224-L232

    it('should return false in a shadow DOM pass', () => {
      const node = document.createElement('div');
      node.innerHTML = '<li>My list item </li>';
      const shadow = node.attachShadow({ mode: 'open' });
      shadow.innerHTML = '<ul><slot></slot></ul>';

      const checkArgs = checkSetup(node, 'ul');
      assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
    });

But it looks like that is a specific mode / option in axe-core and not the default behavior?


Solution

  • In theory, whatever you have said makes sense. It means following things are true:

    • Custom elements have transparent content model as per specification.
    • As per specification, when transparent elements are nested in each other, the process has to be applied iteratively.

    So, it means that you should be allowed to create completely custom lists. So, this should work:

    <script>
      class MyList extends HTMLElement {
        constructor() {
          super()
            .attachShadow({mode: 'open'})
            .innerHTML = `
            <ul aria-label="Custom List">
              <slot></slot>
            </ul>`;
        }
      }
      class MyListItem extends HTMLElement {
        constructor() {
          super()
            .attachShadow({mode: 'open'})
            .innerHTML = `<li><slot></slot></li>`;
        }
        connectedCallback(){
          console.log(this.parentNode.nodeName);
        }
      }
      customElements.define('my-list', MyList);
      customElements.define('my-list-item', MyListItem);
    </script>
    
    <my-list aria-label="Custom Wrapper">
      <my-list-item>Item 1</my-list-item>
      <my-list-item>Item 2</my-list-item>
    </my-list>

    In this case, the <ul> lives a different shadow root than those of <li> elements. For this above definition, the browser ends up generating the following accessibility tree (from the Chrome browser):

    Accessibility Tree

    So, as per specs, the browser is properly generating the accessibility tree which is was most of the tools should see. My best guess is that axe-core is relying on the same specs for this given test. But there in practice, things are different.

    • Many assistive have custom parser which break and/or make assumption about the HTML structure (ShadowRoot is a common hindrance)
    • Even the W3C validator doesn't recognize the custom elements.
    • Although, the custom elements spec officially consider custom elements with transparent content model, the specs for <ul> and <ol> still explicitly restrict them to have only <li>, <script> and <template>. (This should be addressed in future).

    So, as per spec, you are good to have custom list elements within your custom list in a completely different Shadow DOM, however in practice, the accessibility will still take a hit. If possible, better to rely on classical ul > li or ol > li hierarchy.