question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Focus trapping fails to account for elements within shadow DOM

See original GitHub issue

We are building a web component that uses a11y-dialog—this is similar to the framework implementations of the library but for web components.

We’ve noticed that focusable elements within shadow DOM are not picked up by the logic in _maintainFocus. This is because event.target.closest is not able to see into shadow DOM.

I’m wondering if you’re open to a PR (and/or collaboration) that would upgrade this logic with shadow DOM support so this library can work properly with web components.

Thanks in advance!

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
vilchesalvescommented, May 24, 2022

I was trying to use a11y-dialog with templates and slots, but was hit by this issue. I’m happy to see it here. I ended up reimplementing a lot of it, and in the meantime I wrote something that might help you as well.

/**
 * @param {string} selector
 * @param {DocumentFragment | Element} root
 * @returns {HTMLElement[]}
 */
export const piercingSelectorAll = (selector, root) => {
  const result = [];

  // matching the root against the selector
  if (root instanceof HTMLElement && root.matches(selector)) {
    result.push(root);
  }

  // matching the root's children against the selector
  const children = /** @type {HTMLElement[]} */ (
    Array.from(root.querySelectorAll(selector))
  );
  result.push(...children);

  // matching shadowRoot's children against the selector
  for (const element of result) {
    if (element.shadowRoot) {
      result.push(...piercingSelectorAll(selector, element.shadowRoot));
    }
  }

  return result;
};

    const slots = Array.from(this.querySelectorAll("slot"));

    const assignedElements = slots.flatMap((slot) =>
      slot.assignedElements({ flatten: true })
    );

    const selectableElements = Array.from(
      new Set(
        assignedElements.flatMap((element) =>
          piercingSelectorAll(focusableSelectors.join(","), element)
        )
      )
    );

So then the $$ function could be something like:

  function $$(selector, context) {
    return selectableElements.filter(element => element.matches(selector))
  }

I hope you find it useful. 😃

1reaction
elidupuiscommented, Mar 23, 2022

Great to hear!

Admittedly, this stuff is relatively new to me as well. I’ve done some initial research and found https://github.com/salesforce/kagekiri, which provides “Shadow DOM-piercing query APIs”. At this point, I’m assuming we could pull the relevant logic out of that library or use it as a guide for implementing the specific methods we need in a11y-dialog. I think the element.closest() logic in _maintainFocus I mentioned above is only a couple of short methods but I’ve only done preliminary testing on that.

That said, I’m realizing that basically anywhere a11y-dialog currently uses querySelector (or similar APIs) likely doesn’t work with web components because it can’t pierce the shadow DOM. I’ll do a bit more digging to assess the scope of this and share my findings—hopefully within a couple of days.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Problem inside shadow dom · Issue #496 · focus-trap ...
My understanding is that the inner most element that is clicked is event.composedPath()[0] always, but target for a shadow dom target will be ......
Read more >
Managing focus in the shadow DOM | Read the Tea Leaves
To be fair to focusable , though, many other libraries in the same category (focus traps, “get all tabbable elements,” accessible dialogs, etc.) ......
Read more >
javascript - shadowElement.focus() — impossible?
I've found that it seems impossible to focus on any element in the shadow dom. we have shadow options like delegatesFocus , and...
Read more >
focus-trap
Any onDeactivate callback will not be called, and focus will not return to the element that was focused before the trap's activation. But...
Read more >
Focus inside Shadow DOM
In this demo, the first two input elements are part of a shadow root, but the last one is not — it's a...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found