Skip to content

Conversation

@jonrohan
Copy link
Member

@jonrohan jonrohan commented Oct 23, 2025

Closes https://github.com/github/primer/issues/5907

Adds a delay prop to the Spinner component that delays rendering by 1000ms. This helps prevent spinner flash for quick loading states, improving the user experience by avoiding jarring visual changes when content loads quickly.

Changelog

New

  • Added delay prop (boolean, defaults to false) to Spinner component that delays rendering by 1000ms
  • Added WithDelay story to demonstrate the delay functionality

Changed

  • Fixed initial visibility state logic in Spinner component to properly support the delay prop

Removed

N/A

Rollout strategy

  • Minor release
  • Patch release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

This is a new feature that adds an optional prop with a safe default value (false), making it backwards compatible.

Testing & Reviewing

  1. View the Storybook story: Check the "With Delay" story in Components/Spinner/Features to see the delay in action
  2. Run unit tests: The PR includes 4 new test cases covering:
    • Immediate rendering when delay={false} (default behavior)
    • No initial rendering when delay={true}
    • Rendering after 1000ms when delay={true}
    • Proper cleanup of timeout on unmount

Merge checklist

@changeset-bot
Copy link

changeset-bot bot commented Oct 23, 2025

🦋 Changeset detected

Latest commit: c3e91bd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added staff Author is a staff member integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm labels Oct 23, 2025
@github-actions
Copy link
Contributor

👋 Hi, this pull request contains changes to the source code that github/github depends on. If you are GitHub staff, we recommend testing these changes with github/github using the integration workflow. Thanks!

Changed the version of @primer/react from patch to minor and added a delay prop to the Spinner component.
@github-actions github-actions bot requested a deployment to storybook-preview-7059 October 23, 2025 19:14 Abandoned
@github-actions github-actions bot temporarily deployed to storybook-preview-7059 October 23, 2025 19:23 Inactive
Comment on lines 24 to 25
/** Number of milliseconds to delay the spinner before rendering. */
delay?: number
Copy link
Member

Choose a reason for hiding this comment

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

One idea for this with our guidelines could be to have it be a boolean that uses our 1000ms value by default instead of everyone needing to supply that value manually.

@github-actions github-actions bot temporarily deployed to storybook-preview-7059 November 13, 2025 22:54 Inactive
@jonrohan jonrohan changed the title Add a delay prop to set how long to wait before rendering the Spinner feat(Spinner): Adds a delay prop to the Spinner component that delays rendering by 1000ms. Nov 13, 2025
@jonrohan jonrohan marked this pull request as ready for review November 13, 2025 22:56
@jonrohan jonrohan requested a review from a team as a code owner November 13, 2025 22:56
Copilot finished reviewing on behalf of jonrohan November 13, 2025 23:00
@github-actions github-actions bot requested a deployment to storybook-preview-7059 November 13, 2025 23:00 Abandoned
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds a delay prop to the Spinner component that delays rendering by 1000ms, helping to prevent spinner flash for quick loading states and improving user experience.

Key Changes

  • Added delay boolean prop (defaults to false) that delays spinner rendering by 1000ms using React hooks
  • Implemented proper timeout cleanup on component unmount
  • Added comprehensive test coverage with 4 new test cases covering immediate rendering, delayed rendering, and cleanup behavior

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/react/src/Spinner/Spinner.tsx Added delay logic using useState and useEffect to conditionally render spinner after 1000ms timeout
packages/react/src/Spinner/Spinner.test.tsx Added fake timer setup and 4 test cases to verify delay behavior and cleanup
packages/react/src/Spinner/Spinner.features.stories.tsx Added WithDelay story to demonstrate the delay functionality
packages/react/src/Spinner/Spinner.docs.json Updated documentation to include delay prop definition and new story reference
.changeset/shaggy-pants-remain.md Added changeset marking this as a minor release

Comment on lines 8 to 15
beforeEach(() => {
vi.useFakeTimers()
})

afterEach(() => {
vi.restoreAllMocks()
vi.useRealTimers()
})
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The fake timers setup in beforeEach affects all tests in the file, including existing tests that don't need fake timers. This could cause issues with tests that rely on real timers or async behavior (e.g., tests that use waitFor or other time-dependent operations).

Consider scoping the fake timer setup to only the delay-related tests by moving it inside those specific test cases or creating a separate describe block for delay tests:

describe('delay behavior', () => {
  beforeEach(() => {
    vi.useFakeTimers()
  })

  afterEach(() => {
    vi.restoreAllMocks()
    vi.useRealTimers()
  })

  it('should render immediately when delay is false', () => {
    // test code
  })
  // other delay tests...
})

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 60
{
"a11yReviewed": "2025-01-08",
"id": "spinner",
"importPath": "@primer/react",
"name": "Spinner",
"status": "alpha",
"a11yReviewed": "2025-01-08",
"stories": [
"props": [
{
"id": "components-spinner--default"
"description": "Sets the width and height of the spinner.",
"name": "size",
"type": "'small' | 'medium' | 'large'"
},
{
"id": "components-spinner-features--small"
"defaultValue": "Loading",
"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.",
"name": "srText",
"type": "string | null"
},
{
"id": "components-spinner-features--large"
"deprecated": true,
"description": "Sets the text conveyed by assistive technologies such as screen readers.",
"name": "aria-label",
"type": "string"
},
{
"id": "components-spinner-features--suppress-screen-reader-text"
"defaultValue": "",
"description": "",
"name": "className",
"type": "string"
},
{
"name": "data-*",
"type": "string"
},
{
"defaultValue": "false",
"description": "Whether to delay the spinner before rendering by the defined 1000ms.",
"name": "delay",
"type": "boolean"
}
],
"importPath": "@primer/react",
"props": [
"status": "alpha",
"stories": [
{
"name": "size",
"type": "'small' | 'medium' | 'large'",
"description": "Sets the width and height of the spinner."
"id": "components-spinner--default"
},
{
"name": "srText",
"type": "string | null",
"defaultValue": "Loading",
"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."
"id": "components-spinner-features--small"
},
{
"name": "aria-label",
"type": "string",
"description": "Sets the text conveyed by assistive technologies such as screen readers.",
"deprecated": true
"id": "components-spinner-features--large"
},
{
"name": "className",
"type": "string",
"description": "",
"defaultValue": ""
"id": "components-spinner-features--suppress-screen-reader-text"
},
{
"name": "data-*",
"type": "string"
"id": "components-spinner-features--with-delay"
}
],
"subcomponents": []
}
}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The entire file has been reformatted with fields reordered (e.g., a11yReviewed and importPath moved before their original positions). This appears to be unintentional reformatting unrelated to the delay feature.

Based on other .docs.json files in the codebase (e.g., Button.docs.json, Stack.docs.json), the standard field order is: id, name, status, a11yReviewed, stories, importPath, props, subcomponents.

Consider reverting the file to its original structure and only adding the new delay prop entry and the new story reference, without changing the order of existing fields.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +49
const timeoutId = setTimeout(() => {
setIsVisible(true)
}, 1000)

return () => clearTimeout(timeoutId)
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The useEffect dependency array only includes [delay], but if the delay prop changes from false to true or vice versa after the component has mounted, the behavior might be unexpected:

  1. If delay changes from false to true after mount, the spinner will remain visible (since isVisible is already true)
  2. If delay changes from true to false after the timeout has started, a new timeout won't be cleared, but the component will start a new timeout

While changing the delay prop after mount is likely an edge case, consider adding a comment documenting that the delay prop should not be changed after mount, or add logic to handle prop changes:

useEffect(() => {
  if (delay) {
    setIsVisible(false) // Reset visibility when delay prop changes
    const timeoutId = setTimeout(() => {
      setIsVisible(true)
    }, 1000)

    return () => clearTimeout(timeoutId)
  } else {
    setIsVisible(true) // Immediately show when delay is removed
  }
}, [delay])
Suggested change
const timeoutId = setTimeout(() => {
setIsVisible(true)
}, 1000)
return () => clearTimeout(timeoutId)
setIsVisible(false) // Reset visibility when delay prop changes
const timeoutId = setTimeout(() => {
setIsVisible(true)
}, 1000)
return () => clearTimeout(timeoutId)
} else {
setIsVisible(true) // Immediately show when delay is removed

Copilot uses AI. Check for mistakes.
if (delay) {
const timeoutId = setTimeout(() => {
setIsVisible(true)
}, 1000)
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

[nitpick] The delay duration of 1000 milliseconds is hardcoded in multiple places (here and in the JSDoc comment on line 24). Consider extracting this to a named constant at the top of the file to improve maintainability:

const SPINNER_DELAY_MS = 1000;

This makes it easier to adjust the delay in the future and ensures consistency between the implementation and documentation.

Copilot uses AI. Check for mistakes.
@jonrohan jonrohan enabled auto-merge November 13, 2025 23:13
@jonrohan jonrohan added this pull request to the merge queue Nov 13, 2025
Merged via the queue into main with commit fd3d09e Nov 13, 2025
49 checks passed
@jonrohan jonrohan deleted the spinner_delay branch November 13, 2025 23:27
@primer primer bot mentioned this pull request Nov 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm staff Author is a staff member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants