Skip to content

Commit

Permalink
chore(meter): add docs, update designs
Browse files Browse the repository at this point in the history
  • Loading branch information
nkrantz committed Sep 7, 2023
1 parent 0b5f37e commit 6a7ed21
Show file tree
Hide file tree
Showing 13 changed files with 587 additions and 143 deletions.
6 changes: 6 additions & 0 deletions .changeset/blue-phones-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/meter': patch
'@twilio-paste/core': patch
---

[Meter] Finalize designs of Meter component, bump to Production status, and add documentation.
5 changes: 5 additions & 0 deletions .changeset/olive-lizards-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@twilio-paste/codemods': patch
---

[Codemods] New export from Meter package: MeterLabel
1 change: 1 addition & 0 deletions cypress/integration/sitemap-vrt/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const SITEMAP = [
'/components/list/',
'/components/minimizable-dialog/',
'/components/media-object/',
'/components/meter',
'/components/pagination/',
'/components/modal/',
'/components/menu/',
Expand Down
32 changes: 23 additions & 9 deletions packages/paste-core/components/meter/__tests__/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {render, screen} from '@testing-library/react';
import {Theme} from '@twilio-paste/theme';
import type {RenderOptions} from '@testing-library/react';

import {Default, HiddenValueLabelAriaLabel, Customized} from '../stories/index.stories';
import {Default, AriaLabel, Customized} from '../stories/index.stories';

const ThemeWrapper: RenderOptions['wrapper'] = ({children}) => (
<Theme.Provider theme="default">{children}</Theme.Provider>
Expand All @@ -21,33 +21,47 @@ describe('Meter', () => {
expect(meter).toHaveAttribute('aria-valuemax', '100');
expect(meter).toHaveAttribute('aria-valuenow', '75');
expect(meter).toHaveAttribute('aria-valuetext', '75%');
expect(meter).toHaveAttribute('id', 'meter');
expect(meter).toHaveAttribute('aria-labelledby', 'meterMETER_LABEL');
expect(meter).toHaveAttribute('id');
expect(meter).toHaveAttribute('aria-labelledby');
});

it('should apply aria-label correctly', () => {
render(<HiddenValueLabelAriaLabel />, {wrapper: ThemeWrapper});
render(<AriaLabel />, {wrapper: ThemeWrapper});
const meter = screen.getByRole('meter');
expect(meter).toHaveAttribute('aria-label', 'Fuel level');
expect(meter).toHaveAttribute('aria-label', 'Storage space');
expect(meter).not.toHaveAttribute('aria-labelledby');
});
});

