(function SkeletonBox(
- {height, width, className, style, ...props},
+ {delay, height, width, className, style, ...props},
ref,
) {
+ const {style: loadingStyle} = useLoadingVisibility(delay)
+ const containerStyle = {
+ height,
+ width,
+ ...loadingStyle,
+ ...style,
+ }
+
return (
}
className={clsx(className, classes.SkeletonBox)}
- style={{height, width, ...(style || {})}}
+ style={containerStyle}
{...props}
/>
)
diff --git a/packages/react/src/SkeletonAvatar/SkeletonAvatar.tsx b/packages/react/src/SkeletonAvatar/SkeletonAvatar.tsx
index e3724053d5f..5cb54b50751 100644
--- a/packages/react/src/SkeletonAvatar/SkeletonAvatar.tsx
+++ b/packages/react/src/SkeletonAvatar/SkeletonAvatar.tsx
@@ -1,19 +1,22 @@
+import {clsx} from 'clsx'
import type React from 'react'
import {isResponsiveValue} from '../hooks/useResponsiveValue'
import type {AvatarProps} from '../Avatar'
import {DEFAULT_AVATAR_SIZE} from '../Avatar/Avatar'
import {SkeletonBox} from '../Skeleton'
+import {useLoadingVisibility} from '../loading'
+import type {LoadingDelay} from '../loading'
import classes from './SkeletonAvatar.module.css'
-import {clsx} from 'clsx'
-interface SkeletonAvatarProps extends Omit, 'size'> {
+interface SkeletonAvatarProps extends Omit, 'size'>, LoadingDelay {
/** Class name for custom styling */
className?: string
size?: AvatarProps['size']
square?: AvatarProps['square']
}
-function SkeletonAvatar({size = DEFAULT_AVATAR_SIZE, square, className, style, ...rest}: SkeletonAvatarProps) {
+function SkeletonAvatar({delay, size = DEFAULT_AVATAR_SIZE, square, className, style, ...rest}: SkeletonAvatarProps) {
+ const {style: loadingStyle} = useLoadingVisibility(delay)
const responsive = isResponsiveValue(size)
const cssSizeVars = {} as Record
@@ -32,7 +35,7 @@ function SkeletonAvatar({size = DEFAULT_AVATAR_SIZE, square, className, style, .
data-component="SkeletonAvatar"
data-responsive={responsive ? '' : undefined}
data-square={square ? '' : undefined}
- style={{...(style || {}), ...cssSizeVars}}
+ style={{...style, ...cssSizeVars, ...loadingStyle}}
/>
)
}
diff --git a/packages/react/src/SkeletonText/SkeletonText.tsx b/packages/react/src/SkeletonText/SkeletonText.tsx
index 37ebb21b013..f2e7c543428 100644
--- a/packages/react/src/SkeletonText/SkeletonText.tsx
+++ b/packages/react/src/SkeletonText/SkeletonText.tsx
@@ -1,10 +1,12 @@
+import {clsx} from 'clsx'
import type React from 'react'
import {type HTMLProps} from 'react'
-import classes from './SkeletonText.module.css'
-import {clsx} from 'clsx'
import {SkeletonBox} from '../Skeleton'
+import classes from './SkeletonText.module.css'
+import {useLoadingVisibility} from '../loading'
+import type {LoadingDelay} from '../loading'
-interface SkeletonTextProps extends Omit, 'size'> {
+interface SkeletonTextProps extends Omit, 'size'>, LoadingDelay {
/** Size of the text that the skeleton is replacing. */
size?: 'display' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall' | 'subtitle'
/** Number of lines of skeleton text to render. */
@@ -15,7 +17,9 @@ interface SkeletonTextProps extends Omit, 'size'> {
className?: string
}
-function SkeletonText({lines = 1, maxWidth, size = 'bodyMedium', className, style, ...rest}: SkeletonTextProps) {
+function SkeletonText({delay, lines = 1, maxWidth, size = 'bodyMedium', className, style, ...rest}: SkeletonTextProps) {
+ const {style: loadingStyle} = useLoadingVisibility(delay)
+
if (lines < 2) {
return (
diff --git a/packages/react/src/Spinner/Spinner.tsx b/packages/react/src/Spinner/Spinner.tsx
index 73945964036..bcaef21ef14 100644
--- a/packages/react/src/Spinner/Spinner.tsx
+++ b/packages/react/src/Spinner/Spinner.tsx
@@ -1,12 +1,14 @@
import {clsx} from 'clsx'
import type React from 'react'
-import {useCallback, useEffect, useRef, useState, useSyncExternalStore} from 'react'
+import {useCallback, useRef, useSyncExternalStore} from 'react'
import {VisuallyHidden} from '../VisuallyHidden'
import type {HTMLDataAttributes} from '../internal/internal-types'
import {useId} from '../hooks'
import classes from './Spinner.module.css'
import {useMedia} from '../hooks/useMedia'
import {useFeatureFlag} from '../FeatureFlags'
+import {useLoadingVisibility} from '../loading'
+import type {LoadingDelay} from '../loading'
const sizeMap = {
small: '16px',
@@ -25,7 +27,8 @@ export type SpinnerProps = {
style?: React.CSSProperties
/** Whether to delay the spinner before rendering by the defined 1000ms. */
delay?: boolean
-} & HTMLDataAttributes
+} & LoadingDelay &
+ HTMLDataAttributes
function Spinner({
size: sizeKey = 'medium',
@@ -38,26 +41,11 @@ function Spinner({
}: SpinnerProps) {
const syncAnimationsEnabled = useFeatureFlag('primer_react_spinner_synchronize_animations')
const animationRef = useSpinnerAnimation()
+ const {style: loadingStyle} = useLoadingVisibility(delay)
const size = sizeMap[sizeKey]
const hasHiddenLabel = srText !== null && ariaLabel === undefined
const labelId = useId()
- const [isVisible, setIsVisible] = useState(!delay)
-
- useEffect(() => {
- if (delay) {
- const timeoutId = setTimeout(() => {
- setIsVisible(true)
- }, 1000)
-
- return () => clearTimeout(timeoutId)
- }
- }, [delay])
-
- if (!isVisible) {
- return null
- }
-
return (
/* inline-flex removes the extra line height */
@@ -71,7 +59,10 @@ function Spinner({
aria-label={ariaLabel ?? undefined}
aria-labelledby={hasHiddenLabel ? labelId : undefined}
className={clsx(className, classes.SpinnerAnimation)}
- style={style}
+ style={{
+ ...style,
+ ...loadingStyle,
+ }}
{...props}
>
{
+ if (delay === undefined || delay === false) {
+ return
+ }
+
+ const delayMs = typeof delay === 'number' ? delay : DEFAULT_DELAY_MS
+ const timeoutId = setTimeout(() => {
+ setVisible(true)
+ }, delayMs)
+
+ return () => {
+ clearTimeout(timeoutId)
+ }
+ }, [delay])
+
+ return {
+ style,
+ }
+}
+
+export {useLoadingVisibility}
+export type {LoadingDelay}