Skip to content

Commit

Permalink
feat(input): Add field helper
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanoglesby08 committed Sep 11, 2017
1 parent f0b3ce6 commit 2b5de94
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 47 deletions.
44 changes: 44 additions & 0 deletions src/components/Input/Helper/Helper.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react'
import PropTypes from 'prop-types'

import safeRest from '../../../safeRest'
import ColoredTextProvider from '../../Typography/ColoredTextProvider/ColoredTextProvider'
import Paragraph from '../../Typography/Paragraph/Paragraph'

import styles from './Helper.modules.scss'

const getClassName = feedback => (feedback ? styles[feedback] : styles.default)

const getContent = (feedback, children) => {
const content = (
<Paragraph>
{children}
</Paragraph>
)

if (feedback === 'error') {
return (
<ColoredTextProvider colorClassName={styles.errorText}>
{content}
</ColoredTextProvider>
)
}

return content
}

const Helper = ({ feedback, children, ...rest }) => (
<div {...safeRest(rest)} className={getClassName(feedback)}>
{getContent(feedback, children)}
</div>
)
Helper.propTypes = {
feedback: PropTypes.oneOf(['success', 'error']),
children: PropTypes.node.isRequired
}
Helper.defaultProps = {
feedback: undefined
}
Helper.displayName = 'Input.Helper'

export default Helper
36 changes: 36 additions & 0 deletions src/components/Input/Helper/Helper.modules.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@import '../../../scss/settings/colours';
@import '../../../scss/settings/spacing';

$border-radius: 4px;

.base {
padding: $spacing-base;
margin-bottom: $spacing-tight; // TODO: This should be moved up to be consistent with form field spacing

border-radius: $border-radius;

// transition: background-color .1s linear; TODO: Why?
}

.default {
composes: base;

background: $color-athens-grey;
}

.success {
composes: base;

background: $color-panache;
}

.error {
composes: base;

background-color: $color-lavender-blush;
}

// TODO: This is duplicated in Notification...
.errorText {
color: $color-cardinal;
}
64 changes: 64 additions & 0 deletions src/components/Input/Helper/__tests__/Helper.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react'
import { shallow, render } from 'enzyme'
import toJson from 'enzyme-to-json'

import ColoredTextProvider from '../../../Typography/ColoredTextProvider/ColoredTextProvider'
import Paragraph from '../../../Typography/Paragraph/Paragraph'

import Helper from '../Helper'

