-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(component): update error state handling on form component (#129)
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
1 parent
16780fe
commit e665479
Showing
26 changed files
with
429 additions
and
230 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
24 changes: 24 additions & 0 deletions
24
packages/big-design/src/components/Form/Group/__snapshots__/spec.tsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
/> | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './Group'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.