diff --git a/README.md b/README.md index 70feb3bae8..8c8a664fae 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,31 @@ function onInputKeyDown(event) { /> ``` +### Supporting browser autofill + +Add the `autoComplete` prop to your select. Please notice that this feature is ignored for a Multiselect. The `autosize` prop should be set to `false` because autosize will shrink the input so the clickable area to trigger autofill suggestions does not expand over the whole select field. + +The value for `autoComplete` should be from [this list of possible values](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autocomplete). + +Usage example: +```JS +` tag | | `noResultsText` | string | 'No results found' | placeholder displayed when there are no matching search results or a falsy value to hide it (can also be a react component) | +| `onAutoFill` | function | undefined | onAutoFill handler: `function(event) {}` | | `onBlur` | function | undefined | onBlur handler: `function(event) {}` | | `onBlurResetsInput` | boolean | true | Whether to clear input on blur or not. If set to false, it only works if onCloseResetsInput is false as well. | | `onChange` | function | undefined | onChange handler: `function(newOption) {}` | diff --git a/examples/src/app.js b/examples/src/app.js index ba23d8954b..8d67c39d95 100644 --- a/examples/src/app.js +++ b/examples/src/app.js @@ -15,10 +15,12 @@ import NumericSelect from './components/NumericSelect'; import BooleanSelect from './components/BooleanSelect'; import Virtualized from './components/Virtualized'; import States from './components/States'; +import AutoComplete from './components/AutoComplete'; ReactDOM.render(
+ diff --git a/examples/src/components/AutoComplete.js b/examples/src/components/AutoComplete.js new file mode 100644 index 0000000000..186ead81c8 --- /dev/null +++ b/examples/src/components/AutoComplete.js @@ -0,0 +1,54 @@ +import React from 'react'; +import createClass from 'create-react-class'; +import PropTypes from 'prop-types'; +import Select from 'react-select'; +import { COUNTRIES } from '../data/countries'; + +var AutoCompleteField = createClass({ + displayName: 'AutoCompleteField', + propTypes: { + label: PropTypes.string, + autoComplete: PropTypes.string, + }, + componentDidMount() { + // reveal hidden input for testing + document.querySelector('[autocomplete="country"]').classList.remove('Select-hidden'); + }, + getInitialState () { + return { + selectValue: '', + }; + }, + updateValue (newValue) { + this.setState({ + selectValue: newValue, + }); + }, + handleAutoFill (e) { + console.log(e); + }, + render () { + return ( +
+

{this.props.label} (Source)

+ + + this.inputField = ref} id={useAutoComplete ? this.props.id : null} {...inputProps} />
); } @@ -1021,7 +1114,7 @@ class Select extends React.Component { labelKey: this.props.labelKey, onFocus: this.focusOption, onOptionRef: this.onOptionRef, - onSelect: this.selectValue, + onSelect: this.handleSelect, optionClassName: this.props.optionClassName, optionComponent: this.props.optionComponent, optionRenderer: this.props.optionRenderer || this.getOptionLabel, @@ -1043,28 +1136,30 @@ class Select extends React.Component { } renderHiddenField (valueArray) { + const hasValue = this.getValueArray(this.props.value).length; + const useAutoComplete = this.isAutoCompleteEnabled(); + if (!this.props.name) return; - if (this.props.joinValues) { + if (!this.props.multi || this.props.joinValues) { let value = valueArray.map(i => stringifyValue(i[this.props.valueKey])).join(this.props.delimiter); return ( this.value = ref} - type="hidden" - value={value} - /> + autoComplete={useAutoComplete ? this.props.autoComplete : 'off'} + name={this.props.name} + value={value} /> ); } return valueArray.map((item, index) => ( - + autoComplete={useAutoComplete ? this.props.autoComplete : 'off'} + name={this.props.name} + value={stringifyValue(item[this.props.valueKey])} /> )); } @@ -1133,6 +1228,8 @@ class Select extends React.Component { } let className = classNames('Select', this.props.className, { 'has-value': valueArray.length, + 'is-autofill': this.state.isAutoFill, + 'is-autofilled': this.state.isAutoFilled, 'is-clearable': this.props.clearable, 'is-disabled': this.props.disabled, 'is-focused': this.state.isFocused, @@ -1194,6 +1291,7 @@ Select.propTypes = { 'aria-labelledby': PropTypes.string, // html id of an element that should be used as the label (for assistive tech) arrowRenderer: PropTypes.func, // create the drop-down caret element autoBlur: PropTypes.bool, // automatically blur the component when an option is selected + autoComplete: PropTypes.string, // support for form auto completion autoFocus: PropTypes.bool, // autofocus the component on mount autofocus: PropTypes.bool, // deprecated; use autoFocus instead autosize: PropTypes.bool, // whether to enable autosizing or not @@ -1229,6 +1327,7 @@ Select.propTypes = { multi: PropTypes.bool, // multi-value input name: PropTypes.string, // generates a hidden tag with this field name for html forms noResultsText: stringOrNode, // placeholder displayed when there are no matching search results + onAutoFill: PropTypes.func, // fires when autofill state changed onBlur: PropTypes.func, // onBlur handler: function (event) {} onBlurResetsInput: PropTypes.bool, // whether input is cleared on blur onChange: PropTypes.func, // onChange handler: function (newValue) {} diff --git a/test/Select-test.js b/test/Select-test.js index 946b1f7858..1e26f25f47 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -26,7 +26,7 @@ var Select = require('../src').default; // The displayed text of the currently selected item, when items collapsed var DISPLAYED_SELECTION_SELECTOR = '.Select-value'; -var FORM_VALUE_SELECTOR = '.Select > input'; +var FORM_VALUE_SELECTOR = '.Select-hidden'; var PLACEHOLDER_SELECTOR = '.Select-placeholder'; var ARROW_UP = { keyCode: 38, key: 'ArrowUp' }; @@ -64,7 +64,11 @@ describe('Select', () => { return ReactDOM.findDOMNode(instance).querySelector('.Select-control'); }; - var enterSingleCharacter = () => { + var getHiddenInput = (instane) => { + return ReactDOM.findDOMNode(instance).querySelector(FORM_VALUE_SELECTOR); + }; + + var enterSingleCharacter = () =>{ TestUtils.Simulate.keyDown(searchInputNode, { keyCode: 65, key: 'a' }); }; @@ -120,6 +124,14 @@ describe('Select', () => { TestUtils.Simulate.change(searchInputNode, { target: { value: text } }); }; + var simulateAutoFill = (text) => { + const hiddenInput = getHiddenInput(instance); + if (!hiddenInput) { + throw new Error('Can\'t simulateAutoFill, hiddenInput doesn\'t exist'); + } + TestUtils.Simulate.change(hiddenInput, { target: { value: text } }); + }; + var clickArrowToOpen = () => { var selectArrow = ReactDOM.findDOMNode(instance).querySelector('.Select-arrow'); TestUtils.Simulate.mouseDown(selectArrow, { button: 0 }); @@ -634,6 +646,51 @@ describe('Select', () => { }); }); + describe('with autoComplete', () => { + beforeEach(() => { + options = [ + { value: 0, label: 'Zero' }, + { value: 1, label: 'One' }, + { value: 2, label: 'Two' }, + { value: 3, label: 'Three' } + ]; + + wrapper = createControlWithWrapper({ + value: null, + autoComplete: 'test', + multi: false, + name: 'field', + options: options, + simpleValue: true, + }); + }); + + it('calls onChange with the autofilled value', () => { + simulateAutoFill(3); + expect(onChange, 'was called with', 3); + }); + + it('changes the state on autofill', () => { + simulateAutoFill(3); + expect(instance.state.isAutoFilled, 'to be', true); + }); + + it('adds a class on autofill', () => { + simulateAutoFill(3); + expect(ReactDOM.findDOMNode(instance), + 'to have attributes', { + class: 'is-autofilled' + }); + }); + + it('adds the autoComplete attribute to the hidden input', () => { + expect(getHiddenInput(instance), + 'to have attributes', { + autocomplete: 'test' + }); + }); + }); + describe('with values as numbers', () => { beforeEach(() => { options = [