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

feat(component): convert Form to FC and remove static members #324

Merged
merged 1 commit into from
Jan 14, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions packages/big-design/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import '@testing-library/jest-dom/extend-expect';

import * as utils from './src/utils';
import { warning } from './src/utils/warning';

/**
* Because this util generates straightforward IDs which are saved in the snapshots,
Expand All @@ -19,20 +18,15 @@ jest.mock('./src/utils', () => {

return {
...jest.requireActual('./src/utils'),
warning: jest.fn(),
resetCounter: () => (counter = 0),
uniqueId: (context: string) => {
return `${context}${counter++}`;
},
};
});

jest.mock('./src/utils/warning', () => {
return {
warning: jest.fn(),
};
});

afterEach(() => {
(utils as any).resetCounter();
(warning as any).mockClear();
jest.clearAllMocks();
});
2 changes: 1 addition & 1 deletion packages/big-design/src/components/Checkbox/spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { fireEvent, render } from '@test/utils';
import 'jest-styled-components';
import React from 'react';

import { warning } from '../../utils/warning';
import { warning } from '../../utils';

import { Checkbox } from './index';
import { CheckboxLabel } from './Label';
Expand Down
35 changes: 10 additions & 25 deletions packages/big-design/src/components/Form/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import hoistNonReactStatics from 'hoist-non-react-statics';
import React, { Ref } from 'react';
import React, { forwardRef, Ref } from 'react';

import { Fieldset } from '../Fieldset';
import { typedMemo } from '../../utils';

import { StyledForm } from './styled';
import { FormControlError } from './Error';
import { FormGroup } from './Group';
import { FormControlLabel } from './Label';

interface PrivateProps {
forwardedRef: Ref<HTMLFormElement>;
Expand All @@ -16,23 +12,12 @@ export type FormProps = React.FormHTMLAttributes<HTMLFormElement> & {
fullWidth?: boolean;
};

class StyleableForm extends React.PureComponent<PrivateProps & FormProps> {
static Label = FormControlLabel;
static Error = FormControlError;
static Fieldset = Fieldset;
static Group = FormGroup;
const StyleableForm: React.FC<PrivateProps & FormProps> = ({ forwardedRef, ...props }) => (
<StyledForm {...props} ref={forwardedRef} />
);

render() {
const { forwardedRef, ...props } = this.props;

return <StyledForm {...props} ref={forwardedRef} />;
}
}

const FormWithForwardedRef = React.forwardRef<HTMLFormElement, FormProps>(({ className, style, ...props }, ref) => (
<StyleableForm {...props} forwardedRef={ref} />
));

export const Form = hoistNonReactStatics(FormWithForwardedRef, StyleableForm);

Form.displayName = 'Form';
export const Form = typedMemo(
forwardRef<HTMLFormElement, FormProps>(({ className, style, ...props }, ref) => (
<StyleableForm {...props} forwardedRef={ref} />
)),
);
19 changes: 13 additions & 6 deletions packages/big-design/src/components/Form/Group/Group.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ErrorIcon } from '@bigcommerce/big-design-icons';
import React from 'react';

import { uniqueId } from '../../../utils';
import { warning } from '../../../utils';
import { useUniqueId } from '../../../utils/useUniqueId';
import { Checkbox } from '../../Checkbox';
import { Radio } from '../../Radio';
import { FormControlError } from '../Error';
Expand All @@ -22,7 +23,7 @@ export const FormGroup: React.FC<GroupProps> = props => {
const renderErrors = () => {
// If Form.Group has errors prop, don't generate errors from children
if (groupErrors) {
return generateErrors(groupErrors);
return generateErrors(groupErrors, true);
}

return React.Children.map(children, child => {
Expand Down Expand Up @@ -51,8 +52,8 @@ export const FormGroup: React.FC<GroupProps> = props => {
);
};

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

if (typeof errors === 'string') {
return (
Expand All @@ -73,8 +74,14 @@ function generateErrors(errors: GroupProps['errors']): React.ReactNode {
}

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

return null;
if (!errors) {
return null;
}

if (fromGroup) {
warning('errors must be either a string, FormControlError, or an array of strings or FormControlError components.');
}
}
14 changes: 14 additions & 0 deletions packages/big-design/src/components/Form/Group/spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { render } from '@test/utils';
import 'jest-styled-components';
import React from 'react';

import { warning } from '../../../utils';
import { Input } from '../../Input';
import { FormControlError } from '../Error';

Expand Down Expand Up @@ -79,3 +80,16 @@ test('renders error prop with an array of FormControlError elements', () => {

testIds.forEach(id => expect(getByTestId(id)).toBeInTheDocument());
});

test('does not render invalid errors', () => {
const testId = 'test';
const errors = ['Error', <FormControlError>Error</FormControlError>, <div data-testid="testId">Error</div>];
const { queryByTestId } = render(
<FormGroup errors={errors}>
<Input />
</FormGroup>,
);

expect(warning).toBeCalledTimes(1);
expect(queryByTestId(testId)).not.toBeInTheDocument();
});
16 changes: 5 additions & 11 deletions packages/big-design/src/components/Form/spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import { Fieldset } from '../Fieldset';
import { Input } from '../Input';

import { Form } from './index';
import { Form, FormGroup } from './index';

test('forwards ref', () => {
const ref = React.createRef<HTMLFormElement>();
Expand All @@ -26,34 +26,28 @@ test('calls onSubmit', () => {
expect(onSubmit).toHaveBeenCalled();
});

test('exposes expected statics', () => {
expect(Form.Error).toBeDefined();
expect(Form.Label).toBeDefined();
expect(Form.Group).toBeDefined();
});

test('simple form render', () => {
const { container } = render(
<Form>
<Fieldset
legend="Primary contact"
description="Minim velit quis aute adipisicing adipisicing do do exercitation cupidatat enim ex voluptate consequat labore."
>
<Form.Group>
<FormGroup>
<Input
label="First Name"
description="This is an example description for First Name"
placeholder="Placeholder text"
/>
</Form.Group>
</FormGroup>

<Form.Group>
<FormGroup>
<Input
label="Middle Name"
description="This is an example description for Last Name. Featuring a Left Icon."
placeholder="Placeholder text"
/>
</Form.Group>
</FormGroup>
</Fieldset>
</Form>,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,6 @@ exports[`renders all together 1`] = `
width: 1.5rem;
}

.c1 {
color: #313440;
margin: 0 0 1rem;
color: #5E637A;
font-size: 0.875rem;
font-weight: 400;
line-height: 1.25rem;
margin: 0 0 0.75rem;
}

.c1:last-child {
margin-bottom: 0;
}

.c2 {
-webkit-transition: all 150ms ease-out;
transition: all 150ms ease-out;
Expand Down Expand Up @@ -119,6 +105,20 @@ exports[`renders all together 1`] = `
margin-top: -0.25rem;
}

.c1 {
color: #313440;
margin: 0 0 1rem;
color: #5E637A;
font-size: 0.875rem;
font-weight: 400;
line-height: 1.25rem;
margin: 0 0 0.75rem;
}

.c1:last-child {
margin-bottom: 0;
}

.c0 {
color: #313440;
margin: 0 0 1rem;
Expand Down
38 changes: 19 additions & 19 deletions packages/big-design/src/components/Input/spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'jest-styled-components';
import React from 'react';

import { warning } from '../../utils';
import { Form, FormControlDescription, FormControlError, FormControlLabel } from '../Form';
import { FormControlDescription, FormControlError, FormControlLabel, FormGroup } from '../Form';

import { Input } from './index';

Expand Down Expand Up @@ -76,9 +76,9 @@ test('renders a description', () => {
test('renders an error', () => {
const errorText = 'This is an error';
const { queryByText } = render(
<Form.Group>
<FormGroup>
<Input error={errorText} />
</Form.Group>,
</FormGroup>,
);

expect(queryByText(errorText)).toBeInTheDocument();
Expand Down Expand Up @@ -155,9 +155,9 @@ test('accepts an Error Component', () => {
);

const { queryByTestId } = render(
<Form.Group>
<FormGroup>
<Input error={CustomError} />
</Form.Group>,
</FormGroup>,
);

expect(queryByTestId('test')).toBeInTheDocument();
Expand All @@ -174,9 +174,9 @@ test('does not accept non-Error Components', () => {
);

const { queryByTestId } = render(
<Form.Group>
<FormGroup>
<Input error={NotAnError} />
</Form.Group>,
</FormGroup>,
);

expect(queryByTestId('test')).not.toBeInTheDocument();
Expand Down Expand Up @@ -219,17 +219,17 @@ test('renders all together', () => {
test('error shows with valid string', () => {
const error = 'Error';
const { container, rerender } = render(
<Form.Group>
<FormGroup>
<Input error="" />
</Form.Group>,
</FormGroup>,
);

expect(container.querySelector('[class*="StyledError"]')).not.toBeInTheDocument();

rerender(
<Form.Group>
<FormGroup>
<Input error={error} />
</Form.Group>,
</FormGroup>,
);

expect(container.querySelector('[class*="StyledError"]')).toBeInTheDocument();
Expand All @@ -238,9 +238,9 @@ test('error shows with valid string', () => {
test('error shows when an array of strings', () => {
const errors = ['Error 0', 'Error 1'];
const { getByText } = render(
<Form.Group>
<FormGroup>
<Input error={errors} />
</Form.Group>,
</FormGroup>,
);

errors.forEach(error => expect(getByText(error)).toBeInTheDocument());
Expand All @@ -250,9 +250,9 @@ test('error shows when an array of Errors', () => {
const testIds = ['error_0', 'error_1'];
const errors = testIds.map(id => <FormControlError data-testid={id}>Error</FormControlError>);
const { getByTestId } = render(
<Form.Group>
<FormGroup>
<Input error={errors} />
</Form.Group>,
</FormGroup>,
);

testIds.forEach(id => expect(getByTestId(id)).toBeInTheDocument());
Expand All @@ -262,9 +262,9 @@ describe('error does not show when invalid type', () => {
test('single element', () => {
const error = <div data-testid="err">Error</div>;
const { queryByTestId } = render(
<Form.Group>
<FormGroup>
<Input error={error} />
</Form.Group>,
</FormGroup>,
);

expect(warning).toHaveBeenCalledTimes(1);
Expand All @@ -275,9 +275,9 @@ describe('error does not show when invalid type', () => {
const errors = ['Error', <FormControlError>Error</FormControlError>, <div data-testid="err">Error</div>];

const { queryByTestId } = render(
<Form.Group>
<FormGroup>
<Input error={errors} />
</Form.Group>,
</FormGroup>,
);

expect(warning).toHaveBeenCalledTimes(1);
Expand Down
5 changes: 3 additions & 2 deletions packages/big-design/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import focusTrap, { FocusTrap } from 'focus-trap';
import React, { createRef, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

import { typedMemo, uniqueId } from '../../utils';
import { typedMemo } from '../../utils';
import { useUniqueId } from '../../utils/useUniqueId';
import { Button, ButtonProps } from '../Button';
import { H2 } from '../Typography';

Expand Down Expand Up @@ -46,7 +47,7 @@ export const Modal: React.FC<ModalProps> = typedMemo(
const [internalTrap, setInternalTrap] = useState<FocusTrap | null>(null);
const [initialBodyOverflowY, setInitialBodyOverflowY] = useState('');
const [modalContainer, setModalContainer] = useState<HTMLDivElement | null>(null);
const headerUniqueId = useMemo(() => uniqueId('modal_header_'), []);
const headerUniqueId = useUniqueId('modal_header');
const modalRef = createRef<HTMLDivElement>();
const previousFocus = useRef(typeof document !== 'undefined' ? document.activeElement : null);

Expand Down
Loading