Search code examples
javascriptreactjsjsx

How to overwrite a current character inside input box (OTP inputs) when it is already full?


I am trying to create an input box for a timer/OTP styled input.

How to overwrite a current character inside input box w length 1 (OTP inputs) when it is already full?

My problem is that the input is length 1, and we move the cursor/focus to the next only after catching a single character... but once a char is filled, you have to manually backspace and delete the char and then you can refill...

currently inside input box(0) but since it is already filled, I can't overwrite it

Code Sandbox Example

const { useState, useRef } = React;

function App() {
    const [time, setTime] = useState(null);
    const [running, setRunning] = useState(false);
    const [editState, setEditState] = useState(false);
    const [hms, setHms] = useState(Array(6).fill(0));
    const inputs = useRef([]);

    const handleChange = (e, idx) => {
        const {value} = e.target;
        if (value.match(/^\d$/)) {
            const newHms = [...hms];
            newHms[idx] = value;
            setHms(newHms);

            if (idx < 6) {
                inputs.current[idx+1].focus();
            }

            if (value === '') {
                if (idx > 0) {
                    inputs.current[idx - 1].focus();
                }
            }
        }
    }

    const handleKeyDown = (e, idx) => {
        if (e.key === 'Backspace') {
            if (hms[idx] != '') {
                const newHms = [...hms];
                newHms[idx] = '';
                setHms(newHms);
                return;
            }

            if (idx > 0) {
                inputs.current[idx - 1].focus();
            }
        }
    }
    return (
        <>
            <div id='root' style={{backgroundColor: "#e3b23c", width:"400px", height:"400px"}}>
                {true && <div>
                    {
                        hms.map((_, idx) => {
                            return (
                                <span>
                                    <input
                                        key={idx}
                                        type='text'
                                        maxLength="1"
                                        value={hms[idx]}
                                        onKeyDown={(e) => handleKeyDown(e, idx)}
                                        onChange={(e) => handleChange(e, idx)}
                                        ref={(el) => (inputs.current[idx] = el)}
                                        style={{width: '40px',
                                            height: '40px',
                                            margin: '0 5px',
                                            textAlign: 'center',
                                            fontSize: '18px',
                                            border: '1px solid #ccc',
                                            borderRadius: '4px'}
                                        }
                                        ></input>
                                        {(idx == 1 || idx == 3) && <span style={{
                                            fontWeight: 600,
                                            fontSize: "30px"
                                        }}>:</span>}
                                </span>
                            );
                        })
                    }
                </div>}
                <div>
                    Hours : Minutes : Seconds
                </div>
            </div>
        </>
    )
}

ReactDOM.render(<App />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>


Solution

  • Your input is already filled, and unless you select the input value and then update the value, changes will not be triggered and hence onChange will not be triggered.

    So, you have to trigger this change manually, and although onChange is only called on change, onKeyDown will be called on every keydown even if there is no actualy change of value.

    So, the desired behaviour is achievable with the small tweak of moving all your logic from onChange handler to onKeyDown handler.

     const handleChange = (e, idx) => {
      };
    
      const handleKeyDown = (e, idx) => {
        const keyValue = e.key;
        if (keyValue === "Backspace") {
          if (hms[idx] != "") {
            const newHms = [...hms];
            newHms[idx] = "";
            setHms(newHms);
            return;
          }
    
          if (idx > 0) {
            inputs.current[idx - 1].focus();
          }
        } else if (keyValue.match(/^\d$/)) {
          const newHms = [...hms];
          newHms[idx] = keyValue;
          setHms(newHms);
    
          if (idx < 5) {
            inputs.current[idx + 1].focus();
          }
    
        }
      };
    
    

    Here is the codesandbox