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:
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:
- The toggle buttons need to always be in the DOM, so hide them rather than only rendering if the focus is there.
- 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"} /> </> ); }