Skip to content

Commit

Permalink
feat(meter): add new meter component
Browse files Browse the repository at this point in the history
  • Loading branch information
anuradha9712 committed Jan 28, 2025
1 parent 6d4b6af commit 2c2975b
Show file tree
Hide file tree
Showing 19 changed files with 155,294 additions and 0 deletions.
213 changes: 213 additions & 0 deletions core/components/atoms/meter/Meter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import * as React from 'react';
import { Step, StepColor, StepType } from './Step';
import { BaseProps } from '@/utils/types';
import { Text } from '@/index';
import styles from '@css/components/meter.module.css';
import classNames from 'classnames';
import { useMeterValues } from './useMeterValues';

export type MeterLabelSize = 'small' | 'regular' | 'large';
export type MeterSize = 'small' | 'regular' | 'large';

export type RenderLabelProps = {
filledSteps: number;
value: number;
min: number;
max: number;
stepCount: number;
percentage: number;
};

export type FillStepProps = {
value: number;
stepCount: number;
min: number;
max: number;
};

export type MeterValueProps = {
value: number;
min: number;
max: number;
stepCount: number;
getFilledSteps?: (props: FillStepProps) => number;
};

export interface MeterProps extends BaseProps, React.HTMLAttributes<HTMLDivElement> {
/**
* Value of the `Meter`
*/
value: number;
/**
* Minimum range of the `Meter`
*/
min: number;
/**
* Maximum range of the `Meter`
*/
max: number;
/**
* Total number of steps in the `Meter`
*/
stepCount: number;
/**
* Color of the empty `Step`
*/
emptyColor?: string;
/**
* Color of the filled `Step`
*/
fillColor?: StepColor | StepColor[];
/**
* Size of the Meter
*/
meterSize: MeterSize;
/**
* Size of the Meter `Label`
*/
labelSize?: MeterLabelSize;
/**
* Render prop to display Meter Label
*
* <pre className="DocPage-codeBlock">
* RenderLabelProps: {
* filledSteps: number;
* value: number;
* min: number;
* max: number;
* stepCount: number;
* percentage: number;
* }
* </pre>
*
* It receives an object with the following properties:
* `filledSteps`: Number of filled steps in the Meter <br />
* `value`: Value of the Meter <br />
* `min`: Minimum range of the Meter <br />
* `max`: Maximum range of the Meter <br />
* `stepCount`: Total number of steps in the Meter <br />
* `percentage`: Percentage of the Meter filled <br />
*
*/
renderLabel?: (props: RenderLabelProps) => React.ReactText;
/**
* Determines whether to show default value label in percentage
*/
showLabel?: boolean;
/**
* Custom function to calculate the number of filled steps
* @param FillStepProps
* @returns number
* @default calculateFilledSteps
*
* <pre className="DocPage-codeBlock">
* FillStepProps: {
* value: number;
* stepCount: number;
* min: number;
* max: number;
* }
* </pre>
*/
getFilledSteps?: (props: FillStepProps) => number;
/**
* Aria label for the Meter
*/
ariaLabel?: string;
}

/**
* **Note:**
* - Meter component is using `> half step range` logic to calculate filled steps
* - 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
* - 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
* - To render [custom label](https://mds.innovaccer.com/?path=/docs/components-meter-all--all), you can pass a render prop `renderLabel`
*/

export const Meter = (props: MeterProps) => {
const {
value,
min,
max,
stepCount,
emptyColor,
fillColor,
getFilledSteps,
meterSize,
className,
renderLabel,
labelSize,
ariaLabel,
showLabel,
...rest
} = props;

const { filledSteps, percentage } = useMeterValues({ value, min, max, stepCount, getFilledSteps });

const steps = Array.from({ length: stepCount }, (_, index) => {
const type = index < filledSteps ? 'filled' : 'empty';
const stepColor = Array.isArray(fillColor) ? fillColor[index % fillColor.length] : fillColor;

const stepClassName = classNames({
['mr-2']: index < stepCount - 1,
});

const stepProps = {
stepSize: meterSize,
emptyColor,
type: type as StepType,
fillColor: stepColor,
className: stepClassName,
};

return <Step key={index} {...stepProps} />;
});

const renderLabelProps = {
filledSteps,
value,
min,
max,
stepCount,
percentage,
};

const label = renderLabel ? renderLabel(renderLabelProps) : `${percentage}%`;

return (
<div
data-test="DesignSystem-Meter"
className={classNames(styles.Meter, className)}
role="meter"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
aria-label={ariaLabel}
{...rest}
>
{steps}
{(showLabel || renderLabel) && (
<Text className="ml-4" size={labelSize || meterSize} data-test="DesignSystem-Meter-Label">
{label}
</Text>
)}
</div>
);
};

