简体   繁体   中英

React.js + MUI: Modal closes when clicking on Select component

for MUI learning purposes I'm creating a simple CRUD app with a modal. That modal contains a simple form with a few TextField and one Select components. THe issue is, that when clicking on the Select component, the modal closes.

Modal:

<ClickAwayListener
    onClickAway={handleClickAway}
  >
    <Box sx={{ marginTop: '80px' }}>
      <Button
        sx={{
          borderRadius: '8px',
          backgroundColor: '#fff',
          color: '#091fbb',
          border: '1px solid #091fbb'
        }}
        onClick={handleOpen}
      >
        Add new
      </Button>

      <Modal
        hideBackdrop
        open={open}
        onClose={handleClose}
        sx={{
          position: 'absolute',
          top: '50%',
          left: '50%',
          transform: 'translate(-50%, -50%)',
          backgroundColor: '#fff',
          border: '1px solid #b9c2ff',
          borderRadius: '8px',
          height: 'fit-content',
          width: 400,
          boxShadow: 2,
        }}
      >
        <form
          onSubmit={handleSubmit}
          style={{
            display: 'flex',
            flexDirection: 'column',
            paddingTop: '12px',
            paddingLeft: '18px',
            paddingRight: '18px',
            paddingBottom: '30px',
          }}
        >
          <Typography variant='h6' sx={{ my: 2, textAlign: 'center' }}>ADD NEW PARTICIPANT</Typography>

          <FormControl sx={{ my: 1 }}>
            <Typography variant='body2'>Fullname</Typography>
            <TextField
              variant='standard'
              value={fullname}
              onChange={(e) => setFullname(e.target.value)}
            />
          </FormControl>

          <FormControl sx={{ my: 1 }}>
            <Typography variant='body2'>Gender</Typography>
            <Select
              variant='standard'
              value={gender}
              MenuProps={{
                onClick: e => {
                  e.preventDefault();
                }
              }}
              onChange={(e) => setGender(e.target.value)}
            >
              <MenuItem value="None"><em>None</em></MenuItem>
              <MenuItem value='Male'>Male</MenuItem>
              <MenuItem value='Female'>Female</MenuItem>
              <MenuItem value='Other'>Other</MenuItem>
            </Select>
          </FormControl>

          <FormControl sx={{ my: 1 }}>
            <Typography variant='body2'>Email</Typography>
            <TextField
              variant='standard'
              value={email}
              onChange={(e) => setEmail(e.target.value)}
            />
          </FormControl>

          <FormControl sx={{ my: 1 }}>
            <Typography variant='body2'>Phone nr</Typography>
            <TextField
              variant='standard'
              value={phone}
              onChange={(e) => setPhone(e.target.value)}
            />
          </FormControl>

          <FormControl sx={{ my: 1 }}>
            <Typography variant='body2'>Description</Typography>
            <TextField
              variant='standard'
              value={description}
              onChange={(e) => setDescription(e.target.value)}
              multiline
              rows={3}
            />
          </FormControl>

          { !isLoading && <Button
            variant='contained'
            type='submit'
            sx={{
              backgroundColor: '#091fbb'
            }}>
            Add participant
          </Button>}

          { isLoading && <Button
            variant='contained'
            type='submit'
            disabled
            sx={{
              backgroundColor: '#091fbb'
            }}>
            Adding participant...
          </Button>}

        </form>
      </Modal>
    </Box>
  </ClickAwayListener>

Handler functions and states for Modal:

const [open, setOpen] = useState(false);
const [fullname, setFullname] = useState('');
const [gender, setGender] = useState('None');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [description, setDescription] = useState('');
const [isLoading, setIsLoading] = useState(false);

const handleOpen = () => {
  setOpen(!open);
};

const handleClose = () => {
  setFullname('');
  setGender('None');
  setEmail('');
  setPhone('');
  setDescription('');

  setOpen(false);
};

const handleClickAway = (e) => {
if (!e.target.classList.contains('MuiMenuItem-root')) {
  setFullname('');
  setGender('None');
  setEmail('');
  setPhone('');
  setDescription('');

  setOpen(false);
  }
};

const handleSubmit = (e) => {
  e.preventDefault();
  const newParticipant = { fullname, gender, email, phone, description };

  const requestOptions = {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newParticipant)
  };

  setIsLoading(true);

  fetch('http://localhost:8000/participants', requestOptions)
  .then(() => {
    setFullname('');
    setGender('None');
    setEmail('');
    setPhone('');
    setDescription('');

    setIsLoading(false);
    setOpen(!open);
  })

};

Could anyone advise on how to solve this? Adding MenuProps to prevent default behavior on the Select component and the if statement in handleClickAway function didnt help in my case, even though that helped other who were facing the same issue.

Assuming that the goal is to have Select work in Modal without closing it, perhaps the default behavior of Modal could be enough and use of ClickAwayListener may be not be necessary.

Instead of styling Modal directly with the sx prop, try wrap the modal content in a Box and style this container. This preserves the default behavior of Modal , so that clicking on Select would not trigger the closing of it.

Since Modal internally detect click on the backdrop to close itself, consider to style the backdrop with a transparent background instead of disabling it, so that the use of ClickAwayListener could also be omitted.

Demo of simplified example on:stackblitz (excluded all data handling)

<Modal
  open={open}
  onClose={handleClose}
  // 👇 Style the backdrop to be transparent
  slotProps={{ backdrop: { sx: { background: "transparent" } } }}
>
  <Box
    // 👇 Style the container Box for modal content
    sx={{
      position: "absolute",
      top: "50%",
      left: "50%",
      transform: "translate(-50%, -50%)",
      backgroundColor: "#fff",
      border: "1px solid #b9c2ff",
      borderRadius: "8px",
      height: "fit-content",
      width: 400,
      boxShadow: 2,
    }}
  >
    {/* Modal content here */}
  </Box>
</Modal>

This happens because the menu is by default mounted in the DOM outside of the modal HTML hierarchy; the Select component uses a Menu component which in turn uses a Popper component. Looking at the API documentation for Popper :

The children will be under the DOM hierarchy of the parent component.

disablePortal:bool = false

A simple solution is to override this default in MenuProps , which will cause the component to be rendered as a child to the Modal and will no longer trigger the ClickAwayListener callback. I'm not aware of any downsides to this approach.

<Select
  variant='standard'
  value={gender}
  MenuProps={{
    disablePortal: true, // <--- HERE
    onClick: e => {
      e.preventDefault();
    }
  }}
  onChange={(e) => setGender(e.target.value)}
> . . . </Select>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM