diff --git a/__snapshots__/src/index.test.js.md b/__snapshots__/src/index.test.js.md index 182f516c..1a45f104 100644 --- a/__snapshots__/src/index.test.js.md +++ b/__snapshots__/src/index.test.js.md @@ -11,6 +11,8 @@ Generated by [AVA](https://ava.li).
{ console.log('onNodeToggle::', curNode) } + onFocus = (node, action) => { + console.log('onFocus::', action, node) + } + onBlur = (node, action) => { + console.log('onBlur::', action, node) + } onOptionsChange = value => { this.setState({ [value]: !this.state[value] }) @@ -121,6 +127,8 @@ class WithOptions extends PureComponent { id="rdts" data={data} onChange={this.onChange} + onBlur={this.onBlur} + onFocus={this.onFocus} onAction={this.onAction} onNodeToggle={this.onNodeToggle} clearSearchOnChange={clearSearchOnChange} diff --git a/src/index.js b/src/index.js index 4081b403..e1cebad6 100644 --- a/src/index.js +++ b/src/index.js @@ -64,6 +64,7 @@ class DropdownTreeSelect extends Component { this.state = { searchModeOn: false, currentFocus: undefined, + isManagingFocus: false, } this.clientId = props.id || clientIdGenerator.get(this) } @@ -106,40 +107,59 @@ class DropdownTreeSelect extends Component { } componentWillUnmount() { - document.removeEventListener('click', this.handleOutsideClick, false) + document.removeEventListener('click', this.handleDropdownCollapse, false) } componentWillReceiveProps(nextProps) { this.initNewProps(nextProps) } + onBlur = () => { + // setTimeout runs afterwards in the event. + // If onFocus is triggered immediately in a child component, clearTimeout will stop setTimeout to run. + this._timeoutID = setTimeout(() => { + if (this.state.isManagingFocus) { + this.props.onBlur() + this.setState({ + isManagingFocus: false, + }) + } + }, 0) + } + + onFocus = () => { + clearTimeout(this._timeoutID) + if (!this.state.isManagingFocus) { + this.props.onFocus() + this.setState({ + isManagingFocus: true, + }) + } + } + handleClick = (e, callback) => { this.setState(prevState => { // keep dropdown active when typing in search box const showDropdown = this.props.showDropdown === 'always' || this.keepDropdownActive || !prevState.showDropdown - // register event listeners only if there is a state change - if (showDropdown !== prevState.showDropdown) { - if (showDropdown) { - document.addEventListener('click', this.handleOutsideClick, false) - } else { - document.removeEventListener('click', this.handleOutsideClick, false) - } + const searchStateReset = !showDropdown ? this.resetSearchState() : {} + // adds event listener for collapsing the dropdown + if (this.state.isManagingFocus && this.props.showDropdown !== 'always') { + document.addEventListener('click', this.handleDropdownCollapse, false) } - if (showDropdown) this.props.onFocus() - else this.props.onBlur() - - return !showDropdown ? { showDropdown, ...this.resetSearchState() } : { showDropdown } + return { + showDropdown, + searchStateReset, + } }, callback) } - handleOutsideClick = e => { - if (this.props.showDropdown === 'always' || !isOutsideClick(e, this.node)) { - return - } - - this.handleClick() + handleDropdownCollapse = e => { + if (!isOutsideClick(e, this.node)) return + document.removeEventListener('click', this.handleDropdownCollapse, false) + const showDropdown = this.props.showDropdown === 'always' + this.setState({ showDropdown: showDropdown }) } onInputChange = value => { @@ -157,12 +177,15 @@ class DropdownTreeSelect extends Component { }) } - onTagRemove = (id, isKeyboardEvent) => { + onTagRemove = id => { const { tags: prevTags } = this.state this.onCheckboxChange(id, false, tags => { - if (!isKeyboardEvent) return - - keyboardNavigation.getNextFocusAfterTagDelete(id, prevTags, tags, this.searchInput).focus() + const nextFocus = keyboardNavigation.getNextFocusAfterTagDelete(id, prevTags, tags, this.searchInput) + if (nextFocus) { + nextFocus.focus() + } else { + this.onBlur() + } }) } @@ -201,7 +224,7 @@ class DropdownTreeSelect extends Component { } if (isSingleSelect && !showDropdown) { - document.removeEventListener('click', this.handleOutsideClick, false) + document.removeEventListener('click', this.handleDropdownCollapse, false) } keyboardNavigation.adjustFocusedProps(currentFocusNode, node) @@ -311,6 +334,8 @@ class DropdownTreeSelect extends Component { return (
{ test('detects click outside', t => { const { tree } = t.context const wrapper = mount() - const handleOutsideClick = spy(wrapper.instance(), 'handleOutsideClick') + const handleDropdownCollapse = spy(wrapper.instance(), 'handleDropdownCollapse') + wrapper.instance().onFocus() wrapper.instance().handleClick() t.true(wrapper.state().showDropdown) const event = new MouseEvent('click', { bubbles: true, cancelable: true }) global.document.dispatchEvent(event) - t.true(handleOutsideClick.calledOnce) + t.true(handleDropdownCollapse.calledOnce) t.false(wrapper.state().showDropdown) }) @@ -270,7 +271,7 @@ test('detects click inside', t => { target: checkboxItem, }) Object.defineProperty(event, 'target', { value: checkboxItem, enumerable: true }) - wrapper.instance().handleOutsideClick(event) + wrapper.instance().handleDropdownCollapse(event) t.true(wrapper.state().showDropdown) }) @@ -291,7 +292,7 @@ test('detects click outside when other dropdown instance', t => { target: searchInput, }) Object.defineProperty(event, 'target', { value: searchInput, enumerable: true }) - wrapper1.instance().handleOutsideClick(event) + wrapper1.instance().handleDropdownCollapse(event) t.false(wrapper1.state().showDropdown) })