Skip to content

Commit

Permalink
feat(help text): add customization (#1852)
Browse files Browse the repository at this point in the history
* chore(help-text): add customization capability

* test(input): add help text to accessibility tests

* refactor(help-text): replace Flex w/ Box, tests

* chore(help-text): fix TS errors

* chore(help-text):  remove unneeded dependencies

Co-authored-by: Glorili Alejandro <galejandro@twilio.com>
Co-authored-by: Simon Taggart <staggart@twilio.com>
  • Loading branch information
3 people committed Sep 20, 2021
1 parent 1a804b5 commit cba9f8f
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 55 deletions.
6 changes: 6 additions & 0 deletions .changeset/moody-elephants-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/help-text': minor
'@twilio-paste/core': minor
---

[Help-Text] Enable Component to respect element customizations set on the customization provider. Component now enables setting an element name on the underlying HTML element and checks the emotion theme object to determine whether it should merge in custom styles to the ones set by the component author.
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import * as React from 'react';
import {shallow} from 'enzyme';
import {CustomizationProvider} from '@twilio-paste/customization';
import {render, screen} from '@testing-library/react';
import {matchers} from 'jest-emotion';
import {HelpText} from '../src';

expect.extend(matchers);

describe('HelpText variant prop', () => {
const container = shallow(<HelpText variant="error" />);

Expand All @@ -10,14 +15,88 @@ describe('HelpText variant prop', () => {
});

it('should have colorTextError', () => {
expect(container.find('Text').prop('color')).toEqual('colorTextError');
expect(container.prop('color')).toEqual('colorTextError');
});
});

describe('HelpText marginTop prop', () => {
const container = shallow(<HelpText marginTop="space0" />);

it('should have marginTop: space0', () => {
expect(container.find('Flex').prop('marginTop')).toEqual('space0');
expect(container.prop('marginTop')).toEqual('space0');
});
});

describe('HelpText HTML attributes', () => {
it('should set element data attribute for help text', (): void => {
const {container} = render(<HelpText data-testid="help_text">This is help text.</HelpText>);

expect(container.querySelector('[data-paste-element="HELP_TEXT"]')).toBeInTheDocument();
expect(screen.getByTestId('help_text').getAttribute('data-paste-element')).toEqual('HELP_TEXT');
});

it('should set custom element data attribute for flex wrapper and help text', (): void => {
const {container} = render(
<HelpText element="foo" data-testid="help_text">
This is help text.
</HelpText>
);

expect(container.querySelector('[data-paste-element="foo"]')).toBeInTheDocument();
expect(screen.getByTestId('help_text').getAttribute('data-paste-element')).toEqual('foo');
});
});

describe('Customization', () => {
it('should customize help text default and error variant', (): void => {
render(
<CustomizationProvider
baseTheme="default"
// @ts-expect-error global test variable
theme={TestTheme}
elements={{
HELP_TEXT: {
color: 'colorTextSuccess',
fontWeight: 'fontWeightBold',
variants: {error: {color: 'colorTextWarningStrong'}},
},
}}
>
<HelpText data-testid="help_text">This is help text.</HelpText>
<HelpText data-testid="help_text_error" variant="error">
This is error help text.
</HelpText>
</CustomizationProvider>
);
expect(screen.getByTestId('help_text')).toHaveStyleRule('font-weight', '700');
expect(screen.getByTestId('help_text')).toHaveStyleRule('color', 'rgb(14,124,58)');
expect(screen.getByTestId('help_text_error')).toHaveStyleRule('color', 'rgb(141,49,24)');
});

it('should customize help text default and error variant with custom element name', (): void => {
render(
<CustomizationProvider
baseTheme="default"
// @ts-expect-error global test variable
theme={TestTheme}
elements={{
foo: {
color: 'colorTextSuccess',
fontWeight: 'fontWeightBold',
variants: {error: {color: 'colorTextWarningStrong'}},
},
}}
>
<HelpText element="foo" data-testid="help_text">
This is help text.
</HelpText>
<HelpText data-testid="help_text_error" variant="error" element="foo">
This is error help text.
</HelpText>
</CustomizationProvider>
);
expect(screen.getByTestId('help_text')).toHaveStyleRule('font-weight', '700');
expect(screen.getByTestId('help_text')).toHaveStyleRule('color', 'rgb(14,124,58)');
expect(screen.getByTestId('help_text_error')).toHaveStyleRule('color', 'rgb(141,49,24)');
});
});
4 changes: 0 additions & 4 deletions packages/paste-core/components/help-text/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@
"peerDependencies": {
"@twilio-paste/box": "^4.0.2",
"@twilio-paste/design-tokens": "^6.6.0",
"@twilio-paste/flex": "^2.0.2",
"@twilio-paste/icons": "^5.1.1",
"@twilio-paste/style-props": "^3.0.1",
"@twilio-paste/styling-library": "^0.3.1",
"@twilio-paste/text": "^4.0.1",
"@twilio-paste/theme": "^5.0.1",
"@twilio-paste/types": "^3.1.1",
"@twilio-paste/uid-library": "^0.2.1",
Expand All @@ -42,11 +40,9 @@
"devDependencies": {
"@twilio-paste/box": "^4.0.5",
"@twilio-paste/design-tokens": "^6.7.0",
"@twilio-paste/flex": "^2.0.3",
"@twilio-paste/icons": "^5.1.2",
"@twilio-paste/style-props": "^3.0.4",
"@twilio-paste/styling-library": "^0.3.2",
"@twilio-paste/text": "^4.0.3",
"@twilio-paste/theme": "^5.1.0",
"@twilio-paste/types": "^3.1.1",
"@twilio-paste/uid-library": "^0.2.1",
Expand Down
101 changes: 56 additions & 45 deletions packages/paste-core/components/help-text/src/HelpText.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import {ValueOf} from '@twilio-paste/types';
import {Box} from '@twilio-paste/box';
import {Flex} from '@twilio-paste/flex';
import {Text, safelySpreadTextProps} from '@twilio-paste/text';
import {TextColor} from '@twilio-paste/style-props';
import type {ValueOf} from '@twilio-paste/types';
import {Box, safelySpreadBoxProps} from '@twilio-paste/box';
import type {BoxProps} from '@twilio-paste/box';
import type {TextColor} from '@twilio-paste/style-props';
import {ErrorIcon} from '@twilio-paste/icons/esm/ErrorIcon';

export const HelpTextVariants = {
Expand All @@ -17,63 +16,75 @@ export const HelpTextVariants = {
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type HelpTextVariants = ValueOf<typeof HelpTextVariants>;

export interface HelpTextProps extends React.HTMLAttributes<HTMLDivElement> {
export interface HelpTextProps extends React.HTMLAttributes<HTMLDivElement>, Pick<BoxProps, 'element'> {
marginTop?: 'space0';
variant?: HelpTextVariants;
}

const HelpText = React.forwardRef<HTMLDivElement, HelpTextProps>(({marginTop, children, variant, ...props}, ref) => {
let icon = null;
switch (variant) {
case HelpTextVariants.ERROR:
icon = (
<Box flexShrink={0}>
<ErrorIcon color="colorTextError" decorative size="sizeIcon20" />
</Box>
);
break;
case HelpTextVariants.ERROR_INVERSE:
icon = (
<Box flexShrink={0}>
<ErrorIcon color="colorTextErrorWeak" decorative size="sizeIcon20" />
</Box>
);
break;
default:
break;
}
type VariantOptionsProps = {
[key in HelpTextVariants]: {
textColor: TextColor;
icon: JSX.Element | null;
};
};
const VariantOptions: VariantOptionsProps = {
[HelpTextVariants.DEFAULT]: {
textColor: 'colorTextWeak',
icon: null,
},
[HelpTextVariants.INVERSE]: {
textColor: 'colorTextInverseWeak',
icon: null,
},
[HelpTextVariants.ERROR]: {
textColor: 'colorTextError',
icon: (
<Box flexShrink={0}>
<ErrorIcon color="colorTextError" decorative size="sizeIcon20" />
</Box>
),
},
[HelpTextVariants.ERROR_INVERSE]: {
textColor: 'colorTextErrorWeak',
icon: (
<Box flexShrink={0}>
<ErrorIcon color="colorTextErrorWeak" decorative size="sizeIcon20" />
</Box>
),
},
};

let textColor = 'colorTextWeak' as TextColor;
if (variant === 'error') {
textColor = 'colorTextError';
} else if (variant === 'error_inverse') {
textColor = 'colorTextErrorWeak';
} else if (variant === 'inverse') {
textColor = 'colorTextInverseWeak';
}
const HelpText = React.forwardRef<HTMLDivElement, HelpTextProps>(
({marginTop, children, variant = 'default', element = 'HELP_TEXT', ...props}, ref) => {
const {textColor, icon} = VariantOptions[variant];

return (
<Flex marginTop={marginTop || 'space30'} ref={ref}>
{icon}
<Text
{...safelySpreadTextProps(props)}
return (
<Box
{...safelySpreadBoxProps(props)}
display="flex"
columnGap="space20"
marginTop={marginTop || 'space30'}
ref={ref}
element={element}
variant={variant}
as="div"
color={textColor}
fontSize="fontSize30"
lineHeight="lineHeight30"
marginLeft={icon ? 'space20' : undefined}
>
{children}
</Text>
</Flex>
);
});
{icon}
<span>{children}</span>
</Box>
);
}
);

HelpText.displayName = 'HelpText';

if (process.env.NODE_ENV === 'development') {
HelpText.propTypes = {
marginTop: PropTypes.oneOf(['space0']),
element: PropTypes.string,
};
}

Expand Down
69 changes: 65 additions & 4 deletions packages/paste-core/components/help-text/stories/input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as React from 'react';
import {Box} from '@twilio-paste/box';
import {Paragraph} from '@twilio-paste/paragraph';
import {CustomizationProvider} from '@twilio-paste/customization';
import {Stack} from '@twilio-paste/stack';
import {Card} from '@twilio-paste/card';
import {HelpText} from '../src';

// eslint-disable-next-line import/no-default-export
Expand All @@ -11,8 +15,11 @@ export default {
export const Default = (): React.ReactNode => {
return (
<>
<HelpText>Info that helps a user with this field.</HelpText>
<HelpText variant="error">Info that helps a user with this field.</HelpText>
<HelpText>Please enter a valid email.</HelpText>
<HelpText variant="error">Please enter a valid email.</HelpText>
<Box maxWidth="size20">
<HelpText variant="error">Please enter a valid email.</HelpText>
</Box>
</>
);
};
Expand All @@ -24,12 +31,66 @@ Default.story = {
export const Inverse = (): React.ReactNode => {
return (
<Box padding="space70" backgroundColor="colorBackgroundBodyInverse">
<HelpText variant="inverse">Info that helps a user with this field.</HelpText>
<HelpText variant="error_inverse">Info that helps a user with this field.</HelpText>
<HelpText variant="inverse">Limit to 60 characters.</HelpText>
<HelpText variant="error_inverse">Limit to 60 characters.</HelpText>
</Box>
);
};

Inverse.story = {
name: 'inverse',
};

export const Customized = (): React.ReactNode => {
return (
<>
<Stack orientation="vertical" spacing="space80">
<Box>
<Paragraph>Default Help Text:</Paragraph>
<Card>
<HelpText>Please enter a valid phone number.</HelpText>
<HelpText variant="error">Please enter a valid phone number.</HelpText>
</Card>
</Box>
<Box>
<Paragraph>Customized Help Text:</Paragraph>
<Card>
<CustomizationProvider
baseTheme="default"
elements={{
HELP_TEXT: {
color: 'colorTextSuccess',
fontWeight: 'fontWeightBold',
variants: {error: {color: 'colorTextWarningStrong'}},
},
}}
>
<HelpText>Please enter a valid phone number.</HelpText>
<HelpText variant="error">Please enter a valid phone number.</HelpText>
</CustomizationProvider>
</Card>
</Box>
<Box>
<Paragraph>Customized Help Text with custom element attribute:</Paragraph>
<Card>
<CustomizationProvider
baseTheme="default"
elements={{
foo: {
color: 'colorTextSuccess',
fontWeight: 'fontWeightBold',
variants: {error: {color: 'colorTextWarningStrong'}},
},
}}
>
<HelpText element="foo">Please enter a valid phone number.</HelpText>
<HelpText element="foo" variant="error">
Please enter a valid phone number.
</HelpText>
</CustomizationProvider>
</Card>
</Box>
</Stack>
</>
);
};
2 changes: 2 additions & 0 deletions packages/paste-core/components/input/__tests__/input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('Input render', () => {
<Theme.Provider theme="console">
<Label htmlFor="input_1">Label Text</Label>
<Input id="input_1" type="text" value="test" onChange={NOOP} />
<HelpText>Help text.</HelpText>
</Theme.Provider>
);
const results = await axe(container);
Expand All @@ -87,6 +88,7 @@ describe('Input render', () => {
Label Text
</Label>
<Input id="input_2" type="text" value="test" onChange={NOOP} disabled />
<HelpText>Help text.</HelpText>
</Theme.Provider>
);
const results = await axe(container);
Expand Down

0 comments on commit cba9f8f

Please sign in to comment.