When using custom elements with a shadowRoot, the focusVisibleClassName is applied to the outer-most custom element, instead of the actual element that is focused within the shadow DOM.
focusVisibleClassName should be added to deepest-level activeElement when a shadowRoot is present.
focusVisibleClassName is applied to the outer-most custom element that is returned from document.activeElement.
Traverse activeElement targets that have a shadowRoot to find the lowest level active element for a given component; or just return document.activeElement if no shadowRoot is present.
Following solution appears to work as expected:
diff --git a/packages/material-ui/src/ButtonBase/focusVisible.js b/packages/material-ui/src/ButtonBase/focusVisible.js
index d10c1a00d..462376f72 100644
--- a/packages/material-ui/src/ButtonBase/focusVisible.js
+++ b/packages/material-ui/src/ButtonBase/focusVisible.js
@@ -16,10 +16,11 @@ export function detectFocusVisible(instance, element, callback, attempt = 1) {
instance.focusVisibleTimeout = setTimeout(() => {
const doc = ownerDocument(element);
+ const activeElement = findActiveElement(doc);
if (
internal.focusKeyPressed &&
- (doc.activeElement === element || element.contains(doc.activeElement))
+ (activeElement === element || element.contains(activeElement))
) {
callback();
} else if (attempt < instance.focusVisibleMaxCheckTimes) {
@@ -28,6 +29,18 @@ export function detectFocusVisible(instance, element, callback, attempt = 1) {
}, instance.focusVisibleCheckTime);
}
+function findActiveElement(doc) {
+ let activeElement = doc.activeElement;
+ while (
+ activeElement &&
+ activeElement.shadowRoot &&
+ activeElement.shadowRoot.activeElement
+ ) {
+ activeElement = activeElement.shadowRoot.activeElement;
+ }
+ return activeElement;
+}
+
const FOCUS_KEYS = ['tab', 'enter', 'space', 'esc', 'up', 'down', 'left', 'right'];
function isFocusKey(event) {
Does :focus-visible also apply to elements within a shadow root?
Does
:focus-visiblealso apply to elements within a shadow root?
This is potentially handled by delegatesFocus? http://w3c.github.io/webcomponents/spec/shadow/#widl-ShadowRootInit-delegatesFocus
Although, a similar issue to this one was raised on the WICG/focus-visible polyfill @ https://github.com/WICG/focus-visible/issues/28
Do you have a reproduction example? What the use case?
Sure. https://codesandbox.io/s/8lxr89x2p2
You can see that tabbing only applies focus to the elements not inside a shadow root.
With the above changes to the activeElement code, it works as expected.
@jaipe Well done with the JSS setup for shadow DOM. In the documentation, I wish we had a demo with an iframe, a popup, and a shadow dom.
The fix looks good enough to me. There is a bundle size overhead concern but the fact it's usable within a shadow DOM is a convincing argument. @eps1lon What do you think?
I wasn't very convinced at first but native buttons do gain a visible focus state within a shadow root so I'd say we improve the polyfill.
Can I take this one??
Actually already got something working locally. I can just tidy it up and add some tests.
@jaipe I'm using your codesandbox example example to render an the AppBar component
However, I cannot get the click events to work properly. Have you found any solution to this?
@jaipe I'm using your codesandbox example example to render an the AppBar component
However, I cannot get the click events to work properly. Have you found any solution to this?
Can you send an updated code sandbox example?
Hi, here is a fork of your project with the aforementioned component included. In the demo you can see the click's won't work. Not sure how to fix them. I read about it a little (shadow root blocks outer events) but not sure how to fix it.
Hey. You'll probably need something like this, so the events re-target and React knows about them. React listens at the document level for events (yet events don't bubble out of the shadow root by default).
Most helpful comment
Actually already got something working locally. I can just tidy it up and add some tests.