Skip to content

Commit

Permalink
feat(861n1w5zd): helper text (#12)
Browse files Browse the repository at this point in the history
* feat: add helper text component

* feat: add tests

* fix: restore tsconfig

* fix: fix index import

* fix: fix beforeIcon props

* fix: fix types in helper text config

* fix: fix import

* fix: review changes

* fix icon type

* fix: refactor tests

* fix: refactor tests

* fix: removed falsy checking
  • Loading branch information
mateusz-kleszcz authored Aug 8, 2023
1 parent a1885a3 commit d9acc62
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 0 deletions.
18 changes: 18 additions & 0 deletions src/components/HelperText/HelperText.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { SystemProps } from '@xstyled/styled-components';

import { config } from './HelperText.styles';
import { HelperTextIntent } from './HelperTextIntent.type';

import { Theme } from '@/theme';
import { DeepPartial } from '@/utility-types/DeepPartial';

export type HelperTextProps = {
intent?: HelperTextIntent;
counter?: {
current: number;
max: number;
};
beforeIcon?: boolean;
text: string;
custom?: DeepPartial<SystemProps<Theme> & typeof config>;
};
41 changes: 41 additions & 0 deletions src/components/HelperText/HelperText.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from '@storybook/react';

import { HelperText } from './HelperText';

const meta = {
title: 'Components/HelperText',
component: HelperText,
tags: ['autodocs'],
args: {
text: 'Helper text',
intent: 'none',
},
argTypes: {
intent: {
options: ['none', 'alert', 'success'],
defaultValue: 'none',
control: { type: 'radio' },
},
},
} satisfies Meta<typeof HelperText>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const DefaultWithIcon: Story = {
args: {
beforeIcon: true,
},
};

export const DefaultWithCounter: Story = {
args: {
counter: {
current: 0,
max: 0,
},
},
};
46 changes: 46 additions & 0 deletions src/components/HelperText/HelperText.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { SystemProps } from '@xstyled/styled-components';

import { HelperTextIntent } from './HelperTextIntent.type';

import { Theme } from '@/theme';
import { IconName } from '@/utility-types/IconName';

type Config = {
intent: Record<HelperTextIntent, SystemProps>;
icon: Record<HelperTextIntent, SystemProps & { name: IconName<16> }>;
iconContainer: SystemProps;
};

export const config = {
display: 'flex',
alignItems: 'flex-start',
gap: 'component-gap-large',
text: 'medium-150',
intent: {
none: {
color: 'content-secondary',
},
alert: {
color: 'content-negative-secondary',
},
success: {
color: 'content-positive-secondary',
},
},
icon: {
none: {
name: '16-info',
},
alert: {
name: '16-alert-full',
},
success: {
name: '16-check',
},
},
iconContainer: {
display: 'flex',
alignItems: 'center',
minHeight: '2xSmall',
},
} as const satisfies SystemProps<Theme> & Config;
97 changes: 97 additions & 0 deletions src/components/HelperText/HelperText.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { HelperText } from './HelperText';
import { render } from '../../tests/render';

const getHelperText = (jsx: JSX.Element) => {
const { queryByTestId } = render(jsx);

return {
text: queryByTestId('helper-text'),
icon: queryByTestId('helper-text-icon'),
counter: queryByTestId('helper-text-counter'),
};
};

describe('HelperText', () => {
it('should render the helper text', () => {
const { text } = getHelperText(<HelperText text="Hello there" />);
expect(text).toBeInTheDocument();
});

it('should render correct text', () => {
const { text } = getHelperText(<HelperText text="Hello there" />);
expect(text).toHaveTextContent('Hello there');
});

it('should render correct intent color (none)', () => {
const { text, icon, counter } = getHelperText(
<HelperText
text="Hello there"
intent="none"
beforeIcon
counter={{ current: 0, max: 0 }}
/>
);
expect(text).toHaveStyle('color: rgb(85, 85, 85);');
expect(icon).toHaveStyle('color: rgb(85, 85, 85);');
expect(counter).toHaveStyle('color: rgb(85, 85, 85);');
});

it('should render correct intent color (alert)', () => {
const { text, icon, counter } = getHelperText(
<HelperText
text="Hello there"
intent="alert"
beforeIcon
counter={{ current: 0, max: 0 }}
/>
);
expect(text).toHaveStyle('color: rgb(197, 52, 52);');
expect(icon).toHaveStyle('color: rgb(197, 52, 52);');
expect(counter).toHaveStyle('color: rgb(197, 52, 52);');
});

it('should render correct intent color (success)', () => {
const { text, icon, counter } = getHelperText(
<HelperText
text="Hello there"
intent="success"
beforeIcon
counter={{ current: 0, max: 0 }}
/>
);
expect(text).toHaveStyle('color: rgb(29, 29, 29);');
expect(icon).toHaveStyle('color: rgb(29, 29, 29);');
expect(counter).toHaveStyle('color: rgb(29, 29, 29);');
});

it('should render icon if passed as a prop', () => {
const { icon } = getHelperText(
<HelperText text="Hello there" beforeIcon />
);
expect(icon).toBeInTheDocument();
});

it('should not render icon if not passed as a prop', () => {
const { icon } = getHelperText(<HelperText text="Hello there" />);
expect(icon).toBeNull();
});

it('should render counter if passed as a prop', () => {
const { counter } = getHelperText(
<HelperText text="Hello there" counter={{ current: 0, max: 0 }} />
);
expect(counter).toBeInTheDocument();
});

it('should not render counter if not passed as a prop', () => {
const { counter } = getHelperText(<HelperText text="Hello there" />);
expect(counter).toBeNull();
});

it('should throw an error if wrong config is provided', () => {
expect(() =>
// @ts-expect-error testing wrong appearance
render(<HelperText intent="default" />)
).toThrowError();
});
});
48 changes: 48 additions & 0 deletions src/components/HelperText/HelperText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Icon } from '@virtuslab/tetrisly-icons';
import { merge } from 'lodash';

import { HelperTextProps } from './HelperText.props';
import { config as defaultConfig } from './HelperText.styles';
import { tet } from '../../tetrisly';

import { isKeyOf } from '@/services';
import { MarginProps } from '@/types/MarginProps';

export const HelperText = ({
intent = 'none',
beforeIcon = false,
counter,
text,
custom = {},
...rest
}: HelperTextProps & MarginProps) => {
const {
intent: intentStyles,
icon: iconStyles,
iconContainer: iconContainerStyles,
...restStyles
} = merge(defaultConfig, custom);

if (!isKeyOf(intentStyles, intent)) {
throw new Error(`${intent} is not a valid intent`);
}

const styles = {
...restStyles,
...intentStyles[intent],
};

return (
<tet.div {...styles} {...rest} data-testid="helper-text">
{beforeIcon && (
<tet.span {...iconContainerStyles}>
<Icon {...iconStyles[intent]} data-testid="helper-text-icon" />
</tet.span>
)}
{text}
{!!counter && (
<tet.span data-testid="helper-text-counter">{`${counter.current}/${counter.max}`}</tet.span>
)}
</tet.div>
);
};
1 change: 1 addition & 0 deletions src/components/HelperText/HelperTextIntent.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type HelperTextIntent = 'none' | 'alert' | 'success';
1 change: 1 addition & 0 deletions src/components/HelperText/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HelperText } from './HelperText';
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { Button } from './components/Button';
export { HelperText } from './components/HelperText';
export { StatusDot } from './components/StatusDot';

0 comments on commit d9acc62

Please sign in to comment.