From 1664d5e9708d6085c003aaef2d2feb9343f0e0d6 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 11 May 2023 09:16:14 +0400 Subject: [PATCH 001/131] Navigation: Unlock private APIs outside of the component (#50509) --- .../src/navigation/edit/menu-inspector-controls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/edit/menu-inspector-controls.js b/packages/block-library/src/navigation/edit/menu-inspector-controls.js index 38f97a9d8f90ba..20a8097be22720 100644 --- a/packages/block-library/src/navigation/edit/menu-inspector-controls.js +++ b/packages/block-library/src/navigation/edit/menu-inspector-controls.js @@ -25,6 +25,7 @@ import useNavigationMenu from '../use-navigation-menu'; /* translators: %s: The name of a menu. */ const actionLabel = __( "Switch to '%s'" ); +const { OffCanvasEditor, LeafMoreMenu } = unlock( blockEditorPrivateApis ); const MainContent = ( { clientId, @@ -33,7 +34,6 @@ const MainContent = ( { isNavigationMenuMissing, onCreateNew, } ) => { - const { OffCanvasEditor, LeafMoreMenu } = unlock( blockEditorPrivateApis ); // Provide a hierarchy of clientIds for the given Navigation block (clientId). // This is required else the list view will display the entire block tree. const clientIdsTree = useSelect( From ed9be12a1d429691994e8e1072e44c34ea26f807 Mon Sep 17 00:00:00 2001 From: Juan Aldasoro Date: Thu, 11 May 2023 08:00:04 +0200 Subject: [PATCH 002/131] Remove the loader from sidebar navigation screen. (#50326) --- .../index.js | 10 ---------- .../loader.js | 9 --------- .../navigation-menu-content.js | 2 -- .../style.scss | 20 ------------------- 4 files changed, 41 deletions(-) delete mode 100644 packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/loader.js diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js index 8cf795adad0961..e6b1d9069ca576 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js @@ -14,7 +14,6 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; */ import SidebarNavigationScreen from '../sidebar-navigation-screen'; import NavigationMenuContent from './navigation-menu-content'; -import { NavigationMenuLoader } from './loader'; import { unlock } from '../../private-apis'; import { store as editSiteStore } from '../../store'; import { @@ -75,7 +74,6 @@ export default function SidebarNavigationScreenNavigationMenus() { ]; }, [ firstNavigationMenu ] ); - const isLoading = ! hasResolvedNavigationMenus; const hasNavigationMenus = !! navigationMenus?.length; const onSelect = useCallback( @@ -116,14 +114,6 @@ export default function SidebarNavigationScreenNavigationMenus() { ); } - if ( ! hasResolvedNavigationMenus || isLoading ) { - return ( - - - - ); - } - return ( -
-
-
- - ); -} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/navigation-menu-content.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/navigation-menu-content.js index d7d4e18fc2b51f..f551ff0a826b08 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/navigation-menu-content.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/navigation-menu-content.js @@ -19,7 +19,6 @@ import { store as coreStore } from '@wordpress/core-data'; * Internal dependencies */ import { unlock } from '../../private-apis'; -import { NavigationMenuLoader } from './loader'; function CustomLinkAdditionalBlockUI( { block, onClose } ) { const { updateBlockAttributes } = useDispatch( blockEditorStore ); @@ -179,7 +178,6 @@ export default function NavigationMenuContent( { rootClientId, onSelect } ) { // For example a navigation page list load its items has an effect on edit to load its items. return ( <> - { isLoading && } { ! isLoading && ( Date: Thu, 11 May 2023 10:38:56 +0400 Subject: [PATCH 003/131] Edit Site: Unlock private APIs outside of the component (#50534) --- .../src/components/secondary-sidebar/list-view-sidebar.js | 3 ++- .../navigation-menu-content.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js index 82c9739da1e75b..37c85216b9a35a 100644 --- a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js @@ -19,6 +19,8 @@ import { ESCAPE } from '@wordpress/keycodes'; import { store as editSiteStore } from '../../store'; import { unlock } from '../../private-apis'; +const { PrivateListView } = unlock( blockEditorPrivateApis ); + export default function ListViewSidebar() { const { setIsListViewOpened } = useDispatch( editSiteStore ); @@ -31,7 +33,6 @@ export default function ListViewSidebar() { } } - const { PrivateListView } = unlock( blockEditorPrivateApis ); return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
{ if ( From 2f77022533949e906c1db95016853c306e196c31 Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Thu, 11 May 2023 11:13:04 +0100 Subject: [PATCH 004/131] Remove the check for draft navigation menus from the UnsavedInnerBlocks component (#49161) * Navigation: Remove the check for draft navigation menus from the UnsavedInnerBlocks component * Add a new selector to check the status of all navigation menus --- .../navigation/edit/unsaved-inner-blocks.js | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js b/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js index 5fe3c94715309e..51a8d2aed7fe29 100644 --- a/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js +++ b/packages/block-library/src/navigation/edit/unsaved-inner-blocks.js @@ -10,16 +10,10 @@ import { useContext, useEffect, useRef, useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import useNavigationMenu from '../use-navigation-menu'; import { areBlocksDirty } from './are-blocks-dirty'; import { DEFAULT_BLOCK, ALLOWED_BLOCKS } from '../constants'; const EMPTY_OBJECT = {}; -const DRAFT_MENU_PARAMS = [ - 'postType', - 'wp_navigation', - { status: 'draft', per_page: -1 }, -]; export default function UnsavedInnerBlocks( { blocks, @@ -75,35 +69,30 @@ export default function UnsavedInnerBlocks( { } ); - const { isSaving, hasResolvedDraftNavigationMenus } = useSelect( + const { isSaving, hasResolvedAllNavigationMenus } = useSelect( ( select ) => { if ( isDisabled ) { return EMPTY_OBJECT; } - const { - getEntityRecords, - hasFinishedResolution, - isSavingEntityRecord, - } = select( coreStore ); + const { hasFinishedResolution, isSavingEntityRecord } = + select( coreStore ); return { isSaving: isSavingEntityRecord( 'postType', 'wp_navigation' ), - draftNavigationMenus: getEntityRecords( - // This is needed so that hasResolvedDraftNavigationMenus gives the correct status. - ...DRAFT_MENU_PARAMS - ), - hasResolvedDraftNavigationMenus: hasFinishedResolution( + hasResolvedAllNavigationMenus: hasFinishedResolution( 'getEntityRecords', - DRAFT_MENU_PARAMS + [ + 'postType', + 'wp_navigation', + { per_page: -1, status: [ 'publish', 'draft' ] }, + ] ), }; }, [ isDisabled ] ); - const { hasResolvedNavigationMenus } = useNavigationMenu(); - // Automatically save the uncontrolled blocks. useEffect( () => { // The block will be disabled when used in a BlockPreview. @@ -121,8 +110,7 @@ export default function UnsavedInnerBlocks( { if ( isDisabled || isSaving || - ! hasResolvedDraftNavigationMenus || - ! hasResolvedNavigationMenus || + ! hasResolvedAllNavigationMenus || ! hasSelection || ! innerBlocksAreDirty ) { @@ -135,8 +123,7 @@ export default function UnsavedInnerBlocks( { createNavigationMenu, isDisabled, isSaving, - hasResolvedDraftNavigationMenus, - hasResolvedNavigationMenus, + hasResolvedAllNavigationMenus, innerBlocksAreDirty, hasSelection, ] ); From cb878404d49e1a22829deefa3ceb614d22d5cfcc Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 11 May 2023 05:41:51 -0500 Subject: [PATCH 005/131] Apply useShouldContextualToolbarShow to edit site header navigable toolbar (#50349) --- .../src/components/header-edit-mode/index.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index 3f931ef0fb60fd..94ff901203e0c3 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -14,6 +14,7 @@ import { __experimentalPreviewOptions as PreviewOptions, NavigableToolbar, store as blockEditorStore, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { useSelect, useDispatch } from '@wordpress/data'; import { PinnedItems } from '@wordpress/interface'; @@ -125,6 +126,19 @@ export default function HeaderEditMode() { [ setIsListViewOpened, isListViewOpen ] ); + const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); + const { + shouldShowContextualToolbar, + canFocusHiddenToolbar, + fixedToolbarCanBeFocused, + } = useShouldContextualToolbarShow(); + // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. + // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. + const blockToolbarCanBeFocused = + shouldShowContextualToolbar || + canFocusHiddenToolbar || + fixedToolbarCanBeFocused; + const hasDefaultEditorCanvasView = ! useHasEditorCanvasContainer(); const isFocusMode = templateType === 'wp_template_part'; @@ -150,6 +164,9 @@ export default function HeaderEditMode() {
Date: Thu, 11 May 2023 13:42:31 +0100 Subject: [PATCH 006/131] Improve Nav block loading UX by preloading Navigation menu requests (#48683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Scaffold out preloading * All GET and OPTIONS preloading * Make preload paths more readable via add_query_arg * Add comment to unusual usage * Resolve PHPCS * Fix formatting * Rename away from “permissions” * Limit to Site Editor * Add context to doc block * Preload Browse Mode sidebar Navigation * Remove redundant preload * Use int not string for numeric --- .../navigation-block-preloading.php | 63 +++++++++++++++++++ lib/load.php | 1 + 2 files changed, 64 insertions(+) create mode 100644 lib/compat/wordpress-6.3/navigation-block-preloading.php diff --git a/lib/compat/wordpress-6.3/navigation-block-preloading.php b/lib/compat/wordpress-6.3/navigation-block-preloading.php new file mode 100644 index 00000000000000..82fe81b236a590 --- /dev/null +++ b/lib/compat/wordpress-6.3/navigation-block-preloading.php @@ -0,0 +1,63 @@ +name ) && 'core/edit-site' !== $context->name ) { + return $preload_paths; + } + + $navigation_rest_route = rest_get_route_for_post_type_items( + 'wp_navigation' + ); + + // Preload the OPTIONS request for all Navigation posts request. + $preload_paths[] = array( $navigation_rest_route, 'OPTIONS' ); + + // Preload the GET request for ALL 'published' or 'draft' Navigation posts. + $preload_paths[] = array( + add_query_arg( + array( + 'context' => 'edit', + 'per_page' => 100, + '_locale' => 'user', + // array indices are required to avoid query being encoded and not matching in cache. + 'status[0]' => 'publish', + 'status[1]' => 'draft', + ), + $navigation_rest_route + ), + 'GET', + ); + + // Preload request for Browse Mode sidebar "Navigation" section. + $preload_paths[] = array( + add_query_arg( + array( + 'context' => 'edit', + 'per_page' => 1, + 'status' => 'publish', + 'order' => 'desc', + 'orderby' => 'date', + ), + $navigation_rest_route + ), + 'GET', + ); + + return $preload_paths; +} +add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_preload_navigation_posts', 10, 2 ); diff --git a/lib/load.php b/lib/load.php index 79b076251e54ad..37dc2305678940 100644 --- a/lib/load.php +++ b/lib/load.php @@ -49,6 +49,7 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php'; require_once __DIR__ . '/compat/wordpress-6.3/rest-api.php'; require_once __DIR__ . '/compat/wordpress-6.3/theme-previews.php'; + require_once __DIR__ . '/compat/wordpress-6.3/navigation-block-preloading.php'; // Experimental. if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) { From 37d98396f63dd7463287fd9d2124a179a1f68458 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Thu, 11 May 2023 15:44:57 +0300 Subject: [PATCH 007/131] Add `lang` and `dir` attributes to text-formatting tools (#49985) * WIP - lang attribute * applying attributes * Porting of JB plugin. * lang & dir are defined in the upper scope * add translation icon * rename to language * migrate to useAnchor * Use the title * rename function to Edit * Strings tweaks * Fix styles * a11y tweak - add aria-describedby to the paragraph * Use "help" instead of a paragraph * Remove aria-describedby * Add missing role="menuitemcheckbox" * Update packages/format-library/src/language/index.js Co-authored-by: George Mamadashvili * Update packages/format-library/src/language/index.js Co-authored-by: George Mamadashvili * move the language format definition before the component * change anchorRef to popoverAnchor * Revert "move the language format definition before the component" This reverts commit 560fcbb27dccfb44ce2902b896bd874a23c4937c. * Move language above Edit * change class to has-language * Use a form * change classnames for the form and the popover * Update packages/format-library/src/language/index.js Co-authored-by: George Mamadashvili * Update packages/format-library/src/language/index.js Co-authored-by: George Mamadashvili * CS fix * Add event.preventDefault() * Update icon * Use a dropdown & automate RTL * Revert "Use a dropdown & automate RTL" This reverts commit df5bb36a1c5edee0159ca8465d7f0b5f21201ef0. * rename icon from "translation" to "language" * right-align the button --------- Co-authored-by: Andrea Fercia Co-authored-by: George Mamadashvili --- .../format-library/src/default-formats.js | 2 + packages/format-library/src/language/index.js | 124 ++++++++++++++++++ .../format-library/src/language/style.scss | 6 + packages/format-library/src/style.scss | 1 + packages/icons/src/index.js | 1 + packages/icons/src/library/language.js | 12 ++ 6 files changed, 146 insertions(+) create mode 100644 packages/format-library/src/language/index.js create mode 100644 packages/format-library/src/language/style.scss create mode 100644 packages/icons/src/library/language.js diff --git a/packages/format-library/src/default-formats.js b/packages/format-library/src/default-formats.js index 791cabb1f118e4..c94c347156ff0e 100644 --- a/packages/format-library/src/default-formats.js +++ b/packages/format-library/src/default-formats.js @@ -13,6 +13,7 @@ import { subscript } from './subscript'; import { superscript } from './superscript'; import { keyboard } from './keyboard'; import { unknown } from './unknown'; +import { language } from './language'; export default [ bold, @@ -27,4 +28,5 @@ export default [ superscript, keyboard, unknown, + language, ]; diff --git a/packages/format-library/src/language/index.js b/packages/format-library/src/language/index.js new file mode 100644 index 00000000000000..60f35c98a4aaec --- /dev/null +++ b/packages/format-library/src/language/index.js @@ -0,0 +1,124 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * WordPress dependencies + */ +import { RichTextToolbarButton } from '@wordpress/block-editor'; +import { + TextControl, + SelectControl, + Button, + Popover, + __experimentalHStack as HStack, +} from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { applyFormat, removeFormat, useAnchor } from '@wordpress/rich-text'; +import { language as languageIcon } from '@wordpress/icons'; + +const name = 'core/language'; +const title = __( 'Language' ); + +export const language = { + name, + tagName: 'span', + className: 'has-language', + edit: Edit, + title, +}; + +function Edit( props ) { + const { contentRef, isActive, onChange, value } = props; + const popoverAnchor = useAnchor( { + editableContentElement: contentRef.current, + settings: language, + } ); + + const [ lang, setLang ] = useState( '' ); + const [ dir, setDir ] = useState( 'ltr' ); + + const [ isPopoverVisible, setIsPopoverVisible ] = useState( false ); + const togglePopover = () => { + setIsPopoverVisible( ( state ) => ! state ); + setLang( '' ); + setDir( 'ltr' ); + }; + + return ( + <> + { + if ( isActive ) { + onChange( removeFormat( value, name ) ); + } else { + togglePopover(); + } + } } + isActive={ isActive } + role="menuitemcheckbox" + /> + + { isPopoverVisible && ( + +
{ + onChange( + applyFormat( value, { + type: name, + attributes: { + lang, + dir, + }, + } ) + ); + togglePopover(); + event.preventDefault(); + } } + > + setLang( val ) } + help={ __( + 'A valid language attribute, like "en" or "fr".' + ) } + /> + setDir( val ) } + /> + + - - - - - + + + + ) } diff --git a/packages/edit-site/src/components/create-template-part-modal/index.js b/packages/edit-site/src/components/create-template-part-modal/index.js index e2cbdbf89288cd..f91ec2c8c8b2de 100644 --- a/packages/edit-site/src/components/create-template-part-modal/index.js +++ b/packages/edit-site/src/components/create-template-part-modal/index.js @@ -13,6 +13,7 @@ import { Modal, __experimentalRadioGroup as RadioGroup, __experimentalRadio as Radio, + __experimentalHStack as HStack, __experimentalVStack as VStack, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -106,31 +107,24 @@ export default function CreateTemplatePartModal( { closeModal, onCreate } ) { ) } - - - - - - - - + + + + From 4ebe5ca67b311360297c7f38857790fcfcab5fd2 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 12 May 2023 10:18:46 +0200 Subject: [PATCH 013/131] Mobile Release v1.95.0 (#50547) * Release script: Update react-native-editor version to 1.95.0 * Release script: Update with changes from 'npm run core preios' * Update react-native-editor changelog --- packages/react-native-aztec/package.json | 2 +- packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 2 ++ packages/react-native-editor/ios/Podfile.lock | 8 ++++---- packages/react-native-editor/package.json | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index 554e037c47d618..c9f769f306a705 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.94.0", + "version": "1.95.0", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index 81eb7a974b290f..e632331fa2485e 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.94.0", + "version": "1.95.0", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index d8715f7601b579..6c98c6a5b8a35e 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,8 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased + +## 1.95.0 - [*] Fix crash when trying to convert to regular blocks an undefined/deleted reusable block [#50475] - [**] Tapping on a nested block now gets focus directly instead of having to tap multiple times depeding on the nesting levels. [#50108] - [*] Use host app namespace in reusable block message [#50478] diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index 1c5441c010b172..8fcaecffe9aa3d 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -13,7 +13,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.69.4) - fmt (6.2.1) - glog (0.3.5) - - Gutenberg (1.94.0): + - Gutenberg (1.95.0): - React-Core (= 0.69.4) - React-CoreModules (= 0.69.4) - React-RCTImage (= 0.69.4) @@ -360,7 +360,7 @@ PODS: - React-Core - RNSVG (9.13.6): - React-Core - - RNTAztecView (1.94.0): + - RNTAztecView (1.95.0): - React-Core - WordPress-Aztec-iOS (~> 1.19.8) - SDWebImage (5.11.1): @@ -540,7 +540,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: 2ff441cbe6e58c1778d8a5cf3311831a6a8c0809 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a - Gutenberg: 001ebfc576414f3e862e909b6f5abd5d38fe672e + Gutenberg: 98fc15135123cdfcfa6ad1fc35c8012014dab488 libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a RCTRequired: bd9d2ab0fda10171fcbcf9ba61a7df4dc15a28f4 @@ -582,7 +582,7 @@ SPEC CHECKSUMS: RNReanimated: bea6acb5fdcbd8ca27641180579d09e3434f803c RNScreens: 953633729a42e23ad0c93574d676b361e3335e8b RNSVG: 36a7359c428dcb7c6bce1cc546fbfebe069809b0 - RNTAztecView: 75c7cd1b1047035b64c3860d9d43fa2729ca59c6 + RNTAztecView: 95adb6b60e5d430ecf5eb710ff7813794d17ddc8 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d WordPress-Aztec-iOS: 7d11d598f14c82c727c08b56bd35fbeb7dafb504 diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index a926ef425d07b6..c70d9e013734e4 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.94.0", + "version": "1.95.0", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 5ef52d59b8078bab4b0a3134e3d4becb5fb83192 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Fri, 12 May 2023 11:46:24 +0300 Subject: [PATCH 014/131] [Site Editor]: Always show the `styles` navigation item (#50573) --- .../index.js | 37 +++++++++++- .../sidebar-navigation-screen-main/index.js | 60 +++++++------------ 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js index b15ed6c5276f00..06e486739ed74a 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js @@ -3,7 +3,9 @@ */ import { __ } from '@wordpress/i18n'; import { edit } from '@wordpress/icons'; -import { useDispatch } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { __experimentalNavigatorButton as NavigatorButton } from '@wordpress/components'; /** * Internal dependencies @@ -13,6 +15,39 @@ import StyleVariationsContainer from '../global-styles/style-variations-containe import { unlock } from '../../private-apis'; import { store as editSiteStore } from '../../store'; import SidebarButton from '../sidebar-button'; +import SidebarNavigationItem from '../sidebar-navigation-item'; + +export function SidebarNavigationItemGlobalStyles( props ) { + const { openGeneralSidebar } = useDispatch( editSiteStore ); + const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); + const hasGlobalStyleVariations = useSelect( + ( select ) => + !! select( + coreStore + ).__experimentalGetCurrentThemeGlobalStylesVariations()?.length, + [] + ); + if ( hasGlobalStyleVariations ) { + return ( + + ); + } + return ( + { + // switch to edit mode. + setCanvasMode( 'edit' ); + // open global styles sidebar. + openGeneralSidebar( 'edit-site/global-styles' ); + } } + /> + ); +} export default function SidebarNavigationScreenGlobalStyles() { const { openGeneralSidebar } = useDispatch( editSiteStore ); diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js index a35e07220d52c4..7c9e4b3bf91968 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js @@ -15,35 +15,24 @@ import { store as coreStore } from '@wordpress/core-data'; */ import SidebarNavigationScreen from '../sidebar-navigation-screen'; import SidebarNavigationItem from '../sidebar-navigation-item'; +import { SidebarNavigationItemGlobalStyles } from '../sidebar-navigation-screen-global-styles'; export default function SidebarNavigationScreenMain() { - const { hasNavigationMenus, hasGlobalStyleVariations } = useSelect( - ( select ) => { - const { - getEntityRecords, - __experimentalGetCurrentThemeGlobalStylesVariations, - } = select( coreStore ); - // The query needs to be the same as in the "SidebarNavigationScreenNavigationMenus" component, - // to avoid double network calls. - const navigationMenus = getEntityRecords( - 'postType', - 'wp_navigation', - { - per_page: 1, - status: 'publish', - order: 'desc', - orderby: 'date', - } - ); - return { - hasNavigationMenus: !! navigationMenus?.length, - hasGlobalStyleVariations: - !! __experimentalGetCurrentThemeGlobalStylesVariations() - ?.length, - }; - }, - [] - ); + const hasNavigationMenus = useSelect( ( select ) => { + // The query needs to be the same as in the "SidebarNavigationScreenNavigationMenus" component, + // to avoid double network calls. + const navigationMenus = select( coreStore ).getEntityRecords( + 'postType', + 'wp_navigation', + { + per_page: 1, + status: 'publish', + order: 'desc', + orderby: 'date', + } + ); + return !! navigationMenus?.length; + }, [] ); const showNavigationScreen = process.env.IS_GUTENBERG_PLUGIN ? hasNavigationMenus : false; @@ -66,17 +55,12 @@ export default function SidebarNavigationScreenMain() { { __( 'Navigation' ) } ) } - { hasGlobalStyleVariations && ( - - { __( 'Styles' ) } - - ) } - + + { __( 'Styles' ) } + Date: Fri, 12 May 2023 13:01:04 +0300 Subject: [PATCH 015/131] Edit Site: Optimize loading useSelect call (#50546) Co-authored-by: Jarda Snajdr --- packages/edit-site/src/components/editor/index.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 500402fca29004..777f6dd53afdf6 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -52,13 +52,15 @@ const interfaceLabels = { function useIsSiteEditorLoading() { const { isLoaded: hasLoadedPost } = useEditedEntityRecord(); - const { hasResolvingSelectors } = useSelect( ( select ) => { - return { - hasResolvingSelectors: select( coreStore ).hasResolvingSelectors(), - }; - } ); const [ loaded, setLoaded ] = useState( false ); - const inLoadingPause = ! loaded && ! hasResolvingSelectors; + const inLoadingPause = useSelect( + ( select ) => { + const hasResolvingSelectors = + select( coreStore ).hasResolvingSelectors(); + return ! loaded && ! hasResolvingSelectors; + }, + [ loaded ] + ); useEffect( () => { if ( inLoadingPause ) { From 34837405d999c74d6336d4a088b4b5503140f960 Mon Sep 17 00:00:00 2001 From: Karol Manijak Date: Fri, 12 May 2023 12:27:15 +0200 Subject: [PATCH 016/131] Adjust the logic of Post Title edit, so it avoids unnecessary OPTIONS requests (#49839) * Adjust the logic of Post Title edit, so it avoids unnecessary OPTIONS requests * Fix lint issues * Update the change comment description * Refactor useCanEditEntity arguments in post-title following code review suggestion --- packages/block-library/src/post-title/edit.js | 94 ++++++++++--------- 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/packages/block-library/src/post-title/edit.js b/packages/block-library/src/post-title/edit.js index defc79e8dd4ca4..8cd71881e06dec 100644 --- a/packages/block-library/src/post-title/edit.js +++ b/packages/block-library/src/post-title/edit.js @@ -32,7 +32,17 @@ export default function PostTitleEdit( { } ) { const TagName = 0 === level ? 'p' : 'h' + level; const isDescendentOfQueryLoop = Number.isFinite( queryId ); - const userCanEdit = useCanEditEntity( 'postType', postType, postId ); + /** + * Hack: useCanEditEntity may trigger an OPTIONS request to the REST API via the canUser resolver. + * However, when the Post Title is a descendant of a Query Loop block, the title cannot be edited. + * In order to avoid these unnecessary requests, we call the hook without + * the proper data, resulting in returning early without making them. + */ + const userCanEdit = useCanEditEntity( + 'postType', + ! isDescendentOfQueryLoop && postType, + postId + ); const [ rawTitle = '', setTitle, fullTitle ] = useEntityProp( 'postType', postType, @@ -54,56 +64,52 @@ export default function PostTitleEdit( { ); if ( postType && postId ) { - titleElement = - userCanEdit && ! isDescendentOfQueryLoop ? ( + titleElement = userCanEdit ? ( + + ) : ( + <TagName + { ...blockProps } + dangerouslySetInnerHTML={ { __html: fullTitle?.rendered } } + /> + ); + } + + if ( isLink && postType && postId ) { + titleElement = userCanEdit ? ( + <TagName { ...blockProps }> <PlainText - tagName={ TagName } - placeholder={ __( 'No Title' ) } + tagName="a" + href={ link } + target={ linkTarget } + rel={ rel } + placeholder={ ! rawTitle.length ? __( 'No Title' ) : null } value={ rawTitle } onChange={ setTitle } __experimentalVersion={ 2 } __unstableOnSplitAtEnd={ onSplitAtEnd } - { ...blockProps } /> - ) : ( - <TagName - { ...blockProps } - dangerouslySetInnerHTML={ { __html: fullTitle?.rendered } } + </TagName> + ) : ( + <TagName { ...blockProps }> + <a + href={ link } + target={ linkTarget } + rel={ rel } + onClick={ ( event ) => event.preventDefault() } + dangerouslySetInnerHTML={ { + __html: fullTitle?.rendered, + } } /> - ); - } - - if ( isLink && postType && postId ) { - titleElement = - userCanEdit && ! isDescendentOfQueryLoop ? ( - <TagName { ...blockProps }> - <PlainText - tagName="a" - href={ link } - target={ linkTarget } - rel={ rel } - placeholder={ - ! rawTitle.length ? __( 'No Title' ) : null - } - value={ rawTitle } - onChange={ setTitle } - __experimentalVersion={ 2 } - __unstableOnSplitAtEnd={ onSplitAtEnd } - /> - </TagName> - ) : ( - <TagName { ...blockProps }> - <a - href={ link } - target={ linkTarget } - rel={ rel } - onClick={ ( event ) => event.preventDefault() } - dangerouslySetInnerHTML={ { - __html: fullTitle?.rendered, - } } - /> - </TagName> - ); + </TagName> + ); } return ( From 71644b7e6f55c04fe36feb96f55b1f3493717baa Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco <gerardo.pacheco@automattic.com> Date: Fri, 12 May 2023 13:28:33 +0200 Subject: [PATCH 017/131] [Mobile] - Update E2E config to support an iPad simulator (#50528) * Mobile - Update E2E config to support an iPad simulator * Mobile - E2E utils - Update code to not add an extra object spread --- .../__device-tests__/helpers/caps.js | 14 ++++++++------ .../__device-tests__/helpers/utils.js | 5 +++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/helpers/caps.js b/packages/react-native-editor/__device-tests__/helpers/caps.js index bc776eb8aba20e..b9e6f61bd445aa 100644 --- a/packages/react-native-editor/__device-tests__/helpers/caps.js +++ b/packages/react-native-editor/__device-tests__/helpers/caps.js @@ -11,18 +11,20 @@ const ios = { }, }; -exports.iosLocal = { +exports.iosLocal = ( { iPadDevice = false } ) => ( { ...ios, - deviceName: 'iPhone 13', + deviceName: ! iPadDevice ? 'iPhone 13' : 'iPad Pro (9.7-inch)', wdaLaunchTimeout: 240000, usePrebuiltWDA: true, -}; +} ); -exports.iosServer = { +exports.iosServer = ( { iPadDevice = false } ) => ( { ...ios, platformVersion: '15.4', // Supported Sauce Labs platforms can be found here: https://saucelabs.com/rest/v1/info/platforms/appium - deviceName: 'iPhone 13 Simulator', -}; + deviceName: ! iPadDevice + ? 'iPhone 13 Simulator' + : 'iPad Pro (9.7 inch) Simulator', +} ); exports.android = { browserName: '', diff --git a/packages/react-native-editor/__device-tests__/helpers/utils.js b/packages/react-native-editor/__device-tests__/helpers/utils.js index 3c08eb381b1fdd..4591cd4176d251 100644 --- a/packages/react-native-editor/__device-tests__/helpers/utils.js +++ b/packages/react-native-editor/__device-tests__/helpers/utils.js @@ -16,6 +16,7 @@ const AppiumLocal = require( './appium-local' ); // Platform setup. const defaultPlatform = 'android'; const rnPlatform = process.env.TEST_RN_PLATFORM || defaultPlatform; +const iPadDevice = process.env.IPAD; // Environment setup, local environment or Sauce Labs. const defaultEnvironment = 'local'; @@ -115,10 +116,10 @@ const setupDriver = async () => { desiredCaps.app = `sauce-storage:Gutenberg-${ safeBranchName }.apk`; // App should be preloaded to sauce storage, this can also be a URL. } } else { - desiredCaps = { ...iosServer }; + desiredCaps = iosServer( { iPadDevice } ); desiredCaps.app = `sauce-storage:Gutenberg-${ safeBranchName }.app.zip`; // App should be preloaded to sauce storage, this can also be a URL. if ( isLocalEnvironment() ) { - desiredCaps = { ...iosLocal }; + desiredCaps = iosLocal( { iPadDevice } ); const iosPlatformVersions = getIOSPlatformVersions(); if ( iosPlatformVersions.length === 0 ) { From 25dadb0aa62c3effa1d9c6526cb07c98e89a9852 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher <saxonafletcher@gmail.com> Date: Fri, 12 May 2023 21:36:42 +1000 Subject: [PATCH 018/131] update site editor sidebar alignment (#50561) --- .../src/components/sidebar-navigation-screen/style.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss index a1a6209036eba6..b72edaba63f7df 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss @@ -6,7 +6,7 @@ } .edit-site-sidebar-navigation-screen__content { - margin: 0 $grid-unit-20 $grid-unit-20 $button-size; + margin: 0 0 $grid-unit-20 0; color: $gray-600; //z-index: z-index(".edit-site-sidebar-navigation-screen__content"); } @@ -34,7 +34,6 @@ box-shadow: 0 $grid-unit-10 $grid-unit-20 $gray-900; margin-bottom: $grid-unit-10; padding-bottom: $grid-unit-10; - padding-right: $grid-unit-20; z-index: z-index(".edit-site-sidebar-navigation-screen__title-icon"); } From 524f7ca1ad53a4356e7772d23324e6344912e96a Mon Sep 17 00:00:00 2001 From: Ben Dwyer <ben@scruffian.com> Date: Fri, 12 May 2023 12:59:43 +0100 Subject: [PATCH 019/131] Enable iframe-inline-styles e2e test (#50548) --- .../specs/editor/plugins/iframed-inline-styles.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-tests/specs/editor/plugins/iframed-inline-styles.test.js b/packages/e2e-tests/specs/editor/plugins/iframed-inline-styles.test.js index 6e6b3d670082c2..52b22e550f41ce 100644 --- a/packages/e2e-tests/specs/editor/plugins/iframed-inline-styles.test.js +++ b/packages/e2e-tests/specs/editor/plugins/iframed-inline-styles.test.js @@ -32,7 +32,7 @@ describe( 'iframed inline styles', () => { } ); // Skip flaky test. See https://github.com/WordPress/gutenberg/issues/35172 - it.skip( 'should load inline styles in iframe', async () => { + it( 'should load inline styles in iframe', async () => { await insertBlock( 'Iframed Inline Styles' ); expect( await getEditedPostContent() ).toMatchSnapshot(); From 0bf1e7978dfdc67296149caf24a0bd9967c1c9da Mon Sep 17 00:00:00 2001 From: David Calhoun <github@davidcalhoun.me> Date: Fri, 12 May 2023 08:05:18 -0400 Subject: [PATCH 020/131] perf: Reduce inner blocks tree depth (#50447) * test: Await overlooked asynchronous task The test helper executes asynchronous updates to the layout. If this is not awaited, test failures can occur in certain circumstances. * feat: Combine BlockList and BlockListCompact Reduce code duplication between the separate components, e.g. item render, footer, end-of-list block appender button, empty list placeholder. * wip: Gallery, Columns working; Buttons broken Partially fix multi-column layouts by passing existing styles to a wrapping view. All of the passed styles may not be necessary, need to investigate. Also, Buttons render broken, wrapping lines and overflowing the parent container. * test: Fix nested lists tests Nested lists now rely upon `BlockListItem`, which returns `null` if the `blockWidth` has not yet been set. In order for test queries for nested list items to succeed, we must trigger the `onLayout` callback for the nested block lists. `BlockListItem` is now used to expand capabilities for nested blocks, e.g. multi-column grid items. * wip: Fix Blocks button layout Fixes Buttons, but Columns remain broken. * refactor: Remove unused BlockList title prop * refactor: Separate inner block list styles The list is no longer shared, so we can merely set the appropriate styles on each list element. * refactor: Remove unused virtualized listKey Now that inner blocks do not use a virtualized list, a unique list key is no longer necessary. * refactor: Remove outdated inline comment The scroll ref is no longer passed to inner blocks as they do not use virtualized lists. * wip: Explore nest Columns fixes The relocated styles may need to be separate from the top-level block list element, as the styles may conflict with other styles. It does not fix Columns, however. * Mobile - BlockList - Fix layout issues for stacked horizontally blocks * fix: Revert Buttons alignment workaround This caused issues for non-Buttons inner block alignment. The original issue of Buttons inner blocks not rendering inline was addressed in 43c0b14918f8ed03037c01d94321922dc31a7fa3. * fix: Column width sums less than 100% correct align These styles caused columns to align to the center when their width sum did not equal or execeed 100%. Removing these styles did not appear to negatively impact other inner blocks or the use cases outlined in https://github.com/WordPress/gutenberg/pull/25621. * refactor: Inner blocks use clientId key This mirrors the approach for cells of top-level block lists. It is also likely more robust, providing better performance when reordering blocks. * refactor: Remove unnecessary key The inner blocks list now sets a key on a wrapping view, rather than via the `renderItem` function. Thus, this key is no superfluous. * fix: Prevent block list footer from stretching to parent height The parent flex styles were applied to the footer element. This wrapping view prevents those styles from erroneously stretching the provided footer element. * Mobile - List block - Remove usage of useCompactList and disables the option to render the appender for InnerBlocks * test: Update Android e2e Buttons block Xpath selector This Xpath selector became outdated with the block list refactor. * test: Delay query for block actions menu Appium reported this cached element had been removed from the DOM. Relocating the query seemingly resolved this issue. --------- Co-authored-by: Gerardo <gerardo.pacheco@automattic.com> --- .../block-list/block-list-compact.native.js | 63 ---- .../src/components/block-list/index.native.js | 293 +++++++++--------- .../block-list/test/index.native.js | 4 +- .../components/inner-blocks/index.native.js | 6 +- .../src/list-item/edit.native.js | 2 +- packages/block-library/src/list/edit.js | 2 +- .../src/list/test/edit.native.js | 17 +- .../__device-tests__/pages/editor-page.js | 10 +- 8 files changed, 179 insertions(+), 218 deletions(-) delete mode 100644 packages/block-editor/src/components/block-list/block-list-compact.native.js diff --git a/packages/block-editor/src/components/block-list/block-list-compact.native.js b/packages/block-editor/src/components/block-list/block-list-compact.native.js deleted file mode 100644 index ad9d0c7301eed3..00000000000000 --- a/packages/block-editor/src/components/block-list/block-list-compact.native.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * External dependencies - */ -import { View } from 'react-native'; - -/** - * WordPress dependencies - */ -import { store as blockEditorStore } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import styles from './style.scss'; -import BlockListBlock from './block'; - -/** - * NOTE: This is a component currently used by the List block (V2) - * It only passes the needed props for this block, if other blocks will use it - * make sure you pass other props that might be required coming from: - * components/inner-blocks/index.native.js - */ - -function BlockListCompact( props ) { - const { - marginHorizontal = styles.defaultBlock.marginLeft, - marginVertical = styles.defaultBlock.marginTop, - rootClientId, - } = props; - const { blockClientIds } = useSelect( - ( select ) => { - const { getBlockOrder } = select( blockEditorStore ); - const blockOrder = getBlockOrder( rootClientId ); - - return { - blockClientIds: blockOrder, - }; - }, - [ rootClientId ] - ); - - const containerStyle = { - marginVertical: -marginVertical, - marginHorizontal: -marginHorizontal, - }; - - return ( - <View style={ containerStyle } testID="block-list-wrapper"> - { blockClientIds.map( ( currentClientId ) => ( - <BlockListBlock - clientId={ currentClientId } - rootClientId={ rootClientId } - key={ currentClientId } - marginHorizontal={ marginHorizontal } - marginVertical={ marginVertical } - /> - ) ) } - </View> - ); -} - -export default BlockListCompact; diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 48f839b156d455..587d03dd882542 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -26,7 +26,6 @@ import BlockListItem from './block-list-item'; import BlockListItemCell from './block-list-item-cell'; import { BlockListProvider, - BlockListConsumer, DEFAULT_BLOCK_LIST_CONTEXT, } from './block-list-context'; import { BlockDraggableWrapper } from '../block-draggable'; @@ -35,14 +34,7 @@ import { store as blockEditorStore } from '../../store'; const identity = ( x ) => x; const stylesMemo = {}; -const getStyles = ( - isRootList, - isStackedHorizontally, - horizontalAlignment -) => { - if ( isRootList ) { - return; - } +const getStyles = ( isStackedHorizontally, horizontalAlignment ) => { const styleName = `${ isStackedHorizontally }-${ horizontalAlignment }`; if ( stylesMemo[ styleName ] ) { return stylesMemo[ styleName ]; @@ -74,7 +66,6 @@ export default function BlockList( { renderAppender, renderFooterAppender, rootClientId, - title, withFooter = true, } ) { const { @@ -190,114 +181,6 @@ export default function BlockList( { } }; - const renderList = ( extraProps = {} ) => { - const { parentScrollRef, onScroll } = extraProps; - - const { blockToolbar, headerToolbar, floatingToolbar } = styles; - - const containerStyle = { - flex: isRootList ? 1 : 0, - // We set negative margin in the parent to remove the edge spacing between parent block and child block in ineer blocks. - marginVertical: isRootList ? 0 : -marginVertical, - marginHorizontal: isRootList ? 0 : -marginHorizontal, - }; - - const isContentStretch = contentResizeMode === 'stretch'; - const isMultiBlocks = blockClientIds.length > 1; - const { isWider } = alignmentHelpers; - const extraScrollHeight = - headerToolbar.height + - blockToolbar.height + - ( isFloatingToolbarVisible ? floatingToolbar.height : 0 ); - - const scrollViewStyle = [ - { flex: isRootList ? 1 : 0 }, - ! isRootList && styles.overflowVisible, - ]; - - return ( - <View - style={ containerStyle } - onAccessibilityEscape={ clearSelectedBlock } - onLayout={ onLayout } - testID="block-list-wrapper" - > - <KeyboardAwareFlatList - { ...( Platform.OS === 'android' - ? { removeClippedSubviews: false } - : {} ) } // Disable clipping on Android to fix focus losing. See https://github.com/wordpress-mobile/gutenberg-mobile/pull/741#issuecomment-472746541 - accessibilityLabel="block-list" - innerRef={ ( ref ) => { - scrollViewRef.current = parentScrollRef || ref; - } } - extraScrollHeight={ extraScrollHeight } - keyboardShouldPersistTaps="always" - scrollViewStyle={ scrollViewStyle } - extraData={ getExtraData() } - scrollEnabled={ isRootList } - contentContainerStyle={ [ - horizontal && styles.horizontalContentContainer, - isWider( blockWidth, 'medium' ) && - ( isContentStretch && isMultiBlocks - ? styles.horizontalContentContainerStretch - : styles.horizontalContentContainerCenter ), - ] } - style={ getStyles( - isRootList, - isStackedHorizontally, - horizontalAlignment - ) } - data={ blockClientIds } - keyExtractor={ identity } - listKey={ - rootClientId ? `list-${ rootClientId }` : 'list-root' - } - renderItem={ renderItem } - CellRendererComponent={ BlockListItemCell } - shouldPreventAutomaticScroll={ - shouldFlatListPreventAutomaticScroll - } - title={ title } - ListHeaderComponent={ header } - ListEmptyComponent={ - ! isReadOnly && ( - <EmptyList - orientation={ orientation } - rootClientId={ rootClientId } - renderAppender={ renderAppender } - renderFooterAppender={ renderFooterAppender } - /> - ) - } - ListFooterComponent={ - <Footer - addBlockToEndOfPost={ addBlockToEndOfPost } - isReadOnly={ isReadOnly } - renderFooterAppender={ renderFooterAppender } - withFooter={ withFooter } - /> - } - onScroll={ onScroll } - /> - { shouldShowInnerBlockAppender() && ( - <View - style={ { - marginHorizontal: - marginHorizontal - - styles.innerAppender.marginLeft, - } } - > - <BlockListAppender - rootClientId={ rootClientId } - renderAppender={ renderAppender } - showSeparator - /> - </View> - ) } - </View> - ); - }; - const renderItem = ( { item: clientId, index } ) => { // Extracting the grid item properties here to avoid // re-renders in the blockListItem component. @@ -328,29 +211,159 @@ export default function BlockList( { ); }; - // Use of Context to propagate the main scroll ref to its children e.g InnerBlocks. - const blockList = isRootList ? ( - <BlockListProvider - value={ { - ...DEFAULT_BLOCK_LIST_CONTEXT, - scrollRef: scrollViewRef.current, - } } + const { blockToolbar, headerToolbar, floatingToolbar } = styles; + + const containerStyle = { + flex: isRootList ? 1 : 0, + // We set negative margin in the parent to remove the edge spacing between parent block and child block in ineer blocks. + marginVertical: isRootList ? 0 : -marginVertical, + marginHorizontal: isRootList ? 0 : -marginHorizontal, + }; + + const isContentStretch = contentResizeMode === 'stretch'; + const isMultiBlocks = blockClientIds.length > 1; + const { isWider } = alignmentHelpers; + const extraScrollHeight = + headerToolbar.height + + blockToolbar.height + + ( isFloatingToolbarVisible ? floatingToolbar.height : 0 ); + + return ( + <View + style={ containerStyle } + onAccessibilityEscape={ clearSelectedBlock } + onLayout={ onLayout } + testID="block-list-wrapper" > - <BlockDraggableWrapper isRTL={ isRTL }> - { ( { onScroll } ) => renderList( { onScroll } ) } - </BlockDraggableWrapper> - </BlockListProvider> - ) : ( - <BlockListConsumer> - { ( { scrollRef } ) => - renderList( { - parentScrollRef: scrollRef, - } ) - } - </BlockListConsumer> + { isRootList ? ( + <BlockListProvider + value={ { + ...DEFAULT_BLOCK_LIST_CONTEXT, + scrollRef: scrollViewRef.current, + } } + > + <BlockDraggableWrapper isRTL={ isRTL }> + { ( { onScroll } ) => ( + <KeyboardAwareFlatList + { ...( Platform.OS === 'android' + ? { removeClippedSubviews: false } + : {} ) } // Disable clipping on Android to fix focus losing. See https://github.com/wordpress-mobile/gutenberg-mobile/pull/741#issuecomment-472746541 + accessibilityLabel="block-list" + innerRef={ ( ref ) => { + scrollViewRef.current = ref; + } } + extraScrollHeight={ extraScrollHeight } + keyboardShouldPersistTaps="always" + scrollViewStyle={ { flex: 1 } } + extraData={ getExtraData() } + scrollEnabled={ isRootList } + contentContainerStyle={ [ + horizontal && + styles.horizontalContentContainer, + isWider( blockWidth, 'medium' ) && + ( isContentStretch && isMultiBlocks + ? styles.horizontalContentContainerStretch + : styles.horizontalContentContainerCenter ), + ] } + data={ blockClientIds } + keyExtractor={ identity } + renderItem={ renderItem } + CellRendererComponent={ BlockListItemCell } + shouldPreventAutomaticScroll={ + shouldFlatListPreventAutomaticScroll + } + ListHeaderComponent={ header } + ListEmptyComponent={ + ! isReadOnly && ( + <EmptyList + orientation={ orientation } + rootClientId={ rootClientId } + renderAppender={ renderAppender } + renderFooterAppender={ + renderFooterAppender + } + /> + ) + } + ListFooterComponent={ + <Footer + addBlockToEndOfPost={ + addBlockToEndOfPost + } + isReadOnly={ isReadOnly } + renderFooterAppender={ + renderFooterAppender + } + withFooter={ withFooter } + /> + } + onScroll={ onScroll } + /> + ) } + </BlockDraggableWrapper> + </BlockListProvider> + ) : ( + <> + { blockClientIds.length > 0 ? ( + <View style={ [ { flex: 0 }, styles.overflowVisible ] }> + <View + style={ [ + ...getStyles( + isStackedHorizontally, + horizontalAlignment + ), + horizontal && + styles.horizontalContentContainer, + ] } + > + { blockClientIds.map( + ( currentClientId, index ) => { + return ( + <View key={ currentClientId }> + { renderItem( { + item: currentClientId, + index, + } ) } + </View> + ); + } + ) } + <Footer + addBlockToEndOfPost={ addBlockToEndOfPost } + isReadOnly={ isReadOnly } + renderFooterAppender={ + renderFooterAppender + } + withFooter={ withFooter } + /> + </View> + </View> + ) : ( + <EmptyList + orientation={ orientation } + rootClientId={ rootClientId } + renderAppender={ renderAppender } + renderFooterAppender={ renderFooterAppender } + /> + ) } + </> + ) } + { shouldShowInnerBlockAppender() && ( + <View + style={ { + marginHorizontal: + marginHorizontal - styles.innerAppender.marginLeft, + } } + > + <BlockListAppender + rootClientId={ rootClientId } + renderAppender={ renderAppender } + showSeparator + /> + </View> + ) } + </View> ); - - return blockList; } function Footer( { @@ -375,7 +388,7 @@ function Footer( { </> ); } else if ( renderFooterAppender ) { - return renderFooterAppender(); + return <View>{ renderFooterAppender() }</View>; } return null; diff --git a/packages/block-editor/src/components/block-list/test/index.native.js b/packages/block-editor/src/components/block-list/test/index.native.js index dee27b963d1626..e8d6c8388f1306 100644 --- a/packages/block-editor/src/components/block-list/test/index.native.js +++ b/packages/block-editor/src/components/block-list/test/index.native.js @@ -52,7 +52,7 @@ describe( 'BlockList', () => { await addBlock( screen, 'Social Icons' ); const socialLinksBlock = await getBlock( screen, 'Social Icons' ); fireEvent.press( socialLinksBlock ); - triggerBlockListLayout( socialLinksBlock ); + await triggerBlockListLayout( socialLinksBlock ); // Act fireEvent.press( @@ -82,7 +82,7 @@ describe( 'BlockList', () => { await initializeEditor(); await addBlock( screen, 'Group' ); const groupBlock = await getBlock( screen, 'Group' ); - triggerBlockListLayout( groupBlock ); + await triggerBlockListLayout( groupBlock ); // Assert expect( diff --git a/packages/block-editor/src/components/inner-blocks/index.native.js b/packages/block-editor/src/components/inner-blocks/index.native.js index 3ba3b8e8321a2b..e635230b5c2425 100644 --- a/packages/block-editor/src/components/inner-blocks/index.native.js +++ b/packages/block-editor/src/components/inner-blocks/index.native.js @@ -17,7 +17,6 @@ import useBlockContext from './use-block-context'; * Internal dependencies */ import BlockList from '../block-list'; -import BlockListCompact from '../block-list/block-list-compact'; import { useBlockEditContext } from '../block-edit/context'; import useBlockSync from '../provider/use-block-sync'; import { BlockContextProvider } from '../block-context'; @@ -92,7 +91,6 @@ function UncontrolledInnerBlocks( props ) { blockWidth, __experimentalLayout: layout = defaultLayout, gridProperties, - useCompactList, } = props; const context = useBlockContext( clientId ); @@ -106,12 +104,10 @@ function UncontrolledInnerBlocks( props ) { templateInsertUpdatesSelection ); - const BlockListComponent = useCompactList ? BlockListCompact : BlockList; - return ( <LayoutProvider value={ layout }> <BlockContextProvider value={ context }> - <BlockListComponent + <BlockList marginVertical={ marginVertical } marginHorizontal={ marginHorizontal } rootClientId={ clientId } diff --git a/packages/block-library/src/list-item/edit.native.js b/packages/block-library/src/list-item/edit.native.js index 5f365dd9a5816b..cf2e77c08d2e83 100644 --- a/packages/block-library/src/list-item/edit.native.js +++ b/packages/block-library/src/list-item/edit.native.js @@ -91,7 +91,7 @@ export default function ListItemEdit( { const innerBlocksProps = useInnerBlocksProps( blockProps, { allowedBlocks: [ 'core/list' ], - useCompactList: true, + renderAppender: false, } ); // Set default placeholder text color from light/dark scheme or base colors diff --git a/packages/block-library/src/list/edit.js b/packages/block-library/src/list/edit.js index 2d68aaf909f45e..24d5ead74c47d4 100644 --- a/packages/block-library/src/list/edit.js +++ b/packages/block-library/src/list/edit.js @@ -135,7 +135,7 @@ export default function Edit( { attributes, setAttributes, clientId, style } ) { ...( Platform.isNative && { marginVertical: NATIVE_MARGIN_SPACING, marginHorizontal: NATIVE_MARGIN_SPACING, - useCompactList: true, + renderAppender: false, } ), } ); useMigrateOnLoad( attributes, clientId ); diff --git a/packages/block-library/src/list/test/edit.native.js b/packages/block-library/src/list/test/edit.native.js index 67470228afc63e..5374071f4e8090 100644 --- a/packages/block-library/src/list/test/edit.native.js +++ b/packages/block-library/src/list/test/edit.native.js @@ -70,6 +70,7 @@ describe( 'List block', () => { // Select List block const [ listBlock ] = screen.getAllByLabelText( /List Block\. Row 1/ ); fireEvent.press( listBlock ); + await triggerBlockListLayout( listBlock ); // Select List Item block const [ listItemBlock ] = screen.getAllByLabelText( @@ -117,14 +118,15 @@ describe( 'List block', () => { // Select List block const [ listBlock ] = screen.getAllByLabelText( /List Block\. Row 1/ ); - fireEvent.press( listBlock ); + await triggerBlockListLayout( listBlock ); // Select List Item block const [ firstNestedLevelBlock ] = within( listBlock ).getAllByLabelText( /List item Block\. Row 2/ ); fireEvent.press( firstNestedLevelBlock ); + await triggerBlockListLayout( firstNestedLevelBlock ); // Select second level list const [ secondNestedLevelBlock ] = within( @@ -152,6 +154,7 @@ describe( 'List block', () => { // Select List block const [ listBlock ] = screen.getAllByLabelText( /List Block\. Row 1/ ); fireEvent.press( listBlock ); + await triggerBlockListLayout( listBlock ); // Select Secont List Item block const [ listItemBlock ] = screen.getAllByLabelText( @@ -163,6 +166,12 @@ describe( 'List block', () => { const indentButton = screen.getByLabelText( 'Indent' ); fireEvent.press( indentButton ); + // Await recently indented list item layout + const [ listItemBlock1 ] = screen.getAllByLabelText( + /List item Block\. Row 1/ + ); + await triggerBlockListLayout( listItemBlock1 ); + expect( getEditorHtml() ).toMatchSnapshot(); } ); @@ -184,17 +193,21 @@ describe( 'List block', () => { // Select List block const [ listBlock ] = screen.getAllByLabelText( /List Block\. Row 1/ ); fireEvent.press( listBlock ); + await triggerBlockListLayout( listBlock ); // Select List Item block const [ firstNestedLevelBlock ] = within( listBlock ).getAllByLabelText( /List item Block\. Row 1/ ); fireEvent.press( firstNestedLevelBlock ); + await triggerBlockListLayout( firstNestedLevelBlock ); // Select Inner block List const [ innerBlockList ] = within( firstNestedLevelBlock ).getAllByLabelText( /List Block\. Row 1/ ); + fireEvent.press( innerBlockList ); + await triggerBlockListLayout( innerBlockList ); // Select nested List Item block const [ listItemBlock ] = within( innerBlockList ).getAllByLabelText( @@ -336,6 +349,7 @@ describe( 'List block', () => { // Select List block const [ listBlock ] = screen.getAllByLabelText( /List Block\. Row 2/ ); fireEvent.press( listBlock ); + await triggerBlockListLayout( listBlock ); // Select List Item block const [ listItemBlock ] = within( listBlock ).getAllByLabelText( @@ -384,6 +398,7 @@ describe( 'List block', () => { // Select List block const [ listBlock ] = screen.getAllByLabelText( /List Block\. Row 2/ ); fireEvent.press( listBlock ); + await triggerBlockListLayout( listBlock ); // Select List Item block const [ listItemBlock ] = within( listBlock ).getAllByLabelText( diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index 433c3cd706c9cc..4c0b7249a8e77e 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -547,10 +547,6 @@ class EditorPage { : '//XCUIElementTypeButton'; const blockActionsMenuButtonIdentifier = `Open Block Actions Menu`; const blockActionsMenuButtonLocator = `${ buttonElementName }[contains(@${ this.accessibilityIdXPathAttrib }, "${ blockActionsMenuButtonIdentifier }")]`; - const blockActionsMenuButton = await waitForVisible( - this.driver, - blockActionsMenuButtonLocator - ); if ( isAndroid() ) { const block = await this.getBlockAtPosition( blockName, position ); let checkList = await this.driver.elementsByXPath( @@ -564,6 +560,10 @@ class EditorPage { } } + const blockActionsMenuButton = await waitForVisible( + this.driver, + blockActionsMenuButtonLocator + ); await blockActionsMenuButton.click(); const removeActionButtonIdentifier = 'Remove block'; const removeActionButtonLocator = `${ buttonElementName }[contains(@${ this.accessibilityIdXPathAttrib }, "${ removeActionButtonIdentifier }")]`; @@ -911,7 +911,7 @@ class EditorPage { async addButtonWithInlineAppender( position = 1 ) { const appenderButton = isAndroid() ? await this.waitForElementToBeDisplayedByXPath( - `//android.view.ViewGroup[@content-desc="block-list"]/android.view.ViewGroup[${ position }]/android.widget.Button` + `//android.widget.Button[@content-desc="Buttons Block. Row 1"]/android.view.ViewGroup/android.view.ViewGroup[1]/android.widget.Button[${ position }]` ) : await this.waitForElementToBeDisplayedById( 'appender-button' ); await appenderButton.click(); From 07b87a100da825051c5d5d26cf780c1e529c8f2d Mon Sep 17 00:00:00 2001 From: Siobhan Bamber <siobhan@automattic.com> Date: Fri, 12 May 2023 13:30:34 +0100 Subject: [PATCH 021/131] feat: Add disabled prop to SwitchCell component (#50560) --- .../src/mobile/bottom-sheet/switch-cell.native.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/components/src/mobile/bottom-sheet/switch-cell.native.js b/packages/components/src/mobile/bottom-sheet/switch-cell.native.js index 9dcaf794df750b..53b804bedfb7b2 100644 --- a/packages/components/src/mobile/bottom-sheet/switch-cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/switch-cell.native.js @@ -13,7 +13,7 @@ import { __, _x, sprintf } from '@wordpress/i18n'; import Cell from './cell'; export default function BottomSheetSwitchCell( props ) { - const { value, onValueChange, ...cellProps } = props; + const { value, onValueChange, disabled, ...cellProps } = props; const onPress = () => { onValueChange( ! value ); @@ -60,8 +60,13 @@ export default function BottomSheetSwitchCell( props ) { onPress={ onPress } editable={ false } value={ '' } + disabled={ disabled } > - <Switch value={ value } onValueChange={ onValueChange } /> + <Switch + value={ value } + onValueChange={ onValueChange } + disabled={ disabled } + /> </Cell> ); } From f6291fc399dbe1a373a1eaf60a01296cd4670bd8 Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco <gerardo.pacheco@automattic.com> Date: Fri, 12 May 2023 15:31:58 +0200 Subject: [PATCH 022/131] [Mobile] EmptyList now uses `useEditorWrapperStyles` hook and removes `ReadableContentView` (#50552) * Mobile - BlockList - EmptyList: Use useEditorWrapperStyles hook instead of ReadableContentView * Mobile - Remove ReadableContentView component --- .../src/components/block-list/index.native.js | 24 +++--- packages/components/src/index.native.js | 1 - .../readable-content-view/index.native.js | 85 ------------------- .../readable-content-view/style.native.scss | 30 ------- 4 files changed, 11 insertions(+), 129 deletions(-) delete mode 100644 packages/components/src/mobile/readable-content-view/index.native.js delete mode 100644 packages/components/src/mobile/readable-content-view/style.native.scss diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 587d03dd882542..da367ecc0ebea6 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -11,7 +11,6 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { createBlock } from '@wordpress/blocks'; import { KeyboardAwareFlatList, - ReadableContentView, WIDE_ALIGNMENTS, alignmentHelpers, } from '@wordpress/components'; @@ -29,6 +28,7 @@ import { DEFAULT_BLOCK_LIST_CONTEXT, } from './block-list-context'; import { BlockDraggableWrapper } from '../block-draggable'; +import { useEditorWrapperStyles } from '../../hooks/use-editor-wrapper-styles'; import { store as blockEditorStore } from '../../store'; const identity = ( x ) => x; @@ -423,24 +423,22 @@ function EmptyList( { ! blockClientIds[ insertionPoint.index ] ), }; } ); + const align = renderAppender ? WIDE_ALIGNMENTS.alignments.full : undefined; + const [ wrapperStyles ] = useEditorWrapperStyles( { align } ); if ( renderFooterAppender || renderAppender === false ) { return null; } + const containerStyles = [ styles.defaultAppender, wrapperStyles ]; + return ( - <View style={ styles.defaultAppender }> - <ReadableContentView - align={ - renderAppender ? WIDE_ALIGNMENTS.alignments.full : undefined - } - > - <BlockListAppender - rootClientId={ rootClientId } - renderAppender={ renderAppender } - showSeparator={ shouldShowInsertionPoint } - /> - </ReadableContentView> + <View style={ containerStyles }> + <BlockListAppender + rootClientId={ rootClientId } + renderAppender={ renderAppender } + showSeparator={ shouldShowInsertionPoint } + /> </View> ); } diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 76a750343418c5..9a66a62f98e187 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -94,7 +94,6 @@ export { default as HTMLTextInput } from './mobile/html-text-input'; export { default as KeyboardAvoidingView } from './mobile/keyboard-avoiding-view'; export { default as KeyboardAwareFlatList } from './mobile/keyboard-aware-flat-list'; export { default as Picker } from './mobile/picker'; -export { default as ReadableContentView } from './mobile/readable-content-view'; export { default as CycleSelectControl } from './mobile/cycle-select-control'; export { default as Gradient } from './mobile/gradient'; export { default as ColorSettings } from './mobile/color-settings'; diff --git a/packages/components/src/mobile/readable-content-view/index.native.js b/packages/components/src/mobile/readable-content-view/index.native.js deleted file mode 100644 index 59cdd224c81ca4..00000000000000 --- a/packages/components/src/mobile/readable-content-view/index.native.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * External dependencies - */ -import { View, Dimensions } from 'react-native'; - -/** - * WordPress dependencies - */ -import { useState, useEffect } from '@wordpress/element'; -import { ALIGNMENT_BREAKPOINTS, WIDE_ALIGNMENTS } from '@wordpress/components'; -/** - * Internal dependencies - */ -import styles from './style.scss'; - -const PIXEL_RATIO = 2; - -const ReadableContentView = ( { align, reversed, children, style } ) => { - const { width, height } = Dimensions.get( 'window' ); - const [ windowWidth, setWindowWidth ] = useState( width ); - const [ windowRatio, setWindowRatio ] = useState( width / height ); - - function onDimensionsChange( { window } ) { - setWindowWidth( window.width ); - setWindowRatio( window.width / window.height ); - } - - useEffect( () => { - const dimensionsChangeSubscription = Dimensions.addEventListener( - 'change', - onDimensionsChange - ); - - return () => { - dimensionsChangeSubscription.remove(); - }; - }, [] ); - - function getWideStyles() { - if ( - windowRatio >= PIXEL_RATIO && - windowWidth < ALIGNMENT_BREAKPOINTS.large - ) { - return styles.wideLandscape; - } - - if ( windowWidth <= ALIGNMENT_BREAKPOINTS.small ) { - return { maxWidth: windowWidth }; - } - - if ( - windowWidth >= ALIGNMENT_BREAKPOINTS.medium && - windowWidth < ALIGNMENT_BREAKPOINTS.wide - ) { - return styles.wideMedium; - } - } - - return ( - <View style={ styles.container }> - <View - style={ [ - reversed - ? styles.reversedCenteredContent - : styles.centeredContent, - style, - styles[ align ], - align === WIDE_ALIGNMENTS.alignments.wide && - getWideStyles(), - ] } - > - { children } - </View> - </View> - ); -}; - -const isContentMaxWidth = () => { - const { width } = Dimensions.get( 'window' ); - return width > styles.centeredContent.maxWidth; -}; - -ReadableContentView.isContentMaxWidth = isContentMaxWidth; - -export default ReadableContentView; diff --git a/packages/components/src/mobile/readable-content-view/style.native.scss b/packages/components/src/mobile/readable-content-view/style.native.scss deleted file mode 100644 index 2560d7ac6b3e1c..00000000000000 --- a/packages/components/src/mobile/readable-content-view/style.native.scss +++ /dev/null @@ -1,30 +0,0 @@ -.container { - align-items: center; -} - -.centeredContent { - width: 100%; - max-width: 580; -} - -.reversedCenteredContent { - flex-direction: column-reverse; - width: 100%; - max-width: 580; -} - -.full { - max-width: 100%; -} - -.wide { - max-width: 1054; -} - -.wideMedium { - max-width: 770; -} - -.wideLandscape { - max-width: 662; -} From 6606da447be0ba0885573add567dbb97b42c8ed2 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Fri, 12 May 2023 16:48:51 +0300 Subject: [PATCH 023/131] Block Library: Remove unnecessary lodash mock from Buttons tests (#50588) --- packages/block-library/src/buttons/test/edit.native.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/block-library/src/buttons/test/edit.native.js b/packages/block-library/src/buttons/test/edit.native.js index ff79be61b92909..5ba97e7bb4caf3 100644 --- a/packages/block-library/src/buttons/test/edit.native.js +++ b/packages/block-library/src/buttons/test/edit.native.js @@ -19,15 +19,6 @@ import { import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks'; import { registerCoreBlocks } from '@wordpress/block-library'; -// Mock debounce to prevent potentially belated state updates. -jest.mock( 'lodash', () => ( { - ...jest.requireActual( 'lodash' ), - debounce: ( fn ) => { - fn.cancel = jest.fn(); - return fn; - }, -} ) ); - const BUTTONS_HTML = `<!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button /--></div> <!-- /wp:buttons -->`; From 831a536c8ca60d8d8d84380052761a4609c4c1d4 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Fri, 12 May 2023 17:06:37 +0300 Subject: [PATCH 024/131] Lodash: Remove from template part block (#50586) * Lodash: Remove from template part block * Fix logic --- packages/block-library/src/template-part/edit/index.js | 10 ++++------ .../src/template-part/edit/utils/hooks.js | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/block-library/src/template-part/edit/index.js b/packages/block-library/src/template-part/edit/index.js index 667d78dbfebbf7..b7ae4f6043afb1 100644 --- a/packages/block-library/src/template-part/edit/index.js +++ b/packages/block-library/src/template-part/edit/index.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { isEmpty } from 'lodash'; - /** * WordPress dependencies */ @@ -75,7 +70,10 @@ export default function TemplatePartEdit( { return { innerBlocks: getBlocks( clientId ), isResolved: hasResolvedEntity, - isMissing: hasResolvedEntity && isEmpty( entityRecord ), + isMissing: + hasResolvedEntity && + ( ! entityRecord || + Object.keys( entityRecord ).length === 0 ), area: _area, }; }, diff --git a/packages/block-library/src/template-part/edit/utils/hooks.js b/packages/block-library/src/template-part/edit/utils/hooks.js index c28b138dd3df6f..39daa4080c8160 100644 --- a/packages/block-library/src/template-part/edit/utils/hooks.js +++ b/packages/block-library/src/template-part/edit/utils/hooks.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { kebabCase } from 'lodash'; +import { paramCase as kebabCase } from 'change-case'; /** * WordPress dependencies From 298df3a5d013ae240fd72068787b9bb83875f99d Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Fri, 12 May 2023 17:06:54 +0300 Subject: [PATCH 025/131] Lodash: Remove from Media & Text block (#50587) --- packages/block-library/src/media-text/deprecated.js | 9 ++++----- packages/block-library/src/media-text/save.js | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/block-library/src/media-text/deprecated.js b/packages/block-library/src/media-text/deprecated.js index 2d061213dface8..93366f827055ba 100644 --- a/packages/block-library/src/media-text/deprecated.js +++ b/packages/block-library/src/media-text/deprecated.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import { isEmpty } from 'lodash'; /** * WordPress dependencies @@ -258,7 +257,7 @@ const v6 = { } = attributes; const mediaSizeSlug = attributes.mediaSizeSlug || DEFAULT_MEDIA_SIZE_SLUG; - const newRel = isEmpty( rel ) ? undefined : rel; + const newRel = ! rel ? undefined : rel; const imageClasses = classnames( { [ `wp-image-${ mediaId }` ]: mediaId && mediaType === 'image', @@ -387,7 +386,7 @@ const v5 = { } = attributes; const mediaSizeSlug = attributes.mediaSizeSlug || DEFAULT_MEDIA_SIZE_SLUG; - const newRel = isEmpty( rel ) ? undefined : rel; + const newRel = ! rel ? undefined : rel; const imageClasses = classnames( { [ `wp-image-${ mediaId }` ]: mediaId && mediaType === 'image', @@ -501,7 +500,7 @@ const v4 = { } = attributes; const mediaSizeSlug = attributes.mediaSizeSlug || DEFAULT_MEDIA_SIZE_SLUG; - const newRel = isEmpty( rel ) ? undefined : rel; + const newRel = ! rel ? undefined : rel; const imageClasses = classnames( { [ `wp-image-${ mediaId }` ]: mediaId && mediaType === 'image', @@ -646,7 +645,7 @@ const v3 = { linkTarget, rel, } = attributes; - const newRel = isEmpty( rel ) ? undefined : rel; + const newRel = ! rel ? undefined : rel; let image = ( <img diff --git a/packages/block-library/src/media-text/save.js b/packages/block-library/src/media-text/save.js index 9c8607278cbe9b..0a2b1ab3d3946c 100644 --- a/packages/block-library/src/media-text/save.js +++ b/packages/block-library/src/media-text/save.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import { isEmpty } from 'lodash'; /** * WordPress dependencies @@ -36,7 +35,7 @@ export default function save( { attributes } ) { rel, } = attributes; const mediaSizeSlug = attributes.mediaSizeSlug || DEFAULT_MEDIA_SIZE_SLUG; - const newRel = isEmpty( rel ) ? undefined : rel; + const newRel = ! rel ? undefined : rel; const imageClasses = classnames( { [ `wp-image-${ mediaId }` ]: mediaId && mediaType === 'image', From 7250336918afb258e78c9f05cde72187b5e7b6e2 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Fri, 12 May 2023 17:38:21 +0300 Subject: [PATCH 026/131] Lodash: Remove from Latest Posts block (#50593) --- packages/block-library/src/latest-posts/edit.native.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/block-library/src/latest-posts/edit.native.js b/packages/block-library/src/latest-posts/edit.native.js index c5b9f644d7cbdb..c860cadc6b2144 100644 --- a/packages/block-library/src/latest-posts/edit.native.js +++ b/packages/block-library/src/latest-posts/edit.native.js @@ -2,7 +2,6 @@ * External dependencies */ import { TouchableWithoutFeedback, View, Text } from 'react-native'; -import { isEmpty } from 'lodash'; /** * WordPress dependencies @@ -63,9 +62,7 @@ class LatestPostsEdit extends Component { .then( ( categoriesList ) => { if ( this.isStillMounted ) { this.setState( { - categoriesList: isEmpty( categoriesList ) - ? [] - : categoriesList, + categoriesList, } ); } } ) From 99c3dda88168b262a65ceb33cd6dbce6c5e02b40 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Fri, 12 May 2023 17:41:28 +0300 Subject: [PATCH 027/131] Lodash: Remove from Image block (#50592) --- .../block-library/src/image/deprecated.js | 5 ++-- packages/block-library/src/image/edit.js | 5 ++-- packages/block-library/src/image/image.js | 8 ++--- packages/block-library/src/image/save.js | 7 +++-- packages/block-library/src/image/utils.js | 29 +++++++------------ 5 files changed, 22 insertions(+), 32 deletions(-) diff --git a/packages/block-library/src/image/deprecated.js b/packages/block-library/src/image/deprecated.js index 9f5aaee4f7d21a..9b7a41cab188de 100644 --- a/packages/block-library/src/image/deprecated.js +++ b/packages/block-library/src/image/deprecated.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import { isEmpty } from 'lodash'; /** * WordPress dependencies @@ -122,7 +121,7 @@ const deprecated = [ title, } = attributes; - const newRel = isEmpty( rel ) ? undefined : rel; + const newRel = ! rel ? undefined : rel; const classes = classnames( { [ `align${ align }` ]: align, @@ -202,7 +201,7 @@ const deprecated = [ title, } = attributes; - const newRel = isEmpty( rel ) ? undefined : rel; + const newRel = ! rel ? undefined : rel; const classes = classnames( { [ `align${ align }` ]: align, diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 19e8196dfc7a51..c4d8de316ea997 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import { isEmpty } from 'lodash'; /** * WordPress dependencies @@ -320,7 +319,9 @@ export function ImageEdit( { 'is-resized': !! width || !! height, [ `size-${ sizeSlug }` ]: sizeSlug, 'has-custom-border': - !! borderProps.className || ! isEmpty( borderProps.style ), + !! borderProps.className || + ( borderProps.style && + Object.keys( borderProps.style ).length > 0 ), } ); const blockProps = useBlockProps( { diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index c513ede8b9fe29..abc65f7022acd3 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { isEmpty } from 'lodash'; - /** * WordPress dependencies */ @@ -480,7 +475,8 @@ export default function Image( { const borderProps = useBorderProps( attributes ); const isRounded = attributes.className?.includes( 'is-style-rounded' ); const hasCustomBorder = - !! borderProps.className || ! isEmpty( borderProps.style ); + !! borderProps.className || + ( borderProps.style && Object.keys( borderProps.style ).length > 0 ); let img = ( // Disable reason: Image itself is not meant to be interactive, but diff --git a/packages/block-library/src/image/save.js b/packages/block-library/src/image/save.js index 872ea980d8cf84..d0fd5ef3d6f98b 100644 --- a/packages/block-library/src/image/save.js +++ b/packages/block-library/src/image/save.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import { isEmpty } from 'lodash'; /** * WordPress dependencies @@ -31,7 +30,7 @@ export default function save( { attributes } ) { title, } = attributes; - const newRel = isEmpty( rel ) ? undefined : rel; + const newRel = ! rel ? undefined : rel; const borderProps = getBorderClassesAndStyles( attributes ); const classes = classnames( { @@ -39,7 +38,9 @@ export default function save( { attributes } ) { [ `size-${ sizeSlug }` ]: sizeSlug, 'is-resized': width || height, 'has-custom-border': - !! borderProps.className || ! isEmpty( borderProps.style ), + !! borderProps.className || + ( borderProps.style && + Object.keys( borderProps.style ).length > 0 ), } ); const imageClasses = classnames( borderProps.className, { diff --git a/packages/block-library/src/image/utils.js b/packages/block-library/src/image/utils.js index 71e44517517f36..839628fa978b00 100644 --- a/packages/block-library/src/image/utils.js +++ b/packages/block-library/src/image/utils.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { isEmpty } from 'lodash'; - /** * Internal dependencies */ @@ -11,21 +6,19 @@ import { NEW_TAB_REL } from './constants'; export function removeNewTabRel( currentRel ) { let newRel = currentRel; - if ( currentRel !== undefined && ! isEmpty( newRel ) ) { - if ( ! isEmpty( newRel ) ) { - NEW_TAB_REL.forEach( ( relVal ) => { - const regExp = new RegExp( '\\b' + relVal + '\\b', 'gi' ); - newRel = newRel.replace( regExp, '' ); - } ); + if ( currentRel !== undefined && newRel ) { + NEW_TAB_REL.forEach( ( relVal ) => { + const regExp = new RegExp( '\\b' + relVal + '\\b', 'gi' ); + newRel = newRel.replace( regExp, '' ); + } ); - // Only trim if NEW_TAB_REL values was replaced. - if ( newRel !== currentRel ) { - newRel = newRel.trim(); - } + // Only trim if NEW_TAB_REL values was replaced. + if ( newRel !== currentRel ) { + newRel = newRel.trim(); + } - if ( isEmpty( newRel ) ) { - newRel = undefined; - } + if ( ! newRel ) { + newRel = undefined; } } From f32c9a4700a6e72c67806b3d6012bdc1ab36bc1f Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Sat, 13 May 2023 11:51:02 +1000 Subject: [PATCH 028/131] This commit: (#50563) - adds an `_edit_link` property to `wp_global_styles`, `wp_template`, and `wp_template-part` custom post type schemata via filter - uses the `_edit_link` value to create an edit link via the `get_edit_post_link` hook --- lib/compat/wordpress-6.3/link-template.php | 34 ++++++++++++++++++++++ lib/compat/wordpress-6.3/rest-api.php | 20 +++++++++++-- lib/load.php | 1 + 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 lib/compat/wordpress-6.3/link-template.php diff --git a/lib/compat/wordpress-6.3/link-template.php b/lib/compat/wordpress-6.3/link-template.php new file mode 100644 index 00000000000000..366dedba2b5aaf --- /dev/null +++ b/lib/compat/wordpress-6.3/link-template.php @@ -0,0 +1,34 @@ +<?php +/** + * Overrides Core's wp-includes/link-template.php for WP 6.3. + * + * @package gutenberg + */ + +/** + * Updates the post edit link using the `_edit_link` property in wp_global_styles`, `wp_template`, + * and `wp_template_part` custom post types. + * + * `_edit_link` for these custom post types is added by `gutenberg_update_templates_template_parts_rest_controller()` + * in lib/compat/wordpress-6.3/rest-api.php. + * + * This functionality has already been ported to Core. See https://github.com/WordPress/gutenberg/issues/48065 + * The following hook is a modified version that passes only 2 arguments to `sprintf()` to be compatible with WP <= 6.2. + * + * @param string $link The edit link. + * @param int $post_id Post ID. + * @return string|null The edit post link for the given post. Null if the post type does not exist + * or does not allow an editing UI. + */ +function gutenberg_update_get_edit_post_link( $link, $post_id ) { + $post = get_post( $post_id ); + + if ( 'wp_template' === $post->post_type || 'wp_template_part' === $post->post_type ) { + $post_type_object = get_post_type_object( $post->post_type ); + $slug = urlencode( get_stylesheet() . '//' . $post->post_name ); + $link = admin_url( sprintf( $post_type_object->_edit_link, $slug ) ); + } + return $link; +} + +add_filter( 'get_edit_post_link', 'gutenberg_update_get_edit_post_link', 10, 2 ); diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php index e111980887ef5a..757bd317dcd548 100644 --- a/lib/compat/wordpress-6.3/rest-api.php +++ b/lib/compat/wordpress-6.3/rest-api.php @@ -15,21 +15,35 @@ function gutenberg_register_rest_pattern_directory() { add_action( 'rest_api_init', 'gutenberg_register_rest_pattern_directory' ); /** - * Update `wp_template` and `wp_template-part` post types to use - * Gutenberg's REST controller. + * Updates `wp_template` and `wp_template_part` post types to use + * Gutenberg's REST controllers + * + * Adds `_edit_link` to the `wp_global_styles`, `wp_template`, + * and `wp_template_part` post type schemata. See https://github.com/WordPress/gutenberg/issues/48065 * * @param array $args Array of arguments for registering a post type. * @param string $post_type Post type key. */ function gutenberg_update_templates_template_parts_rest_controller( $args, $post_type ) { if ( in_array( $post_type, array( 'wp_template', 'wp_template_part' ), true ) ) { + $template_edit_link = 'site-editor.php?' . build_query( + array( + 'postType' => $post_type, + 'postId' => '%s', + 'canvas' => 'edit', + ) + ); + $args['_edit_link'] = $template_edit_link; $args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller_6_3'; } + + if ( in_array( $post_type, array( 'wp_global_styles' ), true ) ) { + $args['_edit_link'] = '/site-editor.php?canvas=edit'; + } return $args; } add_filter( 'register_post_type_args', 'gutenberg_update_templates_template_parts_rest_controller', 10, 2 ); - /** * Registers the Global Styles Revisions REST API routes. */ diff --git a/lib/load.php b/lib/load.php index 8883f66c267941..31bf94d06a4641 100644 --- a/lib/load.php +++ b/lib/load.php @@ -50,6 +50,7 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/compat/wordpress-6.3/rest-api.php'; require_once __DIR__ . '/compat/wordpress-6.3/theme-previews.php'; require_once __DIR__ . '/compat/wordpress-6.3/navigation-block-preloading.php'; + require_once __DIR__ . '/compat/wordpress-6.3/link-template.php'; // Experimental. if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) { From 88530161172af2ee7c6aec0b06cd12bf0f34e9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Sat, 13 May 2023 07:00:46 +0200 Subject: [PATCH 029/131] Plugin: Fix building plugin zip to include interactive blocks (#50598) --- bin/build-plugin-zip.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/build-plugin-zip.sh b/bin/build-plugin-zip.sh index 4ba931c4a4aeb6..52d473cd8d4016 100755 --- a/bin/build-plugin-zip.sh +++ b/bin/build-plugin-zip.sh @@ -83,6 +83,7 @@ build_files=$( build/block-library/blocks/*.php \ build/block-library/blocks/*/block.json \ build/block-library/blocks/*/*.{js,js.map,css,asset.php} \ + build/block-library/interactive-blocks/*.js \ build/edit-widgets/blocks/*/block.json \ build/widgets/blocks/*.php \ build/widgets/blocks/*/block.json \ From 4af25a14e9d46fbdaca643df22417b068f0894c1 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Mon, 15 May 2023 08:40:57 +1000 Subject: [PATCH 030/131] Navigation: Fix warning when stretch justification is used (#50568) --- packages/block-library/src/navigation/index.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 37c760a30f959a..5756724bfa7182 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -470,7 +470,10 @@ function render_block_core_navigation( $attributes, $content, $block ) { // Restore legacy classnames for submenu positioning. $layout_class = ''; - if ( isset( $attributes['layout']['justifyContent'] ) ) { + if ( + isset( $attributes['layout']['justifyContent'] ) && + isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) + ) { $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; } if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { From 91dce9598086e0675f743ecb80d9fd3e786859be Mon Sep 17 00:00:00 2001 From: Glen Davies <glendaviesnz@users.noreply.github.com> Date: Mon, 15 May 2023 13:45:00 +1200 Subject: [PATCH 031/131] Pattern block: Add experimental flag and syncStatus attrib to allow development of partial syncing (#50533) --- docs/reference-guides/core-blocks.md | 2 +- lib/experimental/editor-settings.php | 3 + lib/experiments-page.php | 12 ++++ packages/block-library/src/pattern/block.json | 4 ++ packages/block-library/src/pattern/edit.js | 65 ++++++++++++++----- packages/block-library/src/pattern/index.js | 9 +-- packages/block-library/src/pattern/index.php | 14 +++- packages/block-library/src/pattern/v1/edit.js | 51 +++++++++++++++ 8 files changed, 137 insertions(+), 23 deletions(-) create mode 100644 packages/block-library/src/pattern/v1/edit.js diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index e5d20bb7b1edd5..04710602d28f0b 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -465,7 +465,7 @@ Show a block pattern. ([Source](https://github.com/WordPress/gutenberg/tree/trun - **Name:** core/pattern - **Category:** theme - **Supports:** ~~html~~, ~~inserter~~ -- **Attributes:** slug +- **Attributes:** slug, syncStatus ## Post Author diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index c7dd5850a505c3..8d4046530fbc95 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -101,6 +101,9 @@ function gutenberg_enable_experiments() { if ( $gutenberg_experiments && array_key_exists( 'gutenberg-theme-previews', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableThemePreviews = true', 'before' ); } + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-pattern-enhancements', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnablePatternEnhancements = true', 'before' ); + } } diff --git a/lib/experiments-page.php b/lib/experiments-page.php index ee51f5bea49f96..e39a9dbefb9c16 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -125,6 +125,18 @@ function gutenberg_initialize_experiments_settings() { ) ); + add_settings_field( + 'gutenberg-pattern-enhancements', + __( 'Pattern enhancements', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Test the Pattern block enhancements', 'gutenberg' ), + 'id' => 'gutenberg-pattern-enhancements', + ) + ); + register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/packages/block-library/src/pattern/block.json b/packages/block-library/src/pattern/block.json index da023142403c87..82372fe1680984 100644 --- a/packages/block-library/src/pattern/block.json +++ b/packages/block-library/src/pattern/block.json @@ -13,6 +13,10 @@ "attributes": { "slug": { "type": "string" + }, + "syncStatus": { + "type": [ "string", "boolean" ], + "enum": [ "full", "partial" ] } } } diff --git a/packages/block-library/src/pattern/edit.js b/packages/block-library/src/pattern/edit.js index be0b778eb4ae19..5072d577172b0a 100644 --- a/packages/block-library/src/pattern/edit.js +++ b/packages/block-library/src/pattern/edit.js @@ -1,31 +1,41 @@ /** * WordPress dependencies */ +import { cloneBlock } from '@wordpress/blocks'; import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; import { store as blockEditorStore, useBlockProps, + useInnerBlocksProps, } from '@wordpress/block-editor'; const PatternEdit = ( { attributes, clientId } ) => { - const selectedPattern = useSelect( - ( select ) => - select( blockEditorStore ).__experimentalGetParsedPattern( - attributes.slug - ), - [ attributes.slug ] + const { slug, syncStatus } = attributes; + const { selectedPattern, innerBlocks } = useSelect( + ( select ) => { + return { + selectedPattern: + select( blockEditorStore ).__experimentalGetParsedPattern( + slug + ), + innerBlocks: + select( blockEditorStore ).getBlock( clientId ) + ?.innerBlocks, + }; + }, + [ slug, clientId ] ); - - const { replaceBlocks, __unstableMarkNextChangeAsNotPersistent } = - useDispatch( blockEditorStore ); + const { + replaceBlocks, + replaceInnerBlocks, + __unstableMarkNextChangeAsNotPersistent, + } = useDispatch( blockEditorStore ); // Run this effect when the component loads. // This adds the Pattern's contents to the post. - // This change won't be saved. - // It will continue to pull from the pattern file unless changes are made to its respective template part. useEffect( () => { - if ( selectedPattern?.blocks ) { + if ( selectedPattern?.blocks && ! innerBlocks?.length ) { // We batch updates to block list settings to avoid triggering cascading renders // for each container block included in a tree and optimize initial render. // Since the above uses microtasks, we need to use a microtask here as well, @@ -33,14 +43,39 @@ const PatternEdit = ( { attributes, clientId } ) => { // inner blocks but doesn't have blockSettings in the state. window.queueMicrotask( () => { __unstableMarkNextChangeAsNotPersistent(); + if ( syncStatus === 'partial' ) { + replaceInnerBlocks( + clientId, + selectedPattern.blocks.map( ( block ) => + cloneBlock( block ) + ) + ); + return; + } replaceBlocks( clientId, selectedPattern.blocks ); } ); } - }, [ clientId, selectedPattern?.blocks ] ); + }, [ + clientId, + selectedPattern?.blocks, + replaceInnerBlocks, + __unstableMarkNextChangeAsNotPersistent, + innerBlocks, + syncStatus, + replaceBlocks, + ] ); + + const blockProps = useBlockProps(); + + const innerBlocksProps = useInnerBlocksProps( blockProps, { + templateLock: syncStatus === 'partial' ? 'contentOnly' : false, + } ); - const props = useBlockProps(); + if ( syncStatus !== 'partial' ) { + return <div { ...blockProps } />; + } - return <div { ...props } />; + return <div { ...innerBlocksProps } />; }; export default PatternEdit; diff --git a/packages/block-library/src/pattern/index.js b/packages/block-library/src/pattern/index.js index e4af712da8bb29..27e74510eb5972 100644 --- a/packages/block-library/src/pattern/index.js +++ b/packages/block-library/src/pattern/index.js @@ -3,13 +3,14 @@ */ import initBlock from '../utils/init-block'; import metadata from './block.json'; -import PatternEdit from './edit'; +import PatternEditV1 from './v1/edit'; +import PatternEditV2 from './edit'; const { name } = metadata; export { metadata, name }; -export const settings = { - edit: PatternEdit, -}; +export const settings = window?.__experimentalEnablePatternEnhancements + ? { edit: PatternEditV2 } + : { edit: PatternEditV1 }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/pattern/index.php b/packages/block-library/src/pattern/index.php index 32a08601ca8089..4af986c423d012 100644 --- a/packages/block-library/src/pattern/index.php +++ b/packages/block-library/src/pattern/index.php @@ -22,14 +22,22 @@ function register_block_core_pattern() { /** * Renders the `core/pattern` block on the server. * - * @param array $attributes Block attributes. + * @param array $attributes Block attributes. + * @param string $content The block rendered content. * * @return string Returns the output of the pattern. */ -function render_block_core_pattern( $attributes ) { +function render_block_core_pattern( $attributes, $content ) { if ( empty( $attributes['slug'] ) ) { return ''; } + $slug_classname = str_replace( '/', '-', $attributes['slug'] ); + $classnames = isset( $attributes['className'] ) ? $attributes['className'] . ' ' . $slug_classname : $slug_classname; + $wrapper = '<div class="' . esc_attr( $classnames ) . '">%s</div>'; + + if ( isset( $attributes['syncStatus'] ) && 'unsynced' === $attributes['syncStatus'] ) { + return sprintf( $wrapper, $content ); + } $slug = $attributes['slug']; $registry = WP_Block_Patterns_Registry::get_instance(); @@ -38,7 +46,7 @@ function render_block_core_pattern( $attributes ) { } $pattern = $registry->get_registered( $slug ); - return do_blocks( $pattern['content'] ); + return sprintf( $wrapper, do_blocks( $pattern['content'] ) ); } add_action( 'init', 'register_block_core_pattern' ); diff --git a/packages/block-library/src/pattern/v1/edit.js b/packages/block-library/src/pattern/v1/edit.js new file mode 100644 index 00000000000000..aa475809ccb44f --- /dev/null +++ b/packages/block-library/src/pattern/v1/edit.js @@ -0,0 +1,51 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; +import { + store as blockEditorStore, + useBlockProps, +} from '@wordpress/block-editor'; + +const PatternEdit = ( { attributes, clientId } ) => { + const selectedPattern = useSelect( + ( select ) => + select( blockEditorStore ).__experimentalGetParsedPattern( + attributes.slug + ), + [ attributes.slug ] + ); + + const { replaceBlocks, __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); + + // Run this effect when the component loads. + // This adds the Pattern's contents to the post. + // This change won't be saved. + // It will continue to pull from the pattern file unless changes are made to its respective template part. + useEffect( () => { + if ( selectedPattern?.blocks ) { + // We batch updates to block list settings to avoid triggering cascading renders + // for each container block included in a tree and optimize initial render. + // Since the above uses microtasks, we need to use a microtask here as well, + // because nested pattern blocks cannot be inserted if the parent block supports + // inner blocks but doesn't have blockSettings in the state. + window.queueMicrotask( () => { + __unstableMarkNextChangeAsNotPersistent(); + replaceBlocks( clientId, selectedPattern.blocks ); + } ); + } + }, [ + clientId, + selectedPattern?.blocks, + __unstableMarkNextChangeAsNotPersistent, + replaceBlocks, + ] ); + + const props = useBlockProps(); + + return <div { ...props } />; +}; + +export default PatternEdit; From 596838f7eaa5c8621f1b49df7dd87a096c44064d Mon Sep 17 00:00:00 2001 From: Brian Coords <bacoords@gmail.com> Date: Sun, 14 May 2023 19:11:32 -0700 Subject: [PATCH 032/131] Update README.md to include a link to the docs (#50606) --- packages/icons/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/icons/README.md b/packages/icons/README.md index 3c692db7539e16..db4764cfa92b20 100644 --- a/packages/icons/README.md +++ b/packages/icons/README.md @@ -26,6 +26,10 @@ import { Icon, check } from '@wordpress/icons'; | ------ | --------- | ------- | ----------------------- | | `size` | `integer` | `24` | Size of icon in pixels. | +## Docs & Examples + +You can browse the icons docs and examples at [https://wordpress.github.io/gutenberg/?path=/docs/icons-icon--default](https://wordpress.github.io/gutenberg/?path=/docs/icons-icon--default) + ## Contributing to this package This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. From a9fbca537cbbc3279ddda04e8c236405dcdbecb5 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Mon, 15 May 2023 12:41:24 +1000 Subject: [PATCH 033/131] List View: Ensure settings menu is visible when focused (#50572) * List View: Ensure settings menu is visible when focused * Update comment --- packages/block-editor/src/components/list-view/style.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index d5a39c53751a23..c2c35f78e8dfce 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -221,8 +221,9 @@ opacity: 0; } - // Show on hover, visible, and show above to keep the hit area size. + // Show on hover, visible, when focused, and show above to keep the hit area size. &:hover, + &:focus-within, &.is-visible { > * { opacity: 1; From 024d0043c537512534201f6017bc8c6ac8975bd3 Mon Sep 17 00:00:00 2001 From: Glen Davies <glendaviesnz@users.noreply.github.com> Date: Mon, 15 May 2023 14:42:01 +1200 Subject: [PATCH 034/131] Prevent stacking of the entity revert and site save notices (#50302) --- .../edit-site/src/components/list/actions/index.js | 12 +++++++++--- packages/edit-site/src/store/actions.js | 7 +++---- .../src/components/entities-saved-states/index.js | 5 ++++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/edit-site/src/components/list/actions/index.js b/packages/edit-site/src/components/list/actions/index.js index 74cdb5bc65882b..186e56ef1dd86e 100644 --- a/packages/edit-site/src/components/list/actions/index.js +++ b/packages/edit-site/src/components/list/actions/index.js @@ -19,9 +19,8 @@ import RenameMenuItem from './rename-menu-item'; export default function Actions( { template } ) { const { removeTemplate, revertTemplate } = useDispatch( editSiteStore ); const { saveEditedEntityRecord } = useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = + const { createSuccessNotice, createErrorNotice, removeNotice } = useDispatch( noticesStore ); - const isRemovable = isTemplateRemovable( template ); const isRevertable = isTemplateRevertable( template ); @@ -30,6 +29,8 @@ export default function Actions( { template } ) { } async function revertAndSaveTemplate() { + const noticeId = 'edit-site-template-reverted'; + removeNotice( noticeId ); try { await revertTemplate( template, { allowUndo: false } ); await saveEditedEntityRecord( @@ -37,9 +38,14 @@ export default function Actions( { template } ) { template.type, template.id ); + const notice = + template.type === 'wp_template' + ? __( 'Template reverted.' ) + : __( 'Template part reverted.' ); - createSuccessNotice( __( 'Entity reverted.' ), { + createSuccessNotice( notice, { type: 'snackbar', + id: noticeId, } ); } catch ( error ) { const errorMessage = diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 48ac0a343a59fc..66edf87d9d8a0b 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -365,6 +365,8 @@ export function setIsSaveViewOpened( isOpen ) { export const revertTemplate = ( template, { allowUndo = true } = {} ) => async ( { registry } ) => { + const noticeId = 'edit-site-template-reverted'; + registry.dispatch( noticesStore ).removeNotice( noticeId ); if ( ! isTemplateRevertable( template ) ) { registry .dispatch( noticesStore ) @@ -466,6 +468,7 @@ export const revertTemplate = .dispatch( noticesStore ) .createSuccessNotice( __( 'Template reverted.' ), { type: 'snackbar', + id: noticeId, actions: [ { label: __( 'Undo' ), @@ -473,10 +476,6 @@ export const revertTemplate = }, ], } ); - } else { - registry - .dispatch( noticesStore ) - .createSuccessNotice( __( 'Template reverted.' ) ); } } catch ( error ) { const errorMessage = diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 05b031f1e29e6f..29bef117243036 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -95,7 +95,7 @@ export default function EntitiesSavedStates( { close, onSave = identity } ) { const { __unstableMarkLastChangeAsPersistent } = useDispatch( blockEditorStore ); - const { createSuccessNotice, createErrorNotice } = + const { createSuccessNotice, createErrorNotice, removeNotice } = useDispatch( noticesStore ); // To group entities by type. @@ -148,6 +148,8 @@ export default function EntitiesSavedStates( { close, onSave = identity } ) { }; const saveCheckedEntitiesAndActivate = () => { + const saveNoticeId = 'site-editor-save-success'; + removeNotice( saveNoticeId ); const entitiesToSave = dirtyEntityRecords.filter( ( { kind, name, key, property } ) => { return ! unselectedEntities.some( @@ -208,6 +210,7 @@ export default function EntitiesSavedStates( { close, onSave = identity } ) { } else { createSuccessNotice( __( 'Site updated.' ), { type: 'snackbar', + id: saveNoticeId, } ); } } ) From 3f5da7aa3b3b2f37301a498f401c7623d989bb9d Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Mon, 15 May 2023 14:00:42 +1000 Subject: [PATCH 035/131] Removing `isStylePreview` prop, which is not used anywhere (#50622) --- .../block-editor/src/components/block-styles/preview-panel.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/block-styles/preview-panel.js b/packages/block-editor/src/components/block-styles/preview-panel.js index 3cd24ec03be756..2693e175ba3fde 100644 --- a/packages/block-editor/src/components/block-styles/preview-panel.js +++ b/packages/block-editor/src/components/block-styles/preview-panel.js @@ -33,7 +33,5 @@ export default function BlockStylesPreviewPanel( { }; }, [ genericPreviewBlock, styleClassName ] ); - return ( - <InserterPreviewPanel item={ previewBlocks } isStylePreview={ true } /> - ); + return <InserterPreviewPanel item={ previewBlocks } />; } From c6ed0e45f4c1d9302a0cc6b6119c9ee92b929896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Mon, 15 May 2023 09:13:24 +0200 Subject: [PATCH 036/131] Move gutenberg_get_global_styles (#50596) From: lib/compat/wordpress-6.3/get-global-styles-and-settings.php To: lib/global-styles-and-settings.php The reason is that while the global styles APIs continue to be developed we need the plugin classes to be in use by the public API. --- .../get-global-styles-and-settings.php | 34 ------------------- lib/global-styles-and-settings.php | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php index 609d9b3de38a16..bb489664e1eea9 100644 --- a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php @@ -124,37 +124,3 @@ function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = f function gutenberg_get_remote_theme_patterns() { return WP_Theme_JSON_Resolver_Gutenberg::get_theme_data( array(), array( 'with_supports' => false ) )->get_patterns(); } - -/** - * Gets the styles resulting of merging core, theme, and user data. - * - * @since 5.9.0 - * @since 6.3.0 the internal link format "var:preset|color|secondary" is resolved - * to "var(--wp--preset--font-size--small)" so consumers don't have to. - * - * @param array $path Path to the specific style to retrieve. Optional. - * If empty, will return all styles. - * @param array $context { - * Metadata to know where to retrieve the $path from. Optional. - * - * @type string $block_name Which block to retrieve the styles from. - * If empty, it'll return the styles for the global context. - * @type string $origin Which origin to take data from. - * Valid values are 'all' (core, theme, and user) or 'base' (core and theme). - * If empty or unknown, 'all' is used. - * } - * @return array The styles to retrieve. - */ -function gutenberg_get_global_styles( $path = array(), $context = array() ) { - if ( ! empty( $context['block_name'] ) ) { - $path = array_merge( array( 'blocks', $context['block_name'] ), $path ); - } - - $origin = 'custom'; - if ( isset( $context['origin'] ) && 'base' === $context['origin'] ) { - $origin = 'theme'; - } - $styles = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( $origin )->get_raw_data()['styles']; - - return _wp_array_get( $styles, $path, $styles ); -} diff --git a/lib/global-styles-and-settings.php b/lib/global-styles-and-settings.php index b239080f298c2c..3195b14e899e39 100644 --- a/lib/global-styles-and-settings.php +++ b/lib/global-styles-and-settings.php @@ -231,3 +231,37 @@ function _gutenberg_clean_theme_json_caches() { } add_action( 'start_previewing_theme', '_gutenberg_clean_theme_json_caches' ); add_action( 'switch_theme', '_gutenberg_clean_theme_json_caches' ); + +/** + * Gets the styles resulting of merging core, theme, and user data. + * + * @since 5.9.0 + * @since 6.3.0 the internal link format "var:preset|color|secondary" is resolved + * to "var(--wp--preset--font-size--small)" so consumers don't have to. + * + * @param array $path Path to the specific style to retrieve. Optional. + * If empty, will return all styles. + * @param array $context { + * Metadata to know where to retrieve the $path from. Optional. + * + * @type string $block_name Which block to retrieve the styles from. + * If empty, it'll return the styles for the global context. + * @type string $origin Which origin to take data from. + * Valid values are 'all' (core, theme, and user) or 'base' (core and theme). + * If empty or unknown, 'all' is used. + * } + * @return array The styles to retrieve. + */ +function gutenberg_get_global_styles( $path = array(), $context = array() ) { + if ( ! empty( $context['block_name'] ) ) { + $path = array_merge( array( 'blocks', $context['block_name'] ), $path ); + } + + $origin = 'custom'; + if ( isset( $context['origin'] ) && 'base' === $context['origin'] ) { + $origin = 'theme'; + } + $styles = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( $origin )->get_raw_data()['styles']; + + return _wp_array_get( $styles, $path, $styles ); +} From 71c18bdb08233841ccad167ece756248345b48b9 Mon Sep 17 00:00:00 2001 From: John Hooks <bitmachina@outlook.com> Date: Mon, 15 May 2023 00:42:18 -0700 Subject: [PATCH 037/131] chore: update memize to v2 (#50172) * chore: update memize to v2 fix: update package-lock.json * chore: use @gziolo's npm install tip * chore: update memize to v2.1.0 --- package-lock.json | 58 +++++++++++----------- packages/block-library/package.json | 2 +- packages/blocks/package.json | 2 +- packages/components/package.json | 2 +- packages/core-data/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/edit-site/package.json | 2 +- packages/edit-site/src/store/test/utils.js | 20 ++++++-- packages/editor/package.json | 2 +- packages/i18n/package.json | 2 +- packages/plugins/package.json | 2 +- packages/rich-text/package.json | 2 +- packages/shortcode/package.json | 2 +- 13 files changed, 55 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index d798b89d8e91a7..a96e865acae9c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16922,7 +16922,7 @@ "fast-average-color": "^9.1.1", "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21", - "memize": "^1.1.0", + "memize": "^2.1.0", "micromodal": "^0.4.10", "preact": "^10.13.2", "remove-accents": "^0.4.2" @@ -16965,7 +16965,7 @@ "hpq": "^1.3.0", "is-plain-object": "^5.0.0", "lodash": "^4.17.21", - "memize": "^1.1.0", + "memize": "^2.1.0", "rememo": "^4.0.2", "remove-accents": "^0.4.2", "showdown": "^1.9.1", @@ -17033,7 +17033,7 @@ "gradient-parser": "^0.1.5", "highlight-words-core": "^1.2.2", "is-plain-object": "^5.0.0", - "memize": "^1.1.0", + "memize": "^2.1.0", "path-to-regexp": "^6.2.1", "re-resizable": "^6.4.0", "react-colorful": "^5.3.1", @@ -17143,7 +17143,7 @@ "change-case": "^4.1.2", "equivalent-key-map": "^0.2.2", "fast-deep-equal": "^3.1.3", - "memize": "^1.1.0", + "memize": "^2.1.0", "rememo": "^4.0.2", "uuid": "^8.3.0" } @@ -17363,7 +17363,7 @@ "@wordpress/warning": "file:packages/warning", "@wordpress/widgets": "file:packages/widgets", "classnames": "^2.3.1", - "memize": "^1.1.0", + "memize": "^2.1.0", "rememo": "^4.0.2" }, "dependencies": { @@ -17440,7 +17440,7 @@ "downloadjs": "^1.4.7", "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21", - "memize": "^1.1.0", + "memize": "^2.1.0", "react-autosize-textarea": "^7.1.0", "rememo": "^4.0.2" }, @@ -17519,7 +17519,7 @@ "classnames": "^2.3.1", "date-fns": "^2.28.0", "escape-html": "^1.0.3", - "memize": "^1.1.0", + "memize": "^2.1.0", "react-autosize-textarea": "^7.1.0", "rememo": "^4.0.2", "remove-accents": "^0.4.2" @@ -17741,7 +17741,7 @@ "@babel/runtime": "^7.16.0", "@wordpress/hooks": "file:packages/hooks", "gettext-parser": "^1.3.1", - "memize": "^1.1.0", + "memize": "^2.1.0", "sprintf-js": "^1.1.1", "tannin": "^1.2.0" } @@ -17885,7 +17885,7 @@ "@wordpress/hooks": "file:packages/hooks", "@wordpress/icons": "file:packages/icons", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", - "memize": "^1.1.0" + "memize": "^2.0.1" } }, "@wordpress/postcss-plugins-preset": { @@ -18204,7 +18204,7 @@ "@wordpress/escape-html": "file:packages/escape-html", "@wordpress/i18n": "file:packages/i18n", "@wordpress/keycodes": "file:packages/keycodes", - "memize": "^1.1.0", + "memize": "^2.1.0", "rememo": "^4.0.2" } }, @@ -18300,7 +18300,7 @@ "version": "file:packages/shortcode", "requires": { "@babel/runtime": "^7.16.0", - "memize": "^1.1.0" + "memize": "^2.0.1" } }, "@wordpress/style-engine": { @@ -25536,7 +25536,7 @@ "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", "dev": true }, "array-includes": { @@ -28899,7 +28899,7 @@ "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, "code-point-at": { @@ -30476,7 +30476,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": { @@ -30705,7 +30705,7 @@ "debuglog": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=", "dev": true }, "decache": { @@ -35547,7 +35547,7 @@ "git-remote-origin-url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", - "integrity": "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==", + "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", "dev": true, "requires": { "gitconfiglocal": "^1.0.0", @@ -35594,7 +35594,7 @@ "gitconfiglocal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", - "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==", + "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", "dev": true, "requires": { "ini": "^1.3.2" @@ -36868,7 +36868,7 @@ "humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", "dev": true, "requires": { "ms": "^2.0.0" @@ -37884,7 +37884,7 @@ "is-text-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", "dev": true, "requires": { "text-extensions": "^1.0.0" @@ -39657,7 +39657,7 @@ "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, "jsprim": { @@ -40757,7 +40757,7 @@ "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", - "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", "dev": true }, "lodash.isplainobject": { @@ -41037,7 +41037,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": { @@ -41821,9 +41821,9 @@ } }, "memize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/memize/-/memize-1.1.0.tgz", - "integrity": "sha512-K4FcPETOMTwe7KL2LK0orMhpOmWD2wRGwWWpbZy0fyArwsyIKR8YJVz8+efBAh3BO4zPqlSICu4vsLTRRqtFAg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/memize/-/memize-2.1.0.tgz", + "integrity": "sha512-yywVJy8ctVlN5lNPxsep5urnZ6TTclwPEyigM9M3Bi8vseJBOfqNrGWN/r8NzuIt3PovM323W04blJfGQfQSVg==" }, "memoize-one": { "version": "5.2.1", @@ -48260,7 +48260,7 @@ "promzard": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz", - "integrity": "sha512-JZeYqd7UAcHCwI+sTOeUDYkvEU+1bQ7iE0UT1MgB/tERkAPkesW46MrpIySzODi+owTjZtiF8Ay5j9m60KmMBw==", + "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", "dev": true, "requires": { "read": "1" @@ -48294,7 +48294,7 @@ "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "dev": true }, "protocols": { @@ -49851,7 +49851,7 @@ "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", "dev": true, "requires": { "mute-stream": "~0.0.4" @@ -55208,7 +55208,7 @@ "temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", - "integrity": "sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==", + "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", "dev": true }, "terminal-link": { diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 8ba08989c84d26..17f5ce6d8f242e 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -70,7 +70,7 @@ "fast-average-color": "^9.1.1", "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21", - "memize": "^1.1.0", + "memize": "^2.1.0", "micromodal": "^0.4.10", "preact": "^10.13.2", "remove-accents": "^0.4.2" diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 39f31a7f0d86da..720ca248e943fd 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -49,7 +49,7 @@ "hpq": "^1.3.0", "is-plain-object": "^5.0.0", "lodash": "^4.17.21", - "memize": "^1.1.0", + "memize": "^2.1.0", "rememo": "^4.0.2", "remove-accents": "^0.4.2", "showdown": "^1.9.1", diff --git a/packages/components/package.json b/packages/components/package.json index 88446b1a9fb18e..0077e73d3f1da3 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -68,7 +68,7 @@ "gradient-parser": "^0.1.5", "highlight-words-core": "^1.2.2", "is-plain-object": "^5.0.0", - "memize": "^1.1.0", + "memize": "^2.1.0", "path-to-regexp": "^6.2.1", "re-resizable": "^6.4.0", "react-colorful": "^5.3.1", diff --git a/packages/core-data/package.json b/packages/core-data/package.json index d3976a41d06e77..5c303cf3d965af 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -44,7 +44,7 @@ "change-case": "^4.1.2", "equivalent-key-map": "^0.2.2", "fast-deep-equal": "^3.1.3", - "memize": "^1.1.0", + "memize": "^2.1.0", "rememo": "^4.0.2", "uuid": "^8.3.0" }, diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index e201414635b55d..ec434131a855b9 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -58,7 +58,7 @@ "@wordpress/warning": "file:../warning", "@wordpress/widgets": "file:../widgets", "classnames": "^2.3.1", - "memize": "^1.1.0", + "memize": "^2.1.0", "rememo": "^4.0.2" }, "peerDependencies": { diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 6e89e6979dfb30..efbc8349fe8d9f 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -66,7 +66,7 @@ "downloadjs": "^1.4.7", "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21", - "memize": "^1.1.0", + "memize": "^2.1.0", "react-autosize-textarea": "^7.1.0", "rememo": "^4.0.2" }, diff --git a/packages/edit-site/src/store/test/utils.js b/packages/edit-site/src/store/test/utils.js index fd10317be5b3bc..f5552f81cd179b 100644 --- a/packages/edit-site/src/store/test/utils.js +++ b/packages/edit-site/src/store/test/utils.js @@ -145,9 +145,14 @@ describe( 'utils', () => { ).toEqual( FLATTENED_BLOCKS ); // The function has been called twice with the same params, so the cache size should be 1. - const [ , , originalSize ] = - getFilteredTemplatePartBlocks.getCache(); - expect( originalSize ).toBe( 1 ); + /** + * TODO what should be done about this? + * Can it be tested another way? + * Is it necessary? + */ + // const [ , , originalSize ] = + // getFilteredTemplatePartBlocks.getCache(); + // expect( originalSize ).toBe( 1 ); // Call the function again, with different params. expect( @@ -174,8 +179,13 @@ describe( 'utils', () => { ] ); // The function has been called with different params, so the cache size should now be 2. - const [ , , finalSize ] = getFilteredTemplatePartBlocks.getCache(); - expect( finalSize ).toBe( 2 ); + /** + * TODO what should be done about this? + * Can it be tested another way? + * Is it necessary? + */ + // const [ , , finalSize ] = getFilteredTemplatePartBlocks.getCache(); + // expect( finalSize ).toBe( 2 ); } ); } ); } ); diff --git a/packages/editor/package.json b/packages/editor/package.json index 3a383c3773d634..697056615f4631 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -62,7 +62,7 @@ "classnames": "^2.3.1", "date-fns": "^2.28.0", "escape-html": "^1.0.3", - "memize": "^1.1.0", + "memize": "^2.1.0", "react-autosize-textarea": "^7.1.0", "rememo": "^4.0.2", "remove-accents": "^0.4.2" diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 6c11f0104c3daa..a833920adbe72c 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -32,7 +32,7 @@ "@babel/runtime": "^7.16.0", "@wordpress/hooks": "file:../hooks", "gettext-parser": "^1.3.1", - "memize": "^1.1.0", + "memize": "^2.1.0", "sprintf-js": "^1.1.1", "tannin": "^1.2.0" }, diff --git a/packages/plugins/package.json b/packages/plugins/package.json index 2e6a7940af4a09..3f190af9288840 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -33,7 +33,7 @@ "@wordpress/hooks": "file:../hooks", "@wordpress/icons": "file:../icons", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", - "memize": "^1.1.0" + "memize": "^2.0.1" }, "peerDependencies": { "react": "^18.0.0" diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index a25b8ee52cf2ba..5811c3e90f0bac 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -39,7 +39,7 @@ "@wordpress/escape-html": "file:../escape-html", "@wordpress/i18n": "file:../i18n", "@wordpress/keycodes": "file:../keycodes", - "memize": "^1.1.0", + "memize": "^2.1.0", "rememo": "^4.0.2" }, "peerDependencies": { diff --git a/packages/shortcode/package.json b/packages/shortcode/package.json index 25acdf8784dc18..c730777b20b931 100644 --- a/packages/shortcode/package.json +++ b/packages/shortcode/package.json @@ -26,7 +26,7 @@ "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.16.0", - "memize": "^1.1.0" + "memize": "^2.0.1" }, "publishConfig": { "access": "public" From 3bf54f6488efaa9a02d2569ec6d54c04e2b5477e Mon Sep 17 00:00:00 2001 From: Daniel Richards <daniel.richards@automattic.com> Date: Mon, 15 May 2023 15:50:45 +0800 Subject: [PATCH 038/131] Ensure pattern block doesn't duplicate client ids (#50629) Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> --- packages/block-library/src/pattern/edit.js | 14 +++++++------- packages/block-library/src/pattern/v1/edit.js | 8 +++++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/block-library/src/pattern/edit.js b/packages/block-library/src/pattern/edit.js index 5072d577172b0a..c69ac359659cf5 100644 --- a/packages/block-library/src/pattern/edit.js +++ b/packages/block-library/src/pattern/edit.js @@ -42,17 +42,17 @@ const PatternEdit = ( { attributes, clientId } ) => { // because nested pattern blocks cannot be inserted if the parent block supports // inner blocks but doesn't have blockSettings in the state. window.queueMicrotask( () => { + // Clone blocks from the pattern before insertion to ensure they receive + // distinct client ids. See https://github.com/WordPress/gutenberg/issues/50628. + const clonedBlocks = selectedPattern.blocks.map( ( block ) => + cloneBlock( block ) + ); __unstableMarkNextChangeAsNotPersistent(); if ( syncStatus === 'partial' ) { - replaceInnerBlocks( - clientId, - selectedPattern.blocks.map( ( block ) => - cloneBlock( block ) - ) - ); + replaceInnerBlocks( clientId, clonedBlocks ); return; } - replaceBlocks( clientId, selectedPattern.blocks ); + replaceBlocks( clientId, clonedBlocks ); } ); } }, [ diff --git a/packages/block-library/src/pattern/v1/edit.js b/packages/block-library/src/pattern/v1/edit.js index aa475809ccb44f..b4900536ec274f 100644 --- a/packages/block-library/src/pattern/v1/edit.js +++ b/packages/block-library/src/pattern/v1/edit.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import { cloneBlock } from '@wordpress/blocks'; import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; import { @@ -32,8 +33,13 @@ const PatternEdit = ( { attributes, clientId } ) => { // because nested pattern blocks cannot be inserted if the parent block supports // inner blocks but doesn't have blockSettings in the state. window.queueMicrotask( () => { + // Clone blocks from the pattern before insertion to ensure they receive + // distinct client ids. See https://github.com/WordPress/gutenberg/issues/50628. + const clonedBlocks = selectedPattern.blocks.map( ( block ) => + cloneBlock( block ) + ); __unstableMarkNextChangeAsNotPersistent(); - replaceBlocks( clientId, selectedPattern.blocks ); + replaceBlocks( clientId, clonedBlocks ); } ); } }, [ From d66e670b6c253e8d89cd63044a44297c1380c139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Mon, 15 May 2023 09:59:33 +0200 Subject: [PATCH 039/131] Move `gutenberg_get_remote_theme_patterns` (#50597) --- .../wordpress-6.3/get-global-styles-and-settings.php | 12 ------------ lib/global-styles-and-settings.php | 12 ++++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php index bb489664e1eea9..009fa6253f79d4 100644 --- a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php @@ -112,15 +112,3 @@ function wp_get_block_css_selector( $block_type, $target = 'root', $fallback = f return null; } } - -/** - * Returns the current theme's wanted patterns(slugs) to be - * registered from Pattern Directory. - * - * @since 6.3.0 - * - * @return string[] - */ -function gutenberg_get_remote_theme_patterns() { - return WP_Theme_JSON_Resolver_Gutenberg::get_theme_data( array(), array( 'with_supports' => false ) )->get_patterns(); -} diff --git a/lib/global-styles-and-settings.php b/lib/global-styles-and-settings.php index 3195b14e899e39..bed5ab1e4408b0 100644 --- a/lib/global-styles-and-settings.php +++ b/lib/global-styles-and-settings.php @@ -265,3 +265,15 @@ function gutenberg_get_global_styles( $path = array(), $context = array() ) { return _wp_array_get( $styles, $path, $styles ); } + +/** + * Returns the current theme's wanted patterns (slugs) to be + * registered from Pattern Directory. + * + * @since 6.3.0 + * + * @return string[] + */ +function gutenberg_get_remote_theme_patterns() { + return WP_Theme_JSON_Resolver_Gutenberg::get_theme_data( array(), array( 'with_supports' => false ) )->get_patterns(); +} From 6c83e672b7b7ca3adc093faa024ebc432ee722a0 Mon Sep 17 00:00:00 2001 From: Joen A <1204802+jasmussen@users.noreply.github.com> Date: Mon, 15 May 2023 10:09:54 +0200 Subject: [PATCH 040/131] Button: Update disabled state to be without background. (#50496) * Button: Update disabled state to be without background. * Update changelog. * Merge changelog section * Remove outdated TODO comment --------- Co-authored-by: Lena Morita <lena@jaguchi.com> --- packages/components/CHANGELOG.md | 1 + packages/components/src/button/style.scss | 17 +++++------------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d67bb0fc303cc3..d49f137f467c84 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,7 @@ ### Bug Fix - `NavigableContainer`: do not trap focus in `TabbableContainer` ([#49846](https://github.com/WordPress/gutenberg/pull/49846)). +- Update `<Button>` component to have a transparent background for its tertiary disabled state, to match its enabled state. ([#50496](https://github.com/WordPress/gutenberg/pull/50496)). ### Internal diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss index 3ec844906b0390..fefc93b4fa8e0e 100644 --- a/packages/components/src/button/style.scss +++ b/packages/components/src/button/style.scss @@ -118,21 +118,14 @@ outline: 1px solid transparent; &:active:not(:disabled) { - background: $components-color-gray-300; - color: $components-color-accent-darker-10; box-shadow: none; } - &:hover:not(:disabled) { - color: $components-color-accent-darker-10; - } - &:disabled, &[aria-disabled="true"], &[aria-disabled="true"]:hover { - // TODO: Prepare for theming (https://github.com/WordPress/gutenberg/pull/45466/files#r1030872724) - color: lighten($gray-700, 5%); - background: lighten($gray-300, 5%); + color: $gray-600; + background: transparent; transform: none; opacity: 1; box-shadow: none; @@ -165,12 +158,12 @@ color: $components-color-accent; background: transparent; - &:hover:not(:disabled) { + &:hover:not(:disabled, [aria-disabled="true"]) { // TODO: Prepare for theming (https://github.com/WordPress/gutenberg/pull/45466/files#r1030872724) background: rgba(var(--wp-admin-theme-color--rgb), 0.04); } - &:active:not(:disabled) { + &:active:not(:disabled, [aria-disabled="true"]) { // TODO: Prepare for theming (https://github.com/WordPress/gutenberg/pull/45466/files#r1030872724) background: rgba(var(--wp-admin-theme-color--rgb), 0.08); } @@ -239,7 +232,7 @@ } } - &:not([aria-disabled="true"]):active { + &:not(:disabled, [aria-disabled="true"]):active { color: $components-color-foreground; } From 9e6b646936bfb4a3eef99dd9ecab48bb542006ad Mon Sep 17 00:00:00 2001 From: Edwin Takahashi <egao@outlook.com> Date: Mon, 15 May 2023 17:21:21 +0900 Subject: [PATCH 041/131] Migrate Cover Block tests to Playwright (#45784) * Add new Playwright E2E tests for the cover block. * Add comment * Update block list selector * test/e2e/specs/editor/blocks/cover.spec.js - update spec with accessibility-based selectors where possible. * test/e2e/specs/editor/blocks/cover.spec.js - rebase, rebuild and update the selectors * packages/e2e-tests/specs/editor/blocks/cover.test.js - remove Puppeteer file. * test/e2e/specs/editor/blocks/cover.spec.js - clarify use of `span` element in the first spec. * - add comment explaing use of CSS selector for '.components-resizable-box__handle-bottom'. - move image upload and filename methods previously handled by `getTestImage` into a POM-like function at bottom of file. * - remove testing `test.only` * - define the coverBlock locator after inserting the block, and use that locator as starting point for all other interactions. - use `getComputedStyle` instad of `node.style.<attribute>`. - replace implementation detail check on image upload with a `expect.toPass` check. - remove steps when resizing box. - replace implementation detail check for coverbox height with truthy check. * - fix the import to use `import`. - add comment explaining the use of `span[aria-hidden="true"]`. * - anchor selectors on a parent selector. * - use the Cover block's resize handle as basis for calculation when resizing the block. * - remove testing "only" * - check the src repeatedly to contain the fileBasename instead. * - move `getBackgroundColorAndOpacity` into the ImageBlockUtils POM. * Rename POM to CoverBlockUtils --- .../specs/editor/blocks/cover.test.js | 177 ----------- test/e2e/specs/editor/blocks/cover.spec.js | 284 ++++++++++++++++++ 2 files changed, 284 insertions(+), 177 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/blocks/cover.test.js create mode 100644 test/e2e/specs/editor/blocks/cover.spec.js diff --git a/packages/e2e-tests/specs/editor/blocks/cover.test.js b/packages/e2e-tests/specs/editor/blocks/cover.test.js deleted file mode 100644 index 56ed8455aefa9c..00000000000000 --- a/packages/e2e-tests/specs/editor/blocks/cover.test.js +++ /dev/null @@ -1,177 +0,0 @@ -/** - * External dependencies - */ -import path from 'path'; -import fs from 'fs'; -import os from 'os'; -import { v4 as uuid } from 'uuid'; - -/** - * WordPress dependencies - */ -import { - insertBlock, - createNewPost, - openDocumentSettingsSidebar, - switchBlockInspectorTab, - transformBlockTo, -} from '@wordpress/e2e-test-utils'; - -async function upload( selector ) { - const inputElement = await page.waitForSelector( - `${ selector } input[type="file"]` - ); - const testImagePath = path.join( - __dirname, - '..', - '..', - '..', - 'assets', - '10x10_e2e_test_image_z9T8jK.png' - ); - const filename = uuid(); - const tmpFileName = path.join( os.tmpdir(), filename + '.png' ); - fs.copyFileSync( testImagePath, tmpFileName ); - await inputElement.uploadFile( tmpFileName ); - await page.waitForSelector( `${ selector } img[src$="${ filename }.png"]` ); - return filename; -} - -describe( 'Cover', () => { - beforeEach( async () => { - await createNewPost(); - } ); - - it( 'can set background image using image upload on block placeholder', async () => { - await insertBlock( 'Cover' ); - // Create the block using uploaded image. - const sourceImageFilename = await upload( '.wp-block-cover' ); - // Get the block's background image URL. - const blockImage = await page.waitForSelector( '.wp-block-cover img' ); - const blockImageUrl = await blockImage.evaluate( ( el ) => el.src ); - - expect( blockImageUrl ).toContain( sourceImageFilename ); - } ); - - it( 'dims background image down by 50% by default', async () => { - await insertBlock( 'Cover' ); - // Create the block using uploaded image. - await upload( '.wp-block-cover' ); - // Get the block's background dim color and its opacity. - const backgroundDim = await page.waitForSelector( - '.wp-block-cover .has-background-dim' - ); - const [ backgroundDimColor, backgroundDimOpacity ] = - await page.evaluate( ( el ) => { - const computedStyle = window.getComputedStyle( el ); - return [ computedStyle.backgroundColor, computedStyle.opacity ]; - }, backgroundDim ); - - expect( backgroundDimColor ).toBe( 'rgb(0, 0, 0)' ); - expect( backgroundDimOpacity ).toBe( '0.5' ); - } ); - - it( 'can be resized using drag & drop', async () => { - await insertBlock( 'Cover' ); - // Close the inserter. - await page.click( '.edit-post-header-toolbar__inserter-toggle' ); - // Open the sidebar. - await openDocumentSettingsSidebar(); - // Choose the first solid color as the background of the cover. - await page.click( - '.components-circular-option-picker__option-wrapper:first-child button' - ); - - // Select the cover block. By default the child paragraph gets selected. - await page.click( - '.edit-post-header-toolbar__document-overview-toggle' - ); - await page.click( - '.block-editor-list-view-block__contents-container a' - ); - - switchBlockInspectorTab( 'Styles' ); - const heightInputHandle = await page.waitForSelector( - 'input[id*="block-cover-height-input"]' - ); - - // Verify the height of the cover is not defined. - expect( - await page.evaluate( ( { value } ) => value, heightInputHandle ) - ).toBe( '' ); - - const resizeButton = await page.$( - '.components-resizable-box__handle-bottom' - ); - const boundingBoxResizeButton = await resizeButton.boundingBox(); - const coordinatesResizeButton = { - x: boundingBoxResizeButton.x + boundingBoxResizeButton.width / 2, - y: boundingBoxResizeButton.y + boundingBoxResizeButton.height / 2, - }; - - // Move the mouse to the position of the resize button. - await page.mouse.move( - coordinatesResizeButton.x, - coordinatesResizeButton.y - ); - - // Trigger a mousedown event against the resize button. - // Using page.mouse.down does not works because it triggers a global event, - // not an event for that element. - page.evaluate( ( { x, y } ) => { - const element = document.querySelector( - '.components-resizable-box__handle-bottom' - ); - event = document.createEvent( 'CustomEvent' ); - event.initCustomEvent( 'mousedown', true, true, null ); - event.clientX = x; - event.clientY = y; - element.dispatchEvent( event ); - }, coordinatesResizeButton ); - - // Move the mouse to resize the cover. - await page.mouse.move( - coordinatesResizeButton.x, - coordinatesResizeButton.y + 100, - { steps: 10 } - ); - - // Release the mouse. - await page.mouse.up(); - - // Verify the height of the cover has changed. - expect( - await page.evaluate( - ( { value } ) => Number.parseInt( value ), - heightInputHandle - ) - ).toBeGreaterThan( 100 ); - } ); - - it( 'dims the background image down by 50% when transformed from the Image block', async () => { - await insertBlock( 'Image' ); - // Upload image and transform to the Cover block. - const filename = await upload( '.wp-block-image' ); - await page.waitForSelector( - `.wp-block-image img[src$="${ filename }.png"]` - ); - - // Focus the block wrapper before trying to convert to make sure figcaption toolbar is not obscuring - // the block toolbar. - await page.focus( '.wp-block-image' ); - await transformBlockTo( 'Cover' ); - - // Get the block's background dim color and its opacity. - const backgroundDim = await page.waitForSelector( - '.wp-block-cover .has-background-dim' - ); - const [ backgroundDimColor, backgroundDimOpacity ] = - await page.evaluate( ( el ) => { - const computedStyle = window.getComputedStyle( el ); - return [ computedStyle.backgroundColor, computedStyle.opacity ]; - }, backgroundDim ); - - expect( backgroundDimColor ).toBe( 'rgb(0, 0, 0)' ); - expect( backgroundDimOpacity ).toBe( '0.5' ); - } ); -} ); diff --git a/test/e2e/specs/editor/blocks/cover.spec.js b/test/e2e/specs/editor/blocks/cover.spec.js new file mode 100644 index 00000000000000..02176167813450 --- /dev/null +++ b/test/e2e/specs/editor/blocks/cover.spec.js @@ -0,0 +1,284 @@ +/** + * External dependencies + */ +const path = require( 'path' ); +const fs = require( 'fs/promises' ); +const os = require( 'os' ); +const { v4: uuid } = require( 'uuid' ); + +/** @typedef {import('@playwright/test').Page} Page */ + +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.use( { + coverBlockUtils: async ( { page }, use ) => { + await use( new CoverBlockUtils( { page } ) ); + }, +} ); + +test.describe( 'Cover', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'can set overlay color using color picker on block placeholder', async ( { + page, + editor, + coverBlockUtils, + } ) => { + await editor.insertBlock( { name: 'core/cover' } ); + const coverBlock = page.getByRole( 'document', { + name: 'Block: Cover', + } ); + + // Locate the Black color swatch. + const blackColorSwatch = coverBlock.getByRole( 'button', { + name: 'Color: Black', + } ); + await expect( blackColorSwatch ).toBeVisible(); + + // Get the RGB value of Black. + const [ blackRGB ] = await coverBlockUtils.getBackgroundColorAndOpacity( + coverBlock + ); + + // Create the block by clicking selected color button. + await blackColorSwatch.click(); + + // Get the RGB value of the background dim. + const [ actualRGB ] = + await coverBlockUtils.getBackgroundColorAndOpacity( coverBlock ); + + expect( blackRGB ).toEqual( actualRGB ); + } ); + + test( 'can set background image using image upload on block placeholder', async ( { + page, + editor, + coverBlockUtils, + } ) => { + await editor.insertBlock( { name: 'core/cover' } ); + const coverBlock = page.getByRole( 'document', { + name: 'Block: Cover', + } ); + + const filename = await coverBlockUtils.upload( + coverBlock.getByTestId( 'form-file-upload-input' ) + ); + const fileBasename = path.basename( filename ); + + // Wait for the img's src attribute to be prefixed with http. + // Otherwise, the URL for the img src attribute starts is a placeholder + // beginning with `blob`. + await expect( async () => { + const src = await coverBlock.locator( 'img' ).getAttribute( 'src' ); + expect( src.includes( fileBasename ) ).toBe( true ); + } ).toPass(); + } ); + + test( 'dims background image down by 50% by default', async ( { + page, + editor, + coverBlockUtils, + } ) => { + await editor.insertBlock( { name: 'core/cover' } ); + const coverBlock = page.getByRole( 'document', { + name: 'Block: Cover', + } ); + + await coverBlockUtils.upload( + coverBlock.getByTestId( 'form-file-upload-input' ) + ); + + // The hidden span must be used as the target for opacity and color value. + // Using the Cover block to calculate the opacity results in an incorrect value of 1. + // The hidden span value returns the correct opacity at 0.5. + const [ backgroundDimColor, backgroundDimOpacity ] = + await coverBlockUtils.getBackgroundColorAndOpacity( + coverBlock.locator( 'span[aria-hidden="true"]' ) + ); + expect( backgroundDimColor ).toBe( 'rgb(0, 0, 0)' ); + expect( backgroundDimOpacity ).toBe( '0.5' ); + } ); + + test( 'can have the title edited', async ( { page, editor } ) => { + const titleText = 'foo'; + + await editor.insertBlock( { name: 'core/cover' } ); + const coverBlock = page.getByRole( 'document', { + name: 'Block: Cover', + } ); + + // Choose a color swatch to transform the placeholder block into + // a functioning block. + await coverBlock + .getByRole( 'button', { + name: 'Color: Black', + } ) + .click(); + + // Activate the paragraph block inside the Cover block. + // The name of the block differs depending on whether text has been entered or not. + const coverBlockParagraph = coverBlock.getByRole( 'document', { + name: /Paragraph block|Empty block; start writing or type forward slash to choose a block/, + } ); + await expect( coverBlockParagraph ).toBeEditable(); + + await coverBlockParagraph.fill( titleText ); + + await expect( coverBlockParagraph ).toContainText( titleText ); + } ); + + test( 'can be resized using drag & drop', async ( { page, editor } ) => { + await editor.insertBlock( { name: 'core/cover' } ); + const coverBlock = page.getByRole( 'document', { + name: 'Block: Cover', + } ); + await coverBlock + .getByRole( 'button', { + name: 'Color: Black', + } ) + .click(); + + // Open the document sidebar. + await editor.openDocumentSettingsSidebar(); + + // Open the block list viewer from the Editor toolbar. + await page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Document Overview' } ) + .click(); + + // Select the Cover block from the Document Overview. + await page + .getByRole( 'region', { name: 'Document Overview' } ) + .getByRole( 'link', { name: 'Cover' } ) + .click(); + + // In the Block Editor Settings panel, click on the Styles subpanel. + const coverBlockEditorSettings = page.getByRole( 'region', { + name: 'Editor settings', + } ); + await coverBlockEditorSettings + .getByRole( 'tab', { name: 'Styles' } ) + .click(); + + // Ensure there the default value for the minimum height of cover is undefined. + const defaultHeightValue = await coverBlockEditorSettings + .getByLabel( 'Minimum height of cover' ) + .inputValue(); + expect( defaultHeightValue ).toBeFalsy(); + + // There is no accessible locator for the draggable block resize edge, + // which is he bottom edge of the Cover block. + // Therefore a CSS selector must be used. + const coverBlockResizeHandle = page.locator( + '.components-resizable-box__handle-bottom' + ); + + // Establish the existing bounding boxes for the Cover block + // and the Cover block's resizing handle. + const coverBlockBox = await coverBlock.boundingBox(); + const coverBlockResizeHandleBox = + await coverBlockResizeHandle.boundingBox(); + expect( coverBlockBox.height ).toBeTruthy(); + expect( coverBlockResizeHandleBox.height ).toBeTruthy(); + + // Increse the Cover block height by 100px. + await coverBlockResizeHandle.hover(); + await page.mouse.down(); + + // Counter-intuitively, the mouse movement calculation should not be made using the + // Cover block's bounding box, but rather based on the coordinates of the + // resize handle. + await page.mouse.move( + coverBlockResizeHandleBox.x + coverBlockResizeHandleBox.width / 2, + coverBlockResizeHandleBox.y + 100 + ); + await page.mouse.up(); + + const newCoverBlockBox = await coverBlock.boundingBox(); + expect( newCoverBlockBox.height ).toBe( coverBlockBox.height + 100 ); + } ); + + test( 'dims the background image down by 50% when transformed from the Image block', async ( { + page, + editor, + coverBlockUtils, + } ) => { + await editor.insertBlock( { name: 'core/image' } ); + + const imageBlock = page.getByRole( 'document', { + name: 'Block: Image', + } ); + + await coverBlockUtils.upload( + imageBlock.getByTestId( 'form-file-upload-input' ) + ); + + await expect( + page + .getByRole( 'document', { name: 'Block: Image' } ) + .locator( 'img' ) + ).toBeVisible(); + + await editor.transformBlockTo( 'core/cover' ); + + const coverBlock = page.getByRole( 'document', { + name: 'Block: Cover', + } ); + + // The hidden span must be used as the target for opacity and color value. + // Using the Cover block to calculate the opacity results in an incorrect value of 1. + // The hidden span value returns the correct opacity at 0.5. + const [ backgroundDimColor, backgroundDimOpacity ] = + await coverBlockUtils.getBackgroundColorAndOpacity( + coverBlock.locator( 'span[aria-hidden="true"]' ) + ); + + // The hidden span must be used as the target for opacity and color value. + // Using the Cover block to calculate the opacity results in an incorrect value of 1. + // The hidden span value returns the correct opacity at 0.5. + expect( backgroundDimColor ).toBe( 'rgb(0, 0, 0)' ); + expect( backgroundDimOpacity ).toBe( '0.5' ); + } ); +} ); + +class CoverBlockUtils { + constructor( { page } ) { + /** @type {Page} */ + this.page = page; + + this.TEST_IMAGE_FILE_PATH = path.join( + __dirname, + '..', + '..', + '..', + 'assets', + '10x10_e2e_test_image_z9T8jK.png' + ); + } + + async upload( locator ) { + const tmpDirectory = await fs.mkdtemp( + path.join( os.tmpdir(), 'gutenberg-test-image-' ) + ); + const filename = uuid(); + const tmpFileName = path.join( tmpDirectory, filename + '.png' ); + await fs.copyFile( this.TEST_IMAGE_FILE_PATH, tmpFileName ); + + await locator.setInputFiles( tmpFileName ); + + return filename; + } + + async getBackgroundColorAndOpacity( locator ) { + return await locator.evaluate( ( el ) => { + const computedStyle = window.getComputedStyle( el ); + return [ computedStyle.backgroundColor, computedStyle.opacity ]; + } ); + } +} From efaadb12954f24a48450718f02446d7e9705a652 Mon Sep 17 00:00:00 2001 From: Glen Davies <glendaviesnz@users.noreply.github.com> Date: Mon, 15 May 2023 20:24:55 +1200 Subject: [PATCH 042/131] Simplify the template revert snackbar by showing template name which removes the need to remove the notice (#50626) --- .../src/components/list/actions/index.js | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/edit-site/src/components/list/actions/index.js b/packages/edit-site/src/components/list/actions/index.js index 186e56ef1dd86e..652cbe0f74e15a 100644 --- a/packages/edit-site/src/components/list/actions/index.js +++ b/packages/edit-site/src/components/list/actions/index.js @@ -3,7 +3,7 @@ */ import { useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; import { moreVertical } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; @@ -19,7 +19,7 @@ import RenameMenuItem from './rename-menu-item'; export default function Actions( { template } ) { const { removeTemplate, revertTemplate } = useDispatch( editSiteStore ); const { saveEditedEntityRecord } = useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice, removeNotice } = + const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); const isRemovable = isTemplateRemovable( template ); const isRevertable = isTemplateRevertable( template ); @@ -29,8 +29,6 @@ export default function Actions( { template } ) { } async function revertAndSaveTemplate() { - const noticeId = 'edit-site-template-reverted'; - removeNotice( noticeId ); try { await revertTemplate( template, { allowUndo: false } ); await saveEditedEntityRecord( @@ -38,15 +36,18 @@ export default function Actions( { template } ) { template.type, template.id ); - const notice = - template.type === 'wp_template' - ? __( 'Template reverted.' ) - : __( 'Template part reverted.' ); - createSuccessNotice( notice, { - type: 'snackbar', - id: noticeId, - } ); + createSuccessNotice( + sprintf( + /* translators: The template/part's name. */ + __( '"%s" reverted.' ), + template.title.rendered + ), + { + type: 'snackbar', + id: 'edit-site-template-reverted', + } + ); } catch ( error ) { const errorMessage = error.message && error.code !== 'unknown_error' From 541b0dde9023f85d29890eaf4caa86e867cdae11 Mon Sep 17 00:00:00 2001 From: Marco Ciampini <marco.ciampo@gmail.com> Date: Mon, 15 May 2023 13:36:06 +0200 Subject: [PATCH 043/131] Site editor: convert device type margin styles into non-shorthand syntax (#50441) * Split margin styles into individual size * Apply iframe custom marings only when scale is not 1 --- .../src/components/iframe/index.js | 21 +++++++++++-------- .../src/components/use-resize-canvas/index.js | 10 ++++++++- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index c6639f9e057e1c..a61200b4b1f1ca 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -260,15 +260,18 @@ function Iframe( { style={ { ...props.style, height: expand ? contentHeight : props.style?.height, - marginTop: scale - ? -marginFromScaling + frameSize - : props.style?.marginTop, - marginBottom: scale - ? -marginFromScaling + frameSize - : props.style?.marginBottom, - transform: scale - ? `scale( ${ scale } )` - : props.style?.transform, + marginTop: + scale !== 1 + ? -marginFromScaling + frameSize + : props.style?.marginTop, + marginBottom: + scale !== 1 + ? -marginFromScaling + frameSize + : props.style?.marginBottom, + transform: + scale !== 1 + ? `scale( ${ scale } )` + : props.style?.transform, transition: 'all .3s', } } ref={ useMergeRefs( [ ref, setRef ] ) } diff --git a/packages/block-editor/src/components/use-resize-canvas/index.js b/packages/block-editor/src/components/use-resize-canvas/index.js index d329e20d6106e4..fab0b7a15e2afd 100644 --- a/packages/block-editor/src/components/use-resize-canvas/index.js +++ b/packages/block-editor/src/components/use-resize-canvas/index.js @@ -47,12 +47,20 @@ export default function useResizeCanvas( deviceType ) { const contentInlineStyles = ( device ) => { const height = device === 'Mobile' ? '768px' : '1024px'; + const marginVertical = marginValue() + 'px'; + const marginHorizontal = 'auto'; + switch ( device ) { case 'Tablet': case 'Mobile': return { width: getCanvasWidth( device ), - margin: marginValue() + 'px auto', + // Keeping margin styles separate to avoid warnings + // when those props get overridden in the iframe component + marginTop: marginVertical, + marginBottom: marginVertical, + marginLeft: marginHorizontal, + marginRight: marginHorizontal, height, borderRadius: '2px 2px 2px 2px', border: '1px solid #ddd', From adefb8905a17bc3d2b991fc15a3e1a387d10e0c2 Mon Sep 17 00:00:00 2001 From: Ben Dwyer <ben@scruffian.com> Date: Mon, 15 May 2023 15:56:30 +0100 Subject: [PATCH 044/131] Add new API to allow inserter items to be prioritised (#50510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Proposing a way to sort items in the block inspector based on allowed blocks * Add inserterPriority API to inner blocks * Sort inserter based on inserterPriority prop from block list settings * Use new inserterPriority API in Nav block * Correct comment Co-authored-by: Andrei Draganescu <me@andreidraganescu.info> * Remove redundant prop * Avoid stale inserterPriority * Make sorting function stable * Renaming * Remove redundant comment * remove spacer * Set prioritisedBlocks as empty array when no blockListSettings are found. There are instances in the main inserter search results where the rootClientId is undefined, such as when searching for a block. This means there are no blockListSettings, which ends up in no `proritisedBlocks` property being returned and it crashses the app. * proritise -> prioritize for consistency * Update packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js Co-authored-by: Alex Lende <alex@lende.xyz> * Renaming constant to match updated name * Add prioritizedKInnerBlocks to the inner-blocks README * lint fix * update comment * update comment * pass the correct props to useNestedSettingsUpdate * Use stable ref Co-authored-by: George Mamadashvili <georgemamadashvili@gmail.com> * Register the test Plugin for e2e tests * Register block with prioritzedInserterBlocks set * Add initial test scaffold * Tidy up scaffolded test * Add test for new API It fails :( * Try removing sort from helper Why is this even here? It shouldn’t transfer results like this. * Fix test * Add test to check does not override allowedBlocks when conflicted * Add additional assertion for retaining of correct number of results * Ensure tests reflect target of Quick Inserter * sort allowed blocks on the tests that consume getAllBlockInserterItemTitles * Improve e2e test comment * Update packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js * Update packages/block-editor/src/components/inner-blocks/README.md --------- Co-authored-by: Dave Smith <getdavemail@gmail.com> Co-authored-by: Andrei Draganescu <me@andreidraganescu.info> Co-authored-by: Jerry Jones <jones.jeremydavid@gmail.com> Co-authored-by: Alex Lende <alex@lende.xyz> Co-authored-by: George Mamadashvili <georgemamadashvili@gmail.com> Co-authored-by: MaggieCabrera <maggie.cabrera@automattic.com> --- .../src/components/inner-blocks/README.md | 5 + .../src/components/inner-blocks/index.js | 2 + .../components/inner-blocks/index.native.js | 16 ++- .../use-nested-settings-update.js | 23 +++- .../src/components/inserter/index.js | 10 +- .../src/components/inserter/quick-inserter.js | 2 - .../src/components/inserter/search-results.js | 55 ++++++++- .../components/off-canvas-editor/appender.js | 26 +---- .../block-library/src/navigation/constants.js | 5 + .../src/navigation/edit/inner-blocks.js | 7 +- .../src/get-all-block-inserter-item-titles.js | 2 +- ...ner-blocks-prioritized-inserter-blocks.php | 28 +++++ .../index.js | 82 +++++++++++++ .../specs/editor/plugins/child-blocks.test.js | 3 +- .../inner-blocks-allowed-blocks.test.js | 5 +- ...blocks-prioritized-inserter-blocks.test.js | 108 ++++++++++++++++++ 16 files changed, 328 insertions(+), 51 deletions(-) create mode 100644 packages/e2e-tests/plugins/inner-blocks-prioritized-inserter-blocks.php create mode 100644 packages/e2e-tests/plugins/inner-blocks-prioritized-inserter-blocks/index.js create mode 100644 packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js diff --git a/packages/block-editor/src/components/inner-blocks/README.md b/packages/block-editor/src/components/inner-blocks/README.md index eb42da998f0d0c..5ecd9c90898210 100644 --- a/packages/block-editor/src/components/inner-blocks/README.md +++ b/packages/block-editor/src/components/inner-blocks/README.md @@ -180,3 +180,8 @@ For example, a button block, deeply nested in several levels of block `X` that u - **Type:** `Function` - **Default:** - `undefined`. The placeholder is an optional function that can be passed in to be a rendered component placed in front of the appender. This can be used to represent an example state prior to any blocks being placed. See the Social Links for an implementation example. + +### `prioritizedInserterBlocks` + +- **Type:** `Array` +- **Default:** - `undefined`. Determines which block types should be shown in the block inserter. For example, when inserting a block within the Navigation block we specify `core/navigation-link` and `core/navigation-link/page` as these are the most commonly used inner blocks. `prioritizedInserterBlocks` takes an array of the form {blockName}/{variationName}, where {variationName} is optional. diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index d83f62cf2b45cd..bf33abad4864f3 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -45,6 +45,7 @@ function UncontrolledInnerBlocks( props ) { const { clientId, allowedBlocks, + prioritizedInserterBlocks, __experimentalDefaultBlock, __experimentalDirectInsert, template, @@ -62,6 +63,7 @@ function UncontrolledInnerBlocks( props ) { useNestedSettingsUpdate( clientId, allowedBlocks, + prioritizedInserterBlocks, __experimentalDefaultBlock, __experimentalDirectInsert, templateLock, diff --git a/packages/block-editor/src/components/inner-blocks/index.native.js b/packages/block-editor/src/components/inner-blocks/index.native.js index e635230b5c2425..54e168f8ee43f4 100644 --- a/packages/block-editor/src/components/inner-blocks/index.native.js +++ b/packages/block-editor/src/components/inner-blocks/index.native.js @@ -72,9 +72,13 @@ function UncontrolledInnerBlocks( props ) { const { clientId, allowedBlocks, + prioritizedInserterBlocks, + __experimentalDefaultBlock, + __experimentalDirectInsert, template, templateLock, templateInsertUpdatesSelection, + __experimentalCaptureToolbars: captureToolbars, orientation, renderAppender, renderFooterAppender, @@ -95,7 +99,17 @@ function UncontrolledInnerBlocks( props ) { const context = useBlockContext( clientId ); - useNestedSettingsUpdate( clientId, allowedBlocks, templateLock ); + useNestedSettingsUpdate( + clientId, + allowedBlocks, + prioritizedInserterBlocks, + __experimentalDefaultBlock, + __experimentalDirectInsert, + templateLock, + captureToolbars, + orientation, + layout + ); useInnerBlockTemplateSync( clientId, diff --git a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js index d9518eb303a044..49d2da85688c3f 100644 --- a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js +++ b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js @@ -25,6 +25,7 @@ const pendingSettingsUpdates = new WeakMap(); * @param {string} clientId The client ID of the block to update. * @param {string[]} allowedBlocks An array of block names which are permitted * in inner blocks. + * @param {string[]} prioritizedInserterBlocks Block names and/or block variations to be prioritized in the inserter, in the format {blockName}/{variationName}. * @param {?WPDirectInsertBlock} __experimentalDefaultBlock The default block to insert: [ blockName, { blockAttributes } ]. * @param {?Function|boolean} __experimentalDirectInsert If a default block should be inserted directly by the * appender. @@ -40,6 +41,7 @@ const pendingSettingsUpdates = new WeakMap(); export default function useNestedSettingsUpdate( clientId, allowedBlocks, + prioritizedInserterBlocks, __experimentalDefaultBlock, __experimentalDirectInsert, templateLock, @@ -64,13 +66,27 @@ export default function useNestedSettingsUpdate( [ clientId ] ); - // Memoize as inner blocks implementors often pass a new array on every - // render. - const _allowedBlocks = useMemo( () => allowedBlocks, allowedBlocks ); + // Memoize allowedBlocks and prioritisedInnerBlocks based on the contents + // of the arrays. Implementors often pass a new array on every render, + // and the contents of the arrays are just strings, so the entire array + // can be passed as dependencies. + + const _allowedBlocks = useMemo( + () => allowedBlocks, + // eslint-disable-next-line react-hooks/exhaustive-deps + allowedBlocks + ); + + const _prioritizedInserterBlocks = useMemo( + () => prioritizedInserterBlocks, + // eslint-disable-next-line react-hooks/exhaustive-deps + prioritizedInserterBlocks + ); useLayoutEffect( () => { const newSettings = { allowedBlocks: _allowedBlocks, + prioritizedInserterBlocks: _prioritizedInserterBlocks, templateLock: templateLock === undefined || parentLock === 'contentOnly' ? parentLock @@ -130,6 +146,7 @@ export default function useNestedSettingsUpdate( clientId, blockListSettings, _allowedBlocks, + _prioritizedInserterBlocks, __experimentalDefaultBlock, __experimentalDirectInsert, templateLock, diff --git a/packages/block-editor/src/components/inserter/index.js b/packages/block-editor/src/components/inserter/index.js index 4acf5e3746eb88..9c24497e5a9078 100644 --- a/packages/block-editor/src/components/inserter/index.js +++ b/packages/block-editor/src/components/inserter/index.js @@ -150,7 +150,6 @@ class PrivateInserter extends Component { prioritizePatterns, onSelectOrClose, selectBlockOnInsert, - orderInitialBlockItems, } = this.props; if ( isQuick ) { @@ -174,7 +173,6 @@ class PrivateInserter extends Component { isAppender={ isAppender } prioritizePatterns={ prioritizePatterns } selectBlockOnInsert={ selectBlockOnInsert } - orderInitialBlockItems={ orderInitialBlockItems } /> ); } @@ -426,13 +424,7 @@ export const ComposedPrivateInserter = compose( [ ] )( PrivateInserter ); const Inserter = forwardRef( ( props, ref ) => { - return ( - <ComposedPrivateInserter - ref={ ref } - { ...props } - orderInitialBlockItems={ undefined } - /> - ); + return <ComposedPrivateInserter ref={ ref } { ...props } />; } ); export default Inserter; diff --git a/packages/block-editor/src/components/inserter/quick-inserter.js b/packages/block-editor/src/components/inserter/quick-inserter.js index 9fe96f091a86e8..540b51a4757e0d 100644 --- a/packages/block-editor/src/components/inserter/quick-inserter.js +++ b/packages/block-editor/src/components/inserter/quick-inserter.js @@ -32,7 +32,6 @@ export default function QuickInserter( { isAppender, prioritizePatterns, selectBlockOnInsert, - orderInitialBlockItems, } ) { const [ filterValue, setFilterValue ] = useState( '' ); const [ destinationRootClientId, onInsertBlocks ] = useInsertionPoint( { @@ -125,7 +124,6 @@ export default function QuickInserter( { isDraggable={ false } prioritizePatterns={ prioritizePatterns } selectBlockOnInsert={ selectBlockOnInsert } - orderInitialBlockItems={ orderInitialBlockItems } /> </div> diff --git a/packages/block-editor/src/components/inserter/search-results.js b/packages/block-editor/src/components/inserter/search-results.js index 6dc85af2653311..b2dc15d586adf9 100644 --- a/packages/block-editor/src/components/inserter/search-results.js +++ b/packages/block-editor/src/components/inserter/search-results.js @@ -6,6 +6,7 @@ import { __, _n, sprintf } from '@wordpress/i18n'; import { VisuallyHidden } from '@wordpress/components'; import { useDebounce, useAsyncList } from '@wordpress/compose'; import { speak } from '@wordpress/a11y'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -21,6 +22,7 @@ import useBlockTypesState from './hooks/use-block-types-state'; import { searchBlockItems, searchItems } from './search-items'; import InserterListbox from '../inserter-listbox'; import { orderBy } from '../../utils/sorting'; +import { store as blockEditorStore } from '../../store'; const INITIAL_INSERTER_RESULTS = 9; /** @@ -31,6 +33,24 @@ const INITIAL_INSERTER_RESULTS = 9; */ const EMPTY_ARRAY = []; +const orderInitialBlockItems = ( items, priority ) => { + if ( ! priority ) { + return items; + } + + items.sort( ( { id: aName }, { id: bName } ) => { + // Sort block items according to `priority`. + let aIndex = priority.indexOf( aName ); + let bIndex = priority.indexOf( bName ); + // All other block items should come after that. + if ( aIndex < 0 ) aIndex = priority.length; + if ( bIndex < 0 ) bIndex = priority.length; + return aIndex - bIndex; + } ); + + return items; +}; + function InserterSearchResults( { filterValue, onSelect, @@ -46,10 +66,22 @@ function InserterSearchResults( { shouldFocusBlock = true, prioritizePatterns, selectBlockOnInsert, - orderInitialBlockItems, } ) { const debouncedSpeak = useDebounce( speak, 500 ); + const { prioritizedBlocks } = useSelect( + ( select ) => { + const blockListSettings = + select( blockEditorStore ).getBlockListSettings( rootClientId ); + + return { + prioritizedBlocks: + blockListSettings?.prioritizedInserterBlocks || EMPTY_ARRAY, + }; + }, + [ rootClientId ] + ); + const [ destinationRootClientId, onInsertBlocks ] = useInsertionPoint( { onSelect, rootClientId, @@ -89,10 +121,16 @@ function InserterSearchResults( { if ( maxBlockTypesToShow === 0 ) { return []; } + let orderedItems = orderBy( blockTypes, 'frecency', 'desc' ); - if ( ! filterValue && orderInitialBlockItems ) { - orderedItems = orderInitialBlockItems( orderedItems ); + + if ( ! filterValue && prioritizedBlocks.length ) { + orderedItems = orderInitialBlockItems( + orderedItems, + prioritizedBlocks + ); } + const results = searchBlockItems( orderedItems, blockTypeCategories, @@ -108,8 +146,8 @@ function InserterSearchResults( { blockTypes, blockTypeCategories, blockTypeCollections, - maxBlockTypes, - orderInitialBlockItems, + maxBlockTypesToShow, + prioritizedBlocks, ] ); // Announce search results on change. @@ -124,7 +162,12 @@ function InserterSearchResults( { count ); debouncedSpeak( resultsFoundMessage ); - }, [ filterValue, debouncedSpeak ] ); + }, [ + filterValue, + debouncedSpeak, + filteredBlockTypes, + filteredBlockPatterns, + ] ); const currentShownBlockTypes = useAsyncList( filteredBlockTypes, { step: INITIAL_INSERTER_RESULTS, diff --git a/packages/block-editor/src/components/off-canvas-editor/appender.js b/packages/block-editor/src/components/off-canvas-editor/appender.js index 5f981d5a90ca51..1b91f5bdd76845 100644 --- a/packages/block-editor/src/components/off-canvas-editor/appender.js +++ b/packages/block-editor/src/components/off-canvas-editor/appender.js @@ -4,12 +4,7 @@ import { useInstanceId } from '@wordpress/compose'; import { speak } from '@wordpress/a11y'; import { useSelect } from '@wordpress/data'; -import { - forwardRef, - useState, - useEffect, - useCallback, -} from '@wordpress/element'; +import { forwardRef, useState, useEffect } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; /** @@ -19,11 +14,6 @@ import { store as blockEditorStore } from '../../store'; import useBlockDisplayTitle from '../block-title/use-block-display-title'; import { ComposedPrivateInserter as PrivateInserter } from '../inserter'; -const prioritizedInserterBlocks = [ - 'core/navigation-link/page', - 'core/navigation-link', -]; - export const Appender = forwardRef( ( { nestingLevel, blockCount, clientId, ...props }, ref ) => { const [ insertedBlock, setInsertedBlock ] = useState( null ); @@ -68,19 +58,6 @@ export const Appender = forwardRef( ); }, [ insertedBlockTitle ] ); - const orderInitialBlockItems = useCallback( ( items ) => { - items.sort( ( { id: aName }, { id: bName } ) => { - // Sort block items according to `prioritizedInserterBlocks`. - let aIndex = prioritizedInserterBlocks.indexOf( aName ); - let bIndex = prioritizedInserterBlocks.indexOf( bName ); - // All other block items should come after that. - if ( aIndex < 0 ) aIndex = prioritizedInserterBlocks.length; - if ( bIndex < 0 ) bIndex = prioritizedInserterBlocks.length; - return aIndex - bIndex; - } ); - return items; - }, [] ); - if ( hideInserter ) { return null; } @@ -110,7 +87,6 @@ export const Appender = forwardRef( setInsertedBlock( maybeInsertedBlock ); } } } - orderInitialBlockItems={ orderInitialBlockItems } /> <div className="offcanvas-editor-appender__description" diff --git a/packages/block-library/src/navigation/constants.js b/packages/block-library/src/navigation/constants.js index 50579780522435..9bdf8736d7c2b6 100644 --- a/packages/block-library/src/navigation/constants.js +++ b/packages/block-library/src/navigation/constants.js @@ -14,3 +14,8 @@ export const ALLOWED_BLOCKS = [ 'core/navigation-submenu', 'core/loginout', ]; + +export const PRIORITIZED_INSERTER_BLOCKS = [ + 'core/navigation-link/page', + 'core/navigation-link', +]; diff --git a/packages/block-library/src/navigation/edit/inner-blocks.js b/packages/block-library/src/navigation/edit/inner-blocks.js index 17a6896a80fdbe..669703f002dbb2 100644 --- a/packages/block-library/src/navigation/edit/inner-blocks.js +++ b/packages/block-library/src/navigation/edit/inner-blocks.js @@ -14,7 +14,11 @@ import { useMemo } from '@wordpress/element'; * Internal dependencies */ import PlaceholderPreview from './placeholder/placeholder-preview'; -import { DEFAULT_BLOCK, ALLOWED_BLOCKS } from '../constants'; +import { + DEFAULT_BLOCK, + ALLOWED_BLOCKS, + PRIORITIZED_INSERTER_BLOCKS, +} from '../constants'; export default function NavigationInnerBlocks( { clientId, @@ -93,6 +97,7 @@ export default function NavigationInnerBlocks( { onInput, onChange, allowedBlocks: ALLOWED_BLOCKS, + prioritizedInserterBlocks: PRIORITIZED_INSERTER_BLOCKS, __experimentalDefaultBlock: DEFAULT_BLOCK, __experimentalDirectInsert: shouldDirectInsert, orientation, diff --git a/packages/e2e-test-utils/src/get-all-block-inserter-item-titles.js b/packages/e2e-test-utils/src/get-all-block-inserter-item-titles.js index d9d27541ac6ef6..5c0525c80b2b8c 100644 --- a/packages/e2e-test-utils/src/get-all-block-inserter-item-titles.js +++ b/packages/e2e-test-utils/src/get-all-block-inserter-item-titles.js @@ -20,5 +20,5 @@ export async function getAllBlockInserterItemTitles() { return inserterItem.innerText; } ); } ); - return [ ...new Set( inserterItemTitles ) ].sort(); + return [ ...new Set( inserterItemTitles ) ]; } diff --git a/packages/e2e-tests/plugins/inner-blocks-prioritized-inserter-blocks.php b/packages/e2e-tests/plugins/inner-blocks-prioritized-inserter-blocks.php new file mode 100644 index 00000000000000..8a2feb9f35a178 --- /dev/null +++ b/packages/e2e-tests/plugins/inner-blocks-prioritized-inserter-blocks.php @@ -0,0 +1,28 @@ +<?php +/** + * Plugin Name: Gutenberg Test InnerBlocks Prioritized Inserter Blocks + * Plugin URI: https://github.com/WordPress/gutenberg + * Author: Gutenberg Team + * + * @package gutenberg-test-inner-blocks-prioritized-inserter-blocks + */ + +/** + * Registers a custom script for the plugin. + */ +function enqueue_inner_blocks_prioritized_inserter_blocks_script() { + wp_enqueue_script( + 'gutenberg-test-inner-blocks-prioritized-inserter-blocks', + plugins_url( 'inner-blocks-prioritized-inserter-blocks/index.js', __FILE__ ), + array( + 'wp-blocks', + 'wp-block-editor', + 'wp-element', + 'wp-i18n', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'inner-blocks-prioritized-inserter-blocks/index.js' ), + true + ); +} + +add_action( 'init', 'enqueue_inner_blocks_prioritized_inserter_blocks_script' ); diff --git a/packages/e2e-tests/plugins/inner-blocks-prioritized-inserter-blocks/index.js b/packages/e2e-tests/plugins/inner-blocks-prioritized-inserter-blocks/index.js new file mode 100644 index 00000000000000..d5ded59237a182 --- /dev/null +++ b/packages/e2e-tests/plugins/inner-blocks-prioritized-inserter-blocks/index.js @@ -0,0 +1,82 @@ +( function () { + const { registerBlockType } = wp.blocks; + const { createElement: el } = wp.element; + const { InnerBlocks } = wp.blockEditor; + + const divProps = { + className: 'product', + style: { outline: '1px solid gray', padding: 5 }, + }; + + // without a placeholder within the inner blocks it can be difficult to select the block using e2e tests + // especially using Puppeteer, so we use an image block which has a placeholder. + const template = [ + [ 'core/image' ], + ]; + + const save = function () { + return el( 'div', divProps, el( InnerBlocks.Content ) ); + }; + registerBlockType( 'test/prioritized-inserter-blocks-unset', { + title: 'Prioritized Inserter Blocks Unset', + icon: 'carrot', + category: 'text', + + edit() { + return el( 'div', divProps, el( InnerBlocks, { template } ) ); + }, + + save, + } ); + + registerBlockType( 'test/prioritized-inserter-blocks-set', { + title: 'Prioritized Inserter Blocks Set', + icon: 'carrot', + category: 'text', + edit() { + return el( + 'div', + divProps, + el( InnerBlocks, { + template, + prioritizedInserterBlocks: [ + 'core/audio', + 'core/spacer', + 'core/code', + ], + } ) + ); + }, + + save, + } ); + + registerBlockType( 'test/prioritized-inserter-blocks-set-with-conflicting-allowed-blocks', { + title: 'Prioritized Inserter Blocks Set With Conflicting Allowed Blocks', + icon: 'carrot', + category: 'text', + edit() { + return el( + 'div', + divProps, + el( InnerBlocks, { + template, + allowedBlocks: [ + 'core/spacer', + 'core/code', + 'core/paragraph', + 'core/heading' + ], + prioritizedInserterBlocks: [ + 'core/audio', // this is **not** in the allowedBlocks list + 'core/spacer', + 'core/code', + ], + } ) + ); + }, + + save, + } ); + +} )(); diff --git a/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js b/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js index 183813a9511362..c7ca368003397e 100644 --- a/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js +++ b/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js @@ -56,7 +56,8 @@ describe( 'Child Blocks', () => { '[data-type="test/child-blocks-restricted-parent"] .block-editor-default-block-appender' ); await openGlobalBlockInserter(); - expect( await getAllBlockInserterItemTitles() ).toEqual( [ + const allowedBlocks = await getAllBlockInserterItemTitles(); + expect( allowedBlocks.sort() ).toEqual( [ 'Child Blocks Child', 'Image', 'Paragraph', diff --git a/packages/e2e-tests/specs/editor/plugins/inner-blocks-allowed-blocks.test.js b/packages/e2e-tests/specs/editor/plugins/inner-blocks-allowed-blocks.test.js index 3eb1ee4775f74b..4431d3bd5802f0 100644 --- a/packages/e2e-tests/specs/editor/plugins/inner-blocks-allowed-blocks.test.js +++ b/packages/e2e-tests/specs/editor/plugins/inner-blocks-allowed-blocks.test.js @@ -50,7 +50,8 @@ describe( 'Allowed Blocks Setting on InnerBlocks', () => { await page.waitForSelector( childParagraphSelector ); await page.click( childParagraphSelector ); await openGlobalBlockInserter(); - expect( await getAllBlockInserterItemTitles() ).toEqual( [ + const allowedBlocks = await getAllBlockInserterItemTitles(); + expect( allowedBlocks.sort() ).toEqual( [ 'Button', 'Gallery', 'List', @@ -75,7 +76,7 @@ describe( 'Allowed Blocks Setting on InnerBlocks', () => { await page.$x( `//button//span[contains(text(), 'List')]` ) )[ 0 ]; await insertButton.click(); - // Select the list wrapper so the image is inserable. + // Select the list wrapper so the image is insertable. await page.keyboard.press( 'ArrowUp' ); await insertBlock( 'Image' ); await closeGlobalBlockInserter(); diff --git a/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js b/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js new file mode 100644 index 00000000000000..c34e402ea19806 --- /dev/null +++ b/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js @@ -0,0 +1,108 @@ +/** + * WordPress dependencies + */ +import { + activatePlugin, + createNewPost, + deactivatePlugin, + getAllBlockInserterItemTitles, + insertBlock, + closeGlobalBlockInserter, +} from '@wordpress/e2e-test-utils'; + +const QUICK_INSERTER_RESULTS_SELECTOR = + '.block-editor-inserter__quick-inserter-results'; + +describe( 'Prioritized Inserter Blocks Setting on InnerBlocks', () => { + beforeAll( async () => { + await activatePlugin( + 'gutenberg-test-innerblocks-prioritized-inserter-blocks' + ); + } ); + + beforeEach( async () => { + await createNewPost(); + } ); + + afterAll( async () => { + await deactivatePlugin( + 'gutenberg-test-innerblocks-prioritized-inserter-blocks' + ); + } ); + + describe( 'Quick inserter', () => { + it( 'uses defaulting ordering if prioritzed blocks setting was not set', async () => { + const parentBlockSelector = + '[data-type="test/prioritized-inserter-blocks-unset"]'; + await insertBlock( 'Prioritized Inserter Blocks Unset' ); + await closeGlobalBlockInserter(); + + await page.waitForSelector( parentBlockSelector ); + + await page.click( + `${ parentBlockSelector } .block-list-appender .block-editor-inserter__toggle` + ); + + await page.waitForSelector( QUICK_INSERTER_RESULTS_SELECTOR ); + + await expect( await getAllBlockInserterItemTitles() ).toHaveLength( + 6 + ); + } ); + + it( 'uses the priority ordering if prioritzed blocks setting is set', async () => { + const parentBlockSelector = + '[data-type="test/prioritized-inserter-blocks-set"]'; + await insertBlock( 'Prioritized Inserter Blocks Set' ); + await closeGlobalBlockInserter(); + + await page.waitForSelector( parentBlockSelector ); + + await page.click( + `${ parentBlockSelector } .block-list-appender .block-editor-inserter__toggle` + ); + + await page.waitForSelector( QUICK_INSERTER_RESULTS_SELECTOR ); + + // Should still be only 6 results regardless of the priority ordering. + const inserterItems = await getAllBlockInserterItemTitles(); + + // Should still be only 6 results regardless of the priority ordering. + expect( inserterItems ).toHaveLength( 6 ); + + expect( inserterItems.slice( 0, 3 ) ).toEqual( [ + 'Audio', + 'Spacer', + 'Code', + ] ); + } ); + + it( 'obeys allowed blocks over prioritzed blocks setting if conflicted', async () => { + const parentBlockSelector = + '[data-type="test/prioritized-inserter-blocks-set-with-conflicting-allowed-blocks"]'; + await insertBlock( + 'Prioritized Inserter Blocks Set With Conflicting Allowed Blocks' + ); + await closeGlobalBlockInserter(); + + await page.waitForSelector( parentBlockSelector ); + + await page.click( + `${ parentBlockSelector } .block-list-appender .block-editor-inserter__toggle` + ); + + await page.waitForSelector( QUICK_INSERTER_RESULTS_SELECTOR ); + + const inserterItems = await getAllBlockInserterItemTitles(); + + expect( inserterItems.slice( 0, 3 ) ).toEqual( [ + 'Spacer', + 'Code', + 'Paragraph', + ] ); + expect( inserterItems ).toEqual( + expect.not.arrayContaining( [ 'Audio' ] ) + ); + } ); + } ); +} ); From f8d74a6230b81a7980f492ba5c1a38cc23f28315 Mon Sep 17 00:00:00 2001 From: George Mamadashvili <georgemamadashvili@gmail.com> Date: Mon, 15 May 2023 20:24:19 +0400 Subject: [PATCH 045/131] Unlock useShouldContextualToolbarShow outside of the component (#50612) * Edit Post: Unlock useShouldContextualToolbarShow outside of the component * Feedback --- .../edit-post/src/components/header/header-toolbar/index.js | 4 ++-- packages/edit-site/src/components/header-edit-mode/index.js | 3 ++- packages/edit-widgets/src/components/header/index.js | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index c479741317459c..337c27bed00d99 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -26,6 +26,8 @@ import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as editPostStore } from '../../../store'; import { unlock } from '../../../private-apis'; +const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); + const preventDefault = ( event ) => { event.preventDefault(); }; @@ -67,8 +69,6 @@ function HeaderToolbar() { }; }, [] ); - const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); - const isLargeViewport = useViewportMatch( 'medium' ); const isWideViewport = useViewportMatch( 'wide' ); const { diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index 94ff901203e0c3..0878cb4faae2d2 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -45,6 +45,8 @@ import { } from '../editor-canvas-container'; import { unlock } from '../../private-apis'; +const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); + const preventDefault = ( event ) => { event.preventDefault(); }; @@ -126,7 +128,6 @@ export default function HeaderEditMode() { [ setIsListViewOpened, isListViewOpen ] ); - const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); const { shouldShowContextualToolbar, canFocusHiddenToolbar, diff --git a/packages/edit-widgets/src/components/header/index.js b/packages/edit-widgets/src/components/header/index.js index 8bd1e226aeda66..e2691f9c74c436 100644 --- a/packages/edit-widgets/src/components/header/index.js +++ b/packages/edit-widgets/src/components/header/index.js @@ -25,6 +25,8 @@ import useLastSelectedWidgetArea from '../../hooks/use-last-selected-widget-area import { store as editWidgetsStore } from '../../store'; import { unlock } from '../../private-apis'; +const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); + function Header() { const isMediumViewport = useViewportMatch( 'medium' ); const inserterButton = useRef(); @@ -72,7 +74,6 @@ function Header() { [ setIsListViewOpened, isListViewOpen ] ); - const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); const { shouldShowContextualToolbar, canFocusHiddenToolbar, From a0e2d1dc8b9cb6b7aa4d93e6a5b5be5b332e4b41 Mon Sep 17 00:00:00 2001 From: Glen Davies <glendaviesnz@users.noreply.github.com> Date: Tue, 16 May 2023 10:55:23 +1200 Subject: [PATCH 046/131] Give template deleted snackbar an id so only one appears at a time (#50625) --- packages/edit-site/src/store/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 66edf87d9d8a0b..d1f41d5a62aca5 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -146,7 +146,7 @@ export const removeTemplate = __( '"%s" deleted.' ), template.title.rendered ), - { type: 'snackbar' } + { type: 'snackbar', id: 'site-editor-template-deleted-success' } ); } catch ( error ) { const errorMessage = From fc714ab6704e8c159bcfbbdd63b26e76fe1c346c Mon Sep 17 00:00:00 2001 From: Glen Davies <glendaviesnz@users.noreply.github.com> Date: Tue, 16 May 2023 10:57:02 +1200 Subject: [PATCH 047/131] Removes the Post Content block from the inserter in the post editor as it can't be used there (#50620) --- packages/edit-post/src/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 1e0509f3e8f218..156c434e7a3761 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -79,7 +79,7 @@ export function initializeEditor( } /* - * Prevent adding template part in the post editor. + * Prevent adding template part and post content block in the post editor. * Only add the filter when the post editor is initialized, not imported. * Also only add the filter(s) after registerCoreBlocks() * so that common filters in the block library are not overwritten. @@ -90,7 +90,8 @@ export function initializeEditor( ( canInsert, blockType ) => { if ( ! select( editPostStore ).isEditingTemplate() && - blockType.name === 'core/template-part' + ( blockType.name === 'core/template-part' || + blockType.name === 'core/post-content' ) ) { return false; } From b0d2da03af1507af41d8bd1bae498b5c3bc1d6da Mon Sep 17 00:00:00 2001 From: Glen Davies <glendaviesnz@users.noreply.github.com> Date: Tue, 16 May 2023 14:14:48 +1200 Subject: [PATCH 048/131] Add slug as classname to pattern block wrapper (#50641) --- packages/block-library/src/pattern/edit.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/pattern/edit.js b/packages/block-library/src/pattern/edit.js index c69ac359659cf5..c22536a59eb03f 100644 --- a/packages/block-library/src/pattern/edit.js +++ b/packages/block-library/src/pattern/edit.js @@ -65,7 +65,9 @@ const PatternEdit = ( { attributes, clientId } ) => { replaceBlocks, ] ); - const blockProps = useBlockProps(); + const blockProps = useBlockProps( { + className: slug?.replace( '/', '-' ), + } ); const innerBlocksProps = useInnerBlocksProps( blockProps, { templateLock: syncStatus === 'partial' ? 'contentOnly' : false, From 744ab2c735df9d7a197e7db2cb168c3ef79bffe6 Mon Sep 17 00:00:00 2001 From: Glen Davies <glendaviesnz@users.noreply.github.com> Date: Tue, 16 May 2023 16:56:24 +1200 Subject: [PATCH 049/131] Pattern block: update frontend render code to match the new version of syncStatus attrib (#50646) --- packages/block-library/src/pattern/index.php | 25 ++++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/block-library/src/pattern/index.php b/packages/block-library/src/pattern/index.php index 4af986c423d012..cb3be0370a4f6f 100644 --- a/packages/block-library/src/pattern/index.php +++ b/packages/block-library/src/pattern/index.php @@ -22,30 +22,35 @@ function register_block_core_pattern() { /** * Renders the `core/pattern` block on the server. * - * @param array $attributes Block attributes. - * @param string $content The block rendered content. + * @param array $attributes Block attributes. * * @return string Returns the output of the pattern. */ -function render_block_core_pattern( $attributes, $content ) { +function render_block_core_pattern( $attributes ) { if ( empty( $attributes['slug'] ) ) { return ''; } - $slug_classname = str_replace( '/', '-', $attributes['slug'] ); - $classnames = isset( $attributes['className'] ) ? $attributes['className'] . ' ' . $slug_classname : $slug_classname; - $wrapper = '<div class="' . esc_attr( $classnames ) . '">%s</div>'; - - if ( isset( $attributes['syncStatus'] ) && 'unsynced' === $attributes['syncStatus'] ) { - return sprintf( $wrapper, $content ); - } $slug = $attributes['slug']; $registry = WP_Block_Patterns_Registry::get_instance(); + if ( ! $registry->is_registered( $slug ) ) { return ''; } $pattern = $registry->get_registered( $slug ); + + // Currently all existing blocks should be returned here without a wp-block-pattern wrapper + // as the syncStatus attribute is only used if the gutenberg-pattern-enhancements experiment + // is enabled. + if ( ! isset( $attributes['syncStatus'] ) ) { + return do_blocks( $pattern['content'] ); + } + + $block_classnames = 'wp-block-pattern ' . str_replace( '/', '-', $attributes['slug'] ); + $classnames = isset( $attributes['className'] ) ? $attributes['className'] . ' ' . $block_classnames : $block_classnames; + $wrapper = '<div class="' . esc_attr( $classnames ) . '">%s</div>'; + return sprintf( $wrapper, do_blocks( $pattern['content'] ) ); } From 4adde6c94a5fbfad2f5876349cb182a18ee041b8 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Tue, 16 May 2023 11:07:42 +0300 Subject: [PATCH 050/131] List block: fix merging nested list into paragraph (#50634) --- .../src/components/block-list/block.js | 56 ++++++++-------- .../block-library/src/list-item/transforms.js | 6 +- test/e2e/specs/editor/blocks/list.spec.js | 65 +++++++++++++++++++ 3 files changed, 97 insertions(+), 30 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 3980dd7b2aead3..c385ea4cd6367f 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -376,26 +376,26 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { ) { removeBlock( _clientId ); } else { - if ( - canInsertBlockType( - getBlockName( firstClientId ), - targetRootClientId - ) - ) { - moveBlocksToPosition( - [ firstClientId ], - _clientId, - targetRootClientId, - getBlockIndex( _clientId ) - ); - } else { - const replacement = switchToBlockType( - getBlock( firstClientId ), - getDefaultBlockName() - ); - - if ( replacement && replacement.length ) { - registry.batch( () => { + registry.batch( () => { + if ( + canInsertBlockType( + getBlockName( firstClientId ), + targetRootClientId + ) + ) { + moveBlocksToPosition( + [ firstClientId ], + _clientId, + targetRootClientId, + getBlockIndex( _clientId ) + ); + } else { + const replacement = switchToBlockType( + getBlock( firstClientId ), + getDefaultBlockName() + ); + + if ( replacement && replacement.length ) { insertBlocks( replacement, getBlockIndex( _clientId ), @@ -403,16 +403,16 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { changeSelection ); removeBlock( firstClientId, false ); - } ); + } } - } - if ( - ! getBlockOrder( _clientId ).length && - isUnmodifiedBlock( getBlock( _clientId ) ) - ) { - removeBlock( _clientId, false ); - } + if ( + ! getBlockOrder( _clientId ).length && + isUnmodifiedBlock( getBlock( _clientId ) ) + ) { + removeBlock( _clientId, false ); + } + } ); } } diff --git a/packages/block-library/src/list-item/transforms.js b/packages/block-library/src/list-item/transforms.js index 6e05f8501b5a3b..7918b45a6b0279 100644 --- a/packages/block-library/src/list-item/transforms.js +++ b/packages/block-library/src/list-item/transforms.js @@ -1,15 +1,17 @@ /** * WordPress dependencies */ -import { createBlock } from '@wordpress/blocks'; +import { createBlock, cloneBlock } from '@wordpress/blocks'; const transforms = { to: [ { type: 'block', blocks: [ 'core/paragraph' ], - transform: ( attributes ) => + transform: ( attributes, innerBlocks = [] ) => [ createBlock( 'core/paragraph', attributes ), + ...innerBlocks.map( ( block ) => cloneBlock( block ) ), + ], }, ], }; diff --git a/test/e2e/specs/editor/blocks/list.spec.js b/test/e2e/specs/editor/blocks/list.spec.js index bdf366fd73f47b..a4af98f0ba0578 100644 --- a/test/e2e/specs/editor/blocks/list.spec.js +++ b/test/e2e/specs/editor/blocks/list.spec.js @@ -486,6 +486,71 @@ test.describe( 'List (@firefox)', () => { ); } ); + test( 'should keep nested list items when merging with paragraph', async ( { + editor, + page, + pageUtils, + } ) => { + const startingContent = [ + { + name: 'core/paragraph', + attributes: { content: 'p' }, + }, + { + name: 'core/list', + innerBlocks: [ + { + name: 'core/list-item', + attributes: { content: '1' }, + innerBlocks: [ + { + name: 'core/list', + innerBlocks: [ + { + name: 'core/list-item', + attributes: { content: 'i' }, + }, + ], + }, + ], + }, + ], + }, + ]; + for ( const block of startingContent ) { + await editor.insertBlock( block ); + } + + // Move the caret in front of "1" in the first list item. + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Backspace' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'p' }, + }, + { + name: 'core/paragraph', + attributes: { content: '1' }, + }, + { + name: 'core/list', + innerBlocks: [ + { + name: 'core/list-item', + attributes: { content: 'i' }, + }, + ], + }, + ] ); + + await pageUtils.pressKeys( 'primary+z' ); + + await expect.poll( editor.getBlocks ).toMatchObject( startingContent ); + } ); + test( 'should split into two ordered lists with paragraph', async ( { editor, page, From 25dbbbb9689025311c4375229cc799d2cf46303e Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 16 May 2023 18:21:22 +1000 Subject: [PATCH 051/131] Dimensions Panel: Fix resetting of axial spacing controls (#50654) --- .../src/components/global-styles/dimensions-panel.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index 28b2f80fc12d64..d884f46bfda1cf 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -103,8 +103,9 @@ function useHasSpacingPresets( settings ) { } function filterValuesBySides( values, sides ) { - if ( ! sides ) { - // If no custom side configuration all sides are opted into by default. + // If no custom side configuration, all sides are opted into by default. + // Without any values, we have nothing to filter either. + if ( ! sides || ! values ) { return values; } From b9b7c1bd08005bbc6805fdd260d20a1b103874ff Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Tue, 16 May 2023 11:16:35 +0100 Subject: [PATCH 052/131] Add contextual commands (#50543) --- .../commands/src/components/command-menu.js | 69 ++++++++-------- packages/commands/src/components/style.scss | 9 ++- .../commands/src/hooks/use-command-context.js | 32 ++++++++ .../commands/src/hooks/use-command-loader.js | 18 +++-- packages/commands/src/hooks/use-command.js | 6 +- packages/commands/src/private-apis.js | 2 + packages/commands/src/store/actions.js | 48 ++++++----- packages/commands/src/store/reducer.js | 59 +++++++------- packages/commands/src/store/selectors.js | 38 +++++---- .../src/site-editor-navigation-commands.js | 52 ++++++------ .../edit-site/src/components/layout/index.js | 20 ++++- .../hooks/commands/use-edit-mode-commands.js | 80 +++++++++++++++++++ 12 files changed, 290 insertions(+), 143 deletions(-) create mode 100644 packages/commands/src/hooks/use-command-context.js create mode 100644 packages/edit-site/src/hooks/commands/use-edit-mode-commands.js diff --git a/packages/commands/src/components/command-menu.js b/packages/commands/src/components/command-menu.js index d7bafca0a3931f..75bc6f4e827a26 100644 --- a/packages/commands/src/components/command-menu.js +++ b/packages/commands/src/components/command-menu.js @@ -31,13 +31,13 @@ function CommandMenuLoader( { name, search, hook, setLoader, close } ) { setLoader( name, isLoading ); }, [ setLoader, name, isLoading ] ); + if ( ! commands.length ) { + return null; + } + return ( <> <Command.List> - { isLoading && ( - <Command.Loading>{ __( 'Searching…' ) }</Command.Loading> - ) } - { commands.map( ( command ) => ( <Command.Item key={ command.name } @@ -89,18 +89,22 @@ export function CommandMenuLoaderWrapper( { hook, search, setLoader, close } ) { ); } -export function CommandMenuGroup( { group, search, setLoader, close } ) { +export function CommandMenuGroup( { isContextual, search, setLoader, close } ) { const { commands, loaders } = useSelect( ( select ) => { const { getCommands, getCommandLoaders } = select( commandsStore ); return { - commands: getCommands( group ), - loaders: getCommandLoaders( group ), + commands: getCommands( isContextual ), + loaders: getCommandLoaders( isContextual ), }; }, - [ group ] + [ isContextual ] ); + if ( ! commands.length && ! loaders.length ) { + return null; + } + return ( <Command.Group> { commands.map( ( command ) => ( @@ -139,13 +143,10 @@ export function CommandMenuGroup( { group, search, setLoader, close } ) { export function CommandMenu() { const { registerShortcut } = useDispatch( keyboardShortcutsStore ); const [ search, setSearch ] = useState( '' ); - const { groups, isOpen } = useSelect( ( select ) => { - const { getGroups, isOpen: _isOpen } = select( commandsStore ); - return { - groups: getGroups(), - isOpen: _isOpen(), - }; - }, [] ); + const isOpen = useSelect( + ( select ) => select( commandsStore ).isOpen(), + [] + ); const { open, close } = useDispatch( commandsStore ); const [ loaders, setLoaders ] = useState( {} ); const commandMenuInput = useRef(); @@ -219,24 +220,26 @@ export function CommandMenu() { placeholder={ __( 'Type a command or search' ) } /> </div> - { search && ( - <Command.List> - { ! isLoading && ( - <Command.Empty> - { __( 'No results found.' ) } - </Command.Empty> - ) } - { groups.map( ( group ) => ( - <CommandMenuGroup - key={ group } - group={ group } - search={ search } - setLoader={ setLoader } - close={ closeAndReset } - /> - ) ) } - </Command.List> - ) } + <Command.List> + { search && ! isLoading && ( + <Command.Empty> + { __( 'No results found.' ) } + </Command.Empty> + ) } + <CommandMenuGroup + search={ search } + setLoader={ setLoader } + close={ closeAndReset } + isContextual + /> + { search && ( + <CommandMenuGroup + search={ search } + setLoader={ setLoader } + close={ closeAndReset } + /> + ) } + </Command.List> </Command> </div> </Modal> diff --git a/packages/commands/src/components/style.scss b/packages/commands/src/components/style.scss index f6f55e95103d09..11114cce856ba5 100644 --- a/packages/commands/src/components/style.scss +++ b/packages/commands/src/components/style.scss @@ -93,7 +93,10 @@ [cmdk-root] > [cmdk-list] { max-height: 400px; overflow: auto; - padding: $grid-unit; + + & > [cmdk-list-sizer] :has([cmdk-group-items]:not(:empty)) { + padding: $grid-unit; + } } [cmdk-empty] { @@ -112,6 +115,10 @@ [cmdk-list-sizer] { position: relative; } + + [cmdk-group]:has([cmdk-group-items]:not(:empty)) + [cmdk-group]:has([cmdk-group-items]:not(:empty)) { + border-top: 1px solid $gray-200; + } } .commands-command-menu__item mark { diff --git a/packages/commands/src/hooks/use-command-context.js b/packages/commands/src/hooks/use-command-context.js new file mode 100644 index 00000000000000..c53e4131890c7e --- /dev/null +++ b/packages/commands/src/hooks/use-command-context.js @@ -0,0 +1,32 @@ +/** + * WordPress dependencies + */ +import { useEffect, useRef } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as commandsStore } from '../store'; + +/** + * Sets the active context of the command center + * + * @param {string} context Context to set. + */ +export default function useCommandContext( context ) { + const { getContext } = useSelect( commandsStore ); + const initialContext = useRef( getContext() ); + const { setContext } = useDispatch( commandsStore ); + + useEffect( () => { + setContext( context ); + }, [ context, setContext ] ); + + // This effects ensures that on unmount, we restore the context + // that was set before the component actually mounts. + useEffect( () => { + const initialContextRef = initialContext.current; + return () => setContext( initialContextRef ); + }, [ setContext ] ); +} diff --git a/packages/commands/src/hooks/use-command-loader.js b/packages/commands/src/hooks/use-command-loader.js index ac81873c01a2ea..084b8fb0fba24f 100644 --- a/packages/commands/src/hooks/use-command-loader.js +++ b/packages/commands/src/hooks/use-command-loader.js @@ -14,17 +14,23 @@ import { store as commandsStore } from '../store'; * * @param {import('../store/actions').WPCommandLoaderConfig} loader command loader config. */ -export default function useCommandLoader( { name, group, hook } ) { +export default function useCommandLoader( loader ) { const { registerCommandLoader, unregisterCommandLoader } = useDispatch( commandsStore ); useEffect( () => { registerCommandLoader( { - name, - group, - hook, + name: loader.name, + hook: loader.hook, + context: loader.context, } ); return () => { - unregisterCommandLoader( name, group ); + unregisterCommandLoader( loader.name ); }; - }, [ name, group, hook, registerCommandLoader, unregisterCommandLoader ] ); + }, [ + loader.name, + loader.hook, + loader.context, + registerCommandLoader, + unregisterCommandLoader, + ] ); } diff --git a/packages/commands/src/hooks/use-command.js b/packages/commands/src/hooks/use-command.js index 4ed8f1bcd930e5..d3edbf4e17fdc0 100644 --- a/packages/commands/src/hooks/use-command.js +++ b/packages/commands/src/hooks/use-command.js @@ -24,19 +24,19 @@ export default function useCommand( command ) { useEffect( () => { registerCommand( { name: command.name, - group: command.group, + context: command.context, label: command.label, icon: command.icon, callback: currentCallback.current, } ); return () => { - unregisterCommand( command.name, command.group ); + unregisterCommand( command.name ); }; }, [ command.name, command.label, - command.group, command.icon, + command.context, registerCommand, unregisterCommand, ] ); diff --git a/packages/commands/src/private-apis.js b/packages/commands/src/private-apis.js index a116c089fb8414..3e8bfab2343c18 100644 --- a/packages/commands/src/private-apis.js +++ b/packages/commands/src/private-apis.js @@ -8,6 +8,7 @@ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/pri */ import { default as useCommand } from './hooks/use-command'; import { default as useCommandLoader } from './hooks/use-command-loader'; +import { default as useCommandContext } from './hooks/use-command-context'; import { store } from './store'; export const { lock, unlock } = @@ -20,5 +21,6 @@ export const privateApis = {}; lock( privateApis, { useCommand, useCommandLoader, + useCommandContext, store, } ); diff --git a/packages/commands/src/store/actions.js b/packages/commands/src/store/actions.js index 66d3c62af57065..81f3ddf7222089 100644 --- a/packages/commands/src/store/actions.js +++ b/packages/commands/src/store/actions.js @@ -7,7 +7,7 @@ * * @property {string} name Command name. * @property {string} label Command label. - * @property {string=} group Command group. + * @property {string=} context Command context. * @property {JSX.Element} icon Command icon. * @property {Function} callback Command callback. */ @@ -21,9 +21,9 @@ * * @typedef {Object} WPCommandLoaderConfig * - * @property {string} name Command loader name. - * @property {string=} group Command loader group. - * @property {WPCommandLoaderHook} hook Command loader hook. + * @property {string} name Command loader name. + * @property {string=} context Command loader context. + * @property {WPCommandLoaderHook} hook Command loader hook. */ /** @@ -33,30 +33,24 @@ * * @return {Object} action. */ -export function registerCommand( { name, label, icon, callback, group = '' } ) { +export function registerCommand( config ) { return { type: 'REGISTER_COMMAND', - name, - label, - icon, - callback, - group, + ...config, }; } /** * Returns an action object used to unregister a command. * - * @param {string} name Command name. - * @param {string} group Command group. + * @param {string} name Command name. * * @return {Object} action. */ -export function unregisterCommand( name, group ) { +export function unregisterCommand( name ) { return { type: 'UNREGISTER_COMMAND', name, - group, }; } @@ -67,28 +61,24 @@ export function unregisterCommand( name, group ) { * * @return {Object} action. */ -export function registerCommandLoader( { name, group = '', hook } ) { +export function registerCommandLoader( config ) { return { type: 'REGISTER_COMMAND_LOADER', - name, - group, - hook, + ...config, }; } /** * Unregister command loader hook. * - * @param {string} name Command loader name. - * @param {string} group Command loader group. + * @param {string} name Command loader name. * * @return {Object} action. */ -export function unregisterCommandLoader( name, group ) { +export function unregisterCommandLoader( name ) { return { type: 'UNREGISTER_COMMAND_LOADER', name, - group, }; } @@ -113,3 +103,17 @@ export function close() { type: 'CLOSE', }; } + +/** + * Sets the active context. + * + * @param {string} context Context. + * + * @return {Object} action. + */ +export function setContext( context ) { + return { + type: 'SET_CONTEXT', + context, + }; +} diff --git a/packages/commands/src/store/reducer.js b/packages/commands/src/store/reducer.js index df2e4bfdc7a791..e61de53d21a706 100644 --- a/packages/commands/src/store/reducer.js +++ b/packages/commands/src/store/reducer.js @@ -16,24 +16,17 @@ function commands( state = {}, action ) { case 'REGISTER_COMMAND': return { ...state, - [ action.group ]: { - ...state[ action.group ], - [ action.name ]: { - name: action.name, - label: action.label, - group: action.group, - callback: action.callback, - icon: action.icon, - }, + [ action.name ]: { + name: action.name, + label: action.label, + context: action.context, + callback: action.callback, + icon: action.icon, }, }; case 'UNREGISTER_COMMAND': { - const { [ action.name ]: _, ...remainingState } = - state?.[ action.group ]; - return { - ...state, - [ action.group ]: remainingState, - }; + const { [ action.name ]: _, ...remainingState } = state; + return remainingState; } } @@ -53,21 +46,15 @@ function commandLoaders( state = {}, action ) { case 'REGISTER_COMMAND_LOADER': return { ...state, - [ action.group ]: { - ...state[ action.group ], - [ action.name ]: { - name: action.name, - hook: action.hook, - }, + [ action.name ]: { + name: action.name, + context: action.context, + hook: action.hook, }, }; case 'UNREGISTER_COMMAND_LOADER': { - const { [ action.name ]: _, ...remainingState } = - state?.[ action.group ]; - return { - ...state, - [ action.group ]: remainingState, - }; + const { [ action.name ]: _, ...remainingState } = state; + return remainingState; } } @@ -93,10 +80,28 @@ function isOpen( state = false, action ) { return state; } +/** + * Reducer returning the command center's active context. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +function context( state = 'root', action ) { + switch ( action.type ) { + case 'SET_CONTEXT': + return action.context; + } + + return state; +} + const reducer = combineReducers( { commands, commandLoaders, isOpen, + context, } ); export default reducer; diff --git a/packages/commands/src/store/selectors.js b/packages/commands/src/store/selectors.js index 4b2d9cc8147813..7aba1ba9fb7cf3 100644 --- a/packages/commands/src/store/selectors.js +++ b/packages/commands/src/store/selectors.js @@ -3,32 +3,30 @@ */ import createSelector from 'rememo'; -function unique( array ) { - return array.filter( - ( value, index, current ) => current.indexOf( value ) === index - ); -} - -export const getGroups = createSelector( - ( state ) => - unique( - Object.keys( state.commands ).concat( - Object.keys( state.commandLoaders ) - ) - ), - ( state ) => [ state.commands, state.commandLoaders ] -); - export const getCommands = createSelector( - ( state, group ) => Object.values( state.commands[ group ] ?? {} ), - ( state, group ) => [ state.commands[ group ] ] + ( state, contextual = false ) => + Object.values( state.commands ).filter( ( command ) => { + const isContextual = + command.context && command.context === state.context; + return contextual ? isContextual : ! isContextual; + } ), + ( state ) => [ state.commands ] ); export const getCommandLoaders = createSelector( - ( state, group ) => Object.values( state.commandLoaders[ group ] ?? {} ), - ( state, group ) => [ state.commandLoaders[ group ] ] + ( state, contextual = false ) => + Object.values( state.commandLoaders ).filter( ( loader ) => { + const isContextual = + loader.context && loader.context === state.context; + return contextual ? isContextual : ! isContextual; + } ), + ( state ) => [ state.commandLoaders ] ); export function isOpen( state ) { return state.isOpen; } + +export function getContext( state ) { + return state.context; +} diff --git a/packages/core-commands/src/site-editor-navigation-commands.js b/packages/core-commands/src/site-editor-navigation-commands.js index f6524e3ba0211a..310b0c1c1e17a7 100644 --- a/packages/core-commands/src/site-editor-navigation-commands.js +++ b/packages/core-commands/src/site-editor-navigation-commands.js @@ -31,29 +31,31 @@ const getNavigationCommandLoaderPerPostType = ( postType ) => const supportsSearch = ! [ 'wp_template', 'wp_template_part' ].includes( postType ); - const deps = supportsSearch ? [ search ] : []; - const { records, isLoading } = useSelect( ( select ) => { - const { getEntityRecords } = select( coreStore ); - const query = supportsSearch - ? { - search: !! search ? search : undefined, - per_page: 10, - orderby: search ? 'relevance' : 'date', - } - : { - per_page: -1, - }; - return { - records: getEntityRecords( 'postType', postType, query ), - isLoading: ! select( coreStore ).hasFinishedResolution( - 'getEntityRecords', - [ 'postType', postType, query ] - ), - // We're using the string literal to check whether we're in the site editor. - /* eslint-disable-next-line @wordpress/data-no-store-string-literals */ - isSiteEditor: !! select( 'edit-site' ), - }; - }, deps ); + const { records, isLoading } = useSelect( + ( select ) => { + const { getEntityRecords } = select( coreStore ); + const query = supportsSearch + ? { + search: !! search ? search : undefined, + per_page: 10, + orderby: search ? 'relevance' : 'date', + } + : { + per_page: -1, + }; + return { + records: getEntityRecords( 'postType', postType, query ), + isLoading: ! select( coreStore ).hasFinishedResolution( + 'getEntityRecords', + [ 'postType', postType, query ] + ), + // We're using the string literal to check whether we're in the site editor. + /* eslint-disable-next-line @wordpress/data-no-store-string-literals */ + isSiteEditor: !! select( 'edit-site' ), + }; + }, + [ supportsSearch, search ] + ); const commands = useMemo( () => { return ( records ?? [] ).slice( 0, 10 ).map( ( record ) => { @@ -108,22 +110,18 @@ const useTemplatePartNavigationCommandLoader = export function useSiteEditorNavigationCommands() { useCommandLoader( { name: 'core/edit-site/navigate-pages', - group: __( 'Pages' ), hook: usePageNavigationCommandLoader, } ); useCommandLoader( { name: 'core/edit-site/navigate-posts', - group: __( 'Posts' ), hook: usePostNavigationCommandLoader, } ); useCommandLoader( { name: 'core/edit-site/navigate-templates', - group: __( 'Templates' ), hook: useTemplateNavigationCommandLoader, } ); useCommandLoader( { name: 'core/edit-site/navigate-template-parts', - group: __( 'Template Parts' ), hook: useTemplatePartNavigationCommandLoader, } ); } diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 446556719c923e..0ca3c9b6422205 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -22,10 +22,13 @@ import { __ } from '@wordpress/i18n'; import { useState, useRef } from '@wordpress/element'; import { NavigableRegion } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; -import { CommandMenu } from '@wordpress/commands'; +import { + CommandMenu, + privateApis as commandsPrivateApis, +} from '@wordpress/commands'; import { store as preferencesStore } from '@wordpress/preferences'; import { privateApis as routerPrivateApis } from '@wordpress/router'; -import { privateApis as coreCmmandsPrivateApis } from '@wordpress/core-commands'; +import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands'; /** * Internal dependencies @@ -45,9 +48,10 @@ import { unlock } from '../../private-apis'; import SavePanel from '../save-panel'; import KeyboardShortcutsRegister from '../keyboard-shortcuts/register'; import KeyboardShortcutsGlobal from '../keyboard-shortcuts/global'; +import { useEditModeCommands } from '../../hooks/commands/use-edit-mode-commands'; -const { useCommands } = unlock( coreCmmandsPrivateApis ); - +const { useCommands } = unlock( coreCommandsPrivateApis ); +const { useCommandContext } = unlock( commandsPrivateApis ); const { useLocation } = unlock( routerPrivateApis ); const ANIMATION_DURATION = 0.5; @@ -68,6 +72,7 @@ export default function Layout() { useInitEditedEntityFromURL(); useSyncCanvasModeWithURL(); useCommands(); + useEditModeCommands(); const hubRef = useRef(); const { params } = useLocation(); @@ -122,6 +127,13 @@ export default function Layout() { canvasWidth = canvasSize.width - canvasPadding; } + // Sets the right context for the command center + const commandContext = + canvasMode === 'edit' && isEditorPage + ? 'site-editor-edit' + : 'site-editor'; + useCommandContext( commandContext ); + // Synchronizing the URL with the store value of canvasMode happens in an effect // This condition ensures the component is only rendered after the synchronization happens // which prevents any animations due to potential canvasMode value change. diff --git a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js new file mode 100644 index 00000000000000..59071a4088f426 --- /dev/null +++ b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js @@ -0,0 +1,80 @@ +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { trash, backup } from '@wordpress/icons'; +import { privateApis as commandsPrivateApis } from '@wordpress/commands'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../store'; +import useEditedEntityRecord from '../../components/use-edited-entity-record'; +import isTemplateRemovable from '../../utils/is-template-removable'; +import isTemplateRevertable from '../../utils/is-template-revertable'; +import { unlock } from '../../private-apis'; + +const { useCommandLoader } = unlock( commandsPrivateApis ); +const { useHistory } = unlock( routerPrivateApis ); + +function useEditModeCommandLoader() { + const { removeTemplate, revertTemplate } = useDispatch( editSiteStore ); + const history = useHistory(); + const { isLoaded, record: template } = useEditedEntityRecord(); + const isRemovable = + isLoaded && !! template && isTemplateRemovable( template ); + const isRevertable = + isLoaded && !! template && isTemplateRevertable( template ); + + const commands = []; + if ( isRemovable ) { + const label = + template.type === 'wp_template' + ? __( 'Delete template' ) + : __( 'Delete template part' ); + commands.push( { + name: label, + label, + icon: trash, + context: 'site-editor-edit', + callback: ( { close } ) => { + removeTemplate( template ); + // Navigate to the template list + history.push( { + path: '/' + template.type, + } ); + close(); + }, + } ); + } + if ( isRevertable ) { + const label = + template.type === 'wp_template' + ? __( 'Reset template' ) + : __( 'Reset template part' ); + commands.push( { + name: label, + label, + icon: backup, + callback: ( { close } ) => { + revertTemplate( template ); + close(); + }, + } ); + } + + return { + isLoading: ! isLoaded, + commands, + }; +} + +export function useEditModeCommands() { + useCommandLoader( { + name: 'core/edit-site/manipulate-document', + hook: useEditModeCommandLoader, + context: 'site-editor-edit', + } ); +} From 283c67410985f7f7a9b02f4a505e9d7825c4a2aa Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos <aristath@gmail.com> Date: Tue, 16 May 2023 13:19:09 +0300 Subject: [PATCH 053/131] Fix coding-standards issues in test themes (#50656) --- phpcs.xml.dist | 6 ++++++ test/emptytheme/functions.php | 10 +++++++++- test/emptytheme/index.php | 9 +++++++++ test/gutenberg-test-themes/emptyhybrid/functions.php | 8 ++++++++ test/gutenberg-test-themes/emptyhybrid/index.php | 8 ++++++++ test/gutenberg-test-themes/style-variations/index.php | 9 +++++++++ 6 files changed, 49 insertions(+), 1 deletion(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 5520b6898f15a6..6f40a38522ad6c 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -28,6 +28,7 @@ <rule ref="WordPress.WP.I18n.MissingArgDomainDefault"> <exclude-pattern>lib/compat/*</exclude-pattern> <exclude-pattern>packages/block-library/src/*</exclude-pattern> + <exclude-pattern>build/block-library/*</exclude-pattern> </rule> <arg value="ps"/> @@ -45,6 +46,10 @@ <!-- Exclude generated files --> <exclude-pattern>./packages/block-serialization-spec-parser/parser.php</exclude-pattern> + <exclude-pattern>node_modules/*</exclude-pattern> + + <!-- Exclude third party libraries --> + <exclude-pattern>./vendor/*</exclude-pattern> <!-- These special comments are markers for the build process --> <rule ref="Squiz.Commenting.InlineComment.WrongStyle"> @@ -57,6 +62,7 @@ </rule> <rule ref="Squiz.Commenting.FileComment.Missing"> <exclude-pattern>phpunit/*</exclude-pattern> + <exclude-pattern>**/*.min.asset.php</exclude-pattern> </rule> <rule ref="Squiz.Commenting.ClassComment.Missing"> <exclude-pattern>phpunit/*</exclude-pattern> diff --git a/test/emptytheme/functions.php b/test/emptytheme/functions.php index 5ee5593c5c8e4e..137cbef8de59d4 100644 --- a/test/emptytheme/functions.php +++ b/test/emptytheme/functions.php @@ -1,7 +1,15 @@ <?php +/** + * Empty theme functions and definitions. + * + * @package Gutenberg + */ if ( ! function_exists( 'emptytheme_support' ) ) : - function emptytheme_support() { + /** + * Add theme support for various features. + */ + function emptytheme_support() { // Adding support for core block visual styles. add_theme_support( 'wp-block-styles' ); diff --git a/test/emptytheme/index.php b/test/emptytheme/index.php index e69de29bb2d1d6..2b6276e2baac50 100644 --- a/test/emptytheme/index.php +++ b/test/emptytheme/index.php @@ -0,0 +1,9 @@ +<?php +/** + * Empty theme index.php file. + * + * @package Gutenberg + */ + +// Silence is golden. +return; diff --git a/test/gutenberg-test-themes/emptyhybrid/functions.php b/test/gutenberg-test-themes/emptyhybrid/functions.php index 6a7da4c918c732..350fe964691434 100644 --- a/test/gutenberg-test-themes/emptyhybrid/functions.php +++ b/test/gutenberg-test-themes/emptyhybrid/functions.php @@ -1,6 +1,14 @@ <?php +/** + * Empty theme functions and definitions. + * + * @package Gutenberg + */ if ( ! function_exists( 'emptyhybrid_support' ) ) : + /** + * Add theme support for various features. + */ function emptyhybrid_support() { // Adding support for core block visual styles. diff --git a/test/gutenberg-test-themes/emptyhybrid/index.php b/test/gutenberg-test-themes/emptyhybrid/index.php index d705808546827b..8f7e81b407e937 100644 --- a/test/gutenberg-test-themes/emptyhybrid/index.php +++ b/test/gutenberg-test-themes/emptyhybrid/index.php @@ -1,3 +1,11 @@ +<?php +/** + * Empty-Hybrid theme index.php file. + * + * @package Gutenberg + */ + +?> <!doctype html> <html <?php language_attributes(); ?>> <head> diff --git a/test/gutenberg-test-themes/style-variations/index.php b/test/gutenberg-test-themes/style-variations/index.php index e69de29bb2d1d6..0c6530acc1aaff 100644 --- a/test/gutenberg-test-themes/style-variations/index.php +++ b/test/gutenberg-test-themes/style-variations/index.php @@ -0,0 +1,9 @@ +<?php +/** + * Theme index.php file. + * + * @package Gutenberg + */ + +// Silence is golden. +return; From 4deac6282413e8cbfc262325912a6dc6187bb03b Mon Sep 17 00:00:00 2001 From: Juan Aldasoro <juanfraa@gmail.com> Date: Tue, 16 May 2023 14:02:44 +0200 Subject: [PATCH 054/131] Update `rel` and `title` labels for navigation and submenu links (#50214) * Update `rel` and `title` labels for navigation and submenu links. * Apply suggestions from code review. * Apply suggestions from code review. --- packages/block-library/src/navigation-link/edit.js | 10 ++++++++-- packages/block-library/src/navigation-submenu/edit.js | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index 68f73f2bcd2125..1d097f752a9f06 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -469,8 +469,11 @@ export default function NavigationLinkEdit( { onChange={ ( titleValue ) => { setAttributes( { title: titleValue } ); } } - label={ __( 'Link title' ) } + label={ __( 'Title attribute' ) } autoComplete="off" + help={ __( + 'Additional information to help clarify the purpose of the link.' + ) } /> <TextControl __nextHasNoMarginBottom @@ -478,8 +481,11 @@ export default function NavigationLinkEdit( { onChange={ ( relValue ) => { setAttributes( { rel: relValue } ); } } - label={ __( 'Link rel' ) } + label={ __( 'Rel attribute' ) } autoComplete="off" + help={ __( + 'The relationship of the linked URL as space-separated link types.' + ) } /> </PanelBody> </InspectorControls> diff --git a/packages/block-library/src/navigation-submenu/edit.js b/packages/block-library/src/navigation-submenu/edit.js index ed23540e38d953..7707a6442111e5 100644 --- a/packages/block-library/src/navigation-submenu/edit.js +++ b/packages/block-library/src/navigation-submenu/edit.js @@ -422,8 +422,11 @@ export default function NavigationSubmenuEdit( { onChange={ ( titleValue ) => { setAttributes( { title: titleValue } ); } } - label={ __( 'Link title' ) } + label={ __( 'Title attribute' ) } autoComplete="off" + help={ __( + 'Additional information to help clarify the purpose of the link.' + ) } /> <TextControl __nextHasNoMarginBottom @@ -431,8 +434,11 @@ export default function NavigationSubmenuEdit( { onChange={ ( relValue ) => { setAttributes( { rel: relValue } ); } } - label={ __( 'Link rel' ) } + label={ __( 'Rel attribute' ) } autoComplete="off" + help={ __( + 'The relationship of the linked URL as space-separated link types.' + ) } /> </PanelBody> </InspectorControls> From 07659682b0c8614b39ac2ed757f6c4654826eeb0 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Tue, 16 May 2023 15:37:57 +0300 Subject: [PATCH 055/131] Add `prioritizedInserterBlocks` to slash inserter (#50658) --- .../block-editor/src/autocompleters/block.js | 22 +++++++++++----- .../src/components/inserter/search-results.js | 21 ++------------- .../src/utils/order-inserter-block-items.js | 26 +++++++++++++++++++ ...blocks-prioritized-inserter-blocks.test.js | 23 ++++++++++++++++ 4 files changed, 66 insertions(+), 26 deletions(-) create mode 100644 packages/block-editor/src/utils/order-inserter-block-items.js diff --git a/packages/block-editor/src/autocompleters/block.js b/packages/block-editor/src/autocompleters/block.js index 9c1747fe867b62..fb5300aab7ff82 100644 --- a/packages/block-editor/src/autocompleters/block.js +++ b/packages/block-editor/src/autocompleters/block.js @@ -16,6 +16,7 @@ import useBlockTypesState from '../components/inserter/hooks/use-block-types-sta import BlockIcon from '../components/block-icon'; import { store as blockEditorStore } from '../store'; import { orderBy } from '../utils/sorting'; +import { orderInserterBlockItems } from '../utils/order-inserter-block-items'; const noop = () => {}; const SHOWN_BLOCK_TYPES = 9; @@ -34,23 +35,26 @@ function createBlockCompleter() { triggerPrefix: '/', useItems( filterValue ) { - const { rootClientId, selectedBlockName } = useSelect( - ( select ) => { + const { rootClientId, selectedBlockName, prioritizedBlocks } = + useSelect( ( select ) => { const { getSelectedBlockClientId, getBlockName, getBlockInsertionPoint, + getBlockListSettings, } = select( blockEditorStore ); const selectedBlockClientId = getSelectedBlockClientId(); + const _rootClientId = getBlockInsertionPoint().rootClientId; return { selectedBlockName: selectedBlockClientId ? getBlockName( selectedBlockClientId ) : null, - rootClientId: getBlockInsertionPoint().rootClientId, + rootClientId: _rootClientId, + prioritizedBlocks: + getBlockListSettings( _rootClientId ) + ?.prioritizedInserterBlocks, }; - }, - [] - ); + }, [] ); const [ items, categories, collections ] = useBlockTypesState( rootClientId, noop @@ -64,7 +68,10 @@ function createBlockCompleter() { collections, filterValue ) - : orderBy( items, 'frecency', 'desc' ); + : orderInserterBlockItems( + orderBy( items, 'frecency', 'desc' ), + prioritizedBlocks + ); return initialFilteredItems .filter( ( item ) => item.name !== selectedBlockName ) @@ -75,6 +82,7 @@ function createBlockCompleter() { items, categories, collections, + prioritizedBlocks, ] ); const options = useMemo( diff --git a/packages/block-editor/src/components/inserter/search-results.js b/packages/block-editor/src/components/inserter/search-results.js index b2dc15d586adf9..b32cfcf1ecb3f2 100644 --- a/packages/block-editor/src/components/inserter/search-results.js +++ b/packages/block-editor/src/components/inserter/search-results.js @@ -22,6 +22,7 @@ import useBlockTypesState from './hooks/use-block-types-state'; import { searchBlockItems, searchItems } from './search-items'; import InserterListbox from '../inserter-listbox'; import { orderBy } from '../../utils/sorting'; +import { orderInserterBlockItems } from '../../utils/order-inserter-block-items'; import { store as blockEditorStore } from '../../store'; const INITIAL_INSERTER_RESULTS = 9; @@ -33,24 +34,6 @@ const INITIAL_INSERTER_RESULTS = 9; */ const EMPTY_ARRAY = []; -const orderInitialBlockItems = ( items, priority ) => { - if ( ! priority ) { - return items; - } - - items.sort( ( { id: aName }, { id: bName } ) => { - // Sort block items according to `priority`. - let aIndex = priority.indexOf( aName ); - let bIndex = priority.indexOf( bName ); - // All other block items should come after that. - if ( aIndex < 0 ) aIndex = priority.length; - if ( bIndex < 0 ) bIndex = priority.length; - return aIndex - bIndex; - } ); - - return items; -}; - function InserterSearchResults( { filterValue, onSelect, @@ -125,7 +108,7 @@ function InserterSearchResults( { let orderedItems = orderBy( blockTypes, 'frecency', 'desc' ); if ( ! filterValue && prioritizedBlocks.length ) { - orderedItems = orderInitialBlockItems( + orderedItems = orderInserterBlockItems( orderedItems, prioritizedBlocks ); diff --git a/packages/block-editor/src/utils/order-inserter-block-items.js b/packages/block-editor/src/utils/order-inserter-block-items.js new file mode 100644 index 00000000000000..696879b89db5e8 --- /dev/null +++ b/packages/block-editor/src/utils/order-inserter-block-items.js @@ -0,0 +1,26 @@ +/** @typedef {import('../store/selectors').WPEditorInserterItem} WPEditorInserterItem */ + +/** + * Helper function to order inserter block items according to a provided array of prioritized blocks. + * + * @param {WPEditorInserterItem[]} items The array of editor inserter block items to be sorted. + * @param {string[]} priority The array of block names to be prioritized. + * @return {WPEditorInserterItem[]} The sorted array of editor inserter block items. + */ +export const orderInserterBlockItems = ( items, priority ) => { + if ( ! priority ) { + return items; + } + + items.sort( ( { id: aName }, { id: bName } ) => { + // Sort block items according to `priority`. + let aIndex = priority.indexOf( aName ); + let bIndex = priority.indexOf( bName ); + // All other block items should come after that. + if ( aIndex < 0 ) aIndex = priority.length; + if ( bIndex < 0 ) bIndex = priority.length; + return aIndex - bIndex; + } ); + + return items; +}; diff --git a/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js b/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js index c34e402ea19806..77ae8d38274325 100644 --- a/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js +++ b/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js @@ -105,4 +105,27 @@ describe( 'Prioritized Inserter Blocks Setting on InnerBlocks', () => { ); } ); } ); + describe( 'Slash inserter', () => { + it( 'uses the priority ordering if prioritzed blocks setting is set', async () => { + await insertBlock( 'Prioritized Inserter Blocks Set' ); + await page.click( '[data-type="core/image"]' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '/' ); + // Wait for the results to display. + await page.waitForSelector( '.components-autocomplete__result' ); + const inserterItemTitles = await page.evaluate( () => { + return Array.from( + document.querySelectorAll( + '.components-autocomplete__result' + ) + ).map( ( { innerText } ) => innerText ); + } ); + expect( inserterItemTitles ).toHaveLength( 9 ); // Default suggested blocks number. + expect( inserterItemTitles.slice( 0, 3 ) ).toEqual( [ + 'Audio', + 'Spacer', + 'Code', + ] ); + } ); + } ); } ); From 60984b3daa8d8b986775aa1e450f6c0fcd5644d1 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Tue, 16 May 2023 14:19:04 +0100 Subject: [PATCH 056/131] Command center: Add searchLabel property to commands (#50663) Co-authored-by: Nik Tsekouras <ntsekouras@outlook.com> --- packages/commands/src/components/command-menu.js | 4 ++-- packages/commands/src/hooks/use-command.js | 2 ++ packages/commands/src/store/actions.js | 11 ++++++----- packages/commands/src/store/reducer.js | 1 + packages/core-commands/src/add-post-type-commands.js | 4 ++-- .../src/site-editor-navigation-commands.js | 3 ++- .../src/hooks/commands/use-edit-mode-commands.js | 4 ++-- 7 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/commands/src/components/command-menu.js b/packages/commands/src/components/command-menu.js index 75bc6f4e827a26..9f59db3f6f53cb 100644 --- a/packages/commands/src/components/command-menu.js +++ b/packages/commands/src/components/command-menu.js @@ -41,7 +41,7 @@ function CommandMenuLoader( { name, search, hook, setLoader, close } ) { { commands.map( ( command ) => ( <Command.Item key={ command.name } - value={ command.name } + value={ command.searchLabel ?? command.label } onSelect={ () => command.callback( { close } ) } > <HStack @@ -110,7 +110,7 @@ export function CommandMenuGroup( { isContextual, search, setLoader, close } ) { { commands.map( ( command ) => ( <Command.Item key={ command.name } - value={ command.name } + value={ command.searchLabel ?? command.label } onSelect={ () => command.callback( { close } ) } > <HStack diff --git a/packages/commands/src/hooks/use-command.js b/packages/commands/src/hooks/use-command.js index d3edbf4e17fdc0..e3f56662b91f29 100644 --- a/packages/commands/src/hooks/use-command.js +++ b/packages/commands/src/hooks/use-command.js @@ -26,6 +26,7 @@ export default function useCommand( command ) { name: command.name, context: command.context, label: command.label, + searchLabel: command.searchLabel, icon: command.icon, callback: currentCallback.current, } ); @@ -35,6 +36,7 @@ export default function useCommand( command ) { }, [ command.name, command.label, + command.searchLabel, command.icon, command.context, registerCommand, diff --git a/packages/commands/src/store/actions.js b/packages/commands/src/store/actions.js index 81f3ddf7222089..6162f1497cf0ad 100644 --- a/packages/commands/src/store/actions.js +++ b/packages/commands/src/store/actions.js @@ -5,11 +5,12 @@ * * @typedef {Object} WPCommandConfig * - * @property {string} name Command name. - * @property {string} label Command label. - * @property {string=} context Command context. - * @property {JSX.Element} icon Command icon. - * @property {Function} callback Command callback. + * @property {string} name Command name. + * @property {string} label Command label. + * @property {string=} searchLabel Command search label. + * @property {string=} context Command context. + * @property {JSX.Element} icon Command icon. + * @property {Function} callback Command callback. */ /** diff --git a/packages/commands/src/store/reducer.js b/packages/commands/src/store/reducer.js index e61de53d21a706..c2bb73b189f16b 100644 --- a/packages/commands/src/store/reducer.js +++ b/packages/commands/src/store/reducer.js @@ -19,6 +19,7 @@ function commands( state = {}, action ) { [ action.name ]: { name: action.name, label: action.label, + searchLabel: action.searchLabel, context: action.context, callback: action.callback, icon: action.icon, diff --git a/packages/core-commands/src/add-post-type-commands.js b/packages/core-commands/src/add-post-type-commands.js index afd783c7c0cc35..3c2a7ea8c52cdd 100644 --- a/packages/core-commands/src/add-post-type-commands.js +++ b/packages/core-commands/src/add-post-type-commands.js @@ -14,7 +14,7 @@ const { useCommand } = unlock( privateApis ); export function useAddPostTypeCommands() { useCommand( { - name: 'add new post', + name: 'core/add-new-post', label: __( 'Add new post' ), icon: plus, callback: () => { @@ -22,7 +22,7 @@ export function useAddPostTypeCommands() { }, } ); useCommand( { - name: 'add new page', + name: 'core/add-new-page', label: __( 'Add new page' ), icon: plus, callback: () => { diff --git a/packages/core-commands/src/site-editor-navigation-commands.js b/packages/core-commands/src/site-editor-navigation-commands.js index 310b0c1c1e17a7..1bf29fa08f9cea 100644 --- a/packages/core-commands/src/site-editor-navigation-commands.js +++ b/packages/core-commands/src/site-editor-navigation-commands.js @@ -66,7 +66,8 @@ const getNavigationCommandLoaderPerPostType = ( postType ) => ? { canvas: getQueryArg( window.location.href, 'canvas' ) } : {}; return { - name: record.title?.rendered + ' ' + record.id, + name: postType + '-' + record.id, + searchLabel: record.title?.rendered + ' ' + record.id, label: record.title?.rendered ? record.title?.rendered : __( '(no title)' ), diff --git a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js index 59071a4088f426..67b7164edcdab5 100644 --- a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js +++ b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js @@ -35,7 +35,7 @@ function useEditModeCommandLoader() { ? __( 'Delete template' ) : __( 'Delete template part' ); commands.push( { - name: label, + name: 'core/remove-template', label, icon: trash, context: 'site-editor-edit', @@ -55,7 +55,7 @@ function useEditModeCommandLoader() { ? __( 'Reset template' ) : __( 'Reset template part' ); commands.push( { - name: label, + name: 'core/reset-template', label, icon: backup, callback: ( { close } ) => { From 37dc9f67575153857b59550fc964cc2954f45ce2 Mon Sep 17 00:00:00 2001 From: Jason Johnston <jhnstn@users.noreply.github.com> Date: Tue, 16 May 2023 10:23:29 -0400 Subject: [PATCH 057/131] [RNMobile] Fix embed webview endcoding (#50555) * Fix embed webview endcoding * Clean up imports --------- Co-authored-by: jhnstn <jhnstn@pm.me> --- .../GutenbergEmbedWebViewActivity.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergEmbedWebViewActivity.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergEmbedWebViewActivity.java index 30125a853ad0fb..19f25bdd77e9c2 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergEmbedWebViewActivity.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergEmbedWebViewActivity.java @@ -4,11 +4,8 @@ import android.graphics.Bitmap; import android.os.Bundle; import android.os.Handler; -import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.webkit.CookieManager; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; @@ -80,7 +77,7 @@ public void onProgressChanged(WebView view, int progress) { protected void load() { String content = getIntent().getExtras().getString(ARG_CONTENT); - mWebView.loadData(content, "text/html", "UTF-8"); + mWebView.loadDataWithBaseURL(null, content, "text/html", "UTF-8", null); } private void setupToolbar() { From 4556c868605af0e08c808537f15676302c6c6824 Mon Sep 17 00:00:00 2001 From: Marco Ciampini <marco.ciampo@gmail.com> Date: Tue, 16 May 2023 20:59:20 +0200 Subject: [PATCH 058/131] Template pattern modal: remove internal modal classnames (#50655) * Template pattern modal: remove internal modal classnames * Update snapshots * CHANGELOG --- packages/components/CHANGELOG.md | 4 ++ packages/components/src/modal/index.tsx | 7 +--- .../test/__snapshots__/index.js.snap | 4 +- .../test/__snapshots__/index.js.snap | 8 +--- .../start-template-options/style.scss | 41 +++++++++---------- 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d49f137f467c84..6e46c3b70f495a 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Internal + +- `Modal`: Remove children container's unused class name ([#50655](https://github.com/WordPress/gutenberg/pull/50655)). + ## 24.0.0 (2023-05-10) ### Breaking Changes diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index a22dc2a4ec6713..d9c7b602b83920 100644 --- a/packages/components/src/modal/index.tsx +++ b/packages/components/src/modal/index.tsx @@ -252,12 +252,7 @@ function UnforwardedModal( ) } </div> ) } - <div - ref={ childrenContainerRef } - className="components-modal__children-container" - > - { children } - </div> + <div ref={ childrenContainerRef }>{ children }</div> </div> </div> </StyleProvider> diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap index 5311dfa07b5c4e..2ad8e457bae303 100644 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap @@ -43,9 +43,7 @@ exports[`KeyboardShortcutHelpModal should match snapshot when the modal is activ </svg> </button> </div> - <div - class="components-modal__children-container" - > + <div> <section class="edit-post-keyboard-shortcut-help-modal__section edit-post-keyboard-shortcut-help-modal__main-shortcuts" > diff --git a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap index 5a6ff1e5fffee4..2d43aa7922f361 100644 --- a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap @@ -102,9 +102,7 @@ exports[`EditPostPreferencesModal should match snapshot when the modal is active </svg> </button> </div> - <div - class="components-modal__children-container" - > + <div> <div class="interface-preferences__tabs" > @@ -725,9 +723,7 @@ exports[`EditPostPreferencesModal should match snapshot when the modal is active </svg> </button> </div> - <div - class="components-modal__children-container" - > + <div> <div class="components-navigator-provider interface-preferences__provider emotion-0 emotion-1" data-wp-c16t="true" diff --git a/packages/edit-site/src/components/start-template-options/style.scss b/packages/edit-site/src/components/start-template-options/style.scss index 715e829ecab3ac..7515861099b380 100644 --- a/packages/edit-site/src/components/start-template-options/style.scss +++ b/packages/edit-site/src/components/start-template-options/style.scss @@ -1,28 +1,27 @@ -.edit-site-start-template-options__modal { - .components-modal__content { - padding-bottom: 0; - } +$actions-height: 92px; - .components-modal__children-container { - display: flex; - height: 100%; - flex-direction: column; - - .edit-site-start-template-options__modal__actions { - margin-top: auto; - position: sticky; - bottom: 0; - background-color: $white; - margin-left: - $grid-unit-40; - margin-right: - $grid-unit-40; - padding: $grid-unit-30 $grid-unit-40 $grid-unit-40; - border-top: 1px solid $gray-300; - z-index: z-index(".edit-site-start-template-options__modal__actions"); - } +.edit-site-start-template-options__modal { + .edit-site-start-template-options__modal__actions { + position: absolute; + bottom: 0; + width: 100%; + height: $actions-height; + background-color: $white; + margin-left: - $grid-unit-40; + margin-right: - $grid-unit-40; + padding-left: $grid-unit-40; + padding-right: $grid-unit-40; + border-top: 1px solid $gray-300; + z-index: z-index(".edit-site-start-template-options__modal__actions"); } .block-editor-block-patterns-list { - padding-bottom: $grid-unit-40; + // Since the actions container is positioned absolutely, + // this padding bottom ensures that the content wrapper will properly + // detect overflowing content and start showing scrollbars at the right + // moment. Without this padding, the content would render under the actions + // bar without causing the wrapper to show a scrollbar. + padding-bottom: $actions-height; } } From 76319f8ac3a68c01966e6ba745511bf5a4677756 Mon Sep 17 00:00:00 2001 From: Jerry Jones <jones.jeremydavid@gmail.com> Date: Tue, 16 May 2023 16:30:10 -0500 Subject: [PATCH 059/131] Fix navigation tests by creating pages for link control (#50680) --- .../specs/editor/blocks/navigation.spec.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/e2e/specs/editor/blocks/navigation.spec.js b/test/e2e/specs/editor/blocks/navigation.spec.js index dc303141957310..e776b160d55bc8 100644 --- a/test/e2e/specs/editor/blocks/navigation.spec.js +++ b/test/e2e/specs/editor/blocks/navigation.spec.js @@ -396,6 +396,26 @@ test.describe( 'Navigation block', () => { <!-- /wp:navigation-submenu -->`, }; + test.beforeAll( async ( { requestUtils } ) => { + // We need pages to be published so the Link Control can return pages + await requestUtils.createPage( { + title: 'Test Page 1', + status: 'publish', + } ); + await requestUtils.createPage( { + title: 'Test Page 2', + status: 'publish', + } ); + await requestUtils.createPage( { + title: 'Test Page 3', + status: 'publish', + } ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deleteAllPages(); + } ); + test.use( { linkControl: async ( { page }, use ) => { await use( new LinkControl( { page } ) ); From f0266c22cdbab064482c5e57d6a63a29b232cf2d Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Wed, 17 May 2023 09:57:29 +1000 Subject: [PATCH 060/131] Editor canvas container: include resizeable iframe in component (#50682) * Testing out zoom mode in the revisions view Also making the resizable iframe optional * Reverting zoom changes * reinstate enableresizing --- .../src/components/block-editor/index.js | 5 +- .../editor-canvas-container/index.js | 47 +++++++++++-------- .../src/components/revisions/index.js | 1 + .../src/components/style-book/index.js | 5 +- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index 4957beee030f2b..cc5e7c8d9254df 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -143,6 +143,7 @@ export default function BlockEditor() { const [ resizeObserver, sizes ] = useResizeObserver(); const isTemplatePart = templateType === 'wp_template_part'; + const hasBlocks = blocks.length !== 0; const enableResizing = isTemplatePart && @@ -169,9 +170,7 @@ export default function BlockEditor() { { ( [ editorCanvasView ] ) => editorCanvasView ? ( <div className="edit-site-visual-editor is-focus-mode"> - <ResizableEditor enableResizing> - { editorCanvasView } - </ResizableEditor> + { editorCanvasView } </div> ) : ( <BlockTools diff --git a/packages/edit-site/src/components/editor-canvas-container/index.js b/packages/edit-site/src/components/editor-canvas-container/index.js index 091c5b9ec60f62..3647ff2e49f43f 100644 --- a/packages/edit-site/src/components/editor-canvas-container/index.js +++ b/packages/edit-site/src/components/editor-canvas-container/index.js @@ -18,6 +18,7 @@ import { useFocusOnMount, useFocusReturn } from '@wordpress/compose'; */ import { unlock } from '../../private-apis'; import { store as editSiteStore } from '../../store'; +import ResizableEditor from '../block-editor/resizable-editor'; /** * Returns a translated string for the title of the editor canvas container. @@ -46,7 +47,12 @@ const { Fill: EditorCanvasContainerFill, } = createPrivateSlotFill( SLOT_FILL_NAME ); -function EditorCanvasContainer( { children, closeButtonLabel, onClose } ) { +function EditorCanvasContainer( { + children, + closeButtonLabel, + onClose, + enableResizing = false, +} ) { const editorCanvasContainerView = useSelect( ( select ) => unlock( select( editSiteStore ) ).getEditorCanvasContainerView(), @@ -62,6 +68,7 @@ function EditorCanvasContainer( { children, closeButtonLabel, onClose } ) { () => getEditorCanvasContainerTitle( editorCanvasContainerView ), [ editorCanvasContainerView ] ); + function onCloseContainer() { if ( typeof onClose === 'function' ) { onClose(); @@ -97,24 +104,26 @@ function EditorCanvasContainer( { children, closeButtonLabel, onClose } ) { return ( <EditorCanvasContainerFill> - { /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */ } - <section - className="edit-site-editor-canvas-container" - ref={ shouldShowCloseButton ? focusOnMountRef : null } - onKeyDown={ closeOnEscape } - aria-label={ title } - > - { shouldShowCloseButton && ( - <Button - className="edit-site-editor-canvas-container__close-button" - icon={ closeSmall } - label={ closeButtonLabel || __( 'Close' ) } - onClick={ onCloseContainer } - showTooltip={ false } - /> - ) } - { childrenWithProps } - </section> + <ResizableEditor enableResizing={ enableResizing }> + { /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */ } + <section + className="edit-site-editor-canvas-container" + ref={ shouldShowCloseButton ? focusOnMountRef : null } + onKeyDown={ closeOnEscape } + aria-label={ title } + > + { shouldShowCloseButton && ( + <Button + className="edit-site-editor-canvas-container__close-button" + icon={ closeSmall } + label={ closeButtonLabel || __( 'Close' ) } + onClick={ onCloseContainer } + showTooltip={ false } + /> + ) } + { childrenWithProps } + </section> + </ResizableEditor> </EditorCanvasContainerFill> ); } diff --git a/packages/edit-site/src/components/revisions/index.js b/packages/edit-site/src/components/revisions/index.js index 347d05094ef376..3e06b6415cc4ea 100644 --- a/packages/edit-site/src/components/revisions/index.js +++ b/packages/edit-site/src/components/revisions/index.js @@ -74,6 +74,7 @@ function Revisions( { onClose, userConfig, blocks } ) { title={ __( 'Revisions' ) } onClose={ onClose } closeButtonLabel={ __( 'Close revisions' ) } + enableResizing={ true } > <Iframe className="edit-site-revisions__iframe" diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index 650f38eea95b94..72e8abad629eb1 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -192,7 +192,10 @@ function StyleBook( { isSelected, onSelect } ) { ); return ( - <EditorCanvasContainer closeButtonLabel={ __( 'Close Style Book' ) }> + <EditorCanvasContainer + enableResizing={ true } + closeButtonLabel={ __( 'Close Style Book' ) } + > <div className={ classnames( 'edit-site-style-book', { 'is-wide': sizes.width > 600, From 86dfd21a8398d6acc1d492989c4bc0c0e7b9da98 Mon Sep 17 00:00:00 2001 From: George Mamadashvili <georgemamadashvili@gmail.com> Date: Wed, 17 May 2023 12:08:06 +0400 Subject: [PATCH 061/131] Edit Site: Simplofy variation selectors (#50687) --- .../components/global-styles/screen-root.js | 8 +++-- .../style-variations-container.js | 31 ++++++++----------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/screen-root.js b/packages/edit-site/src/components/global-styles/screen-root.js index 4a4ce14e82e6f2..1d9be8b11b3f33 100644 --- a/packages/edit-site/src/components/global-styles/screen-root.js +++ b/packages/edit-site/src/components/global-styles/screen-root.js @@ -31,7 +31,7 @@ function ScreenRoot() { const { useGlobalStyle } = unlock( blockEditorPrivateApis ); const [ customCSS ] = useGlobalStyle( 'css' ); - const { variations, canEditCSS } = useSelect( ( select ) => { + const { hasVariations, canEditCSS } = useSelect( ( select ) => { const { getEntityRecord, __experimentalGetCurrentGlobalStylesId, @@ -44,7 +44,9 @@ function ScreenRoot() { : undefined; return { - variations: __experimentalGetCurrentThemeGlobalStylesVariations(), + hasVariations: + !! __experimentalGetCurrentThemeGlobalStylesVariations() + ?.length, canEditCSS: !! globalStyles?._links?.[ 'wp:action-edit-css' ] ?? false, }; @@ -59,7 +61,7 @@ function ScreenRoot() { <StylesPreview /> </CardMedia> </Card> - { !! variations?.length && ( + { hasVariations && ( <ItemGroup> <NavigationButtonAsItem path="/variations" diff --git a/packages/edit-site/src/components/global-styles/style-variations-container.js b/packages/edit-site/src/components/global-styles/style-variations-container.js index 074d86af4f80fb..9ab54f6e070caf 100644 --- a/packages/edit-site/src/components/global-styles/style-variations-container.js +++ b/packages/edit-site/src/components/global-styles/style-variations-container.js @@ -91,13 +91,10 @@ function Variation( { variation } ) { } export default function StyleVariationsContainer() { - const { variations } = useSelect( ( select ) => { - return { - variations: - select( - coreStore - ).__experimentalGetCurrentThemeGlobalStylesVariations() || [], - }; + const variations = useSelect( ( select ) => { + return select( + coreStore + ).__experimentalGetCurrentThemeGlobalStylesVariations(); }, [] ); const withEmptyVariation = useMemo( () => { @@ -107,7 +104,7 @@ export default function StyleVariationsContainer() { settings: {}, styles: {}, }, - ...variations.map( ( variation ) => ( { + ...( variations ?? [] ).map( ( variation ) => ( { ...variation, settings: variation.settings ?? {}, styles: variation.styles ?? {}, @@ -116,15 +113,13 @@ export default function StyleVariationsContainer() { }, [ variations ] ); return ( - <> - <Grid - columns={ 2 } - className="edit-site-global-styles-style-variations-container" - > - { withEmptyVariation?.map( ( variation, index ) => ( - <Variation key={ index } variation={ variation } /> - ) ) } - </Grid> - </> + <Grid + columns={ 2 } + className="edit-site-global-styles-style-variations-container" + > + { withEmptyVariation.map( ( variation, index ) => ( + <Variation key={ index } variation={ variation } /> + ) ) } + </Grid> ); } From 83a33e7c24ee1db8fe426052ac73137f51289234 Mon Sep 17 00:00:00 2001 From: George Mamadashvili <georgemamadashvili@gmail.com> Date: Wed, 17 May 2023 12:08:30 +0400 Subject: [PATCH 062/131] Cover: Unlock private APIs outside of the component (#50686) --- .../block-library/src/cover/edit/resizable-cover-popover.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/cover/edit/resizable-cover-popover.js b/packages/block-library/src/cover/edit/resizable-cover-popover.js index 7567da82e84362..cf37294f95f018 100644 --- a/packages/block-library/src/cover/edit/resizable-cover-popover.js +++ b/packages/block-library/src/cover/edit/resizable-cover-popover.js @@ -25,6 +25,8 @@ const RESIZABLE_BOX_ENABLE_OPTION = { topLeft: false, }; +const { ResizableBoxPopover } = unlock( blockEditorPrivateApis ); + export default function ResizableCoverPopover( { className, height, @@ -37,7 +39,6 @@ export default function ResizableCoverPopover( { width, ...props } ) { - const { ResizableBoxPopover } = unlock( blockEditorPrivateApis ); const [ isResizing, setIsResizing ] = useState( false ); const dimensions = useMemo( () => ( { height, minHeight, width } ), From 522e1f13be46043291b06108aac5a16f4dd11391 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Wed, 17 May 2023 11:34:03 +0300 Subject: [PATCH 063/131] Lodash: Remove from Gallery block (#50591) --- packages/block-library/src/gallery/v1/edit.js | 7 +------ .../block-library/src/gallery/v1/gallery-image.native.js | 3 +-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/block-library/src/gallery/v1/edit.js b/packages/block-library/src/gallery/v1/edit.js index 6a1eb1c631a900..5820fbe0be9487 100644 --- a/packages/block-library/src/gallery/v1/edit.js +++ b/packages/block-library/src/gallery/v1/edit.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { isEmpty } from 'lodash'; - /** * WordPress dependencies */ @@ -385,7 +380,7 @@ function GalleryEdit( props ) { } const imageSizeOptions = getImagesSizeOptions(); - const shouldShowSizeOptions = hasImages && ! isEmpty( imageSizeOptions ); + const shouldShowSizeOptions = hasImages && imageSizeOptions.length > 0; return ( <> diff --git a/packages/block-library/src/gallery/v1/gallery-image.native.js b/packages/block-library/src/gallery/v1/gallery-image.native.js index 7fa6ab4ab7ea75..b887ca0bbfe04f 100644 --- a/packages/block-library/src/gallery/v1/gallery-image.native.js +++ b/packages/block-library/src/gallery/v1/gallery-image.native.js @@ -7,7 +7,6 @@ import { ScrollView, TouchableWithoutFeedback, } from 'react-native'; -import { isEmpty } from 'lodash'; /** * WordPress dependencies @@ -334,7 +333,7 @@ class GalleryImage extends Component { accessibilityLabelImageContainer() { const { caption, 'aria-label': ariaLabel } = this.props; - return isEmpty( caption ) + return ! caption ? ariaLabel : ariaLabel + '. ' + From 6f2d112399b007ccf33e913183e36446d841c9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Wed, 17 May 2023 10:45:07 +0200 Subject: [PATCH 064/131] File: Add experimental integration with Interactivity API (#50377) * File: Add experimental integration with Interactivity API * File: Mark the block as an island to activate directives * Improve webpack config * Refactor code with the latest changes applied to `trunk` * Refactor init effect with bind for `hidden` attribute --- bin/build-plugin-zip.sh | 2 +- lib/experimental/editor-settings.php | 3 - ...ion-block-interactivity.php => blocks.php} | 57 +++++++++++++------ .../interactivity-api/script-loader.php | 43 ++++++++------ lib/experiments-page.php | 8 +-- lib/load.php | 6 +- .../block-library/src/file/interactivity.js | 17 ++++++ .../src/file/{utils.js => utils/index.js} | 0 .../lib/index.js | 10 ++++ tools/webpack/blocks.js | 24 ++++++-- 10 files changed, 120 insertions(+), 50 deletions(-) rename lib/experimental/interactivity-api/{navigation-block-interactivity.php => blocks.php} (83%) create mode 100644 packages/block-library/src/file/interactivity.js rename packages/block-library/src/file/{utils.js => utils/index.js} (100%) diff --git a/bin/build-plugin-zip.sh b/bin/build-plugin-zip.sh index 52d473cd8d4016..131e434d1383d0 100755 --- a/bin/build-plugin-zip.sh +++ b/bin/build-plugin-zip.sh @@ -83,7 +83,7 @@ build_files=$( build/block-library/blocks/*.php \ build/block-library/blocks/*/block.json \ build/block-library/blocks/*/*.{js,js.map,css,asset.php} \ - build/block-library/interactive-blocks/*.js \ + build/block-library/interactivity/*.{js,js.map,asset.php} \ build/edit-widgets/blocks/*/block.json \ build/widgets/blocks/*.php \ build/widgets/blocks/*/block.json \ diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 8d4046530fbc95..bf9acb7b70d4dd 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -95,9 +95,6 @@ function gutenberg_enable_experiments() { if ( $gutenberg_experiments && array_key_exists( 'gutenberg-details-blocks', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableDetailsBlocks = true', 'before' ); } - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-interactivity-api-navigation-block', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableNavigationBlockInteractivity = true', 'before' ); - } if ( $gutenberg_experiments && array_key_exists( 'gutenberg-theme-previews', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableThemePreviews = true', 'before' ); } diff --git a/lib/experimental/interactivity-api/navigation-block-interactivity.php b/lib/experimental/interactivity-api/blocks.php similarity index 83% rename from lib/experimental/interactivity-api/navigation-block-interactivity.php rename to lib/experimental/interactivity-api/blocks.php index 21e1e0381b70ed..755c1d1d4fa7d7 100644 --- a/lib/experimental/interactivity-api/navigation-block-interactivity.php +++ b/lib/experimental/interactivity-api/blocks.php @@ -6,6 +6,29 @@ * @package gutenberg */ +/** + * Adds Interactivity API directives to the File block markup using the Tag Processor. + * + * @param string $block_content Markup of the File block. + * @param array $block The full block, including name and attributes. + * @param WP_Block $instance The block instance. + * + * @return string File block markup with the directives injected when applicable. + */ +function gutenberg_block_core_file_add_directives_to_content( $block_content, $block, $instance ) { + if ( empty( $instance->attributes['displayPreview'] ) ) { + return $block_content; + } + $processor = new WP_HTML_Tag_Processor( $block_content ); + $processor->next_tag(); + $processor->set_attribute( 'data-wp-island', '' ); + $processor->next_tag( 'object' ); + $processor->set_attribute( 'data-wp-bind.hidden', 'selectors.core.file.hasNoPdfPreview' ); + $processor->set_attribute( 'hidden', true ); + return $processor->get_updated_html(); +} +add_filter( 'render_block_core/file', 'gutenberg_block_core_file_add_directives_to_content', 10, 3 ); + /** * Add Interactivity API directives to the navigation block markup using the Tag Processor * The final HTML of the navigation block will look similar to this: @@ -221,20 +244,20 @@ function gutenberg_block_core_navigation_add_directives_to_submenu( $w ) { add_filter( 'render_block_core/navigation', 'gutenberg_block_core_navigation_add_directives_to_markup', 10, 1 ); -// Enqueue the `interactivity.js` file with the store. -add_filter( - 'block_type_metadata', - function ( $metadata ) { - if ( 'core/navigation' === $metadata['name'] ) { - wp_register_script( - 'wp-block-navigation-view', - gutenberg_url( 'build/block-library/interactive-blocks/navigation.min.js' ), - array( 'wp-interactivity-runtime' ) - ); - $metadata['viewScript'] = array( 'wp-block-navigation-view' ); - } - return $metadata; - }, - 10, - 1 -); +/** + * Replaces view script for the File and Navigation blocks with version using Interactivity API. + * + * @param array $metadata Block metadata as read in via block.json. + * + * @return array Filtered block type metadata. + */ +function gutenberg_block_update_interactive_view_script( $metadata ) { + if ( + in_array( $metadata['name'], array( 'core/file', 'core/navigation' ), true ) && + str_contains( $metadata['file'], 'build/block-library/blocks' ) + ) { + $metadata['viewScript'] = array( 'file:./interactivity.min.js' ); + } + return $metadata; +} +add_filter( 'block_type_metadata', 'gutenberg_block_update_interactive_view_script', 10, 1 ); diff --git a/lib/experimental/interactivity-api/script-loader.php b/lib/experimental/interactivity-api/script-loader.php index 15804ca1e92444..63453713dd18cf 100644 --- a/lib/experimental/interactivity-api/script-loader.php +++ b/lib/experimental/interactivity-api/script-loader.php @@ -11,22 +11,28 @@ * @param WP_Scripts $scripts WP_Scripts instance. */ function gutenberg_register_interactivity_scripts( $scripts ) { - gutenberg_override_script( - $scripts, - 'wp-interactivity-runtime', - gutenberg_url( - 'build/block-library/interactive-blocks/interactivity.min.js' - ), - array( 'wp-interactivity-vendors' ) - ); + // When in production, use the plugin's version as the default asset version; + // else (for development or test) default to use the current time. + $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); - gutenberg_override_script( - $scripts, - 'wp-interactivity-vendors', - gutenberg_url( - 'build/block-library/interactive-blocks/vendors.min.js' - ) - ); + foreach ( array( 'vendors', 'runtime' ) as $script_name ) { + $script_path = "build/block-library/interactivity/$script_name.min.js"; + // Replace extension with `.asset.php` to find the generated dependencies file. + $asset_file = gutenberg_dir_path() . substr( $script_path, 0, -( strlen( '.js' ) ) ) . '.asset.php'; + $asset = file_exists( $asset_file ) + ? require $asset_file + : null; + $dependencies = isset( $asset['dependencies'] ) ? $asset['dependencies'] : array(); + $version = isset( $asset['version'] ) ? $asset['version'] : $default_version; + + gutenberg_override_script( + $scripts, + "wp-interactivity-$script_name", + gutenberg_url( $script_path ), + $dependencies, + $version + ); + } } add_action( 'wp_default_scripts', 'gutenberg_register_interactivity_scripts', 10, 1 ); @@ -34,11 +40,12 @@ function gutenberg_register_interactivity_scripts( $scripts ) { * Adds the "defer" attribute to all the interactivity script tags. * * @param string $tag The generated script tag. + * @param string $handle The script handle. * * @return string The modified script tag. */ -function gutenberg_interactivity_scripts_add_defer_attribute( $tag ) { - if ( str_contains( $tag, '/block-library/interactive-blocks/' ) ) { +function gutenberg_interactivity_scripts_add_defer_attribute( $tag, $handle ) { + if ( str_starts_with( $handle, 'wp-interactivity-' ) || str_contains( $tag, '/interactivity.min.js' ) ) { $p = new WP_HTML_Tag_Processor( $tag ); $p->next_tag( array( 'tag' => 'script' ) ); $p->set_attribute( 'defer', true ); @@ -46,4 +53,4 @@ function gutenberg_interactivity_scripts_add_defer_attribute( $tag ) { } return $tag; } -add_filter( 'script_loader_tag', 'gutenberg_interactivity_scripts_add_defer_attribute', 10, 1 ); +add_filter( 'script_loader_tag', 'gutenberg_interactivity_scripts_add_defer_attribute', 10, 2 ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index e39a9dbefb9c16..521d04b75b34be 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -114,14 +114,14 @@ function gutenberg_initialize_experiments_settings() { ); add_settings_field( - 'gutenberg-interactivity-api-navigation-block', - __( 'Navigation block', 'gutenberg' ), + 'gutenberg-interactivity-api-core-blocks', + __( 'Core blocks', 'gutenberg' ), 'gutenberg_display_experiment_field', 'gutenberg-experiments', 'gutenberg_experiments_section', array( - 'label' => __( 'Test the Navigation block using the Interactivity API', 'gutenberg' ), - 'id' => 'gutenberg-interactivity-api-navigation-block', + 'label' => __( 'Test the core blocks using the Interactivity API', 'gutenberg' ), + 'id' => 'gutenberg-interactivity-api-core-blocks', ) ); diff --git a/lib/load.php b/lib/load.php index 31bf94d06a4641..b8ec4a4d607849 100644 --- a/lib/load.php +++ b/lib/load.php @@ -99,13 +99,13 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/block-editor-settings-mobile.php'; require __DIR__ . '/experimental/block-editor-settings.php'; require __DIR__ . '/experimental/blocks.php'; -require __DIR__ . '/experimental/interactivity-api/script-loader.php'; require __DIR__ . '/experimental/navigation-theme-opt-in.php'; require __DIR__ . '/experimental/kses.php'; require __DIR__ . '/experimental/l10n.php'; require __DIR__ . '/experimental/navigation-fallback.php'; -if ( gutenberg_is_experiment_enabled( 'gutenberg-interactivity-api-navigation-block' ) ) { - require __DIR__ . '/experimental/interactivity-api/navigation-block-interactivity.php'; +if ( gutenberg_is_experiment_enabled( 'gutenberg-interactivity-api-core-blocks' ) ) { + require __DIR__ . '/experimental/interactivity-api/script-loader.php'; + require __DIR__ . '/experimental/interactivity-api/blocks.php'; } // Fonts API. diff --git a/packages/block-library/src/file/interactivity.js b/packages/block-library/src/file/interactivity.js new file mode 100644 index 00000000000000..cf9ae41002b276 --- /dev/null +++ b/packages/block-library/src/file/interactivity.js @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { store } from '../utils/interactivity'; +import { browserSupportsPdfs } from './utils'; + +store( { + selectors: { + core: { + file: { + hasNoPdfPreview() { + return ! browserSupportsPdfs(); + }, + }, + }, + }, +} ); diff --git a/packages/block-library/src/file/utils.js b/packages/block-library/src/file/utils/index.js similarity index 100% rename from packages/block-library/src/file/utils.js rename to packages/block-library/src/file/utils/index.js diff --git a/packages/dependency-extraction-webpack-plugin/lib/index.js b/packages/dependency-extraction-webpack-plugin/lib/index.js index 581274c3684f93..3da2286ddbd57d 100644 --- a/packages/dependency-extraction-webpack-plugin/lib/index.js +++ b/packages/dependency-extraction-webpack-plugin/lib/index.js @@ -27,6 +27,7 @@ class DependencyExtractionWebpackPlugin { combinedOutputFile: null, externalizedReport: false, injectPolyfill: false, + __experimentalInjectInteractivityRuntime: false, outputFormat: 'php', outputFilename: null, useDefaults: true, @@ -142,6 +143,7 @@ class DependencyExtractionWebpackPlugin { combinedOutputFile, externalizedReport, injectPolyfill, + __experimentalInjectInteractivityRuntime, outputFormat, outputFilename, } = this.options; @@ -184,6 +186,14 @@ class DependencyExtractionWebpackPlugin { if ( injectPolyfill ) { chunkDeps.add( 'wp-polyfill' ); } + // Temporary fix for Interactivity API until it gets moved to its package. + if ( __experimentalInjectInteractivityRuntime ) { + if ( ! chunkJSFile.startsWith( './interactivity/' ) ) { + chunkDeps.add( 'wp-interactivity-runtime' ); + } else if ( './interactivity/runtime.min.js' === chunkJSFile ) { + chunkDeps.add( 'wp-interactivity-vendors' ); + } + } const processModule = ( { userRequest } ) => { if ( this.externalizedDeps.has( userRequest ) ) { diff --git a/tools/webpack/blocks.js b/tools/webpack/blocks.js index d8bcf0559fdbd7..481b8b457a1967 100644 --- a/tools/webpack/blocks.js +++ b/tools/webpack/blocks.js @@ -220,16 +220,23 @@ module.exports = [ ].filter( Boolean ), }, { + ...baseConfig, + watchOptions: { + aggregateTimeout: 200, + }, + name: 'interactivity', entry: { + file: './packages/block-library/src/file/interactivity.js', navigation: './packages/block-library/src/navigation/interactivity.js', }, output: { devtoolNamespace: 'wp', - filename: './build/block-library/interactive-blocks/[name].min.js', - path: join( __dirname, '..', '..' ), + filename: './blocks/[name]/interactivity.min.js', + path: join( __dirname, '..', '..', 'build', 'block-library' ), }, optimization: { + ...baseConfig.optimization, runtimeChunk: { name: 'vendors', }, @@ -238,12 +245,14 @@ module.exports = [ vendors: { name: 'vendors', test: /[\\/]node_modules[\\/]/, + filename: './interactivity/[name].min.js', minSize: 0, chunks: 'all', }, - interactivity: { - name: 'interactivity', + runtime: { + name: 'runtime', test: /[\\/]utils\/interactivity[\\/]/, + filename: './interactivity/[name].min.js', chunks: 'all', minSize: 0, priority: -10, @@ -279,5 +288,12 @@ module.exports = [ }, ], }, + plugins: [ + ...plugins, + new DependencyExtractionWebpackPlugin( { + __experimentalInjectInteractivityRuntime: true, + injectPolyfill: false, + } ), + ].filter( Boolean ), }, ]; From 9018b154996a8e4cabcf41a1800edd2513cf20a0 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Wed, 17 May 2023 12:15:16 +0300 Subject: [PATCH 065/131] [Site Editor]: Update the add template menu (#50595) * [Site Editor]: Update the add template menu * use single Modal part 1 * rename components * remove obsolete styles * update e2e test * tweak styles * Modal dimensions * Update packages/edit-site/src/components/add-new-template/style.scss Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com> --------- Co-authored-by: James Koster <james@jameskoster.co.uk> Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com> --- ...d-custom-generic-template-modal-content.js | 82 +++++ .../add-custom-generic-template-modal.js | 101 ------- ...s => add-custom-template-modal-content.js} | 39 +-- .../add-new-template/new-template.js | 286 +++++++++--------- .../components/add-new-template/style.scss | 139 +++++---- .../site-editor-url-navigation.spec.js | 2 +- 6 files changed, 312 insertions(+), 337 deletions(-) create mode 100644 packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal-content.js delete mode 100644 packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal.js rename packages/edit-site/src/components/add-new-template/{add-custom-template-modal.js => add-custom-template-modal-content.js} (89%) diff --git a/packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal-content.js b/packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal-content.js new file mode 100644 index 00000000000000..6da96e791679b2 --- /dev/null +++ b/packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal-content.js @@ -0,0 +1,82 @@ +/** + * External dependencies + */ +import { kebabCase } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { + Button, + TextControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; + +function AddCustomGenericTemplateModalContent( { onClose, createTemplate } ) { + const [ title, setTitle ] = useState( '' ); + const defaultTitle = __( 'Custom Template' ); + const [ isBusy, setIsBusy ] = useState( false ); + async function onCreateTemplate( event ) { + event.preventDefault(); + if ( isBusy ) { + return; + } + setIsBusy( true ); + try { + await createTemplate( + { + slug: + 'wp-custom-template-' + + kebabCase( title || defaultTitle ), + title: title || defaultTitle, + }, + false + ); + } finally { + setIsBusy( false ); + } + } + return ( + <form onSubmit={ onCreateTemplate }> + <VStack spacing={ 6 }> + <TextControl + __nextHasNoMarginBottom + label={ __( 'Name' ) } + value={ title } + onChange={ setTitle } + placeholder={ defaultTitle } + disabled={ isBusy } + help={ __( + 'Describe the template, e.g. "Post with sidebar".' + ) } + /> + <HStack + className="edit-site-custom-generic-template__modal-actions" + justify="right" + > + <Button + variant="tertiary" + onClick={ () => { + onClose(); + } } + > + { __( 'Cancel' ) } + </Button> + <Button + variant="primary" + type="submit" + isBusy={ isBusy } + aria-disabled={ isBusy } + > + { __( 'Create' ) } + </Button> + </HStack> + </VStack> + </form> + ); +} + +export default AddCustomGenericTemplateModalContent; diff --git a/packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal.js b/packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal.js deleted file mode 100644 index 05688d032a5917..00000000000000 --- a/packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * External dependencies - */ -import { kebabCase } from 'lodash'; - -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { - Button, - Modal, - TextControl, - __experimentalHStack as HStack, - __experimentalVStack as VStack, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import TemplateActionsLoadingScreen from './template-actions-loading-screen'; - -function AddCustomGenericTemplateModal( { - onClose, - createTemplate, - isCreatingTemplate, -} ) { - const [ title, setTitle ] = useState( '' ); - const defaultTitle = __( 'Custom Template' ); - const [ isBusy, setIsBusy ] = useState( false ); - async function onCreateTemplate( event ) { - event.preventDefault(); - if ( isBusy ) { - return; - } - setIsBusy( true ); - try { - await createTemplate( - { - slug: - 'wp-custom-template-' + - kebabCase( title || defaultTitle ), - title: title || defaultTitle, - }, - false - ); - } finally { - setIsBusy( false ); - } - } - return ( - <Modal - title={ __( 'Create custom template' ) } - onRequestClose={ () => { - onClose(); - } } - overlayClassName="edit-site-custom-generic-template__modal" - > - { isCreatingTemplate && <TemplateActionsLoadingScreen /> } - <form onSubmit={ onCreateTemplate }> - <VStack spacing={ 6 }> - <TextControl - __nextHasNoMarginBottom - label={ __( 'Name' ) } - value={ title } - onChange={ setTitle } - placeholder={ defaultTitle } - disabled={ isBusy } - help={ __( - 'Describe the template, e.g. "Post with sidebar".' - ) } - /> - <HStack - className="edit-site-custom-generic-template__modal-actions" - justify="right" - > - <Button - variant="tertiary" - onClick={ () => { - onClose(); - } } - > - { __( 'Cancel' ) } - </Button> - <Button - variant="primary" - type="submit" - isBusy={ isBusy } - aria-disabled={ isBusy } - > - { __( 'Create' ) } - </Button> - </HStack> - </VStack> - </form> - </Modal> - ); -} - -export default AddCustomGenericTemplateModal; diff --git a/packages/edit-site/src/components/add-new-template/add-custom-template-modal.js b/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js similarity index 89% rename from packages/edit-site/src/components/add-new-template/add-custom-template-modal.js rename to packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js index ccab5d82bd0587..5636ec16e1ac1d 100644 --- a/packages/edit-site/src/components/add-new-template/add-custom-template-modal.js +++ b/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js @@ -2,12 +2,11 @@ * WordPress dependencies */ import { useState, useMemo, useEffect } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { Button, Flex, FlexItem, - Modal, SearchControl, TextHighlight, __experimentalText as Text, @@ -23,7 +22,6 @@ import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies */ -import TemplateActionsLoadingScreen from './template-actions-loading-screen'; import { mapToIHasNameAndId } from './utils'; const EMPTY_ARRAY = []; @@ -179,36 +177,25 @@ function SuggestionList( { entityForSuggestions, onSelect } ) { ); } -function AddCustomTemplateModal( { - onClose, - onSelect, - entityForSuggestions, - isCreatingTemplate, -} ) { +function AddCustomTemplateModalContent( { onSelect, entityForSuggestions } ) { const [ showSearchEntities, setShowSearchEntities ] = useState( entityForSuggestions.hasGeneralTemplate ); - const baseCssClass = 'edit-site-custom-template-modal'; return ( - <Modal - title={ sprintf( - // translators: %s: Name of the post type e.g: "Post". - __( 'Add template: %s' ), - entityForSuggestions.labels.singular_name - ) } - className={ baseCssClass } - onRequestClose={ onClose } + <VStack + spacing={ 4 } + className="edit-site-custom-template-modal__contents-wrapper" + alignment="left" > - { isCreatingTemplate && <TemplateActionsLoadingScreen /> } { ! showSearchEntities && ( - <VStack spacing={ 4 }> + <> <Text as="p"> { __( 'Select whether to create a single template for all items or a specific one.' ) } </Text> <Flex - className={ `${ baseCssClass }__contents` } + className="edit-site-custom-template-modal__contents" gap="4" align="initial" > @@ -272,10 +259,10 @@ function AddCustomTemplateModal( { </Text> </FlexItem> </Flex> - </VStack> + </> ) } { showSearchEntities && ( - <VStack spacing={ 4 }> + <> <Text as="p"> { __( 'This template will be used only for the specific item chosen.' @@ -285,10 +272,10 @@ function AddCustomTemplateModal( { entityForSuggestions={ entityForSuggestions } onSelect={ onSelect } /> - </VStack> + </> ) } - </Modal> + </VStack> ); } -export default AddCustomTemplateModal; +export default AddCustomTemplateModalContent; diff --git a/packages/edit-site/src/components/add-new-template/new-template.js b/packages/edit-site/src/components/add-new-template/new-template.js index 3db38fffdee81a..841d45749e6285 100644 --- a/packages/edit-site/src/components/add-new-template/new-template.js +++ b/packages/edit-site/src/components/add-new-template/new-template.js @@ -1,34 +1,22 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ import { - DropdownMenu, - MenuGroup, - MenuItem, - Tooltip, - VisuallyHidden, + Button, + Modal, + __experimentalGrid as Grid, + __experimentalText as Text, + __experimentalVStack as VStack, } from '@wordpress/components'; import { useState } from '@wordpress/element'; import { useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { - archive, - blockMeta, - category, - home, - list, - media, - notFound, - page, - plus, - post, - postAuthor, - postDate, - postList, - search, - tag, - layout as customGenericTemplateIcon, -} from '@wordpress/icons'; +import { plus } from '@wordpress/icons'; import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { privateApis as routerPrivateApis } from '@wordpress/router'; @@ -36,7 +24,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies */ -import AddCustomTemplateModal from './add-custom-template-modal'; +import AddCustomTemplateModalContent from './add-custom-template-modal-content'; import { useExistingTemplates, useDefaultTemplateTypes, @@ -45,7 +33,7 @@ import { useAuthorMenuItem, usePostTypeArchiveMenuItems, } from './utils'; -import AddCustomGenericTemplateModal from './add-custom-generic-template-modal'; +import AddCustomGenericTemplateModalContent from './add-custom-generic-template-modal-content'; import TemplateActionsLoadingScreen from './template-actions-loading-screen'; import { store as editSiteStore } from '../../store'; import { unlock } from '../../private-apis'; @@ -68,21 +56,35 @@ const DEFAULT_TEMPLATE_SLUGS = [ '404', ]; -const TEMPLATE_ICONS = { - 'front-page': home, - home: postList, - single: post, - page, - archive, - search, - 404: notFound, - index: list, - category, - author: postAuthor, - taxonomy: blockMeta, - date: postDate, - tag, - attachment: media, +function TemplateListItem( { title, description, onClick } ) { + return ( + <Button onClick={ onClick }> + <VStack + as="span" + spacing={ 2 } + justify="flex-start" + style={ { width: '100%' } } + > + <Text + weight={ 500 } + lineHeight={ 1.53846153846 } // 20px + > + { title } + </Text> + <Text + lineHeight={ 1.53846153846 } // 20px + > + { description } + </Text> + </VStack> + </Button> + ); +} + +const modalContentMap = { + templatesList: 1, + customTemplate: 2, + customGenericTemplate: 3, }; export default function NewTemplate( { @@ -90,8 +92,10 @@ export default function NewTemplate( { toggleProps, showIcon = true, } ) { - const [ showCustomTemplateModal, setShowCustomTemplateModal ] = - useState( false ); + const [ showModal, setShowModal ] = useState( false ); + const [ modalContent, setModalContent ] = useState( + modalContentMap.templatesList + ); const [ showCustomGenericTemplateModal, setShowCustomGenericTemplateModal, @@ -159,130 +163,114 @@ export default function NewTemplate( { setIsCreatingTemplate( false ); } } + const onModalClose = () => { + setShowModal( false ); + setModalContent( modalContentMap.templatesList ); + }; - const missingTemplates = useMissingTemplates( - setEntityForSuggestions, - setShowCustomTemplateModal + const missingTemplates = useMissingTemplates( setEntityForSuggestions, () => + setModalContent( modalContentMap.customTemplate ) ); if ( ! missingTemplates.length ) { return null; } + const { as: Toggle = Button, ...restToggleProps } = toggleProps ?? {}; - const customTemplateDescription = __( - 'A custom template can be manually applied to any post or page.' - ); - + let modalTitle = __( 'Add template' ); + if ( modalContent === modalContentMap.customTemplate ) { + modalTitle = sprintf( + // translators: %s: Name of the post type e.g: "Post". + __( 'Add template: %s' ), + entityForSuggestions.labels.singular_name + ); + } else if ( showCustomGenericTemplateModal ) { + modalTitle = __( 'Create custom template' ); + } return ( <> - <DropdownMenu - className="edit-site-new-template-dropdown" + { isCreatingTemplate && <TemplateActionsLoadingScreen /> } + <Toggle + { ...restToggleProps } + onClick={ () => setShowModal( true ) } icon={ showIcon ? plus : null } - text={ showIcon ? null : postType.labels.add_new } label={ postType.labels.add_new_item } - popoverProps={ { - noArrow: false, - } } - toggleProps={ toggleProps } > - { () => ( - <> - { isCreatingTemplate && ( - <TemplateActionsLoadingScreen /> - ) } - <div className="edit-site-new-template-dropdown__menu-groups"> - <MenuGroup label={ postType.labels.add_new_item }> - { missingTemplates.map( ( template ) => { - const { - title, - description, - slug, - onClick, - icon, - } = template; - return ( - <Tooltip - key={ slug } - position="top right" - text={ description } - className="edit-site-new-template-dropdown__menu-item-tooltip" - > - <MenuItem - icon={ - icon || - TEMPLATE_ICONS[ slug ] || - post - } - iconPosition="left" - onClick={ () => - onClick - ? onClick( template ) - : createTemplate( - template - ) - } - > - { title } - { /* TODO: This probably won't be needed if the <Tooltip> component is accessible. - * @see https://github.com/WordPress/gutenberg/issues/48222 */ } - <VisuallyHidden> - { description } - </VisuallyHidden> - </MenuItem> - </Tooltip> - ); - } ) } - </MenuGroup> - <MenuGroup> - <Tooltip - position="top right" - text={ customTemplateDescription } - className="edit-site-new-template-dropdown__menu-item-tooltip" - > - <MenuItem - icon={ customGenericTemplateIcon } - iconPosition="left" + { showIcon ? null : postType.labels.add_new_item } + </Toggle> + { showModal && ( + <Modal + title={ modalTitle } + className={ classnames( + 'edit-site-add-new-template__modal', + { + 'edit-site-add-new-template__modal_template_list': + modalContent === modalContentMap.templatesList, + 'edit-site-custom-template-modal': + modalContent === modalContentMap.customTemplate, + } + ) } + onRequestClose={ onModalClose } + overlayClassName={ + modalContent === modalContentMap.customGenericTemplate + ? 'edit-site-custom-generic-template__modal' + : undefined + } + > + { modalContent === modalContentMap.templatesList && ( + <Grid + columns={ 3 } + gap={ 4 } + align="flex-start" + justify="center" + className="edit-site-add-new-template__template-list__contents" + > + { missingTemplates.map( ( template ) => { + const { title, description, slug, onClick } = + template; + return ( + <TemplateListItem + key={ slug } + title={ title } + description={ description } onClick={ () => - setShowCustomGenericTemplateModal( - true - ) + onClick + ? onClick( template ) + : createTemplate( template ) } - > - { __( 'Custom template' ) } - { /* TODO: This probably won't be needed if the <Tooltip> component is accessible. - * @see https://github.com/WordPress/gutenberg/issues/48222 */ } - <VisuallyHidden> - { customTemplateDescription } - </VisuallyHidden> - </MenuItem> - </Tooltip> - </MenuGroup> - </div> - </> - ) } - </DropdownMenu> - { showCustomTemplateModal && ( - <AddCustomTemplateModal - onClose={ () => setShowCustomTemplateModal( false ) } - onSelect={ createTemplate } - entityForSuggestions={ entityForSuggestions } - isCreatingTemplate={ isCreatingTemplate } - /> - ) } - { showCustomGenericTemplateModal && ( - <AddCustomGenericTemplateModal - onClose={ () => setShowCustomGenericTemplateModal( false ) } - createTemplate={ createTemplate } - isCreatingTemplate={ isCreatingTemplate } - /> + /> + ); + } ) } + <TemplateListItem + title={ __( 'Custom template' ) } + description={ __( + 'A custom template can be manually applied to any post or page.' + ) } + onClick={ () => + setShowCustomGenericTemplateModal( true ) + } + /> + </Grid> + ) } + { modalContent === modalContentMap.customTemplate && ( + <AddCustomTemplateModalContent + onSelect={ createTemplate } + entityForSuggestions={ entityForSuggestions } + /> + ) } + { modalContent === + modalContentMap.customGenericTemplate && ( + <AddCustomGenericTemplateModalContent + onClose={ onModalClose } + createTemplate={ createTemplate } + /> + ) } + </Modal> ) } </> ); } -function useMissingTemplates( - setEntityForSuggestions, - setShowCustomTemplateModal -) { +function useMissingTemplates( setEntityForSuggestions, onClick ) { const existingTemplates = useExistingTemplates(); const defaultTemplateTypes = useDefaultTemplateTypes(); const existingTemplateSlugs = ( existingTemplates || [] ).map( @@ -294,7 +282,7 @@ function useMissingTemplates( ! existingTemplateSlugs.includes( template.slug ) ); const onClickMenuItem = ( _entityForSuggestions ) => { - setShowCustomTemplateModal( true ); + onClick?.(); setEntityForSuggestions( _entityForSuggestions ); }; // We need to replace existing default template types with diff --git a/packages/edit-site/src/components/add-new-template/style.scss b/packages/edit-site/src/components/add-new-template/style.scss index fc33da1f35a133..f0617ad62fc5b9 100644 --- a/packages/edit-site/src/components/add-new-template/style.scss +++ b/packages/edit-site/src/components/add-new-template/style.scss @@ -1,70 +1,23 @@ -.edit-site-new-template-dropdown { - .edit-site-new-template-dropdown__menu-groups { - @include break-small() { - min-width: 300px; - } - } +.edit-site-custom-template-modal { + &__contents-wrapper { + height: 100%; + justify-content: flex-start !important; // Required as topLeft alignment isn't working on VStack - // The specificity is needed to override the default tooltip styles. - &__menu-item-tooltip.components-tooltip .components-popover__content { - max-width: 320px; - padding: $grid-unit-10 $grid-unit-15; - border-radius: 2px; - white-space: pre-wrap; - min-width: 0; - width: auto; - text-align: left; - } -} + > * { + width: 100%; + } -.edit-site-custom-template-modal { - &__suggestions_list { - margin-left: - $grid-unit-15; - margin-right: - $grid-unit-15; + &__suggestions_list { + margin-left: - $grid-unit-15; + margin-right: - $grid-unit-15; + width: calc(100% + #{$grid-unit-15 * 2}); + } } &__contents { > .components-button { - padding: $grid-unit-30; - border-radius: $radius-block-ui; height: auto; - display: flex; - flex-direction: column; justify-content: center; - border: $border-width solid $gray-300; - - // Show the boundary of the button, in High Contrast Mode. - outline: 1px solid transparent; - - span:first-child { - color: $gray-900; - } - - span { - color: $gray-700; - } - - &:hover { - color: var(--wp-admin-theme-color-darker-10); - background: rgba(var(--wp-admin-theme-color--rgb), 0.04); - border-color: transparent; - - span { - color: var(--wp-admin-theme-color); - } - } - - &:focus { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - border-color: transparent; - - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 3px solid transparent; - - span:first-child { - color: var(--wp-admin-theme-color); - } - } } } @@ -86,7 +39,6 @@ .edit-site-custom-template-modal__suggestions_list { @include break-small() { - height: 232px; overflow: scroll; } @@ -177,5 +129,72 @@ align-items: center; justify-content: center; height: 100%; + position: absolute; + left: 50%; + transform: translateX(-50%); + } +} + +.edit-site-add-new-template__modal { + max-width: $grid-unit-80 * 13; + width: calc(100% - #{$grid-unit-80}); + margin-top: $grid-unit-80; + max-height: calc(100% - #{$grid-unit-80 * 2}); + + @include break-large() { + width: calc(100% - #{$grid-unit-80 * 2}); + } +} + +.edit-site-custom-template-modal__contents, +.edit-site-add-new-template__template-list__contents { + > .components-button { + padding: $grid-unit-30; + border-radius: $radius-block-ui; + display: flex; + flex-direction: column; + border: $border-width solid $gray-300; + min-height: $grid-unit-80 * 3; + + // Show the boundary of the button, in High Contrast Mode. + outline: 1px solid transparent; + + span:first-child { + color: $gray-900; + } + + span { + color: $gray-700; + } + + &:hover { + color: var(--wp-admin-theme-color-darker-10); + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + border-color: transparent; + + span { + color: var(--wp-admin-theme-color); + } + } + + &:focus { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + border-color: transparent; + + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 3px solid transparent; + + span:first-child { + color: var(--wp-admin-theme-color); + } + } + } +} + +.edit-site-add-new-template__template-list__contents { + > .components-button { + height: 100%; + text-align: start; + align-items: flex-start; } } diff --git a/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js b/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js index edc4909a98c979..5668832e4162c3 100644 --- a/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js +++ b/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js @@ -45,7 +45,7 @@ test.describe( 'Site editor url navigation', () => { ] ); await page.click( 'role=button[name="Add New Template"i]' ); await page - .getByRole( 'menuitem', { + .getByRole( 'button', { name: 'Single item: Post', } ) .click(); From 2452a92dce0e2db2b8b58be3089e7c31e4f74b45 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 17 May 2023 11:11:49 +0100 Subject: [PATCH 066/131] Marks the commands APIs as stable (#50691) --- packages/commands/README.md | 16 ++++++++++++++++ packages/commands/src/index.js | 2 ++ packages/commands/src/private-apis.js | 4 ---- .../core-commands/src/add-post-type-commands.js | 9 +-------- .../src/site-editor-navigation-commands.js | 3 +-- .../src/hooks/commands/use-edit-mode-commands.js | 3 +-- 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/commands/README.md b/packages/commands/README.md index 3113c540e4dd05..0e0afdce394c99 100644 --- a/packages/commands/README.md +++ b/packages/commands/README.md @@ -24,6 +24,22 @@ Undocumented declaration. Undocumented declaration. +### useCommand + +Attach a command to the Global command menu. + +_Parameters_ + +- _command_ `import('../store/actions').WPCommandConfig`: command config. + +### useCommandLoader + +Attach a command loader to the Global command menu. + +_Parameters_ + +- _loader_ `import('../store/actions').WPCommandLoaderConfig`: command loader config. + <!-- END TOKEN(Autogenerated API docs) --> ## Contributing to this package diff --git a/packages/commands/src/index.js b/packages/commands/src/index.js index fbac76197f0b24..afc7ac27b7d5f4 100644 --- a/packages/commands/src/index.js +++ b/packages/commands/src/index.js @@ -1,2 +1,4 @@ export { CommandMenu } from './components/command-menu'; export { privateApis } from './private-apis'; +export { default as useCommand } from './hooks/use-command'; +export { default as useCommandLoader } from './hooks/use-command-loader'; diff --git a/packages/commands/src/private-apis.js b/packages/commands/src/private-apis.js index 3e8bfab2343c18..7348711efd3517 100644 --- a/packages/commands/src/private-apis.js +++ b/packages/commands/src/private-apis.js @@ -6,8 +6,6 @@ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/pri /** * Internal dependencies */ -import { default as useCommand } from './hooks/use-command'; -import { default as useCommandLoader } from './hooks/use-command-loader'; import { default as useCommandContext } from './hooks/use-command-context'; import { store } from './store'; @@ -19,8 +17,6 @@ export const { lock, unlock } = export const privateApis = {}; lock( privateApis, { - useCommand, - useCommandLoader, useCommandContext, store, } ); diff --git a/packages/core-commands/src/add-post-type-commands.js b/packages/core-commands/src/add-post-type-commands.js index 3c2a7ea8c52cdd..47e6014f569444 100644 --- a/packages/core-commands/src/add-post-type-commands.js +++ b/packages/core-commands/src/add-post-type-commands.js @@ -1,17 +1,10 @@ /** * WordPress dependencies */ -import { privateApis } from '@wordpress/commands'; +import { useCommand } from '@wordpress/commands'; import { __ } from '@wordpress/i18n'; import { plus } from '@wordpress/icons'; -/** - * Internal dependencies - */ -import { unlock } from './lock-unlock'; - -const { useCommand } = unlock( privateApis ); - export function useAddPostTypeCommands() { useCommand( { name: 'core/add-new-post', diff --git a/packages/core-commands/src/site-editor-navigation-commands.js b/packages/core-commands/src/site-editor-navigation-commands.js index 1bf29fa08f9cea..31f65bb98579e2 100644 --- a/packages/core-commands/src/site-editor-navigation-commands.js +++ b/packages/core-commands/src/site-editor-navigation-commands.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { privateApis } from '@wordpress/commands'; +import { useCommandLoader } from '@wordpress/commands'; import { __ } from '@wordpress/i18n'; import { useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; @@ -15,7 +15,6 @@ import { getQueryArg, addQueryArgs, getPath } from '@wordpress/url'; */ import { unlock } from './lock-unlock'; -const { useCommandLoader } = unlock( privateApis ); const { useHistory } = unlock( routerPrivateApis ); const icons = { diff --git a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js index 67b7164edcdab5..4aef33ac2b1cfc 100644 --- a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js +++ b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js @@ -4,7 +4,7 @@ import { useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { trash, backup } from '@wordpress/icons'; -import { privateApis as commandsPrivateApis } from '@wordpress/commands'; +import { useCommandLoader } from '@wordpress/commands'; import { privateApis as routerPrivateApis } from '@wordpress/router'; /** @@ -16,7 +16,6 @@ import isTemplateRemovable from '../../utils/is-template-removable'; import isTemplateRevertable from '../../utils/is-template-revertable'; import { unlock } from '../../private-apis'; -const { useCommandLoader } = unlock( commandsPrivateApis ); const { useHistory } = unlock( routerPrivateApis ); function useEditModeCommandLoader() { From 22c837b2f93ffed6dc254ba1a721218b2ca8a63a Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Wed, 17 May 2023 08:15:32 -0400 Subject: [PATCH 067/131] DropdownMenu: refactor to TypeScript (#50187) --- packages/components/CHANGELOG.md | 2 + .../components/src/dropdown-menu/README.md | 34 ++--- .../src/dropdown-menu/{index.js => index.tsx} | 136 ++++++++++++++--- .../stories/{index.js => index.tsx} | 35 ++--- .../test/{index.js => index.tsx} | 11 +- .../components/src/dropdown-menu/types.ts | 143 ++++++++++++++++++ .../components/src/toolbar/stories/index.tsx | 53 +++---- 7 files changed, 312 insertions(+), 102 deletions(-) rename packages/components/src/dropdown-menu/{index.js => index.tsx} (54%) rename packages/components/src/dropdown-menu/stories/{index.js => index.tsx} (79%) rename packages/components/src/dropdown-menu/test/{index.js => index.tsx} (89%) create mode 100644 packages/components/src/dropdown-menu/types.ts diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6e46c3b70f495a..d4cc445dd379d9 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -5,6 +5,8 @@ ### Internal - `Modal`: Remove children container's unused class name ([#50655](https://github.com/WordPress/gutenberg/pull/50655)). +- `DropdownMenu`: Convert to TypeScript ([#50187](https://github.com/WordPress/gutenberg/pull/50187)). + ## 24.0.0 (2023-05-10) diff --git a/packages/components/src/dropdown-menu/README.md b/packages/components/src/dropdown-menu/README.md index fc6fc4ba708c9e..e1e4c7bf031b0f 100644 --- a/packages/components/src/dropdown-menu/README.md +++ b/packages/components/src/dropdown-menu/README.md @@ -131,80 +131,70 @@ const MyDropdownMenu = () => ( The component accepts the following props: -#### icon +#### `icon`: `string | null` The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug to be shown in the collapsed menu button. -- Type: `String|null` - Required: No - Default: `"menu"` See also: [https://developer.wordpress.org/resource/dashicons/](https://developer.wordpress.org/resource/dashicons/) -#### label +#### `label`: `string` A human-readable label to present as accessibility text on the focused collapsed menu button. -- Type: `String` - Required: Yes -#### controls +#### `controls:` `DropdownOption[] | DropdownOption[][]` -An array of objects describing the options to be shown in the expanded menu. +An array or nested array of objects describing the options to be shown in the expanded menu. Each object should include an `icon` [Dashicon](https://developer.wordpress.org/resource/dashicons/) slug string, a human-readable `title` string, `isDisabled` boolean flag and an `onClick` function callback to invoke when the option is selected. -A valid DropdownMenu must specify one or the other of a `controls` or `children` prop. - -- Type: `Array` +A valid DropdownMenu must specify a `controls` or `children` prop, or both. - Required: No -#### children +#### `children`: `( callbackProps: DropdownCallbackProps ) => ReactNode` A [function render prop](https://reactjs.org/docs/render-props.html#using-props-other-than-render) which should return an element or elements valid for use in a DropdownMenu: `MenuItem`, `MenuItemsChoice`, or `MenuGroup`. Its first argument is a props object including the same values as given to a [`Dropdown`'s `renderContent`](/packages/components/src/dropdown#rendercontent) (`isOpen`, `onToggle`, `onClose`). -A valid DropdownMenu must specify one or the other of a `controls` or `children` prop. +A valid DropdownMenu must specify a `controls` or `children` prop, or both. -- Type: `Function` - Required: No See also: [https://developer.wordpress.org/resource/dashicons/](https://developer.wordpress.org/resource/dashicons/) -#### className +#### `className`: `string` A class name to apply to the dropdown menu's toggle element wrapper. -- Type: `String` - Required: No -#### popoverProps +#### `popoverProps`: `DropdownProps[ 'popoverProps' ]` Properties of `popoverProps` object will be passed as props to the nested `Popover` component. Use this object to modify props available for the `Popover` component that are not already exposed in the `DropdownMenu` component, e.g.: the direction in which the popover should open relative to its parent node set with `position` prop. -- Type: `Object` - Required: No -#### toggleProps +#### `toggleProps`: `ToggleProps` Properties of `toggleProps` object will be passed as props to the nested `Button` component in the `renderToggle` implementation of the `Dropdown` component used internally. Use this object to modify props available for the `Button` component that are not already exposed in the `DropdownMenu` component, e.g.: the tooltip text displayed on hover set with `tooltip` prop. -- Type: `Object` - Required: No -#### menuProps +#### `menuProps`: `NavigableContainerProps` Properties of `menuProps` object will be passed as props to the nested `NavigableMenu` component in the `renderContent` implementation of the `Dropdown` component used internally. Use this object to modify props available for the `NavigableMenu` component that are not already exposed in the `DropdownMenu` component, e.g.: the orientation of the menu set with `orientation` prop. -- Type: `Object` - Required: No -#### disableOpenOnArrowDown +#### `disableOpenOnArrowDown`: `boolean` In some contexts, the arrow down key used to open the dropdown menu might need to be disabled—for example when that key is used to perform another action. -- Type: `boolean` - Required: No - Default: `false` diff --git a/packages/components/src/dropdown-menu/index.js b/packages/components/src/dropdown-menu/index.tsx similarity index 54% rename from packages/components/src/dropdown-menu/index.js rename to packages/components/src/dropdown-menu/index.tsx index 481e87cd102b7d..805bcd06611798 100644 --- a/packages/components/src/dropdown-menu/index.js +++ b/packages/components/src/dropdown-menu/index.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck /** * External dependencies */ @@ -15,9 +14,12 @@ import { menu } from '@wordpress/icons'; import Button from '../button'; import Dropdown from '../dropdown'; import { NavigableMenu } from '../navigable-container'; +import type { DropdownMenuProps, DropdownOption } from './types'; -function mergeProps( defaultProps = {}, props = {} ) { - const mergedProps = { +function mergeProps< + T extends { className?: string; [ key: string ]: unknown } +>( defaultProps: Partial< T > = {}, props: T = {} as T ) { + const mergedProps: T = { ...defaultProps, ...props, }; @@ -32,17 +34,92 @@ function mergeProps( defaultProps = {}, props = {} ) { return mergedProps; } +function isFunction( maybeFunc: unknown ): maybeFunc is () => void { + return typeof maybeFunc === 'function'; +} + /** - * Whether the argument is a function. * - * @param {*} maybeFunc The argument to check. - * @return {boolean} True if the argument is a function, false otherwise. + * The DropdownMenu displays a list of actions (each contained in a MenuItem, + * MenuItemsChoice, or MenuGroup) in a compact way. It appears in a Popover + * after the user has interacted with an element (a button or icon) or when + * they perform a specific action. + * + * Render a Dropdown Menu with a set of controls: + * + * ```jsx + * import { DropdownMenu } from '@wordpress/components'; + * import { + * more, + * arrowLeft, + * arrowRight, + * arrowUp, + * arrowDown, + * } from '@wordpress/icons'; + * + * const MyDropdownMenu = () => ( + * <DropdownMenu + * icon={ more } + * label="Select a direction" + * controls={ [ + * { + * title: 'Up', + * icon: arrowUp, + * onClick: () => console.log( 'up' ), + * }, + * { + * title: 'Right', + * icon: arrowRight, + * onClick: () => console.log( 'right' ), + * }, + * { + * title: 'Down', + * icon: arrowDown, + * onClick: () => console.log( 'down' ), + * }, + * { + * title: 'Left', + * icon: arrowLeft, + * onClick: () => console.log( 'left' ), + * }, + * ] } + * /> + * ); + * ``` + * + * Alternatively, specify a `children` function which returns elements valid for + * use in a DropdownMenu: `MenuItem`, `MenuItemsChoice`, or `MenuGroup`. + * + * ```jsx + * import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; + * import { more, arrowUp, arrowDown, trash } from '@wordpress/icons'; + * + * const MyDropdownMenu = () => ( + * <DropdownMenu icon={ more } label="Select a direction"> + * { ( { onClose } ) => ( + * <> + * <MenuGroup> + * <MenuItem icon={ arrowUp } onClick={ onClose }> + * Move Up + * </MenuItem> + * <MenuItem icon={ arrowDown } onClick={ onClose }> + * Move Down + * </MenuItem> + * </MenuGroup> + * <MenuGroup> + * <MenuItem icon={ trash } onClick={ onClose }> + * Remove + * </MenuItem> + * </MenuGroup> + * </> + * ) } + * </DropdownMenu> + * ); + * ``` + * */ -function isFunction( maybeFunc ) { - return typeof maybeFunc === 'function'; -} -function DropdownMenu( dropdownMenuProps ) { +function DropdownMenu( dropdownMenuProps: DropdownMenuProps ) { const { children, className, @@ -62,13 +139,18 @@ function DropdownMenu( dropdownMenuProps ) { } // Normalize controls to nested array of objects (sets of controls) - let controlSets; + let controlSets: DropdownOption[][]; if ( controls?.length ) { + // @ts-expect-error The check below is needed because `DropdownMenus` + // rendered by `ToolBarGroup` receive controls as a nested array. controlSets = controls; if ( ! Array.isArray( controlSets[ 0 ] ) ) { - controlSets = [ controlSets ]; + // This is not ideal, but at this point we know that `controls` is + // not a nested array, even if TypeScript doesn't. + controlSets = [ controls as DropdownOption[] ]; } } + const mergedPopoverProps = mergeProps( { className: 'components-dropdown-menu__popover', @@ -81,7 +163,7 @@ function DropdownMenu( dropdownMenuProps ) { className={ classnames( 'components-dropdown-menu', className ) } popoverProps={ mergedPopoverProps } renderToggle={ ( { isOpen, onToggle } ) => { - const openOnArrowDown = ( event ) => { + const openOnArrowDown = ( event: React.KeyboardEvent ) => { if ( disableOpenOnArrowDown ) { return; } @@ -110,18 +192,22 @@ function DropdownMenu( dropdownMenuProps ) { <Toggle { ...mergedToggleProps } icon={ icon } - onClick={ ( event ) => { - onToggle( event ); - if ( mergedToggleProps.onClick ) { - mergedToggleProps.onClick( event ); - } - } } - onKeyDown={ ( event ) => { - openOnArrowDown( event ); - if ( mergedToggleProps.onKeyDown ) { - mergedToggleProps.onKeyDown( event ); - } - } } + onClick={ + ( ( event ) => { + onToggle(); + if ( mergedToggleProps.onClick ) { + mergedToggleProps.onClick( event ); + } + } ) as React.MouseEventHandler< HTMLButtonElement > + } + onKeyDown={ + ( ( event ) => { + openOnArrowDown( event ); + if ( mergedToggleProps.onKeyDown ) { + mergedToggleProps.onKeyDown( event ); + } + } ) as React.KeyboardEventHandler< HTMLButtonElement > + } aria-haspopup="true" aria-expanded={ isOpen } label={ label } diff --git a/packages/components/src/dropdown-menu/stories/index.js b/packages/components/src/dropdown-menu/stories/index.tsx similarity index 79% rename from packages/components/src/dropdown-menu/stories/index.js rename to packages/components/src/dropdown-menu/stories/index.tsx index 33477854721ee0..97a51371d1ab8f 100644 --- a/packages/components/src/dropdown-menu/stories/index.js +++ b/packages/components/src/dropdown-menu/stories/index.tsx @@ -1,8 +1,12 @@ +/** + * External dependencies + */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; /** * Internal dependencies */ -import DropdownMenu from '../'; -import { MenuGroup, MenuItem } from '../../'; +import DropdownMenu from '..'; +import { MenuGroup, MenuItem } from '../..'; /** * WordPress dependencies @@ -16,37 +20,24 @@ import { trash, } from '@wordpress/icons'; -export default { +const meta: ComponentMeta< typeof DropdownMenu > = { title: 'Components/DropdownMenu', component: DropdownMenu, + parameters: { + controls: { expanded: true }, + docs: { source: { state: 'open' } }, + }, argTypes: { - className: { control: { type: 'text' } }, - children: { control: { type: null } }, - disableOpenOnArrowDown: { control: { type: 'boolean' } }, icon: { options: [ 'menu', 'chevronDown', 'more' ], mapping: { menu, chevronDown, more }, control: { type: 'select' }, }, - menuProps: { - control: { type: 'object' }, - }, - noIcons: { control: { type: 'boolean' } }, - popoverProps: { - control: { type: 'object' }, - }, - text: { control: { type: 'text' } }, - toggleProps: { - control: { type: 'object' }, - }, - }, - parameters: { - controls: { expanded: true }, - docs: { source: { state: 'open' } }, }, }; +export default meta; -const Template = ( props ) => ( +const Template: ComponentStory< typeof DropdownMenu > = ( props ) => ( <div style={ { height: 150 } }> <DropdownMenu { ...props } /> </div> diff --git a/packages/components/src/dropdown-menu/test/index.js b/packages/components/src/dropdown-menu/test/index.tsx similarity index 89% rename from packages/components/src/dropdown-menu/test/index.js rename to packages/components/src/dropdown-menu/test/index.tsx index b40ab218ccd9f6..118e991812367e 100644 --- a/packages/components/src/dropdown-menu/test/index.js +++ b/packages/components/src/dropdown-menu/test/index.tsx @@ -12,19 +12,19 @@ import { arrowLeft, arrowRight, arrowUp, arrowDown } from '@wordpress/icons'; /** * Internal dependencies */ -import DropdownMenu from '../'; -import { MenuItem } from '../../'; +import DropdownMenu from '..'; +import { MenuItem } from '../..'; describe( 'DropdownMenu', () => { it( 'should not render when neither controls nor children are assigned', () => { - render( <DropdownMenu /> ); + render( <DropdownMenu label="Open dropdown" /> ); // The button toggle should not even be rendered expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument(); } ); it( 'should not render when controls are empty and children is not specified', () => { - render( <DropdownMenu controls={ [] } /> ); + render( <DropdownMenu label="Open dropdown" controls={ [] } /> ); // The button toggle should not even be rendered expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument(); @@ -56,7 +56,7 @@ describe( 'DropdownMenu', () => { }, ]; - render( <DropdownMenu controls={ controls } /> ); + render( <DropdownMenu label="Open dropdown" controls={ controls } /> ); // Move focus on the toggle button await user.tab(); @@ -78,6 +78,7 @@ describe( 'DropdownMenu', () => { render( <DropdownMenu + label="Open dropdown" children={ ( { onClose } ) => <MenuItem onClick={ onClose } /> } /> ); diff --git a/packages/components/src/dropdown-menu/types.ts b/packages/components/src/dropdown-menu/types.ts new file mode 100644 index 00000000000000..badfcb54d60727 --- /dev/null +++ b/packages/components/src/dropdown-menu/types.ts @@ -0,0 +1,143 @@ +/** + * External dependencies + */ +import type { ReactNode } from 'react'; +/** + * Internal dependencies + */ +import type { ButtonAsButtonProps } from '../button/types'; +import type { WordPressComponentProps } from '../ui/context'; +import type { DropdownProps } from '../dropdown/types'; +import type { Props as IconProps } from '../icon'; +import type { NavigableMenuProps } from '../navigable-container/types'; + +export type DropdownOption = { + /** + * The Dashicon icon slug to be shown for the option. + */ + icon?: IconProps[ 'icon' ]; + /** + * A human-readable title to display for the option. + */ + title: string; + /** + * Whether or not the option is disabled. + * + * @default false + */ + isDisabled?: boolean; + /** + * A callback function to invoke when the option is selected. + */ + onClick?: () => void; + /** + * Whether or not the control is currently active. + */ + isActive?: boolean; + /** + * Text to use for the internal `Button` component's tooltip. + */ + label?: string; + /** + * The role to apply to the option's HTML element + */ + role?: HTMLElement[ 'role' ]; +}; + +type DropdownCallbackProps = { + isOpen: boolean; + onToggle: () => void; + onClose: () => void; +}; + +// Manually including `as` prop because `WordPressComponentProps` polymorhpism +// creates a union that is too large for TypeScript to handle. +type ToggleProps = Partial< + Omit< + WordPressComponentProps< ButtonAsButtonProps, 'button', false >, + 'label' | 'text' + > +> & { + as?: React.ElementType | keyof JSX.IntrinsicElements; +}; + +export type DropdownMenuProps = { + /** + * The Dashicon icon slug to be shown in the collapsed menu button. + * + * @default "menu" + */ + icon?: IconProps[ 'icon' ] | null; + /** + * A human-readable label to present as accessibility text on the focused + * collapsed menu button. + */ + label: string; + /** + * A class name to apply to the dropdown menu's toggle element wrapper. + */ + className?: string; + /** + * Properties of `popoverProps` object will be passed as props to the nested + * `Popover` component. + * Use this object to modify props available for the `Popover` component that + * are not already exposed in the `DropdownMenu` component, e.g.: the + * direction in which the popover should open relative to its parent node + * set with `position` prop. + */ + popoverProps?: DropdownProps[ 'popoverProps' ]; + /** + * Properties of `toggleProps` object will be passed as props to the nested + * `Button` component in the `renderToggle` implementation of the `Dropdown` + * component used internally. + * Use this object to modify props available for the `Button` component that + * are not already exposed in the `DropdownMenu` component, e.g.: the tooltip + * text displayed on hover set with `tooltip` prop. + */ + toggleProps?: ToggleProps; + /** + * Properties of `menuProps` object will be passed as props to the nested + * `NavigableMenu` component in the `renderContent` implementation of the + * `Dropdown` component used internally. + * Use this object to modify props available for the `NavigableMenu` + * component that are not already exposed in the `DropdownMenu` component, + * e.g.: the orientation of the menu set with `orientation` prop. + */ + menuProps?: Omit< Partial< NavigableMenuProps >, 'children' >; + /** + * In some contexts, the arrow down key used to open the dropdown menu might + * need to be disabled—for example when that key is used to perform another + * action. + * + * @default false + */ + disableOpenOnArrowDown?: boolean; + /** + * Text to display on the nested `Button` component in the `renderToggle` + * implementation of the `Dropdown` component used internally. + */ + text?: string; + /** + * Whether or not `no-icons` should be added to the menu's `className`. + */ + noIcons?: boolean; + /** + * A [function render prop](https://reactjs.org/docs/render-props.html#using-props-other-than-render) + * which should return an element or elements valid for use in a DropdownMenu: + * `MenuItem`, `MenuItemsChoice`, or `MenuGroup`. Its first argument is a + * props object including the same values as given to a `Dropdown`'s + * `renderContent` (`isOpen`, `onToggle`, `onClose`). + * + * A valid DropdownMenu must specify a `controls` or `children` prop, or both. + */ + children?: ( callbackProps: DropdownCallbackProps ) => ReactNode; + /** + * An array or nested array of objects describing the options to be shown in + * the expanded menu. Each object should include an `icon` Dashicon slug + * string, a human-readable `title` string, `isDisabled` boolean flag, and + * an `onClick` function callback to invoke when the option is selected. + * + * A valid DropdownMenu must specify a `controls` or `children` prop, or both. + */ + controls?: DropdownOption[] | DropdownOption[][]; +}; diff --git a/packages/components/src/toolbar/stories/index.tsx b/packages/components/src/toolbar/stories/index.tsx index fd0fb9587347de..17a8e64111eb23 100644 --- a/packages/components/src/toolbar/stories/index.tsx +++ b/packages/components/src/toolbar/stories/index.tsx @@ -82,34 +82,31 @@ Default.args = { </ToolbarGroup> <ToolbarGroup> <ToolbarItem> - { - // @ts-expect-error TODO: Remove when DropdownMenu is typed - ( toggleProps ) => { - return ( - <DropdownMenu - hasArrowIndicator - icon={ alignLeft } - label="Align" - controls={ [ - { - icon: alignLeft, - title: 'Align left', - isActive: true, - }, - { - icon: alignCenter, - title: 'Align center', - }, - { - icon: alignRight, - title: 'Align right', - }, - ] } - toggleProps={ toggleProps } - /> - ); - } - } + { /* There is an issue here with TS not recognizing the + * `RenderProp` being passed. + * @ts-expect-error */ } + { ( toggleProps ) => ( + <DropdownMenu + icon={ alignLeft } + label="Align" + controls={ [ + { + icon: alignLeft, + title: 'Align left', + isActive: true, + }, + { + icon: alignCenter, + title: 'Align center', + }, + { + icon: alignRight, + title: 'Align right', + }, + ] } + toggleProps={ toggleProps } + /> + ) } </ToolbarItem> </ToolbarGroup> <ToolbarGroup> From b1066e5d936344fa311f2201da5b270283abca4d Mon Sep 17 00:00:00 2001 From: Ben Dwyer <ben@scruffian.com> Date: Wed, 17 May 2023 15:18:13 +0100 Subject: [PATCH 068/131] Navigation: Use the ListView in the Navigation block inspector controls (#49417) * avigtion: Use the ListView in the Navigation block inspector controls * use the hook from list view * remove prop drilling * remove prop drilling * Allow list view to scroll * add custom scrollbars on hover * update navigation block tests to account for using the list view * fix test --- .../src/components/list-view/index.js | 9 +- .../src/navigation-link/update-attributes.js | 4 +- .../src/navigation-link/use-inserted-block.js | 43 +++++++++ .../edit/menu-inspector-controls.js | 89 +++++++++++++++++-- .../block-library/src/navigation/editor.scss | 6 ++ .../specs/editor/blocks/navigation.spec.js | 39 ++++---- 6 files changed, 159 insertions(+), 31 deletions(-) create mode 100644 packages/block-library/src/navigation-link/use-inserted-block.js diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 1c0a093e208358..5e04a9cf9e1742 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -264,18 +264,23 @@ function ListViewComponent( </AsyncModeProvider> ); } + +// This is the private API for the ListView component. +// It allows access to all props, not just the public ones. export const PrivateListView = forwardRef( ListViewComponent ); +// This is the public API for the ListView component. +// We wrap the PrivateListView component to hide some props from the public API. export default forwardRef( ( props, ref ) => { return ( <PrivateListView ref={ ref } { ...props } showAppender={ false } - blockSettingsMenu={ BlockSettingsDropdown } rootClientId={ null } onSelect={ null } - renderAdditionalBlockUICallback={ null } + renderAdditionalBlockUI={ null } + blockSettingsMenu={ undefined } /> ); } ); diff --git a/packages/block-library/src/navigation-link/update-attributes.js b/packages/block-library/src/navigation-link/update-attributes.js index f618b5f03ef2fe..5133cae3878338 100644 --- a/packages/block-library/src/navigation-link/update-attributes.js +++ b/packages/block-library/src/navigation-link/update-attributes.js @@ -1,8 +1,8 @@ /** * WordPress dependencies */ -import { safeDecodeURI } from '@wordpress/url'; import { escapeHTML } from '@wordpress/escape-html'; +import { safeDecodeURI } from '@wordpress/url'; /** * @typedef {'post-type'|'custom'|'taxonomy'|'post-type-archive'} WPNavigationLinkKind @@ -89,7 +89,7 @@ export const updateAttributes = ( setAttributes( { // Passed `url` may already be encoded. To prevent double encoding, decodeURI is executed to revert to the original string. - ...{ url: newUrl ? encodeURI( safeDecodeURI( newUrl ) ) : newUrl }, + ...( newUrl && { url: encodeURI( safeDecodeURI( newUrl ) ) } ), ...( label && { label } ), ...( undefined !== opensInNewTab && { opensInNewTab } ), ...( id && Number.isInteger( id ) && { id } ), diff --git a/packages/block-library/src/navigation-link/use-inserted-block.js b/packages/block-library/src/navigation-link/use-inserted-block.js new file mode 100644 index 00000000000000..2644ca2e04f514 --- /dev/null +++ b/packages/block-library/src/navigation-link/use-inserted-block.js @@ -0,0 +1,43 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; + +export const useInsertedBlock = ( insertedBlockClientId ) => { + const { insertedBlockAttributes, insertedBlockName } = useSelect( + ( select ) => { + const { getBlockName, getBlockAttributes } = + select( blockEditorStore ); + + return { + insertedBlockAttributes: getBlockAttributes( + insertedBlockClientId + ), + insertedBlockName: getBlockName( insertedBlockClientId ), + }; + }, + [ insertedBlockClientId ] + ); + + const { updateBlockAttributes } = useDispatch( blockEditorStore ); + + const setInsertedBlockAttributes = ( _updatedAttributes ) => { + if ( ! insertedBlockClientId ) return; + updateBlockAttributes( insertedBlockClientId, _updatedAttributes ); + }; + + if ( ! insertedBlockClientId ) { + return { + insertedBlockAttributes: undefined, + insertedBlockName: undefined, + setInsertedBlockAttributes, + }; + } + + return { + insertedBlockAttributes, + insertedBlockName, + setInsertedBlockAttributes, + }; +}; diff --git a/packages/block-library/src/navigation/edit/menu-inspector-controls.js b/packages/block-library/src/navigation/edit/menu-inspector-controls.js index 19bc18fc069d05..1bd3e51063ec98 100644 --- a/packages/block-library/src/navigation/edit/menu-inspector-controls.js +++ b/packages/block-library/src/navigation/edit/menu-inspector-controls.js @@ -13,6 +13,7 @@ import { Spinner, } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; +import { useState, useEffect } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; /** @@ -23,9 +24,16 @@ import { unlock } from '../../private-apis'; import DeletedNavigationWarning from './deleted-navigation-warning'; import useNavigationMenu from '../use-navigation-menu'; import LeafMoreMenu from './leaf-more-menu'; +import { updateAttributes } from '../../navigation-link/update-attributes'; +import { LinkUI } from '../../navigation-link/link-ui'; +import { useInsertedBlock } from '../../navigation-link/use-inserted-block'; /* translators: %s: The name of a menu. */ const actionLabel = __( "Switch to '%s'" ); +const BLOCKS_WITH_LINK_UI_SUPPORT = [ + 'core/navigation-link', + 'core/navigation-submenu', +]; const MainContent = ( { clientId, @@ -34,7 +42,7 @@ const MainContent = ( { isNavigationMenuMissing, onCreateNew, } ) => { - const { OffCanvasEditor } = unlock( blockEditorPrivateApis ); + const { PrivateListView } = unlock( blockEditorPrivateApis ); // Provide a hierarchy of clientIds for the given Navigation block (clientId). // This is required else the list view will display the entire block tree. @@ -45,6 +53,42 @@ const MainContent = ( { }, [ clientId ] ); + + const [ clientIdWithOpenLinkUI, setClientIdWithOpenLinkUI ] = useState(); + const { lastInsertedBlockClientId } = useSelect( ( select ) => { + const { getLastInsertedBlocksClientIds } = unlock( + select( blockEditorStore ) + ); + const lastInsertedBlocksClientIds = getLastInsertedBlocksClientIds(); + return { + lastInsertedBlockClientId: + lastInsertedBlocksClientIds && lastInsertedBlocksClientIds[ 0 ], + }; + }, [] ); + + const { + insertedBlockAttributes, + insertedBlockName, + setInsertedBlockAttributes, + } = useInsertedBlock( lastInsertedBlockClientId ); + + const hasExistingLinkValue = insertedBlockAttributes?.url; + + useEffect( () => { + if ( + lastInsertedBlockClientId && + BLOCKS_WITH_LINK_UI_SUPPORT?.includes( insertedBlockName ) && + ! hasExistingLinkValue // don't re-show the Link UI if the block already has a link value. + ) { + setClientIdWithOpenLinkUI( lastInsertedBlockClientId ); + } + }, [ + lastInsertedBlockClientId, + clientId, + insertedBlockName, + hasExistingLinkValue, + ] ); + const { navigationMenu } = useNavigationMenu( currentMenuId ); if ( currentMenuId && isNavigationMenuMissing ) { @@ -59,19 +103,46 @@ const MainContent = ( { ? sprintf( /* translators: %s: The name of a menu. */ __( 'Structure for navigation menu: %s' ), - navigationMenu?.title || __( 'Untitled menu' ) + navigationMenu?.title?.rendered || __( 'Untitled menu' ) ) : __( 'You have not yet created any menus. Displaying a list of your Pages' ); + + const renderLinkUI = ( block ) => { + return ( + clientIdWithOpenLinkUI === block.clientId && ( + <LinkUI + clientId={ lastInsertedBlockClientId } + link={ insertedBlockAttributes } + onClose={ () => setClientIdWithOpenLinkUI( null ) } + hasCreateSuggestion={ false } + onChange={ ( updatedValue ) => { + updateAttributes( + updatedValue, + setInsertedBlockAttributes, + insertedBlockAttributes + ); + setClientIdWithOpenLinkUI( null ); + } } + onCancel={ () => setClientIdWithOpenLinkUI( null ) } + /> + ) + ); + }; + return ( - <OffCanvasEditor - blocks={ clientIdsTree } - parentClientId={ clientId } - isExpanded={ true } - LeafMoreMenu={ LeafMoreMenu } - description={ description } - /> + <div className="wp-block-navigation__menu-inspector-controls"> + <PrivateListView + blocks={ clientIdsTree } + rootClientId={ clientId } + isExpanded + description={ description } + showAppender + blockSettingsMenu={ LeafMoreMenu } + renderAdditionalBlockUI={ renderLinkUI } + /> + </div> ); }; diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index ac1db543d22100..1ba36e084da2ae 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -657,3 +657,9 @@ body.editor-styles-wrapper .wp-block-navigation__responsive-container.is-menu-op .wp-block-navigation__responsive-container-open.components-button { opacity: 1; } + +.wp-block-navigation__menu-inspector-controls { + overflow-x: auto; + + @include custom-scrollbars-on-hover(transparent, $gray-600); +} diff --git a/test/e2e/specs/editor/blocks/navigation.spec.js b/test/e2e/specs/editor/blocks/navigation.spec.js index e776b160d55bc8..e35346efca56b4 100644 --- a/test/e2e/specs/editor/blocks/navigation.spec.js +++ b/test/e2e/specs/editor/blocks/navigation.spec.js @@ -483,7 +483,7 @@ test.describe( 'Navigation block', () => { await expect( listView .getByRole( 'gridcell', { - name: 'Page Link link', + name: 'Page Link', } ) .filter( { hasText: 'Block 1 of 2, Level 1', // proxy for filtering by description. @@ -494,7 +494,7 @@ test.describe( 'Navigation block', () => { await expect( listView .getByRole( 'gridcell', { - name: 'Submenu link', + name: 'Submenu', } ) .filter( { hasText: 'Block 2 of 2, Level 1', // proxy for filtering by description. @@ -505,7 +505,7 @@ test.describe( 'Navigation block', () => { await expect( listView .getByRole( 'gridcell', { - name: 'Page Link link', + name: 'Page Link', } ) .filter( { hasText: 'Block 1 of 1, Level 2', // proxy for filtering by description. @@ -588,7 +588,7 @@ test.describe( 'Navigation block', () => { await expect( listView .getByRole( 'gridcell', { - name: 'Page Link link', + name: 'Page Link', } ) .filter( { hasText: 'Block 3 of 3, Level 1', // proxy for filtering by description. @@ -616,7 +616,7 @@ test.describe( 'Navigation block', () => { } ); const submenuOptions = listView.getByRole( 'button', { - name: 'Options for Submenu block', + name: 'Options for Submenu', } ); // Open the options menu. @@ -626,7 +626,7 @@ test.describe( 'Navigation block', () => { // outside of the treegrid. const removeBlockOption = page .getByRole( 'menu', { - name: 'Options for Submenu block', + name: 'Options for Submenu', } ) .getByRole( 'menuitem', { name: 'Remove Top Level Item 2', @@ -638,7 +638,7 @@ test.describe( 'Navigation block', () => { await expect( listView .getByRole( 'gridcell', { - name: 'Submenu link', + name: 'Submenu', } ) .filter( { hasText: 'Block 2 of 2, Level 1', // proxy for filtering by description. @@ -666,10 +666,12 @@ test.describe( 'Navigation block', () => { } ); // Click on the first menu item to open its settings. - const firstMenuItemAnchor = listView.getByRole( 'link', { - name: 'Top Level Item 1', - includeHidden: true, - } ); + const firstMenuItemAnchor = listView + .getByRole( 'link', { + name: 'Page', + includeHidden: true, + } ) + .getByText( 'Top Level Item 1' ); await firstMenuItemAnchor.click(); // Get the settings panel. @@ -730,7 +732,7 @@ test.describe( 'Navigation block', () => { await expect( listViewPanel .getByRole( 'gridcell', { - name: 'Page Link link', + name: 'Page Link', } ) .filter( { hasText: 'Block 1 of 2, Level 1', // proxy for filtering by description. @@ -761,7 +763,7 @@ test.describe( 'Navigation block', () => { // click on options menu for the first menu item and select remove. const firstMenuItem = listView .getByRole( 'gridcell', { - name: 'Page Link link', + name: 'Page Link', } ) .filter( { hasText: 'Block 1 of 2, Level 1', // proxy for filtering by description. @@ -771,7 +773,7 @@ test.describe( 'Navigation block', () => { const firstItemOptions = firstMenuItem .locator( '..' ) // parent selector. .getByRole( 'button', { - name: 'Options for Page Link block', + name: 'Options for Page Link', } ); // Open the options menu. @@ -782,10 +784,10 @@ test.describe( 'Navigation block', () => { // outside of the treegrid. const addSubmenuOption = page .getByRole( 'menu', { - name: 'Options for Page Link block', + name: 'Options for Page Link', } ) .getByRole( 'menuitem', { - name: 'Add submenu link', + name: 'Add submenu', } ); await addSubmenuOption.click(); @@ -798,7 +800,7 @@ test.describe( 'Navigation block', () => { await expect( listView .getByRole( 'gridcell', { - name: 'Custom Link link', + name: 'Custom Link', } ) .filter( { hasText: 'Block 1 of 1, Level 2', // proxy for filtering by description. @@ -811,7 +813,7 @@ test.describe( 'Navigation block', () => { await expect( listView .getByRole( 'gridcell', { - name: 'Submenu link', + name: 'Submenu', } ) .filter( { hasText: 'Block 1 of 2, Level 1', // proxy for filtering by description. @@ -1030,6 +1032,7 @@ test.describe( 'Navigation block - Frontend interactivity', () => { } ); const innerElement = page.getByRole( 'link', { name: 'Simple Submenu Link 1', + includeHidden: true, } ); await expect( innerElement ).toBeHidden(); await simpleSubmenuButton.click(); From e312f9c47dafc817a40a76f155ecce253bcb1675 Mon Sep 17 00:00:00 2001 From: Bart Kalisz <bartlomiej.kalisz@gmail.com> Date: Wed, 17 May 2023 16:54:01 +0200 Subject: [PATCH 069/131] Fix release performance tests (#50699) --- .github/workflows/performance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 9ced7ce0f4684e..7a5c523c8324b6 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -54,7 +54,7 @@ jobs: WP_VERSION=$(awk -F ': ' '/^Tested up to/{print $2}' readme.txt) IFS=. read -ra WP_VERSION_ARRAY <<< "$WP_VERSION" WP_MAJOR="${WP_VERSION_ARRAY[0]}.${WP_VERSION_ARRAY[1]}" - ./bin/plugin/cli.js perf "wp/$WP_MAJOR" "$PREVIOUS_RELEASE_BRANCH" "$CURRENT_RELEASE_BRANCH" --wp-version "$WP_MAJOR" + ./bin/plugin/cli.js perf "wp/$WP_MAJOR" "$PREVIOUS_RELEASE_BRANCH" "$CURRENT_RELEASE_BRANCH" --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" - name: Compare performance with base branch if: github.event_name == 'push' From 01248b8b9cac2967d6cb13d887f65b9e019bbc67 Mon Sep 17 00:00:00 2001 From: Jerry Jones <jones.jeremydavid@gmail.com> Date: Wed, 17 May 2023 10:52:18 -0500 Subject: [PATCH 070/131] Combine frontend navigation tests into fewer tests to speed up e2e tests (#50681) There is quite a bit of setup needed for each of the frontend navigation tests, and by combining them into fewer tests we can speed them up and follow the best practices of write fewer but longer tests: https://playwright.dev/docs/best-practices#write-fewer-tests-but-longer-tests * Simplify setup logic for creating navigation menu for frontend interaction tests * Combine submenu interaction tests into one test * Rename parent test to make it more clearly different from child test * Combine overlay menu interactions into one test --- test/e2e/artifacts/storage-states/admin.json | 1 + .../specs/editor/blocks/navigation.spec.js | 304 +++--------------- 2 files changed, 53 insertions(+), 252 deletions(-) create mode 100644 test/e2e/artifacts/storage-states/admin.json diff --git a/test/e2e/artifacts/storage-states/admin.json b/test/e2e/artifacts/storage-states/admin.json new file mode 100644 index 00000000000000..cd90306028e8a5 --- /dev/null +++ b/test/e2e/artifacts/storage-states/admin.json @@ -0,0 +1 @@ +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"localhost","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wordpress_23778236db82f19306f247e20a353a99","value":"admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C45404d74460259bc9148f2357f2180af488d65921b10ed5981fff860afa5c8ca","domain":"localhost","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":false,"sameSite":"Lax"},{"name":"wordpress_23778236db82f19306f247e20a353a99","value":"admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C45404d74460259bc9148f2357f2180af488d65921b10ed5981fff860afa5c8ca","domain":"localhost","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":false,"sameSite":"Lax"},{"name":"wordpress_logged_in_23778236db82f19306f247e20a353a99","value":"admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C8ace4a8f867bc4e587d5264662296f90fcd133710cd0dd3386a92801816bd5d1","domain":"localhost","path":"/","expires":-1,"httpOnly":true,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"editor%3Dtinymce","domain":"localhost","path":"/","expires":1715809558.14,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1684273558","domain":"localhost","path":"/","expires":1715809558.14,"httpOnly":false,"secure":false,"sameSite":"Lax"}],"nonce":"fe590a7aae","rootURL":"http://localhost:8889/index.php?rest_route=/"} \ No newline at end of file diff --git a/test/e2e/specs/editor/blocks/navigation.spec.js b/test/e2e/specs/editor/blocks/navigation.spec.js index e35346efca56b4..103e4bc9d57150 100644 --- a/test/e2e/specs/editor/blocks/navigation.spec.js +++ b/test/e2e/specs/editor/blocks/navigation.spec.js @@ -863,9 +863,7 @@ test.describe( 'Navigation block - Frontend interactivity', () => { await editor.saveSiteEditorEntities(); } ); - test( 'overlay menu opens on click on open menu button', async ( { - page, - } ) => { + test( 'Overlay menu interactions', async ( { page } ) => { await page.goto( '/' ); const overlayMenuFirstElement = page.getByRole( 'link', { name: 'Item 1', @@ -874,91 +872,40 @@ test.describe( 'Navigation block - Frontend interactivity', () => { name: 'Open menu', } ); - await expect( overlayMenuFirstElement ).toBeHidden(); - await openMenuButton.click(); - await expect( overlayMenuFirstElement ).toBeVisible(); - } ); - - test( 'overlay menu closes on click on close menu button', async ( { - page, - } ) => { - await page.goto( '/' ); - const overlayMenuFirstElement = page.getByRole( 'link', { - name: 'Item 1', - } ); - const openMenuButton = page.getByRole( 'button', { - name: 'Open menu', - } ); const closeMenuButton = page.getByRole( 'button', { name: 'Close menu', } ); - await expect( overlayMenuFirstElement ).toBeHidden(); - await openMenuButton.click(); - await expect( overlayMenuFirstElement ).toBeVisible(); - await closeMenuButton.click(); - await expect( overlayMenuFirstElement ).toBeHidden(); - } ); - test( 'overlay menu closes on ESC key', async ( { page } ) => { - await page.goto( '/' ); - const overlayMenuFirstElement = page.getByRole( 'link', { - name: 'Item 1', - } ); - const openMenuButton = page.getByRole( 'button', { - name: 'Open menu', - } ); + // Test: overlay menu opens on click on open menu button await expect( overlayMenuFirstElement ).toBeHidden(); - await openMenuButton.focus(); - await page.keyboard.press( 'Enter' ); + await openMenuButton.click(); await expect( overlayMenuFirstElement ).toBeVisible(); - await page.keyboard.press( 'Escape' ); - await expect( overlayMenuFirstElement ).toBeHidden(); - await expect( openMenuButton ).toBeFocused(); - } ); - test( 'overlay menu focuses on first element after opening', async ( { - page, - } ) => { - await page.goto( '/' ); - const overlayMenuFirstElement = page.getByRole( 'link', { - name: 'Item 1', - } ); - const openMenuButton = page.getByRole( 'button', { - name: 'Open menu', - } ); - await expect( overlayMenuFirstElement ).toBeHidden(); - await openMenuButton.focus(); - await page.keyboard.press( 'Enter' ); - await expect( overlayMenuFirstElement ).toBeVisible(); + // Test: overlay menu focuses on first element after opening await expect( overlayMenuFirstElement ).toBeFocused(); - } ); - test( 'overlay menu traps focus', async ( { page } ) => { - await page.goto( '/' ); - const overlayMenuFirstElement = page.getByRole( 'link', { - name: 'Item 1', - } ); - const openMenuButton = page.getByRole( 'button', { - name: 'Open menu', - } ); - const closeMenuButton = page.getByRole( 'button', { - name: 'Close menu', - } ); - await expect( overlayMenuFirstElement ).toBeHidden(); - await openMenuButton.focus(); - await page.keyboard.press( 'Enter' ); - await expect( overlayMenuFirstElement ).toBeVisible(); - await expect( overlayMenuFirstElement ).toBeFocused(); + // Test: overlay menu traps focus await page.keyboard.press( 'Tab' ); await page.keyboard.press( 'Tab' ); await expect( closeMenuButton ).toBeFocused(); await page.keyboard.press( 'Shift+Tab' ); await page.keyboard.press( 'Shift+Tab' ); await expect( overlayMenuFirstElement ).toBeFocused(); + + // Test: overlay menu closes on click on close menu button + await closeMenuButton.click(); + await expect( overlayMenuFirstElement ).toBeHidden(); + + // Test: overlay menu closes on ESC key + await openMenuButton.click(); + await expect( overlayMenuFirstElement ).toBeVisible(); + await page.keyboard.press( 'Escape' ); + await expect( overlayMenuFirstElement ).toBeHidden(); + await expect( openMenuButton ).toBeFocused(); } ); } ); - test.describe( 'Submenus (Open on click)', () => { + test.describe( 'Submenu mouse and keyboard interactions', () => { test.beforeEach( async ( { admin, editor, requestUtils } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//header', @@ -989,7 +936,7 @@ test.describe( 'Navigation block - Frontend interactivity', () => { await editor.saveSiteEditorEntities(); } ); - test( 'submenu opens on click', async ( { page } ) => { + test( 'Submenu interactions', async ( { page } ) => { await page.goto( '/' ); const simpleSubmenuButton = page.getByRole( 'button', { name: 'Simple Submenu', @@ -997,13 +944,6 @@ test.describe( 'Navigation block - Frontend interactivity', () => { const innerElement = page.getByRole( 'link', { name: 'Simple Submenu Link 1', } ); - await expect( innerElement ).toBeHidden(); - await simpleSubmenuButton.click(); - await expect( innerElement ).toBeVisible(); - } ); - - test( 'nested submenu opens on click', async ( { page } ) => { - await page.goto( '/' ); const complexSubmenuButton = page.getByRole( 'button', { name: 'Complex Submenu', } ); @@ -1016,6 +956,17 @@ test.describe( 'Navigation block - Frontend interactivity', () => { const secondLevelElement = page.getByRole( 'link', { name: 'Nested Submenu Link 1', } ); + + // Test: submenu opens on click + await expect( innerElement ).toBeHidden(); + await simpleSubmenuButton.click(); + await expect( innerElement ).toBeVisible(); + + // Test: submenu closes on click outside submenu + await page.click( 'body' ); + await expect( innerElement ).toBeHidden(); + + // Test: nested submenu opens on click await complexSubmenuButton.click(); await expect( firstLevelElement ).toBeVisible(); await expect( secondLevelElement ).toBeHidden(); @@ -1023,53 +974,23 @@ test.describe( 'Navigation block - Frontend interactivity', () => { await nestedSubmenuButton.click(); await expect( firstLevelElement ).toBeVisible(); await expect( secondLevelElement ).toBeVisible(); - } ); - test( 'submenu closes on click outside', async ( { page } ) => { - await page.goto( '/' ); - const simpleSubmenuButton = page.getByRole( 'button', { - name: 'Simple Submenu', - } ); - const innerElement = page.getByRole( 'link', { - name: 'Simple Submenu Link 1', - includeHidden: true, - } ); - await expect( innerElement ).toBeHidden(); - await simpleSubmenuButton.click(); - await expect( innerElement ).toBeVisible(); + // Test: nested submenus close on click outside submenu await page.click( 'body' ); - await expect( innerElement ).toBeHidden(); - } ); + await expect( firstLevelElement ).toBeHidden(); + await expect( secondLevelElement ).toBeHidden(); - test( 'submenu closes on ESC key', async ( { page } ) => { - await page.goto( '/' ); - const simpleSubmenuButton = page.getByRole( 'button', { - name: 'Simple Submenu', - } ); - const innerElement = page.getByRole( 'link', { - name: 'Simple Submenu Link 1', - } ); - await expect( innerElement ).toBeHidden(); + // Test: submenu opens on Enter keypress await simpleSubmenuButton.focus(); await page.keyboard.press( 'Enter' ); await expect( innerElement ).toBeVisible(); + + // Test: submenu closes on ESC key and focuses parent link await page.keyboard.press( 'Escape' ); await expect( innerElement ).toBeHidden(); await expect( simpleSubmenuButton ).toBeFocused(); - } ); - test( 'submenu closes on tab outside submenu', async ( { page } ) => { - await page.goto( '/' ); - const simpleSubmenuButton = page.getByRole( 'button', { - name: 'Simple Submenu', - } ); - const complexSubmenuButton = page.getByRole( 'button', { - name: 'Complex Submenu', - } ); - const innerElement = page.getByRole( 'link', { - name: 'Simple Submenu Link 1', - } ); - await expect( innerElement ).toBeHidden(); + // Test: submenu closes on tab outside submenu await simpleSubmenuButton.focus(); await page.keyboard.press( 'Enter' ); await expect( innerElement ).toBeVisible(); @@ -1079,80 +1000,8 @@ test.describe( 'Navigation block - Frontend interactivity', () => { await page.keyboard.press( 'Tab' ); await expect( innerElement ).toBeHidden(); await expect( complexSubmenuButton ).toBeFocused(); - } ); - - test( 'nested submenu closes on click outside', async ( { page } ) => { - await page.goto( '/' ); - const complexSubmenuButton = page.getByRole( 'button', { - name: 'Complex Submenu', - } ); - const nestedSubmenuButton = page.getByRole( 'button', { - name: 'Nested Submenu', - } ); - const firstLevelElement = page.getByRole( 'link', { - name: 'Complex Submenu Link 1', - } ); - const secondLevelElement = page.getByRole( 'link', { - name: 'Nested Submenu Link 1', - } ); - await complexSubmenuButton.click(); - await expect( firstLevelElement ).toBeVisible(); - await expect( secondLevelElement ).toBeHidden(); - - await nestedSubmenuButton.click(); - await expect( firstLevelElement ).toBeVisible(); - await expect( secondLevelElement ).toBeVisible(); - await page.click( 'body' ); - await expect( firstLevelElement ).toBeHidden(); - await expect( secondLevelElement ).toBeHidden(); - } ); - - test( 'nested submenu closes on ESC key', async ( { page } ) => { - await page.goto( '/' ); - const complexSubmenuButton = page.getByRole( 'button', { - name: 'Complex Submenu', - } ); - const nestedSubmenuButton = page.getByRole( 'button', { - name: 'Nested Submenu', - } ); - const firstLevelElement = page.getByRole( 'link', { - name: 'Complex Submenu Link 1', - } ); - const secondLevelElement = page.getByRole( 'link', { - name: 'Nested Submenu Link 1', - } ); - await complexSubmenuButton.focus(); - await page.keyboard.press( 'Enter' ); - await expect( firstLevelElement ).toBeVisible(); - await expect( secondLevelElement ).toBeHidden(); - - await nestedSubmenuButton.click(); - await expect( firstLevelElement ).toBeVisible(); - await expect( secondLevelElement ).toBeVisible(); - - await page.keyboard.press( 'Escape' ); - await expect( firstLevelElement ).toBeHidden(); - await expect( secondLevelElement ).toBeHidden(); - await expect( complexSubmenuButton ).toBeFocused(); - } ); - - test( 'only nested submenu closes on tab outside', async ( { - page, - } ) => { - await page.goto( '/' ); - const complexSubmenuButton = page.getByRole( 'button', { - name: 'Complex Submenu', - } ); - const nestedSubmenuButton = page.getByRole( 'button', { - name: 'Nested Submenu', - } ); - const firstLevelElement = page.getByRole( 'link', { - name: 'Complex Submenu Link 1', - } ); - const secondLevelElement = page.getByRole( 'link', { - name: 'Nested Submenu Link 1', - } ); + // Test: only nested submenu closes on tab outside await complexSubmenuButton.focus(); await page.keyboard.press( 'Enter' ); await expect( firstLevelElement ).toBeVisible(); @@ -1229,35 +1078,17 @@ test.describe( 'Navigation block - Frontend interactivity', () => { } ); test.describe( 'Page list block', () => { - test.beforeEach( async ( { admin, editor, page, requestUtils } ) => { - // Create parent page. - await admin.createNewPost( { - postType: 'page', + test.beforeEach( async ( { admin, editor, requestUtils } ) => { + const parentPage = await requestUtils.createPage( { title: 'Parent Page', + status: 'publish', } ); - await editor.publishPost(); - // Create subpage. - await admin.createNewPost( { - postType: 'page', + await requestUtils.createPage( { title: 'Subpage', + status: 'publish', + parent: parentPage.id, } ); - await editor.openDocumentSettingsSidebar(); - const parentPageList = page.getByLabel( 'Parent page:' ); - if ( await parentPageList.isHidden() ) { - await page - .getByRole( 'button', { - name: 'Page Attributes', - } ) - .click(); - } - await parentPageList.click(); - await page - .getByRole( 'option', { - name: 'Parent Page', - } ) - .click(); - await editor.publishPost(); await admin.visitSiteEditor( { postId: 'emptytheme//header', @@ -1265,7 +1096,7 @@ test.describe( 'Navigation block - Frontend interactivity', () => { } ); await editor.canvas.click( 'body' ); await requestUtils.createNavigationMenu( { - title: 'Hidden menu', + title: 'Page list menu', content: ` <!-- wp:page-list /--> <!-- wp:navigation-link {"label":"Link","type":"custom","url":"http://www.wordpress.org/","isTopLevelLink":true} /--> @@ -1278,7 +1109,7 @@ test.describe( 'Navigation block - Frontend interactivity', () => { await editor.saveSiteEditorEntities(); } ); - test( 'page-list submenu opens on click', async ( { page } ) => { + test( 'page-list submenu user interactions', async ( { page } ) => { await page.goto( '/' ); const submenuButton = page.getByRole( 'button', { name: 'Parent Page', @@ -1287,58 +1118,27 @@ test.describe( 'Navigation block - Frontend interactivity', () => { name: 'Subpage', } ); await expect( innerElement ).toBeHidden(); - await submenuButton.click(); - await expect( innerElement ).toBeVisible(); - } ); - test( 'page-list submenu closes on click outside', async ( { - page, - } ) => { - await page.goto( '/' ); - const submenuButton = page.getByRole( 'button', { - name: 'Parent Page', - } ); - const innerElement = page.getByRole( 'link', { - name: 'Subpage', - } ); - await expect( innerElement ).toBeHidden(); + // page-list submenu opens on click await submenuButton.click(); await expect( innerElement ).toBeVisible(); + + // page-list submenu closes on click outside await page.click( 'body' ); await expect( innerElement ).toBeHidden(); - } ); - test( 'page-list submenu closes on ESC key', async ( { page } ) => { - await page.goto( '/' ); - const submenuButton = page.getByRole( 'button', { - name: 'Parent Page', - } ); - const innerElement = page.getByRole( 'link', { - name: 'Subpage', - } ); - await expect( innerElement ).toBeHidden(); + // page-list submenu opens on enter keypress await submenuButton.focus(); await page.keyboard.press( 'Enter' ); await expect( innerElement ).toBeVisible(); + + // page-list submenu closes on ESC key and focuses submenu button await page.keyboard.press( 'Escape' ); await expect( innerElement ).toBeHidden(); await expect( submenuButton ).toBeFocused(); - } ); - test( 'page-list submenu closes on tab outside submenu', async ( { - page, - } ) => { - await page.goto( '/' ); - const submenuButton = page.getByRole( 'button', { - name: 'Parent Page', - } ); - const innerElement = page.getByRole( 'link', { - name: 'Subpage', - } ); - await expect( innerElement ).toBeHidden(); - await submenuButton.focus(); + // page-list submenu closes on tab outside submenu await page.keyboard.press( 'Enter' ); - await expect( innerElement ).toBeVisible(); // Tab to first element. await page.keyboard.press( 'Tab' ); // Tab outside the submenu. From 8d9752ed5a3457ee4fd584c4a20b1823db2cbb54 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Wed, 17 May 2023 19:09:03 +0300 Subject: [PATCH 071/131] Add block variations transformation in block switcher (#50139) * Add block variations transformation in block switcher * move variation transforms to the top of the list * fix error on multi select * add e2e test * Update test/e2e/specs/editor/various/block-switcher-test.spec.js Co-authored-by: Kai Hao <kevin830726@gmail.com> * update e2e test * Update test/e2e/specs/editor/various/block-switcher-test.spec.js Co-authored-by: Kai Hao <kevin830726@gmail.com> --------- Co-authored-by: Kai Hao <kevin830726@gmail.com> --- .../block-transformations-menu.js | 12 ++ .../block-variation-transformations.js | 115 ++++++++++++++++++ .../src/components/block-switcher/index.js | 43 ++++++- .../various/block-switcher-test.spec.js | 60 +++++++++ 4 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 packages/block-editor/src/components/block-switcher/block-variation-transformations.js create mode 100644 test/e2e/specs/editor/various/block-switcher-test.spec.js diff --git a/packages/block-editor/src/components/block-switcher/block-transformations-menu.js b/packages/block-editor/src/components/block-switcher/block-transformations-menu.js index 6b88fddb1163fa..033201d7facadb 100644 --- a/packages/block-editor/src/components/block-switcher/block-transformations-menu.js +++ b/packages/block-editor/src/components/block-switcher/block-transformations-menu.js @@ -14,6 +14,7 @@ import { useState, useMemo } from '@wordpress/element'; */ import BlockIcon from '../block-icon'; import PreviewBlockPopover from './preview-block-popover'; +import BlockVariationTransformations from './block-variation-transformations'; /** * Helper hook to group transformations to display them in a specific order in the UI. @@ -65,7 +66,9 @@ function useGroupedTransforms( possibleBlockTransformations ) { const BlockTransformationsMenu = ( { className, possibleBlockTransformations, + possibleBlockVariationTransformations, onSelect, + onSelectVariation, blocks, } ) => { const [ hoveredTransformItemName, setHoveredTransformItemName ] = @@ -95,6 +98,15 @@ const BlockTransformationsMenu = ( { ) } /> ) } + { !! possibleBlockVariationTransformations?.length && ( + <BlockVariationTransformations + transformations={ + possibleBlockVariationTransformations + } + blocks={ blocks } + onSelect={ onSelectVariation } + /> + ) } { priorityTextTransformations.map( ( item ) => ( <BlockTranformationItem key={ item.name } diff --git a/packages/block-editor/src/components/block-switcher/block-variation-transformations.js b/packages/block-editor/src/components/block-switcher/block-variation-transformations.js new file mode 100644 index 00000000000000..c2ec449dc61f94 --- /dev/null +++ b/packages/block-editor/src/components/block-switcher/block-variation-transformations.js @@ -0,0 +1,115 @@ +/** + * WordPress dependencies + */ +import { MenuItem } from '@wordpress/components'; +import { + getBlockMenuDefaultClassName, + cloneBlock, + store as blocksStore, +} from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; +import { useState, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import BlockIcon from '../block-icon'; +import PreviewBlockPopover from './preview-block-popover'; + +const EMPTY_OBJECT = {}; + +export function useBlockVariationTransforms( { clientIds, blocks } ) { + const { activeBlockVariation, blockVariationTransformations } = useSelect( + ( select ) => { + const { + getBlockRootClientId, + getBlockAttributes, + canRemoveBlocks, + } = select( blockEditorStore ); + const { getActiveBlockVariation, getBlockVariations } = + select( blocksStore ); + const rootClientId = getBlockRootClientId( + Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds + ); + const canRemove = canRemoveBlocks( clientIds, rootClientId ); + // Only handle single selected blocks for now. + if ( blocks.length !== 1 || ! canRemove ) { + return EMPTY_OBJECT; + } + const [ firstBlock ] = blocks; + return { + blockVariationTransformations: getBlockVariations( + firstBlock.name, + 'transform' + ), + activeBlockVariation: getActiveBlockVariation( + firstBlock.name, + getBlockAttributes( firstBlock.clientId ) + ), + }; + }, + [ clientIds, blocks ] + ); + const transformations = useMemo( () => { + return blockVariationTransformations?.filter( + ( { name } ) => name !== activeBlockVariation?.name + ); + }, [ blockVariationTransformations, activeBlockVariation ] ); + return transformations; +} + +const BlockVariationTransformations = ( { + transformations, + onSelect, + blocks, +} ) => { + const [ hoveredTransformItemName, setHoveredTransformItemName ] = + useState(); + return ( + <> + { hoveredTransformItemName && ( + <PreviewBlockPopover + blocks={ cloneBlock( + blocks[ 0 ], + transformations.find( + ( { name } ) => name === hoveredTransformItemName + ).attributes + ) } + /> + ) } + { transformations?.map( ( item ) => ( + <BlockVariationTranformationItem + key={ item.name } + item={ item } + onSelect={ onSelect } + setHoveredTransformItemName={ setHoveredTransformItemName } + /> + ) ) } + </> + ); +}; + +function BlockVariationTranformationItem( { + item, + onSelect, + setHoveredTransformItemName, +} ) { + const { name, icon, title } = item; + return ( + <MenuItem + className={ getBlockMenuDefaultClassName( name ) } + onClick={ ( event ) => { + event.preventDefault(); + onSelect( name ); + } } + onMouseLeave={ () => setHoveredTransformItemName( null ) } + onMouseEnter={ () => setHoveredTransformItemName( name ) } + > + <BlockIcon icon={ icon } showColors /> + { title } + </MenuItem> + ); +} + +export default BlockVariationTransformations; diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 696b097757d335..a4c15a9e17062a 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -24,12 +24,14 @@ import { store as blockEditorStore } from '../../store'; import useBlockDisplayInformation from '../use-block-display-information'; import BlockIcon from '../block-icon'; import BlockTransformationsMenu from './block-transformations-menu'; +import { useBlockVariationTransforms } from './block-variation-transformations'; import BlockStylesMenu from './block-styles-menu'; import PatternTransformationsMenu from './pattern-transformations-menu'; import useBlockDisplayTitle from '../block-title/use-block-display-title'; export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { - const { replaceBlocks, multiSelect } = useDispatch( blockEditorStore ); + const { replaceBlocks, multiSelect, updateBlockAttributes } = + useDispatch( blockEditorStore ); const blockInformation = useBlockDisplayInformation( blocks[ 0 ].clientId ); const { possibleBlockTransformations, @@ -43,9 +45,9 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { getBlockRootClientId, getBlockTransformItems, __experimentalGetPatternTransformItems, + canRemoveBlocks, } = select( blockEditorStore ); const { getBlockStyles, getBlockType } = select( blocksStore ); - const { canRemoveBlocks } = select( blockEditorStore ); const rootClientId = getBlockRootClientId( Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds ); @@ -82,6 +84,11 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { [ clientIds, blocks, blockInformation?.icon ] ); + const blockVariationTransformations = useBlockVariationTransforms( { + clientIds, + blocks, + } ); + const blockTitle = useBlockDisplayTitle( { clientId: Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds, maximumLength: 35, @@ -105,6 +112,14 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { selectForMultipleBlocks( newBlocks ); } + function onBlockVariationTransform( name ) { + updateBlockAttributes( blocks[ 0 ].clientId, { + ...blockVariationTransformations.find( + ( { name: variationName } ) => variationName === name + ).attributes, + } ); + } + // Pattern transformation through the `Patterns` API. function onPatternTransform( transformedBlocks ) { replaceBlocks( clientIds, transformedBlocks ); @@ -118,8 +133,14 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { */ const hasPossibleBlockTransformations = !! possibleBlockTransformations.length && canRemove && ! isTemplate; + const hasPossibleBlockVariationTransformations = + !! blockVariationTransformations?.length; const hasPatternTransformation = !! patterns?.length && canRemove; - if ( ! hasBlockStyles && ! hasPossibleBlockTransformations ) { + if ( + ! hasBlockStyles && + ! hasPossibleBlockTransformations && + ! hasPossibleBlockVariationTransformations + ) { return ( <ToolbarGroup> <ToolbarButton @@ -160,9 +181,12 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { blocks.length ); + const hasBlockOrBlockVariationTransforms = + hasPossibleBlockTransformations || + hasPossibleBlockVariationTransformations; const showDropDown = hasBlockStyles || - hasPossibleBlockTransformations || + hasBlockOrBlockVariationTransforms || hasPatternTransformation; return ( <ToolbarGroup> @@ -213,17 +237,26 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { } } /> ) } - { hasPossibleBlockTransformations && ( + { hasBlockOrBlockVariationTransforms && ( <BlockTransformationsMenu className="block-editor-block-switcher__transforms__menugroup" possibleBlockTransformations={ possibleBlockTransformations } + possibleBlockVariationTransformations={ + blockVariationTransformations + } blocks={ blocks } onSelect={ ( name ) => { onBlockTransform( name ); onClose(); } } + onSelectVariation={ ( name ) => { + onBlockVariationTransform( + name + ); + onClose(); + } } /> ) } { hasBlockStyles && ( diff --git a/test/e2e/specs/editor/various/block-switcher-test.spec.js b/test/e2e/specs/editor/various/block-switcher-test.spec.js new file mode 100644 index 00000000000000..12fd843ed2ed12 --- /dev/null +++ b/test/e2e/specs/editor/various/block-switcher-test.spec.js @@ -0,0 +1,60 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Block Switcher', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'Block variation transforms', async ( { editor, page } ) => { + // This is the `stack` Group variation. + await editor.insertBlock( { + name: 'core/group', + attributes: { + layout: { + type: 'flex', + orientation: 'vertical', + }, + }, + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: '1' }, + }, + ], + } ); + // Transform to `Stack` variation. + await editor.clickBlockToolbarButton( 'Stack' ); + const variations = page + .getByRole( 'menu', { name: 'Stack' } ) + .getByRole( 'group' ); + await expect( + variations.getByRole( 'menuitem', { name: 'Stack' } ) + ).toBeHidden(); + await variations.getByRole( 'menuitem', { name: 'Row' } ).click(); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/group', + attributes: expect.objectContaining( { + layout: { + type: 'flex', + flexWrap: 'nowrap', + orientation: undefined, + }, + } ), + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: '1' }, + }, + ], + }, + ] ); + await editor.clickBlockToolbarButton( 'Row' ); + await expect( + page.locator( 'role=menuitem[name="Stack"i]' ) + ).toBeVisible(); + } ); +} ); From 6caf489e5baecd5e8cda627236f5bd71bce5c82a Mon Sep 17 00:00:00 2001 From: Jerry Jones <jones.jeremydavid@gmail.com> Date: Wed, 17 May 2023 11:55:28 -0500 Subject: [PATCH 072/131] Respect showAppender when there are no items in list view (#50711) --- packages/block-editor/src/components/list-view/index.js | 4 ++-- .../src/navigation/edit/menu-inspector-controls.js | 5 +++++ packages/block-library/src/navigation/editor.scss | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 5e04a9cf9e1742..60c08abaf90895 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -224,8 +224,8 @@ function ListViewComponent( ] ); - // If there are no blocks to show, do not render the list view. - if ( ! clientIdsTree.length ) { + // If there are no blocks to show and we're not showing the appender, do not render the list view. + if ( ! clientIdsTree.length && ! showAppender ) { return null; } diff --git a/packages/block-library/src/navigation/edit/menu-inspector-controls.js b/packages/block-library/src/navigation/edit/menu-inspector-controls.js index 1bd3e51063ec98..d4c9506c51d8c0 100644 --- a/packages/block-library/src/navigation/edit/menu-inspector-controls.js +++ b/packages/block-library/src/navigation/edit/menu-inspector-controls.js @@ -133,6 +133,11 @@ const MainContent = ( { return ( <div className="wp-block-navigation__menu-inspector-controls"> + { clientIdsTree.length === 0 && ( + <p className="wp-block-navigation__menu-inspector-controls__empty-message"> + { __( 'This navigation menu is empty.' ) } + </p> + ) } <PrivateListView blocks={ clientIdsTree } rootClientId={ clientId } diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index 1ba36e084da2ae..949a7c773eb6e6 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -663,3 +663,7 @@ body.editor-styles-wrapper .wp-block-navigation__responsive-container.is-menu-op @include custom-scrollbars-on-hover(transparent, $gray-600); } + +.wp-block-navigation__menu-inspector-controls__empty-message { + margin-left: 24px; +} From 5750a77b028eb0c60fe5b3d2da6b7f7133bf37bf Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Wed, 17 May 2023 17:31:47 +0000 Subject: [PATCH 073/131] Bump plugin version to 15.8.0 --- gutenberg.php | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index ce72c8eb81e4c6..612a4325fd2bc0 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.1 * Requires PHP: 5.6 - * Version: 15.8.0-rc.1 + * Version: 15.8.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index a96e865acae9c2..6849e395790b62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "15.8.0-rc.1", + "version": "15.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a1122bfda1142f..4d4729eff61741 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "15.8.0-rc.1", + "version": "15.8.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From 5e1a0a8deadd28b747bffdddb4b576bfbac72829 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Wed, 17 May 2023 17:44:54 +0000 Subject: [PATCH 074/131] Update Changelog for 15.8.0 --- changelog.txt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index c0eb6f61a62ae2..dee5a1671cf931 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,11 @@ == Changelog == -= 15.8.0-rc.1 = += 15.8.0 = + +## Contributors + +The following contributors merged PRs in this release: + ## Changelog @@ -103,6 +108,8 @@ - Fix/wp get global styles for custom props returns internal variable. ([50366](https://github.com/WordPress/gutenberg/pull/50366)) - Revisions controller: Fix author and date fields. ([50117](https://github.com/WordPress/gutenberg/pull/50117)) - Fix hover/focus styles for `style variation` buttons. ([50056](https://github.com/WordPress/gutenberg/pull/50056)) +- Use bundled files for enqueuing global styles. ([50310](https://github.com/WordPress/gutenberg/pull/50310)) + #### Block Editor - Fix issue with margin collapsing when selecting blocks. ([50215](https://github.com/WordPress/gutenberg/pull/50215)) @@ -158,6 +165,7 @@ #### Interactivity API - Add Interactivity API runtime. ([49994](https://github.com/WordPress/gutenberg/pull/49994)) - Navigation block with the Interactivity API. ([50041](https://github.com/WordPress/gutenberg/pull/50041)) +- Fix building plugin zip to include interactive blocks. ([50598](https://github.com/WordPress/gutenberg/pull/50598)) #### Command Center - Update the experiment label. ([50467](https://github.com/WordPress/gutenberg/pull/50467)) @@ -298,6 +306,8 @@ The following contributors merged PRs in this release: @aaronrobertshaw @afercia @ajlende @alexstine @andrewserong @apeatling @artemiomorales @aurooba @bph @chad1008 @ciampo @DAreRodz @dcalhoun @draganescu @ecgan @fluiddot @fullofcaffeine @geriux @getdave @glendaviesnz @gziolo @hellofromtonya @ironprogrammer @jameskoster @jasmussen @jeryj @jhnstn @johnhooks @jsnajdr @juanfra @kevin940726 @kienstra @Mamaduka @margolisj @mburridge @mirka @mokagio @mtias @n2erjo00 @ndiego @noahtallen @noisysocks @ntsekouras @oandregal @ObliviousHarmony @ocean90 @ockham @pavanpatil1 @pooja-muchandikar @priethor @ramonjd @richtabor @samnajian @SantosGuillamot @scruffian @SiobhyB @t-hamano @talldan @tellthemachines @torounit @tyxla @westonruter @youknowriad + + = 15.7.1 = From 2922d6b3e109febc6c5e8fb54ba5d7d4fafdcec0 Mon Sep 17 00:00:00 2001 From: Rich Tabor <hi@richtabor.com> Date: Wed, 17 May 2023 14:08:05 -0400 Subject: [PATCH 075/131] Add wide align support to code block (#50710) --- docs/reference-guides/core-blocks.md | 2 +- packages/block-library/src/code/block.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 04710602d28f0b..6f7314d5e51880 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -86,7 +86,7 @@ Display code snippets that respect your spacing and tabs. ([Source](https://gith - **Name:** core/code - **Category:** text -- **Supports:** anchor, color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight) +- **Supports:** align (wide), anchor, color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** content ## Column diff --git a/packages/block-library/src/code/block.json b/packages/block-library/src/code/block.json index 69278002295695..660a2faafaf92c 100644 --- a/packages/block-library/src/code/block.json +++ b/packages/block-library/src/code/block.json @@ -14,6 +14,7 @@ } }, "supports": { + "align": [ "wide" ], "anchor": true, "typography": { "fontSize": true, From 91dbcff433686d3b059fd727c27a8287504220f3 Mon Sep 17 00:00:00 2001 From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com> Date: Wed, 17 May 2023 11:58:44 -0700 Subject: [PATCH 076/131] Added Unknown Config Option Detection (#50642) --- packages/env/CHANGELOG.md | 4 + packages/env/lib/config/parse-config.js | 114 +++++++++++++++---- packages/env/lib/config/test/parse-config.js | 61 ++++++++++ 3 files changed, 154 insertions(+), 25 deletions(-) diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index a3c57368359f59..cfdbf35f870a34 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancement + +- Validate whether or not config options exist to prevent accidentally including ones that don't. + ## 7.0.0 (2023-05-10) ### Breaking Change diff --git a/packages/env/lib/config/parse-config.js b/packages/env/lib/config/parse-config.js index aa39d94ebfd9f8..1181f2f9e6bb3c 100644 --- a/packages/env/lib/config/parse-config.js +++ b/packages/env/lib/config/parse-config.js @@ -34,9 +34,10 @@ const mergeConfigs = require( './merge-configs' ); * The root configuration options. * * @typedef WPRootConfigOptions - * @property {number} port The port to use in the development environment. - * @property {number} testsPort The port to use in the tests environment. - * @property {string|null} afterSetup The command(s) to run after configuring WordPress on start and clean. + * @property {number} port The port to use in the development environment. + * @property {number} testsPort The port to use in the tests environment. + * @property {string|null} afterSetup The command(s) to run after configuring WordPress on start and clean. + * @property {Object.<string, WPEnvironmentConfig>} env The environment-specific configuration options. */ /** @@ -69,6 +70,33 @@ const mergeConfigs = require( './merge-configs' ); * @property {string} basename Name that identifies the WordPress installation, plugin or theme. */ +/** + * An object containing all of the default configuration options for environment-specific configurations. + * Unless otherwise set at the root-level or the environment-level, these are the values that will be + * parsed into the environment. This is useful for tracking known configuration options since these + * are the only configuration options that can be set in each environment. + */ +const DEFAULT_ENVIRONMENT_CONFIG = { + core: null, + phpVersion: null, + plugins: [], + themes: [], + port: 8888, + testsPort: 8889, + mappings: {}, + config: { + WP_DEBUG: true, + SCRIPT_DEBUG: true, + WP_ENVIRONMENT_TYPE: 'local', + WP_PHP_BINARY: 'php', + WP_TESTS_EMAIL: 'admin@example.org', + WP_TESTS_TITLE: 'Test Blog', + WP_TESTS_DOMAIN: 'localhost', + WP_SITEURL: 'http://localhost', + WP_HOME: 'http://localhost', + }, +}; + /** * Given a directory, this parses any relevant config files and * constructs an object in the format used internally. @@ -164,9 +192,7 @@ async function getDefaultConfig( configDirectoryPath, { shouldInferType, cacheDirectoryPath } ) { - // Our default config should try to infer what type of project - // this is in order to automatically map the current directory. - const detectedType = shouldInferType + const detectedDirectoryType = shouldInferType ? await detectDirectoryType( configDirectoryPath ) : null; @@ -175,24 +201,28 @@ async function getDefaultConfig( // config objects easier because once merged we don't need to // verify that a given option exists before using it. const rawConfig = { - core: detectedType === 'core' ? '.' : null, - phpVersion: null, - plugins: detectedType === 'plugin' ? [ '.' ] : [], - themes: detectedType === 'theme' ? [ '.' ] : [], - port: 8888, - testsPort: 8889, - mappings: {}, - config: { - WP_DEBUG: true, - SCRIPT_DEBUG: true, - WP_ENVIRONMENT_TYPE: 'local', - WP_PHP_BINARY: 'php', - WP_TESTS_EMAIL: 'admin@example.org', - WP_TESTS_TITLE: 'Test Blog', - WP_TESTS_DOMAIN: 'localhost', - WP_SITEURL: 'http://localhost', - WP_HOME: 'http://localhost', - }, + // Since the root config is the base "environment" config for + // all environments, we will start with those defaults. + ...DEFAULT_ENVIRONMENT_CONFIG, + + // When the current directory has no configuration file we support a zero-config mode of operation. + // This works by using the default options and inferring how to map the current directory based + // on the contents of the directory. + core: + detectedDirectoryType === 'core' + ? '.' + : DEFAULT_ENVIRONMENT_CONFIG.core, + plugins: + detectedDirectoryType === 'plugin' + ? [ '.' ] + : DEFAULT_ENVIRONMENT_CONFIG.plugins, + themes: + detectedDirectoryType === 'theme' + ? [ '.' ] + : DEFAULT_ENVIRONMENT_CONFIG.themes, + + // These configuration options are root-only and should not be present + // on environment-specific configuration objects. afterSetup: null, env: { development: {}, @@ -292,7 +322,10 @@ async function parseRootConfig( configFile, rawConfig, options ) { configFile, null, rawConfig, - options + { + ...options, + rootConfig: true, + } ); // Parse any root-only options. @@ -333,6 +366,7 @@ async function parseRootConfig( configFile, rawConfig, options ) { * @param {Object} config A config object to parse. * @param {Object} options * @param {string} options.cacheDirectoryPath Path to the work directory located in ~/.wp-env. + * @param {boolean} options.rootConfig Indicates whether or not this is the root config object. * * @return {Promise<WPEnvironmentConfig>} The environment config object. */ @@ -348,6 +382,36 @@ async function parseEnvironmentConfig( const environmentPrefix = environment ? environment + '.' : ''; + // Before we move forward with parsing we should make sure that there aren't any + // configuration options that do not exist. This helps prevent silent failures + // when a user sets up their configuration incorrectly. + for ( const key in config ) { + if ( DEFAULT_ENVIRONMENT_CONFIG[ key ] !== undefined ) { + continue; + } + + // We should also check root-only options for the root config + // because these aren't part of the above defaults but are + // configuration options that we will parse. + switch ( key ) { + case 'testsPort': + case 'afterSetup': + case 'env': { + if ( options.rootConfig ) { + continue; + } + + break; + } + } + + throw new ValidationError( + `Invalid ${ configFile }: "${ environmentPrefix }${ key }" is not a configuration option.` + ); + } + + // Parse each option individually so that we can handle the validation + // and any conversion that is required to use the option. const parsedConfig = {}; if ( config.port !== undefined ) { diff --git a/packages/env/lib/config/test/parse-config.js b/packages/env/lib/config/test/parse-config.js index ce5e28809ecd23..7edb5a75d6f7c1 100644 --- a/packages/env/lib/config/test/parse-config.js +++ b/packages/env/lib/config/test/parse-config.js @@ -338,5 +338,66 @@ describe( 'parseConfig', () => { ); } } ); + + it( 'throws for unknown config options', async () => { + const configFileLocation = path.resolve( './.wp-env.json' ); + readRawConfigFile.mockImplementation( async ( configFile ) => { + if ( configFile === path.resolve( './.wp-env.json' ) ) { + return { + test: 'test', + }; + } + + if ( configFile === path.resolve( './.wp-env.override.json' ) ) { + return {}; + } + + throw new Error( 'Invalid File: ' + configFile ); + } ); + + expect.assertions( 1 ); + try { + await parseConfig( './', '/cache' ); + } catch ( error ) { + expect( error ).toEqual( + new ValidationError( + `Invalid ${ configFileLocation }: "test" is not a configuration option.` + ) + ); + } + } ); + + it( 'throws for root-only config options', async () => { + const configFileLocation = path.resolve( './.wp-env.json' ); + readRawConfigFile.mockImplementation( async ( configFile ) => { + if ( configFile === configFileLocation ) { + return { + env: { + development: { + // Only the root can have environment-specific configurations. + env: {}, + }, + }, + }; + } + + if ( configFile === path.resolve( './.wp-env.override.json' ) ) { + return {}; + } + + throw new Error( 'Invalid File: ' + configFile ); + } ); + + expect.assertions( 1 ); + try { + await parseConfig( './', '/cache' ); + } catch ( error ) { + expect( error ).toEqual( + new ValidationError( + `Invalid ${ configFileLocation }: "development.env" is not a configuration option.` + ) + ); + } + } ); } ); /* eslint-enable jest/no-conditional-expect */ From 6bfafd02ca027440a46a3a04ee5c84b6c8eca670 Mon Sep 17 00:00:00 2001 From: Alex Lende <alex@lende.xyz> Date: Wed, 17 May 2023 12:59:22 -0600 Subject: [PATCH 077/131] Fix custom duotone filters in frontend (#50678) --- lib/experimental/kses.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/experimental/kses.php b/lib/experimental/kses.php index 1138aa67933ef0..27f087ad05a985 100644 --- a/lib/experimental/kses.php +++ b/lib/experimental/kses.php @@ -78,7 +78,7 @@ function gutenberg_override_core_kses_init_filters() { */ function allow_filter_in_styles( $allow_css, $css_test_string ) { if ( preg_match( - "/^filter:\s*url\('#wp-duotone-[-a-zA-Z0-9]+'\) !important$/", + "/^filter:\s*url\((['\"]?)#wp-duotone-[-a-zA-Z0-9]+\\1\)(\s+!important)?$/", $css_test_string ) ) { return true; From c34280bbf0593f4bf0bc24b210bfe42afb61e119 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Thu, 18 May 2023 09:26:47 +1000 Subject: [PATCH 078/131] Styles Navigation Screen: Add Style Book (#50566) * Styles Navigation Screen: Add button to open Style Book * Remove state and useEffect, use async / await instead * Try exposing Style Book in browse mode * Try hiding Style Book tabs, add keyboard behaviour * Revert background color removal * Clear the editor canvas container view when accessing the main navigation screen * Fix enableResizing, move prevent default to the Style Book component for simplicity * Tidy code a little Co-authored-by: ramon <ramonjd@gmail.com> --------- Co-authored-by: ramon <ramonjd@gmail.com> --- .../index.js | 90 +++++++-- .../sidebar-navigation-screen-main/index.js | 21 ++- .../src/components/style-book/index.js | 174 +++++++++++++----- .../src/components/style-book/style.scss | 19 ++ 4 files changed, 239 insertions(+), 65 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js index 06e486739ed74a..a7b8add9dd54fe 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js @@ -2,10 +2,11 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { edit } from '@wordpress/icons'; +import { edit, seen } from '@wordpress/icons'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { __experimentalNavigatorButton as NavigatorButton } from '@wordpress/components'; +import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -16,6 +17,7 @@ import { unlock } from '../../private-apis'; import { store as editSiteStore } from '../../store'; import SidebarButton from '../sidebar-button'; import SidebarNavigationItem from '../sidebar-navigation-item'; +import StyleBook from '../style-book'; export function SidebarNavigationItemGlobalStyles( props ) { const { openGeneralSidebar } = useDispatch( editSiteStore ); @@ -51,26 +53,74 @@ export function SidebarNavigationItemGlobalStyles( props ) { export default function SidebarNavigationScreenGlobalStyles() { const { openGeneralSidebar } = useDispatch( editSiteStore ); - const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); + const isMobileViewport = useViewportMatch( 'medium', '<' ); + const { setCanvasMode, setEditorCanvasContainerView } = unlock( + useDispatch( editSiteStore ) + ); + + const isStyleBookOpened = useSelect( + ( select ) => + 'style-book' === + unlock( select( editSiteStore ) ).getEditorCanvasContainerView(), + [] + ); + + const openGlobalStyles = async () => + Promise.all( [ + setCanvasMode( 'edit' ), + openGeneralSidebar( 'edit-site/global-styles' ), + ] ); + + const openStyleBook = async () => { + await openGlobalStyles(); + // Open the Style Book once the canvas mode is set to edit, + // and the global styles sidebar is open. This ensures that + // the Style Book is not prematurely closed. + setEditorCanvasContainerView( 'style-book' ); + }; + return ( - <SidebarNavigationScreen - title={ __( 'Styles' ) } - description={ __( - 'Choose a different style combination for the theme styles.' - ) } - content={ <StyleVariationsContainer /> } - actions={ - <SidebarButton - icon={ edit } - label={ __( 'Edit styles' ) } - onClick={ () => { - // switch to edit mode. - setCanvasMode( 'edit' ); - // open global styles sidebar. - openGeneralSidebar( 'edit-site/global-styles' ); - } } + <> + <SidebarNavigationScreen + title={ __( 'Styles' ) } + description={ __( + 'Choose a different style combination for the theme styles.' + ) } + content={ <StyleVariationsContainer /> } + actions={ + <div> + { ! isMobileViewport && ( + <SidebarButton + icon={ seen } + label={ __( 'Style Book' ) } + onClick={ () => + setEditorCanvasContainerView( + ! isStyleBookOpened + ? 'style-book' + : undefined + ) + } + isPressed={ isStyleBookOpened } + /> + ) } + <SidebarButton + icon={ edit } + label={ __( 'Edit styles' ) } + onClick={ async () => await openGlobalStyles() } + /> + </div> + } + /> + { isStyleBookOpened && ! isMobileViewport && ( + <StyleBook + enableResizing={ false } + isSelected={ () => false } + onClick={ openStyleBook } + onSelect={ openStyleBook } + showCloseButton={ false } + showTabs={ false } /> - } - /> + ) } + </> ); } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js index 7c9e4b3bf91968..7ad0dc07ae0f0e 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js @@ -7,8 +7,9 @@ import { } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { layout, symbol, navigation, styles, page } from '@wordpress/icons'; -import { useSelect } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; +import { useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -16,6 +17,8 @@ import { store as coreStore } from '@wordpress/core-data'; import SidebarNavigationScreen from '../sidebar-navigation-screen'; import SidebarNavigationItem from '../sidebar-navigation-item'; import { SidebarNavigationItemGlobalStyles } from '../sidebar-navigation-screen-global-styles'; +import { unlock } from '../../private-apis'; +import { store as editSiteStore } from '../../store'; export default function SidebarNavigationScreenMain() { const hasNavigationMenus = useSelect( ( select ) => { @@ -36,6 +39,22 @@ export default function SidebarNavigationScreenMain() { const showNavigationScreen = process.env.IS_GUTENBERG_PLUGIN ? hasNavigationMenus : false; + + const editorCanvasContainerView = useSelect( ( select ) => { + return unlock( select( editSiteStore ) ).getEditorCanvasContainerView(); + }, [] ); + + const { setEditorCanvasContainerView } = unlock( + useDispatch( editSiteStore ) + ); + + // Clear the editor canvas container view when accessing the main navigation screen. + useEffect( () => { + if ( editorCanvasContainerView ) { + setEditorCanvasContainerView( undefined ); + } + }, [ editorCanvasContainerView, setEditorCanvasContainerView ] ); + return ( <SidebarNavigationScreen isRoot diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index 72e8abad629eb1..c86d6df65045a2 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -13,6 +13,7 @@ import { Disabled, TabPanel, } from '@wordpress/components'; + import { __, sprintf } from '@wordpress/i18n'; import { getCategories, @@ -29,7 +30,8 @@ import { } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; import { useResizeObserver } from '@wordpress/compose'; -import { useMemo, memo } from '@wordpress/element'; +import { useMemo, useState, memo } from '@wordpress/element'; +import { ENTER, SPACE } from '@wordpress/keycodes'; /** * Internal dependencies @@ -161,7 +163,14 @@ function getExamples() { return [ headingsExample, ...otherExamples ]; } -function StyleBook( { isSelected, onSelect } ) { +function StyleBook( { + enableResizing = true, + isSelected, + onClick, + onSelect, + showCloseButton = true, + showTabs = true, +} ) { const [ resizeObserver, sizes ] = useResizeObserver(); const [ textColor ] = useGlobalStyle( 'color.text' ); const [ backgroundColor ] = useGlobalStyle( 'color.background' ); @@ -193,12 +202,15 @@ function StyleBook( { isSelected, onSelect } ) { return ( <EditorCanvasContainer - enableResizing={ true } - closeButtonLabel={ __( 'Close Style Book' ) } + enableResizing={ enableResizing } + closeButtonLabel={ + showCloseButton ? __( 'Close Style Book' ) : null + } > <div className={ classnames( 'edit-site-style-book', { 'is-wide': sizes.width > 600, + 'is-button': !! onClick, } ) } style={ { color: textColor, @@ -206,53 +218,125 @@ function StyleBook( { isSelected, onSelect } ) { } } > { resizeObserver } - <TabPanel - className="edit-site-style-book__tab-panel" - tabs={ tabs } - > - { ( tab ) => ( - <Iframe - className="edit-site-style-book__iframe" - name="style-book-canvas" - tabIndex={ 0 } - > - <EditorStyles styles={ settings.styles } /> - <style> - { - // Forming a "block formatting context" to prevent margin collapsing. - // @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context - `.is-root-container { display: flow-root; } - body { position: relative; padding: 32px !important; }` + - STYLE_BOOK_IFRAME_STYLES - } - </style> - <Examples - className={ classnames( - 'edit-site-style-book__examples', - { - 'is-wide': sizes.width > 600, - } - ) } - examples={ examples } + { showTabs ? ( + <TabPanel + className="edit-site-style-book__tab-panel" + tabs={ tabs } + > + { ( tab ) => ( + <StyleBookBody category={ tab.name } - label={ sprintf( - // translators: %s: Category of blocks, e.g. Text. - __( - 'Examples of blocks in the %s category' - ), - tab.title - ) } + examples={ examples } isSelected={ isSelected } onSelect={ onSelect } + settings={ settings } + sizes={ sizes } + title={ tab.title } /> - </Iframe> - ) } - </TabPanel> + ) } + </TabPanel> + ) : ( + <StyleBookBody + examples={ examples } + isSelected={ isSelected } + onClick={ onClick } + onSelect={ onSelect } + settings={ settings } + sizes={ sizes } + /> + ) } </div> </EditorCanvasContainer> ); } +const StyleBookBody = ( { + category, + examples, + isSelected, + onClick, + onSelect, + settings, + sizes, + title, +} ) => { + const [ isFocused, setIsFocused ] = useState( false ); + + // The presence of an `onClick` prop indicates that the Style Book is being used as a button. + // In this case, add additional props to the iframe to make it behave like a button. + const buttonModeProps = { + role: 'button', + onFocus: () => setIsFocused( true ), + onBlur: () => setIsFocused( false ), + onKeyDown: ( event ) => { + if ( event.defaultPrevented ) { + return; + } + const { keyCode } = event; + if ( onClick && ( keyCode === ENTER || keyCode === SPACE ) ) { + event.preventDefault(); + onClick( event ); + } + }, + onClick: ( event ) => { + if ( event.defaultPrevented ) { + return; + } + if ( onClick ) { + event.preventDefault(); + onClick( event ); + } + }, + readonly: true, + }; + + const buttonModeStyles = onClick + ? 'body { cursor: pointer; } body * { pointer-events: none; }' + : ''; + + return ( + <Iframe + className={ classnames( 'edit-site-style-book__iframe', { + 'is-focused': isFocused && !! onClick, + 'is-button': !! onClick, + } ) } + name="style-book-canvas" + tabIndex={ 0 } + { ...( onClick ? buttonModeProps : {} ) } + > + <EditorStyles styles={ settings.styles } /> + <style> + { + // Forming a "block formatting context" to prevent margin collapsing. + // @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context + `.is-root-container { display: flow-root; } + body { position: relative; padding: 32px !important; }` + + STYLE_BOOK_IFRAME_STYLES + + buttonModeStyles + } + </style> + <Examples + className={ classnames( 'edit-site-style-book__examples', { + 'is-wide': sizes.width > 600, + } ) } + examples={ examples } + category={ category } + label={ + title + ? sprintf( + // translators: %s: Category of blocks, e.g. Text. + __( 'Examples of blocks in the %s category' ), + title + ) + : __( 'Examples of blocks' ) + } + isSelected={ isSelected } + onSelect={ onSelect } + /> + </Iframe> + ); +}; + const Examples = memo( ( { className, examples, category, label, isSelected, onSelect } ) => { const composite = useCompositeState( { orientation: 'vertical' } ); @@ -263,7 +347,9 @@ const Examples = memo( aria-label={ label } > { examples - .filter( ( example ) => example.category === category ) + .filter( ( example ) => + category ? example.category === category : true + ) .map( ( example ) => ( <Example key={ example.name } @@ -273,7 +359,7 @@ const Examples = memo( blocks={ example.blocks } isSelected={ isSelected( example.name ) } onClick={ () => { - onSelect( example.name ); + onSelect?.( example.name ); } } /> ) ) } diff --git a/packages/edit-site/src/components/style-book/style.scss b/packages/edit-site/src/components/style-book/style.scss index 6dcc1fec328abc..0ddefb055a8d8d 100644 --- a/packages/edit-site/src/components/style-book/style.scss +++ b/packages/edit-site/src/components/style-book/style.scss @@ -1,3 +1,22 @@ +.edit-site-style-book { + // Ensure the style book fills the available vertical space. + // This is useful when the style book is used to fill a frame. + height: 100%; + &.is-button { + border-radius: $radius-block-ui * 4; + } +} + +.edit-site-style-book__iframe { + &.is-button { + border-radius: $radius-block-ui * 4; + } + &.is-focused { + outline: calc(2 * var(--wp-admin-border-width-focus)) solid var(--wp-admin-theme-color); + outline-offset: calc(-2 * var(--wp-admin-border-width-focus)); + } +} + .edit-site-style-book__tab-panel { .components-tab-panel__tabs { background: $white; From 671d72704b842228f5dc1a9e42dd433311be9ac4 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Thu, 18 May 2023 11:24:30 +1000 Subject: [PATCH 079/131] DateTime: Remove deprecated props (and fix static analysis action in trunk) (#50724) * DateTime: Try removing deprecated props * Update changelog * Remove additional references to the deprecated props * Bump caniuse-lite version --- package-lock.json | 6 +- .../publish-date-time-picker/index.js | 2 - packages/components/CHANGELOG.md | 4 + packages/components/src/date-time/README.md | 16 -- .../src/date-time/date-time/index.tsx | 172 ++---------------- .../src/date-time/date-time/styles.ts | 4 - .../src/date-time/stories/date-time.tsx | 4 - packages/components/src/date-time/types.ts | 16 -- .../test/__snapshots__/build.js.snap | 32 ++-- .../test/__snapshots__/build.js.snap | 4 +- 10 files changed, 42 insertions(+), 218 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6849e395790b62..040fe3911f3772 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28177,9 +28177,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001434", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", - "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==" + "version": "1.0.30001488", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz", + "integrity": "sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==" }, "capital-case": { "version": "1.0.4", diff --git a/packages/block-editor/src/components/publish-date-time-picker/index.js b/packages/block-editor/src/components/publish-date-time-picker/index.js index 364fb095948024..418006cf854c14 100644 --- a/packages/block-editor/src/components/publish-date-time-picker/index.js +++ b/packages/block-editor/src/components/publish-date-time-picker/index.js @@ -29,8 +29,6 @@ function PublishDateTimePicker( /> <DateTimePicker startOfWeek={ getSettings().l10n.startOfWeek } - __nextRemoveHelpButton - __nextRemoveResetButton onChange={ onChange } { ...additionalProps } /> diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d4cc445dd379d9..6c00d6e22027bc 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Changes + +- `DateTime`: Remove previously deprecated props, `__nextRemoveHelpButton` and `__nextRemoveResetButton` ([#50724](https://github.com/WordPress/gutenberg/pull/50724)). + ### Internal - `Modal`: Remove children container's unused class name ([#50655](https://github.com/WordPress/gutenberg/pull/50655)). diff --git a/packages/components/src/date-time/README.md b/packages/components/src/date-time/README.md index e01d9074953f25..c65cf6d3a1aaea 100644 --- a/packages/components/src/date-time/README.md +++ b/packages/components/src/date-time/README.md @@ -26,8 +26,6 @@ const MyDateTimePicker = () => { currentDate={ date } onChange={ ( newDate ) => setDate( newDate ) } is12Hour={ true } - __nextRemoveHelpButton - __nextRemoveResetButton /> ); }; @@ -83,17 +81,3 @@ The day that the week should start on. 0 for Sunday, 1 for Monday, etc. - Required: No - Default: 0 - -### `__nextRemoveHelpButton`: `boolean` - -Start opting in to not displaying a Help button which will become the default in a future version. - -- Required: No -- Default: `false` - -### `__nextRemoveResetButton`: `boolean` - -Start opting in to not displaying a Reset button which will become the default in a future version. - -- Required: No -- Default: `false` diff --git a/packages/components/src/date-time/date-time/index.tsx b/packages/components/src/date-time/date-time/index.tsx index 123f20e76c640b..a75b9fe57c14ed 100644 --- a/packages/components/src/date-time/date-time/index.tsx +++ b/packages/components/src/date-time/date-time/index.tsx @@ -6,21 +6,16 @@ import type { ForwardedRef } from 'react'; /** * WordPress dependencies */ -import { useState, forwardRef } from '@wordpress/element'; +import { forwardRef } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; -import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ -import Button from '../../button'; import { default as DatePicker } from '../date'; import { default as TimePicker } from '../time'; import type { DateTimePickerProps } from '../types'; -import { Wrapper, CalendarHelp } from './styles'; -import { HStack } from '../../h-stack'; -import { Heading } from '../../heading'; -import { Spacer } from '../../spacer'; +import { Wrapper } from './styles'; export { DatePicker, TimePicker }; @@ -35,157 +30,26 @@ function UnforwardedDateTimePicker( onChange, events, startOfWeek, - __nextRemoveHelpButton = false, - __nextRemoveResetButton = false, }: DateTimePickerProps, ref: ForwardedRef< any > ) { - if ( ! __nextRemoveHelpButton ) { - deprecated( 'Help button in wp.components.DateTimePicker', { - since: '13.4', - version: '15.8', // One year of plugin releases. - hint: 'Set the `__nextRemoveHelpButton` prop to `true` to remove this warning and opt in to the new behaviour, which will become the default in a future version.', - } ); - } - if ( ! __nextRemoveResetButton ) { - deprecated( 'Reset button in wp.components.DateTimePicker', { - since: '13.4', - version: '15.8', // One year of plugin releases. - hint: 'Set the `__nextRemoveResetButton` prop to `true` to remove this warning and opt in to the new behaviour, which will become the default in a future version.', - } ); - } - - const [ calendarHelpIsVisible, setCalendarHelpIsVisible ] = - useState( false ); - - function onClickDescriptionToggle() { - setCalendarHelpIsVisible( ! calendarHelpIsVisible ); - } - return ( <Wrapper ref={ ref } className="components-datetime" spacing={ 4 }> - { ! calendarHelpIsVisible && ( - <> - <TimePicker - currentTime={ currentDate } - onChange={ onChange } - is12Hour={ is12Hour } - /> - <DatePicker - currentDate={ currentDate } - onChange={ onChange } - isInvalidDate={ isInvalidDate } - events={ events } - onMonthPreviewed={ onMonthPreviewed } - startOfWeek={ startOfWeek } - /> - </> - ) } - { calendarHelpIsVisible && ( - <CalendarHelp - className="components-datetime__calendar-help" // Unused, for backwards compatibility. - > - <Heading level={ 4 }>{ __( 'Click to Select' ) }</Heading> - <ul> - <li> - { __( - 'Click the right or left arrows to select other months in the past or the future.' - ) } - </li> - <li>{ __( 'Click the desired day to select it.' ) }</li> - </ul> - <Heading level={ 4 }> - { __( 'Navigating with a keyboard' ) } - </Heading> - <ul> - <li> - <abbr - aria-label={ _x( 'Enter', 'keyboard button' ) } - > - ↵ - </abbr> - { - ' ' /* JSX removes whitespace, but a space is required for screen readers. */ - } - <span>{ __( 'Select the date in focus.' ) }</span> - </li> - <li> - <abbr aria-label={ __( 'Left and Right Arrows' ) }> - ←/→ - </abbr> - { - ' ' /* JSX removes whitespace, but a space is required for screen readers. */ - } - { __( - 'Move backward (left) or forward (right) by one day.' - ) } - </li> - <li> - <abbr aria-label={ __( 'Up and Down Arrows' ) }> - ↑/↓ - </abbr> - { - ' ' /* JSX removes whitespace, but a space is required for screen readers. */ - } - { __( - 'Move backward (up) or forward (down) by one week.' - ) } - </li> - <li> - <abbr aria-label={ __( 'Page Up and Page Down' ) }> - { __( 'PgUp/PgDn' ) } - </abbr> - { - ' ' /* JSX removes whitespace, but a space is required for screen readers. */ - } - { __( - 'Move backward (PgUp) or forward (PgDn) by one month.' - ) } - </li> - <li> - <abbr aria-label={ __( 'Home and End' ) }> - { /* Translators: Home/End reffer to the 'Home' and 'End' buttons on the keyboard.*/ } - { __( 'Home/End' ) } - </abbr> - { - ' ' /* JSX removes whitespace, but a space is required for screen readers. */ - } - { __( - 'Go to the first (Home) or last (End) day of a week.' - ) } - </li> - </ul> - </CalendarHelp> - ) } - { ( ! __nextRemoveResetButton || ! __nextRemoveHelpButton ) && ( - <HStack - className="components-datetime__buttons" // Unused, for backwards compatibility. - > - { ! __nextRemoveResetButton && - ! calendarHelpIsVisible && - currentDate && ( - <Button - className="components-datetime__date-reset-button" // Unused, for backwards compatibility. - variant="link" - onClick={ () => onChange?.( null ) } - > - { __( 'Reset' ) } - </Button> - ) } - <Spacer /> - { ! __nextRemoveHelpButton && ( - <Button - className="components-datetime__date-help-toggle" // Unused, for backwards compatibility. - variant="link" - onClick={ onClickDescriptionToggle } - > - { calendarHelpIsVisible - ? __( 'Close' ) - : __( 'Calendar Help' ) } - </Button> - ) } - </HStack> - ) } + <> + <TimePicker + currentTime={ currentDate } + onChange={ onChange } + is12Hour={ is12Hour } + /> + <DatePicker + currentDate={ currentDate } + onChange={ onChange } + isInvalidDate={ isInvalidDate } + events={ events } + onMonthPreviewed={ onMonthPreviewed } + startOfWeek={ startOfWeek } + /> + </> </Wrapper> ); } @@ -207,8 +71,6 @@ function UnforwardedDateTimePicker( * currentDate={ date } * onChange={ ( newDate ) => setDate( newDate ) } * is12Hour - * __nextRemoveHelpButton - * __nextRemoveResetButton * /> * ); * }; diff --git a/packages/components/src/date-time/date-time/styles.ts b/packages/components/src/date-time/date-time/styles.ts index a7f23eb276e047..51dc6e71f0c468 100644 --- a/packages/components/src/date-time/date-time/styles.ts +++ b/packages/components/src/date-time/date-time/styles.ts @@ -11,7 +11,3 @@ import { VStack } from '../../v-stack'; export const Wrapper = styled( VStack )` box-sizing: border-box; `; - -export const CalendarHelp = styled.div` - min-width: 260px; -`; diff --git a/packages/components/src/date-time/stories/date-time.tsx b/packages/components/src/date-time/stories/date-time.tsx index 53d369728d34d7..447b9cf265ab86 100644 --- a/packages/components/src/date-time/stories/date-time.tsx +++ b/packages/components/src/date-time/stories/date-time.tsx @@ -52,10 +52,6 @@ const Template: ComponentStory< typeof DateTimePicker > = ( { export const Default: ComponentStory< typeof DateTimePicker > = Template.bind( {} ); -Default.args = { - __nextRemoveHelpButton: true, - __nextRemoveResetButton: true, -}; export const WithEvents: ComponentStory< typeof DateTimePicker > = Template.bind( {} ); diff --git a/packages/components/src/date-time/types.ts b/packages/components/src/date-time/types.ts index ae99a7082dcd27..02df096a866b9c 100644 --- a/packages/components/src/date-time/types.ts +++ b/packages/components/src/date-time/types.ts @@ -73,20 +73,4 @@ export type DateTimePickerProps = Omit< DatePickerProps, 'onChange' > & * passed the date and time as an argument. */ onChange?: ( date: string | null ) => void; - - /** - * Start opting in to not displaying a Help button which will become the - * default in a future version. - * - * @default false - */ - __nextRemoveHelpButton?: boolean; - - /** - * Start opting in to not displaying a Reset button which will become - * the default in a future version. - * - * @default false - */ - __nextRemoveResetButton?: boolean; }; diff --git a/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap b/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap index f5bf010b1ef8d8..42102ca2cb1322 100644 --- a/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap +++ b/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DependencyExtractionWebpackPlugin Webpack \`combine-assets\` should produce expected output: Asset file 'assets.php' should match snapshot 1`] = ` -"<?php return array('fileA.js' => array('dependencies' => array('lodash', 'wp-blob'), 'version' => 'bf200ecb3dcb6881a1f3'), 'fileB.js' => array('dependencies' => array('wp-token-list'), 'version' => '0af6c51a8e6ac934b85a')); +"<?php return array('fileA.js' => array('dependencies' => array('lodash', 'wp-blob'), 'version' => 'cf268e19006bef774112'), 'fileB.js' => array('dependencies' => array('wp-token-list'), 'version' => '7f3970305cf0aecb54ab')); " `; @@ -32,7 +32,7 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`combine-assets\` should pro `; exports[`DependencyExtractionWebpackPlugin Webpack \`dynamic-import\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array('wp-blob'), 'version' => '782d84ec5d7303bb6bd2'); +"<?php return array('dependencies' => array('wp-blob'), 'version' => 'c8be4fceac30d1d00ca7'); " `; @@ -50,7 +50,7 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`dynamic-import\` should pro `; exports[`DependencyExtractionWebpackPlugin Webpack \`function-output-filename\` should produce expected output: Asset file 'chunk--main--main.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '4c78134607e6ed966df3'); +"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '9b7ebe61044661fdabda'); " `; @@ -73,7 +73,7 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`function-output-filename\` `; exports[`DependencyExtractionWebpackPlugin Webpack \`has-extension-suffix\` should produce expected output: Asset file 'index.min.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => 'dabeb91f3cb9dd73d48d'); +"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '49dba68ef238f954b358'); " `; @@ -96,21 +96,21 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`has-extension-suffix\` shou `; exports[`DependencyExtractionWebpackPlugin Webpack \`no-default\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array(), 'version' => 'bb85a9737103c7054b00'); +"<?php return array('dependencies' => array(), 'version' => 'f7e2cb527e601f74f8bd'); " `; exports[`DependencyExtractionWebpackPlugin Webpack \`no-default\` should produce expected output: External modules should match snapshot 1`] = `[]`; exports[`DependencyExtractionWebpackPlugin Webpack \`no-deps\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array(), 'version' => '091ffcd70d94dd16e773'); +"<?php return array('dependencies' => array(), 'version' => '143ed23d4b8be5611fcb'); " `; exports[`DependencyExtractionWebpackPlugin Webpack \`no-deps\` should produce expected output: External modules should match snapshot 1`] = `[]`; exports[`DependencyExtractionWebpackPlugin Webpack \`option-function-output-filename\` should produce expected output: Asset file 'chunk--main--main.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '4c78134607e6ed966df3'); +"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '9b7ebe61044661fdabda'); " `; @@ -133,7 +133,7 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`option-function-output-file `; exports[`DependencyExtractionWebpackPlugin Webpack \`option-output-filename\` should produce expected output: Asset file 'main-foo.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '4c78134607e6ed966df3'); +"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '9b7ebe61044661fdabda'); " `; @@ -155,7 +155,7 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`option-output-filename\` sh ] `; -exports[`DependencyExtractionWebpackPlugin Webpack \`output-format-json\` should produce expected output: Asset file 'main.asset.json' should match snapshot 1`] = `"{"dependencies":["lodash"],"version":"a8f35bfc9f46482cc48a"}"`; +exports[`DependencyExtractionWebpackPlugin Webpack \`output-format-json\` should produce expected output: Asset file 'main.asset.json' should match snapshot 1`] = `"{"dependencies":["lodash"],"version":"4c42b9646049ad2e9438"}"`; exports[`DependencyExtractionWebpackPlugin Webpack \`output-format-json\` should produce expected output: External modules should match snapshot 1`] = ` [ @@ -168,7 +168,7 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`output-format-json\` should `; exports[`DependencyExtractionWebpackPlugin Webpack \`overrides\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array('wp-blob', 'wp-script-handle-for-rxjs', 'wp-url'), 'version' => '2a29b245fc3d0509b5a8'); +"<?php return array('dependencies' => array('wp-blob', 'wp-script-handle-for-rxjs', 'wp-url'), 'version' => '708c71445153f1d07e4a'); " `; @@ -207,17 +207,17 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`overrides\` should produce `; exports[`DependencyExtractionWebpackPlugin Webpack \`runtime-chunk-single\` should produce expected output: Asset file 'a.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array('wp-blob'), 'version' => '4514ed711f6c035e0887'); +"<?php return array('dependencies' => array('wp-blob'), 'version' => '09a0c551770a351c5ca7'); " `; exports[`DependencyExtractionWebpackPlugin Webpack \`runtime-chunk-single\` should produce expected output: Asset file 'b.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '168d32b5fb42f9e5d8ce'); +"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => 'c9f00d690a9f72438910'); " `; exports[`DependencyExtractionWebpackPlugin Webpack \`runtime-chunk-single\` should produce expected output: Asset file 'runtime.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array(), 'version' => 'd3c2ce2cb84ff74b92e0'); +"<?php return array('dependencies' => array(), 'version' => '46ea0ff11ac53fa5e88b'); " `; @@ -240,7 +240,7 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`runtime-chunk-single\` shou `; exports[`DependencyExtractionWebpackPlugin Webpack \`style-imports\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '04b9da7eff6fbfcb0452'); +"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => 'd8c0ee89d933a3809c0e'); " `; @@ -263,7 +263,7 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`style-imports\` should prod `; exports[`DependencyExtractionWebpackPlugin Webpack \`wordpress\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '4c78134607e6ed966df3'); +"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '9b7ebe61044661fdabda'); " `; @@ -286,7 +286,7 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`wordpress\` should produce `; exports[`DependencyExtractionWebpackPlugin Webpack \`wordpress-require\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` -"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => 'ed2bd4e7df46768bb3c2'); +"<?php return array('dependencies' => array('lodash', 'wp-blob'), 'version' => '40370eb4ce6428562da6'); " `; diff --git a/packages/readable-js-assets-webpack-plugin/test/__snapshots__/build.js.snap b/packages/readable-js-assets-webpack-plugin/test/__snapshots__/build.js.snap index e5a7d6f6accc84..c033e572e8e5e4 100644 --- a/packages/readable-js-assets-webpack-plugin/test/__snapshots__/build.js.snap +++ b/packages/readable-js-assets-webpack-plugin/test/__snapshots__/build.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ReadableJsAssetsWebpackPlugin should produce the expected output: Asset file index.js should match snapshot 1`] = ` -"/******/ (function() { // webpackBootstrap +"/******/ (() => { // webpackBootstrap var __webpack_exports__ = {}; function notMinified() { // eslint-disable-next-line no-console @@ -16,7 +16,7 @@ notMinified(); exports[`ReadableJsAssetsWebpackPlugin should produce the expected output: Asset file index.min.js should match snapshot 1`] = `"console.log("hello");"`; exports[`ReadableJsAssetsWebpackPlugin should produce the expected output: Asset file view.js should match snapshot 1`] = ` -"/******/ (function() { // webpackBootstrap +"/******/ (() => { // webpackBootstrap var __webpack_exports__ = {}; function notMinified() { // eslint-disable-next-line no-console From b0d20ed901c4f0580a47299f01c5f6aff71fa664 Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Thu, 18 May 2023 15:46:58 +1000 Subject: [PATCH 080/131] Global styles revisions: highlight currently-loaded revision (#50725) * Rename current to selected to reflect the usage Ensure the latest revision is highlighted even when there is no selected id Add useEffect dependencies Updating tests Align text to left for long strings * use a stable reference of an empty array --- .../global-styles/screen-revisions/index.js | 14 +---- .../screen-revisions/revisions-buttons.js | 58 +++++++++++-------- .../global-styles/screen-revisions/style.scss | 5 +- .../test/use-global-styles-revisions.js | 19 +++++- .../use-global-styles-revisions.js | 34 +++++------ 5 files changed, 70 insertions(+), 60 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index 92d5ae7d2bb68b..8a3e84e0ee41eb 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -42,19 +42,11 @@ function ScreenRevisions() { blocks: select( blockEditorStore ).getBlocks(), }; }, [] ); - const { revisions, isLoading, hasUnsavedChanges } = useGlobalStylesRevisions(); + const [ selectedRevisionId, setSelectedRevisionId ] = useState(); const [ globalStylesRevision, setGlobalStylesRevision ] = useState( userConfig ); - - const [ currentRevisionId, setCurrentRevisionId ] = useState( - /* - * We need this for the first render, - * otherwise the unsaved changes haven't been merged into the revisions array yet. - */ - hasUnsavedChanges ? 'unsaved' : revisions?.[ 0 ]?.id - ); const [ isLoadingRevisionWithUnsavedChanges, setIsLoadingRevisionWithUnsavedChanges, @@ -89,7 +81,7 @@ function ScreenRevisions() { settings: revision?.settings, id: revision?.id, } ); - setCurrentRevisionId( revision?.id ); + setSelectedRevisionId( revision?.id ); }; const isLoadButtonEnabled = @@ -117,7 +109,7 @@ function ScreenRevisions() { <div className="edit-site-global-styles-screen-revisions"> <RevisionsButtons onChange={ selectRevision } - currentRevisionId={ currentRevisionId } + selectedRevisionId={ selectedRevisionId } userRevisions={ revisions } /> { isLoadButtonEnabled && ( diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js index c4d624bf1727e2..f32441f6a41b06 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js @@ -18,9 +18,8 @@ import { dateI18n, getDate, humanTimeDiff, getSettings } from '@wordpress/date'; */ function getRevisionLabel( revision ) { const authorDisplayName = revision?.author?.name || __( 'User' ); - const isUnsaved = 'unsaved' === revision?.id; - if ( isUnsaved ) { + if ( 'unsaved' === revision?.id ) { return sprintf( /* translators: %(name)s author display name */ __( 'Unsaved changes by %(name)s' ), @@ -57,45 +56,42 @@ function getRevisionLabel( revision ) { * Returns a rendered list of revisions buttons. * * @typedef {Object} props - * @property {Array<Object>} userRevisions A collection of user revisions. - * @property {number} currentRevisionId Callback fired when the modal is closed or action cancelled. - * @property {Function} onChange Callback fired when a revision is selected. + * @property {Array<Object>} userRevisions A collection of user revisions. + * @property {number} selectedRevisionId The id of the currently-selected revision. + * @property {Function} onChange Callback fired when a revision is selected. * - * @param {props} Component props. + * @param {props} Component props. * @return {JSX.Element} The modal component. */ -function RevisionsButtons( { userRevisions, currentRevisionId, onChange } ) { +function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { return ( <ol className="edit-site-global-styles-screen-revisions__revisions-list" aria-label={ __( 'Global styles revisions' ) } role="group" > - { userRevisions.map( ( revision ) => { - const { id, author, isLatest, modified } = revision; + { userRevisions.map( ( revision, index ) => { + const { id, author, modified } = revision; const authorDisplayName = author?.name || __( 'User' ); const authorAvatar = author?.avatar_urls?.[ '48' ]; - /* - * If the currentId hasn't been selected yet, the first revision is - * the current one so long as the API returns revisions in descending order. - */ - const isActive = !! currentRevisionId - ? id === currentRevisionId - : isLatest; + const isUnsaved = 'unsaved' === revision?.id; + const isSelected = selectedRevisionId + ? selectedRevisionId === revision?.id + : index === 0; return ( <li className={ classnames( 'edit-site-global-styles-screen-revisions__revision-item', { - 'is-current': isActive, + 'is-selected': isSelected, } ) } key={ id } > <Button className="edit-site-global-styles-screen-revisions__revision-button" - disabled={ isActive } + disabled={ isSelected } onClick={ () => { onChange( revision ); } } @@ -106,13 +102,25 @@ function RevisionsButtons( { userRevisions, currentRevisionId, onChange } ) { { humanTimeDiff( modified ) } </time> <span className="edit-site-global-styles-screen-revisions__meta"> - { sprintf( - /* translators: %(name)s author display name */ - __( 'Changes saved by %(name)s' ), - { - name: authorDisplayName, - } - ) } + { isUnsaved + ? sprintf( + /* translators: %(name)s author display name */ + __( + 'Unsaved changes by %(name)s' + ), + { + name: authorDisplayName, + } + ) + : sprintf( + /* translators: %(name)s author display name */ + __( + 'Changes saved by %(name)s' + ), + { + name: authorDisplayName, + } + ) } <img alt={ author?.name } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss index 2a214663447a84..238f3f7d116e19 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss +++ b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss @@ -36,7 +36,7 @@ left: 0; transform: translate(-50%, -50%); } - &.is-current::before { + &.is-selected::before { background: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); } } @@ -56,7 +56,7 @@ } } -.is-current { +.is-selected { .edit-site-global-styles-screen-revisions__revision-button { color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); opacity: 1; @@ -86,6 +86,7 @@ justify-content: space-between; width: 100%; align-items: center; + text-align: left; img { width: $grid-unit-20; diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/test/use-global-styles-revisions.js b/packages/edit-site/src/components/global-styles/screen-revisions/test/use-global-styles-revisions.js index d1e36ec9e595f1..0b7d086c1120fb 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/test/use-global-styles-revisions.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/test/use-global-styles-revisions.js @@ -49,7 +49,6 @@ describe( 'useGlobalStylesRevisions', () => { styles: {}, }, ], - isLoading: false, }; it( 'returns loaded revisions with no unsaved changes', () => { @@ -109,10 +108,24 @@ describe( 'useGlobalStylesRevisions', () => { ] ); } ); - it( 'returns empty revisions when still loading', () => { + it( 'returns empty revisions', () => { useSelect.mockImplementation( () => ( { ...selectValue, - isLoading: true, + revisions: [], + } ) ); + + const { result } = renderHook( () => useGlobalStylesRevisions() ); + const { revisions, isLoading, hasUnsavedChanges } = result.current; + + expect( isLoading ).toBe( true ); + expect( hasUnsavedChanges ).toBe( false ); + expect( revisions ).toEqual( [] ); + } ); + + it( 'returns empty revisions when authors are not yet available', () => { + useSelect.mockImplementation( () => ( { + ...selectValue, + authors: [], } ) ); const { result } = renderHook( () => useGlobalStylesRevisions() ); diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js index e8f714ba72e9ba..94d9296989eee1 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js @@ -20,53 +20,46 @@ const SITE_EDITOR_AUTHORS_QUERY = { context: 'view', capabilities: [ 'edit_theme_options' ], }; - +const EMPTY_ARRAY = []; const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); export default function useGlobalStylesRevisions() { const { user: userConfig } = useContext( GlobalStylesContext ); - const { authors, currentUser, isDirty, revisions, isLoading } = useSelect( + const { authors, currentUser, isDirty, revisions } = useSelect( ( select ) => { const { __experimentalGetDirtyEntityRecords, getCurrentUser, getUsers, getCurrentThemeGlobalStylesRevisions, - isResolving, } = select( coreStore ); const dirtyEntityRecords = __experimentalGetDirtyEntityRecords(); const _currentUser = getCurrentUser(); const _isDirty = dirtyEntityRecords.length > 0; const globalStylesRevisions = - getCurrentThemeGlobalStylesRevisions() || []; - const _authors = getUsers( SITE_EDITOR_AUTHORS_QUERY ); + getCurrentThemeGlobalStylesRevisions() || EMPTY_ARRAY; + const _authors = + getUsers( SITE_EDITOR_AUTHORS_QUERY ) || EMPTY_ARRAY; return { authors: _authors, currentUser: _currentUser, isDirty: _isDirty, revisions: globalStylesRevisions, - isLoading: - ! globalStylesRevisions.length || - isResolving( 'getUsers', [ SITE_EDITOR_AUTHORS_QUERY ] ), }; }, [] ); return useMemo( () => { let _modifiedRevisions = []; - if ( isLoading || ! revisions.length ) { + if ( ! authors.length || ! revisions.length ) { return { revisions: _modifiedRevisions, hasUnsavedChanges: isDirty, - isLoading, + isLoading: true, }; } - /* - * Adds a flag to the first revision, which is the latest. - * Also adds author information to the revision. - * Then, if there are unsaved changes in the editor, create a - * new "revision" item that represents the unsaved changes. - */ + + // Adds author details to each revision. _modifiedRevisions = revisions.map( ( revision ) => { return { ...revision, @@ -76,10 +69,12 @@ export default function useGlobalStylesRevisions() { }; } ); - if ( _modifiedRevisions[ 0 ]?.id !== 'unsaved' ) { + // Flags the most current saved revision. + if ( _modifiedRevisions[ 0 ].id !== 'unsaved' ) { _modifiedRevisions[ 0 ].isLatest = true; } + // Adds an item for unsaved changes. if ( isDirty && ! isEmpty( userConfig ) && currentUser ) { const unsavedRevision = { id: 'unsaved', @@ -94,10 +89,11 @@ export default function useGlobalStylesRevisions() { _modifiedRevisions.unshift( unsavedRevision ); } + return { revisions: _modifiedRevisions, hasUnsavedChanges: isDirty, - isLoading, + isLoading: false, }; - }, [ revisions.length, isDirty, isLoading ] ); + }, [ isDirty, revisions, currentUser, authors, userConfig ] ); } From 2d794c725d1578ff3bd49ee6dedef9a5bb69c58f Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos <aristath@gmail.com> Date: Thu, 18 May 2023 09:39:35 +0300 Subject: [PATCH 081/131] Use `bdo` element when setting the language and direction attributes (#50632) --- packages/format-library/src/language/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/format-library/src/language/index.js b/packages/format-library/src/language/index.js index 60f35c98a4aaec..664f28126c1f2c 100644 --- a/packages/format-library/src/language/index.js +++ b/packages/format-library/src/language/index.js @@ -23,8 +23,8 @@ const title = __( 'Language' ); export const language = { name, - tagName: 'span', - className: 'has-language', + tagName: 'bdo', + className: null, edit: Edit, title, }; From a0a1fd51e542b5c97f166f7be1679c2858dbb96e Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Thu, 18 May 2023 10:00:30 +0200 Subject: [PATCH 082/131] Update `runtime` test field in WebPack config (#50727) --- tools/webpack/blocks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/webpack/blocks.js b/tools/webpack/blocks.js index 481b8b457a1967..b1ef921d1c80f7 100644 --- a/tools/webpack/blocks.js +++ b/tools/webpack/blocks.js @@ -251,7 +251,7 @@ module.exports = [ }, runtime: { name: 'runtime', - test: /[\\/]utils\/interactivity[\\/]/, + test: /[\\/]utils[\\/]interactivity[\\/]/, filename: './interactivity/[name].min.js', chunks: 'all', minSize: 0, From 91fc1cc0adf49861d9f96679afcb99c849b6d364 Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco <gerardo.pacheco@automattic.com> Date: Thu, 18 May 2023 12:57:50 +0200 Subject: [PATCH 083/131] [Mobile] - Block selection - Expand tapping on nested blocks directly (#50672) * Mobile - Block selection - Expand tapping on nested blocks directly * Mobile - E2E helpers - Add navigateUp helper to navigate upwards * Mobile - E2E helpers - Update navigateUp to call driver within its context * Mobile - Update Changelog --- .../src/components/block-list/block.native.js | 15 ++------------- packages/react-native-editor/CHANGELOG.md | 3 ++- .../__device-tests__/pages/editor-page.js | 13 +++++++++++++ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index 2c4516db060176..c342025678b6d9 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -162,7 +162,6 @@ function BlockListBlock( { blockType, draggingClientId, draggingEnabled, - firstToSelectId, isDescendantOfParentSelected, isDescendentBlockSelected, isParentSelected, @@ -174,7 +173,6 @@ function BlockListBlock( { getBlockHierarchyRootClientId, getBlockIndex, getBlockParents, - getLowestCommonAncestorWithSelectedBlock, getSelectedBlockClientId, getSettings, hasSelectedInnerBlock, @@ -198,13 +196,6 @@ function BlockListBlock( { selectedParents.includes( rootClientId ); const hasInnerBlocks = getBlockCount( clientId ) > 0; - const commonAncestor = - getLowestCommonAncestorWithSelectedBlock( clientId ); - const commonAncestorIndex = parents.indexOf( commonAncestor ) - 1; - const firstBlockToSelectId = commonAncestor - ? parents[ commonAncestorIndex ] - : parents[ parents.length - 1 ]; - // For blocks with inner blocks, we only enable the dragging in the nested // blocks if any of them are selected. This way we prevent the long-press // gesture from being disabled for elements within the block UI. @@ -223,7 +214,6 @@ function BlockListBlock( { blockType: currentBlockType, draggingClientId: currentDraggingClientId, draggingEnabled: isDraggingEnabled, - firstToSelectId: firstBlockToSelectId, isDescendantOfParentSelected: descendantOfParentSelected, isDescendentBlockSelected: descendentBlockSelected, isParentSelected: parentSelected, @@ -245,11 +235,10 @@ function BlockListBlock( { [ clientId, removeBlock ] ); const onFocus = useCallback( () => { - const blockId = firstToSelectId ?? clientId; if ( ! isSelected ) { - selectBlock( blockId ); + selectBlock( clientId ); } - }, [ selectBlock, clientId, firstToSelectId, isSelected ] ); + }, [ selectBlock, clientId, isSelected ] ); const onLayout = useCallback( ( { nativeEvent } ) => { diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 6c98c6a5b8a35e..eebc693a26b019 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,10 +10,11 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased +- [**] Tapping on all nested blocks gets focus directly instead of having to tap multiple times depeding on the nesting levels. [#50672] ## 1.95.0 - [*] Fix crash when trying to convert to regular blocks an undefined/deleted reusable block [#50475] -- [**] Tapping on a nested block now gets focus directly instead of having to tap multiple times depeding on the nesting levels. [#50108] +- [**] Tapping on nested text blocks gets focus directly instead of having to tap multiple times depeding on the nesting levels. [#50108] - [*] Use host app namespace in reusable block message [#50478] ## 1.94.0 diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index 4c0b7249a8e77e..defc7bf277c93f 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -499,6 +499,19 @@ class EditorPage { await toolBarButton.click(); } + async navigateUp() { + let navigateUpElements = []; + do { + await this.driver.sleep( 2000 ); + navigateUpElements = await this.driver.elementsByAccessibilityId( + 'Navigate Up' + ); + if ( navigateUpElements.length > 0 ) { + await navigateUpElements[ 0 ].click(); + } + } while ( navigateUpElements.length > 0 ); + } + // ========================= // Inline toolbar functions // ========================= From 6725f29434ebf538f3ba2440b5f83711a1283711 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Thu, 18 May 2023 14:05:04 +0300 Subject: [PATCH 084/131] Fix Global Styles sidebar block selection on zoom out mode (#50708) * Fix Global Styles sidebar block selection on zoom out mode * [Global Styles]: Enable deep linking to the selected block only in the `Blocks` screen --- .../src/components/global-styles/ui.js | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 133e8d61d11489..f15fcff11aa560 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -18,7 +18,7 @@ import { __, sprintf, _n } from '@wordpress/i18n'; import { store as preferencesStore } from '@wordpress/preferences'; import { moreVertical } from '@wordpress/icons'; import { store as coreStore } from '@wordpress/core-data'; -import { useEffect, useRef } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -203,7 +203,6 @@ function GlobalStylesStyleBook() { function GlobalStylesBlockLink() { const navigator = useNavigator(); - const isMounted = useRef(); const { selectedBlockName, selectedBlockClientId } = useSelect( ( select ) => { const { getSelectedBlockClientId, getBlockName } = @@ -217,20 +216,23 @@ function GlobalStylesBlockLink() { [] ); const blockHasGlobalStyles = useBlockHasGlobalStyles( selectedBlockName ); + // When we're in the `Blocks` screen enable deep linking to the selected block. useEffect( () => { - // Avoid navigating to the block screen on mount. - if ( ! isMounted.current ) { - isMounted.current = true; + if ( ! selectedBlockClientId || ! blockHasGlobalStyles ) { return; } - if ( ! selectedBlockClientId || ! blockHasGlobalStyles ) { + const currentPath = navigator.location.path; + if ( + currentPath !== '/blocks' && + ! currentPath.startsWith( '/blocks/' ) + ) { return; } - const path = '/blocks/' + encodeURIComponent( selectedBlockName ); + const newPath = '/blocks/' + encodeURIComponent( selectedBlockName ); // Avoid navigating to the same path. This can happen when selecting // a new block of the same type. - if ( path !== navigator.location.path ) { - navigator.goTo( path, { skipFocus: true } ); + if ( newPath !== currentPath ) { + navigator.goTo( newPath, { skipFocus: true } ); } }, [ selectedBlockClientId, selectedBlockName, blockHasGlobalStyles ] ); } From 589fc1820eb0d8271a5f2af87a310eef89872b1a Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Thu, 18 May 2023 14:36:20 +0200 Subject: [PATCH 085/131] Fix labelling, description, and focus style of the block transform to pattern previews (#50577) * Set ARIA attributes on the correct element. * Fix pattern previews focus style and width. --- .../block-switcher/pattern-transformations-menu.js | 10 +++++----- .../src/components/block-switcher/style.scss | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js index c3516f7e3ec794..83eecd329d8c4c 100644 --- a/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js +++ b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js @@ -105,15 +105,15 @@ function BlockPattern( { pattern, onSelect, composite } ) { `${ baseClassName }-list__item-description` ); return ( - <div - className={ `${ baseClassName }-list__list-item` } - aria-label={ pattern.title } - aria-describedby={ pattern.description ? descriptionId : undefined } - > + <div className={ `${ baseClassName }-list__list-item` }> <CompositeItem role="option" as="div" { ...composite } + aria-label={ pattern.title } + aria-describedby={ + pattern.description ? descriptionId : undefined + } className={ `${ baseClassName }-list__item` } onClick={ () => onSelect( pattern.transformedBlocks ) } > diff --git a/packages/block-editor/src/components/block-switcher/style.scss b/packages/block-editor/src/components/block-switcher/style.scss index 2a8acbf4130145..d587dddc35f99e 100644 --- a/packages/block-editor/src/components/block-switcher/style.scss +++ b/packages/block-editor/src/components/block-switcher/style.scss @@ -109,19 +109,20 @@ } .components-popover__content { - box-shadow: none; + width: 300px; border: $border-width solid $gray-900; background: $white; border-radius: $radius-block-ui; outline: none; + box-shadow: none; } .block-editor-block-switcher__preview { - width: 300px; - height: auto; - // Subtract margin from max-height. + // Subtract vertical margin from max-height. max-height: calc(500px - #{$grid-unit-40}); - margin: $grid-unit-20; + margin: $grid-unit-20 0; + // Use padding to prevent the pattern previews focus style from being cut-off. + padding: 0 $grid-unit-20; overflow: hidden; } } From 37a691faacdb98cefa201c558bbd7fe18f171ebd Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Thu, 18 May 2023 16:11:11 +0300 Subject: [PATCH 086/131] Edit Site: Fix `useEditedEntityRecord()` loading state (#50730) * Edit Site: Fix useEditedEntityRecord() loading state * Use hasFinishedResolution instead * Also check for post ID --- .../src/components/use-edited-entity-record/index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/use-edited-entity-record/index.js b/packages/edit-site/src/components/use-edited-entity-record/index.js index 99da66d9d23c08..59efbea4da3803 100644 --- a/packages/edit-site/src/components/use-edited-entity-record/index.js +++ b/packages/edit-site/src/components/use-edited-entity-record/index.js @@ -16,7 +16,8 @@ export default function useEditedEntityRecord( postType, postId ) { ( select ) => { const { getEditedPostType, getEditedPostId } = select( editSiteStore ); - const { getEditedEntityRecord } = select( coreStore ); + const { getEditedEntityRecord, hasFinishedResolution } = + select( coreStore ); const { __experimentalGetTemplateInfo: getTemplateInfo } = select( editorStore ); const usedPostType = postType ?? getEditedPostType(); @@ -26,7 +27,13 @@ export default function useEditedEntityRecord( postType, postId ) { usedPostType, usedPostId ); - const _isLoaded = !! usedPostId; + const _isLoaded = + usedPostId && + hasFinishedResolution( 'getEditedEntityRecord', [ + 'postType', + usedPostType, + usedPostId, + ] ); const templateInfo = getTemplateInfo( _record ); return { From 0f893f78ccacb9126f2b4f953210b79e1176cc34 Mon Sep 17 00:00:00 2001 From: David Calhoun <github@davidcalhoun.me> Date: Thu, 18 May 2023 09:27:44 -0400 Subject: [PATCH 087/131] docs: Fix change log typo (#50737) --- packages/react-native-editor/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index eebc693a26b019..8918f5522c1a3a 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,7 +10,7 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased -- [**] Tapping on all nested blocks gets focus directly instead of having to tap multiple times depeding on the nesting levels. [#50672] +- [**] Tapping on all nested blocks gets focus directly instead of having to tap multiple times depending on the nesting levels. [#50672] ## 1.95.0 - [*] Fix crash when trying to convert to regular blocks an undefined/deleted reusable block [#50475] From 02a7c49707159f8b79b587fa84f1301c5751e278 Mon Sep 17 00:00:00 2001 From: George Mamadashvili <georgemamadashvili@gmail.com> Date: Thu, 18 May 2023 18:43:12 +0400 Subject: [PATCH 088/131] Fix flaky media inserter drag-and-dropping e2e test (#50740) --- test/e2e/specs/editor/blocks/image.spec.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index eb5d9e2780b445..82a7e1ac71cce5 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -552,9 +552,16 @@ test.describe( 'Image', () => { } ); async function openMediaTab() { - await page - .getByRole( 'button', { name: 'Toggle block inserter' } ) - .click(); + const blockInserter = page.getByRole( 'button', { + name: 'Toggle block inserter', + } ); + const isClosed = + ( await blockInserter.getAttribute( 'aria-pressed' ) ) === + 'false'; + + if ( isClosed ) { + await blockInserter.click(); + } await blockLibrary.getByRole( 'tab', { name: 'Media' } ).click(); From b05777639618d8c3b79451c1570b5c0a881910f5 Mon Sep 17 00:00:00 2001 From: George Mamadashvili <georgemamadashvili@gmail.com> Date: Thu, 18 May 2023 19:03:34 +0400 Subject: [PATCH 089/131] Block Editor: Remove unused 'useIsDimensionsSupportValid' method (#50735) --- packages/block-editor/src/hooks/dimensions.js | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 02c3931ef9850d..084763f0c21b16 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -136,43 +136,3 @@ export function useCustomSides() { version: '6.4', } ); } - -/** - * Custom hook to determine whether the sides configured in the - * block support are valid. A dimension property cannot declare - * support for a mix of axial and individual sides. - * - * @param {string} blockName Block name. - * @param {string} feature The feature custom sides relate to e.g. padding or margins. - * - * @return {boolean} If the feature has a valid configuration of sides. - */ -export function useIsDimensionsSupportValid( blockName, feature ) { - const sides = useCustomSides( blockName, feature ); - - if ( - sides && - sides.some( ( side ) => ALL_SIDES.includes( side ) ) && - sides.some( ( side ) => AXIAL_SIDES.includes( side ) ) - ) { - // eslint-disable-next-line no-console - console.warn( - `The ${ feature } support for the "${ blockName }" block can not be configured to support both axial and arbitrary sides.` - ); - return false; - } - - if ( - sides?.length && - feature === 'blockGap' && - ! AXIAL_SIDES.every( ( side ) => sides.includes( side ) ) - ) { - // eslint-disable-next-line no-console - console.warn( - `The ${ feature } support for the "${ blockName }" block can not be configured to support arbitrary sides.` - ); - return false; - } - - return true; -} From 0146b37f8dc77b08e1364a1011d9a6fb793498d0 Mon Sep 17 00:00:00 2001 From: Joen A <1204802+jasmussen@users.noreply.github.com> Date: Thu, 18 May 2023 18:10:20 +0200 Subject: [PATCH 090/131] Try: Smaller external link icon (#50728) * Try: Smaller external link icon. * Update path --------- Co-authored-by: James Koster <james@jameskoster.co.uk> --- packages/icons/src/library/external.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/icons/src/library/external.js b/packages/icons/src/library/external.js index 15168b6cb501d0..c5117a82275a20 100644 --- a/packages/icons/src/library/external.js +++ b/packages/icons/src/library/external.js @@ -5,7 +5,7 @@ import { Path, SVG } from '@wordpress/primitives'; const external = ( <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> - <Path d="M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z" /> + <Path d="M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z" /> </SVG> ); From 714d1f05f655e64df1daa76b046224143a2798f7 Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco <gerardo.pacheco@automattic.com> Date: Thu, 18 May 2023 19:59:15 +0200 Subject: [PATCH 091/131] Mobile - E2E test - Update code to use the new navigateUp helper (#50736) * Mobile - E2E test - Update code to use the new navigateUp helper * Mobile - E2E helpers - Update navigateUp helper to include recursive option with a false default value * Mobile - E2E Helpers - Rename navigateUp to moveBlockSelectionUp with toRoot as options param * Mobile - E2E test - Update usage of navigateUp to new moveBlockSelectionUp name --- .../gutenberg-editor-media-blocks-@canary.test.js | 8 ++------ .../__device-tests__/pages/editor-page.js | 5 ++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-media-blocks-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-media-blocks-@canary.test.js index 78215dcecc68cf..6fd68a7a4aff34 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-media-blocks-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-media-blocks-@canary.test.js @@ -133,9 +133,7 @@ onlyOniOS( 'Gutenberg Editor Cover Block test', () => { expect( coverBlock ).toBeTruthy(); // Navigate upwards to select parent block - const navigateUpElement = - await editorPage.waitForElementToBeDisplayedById( 'Navigate Up' ); - await navigateUpElement.click(); + await editorPage.moveBlockSelectionUp(); await editorPage.removeBlockAtPosition( blockNames.cover ); } ); @@ -150,9 +148,7 @@ onlyOniOS( 'Gutenberg Editor Cover Block test', () => { ); await coverBlock.click(); // Navigate upwards to select parent block - const navigateUpElement = - await editorPage.waitForElementToBeDisplayedById( 'Navigate Up' ); - await navigateUpElement.click(); + await editorPage.moveBlockSelectionUp(); await editorPage.openBlockSettings(); await editorPage.clickAddMediaFromCoverBlock(); diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index defc7bf277c93f..3b06187482a6f4 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -499,7 +499,7 @@ class EditorPage { await toolBarButton.click(); } - async navigateUp() { + async moveBlockSelectionUp( options = { toRoot: false } ) { let navigateUpElements = []; do { await this.driver.sleep( 2000 ); @@ -509,6 +509,9 @@ class EditorPage { if ( navigateUpElements.length > 0 ) { await navigateUpElements[ 0 ].click(); } + if ( ! options.toRoot ) { + break; + } } while ( navigateUpElements.length > 0 ); } From aa8bc5542ff2c829528a8e8ea1a7dc6c40e20001 Mon Sep 17 00:00:00 2001 From: Ben Dwyer <ben@scruffian.com> Date: Thu, 18 May 2023 23:23:17 +0100 Subject: [PATCH 092/131] Remove OffCanvasEditor (#50705) --- .../components/off-canvas-editor/README.md | 5 - .../components/off-canvas-editor/appender.js | 100 ----- .../off-canvas-editor/block-contents.js | 156 -------- .../off-canvas-editor/block-select-button.js | 128 ------- .../src/components/off-canvas-editor/block.js | 349 ------------------ .../components/off-canvas-editor/branch.js | 238 ------------ .../components/off-canvas-editor/context.js | 8 - .../off-canvas-editor/drop-indicator.js | 126 ------- .../components/off-canvas-editor/expander.js | 26 -- .../src/components/off-canvas-editor/index.js | 271 -------------- .../src/components/off-canvas-editor/leaf.js | 52 --- .../components/off-canvas-editor/link-ui.js | 167 --------- .../components/off-canvas-editor/style.scss | 34 -- .../test/use-inserted-block.js | 108 ------ .../off-canvas-editor/test/utils.js | 50 --- .../off-canvas-editor/update-attributes.js | 99 ----- .../off-canvas-editor/use-block-selection.js | 169 --------- .../off-canvas-editor/use-inserted-block.js | 47 --- .../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 --- packages/block-editor/src/private-apis.js | 2 - packages/block-editor/src/style.scss | 3 - 24 files changed, 2543 deletions(-) delete mode 100644 packages/block-editor/src/components/off-canvas-editor/README.md delete mode 100644 packages/block-editor/src/components/off-canvas-editor/appender.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/block-contents.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/block-select-button.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/block.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/branch.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/context.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/drop-indicator.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/expander.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/index.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/leaf.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/link-ui.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/style.scss delete mode 100644 packages/block-editor/src/components/off-canvas-editor/test/use-inserted-block.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/test/utils.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/update-attributes.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/use-block-selection.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/use-inserted-block.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/use-list-view-client-ids.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/use-list-view-drop-zone.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/use-list-view-expand-selected-item.js delete mode 100644 packages/block-editor/src/components/off-canvas-editor/utils.js diff --git a/packages/block-editor/src/components/off-canvas-editor/README.md b/packages/block-editor/src/components/off-canvas-editor/README.md deleted file mode 100644 index c2f5293edf422d..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Off Canvas Editor - -The OffCanvasEditor 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/appender.js b/packages/block-editor/src/components/off-canvas-editor/appender.js deleted file mode 100644 index 1b91f5bdd76845..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/appender.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * WordPress dependencies - */ -import { useInstanceId } from '@wordpress/compose'; -import { speak } from '@wordpress/a11y'; -import { useSelect } from '@wordpress/data'; -import { forwardRef, useState, useEffect } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../../store'; -import useBlockDisplayTitle from '../block-title/use-block-display-title'; -import { ComposedPrivateInserter as PrivateInserter } from '../inserter'; - -export const Appender = forwardRef( - ( { nestingLevel, blockCount, clientId, ...props }, ref ) => { - const [ insertedBlock, setInsertedBlock ] = useState( null ); - - const instanceId = useInstanceId( Appender ); - const { hideInserter } = useSelect( - ( select ) => { - const { getTemplateLock, __unstableGetEditorMode } = - select( blockEditorStore ); - - return { - hideInserter: - !! getTemplateLock( clientId ) || - __unstableGetEditorMode() === 'zoom-out', - }; - }, - [ clientId ] - ); - - const blockTitle = useBlockDisplayTitle( { - clientId, - context: 'list-view', - } ); - - const insertedBlockTitle = useBlockDisplayTitle( { - clientId: insertedBlock?.clientId, - context: 'list-view', - } ); - - useEffect( () => { - if ( ! insertedBlockTitle?.length ) { - return; - } - - speak( - sprintf( - // translators: %s: name of block being inserted (i.e. Paragraph, Image, Group etc) - __( '%s block inserted' ), - insertedBlockTitle - ), - 'assertive' - ); - }, [ insertedBlockTitle ] ); - - if ( hideInserter ) { - return null; - } - const descriptionId = `off-canvas-editor-appender__${ instanceId }`; - const description = sprintf( - /* translators: 1: The name of the block. 2: The numerical position of the block. 3: The level of nesting for the block. */ - __( 'Append to %1$s block at position %2$d, Level %3$d' ), - blockTitle, - blockCount + 1, - nestingLevel - ); - - return ( - <div className="offcanvas-editor-appender"> - <PrivateInserter - ref={ ref } - rootClientId={ clientId } - position="bottom right" - isAppender - selectBlockOnInsert={ false } - shouldDirectInsert={ false } - __experimentalIsQuick - { ...props } - toggleProps={ { 'aria-describedby': descriptionId } } - onSelectOrClose={ ( maybeInsertedBlock ) => { - if ( maybeInsertedBlock?.clientId ) { - setInsertedBlock( maybeInsertedBlock ); - } - } } - /> - <div - className="offcanvas-editor-appender__description" - id={ descriptionId } - > - { description } - </div> - </div> - ); - } -); 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 deleted file mode 100644 index 796240b0a143cc..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/block-contents.js +++ /dev/null @@ -1,156 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { forwardRef, useEffect, useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; -import ListViewBlockSelectButton from './block-select-button'; -import BlockDraggable from '../block-draggable'; -import { store as blockEditorStore } from '../../store'; -import { updateAttributes } from './update-attributes'; -import { LinkUI } from './link-ui'; -import { useInsertedBlock } from './use-inserted-block'; -import { useListViewContext } from './context'; - -const BLOCKS_WITH_LINK_UI_SUPPORT = [ - 'core/navigation-link', - 'core/navigation-submenu', -]; - -const ListViewBlockContents = forwardRef( - ( - { - onClick, - onToggleExpanded, - block, - isSelected, - position, - siblingBlockCount, - level, - isExpanded, - selectedClientIds, - ...props - }, - ref - ) => { - const { clientId } = block; - const [ isLinkUIOpen, setIsLinkUIOpen ] = useState(); - const { - blockMovingClientId, - selectedBlockInBlockEditor, - lastInsertedBlockClientId, - } = useSelect( - ( select ) => { - const { - hasBlockMovingClientId, - getSelectedBlockClientId, - getLastInsertedBlocksClientIds, - } = unlock( select( blockEditorStore ) ); - const lastInsertedBlocksClientIds = - getLastInsertedBlocksClientIds(); - return { - blockMovingClientId: hasBlockMovingClientId(), - selectedBlockInBlockEditor: getSelectedBlockClientId(), - lastInsertedBlockClientId: - lastInsertedBlocksClientIds && - lastInsertedBlocksClientIds[ 0 ], - }; - }, - [ clientId ] - ); - - const { - insertedBlockAttributes, - insertedBlockName, - setInsertedBlockAttributes, - } = useInsertedBlock( lastInsertedBlockClientId ); - - const hasExistingLinkValue = insertedBlockAttributes?.url; - - useEffect( () => { - if ( - clientId === lastInsertedBlockClientId && - BLOCKS_WITH_LINK_UI_SUPPORT?.includes( insertedBlockName ) && - ! hasExistingLinkValue // don't re-show the Link UI if the block already has a link value. - ) { - setIsLinkUIOpen( true ); - } - }, [ - lastInsertedBlockClientId, - clientId, - insertedBlockName, - hasExistingLinkValue, - ] ); - - const { renderAdditionalBlockUI } = useListViewContext(); - - 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 ( - <> - { renderAdditionalBlockUI && renderAdditionalBlockUI( block ) } - { isLinkUIOpen && ( - <LinkUI - clientId={ lastInsertedBlockClientId } - link={ insertedBlockAttributes } - onClose={ () => setIsLinkUIOpen( false ) } - hasCreateSuggestion={ false } - onChange={ ( updatedValue ) => { - updateAttributes( - updatedValue, - setInsertedBlockAttributes, - insertedBlockAttributes - ); - setIsLinkUIOpen( false ); - } } - onCancel={ () => setIsLinkUIOpen( false ) } - /> - ) } - <BlockDraggable clientIds={ draggableClientIds }> - { ( { draggable, onDragStart, onDragEnd } ) => ( - <ListViewBlockSelectButton - ref={ ref } - className={ className } - block={ block } - onClick={ onClick } - onToggleExpanded={ onToggleExpanded } - isSelected={ isSelected } - position={ position } - siblingBlockCount={ siblingBlockCount } - level={ level } - draggable={ draggable } - onDragStart={ onDragStart } - onDragEnd={ onDragEnd } - isExpanded={ isExpanded } - { ...props } - /> - ) } - </BlockDraggable> - </> - ); - } -); - -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 deleted file mode 100644 index 51404f6a39dbc5..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/block-select-button.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * 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, lockSmall as lock } from '@wordpress/icons'; -import { SPACE, ENTER } from '@wordpress/keycodes'; -import { sprintf, __ } from '@wordpress/i18n'; - -/** - * 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, - onClick, - onToggleExpanded, - tabIndex, - onFocus, - onDragStart, - onDragEnd, - draggable, - }, - ref -) { - const { clientId } = block; - 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 ); - } - } - - const editAriaLabel = blockInformation - ? sprintf( - // translators: %s: The title of the block. - __( 'Edit %s block' ), - blockInformation.title - ) - : __( 'Edit' ); - - return ( - <> - <Button - className={ classnames( - 'block-editor-list-view-block-select-button', - className - ) } - onClick={ onClick } - onKeyDown={ onKeyDownHandler } - ref={ ref } - tabIndex={ tabIndex } - onFocus={ onFocus } - onDragStart={ onDragStartHandler } - onDragEnd={ onDragEnd } - draggable={ draggable } - href={ `#block-${ clientId }` } - aria-hidden={ true } - title={ editAriaLabel } - > - <ListViewExpander onClick={ onToggleExpanded } /> - <BlockIcon - icon={ blockInformation?.icon } - showColors - context="list-view" - /> - <HStack - alignment="center" - className="block-editor-list-view-block-select-button__label-wrapper" - justify="flex-start" - spacing={ 1 } - > - <span className="block-editor-list-view-block-select-button__title"> - <Truncate ellipsizeMode="auto">{ blockTitle }</Truncate> - </span> - { blockInformation?.anchor && ( - <span className="block-editor-list-view-block-select-button__anchor-wrapper"> - <Truncate - className="block-editor-list-view-block-select-button__anchor" - ellipsizeMode="auto" - > - { blockInformation.anchor } - </Truncate> - </span> - ) } - { isLocked && ( - <span className="block-editor-list-view-block-select-button__lock"> - <Icon icon={ lock } /> - </span> - ) } - </HStack> - </Button> - </> - ); -} - -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 deleted file mode 100644 index d5bc4e46c6ce97..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/block.js +++ /dev/null @@ -1,349 +0,0 @@ -/** - * 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: { clientId }, - isDragged, - isSelected, - isBranchSelected, - selectBlock, - position, - level, - rowCount, - siblingBlockCount, - showBlockMovers, - path, - isExpanded, - selectedClientIds, - preventAnnouncement, -} ) { - const cellRef = useRef( null ); - const [ isHovered, setIsHovered ] = useState( false ); - - 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 block = useSelect( - ( select ) => select( blockEditorStore ).getBlock( clientId ), - [ clientId ] - ); - - // 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 { isTreeGridMounted, expand, expandedState, collapse, LeafMoreMenu } = - useListViewContext(); - - 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 ] - ); - - const instanceId = useInstanceId( ListViewBlock ); - - if ( ! block ) { - return null; - } - - // 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 = - !! block && - hasBlockSupport( block.name, '__experimentalToolbar', true ); - - 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 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 } - ); - - let colSpan; - if ( hasRenderedMovers ) { - colSpan = 1; - } else if ( ! showBlockActions ) { - colSpan = 2; - } - - 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 ]; - - const MoreMenuComponent = LeafMoreMenu - ? LeafMoreMenu - : BlockSettingsDropdown; - - return ( - <ListViewLeaf - className={ classes } - onMouseEnter={ onMouseEnter } - onMouseLeave={ onMouseLeave } - onFocus={ onMouseEnter } - onBlur={ onMouseLeave } - level={ level } - position={ position } - rowCount={ rowCount } - path={ path } - id={ `list-view-block-${ clientId }` } - data-block={ clientId } - isExpanded={ isContentLocked ? undefined : isExpanded } - aria-selected={ !! isSelected || forceSelectionContentLock } - > - <TreeGridCell - className="block-editor-list-view-block__contents-cell" - colSpan={ colSpan } - ref={ cellRef } - aria-label={ blockAriaLabel } - aria-selected={ !! isSelected || forceSelectionContentLock } - aria-expanded={ isContentLocked ? undefined : isExpanded } - aria-describedby={ descriptionId } - > - { ( { ref, tabIndex, onFocus } ) => ( - <div className="block-editor-list-view-block__contents-container"> - <ListViewBlockContents - block={ block } - onClick={ selectEditorBlock } - onToggleExpanded={ toggleExpanded } - isSelected={ isSelected } - position={ position } - siblingBlockCount={ siblingBlockCount } - level={ level } - ref={ ref } - tabIndex={ tabIndex } - onFocus={ onFocus } - isExpanded={ isExpanded } - selectedClientIds={ selectedClientIds } - preventAnnouncement={ preventAnnouncement } - /> - <div - className="block-editor-list-view-block-select-button__description" - id={ descriptionId } - > - { blockPositionDescription } - </div> - </div> - ) } - </TreeGridCell> - { hasRenderedMovers && ( - <> - <TreeGridCell - className={ moverCellClassName } - withoutGridItem - > - <TreeGridItem> - { ( { ref, tabIndex, onFocus } ) => ( - <BlockMoverUpButton - orientation="vertical" - clientIds={ [ clientId ] } - ref={ ref } - tabIndex={ tabIndex } - onFocus={ onFocus } - /> - ) } - </TreeGridItem> - <TreeGridItem> - { ( { ref, tabIndex, onFocus } ) => ( - <BlockMoverDownButton - orientation="vertical" - clientIds={ [ clientId ] } - ref={ ref } - tabIndex={ tabIndex } - onFocus={ onFocus } - /> - ) } - </TreeGridItem> - </TreeGridCell> - </> - ) } - - { showBlockActions && ( - <> - <TreeGridCell - className={ listViewBlockSettingsClassName } - aria-selected={ - !! isSelected || forceSelectionContentLock - } - > - { ( { ref, tabIndex, onFocus } ) => ( - <> - <MoreMenuComponent - clientIds={ dropdownClientIds } - block={ block } - clientId={ clientId } - icon={ moreVertical } - label={ settingsAriaLabel } - toggleProps={ { - ref, - className: - 'block-editor-list-view-block__menu', - tabIndex, - onFocus, - } } - disableOpenOnArrowDown - __experimentalSelectBlock={ - updateSelection - } - expandedState={ expandedState } - expand={ expand } - /> - </> - ) } - </TreeGridCell> - </> - ) } - </ListViewLeaf> - ); -} - -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 deleted file mode 100644 index 3749ef65add494..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/branch.js +++ /dev/null @@ -1,238 +0,0 @@ -/** - * WordPress dependencies - */ -import { - __experimentalTreeGridRow as TreeGridRow, - __experimentalTreeGridCell as TreeGridCell, -} from '@wordpress/components'; -import { AsyncModeProvider, useSelect } from '@wordpress/data'; -import { memo } from '@wordpress/element'; - -/** - * Internal dependencies - */ - -import { Appender } from './appender'; -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; - }; - -const noop = () => {}; - -function ListViewBranch( props ) { - const { - blocks, - selectBlock = noop, - showBlockMovers, - selectedClientIds, - level = 1, - path = '', - isBranchSelected = false, - listPosition = 0, - fixedListWindow, - isExpanded, - parentId, - shouldShowInnerBlocks = true, - showAppender: showAppenderProp = true, - } = props; - - const isContentLocked = useSelect( - ( select ) => { - return !! ( - parentId && - select( blockEditorStore ).getTemplateLock( parentId ) === - 'contentOnly' - ); - }, - [ parentId ] - ); - - const { expandedState, draggedClientIds } = useListViewContext(); - - if ( isContentLocked ) { - return null; - } - - // Only show the appender at the first level. - const showAppender = showAppenderProp && level === 1; - - const filteredBlocks = blocks.filter( Boolean ); - const blockCount = filteredBlocks.length; - - // The appender means an extra row in List View, so add 1 to the row count. - const rowCount = showAppender ? blockCount + 1 : blockCount; - 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 ( - <AsyncModeProvider key={ clientId } value={ ! isSelected }> - { showBlock && ( - <ListViewBlock - block={ block } - selectBlock={ selectBlock } - isSelected={ isSelected } - isBranchSelected={ isSelectedBranch } - isDragged={ isDragged } - level={ level } - position={ position } - rowCount={ rowCount } - siblingBlockCount={ blockCount } - showBlockMovers={ showBlockMovers } - path={ updatedPath } - isExpanded={ shouldExpand } - listPosition={ nextPosition } - selectedClientIds={ selectedClientIds } - /> - ) } - { ! showBlock && ( - <tr> - <td className="block-editor-list-view-placeholder" /> - </tr> - ) } - { hasNestedBlocks && shouldExpand && ! isDragged && ( - <ListViewBranch - parentId={ clientId } - blocks={ innerBlocks } - selectBlock={ selectBlock } - showBlockMovers={ showBlockMovers } - level={ level + 1 } - path={ updatedPath } - listPosition={ nextPosition + 1 } - fixedListWindow={ fixedListWindow } - isBranchSelected={ isSelectedBranch } - selectedClientIds={ selectedClientIds } - isExpanded={ isExpanded } - showAppender={ showAppenderProp } - /> - ) } - </AsyncModeProvider> - ); - } ) } - { showAppender && ( - <TreeGridRow - level={ level } - setSize={ rowCount } - positionInSet={ rowCount } - isExpanded={ true } - > - <TreeGridCell> - { ( treeGridCellProps ) => ( - <Appender - clientId={ parentId } - nestingLevel={ level } - blockCount={ blockCount } - { ...treeGridCellProps } - /> - ) } - </TreeGridCell> - </TreeGridRow> - ) } - </> - ); -} - -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 deleted file mode 100644 index c837dce9ca23fd..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/context.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * 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 deleted file mode 100644 index 1e8d51a73919ab..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/drop-indicator.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * 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 ( - <Popover - animate={ false } - anchor={ popoverAnchor } - focusOnMount={ false } - className="block-editor-list-view-drop-indicator" - variant="unstyled" - > - <div - style={ style } - className="block-editor-list-view-drop-indicator__line" - /> - </Popover> - ); -} diff --git a/packages/block-editor/src/components/off-canvas-editor/expander.js b/packages/block-editor/src/components/off-canvas-editor/expander.js deleted file mode 100644 index 3b93f8ad01185c..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/expander.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 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 - <span - className="block-editor-list-view__expander" - onClick={ ( event ) => onClick( event, { forceToggle: true } ) } - aria-hidden="true" - > - <Icon icon={ isRTL() ? chevronLeftSmall : chevronRightSmall } /> - </span> - ); -} diff --git a/packages/block-editor/src/components/off-canvas-editor/index.js b/packages/block-editor/src/components/off-canvas-editor/index.js deleted file mode 100644 index 97b8a4d4a71c51..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/index.js +++ /dev/null @@ -1,271 +0,0 @@ -/** - * WordPress dependencies - */ -import { - useMergeRefs, - __experimentalUseFixedWindowList as useFixedWindowList, -} from '@wordpress/compose'; -import { - __experimentalTreeGrid as TreeGrid, - __experimentalTreeGridRow as TreeGridRow, - __experimentalTreeGridCell as TreeGridCell, -} 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 {string} props.parentClientId The client id of the parent block. - * @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 {Object} props.LeafMoreMenu Optional more menu substitution. - * @param {string} props.description Optional accessible description for the tree grid component. - * @param {string} props.onSelect Optional callback to be invoked when a block is selected. - * @param {string} props.showAppender Flag to show or hide the block appender. - * @param {Function} props.renderAdditionalBlockUI Function that renders additional block content UI. - * @param {Object} ref Forwarded ref. - */ -function OffCanvasEditor( - { - id, - parentClientId, - blocks, - showBlockMovers = false, - isExpanded = false, - showAppender = true, - LeafMoreMenu, - description = __( 'Block navigation structure' ), - onSelect, - renderAdditionalBlockUI, - }, - ref -) { - const { getBlock } = useSelect( blockEditorStore ); - 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, blocks ] - ); - - 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, blockClientId ) => { - updateBlockSelection( event, blockClientId ); - setSelectedTreeId( blockClientId ); - if ( onSelect ) { - onSelect( getBlock( blockClientId ) ); - } - }, - [ setSelectedTreeId, updateBlockSelection, onSelect, getBlock ] - ); - 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( - ( blockClientId ) => { - if ( ! blockClientId ) { - return; - } - setExpandedState( { - type: 'expand', - clientIds: [ blockClientId ], - } ); - }, - [ setExpandedState ] - ); - const collapse = useCallback( - ( blockClientId ) => { - if ( ! blockClientId ) { - return; - } - setExpandedState( { - type: 'collapse', - clientIds: [ blockClientId ], - } ); - }, - [ 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, - LeafMoreMenu, - renderAdditionalBlockUI, - } ), - [ - isMounted.current, - draggedClientIds, - expandedState, - expand, - collapse, - LeafMoreMenu, - renderAdditionalBlockUI, - ] - ); - - return ( - <AsyncModeProvider value={ true }> - <ListViewDropIndicator - listViewRef={ elementRef } - blockDropTarget={ blockDropTarget } - /> - <div className="offcanvas-editor-list-view-tree-wrapper"> - <TreeGrid - id={ id } - className="block-editor-list-view-tree" - aria-label={ __( 'Block navigation structure' ) } - ref={ treeGridRef } - onCollapseRow={ collapseRow } - onExpandRow={ expandRow } - onFocusRow={ focusRow } - // eslint-disable-next-line jsx-a11y/aria-props - aria-description={ description } - > - <ListViewContext.Provider value={ contextValue }> - <ListViewBranch - parentId={ parentClientId } - blocks={ clientIdsTree } - selectBlock={ selectEditorBlock } - showBlockMovers={ showBlockMovers } - fixedListWindow={ fixedListWindow } - selectedClientIds={ selectedClientIds } - isExpanded={ isExpanded } - shouldShowInnerBlocks={ shouldShowInnerBlocks } - showAppender={ showAppender } - /> - <TreeGridRow - level={ 1 } - setSize={ 1 } - positionInSet={ 1 } - isExpanded={ true } - > - { ! clientIdsTree.length && ( - <TreeGridCell withoutGridItem> - <div className="offcanvas-editor-list-view-is-empty"> - { __( - 'Your menu is currently empty. Add your first menu item to get started.' - ) } - </div> - </TreeGridCell> - ) } - </TreeGridRow> - </ListViewContext.Provider> - </TreeGrid> - </div> - </AsyncModeProvider> - ); -} - -export default forwardRef( OffCanvasEditor ); diff --git a/packages/block-editor/src/components/off-canvas-editor/leaf.js b/packages/block-editor/src/components/off-canvas-editor/leaf.js deleted file mode 100644 index 7d74c85ffeb367..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/leaf.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * 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 ( - <AnimatedTreeGridRow - ref={ ref } - className={ classnames( - 'block-editor-list-view-leaf', - 'offcanvas-editor-list-view-leaf', - className - ) } - level={ level } - positionInSet={ position } - setSize={ rowCount } - { ...props } - > - { children } - </AnimatedTreeGridRow> - ); -} diff --git a/packages/block-editor/src/components/off-canvas-editor/link-ui.js b/packages/block-editor/src/components/off-canvas-editor/link-ui.js deleted file mode 100644 index f6b5e2538d9e7c..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/link-ui.js +++ /dev/null @@ -1,167 +0,0 @@ -// Note: this file is copied directly from packages/block-library/src/navigation-link/link-ui.js - -/** - * WordPress dependencies - */ -import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; -import { Popover, Button } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { switchToBlockType } from '@wordpress/blocks'; -import { useSelect, useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../../store'; -import LinkControl from '../link-control'; -import BlockIcon from '../block-icon'; - -/** - * Given the Link block's type attribute, return the query params to give to - * /wp/v2/search. - * - * @param {string} type Link block's type attribute. - * @param {string} kind Link block's entity of kind (post-type|taxonomy) - * @return {{ type?: string, subtype?: string }} Search query params. - */ -export function getSuggestionsQuery( type, kind ) { - switch ( type ) { - case 'post': - case 'page': - return { type: 'post', subtype: type }; - case 'category': - return { type: 'term', subtype: 'category' }; - case 'tag': - return { type: 'term', subtype: 'post_tag' }; - case 'post_format': - return { type: 'post-format' }; - default: - if ( kind === 'taxonomy' ) { - return { type: 'term', subtype: type }; - } - if ( kind === 'post-type' ) { - return { type: 'post', subtype: type }; - } - return {}; - } -} - -/** - * Add transforms to Link Control - * - * @param {Object} props Component props. - * @param {string} props.clientId Block client ID. - */ -function LinkControlTransforms( { clientId } ) { - const { getBlock, blockTransforms } = useSelect( - ( select ) => { - const { - getBlock: _getBlock, - getBlockRootClientId, - getBlockTransformItems, - } = select( blockEditorStore ); - - return { - getBlock: _getBlock, - blockTransforms: getBlockTransformItems( - _getBlock( clientId ), - getBlockRootClientId( clientId ) - ), - }; - }, - [ clientId ] - ); - - const { replaceBlock } = useDispatch( blockEditorStore ); - - const featuredBlocks = [ - 'core/page-list', - 'core/site-logo', - 'core/social-links', - 'core/search', - ]; - - const transforms = blockTransforms.filter( ( item ) => { - return featuredBlocks.includes( item.name ); - } ); - - if ( ! transforms?.length ) { - return null; - } - - if ( ! clientId ) { - return null; - } - - return ( - <div className="link-control-transform"> - <h3 className="link-control-transform__subheading"> - { __( 'Transform' ) } - </h3> - <div className="link-control-transform__items"> - { transforms.map( ( item ) => { - return ( - <Button - key={ `transform-${ item.name }` } - onClick={ () => - replaceBlock( - clientId, - switchToBlockType( - getBlock( clientId ), - item.name - ) - ) - } - className="link-control-transform__item" - > - <BlockIcon icon={ item.icon } /> - { item.title } - </Button> - ); - } ) } - </div> - </div> - ); -} - -export function LinkUI( props ) { - const { label, url, opensInNewTab, type, kind } = props.link; - const link = { - url, - opensInNewTab, - title: label && stripHTML( label ), - }; - - return ( - <Popover - placement="bottom" - onClose={ props.onClose } - anchor={ props.anchor } - shift - > - <LinkControl - hasTextControl - hasRichPreviews - className={ props.className } - value={ link } - showInitialSuggestions={ true } - withCreateSuggestion={ props.hasCreateSuggestion } - noDirectEntry={ !! type } - noURLSuggestion={ !! type } - suggestionsQuery={ getSuggestionsQuery( type, kind ) } - onChange={ props.onChange } - onRemove={ props.onRemove } - onCancel={ props.onCancel } - renderControlBottom={ - ! url - ? () => ( - <LinkControlTransforms - clientId={ props.clientId } - /> - ) - : null - } - /> - </Popover> - ); -} diff --git a/packages/block-editor/src/components/off-canvas-editor/style.scss b/packages/block-editor/src/components/off-canvas-editor/style.scss deleted file mode 100644 index 6cf9f312265e30..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/style.scss +++ /dev/null @@ -1,34 +0,0 @@ -.offcanvas-editor-appender .block-editor-inserter__toggle { - background-color: #1e1e1e; - color: #fff; - margin: $grid-unit-10 0 0 24px; - border-radius: 2px; - height: 24px; - min-width: 24px; - padding: 0; - - &:hover, - &:focus { - background: var(--wp-admin-theme-color); - color: #fff; - } -} - -.offcanvas-editor-appender__description { - display: none; -} - -.offcanvas-editor-list-view-tree-wrapper { - max-width: 100%; - overflow-x: auto; -} - -.offcanvas-editor-list-view-leaf { - display: block; - // sidebar width - tab panel padding - max-width: $sidebar-width - (2 * $grid-unit-20); -} - -.offcanvas-editor-list-view-is-empty { - margin-left: $grid-unit-20; -} diff --git a/packages/block-editor/src/components/off-canvas-editor/test/use-inserted-block.js b/packages/block-editor/src/components/off-canvas-editor/test/use-inserted-block.js deleted file mode 100644 index f4e6746581e008..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/test/use-inserted-block.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Internal dependencies - */ -import { useInsertedBlock } from '../use-inserted-block'; - -/** - * WordPress dependencies - */ -import { useDispatch, useSelect } from '@wordpress/data'; - -/** - * External dependencies - */ -import { act, renderHook } from '@testing-library/react'; - -jest.mock( '@wordpress/data/src/components/use-select', () => { - // This allows us to tweak the returned value on each test. - const mock = jest.fn(); - return mock; -} ); - -jest.mock( '@wordpress/data/src/components/use-dispatch', () => ( { - useDispatch: jest.fn(), -} ) ); - -describe( 'useInsertedBlock', () => { - const mockUpdateBlockAttributes = jest.fn(); - - it( 'returns undefined values when called without a block clientId', () => { - useSelect.mockImplementation( () => ( { - insertedBlockAttributes: { - 'some-attribute': 'some-value', - }, - insertedBlockName: 'core/navigation-link', - } ) ); - - useDispatch.mockImplementation( () => ( { - updateBlockAttributes: mockUpdateBlockAttributes, - } ) ); - - const { result } = renderHook( () => useInsertedBlock() ); - - const { - insertedBlockName, - insertedBlockAttributes, - setInsertedBlockAttributes, - } = result.current; - - expect( insertedBlockName ).toBeUndefined(); - expect( insertedBlockAttributes ).toBeUndefined(); - expect( - setInsertedBlockAttributes( { 'some-attribute': 'new-value' } ) - ).toBeUndefined(); - } ); - - it( 'returns name and attributes when called with a block clientId', () => { - useSelect.mockImplementation( () => ( { - insertedBlockAttributes: { - 'some-attribute': 'some-value', - }, - insertedBlockName: 'core/navigation-link', - } ) ); - - useDispatch.mockImplementation( () => ( { - updateBlockAttributes: mockUpdateBlockAttributes, - } ) ); - - const { result } = renderHook( () => - useInsertedBlock( 'some-client-id-here' ) - ); - - const { insertedBlockName, insertedBlockAttributes } = result.current; - - expect( insertedBlockName ).toBe( 'core/navigation-link' ); - expect( insertedBlockAttributes ).toEqual( { - 'some-attribute': 'some-value', - } ); - } ); - - it( 'dispatches updateBlockAttributes on provided client ID with new attributes when setInsertedBlockAttributes is called', () => { - useSelect.mockImplementation( () => ( { - insertedBlockAttributes: { - 'some-attribute': 'some-value', - }, - insertedBlockName: 'core/navigation-link', - } ) ); - - useDispatch.mockImplementation( () => ( { - updateBlockAttributes: mockUpdateBlockAttributes, - } ) ); - - const clientId = '123456789'; - - const { result } = renderHook( () => useInsertedBlock( clientId ) ); - - const { setInsertedBlockAttributes } = result.current; - - act( () => { - setInsertedBlockAttributes( { - 'some-attribute': 'new-value', - } ); - } ); - - expect( mockUpdateBlockAttributes ).toHaveBeenCalledWith( clientId, { - 'some-attribute': 'new-value', - } ); - } ); -} ); 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 deleted file mode 100644 index 78d78a9d90069c..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/test/utils.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 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/update-attributes.js b/packages/block-editor/src/components/off-canvas-editor/update-attributes.js deleted file mode 100644 index 5133cae3878338..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/update-attributes.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * WordPress dependencies - */ -import { escapeHTML } from '@wordpress/escape-html'; -import { safeDecodeURI } from '@wordpress/url'; - -/** - * @typedef {'post-type'|'custom'|'taxonomy'|'post-type-archive'} WPNavigationLinkKind - */ -/** - * Navigation Link Block Attributes - * - * @typedef {Object} WPNavigationLinkBlockAttributes - * - * @property {string} [label] Link text. - * @property {WPNavigationLinkKind} [kind] Kind is used to differentiate between term and post ids to check post draft status. - * @property {string} [type] The type such as post, page, tag, category and other custom types. - * @property {string} [rel] The relationship of the linked URL. - * @property {number} [id] A post or term id. - * @property {boolean} [opensInNewTab] Sets link target to _blank when true. - * @property {string} [url] Link href. - * @property {string} [title] Link title attribute. - */ -/** - * Link Control onChange handler that updates block attributes when a setting is changed. - * - * @param {Object} updatedValue New block attributes to update. - * @param {Function} setAttributes Block attribute update function. - * @param {WPNavigationLinkBlockAttributes} blockAttributes Current block attributes. - * - */ - -export const updateAttributes = ( - updatedValue = {}, - setAttributes, - blockAttributes = {} -) => { - const { - label: originalLabel = '', - kind: originalKind = '', - type: originalType = '', - } = blockAttributes; - - const { - title: newLabel = '', // the title of any provided Post. - url: newUrl = '', - opensInNewTab, - id, - kind: newKind = originalKind, - type: newType = originalType, - } = updatedValue; - - const newLabelWithoutHttp = newLabel.replace( /http(s?):\/\//gi, '' ); - const newUrlWithoutHttp = newUrl.replace( /http(s?):\/\//gi, '' ); - - const useNewLabel = - newLabel && - newLabel !== originalLabel && - // LinkControl without the title field relies - // on the check below. Specifically, it assumes that - // the URL is the same as a title. - // This logic a) looks suspicious and b) should really - // live in the LinkControl and not here. It's a great - // candidate for future refactoring. - newLabelWithoutHttp !== newUrlWithoutHttp; - - // Unfortunately this causes the escaping model to be inverted. - // The escaped content is stored in the block attributes (and ultimately in the database), - // and then the raw data is "recovered" when outputting into the DOM. - // It would be preferable to store the **raw** data in the block attributes and escape it in JS. - // Why? Because there isn't one way to escape data. Depending on the context, you need to do - // different transforms. It doesn't make sense to me to choose one of them for the purposes of storage. - // See also: - // - https://github.com/WordPress/gutenberg/pull/41063 - // - https://github.com/WordPress/gutenberg/pull/18617. - const label = useNewLabel - ? escapeHTML( newLabel ) - : originalLabel || escapeHTML( newUrlWithoutHttp ); - - // In https://github.com/WordPress/gutenberg/pull/24670 we decided to use "tag" in favor of "post_tag" - const type = newType === 'post_tag' ? 'tag' : newType.replace( '-', '_' ); - - const isBuiltInType = - [ 'post', 'page', 'tag', 'category' ].indexOf( type ) > -1; - - const isCustomLink = - ( ! newKind && ! isBuiltInType ) || newKind === 'custom'; - const kind = isCustomLink ? 'custom' : newKind; - - setAttributes( { - // Passed `url` may already be encoded. To prevent double encoding, decodeURI is executed to revert to the original string. - ...( newUrl && { url: encodeURI( safeDecodeURI( newUrl ) ) } ), - ...( label && { label } ), - ...( undefined !== opensInNewTab && { opensInNewTab } ), - ...( id && Number.isInteger( id ) && { id } ), - ...( kind && { kind } ), - ...( type && type !== 'URL' && { type } ), - } ); -}; 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 deleted file mode 100644 index 59aaaeacb01d40..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/use-block-selection.js +++ /dev/null @@ -1,169 +0,0 @@ -/** - * 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-inserted-block.js b/packages/block-editor/src/components/off-canvas-editor/use-inserted-block.js deleted file mode 100644 index 0e5a25c980a1c3..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/use-inserted-block.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect, useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../../store'; - -export const useInsertedBlock = ( insertedBlockClientId ) => { - const { insertedBlockAttributes, insertedBlockName } = useSelect( - ( select ) => { - const { getBlockName, getBlockAttributes } = - select( blockEditorStore ); - - return { - insertedBlockAttributes: getBlockAttributes( - insertedBlockClientId - ), - insertedBlockName: getBlockName( insertedBlockClientId ), - }; - }, - [ insertedBlockClientId ] - ); - - const { updateBlockAttributes } = useDispatch( blockEditorStore ); - - const setInsertedBlockAttributes = ( _updatedAttributes ) => { - if ( ! insertedBlockClientId ) return; - updateBlockAttributes( insertedBlockClientId, _updatedAttributes ); - }; - - if ( ! insertedBlockClientId ) { - return { - insertedBlockAttributes: undefined, - insertedBlockName: undefined, - setInsertedBlockAttributes, - }; - } - - return { - insertedBlockAttributes, - insertedBlockName, - setInsertedBlockAttributes, - }; -}; 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 deleted file mode 100644 index 5dafa765f16ea5..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/use-list-view-client-ids.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 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 deleted file mode 100644 index 680beafd3c07cd..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/use-list-view-drop-zone.js +++ /dev/null @@ -1,260 +0,0 @@ -/** - * 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 | undefined} 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 deleted file mode 100644 index 09b5e09e4713a3..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/use-list-view-expand-selected-item.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * 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 deleted file mode 100644 index f53f5a4cd4884a..00000000000000 --- a/packages/block-editor/src/components/off-canvas-editor/utils.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * 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-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index a7357311eedbe8..5a451a4a58a2a3 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -4,7 +4,6 @@ import * as globalStyles from './components/global-styles'; import { ExperimentalBlockEditorProvider } from './components/provider'; import { lock } from './lock-unlock'; -import OffCanvasEditor from './components/off-canvas-editor'; import ResizableBoxPopover from './components/resizable-box-popover'; import { ComposedPrivateInserter as PrivateInserter } from './components/inserter'; import { PrivateListView } from './components/list-view'; @@ -19,7 +18,6 @@ export const privateApis = {}; lock( privateApis, { ...globalStyles, ExperimentalBlockEditorProvider, - OffCanvasEditor, PrivateInserter, PrivateListView, ResizableBoxPopover, diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index d5ec18cb4c69ff..5eafc0766ae220 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -59,7 +59,4 @@ @import "./components/preview-options/style.scss"; @import "./components/spacing-sizes-control/style.scss"; -// Experiments. -@import "./components/off-canvas-editor/style.scss"; - @include wordpress-admin-schemes(); From bf80b72b08932900b38e85e060cdf0f5ae96ecf7 Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Fri, 19 May 2023 13:29:55 +1000 Subject: [PATCH 093/131] $revisions_controller is not used. Let's delete it. (#50763) --- .../class-gutenberg-rest-global-styles-controller-6-3.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php index 5eeb0a1014aed6..09b0f4ec65dbf6 100644 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php +++ b/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php @@ -10,14 +10,6 @@ * Base Global Styles REST API Controller. */ class Gutenberg_REST_Global_Styles_Controller_6_3 extends Gutenberg_REST_Global_Styles_Controller_6_2 { - /** - * Revision controller. - * - * @since 6.3.0 - * @var WP_REST_Revisions_Controller - */ - private $revisions_controller; - /** * Prepares links for the request. * From a8da93f061224a93ca6ea0c6ab311021e6dba7ba Mon Sep 17 00:00:00 2001 From: Michael Burridge <mburridge@zyriab.co.uk> Date: Fri, 19 May 2023 07:18:04 +0100 Subject: [PATCH 094/131] Minor updates to theme.json schema pages (#50742) * Minor updates to theme.json schema pages --- docs/manifest.json | 4 ++-- .../theme-json-reference/theme-json-living.md | 23 ++++++++++++++----- .../theme-json-reference/theme-json-v1.md | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index 8b6677d39c5591..d6759f051a6791 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -594,13 +594,13 @@ "parent": "reference-guides" }, { - "title": "Version 2 (living reference)", + "title": "Theme.json Version 2", "slug": "theme-json-living", "markdown_source": "../docs/reference-guides/theme-json-reference/theme-json-living.md", "parent": "theme-json-reference" }, { - "title": "Version 1 Reference", + "title": "Theme.json Version 1 Reference", "slug": "theme-json-v1", "markdown_source": "../docs/reference-guides/theme-json-reference/theme-json-v1.md", "parent": "theme-json-reference" diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 505596819bfb4c..32997395754307 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -1,16 +1,27 @@ -# Version 2 (living reference) +# Theme.json Version 2 -> This is the living specification for the **version 2** of theme.json. This version works with WordPress 5.9 or later, and the latest Gutenberg plugin. +> This is the living specification for **version 2** of `theme.json`. This version works with WordPress 5.9 or later, and the latest Gutenberg plugin. > -> There're related documents you may be interested in: the [theme.json v1](/docs/reference-guides/theme-json-reference/theme-json-v1.md) specification and the [reference to migrate from theme.json v1 to v2](/docs/reference-guides/theme-json-reference/theme-json-migrations.md). +> There are some related documents that you may be interested in: +> - the [theme.json v1](/docs/reference-guides/theme-json-reference/theme-json-v1.md) specification, and +> - the [reference to migrate from theme.json v1 to v2](/docs/reference-guides/theme-json-reference/theme-json-migrations.md). -This reference guide lists the settings and style properties defined in the theme.json schema. See the [theme.json how to guide](/docs/how-to-guides/themes/theme-json.md) for examples and guide on how to use the theme.json file in your theme. +This reference guide lists the settings and style properties defined in the `theme.json` schema. See the [theme.json how to guide](/docs/how-to-guides/themes/theme-json.md) for examples and guidance on how to use the `theme.json` file in your theme. ## Schema -It can be difficult to remember the theme.json settings and properties while you develop, so a JSON scheme was created to help. The schema is available at https://schemas.wp.org/trunk/theme.json +Remembering the `theme.json` settings and properties while you develop can be difficult, so a [JSON schema](https://schemas.wp.org/trunk/theme.json) was created to help. + +Code editors can pick up the schema and can provide helpful hints and suggestions such as tooltips, autocomplete, or schema validation in the editor. To use the schema in Visual Studio Code, add `$schema`: "https://schemas.wp.org/trunk/theme.json" to the beginning of your theme.json file together with a `version` corresponding to the version you wish to use, e.g.: + +``` +{ + "$schema": "https://schemas.wp.org/trunk/theme.json", + "version": 2, + ... +} +``` -Code editors can pick up the schema and can provide help like tooltips, autocomplete, or schema validation in the editor. To use the schema in Visual Studio Code, add "$schema": "https://schemas.wp.org/trunk/theme.json" to the beginning of your theme.json file. <!-- START TOKEN Autogenerated - DO NOT EDIT --> ## Settings diff --git a/docs/reference-guides/theme-json-reference/theme-json-v1.md b/docs/reference-guides/theme-json-reference/theme-json-v1.md index d0ed8a70a31102..3e7096ee420ef6 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-v1.md +++ b/docs/reference-guides/theme-json-reference/theme-json-v1.md @@ -1,4 +1,4 @@ -# Version 1 Reference +# Theme.json Version 1 Reference Theme.json version 2 has been released, see the [theme.json migration guide](/docs/reference-guides/theme-json-reference/theme-json-migrations.md#migrating-from-v1-to-v2) for updating to the latest version. From ac28fa6dcd868bd9d3a0918ffd9105f0febf3828 Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Fri, 19 May 2023 10:18:12 +0200 Subject: [PATCH 095/131] Support negation operator in selectors in the Interactivity API (#50732) * Support negation operator in selectors * Change file block to use negation operator --- lib/experimental/interactivity-api/blocks.php | 2 +- .../block-library/src/file/interactivity.js | 6 ++--- .../src/utils/interactivity/hooks.js | 24 ++++++++++--------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/experimental/interactivity-api/blocks.php b/lib/experimental/interactivity-api/blocks.php index 755c1d1d4fa7d7..647bad93c0d03b 100644 --- a/lib/experimental/interactivity-api/blocks.php +++ b/lib/experimental/interactivity-api/blocks.php @@ -23,7 +23,7 @@ function gutenberg_block_core_file_add_directives_to_content( $block_content, $b $processor->next_tag(); $processor->set_attribute( 'data-wp-island', '' ); $processor->next_tag( 'object' ); - $processor->set_attribute( 'data-wp-bind.hidden', 'selectors.core.file.hasNoPdfPreview' ); + $processor->set_attribute( 'data-wp-bind.hidden', '!selectors.core.file.hasPdfPreview' ); $processor->set_attribute( 'hidden', true ); return $processor->get_updated_html(); } diff --git a/packages/block-library/src/file/interactivity.js b/packages/block-library/src/file/interactivity.js index cf9ae41002b276..8060f7addf3a2e 100644 --- a/packages/block-library/src/file/interactivity.js +++ b/packages/block-library/src/file/interactivity.js @@ -2,15 +2,13 @@ * Internal dependencies */ import { store } from '../utils/interactivity'; -import { browserSupportsPdfs } from './utils'; +import { browserSupportsPdfs as hasPdfPreview } from './utils'; store( { selectors: { core: { file: { - hasNoPdfPreview() { - return ! browserSupportsPdfs(); - }, + hasPdfPreview, }, }, }, diff --git a/packages/block-library/src/utils/interactivity/hooks.js b/packages/block-library/src/utils/interactivity/hooks.js index ca3bd20964d511..072c01641a59c0 100644 --- a/packages/block-library/src/utils/interactivity/hooks.js +++ b/packages/block-library/src/utils/interactivity/hooks.js @@ -19,26 +19,28 @@ export const directive = ( name, cb ) => { // Resolve the path to some property of the store object. const resolve = ( path, ctx ) => { - // If path starts with !, remove it and save a flag. - const hasNegationOperator = - path[ 0 ] === '!' && !! ( path = path.slice( 1 ) ); let current = { ...store, context: ctx }; path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) ); - return hasNegationOperator ? ! current : current; + return current; }; // Generate the evaluate function. const getEvaluate = ( { ref } = {} ) => ( path, extraArgs = {} ) => { + // If path starts with !, remove it and save a flag. + const hasNegationOperator = + path[ 0 ] === '!' && !! ( path = path.slice( 1 ) ); const value = resolve( path, extraArgs.context ); - return typeof value === 'function' - ? value( { - ref: ref.current, - ...store, - ...extraArgs, - } ) - : value; + const returnValue = + typeof value === 'function' + ? value( { + ref: ref.current, + ...store, + ...extraArgs, + } ) + : value; + return hasNegationOperator ? ! returnValue : returnValue; }; // Directive wrapper. From f91be8d47fed17f4968c7b7a3dbb1dd36bca8917 Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Fri, 19 May 2023 11:16:57 +0200 Subject: [PATCH 096/131] Polish experimental navigation block (#50670) * Remove unnecessary `async` in `effects` * Fix page list block and multiple submenus * Use `requestUtils.createPage` in Page list tests * Rename effect to `initMenu` * Fix typo --- lib/experimental/interactivity-api/blocks.php | 25 ++----- .../src/navigation/interactivity.js | 4 +- test/e2e/artifacts/storage-states/admin.json | 67 ++++++++++++++++++- 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/lib/experimental/interactivity-api/blocks.php b/lib/experimental/interactivity-api/blocks.php index 647bad93c0d03b..1c6e7d2b1fbb9b 100644 --- a/lib/experimental/interactivity-api/blocks.php +++ b/lib/experimental/interactivity-api/blocks.php @@ -47,8 +47,8 @@ function gutenberg_block_core_file_add_directives_to_content( $block_content, $b * data-wp-class.has-modal-open="context.core.navigation.isMenuOpen" * data-wp-class.is-menu-open="context.core.navigation.isMenuOpen" * data-wp-bind.aria-hidden="!context.core.navigation.isMenuOpen" - * data-wp-effect="effects.core.navigation.initModal" - * data-wp-on.keydow="actions.core.navigation.handleMenuKeydown" + * data-wp-effect="effects.core.navigation.initMenu" + * data-wp-on.keydown="actions.core.navigation.handleMenuKeydown" * data-wp-on.focusout="actions.core.navigation.handleMenuFocusout" * tabindex="-1" * > @@ -97,21 +97,6 @@ function gutenberg_block_core_navigation_add_directives_to_markup( $block_conten // If the open modal button not found, we handle submenus immediately. $w = new WP_HTML_Tag_Processor( $w->get_updated_html() ); - // Add directives to the menu container. - if ( $w->next_tag( - array( - 'tag_name' => 'UL', - 'class_name' => 'wp-block-navigation__container', - ) - ) ) { - $w->set_attribute( 'data-wp-class.is-menu-open', 'context.core.navigation.isMenuOpen' ); - $w->set_attribute( 'data-wp-bind.aria-hidden', '!context.core.navigation.isMenuOpen' ); - $w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initModal' ); - $w->set_attribute( 'data-wp-on.keydown', 'actions.core.navigation.handleMenuKeydown' ); - $w->set_attribute( 'data-wp-on.focusout', 'actions.core.navigation.handleMenuFocusout' ); - $w->set_attribute( 'tabindex', '-1' ); - }; - gutenberg_block_core_navigation_add_directives_to_submenu( $w ); return $w->get_updated_html(); @@ -127,7 +112,7 @@ function gutenberg_block_core_navigation_add_directives_to_markup( $block_conten $w->set_attribute( 'data-wp-class.has-modal-open', 'context.core.navigation.isMenuOpen' ); $w->set_attribute( 'data-wp-class.is-menu-open', 'context.core.navigation.isMenuOpen' ); $w->set_attribute( 'data-wp-bind.aria-hidden', '!context.core.navigation.isMenuOpen' ); - $w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initModal' ); + $w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initMenu' ); $w->set_attribute( 'data-wp-on.keydown', 'actions.core.navigation.handleMenuKeydown' ); $w->set_attribute( 'data-wp-on.focusout', 'actions.core.navigation.handleMenuFocusout' ); $w->set_attribute( 'tabindex', '-1' ); @@ -191,7 +176,7 @@ function gutenberg_block_core_navigation_add_directives_to_markup( $block_conten * <span>Title</span> * <ul * class="wp-block-navigation__submenu-container" - * data-wp-effect="effects.core.navigation.initModal" + * data-wp-effect="effects.core.navigation.initMenu" * data-wp-on.focusout="actions.core.navigation.handleMenuFocusout" * data-wp-on.keydown="actions.core.navigation.handleMenuKeydown" * > @@ -233,7 +218,7 @@ function gutenberg_block_core_navigation_add_directives_to_submenu( $w ) { 'class_name' => 'wp-block-navigation__submenu-container', ) ) ) { - $w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initModal' ); + $w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initMenu' ); $w->set_attribute( 'data-wp-on.focusout', 'actions.core.navigation.handleMenuFocusout' ); $w->set_attribute( 'data-wp-on.keydown', 'actions.core.navigation.handleMenuKeydown' ); }; diff --git a/packages/block-library/src/navigation/interactivity.js b/packages/block-library/src/navigation/interactivity.js index bf537ceb803a4d..b147d651d032bf 100644 --- a/packages/block-library/src/navigation/interactivity.js +++ b/packages/block-library/src/navigation/interactivity.js @@ -21,7 +21,7 @@ store( { effects: { core: { navigation: { - initModal: async ( { context, ref } ) => { + initMenu: ( { context, ref } ) => { if ( context.core.navigation.isMenuOpen ) { const focusableElements = ref.querySelectorAll( focusableSelectors ); @@ -32,7 +32,7 @@ store( { focusableElements[ focusableElements.length - 1 ]; } }, - focusFirstElement: async ( { context, ref } ) => { + focusFirstElement: ( { context, ref } ) => { if ( context.core.navigation.isMenuOpen ) { ref.querySelector( '.wp-block-navigation-item > *:first-child' diff --git a/test/e2e/artifacts/storage-states/admin.json b/test/e2e/artifacts/storage-states/admin.json index cd90306028e8a5..cae09dbcd0eac7 100644 --- a/test/e2e/artifacts/storage-states/admin.json +++ b/test/e2e/artifacts/storage-states/admin.json @@ -1 +1,66 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"localhost","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wordpress_23778236db82f19306f247e20a353a99","value":"admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C45404d74460259bc9148f2357f2180af488d65921b10ed5981fff860afa5c8ca","domain":"localhost","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":false,"sameSite":"Lax"},{"name":"wordpress_23778236db82f19306f247e20a353a99","value":"admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C45404d74460259bc9148f2357f2180af488d65921b10ed5981fff860afa5c8ca","domain":"localhost","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":false,"sameSite":"Lax"},{"name":"wordpress_logged_in_23778236db82f19306f247e20a353a99","value":"admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C8ace4a8f867bc4e587d5264662296f90fcd133710cd0dd3386a92801816bd5d1","domain":"localhost","path":"/","expires":-1,"httpOnly":true,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"editor%3Dtinymce","domain":"localhost","path":"/","expires":1715809558.14,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1684273558","domain":"localhost","path":"/","expires":1715809558.14,"httpOnly":false,"secure":false,"sameSite":"Lax"}],"nonce":"fe590a7aae","rootURL":"http://localhost:8889/index.php?rest_route=/"} \ No newline at end of file +{ + "cookies": [ + { + "name": "wordpress_test_cookie", + "value": "WP%20Cookie%20check", + "domain": "localhost", + "path": "/", + "expires": -1, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "wordpress_23778236db82f19306f247e20a353a99", + "value": "admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C45404d74460259bc9148f2357f2180af488d65921b10ed5981fff860afa5c8ca", + "domain": "localhost", + "path": "/wp-content/plugins", + "expires": -1, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "wordpress_23778236db82f19306f247e20a353a99", + "value": "admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C45404d74460259bc9148f2357f2180af488d65921b10ed5981fff860afa5c8ca", + "domain": "localhost", + "path": "/wp-admin", + "expires": -1, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "wordpress_logged_in_23778236db82f19306f247e20a353a99", + "value": "admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C8ace4a8f867bc4e587d5264662296f90fcd133710cd0dd3386a92801816bd5d1", + "domain": "localhost", + "path": "/", + "expires": -1, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "wp-settings-1", + "value": "editor%3Dtinymce", + "domain": "localhost", + "path": "/", + "expires": 1715809558.14, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "wp-settings-time-1", + "value": "1684273558", + "domain": "localhost", + "path": "/", + "expires": 1715809558.14, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + } + ], + "nonce": "fe590a7aae", + "rootURL": "http://localhost:8889/index.php?rest_route=/" +} From c1238e4700fe948291e17d1c8cbddd69ab25bfaa Mon Sep 17 00:00:00 2001 From: Glen Davies <glendaviesnz@users.noreply.github.com> Date: Fri, 19 May 2023 21:25:48 +1200 Subject: [PATCH 097/131] Force display of in custom css input boxes to LTR (#50768) --- packages/block-editor/src/components/global-styles/style.scss | 3 +++ packages/edit-site/src/components/global-styles/style.scss | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/block-editor/src/components/global-styles/style.scss b/packages/block-editor/src/components/global-styles/style.scss index 31d22afec37764..46429ea5c47765 100644 --- a/packages/block-editor/src/components/global-styles/style.scss +++ b/packages/block-editor/src/components/global-styles/style.scss @@ -44,6 +44,9 @@ .block-editor-global-styles-advanced-panel__custom-css-input textarea { font-family: $editor_html_font; + // CSS input is always LTR regardless of language. + /*rtl:ignore*/ + direction: ltr; } .block-editor-global-styles-advanced-panel__custom-css-validation-wrapper { diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index 87f0cfdac44d19..8bc5efab2a4acc 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -143,6 +143,9 @@ .components-textarea-control__input { flex: 1 1 auto; + // CSS input is always LTR regardless of language. + /*rtl:ignore*/ + direction: ltr; } } } From fc3c4f506ecf3977e653dd06fc003f16000c74cd Mon Sep 17 00:00:00 2001 From: Marco Ciampini <marco.ciampo@gmail.com> Date: Fri, 19 May 2023 12:45:38 +0200 Subject: [PATCH 098/131] Add new experimental version of DropdownMenu (#49473) * Add Radix deps * Re-export radix dropdown with some custom styles * Add sample story * Start aggregating some components * Improve storybook config * Use better colors * Fix styles * Move types to external file * Namespace all styled radix components under "DropdownMenuStyled" namespace * Use dot icon for radio items instead of check icon * Pass portal props to portal * Add DropdownSubMenu component * Improve stories to use high-level components * Update notes * Comment out unused import * Use menu groups * draft section in contributing guidelines * Use wordpress icons instead of `@radix-ui/react-icons` * Add custom `icon` prop to `DropdownMenuItem` * Add missing alignment property for sub trigger * Extract animation variables * Align colors more closely to WordPress styles * Add border to dropdown container * Increase spacing, use `space` util * Use font utils * Add focus-visible outline * Alternative styling for submenu triggers * Pick only selected props for DropdownMenu * Rewrite Storybook example to support controls * DropdownMenuItem: Pick only selected props, support prefix/suffix * Remove forwarded refs * Playing around with asChild and ref forwarding * Updated remaining props * Add suffix to checkbox / radio items * Tweak styles (remove arrow, box shadow, center Storybook example) * Try separate submenu trigger component * Move layout wrapper to decorator * Do not use `asChild` for indicator wrappers * Remove solved TODOs * RTL support * Add support for reduced motion * Expose component via lock APIs * Polish styles * Generalize storybook * Focus/hover styles * Refine styles * Tweak storybook example (move separators out of groups, better item text) * Add unit tests * Move legacy implementation to v1 subfolder, move new implementation to v2 folder * READMEs * Tidy up types and JSDocs * CHANGELOG * Update Storybook example title * Fix imports * Fix storybook path * Fix test import path * Fix manifest.json * Move legacy files back to where they were, v2 to separate dropdown-menu-v2 folder, fix docs * Update CHANGELOG and fix formatting * Use WP Button instead of custom button * Remove unnecessary decorator with shared context, using private state instead * Remove opinionated wrapper centering styles * Update READMEs: remove v1, add experimental callout * Fix OG DropdownMenu import syntax to be consistent with TypeScript imports across the package --------- Co-authored-by: Lena Morita <lena@jaguchi.com> --- docs/tool/manifest.js | 1 + package-lock.json | 333 +++++++ packages/components/CHANGELOG.md | 1 + packages/components/CONTRIBUTING.md | 10 + packages/components/package.json | 3 +- .../components/src/dropdown-menu-v2/README.md | 392 +++++++++ .../components/src/dropdown-menu-v2/index.tsx | 241 ++++++ .../src/dropdown-menu-v2/stories/index.tsx | 193 +++++ .../components/src/dropdown-menu-v2/styles.ts | 263 ++++++ .../src/dropdown-menu-v2/test/index.tsx | 816 ++++++++++++++++++ .../components/src/dropdown-menu-v2/types.ts | 250 ++++++ .../src/dropdown-menu/stories/index.tsx | 3 +- .../src/dropdown-menu/test/index.tsx | 2 +- packages/components/src/private-apis.ts | 22 + 14 files changed, 2527 insertions(+), 3 deletions(-) create mode 100644 packages/components/src/dropdown-menu-v2/README.md create mode 100644 packages/components/src/dropdown-menu-v2/index.tsx create mode 100644 packages/components/src/dropdown-menu-v2/stories/index.tsx create mode 100644 packages/components/src/dropdown-menu-v2/styles.ts create mode 100644 packages/components/src/dropdown-menu-v2/test/index.tsx create mode 100644 packages/components/src/dropdown-menu-v2/types.ts diff --git a/docs/tool/manifest.js b/docs/tool/manifest.js index e407c47e7284be..d3ed5d61dc0bb1 100644 --- a/docs/tool/manifest.js +++ b/docs/tool/manifest.js @@ -14,6 +14,7 @@ const componentPaths = glob( 'packages/components/src/*/**/README.md', { '**/src/ui/**/README.md', 'packages/components/src/theme/README.md', 'packages/components/src/view/README.md', + 'packages/components/src/dropdown-menu-v2/README.md', ], } ); const packagePaths = glob( 'packages/*/package.json' ) diff --git a/package-lock.json b/package-lock.json index 040fe3911f3772..041a64c8600e77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7125,6 +7125,67 @@ "@babel/runtime": "^7.13.10" } }, + "@radix-ui/react-arrow": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz", + "integrity": "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.2" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } + } + }, + "@radix-ui/react-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.2.tgz", + "integrity": "sha512-s8WdQQ6wNXpaxdZ308KSr8fEWGrg4un8i4r/w7fhiS4ElRNjk5rRcl0/C6TANG2LvLOGIxtzo/jAg6Qf73TEBw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-slot": "1.0.1" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } + } + }, "@radix-ui/react-compose-refs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", @@ -7163,6 +7224,14 @@ "react-remove-scroll": "2.5.4" } }, + "@radix-ui/react-direction": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz", + "integrity": "sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, "@radix-ui/react-dismissable-layer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz", @@ -7176,6 +7245,41 @@ "@radix-ui/react-use-escape-keydown": "1.0.0" } }, + "@radix-ui/react-dropdown-menu": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.4.tgz", + "integrity": "sha512-y6AT9+MydyXcByivdK1+QpjWoKaC7MLjkS/cH1Q3keEyMvDkiY85m8o2Bi6+Z1PPUlCsMULopxagQOSfN0wahg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-menu": "2.0.4", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } + } + }, "@radix-ui/react-focus-guards": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz", @@ -7204,6 +7308,166 @@ "@radix-ui/react-use-layout-effect": "1.0.0" } }, + "@radix-ui/react-menu": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.4.tgz", + "integrity": "sha512-mzKR47tZ1t193trEqlQoJvzY4u9vYfVH16ryBrVrCAGZzkgyWnMQYEZdUkM7y8ak9mrkKtJiqB47TlEnubeOFQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-collection": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.3", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.2", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-popper": "1.1.1", + "@radix-ui/react-portal": "1.0.2", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-roving-focus": "1.0.3", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "dependencies": { + "@radix-ui/react-dismissable-layer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.3.tgz", + "integrity": "sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-escape-keydown": "1.0.2" + } + }, + "@radix-ui/react-focus-scope": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.2.tgz", + "integrity": "sha512-spwXlNTfeIprt+kaEWE/qYuYT3ZAqJiAGjN/JgdvgVDTu8yc+HuX+WOWXrKliKnLnwck0F6JDkqIERncnih+4A==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0" + } + }, + "@radix-ui/react-portal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.2.tgz", + "integrity": "sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.2" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + }, + "@radix-ui/react-use-escape-keydown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.2.tgz", + "integrity": "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + } + }, + "react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "requires": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + } + } + } + }, + "@radix-ui/react-popper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.1.tgz", + "integrity": "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "dependencies": { + "@floating-ui/core": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz", + "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==" + }, + "@floating-ui/dom": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz", + "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==", + "requires": { + "@floating-ui/core": "^0.7.3" + } + }, + "@floating-ui/react-dom": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.2.tgz", + "integrity": "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==", + "requires": { + "@floating-ui/dom": "^0.5.3", + "use-isomorphic-layout-effect": "^1.1.1" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } + } + }, "@radix-ui/react-portal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.0.tgz", @@ -7232,6 +7496,43 @@ "@radix-ui/react-slot": "1.0.0" } }, + "@radix-ui/react-roving-focus": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.3.tgz", + "integrity": "sha512-stjCkIoMe6h+1fWtXlA6cRfikdBzCLp3SnVk7c48cv/uy3DTGoXhN76YaOYUJuy3aEDvDIKwKR5KSmvrtPvQPQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-collection": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } + } + }, "@radix-ui/react-slot": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", @@ -7275,6 +7576,32 @@ "@babel/runtime": "^7.13.10" } }, + "@radix-ui/react-use-rect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz", + "integrity": "sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.0" + } + }, + "@radix-ui/react-use-size": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz", + "integrity": "sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + } + }, + "@radix-ui/rect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.0.tgz", + "integrity": "sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, "@react-native-clipboard/clipboard": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.9.0.tgz", @@ -17003,6 +17330,7 @@ "@emotion/styled": "^11.6.0", "@emotion/utils": "^1.0.0", "@floating-ui/react-dom": "1.0.0", + "@radix-ui/react-dropdown-menu": "^2.0.4", "@use-gesture/react": "^10.2.24", "@wordpress/a11y": "file:packages/a11y", "@wordpress/compose": "file:packages/compose", @@ -56292,6 +56620,11 @@ "tslib": "^2.0.0" } }, + "use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==" + }, "use-lilius": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/use-lilius/-/use-lilius-2.0.1.tgz", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6c00d6e22027bc..257d0545f42236 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -10,6 +10,7 @@ - `Modal`: Remove children container's unused class name ([#50655](https://github.com/WordPress/gutenberg/pull/50655)). - `DropdownMenu`: Convert to TypeScript ([#50187](https://github.com/WordPress/gutenberg/pull/50187)). +- Added experimental v2 of `DropdownMenu` ([#49473](https://github.com/WordPress/gutenberg/pull/49473)). ## 24.0.0 (2023-05-10) diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md index 8464a4a7327447..bf7569a19ddba0 100644 --- a/packages/components/CONTRIBUTING.md +++ b/packages/components/CONTRIBUTING.md @@ -20,6 +20,7 @@ For an example of a component that follows these requirements, take a look at [` - [README example](#README-example) - [Folder structure](#folder-structure) - [TypeScript migration guide](#refactoring-a-component-to-typescript) +- [Using Radix UI primitives](#using-radix-ui-primitives) ## Introducing new components @@ -639,3 +640,12 @@ Given a component folder (e.g. `packages/components/src/unit-control`): 11. Convert unit tests. 1. Rename test file extensions from `.js` to `.tsx`. 2. Fix all TypeScript errors. + +## Using Radix UI primitives + +Useful links: + +- [online docs](https://www.radix-ui.com/docs/primitives/overview/introduction) +- [repo](https://github.com/radix-ui/primitives) — useful for: + - inspecting source code + - running storybook examples (`yarn install && yarn dev`) diff --git a/packages/components/package.json b/packages/components/package.json index 0077e73d3f1da3..0339100948a019 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -38,6 +38,7 @@ "@emotion/styled": "^11.6.0", "@emotion/utils": "^1.0.0", "@floating-ui/react-dom": "1.0.0", + "@radix-ui/react-dropdown-menu": "^2.0.4", "@use-gesture/react": "^10.2.24", "@wordpress/a11y": "file:../a11y", "@wordpress/compose": "file:../compose", @@ -85,4 +86,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/packages/components/src/dropdown-menu-v2/README.md b/packages/components/src/dropdown-menu-v2/README.md new file mode 100644 index 00000000000000..c3896f8ca11093 --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/README.md @@ -0,0 +1,392 @@ +# `DropdownMenu` (v2) + +<div class="callout callout-alert"> +This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. +</div> + +`DropdownMenu` displays a menu to the user (such as a set of actions or functions) triggered by a button. + + +## Design guidelines + +### Usage + +#### When to use a DropdownMenu + +Use a DropdownMenu when you want users to: + +- Choose an action or change a setting from a list, AND +- Only see the available choices contextually. + +`DropdownMenu` is a React component to render an expandable menu of buttons. It is similar in purpose to a `<select>` element, with the distinction that it does not maintain a value. Instead, each option behaves as an action button. + +If you need to display all the available options at all times, consider using a Toolbar instead. Use a `DropdownMenu` to display a list of actions after the user interacts with a button. + +**Do** +Use a `DropdownMenu` to display a list of actions after the user interacts with an icon. + +**Don’t** use a `DropdownMenu` for important actions that should always be visible. Use a `Toolbar` instead. + +**Don’t** +Don’t use a `DropdownMenu` for frequently used actions. Use a `Toolbar` instead. + +#### Behavior + +Generally, the parent button should indicate that interacting with it will show a `DropdownMenu`. + +The parent button should retain the same visual styling regardless of whether the `DropdownMenu` is displayed or not. + +#### Placement + +The `DropdownMenu` should typically appear directly below, or below and to the left of, the parent button. If there isn’t enough space below to display the full `DropdownMenu`, it can be displayed instead above the parent button. + +## Development guidelines + +This component is still highly experimental, and it's not normally accessible to consumers of the `@wordpress/components` package. + +The component exposes a set of components that are meant to be used in combination with each other in order to implement a `DropdownMenu` correctly. + +### `DropdownMenu` + +The root component, used to specify the menu's trigger and its contents. + +#### Props + +The component accepts the following props: + +##### `trigger`: `React.ReactNode` + +The trigger button + +- Required: yes + +##### `children`: `React.ReactNode` + +The contents of the dropdown + +- Required: yes + +##### `defaultOpen`: `boolean` + +The open state of the dropdown menu when it is initially rendered. Use when you do not need to control its open state. + +- Required: no + +##### `open`: `boolean` + +The controlled open state of the dropdown menu. Must be used in conjunction with `onOpenChange` + +- Required: no + +##### `onOpenChange`: `(open: boolean) => void` + +Event handler called when the open state of the dropdown menu changes. + +- Required: no + +##### `modal`: `boolean` + +The modality of the dropdown menu. When set to true, interaction with outside elements will be disabled and only menu content will be visible to screen readers. + +- Required: no +- Default: `true` + +##### `side`: `"bottom" | "left" | "right" | "top"` + +The preferred side of the trigger to render against when open. Will be reversed when collisions occur and avoidCollisions is enabled. + +- Required: no +- Default: `"bottom"` + +##### `sideOffset`: `number` + +The distance in pixels from the trigger. + +- Required: no +- Default: `0` + +##### `align`: `"end" | "start" | "center"` + +The preferred alignment against the trigger. May change when collisions occur. + +- Required: no +- Default: `"center"` + +##### `alignOffset`: `number` + +An offset in pixels from the "start" or "end" alignment options. + +- Required: no +- Default: `0` + +### `DropdownMenuItem` + +Used to render a menu item. + +#### Props + +The component accepts the following props: + +##### `children`: `React.ReactNode` + +The contents of the item + +- Required: yes + +##### `disabled`: `boolean` + +- Required: no +- Default: `false` + +##### `onSelect`: `(event: Event) => void` + +Event handler called when the user selects an item (via mouse or keyboard). Calling `event.preventDefault` in this handler will prevent the dropdown menu from closing when selecting that item. + +- Required: no + +##### `textValue`: `string` + +Optional text used for typeahead purposes. By default the typeahead behavior will use the `.textContent` of the item. Use this when the content is complex, or you have non-textual content inside. + +- Required: no + +##### `prefix`: `React.ReactNode` + +The contents of the item's prefix. + +- Required: no + +##### `suffix`: `React.ReactNode` + +The contents of the item's suffix. + +- Required: no + +### `DropdownSubMenu` + +Used to render a nested submenu. + +#### Props + +The component accepts the following props: +##### `trigger`: `React.ReactNode` + +The contents rendered inside the trigger. The trigger should be an instance of the `DropdownSubMenuTrigger` component. + +- Required: yes + +##### `children`: `React.ReactNode` + +The contents of the dropdown + +- Required: yes + +##### `defaultOpen`: `boolean` + +The open state of the dropdown menu when it is initially rendered. Use when you do not need to control its open state. + +- Required: no + +##### `open`: `boolean` + +The controlled open state of the dropdown menu. Must be used in conjunction with `onOpenChange` + +- Required: no + +##### `onOpenChange`: `(open: boolean) => void` + +Event handler called when the open state of the dropdown menu changes. + +- Required: no + +##### `disabled`: `boolean` + +When `true`, prevents the user from interacting with the item. + +- Required: no + +##### `textValue`: `string` + +Optional text used for typeahead purposes for the trigger. By default the typeahead behavior will use the `.textContent` of the trigger. Use this when the content is complex, or you have non-textual content inside. + +- Required: no + +### `DropdownSubMenuTrigger` + +Used to render a submenu trigger. + +#### Props + +The component accepts the following props: + +##### `children`: `React.ReactNode` + +The contents of the item + +- Required: yes + +##### `prefix`: `React.ReactNode` + +The contents of the item's prefix. + +- Required: no + +##### `suffix`: `React.ReactNode` + +The contents of the item's suffix. + +- Default: a chevron icon +- Required: The standard chevron icon for a submenu trigger + +### `DropdownMenuCheckboxItem` + +Used to render a checkbox item. + +#### Props + +The component accepts the following props: + +##### `children`: `React.ReactNode` + +The contents of the checkbox item + +- Required: yes + +##### `checked`: `boolean` + +The controlled checked state of the item. Must be used in conjunction with `onCheckedChange`. + +- Required: no +- Default: `false` + +##### `onCheckedChange`: `(checked: boolean) => void)` + +Event handler called when the checked state changes. + +- Required: no + +##### `disabled`: `boolean` + +When `true`, prevents the user from interacting with the item. + +- Required: no + +##### `onSelect`: `(event: Event) => void` + +Event handler called when the user selects an item (via mouse or keyboard). Calling `event.preventDefault` in this handler will prevent the dropdown menu from closing when selecting that item. + +- Required: no + +##### `textValue`: `string` + +Optional text used for typeahead purposes. By default the typeahead behavior will use the `.textContent` of the item. Use this when the content is complex, or you have non-textual content inside. + +- Required: no + +##### `suffix`: `React.ReactNode` + +The contents of the checkbox item's suffix. + +- Required: no + +### `DropdownMenuRadioGroup` + +Used to render a radio group. + +#### Props + +The component accepts the following props: + +##### `children`: `React.ReactNode` + +The contents of the radio group + +- Required: yes + +##### `value`: `string` + +The value of the selected item in the group. + +- Required: no + +##### `onValueChange`: `(value: string) => void` + +Event handler called when the value changes. + +- Required: no + +### `DropdownMenuRadioItem` + +Used to render a radio item. + +#### Props + +The component accepts the following props: + +##### `children`: `React.ReactNode` + +The contents of the item. + +- Required: yes + +##### `value`: `string` + +The unique value of the item. + +- Required: yes + +##### `disabled`: `boolean` + +When `true`, prevents the user from interacting with the item. + +- Required: no + +##### `onSelect`: `(event: Event) => void` + +Event handler called when the user selects an item (via mouse or keyboard). Calling `event.preventDefault` in this handler will prevent the dropdown menu from closing when selecting that item. + +- Required: no + +##### `textValue`: `string` + +Optional text used for typeahead purposes. By default the typeahead behavior will use the `.textContent` of the item. Use this when the content is complex, or you have non-textual content inside. + +- Required: no + +##### `suffix`: `React.ReactNode + +The contents of the radio item's suffix. + +- Required: no + +### `DropdownMenuLabel` + +Used to render a group label. + +#### Props + +The component accepts the following props: + +##### `children`: `React.ReactNode` + +The contents of the group. + +- Required: yes + +### `DropdownMenuGroup` + +Used to group menu items. + +#### Props + +The component accepts the following props: + +##### `children`: `React.ReactNode` + +The contents of the group. + +- Required: yes + +### `DropdownMenuSeparatorProps` + +Used to render a visual separator. diff --git a/packages/components/src/dropdown-menu-v2/index.tsx b/packages/components/src/dropdown-menu-v2/index.tsx new file mode 100644 index 00000000000000..7a0197f69fc3dd --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/index.tsx @@ -0,0 +1,241 @@ +/** + * External dependencies + */ +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; + +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; +import { isRTL } from '@wordpress/i18n'; +import { check, chevronRightSmall, lineSolid } from '@wordpress/icons'; +import { SVG, Circle } from '@wordpress/primitives'; + +/** + * Internal dependencies + */ +import Icon from '../icon'; +import * as DropdownMenuStyled from './styles'; +import type { + DropdownMenuProps, + DropdownSubMenuProps, + DropdownMenuItemProps, + DropdownMenuLabelProps, + DropdownMenuGroupProps, + DropdownMenuCheckboxItemProps, + DropdownMenuRadioGroupProps, + DropdownMenuRadioItemProps, + DropdownMenuSeparatorProps, + DropdownSubMenuTriggerProps, +} from './types'; + +// Menu content's side padding + 4px +const SUB_MENU_OFFSET_SIDE = 12; +// Opposite amount of the top padding of the menu item +const SUB_MENU_OFFSET_ALIGN = -8; + +/** + * `DropdownMenu` displays a menu to the user (such as a set of actions + * or functions) triggered by a button. + */ +export const DropdownMenu = ( { + // Root props + defaultOpen, + open, + onOpenChange, + modal = true, + // Content positioning props + side = 'bottom', + sideOffset = 0, + align = 'center', + alignOffset = 0, + // Render props + children, + trigger, +}: DropdownMenuProps ) => { + return ( + <DropdownMenuPrimitive.Root + defaultOpen={ defaultOpen } + open={ open } + onOpenChange={ onOpenChange } + modal={ modal } + dir={ isRTL() ? 'rtl' : 'ltr' } + > + <DropdownMenuPrimitive.Trigger asChild> + { trigger } + </DropdownMenuPrimitive.Trigger> + <DropdownMenuPrimitive.Portal> + <DropdownMenuStyled.Content + side={ side } + align={ align } + sideOffset={ sideOffset } + alignOffset={ alignOffset } + loop={ true } + > + { children } + </DropdownMenuStyled.Content> + </DropdownMenuPrimitive.Portal> + </DropdownMenuPrimitive.Root> + ); +}; + +export const DropdownSubMenuTrigger = ( { + prefix, + suffix = ( + <DropdownMenuStyled.SubmenuRtlChevronIcon + icon={ chevronRightSmall } + size={ 24 } + /> + ), + children, +}: DropdownSubMenuTriggerProps ) => { + return ( + <> + { prefix && ( + <DropdownMenuStyled.ItemPrefixWrapper> + { prefix } + </DropdownMenuStyled.ItemPrefixWrapper> + ) } + { children } + { suffix && ( + <DropdownMenuStyled.ItemSuffixWrapper> + { suffix } + </DropdownMenuStyled.ItemSuffixWrapper> + ) } + </> + ); +}; + +export const DropdownSubMenu = ( { + // Sub props + defaultOpen, + open, + onOpenChange, + // Sub trigger props + disabled, + textValue, + // Render props + children, + trigger, +}: DropdownSubMenuProps ) => { + return ( + <DropdownMenuPrimitive.Sub + defaultOpen={ defaultOpen } + open={ open } + onOpenChange={ onOpenChange } + > + <DropdownMenuStyled.SubTrigger + disabled={ disabled } + textValue={ textValue } + > + { trigger } + </DropdownMenuStyled.SubTrigger> + <DropdownMenuPrimitive.Portal> + <DropdownMenuStyled.SubContent + loop + sideOffset={ SUB_MENU_OFFSET_SIDE } + alignOffset={ SUB_MENU_OFFSET_ALIGN } + > + { children } + </DropdownMenuStyled.SubContent> + </DropdownMenuPrimitive.Portal> + </DropdownMenuPrimitive.Sub> + ); +}; + +export const DropdownMenuLabel = ( props: DropdownMenuLabelProps ) => ( + <DropdownMenuStyled.Label { ...props } /> +); + +export const DropdownMenuGroup = ( props: DropdownMenuGroupProps ) => ( + <DropdownMenuPrimitive.Group { ...props } /> +); + +export const DropdownMenuItem = forwardRef( + ( + { children, prefix, suffix, ...props }: DropdownMenuItemProps, + forwardedRef: React.ForwardedRef< any > + ) => { + return ( + <DropdownMenuStyled.Item { ...props } ref={ forwardedRef }> + { prefix && ( + <DropdownMenuStyled.ItemPrefixWrapper> + { prefix } + </DropdownMenuStyled.ItemPrefixWrapper> + ) } + { children } + { suffix && ( + <DropdownMenuStyled.ItemSuffixWrapper> + { suffix } + </DropdownMenuStyled.ItemSuffixWrapper> + ) } + </DropdownMenuStyled.Item> + ); + } +); + +export const DropdownMenuCheckboxItem = ( { + children, + checked = false, + suffix, + ...props +}: DropdownMenuCheckboxItemProps ) => { + return ( + <DropdownMenuStyled.CheckboxItem { ...props } checked={ checked }> + <DropdownMenuStyled.ItemPrefixWrapper> + <DropdownMenuStyled.ItemIndicator> + { ( checked === 'indeterminate' || checked === true ) && ( + <Icon + icon={ + checked === 'indeterminate' ? lineSolid : check + } + size={ 24 } + /> + ) } + </DropdownMenuStyled.ItemIndicator> + </DropdownMenuStyled.ItemPrefixWrapper> + { children } + { suffix && ( + <DropdownMenuStyled.ItemSuffixWrapper> + { suffix } + </DropdownMenuStyled.ItemSuffixWrapper> + ) } + </DropdownMenuStyled.CheckboxItem> + ); +}; + +export const DropdownMenuRadioGroup = ( + props: DropdownMenuRadioGroupProps +) => <DropdownMenuPrimitive.RadioGroup { ...props } />; + +const radioDot = ( + <SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <Circle cx={ 12 } cy={ 12 } r={ 3 } fill="currentColor"></Circle> + </SVG> +); + +export const DropdownMenuRadioItem = ( { + children, + suffix, + ...props +}: DropdownMenuRadioItemProps ) => { + return ( + <DropdownMenuStyled.RadioItem { ...props }> + <DropdownMenuStyled.ItemPrefixWrapper> + <DropdownMenuStyled.ItemIndicator> + <Icon icon={ radioDot } size={ 22 } /> + </DropdownMenuStyled.ItemIndicator> + </DropdownMenuStyled.ItemPrefixWrapper> + { children } + { suffix && ( + <DropdownMenuStyled.ItemSuffixWrapper> + { suffix } + </DropdownMenuStyled.ItemSuffixWrapper> + ) } + </DropdownMenuStyled.RadioItem> + ); +}; + +export const DropdownMenuSeparator = ( props: DropdownMenuSeparatorProps ) => ( + <DropdownMenuStyled.Separator { ...props } /> +); diff --git a/packages/components/src/dropdown-menu-v2/stories/index.tsx b/packages/components/src/dropdown-menu-v2/stories/index.tsx new file mode 100644 index 00000000000000..1171273028f9e1 --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/stories/index.tsx @@ -0,0 +1,193 @@ +/** + * External dependencies + */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import styled from '@emotion/styled'; + +/** + * Internal dependencies + */ +import { + DropdownMenu, + DropdownMenuItem, + DropdownSubMenu, + DropdownMenuSeparator, + DropdownMenuCheckboxItem, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownSubMenuTrigger, +} from '..'; +import Button from '../../button'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { menu, wordpress } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import Icon from '../../icon'; + +const meta: ComponentMeta< typeof DropdownMenu > = { + title: 'Components (Experimental)/DropdownMenu v2', + component: DropdownMenu, + subcomponents: { + DropdownMenuItem, + DropdownSubMenu, + DropdownSubMenuTrigger, + DropdownMenuSeparator, + DropdownMenuCheckboxItem, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + }, + argTypes: { + children: { control: { type: null } }, + trigger: { control: { type: null } }, + }, + parameters: { + actions: { argTypesRegex: '^on.*' }, + controls: { expanded: true }, + docs: { source: { state: 'open', excludeDecorators: true } }, + }, + decorators: [ + // Layout wrapper + ( Story ) => ( + <div + style={ { + minHeight: '300px', + } } + > + <Story /> + </div> + ), + ], +}; +export default meta; + +const ItemHelpText = styled.span` + font-size: 10px; + color: #777; + + /* "> * > &" syntax is to target only immediate parent menu item */ + [data-highlighted] > * > &, + [data-state='open'] > * > &, + [data-disabled] > * & { + color: inherit; + } +`; + +const CheckboxItemsGroup = () => { + const [ itemOneChecked, setItemOneChecked ] = useState( true ); + const [ itemTwoChecked, setItemTwoChecked ] = useState( false ); + + return ( + <DropdownMenuGroup> + <DropdownMenuLabel>Checkbox group label</DropdownMenuLabel> + <DropdownMenuCheckboxItem + checked={ itemOneChecked } + onCheckedChange={ setItemOneChecked } + suffix={ <span>⌘+B</span> } + > + Checkbox item one + </DropdownMenuCheckboxItem> + + <DropdownMenuCheckboxItem + checked={ itemTwoChecked } + onCheckedChange={ setItemTwoChecked } + > + Checkbox item two + </DropdownMenuCheckboxItem> + </DropdownMenuGroup> + ); +}; + +const RadioItemsGroup = () => { + const [ radioValue, setRadioValue ] = useState( 'radio-one' ); + + return ( + <DropdownMenuRadioGroup + value={ radioValue } + onValueChange={ setRadioValue } + > + <DropdownMenuLabel>Radio group label</DropdownMenuLabel> + <DropdownMenuRadioItem value="radio-one"> + Radio item one + </DropdownMenuRadioItem> + <DropdownMenuRadioItem value="radio-two"> + Radio item two + </DropdownMenuRadioItem> + </DropdownMenuRadioGroup> + ); +}; + +const Template: ComponentStory< typeof DropdownMenu > = ( props ) => ( + <DropdownMenu { ...props } /> +); +export const Default = Template.bind( {} ); +Default.args = { + trigger: <Button __next40pxDefaultSize label="Open menu" icon={ menu } />, + sideOffset: 12, + children: ( + <> + <DropdownMenuGroup> + <DropdownMenuItem>Menu item</DropdownMenuItem> + <DropdownMenuItem + prefix={ <Icon icon={ wordpress } size={ 18 } /> } + > + Menu item with prefix + </DropdownMenuItem> + <DropdownMenuItem suffix={ <span>⌥⌘T</span> }> + Menu item with suffix + </DropdownMenuItem> + <DropdownMenuItem disabled>Disabled menu item</DropdownMenuItem> + <DropdownSubMenu + trigger={ + <DropdownSubMenuTrigger>Submenu</DropdownSubMenuTrigger> + } + > + <DropdownMenuItem suffix={ <span>⌘+S</span> }> + Submenu item with suffix + </DropdownMenuItem> + <DropdownMenuItem> + <div + style={ { + display: 'inline-flex', + flexDirection: 'column', + } } + > + Submenu item + <ItemHelpText> + With additional custom text + </ItemHelpText> + </div> + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownSubMenu + trigger={ + <DropdownSubMenuTrigger> + Second level submenu + </DropdownSubMenuTrigger> + } + > + <DropdownMenuItem>Submenu item</DropdownMenuItem> + <DropdownMenuItem>Submenu item</DropdownMenuItem> + </DropdownSubMenu> + </DropdownSubMenu> + </DropdownMenuGroup> + + <DropdownMenuSeparator /> + + <CheckboxItemsGroup /> + + <DropdownMenuSeparator /> + + <RadioItemsGroup /> + </> + ), +}; diff --git a/packages/components/src/dropdown-menu-v2/styles.ts b/packages/components/src/dropdown-menu-v2/styles.ts new file mode 100644 index 00000000000000..c8843d052ec724 --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/styles.ts @@ -0,0 +1,263 @@ +/** + * External dependencies + */ +import styled from '@emotion/styled'; +import { css, keyframes } from '@emotion/react'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; + +/** + * Internal dependencies + */ +import { COLORS, font, rtl } from '../utils'; +import { space } from '../ui/utils/space'; +import Icon from '../icon'; + +const ANIMATION_PARAMS = { + SLIDE_AMOUNT: '2px', + DURATION: '400ms', + EASING: 'cubic-bezier( 0.16, 1, 0.3, 1 )', +}; + +const ITEM_PREFIX_WIDTH = space( 7 ); +const ITEM_PADDING_INLINE_START = space( 2 ); +const ITEM_PADDING_INLINE_END = space( 2.5 ); + +const slideUpAndFade = keyframes( { + '0%': { + opacity: 0, + transform: `translateY(${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, + }, + '100%': { opacity: 1, transform: 'translateY(0)' }, +} ); + +const slideRightAndFade = keyframes( { + '0%': { + opacity: 0, + transform: `translateX(-${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, + }, + '100%': { opacity: 1, transform: 'translateX(0)' }, +} ); + +const slideDownAndFade = keyframes( { + '0%': { + opacity: 0, + transform: `translateY(-${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, + }, + '100%': { opacity: 1, transform: 'translateY(0)' }, +} ); + +const slideLeftAndFade = keyframes( { + '0%': { + opacity: 0, + transform: `translateX(${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, + }, + '100%': { opacity: 1, transform: 'translateX(0)' }, +} ); + +const baseContent = css` + min-width: 220px; + background-color: ${ COLORS.ui.background }; + border-radius: 6px; + padding: ${ space( 2 ) }; + box-shadow: 0.1px 4px 16.4px -0.5px rgba( 0, 0, 0, 0.1 ), + 0px 5.5px 7.8px -0.3px rgba( 0, 0, 0, 0.1 ), + 0px 2.7px 3.8px -0.2px rgba( 0, 0, 0, 0.1 ), + 0px 0.7px 1px rgba( 0, 0, 0, 0.1 ); + animation-duration: ${ ANIMATION_PARAMS.DURATION }; + animation-timing-function: ${ ANIMATION_PARAMS.EASING }; + will-change: transform, opacity; + + &[data-side='top'] { + animation-name: ${ slideDownAndFade }; + } + + &[data-side='right'] { + animation-name: ${ slideLeftAndFade }; + } + + &[data-side='bottom'] { + animation-name: ${ slideUpAndFade }; + } + + &[data-side='left'] { + animation-name: ${ slideRightAndFade }; + } + + @media ( prefers-reduced-motion ) { + animation-duration: 0s; + } +`; + +const itemPrefix = css` + width: ${ ITEM_PREFIX_WIDTH }; + display: inline-flex; + align-items: center; + justify-content: center; + /* Prefixes don't get affected by the item's inline start padding */ + margin-inline-start: calc( -1 * ${ ITEM_PADDING_INLINE_START } ); + /* + Negative margin allows the suffix to be as tall as the whole item + (incl. padding) before increasing the items' height. This can be useful, + e.g., when using icons that are bigger than 20x20 px + */ + margin-top: ${ space( -2 ) }; + margin-bottom: ${ space( -2 ) }; +`; + +const itemSuffix = css` + width: max-content; + display: inline-flex; + align-items: center; + justify-content: center; + /* Push prefix to the inline-end of the item */ + margin-inline-start: auto; + /* Minimum space between the item's content and suffix */ + padding-inline-start: ${ space( 6 ) }; + /* + Negative margin allows the suffix to be as tall as the whole item + (incl. padding) before increasing the items' height. This can be useful, + e.g., when using icons that are bigger than 20x20 px + */ + margin-top: ${ space( -2 ) }; + margin-bottom: ${ space( -2 ) }; + + /* + Override color in normal conditions, but inherit the item's color + for altered conditions. + + TODO: + - For now, used opacity like for disabled item, which makes it work + regardless of the theme + - how do we translate this for themes? Should we have a new variable + for "secondary" text? + */ + opacity: 0.6; + + [data-highlighted] > &, + [data-state='open'] > &, + [data-disabled] > & { + opacity: 1; + } +`; + +export const ItemPrefixWrapper = styled.span` + ${ itemPrefix } +`; + +export const ItemSuffixWrapper = styled.span` + ${ itemSuffix } +`; + +const baseItem = css` + all: unset; + font-size: ${ font( 'default.fontSize' ) }; + font-family: inherit; + font-weight: normal; + line-height: 20px; + color: ${ COLORS.gray[ 900 ] }; + border-radius: 3px; + display: flex; + align-items: center; + padding: ${ space( 2 ) } ${ ITEM_PADDING_INLINE_END } ${ space( 2 ) } + ${ ITEM_PADDING_INLINE_START }; + position: relative; + user-select: none; + outline: none; + + &[data-disabled] { + /* + TODO: + - we need a disabled color in the Theme variables + - design specs use opacity instead of setting a new text color + */ + opacity: 0.5; + pointer-events: none; + } + + &[data-highlighted] { + /* + TODO: reconcile with global focus styles + (incl high contrast mode fallbacks) + */ + + background-color: ${ COLORS.ui.theme }; + color: white; + } + + svg { + fill: currentColor; + } + + &:not( :has( ${ ItemPrefixWrapper } ) ) { + padding-inline-start: ${ ITEM_PREFIX_WIDTH }; + } +`; + +export const Content = styled( DropdownMenu.Content )` + ${ baseContent } +`; +export const SubContent = styled( DropdownMenu.SubContent )` + ${ baseContent } +`; + +export const Item = styled( DropdownMenu.Item )` + ${ baseItem } +`; +export const CheckboxItem = styled( DropdownMenu.CheckboxItem )` + ${ baseItem } +`; +export const RadioItem = styled( DropdownMenu.RadioItem )` + ${ baseItem } +`; +export const SubTrigger = styled( DropdownMenu.SubTrigger )` + &[data-state='open']:not( [data-highlighted] ) { + /* TODO: use variable */ + background-color: rgba( 56, 88, 233, 0.04 ); + color: ${ COLORS.ui.theme }; + } + + ${ baseItem } +`; + +export const Label = styled( DropdownMenu.Label )` + box-sizing: border-box; + display: flex; + align-items: center; + min-height: ${ space( 8 ) }; + + padding: ${ space( 2 ) } ${ ITEM_PADDING_INLINE_END } ${ space( 2 ) } + ${ ITEM_PREFIX_WIDTH }; + /* TODO: color doesn't match available UI variables */ + color: ${ COLORS.gray[ 700 ] }; + + /* TODO: font size doesn't match available ones via "font" utils */ + font-size: 11px; + line-height: 1.4; + font-weight: 500; + text-transform: uppercase; +`; + +export const Separator = styled( DropdownMenu.Separator )` + height: 1px; + /* TODO: doesn't match border color from variables */ + background-color: ${ COLORS.ui.borderDisabled }; + /* Negative horizontal margin to make separator go from side to side */ + margin: ${ space( 2 ) } 0; +`; + +export const ItemIndicator = styled( DropdownMenu.ItemIndicator )` + display: inline-flex; + align-items: center; + justify-content: center; +`; + +export const SubmenuRtlChevronIcon = styled( Icon )` + ${ rtl( + { + transform: `scaleX(1) translateX(${ space( 2 ) })`, + }, + { + transform: `scaleX(-1) translateX(${ space( 2 ) })`, + } + )() } +`; diff --git a/packages/components/src/dropdown-menu-v2/test/index.tsx b/packages/components/src/dropdown-menu-v2/test/index.tsx new file mode 100644 index 00000000000000..3138c74557ae1a --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/test/index.tsx @@ -0,0 +1,816 @@ +/** + * External dependencies + */ +import { render, screen, waitFor } from '@testing-library/react'; +import { + default as userEvent, + PointerEventsCheckLevel, +} from '@testing-library/user-event'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownSubMenu, + DropdownSubMenuTrigger, +} from '..'; + +const delay = ( delayInMs: number ) => { + return new Promise( ( resolve ) => setTimeout( resolve, delayInMs ) ); +}; + +describe( 'DropdownMenu', () => { + // See https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/ + it( 'should follow the WAI-ARIA spec', async () => { + // Radio and Checkbox items' + const user = userEvent.setup(); + + render( + <DropdownMenu trigger={ <button>Open dropdown</button> }> + <DropdownMenuItem>Dropdown menu item</DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownSubMenu + trigger={ + <DropdownSubMenuTrigger> + Dropdown submenu + </DropdownSubMenuTrigger> + } + > + <DropdownMenuItem>Dropdown submenu item 1</DropdownMenuItem> + <DropdownMenuItem>Dropdown submenu item 2</DropdownMenuItem> + </DropdownSubMenu> + </DropdownMenu> + ); + + const toggleButton = screen.getByRole( 'button', { + name: 'Open dropdown', + } ); + + expect( toggleButton ).toHaveAttribute( 'aria-haspopup', 'menu' ); + expect( toggleButton ).toHaveAttribute( 'aria-expanded', 'false' ); + + await user.click( toggleButton ); + + expect( toggleButton ).toHaveAttribute( 'aria-expanded', 'true' ); + + expect( screen.getByRole( 'menu' ) ).toHaveFocus(); + expect( screen.getByRole( 'separator' ) ).toHaveAttribute( + 'aria-orientation', + 'horizontal' + ); + expect( screen.getAllByRole( 'menuitem' ) ).toHaveLength( 2 ); + + const submenuTrigger = screen.getByRole( 'menuitem', { + name: 'Dropdown submenu', + } ); + expect( submenuTrigger ).toHaveAttribute( 'aria-haspopup', 'menu' ); + expect( submenuTrigger ).toHaveAttribute( 'aria-expanded', 'false' ); + + await user.hover( submenuTrigger ); + + // Wait for the open animation after hovering + await waitFor( () => + expect( screen.getAllByRole( 'menu' ) ).toHaveLength( 2 ) + ); + + expect( submenuTrigger ).toHaveAttribute( 'aria-expanded', 'true' ); + expect( submenuTrigger ).toHaveAttribute( + 'aria-controls', + screen.getAllByRole( 'menu' )[ 1 ].id + ); + } ); + + describe( 'pointer and keyboard interactions', () => { + it( 'should open when clicking the trigger', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu trigger={ <button>Open dropdown</button> }> + <DropdownMenuItem>Dropdown menu item</DropdownMenuItem> + </DropdownMenu> + ); + + const toggleButton = screen.getByRole( 'button', { + name: 'Open dropdown', + } ); + + // DropdownMenu closed, the content is not displayed + expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument(); + expect( screen.queryByRole( 'menuitem' ) ).not.toBeInTheDocument(); + + // Click to open the menu + await user.click( toggleButton ); + + // DropdownMenu open, the content is displayed + expect( screen.getByRole( 'menu' ) ).toBeInTheDocument(); + expect( screen.getByRole( 'menuitem' ) ).toBeInTheDocument(); + } ); + + it( 'should open when pressing the arrow down key on the trigger', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu trigger={ <button>Open dropdown</button> }> + <DropdownMenuItem>Dropdown menu item</DropdownMenuItem> + </DropdownMenu> + ); + + const toggleButton = screen.getByRole( 'button', { + name: 'Open dropdown', + } ); + + // Move focus on the toggle + await user.keyboard( '{Tab}' ); + + expect( toggleButton ).toHaveFocus(); + + // DropdownMenu closed, the content is not displayed + expect( screen.queryByRole( 'menuitem' ) ).not.toBeInTheDocument(); + + await user.keyboard( '{ArrowDown}' ); + + // DropdownMenu open, the content is displayed + expect( screen.getByRole( 'menuitem' ) ).toBeInTheDocument(); + } ); + + it( 'should close when pressing the escape key', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu + defaultOpen + trigger={ <button>Open dropdown</button> } + > + <DropdownMenuItem>Dropdown menu item</DropdownMenuItem> + </DropdownMenu> + ); + + // The menu is focused automatically when `defaultOpen` is set. + expect( screen.getByRole( 'menu' ) ).toHaveFocus(); + + // Pressing esc will close the menu and move focus to the toggle + await user.keyboard( '{Escape}' ); + + expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument(); + expect( + screen.getByRole( 'button', { name: 'Open dropdown' } ) + ).toHaveFocus(); + } ); + + it( 'should close when clicking outside of the content', async () => { + const user = userEvent.setup( { + // Disabling this check otherwise testing-library would complain + // when clicking on document.body to close the dropdown menu. + pointerEventsCheck: PointerEventsCheckLevel.Never, + } ); + + render( + <DropdownMenu + defaultOpen + trigger={ <button>Open dropdown</button> } + > + <DropdownMenuItem>Dropdown menu item</DropdownMenuItem> + </DropdownMenu> + ); + + expect( screen.getByRole( 'menu' ) ).toBeInTheDocument(); + + // Click on the body (ie. outside of the dropdown menu) + await user.click( document.body ); + + expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument(); + } ); + + it( 'should close when clicking on a menu item', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu + defaultOpen + trigger={ <button>Open dropdown</button> } + > + <DropdownMenuItem>Dropdown menu item</DropdownMenuItem> + </DropdownMenu> + ); + + expect( screen.getByRole( 'menu' ) ).toBeInTheDocument(); + + // Clicking a menu item will close the menu + await user.click( screen.getByRole( 'menuitem' ) ); + + expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument(); + } ); + + it( 'should not close when clicking on a disabled menu item', async () => { + const user = userEvent.setup( { + // Disabling this check otherwise testing-library would complain + // when clicking on a disabled element with pointer-events: none + pointerEventsCheck: PointerEventsCheckLevel.Never, + } ); + + render( + <DropdownMenu + defaultOpen + trigger={ <button>Open dropdown</button> } + > + <DropdownMenuItem disabled> + Dropdown menu item + </DropdownMenuItem> + </DropdownMenu> + ); + + expect( screen.getByRole( 'menu' ) ).toBeInTheDocument(); + + // Clicking a disabled menu item won't close the menu + await user.click( screen.getByRole( 'menuitem' ) ); + + expect( screen.getByRole( 'menu' ) ).toBeInTheDocument(); + } ); + + it( 'should reveal submenu content when hovering over the submenu trigger', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu + defaultOpen + trigger={ <button>Open dropdown</button> } + > + <DropdownMenuItem>Dropdown menu item 1</DropdownMenuItem> + <DropdownMenuItem>Dropdown menu item 2</DropdownMenuItem> + <DropdownSubMenu + trigger={ + <DropdownSubMenuTrigger> + Dropdown submenu + </DropdownSubMenuTrigger> + } + > + <DropdownMenuItem> + Dropdown submenu item 1 + </DropdownMenuItem> + <DropdownMenuItem> + Dropdown submenu item 2 + </DropdownMenuItem> + </DropdownSubMenu> + <DropdownMenuItem>Dropdown menu item 3</DropdownMenuItem> + </DropdownMenu> + ); + + // Before hover, submenu items are not rendered + expect( + screen.queryByRole( 'menuitem', { + name: 'Dropdown submenu item 1', + } ) + ).not.toBeInTheDocument(); + + await user.hover( + screen.getByRole( 'menuitem', { name: 'Dropdown submenu' } ) + ); + + // After hover, submenu items are rendered + // Reason for `findByRole`: due to the animation, we've got to wait + // a short amount of time for the submenu to appear + await screen.findByRole( 'menuitem', { + name: 'Dropdown submenu item 1', + } ); + } ); + + it( 'should navigate menu items and subitems using the arrow, spacebar and enter keys', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu + defaultOpen + trigger={ <button>Open dropdown</button> } + > + <DropdownMenuItem>Dropdown menu item 1</DropdownMenuItem> + <DropdownMenuItem>Dropdown menu item 2</DropdownMenuItem> + <DropdownSubMenu + trigger={ + <DropdownSubMenuTrigger> + Dropdown submenu + </DropdownSubMenuTrigger> + } + > + <DropdownMenuItem> + Dropdown submenu item 1 + </DropdownMenuItem> + <DropdownMenuItem> + Dropdown submenu item 2 + </DropdownMenuItem> + </DropdownSubMenu> + <DropdownMenuItem>Dropdown menu item 3</DropdownMenuItem> + </DropdownMenu> + ); + + // The menu is focused automatically when `defaultOpen` is set. + expect( screen.getByRole( 'menu' ) ).toHaveFocus(); + + // Arrow up/down selects menu items + // The selection wraps around from last to first and viceversa + await user.keyboard( '{ArrowDown}' ); + expect( + screen.getByRole( 'menuitem', { name: 'Dropdown menu item 1' } ) + ).toHaveFocus(); + + await user.keyboard( '{ArrowDown}' ); + expect( + screen.getByRole( 'menuitem', { name: 'Dropdown menu item 2' } ) + ).toHaveFocus(); + + await user.keyboard( '{ArrowDown}' ); + expect( + screen.getByRole( 'menuitem', { name: 'Dropdown submenu' } ) + ).toHaveFocus(); + + await user.keyboard( '{ArrowDown}' ); + expect( + screen.getByRole( 'menuitem', { name: 'Dropdown menu item 3' } ) + ).toHaveFocus(); + + await user.keyboard( '{ArrowDown}' ); + expect( + screen.getByRole( 'menuitem', { name: 'Dropdown menu item 1' } ) + ).toHaveFocus(); + + await user.keyboard( '{ArrowUp}' ); + expect( + screen.getByRole( 'menuitem', { name: 'Dropdown menu item 3' } ) + ).toHaveFocus(); + + await user.keyboard( '{ArrowUp}' ); + expect( + screen.getByRole( 'menuitem', { name: 'Dropdown submenu' } ) + ).toHaveFocus(); + + // Arrow right/left can be used to enter/leave submenus + await user.keyboard( '{ArrowRight}' ); + expect( + screen.getByRole( 'menuitem', { + name: 'Dropdown submenu item 1', + } ) + ).toHaveFocus(); + + await user.keyboard( '{ArrowDown}' ); + expect( + screen.getByRole( 'menuitem', { + name: 'Dropdown submenu item 2', + } ) + ).toHaveFocus(); + + await user.keyboard( '{ArrowLeft}' ); + expect( + screen.getByRole( 'menuitem', { + name: 'Dropdown submenu', + } ) + ).toHaveFocus(); + + // Spacebar or enter key can also be used to enter a submenu + await user.keyboard( '{Enter}' ); + expect( + screen.getByRole( 'menuitem', { + name: 'Dropdown submenu item 1', + } ) + ).toHaveFocus(); + + await user.keyboard( '{ArrowLeft}' ); + expect( + screen.getByRole( 'menuitem', { + name: 'Dropdown submenu', + } ) + ).toHaveFocus(); + + await user.keyboard( '{Spacebar}' ); + expect( + screen.getByRole( 'menuitem', { + name: 'Dropdown submenu item 1', + } ) + ).toHaveFocus(); + + await user.keyboard( '{ArrowLeft}' ); + expect( + screen.getByRole( 'menuitem', { + name: 'Dropdown submenu', + } ) + ).toHaveFocus(); + } ); + + it( 'should check menu radio items', async () => { + const user = userEvent.setup(); + + const onRadioValueChangeSpy = jest.fn(); + + const ControlledRadioGroup = () => { + const [ radioValue, setRadioValue ] = useState< string >(); + return ( + <DropdownMenu trigger={ <button>Open dropdown</button> }> + <DropdownMenuRadioGroup + value={ radioValue } + onValueChange={ ( value ) => { + onRadioValueChangeSpy( value ); + setRadioValue( value ); + } } + > + <DropdownMenuLabel> + Radio group label + </DropdownMenuLabel> + <DropdownMenuRadioItem value="radio-one"> + Radio item one + </DropdownMenuRadioItem> + <DropdownMenuRadioItem value="radio-two"> + Radio item two + </DropdownMenuRadioItem> + </DropdownMenuRadioGroup> + </DropdownMenu> + ); + }; + + render( <ControlledRadioGroup /> ); + + // Open dropdown + await user.click( + screen.getByRole( 'button', { name: 'Open dropdown' } ) + ); + + // No radios should be checked at this point + expect( screen.getAllByRole( 'menuitemradio' ) ).toHaveLength( 2 ); + expect( + screen.getByRole( 'menuitemradio', { name: 'Radio item one' } ) + ).not.toBeChecked(); + expect( + screen.getByRole( 'menuitemradio', { name: 'Radio item two' } ) + ).not.toBeChecked(); + + // Click first radio item, make sure that the callback fires + await user.click( + screen.getByRole( 'menuitemradio', { name: 'Radio item one' } ) + ); + expect( onRadioValueChangeSpy ).toHaveBeenCalledTimes( 1 ); + expect( onRadioValueChangeSpy ).toHaveBeenLastCalledWith( + 'radio-one' + ); + + // Open dropdown + await user.click( + screen.getByRole( 'button', { name: 'Open dropdown' } ) + ); + + // Make sure that first radio is checked + expect( + screen.getByRole( 'menuitemradio', { name: 'Radio item one' } ) + ).toBeChecked(); + expect( + screen.getByRole( 'menuitemradio', { name: 'Radio item two' } ) + ).not.toBeChecked(); + + // Click second radio item, make sure that the callback fires + await user.click( + screen.getByRole( 'menuitemradio', { name: 'Radio item two' } ) + ); + expect( onRadioValueChangeSpy ).toHaveBeenCalledTimes( 2 ); + expect( onRadioValueChangeSpy ).toHaveBeenLastCalledWith( + 'radio-two' + ); + + // Open dropdown + await user.click( + screen.getByRole( 'button', { name: 'Open dropdown' } ) + ); + + // Make sure that second radio is selected + expect( + screen.getByRole( 'menuitemradio', { name: 'Radio item one' } ) + ).not.toBeChecked(); + expect( + screen.getByRole( 'menuitemradio', { name: 'Radio item two' } ) + ).toBeChecked(); + } ); + + it( 'should check menu checkbox items', async () => { + const user = userEvent.setup(); + + const onCheckboxValueChangeSpy = jest.fn(); + + const ControlledRadioGroup = () => { + const [ itemOneChecked, setItemOneChecked ] = + useState< boolean >(); + const [ itemTwoChecked, setItemTwoChecked ] = + useState< boolean >(); + return ( + <DropdownMenu trigger={ <button>Open dropdown</button> }> + <DropdownMenuLabel> + Checkbox group label + </DropdownMenuLabel> + <DropdownMenuCheckboxItem + checked={ itemOneChecked } + onCheckedChange={ ( checked ) => { + setItemOneChecked( checked ); + onCheckboxValueChangeSpy( 'item-one', checked ); + } } + > + Checkbox item one + </DropdownMenuCheckboxItem> + + <DropdownMenuCheckboxItem + checked={ itemTwoChecked } + onCheckedChange={ ( checked ) => { + setItemTwoChecked( checked ); + onCheckboxValueChangeSpy( 'item-two', checked ); + } } + > + Checkbox item two + </DropdownMenuCheckboxItem> + </DropdownMenu> + ); + }; + + render( <ControlledRadioGroup /> ); + + // Open dropdown + await user.click( + screen.getByRole( 'button', { name: 'Open dropdown' } ) + ); + + // No checkboxes should be checked at this point + expect( screen.getAllByRole( 'menuitemcheckbox' ) ).toHaveLength( + 2 + ); + expect( + screen.getByRole( 'menuitemcheckbox', { + name: 'Checkbox item one', + } ) + ).not.toBeChecked(); + expect( + screen.getByRole( 'menuitemcheckbox', { + name: 'Checkbox item two', + } ) + ).not.toBeChecked(); + + // Click first checkbox item, make sure that the callback fires + await user.click( + screen.getByRole( 'menuitemcheckbox', { + name: 'Checkbox item one', + } ) + ); + expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 1 ); + expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith( + 'item-one', + true + ); + + // Open dropdown + await user.click( + screen.getByRole( 'button', { name: 'Open dropdown' } ) + ); + + // Make sure that first checkbox is checked + expect( + screen.getByRole( 'menuitemcheckbox', { + name: 'Checkbox item one', + } ) + ).toBeChecked(); + + // Click second checkbox item, make sure that the callback fires + await user.click( + screen.getByRole( 'menuitemcheckbox', { + name: 'Checkbox item two', + } ) + ); + expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 2 ); + expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith( + 'item-two', + true + ); + + // Open dropdown + await user.click( + screen.getByRole( 'button', { name: 'Open dropdown' } ) + ); + + // Make sure that second checkbox is selected + expect( + screen.getByRole( 'menuitemcheckbox', { + name: 'Checkbox item two', + } ) + ).toBeChecked(); + + // Click second checkbox item, make sure that the callback fires + await user.click( + screen.getByRole( 'menuitemcheckbox', { + name: 'Checkbox item two', + } ) + ); + expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 3 ); + expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith( + 'item-two', + false + ); + + // Open dropdown + await user.click( + screen.getByRole( 'button', { name: 'Open dropdown' } ) + ); + + // Make sure that second checkbox is unselected + expect( + screen.getByRole( 'menuitemcheckbox', { + name: 'Checkbox item two', + } ) + ).not.toBeChecked(); + } ); + } ); + + describe( 'items prefix and suffix', () => { + it( 'should display a prefix on regular items', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu trigger={ <button>Open dropdown</button> }> + <DropdownMenuItem prefix={ <>Item prefix</> }> + Dropdown menu item + </DropdownMenuItem> + </DropdownMenu> + ); + + // Click to open the menu + await user.click( + screen.getByRole( 'button', { + name: 'Open dropdown', + } ) + ); + + // The contents of the prefix are rendered before the item's children + expect( + screen.getByRole( 'menuitem', { + name: 'Item prefix Dropdown menu item', + } ) + ).toBeInTheDocument(); + } ); + + it( 'should display a suffix on regular items', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu trigger={ <button>Open dropdown</button> }> + <DropdownMenuItem suffix={ <>Item suffix</> }> + Dropdown menu item + </DropdownMenuItem> + </DropdownMenu> + ); + + // Click to open the menu + await user.click( + screen.getByRole( 'button', { + name: 'Open dropdown', + } ) + ); + + // The contents of the suffix are rendered after the item's children + expect( + screen.getByRole( 'menuitem', { + name: 'Dropdown menu item Item suffix', + } ) + ).toBeInTheDocument(); + } ); + + it( 'should display a suffix on radio items', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu trigger={ <button>Open dropdown</button> }> + <DropdownMenuRadioGroup> + <DropdownMenuRadioItem + value="radio-one" + suffix="Radio suffix" + > + Radio item one + </DropdownMenuRadioItem> + </DropdownMenuRadioGroup> + </DropdownMenu> + ); + + // Click to open the menu + await user.click( + screen.getByRole( 'button', { + name: 'Open dropdown', + } ) + ); + + // The contents of the suffix are rendered after the item's children + expect( + screen.getByRole( 'menuitemradio', { + name: 'Radio item one Radio suffix', + } ) + ).toBeInTheDocument(); + } ); + + it( 'should display a suffix on checkbox items', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu trigger={ <button>Open dropdown</button> }> + <DropdownMenuCheckboxItem suffix={ 'Checkbox suffix' }> + Checkbox item one + </DropdownMenuCheckboxItem> + </DropdownMenu> + ); + + // Click to open the menu + await user.click( + screen.getByRole( 'button', { + name: 'Open dropdown', + } ) + ); + + // The contents of the suffix are rendered after the item's children + expect( + screen.getByRole( 'menuitemcheckbox', { + name: 'Checkbox item one Checkbox suffix', + } ) + ).toBeInTheDocument(); + } ); + } ); + + describe( 'typeahead', () => { + it( 'should highlight matching item', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu trigger={ <button>Open dropdown</button> }> + <DropdownMenuItem>One</DropdownMenuItem> + <DropdownMenuItem>Two</DropdownMenuItem> + </DropdownMenu> + ); + + // Click to open the menu + await user.click( + screen.getByRole( 'button', { + name: 'Open dropdown', + } ) + ); + expect( screen.getByRole( 'menu' ) ).toBeInTheDocument(); + + // Type "tw", it should match and focus the item with content "Two" + await user.keyboard( 'tw' ); + expect( + screen.getByRole( 'menuitem', { name: 'Two' } ) + ).toHaveFocus(); + + // Wait for the typeahead timer to reset and interpret + // the next keystrokes as a new search + await delay( 1000 ); + + // Type "on", it should match and focus the item with content "One" + await user.keyboard( 'on' ); + expect( + screen.getByRole( 'menuitem', { name: 'One' } ) + ).toHaveFocus(); + } ); + + it( 'should use the textValue prop if specificied', async () => { + const user = userEvent.setup(); + + render( + <DropdownMenu trigger={ <button>Open dropdown</button> }> + <DropdownMenuItem>One</DropdownMenuItem> + <DropdownMenuItem textValue="Four">Two</DropdownMenuItem> + </DropdownMenu> + ); + + // Click to open the menu + await user.click( + screen.getByRole( 'button', { + name: 'Open dropdown', + } ) + ); + expect( screen.getByRole( 'menu' ) ).toBeInTheDocument(); + + // Type "tw", it should not match the item with content "Two" because it + // that item specifies the "textValue" prop. Therefore, the menu container + // retains focus. + await user.keyboard( 'tw' ); + expect( screen.getByRole( 'menu' ) ).toHaveFocus(); + + // Wait for the typeahead timer to reset and interpret + // the next keystrokes as a new search + await delay( 1000 ); + + // Type "fo", it should match and focus the item with textValue "Four" + await user.keyboard( 'fo' ); + expect( + screen.getByRole( 'menuitem', { name: 'Two' } ) + ).toHaveFocus(); + } ); + } ); +} ); diff --git a/packages/components/src/dropdown-menu-v2/types.ts b/packages/components/src/dropdown-menu-v2/types.ts new file mode 100644 index 00000000000000..1fb246fafd6537 --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/types.ts @@ -0,0 +1,250 @@ +/** + * External dependencies + */ +import type * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; + +export type DropdownMenuProps = { + /** + * The open state of the dropdown menu when it is initially rendered. Use when + * you do not need to control its open state. + * + */ + defaultOpen?: DropdownMenuPrimitive.DropdownMenuProps[ 'defaultOpen' ]; + /** + * The controlled open state of the dropdown menu. Must be used in conjunction + * with `onOpenChange`. + */ + open?: DropdownMenuPrimitive.DropdownMenuProps[ 'open' ]; + /** + * Event handler called when the open state of the dropdown menu changes. + */ + onOpenChange?: DropdownMenuPrimitive.DropdownMenuProps[ 'onOpenChange' ]; + /** + * The modality of the dropdown menu. When set to true, interaction with + * outside elements will be disabled and only menu content will be visible to + * screen readers. + * + * @default true + */ + modal?: DropdownMenuPrimitive.DropdownMenuProps[ 'modal' ]; + /** + * The preferred side of the trigger to render against when open. + * Will be reversed when collisions occur and avoidCollisions is enabled. + * + * @default 'bottom' + */ + side?: DropdownMenuPrimitive.DropdownMenuContentProps[ 'side' ]; + /** + * The distance in pixels from the trigger. + * + * @default 0 + */ + sideOffset?: DropdownMenuPrimitive.DropdownMenuContentProps[ 'sideOffset' ]; + /** + * The preferred alignment against the trigger. + * May change when collisions occur. + * + * @default 'center' + */ + align?: DropdownMenuPrimitive.DropdownMenuContentProps[ 'align' ]; + /** + * An offset in pixels from the "start" or "end" alignment options. + * + * @default 0 + */ + alignOffset?: DropdownMenuPrimitive.DropdownMenuContentProps[ 'alignOffset' ]; + /** + * The trigger button. + */ + trigger: React.ReactNode; + /** + * The contents of the dropdown + */ + children: React.ReactNode; +}; + +export type DropdownSubMenuTriggerProps = { + /** + * The contents of the item. + */ + children: React.ReactNode; + /** + * The contents of the item's prefix. + */ + prefix?: React.ReactNode; + /** + * The contents of the item's suffix. + * + * @default The standard chevron icon for a submenu trigger. + */ + suffix?: React.ReactNode; +}; + +export type DropdownSubMenuProps = { + /** + * The open state of the submenu when it is initially rendered. Use when you + * do not need to control its open state. + */ + defaultOpen?: DropdownMenuPrimitive.DropdownMenuSubProps[ 'defaultOpen' ]; + /** + * The controlled open state of the submenu. Must be used in conjunction with + * `onOpenChange`. + */ + open?: DropdownMenuPrimitive.DropdownMenuSubProps[ 'open' ]; + /** + * Event handler called when the open state of the submenu changes. + */ + onOpenChange?: DropdownMenuPrimitive.DropdownMenuSubProps[ 'onOpenChange' ]; + /** + * When `true`, prevents the user from interacting with the item. + */ + disabled?: DropdownMenuPrimitive.DropdownMenuSubTriggerProps[ 'disabled' ]; + /** + * Optional text used for typeahead purposes for the trigger. By default the typeahead + * behavior will use the `.textContent` of the trigger. Use this when the content + * is complex, or you have non-textual content inside. + */ + textValue?: DropdownMenuPrimitive.DropdownMenuSubTriggerProps[ 'textValue' ]; + /** + * The contents rendered inside the trigger. The trigger should be + * an instance of the `DropdownSubMenuTriggerProps` component. + */ + trigger: React.ReactNode; + /** + * The contents of the dropdown sub menu + */ + children: React.ReactNode; +}; + +export type DropdownMenuItemProps = { + /** + * When true, prevents the user from interacting with the item. + * + * @default false + */ + disabled?: DropdownMenuPrimitive.DropdownMenuItemProps[ 'disabled' ]; + /** + * Event handler called when the user selects an item (via mouse or keyboard). + * Calling `event.preventDefault` in this handler will prevent the dropdown + * menu from closing when selecting that item. + */ + onSelect?: DropdownMenuPrimitive.DropdownMenuItemProps[ 'onSelect' ]; + /** + * Optional text used for typeahead purposes. By default the typeahead + * behavior will use the `.textContent` of the item. Use this when the content + * is complex, or you have non-textual content inside. + */ + textValue?: DropdownMenuPrimitive.DropdownMenuItemProps[ 'textValue' ]; + /** + * The contents of the item + */ + children: React.ReactNode; + /** + * The contents of the item's prefix + */ + prefix?: React.ReactNode; + /** + * The contents of the item's suffix + */ + suffix?: React.ReactNode; +}; + +export type DropdownMenuCheckboxItemProps = { + /** + * The controlled checked state of the item. + * Must be used in conjunction with `onCheckedChange`. + * + * @default false + */ + checked?: DropdownMenuPrimitive.DropdownMenuCheckboxItemProps[ 'checked' ]; + /** + * Event handler called when the checked state changes. + */ + onCheckedChange?: DropdownMenuPrimitive.DropdownMenuCheckboxItemProps[ 'onCheckedChange' ]; + /** + * When `true`, prevents the user from interacting with the item. + */ + disabled?: DropdownMenuPrimitive.DropdownMenuCheckboxItemProps[ 'disabled' ]; + /** + * Event handler called when the user selects an item (via mouse or keyboard). + * Calling `event.preventDefault` in this handler will prevent the dropdown + * menu from closing when selecting that item. + */ + onSelect?: DropdownMenuPrimitive.DropdownMenuCheckboxItemProps[ 'onSelect' ]; + /** + * Optional text used for typeahead purposes. By default the typeahead + * behavior will use the `.textContent` of the item. Use this when the content + * is complex, or you have non-textual content inside. + */ + textValue?: DropdownMenuPrimitive.DropdownMenuCheckboxItemProps[ 'textValue' ]; + /** + * The contents of the checkbox item + */ + children: React.ReactNode; + /** + * The contents of the checkbox item's suffix + */ + suffix?: React.ReactNode; +}; + +export type DropdownMenuRadioGroupProps = { + /** + * The value of the selected item in the group. + */ + value?: DropdownMenuPrimitive.DropdownMenuRadioGroupProps[ 'value' ]; + /** + * Event handler called when the value changes. + */ + onValueChange?: DropdownMenuPrimitive.DropdownMenuRadioGroupProps[ 'onValueChange' ]; + /** + * The contents of the radio group + */ + children: React.ReactNode; +}; + +export type DropdownMenuRadioItemProps = { + /** + * The unique value of the item. + */ + value: DropdownMenuPrimitive.DropdownMenuRadioItemProps[ 'value' ]; + /** + * When `true`, prevents the user from interacting with the item. + */ + disabled?: DropdownMenuPrimitive.DropdownMenuRadioItemProps[ 'disabled' ]; + /** + * Event handler called when the user selects an item (via mouse or keyboard). + * Calling `event.preventDefault` in this handler will prevent the dropdown + * menu from closing when selecting that item. + */ + onSelect?: DropdownMenuPrimitive.DropdownMenuRadioItemProps[ 'onSelect' ]; + /** + * Optional text used for typeahead purposes. By default the typeahead + * behavior will use the `.textContent` of the item. Use this when the content + * is complex, or you have non-textual content inside. + */ + textValue?: DropdownMenuPrimitive.DropdownMenuRadioItemProps[ 'textValue' ]; + /** + * The contents of the radio item + */ + children: React.ReactNode; + /** + * The contents of the radio item's suffix + */ + suffix?: React.ReactNode; +}; + +export type DropdownMenuLabelProps = { + /** + * The contents of the label + */ + children: React.ReactNode; +}; + +export type DropdownMenuGroupProps = { + /** + * The contents of the group + */ + children: React.ReactNode; +}; + +export type DropdownMenuSeparatorProps = {}; diff --git a/packages/components/src/dropdown-menu/stories/index.tsx b/packages/components/src/dropdown-menu/stories/index.tsx index 97a51371d1ab8f..8bc652269422e8 100644 --- a/packages/components/src/dropdown-menu/stories/index.tsx +++ b/packages/components/src/dropdown-menu/stories/index.tsx @@ -6,7 +6,8 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; * Internal dependencies */ import DropdownMenu from '..'; -import { MenuGroup, MenuItem } from '../..'; +import MenuItem from '../../menu-item'; +import MenuGroup from '../../menu-group'; /** * WordPress dependencies diff --git a/packages/components/src/dropdown-menu/test/index.tsx b/packages/components/src/dropdown-menu/test/index.tsx index 118e991812367e..9bee9f26605085 100644 --- a/packages/components/src/dropdown-menu/test/index.tsx +++ b/packages/components/src/dropdown-menu/test/index.tsx @@ -13,7 +13,7 @@ import { arrowLeft, arrowRight, arrowUp, arrowDown } from '@wordpress/icons'; * Internal dependencies */ import DropdownMenu from '..'; -import { MenuItem } from '../..'; +import MenuItem from '../../menu-item'; describe( 'DropdownMenu', () => { it( 'should not render when neither controls nor children are assigned', () => { diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index e114559e5088c2..3d94ac4a44ea2d 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -9,6 +9,18 @@ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/pri import { default as CustomSelectControl } from './custom-select-control'; import { positionToPlacement as __experimentalPopoverLegacyPositionToPlacement } from './popover/utils'; import { createPrivateSlotFill } from './slot-fill'; +import { + DropdownMenu as DropdownMenuV2, + DropdownMenuCheckboxItem as DropdownMenuCheckboxItemV2, + DropdownMenuGroup as DropdownMenuGroupV2, + DropdownMenuItem as DropdownMenuItemV2, + DropdownMenuLabel as DropdownMenuLabelV2, + DropdownMenuRadioGroup as DropdownMenuRadioGroupV2, + DropdownMenuRadioItem as DropdownMenuRadioItemV2, + DropdownMenuSeparator as DropdownMenuSeparatorV2, + DropdownSubMenu as DropdownSubMenuV2, + DropdownSubMenuTrigger as DropdownSubMenuTriggerV2, +} from './dropdown-menu-v2'; export const { lock, unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( @@ -21,4 +33,14 @@ lock( privateApis, { CustomSelectControl, __experimentalPopoverLegacyPositionToPlacement, createPrivateSlotFill, + DropdownMenuV2, + DropdownMenuCheckboxItemV2, + DropdownMenuGroupV2, + DropdownMenuItemV2, + DropdownMenuLabelV2, + DropdownMenuRadioGroupV2, + DropdownMenuRadioItemV2, + DropdownMenuSeparatorV2, + DropdownSubMenuV2, + DropdownSubMenuTriggerV2, } ); From 7a9378333b44ff28ab22b09b04d789ebcbc3f92b Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Fri, 19 May 2023 14:01:42 +0300 Subject: [PATCH 099/131] Remove `unwrap` from transforms and add `ungroup` to more blocks (#50385) * Remove `unwrap` from transforms and add `ungroup` to more blocks * add docs * update e2e tests * add e2e test * Refactor useConvertToGroupButtonProps for readability * Apply suggestions from code review Co-authored-by: Miguel Fonseca <miguelcsf@gmail.com> * update docs * reword ungrouping in docs * update e2e test * [RNMobile] Add Group and Ungroup block actions (#50693) * Add `useConvertToGroupButtons` hook Most of the logic of this hook has been extracted from `ConvertToGroupButton` component. The main difference is that we return the configuration for the block actions instead of a component. * Add Group and Ungroup options to block actions menu * Remove `canUnwrap` option from `getBlockTransformOptions` test helper * Update tests for Group, Quote and Columns blocks --------- Co-authored-by: Miguel Fonseca <miguelcsf@gmail.com> Co-authored-by: Carlos Garcia <fluiddot@gmail.com> --- .../block-api/block-transforms.md | 30 ++++++- .../block-actions-menu.native.js | 23 ++++++ .../convert-to-group-buttons/index.js | 11 ++- .../convert-to-group-buttons/index.native.js | 80 ++++++++++++++++++- .../use-convert-to-group-button-props.js | 70 ++++++++-------- packages/block-editor/src/store/selectors.js | 16 +--- .../__snapshots__/transforms.native.js.snap | 2 +- .../src/columns/test/transforms.native.js | 8 +- .../block-library/src/columns/transforms.js | 10 +-- .../__snapshots__/transforms.native.js.snap | 2 +- .../src/group/test/transforms.native.js | 8 +- .../block-library/src/group/transforms.js | 7 -- .../__snapshots__/transforms.native.js.snap | 2 +- .../src/quote/test/transforms.native.js | 8 +- .../block-library/src/quote/transforms.js | 22 +++-- packages/blocks/src/api/factory.js | 13 +-- test/e2e/specs/editor/blocks/columns.spec.js | 36 +++++++++ test/e2e/specs/editor/blocks/quote.spec.js | 12 +-- .../get-block-transform-options.js | 30 ++----- 19 files changed, 242 insertions(+), 148 deletions(-) diff --git a/docs/reference-guides/block-api/block-transforms.md b/docs/reference-guides/block-api/block-transforms.md index b2970e3d14ff2b..5efc76064fe311 100644 --- a/docs/reference-guides/block-api/block-transforms.md +++ b/docs/reference-guides/block-api/block-transforms.md @@ -228,7 +228,7 @@ When pasting content it's possible to define a [content model](https://html.spec When writing `raw` transforms you can control this by supplying a `schema` which describes allowable content and which will be applied to clean up the pasted content before attempting to match with your block. The schemas are passed into [`cleanNodeList` from `@wordpress/dom`](https://github.com/wordpress/gutenberg/blob/trunk/packages/dom/src/dom/clean-node-list.js); check there for a [complete description of the schema](https://github.com/wordpress/gutenberg/blob/trunk/packages/dom/src/phrasing-content.js). ```js -schema = { span: { children: { '#text': {} } } } +schema = { span: { children: { '#text': {} } } }; ``` **Example: a custom content model** @@ -237,8 +237,8 @@ Suppose we want to match the following HTML snippet and turn it into some kind o ```html <div data-post-id="13"> - <h2>The Post Title</h2> - <p>Some <em>great</em> content.</p> + <h2>The Post Title</h2> + <p>Some <em>great</em> content.</p> </div> ``` @@ -270,7 +270,7 @@ A transformation of type `shortcode` is an object that takes the following param - **type** _(string)_: the value `shortcode`. - **tag** _(string|array)_: the shortcode tag or list of shortcode aliases this transform can work with. -- **transform** _(function, optional): a callback that receives the shortcode attributes as the first argument and the [WPShortcodeMatch](/packages/shortcode/README.md#next) as the second. It should return a block object or an array of block objects. When this parameter is defined, it will take precedence over the `attributes` parameter. +- **transform** _(function, optional)_: a callback that receives the shortcode attributes as the first argument and the [WPShortcodeMatch](/packages/shortcode/README.md#next) as the second. It should return a block object or an array of block objects. When this parameter is defined, it will take precedence over the `attributes` parameter. - **attributes** _(object, optional)_: object representing where the block attributes should be sourced from, according to the attributes shape defined by the [block configuration object](./block-registration.md). If a particular attribute contains a `shortcode` key, it should be a function that receives the shortcode attributes as the first arguments and the [WPShortcodeMatch](/packages/shortcode/README.md#next) as second, and returns a value for the attribute that will be sourced in the block's comment. - **isMatch** _(function, optional)_: a callback that receives the shortcode attributes per the [Shortcode API](https://codex.wordpress.org/Shortcode_API) and should return a boolean. Returning `false` from this function will prevent the shortcode to be transformed into this block. - **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. @@ -336,3 +336,25 @@ transforms: { ] }, ``` + +## `ungroup` blocks + +Via the optional `transforms` key of the block configuration, blocks can use the `ungroup` subkey to define the blocks that will replace the block being processed. These new blocks will usually be a subset of the existing inner blocks, but could also include new blocks. + +If a block has an `ungroup` transform, it is eligible for ungrouping, without the requirement of being the default grouping block. The UI used to ungroup a block with this API is the same as the one used for the default grouping block. In order for the Ungroup button to be displayed, we must have a single grouping block selected, which also contains some inner blocks. + +**ungroup** is a callback function that receives the attributes and inner blocks of the block being processed. It should return an array of block objects. + +Example: + +```js +export const settings = { + title: 'My grouping Block Title', + description: 'My grouping block description', + /* ... */ + transforms: { + ungroup: ( attributes, innerBlocks ) => + innerBlocks.flatMap( ( innerBlock ) => innerBlock.innerBlocks ), + }, +}; +``` diff --git a/packages/block-editor/src/components/block-mobile-toolbar/block-actions-menu.native.js b/packages/block-editor/src/components/block-mobile-toolbar/block-actions-menu.native.js index 0183fca510af7c..08b5672738a2b7 100644 --- a/packages/block-editor/src/components/block-mobile-toolbar/block-actions-menu.native.js +++ b/packages/block-editor/src/components/block-mobile-toolbar/block-actions-menu.native.js @@ -39,6 +39,10 @@ import { store as coreStore } from '@wordpress/core-data'; import { getMoversSetup } from '../block-mover/mover-description'; import { store as blockEditorStore } from '../../store'; import BlockTransformationsMenu from '../block-switcher/block-transformations-menu'; +import { + useConvertToGroupButtons, + useConvertToGroupButtonProps, +} from '../convert-to-group-buttons'; const BlockActionsMenu = ( { // Select. @@ -55,6 +59,7 @@ const BlockActionsMenu = ( { rootClientId, selectedBlockClientId, selectedBlockPossibleTransformations, + canRemove, // Dispatch. createSuccessNotice, convertToRegularBlocks, @@ -93,6 +98,17 @@ const BlockActionsMenu = ( { }, } = getMoversSetup( isStackedHorizontally, moversOptions ); + // Check if selected block is Groupable and/or Ungroupable. + const convertToGroupButtonProps = useConvertToGroupButtonProps( [ + selectedBlockClientId, + ] ); + const { isGroupable, isUngroupable } = convertToGroupButtonProps; + const showConvertToGroupButton = + ( isGroupable || isUngroupable ) && canRemove; + const convertToGroupButtons = useConvertToGroupButtons( { + ...convertToGroupButtonProps, + } ); + const allOptions = { settings: { id: 'settingsOption', @@ -229,6 +245,10 @@ const BlockActionsMenu = ( { canDuplicate && allOptions.cutButton, canDuplicate && isPasteEnabled && allOptions.pasteButton, canDuplicate && allOptions.duplicateButton, + showConvertToGroupButton && isGroupable && convertToGroupButtons.group, + showConvertToGroupButton && + isUngroupable && + convertToGroupButtons.ungroup, isReusableBlockType && innerBlockCount > 0 && allOptions.convertToRegularBlocks, @@ -327,6 +347,7 @@ export default compose( getSelectedBlockClientIds, canInsertBlockType, getTemplateLock, + canRemoveBlock, } = select( blockEditorStore ); const block = getBlock( clientId ); const blockName = getBlockName( clientId ); @@ -363,6 +384,7 @@ export default compose( const selectedBlockPossibleTransformations = selectedBlock ? getBlockTransformItems( selectedBlock, rootClientId ) : EMPTY_BLOCK_LIST; + const canRemove = canRemoveBlock( selectedBlockClientId ); const isReusableBlockType = block ? isReusableBlock( block ) : false; const reusableBlock = isReusableBlockType @@ -388,6 +410,7 @@ export default compose( rootClientId, selectedBlockClientId, selectedBlockPossibleTransformations, + canRemove, }; } ), withDispatch( diff --git a/packages/block-editor/src/components/convert-to-group-buttons/index.js b/packages/block-editor/src/components/convert-to-group-buttons/index.js index 233c859bdefb71..c2bb3fb25b8452 100644 --- a/packages/block-editor/src/components/convert-to-group-buttons/index.js +++ b/packages/block-editor/src/components/convert-to-group-buttons/index.js @@ -17,6 +17,7 @@ function ConvertToGroupButton( { clientIds, isGroupable, isUngroupable, + onUngroup, blocksSelection, groupingBlockName, onClose = () => {}, @@ -34,10 +35,16 @@ function ConvertToGroupButton( { }; const onConvertFromGroup = () => { - const innerBlocks = blocksSelection[ 0 ].innerBlocks; + let innerBlocks = blocksSelection[ 0 ].innerBlocks; if ( ! innerBlocks.length ) { return; } + if ( onUngroup ) { + innerBlocks = onUngroup( + blocksSelection[ 0 ].attributes, + blocksSelection[ 0 ].innerBlocks + ); + } replaceBlocks( clientIds, innerBlocks ); }; @@ -66,7 +73,7 @@ function ConvertToGroupButton( { > { _x( 'Ungroup', - 'Ungrouping blocks from within a Group block back into individual blocks within the Editor ' + 'Ungrouping blocks from within a grouping block back into individual blocks within the Editor ' ) } </MenuItem> ) } diff --git a/packages/block-editor/src/components/convert-to-group-buttons/index.native.js b/packages/block-editor/src/components/convert-to-group-buttons/index.native.js index 461f67a0a4bcbe..1a85d43ff01ed0 100644 --- a/packages/block-editor/src/components/convert-to-group-buttons/index.native.js +++ b/packages/block-editor/src/components/convert-to-group-buttons/index.native.js @@ -1 +1,79 @@ -export default () => null; +/** + * WordPress dependencies + */ +import { __, _x } from '@wordpress/i18n'; +import { switchToBlockType } from '@wordpress/blocks'; +import { useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import useConvertToGroupButtonProps from './use-convert-to-group-button-props'; + +function useConvertToGroupButtons( { + clientIds, + onUngroup, + blocksSelection, + groupingBlockName, +} ) { + const { replaceBlocks } = useDispatch( blockEditorStore ); + const { createSuccessNotice } = useDispatch( noticesStore ); + const onConvertToGroup = () => { + // Activate the `transform` on the Grouping Block which does the conversion. + const newBlocks = switchToBlockType( + blocksSelection, + groupingBlockName + ); + if ( newBlocks ) { + replaceBlocks( clientIds, newBlocks ); + } + }; + + const onConvertFromGroup = () => { + let innerBlocks = blocksSelection[ 0 ].innerBlocks; + if ( ! innerBlocks.length ) { + return; + } + if ( onUngroup ) { + innerBlocks = onUngroup( + blocksSelection[ 0 ].attributes, + blocksSelection[ 0 ].innerBlocks + ); + } + replaceBlocks( clientIds, innerBlocks ); + }; + + return { + group: { + id: 'groupButtonOption', + label: _x( 'Group', 'verb' ), + value: 'groupButtonOption', + onSelect: () => { + onConvertToGroup(); + createSuccessNotice( + // translators: displayed right after the block is grouped + __( 'Block grouped' ) + ); + }, + }, + ungroup: { + id: 'ungroupButtonOption', + label: _x( + 'Ungroup', + 'Ungrouping blocks from within a grouping block back into individual blocks within the Editor' + ), + value: 'ungroupButtonOption', + onSelect: () => { + onConvertFromGroup(); + createSuccessNotice( + // translators: displayed right after the block is ungrouped. + __( 'Block ungrouped' ) + ); + }, + }, + }; +} + +export { useConvertToGroupButtons, useConvertToGroupButtonProps }; diff --git a/packages/block-editor/src/components/convert-to-group-buttons/use-convert-to-group-button-props.js b/packages/block-editor/src/components/convert-to-group-buttons/use-convert-to-group-button-props.js index 0056f3c4543c4c..197dca486d0e3c 100644 --- a/packages/block-editor/src/components/convert-to-group-buttons/use-convert-to-group-button-props.js +++ b/packages/block-editor/src/components/convert-to-group-buttons/use-convert-to-group-button-props.js @@ -31,13 +31,7 @@ import { store as blockEditorStore } from '../../store'; * @return {ConvertToGroupButtonProps} Returns the properties needed by `ConvertToGroupButton`. */ export default function useConvertToGroupButtonProps( selectedClientIds ) { - const { - clientIds, - isGroupable, - isUngroupable, - blocksSelection, - groupingBlockName, - } = useSelect( + return useSelect( ( select ) => { const { getBlockRootClientId, @@ -45,54 +39,54 @@ export default function useConvertToGroupButtonProps( selectedClientIds ) { canInsertBlockType, getSelectedBlockClientIds, } = select( blockEditorStore ); - const { getGroupingBlockName } = select( blocksStore ); - - const _clientIds = selectedClientIds?.length + const { getGroupingBlockName, getBlockType } = + select( blocksStore ); + const clientIds = selectedClientIds?.length ? selectedClientIds : getSelectedBlockClientIds(); - const _groupingBlockName = getGroupingBlockName(); + const groupingBlockName = getGroupingBlockName(); - const rootClientId = !! _clientIds?.length - ? getBlockRootClientId( _clientIds[ 0 ] ) + const rootClientId = clientIds?.length + ? getBlockRootClientId( clientIds[ 0 ] ) : undefined; const groupingBlockAvailable = canInsertBlockType( - _groupingBlockName, + groupingBlockName, rootClientId ); - const _blocksSelection = getBlocksByClientId( _clientIds ); - - const isSingleGroupingBlock = - _blocksSelection.length === 1 && - _blocksSelection[ 0 ]?.name === _groupingBlockName; + const blocksSelection = getBlocksByClientId( clientIds ); + const isSingleBlockSelected = blocksSelection.length === 1; + const [ firstSelectedBlock ] = blocksSelection; + // A block is ungroupable if it is a single grouping block with inner blocks. + // If a block has an `ungroup` transform, it is also ungroupable, without the + // requirement of being the default grouping block. + // Do we have a single grouping Block selected and does that group have inner blocks? + const isUngroupable = + isSingleBlockSelected && + ( firstSelectedBlock.name === groupingBlockName || + getBlockType( firstSelectedBlock.name )?.transforms + ?.ungroup ) && + !! firstSelectedBlock.innerBlocks.length; // Do we have // 1. Grouping block available to be inserted? // 2. One or more blocks selected - const _isGroupable = - groupingBlockAvailable && _blocksSelection.length; + const isGroupable = + groupingBlockAvailable && blocksSelection.length; - // Do we have a single Group Block selected and does that group have inner blocks? - const _isUngroupable = - isSingleGroupingBlock && - !! _blocksSelection[ 0 ].innerBlocks.length; return { - clientIds: _clientIds, - isGroupable: _isGroupable, - isUngroupable: _isUngroupable, - blocksSelection: _blocksSelection, - groupingBlockName: _groupingBlockName, + clientIds, + isGroupable, + isUngroupable, + blocksSelection, + groupingBlockName, + onUngroup: + isUngroupable && + getBlockType( firstSelectedBlock.name )?.transforms + ?.ungroup, }; }, [ selectedClientIds ] ); - - return { - clientIds, - isGroupable, - isUngroupable, - blocksSelection, - groupingBlockName, - }; } diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index e61e6063a33e65..59c36dca2b8237 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -18,7 +18,6 @@ import { import { Platform } from '@wordpress/element'; import { applyFilters } from '@wordpress/hooks'; import { symbol } from '@wordpress/icons'; -import { __ } from '@wordpress/i18n'; import { create, remove, toHTMLString } from '@wordpress/rich-text'; import deprecated from '@wordpress/deprecated'; @@ -2101,7 +2100,6 @@ export const getInserterItems = createSelector( export const getBlockTransformItems = createSelector( ( state, blocks, rootClientId = null ) => { const normalizedBlocks = Array.isArray( blocks ) ? blocks : [ blocks ]; - const [ sourceBlock ] = normalizedBlocks; const buildBlockTypeTransformItem = buildBlockTypeItem( state, { buildScope: 'transform', } ); @@ -2118,22 +2116,10 @@ export const getBlockTransformItems = createSelector( ] ) ); - // Consider unwraping the highest priority. - itemsByName[ '*' ] = { - frecency: +Infinity, - id: '*', - isDisabled: false, - name: '*', - title: __( 'Unwrap' ), - icon: itemsByName[ sourceBlock?.name ]?.icon, - }; - const possibleTransforms = getPossibleBlockTransformations( normalizedBlocks ).reduce( ( accumulator, block ) => { - if ( block === '*' ) { - accumulator.push( itemsByName[ '*' ] ); - } else if ( itemsByName[ block?.name ] ) { + if ( itemsByName[ block?.name ] ) { accumulator.push( itemsByName[ block.name ] ); } return accumulator; diff --git a/packages/block-library/src/columns/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/columns/test/__snapshots__/transforms.native.js.snap index ee8eb3d1a2b164..939638c4c579e1 100644 --- a/packages/block-library/src/columns/test/__snapshots__/transforms.native.js.snap +++ b/packages/block-library/src/columns/test/__snapshots__/transforms.native.js.snap @@ -34,7 +34,7 @@ exports[`Columns block transforms to Group block 1`] = ` <!-- /wp:group -->" `; -exports[`Columns block transforms unwraps content 1`] = ` +exports[`Columns block transforms ungroups block 1`] = ` "<!-- wp:paragraph {"align":"left"} --> <p class="has-text-align-left"><strong>Built with modern technology.</strong></p> <!-- /wp:paragraph --> diff --git a/packages/block-library/src/columns/test/transforms.native.js b/packages/block-library/src/columns/test/transforms.native.js index 12ae9d9c0829f2..a2103bdb86aa00 100644 --- a/packages/block-library/src/columns/test/transforms.native.js +++ b/packages/block-library/src/columns/test/transforms.native.js @@ -60,14 +60,13 @@ describe( `${ block } block transforms`, () => { expect( getEditorHtml() ).toMatchSnapshot(); } ); - it( 'unwraps content', async () => { + it( 'ungroups block', async () => { const screen = await initializeEditor( { initialHtml } ); const { getByText } = screen; fireEvent.press( getBlock( screen, block ) ); await openBlockActionsMenu( screen ); - fireEvent.press( getByText( 'Transform block…' ) ); - fireEvent.press( getByText( 'Unwrap' ) ); + fireEvent.press( getByText( 'Ungroup' ) ); // The first block created is the content of the Paragraph block. const paragraph = getBlock( screen, 'Paragraph', 0 ); @@ -83,8 +82,7 @@ describe( `${ block } block transforms`, () => { const screen = await initializeEditor( { initialHtml } ); const transformOptions = await getBlockTransformOptions( screen, - block, - { canUnwrap: true } + block ); expect( transformOptions ).toHaveLength( blockTransforms.length ); } ); diff --git a/packages/block-library/src/columns/transforms.js b/packages/block-library/src/columns/transforms.js index fda65df8659d8f..3a15892431f251 100644 --- a/packages/block-library/src/columns/transforms.js +++ b/packages/block-library/src/columns/transforms.js @@ -105,14 +105,8 @@ const transforms = { }, }, ], - to: [ - { - type: 'block', - blocks: [ '*' ], - transform: ( attributes, innerBlocks ) => - innerBlocks.flatMap( ( innerBlock ) => innerBlock.innerBlocks ), - }, - ], + ungroup: ( attributes, innerBlocks ) => + innerBlocks.flatMap( ( innerBlock ) => innerBlock.innerBlocks ), }; export default transforms; diff --git a/packages/block-library/src/group/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/group/test/__snapshots__/transforms.native.js.snap index 174bb9b52c2e11..ca0bd5b36ee0c4 100644 --- a/packages/block-library/src/group/test/__snapshots__/transforms.native.js.snap +++ b/packages/block-library/src/group/test/__snapshots__/transforms.native.js.snap @@ -20,7 +20,7 @@ exports[`Group block transforms to Columns block 1`] = ` <!-- /wp:columns -->" `; -exports[`Group block transforms unwraps content 1`] = ` +exports[`Group block transforms ungroups block 1`] = ` "<!-- wp:paragraph --> <p>One.</p> <!-- /wp:paragraph --> diff --git a/packages/block-library/src/group/test/transforms.native.js b/packages/block-library/src/group/test/transforms.native.js index d9540dc5c51d5f..9293482f119055 100644 --- a/packages/block-library/src/group/test/transforms.native.js +++ b/packages/block-library/src/group/test/transforms.native.js @@ -44,14 +44,13 @@ describe( `${ block } block transforms`, () => { expect( getEditorHtml() ).toMatchSnapshot(); } ); - it( 'unwraps content', async () => { + it( 'ungroups block', async () => { const screen = await initializeEditor( { initialHtml } ); const { getByText } = screen; fireEvent.press( getBlock( screen, block ) ); await openBlockActionsMenu( screen ); - fireEvent.press( getByText( 'Transform block…' ) ); - fireEvent.press( getByText( 'Unwrap' ) ); + fireEvent.press( getByText( 'Ungroup' ) ); // The first block created is the content of the Paragraph block. const paragraph = getBlock( screen, 'Paragraph', 0 ); @@ -67,8 +66,7 @@ describe( `${ block } block transforms`, () => { const screen = await initializeEditor( { initialHtml } ); const transformOptions = await getBlockTransformOptions( screen, - block, - { canUnwrap: true } + block ); expect( transformOptions ).toHaveLength( blockTransforms.length ); } ); diff --git a/packages/block-library/src/group/transforms.js b/packages/block-library/src/group/transforms.js index 5649529c6501d2..851dfcdb7530bb 100644 --- a/packages/block-library/src/group/transforms.js +++ b/packages/block-library/src/group/transforms.js @@ -48,13 +48,6 @@ const transforms = { }, }, ], - to: [ - { - type: 'block', - blocks: [ '*' ], - transform: ( attributes, innerBlocks ) => innerBlocks, - }, - ], }; export default transforms; diff --git a/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap index bc337691dd27a7..5b5df918f2beeb 100644 --- a/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap +++ b/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap @@ -28,7 +28,7 @@ exports[`Quote block transforms to Pullquote block 1`] = ` <!-- /wp:pullquote -->" `; -exports[`Quote block transforms unwraps content 1`] = ` +exports[`Quote block transforms ungroups block 1`] = ` "<!-- wp:paragraph --> <p>"This will make running your own blog a viable alternative again."</p> <!-- /wp:paragraph --> diff --git a/packages/block-library/src/quote/test/transforms.native.js b/packages/block-library/src/quote/test/transforms.native.js index 75cb887a4872be..46c4eb2b6f9727 100644 --- a/packages/block-library/src/quote/test/transforms.native.js +++ b/packages/block-library/src/quote/test/transforms.native.js @@ -36,14 +36,13 @@ describe( `${ block } block transforms`, () => { expect( getEditorHtml() ).toMatchSnapshot(); } ); - it( 'unwraps content', async () => { + it( 'ungroups block', async () => { const screen = await initializeEditor( { initialHtml } ); const { getByText } = screen; fireEvent.press( getBlock( screen, block ) ); await openBlockActionsMenu( screen ); - fireEvent.press( getByText( 'Transform block…' ) ); - fireEvent.press( getByText( 'Unwrap' ) ); + fireEvent.press( getByText( 'Ungroup' ) ); // The first block created is the content of the Paragraph block. const paragraph = getBlock( screen, 'Paragraph', 0 ); @@ -59,8 +58,7 @@ describe( `${ block } block transforms`, () => { const screen = await initializeEditor( { initialHtml } ); const transformOptions = await getBlockTransformOptions( screen, - block, - { canUnwrap: true } + block ); expect( transformOptions ).toHaveLength( blockTransforms.length ); } ); diff --git a/packages/block-library/src/quote/transforms.js b/packages/block-library/src/quote/transforms.js index d3c34ee5178941..d4cd77177bf030 100644 --- a/packages/block-library/src/quote/transforms.js +++ b/packages/block-library/src/quote/transforms.js @@ -126,20 +126,16 @@ const transforms = { : innerBlocks ), }, - { - type: 'block', - blocks: [ '*' ], - transform: ( { citation }, innerBlocks ) => - citation - ? [ - ...innerBlocks, - createBlock( 'core/paragraph', { - content: citation, - } ), - ] - : innerBlocks, - }, ], + ungroup: ( { citation }, innerBlocks ) => + citation + ? [ + ...innerBlocks, + createBlock( 'core/paragraph', { + content: citation, + } ), + ] + : innerBlocks, }; export default transforms; diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js index 6df8eb00005a8e..7112ae25ccdde2 100644 --- a/packages/blocks/src/api/factory.js +++ b/packages/blocks/src/api/factory.js @@ -278,9 +278,7 @@ const getBlockTypesForPossibleToTransforms = ( blocks ) => { .flat(); // Map block names to block types. - return blockNames.map( ( name ) => - name === '*' ? name : getBlockType( name ) - ); + return blockNames.map( getBlockType ); }; /** @@ -473,7 +471,8 @@ export function switchToBlockType( blocks, name ) { transformationsTo, ( t ) => t.type === 'block' && - t.blocks.indexOf( name ) !== -1 && + ( isWildcardBlockTransform( t ) || + t.blocks.indexOf( name ) !== -1 ) && ( ! isMultiBlock || t.isMultiBlock ) && maybeCheckTransformIsMatch( t, blocksArray ) ) || @@ -539,12 +538,6 @@ export function switchToBlockType( blocks, name ) { return null; } - // When unwrapping blocks (`switchToBlockType( wrapperblocks, '*' )`), do - // not run filters on the unwrapped blocks. They shoud remain as they are. - if ( name === '*' ) { - return transformationResults; - } - const hasSwitchedBlock = transformationResults.some( ( result ) => result.name === name ); diff --git a/test/e2e/specs/editor/blocks/columns.spec.js b/test/e2e/specs/editor/blocks/columns.spec.js index c2078c861c4a89..247a49becc3735 100644 --- a/test/e2e/specs/editor/blocks/columns.spec.js +++ b/test/e2e/specs/editor/blocks/columns.spec.js @@ -82,4 +82,40 @@ test.describe( 'Columns', () => { await pageUtils.pressKeys( 'Tab' ); await expect( columnsChangeInput ).toHaveValue( '3' ); } ); + test( 'Ungroup properly', async ( { editor } ) => { + await editor.insertBlock( { + name: 'core/columns', + innerBlocks: [ + { + name: 'core/column', + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: '1' }, + }, + ], + }, + { + name: 'core/column', + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: '2' }, + }, + ], + }, + ], + } ); + await editor.clickBlockOptionsMenuItem( 'Ungroup' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '1' }, + }, + { + name: 'core/paragraph', + attributes: { content: '2' }, + }, + ] ); + } ); } ); diff --git a/test/e2e/specs/editor/blocks/quote.spec.js b/test/e2e/specs/editor/blocks/quote.spec.js index 6e724ab09afd7b..bff5bb69685357 100644 --- a/test/e2e/specs/editor/blocks/quote.spec.js +++ b/test/e2e/specs/editor/blocks/quote.spec.js @@ -138,8 +138,7 @@ test.describe( 'Quote', () => { await page.keyboard.type( 'two' ); // Navigate to the citation to select the block. await page.keyboard.press( 'ArrowRight' ); - // Unwrap the block. - await editor.transformBlockTo( '*' ); + await editor.clickBlockOptionsMenuItem( 'Ungroup' ); expect( await editor.getEditedPostContent() ).toBe( `<!-- wp:paragraph --> <p>one</p> @@ -161,8 +160,7 @@ test.describe( 'Quote', () => { await page.keyboard.type( 'two' ); await page.keyboard.press( 'ArrowRight' ); await page.keyboard.type( 'cite' ); - // Unwrap the block. - await editor.transformBlockTo( '*' ); + await editor.clickBlockOptionsMenuItem( 'Ungroup' ); expect( await editor.getEditedPostContent() ).toBe( `<!-- wp:paragraph --> <p>one</p> @@ -185,8 +183,7 @@ test.describe( 'Quote', () => { await editor.insertBlock( { name: 'core/quote' } ); await page.keyboard.press( 'ArrowRight' ); await page.keyboard.type( 'cite' ); - // Unwrap the block. - await editor.transformBlockTo( '*' ); + await editor.clickBlockOptionsMenuItem( 'Ungroup' ); expect( await editor.getEditedPostContent() ).toBe( `<!-- wp:paragraph --> <p></p> @@ -205,8 +202,7 @@ test.describe( 'Quote', () => { await editor.insertBlock( { name: 'core/quote' } ); // Select the quote await page.keyboard.press( 'ArrowRight' ); - // Unwrap the block. - await editor.transformBlockTo( '*' ); + await editor.clickBlockOptionsMenuItem( 'Ungroup' ); expect( await editor.getEditedPostContent() ).toBe( '' ); } ); } ); diff --git a/test/native/integration-test-helpers/get-block-transform-options.js b/test/native/integration-test-helpers/get-block-transform-options.js index e9bd35261ed740..02ca81a1d5ccc0 100644 --- a/test/native/integration-test-helpers/get-block-transform-options.js +++ b/test/native/integration-test-helpers/get-block-transform-options.js @@ -12,36 +12,18 @@ import { openBlockActionsMenu } from './open-block-actions-menu'; /** * Transforms the selected block to a specified block. * - * @param {import('@testing-library/react-native').RenderAPI} screen A Testing Library screen. - * @param {string} blockName Name of the block. - * @param {Object} [options] Configuration options. - * @param {number} [options.canUnwrap] True if the block can be unwrapped. + * @param {import('@testing-library/react-native').RenderAPI} screen A Testing Library screen. + * @param {string} blockName Name of the block. * @return {[import('react-test-renderer').ReactTestInstance]} Block transform options. */ -export const getBlockTransformOptions = async ( - screen, - blockName, - { canUnwrap = false } = {} -) => { +export const getBlockTransformOptions = async ( screen, blockName ) => { const { getByTestId, getByText } = screen; fireEvent.press( getBlock( screen, blockName ) ); await openBlockActionsMenu( screen ); fireEvent.press( getByText( 'Transform block…' ) ); - let blockTransformButtons = within( - getByTestId( 'block-transformations-menu' ) - ).getAllByRole( 'button' ); - - // Remove Unwrap option as it's not a direct block transformation. - if ( canUnwrap ) { - const unwrapButton = within( - getByTestId( 'block-transformations-menu' ) - ).getByLabelText( 'Unwrap' ); - blockTransformButtons = blockTransformButtons.filter( - ( button ) => button !== unwrapButton - ); - } - - return blockTransformButtons; + return within( getByTestId( 'block-transformations-menu' ) ).getAllByRole( + 'button' + ); }; From 542fa174ba7690e00cf312d07fab1ea933c7ac07 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 19 May 2023 12:37:15 +0100 Subject: [PATCH 100/131] Update the document title in the site editor to open the command center (#50369) Co-authored-by: James Koster <james@jameskoster.co.uk> --- .../document-actions/index.js | 156 +++------------- .../document-actions/style.scss | 87 +++------ .../components/header-edit-mode/style.scss | 1 + .../template-details/edit-template-title.js | 41 ----- .../src/components/template-details/index.js | 113 ------------ .../components/template-details/style.scss | 72 -------- .../template-details/template-areas.js | 167 ------------------ .../template-part-area-selector.js | 39 ---- .../use-edited-entity-record/index.js | 4 +- packages/edit-site/src/style.scss | 1 - .../specs/site-editor/browser-history.spec.js | 22 --- test/e2e/specs/site-editor/style-book.spec.js | 3 - .../specs/site-editor/template-revert.spec.js | 32 +++- test/e2e/specs/site-editor/title.spec.js | 25 --- 14 files changed, 88 insertions(+), 675 deletions(-) delete mode 100644 packages/edit-site/src/components/template-details/edit-template-title.js delete mode 100644 packages/edit-site/src/components/template-details/index.js delete mode 100644 packages/edit-site/src/components/template-details/style.scss delete mode 100644 packages/edit-site/src/components/template-details/template-areas.js delete mode 100644 packages/edit-site/src/components/template-details/template-part-area-selector.js diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js b/packages/edit-site/src/components/header-edit-mode/document-actions/index.js index e738f4bd23d135..97bb0acb3bdd49 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js +++ b/packages/edit-site/src/components/header-edit-mode/document-actions/index.js @@ -1,112 +1,39 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ import { sprintf, __ } from '@wordpress/i18n'; +import { useDispatch } from '@wordpress/data'; import { - __experimentalGetBlockLabel as getBlockLabel, - getBlockType, -} from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; -import { - Dropdown, Button, VisuallyHidden, __experimentalText as Text, + __experimentalHStack as HStack, } from '@wordpress/components'; -import { chevronDown } from '@wordpress/icons'; -import { useState, useMemo } from '@wordpress/element'; -import { - store as blockEditorStore, - useBlockDisplayInformation, - BlockIcon, -} from '@wordpress/block-editor'; -import { store as preferencesStore } from '@wordpress/preferences'; +import { BlockIcon } from '@wordpress/block-editor'; +import { privateApis as commandsPrivateApis } from '@wordpress/commands'; /** * Internal dependencies */ -import TemplateDetails from '../../template-details'; import useEditedEntityRecord from '../../use-edited-entity-record'; +import { unlock } from '../../../private-apis'; -function getBlockDisplayText( block ) { - if ( block ) { - const blockType = getBlockType( block.name ); - return blockType ? getBlockLabel( blockType, block.attributes ) : null; - } - return null; -} - -function useSecondaryText() { - const { getBlock } = useSelect( blockEditorStore ); - const activeEntityBlockId = useSelect( - ( select ) => - select( - blockEditorStore - ).__experimentalGetActiveBlockIdByBlockNames( [ - 'core/template-part', - ] ), - [] - ); - - const blockInformation = useBlockDisplayInformation( activeEntityBlockId ); - - if ( activeEntityBlockId ) { - return { - label: getBlockDisplayText( getBlock( activeEntityBlockId ) ), - isActive: true, - icon: blockInformation?.icon, - }; - } - - return {}; -} +const { store: commandsStore } = unlock( commandsPrivateApis ); export default function DocumentActions() { - const showIconLabels = useSelect( - ( select ) => - select( preferencesStore ).get( - 'core/edit-site', - 'showIconLabels' - ), - [] - ); - const { isLoaded, record, getTitle } = useEditedEntityRecord(); - const { label, icon } = useSecondaryText(); - - // Use internal state instead of a ref to make sure that the component - // re-renders when the popover's anchor updates. - const [ popoverAnchor, setPopoverAnchor ] = useState( null ); - - // Memoize popoverProps to avoid returning a new object every time. - const popoverProps = useMemo( - () => ( { - // Use the title wrapper as the popover anchor so that the dropdown is - // centered over the whole title area rather than just one part of it. - anchor: popoverAnchor, - placement: 'bottom', - } ), - [ popoverAnchor ] - ); + const { open: openCommandCenter } = useDispatch( commandsStore ); + const { isLoaded, record, getTitle, icon } = useEditedEntityRecord(); // Return a simple loading indicator until we have information to show. if ( ! isLoaded ) { - return ( - <div className="edit-site-document-actions"> - { __( 'Loading…' ) } - </div> - ); + return null; } // Return feedback that the template does not seem to exist. if ( ! record ) { return ( <div className="edit-site-document-actions"> - { __( 'Template not found' ) } + { __( 'Document not found' ) } </div> ); } @@ -116,21 +43,21 @@ export default function DocumentActions() { ? __( 'template part' ) : __( 'template' ); + const isMac = /Mac|iPod|iPhone|iPad/.test( window.navigator.platform ); + return ( - <div - className={ classnames( 'edit-site-document-actions', { - 'has-secondary-label': !! label, - } ) } + <Button + className="edit-site-document-actions" + onClick={ () => openCommandCenter() } > - <div - ref={ setPopoverAnchor } - className="edit-site-document-actions__title-wrapper" + <span className="edit-site-document-actions__left"></span> + <HStack + spacing={ 1 } + justify="center" + className="edit-site-document-actions__title" > - <Text - size="body" - className="edit-site-document-actions__title" - as="h1" - > + <BlockIcon icon={ icon } /> + <Text size="body" as="h1"> <VisuallyHidden as="span"> { sprintf( /* translators: %s: the entity being edited, like "template"*/ @@ -140,39 +67,10 @@ export default function DocumentActions() { </VisuallyHidden> { getTitle() } </Text> - <div className="edit-site-document-actions__secondary-item"> - <BlockIcon icon={ icon } showColors /> - <Text size="body">{ label ?? '' }</Text> - </div> - - <Dropdown - popoverProps={ popoverProps } - renderToggle={ ( { isOpen, onToggle } ) => ( - <Button - className="edit-site-document-actions__get-info" - icon={ chevronDown } - aria-expanded={ isOpen } - aria-haspopup="true" - onClick={ onToggle } - variant={ showIconLabels ? 'tertiary' : undefined } - label={ sprintf( - /* translators: %s: the entity to see details about, like "template"*/ - __( 'Show %s details' ), - entityLabel - ) } - > - { showIconLabels && __( 'Details' ) } - </Button> - ) } - contentClassName="edit-site-document-actions__info-dropdown" - renderContent={ ( { onClose } ) => ( - <TemplateDetails - template={ record } - onClose={ onClose } - /> - ) } - /> - </div> - </div> + </HStack> + <span className="edit-site-document-actions__shortcut"> + { isMac ? '⌘' : 'Ctrl' } K + </span> + </Button> ); } diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss index 43915135ec276b..247b901975fd8e 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss +++ b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss @@ -1,79 +1,48 @@ .edit-site-document-actions { display: flex; - flex-direction: column; - justify-content: center; - padding: 0 $grid-unit; - height: 100%; + align-items: center; + gap: $grid-unit; + height: $button-size; + padding: $grid-unit; + justify-content: space-between; // Flex items will, by default, refuse to shrink below a minimum // intrinsic width. In order to shrink this flexbox item, and // subsequently truncate child text, we set an explicit min-width. // See https://dev.w3.org/csswg/css-flexbox/#min-size-auto min-width: 0; + background: $gray-100; + border-radius: 4px; + width: min(100%, 450px); - .edit-site-document-actions__title-wrapper { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - - // See comment above about min-width - min-width: 0; - - .components-dropdown { - display: inline-flex; - margin-left: $grid-unit-05; - - .components-button { - min-width: 0; - padding: 0; - } - } - } - - .edit-site-document-actions__title-wrapper > h1 { - margin: 0; - - // See comment above about min-width - min-width: 0; - } - - .edit-site-document-actions__title { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + &:hover { color: currentColor; + background: $gray-200; } +} - .edit-site-document-actions__secondary-item { - display: flex; - align-items: center; +.edit-site-document-actions__title { + flex-grow: 1; + color: var(--wp-block-synced-color); + overflow: hidden; + + h1 { + color: var(--wp-block-synced-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: 0; - opacity: 0; - padding: 0; - transition: all ease 0.2s; - background: rgba(var(--wp-block-synced-color--rgb), 0.04); - border-radius: 2px; - @include reduce-motion(transition); - - .block-editor-block-icon.has-colors { - color: var(--wp-block-synced-color); - } } +} - &.has-secondary-label { - .edit-site-document-actions__secondary-item { - opacity: 1; - padding: 0 4px; - max-width: 180px; - margin-left: 6px; - } +.edit-site-document-actions__shortcut { + flex-shrink: 0; + color: $gray-700; + width: #{$grid-unit * 4.5}; + &:hover { + color: $gray-700; } } -.edit-site-document-actions__info-dropdown > .components-popover__content { - padding: 0; - min-width: 240px; +.edit-site-document-actions__left { + min-width: $button-size; + flex-shrink: 0; } diff --git a/packages/edit-site/src/components/header-edit-mode/style.scss b/packages/edit-site/src/components/header-edit-mode/style.scss index d26bad43e356da..bbaf896076a099 100644 --- a/packages/edit-site/src/components/header-edit-mode/style.scss +++ b/packages/edit-site/src/components/header-edit-mode/style.scss @@ -27,6 +27,7 @@ $header-toolbar-min-width: 335px; align-items: center; height: 100%; flex-grow: 1; + margin: 0 $grid-unit-10; justify-content: center; // Flex items will, by default, refuse to shrink below a minimum // intrinsic width. In order to shrink this flexbox item, and diff --git a/packages/edit-site/src/components/template-details/edit-template-title.js b/packages/edit-site/src/components/template-details/edit-template-title.js deleted file mode 100644 index ac2266a0a03d76..00000000000000 --- a/packages/edit-site/src/components/template-details/edit-template-title.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { TextControl } from '@wordpress/components'; -import { useEntityProp } from '@wordpress/core-data'; -import { useState } from '@wordpress/element'; - -export default function EditTemplateTitle( { template } ) { - const [ forceEmpty, setForceEmpty ] = useState( false ); - const [ title, setTitle ] = useEntityProp( - 'postType', - template.type, - 'title', - template.id - ); - - return ( - <TextControl - __nextHasNoMarginBottom - label={ __( 'Title' ) } - value={ forceEmpty ? '' : title } - help={ - template.type !== 'wp_template_part' - ? __( - 'Give the template a title that indicates its purpose, e.g. "Full Width".' - ) - : null - } - onChange={ ( newTitle ) => { - if ( ! newTitle && ! forceEmpty ) { - setForceEmpty( true ); - return; - } - setForceEmpty( false ); - setTitle( newTitle ); - } } - onBlur={ () => setForceEmpty( false ) } - /> - ); -} diff --git a/packages/edit-site/src/components/template-details/index.js b/packages/edit-site/src/components/template-details/index.js deleted file mode 100644 index 4f0c23755f857c..00000000000000 --- a/packages/edit-site/src/components/template-details/index.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { - Button, - MenuGroup, - MenuItem, - __experimentalVStack as VStack, - __experimentalText as Text, -} from '@wordpress/components'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { store as editorStore } from '@wordpress/editor'; -import { decodeEntities } from '@wordpress/html-entities'; - -/** - * Internal dependencies - */ -import isTemplateRevertable from '../../utils/is-template-revertable'; -import { store as editSiteStore } from '../../store'; -import EditTemplateTitle from './edit-template-title'; -import { useLink } from '../routes/link'; -import TemplatePartAreaSelector from './template-part-area-selector'; - -export default function TemplateDetails( { template, onClose } ) { - const { title, description } = useSelect( - ( select ) => - select( editorStore ).__experimentalGetTemplateInfo( template ), - [] - ); - const { revertTemplate } = useDispatch( editSiteStore ); - - // TODO: We should update this to filter by template part's areas as well. - const browseAllLinkProps = useLink( { - path: '/' + template.type + '/all', - } ); - - const isTemplatePart = template.type === 'wp_template_part'; - - // Only user-created and non-default templates can change the name. - // But any user-created template part can be renamed. - const canEditTitle = isTemplatePart - ? ! template.has_theme_file - : template.is_custom && ! template.has_theme_file; - - if ( ! template ) { - return null; - } - - const revert = () => { - revertTemplate( template ); - onClose(); - }; - - return ( - <div className="edit-site-template-details"> - <VStack className="edit-site-template-details__group" spacing={ 3 }> - { canEditTitle ? ( - <EditTemplateTitle template={ template } /> - ) : ( - <Text - size={ 16 } - weight={ 600 } - className="edit-site-template-details__title" - as="p" - > - { decodeEntities( title ) } - </Text> - ) } - - { description && ( - <Text - size="body" - className="edit-site-template-details__description" - as="p" - > - { decodeEntities( description ) } - </Text> - ) } - </VStack> - - { isTemplatePart && ( - <div className="edit-site-template-details__group"> - <TemplatePartAreaSelector id={ template.id } /> - </div> - ) } - - { isTemplateRevertable( template ) && ( - <MenuGroup className="edit-site-template-details__group edit-site-template-details__revert"> - <MenuItem - className="edit-site-template-details__revert-button" - info={ __( - 'Use the template as supplied by the theme.' - ) } - onClick={ revert } - > - { __( 'Clear customizations' ) } - </MenuItem> - </MenuGroup> - ) } - - <Button - className="edit-site-template-details__show-all-button" - { ...browseAllLinkProps } - onClick={ () => onClose() } - > - { template?.type === 'wp_template' - ? __( 'Manage all templates' ) - : __( 'Manage all template parts' ) } - </Button> - </div> - ); -} diff --git a/packages/edit-site/src/components/template-details/style.scss b/packages/edit-site/src/components/template-details/style.scss deleted file mode 100644 index ee840be7c43db7..00000000000000 --- a/packages/edit-site/src/components/template-details/style.scss +++ /dev/null @@ -1,72 +0,0 @@ -.edit-site-template-details { - .edit-site-template-details__group { - margin: 0; - padding: $grid-unit-20; - } - - .edit-site-template-details__group + .edit-site-template-details__group { - border-top: $border-width solid $gray-400; - } - - .edit-site-template-details__description { - color: $gray-700; - } - - // The group already has a 8px padding inside, so overriding the parent padding. - .edit-site-template-details__group.edit-site-template-details__template-areas { - padding: $grid-unit-10; - } - - .edit-site-template-details__template-areas-item { - position: relative; - - .components-menu-items__item-icon { - color: var(--wp-block-synced-color); - } - - .edit-site-template-details__template-areas-item-more { - position: absolute; - right: 0; - top: 0; - bottom: 0; - margin: auto 0; - } - } - - .edit-site-template-details__revert { - // Make some spaces for the revert button to have some paddings. - padding: $grid-unit-15 $grid-unit; - } - - .edit-site-template-details__revert-button { - height: auto; - padding: $grid-unit-05 $grid-unit; - text-align: left; - - &:focus:not(:disabled) { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 0 0 3px $white; - } - } - - .edit-site-template-details__show-all-button.components-button { - display: flex; - justify-content: center; - background: $gray-900; - color: $white; - width: 100%; - height: ($button-size + $grid-unit-10); - border-radius: 0; - - &:hover { - color: $white; - } - - &:active { - color: $gray-400; - } - - &:focus:not(:disabled) { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 0 0 3px $white; - } - } -} diff --git a/packages/edit-site/src/components/template-details/template-areas.js b/packages/edit-site/src/components/template-details/template-areas.js deleted file mode 100644 index 2c6a509ddc9d64..00000000000000 --- a/packages/edit-site/src/components/template-details/template-areas.js +++ /dev/null @@ -1,167 +0,0 @@ -/** - * WordPress dependencies - */ -import { sprintf, __ } from '@wordpress/i18n'; -import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as editorStore } from '@wordpress/editor'; -import { store as blockEditorStore } from '@wordpress/block-editor'; -import { moreVertical } from '@wordpress/icons'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import { store as editSiteStore } from '../../store'; -import isTemplateRevertable from '../../utils/is-template-revertable'; -import { useLink } from '../routes/link'; -import { unlock } from '../../private-apis'; - -const { useLocation } = unlock( routerPrivateApis ); - -function TemplatePartItemMore( { - onClose, - templatePart, - closeTemplateDetailsDropdown, -} ) { - const { revertTemplate } = useDispatch( editSiteStore ); - const { params } = useLocation(); - const editLinkProps = useLink( - { - postId: templatePart.id, - postType: templatePart.type, - }, - { - fromTemplateId: params.postId, - } - ); - - function editTemplatePart( event ) { - editLinkProps.onClick( event ); - onClose(); - closeTemplateDetailsDropdown(); - } - - function clearCustomizations() { - revertTemplate( templatePart ); - onClose(); - closeTemplateDetailsDropdown(); - } - - return ( - <> - <MenuGroup> - <MenuItem { ...editLinkProps } onClick={ editTemplatePart }> - { sprintf( - /* translators: %s: template part title */ - __( 'Edit %s' ), - templatePart.title?.rendered - ) } - </MenuItem> - </MenuGroup> - { isTemplateRevertable( templatePart ) && ( - <MenuGroup> - <MenuItem - info={ __( - 'Use the template part as supplied by the theme.' - ) } - onClick={ clearCustomizations } - > - { __( 'Clear customizations' ) } - </MenuItem> - </MenuGroup> - ) } - </> - ); -} - -function TemplatePartItem( { - templatePart, - clientId, - closeTemplateDetailsDropdown, -} ) { - const { selectBlock, toggleBlockHighlight } = - useDispatch( blockEditorStore ); - const templatePartArea = useSelect( - ( select ) => { - const defaultAreas = - select( - editorStore - ).__experimentalGetDefaultTemplatePartAreas(); - - return defaultAreas.find( - ( defaultArea ) => defaultArea.area === templatePart.area - ); - }, - [ templatePart.area ] - ); - const highlightBlock = () => toggleBlockHighlight( clientId, true ); - const cancelHighlightBlock = () => toggleBlockHighlight( clientId, false ); - - return ( - <div - role="menuitem" - className="edit-site-template-details__template-areas-item" - > - <MenuItem - role="button" - icon={ templatePartArea?.icon } - iconPosition="left" - onClick={ () => { - selectBlock( clientId ); - } } - onMouseOver={ highlightBlock } - onMouseLeave={ cancelHighlightBlock } - onFocus={ highlightBlock } - onBlur={ cancelHighlightBlock } - > - { templatePartArea?.label } - </MenuItem> - - <DropdownMenu - icon={ moreVertical } - label={ __( 'More options' ) } - className="edit-site-template-details__template-areas-item-more" - > - { ( { onClose } ) => ( - <TemplatePartItemMore - onClose={ onClose } - templatePart={ templatePart } - closeTemplateDetailsDropdown={ - closeTemplateDetailsDropdown - } - /> - ) } - </DropdownMenu> - </div> - ); -} - -export default function TemplateAreas( { closeTemplateDetailsDropdown } ) { - const templateParts = useSelect( - ( select ) => select( editSiteStore ).getCurrentTemplateTemplateParts(), - [] - ); - - if ( ! templateParts.length ) { - return null; - } - - return ( - <MenuGroup - label={ __( 'Areas' ) } - className="edit-site-template-details__group edit-site-template-details__template-areas" - > - { templateParts.map( ( { templatePart, block } ) => ( - <TemplatePartItem - key={ templatePart.slug } - clientId={ block.clientId } - templatePart={ templatePart } - closeTemplateDetailsDropdown={ - closeTemplateDetailsDropdown - } - /> - ) ) } - </MenuGroup> - ); -} diff --git a/packages/edit-site/src/components/template-details/template-part-area-selector.js b/packages/edit-site/src/components/template-details/template-part-area-selector.js deleted file mode 100644 index 4823fea501ce14..00000000000000 --- a/packages/edit-site/src/components/template-details/template-part-area-selector.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { SelectControl } from '@wordpress/components'; -import { useEntityProp } from '@wordpress/core-data'; -import { useSelect } from '@wordpress/data'; -import { store as editorStore } from '@wordpress/editor'; - -export default function TemplatePartAreaSelector( { id } ) { - const [ area, setArea ] = useEntityProp( - 'postType', - 'wp_template_part', - 'area', - id - ); - - const definedAreas = useSelect( - ( select ) => - select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), - [] - ); - - const areaOptions = definedAreas.map( ( { label, area: _area } ) => ( { - label, - value: _area, - } ) ); - - return ( - <SelectControl - __nextHasNoMarginBottom - label={ __( 'Area' ) } - labelPosition="top" - options={ areaOptions } - value={ area } - onChange={ setArea } - /> - ); -} diff --git a/packages/edit-site/src/components/use-edited-entity-record/index.js b/packages/edit-site/src/components/use-edited-entity-record/index.js index 59efbea4da3803..22a8bdc32a94a0 100644 --- a/packages/edit-site/src/components/use-edited-entity-record/index.js +++ b/packages/edit-site/src/components/use-edited-entity-record/index.js @@ -12,7 +12,7 @@ import { decodeEntities } from '@wordpress/html-entities'; import { store as editSiteStore } from '../../store'; export default function useEditedEntityRecord( postType, postId ) { - const { record, title, description, isLoaded } = useSelect( + const { record, title, description, isLoaded, icon } = useSelect( ( select ) => { const { getEditedPostType, getEditedPostId } = select( editSiteStore ); @@ -41,6 +41,7 @@ export default function useEditedEntityRecord( postType, postId ) { title: templateInfo.title, description: templateInfo.description, isLoaded: _isLoaded, + icon: templateInfo.icon, }; }, [ postType, postId ] @@ -48,6 +49,7 @@ export default function useEditedEntityRecord( postType, postId ) { return { isLoaded, + icon, record, getTitle: () => ( title ? decodeEntities( title ) : null ), getDescription: () => diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 679d13a08277ac..7a0233ebee5247 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -13,7 +13,6 @@ @import "./components/sidebar-edit-mode/settings-header/style.scss"; @import "./components/sidebar-edit-mode/template-card/style.scss"; @import "./components/editor/style.scss"; -@import "./components/template-details/style.scss"; @import "./components/create-template-part-modal/style.scss"; @import "./components/secondary-sidebar/style.scss"; @import "./components/welcome-guide/style.scss"; diff --git a/test/e2e/specs/site-editor/browser-history.spec.js b/test/e2e/specs/site-editor/browser-history.spec.js index bbfd2e8c5b86b8..2dec72953765f8 100644 --- a/test/e2e/specs/site-editor/browser-history.spec.js +++ b/test/e2e/specs/site-editor/browser-history.spec.js @@ -35,26 +35,4 @@ test.describe( 'Site editor browser history', () => { await page.goBack(); await expect( page ).toHaveURL( '/wp-admin/index.php' ); } ); - - test( 'Opens the template list from the template details view', async ( { - admin, - page, - } ) => { - await admin.visitSiteEditor( { - postType: 'wp_template', - postId: 'emptytheme//index', - canvas: 'edit', - } ); - - // Navigate to the template list - await page.click( 'role=button[name="Show template details"]' ); - await page.click( 'role=link[name="Manage all templates"]' ); - - await expect( page ).toHaveURL( - '/wp-admin/site-editor.php?path=%2Fwp_template%2Fall' - ); - - const title = page.getByRole( 'heading', { level: 1 } ); - await expect( title ).toHaveText( 'Templates' ); - } ); } ); diff --git a/test/e2e/specs/site-editor/style-book.spec.js b/test/e2e/specs/site-editor/style-book.spec.js index 5cdf1c2a0e59e7..6231175584554b 100644 --- a/test/e2e/specs/site-editor/style-book.spec.js +++ b/test/e2e/specs/site-editor/style-book.spec.js @@ -40,9 +40,6 @@ test.describe( 'Style Book', () => { await expect( page.locator( 'role=button[name="Redo"i]' ) ).not.toBeVisible(); - await expect( - page.locator( 'role=button[name="Show template details"i]' ) - ).not.toBeVisible(); await expect( page.locator( 'role=button[name="View"i]' ) ).not.toBeVisible(); diff --git a/test/e2e/specs/site-editor/template-revert.spec.js b/test/e2e/specs/site-editor/template-revert.spec.js index f1f6b3eb5d014e..a13abab881d9b9 100644 --- a/test/e2e/specs/site-editor/template-revert.spec.js +++ b/test/e2e/specs/site-editor/template-revert.spec.js @@ -39,11 +39,23 @@ test.describe( 'Template Revert', () => { await templateRevertUtils.revertTemplate(); await editor.saveSiteEditorEntities(); - await page.click( 'role=button[name="Show template details"i]' ); + await page.click( 'role=button[name="Settings"i]' ); + const isTemplateTabVisible = await page + .locator( + 'role=region[name="Editor settings"i] >> role=button[name="Template"i]' + ) + .isVisible(); + if ( isTemplateTabVisible ) { + await page.click( + 'role=region[name="Editor settings"i] >> role=button[name="Template"i]' + ); + } // The revert button isn't visible anymore. await expect( - page.locator( 'role=menuitem[name=/Clear customizations/i]' ) + page.locator( + 'role=region[name="Editor settings"i] >> role=button[name="Actions"i]' + ) ).not.toBeVisible(); } ); @@ -279,11 +291,25 @@ class TemplateRevertUtils { } async revertTemplate() { - await this.page.click( 'role=button[name="Show template details"i]' ); + await this.page.click( 'role=button[name="Settings"i]' ); + const isTemplateTabVisible = await this.page + .locator( + 'role=region[name="Editor settings"i] >> role=button[name="Template"i]' + ) + .isVisible(); + if ( isTemplateTabVisible ) { + await this.page.click( + 'role=region[name="Editor settings"i] >> role=button[name="Template"i]' + ); + } + await this.page.click( + 'role=region[name="Editor settings"i] >> role=button[name="Actions"i]' + ); await this.page.click( 'role=menuitem[name=/Clear customizations/i]' ); await this.page.waitForSelector( 'role=button[name="Dismiss this notice"i] >> text="Template reverted."' ); + await this.page.click( 'role=button[name="Settings"i]' ); } async getCurrentSiteEditorContent() { diff --git a/test/e2e/specs/site-editor/title.spec.js b/test/e2e/specs/site-editor/title.spec.js index 585b75c1507be3..21cfc544829705 100644 --- a/test/e2e/specs/site-editor/title.spec.js +++ b/test/e2e/specs/site-editor/title.spec.js @@ -45,29 +45,4 @@ test.describe( 'Site editor title', () => { await expect( title ).toHaveText( 'Editing template part: header' ); } ); - - test( "displays the selected template part's name in the secondary title when a template part is selected from List View", async ( { - admin, - page, - } ) => { - await admin.visitSiteEditor( { - postId: 'emptytheme//index', - postType: 'wp_template', - canvas: 'edit', - } ); - // Select the header template part via list view. - await page.click( 'role=button[name="List View"i]' ); - const listView = page.locator( - 'role=treegrid[name="Block navigation structure"i]' - ); - await listView.locator( 'role=gridcell >> text="header"' ).click(); - await page.click( 'role=button[name="Close"i]' ); - - // Evaluate the document settings secondary title. - const secondaryTitle = page.locator( - '.edit-site-document-actions__secondary-item' - ); - - await expect( secondaryTitle ).toHaveText( 'header' ); - } ); } ); From d746e558a034ab38fbbb62bfe7ffa401b6422621 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 19 May 2023 13:07:50 +0100 Subject: [PATCH 101/131] Remove the experimental flag of the command center (#50781) --- lib/experimental/editor-settings.php | 6 ------ lib/experiments-page.php | 12 ------------ packages/edit-post/src/editor.js | 4 +--- .../edit-site/src/components/layout/index.js | 2 +- .../edit-site/src/components/site-hub/index.js | 17 ++++++++--------- 5 files changed, 10 insertions(+), 31 deletions(-) diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index bf9acb7b70d4dd..96cd4e48440394 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -83,12 +83,6 @@ function gutenberg_enable_experiments() { if ( $gutenberg_experiments && array_key_exists( 'gutenberg-color-randomizer', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableColorRandomizer = true', 'before' ); } - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-command-center', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-edit-site', 'window.__experimentalEnableCommandCenter = true', 'before' ); - } - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-command-center', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-edit-post', 'window.__experimentalEnableCommandCenter = true', 'before' ); - } if ( $gutenberg_experiments && array_key_exists( 'gutenberg-group-grid-variation', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableGroupGridVariation = true', 'before' ); } diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 521d04b75b34be..9e31815f3f50ff 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -65,18 +65,6 @@ function gutenberg_initialize_experiments_settings() { ) ); - add_settings_field( - 'gutenberg-command-center', - __( 'Command center ', 'gutenberg' ), - 'gutenberg_display_experiment_field', - 'gutenberg-experiments', - 'gutenberg_experiments_section', - array( - 'label' => __( 'Test the command center; Open it using cmd + k in the site or post editors.', 'gutenberg' ), - 'id' => 'gutenberg-command-center', - ) - ); - add_settings_field( 'gutenberg-group-grid-variation', __( 'Grid variation for Group block ', 'gutenberg' ), diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index e8fec1219a5594..62e92218e878ea 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -189,9 +189,7 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { { ...props } > <ErrorBoundary> - { window?.__experimentalEnableCommandCenter && ( - <CommandMenu /> - ) } + <CommandMenu /> <EditorInitialization postId={ postId } /> <Layout styles={ styles } /> </ErrorBoundary> diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 0ca3c9b6422205..54b3f6ab22a1da 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -143,7 +143,7 @@ export default function Layout() { return ( <> - { window?.__experimentalEnableCommandCenter && <CommandMenu /> } + <CommandMenu /> <KeyboardShortcutsRegister /> <KeyboardShortcutsGlobal /> { fullResizer } diff --git a/packages/edit-site/src/components/site-hub/index.js b/packages/edit-site/src/components/site-hub/index.js index eadd9bbad68b97..b477c5b14f1622 100644 --- a/packages/edit-site/src/components/site-hub/index.js +++ b/packages/edit-site/src/components/site-hub/index.js @@ -150,15 +150,14 @@ const SiteHub = forwardRef( ( props, ref ) => { </motion.div> </AnimatePresence> </HStack> - { window?.__experimentalEnableCommandCenter && - canvasMode === 'view' && ( - <Button - className="edit-site-site-hub_toggle-command-center" - icon={ search } - onClick={ () => openCommandCenter() } - label={ __( 'Open command center' ) } - /> - ) } + { canvasMode === 'view' && ( + <Button + className="edit-site-site-hub_toggle-command-center" + icon={ search } + onClick={ () => openCommandCenter() } + label={ __( 'Open command center' ) } + /> + ) } </HStack> </motion.div> ); From ce29b86a56a18696af4b76208e3c668c194e0781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joni=20Erkkil=C3=A4?= <62872075+n2erjo00@users.noreply.github.com> Date: Fri, 19 May 2023 15:13:41 +0300 Subject: [PATCH 102/131] Added wrapper element for RichText in File block (#50607) * Added wrapper element for RichText this will make editor able find focus on RichText even if there is no value * Removed wrapper div from RichText element * Added display: inline-block declaration for non-button links inside file block --- packages/block-library/src/file/editor.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/block-library/src/file/editor.scss b/packages/block-library/src/file/editor.scss index f9d1f544ada07c..365ba5ec1dc222 100644 --- a/packages/block-library/src/file/editor.scss +++ b/packages/block-library/src/file/editor.scss @@ -35,6 +35,10 @@ a { min-width: 1em; + + &:not(.wp-block-file__button) { + display: inline-block; + } } .wp-block-file__button-richtext-wrapper { From 064f7319b8f7a8860ef4d295e37a53e46bcebae6 Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Fri, 19 May 2023 14:18:11 +0200 Subject: [PATCH 103/131] Add transparent outline to input control BackdropUI focus style. (#50772) * Add transparent outline to input control BackdropUI focus style. * Add changelog entry. --- packages/components/CHANGELOG.md | 5 ++++- .../src/input-control/styles/input-control-styles.tsx | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 257d0545f42236..388aef8d5a3df5 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -12,6 +12,9 @@ - `DropdownMenu`: Convert to TypeScript ([#50187](https://github.com/WordPress/gutenberg/pull/50187)). - Added experimental v2 of `DropdownMenu` ([#49473](https://github.com/WordPress/gutenberg/pull/49473)). +### Bug Fix + +- `InputControl`: Fix focus style to support Windows High Contrast mode ([#50772](https://github.com/WordPress/gutenberg/pull/50772)). ## 24.0.0 (2023-05-10) @@ -22,7 +25,7 @@ ### Bug Fix - `NavigableContainer`: do not trap focus in `TabbableContainer` ([#49846](https://github.com/WordPress/gutenberg/pull/49846)). -- Update `<Button>` component to have a transparent background for its tertiary disabled state, to match its enabled state. ([#50496](https://github.com/WordPress/gutenberg/pull/50496)). +- Update `<Button>` component to have a transparent background for its tertiary disabled state, to match its enabled state. ([#50496](https://github.com/WordPress/gutenberg/pull/50496)). ### Internal diff --git a/packages/components/src/input-control/styles/input-control-styles.tsx b/packages/components/src/input-control/styles/input-control-styles.tsx index 1be98530d1fa48..3548be6f7260c5 100644 --- a/packages/components/src/input-control/styles/input-control-styles.tsx +++ b/packages/components/src/input-control/styles/input-control-styles.tsx @@ -270,9 +270,14 @@ const backdropFocusedStyles = ( { let borderColor = isFocused ? COLORS.ui.borderFocus : COLORS.ui.border; let boxShadow; + let outline; + let outlineOffset; if ( isFocused ) { boxShadow = `0 0 0 1px ${ COLORS.ui.borderFocus } inset`; + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline = `2px solid transparent`; + outlineOffset = `-2px`; } if ( disabled ) { @@ -284,6 +289,8 @@ const backdropFocusedStyles = ( { borderColor, borderStyle: 'solid', borderWidth: 1, + outline, + outlineOffset, } ); }; From c689d9ff27be17c6a47d07517c87f7bec6b95a23 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher <saxonafletcher@gmail.com> Date: Sat, 20 May 2023 05:25:20 +1000 Subject: [PATCH 104/131] Update frame resizing (#49910) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * frame resizer centered * Use a lerp function to modify the height of the frame. It should gradually reduce from original aspect ratio until reaching a 9 / 19.5 view. * Make the frame full screen when the user resizes it to the left. Reset the initial aspect ratio if the frame is resized slightly, and trigger full screen if the frame is resized far enough over the sidebar. * Ensure the frame grows only to the left when going above its size. * Disable user selection while resizing the frame. * Make it easier to grab the handle. * Switch to setTimeout and set a fixed resizeRatio. * Modify oversized calculation to reduce resizing bug. * Avoid timer * Temp working * Clean up CSS * More cleanup * Refactor lerpy parts * More cleanup * Rename `isFull` to `isFullWidth` * Improve maintainability * More cleanup * Match component classnames * Invert control for flex changes * Calculate fluid resize ratio * Prevent React re-render loop warning * Always show handle when resizing * Maintain resizing cursor when resizing * Improve code comments * Exclude `ListPage` from ResizableFrame * Use CSS var for accent color * Handle spinner gracefully * Lift loading state so resizing can be disabled * Change max width for less jankiness * Remove outdated padding animation * Clean up magic numbers * Update saveSiteEditorEntities() locators * Update StyleBook.open() locators * Quickfix: Wait until load spinner is gone * Revert to class-based save detection The `.getByRole()` way resolves a bit too early. --------- Co-authored-by: Matías Ventura <mv@matiasventura.com> Co-authored-by: Lena Morita <lena@jaguchi.com> Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com> Co-authored-by: Bart Kalisz <bartlomiej.kalisz@gmail.com> --- .../src/admin/visit-site-editor.ts | 5 + .../src/editor/site-editor.ts | 28 +- .../edit-site/src/components/editor/index.js | 56 ++-- .../src/components/editor/style.scss | 10 + .../edit-site/src/components/layout/hooks.js | 46 ++++ .../edit-site/src/components/layout/index.js | 147 +++------- .../src/components/layout/style.scss | 10 +- .../src/components/resizable-frame/index.js | 253 ++++++++++++++++++ .../src/components/resizable-frame/style.scss | 69 +++++ packages/edit-site/src/style.scss | 1 + test/e2e/specs/site-editor/style-book.spec.js | 7 +- 11 files changed, 461 insertions(+), 171 deletions(-) create mode 100644 packages/edit-site/src/components/layout/hooks.js create mode 100644 packages/edit-site/src/components/resizable-frame/index.js create mode 100644 packages/edit-site/src/components/resizable-frame/style.scss diff --git a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts index a4c3adc747a1e7..bd25796d25eccf 100644 --- a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts +++ b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts @@ -54,4 +54,9 @@ export async function visitSiteEditor( .locator( 'body > *' ) .first() .waitFor(); + + // TODO: Ideally the content underneath the spinner should be marked inert until it's ready. + await this.page + .locator( '.edit-site-canvas-spinner' ) + .waitFor( { state: 'hidden' } ); } diff --git a/packages/e2e-test-utils-playwright/src/editor/site-editor.ts b/packages/e2e-test-utils-playwright/src/editor/site-editor.ts index d3fb58f9aab401..432e8c15b120a7 100644 --- a/packages/e2e-test-utils-playwright/src/editor/site-editor.ts +++ b/packages/e2e-test-utils-playwright/src/editor/site-editor.ts @@ -9,15 +9,25 @@ import type { Editor } from './index'; * @param this */ export async function saveSiteEditorEntities( this: Editor ) { - await this.page.click( - 'role=region[name="Editor top bar"i] >> role=button[name="Save"i]' - ); + const editorTopBar = this.page.getByRole( 'region', { + name: 'Editor top bar', + } ); + const savePanel = this.page.getByRole( 'region', { name: 'Save panel' } ); + + // First Save button in the top bar. + await editorTopBar + .getByRole( 'button', { name: 'Save', exact: true } ) + .click(); + // Second Save button in the entities panel. - await this.page.click( - 'role=region[name="Save panel"i] >> role=button[name="Save"i]' - ); + await savePanel + .getByRole( 'button', { name: 'Save', exact: true } ) + .click(); + // A role selector cannot be used here because it needs to check that the `is-busy` class is not present. - await this.page.waitForSelector( '[aria-label="Saved"].is-busy', { - state: 'hidden', - } ); + await this.page + .locator( '[aria-label="Editor top bar"] [aria-label="Saved"].is-busy' ) + .waitFor( { + state: 'hidden', + } ); } diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 777f6dd53afdf6..44b31c6945f57f 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -1,10 +1,15 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ -import { useEffect, useMemo, useState } from '@wordpress/element'; +import { useMemo } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; import { Notice } from '@wordpress/components'; -import { EntityProvider, store as coreStore } from '@wordpress/core-data'; +import { EntityProvider } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; import { BlockContextProvider, @@ -50,42 +55,7 @@ const interfaceLabels = { footer: __( 'Editor footer' ), }; -function useIsSiteEditorLoading() { - const { isLoaded: hasLoadedPost } = useEditedEntityRecord(); - const [ loaded, setLoaded ] = useState( false ); - const inLoadingPause = useSelect( - ( select ) => { - const hasResolvingSelectors = - select( coreStore ).hasResolvingSelectors(); - return ! loaded && ! hasResolvingSelectors; - }, - [ loaded ] - ); - - useEffect( () => { - if ( inLoadingPause ) { - /* - * We're using an arbitrary 1s timeout here to catch brief moments - * without any resolving selectors that would result in displaying - * brief flickers of loading state and loaded state. - * - * It's worth experimenting with different values, since this also - * adds 1s of artificial delay after loading has finished. - */ - const timeout = setTimeout( () => { - setLoaded( true ); - }, 1000 ); - - return () => { - clearTimeout( timeout ); - }; - } - }, [ inLoadingPause ] ); - - return ! loaded || ! hasLoadedPost; -} - -export default function Editor() { +export default function Editor( { isLoading } ) { const { record: editedPost, getTitle, @@ -188,8 +158,6 @@ export default function Editor() { // action in <URlQueryController> from double-announcing. useTitle( hasLoadedPost && title ); - const isLoading = useIsSiteEditorLoading(); - return ( <> { isLoading ? <CanvasSpinner /> : null } @@ -205,7 +173,13 @@ export default function Editor() { { isEditMode && <StartTemplateOptions /> } <InterfaceSkeleton enableRegionNavigation={ false } - className={ showIconLabels && 'show-icon-labels' } + className={ classnames( + 'edit-site-editor__interface-skeleton', + { + 'show-icon-labels': showIconLabels, + 'is-loading': isLoading, + } + ) } notices={ ( isEditMode || window?.__experimentalEnableThemePreviews ) && ( diff --git a/packages/edit-site/src/components/editor/style.scss b/packages/edit-site/src/components/editor/style.scss index 1a24d3ee1475e7..b8795d9ba7cb31 100644 --- a/packages/edit-site/src/components/editor/style.scss +++ b/packages/edit-site/src/components/editor/style.scss @@ -1,3 +1,13 @@ +.edit-site-editor__interface-skeleton { + opacity: 1; + transition: opacity 0.1s ease-out; + @include reduce-motion("transition"); + + &.is-loading { + opacity: 0; + } +} + .edit-site-editor__toggle-save-panel { box-sizing: border-box; width: $sidebar-width; diff --git a/packages/edit-site/src/components/layout/hooks.js b/packages/edit-site/src/components/layout/hooks.js new file mode 100644 index 00000000000000..7a89987cb7482c --- /dev/null +++ b/packages/edit-site/src/components/layout/hooks.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import useEditedEntityRecord from '../use-edited-entity-record'; + +export function useIsSiteEditorLoading() { + const { isLoaded: hasLoadedPost } = useEditedEntityRecord(); + const [ loaded, setLoaded ] = useState( false ); + const inLoadingPause = useSelect( + ( select ) => { + const hasResolvingSelectors = + select( coreStore ).hasResolvingSelectors(); + return ! loaded && ! hasResolvingSelectors; + }, + [ loaded ] + ); + + useEffect( () => { + if ( inLoadingPause ) { + /* + * We're using an arbitrary 1s timeout here to catch brief moments + * without any resolving selectors that would result in displaying + * brief flickers of loading state and loaded state. + * + * It's worth experimenting with different values, since this also + * adds 1s of artificial delay after loading has finished. + */ + const timeout = setTimeout( () => { + setLoaded( true ); + }, 1000 ); + + return () => { + clearTimeout( timeout ); + }; + } + }, [ inLoadingPause ] ); + + return ! loaded || ! hasLoadedPost; +} diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 54b3f6ab22a1da..83527bae6d7e3b 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -11,7 +11,6 @@ import { __unstableMotion as motion, __unstableAnimatePresence as AnimatePresence, __unstableUseNavigateRegions as useNavigateRegions, - ResizableBox, } from '@wordpress/components'; import { useReducedMotion, @@ -42,30 +41,20 @@ import getIsListPage from '../../utils/get-is-list-page'; import Header from '../header-edit-mode'; import useInitEditedEntityFromURL from '../sync-state-with-url/use-init-edited-entity-from-url'; import SiteHub from '../site-hub'; -import ResizeHandle from '../block-editor/resize-handle'; +import ResizableFrame from '../resizable-frame'; import useSyncCanvasModeWithURL from '../sync-state-with-url/use-sync-canvas-mode-with-url'; import { unlock } from '../../private-apis'; import SavePanel from '../save-panel'; import KeyboardShortcutsRegister from '../keyboard-shortcuts/register'; import KeyboardShortcutsGlobal from '../keyboard-shortcuts/global'; import { useEditModeCommands } from '../../hooks/commands/use-edit-mode-commands'; +import { useIsSiteEditorLoading } from './hooks'; const { useCommands } = unlock( coreCommandsPrivateApis ); const { useCommandContext } = unlock( commandsPrivateApis ); const { useLocation } = unlock( routerPrivateApis ); const ANIMATION_DURATION = 0.5; -const emptyResizeHandleStyles = { - position: undefined, - userSelect: undefined, - cursor: undefined, - width: undefined, - height: undefined, - top: undefined, - right: undefined, - bottom: undefined, - left: undefined, -}; export default function Layout() { // This ensures the edited entity id and type are initialized properly. @@ -96,36 +85,26 @@ export default function Layout() { select( preferencesStore ).get( 'fixedToolbar' ), }; }, [] ); + const isEditing = canvasMode === 'edit'; const navigateRegionsProps = useNavigateRegions( { previous: previousShortcut, next: nextShortcut, } ); const disableMotion = useReducedMotion(); const isMobileViewport = useViewportMatch( 'medium', '<' ); - const canvasPadding = isMobileViewport ? 0 : 24; const showSidebar = ( isMobileViewport && ! isListPage ) || ( ! isMobileViewport && ( canvasMode === 'view' || ! isEditorPage ) ); const showCanvas = - ( isMobileViewport && isEditorPage && canvasMode === 'edit' ) || + ( isMobileViewport && isEditorPage && isEditing ) || ! isMobileViewport || ! isEditorPage; - const showFrame = - ( ! isEditorPage && ! isMobileViewport ) || - ( ! isMobileViewport && isEditorPage && canvasMode === 'view' ); const isFullCanvas = - ( isMobileViewport && isListPage ) || - ( isEditorPage && canvasMode === 'edit' ); + ( isMobileViewport && isListPage ) || ( isEditorPage && isEditing ); const [ canvasResizer, canvasSize ] = useResizeObserver(); - const [ fullResizer, fullSize ] = useResizeObserver(); - const [ forcedWidth, setForcedWidth ] = useState( null ); - const [ isResizing, setIsResizing ] = useState( false ); - const isResizingEnabled = ! isMobileViewport && canvasMode === 'view'; - const defaultSidebarWidth = isMobileViewport ? '100vw' : 360; - let canvasWidth = isResizing ? '100%' : fullSize.width; - if ( showFrame && ! isResizing ) { - canvasWidth = canvasSize.width - canvasPadding; - } + const [ fullResizer ] = useResizeObserver(); + const [ isResizing ] = useState( false ); + const isEditorLoading = useIsSiteEditorLoading(); // Sets the right context for the command center const commandContext = @@ -155,7 +134,7 @@ export default function Layout() { navigateRegionsProps.className, { 'is-full-canvas': isFullCanvas, - 'is-edit-mode': canvasMode === 'edit', + 'is-edit-mode': isEditing, 'has-fixed-toolbar': hasFixedToolbar, } ) } @@ -163,7 +142,7 @@ export default function Layout() { <SiteHub ref={ hubRef } className="edit-site-layout__hub" /> <AnimatePresence initial={ false }> - { isEditorPage && canvasMode === 'edit' && ( + { isEditorPage && isEditing && ( <NavigableRegion className="edit-site-layout__header" ariaLabel={ __( 'Editor top bar' ) } @@ -185,7 +164,7 @@ export default function Layout() { ease: 'easeOut', } } > - <Header /> + { isEditing && <Header /> } </NavigableRegion> ) } </AnimatePresence> @@ -193,8 +172,7 @@ export default function Layout() { <div className="edit-site-layout__content"> <AnimatePresence initial={ false }> { showSidebar && ( - <ResizableBox - as={ motion.div } + <motion.div initial={ { opacity: 0, } } @@ -206,69 +184,17 @@ export default function Layout() { } } transition={ { type: 'tween', - duration: - disableMotion || isResizing - ? 0 - : ANIMATION_DURATION, + duration: ANIMATION_DURATION, ease: 'easeOut', } } - size={ { - height: '100%', - width: - isResizingEnabled && forcedWidth - ? forcedWidth - : defaultSidebarWidth, - } } className="edit-site-layout__sidebar" - enable={ { - right: isResizingEnabled, - } } - onResizeStop={ ( event, direction, elt ) => { - setForcedWidth( elt.clientWidth ); - setIsResizing( false ); - } } - onResizeStart={ () => { - setIsResizing( true ); - } } - onResize={ ( event, direction, elt ) => { - // This is a performance optimization - // We set the width imperatively to avoid re-rendering - // the whole component while resizing. - hubRef.current.style.width = - elt.clientWidth - 48 + 'px'; - } } - handleComponent={ { - right: ( - <ResizeHandle - direction="right" - variation="separator" - resizeWidthBy={ ( delta ) => { - setForcedWidth( - ( forcedWidth ?? - defaultSidebarWidth ) + - delta - ); - } } - /> - ), - } } - handleClasses={ undefined } - handleStyles={ { - right: emptyResizeHandleStyles, - } } - minWidth={ isResizingEnabled ? 320 : undefined } - maxWidth={ - isResizingEnabled && fullSize - ? fullSize.width - 360 - : undefined - } > <NavigableRegion ariaLabel={ __( 'Navigation' ) } > <Sidebar /> </NavigableRegion> - </ResizableBox> + </motion.div> ) } </AnimatePresence> @@ -282,10 +208,6 @@ export default function Layout() { 'is-resizing': isResizing, } ) } - style={ { - paddingTop: showFrame ? canvasPadding : 0, - paddingBottom: showFrame ? canvasPadding : 0, - } } > { canvasResizer } { !! canvasSize.width && ( @@ -317,31 +239,22 @@ export default function Layout() { ease: 'easeOut', } } > - <motion.div - style={ { - position: 'absolute', - top: 0, - left: 0, - bottom: 0, - } } - initial={ false } - animate={ { - width: canvasWidth, - } } - transition={ { - type: 'tween', - duration: - disableMotion || isResizing - ? 0 - : ANIMATION_DURATION, - ease: 'easeOut', - } } - > - <ErrorBoundary> - { isEditorPage && <Editor /> } - { isListPage && <ListPage /> } - </ErrorBoundary> - </motion.div> + <ErrorBoundary> + { isEditorPage && ( + <ResizableFrame + isReady={ ! isEditorLoading } + isFullWidth={ isEditing } + oversizedClassName="edit-site-layout__resizable-frame-oversized" + > + <Editor + isLoading={ + isEditorLoading + } + /> + </ResizableFrame> + ) } + { isListPage && <ListPage /> } + </ErrorBoundary> </motion.div> ) } </div> diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 89a6fa3c9ccf18..ecb15aac8fe1e0 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -105,7 +105,13 @@ left: 0; bottom: 0; width: 100%; - overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + + &:has(.edit-site-layout__resizable-frame-oversized) { + justify-content: flex-end; + } & > div { color: $gray-900; @@ -243,5 +249,5 @@ z-index: 3; } } - } + diff --git a/packages/edit-site/src/components/resizable-frame/index.js b/packages/edit-site/src/components/resizable-frame/index.js new file mode 100644 index 00000000000000..f5dff65f3749b5 --- /dev/null +++ b/packages/edit-site/src/components/resizable-frame/index.js @@ -0,0 +1,253 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useState, useRef, useEffect } from '@wordpress/element'; +import { + ResizableBox, + __unstableMotion as motion, +} from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { unlock } from '../../private-apis'; +import { store as editSiteStore } from '../../store'; + +// Removes the inline styles in the drag handles. +const HANDLE_STYLES_OVERRIDE = { + position: undefined, + userSelect: undefined, + cursor: undefined, + width: undefined, + height: undefined, + top: undefined, + right: undefined, + bottom: undefined, + left: undefined, +}; + +// The minimum width of the frame (in px) while resizing. +const FRAME_MIN_WIDTH = 340; +// The reference width of the frame (in px) used to calculate the aspect ratio. +const FRAME_REFERENCE_WIDTH = 1300; +// 9 : 19.5 is the target aspect ratio enforced (when possible) while resizing. +const FRAME_TARGET_ASPECT_RATIO = 9 / 19.5; +// The minimum distance (in px) between the frame resize handle and the +// viewport's edge. If the frame is resized to be closer to the viewport's edge +// than this distance, then "canvas mode" will be enabled. +const SNAP_TO_EDIT_CANVAS_MODE_THRESHOLD = 200; + +function calculateNewHeight( width, initialAspectRatio ) { + const lerp = ( a, b, amount ) => { + return a + ( b - a ) * amount; + }; + + // Calculate the intermediate aspect ratio based on the current width. + const lerpFactor = + 1 - + Math.max( + 0, + Math.min( + 1, + ( width - FRAME_MIN_WIDTH ) / + ( FRAME_REFERENCE_WIDTH - FRAME_MIN_WIDTH ) + ) + ); + + // Calculate the height based on the intermediate aspect ratio + // ensuring the frame arrives at the target aspect ratio. + const intermediateAspectRatio = lerp( + initialAspectRatio, + FRAME_TARGET_ASPECT_RATIO, + lerpFactor + ); + + return width / intermediateAspectRatio; +} + +function ResizableFrame( { + isFullWidth, + isReady, + children, + oversizedClassName, +} ) { + const [ frameSize, setFrameSize ] = useState( { + width: '100%', + height: '100%', + } ); + // The width of the resizable frame when a new resize gesture starts. + const [ startingWidth, setStartingWidth ] = useState(); + const [ isResizing, setIsResizing ] = useState( false ); + const [ isHovering, setIsHovering ] = useState( false ); + const [ isOversized, setIsOversized ] = useState( false ); + const [ resizeRatio, setResizeRatio ] = useState( 1 ); + const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); + const initialAspectRatioRef = useRef( null ); + // The width of the resizable frame on initial render. + const initialComputedWidthRef = useRef( null ); + const FRAME_TRANSITION = { type: 'tween', duration: isResizing ? 0 : 0.5 }; + const frameRef = useRef( null ); + + // Remember frame dimensions on initial render. + useEffect( () => { + const { offsetWidth, offsetHeight } = frameRef.current.resizable; + initialComputedWidthRef.current = offsetWidth; + initialAspectRatioRef.current = offsetWidth / offsetHeight; + }, [] ); + + const handleResizeStart = ( _event, _direction, ref ) => { + // Remember the starting width so we don't have to get `ref.offsetWidth` on + // every resize event thereafter, which will cause layout thrashing. + setStartingWidth( ref.offsetWidth ); + setIsResizing( true ); + }; + + // Calculate the frame size based on the window width as its resized. + const handleResize = ( _event, _direction, _ref, delta ) => { + const normalizedDelta = delta.width / resizeRatio; + const deltaAbs = Math.abs( normalizedDelta ); + const maxDoubledDelta = + delta.width < 0 // is shrinking + ? deltaAbs + : ( initialComputedWidthRef.current - startingWidth ) / 2; + const deltaToDouble = Math.min( deltaAbs, maxDoubledDelta ); + const doubleSegment = deltaAbs === 0 ? 0 : deltaToDouble / deltaAbs; + const singleSegment = 1 - doubleSegment; + + setResizeRatio( singleSegment + doubleSegment * 2 ); + + const updatedWidth = startingWidth + delta.width; + + setIsOversized( updatedWidth > initialComputedWidthRef.current ); + + // Width will be controlled by the library (via `resizeRatio`), + // so we only need to update the height. + setFrameSize( { + height: isOversized + ? '100%' + : calculateNewHeight( + updatedWidth, + initialAspectRatioRef.current + ), + } ); + }; + + const handleResizeStop = ( _event, _direction, ref ) => { + setIsResizing( false ); + + if ( ! isOversized ) { + return; + } + + setIsOversized( false ); + + const remainingWidth = + ref.ownerDocument.documentElement.offsetWidth - ref.offsetWidth; + + if ( remainingWidth > SNAP_TO_EDIT_CANVAS_MODE_THRESHOLD ) { + // Reset the initial aspect ratio if the frame is resized slightly + // above the sidebar but not far enough to trigger full screen. + setFrameSize( { width: '100%', height: '100%' } ); + } else { + // Trigger full screen if the frame is resized far enough to the left. + setCanvasMode( 'edit' ); + } + }; + + const frameAnimationVariants = { + default: { + flexGrow: 0, + height: frameSize.height, + }, + fullWidth: { + flexGrow: 1, + height: frameSize.height, + }, + }; + + return ( + <ResizableBox + as={ motion.div } + ref={ frameRef } + initial={ false } + variants={ frameAnimationVariants } + animate={ isFullWidth ? 'fullWidth' : 'default' } + onAnimationComplete={ ( definition ) => { + if ( definition === 'fullWidth' ) + setFrameSize( { width: '100%', height: '100%' } ); + } } + transition={ FRAME_TRANSITION } + size={ frameSize } + enable={ { + top: false, + right: false, + bottom: false, + // Resizing will be disabled until the editor content is loaded. + left: isReady, + topRight: false, + bottomRight: false, + bottomLeft: false, + topLeft: false, + } } + resizeRatio={ resizeRatio } + handleClasses={ undefined } + handleStyles={ { + left: HANDLE_STYLES_OVERRIDE, + right: HANDLE_STYLES_OVERRIDE, + } } + minWidth={ FRAME_MIN_WIDTH } + maxWidth={ isFullWidth ? '100%' : '150%' } + maxHeight={ '100%' } + onMouseOver={ () => setIsHovering( true ) } + onMouseOut={ () => setIsHovering( false ) } + handleComponent={ { + left: + isHovering || isResizing ? ( + <motion.div + key="handle" + className="edit-site-resizable-frame__handle" + title="Drag to resize" + initial={ { + opacity: 0, + left: 0, + } } + animate={ { + opacity: 1, + left: -15, + } } + exit={ { + opacity: 0, + left: 0, + } } + whileHover={ { scale: 1.1 } } + /> + ) : null, + } } + onResizeStart={ handleResizeStart } + onResize={ handleResize } + onResizeStop={ handleResizeStop } + className={ classnames( 'edit-site-resizable-frame__inner', { + 'is-resizing': isResizing, + [ oversizedClassName ]: isOversized, + } ) } + > + <motion.div + className="edit-site-resizable-frame__inner-content" + animate={ { + borderRadius: isFullWidth ? 0 : 8, + } } + transition={ FRAME_TRANSITION } + > + { children } + </motion.div> + </ResizableBox> + ); +} + +export default ResizableFrame; diff --git a/packages/edit-site/src/components/resizable-frame/style.scss b/packages/edit-site/src/components/resizable-frame/style.scss new file mode 100644 index 00000000000000..2bd478b9bf9916 --- /dev/null +++ b/packages/edit-site/src/components/resizable-frame/style.scss @@ -0,0 +1,69 @@ +.edit-site-resizable-frame__inner { + position: relative; + + &.is-resizing { + @at-root { + body:has(&) { + cursor: col-resize; + user-select: none; + -webkit-user-select: none; + } + } + + &::before { + // This covers the whole content which ensures mouse up triggers + // even if the content is "inert". + position: absolute; + z-index: 1; + inset: 0; + content: ""; + } + } +} + +.edit-site-resizable-frame__inner-content { + position: absolute; + z-index: 0; + inset: 0; +} + +.edit-site-resizable-frame__handle { + position: absolute; + width: 5px; + height: 50px; + background-color: rgba(255, 255, 255, 0.3); + z-index: 100; + border-radius: 5px; + cursor: col-resize; + display: flex; + align-items: center; + justify-content: flex-end; + top: 50%; + &::before { + position: absolute; + left: 100%; + height: 100%; + width: $grid-unit-30; + content: ""; + } + + &::after { + position: absolute; + right: 100%; + height: 100%; + width: $grid-unit-30; + content: ""; + } + + &:hover { + background-color: var(--wp-admin-theme-color); + } + + .edit-site-resizable-frame__handle-label { + border-radius: 2px; + background: var(--wp-admin-theme-color); + padding: 4px 8px; + color: #fff; + margin-right: $grid-unit-10; + } +} diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 7a0233ebee5247..3be15cd02d2599 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -34,6 +34,7 @@ @import "./components/site-icon/style.scss"; @import "./components/style-book/style.scss"; @import "./components/editor-canvas-container/style.scss"; +@import "./components/resizable-frame/style.scss"; @import "./hooks/push-changes-to-global-styles/style.scss"; html #wpadminbar { diff --git a/test/e2e/specs/site-editor/style-book.spec.js b/test/e2e/specs/site-editor/style-book.spec.js index 6231175584554b..b7002c6dea8f0e 100644 --- a/test/e2e/specs/site-editor/style-book.spec.js +++ b/test/e2e/specs/site-editor/style-book.spec.js @@ -173,7 +173,10 @@ class StyleBook { async open() { await this.disableWelcomeGuide(); - await this.page.click( 'role=button[name="Styles"i]' ); - await this.page.click( 'role=button[name="Style Book"i]' ); + await this.page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Styles' } ) + .click(); + await this.page.getByRole( 'button', { name: 'Style Book' } ).click(); } } From 3b718783c04dc7bb24865873526c71c5277bede5 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Fri, 19 May 2023 22:41:57 +0300 Subject: [PATCH 105/131] Site Editor navigation: Add corresponding area icon to template part menu items (#50791) --- .../sidebar-navigation-screen-templates/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js index 8c9c5638884737..9ce4b5eccfb9b7 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js @@ -11,6 +11,7 @@ import { useEntityRecords } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; import { decodeEntities } from '@wordpress/html-entities'; import { useViewportMatch } from '@wordpress/compose'; +import { getTemplatePartIcon } from '@wordpress/editor'; /** * Internal dependencies @@ -83,7 +84,7 @@ export default function SidebarNavigationScreenTemplates() { } ); const canCreate = ! isMobileViewport && ! isTemplatePartsMode; - + const isTemplateList = postType === 'wp_template'; return ( <SidebarNavigationScreen isRoot={ isTemplatePartsMode } @@ -115,6 +116,10 @@ export default function SidebarNavigationScreenTemplates() { postId={ template.id } key={ template.id } withChevron + icon={ + ! isTemplateList && + getTemplatePartIcon( template.area ) + } > { decodeEntities( template.title?.rendered || From 55847136109b13502078c620ac6921f707157405 Mon Sep 17 00:00:00 2001 From: Alex Lende <alex@lende.xyz> Date: Fri, 19 May 2023 15:02:59 -0500 Subject: [PATCH 106/131] Better error message when theme.json styles use a duotone preset not in settings (#50714) * Show a more specific error when theme.json styles use a preset not from settings * Remove the period in the error message * Add gutenberg domain to translation --- lib/class-wp-duotone-gutenberg.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/class-wp-duotone-gutenberg.php b/lib/class-wp-duotone-gutenberg.php index b461e4ba0a64fc..b18121086bae0e 100644 --- a/lib/class-wp-duotone-gutenberg.php +++ b/lib/class-wp-duotone-gutenberg.php @@ -712,6 +712,15 @@ private static function enqueue_custom_filter( $filter_id, $duotone_selector, $f * @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-blue-orange)' or 'unset'. */ private static function enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value ) { + if ( ! array_key_exists( $filter_id, self::$global_styles_presets ) ) { + $error_message = sprintf( + /* translators: %s: duotone filter ID */ + __( 'The duotone id "%s" is not registered in theme.json settings', 'gutenberg' ), + $filter_id + ); + _doing_it_wrong( __METHOD__, $error_message, '6.3.0' ); + return; + } self::$used_global_styles_presets[ $filter_id ] = self::$global_styles_presets[ $filter_id ]; self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, self::$global_styles_presets[ $filter_id ] ); } From dc3832f0f5ddac3ad4ce2cb95999e8b2f26aa2e3 Mon Sep 17 00:00:00 2001 From: Dave Smith <getdavemail@gmail.com> Date: Fri, 19 May 2023 22:42:45 +0100 Subject: [PATCH 107/131] Fix inconsistent Link UI in Nav block list view editor (#50774) Co-authored-by: Ben Dwyer <ben@scruffian.com> Co-authored-by: Jerry Jones <jones.jeremydavid@gmail.com> --- .../src/components/list-view/appender.js | 5 +- .../components/list-view/block-contents.js | 10 +- .../src/components/list-view/block.js | 2 + .../src/components/list-view/index.js | 8 ++ .../src/store/private-selectors.js | 10 -- .../src/store/test/private-selectors.js | 30 +---- .../src/navigation-link/use-inserted-block.js | 43 ------ .../src/navigation/edit/leaf-more-menu.js | 15 ++- .../edit/menu-inspector-controls.js | 80 +++++------- .../specs/editor/blocks/navigation.spec.js | 122 +++++++++++++++++- 10 files changed, 190 insertions(+), 135 deletions(-) delete mode 100644 packages/block-library/src/navigation-link/use-inserted-block.js diff --git a/packages/block-editor/src/components/list-view/appender.js b/packages/block-editor/src/components/list-view/appender.js index a006b91e860c20..cb731bbf227a8b 100644 --- a/packages/block-editor/src/components/list-view/appender.js +++ b/packages/block-editor/src/components/list-view/appender.js @@ -4,7 +4,7 @@ import { useInstanceId } from '@wordpress/compose'; import { speak } from '@wordpress/a11y'; import { useSelect } from '@wordpress/data'; -import { forwardRef, useState, useEffect } from '@wordpress/element'; +import { forwardRef, useEffect } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; /** @@ -12,11 +12,12 @@ import { __, sprintf } from '@wordpress/i18n'; */ import { store as blockEditorStore } from '../../store'; import useBlockDisplayTitle from '../block-title/use-block-display-title'; +import { useListViewContext } from './context'; import Inserter from '../inserter'; export const Appender = forwardRef( ( { nestingLevel, blockCount, clientId, ...props }, ref ) => { - const [ insertedBlock, setInsertedBlock ] = useState( null ); + const { insertedBlock, setInsertedBlock } = useListViewContext(); const instanceId = useInstanceId( Appender ); const { hideInserter } = useSelect( diff --git a/packages/block-editor/src/components/list-view/block-contents.js b/packages/block-editor/src/components/list-view/block-contents.js index a1f5f3562cfd40..37bc35c6a528d6 100644 --- a/packages/block-editor/src/components/list-view/block-contents.js +++ b/packages/block-editor/src/components/list-view/block-contents.js @@ -47,7 +47,8 @@ const ListViewBlockContents = forwardRef( [ clientId ] ); - const { renderAdditionalBlockUI } = useListViewContext(); + const { renderAdditionalBlockUI, insertedBlock, setInsertedBlock } = + useListViewContext(); const isBlockMoveTarget = blockMovingClientId && selectedBlockInBlockEditor === clientId; @@ -66,7 +67,12 @@ const ListViewBlockContents = forwardRef( return ( <> - { renderAdditionalBlockUI && renderAdditionalBlockUI( block ) } + { renderAdditionalBlockUI && + renderAdditionalBlockUI( + block, + insertedBlock, + setInsertedBlock + ) } <BlockDraggable clientIds={ draggableClientIds }> { ( { draggable, onDragStart, onDragEnd } ) => ( <ListViewBlockSelectButton diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index 1afde32cf1ec61..004eb9061cf20e 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -135,6 +135,7 @@ function ListViewBlock( { BlockSettingsMenu, listViewInstanceId, expandedState, + setInsertedBlock, } = useListViewContext(); const hasSiblings = siblingBlockCount > 0; @@ -339,6 +340,7 @@ function ListViewBlock( { __experimentalSelectBlock={ updateSelection } expand={ expand } expandedState={ expandedState } + setInsertedBlock={ setInsertedBlock } /> ) } </TreeGridCell> diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 60c08abaf90895..298e3336ffebcd 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -16,6 +16,7 @@ import { useRef, useReducer, forwardRef, + useState, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -128,6 +129,9 @@ function ListViewComponent( const treeGridRef = useMergeRefs( [ elementRef, dropZoneRef, ref ] ); const isMounted = useRef( false ); + + const [ insertedBlock, setInsertedBlock ] = useState( null ); + const { setSelectedTreeId } = useListViewExpandSelectedItem( { firstSelectedBlockClientId: selectedClientIds[ 0 ], setExpandedState, @@ -212,6 +216,8 @@ function ListViewComponent( BlockSettingsMenu, listViewInstanceId: instanceId, renderAdditionalBlockUI, + insertedBlock, + setInsertedBlock, } ), [ draggedClientIds, @@ -221,6 +227,8 @@ function ListViewComponent( BlockSettingsMenu, instanceId, renderAdditionalBlockUI, + insertedBlock, + setInsertedBlock, ] ); diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 60712e6b8eb6e0..dede6eccccbe50 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -8,13 +8,3 @@ export function isBlockInterfaceHidden( state ) { return state.isBlockInterfaceHidden; } - -/** - * Gets the client ids of the last inserted blocks. - * - * @param {Object} state Global application state. - * @return {Array|undefined} Client Ids of the last inserted block(s). - */ -export function getLastInsertedBlocksClientIds( state ) { - return state?.lastBlockInserted?.clientIds; -} diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js index c5df265f75db35..2c287ceda0f88f 100644 --- a/packages/block-editor/src/store/test/private-selectors.js +++ b/packages/block-editor/src/store/test/private-selectors.js @@ -1,10 +1,7 @@ /** * Internal dependencies */ -import { - isBlockInterfaceHidden, - getLastInsertedBlocksClientIds, -} from '../private-selectors'; +import { isBlockInterfaceHidden } from '../private-selectors'; describe( 'private selectors', () => { describe( 'isBlockInterfaceHidden', () => { @@ -24,29 +21,4 @@ describe( 'private selectors', () => { expect( isBlockInterfaceHidden( state ) ).toBe( false ); } ); } ); - - describe( 'getLastInsertedBlocksClientIds', () => { - it( 'should return undefined if no blocks have been inserted', () => { - const state = { - lastBlockInserted: {}, - }; - - expect( getLastInsertedBlocksClientIds( state ) ).toEqual( - undefined - ); - } ); - - it( 'should return clientIds if blocks have been inserted', () => { - const state = { - lastBlockInserted: { - clientIds: [ '123456', '78910' ], - }, - }; - - expect( getLastInsertedBlocksClientIds( state ) ).toEqual( [ - '123456', - '78910', - ] ); - } ); - } ); } ); diff --git a/packages/block-library/src/navigation-link/use-inserted-block.js b/packages/block-library/src/navigation-link/use-inserted-block.js deleted file mode 100644 index 2644ca2e04f514..00000000000000 --- a/packages/block-library/src/navigation-link/use-inserted-block.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as blockEditorStore } from '@wordpress/block-editor'; - -export const useInsertedBlock = ( insertedBlockClientId ) => { - const { insertedBlockAttributes, insertedBlockName } = useSelect( - ( select ) => { - const { getBlockName, getBlockAttributes } = - select( blockEditorStore ); - - return { - insertedBlockAttributes: getBlockAttributes( - insertedBlockClientId - ), - insertedBlockName: getBlockName( insertedBlockClientId ), - }; - }, - [ insertedBlockClientId ] - ); - - const { updateBlockAttributes } = useDispatch( blockEditorStore ); - - const setInsertedBlockAttributes = ( _updatedAttributes ) => { - if ( ! insertedBlockClientId ) return; - updateBlockAttributes( insertedBlockClientId, _updatedAttributes ); - }; - - if ( ! insertedBlockClientId ) { - return { - insertedBlockAttributes: undefined, - insertedBlockName: undefined, - setInsertedBlockAttributes, - }; - } - - return { - insertedBlockAttributes, - insertedBlockName, - setInsertedBlockAttributes, - }; -}; diff --git a/packages/block-library/src/navigation/edit/leaf-more-menu.js b/packages/block-library/src/navigation/edit/leaf-more-menu.js index f57335ce2bef60..4fe45ac5def83f 100644 --- a/packages/block-library/src/navigation/edit/leaf-more-menu.js +++ b/packages/block-library/src/navigation/edit/leaf-more-menu.js @@ -24,7 +24,13 @@ const BLOCKS_THAT_CAN_BE_CONVERTED_TO_SUBMENU = [ 'core/navigation-submenu', ]; -function AddSubmenuItem( { block, onClose, expandedState, expand } ) { +function AddSubmenuItem( { + block, + onClose, + expandedState, + expand, + setInsertedBlock, +} ) { const { insertBlock, replaceBlock, replaceInnerBlocks } = useDispatch( blockEditorStore ); @@ -69,6 +75,12 @@ function AddSubmenuItem( { block, onClose, expandedState, expand } ) { updateSelectionOnInsert ); } + + // This call sets the local List View state for the "last inserted block". + // This is required for the Nav Block to determine whether or not to display + // the Link UI for this new block. + setInsertedBlock( newLink ); + if ( ! expandedState[ block.clientId ] ) { expand( block.clientId ); } @@ -138,6 +150,7 @@ export default function LeafMoreMenu( props ) { expanded expandedState={ props.expandedState } expand={ props.expand } + setInsertedBlock={ props.setInsertedBlock } /> </MenuGroup> <MenuGroup> diff --git a/packages/block-library/src/navigation/edit/menu-inspector-controls.js b/packages/block-library/src/navigation/edit/menu-inspector-controls.js index d4c9506c51d8c0..23f005beb43502 100644 --- a/packages/block-library/src/navigation/edit/menu-inspector-controls.js +++ b/packages/block-library/src/navigation/edit/menu-inspector-controls.js @@ -12,8 +12,7 @@ import { __experimentalHeading as Heading, Spinner, } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; -import { useState, useEffect } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; import { __, sprintf } from '@wordpress/i18n'; /** @@ -26,7 +25,6 @@ import useNavigationMenu from '../use-navigation-menu'; import LeafMoreMenu from './leaf-more-menu'; import { updateAttributes } from '../../navigation-link/update-attributes'; import { LinkUI } from '../../navigation-link/link-ui'; -import { useInsertedBlock } from '../../navigation-link/use-inserted-block'; /* translators: %s: The name of a menu. */ const actionLabel = __( "Switch to '%s'" ); @@ -54,40 +52,13 @@ const MainContent = ( { [ clientId ] ); - const [ clientIdWithOpenLinkUI, setClientIdWithOpenLinkUI ] = useState(); - const { lastInsertedBlockClientId } = useSelect( ( select ) => { - const { getLastInsertedBlocksClientIds } = unlock( - select( blockEditorStore ) - ); - const lastInsertedBlocksClientIds = getLastInsertedBlocksClientIds(); - return { - lastInsertedBlockClientId: - lastInsertedBlocksClientIds && lastInsertedBlocksClientIds[ 0 ], - }; - }, [] ); + const { updateBlockAttributes } = useDispatch( blockEditorStore ); - const { - insertedBlockAttributes, - insertedBlockName, - setInsertedBlockAttributes, - } = useInsertedBlock( lastInsertedBlockClientId ); - - const hasExistingLinkValue = insertedBlockAttributes?.url; - - useEffect( () => { - if ( - lastInsertedBlockClientId && - BLOCKS_WITH_LINK_UI_SUPPORT?.includes( insertedBlockName ) && - ! hasExistingLinkValue // don't re-show the Link UI if the block already has a link value. - ) { - setClientIdWithOpenLinkUI( lastInsertedBlockClientId ); - } - }, [ - lastInsertedBlockClientId, - clientId, - insertedBlockName, - hasExistingLinkValue, - ] ); + const setInsertedBlockAttributes = + ( _insertedBlockClientId ) => ( _updatedAttributes ) => { + if ( ! _insertedBlockClientId ) return; + updateBlockAttributes( _insertedBlockClientId, _updatedAttributes ); + }; const { navigationMenu } = useNavigationMenu( currentMenuId ); @@ -109,23 +80,42 @@ const MainContent = ( { 'You have not yet created any menus. Displaying a list of your Pages' ); - const renderLinkUI = ( block ) => { + const renderLinkUI = ( + currentBlock, + lastInsertedBlock, + setLastInsertedBlock + ) => { + const blockSupportsLinkUI = BLOCKS_WITH_LINK_UI_SUPPORT?.includes( + lastInsertedBlock?.name + ); + const currentBlockWasJustInserted = + lastInsertedBlock?.clientId === currentBlock.clientId; + + const shouldShowLinkUIForBlock = + blockSupportsLinkUI && currentBlockWasJustInserted; + return ( - clientIdWithOpenLinkUI === block.clientId && ( + shouldShowLinkUIForBlock && ( <LinkUI - clientId={ lastInsertedBlockClientId } - link={ insertedBlockAttributes } - onClose={ () => setClientIdWithOpenLinkUI( null ) } + clientId={ lastInsertedBlock?.clientId } + link={ lastInsertedBlock?.attributes } + onClose={ () => { + setLastInsertedBlock( null ); + } } hasCreateSuggestion={ false } onChange={ ( updatedValue ) => { updateAttributes( updatedValue, - setInsertedBlockAttributes, - insertedBlockAttributes + setInsertedBlockAttributes( + lastInsertedBlock?.clientId + ), + lastInsertedBlock?.attributes ); - setClientIdWithOpenLinkUI( null ); + setLastInsertedBlock( null ); + } } + onCancel={ () => { + setLastInsertedBlock( null ); } } - onCancel={ () => setClientIdWithOpenLinkUI( null ) } /> ) ); diff --git a/test/e2e/specs/editor/blocks/navigation.spec.js b/test/e2e/specs/editor/blocks/navigation.spec.js index 103e4bc9d57150..835b05e570e992 100644 --- a/test/e2e/specs/editor/blocks/navigation.spec.js +++ b/test/e2e/specs/editor/blocks/navigation.spec.js @@ -522,9 +522,25 @@ test.describe( 'Navigation block', () => { linkControl, } ) => { await admin.createNewPost(); - await requestUtils.createNavigationMenu( navMenuBlocksFixture ); + const { id: menuId } = await requestUtils.createNavigationMenu( + navMenuBlocksFixture + ); - await editor.insertBlock( { name: 'core/navigation' } ); + // Insert x2 blocks as a stress test as several bugs have been found with inserting + // blocks into the navigation block when there are multiple blocks referencing the + // **same** menu. + await editor.insertBlock( { + name: 'core/navigation', + attributes: { + ref: menuId, + }, + } ); + await editor.insertBlock( { + name: 'core/navigation', + attributes: { + ref: menuId, + }, + } ); await editor.openDocumentSettingsSidebar(); @@ -572,11 +588,17 @@ test.describe( 'Navigation block', () => { // Expect to see the Link creation UI be focused. const linkUIInput = linkControl.getSearchInput(); + // Coverage for bug whereby Link UI input would be incorrectly prepopulated. + // It should: + // - be focused - should not be in "preview" mode but rather ready to accept input. + // - be empty - not pre-populated + // See: https://github.com/WordPress/gutenberg/issues/50733 await expect( linkUIInput ).toBeFocused(); + await expect( linkUIInput ).toBeEmpty(); const firstResult = await linkControl.getNthSearchResult( 0 ); - // Grab the text from the first result so we can check it was inserted. + // Grab the text from the first result so we can check (later on) that it was inserted. const firstResultText = await linkControl.getSearchResultText( firstResult ); @@ -821,6 +843,92 @@ test.describe( 'Navigation block', () => { .getByText( 'Top Level Item 1' ) ).toBeVisible(); } ); + + test( `does not display link interface for blocks that have not just been inserted`, async ( { + admin, + page, + editor, + requestUtils, + linkControl, + } ) => { + // Provides coverage for a bug whereby the Link UI would be unexpectedly displayed for the last + // inserted block even if the block had been deselected and then reselected. + // See: https://github.com/WordPress/gutenberg/issues/50601 + + await admin.createNewPost(); + const { id: menuId } = await requestUtils.createNavigationMenu( + navMenuBlocksFixture + ); + + // Insert x2 blocks as a stress test as several bugs have been found with inserting + // blocks into the navigation block when there are multiple blocks referencing the + // **same** menu. + await editor.insertBlock( { + name: 'core/navigation', + attributes: { + ref: menuId, + }, + } ); + await editor.insertBlock( { + name: 'core/navigation', + attributes: { + ref: menuId, + }, + } ); + + await editor.openDocumentSettingsSidebar(); + + const listView = page.getByRole( 'treegrid', { + name: 'Block navigation structure', + description: 'Structure for navigation menu: Test Menu', + } ); + + await listView + .getByRole( 'button', { + name: 'Add block', + } ) + .click(); + + const blockResults = page.getByRole( 'listbox', { + name: 'Blocks', + } ); + + await expect( blockResults ).toBeVisible(); + + const blockResultOptions = blockResults.getByRole( 'option' ); + + // Select the Page Link option. + await blockResultOptions.nth( 0 ).click(); + + // Immediately dismiss the Link UI thereby not populating the `url` attribute + // of the block. + await linkControl.pressCancel(); + + // Get the Inspector Tabs. + const blockSettings = page.getByRole( 'region', { + name: 'Editor settings', + } ); + + // Trigger "unmount" of the List View. + await blockSettings + .getByRole( 'tab', { + name: 'Settings', + } ) + .click(); + + // "Remount" the List View. + // this is where the bug previously occurred. + await blockSettings + .getByRole( 'tab', { + name: 'List View', + } ) + .click(); + + // Check that despite being the last inserted block, the Link UI is not displayed + // in this scenario because it was not **just** inserted into the List View (i.e. + // we have unmounted the list view and then remounted it). + await expect( linkControl.getSearchInput() ).not.toBeVisible(); + } ); } ); } ); @@ -1159,6 +1267,14 @@ class LinkControl { } ); } + async pressCancel() { + const cancelButton = this.page.getByRole( 'button', { + name: 'Cancel', + } ); + + return cancelButton.click(); + } + async getSearchResults() { const searchInput = this.getSearchInput(); From 3dc64ad8bafe7b197a933a2a3a4aa1c082215e99 Mon Sep 17 00:00:00 2001 From: antpb <itartist.pdf@gmail.com> Date: Fri, 19 May 2023 20:07:02 -0500 Subject: [PATCH 108/131] Process template part shortcodes before blocks (#50801) * Process shortcodes before processing blocks so that dynamic blocks, by default, do not have shortcodes expanded * Revert "Process shortcodes before processing blocks so that dynamic blocks, by default, do not have shortcodes expanded" This reverts commit 00374e02f40c6a70f561609393023f3a7695662c. * Process shortcodes before processing blocks so that dynamic blocks, by default, do not have shortcodes expanded --- packages/block-library/src/template-part/index.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/template-part/index.php b/packages/block-library/src/template-part/index.php index e12f67566a0fdb..d3de7d0b3afbd5 100644 --- a/packages/block-library/src/template-part/index.php +++ b/packages/block-library/src/template-part/index.php @@ -143,14 +143,14 @@ function render_block_core_template_part( $attributes ) { } // Run through the actions that are typically taken on the_content. + $content = shortcode_unautop( $content ); + $content = do_shortcode( $content ); $seen_ids[ $template_part_id ] = true; $content = do_blocks( $content ); unset( $seen_ids[ $template_part_id ] ); $content = wptexturize( $content ); $content = convert_smilies( $content ); - $content = shortcode_unautop( $content ); $content = wp_filter_content_tags( $content, "template_part_{$area}" ); - $content = do_shortcode( $content ); // Handle embeds for block template parts. global $wp_embed; From e6191190d908c77e941f145d91baaa76cc275a6f Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Sat, 20 May 2023 02:08:09 +0000 Subject: [PATCH 109/131] Bump plugin version to 15.8.1 --- gutenberg.php | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 612a4325fd2bc0..dbf58c000a107f 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.1 * Requires PHP: 5.6 - * Version: 15.8.0 + * Version: 15.8.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 041a64c8600e77..5833c451c403be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "15.8.0", + "version": "15.8.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4d4729eff61741..e67cb21cfaf44b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "15.8.0", + "version": "15.8.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From f861fa025971767c5214ea128d6125dee564f2bc Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Sat, 20 May 2023 02:17:42 +0000 Subject: [PATCH 110/131] Update Changelog for 15.8.1 --- changelog.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/changelog.txt b/changelog.txt index dee5a1671cf931..5e96e8b0959996 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,17 @@ == Changelog == += 15.8.1 = + + + +Error: There are no unreleased pull requests associated with the milestone. + at fetchAllPullRequests (/home/runner/work/gutenberg/gutenberg/bin/plugin/commands/changelog.js:684:10) + at processTicksAndRejections (internal/process/task_queues.js:95:5) + at async createChangelog (/home/runner/work/gutenberg/gutenberg/bin/plugin/commands/changelog.js:994:24) + at async getReleaseChangelog (/home/runner/work/gutenberg/gutenberg/bin/plugin/commands/changelog.js:1020:2) + at async Command.<anonymous> (/home/runner/work/gutenberg/gutenberg/bin/plugin/cli.js:11:4) + + = 15.8.0 = ## Contributors From dc2eebac1181c6584da25fb7882257faa47b8f48 Mon Sep 17 00:00:00 2001 From: Jerry Jones <jones.jeremydavid@gmail.com> Date: Sat, 20 May 2023 00:12:31 -0500 Subject: [PATCH 111/131] Remove unintentionally added test artifact (#50795) --- test/e2e/artifacts/storage-states/admin.json | 66 -------------------- 1 file changed, 66 deletions(-) delete mode 100644 test/e2e/artifacts/storage-states/admin.json diff --git a/test/e2e/artifacts/storage-states/admin.json b/test/e2e/artifacts/storage-states/admin.json deleted file mode 100644 index cae09dbcd0eac7..00000000000000 --- a/test/e2e/artifacts/storage-states/admin.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "cookies": [ - { - "name": "wordpress_test_cookie", - "value": "WP%20Cookie%20check", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": false, - "secure": false, - "sameSite": "Lax" - }, - { - "name": "wordpress_23778236db82f19306f247e20a353a99", - "value": "admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C45404d74460259bc9148f2357f2180af488d65921b10ed5981fff860afa5c8ca", - "domain": "localhost", - "path": "/wp-content/plugins", - "expires": -1, - "httpOnly": true, - "secure": false, - "sameSite": "Lax" - }, - { - "name": "wordpress_23778236db82f19306f247e20a353a99", - "value": "admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C45404d74460259bc9148f2357f2180af488d65921b10ed5981fff860afa5c8ca", - "domain": "localhost", - "path": "/wp-admin", - "expires": -1, - "httpOnly": true, - "secure": false, - "sameSite": "Lax" - }, - { - "name": "wordpress_logged_in_23778236db82f19306f247e20a353a99", - "value": "admin%7C1684446357%7Cdx01QADx42SHH9s4ikPkhkS07FzxnWES5FY2SsnwL7v%7C8ace4a8f867bc4e587d5264662296f90fcd133710cd0dd3386a92801816bd5d1", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": true, - "secure": false, - "sameSite": "Lax" - }, - { - "name": "wp-settings-1", - "value": "editor%3Dtinymce", - "domain": "localhost", - "path": "/", - "expires": 1715809558.14, - "httpOnly": false, - "secure": false, - "sameSite": "Lax" - }, - { - "name": "wp-settings-time-1", - "value": "1684273558", - "domain": "localhost", - "path": "/", - "expires": 1715809558.14, - "httpOnly": false, - "secure": false, - "sameSite": "Lax" - } - ], - "nonce": "fe590a7aae", - "rootURL": "http://localhost:8889/index.php?rest_route=/" -} From 1c0bde90445f144d35f5a137aa2673fd7c9c74a3 Mon Sep 17 00:00:00 2001 From: Kutsu <15826102+kutsu123@users.noreply.github.com> Date: Sat, 20 May 2023 16:12:07 +0900 Subject: [PATCH 112/131] add grab cursor style for mover button (#50808) --- packages/block-editor/src/components/block-mover/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-editor/src/components/block-mover/style.scss b/packages/block-editor/src/components/block-mover/style.scss index 9a20f7d8e3e3d4..c4ad57d673c025 100644 --- a/packages/block-editor/src/components/block-mover/style.scss +++ b/packages/block-editor/src/components/block-mover/style.scss @@ -55,6 +55,7 @@ } .block-editor-block-mover__drag-handle { + cursor: grab; @include break-small() { width: $block-toolbar-height * 0.5; min-width: 0 !important; // overrides default button width. From d42095cc303e8505e8f524e1df762713aae5b9bd Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Sun, 21 May 2023 01:56:20 +0900 Subject: [PATCH 113/131] Add t-hamano as codeowner for `env` package (#50817) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d578b20c6cb61e..c29a18dcee2e04 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -125,7 +125,7 @@ /packages/report-flaky-tests @kevin940726 # wp-env -/packages/env @noahtallen @ObliviousHarmony +/packages/env @noahtallen @ObliviousHarmony @t-hamano # PHP /lib @spacedmonkey From 2e40379781f0a188d46afcf4afefd0c1aaad1c5f Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Mon, 22 May 2023 08:06:49 +1000 Subject: [PATCH 114/131] We want to have at least 2 revisions before showing the revisions link. Removing the `+ 1` ensures this. (#50762) --- .../components/sidebar-edit-mode/template-revisions/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-revisions/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-revisions/index.js index 3782dccfb7e287..e2e6f48a7a966b 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-revisions/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-revisions/index.js @@ -19,8 +19,7 @@ const useRevisionData = () => { currentTemplate?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id ?? null; const revisionsCount = - ( currentTemplate?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0 ) + - 1; + currentTemplate?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; return { currentTemplate, From c40aad6f9e2f8f63df60d591be250eb2fe05b541 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 22 May 2023 14:43:44 +1000 Subject: [PATCH 115/131] Library: Rename template parts to library in nav (#50769) --- .../sidebar-navigation-item/style.scss | 9 +++++ .../sidebar-navigation-screen-main/index.js | 2 +- .../index.js | 39 +++++++++++++------ .../site-editor-url-navigation.spec.js | 2 +- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-item/style.scss b/packages/edit-site/src/components/sidebar-navigation-item/style.scss index fd9da59eaa888c..e9ec7ecf91909e 100644 --- a/packages/edit-site/src/components/sidebar-navigation-item/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-item/style.scss @@ -19,6 +19,15 @@ .edit-site-sidebar-navigation-item__drilldown-indicator { fill: $gray-700; } + + &:is(a) { + text-decoration: none; + + &:focus { + box-shadow: none; + outline: none; + } + } } .edit-site-sidebar-navigation-screen__content .block-editor-list-view-block-select-button { diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js index 7ad0dc07ae0f0e..2419383f72a03d 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js @@ -102,7 +102,7 @@ export default function SidebarNavigationScreenMain() { withChevron icon={ symbol } > - { __( 'Template Parts' ) } + { __( 'Library' ) } </NavigatorButton> </ItemGroup> } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js index 9ce4b5eccfb9b7..deea636781fb10 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js @@ -37,12 +37,13 @@ const config = { }, wp_template_part: { labels: { - title: __( 'Template parts' ), - loading: __( 'Loading template parts' ), - notFound: __( 'No template parts found' ), + title: __( 'Library' ), + loading: __( 'Loading library' ), + notFound: __( 'No patterns found' ), manage: __( 'Manage all template parts' ), + reusableBlocks: __( 'Manage reusable blocks' ), description: __( - 'Template Parts are small pieces of a layout that can be reused across multiple templates and always appear the same way. Common template parts include the site header, footer, or sidebar.' + 'Manage what patterns are available when editing your site.' ), }, }, @@ -128,14 +129,28 @@ export default function SidebarNavigationScreenTemplates() { </TemplateItem> ) ) } { ! isMobileViewport && ( - <SidebarNavigationItem - className="edit-site-sidebar-navigation-screen-templates__see-all" - { ...browseAllLink } - children={ - config[ postType ].labels.manage - } - withChevron - /> + <> + <SidebarNavigationItem + className="edit-site-sidebar-navigation-screen-templates__see-all" + withChevron + { ...browseAllLink } + > + { config[ postType ].labels.manage } + </SidebarNavigationItem> + { !! config[ postType ].labels + .reusableBlocks && ( + <SidebarNavigationItem + as="a" + href="edit.php?post_type=wp_block" + withChevron + > + { + config[ postType ].labels + .reusableBlocks + } + </SidebarNavigationItem> + ) } + </> ) } </ItemGroup> ) } diff --git a/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js b/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js index 5668832e4162c3..5bd1fbd29d52b7 100644 --- a/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js +++ b/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js @@ -62,7 +62,7 @@ test.describe( 'Site editor url navigation', () => { page, } ) => { await admin.visitSiteEditor(); - await page.click( 'role=button[name="Template Parts"i]' ); + await page.click( 'role=button[name="Library"i]' ); await page.click( 'role=button[name="Add New"i]' ); // Fill in a name in the dialog that pops up. await page.type( From 0b6a426040e430e660c75b72e0f7497f4651cb8a Mon Sep 17 00:00:00 2001 From: tomoki shimomura <shimotmk1104@gmail.com> Date: Mon, 22 May 2023 15:35:37 +0900 Subject: [PATCH 116/131] Fix column block category (#46048) * fix/column/category * docs --- docs/reference-guides/core-blocks.md | 2 +- packages/block-library/src/column/block.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 6f7314d5e51880..423676e2afe320 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -94,7 +94,7 @@ Display code snippets that respect your spacing and tabs. ([Source](https://gith A single column within a columns block. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/column)) - **Name:** core/column -- **Category:** text +- **Category:** design - **Supports:** anchor, color (background, gradients, link, text), spacing (blockGap, padding), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** allowedBlocks, templateLock, verticalAlignment, width diff --git a/packages/block-library/src/column/block.json b/packages/block-library/src/column/block.json index 3e2aad8865e21c..6c032d347248e1 100644 --- a/packages/block-library/src/column/block.json +++ b/packages/block-library/src/column/block.json @@ -3,7 +3,7 @@ "apiVersion": 2, "name": "core/column", "title": "Column", - "category": "text", + "category": "design", "parent": [ "core/columns" ], "description": "A single column within a columns block.", "textdomain": "default", From 8b12c1ca178d6376da1bcf7879a06cb77e1cd517 Mon Sep 17 00:00:00 2001 From: Falguni Desai <falgunihdesai@gmail.com> Date: Mon, 22 May 2023 12:49:42 +0400 Subject: [PATCH 117/131] Update border and focus style of the Input selector in ColorPicker Component (#50703) --- packages/components/CHANGELOG.md | 1 + packages/components/src/color-picker/styles.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 388aef8d5a3df5..83d74153ccb1cc 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,7 @@ - `Modal`: Remove children container's unused class name ([#50655](https://github.com/WordPress/gutenberg/pull/50655)). - `DropdownMenu`: Convert to TypeScript ([#50187](https://github.com/WordPress/gutenberg/pull/50187)). - Added experimental v2 of `DropdownMenu` ([#49473](https://github.com/WordPress/gutenberg/pull/49473)). +- `ColorPicker`: its private `SelectControl` component no longer hides BackdropUI, thus making its focus state visible for keyboard users ([#50703](https://github.com/WordPress/gutenberg/pull/50703)). ### Bug Fix diff --git a/packages/components/src/color-picker/styles.ts b/packages/components/src/color-picker/styles.ts index bf25707ba9c936..1592f6a201656a 100644 --- a/packages/components/src/color-picker/styles.ts +++ b/packages/components/src/color-picker/styles.ts @@ -30,7 +30,7 @@ export const SelectControl = styled( InnerSelectControl )` margin-left: ${ space( -2 ) }; width: 5em; ${ BackdropUI } { - display: none; + display: block; } `; From 801681b0be5f4a4551ffdb0a3ff08b797079db63 Mon Sep 17 00:00:00 2001 From: Dave Smith <getdavemail@gmail.com> Date: Mon, 22 May 2023 11:17:20 +0100 Subject: [PATCH 118/131] Remove all edit functionality from Navigation in Browse Mode (#50788) * Remove Add Submenu option * Remove Link UI entirely --- .../leaf-more-menu.js | 77 +------------------ .../navigation-menu-content.js | 68 +--------------- 2 files changed, 4 insertions(+), 141 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js index ab8c70fc852a62..7b7472c8e30bf2 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js @@ -1,13 +1,8 @@ /** * WordPress dependencies */ -import { createBlock } from '@wordpress/blocks'; -import { - addSubmenu, - chevronUp, - chevronDown, - moreVertical, -} from '@wordpress/icons'; + +import { chevronUp, chevronDown, moreVertical } from '@wordpress/icons'; import { DropdownMenu, MenuItem, MenuGroup } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { __, sprintf } from '@wordpress/i18n'; @@ -19,67 +14,6 @@ const POPOVER_PROPS = { variant: 'toolbar', }; -const BLOCKS_THAT_CAN_BE_CONVERTED_TO_SUBMENU = [ - 'core/navigation-link', - 'core/navigation-submenu', -]; - -function AddSubmenuItem( { block, onClose, expandedState, expand } ) { - const { insertBlock, replaceBlock, replaceInnerBlocks } = - useDispatch( blockEditorStore ); - - const clientId = block.clientId; - const isDisabled = ! BLOCKS_THAT_CAN_BE_CONVERTED_TO_SUBMENU.includes( - block.name - ); - return ( - <MenuItem - icon={ addSubmenu } - disabled={ isDisabled } - onClick={ () => { - const updateSelectionOnInsert = false; - const newLink = createBlock( 'core/navigation-link' ); - - if ( block.name === 'core/navigation-submenu' ) { - insertBlock( - newLink, - block.innerBlocks.length, - clientId, - updateSelectionOnInsert - ); - } else { - // Convert to a submenu if the block currently isn't one. - const newSubmenu = createBlock( - 'core/navigation-submenu', - block.attributes, - block.innerBlocks - ); - - // The following must happen as two independent actions. - // Why? Because the offcanvas editor relies on the getLastInsertedBlocksClientIds - // selector to determine which block is "active". As the UX needs the newLink to be - // the "active" block it must be the last block to be inserted. - // Therefore the Submenu is first created and **then** the newLink is inserted - // thus ensuring it is the last inserted block. - replaceBlock( clientId, newSubmenu ); - - replaceInnerBlocks( - newSubmenu.clientId, - [ newLink ], - updateSelectionOnInsert - ); - } - if ( ! expandedState[ block.clientId ] ) { - expand( block.clientId ); - } - onClose(); - } } - > - { __( 'Add submenu link' ) } - </MenuItem> - ); -} - export default function LeafMoreMenu( props ) { const { block } = props; const { clientId } = block; @@ -131,13 +65,6 @@ export default function LeafMoreMenu( props ) { > { __( 'Move down' ) } </MenuItem> - <AddSubmenuItem - block={ block } - onClose={ onClose } - expanded - expandedState={ props.expandedState } - expand={ props.expand } - /> </MenuGroup> <MenuGroup> <MenuItem diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/navigation-menu-content.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/navigation-menu-content.js index b205e77a65d73f..9d25db73d45b6d 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/navigation-menu-content.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/navigation-menu-content.js @@ -6,12 +6,10 @@ import { store as blockEditorStore, BlockList, BlockTools, - __experimentalLinkControl as LinkControl, } from '@wordpress/block-editor'; import { useDispatch, useSelect } from '@wordpress/data'; import { createBlock } from '@wordpress/blocks'; -import { Popover, VisuallyHidden } from '@wordpress/components'; -import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; +import { VisuallyHidden } from '@wordpress/components'; import { useCallback, useEffect, useState } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; @@ -21,33 +19,6 @@ import { store as coreStore } from '@wordpress/core-data'; import { unlock } from '../../private-apis'; import LeafMoreMenu from './leaf-more-menu'; -function CustomLinkAdditionalBlockUI( { block, onClose } ) { - const { updateBlockAttributes } = useDispatch( blockEditorStore ); - const { label, url, opensInNewTab } = block.attributes; - const link = { - url, - opensInNewTab, - title: label && stripHTML( label ), - }; - return ( - <Popover placement="bottom" shift onClose={ onClose }> - <LinkControl - hasTextControl - hasRichPreviews - value={ link } - onChange={ ( updatedValue ) => { - updateBlockAttributes( block.clientId, { - label: updatedValue.title, - url: updatedValue.url, - opensInNewTab: updatedValue.opensInNewTab, - } ); - onClose(); - } } - onCancel={ onClose } - /> - </Popover> - ); -} // Needs to be kept in sync with the query used at packages/block-library/src/page-list/edit.js. const MAX_PAGE_COUNT = 100; const PAGES_QUERY = [ @@ -102,29 +73,6 @@ export default function NavigationMenuContent( { rootClientId, onSelect } ) { const { replaceBlock, __unstableMarkNextChangeAsNotPersistent } = useDispatch( blockEditorStore ); - const [ customLinkEditPopoverOpenId, setIsCustomLinkEditPopoverOpenId ] = - useState( false ); - - const renderAdditionalBlockUICallback = useCallback( - ( block ) => { - if ( - customLinkEditPopoverOpenId && - block.clientId === customLinkEditPopoverOpenId - ) { - return ( - <CustomLinkAdditionalBlockUI - block={ block } - onClose={ () => { - setIsCustomLinkEditPopoverOpenId( false ); - } } - /> - ); - } - return null; - }, - [ customLinkEditPopoverOpenId, setIsCustomLinkEditPopoverOpenId ] - ); - // Delay loading stop by 50ms to avoid flickering. useEffect( () => { let timeoutId; @@ -156,22 +104,11 @@ export default function NavigationMenuContent( { rootClientId, onSelect } ) { block.clientId, createBlock( 'core/navigation-link', block.attributes ) ); - } else if ( - block.name === 'core/navigation-link' && - block.attributes.kind === 'custom' && - block.attributes.url - ) { - setIsCustomLinkEditPopoverOpenId( block.clientId ); } else { onSelect( block ); } }, - [ - onSelect, - __unstableMarkNextChangeAsNotPersistent, - replaceBlock, - setIsCustomLinkEditPopoverOpenId, - ] + [ onSelect, __unstableMarkNextChangeAsNotPersistent, replaceBlock ] ); // The hidden block is needed because it makes block edit side effects trigger. @@ -188,7 +125,6 @@ export default function NavigationMenuContent( { rootClientId, onSelect } ) { onSelect={ offCanvasOnselect } blockSettingsMenu={ LeafMoreMenu } showAppender={ false } - renderAdditionalBlockUI={ renderAdditionalBlockUICallback } /> ) } <VisuallyHidden aria-hidden="true"> From 1939eae338c4d9122c4ea0a9fe7e87c23d957b95 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 22 May 2023 11:32:30 +0100 Subject: [PATCH 119/131] Command center: Enable e2e tests (#50833) --- .../specs/site-editor/command-center.spec.js | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/test/e2e/specs/site-editor/command-center.spec.js b/test/e2e/specs/site-editor/command-center.spec.js index 936c8838db426e..9661a91a6abc78 100644 --- a/test/e2e/specs/site-editor/command-center.spec.js +++ b/test/e2e/specs/site-editor/command-center.spec.js @@ -17,36 +17,32 @@ test.describe( 'Site editor command center', () => { await admin.visitSiteEditor(); } ); - test.skip( 'Open the command center and navigate to the page create page', async ( { + test( 'Open the command center and navigate to the page create page', async ( { page, } ) => { + await page + .getByRole( 'button', { name: 'Open command center' } ) + .focus(); await page.keyboard.press( 'Meta+k' ); - const newPageButton = page.locator( - 'role=option[name="Create a new page"i]' - ); - await expect( newPageButton ).toBeVisible(); - - // Type a random post title - await page.keyboard.type( 'E2E Test Post' ); - await page.click( - 'role=option[name="Create a new post \\"E2E Test Post\\""i]' - ); - + await page.keyboard.type( 'new page' ); + await page.getByRole( 'option', { name: 'Add new page' } ).click(); await page.waitForSelector( 'iframe[name="editor-canvas"]' ); const frame = page.frame( 'editor-canvas' ); - const postTitleInput = frame.locator( - 'role=textbox[name=/Add title/i]' - ); - await expect( postTitleInput ).toHaveText( 'E2E Test Post' ); + await expect( + frame.getByRole( 'textbox', { name: 'Add title' } ) + ).toBeVisible(); } ); - test.skip( 'Open the command center and navigate to a template', async ( { + test( 'Open the command center and navigate to a template', async ( { page, } ) => { - await page.keyboard.press( 'Meta+k' ); - + await page + .getByRole( 'button', { name: 'Open command center' } ) + .click(); await page.keyboard.type( 'index' ); - await page.click( 'role=option[name="index"i]' ); - await expect( page.locator( 'h2' ) ).toHaveText( 'Index' ); + await page.getByRole( 'option', { name: 'index' } ).click(); + await expect( page.getByRole( 'heading', { level: 2 } ) ).toHaveText( + 'Index' + ); } ); } ); From d6a482edca22349d61b54b16b71ffc21d6d64c16 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Mon, 22 May 2023 14:22:12 +0300 Subject: [PATCH 120/131] Fix custom template creation regression (#50797) * Fix custom template creation regression * update custom template description to match post editor modal * add regression test * use `getByRole` in e2e tests --- ...d-custom-generic-template-modal-content.js | 2 +- .../add-new-template/new-template.js | 10 ++--- .../components/add-new-template/style.scss | 6 --- test/e2e/specs/site-editor/templates.spec.js | 43 +++++++++++++++++++ 4 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 test/e2e/specs/site-editor/templates.spec.js diff --git a/packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal-content.js b/packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal-content.js index 6da96e791679b2..9610ad1d4c3a47 100644 --- a/packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal-content.js +++ b/packages/edit-site/src/components/add-new-template/add-custom-generic-template-modal-content.js @@ -50,7 +50,7 @@ function AddCustomGenericTemplateModalContent( { onClose, createTemplate } ) { placeholder={ defaultTitle } disabled={ isBusy } help={ __( - 'Describe the template, e.g. "Post with sidebar".' + 'Describe the template, e.g. "Post with sidebar". A custom template can be manually applied to any post or page.' ) } /> <HStack diff --git a/packages/edit-site/src/components/add-new-template/new-template.js b/packages/edit-site/src/components/add-new-template/new-template.js index 841d45749e6285..0a3037603226fc 100644 --- a/packages/edit-site/src/components/add-new-template/new-template.js +++ b/packages/edit-site/src/components/add-new-template/new-template.js @@ -96,10 +96,6 @@ export default function NewTemplate( { const [ modalContent, setModalContent ] = useState( modalContentMap.templatesList ); - const [ - showCustomGenericTemplateModal, - setShowCustomGenericTemplateModal, - ] = useState( false ); const [ entityForSuggestions, setEntityForSuggestions ] = useState( {} ); const [ isCreatingTemplate, setIsCreatingTemplate ] = useState( false ); @@ -183,7 +179,7 @@ export default function NewTemplate( { __( 'Add template: %s' ), entityForSuggestions.labels.singular_name ); - } else if ( showCustomGenericTemplateModal ) { + } else if ( modalContent === modalContentMap.customGenericTemplate ) { modalTitle = __( 'Create custom template' ); } return ( @@ -246,7 +242,9 @@ export default function NewTemplate( { 'A custom template can be manually applied to any post or page.' ) } onClick={ () => - setShowCustomGenericTemplateModal( true ) + setModalContent( + modalContentMap.customGenericTemplate + ) } /> </Grid> diff --git a/packages/edit-site/src/components/add-new-template/style.scss b/packages/edit-site/src/components/add-new-template/style.scss index f0617ad62fc5b9..c8c2ef9f54a071 100644 --- a/packages/edit-site/src/components/add-new-template/style.scss +++ b/packages/edit-site/src/components/add-new-template/style.scss @@ -98,12 +98,6 @@ } .edit-site-custom-generic-template__modal { - .components-base-control { - @include break-medium() { - width: $grid-unit * 40; - } - } - .components-modal__header { border-bottom: none; } diff --git a/test/e2e/specs/site-editor/templates.spec.js b/test/e2e/specs/site-editor/templates.spec.js new file mode 100644 index 00000000000000..c0dbadad2dbfb9 --- /dev/null +++ b/test/e2e/specs/site-editor/templates.spec.js @@ -0,0 +1,43 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Templates', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + } ); + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllTemplates( 'wp_template' ); + } ); + test( 'Create a custom template', async ( { admin, page } ) => { + const templateName = 'demo'; + await admin.visitSiteEditor(); + await page.getByRole( 'button', { name: 'Templates' } ).click(); + await page.getByRole( 'button', { name: 'Add New Template' } ).click(); + await page + .getByRole( 'button', { + name: 'A custom template can be manually applied to any post or page.', + } ) + .click(); + // Fill the template title and submit. + const newTemplateDialog = page.locator( + 'role=dialog[name="Create custom template"i]' + ); + const templateNameInput = newTemplateDialog.locator( + 'role=textbox[name="Name"i]' + ); + await templateNameInput.fill( templateName ); + await page.keyboard.press( 'Enter' ); + // Close the pattern suggestions dialog. + await page + .getByRole( 'dialog', { name: 'Choose a pattern' } ) + .getByRole( 'button', { name: 'Close' } ) + .click(); + await expect( + page.locator( + `role=button[name="Dismiss this notice"i] >> text="${ templateName }" successfully created.` + ) + ).toBeVisible(); + } ); +} ); From 119a279d7bd55a9b878d964025c0a90a9541a374 Mon Sep 17 00:00:00 2001 From: Lena Morita <lena@jaguchi.com> Date: Mon, 22 May 2023 20:50:38 +0900 Subject: [PATCH 121/131] Fix width of Template Parts view (#50836) --- packages/edit-site/src/components/list/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/edit-site/src/components/list/style.scss b/packages/edit-site/src/components/list/style.scss index 72b13b3a588be7..59d37ebf3d3139 100644 --- a/packages/edit-site/src/components/list/style.scss +++ b/packages/edit-site/src/components/list/style.scss @@ -33,6 +33,7 @@ .edit-site { .edit-site-list { + flex-grow: 1; background: $white; border-radius: $radius-block-ui * 4; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.8), 0 8px 10px -6px rgba(0, 0, 0, 0.8); From 166256ed6c18d409f31fcefdf9b235d8666d2f3a Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 22 May 2023 13:57:52 +0100 Subject: [PATCH 122/131] Fix contextual commands selectors (#50829) --- packages/commands/src/store/selectors.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/commands/src/store/selectors.js b/packages/commands/src/store/selectors.js index 7aba1ba9fb7cf3..9795a87dc86313 100644 --- a/packages/commands/src/store/selectors.js +++ b/packages/commands/src/store/selectors.js @@ -10,7 +10,7 @@ export const getCommands = createSelector( command.context && command.context === state.context; return contextual ? isContextual : ! isContextual; } ), - ( state ) => [ state.commands ] + ( state ) => [ state.commands, state.context ] ); export const getCommandLoaders = createSelector( @@ -20,7 +20,7 @@ export const getCommandLoaders = createSelector( loader.context && loader.context === state.context; return contextual ? isContextual : ! isContextual; } ), - ( state ) => [ state.commandLoaders ] + ( state ) => [ state.commandLoaders, state.context ] ); export function isOpen( state ) { From eecc955093e497fcb8b3a1d2400a934997014ba6 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Mon, 22 May 2023 16:42:10 +0300 Subject: [PATCH 123/131] [Site Editor]: Sort template parts by type in navigation screen (#50841) --- .../index.js | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js index deea636781fb10..c689ae063b15b5 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js @@ -46,6 +46,22 @@ const config = { 'Manage what patterns are available when editing your site.' ), }, + sortCallback: ( items ) => { + const groupedByArea = items.reduce( + ( accumulator, item ) => { + const key = accumulator[ item.area ] ? item.area : 'rest'; + accumulator[ key ].push( item ); + return accumulator; + }, + { header: [], footer: [], sidebar: [], rest: [] } + ); + return [ + ...groupedByArea.header, + ...groupedByArea.footer, + ...groupedByArea.sidebar, + ...groupedByArea.rest, + ]; + }, }, }; @@ -75,10 +91,13 @@ export default function SidebarNavigationScreenTemplates() { per_page: -1, } ); - const sortedTemplates = templates ? [ ...templates ] : []; + let sortedTemplates = templates ? [ ...templates ] : []; sortedTemplates.sort( ( a, b ) => a.title.rendered.localeCompare( b.title.rendered ) ); + if ( config[ postType ].sortCallback ) { + sortedTemplates = config[ postType ].sortCallback( sortedTemplates ); + } const browseAllLink = useLink( { path: '/' + postType + '/all', From a093361fe96a7dc3763a50949952fba638a382c2 Mon Sep 17 00:00:00 2001 From: Carlos Garcia <fluiddot@gmail.com> Date: Mon, 22 May 2023 15:48:53 +0200 Subject: [PATCH 124/131] [RNMobile] Add disabled style to `Cell` component (#50665) * Add disabled style to Cell component * Add `disabled` prop to `BottomSheetTextControl` component * Pass `disabled` prop in `BottomSheetSelectControl` component * Pass `disabled` prop in `BottomSheetStepperCell` component * Pass `disabled` with the rest of cell props in `BottomSheetRangeCell` component * Pass `disabled` prop in `RangeControl` component * Pass `disabled` with the rest of cell props in `BottomSheetSwitchCell` component * Disable `TextInput` component in `Cell` component when disabled * Add disabled state to `Cell` component children * Increase opacity of cell disabled style * Allow customize `Cell` component disabled style * Customize disabled style for `BottomSheetSwitchCell` component * Remove `aria-disabled` prop in children container of `Cell` component Seems it's not needed as the parent component (`TouchableRipple`) is already marked as disabled. * Remove unneeded default value in `BottomSheetTextControl` component * Pass `disabled` prop in `BottomSheetRangeCell` component * Only disable Slider component on Android On iOS the default disabled style of the Slider already makes it transparent. So, in order to avoid make it too transparent with the Cell's disabled style, we only disable it on Android. * Add placeholder text color for disabled state of `Cell` * Mock placeholder color style * Update react-native-editor changelog --- .../index.native.js | 2 ++ .../bottom-sheet-text-control/index.native.js | 2 ++ .../src/mobile/bottom-sheet/cell.native.js | 31 ++++++++++++++++--- .../mobile/bottom-sheet/range-cell.native.js | 3 +- .../bottom-sheet/stepper-cell/index.native.js | 2 ++ .../mobile/bottom-sheet/styles.native.scss | 14 ++++++++- .../mobile/bottom-sheet/switch-cell.native.js | 3 ++ .../src/range-control/index.native.js | 3 ++ packages/react-native-editor/CHANGELOG.md | 1 + test/native/__mocks__/styleMock.js | 3 ++ 10 files changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/components/src/mobile/bottom-sheet-select-control/index.native.js b/packages/components/src/mobile/bottom-sheet-select-control/index.native.js index cc0a340104f86c..a379b950a5145c 100644 --- a/packages/components/src/mobile/bottom-sheet-select-control/index.native.js +++ b/packages/components/src/mobile/bottom-sheet-select-control/index.native.js @@ -22,6 +22,7 @@ const BottomSheetSelectControl = ( { options: items, onChange, value: selectedValue, + disabled, } ) => { const [ showSubSheet, setShowSubSheet ] = useState( false ); const navigation = useNavigation(); @@ -68,6 +69,7 @@ const BottomSheetSelectControl = ( { __( 'Navigates to select %s' ), label ) } + disabled={ disabled } > <Icon icon={ chevronRight }></Icon> </BottomSheet.Cell> diff --git a/packages/components/src/mobile/bottom-sheet-text-control/index.native.js b/packages/components/src/mobile/bottom-sheet-text-control/index.native.js index bb3a5ec72e2588..be7ddd8e9085e2 100644 --- a/packages/components/src/mobile/bottom-sheet-text-control/index.native.js +++ b/packages/components/src/mobile/bottom-sheet-text-control/index.native.js @@ -29,6 +29,7 @@ const BottomSheetTextControl = ( { icon, footerNote, cellPlaceholder, + disabled, } ) => { const [ showSubSheet, setShowSubSheet ] = useState( false ); const navigation = useNavigation(); @@ -62,6 +63,7 @@ const BottomSheetTextControl = ( { onPress={ openSubSheet } value={ initialValue || '' } placeholder={ cellPlaceholder || placeholder || '' } + disabled={ disabled } > <Icon icon={ chevronRight }></Icon> </BottomSheet.Cell> diff --git a/packages/components/src/mobile/bottom-sheet/cell.native.js b/packages/components/src/mobile/bottom-sheet/cell.native.js index 37ea9b74bc6ace..ec359f7a9a4952 100644 --- a/packages/components/src/mobile/bottom-sheet/cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/cell.native.js @@ -92,6 +92,7 @@ class BottomSheetCell extends Component { accessibilityHint, accessibilityRole, disabled = false, + disabledStyle = styles.cellDisabled, activeOpacity, onPress, onLongPress, @@ -223,11 +224,22 @@ class BottomSheetCell extends Component { styles.cellValue, styles.cellTextDark ); - const finalStyle = { + const textInputStyle = { ...cellValueStyle, ...valueStyle, ...styleRTL, }; + const placeholderTextColor = disabled + ? this.props.getStylesFromColorScheme( + styles.placeholderColorDisabled, + styles.placeholderColorDisabledDark + ).color + : styles.placeholderColor.color; + const textStyle = { + ...( disabled && styles.cellDisabled ), + ...cellValueStyle, + ...valueStyle, + }; // To be able to show the `middle` ellipsizeMode on editable cells // we show the TextInput just when the user wants to edit the value, @@ -238,10 +250,10 @@ class BottomSheetCell extends Component { <TextInput ref={ ( c ) => ( this._valueTextInput = c ) } numberOfLines={ 1 } - style={ finalStyle } + style={ textInputStyle } value={ value } placeholder={ valuePlaceholder } - placeholderTextColor={ '#87a6bc' } + placeholderTextColor={ placeholderTextColor } onChangeText={ onChangeValue } editable={ isValueEditable } pointerEvents={ @@ -251,11 +263,12 @@ class BottomSheetCell extends Component { onBlur={ finishEditing } onSubmitEditing={ onSubmit } keyboardType={ this.typeToKeyboardType( type, step ) } + disabled={ disabled } { ...valueProps } /> ) : ( <Text - style={ { ...cellValueStyle, ...valueStyle } } + style={ textStyle } numberOfLines={ 1 } ellipsizeMode={ 'middle' } > @@ -418,7 +431,15 @@ class BottomSheetCell extends Component { /> ) } { showValue && getValueComponent() } - { children } + <View + style={ [ + disabled && disabledStyle, + styles.cellRowContainer, + ] } + pointerEvents={ disabled ? 'none' : 'auto' } + > + { children } + </View> </View> { help && ( <Text style={ [ cellHelpStyle, styles.placeholderColor ] }> diff --git a/packages/components/src/mobile/bottom-sheet/range-cell.native.js b/packages/components/src/mobile/bottom-sheet/range-cell.native.js index 7d7bafd4acd0cd..592acbd6810b51 100644 --- a/packages/components/src/mobile/bottom-sheet/range-cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/range-cell.native.js @@ -218,6 +218,7 @@ class BottomSheetRangeCell extends Component { activeOpacity={ 1 } accessible={ false } valueStyle={ styles.valueStyle } + disabled={ disabled } > <View style={ containerStyle }> { preview } @@ -225,7 +226,7 @@ class BottomSheetRangeCell extends Component { testID={ `Slider ${ cellProps.label }` } value={ sliderValue } defaultValue={ defaultValue } - disabled={ disabled } + disabled={ disabled && ! isIOS } step={ step } minimumValue={ minimumValue } maximumValue={ maximumValue } diff --git a/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js b/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js index e6a003a915d454..5328d26df53c9a 100644 --- a/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js @@ -144,6 +144,7 @@ class BottomSheetStepperCell extends Component { openUnitPicker, decimalNum, cellContainerStyle, + disabled, } = this.props; const { inputValue } = this.state; const isMinValue = value === min; @@ -215,6 +216,7 @@ class BottomSheetStepperCell extends Component { labelStyle={ labelStyle } leftAlign={ true } separatorType={ separatorType } + disabled={ disabled } > <View style={ preview && containerStyle }> { preview } diff --git a/packages/components/src/mobile/bottom-sheet/styles.native.scss b/packages/components/src/mobile/bottom-sheet/styles.native.scss index 4d00d452304615..ccec337a35edcd 100644 --- a/packages/components/src/mobile/bottom-sheet/styles.native.scss +++ b/packages/components/src/mobile/bottom-sheet/styles.native.scss @@ -284,7 +284,15 @@ // used in both light and dark modes .placeholderColor { - color: #87a6bc; + color: $gray; +} + +.placeholderColorDisabled { + color: lighten($gray, 20%); +} + +.placeholderColorDisabledDark { + color: lighten($gray-dark, 10%); } .applyButton { @@ -317,3 +325,7 @@ .cellSubLabelTextDark { color: $sub-heading-dark; } + +.cellDisabled { + opacity: 0.3; +} diff --git a/packages/components/src/mobile/bottom-sheet/switch-cell.native.js b/packages/components/src/mobile/bottom-sheet/switch-cell.native.js index 53b804bedfb7b2..f16bb0d7b237a7 100644 --- a/packages/components/src/mobile/bottom-sheet/switch-cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/switch-cell.native.js @@ -12,6 +12,8 @@ import { __, _x, sprintf } from '@wordpress/i18n'; */ import Cell from './cell'; +const EMPTY_STYLE = {}; + export default function BottomSheetSwitchCell( props ) { const { value, onValueChange, disabled, ...cellProps } = props; @@ -61,6 +63,7 @@ export default function BottomSheetSwitchCell( props ) { editable={ false } value={ '' } disabled={ disabled } + disabledStyle={ EMPTY_STYLE } > <Switch value={ value } diff --git a/packages/components/src/range-control/index.native.js b/packages/components/src/range-control/index.native.js index 4717502785561d..3250680ee9a937 100644 --- a/packages/components/src/range-control/index.native.js +++ b/packages/components/src/range-control/index.native.js @@ -25,6 +25,7 @@ const RangeControl = memo( max, type, separatorType, + disabled, ...props } ) => { if ( type === 'stepper' ) { @@ -36,6 +37,7 @@ const RangeControl = memo( onChange={ onChange } separatorType={ separatorType } value={ value } + disabled={ disabled } /> ); } @@ -61,6 +63,7 @@ const RangeControl = memo( allowReset={ allowReset } defaultValue={ initialSliderValue } separatorType={ separatorType } + disabled={ disabled } { ...props } /> ); diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 8918f5522c1a3a..429927a3d10ed1 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -11,6 +11,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [**] Tapping on all nested blocks gets focus directly instead of having to tap multiple times depending on the nesting levels. [#50672] +- [*] Add disabled style to `Cell` component [#50665] ## 1.95.0 - [*] Fix crash when trying to convert to regular blocks an undefined/deleted reusable block [#50475] diff --git a/test/native/__mocks__/styleMock.js b/test/native/__mocks__/styleMock.js index 5b7087778e04cc..f52f60f233560b 100644 --- a/test/native/__mocks__/styleMock.js +++ b/test/native/__mocks__/styleMock.js @@ -184,4 +184,7 @@ module.exports = { 'components-picker__button-title': { color: 'white', }, + placeholderColor: { + color: 'gray', + }, }; From ffc592a47ba4d5ce27015487385bdffd32eeed9e Mon Sep 17 00:00:00 2001 From: Tonya Mork <tonya.mork@automattic.com> Date: Mon, 22 May 2023 09:20:33 -0500 Subject: [PATCH 125/131] [Fonts API] Automatically enqueue user-selected global fonts (#50529) * Adds user-select fonts enqueuer. * Auto queue before printing * Always set $handles to false when empty When invoked as a hooked callback, it receives an empty string. Empty is the same as false. * Use WP_Theme_JSON_Resolver_Gutenberg::get_user_data(). From PR #50499. Props @oandregal. * Adds print tests. Moves datasets to trait. --- .../fonts-api/class-wp-fonts-resolver.php | 109 ++++++++ lib/experimental/fonts-api/fonts-api.php | 24 +- lib/load.php | 2 + phpunit/fonts-api/wp-fonts-testcase.php | 42 ++++ phpunit/fonts-api/wp-fonts-tests-dataset.php | 232 ++++++++++++++++++ .../enqueueUserSelectedFonts-test.php | 131 ++++++++++ phpunit/fonts-api/wpPrintFonts-test.php | 78 ++++-- 7 files changed, 585 insertions(+), 33 deletions(-) create mode 100644 lib/experimental/fonts-api/class-wp-fonts-resolver.php create mode 100644 phpunit/fonts-api/wpFontsResolver/enqueueUserSelectedFonts-test.php diff --git a/lib/experimental/fonts-api/class-wp-fonts-resolver.php b/lib/experimental/fonts-api/class-wp-fonts-resolver.php new file mode 100644 index 00000000000000..d3d0ecba992b88 --- /dev/null +++ b/lib/experimental/fonts-api/class-wp-fonts-resolver.php @@ -0,0 +1,109 @@ +<?php +/** + * WP_Fonts_Resolver class. + * + * @package WordPress + * @subpackage Fonts API + * @since X.X.X + */ + +if ( class_exists( 'WP_Fonts_Resolver' ) ) { + return; +} + +/** + * The Fonts API Resolver abstracts the processing of different data sources + * (such as theme.json and global styles) for font interactions with the API. + * + * This class is for internal core usage and is not supposed to be used by + * extenders (plugins and/or themes). + * + * @access private + */ +class WP_Fonts_Resolver { + /** + * Defines the key structure in global styles to the fontFamily + * user-selected font. + * + * @since X.X.X + * + * @var string[][] + */ + protected static $global_styles_font_family_structure = array( + array( 'elements', 'link', 'typography', 'fontFamily' ), + array( 'elements', 'heading', 'typography', 'fontFamily' ), + array( 'elements', 'caption', 'typography', 'fontFamily' ), + array( 'elements', 'button', 'typography', 'fontFamily' ), + array( 'typography', 'fontFamily' ), + ); + + /** + * Enqueues user-selected fonts via global styles. + * + * @since X.X.X + * + * @return array User selected font-families when exists, else empty array. + */ + public static function enqueue_user_selected_fonts() { + $user_selected_fonts = array(); + $user_global_styles = WP_Theme_JSON_Resolver_Gutenberg::get_user_data()->get_raw_data(); + if ( isset( $user_global_styles['styles'] ) ) { + $user_selected_fonts = static::get_user_selected_fonts( $user_global_styles['styles'] ); + } + + if ( empty( $user_selected_fonts ) ) { + return array(); + } + + wp_enqueue_fonts( $user_selected_fonts ); + return $user_selected_fonts; + } + + /** + * Gets the user-selected font-family handles. + * + * @since X.X.X + * + * @param array $global_styles Global styles potentially containing user-selected fonts. + * @return array User-selected font-families. + */ + private static function get_user_selected_fonts( array $global_styles ) { + $font_families = array(); + + foreach ( static::$global_styles_font_family_structure as $path ) { + $style_value = _wp_array_get( $global_styles, $path, '' ); + + $font_family = static::get_value_from_style( $style_value ); + if ( '' !== $font_family ) { + $font_families[] = $font_family; + } + } + + return array_unique( $font_families ); + } + + /** + * Get the value (i.e. preset slug) from the given style value. + * + * @since X.X.X + * + * @param string $style The style to parse. + * @param string $preset_type Optional. The type to parse. Default 'font-family'. + * @return string Preset slug. + */ + private static function get_value_from_style( $style, $preset_type = 'font-family' ) { + if ( '' === $style ) { + return ''; + } + + $starting_pattern = "var(--wp--preset--{$preset_type}--"; + $ending_pattern = ')'; + if ( ! str_starts_with( $style, $starting_pattern ) ) { + return ''; + } + + $offset = strlen( $starting_pattern ); + $length = strpos( $style, $ending_pattern ) - $offset; + return substr( $style, $offset, $length ); + } +} diff --git a/lib/experimental/fonts-api/fonts-api.php b/lib/experimental/fonts-api/fonts-api.php index 5f3f7a60ee8839..7075f20f76a217 100644 --- a/lib/experimental/fonts-api/fonts-api.php +++ b/lib/experimental/fonts-api/fonts-api.php @@ -198,19 +198,17 @@ function wp_print_fonts( $handles = false ) { return array(); } - // Skip this reassignment decision-making when using the default of `false`. - if ( false !== $handles ) { - // When `true`, print all registered fonts for the iframed editor. - if ( $in_iframed_editor ) { - $queue = $wp_fonts->queue; - $done = $wp_fonts->done; - $wp_fonts->done = array(); - $wp_fonts->queue = $registered; - $handles = false; - } elseif ( empty( $handles ) ) { - // When falsey, assign `false` to print enqueued fonts. - $handles = false; - } + if ( empty( $handles ) ) { + // Automatically enqueue all user-selected fonts. + WP_Fonts_Resolver::enqueue_user_selected_fonts(); + $handles = false; + } elseif ( $in_iframed_editor ) { + // Print all registered fonts for the iframed editor. + $queue = $wp_fonts->queue; + $done = $wp_fonts->done; + $wp_fonts->done = array(); + $wp_fonts->queue = $registered; + $handles = false; } _wp_scripts_maybe_doing_it_wrong( __FUNCTION__ ); diff --git a/lib/load.php b/lib/load.php index b8ec4a4d607849..a97a7cdc881f5e 100644 --- a/lib/load.php +++ b/lib/load.php @@ -116,7 +116,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/fonts-api/register-fonts-from-theme-json.php'; require __DIR__ . '/experimental/fonts-api/class-wp-fonts.php'; require __DIR__ . '/experimental/fonts-api/class-wp-fonts-provider-local.php'; + require __DIR__ . '/experimental/fonts-api/class-wp-fonts-resolver.php'; require __DIR__ . '/experimental/fonts-api/fonts-api.php'; + // BC Layer files, which will not be backported to WP Core. require __DIR__ . '/experimental/fonts-api/bc-layer/class-gutenberg-fonts-api-bc-layer.php'; require __DIR__ . '/experimental/fonts-api/bc-layer/webfonts-deprecations.php'; diff --git a/phpunit/fonts-api/wp-fonts-testcase.php b/phpunit/fonts-api/wp-fonts-testcase.php index adb70eca98b0fc..f7f7fb04b50625 100644 --- a/phpunit/fonts-api/wp-fonts-testcase.php +++ b/phpunit/fonts-api/wp-fonts-testcase.php @@ -59,6 +59,13 @@ abstract class WP_Fonts_TestCase extends WP_UnitTestCase { */ protected $orig_theme_dir; + /** + * Administrator ID. + * + * @var int + */ + protected static $administrator_id = 0; + public static function set_up_before_class() { parent::set_up_before_class(); @@ -346,4 +353,39 @@ protected function get_handles_for_provider( array $fonts, $provider_id ) { return $handles; } + + protected static function set_up_admin_user() { + self::$administrator_id = self::factory()->user->create( + array( + 'role' => 'administrator', + 'user_email' => 'administrator@example.com', + ) + ); + } + + /** + * Sets up the global styles. + * + * @param array $styles User-selected styles structure. + * @param array $theme Optional. Theme to switch to for the test. Default 'fonts-block-theme'. + */ + protected function set_up_global_styles( array $styles, $theme = 'fonts-block-theme' ) { + switch_theme( $theme ); + + if ( empty( $styles ) ) { + return; + } + + // Make sure there is data from the user origin. + wp_set_current_user( self::$administrator_id ); + $user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( wp_get_theme(), true ); + $config = json_decode( $user_cpt['post_content'], true ); + + // Add the test styles. + $config['styles'] = $styles; + + // Update the global styles and settings post. + $user_cpt['post_content'] = wp_json_encode( $config ); + wp_update_post( $user_cpt, true, false ); + } } diff --git a/phpunit/fonts-api/wp-fonts-tests-dataset.php b/phpunit/fonts-api/wp-fonts-tests-dataset.php index 79d2387b52cf53..b882965598d360 100644 --- a/phpunit/fonts-api/wp-fonts-tests-dataset.php +++ b/phpunit/fonts-api/wp-fonts-tests-dataset.php @@ -1161,4 +1161,236 @@ protected function get_registered_mock_fonts() { ), ); } + + /** + * Data provider. + * + * @return array + */ + public function data_print_user_selected_fonts() { + $global_styles = $this->get_mock_user_selected_fonts_global_styles(); + $font_faces = $this->get_registered_fonts_css(); + + return array( + 'print font1' => array( + 'global_styles' => $global_styles['font1'], + 'expected_done' => array( + 'font1-300-normal', + 'font1-300-italic', + 'font1-900-normal', + 'font1', + ), + 'expected_output' => sprintf( + '<mock id="wp-fonts-mock" attr="some-attr">%s; %s; %s</mock>\n', + $font_faces['font1-300-normal'], + $font_faces['font1-300-italic'], + $font_faces['font1-900-normal'] + ), + ), + 'print font2' => array( + 'global_styles' => $global_styles['font2'], + 'expected_done' => array( 'font2-200-900-normal', 'font2-200-900-italic', 'font2' ), + 'expected_output' => sprintf( + '<mock id="wp-fonts-mock" attr="some-attr">%s; %s</mock>\n', + $font_faces['font2-200-900-normal'], + $font_faces['font2-200-900-italic'] + ), + ), + 'print font3' => array( + 'global_styles' => $global_styles['font3'], + 'expected_done' => array( 'font3', 'font3-bold-normal' ), + 'expected_output' => sprintf( + '<mock id="wp-fonts-mock" attr="some-attr">%s</mock>\n', + $font_faces['font3-bold-normal'] + ), + ), + 'print all fonts' => array( + 'global_styles' => $global_styles['all'], + 'expected_done' => array( + 'font1-300-normal', + 'font1-300-italic', + 'font1-900-normal', + 'font1', + 'font2-200-900-normal', + 'font2-200-900-italic', + 'font2', + 'font3-bold-normal', + 'font3', + ), + 'expected_output' => sprintf( + '<mock id="wp-fonts-mock" attr="some-attr">%s; %s; %s; %s; %s; %s</mock>\n', + $font_faces['font1-300-normal'], + $font_faces['font1-300-italic'], + $font_faces['font1-900-normal'], + $font_faces['font2-200-900-normal'], + $font_faces['font2-200-900-italic'], + $font_faces['font3-bold-normal'] + ), + ), + 'print all valid fonts' => array( + 'global_styles' => $global_styles['all with invalid element'], + 'expected_done' => array( + 'font1-300-normal', + 'font1-300-italic', + 'font1-900-normal', + 'font1', + 'font2-200-900-normal', + 'font2-200-900-italic', + 'font2', + 'font3-bold-normal', + 'font3', + ), + 'expected_output' => sprintf( + '<mock id="wp-fonts-mock" attr="some-attr">%s; %s; %s; %s; %s; %s</mock>\n', + $font_faces['font1-300-normal'], + $font_faces['font1-300-italic'], + $font_faces['font1-900-normal'], + $font_faces['font2-200-900-normal'], + $font_faces['font2-200-900-italic'], + $font_faces['font3-bold-normal'] + ), + ), + ); + } + + /** + * Gets user-selected fonts for global styles for the mock provider. + * + * @since X.X.X + * + * @return array + */ + protected function get_mock_user_selected_fonts_global_styles() { + return array( + 'font1' => array( + 'elements' => array( + 'heading' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font1', + 'fontStyle' => 'normal', + 'fontWeight' => '300', + ), + ), + 'caption' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font1', + 'fontStyle' => 'italic', + 'fontWeight' => '300', + ), + ), + ), + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font1', + 'fontStyle' => 'normal', + 'fontWeight' => '900', + ), + ), + 'font2' => array( + 'elements' => array( + 'heading' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font2', + 'fontStyle' => 'normal', + 'fontWeight' => '200-900', + ), + ), + 'button' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font2', + 'fontStyle' => 'italic', + 'fontWeight' => '200-900', + ), + ), + ), + ), + 'font3' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font3', + 'fontStyle' => 'normal', + 'fontWeight' => 'bold', + ), + ), + 'all' => array( + 'elements' => array( + 'link' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font1', + 'fontStyle' => 'italic', + 'fontWeight' => '300', + ), + ), + 'heading' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font1', + 'fontStyle' => 'normal', + 'fontWeight' => '900', + ), + ), + 'caption' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font1', + 'fontStyle' => 'italic', + 'fontWeight' => '300', + ), + ), + 'button' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font2', + 'fontStyle' => 'normal', + 'fontWeight' => '200-900', + ), + ), + ), + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font3', + 'fontStyle' => 'normal', + 'fontWeight' => 'bold', + ), + ), + 'all with invalid element' => array( + 'elements' => array( + 'link' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font1', + 'fontStyle' => 'italic', + 'fontWeight' => '300', + ), + ), + 'heading' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font1', + 'fontStyle' => 'normal', + 'fontWeight' => '900', + ), + ), + 'caption' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font1', + 'fontStyle' => 'italic', + 'fontWeight' => '300', + ), + ), + 'button' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font2', + 'fontStyle' => 'normal', + 'fontWeight' => '200-900', + ), + ), + 'invalid' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font2', + 'fontStyle' => 'italic', + 'fontWeight' => '200-900', + ), + ), + ), + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font3', + 'fontStyle' => 'normal', + 'fontWeight' => 'bold', + ), + ), + ); + } } diff --git a/phpunit/fonts-api/wpFontsResolver/enqueueUserSelectedFonts-test.php b/phpunit/fonts-api/wpFontsResolver/enqueueUserSelectedFonts-test.php new file mode 100644 index 00000000000000..c3ba0fd1f72ac6 --- /dev/null +++ b/phpunit/fonts-api/wpFontsResolver/enqueueUserSelectedFonts-test.php @@ -0,0 +1,131 @@ +<?php +/** + * WP_Fonts_Resolver::enqueue_user_selected_fonts() tests. + * + * @package WordPress + * @subpackage Fonts API + */ + +require_once __DIR__ . '/../wp-fonts-testcase.php'; + +/** + * @group fontsapi + * @covers WP_Fonts_Resolver::enqueue_user_selected_fonts + */ +class Tests_Fonts_WpFontsResolver_EnqueueUserSelectedFonts extends WP_Fonts_TestCase { + + public static function set_up_before_class() { + self::$requires_switch_theme_fixtures = true; + + parent::set_up_before_class(); + + self::$administrator_id = self::factory()->user->create( + array( + 'role' => 'administrator', + 'user_email' => 'administrator@example.com', + ) + ); + } + + /** + * @dataProvider data_should_not_enqueue_when_no_user_selected_fonts + * + * @param array $styles Optional. Test styles. Default empty array. + */ + public function test_should_not_enqueue_when_no_user_selected_fonts( $styles = array() ) { + $this->set_up_global_styles( $styles ); + + $mock = $this->set_up_mock( 'enqueue' ); + $mock->expects( $this->never() ) + ->method( 'enqueue' ); + + $expected = array(); + $this->assertSame( $expected, WP_Fonts_Resolver::enqueue_user_selected_fonts() ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_not_enqueue_when_no_user_selected_fonts() { + return array( + 'no user-selected styles' => array(), + 'invalid element' => array( + array( + 'elements' => array( + 'invalid' => array( + 'typography' => array( + 'fontFamily' => 'var:preset|font-family|font1', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + ), + ), + ), + ), + ), + ); + } + + /** + * @dataProvider data_should_enqueue_when_user_selected_fonts + * + * @param array $styles Test styles. + * @param array $expected Expected results. + */ + public function test_should_enqueue_when_user_selected_fonts( $styles, $expected ) { + $mock = $this->set_up_mock( 'enqueue' ); + $mock->expects( $this->once() ) + ->method( 'enqueue' ) + ->with( + $this->identicalTo( $expected ) + ); + + $this->set_up_global_styles( $styles ); + + $this->assertSameSets( $expected, WP_Fonts_Resolver::enqueue_user_selected_fonts() ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_enqueue_when_user_selected_fonts() { + $global_styles = $this->get_mock_user_selected_fonts_global_styles(); + + return array( + 'heading, caption, text' => array( + 'styles' => $global_styles['font1'], + 'expected' => array( 'font1' ), + ), + 'heading, button' => array( + 'styles' => $global_styles['font2'], + 'expected' => array( 'font2' ), + ), + 'text' => array( + 'styles' => $global_styles['font3'], + 'expected' => array( 'font3' ), + ), + 'all' => array( + 'styles' => $global_styles['all'], + 'expected' => array( + 0 => 'font1', + // font1 occurs 2 more times and gets removed as duplicates. + 3 => 'font2', + 4 => 'font3', + ), + ), + 'all with invalid element' => array( + 'styles' => $global_styles['all with invalid element'], + 'expected' => array( + 0 => 'font1', + // font1 occurs 2 more times and gets removed as duplicates. + 3 => 'font2', + // Skips font2 for the "invalid" element. + 4 => 'font3', + ), + ), + ); + } +} diff --git a/phpunit/fonts-api/wpPrintFonts-test.php b/phpunit/fonts-api/wpPrintFonts-test.php index cb500aaa0430f4..4f50cf6a8cd5ea 100644 --- a/phpunit/fonts-api/wpPrintFonts-test.php +++ b/phpunit/fonts-api/wpPrintFonts-test.php @@ -15,6 +15,14 @@ */ class Tests_Fonts_WpPrintFonts extends WP_Fonts_TestCase { + public static function set_up_before_class() { + self::$requires_switch_theme_fixtures = true; + + parent::set_up_before_class(); + + static::set_up_admin_user(); + } + public function test_should_return_empty_array_when_no_fonts_registered() { $this->assertSame( array(), wp_print_fonts() ); } @@ -104,26 +112,6 @@ public function test_should_print_handles_when_not_enqueued( $setup, $expected_d $this->assertSameSets( $expected_done, $actual_done, 'Printed handles should match' ); } - /** - * Sets up the dependencies for integration test. - * - * @param array $setup Dependencies to set up. - * @param WP_Fonts $wp_fonts Instance of WP_Fonts. - * @param bool $enqueue Whether to enqueue. Default true. - */ - private function setup_integrated_deps( array $setup, $wp_fonts, $enqueue = true ) { - foreach ( $setup['provider'] as $provider ) { - $wp_fonts->register_provider( $provider['id'], $provider['class'] ); - } - foreach ( $setup['registered'] as $handle => $variations ) { - $this->setup_register( $handle, $variations, $wp_fonts ); - } - - if ( $enqueue ) { - $wp_fonts->enqueue( $setup['enqueued'] ); - } - } - /** * @dataProvider data_should_print_all_registered_fonts_for_iframed_editor * @@ -189,4 +177,54 @@ public function data_should_print_all_registered_fonts_for_iframed_editor() { ), ); } + + /** + * Integration test for printing user-selected global fonts. + * This test registers providers and fonts and then enqueues before testing the printing functionality. + * + * @dataProvider data_print_user_selected_fonts + * + * @param array $global_styles Test set up information for provider, fonts, and enqueued. + * @param array $expected_done Expected array of printed handles. + * @param string $expected_output Expected printed output. + */ + public function test_should_print_user_selected_fonts( $global_styles, $expected_done, $expected_output ) { + $wp_fonts = wp_fonts(); + + $setup = array( + 'provider' => array( 'mock' => $this->get_provider_definitions( 'mock' ) ), + 'registered' => $this->get_registered_mock_fonts(), + 'global_styles' => $global_styles, + ); + $this->setup_integrated_deps( $setup, $wp_fonts, false ); + + $this->expectOutputString( $expected_output ); + $actual_printed_fonts = wp_print_fonts(); + $this->assertSameSets( $expected_done, $actual_printed_fonts, 'Should print font-faces for given user-selected fonts' ); + } + + + /** + * Sets up the dependencies for integration test. + * + * @param array $setup Dependencies to set up. + * @param WP_Fonts $wp_fonts Instance of WP_Fonts. + * @param bool $enqueue Whether to enqueue. Default true. + */ + private function setup_integrated_deps( array $setup, $wp_fonts, $enqueue = true ) { + foreach ( $setup['provider'] as $provider ) { + $wp_fonts->register_provider( $provider['id'], $provider['class'] ); + } + foreach ( $setup['registered'] as $handle => $variations ) { + $this->setup_register( $handle, $variations, $wp_fonts ); + } + + if ( $enqueue ) { + $wp_fonts->enqueue( $setup['enqueued'] ); + } + + if ( ! empty( $setup['global_styles'] ) ) { + $this->set_up_global_styles( $setup['global_styles'] ); + } + } } From 1a12859fc92dfb23af3359090c298bebf40f2186 Mon Sep 17 00:00:00 2001 From: David Calhoun <github@davidcalhoun.me> Date: Mon, 22 May 2023 11:20:30 -0400 Subject: [PATCH 126/131] test: Fix React prop typo --- packages/format-library/src/link/modal.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/format-library/src/link/modal.native.js b/packages/format-library/src/link/modal.native.js index 8d29437385f1c6..a41c435df214b9 100644 --- a/packages/format-library/src/link/modal.native.js +++ b/packages/format-library/src/link/modal.native.js @@ -19,7 +19,7 @@ const ModalLinkUI = ( { isVisible, ...restProps } ) => { hideHeader onClose={ restProps.onClose } hasNavigation - testId={ 'link-settings-modal' } + testID="link-settings-modal" > <BottomSheet.NavigationContainer animate main> <BottomSheet.NavigationScreen name={ screens.settings }> From c1336f38e3c411e4471c8b8025b2f9388aba2cf7 Mon Sep 17 00:00:00 2001 From: David Calhoun <github@davidcalhoun.me> Date: Mon, 22 May 2023 11:22:44 -0400 Subject: [PATCH 127/131] test: Simplify test queries The targeted elements do not require asynchronous queries. Using synchronous queries is more concise and easier to comprehend. --- test/native/integration/editor-history.native.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/test/native/integration/editor-history.native.js b/test/native/integration/editor-history.native.js index e5248c39ab4048..0c336f92be47bd 100644 --- a/test/native/integration/editor-history.native.js +++ b/test/native/integration/editor-history.native.js @@ -2,7 +2,6 @@ * External dependencies */ import { - act, addBlock, dismissModal, getBlock, @@ -12,7 +11,6 @@ import { initializeEditor, setupCoreBlocks, selectRangeInRichText, - waitFor, within, } from 'test/helpers'; @@ -188,24 +186,18 @@ describe( 'Editor History', () => { } ); // Act - const paragraphBlock = await waitFor( () => - getBlock( screen, 'Paragraph' ) - ); - + const paragraphBlock = getBlock( screen, 'Paragraph' ); fireEvent.press( paragraphBlock ); const paragraphTextInput = within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); selectRangeInRichText( paragraphTextInput, 2, 7 ); - // Await React Navigation: https://github.com/WordPress/gutenberg/issues/35685#issuecomment-961919931 - await act( () => fireEvent.press( screen.getByLabelText( 'Link' ) ) ); + fireEvent.press( screen.getByLabelText( 'Link' ) ); - const newTabButton = await waitFor( () => - screen.getByText( 'Open in new tab' ) - ); + const newTabButton = screen.getByText( 'Open in new tab' ); fireEvent.press( newTabButton ); - await dismissModal( screen.getByTestId( 'link-settings-modal' ) ); + dismissModal( screen.getByTestId( 'link-settings-modal' ) ); typeInRichText( paragraphTextInput, From 2c272f76b50c5023d49f12e907f981d50c2ad19d Mon Sep 17 00:00:00 2001 From: David Calhoun <github@davidcalhoun.me> Date: Mon, 22 May 2023 11:24:45 -0400 Subject: [PATCH 128/131] test: Increase editor history link test accuracy The targeted bug occurs when: 1. Changing a link to "Open in a new tab." 2. Performing additional changes, e.g. typing. 3. Undoing both actions. 4. Redoing both actions. Thus, this changes the initial HTML, adds additional undo/redo invocations, and updates inline snapshots to match the expected outcomes. --- test/native/integration/editor-history.native.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/native/integration/editor-history.native.js b/test/native/integration/editor-history.native.js index 0c336f92be47bd..35ab4d54a0a08e 100644 --- a/test/native/integration/editor-history.native.js +++ b/test/native/integration/editor-history.native.js @@ -179,7 +179,7 @@ describe( 'Editor History', () => { it( 'should preserve editor history when a link has been added and configured to open in a new tab', async () => { // Arrange const initialHtml = ` - <!-- wp:paragraph --><p>A <a href="http://wordpress.org" target="_blank" rel="noreferrer noopener">quick</a> brown fox jumps over the lazy dog.</p><!-- /wp:paragraph --> + <!-- wp:paragraph --><p>A <a href="http://wordpress.org">quick</a> brown fox jumps over the lazy dog.</p><!-- /wp:paragraph --> `; const screen = await initializeEditor( { initialHtml, @@ -201,7 +201,7 @@ describe( 'Editor History', () => { typeInRichText( paragraphTextInput, - 'A quick brown fox jumps over the lazy dog.' + ' A quick brown fox jumps over the lazy dog.' ); // Assert @@ -213,21 +213,23 @@ describe( 'Editor History', () => { // Act fireEvent.press( screen.getByLabelText( 'Undo' ) ); + fireEvent.press( screen.getByLabelText( 'Undo' ) ); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` "<!-- wp:paragraph --> - <p></p> + <p>A <a href="http://wordpress.org">quick</a> brown fox jumps over the lazy dog.</p> <!-- /wp:paragraph -->" ` ); // Act fireEvent.press( screen.getByLabelText( 'Redo' ) ); + fireEvent.press( screen.getByLabelText( 'Redo' ) ); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` "<!-- wp:paragraph --> - <p>A <a href="http://wordpress.org" target="_blank" rel="noreferrer noopener">quick</a> brown fox jumps over the lazy dog.</p> + <p>A <a href="http://wordpress.org" target="_blank" rel="noreferrer noopener">quick</a> brown fox jumps over the lazy dog. A quick brown fox jumps over the lazy dog.</p> <!-- /wp:paragraph -->" ` ); } ); From 85ae2f8305087aae0080c8350d993f87b9200274 Mon Sep 17 00:00:00 2001 From: megane9988 <info@m-g-n.me> Date: Tue, 23 May 2023 01:37:47 +0900 Subject: [PATCH 129/131] Add an outline when the color picker select box is focused (#50609) * Add outline when color picker select box is focused * fix CHANGELOG * Update packages/components/CHANGELOG.md add empty line Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com> --------- Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com> --- packages/components/CHANGELOG.md | 1 + packages/components/src/color-picker/styles.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 83d74153ccb1cc..7de5a4537097b5 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -15,6 +15,7 @@ ### Bug Fix +- `ColorPicker`: Add an outline when the color picker select box is focused([#50609](https://github.com/WordPress/gutenberg/pull/50609)). - `InputControl`: Fix focus style to support Windows High Contrast mode ([#50772](https://github.com/WordPress/gutenberg/pull/50772)). ## 24.0.0 (2023-05-10) diff --git a/packages/components/src/color-picker/styles.ts b/packages/components/src/color-picker/styles.ts index 1592f6a201656a..f78db13a8fd73f 100644 --- a/packages/components/src/color-picker/styles.ts +++ b/packages/components/src/color-picker/styles.ts @@ -29,8 +29,13 @@ export const NumberControlWrapper = styled( NumberControl )` export const SelectControl = styled( InnerSelectControl )` margin-left: ${ space( -2 ) }; width: 5em; - ${ BackdropUI } { - display: block; + /* + * Remove border, but preserve focus styles + * TODO: this override should be removed, + * see https://github.com/WordPress/gutenberg/pull/50609 + */ + select:not( :focus ) ~ ${ BackdropUI }${ BackdropUI }${ BackdropUI } { + border-color: transparent; } `; From c11ac48758e579ca4abd36240c66a92f814ae3e3 Mon Sep 17 00:00:00 2001 From: James Koster <james@jameskoster.co.uk> Date: Mon, 22 May 2023 18:23:22 +0100 Subject: [PATCH 130/131] Update tooltip colors (#50792) * Update tooltip colors * Update changelog --- packages/components/CHANGELOG.md | 4 ++++ packages/components/src/tooltip/style.scss | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 7de5a4537097b5..b299cdf3e0c153 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -18,6 +18,10 @@ - `ColorPicker`: Add an outline when the color picker select box is focused([#50609](https://github.com/WordPress/gutenberg/pull/50609)). - `InputControl`: Fix focus style to support Windows High Contrast mode ([#50772](https://github.com/WordPress/gutenberg/pull/50772)). +### Enhancements + +- `Tooltip`: Update background color so tooltip boundaries are more visible in the site editor ([#50792](https://github.com/WordPress/gutenberg/pull/50792)). + ## 24.0.0 (2023-05-10) ### Breaking Changes diff --git a/packages/components/src/tooltip/style.scss b/packages/components/src/tooltip/style.scss index 30d480c904f81b..1a5e6c401542b1 100644 --- a/packages/components/src/tooltip/style.scss +++ b/packages/components/src/tooltip/style.scss @@ -7,11 +7,11 @@ } .components-tooltip .components-popover__content { - background: $components-color-foreground; // TODO: Discuss with designers. + background: $black; // TODO: Discuss with designers. border-radius: $radius-block-ui; border-width: 0; outline: none; - color: $components-color-foreground-inverted; + color: $gray-100; white-space: nowrap; text-align: center; line-height: 1.4; From 7bac5ee61413f4578757c56643dbe6afc548a8c2 Mon Sep 17 00:00:00 2001 From: Alex Lende <alex@lende.xyz> Date: Mon, 22 May 2023 13:17:23 -0500 Subject: [PATCH 131/131] Fix gutenberg_get_block_editor_settings overriding other hooks (#50760) --- lib/block-editor-settings.php | 77 +++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/lib/block-editor-settings.php b/lib/block-editor-settings.php index c0f514da8d85ef..53668e114e04cb 100644 --- a/lib/block-editor-settings.php +++ b/lib/block-editor-settings.php @@ -6,7 +6,12 @@ */ /** - * Adds styles and __experimentalFeatures to the block editor settings. + * Replaces core 'styles' and '__experimentalFeatures' block editor settings from + * wordpress-develop/block-editor.php with the Gutenberg versions. Much of the + * code is copied from get_block_editor_settings() in that file. + * + * This hook should run first as it completely replaces the core settings that + * other hooks may need to update. * * Note: The settings that are WP version specific should be handled inside the `compat` directory. * @@ -15,7 +20,6 @@ * @return array New block editor settings. */ function gutenberg_get_block_editor_settings( $settings ) { - // Recreate global styles. $global_styles = array(); $presets = array( array( @@ -74,9 +78,74 @@ function gutenberg_get_block_editor_settings( $settings ) { $settings['styles'] = array_merge( $global_styles, get_block_editor_theme_styles() ); - // Copied from get_block_editor_settings() at wordpress-develop/block-editor.php. $settings['__experimentalFeatures'] = gutenberg_get_global_settings(); + // These settings may need to be updated based on data coming from theme.json sources. + if ( isset( $settings['__experimentalFeatures']['color']['palette'] ) ) { + $colors_by_origin = $settings['__experimentalFeatures']['color']['palette']; + $settings['colors'] = isset( $colors_by_origin['custom'] ) ? + $colors_by_origin['custom'] : ( + isset( $colors_by_origin['theme'] ) ? + $colors_by_origin['theme'] : + $colors_by_origin['default'] + ); + } + if ( isset( $settings['__experimentalFeatures']['color']['gradients'] ) ) { + $gradients_by_origin = $settings['__experimentalFeatures']['color']['gradients']; + $settings['gradients'] = isset( $gradients_by_origin['custom'] ) ? + $gradients_by_origin['custom'] : ( + isset( $gradients_by_origin['theme'] ) ? + $gradients_by_origin['theme'] : + $gradients_by_origin['default'] + ); + } + if ( isset( $settings['__experimentalFeatures']['typography']['fontSizes'] ) ) { + $font_sizes_by_origin = $settings['__experimentalFeatures']['typography']['fontSizes']; + $settings['fontSizes'] = isset( $font_sizes_by_origin['custom'] ) ? + $font_sizes_by_origin['custom'] : ( + isset( $font_sizes_by_origin['theme'] ) ? + $font_sizes_by_origin['theme'] : + $font_sizes_by_origin['default'] + ); + } + if ( isset( $settings['__experimentalFeatures']['color']['custom'] ) ) { + $settings['disableCustomColors'] = ! $settings['__experimentalFeatures']['color']['custom']; + unset( $settings['__experimentalFeatures']['color']['custom'] ); + } + if ( isset( $settings['__experimentalFeatures']['color']['customGradient'] ) ) { + $settings['disableCustomGradients'] = ! $settings['__experimentalFeatures']['color']['customGradient']; + unset( $settings['__experimentalFeatures']['color']['customGradient'] ); + } + if ( isset( $settings['__experimentalFeatures']['typography']['customFontSize'] ) ) { + $settings['disableCustomFontSizes'] = ! $settings['__experimentalFeatures']['typography']['customFontSize']; + unset( $settings['__experimentalFeatures']['typography']['customFontSize'] ); + } + if ( isset( $settings['__experimentalFeatures']['typography']['lineHeight'] ) ) { + $settings['enableCustomLineHeight'] = $settings['__experimentalFeatures']['typography']['lineHeight']; + unset( $settings['__experimentalFeatures']['typography']['lineHeight'] ); + } + if ( isset( $settings['__experimentalFeatures']['spacing']['units'] ) ) { + $settings['enableCustomUnits'] = $settings['__experimentalFeatures']['spacing']['units']; + unset( $settings['__experimentalFeatures']['spacing']['units'] ); + } + if ( isset( $settings['__experimentalFeatures']['spacing']['padding'] ) ) { + $settings['enableCustomSpacing'] = $settings['__experimentalFeatures']['spacing']['padding']; + unset( $settings['__experimentalFeatures']['spacing']['padding'] ); + } + if ( isset( $settings['__experimentalFeatures']['spacing']['customSpacingSize'] ) ) { + $settings['disableCustomSpacingSizes'] = ! $settings['__experimentalFeatures']['spacing']['customSpacingSize']; + unset( $settings['__experimentalFeatures']['spacing']['customSpacingSize'] ); + } + + if ( isset( $settings['__experimentalFeatures']['spacing']['spacingSizes'] ) ) { + $spacing_sizes_by_origin = $settings['__experimentalFeatures']['spacing']['spacingSizes']; + $settings['spacingSizes'] = isset( $spacing_sizes_by_origin['custom'] ) ? + $spacing_sizes_by_origin['custom'] : ( + isset( $spacing_sizes_by_origin['theme'] ) ? + $spacing_sizes_by_origin['theme'] : + $spacing_sizes_by_origin['default'] + ); + } return $settings; } -add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings', PHP_INT_MAX ); +add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings', 0 );