Skip to content
Merged
62 changes: 57 additions & 5 deletions packages/editor/src/components/collab-sidebar/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ function Thread( {
selectedThread,
commentLastUpdated,
} );
const isKeyboardTabbingRef = useRef( false );

const onMouseEnter = () => {
debouncedToggleBlockHighlight( thread.blockClientId, true );
Expand All @@ -479,13 +480,48 @@ function Thread( {
debouncedToggleBlockHighlight( thread.blockClientId, false );
};

const onFocus = () => {
toggleBlockHighlight( thread.blockClientId, true );
};

const onBlur = ( event ) => {
const isNoteFocused = event.relatedTarget?.closest(
'.editor-collab-sidebar-panel__thread'
);
const isDialogFocused =
event.relatedTarget?.closest( '[role="dialog"]' );
const isTabbing = isKeyboardTabbingRef.current;

// When another note is clicked, do nothing because the current note is automatically closed.
if ( isNoteFocused && ! isTabbing ) {
return;
}
// When deleting a note, a dialog appears, but the note should not be collapsed.
if ( isDialogFocused ) {
return;
}
// When tabbing, do nothing if the focus is within the current note.
if (
isTabbing &&
event.currentTarget.contains( event.relatedTarget )
) {
return;
}

// Closes a note that has lost focus when any of the following conditions are met:
// - An element other than a note is clicked.
// - Focus was lost by tabbing.
toggleBlockHighlight( thread.blockClientId, false );
unselectThread();
};

const handleCommentSelect = () => {
setNewNoteFormState( 'closed' );
setSelectedThread( thread.id );
toggleBlockSpotlight( thread.blockClientId, true );
if ( !! thread.blockClientId ) {
// Pass `null` as the second parameter to prevent focusing the block.
selectBlock( thread.blockClientId, null );
toggleBlockSpotlight( thread.blockClientId, true );
}
};

Expand Down Expand Up @@ -547,9 +583,20 @@ function Thread( {
onClick={ handleCommentSelect }
onMouseEnter={ onMouseEnter }
onMouseLeave={ onMouseLeave }
onFocus={ onMouseEnter }
onBlur={ onMouseLeave }
onKeyDown={ onKeyDown }
onFocus={ onFocus }
onBlur={ onBlur }
onKeyUp={ ( event ) => {
if ( event.key === 'Tab' ) {
isKeyboardTabbingRef.current = false;
}
} }
onKeyDown={ ( event ) => {
if ( event.key === 'Tab' ) {
isKeyboardTabbingRef.current = true;
} else {
onKeyDown( event );
}
} }
tabIndex={ 0 }
role="treeitem"
aria-label={ ariaLabel }
Expand Down Expand Up @@ -828,7 +875,12 @@ const CommentBoard = ( {
/>
}
/>
<Menu.Popover>
<Menu.Popover
// The menu popover is rendered in a portal, which causes focus to be
// lost and the note to be collapsed unintentionally. To prevent this,
// the popover should be rendered as an inline.
modal={ false }
>
{ moreActions.map( ( action ) => (
<Menu.Item
key={ action.id }
Expand Down
25 changes: 25 additions & 0 deletions test/e2e/specs/editor/various/block-comments.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,31 @@ test.describe( 'Block Comments', () => {
await expect( thread ).toBeFocused();
} );

test( 'should collapse a comment when the focus moves outside the note', async ( {
page,
blockCommentUtils,
} ) => {
await blockCommentUtils.addBlockWithComment( {
type: 'core/heading',
attributes: { content: 'Testing block comments' },
comment: 'Test comment',
} );

const thread = page
.getByRole( 'region', {
name: 'Editor settings',
} )
.getByRole( 'treeitem', {
name: 'Note: Test comment',
} );

await thread.click();
await expect( thread ).toHaveAttribute( 'aria-expanded', 'true' );
await page.keyboard.press( 'Shift+Tab' );
await expect( thread ).not.toBeFocused();
await expect( thread ).toHaveAttribute( 'aria-expanded', 'false' );
} );

test( 'should have accessible name for the comment threads', async ( {
page,
blockCommentUtils,
Expand Down
Loading