describe('Helper', () => {
const defaultChildren = 'Some helper text.'
const doShallow = (props = {}, children = defaultChildren) => (
shallow(<Helper {...props}>{children}</Helper>)
)
const doRender = (props = {}, children = defaultChildren) => (
render(<Helper {...props}>{children}</Helper>)
)

it('renders', () => {
const helper = doRender()

expect(toJson(helper)).toMatchSnapshot()
})

it('can have a feedback state', () => {
let helper = doShallow()
expect(helper).toHaveClassName('default')

helper = doShallow({ feedback: 'success' })
expect(helper).toHaveClassName('success')
})

it('does not color the success content', () => {
const helper = doShallow({ feedback: 'success' }, 'A success message')

expect(helper).toContainReact(
<Paragraph>A success message</Paragraph>
)
})

it('colors the error content', () => {
const helper = doShallow({ feedback: 'error' }, 'An error message')

expect(helper).toContainReact(
<ColoredTextProvider colorClassName="errorText">
<Paragraph>An error message</Paragraph>
</ColoredTextProvider>
)
})

it('passes additional attributes to the element', () => {
const helper = doShallow({ id: 'the-helper', role: 'alert' })

expect(helper).toHaveProp('id', 'the-helper')
expect(helper).toHaveProp('role', 'alert')
})

it('does not allow custom CSS', () => {
const helper = doShallow({ className: 'my-custom-class', style: { color: 'hotpink' } })

expect(helper).not.toHaveProp('className', 'my-custom-class')
expect(helper).not.toHaveProp('style')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Helper renders 1`] = `
<div
class="default"
>
<p
class="
noSpacing
color
medium
mediumFont
leftAlign
"
>
Some helper text.
</p>
</div>
`;
40 changes: 28 additions & 12 deletions src/components/Input/Input.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types'

import Icon from '../../old-components/Icon/Icon'
import ColoredTextProvider from '../Typography/ColoredTextProvider/ColoredTextProvider'
import Paragraph from '../Typography/Paragraph/Paragraph'
import Fade from './Fade'
import Text from '../Typography/Text/Text'
import safeRest from '../../safeRest'
import Helper from './Helper/Helper'
import Fade from './Fade'

import styles from './Input.modules.scss'

Expand Down Expand Up @@ -84,23 +84,21 @@ class Input extends React.Component {
}

render() {
const { type, label, feedback, error, ...rest } = this.props
const { type, label, feedback, error, helper, ...rest } = this.props

const id = rest.id || rest.name || textToId(label)
const wrapperClassName = getWrapperClassName(feedback, this.state.focused, rest.disabled)
const showIcon = showFeedbackIcon(feedback, this.state.focused)

return (
<div>
<label htmlFor={id} className={styles.label}>{label}</label>
<label htmlFor={id} className={styles.label}>
<Text size="medium" bold>{label}</Text>
</label>

{ error &&
<div className={styles.errorMessage}>
<ColoredTextProvider colorClassName={styles.errorText}>
<Paragraph>{error}</Paragraph>
</ColoredTextProvider>
</div>
}
{ helper && React.cloneElement(helper, { feedback }) }

{ error && <Helper feedback="error">{error}</Helper> }

<div className={wrapperClassName} data-testID="inputWrapper">
<input
Expand Down Expand Up @@ -131,6 +129,21 @@ Input.propTypes = {
]),
feedback: PropTypes.oneOf(['success', 'error']),
error: PropTypes.string,
/* eslint-disable consistent-return */
helper: (props, propName, componentName) => {
const prop = props[propName]

if (!prop) {
return
}

if (prop.type !== Helper) {
return new Error(
`Unsupported value for \`helper\` on \`${componentName}\` component. Must be a \`Helper\` component.`
)
}
},
/* eslint-enable consistent-return */
onChange: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func
Expand All @@ -141,9 +154,12 @@ Input.defaultProps = {
value: '',
feedback: undefined,
error: undefined,
helper: undefined,
onChange: undefined,
onFocus: undefined,
onBlur: undefined
}

Input.Helper = Helper

export default Input
15 changes: 9 additions & 6 deletions src/components/Input/Input.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
```
<div>
<Input label="First name" />
<Input label="First name (default)" />
<Input type="password" label="Password" value="123abc" />
<Input type="number" label="Age" value="32" />
<Input label="First name 2" autoFocus />
<Input label="First name (auto focus)" autoFocus />
<Input label="First name 3" disabled />
<Input label="First name (disabled)" disabled />
<Input label="First name 4" feedback="success" />
<Input label="First name 5" feedback="error" />
<Input label="First name (success)" feedback="success" />
<Input label="First name (error)" feedback="error" />
<Input label="First name 6" feedback="error" error="First name is required" />
<Input label="First name (error msg)" feedback="error" error="First name is required" />
<Input label="First name (helper)" feedback="success"
helper={<Input.Helper>Some helper stuff</Input.Helper>}
/>
</div>
```
20 changes: 1 addition & 19 deletions src/components/Input/Input.modules.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@ $border-radius: 4px;
// TODO: Bring over/globalize the browser specific styles for the pseudo elements/placeholders

.label {
composes: medium boldFont from '../Typography/Text/Text.modules.scss';

display: block;
margin-bottom: $spacing-x-tight;

color: $color-text;
margin-bottom: $spacing-x-tight;
}

.inputWrapper {
Expand Down Expand Up @@ -97,18 +94,3 @@ input.input {
.icon {
padding: 0 $spacing-base;
}

.errorMessage {
padding: $spacing-base;
margin-bottom: $spacing-tight;

background-color: $color-lavender-blush;
border-radius: $border-radius;

// transition: background-color .1s linear; TODO: Why?
}

// TODO: This is duplicated in Notification...
.errorText {
color: $color-cardinal;
}
31 changes: 23 additions & 8 deletions src/components/Input/__tests__/Input.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { shallow, render } from 'enzyme'
import toJson from 'enzyme-to-json'

import Icon from '../../../old-components/Icon/Icon'
import ColoredTextProvider from '../../Typography/ColoredTextProvider/ColoredTextProvider'
import Paragraph from '../../Typography/Paragraph/Paragraph'
import Text from '../../Typography/Text/Text'
import Fade from '../Fade'
import Input from '../Input'
import Helper from '../Helper/Helper'

describe('Input', () => {
const defaultProps = {
Expand Down Expand Up @@ -41,7 +41,7 @@ describe('Input', () => {
it('must have a label', () => {
const input = doShallow({ label: 'The label' })

expect(input.find('label')).toHaveText('The label')
expect(input.find('label')).toContainReact(<Text size="medium" bold>The label</Text>)
})

describe('connecting the label to the input', () => {
Expand Down Expand Up @@ -195,11 +195,26 @@ describe('Input', () => {
it('can have an error message', () => {
const input = doShallow({ error: 'Oh no a terrible error!' })

expect(input).toContainReact(
<ColoredTextProvider colorClassName="errorText">
<Paragraph>Oh no a terrible error!</Paragraph>
</ColoredTextProvider>
)
expect(input).toContainReact(<Helper feedback="error">Oh no a terrible error!</Helper>)
})

describe('helpers', () => {
it('can have a helper', () => {
const helper = <Input.Helper>Some helper text.</Input.Helper>
const input = doShallow({ helper })

expect(input).toContainReact(helper)
})

it('styles itself based on the input feedback state', () => {
const helper = <Input.Helper>Some helper text.</Input.Helper>

let input = doShallow({ feedback: 'success', helper })
expect(input.find(Input.Helper).dive()).toHaveClassName('success')

input = doShallow({ feedback: 'error', helper })
expect(input.find(Input.Helper).dive()).toHaveClassName('error')
})
})

it('passes additional attributes to the input element', () => {
Expand Down
20 changes: 18 additions & 2 deletions src/components/Input/__tests__/__snapshots__/Input.spec.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ exports[`Input renders 1`] = `
class="label"
for="the-label"
>
The label
<span
class="
medium
boldFont
color
"
>
The label
</span>
</label>
<div
class="default"
Expand All @@ -28,7 +36,15 @@ exports[`Input renders with a feedback state and icon 1`] = `
class="label"
for="the-label"
>
The label
<span
class="
medium
boldFont
color
"
>
The label
</span>
</label>
<div
class="error"
Expand Down

0 comments on commit 2b5de94

Please sign in to comment.