-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(Dropdown): fix minCharacter
prop behaviour
#1722
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -334,19 +334,19 @@ export default class Dropdown extends Component { | |
static defaultProps = { | ||
additionLabel: 'Add ', | ||
additionPosition: 'top', | ||
closeOnBlur: true, | ||
icon: 'dropdown', | ||
minCharacters: 1, | ||
noResultsMessage: 'No results found.', | ||
openOnFocus: true, | ||
renderLabel: ({ text }) => text, | ||
selectOnBlur: true, | ||
openOnFocus: true, | ||
closeOnBlur: true, | ||
minCharacters: 1, | ||
} | ||
|
||
static autoControlledProps = [ | ||
'open', | ||
'value', | ||
'selectedLabel', | ||
'value', | ||
] | ||
|
||
static _meta = { | ||
|
@@ -420,9 +420,11 @@ export default class Dropdown extends Component { | |
if (!prevState.focus && this.state.focus) { | ||
debug('dropdown focused') | ||
if (!this.isMouseDown) { | ||
const { openOnFocus } = this.props | ||
const { minCharacters, openOnFocus, search } = this.props | ||
const openable = !search || (search && minCharacters === 1) | ||
|
||
debug('mouse is not down, opening') | ||
if (openOnFocus) this.open() | ||
if (openOnFocus && openable) this.open() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defines behaviour with |
||
} | ||
if (!this.state.open) { | ||
document.addEventListener('keydown', this.openOnArrow) | ||
|
@@ -492,10 +494,8 @@ export default class Dropdown extends Component { | |
// onChange needs to receive a value | ||
// can't rely on props.value if we are controlled | ||
handleChange = (e, value) => { | ||
debug('handleChange()') | ||
debug(value) | ||
const { onChange } = this.props | ||
if (onChange) onChange(e, { ...this.props, value }) | ||
debug('handleChange()', value) | ||
_.invoke(this.props, 'onChange', e, { ...this.props, value }) | ||
} | ||
|
||
closeOnChange = (e) => { | ||
|
@@ -580,11 +580,13 @@ export default class Dropdown extends Component { | |
} | ||
|
||
selectItemOnEnter = (e) => { | ||
debug('selectItemOnEnter()') | ||
debug(keyboardKey.getName(e)) | ||
debug('selectItemOnEnter()', keyboardKey.getName(e)) | ||
const { search } = this.props | ||
|
||
if (keyboardKey.getCode(e) !== keyboardKey.Enter) return | ||
e.preventDefault() | ||
|
||
if (search && _.isEmpty(this.getMenuOptions())) return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Disables |
||
this.makeSelectedItemActive(e) | ||
this.closeOnChange(e) | ||
} | ||
|
@@ -624,29 +626,34 @@ export default class Dropdown extends Component { | |
|
||
handleMouseDown = (e) => { | ||
debug('handleMouseDown()') | ||
const { onMouseDown } = this.props | ||
if (onMouseDown) onMouseDown(e, this.props) | ||
|
||
this.isMouseDown = true | ||
_.invoke(this.props, 'onMouseDown', e, this.props) | ||
// Do not access document when server side rendering | ||
if (!isBrowser) return | ||
document.addEventListener('mouseup', this.handleDocumentMouseUp) | ||
if (isBrowser) document.addEventListener('mouseup', this.handleDocumentMouseUp) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor refactor. |
||
} | ||
|
||
handleDocumentMouseUp = () => { | ||
debug('handleDocumentMouseUp()') | ||
|
||
this.isMouseDown = false | ||
// Do not access document when server side rendering | ||
if (!isBrowser) return | ||
document.removeEventListener('mouseup', this.handleDocumentMouseUp) | ||
if (isBrowser) document.removeEventListener('mouseup', this.handleDocumentMouseUp) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor refactor. |
||
} | ||
|
||
handleClick = (e) => { | ||
handleClick = e => { | ||
debug('handleClick()', e) | ||
const { onClick } = this.props | ||
if (onClick) onClick(e, this.props) | ||
|
||
const { minCharacters, search } = this.props | ||
const { open, searchQuery } = this.state | ||
|
||
_.invoke(this.props, 'onClick', e, this.props) | ||
// prevent closeOnDocumentClick() | ||
e.stopPropagation() | ||
this.toggle(e) | ||
|
||
if (!search) return this.toggle(e) | ||
if (open) return | ||
if (searchQuery.length >= minCharacters || minCharacters === 1) this.open(e) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update behaviour with click on search input. |
||
} | ||
|
||
handleItemClick = (e, item) => { | ||
|
@@ -683,14 +690,15 @@ export default class Dropdown extends Component { | |
|
||
handleFocus = (e) => { | ||
debug('handleFocus()') | ||
const { onFocus } = this.props | ||
const { focus } = this.state | ||
|
||
if (focus) return | ||
if (onFocus) onFocus(e, this.props) | ||
|
||
_.invoke(this.props, 'onFocus', e, this.props) | ||
this.setState({ focus: true }) | ||
} | ||
|
||
handleBlur = (e) => { | ||
handleBlur = e => { | ||
debug('handleBlur()') | ||
|
||
// Heads up! Don't remove this. | ||
|
@@ -709,26 +717,28 @@ export default class Dropdown extends Component { | |
this.setState({ focus: false, searchQuery: '' }) | ||
} | ||
|
||
handleSearchChange = (e) => { | ||
debug('handleSearchChange()') | ||
debug(e.target.value) | ||
handleSearchChange = e => { | ||
debug('handleSearchChange()', e) | ||
// prevent propagating to this.props.onChange() | ||
e.stopPropagation() | ||
const { search, onSearchChange, minCharacters } = this.props | ||
const { open } = this.state | ||
const newQuery = e.target.value | ||
|
||
if (onSearchChange) onSearchChange(e, newQuery) | ||
const { minCharacters } = this.props | ||
const { open } = this.state | ||
const newQuery = _.get(e, 'target.value', '') | ||
|
||
if (newQuery.length >= minCharacters) { | ||
// open search dropdown on search query | ||
if (search && newQuery && !open) this.open() | ||
_.invoke(this.props, 'onSearchChange', e, newQuery) | ||
this.setState({ | ||
selectedIndex: 0, | ||
searchQuery: newQuery, | ||
}) | ||
|
||
this.setState({ | ||
selectedIndex: 0, | ||
searchQuery: newQuery, | ||
}) | ||
// open search dropdown on search query | ||
if (!open && newQuery.length >= minCharacters) { | ||
this.open() | ||
return | ||
} | ||
// close search dropdown if search query is too small | ||
if (open && minCharacters !== 1 && newQuery.length < minCharacters) this.close() | ||
} | ||
|
||
// ---------------------------------------- | ||
|
@@ -1013,22 +1023,7 @@ export default class Dropdown extends Component { | |
this.setState({ focus: hasFocus }) | ||
} | ||
|
||
toggle = (e) => { | ||
if (!this.state.open) { | ||
this.open(e) | ||
return | ||
} | ||
|
||
const { search } = this.props | ||
const options = this.getMenuOptions() | ||
|
||
if (search && _.isEmpty(options)) { | ||
e.preventDefault() | ||
return | ||
} | ||
|
||
this.close(e) | ||
} | ||
toggle = e => this.state.open ? this.close(e) : this.open(e) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Method was updated because its logic was moved to |
||
|
||
// ---------------------------------------- | ||
// Render | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1420,6 +1420,76 @@ describe('Dropdown', () => { | |
}) | ||
}) | ||
|
||
describe('onClick', () => { | ||
it('is called with (event, props)', () => { | ||
const onClick = sandbox.spy() | ||
wrapperMount(<Dropdown onClick={onClick} options={options} />) | ||
wrapper.simulate('click', { stopPropagation: _.noop }) | ||
|
||
onClick.should.have.been.calledOnce() | ||
onClick.should.have.been.calledWithMatch({ }, { options }) | ||
}) | ||
|
||
it("toggles the dropdown when it's not searchable", () => { | ||
wrapperMount(<Dropdown options={options} />) | ||
|
||
wrapper.simulate('click') | ||
dropdownMenuIsOpen() | ||
|
||
wrapper.simulate('click') | ||
dropdownMenuIsClosed() | ||
}) | ||
|
||
it("opens the dropdown when it's searchable, but don't close", () => { | ||
wrapperMount(<Dropdown options={options} search />) | ||
|
||
wrapper.simulate('click') | ||
dropdownMenuIsOpen() | ||
|
||
wrapper.simulate('click') | ||
dropdownMenuIsOpen() | ||
}) | ||
|
||
it("don't open the dropdown when it's searchable and minCharacters is more that default value", () => { | ||
wrapperMount(<Dropdown minCharacters={3} options={options} search />) | ||
|
||
wrapper.simulate('click') | ||
dropdownMenuIsClosed() | ||
}) | ||
}) | ||
|
||
describe('onFocus', () => { | ||
it('is called with (event, props)', () => { | ||
const onFocus = sandbox.spy() | ||
wrapperMount(<Dropdown onFocus={onFocus} options={options} />) | ||
wrapper.simulate('focus') | ||
|
||
onFocus.should.have.been.calledOnce() | ||
onFocus.should.have.been.calledWithMatch({ }, { options }) | ||
}) | ||
|
||
it("opens the dropdown when it's not searchable", () => { | ||
wrapperMount(<Dropdown options={options} />) | ||
|
||
wrapper.simulate('focus') | ||
dropdownMenuIsOpen() | ||
}) | ||
|
||
it("opens the dropdown when it's searchable", () => { | ||
wrapperMount(<Dropdown options={options} search />) | ||
|
||
wrapper.simulate('focus') | ||
dropdownMenuIsOpen() | ||
}) | ||
|
||
it("don't open the dropdown when it's searchable and minCharacters is more that default value", () => { | ||
wrapperMount(<Dropdown minCharacters={3} options={options} search />) | ||
|
||
wrapper.simulate('focus') | ||
dropdownMenuIsClosed() | ||
}) | ||
}) | ||
|
||
describe('onSearchChange', () => { | ||
it('is called with (event, value) on search input change', () => { | ||
const spy = sandbox.spy() | ||
|
@@ -1430,6 +1500,30 @@ describe('Dropdown', () => { | |
spy.should.have.been.calledOnce() | ||
spy.should.have.been.calledWithMatch({ target: { value: 'a' } }, 'a') | ||
}) | ||
|
||
it("don't open the menu on change if query's length is less than minCharacters", () => { | ||
wrapperMount(<Dropdown minCharacters={3} options={options} selection search />) | ||
|
||
dropdownMenuIsClosed() | ||
|
||
// simulate search with query's length is less than minCharacters | ||
wrapper | ||
.find('input.search') | ||
.simulate('change', { target: { value: 'a' } }) | ||
|
||
dropdownMenuIsClosed() | ||
}) | ||
|
||
it("closes the opened menu on change if query's length is less than minCharacters", () => { | ||
wrapperMount(<Dropdown minCharacters={3} options={options} selection search />) | ||
const input = wrapper.find('input.search') | ||
|
||
input.simulate('change', { target: { value: 'abc' } }) | ||
dropdownMenuIsOpen() | ||
|
||
input.simulate('change', { target: { value: 'a' } }) | ||
dropdownMenuIsClosed() | ||
}) | ||
}) | ||
|
||
describe('options', () => { | ||
|
@@ -1618,19 +1712,6 @@ describe('Dropdown', () => { | |
dropdownMenuIsOpen() | ||
}) | ||
|
||
it("Don't open the menu on change if query's length is less than minCharacters", () => { | ||
wrapperMount(<Dropdown options={options} selection search minCharacters={4} />) | ||
|
||
dropdownMenuIsClosed() | ||
|
||
// simulate search with query's length is less than minCharacters | ||
wrapper | ||
.find('input.search') | ||
.simulate('change', { target: { value: faker.hacker.noun().substring(0, 1) } }) | ||
|
||
dropdownMenuIsClosed() | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test was moved to another section. |
||
|
||
it('does not call onChange on query change', () => { | ||
const onChangeSpy = sandbox.spy() | ||
wrapperMount(<Dropdown options={options} selection search onChange={onChangeSpy} />) | ||
|
@@ -1737,6 +1818,17 @@ describe('Dropdown', () => { | |
.at(1) | ||
.should.not.have.prop('selected', true) | ||
}) | ||
|
||
it('does not close the menu when options are empty', () => { | ||
wrapperMount(<Dropdown options={options} search selection />) | ||
wrapper.simulate('click') | ||
|
||
wrapper.find('input.search') | ||
.simulate('change', { target: { value: 'foo' } }) | ||
domEvent.keyDown(document, { key: 'Enter' }) | ||
|
||
dropdownMenuIsOpen() | ||
}) | ||
}) | ||
|
||
describe('no results message', () => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only prop sort there.