Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Popover] FocusTrap in a Shadow DOM loses the focus #34980

Open
2 tasks done
dangoodman opened this issue Nov 2, 2022 · 7 comments
Open
2 tasks done

[Popover] FocusTrap in a Shadow DOM loses the focus #34980

dangoodman opened this issue Nov 2, 2022 · 7 comments
Labels
bug 🐛 Something doesn't work component: FocusTrap The React component. component: Popover The React component.

Comments

@dangoodman
Copy link

dangoodman commented Nov 2, 2022

Duplicates

  • I have searched the existing issues

Latest version

  • I have tested the latest version

Steps to reproduce 🕹

Link to live example:
https://codesandbox.io/s/wandering-framework-25fyeg?file=/demo.tsx

Steps:

  1. Open the Popover and focus the input
  2. Press Tab or Shift+Tab a few times
  3. Notice the focus has escaped the popover

Current behavior 😯

FocusTrap doesn't enforce focus to stay inside it if the component is inside a shadow dom.

Expected behavior 🤔

FocusTrap inside a shadow dom should be working the same way it does outside of a shadow dom.

Context 🔦

No response

Your environment 🌎

npx @mui/envinfo
Reproduces in the latest stable Chrome and Safari. Did not test with other browsers.

  System:
    OS: macOS 13.0
  Binaries:
    Node: 18.11.0 - /opt/homebrew/bin/node
    Yarn: Not Found
    npm: 8.19.2 - /opt/homebrew/bin/npm
  Browsers:
    Chrome: 107.0.5304.87
    Edge: Not Found
    Firefox: Not Found
    Safari: 16.1
  npmPackages:
    @emotion/react: ^11.10.5 => 11.10.5 
    @emotion/styled: ^11.10.5 => 11.10.5 
    @mui/base:  5.0.0-alpha.104 
    @mui/core-downloads-tracker:  5.10.12 
    @mui/icons-material: ^5.10.9 => 5.10.9 
    @mui/material: ^5.10.12 => 5.10.12 
    @mui/private-theming:  5.10.9 
    @mui/styled-engine:  5.10.8 
    @mui/system:  5.10.12 
    @mui/types:  7.2.0 
    @mui/utils:  5.10.9 
    @types/react: ^18.0.24 => 18.0.24 
    react: ^18.2.0 => 18.2.0 
    react-dom: ^18.2.0 => 18.2.0 
    typescript: ^4.8.4 => 4.8.4 

@dangoodman dangoodman added the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Nov 2, 2022
@zannager zannager added the component: FocusTrap The React component. label Nov 2, 2022
@hbjORbj hbjORbj added the component: Popover The React component. label Nov 3, 2022
@hbjORbj hbjORbj changed the title FocusTrap in a Shadom DOM loses the focus [Popover] FocusTrap in a Shadom DOM loses the focus Nov 3, 2022
@michaldudak
Copy link
Member

Thanks for reporting this. The bug stems from the FocusTrap not being aware it's inside shadow DOM. It uses document.activeElement, which is being set to the shadow root, not the element that actually has focus within the shadow DOM.

The fix would likely involve replacing document.activeElement by node.getRootNode().activeElement. Would you like to work on a PR?

@michaldudak michaldudak added bug 🐛 Something doesn't work and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Nov 4, 2022
@michaldudak michaldudak removed their assignment Nov 4, 2022
@dangoodman
Copy link
Author

I had a patch utilizing node.getRootNode().activeElement for the 4.x version of FocusTrap. It worked then. So, I have tried to patch the 5.x version similarly but have had no luck. The new code seems a bit trickier. I'm not sure if I have enough understanding about the browser-specific aspects and the logic in general.

@ZeeshanTamboli ZeeshanTamboli changed the title [Popover] FocusTrap in a Shadom DOM loses the focus [Popover] FocusTrap in a Shadow DOM loses the focus Nov 7, 2022
@m4theushw
Copy link
Member

Recently we had a report that the date range picker was not working with the Shadow DOM. The document.activeElement was also the problem. We were able to replace document.activeElement by an utility function that searches the correct element inside the shadow tree. Maybe the solution from mui/mui-x#6952 can be used here.

@arminbashizade
Copy link

I've been trying to fix this in #35316 I've added the utility method that @m4theushw suggested, and that's working for Dialog component, but not all FocusTrap cases, going through the unit tests for FocusTrap I noticed that "focusin" event does not get triggered with every Tab inside Shadow DOM, I've been looking at custom events that address that issue, for example this one: https://gist.github.com/samthor/7b307408e73784971ef0fcf4a8af6edd

@CWSites
Copy link

CWSites commented Sep 12, 2023

Any update here? Seems like no movement since January.

@arminbashizade
Copy link

arminbashizade commented Sep 12, 2023

@CWSites I wasn't able to make my PR #35316 work, and I'm not actively working on it, but here's my workaround:
I used focus-trap-react which has an option to pass the root for where the focus should be trapped. This solution requires creating a React context and storing the Shadow container in it, which then can be retrieved in AccessibleDialog to be passed to FocusTrap.

import Dialog, { DialogProps } from '@mui/material/Dialog';
import FocusTrap from 'focus-trap-react';
import { useContext, useEffect } from 'react';
import { RootContext } from '../helpers/root-context';

export const AccessibleDialog = (props: DialogProps) => {
  const root = useContext(RootContext) as Document;
  const { disablePortal, ...otherProps } = props; // ignore disablePortal
  useEffect(() => {
    // disable tabbing for MUI's sentinel divs
    const muiSentinels = [
      root.querySelector('[data-testid=sentinelStart]'),
      root.querySelector('[data-testid=sentinelEnd]'),
    ];

    for (const comp of muiSentinels) {
      if (comp && comp.attributes && comp.attributes.getNamedItem('tabindex')) {
        comp.setAttribute('tabindex', '-1');
      }
    }
  }, [root]);

  return (
    <FocusTrap
      focusTrapOptions={{
        document: root,
      }}
    >
      <Dialog {...otherProps} disablePortal />
    </FocusTrap>
  );
};

RootContext is just a simple context:

import { createContext } from 'react';

export const RootContext = createContext<DocumentOrShadowRoot>(document);

and when creating your Shadow DOM:

//...
const shadowContainer = someElement.attachShadow({ mode: 'open' });
const shadowRootElement = document.createElement('div');
shadowContainer.appendChild(shadowRootElement);

const reactRoot = createRoot(shadowRootElement);
reactRoot.render(
  <RootContext.Provider value={shadowContainer}>
    <SomeReactComponentThatUsesAccessibleDialog />
  </RootContext.Provider>
);

(see this page from MUI docs for more info on using Shadow DOM: https://mui.com/material-ui/guides/shadow-dom/)

@CWSites
Copy link

CWSites commented Sep 14, 2023

@arminbashizade thank you for the details on how you got around this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐛 Something doesn't work component: FocusTrap The React component. component: Popover The React component.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants