Skip to content
Advertisement

How to properly control focus and blur events on a React-Bootstrap InputGroup?

I have a single input element from react-bootstrap that will allow the user to change the field value and 2 buttons will appear, one to accept the changes and the other will cancel the changes leaving the original value in.

I manage to control the focus and blur events by delegating the listeners to the wrapping component, my thinking is that since the focus is still within the wrapping component, I won’t lose its focus, but pressing the inner buttons seems to blur the focus, therefore the Accept and Cancel buttons don’t fire any events…

Here is my code example:

Edit Edit Cell Form

import { useState, useEffect, useRef } from "react";
import { InputGroup, Button, FormControl } from "react-bootstrap";

import "./styles.css";

const InputField = ({ title }) => {
  const formRef = useRef(null);
  const [value, setValue] = useState(title);
  const [toggleButtons, setToggleButtons] = useState(false);

  const onChange = (e) => {
    setValue(e.target.value);
  };

  const onFocus = () => {
    setToggleButtons(true);
  };

  const onBlur = () => {
    setToggleButtons(false);
  };

  const acceptChange = () => {
    console.log("Accept");
    setToggleButtons(false);
  };

  const cancelChange = () => {
    console.log("Cancel");
    setToggleButtons(false);
  };

  useEffect(() => {
    const form = formRef.current;
    form.addEventListener("focus", onFocus);
    form.addEventListener("blur", onBlur);

    return () => {
      form.removeEventListener("focus", onFocus);
      form.removeEventListener("blur", onBlur);
    };
  }, []);

  return (
    <div className="App">
      <InputGroup className="m-3" style={{ width: "400px" }}>
        <FormControl
          ref={formRef}
          value={value}
          onChange={onChange}
          // onFocus={onFocus}
          // onBlur={onBlur}
        />
        {toggleButtons ? (
          <InputGroup.Append>
            <Button variant="outline-secondary" onClick={() => acceptChange()}>
              Accept
            </Button>
            <Button variant="outline-secondary" onClick={() => cancelChange()}>
              Cancel
            </Button>
          </InputGroup.Append>
        ) : null}
      </InputGroup>
    </div>
  );
};

export default function App() {
  return (
    <>
      <InputField title={"Input 1"} />
      <InputField title={"Input 2"} />
      <InputField title={"Input 3"} />
      <InputField title={"Input 4"} />
    </>
  );
}

Advertisement

Answer

A couple of changes are needed to make this work:

  1. The toggle buttons need to always be in the DOM, so hide them rather than only rendering if the focus is there.
  2. To avoid hiding the buttons when the blur occurs from the input to one of the buttons you can check if the newly focused element is a sibling of the input by using the event’s relatedTarget and the currentTarget.parentNode.

For example:

import { useState } from "react";
import { InputGroup, Button, FormControl } from "react-bootstrap";

import "./styles.css";

const InputField = ({ title }) => {
  const [value, setValue] = useState(title);
  const [toggleButtons, setToggleButtons] = useState(false);

  const onChange = (e) => {
    setValue(e.target.value);
  };

  const onFocus = () => {
    setToggleButtons(true);
  };

  const onBlur = (e) => {
    if (!e.currentTarget.parentNode.contains(e.relatedTarget)) {
      setToggleButtons(false);
    }
  };

  const acceptChange = () => {
    console.log("Accept");
    setToggleButtons(false);
  };

  const cancelChange = () => {
    console.log("Cancel");
    setToggleButtons(false);
  };

  return (
    <div className="App">
      <InputGroup className="m-3" style={{ width: "400px" }}>
        <FormControl
          value={value}
          onChange={onChange}
          onFocus={onFocus}
          onBlur={onBlur}
        />
        <InputGroup.Append className={toggleButtons ? "d-flex" : "d-none"}>
          <Button
            onBlur={onBlur}
            variant="outline-secondary"
            onClick={() => acceptChange()}
          >
            Accept
          </Button>
          <Button
            onBlur={onBlur}
            variant="outline-secondary"
            onClick={() => cancelChange()}
          >
            Cancel
          </Button>
        </InputGroup.Append>
      </InputGroup>
    </div>
  );
};

export default function App() {
  return (
    <>
      <InputField title={"Input 1"} />
      <InputField title={"Input 2"} />
      <InputField title={"Input 3"} />
      <InputField title={"Input 4"} />
    </>
  );
}

https://codesandbox.io/s/input-group-focus-slwoh

User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement