Make 2FA input play nicer with phone keyboards (#2199)

This commit is contained in:
SleeplessOne1917 2023-10-24 01:25:06 +00:00 committed by GitHub
parent 2b5068187c
commit 25b06124fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 33 additions and 67 deletions

@ -1 +1 @@
Subproject commit abd40d4737fa732321fd7b62e42bbfcd51081cb6 Subproject commit 6fbc86932a03c4d40829ee4a3395259b2a7660e5

View file

@ -25,64 +25,42 @@ interface TotpModalState {
const TOTP_LENGTH = 6; const TOTP_LENGTH = 6;
async function handleSubmit(modal: TotpModal, totp: string) { async function handleSubmit(i: TotpModal, totp: string) {
const successful = await modal.props.onSubmit(totp); const successful = await i.props.onSubmit(totp);
if (!successful) { if (!successful) {
modal.setState({ totp: "" }); i.setState({ totp: "" });
for (const inputRef of modal.inputRefs) { i.inputRef.current?.focus();
inputRef!.value = "";
}
modal.inputRefs[0]?.focus();
} }
} }
function handleInput( function handleInput(i: TotpModal, event: any) {
{ modal, i }: { modal: TotpModal; i: number },
event: any,
) {
if (isNaN(event.target.value)) { if (isNaN(event.target.value)) {
modal.inputRefs[i]!.value = "";
return; return;
} }
modal.setState(prev => ({ ...prev, totp: prev.totp + event.target.value })); i.setState({
modal.inputRefs[i + 1]?.focus(); totp: event.target.value,
});
const { totp } = modal.state; const { totp } = i.state;
if (totp.length >= TOTP_LENGTH) { if (totp.length >= TOTP_LENGTH) {
handleSubmit(modal, totp); handleSubmit(i, totp);
} }
} }
function handleKeyUp( function handlePaste(i: TotpModal, event: any) {
{ modal, i }: { modal: TotpModal; i: number },
event: any,
) {
if (event.key === "Backspace" && i > 0) {
modal.setState(prev => ({
...prev,
totp: prev.totp.slice(0, prev.totp.length - 1),
}));
modal.inputRefs[i - 1]?.focus();
}
}
function handlePaste(modal: TotpModal, event: any) {
event.preventDefault(); event.preventDefault();
const text: string = event.clipboardData.getData("text")?.trim(); const text: string = event.clipboardData.getData("text")?.trim();
if (text.length > TOTP_LENGTH || isNaN(Number(text))) { if (text.length > TOTP_LENGTH || isNaN(Number(text))) {
toast(I18NextService.i18n.t("invalid_totp_code"), "danger"); toast(I18NextService.i18n.t("invalid_totp_code"), "danger");
modal.clearTotp(); i.clearTotp();
} else { } else {
[...text].forEach((num, i) => { i.setState({ totp: text });
modal.inputRefs[i]!.value = num;
});
modal.setState({ totp: text });
if (text.length === TOTP_LENGTH) { if (text.length === TOTP_LENGTH) {
handleSubmit(modal, text); handleSubmit(i, text);
} }
} }
} }
@ -91,8 +69,8 @@ export default class TotpModal extends Component<
TotpModalProps, TotpModalProps,
TotpModalState TotpModalState
> { > {
private readonly modalDivRef: RefObject<HTMLDivElement>; readonly modalDivRef: RefObject<HTMLDivElement>;
inputRefs: (HTMLInputElement | null)[] = []; readonly inputRef: RefObject<HTMLInputElement>;
modal: Modal; modal: Modal;
state: TotpModalState = { state: TotpModalState = {
totp: "", totp: "",
@ -102,6 +80,7 @@ export default class TotpModal extends Component<
super(props, context); super(props, context);
this.modalDivRef = createRef(); this.modalDivRef = createRef();
this.inputRef = createRef();
this.clearTotp = this.clearTotp.bind(this); this.clearTotp = this.clearTotp.bind(this);
this.handleShow = this.handleShow.bind(this); this.handleShow = this.handleShow.bind(this);
@ -207,34 +186,24 @@ export default class TotpModal extends Component<
<form id="totp-form"> <form id="totp-form">
<label <label
className="form-label ms-2 mt-4 fw-bold" className="form-label ms-2 mt-4 fw-bold"
id="totp-input-label" htmlFor="totp-input"
htmlFor="totp-input-0"
> >
{I18NextService.i18n.t("enter_totp_code")} {I18NextService.i18n.t("enter_totp_code")}
</label> </label>
<div className="d-flex justify-content-between align-items-center p-2"> <div className="d-flex justify-content-between align-items-center p-2">
{Array.from(Array(TOTP_LENGTH).keys()).map(i => ( <input
<input type="text"
key={ inputMode="numeric"
i /*While using indices as keys is usually bad practice, in this case we don't have to worry about the order of the list items changing.*/ autoComplete="one-time-code"
} maxLength={TOTP_LENGTH}
type="text" id="totp-input"
inputMode="numeric" className="form-control form-control-lg mx-2 p-1 p-md-2 text-center"
autoComplete="one-time-code" onInput={linkEvent(this, handleInput)}
maxLength={1} onPaste={linkEvent(this, handlePaste)}
disabled={totp.length !== i} ref={this.inputRef}
aria-labelledby="totp-input-label" enterKeyHint="done"
id={`totp-input-${i}`} value={totp}
className="form-control form-control-lg mx-2 p-1 p-md-2 text-center" />
onInput={linkEvent({ modal: this, i }, handleInput)}
onKeyUp={linkEvent({ modal: this, i }, handleKeyUp)}
onPaste={linkEvent(this, handlePaste)}
ref={element => {
this.inputRefs[i] = element;
}}
enterKeyHint="done"
/>
))}
</div> </div>
</form> </form>
</div> </div>
@ -255,13 +224,10 @@ export default class TotpModal extends Component<
clearTotp() { clearTotp() {
this.setState({ totp: "" }); this.setState({ totp: "" });
for (const inputRef of this.inputRefs) {
inputRef!.value = "";
}
} }
async handleShow() { async handleShow() {
this.inputRefs[0]?.focus(); this.inputRef.current?.focus();
if (this.props.type === "generate") { if (this.props.type === "generate") {
const { getSVG } = await import("@shortcm/qr-image/lib/svg"); const { getSVG } = await import("@shortcm/qr-image/lib/svg");