Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto select TOTP input contents when focused #166

Open
wants to merge 8 commits into
base: trunk
Choose a base branch
from
64 changes: 41 additions & 23 deletions settings/src/components/auto-tabbing-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,60 @@ import { useCallback } from '@wordpress/element';
import NumericControl from './numeric-control';

const AutoTabbingInput = ( props ) => {
const { inputs, setInputs, onComplete, error } = props;
const { inputs, setInputs, error, setError } = props;

const handleChange = useCallback( ( value, event, index, inputRef ) => {
const handleChange = useCallback( ( value, event, index ) => {
setInputs( ( prevInputs ) => {
const newInputs = [ ...prevInputs ];

// Clean input
if ( value.trim() === '' ) {
event.target.value = '';
value = '';
}

newInputs[ index ] = value;

// Check if all inputs are filled
const allFilled = newInputs.every( ( input ) => '' !== input );
if ( allFilled && onComplete ) {
onComplete( true );
} else {
onComplete( false );
}
newInputs[ index ] = value.trim() === '' ? '' : value;

return newInputs;
} );
}, [] );

const handleKeyDown = useCallback( ( value, event, index, inputElement ) => {
// Ignore keys associated with input navigation and paste events.
if ( [ 'Tab', 'Shift', 'Meta', 'Backspace' ].includes( event.key ) ) {
return;
}

if ( !! value && inputElement.nextElementSibling ) {
inputElement.nextElementSibling.focus();
}
}, [] );

if ( value && '' !== value.trim() && inputRef.current.nextElementSibling ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change handler is now only responsible for updating the input values

inputRef.current.nextElementSibling.focus();
const handleKeyUp = useCallback( ( value, event, index, inputElement ) => {
if ( event.key === 'Backspace' && inputElement.previousElementSibling ) {
inputElement.previousElementSibling.focus();
}
}, [] );

const handleKeyDown = useCallback( ( value, event, index, inputRef ) => {
if ( event.key === 'Backspace' && ! value && inputRef.current.previousElementSibling ) {
inputRef.current.previousElementSibling.focus();
const handleFocus = useCallback(
( value, event, index, inputElement ) => inputElement.select(),
[]
);

const handlePaste = useCallback( ( event ) => {
event.preventDefault();

const newInputs = event.clipboardData
.getData( 'Text' )
.replace( /[^0-9]/g, '' )
.split( '' );

if ( inputs.length === newInputs.length ) {
setInputs( newInputs );
} else {
setError( 'The code you pasted is not the correct length.' );
}
}, [] );

return (
<div className={ 'wporg-2fa__auto-tabbing-input' + ( error ? ' is-error' : '' ) }>
<div
className={ 'wporg-2fa__auto-tabbing-input' + ( error ? ' is-error' : '' ) }
onPaste={ handlePaste }
>
{ inputs.map( ( value, index ) => (
<NumericControl
{ ...props }
Expand All @@ -55,6 +71,8 @@ const AutoTabbingInput = ( props ) => {
index={ index }
onChange={ handleChange }
onKeyDown={ handleKeyDown }
onKeyUp={ handleKeyUp }
onFocus={ handleFocus }
maxLength="1"
required
/>
Expand Down
48 changes: 35 additions & 13 deletions settings/src/components/numeric-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,47 @@ import { useRef, useCallback } from '@wordpress/element';
* using the underlying `input[type="number"]`, which has some accessibility issues.
*
* @param props
* @param props.autoComplete
* @param props.pattern
* @param props.title
* @param props.onChange
* @param props.onFocus
* @param props.onKeyDown
* @param props.onKeyUp
* @param props.index
* @param props.value
* @param props.maxLength
* @param props.required
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number#accessibility
* @see https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/
* @see https://stackoverflow.com/a/66759105/450127
*/
export default function NumericControl( props ) {
const { autoComplete, pattern, title, onChange, onKeyDown, index, value, maxLength, required } =
props;

export default function NumericControl( {
autoComplete,
pattern,
title,
onChange,
onFocus,
onKeyDown,
onKeyUp,
index,
value,
maxLength,
required,
} ) {
const inputRef = useRef( null );

const handleChange = useCallback(
const createHandler = ( handler ) => ( event ) =>
// Most callers will only need the value, so make it convenient for them.
( event ) => onChange && onChange( event.target.value, event, index, inputRef ),
[]
);
handler && handler( event.target.value, event, index, inputRef.current );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the consumer needs to know about the ref, we can just pass the current value back


const handleKeyDown = useCallback(
// Most callers will only need the value, so make it convenient for them.
( event ) => onKeyDown && onKeyDown( event.target.value, event, index, inputRef ),
[]
);
const handleChange = useCallback( createHandler( onChange ), [] );

const handleFocus = useCallback( createHandler( onFocus ), [] );

const handleKeyDown = useCallback( createHandler( onKeyDown ), [] );

const handleKeyUp = useCallback( createHandler( onKeyUp ), [] );

return (
<input
Expand All @@ -44,7 +64,9 @@ export default function NumericControl( props ) {
pattern={ pattern || '[0-9 ]*' }
title={ title || 'Only numbers and spaces are allowed' }
onChange={ handleChange }
onFocus={ handleFocus }
onKeyDown={ handleKeyDown }
onKeyUp={ handleKeyUp }
data-1p-ignore // Prevent 1Password from showing up
value={ value }
maxLength={ maxLength }
Expand Down
21 changes: 13 additions & 8 deletions settings/src/components/totp.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import apiFetch from '@wordpress/api-fetch';
import { Button, Notice, Flex } from '@wordpress/components';
import { Icon, cancelCircleFilled } from '@wordpress/icons';
import { RawHTML, useCallback, useContext, useEffect, useState } from '@wordpress/element';
import { RawHTML, useCallback, useContext, useEffect, useRef, useState } from '@wordpress/element';

/**
* Internal dependencies
Expand Down Expand Up @@ -222,18 +222,23 @@ function createQrCode( data ) {
* @param props.setError
*/
function SetupForm( { handleEnable, qrCodeUrl, secretKey, inputs, setInputs, error, setError } ) {
const [ isInputComplete, setIsInputComplete ] = useState( false );
const inputsRef = useRef( inputs );

useEffect( () => {
if ( error && inputs.some( ( input ) => input === '' ) ) {
const prevInputs = inputsRef.current;
inputsRef.current = inputs;

// Clear the error if any of the inputs have changed
if ( error && inputs.some( ( input, index ) => input !== prevInputs[ index ] ) ) {
setError( '' );
}
}, [ error, inputs ] );
}, [ error, inputs, inputsRef ] );

const handleComplete = useCallback( ( isComplete ) => setIsInputComplete( isComplete ), [] );
const handleClearClick = useCallback( () => setInputs( Array( 6 ).fill( '' ) ), [] );
const handleClearClick = useCallback( () => {
setInputs( Array( 6 ).fill( '' ) );
}, [] );

const canSubmit = qrCodeUrl && secretKey && isInputComplete;
const canSubmit = qrCodeUrl && secretKey && inputs.every( ( input ) => !! input );

return (
<Flex
Expand All @@ -255,7 +260,7 @@ function SetupForm( { handleEnable, qrCodeUrl, secretKey, inputs, setInputs, err
inputs={ inputs }
setInputs={ setInputs }
error={ error }
onComplete={ handleComplete }
setError={ setError }
/>

<div className="wporg-2fa__submit-btn-container">
Expand Down