From f30aee4cae5e390eb91aa46880f99e8b7a2ed250 Mon Sep 17 00:00:00 2001 From: Alexander Fedyashov Date: Tue, 14 Mar 2017 22:09:22 +0200 Subject: [PATCH] feat(Search): add handling of html input props (#1442) * feat(Search): add name prop * feat(Search): add name prop * feat(Search): add name prop --- src/elements/Input/Input.js | 46 +++--------------- src/lib/htmlInputPropsUtils.js | 54 ++++++++++++++++++++++ src/lib/index.js | 8 ++++ src/modules/Search/Search.d.ts | 3 -- src/modules/Search/Search.js | 24 +++++----- test/specs/elements/Input/Input-test.js | 6 +-- test/specs/lib/htmlInputPropsUtils-test.js | 32 +++++++++++++ test/specs/modules/Search/Search-test.js | 32 ++++++------- 8 files changed, 131 insertions(+), 74 deletions(-) create mode 100644 src/lib/htmlInputPropsUtils.js create mode 100644 test/specs/lib/htmlInputPropsUtils-test.js diff --git a/src/elements/Input/Input.js b/src/elements/Input/Input.js index 1ead08fbeb..8688caff61 100644 --- a/src/elements/Input/Input.js +++ b/src/elements/Input/Input.js @@ -9,6 +9,7 @@ import { getElementType, getUnhandledProps, META, + partitionHTMLInputProps, SUI, useKeyOnly, useValueAndKey, @@ -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 @@ -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() { @@ -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 diff --git a/src/lib/htmlInputPropsUtils.js b/src/lib/htmlInputPropsUtils.js new file mode 100644 index 0000000000..accf8ffda5 --- /dev/null +++ b/src/lib/htmlInputPropsUtils.js @@ -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] +} diff --git a/src/lib/index.js b/src/lib/index.js index ba6f9c627f..a241be811d 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -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' diff --git a/src/modules/Search/Search.d.ts b/src/modules/Search/Search.d.ts index 709bbdf570..f9836e387e 100644 --- a/src/modules/Search/Search.d.ts +++ b/src/modules/Search/Search.d.ts @@ -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 diff --git a/src/modules/Search/Search.js b/src/modules/Search/Search.js index daabaa7240..7e27fbf9e3 100644 --- a/src/modules/Search/Search.js +++ b/src/modules/Search/Search.js @@ -7,11 +7,13 @@ import { customPropTypes, getElementType, getUnhandledProps, + htmlInputAttrs, isBrowser, keyboardKey, makeDebugger, META, objectDiff, + partitionHTMLInputProps, SUI, useKeyOnly, useValueAndKey, @@ -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 @@ -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, }) } @@ -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 ( - {this.renderSearchInput()} + {this.renderSearchInput(htmlInputProps)} {this.renderResultsMenu()} ) diff --git a/test/specs/elements/Input/Input-test.js b/test/specs/elements/Input/Input-test.js index 44fdec32d6..597084c997 100644 --- a/test/specs/elements/Input/Input-test.js +++ b/test/specs/elements/Input/Input-test.js @@ -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' @@ -113,7 +113,7 @@ describe('Input', () => { }) describe('input props', () => { - htmlInputPropNames.forEach(propName => { + htmlInputProps.forEach(propName => { it(`passes \`${propName}\` to the `, () => { const propValue = propName === 'onChange' ? () => null : 'foo' const wrapper = shallow() diff --git a/test/specs/lib/htmlInputPropsUtils-test.js b/test/specs/lib/htmlInputPropsUtils-test.js new file mode 100644 index 0000000000..ae003c4d09 --- /dev/null +++ b/test/specs/lib/htmlInputPropsUtils-test.js @@ -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' }) + }) +}) diff --git a/test/specs/modules/Search/Search-test.js b/test/specs/modules/Search/Search-test.js index 4b11e49bd7..da94f019ae 100644 --- a/test/specs/modules/Search/Search-test.js +++ b/test/specs/modules/Search/Search-test.js @@ -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 @@ -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') @@ -728,18 +730,16 @@ describe('Search', () => { }) }) - describe('placeholder', () => { - it('is present when defined', () => { - wrapperShallow() - .find('Input') - .first() - .should.have.prop('placeholder', 'hi') - }) - it('is not present when not defined', () => { - wrapperShallow() - .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 `, () => { + wrapperMount() + .find('input') + .should.have.prop(propName) + }) }) }) })