Skip to content

Commit

Permalink
feat(breakingChanges): add a new prop to opt-into breaking changes
Browse files Browse the repository at this point in the history
In particular, add the `resetInputOnSelection` option. Closes #243
  • Loading branch information
yp authored and Kent C. Dodds committed Nov 12, 2017
1 parent 11a3525 commit 831aeab
Show file tree
Hide file tree
Showing 7 changed files with 515 additions and 1 deletion.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,27 @@ and `getLabelProps`. Also, you can use the `id` prop on the component

</details>

## Upcoming Breaking Changes

We try to avoid breaking changes when possible and try to adhere to
[semver][semver]. Sometimes breaking changes are necessary and we'll make
the transition as smooth as possible. This is why there's a prop available
which will allow you to opt into breaking changes. It looks like this:

```javascript
<Downshift breakingChanges={{ /* breaking change flags here */ }}>
/* your render function here */
</Downshift>
```

To opt-into a breaking change, simply provide the key and value in the
`breakingChanges` object prop for each breaking change mentioned below:

1. `resetInputOnSelection` - Enable with the value of `true`. For more information, see [#243](https://github.com/paypal/downshift/issues/243)

When a new major version is released, then the code to support the old
functionality will be removed and the breaking change version will be the
default, so it's suggested you enable these as soon as you are aware of them.

## Inspiration

Expand Down Expand Up @@ -721,3 +742,4 @@ MIT
[react-training]: https://reacttraining.com/
[advanced-react]: https://courses.reacttraining.com/courses/enrolled/200086
[fac]: https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9
[semver]: http://semver.org/
64 changes: 64 additions & 0 deletions src/__tests__/downshift.lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,70 @@ test('props update of selectedItem will update the inputValue state', () => {
)
})

test('item selection when selectedItem is controlled will update the inputValue state after selectedItem prop has been updated', () => {
const itemToString = jest.fn(x => x)
let renderArg
const childSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return <div />
})
const wrapper = mount(
<Downshift
selectedItem="foo"
itemToString={itemToString}
breakingChanges={{resetInputOnSelection: false}}
// Explicitly set to false even if this is the default behaviour to highlight that this test
// will fail on v2.
>
{childSpy}
</Downshift>,
)
childSpy.mockClear()
itemToString.mockClear()
const newSelectedItem = 'newfoo'
renderArg.selectItem(newSelectedItem)
expect(childSpy).toHaveBeenLastCalledWith(
expect.objectContaining({inputValue: newSelectedItem}),
)
wrapper.setProps({selectedItem: newSelectedItem})
expect(itemToString).toHaveBeenCalledTimes(2)
expect(itemToString).toHaveBeenCalledWith(newSelectedItem)
expect(childSpy).toHaveBeenLastCalledWith(
expect.objectContaining({inputValue: newSelectedItem}),
)
})

test('v2 BREAKING CHANGE item selection when selectedItem is controlled will update the inputValue state after selectedItem prop has been updated', () => {
const itemToString = jest.fn(x => x)
let renderArg
const childSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return <div />
})
const wrapper = mount(
<Downshift
selectedItem="foo"
itemToString={itemToString}
breakingChanges={{resetInputOnSelection: true}}
>
{childSpy}
</Downshift>,
)
childSpy.mockClear()
itemToString.mockClear()
const newSelectedItem = 'newfoo'
renderArg.selectItem(newSelectedItem)
expect(childSpy).not.toHaveBeenLastCalledWith(
expect.objectContaining({inputValue: newSelectedItem}),
)
wrapper.setProps({selectedItem: newSelectedItem})
expect(itemToString).toHaveBeenCalledTimes(1)
expect(itemToString).toHaveBeenCalledWith(newSelectedItem)
expect(childSpy).toHaveBeenLastCalledWith(
expect.objectContaining({inputValue: newSelectedItem}),
)
})

test('props update of selectedItem will not update inputValue state', () => {
const onInputValueChangeSpy = jest.fn(() => null)
const wrapper = mount(
Expand Down
36 changes: 36 additions & 0 deletions src/__tests__/downshift.props.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ test('onStateChange called with changes and downshift state and helpers', () =>
const {selectItem} = setup({
...controlledState,
onStateChange: handleStateChange,
breakingChanges: {
resetInputOnSelection: false,
// Explicitly set to false even if this is the default behaviour to highlight that this test
// will fail on v2.
},
})
const itemToSelect = 'foo'
selectItem(itemToSelect)
Expand All @@ -34,6 +39,37 @@ test('onStateChange called with changes and downshift state and helpers', () =>
)
})

test('v2 BREAKING CHANGE onStateChange called with changes and downshift state and helpers', () => {
const handleStateChange = jest.fn()
const controlledState = {
inputValue: '',
selectedItem: null,
}
const {selectItem} = setup({
...controlledState,
onStateChange: handleStateChange,
breakingChanges: {
resetInputOnSelection: true,
},
})
const itemToSelect = 'foo'
selectItem(itemToSelect)
const changes = {
type: Downshift.stateChangeTypes.unknown,
selectedItem: itemToSelect,
}
const stateAndHelpers = {
...controlledState,
isOpen: false,
highlightedIndex: null,
selectItem,
}
expect(handleStateChange).toHaveBeenLastCalledWith(
changes,
expect.objectContaining(stateAndHelpers),
)
})

test('onChange called when clearSelection is triggered', () => {
const handleChange = jest.fn()
const {clearSelection} = setup({
Expand Down
10 changes: 9 additions & 1 deletion src/downshift.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ class Downshift extends Component {
isOpen: PropTypes.bool,
inputValue: PropTypes.string,
highlightedIndex: PropTypes.number,
breakingChanges: PropTypes.shape({
resetInputOnSelection: PropTypes.bool,
}),
/* eslint-enable */
}

Expand All @@ -75,6 +78,7 @@ class Downshift extends Component {
typeof window === 'undefined' /* istanbul ignore next (ssr) */
? {}
: window,
breakingChanges: {},
}

// this is an experimental feature
Expand Down Expand Up @@ -240,7 +244,11 @@ class Downshift extends Component {
isOpen: false,
highlightedIndex: this.props.defaultHighlightedIndex,
selectedItem: item,
inputValue: this.props.itemToString(item),
inputValue:
this.isControlledProp('selectedItem') &&
this.props.breakingChanges.resetInputOnSelection
? this.props.defaultInputValue
: this.props.itemToString(item),
...otherStateToSet,
},
cbToCb(cb),
Expand Down
4 changes: 4 additions & 0 deletions stories/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import Basic from './examples/basic'
import Dropdown from './examples/dropdown'
import Form from './examples/form'
import Controlled from './examples/controlled'
import SemiControlled from './examples/semi-controlled'
import SemiControlledList from './examples/semi-controlled-list'
import Multiple from './examples/multiple'
import Autosuggest from './examples/react-autosuggest'
import SemanticUI from './examples/semantic-ui'
Expand All @@ -26,6 +28,8 @@ function loadStories() {
.add('dropdown', () => <Dropdown />)
.add('form', () => <Form />)
.add('controlled', () => <Controlled />)
.add('semi-controlled', () => <SemiControlled />)
.add('semi-controlled-list', () => <SemiControlledList />)
.add('multiple', () => <Multiple />)
.add('autosuggest', () => <Autosuggest />)
.add('semantic-ui', () => <SemanticUI />)
Expand Down
Loading

0 comments on commit 831aeab

Please sign in to comment.