Skip to content

Commit

Permalink
Clear selected block when focusing on element outside the editor (#31530
Browse files Browse the repository at this point in the history
)

* Clear selected block when focusing on element outside the editor

* Add comments

* Add tests

* Fix lint errors
  • Loading branch information
kevin940726 authored May 8, 2021
1 parent 30e6eb0 commit a3ad0b5
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
/**
* WordPress dependencies
*/
import { useState, useEffect, createPortal } from '@wordpress/element';
import { useState, useEffect, useRef, createPortal } from '@wordpress/element';
import { SlotFillProvider, Popover } from '@wordpress/components';

/**
* Internal dependencies
*/
import SidebarBlockEditor from '../sidebar-block-editor';
import FocusControl from '../focus-control';
import SidebarControls from '../sidebar-controls';
import useClearSelectedBlock from './use-clear-selected-block';

export default function CustomizeWidgets( {
api,
sidebarControls,
blockEditorSettings,
} ) {
const [ activeSidebarControl, setActiveSidebarControl ] = useState( null );
const parentContainer = document.getElementById(
'customize-theme-controls'
);
const popoverRef = useRef();

useClearSelectedBlock( activeSidebarControl, popoverRef );

useEffect( () => {
const unsubscribers = sidebarControls.map( ( sidebarControl ) =>
Expand Down Expand Up @@ -44,14 +52,28 @@ export default function CustomizeWidgets( {
activeSidebarControl.container[ 0 ]
);

// We have to portal this to the parent of both the editor and the inspector,
// so that the popovers will appear above both of them.
const popover =
parentContainer &&
createPortal(
<div ref={ popoverRef }>
<Popover.Slot />
</div>,
parentContainer
);

return (
<SidebarControls
sidebarControls={ sidebarControls }
activeSidebarControl={ activeSidebarControl }
>
<FocusControl api={ api } sidebarControls={ sidebarControls }>
{ activeSidebar }
</FocusControl>
</SidebarControls>
<SlotFillProvider>
<SidebarControls
sidebarControls={ sidebarControls }
activeSidebarControl={ activeSidebarControl }
>
<FocusControl api={ api } sidebarControls={ sidebarControls }>
{ activeSidebar }
{ popover }
</FocusControl>
</SidebarControls>
</SlotFillProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* WordPress dependencies
*/
import { useEffect } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';

/**
* We can't just use <BlockSelectionClearer> because the customizer has
* many root nodes rather than just one in the post editor.
* We need to listen to the focus events in all those roots, and also in
* the preview iframe.
*
* @param {Object} sidebarControl The sidebar control instance.
* @param {Object} popoverRef The ref object of the popover node container.
*/
export default function useClearSelectedBlock( sidebarControl, popoverRef ) {
const { hasSelectedBlock, hasMultiSelection } = useSelect(
blockEditorStore
);
const { clearSelectedBlock } = useDispatch( blockEditorStore );

useEffect( () => {
if ( popoverRef.current && sidebarControl ) {
const inspectorContainer =
sidebarControl.inspector.contentContainer[ 0 ];
const container = sidebarControl.container[ 0 ];
const ownerDocument = container.ownerDocument;
const ownerWindow = ownerDocument.defaultView;

function handleClearSelectedBlock( element ) {
if (
// 1. Make sure there are blocks being selected.
( hasSelectedBlock() || hasMultiSelection() ) &&
// 2. The element should exist in the DOM (not deleted).
element &&
ownerDocument.contains( element ) &&
// 3. It should also not exist in the container, inspector, nor the popover.
! container.contains( element ) &&
! popoverRef.current.contains( element ) &&
! inspectorContainer.contains( element )
) {
clearSelectedBlock();
}
}

// Handle focusing in the same document.
function handleFocus( event ) {
handleClearSelectedBlock( event.target );
}
// Handle focusing outside the current document, like to iframes.
function handleBlur() {
handleClearSelectedBlock( ownerDocument.activeElement );
}

ownerDocument.addEventListener( 'focusin', handleFocus );
ownerWindow.addEventListener( 'blur', handleBlur );

return () => {
ownerDocument.removeEventListener( 'focusin', handleFocus );
ownerWindow.removeEventListener( 'blur', handleBlur );
};
}
}, [
popoverRef,
sidebarControl,
hasSelectedBlock,
hasMultiSelection,
clearSelectedBlock,
] );
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
BlockEditorKeyboardShortcuts,
__unstableBlockSettingsMenuFirstItem,
} from '@wordpress/block-editor';
import { SlotFillProvider, Popover } from '@wordpress/components';
import { uploadMedia } from '@wordpress/media-utils';

/**
Expand Down Expand Up @@ -60,61 +59,48 @@ export default function SidebarBlockEditor( {
mediaUpload: mediaUploadBlockEditor,
};
}, [ hasUploadPermissions, blockEditorSettings ] );
const parentContainer = document.getElementById(
'customize-theme-controls'
);

return (
<>
<BlockEditorKeyboardShortcuts.Register />
<SlotFillProvider>
<SidebarEditorProvider
sidebar={ sidebar }
settings={ settings }
>
<BlockEditorKeyboardShortcuts />

<Header
inserter={ inserter }
isInserterOpened={ isInserterOpened }
setIsInserterOpened={ setIsInserterOpened }
/>
<SidebarEditorProvider sidebar={ sidebar } settings={ settings }>
<BlockEditorKeyboardShortcuts />

<BlockTools>
<BlockSelectionClearer>
<WritingFlow>
<ObserveTyping>
<BlockList />
</ObserveTyping>
</WritingFlow>
</BlockSelectionClearer>
</BlockTools>
<Header
inserter={ inserter }
isInserterOpened={ isInserterOpened }
setIsInserterOpened={ setIsInserterOpened }
/>

{ createPortal(
// This is a temporary hack to prevent button component inside <BlockInspector>
// from submitting form when type="button" is not specified.
<form onSubmit={ ( event ) => event.preventDefault() }>
<BlockInspector />
</form>,
inspector.contentContainer[ 0 ]
) }
</SidebarEditorProvider>
<BlockTools>
<BlockSelectionClearer>
<WritingFlow>
<ObserveTyping>
<BlockList />
</ObserveTyping>
</WritingFlow>
</BlockSelectionClearer>
</BlockTools>

<__unstableBlockSettingsMenuFirstItem>
{ ( { onClose } ) => (
<BlockInspectorButton
inspector={ inspector }
closeMenu={ onClose }
/>
) }
</__unstableBlockSettingsMenuFirstItem>
{ createPortal(
// This is a temporary hack to prevent button component inside <BlockInspector>
// from submitting form when type="button" is not specified.
<form onSubmit={ ( event ) => event.preventDefault() }>
<BlockInspector />
</form>,
inspector.contentContainer[ 0 ]
) }
</SidebarEditorProvider>

{
// We have to portal this to the parent of both the editor and the inspector,
// so that the popovers will appear above both of them.
createPortal( <Popover.Slot />, parentContainer )
}
</SlotFillProvider>
<__unstableBlockSettingsMenuFirstItem>
{ ( { onClose } ) => (
<BlockInspectorButton
inspector={ inspector }
closeMenu={ onClose }
/>
) }
</__unstableBlockSettingsMenuFirstItem>
</>
);
}
68 changes: 68 additions & 0 deletions packages/e2e-tests/specs/experiments/customizing-widgets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,72 @@ describe( 'Widgets Customizer', () => {
"The page delivered both an 'X-Frame-Options' header and a 'Content-Security-Policy' header with a 'frame-ancestors' directive. Although the 'X-Frame-Options' header alone would have blocked embedding, it has been ignored."
);
} );

it( 'should clear block selection', async () => {
const widgetsPanel = await find( {
role: 'heading',
name: /Widgets/,
level: 3,
} );
await widgetsPanel.click();

const footer1Section = await find( {
role: 'heading',
name: /^Footer #1/,
level: 3,
} );
await footer1Section.click();

const paragraphBlock = await addBlock( 'Paragraph' );
await page.keyboard.type( 'First Paragraph' );
await showBlockToolbar();

const sectionHeading = await find( {
role: 'heading',
name: 'Customizing ▸ Widgets Footer #1',
level: 3,
} );
await sectionHeading.click();

// Expect clicking on the section title should clear the selection.
await expect( {
role: 'toolbar',
name: 'Block tools',
} ).not.toBeFound();

await paragraphBlock.focus();
await showBlockToolbar();

const preview = await page.$( '#customize-preview' );
await preview.click();

// Expect clicking on the preview iframe should clear the selection.
await expect( {
role: 'toolbar',
name: 'Block tools',
} ).not.toBeFound();

await paragraphBlock.focus();
await showBlockToolbar();

const editorContainer = await page.$(
'#customize-control-sidebars_widgets-sidebar-1'
);
const { x, y, width, height } = await editorContainer.boundingBox();
// Simulate Clicking on the empty space at the end of the editor.
await page.mouse.click( x + width / 2, y + height + 10 );

// Expect clicking on the empty space at the end of the editor
// should clear the selection.
await expect( {
role: 'toolbar',
name: 'Block tools',
} ).not.toBeFound();

expect( console ).toHaveWarned(
"The page delivered both an 'X-Frame-Options' header and a 'Content-Security-Policy' header with a 'frame-ancestors' directive. Although the 'X-Frame-Options' header alone would have blocked embedding, it has been ignored."
);
} );
} );

async function setWidgetsCustomizerExperiment( enabled ) {
Expand Down Expand Up @@ -517,4 +583,6 @@ async function addBlock( blockName ) {
selector: '.is-selected[data-block]',
} );
await addedBlock.focus();

return addedBlock;
}

0 comments on commit a3ad0b5

Please sign in to comment.