@@ -2,88 +2,126 @@ import { Button } from '@/components/ui/button';
22import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card' ;
33import { regenerateRecoveryCodes } from '@/routes/two-factor' ;
44import { Form } from '@inertiajs/react' ;
5- import { Eye , EyeOff , LockKeyhole , RefreshCw } from 'lucide-react' ;
6- import { useEffect , useRef , useState } from 'react' ;
5+ import { Eye , EyeOff , LockKeyhole , LucideIcon , RefreshCw } from 'lucide-react' ;
6+ import { useCallback , useEffect , useRef , useState } from 'react' ;
77
88interface TwoFactorRecoveryCodesProps {
99 recoveryCodesList : string [ ] ;
1010 fetchRecoveryCodes : ( ) => Promise < void > ;
1111}
1212
13- export default function TwoFactorRecoveryCodes ( { recoveryCodesList, fetchRecoveryCodes } : TwoFactorRecoveryCodesProps ) {
14- const [ isRecoveryCodesVisible , setIsRecoveryCodesVisible ] = useState < boolean > ( false ) ;
15- const recoveryCodeSectionRef = useRef < HTMLDivElement | null > ( null ) ;
13+ export default function TwoFactorRecoveryCodes ( {
14+ recoveryCodesList,
15+ fetchRecoveryCodes,
16+ } : TwoFactorRecoveryCodesProps ) {
17+ const [ isCodesVisible , setIsCodesVisible ] = useState < boolean > ( false ) ;
18+ const codesSectionRef = useRef < HTMLDivElement | null > ( null ) ;
1619
17- const toggleRecoveryCodesVisibility = async ( ) => {
18- if ( ! isRecoveryCodesVisible && ! recoveryCodesList . length ) {
20+ const toggleCodesVisibility = useCallback ( async ( ) => {
21+ if ( ! isCodesVisible && ! recoveryCodesList . length ) {
1922 await fetchRecoveryCodes ( ) ;
2023 }
2124
22- setIsRecoveryCodesVisible ( ! isRecoveryCodesVisible ) ;
25+ setIsCodesVisible ( ! isCodesVisible ) ;
2326
24- if ( ! isRecoveryCodesVisible ) {
27+ if ( ! isCodesVisible ) {
2528 setTimeout ( ( ) => {
26- recoveryCodeSectionRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
29+ codesSectionRef . current ?. scrollIntoView ( {
30+ behavior : 'smooth' ,
31+ block : 'nearest'
32+ } ) ;
2733 } ) ;
2834 }
29- } ;
35+ } , [ isCodesVisible , recoveryCodesList . length , fetchRecoveryCodes ] ) ;
3036
3137 useEffect ( ( ) => {
3238 if ( ! recoveryCodesList . length ) {
3339 fetchRecoveryCodes ( ) ;
3440 }
3541 } , [ recoveryCodesList . length , fetchRecoveryCodes ] ) ;
3642
43+ const IconComponent : LucideIcon = isCodesVisible ? EyeOff : Eye ;
44+
3745 return (
3846 < Card >
3947 < CardHeader >
4048 < CardTitle className = "flex gap-3" >
41- < LockKeyhole className = "size-4" />
49+ < LockKeyhole className = "size-4" aria-hidden = "true" />
4250 2FA Recovery Codes
4351 </ CardTitle >
4452 < CardDescription >
45- Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.
53+ Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.
4654 </ CardDescription >
4755 </ CardHeader >
4856 < CardContent >
4957 < div className = "flex flex-col gap-3 select-none sm:flex-row sm:items-center sm:justify-between" >
50- < Button onClick = { toggleRecoveryCodesVisibility } className = "w-fit" >
51- { isRecoveryCodesVisible ? < EyeOff className = "size-4" /> : < Eye className = "size-4" /> }
52- { isRecoveryCodesVisible ? 'Hide' : 'View' } Recovery Codes
58+ < Button
59+ onClick = { toggleCodesVisibility }
60+ className = "w-fit"
61+ aria-expanded = { isCodesVisible }
62+ aria-controls = "recovery-codes-section"
63+ >
64+ < IconComponent className = "size-4" aria-hidden = "true" />
65+ { isCodesVisible ? 'Hide' : 'View' } Recovery Codes
5366 </ Button >
5467
55- { isRecoveryCodesVisible && (
68+ { isCodesVisible && (
5669 < Form { ...regenerateRecoveryCodes . form ( ) } options = { { preserveScroll : true } } onSuccess = { fetchRecoveryCodes } >
5770 { ( { processing } ) => (
58- < Button variant = "secondary" type = "submit" disabled = { processing } >
59- < RefreshCw className = { `mr-2 size-4 ${ processing ? 'animate-spin' : '' } ` } />
71+ < Button
72+ variant = "secondary"
73+ type = "submit"
74+ disabled = { processing }
75+ aria-describedby = "regenerate-warning"
76+ >
77+ < RefreshCw
78+ className = { `mr-2 size-4 ${ processing ? 'animate-spin' : '' } ` }
79+ aria-hidden = "true"
80+ />
6081 { processing ? 'Regenerating...' : 'Regenerate Codes' }
6182 </ Button >
6283 ) }
6384 </ Form >
6485 ) }
6586 </ div >
6687 < div
88+ id = "recovery-codes-section"
6789 className = { `relative overflow-hidden transition-all duration-300 ${
68- isRecoveryCodesVisible ? 'h-auto opacity-100' : 'h-0 opacity-0'
90+ isCodesVisible ? 'h-auto opacity-100' : 'h-0 opacity-0'
6991 } `}
92+ aria-hidden = { ! isCodesVisible }
7093 >
7194 < div className = "mt-3 space-y-3" >
72- < div ref = { recoveryCodeSectionRef } className = "grid gap-1 rounded-lg bg-muted p-4 font-mono text-sm" >
95+ < div
96+ ref = { codesSectionRef }
97+ className = "grid gap-1 rounded-lg bg-muted p-4 font-mono text-sm"
98+ role = "list"
99+ aria-label = "Recovery codes"
100+ >
73101 { ! recoveryCodesList . length ? (
74- < div className = "space-y-2" >
75- { Array . from ( { length : 8 } , ( _ , n ) => (
76- < div key = { n } className = "h-4 animate-pulse rounded bg-muted-foreground/20" />
102+ < div className = "space-y-2" aria-label = "Loading recovery codes" >
103+ { Array . from ( { length : 8 } , ( _ , index ) => (
104+ < div
105+ key = { index }
106+ className = "h-4 animate-pulse rounded bg-muted-foreground/20"
107+ aria-hidden = "true"
108+ />
77109 ) ) }
78110 </ div >
79111 ) : (
80- recoveryCodesList . map ( ( code , index ) => < div key = { index } > { code } </ div > )
112+ recoveryCodesList . map ( ( code , index ) => (
113+ < div key = { index } role = "listitem" className = "select-text" >
114+ { code }
115+ </ div >
116+ ) )
81117 ) }
82118 </ div >
83- < p className = "text-xs text-muted-foreground select-none" >
84- Each can be used once to access your account and will be removed after use. If you need more, click{ ' ' }
85- < span className = "font-bold" > Regenerate Codes</ span > above.
86- </ p >
119+ < div className = "text-xs text-muted-foreground select-none" >
120+ < p id = "regenerate-warning" >
121+ Each recovery code can be used once to access your account and will be removed after use.
122+ If you need more, click < span className = "font-bold" > Regenerate Codes</ span > above.
123+ </ p >
124+ </ div >
87125 </ div >
88126 </ div >
89127 </ CardContent >
0 commit comments