DEV Community

Cover image for Using usePopper and styled-components to create a practical dropdown from scratch

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

Tanner Hallman on April 30, 2020

Backstory So my team and I are trying to create our own reusable UI component library that's not based on any UI frameworks and everythi...
Collapse
 
sergeyt profile image
Sergey Todyshev

a bit modified version using "react-outside-click-handler" and "without styled-components":

import React, { useState, useRef } from "react"; import { usePopper } from "react-popper"; import OutsideClickHandler from "react-outside-click-handler"; function DefaultButton(props) { return <span className="dots" {...props}>...</span>; } export default function Dropdown({ button: Button = DefaultButton, children }) { const [visible, setVisible] = useState(false); const referenceRef = useRef(null); const popperRef = useRef(null); const { styles, attributes } = usePopper( referenceRef.current, popperRef.current, { placement: "bottom", modifiers: [ { name: "offset", enabled: true, options: { offset: [0, 10], }, }, ], } ); const hide = () => setVisible(false); function handleDropdownClick(e: any) { e.preventDefault(); setVisible(true); } const containerStyle: any = { ...styles.popper, display: visible ? "flex" : "none", zIndex: 999, flexDirection: "column", backgroundColor: "#FFF", borderRadius: "4px", boxShadow: "0 0 8px 0 rgba(0, 0, 0, 0.14)", padding: "10px", }; return ( <React.Fragment> <OutsideClickHandler onOutsideClick={hide}> <span ref={referenceRef} onClick={handleDropdownClick}> <Button /> </span> </OutsideClickHandler> <div ref={popperRef} style={containerStyle} {...attributes.popper}> <OutsideClickHandler onOutsideClick={hide}> {children} </OutsideClickHandler> </div> </React.Fragment> ); } 
Enter fullscreen mode Exit fullscreen mode

this snippet might be reused too

Collapse
 
tannerhallman profile image
Tanner Hallman

Ah cool, thanks for sharing!

Collapse
 
njose profile image
Sebastian Njose

Project deadline around the corner, and you just saved me. Thanks mate :-)

Collapse
 
tannerhallman profile image
Tanner Hallman

Cheers! (from the future)

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
Collapse
 
waifutech profile image
Lewd Technologies Inc. • Edited

Making that arrow work requires some reverse-engineering on your part. Long story short:

.dropdownPopper ... &[data-popper-placement^='top'] > [data-popper-arrow] bottom: -4px &[data-popper-placement^='right'] > [data-popper-arrow] left: -4px &[data-popper-placement^='bottom'] > [data-popper-arrow] top: -4px &[data-popper-placement^='left'] > [data-popper-arrow] right: -4px [data-popper-arrow] position: absolute 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
cmdcolin profile image
Colin Diesh

This is a great guide, thanks. Still struggling with random stuff about poppers in my own app but love this as a reference as the react-popper hooks example is too bare bones....

Random note: your code sandbox has this

background-color: "#FFF";

But I think it should be with no quotes

background-color: #FFF;

Collapse
 
drewkiimon profile image
Andrew Pagan

You're an absolute life saver! Thank you so much :')

Collapse
 
tannerhallman profile image
Tanner Hallman

Np andrew!

Collapse
 
palashkaria profile image
Palash Karia

In the last example here, the popover closes even when an item inside the menu is clicked

You might want to use a useOutsideClick hook maybe? usehooks.com/useOnClickOutside/

Collapse
 
tannerhallman profile image
Tanner Hallman

That is the expected behavior of a dropdown component in my experience and the direction I was headed with this example. It is definitely a nice feature if you're looking to persist the visibility of the dropdown menu outside clicks. Thanks for the link!

Collapse
 
moustafa3attia profile image
Moustafa Attia

It's funny how you clarified it way better than the official docs. Great work my man. I'm interested to know what happened to that PR! Would like to help!

Collapse
 
tannerhallman profile image
Tanner Hallman

Never got around to it, as we do. Did you ever end up looking into it?