1414 * license that can be found in the LICENSE file or at
1515 * https://opensource.org/licenses/MIT.
1616 */
17- import {
18- forwardRef ,
19- useEffect ,
20- useImperativeHandle ,
21- useRef ,
22- type default as React ,
23- } from 'react'
17+ import { useEffect , useRef } from 'react'
2418
2519import { KEYS } from '~/ui/util/keys'
2620import { invariant } from '~/util/invariant'
@@ -38,11 +32,6 @@ export type AuthCodeProps = {
3832 dashAfterIdxs ?: number [ ]
3933}
4034
41- export type AuthCodeRef = {
42- focus : ( ) => void
43- clear : ( ) => void
44- }
45-
4635const INPUT_PATTERN = '[a-zA-Z]{1}'
4736
4837// the reason these helpers are here is we to skip the dashes when we're looking
@@ -77,157 +66,133 @@ const Dash = () => (
7766)
7867
7968// See https://github.com/drac94/react-auth-code-input
80- export const AuthCodeInput = forwardRef < AuthCodeRef , AuthCodeProps > (
81- (
82- {
83- ariaLabel,
84- autoFocus = true ,
85- containerClassName,
86- disabled,
87- inputClassName,
88- length = 6 ,
89- placeholder,
90- onChange,
91- dashAfterIdxs = [ ] ,
92- } ,
93- ref
94- ) => {
95- invariant ( ! isNaN ( length ) || length > 0 , 'Length must be a number greater than 0' )
96- invariant (
97- dashAfterIdxs . every ( ( i ) => 0 <= i && i < length - 1 ) ,
98- '"Dash after" indices must mark spots between inputs, i.e., 0 <= i < length - 1'
99- )
100-
101- const inputsRef = useRef < Array < HTMLInputElement > > ( [ ] )
102-
103- useImperativeHandle ( ref , ( ) => ( {
104- focus : ( ) => {
105- if ( inputsRef . current ) {
106- inputsRef . current [ 0 ] . focus ( )
107- }
108- } ,
109- clear : ( ) => {
110- if ( inputsRef . current ) {
111- for ( const input of inputsRef . current ) {
112- input . value = ''
113- }
114- inputsRef . current [ 0 ] . focus ( )
115- }
116- sendResult ( )
117- } ,
118- } ) )
119-
120- useEffect ( ( ) => {
121- if ( autoFocus ) {
122- inputsRef . current [ 0 ] . focus ( )
123- }
124- } , [ autoFocus ] )
125-
126- const sendResult = ( ) => {
127- // user_code is always uppercase
128- // https://github.com/oxidecomputer/omicron/blob/c63fe1658674186d974e3287afdce09b07912afd/nexus/db-model/src/device_auth.rs#L72-L77
129- const res = inputsRef . current
130- . map ( ( input ) => input . value )
131- . join ( '' )
132- . toUpperCase ( )
133- onChange ?.( res )
69+ export function AuthCodeInput ( {
70+ ariaLabel,
71+ autoFocus = true ,
72+ containerClassName,
73+ disabled,
74+ inputClassName,
75+ length = 6 ,
76+ placeholder,
77+ onChange,
78+ dashAfterIdxs = [ ] ,
79+ } : AuthCodeProps ) {
80+ invariant ( ! isNaN ( length ) || length > 0 , 'Length must be a number greater than 0' )
81+ invariant (
82+ dashAfterIdxs . every ( ( i ) => 0 <= i && i < length - 1 ) ,
83+ '"Dash after" indices must mark spots between inputs, i.e., 0 <= i < length - 1'
84+ )
85+
86+ const inputsRef = useRef < Array < HTMLInputElement > > ( [ ] )
87+
88+ useEffect ( ( ) => {
89+ if ( autoFocus ) {
90+ inputsRef . current [ 0 ] . focus ( )
13491 }
92+ } , [ autoFocus ] )
93+
94+ const sendResult = ( ) => {
95+ // user_code is always uppercase
96+ // https://github.com/oxidecomputer/omicron/blob/c63fe1658674186d974e3287afdce09b07912afd/nexus/db-model/src/device_auth.rs#L72-L77
97+ const res = inputsRef . current
98+ . map ( ( input ) => input . value )
99+ . join ( '' )
100+ . toUpperCase ( )
101+ onChange ?.( res )
102+ }
135103
136- const handleOnChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
137- const { value } = e . target
138- const nextInput = getNextInputSibling ( e . target )
139- if ( value . length > 1 ) {
140- e . target . value = value . charAt ( 0 )
104+ const handleOnChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
105+ const { value } = e . target
106+ const nextInput = getNextInputSibling ( e . target )
107+ if ( value . length > 1 ) {
108+ e . target . value = value . charAt ( 0 )
109+ if ( nextInput ) {
110+ nextInput . focus ( )
111+ }
112+ } else {
113+ if ( value . match ( INPUT_PATTERN ) ) {
141114 if ( nextInput ) {
142115 nextInput . focus ( )
143116 }
144117 } else {
145- if ( value . match ( INPUT_PATTERN ) ) {
146- if ( nextInput ) {
147- nextInput . focus ( )
148- }
149- } else {
150- e . target . value = ''
151- }
118+ e . target . value = ''
152119 }
153- sendResult ( )
154120 }
121+ sendResult ( )
122+ }
155123
156- const handleOnKeyDown = ( e : React . KeyboardEvent < HTMLInputElement > ) => {
157- const target = e . target as HTMLInputElement
158- if ( e . key === KEYS . backspace ) {
159- if ( target . value === '' ) {
160- const prevInput = getPrevInputSibling ( target )
161- if ( prevInput !== null ) {
162- prevInput . value = ''
163- prevInput . focus ( )
164- e . preventDefault ( )
165- }
166- } else {
167- target . value = ''
124+ const handleOnKeyDown = ( e : React . KeyboardEvent < HTMLInputElement > ) => {
125+ const target = e . target as HTMLInputElement
126+ if ( e . key === KEYS . backspace ) {
127+ if ( target . value === '' ) {
128+ const prevInput = getPrevInputSibling ( target )
129+ if ( prevInput !== null ) {
130+ prevInput . value = ''
131+ prevInput . focus ( )
132+ e . preventDefault ( )
168133 }
169- sendResult ( )
134+ } else {
135+ target . value = ''
170136 }
137+ sendResult ( )
171138 }
139+ }
172140
173- const handleOnFocus = ( e : React . FocusEvent < HTMLInputElement > ) => {
174- e . target . select ( )
175- }
141+ const handleOnFocus = ( e : React . FocusEvent < HTMLInputElement > ) => {
142+ e . target . select ( )
143+ }
176144
177- const handleOnPaste = ( e : React . ClipboardEvent < HTMLInputElement > ) => {
178- const pastedValue = e . clipboardData . getData ( 'Text' )
179-
180- let currentInput = 0
181-
182- for ( let i = 0 ; i < pastedValue . length ; i ++ ) {
183- const pastedCharacter = pastedValue . charAt ( i )
184- const currentValue = inputsRef . current [ currentInput ] . value
185- if ( pastedCharacter . match ( INPUT_PATTERN ) && ! currentValue ) {
186- const input = inputsRef . current [ currentInput ]
187- input . value = pastedCharacter
188- const nextInput = getNextInputSibling ( input )
189- if ( nextInput !== null ) {
190- nextInput . focus ( )
191- currentInput ++
192- }
193- }
194- }
195- sendResult ( )
145+ const handleOnPaste = ( e : React . ClipboardEvent < HTMLInputElement > ) => {
146+ const pastedValue = e . clipboardData . getData ( 'Text' )
196147
197- e . preventDefault ( )
198- }
148+ let currentInput = 0
199149
200- const inputs = [ ]
201- for ( let i = 0 ; i < length ; i ++ ) {
202- inputs . push (
203- < input
204- key = { i }
205- type = "text"
206- inputMode = "text"
207- onChange = { handleOnChange }
208- onKeyDown = { handleOnKeyDown }
209- onFocus = { handleOnFocus }
210- onPaste = { handleOnPaste }
211- pattern = { INPUT_PATTERN }
212- ref = { ( el : HTMLInputElement ) => {
213- inputsRef . current [ i ] = el
214- } }
215- maxLength = { 1 }
216- className = { inputClassName }
217- autoComplete = "off"
218- aria-label = {
219- ariaLabel ? `${ ariaLabel } . Character ${ i + 1 } .` : `Character ${ i + 1 } .`
220- }
221- disabled = { disabled }
222- placeholder = { placeholder }
223- />
224- )
225-
226- if ( dashAfterIdxs . includes ( i ) ) {
227- inputs . push ( < Dash key = { `${ i } -dash` } /> )
150+ for ( let i = 0 ; i < pastedValue . length ; i ++ ) {
151+ const pastedCharacter = pastedValue . charAt ( i )
152+ const currentValue = inputsRef . current [ currentInput ] . value
153+ if ( pastedCharacter . match ( INPUT_PATTERN ) && ! currentValue ) {
154+ const input = inputsRef . current [ currentInput ]
155+ input . value = pastedCharacter
156+ const nextInput = getNextInputSibling ( input )
157+ if ( nextInput !== null ) {
158+ nextInput . focus ( )
159+ currentInput ++
160+ }
228161 }
229162 }
163+ sendResult ( )
230164
231- return < div className = { containerClassName } > { inputs } </ div >
165+ e . preventDefault ( )
232166 }
233- )
167+
168+ const inputs = [ ]
169+ for ( let i = 0 ; i < length ; i ++ ) {
170+ inputs . push (
171+ < input
172+ key = { i }
173+ type = "text"
174+ inputMode = "text"
175+ onChange = { handleOnChange }
176+ onKeyDown = { handleOnKeyDown }
177+ onFocus = { handleOnFocus }
178+ onPaste = { handleOnPaste }
179+ pattern = { INPUT_PATTERN }
180+ ref = { ( el : HTMLInputElement ) => {
181+ inputsRef . current [ i ] = el
182+ } }
183+ maxLength = { 1 }
184+ className = { inputClassName }
185+ autoComplete = "off"
186+ aria-label = { ariaLabel ? `${ ariaLabel } . Character ${ i + 1 } .` : `Character ${ i + 1 } .` }
187+ disabled = { disabled }
188+ placeholder = { placeholder }
189+ />
190+ )
191+
192+ if ( dashAfterIdxs . includes ( i ) ) {
193+ inputs . push ( < Dash key = { `${ i } -dash` } /> )
194+ }
195+ }
196+
197+ return < div className = { containerClassName } > { inputs } </ div >
198+ }
0 commit comments