Skip to content

Commit 7b21069

Browse files
BillLeoutsakosvl346Bill Leoutsakos
andauthored
refactor(auth): share loading skeleton treatments (#8254)
* refactor(auth): share loading skeleton treatments * test(auth): cover shared loading shapes and composition --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
1 parent c6b49d0 commit 7b21069

7 files changed

Lines changed: 175 additions & 71 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import type { ReactElement } from 'react'
5+
import { renderToStaticMarkup } from 'react-dom/server'
6+
import { describe, expect, it } from 'vitest'
7+
import {
8+
AuthLoadingAlternateActions,
9+
AuthLoadingField,
10+
AuthLoadingFrame,
11+
AuthLoadingSkeleton,
12+
} from '@/app/(auth)/components/auth-loading-skeleton'
13+
14+
function render(element: ReactElement): HTMLDivElement {
15+
const container = document.createElement('div')
16+
container.innerHTML = renderToStaticMarkup(element)
17+
return container
18+
}
19+
20+
describe('auth loading skeletons', () => {
21+
it.each([
22+
['title', 'h-[38px]', 'rounded-[4px]'],
23+
['label', 'h-[14px]', 'rounded-[4px]'],
24+
['control', 'h-[44px]', 'rounded-[10px]'],
25+
['divider', 'h-[1px]', 'rounded-[1px]'],
26+
] as const)('retains the %s placeholder geometry', (shape, height, radius) => {
27+
const container = render(<AuthLoadingSkeleton shape={shape} className='w-[93px]' />)
28+
const skeleton = container.firstElementChild
29+
30+
expect(skeleton?.classList.contains('animate-pulse')).toBe(true)
31+
expect(skeleton?.classList.contains('bg-[var(--surface-active)]')).toBe(true)
32+
expect(skeleton?.classList.contains(height)).toBe(true)
33+
expect(skeleton?.classList.contains(radius)).toBe(true)
34+
expect(skeleton?.classList.contains('w-[93px]')).toBe(true)
35+
})
36+
37+
it('keeps one label, three controls, and one divider in the shared field and actions', () => {
38+
const container = render(
39+
<AuthLoadingFrame>
40+
<AuthLoadingField labelWidthClassName='w-[86px]' />
41+
<AuthLoadingAlternateActions />
42+
</AuthLoadingFrame>
43+
)
44+
const frame = container.firstElementChild
45+
const skeletons = Array.from(container.querySelectorAll('.animate-pulse'))
46+
47+
expect(frame?.classList.contains('flex-col')).toBe(true)
48+
expect(skeletons).toHaveLength(5)
49+
expect(skeletons.filter((el) => el.classList.contains('h-[14px]'))).toHaveLength(1)
50+
expect(skeletons.filter((el) => el.classList.contains('h-[44px]'))).toHaveLength(3)
51+
expect(skeletons.filter((el) => el.classList.contains('h-[1px]'))).toHaveLength(1)
52+
expect(skeletons[0].classList.contains('w-[86px]')).toBe(true)
53+
})
54+
})
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { ReactNode } from 'react'
2+
import { cn, Skeleton } from '@sim/emcn'
3+
import { cva, type VariantProps } from 'class-variance-authority'
4+
5+
const authLoadingSkeletonVariants = cva('', {
6+
variants: {
7+
shape: {
8+
title: 'h-[38px] rounded-[4px]',
9+
label: 'h-[14px] rounded-[4px]',
10+
control: 'h-[44px] rounded-[10px]',
11+
divider: 'h-[1px] rounded-[1px]',
12+
},
13+
},
14+
})
15+
16+
interface AuthLoadingSkeletonProps extends VariantProps<typeof authLoadingSkeletonVariants> {
17+
className?: string
18+
}
19+
20+
/** Placeholder geometry shared by the auth route loading states. */
21+
export function AuthLoadingSkeleton({ shape, className }: AuthLoadingSkeletonProps) {
22+
return <Skeleton className={cn(authLoadingSkeletonVariants({ shape }), className)} />
23+
}
24+
25+
interface AuthLoadingFrameProps {
26+
children: ReactNode
27+
}
28+
29+
export function AuthLoadingFrame({ children }: AuthLoadingFrameProps) {
30+
return <div className='flex flex-col items-center'>{children}</div>
31+
}
32+
33+
interface AuthLoadingFieldProps {
34+
labelWidthClassName: string
35+
className?: string
36+
}
37+
38+
export function AuthLoadingField({ labelWidthClassName, className }: AuthLoadingFieldProps) {
39+
return (
40+
<div className={cn('w-full space-y-2', className)}>
41+
<AuthLoadingSkeleton shape='label' className={labelWidthClassName} />
42+
<AuthLoadingSkeleton shape='control' className='w-full' />
43+
</div>
44+
)
45+
}
46+
47+
/** The divider and paired provider-action placeholders on login and signup. */
48+
export function AuthLoadingAlternateActions() {
49+
return (
50+
<>
51+
<AuthLoadingSkeleton shape='divider' className='mt-6 w-full' />
52+
<div className='mt-6 flex w-full gap-3'>
53+
<AuthLoadingSkeleton shape='control' className='flex-1' />
54+
<AuthLoadingSkeleton shape='control' className='flex-1' />
55+
</div>
56+
</>
57+
)
58+
}
Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
1-
import { Skeleton } from '@sim/emcn'
1+
import {
2+
AuthLoadingAlternateActions,
3+
AuthLoadingField,
4+
AuthLoadingFrame,
5+
AuthLoadingSkeleton,
6+
} from '@/app/(auth)/components/auth-loading-skeleton'
27

38
export default function LoginLoading() {
49
return (
5-
<div className='flex flex-col items-center'>
6-
<Skeleton className='h-[38px] w-[80px] rounded-[4px]' />
7-
<div className='mt-8 w-full space-y-2'>
8-
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
9-
<Skeleton className='h-[44px] w-full rounded-[10px]' />
10-
</div>
11-
<div className='mt-4 w-full space-y-2'>
12-
<Skeleton className='h-[14px] w-[64px] rounded-[4px]' />
13-
<Skeleton className='h-[44px] w-full rounded-[10px]' />
14-
</div>
15-
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
16-
<Skeleton className='mt-6 h-[1px] w-full rounded-[1px]' />
17-
<div className='mt-6 flex w-full gap-3'>
18-
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
19-
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
20-
</div>
21-
<Skeleton className='mt-6 h-[14px] w-[200px] rounded-[4px]' />
22-
</div>
10+
<AuthLoadingFrame>
11+
<AuthLoadingSkeleton shape='title' className='w-[80px]' />
12+
<AuthLoadingField labelWidthClassName='w-[40px]' className='mt-8' />
13+
<AuthLoadingField labelWidthClassName='w-[64px]' className='mt-4' />
14+
<AuthLoadingSkeleton shape='control' className='mt-6 w-full' />
15+
<AuthLoadingAlternateActions />
16+
<AuthLoadingSkeleton shape='label' className='mt-6 w-[200px]' />
17+
</AuthLoadingFrame>
2318
)
2419
}
Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { Skeleton } from '@sim/emcn'
1+
import {
2+
AuthLoadingField,
3+
AuthLoadingFrame,
4+
AuthLoadingSkeleton,
5+
} from '@/app/(auth)/components/auth-loading-skeleton'
26

37
export default function ResetPasswordLoading() {
48
return (
5-
<div className='flex flex-col items-center'>
6-
<Skeleton className='h-[38px] w-[160px] rounded-[4px]' />
7-
<Skeleton className='mt-3 h-[14px] w-[280px] rounded-[4px]' />
8-
<div className='mt-8 w-full space-y-2'>
9-
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
10-
<Skeleton className='h-[44px] w-full rounded-[10px]' />
11-
</div>
12-
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
13-
<Skeleton className='mt-6 h-[14px] w-[120px] rounded-[4px]' />
14-
</div>
9+
<AuthLoadingFrame>
10+
<AuthLoadingSkeleton shape='title' className='w-[160px]' />
11+
<AuthLoadingSkeleton shape='label' className='mt-3 w-[280px]' />
12+
<AuthLoadingField labelWidthClassName='w-[40px]' className='mt-8' />
13+
<AuthLoadingSkeleton shape='control' className='mt-6 w-full' />
14+
<AuthLoadingSkeleton shape='label' className='mt-6 w-[120px]' />
15+
</AuthLoadingFrame>
1516
)
1617
}
Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,20 @@
1-
import { Skeleton } from '@sim/emcn'
1+
import {
2+
AuthLoadingAlternateActions,
3+
AuthLoadingField,
4+
AuthLoadingFrame,
5+
AuthLoadingSkeleton,
6+
} from '@/app/(auth)/components/auth-loading-skeleton'
27

38
export default function SignupLoading() {
49
return (
5-
<div className='flex flex-col items-center'>
6-
<Skeleton className='h-[38px] w-[100px] rounded-[4px]' />
7-
<div className='mt-8 w-full space-y-2'>
8-
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
9-
<Skeleton className='h-[44px] w-full rounded-[10px]' />
10-
</div>
11-
<div className='mt-4 w-full space-y-2'>
12-
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
13-
<Skeleton className='h-[44px] w-full rounded-[10px]' />
14-
</div>
15-
<div className='mt-4 w-full space-y-2'>
16-
<Skeleton className='h-[14px] w-[64px] rounded-[4px]' />
17-
<Skeleton className='h-[44px] w-full rounded-[10px]' />
18-
</div>
19-
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
20-
<Skeleton className='mt-6 h-[1px] w-full rounded-[1px]' />
21-
<div className='mt-6 flex w-full gap-3'>
22-
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
23-
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
24-
</div>
25-
<Skeleton className='mt-6 h-[14px] w-[220px] rounded-[4px]' />
26-
</div>
10+
<AuthLoadingFrame>
11+
<AuthLoadingSkeleton shape='title' className='w-[100px]' />
12+
<AuthLoadingField labelWidthClassName='w-[40px]' className='mt-8' />
13+
<AuthLoadingField labelWidthClassName='w-[40px]' className='mt-4' />
14+
<AuthLoadingField labelWidthClassName='w-[64px]' className='mt-4' />
15+
<AuthLoadingSkeleton shape='control' className='mt-6 w-full' />
16+
<AuthLoadingAlternateActions />
17+
<AuthLoadingSkeleton shape='label' className='mt-6 w-[220px]' />
18+
</AuthLoadingFrame>
2719
)
2820
}
Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { Skeleton } from '@sim/emcn'
1+
import {
2+
AuthLoadingField,
3+
AuthLoadingFrame,
4+
AuthLoadingSkeleton,
5+
} from '@/app/(auth)/components/auth-loading-skeleton'
26

37
export default function SSOLoading() {
48
return (
5-
<div className='flex flex-col items-center'>
6-
<Skeleton className='h-[38px] w-[120px] rounded-[4px]' />
7-
<Skeleton className='mt-3 h-[14px] w-[260px] rounded-[4px]' />
8-
<div className='mt-8 w-full space-y-2'>
9-
<Skeleton className='h-[14px] w-[80px] rounded-[4px]' />
10-
<Skeleton className='h-[44px] w-full rounded-[10px]' />
11-
</div>
12-
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
13-
<Skeleton className='mt-6 h-[14px] w-[120px] rounded-[4px]' />
14-
</div>
9+
<AuthLoadingFrame>
10+
<AuthLoadingSkeleton shape='title' className='w-[120px]' />
11+
<AuthLoadingSkeleton shape='label' className='mt-3 w-[260px]' />
12+
<AuthLoadingField labelWidthClassName='w-[80px]' className='mt-8' />
13+
<AuthLoadingSkeleton shape='control' className='mt-6 w-full' />
14+
<AuthLoadingSkeleton shape='label' className='mt-6 w-[120px]' />
15+
</AuthLoadingFrame>
1516
)
1617
}
Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { Skeleton } from '@sim/emcn'
1+
import {
2+
AuthLoadingFrame,
3+
AuthLoadingSkeleton,
4+
} from '@/app/(auth)/components/auth-loading-skeleton'
25

36
export default function VerifyLoading() {
47
return (
5-
<div className='flex flex-col items-center'>
6-
<Skeleton className='h-[38px] w-[180px] rounded-[4px]' />
7-
<Skeleton className='mt-3 h-[14px] w-[300px] rounded-[4px]' />
8-
<Skeleton className='mt-1 h-[14px] w-[240px] rounded-[4px]' />
9-
<Skeleton className='mt-8 h-[44px] w-full rounded-[10px]' />
10-
</div>
8+
<AuthLoadingFrame>
9+
<AuthLoadingSkeleton shape='title' className='w-[180px]' />
10+
<AuthLoadingSkeleton shape='label' className='mt-3 w-[300px]' />
11+
<AuthLoadingSkeleton shape='label' className='mt-1 w-[240px]' />
12+
<AuthLoadingSkeleton shape='control' className='mt-8 w-full' />
13+
</AuthLoadingFrame>
1114
)
1215
}

0 commit comments

Comments
 (0)