-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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 block lock toolbar item stealing focus when mounted with StrictMode #57185
Conversation
Size Change: +753 B (0%) Total Size: 1.71 MB
ℹ️ View Unchanged
|
// We only want to allow this effeect to happen if the modal has been opened. | ||
// The issue is when we're returning focus from the block lock modal to a toolbar, | ||
// so it can only happen after a modal has been opened. Without this, the toolbar | ||
// will steal focus on rerenders. |
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.
Based on this comment, I'm wondering if this fix will cover all use-cases. What happens if the modal has been opened and closed, could this effect trigger at another moment after the modal was closed (not right after).
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 this isn't a great fix. Definitely a hotfix at best. It's better than the current check, since it at least only surfaces in a rarer instance than the current check.
Rather than try to guess if we need to force focus to the toolbar (and where to focus), I think a better solution would be:
If the Lock Button has rendered, force it to stay in the toolbar until it is unmounted as a "lock" action button.
This would mean there would be an "Unlocked" icon until the block toolbar is has been unmounted, but it would improve the UX since the modal could always send focus back to the button that triggered it (and fix this focus stealing issue). Clicking the "Unlocked" icon would trigger the lock modal again. Again, this would only show after unlocking the block. It would not show for all toolbars.
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.
Agreed — this sounds like a good idea that we should try out separately from this hot fix.
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.
Would love a look from Marco here, but this I'm fine shipping this hot fix. Thanks Jerry for looking into 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.
Thank you for the ping, and thank you for iterating on this fix!
I left a few suggestions, although I'm not sure how to test them in the editor and/or Storybook (I didn't know where to look for testing instructions)
@@ -33,10 +34,23 @@ export default function BlockLockToolbar( { clientId, wrapperRef } ) { | |||
useEffect( () => { | |||
if ( isFirstRender.current ) { | |||
isFirstRender.current = false; | |||
hasModalOpened.current = false; |
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.
Since hasModalOpened.current
is already initialized as false
, is this line necessary?
// We only want to allow this effeect to happen if the modal has been opened. | ||
// The issue is when we're returning focus from the block lock modal to a toolbar, | ||
// so it can only happen after a modal has been opened. Without this, the toolbar | ||
// will steal focus on rerenders. |
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.
Agreed — this sounds like a good idea that we should try out separately from this hot fix.
if ( isModalOpen && ! hasModalOpened.current ) { | ||
hasModalOpened.current = true; | ||
} |
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.
(Optional) Putting together a couple of suggestions left above, we could also split the logic into three smaller useEffect
s — one for the "first render" logic, one for the "has modal ever opened" logic, and one for focus restoration
diff --git a/packages/block-editor/src/components/block-lock/toolbar.js b/packages/block-editor/src/components/block-lock/toolbar.js
index 3385dd1b15..2c69fe3143 100644
--- a/packages/block-editor/src/components/block-lock/toolbar.js
+++ b/packages/block-editor/src/components/block-lock/toolbar.js
@@ -28,42 +28,47 @@ export default function BlockLockToolbar( { clientId, wrapperRef } ) {
const shouldHideBlockLockUI =
! canLock || ( canEdit && canMove && canRemove );
- // Restore focus manually on the first focusable element in the toolbar
- // when the block lock modal is closed and the block is not locked anymore.
- // See https://github.com/WordPress/gutenberg/issues/51447
useEffect( () => {
if ( isFirstRender.current ) {
isFirstRender.current = false;
- hasModalOpened.current = false;
- return;
}
+ }, [] );
+ useEffect( () => {
if ( isModalOpen && ! hasModalOpened.current ) {
hasModalOpened.current = true;
}
+ }, [ isModalOpen ] );
+ // Restore focus manually on the first focusable element in the toolbar
+ // when the block lock modal is closed and the block is not locked anymore.
+ // See https://github.com/WordPress/gutenberg/issues/51447
+ useEffect( () => {
// We only want to allow this effeect to happen if the modal has been opened.
// The issue is when we're returning focus from the block lock modal to a toolbar,
// so it can only happen after a modal has been opened. Without this, the toolbar
// will steal focus on rerenders.
if (
- hasModalOpened.current &&
- ! isModalOpen &&
- shouldHideBlockLockUI
+ isFirstRender.current ||
+ ! hasModalOpened.current ||
+ isModalOpen ||
+ ! shouldHideBlockLockUI
) {
- focus.focusable
- .find( wrapperRef.current, {
- sequential: false,
- } )
- .find(
- ( element ) =>
- element.tagName === 'BUTTON' &&
- element !== lockButtonRef.current
- )
- ?.focus();
+ return;
}
+
+ focus.focusable
+ .find( wrapperRef.current, {
+ sequential: false,
+ } )
+ .find(
+ ( element ) =>
+ element.tagName === 'BUTTON' &&
+ element !== lockButtonRef.current
+ )
+ ?.focus();
// wrapperRef is a reference object and should be stable
- }, [ isModalOpen, shouldHideBlockLockUI, hasModalOpened, wrapperRef ] );
+ }, [ isModalOpen, shouldHideBlockLockUI, wrapperRef ] );
if ( shouldHideBlockLockUI ) {
return null;
Potential fix to #57139. I'd like to use BlockToolbar and
<NavigableToolbar />
instead, but haven't looked into if this is possible. This is just a quick patch with the current implementation.What?
Fixes block toolbar stealing focus on mount when using
<StrictMode />
due touseEffect
running twice.Why?
Fixing a bug exposed during
<StrictMode />
testing.How?
Adds a condition to only send focus to the toolbar if the modal has been opened while the toolbar is mounted. This fixes the main issue and makes it unlikely to steal focus unexpectedly.
Testing Instructions
<PrivateBlockToolbar />
from<BlockToolbarPopover />
in a<StrictMode />
component (import { StrictMode } from 'react';
).Screenshots or screencast