Skip to content

Commit

Permalink
feat(Search): add handling of html input props (#1442)
Browse files Browse the repository at this point in the history
* feat(Search): add name prop

* feat(Search): add name prop

* feat(Search): add name prop
  • Loading branch information
layershifter authored and levithomason committed Mar 14, 2017
1 parent 7d72812 commit f30aee4
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 74 deletions.
46 changes: 6 additions & 40 deletions src/elements/Input/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getElementType,
getUnhandledProps,
META,
partitionHTMLInputProps,
SUI,
useKeyOnly,
useValueAndKey,
Expand All @@ -17,40 +18,6 @@ import Button from '../../elements/Button'
import Icon from '../../elements/Icon'
import Label from '../../elements/Label'

export const htmlInputPropNames = [
// REACT
'selected', 'defaultValue', 'defaultChecked',

// LIMITED HTML PROPS
'autoCapitalize', 'autoComplete', 'autoFocus', 'checked', 'form', 'max', 'maxLength', 'min', 'multiple',
'name', 'pattern', 'placeholder', 'readOnly', 'required', 'step', 'type', 'value',

// Heads Up!
// Do not pass disabled, it duplicates the SUI CSS opacity rule.
// 'disabled',

// EVENTS
// keyboard
'onKeyDown', 'onKeyPress', 'onKeyUp',

// focus
'onFocus', 'onBlur',

// form
'onChange', 'onInput',

// mouse
'onClick', 'onContextMenu',
'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave', 'onDragOver', 'onDragStart', 'onDrop',
'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver', 'onMouseUp',

// selection
'onSelect',

// touch
'onTouchCancel', 'onTouchEnd', 'onTouchMove', 'onTouchStart',
]

/**
* An Input is a field used to elicit a response from a user.
* @see Button
Expand Down Expand Up @@ -148,10 +115,10 @@ class Input extends Component {
}

handleChange = (e) => {
const { onChange } = this.props
const value = _.get(e, 'target.value')

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

render() {
Expand Down Expand Up @@ -195,14 +162,13 @@ class Input extends Component {
className,
)
const unhandled = getUnhandledProps(Input, this.props)
const ElementType = getElementType(Input, this.props)

const rest = _.omit(unhandled, htmlInputPropNames)
// Heads up! We should pass `type` prop manually because `Input` component handles it
const [htmlInputProps, rest] = partitionHTMLInputProps({ ...unhandled, type })

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

const ElementType = getElementType(Input, this.props)

// tabIndex
if (!_.isNil(tabIndex)) htmlInputProps.tabIndex = tabIndex
else if (disabled) htmlInputProps.tabIndex = -1
Expand Down
54 changes: 54 additions & 0 deletions src/lib/htmlInputPropsUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import _ from 'lodash'

export const htmlInputAttrs = [
// REACT
'selected', 'defaultValue', 'defaultChecked',

// LIMITED HTML PROPS
'autoCapitalize', 'autoComplete', 'autoFocus', 'checked', 'form', 'max', 'maxLength', 'min', 'multiple',
'name', 'pattern', 'placeholder', 'readOnly', 'required', 'step', 'type', 'value',

// Heads Up!
// Do not pass disabled, it duplicates the SUI CSS opacity rule.
// 'disabled',
]

export const htmlInputEvents = [
// EVENTS
// keyboard
'onKeyDown', 'onKeyPress', 'onKeyUp',

// focus
'onFocus', 'onBlur',

// form
'onChange', 'onInput',

// mouse
'onClick', 'onContextMenu',
'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave', 'onDragOver', 'onDragStart', 'onDrop',
'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver', 'onMouseUp',

// selection
'onSelect',

// touch
'onTouchCancel', 'onTouchEnd', 'onTouchMove', 'onTouchStart',
]

export const htmlInputProps = [...htmlInputAttrs, ...htmlInputEvents]

/**
* Returns an array of objects consisting of: props of html input element and rest.
* @param {object} props A ReactElement props object
* @param {array} [htmlProps] An array of html input props
* @returns {[{}, {}]} An array of objects
*/
export const partitionHTMLInputProps = (props, htmlProps = htmlInputProps) => {
const inputProps = {}
const rest = {}

_.forEach(props, (val, prop) => _.includes(htmlProps, prop) ? (inputProps[prop] = val) : (rest[prop] = val))

return [inputProps, rest]
}
8 changes: 8 additions & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export {
export * from './factories'
export { default as getUnhandledProps } from './getUnhandledProps'
export { default as getElementType } from './getElementType'

export {
htmlInputAttrs,
htmlInputEvents,
htmlInputProps,
partitionHTMLInputProps,
} from './htmlInputPropsUtils'

export { default as isBrowser } from './isBrowser'
export { default as leven } from './leven'
export * as META from './META'
Expand Down
3 changes: 0 additions & 3 deletions src/modules/Search/Search.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ interface SearchProps {
/** Controls whether or not the results menu is displayed. */
open?: boolean;

/** Placeholder of the search input. */
placeholder?: string;

/**
* One of:
* - array of Search.Result props e.g. `{ title: '', description: '' }` or
Expand Down
24 changes: 12 additions & 12 deletions src/modules/Search/Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import {
customPropTypes,
getElementType,
getUnhandledProps,
htmlInputAttrs,
isBrowser,
keyboardKey,
makeDebugger,
META,
objectDiff,
partitionHTMLInputProps,
SUI,
useKeyOnly,
useValueAndKey,
Expand Down Expand Up @@ -59,9 +61,6 @@ export default class Search extends Component {
/** Controls whether or not the results menu is displayed. */
open: PropTypes.bool,

/** Placeholder of the search input. */
placeholder: PropTypes.string,

/**
* One of:
* - array of Search.Result props e.g. `{ title: '', description: '' }` or
Expand Down Expand Up @@ -514,19 +513,19 @@ export default class Search extends Component {
// Render
// ----------------------------------------

renderSearchInput = () => {
const { icon, input, placeholder } = this.props
renderSearchInput = rest => {
const { icon, input } = this.props
const { value } = this.state

return Input.create(input, {
value,
placeholder,
...rest,
icon,
input: { className: 'prompt', tabIndex: '0', autoComplete: 'off' },
onBlur: this.handleBlur,
onChange: this.handleSearchChange,
onFocus: this.handleFocus,
onClick: this.handleInputClick,
input: { className: 'prompt', tabIndex: '0', autoComplete: 'off' },
icon,
onFocus: this.handleFocus,
value,
})
}

Expand Down Expand Up @@ -645,8 +644,9 @@ export default class Search extends Component {
'search',
className,
)
const rest = getUnhandledProps(Search, this.props)
const unhandled = getUnhandledProps(Search, this.props)
const ElementType = getElementType(Search, this.props)
const [htmlInputProps, rest] = partitionHTMLInputProps(unhandled, htmlInputAttrs)

return (
<ElementType
Expand All @@ -656,7 +656,7 @@ export default class Search extends Component {
onFocus={this.handleFocus}
onMouseDown={this.handleMouseDown}
>
{this.renderSearchInput()}
{this.renderSearchInput(htmlInputProps)}
{this.renderResultsMenu()}
</ElementType>
)
Expand Down
6 changes: 3 additions & 3 deletions test/specs/elements/Input/Input-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import cx from 'classnames'
import _ from 'lodash'
import React from 'react'

import Input, { htmlInputPropNames } from 'src/elements/Input/Input'
import { SUI } from 'src/lib'
import Input from 'src/elements/Input/Input'
import { htmlInputProps, SUI } from 'src/lib'
import * as common from 'test/specs/commonTests'
import { sandbox } from 'test/utils'

Expand Down Expand Up @@ -113,7 +113,7 @@ describe('Input', () => {
})

describe('input props', () => {
htmlInputPropNames.forEach(propName => {
htmlInputProps.forEach(propName => {
it(`passes \`${propName}\` to the <input>`, () => {
const propValue = propName === 'onChange' ? () => null : 'foo'
const wrapper = shallow(<Input {...{ [propName]: propValue }} />)
Expand Down
32 changes: 32 additions & 0 deletions test/specs/lib/htmlInputPropsUtils-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { partitionHTMLInputProps } from 'src/lib/htmlInputPropsUtils'

const props = {
className: 'foo',
name: 'bar',
placeholder: 'baz',
required: true,
}

describe('partitionHTMLInputProps', () => {
it('should return two arrays with objects', () => {
partitionHTMLInputProps(props).should.have.lengthOf(2)
})

it('should split props by definition', () => {
const [htmlInputProps, rest] = partitionHTMLInputProps(props)

htmlInputProps.should.deep.equal({
name: 'bar',
placeholder: 'baz',
required: true,
})
rest.should.deep.equal({ className: 'foo' })
})

it('should split props by own definition', () => {
const [htmlInputProps, rest] = partitionHTMLInputProps(props, ['placeholder', 'required'])

htmlInputProps.should.deep.equal({ placeholder: 'baz', required: true })
rest.should.deep.equal({ className: 'foo', name: 'bar' })
})
})
32 changes: 16 additions & 16 deletions test/specs/modules/Search/Search-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import _ from 'lodash'
import faker from 'faker'
import React from 'react'

import * as common from 'test/specs/commonTests'
import { domEvent, sandbox } from 'test/utils'
import Search from 'src/modules/Search/Search'
import { htmlInputAttrs } from 'src/lib'
import Search from 'src/modules/Search'
import SearchCategory from 'src/modules/Search/SearchCategory'
import SearchResult from 'src/modules/Search/SearchResult'
import SearchResults from 'src/modules/Search/SearchResults'
import * as common from 'test/specs/commonTests'
import { domEvent, sandbox } from 'test/utils'

let attachTo
let options
Expand Down Expand Up @@ -76,8 +77,9 @@ describe('Search', () => {
})

common.isConformant(Search)
common.hasUIClassName(Search)
common.hasSubComponents(Search, [SearchCategory, SearchResult, SearchResults])
common.hasUIClassName(Search)

common.propKeyOnlyToClassName(Search, 'category')
common.propKeyOnlyToClassName(Search, 'fluid')
common.propKeyOnlyToClassName(Search, 'loading')
Expand Down Expand Up @@ -728,18 +730,16 @@ describe('Search', () => {
})
})

describe('placeholder', () => {
it('is present when defined', () => {
wrapperShallow(<Search results={options} minCharacters={0} placeholder='hi' />)
.find('Input')
.first()
.should.have.prop('placeholder', 'hi')
})
it('is not present when not defined', () => {
wrapperShallow(<Search results={options} minCharacters={0} />)
.find('Input')
.first()
.should.not.have.prop('placeholder')
describe('input props', () => {
// Search handles some of html props
const props = _.without(htmlInputAttrs, 'defaultValue')

props.forEach(propName => {
it(`passes "${propName}" to the <input>`, () => {
wrapperMount(<Search {...{ [propName]: 'foo' }} />)
.find('input')
.should.have.prop(propName)
})
})
})
})

0 comments on commit f30aee4

Please sign in to comment.