Skip to content

Commit

Permalink
feat(components): add Meter (#1462)
Browse files Browse the repository at this point in the history
* feat(components): add `Meter`

* feat: update styles

* fix: styles

* fix: remove tabular
  • Loading branch information
Niznikr authored Nov 4, 2024
1 parent d422f17 commit a194110
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-beds-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@launchpad-ui/components": patch
---

Add `Meter`
11 changes: 11 additions & 0 deletions packages/components/__tests__/Meter.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest';

import { render, screen } from '../../../test/utils';
import { Meter } from '../src';

describe('Meter', () => {
it('renders', () => {
render(<Meter value={60} aria-label="percent" />);
expect(screen.getByRole('meter')).toBeVisible();
});
});
64 changes: 64 additions & 0 deletions packages/components/src/Meter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { ForwardedRef } from 'react';
import type { MeterProps } from 'react-aria-components';

import { cva } from 'class-variance-authority';
import { forwardRef } from 'react';
import { Meter as AriaMeter, composeRenderProps } from 'react-aria-components';
import styles from './styles/Meter.module.css';

const meter = cva(styles.meter);

const icon = cva(styles.base);

const _Meter = (props: MeterProps, ref: ForwardedRef<HTMLDivElement>) => {
const center = 64;
const strokeWidth = 8;
const r = 64 - strokeWidth;
const c = 2 * r * Math.PI;

return (
<AriaMeter
{...props}
ref={ref}
className={composeRenderProps(props.className, (className, renderProps) =>
meter({ ...renderProps, className }),
)}
>
{({ percentage, valueText }) => (
// biome-ignore lint/a11y/noSvgWithoutTitle: <explanation>
<svg viewBox="0 0 128 128" fill="none" strokeWidth={strokeWidth} className={icon()}>
<circle
cx={center}
cy={center}
r={r}
strokeWidth={strokeWidth}
className={styles.outerCircle}
/>
<circle
cx={center}
cy={center}
r={r}
strokeDasharray={`${c} ${c}`}
strokeDashoffset={c - (percentage / 100) * c}
transform="rotate(-90 64 64)"
className={styles.innerCircle}
/>
<text x={center} y={center} className={styles.value}>
{valueText?.match(/\d+/)?.[0]}
<tspan className={styles.unit}>{valueText?.replace(/\d+/, '')}</tspan>
</text>
</svg>
)}
</AriaMeter>
);
};

/**
* A meter represents a quantity within a known range, or a fractional value.
*
* https://react-spectrum.adobe.com/react-aria/Meter.html
*/
const Meter = forwardRef(_Meter);

export { Meter };
export type { MeterProps };
2 changes: 2 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type { LinkButtonProps } from './LinkButton';
export type { LinkIconButtonProps } from './LinkIconButton';
export type { ListBoxProps, ListBoxItemProps } from './ListBox';
export type { MenuProps, MenuItemProps, MenuTriggerProps, SubmenuTriggerProps } from './Menu';
export type { MeterProps } from './Meter';
export type { ModalProps, ModalOverlayProps } from './Modal';
export type { NumberFieldProps } from './NumberField';
export type { OverlayArrowProps, PopoverProps } from './Popover';
Expand Down Expand Up @@ -113,6 +114,7 @@ export { LinkButton } from './LinkButton';
export { LinkIconButton } from './LinkIconButton';
export { ListBox, ListBoxItem } from './ListBox';
export { Menu, MenuItem, MenuTrigger, SubmenuTrigger } from './Menu';
export { Meter } from './Meter';
export { Modal, ModalOverlay } from './Modal';
export { NumberField } from './NumberField';
export { OverlayArrow, Popover } from './Popover';
Expand Down
33 changes: 33 additions & 0 deletions packages/components/src/styles/Meter.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.meter {
--fill-color: var(--lp-color-brand-cyan-base);

display: inline-flex;
}

.outerCircle {
stroke: var(--lp-color-bg-ui-tertiary);
}

.innerCircle {
stroke: var(--fill-color);
stroke-linecap: round;
}

.value {
fill: var(--lp-color-text-ui-primary-base);
font: var(--lp-text-body-1-semibold);
font-size: var(--lp-font-size-600);
text-anchor: middle;
dominant-baseline: middle;
}

.unit {
font-size: var(--lp-font-size-200);
text-anchor: middle;
dominant-baseline: middle;
}

.base {
height: var(--lp-size-128);
width: var(--lp-size-128);
}
50 changes: 50 additions & 0 deletions packages/components/stories/Meter.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Meta, StoryObj } from '@storybook/react';

import { vars } from '@launchpad-ui/vars';

import { Meter } from '../src';

const meta: Meta<typeof Meter> = {
component: Meter,
title: 'Components/Status/Meter',
parameters: {
status: {
type: import.meta.env.STORYBOOK_PACKAGE_STATUS__COMPONENTS,
},
a11y: {
options: {
rules: {
// https://github.com/adobe/react-spectrum/issues/6627#issuecomment-2192595638
'aria-allowed-attr': { enabled: false },
'aria-prohibited-attr': { enabled: false },
},
},
},
},
};

export default meta;

type Story = StoryObj<typeof Meter>;

export const Example: Story = {
args: { value: 14, 'aria-label': 'percent diff' },
};

export const Values: Story = {
render: () => {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: vars.spacing[400],
}}
>
<Meter value={1} aria-label="1" />
<Meter value={25} aria-label="25" />
<Meter value={100} aria-label="100" />
</div>
);
},
};

0 comments on commit a194110

Please sign in to comment.