Skip to content

Commit 8610ed3

Browse files
committed
feat(meter): add new meter component
1 parent 98e3b2a commit 8610ed3

19 files changed

Lines changed: 155294 additions & 0 deletions
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import * as React from 'react';
2+
import { Step, StepColor, StepType } from './Step';
3+
import { BaseProps } from '@/utils/types';
4+
import { Text } from '@/index';
5+
import styles from '@css/components/meter.module.css';
6+
import classNames from 'classnames';
7+
import { useMeterValues } from './useMeterValues';
8+
9+
export type MeterLabelSize = 'small' | 'regular' | 'large';
10+
export type MeterSize = 'small' | 'regular' | 'large';
11+
12+
export type RenderLabelProps = {
13+
filledSteps: number;
14+
value: number;
15+
min: number;
16+
max: number;
17+
stepCount: number;
18+
percentage: number;
19+
};
20+
21+
export type FillStepProps = {
22+
value: number;
23+
stepCount: number;
24+
min: number;
25+
max: number;
26+
};
27+
28+
export type MeterValueProps = {
29+
value: number;
30+
min: number;
31+
max: number;
32+
stepCount: number;
33+
getFilledSteps?: (props: FillStepProps) => number;
34+
};
35+
36+
export interface MeterProps extends BaseProps, React.HTMLAttributes<HTMLDivElement> {
37+
/**
38+
* Value of the `Meter`
39+
*/
40+
value: number;
41+
/**
42+
* Minimum range of the `Meter`
43+
*/
44+
min: number;
45+
/**
46+
* Maximum range of the `Meter`
47+
*/
48+
max: number;
49+
/**
50+
* Total number of steps in the `Meter`
51+
*/
52+
stepCount: number;
53+
/**
54+
* Color of the empty `Step`
55+
*/
56+
emptyColor?: string;
57+
/**
58+
* Color of the filled `Step`
59+
*/
60+
fillColor?: StepColor | StepColor[];
61+
/**
62+
* Size of the Meter
63+
*/
64+
meterSize: MeterSize;
65+
/**
66+
* Size of the Meter `Label`
67+
*/
68+
labelSize?: MeterLabelSize;
69+
/**
70+
* Render prop to display Meter Label
71+
*
72+
* <pre className="DocPage-codeBlock">
73+
* RenderLabelProps: {
74+
* filledSteps: number;
75+
* value: number;
76+
* min: number;
77+
* max: number;
78+
* stepCount: number;
79+
* percentage: number;
80+
* }
81+
* </pre>
82+
*
83+
* It receives an object with the following properties:
84+
* `filledSteps`: Number of filled steps in the Meter <br />
85+
* `value`: Value of the Meter <br />
86+
* `min`: Minimum range of the Meter <br />
87+
* `max`: Maximum range of the Meter <br />
88+
* `stepCount`: Total number of steps in the Meter <br />
89+
* `percentage`: Percentage of the Meter filled <br />
90+
*
91+
*/
92+
renderLabel?: (props: RenderLabelProps) => React.ReactText;
93+
/**
94+
* Determines whether to show default value label in percentage
95+
*/
96+
showLabel?: boolean;
97+
/**
98+
* Custom function to calculate the number of filled steps
99+
* @param FillStepProps
100+
* @returns number
101+
* @default calculateFilledSteps
102+
*
103+
* <pre className="DocPage-codeBlock">
104+
* FillStepProps: {
105+
* value: number;
106+
* stepCount: number;
107+
* min: number;
108+
* max: number;
109+
* }
110+
* </pre>
111+
*/
112+
getFilledSteps?: (props: FillStepProps) => number;
113+
/**
114+
* Aria label for the Meter
115+
*/
116+
ariaLabel?: string;
117+
}
118+
119+
/**
120+
* **Note:**
121+
* - Meter component is using `> half step range` logic to calculate filled steps
122+
* - Use [custom hook `useMeterValues`](https://mds.innovaccer.com/?path=/docs/components-meter-custom-label--custom-label) to get number of filled steps and percentage internally calculated by the component
123+
* - To use a [custom logic](https://mds.innovaccer.com/?path=/docs/components-meter-custom-step-logic--custom-step-logic) to calculate filled steps, you can pass it as `getFilledSteps` prop
124+
* - To render [custom label](https://mds.innovaccer.com/?path=/docs/components-meter-all--all), you can pass a render prop `renderLabel`
125+
*/
126+
127+
export const Meter = (props: MeterProps) => {
128+
const {
129+
value,
130+
min,
131+
max,
132+
stepCount,
133+
emptyColor,
134+
fillColor,
135+
getFilledSteps,
136+
meterSize,
137+
className,
138+
renderLabel,
139+
labelSize,
140+
ariaLabel,
141+
showLabel,
142+
...rest
143+
} = props;
144+
145+
const { filledSteps, percentage } = useMeterValues({ value, min, max, stepCount, getFilledSteps });
146+
147+
const steps = Array.from({ length: stepCount }, (_, index) => {
148+
const type = index < filledSteps ? 'filled' : 'empty';
149+
const stepColor = Array.isArray(fillColor) ? fillColor[index % fillColor.length] : fillColor;
150+
151+
const stepClassName = classNames({
152+
['mr-2']: index < stepCount - 1,
153+
});
154+
155+
const stepProps = {
156+
stepSize: meterSize,
157+
emptyColor,
158+
type: type as StepType,
159+
fillColor: stepColor,
160+
className: stepClassName,
161+
};
162+
163+
return <Step key={index} {...stepProps} />;
164+
});
165+
166+
const renderLabelProps = {
167+
filledSteps,
168+
value,
169+
min,
170+
max,
171+
stepCount,
172+
percentage,
173+
};
174+
175+
const label = renderLabel ? renderLabel(renderLabelProps) : `${percentage}%`;
176+
177+
return (
178+
<div
179+
data-test="DesignSystem-Meter"
180+
className={classNames(styles.Meter, className)}
181+
role="meter"
182+
aria-valuemin={min}
183+
aria-valuemax={max}
184+
aria-valuenow={value}
185+
aria-label={ariaLabel}
186+
{...rest}
187+
>
188+
{steps}
189+
{(showLabel || renderLabel) && (
190+
<Text className="ml-4" size={labelSize || meterSize} data-test="DesignSystem-Meter-Label">
191+
{label}
192+
</Text>
193+
)}
194+
</div>
195+
);
196+
};
197+
198+
Meter.displayName = 'Meter';
199+
Meter.defaultProps = {
200+
value: 0,
201+
min: 0,
202+
max: 100,
203+
stepCount: 5,
204+
fillColor: 'info',
205+
meterSize: 'regular',
206+
type: 'empty',
207+
showLabel: true,
208+
emptyColor: 'var(--secondary-light)',
209+
};
210+
211+
Meter.useMeterValues = useMeterValues;
212+
213+
export default Meter;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as React from 'react';
2+
import classNames from 'classnames';
3+
import { BaseProps, extractBaseProps } from '@/utils/types';
4+
import styles from '@css/components/meter.module.css';
5+
6+
export type StepSize = 'small' | 'regular' | 'large';
7+
export type StepType = 'filled' | 'empty';
8+
export type StepColor = 'info' | 'success' | 'warning' | 'alert';
9+
10+
export interface StepProps extends BaseProps {
11+
/**
12+
* Size of the `Step`
13+
*/
14+
stepSize: StepSize;
15+
/**
16+
* Type of the `Step`
17+
*/
18+
type: StepType;
19+
/**
20+
* Color of the empty `Step`
21+
*/
22+
emptyColor?: string;
23+
/**
24+
* Color of the filled `Step`
25+
*/
26+
fillColor?: StepColor;
27+
}
28+
29+
export const Step = (props: StepProps) => {
30+
const { stepSize, type, fillColor, className, emptyColor } = props;
31+
32+
const baseProps = extractBaseProps(props);
33+
34+
const classes = classNames(
35+
{
36+
[styles['Meter-Step']]: true,
37+
[styles[`Meter-Step--${type}`]]: type,
38+
[styles[`Meter-Step--${stepSize}`]]: stepSize,
39+
[styles[`Meter-Step--${fillColor}`]]: type === 'filled' && fillColor,
40+
},
41+
className
42+
);
43+
44+
const emptyStyle = type === 'empty' ? { background: emptyColor } : {};
45+
46+
return (
47+
<span
48+
data-test="DesignSystem-Meter-Step"
49+
{...baseProps}
50+
style={emptyStyle}
51+
className={classes}
52+
role="presentation"
53+
aria-hidden="true"
54+
/>
55+
);
56+
};
57+
58+
Step.displayName = 'Step';
59+
Step.defaultProps = {
60+
stepSize: 'regular',
61+
type: 'empty',
62+
emptyColor: 'var(--secondary-light)',
63+
};
64+
65+
export default Step;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as React from 'react';
2+
import { Meter, Text } from '@/index';
3+
4+
// CSF format story
5+
export const appearance = () => {
6+
const appearance = ['alert', 'warning', 'success', 'info'];
7+
return (
8+
<div className="d-flex flex-column justify-content-around">
9+
{appearance.map((color, index) => (
10+
<div key={index} className="d-flex align-items-center mb-5">
11+
<Text weight="medium" className="mr-5">
12+
{color.charAt(0).toUpperCase() + color.slice(1)}:
13+
</Text>
14+
<Meter value={40} fillColor={color} />
15+
</div>
16+
))}
17+
</div>
18+
);
19+
};
20+
21+
export default {
22+
title: 'Components/Meter/Variants/Appearance',
23+
component: Meter,
24+
parameters: {
25+
docs: {
26+
docPage: {
27+
title: 'Meter',
28+
},
29+
},
30+
},
31+
};
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+
import { Meter, Text } from '@/index';
3+
4+
// CSF format story
5+
export const customLabel = () => {
6+
const value = 100;
7+
const min = 50;
8+
const max = 150;
9+
const stepCount = 5;
10+
11+
const { filledSteps, percentage } = Meter.useMeterValues({ value, min, max, stepCount });
12+
13+
return (
14+
<div className="d-flex flex-column">
15+
<Meter value={value} stepCount={stepCount} min={min} max={max} showLabel={false} />
16+
<Text size="small" appearance="subtle" className="mt-6">
17+
{filledSteps} batches completed ({percentage}%)
18+
</Text>
19+
</div>
20+
);
21+
};
22+
23+
const customCode = `() => {
24+
const value = 100;
25+
const min = 50;
26+
const max = 150;
27+
const stepCount = 5;
28+
29+
const { filledSteps, percentage } = Meter.useMeterValues({ value, min, max, stepCount });
30+
31+
return (
32+
<div className="d-flex flex-column">
33+
<Meter value={value} stepCount={stepCount} min={min} max={max} showLabel={false} />
34+
<Text size="small" appearance="subtle" className="mt-6">
35+
{filledSteps} batches completed ({percentage}%)
36+
</Text>
37+
</div>
38+
);
39+
}`;
40+
41+
export default {
42+
title: 'Components/Meter/Custom Label',
43+
component: Meter,
44+
parameters: {
45+
docs: {
46+
docPage: {
47+
title: 'Meter',
48+
customCode,
49+
},
50+
},
51+
},
52+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as React from 'react';
2+
import { Meter } from '@/index';
3+
4+
// CSF format story
5+
export const customLabelSize = () => {
6+
return <Meter value={40} meterSize="small" labelSize="large" />;
7+
};
8+
9+
export default {
10+
title: 'Components/Meter/Variants/Custom Label Size',
11+
component: Meter,
12+
parameters: {
13+
docs: {
14+
docPage: {
15+
title: 'Meter',
16+
},
17+
},
18+
},
19+
};

0 commit comments

Comments
 (0)