DEV Community

Discussion on: Using usePopper and styled-components to create a practical dropdown from scratch

Collapse
 
nuesslerm profile image
M N • Edited

Thanks a lot for this nice codesandbox. it helped me a lot to get my DropDown component to work in quite a short amount of time.
I want to share my modified version as a sandbox link. I used TS and my own useHandleClickOutside hook, but you could also use Sergey's "react-outside-click-handler" dep. I don't think that makes any difference.

I modularised it to accept React.ReactNodes as dropdown items and some popper props.

codesandbox.io/s/1-react-popper-va...

import React, { FC, useState, useRef } from "react";
import styled from "styled-components";
import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core";
import useHandleClickOutside from "./useHandleClickOutside";

interface DropdownContainerProps {
  open: boolean;
}

const DropdownContainer = styled.div<DropdownContainerProps>`
  display: ${({ open }) => (open ? "flex" : "none")};
  width: 100%;
  flex-direction: column;
  background-color: #fff;
  border-radius: 5px;
  box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.14);
  padding: 5px;
`;

const DropdownItem = styled.div`
  justify-content: flex-start;
  height: 40px;
  padding-right: 10px;
  padding-left: 10px;
  align-items: center;

  &:hover {
    background-color: #00ffff;
  }
  &:active {
    font-weight: 700;
    color: #00ffff;
  }
`;

const DropDownTrigger = styled.button`
  border: none;
  background: none;
  font-size: 16px;
  font-family: inherit;
`;

const DefaultTitle: FC = () => {
  return <div>What</div>;
};

interface DropDownProps {
  children: React.ReactNode;
  titleElement?: React.ReactElement;
  placement?: Placement;
  offset?: { horizontal: number; vertical: number };
}

const Dropdown: FC<DropDownProps> = ({
  titleElement: TitleElement = <DefaultTitle />,
  placement = "bottom",
  offset = { horizontal: 0, vertical: 0 },
  children
}) => {
  const [open, setOpen] = useState(false);
  const referenceRef = useRef(null);
  const popperRef = useRef(null);

  const toggle = () => setOpen(!open);

  const { ref: DropDownRef } = useHandleClickOutside(setOpen);

  const { horizontal, vertical } = offset;
  const { styles, attributes } = usePopper(
    referenceRef.current,
    popperRef.current,
    {
      placement,
      modifiers: [
        {
          name: "offset",
          enabled: true,
          options: {
            offset: [horizontal, vertical]
          }
        }
      ]
    }
  );

  function handleDropdownClick(e: any) {
    e.preventDefault();
    toggle();
  }

  return (
    <div ref={DropDownRef}>
      <DropDownTrigger
        type="button"
        ref={referenceRef}
        onClick={handleDropdownClick}
      >
        {TitleElement}
      </DropDownTrigger>
      <div ref={popperRef} style={styles.popper} {...attributes.popper}>
        <DropdownContainer style={styles.offset} open={open}>
          {children &&
            React.Children.map(children, (child) => {
              return <DropdownItem>{child}</DropdownItem>;
            })}
        </DropdownContainer>
      </div>
    </div>
  );
};

export default Dropdown;
Enter fullscreen mode Exit fullscreen mode

App.tsx

import React from "react";
import DropDown from "./DropDown";

const App = () => {
  return (
    <DropDown titleElement={<div>{"Click Me!"}</div>}>
      <div>1 asdf</div>
      <div>2 asdf</div>
      <div>3 asdf</div>
      <div>4 asdf</div>
    </DropDown>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode