Skip to content

Commit

Permalink
refactor(Progress): replaced class components by functional components (
Browse files Browse the repository at this point in the history
  • Loading branch information
atroynikov authored and amje committed Feb 6, 2024
1 parent 6749bb8 commit cda8eb6
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 240 deletions.
262 changes: 23 additions & 239 deletions src/components/Progress/Progress.tsx
Original file line number Diff line number Diff line change
@@ -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<ProgressDefaultProps> {
/** 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<ProgressDefaultProps> {
/** 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<ProgressProps> {
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 (
<div className={b({size}, className)} data-qa={qa}>
{this.renderText()}
{this.renderContent()}
</div>
);
}

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 (
<div className={className} style={style}>
{this.renderInnerText(offset)}
</div>
);
}

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<ItemStyle> = {width: `${-offset}%`};

return (
<div className={className} style={style}>
<div className={b('item')} style={itemStyle} />
{stack.map(
(
{
value: itemValue,
color,
title,
theme,
loading = false,
className: itemClassName,
content,
}: Stack,
index: number,
) => {
itemStyle = {width: `${itemValue}%`, backgroundColor: color};

const modifiers: Record<string, string | boolean> = {
loading,
};

if (typeof color === 'undefined') {
modifiers.theme = theme || 'default';
}

if (Progress.isFiniteNumber(value)) {
return (
<div
key={index}
className={b('item', modifiers, itemClassName)}
style={itemStyle}
title={title}
>
{content}
</div>
);
}

return null;
},
)}
{this.renderInnerText(offset)}
</div>
);
}

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 (
<div className={className} style={style}>
{text}
</div>
);
}

private renderText() {
const {text} = this.props;
const className = b('text');

return <div className={className}>{text}</div>;
}
}
export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(function Progress(
props,
ref,
) {
const {text = '', theme = 'default', size = 'm', loading = false, className, qa} = props;
const resolvedProps: ProgressProps = {...props, text, theme, size, loading};

return (
<div ref={ref} className={progressBlock({size}, className)} data-qa={qa}>
<div className={progressBlock('text')}>{text}</div>
{isProgressWithStack(resolvedProps) ? (
<ProgressWithStack {...resolvedProps} />
) : (
<ProgressWithValue {...resolvedProps} />
)}
</div>
);
});
25 changes: 25 additions & 0 deletions src/components/Progress/ProgressInnerText.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={progressBlock('text-inner')}
style={{transform: `translateX(calc(var(--g-flow-direction) * ${-offset}%))`}}
>
{text}
</div>
);
}
35 changes: 35 additions & 0 deletions src/components/Progress/ProgressStackItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={progressBlock('item', modifiers, className)}
style={{width: `${value}%`, backgroundColor: color}}
title={title}
>
{content}
</div>
);
}

return null;
}
25 changes: 25 additions & 0 deletions src/components/Progress/ProgressWithStack.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={progressBlock('stack', stackClassName)}
style={{transform: `translateX(calc(var(--g-flow-direction) * ${offset}%))`}}
>
<div className={progressBlock('item')} style={{width: `${-offset}%`}} />
{stack.map((item, index) => (
<ProgressStackItem key={index} item={item} />
))}
<ProgressInnerText offset={offset} text={text} />
</div>
);
}
24 changes: 24 additions & 0 deletions src/components/Progress/ProgressWithValue.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={progressBlock('item', {theme: getTheme(props), loading})}
style={{transform: `translateX(calc(var(--g-flow-direction) * ${offset}%))`}}
>
<ProgressInnerText offset={offset} text={text} />
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/Progress/__stories__/Progress.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions src/components/Progress/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {block} from '../utils/cn';

export const progressBlock = block('progress');
Loading

0 comments on commit cda8eb6

Please sign in to comment.