Sir Rubberduck
Sir Rubberduck

Reputation: 2262

How to focus next input on pressing enter, in functional components? (TAB behavior)

I want to implement the TAB behavior, but with pressing ENTER.

Here is my attempt to solve it, but it doesn't work due to not using Refs.

My idea is to state in each input, which element should be focused next, and the Enter keyup event handler setting that value as the new focused field.

I've seen examples using useHook but I can't figure how to use it, without spamming a ton of useState's for each input.

Here is a Sandbox of the code below.

import React, { useState, useEffect } from "react";

function Form(props) {
  // Holds the ID of the focused element
  const [focusedElementId, setFocusedElementId] = useState("input-one");

  // The actual focusing, after each re-render
  useEffect(() => {
    document.getElementById(focusedElementId).focus();
  }, []);

  useEffect(() => {
    console.log("STATE:", focusedElementId);
  }, [focusedElementId]);

  // When Enter is pressed, set the focused state to the next element ID provided in each input
  function handleKeyUp(e) {
    e.which = e.which || e.keyCode;
    if (e.which == 13) {
      let nextElementId = e.target.attributes["data-focus"].value;
      console.log(`HANDLER: Enter - focusing ${nextElementId}`);
      setFocusedElementId(nextElementId);
    }
  }

  return (
    <div>
      <div className="form-items" style={{ display: "flex" }}>

        <div className="input-group">
          <label>{"Input One"}</label>
          <br />
          <input
            type={"text"}
            id={"input-one"}
            onKeyUp={handleKeyUp}
            data-focus={"input-two"}
          />
        </div>

        <div className="input-group">
          <label>{"Input Two"}</label>
          <br />
          <input
            type={"text"}
            id={"input-two"}
            onKeyUp={handleKeyUp}
            data-focus={"input-three"}
          />
        </div>

        <div className="input-group">
          <label>{"Input Three"}</label>
          <br />
          <input
            type={"text"}
            id={"input-three"}
            onKeyUp={handleKeyUp}
            data-focus={"input-one"}
          />
        </div>

      </div>
    </div>
  );
}

export default Form;

Upvotes: 5

Views: 7973

Answers (4)

Ben in CA
Ben in CA

Reputation: 851

This is similar to Tim's version, without needing refs, but his didn't work for me, but with some adaptations this works for me:

  const handleKeyDown = (e) => {
    if (e.key === 'Enter') {
      const fields = Array.from(document.querySelectorAll('input')) || []
      const position = fields.indexOf(e.target)
      fields[position + 1] && fields[position + 1].focus()
    }
  }

And on the input, simply use onKeyDown={handleKeyDown}

Upvotes: 2

Karan Janthe
Karan Janthe

Reputation: 37

created this hook

import  { useCallback, useEffect } from 'react'

export default function useFormTab() {
    const keyDownHandler = useCallback((event: KeyboardEvent) => { 
        const target = event.target as HTMLButtonElement
        if (event.keyCode === 13  && target.nodeName === "INPUT") {
            var form = target.form;
            var index = Array.prototype.indexOf.call(form, event.target);
            // @ts-ignore
            form.elements[index + 2].focus();
            event.preventDefault();
          }
     }, []);

  useEffect(() => {
    
    document.addEventListener("keydown", keyDownHandler);
    return () => document.removeEventListener("keydown", keyDownHandler);
  }, [])
  
}

Upvotes: 0

Tim Feeley
Tim Feeley

Reputation: 115

Here's a much more scalable version; just attach at the container element:

function onKeyDown(e) {
    if (e.key === 'Enter') {
      const fields =
        Array.from(e.currentTarget.querySelectorAll('input')) ||
        []
      const position = fields.indexOf(
        e.target // as HTMLInputElement (for TypeScript)
      )
      fields[position + 1] && fields[position + 1].focus()
    }
  }

Upvotes: 1

Sir Rubberduck
Sir Rubberduck

Reputation: 2262

I managed to get it working with refs.

Here's the sandbox.

It looks simple and clean enough, but I am still curious about your opinions.

import React from "react";

function Form() {
  let one = React.createRef();
  let two = React.createRef();
  let three = React.createRef();

  // When Enter is pressed, set the focused state to the next element ID provided in each input
  function handleKeyUp(e) {

    e.which = e.which || e.keyCode;

    // If the key press is Enter
    if (e.which == 13) {
      switch (e.target.id) {
        case "input-one":
          two.current.focus();
          break;
        case "input-two":
          three.current.focus();
          break;
        case "input-three":
          one.current.focus();
          break;

        default:
          break;
      }
    }
  }

  return (
    <div>
      <div className="form-items" style={{ display: "flex" }}>
        <div className="input-group">
          <label>{"Input One"}</label>
          <br />
          <input
            type={"text"}
            id={"input-one"}
            onKeyUp={handleKeyUp}
            ref={one}
          />
        </div>

        <div className="input-group">
          <label>{"Input Two"}</label>
          <br />
          <input
            type={"text"}
            id={"input-two"}
            onKeyUp={handleKeyUp}
            ref={two}
          />
        </div>

        <div className="input-group">
          <label>{"Input Three"}</label>
          <br />
          <input
            type={"text"}
            id={"input-three"}
            onKeyUp={handleKeyUp}
            ref={three}
          />
        </div>
      </div>
    </div>
  );
}

export default Form;

Upvotes: 5

Related Questions