material-ui icon indicating copy to clipboard operation
material-ui copied to clipboard

Select and Menu components freeze inside custom HTMLElement

Open PatriciaRomaniuc opened this issue 3 months ago • 5 comments

Steps to reproduce

minimal code example:

import React, { createRef } from 'react';
import { createRoot } from 'react-dom/client';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';

class MyCustomElement extends HTMLElement {
  connectedCallback() {
    this._root = createRoot(this);
    this._root.render(<MySelect />);
  }
  disconnectedCallback() {
    this._root.unmount();
  }
}

const MySelect = () => {
  const [value, setValue] = React.useState('');
  const ref = createRef();

  return (
    <Select
      value={value}
      onChange={(e) => setValue(e.target.value)}
      inputRef={ref}
    >
      <MenuItem value="one">One</MenuItem>
      <MenuItem value="two">Two</MenuItem>
    </Select>
  );
};

customElements.define('my-custom-element', MyCustomElement);

Current behavior

When using MUI Select inside a custom HTMLElement with React createRoot(this) opening the Select causes the page to freeze and throws:

useTimeout.js:25 Uncaught TypeError: fn is not a function

This appears to be related to MUI Select relying on Popover + Grow + internal refs, which break when the component is rendered directly inside a custom element.

using transitionDuration={0} into MenuProps fixes the issue, but it's not ideal

Expected behavior

The Select dropdown should open without freezing, similar to how Popover works in a stable container.

Context

No response

Your environment

npx @mui/envinfo

System: OS: macOS 14.6.1 Binaries: Node: 20.8.0 - /Users/patriciaromaniuc/.nvm/versions/node/v20.8.0/bin/node npm: 10.1.0 - /Users/patriciaromaniuc/.nvm/versions/node/v20.8.0/bin/npm pnpm: 8.9.0 - /Users/patriciaromaniuc/.nvm/versions/node/v20.8.0/bin/pnpm Browsers: Chrome: 142.0.7444.60 Edge: Not Found Firefox: 137.0 Safari: 18.3.1 npmPackages: @emotion/react: 11.14.0 @emotion/styled: 11.14.1 @mui/core-downloads-tracker: 7.3.4 @mui/icons-material: 7.3.4 @mui/material: 7.3.4 @mui/types: 7.4.7 @types/react: 19.2.2 react: 18.2.0 => 18.2.0 react-dom: 18.2.0 => 18.2.0 styled-components: 5.3.11 typescript: 3.9.10

Search keywords: useTimeout, Select, Menu, Grow

PatriciaRomaniuc avatar Nov 06 '25 14:11 PatriciaRomaniuc

Please provide a live reproduction in the form of Stackblitz or CodeSandbox. Here's a good starting template: https://stackblitz.com/github/mui/material-ui/tree/master/examples/material-ui-vite-ts?file=src%2FApp.tsx

ZeeshanTamboli avatar Nov 08 '25 05:11 ZeeshanTamboli

@ZeeshanTamboli can I work on this issue ?

rithik56 avatar Nov 08 '25 11:11 rithik56

I tried to make an example here https://stackblitz.com/edit/github-gshwyya8?file=src%2FApp.jsx but something is off with the custom elements declaration in this type of environment. @rithik56 if you want to help, that would be great.

PatriciaRomaniuc avatar Nov 10 '25 08:11 PatriciaRomaniuc

I did add a workaround to this issue and but it's not great since the focus is also an issue and I am basically trying to make it look like MUI transition without it

import React from 'react';
import { Select } from '@mui/material';

export const WebComponentSafeTransition = React.forwardRef(({ children, in: inProp }, ref) => {
    const [mounted, setMounted] = React.useState(false);

    React.useEffect(() => {
        if (inProp) {
            // Use RAF to ensure the element is mounted before applying the visible class
            requestAnimationFrame(() => {
                requestAnimationFrame(() => {
                    setMounted(true);
                });
            });
        } else {
            setMounted(false);
        }
    }, [inProp]);

    if (!inProp && !mounted) {
        return null;
    }

    const style = {
        opacity: mounted ? 1 : 0,
        transform: mounted ? 'scaleY(1) translateY(0)' : 'scaleY(0.3) translateY(-10px)',
        transition: 'opacity 350ms cubic-bezier(0.4, 0, 0.2, 1), transform 350ms cubic-bezier(0.2, 1, 0.2, 1)',
        transformOrigin: 'top center',
    };

    return (
        <div ref={ref} style={style}>
            {children}
        </div>
    );
});

/**
 * Web Component-safe Select wrapper
 *
 * Fixes MUI Select crash when used inside custom HTMLElements with createRoot(this)
 *
 * Required fixes:
 * - disablePortal: true - Keeps menu in Web Component's React tree
 * - TransitionComponent: Custom - Bypasses Grow transition's useTimeout
 *
 * Features:
 * - Dropdown-style animation (scaleY + translateY)
 * - Menu may be clipped by parent overflow: hidden
 * - Menu won't break out of scrolling containers
 *
 * @param {object} props - All standard MUI Select props
 */
export const WebComponentSafeSelect = ({ MenuProps = {}, ...props }) => {
    const ref = React.useRef(null);

    // implement onClose and onOpen to manage focused class based on workaround for MUI bug found in stackoverflow
    const onClose = () => {
        ref.current?.previousSibling?.classList.remove('Mui-focused');
        ref.current?.classList.remove('Mui-focused');

        if (props.onClose) {
            props.onClose();
        }
    };

    const onOpen = () => {
        ref.current?.previousSibling?.classList.add('Mui-focused');
        ref.current?.classList.add('Mui-focused');

        if (props.onOpen) {
            props.onOpen();
        }
    };

    return (
        <Select
            {...props}
            ref={ref}
            autoFocus={false}
            onOpen={onOpen}
            onClose={onClose}
            MenuProps={{
                ...MenuProps,
                disablePortal: true,
                TransitionComponent: WebComponentSafeTransition,
                disableAutoFocusItem: true,
                PaperProps: {
                    style: {
                        maxHeight: '300px',
                        marginTop: '4px',
                        ...MenuProps.PaperProps?.style,
                    },
                    ...MenuProps.PaperProps,
                },
            }}
        />
    );
};

export default WebComponentSafeSelect;

PatriciaRomaniuc avatar Nov 10 '25 08:11 PatriciaRomaniuc

We can't help without a clear reproduction. You can also provide a minimal example in the form of a GitHub repository with steps to reproduce the issue.

ZeeshanTamboli avatar Nov 10 '25 13:11 ZeeshanTamboli

Update: found a solution and the components now work by adding transitionDuration={{ enter: 225, exit: 195 }} . It seems like the issue is that the defaults for the react-transition-group ar missing in this type of setup with custom HTMLElement (vs normal behavior of simple react components) Hope this helps anyone that is facing this issue.

PatriciaRomaniuc avatar Nov 11 '25 10:11 PatriciaRomaniuc

@PatriciaRomaniuc Great, will close this issue then.

ZeeshanTamboli avatar Nov 11 '25 11:11 ZeeshanTamboli

This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue. Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.

github-actions[bot] avatar Nov 11 '25 11:11 github-actions[bot]