Skip to content

Commit 8f2d9c7

Browse files
committed
feat: add CircularProgress component with live demo and tests
1 parent caf900f commit 8f2d9c7

4 files changed

Lines changed: 186 additions & 0 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
---
3+
4+
import { LiveProvider, LiveEditor, LiveError, LivePreview } from 'react-live'
5+
import AutoPropsTable from '../../src/components/auto-props-table'
6+
import { CircularProgress } from 'react-loader-spinner'
7+
8+
# CircularProgress
9+
10+
<LiveProvider
11+
code={`
12+
render(<CircularProgress
13+
height="100"
14+
width="100"
15+
color="#4fa94d"
16+
ariaLabel="circular-progress-loading"
17+
wrapperStyle={{}}
18+
wrapperClass="wrapper-class"
19+
visible={true}
20+
strokeWidth={2}
21+
animationDuration={1}
22+
/>)
23+
`}
24+
scope={{ CircularProgress }}
25+
noInline={true}
26+
>
27+
<LivePreview />
28+
<br />
29+
<LiveEditor />
30+
<LiveError />
31+
</LiveProvider>
32+
33+
---
34+
35+
### Props
36+
37+
<AutoPropsTable component="CircularProgress" exclude={["gradientType", "gradientAngle", "colors"]} />

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ export { Discuss } from './loader/discuss'
3333
export { ColorRing } from './loader/color-ring'
3434
export { Comment } from './loader/comment'
3535
export { Blocks } from './loader/blocks'
36+
export { CircularProgress } from './loader/circular-progress'
3637
export { Hourglass } from './loader/hourglass'

