Search code examples
reactjsdropdownstyled-components

styled-components onFocus and onBlur event does not work


I am using React JS + Typescript for my app. For styling I am using styled-components. I am really new in styled components. I have created one dropdown. The logic works fine. But when I am outside dropdown button it still display the option list. I want to hide the list when it is outside of the dropdown. I tried every single way but I did not succeed. This is it looks like when it is out of the focus:

enter image description here

This is my Dropdown component

import React, { useState } from "react";
import styled from "styled-components";
import Arrow from './Arrow.svg'

const Wrapper = styled.div`
  box-sizing: border-box;
  border: 1 solid #d2d6dc;
`;

const MenuLabel = styled.span`
box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);
border-radius: .375rem;
& after,
& before:{
  box-sizing: border-box;
  border: 0 solid #d2d6dc;
}
`;

const ItemList = styled.div`
color: #798697;
background: white;
line-height: 30px;
padding: .25em 2em .25em 2em;
cursor: defaul;
user-select: none;
transition: all .25s ease;
&:hover,
&.selected {
  background: #F7F7F7;
  color: #4A4A4A;
}

`;

const Button = styled.button<{ isOpen?: boolean }>`
display:inline-flex;
padding: 10px 30px;
font-size: 14px;
justify-content: center;
border-radius:5px;
position: relative;
background: white;
font-size: 12px;
border-color: gray;
transition: ease-in-out .2s;
& hover:{
color: gray;
}
&:focus {
  border: 1px solid blue;
  outline: none;
}

`

const CaratContainer = styled.img<{ isOpen?: boolean }>`
  transform: ${props => (props.isOpen ? "rotate(180deg)" : "rotate(0deg)")};
  transition: all 0.2s ease;
  height: 30px;
  width:20px
`;

const DropDown = styled.ul`
`

export interface IOptions {
  label: string;
  value: number;
}

export interface IDropdown {
  labelDefault: string;
  options: IOptions[];
  onClick?: () => void;
  style?: React.CSSProperties;
  onFocus?: () => void;
  onBlur?: () => void;
}


const Dropdown = ({ labelDefault, options, onClick, style, onFocus, onBlur }: IDropdown) => {
  const [isOpened, setIsOpened] = useState(false);
  const [selectedOption, setSelectedOption] = useState("");
  const [label, setLabel] = useState<string>("");
  const [isFocussed, setIsFocussed] = useState(false)

  const handleSelectedItem = (obj: any) => {
    setSelectedOption(obj.value);
    setLabel(obj.label);
    setIsOpened(!isOpened);
  };

  return (
    <Wrapper >

      <Button onClick={() => setIsOpened(!isOpened)}
        onFocus={() => { //I tried in onFocus but did not
          setIsFocussed(true);
          onFocus && onFocus()
        }}
        onBlur={() => {
          setIsFocussed(false);
          onBlur && onBlur()
        }}
        style={style}
      >
        <p> {selectedOption ? label : labelDefault}</p>
        <CaratContainer isOpen={isOpened} src={Arrow} />
      </Button>
      {isOpened ? options.map(el => (
        <ItemList     //This is the list I want to hide when it out of the dropbutton. 
          key={el.value.toString()}
          onClick={() => handleSelectedItem(el)}
          onFocus={() => {
            setIsFocussed(true);
            onFocus && onFocus()
          }}
          onBlur={() => {
            setIsFocussed(false);
            onBlur && onBlur()
          }}
        >
          {el.label}
        </ItemList>
      )) : null}


    </Wrapper>
  );
}
export default Dropdown;

This is the parent component

import * as React from "react";
import Dropdown from "./dropdown";

const MockData = [
  { label: "one", value: 1 },
  { label: "two", value: 2 },
  { label: "three", value: 3 }
];

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <Dropdown labelDefault="Select a label" options={MockData} />
    </div>
  );
}

Solution

  • If I understand your issue/question, it sounds like you want the dropdown to close when it loses focus.

    Edit - Use a "handle external event" function, don't worry about focus management

    Create an effect that adds/removes an event listener for event originating outside the wrapper of the dropdown.

    const dropdownRef = useRef();
    
    useEffect(() => {
      const externalEventHandler = e => {
        if (!isOpened) return;
    
        const node = dropdownRef.current;
    
        if (node && node.contains(e.target)) {
          return;
        }
    
        setIsOpened(false);
      }
    
      if (isOpened) {
        document.addEventListener('click', externalEventHandler);
      } else {
        document.removeEventListener('click', externalEventHandler);
      }
      
      return () => {
        document.removeEventListener('click', externalEventHandler);
      }
    }, [isOpened]);
    

    Had to make the wrapper a non-block level element else you have to click entirely above or below the dropdown.

    const Wrapper = styled.div`
      display: inline; // <-- no block level element
      box-sizing: border-box;
      border: 1 solid #d2d6dc;
    `;
    

    Edit fervent-fog-btwei