From 50e79c0e68205ede037359982bdd47ba509fb2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D1=80=D1=8C=D1=8F=20=D0=90=D0=BB=D0=B5=D1=85?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0?= Date: Thu, 6 Jun 2024 14:18:14 +0300 Subject: [PATCH] feat(CircularProgress): add new component --- packages/configs/eslint-config/index.js | 5 + .../CircularProgress/CircularProgress.api.mdx | 37 +++ .../CircularProgress.classes.ts | 53 ++++ .../CircularProgress.stories.tsx | 49 ++++ .../CircularProgress/CircularProgress.tsx | 231 ++++++++++++++++++ .../CircularProgress.types.ts | 65 +++++ .../src/components/CircularProgress/index.ts | 3 + packages/react/src/components/index.ts | 1 + packages/react/src/overrides.d.ts | 7 + 9 files changed, 451 insertions(+) create mode 100644 packages/react/src/components/CircularProgress/CircularProgress.api.mdx create mode 100644 packages/react/src/components/CircularProgress/CircularProgress.classes.ts create mode 100644 packages/react/src/components/CircularProgress/CircularProgress.stories.tsx create mode 100644 packages/react/src/components/CircularProgress/CircularProgress.tsx create mode 100644 packages/react/src/components/CircularProgress/CircularProgress.types.ts create mode 100644 packages/react/src/components/CircularProgress/index.ts diff --git a/packages/configs/eslint-config/index.js b/packages/configs/eslint-config/index.js index 304513900..76b3666c8 100644 --- a/packages/configs/eslint-config/index.js +++ b/packages/configs/eslint-config/index.js @@ -77,6 +77,7 @@ export default tseslint.config( 'Button', 'ButtonBase', 'Checkbox', + 'CircularProgress', 'Dialog', 'DialogActions', 'DialogContent', @@ -146,6 +147,10 @@ export default tseslint.config( group: ['@mui/material/Checkbox'], importNames: ['default'], }, + { + group: ['@mui/material/CircularProgress'], + importNames: ['default'], + }, { group: ['@mui/material/Dialog'], importNames: ['default'], diff --git a/packages/react/src/components/CircularProgress/CircularProgress.api.mdx b/packages/react/src/components/CircularProgress/CircularProgress.api.mdx new file mode 100644 index 000000000..92f6f1038 --- /dev/null +++ b/packages/react/src/components/CircularProgress/CircularProgress.api.mdx @@ -0,0 +1,37 @@ +import { Meta } from '@storybook/addon-docs'; +import LinkTo from '@storybook/addon-links/react'; +import { TableInterface } from '~storybook/components/TableInterface'; + + + +# CircularProgress API + +```js +import { CircularProgress } from '@elonkit/react'; +``` + +## Component name + +The name `ESCircularProgress` can be used when providing default props or style overrides in the theme. + +## Props + + + +
+ +## CSS + + + +
+ +## Demos + +
    +
  • + + CircularProgress + +
  • +
diff --git a/packages/react/src/components/CircularProgress/CircularProgress.classes.ts b/packages/react/src/components/CircularProgress/CircularProgress.classes.ts new file mode 100644 index 000000000..50b4b6e80 --- /dev/null +++ b/packages/react/src/components/CircularProgress/CircularProgress.classes.ts @@ -0,0 +1,53 @@ +import { generateUtilityClass, generateUtilityClasses } from '@mui/material'; + +export interface CircularProgressClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the root element if `variant="determinate"`. */ + determinate: string; + /** Styles applied to the root element if `variant="indeterminate"`. */ + indeterminate: string; + /** Styles applied to the root element if `color="primary"`. */ + colorPrimary: string; + /** Styles applied to the root element if `color="secondary"`. */ + colorSecondary: string; + /** Styles applied to the svg element. */ + svg: string; + /** Styles applied to the `circle` svg path. */ + circle: string; + /** Styles applied to the `circle` svg path if `variant="determinate"`. + * @deprecated Combine the [.MuiCircularProgress-circle](/material-ui/api/circular-progress/#circular-progress-classes-circle) and [.MuiCircularProgress-determinate](/material-ui/api/circular-progress/#circular-progress-classes-determinate) classes instead. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details. + */ + circleDeterminate: string; + /** Styles applied to the `circle` svg path if `variant="indeterminate"`. + * @deprecated Combine the [.MuiCircularProgress-circle](/material-ui/api/circular-progress/#circular-progress-classes-circle) and [.MuiCircularProgress-indeterminate](/material-ui/api/circular-progress/#circular-progress-classes-indeterminate) classes instead. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details. + */ + circleIndeterminate: string; + /** Styles applied to the `circle` svg path if `disableShrink={true}`. */ + circleDisableShrink: string; + /** Styles applied to the background `circle` svg path. */ + background: string; + /** Styles applied to the content. */ + content: string; +} + +export type CircularProgressClassKey = keyof CircularProgressClasses; + +export function getCircularProgressUtilityClass(slot: string): string { + return generateUtilityClass('MuiCircularProgress', slot); +} + +export const circularProgressClasses: CircularProgressClasses = generateUtilityClasses('MuiCircularProgress', [ + 'root', + 'determinate', + 'indeterminate', + 'colorPrimary', + 'colorSecondary', + 'svg', + 'circle', + 'circleDeterminate', + 'circleIndeterminate', + 'circleDisableShrink', + 'background', + 'content', +]); diff --git a/packages/react/src/components/CircularProgress/CircularProgress.stories.tsx b/packages/react/src/components/CircularProgress/CircularProgress.stories.tsx new file mode 100644 index 000000000..86b9a2ecf --- /dev/null +++ b/packages/react/src/components/CircularProgress/CircularProgress.stories.tsx @@ -0,0 +1,49 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { Typography } from '@mui/material'; + +import { CircularProgress } from '.'; + +const meta: Meta = { + tags: ['autodocs'], + component: CircularProgress, + parameters: { + references: ['CircularProgress'], + }, + argTypes: { + variant: { + options: ['determinate', 'indeterminate'], + control: { + type: 'select', + }, + }, + color: { + options: ['primary', 'secondary', 'error', 'info', 'success', 'warning', 'inherit'], + control: { + type: 'select', + }, + }, + }, + args: { + variant: 'indeterminate', + color: 'primary', + value: 20, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Demo: Story = { + render: (args) => { + return ( + + {`${Math.round(args.value ?? 0)}%`} + + ); + }, +}; diff --git a/packages/react/src/components/CircularProgress/CircularProgress.tsx b/packages/react/src/components/CircularProgress/CircularProgress.tsx new file mode 100644 index 000000000..f1dad3772 --- /dev/null +++ b/packages/react/src/components/CircularProgress/CircularProgress.tsx @@ -0,0 +1,231 @@ +import { forwardRef } from 'react'; + +import { CircularProgressProps } from './CircularProgress.types'; + +import clsx from 'clsx'; +import { getCircularProgressUtilityClass } from './CircularProgress.classes'; + +import { unstable_composeClasses as composeClasses } from '@mui/base'; + +import { styled, useThemeProps } from '@mui/material/styles'; +import { css, keyframes } from '@mui/system'; +import { capitalize } from '@mui/material'; + +const SIZE = 44; + +const circularRotateKeyframe = keyframes` + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +`; + +const circularDashKeyframe = keyframes` + 0% { + stroke-dasharray: 1px, 200px; + stroke-dashoffset: 0; + } + + 50% { + stroke-dasharray: 100px, 200px; + stroke-dashoffset: -15px; + } + + 100% { + stroke-dasharray: 100px, 200px; + stroke-dashoffset: -125px; + } +`; + +type CircularProgressOwnerState = { + classes?: CircularProgressProps['classes']; + variant: NonNullable; + color: NonNullable; + disableShrink: NonNullable; + thickness: NonNullable; + value: NonNullable; +}; + +const useUtilityClasses = (ownerState: CircularProgressOwnerState) => { + const { classes, variant, color, disableShrink } = ownerState; + + const slots = { + root: ['root', variant, `color${capitalize(color)}`], + svg: ['svg'], + circle: ['circle', `circle${capitalize(variant)}`, disableShrink && 'circleDisableShrink'], + background: ['background'], + content: ['content'], + }; + + return composeClasses(slots, getCircularProgressUtilityClass, classes); +}; + +const CircularProgressRoot = styled('span', { + name: 'ESCircularProgress', + slot: 'Root', + overridesResolver: (props, styles) => { + const { ownerState } = props; + + return [styles.root, styles[ownerState.variant], styles[`color${capitalize(ownerState.color)}`]]; + }, +})<{ ownerState: CircularProgressOwnerState }>(({ ownerState: { color, variant }, theme }) => ({ + display: 'inline-block', + color: theme.vars.palette[color][300], + position: 'relative', + + ...(variant === 'determinate' && { + transition: theme.transitions.create('transform'), + }), +})); + +const CircularProgressSVG = styled('svg', { + name: 'ESCircularProgress', + slot: 'Svg', + overridesResolver: (props, styles) => { + return styles.svg; + }, +})<{ ownerState: CircularProgressOwnerState }>(({ ownerState: { variant } }) => ({ + transform: 'rotate(-90deg)', + display: 'block', + ...(variant === 'indeterminate' && { + animation: `${circularRotateKeyframe} 1.4s linear infinite`, + }), +})); + +const CircularProgressCircle = styled('circle', { + name: 'ESCircularProgress', + slot: 'Circle', + overridesResolver: (props, styles) => { + const { ownerState } = props; + + return [ + styles.circle, + styles[`circle${capitalize(ownerState.variant)}`], + ownerState.disableShrink && styles.circleDisableShrink, + ]; + }, +})<{ ownerState: CircularProgressOwnerState }>( + ({ ownerState: { variant, thickness, value }, theme }) => ({ + stroke: 'currentColor', + ...(variant === 'determinate' && { + transition: theme.transitions.create('stroke-dashoffset'), + strokeDasharray: (2 * Math.PI * ((SIZE - thickness) / 2)).toFixed(3), + strokeDashoffset: `${(((100 - value) / 100) * (2 * Math.PI * ((SIZE - thickness) / 2))).toFixed(3)}px`, + }), + ...(variant === 'indeterminate' && { + strokeDasharray: '80px, 200px', + strokeDashoffset: 0, + animation: `${circularDashKeyframe} 1.4s ease-in-out infinite`, + }), + }), + ({ ownerState }) => + ownerState.variant === 'indeterminate' && + css` + animation: ${circularDashKeyframe} 1.4s ease-in-out infinite; + ` +); + +const CircularProgressBackground = styled('circle', { + name: 'ESCircularProgress', + slot: 'Background', + overridesResolver: (props, styles) => styles.background, +})<{ ownerState: CircularProgressOwnerState }>(({ ownerState: { color }, theme }) => ({ + stroke: theme.vars.palette[color].A400, + transition: theme.transitions.create('stroke-dashoffset'), +})); + +const CircularProgressContent = styled('div', { + name: 'ESCircularProgress', + slot: 'Content', + overridesResolver: (props, styles) => styles.content, +})(() => ({ + top: 0, + left: 0, + bottom: 0, + right: 0, + position: 'absolute', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +})); + +export const CircularProgress = forwardRef( + function CircularProgress(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'ESCircularProgress' }); + const { + children, + className, + color = 'primary', + disableShrink = false, + size = 40, + thickness = 3.6, + value = 0, + variant = 'indeterminate', + ...other + } = props; + + const ownerState = { + color, + disableShrink, + size, + thickness, + value, + variant, + ...props, + }; + + const classes = useUtilityClasses(ownerState); + + // const circleStyle = {}; + // const rootStyle = {}; + // const rootProps = {}; + + // if (variant === 'determinate') { + // const circumference = 2 * Math.PI * ((SIZE - thickness) / 2); + // circleStyle.strokeDasharray = circumference.toFixed(3); + // circleStyle.strokeDashoffset = `${(((100 - value) / 100) * circumference).toFixed(3)}px`; + // rootStyle.transform = 'rotate(-90deg)'; + // } + + return ( + + + + + + + {!!children && {children}} + + ); + } +); diff --git a/packages/react/src/components/CircularProgress/CircularProgress.types.ts b/packages/react/src/components/CircularProgress/CircularProgress.types.ts new file mode 100644 index 000000000..5bf5eed6d --- /dev/null +++ b/packages/react/src/components/CircularProgress/CircularProgress.types.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type */ + +import { ReactNode } from 'react'; + +import { CircularProgressClasses } from './CircularProgress.classes'; + +import { SxProps, Theme } from '@mui/material'; + +import { OverridableStringUnion } from '@mui/types'; + +export interface CircularProgressPropsColorOverrides {} + +export interface CircularProgressProps { + children: ReactNode; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + + /** Class applied to the root element. */ + className?: string; + + /** + * The color of the component. + * @default 'primary' + */ + color?: OverridableStringUnion< + 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning', + CircularProgressPropsColorOverrides + >; + /** + * If `true`, the shrink animation is disabled. + * This only works if variant is `indeterminate`. + * @default false + */ + disableShrink?: boolean; + /** + * The size of the component. + * If using a number, the pixel unit is assumed. + * If using a string, you need to provide the CSS unit, for example '3rem'. + * @default 40 + */ + size?: number | string; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + /** + * The thickness of the circle. + * @default 3.6 + */ + thickness?: number; + /** + * The value of the progress indicator for the determinate variant. + * Value between 0 and 100. + * @default 0 + */ + value?: number; + /** + * The variant to use. + * Use indeterminate when there is no progress value. + * @default 'indeterminate' + */ + variant?: 'determinate' | 'indeterminate'; +} diff --git a/packages/react/src/components/CircularProgress/index.ts b/packages/react/src/components/CircularProgress/index.ts new file mode 100644 index 000000000..ff62e5458 --- /dev/null +++ b/packages/react/src/components/CircularProgress/index.ts @@ -0,0 +1,3 @@ +export { CircularProgress } from './CircularProgress'; +export { CircularProgressClasses, circularProgressClasses, CircularProgressClassKey } from './CircularProgress.classes'; +export { CircularProgressProps } from './CircularProgress.types'; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index df146bfcb..80d54f29a 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -17,6 +17,7 @@ export * from './Calendar'; export * from './Checkbox'; export * from './Chip'; export * from './Chips'; +export * from './CircularProgress'; export * from './DateAdapter'; export * from './Dialog'; export * from './DialogStack'; diff --git a/packages/react/src/overrides.d.ts b/packages/react/src/overrides.d.ts index 2e751aa35..a0a159a35 100644 --- a/packages/react/src/overrides.d.ts +++ b/packages/react/src/overrides.d.ts @@ -73,6 +73,7 @@ import { import { CheckboxClassKey, CheckboxProps, CheckboxIconClassKey, CheckboxIconProps } from './components/Checkbox'; import { ChipClassKey, ChipProps } from './components/Chip'; import { ChipsClassKey, ChipsProps } from './components/Chips'; +import { CircularProgressClassKey, CircularProgressProps } from './components/CircularProgress'; import { DialogActionsClassKey, DialogActionsProps, @@ -366,6 +367,7 @@ declare module '@mui/material/styles/props' { ESCheckboxIcon: CheckboxIconProps; ESChip: ChipProps; ESChips: ChipsProps; + ESCircularProgress: CircularProgressProps; ESDialog: DialogProps; ESDialogActions: DialogActionsProps; ESDialogArrow: DialogArrowProps; @@ -505,6 +507,7 @@ declare module '@mui/material/styles/overrides' { ESCheckboxIcon: CheckboxIconClassKey; ESChip: ChipClassKey; ESChips: ChipsClassKey; + ESCircularProgress: CircularProgressClassKey; ESDialog: DialogClassKey; ESDialogActions: DialogActionsClassKey; ESDialogArrow: DialogArrowClassKey; @@ -733,6 +736,10 @@ declare module '@mui/material/styles/components' { defaultProps?: ComponentsProps['ESChips']; styleOverrides?: ComponentsOverrides['ESChips']; }; + ESCircularProgress?: { + defaultProps?: ComponentsProps['ESCircularProgress']; + styleOverrides?: ComponentsOverrides['ESCircularProgress']; + }; ESDialog?: { defaultProps?: ComponentsProps['ESDialog']; styleOverrides?: ComponentsOverrides['ESDialog'];