react-otp-input icon indicating copy to clipboard operation
react-otp-input copied to clipboard

autoComplete Issue

Open umidghimire opened this issue 5 years ago • 15 comments

  • Steps to reproduce the issue: While coping OTP from iOS Safari keyboard only the first number gets pasted.

  • Relevant error messages and/or screenshots: Screenshot (66) Screenshot (68)

umidghimire avatar Aug 07 '20 10:08 umidghimire

This would certainly be a desirable feature addition!

apollonian avatar Aug 10 '20 05:08 apollonian

@apollonian I would like to work on this issue.

dewanshrawat15 avatar Oct 01 '20 04:10 dewanshrawat15

@dewanshrawat15 Sure!

apollonian avatar Oct 01 '20 05:10 apollonian

@apollonian Please check #121 too.

dewanshrawat15 avatar Oct 01 '20 06:10 dewanshrawat15

@dewanshrawat15 setting autocomplete="one-time-code" for input field might help. Referring to https://developer.apple.com/documentation/security/password_autofill/enabling_password_autofill_on_an_html_input_element I don't have a real device to give it a try.

jacobwritescode avatar Oct 11 '20 11:10 jacobwritescode

Check this fork of this plugin. Everything is already done, you need just to copy it. https://github.com/untsop/react-otp-input

spargo-protek avatar Feb 04 '21 18:02 spargo-protek

is it fixed?

jigeshraval avatar Feb 21 '21 19:02 jigeshraval

Check this fork of this plugin. Everything is already done, you need just to copy it. https://github.com/untsop/react-otp-input

It looks outdated, what exactly you're trying to refer here?

jigeshraval avatar Feb 21 '21 19:02 jigeshraval

I just wrote the fix for this to support the mobile devices.. Give it a look.

matthewdfleming avatar Aug 25 '21 16:08 matthewdfleming

Is it the same problem https://github.com/devfolioco/react-otp-input/issues/280?

dankolt avatar Nov 10 '21 13:11 dankolt

@matthewdfleming @dankolt @jigeshraval how did you guys solved this?

LuisValgoi avatar Dec 20 '21 16:12 LuisValgoi

WARNING

For all of you that will eventually face this:

Since the repo it's not maintained anymore, I had to copy the original implementation + fix the autocomplete bug issue.

Check below the details:

USAGE

...
import { OtpInputProps } from "react-otp-input";
import OtpInput from "./OtpInput";
...

IMPLEMENTATION

import React, { Component, PureComponent } from "react";

// keyCode constants
const BACKSPACE = 8;
const LEFT_ARROW = 37;
const RIGHT_ARROW = 39;
const DELETE = 46;
const SPACEBAR = 32;

// Doesn't really check if it's a style Object
// Basic implementation to check if it's not a string
// of classNames and is an Object
const isStyleObject = (obj) => typeof obj === "object";

class SingleOtpInput extends PureComponent {
    constructor(props) {
        super(props);
        this.input = React.createRef();
    }

    // Focus on first render
    // Only when shouldAutoFocus is true
    componentDidMount() {
        const { focus, shouldAutoFocus } = this.props;
        const { current: inputEl } = this.input;

        if (inputEl && focus && shouldAutoFocus) {
            inputEl.focus();
        }
    }

    componentDidUpdate(prevProps) {
        const { focus } = this.props;
        const { current: inputEl } = this.input;

        // Check if focusedInput changed
        // Prevent calling function if input already in focus
        if (prevProps.focus !== focus && inputEl && focus) {
            inputEl.focus();
            inputEl.select();
        }
    }

    getClasses = (...classes) => classes.filter((event) => !isStyleObject(event) && event !== false).join(" ");

    getType = () => {
        const { isInputSecure, isInputNum } = this.props;

        if (isInputSecure) {
            return "password";
        }

        if (isInputNum) {
            return "tel";
        }

        return "text";
    };

