Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rotten-mugs-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

feat: make Spinner's delay customizable
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/react/src/Spinner/Spinner.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
},
{
"defaultValue": "false",
"description": "Whether to delay the spinner before rendering by the defined 1000ms.",
"description": "Controls whether and how long to delay rendering the spinner. Set to `true` to delay by 1000ms, `'short'` to delay by 300ms, `'long'` to delay by 1000ms, or provide a custom number of milliseconds.",
"name": "delay",
"type": "boolean"
"type": "boolean | 'short' | 'long' | number"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

boolean will be going away after some dotcom updates, to align with SkeletonBox

}
],
"status": "alpha",
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/Spinner/Spinner.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ export const SuppressScreenReaderText = () => (
</Stack>
)

export const WithDelay = () => <Spinner delay />
export const WithDelay = () => <Spinner delay="long" />
135 changes: 133 additions & 2 deletions packages/react/src/Spinner/Spinner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ describe('Spinner', () => {
expect(container.querySelector('svg')).toBeInTheDocument()
})

it('should render immediately when no delay is provided', () => {
const {container} = render(<Spinner />)
expect(container.querySelector('svg')).toBeInTheDocument()
})

it('should not render immediately when delay is true', () => {
const {container} = render(<Spinner delay={true} />)
expect(container.querySelector('svg')).not.toBeInTheDocument()
Expand All @@ -73,9 +78,96 @@ describe('Spinner', () => {
// Not visible initially
expect(container.querySelector('svg')).not.toBeInTheDocument()

// Advance timers by 1000ms
// Advance timers by less than 1000ms
act(() => {
vi.advanceTimersByTime(800)
})

// Still not visible
expect(container.querySelector('svg')).not.toBeInTheDocument()

// Advance timers to complete the delay (1000ms)
act(() => {
vi.advanceTimersByTime(1000)
vi.advanceTimersByTime(200)
})

// Now it should be visible
expect(container.querySelector('svg')).toBeInTheDocument()
})

it('should not render immediately when delay is "short"', () => {
const {container} = render(<Spinner delay="short" />)
expect(container.querySelector('svg')).not.toBeInTheDocument()
})

it('should render after 300ms when delay is "short"', () => {
const {container} = render(<Spinner delay="short" />)

// Not visible initially
expect(container.querySelector('svg')).not.toBeInTheDocument()

// Advance timers by less than 300ms
act(() => {
vi.advanceTimersByTime(250)
})

// Still not visible
expect(container.querySelector('svg')).not.toBeInTheDocument()

// Advance timers to complete the short delay (300ms)
act(() => {
vi.advanceTimersByTime(50)
})

// Now it should be visible
expect(container.querySelector('svg')).toBeInTheDocument()
})

it('should not render immediately when delay is "long"', () => {
const {container} = render(<Spinner delay="long" />)
expect(container.querySelector('svg')).not.toBeInTheDocument()
})

it('should render after 1000ms when delay is "long"', () => {
const {container} = render(<Spinner delay="long" />)

// Not visible initially
expect(container.querySelector('svg')).not.toBeInTheDocument()

// Advance timers by less than 1000ms
act(() => {
vi.advanceTimersByTime(800)
})

// Still not visible
expect(container.querySelector('svg')).not.toBeInTheDocument()

// Advance timers to complete the long delay (1000ms)
act(() => {
vi.advanceTimersByTime(200)
})

// Now it should be visible
expect(container.querySelector('svg')).toBeInTheDocument()
})

it('should render after custom ms when delay is a number', () => {
const {container} = render(<Spinner delay={500} />)

// Not visible initially
expect(container.querySelector('svg')).not.toBeInTheDocument()

// Advance timers by less than the custom delay (500ms)
act(() => {
vi.advanceTimersByTime(400)
})

// Still not visible
expect(container.querySelector('svg')).not.toBeInTheDocument()

// Advance timers to complete the custom delay
act(() => {
vi.advanceTimersByTime(100)
})

// Now it should be visible
Expand All @@ -94,5 +186,44 @@ describe('Spinner', () => {
// No errors should occur
expect(true).toBe(true)
})

it('should cleanup timeout on unmount when delay is "short"', () => {
const {unmount} = render(<Spinner delay="short" />)

// Unmount before the delay completes
unmount()

// Advance timers to see if there are any side effects
vi.advanceTimersByTime(300)

// No errors should occur
expect(true).toBe(true)
})

it('should cleanup timeout on unmount when delay is "long"', () => {
const {unmount} = render(<Spinner delay="long" />)

// Unmount before the delay completes
unmount()

// Advance timers to see if there are any side effects
vi.advanceTimersByTime(1000)

// No errors should occur
expect(true).toBe(true)
})

it('should cleanup timeout on unmount when delay is a number', () => {
const {unmount} = render(<Spinner delay={500} />)

// Unmount before the delay completes
unmount()

// Advance timers to see if there are any side effects
vi.advanceTimersByTime(500)

// No errors should occur
expect(true).toBe(true)
})
})
})
7 changes: 4 additions & 3 deletions packages/react/src/Spinner/Spinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export type SpinnerProps = {
'aria-label'?: string
className?: string
style?: React.CSSProperties
/** Whether to delay the spinner before rendering by the defined 1000ms. */
delay?: boolean
/** Controls whether and how long to delay rendering the spinner. Set to `true` to delay by 1000ms, `'short'` to delay by 300ms, `'long'` to delay by 1000ms, or provide a custom number of milliseconds. */
delay?: boolean | 'short' | 'long' | number
} & HTMLDataAttributes

function Spinner({
Expand All @@ -46,9 +46,10 @@ function Spinner({

useEffect(() => {
if (delay) {
const delayDuration = typeof delay === 'number' ? delay : delay === 'short' ? 300 : 1000
const timeoutId = setTimeout(() => {
setIsVisible(true)
}, 1000)
}, delayDuration)

return () => clearTimeout(timeoutId)
}
Expand Down
Loading