Recently, I encountered a problem while attempting to dynamically set the initial focus within a modal. Specifically, I faced an issue when the first item was located inside a shadow DOM.
Since this control was intended for use by various developers, I had no prior knowledge of the content it would contain. However, it was crucial for accessibility reasons to ensure that the first item received the initial focus.
To address this, the following code will identify all focusable elements within a parent node, including those within potentially nested shadow DOMs.
Find All Elements
function findAllElementsIncludingShadowDom(rootNode, selector) {
const elements = [];
function traverseElements(rootNode, selector) {
for (let el of rootNode.querySelectorAll(selector || '*')) {
if (el.shadowRoot) {
traverseElements(el.shadowRoot, selector);
} else {
elements.push(el)
}
}
}
traverseElements(rootNode, selector);
return elements;
}
With the provided HTML, the following code can be used to locate all elements within the ‘main’ div as well as within the my-component custom element and any enclosed shadow DOMs it encompasses.
<div id="main">
<div>dgsfgdf</div>
<my-component></my-component>
<span id="dsfsd"></span>
</div>
Find focusable elements
//Usage
const el = document.querySelector('#main');
const selector = '*:not(br,span,script,p,style,div)';
const result = findAllElementsIncludingShadowDom(el, selector);
//Now filter by just focusable elements
const focusSelector = `a[href],area[href],input:not([disabled]),
select:not([disabled]),textarea:not([disabled]),button:not([disabled])`;
results = results.filter(focusSelector);
Focusable and visible elements
The above returns all the focusable elements but the next step is to filter them to elements that are visible.
This includes display:none, visibility:hidden and opacity:0.
results = results.filter(i => {
const rect = i.getBoundingClientRect();
var computed = getComputedStyle(i);
//If it has no height or its computed visibility is hidden
return !(
rect.width === 0 && rect.height === 0
|| computed.visibility === 'hidden'
|| computed.opacity === '0'
);
});