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
+
+```
+
+You can use the `onAutoFill` callback to react to autofill state changes.
+This feature is currently only tested in Chrome >= v63.
+
### Select Props
| Property | Type | Default | Description |
@@ -345,6 +370,7 @@ function onInputKeyDown(event) {
| `aria`-labelledby | string | undefined | HTML ID of an element that should be used as the label (for assistive tech) |
| `arrowRenderer` | function | undefined | Renders a custom drop-down arrow to be shown in the right-hand side of the select: `arrowRenderer({ onMouseDown, isOpen })`. Won't render when set to `null`
| `autoBlur` | boolean | false | Blurs the input element after a selection has been made. Handy for lowering the keyboard on mobile devices |
+| `autoComplete` | string | undefined | Value to support autofill feature |
| `autofocus` | boolean | undefined | deprecated; use the autoFocus prop instead |
| `autoFocus` | boolean | undefined | autofocus the component on mount |
| `autoload` | boolean | true | whether to auto-load the default async options set |
@@ -381,6 +407,7 @@ function onInputKeyDown(event) {
| `multi` | boolean | undefined | multi-value input |
| `name` | string | undefined | field name, for hidden ` ` 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 (
+
+ );
+ }
+});
+
+
+module.exports = AutoCompleteField;
diff --git a/examples/src/data/countries.js b/examples/src/data/countries.js
new file mode 100644
index 0000000000..35621f0ca9
--- /dev/null
+++ b/examples/src/data/countries.js
@@ -0,0 +1,245 @@
+exports.COUNTRIES = [
+ { label: 'Afghanistan', value: 'AF' },
+ { label: 'Ă…land Islands', value: 'AX' },
+ { label: 'Albania', value: 'AL' },
+ { label: 'Algeria', value: 'DZ' },
+ { label: 'American Samoa', value: 'AS' },
+ { label: 'AndorrA', value: 'AD' },
+ { label: 'Angola', value: 'AO' },
+ { label: 'Anguilla', value: 'AI' },
+ { label: 'Antarctica', value: 'AQ' },
+ { label: 'Antigua and Barbuda', value: 'AG' },
+ { label: 'Argentina', value: 'AR' },
+ { label: 'Armenia', value: 'AM' },
+ { label: 'Aruba', value: 'AW' },
+ { label: 'Australia', value: 'AU' },
+ { label: 'Austria', value: 'AT' },
+ { label: 'Azerbaijan', value: 'AZ' },
+ { label: 'Bahamas', value: 'BS' },
+ { label: 'Bahrain', value: 'BH' },
+ { label: 'Bangladesh', value: 'BD' },
+ { label: 'Barbados', value: 'BB' },
+ { label: 'Belarus', value: 'BY' },
+ { label: 'Belgium', value: 'BE' },
+ { label: 'Belize', value: 'BZ' },
+ { label: 'Benin', value: 'BJ' },
+ { label: 'Bermuda', value: 'BM' },
+ { label: 'Bhutan', value: 'BT' },
+ { label: 'Bolivia', value: 'BO' },
+ { label: 'Bosnia and Herzegovina', value: 'BA' },
+ { label: 'Botswana', value: 'BW' },
+ { label: 'Bouvet Island', value: 'BV' },
+ { label: 'Brazil', value: 'BR' },
+ { label: 'British Indian Ocean Territory', value: 'IO' },
+ { label: 'Brunei Darussalam', value: 'BN' },
+ { label: 'Bulgaria', value: 'BG' },
+ { label: 'Burkina Faso', value: 'BF' },
+ { label: 'Burundi', value: 'BI' },
+ { label: 'Cambodia', value: 'KH' },
+ { label: 'Cameroon', value: 'CM' },
+ { label: 'Canada', value: 'CA' },
+ { label: 'Cape Verde', value: 'CV' },
+ { label: 'Cayman Islands', value: 'KY' },
+ { label: 'Central African Republic', value: 'CF' },
+ { label: 'Chad', value: 'TD' },
+ { label: 'Chile', value: 'CL' },
+ { label: 'China', value: 'CN' },
+ { label: 'Christmas Island', value: 'CX' },
+ { label: 'Cocos (Keeling) Islands', value: 'CC' },
+ { label: 'Colombia', value: 'CO' },
+ { label: 'Comoros', value: 'KM' },
+ { label: 'Congo', value: 'CG' },
+ { label: 'Congo, The Democratic Republic of the', value: 'CD' },
+ { label: 'Cook Islands', value: 'CK' },
+ { label: 'Costa Rica', value: 'CR' },
+ { label: 'Cote D\'Ivoire', value: 'CI' },
+ { label: 'Croatia', value: 'HR' },
+ { label: 'Cuba', value: 'CU' },
+ { label: 'Cyprus', value: 'CY' },
+ { label: 'Czech Republic', value: 'CZ' },
+ { label: 'Denmark', value: 'DK' },
+ { label: 'Djibouti', value: 'DJ' },
+ { label: 'Dominica', value: 'DM' },
+ { label: 'Dominican Republic', value: 'DO' },
+ { label: 'Ecuador', value: 'EC' },
+ { label: 'Egypt', value: 'EG' },
+ { label: 'El Salvador', value: 'SV' },
+ { label: 'Equatorial Guinea', value: 'GQ' },
+ { label: 'Eritrea', value: 'ER' },
+ { label: 'Estonia', value: 'EE' },
+ { label: 'Ethiopia', value: 'ET' },
+ { label: 'Falkland Islands (Malvinas)', value: 'FK' },
+ { label: 'Faroe Islands', value: 'FO' },
+ { label: 'Fiji', value: 'FJ' },
+ { label: 'Finland', value: 'FI' },
+ { label: 'France', value: 'FR' },
+ { label: 'French Guiana', value: 'GF' },
+ { label: 'French Polynesia', value: 'PF' },
+ { label: 'French Southern Territories', value: 'TF' },
+ { label: 'Gabon', value: 'GA' },
+ { label: 'Gambia', value: 'GM' },
+ { label: 'Georgia', value: 'GE' },
+ { label: 'Germany', value: 'DE' },
+ { label: 'Ghana', value: 'GH' },
+ { label: 'Gibraltar', value: 'GI' },
+ { label: 'Greece', value: 'GR' },
+ { label: 'Greenland', value: 'GL' },
+ { label: 'Grenada', value: 'GD' },
+ { label: 'Guadeloupe', value: 'GP' },
+ { label: 'Guam', value: 'GU' },
+ { label: 'Guatemala', value: 'GT' },
+ { label: 'Guernsey', value: 'GG' },
+ { label: 'Guinea', value: 'GN' },
+ { label: 'Guinea-Bissau', value: 'GW' },
+ { label: 'Guyana', value: 'GY' },
+ { label: 'Haiti', value: 'HT' },
+ { label: 'Heard Island and Mcdonald Islands', value: 'HM' },
+ { label: 'Holy See (Vatican City State)', value: 'VA' },
+ { label: 'Honduras', value: 'HN' },
+ { label: 'Hong Kong', value: 'HK' },
+ { label: 'Hungary', value: 'HU' },
+ { label: 'Iceland', value: 'IS' },
+ { label: 'India', value: 'IN' },
+ { label: 'Indonesia', value: 'ID' },
+ { label: 'Iran, Islamic Republic Of', value: 'IR' },
+ { label: 'Iraq', value: 'IQ' },
+ { label: 'Ireland', value: 'IE' },
+ { label: 'Isle of Man', value: 'IM' },
+ { label: 'Israel', value: 'IL' },
+ { label: 'Italy', value: 'IT' },
+ { label: 'Jamaica', value: 'JM' },
+ { label: 'Japan', value: 'JP' },
+ { label: 'Jersey', value: 'JE' },
+ { label: 'Jordan', value: 'JO' },
+ { label: 'Kazakhstan', value: 'KZ' },
+ { label: 'Kenya', value: 'KE' },
+ { label: 'Kiribati', value: 'KI' },
+ { label: 'Korea, Democratic People\'S Republic of', value: 'KP' },
+ { label: 'Korea, Republic of', value: 'KR' },
+ { label: 'Kuwait', value: 'KW' },
+ { label: 'Kyrgyzstan', value: 'KG' },
+ { label: 'Lao People\'S Democratic Republic', value: 'LA' },
+ { label: 'Latvia', value: 'LV' },
+ { label: 'Lebanon', value: 'LB' },
+ { label: 'Lesotho', value: 'LS' },
+ { label: 'Liberia', value: 'LR' },
+ { label: 'Libyan Arab Jamahiriya', value: 'LY' },
+ { label: 'Liechtenstein', value: 'LI' },
+ { label: 'Lithuania', value: 'LT' },
+ { label: 'Luxembourg', value: 'LU' },
+ { label: 'Macao', value: 'MO' },
+ { label: 'Macedonia, The Former Yugoslav Republic of', value: 'MK' },
+ { label: 'Madagascar', value: 'MG' },
+ { label: 'Malawi', value: 'MW' },
+ { label: 'Malaysia', value: 'MY' },
+ { label: 'Maldives', value: 'MV' },
+ { label: 'Mali', value: 'ML' },
+ { label: 'Malta', value: 'MT' },
+ { label: 'Marshall Islands', value: 'MH' },
+ { label: 'Martinique', value: 'MQ' },
+ { label: 'Mauritania', value: 'MR' },
+ { label: 'Mauritius', value: 'MU' },
+ { label: 'Mayotte', value: 'YT' },
+ { label: 'Mexico', value: 'MX' },
+ { label: 'Micronesia, Federated States of', value: 'FM' },
+ { label: 'Moldova, Republic of', value: 'MD' },
+ { label: 'Monaco', value: 'MC' },
+ { label: 'Mongolia', value: 'MN' },
+ { label: 'Montserrat', value: 'MS' },
+ { label: 'Morocco', value: 'MA' },
+ { label: 'Mozambique', value: 'MZ' },
+ { label: 'Myanmar', value: 'MM' },
+ { label: 'Namibia', value: 'NA' },
+ { label: 'Nauru', value: 'NR' },
+ { label: 'Nepal', value: 'NP' },
+ { label: 'Netherlands', value: 'NL' },
+ { label: 'Netherlands Antilles', value: 'AN' },
+ { label: 'New Caledonia', value: 'NC' },
+ { label: 'New Zealand', value: 'NZ' },
+ { label: 'Nicaragua', value: 'NI' },
+ { label: 'Niger', value: 'NE' },
+ { label: 'Nigeria', value: 'NG' },
+ { label: 'Niue', value: 'NU' },
+ { label: 'Norfolk Island', value: 'NF' },
+ { label: 'Northern Mariana Islands', value: 'MP' },
+ { label: 'Norway', value: 'NO' },
+ { label: 'Oman', value: 'OM' },
+ { label: 'Pakistan', value: 'PK' },
+ { label: 'Palau', value: 'PW' },
+ { label: 'Palestinian Territory, Occupied', value: 'PS' },
+ { label: 'Panama', value: 'PA' },
+ { label: 'Papua New Guinea', value: 'PG' },
+ { label: 'Paraguay', value: 'PY' },
+ { label: 'Peru', value: 'PE' },
+ { label: 'Philippines', value: 'PH' },
+ { label: 'Pitcairn', value: 'PN' },
+ { label: 'Poland', value: 'PL' },
+ { label: 'Portugal', value: 'PT' },
+ { label: 'Puerto Rico', value: 'PR' },
+ { label: 'Qatar', value: 'QA' },
+ { label: 'Reunion', value: 'RE' },
+ { label: 'Romania', value: 'RO' },
+ { label: 'Russian Federation', value: 'RU' },
+ { label: 'RWANDA', value: 'RW' },
+ { label: 'Saint Helena', value: 'SH' },
+ { label: 'Saint Kitts and Nevis', value: 'KN' },
+ { label: 'Saint Lucia', value: 'LC' },
+ { label: 'Saint Pierre and Miquelon', value: 'PM' },
+ { label: 'Saint Vincent and the Grenadines', value: 'VC' },
+ { label: 'Samoa', value: 'WS' },
+ { label: 'San Marino', value: 'SM' },
+ { label: 'Sao Tome and Principe', value: 'ST' },
+ { label: 'Saudi Arabia', value: 'SA' },
+ { label: 'Senegal', value: 'SN' },
+ { label: 'Serbia and Montenegro', value: 'CS' },
+ { label: 'Seychelles', value: 'SC' },
+ { label: 'Sierra Leone', value: 'SL' },
+ { label: 'Singapore', value: 'SG' },
+ { label: 'Slovakia', value: 'SK' },
+ { label: 'Slovenia', value: 'SI' },
+ { label: 'Solomon Islands', value: 'SB' },
+ { label: 'Somalia', value: 'SO' },
+ { label: 'South Africa', value: 'ZA' },
+ { label: 'South Georgia and the South Sandwich Islands', value: 'GS' },
+ { label: 'Spain', value: 'ES' },
+ { label: 'Sri Lanka', value: 'LK' },
+ { label: 'Sudan', value: 'SD' },
+ { label: 'Suriname', value: 'SR' },
+ { label: 'Svalbard and Jan Mayen', value: 'SJ' },
+ { label: 'Swaziland', value: 'SZ' },
+ { label: 'Sweden', value: 'SE' },
+ { label: 'Switzerland', value: 'CH' },
+ { label: 'Syrian Arab Republic', value: 'SY' },
+ { label: 'Taiwan, Province of China', value: 'TW' },
+ { label: 'Tajikistan', value: 'TJ' },
+ { label: 'Tanzania, United Republic of', value: 'TZ' },
+ { label: 'Thailand', value: 'TH' },
+ { label: 'Timor-Leste', value: 'TL' },
+ { label: 'Togo', value: 'TG' },
+ { label: 'Tokelau', value: 'TK' },
+ { label: 'Tonga', value: 'TO' },
+ { label: 'Trinidad and Tobago', value: 'TT' },
+ { label: 'Tunisia', value: 'TN' },
+ { label: 'Turkey', value: 'TR' },
+ { label: 'Turkmenistan', value: 'TM' },
+ { label: 'Turks and Caicos Islands', value: 'TC' },
+ { label: 'Tuvalu', value: 'TV' },
+ { label: 'Uganda', value: 'UG' },
+ { label: 'Ukraine', value: 'UA' },
+ { label: 'United Arab Emirates', value: 'AE' },
+ { label: 'United Kingdom', value: 'GB' },
+ { label: 'United States', value: 'US' },
+ { label: 'United States Minor Outlying Islands', value: 'UM' },
+ { label: 'Uruguay', value: 'UY' },
+ { label: 'Uzbekistan', value: 'UZ' },
+ { label: 'Vanuatu', value: 'VU' },
+ { label: 'Venezuela', value: 'VE' },
+ { label: 'Viet Nam', value: 'VN' },
+ { label: 'Virgin Islands, British', value: 'VG' },
+ { label: 'Virgin Islands, U.S.', value: 'VI' },
+ { label: 'Wallis and Futuna', value: 'WF' },
+ { label: 'Western Sahara', value: 'EH' },
+ { label: 'Yemen', value: 'YE' },
+ { label: 'Zambia', value: 'ZM' },
+ { label: 'Zimbabwe', value: 'ZW' }
+];
diff --git a/less/control.less b/less/control.less
index 672f063f0f..fbeb60b5b2 100644
--- a/less/control.less
+++ b/less/control.less
@@ -4,6 +4,11 @@
// Mixins
+// Animations
+
+@-webkit-keyframes onAutoFillStart { from {/**/} to {/**/}}
+@-webkit-keyframes onAutoFillCancel { from {/**/} to {/**/}}
+
// focused styles
.Select-focus-state(@color) {
border-color: @color;
@@ -22,6 +27,19 @@
.Select {
position: relative;
+ input:-webkit-autofill {
+ // Expose a hook for JavaScript when autofill is shown
+ // JavaScript can capture 'animationstart' events
+ // @see https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7
+ animation-name: onAutoFillStart;
+ }
+
+ input:not(:-webkit-autofill) {
+ // Expose a hook for JS onAutoFillCancel
+ // JavaScript can capture 'animationstart' events
+ animation-name: onAutoFillCancel;
+ }
+
// disable some browser-specific behaviours that break the input
input::-webkit-contacts-auto-fill-button,
input::-webkit-credentials-auto-fill-button {
@@ -128,6 +146,15 @@
direction: rtl;
text-align: right;
}
+
+ &.is-autofilled > .Select-control,
+ &.is-autofill > .Select-control,
+ &.is-autofill.is-focused .Select-input,
+ &.is-autofill.is-focused:not(.is-open) > .Select-control {
+ @media (-webkit-min-device-pixel-ratio:0) {
+ background-color: @chrome-autofill-color;
+ }
+ }
}
// base
@@ -170,6 +197,7 @@
position: absolute;
right: 0;
top: 0;
+ pointer-events: none;
// crop text
max-width: 100%;
@@ -307,6 +335,19 @@
float: left;
}
+// visually hidden fields
+
+.Select-hidden {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+}
+
// Animation
// ------------------------------
diff --git a/less/select.less b/less/select.less
index 7462e2f3b7..113d673791 100644
--- a/less/select.less
+++ b/less/select.less
@@ -87,6 +87,9 @@
@select-item-padding-horizontal: 5px;
@select-item-padding-vertical: 2px;
+// browser specific
+@chrome-autofill-color: #faffbd; // simulate Chrome's native autofill styling
+
// imports
@import "control.less";
@import "menu.less";
diff --git a/scss/control.scss b/scss/control.scss
index 3c54a1480f..0ea707c2c9 100644
--- a/scss/control.scss
+++ b/scss/control.scss
@@ -5,9 +5,26 @@
@import 'spinner';
@import 'mixins';
+// Animations
+@-webkit-keyframes onAutoFillStart { from {/**/} to {/**/}}
+@-webkit-keyframes onAutoFillCancel { from {/**/} to {/**/}}
+
.Select {
position: relative;
+ input:-webkit-autofill {
+ // Expose a hook for JavaScript when autofill is shown
+ // JavaScript can capture 'animationstart' events
+ // @see https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7
+ animation-name: onAutoFillStart;
+ }
+
+ input:not(:-webkit-autofill) {
+ // Expose a hook for JS onAutoFillCancel
+ // JavaScript can capture 'animationstart' events
+ animation-name: onAutoFillCancel;
+ }
+
// disable some browser-specific behaviours that break the input
input::-webkit-contacts-auto-fill-button,
input::-webkit-credentials-auto-fill-button {
@@ -115,6 +132,15 @@
direction: rtl;
text-align: right;
}
+
+ &.is-autofilled > .Select-control,
+ &.is-autofill > .Select-control,
+ &.is-autofill.is-focused .Select-input,
+ &.is-autofill.is-focused:not(.is-open) > .Select-control {
+ @media (-webkit-min-device-pixel-ratio:0) {
+ background-color: $chrome-autofill-color;
+ }
+ }
}
// base
@@ -157,6 +183,7 @@
position: absolute;
right: 0;
top: 0;
+ pointer-events: none;
// crop text
max-width: 100%;
@@ -285,8 +312,18 @@
position: relative;
}
+// visually hidden fields
-
+.Select-hidden {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+}
// Animation
// ------------------------------
diff --git a/scss/select.scss b/scss/select.scss
index c1b3fae420..b4c842e610 100644
--- a/scss/select.scss
+++ b/scss/select.scss
@@ -73,3 +73,6 @@ $select-item-hover-bg: darken($select-item-bg, 5%) !default;
$select-item-disabled-color: #333 !default;
$select-item-disabled-bg: #fcfcfc !default;
$select-item-disabled-border-color: darken($select-item-disabled-bg, 10%) !default;
+
+// browser specific
+$chrome-autofill-color: #faffbd; // simulate Chrome's native autofill styling
diff --git a/src/Select.js b/src/Select.js
index 75d25a7a66..138f65ee84 100644
--- a/src/Select.js
+++ b/src/Select.js
@@ -46,11 +46,11 @@ const shouldShowValue = (state, props) => {
return false;
};
-const shouldShowPlaceholder = (state, props, isOpen) => {
- const { inputValue, isPseudoFocused, isFocused } = state;
+const shouldShowPlaceholder = (state, props, isOpen, hasValue) => {
+ const { inputValue, isPseudoFocused, isFocused, isAutoFill } = state;
const { onSelectResetsInput } = props;
- return !inputValue || !onSelectResetsInput && !isOpen && !isPseudoFocused && !isFocused;
+ return !inputValue && (!isAutoFill && !hasValue) || !onSelectResetsInput && !isOpen && !isPseudoFocused && !isFocused;
};
/**
@@ -77,18 +77,23 @@ class Select extends React.Component {
constructor (props) {
super(props);
[
+ 'addAutoFillListener',
'clearValue',
'focusOption',
'getOptionLabel',
+ 'handleAutoFillAnimation',
+ 'handleInputClick',
'handleInputBlur',
'handleInputChange',
'handleInputFocus',
'handleInputValueChange',
+ 'handleHiddenInputChange',
'handleKeyDown',
'handleMenuScroll',
'handleMouseDown',
'handleMouseDownOnArrow',
'handleMouseDownOnMenu',
+ 'handleSelect',
'handleTouchEnd',
'handleTouchEndClearValue',
'handleTouchMove',
@@ -102,6 +107,8 @@ class Select extends React.Component {
this.state = {
inputValue: '',
+ isAutoFill: false,
+ isAutoFilled: false,
isFocused: false,
isOpen: false,
isPseudoFocused: false,
@@ -127,6 +134,12 @@ class Select extends React.Component {
if (this.props.autoFocus || this.props.autofocus) {
this.focus();
}
+ if (this.props.autoComplete && this.props.multi) {
+ console.warn('Warning: To enable autoComplete the prop "multi" must be set to "false".');
+ }
+ if (this.isAutoCompleteEnabled()) {
+ this.addAutoFillListener();
+ }
}
componentWillReceiveProps (nextProps) {
@@ -202,6 +215,27 @@ class Select extends React.Component {
this.toggleTouchOutsideEvent(false);
}
+ isAutoCompleteEnabled() {
+ return !this.props.multi && this.props.autoComplete && this.props.autoComplete !== 'off';
+ }
+
+ handleAutoFillAnimation (e) {
+ if (e.animationName === 'onAutoFillStart') {
+ this.setAutofill();
+ } else {
+ this.clearAutofill();
+ this.clearAutofilled();
+ }
+ this.props.onAutoFill && this.props.onAutoFill(e);
+ }
+
+ addAutoFillListener () {
+ // hack to receive -webkit-autofill event
+ // @see https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7
+ this.inputField && this.inputField.addEventListener('animationstart', this.handleAutoFillAnimation, false);
+ this.value && this.value.addEventListener('animationstart', this.handleAutoFillAnimation, false);
+ }
+
toggleTouchOutsideEvent (enabled) {
if (enabled) {
if (!document.addEventListener && document.attachEvent) {
@@ -425,14 +459,13 @@ class Select extends React.Component {
handleInputChange (event) {
let newInputValue = event.target.value;
-
if (this.state.inputValue !== event.target.value) {
newInputValue = this.handleInputValueChange(newInputValue);
}
this.setState({
inputValue: newInputValue,
- isOpen: true,
+ isOpen: !this.state.isAutoFilled,
isPseudoFocused: false,
});
}
@@ -447,6 +480,17 @@ class Select extends React.Component {
this.setState({
inputValue: newValue
});
+
+ if (this.state.isAutoFill) {
+ this.setAutofilled();
+ this.selectValue({ value: newInputValue });
+ } else {
+ this.clearAutofilled();
+ this.setState({
+ isPseudoFocused: false,
+ inputValue: newInputValue,
+ });
+ }
}
handleInputValueChange(newValue) {
@@ -460,6 +504,13 @@ class Select extends React.Component {
return newValue;
}
+ handleHiddenInputChange (e) {
+ this.selectValue({ value: e.target.value });
+ // basically a change event on the hidden field means the autoComplete function of the browser was used
+ this.setAutofilled();
+ }
+
+
handleKeyDown (event) {
if (this.props.disabled) return;
@@ -567,6 +618,11 @@ class Select extends React.Component {
}
}
+ handleSelect (value) {
+ this.clearAutofilled();
+ this.selectValue(value);
+ }
+
getOptionLabel (op) {
return op[this.props.labelKey];
}
@@ -668,6 +724,7 @@ class Select extends React.Component {
this.focus();
}
+
clearValue (event) {
// if the event was triggered by a mousedown and not the primary
// button, ignore it.
@@ -678,6 +735,7 @@ class Select extends React.Component {
event.preventDefault();
this.setValue(this.getResetValue());
+ this.clearAutofilled();
this.setState({
inputValue: this.handleInputValueChange(''),
isOpen: false,
@@ -686,6 +744,31 @@ class Select extends React.Component {
this._focusAfterClear = true;
}
+ setAutofill () {
+ this.setState({
+ isAutoFill: true,
+ isPseudoFocused: false,
+ });
+ }
+
+ clearAutofill () {
+ this.setState({
+ isAutoFill: false,
+ });
+ }
+
+ setAutofilled () {
+ this.setState({
+ isAutoFilled: true,
+ });
+ }
+
+ clearAutofilled () {
+ this.setState({
+ isAutoFilled: false,
+ });
+ }
+
getResetValue () {
if (this.props.resetValue !== undefined) {
return this.props.resetValue;
@@ -808,10 +891,11 @@ class Select extends React.Component {
}
renderValue (valueArray, isOpen) {
+ const hasValue = this.getValueArray(this.props.value).length;
let renderLabel = this.props.valueRenderer || this.getOptionLabel;
let ValueComponent = this.props.valueComponent;
if (!valueArray.length) {
- const showPlaceholder = shouldShowPlaceholder(this.state, this.props, isOpen);
+ const showPlaceholder = shouldShowPlaceholder(this.state, this.props, isOpen, hasValue);
return showPlaceholder ?
{this.props.placeholder}
: null;
}
let onClick = this.props.onValueClick ? this.handleValueClick : null;
@@ -844,15 +928,22 @@ class Select extends React.Component {
placeholder={this.props.placeholder}
value={valueArray[0]}
>
- {renderLabel(valueArray[0])}
+ { renderLabel(valueArray[0]) }
);
}
}
+ handleInputClick(e) {
+ this._openAfterFocus = true;
+ this.focus();
+ }
+
renderInput (valueArray, focusedOptionIndex) {
const className = classNames('Select-input', this.props.inputProps.className);
const isOpen = this.state.isOpen;
+ const hasValue = this.getValueArray(this.props.value).length;
+ const useAutoComplete = this.isAutoCompleteEnabled() && !hasValue;
const ariaOwns = classNames({
[this._instancePrefix + '-list']: isOpen,
@@ -870,6 +961,7 @@ class Select extends React.Component {
const inputProps = {
...this.props.inputProps,
+ autoComplete: useAutoComplete ? this.props.autoComplete : null,
'aria-activedescendant': isOpen ? this._instancePrefix + '-option-' + focusedOptionIndex : this._instancePrefix + '-value',
'aria-describedby': this.props['aria-describedby'],
'aria-expanded': '' + isOpen,
@@ -881,6 +973,7 @@ class Select extends React.Component {
onBlur: this.handleInputBlur,
onChange: this.handleInputChange,
onFocus: this.handleInputFocus,
+ onClick: this.handleInputClick,
ref: ref => this.input = ref,
role: 'combobox',
required: this.state.required,
@@ -921,12 +1014,12 @@ class Select extends React.Component {
if (this.props.autosize) {
return (
-
+
this.inputField = ref} id={useAutoComplete ? this.props.id : null} {...inputProps} minWidth="5" />
);
}
return (
-
+ 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 = [