describe('Customization', () => {
it('should set default data-paste-element attribute on meter', () => {
render(<Customized />);
const meterOne = screen.getByTestId('meter_one');
expect(meterOne).toHaveAttribute(elementAttr, 'METER');
const meterLabelOne = screen.getByTestId('meter_label_one');
expect(meterLabelOne).toHaveAttribute(elementAttr, 'METER_LABEL');
expect(meterLabelOne.parentElement).toHaveAttribute(elementAttr, 'METER_LABEL_WRAPPER');
expect(meterLabelOne.nextElementSibling).toHaveAttribute(elementAttr, 'METER_LABEL_VALUE_LABEL');
const meterOne = screen.getByTestId('meter_one');
expect(meterOne).toHaveAttribute(elementAttr, 'METER');
expect(meterOne.firstElementChild).toHaveAttribute(elementAttr, 'METER_BAR');
expect(meterOne.firstElementChild?.firstElementChild).toHaveAttribute(elementAttr, 'METER_FILL');
expect(meterOne.lastElementChild).toHaveAttribute(elementAttr, 'METER_MIN_MAX_WRAPPER');
expect(meterOne.lastElementChild?.firstElementChild).toHaveAttribute(elementAttr, 'METER_MIN');
expect(meterOne.lastElementChild?.lastElementChild).toHaveAttribute(elementAttr, 'METER_MAX');
});

it('should set custom data-paste-element attribute on meter', () => {
render(<Customized />);
const meterTwo = screen.getByTestId('meter_two');
expect(meterTwo).toHaveAttribute(elementAttr, 'FOO');
const meterLabelTwo = screen.getByTestId('meter_label_two');
expect(meterLabelTwo).toHaveAttribute(elementAttr, 'FOO_LABEL');
expect(meterLabelTwo.parentElement).toHaveAttribute(elementAttr, 'FOO_LABEL_WRAPPER');
expect(meterLabelTwo.nextElementSibling).toHaveAttribute(elementAttr, 'FOO_LABEL_VALUE_LABEL');
const meterTwo = screen.getByTestId('meter_two');
expect(meterTwo).toHaveAttribute(elementAttr, 'FOO');
expect(meterTwo.firstElementChild).toHaveAttribute(elementAttr, 'FOO_BAR');
expect(meterTwo.firstElementChild?.firstElementChild).toHaveAttribute(elementAttr, 'FOO_FILL');
expect(meterTwo.lastElementChild).toHaveAttribute(elementAttr, 'FOO_MIN_MAX_WRAPPER');
expect(meterTwo.lastElementChild?.firstElementChild).toHaveAttribute(elementAttr, 'FOO_MIN');
expect(meterTwo.lastElementChild?.lastElementChild).toHaveAttribute(elementAttr, 'FOO_MAX');
});
});
});
2 changes: 1 addition & 1 deletion packages/paste-core/components/meter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@twilio-paste/meter",
"version": "1.0.0",
"category": "data display",
"status": "alpha",
"status": "production",
"description": "Meter is a visual representation of a numerical value within a known range.",
"author": "Twilio Inc.",
"license": "MIT",
Expand Down
126 changes: 73 additions & 53 deletions packages/paste-core/components/meter/src/Meter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,73 +9,93 @@ import {LABEL_SUFFIX} from './constants';
export interface MeterProps extends HTMLPasteProps<'meter'>, Pick<BoxProps, 'element'> {
minValue?: number;
maxValue?: number;
minLabel?: string;
maxLabel?: string;
value?: number;
id: string;
showValueLabel?: boolean;
formatOptions?: Intl.NumberFormatOptions; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options
valueLabel?: string;
'aria-label'?: string;
'aria-describedby'?: string;
'aria-labelledby'?: string;
/*
* The following props don't exist on the react-aria useMeter hook but do exist on the HTML meter element.
* They can be added back into the Paste Meter API depending on the finalized spec & designs.
*
* low?: number;
* high?: number;
* optimum?: number;
*/
}

