diff --git a/packages/react/src/Skeleton/SkeletonBox.stories.tsx b/packages/react/src/Skeleton/SkeletonBox.stories.tsx index ecf62f36ba8..5b5c78ffd2b 100644 --- a/packages/react/src/Skeleton/SkeletonBox.stories.tsx +++ b/packages/react/src/Skeleton/SkeletonBox.stories.tsx @@ -12,6 +12,9 @@ export const Default = () => export const Playground: StoryFn> = args => Playground.argTypes = { + delay: { + type: 'number', + }, height: { type: 'string', }, diff --git a/packages/react/src/Skeleton/SkeletonBox.tsx b/packages/react/src/Skeleton/SkeletonBox.tsx index eb52dc7800b..ecceaf31885 100644 --- a/packages/react/src/Skeleton/SkeletonBox.tsx +++ b/packages/react/src/Skeleton/SkeletonBox.tsx @@ -1,6 +1,8 @@ import React from 'react' import {type CSSProperties, type HTMLProps} from 'react' import {clsx} from 'clsx' +import {useLoadingVisibility} from '../loading' +import type {LoadingDelay} from '../loading' import classes from './SkeletonBox.module.css' export type SkeletonBoxProps = { @@ -10,17 +12,26 @@ export type SkeletonBoxProps = { width?: CSSProperties['width'] /** The className of the skeleton box */ className?: string -} & HTMLProps +} & LoadingDelay & + HTMLProps export const SkeletonBox = React.forwardRef(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}