Skip to content

Conversation

@jeryj
Copy link
Contributor

@jeryj jeryj commented Aug 19, 2025

What?

Closes #71281, which is a blocking issue in #71163 (comment)

When a popover unmounts, it should call its related onClose, if provided.

Why?

It makes sense - the popover is closing, so the onClose should be called.

How?

Remove cancelling the onBlur check when unmounting the popover.

Testing Instructions

Testing Instructions for Keyboard

Screenshots or screencast

Before

Screen.Recording.2025-08-19.at.12.09.26.PM.mov

After

Screen.Recording.2025-08-19.at.11.51.55.AM.mov

@jeryj jeryj requested a review from ajitbohra as a code owner August 19, 2025 15:39
@github-actions
Copy link

github-actions bot commented Aug 19, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: jeryj <[email protected]>
Co-authored-by: getdave <[email protected]>
Co-authored-by: aduth <[email protected]>
Co-authored-by: Mamaduka <[email protected]>
Co-authored-by: ciampo <[email protected]>
Co-authored-by: stokesman <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

Flaky tests detected in 4ed0dee.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/17075051117
📝 Reported issues:

@jeryj jeryj changed the title Removing cancelling blur checks to see if tests pass Fix popover not calling onClose on unmount Aug 19, 2025
There was a cancelBlurCheck running on unmount, which prevented the onClose for the popover from running when the popover was unmounted.
Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the discussion in #71163, my hope is we could get rid of the timeout altogether, and instead check event.relatedTarget synchronously as part of the onBlur handler.

Roughly something like:

const onBlur = useCallback( ( event ) => {
  if ( ! event.target.contains( event.relatedTarget ) ) {
    onFocusOutside();
  }
} );

return { onBlur };

How feasible would that be?

@ciampo
Copy link
Contributor

ciampo commented Aug 19, 2025

What were the original reasons for the lines of code being deleted?

We need to make sure that this change, which targets a specific use case of the Popover component, won't cause regressions in other use cases (since the useFocusOutside is a much more general hook)

@jeryj
Copy link
Contributor Author

jeryj commented Aug 19, 2025

@ciampo - It fixes the bug we're experiencing in #71163 (comment) where the popover onClose isn't being called, even though the popover is being closed.

@aduth - I'm looking at the implmentation using just the onBlur: ! event.target.contains( event.relatedTarget ). I think I'll have to pass a ref into it though. I'm seeing event.target being logged as the element that is losing focus, and event.relatedTarget as the element receiving focus. The container seems hard to parse out since it's all using portals to populate the popover. Is passing a ref from useDialog a viable option here, or would that create too many back compat issues?

@aduth
Copy link
Member

aduth commented Aug 19, 2025

The container seems hard to parse out since it's all using portals to populate the popover. Is passing a ref from useDialog a viable option here, or would that create too many back compat issues?

Ah, right. Maybe we want currentTarget instead of target to make sure the element is the one to which the event is being assigned?

Passing a ref might be problematic if the component using the hook has its own ref that it expects to apply, so I'm not sure how far we'd be able to get with that.

@jeryj
Copy link
Contributor Author

jeryj commented Aug 19, 2025

@aduth I created an alternative approach using just the onBlur and passing a ref in from the dialog. I'm not sure what all it will break :) #71256

If the idea works, then maybe we can find an alternate reliable way to access the container.

@jeryj
Copy link
Contributor Author

jeryj commented Aug 25, 2025

@aduth I created an alternative approach using just the onBlur and passing a ref in from the dialog. I'm not sure what all it will break :) #71256

If the idea works, then maybe we can find an alternate reliable way to access the container.

I commented this on #71256, but it's appropriate to share here too:

I think running the callback only onBlur is the wrong approach. It should run after focus resolves (onFocus), because we'll get into weird states where the onClose callback sets states before the onMouseUp event finishes. This means you can't click a button to open the popover, then click the same button to close it (the popover will reopen). I think the trunk approach of queueing and canceling timeouts is a really clever and correct approach to this.

What do you think? Can we move forward with removing the unmount that cancels the blur check which prevents the focus outside onClose callback from running?

@aduth
Copy link
Member

aduth commented Aug 25, 2025

What do you think? Can we move forward with removing the unmount that cancels the blur check which prevents the focus outside onClose callback from running?

