Skip to content

Commit fd3d09e

Browse files
authored
feat(Spinner): Adds a delay prop to the Spinner component that delays rendering by 1000ms. (#7059)
1 parent 26b45ba commit fd3d09e

File tree

5 files changed

+114
-28
lines changed

5 files changed

+114
-28
lines changed

.changeset/shaggy-pants-remain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/react": minor
3+
---
4+
5+
feat(Spinner): Adds a delay prop to the Spinner component that delays rendering by 1000ms.
Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,60 @@
11
{
2+
"a11yReviewed": "2025-01-08",
23
"id": "spinner",
4+
"importPath": "@primer/react",
35
"name": "Spinner",
4-
"status": "alpha",
5-
"a11yReviewed": "2025-01-08",
6-
"stories": [
6+
"props": [
77
{
8-
"id": "components-spinner--default"
8+
"description": "Sets the width and height of the spinner.",
9+
"name": "size",
10+
"type": "'small' | 'medium' | 'large'"
911
},
1012
{
11-
"id": "components-spinner-features--small"
13+
"defaultValue": "Loading",
14+
"description": "Sets the text conveyed by assistive technologies such as screen readers. Set to `null` if the loading state is displayed in a text node somewhere else on the page.",
15+
"name": "srText",
16+
"type": "string | null"
1217
},
1318
{
14-
"id": "components-spinner-features--large"
19+
"deprecated": true,
20+
"description": "Sets the text conveyed by assistive technologies such as screen readers.",
21+
"name": "aria-label",
22+
"type": "string"
1523
},
1624
{
17-
"id": "components-spinner-features--suppress-screen-reader-text"
25+
"defaultValue": "",
26+
"description": "",
27+
"name": "className",
28+
"type": "string"
29+
},
30+
{
31+
"name": "data-*",
32+
"type": "string"
33+
},
34+
{
35+
"defaultValue": "false",
36+
"description": "Whether to delay the spinner before rendering by the defined 1000ms.",
37+
"name": "delay",
38+
"type": "boolean"
1839
}
1940
],
20-
"importPath": "@primer/react",
21-
"props": [
41+
"status": "alpha",
42+
"stories": [
2243
{
23-
"name": "size",
24-
"type": "'small' | 'medium' | 'large'",
25-
"description": "Sets the width and height of the spinner."
44+
"id": "components-spinner--default"
2645
},
2746
{
28-
"name": "srText",
29-
"type": "string | null",
30-
"defaultValue": "Loading",
31-
"description": "Sets the text conveyed by assistive technologies such as screen readers. Set to `null` if the loading state is displayed in a text node somewhere else on the page."
47+
"id": "components-spinner-features--small"
3248
},
3349
{
34-
"name": "aria-label",
35-
"type": "string",
36-
"description": "Sets the text conveyed by assistive technologies such as screen readers.",
37-
"deprecated": true
50+
"id": "components-spinner-features--large"
3851
},
3952
{
40-
"name": "className",
41-
"type": "string",
42-
"description": "",
43-
"defaultValue": ""
53+
"id": "components-spinner-features--suppress-screen-reader-text"
4454
},
4555
{
46-
"name": "data-*",
47-
"type": "string"
56+
"id": "components-spinner-features--with-delay"
4857
}
4958
],
5059
"subcomponents": []
51-
}
60+
}

packages/react/src/Spinner/Spinner.features.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ export const SuppressScreenReaderText = () => (
1919
<AriaStatus>Loading...</AriaStatus>
2020
</Stack>
2121
)
22+
23+
export const WithDelay = () => <Spinner delay />

packages/react/src/Spinner/Spinner.test.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type {SpinnerProps} from '..'
22
import {Spinner} from '..'
33
import {render, screen} from '@testing-library/react'
4-
import {describe, expect, it} from 'vitest'
4+
import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
5+
import {act} from 'react'
56

67
describe('Spinner', () => {
78
it('should support `className` on the outermost element', () => {
@@ -46,4 +47,53 @@ describe('Spinner', () => {
4647
expectSize('medium', '32px')
4748
expectSize('large', '64px')
4849
})
50+
51+
describe('delay behavior', () => {
52+
beforeEach(() => {
53+
vi.useFakeTimers()
54+
})
55+
56+
afterEach(() => {
57+
vi.restoreAllMocks()
58+
vi.useRealTimers()
59+
})
60+
61+
it('should render immediately when delay is false', () => {
62+
const {container} = render(<Spinner delay={false} />)
63+
expect(container.querySelector('svg')).toBeInTheDocument()
64+
})
65+
66+
it('should not render immediately when delay is true', () => {
67+
const {container} = render(<Spinner delay={true} />)
68+
expect(container.querySelector('svg')).not.toBeInTheDocument()
69+
})
70+
71+
it('should render after 1000ms when delay is true', () => {
72+
const {container} = render(<Spinner delay={true} />)
73+
74+
// Not visible initially
75+
expect(container.querySelector('svg')).not.toBeInTheDocument()
76+
77+
// Advance timers by 1000ms
78+
act(() => {
79+
vi.advanceTimersByTime(1000)
80+
})
81+
82+
// Now it should be visible
83+
expect(container.querySelector('svg')).toBeInTheDocument()
84+
})
85+
86+
it('should cleanup timeout on unmount when delay is true', () => {
87+
const {unmount} = render(<Spinner delay={true} />)
88+
89+
// Unmount before the delay completes
90+
unmount()
91+
92+
// Advance timers to see if there are any side effects
93+
vi.advanceTimersByTime(1000)
94+
95+
// No errors should occur
96+
expect(true).toBe(true)
97+
})
98+
})
4999
})

packages/react/src/Spinner/Spinner.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type React from 'react'
2+
import {useState, useEffect} from 'react'
23
import {VisuallyHidden} from '../VisuallyHidden'
34
import type {HTMLDataAttributes} from '../internal/internal-types'
45
import {useId} from '../hooks'
@@ -20,6 +21,8 @@ export type SpinnerProps = {
2021
'aria-label'?: string
2122
className?: string
2223
style?: React.CSSProperties
24+
/** Whether to delay the spinner before rendering by the defined 1000ms. */
25+
delay?: boolean
2326
} & HTMLDataAttributes
2427

2528
function Spinner({
@@ -28,12 +31,29 @@ function Spinner({
2831
'aria-label': ariaLabel,
2932
className,
3033
style,
34+
delay = false,
3135
...props
3236
}: SpinnerProps) {
3337
const size = sizeMap[sizeKey]
3438
const hasHiddenLabel = srText !== null && ariaLabel === undefined
3539
const labelId = useId()
3640

41+
const [isVisible, setIsVisible] = useState(!delay)
42+
43+
useEffect(() => {
44+
if (delay) {
45+
const timeoutId = setTimeout(() => {
46+
setIsVisible(true)
47+
}, 1000)
48+
49+
return () => clearTimeout(timeoutId)
50+
}
51+
}, [delay])
52+
53+
if (!isVisible) {
54+
return null
55+
}
56+
3757
return (
3858
/* inline-flex removes the extra line height */
3959
<span className={classes.Box}>

0 commit comments

Comments
 (0)