    render() {
        const {
            placeholder,
            separator,
            isLastChild,
            inputStyle,
            focus,
            isDisabled,
            hasErrored,
            errorStyle,
            focusStyle,
            disabledStyle,
            shouldAutoFocus,
            isInputNum,
            index,
            value,
            className,
            isInputSecure,
            ...rest
        } = this.props;

        return (
            <div className={className} style={{ display: "flex", alignItems: "center" }}>
                <input
                    aria-label={`${index === 0 ? "Please enter verification code. " : ""}${isInputNum ? "Digit" : "Character"} ${
                        index + 1
                    }`}
                    autoComplete="off"
                    style={Object.assign(
                        { width: "1em", textAlign: "center" },
                        isStyleObject(inputStyle) && inputStyle,
                        focus && isStyleObject(focusStyle) && focusStyle,
                        isDisabled && isStyleObject(disabledStyle) && disabledStyle,
                        hasErrored && isStyleObject(errorStyle) && errorStyle
                    )}
                    placeholder={placeholder}
                    className={this.getClasses(
                        inputStyle,
                        focus && focusStyle,
                        isDisabled && disabledStyle,
                        hasErrored && errorStyle
                    )}
                    type={this.getType()}
                    maxLength={isInputNum}
                    ref={this.input}
                    disabled={isDisabled}
                    value={value ? value : ""}
                    {...rest}
                />
                {!isLastChild && separator}
            </div>
        );
    }
}

class OtpInput extends Component {
    static defaultProps = {
        numInputs: 4,
        onChange: (otp) => console.log(otp),
        isDisabled: false,
        shouldAutoFocus: false,
        value: "",
        isInputSecure: false,
    };

    state = {
        activeInput: 0,
    };

    getOtpValue = () => (this.props.value ? this.props.value.toString().split("") : []);

    getPlaceholderValue = () => {
        const { placeholder, numInputs } = this.props;

        if (typeof placeholder === "string") {
            if (placeholder.length === numInputs) {
                return placeholder;
            }

            if (placeholder.length > 0) {
                console.error("Length of the placeholder should be equal to the number of inputs.");
            }
        }
    };

    // Helper to return OTP from input
    handleOtpChange = (otp) => {
        const { onChange } = this.props;
        const otpValue = otp.join("");

        onChange(otpValue);
    };

    isInputValueValid = (value) => {
        const isTypeValid = this.props.isInputNum ? !isNaN(parseInt(value, 10)) : typeof value === "string";

        return isTypeValid && value.trim().length === 1;
    };

    // Focus on input by index
    focusInput = (input) => {
        const { numInputs } = this.props;
        const activeInput = Math.max(Math.min(numInputs - 1, input), 0);

        this.setState({ activeInput });
    };

    // Focus on next input
    focusNextInput = () => {
        const { activeInput } = this.state;
        this.focusInput(activeInput + 1);
    };

    // Focus on previous input
    focusPrevInput = () => {
        const { activeInput } = this.state;
        this.focusInput(activeInput - 1);
    };

    // Change OTP value at focused input
    changeCodeAtFocus = (value) => {
        const { activeInput } = this.state;
        const otp = this.getOtpValue();
        otp[activeInput] = value[0];

        this.handleOtpChange(otp);
    };

    // Handle pasted OTP
    handleOnPaste = (event) => {
        event.preventDefault();

        const { activeInput } = this.state;
        const { numInputs, isDisabled } = this.props;

        if (isDisabled) {
            return;
        }

        const otp = this.getOtpValue();
        let nextActiveInput = activeInput;

        // Get pastedData in an array of max size (num of inputs - current position)
        const pastedData = e.clipboardData
            .getData("text/plain")
            .slice(0, numInputs - activeInput)
            .split("");

        // Paste data from focused input onwards
        for (let pos = 0; pos < numInputs; ++pos) {
            if (pos >= activeInput && pastedData.length > 0) {
                otp[pos] = pastedData.shift();
                nextActiveInput++;
            }
        }

        this.setState({ activeInput: nextActiveInput }, () => {
            this.focusInput(nextActiveInput);
            this.handleOtpChange(otp);
        });
    };

    handleOnChange = (event) => {
        const { value } = event.target;

        if (this.isInputValueValid(value)) {
            this.changeCodeAtFocus(value);
        }
    };

    // Handle cases of backspace, delete, left arrow, right arrow, space
    handleOnKeyDown = (event) => {
        if (event.keyCode === BACKSPACE || event.key === "Backspace") {
            event.preventDefault();
            this.changeCodeAtFocus("");
            this.focusPrevInput();
        } else if (event.keyCode === DELETE || event.key === "Delete") {
            event.preventDefault();
            this.changeCodeAtFocus("");
        } else if (event.keyCode === LEFT_ARROW || event.key === "ArrowLeft") {
            event.preventDefault();
            this.focusPrevInput();
        } else if (event.keyCode === RIGHT_ARROW || event.key === "ArrowRight") {
            event.preventDefault();
            this.focusNextInput();
        } else if (event.keyCode === SPACEBAR || event.key === " " || event.key === "Spacebar" || event.key === "Space") {
            event.preventDefault();
        }
    };

