-
Notifications
You must be signed in to change notification settings - Fork 267
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: Trigger onFocus/onBlur #425
base: main
Are you sure you want to change the base?
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 |
---|---|---|
|
@@ -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() | ||
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. This may not be an onBlur as the user would still be in the input, wouldn't they? 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. i can not see a case where the component is on focus. if the focused element is removed, and there is nothing else you can focus on, doesn't that mean the component loses focus? what am i missing? |
||
} | ||
}) | ||
} | ||
|
||
|
@@ -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 ( | ||
<div | ||
id={this.clientId} | ||
onBlur={this.onBlur} | ||
onFocus={this.onFocus} | ||
className={[this.props.className && this.props.className, 'react-dropdown-tree-select'] | ||
.filter(Boolean) | ||
.join(' ')} | ||
|
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.
setTimeout/clearTimeout seems like a fragile solution. Not just from a timing perspective but I think it makes it harder to move towards hooks. I was going to ask if you have you looked at using requestAnimationFrame in lieu of setTimeout but then I see that it's being used in conjunction with clearTimeout so RAF might not be an option.
I'm curious though - why take on the additional burden of keeping track of things instead of detecting outside clicks?
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.
I agree it looks a bit like smelly code, but it does not delay in terms of time but on one cycle on the event loop.
I choose this solution, first, because it builds over the existing onFocus/blur events from the dom and it doesn't need to treat corner cases. in my opinion this is easier to maintain then associating the blur focus with other events like opening and closing the dropdown. Secondly, the show stopper for me was the complexity of having the blur triggered when using keyboard navigation in a form. you could be listening on tab on the last element but you can not be sure which one is it,you have to check the props. it seemed to add to much complexity on the code.
i also tried attaching a handler like it is done for closing the dropdown when clicking outside but i ran into some corner cases, and i am sure i cannot think of all of them. also it might be harder to maintain.