Skip to content

Commit 8f34efa

Browse files
author
Hector Arce De Las Heras
committed
Include new stepper progress component
1 parent f8954db commit 8f34efa

19 files changed

+511
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kubit-ui-web/react-components",
3-
"version": "1.6.0",
3+
"version": "1.6.1",
44
"description": "Kubit React Components is a customizable, accessible library of React web components, designed to enhance your application's user experience",
55
"author": {
66
"name": "Kubit",

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,4 @@ export * from './dropdownSelected';
8181
export * from './portal';
8282
export * from './tagV2';
8383
export * from './selectorBoxFile';
84+
export * from './stepperProgress';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { screen } from '@testing-library/react';
2+
import React from 'react';
3+
4+
import { axe } from 'jest-axe';
5+
import { renderProvider } from 'tests/renderProvider/renderProvider.utility';
6+
7+
import { StepperProgress } from '../index';
8+
9+
const mockProps = {
10+
dataTestId: 'testId',
11+
ariaLabel: 'Content loading...',
12+
maxSteps: 10,
13+
currentStep: 3,
14+
variant: 'DEFAULT',
15+
};
16+
17+
describe('StepperProgress component', () => {
18+
it('Should be display the component correctly', async () => {
19+
const { container } = renderProvider(<StepperProgress {...mockProps} />);
20+
21+
const stepperProgress = screen.getByTestId(mockProps.dataTestId);
22+
expect(stepperProgress).toBeInTheDocument();
23+
24+
const results = await axe(container);
25+
expect(container).toHTMLValidate();
26+
expect(results).toHaveNoViolations();
27+
});
28+
it('Should have a helper text', () => {
29+
renderProvider(<StepperProgress {...mockProps} />);
30+
31+
const helpTextContent = `${mockProps.currentStep} steps of ${mockProps.maxSteps} completed`;
32+
const helpText = screen.getByTestId(mockProps.dataTestId + 'HelpText');
33+
expect(helpText.textContent).toBe(helpTextContent);
34+
});
35+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { currentPercentage } from '../currentPercentage';
2+
3+
describe('currentPercentage', () => {
4+
it('currentPercentage default', () => {
5+
expect(currentPercentage(1, 0, 2)).toBe(50);
6+
});
7+
it('currentPercentage with 0', () => {
8+
expect(currentPercentage(0, 0, 2)).toBe(0);
9+
});
10+
it('currentPercentage with 100', () => {
11+
expect(currentPercentage(2, 0, 2)).toBe(100);
12+
});
13+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const currentPercentage = (
2+
currentStep: number,
3+
initialStep: number,
4+
maxSteps: number
5+
): number => {
6+
const scaledValue = (currentStep - initialStep) / (maxSteps - initialStep);
7+
const percentage = 100 * scaledValue;
8+
return Math.round(percentage);
9+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type {
2+
IStepperProgressStyled,
3+
IStepperProgressStandAlone,
4+
IStepperProgress,
5+
StepperProgressCommonProps,
6+
StepperProgressStylesType,
7+
} from './types';
8+
9+
export { StepperProgress } from './stepperProgress';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import styled from 'styled-components';
2+
3+
import { getStyles } from '@/utils';
4+
5+
import { IStepperProgressStyled } from './types';
6+
7+
export const StepperProgressContainerStyled = styled.div<IStepperProgressStyled>`
8+
${({ styles }) => getStyles(styles.container)};
9+
`;
10+
11+
export const StepperProgressStyled = styled.progress<IStepperProgressStyled>`
12+
appearance: none;
13+
-webkit-appearance: none;
14+
-moz-appearance: none;
15+
${({ styles }) => getStyles(styles.progress)};
16+
17+
&::-webkit-progress-bar {
18+
${({ styles }) => getStyles(styles.progress?.webkitProgressBar)};
19+
}
20+
21+
&::-webkit-progress-value {
22+
${({ styles }) => getStyles(styles.progress?.webkitProgressValue)};
23+
}
24+
25+
::-moz-progress-bar {
26+
${({ styles }) => getStyles(styles.progress?.mozProgressBar)};
27+
}
28+
`;
29+
30+
export const StepperProgressHelpText = styled.div<IStepperProgressStyled>`
31+
${({ styles }) => getStyles(styles.helpTextContainer)};
32+
`;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as React from 'react';
2+
3+
import { STYLES_NAME } from '@/constants';
4+
import { useStyles } from '@/hooks/useStyles/useStyles';
5+
import { ErrorBoundary, FallbackComponent } from '@/provider/errorBoundary';
6+
7+
import { StepperProgressStandAlone } from './stepperProgressStandAlone';
8+
import { IStepperProgress, IStepperProgressStandAlone, StepperProgressCommonProps } from './types';
9+
10+
const StepperProgressComponent = React.forwardRef(
11+
<V extends string | unknown>(
12+
{ initialStep = 0, ctv, ...props }: IStepperProgress<V>,
13+
ref: React.ForwardedRef<HTMLDivElement> | undefined | null
14+
): JSX.Element => {
15+
const styles = useStyles<StepperProgressCommonProps, V>(
16+
STYLES_NAME.STEPPER_PROGRESS,
17+
props.variant,
18+
ctv
19+
);
20+
21+
return (
22+
<StepperProgressStandAlone ref={ref} initialStep={initialStep} styles={styles} {...props} />
23+
);
24+
}
25+
);
26+
StepperProgressComponent.displayName = 'StepperProgressComponent';
27+
28+
const StepperProgressBoundary = <V extends string | unknown>(
29+
props: IStepperProgress<V>,
30+
ref: React.ForwardedRef<HTMLDivElement> | undefined | null
31+
): JSX.Element => (
32+
<ErrorBoundary
33+
fallBackComponent={
34+
<FallbackComponent>
35+
<StepperProgressStandAlone
36+
{...(props as unknown as IStepperProgressStandAlone)}
37+
ref={ref}
38+
/>
39+
</FallbackComponent>
40+
}
41+
>
42+
<StepperProgressComponent {...props} ref={ref} />
43+
</ErrorBoundary>
44+
);
45+
46+
const StepperProgress = React.forwardRef(StepperProgressBoundary) as <V extends string | unknown>(
47+
props: React.PropsWithChildren<IStepperProgress<V>> & {
48+
ref?: React.ForwardedRef<HTMLDivElement> | undefined | null;
49+
}
50+
) => ReturnType<typeof StepperProgressBoundary>;
51+
52+
export { StepperProgress };
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as React from 'react';
2+
3+
import { Text, TextComponentType } from '@/components/text';
4+
import { useId } from '@/hooks';
5+
import { AriaLiveOptionType } from '@/types';
6+
7+
// helpers
8+
import { currentPercentage } from './helpers/currentPercentage';
9+
// styles
10+
import {
11+
StepperProgressContainerStyled,
12+
StepperProgressHelpText,
13+
StepperProgressStyled,
14+
} from './stepperProgress.styled';
15+
import { IStepperProgressStandAlone } from './types/stepperProgress';
16+
17+
const maxProgress = 100;
18+
19+
const StepperProgressStandAloneComponent = (
20+
{
21+
dataTestId,
22+
id,
23+
styles,
24+
initialStep,
25+
maxSteps,
26+
currentStep,
27+
prefixAriaLabel = {
28+
step: 'step',
29+
of: 'of',
30+
completed: 'completed',
31+
},
32+
}: IStepperProgressStandAlone,
33+
ref: React.ForwardedRef<HTMLDivElement> | undefined | null
34+
): JSX.Element => {
35+
const _currentStep = Math.min(Math.max(currentStep, initialStep), maxSteps);
36+
const currentProgress = currentPercentage(_currentStep, initialStep, maxSteps);
37+
const uniqueId = useId('stepperProgress');
38+
const stepperProgressId = id ?? uniqueId;
39+
40+
prefixAriaLabel.step = _currentStep === 1 ? 'step' : 'steps';
41+
42+
return (
43+
<StepperProgressContainerStyled ref={ref} styles={styles}>
44+
<StepperProgressStyled
45+
aria-hidden
46+
data-testid={dataTestId}
47+
id={stepperProgressId}
48+
max={maxProgress}
49+
styles={styles}
50+
value={currentProgress}
51+
/>
52+
<StepperProgressHelpText
53+
aria-describedby={stepperProgressId}
54+
aria-live={AriaLiveOptionType.POLITE}
55+
data-testid={dataTestId + 'HelpText'}
56+
styles={styles}
57+
>
58+
<Text component={TextComponentType.SPAN} customTypography={styles.currentStep}>
59+
{`${_currentStep} ${prefixAriaLabel?.step} `}
60+
<Text component={TextComponentType.SPAN} customTypography={styles.maxStep}>
61+
{`${prefixAriaLabel?.of} ${maxSteps} ${prefixAriaLabel?.completed}`}
62+
</Text>
63+
</Text>
64+
</StepperProgressHelpText>
65+
</StepperProgressContainerStyled>
66+
);
67+
};
68+
69+
export const StepperProgressStandAlone = React.forwardRef(StepperProgressStandAloneComponent);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { CATEGORY_CONTROL } from '@/constants/categoryControl';
2+
import { IThemeObjectVariants } from '@/designSystem/themesObject';
3+
import { ArgTypesReturn } from '@/types';
4+
5+
export const argtypes = (
6+
variantsObject: IThemeObjectVariants,
7+
themeSelected: string
8+
): ArgTypesReturn => {
9+
return {
10+
variant: {
11+
description: 'Select the component theme',
12+
type: { name: 'string', required: true },
13+
control: { type: 'select' },
14+
options: Object.keys(variantsObject[themeSelected].StepperProgressVariants || {}),
15+
table: {
16+
type: {
17+
summary: 'string',
18+
},
19+
category: CATEGORY_CONTROL.MODIFIERS,
20+
},
21+
},
22+
maxSteps: {
23+
description: 'Max steps numbers. Max value: 10',
24+
type: { name: 'number', required: true },
25+
control: { type: 'number', min: 0, max: 10 },
26+
table: {
27+
type: {
28+
summary: 'number',
29+
},
30+
category: CATEGORY_CONTROL.MODIFIERS,
31+
},
32+
},
33+
initialStep: {
34+
description: 'Initial Step',
35+
type: { name: 'number' },
36+
control: { type: 'number' },
37+
table: {
38+
type: {
39+
summary: 'number',
40+
},
41+
category: CATEGORY_CONTROL.MODIFIERS,
42+
},
43+
},
44+
currentStep: {
45+
description: 'Current step number, It shouldnt be greater than maximum value of maxSteps',
46+
type: { name: 'number', required: true },
47+
control: { type: 'number', min: 0, max: 10 },
48+
table: {
49+
type: {
50+
summary: 'number',
51+
},
52+
category: CATEGORY_CONTROL.MODIFIERS,
53+
},
54+
},
55+
ariaLabel: {
56+
description: 'Aria label text for progress bar',
57+
type: { name: 'string' },
58+
control: { type: 'text' },
59+
table: {
60+
type: {
61+
summary: 'string',
62+
},
63+
category: CATEGORY_CONTROL.ACCESIBILITY,
64+
},
65+
},
66+
prefixAriaLabel: {
67+
description: 'Prefixes for creating the visual text',
68+
type: { name: '{ step: string; of: string, completed: string }' },
69+
control: { type: 'object' },
70+
table: {
71+
type: {
72+
summary: '{ step: string; of: string, completed: string }',
73+
},
74+
defaultValue: {
75+
// eslint-disable-next-line quotes
76+
summary: "{ step: 'step', of: 'of', completed: 'completed' }",
77+
},
78+
category: CATEGORY_CONTROL.CONTENT,
79+
},
80+
},
81+
id: {
82+
description: 'Id',
83+
type: { name: 'string' },
84+
control: { type: 'text' },
85+
table: {
86+
type: {
87+
summary: 'string',
88+
},
89+
category: CATEGORY_CONTROL.ACCESIBILITY,
90+
},
91+
},
92+
dataTestId: {
93+
description: 'Test id',
94+
type: { name: 'string' },
95+
control: false,
96+
table: {
97+
type: {
98+
summary: 'string',
99+
},
100+
category: CATEGORY_CONTROL.TESTING,
101+
},
102+
},
103+
ctv: {
104+
description: 'Object used for update variant styles',
105+
type: { name: 'object' },
106+
control: { type: 'object' },
107+
table: {
108+
type: {
109+
summary: 'object',
110+
},
111+
category: CATEGORY_CONTROL.CUSTOMIZATION,
112+
},
113+
},
114+
};
115+
};

0 commit comments

Comments
 (0)