src/loader/circular-progress.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React, { FunctionComponent, ReactElement } from 'react'
2+
import { BaseProps, DEFAULT_COLOR, DEFAULT_WAI_ARIA_ATTRIBUTE } from '../type'
3+
import { SvgWrapper } from '../shared/svg-wrapper'
4+
import { SVG_NAMESPACE } from '../shared/constants'
5+
6+
interface CircularProgressProps extends BaseProps {
7+
strokeWidth?: string | number
8+
secondaryColor?: string
9+
animationDuration?: string | number
10+
}
11+
12+
export const CircularProgress: FunctionComponent<CircularProgressProps> = ({
13+
height = '100',
14+
width = '100',
15+
color = DEFAULT_COLOR,
16+
secondaryColor,
17+
ariaLabel = 'circular-progress-loading',
18+
wrapperStyle = {},
19+
wrapperClass,
20+
visible = true,
21+
strokeWidth = 2,
22+
animationDuration = 1,
23+
}): ReactElement => (
24+
<SvgWrapper
25+
$visible={visible}
26+
style={{ ...wrapperStyle }}
27+
className={wrapperClass}
28+
data-testid="circular-progress-loading"
29+
aria-label={ariaLabel}
30+
{...DEFAULT_WAI_ARIA_ATTRIBUTE}
31+
>
32+
<svg
33+
height={`${height}`}
34+
width={`${width}`}
35+
fill="none"
36+
viewBox="0 0 16 16"
37+
xmlns={SVG_NAMESPACE}
38+
data-testid="circular-progress-svg"
39+
style={{
40+
animation: `spin ${animationDuration}s linear infinite`,
41+
}}
42+
>
43+
<title>Circular Progress</title>
44+
<desc>Animated circular progress indicator</desc>
45+
<style>
46+
{`
47+
@keyframes spin {
48+
from { transform: rotate(0deg); }
49+
to { transform: rotate(360deg); }
50+
}
51+
`}
52+
</style>
53+
{/* Background circle with opacity */}
54+
<path
55+
fill="none"
56+
stroke={secondaryColor || color}
57+
strokeWidth={strokeWidth}
58+
d="M3.05 3.05a7 7 0 1 1 9.9 9.9 7 7 0 0 1-9.9-9.9Z"
59+
opacity="0.5"
60+
/>
61+
{/* Inner filled circle */}
62+
<path
63+
fill={color}
64+
fillRule="evenodd"
65+
d="M8 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z"
66+
clipRule="evenodd"
67+
/>
68+
{/* Progress arc */}
69+
<path
70+
fill={color}
71+
d="M14 8a6 6 0 0 0-6-6V0a8 8 0 0 1 8 8h-2Z"
72+
/>
73+
</svg>
74+
</SvgWrapper>
75+
)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from 'react'
2+
import { CircularProgress } from '../../src'
3+
import { render, screen } from '@testing-library/react'
4+
5+
describe('CircularProgress Loader', () => {
6+
it('Should render without crashing', () => {
7+
render(<CircularProgress wrapperClass="test-class" wrapperStyle={{ opacity: '1' }} />)
8+
const loader = screen.getByTestId('circular-progress-loading')
9+
expect(loader).toBeInTheDocument()
10+
expect(loader).toHaveStyle('display: flex')
11+
expect(loader).toHaveAttribute('aria-label', 'circular-progress-loading')
12+
expect(loader).toHaveAttribute('aria-busy', 'true')
13+
expect(loader).toHaveAttribute('role', 'progressbar')
14+
expect(loader).toHaveClass('test-class')
15+
expect(loader).toHaveStyle('opacity:1')
16+
17+
const svg = screen.getByTestId('circular-progress-svg')
18+
expect(svg).toBeInTheDocument()
19+
expect(svg).toHaveAttribute('height', '100')
20+
expect(svg).toHaveAttribute('width', '100')
21+
expect(svg).toHaveAttribute('fill', 'none')
22+
})
23+
24+
it('Should apply props passed externally', () => {
25+
render(
26+
<CircularProgress
27+
wrapperClass="test-class"
28+
wrapperStyle={{ opacity: '1' }}
29+
width="200"
30+
height="200"
31+
ariaLabel="test-aria-label"
32+
color="red"
33+
secondaryColor="blue"
34+
strokeWidth="4"
35+
animationDuration="2"
36+
visible={false}
37+
/>
38+
)
39+
const loader = screen.getByTestId('circular-progress-loading')
40+
expect(loader).toBeInTheDocument()
41+
expect(loader).toHaveAttribute('aria-label', 'test-aria-label')
42+
expect(loader).toHaveClass('test-class')
43+
expect(loader).toHaveStyle('opacity:1')
44+
expect(loader).toHaveStyle('display:none')
45+
46+
const svg = screen.getByTestId('circular-progress-svg')
47+
expect(svg).toBeInTheDocument()
48+
expect(svg).toHaveAttribute('height', '200')
49+
expect(svg).toHaveAttribute('width', '200')
50+
expect(svg).toHaveStyle('animation: spin 2s linear infinite')
51+
})
52+
53+
it('Should use default color when secondaryColor is not provided', () => {
54+
render(<CircularProgress color="green" />)
55+
const svg = screen.getByTestId('circular-progress-svg')
56+
const backgroundPath = svg.querySelector('path[opacity="0.5"]')
57+
expect(backgroundPath).toHaveAttribute('stroke', 'green')
58+
})
59+
60+
it('Should use secondaryColor when provided', () => {
61+
render(<CircularProgress color="green" secondaryColor="red" />)
62+
const svg = screen.getByTestId('circular-progress-svg')
63+
const backgroundPath = svg.querySelector('path[opacity="0.5"]')
64+
expect(backgroundPath).toHaveAttribute('stroke', 'red')
65+
})
66+
67+
it('Should apply custom strokeWidth', () => {
68+
render(<CircularProgress strokeWidth="3" />)
69+
const svg = screen.getByTestId('circular-progress-svg')
70+
const backgroundPath = svg.querySelector('path[opacity="0.5"]')
71+
expect(backgroundPath).toHaveAttribute('stroke-width', '3')
72+
})
73+
})

0 commit comments

Comments
 (0)