    // The content may not have changed, but some input took place hence change the focus
    handleOnInput = (event) => {
        if (!event.target.value) return;
        if (event.target.value && event.target.value.length > 1) {
            // this code happens when the users click the 'from your messages' code at the top of the keyboard
            // essentially the mobile OS's act as though someone has pressed all of the OTP numbers all at once
            // rather than using a 'paste' style.
            event.preventDefault();
            const { numInputs } = this.props;
            const { activeInput } = this.state;
            const otp = this.getOtpValue();

            // Get pastedData in an array of max size (num of inputs - current position)
            const pastedData = event.target.value.slice(0, numInputs - activeInput).split("");
            // Paste data from focused input onwards
            for (let pos = 0; pos < numInputs; ++pos) {
                if (pos >= activeInput && pastedData.length > 0) {
                    otp[pos] = pastedData.shift();
                }
            }
            this.handleOtpChange(otp);
            this.focusInput(this.props.numInputs);
        } else if (this.isInputValueValid(event.target.value)) {
            this.focusNextInput();
        } else {
            // This is a workaround for dealing with keyCode "229 Unidentified" on Android.

            if (!this.props.isInputNum) {
                const { nativeEvent } = event;

                if (nativeEvent.data === null && nativeEvent.inputType === "deleteContentBackward") {
                    event.preventDefault();
                    this.changeCodeAtFocus("");
                    this.focusPrevInput();
                }
            }
        }
    };

    renderInputs = () => {
        const { activeInput } = this.state;
        const {
            numInputs,
            inputStyle,
            focusStyle,
            separator,
            isDisabled,
            disabledStyle,
            hasErrored,
            errorStyle,
            shouldAutoFocus,
            isInputNum,
            isInputSecure,
            className,
        } = this.props;

        const inputs = [];
        const otp = this.getOtpValue();
        const placeholder = this.getPlaceholderValue();
        const dataCy = this.props["data-cy"];
        const dataTestId = this.props["data-testid"];

        for (let i = 0; i < numInputs; i++) {
            inputs.push(
                <SingleOtpInput
                    placeholder={placeholder && placeholder[i]}
                    key={i}
                    index={i}
                    focus={activeInput === i}
                    value={otp && otp[i]}
                    onChange={this.handleOnChange}
                    onKeyDown={this.handleOnKeyDown}
                    onInput={this.handleOnInput}
                    onPaste={this.handleOnPaste}
                    onFocus={(event) => {
                        this.setState({ activeInput: i });
                        event.target.select();
                    }}
                    onBlur={() => this.setState({ activeInput: -1 })}
                    separator={separator}
                    inputStyle={inputStyle}
                    focusStyle={focusStyle}
                    isLastChild={i === numInputs - 1}
                    isDisabled={isDisabled}
                    disabledStyle={disabledStyle}
                    hasErrored={hasErrored}
                    errorStyle={errorStyle}
                    shouldAutoFocus={shouldAutoFocus}
                    isInputNum={isInputNum}
                    isInputSecure={isInputSecure}
                    className={className}
                    data-cy={dataCy && `${dataCy}-${i}`}
                    data-testid={dataTestId && `${dataTestId}-${i}`}
                />
            );
        }

        return inputs;
    };

    render() {
        const { containerStyle } = this.props;

        return (
            <div
                style={Object.assign({ display: "flex" }, isStyleObject(containerStyle) && containerStyle)}
                className={!isStyleObject(containerStyle) ? containerStyle : ""}
            >
                {this.renderInputs()}
            </div>
        );
    }
}

export default OtpInput;

LuisValgoi avatar Dec 20 '21 17:12 LuisValgoi

@LuisValgoi I just implement your code, its not woking on my ios device in chrome browser

sgarchavada avatar May 17 '22 12:05 sgarchavada

@LuisValgoi @matthewdfleming The code does not work when you work with iOS on chrome. Any workaround on the same. PRs are welcomed at https://github.com/ritikbanger/react18-input-otp

ritikbanger avatar Aug 11 '22 16:08 ritikbanger

I believe this would be fixed by my PR here: https://github.com/devfolioco/react-otp-input/pull/428

hogiyogi avatar Nov 09 '23 17:11 hogiyogi