const Meter = React.forwardRef<HTMLMeterElement, MeterProps>(({element = 'METER', id, ...props}, ref) => {
const {value = 0, minValue = 0, maxValue = 100, showValueLabel = true} = props;
const {meterProps} = useMeter(props);
const Meter = React.forwardRef<HTMLMeterElement, MeterProps>(
({element = 'METER', id, minLabel, maxLabel, ...props}, ref) => {
const {value = 0, minValue = 0, maxValue = 100} = props;
const {meterProps} = useMeter(props);

// Calculate the width of the bar as a percentage
const percentage = (value - minValue) / (maxValue - minValue);
const fillWidth = `${Math.round(percentage * 100)}%`;
// Calculate the width of the bar as a percentage
const percentage = (value - minValue) / (maxValue - minValue);
const fillWidth = `${Math.round(percentage * 100)}%`;

/*
* Since ProgressBar isn't a form element, we cannot use htmlFor from the regular label
* so we create a ProgressBarLabel component that behaves like a regular form Label
* but leverages aria-labelledby instead of htmlFor transparently.
*/
let labelledBy = props['aria-labelledby'];
if (labelledBy == null && props['aria-label'] == null && id != null) {
labelledBy = `${id}${LABEL_SUFFIX}`;
}
/*
* Since Meter isn't a form element, we cannot use htmlFor from the regular Label
* so we created a MeterLabel component that behaves like a regular form Label
* but leverages aria-labelledby instead of htmlFor under the hood.
* `aria-labelledby` and `aria-label` can still be passed for custom labelling options.
*/
let labelledBy = props['aria-labelledby'];
if (labelledBy == null && props['aria-label'] == null && id != null) {
labelledBy = `${id}${LABEL_SUFFIX}`;
}

return (
<Box
as="div"
{...meterProps}
role="meter"
id={id}
maxWidth="size30"
position="relative"
element={element}
aria-labelledby={labelledBy}
>
return (
<Box
display="flex"
width="fit-content"
position="absolute"
right="0"
top="spaceNegative70"
element={`${element}_VALUE_LABEL_WRAPPER`}
as="div"
{...meterProps}
role="meter"
id={id}
ref={ref}
width="100%"
position="relative"
element={element}
aria-labelledby={labelledBy}
>
{showValueLabel && (
<Text as="span" element={`${element}_VALUE_LABEL`}>
{meterProps['aria-valuetext']}
</Text>
<Box
height="10px"
backgroundColor="colorBackgroundStrong"
borderRadius="borderRadiusPill"
element={`${element}_BAR`}
>
<Box
width={fillWidth}
height="10px"
backgroundColor="colorBackgroundPrimaryStronger"
borderTopLeftRadius="borderRadiusPill"
borderBottomLeftRadius="borderRadiusPill"
borderTopRightRadius={fillWidth === '100%' ? 'borderRadiusPill' : 'borderRadius10'}
borderBottomRightRadius={fillWidth === '100%' ? 'borderRadiusPill' : 'borderRadius10'}
element={`${element}_FILL`}
/>
</Box>
{(minLabel || maxLabel) && (
<Box
display="flex"
flexDirection="row"
justifyContent="space-between"
marginTop="space20"
aria-hidden="true"
element={`${element}_MIN_MAX_WRAPPER`}
>
{minLabel ? (
<Text as="span" color="colorTextWeak" fontWeight="fontWeightNormal" element={`${element}_MIN`}>
{minLabel}
</Text>
) : (
<span />
)}
{maxLabel ? (
<Text as="span" color="colorTextWeak" fontWeight="fontWeightNormal" element={`${element}_MAX`}>
{maxLabel}
</Text>
) : (
<span />
)}
</Box>
)}
</Box>
<Box height="10px" backgroundColor="colorBackground" element={`${element}_BAR`} ref={ref}>
<Box width={fillWidth} height="10px" backgroundColor="colorBackgroundAvailable" element={`${element}_FILL`} />
</Box>
</Box>
);
});
);
}
);

Meter.displayName = 'Meter';

Expand Down
32 changes: 27 additions & 5 deletions packages/paste-core/components/meter/src/MeterLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
import * as React from 'react';
import {type BoxProps} from '@twilio-paste/box';
import {type BoxProps, Box} from '@twilio-paste/box';
import {Label} from '@twilio-paste/label';
import {Text} from '@twilio-paste/text';
import type {HTMLPasteProps} from '@twilio-paste/types';

import {LABEL_SUFFIX} from './constants';

export interface MeterLabelProps extends HTMLPasteProps<'div'>, Pick<BoxProps, 'element'> {
children: string;
htmlFor: string;
valueLabel?: string;
}

const MeterLabel = React.forwardRef<HTMLLabelElement, MeterLabelProps>(
({element = 'METER_LABEL', children, htmlFor, ...labelProps}, ref) => {
({element = 'METER_LABEL', children, htmlFor, valueLabel, ...labelProps}, ref) => {
return (
<Label {...labelProps} as="div" element={element} id={`${htmlFor}${LABEL_SUFFIX}`} ref={ref}>
{children}
</Label>
<Box
display="flex"
flexDirection="row"
justifyContent="space-between"
alignItems="flex-end"
element={`${element}_WRAPPER`}
>
<Label {...labelProps} as="div" element={element} id={`${htmlFor}${LABEL_SUFFIX}`} ref={ref}>
{children}
</Label>
{valueLabel && (
<Text
as="span"
fontWeight="fontWeightSemibold"
marginBottom="space20"
marginLeft="space20"
aria-hidden="true"
element={`${element}_VALUE_LABEL`}
>
{valueLabel}
</Text>
)}
</Box>
);
}
);
Expand Down

0 comments on commit 6a7ed21

Please sign in to comment.