diff --git a/src/components/Progress/Progress.tsx b/src/components/Progress/Progress.tsx index 06f03f487b..506a899ed2 100644 --- a/src/components/Progress/Progress.tsx +++ b/src/components/Progress/Progress.tsx @@ -1,244 +1,28 @@ import React from 'react'; -import _sumBy from 'lodash/sumBy'; - -import type {QAProps} from '../types'; -import {block} from '../utils/cn'; +import {ProgressWithStack} from './ProgressWithStack'; +import {ProgressWithValue} from './ProgressWithValue'; +import {progressBlock} from './constants'; +import type {ProgressProps} from './types'; +import {isProgressWithStack} from './types'; import './Progress.scss'; -const b = block('progress'); - -export type ProgressTheme = 'default' | 'success' | 'warning' | 'danger' | 'info' | 'misc'; -export type ProgressSize = 'xs' | 's' | 'm'; -export type ProgressValue = number; - -interface Stack { - value: ProgressValue; - color?: string; - title?: string; - theme?: ProgressTheme; - loading?: boolean; - className?: string; - content?: React.ReactNode; -} - -export interface ProgressColorStops { - theme: ProgressTheme; - stop: number; -} - -interface ProgressGeneralProps extends QAProps { - /** ClassName of element */ - className?: string; -} - -interface ProgressDefaultProps { - /** Text inside progress bar */ - text: string; - /** Theme */ - theme: ProgressTheme; - /** Size. Text of progress bar is displayed in `m` size only. */ - size: ProgressSize; - /** Loading. Аdds loading animation */ - loading?: boolean; -} - -interface ProgressWithValue extends ProgressGeneralProps, Partial { - /** Current progress value. Available range is from 0 to 100. If `stack` property is passed `value` is not required and behaves as maxValue. */ - value: ProgressValue; - /** ProgressTheme breakpoints. [Details](#colorstops) */ - colorStops?: ProgressColorStops[]; - /** Alternative value of `colorStops`. Available range is from 0 to 100. */ - colorStopsValue?: ProgressValue; -} - -interface ProgressWithStack extends ProgressGeneralProps, Partial { - /** Configuration of composite progress bar. Not required if a `value` property is passed. [Details](#stack) */ - stack: Stack[]; - value?: ProgressValue; - /** ClassName of stack element */ - stackClassName?: string; -} - -export type ProgressProps = ProgressWithStack | ProgressWithValue; - -export class Progress extends React.Component { - static defaultProps: ProgressDefaultProps = { - text: '', - theme: 'default', - size: 'm', - loading: false, - }; - - static isFiniteNumber(value: number): boolean { - return isFinite(value) && !isNaN(value); - } - - static isBetween(value: number, min: number, max: number): boolean { - return value >= min && value <= max; - } - - static getOffset(value: number): number { - return value < 100 ? value - 100 : 0; - } - - static getValueFromStack(stack: Stack[]): number { - return _sumBy(stack, (item: Stack) => item.value); - } - - static isProgressWithStack(props: ProgressProps): props is ProgressWithStack { - return (props as ProgressWithStack).stack !== undefined; - } - - render() { - const {size, className, qa} = this.props; - - return ( -
- {this.renderText()} - {this.renderContent()} -
- ); - } - - private getTheme(): ProgressTheme { - const progressProps: ProgressProps = this.props; - if (Progress.isProgressWithStack(progressProps)) { - throw new Error('Unexpected behavior'); - } - - const {theme, colorStops, colorStopsValue, value} = progressProps; - - if (colorStops) { - const matchingColorStopItem: ProgressColorStops | undefined = colorStops.find( - (item: ProgressColorStops, index: number) => { - const currentValue: ProgressValue = - typeof colorStopsValue === 'number' ? colorStopsValue : value; - - return Progress.isBetween( - currentValue, - index > 1 ? colorStops[index - 1].stop : 0, - index < colorStops.length - 1 ? item.stop : 100, - ); - }, - ); - - return matchingColorStopItem ? matchingColorStopItem.theme : (theme as ProgressTheme); - } - - return theme as ProgressTheme; - } - - private renderContent() { - const progressProps: ProgressProps = this.props; - if (Progress.isProgressWithStack(progressProps)) { - return this.renderStack(progressProps); - } else { - return this.renderItem(progressProps); - } - } - - private renderItem(props: ProgressWithValue) { - const {value} = props; - - const className = b('item', {theme: this.getTheme(), loading: this.props.loading}); - - const offset = Progress.getOffset(value); - const style = {transform: `translateX(calc(var(--g-flow-direction) * ${offset}%))`}; - - if (Progress.isFiniteNumber(value)) { - return ( -
- {this.renderInnerText(offset)} -
- ); - } - - return null; - } - - private renderStack(props: ProgressWithStack) { - const {stack, stackClassName} = props; - - const className = b('stack', stackClassName); - const value = props.value || Progress.getValueFromStack(stack); - const offset = Progress.getOffset(value); - const style = {transform: `translateX(calc(var(--g-flow-direction) * ${offset}%))`}; - - interface ItemStyle { - width: string; - backgroundColor: string; - } - let itemStyle: Partial = {width: `${-offset}%`}; - - return ( -
-
- {stack.map( - ( - { - value: itemValue, - color, - title, - theme, - loading = false, - className: itemClassName, - content, - }: Stack, - index: number, - ) => { - itemStyle = {width: `${itemValue}%`, backgroundColor: color}; - - const modifiers: Record = { - loading, - }; - - if (typeof color === 'undefined') { - modifiers.theme = theme || 'default'; - } - - if (Progress.isFiniteNumber(value)) { - return ( -
- {content} -
- ); - } - - return null; - }, - )} - {this.renderInnerText(offset)} -
- ); - } - - private renderInnerText(offset: number) { - const {text} = this.props; - if (!text) { - return null; - } - - const className = b('text-inner'); - const style = {transform: `translateX(calc(var(--g-flow-direction) * ${-offset}%))`}; - - return ( -
- {text} -
- ); - } - - private renderText() { - const {text} = this.props; - const className = b('text'); - - return
{text}
; - } -} +export const Progress = React.forwardRef(function Progress( + props, + ref, +) { + const {text = '', theme = 'default', size = 'm', loading = false, className, qa} = props; + const resolvedProps: ProgressProps = {...props, text, theme, size, loading}; + + return ( +
+
{text}
+ {isProgressWithStack(resolvedProps) ? ( + + ) : ( + + )} +
+ ); +}); diff --git a/src/components/Progress/ProgressInnerText.tsx b/src/components/Progress/ProgressInnerText.tsx new file mode 100644 index 0000000000..9a34df4d96 --- /dev/null +++ b/src/components/Progress/ProgressInnerText.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import {progressBlock} from './constants'; + +export interface ProgressInnerTextProps { + text?: string; + offset?: number; +} + +export function ProgressInnerText(props: ProgressInnerTextProps) { + const {text, offset = 0} = props; + + if (!text) { + return null; + } + + return ( +
+ {text} +
+ ); +} diff --git a/src/components/Progress/ProgressStackItem.tsx b/src/components/Progress/ProgressStackItem.tsx new file mode 100644 index 0000000000..a8f996daa7 --- /dev/null +++ b/src/components/Progress/ProgressStackItem.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import type {CnMods} from '../utils/cn'; + +import {progressBlock} from './constants'; +import type {Stack} from './types'; + +export interface ProgressStackItemProps { + item: Stack; +} + +export function ProgressStackItem({item}: ProgressStackItemProps) { + const {value, color, className, theme, title, content, loading} = item; + const modifiers: CnMods = { + loading, + }; + + if (typeof color === 'undefined') { + modifiers.theme = theme || 'default'; + } + + if (Number.isFinite(value)) { + return ( +
+ {content} +
+ ); + } + + return null; +} diff --git a/src/components/Progress/ProgressWithStack.tsx b/src/components/Progress/ProgressWithStack.tsx new file mode 100644 index 0000000000..e0c5f202f8 --- /dev/null +++ b/src/components/Progress/ProgressWithStack.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import {ProgressInnerText} from './ProgressInnerText'; +import {ProgressStackItem} from './ProgressStackItem'; +import {progressBlock} from './constants'; +import type {ProgressWithStackProps} from './types'; +import {getOffset, getValueFromStack} from './utils'; + +export function ProgressWithStack(props: ProgressWithStackProps) { + const {stack, stackClassName, value, text} = props; + const offset = getOffset(value || getValueFromStack(stack)); + + return ( +
+
+ {stack.map((item, index) => ( + + ))} + +
+ ); +} diff --git a/src/components/Progress/ProgressWithValue.tsx b/src/components/Progress/ProgressWithValue.tsx new file mode 100644 index 0000000000..37e713d223 --- /dev/null +++ b/src/components/Progress/ProgressWithValue.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import {ProgressInnerText} from './ProgressInnerText'; +import {progressBlock} from './constants'; +import type {ProgressWithValueProps} from './types'; +import {getOffset, getTheme} from './utils'; + +export function ProgressWithValue(props: ProgressWithValueProps) { + const {value, loading, text} = props; + const offset = getOffset(value); + + if (!Number.isFinite(value)) { + return null; + } + + return ( +
+ +
+ ); +} diff --git a/src/components/Progress/__stories__/Progress.stories.tsx b/src/components/Progress/__stories__/Progress.stories.tsx index e4014f3aae..f6cbd6c62c 100644 --- a/src/components/Progress/__stories__/Progress.stories.tsx +++ b/src/components/Progress/__stories__/Progress.stories.tsx @@ -3,7 +3,7 @@ import React from 'react'; import type {Meta, StoryFn} from '@storybook/react'; import {Progress} from '../Progress'; -import type {ProgressTheme} from '../Progress'; +import type {ProgressTheme} from '../types'; export default { title: 'Components/Feedback/Progress', diff --git a/src/components/Progress/constants.ts b/src/components/Progress/constants.ts new file mode 100644 index 0000000000..784f95f3ce --- /dev/null +++ b/src/components/Progress/constants.ts @@ -0,0 +1,3 @@ +import {block} from '../utils/cn'; + +export const progressBlock = block('progress'); diff --git a/src/components/Progress/index.ts b/src/components/Progress/index.ts index 1803677d94..e7962321e9 100644 --- a/src/components/Progress/index.ts +++ b/src/components/Progress/index.ts @@ -1 +1,9 @@ export * from './Progress'; +export type { + ProgressColorStops, + ProgressProps, + ProgressSize, + ProgressTheme, + ProgressValue, + Stack, +} from './types'; diff --git a/src/components/Progress/types.ts b/src/components/Progress/types.ts new file mode 100644 index 0000000000..faa4cde9d0 --- /dev/null +++ b/src/components/Progress/types.ts @@ -0,0 +1,65 @@ +import type React from 'react'; + +import type {QAProps} from '../types'; + +export type ProgressTheme = 'default' | 'success' | 'warning' | 'danger' | 'info' | 'misc'; +export type ProgressSize = 'xs' | 's' | 'm'; +export type ProgressValue = number; + +export interface Stack { + value: ProgressValue; + color?: string; + title?: string; + theme?: ProgressTheme; + loading?: boolean; + className?: string; + content?: React.ReactNode; +} + +export interface ProgressColorStops { + theme: ProgressTheme; + stop: number; +} + +interface ProgressGeneralProps extends QAProps { + /** ClassName of element */ + className?: string; +} + +export interface ProgressDefaultProps { + /** Text inside progress bar */ + text: string; + /** Theme */ + theme: ProgressTheme; + /** Size. Text of progress bar is displayed in `m` size only. */ + size: ProgressSize; + /** Loading. Аdds loading animation */ + loading?: boolean; +} + +export interface ProgressWithValueProps + extends ProgressGeneralProps, + Partial { + /** Current progress value. Available range is from 0 to 100. If `stack` property is passed `value` is not required and behaves as maxValue. */ + value: ProgressValue; + /** ProgressTheme breakpoints. [Details](#colorstops) */ + colorStops?: ProgressColorStops[]; + /** Alternative value of `colorStops`. Available range is from 0 to 100. */ + colorStopsValue?: ProgressValue; +} + +export interface ProgressWithStackProps + extends ProgressGeneralProps, + Partial { + /** Configuration of composite progress bar. Not required if a `value` property is passed. [Details](#stack) */ + stack: Stack[]; + value?: ProgressValue; + /** ClassName of stack element */ + stackClassName?: string; +} + +export type ProgressProps = ProgressWithStackProps | ProgressWithValueProps; + +export function isProgressWithStack(props: ProgressProps): props is ProgressWithStackProps { + return (props as ProgressWithStackProps).stack !== undefined; +} diff --git a/src/components/Progress/utils.ts b/src/components/Progress/utils.ts new file mode 100644 index 0000000000..ca65c4705d --- /dev/null +++ b/src/components/Progress/utils.ts @@ -0,0 +1,27 @@ +import type {ProgressTheme, ProgressWithValueProps, Stack} from './types'; + +export function getOffset(value: number): number { + return value < 100 ? value - 100 : 0; +} + +export function getValueFromStack(stack: Stack[]): number { + return stack.reduce((sum, {value}) => sum + value, 0); +} + +export function getTheme(props: ProgressWithValueProps): ProgressTheme { + const {theme, colorStops, colorStopsValue, value} = props; + + if (colorStops) { + const matchingColorStopItem = colorStops.find((item, index) => { + const currentValue = typeof colorStopsValue === 'number' ? colorStopsValue : value; + const minValue = index > 1 ? colorStops[index - 1].stop : 0; + const maxValue = index < colorStops.length - 1 ? item.stop : 100; + + return currentValue >= minValue && currentValue <= maxValue; + }); + + return matchingColorStopItem ? matchingColorStopItem.theme : (theme as ProgressTheme); + } + + return theme as ProgressTheme; +}