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:

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:

    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.

    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.