From 40db51720dcf9973c3d8f2bbfbe71cb11cfb00ca Mon Sep 17 00:00:00 2001 From: cbergmiller Date: Sat, 3 Jun 2017 15:53:45 +0200 Subject: [PATCH 01/19] [FIX] #1651 moved option prop sync to componentWillReceiveProps (#1765) * [FIX] #1651 moved option prop sync to componentWillReceiveProps * [ADD] test for option prop sync by componentWillReceiveProps --- src/Async.js | 15 ++++++--------- test/Async-test.js | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Async.js b/src/Async.js index de2c8ad9..f4dafd19 100644 --- a/src/Async.js +++ b/src/Async.js @@ -68,15 +68,12 @@ export default class Async extends Component { } } - componentWillUpdate (nextProps, nextState) { - const propertiesToSync = ['options']; - propertiesToSync.forEach((prop) => { - if (this.props[prop] !== nextProps[prop]) { - this.setState({ - [prop]: nextProps[prop] - }); - } - }); + componentWillReceiveProps(nextProps) { + if (nextProps.options !== this.props.options) { + this.setState({ + options: nextProps.options, + }); + } } clearOptions() { diff --git a/test/Async-test.js b/test/Async-test.js index 8742e315..dd5a38fa 100644 --- a/test/Async-test.js +++ b/test/Async-test.js @@ -447,4 +447,20 @@ describe('Async', () => { expect(input, 'to equal', document.activeElement); }); }); + + + describe('props sync test', () => { + it('should update options on componentWillReceiveProps', () => { + createControl({ + }); + asyncInstance.componentWillReceiveProps({ + options: [{ + label: 'bar', + value: 'foo', + }] + }); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 1); + expect(asyncNode.querySelector('[role=option]').textContent, 'to equal', 'bar'); + }); + }); }); From 07dc061f7fb2c6eb8db120f5315f57352cb867f2 Mon Sep 17 00:00:00 2001 From: cbergmiller Date: Tue, 6 Jun 2017 07:04:20 +0200 Subject: [PATCH 02/19] [ADD] Clarified the onInputChange prop signature in the docs (#1773) * [ADD] Clarified the onInputChange prop signature in the docs * [ADD] requested changes --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ab43b05..f6b3f9e6 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,8 @@ The custom `menuRenderer` property accepts the following named parameters: ### Updating input values with onInputChange -You can manipulate the input using the onInputChange and returning a new value. +You can manipulate the input by providing a `onInputChange` callback that returns a new value. +**Please note:** When you want to use `onInputChange` only to listen to the input updates, you still have to return the unchanged value! ```js function cleanInput(inputValue) { @@ -385,7 +386,7 @@ function onInputKeyDown(event) { | onClose | func | undefined | handler for when the menu closes: `function () {}` | | onCloseResetsInput | bool | true | whether to clear input when closing the menu through the arrow | | onFocus | func | undefined | onFocus handler: `function(event) {}` | -| onInputChange | func | undefined | onInputChange handler: `function(inputValue) {}` | +| onInputChange | func | undefined | onInputChange handler/interceptor: `function(inputValue: string): string` | | onInputKeyDown | func | undefined | input keyDown handler; call `event.preventDefault()` to override default `Select` behavior: `function(event) {}` | | onOpen | func | undefined | handler for when the menu opens: `function () {}` | | onValueClick | func | undefined | onClick handler for value labels: `function (value, event) {}` | From ec53f0b2aa1d8287a7050b8d3f13b9b8a203c93f Mon Sep 17 00:00:00 2001 From: Nikita Balabaev Date: Wed, 7 Jun 2017 16:37:46 +0700 Subject: [PATCH 03/19] Update Select.js Replace whitespaces with tabs and vice versa for consistency reasons --- src/Select.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Select.js b/src/Select.js index 63d08bc8..11c69af5 100644 --- a/src/Select.js +++ b/src/Select.js @@ -48,19 +48,19 @@ const Select = createClass({ propTypes: { addLabelText: PropTypes.string, // placeholder displayed when you want to add a label on a multi-value input - 'aria-describedby': PropTypes.string, // HTML ID(s) of element(s) that should be used to describe this input (for assistive tech) + 'aria-describedby': PropTypes.string, // HTML ID(s) of element(s) that should be used to describe this input (for assistive tech) 'aria-label': PropTypes.string, // Aria label (for assistive tech) - 'aria-labelledby': PropTypes.string, // HTML ID of an element that should be used as the label (for assistive tech) - arrowRenderer: PropTypes.func, // Create drop-down caret element + 'aria-labelledby': PropTypes.string, // HTML ID of an element that should be used as the label (for assistive tech) + arrowRenderer: PropTypes.func, // Create drop-down caret element autoBlur: PropTypes.bool, // automatically blur the component when an option is selected autofocus: PropTypes.bool, // autofocus the component on mount autosize: PropTypes.bool, // whether to enable autosizing or not backspaceRemoves: PropTypes.bool, // whether backspace removes an item if there is no text input backspaceToRemoveMessage: PropTypes.string, // Message to use for screenreaders to press backspace to remove the current item - {label} is replaced with the item label className: PropTypes.string, // className for the outer element - clearAllText: stringOrNode, // title for the "clear" control when multi: true + clearAllText: stringOrNode, // title for the "clear" control when multi: true clearRenderer: PropTypes.func, // create clearable x element - clearValueText: stringOrNode, // title for the "clear" control + clearValueText: stringOrNode, // title for the "clear" control clearable: PropTypes.bool, // should it be possible to reset value deleteRemoves: PropTypes.bool, // whether backspace removes an item if there is no text input delimiter: PropTypes.string, // delimiter to use to join multiple values for the hidden field value @@ -84,12 +84,12 @@ const Select = createClass({ menuStyle: PropTypes.object, // optional style to apply to the menu 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 + noResultsText: stringOrNode, // placeholder displayed when there are no matching search results onBlur: PropTypes.func, // onBlur handler: function (event) {} onBlurResetsInput: PropTypes.bool, // whether input is cleared on blur onChange: PropTypes.func, // onChange handler: function (newValue) {} onClose: PropTypes.func, // fires when the menu is closed - onCloseResetsInput: PropTypes.bool, // whether input is cleared when menu is closed through the arrow + onCloseResetsInput: PropTypes.bool, // whether input is cleared when menu is closed through the arrow onFocus: PropTypes.func, // onFocus handler: function (event) {} onInputChange: PropTypes.func, // onInputChange handler: function (inputValue) {} onInputKeyDown: PropTypes.func, // input keyDown handler: function (event) {} @@ -103,7 +103,7 @@ const Select = createClass({ optionRenderer: PropTypes.func, // optionRenderer: function (option) {} options: PropTypes.array, // array of options pageSize: PropTypes.number, // number of entries to page when using page up/down keys - placeholder: stringOrNode, // field placeholder, displayed when there's no value + placeholder: stringOrNode, // field placeholder, displayed when there's no value required: PropTypes.bool, // applies HTML5 required attribute when needed resetValue: PropTypes.any, // value to use when you clear the control scrollMenuIntoView: PropTypes.bool, // boolean to enable the viewport to shift so that the full menu fully visible when engaged @@ -922,7 +922,7 @@ const Select = createClass({ renderArrow () { const onMouseDown = this.handleMouseDownOnArrow; - const isOpen = this.state.isOpen; + const isOpen = this.state.isOpen; const arrow = this.props.arrowRenderer({ onMouseDown, isOpen }); return ( From 40a9eb231b505b23dafbde43db11a66ce57fda5b Mon Sep 17 00:00:00 2001 From: Jeremy Liberman Date: Tue, 13 Jun 2017 20:55:49 -0500 Subject: [PATCH 04/19] Fix backspace handling for non-multi select controls. Fixes #638 (#773) --- src/Select.js | 2 +- test/Select-test.js | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Select.js b/src/Select.js index 63d08bc8..bcf0cebb 100644 --- a/src/Select.js +++ b/src/Select.js @@ -644,7 +644,7 @@ const Select = createClass({ var valueArray = this.getValueArray(this.props.value); if (!valueArray.length) return; if (valueArray[valueArray.length-1].clearableValue === false) return; - this.setValue(valueArray.slice(0, valueArray.length - 1)); + this.setValue(this.props.multi ? valueArray.slice(0, valueArray.length - 1) : null); }, removeValue (value) { diff --git a/test/Select-test.js b/test/Select-test.js index 8cc43a8c..26080bed 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -1850,6 +1850,19 @@ describe('Select', () => { ]); }); + it('removes the last item with backspace', () => { + + wrapper.setPropsForChild({ + multi: false, + value: 'one' + }); + onChange.reset(); // Ignore previous onChange calls + + pressBackspace(); + + expect(onChange, 'was called with', null); + }); + it('doesn\'t show the X if clearableValue=false', () => { setValueProp(['two']); From ce81389fe826929f9e29a013d49f0fea446ba1dc Mon Sep 17 00:00:00 2001 From: Jed Watson Date: Fri, 16 Jun 2017 14:21:36 +1000 Subject: [PATCH 05/19] Adding merged changes to changelog --- HISTORY.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 9bcf688f..77270ce5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # React-Select +## Next + +* fixed; issues synchronising options props in `Async`, thanks [cbergmiller](https://github.com/cbergmiller) +* fixed; backspace handling for non-multi select controls, thanks [Jeremy Liberman](https://github.com/MrLeebo) + ## v1.0.0-rc.5 / 2017-05-25 * fixed; Allow `falsey` values to be clearable, thanks [Simon Gaestel](https://github.com/sgaestel) From d8a34ec4e6e28eb6ce8a2eaac76a20e1945b35ae Mon Sep 17 00:00:00 2001 From: Lukas Stuart-Fry Date: Wed, 21 Jun 2017 10:05:35 -0700 Subject: [PATCH 06/19] Update README.md to include 'valueComponent' prop (#1803) * Update README.md to include 'valueComponent' prop Thanks to some very helpful feedback I received in this repo (and reading through some issues other users were having) I found there was a valueComponent prop that I could utilize that made my life so much easier. I think it needs to be included in the list of available props in the docs. * Update README.md to include 'valueComponent' prop (edited with suggested changes made) * Minor copy edits of valueComponent description --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f6b3f9e6..18b0731a 100644 --- a/README.md +++ b/README.md @@ -400,6 +400,7 @@ function onInputKeyDown(event) { | loadingPlaceholder | string\|node | 'Loading...' | label to prompt for loading search result | | tabSelectsValue | bool | true | whether to select the currently focused value when the `[tab]` key is pressed | | value | any | undefined | initial field value | +| valueComponent | func | | function which returns a custom way to render/manage the value selected `` | | valueKey | string | 'value' | the option property to use for the value | | valueRenderer | func | undefined | function which returns a custom way to render the value selected `function (option) {}` | From b06824784cd3b523bffb0fa621dc298ad1751a16 Mon Sep 17 00:00:00 2001 From: andreme Date: Fri, 23 Jun 2017 13:48:45 +1000 Subject: [PATCH 07/19] Hide create option after closing menu (#1306) * hide create option after closing menu * cleaned up tests --- src/Select.js | 32 +++++++++++++++++++------------- test/Creatable-test.js | 29 +++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/Select.js b/src/Select.js index ea78da39..54c6634a 100644 --- a/src/Select.js +++ b/src/Select.js @@ -396,13 +396,12 @@ const Select = createClass({ this.setState({ isOpen: false, isPseudoFocused: this.state.isFocused && !this.props.multi, - inputValue: '' + inputValue: this.handleInputValueChange('') }); } else { this.setState({ isOpen: false, - isPseudoFocused: this.state.isFocused && !this.props.multi, - inputValue: this.state.inputValue + isPseudoFocused: this.state.isFocused && !this.props.multi }); } this.hasScrolledToOption = false; @@ -437,7 +436,7 @@ const Select = createClass({ isPseudoFocused: false, }; if (this.props.onBlurResetsInput) { - onBlurredState.inputValue = ''; + onBlurredState.inputValue = this.handleInputValueChange(''); } this.setState(onBlurredState); }, @@ -445,12 +444,8 @@ const Select = createClass({ handleInputChange (event) { let newInputValue = event.target.value; - if (this.state.inputValue !== event.target.value && this.props.onInputChange) { - let nextState = this.props.onInputChange(newInputValue); - // Note: != used deliberately here to catch undefined and null - if (nextState != null && typeof nextState !== 'object') { - newInputValue = '' + nextState; - } + if (this.state.inputValue !== event.target.value) { + newInputValue = this.handleInputValueChange(newInputValue); } this.setState({ @@ -460,6 +455,17 @@ const Select = createClass({ }); }, + handleInputValueChange(newValue) { + if (this.props.onInputChange) { + let nextState = this.props.onInputChange(newValue); + // Note: != used deliberately here to catch undefined and null + if (nextState != null && typeof nextState !== 'object') { + newValue = '' + nextState; + } + } + return newValue; + }, + handleKeyDown (event) { if (this.props.disabled) return; @@ -610,7 +616,7 @@ const Select = createClass({ this.hasScrolledToOption = false; if (this.props.multi) { this.setState({ - inputValue: '', + inputValue: this.handleInputValueChange(''), focusedIndex: null }, () => { this.addValue(value); @@ -618,7 +624,7 @@ const Select = createClass({ } else { this.setState({ isOpen: false, - inputValue: '', + inputValue: this.handleInputValueChange(''), isPseudoFocused: this.state.isFocused, }, () => { this.setValue(value); @@ -664,7 +670,7 @@ const Select = createClass({ this.setValue(this.getResetValue()); this.setState({ isOpen: false, - inputValue: '', + inputValue: this.handleInputValueChange(''), }, this.focus); }, diff --git a/test/Creatable-test.js b/test/Creatable-test.js index 45014c10..08118d62 100644 --- a/test/Creatable-test.js +++ b/test/Creatable-test.js @@ -19,7 +19,7 @@ var TestUtils = require('react-addons-test-utils'); var Select = require('../src/Select'); describe('Creatable', () => { - let creatableInstance, creatableNode, filterInputNode, innserSelectInstance, renderer; + let creatableInstance, creatableNode, filterInputNode, innerSelectInstance, renderer; beforeEach(() => renderer = TestUtils.createRenderer()); @@ -36,7 +36,7 @@ describe('Creatable', () => { ); creatableNode = ReactDOM.findDOMNode(creatableInstance); - innserSelectInstance = creatableInstance.select; + innerSelectInstance = creatableInstance.select; findAndFocusInputControl(); }; @@ -106,7 +106,7 @@ describe('Creatable', () => { createControl({ filterOptions: () => null }); - typeSearchText('test');; + typeSearchText('test'); }); it('should not show a "create..." prompt if current filter text is not a valid option (as determined by :isValidNewOption prop)', () => { @@ -150,7 +150,6 @@ describe('Creatable', () => { const options = [{ label: 'One', value: 1 }]; createControl({ options, - shouldKeyDownEventCreateNewOption: ({ keyCode }) => keyCode === 13 }); typeSearchText('on'); // ['Create option "on"', 'One'] TestUtils.Simulate.keyDown(filterInputNode, { keyCode: 40, key: 'ArrowDown' }); // Select 'One' @@ -158,6 +157,28 @@ describe('Creatable', () => { expect(options, 'to have length', 1); }); + it('should remove the new option after closing on selecting option', () => { + createControl(); + typeSearchText('9'); + TestUtils.Simulate.keyDown(filterInputNode, { keyCode: 40, key: 'ArrowDown' }); + TestUtils.Simulate.keyDown(filterInputNode, { keyCode: 13 }); + expect(creatableInstance.inputValue, 'to equal', ''); + }); + + it('should remove the new option after closing on escape', () => { + createControl(); + typeSearchText('9'); + TestUtils.Simulate.keyDown(filterInputNode, { keyCode: 27 }); + expect(creatableInstance.inputValue, 'to equal', ''); + }); + + it('should remove the new option after closing on blur', () => { + createControl(); + typeSearchText('9'); + TestUtils.Simulate.blur(filterInputNode); + expect(creatableInstance.inputValue, 'to equal', ''); + }); + it('should allow a custom select type to be rendered via the :children property', () => { let childProps; createControl({ From 26169305a721ec3099a912cea2f6ed38e6dc9c4c Mon Sep 17 00:00:00 2001 From: Roman Masyhar Date: Mon, 26 Jun 2017 11:49:49 +0700 Subject: [PATCH 08/19] Fix Usage Docs Example (#1799) Before this change, console log in browser will throw > Selected: [object Object] After this change console log in browser will throw some nice information. > Selected: {"value":"three","label":"Three"} --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 18b0731a..e06550fe 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ var options = [ ]; function logChange(val) { - console.log("Selected: " + val); + console.log("Selected: " + JSON.stringify(val)); } tag with this field name for html forms - noResultsText: stringOrNode, // placeholder displayed when there are no matching search results - onBlur: PropTypes.func, // onBlur handler: function (event) {} - onBlurResetsInput: PropTypes.bool, // whether input is cleared on blur - onChange: PropTypes.func, // onChange handler: function (newValue) {} - onClose: PropTypes.func, // fires when the menu is closed - onCloseResetsInput: PropTypes.bool, // whether input is cleared when menu is closed through the arrow - onFocus: PropTypes.func, // onFocus handler: function (event) {} - onInputChange: PropTypes.func, // onInputChange handler: function (inputValue) {} - onInputKeyDown: PropTypes.func, // input keyDown handler: function (event) {} - onMenuScrollToBottom: PropTypes.func, // fires when the menu is scrolled to the bottom; can be used to paginate options - onOpen: PropTypes.func, // fires when the menu is opened - onValueClick: PropTypes.func, // onClick handler for value labels: function (value, event) {} - openAfterFocus: PropTypes.bool, // boolean to enable opening dropdown when focused - openOnFocus: PropTypes.bool, // always open options menu on focus - optionClassName: PropTypes.string, // additional class(es) to apply to the