I was hopeful we could get rid of the timeouts because asynchronous always adds a lot more complexity and room for edge cases (like the one we're dealing with), but realistically I didn't have high confidence we'd be able to do it.

Regardless, I think the change you're presenting is logical; if a component is mounted, it should call onClose.

To @ciampo's point, I think it'd help give us confidence if we knew why these were here in the first place, and the potential scope of what might be impacted by the change so we can at least do a spot check on those behaviors not regressing. It could also be helpful to have some sort of test coverage for the type of behavior we're expecting to see (i.e. calls onClose on unmount).

@jeryj
Copy link
Contributor Author

jeryj commented Aug 25, 2025

@aduth Went digging. Here's what I found:

This concept was first added in 2017 in #3235. There is no specific mention or explanation of componentWillUnmount cancelling the blur check. It's mentioned that the HOC is built upon this closed PR's concept for fixing a blur event bug within the Clipboard Button component. The clipboard component does have an componentWillUnmount that destroys the clipboard contents (Note linked code is collapesed). It feels like a stretch, but maybe it was scaffolded off the clipboard component, and it didn't cause any obvious issue, so it was left in?

There is no mention of needing it to fix a specific bug. It has just been there since the beginning. Then, when it was changed to a hook, it was added because they were rewriting the HOC. The philosophy to not make changes unless necessary is clear , so the basis for the unmount goes back to the original PR. There is some related conversation about why the cancelling is happening, but it's more for when the callback becomes null rather than the unmounting.

So, I don't see any specific conversation around why specifically it was added or what it fixes. If I were arguing for its inclusion, I would say it's because we shouldn't leave a dangling timeout without being cleaned up. This seems potentially valid?

If so, then a possible fix here isn't to remove the unmount, but to cancel the timeout and then immediately execute the callback:

// Cancel blur checks on unmount.
useEffect( () => {
	return () => {
	    cancelBlurCheck();
	    currentOnFocusOutsideRef.current();
	 }   
}, [] );

The issues with doing that would be:

Long story short: Since the unmount cancelling the timeout is causing a bug for us, and I don't see a clear reason why it was added, I feel comfortable with removing the unmount hook if we don't see it causing any new problems.

@aduth
Copy link
Member

aduth commented Aug 27, 2025

@jeryj Thanks for digging into that, and apologies on behalf of my past self for not leaving more context on the blur cancellation behavior 😅 My sense was that it was a browser-specific quirk ("A blur event's relatedTarget is not reliable in IE11" in #3235), hence hammering so much on the potential removal now that we no longer support IE11.

Long story short: Since the unmount cancelling the timeout is causing a bug for us, and I don't see a clear reason why it was added, I feel comfortable with removing the unmount hook if we don't see it causing any new problems.

Considering everything you mentioned, I agree that it's best to be consistent in how we're triggering this callback asynchronously, like you have in the proposed implementation.

Have you been able to do some testing on potential impact to other components using this? I see it's not too many, with most of its usage going through the higher-order component (ComboBox uses withFocusOutside) or higher-level abstractions (useDialog, in turn used primarily in Popover, but also in EntitiesSavedStates).

@jeryj
Copy link
Contributor Author

jeryj commented Aug 27, 2025

Have you been able to do some testing on potential impact to other components using this?

Nothing extensive beyond seeing that all playwright tests pass and clicking around to see if there's any surprising behaviors. I'm hoping our e2e test suite is diverse enough that it would catch if there was a regression somewhere on a flow.

@getdave
Copy link
Contributor

getdave commented Sep 1, 2025

I preferred the idea of clearing the timeouts but then immediately triggering the callback.

We could do this "safely" by:

  • wrapping the execution of the callback in a try/catch
  • using queueMicrotask to run the callback after the component has unmounted
  • using a synthetic event as event

What do you think? Is this taking things too far?

@jeryj
Copy link
Contributor Author

jeryj commented Sep 2, 2025

What do you think? Is this taking things too far?

I think it may be riskier, as the normal flow and unmount flow would be separate. For example:

  • queueMicrotask might not run at the same time in the cycle as setTimeout( () => {}, 0 );
  • The original event and synthetic event might not be identical

So, I think the risk of creating a new system to handle unmount that is separate from the default flow is greater than having a potential uncleared timeout. That's my untested opinion. Very open to being wrong here :)

@stokesman
Copy link
Contributor

If I were arguing for its inclusion, I would say it's because we shouldn't leave a dangling timeout without being cleaned up. This seems potentially valid?

I doubt it's valid in this case. I don't see how this can be a dangling timeout because it's only ever queued if focus had been within the tree. Even when the component will unmount, the blur event happens and queues the timeout. The callback should be invoked.

It seems to me the only risk here is a breaking change. Perhaps if someone had created a workaround for this their callback would execute twice though of course that's not necessarily going to break anything.

I think this is a bug fix but I understand the need for caution.

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I'm generally in favor of this approach if it helps unblock #71163, even if there's additional opportunities to explore with synchronous callbacks, which could be done as iterative improvements. I also see some value in being consistent across the board in how the hook calls the callback (rather than a mix of some synchronous and some asynchronous callbacks).

While I think this looks good, a couple questions that come to mind in last-minute thoughts about the approach:

  • Are there potential issues that could occur if the popover is unmounted because the parent component is also unmounted? I'm imagining onClose being called on some parent component that's been unmounted and that parent component callback taking some action like setting state, and causing warnings like "Can't perform a React state update on an unmounted component."
  • If this is fixing something and we're relying on tests to give confidence in this not causing regressions, should there not also be some test code revisions here to validate the expected change in behavior?

@jeryj
Copy link
Contributor Author

jeryj commented Sep 2, 2025

Are there potential issues that could occur if the popover is unmounted because the parent component is also unmounted? I'm imagining onClose being called on some parent component that's been unmounted and that parent component callback taking some action like setting state, and causing warnings like "Can't perform a React state update on an unmounted component."

If the parent component using the useFocusCallback is unmounted, would this effect that cancels the blur check be triggered, which would cancel the callback? Something like:

  • Parent is unmounted
  • onFocusOutside (the callback) becomes null? (I'm not sure this is what happens)
  • The useEffect with the onFocusOutside dependency changes, cancelling the blur check.
  • setTimeout doesn't run.

If this is fixing something and we're relying on tests to give confidence in this not causing regressions, should there not also be some test code revisions here to validate the expected change in behavior?

Yup! That should be the case. I'll get a test that demonstrate the callback runs on unmount.

@aduth
Copy link
Member

aduth commented Sep 2, 2025

onFocusOutside (the callback) becomes null? (I'm not sure this is what happens)

I'm not sure either 😄 which is the concern. My hunch is that the function still exists, and that this is the exact sort of scenario why the warning message exists in React in the first place.

Copy link
Contributor

@getdave getdave left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to take a leap and approve this one.

Update: I added some unit tests coverage, although I'd appreciate a confidence check that it's covering what I think it's covering 😅

I have removed all custom implementation from #71163. I then applied this PR to that one. I then added a temporary setState call to the onClose callback to see if the unmount of the parent would cause React to fire its warning:

<LinkUI
			clientId={ insertedBlock?.clientId }
			link={ insertedBlock?.attributes }
			onBlockInsert={ handleSetInsertedBlock }
			onClose={ () => {
				// Use cleanup function
				cleanupInsertedBlock();
				setRandomState( 'crash all the things!' );
			} }
....

I did not see any warning. I don't think this is necessarily conclusive proof that we'll never encounter such a scenario but it's a good indicator.

There also seems to be a general consensus that the chances of bugs are relatively low and that we should ship and iterate on improve the approach if issues are uncovered.

If anyone disagrees please let us know, otherwise I'll let @jeryj press the merge button when he's online later today.

@getdave getdave added the [Type] Bug An existing feature does not function as intended label Sep 3, 2025
@getdave getdave self-requested a review September 3, 2025 09:34
@github-actions
Copy link

github-actions bot commented Sep 3, 2025

Size Change: -8 B (0%)

Total Size: 1.92 MB

Filename Size Change
build/compose/index.min.js 12.8 kB -8 B (-0.06%)
ℹ️ View Unchanged
Filename Size
build-module/a11y/index.min.js 482 B
build-module/block-library/accordion/view.min.js 427 B
build-module/block-library/file/view.min.js 466 B
build-module/block-library/form/view.min.js 533 B
build-module/block-library/image/view.min.js 1.78 kB
build-module/block-library/navigation/view.min.js 1.19 kB
build-module/block-library/query/view.min.js 767 B
build-module/block-library/search/view.min.js 639 B
build-module/interactivity-router/full-page.min.js 565 B
build-module/interactivity-router/index.min.js 11.4 kB
build-module/interactivity/debug.min.js 17.6 kB
build-module/interactivity/index.min.js 14 kB
build/a11y/index.min.js 925 B
build/annotations/index.min.js 2.13 kB
build/api-fetch/index.min.js 2.41 kB
build/autop/index.min.js 2.12 kB
build/blob/index.min.js 579 B
build/block-directory/index.min.js 7.18 kB
build/block-directory/style-rtl.css 1.03 kB
build/block-directory/style.css 1.03 kB
build/block-editor/content-rtl.css 4.43 kB
build/block-editor/content.css 4.42 kB
build/block-editor/default-editor-styles-rtl.css 392 B
build/block-editor/default-editor-styles.css 392 B
build/block-editor/index.min.js 267 kB
build/block-editor/style-rtl.css 15.9 kB
build/block-editor/style.css 15.9 kB
build/block-library/blocks/accordion/style-rtl.css 569 B
build/block-library/blocks/accordion/style.css 568 B
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 61 B
build/block-library/blocks/archives/style-rtl.css 90 B
build/block-library/blocks/archives/style.css 90 B
build/block-library/blocks/audio/editor-rtl.css 149 B
build/block-library/blocks/audio/editor.css 151 B
build/block-library/blocks/audio/style-rtl.css 132 B
build/block-library/blocks/audio/style.css 132 B
build/block-library/blocks/audio/theme-rtl.css 134 B
build/block-library/blocks/audio/theme.css 134 B
build/block-library/blocks/avatar/editor-rtl.css 115 B
build/block-library/blocks/avatar/editor.css 115 B
build/block-library/blocks/avatar/style-rtl.css 104 B
build/block-library/blocks/avatar/style.css 104 B
build/block-library/blocks/button/editor-rtl.css 265 B
build/block-library/blocks/button/editor.css 265 B
build/block-library/blocks/button/style-rtl.css 554 B
build/block-library/blocks/button/style.css 554 B
build/block-library/blocks/buttons/editor-rtl.css 291 B
build/block-library/blocks/buttons/editor.css 291 B
build/block-library/blocks/buttons/style-rtl.css 349 B
build/block-library/blocks/buttons/style.css 349 B
build/block-library/blocks/calendar/style-rtl.css 239 B
build/block-library/blocks/calendar/style.css 239 B
build/block-library/blocks/categories/editor-rtl.css 132 B
build/block-library/blocks/categories/editor.css 131 B
build/block-library/blocks/categories/style-rtl.css 152 B
build/block-library/blocks/categories/style.css 152 B
build/block-library/blocks/code/editor-rtl.css 53 B
build/block-library/blocks/code/editor.css 53 B
build/block-library/blocks/code/style-rtl.css 139 B
build/block-library/blocks/code/style.css 139 B
build/block-library/blocks/code/theme-rtl.css 122 B
build/block-library/blocks/code/theme.css 122 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 420 B
build/block-library/blocks/columns/style.css 420 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 124 B
build/block-library/blocks/comment-author-avatar/editor.css 124 B
build/block-library/blocks/comment-author-name/style-rtl.css 72 B
build/block-library/blocks/comment-author-name/style.css 72 B
build/block-library/blocks/comment-content/style-rtl.css 120 B
build/block-library/blocks/comment-content/style.css 120 B
build/block-library/blocks/comment-date/style-rtl.css 65 B
build/block-library/blocks/comment-date/style.css 65 B
build/block-library/blocks/comment-edit-link/style-rtl.css 70 B
build/block-library/blocks/comment-edit-link/style.css 70 B
build/block-library/blocks/comment-reply-link/style-rtl.css 71 B
build/block-library/blocks/comment-reply-link/style.css 71 B
build/block-library/blocks/comment-template/style-rtl.css 191 B
build/block-library/blocks/comment-template/style.css 191 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 168 B
build/block-library/blocks/comments-pagination/editor.css 168 B
build/block-library/blocks/comments-pagination/style-rtl.css 201 B
build/block-library/blocks/comments-pagination/style.css 201 B
build/block-library/blocks/comments-title/editor-rtl.css 75 B
build/block-library/blocks/comments-title/editor.css 75 B
build/block-library/blocks/comments/editor-rtl.css 842 B
build/block-library/blocks/comments/editor.css 842 B
build/block-library/blocks/comments/style-rtl.css 637 B
build/block-library/blocks/comments/style.css 637 B
build/block-library/blocks/cover/editor-rtl.css 631 B
build/block-library/blocks/cover/editor.css 631 B
build/block-library/blocks/cover/style-rtl.css 1.7 kB
build/block-library/blocks/cover/style.css 1.69 kB
build/block-library/blocks/details/editor-rtl.css 65 B
build/block-library/blocks/details/editor.css 65 B
build/block-library/blocks/details/style-rtl.css 86 B
build/block-library/blocks/details/style.css 86 B
build/block-library/blocks/embed/editor-rtl.css 331 B
build/block-library/blocks/embed/editor.css 331 B
build/block-library/blocks/embed/style-rtl.css 419 B
build/block-library/blocks/embed/style.css 419 B
build/block-library/blocks/embed/theme-rtl.css 133 B
build/block-library/blocks/embed/theme.css 133 B
build/block-library/blocks/file/editor-rtl.css 326 B
build/block-library/blocks/file/editor.css 326 B
build/block-library/blocks/file/style-rtl.css 278 B
build/block-library/blocks/file/style.css 278 B
build/block-library/blocks/footnotes/style-rtl.css 198 B
build/block-library/blocks/footnotes/style.css 197 B
build/block-library/blocks/form-input/editor-rtl.css 229 B
build/block-library/blocks/form-input/editor.css 229 B
build/block-library/blocks/form-input/style-rtl.css 349 B
build/block-library/blocks/form-input/style.css 349 B
build/block-library/blocks/form-submission-notification/editor-rtl.css 344 B
build/block-library/blocks/form-submission-notification/editor.css 341 B
build/block-library/blocks/form-submit-button/style-rtl.css 69 B
build/block-library/blocks/form-submit-button/style.css 69 B
build/block-library/blocks/freeform/editor-rtl.css 2.59 kB
build/block-library/blocks/freeform/editor.css 2.59 kB
build/block-library/blocks/gallery/editor-rtl.css 615 B
build/block-library/blocks/gallery/editor.css 616 B
build/block-library/blocks/gallery/style-rtl.css 1.83 kB
build/block-library/blocks/gallery/style.css 1.83 kB
build/block-library/blocks/gallery/theme-rtl.css 108 B
build/block-library/blocks/gallery/theme.css 108 B
build/block-library/blocks/group/editor-rtl.css 334 B
build/block-library/blocks/group/editor.css 334 B
build/block-library/blocks/group/style-rtl.css 103 B
build/block-library/blocks/group/style.css 103 B
build/block-library/blocks/group/theme-rtl.css 79 B
build/block-library/blocks/group/theme.css 79 B
build/block-library/blocks/heading/style-rtl.css 188 B
build/block-library/blocks/heading/style.css 188 B
build/block-library/blocks/html/editor-rtl.css 353 B
build/block-library/blocks/html/editor.css 354 B
build/block-library/blocks/image/editor-rtl.css 763 B
build/block-library/blocks/image/editor.css 763 B
build/block-library/blocks/image/style-rtl.css 1.6 kB
build/block-library/blocks/image/style.css 1.59 kB
build/block-library/blocks/image/theme-rtl.css 137 B
build/block-library/blocks/image/theme.css 137 B
build/block-library/blocks/latest-comments/style-rtl.css 355 B
build/block-library/blocks/latest-comments/style.css 354 B
build/block-library/blocks/latest-posts/editor-rtl.css 139 B
build/block-library/blocks/latest-posts/editor.css 138 B
build/block-library/blocks/latest-posts/style-rtl.css 520 B
build/block-library/blocks/latest-posts/style.css 520 B
build/block-library/blocks/list/style-rtl.css 107 B
build/block-library/blocks/list/style.css 107 B
build/block-library/blocks/loginout/style-rtl.css 61 B
build/block-library/blocks/loginout/style.css 61 B
build/block-library/blocks/media-text/editor-rtl.css 321 B
build/block-library/blocks/media-text/editor.css 320 B
build/block-library/blocks/media-text/style-rtl.css 543 B
build/block-library/blocks/media-text/style.css 542 B
build/block-library/blocks/more/editor-rtl.css 393 B
build/block-library/blocks/more/editor.css 393 B
build/block-library/blocks/navigation-link/editor-rtl.css 625 B
build/block-library/blocks/navigation-link/editor.css 628 B
build/block-library/blocks/navigation-link/style-rtl.css 190 B
build/block-library/blocks/navigation-link/style.css 188 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 295 B
build/block-library/blocks/navigation-submenu/editor.css 294 B
build/block-library/blocks/navigation/editor-rtl.css 2.23 kB
build/block-library/blocks/navigation/editor.css 2.24 kB
build/block-library/blocks/navigation/style-rtl.css 2.27 kB
build/block-library/blocks/navigation/style.css 2.26 kB
build/block-library/blocks/nextpage/editor-rtl.css 392 B
build/block-library/blocks/nextpage/editor.css 392 B
build/block-library/blocks/page-list/editor-rtl.css 356 B
build/block-library/blocks/page-list/editor.css 356 B
build/block-library/blocks/page-list/style-rtl.css 192 B
build/block-library/blocks/page-list/style.css 192 B
build/block-library/blocks/paragraph/editor-rtl.css 251 B
build/block-library/blocks/paragraph/editor.css 251 B
build/block-library/blocks/paragraph/style-rtl.css 341 B
build/block-library/blocks/paragraph/style.css 340 B
build/block-library/blocks/post-author-biography/style-rtl.css 74 B
build/block-library/blocks/post-author-biography/style.css 74 B
build/block-library/blocks/post-author-name/style-rtl.css 69 B
build/block-library/blocks/post-author-name/style.css 69 B
build/block-library/blocks/post-author/style-rtl.css 188 B
build/block-library/blocks/post-author/style.css 189 B
build/block-library/blocks/post-comments-count/style-rtl.css 72 B
build/block-library/blocks/post-comments-count/style.css 72 B
build/block-library/blocks/post-comments-form/editor-rtl.css 96 B
build/block-library/blocks/post-comments-form/editor.css 96 B
build/block-library/blocks/post-comments-form/style-rtl.css 527 B
build/block-library/blocks/post-comments-form/style.css 528 B
build/block-library/blocks/post-comments-link/style-rtl.css 71 B
build/block-library/blocks/post-comments-link/style.css 71 B
build/block-library/blocks/post-content/style-rtl.css 61 B
build/block-library/blocks/post-content/style.css 61 B
build/block-library/blocks/post-date/style-rtl.css 62 B
build/block-library/blocks/post-date/style.css 62 B
build/block-library/blocks/post-excerpt/editor-rtl.css 71 B
build/block-library/blocks/post-excerpt/editor.css 71 B
build/block-library/blocks/post-excerpt/style-rtl.css 155 B
build/block-library/blocks/post-excerpt/style.css 155 B
build/block-library/blocks/post-featured-image/editor-rtl.css 715 B
build/block-library/blocks/post-featured-image/editor.css 712 B
build/block-library/blocks/post-featured-image/style-rtl.css 347 B
build/block-library/blocks/post-featured-image/style.css 347 B
build/block-library/blocks/post-navigation-link/style-rtl.css 215 B
build/block-library/blocks/post-navigation-link/style.css 214 B
build/block-library/blocks/post-template/style-rtl.css 414 B
build/block-library/blocks/post-template/style.css 414 B
build/block-library/blocks/post-terms/style-rtl.css 96 B
build/block-library/blocks/post-terms/style.css 96 B
build/block-library/blocks/post-time-to-read/style-rtl.css 70 B
build/block-library/blocks/post-time-to-read/style.css 70 B
build/block-library/blocks/post-title/style-rtl.css 162 B
build/block-library/blocks/post-title/style.css 162 B
build/block-library/blocks/preformatted/style-rtl.css 125 B
build/block-library/blocks/preformatted/style.css 125 B
build/block-library/blocks/pullquote/editor-rtl.css 133 B
build/block-library/blocks/pullquote/editor.css 133 B
build/block-library/blocks/pullquote/style-rtl.css 365 B
build/block-library/blocks/pullquote/style.css 365 B
build/block-library/blocks/pullquote/theme-rtl.css 176 B
build/block-library/blocks/pullquote/theme.css 176 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 121 B
build/block-library/blocks/query-pagination-numbers/editor.css 118 B
build/block-library/blocks/query-pagination/editor-rtl.css 154 B
build/block-library/blocks/query-pagination/editor.css 154 B
build/block-library/blocks/query-pagination/style-rtl.css 237 B
build/block-library/blocks/query-pagination/style.css 237 B
build/block-library/blocks/query-title/style-rtl.css 64 B
build/block-library/blocks/query-title/style.css 64 B
build/block-library/blocks/query-total/style-rtl.css 64 B
build/block-library/blocks/query-total/style.css 64 B
build/block-library/blocks/query/editor-rtl.css 432 B
build/block-library/blocks/query/editor.css 432 B
build/block-library/blocks/quote/style-rtl.css 238 B
build/block-library/blocks/quote/style.css 238 B
build/block-library/blocks/quote/theme-rtl.css 233 B
build/block-library/blocks/quote/theme.css 236 B
build/block-library/blocks/read-more/style-rtl.css 131 B
build/block-library/blocks/read-more/style.css 131 B
build/block-library/blocks/rss/editor-rtl.css 126 B
build/block-library/blocks/rss/editor.css 126 B
build/block-library/blocks/rss/style-rtl.css 284 B
build/block-library/blocks/rss/style.css 283 B
build/block-library/blocks/search/editor-rtl.css 199 B
build/block-library/blocks/search/editor.css 199 B
build/block-library/blocks/search/style-rtl.css 674 B
build/block-library/blocks/search/style.css 671 B
build/block-library/blocks/search/theme-rtl.css 113 B
build/block-library/blocks/search/theme.css 113 B
build/block-library/blocks/separator/editor-rtl.css 100 B
build/block-library/blocks/separator/editor.css 100 B
build/block-library/blocks/separator/style-rtl.css 248 B
build/block-library/blocks/separator/style.css 248 B
build/block-library/blocks/separator/theme-rtl.css 195 B
build/block-library/blocks/separator/theme.css 195 B
build/block-library/blocks/shortcode/editor-rtl.css 286 B
build/block-library/blocks/shortcode/editor.css 286 B
build/block-library/blocks/site-logo/editor-rtl.css 773 B
build/block-library/blocks/site-logo/editor.css 770 B
build/block-library/blocks/site-logo/style-rtl.css 218 B
build/block-library/blocks/site-logo/style.css 218 B
build/block-library/blocks/site-tagline/editor-rtl.css 87 B
build/block-library/blocks/site-tagline/editor.css 87 B
build/block-library/blocks/site-tagline/style-rtl.css 65 B
build/block-library/blocks/site-tagline/style.css 65 B
build/block-library/blocks/site-title/editor-rtl.css 85 B
build/block-library/blocks/site-title/editor.css 85 B
build/block-library/blocks/site-title/style-rtl.css 143 B
build/block-library/blocks/site-title/style.css 143 B
build/block-library/blocks/social-link/editor-rtl.css 314 B
build/block-library/blocks/social-link/editor.css 314 B
build/block-library/blocks/social-links/editor-rtl.css 339 B
build/block-library/blocks/social-links/editor.css 338 B
build/block-library/blocks/social-links/style-rtl.css 1.51 kB
build/block-library/blocks/social-links/style.css 1.51 kB
build/block-library/blocks/spacer/editor-rtl.css 346 B
build/block-library/blocks/spacer/editor.css 346 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table-of-contents/style-rtl.css 83 B
build/block-library/blocks/table-of-contents/style.css 83 B
build/block-library/blocks/table/editor-rtl.css 394 B
build/block-library/blocks/table/editor.css 394 B
build/block-library/blocks/table/style-rtl.css 640 B
build/block-library/blocks/table/style.css 639 B
build/block-library/blocks/table/theme-rtl.css 152 B
build/block-library/blocks/table/theme.css 152 B
build/block-library/blocks/tag-cloud/editor-rtl.css 92 B
build/block-library/blocks/tag-cloud/editor.css 92 B
build/block-library/blocks/tag-cloud/style-rtl.css 248 B
build/block-library/blocks/tag-cloud/style.css 248 B
build/block-library/blocks/template-part/editor-rtl.css 368 B
build/block-library/blocks/template-part/editor.css 368 B
build/block-library/blocks/template-part/theme-rtl.css 113 B
build/block-library/blocks/template-part/theme.css 113 B
build/block-library/blocks/term-description/style-rtl.css 126 B
build/block-library/blocks/term-description/style.css 126 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 165 B
build/block-library/blocks/text-columns/style.css 165 B
build/block-library/blocks/verse/style-rtl.css 98 B
build/block-library/blocks/verse/style.css 98 B
build/block-library/blocks/video/editor-rtl.css 413 B
build/block-library/blocks/video/editor.css 414 B
build/block-library/blocks/video/style-rtl.css 202 B
build/block-library/blocks/video/style.css 202 B
build/block-library/blocks/video/theme-rtl.css 134 B
build/block-library/blocks/video/theme.css 134 B
build/block-library/classic-rtl.css 179 B
build/block-library/classic.css 179 B
build/block-library/common-rtl.css 1.08 kB
build/block-library/common.css 1.08 kB
build/block-library/editor-elements-rtl.css 75 B
build/block-library/editor-elements.css 75 B
build/block-library/editor-rtl.css 11.5 kB
build/block-library/editor.css 11.5 kB
build/block-library/elements-rtl.css 54 B
build/block-library/elements.css 54 B
build/block-library/index.min.js 233 kB
build/block-library/reset-rtl.css 472 B
build/block-library/reset.css 472 B
build/block-library/style-rtl.css 15.4 kB
build/block-library/style.css 15.4 kB
build/block-library/theme-rtl.css 715 B
build/block-library/theme.css 719 B
build/block-serialization-default-parser/index.min.js 1.12 kB
build/block-serialization-spec-parser/index.min.js 2.87 kB
build/blocks/index.min.js 52.6 kB
build/commands/index.min.js 16.3 kB
build/commands/style-rtl.css 956 B
build/commands/style.css 953 B
build/components/index.min.js 252 kB
build/components/style-rtl.css 13.7 kB
build/components/style.css 13.7 kB
build/core-commands/index.min.js 3.69 kB
build/core-data/index.min.js 74.9 kB
build/customize-widgets/index.min.js 11 kB
build/customize-widgets/style-rtl.css 1.43 kB
build/customize-widgets/style.css 1.43 kB
build/data-controls/index.min.js 641 B
build/data/index.min.js 8.7 kB
build/date/index.min.js 18 kB
build/deprecated/index.min.js 458 B
build/dom-ready/index.min.js 325 B
build/dom/index.min.js 4.68 kB
build/edit-post/classic-rtl.css 577 B
build/edit-post/classic.css 578 B
build/edit-post/index.min.js 13.3 kB
build/edit-post/style-rtl.css 2.69 kB
build/edit-post/style.css 2.69 kB
build/edit-site/index.min.js 236 kB
build/edit-site/posts-rtl.css 8.96 kB
build/edit-site/posts.css 8.97 kB
build/edit-site/style-rtl.css 15.1 kB
build/edit-site/style.css 15.1 kB
build/edit-widgets/index.min.js 17.8 kB
build/edit-widgets/style-rtl.css 4.05 kB
build/edit-widgets/style.css 4.06 kB
build/editor/index.min.js 127 kB
build/editor/style-rtl.css 9.2 kB
build/editor/style.css 9.21 kB
build/element/index.min.js 4.84 kB
build/escape-html/index.min.js 537 B
build/format-library/index.min.js 8.23 kB
build/format-library/style-rtl.css 472 B
build/format-library/style.css 472 B
build/hooks/index.min.js 1.65 kB
build/html-entities/index.min.js 467 B
build/i18n/index.min.js 2.23 kB
build/is-shallow-equal/index.min.js 526 B
build/keyboard-shortcuts/index.min.js 1.32 kB
build/keycodes/index.min.js 1.46 kB
build/list-reusable-blocks/index.min.js 2.13 kB
build/list-reusable-blocks/style-rtl.css 847 B
build/list-reusable-blocks/style.css 848 B
build/media-utils/index.min.js 3.69 kB
build/notices/index.min.js 946 B
build/nux/index.min.js 1.62 kB
build/nux/style-rtl.css 767 B
build/nux/style.css 763 B
build/patterns/index.min.js 7.36 kB
build/patterns/style-rtl.css 687 B
build/patterns/style.css 685 B
build/plugins/index.min.js 1.87 kB
build/preferences-persistence/index.min.js 2.06 kB
build/preferences/index.min.js 2.9 kB
build/preferences/style-rtl.css 562 B
build/preferences/style.css 562 B
build/primitives/index.min.js 829 B
build/priority-queue/index.min.js 1.54 kB
build/private-apis/index.min.js 978 B
build/react-i18n/index.min.js 640 B
build/react-refresh-entry/index.min.js 9.47 kB
build/react-refresh-runtime/index.min.js 6.76 kB
build/redux-routine/index.min.js 2.7 kB
build/reusable-blocks/index.min.js 2.53 kB
build/reusable-blocks/style-rtl.css 255 B
build/reusable-blocks/style.css 255 B
build/rich-text/index.min.js 12.2 kB
build/router/index.min.js 5.47 kB
build/server-side-render/index.min.js 1.6 kB
build/shortcode/index.min.js 1.4 kB
build/style-engine/index.min.js 2.04 kB
build/token-list/index.min.js 581 B
build/url/index.min.js 3.97 kB
build/vendors/react-dom.min.js 41.7 kB
build/vendors/react-jsx-runtime.min.js 556 B
build/vendors/react.min.js 4.02 kB
build/viewport/index.min.js 965 B
build/vips/index.min.js 36.2 kB
build/warning/index.min.js 250 B
build/widgets/index.min.js 7.16 kB
build/widgets/style-rtl.css 1.16 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.04 kB

compressed-size-action

@Mamaduka
Copy link
Member

Mamaduka commented Sep 3, 2025

Can't perform a React state update on an unmounted component.

React no longer triggers setState warnings, since version 18.

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't perform a React state update on an unmounted component.

React no longer triggers setState warnings, since version 18.

Thanks @Mamaduka , I overlooked that. Based on the context of that change and some more thought about my previous concerns, the sense I have is that: If a component using useFocusOutside unmounts because its parent unmounts, any callbacks on that parent will still be called, even though its unmounted. That could cause some unintentional side-effects depending on what those callbacks are doing, but in the case of React-related effects like setState, they'll be effectively treated as a no-op (and no longer log as of v18). It feels like a pretty low risk overall.

Comment on lines 143 to 146
// Wait for the queued timeout to execute
await new Promise( ( resolve ) => setTimeout( resolve, 10 ) );

expect( mockOnFocusOutside ).toHaveBeenCalled();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd discourage a real-time sleep like this, both because it's pretty fragile, and because it artificially prolongs the test runtime (although not by much in this case).

Could we do something where we set up the mock implementation to finish the test? Either using the done callback, or maybe something with Promise.withResolvers, e.g.

const { promise, resolve } = Promise.withResolvers();
mockOnFocusOutside.mockImplementation( resolve );
unmount();
await promise;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The withResolvers is such a nice addition to the promises ⭐

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL about withResolvers(). Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like Promise.withResolvers() is only available in Node.js 22+, but the project requires Node.js >=20.10.0. This was leading to unit test failures on CI.

I've reverted to using the traditional manual deferred promise pattern for better compatibility.

@Mamaduka
Copy link
Member

Mamaduka commented Sep 3, 2025

@aduth, yes, the local React shouldn't be a problem. I guess this would be a breaking change for users who have more complex onClose callbacks, though I hope that's rare.

P.S. I was debugging an odd inserter bug, which reminded me of this PR - #65598 (comment).

Replace fragile setTimeout approach with Promise.withResolvers() to make
the test more reliable and faster. The test now waits for the actual
callback to be called rather than an arbitrary timeout.

Addresses feedback from PR #71252 review.
…l deferred pattern

Promise.withResolvers() is only available in Node.js 22+, but the project
requires Node.js >=20.10.0. Use the traditional manual deferred promise
pattern for better compatibility.

This maintains the same improved test reliability while ensuring
compatibility with the project's Node.js version requirements.
@getdave getdave enabled auto-merge (squash) September 3, 2025 13:35
@getdave getdave merged commit a1a6a97 into trunk Sep 3, 2025
70 of 71 checks passed
@getdave getdave deleted the try/remove-cancel-blur-check branch September 3, 2025 14:10
@github-actions github-actions bot added this to the Gutenberg 21.7 milestone Sep 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Components /packages/components [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useFocusOutside doesn't run callback when unmounted

7 participants