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:
<li>
containing shadowDOM children besides just the <slot>
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?
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):
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.
<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.