diff --git a/changelog.txt b/changelog.txt index 2f7d2f02ff629..65745b416e4bd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,20 @@ == Changelog == += 17.2.1 = + +## Changelog + +### Bug Fixes + +- Fix: Fatal php error if a template was created by an author that was deleted ([56990](https://github.com/WordPress/gutenberg/pull/56990)) + +## Contributors + +The following contributors merged PRs in this release: + +@jorgefilipecosta + + = 17.2.0 = diff --git a/gutenberg.php b/gutenberg.php index 7970dd5461fc4..f39a0cb519c8a 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.3 * Requires PHP: 7.0 - * Version: 17.2.0 + * Version: 17.2.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/compat/wordpress-6.5/rest-api.php b/lib/compat/wordpress-6.5/rest-api.php index 3b82815c41e42..12d789fb58b86 100644 --- a/lib/compat/wordpress-6.5/rest-api.php +++ b/lib/compat/wordpress-6.5/rest-api.php @@ -91,7 +91,11 @@ function _gutenberg_get_wp_templates_author_text_field( $template_object ) { case 'site': return get_bloginfo( 'name' ); case 'user': - return get_user_by( 'id', $template_object['author'] )->get( 'display_name' ); + $author = get_user_by( 'id', $template_object['author'] ); + if ( ! $author ) { + return __( 'Unknown author', 'gutenberg' ); + } + return $author->get( 'display_name' ); } } diff --git a/package-lock.json b/package-lock.json index ded852521693f..6d9382ec9fb2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.2.0", + "version": "17.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.2.0", + "version": "17.2.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 122a1368eaf1c..580c100354a96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.2.0", + "version": "17.2.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/block-editor/src/components/use-block-drop-zone/index.js b/packages/block-editor/src/components/use-block-drop-zone/index.js index 25dc6ee408982..cb3c3ae6a28a3 100644 --- a/packages/block-editor/src/components/use-block-drop-zone/index.js +++ b/packages/block-editor/src/components/use-block-drop-zone/index.js @@ -20,6 +20,10 @@ import { } from '../../utils/math'; import { store as blockEditorStore } from '../../store'; +const THRESHOLD_DISTANCE = 30; +const MINIMUM_HEIGHT_FOR_THRESHOLD = 120; +const MINIMUM_WIDTH_FOR_THRESHOLD = 120; + /** @typedef {import('../../utils/math').WPPoint} WPPoint */ /** @typedef {import('../use-on-block-drop/types').WPDropOperation} WPDropOperation */ @@ -48,24 +52,86 @@ import { store as blockEditorStore } from '../../store'; * @param {WPBlockData[]} blocksData The block data list. * @param {WPPoint} position The position of the item being dragged. * @param {WPBlockListOrientation} orientation The orientation of the block list. + * @param {Object} options Additional options. * @return {[number, WPDropOperation]} The drop target position. */ export function getDropTargetPosition( blocksData, position, - orientation = 'vertical' + orientation = 'vertical', + options = {} ) { const allowedEdges = orientation === 'horizontal' ? [ 'left', 'right' ] : [ 'top', 'bottom' ]; - const isRightToLeft = isRTL(); - let nearestIndex = 0; let insertPosition = 'before'; let minDistance = Infinity; + const { + dropZoneElement, + parentBlockOrientation, + rootBlockIndex = 0, + } = options; + + // Allow before/after when dragging over the top/bottom edges of the drop zone. + if ( dropZoneElement && parentBlockOrientation !== 'horizontal' ) { + const rect = dropZoneElement.getBoundingClientRect(); + const [ distance, edge ] = getDistanceToNearestEdge( position, rect, [ + 'top', + 'bottom', + ] ); + + // If dragging over the top or bottom of the drop zone, insert the block + // before or after the parent block. This only applies to blocks that use + // a drop zone element, typically container blocks such as Group or Cover. + if ( + rect.height > MINIMUM_HEIGHT_FOR_THRESHOLD && + distance < THRESHOLD_DISTANCE + ) { + if ( edge === 'top' ) { + return [ rootBlockIndex, 'before' ]; + } + if ( edge === 'bottom' ) { + return [ rootBlockIndex + 1, 'after' ]; + } + } + } + + const isRightToLeft = isRTL(); + + // Allow before/after when dragging over the left/right edges of the drop zone. + if ( dropZoneElement && parentBlockOrientation === 'horizontal' ) { + const rect = dropZoneElement.getBoundingClientRect(); + const [ distance, edge ] = getDistanceToNearestEdge( position, rect, [ + 'left', + 'right', + ] ); + + // If dragging over the left or right of the drop zone, insert the block + // before or after the parent block. This only applies to blocks that use + // a drop zone element, typically container blocks such as Group. + if ( + rect.width > MINIMUM_WIDTH_FOR_THRESHOLD && + distance < THRESHOLD_DISTANCE + ) { + if ( + ( isRightToLeft && edge === 'right' ) || + ( ! isRightToLeft && edge === 'left' ) + ) { + return [ rootBlockIndex, 'before' ]; + } + if ( + ( isRightToLeft && edge === 'left' ) || + ( ! isRightToLeft && edge === 'right' ) + ) { + return [ rootBlockIndex + 1, 'after' ]; + } + } + } + blocksData.forEach( ( { isUnmodifiedDefaultBlock, getBoundingClientRect, blockIndex } ) => { const rect = getBoundingClientRect(); @@ -150,19 +216,27 @@ export default function useBlockDropZone( { operation: 'insert', } ); - const isDisabled = useSelect( + const { isDisabled, parentBlockClientId, rootBlockIndex } = useSelect( ( select ) => { const { __unstableIsWithinBlockOverlay, __unstableHasActiveBlockOverlayActive, + getBlockIndex, + getBlockParents, getBlockEditingMode, } = select( blockEditorStore ); const blockEditingMode = getBlockEditingMode( targetRootClientId ); - return ( - blockEditingMode !== 'default' || - __unstableHasActiveBlockOverlayActive( targetRootClientId ) || - __unstableIsWithinBlockOverlay( targetRootClientId ) - ); + return { + parentBlockClientId: + getBlockParents( targetRootClientId, true )[ 0 ] || '', + rootBlockIndex: getBlockIndex( targetRootClientId ), + isDisabled: + blockEditingMode !== 'default' || + __unstableHasActiveBlockOverlayActive( + targetRootClientId + ) || + __unstableIsWithinBlockOverlay( targetRootClientId ), + }; }, [ targetRootClientId ] ); @@ -172,9 +246,15 @@ export default function useBlockDropZone( { const { showInsertionPoint, hideInsertionPoint } = useDispatch( blockEditorStore ); - const onBlockDrop = useOnBlockDrop( targetRootClientId, dropTarget.index, { - operation: dropTarget.operation, - } ); + const onBlockDrop = useOnBlockDrop( + dropTarget.operation === 'before' || dropTarget.operation === 'after' + ? parentBlockClientId + : targetRootClientId, + dropTarget.index, + { + operation: dropTarget.operation, + } + ); const throttled = useThrottle( useCallback( ( event, ownerDocument ) => { @@ -211,7 +291,16 @@ export default function useBlockDropZone( { const [ targetIndex, operation ] = getDropTargetPosition( blocksData, { x: event.clientX, y: event.clientY }, - getBlockListSettings( targetRootClientId )?.orientation + getBlockListSettings( targetRootClientId )?.orientation, + { + dropZoneElement, + parentBlockClientId, + parentBlockOrientation: parentBlockClientId + ? getBlockListSettings( parentBlockClientId ) + ?.orientation + : undefined, + rootBlockIndex, + } ); registry.batch( () => { @@ -219,18 +308,29 @@ export default function useBlockDropZone( { index: targetIndex, operation, } ); - showInsertionPoint( targetRootClientId, targetIndex, { + + const insertionPointClientId = [ + 'before', + 'after', + ].includes( operation ) + ? parentBlockClientId + : targetRootClientId; + + showInsertionPoint( insertionPointClientId, targetIndex, { operation, } ); } ); }, [ + dropZoneElement, getBlocks, targetRootClientId, getBlockListSettings, registry, showInsertionPoint, getBlockIndex, + parentBlockClientId, + rootBlockIndex, ] ), 200 diff --git a/packages/block-editor/src/components/use-on-block-drop/index.js b/packages/block-editor/src/components/use-on-block-drop/index.js index 72ea6a698c343..ab0da8ad99e2a 100644 --- a/packages/block-editor/src/components/use-on-block-drop/index.js +++ b/packages/block-editor/src/components/use-on-block-drop/index.js @@ -292,9 +292,10 @@ export default function useOnBlockDrop( operation, getBlockOrder, getBlocksByClientId, - insertBlocks, moveBlocksToPosition, + registry, removeBlocks, + replaceBlocks, targetBlockIndex, targetRootClientId, ] diff --git a/packages/block-library/src/form-input/deprecated.js b/packages/block-library/src/form-input/deprecated.js new file mode 100644 index 0000000000000..bb1fdf6e40204 --- /dev/null +++ b/packages/block-library/src/form-input/deprecated.js @@ -0,0 +1,142 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; +import removeAccents from 'remove-accents'; + +/** + * WordPress dependencies + */ +import { + RichText, + __experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, + __experimentalGetColorClassesAndStyles as getColorClassesAndStyles, +} from '@wordpress/block-editor'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; + +const getNameFromLabelV1 = ( content ) => { + return ( + removeAccents( stripHTML( content ) ) + // Convert anything that's not a letter or number to a hyphen. + .replace( /[^\p{L}\p{N}]+/gu, '-' ) + // Convert to lowercase + .toLowerCase() + // Remove any remaining leading or trailing hyphens. + .replace( /(^-+)|(-+$)/g, '' ) + ); +}; + +// Version without wrapper div in saved markup +// See: https://github.com/WordPress/gutenberg/pull/56507 +const v1 = { + attributes: { + type: { + type: 'string', + default: 'text', + }, + name: { + type: 'string', + }, + label: { + type: 'string', + default: 'Label', + selector: '.wp-block-form-input__label-content', + source: 'html', + __experimentalRole: 'content', + }, + inlineLabel: { + type: 'boolean', + default: false, + }, + required: { + type: 'boolean', + default: false, + selector: '.wp-block-form-input__input', + source: 'attribute', + attribute: 'required', + }, + placeholder: { + type: 'string', + selector: '.wp-block-form-input__input', + source: 'attribute', + attribute: 'placeholder', + __experimentalRole: 'content', + }, + value: { + type: 'string', + default: '', + selector: 'input', + source: 'attribute', + attribute: 'value', + }, + visibilityPermissions: { + type: 'string', + default: 'all', + }, + }, + supports: { + className: false, + anchor: true, + reusable: false, + spacing: { + margin: [ 'top', 'bottom' ], + }, + __experimentalBorder: { + radius: true, + __experimentalSkipSerialization: true, + __experimentalDefaultControls: { + radius: true, + }, + }, + }, + save( { attributes } ) { + const { type, name, label, inlineLabel, required, placeholder, value } = + attributes; + + const borderProps = getBorderClassesAndStyles( attributes ); + const colorProps = getColorClassesAndStyles( attributes ); + + const inputStyle = { + ...borderProps.style, + ...colorProps.style, + }; + + const inputClasses = classNames( + 'wp-block-form-input__input', + colorProps.className, + borderProps.className + ); + const TagName = type === 'textarea' ? 'textarea' : 'input'; + + if ( 'hidden' === type ) { + return ; + } + + /* eslint-disable jsx-a11y/label-has-associated-control */ + return ( + + ); + /* eslint-enable jsx-a11y/label-has-associated-control */ + }, +}; + +const deprecated = [ v1 ]; + +export default deprecated; diff --git a/packages/block-library/src/form-input/edit.js b/packages/block-library/src/form-input/edit.js index 0742c22c22f42..0b34c70fbad2d 100644 --- a/packages/block-library/src/form-input/edit.js +++ b/packages/block-library/src/form-input/edit.js @@ -59,7 +59,7 @@ function InputFieldBlock( { attributes, setAttributes, className } ) { ) } - + ; } - /* eslint-disable jsx-a11y/label-has-associated-control */ return ( - +
+ { /* eslint-disable jsx-a11y/label-has-associated-control */ } + + { /* eslint-enable jsx-a11y/label-has-associated-control */ } +
); - /* eslint-enable jsx-a11y/label-has-associated-control */ } diff --git a/packages/block-library/src/group/edit.js b/packages/block-library/src/group/edit.js index 9c8690c4e0e8e..a763bc95e60d7 100644 --- a/packages/block-library/src/group/edit.js +++ b/packages/block-library/src/group/edit.js @@ -10,6 +10,7 @@ import { store as blockEditorStore, } from '@wordpress/block-editor'; import { SelectControl } from '@wordpress/components'; +import { useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { View } from '@wordpress/primitives'; @@ -97,7 +98,8 @@ function GroupEdit( { attributes, name, setAttributes, clientId } ) { themeSupportsLayout || type === 'flex' || type === 'grid'; // Hooks. - const blockProps = useBlockProps(); + const ref = useRef(); + const blockProps = useBlockProps( { ref } ); const [ showPlaceholder, setShowPlaceholder ] = useShouldShowPlaceHolder( { attributes, @@ -124,6 +126,7 @@ function GroupEdit( { attributes, name, setAttributes, clientId } ) { ? blockProps : { className: 'wp-block-group__inner-container' }, { + dropZoneElement: ref.current, templateLock, allowedBlocks, renderAppender, diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index 3a0f4f0688ac9..ac766f69dd846 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -49,6 +49,48 @@ function hasDropCapDisabled( align ) { return align === ( isRTL() ? 'left' : 'right' ) || align === 'center'; } +function DropCapControl( { clientId, attributes, setAttributes } ) { + // Please do no add a useSelect call to the paragraph block unconditionaly. + // Every useSelect added to a (frequestly used) block will degrade the load + // and type bit. By moving it within InspectorControls, the subscription is + // now only added for the selected block(s). + const [ isDropCapFeatureEnabled ] = useSettings( 'typography.dropCap' ); + + if ( ! isDropCapFeatureEnabled ) { + return null; + } + + const { align, dropCap } = attributes; + + let helpText; + if ( hasDropCapDisabled( align ) ) { + helpText = __( 'Not available for aligned text.' ); + } else if ( dropCap ) { + helpText = __( 'Showing large initial letter.' ); + } else { + helpText = __( 'Toggle to show a large initial letter.' ); + } + + return ( + !! dropCap } + label={ __( 'Drop cap' ) } + onDeselect={ () => setAttributes( { dropCap: undefined } ) } + resetAllFilter={ () => ( { dropCap: undefined } ) } + panelId={ clientId } + > + setAttributes( { dropCap: ! dropCap } ) } + help={ helpText } + disabled={ hasDropCapDisabled( align ) ? true : false } + /> + + ); +} + function ParagraphBlock( { attributes, mergeBlocks, @@ -58,7 +100,6 @@ function ParagraphBlock( { clientId, } ) { const { align, content, direction, dropCap, placeholder } = attributes; - const [ isDropCapFeatureEnabled ] = useSettings( 'typography.dropCap' ); const blockProps = useBlockProps( { ref: useOnEnter( { clientId, content } ), className: classnames( { @@ -68,15 +109,6 @@ function ParagraphBlock( { style: { direction }, } ); - let helpText; - if ( hasDropCapDisabled( align ) ) { - helpText = __( 'Not available for aligned text.' ); - } else if ( dropCap ) { - helpText = __( 'Showing large initial letter.' ); - } else { - helpText = __( 'Toggle to show a large initial letter.' ); - } - return ( <> @@ -98,32 +130,13 @@ function ParagraphBlock( { } /> - { isDropCapFeatureEnabled && ( - - !! dropCap } - label={ __( 'Drop cap' ) } - onDeselect={ () => - setAttributes( { dropCap: undefined } ) - } - resetAllFilter={ () => ( { dropCap: undefined } ) } - panelId={ clientId } - > - - setAttributes( { dropCap: ! dropCap } ) - } - help={ helpText } - disabled={ - hasDropCapDisabled( align ) ? true : false - } - /> - - - ) } + + + { event.preventDefault(); - if ( - sortedDirection === direction - ) { - onChangeView( { - ...view, - sort: undefined, - } ); - } else { - onChangeView( { - ...view, - sort: { - field: field.id, - direction, - }, - } ); - } + onChangeView( { + ...view, + sort: { + field: field.id, + direction, + }, + } ); } } > { info.label } diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index e34d99008657b..3f5891f076791 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -118,24 +118,13 @@ function HeaderMenu( { field, view, onChangeView } ) { } onSelect={ ( event ) => { event.preventDefault(); - if ( - isSorted && - view.sort.direction === - direction - ) { - onChangeView( { - ...view, - sort: undefined, - } ); - } else { - onChangeView( { - ...view, - sort: { - field: field.id, - direction, - }, - } ); - } + onChangeView( { + ...view, + sort: { + field: field.id, + direction, + }, + } ); } } > { info.label } diff --git a/packages/e2e-tests/specs/editor/various/change-detection.test.js b/packages/e2e-tests/specs/editor/various/change-detection.test.js index 62057c4cbb2bc..0eb673671222f 100644 --- a/packages/e2e-tests/specs/editor/various/change-detection.test.js +++ b/packages/e2e-tests/specs/editor/various/change-detection.test.js @@ -370,7 +370,11 @@ describe( 'Change detection', () => { it( 'consecutive edits to the same attribute should mark the post as dirty after a save', async () => { // Open the sidebar block settings. await openDocumentSettingsSidebar(); - await page.click( '.edit-post-sidebar__panel-tab[data-label="Block"]' ); + + const blockInspectorTab = await page.waitForXPath( + '//button[@role="tab"][contains(text(), "Block")]' + ); + await blockInspectorTab.click(); // Insert a paragraph. await clickBlockAppender(); diff --git a/packages/e2e-tests/specs/editor/various/editor-modes.test.js b/packages/e2e-tests/specs/editor/various/editor-modes.test.js index 81878ebf7208e..aea6536f605bb 100644 --- a/packages/e2e-tests/specs/editor/various/editor-modes.test.js +++ b/packages/e2e-tests/specs/editor/various/editor-modes.test.js @@ -102,21 +102,24 @@ describe( 'Editing modes (visual/HTML)', () => { expect( title ).toBe( 'Paragraph' ); // The Block inspector should be active. - let blockInspectorTab = await page.$( - '.edit-post-sidebar__panel-tab.is-active[data-label="Block"]' + let [ blockInspectorTab ] = await page.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Block")]' ); expect( blockInspectorTab ).not.toBeNull(); await switchEditorModeTo( 'Code' ); // The Block inspector should not be active anymore. - blockInspectorTab = await page.$( - '.edit-post-sidebar__panel-tab.is-active[data-label="Block"]' + [ blockInspectorTab ] = await page.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Block")]' ); - expect( blockInspectorTab ).toBeNull(); + expect( blockInspectorTab ).toBeUndefined(); // No block is selected. - await page.click( '.edit-post-sidebar__panel-tab[data-label="Block"]' ); + const inactiveBlockInspectorTab = await page.waitForXPath( + '//button[@role="tab"][contains(text(), "Block")]' + ); + inactiveBlockInspectorTab.click(); const noBlocksElement = await page.$( '.block-editor-block-inspector__no-blocks' ); diff --git a/packages/e2e-tests/specs/editor/various/preferences.test.js b/packages/e2e-tests/specs/editor/various/preferences.test.js index 98249637c7e96..54990a4004422 100644 --- a/packages/e2e-tests/specs/editor/various/preferences.test.js +++ b/packages/e2e-tests/specs/editor/various/preferences.test.js @@ -17,7 +17,7 @@ describe( 'preferences', () => { async function getActiveSidebarTabText() { try { return await page.$eval( - '.edit-post-sidebar__panel-tab.is-active', + 'div[aria-label="Editor settings"] [role="tab"][aria-selected="true"]', ( node ) => node.textContent ); } catch ( error ) { @@ -29,11 +29,15 @@ describe( 'preferences', () => { } it( 'remembers sidebar dismissal between sessions', async () => { + const blockTab = await page.waitForXPath( + `//button[@role="tab"][contains(text(), 'Block')]` + ); + // Open by default. expect( await getActiveSidebarTabText() ).toBe( 'Post' ); // Change to "Block" tab. - await page.click( '.edit-post-sidebar__panel-tab[aria-label="Block"]' ); + await blockTab.click(); expect( await getActiveSidebarTabText() ).toBe( 'Block' ); // Regression test: Reload resets to document tab. @@ -46,7 +50,7 @@ describe( 'preferences', () => { // Dismiss. await page.click( - '.edit-post-sidebar__panel-tabs [aria-label="Close Settings"]' + 'div[aria-label="Editor settings"] div[role="tablist"] + button[aria-label="Close Settings"]' ); expect( await getActiveSidebarTabText() ).toBe( null ); diff --git a/packages/e2e-tests/specs/editor/various/sidebar.test.js b/packages/e2e-tests/specs/editor/various/sidebar.test.js index 2e5d46eec2f7a..0cd39093aabb8 100644 --- a/packages/e2e-tests/specs/editor/various/sidebar.test.js +++ b/packages/e2e-tests/specs/editor/various/sidebar.test.js @@ -13,7 +13,8 @@ import { } from '@wordpress/e2e-test-utils'; const SIDEBAR_SELECTOR = '.edit-post-sidebar'; -const ACTIVE_SIDEBAR_TAB_SELECTOR = '.edit-post-sidebar__panel-tab.is-active'; +const ACTIVE_SIDEBAR_TAB_SELECTOR = + 'div[aria-label="Editor settings"] [role="tab"][aria-selected="true"]'; const ACTIVE_SIDEBAR_BUTTON_TEXT = 'Post'; describe( 'Sidebar', () => { @@ -99,22 +100,24 @@ describe( 'Sidebar', () => { // Tab lands at first (presumed selected) option "Post". await page.keyboard.press( 'Tab' ); - const isActiveDocumentTab = await page.evaluate( - () => - document.activeElement.textContent === 'Post' && - document.activeElement.classList.contains( 'is-active' ) + + // The Post tab should be focused and selected. + const [ documentInspectorTab ] = await page.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Post")]' ); - expect( isActiveDocumentTab ).toBe( true ); + expect( documentInspectorTab ).toBeDefined(); + expect( documentInspectorTab ).toHaveFocus(); - // Tab into and activate "Block". - await page.keyboard.press( 'Tab' ); + // Arrow key into and activate "Block". + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.press( 'Space' ); - const isActiveBlockTab = await page.evaluate( - () => - document.activeElement.textContent === 'Block' && - document.activeElement.classList.contains( 'is-active' ) + + // The Block tab should be focused and selected. + const [ blockInspectorTab ] = await page.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Block")]' ); - expect( isActiveBlockTab ).toBe( true ); + expect( blockInspectorTab ).toBeDefined(); + expect( blockInspectorTab ).toHaveFocus(); } ); it( 'should be possible to programmatically remove Document Settings panels', async () => { diff --git a/packages/edit-post/src/components/sidebar/settings-header/index.js b/packages/edit-post/src/components/sidebar/settings-header/index.js index ef32450e7209f..368bd3e9e50db 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/index.js +++ b/packages/edit-post/src/components/sidebar/settings-header/index.js @@ -1,22 +1,20 @@ /** * WordPress dependencies */ -import { Button } from '@wordpress/components'; -import { __, _x, sprintf } from '@wordpress/i18n'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { __, _x } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ -import { store as editPostStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; +import { sidebars } from '../settings-sidebar'; -const SettingsHeader = ( { sidebarName } ) => { - const { openGeneralSidebar } = useDispatch( editPostStore ); - const openDocumentSettings = () => - openGeneralSidebar( 'edit-post/document' ); - const openBlockSettings = () => openGeneralSidebar( 'edit-post/block' ); +const { Tabs } = unlock( componentsPrivateApis ); +const SettingsHeader = () => { const { documentLabel, isTemplateMode } = useSelect( ( select ) => { const { getPostTypeLabel, getRenderingMode } = select( editorStore ); @@ -27,66 +25,16 @@ const SettingsHeader = ( { sidebarName } ) => { }; }, [] ); - const [ documentAriaLabel, documentActiveClass ] = - sidebarName === 'edit-post/document' - ? // translators: ARIA label for the Document sidebar tab, selected. %s: Document label. - [ sprintf( __( '%s (selected)' ), documentLabel ), 'is-active' ] - : [ documentLabel, '' ]; - - const [ blockAriaLabel, blockActiveClass ] = - sidebarName === 'edit-post/block' - ? // translators: ARIA label for the Block Settings Sidebar tab, selected. - [ __( 'Block (selected)' ), 'is-active' ] - : // translators: ARIA label for the Block Settings Sidebar tab, not selected. - [ __( 'Block' ), '' ]; - - const [ templateAriaLabel, templateActiveClass ] = - sidebarName === 'edit-post/document' - ? [ __( 'Template (selected)' ), 'is-active' ] - : [ __( 'Template' ), '' ]; - - /* Use a list so screen readers will announce how many tabs there are. */ return ( -
    - { ! isTemplateMode && ( -
  • - -
  • - ) } - { isTemplateMode && ( -
  • - -
  • - ) } -
  • - -
  • -
+ + + { isTemplateMode ? __( 'Template' ) : documentLabel } + + + { /* translators: Text label for the Block Settings Sidebar tab. */ } + { __( 'Block' ) } + + ); }; diff --git a/packages/edit-post/src/components/sidebar/settings-header/style.scss b/packages/edit-post/src/components/sidebar/settings-header/style.scss deleted file mode 100644 index aaf7698cb6ddb..0000000000000 --- a/packages/edit-post/src/components/sidebar/settings-header/style.scss +++ /dev/null @@ -1,74 +0,0 @@ -// This tab style CSS is duplicated verbatim in -// /packages/components/src/tab-panel/style.scss -.components-button.edit-post-sidebar__panel-tab { - position: relative; - border-radius: 0; - height: $grid-unit-60; - background: transparent; - border: none; - box-shadow: none; - cursor: pointer; - padding: 3px $grid-unit-20; // Use padding to offset the is-active border, this benefits Windows High Contrast mode - margin-left: 0; - font-weight: 500; - - &:focus:not(:disabled) { - position: relative; - box-shadow: none; - outline: none; - } - - // Tab indicator - &::after { - content: ""; - position: absolute; - right: 0; - bottom: 0; - left: 0; - pointer-events: none; - - // Draw the indicator. - background: var(--wp-admin-theme-color); - height: calc(0 * var(--wp-admin-border-width-focus)); - border-radius: 0; - - // Animation - transition: all 0.1s linear; - @include reduce-motion("transition"); - } - - // Active. - &.is-active::after { - height: calc(1 * var(--wp-admin-border-width-focus)); - - // Windows high contrast mode. - outline: 2px solid transparent; - outline-offset: -1px; - } - - // Focus. - &::before { - content: ""; - position: absolute; - top: $grid-unit-15; - right: $grid-unit-15; - bottom: $grid-unit-15; - left: $grid-unit-15; - pointer-events: none; - - // Draw the indicator. - box-shadow: 0 0 0 0 transparent; - border-radius: $radius-block-ui; - - // Animation - transition: all 0.1s linear; - @include reduce-motion("transition"); - } - - &:focus-visible::before { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - - // Windows high contrast mode. - outline: 2px solid transparent; - } -} diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index e566ea400c12b..9fa27c6ac2ade 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -5,8 +5,8 @@ import { BlockInspector, store as blockEditorStore, } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; -import { Platform } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { Platform, useCallback, useContext } from '@wordpress/element'; import { isRTL, __ } from '@wordpress/i18n'; import { drawerLeft, drawerRight } from '@wordpress/icons'; import { store as interfaceStore } from '@wordpress/interface'; @@ -29,54 +29,43 @@ import PluginDocumentSettingPanel from '../plugin-document-setting-panel'; import PluginSidebarEditPost from '../plugin-sidebar'; import TemplateSummary from '../template-summary'; import { store as editPostStore } from '../../../store'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { unlock } from '../../../lock-unlock'; + +const { Tabs } = unlock( componentsPrivateApis ); const SIDEBAR_ACTIVE_BY_DEFAULT = Platform.select( { web: true, native: false, } ); +export const sidebars = { + document: 'edit-post/document', + block: 'edit-post/block', +}; -const SettingsSidebar = () => { - const { sidebarName, keyboardShortcut, isTemplateMode } = useSelect( - ( select ) => { - // The settings sidebar is used by the edit-post/document and edit-post/block sidebars. - // sidebarName represents the sidebar that is active or that should be active when the SettingsSidebar toggle button is pressed. - // If one of the two sidebars is active the component will contain the content of that sidebar. - // When neither of the two sidebars is active we can not simply return null, because the PluginSidebarEditPost - // component, besides being used to render the sidebar, also renders the toggle button. In that case sidebarName - // should contain the sidebar that will be active when the toggle button is pressed. If a block - // is selected, that should be edit-post/block otherwise it's edit-post/document. - let sidebar = select( interfaceStore ).getActiveComplementaryArea( - editPostStore.name - ); - if ( - ! [ 'edit-post/document', 'edit-post/block' ].includes( - sidebar - ) - ) { - if ( select( blockEditorStore ).getBlockSelectionStart() ) { - sidebar = 'edit-post/block'; - } - sidebar = 'edit-post/document'; - } - const shortcut = select( - keyboardShortcutsStore - ).getShortcutRepresentation( 'core/edit-post/toggle-sidebar' ); - return { - sidebarName: sidebar, - keyboardShortcut: shortcut, - isTemplateMode: - select( editorStore ).getRenderingMode() === - 'template-only', - }; - }, - [] - ); +const SidebarContent = ( { + sidebarName, + keyboardShortcut, + isTemplateMode, +} ) => { + // Because `PluginSidebarEditPost` renders a `ComplementaryArea`, we + // need to forward the `Tabs` context so it can be passed through the + // underlying slot/fill. + const tabsContextValue = useContext( Tabs.Context ); return ( } + header={ + + + + } closeLabel={ __( 'Close Settings' ) } + // This classname is added so we can apply a corrective negative + // margin to the panel. + // see https://github.com/WordPress/gutenberg/pull/55360#pullrequestreview-1737671049 + className="edit-post-sidebar__panel" headerClassName="edit-post-sidebar__panel-tabs" /* translators: button label text should, if possible, be under 16 characters. */ title={ __( 'Settings' ) } @@ -84,25 +73,96 @@ const SettingsSidebar = () => { icon={ isRTL() ? drawerLeft : drawerRight } isActiveByDefault={ SIDEBAR_ACTIVE_BY_DEFAULT } > - { ! isTemplateMode && sidebarName === 'edit-post/document' && ( - <> - - - - - - - - - - - ) } - { isTemplateMode && sidebarName === 'edit-post/document' && ( - - ) } - { sidebarName === 'edit-post/block' && } + + + { ! isTemplateMode && ( + <> + + + + + + + + + + + ) } + { isTemplateMode && } + + + + + ); }; +const SettingsSidebar = () => { + const { + sidebarName, + isSettingsSidebarActive, + keyboardShortcut, + isTemplateMode, + } = useSelect( ( select ) => { + // The settings sidebar is used by the edit-post/document and edit-post/block sidebars. + // sidebarName represents the sidebar that is active or that should be active when the SettingsSidebar toggle button is pressed. + // If one of the two sidebars is active the component will contain the content of that sidebar. + // When neither of the two sidebars is active we can not simply return null, because the PluginSidebarEditPost + // component, besides being used to render the sidebar, also renders the toggle button. In that case sidebarName + // should contain the sidebar that will be active when the toggle button is pressed. If a block + // is selected, that should be edit-post/block otherwise it's edit-post/document. + let sidebar = select( interfaceStore ).getActiveComplementaryArea( + editPostStore.name + ); + let isSettingsSidebar = true; + if ( ! [ sidebars.document, sidebars.block ].includes( sidebar ) ) { + isSettingsSidebar = false; + if ( select( blockEditorStore ).getBlockSelectionStart() ) { + sidebar = sidebars.block; + } + sidebar = sidebars.document; + } + const shortcut = select( + keyboardShortcutsStore + ).getShortcutRepresentation( 'core/edit-post/toggle-sidebar' ); + return { + sidebarName: sidebar, + isSettingsSidebarActive: isSettingsSidebar, + keyboardShortcut: shortcut, + isTemplateMode: + select( editorStore ).getRenderingMode() === 'template-only', + }; + }, [] ); + + const { openGeneralSidebar } = useDispatch( editPostStore ); + + const onTabSelect = useCallback( + ( newSelectedTabId ) => { + if ( !! newSelectedTabId ) { + openGeneralSidebar( newSelectedTabId ); + } + }, + [ openGeneralSidebar ] + ); + + return ( + + + + ); +}; + export default SettingsSidebar; diff --git a/packages/edit-post/src/components/sidebar/style.scss b/packages/edit-post/src/components/sidebar/style.scss index 7b10eaec0d224..1921c5cfd7b31 100644 --- a/packages/edit-post/src/components/sidebar/style.scss +++ b/packages/edit-post/src/components/sidebar/style.scss @@ -1,20 +1,8 @@ .components-panel__header.edit-post-sidebar__panel-tabs { - justify-content: flex-start; padding-left: 0; padding-right: $grid-unit-20; - border-top: 0; - margin-top: 0; - - ul { - display: flex; - } - li { - margin: 0; - } .components-button.has-icon { - display: none; - margin: 0 0 0 auto; padding: 0; min-width: $icon-size; height: $icon-size; @@ -24,3 +12,7 @@ } } } + +.edit-post-sidebar__panel { + margin-top: -1px; +} diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index 53219bc6a3736..88916bf70f76d 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -12,7 +12,6 @@ @import "./components/sidebar/post-format/style.scss"; @import "./components/sidebar/post-slug/style.scss"; @import "./components/sidebar/post-visibility/style.scss"; -@import "./components/sidebar/settings-header/style.scss"; @import "./components/sidebar/template-summary/style.scss"; @import "./components/text-editor/style.scss"; @import "./components/visual-editor/style.scss"; diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/get-revision-changes.js b/packages/edit-site/src/components/global-styles/screen-revisions/get-revision-changes.js new file mode 100644 index 0000000000000..fed075eb923ff --- /dev/null +++ b/packages/edit-site/src/components/global-styles/screen-revisions/get-revision-changes.js @@ -0,0 +1,171 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +const globalStylesChangesCache = new Map(); +const EMPTY_ARRAY = []; + +const translationMap = { + caption: __( 'Caption' ), + link: __( 'Link' ), + button: __( 'Button' ), + heading: __( 'Heading' ), + 'settings.color': __( 'Color settings' ), + 'settings.typography': __( 'Typography settings' ), + 'styles.color': __( 'Colors' ), + 'styles.spacing': __( 'Spacing' ), + 'styles.typography': __( 'Typography' ), +}; + +const isObject = ( obj ) => obj !== null && typeof obj === 'object'; + +/** + * Get the translation for a given global styles key. + * @param {string} key A key representing a path to a global style property or setting. + * @param {Record} blockNames A key/value pair object of block names and their rendered titles. + * @return {string|undefined} A translated key or undefined if no translation exists. + */ +function getTranslation( key, blockNames ) { + if ( translationMap[ key ] ) { + return translationMap[ key ]; + } + + const keyArray = key.split( '.' ); + + if ( keyArray?.[ 0 ] === 'blocks' ) { + const blockName = blockNames[ keyArray[ 1 ] ]; + return blockName + ? sprintf( + // translators: %s: block name. + __( '%s block' ), + blockName + ) + : keyArray[ 1 ]; + } + + if ( keyArray?.[ 0 ] === 'elements' ) { + return sprintf( + // translators: %s: element name, e.g., heading button, link, caption. + __( '%s element' ), + translationMap[ keyArray[ 1 ] ] + ); + } + + return undefined; +} + +/** + * A deep comparison of two objects, optimized for comparing global styles. + * @param {Object} changedObject The changed object to compare. + * @param {Object} originalObject The original object to compare against. + * @param {string} parentPath A key/value pair object of block names and their rendered titles. + * @return {string[]} An array of paths whose values have changed. + */ +function deepCompare( changedObject, originalObject, parentPath = '' ) { + // We have two non-object values to compare. + if ( ! isObject( changedObject ) && ! isObject( originalObject ) ) { + /* + * Only return a path if the value has changed. + * And then only the path name up to 2 levels deep. + */ + return changedObject !== originalObject + ? parentPath.split( '.' ).slice( 0, 2 ).join( '.' ) + : undefined; + } + + // Enable comparison when an object doesn't have a corresponding property to compare. + changedObject = isObject( changedObject ) ? changedObject : {}; + originalObject = isObject( originalObject ) ? originalObject : {}; + + const allKeys = new Set( [ + ...Object.keys( changedObject ), + ...Object.keys( originalObject ), + ] ); + + let diffs = []; + for ( const key of allKeys ) { + const path = parentPath ? parentPath + '.' + key : key; + const changedPath = deepCompare( + changedObject[ key ], + originalObject[ key ], + path + ); + if ( changedPath ) { + diffs = diffs.concat( changedPath ); + } + } + return diffs; +} + +/** + * Get an array of translated summarized global styles changes. + * Results are cached using a Map() key of `JSON.stringify( { revision, previousRevision } )`. + * + * @param {Object} revision The changed object to compare. + * @param {Object} previousRevision The original object to compare against. + * @param {Record} blockNames A key/value pair object of block names and their rendered titles. + * @return {string[]} An array of translated changes. + */ +export default function getRevisionChanges( + revision, + previousRevision, + blockNames +) { + const cacheKey = JSON.stringify( { revision, previousRevision } ); + + if ( globalStylesChangesCache.has( cacheKey ) ) { + return globalStylesChangesCache.get( cacheKey ); + } + + /* + * Compare the two revisions with normalized keys. + * The order of these keys determines the order in which + * they'll appear in the results. + */ + const changedValueTree = deepCompare( + { + styles: { + color: revision?.styles?.color, + typography: revision?.styles?.typography, + spacing: revision?.styles?.spacing, + }, + blocks: revision?.styles?.blocks, + elements: revision?.styles?.elements, + settings: revision?.settings, + }, + { + styles: { + color: previousRevision?.styles?.color, + typography: previousRevision?.styles?.typography, + spacing: previousRevision?.styles?.spacing, + }, + blocks: previousRevision?.styles?.blocks, + elements: previousRevision?.styles?.elements, + settings: previousRevision?.settings, + } + ); + + if ( ! changedValueTree.length ) { + globalStylesChangesCache.set( cacheKey, EMPTY_ARRAY ); + return EMPTY_ARRAY; + } + + // Remove duplicate results. + const result = [ ...new Set( changedValueTree ) ] + /* + * Translate the keys. + * Remove duplicate or empty translations. + */ + .reduce( ( acc, curr ) => { + const translation = getTranslation( curr, blockNames ); + if ( translation && ! acc.includes( translation ) ) { + acc.push( translation ); + } + return acc; + }, [] ); + + globalStylesChangesCache.set( cacheKey, result ); + + return result; +} diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index 90bf68e579cb7..aa380c5a9fbd0 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -7,7 +7,6 @@ import { __experimentalUseNavigator as useNavigator, __experimentalConfirmDialog as ConfirmDialog, Spinner, - __experimentalSpacer as Spacer, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; @@ -135,7 +134,8 @@ function ScreenRevisions() { } }, [ shouldSelectFirstItem, firstRevision ] ); - // Only display load button if there is a revision to load and it is different from the current editor styles. + // Only display load button if there is a revision to load, + // and it is different from the current editor styles. const isLoadButtonEnabled = !! currentlySelectedRevisionId && ! selectedRevisionMatchesEditorStyles; const shouldShowRevisions = ! isLoading && revisions.length; @@ -156,7 +156,7 @@ function ScreenRevisions() { { isLoading && ( ) } - { shouldShowRevisions ? ( + { shouldShowRevisions && ( <> { isLoadButtonEnabled && ( @@ -215,14 +216,6 @@ function ScreenRevisions() { ) } - ) : ( - - { - // Adding an existing translation here in case these changes are shipped to WordPress 6.3. - // Later we could update to something better, e.g., "There are currently no style revisions.". - __( 'No results found.' ) - } - ) } ); diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js index 2786bf6d79121..0893006942572 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js @@ -6,28 +6,69 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { __, sprintf } from '@wordpress/i18n'; +import { __, _n, sprintf } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import { dateI18n, getDate, humanTimeDiff, getSettings } from '@wordpress/date'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; +import { getBlockTypes } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import getRevisionChanges from './get-revision-changes'; const DAY_IN_MILLISECONDS = 60 * 60 * 1000 * 24; +const MAX_CHANGES = 7; + +function ChangesSummary( { revision, previousRevision, blockNames } ) { + const changes = getRevisionChanges( + revision, + previousRevision, + blockNames + ); + const changesLength = changes.length; + + if ( ! changesLength ) { + return null; + } + + // Truncate to `n` results if necessary. + if ( changesLength > MAX_CHANGES ) { + const deleteCount = changesLength - MAX_CHANGES; + const andMoreText = sprintf( + // translators: %d: number of global styles changes that are not displayed in the UI. + _n( '…and %d more change.', '…and %d more changes.', deleteCount ), + deleteCount + ); + changes.splice( MAX_CHANGES, deleteCount, andMoreText ); + } + + return ( + + { changes.join( ', ' ) } + + ); +} /** * Returns a button label for the revision. * * @param {string|number} id A revision object. - * @param {boolean} isLatest Whether the revision is the most current. * @param {string} authorDisplayName Author name. * @param {string} formattedModifiedDate Revision modified date formatted. + * @param {boolean} areStylesEqual Whether the revision matches the current editor styles. * @return {string} Translated label. */ function getRevisionLabel( id, - isLatest, authorDisplayName, - formattedModifiedDate + formattedModifiedDate, + areStylesEqual ) { if ( 'parent' === id ) { return __( 'Reset the styles to the theme defaults' ); @@ -35,21 +76,23 @@ function getRevisionLabel( if ( 'unsaved' === id ) { return sprintf( - /* translators: %s author display name */ + /* translators: %s: author display name */ __( 'Unsaved changes by %s' ), authorDisplayName ); } - return isLatest + return areStylesEqual ? sprintf( - /* translators: %1$s author display name, %2$s: revision creation date */ - __( 'Changes saved by %1$s on %2$s (current)' ), + // translators: %1$s: author display name, %2$s: revision creation date. + __( + 'Changes saved by %1$s on %2$s. This revision matches current editor styles.' + ), authorDisplayName, formattedModifiedDate ) : sprintf( - /* translators: %1$s author display name, %2$s: revision creation date */ + // translators: %1$s: author display name, %2$s: revision creation date. __( 'Changes saved by %1$s on %2$s' ), authorDisplayName, formattedModifiedDate @@ -67,7 +110,12 @@ function getRevisionLabel( * @param {props} Component props. * @return {JSX.Element} The modal component. */ -function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { +function RevisionsButtons( { + userRevisions, + selectedRevisionId, + onChange, + canApplyRevision, +} ) { const { currentThemeName, currentUser } = useSelect( ( select ) => { const { getCurrentTheme, getCurrentUser } = select( coreStore ); const currentTheme = getCurrentTheme(); @@ -77,8 +125,15 @@ function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { currentUser: getCurrentUser(), }; }, [] ); + const blockNames = useMemo( () => { + const blockTypes = getBlockTypes(); + return blockTypes.reduce( ( accumulator, { name, title } ) => { + accumulator[ name ] = title; + return accumulator; + }, {} ); + }, [] ); const dateNowInMs = getDate().getTime(); - const { date: dateFormat, datetimeAbbreviated } = getSettings().formats; + const { datetimeAbbreviated } = getSettings().formats; return (
    { userRevisions.map( ( revision, index ) => { - const { id, isLatest, author, modified } = revision; + const { id, author, modified } = revision; const isUnsaved = 'unsaved' === id; // Unsaved changes are created by the current user. const revisionAuthor = isUnsaved ? currentUser : author; const authorDisplayName = revisionAuthor?.name || __( 'User' ); const authorAvatar = revisionAuthor?.avatar_urls?.[ '48' ]; + const isFirstItem = index === 0; const isSelected = selectedRevisionId ? selectedRevisionId === id - : index === 0; + : isFirstItem; + const areStylesEqual = ! canApplyRevision && isSelected; const isReset = 'parent' === id; const modifiedDate = getDate( modified ); const displayDate = modified && dateNowInMs - modifiedDate.getTime() > DAY_IN_MILLISECONDS - ? dateI18n( dateFormat, modifiedDate ) + ? dateI18n( datetimeAbbreviated, modifiedDate ) : humanTimeDiff( modified ); const revisionLabel = getRevisionLabel( id, - isLatest, authorDisplayName, - dateI18n( datetimeAbbreviated, modifiedDate ) + dateI18n( datetimeAbbreviated, modifiedDate ), + areStylesEqual ); return ( @@ -116,6 +173,7 @@ function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { 'edit-site-global-styles-screen-revisions__revision-item', { 'is-selected': isSelected, + 'is-active': areStylesEqual, 'is-reset': isReset, } ) } @@ -127,7 +185,7 @@ function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { onClick={ () => { onChange( revision ); } } - label={ revisionLabel } + aria-label={ revisionLabel } > { isReset ? ( @@ -150,6 +208,17 @@ function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { { displayDate } ) } + { isSelected && ( + + ) } { { + const revision = { + id: 10, + styles: { + typography: { + fontSize: 'var(--wp--preset--font-size--potato)', + fontStyle: 'normal', + fontWeight: '600', + lineHeight: '1.85', + fontFamily: 'var(--wp--preset--font-family--asparagus)', + }, + spacing: { + padding: { + top: '36px', + right: '89px', + bottom: '133px', + left: 'var(--wp--preset--spacing--20)', + }, + blockGap: '114px', + }, + elements: { + heading: { + typography: { + letterSpacing: '37px', + }, + }, + caption: { + color: { + text: 'var(--wp--preset--color--pineapple)', + }, + }, + }, + color: { + text: 'var(--wp--preset--color--tomato)', + }, + blocks: { + 'core/paragraph': { + color: { + text: '#000000', + }, + }, + }, + }, + settings: { + color: { + palette: { + theme: [ + { + slug: 'one', + color: 'pink', + }, + ], + }, + }, + }, + }; + const previousRevision = { + id: 9, + styles: { + typography: { + fontSize: 'var(--wp--preset--font-size--fungus)', + fontStyle: 'normal', + fontWeight: '600', + lineHeight: '1.85', + fontFamily: 'var(--wp--preset--font-family--grapes)', + }, + spacing: { + padding: { + top: '36px', + right: '89px', + bottom: '133px', + left: 'var(--wp--preset--spacing--20)', + }, + blockGap: '114px', + }, + elements: { + heading: { + typography: { + letterSpacing: '37px', + }, + }, + caption: { + typography: { + fontSize: '1.11rem', + fontStyle: 'normal', + fontWeight: '600', + }, + }, + link: { + typography: { + lineHeight: 2, + textDecoration: 'line-through', + }, + color: { + text: 'var(--wp--preset--color--egg)', + }, + }, + }, + color: { + text: 'var(--wp--preset--color--tomato)', + background: 'var(--wp--preset--color--pumpkin)', + }, + blocks: { + 'core/paragraph': { + color: { + text: '#fff', + }, + }, + }, + }, + settings: { + color: { + palette: { + theme: [ + { + slug: 'one', + color: 'blue', + }, + ], + }, + }, + }, + }; + const blockNames = { + 'core/paragraph': 'Paragraph', + }; + it( 'returns a list of changes and caches them', () => { + const resultA = getRevisionChanges( + revision, + previousRevision, + blockNames + ); + expect( resultA ).toEqual( [ + 'Colors', + 'Typography', + 'Paragraph block', + 'Caption element', + 'Link element', + 'Color settings', + ] ); + + const resultB = getRevisionChanges( + revision, + previousRevision, + blockNames + ); + + expect( resultA ).toBe( resultB ); + } ); + + it( 'skips unknown and unchanged keys', () => { + const result = getRevisionChanges( + { + styles: { + frogs: { + legs: 'green', + }, + typography: { + fontSize: '1rem', + }, + settings: { + '': { + '': 'foo', + }, + }, + }, + }, + { + styles: { + frogs: { + legs: 'yellow', + }, + typography: { + fontSize: '1rem', + }, + settings: { + '': { + '': 'bar', + }, + }, + }, + } + ); + expect( result ).toEqual( [] ); + } ); +} ); diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js index e51f9ba970c4c..07de0cb73ff44 100644 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -83,7 +83,7 @@ function TemplateTitle( { item } ) { const { isCustomized } = useAddedBy( item.type, item.id ); return ( - + + ); +} + function SidebarScreens() { useSyncPathWithURL(); return ( <> - + - - + + - - + + - - + + - - + + - - + + - + { window?.__experimentalAdminViews && ( - + } /> - + ) } - + - - + + - - + + - - + + - - + + - + ); } diff --git a/packages/edit-site/src/components/sidebar/style.scss b/packages/edit-site/src/components/sidebar/style.scss index 9a3644cc830d5..ef24b0d4b8cf6 100644 --- a/packages/edit-site/src/components/sidebar/style.scss +++ b/packages/edit-site/src/components/sidebar/style.scss @@ -1,14 +1,17 @@ .edit-site-sidebar__content { flex-grow: 1; overflow-y: auto; +} + +.edit-site-sidebar__screen-wrapper { + @include custom-scrollbars-on-hover(transparent, $gray-700); + scrollbar-gutter: stable; + display: flex; + flex-direction: column; + height: 100%; - .components-navigator-screen { - @include custom-scrollbars-on-hover(transparent, $gray-700); - scrollbar-gutter: stable; - display: flex; - flex-direction: column; - height: 100%; - } + // This matches the logo padding + padding: 0 $grid-unit-15; } .edit-site-sidebar__footer { @@ -17,8 +20,3 @@ margin: 0 $canvas-padding; padding: $canvas-padding 0; } - -.edit-site-sidebar__content > div { - // This matches the logo padding - padding: 0 $grid-unit-15; -} diff --git a/test/e2e/specs/editor/plugins/custom-post-types.spec.js b/test/e2e/specs/editor/plugins/custom-post-types.spec.js index 17a497f26cee0..01dde03650ef7 100644 --- a/test/e2e/specs/editor/plugins/custom-post-types.spec.js +++ b/test/e2e/specs/editor/plugins/custom-post-types.spec.js @@ -31,7 +31,7 @@ test.describe( 'Test Custom Post Types', () => { await editor.openDocumentSettingsSidebar(); await page .getByRole( 'region', { name: 'Editor settings' } ) - .getByRole( 'button', { + .getByRole( 'tab', { name: 'Hierarchical No Title', } ) .click(); diff --git a/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js b/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js index f0bfe5bff203f..a695b0a9ead67 100644 --- a/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js +++ b/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js @@ -127,7 +127,7 @@ test.describe( 'Navigating the block hierarchy', () => { await pageUtils.pressKeys( 'ctrl+`' ); // Navigate to the block settings sidebar and tweak the column count. - await pageUtils.pressKeys( 'Tab', { times: 5 } ); + await pageUtils.pressKeys( 'Tab', { times: 4 } ); await expect( page.getByRole( 'slider', { name: 'Columns' } ) ).toBeFocused(); diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js index 14a2fc653e387..6102f48749543 100644 --- a/test/e2e/specs/editor/various/footnotes.spec.js +++ b/test/e2e/specs/editor/various/footnotes.spec.js @@ -362,7 +362,7 @@ test.describe( 'Footnotes', () => { await editor.openDocumentSettingsSidebar(); await page .getByRole( 'region', { name: 'Editor settings' } ) - .getByRole( 'button', { name: 'Post' } ) + .getByRole( 'tab', { name: 'Post' } ) .click(); await page.locator( 'a:text("2 Revisions")' ).click(); await page.locator( '.revisions-controls .ui-slider-handle' ).focus(); @@ -440,7 +440,7 @@ test.describe( 'Footnotes', () => { await editor.openDocumentSettingsSidebar(); await page .getByRole( 'region', { name: 'Editor settings' } ) - .getByRole( 'button', { name: 'Post' } ) + .getByRole( 'tab', { name: 'Post' } ) .click(); // Visit the published post. diff --git a/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js index 080abe011206a..84536c88227ce 100644 --- a/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js +++ b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js @@ -75,9 +75,7 @@ test.describe( 'Order of block keyboard navigation', () => { ); await page.keyboard.press( 'Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( - 'Post (selected)' - ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Post' ); } ); test( 'allows tabbing in navigation mode if no block is selected (reverse)', async ( { @@ -151,7 +149,7 @@ test.describe( 'Order of block keyboard navigation', () => { ); await page.keyboard.press( 'Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Post' ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Block' ); await pageUtils.pressKeys( 'shift+Tab' ); await KeyboardNavigableBlocks.expectLabelToHaveFocus( @@ -233,7 +231,7 @@ class KeyboardNavigableBlocks { await expect( activeElement ).toHaveText( paragraphText ); await this.page.keyboard.press( 'Tab' ); - await this.expectLabelToHaveFocus( 'Post' ); + await this.expectLabelToHaveFocus( 'Block' ); // Need to shift+tab here to end back in the block. If not, we'll be in the next region and it will only require 4 region jumps instead of 5. await this.pageUtils.pressKeys( 'shift+Tab' ); diff --git a/test/e2e/specs/site-editor/new-templates-list.spec.js b/test/e2e/specs/site-editor/new-templates-list.spec.js index 8bb98b6ad5355..34daeb6d40f09 100644 --- a/test/e2e/specs/site-editor/new-templates-list.spec.js +++ b/test/e2e/specs/site-editor/new-templates-list.spec.js @@ -33,10 +33,7 @@ test.describe( 'Templates', () => { name: 'Template', includeHidden: true, } ) - .getByRole( 'heading', { - level: 3, - includeHidden: true, - } ) + .getByRole( 'link', { includeHidden: true } ) .first(); await expect( firstTitle ).toHaveText( 'Tag Archives' ); // Ascending by title. @@ -57,7 +54,7 @@ test.describe( 'Templates', () => { await page.keyboard.type( 'tag' ); const titles = page .getByRole( 'region', { name: 'Template' } ) - .getByRole( 'heading', { level: 3 } ); + .getByRole( 'link' ); await expect( titles ).toHaveCount( 1 ); await expect( titles.first() ).toHaveText( 'Tag Archives' ); await page.getByRole( 'button', { name: 'Reset filters' } ).click(); diff --git a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js index 2d51b5ac5014b..a27bb28adbb91 100644 --- a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js +++ b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js @@ -55,6 +55,11 @@ test.describe( 'Global styles revisions', () => { name: /^Changes saved by /, } ); + // Shows changes made in the revision. + await expect( + page.getByTestId( 'global-styles-revision-changes' ) + ).toHaveText( 'Colors' ); + // There should be 2 revisions not including the reset to theme defaults button. await expect( revisionButtons ).toHaveCount( currentRevisions.length + 1 diff --git a/test/integration/fixtures/blocks/core__form-input.html b/test/integration/fixtures/blocks/core__form-input.html index f30d44a2503d2..33f1fe88c2c6a 100644 --- a/test/integration/fixtures/blocks/core__form-input.html +++ b/test/integration/fixtures/blocks/core__form-input.html @@ -1,3 +1,3 @@ - +
    diff --git a/test/integration/fixtures/blocks/core__form-input.parsed.json b/test/integration/fixtures/blocks/core__form-input.parsed.json index f92379b595276..5470c653c403b 100644 --- a/test/integration/fixtures/blocks/core__form-input.parsed.json +++ b/test/integration/fixtures/blocks/core__form-input.parsed.json @@ -3,9 +3,9 @@ "blockName": "core/form-input", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\n", + "innerHTML": "\n
    \n", "innerContent": [ - "\n\n" + "\n
    \n" ] } ] diff --git a/test/integration/fixtures/blocks/core__form-input.serialized.html b/test/integration/fixtures/blocks/core__form-input.serialized.html index f30d44a2503d2..33f1fe88c2c6a 100644 --- a/test/integration/fixtures/blocks/core__form-input.serialized.html +++ b/test/integration/fixtures/blocks/core__form-input.serialized.html @@ -1,3 +1,3 @@ - +
    diff --git a/test/integration/fixtures/blocks/core__form-input__deprecated-v1.html b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.html new file mode 100644 index 0000000000000..08ea661838620 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.html @@ -0,0 +1,6 @@ + + + diff --git a/test/integration/fixtures/blocks/core__form-input__deprecated-v1.json b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.json new file mode 100644 index 0000000000000..fee4df284f115 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.json @@ -0,0 +1,15 @@ +[ + { + "name": "core/form-input", + "isValid": true, + "attributes": { + "type": "text", + "label": "Label", + "inlineLabel": false, + "required": false, + "value": "", + "visibilityPermissions": "all" + }, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__form-input__deprecated-v1.parsed.json b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.parsed.json new file mode 100644 index 0000000000000..645337cbfdb4a --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.parsed.json @@ -0,0 +1,11 @@ +[ + { + "blockName": "core/form-input", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n\n", + "innerContent": [ + "\n\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__form-input__deprecated-v1.serialized.html b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.serialized.html new file mode 100644 index 0000000000000..33f1fe88c2c6a --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.serialized.html @@ -0,0 +1,3 @@ + +
    + diff --git a/test/integration/fixtures/blocks/core__form.html b/test/integration/fixtures/blocks/core__form.html index f443172601a3b..825389eb75ecd 100644 --- a/test/integration/fixtures/blocks/core__form.html +++ b/test/integration/fixtures/blocks/core__form.html @@ -1,19 +1,19 @@
    - - + +
    - - + +
    - - + +
    - - + +
    diff --git a/test/integration/fixtures/blocks/core__form.parsed.json b/test/integration/fixtures/blocks/core__form.parsed.json index fb2daff4a4741..e33849b5be504 100644 --- a/test/integration/fixtures/blocks/core__form.parsed.json +++ b/test/integration/fixtures/blocks/core__form.parsed.json @@ -5,52 +5,44 @@ "innerBlocks": [ { "blockName": "core/form-input", - "attrs": { - "label": "Name", - "required": true - }, + "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\n", + "innerHTML": "\n
    \n", "innerContent": [ - "\n\n" + "\n
    \n" ] }, { "blockName": "core/form-input", "attrs": { - "type": "email", - "label": "Email", - "required": true + "type": "email" }, "innerBlocks": [], - "innerHTML": "\n\n", + "innerHTML": "\n
    \n", "innerContent": [ - "\n\n" + "\n
    \n" ] }, { "blockName": "core/form-input", "attrs": { - "type": "url", - "label": "Website" + "type": "url" }, "innerBlocks": [], - "innerHTML": "\n\n", + "innerHTML": "\n
    \n", "innerContent": [ - "\n\n" + "\n
    \n" ] }, { "blockName": "core/form-input", "attrs": { - "type": "textarea", - "label": "Comment", - "required": true + "type": "textarea" }, "innerBlocks": [], - "innerHTML": "\n\n", + "innerHTML": "\n
    \n", "innerContent": [ - "\n\n" + "\n
    \n" ] }, { diff --git a/test/integration/fixtures/blocks/core__form.serialized.html b/test/integration/fixtures/blocks/core__form.serialized.html index 9bfff780f50db..e3209102ce20b 100644 --- a/test/integration/fixtures/blocks/core__form.serialized.html +++ b/test/integration/fixtures/blocks/core__form.serialized.html @@ -1,18 +1,18 @@ - +
    - +
    - +
    - +