diff --git a/packages/boundless-input/README.md b/packages/boundless-input/README.md index 724234ef..8d2196af 100644 --- a/packages/boundless-input/README.md +++ b/packages/boundless-input/README.md @@ -79,6 +79,12 @@ There are no required props. ### Optional Props +- __`component`__ ・ overrides the HTML container tag + + Expects | Default Value + - | - + `string` | `'div'` + - __`hidePlaceholderOnFocus`__ ・ triggers the placeholder to disappear when the input field is focused, reappears when the user has tabbed away or focus is moved Expects | Default Value diff --git a/packages/boundless-input/index.js b/packages/boundless-input/index.js index 6d62d712..eb072c30 100644 --- a/packages/boundless-input/index.js +++ b/packages/boundless-input/index.js @@ -23,6 +23,11 @@ When using `Input` in your project, you may call the following methods on a rend */ export default class Input extends React.PureComponent { static propTypes = { + /** + * overrides the HTML container tag + */ + component: PropTypes.string, + /** * triggers the placeholder to disappear when the input field is focused, reappears when the user has tabbed away or focus is moved */ @@ -43,6 +48,7 @@ export default class Input extends React.PureComponent { } static defaultProps = { + component: 'div', hidePlaceholderOnFocus: true, inputProps: { type: 'text', @@ -118,35 +124,32 @@ export default class Input extends React.PureComponent { getPlaceholderText() { const isNonEmpty = this.state.input !== ''; - const shouldShowPlaceholder = this.props.hidePlaceholderOnFocus === true - ? this.state.isFocused === false && isNonEmpty === false - : isNonEmpty === false; + const shouldShowPlaceholder = this.props.hidePlaceholderOnFocus === true + ? this.state.isFocused === false && isNonEmpty === false + : isNonEmpty === false; return shouldShowPlaceholder ? this.props.inputProps.placeholder : ''; } render() { - const {props} = this; - return ( - <div - {...omit(props, Input.internalKeys)} - ref='wrapper' - className={cx('b-input-wrapper', props.className)} + <this.props.component + {...omit(this.props, Input.internalKeys)} + className={cx('b-input-wrapper', this.props.className)} title={this.getPlaceholderText()}> <input - {...props.inputProps} + {...this.props.inputProps} ref='field' - className={cx('b-input', props.inputProps.className)} + className={cx('b-input', this.props.inputProps.className)} placeholder={null} onBlur={this.handleBlur} onFocus={this.handleFocus} onChange={this.handleChange} /> - <div ref='placeholder' className='b-input-placeholder b-input'> + <div className='b-input-placeholder b-input'> {this.getPlaceholderText()} </div> - </div> + </this.props.component> ); } } diff --git a/packages/boundless-input/index.spec.js b/packages/boundless-input/index.spec.js index b0b806be..4be78d8f 100644 --- a/packages/boundless-input/index.spec.js +++ b/packages/boundless-input/index.spec.js @@ -3,10 +3,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import sinon from 'sinon'; -import {noop} from 'lodash'; import Input from './index'; -import {conformanceChecker} from '../boundless-utils-test-helpers/index'; +import {$, conformanceChecker} from '../boundless-utils-test-helpers/index'; describe('Input component', () => { const mountNode = document.body.appendChild(document.createElement('div')); @@ -29,127 +28,66 @@ describe('Input component', () => { conformanceChecker(render, Input, props); }); - describe('accepts', () => { - it('arbitrary HTML attributes via props.inputProps', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - 'data-id': 'foo', - }} /> - ); - - expect(element.refs.field.getAttribute('data-id')).toBe('foo'); - }); - - it('additional classes via props.inputProps.className', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - className: 'foo', - }} /> - ); - - expect(element.refs.field.classList.contains('foo')).toBe(true); - }); + it('renders .b-input-wrapper', () => { + render(<Input {...props} />); + expect($('.b-input-wrapper')).not.toBeNull(); }); - describe('CSS hook', () => { - const hasClass = (dom, name) => dom.classList.contains(name); - - it('renders .b-input-wrapper', () => { - const element = render(<Input {...props} />); - - expect(hasClass(element.refs.wrapper, 'b-input-wrapper')).toBe(true); - }); + it('renders .b-input', () => { + render(<Input {...props} />); + expect($('.b-input')).not.toBeNull(); + }); - it('renders .b-input', () => { - const element = render(<Input {...props} />); + it('renders .b-input-placeholder', () => { + render(<Input {...props} inputProps={{placeholder: 'foo'}} />); + expect($('.b-input-placeholder')).not.toBeNull(); + }); - expect(hasClass(element.refs.field, 'b-input')).toBe(true); - }); + it('accepts a different wrapper component', () => { + render(<Input {...props} component='article' />); + expect($('article.b-input-wrapper')).not.toBeNull(); }); - it('renders the placeholder facsimile', () => { - const element = render(<Input {...props} />); + it('accepts arbitrary HTML attributes via props.inputProps', () => { + render(<Input {...props} inputProps={{'data-id': 'foo'}} />); + expect($('.b-input[data-id="foo"]')).not.toBeNull(); + }); - expect(element.refs.placeholder).not.toBeUndefined(); - expect(element.refs.placeholder.classList.contains('b-input-placeholder')).toBe(true); + it('accepts CSS classes via props.inputProps', () => { + render(<Input {...props} inputProps={{className: 'foo'}} />); + expect($('.b-input.foo')).not.toBeNull(); }); it('uses the proper placeholder text (via props.inputProps.placeholder)', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - placeholder: 'foo', - }} /> - ); - - expect(element.refs.placeholder.textContent).toBe('foo'); + render(<Input {...props} inputProps={{placeholder: 'foo'}} />); + expect($('.b-input-placeholder').textContent).toBe('foo'); }); it('does not empty the placeholder on input focus if `props.hidePlaceholderOnFocus` is false', () => { - const element = render( - <Input - {...props} - hidePlaceholderOnFocus={false} - inputProps={{ - ...props.inputProps, - placeholder: 'foo', - }} /> - ); - - expect(element.refs.placeholder).not.toBeUndefined(); - expect(element.refs.placeholder.textContent).toBe('foo'); + const element = render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{placeholder: 'foo'}} />); + expect($('.b-input-placeholder').textContent).toBe('foo'); element.handleFocus(); - - expect(element.refs.placeholder.textContent).toBe('foo'); + expect($('.b-input-placeholder').textContent).toBe('foo'); }); it('empties the placeholder on input focus if `props.hidePlaceholderOnFocus` is true', () => { - const element = render( - <Input - {...props} - hidePlaceholderOnFocus={true} - inputProps={{ - ...props.inputProps, - placeholder: 'foo', - }} /> - ); - - expect(element.refs.placeholder).not.toBeUndefined(); - expect(element.refs.placeholder.textContent).toBe('foo'); + const element = render(<Input {...props} hidePlaceholderOnFocus={true} inputProps={{placeholder: 'foo'}} />); + expect($('.b-input-placeholder').textContent).toBe('foo'); element.handleFocus(); - - expect(element.refs.placeholder.textContent).toBe(''); + expect($('.b-input-placeholder').textContent).toBe(''); }); it('fills in the placeholder on input blur if the the input is empty and `props.hidePlaceholderOnFocus` is true', () => { - const element = render( - <Input - {...props} - hidePlaceholderOnFocus={true} - inputProps={{ - ...props.inputProps, - placeholder: 'foo', - }} /> - ); - - expect(element.refs.placeholder).not.toBeUndefined(); - expect(element.refs.placeholder.textContent).toBe('foo'); + const element = render(<Input {...props} hidePlaceholderOnFocus={true} inputProps={{placeholder: 'foo'}} />); + expect($('.b-input-placeholder').textContent).toBe('foo'); element.handleFocus(); - expect(element.refs.placeholder.textContent).toBe(''); + expect($('.b-input-placeholder').textContent).toBe(''); element.handleBlur(); - expect(element.refs.placeholder.textContent).toBe('foo'); + expect($('.b-input-placeholder').textContent).toBe('foo'); }); describe('controlled mode', () => { @@ -157,192 +95,81 @@ describe('Input component', () => { beforeEach(() => sandbox.stub(console, 'error')); it('causes the placeholder to be filled in when the input is empty', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - placeholder: 'foo', - value: '', - }} /> - ); - - expect(element.refs.placeholder.textContent).toBe('foo'); + render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{placeholder: 'foo', value: ''}} />); + expect($('.b-input-placeholder').textContent).toBe('foo'); }); - it('causes the placeholder to be empty when the input is non-empty', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - placeholder: 'foo', - value: 'x', - }} /> - ); - - expect(element.refs.placeholder.textContent).toBe(''); + it('causes the placeholder to be emptied when the input has a value', () => { + render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{placeholder: 'foo', value: 'x'}} />); + expect($('.b-input-placeholder').textContent).toBe(''); }); it('properly manages placeholder visibility across many `props.inputProps.value` changes', () => { - let element; - - element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - onChange: noop, - placeholder: 'foo', - value: 'x', - }} /> - ); - expect(element.refs.placeholder.textContent).toBe(''); - - element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - onChange: noop, - placeholder: 'foo', - value: '', - }} /> - ); - expect(element.refs.placeholder.textContent).toBe('foo'); - - element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - onChange: noop, - placeholder: 'foo', - value: 'x', - }} /> - ); - expect(element.refs.placeholder.textContent).toBe(''); - - element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - onChange: noop, - placeholder: 'foo', - value: 'xy', - }} /> - ); - expect(element.refs.placeholder.textContent).toBe(''); - - element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - onChange: noop, - placeholder: 'foo', - value: '', - }} /> - ); - expect(element.refs.placeholder.textContent).toBe('foo'); + render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{placeholder: 'foo', value: 'x'}} />); + expect($('.b-input-placeholder').textContent).toBe(''); + + render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{placeholder: 'foo', value: ''}} />); + expect($('.b-input-placeholder').textContent).toBe('foo'); + + render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{placeholder: 'foo', value: 'x'}} />); + expect($('.b-input-placeholder').textContent).toBe(''); + + render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{placeholder: 'foo', value: 'xy'}} />); + expect($('.b-input-placeholder').textContent).toBe(''); + + render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{placeholder: 'foo', value: ''}} />); + expect($('.b-input-placeholder').textContent).toBe('foo'); }); - it('changes to the input text result do not result in a setState within the event handler', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - placeholder: 'foo', - value: '', - }} /> - ); + it('change events on the input are ignored and proxied to the composer', () => { + const changeStub = sandbox.stub(); + const element = render(<Input {...props} inputProps={{onChange: changeStub, placeholder: 'foo', value: ''}} />); - sandbox.stub(element, 'setState'); + sandbox.spy(element, 'setState'); element.handleChange({target: {value: 'foobar'}}); expect(element.setState.called).toBe(false); + expect(changeStub.calledOnce).toBe(true); }); }); describe('uncontrolled mode', () => { it('causes the placeholder to be filled in when the input is empty', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - placeholder: 'foo', - }} /> - ); - - expect(element.refs.placeholder.textContent).toBe('foo'); + render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{placeholder: 'foo'}} />); + expect($('.b-input-placeholder').textContent).toBe('foo'); }); it('causes the placeholder to be empty when given `inputProps.defaultValue`', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - placeholder: 'foo', - defaultValue: 'foo', - }} /> - ); - - expect(element.refs.placeholder.textContent).toBe(''); + render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{defaultValue: 'foo', placeholder: 'foo'}} />); + expect($('.b-input-placeholder').textContent).toBe(''); }); it('causes the placeholder to be empty when the input is non-empty', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - placeholder: 'foo', - }} /> - ); + const element = render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{placeholder: 'foo'}} />); element.handleChange({target: {value: 'x'}}); - - expect(element.refs.placeholder.textContent).toBe(''); + expect($('.b-input-placeholder').textContent).toBe(''); }); it('properly manages placeholder visibility across many input changes', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - placeholder: 'foo', - }} /> - ); + const element = render(<Input {...props} hidePlaceholderOnFocus={false} inputProps={{placeholder: 'foo'}} />); - expect(element.refs.placeholder.textContent).toBe('foo'); + expect($('.b-input-placeholder').textContent).toBe('foo'); element.handleChange({target: {value: 'x'}}); - expect(element.refs.placeholder.textContent).toBe(''); + expect($('.b-input-placeholder').textContent).toBe(''); element.handleChange({target: {value: 'xy'}}); - expect(element.refs.placeholder.textContent).toBe(''); + expect($('.b-input-placeholder').textContent).toBe(''); element.handleChange({target: {value: ''}}); - expect(element.refs.placeholder.textContent).toBe('foo'); + expect($('.b-input-placeholder').textContent).toBe('foo'); }); }); describe('getValue()', () => { it('returns the current value of the input field', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - defaultValue: 'bar', - placeholder: 'foo', - }} /> - ); + const element = render(<Input {...props} inputProps={{defaultValue: 'bar'}} />); expect(element.getValue()).toBe('bar'); }); @@ -360,71 +187,38 @@ describe('Input component', () => { it('triggers the inputProps.onChange flow before the value is reset by React for a controlled component', () => { const changeStub = sandbox.stub(); - - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - onChange: changeStub, - value: 'ap', - }} /> - ); + const element = render(<Input {...props} inputProps={{onChange: changeStub, value: 'ap'}} />); element.setValue('foo'); expect(changeStub.calledOnce).toBe(true); }); it('empties the placeholder if set with a non-empty string', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - placeholder: 'bar', - }} /> - ); + const element = render(<Input {...props} inputProps={{placeholder: 'bar'}} />); expect(element.getValue()).toBe(''); - expect(element.refs.placeholder.textContent).toBe('bar'); + expect($('.b-input-placeholder').textContent).toBe('bar'); element.setValue('foo'); expect(element.getValue()).toBe('foo'); - expect(element.refs.placeholder.textContent).toBe(''); + expect($('.b-input-placeholder').textContent).toBe(''); }); it('restores the placeholder if set with an empty string', () => { - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - defaultValue: 'foo', - placeholder: 'bar', - }} /> - ); + const element = render(<Input {...props} inputProps={{defaultValue: 'foo', placeholder: 'bar'}} />); expect(element.getValue()).toBe('foo'); - expect(element.refs.placeholder.textContent).toBe(''); + expect($('.b-input-placeholder').textContent).toBe(''); element.setValue(''); expect(element.getValue()).toBe(''); - expect(element.refs.placeholder.textContent).toBe('bar'); + expect($('.b-input-placeholder').textContent).toBe('bar'); }); }); it('proxies input events to `props.inputProps.onBlur` if provided', () => { const stub = sandbox.stub(); - - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - onBlur: stub, - }} /> - ); - + const element = render(<Input {...props} inputProps={{onBlur: stub}} />); const event = {}; element.handleBlur(event); @@ -435,16 +229,7 @@ describe('Input component', () => { it('proxies input events to `props.inputProps.onFocus` if provided', () => { const stub = sandbox.stub(); - - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - onFocus: stub, - }} /> - ); - + const element = render(<Input {...props} inputProps={{onFocus: stub}} />); const event = {}; element.handleFocus(event); @@ -455,16 +240,7 @@ describe('Input component', () => { it('proxies input events to `props.inputProps.onChange` if provided', () => { const stub = sandbox.stub(); - - const element = render( - <Input - {...props} - inputProps={{ - ...props.inputProps, - onChange: stub, - }} /> - ); - + const element = render(<Input {...props} inputProps={{onChange: stub}} />); const event = {target: {value: 'x'}}; element.handleChange(event); diff --git a/packages/boundless-tokenized-input/README.md b/packages/boundless-tokenized-input/README.md index 5da1daa9..5779c492 100644 --- a/packages/boundless-tokenized-input/README.md +++ b/packages/boundless-tokenized-input/README.md @@ -358,6 +358,12 @@ There are no required props. - | - `bool` | `false` +- __`component`__ ・ overrides the HTML container tag + + Expects | Default Value + - | - + `string` | `'div'` + - __`entities`__ ・ an array of objects that user input is filtered against; at a minimum, each object must have a `text` property and any other supplied property is passed through to the resulting DOM element Expects | Default Value diff --git a/packages/boundless-typeahead/README.md b/packages/boundless-typeahead/README.md index 1a082694..24bb6b73 100644 --- a/packages/boundless-typeahead/README.md +++ b/packages/boundless-typeahead/README.md @@ -397,6 +397,12 @@ There are no required props. - | - `bool` | `false` +- __`component`__ ・ overrides the HTML container tag + + Expects | Default Value + - | - + `string` | `'div'` + - __`entities`__ ・ an array of objects that user input is filtered against; at a minimum, each object must have a `text` property and any other supplied property is passed through to the resulting DOM element Expects | Default Value