From 30689b8de49cecc4ce2e9176a225b001ec4a21d8 Mon Sep 17 00:00:00 2001 From: Kerry Liu Date: Fri, 22 Oct 2021 10:16:57 -0700 Subject: [PATCH] List View: avoid re-rendering all items on block focus (#35706) * List View: add e2e utils for list view open and close * List View: move selection querying to List View Block to avoid re-rendering all items on block focus * List View: simplify useListViewClientIds and remove showOnlyCurrentHierarchy prop * List View: move nested AsyncModeProvider call to ListViewBlock * List View: make available flags present in JSDoc * List View: remove unneeded is-last-of-selected-branch logic * Edit Widgets: use list view sidebar instead of dropdown * Edit Widgets: do not show actions in list view for widget areas --- packages/block-editor/CHANGELOG.md | 8 + .../components/block-navigation/dropdown.js | 1 - .../src/components/list-view/block.js | 270 +++++++++++------- .../src/components/list-view/branch.js | 103 +------ .../src/components/list-view/index.js | 41 +-- .../src/components/list-view/leaf.js | 2 +- .../src/components/list-view/style.scss | 8 +- .../list-view/use-list-view-client-ids.js | 71 +---- packages/e2e-test-utils/README.md | 8 + packages/e2e-test-utils/src/index.js | 1 + packages/e2e-test-utils/src/inserter.js | 14 +- packages/e2e-test-utils/src/list-view.js | 33 +++ .../specs/widgets/editing-widgets.test.js | 51 ++-- packages/edit-widgets/CHANGELOG.md | 4 + .../src/components/header/index.js | 47 ++- .../src/components/layout/interface.js | 60 +--- .../src/components/secondary-sidebar/index.js | 34 +++ .../secondary-sidebar/inserter-sidebar.js | 53 ++++ .../secondary-sidebar/list-view-sidebar.js | 75 +++++ .../components/secondary-sidebar/style.scss | 25 ++ packages/edit-widgets/src/store/actions.js | 13 + packages/edit-widgets/src/store/reducer.js | 33 ++- packages/edit-widgets/src/store/selectors.js | 11 + packages/edit-widgets/src/style.scss | 1 + 24 files changed, 600 insertions(+), 367 deletions(-) create mode 100644 packages/e2e-test-utils/src/list-view.js create mode 100644 packages/edit-widgets/src/components/secondary-sidebar/index.js create mode 100644 packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js create mode 100644 packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js create mode 100644 packages/edit-widgets/src/components/secondary-sidebar/style.scss diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index 2773dbcc5dcbd..8b280692ac135 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 414706ddf7433..a67d0aa2a4e4f 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 f9b7bc8bbc973..97be704702f6e 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 ccc0010b96a05..222fbad712b59 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 9985727a7b451..8ce9f492b44ce 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 8098d44647fc9..41bf4bc34cc66 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 eb05d1426c376..a4d151092a568 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 14b3129bb7409..f6c79a722e864 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 032b522b5105e..df8ca4bd42792 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 1b3d6000aaaef..517f7e5b506ce 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 1bcce329f07df..4e6b769e8ddac 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 0000000000000..4098145dfb440 --- /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 19beaa55174ba..f5aceb0e717fe 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 ff8b4ad7b07ad..9ede641e2f1f5 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 96d8e6634296a..5eaec7a099d7b 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 0969f0dabf9c1..5845321aebab8 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 && ( -
-
-
-
- -
-
- ) - } + 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 0000000000000..74d84238a0663 --- /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 0000000000000..5c60ef401ca5a --- /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 0000000000000..ef072989fb38d --- /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' ) } +
+
+ +
+
+ ); +} 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 0000000000000..5acf945480a52 --- /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 93462625e0a9e..0701274885d71 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 b0fe86db81072..ff942b955edcd 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 a45d5a223e45a..c672454fff165 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 7d75dc6443bda..1a537c325b4e3 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