diff --git a/settings/src/components/auto-tabbing-input.js b/settings/src/components/auto-tabbing-input.js index 1e00e12e..e89c4115 100644 --- a/settings/src/components/auto-tabbing-input.js +++ b/settings/src/components/auto-tabbing-input.js @@ -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 ) { - 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 ( -
+
{ inputs.map( ( value, index ) => ( { index={ index } onChange={ handleChange } onKeyDown={ handleKeyDown } + onKeyUp={ handleKeyUp } + onFocus={ handleFocus } maxLength="1" required /> diff --git a/settings/src/components/numeric-control.js b/settings/src/components/numeric-control.js index 78429427..b13fd64c 100644 --- a/settings/src/components/numeric-control.js +++ b/settings/src/components/numeric-control.js @@ -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 ); - 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 ( { - 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 (