-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Title: Unselect title by blur event (B) #2974
Conversation
Codecov Report
@@ Coverage Diff @@
## master #2974 +/- ##
==========================================
+ Coverage 34.42% 35.35% +0.93%
==========================================
Files 196 197 +1
Lines 5796 6225 +429
Branches 1021 1122 +101
==========================================
+ Hits 1995 2201 +206
- Misses 3217 3389 +172
- Partials 584 635 +51
Continue to review full report at Codecov.
|
Summary of changes
I also spent a bit of time trying to make this a higher order component, or a separate component that just exposed the final blur and focus handlers, but because of the need to combine isSelected and hasFocusedWithin, I wasn't able to generalise it. |
editor/post-title/index.js
Outdated
this.setFocused( target.contains( document.activeElement ) ); | ||
}, 0 ); | ||
} | ||
|
||
handleClickOutside() { |
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.
We can probably delete this now, right?
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.
Yes, clickOutside
relies on this, so if we're removing it, it's likely become unused.
@@ -88,12 +121,15 @@ class PostTitle extends Component { | |||
|
|||
render() { | |||
const { title } = this.props; | |||
const { isSelected } = this.state; | |||
const className = classnames( 'editor-post-title', { 'is-selected': isSelected } ); |
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.
Maybe isSelected should be something like isTyping?
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.
The naming had confused me as well when I first encountered it. I think isTyping
would be an improvement, yes.
@@ -88,12 +121,15 @@ class PostTitle extends Component { | |||
|
|||
render() { | |||
const { title } = this.props; | |||
const { isSelected } = this.state; | |||
const className = classnames( 'editor-post-title', { 'is-selected': isSelected } ); |
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.
The naming had confused me as well when I first encountered it. I think isTyping
would be an improvement, yes.
editor/post-title/index.js
Outdated
className={ className } | ||
onBlur={ this.onOuterBlur } | ||
onFocus={ this.onOuterFocus }> | ||
{ isSelected && hasFocusWithin && <PostPermalink onLinkCopied={ this.focusText } /> } |
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.
You mentioned the clipboard button library being the cause that led to this implementation to focus text again after copying. Is that something that can be (or perhaps is already) fixed at the library? Or fixed on the clipboard button? Maybe using our "withFocusReturn" higher-order component could work as a solution?
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 think that might work, but now that I think about it, doesn't this only work on componentDidUnmount? The only react controlled component is the copy button (the textarea that gets focus is created by the clipboard library itself), and I thought the copy button wasn't being removed from the DOM after clicking on it?
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.
There is also an option in the clipboard library of passing through trigger, and then calling clearSelection (which will focus trigger and then removes the selection), but it would be just calling focus outside of the react in the exact same way. Is that actually preferable?
editor/post-title/index.js
Outdated
onOuterBlur( e ) { | ||
const target = e.currentTarget; | ||
clearTimeout( this.blurTimer ); | ||
this.blurTimer = setTimeout( () => { |
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
is a code smell in components and can often lead to difficult-to-maintain components. It's often a signal that we don't have a full understanding of the natural flow of events and deferring is a convenient waiting mechanism. I'm wondering if there is a better way to implement this.
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.
Yeah, I hate the setTimeout too, however I can't see a current way around it. Maybe you can. I'll explain what I think are the sequence of events
a) you focus inside the post title (fires focus event)
b) you click the copied button
- fires a blur first on the post title (which would cause unselect)
- then fires a focus on the copy button (based on the mousedown)
- then fires a click on the copy button (which is what clipboard library is relying on)
- this then creates a fake textarea and focuses it, does the copying magic and then removes it from the DOM
- then I return focus to the textarea, because now the focused element is removed from the DOM. I think you are right, though, that we could use the returnFocus HOC.
The big problem is that blur fires before the next DOM element is focused. This is an issue with most frameworks. Some browsers might supply a relatedTarget on the blur event, but not all of them ... which has typically led to setTimeout solutions abounding like https://medium.com/@jessebeach/dealing-with-focus-and-blur-in-a-composite-widget-in-react-90d3c3b49a9b and https://gist.github.com/pstoica/4323d3e6e37e8a23dd59 and https://stackoverflow.com/questions/11592966/get-the-newly-focussed-element-if-any-from-the-onblur-event/11592974#11592974.
However, there might be a way. In our previous dealings with these issues, we've often had to resort to a setTimeout. Hopefully, you're aware of a better option.
editor/post-title/index.js
Outdated
} | ||
|
||
componentDidMount() { | ||
document.addEventListener( 'selectionchange', this.onSelectionChange ); | ||
// eslint-disable-next-line react/no-find-dom-node |
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.
The lint rule is correct here: findDOMNode
should be discouraged, and this specific behavior can be recreated with a ref
. More ideally, we'd not rely on the DOM to determine how to assign focus state (the other way around being the preference, state -> DOM).
Also worth noting that this will always cause a re-render immediately after the first render, even if state is not changing (setState
will always cause render, even if the hasFocusWithin
is and stays false
).
When would we expect this element to be the active element after a mount?
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.
Yeah, I'm new to React so I wasn't sure if this would ever be true. I guess it would be true if the input that appeared inside it had autofocus, but maybe not exactly on mounting. I'm happy to set it to false on startup and have nothing here.
editor/post-title/index.js
Outdated
this.setFocused( target.contains( document.activeElement ) ); | ||
}, 0 ); | ||
} | ||
|
||
handleClickOutside() { |
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.
Yes, clickOutside
relies on this, so if we're removing it, it's likely become unused.
…ying from clipboard
I've had another go at this which is working reasonably well. Essentially it uses a similar approach to clickOutside but for focus. I ignore blur events, and just use focusOutside to simulate the same thing. Hoping to update the PR soon. |
@aduth what about this? I don't have particularly extensive tests, but I'm not really sure how to write them. Ideally, I'd want to create a hierarchy of components and some outside the component, and shift focus around and assert when the handleFocusOutside method fires. |
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.
Hmm, I'm wondering if the withFocusOutside
higher-order component is necessary, or if it might be sufficient to fix the clipboard button container and move the onBlur
introduced in #2948 from the Textarea
to the root wrapping div
(accounting for focus within both the textarea and post permalink).
class ClipboardButton extends Component { | ||
// This creates a container to put the textarea in which isn't removed by react | ||
// If react removes the textarea first, then the clipboard fails when trying to remove it | ||
class ClipboardContainer extends Component { |
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.
Does this need to be a separate component, or can we assign the button of ClipboardButton
as the container
?
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.
It needs to have shouldComponentUpdate return false so that the container with the clipboard textarea isn't removed from the DOM. The problem is if it gets redrawn, then the textarea that is inside gets removed, and the clipboard library throws an error when trying to remove it. We just need a component that can get things added to it, and isn't going to get recreated.
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 had it as the button originally, and kept getting an error in the clipboard library when it tried to remove the textarea, because react had already removed it because the button had been redrawn. You would get this error if you tried clicking on the button after you had already clicked on it. Therefore, the idea was to make a component that could have anything inside it, and react wouldn't keep trying to remove it. It's a limitation of using a third party library which appends components into the DOM. Does that make sense?
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.
If you don't supply container, then it defaults to body, which is fine normally, but it will fire a blur which will trigger focus outside. That's why I'm supplying container ... to keep the focus inside the post title block.
this.__wrappedInstance = ref; | ||
// eslint-disable-next-line react/no-find-dom-node | ||
this.__domNode = findDOMNode( ref ); | ||
if ( wrappedRefCallback ) { |
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.
What is the purpose of the wrappedRefCallback
? Is this just inherited from click outside? Probably best to avoid introducing features unless we foresee needing them.
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.
Yeah, based on the click outside. I can remove it.
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.
Removed this extra wrapping.
editor/post-title/index.js
Outdated
setFocused( focused ) { | ||
this.setState( { hasFocusWithin: focused } ); | ||
if ( focused ) { | ||
this.props.clearSelectedBlock(); |
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.
Why is this needed? Ideally the title focus handler shouldn't need to deal with block selection (i.e. it should be block focus leave events clearing its selected state).
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.
When I was testing it before, that wasn't working. But that was an earlier version. I'm happy to remove it and try and trust the blur of the other blocks.
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.
Removed this as it was not required. I think it was from an earlier version where I wasn't keeping isSelected separate.
editor/post-title/index.js
Outdated
} | ||
} | ||
|
||
handleFocusOutside( ) { |
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.
Styling note: There should be no spaces within the parentheses:
No filler spaces in empty constructs (e.g.,
{}
,[]
,fn()
).
https://make.wordpress.org/core/handbook/best-practices/coding-standards/javascript/#spacing
I'll plan to look and see if we can enforce this by ESLint.
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.
Noted.
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.
Fixed.
This is building on #2948
Description
Add some blur and focus handlers to the outer div of the title post so that we can detect focus has transferred outside of the container. Built on the previous PR by allowing the Copy Permalink button to be clicked.
How Has This Been Tested?
I have manually tested it but did not know how to write unit tests for it.
Types of changes
This change is a local change to the post-title component, as well as a small change to Permalink. It requires an additional prop for Permalinks, but they are only used in Titles.
Checklist: