reactjsmaterial-uireact-beautiful-dnd

ClickAwayListener component not working with DragDropContext


I made a dropdown using Button and a Popper using Material UI components where you can click on the button and get a list of subjects to choose from.
To make the popper disappear either we can click on the button again or use a <ClickAwayListener> component which listens to click event and closes the Popper.

Now I've to make the list capable of drag and drop feature so I use the react-beautiful-dnd npm package.
But the <ClickAwayListener> doesn't seem to work when I include <DragDropContext>, <Droppable> and <Draggable> components.

Can anyone help me figure it out?

Here's the code without drag and drop feature. CodeSandbox link https://codesandbox.io/s/gallant-newton-mfmhd?file=/demo.js

const subjectsFromBackend = [
  { name: "Physics", selected: false },
  { name: "Chemistry", selected: false },
  { name: "Biology", selected: false },
  { name: "Mathematics", selected: false },
  { name: "Computer Science", selected: false },
];

const useStyles = makeStyles((theme) => ({
  root: {
    display: "flex"
  },
  paper: {
    marginRight: theme.spacing(2)
  }
}));

export default function MenuListComposition() {
  const classes = useStyles();
  const [open, setOpen] = React.useState(false);
  const anchorRef = React.useRef(null);

  const [subjects, setSubjects] = React.useState(subjectsFromBackend);

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

  const handleClose = () => {
    setOpen(false);
  };

  const ColumnItem = ({ subjectName, selected }) => {
    return (
      <>
        <Grid container>
          <Grid item>
            <Checkbox checked={selected} />
          </Grid>
          <Grid item>{subjectName}</Grid>
        </Grid>
      </>
    );
  };

  return (
    <div className={classes.root}>
      <div>
        <Button
          ref={anchorRef}
          onClick={handleToggle}
          style={{ width: 385, justifyContent: "left", textTransform: "none" }}
        >
          {`Subjects Selected`}
        </Button>
        <Popper
          open={open}
          anchorEl={anchorRef.current}
          role={undefined}
          transition
          disablePortal
        >
          {({ TransitionProps, placement }) => (
            <Grow
              {...TransitionProps}
              style={{
                transformOrigin:
                  placement === "bottom" ? "center top" : "center bottom"
              }}
            >
                <Paper style={{ maxHeight: 200, overflow: "auto", width: 385 }}>
                  <ClickAwayListener onClickAway={handleClose}>
                          <List>
                            {subjects.map((col, index) => (
                              <ListItem>
                                <ColumnItem
                                  subjectName={col.name}
                                  selected={col.selected}
                                />
                              </ListItem>
                            ))}
                          </List>
                  </ClickAwayListener>
                </Paper>
            </Grow>
          )}
        </Popper>
      </div>
    </div>
  );
}



Here's the same code using drag and drop. CodeSandbox Link https://codesandbox.io/s/material-demo-forked-ertti

const subjectsFromBackend = [
  { name: "Physics", selected: false },
  { name: "Chemistry", selected: false },
  { name: "Biology", selected: false },
  { name: "Mathematics", selected: false },
  { name: "Computer Science", selected: false },
];

const useStyles = makeStyles((theme) => ({
  root: {
    display: "flex"
  },
  paper: {
    marginRight: theme.spacing(2)
  }
}));

export default function MenuListComposition() {
  const classes = useStyles();
  const [open, setOpen] = React.useState(false);
  const anchorRef = React.useRef(null);

  const [subjects, setSubjects] = React.useState(subjectsFromBackend);

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

  const handleClose = () => {
    setOpen(false);
  };

  const ColumnItem = ({ subjectName, selected }) => {
    return (
      <>
        <Grid container>
          <Grid item>
            <Checkbox checked={selected} />
          </Grid>
          <Grid item>{subjectName}</Grid>
        </Grid>
      </>
    );
  };

  const onDragEnd = (result, subjects, setSubjects) => {
    const { source, destination } = result;
    if (!destination) return;
    if (source.droppableId !== destination.droppableId) return;

    const copiedItems = [...subjects];
    const [removed] = copiedItems.splice(source.index, 1);
    copiedItems.splice(destination.index, 0, removed);
    setSubjects(copiedItems);
  };

  return (
    <div className={classes.root}>
      <div>
        <Button
          ref={anchorRef}
          onClick={handleToggle}
          style={{ width: 385, justifyContent: "left", textTransform: "none" }}
        >
          {`Subjects Selected`}
        </Button>
        <Popper
          open={open}
          anchorEl={anchorRef.current}
          role={undefined}
          transition
          disablePortal
        >
          {({ TransitionProps, placement }) => (
            <Grow
              {...TransitionProps}
              style={{
                transformOrigin:
                  placement === "bottom" ? "center top" : "center bottom"
              }}
            >
              <DragDropContext
                onDragEnd={(res) => onDragEnd(res, subjects, setSubjects)}
              >
                <Paper style={{ maxHeight: 200, overflow: "auto", width: 385 }}>
                  <ClickAwayListener onClickAway={handleClose}>
                    <Droppable droppableId={"subjectsColumn"}>
                      {(provided, snapshot) => (
                        <div
                          {...provided.droppableProps}
                          ref={provided.innerRef}
                        >
                          <List>
                            {subjects.map((col, index) => (
                              <Draggable
                                key={col.name}
                                draggableId={col.name}
                                index={index}
                              >
                                {(provided, snapshot) => (
                                  <div
                                    ref={provided.innerRef}
                                    {...provided.draggableProps}
                                    {...provided.dragHandleProps}
                                  >
                                    <ListItem>
                                      <ColumnItem
                                        subjectName={col.name}
                                        selected={col.selected}
                                      />
                                    </ListItem>
                                  </div>
                                )}
                              </Draggable>
                            ))}
                          </List>
                          {provided.placeholder}
                        </div>
                      )}
                    </Droppable>
                  </ClickAwayListener>
                </Paper>
              </DragDropContext>
            </Grow>
          )}
        </Popper>
      </div>
    </div>
  );
}

Solution

  • I found out that the ClickAwayListener's child component should be wrapped around a div so that the click event could be triggered properly.