styled-components onFocus and onBlur event does not work

Viewed 1255

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>
  );
}
2 Answers

Remember that onBlur means that the element loses it's focus. That happens when another element gets focus or you just click somewhere on the page, not when the cursor moves out.

Another thing I noticed is that you use the isOpened variable to determine if your ItemList should get rendered and the caret should be rotated, but in the onFocus and onBlur methods you are setting the isFocussed state.

It seems like you're just using setIsFocussed but never isFocussed itself. You might want to check out a ESLint, it will help you catch errors like this easily.

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

Related