From a3b3970bbda02c31b144e33e80d0859242051114 Mon Sep 17 00:00:00 2001 From: Seth Rubenstein Date: Fri, 26 May 2023 20:30:21 -0400 Subject: [PATCH 001/505] This will allow for greater customization of the navigation block, allowing site owners to establish the default core/navigation-link variant they'd like. --- packages/block-library/src/navigation/block.json | 7 +++++++ packages/block-library/src/navigation/edit/index.js | 2 ++ .../block-library/src/navigation/edit/inner-blocks.js | 8 +++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index ce2bed0d8837f..524f4c4616511 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -71,6 +71,13 @@ "templateLock": { "type": [ "string", "boolean" ], "enum": [ "all", "insert", "contentOnly", false ] + }, + "defaultBlock": { + "type": "object", + "default": { + "name": "core/navigation-link", + "attributes": {} + } } }, "providesContext": { diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 52760e6fdfd9b..6a86430a4cc5b 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -93,6 +93,7 @@ function Navigation( { overlayMenu, showSubmenuIcon, templateLock, + defaultBlock, layout: { justifyContent, orientation = 'horizontal', @@ -822,6 +823,7 @@ function Navigation( { } templateLock={ templateLock } orientation={ orientation } + defaultBlock={ defaultBlock } /> ) } diff --git a/packages/block-library/src/navigation/edit/inner-blocks.js b/packages/block-library/src/navigation/edit/inner-blocks.js index 669703f002dbb..0dbe857067a12 100644 --- a/packages/block-library/src/navigation/edit/inner-blocks.js +++ b/packages/block-library/src/navigation/edit/inner-blocks.js @@ -25,6 +25,7 @@ export default function NavigationInnerBlocks( { hasCustomPlaceholder, orientation, templateLock, + defaultBlock, } ) { const { isImmediateParentOfSelectedBlock, @@ -88,6 +89,11 @@ export default function NavigationInnerBlocks( { const showPlaceholder = ! hasCustomPlaceholder && ! hasMenuItems && ! isSelected; + // If the `defaultBlock` attribute is set and itself has an attributes object with values then use it + // otherwise fallback to the DEFAULT_BLOCK constant. + // This allows site owners to set the default `core/navigation-link` variation to use. + const navDefaultBlock = defaultBlock && defaultBlock.attributes && Object.keys(defaultBlock.attributes).length > 0 ? defaultBlock : DEFAULT_BLOCK; + const innerBlocksProps = useInnerBlocksProps( { className: 'wp-block-navigation__container', @@ -98,7 +104,7 @@ export default function NavigationInnerBlocks( { onChange, allowedBlocks: ALLOWED_BLOCKS, prioritizedInserterBlocks: PRIORITIZED_INSERTER_BLOCKS, - __experimentalDefaultBlock: DEFAULT_BLOCK, + __experimentalDefaultBlock: navDefaultBlock, __experimentalDirectInsert: shouldDirectInsert, orientation, templateLock, From e9d426fe05e579400227fb71d4ddf6ed0a21c59b Mon Sep 17 00:00:00 2001 From: Seth Rubenstein Date: Thu, 13 Jul 2023 12:13:22 -0400 Subject: [PATCH 002/505] Update packages/block-library/src/navigation/edit/inner-blocks.js Co-authored-by: Dave Smith --- packages/block-library/src/navigation/edit/inner-blocks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/edit/inner-blocks.js b/packages/block-library/src/navigation/edit/inner-blocks.js index 0dbe857067a12..0c1acfe40b842 100644 --- a/packages/block-library/src/navigation/edit/inner-blocks.js +++ b/packages/block-library/src/navigation/edit/inner-blocks.js @@ -92,7 +92,7 @@ export default function NavigationInnerBlocks( { // If the `defaultBlock` attribute is set and itself has an attributes object with values then use it // otherwise fallback to the DEFAULT_BLOCK constant. // This allows site owners to set the default `core/navigation-link` variation to use. - const navDefaultBlock = defaultBlock && defaultBlock.attributes && Object.keys(defaultBlock.attributes).length > 0 ? defaultBlock : DEFAULT_BLOCK; + const navDefaultBlock = defaultBlock?.attributes && Object.keys(defaultBlock.attributes).length > 0 ? defaultBlock : DEFAULT_BLOCK; const innerBlocksProps = useInnerBlocksProps( { From 7671e7e5465de6d22056fbdb902c558602480f2b Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Sat, 27 May 2023 06:55:16 +0200 Subject: [PATCH 003/505] Add aria labels to the focal point picker component (#50993) * Add aria labels to the focal point picker component * Update CHANGELOG.md * Update button names in the focal point picker test --- packages/components/CHANGELOG.md | 5 +++++ .../components/src/focal-point-picker/controls.tsx | 2 ++ .../components/src/focal-point-picker/test/index.js | 12 +++++++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 97bb1a706aaa3..4d6ea42e8bc0e 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,10 +2,15 @@ ## Unreleased +### Bug Fix + +- `FocalPointUnitControl`: Add aria-labels ([#50993](https://github.com/WordPress/gutenberg/pull/50993)). + ### Experimental - `DropdownMenu` v2: Tweak styles ([#50967](https://github.com/WordPress/gutenberg/pull/50967)). + ## 25.0.0 (2023-05-24) ### Breaking Changes diff --git a/packages/components/src/focal-point-picker/controls.tsx b/packages/components/src/focal-point-picker/controls.tsx index 3e6d33011da73..f204d5736779c 100644 --- a/packages/components/src/focal-point-picker/controls.tsx +++ b/packages/components/src/focal-point-picker/controls.tsx @@ -54,6 +54,7 @@ export default function FocalPointPickerControls( { > @@ -66,6 +67,7 @@ export default function FocalPointPickerControls( { /> diff --git a/packages/components/src/focal-point-picker/test/index.js b/packages/components/src/focal-point-picker/test/index.js index af295150f6cb8..d5c7946cffd86 100644 --- a/packages/components/src/focal-point-picker/test/index.js +++ b/packages/components/src/focal-point-picker/test/index.js @@ -120,7 +120,9 @@ describe( 'FocalPointPicker', () => { const { rerender } = render( ); - const xInput = screen.getByRole( 'spinbutton', { name: 'Left' } ); + const xInput = screen.getByRole( 'spinbutton', { + name: 'Focal point left position', + } ); rerender( ); expect( xInput.value ).toBe( '93' ); } ); @@ -155,10 +157,14 @@ describe( 'FocalPointPicker', () => { ); expect( - screen.getByRole( 'spinbutton', { name: 'Left' } ).value + screen.getByRole( 'spinbutton', { + name: 'Focal point left position', + } ).value ).toBe( '10' ); expect( - screen.getByRole( 'spinbutton', { name: 'Top' } ).value + screen.getByRole( 'spinbutton', { + name: 'Focal point top position', + } ).value ).toBe( '20' ); expect( onChangeSpy ).not.toHaveBeenCalled(); } ); From a03dd317ae72a3d2fc754bdcba57b221a2dfe364 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Sat, 27 May 2023 16:56:02 +0900 Subject: [PATCH 004/505] Details Block: Remove experimental flag and stabilize (#50997) * Details Block: Enable by default on Gutenberg plugin without opt-in * Add fixture * Stabilize the block --- docs/reference-guides/core-blocks.md | 1 - lib/experimental/editor-settings.php | 3 --- lib/experiments-page.php | 12 --------- packages/block-library/src/details/block.json | 1 - packages/block-library/src/index.js | 4 +-- .../fixtures/blocks/core__details.html | 7 ++++++ .../fixtures/blocks/core__details.json | 22 ++++++++++++++++ .../fixtures/blocks/core__details.parsed.json | 25 +++++++++++++++++++ .../blocks/core__details.serialized.html | 5 ++++ 9 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 test/integration/fixtures/blocks/core__details.html create mode 100644 test/integration/fixtures/blocks/core__details.json create mode 100644 test/integration/fixtures/blocks/core__details.parsed.json create mode 100644 test/integration/fixtures/blocks/core__details.serialized.html diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index b84d8edb8e9d9..559417fa5dff7 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -247,7 +247,6 @@ Add an image or video with a text overlay. ([Source](https://github.com/WordPres Hide and show additional content. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/details)) - **Name:** core/details -- **Experimental:** true - **Category:** text - **Supports:** align (full, wide), color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** showContent, summary diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 9571d50fdf3f2..67e08265558e1 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -86,9 +86,6 @@ function gutenberg_enable_experiments() { if ( $gutenberg_experiments && array_key_exists( 'gutenberg-group-grid-variation', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableGroupGridVariation = true', 'before' ); } - 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-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 9e31815f3f50f..eb6a1ea8a7336 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -77,18 +77,6 @@ function gutenberg_initialize_experiments_settings() { ) ); - add_settings_field( - 'gutenberg-details-blocks', - __( 'Details block', 'gutenberg' ), - 'gutenberg_display_experiment_field', - 'gutenberg-experiments', - 'gutenberg_experiments_section', - array( - 'label' => __( 'Test the Details block', 'gutenberg' ), - 'id' => 'gutenberg-details-blocks', - ) - ); - add_settings_field( 'gutenberg-theme-previews', __( 'Block Theme Previews', 'gutenberg' ), diff --git a/packages/block-library/src/details/block.json b/packages/block-library/src/details/block.json index 40321ee6b0c9c..4eb7af8d5ce62 100644 --- a/packages/block-library/src/details/block.json +++ b/packages/block-library/src/details/block.json @@ -1,7 +1,6 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, - "__experimental": true, "name": "core/details", "title": "Details", "category": "text", diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index a0c7b75eac19b..73c2f1eb1140a 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -147,6 +147,7 @@ const getAllBlocks = () => { columns, commentAuthorAvatar, cover, + details, embed, file, group, @@ -226,9 +227,6 @@ const getAllBlocks = () => { queryTitle, postAuthorBiography, ]; - if ( window?.__experimentalEnableDetailsBlocks ) { - blocks.push( details ); - } return blocks.filter( Boolean ); }; diff --git a/test/integration/fixtures/blocks/core__details.html b/test/integration/fixtures/blocks/core__details.html new file mode 100644 index 0000000000000..855ea3f0a4f55 --- /dev/null +++ b/test/integration/fixtures/blocks/core__details.html @@ -0,0 +1,7 @@ + +
Details Summary + +

Details Content

+ +
+ diff --git a/test/integration/fixtures/blocks/core__details.json b/test/integration/fixtures/blocks/core__details.json new file mode 100644 index 0000000000000..e3873e4702db3 --- /dev/null +++ b/test/integration/fixtures/blocks/core__details.json @@ -0,0 +1,22 @@ +[ + { + "name": "core/details", + "isValid": true, + "attributes": { + "showContent": false, + "summary": "Details Summary" + }, + "innerBlocks": [ + { + "name": "core/paragraph", + "isValid": true, + "attributes": { + "content": "Details Content", + "dropCap": false, + "placeholder": "Type / to add a hidden block" + }, + "innerBlocks": [] + } + ] + } +] diff --git a/test/integration/fixtures/blocks/core__details.parsed.json b/test/integration/fixtures/blocks/core__details.parsed.json new file mode 100644 index 0000000000000..3240c013e8e86 --- /dev/null +++ b/test/integration/fixtures/blocks/core__details.parsed.json @@ -0,0 +1,25 @@ +[ + { + "blockName": "core/details", + "attrs": { + "summary": "Details Summary" + }, + "innerBlocks": [ + { + "blockName": "core/paragraph", + "attrs": { + "placeholder": "Type / to add a hidden block" + }, + "innerBlocks": [], + "innerHTML": "\n\t

Details Content

\n\t", + "innerContent": [ "\n\t

Details Content

\n\t" ] + } + ], + "innerHTML": "\n
Details Summary\n\t\n
\n", + "innerContent": [ + "\n
Details Summary\n\t", + null, + "\n
\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__details.serialized.html b/test/integration/fixtures/blocks/core__details.serialized.html new file mode 100644 index 0000000000000..d5d169983bbf3 --- /dev/null +++ b/test/integration/fixtures/blocks/core__details.serialized.html @@ -0,0 +1,5 @@ + +
Details Summary +

Details Content

+
+ From d27d78e49b3b8deac30d577056f2b030316b0342 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sun, 28 May 2023 13:00:10 +0800 Subject: [PATCH 005/505] [ListView] Allow deleting blocks using keyboard (#50422) * Allow deleting blocks using keyboard from listview * Update updateSelection instead * Add support for kyeboard shortcut in the dropdown * Only select block if there isn't any already * More things! --- .../block-settings-dropdown.js | 122 ++-- .../list-view/block-select-button.js | 60 +- .../src/components/list-view/block.js | 42 +- .../src/components/list-view/index.js | 10 +- .../list-view/use-block-selection.js | 4 +- .../specs/editor/various/list-view.spec.js | 560 +++++++++++++----- 6 files changed, 607 insertions(+), 191 deletions(-) diff --git a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js index 2ee452b4fca96..a4087ed84cee3 100644 --- a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js +++ b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js @@ -16,7 +16,10 @@ import { useRef, } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; -import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; +import { + store as keyboardShortcutsStore, + __unstableUseShortcutEventMatch, +} from '@wordpress/keyboard-shortcuts'; import { pipe, useCopyToClipboard } from '@wordpress/compose'; /** @@ -30,7 +33,6 @@ import BlockSettingsMenuControls from '../block-settings-menu-controls'; import { store as blockEditorStore } from '../../store'; import { useShowMoversGestures } from '../block-toolbar/utils'; -const noop = () => {}; const POPOVER_PROPS = { className: 'block-editor-block-settings-menu__popover', position: 'bottom right', @@ -63,7 +65,6 @@ export function BlockSettingsDropdown( { onlyBlock, parentBlockType, previousBlockClientId, - nextBlockClientId, selectedBlockClientIds, } = useSelect( ( select ) => { @@ -72,7 +73,6 @@ export function BlockSettingsDropdown( { getBlockName, getBlockRootClientId, getPreviousBlockClientId, - getNextBlockClientId, getSelectedBlockClientIds, getSettings, getBlockAttributes, @@ -98,12 +98,13 @@ export function BlockSettingsDropdown( { getBlockType( parentBlockName ) ), previousBlockClientId: getPreviousBlockClientId( firstBlockClientId ), - nextBlockClientId: getNextBlockClientId( firstBlockClientId ), selectedBlockClientIds: getSelectedBlockClientIds(), }; }, [ firstBlockClientId ] ); + const { getBlockOrder, getSelectedBlockClientIds } = + useSelect( blockEditorStore ); const shortcuts = useSelect( ( select ) => { const { getShortcutRepresentation } = select( keyboardShortcutsStore ); @@ -120,51 +121,47 @@ export function BlockSettingsDropdown( { ), }; }, [] ); + const isMatch = __unstableUseShortcutEventMatch(); const { selectBlock, toggleBlockHighlight } = useDispatch( blockEditorStore ); + const hasSelectedBlocks = selectedBlockClientIds.length > 0; const updateSelectionAfterDuplicate = useCallback( - __experimentalSelectBlock - ? async ( clientIdsPromise ) => { - const ids = await clientIdsPromise; - if ( ids && ids[ 0 ] ) { - __experimentalSelectBlock( ids[ 0 ] ); - } - } - : noop, + async ( clientIdsPromise ) => { + if ( __experimentalSelectBlock ) { + const ids = await clientIdsPromise; + if ( ids && ids[ 0 ] ) { + __experimentalSelectBlock( ids[ 0 ], false ); + } + } + }, [ __experimentalSelectBlock ] ); - const updateSelectionAfterRemove = useCallback( - __experimentalSelectBlock - ? () => { - const blockToSelect = - previousBlockClientId || - nextBlockClientId || - firstParentClientId; + const updateSelectionAfterRemove = useCallback( () => { + if ( __experimentalSelectBlock ) { + let blockToFocus = previousBlockClientId || firstParentClientId; - if ( - blockToSelect && - // From the block options dropdown, it's possible to remove a block that is not selected, - // in this case, it's not necessary to update the selection since the selected block wasn't removed. - selectedBlockClientIds.includes( firstBlockClientId ) && - // Don't update selection when next/prev block also is in the selection ( and gets removed ), - // In case someone selects all blocks and removes them at once. - ! selectedBlockClientIds.includes( blockToSelect ) - ) { - __experimentalSelectBlock( blockToSelect ); - } - } - : noop, - [ - __experimentalSelectBlock, - previousBlockClientId, - nextBlockClientId, - firstParentClientId, - selectedBlockClientIds, - ] - ); + // Focus the first block if there's no previous block nor parent block. + if ( ! blockToFocus ) { + blockToFocus = getBlockOrder()[ 0 ]; + } + + // Only update the selection if the original selection is removed. + const shouldUpdateSelection = + hasSelectedBlocks && getSelectedBlockClientIds().length === 0; + + __experimentalSelectBlock( blockToFocus, shouldUpdateSelection ); + } + }, [ + __experimentalSelectBlock, + previousBlockClientId, + firstParentClientId, + getBlockOrder, + hasSelectedBlocks, + getSelectedBlockClientIds, + ] ); const removeBlockLabel = count === 1 ? __( 'Delete' ) : __( 'Delete blocks' ); @@ -212,6 +209,49 @@ export function BlockSettingsDropdown( { className="block-editor-block-settings-menu" popoverProps={ POPOVER_PROPS } noIcons + menuProps={ { + /** + * @param {KeyboardEvent} event + */ + onKeyDown( event ) { + if ( event.defaultPrevented ) return; + + if ( + isMatch( 'core/block-editor/remove', event ) && + canRemove + ) { + event.preventDefault(); + updateSelectionAfterRemove( onRemove() ); + } else if ( + isMatch( + 'core/block-editor/duplicate', + event + ) && + canDuplicate + ) { + event.preventDefault(); + updateSelectionAfterDuplicate( onDuplicate() ); + } else if ( + isMatch( + 'core/block-editor/insert-after', + event + ) && + canInsertDefaultBlock + ) { + event.preventDefault(); + onInsertAfter(); + } else if ( + isMatch( + 'core/block-editor/insert-before', + event + ) && + canInsertDefaultBlock + ) { + event.preventDefault(); + onInsertBefore(); + } + }, + } } { ...props } > { ( { onClose } ) => ( diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index 068688a7d5603..ca5e414ae6576 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -13,7 +13,9 @@ import { } from '@wordpress/components'; import { forwardRef } from '@wordpress/element'; import { Icon, lockSmall as lock } from '@wordpress/icons'; -import { SPACE, ENTER } from '@wordpress/keycodes'; +import { SPACE, ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; /** * Internal dependencies @@ -23,6 +25,7 @@ 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'; +import { store as blockEditorStore } from '../../store'; function ListViewBlockSelectButton( { @@ -38,6 +41,7 @@ function ListViewBlockSelectButton( isExpanded, ariaLabel, ariaDescribedBy, + updateFocusAndSelection, }, ref ) { @@ -47,6 +51,15 @@ function ListViewBlockSelectButton( context: 'list-view', } ); const { isLocked } = useBlockLock( clientId ); + const { + getSelectedBlockClientIds, + getPreviousBlockClientId, + getBlockRootClientId, + getBlockOrder, + canRemoveBlocks, + } = useSelect( blockEditorStore ); + const { removeBlocks } = useDispatch( blockEditorStore ); + const isMatch = useShortcutEventMatch(); // 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. @@ -57,9 +70,54 @@ function ListViewBlockSelectButton( onDragStart?.( event ); }; + /** + * @param {KeyboardEvent} event + */ function onKeyDownHandler( event ) { if ( event.keyCode === ENTER || event.keyCode === SPACE ) { onClick( event ); + } else if ( + event.keyCode === BACKSPACE || + event.keyCode === DELETE || + isMatch( 'core/block-editor/remove', event ) + ) { + const selectedBlockClientIds = getSelectedBlockClientIds(); + const isDeletingSelectedBlocks = + selectedBlockClientIds.includes( clientId ); + const firstBlockClientId = isDeletingSelectedBlocks + ? selectedBlockClientIds[ 0 ] + : clientId; + const firstBlockRootClientId = + getBlockRootClientId( firstBlockClientId ); + + const blocksToDelete = isDeletingSelectedBlocks + ? selectedBlockClientIds + : [ clientId ]; + + // Don't update the selection if the blocks cannot be deleted. + if ( ! canRemoveBlocks( blocksToDelete, firstBlockRootClientId ) ) { + return; + } + + let blockToFocus = + getPreviousBlockClientId( firstBlockClientId ) ?? + // If the previous block is not found (when the first block is deleted), + // fallback to focus the parent block. + firstBlockRootClientId; + + removeBlocks( blocksToDelete, false ); + + // Update the selection if the original selection has been removed. + const shouldUpdateSelection = + selectedBlockClientIds.length > 0 && + getSelectedBlockClientIds().length === 0; + + // If there's no previous block nor parent block, focus the first block. + if ( ! blockToFocus ) { + blockToFocus = getBlockOrder()[ 0 ]; + } + + updateFocusAndSelection( blockToFocus, shouldUpdateSelection ); } } diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index dc863dd337c0c..20a385537f9b8 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -22,6 +22,7 @@ import { } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { sprintf, __ } from '@wordpress/i18n'; +import { focus } from '@wordpress/dom'; /** * Internal dependencies @@ -125,6 +126,7 @@ function ListViewBlock( { listViewInstanceId, expandedState, setInsertedBlock, + treeGridElementRef, } = useListViewContext(); const hasSiblings = siblingBlockCount > 0; @@ -165,11 +167,38 @@ function ListViewBlock( { [ clientId, selectBlock ] ); - const updateSelection = useCallback( - ( newClientId ) => { - selectBlock( undefined, newClientId ); + const updateFocusAndSelection = useCallback( + ( focusClientId, shouldSelectBlock ) => { + if ( shouldSelectBlock ) { + selectBlock( undefined, focusClientId, null, null ); + } + + const getFocusElement = () => { + const row = treeGridElementRef.current?.querySelector( + `[role=row][data-block="${ focusClientId }"]` + ); + if ( ! row ) return null; + // Focus the first focusable in the row, which is the ListViewBlockSelectButton. + return focus.focusable.find( row )[ 0 ]; + }; + + let focusElement = getFocusElement(); + if ( focusElement ) { + focusElement.focus(); + } else { + // The element hasn't been painted yet. Defer focusing on the next frame. + // This could happen when all blocks have been deleted and the default block + // hasn't been added to the editor yet. + window.requestAnimationFrame( () => { + focusElement = getFocusElement(); + // Ignore if the element still doesn't exist. + if ( focusElement ) { + focusElement.focus(); + } + } ); + } }, - [ selectBlock ] + [ selectBlock, treeGridElementRef ] ); const toggleExpanded = useCallback( @@ -266,6 +295,7 @@ function ListViewBlock( { selectedClientIds={ selectedClientIds } ariaLabel={ blockAriaLabel } ariaDescribedBy={ descriptionId } + updateFocusAndSelection={ updateFocusAndSelection } />
) } diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 90d85ae8422de..ea637f0fe3131 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -141,8 +141,13 @@ function ListViewComponent( setExpandedState, } ); const selectEditorBlock = useCallback( - ( event, blockClientId ) => { - updateBlockSelection( event, blockClientId ); + /** + * @param {MouseEvent | KeyboardEvent | undefined} event + * @param {string} blockClientId + * @param {null | undefined | -1 | 1} focusPosition + */ + ( event, blockClientId, focusPosition ) => { + updateBlockSelection( event, blockClientId, null, focusPosition ); setSelectedTreeId( blockClientId ); if ( onSelect ) { onSelect( getBlock( blockClientId ) ); @@ -222,6 +227,7 @@ function ListViewComponent( renderAdditionalBlockUI, insertedBlock, setInsertedBlock, + treeGridElementRef: elementRef, } ), [ draggedClientIds, diff --git a/packages/block-editor/src/components/list-view/use-block-selection.js b/packages/block-editor/src/components/list-view/use-block-selection.js index 716995edbdd53..d1bf465d10a9c 100644 --- a/packages/block-editor/src/components/list-view/use-block-selection.js +++ b/packages/block-editor/src/components/list-view/use-block-selection.js @@ -29,9 +29,9 @@ export default function useBlockSelection() { const { getBlockType } = useSelect( blocksStore ); const updateBlockSelection = useCallback( - async ( event, clientId, destinationClientId ) => { + async ( event, clientId, destinationClientId, focusPosition ) => { if ( ! event?.shiftKey ) { - selectBlock( clientId ); + selectBlock( clientId, focusPosition ); return; } diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js index b7021752ea8cf..971d571128bce 100644 --- a/test/e2e/specs/editor/various/list-view.spec.js +++ b/test/e2e/specs/editor/various/list-view.spec.js @@ -4,6 +4,12 @@ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); test.describe( 'List View', () => { + test.use( { + listViewUtils: async ( { page, pageUtils, editor }, use ) => { + await use( new ListViewUtils( { page, pageUtils, editor } ) ); + }, + } ); + test.beforeEach( async ( { admin } ) => { await admin.createNewPost(); } ); @@ -115,146 +121,6 @@ test.describe( 'List View', () => { await expect( listView.getByRole( 'row' ) ).toHaveCount( 2 ); } ); - // Check for regression of https://github.com/WordPress/gutenberg/issues/39026. - test( 'selects the previous block after removing the selected one', async ( { - editor, - page, - pageUtils, - } ) => { - // Insert a couple of blocks of different types. - await editor.insertBlock( { name: 'core/image' } ); - await editor.insertBlock( { name: 'core/heading' } ); - await editor.insertBlock( { name: 'core/paragraph' } ); - - // Open List View. - await pageUtils.pressKeys( 'access+o' ); - const listView = page.getByRole( 'treegrid', { - name: 'Block navigation structure', - } ); - - // The last inserted block should be selected. - await expect( - listView.getByRole( 'gridcell', { - name: 'Paragraph', - exact: true, - selected: true, - } ) - ).toBeVisible(); - - // Remove the Paragraph block via its options menu in List View. - await listView - .getByRole( 'button', { name: 'Options for Paragraph' } ) - .click(); - await page.getByRole( 'menuitem', { name: /Delete/i } ).click(); - - // Heading block should be selected as previous block. - await expect( - editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ) - ).toBeFocused(); - } ); - - // Check for regression of https://github.com/WordPress/gutenberg/issues/39026. - test( 'selects the next block after removing the very first block', async ( { - editor, - page, - pageUtils, - } ) => { - // Insert a couple of blocks of different types. - await editor.insertBlock( { name: 'core/image' } ); - await editor.insertBlock( { name: 'core/heading' } ); - await editor.insertBlock( { name: 'core/paragraph' } ); - - // Open List View. - await pageUtils.pressKeys( 'access+o' ); - const listView = page.getByRole( 'treegrid', { - name: 'Block navigation structure', - } ); - - // The last inserted block should be selected. - await expect( - listView.getByRole( 'gridcell', { - name: 'Paragraph', - exact: true, - selected: true, - } ) - ).toBeVisible(); - - // Select the image block in List View. - await pageUtils.pressKeys( 'ArrowUp', { times: 2 } ); - await expect( - listView.getByRole( 'link', { - name: 'Image', - } ) - ).toBeFocused(); - await page.keyboard.press( 'Enter' ); - - // Remove the Image block via its options menu in List View. - await listView - .getByRole( 'button', { name: 'Options for Image' } ) - .click(); - await page.getByRole( 'menuitem', { name: /Delete/i } ).click(); - - // Heading block should be selected as previous block. - await expect( - editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ) - ).toBeFocused(); - } ); - - /** - * When all the blocks gets removed from the editor, it inserts a default - * paragraph block; make sure that paragraph block gets selected after - * removing blocks from ListView. - */ - test( 'selects the default paragraph block after removing all blocks', async ( { - editor, - page, - pageUtils, - } ) => { - // Insert a couple of blocks of different types. - await editor.insertBlock( { name: 'core/image' } ); - await editor.insertBlock( { name: 'core/heading' } ); - - // Open List View. - await pageUtils.pressKeys( 'access+o' ); - const listView = page.getByRole( 'treegrid', { - name: 'Block navigation structure', - } ); - - // The last inserted block should be selected. - await expect( - listView.getByRole( 'gridcell', { - name: 'Heading', - exact: true, - selected: true, - } ) - ).toBeVisible(); - - // Select the Image block as well. - await pageUtils.pressKeys( 'shift+ArrowUp' ); - await expect( - listView.getByRole( 'gridcell', { - name: 'Image', - exact: true, - selected: true, - } ) - ).toBeVisible(); - - // Remove both blocks. - await listView - .getByRole( 'button', { name: 'Options for Image' } ) - .click(); - await page.getByRole( 'menuitem', { name: /Delete blocks/i } ).click(); - - // Newly created paragraph block should be selected. - await expect( - editor.canvas.getByRole( 'document', { name: /Empty block/i } ) - ).toBeFocused(); - } ); - test( 'expands nested list items', async ( { editor, page, @@ -557,4 +423,418 @@ test.describe( 'List View', () => { } ) ).toBeFocused(); } ); + + test( 'should delete blocks using keyboard', async ( { + editor, + page, + pageUtils, + listViewUtils, + } ) => { + // Insert some blocks of different types. + await editor.insertBlock( { + name: 'core/group', + innerBlocks: [ { name: 'core/pullquote' } ], + } ); + await editor.insertBlock( { + name: 'core/columns', + innerBlocks: [ + { + name: 'core/column', + innerBlocks: [ + { name: 'core/heading' }, + { name: 'core/paragraph' }, + ], + }, + { + name: 'core/column', + innerBlocks: [ { name: 'core/verse' } ], + }, + ], + } ); + await editor.insertBlock( { name: 'core/file' } ); + + // Open List View. + const listView = await listViewUtils.openListView(); + + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'The last inserted block should be selected and focused.' + ) + .toMatchObject( [ + { name: 'core/group' }, + { name: 'core/columns' }, + { name: 'core/file', selected: true, focused: true }, + ] ); + + await page.keyboard.press( 'Delete' ); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Deleting a block should move focus and selection to the previous block' + ) + .toMatchObject( [ + { name: 'core/group' }, + { name: 'core/columns', selected: true, focused: true }, + ] ); + + // Expand the current column. + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'ArrowDown' ); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Move focus but do not select the second column' + ) + .toMatchObject( [ + { name: 'core/group' }, + { + name: 'core/columns', + selected: true, + innerBlocks: [ + { name: 'core/column' }, + { name: 'core/column', focused: true }, + ], + }, + ] ); + + await page.keyboard.press( 'Delete' ); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Deleting a inner block moves focus to the previous inner block' + ) + .toMatchObject( [ + { name: 'core/group' }, + { + name: 'core/columns', + selected: true, + innerBlocks: [ + { + name: 'core/column', + selected: false, + focused: true, + }, + ], + }, + ] ); + + // Expand the current column. + await page.keyboard.press( 'ArrowRight' ); + // Move focus and select the Heading block. + await listView + .getByRole( 'gridcell', { name: 'Heading', exact: true } ) + .dblclick(); + // Select both inner blocks in the column. + await page.keyboard.press( 'Shift+ArrowDown' ); + + await page.keyboard.press( 'Backspace' ); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Deleting multiple blocks moves focus to the parent block' + ) + .toMatchObject( [ + { name: 'core/group' }, + { + name: 'core/columns', + innerBlocks: [ + { + name: 'core/column', + selected: true, + focused: true, + innerBlocks: [], + }, + ], + }, + ] ); + + // Move focus and select the first block. + await listView + .getByRole( 'gridcell', { name: 'Group', exact: true } ) + .dblclick(); + await page.keyboard.press( 'Backspace' ); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Deleting the first block moves focus to the second block' + ) + .toMatchObject( [ + { + name: 'core/columns', + selected: true, + focused: true, + }, + ] ); + + // Keyboard shortcut should also work. + await pageUtils.pressKeys( 'access+z' ); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Deleting the only block left will create a default block and focus/select it' + ) + .toMatchObject( [ + { + name: 'core/paragraph', + selected: true, + focused: true, + }, + ] ); + + await editor.insertBlock( { name: 'core/heading' } ); + await page.evaluate( () => + window.wp.data.dispatch( 'core/block-editor' ).clearSelectedBlock() + ); + await listView + .getByRole( 'gridcell', { name: 'Paragraph' } ) + .getByRole( 'link' ) + .focus(); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Block selection is cleared and focus is on the paragraph block' + ) + .toMatchObject( [ + { name: 'core/paragraph', selected: false, focused: true }, + { name: 'core/heading', selected: false }, + ] ); + + await pageUtils.pressKeys( 'access+z' ); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Deleting blocks without existing selection will not select blocks' + ) + .toMatchObject( [ + { name: 'core/heading', selected: false, focused: true }, + ] ); + + // Insert a block that is locked and cannot be removed. + await editor.insertBlock( { + name: 'core/file', + attributes: { lock: { move: false, remove: true } }, + } ); + // Click on the Heading block to select it. + await listView + .getByRole( 'gridcell', { name: 'Heading', exact: true } ) + .click(); + await listView + .getByRole( 'gridcell', { name: 'File' } ) + .getByRole( 'link' ) + .focus(); + for ( const keys of [ 'Delete', 'Backspace', 'access+z' ] ) { + await pageUtils.pressKeys( keys ); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Trying to delete locked blocks should not do anything' + ) + .toMatchObject( [ + { name: 'core/heading', selected: true, focused: false }, + { name: 'core/file', selected: false, focused: true }, + ] ); + } + } ); + + test( 'block settings dropdown menu', async ( { + editor, + page, + pageUtils, + listViewUtils, + } ) => { + // Insert some blocks of different types. + await editor.insertBlock( { name: 'core/heading' } ); + await editor.insertBlock( { name: 'core/file' } ); + + // Open List View. + const listView = await listViewUtils.openListView(); + + await listView + .getByRole( 'button', { name: 'Options for Heading' } ) + .click(); + + await page + .getByRole( 'menu', { name: 'Options for Heading' } ) + .getByRole( 'menuitem', { name: 'Duplicate' } ) + .click(); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Should duplicate a block and move focus' + ) + .toMatchObject( [ + { name: 'core/heading', selected: false }, + { name: 'core/heading', selected: false, focused: true }, + { name: 'core/file', selected: true }, + ] ); + + await page.keyboard.press( 'Shift+ArrowUp' ); + await listView + .getByRole( 'button', { name: 'Options for Heading' } ) + .first() + .click(); + await page + .getByRole( 'menu', { name: 'Options for Heading' } ) + .getByRole( 'menuitem', { name: 'Delete blocks' } ) + .click(); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Should delete multiple selected blocks using the dropdown menu' + ) + .toMatchObject( [ + { name: 'core/file', selected: true, focused: true }, + ] ); + + await page.keyboard.press( 'ArrowRight' ); + const optionsForFileToggle = listView + .getByRole( 'row' ) + .filter( { + has: page.getByRole( 'gridcell', { name: 'File' } ), + } ) + .getByRole( 'button', { name: 'Options for File' } ); + const optionsForFileMenu = page.getByRole( 'menu', { + name: 'Options for File', + } ); + await expect( + optionsForFileToggle, + 'Pressing arrow right should move focus to the menu dropdown toggle button' + ).toBeFocused(); + + await page.keyboard.press( 'Enter' ); + await expect( + optionsForFileMenu, + 'Pressing Enter should open the menu dropdown' + ).toBeVisible(); + + await page.keyboard.press( 'Escape' ); + await expect( + optionsForFileMenu, + 'Pressing Escape should close the menu dropdown' + ).toBeHidden(); + await expect( + optionsForFileToggle, + 'Should move focus back to the toggle button' + ).toBeFocused(); + + await page.keyboard.press( 'Space' ); + await expect( + optionsForFileMenu, + 'Pressing Space should also open the menu dropdown' + ).toBeVisible(); + + await pageUtils.pressKeys( 'primaryAlt+t' ); // Keyboard shortcut for Insert before. + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Pressing keyboard shortcut should also work when the menu is opened and focused' + ) + .toMatchObject( [ + { name: 'core/paragraph', selected: true, focused: false }, + { name: 'core/file', selected: false, focused: false }, + ] ); + await expect( + optionsForFileMenu, + 'The menu should be closed after pressing keyboard shortcut' + ).toBeHidden(); + + await optionsForFileToggle.click(); + await pageUtils.pressKeys( 'access+z' ); // Keyboard shortcut for Delete. + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Deleting blocks should move focus and selection' + ) + .toMatchObject( [ + { name: 'core/paragraph', selected: true, focused: true }, + ] ); + + // Insert a block that is locked and cannot be removed. + await editor.insertBlock( { + name: 'core/file', + attributes: { lock: { move: false, remove: true } }, + } ); + await optionsForFileToggle.click(); + await expect( + optionsForFileMenu.getByRole( 'menuitem', { name: 'Delete' } ), + 'The delete menu item should be hidden for locked blocks' + ).toBeHidden(); + await pageUtils.pressKeys( 'access+z' ); + await expect + .poll( + listViewUtils.getBlocksWithA11yAttributes, + 'Pressing keyboard shortcut should not delete locked blocks either' + ) + .toMatchObject( [ + { name: 'core/paragraph' }, + { name: 'core/file', selected: true }, + ] ); + await expect( + optionsForFileMenu, + 'The dropdown menu should also be visible' + ).toBeVisible(); + } ); } ); + +/** @typedef {import('@playwright/test').Locator} Locator */ +class ListViewUtils { + #page; + #pageUtils; + #editor; + + constructor( { page, pageUtils, editor } ) { + this.#page = page; + this.#pageUtils = pageUtils; + this.#editor = editor; + + /** @type {Locator} */ + this.listView = page.getByRole( 'treegrid', { + name: 'Block navigation structure', + } ); + } + + /** + * @return {Promise} The list view locator. + */ + openListView = async () => { + await this.#pageUtils.pressKeys( 'access+o' ); + return this.listView; + }; + + getBlocksWithA11yAttributes = async () => { + const selectedRows = await this.listView + .getByRole( 'row' ) + .filter( { + has: this.#page.getByRole( 'gridcell', { selected: true } ), + } ) + .all(); + const selectedClientIds = await Promise.all( + selectedRows.map( ( row ) => row.getAttribute( 'data-block' ) ) + ); + const focusedRows = await this.listView + .getByRole( 'row' ) + .filter( { has: this.#page.locator( ':focus' ) } ) + .all(); + const focusedClientId = + focusedRows.length > 0 + ? await focusedRows[ focusedRows.length - 1 ].getAttribute( + 'data-block' + ) + : null; + // Don't use the util to get the unmodified default block when it's empty. + const blocks = await this.#page.evaluate( () => + window.wp.data.select( 'core/block-editor' ).getBlocks() + ); + function recursivelyApplyAttributes( _blocks ) { + return _blocks.map( ( block ) => ( { + name: block.name, + selected: selectedClientIds.includes( block.clientId ), + focused: block.clientId === focusedClientId, + innerBlocks: recursivelyApplyAttributes( block.innerBlocks ), + } ) ); + } + return recursivelyApplyAttributes( blocks ); + }; +} From 15d5cc100c8bd4cc97c3bfb6bf4983c7234b5bb6 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Sun, 28 May 2023 11:47:17 +0200 Subject: [PATCH 006/505] Refactor code that adds resolver fulfillment to selector calls (#51009) * mapSelector: always look at canonical mappedResolvers * fulfillResolver: doesn't need to check resolver existence * fulfillSelector: doesn't need to be async * fulfillSelector: make it common for all mapped selectors * fulfillResolver: inline into fulfillSelector * fulfillSelector: add missing setTimeout param * mapResolvers: map resolvers and resolving selectors separately --- packages/data/src/redux-store/index.js | 175 +++++++++++-------------- 1 file changed, 75 insertions(+), 100 deletions(-) diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index c63c8fa4a6557..11bda27c128ac 100644 --- a/packages/data/src/redux-store/index.js +++ b/packages/data/src/redux-store/index.js @@ -176,7 +176,6 @@ export default function createReduxStore( key, options ) { lock( store, privateRegistrationFunctions ); const resolversCache = createResolversCache(); - let resolvers; const actions = mapActions( { ...metadataActions, @@ -236,15 +235,15 @@ export default function createReduxStore( key, options ) { } ) ); + let resolvers; if ( options.resolvers ) { - const result = mapResolvers( - options.resolvers, + resolvers = mapResolvers( options.resolvers ); + selectors = mapSelectorsWithResolvers( selectors, + resolvers, store, resolversCache ); - resolvers = result.resolvers; - selectors = result.selectors; } const resolveSelectors = mapResolveSelectors( selectors, store ); @@ -504,20 +503,13 @@ function mapSuspendSelectors( selectors, store ) { } /** - * Returns resolvers with matched selectors for a given namespace. - * Resolvers are side effects invoked once per argument set of a given selector call, - * used in ensuring that the data needs for the selector are satisfied. + * Convert resolvers to a normalized form, an object with `fulfill` method and + * optional methods like `isFulfilled`. * - * @param {Object} resolvers Resolvers to register. - * @param {Object} selectors The current selectors to be modified. - * @param {Object} store The redux store to which the resolvers should be mapped. - * @param {Object} resolversCache Resolvers Cache. + * @param {Object} resolvers Resolver to convert */ -function mapResolvers( resolvers, selectors, store, resolversCache ) { - // The `resolver` can be either a function that does the resolution, or, in more advanced - // cases, an object with a `fullfill` method and other optional methods like `isFulfilled`. - // Here we normalize the `resolver` function to an object with `fulfill` method. - const mappedResolvers = mapValues( resolvers, ( resolver ) => { +function mapResolvers( resolvers ) { + return mapValues( resolvers, ( resolver ) => { if ( resolver.fulfill ) { return resolver; } @@ -527,99 +519,82 @@ function mapResolvers( resolvers, selectors, store, resolversCache ) { fulfill: resolver, // Add the fulfill method. }; } ); +} - const mapSelector = ( selector, selectorName ) => { - const resolver = resolvers[ selectorName ]; - if ( ! resolver ) { - selector.hasResolver = false; - return selector; +/** + * Returns resolvers with matched selectors for a given namespace. + * Resolvers are side effects invoked once per argument set of a given selector call, + * used in ensuring that the data needs for the selector are satisfied. + * + * @param {Object} selectors The current selectors to be modified. + * @param {Object} resolvers Resolvers to register. + * @param {Object} store The redux store to which the resolvers should be mapped. + * @param {Object} resolversCache Resolvers Cache. + */ +function mapSelectorsWithResolvers( + selectors, + resolvers, + store, + resolversCache +) { + function fulfillSelector( resolver, selectorName, args ) { + const state = store.getState(); + + if ( + resolversCache.isRunning( selectorName, args ) || + ( typeof resolver.isFulfilled === 'function' && + resolver.isFulfilled( state, ...args ) ) + ) { + return; } - const selectorResolver = ( ...args ) => { - async function fulfillSelector() { - const state = store.getState(); - - if ( - resolversCache.isRunning( selectorName, args ) || - ( typeof resolver.isFulfilled === 'function' && - resolver.isFulfilled( state, ...args ) ) - ) { - return; - } + const { metadata } = store.__unstableOriginalGetState(); - const { metadata } = store.__unstableOriginalGetState(); - - if ( - metadataSelectors.hasStartedResolution( - metadata, - selectorName, - args - ) - ) { - return; - } + if ( + metadataSelectors.hasStartedResolution( + metadata, + selectorName, + args + ) + ) { + return; + } - resolversCache.markAsRunning( selectorName, args ); + resolversCache.markAsRunning( selectorName, args ); - setTimeout( async () => { - resolversCache.clear( selectorName, args ); - store.dispatch( - metadataActions.startResolution( selectorName, args ) - ); - try { - await fulfillResolver( - store, - mappedResolvers, - selectorName, - ...args - ); - store.dispatch( - metadataActions.finishResolution( - selectorName, - args - ) - ); - } catch ( error ) { - store.dispatch( - metadataActions.failResolution( - selectorName, - args, - error - ) - ); - } - } ); + setTimeout( async () => { + resolversCache.clear( selectorName, args ); + store.dispatch( + metadataActions.startResolution( selectorName, args ) + ); + try { + const action = resolver.fulfill( ...args ); + if ( action ) { + await store.dispatch( action ); + } + store.dispatch( + metadataActions.finishResolution( selectorName, args ) + ); + } catch ( error ) { + store.dispatch( + metadataActions.failResolution( selectorName, args, error ) + ); } + }, 0 ); + } + + return mapValues( selectors, ( selector, selectorName ) => { + const resolver = resolvers[ selectorName ]; + if ( ! resolver ) { + selector.hasResolver = false; + return selector; + } - fulfillSelector( ...args ); + const selectorResolver = ( ...args ) => { + fulfillSelector( resolver, selectorName, args ); return selector( ...args ); }; selectorResolver.hasResolver = true; return selectorResolver; - }; - - return { - resolvers: mappedResolvers, - selectors: mapValues( selectors, mapSelector ), - }; -} - -/** - * Calls a resolver given arguments - * - * @param {Object} store Store reference, for fulfilling via resolvers - * @param {Object} resolvers Store Resolvers - * @param {string} selectorName Selector name to fulfill. - * @param {Array} args Selector Arguments. - */ -async function fulfillResolver( store, resolvers, selectorName, ...args ) { - const resolver = resolvers[ selectorName ]; - if ( ! resolver ) { - return; - } - - const action = resolver.fulfill( ...args ); - if ( action ) { - await store.dispatch( action ); - } + } ); } From 19f73ffc62a2680245103517cd59edf3a764adc5 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Mon, 29 May 2023 11:38:53 +1200 Subject: [PATCH 007/505] Site: editor: Add view site link to site editor nav (#50420) --- .../src/components/site-hub/index.js | 23 +++++++++++++--- .../src/components/site-hub/style.scss | 26 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/site-hub/index.js b/packages/edit-site/src/components/site-hub/index.js index b477c5b14f162..520ff5db2c2dd 100644 --- a/packages/edit-site/src/components/site-hub/index.js +++ b/packages/edit-site/src/components/site-hub/index.js @@ -19,7 +19,7 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { forwardRef } from '@wordpress/element'; -import { search } from '@wordpress/icons'; +import { search, external } from '@wordpress/icons'; import { privateApis as commandsPrivateApis } from '@wordpress/commands'; /** @@ -34,15 +34,20 @@ const { store: commandsStore } = unlock( commandsPrivateApis ); const HUB_ANIMATION_DURATION = 0.3; const SiteHub = forwardRef( ( props, ref ) => { - const { canvasMode, dashboardLink } = useSelect( ( select ) => { + const { canvasMode, dashboardLink, homeUrl } = useSelect( ( select ) => { const { getCanvasMode, getSettings } = unlock( select( editSiteStore ) ); + const { + getUnstableBase, // Site index. + } = select( coreStore ); + return { canvasMode: getCanvasMode(), dashboardLink: getSettings().__experimentalDashboardLink || 'index.php', + homeUrl: getUnstableBase()?.home, }; }, [] ); const { open: openCommandCenter } = useDispatch( commandsStore ); @@ -87,7 +92,11 @@ const SiteHub = forwardRef( ( props, ref ) => { ease: 'easeOut', } } > - + { { decodeEntities( siteTitle ) } + ' + ' . $img[0] . '
'; $body_content = preg_replace( '/]+>/', $button, $content ); - // For the modal, set an ID on the image to be used for an aria-labelledby attribute. - $modal_content = new WP_HTML_Tag_Processor( $content ); - $modal_content->next_tag( 'img' ); - $image_lightbox_id = $modal_content->get_attribute( 'class' ) . '-lightbox'; - $modal_content->set_attribute( 'id', $image_lightbox_id ); - $modal_content = $modal_content->get_updated_html(); - $background_color = wp_get_global_styles( array( 'color', 'background' ) ); $close_button_icon = ''; + $dialog_label = $alt_attribute ? $alt_attribute : __( 'Image' ); + + $close_button_label = __( 'Close' ); + return << - - $modal_content + $content
diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index f1c566d7ae1d9..48ece12d8f347 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -831,14 +831,12 @@ test.describe( 'Image - interactivity', () => { const image = lightbox.locator( 'img' ); await expect( image ).toHaveAttribute( 'src', new RegExp( filename ) ); - await page - .getByRole( 'button', { name: 'Open image lightbox' } ) - .click(); + await page.getByRole( 'button', { name: 'Enlarge image' } ).click(); await expect( lightbox ).toBeVisible(); - const closeButton = page.getByRole( 'button', { - name: 'Close lightbox', + const closeButton = lightbox.getByRole( 'button', { + name: 'Close', } ); await closeButton.click(); @@ -860,11 +858,11 @@ test.describe( 'Image - interactivity', () => { await page.goto( `/?p=${ postId }` ); openLightboxButton = page.getByRole( 'button', { - name: 'Open image lightbox', + name: 'Enlarge image', } ); lightbox = page.getByRole( 'dialog' ); closeButton = lightbox.getByRole( 'button', { - name: 'Close lightbox', + name: 'Close', } ); } ); From 398bec44e7494efcb655f15f7c6bb7cbfdbbd41f Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 29 May 2023 14:18:38 +0400 Subject: [PATCH 013/505] Don't use global 'select' in the Behaviors controls component (#51028) --- packages/block-editor/src/hooks/behaviors.js | 138 +++++++++++-------- 1 file changed, 77 insertions(+), 61 deletions(-) diff --git a/packages/block-editor/src/hooks/behaviors.js b/packages/block-editor/src/hooks/behaviors.js index 8e6ce479f174d..706d3638e345a 100644 --- a/packages/block-editor/src/hooks/behaviors.js +++ b/packages/block-editor/src/hooks/behaviors.js @@ -5,7 +5,7 @@ import { addFilter } from '@wordpress/hooks'; import { SelectControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { createHigherOrderComponent } from '@wordpress/compose'; -import { select } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -18,6 +18,66 @@ import { store as blockEditorStore } from '../store'; */ import merge from 'deepmerge'; +function BehaviorsControl( { blockName, blockBehaviors, onChange } ) { + const { settings, themeBehaviors } = useSelect( + ( select ) => { + const { getBehaviors, getSettings } = select( blockEditorStore ); + + return { + settings: + getSettings()?.__experimentalFeatures?.blocks?.[ blockName ] + ?.behaviors, + themeBehaviors: getBehaviors()?.blocks?.[ blockName ], + }; + }, + [ blockName ] + ); + + if ( + ! settings || + // If every behavior is disabled, do not show the behaviors inspector control. + Object.entries( settings ).every( ( [ , value ] ) => ! value ) + ) { + return null; + } + + // Block behaviors take precedence over theme behaviors. + const behaviors = merge( themeBehaviors, blockBehaviors || {} ); + + const noBehaviorsOption = { + value: '', + label: __( 'No behaviors' ), + }; + + const behaviorsOptions = Object.entries( settings ) + .filter( ( [ , behaviorValue ] ) => behaviorValue ) // Filter out behaviors that are disabled. + .map( ( [ behaviorName ] ) => ( { + value: behaviorName, + label: + // Capitalize the first letter of the behavior name. + behaviorName[ 0 ].toUpperCase() + + behaviorName.slice( 1 ).toLowerCase(), + } ) ); + + const options = [ noBehaviorsOption, ...behaviorsOptions ]; + + return ( + + + + ); +} + /** * Override the default edit UI to include a new block inspector control for * assigning behaviors to blocks if behaviors are enabled in the theme.json. @@ -30,72 +90,28 @@ import merge from 'deepmerge'; */ export const withBehaviors = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { + const blockEdit = ; // Only add behaviors to the core/image block. if ( props.name !== 'core/image' ) { - return ; + return blockEdit; } - const settings = - select( blockEditorStore ).getSettings()?.__experimentalFeatures - ?.blocks?.[ props.name ]?.behaviors; - if ( - ! settings || - // If every behavior is disabled, do not show the behaviors inspector control. - Object.entries( settings ).every( ( [ , value ] ) => ! value ) - ) { - return ; - } - - const { behaviors: blockBehaviors } = props.attributes; - - // Get the theme behaviors for the block from the theme.json. - const themeBehaviors = - select( blockEditorStore ).getBehaviors()?.blocks?.[ props.name ]; - - // Block behaviors take precedence over theme behaviors. - const behaviors = merge( themeBehaviors, blockBehaviors || {} ); - - const noBehaviorsOption = { - value: '', - label: __( 'No behaviors' ), - }; - - const behaviorsOptions = Object.entries( settings ) - .filter( ( [ , behaviorValue ] ) => behaviorValue ) // Filter out behaviors that are disabled. - .map( ( [ behaviorName ] ) => ( { - value: behaviorName, - label: - // Capitalize the first letter of the behavior name. - behaviorName[ 0 ].toUpperCase() + - behaviorName.slice( 1 ).toLowerCase(), - } ) ); - - const options = [ noBehaviorsOption, ...behaviorsOptions ]; - return ( <> - - - { - // If the user selects something, it means that they want to - // change the default value (true) so we save it in the attributes. - props.setAttributes( { - behaviors: { - lightbox: nextValue === 'lightbox', - }, - } ); - } } - hideCancelButton={ true } - help={ __( 'Add behaviors.' ) } - size="__unstable-large" - /> - + { blockEdit } + { + // If the user selects something, it means that they want to + // change the default value (true) so we save it in the attributes. + props.setAttributes( { + behaviors: { + lightbox: nextValue === 'lightbox', + }, + } ); + } } + /> ); }; From 9dcbe88995dd9c0db1f1e0c1025c3b896735ff8b Mon Sep 17 00:00:00 2001 From: Nick Diego Date: Mon, 29 May 2023 05:23:20 -0500 Subject: [PATCH 014/505] Social Links block: Add color classes so icon colors correctly reflect changes in Global Styles (#51020) * Add color classes. * Linting. --- .../block-library/src/social-link/block.json | 2 ++ .../block-library/src/social-link/edit.js | 11 ++++++++- .../block-library/src/social-link/index.php | 23 ++++++++++++++++++- .../block-library/src/social-links/block.json | 2 ++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/social-link/block.json b/packages/block-library/src/social-link/block.json index e81894591b4b3..140cc123ec484 100644 --- a/packages/block-library/src/social-link/block.json +++ b/packages/block-library/src/social-link/block.json @@ -24,7 +24,9 @@ "usesContext": [ "openInNewTab", "showLabels", + "iconColor", "iconColorValue", + "iconBackgroundColor", "iconBackgroundColorValue" ], "supports": { diff --git a/packages/block-library/src/social-link/edit.js b/packages/block-library/src/social-link/edit.js index 738d70e3bdd5e..110bc39713699 100644 --- a/packages/block-library/src/social-link/edit.js +++ b/packages/block-library/src/social-link/edit.js @@ -92,10 +92,19 @@ const SocialLinkEdit = ( { clientId, } ) => { const { url, service, label, rel } = attributes; - const { showLabels, iconColorValue, iconBackgroundColorValue } = context; + const { + showLabels, + iconColor, + iconColorValue, + iconBackgroundColor, + iconBackgroundColorValue, + } = context; const [ showURLPopover, setPopover ] = useState( false ); const classes = classNames( 'wp-social-link', 'wp-social-link-' + service, { 'wp-social-link__is-incomplete': ! url, + [ `has-${ iconColor }-color` ]: iconColor, + [ `has-${ iconBackgroundColor }-background-color` ]: + iconBackgroundColor, } ); // Use internal state instead of a ref to make sure that the component diff --git a/packages/block-library/src/social-link/index.php b/packages/block-library/src/social-link/index.php index 53af6a2e5f485..51ed9374c9bcd 100644 --- a/packages/block-library/src/social-link/index.php +++ b/packages/block-library/src/social-link/index.php @@ -47,7 +47,7 @@ function render_block_core_social_link( $attributes, $content, $block ) { $icon = block_core_social_link_get_icon( $service ); $wrapper_attributes = get_block_wrapper_attributes( array( - 'class' => 'wp-social-link wp-social-link-' . $service, + 'class' => 'wp-social-link wp-social-link-' . $service . block_core_social_link_get_color_classes( $block->context ), 'style' => block_core_social_link_get_color_styles( $block->context ), ) ); @@ -337,3 +337,24 @@ function block_core_social_link_get_color_styles( $context ) { return implode( '', $styles ); } + +/** + * Returns CSS classes for icon and icon background colors. + * + * @param array $context Block context passed to Social Sharing Link. + * + * @return string CSS classes for link's icon and background colors. + */ +function block_core_social_link_get_color_classes( $context ) { + $classes = array(); + + if ( array_key_exists( 'iconColor', $context ) ) { + $classes[] = 'has-' . $context['iconColor'] . '-color'; + } + + if ( array_key_exists( 'iconBackgroundColor', $context ) ) { + $classes[] = 'has-' . $context['iconBackgroundColor'] . '-background-color'; + } + + return ' ' . implode( ' ', $classes ); +} diff --git a/packages/block-library/src/social-links/block.json b/packages/block-library/src/social-links/block.json index a7707cf195134..0e47209f0b8b1 100644 --- a/packages/block-library/src/social-links/block.json +++ b/packages/block-library/src/social-links/block.json @@ -41,7 +41,9 @@ "providesContext": { "openInNewTab": "openInNewTab", "showLabels": "showLabels", + "iconColor": "iconColor", "iconColorValue": "iconColorValue", + "iconBackgroundColor": "iconBackgroundColor", "iconBackgroundColorValue": "iconBackgroundColorValue" }, "supports": { From c40d9a7dd66f5391dd2b7e3bb749c5ac10c8f977 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 29 May 2023 12:31:58 +0100 Subject: [PATCH 015/505] Fix multi-entity multi-property undo redo (#50911) --- docs/reference-guides/data/data-core.md | 4 + packages/core-data/README.md | 4 + packages/core-data/src/actions.js | 17 +- packages/core-data/src/index.js | 1 - packages/core-data/src/private-selectors.ts | 30 +++ packages/core-data/src/reducer.js | 234 +++++++++++--------- packages/core-data/src/selectors.ts | 46 ++-- packages/core-data/src/test/reducer.js | 143 +++++++----- packages/core-data/src/test/selectors.js | 16 +- test/e2e/specs/editor/various/undo.spec.js | 29 +++ 10 files changed, 336 insertions(+), 188 deletions(-) create mode 100644 packages/core-data/src/private-selectors.ts diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index 1ee04e09550e2..8db3a26dd0977 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -358,6 +358,8 @@ _Returns_ ### getRedoEdit +> **Deprecated** since 6.3 + Returns the next edit from the current undo offset for the entity records edits history, if any. _Parameters_ @@ -401,6 +403,8 @@ _Returns_ ### getUndoEdit +> **Deprecated** since 6.3 + Returns the previous edit from the current undo offset for the entity records edits history, if any. _Parameters_ diff --git a/packages/core-data/README.md b/packages/core-data/README.md index dddc3550e03b2..d2ac90a4a5165 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -535,6 +535,8 @@ _Returns_ ### getRedoEdit +> **Deprecated** since 6.3 + Returns the next edit from the current undo offset for the entity records edits history, if any. _Parameters_ @@ -578,6 +580,8 @@ _Returns_ ### getUndoEdit +> **Deprecated** since 6.3 + Returns the previous edit from the current undo offset for the entity records edits history, if any. _Parameters_ diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index ffae417a83cd1..cfab95aae9f8f 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -18,6 +18,7 @@ import { receiveItems, removeItems, receiveQueriedItems } from './queried-data'; import { getOrLoadEntitiesConfig, DEFAULT_ENTITY_KEY } from './entities'; import { createBatch } from './batch'; import { STORE_NAME } from './name'; +import { getUndoEdits, getRedoEdits } from './private-selectors'; /** * Returns an action object used in signalling that authors have been received. @@ -406,14 +407,14 @@ export const editEntityRecord = export const undo = () => ( { select, dispatch } ) => { - const undoEdit = select.getUndoEdit(); + // Todo: we shouldn't have to pass "root" here. + const undoEdit = select( ( state ) => getUndoEdits( state.root ) ); if ( ! undoEdit ) { return; } dispatch( { - type: 'EDIT_ENTITY_RECORD', - ...undoEdit, - meta: { isUndo: true }, + type: 'UNDO', + stackedEdits: undoEdit, } ); }; @@ -424,14 +425,14 @@ export const undo = export const redo = () => ( { select, dispatch } ) => { - const redoEdit = select.getRedoEdit(); + // Todo: we shouldn't have to pass "root" here. + const redoEdit = select( ( state ) => getRedoEdits( state.root ) ); if ( ! redoEdit ) { return; } dispatch( { - type: 'EDIT_ENTITY_RECORD', - ...redoEdit, - meta: { isRedo: true }, + type: 'REDO', + stackedEdits: redoEdit, } ); }; diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js index 43fa4a0b3cd07..c2b491fa8c1ea 100644 --- a/packages/core-data/src/index.js +++ b/packages/core-data/src/index.js @@ -62,7 +62,6 @@ const storeConfig = () => ( { * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore */ export const store = createReduxStore( STORE_NAME, storeConfig() ); - register( store ); export { default as EntityProvider } from './entity-provider'; diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts new file mode 100644 index 0000000000000..0ac2c75023969 --- /dev/null +++ b/packages/core-data/src/private-selectors.ts @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import type { State, UndoEdit } from './selectors'; + +type Optional< T > = T | undefined; + +/** + * Returns the previous edit from the current undo offset + * for the entity records edits history, if any. + * + * @param state State tree. + * + * @return The edit. + */ +export function getUndoEdits( state: State ): Optional< UndoEdit[] > { + return state.undo.list[ state.undo.list.length - 1 + state.undo.offset ]; +} + +/** + * Returns the next edit from the current undo offset + * for the entity records edits history, if any. + * + * @param state State tree. + * + * @return The edit. + */ +export function getRedoEdits( state: State ): Optional< UndoEdit[] > { + return state.undo.list[ state.undo.list.length + state.undo.offset ]; +} diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index f04d543919b8c..b7dd9d73df15a 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -183,6 +183,30 @@ export function themeGlobalStyleVariations( state = {}, action ) { return state; } +const withMultiEntityRecordEdits = ( reducer ) => ( state, action ) => { + if ( action.type === 'UNDO' || action.type === 'REDO' ) { + const { stackedEdits } = action; + + let newState = state; + stackedEdits.forEach( + ( { kind, name, recordId, property, from, to } ) => { + newState = reducer( newState, { + type: 'EDIT_ENTITY_RECORD', + kind, + name, + recordId, + edits: { + [ property ]: action.type === 'UNDO' ? from : to, + }, + } ); + } + ); + return newState; + } + + return reducer( state, action ); +}; + /** * Higher Order Reducer for a given entity config. It supports: * @@ -196,6 +220,8 @@ export function themeGlobalStyleVariations( state = {}, action ) { */ function entity( entityConfig ) { return compose( [ + withMultiEntityRecordEdits, + // Limit to matching action type so we don't attempt to replace action on // an unhandled action. ifMatchingAction( @@ -411,8 +437,9 @@ export const entities = ( state = {}, action ) => { /** * @typedef {Object} UndoStateMeta * - * @property {number} offset Where in the undo stack we are. - * @property {Object} [flattenedUndo] Flattened form of undo stack. + * @property {number} list The undo stack. + * @property {number} offset Where in the undo stack we are. + * @property {Object} cache Cache of unpersisted transient edits. */ /** @typedef {Array & UndoStateMeta} UndoState */ @@ -422,10 +449,7 @@ export const entities = ( state = {}, action ) => { * * @todo Given how we use this we might want to make a custom class for it. */ -const UNDO_INITIAL_STATE = Object.assign( [], { offset: 0 } ); - -/** @type {Object} */ -let lastEditAction; +const UNDO_INITIAL_STATE = { list: [], offset: 0 }; /** * Reducer keeping track of entity edit undo history. @@ -436,107 +460,114 @@ let lastEditAction; * @return {UndoState} Updated state. */ export function undo( state = UNDO_INITIAL_STATE, action ) { + const omitPendingRedos = ( currentState ) => { + return { + ...currentState, + list: currentState.list.slice( + 0, + currentState.offset || undefined + ), + offset: 0, + }; + }; + + const appendCachedEditsToLastUndo = ( currentState ) => { + if ( ! currentState.cache ) { + return currentState; + } + + let nextState = { + ...currentState, + list: [ ...currentState.list ], + }; + nextState = omitPendingRedos( nextState ); + const previousUndoState = nextState.list.pop(); + const updatedUndoState = currentState.cache.reduce( + appendEditToStack, + previousUndoState + ); + nextState.list.push( updatedUndoState ); + + return { + ...nextState, + cache: undefined, + }; + }; + + const appendEditToStack = ( + stack = [], + { kind, name, recordId, property, from, to } + ) => { + const existingEditIndex = stack?.findIndex( + ( { kind: k, name: n, recordId: r, property: p } ) => { + return ( + k === kind && n === name && r === recordId && p === property + ); + } + ); + const nextStack = [ ...stack ]; + if ( existingEditIndex !== -1 ) { + // If the edit is already in the stack leave the initial "from" value. + nextStack[ existingEditIndex ] = { + ...nextStack[ existingEditIndex ], + to, + }; + } else { + nextStack.push( { + kind, + name, + recordId, + property, + from, + to, + } ); + } + return nextStack; + }; + switch ( action.type ) { - case 'EDIT_ENTITY_RECORD': case 'CREATE_UNDO_LEVEL': - let isCreateUndoLevel = action.type === 'CREATE_UNDO_LEVEL'; - const isUndoOrRedo = - ! isCreateUndoLevel && - ( action.meta.isUndo || action.meta.isRedo ); - if ( isCreateUndoLevel ) { - action = lastEditAction; - } else if ( ! isUndoOrRedo ) { - // Don't lose the last edit cache if the new one only has transient edits. - // Transient edits don't create new levels so updating the cache would make - // us skip an edit later when creating levels explicitly. - if ( - Object.keys( action.edits ).some( - ( key ) => ! action.transientEdits[ key ] - ) - ) { - lastEditAction = action; - } else { - lastEditAction = { - ...action, - edits: { - ...( lastEditAction && lastEditAction.edits ), - ...action.edits, - }, - }; - } - } + return appendCachedEditsToLastUndo( state ); - /** @type {UndoState} */ - let nextState; - - if ( isUndoOrRedo ) { - // @ts-ignore we might consider using Object.assign({}, state) - nextState = [ ...state ]; - nextState.offset = - state.offset + ( action.meta.isUndo ? -1 : 1 ); - - if ( state.flattenedUndo ) { - // The first undo in a sequence of undos might happen while we have - // flattened undos in state. If this is the case, we want execution - // to continue as if we were creating an explicit undo level. This - // will result in an extra undo level being appended with the flattened - // undo values. - // We also have to take into account if the `lastEditAction` had opted out - // of being tracked in undo history, like the action that persists the latest - // content right before saving. In that case we have to update the `lastEditAction` - // to avoid returning early before applying the existing flattened undos. - isCreateUndoLevel = true; - if ( ! lastEditAction.meta.undo ) { - lastEditAction.meta.undo = { - edits: {}, - }; - } - action = lastEditAction; - } else { - return nextState; - } - } + case 'UNDO': + case 'REDO': { + const nextState = appendCachedEditsToLastUndo( state ); + return { + ...nextState, + offset: state.offset + ( action.type === 'UNDO' ? -1 : 1 ), + }; + } + case 'EDIT_ENTITY_RECORD': { if ( ! action.meta.undo ) { return state; } - // Transient edits don't create an undo level, but are - // reachable in the next meaningful edit to which they - // are merged. They are defined in the entity's config. - if ( - ! isCreateUndoLevel && - ! Object.keys( action.edits ).some( - ( key ) => ! action.transientEdits[ key ] - ) - ) { - // @ts-ignore we might consider using Object.assign({}, state) - nextState = [ ...state ]; - nextState.flattenedUndo = { - ...state.flattenedUndo, - ...action.edits, + const isCachedChange = Object.keys( action.edits ).every( + ( key ) => action.transientEdits[ key ] + ); + + const edits = Object.keys( action.edits ).map( ( key ) => { + return { + kind: action.kind, + name: action.name, + recordId: action.recordId, + property: key, + from: action.meta.undo.edits[ key ], + to: action.edits[ key ], }; - nextState.offset = state.offset; - return nextState; - } + } ); - // Clear potential redos, because this only supports linear history. - nextState = - // @ts-ignore this needs additional cleanup, probably involving code-level changes - nextState || state.slice( 0, state.offset || undefined ); - nextState.offset = nextState.offset || 0; - nextState.pop(); - if ( ! isCreateUndoLevel ) { - nextState.push( { - kind: action.meta.undo.kind, - name: action.meta.undo.name, - recordId: action.meta.undo.recordId, - edits: { - ...state.flattenedUndo, - ...action.meta.undo.edits, - }, - } ); + if ( isCachedChange ) { + return { + ...state, + cache: edits.reduce( appendEditToStack, state.cache ), + }; } + + let nextState = omitPendingRedos( state ); + nextState = appendCachedEditsToLastUndo( nextState ); + nextState = { ...nextState, list: [ ...nextState.list ] }; // When an edit is a function it's an optimization to avoid running some expensive operation. // We can't rely on the function references being the same so we opt out of comparing them here. const comparisonUndoEdits = Object.values( @@ -546,16 +577,11 @@ export function undo( state = UNDO_INITIAL_STATE, action ) { ( edit ) => typeof edit !== 'function' ); if ( ! isShallowEqual( comparisonUndoEdits, comparisonEdits ) ) { - nextState.push( { - kind: action.kind, - name: action.name, - recordId: action.recordId, - edits: isCreateUndoLevel - ? { ...state.flattenedUndo, ...action.edits } - : action.edits, - } ); + nextState.list.push( edits ); } + return nextState; + } } return state; diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 7513d91810967..a6b7774d37094 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -22,6 +22,7 @@ import { setNestedValue, } from './utils'; import type * as ET from './entity-types'; +import { getUndoEdits, getRedoEdits } from './private-selectors'; // This is an incomplete, high-level approximation of the State type. // It makes the selectors slightly more safe, but is intended to evolve @@ -73,9 +74,18 @@ interface EntityConfig { kind: string; } -interface UndoState extends Array< Object > { - flattenedUndo: unknown; +export interface UndoEdit { + name: string; + kind: string; + recordId: string; + from: any; + to: any; +} + +interface UndoState { + list: Array< UndoEdit[] >; offset: number; + cache: UndoEdit[]; } interface UserState { @@ -884,24 +894,38 @@ function getCurrentUndoOffset( state: State ): number { * Returns the previous edit from the current undo offset * for the entity records edits history, if any. * - * @param state State tree. + * @deprecated since 6.3 + * + * @param state State tree. * * @return The edit. */ export function getUndoEdit( state: State ): Optional< any > { - return state.undo[ state.undo.length - 2 + getCurrentUndoOffset( state ) ]; + deprecated( "select( 'core' ).getUndoEdit()", { + since: '6.3', + } ); + return state.undo.list[ + state.undo.list.length - 2 + getCurrentUndoOffset( state ) + ]?.[ 0 ]; } /** * Returns the next edit from the current undo offset * for the entity records edits history, if any. * - * @param state State tree. + * @deprecated since 6.3 + * + * @param state State tree. * * @return The edit. */ export function getRedoEdit( state: State ): Optional< any > { - return state.undo[ state.undo.length + getCurrentUndoOffset( state ) ]; + deprecated( "select( 'core' ).getRedoEdit()", { + since: '6.3', + } ); + return state.undo.list[ + state.undo.list.length + getCurrentUndoOffset( state ) + ]?.[ 0 ]; } /** @@ -913,7 +937,7 @@ export function getRedoEdit( state: State ): Optional< any > { * @return Whether there is a previous edit or not. */ export function hasUndo( state: State ): boolean { - return Boolean( getUndoEdit( state ) ); + return Boolean( getUndoEdits( state ) ); } /** @@ -925,7 +949,7 @@ export function hasUndo( state: State ): boolean { * @return Whether there is a next edit or not. */ export function hasRedo( state: State ): boolean { - return Boolean( getRedoEdit( state ) ); + return Boolean( getRedoEdits( state ) ); } /** @@ -1142,11 +1166,7 @@ export const hasFetchedAutosaves = createRegistrySelector( export const getReferenceByDistinctEdits = createSelector( // This unused state argument is listed here for the documentation generating tool (docgen). ( state: State ) => [], - ( state: State ) => [ - state.undo.length, - state.undo.offset, - state.undo.flattenedUndo, - ] + ( state: State ) => [ state.undo.list.length, state.undo.offset ] ); /** diff --git a/packages/core-data/src/test/reducer.js b/packages/core-data/src/test/reducer.js index 63caf5fb83b17..4f7d9b9c0d2ae 100644 --- a/packages/core-data/src/test/reducer.js +++ b/packages/core-data/src/test/reducer.js @@ -143,28 +143,34 @@ describe( 'entities', () => { } ); describe( 'undo', () => { - let lastEdits; + let lastValues; let undoState; let expectedUndoState; - const createEditActionPart = ( edits ) => ( { + + const createExpectedDiff = ( property, { from, to } ) => ( { kind: 'someKind', name: 'someName', recordId: 'someRecordId', - edits, + property, + from, + to, } ); const createNextEditAction = ( edits, transientEdits = {} ) => { let action = { - ...createEditActionPart( edits ), + kind: 'someKind', + name: 'someName', + recordId: 'someRecordId', + edits, transientEdits, }; action = { type: 'EDIT_ENTITY_RECORD', ...action, meta: { - undo: { ...action, edits: lastEdits }, + undo: { edits: lastValues }, }, }; - lastEdits = { ...lastEdits, ...edits }; + lastValues = { ...lastValues, ...edits }; return action; }; const createNextUndoState = ( ...args ) => { @@ -172,17 +178,17 @@ describe( 'undo', () => { if ( args[ 0 ] === 'isUndo' || args[ 0 ] === 'isRedo' ) { // We need to "apply" the undo level here and build // the action to move the offset. - lastEdits = - undoState[ - undoState.length + - undoState.offset - - ( args[ 0 ] === 'isUndo' ? 2 : 0 ) - ].edits; + const lastEdits = + undoState.list[ + undoState.list.length - + ( args[ 0 ] === 'isUndo' ? 1 : 0 ) + + undoState.offset + ]; + lastEdits.forEach( ( { property, from, to } ) => { + lastValues[ property ] = args[ 0 ] === 'isUndo' ? from : to; + } ); action = { - type: 'EDIT_ENTITY_RECORD', - meta: { - [ args[ 0 ] ]: true, - }, + type: args[ 0 ] === 'isUndo' ? 'UNDO' : 'REDO', }; } else if ( args[ 0 ] === 'isCreate' ) { action = { type: 'CREATE_UNDO_LEVEL' }; @@ -192,10 +198,9 @@ describe( 'undo', () => { return deepFreeze( undo( undoState, action ) ); }; beforeEach( () => { - lastEdits = {}; + lastValues = {}; undoState = undefined; - expectedUndoState = []; - expectedUndoState.offset = 0; + expectedUndoState = { list: [], offset: 0 }; } ); it( 'initializes', () => { @@ -208,19 +213,41 @@ describe( 'undo', () => { // Check that the first edit creates an undo level for the current state and // one for the new one. undoState = createNextUndoState( { value: 1 } ); - expectedUndoState.push( - createEditActionPart( {} ), - createEditActionPart( { value: 1 } ) - ); + expectedUndoState.list.push( [ + createExpectedDiff( 'value', { from: undefined, to: 1 } ), + ] ); expect( undoState ).toEqual( expectedUndoState ); // Check that the second and third edits just create an undo level for // themselves. undoState = createNextUndoState( { value: 2 } ); - expectedUndoState.push( createEditActionPart( { value: 2 } ) ); + expectedUndoState.list.push( [ + createExpectedDiff( 'value', { from: 1, to: 2 } ), + ] ); expect( undoState ).toEqual( expectedUndoState ); undoState = createNextUndoState( { value: 3 } ); - expectedUndoState.push( createEditActionPart( { value: 3 } ) ); + expectedUndoState.list.push( [ + createExpectedDiff( 'value', { from: 2, to: 3 } ), + ] ); + expect( undoState ).toEqual( expectedUndoState ); + } ); + + it( 'stacks multi-property undo levels', () => { + undoState = createNextUndoState(); + + undoState = createNextUndoState( { value: 1 } ); + undoState = createNextUndoState( { value2: 2 } ); + expectedUndoState.list.push( + [ createExpectedDiff( 'value', { from: undefined, to: 1 } ) ], + [ createExpectedDiff( 'value2', { from: undefined, to: 2 } ) ] + ); + expect( undoState ).toEqual( expectedUndoState ); + + // Check that that creating another undo level merges the "edits" + undoState = createNextUndoState( { value: 2 } ); + expectedUndoState.list.push( [ + createExpectedDiff( 'value', { from: 1, to: 2 } ), + ] ); expect( undoState ).toEqual( expectedUndoState ); } ); @@ -229,11 +256,10 @@ describe( 'undo', () => { undoState = createNextUndoState( { value: 1 } ); undoState = createNextUndoState( { value: 2 } ); undoState = createNextUndoState( { value: 3 } ); - expectedUndoState.push( - createEditActionPart( {} ), - createEditActionPart( { value: 1 } ), - createEditActionPart( { value: 2 } ), - createEditActionPart( { value: 3 } ) + expectedUndoState.list.push( + [ createExpectedDiff( 'value', { from: undefined, to: 1 } ) ], + [ createExpectedDiff( 'value', { from: 1, to: 2 } ) ], + [ createExpectedDiff( 'value', { from: 2, to: 3 } ) ] ); expect( undoState ).toEqual( expectedUndoState ); @@ -255,17 +281,22 @@ describe( 'undo', () => { // Check that another edit will go on top when there // is no undo level offset. undoState = createNextUndoState( { value: 4 } ); - expectedUndoState.push( createEditActionPart( { value: 4 } ) ); + expectedUndoState.list.push( [ + createExpectedDiff( 'value', { from: 3, to: 4 } ), + ] ); expect( undoState ).toEqual( expectedUndoState ); // Check that undoing and editing will slice of // all the levels after the current one. undoState = createNextUndoState( 'isUndo' ); undoState = createNextUndoState( 'isUndo' ); + undoState = createNextUndoState( { value: 5 } ); - expectedUndoState.pop(); - expectedUndoState.pop(); - expectedUndoState.push( createEditActionPart( { value: 5 } ) ); + expectedUndoState.list.pop(); + expectedUndoState.list.pop(); + expectedUndoState.list.push( [ + createExpectedDiff( 'value', { from: 2, to: 5 } ), + ] ); expect( undoState ).toEqual( expectedUndoState ); } ); @@ -277,10 +308,15 @@ describe( 'undo', () => { { transientValue: true } ); undoState = createNextUndoState( { value: 3 } ); - expectedUndoState.push( - createEditActionPart( {} ), - createEditActionPart( { value: 1, transientValue: 2 } ), - createEditActionPart( { value: 3 } ) + expectedUndoState.list.push( + [ + createExpectedDiff( 'value', { from: undefined, to: 1 } ), + createExpectedDiff( 'transientValue', { + from: undefined, + to: 2, + } ), + ], + [ createExpectedDiff( 'value', { from: 1, to: 3 } ) ] ); expect( undoState ).toEqual( expectedUndoState ); } ); @@ -292,10 +328,9 @@ describe( 'undo', () => { // transient edits. undoState = createNextUndoState( { value: 1 } ); undoState = createNextUndoState( 'isCreate' ); - expectedUndoState.push( - createEditActionPart( {} ), - createEditActionPart( { value: 1 } ) - ); + expectedUndoState.list.push( [ + createExpectedDiff( 'value', { from: undefined, to: 1 } ), + ] ); expect( undoState ).toEqual( expectedUndoState ); // Check that transient edits are merged into the last @@ -305,18 +340,19 @@ describe( 'undo', () => { { transientValue: true } ); undoState = createNextUndoState( 'isCreate' ); - expectedUndoState[ - expectedUndoState.length - 1 - ].edits.transientValue = 2; + expectedUndoState.list[ expectedUndoState.list.length - 1 ].push( + createExpectedDiff( 'transientValue', { from: undefined, to: 2 } ) + ); expect( undoState ).toEqual( expectedUndoState ); - // Check that undo levels are created with the latest action, - // even if undone. + // Check that create after undo does nothing. undoState = createNextUndoState( { value: 3 } ); undoState = createNextUndoState( 'isUndo' ); undoState = createNextUndoState( 'isCreate' ); - expectedUndoState.pop(); - expectedUndoState.push( createEditActionPart( { value: 3 } ) ); + expectedUndoState.list.push( [ + createExpectedDiff( 'value', { from: 1, to: 3 } ), + ] ); + expectedUndoState.offset = -1; expect( undoState ).toEqual( expectedUndoState ); } ); @@ -328,10 +364,10 @@ describe( 'undo', () => { { transientValue: true } ); undoState = createNextUndoState( 'isUndo' ); - expectedUndoState.push( - createEditActionPart( {} ), - createEditActionPart( { value: 1, transientValue: 2 } ) - ); + expectedUndoState.list.push( [ + createExpectedDiff( 'value', { from: undefined, to: 1 } ), + createExpectedDiff( 'transientValue', { from: undefined, to: 2 } ), + ] ); expectedUndoState.offset--; expect( undoState ).toEqual( expectedUndoState ); } ); @@ -341,7 +377,6 @@ describe( 'undo', () => { undoState = createNextUndoState(); undoState = createNextUndoState( { value } ); undoState = createNextUndoState( { value: () => {} } ); - expectedUndoState.push( createEditActionPart( { value } ) ); expect( undoState ).toEqual( expectedUndoState ); } ); } ); diff --git a/packages/core-data/src/test/selectors.js b/packages/core-data/src/test/selectors.js index 0ea9e26e50543..84fecc7d07cda 100644 --- a/packages/core-data/src/test/selectors.js +++ b/packages/core-data/src/test/selectors.js @@ -838,20 +838,20 @@ describe( 'getCurrentUser', () => { describe( 'getReferenceByDistinctEdits', () => { it( 'should return referentially equal values across empty states', () => { - const state = { undo: [] }; + const state = { undo: { list: [] } }; expect( getReferenceByDistinctEdits( state ) ).toBe( getReferenceByDistinctEdits( state ) ); - const beforeState = { undo: [] }; - const afterState = { undo: [] }; + const beforeState = { undo: { list: [] } }; + const afterState = { undo: { list: [] } }; expect( getReferenceByDistinctEdits( beforeState ) ).toBe( getReferenceByDistinctEdits( afterState ) ); } ); it( 'should return referentially equal values across unchanging non-empty state', () => { - const undoStates = [ {} ]; + const undoStates = { list: [ {} ] }; const state = { undo: undoStates }; expect( getReferenceByDistinctEdits( state ) ).toBe( getReferenceByDistinctEdits( state ) @@ -866,9 +866,9 @@ describe( 'getReferenceByDistinctEdits', () => { describe( 'when adding edits', () => { it( 'should return referentially different values across changing states', () => { - const beforeState = { undo: [ {} ] }; + const beforeState = { undo: { list: [ {} ] } }; beforeState.undo.offset = 0; - const afterState = { undo: [ {}, {} ] }; + const afterState = { undo: { list: [ {}, {} ] } }; afterState.undo.offset = 1; expect( getReferenceByDistinctEdits( beforeState ) ).not.toBe( getReferenceByDistinctEdits( afterState ) @@ -878,9 +878,9 @@ describe( 'getReferenceByDistinctEdits', () => { describe( 'when using undo', () => { it( 'should return referentially different values across changing states', () => { - const beforeState = { undo: [ {}, {} ] }; + const beforeState = { undo: { list: [ {}, {} ] } }; beforeState.undo.offset = 1; - const afterState = { undo: [ {}, {} ] }; + const afterState = { undo: { list: [ {}, {} ] } }; afterState.undo.offset = 0; expect( getReferenceByDistinctEdits( beforeState ) ).not.toBe( getReferenceByDistinctEdits( afterState ) diff --git a/test/e2e/specs/editor/various/undo.spec.js b/test/e2e/specs/editor/various/undo.spec.js index 29b34ea416ff2..a4b68e1036dcf 100644 --- a/test/e2e/specs/editor/various/undo.spec.js +++ b/test/e2e/specs/editor/various/undo.spec.js @@ -455,6 +455,35 @@ test.describe( 'undo', () => { }, ] ); } ); + + // @see https://github.com/WordPress/gutenberg/issues/12075 + test( 'should be able to undo and redo property cross property changes', async ( { + page, + pageUtils, + } ) => { + await page.getByRole( 'textbox', { name: 'Add title' } ).type( 'a' ); // First step. + await page.keyboard.press( 'Backspace' ); // Second step. + await page.getByRole( 'button', { name: 'Add default block' } ).click(); // third step. + + // Title should be empty + await expect( + page.getByRole( 'textbox', { name: 'Add title' } ) + ).toHaveText( '' ); + + // First undo removes the block. + // Second undo restores the title. + await pageUtils.pressKeys( 'primary+z' ); + await pageUtils.pressKeys( 'primary+z' ); + await expect( + page.getByRole( 'textbox', { name: 'Add title' } ) + ).toHaveText( 'a' ); + + // Redoing the "backspace" should clear the title again. + await pageUtils.pressKeys( 'primaryShift+z' ); + await expect( + page.getByRole( 'textbox', { name: 'Add title' } ) + ).toHaveText( '' ); + } ); } ); class UndoUtils { From bf6d1790a59cc77b07722f30a48c66d21c440ce3 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Mon, 29 May 2023 12:27:03 +0000 Subject: [PATCH 016/505] Bump plugin version to 15.9.0-rc.2 --- 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 9c9e324756258..b3dfd934b68a2 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.9.0-rc.1 + * Version: 15.9.0-rc.2 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 8067c573107d5..963bea8a9dc2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "15.9.0-rc.1", + "version": "15.9.0-rc.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8eccfbe28a79f..42746000729e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "15.9.0-rc.1", + "version": "15.9.0-rc.2", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From c924c4c562926d9efffbc9b9cdc948287dc901e5 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Mon, 29 May 2023 12:56:23 +0000 Subject: [PATCH 017/505] Update Changelog for 15.9.0-rc.2 --- changelog.txt | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/changelog.txt b/changelog.txt index a86bea24af4db..a5748fef9c8cc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,37 @@ == Changelog == += 15.9.0-rc.2 = + +## Changelog + +### Bug Fixes + +#### Patterns +- Library: Revert description change until new grid view lands. ([51039](https://github.com/WordPress/gutenberg/pull/51039)) + +#### Block library +- Social Links: Add color classes so icon colors correctly reflect changes in Global Styles. ([51020](https://github.com/WordPress/gutenberg/pull/51020)) +- Image: Improve the image block lightbox translations, labelling, and escaping. ([50962](https://github.com/WordPress/gutenberg/pull/50962)) + +#### Block editor + +- Don't use global 'select' in the Behaviors controls component. ([51028](https://github.com/WordPress/gutenberg/pull/51028)) +- Lightbox UI appearing with interactivity experiment disabled. ([51025](https://github.com/WordPress/gutenberg/pull/51025)) +- Move "No Behaviors" to be the first option in the list of behaviors. ([50979](https://github.com/WordPress/gutenberg/pull/50979)) +- Revert "Browse Mode: Add snackbar notices. ([50937](https://github.com/WordPress/gutenberg/pull/50937)) + +## First time contributors + +The following PRs were merged by first time contributors: + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @andrewserong @c4rl0sbr4v0 @Mamaduka @michalczaplinski @ndiego @talldan + + = 15.9.0-rc.1 = There has been an error with the Github action that creates the backlog. It will be updated as soon as possible. More info From 8c1319be346df897477f70dea3bdc44edc13c5ab Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 29 May 2023 15:52:20 +0100 Subject: [PATCH 018/505] Add documentation about the entites abstraction and the undo/redo stack (#51052) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André <583546+oandregal@users.noreply.github.com> --- docs/explanations/architecture/README.md | 1 + docs/explanations/architecture/entities.md | 63 ++++++++++++++++++++++ docs/manifest.json | 6 +++ docs/toc.json | 1 + 4 files changed, 71 insertions(+) create mode 100644 docs/explanations/architecture/entities.md diff --git a/docs/explanations/architecture/README.md b/docs/explanations/architecture/README.md index 774df183618d4..2cecdfd70e2d6 100644 --- a/docs/explanations/architecture/README.md +++ b/docs/explanations/architecture/README.md @@ -6,6 +6,7 @@ Let’s look at the big picture and the architectural and UX principles of the b - [Key concepts](/docs/explanations/architecture/key-concepts.md). - [Data format and data flow](/docs/explanations/architecture/data-flow.md). +- [Entities and undo/redo](/docs/explanations/architecture/entities.md). - [Site editing templates](/docs/explanations/architecture/full-site-editing-templates.md). - [Styles in the editor](/docs/explanations/architecture/styles.md). - [Performance](/docs/explanations/architecture/performance.md). diff --git a/docs/explanations/architecture/entities.md b/docs/explanations/architecture/entities.md new file mode 100644 index 0000000000000..368d0a73f87b6 --- /dev/null +++ b/docs/explanations/architecture/entities.md @@ -0,0 +1,63 @@ +# Entities and Undo/Redo. + +The WordPress editors, whether it's the post or site editor, manipulate what we call entity records. These are objects that represent a post, a page, a user, a term, a template, etc. They are the data that is stored in the database and that is manipulated by the editor. Each editor can fetch, edit and save multiple entity records at the same time. + +For instance, when opening a page in the site editor: + - you can edit properties of the page itself (title, content...) + - you can edit properties of the template of the page (content of the template, design...) + - you can edit properties of template parts (header, footer) used with the template. + +The editor keeps track of all these modifications and orchestrates the saving of all these modified records. This happens within the `@wordpress/core-data` package. + + +## Editing entities + +To be able to edit an entity, you need to first fetch it and load it into the `core-data` store. For example, the following code loads the post with ID 1 into the store. (The entity is the post, the post 1 is the entity record). + +````js +wp.data.dispatch( 'core' ).getEntityRecord( 'postType', 'post', 1 ); +```` + +Once the entity is loaded, you can edit it. For example, the following code sets the title of the post to "Hello World". For each fetched entity record, the `core-data` store keeps track of: + - the "persisted" record: The last state of the record as it was fetched from the backend. + - A list of "edits": Unsaved local modifications for one or several properties of the record. + +The package also exposes a set of actions to manipulate the fetched entity records. + +To fetch an entity record, you can call `editEntityRecord`, which takes the entity type, the entity ID and the new entity record as parameters. The following example sets the title of the post with ID 1 to "Hello World". + +````js +wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'post', 1, { title: 'Hello World' } ); +```` + +Once you have edited an entity record, you can save it. The following code saves the post with ID 1. + +````js +wp.data.dispatch( 'core' ).saveEditedEntityRecord( 'postType', 'post', 1 ); +```` + +## Undo/Redo + +Since the WordPress editors allow multiple entity records to be edited at the same time, the `core-data` package keeps track of all the entity records that have been fetched and edited in a common undo/redo stack. Each step in the undo/redo stack contains a list of "edits" that should be undone or redone at the same time when calling the `undo` or `redo` action. + +And to be able to perform both undo and redo operations propertly, each modification in the list of edits contains the following information: + + - Entity kind and name: Each entity in core-data is identified by a tuple kind, name. This corresponds to the identifier of the modified entity. + - Entity Record ID: The ID of the modified record. + - Property: The name of the modified property. + - From: The previous value of the property (needed to apply the undo operation). + - To: The new value of the property (needed to apply the redo operation). + +For example, let's say a user edits the title of a post, followed by a modification to the post slug, and then a modification of the title of a reusable block used with the post for instance. The following information is stored in the undo/redo stack: + + - `[ { kind: 'postType', name: 'post', id: 1, property: 'title', from: '', to: 'Hello World' } ]` + - `[ { kind: 'postType', name: 'post', id: 1, property: 'slug', from: 'Previous slug', to: 'This is the slug of the hello world post' } ]` + - `[ { kind: 'postType', name: 'wp_block', id: 2, property: 'title', from: 'Reusable Block', to: 'Awesome Reusable Block' } ]` + +The store also keep tracks of a "pointer" to the current "undo/redo" step. By default, the pointer always points to the last item in the stack. This pointer is updated when the user performs an undo or redo operation. + +### Transient changes + +The undo/redo core behavior also supports what we call "transient modifications". These are modifications that are not stored in the undo/redo stack right away. For instance, when a user starts typing in a text field, the value of the field is modified in the store, but this modification is not stored in the undo/redo stack until after the user moves to the next word or after a few milliseconds. This is done to avoid creating a new undo/redo step for each character typed by the user. + +So by default, `core-data` store considers all modifications to properties that are marked as "transient" (like the `blocks` property in the post entity) as transient modifications. It keeps these modifications outside the undo/redo stack in what is called a "cache" of modifications and these modifications are only stored in the undo/redo stack when we explicitely call `__unstableCreateUndoLevel` or when the next non transient modification is performed. diff --git a/docs/manifest.json b/docs/manifest.json index d6759f051a679..f10b4dbe06a17 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2039,6 +2039,12 @@ "markdown_source": "../docs/explanations/architecture/data-flow.md", "parent": "architecture" }, + { + "title": "Entities and Undo/Redo.", + "slug": "entities", + "markdown_source": "../docs/explanations/architecture/entities.md", + "parent": "architecture" + }, { "title": "Modularity", "slug": "modularity", diff --git a/docs/toc.json b/docs/toc.json index 7b48e68cbab56..d9acd5fbb38ae 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -302,6 +302,7 @@ "docs/explanations/architecture/README.md": [ { "docs/explanations/architecture/key-concepts.md": [] }, { "docs/explanations/architecture/data-flow.md": [] }, + { "docs/explanations/architecture/entities.md": [] }, { "docs/explanations/architecture/modularity.md": [] }, { "docs/explanations/architecture/performance.md": [] }, { From 54e03ca3f30fb7fbcacb5fdf62909789175e338f Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Mon, 29 May 2023 19:08:25 +0300 Subject: [PATCH 019/505] Properly decode new template title in snackbar (#51057) --- .../edit-site/src/components/add-new-template/new-template.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0a3037603226f..a0ea141bda52e 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 @@ -13,6 +13,7 @@ import { __experimentalText as Text, __experimentalVStack as VStack, } from '@wordpress/components'; +import { decodeEntities } from '@wordpress/html-entities'; import { useState } from '@wordpress/element'; import { useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; @@ -140,7 +141,7 @@ export default function NewTemplate( { sprintf( // translators: %s: Title of the created template e.g: "Category". __( '"%s" successfully created.' ), - newTemplate.title?.rendered || title + decodeEntities( newTemplate.title?.rendered || title ) ), { type: 'snackbar', From ee731378c6bd80fcfa73ad73b1bad4227aef80c9 Mon Sep 17 00:00:00 2001 From: Maggie Date: Mon, 29 May 2023 18:24:38 +0200 Subject: [PATCH 020/505] Link control: migrate tests to Playwright. Can be created by selecting text and using keyboard shortcuts (#50996) * initial setup * fixed utils * update snapshots, commented waitForURLFieldAutoFocus * removed old test * removed unneeded code * removed old snapshots * use locator instead of component class * remove appender function * use locators instead of tabbing around * rename button * avoid snapshots and fix locator for Save button * removed snapshots * added comment --- .../various/__snapshots__/links.test.js.snap | 12 ---- .../specs/editor/various/links.test.js | 42 ----------- test/e2e/specs/editor/blocks/links.spec.js | 69 +++++++++++++++++++ 3 files changed, 69 insertions(+), 54 deletions(-) create mode 100644 test/e2e/specs/editor/blocks/links.spec.js diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap index 7b54b5bd2f598..542f3ed2133db 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap @@ -12,18 +12,6 @@ exports[`Links can be created by selecting text and clicking Link 1`] = ` " `; -exports[`Links can be created by selecting text and using keyboard shortcuts 1`] = ` -" -

This is Gutenberg

-" -`; - -exports[`Links can be created by selecting text and using keyboard shortcuts 2`] = ` -" -

This is Gutenberg

-" -`; - exports[`Links can be created instantly when a URL is selected 1`] = ` "

This is Gutenberg: https://wordpress.org/gutenberg

diff --git a/packages/e2e-tests/specs/editor/various/links.test.js b/packages/e2e-tests/specs/editor/various/links.test.js index 9d8bddff952b6..5920f8463277b 100644 --- a/packages/e2e-tests/specs/editor/various/links.test.js +++ b/packages/e2e-tests/specs/editor/various/links.test.js @@ -103,48 +103,6 @@ describe( 'Links', () => { expect( urlInputValue ).toBe( '' ); } ); - it( 'can be created by selecting text and using keyboard shortcuts', async () => { - // Create a block with some text. - await clickBlockAppender(); - await page.keyboard.type( 'This is Gutenberg' ); - - // Select some text. - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - - // Press Cmd+K to insert a link. - await pressKeyWithModifier( 'primary', 'K' ); - - // Wait for the URL field to auto-focus. - await waitForURLFieldAutoFocus(); - - // Type a URL. - await page.keyboard.type( 'https://wordpress.org/gutenberg' ); - - // Open settings. - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Space' ); - - // Navigate to and toggle the "Open in new tab" checkbox. - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Space' ); - - // Toggle should still have focus and be checked. - await page.waitForSelector( - ':focus:checked.components-checkbox-control__input' - ); - - // Ensure that the contents of the post have not been changed, since at - // this point the link is still not inserted. - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Tab back to the Submit and apply the link. - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Enter' ); - - // The link should have been inserted. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - it( 'can be created without any text selected', async () => { // Create a block with some text. await clickBlockAppender(); diff --git a/test/e2e/specs/editor/blocks/links.spec.js b/test/e2e/specs/editor/blocks/links.spec.js new file mode 100644 index 0000000000000..1e03da549c234 --- /dev/null +++ b/test/e2e/specs/editor/blocks/links.spec.js @@ -0,0 +1,69 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Links', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( `can be created by selecting text and using keyboard shortcuts`, async ( { + page, + editor, + pageUtils, + } ) => { + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'This is Gutenberg' ); + + // Select some text. + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + + // Press Cmd+K to insert a link. + await pageUtils.pressKeys( 'primary+K' ); + + // Type a URL. + await page.keyboard.type( 'https://wordpress.org/gutenberg' ); + + // Open settings. + await page.getByRole( 'button', { name: 'Link Settings' } ).click(); + + // Navigate to and toggle the "Open in new tab" checkbox. + const checkbox = page.getByLabel( 'Open in new tab' ); + await checkbox.click(); + + // Toggle should still have focus and be checked. + await expect( checkbox ).toBeChecked(); + await expect( checkbox ).toBeFocused(); + + // Ensure that the contents of the post have not been changed, since at + // this point the link is still not inserted. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'This is Gutenberg' }, + }, + ] ); + + // Tab back to the Submit and apply the link. + await page + //TODO: change to a better selector when https://github.com/WordPress/gutenberg/issues/51060 is resolved. + .locator( '.block-editor-link-control' ) + .getByRole( 'button', { name: 'Save' } ) + .click(); + + // The link should have been inserted. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: + 'This is Gutenberg', + }, + }, + ] ); + } ); +} ); From bfbff7dc5337496a61facad3e620b8898fd6fc85 Mon Sep 17 00:00:00 2001 From: Maggie Date: Mon, 29 May 2023 19:29:26 +0200 Subject: [PATCH 021/505] Link Control test migration: Should contain a label when it should open in a new tab (#51001) * fixed utils * update snapshots, commented waitForURLFieldAutoFocus * remove appender function * removed snapshots * migrated test * removed old test * removed old snapshots * use proper locator instead of a class * use locators instead of tabbing around, remove appender util function * simplified test * removed snapshots and updated locator of save button * add comment --- .../various/__snapshots__/links.test.js.snap | 12 --- .../specs/editor/various/links.test.js | 77 ------------------- test/e2e/specs/editor/blocks/links.spec.js | 64 +++++++++++++++ 3 files changed, 64 insertions(+), 89 deletions(-) diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap index 542f3ed2133db..541a5456fd4d5 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap @@ -47,15 +47,3 @@ exports[`Links can be removed 1`] = `

This is Gutenberg

" `; - -exports[`Links should contain a label when it should open in a new tab 1`] = ` -" -

This is WordPress

-" -`; - -exports[`Links should contain a label when it should open in a new tab 2`] = ` -" -

This is WordPress

-" -`; diff --git a/packages/e2e-tests/specs/editor/various/links.test.js b/packages/e2e-tests/specs/editor/various/links.test.js index 5920f8463277b..880f786bd4cbe 100644 --- a/packages/e2e-tests/specs/editor/various/links.test.js +++ b/packages/e2e-tests/specs/editor/various/links.test.js @@ -478,83 +478,6 @@ describe( 'Links', () => { ); } ); - it( 'should contain a label when it should open in a new tab', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'This is WordPress' ); - // Select "WordPress". - await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'k' ); - await waitForURLFieldAutoFocus(); - await page.keyboard.type( 'w.org' ); - - // Link settings open - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Space' ); - - // Navigate to and toggle the "Open in new tab" checkbox. - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Space' ); - - // Confirm that focus was not prematurely returned to the paragraph on - // a changing value of the setting. - await page.waitForSelector( - ':focus.components-checkbox-control__input' - ); - - // Submit link. Expect that "Open in new tab" would have been applied - // immediately. - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Enter' ); - - // Wait for Gutenberg to finish the job. - await page.waitForXPath( - '//a[contains(@href,"w.org") and @target="_blank"]' - ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Regression Test: This verifies that the UI is updated according to - // the expected changed values, where previously the value could have - // fallen out of sync with how the UI is displayed (specifically for - // collapsed selections). - // - // See: https://github.com/WordPress/gutenberg/pull/15573 - - // Move caret back into the link. - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - - // Edit link. - await pressKeyWithModifier( 'primary', 'k' ); - await waitForURLFieldAutoFocus(); - await pressKeyWithModifier( 'primary', 'a' ); - await page.keyboard.type( 'wordpress.org' ); - - // Update the link. - await page.keyboard.press( 'Enter' ); - - // Navigate back to the popover. - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - - // Navigate back to inputs to verify appears as changed. - await pressKeyWithModifier( 'primary', 'k' ); - await waitForURLFieldAutoFocus(); - - // Navigate to the "Open in new tab" checkbox. - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Tab' ); - // Uncheck the checkbox. - await page.keyboard.press( 'Space' ); - - // Wait for Gutenberg to finish the job. - await page.waitForXPath( - '//a[contains(@href,"wordpress.org") and not(@target)]' - ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - describe( 'Editing link text', () => { it( 'should not display text input when initially creating the link', async () => { // Create a block with some text. diff --git a/test/e2e/specs/editor/blocks/links.spec.js b/test/e2e/specs/editor/blocks/links.spec.js index 1e03da549c234..208aca3b6e5b3 100644 --- a/test/e2e/specs/editor/blocks/links.spec.js +++ b/test/e2e/specs/editor/blocks/links.spec.js @@ -66,4 +66,68 @@ test.describe( 'Links', () => { }, ] ); } ); + + test( 'can update the url of an existing link', async ( { + page, + editor, + pageUtils, + } ) => { + // Create a block with some text. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.keyboard.type( 'This is WordPress' ); + // Select "WordPress". + await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' ); + await pageUtils.pressKeys( 'primary+k' ); + await page.keyboard.type( 'w.org' ); + + await page + //TODO: change to a better selector when https://github.com/WordPress/gutenberg/issues/51060 is resolved. + .locator( '.block-editor-link-control' ) + .getByRole( 'button', { name: 'Save' } ) + .click(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'This is WordPress', + }, + }, + ] ); + + // Move caret back into the link. + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + + // Edit link. + await pageUtils.pressKeys( 'primary+k' ); + await pageUtils.pressKeys( 'primary+a' ); + await page.keyboard.type( 'wordpress.org' ); + + // Update the link. + await page.keyboard.press( 'Enter' ); + + // Navigate back to the popover. + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + + // Navigate back to inputs to verify appears as changed. + await pageUtils.pressKeys( 'primary+k' ); + const urlInputValue = await page + .getByPlaceholder( 'Search or type url' ) + .inputValue(); + expect( urlInputValue ).toContain( 'wordpress.org' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: + 'This is WordPress', + }, + }, + ] ); + } ); } ); From 897748a0678d9700fcffcd3cbc31f979c9accb7b Mon Sep 17 00:00:00 2001 From: Juan Aldasoro Date: Mon, 29 May 2023 19:40:59 +0200 Subject: [PATCH 022/505] Improve image block lightbox escaping (#51061) * Improve the image block lightbox escaping. --- packages/block-library/src/image/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 3eb84f86af7ed..ab990bc7a0d27 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -68,12 +68,12 @@ function render_block_core_image( $attributes, $content ) { ''; $body_content = preg_replace( '/]+>/', $button, $content ); - $background_color = wp_get_global_styles( array( 'color', 'background' ) ); + $background_color = esc_attr( wp_get_global_styles( array( 'color', 'background' ) ) ); $close_button_icon = ''; - $dialog_label = $alt_attribute ? $alt_attribute : __( 'Image' ); + $dialog_label = $alt_attribute ? esc_attr( $alt_attribute ) : esc_attr__( 'Image' ); - $close_button_label = __( 'Close' ); + $close_button_label = esc_attr__( 'Close' ); return << Date: Mon, 29 May 2023 13:11:56 -0500 Subject: [PATCH 023/505] Remove roadmap doc. (#51062) --- docs/contributors/roadmap.md | 31 ------------------------------- docs/manifest.json | 6 ------ docs/toc.json | 3 +-- 3 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 docs/contributors/roadmap.md diff --git a/docs/contributors/roadmap.md b/docs/contributors/roadmap.md deleted file mode 100644 index ae7aa5ea79773..0000000000000 --- a/docs/contributors/roadmap.md +++ /dev/null @@ -1,31 +0,0 @@ -# Upcoming Projects & Roadmap - -_Complementary to [Phase 2 Scope](https://github.com/WordPress/gutenberg/issues/13113)._ - -This document outlines some of the features currently in development or being considered for the project. It should not be confused with the product roadmap for WordPress itself, even if some areas naturally overlap. The main purpose of it is to give visibility to some of the key problems remaining to be solved and as an invitation for those wanting to collaborate on some of the more complex issues to learn about what needs help. - -Gutenberg is already in use by millions of sites through WordPress, so in order to make substantial changes to the design or updating specifications it is advisable to consider a discussion process ("Request for Comments") showing both an understanding of the impact, both positives and negatives, trade offs and opportunities. - -## Projects - -- **Block Registry** — define an entry point for block identification. ([See active RFC](https://github.com/WordPress/gutenberg/pull/13693).) -- **Live Component Library** — a place to visualize and interact with the UI components and block tools included in the packages. -- **Modular Editor** — allow loading the block editor in several contexts without a dependency to a post object. (Ongoing [pending tasks](https://github.com/WordPress/gutenberg/issues/14043).) -- **Better Validation** — continue to refine the mechanisms used in validating editor content. (See in depth overview at [#11440](https://github.com/WordPress/gutenberg/issues/11440) and [#7604](https://github.com/WordPress/gutenberg/issues/7604).) -- **Block Areas** — build support for areas of blocks that fall outside the content (including relationship with templates, registration, storage, and so on). ([See overview](https://github.com/WordPress/gutenberg/issues/13489).) -- **Multi-Block Editing** — allow modifying attributes of multiple blocks of the same kind at once. -- **Rich Text Roadmap** — continue to develop the capabilities of the rich text package. ([See overview](https://github.com/WordPress/gutenberg/issues/13778).) -- **Common Block Functionality** — coalesce into a preferred mechanism for creating and sharing chunks of functionality (block alignment, color tools, etc) across blocks with a simple and intuitive code interface. (Suggested exploration: React Hooks, [see overview](https://github.com/WordPress/gutenberg/issues/15450).) -- **Responsive Images** — propose mechanisms for handling flexible image sources that can be optimized for loading and takes into account their placement on a page (within main content, a column, sidebar, etc). -- **Async Loading** — propose a strategy for loading block code only when necessary in the editor without overhead for the developer or disrupting the user experience. -- **Styles** — continue to develop the mechanisms for managing block styles and other styling solutions. (See overview at [#7551](https://github.com/WordPress/gutenberg/issues/7551) and [#9534](https://github.com/WordPress/gutenberg/issues/9534).) -- **Bundling Front-end Assets** — explore ways in which front-end styles for blocks could be assembled based on which blocks are used in a given page response. ([See overview](https://github.com/WordPress/gutenberg/issues/5445).) -- **Transforms API** — improve the transform API to allow advanced use-cases: support for async-transforms, access to the block editor settings and bring consistency between the different types of transforms. ([See related issue](https://github.com/WordPress/gutenberg/issues/14755).) - -## Timeline - -The projects outlined above indicate areas of interest but not necessarily development priorities. Sometimes, a product need will accelerate a resolution (as is the case of the block registry), other times community interest might be the driving force. - -- 2019 Q1: Block Registry — First phase. Required for plugin directory "meta" project. -- 2019 Q2: Modular Editor — Requirement for most of phase 2. -- 2019 Q3: Block Areas. diff --git a/docs/manifest.json b/docs/manifest.json index f10b4dbe06a17..8e00a3dcd2b0d 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2314,11 +2314,5 @@ "slug": "versions-in-wordpress", "markdown_source": "../docs/contributors/versions-in-wordpress.md", "parent": "contributors" - }, - { - "title": "Upcoming Projects & Roadmap", - "slug": "roadmap", - "markdown_source": "../docs/contributors/roadmap.md", - "parent": "contributors" } ] diff --git a/docs/toc.json b/docs/toc.json index d9acd5fbb38ae..7512bcea80ecb 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -400,8 +400,7 @@ { "docs/contributors/accessibility-testing.md": [] }, { "docs/contributors/repository-management.md": [] }, { "docs/contributors/folder-structure.md": [] }, - { "docs/contributors/versions-in-wordpress.md": [] }, - { "docs/contributors/roadmap.md": [] } + { "docs/contributors/versions-in-wordpress.md": [] } ] } ] From d336bfb8bb5cb2cbfa7d499093fef016e3c6a8b2 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Tue, 30 May 2023 10:22:36 +1000 Subject: [PATCH 024/505] Try adding dynamic page templates to pages section (#50630) * Try adding dynamic page templates to pages section * remove recent heading in pages * Static homepage case * Back button should go back to Pages * Fix order of home and blog pages * Try fetching all the pages * Stick templates to bottom * Stick item positioning * Revert custom back button behaviour * Move manage link to dynamic pages group * Remove home/posts page suffixes * Address feedback * Add back truncation * Copy array for reorder * Consolidate sticky styles --------- Co-authored-by: Saxon Fletcher Co-authored-by: James Koster --- .../sidebar-navigation-screen-page/style.scss | 5 - .../sidebar-navigation-screen-pages/index.js | 139 +++++++++++++++--- .../style.scss | 4 - .../sidebar-navigation-screen/style.scss | 6 +- packages/edit-site/src/style.scss | 1 - 5 files changed, 119 insertions(+), 36 deletions(-) delete mode 100644 packages/edit-site/src/components/sidebar-navigation-screen-pages/style.scss diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss index 94dd7aa67096b..7f7e6d79b5029 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss @@ -1,8 +1,3 @@ -.edit-site-sidebar-navigation-screen__sticky-section { - padding: $grid-unit-40 0; - margin: $grid-unit-40 $grid-unit-20; -} - .edit-site-sidebar-navigation-screen-page__featured-image-wrapper { background-color: $gray-800; margin-bottom: $grid-unit-20; diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js index ebb38d9478fc4..6a3e02c64825f 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js @@ -5,10 +5,13 @@ import { __experimentalItemGroup as ItemGroup, __experimentalItem as Item, __experimentalTruncate as Truncate, + __experimentalVStack as VStack, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useEntityRecords } from '@wordpress/core-data'; +import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; +import { layout, page, home, loop } from '@wordpress/icons'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -16,22 +19,71 @@ import { decodeEntities } from '@wordpress/html-entities'; import SidebarNavigationScreen from '../sidebar-navigation-screen'; import { useLink } from '../routes/link'; import SidebarNavigationItem from '../sidebar-navigation-item'; -import SidebarNavigationSubtitle from '../sidebar-navigation-subtitle'; -const PageItem = ( { postId, ...props } ) => { +const PageItem = ( { postType = 'page', postId, ...props } ) => { const linkInfo = useLink( { - postType: 'page', + postType, postId, } ); return ; }; export default function SidebarNavigationScreenPages() { - const { records: pages, isResolving: isLoading } = useEntityRecords( + const { records: pages, isResolving: isLoadingPages } = useEntityRecords( 'postType', 'page', - { status: 'any' } + { + status: 'any', + per_page: -1, + } ); + const { records: templates, isResolving: isLoadingTemplates } = + useEntityRecords( 'postType', 'wp_template', { + per_page: -1, + } ); + + const dynamicPageTemplates = templates?.filter( ( { slug } ) => + [ '404', 'search' ].includes( slug ) + ); + + const homeTemplate = + templates?.find( ( template ) => template.slug === 'front-page' ) || + templates?.find( ( template ) => template.slug === 'home' ) || + templates?.find( ( template ) => template.slug === 'index' ); + + const pagesAndTemplates = pages?.concat( dynamicPageTemplates, [ + homeTemplate, + ] ); + + const { frontPage, postsPage } = useSelect( ( select ) => { + const { getEntityRecord } = select( coreStore ); + + const siteSettings = getEntityRecord( 'root', 'site' ); + return { + frontPage: siteSettings?.page_on_front, + postsPage: siteSettings?.page_for_posts, + }; + }, [] ); + + const isHomePageBlog = frontPage === postsPage; + + const reorderedPages = pages && [ ...pages ]; + + if ( ! isHomePageBlog && reorderedPages?.length ) { + const homePageIndex = reorderedPages.findIndex( + ( item ) => item.id === frontPage + ); + const homePage = reorderedPages.splice( homePageIndex, 1 ); + reorderedPages?.splice( 0, 0, ...homePage ); + + const postsPageIndex = reorderedPages.findIndex( + ( item ) => item.id === postsPage + ); + + const blogPage = reorderedPages.splice( postsPageIndex, 1 ); + + reorderedPages.splice( 1, 0, ...blogPage ); + } return ( - { isLoading && ( + { ( isLoadingPages || isLoadingTemplates ) && ( { __( 'Loading pages' ) } ) } - { ! isLoading && ( - <> - - { __( 'Recent' ) } - - - { ! pages?.length && ( - { __( 'No page found' ) } - ) } - { pages?.map( ( page ) => ( + { ! ( isLoadingPages || isLoadingTemplates ) && ( + + { ! pagesAndTemplates?.length && ( + { __( 'No page found' ) } + ) } + { isHomePageBlog && homeTemplate && ( + + + { decodeEntities( + homeTemplate.title?.rendered + ) ?? __( '(no title)' ) } + + + ) } + { reorderedPages?.map( ( item ) => { + let itemIcon; + switch ( item.id ) { + case frontPage: + itemIcon = home; + break; + case postsPage: + itemIcon = loop; + break; + default: + itemIcon = page; + } + return ( { decodeEntities( - page.title?.rendered - ) ?? __( 'Untitled' ) } + item.title?.rendered + ) ?? __( '(no title)' ) } + + + ); + } ) } + + { dynamicPageTemplates?.map( ( item ) => ( + + + { decodeEntities( + item.title?.rendered + ) ?? __( '(no title)' ) } ) ) } @@ -76,8 +169,8 @@ export default function SidebarNavigationScreenPages() { > { __( 'Manage all pages' ) } - - + + ) } } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pages/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-pages/style.scss deleted file mode 100644 index 7bbdd103b6bce..0000000000000 --- a/packages/edit-site/src/components/sidebar-navigation-screen-pages/style.scss +++ /dev/null @@ -1,4 +0,0 @@ -.edit-site-sidebar-navigation-screen-pages__see-all { - /* Overrides the margin that comes from the Item component */ - margin-top: $grid-unit-20 !important; -} 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 f4b284ec92b58..81508a7709d94 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss @@ -73,12 +73,12 @@ } } -.edit-site-sidebar-navigation-screen__sticky-section { +.edit-site-sidebar-navigation-screen__sticky-section.edit-site-sidebar-navigation-screen__sticky-section { position: sticky; bottom: 0; background-color: $gray-900; gap: 0; - padding: $grid-unit-20 0; - margin: $grid-unit-20 0 #{- $grid-unit-20} 0; + padding: $grid-unit-40 0; + margin: $grid-unit-40 $grid-unit-20; border-top: 1px solid $gray-800; } diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 8eff0fe22e038..3aebc258cd510 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -25,7 +25,6 @@ @import "./components/sidebar-button/style.scss"; @import "./components/sidebar-navigation-item/style.scss"; @import "./components/sidebar-navigation-screen/style.scss"; -@import "./components/sidebar-navigation-screen-pages/style.scss"; @import "./components/sidebar-navigation-screen-page/style.scss"; @import "./components/sidebar-navigation-screen-template/style.scss"; @import "./components/sidebar-navigation-screen-templates/style.scss"; From 0a379ee4f75e22d66cb0f1fb44908aa8436240c1 Mon Sep 17 00:00:00 2001 From: Nick Diego Date: Mon, 29 May 2023 20:24:30 -0500 Subject: [PATCH 025/505] Fix formatting and use sentence case for titles. (#51069) --- docs/contributors/code/scripts.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/contributors/code/scripts.md b/docs/contributors/code/scripts.md index d4ce263c80ad1..5cd7efd2fffda 100644 --- a/docs/contributors/code/scripts.md +++ b/docs/contributors/code/scripts.md @@ -2,7 +2,7 @@ The editor provides several vendor and internal scripts to plugin developers. Script names, handles, and descriptions are documented in the table below. -## WP Scripts +## WordPress scripts The editor includes a number of packages to enable various pieces of functionality. Plugin developers can utilize them to create blocks, editor plugins, or generic plugins. @@ -40,7 +40,7 @@ The editor includes a number of packages to enable various pieces of functionali | [Viewport](/packages/viewport/README.md) | wp-viewport | Module for responding to changes in the browser viewport size | | [Wordcount](/packages/wordcount/README.md) | wp-wordcount | WordPress word count utility | -## Vendor Scripts +## Vendor scripts The editor also uses some popular third-party packages and scripts. Plugin developers can use these scripts as well without bundling them in their code (and increasing file sizes). @@ -51,9 +51,10 @@ The editor also uses some popular third-party packages and scripts. Plugin devel | [Moment](https://momentjs.com/) | moment | Parse, validate, manipulate, and display dates and times in JavaScript | | [Lodash](https://lodash.com) | lodash | Lodash is a JavaScript library which provides utility functions for common programming tasks | -## Polyfill Scripts +## Polyfill scripts The editor also provides polyfills for certain features that may not be available in all modern browsers. + It is recommended to use the main `wp-polyfill` script handle which takes care of loading all the below mentioned polyfills. | Script Name | Handle | Description | @@ -67,12 +68,6 @@ It is recommended to use the main `wp-polyfill` script handle which takes care o ## Bundling and code sharing -When using a JavaScript bundler like [webpack](https://webpack.js.org/), the scripts mentioned here -can be excluded from the bundle and provided by WordPress in the form of script dependencies [see -`wp_enqueue_script`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/#default-scripts-included-and-registered-by-wordpress). +When using a JavaScript bundler like [webpack](https://webpack.js.org/), the scripts mentioned here can be excluded from the bundle and provided by WordPress in the form of script dependencies see [`wp_enqueue_script`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/#default-scripts-included-and-registered-by-wordpress). -The -[`@wordpress/dependency-extraction-webpack-plugin`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/dependency-extraction-webpack-plugin) -provides a webpack plugin to help extract WordPress dependencies from bundles. `@wordpress/scripts` -[`build`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/scripts#build) script includes -the plugin by default. +The [`@wordpress/dependency-extraction-webpack-plugin`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/dependency-extraction-webpack-plugin) provides a webpack plugin to help extract WordPress dependencies from bundles. The `@wordpress/scripts` [`build`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/scripts#build) script includes the plugin by default. From bba52a3e68b49e8b6ce29b0da25017c581f81930 Mon Sep 17 00:00:00 2001 From: Nick Diego Date: Mon, 29 May 2023 20:25:14 -0500 Subject: [PATCH 026/505] Fix error in code example. (#51070) --- docs/how-to-guides/curating-the-editor-experience.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to-guides/curating-the-editor-experience.md b/docs/how-to-guides/curating-the-editor-experience.md index 795f0db59eaf7..51054655f51aa 100644 --- a/docs/how-to-guides/curating-the-editor-experience.md +++ b/docs/how-to-guides/curating-the-editor-experience.md @@ -343,7 +343,7 @@ addFilter( 'blockEditor.useSetting.before', 'example/useSetting.before', ( settingValue, settingName, clientId, blockName ) => { - if ( blockName === Media & Text block'core/column' && settingName === 'spacing.units' ) { + if ( blockName === 'core/column' && settingName === 'spacing.units' ) { return [ 'px' ]; } return settingValue; From 97e39725093cf656078127b6a5f32ce3b9faa28d Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Tue, 30 May 2023 10:56:19 +0800 Subject: [PATCH 027/505] Fix screen readers not announcing updated `aria-describedby` in Firefox (#51035) * Fix aria-describedby not updating content * Try aria-description. * Try a different approach * Fix missing prop --------- Co-authored-by: Alex Stine --- .../src/components/list-view/appender.js | 8 ++--- .../list-view/aria-referenced-text.js | 30 +++++++++++++++++++ .../src/components/list-view/block.js | 8 ++--- .../src/components/list-view/style.scss | 5 ---- 4 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 packages/block-editor/src/components/list-view/aria-referenced-text.js diff --git a/packages/block-editor/src/components/list-view/appender.js b/packages/block-editor/src/components/list-view/appender.js index cb731bbf227a8..2d2f633637293 100644 --- a/packages/block-editor/src/components/list-view/appender.js +++ b/packages/block-editor/src/components/list-view/appender.js @@ -14,6 +14,7 @@ import { store as blockEditorStore } from '../../store'; import useBlockDisplayTitle from '../block-title/use-block-display-title'; import { useListViewContext } from './context'; import Inserter from '../inserter'; +import AriaReferencedText from './aria-referenced-text'; export const Appender = forwardRef( ( { nestingLevel, blockCount, clientId, ...props }, ref ) => { @@ -90,12 +91,9 @@ export const Appender = forwardRef( } } } /> -
+ { description } -
+ ); } diff --git a/packages/block-editor/src/components/list-view/aria-referenced-text.js b/packages/block-editor/src/components/list-view/aria-referenced-text.js new file mode 100644 index 0000000000000..b5d7a73e8bcf5 --- /dev/null +++ b/packages/block-editor/src/components/list-view/aria-referenced-text.js @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { useRef, useEffect } from '@wordpress/element'; + +/** + * A component specifically designed to be used as an element referenced + * by ARIA attributes such as `aria-labelledby` or `aria-describedby`. + * + * @param {Object} props Props. + * @param {import('react').ReactNode} props.children + */ +export default function AriaReferencedText( { children, ...props } ) { + const ref = useRef(); + + useEffect( () => { + if ( ref.current ) { + // This seems like a no-op, but it fixes a bug in Firefox where + // it fails to recompute the text when only the text node changes. + // @see https://github.com/WordPress/gutenberg/pull/51035 + ref.current.textContent = ref.current.textContent; + } + }, [ children ] ); + + return ( + + ); +} diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index 20a385537f9b8..e9eb4a7827686 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -40,6 +40,7 @@ import { store as blockEditorStore } from '../../store'; import useBlockDisplayInformation from '../use-block-display-information'; import { useBlockLock } from '../block-lock'; import { unlock } from '../../lock-unlock'; +import AriaReferencedText from './aria-referenced-text'; function ListViewBlock( { block: { clientId }, @@ -297,12 +298,9 @@ function ListViewBlock( { ariaDescribedBy={ descriptionId } updateFocusAndSelection={ updateFocusAndSelection } /> -
+ { blockPositionDescription } -
+ ) } diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index 5a4e80fa926fa..082389f71d4a0 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -340,11 +340,6 @@ } } -.block-editor-list-view-block-select-button__description, -.block-editor-list-view-appender__description { - display: none; -} - .block-editor-list-view-block__contents-cell, .block-editor-list-view-appender__cell { .block-editor-list-view-block__contents-container, From 15a31df3e2562481400824c0a9aa3433d900f2ee Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 30 May 2023 07:39:49 +0200 Subject: [PATCH 028/505] Change label and description for the `gutenberg-interactivity-api-core-blocks` experiments setting. (#51059) --- lib/experiments-page.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/experiments-page.php b/lib/experiments-page.php index eb6a1ea8a7336..3b9dd9437b372 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -91,12 +91,12 @@ function gutenberg_initialize_experiments_settings() { add_settings_field( 'gutenberg-interactivity-api-core-blocks', - __( 'Core blocks', 'gutenberg' ), + __( 'Interactivity API and Behaviors UI', 'gutenberg' ), 'gutenberg_display_experiment_field', 'gutenberg-experiments', 'gutenberg_experiments_section', array( - 'label' => __( 'Test the core blocks using the Interactivity API', 'gutenberg' ), + 'label' => __( 'Use the Interactivity API in File, Navigation and Image core blocks. It also enables the Behaviors UI in the Image block.', 'gutenberg' ), 'id' => 'gutenberg-interactivity-api-core-blocks', ) ); From 62eb41bef8464c490fc69da0fdadc12f5ec5059f Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 30 May 2023 16:34:10 +1000 Subject: [PATCH 029/505] We can't perform a null ?? check on decodeEntities to verify falsy values because decodeEntities will return an empty string if it encounters one. (#51074) Updated Untitled copy to be consistent with pages view. --- .../sidebar-navigation-screen-page/index.js | 15 +++++++++------ .../sidebar-navigation-screen-pages/index.js | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js index 332c6ba62bfd4..fa4baeea631ca 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js @@ -145,9 +145,9 @@ export default function SidebarNavigationScreenPage() { : null; if ( _parentTitle?.title ) { - _parentTitle = _parentTitle.title?.rendered - ? decodeEntities( _parentTitle.title.rendered ) - : __( 'Untitled' ); + _parentTitle = decodeEntities( + _parentTitle.title?.rendered || __( '(no title)' ) + ); } else { _parentTitle = __( 'Top level' ); } @@ -181,7 +181,9 @@ export default function SidebarNavigationScreenPage() { return record ? ( setCanvasMode( 'edit' ) } @@ -218,8 +220,9 @@ export default function SidebarNavigationScreenPage() { altText ? decodeEntities( altText ) : decodeEntities( - record.title?.rendered - ) || __( 'Featured image' ) + record.title?.rendered || + __( 'Featured image' ) + ) } src={ mediaSourceUrl } /> diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js index 6a3e02c64825f..0ad7df02020d1 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js @@ -111,8 +111,9 @@ export default function SidebarNavigationScreenPages() { > { decodeEntities( - homeTemplate.title?.rendered - ) ?? __( '(no title)' ) } + homeTemplate.title?.rendered || + __( '(no title)' ) + ) } ) } @@ -137,8 +138,9 @@ export default function SidebarNavigationScreenPages() { > { decodeEntities( - item.title?.rendered - ) ?? __( '(no title)' ) } + item?.title?.rendered || + __( '(no title)' ) + ) } ); @@ -154,8 +156,9 @@ export default function SidebarNavigationScreenPages() { > { decodeEntities( - item.title?.rendered - ) ?? __( '(no title)' ) } + item.title?.rendered || + __( '(no title)' ) + ) } ) ) } From aae82f871465ac29747478b0df3b9a39ba44eff5 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 30 May 2023 08:37:02 +0100 Subject: [PATCH 030/505] E2e tests: Add an assertion to confirm that the URL changed (#50835) --- test/e2e/specs/site-editor/command-center.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/e2e/specs/site-editor/command-center.spec.js b/test/e2e/specs/site-editor/command-center.spec.js index 9661a91a6abc7..57b07faf4d9b4 100644 --- a/test/e2e/specs/site-editor/command-center.spec.js +++ b/test/e2e/specs/site-editor/command-center.spec.js @@ -28,6 +28,9 @@ test.describe( 'Site editor command center', () => { await page.getByRole( 'option', { name: 'Add new page' } ).click(); await page.waitForSelector( 'iframe[name="editor-canvas"]' ); const frame = page.frame( 'editor-canvas' ); + await expect( page ).toHaveURL( + '/wp-admin/post-new.php?post_type=page' + ); await expect( frame.getByRole( 'textbox', { name: 'Add title' } ) ).toBeVisible(); From dfb78c0d54f2b822f1c95d3c9047fb20b34e677d Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 30 May 2023 12:40:40 +0400 Subject: [PATCH 031/505] Link Control: Add missing translation (#51081) --- packages/block-editor/src/components/link-control/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index 8d5860cd97a99..b90f6d245d953 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -383,7 +383,7 @@ function LinkControl( { __nextHasNoMarginBottom ref={ textInputRef } className="block-editor-link-control__setting block-editor-link-control__text-content" - label="Text" + label={ __( 'Text' ) } value={ internalControlValue?.title } onChange={ setInternalTextInputValue } onKeyDown={ handleSubmitWithEnter } From d39ba86fb2108d5b5b1dcd12505f2da2e16cddf8 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca Date: Tue, 30 May 2023 11:12:32 +0100 Subject: [PATCH 032/505] Docs: Undo/Redo: Minor edits (#51085) --- docs/explanations/architecture/entities.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/explanations/architecture/entities.md b/docs/explanations/architecture/entities.md index 368d0a73f87b6..1e037ed5b2824 100644 --- a/docs/explanations/architecture/entities.md +++ b/docs/explanations/architecture/entities.md @@ -24,7 +24,7 @@ Once the entity is loaded, you can edit it. For example, the following code sets The package also exposes a set of actions to manipulate the fetched entity records. -To fetch an entity record, you can call `editEntityRecord`, which takes the entity type, the entity ID and the new entity record as parameters. The following example sets the title of the post with ID 1 to "Hello World". +To edit an entity record, you can call `editEntityRecord`, which takes the entity type, the entity ID and the new entity record as parameters. The following example sets the title of the post with ID 1 to "Hello World". ````js wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'post', 1, { title: 'Hello World' } ); @@ -42,13 +42,13 @@ Since the WordPress editors allow multiple entity records to be edited at the sa And to be able to perform both undo and redo operations propertly, each modification in the list of edits contains the following information: - - Entity kind and name: Each entity in core-data is identified by a tuple kind, name. This corresponds to the identifier of the modified entity. + - Entity kind and name: Each entity in core-data is identified by the pair _(kind, name)_. This corresponds to the identifier of the modified entity. - Entity Record ID: The ID of the modified record. - Property: The name of the modified property. - From: The previous value of the property (needed to apply the undo operation). - To: The new value of the property (needed to apply the redo operation). -For example, let's say a user edits the title of a post, followed by a modification to the post slug, and then a modification of the title of a reusable block used with the post for instance. The following information is stored in the undo/redo stack: +For example, let's say a user edits the title of a post, followed by a modification to the post slug, and then a modification of the title of a reusable block used with the post. The following information is stored in the undo/redo stack: - `[ { kind: 'postType', name: 'post', id: 1, property: 'title', from: '', to: 'Hello World' } ]` - `[ { kind: 'postType', name: 'post', id: 1, property: 'slug', from: 'Previous slug', to: 'This is the slug of the hello world post' } ]` @@ -60,4 +60,4 @@ The store also keep tracks of a "pointer" to the current "undo/redo" step. By de The undo/redo core behavior also supports what we call "transient modifications". These are modifications that are not stored in the undo/redo stack right away. For instance, when a user starts typing in a text field, the value of the field is modified in the store, but this modification is not stored in the undo/redo stack until after the user moves to the next word or after a few milliseconds. This is done to avoid creating a new undo/redo step for each character typed by the user. -So by default, `core-data` store considers all modifications to properties that are marked as "transient" (like the `blocks` property in the post entity) as transient modifications. It keeps these modifications outside the undo/redo stack in what is called a "cache" of modifications and these modifications are only stored in the undo/redo stack when we explicitely call `__unstableCreateUndoLevel` or when the next non transient modification is performed. +So by default, `core-data` store considers all modifications to properties that are marked as "transient" (like the `blocks` property in the post entity) as transient modifications. It keeps these modifications outside the undo/redo stack in what is called a "cache" of modifications and these modifications are only stored in the undo/redo stack when we explicitely call `__unstableCreateUndoLevel` or when the next non-transient modification is performed. From 7d1fdf9c3a2122e2f938b52ec106fdfc153c54e4 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 30 May 2023 11:26:38 +0100 Subject: [PATCH 033/505] Update the document title in the template mode of the post editor (#50864) --- .../components/header/document-title/index.js | 89 +++++++++ .../header/document-title/style.scss | 61 ++++++ .../header/header-toolbar/style.scss | 1 - .../edit-post/src/components/header/index.js | 6 +- .../src/components/header/style.scss | 6 + .../header/template-title/delete-template.js | 106 ---------- .../template-title/edit-template-title.js | 78 -------- .../components/header/template-title/index.js | 115 ----------- .../header/template-title/style.scss | 94 --------- .../template-title/template-description.js | 42 ---- .../src/components/visual-editor/index.js | 20 +- .../src/components/visual-editor/style.scss | 13 -- packages/edit-post/src/style.scss | 2 +- .../various/post-editor-template-mode.spec.js | 186 +----------------- 14 files changed, 166 insertions(+), 653 deletions(-) create mode 100644 packages/edit-post/src/components/header/document-title/index.js create mode 100644 packages/edit-post/src/components/header/document-title/style.scss delete mode 100644 packages/edit-post/src/components/header/template-title/delete-template.js delete mode 100644 packages/edit-post/src/components/header/template-title/edit-template-title.js delete mode 100644 packages/edit-post/src/components/header/template-title/index.js delete mode 100644 packages/edit-post/src/components/header/template-title/style.scss delete mode 100644 packages/edit-post/src/components/header/template-title/template-description.js diff --git a/packages/edit-post/src/components/header/document-title/index.js b/packages/edit-post/src/components/header/document-title/index.js new file mode 100644 index 0000000000000..1b27a0bacf014 --- /dev/null +++ b/packages/edit-post/src/components/header/document-title/index.js @@ -0,0 +1,89 @@ +/** + * WordPress dependencies + */ +import { __, isRTL } from '@wordpress/i18n'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { BlockIcon, store as blockEditorStore } from '@wordpress/block-editor'; +import { + Button, + VisuallyHidden, + __experimentalHStack as HStack, + __experimentalText as Text, +} from '@wordpress/components'; +import { layout, chevronLeftSmall, chevronRightSmall } from '@wordpress/icons'; +import { privateApis as commandsPrivateApis } from '@wordpress/commands'; +import { displayShortcut } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import { unlock } from '../../../private-apis'; +import { store as editPostStore } from '../../../store'; + +const { store: commandsStore } = unlock( commandsPrivateApis ); + +function DocumentTitle() { + const { template, isEditing } = useSelect( ( select ) => { + const { isEditingTemplate, getEditedPostTemplate } = + select( editPostStore ); + const _isEditing = isEditingTemplate(); + + return { + template: _isEditing ? getEditedPostTemplate() : null, + isEditing: _isEditing, + }; + }, [] ); + const { clearSelectedBlock } = useDispatch( blockEditorStore ); + const { setIsEditingTemplate } = useDispatch( editPostStore ); + const { open: openCommandCenter } = useDispatch( commandsStore ); + + if ( ! isEditing || ! template ) { + return null; + } + + let templateTitle = __( 'Default' ); + if ( template?.title ) { + templateTitle = template.title; + } else if ( !! template ) { + templateTitle = template.slug; + } + + return ( +
+ + + + + + +
+ ); +} + +export default DocumentTitle; diff --git a/packages/edit-post/src/components/header/document-title/style.scss b/packages/edit-post/src/components/header/document-title/style.scss new file mode 100644 index 0000000000000..e39ecf607e430 --- /dev/null +++ b/packages/edit-post/src/components/header/document-title/style.scss @@ -0,0 +1,61 @@ +.edit-post-document-title { + display: flex; + align-items: center; + gap: $grid-unit; + height: $button-size; + 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); + + &:hover { + color: currentColor; + background: $gray-200; + } +} + +.edit-post-document-title__title.components-button { + flex-grow: 1; + color: var(--wp-block-synced-color); + overflow: hidden; + + &:hover { + color: var(--wp-block-synced-color); + } + + h1 { + color: var(--wp-block-synced-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.edit-post-document-title__shortcut { + flex-shrink: 0; + color: $gray-700; + padding: 0 $grid-unit-15; + + &:hover { + color: $gray-700; + } +} + +.edit-post-document-title__left { + min-width: $button-size; + flex-shrink: 0; + + .components-button.has-icon.has-text { + color: $gray-700; + gap: 0; + + &:hover { + color: currentColor; + } + } +} diff --git a/packages/edit-post/src/components/header/header-toolbar/style.scss b/packages/edit-post/src/components/header/header-toolbar/style.scss index 87aec00004c02..694dcb5a2d678 100644 --- a/packages/edit-post/src/components/header/header-toolbar/style.scss +++ b/packages/edit-post/src/components/header/header-toolbar/style.scss @@ -1,6 +1,5 @@ .edit-post-header-toolbar { display: inline-flex; - flex-grow: 1; align-items: center; border: none; diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 09a93424f6903..3306a0fdf1606 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -18,7 +18,7 @@ import { default as DevicePreview } from '../device-preview'; import ViewLink from '../view-link'; import MainDashboardButton from './main-dashboard-button'; import { store as editPostStore } from '../../store'; -import TemplateTitle from './template-title'; +import DocumentTitle from './document-title'; function Header( { setEntitiesSavedStatesCallback } ) { const isLargeViewport = useViewportMatch( 'large' ); @@ -70,7 +70,9 @@ function Header( { setEntitiesSavedStatesCallback } ) { className="edit-post-header__toolbar" > - +
+ +
{ - const { isEditingTemplate, getEditedPostTemplate } = - select( editPostStore ); - const _isEditing = isEditingTemplate(); - return { - template: _isEditing ? getEditedPostTemplate() : null, - }; - }, [] ); - const [ showConfirmDialog, setShowConfirmDialog ] = useState( false ); - - if ( ! template || ! template.wp_id ) { - return null; - } - let templateTitle = template.slug; - if ( template?.title ) { - templateTitle = template.title; - } - - const isRevertable = template?.has_theme_file; - - const onDelete = () => { - clearSelectedBlock(); - setIsEditingTemplate( false ); - setShowConfirmDialog( false ); - - editPost( { - template: '', - } ); - const settings = getEditorSettings(); - const newAvailableTemplates = Object.fromEntries( - Object.entries( settings.availableTemplates ?? {} ).filter( - ( [ id ] ) => id !== template.slug - ) - ); - updateEditorSettings( { - availableTemplates: newAvailableTemplates, - } ); - deleteEntityRecord( 'postType', 'wp_template', template.id, { - throwOnError: true, - } ); - }; - - return ( - - <> - { - setShowConfirmDialog( true ); - } } - info={ - isRevertable - ? __( 'Use the template as supplied by the theme.' ) - : undefined - } - > - { isRevertable - ? __( 'Clear customizations' ) - : __( 'Delete template' ) } - - { - setShowConfirmDialog( false ); - } } - > - { sprintf( - /* translators: %s: template name */ - __( - 'Are you sure you want to delete the %s template? It may be used by other pages or posts.' - ), - templateTitle - ) } - - - - ); -} diff --git a/packages/edit-post/src/components/header/template-title/edit-template-title.js b/packages/edit-post/src/components/header/template-title/edit-template-title.js deleted file mode 100644 index 447ea5e4e02d7..0000000000000 --- a/packages/edit-post/src/components/header/template-title/edit-template-title.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { TextControl } from '@wordpress/components'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { useState } from '@wordpress/element'; -import { store as editorStore } from '@wordpress/editor'; -import { store as coreStore } from '@wordpress/core-data'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - -export default function EditTemplateTitle() { - const [ forceEmpty, setForceEmpty ] = useState( false ); - const { template } = useSelect( ( select ) => { - const { getEditedPostTemplate } = select( editPostStore ); - return { - template: getEditedPostTemplate(), - }; - }, [] ); - - const { editEntityRecord } = useDispatch( coreStore ); - const { getEditorSettings } = useSelect( editorStore ); - const { updateEditorSettings } = useDispatch( editorStore ); - - // Only user-created and non-default templates can change the name. - if ( ! template.is_custom || template.has_theme_file ) { - return null; - } - - let templateTitle = __( 'Default' ); - if ( template?.title ) { - templateTitle = template.title; - } else if ( !! template ) { - templateTitle = template.slug; - } - - return ( -
- { - // Allow having the field temporarily empty while typing. - if ( ! newTitle && ! forceEmpty ) { - setForceEmpty( true ); - return; - } - setForceEmpty( false ); - - const settings = getEditorSettings(); - const newAvailableTemplates = Object.fromEntries( - Object.entries( settings.availableTemplates ?? {} ).map( - ( [ id, existingTitle ] ) => [ - id, - id !== template.slug ? existingTitle : newTitle, - ] - ) - ); - updateEditorSettings( { - availableTemplates: newAvailableTemplates, - } ); - editEntityRecord( 'postType', 'wp_template', template.id, { - title: newTitle, - } ); - } } - onBlur={ () => setForceEmpty( false ) } - /> -
- ); -} diff --git a/packages/edit-post/src/components/header/template-title/index.js b/packages/edit-post/src/components/header/template-title/index.js deleted file mode 100644 index c0745dc0451b7..0000000000000 --- a/packages/edit-post/src/components/header/template-title/index.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { - Dropdown, - Button, - __experimentalText as Text, -} from '@wordpress/components'; -import { chevronDown } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; -import { store as blockEditorStore } from '@wordpress/block-editor'; -import { store as editorStore } from '@wordpress/editor'; -import DeleteTemplate from './delete-template'; -import EditTemplateTitle from './edit-template-title'; -import TemplateDescription from './template-description'; - -function TemplateTitle() { - const { template, isEditing, title } = useSelect( ( select ) => { - const { isEditingTemplate, getEditedPostTemplate } = - select( editPostStore ); - const { getEditedPostAttribute } = select( editorStore ); - - const _isEditing = isEditingTemplate(); - - return { - template: _isEditing ? getEditedPostTemplate() : null, - isEditing: _isEditing, - title: getEditedPostAttribute( 'title' ) - ? getEditedPostAttribute( 'title' ) - : __( 'Untitled' ), - }; - }, [] ); - - const { clearSelectedBlock } = useDispatch( blockEditorStore ); - const { setIsEditingTemplate } = useDispatch( editPostStore ); - - if ( ! isEditing || ! template ) { - return null; - } - - let templateTitle = __( 'Default' ); - if ( template?.title ) { - templateTitle = template.title; - } else if ( !! template ) { - templateTitle = template.slug; - } - - const hasOptions = !! ( - template.custom || - template.wp_id || - template.description - ); - - return ( -
- - { hasOptions ? ( - ( - - ) } - renderContent={ () => ( - <> - - - - - ) } - /> - ) : ( - - { templateTitle } - - ) } -
- ); -} - -export default TemplateTitle; diff --git a/packages/edit-post/src/components/header/template-title/style.scss b/packages/edit-post/src/components/header/template-title/style.scss deleted file mode 100644 index b5fe5120bfb64..0000000000000 --- a/packages/edit-post/src/components/header/template-title/style.scss +++ /dev/null @@ -1,94 +0,0 @@ -.edit-post-template-top-area { - display: flex; - flex-direction: column; - align-content: space-between; - width: 100%; - align-items: center; - - .edit-post-template-title, - .edit-post-template-post-title { - padding: 0; - text-decoration: none; - height: auto; - - &::before { - height: 100%; - } - - &.has-icon { - svg { - order: 1; - margin-right: 0; - } - } - } - - .edit-post-template-title { - color: $gray-900; - } - - .edit-post-template-post-title { - margin-top: $grid-unit-05; - max-width: 160px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: block; - - &::before { - left: 0; - right: 0; - } - - @include break-xlarge() { - max-width: 400px; - } - } -} - -.edit-post-template-top-area__popover { - .components-popover__content { - min-width: 280px; - padding: 0; - } - - .edit-site-template-details__group { - padding: $grid-unit-20; - - .components-base-control__help { - margin-bottom: 0; - } - } - - .edit-post-template-details__description { - color: $gray-700; - } -} - -.edit-post-template-top-area__second-menu-group { - border-top: $border-width solid $gray-300; - padding: $grid-unit-20 $grid-unit-10; - - .edit-post-template-top-area__delete-template-button { - display: flex; - justify-content: center; - padding: $grid-unit-05 $grid-unit; - - &.is-destructive { - padding: inherit; - margin-left: $grid-unit-10; - margin-right: $grid-unit-10; - width: calc(100% - #{($grid-unit * 2)}); - - .components-menu-item__item { - width: auto; - } - } - - .components-menu-item__item { - margin-right: 0; - min-width: 0; - width: 100%; - } - } -} diff --git a/packages/edit-post/src/components/header/template-title/template-description.js b/packages/edit-post/src/components/header/template-title/template-description.js deleted file mode 100644 index 3513496852c33..0000000000000 --- a/packages/edit-post/src/components/header/template-title/template-description.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { - __experimentalHeading as Heading, - __experimentalText as Text, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - -export default function TemplateDescription() { - const { description, title } = useSelect( ( select ) => { - const { getEditedPostTemplate } = select( editPostStore ); - return { - title: getEditedPostTemplate().title, - description: getEditedPostTemplate().description, - }; - }, [] ); - if ( ! description ) { - return null; - } - - return ( -
- - { title } - - - { description } - -
- ); -} diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index ac8902f6a5f7a..638a869aa8350 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -31,11 +31,9 @@ import { __experimentaluseLayoutStyles as useLayoutStyles, } from '@wordpress/block-editor'; import { useEffect, useRef, useMemo } from '@wordpress/element'; -import { Button, __unstableMotion as motion } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { __unstableMotion as motion } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; import { useMergeRefs } from '@wordpress/compose'; -import { arrowLeft } from '@wordpress/icons'; -import { __ } from '@wordpress/i18n'; import { parse } from '@wordpress/blocks'; import { store as coreStore } from '@wordpress/core-data'; @@ -175,8 +173,6 @@ export default function VisualEditor( { styles } ) { _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, }; }, [] ); - const { clearSelectedBlock } = useDispatch( blockEditorStore ); - const { setIsEditingTemplate } = useDispatch( editPostStore ); const desktopCanvasStyles = { height: '100%', width: '100%', @@ -349,18 +345,6 @@ export default function VisualEditor( { styles } ) { } } ref={ blockSelectionClearerRef } > - { isTemplateMode && ( - - ) } { ) ).toBeVisible(); } ); - - test( 'Allow editing the title of a new custom template', async ( { - page, - postEditorTemplateMode, - } ) => { - async function editTemplateTitle( newTitle ) { - await page - .getByRole( 'button', { name: 'Template Options' } ) - .click(); - - await page - .getByRole( 'textbox', { name: 'Title' } ) - .fill( newTitle ); - - const editorContent = page.getByLabel( 'Editor Content' ); - await editorContent.click(); - } - - await postEditorTemplateMode.createPostAndSaveDraft(); - await postEditorTemplateMode.createNewTemplate( 'Foobar' ); - await editTemplateTitle( 'Barfoo' ); - - await expect( - page.getByRole( 'button', { name: 'Template Options' } ) - ).toHaveText( 'Barfoo' ); - } ); - - test.describe( 'Delete Post Template Confirmation Dialog', () => { - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activateTheme( 'twentytwentyone' ); - } ); - - test.beforeEach( async ( { postEditorTemplateMode } ) => { - await postEditorTemplateMode.createPostAndSaveDraft(); - } ); - - [ 'large', 'small' ].forEach( ( viewport ) => { - test( `should retain template if deletion is canceled when the viewport is ${ viewport }`, async ( { - editor, - page, - pageUtils, - postEditorTemplateMode, - } ) => { - await pageUtils.setBrowserViewport( viewport ); - - await postEditorTemplateMode.disableTemplateWelcomeGuide(); - - const templateTitle = `${ viewport } Viewport Deletion Test`; - - await postEditorTemplateMode.createNewTemplate( templateTitle ); - - // Close the settings in small viewport. - if ( viewport === 'small' ) { - await page.click( 'role=button[name="Close settings"i]' ); - } - - // Edit the template. - await editor.insertBlock( { name: 'core/paragraph' } ); - await page.keyboard.type( - 'Just a random paragraph added to the template' - ); - - await postEditorTemplateMode.saveTemplateWithoutPublishing(); - - // Test deletion dialog. - { - const templateDropdown = - postEditorTemplateMode.editorTopBar.locator( - 'role=button[name="Template Options"i]' - ); - await templateDropdown.click(); - await page.click( - 'role=menuitem[name="Delete template"i]' - ); - - const confirmDeletionDialog = page.locator( 'role=dialog' ); - await expect( confirmDeletionDialog ).toBeFocused(); - await expect( - confirmDeletionDialog.locator( - `text=Are you sure you want to delete the ${ templateTitle } template? It may be used by other pages or posts.` - ) - ).toBeVisible(); - - await confirmDeletionDialog - .locator( 'role=button[name="Cancel"i]' ) - .click(); - } - - // Exit template mode. - await page.click( 'role=button[name="Back"i]' ); - - await editor.openDocumentSettingsSidebar(); - - // Move focus to the "Post" panel in the editor sidebar. - const postPanel = - postEditorTemplateMode.editorSettingsSidebar.locator( - 'role=button[name="Post"i]' - ); - await postPanel.click(); - - await postEditorTemplateMode.openTemplatePopover(); - - const templateSelect = page.locator( - 'role=combobox[name="Template"i]' - ); - await expect( templateSelect ).toHaveValue( - `${ viewport }-viewport-deletion-test` - ); - } ); - - test( `should delete template if deletion is confirmed when the viewport is ${ viewport }`, async ( { - editor, - page, - pageUtils, - postEditorTemplateMode, - } ) => { - const templateTitle = `${ viewport } Viewport Deletion Test`; - - await pageUtils.setBrowserViewport( viewport ); - - await postEditorTemplateMode.createNewTemplate( templateTitle ); - - // Close the settings in small viewport. - if ( viewport === 'small' ) { - await page.click( 'role=button[name="Close settings"i]' ); - } - - // Edit the template. - await editor.insertBlock( { name: 'core/paragraph' } ); - await page.keyboard.type( - 'Just a random paragraph added to the template' - ); - - await postEditorTemplateMode.saveTemplateWithoutPublishing(); - - { - const templateDropdown = - postEditorTemplateMode.editorTopBar.locator( - 'role=button[name="Template Options"i]' - ); - await templateDropdown.click(); - await page.click( - 'role=menuitem[name="Delete template"i]' - ); - - const confirmDeletionDialog = page.locator( 'role=dialog' ); - await expect( confirmDeletionDialog ).toBeFocused(); - await expect( - confirmDeletionDialog.locator( - `text=Are you sure you want to delete the ${ templateTitle } template? It may be used by other pages or posts.` - ) - ).toBeVisible(); - - await confirmDeletionDialog - .locator( 'role=button[name="OK"i]' ) - .click(); - } - - // Saving isn't technically necessary, but for themes without any specified templates, - // the removal of the Templates dropdown is delayed. A save and reload allows for this - // delay and prevents flakiness - { - await page.click( 'role=button[name="Save draft"i]' ); - await page.waitForSelector( - 'role=button[name="Dismiss this notice"] >> text=Draft saved' - ); - await page.reload(); - } - - const templateOptions = - postEditorTemplateMode.editorSettingsSidebar.locator( - 'role=combobox[name="Template:"i] >> role=menuitem' - ); - const availableTemplates = - await templateOptions.allTextContents(); - - expect( availableTemplates ).not.toContain( - `${ viewport } Viewport Deletion Test` - ); - } ); - } ); - } ); } ); class PostEditorTemplateMode { @@ -331,7 +149,9 @@ class PostEditorTemplateMode { 'role=button[name="Dismiss this notice"] >> text=Editing template. Changes made here affect all posts and pages that use the template.' ); - await expect( this.editorTopBar ).toHaveText( /Just an FSE Post/ ); + await expect( + this.editorTopBar.getByRole( 'heading[level=1]' ) + ).toHaveText( 'Editing template: Singular' ); } async createPostAndSaveDraft() { From 00931ae2249d3db5b294e4ce558ba3ae5fa24cf5 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Tue, 30 May 2023 13:58:04 +0200 Subject: [PATCH 034/505] Navigation block: Set correct aria-expanded on hover (#50953) * Set correct aria-expanded on hover * Store both `click` and `hover` in `isMenuOpen` * Fix nav menu * Remove menuOpenedOn debugger * Add comments and fix example HTML * Fix PHP lint * Just in case openSubmenusOnClick exists but it's false * Switch to isMenuOpen selector in roleAttribute --- lib/experimental/interactivity-api/blocks.php | 62 +++++---- .../src/navigation/interactivity.js | 130 ++++++++++-------- 2 files changed, 109 insertions(+), 83 deletions(-) diff --git a/lib/experimental/interactivity-api/blocks.php b/lib/experimental/interactivity-api/blocks.php index 3ad6d13d660fb..d9a2b1b2718a8 100644 --- a/lib/experimental/interactivity-api/blocks.php +++ b/lib/experimental/interactivity-api/blocks.php @@ -35,18 +35,18 @@ function gutenberg_block_core_file_add_directives_to_content( $block_content, $b * * * * @param string $block_content Markup of the navigation block. + * @param array $block Block object. * * @return string Navigation block markup with the proper directives */ -function gutenberg_block_core_navigation_add_directives_to_markup( $block_content ) { +function gutenberg_block_core_navigation_add_directives_to_markup( $block_content, $block ) { $w = new WP_HTML_Tag_Processor( $block_content ); // Add directives to the `