Meter.displayName = 'Meter';
Meter.defaultProps = {
value: 0,
min: 0,
max: 100,
stepCount: 5,
fillColor: 'info',
meterSize: 'regular',
type: 'empty',
showLabel: true,
emptyColor: 'var(--secondary-light)',
};

Meter.useMeterValues = useMeterValues;

export default Meter;
65 changes: 65 additions & 0 deletions core/components/atoms/meter/Step.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as React from 'react';
import classNames from 'classnames';
import { BaseProps, extractBaseProps } from '@/utils/types';
import styles from '@css/components/meter.module.css';

export type StepSize = 'small' | 'regular' | 'large';
export type StepType = 'filled' | 'empty';
export type StepColor = 'info' | 'success' | 'warning' | 'alert';

export interface StepProps extends BaseProps {
/**
* Size of the `Step`
*/
stepSize: StepSize;
/**
* Type of the `Step`
*/
type: StepType;
/**
* Color of the empty `Step`
*/
emptyColor?: string;
/**
* Color of the filled `Step`
*/
fillColor?: StepColor;
}

export const Step = (props: StepProps) => {
const { stepSize, type, fillColor, className, emptyColor } = props;

const baseProps = extractBaseProps(props);

const classes = classNames(
{
[styles['Meter-Step']]: true,
[styles[`Meter-Step--${type}`]]: type,
[styles[`Meter-Step--${stepSize}`]]: stepSize,
[styles[`Meter-Step--${fillColor}`]]: type === 'filled' && fillColor,
},
className
);

const emptyStyle = type === 'empty' ? { background: emptyColor } : {};

return (
<span
data-test="DesignSystem-Meter-Step"
{...baseProps}
style={emptyStyle}
className={classes}
role="presentation"
aria-hidden="true"
/>
);
};

Step.displayName = 'Step';
Step.defaultProps = {
stepSize: 'regular',
type: 'empty',
emptyColor: 'var(--secondary-light)',
};

export default Step;
31 changes: 31 additions & 0 deletions core/components/atoms/meter/__stories__/appearance.story.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import { Meter, Text } from '@/index';

// CSF format story
export const appearance = () => {
const appearance = ['alert', 'warning', 'success', 'info'];
return (
<div className="d-flex flex-column justify-content-around">
{appearance.map((color, index) => (
<div key={index} className="d-flex align-items-center mb-5">
<Text weight="medium" className="mr-5">
{color.charAt(0).toUpperCase() + color.slice(1)}:
</Text>
<Meter value={40} fillColor={color} />
</div>
))}
</div>
);
};

export default {
title: 'Components/Meter/Variants/Appearance',
component: Meter,
parameters: {
docs: {
docPage: {
title: 'Meter',
},
},
},
};
52 changes: 52 additions & 0 deletions core/components/atoms/meter/__stories__/customHook.story.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from 'react';
import { Meter, Text } from '@/index';

// CSF format story
export const customLabel = () => {
const value = 100;
const min = 50;
const max = 150;
const stepCount = 5;

const { filledSteps, percentage } = Meter.useMeterValues({ value, min, max, stepCount });

return (
<div className="d-flex flex-column">
<Meter value={value} stepCount={stepCount} min={min} max={max} showLabel={false} />
<Text size="small" appearance="subtle" className="mt-6">
{filledSteps} batches completed ({percentage}%)
</Text>
</div>
);
};

const customCode = `() => {
const value = 100;
const min = 50;
const max = 150;
const stepCount = 5;
const { filledSteps, percentage } = Meter.useMeterValues({ value, min, max, stepCount });
return (
<div className="d-flex flex-column">
<Meter value={value} stepCount={stepCount} min={min} max={max} showLabel={false} />
<Text size="small" appearance="subtle" className="mt-6">
{filledSteps} batches completed ({percentage}%)
</Text>
</div>
);
}`;

export default {
title: 'Components/Meter/Custom Label',
component: Meter,
parameters: {
docs: {
docPage: {
title: 'Meter',
customCode,
},
},
},
};
19 changes: 19 additions & 0 deletions core/components/atoms/meter/__stories__/customLabelSize.story.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';
import { Meter } from '@/index';

// CSF format story
export const customLabelSize = () => {
return <Meter value={40} meterSize="small" labelSize="large" />;
};

export default {
title: 'Components/Meter/Variants/Custom Label Size',
component: Meter,
parameters: {
docs: {
docPage: {
title: 'Meter',
},
},
},
};
Loading

0 comments on commit 2c2975b

Please sign in to comment.