diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md
index 2773dbcc5dcbd9..8b280692ac1353 100644
--- a/packages/block-editor/CHANGELOG.md
+++ b/packages/block-editor/CHANGELOG.md
@@ -2,6 +2,14 @@
## Unreleased
+### Performance
+
+- Avoid re-rendering all List View items on block focus [#35706](https://github.com/WordPress/gutenberg/pull/35706). These changes speed up block focus time in large posts by 80% when List View is open.
+
+### Breaking change
+
+- List View no longer supports the `showOnlyCurrentHierarchy` flag [#35706](https://github.com/WordPress/gutenberg/pull/35706). To display a subset of blocks, use the `blocks` parameter instead.
+
## 7.0.0 (2021-07-29)
### Breaking Change
diff --git a/packages/block-editor/src/components/block-navigation/dropdown.js b/packages/block-editor/src/components/block-navigation/dropdown.js
index 414706ddf74338..a67d0aa2a4e4f8 100644
--- a/packages/block-editor/src/components/block-navigation/dropdown.js
+++ b/packages/block-editor/src/components/block-navigation/dropdown.js
@@ -67,7 +67,6 @@ function BlockNavigationDropdown(
diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js
index f9b7bc8bbc9738..97be704702f6ee 100644
--- a/packages/block-editor/src/components/list-view/block.js
+++ b/packages/block-editor/src/components/list-view/block.js
@@ -11,8 +11,8 @@ import {
__experimentalTreeGridItem as TreeGridItem,
} from '@wordpress/components';
import { moreVertical } from '@wordpress/icons';
-import { useState, useRef, useEffect } from '@wordpress/element';
-import { useDispatch } from '@wordpress/data';
+import { useState, useRef, useEffect, useCallback } from '@wordpress/element';
+import { useDispatch, useSelect, AsyncModeProvider } from '@wordpress/data';
/**
* Internal dependencies
@@ -26,15 +26,12 @@ import ListViewBlockContents from './block-contents';
import BlockSettingsDropdown from '../block-settings-menu/block-settings-dropdown';
import { useListViewContext } from './context';
import { store as blockEditorStore } from '../../store';
+import { isClientIdSelected } from './utils';
export default function ListViewBlock( {
block,
- isSelected,
isDragged,
- isBranchSelected,
- isLastOfSelectedBranch,
- onClick,
- onToggleExpanded,
+ selectBlock,
position,
level,
rowCount,
@@ -49,17 +46,50 @@ export default function ListViewBlock( {
const { toggleBlockHighlight } = useDispatch( blockEditorStore );
+ const {
+ __experimentalFeatures: withExperimentalFeatures,
+ __experimentalPersistentListViewFeatures: withExperimentalPersistentListViewFeatures,
+ __experimentalHideContainerBlockActions: hideContainerBlockActions,
+ isTreeGridMounted,
+ expand,
+ collapse,
+ } = useListViewContext();
+
+ const { isBranchSelected, isSelected } = useSelect(
+ ( select ) => {
+ const {
+ getSelectedBlockClientId,
+ getSelectedBlockClientIds,
+ getBlockParents,
+ } = select( blockEditorStore );
+
+ const selectedClientIds = withExperimentalPersistentListViewFeatures
+ ? getSelectedBlockClientIds()
+ : [ getSelectedBlockClientId() ];
+ const blockParents = getBlockParents( clientId );
+ const _isSelected = isClientIdSelected(
+ clientId,
+ selectedClientIds
+ );
+ return {
+ isSelected: _isSelected,
+ isBranchSelected:
+ _isSelected ||
+ blockParents.some( ( id ) => {
+ return isClientIdSelected( id, selectedClientIds );
+ } ),
+ };
+ },
+ [ withExperimentalPersistentListViewFeatures, clientId ]
+ );
+
const hasSiblings = siblingBlockCount > 0;
const hasRenderedMovers = showBlockMovers && hasSiblings;
const moverCellClassName = classnames(
'block-editor-list-view-block__mover-cell',
{ 'is-visible': isHovered || isSelected }
);
- const {
- __experimentalFeatures: withExperimentalFeatures,
- __experimentalPersistentListViewFeatures: withExperimentalPersistentListViewFeatures,
- isTreeGridMounted,
- } = useListViewContext();
+
const listViewBlockSettingsClassName = classnames(
'block-editor-list-view-block__menu-cell',
{ 'is-visible': isHovered || isSelected }
@@ -82,112 +112,148 @@ export default function ListViewBlock( {
? toggleBlockHighlight
: () => {};
- const onMouseEnter = () => {
+ const onMouseEnter = useCallback( () => {
setIsHovered( true );
highlightBlock( clientId, true );
- };
- const onMouseLeave = () => {
+ }, [ clientId, setIsHovered, highlightBlock ] );
+ const onMouseLeave = useCallback( () => {
setIsHovered( false );
highlightBlock( clientId, false );
- };
+ }, [ clientId, setIsHovered, highlightBlock ] );
+
+ const selectEditorBlock = useCallback(
+ ( event ) => {
+ event.stopPropagation();
+ selectBlock( clientId );
+ },
+ [ clientId, selectBlock ]
+ );
+
+ const toggleExpanded = useCallback(
+ ( event ) => {
+ event.stopPropagation();
+ if ( isExpanded === true ) {
+ collapse( clientId );
+ } else if ( isExpanded === false ) {
+ expand( clientId );
+ }
+ },
+ [ clientId, expand, collapse, isExpanded ]
+ );
+
+ const showBlockActions =
+ withExperimentalFeatures &&
+ //hide actions for blocks like core/widget-areas
+ ( ! hideContainerBlockActions ||
+ ( hideContainerBlockActions && level > 1 ) );
+
+ const hideBlockActions = withExperimentalFeatures && ! showBlockActions;
+
+ let colSpan;
+ if ( hasRenderedMovers ) {
+ colSpan = 2;
+ } else if ( hideBlockActions ) {
+ colSpan = 3;
+ }
const classes = classnames( {
'is-selected': isSelected,
'is-branch-selected':
withExperimentalPersistentListViewFeatures && isBranchSelected,
- 'is-last-of-selected-branch':
- withExperimentalPersistentListViewFeatures &&
- isLastOfSelectedBranch,
'is-dragging': isDragged,
+ 'has-single-cell': hideBlockActions,
} );
return (
-
-
+
- { ( { ref, tabIndex, onFocus } ) => (
-
-
-
- ) }
-
- { hasRenderedMovers && (
- <>
-
-
- { ( { ref, tabIndex, onFocus } ) => (
-
- ) }
-
-
- { ( { ref, tabIndex, onFocus } ) => (
-
- ) }
-
-
- >
- ) }
-
- { withExperimentalFeatures && (
-
+
{ ( { ref, tabIndex, onFocus } ) => (
-
+
+
+
) }
- ) }
-
+ { hasRenderedMovers && (
+ <>
+
+
+ { ( { ref, tabIndex, onFocus } ) => (
+
+ ) }
+
+
+ { ( { ref, tabIndex, onFocus } ) => (
+
+ ) }
+
+
+ >
+ ) }
+
+ { showBlockActions && (
+
+ { ( { ref, tabIndex, onFocus } ) => (
+
+ ) }
+
+ ) }
+
+
);
}
diff --git a/packages/block-editor/src/components/list-view/branch.js b/packages/block-editor/src/components/list-view/branch.js
index ccc0010b96a055..222fbad712b596 100644
--- a/packages/block-editor/src/components/list-view/branch.js
+++ b/packages/block-editor/src/components/list-view/branch.js
@@ -6,149 +6,76 @@ import { map, compact } from 'lodash';
/**
* WordPress dependencies
*/
-import { AsyncModeProvider } from '@wordpress/data';
+import { Fragment } from '@wordpress/element';
/**
* Internal dependencies
*/
import ListViewBlock from './block';
-import ListViewAppender from './appender';
-import { isClientIdSelected } from './utils';
import { useListViewContext } from './context';
export default function ListViewBranch( props ) {
const {
blocks,
selectBlock,
- showAppender,
showBlockMovers,
showNestedBlocks,
- parentBlockClientId,
level = 1,
- terminatedLevels = [],
- path = [],
- isBranchSelected = false,
- isLastOfBranch = false,
+ path = '',
} = props;
- const {
- expandedState,
- expand,
- collapse,
- draggedClientIds,
- selectedClientIds,
- } = useListViewContext();
+ const { expandedState, draggedClientIds } = useListViewContext();
- const isTreeRoot = ! parentBlockClientId;
const filteredBlocks = compact( blocks );
- const itemHasAppender = ( parentClientId ) =>
- showAppender &&
- ! isTreeRoot &&
- isClientIdSelected( parentClientId, selectedClientIds );
- const hasAppender = itemHasAppender( parentBlockClientId );
- // Add +1 to the rowCount to take the block appender into account.
const blockCount = filteredBlocks.length;
- const rowCount = hasAppender ? blockCount + 1 : blockCount;
- const appenderPosition = rowCount;
return (
<>
{ map( filteredBlocks, ( block, index ) => {
const { clientId, innerBlocks } = block;
const position = index + 1;
- const isLastRowAtLevel = rowCount === position;
- const updatedTerminatedLevels = isLastRowAtLevel
- ? [ ...terminatedLevels, level ]
- : terminatedLevels;
- const updatedPath = [ ...path, position ];
+ // This string value is used to trigger an animation change.
+ // This may be removed if we use a different animation library in the future.
+ const updatedPath =
+ path.length > 0
+ ? `${ path }_${ position }`
+ : `${ position }`;
const hasNestedBlocks =
showNestedBlocks && !! innerBlocks && !! innerBlocks.length;
- const hasNestedAppender = itemHasAppender( clientId );
- const hasNestedBranch = hasNestedBlocks || hasNestedAppender;
-
- const isSelected = isClientIdSelected(
- clientId,
- selectedClientIds
- );
- const isSelectedBranch =
- isBranchSelected || ( isSelected && hasNestedBranch );
- // Logic needed to target the last item of a selected branch which might be deeply nested.
- // This is currently only needed for styling purposes. See: `.is-last-of-selected-branch`.
- const isLastBlock = index === blockCount - 1;
- const isLast = isSelected || ( isLastOfBranch && isLastBlock );
- const isLastOfSelectedBranch =
- isLastOfBranch && ! hasNestedBranch && isLastBlock;
-
- const isExpanded = hasNestedBranch
+ const isExpanded = hasNestedBlocks
? expandedState[ clientId ] ?? true
: undefined;
- const selectBlockWithClientId = ( event ) => {
- event.stopPropagation();
- selectBlock( clientId );
- };
-
- const toggleExpanded = ( event ) => {
- event.stopPropagation();
- if ( isExpanded === true ) {
- collapse( clientId );
- } else if ( isExpanded === false ) {
- expand( clientId );
- }
- };
-
- // Make updates to the selected or dragged blocks synchronous,
- // but asynchronous for any other block.
const isDragged = !! draggedClientIds?.includes( clientId );
return (
-
+
- { hasNestedBranch && isExpanded && ! isDragged && (
+ { hasNestedBlocks && isExpanded && ! isDragged && (
) }
-
+
);
} ) }
- { hasAppender && (
-
- ) }
>
);
}
diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js
index 9985727a7b451d..8ce9f492b44ce2 100644
--- a/packages/block-editor/src/components/list-view/index.js
+++ b/packages/block-editor/src/components/list-view/index.js
@@ -46,31 +46,26 @@ const expanded = ( state, action ) => {
* @param {Array} props.blocks Custom subset of block client IDs to be used instead of the default hierarchy.
* @param {Function} props.onSelect Block selection callback.
* @param {boolean} props.showNestedBlocks Flag to enable displaying nested blocks.
- * @param {boolean} props.showOnlyCurrentHierarchy Flag to limit the list to the current hierarchy of blocks.
+ * @param {boolean} props.showBlockMovers Flag to enable block movers
* @param {boolean} props.__experimentalFeatures Flag to enable experimental features.
* @param {boolean} props.__experimentalPersistentListViewFeatures Flag to enable features for the Persistent List View experiment.
+ * @param {boolean} props.__experimentalHideContainerBlockActions Flag to hide actions of top level blocks (like core/widget-area)
* @param {Object} ref Forwarded ref
*/
function ListView(
{
blocks,
- showOnlyCurrentHierarchy,
onSelect = noop,
__experimentalFeatures,
__experimentalPersistentListViewFeatures,
+ __experimentalHideContainerBlockActions,
+ showNestedBlocks,
+ showBlockMovers,
...props
},
ref
) {
- const {
- clientIdsTree,
- selectedClientIds,
- draggedClientIds,
- } = useListViewClientIds(
- blocks,
- showOnlyCurrentHierarchy,
- __experimentalPersistentListViewFeatures
- );
+ const { clientIdsTree, draggedClientIds } = useListViewClientIds( blocks );
const { selectBlock } = useDispatch( blockEditorStore );
const selectEditorBlock = useCallback(
( clientId ) => {
@@ -108,20 +103,26 @@ function ListView(
},
[ setExpandedState ]
);
- const expandRow = ( row ) => {
- expand( row?.dataset?.block );
- };
- const collapseRow = ( row ) => {
- collapse( row?.dataset?.block );
- };
+ const expandRow = useCallback(
+ ( row ) => {
+ expand( row?.dataset?.block );
+ },
+ [ expand ]
+ );
+ const collapseRow = useCallback(
+ ( row ) => {
+ collapse( row?.dataset?.block );
+ },
+ [ collapse ]
+ );
const contextValue = useMemo(
() => ( {
__experimentalFeatures,
__experimentalPersistentListViewFeatures,
+ __experimentalHideContainerBlockActions,
isTreeGridMounted: isMounted.current,
draggedClientIds,
- selectedClientIds,
expandedState,
expand,
collapse,
@@ -129,9 +130,9 @@ function ListView(
[
__experimentalFeatures,
__experimentalPersistentListViewFeatures,
+ __experimentalHideContainerBlockActions,
isMounted.current,
draggedClientIds,
- selectedClientIds,
expandedState,
expand,
collapse,
@@ -155,6 +156,8 @@ function ListView(
diff --git a/packages/block-editor/src/components/list-view/leaf.js b/packages/block-editor/src/components/list-view/leaf.js
index 8098d44647fc98..41bf4bc34cc665 100644
--- a/packages/block-editor/src/components/list-view/leaf.js
+++ b/packages/block-editor/src/components/list-view/leaf.js
@@ -30,7 +30,7 @@ export default function ListViewLeaf( {
isSelected,
adjustScrolling: false,
enableAnimation: true,
- triggerAnimationOnChange: path.join( '_' ),
+ triggerAnimationOnChange: path,
} );
return (
diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss
index eb05d1426c376b..a4d151092a5684 100644
--- a/packages/block-editor/src/components/list-view/style.scss
+++ b/packages/block-editor/src/components/list-view/style.scss
@@ -57,9 +57,6 @@
&.is-branch-selected:not(.is-selected) .block-editor-list-view-block-contents {
border-radius: 0;
}
- &.is-branch-selected.is-last-of-selected-branch .block-editor-list-view-block-contents {
- border-radius: 0 0 2px 2px;
- }
&.is-dragging {
display: none;
@@ -115,6 +112,11 @@
}
}
}
+ // Fix focus styling width when one row has fewer cells.
+ &.has-single-cell .block-editor-list-view-block-contents:focus::after {
+ right: 0;
+ }
+
.block-editor-list-view-block__menu:focus {
box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
z-index: 1;
diff --git a/packages/block-editor/src/components/list-view/use-list-view-client-ids.js b/packages/block-editor/src/components/list-view/use-list-view-client-ids.js
index 14b3129bb74092..f6c79a722e8647 100644
--- a/packages/block-editor/src/components/list-view/use-list-view-client-ids.js
+++ b/packages/block-editor/src/components/list-view/use-list-view-client-ids.js
@@ -7,82 +7,21 @@ import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
-import { isClientIdSelected } from './utils';
import { store as blockEditorStore } from '../../store';
-const useListViewClientIdsTree = (
- blocks,
- selectedClientIds,
- showOnlyCurrentHierarchy
-) =>
- useSelect(
+export default function useListViewClientIds( blocks ) {
+ return useSelect(
( select ) => {
const {
- getBlockHierarchyRootClientId,
- __unstableGetClientIdsTree,
- __unstableGetClientIdWithClientIdsTree,
- } = select( blockEditorStore );
-
- if ( blocks ) {
- return blocks;
- }
-
- const isSingleBlockSelected =
- selectedClientIds && ! Array.isArray( selectedClientIds );
- if ( ! showOnlyCurrentHierarchy || ! isSingleBlockSelected ) {
- return __unstableGetClientIdsTree();
- }
-
- const rootBlock = __unstableGetClientIdWithClientIdsTree(
- getBlockHierarchyRootClientId( selectedClientIds )
- );
- if ( ! rootBlock ) {
- return __unstableGetClientIdsTree();
- }
-
- const hasHierarchy =
- ! isClientIdSelected( rootBlock.clientId, selectedClientIds ) ||
- ( rootBlock.innerBlocks && rootBlock.innerBlocks.length !== 0 );
- if ( hasHierarchy ) {
- return [ rootBlock ];
- }
-
- return __unstableGetClientIdsTree();
- },
- [ blocks, selectedClientIds, showOnlyCurrentHierarchy ]
- );
-
-export default function useListViewClientIds(
- blocks,
- showOnlyCurrentHierarchy,
- __experimentalPersistentListViewFeatures
-) {
- const { selectedClientIds, draggedClientIds } = useSelect(
- ( select ) => {
- const {
- getSelectedBlockClientId,
- getSelectedBlockClientIds,
getDraggedBlockClientIds,
+ __unstableGetClientIdsTree,
} = select( blockEditorStore );
- if ( __experimentalPersistentListViewFeatures ) {
- return {
- selectedClientIds: getSelectedBlockClientIds(),
- draggedClientIds: getDraggedBlockClientIds(),
- };
- }
-
return {
- selectedClientIds: getSelectedBlockClientId(),
draggedClientIds: getDraggedBlockClientIds(),
+ clientIdsTree: blocks ? blocks : __unstableGetClientIdsTree(),
};
},
- [ __experimentalPersistentListViewFeatures ]
- );
- const clientIdsTree = useListViewClientIdsTree(
- blocks,
- selectedClientIds,
- showOnlyCurrentHierarchy
+ [ blocks ]
);
- return { clientIdsTree, selectedClientIds, draggedClientIds };
}
diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md
index 032b522b5105ef..df8ca4bd427927 100644
--- a/packages/e2e-test-utils/README.md
+++ b/packages/e2e-test-utils/README.md
@@ -111,6 +111,10 @@ _Parameters_
Undocumented declaration.
+### closeListView
+
+Closes list view
+
### createEmbeddingMatcher
Creates a function to determine if a request is embedding a certain URL.
@@ -508,6 +512,10 @@ Clicks on the button in the header which opens Document Settings sidebar when it
Opens the global block inserter.
+### openListView
+
+Opens list view
+
### openPreviewPage
Opens the preview page of an edited post.
diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js
index 1b3d6000aaaef7..517f7e5b506ce5 100644
--- a/packages/e2e-test-utils/src/index.js
+++ b/packages/e2e-test-utils/src/index.js
@@ -93,5 +93,6 @@ export {
rest as __experimentalRest,
batch as __experimentalBatch,
} from './rest-api';
+export { openListView, closeListView } from './list-view';
export * from './mocks';
diff --git a/packages/e2e-test-utils/src/inserter.js b/packages/e2e-test-utils/src/inserter.js
index 1bcce329f07dff..4e6b769e8ddacc 100644
--- a/packages/e2e-test-utils/src/inserter.js
+++ b/packages/e2e-test-utils/src/inserter.js
@@ -43,7 +43,12 @@ async function isGlobalInserterOpen() {
// "Add block" selector is required to make sure performance comparison
// doesn't fail on older branches where we still had "Add block" as label.
return !! document.querySelector(
- '.edit-post-header [aria-label="Add block"].is-pressed, .edit-site-header [aria-label="Add block"].is-pressed, .edit-post-header [aria-label="Toggle block inserter"].is-pressed, .edit-site-header [aria-label="Toggle block inserter"].is-pressed'
+ '.edit-post-header [aria-label="Add block"].is-pressed,' +
+ '.edit-site-header [aria-label="Add block"].is-pressed,' +
+ '.edit-post-header [aria-label="Toggle block inserter"].is-pressed,' +
+ '.edit-site-header [aria-label="Toggle block inserter"].is-pressed,' +
+ '.edit-widgets-header [aria-label="Toggle block inserter"].is-pressed,' +
+ '.edit-widgets-header [aria-label="Add block"].is-pressed'
);
} );
}
@@ -54,7 +59,12 @@ export async function toggleGlobalBlockInserter() {
// "Add block" selector is required to make sure performance comparison
// doesn't fail on older branches where we still had "Add block" as label.
await page.click(
- '.edit-post-header [aria-label="Add block"], .edit-site-header [aria-label="Add block"], .edit-post-header [aria-label="Toggle block inserter"], .edit-site-header [aria-label="Toggle block inserter"]'
+ '.edit-post-header [aria-label="Add block"],' +
+ '.edit-site-header [aria-label="Add block"],' +
+ '.edit-post-header [aria-label="Toggle block inserter"],' +
+ '.edit-site-header [aria-label="Toggle block inserter"],' +
+ '.edit-widgets-header [aria-label="Add block"],' +
+ '.edit-widgets-header [aria-label="Toggle block inserter"]'
);
}
diff --git a/packages/e2e-test-utils/src/list-view.js b/packages/e2e-test-utils/src/list-view.js
new file mode 100644
index 00000000000000..4098145dfb4404
--- /dev/null
+++ b/packages/e2e-test-utils/src/list-view.js
@@ -0,0 +1,33 @@
+async function toggleListView() {
+ await page.click(
+ '.edit-post-header-toolbar__list-view-toggle, .edit-site-header-toolbar__list-view-toggle, .edit-widgets-header-toolbar__list-view-toggle'
+ );
+}
+
+async function isListViewOpen() {
+ return await page.evaluate( () => {
+ return !! document.querySelector(
+ '.edit-post-header-toolbar__list-view-toggle.is-pressed, .edit-site-header-toolbar__list-view-toggle.is-pressed, .edit-widgets-header-toolbar__list-view-toggle.is-pressed'
+ );
+ } );
+}
+
+/**
+ * Opens list view
+ */
+export async function openListView() {
+ const isOpen = await isListViewOpen();
+ if ( ! isOpen ) {
+ await toggleListView();
+ }
+}
+
+/**
+ * Closes list view
+ */
+export async function closeListView() {
+ const isOpen = await isListViewOpen();
+ if ( isOpen ) {
+ await toggleListView();
+ }
+}
diff --git a/packages/e2e-tests/specs/widgets/editing-widgets.test.js b/packages/e2e-tests/specs/widgets/editing-widgets.test.js
index 19beaa55174ba8..f5aceb0e717fec 100644
--- a/packages/e2e-tests/specs/widgets/editing-widgets.test.js
+++ b/packages/e2e-tests/specs/widgets/editing-widgets.test.js
@@ -11,6 +11,11 @@ import {
deleteAllWidgets,
pressKeyWithModifier,
__experimentalRest as rest,
+ openListView,
+ closeListView,
+ openGlobalBlockInserter,
+ searchForBlock,
+ closeGlobalBlockInserter,
} from '@wordpress/e2e-test-utils';
/**
@@ -64,12 +69,7 @@ describe( 'Widgets screen', () => {
} );
async function getBlockInGlobalInserter( blockName ) {
- const addBlockButton = await find( {
- role: 'button',
- name: 'Toggle block inserter',
- pressed: false,
- } );
- await addBlockButton.click();
+ await openGlobalBlockInserter();
const blockLibrary = await find( {
role: 'region',
@@ -94,17 +94,11 @@ describe( 'Widgets screen', () => {
} );
await searchBox.type( blockName );
- const addBlock = await find(
- {
- role: 'option',
- name: blockName,
- },
- {
- root: blockLibrary,
- }
- );
+ await searchForBlock( blockName );
- return addBlock;
+ return await page.waitForXPath(
+ `//button//span[contains(text(), '${ blockName }')]`
+ );
}
async function expectInsertionPointIndicatorToBeBelowLastBlock(
@@ -118,10 +112,9 @@ describe( 'Widgets screen', () => {
const lastBlock = childBlocks[ childBlocks.length - 2 ];
const lastBlockBoundingBox = await lastBlock.boundingBox();
- // TODO: Probably need a more accessible way to select this, maybe a test ID or data attribute.
- const insertionPointIndicator = await find( {
- selector: '.block-editor-block-list__insertion-point-indicator',
- } );
+ const insertionPointIndicator = await page.$(
+ '.block-editor-block-list__insertion-point-indicator'
+ );
const insertionPointIndicatorBoundingBox = await insertionPointIndicator.boundingBox();
expect(
@@ -176,7 +169,8 @@ describe( 'Widgets screen', () => {
await page.keyboard.type( 'First Paragraph' );
addParagraphBlock = await getBlockInGlobalInserter( 'Paragraph' );
- await addParagraphBlock.hover();
+ await page.keyboard.press( 'Tab' );
+ await page.keyboard.press( 'Tab' );
await expectInsertionPointIndicatorToBeBelowLastBlock(
firstWidgetArea
@@ -847,6 +841,19 @@ describe( 'Widgets screen', () => {
expect( console ).toHaveErrored( twentyTwentyError );
} );
+
+ it( 'can toggle sidebar list view', async () => {
+ const widgetAreas = await findAll( {
+ role: 'document',
+ name: 'Block: Widget Area',
+ } );
+ await openListView();
+ const listItems = await page.$$(
+ '.edit-widgets-editor__list-view-panel .block-editor-list-view-leaf'
+ );
+ expect( listItems.length >= widgetAreas.length ).toEqual( true );
+ await closeListView();
+ } );
} );
/**
@@ -875,6 +882,8 @@ async function visitWidgetsScreen() {
}
async function saveWidgets() {
+ await closeListView();
+ await closeGlobalBlockInserter();
const updateButton = await find( {
role: 'button',
name: 'Update',
diff --git a/packages/edit-widgets/CHANGELOG.md b/packages/edit-widgets/CHANGELOG.md
index ff8b4ad7b07ade..9ede641e2f1f5b 100644
--- a/packages/edit-widgets/CHANGELOG.md
+++ b/packages/edit-widgets/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Enhancement
+
+- Enable persistent List View in the widget editor [#35706](https://github.com/WordPress/gutenberg/pull/35706).
+
## 3.0.0 (2021-07-29)
### Breaking Change
diff --git a/packages/edit-widgets/src/components/header/index.js b/packages/edit-widgets/src/components/header/index.js
index 96d8e6634296ab..5eaec7a099d7b9 100644
--- a/packages/edit-widgets/src/components/header/index.js
+++ b/packages/edit-widgets/src/components/header/index.js
@@ -5,13 +5,12 @@ import { useSelect, useDispatch } from '@wordpress/data';
import { __, _x } from '@wordpress/i18n';
import { Button, ToolbarItem, VisuallyHidden } from '@wordpress/components';
import {
- BlockNavigationDropdown,
NavigableToolbar,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { PinnedItems } from '@wordpress/interface';
-import { plus } from '@wordpress/icons';
-import { useRef } from '@wordpress/element';
+import { listView, plus } from '@wordpress/icons';
+import { useCallback, useRef } from '@wordpress/element';
import { useViewportMatch } from '@wordpress/compose';
/**
@@ -35,18 +34,25 @@ function Header() {
),
[ widgetAreaClientId ]
);
- const isInserterOpened = useSelect(
- ( select ) => select( editWidgetsStore ).isInserterOpened(),
- []
- );
- const { setIsWidgetAreaOpen, setIsInserterOpened } = useDispatch(
- editWidgetsStore
- );
+ const { isInserterOpen, isListViewOpen } = useSelect( ( select ) => {
+ const { isInserterOpened, isListViewOpened } = select(
+ editWidgetsStore
+ );
+ return {
+ isInserterOpen: isInserterOpened(),
+ isListViewOpen: isListViewOpened(),
+ };
+ }, [] );
+ const {
+ setIsWidgetAreaOpen,
+ setIsInserterOpened,
+ setIsListViewOpened,
+ } = useDispatch( editWidgetsStore );
const { selectBlock } = useDispatch( blockEditorStore );
const handleClick = () => {
- if ( isInserterOpened ) {
+ if ( isInserterOpen ) {
// Focusing the inserter button closes the inserter popover
- inserterButton.current.focus();
+ setIsInserterOpened( false );
} else {
if ( ! isLastSelectedWidgetAreaOpen ) {
// Select the last selected block if hasn't already.
@@ -63,6 +69,11 @@ function Header() {
}
};
+ const toggleListView = useCallback(
+ () => setIsListViewOpened( ! isListViewOpen ),
+ [ setIsListViewOpened, isListViewOpen ]
+ );
+
return (
<>
@@ -89,7 +100,7 @@ function Header() {
as={ Button }
className="edit-widgets-header-toolbar__inserter-toggle"
variant="primary"
- isPressed={ isInserterOpened }
+ isPressed={ isInserterOpen }
onMouseDown={ ( event ) => {
event.preventDefault();
} }
@@ -106,7 +117,15 @@ function Header() {
<>
-
+
>
) }
diff --git a/packages/edit-widgets/src/components/layout/interface.js b/packages/edit-widgets/src/components/layout/interface.js
index 0969f0dabf9c1a..5845321aebab8b 100644
--- a/packages/edit-widgets/src/components/layout/interface.js
+++ b/packages/edit-widgets/src/components/layout/interface.js
@@ -1,16 +1,8 @@
/**
* WordPress dependencies
*/
-import { Button } from '@wordpress/components';
-import {
- __experimentalUseDialog as useDialog,
- useViewportMatch,
-} from '@wordpress/compose';
-import { close } from '@wordpress/icons';
-import {
- __experimentalLibrary as Library,
- BlockBreadcrumb,
-} from '@wordpress/block-editor';
+import { useViewportMatch } from '@wordpress/compose';
+import { BlockBreadcrumb } from '@wordpress/block-editor';
import { useEffect } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import {
@@ -26,8 +18,8 @@ import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
*/
import Header from '../header';
import WidgetAreasBlockEditorContent from '../widget-areas-block-editor-content';
-import useWidgetLibraryInsertionPoint from '../../hooks/use-widget-library-insertion-point';
import { store as editWidgetsStore } from '../../store';
+import SecondarySidebar from '../secondary-sidebar';
const interfaceLabels = {
/* translators: accessibility text for the widgets screen top bar landmark region. */
@@ -43,15 +35,16 @@ const interfaceLabels = {
function Interface( { blockEditorSettings } ) {
const isMobileViewport = useViewportMatch( 'medium', '<' );
const isHugeViewport = useViewportMatch( 'huge', '>=' );
- const { setIsInserterOpened, closeGeneralSidebar } = useDispatch(
- editWidgetsStore
- );
- const { rootClientId, insertionIndex } = useWidgetLibraryInsertionPoint();
-
+ const {
+ setIsInserterOpened,
+ setIsListViewOpened,
+ closeGeneralSidebar,
+ } = useDispatch( editWidgetsStore );
const {
hasBlockBreadCrumbsEnabled,
hasSidebarEnabled,
isInserterOpened,
+ isListViewOpened,
previousShortcut,
nextShortcut,
} = useSelect(
@@ -60,6 +53,7 @@ function Interface( { blockEditorSettings } ) {
interfaceStore
).getActiveComplementaryArea( editWidgetsStore.name ),
isInserterOpened: !! select( editWidgetsStore ).isInserterOpened(),
+ isListViewOpened: !! select( editWidgetsStore ).isListViewOpened(),
hasBlockBreadCrumbsEnabled: select(
interfaceStore
).isFeatureActive( 'core/edit-widgets', 'showBlockBreadcrumbs' ),
@@ -79,47 +73,21 @@ function Interface( { blockEditorSettings } ) {
useEffect( () => {
if ( hasSidebarEnabled && ! isHugeViewport ) {
setIsInserterOpened( false );
+ setIsListViewOpened( false );
}
}, [ hasSidebarEnabled, isHugeViewport ] );
useEffect( () => {
- if ( isInserterOpened && ! isHugeViewport ) {
+ if ( ( isInserterOpened || isListViewOpened ) && ! isHugeViewport ) {
closeGeneralSidebar();
}
- }, [ isInserterOpened, isHugeViewport ] );
-
- const [ inserterDialogRef, inserterDialogProps ] = useDialog( {
- onClose: () => setIsInserterOpened( false ),
- } );
+ }, [ isInserterOpened, isListViewOpened, isHugeViewport ] );
return (
}
- secondarySidebar={
- isInserterOpened && (
-
-
- setIsInserterOpened( false ) }
- />
-
-
-
-
-
- )
- }
+ secondarySidebar={
}
sidebar={
hasSidebarEnabled && (
diff --git a/packages/edit-widgets/src/components/secondary-sidebar/index.js b/packages/edit-widgets/src/components/secondary-sidebar/index.js
new file mode 100644
index 00000000000000..74d84238a06636
--- /dev/null
+++ b/packages/edit-widgets/src/components/secondary-sidebar/index.js
@@ -0,0 +1,34 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+/**
+ * Internal dependencies
+ */
+import { store as editWidgetsStore } from '../../store';
+
+/**
+ * Internal dependencies
+ */
+import InserterSidebar from './inserter-sidebar';
+import ListViewSidebar from './list-view-sidebar';
+
+export default function SecondarySidebar() {
+ const { isInserterOpen, isListViewOpen } = useSelect( ( select ) => {
+ const { isInserterOpened, isListViewOpened } = select(
+ editWidgetsStore
+ );
+ return {
+ isInserterOpen: isInserterOpened(),
+ isListViewOpen: isListViewOpened(),
+ };
+ }, [] );
+
+ if ( isInserterOpen ) {
+ return
;
+ }
+ if ( isListViewOpen ) {
+ return
;
+ }
+ return null;
+}
diff --git a/packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js b/packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js
new file mode 100644
index 00000000000000..5c60ef401ca5a0
--- /dev/null
+++ b/packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js
@@ -0,0 +1,53 @@
+/**
+ * WordPress dependencies
+ */
+import { Button } from '@wordpress/components';
+import { close } from '@wordpress/icons';
+import { __experimentalLibrary as Library } from '@wordpress/block-editor';
+import {
+ useViewportMatch,
+ __experimentalUseDialog as useDialog,
+} from '@wordpress/compose';
+import { useCallback } from '@wordpress/element';
+import { useDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import useWidgetLibraryInsertionPoint from '../../hooks/use-widget-library-insertion-point';
+import { store as editWidgetsStore } from '../../store';
+
+export default function InserterSidebar() {
+ const isMobileViewport = useViewportMatch( 'medium', '<' );
+ const { rootClientId, insertionIndex } = useWidgetLibraryInsertionPoint();
+
+ const { setIsInserterOpened } = useDispatch( editWidgetsStore );
+
+ const closeInserter = useCallback( () => {
+ return () => setIsInserterOpened( false );
+ }, [ setIsInserterOpened ] );
+
+ const [ inserterDialogRef, inserterDialogProps ] = useDialog( {
+ onClose: closeInserter,
+ } );
+
+ return (
+
+ );
+}
diff --git a/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js
new file mode 100644
index 00000000000000..ef072989fb38db
--- /dev/null
+++ b/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js
@@ -0,0 +1,75 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ __experimentalListView as ListView,
+ store as blockEditorStore,
+} from '@wordpress/block-editor';
+import { Button } from '@wordpress/components';
+import {
+ useFocusOnMount,
+ useFocusReturn,
+ useInstanceId,
+ useMergeRefs,
+} from '@wordpress/compose';
+import { useDispatch } from '@wordpress/data';
+import { __ } from '@wordpress/i18n';
+import { closeSmall } from '@wordpress/icons';
+import { ESCAPE } from '@wordpress/keycodes';
+
+/**
+ * Internal dependencies
+ */
+import { store as editWidgetsStore } from '../../store';
+
+export default function ListViewSidebar() {
+ const { setIsListViewOpened } = useDispatch( editWidgetsStore );
+
+ const { clearSelectedBlock, selectBlock } = useDispatch( blockEditorStore );
+ async function selectEditorBlock( clientId ) {
+ await clearSelectedBlock();
+ selectBlock( clientId, -1 );
+ }
+
+ const focusOnMountRef = useFocusOnMount( 'firstElement' );
+ const focusReturnRef = useFocusReturn();
+ function closeOnEscape( event ) {
+ if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) {
+ event.preventDefault();
+ setIsListViewOpened( false );
+ }
+ }
+
+ const instanceId = useInstanceId( ListViewSidebar );
+ const labelId = `edit-widgets-editor__list-view-panel-label-${ instanceId }`;
+
+ return (
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
+
+
+ { __( 'List view' ) }
+ setIsListViewOpened( false ) }
+ />
+
+
+
+
+
+ );
+}
diff --git a/packages/edit-widgets/src/components/secondary-sidebar/style.scss b/packages/edit-widgets/src/components/secondary-sidebar/style.scss
new file mode 100644
index 00000000000000..5acf945480a528
--- /dev/null
+++ b/packages/edit-widgets/src/components/secondary-sidebar/style.scss
@@ -0,0 +1,25 @@
+.edit-widgets-editor__list-view-panel {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ // Same width as the Inserter.
+ // @see packages/block-editor/src/components/inserter/style.scss
+ min-width: 350px;
+}
+
+.edit-widgets-editor__list-view-panel-content {
+ // Leave space for the close button
+ height: calc(100% - #{$button-size} - #{$grid-unit-10});
+ overflow-y: auto;
+ padding: $grid-unit-10;
+}
+
+.edit-widgets-editor__list-view-panel-header {
+ align-items: center;
+ border-bottom: $border-width solid $gray-300;
+ display: flex;
+ justify-content: space-between;
+ height: $grid-unit-60;
+ padding-left: $grid-unit-20;
+ padding-right: $grid-unit-05;
+}
diff --git a/packages/edit-widgets/src/store/actions.js b/packages/edit-widgets/src/store/actions.js
index 93462625e0a9e8..0701274885d71c 100644
--- a/packages/edit-widgets/src/store/actions.js
+++ b/packages/edit-widgets/src/store/actions.js
@@ -361,6 +361,19 @@ export function setIsInserterOpened( value ) {
};
}
+/**
+ * Returns an action object used to open/close the list view.
+ *
+ * @param {boolean} isOpen A boolean representing whether the list view should be opened or closed.
+ * @return {Object} Action object.
+ */
+export function setIsListViewOpened( isOpen ) {
+ return {
+ type: 'SET_IS_LIST_VIEW_OPENED',
+ isOpen,
+ };
+}
+
/**
* Returns an action object signalling that the user closed the sidebar.
*
diff --git a/packages/edit-widgets/src/store/reducer.js b/packages/edit-widgets/src/store/reducer.js
index b0fe86db810723..ff942b955edcdc 100644
--- a/packages/edit-widgets/src/store/reducer.js
+++ b/packages/edit-widgets/src/store/reducer.js
@@ -31,20 +31,45 @@ export function widgetAreasOpenState( state = {}, action ) {
}
/**
- * Reducer tracking whether the inserter is open.
+ * Reducer to set the block inserter panel open or closed.
*
- * @param {boolean|Object} state
- * @param {Object} action
+ * Note: this reducer interacts with the list view panel reducer
+ * to make sure that only one of the two panels is open at the same time.
+ *
+ * @param {Object} state Current state.
+ * @param {Object} action Dispatched action.
*/
-function blockInserterPanel( state = false, action ) {
+export function blockInserterPanel( state = false, action ) {
switch ( action.type ) {
+ case 'SET_IS_LIST_VIEW_OPENED':
+ return action.isOpen ? false : state;
case 'SET_IS_INSERTER_OPENED':
return action.value;
}
return state;
}
+/**
+ * Reducer to set the list view panel open or closed.
+ *
+ * Note: this reducer interacts with the inserter panel reducer
+ * to make sure that only one of the two panels is open at the same time.
+ *
+ * @param {Object} state Current state.
+ * @param {Object} action Dispatched action.
+ */
+export function listViewPanel( state = false, action ) {
+ switch ( action.type ) {
+ case 'SET_IS_INSERTER_OPENED':
+ return action.value ? false : state;
+ case 'SET_IS_LIST_VIEW_OPENED':
+ return action.isOpen;
+ }
+ return state;
+}
+
export default combineReducers( {
blockInserterPanel,
+ listViewPanel,
widgetAreasOpenState,
} );
diff --git a/packages/edit-widgets/src/store/selectors.js b/packages/edit-widgets/src/store/selectors.js
index a45d5a223e45a2..c672454fff1657 100644
--- a/packages/edit-widgets/src/store/selectors.js
+++ b/packages/edit-widgets/src/store/selectors.js
@@ -277,3 +277,14 @@ export const canInsertBlockInWidgetArea = createRegistrySelector(
);
}
);
+
+/**
+ * Returns true if the list view is opened.
+ *
+ * @param {Object} state Global application state.
+ *
+ * @return {boolean} Whether the list view is opened.
+ */
+export function isListViewOpened( state ) {
+ return state.listViewPanel;
+}
diff --git a/packages/edit-widgets/src/style.scss b/packages/edit-widgets/src/style.scss
index 7d75dc6443bda2..1a537c325b4e31 100644
--- a/packages/edit-widgets/src/style.scss
+++ b/packages/edit-widgets/src/style.scss
@@ -10,6 +10,7 @@
@import "./components/layout/style.scss";
@import "./components/welcome-guide/style.scss";
@import "./components/widget-areas-block-editor-content/style.scss";
+@import "./components/secondary-sidebar/style.scss";
// In order to use mix-blend-mode, this element needs to have an explicitly set background-color
// We scope it to .wp-toolbar to be wp-admin only, to prevent bleed into other implementations