Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(meter): add docs, update designs #3463

Merged
merged 1 commit into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

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

question: this is a breaking api change. Are we comfortable marking these changes as a patch when the package is on 1.0.0?

I think it's fine, since the docs aren't even out. But just wanted to call it out

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good point, I agree that it's fine since nobody's using it and it is an alpha component

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
33 changes: 28 additions & 5 deletions packages/paste-core/components/meter/src/MeterLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
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
nkrantz marked this conversation as resolved.
Show resolved Hide resolved
as="span"
fontWeight="fontWeightSemibold"
textAlign="end"
marginBottom="space20"
marginLeft="space20"
aria-hidden="true"
element={`${element}_VALUE_LABEL`}
>
{valueLabel}
</Text>
)}
</Box>
);
}
);
Expand Down