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(Search): add handling of html input props #1442

Merged
merged 5 commits into from
Mar 14, 2017
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
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)
})
})
})
})