Skip to content

Commit

Permalink
feat(component): update error state handling on form component (#129)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Form.Row components are renamed to Form.Group

* feat: wip input error states

* feat: improved error handling

* feat: add inline documentation to Group

* feat: rebase and fix small issues

* test: update tests for feature

* fix: update PR comments
  • Loading branch information
chanceaclark authored Aug 7, 2019
1 parent 16780fe commit e665479
Show file tree
Hide file tree
Showing 26 changed files with 429 additions and 230 deletions.
2 changes: 1 addition & 1 deletion packages/big-design/src/components/Form/Error/Error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import React from 'react';
import { Small, SmallProps } from '../../Typography';

export const Error: React.FC<SmallProps> = ({ className, style, ...props }) => (
<Small color="danger" marginTop="xxSmall" {...props} />
<Small color="danger" margin="none" marginLeft="xxSmall" {...props} />
);
4 changes: 2 additions & 2 deletions packages/big-design/src/components/Form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import React, { Ref } from 'react';
import { StyledForm } from './styled';
import { Error as FormError } from './Error';
import { Fieldset } from './Fieldset';
import { Group } from './Group';
import { Label } from './Label';
import { Row } from './Row';

interface PrivateProps {
forwardedRef: Ref<HTMLFormElement>;
Expand All @@ -17,7 +17,7 @@ class StyleableForm extends React.PureComponent<PrivateProps & FormProps> {
static Label = Label;
static Error = FormError;
static Fieldset = Fieldset;
static Row = Row;
static Group = Group;

render() {
const { forwardedRef, ...props } = this.props;
Expand Down
80 changes: 80 additions & 0 deletions packages/big-design/src/components/Form/Group/Group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ErrorIcon } from '@bigcommerce/big-design-icons';
import React from 'react';

import { uniqueId } from '../../../utils';
import { Checkbox } from '../../Checkbox';
import { Radio } from '../../Radio';
import { Error as FormError } from '../Error';

import { StyledError, StyledGroup, StyledInlineGroup } from './styled';

export interface GroupProps extends React.HTMLAttributes<HTMLDivElement> {
errors?: string | React.ReactChild | Array<string | React.ReactChild>;
}

export const Group: React.FC<GroupProps> = props => {
const { children, errors: groupErrors } = props;
const childrenCount = React.Children.count(children);
const inline = !React.Children.toArray(children).every(child => {
return React.isValidElement(child) && (child.type === Checkbox || child.type === Radio);
});

const renderErrors = () => {
// If Form.Group has errors prop, don't generate errors from children
if (groupErrors) {
return generateErrors(groupErrors);
}

return React.Children.map(children, child => {
if (React.isValidElement(child)) {
const { error } = child.props;

return generateErrors(error);
}
});
};

if (inline) {
return (
<StyledInlineGroup childrenCount={childrenCount}>
{children}
{renderErrors()}
</StyledInlineGroup>
);
}

return (
<StyledGroup>
{children}
{renderErrors()}
</StyledGroup>
);
};

function generateErrors(errors: GroupProps['errors']): React.ReactNode {
const errorKey = uniqueId('formGroup_error_');

if (typeof errors === 'string') {
return (
<StyledError alignItems="center" key={errorKey}>
<ErrorIcon color="danger" />
<FormError>{errors}</FormError>
</StyledError>
);
}

if (React.isValidElement(errors) && errors.type === FormError) {
return (
<StyledError alignItems="center" key={errorKey}>
<ErrorIcon color="danger" />
{errors}
</StyledError>
);
}

if (Array.isArray(errors)) {
return errors.map(error => error && generateErrors(error));
}

return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders a form group 1`] = `
.c0 {
display: grid;
grid-gap: 0.5rem 1rem;
margin-bottom: 1rem;
}
.c0:last-child {
margin-bottom: 0;
}
@media (min-width:720px) {
.c0 .styled__StyledInputWrapper-g32raa-0,
.c0 .styled__StyledTextareaWrapper-c1uos0-0 {
max-width: 26rem;
}
}
<div
class="c0"
/>
`;
1 change: 1 addition & 0 deletions packages/big-design/src/components/Form/Group/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Group';
80 changes: 80 additions & 0 deletions packages/big-design/src/components/Form/Group/spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { render } from '@testing-library/react';
import 'jest-styled-components';
import React from 'react';

import { Input } from '../../Input';

import { Group } from './';

test('renders a form group', () => {
const { container } = render(<Group />);

expect(container.firstChild).toMatchSnapshot();
});

test('renders group with input', () => {
const { container } = render(
<Group>
<Input />
</Group>,
);

expect(container.querySelector('input')).toBeInTheDocument();
});

test('renders group and input with error', () => {
const error = 'Error';
const { getByText } = render(
<Group>
<Input error={error} />
</Group>,
);

expect(getByText(error)).toBeInTheDocument();
});

test('renders group with error prop', () => {
const error = 'Error';
const { getByText } = render(
<Group errors={error}>
<Input />
</Group>,
);

expect(getByText(error)).toBeInTheDocument();
});

test('renders error prop with an array of errors', () => {
const errors = ['Error 1', 'Error 2', 'Error 3'];
const { getByText } = render(
<Group errors={errors}>
<Input />
</Group>,
);

errors.forEach(error => expect(getByText(error)).toBeInTheDocument());
});

test('renders error with Input.Error element', () => {
const testId = 'test';
const errors = <Input.Error data-testid={testId}>Error</Input.Error>;
const { getByTestId } = render(
<Group errors={errors}>
<Input />
</Group>,
);

expect(getByTestId(testId)).toBeInTheDocument();
});

test('renders error prop with an array of Input.Error elements', () => {
const testIds = ['test_1', 'test_2', 'test_3'];
const errors = testIds.map(id => <Input.Error data-testid={id}>Error</Input.Error>);
const { getByTestId } = render(
<Group errors={errors}>
<Input />
</Group>,
);

testIds.forEach(id => expect(getByTestId(id)).toBeInTheDocument());
});
65 changes: 65 additions & 0 deletions packages/big-design/src/components/Form/Group/styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { remCalc, theme as defaultTheme } from '@bigcommerce/big-design-theme';
import styled, { css } from 'styled-components';

import { Flex } from '../../Flex';
import { StyledInputWrapper } from '../../Input/styled';
import { StyledTextareaWrapper } from '../../Textarea/styled';

interface StyledProps {
childrenCount?: number;
}

const SharedGroupStyles = css`
display: grid;
grid-gap: ${({ theme }) => `${theme.spacing.xSmall} ${theme.spacing.medium}`};
margin-bottom: ${({ theme }) => theme.spacing.medium};
${({ theme }) => theme.breakpoints.tablet} {
${StyledInputWrapper},
${StyledTextareaWrapper} {
max-width: ${remCalc(416)};
}
}
&:last-child {
margin-bottom: ${({ theme }) => theme.spacing.none};
}
`;

export const StyledError = styled(Flex)`
flex-direction: row;
`;

export const StyledGroup = styled.div`
${SharedGroupStyles};
`;

export const StyledInlineGroup = styled.div<StyledProps>`
${SharedGroupStyles};
${({ theme }) => theme.breakpoints.tablet} {
${({ childrenCount }) =>
childrenCount === 2 &&
css`
grid-template-columns: repeat(2, ${remCalc(200)});
${StyledError} {
grid-column: 1 / 3;
}
`}
${({ childrenCount }) =>
childrenCount === 3 &&
css`
grid-template-columns: repeat(3, ${remCalc(128)});
${StyledError} {
grid-column: 1 / 4;
}
`}
}
`;

StyledError.defaultProps = { theme: defaultTheme };
StyledGroup.defaultProps = { theme: defaultTheme };
StyledInlineGroup.defaultProps = { theme: defaultTheme };
14 changes: 0 additions & 14 deletions packages/big-design/src/components/Form/Row/Row.tsx

This file was deleted.

1 change: 0 additions & 1 deletion packages/big-design/src/components/Form/Row/index.ts

This file was deleted.

41 changes: 0 additions & 41 deletions packages/big-design/src/components/Form/Row/styled.tsx

This file was deleted.

Loading

0 comments on commit e665479

Please sign in to comment.