From d2824131e8b70a020055815c8ad199bf990d8d21 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Wed, 28 Sep 2022 18:56:54 +0300 Subject: [PATCH 01/17] stub list view implemented similarly to how the site editor navigation sidebar is --- .../src/navigation/edit/index.js | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 918b609f92a0e7..b9ad18a894228e 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -9,11 +9,13 @@ import classnames from 'classnames'; import { useState, useEffect, useRef, Platform } from '@wordpress/element'; import { addQueryArgs } from '@wordpress/url'; import { + __experimentalListView as ListView, InspectorControls, useBlockProps, __experimentalRecursionProvider as RecursionProvider, __experimentalUseHasRecursion as useHasRecursion, store as blockEditorStore, + BlockEditorProvider, withColors, PanelColorSettings, ContrastChecker, @@ -21,7 +23,11 @@ import { Warning, __experimentalUseBlockOverlayActive as useBlockOverlayActive, } from '@wordpress/block-editor'; -import { EntityProvider, store as coreStore } from '@wordpress/core-data'; +import { + useEntityBlockEditor, + EntityProvider, + store as coreStore, +} from '@wordpress/core-data'; import { useDispatch } from '@wordpress/data'; import { @@ -214,6 +220,12 @@ function Navigation( { hasResolvedCanUserCreateNavigationMenu, } = useNavigationMenu( ref ); + const [ navigationInnerBlocks, onInput, onChange ] = useEntityBlockEditor( + 'postType', + 'wp_navigation', + { id: ref } + ); + const navMenuResolvedButMissing = hasResolvedNavigationMenus && isNavigationMenuMissing; @@ -501,6 +513,18 @@ function Navigation( { const stylingInspectorControls = ( + + + + + { hasSubmenuIndicatorSetting && ( { isResponsive && ( From c526e8a04098180a1122ad286f7387531cda3341 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Wed, 28 Sep 2022 22:42:25 +0300 Subject: [PATCH 02/17] follow the design on where the list view is placed --- .../src/navigation/edit/index.js | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index b9ad18a894228e..151c88914046c0 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -513,18 +513,6 @@ function Navigation( { const stylingInspectorControls = ( - - - - - { hasSubmenuIndicatorSetting && ( { isResponsive && ( @@ -700,6 +688,16 @@ function Navigation( { /* translators: %s: The name of a menu. */ actionLabel={ __( "Switch to '%s'" ) } /> + + + { stylingInspectorControls } @@ -791,18 +779,6 @@ function Navigation( { isExpanded={ true } /> - @@ -919,18 +895,6 @@ function Navigation( { isExpanded={ true } /> - { stylingInspectorControls } @@ -957,6 +921,19 @@ function Navigation( { } } /> ) } + ) } diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index 1658c90fa38f11..cf7c04c84f4db3 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -582,6 +582,11 @@ body.editor-styles-wrapper margin-bottom: $grid-unit-20; } +// increased specificity to override button variant +.components-button.is-link.wp-block-navigation-manage-menus-button { + margin-bottom: $grid-unit-20; +} + .wp-block-navigation__overlay-menu-preview { display: flex; align-items: center; From 61bb3c32755bcfc7ba0f12b2a4a9f4052648b14f Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Thu, 29 Sep 2022 23:16:26 +0300 Subject: [PATCH 04/17] test no block selection --- .../list-view/block-select-button.js | 2 +- .../src/navigation/edit/index.js | 45 ++----------------- 2 files changed, 4 insertions(+), 43 deletions(-) diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index 9477eb2cda40c0..d663cf8beaf91a 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -67,7 +67,7 @@ function ListViewBlockSelectButton( 'block-editor-list-view-block-select-button', className ) } - onClick={ onClick } + onClick={ () => null } onKeyDown={ onKeyDownHandler } ref={ ref } tabIndex={ tabIndex } diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 0bea0a6c97a3ac..1225e77cdd1874 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -15,7 +15,6 @@ import { __experimentalRecursionProvider as RecursionProvider, __experimentalUseHasRecursion as useHasRecursion, store as blockEditorStore, - BlockEditorProvider, withColors, PanelColorSettings, ContrastChecker, @@ -23,11 +22,7 @@ import { Warning, __experimentalUseBlockOverlayActive as useBlockOverlayActive, } from '@wordpress/block-editor'; -import { - useEntityBlockEditor, - EntityProvider, - store as coreStore, -} from '@wordpress/core-data'; +import { EntityProvider, store as coreStore } from '@wordpress/core-data'; import { useDispatch } from '@wordpress/data'; import { @@ -220,12 +215,6 @@ function Navigation( { hasResolvedCanUserCreateNavigationMenu, } = useNavigationMenu( ref ); - const [ navigationInnerBlocks, onInput, onChange ] = useEntityBlockEditor( - 'postType', - 'wp_navigation', - { id: ref } - ); - const navMenuResolvedButMissing = hasResolvedNavigationMenus && isNavigationMenuMissing; @@ -688,16 +677,7 @@ function Navigation( { /* translators: %s: The name of a menu. */ actionLabel={ __( "Switch to '%s'" ) } /> - - - + { stylingInspectorControls } @@ -769,16 +749,6 @@ function Navigation( { /* translators: %s: The name of a menu. */ actionLabel={ __( "Switch to '%s'" ) } /> - - - @@ -885,16 +855,7 @@ function Navigation( { /* translators: %s: The name of a menu. */ actionLabel={ __( "Switch to '%s'" ) } /> - - - + { stylingInspectorControls } From ea21df0c2952e17e6c4f37e5728c24176b12a91b Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Fri, 14 Oct 2022 19:27:31 +0300 Subject: [PATCH 05/17] makes the navigation list view implementation a gutenberg experiment --- lib/experimental/editor-settings.php | 12 ++++++++++++ lib/experiments-page.php | 11 +++++++++++ .../block-library/src/navigation/edit/index.js | 17 +++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 2b46a401d4b0e9..3849bdfe5f4d2a 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -83,3 +83,15 @@ function gutenberg_enable_zoomed_out_view() { } add_action( 'admin_init', 'gutenberg_enable_zoomed_out_view' ); + +/** + * Sets a global JS variable used to trigger the availability of zoomed out view. + */ +function gutenberg_enable_off_canvas_navigation_editor() { + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-off-canvas-navigation-editor', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableOffCanvasNavigationEditor = true', 'before' ); + } +} + +add_action( 'admin_init', 'gutenberg_enable_off_canvas_navigation_editor' ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 3d09f05bd655c8..d925b77925cfdd 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -52,6 +52,17 @@ function gutenberg_initialize_experiments_settings() { 'id' => 'gutenberg-zoomed-out-view', ) ); + add_settings_field( + 'gutenberg-off-canvas-navigation-editor', + __( 'Off canvas navigation editor ', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Test a new off canvas editor for navigation block', 'gutenberg' ), + 'id' => 'gutenberg-off-canvas-navigation-editor', + ) + ); register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 1225e77cdd1874..2bd865c07e327c 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -83,6 +83,9 @@ function Navigation( { hasColorSettings = true, customPlaceholder: CustomPlaceholder = null, } ) { + const isOffCanvasNavigationEditorEnabled = + window?.__experimentalEnableOffCanvasNavigationEditor === true; + const { openSubmenusOnClick, overlayMenu, @@ -677,7 +680,12 @@ function Navigation( { /* translators: %s: The name of a menu. */ actionLabel={ __( "Switch to '%s'" ) } /> - + { isOffCanvasNavigationEditorEnabled && ( + + ) } { stylingInspectorControls } @@ -855,7 +863,12 @@ function Navigation( { /* translators: %s: The name of a menu. */ actionLabel={ __( "Switch to '%s'" ) } /> - + { isOffCanvasNavigationEditorEnabled && ( + + ) } { stylingInspectorControls } From c62610f1cb2da7237dd4ae7895d6b8554cae5f96 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Mon, 17 Oct 2022 16:36:35 +0300 Subject: [PATCH 06/17] Removed the hardcoded override of block selection behavior. --- .../src/components/list-view/block-select-button.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index d663cf8beaf91a..9477eb2cda40c0 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -67,7 +67,7 @@ function ListViewBlockSelectButton( 'block-editor-list-view-block-select-button', className ) } - onClick={ () => null } + onClick={ onClick } onKeyDown={ onKeyDownHandler } ref={ ref } tabIndex={ tabIndex } From aa331beb20de5716a1bcb0b5ee10c59f13bf5abd Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Mon, 17 Oct 2022 16:46:04 +0300 Subject: [PATCH 07/17] Adds a top level prop for list view to enable and disable block selection. --- .../block-editor/src/components/list-view/block.js | 7 ++++++- .../block-editor/src/components/list-view/branch.js | 2 ++ .../block-editor/src/components/list-view/index.js | 10 +++++++++- packages/block-library/src/navigation/edit/index.js | 2 ++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index 4d5e2e7ea8c49f..0444630004d87f 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -54,6 +54,7 @@ function ListViewBlock( { isExpanded, selectedClientIds, preventAnnouncement, + selectBlockInCanvas, } ) { const cellRef = useRef( null ); const [ isHovered, setIsHovered ] = useState( false ); @@ -245,7 +246,11 @@ function ListViewBlock( {
{} + } onToggleExpanded={ toggleExpanded } isSelected={ isSelected } position={ position } diff --git a/packages/block-editor/src/components/list-view/branch.js b/packages/block-editor/src/components/list-view/branch.js index 0c2b541f3199a1..c43a0e8737f898 100644 --- a/packages/block-editor/src/components/list-view/branch.js +++ b/packages/block-editor/src/components/list-view/branch.js @@ -92,6 +92,7 @@ function ListViewBranch( props ) { isExpanded, parentId, shouldShowInnerBlocks = true, + selectBlockInCanvas, } = props; const isContentLocked = useSelect( @@ -174,6 +175,7 @@ function ListViewBranch( props ) { isExpanded={ shouldExpand } listPosition={ nextPosition } selectedClientIds={ selectedClientIds } + selectBlockInCanvas={ selectBlockInCanvas } /> ) } { ! showBlock && ( diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 7166b594c276c2..5c3cfec1596143 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -55,10 +55,17 @@ export const BLOCK_LIST_ITEM_HEIGHT = 36; * @param {Array} props.blocks Custom subset of block client IDs to be used instead of the default hierarchy. * @param {boolean} props.showBlockMovers Flag to enable block movers * @param {boolean} props.isExpanded Flag to determine whether nested levels are expanded by default. + * @param {boolean} props.selectBlockInCanvas Flag to determine whether the list view should be a block selection mechanism,. * @param {Object} ref Forwarded ref */ function ListView( - { id, blocks, showBlockMovers = false, isExpanded = false }, + { + id, + blocks, + showBlockMovers = false, + isExpanded = false, + selectBlockInCanvas = true, + }, ref ) { const { clientIdsTree, draggedClientIds, selectedClientIds } = @@ -199,6 +206,7 @@ function ListView( selectedClientIds={ selectedClientIds } isExpanded={ isExpanded } shouldShowInnerBlocks={ shouldShowInnerBlocks } + selectBlockInCanvas={ selectBlockInCanvas } /> diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 2bd865c07e327c..95acf3404504fd 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -684,6 +684,7 @@ function Navigation( { ) } @@ -867,6 +868,7 @@ function Navigation( { ) } From 32300a364ff9871200ccfeb3e3f22088c549c54f Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Mon, 17 Oct 2022 16:52:13 +0300 Subject: [PATCH 08/17] Marks block selection toggle in ListView as experimental. --- packages/block-editor/src/components/list-view/index.js | 4 ++-- packages/block-library/src/navigation/edit/index.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 5c3cfec1596143..ba671c23e27e0a 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -55,7 +55,7 @@ export const BLOCK_LIST_ITEM_HEIGHT = 36; * @param {Array} props.blocks Custom subset of block client IDs to be used instead of the default hierarchy. * @param {boolean} props.showBlockMovers Flag to enable block movers * @param {boolean} props.isExpanded Flag to determine whether nested levels are expanded by default. - * @param {boolean} props.selectBlockInCanvas Flag to determine whether the list view should be a block selection mechanism,. + * @param {boolean} props.__experimentalSelectBlockInCanvas Flag to determine whether the list view should be a block selection mechanism,. * @param {Object} ref Forwarded ref */ function ListView( @@ -64,7 +64,7 @@ function ListView( blocks, showBlockMovers = false, isExpanded = false, - selectBlockInCanvas = true, + __experimentalSelectBlockInCanvas: selectBlockInCanvas = true, }, ref ) { diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 95acf3404504fd..2136f060e2a03a 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -684,7 +684,7 @@ function Navigation( { ) } @@ -868,7 +868,7 @@ function Navigation( { ) } From 0fb6e40d2a775219adb065bca6fb17560c7a0461 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Thu, 20 Oct 2022 11:58:04 +0300 Subject: [PATCH 09/17] Attempts a fork of list view into a new OffCanvasEditor component, to enable faster iteration. --- packages/block-editor/src/components/index.js | 1 + .../src/components/list-view/block.js | 7 +- .../src/components/list-view/branch.js | 2 - .../src/components/list-view/index.js | 10 +- .../components/off-canvas-editor/README.md | 5 + .../off-canvas-editor/block-contents.js | 89 ++++ .../off-canvas-editor/block-select-button.js | 113 +++++ .../src/components/off-canvas-editor/block.js | 333 ++++++++++++++ .../components/off-canvas-editor/branch.js | 212 +++++++++ .../components/off-canvas-editor/context.js | 8 + .../off-canvas-editor/drop-indicator.js | 125 +++++ .../components/off-canvas-editor/expander.js | 26 ++ .../src/components/off-canvas-editor/index.js | 216 +++++++++ .../src/components/off-canvas-editor/leaf.js | 48 ++ .../components/off-canvas-editor/style.scss | 432 ++++++++++++++++++ .../off-canvas-editor/test/utils.js | 50 ++ .../off-canvas-editor/use-block-selection.js | 169 +++++++ .../use-list-view-client-ids.js | 29 ++ .../use-list-view-drop-zone.js | 260 +++++++++++ .../use-list-view-expand-selected-item.js | 58 +++ .../src/components/off-canvas-editor/utils.js | 58 +++ .../src/navigation/edit/index.js | 10 +- 22 files changed, 2239 insertions(+), 22 deletions(-) create mode 100644 packages/block-editor/src/components/off-canvas-editor/README.md create mode 100644 packages/block-editor/src/components/off-canvas-editor/block-contents.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/block-select-button.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/block.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/branch.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/context.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/drop-indicator.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/expander.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/index.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/leaf.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/style.scss create mode 100644 packages/block-editor/src/components/off-canvas-editor/test/utils.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/use-block-selection.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/use-list-view-client-ids.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/use-list-view-drop-zone.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/use-list-view-expand-selected-item.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/utils.js diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 6b7a24887b3423..7743a98066c437 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -73,6 +73,7 @@ export { default as __experimentalLinkControlSearchResults } from './link-contro export { default as __experimentalLinkControlSearchItem } from './link-control/search-item'; export { default as LineHeightControl } from './line-height-control'; export { default as __experimentalListView } from './list-view'; +export { default as __experimentalOffCanvasEditor } from './off-canvas-editor'; export { default as MediaReplaceFlow } from './media-replace-flow'; export { default as MediaPlaceholder } from './media-placeholder'; export { default as MediaUpload } from './media-upload'; diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index 0444630004d87f..4d5e2e7ea8c49f 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -54,7 +54,6 @@ function ListViewBlock( { isExpanded, selectedClientIds, preventAnnouncement, - selectBlockInCanvas, } ) { const cellRef = useRef( null ); const [ isHovered, setIsHovered ] = useState( false ); @@ -246,11 +245,7 @@ function ListViewBlock( {
{} - } + onClick={ selectEditorBlock } onToggleExpanded={ toggleExpanded } isSelected={ isSelected } position={ position } diff --git a/packages/block-editor/src/components/list-view/branch.js b/packages/block-editor/src/components/list-view/branch.js index c43a0e8737f898..0c2b541f3199a1 100644 --- a/packages/block-editor/src/components/list-view/branch.js +++ b/packages/block-editor/src/components/list-view/branch.js @@ -92,7 +92,6 @@ function ListViewBranch( props ) { isExpanded, parentId, shouldShowInnerBlocks = true, - selectBlockInCanvas, } = props; const isContentLocked = useSelect( @@ -175,7 +174,6 @@ function ListViewBranch( props ) { isExpanded={ shouldExpand } listPosition={ nextPosition } selectedClientIds={ selectedClientIds } - selectBlockInCanvas={ selectBlockInCanvas } /> ) } { ! showBlock && ( diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index ba671c23e27e0a..7166b594c276c2 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -55,17 +55,10 @@ export const BLOCK_LIST_ITEM_HEIGHT = 36; * @param {Array} props.blocks Custom subset of block client IDs to be used instead of the default hierarchy. * @param {boolean} props.showBlockMovers Flag to enable block movers * @param {boolean} props.isExpanded Flag to determine whether nested levels are expanded by default. - * @param {boolean} props.__experimentalSelectBlockInCanvas Flag to determine whether the list view should be a block selection mechanism,. * @param {Object} ref Forwarded ref */ function ListView( - { - id, - blocks, - showBlockMovers = false, - isExpanded = false, - __experimentalSelectBlockInCanvas: selectBlockInCanvas = true, - }, + { id, blocks, showBlockMovers = false, isExpanded = false }, ref ) { const { clientIdsTree, draggedClientIds, selectedClientIds } = @@ -206,7 +199,6 @@ function ListView( selectedClientIds={ selectedClientIds } isExpanded={ isExpanded } shouldShowInnerBlocks={ shouldShowInnerBlocks } - selectBlockInCanvas={ selectBlockInCanvas } /> diff --git a/packages/block-editor/src/components/off-canvas-editor/README.md b/packages/block-editor/src/components/off-canvas-editor/README.md new file mode 100644 index 00000000000000..0c5f2a36156a57 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/README.md @@ -0,0 +1,5 @@ +# Experimental Off Canvas Editor + +The __ExperimentalOffCanvasEditor component is a modified ListView compoent. It provides an overview of the hierarchical structure of all blocks in the editor. The blocks are presented vertically one below the other. It enables editing of hierarchy and addition of elements in the block tree without selecting the block instance on the canvas. + +It is an experimental component which may end up completely merged into the ListView component via configuration props. diff --git a/packages/block-editor/src/components/off-canvas-editor/block-contents.js b/packages/block-editor/src/components/off-canvas-editor/block-contents.js new file mode 100644 index 00000000000000..507e7575344ab3 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/block-contents.js @@ -0,0 +1,89 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import ListViewBlockSelectButton from './block-select-button'; +import BlockDraggable from '../block-draggable'; +import { store as blockEditorStore } from '../../store'; + +const ListViewBlockContents = forwardRef( + ( + { + onClick, + onToggleExpanded, + block, + isSelected, + position, + siblingBlockCount, + level, + isExpanded, + selectedClientIds, + ...props + }, + ref + ) => { + const { clientId } = block; + + const { blockMovingClientId, selectedBlockInBlockEditor } = useSelect( + ( select ) => { + const { hasBlockMovingClientId, getSelectedBlockClientId } = + select( blockEditorStore ); + return { + blockMovingClientId: hasBlockMovingClientId(), + selectedBlockInBlockEditor: getSelectedBlockClientId(), + }; + }, + [ clientId ] + ); + + const isBlockMoveTarget = + blockMovingClientId && selectedBlockInBlockEditor === clientId; + + const className = classnames( 'block-editor-list-view-block-contents', { + 'is-dropping-before': isBlockMoveTarget, + } ); + + // Only include all selected blocks if the currently clicked on block + // is one of the selected blocks. This ensures that if a user attempts + // to drag a block that isn't part of the selection, they're still able + // to drag it and rearrange its position. + const draggableClientIds = selectedClientIds.includes( clientId ) + ? selectedClientIds + : [ clientId ]; + + return ( + + { ( { draggable, onDragStart, onDragEnd } ) => ( + + ) } + + ); + } +); + +export default ListViewBlockContents; diff --git a/packages/block-editor/src/components/off-canvas-editor/block-select-button.js b/packages/block-editor/src/components/off-canvas-editor/block-select-button.js new file mode 100644 index 00000000000000..9477eb2cda40c0 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/block-select-button.js @@ -0,0 +1,113 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + Button, + __experimentalHStack as HStack, + __experimentalTruncate as Truncate, +} from '@wordpress/components'; +import { forwardRef } from '@wordpress/element'; +import { Icon, lock } from '@wordpress/icons'; +import { SPACE, ENTER } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import BlockIcon from '../block-icon'; +import useBlockDisplayInformation from '../use-block-display-information'; +import useBlockDisplayTitle from '../block-title/use-block-display-title'; +import ListViewExpander from './expander'; +import { useBlockLock } from '../block-lock'; + +function ListViewBlockSelectButton( + { + className, + block: { clientId }, + onClick, + onToggleExpanded, + tabIndex, + onFocus, + onDragStart, + onDragEnd, + draggable, + }, + ref +) { + const blockInformation = useBlockDisplayInformation( clientId ); + const blockTitle = useBlockDisplayTitle( { + clientId, + context: 'list-view', + } ); + const { isLocked } = useBlockLock( clientId ); + + // The `href` attribute triggers the browser's native HTML drag operations. + // When the link is dragged, the element's outerHTML is set in DataTransfer object as text/html. + // We need to clear any HTML drag data to prevent `pasteHandler` from firing + // inside the `useOnBlockDrop` hook. + const onDragStartHandler = ( event ) => { + event.dataTransfer.clearData(); + onDragStart?.( event ); + }; + + function onKeyDownHandler( event ) { + if ( event.keyCode === ENTER || event.keyCode === SPACE ) { + onClick( event ); + } + } + + return ( + <> + + + ); +} + +export default forwardRef( ListViewBlockSelectButton ); diff --git a/packages/block-editor/src/components/off-canvas-editor/block.js b/packages/block-editor/src/components/off-canvas-editor/block.js new file mode 100644 index 00000000000000..0444630004d87f --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/block.js @@ -0,0 +1,333 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { hasBlockSupport } from '@wordpress/blocks'; +import { + __experimentalTreeGridCell as TreeGridCell, + __experimentalTreeGridItem as TreeGridItem, +} from '@wordpress/components'; +import { useInstanceId } from '@wordpress/compose'; +import { moreVertical } from '@wordpress/icons'; +import { + useState, + useRef, + useEffect, + useCallback, + memo, +} from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { sprintf, __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import ListViewLeaf from './leaf'; +import { + BlockMoverUpButton, + BlockMoverDownButton, +} from '../block-mover/button'; +import ListViewBlockContents from './block-contents'; +import BlockSettingsDropdown from '../block-settings-menu/block-settings-dropdown'; +import { useListViewContext } from './context'; +import { getBlockPositionDescription } from './utils'; +import { store as blockEditorStore } from '../../store'; +import useBlockDisplayInformation from '../use-block-display-information'; +import { useBlockLock } from '../block-lock'; + +function ListViewBlock( { + block, + isDragged, + isSelected, + isBranchSelected, + selectBlock, + position, + level, + rowCount, + siblingBlockCount, + showBlockMovers, + path, + isExpanded, + selectedClientIds, + preventAnnouncement, + selectBlockInCanvas, +} ) { + const cellRef = useRef( null ); + const [ isHovered, setIsHovered ] = useState( false ); + const { clientId } = block; + + const { isLocked, isContentLocked } = useBlockLock( clientId ); + const forceSelectionContentLock = useSelect( + ( select ) => { + if ( isSelected ) { + return false; + } + if ( ! isContentLocked ) { + return false; + } + return select( blockEditorStore ).hasSelectedInnerBlock( + clientId, + true + ); + }, + [ isContentLocked, clientId, isSelected ] + ); + + const isFirstSelectedBlock = + forceSelectionContentLock || + ( isSelected && selectedClientIds[ 0 ] === clientId ); + const isLastSelectedBlock = + forceSelectionContentLock || + ( isSelected && + selectedClientIds[ selectedClientIds.length - 1 ] === clientId ); + + const { toggleBlockHighlight } = useDispatch( blockEditorStore ); + + const blockInformation = useBlockDisplayInformation( clientId ); + const blockName = useSelect( + ( select ) => select( blockEditorStore ).getBlockName( clientId ), + [ clientId ] + ); + + // When a block hides its toolbar it also hides the block settings menu, + // since that menu is part of the toolbar in the editor canvas. + // List View respects this by also hiding the block settings menu. + const showBlockActions = hasBlockSupport( + blockName, + '__experimentalToolbar', + true + ); + const instanceId = useInstanceId( ListViewBlock ); + const descriptionId = `list-view-block-select-button__${ instanceId }`; + const blockPositionDescription = getBlockPositionDescription( + position, + siblingBlockCount, + level + ); + + let blockAriaLabel = __( 'Link' ); + if ( blockInformation ) { + blockAriaLabel = isLocked + ? sprintf( + // translators: %s: The title of the block. This string indicates a link to select the locked block. + __( '%s link (locked)' ), + blockInformation.title + ) + : sprintf( + // translators: %s: The title of the block. This string indicates a link to select the block. + __( '%s link' ), + blockInformation.title + ); + } + + const settingsAriaLabel = blockInformation + ? sprintf( + // translators: %s: The title of the block. + __( 'Options for %s block' ), + blockInformation.title + ) + : __( 'Options' ); + + const { isTreeGridMounted, expand, collapse } = useListViewContext(); + + const hasSiblings = siblingBlockCount > 0; + const hasRenderedMovers = showBlockMovers && hasSiblings; + const moverCellClassName = classnames( + 'block-editor-list-view-block__mover-cell', + { 'is-visible': isHovered || isSelected } + ); + + const listViewBlockSettingsClassName = classnames( + 'block-editor-list-view-block__menu-cell', + { 'is-visible': isHovered || isFirstSelectedBlock } + ); + + // If ListView has experimental features related to the Persistent List View, + // only focus the selected list item on mount; otherwise the list would always + // try to steal the focus from the editor canvas. + useEffect( () => { + if ( ! isTreeGridMounted && isSelected ) { + cellRef.current.focus(); + } + }, [] ); + + const onMouseEnter = useCallback( () => { + setIsHovered( true ); + toggleBlockHighlight( clientId, true ); + }, [ clientId, setIsHovered, toggleBlockHighlight ] ); + const onMouseLeave = useCallback( () => { + setIsHovered( false ); + toggleBlockHighlight( clientId, false ); + }, [ clientId, setIsHovered, toggleBlockHighlight ] ); + + const selectEditorBlock = useCallback( + ( event ) => { + selectBlock( event, clientId ); + event.preventDefault(); + }, + [ clientId, selectBlock ] + ); + + const updateSelection = useCallback( + ( newClientId ) => { + selectBlock( undefined, newClientId ); + }, + [ selectBlock ] + ); + + const toggleExpanded = useCallback( + ( event ) => { + // Prevent shift+click from opening link in a new window when toggling. + event.preventDefault(); + event.stopPropagation(); + if ( isExpanded === true ) { + collapse( clientId ); + } else if ( isExpanded === false ) { + expand( clientId ); + } + }, + [ clientId, expand, collapse, isExpanded ] + ); + + let colSpan; + if ( hasRenderedMovers ) { + colSpan = 2; + } else if ( ! showBlockActions ) { + colSpan = 3; + } + + const classes = classnames( { + 'is-selected': isSelected || forceSelectionContentLock, + 'is-first-selected': isFirstSelectedBlock, + 'is-last-selected': isLastSelectedBlock, + 'is-branch-selected': isBranchSelected, + 'is-dragging': isDragged, + 'has-single-cell': ! showBlockActions, + } ); + + // Only include all selected blocks if the currently clicked on block + // is one of the selected blocks. This ensures that if a user attempts + // to alter a block that isn't part of the selection, they're still able + // to do so. + const dropdownClientIds = selectedClientIds.includes( clientId ) + ? selectedClientIds + : [ clientId ]; + + return ( + + + { ( { ref, tabIndex, onFocus } ) => ( +
+ {} + } + onToggleExpanded={ toggleExpanded } + isSelected={ isSelected } + position={ position } + siblingBlockCount={ siblingBlockCount } + level={ level } + ref={ ref } + tabIndex={ tabIndex } + onFocus={ onFocus } + isExpanded={ isExpanded } + selectedClientIds={ selectedClientIds } + preventAnnouncement={ preventAnnouncement } + /> +
+ { blockPositionDescription } +
+
+ ) } +
+ { hasRenderedMovers && ( + <> + + + { ( { ref, tabIndex, onFocus } ) => ( + + ) } + + + { ( { ref, tabIndex, onFocus } ) => ( + + ) } + + + + ) } + + { showBlockActions && ( + + { ( { ref, tabIndex, onFocus } ) => ( + + ) } + + ) } +
+ ); +} + +export default memo( ListViewBlock ); diff --git a/packages/block-editor/src/components/off-canvas-editor/branch.js b/packages/block-editor/src/components/off-canvas-editor/branch.js new file mode 100644 index 00000000000000..c43a0e8737f898 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/branch.js @@ -0,0 +1,212 @@ +/** + * WordPress dependencies + */ +import { memo } from '@wordpress/element'; +import { AsyncModeProvider, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +/** + * Internal dependencies + */ +import ListViewBlock from './block'; +import { useListViewContext } from './context'; +import { isClientIdSelected } from './utils'; +import { store as blockEditorStore } from '../../store'; + +/** + * Given a block, returns the total number of blocks in that subtree. This is used to help determine + * the list position of a block. + * + * When a block is collapsed, we do not count their children as part of that total. In the current drag + * implementation dragged blocks and their children are not counted. + * + * @param {Object} block block tree + * @param {Object} expandedState state that notes which branches are collapsed + * @param {Array} draggedClientIds a list of dragged client ids + * @param {boolean} isExpandedByDefault flag to determine the default fallback expanded state. + * @return {number} block count + */ +function countBlocks( + block, + expandedState, + draggedClientIds, + isExpandedByDefault +) { + const isDragged = draggedClientIds?.includes( block.clientId ); + if ( isDragged ) { + return 0; + } + const isExpanded = expandedState[ block.clientId ] ?? isExpandedByDefault; + + if ( isExpanded ) { + return ( + 1 + + block.innerBlocks.reduce( + countReducer( + expandedState, + draggedClientIds, + isExpandedByDefault + ), + 0 + ) + ); + } + return 1; +} +const countReducer = + ( expandedState, draggedClientIds, isExpandedByDefault ) => + ( count, block ) => { + const isDragged = draggedClientIds?.includes( block.clientId ); + if ( isDragged ) { + return count; + } + const isExpanded = + expandedState[ block.clientId ] ?? isExpandedByDefault; + if ( isExpanded && block.innerBlocks.length > 0 ) { + return ( + count + + countBlocks( + block, + expandedState, + draggedClientIds, + isExpandedByDefault + ) + ); + } + return count + 1; + }; + +function ListViewBranch( props ) { + const { + blocks, + selectBlock, + showBlockMovers, + selectedClientIds, + level = 1, + path = '', + isBranchSelected = false, + listPosition = 0, + fixedListWindow, + isExpanded, + parentId, + shouldShowInnerBlocks = true, + selectBlockInCanvas, + } = props; + + const isContentLocked = useSelect( + ( select ) => { + return !! ( + parentId && + select( blockEditorStore ).getTemplateLock( parentId ) === + 'contentOnly' + ); + }, + [ parentId ] + ); + + const { expandedState, draggedClientIds } = useListViewContext(); + + if ( isContentLocked ) { + return null; + } + + const filteredBlocks = blocks.filter( Boolean ); + const blockCount = filteredBlocks.length; + let nextPosition = listPosition; + + return ( + <> + { filteredBlocks.map( ( block, index ) => { + const { clientId, innerBlocks } = block; + + if ( index > 0 ) { + nextPosition += countBlocks( + filteredBlocks[ index - 1 ], + expandedState, + draggedClientIds, + isExpanded + ); + } + + const { itemInView } = fixedListWindow; + const blockInView = itemInView( nextPosition ); + + const position = index + 1; + const updatedPath = + path.length > 0 + ? `${ path }_${ position }` + : `${ position }`; + const hasNestedBlocks = !! innerBlocks?.length; + + const shouldExpand = + hasNestedBlocks && shouldShowInnerBlocks + ? expandedState[ clientId ] ?? isExpanded + : undefined; + + const isDragged = !! draggedClientIds?.includes( clientId ); + + const showBlock = isDragged || blockInView; + + // Make updates to the selected or dragged blocks synchronous, + // but asynchronous for any other block. + const isSelected = isClientIdSelected( + clientId, + selectedClientIds + ); + const isSelectedBranch = + isBranchSelected || ( isSelected && hasNestedBlocks ); + return ( + + { showBlock && ( + + ) } + { ! showBlock && ( + + + + ) } + { hasNestedBlocks && shouldExpand && ! isDragged && ( + + ) } + + ); + } ) } + + ); +} + +ListViewBranch.defaultProps = { + selectBlock: () => {}, +}; + +export default memo( ListViewBranch ); diff --git a/packages/block-editor/src/components/off-canvas-editor/context.js b/packages/block-editor/src/components/off-canvas-editor/context.js new file mode 100644 index 00000000000000..c837dce9ca23fd --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/context.js @@ -0,0 +1,8 @@ +/** + * WordPress dependencies + */ +import { createContext, useContext } from '@wordpress/element'; + +export const ListViewContext = createContext( {} ); + +export const useListViewContext = () => useContext( ListViewContext ); diff --git a/packages/block-editor/src/components/off-canvas-editor/drop-indicator.js b/packages/block-editor/src/components/off-canvas-editor/drop-indicator.js new file mode 100644 index 00000000000000..1500e2f887fada --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/drop-indicator.js @@ -0,0 +1,125 @@ +/** + * WordPress dependencies + */ +import { Popover } from '@wordpress/components'; +import { useCallback, useMemo } from '@wordpress/element'; + +export default function ListViewDropIndicator( { + listViewRef, + blockDropTarget, +} ) { + const { rootClientId, clientId, dropPosition } = blockDropTarget || {}; + + const [ rootBlockElement, blockElement ] = useMemo( () => { + if ( ! listViewRef.current ) { + return []; + } + + // The rootClientId will be defined whenever dropping into inner + // block lists, but is undefined when dropping at the root level. + const _rootBlockElement = rootClientId + ? listViewRef.current.querySelector( + `[data-block="${ rootClientId }"]` + ) + : undefined; + + // The clientId represents the sibling block, the dragged block will + // usually be inserted adjacent to it. It will be undefined when + // dropping a block into an empty block list. + const _blockElement = clientId + ? listViewRef.current.querySelector( + `[data-block="${ clientId }"]` + ) + : undefined; + + return [ _rootBlockElement, _blockElement ]; + }, [ rootClientId, clientId ] ); + + // The targetElement is the element that the drop indicator will appear + // before or after. When dropping into an empty block list, blockElement + // is undefined, so the indicator will appear after the rootBlockElement. + const targetElement = blockElement || rootBlockElement; + + const getDropIndicatorIndent = useCallback( () => { + if ( ! rootBlockElement ) { + return 0; + } + + // Calculate the indent using the block icon of the root block. + // Using a classname selector here might be flaky and could be + // improved. + const targetElementRect = targetElement.getBoundingClientRect(); + const rootBlockIconElement = rootBlockElement.querySelector( + '.block-editor-block-icon' + ); + const rootBlockIconRect = rootBlockIconElement.getBoundingClientRect(); + return rootBlockIconRect.right - targetElementRect.left; + }, [ rootBlockElement, targetElement ] ); + + const style = useMemo( () => { + if ( ! targetElement ) { + return {}; + } + + const indent = getDropIndicatorIndent(); + + return { + width: targetElement.offsetWidth - indent, + }; + }, [ getDropIndicatorIndent, targetElement ] ); + + const popoverAnchor = useMemo( () => { + const isValidDropPosition = + dropPosition === 'top' || + dropPosition === 'bottom' || + dropPosition === 'inside'; + if ( ! targetElement || ! isValidDropPosition ) { + return undefined; + } + + return { + ownerDocument: targetElement.ownerDocument, + getBoundingClientRect() { + const rect = targetElement.getBoundingClientRect(); + const indent = getDropIndicatorIndent(); + + const left = rect.left + indent; + const right = rect.right; + let top = 0; + let bottom = 0; + + if ( dropPosition === 'top' ) { + top = rect.top; + bottom = rect.top; + } else { + // `dropPosition` is either `bottom` or `inside` + top = rect.bottom; + bottom = rect.bottom; + } + + const width = right - left; + const height = bottom - top; + + return new window.DOMRect( left, top, width, height ); + }, + }; + }, [ targetElement, dropPosition, getDropIndicatorIndent ] ); + + if ( ! targetElement ) { + return null; + } + + return ( + +
+ + ); +} diff --git a/packages/block-editor/src/components/off-canvas-editor/expander.js b/packages/block-editor/src/components/off-canvas-editor/expander.js new file mode 100644 index 00000000000000..3b93f8ad01185c --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/expander.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { chevronRightSmall, chevronLeftSmall, Icon } from '@wordpress/icons'; +import { isRTL } from '@wordpress/i18n'; + +export default function ListViewExpander( { onClick } ) { + return ( + // Keyboard events are handled by TreeGrid see: components/src/tree-grid/index.js + // + // The expander component is implemented as a pseudo element in the w3 example + // https://www.w3.org/TR/wai-aria-practices/examples/treegrid/treegrid-1.html + // + // We've mimicked this by adding an icon with aria-hidden set to true to hide this from the accessibility tree. + // For the current tree grid implementation, please do not try to make this a button. + // + // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions + onClick( event, { forceToggle: true } ) } + aria-hidden="true" + > + + + ); +} diff --git a/packages/block-editor/src/components/off-canvas-editor/index.js b/packages/block-editor/src/components/off-canvas-editor/index.js new file mode 100644 index 00000000000000..dd88b9ec43cc94 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/index.js @@ -0,0 +1,216 @@ +/** + * WordPress dependencies + */ +import { + useMergeRefs, + __experimentalUseFixedWindowList as useFixedWindowList, +} from '@wordpress/compose'; +import { __experimentalTreeGrid as TreeGrid } from '@wordpress/components'; +import { AsyncModeProvider, useSelect } from '@wordpress/data'; +import { + useCallback, + useEffect, + useMemo, + useRef, + useReducer, + forwardRef, +} from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import ListViewBranch from './branch'; +import { ListViewContext } from './context'; +import ListViewDropIndicator from './drop-indicator'; +import useBlockSelection from './use-block-selection'; +import useListViewClientIds from './use-list-view-client-ids'; +import useListViewDropZone from './use-list-view-drop-zone'; +import useListViewExpandSelectedItem from './use-list-view-expand-selected-item'; +import { store as blockEditorStore } from '../../store'; + +const expanded = ( state, action ) => { + if ( Array.isArray( action.clientIds ) ) { + return { + ...state, + ...action.clientIds.reduce( + ( newState, id ) => ( { + ...newState, + [ id ]: action.type === 'expand', + } ), + {} + ), + }; + } + return state; +}; + +export const BLOCK_LIST_ITEM_HEIGHT = 36; + +/** + * Show a hierarchical list of blocks. + * + * @param {Object} props Components props. + * @param {string} props.id An HTML element id for the root element of ListView. + * @param {Array} props.blocks Custom subset of block client IDs to be used instead of the default hierarchy. + * @param {boolean} props.showBlockMovers Flag to enable block movers + * @param {boolean} props.isExpanded Flag to determine whether nested levels are expanded by default. + * @param {boolean} props.selectBlockInCanvas Flag to determine whether the list view should be a block selection mechanism,. + * @param {Object} ref Forwarded ref + */ +function __ExperimentalOffCanvasEditor( + { + id, + blocks, + showBlockMovers = false, + isExpanded = false, + selectBlockInCanvas = true, + }, + ref +) { + const { clientIdsTree, draggedClientIds, selectedClientIds } = + useListViewClientIds( blocks ); + + const { visibleBlockCount, shouldShowInnerBlocks } = useSelect( + ( select ) => { + const { + getGlobalBlockCount, + getClientIdsOfDescendants, + __unstableGetEditorMode, + } = select( blockEditorStore ); + const draggedBlockCount = + draggedClientIds?.length > 0 + ? getClientIdsOfDescendants( draggedClientIds ).length + 1 + : 0; + return { + visibleBlockCount: getGlobalBlockCount() - draggedBlockCount, + shouldShowInnerBlocks: __unstableGetEditorMode() !== 'zoom-out', + }; + }, + [ draggedClientIds ] + ); + + const { updateBlockSelection } = useBlockSelection(); + + const [ expandedState, setExpandedState ] = useReducer( expanded, {} ); + + const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone(); + const elementRef = useRef(); + const treeGridRef = useMergeRefs( [ elementRef, dropZoneRef, ref ] ); + + const isMounted = useRef( false ); + const { setSelectedTreeId } = useListViewExpandSelectedItem( { + firstSelectedBlockClientId: selectedClientIds[ 0 ], + setExpandedState, + } ); + const selectEditorBlock = useCallback( + ( event, clientId ) => { + updateBlockSelection( event, clientId ); + setSelectedTreeId( clientId ); + }, + [ setSelectedTreeId, updateBlockSelection ] + ); + useEffect( () => { + isMounted.current = true; + }, [] ); + + // List View renders a fixed number of items and relies on each having a fixed item height of 36px. + // If this value changes, we should also change the itemHeight value set in useFixedWindowList. + // See: https://github.com/WordPress/gutenberg/pull/35230 for additional context. + const [ fixedListWindow ] = useFixedWindowList( + elementRef, + BLOCK_LIST_ITEM_HEIGHT, + visibleBlockCount, + { + useWindowing: true, + windowOverscan: 40, + } + ); + + const expand = useCallback( + ( clientId ) => { + if ( ! clientId ) { + return; + } + setExpandedState( { type: 'expand', clientIds: [ clientId ] } ); + }, + [ setExpandedState ] + ); + const collapse = useCallback( + ( clientId ) => { + if ( ! clientId ) { + return; + } + setExpandedState( { type: 'collapse', clientIds: [ clientId ] } ); + }, + [ setExpandedState ] + ); + const expandRow = useCallback( + ( row ) => { + expand( row?.dataset?.block ); + }, + [ expand ] + ); + const collapseRow = useCallback( + ( row ) => { + collapse( row?.dataset?.block ); + }, + [ collapse ] + ); + const focusRow = useCallback( + ( event, startRow, endRow ) => { + if ( event.shiftKey ) { + updateBlockSelection( + event, + startRow?.dataset?.block, + endRow?.dataset?.block + ); + } + }, + [ updateBlockSelection ] + ); + + const contextValue = useMemo( + () => ( { + isTreeGridMounted: isMounted.current, + draggedClientIds, + expandedState, + expand, + collapse, + } ), + [ isMounted.current, draggedClientIds, expandedState, expand, collapse ] + ); + + return ( + + + + + + + + + ); +} +export default forwardRef( __ExperimentalOffCanvasEditor ); diff --git a/packages/block-editor/src/components/off-canvas-editor/leaf.js b/packages/block-editor/src/components/off-canvas-editor/leaf.js new file mode 100644 index 00000000000000..41bf4bc34cc665 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/leaf.js @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { animated } from '@react-spring/web'; +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __experimentalTreeGridRow as TreeGridRow } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import useMovingAnimation from '../use-moving-animation'; + +const AnimatedTreeGridRow = animated( TreeGridRow ); + +export default function ListViewLeaf( { + isSelected, + position, + level, + rowCount, + children, + className, + path, + ...props +} ) { + const ref = useMovingAnimation( { + isSelected, + adjustScrolling: false, + enableAnimation: true, + triggerAnimationOnChange: path, + } ); + + return ( + + { children } + + ); +} diff --git a/packages/block-editor/src/components/off-canvas-editor/style.scss b/packages/block-editor/src/components/off-canvas-editor/style.scss new file mode 100644 index 00000000000000..ce5539dbe3aa7f --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/style.scss @@ -0,0 +1,432 @@ +.block-editor-list-view-tree { + width: 100%; + border-collapse: collapse; + padding: 0; + margin: 0; + + // Move upwards when in modal. + .components-modal__content & { + margin: (-$grid-unit-15) (-$grid-unit-15 * 0.5) 0; + width: calc(100% + #{ $grid-unit-15 }); + } +} + +.block-editor-list-view-leaf { + // Use position relative for row animation. + position: relative; + + // The background has to be applied to the td, not tr, or border-radius won't work. + &.is-selected td { + background: var(--wp-admin-theme-color); + } + &.is-selected .block-editor-list-view-block-contents, + &.is-selected .components-button.has-icon { + color: $white; + } + &.is-selected .block-editor-list-view-block-contents { + // Hide selection styles while a user is dragging blocks/files etc. + .is-dragging-components-draggable & { + background: none; + color: $gray-900; + } + } + &.is-selected .block-editor-list-view-block-contents:focus { + &::after { + box-shadow: + inset 0 0 0 1px $white, + 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } + } + &.is-selected .block-editor-list-view-block__menu:focus { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) $white; + } + + &.is-dragging { + display: none; + } + + // Border radius for corners of the selected item. + &.is-first-selected td:first-child { + border-top-left-radius: $radius-block-ui; + } + &.is-first-selected td:last-child { + border-top-right-radius: $radius-block-ui; + } + &.is-last-selected td:first-child { + border-bottom-left-radius: $radius-block-ui; + } + &.is-last-selected td:last-child { + border-bottom-right-radius: $radius-block-ui; + } + &.is-branch-selected:not(.is-selected) { + // Lighten a CSS variable without introducing a new SASS variable + background: + linear-gradient(transparentize($white, 0.1), transparentize($white, 0.1)), + linear-gradient(var(--wp-admin-theme-color), var(--wp-admin-theme-color)); + } + &.is-branch-selected.is-first-selected td:first-child { + border-top-left-radius: $radius-block-ui; + } + &.is-branch-selected.is-first-selected td:last-child { + border-top-right-radius: $radius-block-ui; + } + &[aria-expanded="false"] { + &.is-branch-selected.is-first-selected td:first-child { + border-top-left-radius: $radius-block-ui; + } + &.is-branch-selected.is-first-selected td:last-child { + border-top-right-radius: $radius-block-ui; + } + &.is-branch-selected.is-last-selected td:first-child { + border-bottom-left-radius: $radius-block-ui; + } + &.is-branch-selected.is-last-selected td:last-child { + border-bottom-right-radius: $radius-block-ui; + } + } + &.is-branch-selected:not(.is-selected) td { + border-radius: 0; + } + + + // List View renders a fixed number of items and relies on each item having a fixed height of 36px. + // If this value changes, we should also change the itemHeight value set in useFixedWindowList. + // See: https://github.com/WordPress/gutenberg/pull/35230 for additional context. + .block-editor-list-view-block-contents { + display: flex; + align-items: center; + width: 100%; + height: auto; + padding: ($grid-unit-15 * 0.5) $grid-unit-05 ($grid-unit-15 * 0.5) 0; + text-align: left; + color: $gray-900; + border-radius: $radius-block-ui; + position: relative; + white-space: nowrap; + + &.is-dropping-before::before { + content: ""; + position: absolute; + pointer-events: none; + transition: border-color 0.1s linear, border-style 0.1s linear, box-shadow 0.1s linear; + top: -2px; + right: 0; + left: 0; + border-top: 4px solid var(--wp-admin-theme-color); + } + + .components-modal__content & { + padding-left: 0; + padding-right: 0; + } + } + + .block-editor-list-view-block-contents:focus { + box-shadow: none; + + &::after { + content: ""; + position: absolute; + top: 0; + right: -(24px + 5px); // Icon size + padding. + bottom: 0; + left: 0; + border-radius: inherit; + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + z-index: 2; + pointer-events: none; + + // Hide focus styles while a user is dragging blocks/files etc. + .is-dragging-components-draggable & { + box-shadow: none; + } + } + } + // 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; + + // Hide focus styles while a user is dragging blocks/files etc. + .is-dragging-components-draggable & { + box-shadow: none; + } + } + + &.is-visible .block-editor-list-view-block-contents { + opacity: 1; + @include edit-post__fade-in-animation; + } + + .block-editor-block-icon { + align-self: flex-start; + margin-right: $grid-unit-10; + width: $icon-size; + } + + .block-editor-list-view-block__menu-cell, + .block-editor-list-view-block__mover-cell, + .block-editor-list-view-block__contents-cell { + padding-top: 0; + padding-bottom: 0; + } + + .block-editor-list-view-block__menu-cell, + .block-editor-list-view-block__mover-cell { + line-height: 0; + width: $button-size; + vertical-align: middle; + @include reduce-motion("transition"); + + > * { + opacity: 0; + } + + // Show on hover, visible, and show above to keep the hit area size. + &:hover, + &.is-visible { + position: relative; + z-index: 1; + + > * { + opacity: 1; + @include edit-post__fade-in-animation; + } + } + + &, + .components-button.has-icon { + width: 24px; + min-width: 24px; + padding: 0; + } + } + + .block-editor-list-view-block__menu-cell { + padding-right: $grid-unit-05; + + .components-button.has-icon { + height: 24px; + } + } + + .block-editor-list-view-block__mover-cell-alignment-wrapper { + display: flex; + height: 100%; + flex-direction: column; + align-items: center; + } + + // Keep the tap target large but the focus target small. + .block-editor-block-mover-button { + position: relative; + width: $button-size; + height: $button-size-small; + + // Position the icon. + svg { + position: relative; + height: $button-size-small; + } + + &.is-up-button { + margin-top: -$grid-unit-15 * 0.5; + align-items: flex-end; + svg { + bottom: -$grid-unit-05; + } + } + + &.is-down-button { + margin-bottom: -$grid-unit-15 * 0.5; + align-items: flex-start; + svg { + top: -$grid-unit-05; + } + } + + // Don't show the focus inherited by the Button component. + &:focus:enabled { + box-shadow: none; + outline: none; + } + + // Focus style. + &:focus { + box-shadow: none; + outline: none; + } + + &:focus::before { + @include block-toolbar-button-style__focus(); + } + + // Focus and toggle pseudo elements. + &::before { + content: ""; + position: absolute; + display: block; + border-radius: $radius-block-ui; + height: 16px; + min-width: 100%; + + // Position the focus rectangle. + left: 0; + right: 0; + + // Animate in. + animation: components-button__appear-animation 0.1s ease; + animation-fill-mode: forwards; + @include reduce-motion("animation"); + } + } + + .block-editor-inserter__toggle { + background: $gray-900; + color: $white; + height: $grid-unit-30; + margin: 6px 6px 6px 1px; + min-width: $grid-unit-30; + + &:active { + color: $white; + } + } + + .block-editor-list-view-block-select-button__label-wrapper { + min-width: 120px; + } + + .block-editor-list-view-block-select-button__title { + flex: 1; + position: relative; + + .components-truncate { + position: absolute; + width: 100%; + transform: translateY(-50%); + } + } + + .block-editor-list-view-block-select-button__anchor-wrapper { + position: relative; + max-width: min(110px, 40%); + width: 100%; + } + + .block-editor-list-view-block-select-button__anchor { + position: absolute; + right: 0; + transform: translateY(-50%); + background: rgba($black, 0.1); + border-radius: $radius-block-ui; + padding: 2px 6px; + max-width: 100%; + box-sizing: border-box; + } + + &.is-selected .block-editor-list-view-block-select-button__anchor { + background: rgba($black, 0.3); + } + + .block-editor-list-view-block-select-button__lock { + line-height: 0; + width: 24px; + min-width: 24px; + margin-left: auto; + padding: 0; + vertical-align: middle; + } +} + +.block-editor-list-view-block-select-button__description, +.block-editor-list-view-appender__description { + display: none; +} + +.block-editor-list-view-block__contents-cell, +.block-editor-list-view-appender__cell { + .block-editor-list-view-block__contents-container, + .block-editor-list-view-appender__container { + display: flex; + } +} + +// Chevron container metrics. +.block-editor-list-view__expander { + height: $icon-size; + margin-left: $grid-unit-05; + width: $icon-size; +} + +// First level of indentation is aria-level 2, max indent is 8. +// Indent is a full icon size, plus 4px which optically aligns child icons to the text label above. +$block-navigation-max-indent: 8; +.block-editor-list-view-leaf[aria-level] .block-editor-list-view__expander { + margin-left: ( $icon-size ) * $block-navigation-max-indent + 4 * ( $block-navigation-max-indent - 1 ); +} + +.block-editor-list-view-leaf:not([aria-level="1"]) { + .block-editor-list-view__expander { + margin-right: 4px; + } +} + +@for $i from 0 to $block-navigation-max-indent { + .block-editor-list-view-leaf[aria-level="#{ $i + 1 }"] .block-editor-list-view__expander { + @if $i - 1 >= 0 { + margin-left: ( $icon-size * $i ) + 4 * ($i - 1); + } + @else { + margin-left: ( $icon-size * $i ); + } + } +} + +.block-editor-list-view-leaf .block-editor-list-view__expander { + visibility: hidden; +} + +// Point downwards when open. +.block-editor-list-view-leaf[aria-expanded="true"] .block-editor-list-view__expander svg { + visibility: visible; + transition: transform 0.2s ease; + transform: rotate(90deg); + @include reduce-motion("transition"); +} + +// Point rightwards when closed +.block-editor-list-view-leaf[aria-expanded="false"] .block-editor-list-view__expander svg { + visibility: visible; + transform: rotate(0deg); + transition: transform 0.2s ease; + @include reduce-motion("transition"); +} + +.block-editor-list-view-drop-indicator { + pointer-events: none; + + .block-editor-list-view-drop-indicator__line { + background: var(--wp-admin-theme-color); + height: $border-width; + } +} + +// Reset some popover defaults for the drop indicator. +.block-editor-list-view-drop-indicator > .components-popover__content { + margin-left: 0; + border: none; + box-shadow: none; + outline: none; +} + +.block-editor-list-view-placeholder { + padding: 0; + margin: 0; + height: 36px; +} + diff --git a/packages/block-editor/src/components/off-canvas-editor/test/utils.js b/packages/block-editor/src/components/off-canvas-editor/test/utils.js new file mode 100644 index 00000000000000..78d78a9d90069c --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/test/utils.js @@ -0,0 +1,50 @@ +/** + * Internal dependencies + */ +import { getCommonDepthClientIds } from '../utils'; + +describe( 'getCommonDepthClientIds', () => { + it( 'should return start and end when no depth is provided', () => { + const result = getCommonDepthClientIds( + 'start-id', + 'clicked-id', + [], + [] + ); + + expect( result ).toEqual( { start: 'start-id', end: 'clicked-id' } ); + } ); + + it( 'should return deepest start and end when depths match', () => { + const result = getCommonDepthClientIds( + 'start-id', + 'clicked-id', + [ 'start-1', 'start-2', 'start-3' ], + [ 'end-1', 'end-2', 'end-3' ] + ); + + expect( result ).toEqual( { start: 'start-id', end: 'clicked-id' } ); + } ); + + it( 'should return shallower ids when start is shallower', () => { + const result = getCommonDepthClientIds( + 'start-id', + 'clicked-id', + [ 'start-1' ], + [ 'end-1', 'end-2', 'end-3' ] + ); + + expect( result ).toEqual( { start: 'start-id', end: 'end-2' } ); + } ); + + it( 'should return shallower ids when end is shallower', () => { + const result = getCommonDepthClientIds( + 'start-id', + 'clicked-id', + [ 'start-1', 'start-2', 'start-3' ], + [ 'end-1', 'end-2' ] + ); + + expect( result ).toEqual( { start: 'start-3', end: 'clicked-id' } ); + } ); +} ); diff --git a/packages/block-editor/src/components/off-canvas-editor/use-block-selection.js b/packages/block-editor/src/components/off-canvas-editor/use-block-selection.js new file mode 100644 index 00000000000000..59aaaeacb01d40 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/use-block-selection.js @@ -0,0 +1,169 @@ +/** + * WordPress dependencies + */ +import { speak } from '@wordpress/a11y'; +import { __, sprintf } from '@wordpress/i18n'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useCallback } from '@wordpress/element'; +import { UP, DOWN, HOME, END } from '@wordpress/keycodes'; +import { store as blocksStore } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import { getCommonDepthClientIds } from './utils'; + +export default function useBlockSelection() { + const { clearSelectedBlock, multiSelect, selectBlock } = + useDispatch( blockEditorStore ); + const { + getBlockName, + getBlockParents, + getBlockSelectionStart, + getBlockSelectionEnd, + getSelectedBlockClientIds, + hasMultiSelection, + hasSelectedBlock, + } = useSelect( blockEditorStore ); + + const { getBlockType } = useSelect( blocksStore ); + + const updateBlockSelection = useCallback( + async ( event, clientId, destinationClientId ) => { + if ( ! event?.shiftKey ) { + selectBlock( clientId ); + return; + } + + // To handle multiple block selection via the `SHIFT` key, prevent + // the browser default behavior of opening the link in a new window. + event.preventDefault(); + + const isKeyPress = + event.type === 'keydown' && + ( event.keyCode === UP || + event.keyCode === DOWN || + event.keyCode === HOME || + event.keyCode === END ); + + // Handle clicking on a block when no blocks are selected, and return early. + if ( + ! isKeyPress && + ! hasSelectedBlock() && + ! hasMultiSelection() + ) { + selectBlock( clientId, null ); + return; + } + + const selectedBlocks = getSelectedBlockClientIds(); + const clientIdWithParents = [ + ...getBlockParents( clientId ), + clientId, + ]; + + if ( + isKeyPress && + ! selectedBlocks.some( ( blockId ) => + clientIdWithParents.includes( blockId ) + ) + ) { + // Ensure that shift-selecting blocks via the keyboard only + // expands the current selection if focusing over already + // selected blocks. Otherwise, clear the selection so that + // a user can create a new selection entirely by keyboard. + await clearSelectedBlock(); + } + + let startTarget = getBlockSelectionStart(); + let endTarget = clientId; + + // Handle keyboard behavior for selecting multiple blocks. + if ( isKeyPress ) { + if ( ! hasSelectedBlock() && ! hasMultiSelection() ) { + // Set the starting point of the selection to the currently + // focused block, if there are no blocks currently selected. + // This ensures that as the selection is expanded or contracted, + // the starting point of the selection is anchored to that block. + startTarget = clientId; + } + if ( destinationClientId ) { + // If the user presses UP or DOWN, we want to ensure that the block they're + // moving to is the target for selection, and not the currently focused one. + endTarget = destinationClientId; + } + } + + const startParents = getBlockParents( startTarget ); + const endParents = getBlockParents( endTarget ); + + const { start, end } = getCommonDepthClientIds( + startTarget, + endTarget, + startParents, + endParents + ); + await multiSelect( start, end, null ); + + // Announce deselected block, or number of deselected blocks if + // the total number of blocks deselected is greater than one. + const updatedSelectedBlocks = getSelectedBlockClientIds(); + + // If the selection is greater than 1 and the Home or End keys + // were used to generate the selection, then skip announcing the + // deselected blocks. + if ( + ( event.keyCode === HOME || event.keyCode === END ) && + updatedSelectedBlocks.length > 1 + ) { + return; + } + + const selectionDiff = selectedBlocks.filter( + ( blockId ) => ! updatedSelectedBlocks.includes( blockId ) + ); + + let label; + if ( selectionDiff.length === 1 ) { + const title = getBlockType( + getBlockName( selectionDiff[ 0 ] ) + )?.title; + if ( title ) { + label = sprintf( + /* translators: %s: block name */ + __( '%s deselected.' ), + title + ); + } + } else if ( selectionDiff.length > 1 ) { + label = sprintf( + /* translators: %s: number of deselected blocks */ + __( '%s blocks deselected.' ), + selectionDiff.length + ); + } + + if ( label ) { + speak( label ); + } + }, + [ + clearSelectedBlock, + getBlockName, + getBlockType, + getBlockParents, + getBlockSelectionStart, + getBlockSelectionEnd, + getSelectedBlockClientIds, + hasMultiSelection, + hasSelectedBlock, + multiSelect, + selectBlock, + ] + ); + + return { + updateBlockSelection, + }; +} diff --git a/packages/block-editor/src/components/off-canvas-editor/use-list-view-client-ids.js b/packages/block-editor/src/components/off-canvas-editor/use-list-view-client-ids.js new file mode 100644 index 00000000000000..5dafa765f16ea5 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/use-list-view-client-ids.js @@ -0,0 +1,29 @@ +/** + * WordPress dependencies + */ + +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +export default function useListViewClientIds( blocks ) { + return useSelect( + ( select ) => { + const { + getDraggedBlockClientIds, + getSelectedBlockClientIds, + __unstableGetClientIdsTree, + } = select( blockEditorStore ); + + return { + selectedClientIds: getSelectedBlockClientIds(), + draggedClientIds: getDraggedBlockClientIds(), + clientIdsTree: blocks ? blocks : __unstableGetClientIdsTree(), + }; + }, + [ blocks ] + ); +} diff --git a/packages/block-editor/src/components/off-canvas-editor/use-list-view-drop-zone.js b/packages/block-editor/src/components/off-canvas-editor/use-list-view-drop-zone.js new file mode 100644 index 00000000000000..346631667c2544 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/use-list-view-drop-zone.js @@ -0,0 +1,260 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useState, useCallback } from '@wordpress/element'; +import { + useThrottle, + __experimentalUseDropZone as useDropZone, +} from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { + getDistanceToNearestEdge, + isPointContainedByRect, +} from '../../utils/math'; +import useOnBlockDrop from '../use-on-block-drop'; +import { store as blockEditorStore } from '../../store'; + +/** @typedef {import('../../utils/math').WPPoint} WPPoint */ + +/** + * The type of a drag event. + * + * @typedef {'default'|'file'|'html'} WPDragEventType + */ + +/** + * An array representing data for blocks in the DOM used by drag and drop. + * + * @typedef {Object} WPListViewDropZoneBlocks + * @property {string} clientId The client id for the block. + * @property {string} rootClientId The root client id for the block. + * @property {number} blockIndex The block's index. + * @property {Element} element The DOM element representing the block. + * @property {number} innerBlockCount The number of inner blocks the block has. + * @property {boolean} isDraggedBlock Whether the block is currently being dragged. + * @property {boolean} canInsertDraggedBlocksAsSibling Whether the dragged block can be a sibling of this block. + * @property {boolean} canInsertDraggedBlocksAsChild Whether the dragged block can be a child of this block. + */ + +/** + * An object containing details of a drop target. + * + * @typedef {Object} WPListViewDropZoneTarget + * @property {string} blockIndex The insertion index. + * @property {string} rootClientId The root client id for the block. + * @property {string|undefined} clientId The client id for the block. + * @property {'top'|'bottom'|'inside'} dropPosition The position relative to the block that the user is dropping to. + * 'inside' refers to nesting as an inner block. + */ + +/** + * Determines whether the user positioning the dragged block to nest as an + * inner block. + * + * Presently this is determined by whether the cursor is on the right hand side + * of the block. + * + * @param {WPPoint} point The point representing the cursor position when dragging. + * @param {DOMRect} rect The rectangle. + */ +function isNestingGesture( point, rect ) { + const blockCenterX = rect.left + rect.width / 2; + return point.x > blockCenterX; +} + +// Block navigation is always a vertical list, so only allow dropping +// to the above or below a block. +const ALLOWED_DROP_EDGES = [ 'top', 'bottom' ]; + +/** + * Given blocks data and the cursor position, compute the drop target. + * + * @param {WPListViewDropZoneBlocks} blocksData Data about the blocks in list view. + * @param {WPPoint} position The point representing the cursor position when dragging. + * + * @return {WPListViewDropZoneTarget} An object containing data about the drop target. + */ +function getListViewDropTarget( blocksData, position ) { + let candidateEdge; + let candidateBlockData; + let candidateDistance; + let candidateRect; + + for ( const blockData of blocksData ) { + if ( blockData.isDraggedBlock ) { + continue; + } + + const rect = blockData.element.getBoundingClientRect(); + const [ distance, edge ] = getDistanceToNearestEdge( + position, + rect, + ALLOWED_DROP_EDGES + ); + + const isCursorWithinBlock = isPointContainedByRect( position, rect ); + if ( + candidateDistance === undefined || + distance < candidateDistance || + isCursorWithinBlock + ) { + candidateDistance = distance; + + const index = blocksData.indexOf( blockData ); + const previousBlockData = blocksData[ index - 1 ]; + + // If dragging near the top of a block and the preceding block + // is at the same level, use the preceding block as the candidate + // instead, as later it makes determining a nesting drop easier. + if ( + edge === 'top' && + previousBlockData && + previousBlockData.rootClientId === blockData.rootClientId && + ! previousBlockData.isDraggedBlock + ) { + candidateBlockData = previousBlockData; + candidateEdge = 'bottom'; + candidateRect = + previousBlockData.element.getBoundingClientRect(); + } else { + candidateBlockData = blockData; + candidateEdge = edge; + candidateRect = rect; + } + + // If the mouse position is within the block, break early + // as the user would intend to drop either before or after + // this block. + // + // This solves an issue where some rows in the list view + // tree overlap slightly due to sub-pixel rendering. + if ( isCursorWithinBlock ) { + break; + } + } + } + + if ( ! candidateBlockData ) { + return; + } + + const isDraggingBelow = candidateEdge === 'bottom'; + + // If the user is dragging towards the bottom of the block check whether + // they might be trying to nest the block as a child. + // If the block already has inner blocks, this should always be treated + // as nesting since the next block in the tree will be the first child. + if ( + isDraggingBelow && + candidateBlockData.canInsertDraggedBlocksAsChild && + ( candidateBlockData.innerBlockCount > 0 || + isNestingGesture( position, candidateRect ) ) + ) { + return { + rootClientId: candidateBlockData.clientId, + blockIndex: 0, + dropPosition: 'inside', + }; + } + + // If dropping as a sibling, but block cannot be inserted in + // this context, return early. + if ( ! candidateBlockData.canInsertDraggedBlocksAsSibling ) { + return; + } + + const offset = isDraggingBelow ? 1 : 0; + return { + rootClientId: candidateBlockData.rootClientId, + clientId: candidateBlockData.clientId, + blockIndex: candidateBlockData.blockIndex + offset, + dropPosition: candidateEdge, + }; +} + +/** + * A react hook for implementing a drop zone in list view. + * + * @return {WPListViewDropZoneTarget} The drop target. + */ +export default function useListViewDropZone() { + const { + getBlockRootClientId, + getBlockIndex, + getBlockCount, + getDraggedBlockClientIds, + canInsertBlocks, + } = useSelect( blockEditorStore ); + const [ target, setTarget ] = useState(); + const { rootClientId: targetRootClientId, blockIndex: targetBlockIndex } = + target || {}; + + const onBlockDrop = useOnBlockDrop( targetRootClientId, targetBlockIndex ); + + const draggedBlockClientIds = getDraggedBlockClientIds(); + const throttled = useThrottle( + useCallback( + ( event, currentTarget ) => { + const position = { x: event.clientX, y: event.clientY }; + const isBlockDrag = !! draggedBlockClientIds?.length; + + const blockElements = Array.from( + currentTarget.querySelectorAll( '[data-block]' ) + ); + + const blocksData = blockElements.map( ( blockElement ) => { + const clientId = blockElement.dataset.block; + const rootClientId = getBlockRootClientId( clientId ); + + return { + clientId, + rootClientId, + blockIndex: getBlockIndex( clientId ), + element: blockElement, + isDraggedBlock: isBlockDrag + ? draggedBlockClientIds.includes( clientId ) + : false, + innerBlockCount: getBlockCount( clientId ), + canInsertDraggedBlocksAsSibling: isBlockDrag + ? canInsertBlocks( + draggedBlockClientIds, + rootClientId + ) + : true, + canInsertDraggedBlocksAsChild: isBlockDrag + ? canInsertBlocks( draggedBlockClientIds, clientId ) + : true, + }; + } ); + + const newTarget = getListViewDropTarget( blocksData, position ); + + if ( newTarget ) { + setTarget( newTarget ); + } + }, + [ draggedBlockClientIds ] + ), + 200 + ); + + const ref = useDropZone( { + onDrop: onBlockDrop, + onDragOver( event ) { + // `currentTarget` is only available while the event is being + // handled, so get it now and pass it to the thottled function. + // https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget + throttled( event, event.currentTarget ); + }, + onDragEnd() { + throttled.cancel(); + setTarget( null ); + }, + } ); + + return { ref, target }; +} diff --git a/packages/block-editor/src/components/off-canvas-editor/use-list-view-expand-selected-item.js b/packages/block-editor/src/components/off-canvas-editor/use-list-view-expand-selected-item.js new file mode 100644 index 00000000000000..09b5e09e4713a3 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/use-list-view-expand-selected-item.js @@ -0,0 +1,58 @@ +/** + * WordPress dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +export default function useListViewExpandSelectedItem( { + firstSelectedBlockClientId, + setExpandedState, +} ) { + const [ selectedTreeId, setSelectedTreeId ] = useState( null ); + const { selectedBlockParentClientIds } = useSelect( + ( select ) => { + const { getBlockParents } = select( blockEditorStore ); + return { + selectedBlockParentClientIds: getBlockParents( + firstSelectedBlockClientId, + false + ), + }; + }, + [ firstSelectedBlockClientId ] + ); + + const parentClientIds = + Array.isArray( selectedBlockParentClientIds ) && + selectedBlockParentClientIds.length + ? selectedBlockParentClientIds + : null; + + // Expand tree when a block is selected. + useEffect( () => { + // If the selectedTreeId is the same as the selected block, + // it means that the block was selected using the block list tree. + if ( selectedTreeId === firstSelectedBlockClientId ) { + return; + } + + // If the selected block has parents, get the top-level parent. + if ( parentClientIds ) { + // If the selected block has parents, + // expand the tree branch. + setExpandedState( { + type: 'expand', + clientIds: selectedBlockParentClientIds, + } ); + } + }, [ firstSelectedBlockClientId ] ); + + return { + setSelectedTreeId, + }; +} diff --git a/packages/block-editor/src/components/off-canvas-editor/utils.js b/packages/block-editor/src/components/off-canvas-editor/utils.js new file mode 100644 index 00000000000000..f53f5a4cd4884a --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/utils.js @@ -0,0 +1,58 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +export const getBlockPositionDescription = ( position, siblingCount, level ) => + sprintf( + /* translators: 1: The numerical position of the block. 2: The total number of blocks. 3. The level of nesting for the block. */ + __( 'Block %1$d of %2$d, Level %3$d' ), + position, + siblingCount, + level + ); + +/** + * Returns true if the client ID occurs within the block selection or multi-selection, + * or false otherwise. + * + * @param {string} clientId Block client ID. + * @param {string|string[]} selectedBlockClientIds Selected block client ID, or an array of multi-selected blocks client IDs. + * + * @return {boolean} Whether the block is in multi-selection set. + */ +export const isClientIdSelected = ( clientId, selectedBlockClientIds ) => + Array.isArray( selectedBlockClientIds ) && selectedBlockClientIds.length + ? selectedBlockClientIds.indexOf( clientId ) !== -1 + : selectedBlockClientIds === clientId; + +/** + * From a start and end clientId of potentially different nesting levels, + * return the nearest-depth ids that have a common level of depth in the + * nesting hierarchy. For multiple block selection, this ensure that the + * selection is always at the same nesting level, and not split across + * separate levels. + * + * @param {string} startId The first id of a selection. + * @param {string} endId The end id of a selection, usually one that has been clicked on. + * @param {string[]} startParents An array of ancestor ids for the start id, in descending order. + * @param {string[]} endParents An array of ancestor ids for the end id, in descending order. + * @return {Object} An object containing the start and end ids. + */ +export function getCommonDepthClientIds( + startId, + endId, + startParents, + endParents +) { + const startPath = [ ...startParents, startId ]; + const endPath = [ ...endParents, endId ]; + const depth = Math.min( startPath.length, endPath.length ) - 1; + const start = startPath[ depth ]; + const end = endPath[ depth ]; + + return { + start, + end, + }; +} diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 2136f060e2a03a..13118389dfc0e9 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -9,7 +9,7 @@ import classnames from 'classnames'; import { useState, useEffect, useRef, Platform } from '@wordpress/element'; import { addQueryArgs } from '@wordpress/url'; import { - __experimentalListView as ListView, + __experimentalOffCanvasEditor as OffCanvasEditor, InspectorControls, useBlockProps, __experimentalRecursionProvider as RecursionProvider, @@ -681,10 +681,10 @@ function Navigation( { actionLabel={ __( "Switch to '%s'" ) } /> { isOffCanvasNavigationEditorEnabled && ( - ) } @@ -865,10 +865,10 @@ function Navigation( { actionLabel={ __( "Switch to '%s'" ) } /> { isOffCanvasNavigationEditorEnabled && ( - ) } From a3de2ca29c2352ed211ca349f8451984285116d9 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Tue, 1 Nov 2022 16:26:09 +0200 Subject: [PATCH 10/17] Adds to the experiment a new menu selector in a more vertical menu --- .../src/navigation/edit/index.js | 126 +++++++++++++----- .../edit/navigation-menu-selector.js | 22 ++- .../block-library/src/navigation/editor.scss | 7 + 3 files changed, 118 insertions(+), 37 deletions(-) diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 13118389dfc0e9..07c544176239e7 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -32,6 +32,8 @@ import { __experimentalToggleGroupControlOption as ToggleGroupControlOption, Button, Spinner, + __experimentalHStack as HStack, + __experimentalHeading as Heading, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; @@ -835,43 +837,103 @@ function Navigation( { - - { - handleUpdateMenu( menuId ); - } } - onSelectClassicMenu={ async ( classicMenu ) => { - const navMenu = await convertClassicMenu( - classicMenu.id, - classicMenu.name, - 'draft' - ); - if ( navMenu ) { - handleUpdateMenu( navMenu.id, { - focusNavigationBlock: true, - } ); - } - } } - onCreateNew={ createUntitledEmptyNavigationMenu } - createNavigationMenuIsSuccess={ - createNavigationMenuIsSuccess - } - createNavigationMenuIsError={ - createNavigationMenuIsError - } - /* translators: %s: The name of a menu. */ - actionLabel={ __( "Switch to '%s'" ) } - /> - { isOffCanvasNavigationEditorEnabled && ( + { isOffCanvasNavigationEditorEnabled && ( + + + + { __( 'Menu' ) } + + { + handleUpdateMenu( menuId ); + } } + onSelectClassicMenu={ async ( + classicMenu + ) => { + const navMenu = + await convertClassicMenu( + classicMenu.id, + classicMenu.name, + 'draft' + ); + if ( navMenu ) { + handleUpdateMenu( navMenu.id, { + focusNavigationBlock: true, + } ); + } + } } + onCreateNew={ + createUntitledEmptyNavigationMenu + } + createNavigationMenuIsSuccess={ + createNavigationMenuIsSuccess + } + createNavigationMenuIsError={ + createNavigationMenuIsError + } + /* translators: %s: The name of a menu. */ + actionLabel={ __( "Switch to '%s'" ) } + /> + + - ) } - + + ) } + { ! isOffCanvasNavigationEditorEnabled && ( + + { + handleUpdateMenu( menuId ); + } } + onSelectClassicMenu={ async ( classicMenu ) => { + const navMenu = await convertClassicMenu( + classicMenu.id, + classicMenu.name, + 'draft' + ); + if ( navMenu ) { + handleUpdateMenu( navMenu.id, { + focusNavigationBlock: true, + } ); + } + } } + onCreateNew={ + createUntitledEmptyNavigationMenu + } + createNavigationMenuIsSuccess={ + createNavigationMenuIsSuccess + } + createNavigationMenuIsError={ + createNavigationMenuIsError + } + /* translators: %s: The name of a menu. */ + actionLabel={ __( "Switch to '%s'" ) } + /> + + + ) } { stylingInspectorControls } { isEntityAvailable && ( diff --git a/packages/block-library/src/navigation/edit/navigation-menu-selector.js b/packages/block-library/src/navigation/edit/navigation-menu-selector.js index 036d86f06f2154..4412b5137e4710 100644 --- a/packages/block-library/src/navigation/edit/navigation-menu-selector.js +++ b/packages/block-library/src/navigation/edit/navigation-menu-selector.js @@ -10,7 +10,7 @@ import { VisuallyHidden, } from '@wordpress/components'; import { useEntityProp } from '@wordpress/core-data'; -import { Icon, chevronUp, chevronDown } from '@wordpress/icons'; +import { Icon, chevronUp, chevronDown, moreVertical } from '@wordpress/icons'; import { __, sprintf } from '@wordpress/i18n'; import { decodeEntities } from '@wordpress/html-entities'; import { useEffect, useMemo, useState } from '@wordpress/element'; @@ -31,6 +31,9 @@ function NavigationMenuSelector( { createNavigationMenuIsError, toggleProps = {}, } ) { + const isOffCanvasNavigationEditorEnabled = + window?.__experimentalEnableOffCanvasNavigationEditor === true; + /* translators: %s: The name of a menu. */ const createActionLabel = __( "Create from '%s'" ); @@ -133,6 +136,7 @@ function NavigationMenuSelector( { ), isBusy: ! enableOptions, + isSmall: true, disabled: ! enableOptions, __experimentalIsFocusable: true, onClick: () => { @@ -161,11 +165,19 @@ function NavigationMenuSelector( { return ( { ( { onClose } ) => ( <> diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index cf7c04c84f4db3..f438b9d8b49774 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -544,6 +544,13 @@ body.editor-styles-wrapper color: inherit; } +.components-heading.wp-block-navigation-off-canvas-editor__title { + margin: 0; +} +.wp-block-navigation-off-canvas-editor__header { + margin-bottom: $grid-unit-10; +} + // Customize the mobile editing. // This can be revisited in the future, but for now, inherit design from the parent. .is-menu-open .wp-block-navigation__responsive-container-content * { From 640cfee54058359bc18c4e6c27f1292372015ee6 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Tue, 1 Nov 2022 16:30:29 +0200 Subject: [PATCH 11/17] remove small prop for menu selector when out of experiment --- .../src/navigation/edit/navigation-menu-selector.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/block-library/src/navigation/edit/navigation-menu-selector.js b/packages/block-library/src/navigation/edit/navigation-menu-selector.js index 4412b5137e4710..8103dfeef3d1ba 100644 --- a/packages/block-library/src/navigation/edit/navigation-menu-selector.js +++ b/packages/block-library/src/navigation/edit/navigation-menu-selector.js @@ -136,7 +136,6 @@ function NavigationMenuSelector( { ), isBusy: ! enableOptions, - isSmall: true, disabled: ! enableOptions, __experimentalIsFocusable: true, onClick: () => { From ec7409b426972b0e0c6cfe4f1034fd0f9a53a930 Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Tue, 1 Nov 2022 17:11:09 +0000 Subject: [PATCH 12/17] small refactoring to remove duplicated code --- package-lock.json | 85 +++++++++-------- package.json | 2 +- .../src/navigation/edit/index.js | 92 ++++++------------- 3 files changed, 76 insertions(+), 103 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e5c9c2d0c2806..c5015952749edc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16968,15 +16968,16 @@ } }, "@typescript-eslint/utils": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.0.tgz", - "integrity": "sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.0.tgz", + "integrity": "sha512-JZ++3+h1vbeG1NUECXQZE3hg0kias9kOtcQr3+JVQ3whnjvKuMyktJAAIj6743OeNPnGBmjj7KEmiDL7qsdnCQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.40.0", - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/typescript-estree": "5.40.0", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" @@ -16994,30 +16995,36 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "dev": true + }, "@typescript-eslint/scope-manager": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.0.tgz", - "integrity": "sha512-d3nPmjUeZtEWRvyReMI4I1MwPGC63E8pDoHy0BnrYjnJgilBD3hv7XOiETKLY/zTwI7kCnBDf2vWTRUVpYw0Uw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.0.tgz", + "integrity": "sha512-l5/3IBHLH0Bv04y+H+zlcLiEMEMjWGaCX6WyHE5Uk2YkSGAMlgdUPsT/ywTSKgu9D1dmmKMYgYZijObfA39Wow==", "dev": true, "requires": { - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/visitor-keys": "5.40.0" + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0" } }, "@typescript-eslint/types": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.0.tgz", - "integrity": "sha512-V1KdQRTXsYpf1Y1fXCeZ+uhjW48Niiw0VGt4V8yzuaDTU8Z1Xl7yQDyQNqyAFcVhpYXIVCEuxSIWTsLDpHgTbw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.0.tgz", + "integrity": "sha512-t4lzO9ZOAUcHY6bXQYRuu+3SSYdD9TS8ooApZft4WARt4/f2Cj/YpvbTe8A4GuhT4bNW72goDMOy7SW71mZwGw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.0.tgz", - "integrity": "sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.0.tgz", + "integrity": "sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/visitor-keys": "5.40.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -17026,12 +17033,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.0.tgz", - "integrity": "sha512-ijJ+6yig+x9XplEpG2K6FUdJeQGGj/15U3S56W9IqXKJqleuD7zJ2AX/miLezwxpd7ZxDAqO87zWufKg+RPZyQ==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.0.tgz", + "integrity": "sha512-QHbu5Hf/2lOEOwy+IUw0GoSCuAzByTAWWrOTKzTzsotiUnWFpuKnXcAhC9YztAf2EElQ0VvIK+pHJUPkM0q7jg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/types": "5.42.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -19599,7 +19606,7 @@ "app-root-dir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz", - "integrity": "sha1-OBh+wt6nV3//Az/8sSFyaS/24Rg=", + "integrity": "sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==", "dev": true }, "app-root-path": { @@ -27881,7 +27888,7 @@ "babel-plugin-add-react-displayname": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz", - "integrity": "sha1-M51M3be2X9YtHfnbn+BN4TQSK9U=", + "integrity": "sha512-LY3+Y0XVDYcShHHorshrDbt4KFWL4bSeniCtl4SYZbask+Syngk1uMPCeN9+nSiZo6zX5s0RTq/J9Pnaaf/KHw==", "dev": true }, "babel-plugin-apply-mdx-type-prop": { @@ -28304,7 +28311,7 @@ "batch-processor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/batch-processor/-/batch-processor-1.0.0.tgz", - "integrity": "sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg=", + "integrity": "sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA==", "dev": true }, "bcrypt-pbkdf": { @@ -31555,7 +31562,7 @@ "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, "cssesc": { @@ -34364,9 +34371,9 @@ } }, "eslint-plugin-testing-library": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.7.2.tgz", - "integrity": "sha512-0ZmHeR/DUUgEzW8rwUBRWxuqntipDtpvxK0hymdHnLlABryJkzd+CAHr+XnISaVsTisZ5MLHp6nQF+8COHLLTA==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.9.1.tgz", + "integrity": "sha512-6BQp3tmb79jLLasPHJmy8DnxREe+2Pgf7L+7o09TSWPfdqqtQfRZmZNetr5mOs3yqZk/MRNxpN3RUpJe0wB4LQ==", "dev": true, "requires": { "@typescript-eslint/utils": "^5.13.0" @@ -37116,7 +37123,7 @@ "has-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-glob/-/has-glob-1.0.0.tgz", - "integrity": "sha1-mqqe7b/7G6OZCnsAEPtnjuAIEgc=", + "integrity": "sha512-D+8A457fBShSEI3tFCj65PAbT++5sKiFtdCdOam0gnfBgw9D277OERk+HM9qYJXmdVLZ/znez10SqHN0BBQ50g==", "dev": true, "requires": { "is-glob": "^3.0.0" @@ -37125,7 +37132,7 @@ "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", "dev": true, "requires": { "is-extglob": "^2.1.0" @@ -39012,7 +39019,7 @@ "is-window": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz", - "integrity": "sha1-LIlspT25feRdPDMTOmXYyfVjSA0=", + "integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg==", "dev": true }, "is-windows": { @@ -42389,7 +42396,7 @@ "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=", + "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", "dev": true }, "js-tokens": { @@ -44077,7 +44084,7 @@ "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", "dev": true }, "macos-release": { @@ -47322,7 +47329,7 @@ "num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==", "dev": true }, "number-is-nan": { @@ -48800,7 +48807,7 @@ "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", "dev": true }, "p-event": { @@ -50374,7 +50381,7 @@ "pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", "dev": true }, "prismjs": { @@ -52680,7 +52687,7 @@ "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", "dev": true }, "remark": { diff --git a/package.json b/package.json index 0dc258e2bcd341..0e7e66a91d4d83 100644 --- a/package.json +++ b/package.json @@ -189,7 +189,7 @@ "eslint-plugin-jest-dom": "4.0.2", "eslint-plugin-playwright": "0.8.0", "eslint-plugin-ssr-friendly": "1.0.6", - "eslint-plugin-testing-library": "5.7.2", + "eslint-plugin-testing-library": "5.9.1", "execa": "4.0.2", "fast-glob": "3.2.7", "filenamify": "4.2.0", diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 07c544176239e7..f8448b36efc73f 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -833,6 +833,33 @@ function Navigation( { ); } + const navigationMenuSelectorInstance = ( + { + handleUpdateMenu( menuId ); + } } + onSelectClassicMenu={ async ( classicMenu ) => { + const navMenu = await convertClassicMenu( + classicMenu.id, + classicMenu.name, + 'draft' + ); + if ( navMenu ) { + handleUpdateMenu( navMenu.id, { + focusNavigationBlock: true, + } ); + } + } } + onCreateNew={ createUntitledEmptyNavigationMenu } + createNavigationMenuIsSuccess={ createNavigationMenuIsSuccess } + createNavigationMenuIsError={ createNavigationMenuIsError } + /* translators: %s: The name of a menu. */ + actionLabel={ __( "Switch to '%s'" ) } + /> + ); + return ( @@ -846,39 +873,7 @@ function Navigation( { > { __( 'Menu' ) } - { - handleUpdateMenu( menuId ); - } } - onSelectClassicMenu={ async ( - classicMenu - ) => { - const navMenu = - await convertClassicMenu( - classicMenu.id, - classicMenu.name, - 'draft' - ); - if ( navMenu ) { - handleUpdateMenu( navMenu.id, { - focusNavigationBlock: true, - } ); - } - } } - onCreateNew={ - createUntitledEmptyNavigationMenu - } - createNavigationMenuIsSuccess={ - createNavigationMenuIsSuccess - } - createNavigationMenuIsError={ - createNavigationMenuIsError - } - /* translators: %s: The name of a menu. */ - actionLabel={ __( "Switch to '%s'" ) } - /> + { navigationMenuSelectorInstance } - { - handleUpdateMenu( menuId ); - } } - onSelectClassicMenu={ async ( classicMenu ) => { - const navMenu = await convertClassicMenu( - classicMenu.id, - classicMenu.name, - 'draft' - ); - if ( navMenu ) { - handleUpdateMenu( navMenu.id, { - focusNavigationBlock: true, - } ); - } - } } - onCreateNew={ - createUntitledEmptyNavigationMenu - } - createNavigationMenuIsSuccess={ - createNavigationMenuIsSuccess - } - createNavigationMenuIsError={ - createNavigationMenuIsError - } - /* translators: %s: The name of a menu. */ - actionLabel={ __( "Switch to '%s'" ) } - /> + { navigationMenuSelectorInstance } + { isOffCanvasNavigationEditorEnabled && ( + + ) } ) } diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index d324c6b6def612..5c7e9a231fc1b5 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -589,6 +589,13 @@ body.editor-styles-wrapper margin-bottom: $grid-unit-20; } +// increased specificity to override button variant +// for the manage menus button in the advanced area +// of the navigation block +.components-button.is-link.wp-block-navigation-manage-menus-button { + margin-bottom: $grid-unit-20; +} + .wp-block-navigation__overlay-menu-preview { display: flex; align-items: center;