material-ui
material-ui copied to clipboard
In shadow DOM, keyboard not working for Select menu
Duplicates
- [X] I have searched the existing issues
Latest version
- [X] I have tested the latest version
Current behavior π―
When Select menu (and its popover) is displayed within a shadow DOM, arrow keys have no effect.
Expected behavior π€
Arrow keys should select menu items.
(I'm not sure to what extent shadow DOM is supported, but it seems to be mostly working otherwise.)
Steps to reproduce πΉ
Steps:
- Load https://codesandbox.io/s/hopeful-noether-88fukr
- This renders a React root into a shadow DOM, and also puts the Emotion styles there using an Emotion CacheProvider.
- Inside it is a Select menu with disablePortal=true (so it can use the styles).
- Click the Select menu so its popover appears.
- Press one of the arrow keys. The selection doesn't change.
Context π¦
Rendering a MUI app entirely within a web component's shadow DOM.
Your environment π
CodeSandbox
We haven't been testing the components in such context. It's not a popular use case after all. If you'd like to investigate the problem, I'll be happy to assist you with a PR.
@michaldudak I wouldn't say it's not a popular use case. Being able to use a MUI with a React app that can be wrapped with a web component using shadowDom will give you more potential clients with big finances who want to move from JSF/JSP to ReactJS and who is willing to pay for the pro version.
There are currently 2 ways to use a React app with JSF/JSP.
- in the *.xhtml file, include the iframe tag and pass a link to index.html created with react-scripts or vite. As a result we have an isolated UI part and we can even mix ReactJS/VueJS/Angular on the same page for example to understand what is best for us, but the downside of this is that I can't pass some attribute values ββduring rendering by java on the server. Also, there is no easy way to communicate between JSF and my React app, plus we lose the concept of SAP.
- second: define a custom html element that wraps the React app with a shadowDom to avoid css mixing. in this case, we can observe the attribute changes and pass them to the React app. Look at this simples example:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
const webComponentName = 'my-superpuper-element'
export class MySuperpuperElement extends HTMLElement {
connectedCallback() {
const shadowElement = this.attachShadow({mode: 'closed'})
const styleElement = document.getElementById(`${webComponentName}-style`)
if (styleElement instanceof HTMLElement) {
shadowElement.appendChild(styleElement)
}
const reactRoot = ReactDOM.createRoot(shadowElement);
reactRoot.render(React.createElement(App, {}));
}
}
if (!customElements.get(webComponentName)) {
customElements.define(webComponentName, MySuperpuperElement);
}
And it would be fit my needs perfectly if MUI provided a clear picture how and when it adds its styles. In my case, with Vite and the plugin 'vite-plugin-css-injected-by-js' all my css are bundled together with one js file and then when I define my custom element I can move
Hence I'd like to ask you pleas may you give an idea how to do this. Or maybe we can request a future that would add a script that can create a custom element of react app that uses MUI (something like this react-to-webcomponent)
PS. I bet this will allow some managers to be more courageous in their decision to migrate their old banking interface to a more modern one that uses React rather than JSP/JSF. Because in this way it would not be necessary to migrate everything at once.
Any suggestions on a workaround? This is still an issue. @yk-jemmic Did you figure a solution?
@tristanjasper I had a slightly different case, but yes, it's still relevant with an example in the topic as well as on their Demo (but it works partially) so keyboard navigation does not work correctly
I've managed to get this to work with a custom onKeyDown handler:
<Select
ref={selectRef}
MenuProps={{
PaperProps: {
onKeyDown: ({ key }: React.KeyboardEvent<HTMLDivElement>) => {
const validKeys = ["ArrowUp", "ArrowDown"];
let newIndex = 0;
if (validKeys.includes(key) && selectRef.current) {
const selectedItem =
(selectRef.current.getElementsByClassName("Mui-focusVisible")[0] ||
selectRef.current.getElementsByClassName("Mui-selected")[0]) as HTMLElement;
const menuList = Array.from(
selectRef.current.querySelectorAll("ul[role='listbox'] li") || [],
);
if (selectedItem && selectedItem.parentNode) {
const selectedIndex = menuList.indexOf(selectedItem);
if (key === "ArrowDown") {
newIndex = selectedIndex + 1 < menuList.length ? selectedIndex + 1 : selectedIndex;
} else if (key === "ArrowUp") {
newIndex = selectedIndex - 1 >= 0 ? selectedIndex - 1 : selectedIndex;
}
}
if (menuList.length > newIndex) {
const newSelectedItem = menuList[newIndex] as HTMLElement;
newSelectedItem.focus();
}
}
},
},
}}
>
Based on preliminary testing, the root cause of the problem is here: https://github.com/mui/material-ui/blob/e0d43b4d7eaedb31422d44873ee8dd354a0257cc/packages/mui-material/src/MenuList/MenuList.js#L164
On mui-x we have also experienced this problem and are using the following utility to resolve the activeElement:
https://github.com/search?q=repo%3Amui%2Fmui-x+%22export+const+getActiveElement%22&type=code
Applying similar changes seems to fix the problem.
Is anyone more familiar with the code able to fix this problem?
It could be a great idea to check all cases, where the active element is resolved and use the shadowRoot compliant method in such cases. π‘