From 2b5de94120e916e414c0ec142e839e35a8d0ab79 Mon Sep 17 00:00:00 2001 From: Ryan Oglesby Date: Mon, 11 Sep 2017 11:47:13 -0500 Subject: [PATCH] feat(input): Add field helper --- src/components/Input/Helper/Helper.jsx | 44 +++++++++++++ .../Input/Helper/Helper.modules.scss | 36 +++++++++++ .../Input/Helper/__tests__/Helper.spec.jsx | 64 +++++++++++++++++++ .../__snapshots__/Helper.spec.jsx.snap | 19 ++++++ src/components/Input/Input.jsx | 40 ++++++++---- src/components/Input/Input.md | 15 +++-- src/components/Input/Input.modules.scss | 20 +----- src/components/Input/__tests__/Input.spec.jsx | 31 ++++++--- .../__snapshots__/Input.spec.jsx.snap | 20 +++++- 9 files changed, 242 insertions(+), 47 deletions(-) create mode 100644 src/components/Input/Helper/Helper.jsx create mode 100644 src/components/Input/Helper/Helper.modules.scss create mode 100644 src/components/Input/Helper/__tests__/Helper.spec.jsx create mode 100644 src/components/Input/Helper/__tests__/__snapshots__/Helper.spec.jsx.snap diff --git a/src/components/Input/Helper/Helper.jsx b/src/components/Input/Helper/Helper.jsx new file mode 100644 index 0000000000..741c014451 --- /dev/null +++ b/src/components/Input/Helper/Helper.jsx @@ -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 = ( + + {children} + + ) + + if (feedback === 'error') { + return ( + + {content} + + ) + } + + return content +} + +const Helper = ({ feedback, children, ...rest }) => ( +
+ {getContent(feedback, children)} +
+) +Helper.propTypes = { + feedback: PropTypes.oneOf(['success', 'error']), + children: PropTypes.node.isRequired +} +Helper.defaultProps = { + feedback: undefined +} +Helper.displayName = 'Input.Helper' + +export default Helper diff --git a/src/components/Input/Helper/Helper.modules.scss b/src/components/Input/Helper/Helper.modules.scss new file mode 100644 index 0000000000..9a97b7d495 --- /dev/null +++ b/src/components/Input/Helper/Helper.modules.scss @@ -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; +} diff --git a/src/components/Input/Helper/__tests__/Helper.spec.jsx b/src/components/Input/Helper/__tests__/Helper.spec.jsx new file mode 100644 index 0000000000..d2a78c6fc0 --- /dev/null +++ b/src/components/Input/Helper/__tests__/Helper.spec.jsx @@ -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({children}) + ) + const doRender = (props = {}, children = defaultChildren) => ( + render({children}) + ) + + 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( + A success message + ) + }) + + it('colors the error content', () => { + const helper = doShallow({ feedback: 'error' }, 'An error message') + + expect(helper).toContainReact( + + An error message + + ) + }) + + 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') + }) +}) diff --git a/src/components/Input/Helper/__tests__/__snapshots__/Helper.spec.jsx.snap b/src/components/Input/Helper/__tests__/__snapshots__/Helper.spec.jsx.snap new file mode 100644 index 0000000000..aa8aeea5ed --- /dev/null +++ b/src/components/Input/Helper/__tests__/__snapshots__/Helper.spec.jsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Helper renders 1`] = ` +
+

+ Some helper text. +

+
+`; diff --git a/src/components/Input/Input.jsx b/src/components/Input/Input.jsx index 0e99d4e941..03dc0b17ec 100644 --- a/src/components/Input/Input.jsx +++ b/src/components/Input/Input.jsx @@ -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' @@ -84,7 +84,7 @@ 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) @@ -92,15 +92,13 @@ class Input extends React.Component { return (
- + - { error && -
- - {error} - -
- } + { helper && React.cloneElement(helper, { feedback }) } + + { error && {error} }
{ + 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 @@ -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 diff --git a/src/components/Input/Input.md b/src/components/Input/Input.md index 80c9006223..2d13f5943e 100644 --- a/src/components/Input/Input.md +++ b/src/components/Input/Input.md @@ -1,18 +1,21 @@ ```
- + - + - + - - + + - + + Some helper stuff} + />
``` diff --git a/src/components/Input/Input.modules.scss b/src/components/Input/Input.modules.scss index 3c537a449e..68a53461d0 100644 --- a/src/components/Input/Input.modules.scss +++ b/src/components/Input/Input.modules.scss @@ -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 { @@ -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; -} diff --git a/src/components/Input/__tests__/Input.spec.jsx b/src/components/Input/__tests__/Input.spec.jsx index feb86db572..27160c68e7 100644 --- a/src/components/Input/__tests__/Input.spec.jsx +++ b/src/components/Input/__tests__/Input.spec.jsx @@ -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 = { @@ -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(The label) }) describe('connecting the label to the input', () => { @@ -195,11 +195,26 @@ describe('Input', () => { it('can have an error message', () => { const input = doShallow({ error: 'Oh no a terrible error!' }) - expect(input).toContainReact( - - Oh no a terrible error! - - ) + expect(input).toContainReact(Oh no a terrible error!) + }) + + describe('helpers', () => { + it('can have a helper', () => { + const helper = Some helper text. + const input = doShallow({ helper }) + + expect(input).toContainReact(helper) + }) + + it('styles itself based on the input feedback state', () => { + const helper = Some helper text. + + 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', () => { diff --git a/src/components/Input/__tests__/__snapshots__/Input.spec.jsx.snap b/src/components/Input/__tests__/__snapshots__/Input.spec.jsx.snap index e4b7828417..7a8cfb58c2 100644 --- a/src/components/Input/__tests__/__snapshots__/Input.spec.jsx.snap +++ b/src/components/Input/__tests__/__snapshots__/Input.spec.jsx.snap @@ -6,7 +6,15 @@ exports[`Input renders 1`] = ` class="label" for="the-label" > - The label + + The label +
- The label + + The label +