Skip to content

Commit

Permalink
feat(Meter): New component (#1281)
Browse files Browse the repository at this point in the history
### Milestones

* [x] Angular
* [x] Circular
* [x] Linear
* [x] Automatic width support (for non numeric widths)
* [x] Forward animation
* [x] Reverse animation
* [x] Accessibility
* [x] prefers-reduced-motion support
* [x] Workaround for small segments
* [x] Workaround for zero segment at the end
* [x] Non rounded lines depending on theme
* [x] Tests
* [x] Playroom Snippets

### Implementation details
* Using `requestAnimationFrame` for animation
* Values array in the range [0..100] (like the progress bar)
* Creating a full circle using the `arc` path has issues. When the start
end end point are the same, it draws the circle in an unexpected
direction. To avoid this effect, a very small threshold value is used to
clamp (not noticeable, a small fraction of a px).

---------

Co-authored-by: Pedro Ladaria <[email protected]>
  • Loading branch information
pladaria and Pedro Ladaria authored Nov 19, 2024
1 parent b9b5d01 commit 1a061d8
Show file tree
Hide file tree
Showing 72 changed files with 792 additions and 4 deletions.
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
"@vanilla-extract/dynamic": "^2.1.1",
"@vanilla-extract/sprinkles": "^1.6.2",
"classnames": "^2.3.1",
"cubic-bezier": "^0.1.2",
"lottie-react": "^2.4.0",
"moment": "^2.29.1",
"react-autosuggest": "^10.1.0",
Expand Down
Binary file not shown.
Binary file not shown.
19 changes: 19 additions & 0 deletions playroom/snippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4142,6 +4142,24 @@ const ratingSnippets: Array<Snippet> = [
},
];

const meterSnippets: Array<Snippet> = [
{
group: 'Meter',
name: 'Meter Linear',
code: '<Meter width={200} type="linear" values={[33, 33, 0]} />',
},
{
group: 'Meter',
name: 'Meter Angular',
code: '<Meter width={200} type="angular" values={[33, 33, 0]} />',
},
{
group: 'Meter',
name: 'Meter Circular',
code: '<Meter width={200} type="circular" values={[33, 33, 0]} />',
},
];

export default [
...buttonSnippets,
...formSnippets,
Expand Down Expand Up @@ -4188,6 +4206,7 @@ export default [
...sliderSnippets,
...cardSnippets,
...exampleScreens,
...meterSnippets,
{
group: 'Progress',
name: 'Stepper',
Expand Down
23 changes: 23 additions & 0 deletions src/__private_stories__/skin-components-story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
Placeholder,
NegativeBox,
IconInvoicePlanFileRegular,
Meter,
} from '..';
import avatarImg from '../__stories__/images/avatar.jpg';
import usingVrImg from '../__stories__/images/using-vr.jpg';
Expand Down Expand Up @@ -315,6 +316,28 @@ export const Default: StoryComponent<Args> = ({variant}) => {
steps={['First', 'Second', 'Third', 'Fourth', 'Fifth']}
/>

{/** Meter */}
<Inline space={16}>
<Meter
width={200}
type="linear"
values={[30, 30, 0]}
arial-label="linear meter"
/>
<Meter
width={200}
type="angular"
values={[30, 30, 0]}
arial-label="angular meter"
/>
<Meter
width={200}
type="circular"
values={[30, 30, 0]}
arial-label="circular meter"
/>
</Inline>

{/** TextLink */}
<TextLink onPress={() => {}}>This is a text link</TextLink>
</ComponentsGroup>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions src/__screenshot_tests__/meter-screenshot-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {openStoryPage, screen} from '../test-utils';

test.each`
values | type | themeVariant
${[0]} | ${'linear'} | ${'default'}
${[0]} | ${'circular'} | ${'default'}
${[0]} | ${'angular'} | ${'default'}
${[100]} | ${'linear'} | ${'default'}
${[100]} | ${'circular'} | ${'default'}
${[100]} | ${'angular'} | ${'default'}
${[33, 33]} | ${'linear'} | ${'default'}
${[33, 33]} | ${'circular'} | ${'default'}
${[33, 33]} | ${'angular'} | ${'default'}
${[33, 33]} | ${'linear'} | ${'inverse'}
${[33, 33]} | ${'circular'} | ${'inverse'}
${[33, 33]} | ${'angular'} | ${'inverse'}
${[33, 33]} | ${'linear'} | ${'media'}
${[33, 33]} | ${'circular'} | ${'media'}
${[33, 33]} | ${'angular'} | ${'media'}
${[20, 20, 20, 20, 0]} | ${'linear'} | ${'default'}
${[20, 20, 20, 20, 0]} | ${'circular'} | ${'default'}
${[20, 20, 20, 20, 0]} | ${'angular'} | ${'default'}
`('Meter $themeVariant $type $values', async ({themeVariant, values, type}) => {
await openStoryPage({
id: 'components-data-visualizations-meter--meter-story',
args: {
themeVariant,
width: 200,
type,
valuesCount: values.length,
...values.reduce(
(acc: Array<number>, value: number, index: number) => ({
...acc,
[`value${index + 1}`]: value,
}),
{}
),
},
});

const stepper = await screen.findByTestId('Meter');
const image = await stepper.screenshot();
expect(image).toMatchImageSnapshot();
});
127 changes: 127 additions & 0 deletions src/__stories__/meter-story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import * as React from 'react';
import {Box, Meter, ResponsiveLayout} from '..';
import beachImg from './images/beach.jpg';

import type {MeterType} from '../meter';

export default {
title: 'Components/Data Visualizations/Meter',
argTypes: {
type: {
options: ['angular', 'circular', 'linear'] as Array<MeterType>,
control: {type: 'select'},
},
themeVariant: {
options: ['default', 'inverse', 'media'],
control: {type: 'select'},
},
valuesCount: {
control: {type: 'range', min: 1, max: 8, step: 1},
},
fullWidth: {
control: {type: 'boolean'},
},
width: {
if: {arg: 'fullWidth', eq: false},
control: {type: 'range', min: 64, max: 600, step: 1},
},
value1: {
control: {type: 'range', min: 0, max: 100, step: 1},
},
value2: {
control: {type: 'range', min: 0, max: 100, step: 1},
},
value3: {
control: {type: 'range', min: 0, max: 100, step: 1},
},
value4: {
control: {type: 'range', min: 0, max: 100, step: 1},
},
value5: {
control: {type: 'range', min: 0, max: 100, step: 1},
},
value6: {
control: {type: 'range', min: 0, max: 100, step: 1},
},
value7: {
control: {type: 'range', min: 0, max: 100, step: 1},
},
value8: {
control: {type: 'range', min: 0, max: 100, step: 1},
},
},
parameters: {
fullScreen: true,
},
};

type MeterStoryArgs = {
type: MeterType;
reverse: boolean;
ariaLabel: string;
themeVariant: 'default' | 'inverse' | 'media';
fullWidth: boolean;
width: number;
valuesCount: number;
value1: number;
value2: number;
value3: number;
value4: number;
value5: number;
value6: number;
value7: number;
value8: number;
};

export const MeterStory: StoryComponent<MeterStoryArgs> = ({
type,
reverse,
themeVariant,
valuesCount,
fullWidth,
width,
ariaLabel,
...valuesArgs
}) => {
const values = Object.values(valuesArgs).slice(0, valuesCount);
return (
<div
style={{
backgroundImage: themeVariant === 'media' ? `url(${beachImg})` : '',
backgroundPosition: 'center',
backgroundSize: 'cover',
}}
>
<ResponsiveLayout variant={themeVariant} fullWidth>
<Box padding={16}>
<Meter
aria-label={ariaLabel || undefined}
type={type}
reverse={reverse}
values={values}
width={fullWidth ? '100%' : width}
/>
</Box>
</ResponsiveLayout>
</div>
);
};

MeterStory.storyName = 'Meter';
MeterStory.args = {
type: 'angular',
reverse: false,
ariaLabel: 'Meter example',
themeVariant: 'default',
fullWidth: false,
width: 400,
valuesCount: 8,
value1: 10,
value2: 10,
value3: 10,
value4: 10,
value5: 10,
value6: 10,
value7: 10,
value8: 10,
};
30 changes: 30 additions & 0 deletions src/__tests__/meter-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import {screen, render} from '@testing-library/react';
import ThemeContextProvider from '../theme-context-provider';
import {makeTheme} from './test-utils';
import Meter from '../meter';

test('Meter custom label', () => {
render(
<ThemeContextProvider theme={makeTheme()}>
<Meter aria-label="Patata" values={[10, 20, 30]} />
</ThemeContextProvider>
);

const meter = screen.getByRole('meter', {name: 'Patata'});
expect(meter).toBeInTheDocument();
});

test('Meter default label', () => {
render(
<ThemeContextProvider theme={makeTheme()}>
<Meter values={[10, 20, 30]} />
</ThemeContextProvider>
);

const expectedLabel =
'Indicador de progreso con 3 secciones, total 60% de 100%. Sección 1: 10%. Sección 2: 20%. Sección 3: 30%';

const meter = screen.getByRole('meter', {name: expectedLabel});
expect(meter).toBeInTheDocument();
});
10 changes: 10 additions & 0 deletions src/__tests__/testid-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DataCard,
DateField,
IconShopRegular,
Meter,
Placeholder,
SearchField,
Stack,
Expand Down Expand Up @@ -173,3 +174,12 @@ test('Buttons test ids', () => {
]
);
});

test('Meter test ids', () => {
checkTestIds(<Meter values={[10, 20, 30]} />, [
{
componentName: 'Meter',
internalTestIds: [],
},
]);
});
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export {default as HorizontalScroll} from './horizontal-scroll';
export {default as HighlightedCard} from './highlighted-card';
export {default as Stepper} from './stepper';
export {ProgressBar, ProgressBarStepped} from './progress-bar';
export {default as Meter} from './meter';
export {Rating, InfoRating} from './rating';
export {VerticalMosaic, HorizontalMosaic} from './mosaic';
export {Timer, TextTimer} from './timer';
Expand Down
Loading

1 comment on commit 1a061d8

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for mistica-web ready!

✅ Preview
https://mistica-ooir3p3g8-flows-projects-65bb050e.vercel.app

Built with commit 1a061d8.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.