/
bearury
/
ui-kit-ce
Обзор
Документация
Войти
/
bearury
/
ui-kit-ce
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
packages/progress/src/CircularProgress/CircularProgress.tsx
406 строк
10 KB
Строганов Федор
fix(progress): исправлено обрезание спиннера при масштабе 90% в браузерах chromium
10 июн 2025, 13:02
10 июн 2025, 13:02
f4f6b90
Код
Авторство
О чём код?
'use client' import * as React from 'react' import { createUseStyles, clsx, useTheme } from '@v-uik/theme' import { useClassList } from '@v-uik/hooks' import { ProgressSize, ProgressSizeProps } from '../common' import { CircularClasses } from './classes' import type { ComponentPropsWithRefFix } from '@v-uik/common' import { BACKDROP_TRANSITION, OverlayBackdrop, OverlayBackdropProps, } from './components' const SIZES: { [key in ProgressSizeProps]: number } = { xlg: 56, lg: 32, md: 24, sm: 16, } const STROKE_WIDTH = 5 const getDynamicStyles = ({ size, color, isFullScreen, isOverlay, }: { size: number color: React.CSSProperties['color'] isFullScreen?: boolean isOverlay?: boolean }) => ({ root: { width: isFullScreen ? '100%' : (isOverlay && '100%') || size, height: isFullScreen ? '100vh' : (isOverlay && '100%') || size, }, path: { stroke: color, }, }) // Функция IIFE потому что передача пропсов работает некорректно при анимации // https://github.com/cssinjs/jss/issues/1216 const useStyles = ({ animationRadius }: CircleStyleProps) => createUseStyles((theme) => { return { root: {}, backdrop: { position: 'fixed', zIndex: 2, inset: 0, backgroundColor: theme.comp.circularProgress.backdropColorBackground, transition: BACKDROP_TRANSITION, }, overlay: { backgroundColor: theme.comp.circularProgress.overlayColorBackground, transition: BACKDROP_TRANSITION, position: 'absolute', width: '100%', height: '100%', inset: 0, zIndex: 2, }, backdropProgress: { position: 'fixed', zIndex: 3, inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', '& $track': { stroke: theme.comp.circularProgress.trackBackdropColorBackground, }, }, overlayProgress: { position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 3, overflow: 'hidden', }, overlayContentBlurry: { filter: 'blur(2px)', }, overlayContent: {}, indeterminate: { '& $circular': { animation: '$rotate 2.3s ease-in-out infinite', }, '& $path': { strokeDashoffset: 0, strokeDasharray: [animationRadius * 0.3, animationRadius], animation: '$dash 2.3s ease-in-out infinite', strokeLinecap: 'butt', }, }, determinate: { '& $path': { transition: 'stroke-dashoffset 600ms ease-in-out', }, }, circular: { position: 'relative', overflow: 'visible', }, track: { stroke: theme.comp.circularProgress.trackColorBackground, }, path: { transformOrigin: 'center', }, percentage: { color: theme.comp.circularProgress.colorText, fontFamily: theme.comp.circularProgress.typographyFontFamily, fontSize: theme.comp.circularProgress.typographyFontSize, lineHeight: theme.comp.circularProgress.typographyLineHeight, letterSpacing: theme.comp.circularProgress.typographyLetterSpacing, fontWeight: theme.comp.circularProgress.typographyFontWeight, }, '@keyframes rotate': { '100%': { transform: 'rotate(360deg)', }, }, '@keyframes dash': { '0%': { strokeDasharray: [1, animationRadius * 1.1], strokeDashoffset: 0, }, '50%': { strokeDasharray: [animationRadius * 0.5, animationRadius * 1.1], strokeDashoffset: -(animationRadius * 0.35), }, '100%': { strokeDasharray: [animationRadius * 0.02, animationRadius * 1.1], strokeDashoffset: -animationRadius, }, }, } }) interface CircleStyleProps { animationRadius: number } export interface CircularProgressProps extends Omit<ComponentPropsWithRefFix<'div'>, 'size'>, OverlayBackdropProps { /** * * @inner */ classes?: Partial<CircularClasses> /** * Значение прогресса. * * @inner */ value?: number /** * Максимальное значение (100%). * * @inner */ max?: number /** * Размер прогресс бара. * * @inner */ size?: ProgressSizeProps | number /** * Настройка размеров прогресс бара. * * @inner */ sizesConfig?: Record<ProgressSizeProps, number> /** * Скрыть задний фон прогресс бара. * * @inner */ hideTrack?: boolean /** * Толщина линии. * * @inner */ thickness?: number /** * Отображение прогресса загрузки внутри круга. * * @inner */ percentageInsideCircle?: React.ReactNode /** * Цвет окружности. * * @inner */ color?: React.CSSProperties['color'] /** * Флаг отображения компонента в полноэкранном режиме. * * @inner */ isFullScreen?: boolean /** * Флаг отображения компонента. Свойство по умолчанию принимает значение `true`. * * @inner */ isLoading?: boolean } export const CircularProgress = React.forwardRef( ( { classes, className: classNameProp, value, max = 100, size = ProgressSize.lg, sizesConfig = SIZES, hideTrack = false, thickness = STROKE_WIDTH, percentageInsideCircle, color, isFullScreen, container, disableEscapePressHandler, onClose, backdropOverlayProps, disableBackdropClickHandler, isLoading = true, children, backdropOverlayWrapperProps, withBlur, overlayContentProps, ...rest }: CircularProgressProps, ref: React.Ref<HTMLDivElement> ) => { // Определение размера спиннера. const spinnerSize = typeof size === 'number' ? size : sizesConfig[size] ?? sizesConfig.lg // рассчитываем радиус const radius = spinnerSize / 2 - thickness / 2 // цифра 6.2 дает нужный эффект при расчете strokeDashoffset const animationRadius = Number((radius * 6.2).toFixed()) // проверка на неопределенность const indeterminate = value === null || value === undefined // длина окружности const circumference = 2 * Math.PI * radius const strokeDasharray = !indeterminate ? circumference.toFixed(3) : undefined const strokeDashoffset = !indeterminate ? ((max - (value as number)) / max) * circumference : undefined const theme = useTheme() const pathColor = color || (isFullScreen && theme.comp.circularProgress.pathBackdropColorBackground) || theme.comp.circularProgress.pathColorBackground const isOverlay = !isFullScreen && !!children const dynamicStyles = getDynamicStyles({ size: spinnerSize, color: pathColor, isFullScreen, isOverlay, }) const classList = useStyles({ animationRadius })() const classesMap = useClassList(classList, classes) const className = clsx(classNameProp, classesMap.root, { [classesMap.indeterminate]: indeterminate, [classesMap.determinate]: !indeterminate, [classesMap.backdropProgress]: isFullScreen, [classesMap.overlayProgress]: isOverlay, }) const overlayContentClassName = clsx(classesMap.overlayContent, { [classesMap.overlayContentBlurry]: withBlur, }) const progressContent = ( <div {...rest} ref={ref} style={{ ...dynamicStyles.root, ...(rest?.style ?? {}) }} aria-valuenow={!indeterminate ? value : undefined} aria-valuemin={!indeterminate ? 1 : undefined} aria-valuemax={!indeterminate ? max : undefined} role="progressbar" className={className} > <svg className={classesMap.circular} width={spinnerSize} height={spinnerSize} viewBox={`0 0 ${spinnerSize} ${spinnerSize}`} > {!hideTrack ? ( <circle role="img" aria-label="track" className={classesMap.track} cx="50%" cy="50%" r={radius} fill="none" strokeWidth={thickness} strokeMiterlimit={10} /> ) : null} <circle role="img" aria-label="bar" style={dynamicStyles.path} className={classesMap.path} cx="50%" cy="50%" r={radius} fill="none" strokeWidth={thickness} strokeMiterlimit={10} strokeDashoffset={strokeDashoffset} strokeDasharray={strokeDasharray} transform="rotate(-90)" /> {percentageInsideCircle && ( <text className={classesMap.percentage} textAnchor="middle" alignmentBaseline="central" x="50%" y="50%" > {percentageInsideCircle} </text> )} </svg> </div> ) if (isFullScreen || children) { return ( <OverlayBackdrop backdropOverlayProps={backdropOverlayProps} backdropOverlayWrapperProps={backdropOverlayWrapperProps} className={isOverlay ? classesMap.overlay : classesMap.backdrop} open={isLoading} container={container} disableBackdropClickHandler={ isOverlay ? true : disableBackdropClickHandler } disableEscapePressHandler={ isOverlay ? true : disableEscapePressHandler } progressContent={progressContent} overlayContentClassName={overlayContentClassName} overlayContentProps={overlayContentProps} onClose={onClose} > {children} </OverlayBackdrop> ) } return isLoading ? progressContent : null } )