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(Input): add onChange(e, props) #846

Merged
merged 2 commits into from
Nov 13, 2016
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
284 changes: 150 additions & 134 deletions src/elements/Input/Input.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import _ from 'lodash'
import React, { PropTypes } from 'react'
import React, { Component, PropTypes } from 'react'
import cx from 'classnames'

import {
Expand Down Expand Up @@ -42,91 +42,7 @@ export const htmlInputPropNames = [
'value',
]

/**
* An Input is a field used to elicit a response from a user
* @see Button
* @see Form
* @see Icon
* @see Label
*/
function Input(props) {
const {
action,
actionPosition,
children,
className,
disabled,
error,
focus,
fluid,
icon,
iconPosition,
inverted,
label,
labelPosition,
loading,
size,
type,
input,
transparent,
} = props

const classes = cx(
'ui',
size,
useValueAndKey(actionPosition, 'action') || useKeyOnly(action, 'action'),
useKeyOnly(disabled, 'disabled'),
useKeyOnly(error, 'error'),
useKeyOnly(focus, 'focus'),
useKeyOnly(fluid, 'fluid'),
useKeyOnly(inverted, 'inverted'),
useValueAndKey(labelPosition, 'labeled') || useKeyOnly(label, 'labeled'),
useKeyOnly(loading, 'loading'),
useKeyOnly(transparent, 'transparent'),
useValueAndKey(iconPosition, 'icon') || useKeyOnly(icon, 'icon'),
className,
'input',
)
const unhandled = getUnhandledProps(Input, props)

const rest = _.omit(unhandled, htmlInputPropNames)
const htmlInputProps = _.pick(props, htmlInputPropNames)
const ElementType = getElementType(Input, props)

if (children) {
return <ElementType {...rest} className={classes}>{children}</ElementType>
}

const actionElement = Button.create(action, elProps => ({
className: cx(
// all action components should have the button className
!_.includes(elProps.className, 'button') && 'button',
),
}))
const iconElement = Icon.create(icon)
const labelElement = Label.create(label, elProps => ({
className: cx(
// all label components should have the label className
!_.includes(elProps.className, 'label') && 'label',
// add 'left|right corner'
_.includes(labelPosition, 'corner') && labelPosition,
),
}))

return (
<ElementType {...rest} className={classes}>
{actionPosition === 'left' && actionElement}
{iconPosition === 'left' && iconElement}
{labelPosition !== 'right' && labelElement}
{createHTMLInput(input || type, htmlInputProps)}
{actionPosition !== 'left' && actionElement}
{iconPosition !== 'left' && iconElement}
{labelPosition === 'right' && labelElement}
</ElementType>
)
}

Input._meta = {
const _meta = {
name: 'Input',
type: META.TYPES.ELEMENT,
props: {
Expand All @@ -137,73 +53,173 @@ Input._meta = {
},
}

Input.defaultProps = {
type: 'text',
}
/**
* An Input is a field used to elicit a response from a user
* @see Button
* @see Form
* @see Icon
* @see Label
*/
class Input extends Component {
static propTypes = {
/** An element type to render as (string or function). */
as: customPropTypes.as,

/** An Input can be formatted to alert the user to an action they may perform */
action: PropTypes.oneOfType([
PropTypes.bool,
customPropTypes.itemShorthand,
]),

Input.propTypes = {
/** An element type to render as (string or function). */
as: customPropTypes.as,
/** An action can appear along side an Input on the left or right */
actionPosition: PropTypes.oneOf(_meta.props.actionPosition),

/** An Input can be formatted to alert the user to an action they may perform */
action: PropTypes.oneOfType([
PropTypes.bool,
customPropTypes.itemShorthand,
]),
/** Primary content. */
children: PropTypes.node,

/** An action can appear along side an Input on the left or right */
actionPosition: PropTypes.oneOf(Input._meta.props.actionPosition),
/** Additional classes. */
className: PropTypes.string,

/** Primary content. */
children: PropTypes.node,
/** An Input field can show that it is disabled */
disabled: PropTypes.bool,

/** Additional classes. */
className: PropTypes.string,
/** An Input field can show the data contains errors */
error: PropTypes.bool,

/** An Input field can show that it is disabled */
disabled: PropTypes.bool,
/** An Input field can show a user is currently interacting with it */
focus: PropTypes.bool,

/** An Input field can show the data contains errors */
error: PropTypes.bool,
/** Take on the size of it's container */
fluid: PropTypes.bool,

/** An Input field can show a user is currently interacting with it */
focus: PropTypes.bool,
/** Optional Icon to display inside the Input */
icon: PropTypes.oneOfType([
PropTypes.bool,
customPropTypes.itemShorthand,
]),

/** Take on the size of it's container */
fluid: PropTypes.bool,
/** An Icon can appear inside an Input on the left or right */
iconPosition: PropTypes.oneOf(_meta.props.iconPosition),

/** Optional Icon to display inside the Input */
icon: PropTypes.oneOfType([
PropTypes.bool,
customPropTypes.itemShorthand,
]),
/** Format to appear on dark backgrounds */
inverted: PropTypes.bool,

/** An Icon can appear inside an Input on the left or right */
iconPosition: PropTypes.oneOf(Input._meta.props.iconPosition),
/** Shorthand for creating the HTML Input */
input: customPropTypes.itemShorthand,

/** Format to appear on dark backgrounds */
inverted: PropTypes.bool,
/** Optional Label to display along side the Input */
label: customPropTypes.itemShorthand,

/** Shorthand for creating the HTML Input */
input: customPropTypes.itemShorthand,
/** A Label can appear outside an Input on the left or right */
labelPosition: PropTypes.oneOf(_meta.props.labelPosition),

/** Optional Label to display along side the Input */
label: customPropTypes.itemShorthand,
/** An Icon Input field can show that it is currently loading data */
loading: PropTypes.bool,

/** A Label can appear outside an Input on the left or right */
labelPosition: PropTypes.oneOf(Input._meta.props.labelPosition),
/** Called with (e, props) on change. */
onChange: PropTypes.func,

/** An Icon Input field can show that it is currently loading data */
loading: PropTypes.bool,
/** An Input can vary in size */
size: PropTypes.oneOf(_meta.props.size),

/** An Input can vary in size */
size: PropTypes.oneOf(Input._meta.props.size),
/** Transparent Input has no background */
transparent: PropTypes.bool,

/** Transparent Input has no background */
transparent: PropTypes.bool,
/** The HTML input type */
type: PropTypes.string,
}

/** The HTML input type */
type: PropTypes.string,
static defaultProps = {
type: 'text',
}

static _meta = _meta

handleChange = (e) => {
const { onChange } = this.props
if (onChange) onChange(e, this.props)
}

render() {
const {
action,
actionPosition,
children,
className,
disabled,
error,
focus,
fluid,
icon,
iconPosition,
inverted,
label,
labelPosition,
loading,
onChange,
size,
type,
input,
transparent,
} = this.props

const classes = cx(
'ui',
size,
useValueAndKey(actionPosition, 'action') || useKeyOnly(action, 'action'),
useKeyOnly(disabled, 'disabled'),
useKeyOnly(error, 'error'),
useKeyOnly(focus, 'focus'),
useKeyOnly(fluid, 'fluid'),
useKeyOnly(inverted, 'inverted'),
useValueAndKey(labelPosition, 'labeled') || useKeyOnly(label, 'labeled'),
useKeyOnly(loading, 'loading'),
useKeyOnly(transparent, 'transparent'),
useValueAndKey(iconPosition, 'icon') || useKeyOnly(icon, 'icon'),
className,
'input',
)
const unhandled = getUnhandledProps(Input, this.props)

const rest = _.omit(unhandled, htmlInputPropNames)

const htmlInputProps = _.pick(this.props, htmlInputPropNames)
if (onChange) htmlInputProps.onChange = this.handleChange

const ElementType = getElementType(Input, this.props)

if (children) {
return <ElementType {...rest} className={classes}>{children}</ElementType>
}

const actionElement = Button.create(action, elProps => ({
className: cx(
// all action components should have the button className
!_.includes(elProps.className, 'button') && 'button',
),
}))
const iconElement = Icon.create(icon)
const labelElement = Label.create(label, elProps => ({
className: cx(
// all label components should have the label className
!_.includes(elProps.className, 'label') && 'label',
// add 'left|right corner'
_.includes(labelPosition, 'corner') && labelPosition,
),
}))

return (
<ElementType {...rest} className={classes}>
{actionPosition === 'left' && actionElement}
{iconPosition === 'left' && iconElement}
{labelPosition !== 'right' && labelElement}
{createHTMLInput(input || type, htmlInputProps)}
{actionPosition !== 'left' && actionElement}
{iconPosition !== 'left' && iconElement}
{labelPosition === 'right' && labelElement}
</ElementType>
)
}
}

export default Input
28 changes: 26 additions & 2 deletions test/specs/elements/Input/Input-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import cx from 'classnames'
import _ from 'lodash'
import React from 'react'
import { sandbox } from 'test/utils'

import Input, { htmlInputPropNames } from 'src/elements/Input/Input'
import * as common from 'test/specs/commonTests'
Expand Down Expand Up @@ -56,10 +57,33 @@ describe('Input', () => {
describe('input props', () => {
htmlInputPropNames.forEach(propName => {
it(`passes \`${propName}\` to the <input>`, () => {
shallow(<Input {...{ [propName]: 'foo' }} />)
const propValue = propName === 'onChange' ? () => null : 'foo'
const wrapper = shallow(<Input {...{ [propName]: propValue }} />)

// account for overloading the onChange prop
const expectedValue = propName === 'onChange'
? wrapper.instance().handleChange
: propValue

wrapper
.find('input')
.should.have.prop(propName, 'foo')
.should.have.prop(propName, expectedValue)
})
})
})

describe('onChange', () => {
it('is called with (event, props) on change', () => {
const spy = sandbox.spy()
const event = { fake: 'event' }
const props = { 'data-foo': 'bar', onChange: spy }

const wrapper = shallow(<Input {...props} />)

wrapper.find('input').simulate('change', event)

spy.should.have.been.calledOnce()
spy.should.have.been.calledWithMatch(event, props)
})
})
})