diff --git a/packages/block-editor/src/components/typewriter/index.js b/packages/block-editor/src/components/typewriter/index.js
index ac83d173a95d9d..b5e230d314a7e2 100644
--- a/packages/block-editor/src/components/typewriter/index.js
+++ b/packages/block-editor/src/components/typewriter/index.js
@@ -273,7 +273,7 @@ function Typewriter( { children } ) {
* challenges in Internet Explorer, and is simply skipped, rendering the given
* props children instead.
*
- * @type {WPComponent}
+ * @type {Component}
*/
const TypewriterOrIEBypass = isIE ? ( props ) => props.children : Typewriter;
diff --git a/packages/block-editor/src/components/unit-control/README.md b/packages/block-editor/src/components/unit-control/README.md
index 4d99d85254decb..e5126e7bf45242 100644
--- a/packages/block-editor/src/components/unit-control/README.md
+++ b/packages/block-editor/src/components/unit-control/README.md
@@ -26,8 +26,8 @@ UnitControl component allows the user to set a value as well as a unit (e.g. `px
Renders a control (`input` and `select`) with the values `10` and `px` parsed from `10px`.
```jsx
+import { useState } from 'react';
import { __experimentalUnitControl as UnitControl } from '@wordpress/block-editor/';
-import { useState } from '@wordpress/element';
const Example = () => {
const [ value, setValue ] = useState( '10px' );
diff --git a/packages/block-editor/src/components/unsupported-block-details/index.native.js b/packages/block-editor/src/components/unsupported-block-details/index.native.js
new file mode 100644
index 00000000000000..a35d96a925676b
--- /dev/null
+++ b/packages/block-editor/src/components/unsupported-block-details/index.native.js
@@ -0,0 +1,183 @@
+/**
+ * External dependencies
+ */
+import { View, Text } from 'react-native';
+
+/**
+ * WordPress dependencies
+ */
+import { BottomSheet, Icon, TextControl } from '@wordpress/components';
+import {
+ requestUnsupportedBlockFallback,
+ sendActionButtonPressedAction,
+ actionButtons,
+} from '@wordpress/react-native-bridge';
+import { help } from '@wordpress/icons';
+import { __ } from '@wordpress/i18n';
+import { usePreferredColorSchemeStyle } from '@wordpress/compose';
+import { getBlockType } from '@wordpress/blocks';
+import { useCallback, useState } from '@wordpress/element';
+import { applyFilters } from '@wordpress/hooks';
+
+/**
+ * Internal dependencies
+ */
+import styles from './style.scss';
+import useUnsupportedBlockEditor from '../use-unsupported-block-editor';
+
+const EMPTY_ARRAY = [];
+
+const UnsupportedBlockDetails = ( {
+ clientId,
+ showSheet,
+ onCloseSheet,
+ customBlockTitle = '',
+ icon,
+ title,
+ description,
+ actionButtonLabel,
+ customActions = EMPTY_ARRAY,
+} ) => {
+ const [ sendFallbackMessage, setSendFallbackMessage ] = useState( false );
+ const [ sendButtonPressMessage, setSendButtonPressMessage ] =
+ useState( false );
+
+ const {
+ blockName,
+ blockContent,
+ isUnsupportedBlockEditorSupported,
+ canEnableUnsupportedBlockEditor,
+ isEditableInUnsupportedBlockEditor,
+ } = useUnsupportedBlockEditor( clientId );
+
+ // Styles
+ const textStyle = usePreferredColorSchemeStyle(
+ styles[ 'unsupported-block-details__text' ],
+ styles[ 'unsupported-block-details__text--dark' ]
+ );
+ const titleStyle = usePreferredColorSchemeStyle(
+ styles[ 'unsupported-block-details__title' ],
+ styles[ 'unsupported-block-details__title--dark' ]
+ );
+ const descriptionStyle = usePreferredColorSchemeStyle(
+ styles[ 'unsupported-block-details__description' ],
+ styles[ 'unsupported-block-details__description--dark' ]
+ );
+ const iconStyle = usePreferredColorSchemeStyle(
+ styles[ 'unsupported-block-details__icon' ],
+ styles[ 'unsupported-block-details__icon--dark' ]
+ );
+ const actionButtonStyle = usePreferredColorSchemeStyle(
+ styles[ 'unsupported-block-details__action-button' ],
+ styles[ 'unsupported-block-details__action-button--dark' ]
+ );
+
+ const blockTitle =
+ customBlockTitle || getBlockType( blockName )?.title || blockName;
+
+ const requestFallback = useCallback( () => {
+ if (
+ canEnableUnsupportedBlockEditor &&
+ isUnsupportedBlockEditorSupported === false
+ ) {
+ onCloseSheet();
+ setSendButtonPressMessage( true );
+ } else {
+ onCloseSheet();
+ setSendFallbackMessage( true );
+ }
+ }, [
+ canEnableUnsupportedBlockEditor,
+ isUnsupportedBlockEditorSupported,
+ onCloseSheet,
+ ] );
+
+ // The description can include extra notes via WP hooks.
+ const descriptionWithNotes = applyFilters(
+ 'native.unsupported_block_details_extra_note',
+ description,
+ blockName
+ );
+
+ const webEditorDefaultLabel = applyFilters(
+ 'native.unsupported_block_details_web_editor_action',
+ __( 'Edit using web editor' )
+ );
+
+ const canUseWebEditor =
+ ( isUnsupportedBlockEditorSupported ||
+ canEnableUnsupportedBlockEditor ) &&
+ isEditableInUnsupportedBlockEditor;
+ const actions = [
+ ...[
+ canUseWebEditor && {
+ label: actionButtonLabel || webEditorDefaultLabel,
+ onPress: requestFallback,
+ },
+ ],
+ ...customActions,
+ ].filter( Boolean );
+
+ return (
+ {
+ if ( sendFallbackMessage ) {
+ // On iOS, onModalHide is called when the controller is still part of the hierarchy.
+ // A small delay will ensure that the controller has already been removed.
+ this.timeout = setTimeout( () => {
+ // For the Classic block, the content is kept in the `content` attribute.
+ requestUnsupportedBlockFallback(
+ blockContent,
+ clientId,
+ blockName,
+ blockTitle
+ );
+ }, 100 );
+ setSendFallbackMessage( false );
+ } else if ( sendButtonPressMessage ) {
+ this.timeout = setTimeout( () => {
+ sendActionButtonPressedAction(
+ actionButtons.missingBlockAlertActionButton
+ );
+ }, 100 );
+ setSendButtonPressMessage( false );
+ }
+ } }
+ >
+
+
+ { title }
+ { isEditableInUnsupportedBlockEditor &&
+ descriptionWithNotes && (
+
+ { descriptionWithNotes }
+
+ ) }
+
+ { actions.map( ( { label, onPress }, index ) => (
+
+ ) ) }
+
+
+ );
+};
+
+export default UnsupportedBlockDetails;
diff --git a/packages/block-editor/src/components/unsupported-block-details/style.native.scss b/packages/block-editor/src/components/unsupported-block-details/style.native.scss
new file mode 100644
index 00000000000000..a12272e6c85938
--- /dev/null
+++ b/packages/block-editor/src/components/unsupported-block-details/style.native.scss
@@ -0,0 +1,56 @@
+.unsupported-block-details__container {
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.unsupported-block-details__icon {
+ size: 36;
+ height: 36;
+ padding-top: 8;
+ padding-bottom: 8;
+ color: $gray;
+}
+
+.unsupported-block-details__icon--dark {
+ color: $gray-20;
+}
+
+.unsupported-block-details__text {
+ text-align: center;
+ color: $gray-dark;
+}
+
+.unsupported-block-details__text--dark {
+ color: $white;
+}
+
+.unsupported-block-details__title {
+ padding-top: 8;
+ padding-bottom: 12;
+ font-size: 20;
+ font-weight: bold;
+ color: $gray-dark;
+}
+
+.unsupported-block-details__title--dark {
+ color: $white;
+}
+
+.unsupported-block-details__description {
+ padding-bottom: 24;
+ font-size: 16;
+ color: $gray-darken-20;
+}
+
+.unsupported-block-details__description--dark {
+ color: $gray-20;
+}
+
+.unsupported-block-details__action-button {
+ color: $blue-50;
+}
+
+.unsupported-block-details__action-button--dark {
+ color: $blue-30;
+}
diff --git a/packages/block-editor/src/components/url-input/README.md b/packages/block-editor/src/components/url-input/README.md
index a10ae409ef61a1..46f673ecd35545 100644
--- a/packages/block-editor/src/components/url-input/README.md
+++ b/packages/block-editor/src/components/url-input/README.md
@@ -53,7 +53,7 @@ wp.blocks.registerBlockType( /* ... */, {
},
edit: function( props ) {
- return wp.element.createElement( wp.blockEditor.URLInputButton, {
+ return React.createElement( wp.blockEditor.URLInputButton, {
className: props.className,
url: props.attributes.url,
onChange: function( url, post ) {
@@ -63,7 +63,7 @@ wp.blocks.registerBlockType( /* ... */, {
},
save: function( props ) {
- return wp.element.createElement( 'a', {
+ return React.createElement( 'a', {
href: props.attributes.url,
}, props.attributes.text );
}
@@ -189,7 +189,7 @@ wp.blocks.registerBlockType( /* ... */, {
},
edit: function( props ) {
- return wp.element.createElement( wp.blockEditor.URLInput, {
+ return React.createElement( wp.blockEditor.URLInput, {
className: props.className,
value: props.attributes.url,
onChange: function( url, post ) {
@@ -199,7 +199,7 @@ wp.blocks.registerBlockType( /* ... */, {
},
save: function( props ) {
- return wp.element.createElement( 'a', {
+ return React.createElement( 'a', {
href: props.attributes.url,
}, props.attributes.text );
}
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 6b0a9300124493..72ea6a698c3439 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
@@ -16,13 +16,13 @@ import { getFilesFromDataTransfer } from '@wordpress/dom';
*/
import { store as blockEditorStore } from '../../store';
-/** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */
+/** @typedef {import('react').SyntheticEvent} SyntheticEvent */
/** @typedef {import('./types').WPDropOperation} WPDropOperation */
/**
* Retrieve the data for a block drop event.
*
- * @param {WPSyntheticEvent} event The drop event.
+ * @param {SyntheticEvent} event The drop event.
*
* @return {Object} An object with block drag and drop data.
*/
diff --git a/packages/block-editor/src/components/use-unsupported-block-editor/index.native.js b/packages/block-editor/src/components/use-unsupported-block-editor/index.native.js
new file mode 100644
index 00000000000000..4b5bbe65857b28
--- /dev/null
+++ b/packages/block-editor/src/components/use-unsupported-block-editor/index.native.js
@@ -0,0 +1,59 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+import { serialize } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { store as blockEditorStore } from '../../store';
+
+// Blocks that can't be edited through the Unsupported block editor identified by their name.
+const UBE_INCOMPATIBLE_BLOCKS = [ 'core/block' ];
+
+/**
+ * Hook that retrieves the settings to determine if the
+ * Unsupported Block Editor can be used in a specific block.
+ *
+ * @param {string} clientId Client ID of block.
+ * @return {Object} Unsupported block editor settings.
+ */
+export default function useUnsupportedBlockEditor( clientId ) {
+ return useSelect(
+ ( select ) => {
+ const { getBlock, getSettings } = select( blockEditorStore );
+ const { capabilities } = getSettings();
+
+ const block = getBlock( clientId );
+ const blockAttributes = block?.attributes || {};
+
+ const blockDetails = {
+ blockName: block?.name,
+ blockContent: serialize( block ? [ block ] : [] ),
+ };
+
+ // If the block is unsupported, use the `original` attributes to identify the block's name.
+ if ( blockDetails.blockName === 'core/missing' ) {
+ blockDetails.blockName = blockAttributes.originalName;
+ blockDetails.blockContent =
+ blockDetails.blockName === 'core/freeform'
+ ? blockAttributes.content
+ : block?.originalContent;
+ }
+
+ return {
+ isUnsupportedBlockEditorSupported:
+ capabilities?.unsupportedBlockEditor === true,
+ canEnableUnsupportedBlockEditor:
+ capabilities?.canEnableUnsupportedBlockEditor === true,
+ isEditableInUnsupportedBlockEditor:
+ ! UBE_INCOMPATIBLE_BLOCKS.includes(
+ blockDetails.blockName
+ ),
+ ...blockDetails,
+ };
+ },
+ [ clientId ]
+ );
+}
diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js
index e3100dc3464c65..f15c1dac0267fd 100644
--- a/packages/block-editor/src/components/writing-flow/index.js
+++ b/packages/block-editor/src/components/writing-flow/index.js
@@ -22,6 +22,7 @@ import useDragSelection from './use-drag-selection';
import useSelectionObserver from './use-selection-observer';
import useClickSelection from './use-click-selection';
import useInput from './use-input';
+import useClipboardHandler from './use-clipboard-handler';
import { store as blockEditorStore } from '../../store';
export function useWritingFlow() {
@@ -35,6 +36,7 @@ export function useWritingFlow() {
before,
useMergeRefs( [
ref,
+ useClipboardHandler(),
useInput(),
useDragSelection(),
useSelectionObserver(),
@@ -93,7 +95,7 @@ function WritingFlow( { children, ...props }, forwardedRef ) {
* Handles selection and navigation across blocks. This component should be
* wrapped around BlockList.
*
- * @param {Object} props Component properties.
- * @param {WPElement} props.children Children to be rendered.
+ * @param {Object} props Component properties.
+ * @param {Element} props.children Children to be rendered.
*/
export default forwardRef( WritingFlow );
diff --git a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js
new file mode 100644
index 00000000000000..5b78d2f8656b61
--- /dev/null
+++ b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js
@@ -0,0 +1,242 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ serialize,
+ pasteHandler,
+ createBlock,
+ findTransform,
+ getBlockTransforms,
+} from '@wordpress/blocks';
+import {
+ documentHasSelection,
+ documentHasUncollapsedSelection,
+ __unstableStripHTML as stripHTML,
+} from '@wordpress/dom';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { useRefEffect } from '@wordpress/compose';
+
+/**
+ * Internal dependencies
+ */
+import { getPasteEventData } from '../../utils/pasting';
+import { store as blockEditorStore } from '../../store';
+import { useNotifyCopy } from '../../utils/use-notify-copy';
+
+export default function useClipboardHandler() {
+ const {
+ getBlocksByClientId,
+ getSelectedBlockClientIds,
+ hasMultiSelection,
+ getSettings,
+ __unstableIsFullySelected,
+ __unstableIsSelectionCollapsed,
+ __unstableIsSelectionMergeable,
+ __unstableGetSelectedBlocksWithPartialSelection,
+ canInsertBlockType,
+ } = useSelect( blockEditorStore );
+ const {
+ flashBlock,
+ removeBlocks,
+ replaceBlocks,
+ __unstableDeleteSelection,
+ __unstableExpandSelection,
+ insertBlocks,
+ } = useDispatch( blockEditorStore );
+ const notifyCopy = useNotifyCopy();
+
+ return useRefEffect( ( node ) => {
+ function handler( event ) {
+ if ( event.defaultPrevented ) {
+ // This was likely already handled in rich-text/use-paste-handler.js.
+ return;
+ }
+
+ const selectedBlockClientIds = getSelectedBlockClientIds();
+
+ if ( selectedBlockClientIds.length === 0 ) {
+ return;
+ }
+
+ // Always handle multiple selected blocks.
+ if ( ! hasMultiSelection() ) {
+ const { target } = event;
+ const { ownerDocument } = target;
+ // If copying, only consider actual text selection as selection.
+ // Otherwise, any focus on an input field is considered.
+ const hasSelection =
+ event.type === 'copy' || event.type === 'cut'
+ ? documentHasUncollapsedSelection( ownerDocument )
+ : documentHasSelection( ownerDocument );
+
+ // Let native copy behaviour take over in input fields.
+ if ( hasSelection ) {
+ return;
+ }
+ }
+
+ if ( ! node.contains( event.target.ownerDocument.activeElement ) ) {
+ return;
+ }
+
+ event.preventDefault();
+
+ const isSelectionMergeable = __unstableIsSelectionMergeable();
+ const shouldHandleWholeBlocks =
+ __unstableIsSelectionCollapsed() || __unstableIsFullySelected();
+ const expandSelectionIsNeeded =
+ ! shouldHandleWholeBlocks && ! isSelectionMergeable;
+ if ( event.type === 'copy' || event.type === 'cut' ) {
+ if ( selectedBlockClientIds.length === 1 ) {
+ flashBlock( selectedBlockClientIds[ 0 ] );
+ }
+ // If we have a partial selection that is not mergeable, just
+ // expand the selection to the whole blocks.
+ if ( expandSelectionIsNeeded ) {
+ __unstableExpandSelection();
+ } else {
+ notifyCopy( event.type, selectedBlockClientIds );
+ let blocks;
+ // Check if we have partial selection.
+ if ( shouldHandleWholeBlocks ) {
+ blocks = getBlocksByClientId( selectedBlockClientIds );
+ } else {
+ const [ head, tail ] =
+ __unstableGetSelectedBlocksWithPartialSelection();
+ const inBetweenBlocks = getBlocksByClientId(
+ selectedBlockClientIds.slice(
+ 1,
+ selectedBlockClientIds.length - 1
+ )
+ );
+ blocks = [ head, ...inBetweenBlocks, tail ];
+ }
+
+ const wrapperBlockName = event.clipboardData.getData(
+ '__unstableWrapperBlockName'
+ );
+
+ if ( wrapperBlockName ) {
+ blocks = createBlock(
+ wrapperBlockName,
+ JSON.parse(
+ event.clipboardData.getData(
+ '__unstableWrapperBlockAttributes'
+ )
+ ),
+ blocks
+ );
+ }
+
+ const serialized = serialize( blocks );
+
+ event.clipboardData.setData(
+ 'text/plain',
+ toPlainText( serialized )
+ );
+ event.clipboardData.setData( 'text/html', serialized );
+ }
+ }
+
+ if ( event.type === 'cut' ) {
+ // We need to also check if at the start we needed to
+ // expand the selection, as in this point we might have
+ // programmatically fully selected the blocks above.
+ if ( shouldHandleWholeBlocks && ! expandSelectionIsNeeded ) {
+ removeBlocks( selectedBlockClientIds );
+ } else {
+ event.target.ownerDocument.activeElement.contentEditable = false;
+ __unstableDeleteSelection();
+ }
+ } else if ( event.type === 'paste' ) {
+ const {
+ __experimentalCanUserUseUnfilteredHTML:
+ canUserUseUnfilteredHTML,
+ } = getSettings();
+ const { plainText, html, files } = getPasteEventData( event );
+ let blocks = [];
+
+ if ( files.length ) {
+ const fromTransforms = getBlockTransforms( 'from' );
+ blocks = files
+ .reduce( ( accumulator, file ) => {
+ const transformation = findTransform(
+ fromTransforms,
+ ( transform ) =>
+ transform.type === 'files' &&
+ transform.isMatch( [ file ] )
+ );
+ if ( transformation ) {
+ accumulator.push(
+ transformation.transform( [ file ] )
+ );
+ }
+ return accumulator;
+ }, [] )
+ .flat();
+ } else {
+ blocks = pasteHandler( {
+ HTML: html,
+ plainText,
+ mode: 'BLOCKS',
+ canUserUseUnfilteredHTML,
+ } );
+ }
+
+ if ( selectedBlockClientIds.length === 1 ) {
+ const [ selectedBlockClientId ] = selectedBlockClientIds;
+
+ if (
+ blocks.every( ( block ) =>
+ canInsertBlockType(
+ block.name,
+ selectedBlockClientId
+ )
+ )
+ ) {
+ insertBlocks(
+ blocks,
+ undefined,
+ selectedBlockClientId
+ );
+ return;
+ }
+ }
+
+ replaceBlocks(
+ selectedBlockClientIds,
+ blocks,
+ blocks.length - 1,
+ -1
+ );
+ }
+ }
+
+ node.ownerDocument.addEventListener( 'copy', handler );
+ node.ownerDocument.addEventListener( 'cut', handler );
+ node.ownerDocument.addEventListener( 'paste', handler );
+
+ return () => {
+ node.ownerDocument.removeEventListener( 'copy', handler );
+ node.ownerDocument.removeEventListener( 'cut', handler );
+ node.ownerDocument.removeEventListener( 'paste', handler );
+ };
+ }, [] );
+}
+
+/**
+ * Given a string of HTML representing serialized blocks, returns the plain
+ * text extracted after stripping the HTML of any tags and fixing line breaks.
+ *
+ * @param {string} html Serialized blocks.
+ * @return {string} The plain-text content with any html removed.
+ */
+function toPlainText( html ) {
+ // Manually handle BR tags as line breaks prior to `stripHTML` call
+ html = html.replace( / /g, '\n' );
+
+ const plainText = stripHTML( html ).trim();
+
+ // Merge any consecutive line breaks
+ return plainText.replace( /\n\n+/g, '\n\n' );
+}
diff --git a/packages/block-editor/src/components/writing-mode-control/index.js b/packages/block-editor/src/components/writing-mode-control/index.js
index 99acc34570e99f..2adf8be14ad395 100644
--- a/packages/block-editor/src/components/writing-mode-control/index.js
+++ b/packages/block-editor/src/components/writing-mode-control/index.js
@@ -31,7 +31,7 @@ const WRITING_MODES = [
* @param {string} props.value Currently selected writing mode.
* @param {Function} props.onChange Handles change in the writing mode selection.
*
- * @return {WPElement} Writing Mode control.
+ * @return {Element} Writing Mode control.
*/
export default function WritingModeControl( { className, value, onChange } ) {
return (
diff --git a/packages/block-editor/src/hooks/anchor.js b/packages/block-editor/src/hooks/anchor.js
index b3fdcd65c541b7..6e8acd5eb7f2e7 100644
--- a/packages/block-editor/src/hooks/anchor.js
+++ b/packages/block-editor/src/hooks/anchor.js
@@ -56,9 +56,9 @@ export function addAttribute( settings ) {
* Override the default edit UI to include a new block inspector control for
* assigning the anchor ID, if block supports anchor.
*
- * @param {WPComponent} BlockEdit Original component.
+ * @param {Component} BlockEdit Original component.
*
- * @return {WPComponent} Wrapped component.
+ * @return {Component} Wrapped component.
*/
export const withInspectorControl = createHigherOrderComponent(
( BlockEdit ) => {
diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js
index 25815ad5d2cae1..4c2849eb8cef2b 100644
--- a/packages/block-editor/src/hooks/background.js
+++ b/packages/block-editor/src/hooks/background.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
/**
* WordPress dependencies
*/
@@ -5,17 +10,17 @@ import { isBlobURL } from '@wordpress/blob';
import { getBlockSupport } from '@wordpress/blocks';
import {
__experimentalToolsPanelItem as ToolsPanelItem,
- Button,
DropZone,
FlexItem,
MenuItem,
+ VisuallyHidden,
__experimentalItemGroup as ItemGroup,
__experimentalHStack as HStack,
__experimentalTruncate as Truncate,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { Platform, useCallback } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { getFilename } from '@wordpress/url';
@@ -24,8 +29,6 @@ import { getFilename } from '@wordpress/url';
*/
import InspectorControls from '../components/inspector-controls';
import MediaReplaceFlow from '../components/media-replace-flow';
-import MediaUpload from '../components/media-upload';
-import MediaUploadCheck from '../components/media-upload/check';
import useSetting from '../components/use-setting';
import { cleanEmptyObject } from './utils';
import { store as blockEditorStore } from '../store';
@@ -96,12 +99,29 @@ export function resetBackgroundImage( { attributes = {}, setAttributes } ) {
} );
}
-function InspectorImagePreview( { label, url: imgUrl } ) {
+function InspectorImagePreview( { label, filename, url: imgUrl } ) {
const imgLabel = label || getFilename( imgUrl );
return (
-
+
+ { imgUrl && (
+
+ ) }
+
{ imgLabel }
+
+ { filename
+ ? sprintf(
+ /* translators: %s: file name */
+ __( 'Selected image: %s' ),
+ filename
+ )
+ : __( 'No image selected' ) }
+
@@ -223,47 +252,29 @@ function BackgroundImagePanelItem( props ) {
panelId={ clientId }
>
- { !! url && (
-
- }
- variant="secondary"
- >
-
resetBackgroundImage( props ) }
- >
- { __( 'Reset ' ) }
-
-
- ) }
- { ! url && (
-
- (
-
-
- { __( 'Add background image' ) }
-
-
-
- ) }
+
-
- ) }
+ }
+ variant="secondary"
+ >
+
resetBackgroundImage( props ) }>
+ { __( 'Reset ' ) }
+
+
+
);
diff --git a/packages/block-editor/src/hooks/background.scss b/packages/block-editor/src/hooks/background.scss
index 0ac17181990191..a81b6acfce2de7 100644
--- a/packages/block-editor/src/hooks/background.scss
+++ b/packages/block-editor/src/hooks/background.scss
@@ -1,17 +1,14 @@
-.block-editor-hooks__background__inspector-upload-container {
+.block-editor-hooks__background__inspector-media-replace-container {
position: relative;
// Since there is no option to skip rendering the drag'n'drop icon in drop
// zone, we hide it for now.
.components-drop-zone__content-icon {
display: none;
}
-}
-.block-editor-hooks__background__inspector-upload-container,
-.block-editor-hooks__background__inspector-media-replace-container {
button.components-button {
color: $gray-900;
- box-shadow: inset 0 0 0 1px $gray-400;
+ box-shadow: inset 0 0 0 $border-width $gray-300;
width: 100%;
display: block;
height: $grid-unit-50;
@@ -34,24 +31,45 @@
text-align: start;
text-align-last: center;
}
-}
-.block-editor-hooks__background__inspector-media-replace-container {
.components-dropdown {
display: block;
}
+}
- img {
- width: 20px;
- min-width: 20px;
- aspect-ratio: 1;
- box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
- border-radius: 50% !important;
- }
+.block-editor-hooks__background__inspector-image-indicator-wrapper {
+ background: #fff linear-gradient(-45deg, transparent 48%, $gray-300 48%, $gray-300 52%, transparent 52%); // Show a diagonal line (crossed out) for empty background image.
+ border-radius: $radius-round !important; // Override the default border-radius inherited from FlexItem.
+ box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2);
+ display: block;
+ width: 20px;
+ height: 20px;
+ flex: none;
- .block-editor-hooks__background__inspector-readonly-logo-preview {
- padding: 6px 12px;
- display: flex;
- height: $grid-unit-50;
+ &.has-image {
+ background: #fff; // No diagonal line for non-empty background image. A background color is in use to account for partially transparent images.
}
}
+
+.block-editor-hooks__background__inspector-image-indicator {
+ background-size: cover;
+ border-radius: $radius-round;
+ width: 20px;
+ height: 20px;
+ display: block;
+ position: relative;
+}
+
+.block-editor-hooks__background__inspector-image-indicator::after {
+ content: "";
+ position: absolute;
+ top: -1px;
+ left: -1px;
+ bottom: -1px;
+ right: -1px;
+ border-radius: $radius-round;
+ box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2);
+ // Show a thin outline in Windows high contrast mode, otherwise the button is invisible.
+ border: 1px solid transparent;
+ box-sizing: inherit;
+}
diff --git a/packages/block-editor/src/hooks/block-rename-ui.js b/packages/block-editor/src/hooks/block-rename-ui.js
index 835a09556aed5e..9025bfee619839 100644
--- a/packages/block-editor/src/hooks/block-rename-ui.js
+++ b/packages/block-editor/src/hooks/block-rename-ui.js
@@ -44,17 +44,21 @@ function RenameModal( { blockName, originalBlockName, onClose, onSave } ) {
);
const handleSubmit = () => {
- // Must be assertive to immediately announce change.
- speak(
- sprintf(
- /* translators: %1$s: type of update (either reset of changed). %2$s: new name/label for the block */
- __( 'Block name %1$s to: "%2$s".' ),
- nameIsOriginal || nameIsEmpty ? __( 'reset' ) : __( 'changed' ),
- editedBlockName
- ),
- 'assertive'
- );
+ const message =
+ nameIsOriginal || nameIsEmpty
+ ? sprintf(
+ /* translators: %s: new name/label for the block */
+ __( 'Block name reset to: "%s".' ),
+ editedBlockName
+ )
+ : sprintf(
+ /* translators: %s: new name/label for the block */
+ __( 'Block name changed to: "%s".' ),
+ editedBlockName
+ );
+ // Must be assertive to immediately announce change.
+ speak( message, 'assertive' );
onSave( editedBlockName );
// Immediate close avoids ability to hit save multiple times.
@@ -69,6 +73,7 @@ function RenameModal( { blockName, originalBlockName, onClose, onSave } ) {
aria={ {
describedby: dialogDescription,
} }
+ focusOnMount="firstContentElement"
>
{ __( 'Enter a custom name for this block.' ) }
diff --git a/packages/block-editor/src/hooks/custom-class-name.js b/packages/block-editor/src/hooks/custom-class-name.js
index 5505c5fcae2cca..087fdffe23255a 100644
--- a/packages/block-editor/src/hooks/custom-class-name.js
+++ b/packages/block-editor/src/hooks/custom-class-name.js
@@ -44,9 +44,9 @@ export function addAttribute( settings ) {
* assigning the custom class name, if block supports custom class name.
* The control is displayed within the Advanced panel in the block inspector.
*
- * @param {WPComponent} BlockEdit Original component.
+ * @param {Component} BlockEdit Original component.
*
- * @return {WPComponent} Wrapped component.
+ * @return {Component} Wrapped component.
*/
export const withInspectorControl = createHigherOrderComponent(
( BlockEdit ) => {
diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js
index dbc8c3ec2c089f..9affb55c3ea71f 100644
--- a/packages/block-editor/src/hooks/custom-fields.js
+++ b/packages/block-editor/src/hooks/custom-fields.js
@@ -40,9 +40,9 @@ function addAttribute( settings ) {
* Currently, only the `core/paragraph` block is supported and there is only a relation
* between paragraph content and a custom field.
*
- * @param {WPComponent} BlockEdit Original component.
+ * @param {Component} BlockEdit Original component.
*
- * @return {WPComponent} Wrapped component.
+ * @return {Component} Wrapped component.
*/
const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
diff --git a/packages/block-editor/src/hooks/font-size.js b/packages/block-editor/src/hooks/font-size.js
index 54d6836a1ef0ad..a9d2facfa26702 100644
--- a/packages/block-editor/src/hooks/font-size.js
+++ b/packages/block-editor/src/hooks/font-size.js
@@ -115,7 +115,7 @@ function addEditProps( settings ) {
*
* @param {Object} props
*
- * @return {WPElement} Font size edit element.
+ * @return {Element} Font size edit element.
*/
export function FontSizeEdit( props ) {
const {
diff --git a/packages/block-editor/src/hooks/line-height.js b/packages/block-editor/src/hooks/line-height.js
index 363766e91b4978..b835f54a12b6c4 100644
--- a/packages/block-editor/src/hooks/line-height.js
+++ b/packages/block-editor/src/hooks/line-height.js
@@ -17,7 +17,7 @@ export const LINE_HEIGHT_SUPPORT_KEY = 'typography.lineHeight';
*
* @param {Object} props
*
- * @return {WPElement} Line height edit element.
+ * @return {Element} Line height edit element.
*/
export function LineHeightEdit( props ) {
const {
diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js
index cfccb23663aa0d..7c1bff6e99692f 100644
--- a/packages/block-editor/src/hooks/position.js
+++ b/packages/block-editor/src/hooks/position.js
@@ -209,7 +209,7 @@ export function useIsPositionDisabled( { name: blockName } = {} ) {
*
* @param {Object} props
*
- * @return {WPElement} Position panel.
+ * @return {Element} Position panel.
*/
export function PositionPanel( props ) {
const {
@@ -296,7 +296,7 @@ export function PositionPanel( props ) {
>
name === category.name
)
) {
@@ -1905,8 +1906,8 @@ export const registerInserterMediaCategory =
return;
}
if (
- inserterMediaCategories.some(
- ( { labels: { name } } ) => name === category.labels?.name
+ registeredInserterMediaCategories.some(
+ ( { labels: { name } = {} } ) => name === category.labels?.name
)
) {
console.error(
@@ -1919,13 +1920,8 @@ export const registerInserterMediaCategory =
// private, so extenders can only add new inserter media categories and don't have any
// control over the core media categories.
dispatch( {
- type: 'UPDATE_SETTINGS',
- settings: {
- inserterMediaCategories: [
- ...inserterMediaCategories,
- { ...category, isExternalResource: true },
- ],
- },
+ type: 'REGISTER_INSERTER_MEDIA_CATEGORY',
+ category: { ...category, isExternalResource: true },
} );
};
diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js
index e77e478ba60466..c4220e6e7e516c 100644
--- a/packages/block-editor/src/store/private-selectors.js
+++ b/packages/block-editor/src/store/private-selectors.js
@@ -45,9 +45,8 @@ export function getLastInsertedBlocksClientIds( state ) {
export const isBlockSubtreeDisabled = createSelector(
( state, clientId ) => {
const isChildSubtreeDisabled = ( childClientId ) => {
- const mode = state.blockEditingModes.get( childClientId );
return (
- ( mode === undefined || mode === 'disabled' ) &&
+ getBlockEditingMode( state, childClientId ) === 'disabled' &&
getBlockOrder( state, childClientId ).every(
isChildSubtreeDisabled
)
@@ -58,7 +57,12 @@ export const isBlockSubtreeDisabled = createSelector(
getBlockOrder( state, clientId ).every( isChildSubtreeDisabled )
);
},
- ( state ) => [ state.blockEditingModes, state.blocks.parents ]
+ ( state ) => [
+ state.blocks.parents,
+ state.blocks.order,
+ state.blockEditingModes,
+ state.blockListSettings,
+ ]
);
/**
@@ -160,3 +164,75 @@ export function getOpenedBlockSettingsMenu( state ) {
export function getStyleOverrides( state ) {
return state.styleOverrides;
}
+
+/** @typedef {import('./actions').InserterMediaCategory} InserterMediaCategory */
+/**
+ * Returns the registered inserter media categories through the public API.
+ *
+ * @param {Object} state Editor state.
+ *
+ * @return {InserterMediaCategory[]} Inserter media categories.
+ */
+export function getRegisteredInserterMediaCategories( state ) {
+ return state.registeredInserterMediaCategories;
+}
+
+/**
+ * Returns an array containing the allowed inserter media categories.
+ * It merges the registered media categories from extenders with the
+ * core ones. It also takes into account the allowed `mime_types`, which
+ * can be altered by `upload_mimes` filter and restrict some of them.
+ *
+ * @param {Object} state Global application state.
+ *
+ * @return {InserterMediaCategory[]} Client IDs of descendants.
+ */
+export const getInserterMediaCategories = createSelector(
+ ( state ) => {
+ const {
+ settings: {
+ inserterMediaCategories,
+ allowedMimeTypes,
+ enableOpenverseMediaCategory,
+ },
+ registeredInserterMediaCategories,
+ } = state;
+ // The allowed `mime_types` can be altered by `upload_mimes` filter and restrict
+ // some of them. In this case we shouldn't add the category to the available media
+ // categories list in the inserter.
+ if (
+ ( ! inserterMediaCategories &&
+ ! registeredInserterMediaCategories.length ) ||
+ ! allowedMimeTypes
+ ) {
+ return;
+ }
+ const coreInserterMediaCategoriesNames =
+ inserterMediaCategories?.map( ( { name } ) => name ) || [];
+ const mergedCategories = [
+ ...( inserterMediaCategories || [] ),
+ ...( registeredInserterMediaCategories || [] ).filter(
+ ( { name } ) =>
+ ! coreInserterMediaCategoriesNames.includes( name )
+ ),
+ ];
+ return mergedCategories.filter( ( category ) => {
+ // Check if Openverse category is enabled.
+ if (
+ ! enableOpenverseMediaCategory &&
+ category.name === 'openverse'
+ ) {
+ return false;
+ }
+ return Object.values( allowedMimeTypes ).some( ( mimeType ) =>
+ mimeType.startsWith( `${ category.mediaType }/` )
+ );
+ } );
+ },
+ ( state ) => [
+ state.settings.inserterMediaCategories,
+ state.settings.allowedMimeTypes,
+ state.settings.enableOpenverseMediaCategory,
+ state.registeredInserterMediaCategories,
+ ]
+);
diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js
index 18048ce138eb23..4373182d986622 100644
--- a/packages/block-editor/src/store/reducer.js
+++ b/packages/block-editor/src/store/reducer.js
@@ -1949,6 +1949,22 @@ export function styleOverrides( state = new Map(), action ) {
return state;
}
+/**
+ * Reducer returning a map of the registered inserter media categories.
+ *
+ * @param {Array} state Current state.
+ * @param {Object} action Dispatched action.
+ *
+ * @return {Array} Updated state.
+ */
+export function registeredInserterMediaCategories( state = [], action ) {
+ switch ( action.type ) {
+ case 'REGISTER_INSERTER_MEDIA_CATEGORY':
+ return [ ...state, action.category ];
+ }
+ return state;
+}
+
const combinedReducers = combineReducers( {
blocks,
isTyping,
@@ -1976,6 +1992,7 @@ const combinedReducers = combineReducers( {
removalPromptData,
blockRemovalRules,
openedBlockSettingsMenu,
+ registeredInserterMediaCategories,
} );
function withAutomaticChangeReset( reducer ) {
diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js
index 2f73e50b602d2c..cb1f8ef49809d8 100644
--- a/packages/block-editor/src/store/selectors.js
+++ b/packages/block-editor/src/store/selectors.js
@@ -1986,7 +1986,7 @@ export const getInserterItems = createSelector(
isDisabled: false,
utility: 1, // Deprecated.
frecency,
- content: reusableBlock.content.raw,
+ content: reusableBlock.content?.raw,
syncStatus: reusableBlock.wp_pattern_sync_status,
};
};
diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js
index f1f8cb29f1406e..e65921e30a6ce7 100644
--- a/packages/block-editor/src/store/test/actions.js
+++ b/packages/block-editor/src/store/test/actions.js
@@ -1279,9 +1279,9 @@ describe( 'actions', () => {
fetch: () => {},
} )( {
select: {
- getSettings: () => ( {
- inserterMediaCategories: [ { name: 'a' } ],
- } ),
+ getRegisteredInserterMediaCategories: () => [
+ { name: 'a' },
+ ],
},
} );
expect( console ).toHaveErroredWith(
@@ -1296,11 +1296,9 @@ describe( 'actions', () => {
fetch: () => {},
} )( {
select: {
- getSettings: () => ( {
- inserterMediaCategories: [
- { labels: { name: 'a' } },
- ],
- } ),
+ getRegisteredInserterMediaCategories: () => [
+ { labels: { name: 'a' } },
+ ],
},
} );
expect( console ).toHaveErroredWith(
@@ -1321,18 +1319,14 @@ describe( 'actions', () => {
const dispatch = jest.fn();
registerInserterMediaCategory( category )( {
select: {
- getSettings: () => ( { inserterMediaCategories } ),
+ getRegisteredInserterMediaCategories: () =>
+ inserterMediaCategories,
},
dispatch,
} );
expect( dispatch ).toHaveBeenLastCalledWith( {
- type: 'UPDATE_SETTINGS',
- settings: {
- inserterMediaCategories: [
- ...inserterMediaCategories,
- { ...category, isExternalResource: true },
- ],
- },
+ type: 'REGISTER_INSERTER_MEDIA_CATEGORY',
+ category: { ...category, isExternalResource: true },
} );
} );
} );
diff --git a/packages/block-editor/src/utils/pasting.js b/packages/block-editor/src/utils/pasting.js
index e962e11050a1d9..f9612aad20f140 100644
--- a/packages/block-editor/src/utils/pasting.js
+++ b/packages/block-editor/src/utils/pasting.js
@@ -3,6 +3,51 @@
*/
import { getFilesFromDataTransfer } from '@wordpress/dom';
+/**
+ * Normalizes a given string of HTML to remove the Windows-specific "Fragment"
+ * comments and any preceding and trailing content.
+ *
+ * @param {string} html the html to be normalized
+ * @return {string} the normalized html
+ */
+function removeWindowsFragments( html ) {
+ const startStr = '';
+ const startIdx = html.indexOf( startStr );
+ if ( startIdx > -1 ) {
+ html = html.substring( startIdx + startStr.length );
+ } else {
+ // No point looking for EndFragment
+ return html;
+ }
+
+ const endStr = '';
+ const endIdx = html.indexOf( endStr );
+ if ( endIdx > -1 ) {
+ html = html.substring( 0, endIdx );
+ }
+
+ return html;
+}
+
+/**
+ * Removes the charset meta tag inserted by Chromium.
+ * See:
+ * - https://github.com/WordPress/gutenberg/issues/33585
+ * - https://bugs.chromium.org/p/chromium/issues/detail?id=1264616#c4
+ *
+ * @param {string} html the html to be stripped of the meta tag.
+ * @return {string} the cleaned html
+ */
+function removeCharsetMetaTag( html ) {
+ const metaTag = ` `;
+
+ if ( html.startsWith( metaTag ) ) {
+ return html.slice( metaTag.length );
+ }
+
+ return html;
+}
+
export function getPasteEventData( { clipboardData } ) {
let plainText = '';
let html = '';
@@ -24,6 +69,12 @@ export function getPasteEventData( { clipboardData } ) {
}
}
+ // Remove Windows-specific metadata appended within copied HTML text.
+ html = removeWindowsFragments( html );
+
+ // Strip meta tag.
+ html = removeCharsetMetaTag( html );
+
const files = getFilesFromDataTransfer( clipboardData );
if (
diff --git a/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap b/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap
index b55f74cfd7bb0b..b9815cdc700b38 100644
--- a/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap
+++ b/packages/block-editor/src/utils/transform-styles/transforms/test/__snapshots__/wrap.js.snap
@@ -22,6 +22,19 @@ color: red;
}"
`;
+exports[`CSS selector wrap should not double wrap selectors 1`] = `
+".my-namespace h1,
+.my-namespace .red {
+color: red;
+}"
+`;
+
+exports[`CSS selector wrap should replace :root selectors 1`] = `
+".my-namespace {
+--my-color: #ff0000;
+}"
+`;
+
exports[`CSS selector wrap should replace root tags 1`] = `
".my-namespace,
.my-namespace h1 {
@@ -49,9 +62,3 @@ color: red;
}
}"
`;
-
-exports[`CSS selector wrap should replace :root selectors 1`] = `
-".my-namespace {
---my-color: #ff0000;
-}"
-`;
diff --git a/packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js b/packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js
index c26bd3761212b1..a1f4f141d21c9b 100644
--- a/packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js
+++ b/packages/block-editor/src/utils/transform-styles/transforms/test/wrap.js
@@ -83,4 +83,13 @@ describe( 'CSS selector wrap', () => {
expect( output ).toMatchSnapshot();
} );
+
+ it( 'should not double wrap selectors', () => {
+ const callback = wrap( '.my-namespace' );
+ const input = ` .my-namespace h1, .red { color: red; }`;
+
+ const output = traverse( input, callback );
+
+ expect( output ).toMatchSnapshot();
+ } );
} );
diff --git a/packages/block-editor/src/utils/transform-styles/transforms/wrap.js b/packages/block-editor/src/utils/transform-styles/transforms/wrap.js
index e61c78dc7e452f..74b940f80352b9 100644
--- a/packages/block-editor/src/utils/transform-styles/transforms/wrap.js
+++ b/packages/block-editor/src/utils/transform-styles/transforms/wrap.js
@@ -27,6 +27,11 @@ const wrap =
return selector;
}
+ // Skip the update when a selector already has a namespace + space (" ").
+ if ( selector.trim().startsWith( `${ namespace } ` ) ) {
+ return selector;
+ }
+
// Anything other than a root tag is always prefixed.
{
if ( ! selector.match( IS_ROOT_TAG ) ) {
diff --git a/packages/block-editor/src/utils/use-notify-copy.js b/packages/block-editor/src/utils/use-notify-copy.js
new file mode 100644
index 00000000000000..0f98577f11bf65
--- /dev/null
+++ b/packages/block-editor/src/utils/use-notify-copy.js
@@ -0,0 +1,63 @@
+/**
+ * WordPress dependencies
+ */
+import { useCallback } from '@wordpress/element';
+import { store as blocksStore } from '@wordpress/blocks';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { __, _n, sprintf } from '@wordpress/i18n';
+import { store as noticesStore } from '@wordpress/notices';
+
+/**
+ * Internal dependencies
+ */
+import { store as blockEditorStore } from '../store';
+
+export function useNotifyCopy() {
+ const { getBlockName } = useSelect( blockEditorStore );
+ const { getBlockType } = useSelect( blocksStore );
+ const { createSuccessNotice } = useDispatch( noticesStore );
+
+ return useCallback( ( eventType, selectedBlockClientIds ) => {
+ let notice = '';
+ if ( selectedBlockClientIds.length === 1 ) {
+ const clientId = selectedBlockClientIds[ 0 ];
+ const title = getBlockType( getBlockName( clientId ) )?.title;
+ notice =
+ eventType === 'copy'
+ ? sprintf(
+ // Translators: Name of the block being copied, e.g. "Paragraph".
+ __( 'Copied "%s" to clipboard.' ),
+ title
+ )
+ : sprintf(
+ // Translators: Name of the block being cut, e.g. "Paragraph".
+ __( 'Moved "%s" to clipboard.' ),
+ title
+ );
+ } else {
+ notice =
+ eventType === 'copy'
+ ? sprintf(
+ // Translators: %d: Number of blocks being copied.
+ _n(
+ 'Copied %d block to clipboard.',
+ 'Copied %d blocks to clipboard.',
+ selectedBlockClientIds.length
+ ),
+ selectedBlockClientIds.length
+ )
+ : sprintf(
+ // Translators: %d: Number of blocks being cut.
+ _n(
+ 'Moved %d block to clipboard.',
+ 'Moved %d blocks to clipboard.',
+ selectedBlockClientIds.length
+ ),
+ selectedBlockClientIds.length
+ );
+ }
+ createSuccessNotice( notice, {
+ type: 'snackbar',
+ } );
+ }, [] );
+}
diff --git a/packages/block-library/package.json b/packages/block-library/package.json
index 6c64a94602ee2d..f80ac488dbc21d 100644
--- a/packages/block-library/package.json
+++ b/packages/block-library/package.json
@@ -69,7 +69,7 @@
"fast-deep-equal": "^3.1.3",
"memize": "^2.1.0",
"remove-accents": "^0.5.0",
- "uuid": "^8.3.0"
+ "uuid": "^9.0.1"
},
"peerDependencies": {
"react": "^18.0.0",
diff --git a/packages/block-library/src/audio/edit.native.js b/packages/block-library/src/audio/edit.native.js
index fbc39ba190d3e2..cbd7f9ff02f8fc 100644
--- a/packages/block-library/src/audio/edit.native.js
+++ b/packages/block-library/src/audio/edit.native.js
@@ -30,7 +30,7 @@ import { audio as icon, replace } from '@wordpress/icons';
import { useState } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
-import { isURL } from '@wordpress/url';
+import { isURL, getProtocol } from '@wordpress/url';
/**
* Internal dependencies
@@ -73,7 +73,7 @@ function AudioEdit( {
function onSelectURL( newSrc ) {
if ( newSrc !== src ) {
- if ( isURL( newSrc ) ) {
+ if ( isURL( newSrc ) && /^https?:/.test( getProtocol( newSrc ) ) ) {
setAttributes( { src: newSrc, id: undefined } );
} else {
createErrorNotice( __( 'Invalid URL. Audio file not found.' ) );
diff --git a/packages/block-library/src/audio/test/edit.native.js b/packages/block-library/src/audio/test/edit.native.js
index b59c36db2f66e6..c191fd2fff7989 100644
--- a/packages/block-library/src/audio/test/edit.native.js
+++ b/packages/block-library/src/audio/test/edit.native.js
@@ -1,13 +1,20 @@
/**
* External dependencies
*/
-import { render } from 'test/helpers';
+import {
+ addBlock,
+ dismissModal,
+ fireEvent,
+ initializeEditor,
+ render,
+ screen,
+ setupCoreBlocks,
+} from 'test/helpers';
/**
* WordPress dependencies
*/
import { BlockEdit } from '@wordpress/block-editor';
-import { registerBlockType, unregisterBlockType } from '@wordpress/blocks';
import {
subscribeMediaUpload,
sendMediaUpload,
@@ -16,7 +23,7 @@ import {
/**
* Internal dependencies
*/
-import { metadata, settings, name } from '../index';
+import { name } from '../index';
// react-native-aztec shouldn't be mocked because these tests are based on
// snapshot testing where we want to keep the original component.
@@ -42,18 +49,9 @@ const getTestComponentWithContent = ( attributes = {} ) => {
);
};
-describe( 'Audio block', () => {
- beforeAll( () => {
- registerBlockType( name, {
- ...metadata,
- ...settings,
- } );
- } );
-
- afterAll( () => {
- unregisterBlockType( name );
- } );
+setupCoreBlocks( [ 'core/audio' ] );
+describe( 'Audio block', () => {
it( 'renders placeholder without crashing', () => {
const component = getTestComponentWithContent();
const rendered = component.toJSON();
@@ -86,4 +84,20 @@ describe( 'Audio block', () => {
const rendered = component.toJSON();
expect( rendered ).toMatchSnapshot();
} );
+
+ it( 'should gracefully handle invalid URLs', async () => {
+ await initializeEditor();
+
+ await addBlock( screen, 'Audio' );
+ fireEvent.press( screen.getByText( 'Insert from URL' ) );
+ fireEvent.changeText(
+ screen.getByPlaceholderText( 'Type a URL' ),
+ 'h://wordpress.org/audio.mp3'
+ );
+ dismissModal( screen.getByTestId( 'bottom-sheet' ) );
+
+ expect(
+ screen.getByText( 'Invalid URL. Audio file not found.' )
+ ).toBeVisible();
+ } );
} );
diff --git a/packages/block-library/src/button/constants.js b/packages/block-library/src/button/constants.js
new file mode 100644
index 00000000000000..66fdc66594b2a0
--- /dev/null
+++ b/packages/block-library/src/button/constants.js
@@ -0,0 +1,3 @@
+export const NEW_TAB_REL = 'noreferrer noopener';
+export const NEW_TAB_TARGET = '_blank';
+export const NOFOLLOW_REL = 'nofollow';
diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js
index 0c1892c6e4c702..81861e44997a4a 100644
--- a/packages/block-library/src/button/edit.js
+++ b/packages/block-library/src/button/edit.js
@@ -3,6 +3,12 @@
*/
import classnames from 'classnames';
+/**
+ * Internal dependencies
+ */
+import { NEW_TAB_TARGET, NOFOLLOW_REL } from './constants';
+import { getUpdatedLinkAttributes } from './get-updated-link-attributes';
+
/**
* WordPress dependencies
*/
@@ -32,9 +38,14 @@ import { displayShortcut, isKeyboardEvent } from '@wordpress/keycodes';
import { link, linkOff } from '@wordpress/icons';
import { createBlock } from '@wordpress/blocks';
import { useMergeRefs } from '@wordpress/compose';
-import { prependHTTP } from '@wordpress/url';
-const NEW_TAB_REL = 'noreferrer noopener';
+const LINK_SETTINGS = [
+ ...LinkControl.DEFAULT_LINK_SETTINGS,
+ {
+ id: 'nofollow',
+ title: __( 'Mark as nofollow' ),
+ },
+];
function WidthPanel( { selectedWidth, setAttributes } ) {
function handleChange( newWidth ) {
@@ -92,22 +103,6 @@ function ButtonEdit( props ) {
const TagName = tagName || 'a';
- function onToggleOpenInNewTab( value ) {
- const newLinkTarget = value ? '_blank' : undefined;
-
- let updatedRel = rel;
- if ( newLinkTarget && ! rel ) {
- updatedRel = NEW_TAB_REL;
- } else if ( ! newLinkTarget && rel === NEW_TAB_REL ) {
- updatedRel = undefined;
- }
-
- setAttributes( {
- linkTarget: newLinkTarget,
- rel: updatedRel,
- } );
- }
-
function setButtonText( newText ) {
// Remove anchor tags from button text content.
setAttributes( { text: newText.replace( /<\/?a[^>]*>/g, '' ) } );
@@ -138,7 +133,8 @@ function ButtonEdit( props ) {
const [ isEditingURL, setIsEditingURL ] = useState( false );
const isURLSet = !! url;
- const opensInNewTab = linkTarget === '_blank';
+ const opensInNewTab = linkTarget === NEW_TAB_TARGET;
+ const nofollow = !! rel?.includes( NOFOLLOW_REL );
const isLinkTag = 'a' === TagName;
function startEditing( event ) {
@@ -164,8 +160,8 @@ function ButtonEdit( props ) {
// Memoize link value to avoid overriding the LinkControl's internal state.
// This is a temporary fix. See https://github.com/WordPress/gutenberg/issues/51256.
const linkValue = useMemo(
- () => ( { url, opensInNewTab } ),
- [ url, opensInNewTab ]
+ () => ( { url, opensInNewTab, nofollow } ),
+ [ url, opensInNewTab, nofollow ]
);
return (
@@ -256,20 +252,25 @@ function ButtonEdit( props ) {
{
- setAttributes( { url: prependHTTP( newURL ) } );
-
- if ( opensInNewTab !== newOpensInNewTab ) {
- onToggleOpenInNewTab( newOpensInNewTab );
- }
- } }
+ nofollow: newNofollow,
+ } ) =>
+ setAttributes(
+ getUpdatedLinkAttributes( {
+ rel,
+ url: newURL,
+ opensInNewTab: newOpensInNewTab,
+ nofollow: newNofollow,
+ } )
+ )
+ }
onRemove={ () => {
unlink();
richTextRef.current?.focus();
} }
forceIsEditingLink={ isEditingURL }
+ settings={ LINK_SETTINGS }
/>
) }
diff --git a/packages/block-library/src/button/get-updated-link-attributes.js b/packages/block-library/src/button/get-updated-link-attributes.js
new file mode 100644
index 00000000000000..97b837a8be20bd
--- /dev/null
+++ b/packages/block-library/src/button/get-updated-link-attributes.js
@@ -0,0 +1,54 @@
+/**
+ * Internal dependencies
+ */
+import { NEW_TAB_REL, NEW_TAB_TARGET, NOFOLLOW_REL } from './constants';
+
+/**
+ * WordPress dependencies
+ */
+import { prependHTTP } from '@wordpress/url';
+
+/**
+ * Updates the link attributes.
+ *
+ * @param {Object} attributes The current block attributes.
+ * @param {string} attributes.rel The current link rel attribute.
+ * @param {string} attributes.url The current link url.
+ * @param {boolean} attributes.opensInNewTab Whether the link should open in a new window.
+ * @param {boolean} attributes.nofollow Whether the link should be marked as nofollow.
+ */
+export function getUpdatedLinkAttributes( {
+ rel = '',
+ url = '',
+ opensInNewTab,
+ nofollow,
+} ) {
+ let newLinkTarget;
+ // Since `rel` is editable attribute, we need to check for existing values and proceed accordingly.
+ let updatedRel = rel;
+
+ if ( opensInNewTab ) {
+ newLinkTarget = NEW_TAB_TARGET;
+ updatedRel = updatedRel?.includes( NEW_TAB_REL )
+ ? updatedRel
+ : updatedRel + ` ${ NEW_TAB_REL }`;
+ } else {
+ const relRegex = new RegExp( `\\b${ NEW_TAB_REL }\\s*`, 'g' );
+ updatedRel = updatedRel?.replace( relRegex, '' ).trim();
+ }
+
+ if ( nofollow ) {
+ updatedRel = updatedRel?.includes( NOFOLLOW_REL )
+ ? updatedRel
+ : updatedRel + ` ${ NOFOLLOW_REL }`;
+ } else {
+ const relRegex = new RegExp( `\\b${ NOFOLLOW_REL }\\s*`, 'g' );
+ updatedRel = updatedRel?.replace( relRegex, '' ).trim();
+ }
+
+ return {
+ url: prependHTTP( url ),
+ linkTarget: newLinkTarget,
+ rel: updatedRel || undefined,
+ };
+}
diff --git a/packages/block-library/src/button/test/get-updated-link-attributes.js b/packages/block-library/src/button/test/get-updated-link-attributes.js
new file mode 100644
index 00000000000000..af0c3da332110c
--- /dev/null
+++ b/packages/block-library/src/button/test/get-updated-link-attributes.js
@@ -0,0 +1,113 @@
+/**
+ * Internal dependencies
+ */
+import { getUpdatedLinkAttributes } from '../get-updated-link-attributes';
+
+describe( 'getUpdatedLinkAttributes method', () => {
+ it( 'should correctly handle unassigned rel', () => {
+ const options = {
+ url: 'example.com',
+ opensInNewTab: true,
+ nofollow: false,
+ };
+
+ const result = getUpdatedLinkAttributes( options );
+
+ expect( result.url ).toEqual( 'http://example.com' );
+ expect( result.linkTarget ).toEqual( '_blank' );
+ expect( result.rel ).toEqual( 'noreferrer noopener' );
+ } );
+
+ it( 'should return empty rel value as undefined', () => {
+ const options = {
+ url: 'example.com',
+ opensInNewTab: false,
+ nofollow: false,
+ };
+
+ const result = getUpdatedLinkAttributes( options );
+
+ expect( result.url ).toEqual( 'http://example.com' );
+ expect( result.linkTarget ).toEqual( undefined );
+ expect( result.rel ).toEqual( undefined );
+ } );
+
+ it( 'should correctly handle rel with existing values', () => {
+ const options = {
+ url: 'example.com',
+ opensInNewTab: true,
+ nofollow: true,
+ rel: 'rel_value',
+ };
+
+ const result = getUpdatedLinkAttributes( options );
+
+ expect( result.url ).toEqual( 'http://example.com' );
+ expect( result.linkTarget ).toEqual( '_blank' );
+ expect( result.rel ).toEqual(
+ 'rel_value noreferrer noopener nofollow'
+ );
+ } );
+
+ it( 'should correctly update link attributes with opensInNewTab', () => {
+ const options = {
+ url: 'example.com',
+ opensInNewTab: true,
+ nofollow: false,
+ rel: 'rel_value',
+ };
+
+ const result = getUpdatedLinkAttributes( options );
+
+ expect( result.url ).toEqual( 'http://example.com' );
+ expect( result.linkTarget ).toEqual( '_blank' );
+ expect( result.rel ).toEqual( 'rel_value noreferrer noopener' );
+ } );
+
+ it( 'should correctly update link attributes with nofollow', () => {
+ const options = {
+ url: 'example.com',
+ opensInNewTab: false,
+ nofollow: true,
+ rel: 'rel_value',
+ };
+
+ const result = getUpdatedLinkAttributes( options );
+
+ expect( result.url ).toEqual( 'http://example.com' );
+ expect( result.linkTarget ).toEqual( undefined );
+ expect( result.rel ).toEqual( 'rel_value nofollow' );
+ } );
+
+ it( 'should correctly handle rel with existing nofollow values and remove duplicates', () => {
+ const options = {
+ url: 'example.com',
+ opensInNewTab: true,
+ nofollow: true,
+ rel: 'rel_value nofollow',
+ };
+
+ const result = getUpdatedLinkAttributes( options );
+
+ expect( result.url ).toEqual( 'http://example.com' );
+ expect( result.linkTarget ).toEqual( '_blank' );
+ expect( result.rel ).toEqual(
+ 'rel_value nofollow noreferrer noopener'
+ );
+ } );
+
+ it( 'should correctly handle rel with existing new tab values and remove duplicates', () => {
+ const options = {
+ url: 'example.com',
+ opensInNewTab: true,
+ nofollow: false,
+ rel: 'rel_value noreferrer noopener',
+ };
+
+ const result = getUpdatedLinkAttributes( options );
+
+ expect( result.url ).toEqual( 'http://example.com' );
+ expect( result.linkTarget ).toEqual( '_blank' );
+ expect( result.rel ).toEqual( 'rel_value noreferrer noopener' );
+ } );
+} );
diff --git a/packages/block-library/src/categories/index.php b/packages/block-library/src/categories/index.php
index 7e3979b7aefe2e..c35376505b134f 100644
--- a/packages/block-library/src/categories/index.php
+++ b/packages/block-library/src/categories/index.php
@@ -70,8 +70,7 @@ function render_block_core_categories( $attributes ) {
function build_dropdown_script_block_core_categories( $dropdown_id ) {
ob_start();
?>
-
', '' ), '', ob_get_clean() ) );
}
/**
diff --git a/packages/block-library/src/comment-template/edit.js b/packages/block-library/src/comment-template/edit.js
index 3d33428f6f4585..50d83289e1ed92 100644
--- a/packages/block-library/src/comment-template/edit.js
+++ b/packages/block-library/src/comment-template/edit.js
@@ -105,7 +105,7 @@ const getCommentsPlaceholder = ( {
* @param {Array} [props.firstCommentId] - ID of the first comment in the array.
* @param {Array} [props.blocks] - Array of blocks returned from
* getBlocks() in parent .
- * @return {WPElement} Inner blocks of the Comment Template
+ * @return {Element} Inner blocks of the Comment Template
*/
function CommentTemplateInnerBlocks( {
comment,
@@ -202,7 +202,7 @@ const MemoizedCommentTemplatePreview = memo( CommentTemplatePreview );
* @param {Array} [props.blocks] - Array of blocks returned from getBlocks() in parent.
* @param {Object} [props.firstCommentId] - The ID of the first comment in the array of
* comment objects.
- * @return {WPElement} List of comments.
+ * @return {Element} List of comments.
*/
const CommentsList = ( {
comments,
diff --git a/packages/block-library/src/cover/block.json b/packages/block-library/src/cover/block.json
index e88dd2d65a3722..c186a2416f5c9e 100644
--- a/packages/block-library/src/cover/block.json
+++ b/packages/block-library/src/cover/block.json
@@ -42,6 +42,9 @@
"customOverlayColor": {
"type": "string"
},
+ "isUserOverlayColor": {
+ "type": "boolean"
+ },
"backgroundType": {
"type": "string",
"default": "image"
diff --git a/packages/block-library/src/cover/deprecated.js b/packages/block-library/src/cover/deprecated.js
index d1801a11ade9de..fc15cb4ac46d49 100644
--- a/packages/block-library/src/cover/deprecated.js
+++ b/packages/block-library/src/cover/deprecated.js
@@ -97,7 +97,7 @@ const blockAttributes = {
},
};
-const v8ToV10BlockAttributes = {
+const v8ToV11BlockAttributes = {
url: {
type: 'string',
},
@@ -164,7 +164,19 @@ const v8ToV10BlockAttributes = {
},
};
-const v7toV10BlockSupports = {
+const v12BlockAttributes = {
+ ...v8ToV11BlockAttributes,
+ useFeaturedImage: {
+ type: 'boolean',
+ default: false,
+ },
+ tagName: {
+ type: 'string',
+ default: 'div',
+ },
+};
+
+const v7toV11BlockSupports = {
anchor: true,
align: true,
html: false,
@@ -182,10 +194,222 @@ const v7toV10BlockSupports = {
},
};
+const v12BlockSupports = {
+ ...v7toV11BlockSupports,
+ spacing: {
+ padding: true,
+ margin: [ 'top', 'bottom' ],
+ blockGap: true,
+ __experimentalDefaultControls: {
+ padding: true,
+ blockGap: true,
+ },
+ },
+ __experimentalBorder: {
+ color: true,
+ radius: true,
+ style: true,
+ width: true,
+ __experimentalDefaultControls: {
+ color: true,
+ radius: true,
+ style: true,
+ width: true,
+ },
+ },
+ color: {
+ __experimentalDuotone:
+ '> .wp-block-cover__image-background, > .wp-block-cover__video-background',
+ heading: true,
+ text: true,
+ background: false,
+ __experimentalSkipSerialization: [ 'gradients' ],
+ enableContrastChecker: false,
+ },
+ typography: {
+ fontSize: true,
+ lineHeight: true,
+ __experimentalFontFamily: true,
+ __experimentalFontWeight: true,
+ __experimentalFontStyle: true,
+ __experimentalTextTransform: true,
+ __experimentalTextDecoration: true,
+ __experimentalLetterSpacing: true,
+ __experimentalDefaultControls: {
+ fontSize: true,
+ },
+ },
+ layout: {
+ allowJustification: false,
+ },
+};
+
+// Deprecation for blocks to prevent auto overlay color from overriding previously set values.
+const v12 = {
+ attributes: v12BlockAttributes,
+ supports: v12BlockSupports,
+ isEligible( attributes ) {
+ return (
+ attributes.customOverlayColor !== undefined ||
+ attributes.overlayColor !== undefined
+ );
+ },
+ migrate( attributes ) {
+ return {
+ ...attributes,
+ isUserOverlayColor: true,
+ };
+ },
+ save( { attributes } ) {
+ const {
+ backgroundType,
+ gradient,
+ contentPosition,
+ customGradient,
+ customOverlayColor,
+ dimRatio,
+ focalPoint,
+ useFeaturedImage,
+ hasParallax,
+ isDark,
+ isRepeated,
+ overlayColor,
+ url,
+ alt,
+ id,
+ minHeight: minHeightProp,
+ minHeightUnit,
+ tagName: Tag,
+ } = attributes;
+ const overlayColorClass = getColorClassName(
+ 'background-color',
+ overlayColor
+ );
+ const gradientClass = __experimentalGetGradientClass( gradient );
+ const minHeight =
+ minHeightProp && minHeightUnit
+ ? `${ minHeightProp }${ minHeightUnit }`
+ : minHeightProp;
+
+ const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType;
+ const isVideoBackground = VIDEO_BACKGROUND_TYPE === backgroundType;
+
+ const isImgElement = ! ( hasParallax || isRepeated );
+
+ const style = {
+ minHeight: minHeight || undefined,
+ };
+
+ const bgStyle = {
+ backgroundColor: ! overlayColorClass
+ ? customOverlayColor
+ : undefined,
+ background: customGradient ? customGradient : undefined,
+ };
+
+ const objectPosition =
+ // prettier-ignore
+ focalPoint && isImgElement
+ ? mediaPosition(focalPoint)
+ : undefined;
+
+ const backgroundImage = url ? `url(${ url })` : undefined;
+
+ const backgroundPosition = mediaPosition( focalPoint );
+
+ const classes = classnames(
+ {
+ 'is-light': ! isDark,
+ 'has-parallax': hasParallax,
+ 'is-repeated': isRepeated,
+ 'has-custom-content-position':
+ ! isContentPositionCenter( contentPosition ),
+ },
+ getPositionClassName( contentPosition )
+ );
+
+ const imgClasses = classnames(
+ 'wp-block-cover__image-background',
+ id ? `wp-image-${ id }` : null,
+ {
+ 'has-parallax': hasParallax,
+ 'is-repeated': isRepeated,
+ }
+ );
+
+ const gradientValue = gradient || customGradient;
+
+ return (
+
+
+
+ { ! useFeaturedImage &&
+ isImageBackground &&
+ url &&
+ ( isImgElement ? (
+
+ ) : (
+
+ ) ) }
+ { isVideoBackground && url && (
+
+ ) }
+
+
+ );
+ },
+};
+
// Deprecation for blocks that does not have a HTML tag option.
const v11 = {
- attributes: v8ToV10BlockAttributes,
- supports: v7toV10BlockSupports,
+ attributes: v8ToV11BlockAttributes,
+ supports: v7toV11BlockSupports,
save( { attributes } ) {
const {
backgroundType,
@@ -334,8 +558,8 @@ const v11 = {
// Deprecation for blocks that renders fixed background as backgroud from the main block container.
const v10 = {
- attributes: v8ToV10BlockAttributes,
- supports: v7toV10BlockSupports,
+ attributes: v8ToV11BlockAttributes,
+ supports: v7toV11BlockSupports,
save( { attributes } ) {
const {
backgroundType,
@@ -471,8 +695,8 @@ const v10 = {
// Deprecation for blocks with `minHeightUnit` set but no `minHeight`.
const v9 = {
- attributes: v8ToV10BlockAttributes,
- supports: v7toV10BlockSupports,
+ attributes: v8ToV11BlockAttributes,
+ supports: v7toV11BlockSupports,
save( { attributes } ) {
const {
backgroundType,
@@ -603,8 +827,8 @@ const v9 = {
// v8: deprecated to remove duplicated gradient classes and swap `wp-block-cover__gradient-background` for `wp-block-cover__background`.
const v8 = {
- attributes: v8ToV10BlockAttributes,
- supports: v7toV10BlockSupports,
+ attributes: v8ToV11BlockAttributes,
+ supports: v7toV11BlockSupports,
save( { attributes } ) {
const {
backgroundType,
@@ -758,7 +982,7 @@ const v7 = {
default: '',
},
},
- supports: v7toV10BlockSupports,
+ supports: v7toV11BlockSupports,
save( { attributes } ) {
const {
backgroundType,
@@ -1449,4 +1673,4 @@ const v1 = {
},
};
-export default [ v11, v10, v9, v8, v7, v6, v5, v4, v3, v2, v1 ];
+export default [ v12, v11, v10, v9, v8, v7, v6, v5, v4, v3, v2, v1 ];
diff --git a/packages/block-library/src/cover/edit.native.js b/packages/block-library/src/cover/edit.native.js
index a34476d67180a9..81ff43128b1a35 100644
--- a/packages/block-library/src/cover/edit.native.js
+++ b/packages/block-library/src/cover/edit.native.js
@@ -207,8 +207,15 @@ const Cover = ( {
const onSelectMedia = ( media ) => {
setDidUploadFail( false );
- const onSelect = attributesFromMedia( setAttributes, dimRatio );
- onSelect( media );
+
+ const mediaAttributes = attributesFromMedia( media );
+ setAttributes( {
+ ...mediaAttributes,
+ focalPoint: undefined,
+ useFeaturedImage: undefined,
+ dimRatio: dimRatio === 100 ? 50 : dimRatio,
+ isDark: undefined,
+ } );
};
const onMediaPressed = () => {
diff --git a/packages/block-library/src/cover/edit/color-utils.js b/packages/block-library/src/cover/edit/color-utils.js
new file mode 100644
index 00000000000000..de62b5dfbb014e
--- /dev/null
+++ b/packages/block-library/src/cover/edit/color-utils.js
@@ -0,0 +1,122 @@
+/**
+ * External dependencies
+ */
+import { colord, extend } from 'colord';
+import namesPlugin from 'colord/plugins/names';
+import { FastAverageColor } from 'fast-average-color';
+import memoize from 'memize';
+
+/**
+ * WordPress dependencies
+ */
+import { applyFilters } from '@wordpress/hooks';
+
+/**
+ * @typedef {import('colord').RgbaColor} RgbaColor
+ */
+
+extend( [ namesPlugin ] );
+
+/**
+ * Fallback color when the average color can't be computed. The image may be
+ * rendering as transparent, and most sites have a light color background.
+ */
+export const DEFAULT_BACKGROUND_COLOR = '#FFF';
+
+/**
+ * Default dim color specified in style.css.
+ */
+export const DEFAULT_OVERLAY_COLOR = '#000';
+
+/**
+ * Performs a Porter Duff composite source over operation on two rgba colors.
+ *
+ * @see {@link https://www.w3.org/TR/compositing-1/#porterduffcompositingoperators_srcover}
+ *
+ * @param {RgbaColor} source Source color.
+ * @param {RgbaColor} dest Destination color.
+ *
+ * @return {RgbaColor} Composite color.
+ */
+export function compositeSourceOver( source, dest ) {
+ return {
+ r: source.r * source.a + dest.r * dest.a * ( 1 - source.a ),
+ g: source.g * source.a + dest.g * dest.a * ( 1 - source.a ),
+ b: source.b * source.a + dest.b * dest.a * ( 1 - source.a ),
+ a: source.a + dest.a * ( 1 - source.a ),
+ };
+}
+
+/**
+ * Retrieves the FastAverageColor singleton.
+ *
+ * @return {FastAverageColor} The FastAverageColor singleton.
+ */
+export function retrieveFastAverageColor() {
+ if ( ! retrieveFastAverageColor.fastAverageColor ) {
+ retrieveFastAverageColor.fastAverageColor = new FastAverageColor();
+ }
+ return retrieveFastAverageColor.fastAverageColor;
+}
+
+/**
+ * Computes the average color of an image.
+ *
+ * @param {string} url The url of the image.
+ *
+ * @return {Promise} Promise of an average color as a hex string.
+ */
+export const getMediaColor = memoize( async ( url ) => {
+ if ( ! url ) {
+ return DEFAULT_BACKGROUND_COLOR;
+ }
+
+ // making the default color rgb for compat with FAC
+ const { r, g, b, a } = colord( DEFAULT_BACKGROUND_COLOR ).toRgb();
+
+ try {
+ const imgCrossOrigin = applyFilters(
+ 'media.crossOrigin',
+ undefined,
+ url
+ );
+ const color = await retrieveFastAverageColor().getColorAsync( url, {
+ // The default color is white, which is the color
+ // that is returned if there's an error.
+ // colord returns alpga 0-1, FAC needs 0-255
+ defaultColor: [ r, g, b, a * 255 ],
+ // Errors that come up don't reject the promise,
+ // so error logging has to be silenced
+ // with this option.
+ silent: process.env.NODE_ENV === 'production',
+ crossOrigin: imgCrossOrigin,
+ } );
+ return color.hex;
+ } catch ( error ) {
+ // If there's an error return the fallback color.
+ return DEFAULT_BACKGROUND_COLOR;
+ }
+} );
+
+/**
+ * Computes if the color combination of the overlay and background color is dark.
+ *
+ * @param {number} dimRatio Opacity of the overlay between 0 and 100.
+ * @param {string} overlayColor CSS color string for the overlay.
+ * @param {string} backgroundColor CSS color string for the background.
+ *
+ * @return {boolean} true if the color combination composite result is dark.
+ */
+export function compositeIsDark( dimRatio, overlayColor, backgroundColor ) {
+ // Opacity doesn't matter if you're overlaying the same color on top of itself.
+ // And background doesn't matter when overlay is fully opaque.
+ if ( overlayColor === backgroundColor || dimRatio === 100 ) {
+ return colord( overlayColor ).isDark();
+ }
+ const overlay = colord( overlayColor )
+ .alpha( dimRatio / 100 )
+ .toRgb();
+ const background = colord( backgroundColor ).toRgb();
+ const composite = compositeSourceOver( overlay, background );
+ return colord( composite ).isDark();
+}
diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js
index 932281b1ab6f1b..6fac6e8b4a5060 100644
--- a/packages/block-library/src/cover/edit/index.js
+++ b/packages/block-library/src/cover/edit/index.js
@@ -2,8 +2,6 @@
* External dependencies
*/
import classnames from 'classnames';
-import { extend } from 'colord';
-import namesPlugin from 'colord/plugins/names';
/**
* WordPress dependencies
@@ -37,14 +35,17 @@ import {
isContentPositionCenter,
getPositionClassName,
mediaPosition,
- getCoverIsDark,
} from '../shared';
import CoverInspectorControls from './inspector-controls';
import CoverBlockControls from './block-controls';
import CoverPlaceholder from './cover-placeholder';
import ResizableCoverPopover from './resizable-cover-popover';
-
-extend( [ namesPlugin ] );
+import {
+ getMediaColor,
+ compositeIsDark,
+ DEFAULT_BACKGROUND_COLOR,
+ DEFAULT_OVERLAY_COLOR,
+} from './color-utils';
function getInnerBlocksTemplate( attributes ) {
return [
@@ -83,6 +84,8 @@ function CoverEdit( {
const {
contentPosition,
id,
+ url: originalUrl,
+ backgroundType: originalBackgroundType,
useFeaturedImage,
dimRatio,
focalPoint,
@@ -95,6 +98,7 @@ function CoverEdit( {
allowedBlocks,
templateLock,
tagName: TagName = 'div',
+ isUserOverlayColor,
} = attributes;
const [ featuredImage ] = useEntityProp(
@@ -114,23 +118,34 @@ function CoverEdit( {
);
const mediaUrl = media?.source_url;
+ // User can change the featured image outside of the block, but we still
+ // need to update the block when that happens. This effect should only
+ // run when the featured image changes in that case. All other cases are
+ // handled in their respective callbacks.
useEffect( () => {
- async function setIsDark() {
- __unstableMarkNextChangeAsNotPersistent();
- const isDarkSetting = await getCoverIsDark(
- mediaUrl,
+ ( async () => {
+ if ( ! useFeaturedImage ) {
+ return;
+ }
+
+ const averageBackgroundColor = await getMediaColor( mediaUrl );
+
+ let newOverlayColor = overlayColor.color;
+ if ( ! isUserOverlayColor ) {
+ newOverlayColor = averageBackgroundColor;
+ __unstableMarkNextChangeAsNotPersistent();
+ setOverlayColor( newOverlayColor );
+ }
+
+ const newIsDark = compositeIsDark(
dimRatio,
- overlayColor.color
+ newOverlayColor,
+ averageBackgroundColor
);
- setAttributes( {
- isDark: isDarkSetting,
- } );
- }
- if ( useFeaturedImage ) {
- setIsDark();
- }
- // We only ever want to run this effect if the mediaUrl changes.
- // All other changes to the isDark state are handled in the appropriate event handlers.
+ __unstableMarkNextChangeAsNotPersistent();
+ setAttributes( { isDark: newIsDark } );
+ } )();
+ // Disable reason: Update the block only when the featured image changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ mediaUrl ] );
@@ -141,32 +156,62 @@ function CoverEdit( {
const url = useFeaturedImage
? mediaUrl
: // Ensure the url is not malformed due to sanitization through `wp_kses`.
- attributes.url?.replaceAll( '&', '&' );
+ originalUrl?.replaceAll( '&', '&' );
const backgroundType = useFeaturedImage
? IMAGE_BACKGROUND_TYPE
- : attributes.backgroundType;
+ : originalBackgroundType;
const { createErrorNotice } = useDispatch( noticesStore );
const { gradientClass, gradientValue } = __experimentalUseGradient();
- const setMedia = attributesFromMedia( setAttributes, dimRatio );
const onSelectMedia = async ( newMedia ) => {
- // Only pass the url to getCoverIsDark if the media is an image as video is not handled.
- const newUrl = newMedia?.type === 'image' ? newMedia.url : undefined;
- const isDarkSetting = await getCoverIsDark(
- newUrl,
- dimRatio,
- overlayColor.color
+ const mediaAttributes = attributesFromMedia( newMedia );
+
+ const averageBackgroundColor = await getMediaColor(
+ newMedia?.type === IMAGE_BACKGROUND_TYPE ? newMedia?.url : undefined
+ );
+
+ let newOverlayColor = overlayColor.color;
+ if ( ! isUserOverlayColor ) {
+ newOverlayColor = averageBackgroundColor;
+ setOverlayColor( newOverlayColor );
+
+ // Make undo revert the next setAttributes and the previous setOverlayColor.
+ __unstableMarkNextChangeAsNotPersistent();
+ }
+
+ const newDimRatio = dimRatio === 100 ? 50 : dimRatio;
+ const newIsDark = compositeIsDark(
+ newDimRatio,
+ newOverlayColor,
+ averageBackgroundColor
);
- setMedia( newMedia, isDarkSetting );
+
+ setAttributes( {
+ ...mediaAttributes,
+ focalPoint: undefined,
+ useFeaturedImage: undefined,
+ dimRatio: newDimRatio,
+ isDark: newIsDark,
+ } );
};
- const onClearMedia = async () => {
- const isDarkSetting = await getCoverIsDark(
- undefined,
+ const onClearMedia = () => {
+ let newOverlayColor = overlayColor.color;
+ if ( ! isUserOverlayColor ) {
+ newOverlayColor = DEFAULT_OVERLAY_COLOR;
+ setOverlayColor( undefined );
+
+ // Make undo revert the next setAttributes and the previous setOverlayColor.
+ __unstableMarkNextChangeAsNotPersistent();
+ }
+
+ const newIsDark = compositeIsDark(
dimRatio,
- overlayColor.color
+ newOverlayColor,
+ DEFAULT_BACKGROUND_COLOR
);
+
setAttributes( {
url: undefined,
id: undefined,
@@ -174,39 +219,50 @@ function CoverEdit( {
focalPoint: undefined,
hasParallax: undefined,
isRepeated: undefined,
- useFeaturedImage: false,
- isDark: isDarkSetting,
+ useFeaturedImage: undefined,
+ isDark: newIsDark,
} );
};
- const onSetOverlayColor = async ( colorValue ) => {
- const isDarkSetting = await getCoverIsDark( url, dimRatio, colorValue );
- setOverlayColor( colorValue );
+ const onSetOverlayColor = async ( newOverlayColor ) => {
+ const averageBackgroundColor = await getMediaColor( url );
+ const newIsDark = compositeIsDark(
+ dimRatio,
+ newOverlayColor,
+ averageBackgroundColor
+ );
+
+ setOverlayColor( newOverlayColor );
+
+ // Make undo revert the next setAttributes and the previous setOverlayColor.
__unstableMarkNextChangeAsNotPersistent();
+
setAttributes( {
- isDark: isDarkSetting,
+ isUserOverlayColor: true,
+ isDark: newIsDark,
} );
};
const onUpdateDimRatio = async ( newDimRatio ) => {
- const isDarkSetting = await getCoverIsDark(
- url,
+ const averageBackgroundColor = await getMediaColor( url );
+ const newIsDark = compositeIsDark(
newDimRatio,
- overlayColor.color
+ overlayColor.color,
+ averageBackgroundColor
);
setAttributes( {
dimRatio: newDimRatio,
- isDark: isDarkSetting,
+ isDark: newIsDark,
} );
};
- const isUploadingMedia = isTemporaryMedia( id, url );
-
const onUploadError = ( message ) => {
createErrorNotice( message, { type: 'snackbar' } );
};
+ const isUploadingMedia = isTemporaryMedia( id, url );
+
const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType;
const isVideoBackground = VIDEO_BACKGROUND_TYPE === backgroundType;
@@ -285,18 +341,43 @@ function CoverEdit( {
};
const toggleUseFeaturedImage = async () => {
- const isDarkSetting = await ( useFeaturedImage
- ? getCoverIsDark( undefined, dimRatio, overlayColor.color )
- : getCoverIsDark( mediaUrl, dimRatio, overlayColor.color ) );
+ const newUseFeaturedImage = ! useFeaturedImage;
+
+ const averageBackgroundColor = newUseFeaturedImage
+ ? await getMediaColor( mediaUrl )
+ : DEFAULT_BACKGROUND_COLOR;
+
+ const newOverlayColor = ! isUserOverlayColor
+ ? averageBackgroundColor
+ : overlayColor.color;
+
+ if ( ! isUserOverlayColor ) {
+ if ( newUseFeaturedImage ) {
+ setOverlayColor( newOverlayColor );
+ } else {
+ setOverlayColor( undefined );
+ }
+
+ // Make undo revert the next setAttributes and the previous setOverlayColor.
+ __unstableMarkNextChangeAsNotPersistent();
+ }
+
+ const newDimRatio = dimRatio === 100 ? 50 : dimRatio;
+ const newIsDark = compositeIsDark(
+ newDimRatio,
+ newOverlayColor,
+ averageBackgroundColor
+ );
+
setAttributes( {
id: undefined,
url: undefined,
- useFeaturedImage: ! useFeaturedImage,
- dimRatio: dimRatio === 100 ? 50 : dimRatio,
+ useFeaturedImage: newUseFeaturedImage,
+ dimRatio: newDimRatio,
backgroundType: useFeaturedImage
? IMAGE_BACKGROUND_TYPE
: undefined,
- isDark: isDarkSetting,
+ isDark: newIsDark,
} );
};
diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js
index 02058243f9f781..ed2501a0d0ec52 100644
--- a/packages/block-library/src/cover/edit/inspector-controls.js
+++ b/packages/block-library/src/cover/edit/inspector-controls.js
@@ -149,7 +149,7 @@ export default function CoverInspectorControls( {
'The element should represent introductory content, typically a group of introductory or navigational aids.'
),
main: __(
- 'The element should be used for the primary content of your document only. '
+ 'The element should be used for the primary content of your document only.'
),
section: __(
"The element should represent a standalone portion of the document that can't be better represented by another element."
diff --git a/packages/block-library/src/cover/shared.js b/packages/block-library/src/cover/shared.js
index 72a42c0d5c70b8..5b59a28fbd00fa 100644
--- a/packages/block-library/src/cover/shared.js
+++ b/packages/block-library/src/cover/shared.js
@@ -1,14 +1,7 @@
-/**
- * External dependencies
- */
-import { FastAverageColor } from 'fast-average-color';
-import { colord } from 'colord';
-
/**
* WordPress dependencies
*/
import { getBlobTypeByURL, isBlobURL } from '@wordpress/blob';
-import { applyFilters } from '@wordpress/hooks';
const POSITION_CLASSNAMES = {
'top left': 'is-position-top-left',
@@ -41,50 +34,47 @@ export function dimRatioToClass( ratio ) {
: 'has-background-dim-' + 10 * Math.round( ratio / 10 );
}
-export function attributesFromMedia( setAttributes, dimRatio ) {
- return ( media, isDark ) => {
- if ( ! media || ! media.url ) {
- setAttributes( { url: undefined, id: undefined, isDark } );
- return;
- }
+export function attributesFromMedia( media ) {
+ if ( ! media || ! media.url ) {
+ return {
+ url: undefined,
+ id: undefined,
+ };
+ }
- if ( isBlobURL( media.url ) ) {
- media.type = getBlobTypeByURL( media.url );
- }
+ if ( isBlobURL( media.url ) ) {
+ media.type = getBlobTypeByURL( media.url );
+ }
- let mediaType;
- // For media selections originated from a file upload.
- if ( media.media_type ) {
- if ( media.media_type === IMAGE_BACKGROUND_TYPE ) {
- mediaType = IMAGE_BACKGROUND_TYPE;
- } else {
- // only images and videos are accepted so if the media_type is not an image we can assume it is a video.
- // Videos contain the media type of 'file' in the object returned from the rest api.
- mediaType = VIDEO_BACKGROUND_TYPE;
- }
+ let mediaType;
+ // For media selections originated from a file upload.
+ if ( media.media_type ) {
+ if ( media.media_type === IMAGE_BACKGROUND_TYPE ) {
+ mediaType = IMAGE_BACKGROUND_TYPE;
} else {
- // For media selections originated from existing files in the media library.
- if (
- media.type !== IMAGE_BACKGROUND_TYPE &&
- media.type !== VIDEO_BACKGROUND_TYPE
- ) {
- return;
- }
- mediaType = media.type;
+ // only images and videos are accepted so if the media_type is not an image we can assume it is a video.
+ // Videos contain the media type of 'file' in the object returned from the rest api.
+ mediaType = VIDEO_BACKGROUND_TYPE;
}
+ } else {
+ // For media selections originated from existing files in the media library.
+ if (
+ media.type !== IMAGE_BACKGROUND_TYPE &&
+ media.type !== VIDEO_BACKGROUND_TYPE
+ ) {
+ return;
+ }
+ mediaType = media.type;
+ }
- setAttributes( {
- isDark,
- dimRatio: dimRatio === 100 ? 50 : dimRatio,
- url: media.url,
- id: media.id,
- alt: media?.alt,
- backgroundType: mediaType,
- focalPoint: undefined,
- ...( mediaType === VIDEO_BACKGROUND_TYPE
- ? { hasParallax: undefined }
- : {} ),
- } );
+ return {
+ url: media.url,
+ id: media.id,
+ alt: media?.alt,
+ backgroundType: mediaType,
+ ...( mediaType === VIDEO_BACKGROUND_TYPE
+ ? { hasParallax: undefined }
+ : {} ),
};
}
@@ -117,86 +107,3 @@ export function getPositionClassName( contentPosition ) {
return POSITION_CLASSNAMES[ contentPosition ];
}
-
-/**
- * Performs a Porter Duff composite source over operation on two rgba colors.
- *
- * @see https://www.w3.org/TR/compositing-1/#porterduffcompositingoperators_srcover
- *
- * @param {import('colord').RgbaColor} source Source color.
- * @param {import('colord').RgbaColor} dest Destination color.
- * @return {import('colord').RgbaColor} Composite color.
- */
-function compositeSourceOver( source, dest ) {
- return {
- r: source.r * source.a + dest.r * dest.a * ( 1 - source.a ),
- g: source.g * source.a + dest.g * dest.a * ( 1 - source.a ),
- b: source.b * source.a + dest.b * dest.a * ( 1 - source.a ),
- a: source.a + dest.a * ( 1 - source.a ),
- };
-}
-
-function retrieveFastAverageColor() {
- if ( ! retrieveFastAverageColor.fastAverageColor ) {
- retrieveFastAverageColor.fastAverageColor = new FastAverageColor();
- }
- return retrieveFastAverageColor.fastAverageColor;
-}
-
-/**
- * This method evaluates if the cover block's background is dark or not and this boolean
- * can then be applied to the relevant attribute to help ensure that text is visible by default.
- * This needs to be recalculated in all of the following Cover block scenarios:
- * - When an overlay image is added, changed or removed
- * - When the featured image is selected as the overlay image, or removed from the overlay
- * - When the overlay color is changed
- * - When the overlay color is removed
- * - When the dimRatio is changed
- *
- * See the comments below for more details about which aspects take priority when
- * calculating the relative darkness of the Cover.
- *
- * @param {string} url
- * @param {number} dimRatio
- * @param {string} overlayColor
- * @return {Promise} True if cover should be considered to be dark.
- */
-export async function getCoverIsDark( url, dimRatio = 50, overlayColor ) {
- const overlay = colord( overlayColor )
- .alpha( dimRatio / 100 )
- .toRgb();
-
- if ( url ) {
- try {
- const imgCrossOrigin = applyFilters(
- 'media.crossOrigin',
- undefined,
- url
- );
- const {
- value: [ r, g, b, a ],
- } = await retrieveFastAverageColor().getColorAsync( url, {
- // Previously the default color was white, but that changed
- // in v6.0.0 so it has to be manually set now.
- defaultColor: [ 255, 255, 255, 255 ],
- // Errors that come up don't reject the promise, so error
- // logging has to be silenced with this option.
- silent: process.env.NODE_ENV === 'production',
- crossOrigin: imgCrossOrigin,
- } );
- // FAC uses 0-255 for alpha, but colord expects 0-1.
- const media = { r, g, b, a: a / 255 };
- const composite = compositeSourceOver( overlay, media );
- return colord( composite ).isDark();
- } catch ( error ) {
- // If there's an error, just assume the image is dark.
- return true;
- }
- }
-
- // Assume a white background because it isn't easy to get the actual
- // parent background color.
- const background = { r: 255, g: 255, b: 255, a: 1 };
- const composite = compositeSourceOver( overlay, background );
- return colord( composite ).isDark();
-}
diff --git a/packages/block-library/src/cover/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/cover/test/__snapshots__/edit.native.js.snap
index 09904a664294b3..25bd83daf06caf 100644
--- a/packages/block-library/src/cover/test/__snapshots__/edit.native.js.snap
+++ b/packages/block-library/src/cover/test/__snapshots__/edit.native.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`color settings clears the selected overlay color and mantains the inner blocks 1`] = `
-"
+"
@@ -17,7 +17,7 @@ exports[`color settings sets a color for the overlay background when the placeho
`;
exports[`color settings sets a gradient overlay background when a solid background was already selected 1`] = `
-"
+"
@@ -33,7 +33,7 @@ exports[`color settings toggles between solid colors and gradients 1`] = `
`;
exports[`minimum height settings changes the height value between units 1`] = `
-"
+"
@@ -41,7 +41,7 @@ exports[`minimum height settings changes the height value between units 1`] = `
`;
exports[`minimum height settings changes the height value to 20(vw) 1`] = `
-"
+"
@@ -49,7 +49,7 @@ exports[`minimum height settings changes the height value to 20(vw) 1`] = `
`;
exports[`minimum height settings disables the decrease button when reaching the minimum value for Pixels (px) 1`] = `
-"
+"
@@ -57,7 +57,7 @@ exports[`minimum height settings disables the decrease button when reaching the
`;
exports[`minimum height settings disables the decrease button when reaching the minimum value for Relative to parent font size (em) 1`] = `
-"
+"
@@ -65,7 +65,7 @@ exports[`minimum height settings disables the decrease button when reaching the
`;
exports[`minimum height settings disables the decrease button when reaching the minimum value for Relative to root font size (rem) 1`] = `
-"
+"
@@ -73,7 +73,7 @@ exports[`minimum height settings disables the decrease button when reaching the
`;
exports[`minimum height settings disables the decrease button when reaching the minimum value for Viewport height (vh) 1`] = `
-"
+"
@@ -81,7 +81,7 @@ exports[`minimum height settings disables the decrease button when reaching the
`;
exports[`minimum height settings disables the decrease button when reaching the minimum value for Viewport width (vw) 1`] = `
-"
+"
@@ -89,9 +89,17 @@ exports[`minimum height settings disables the decrease button when reaching the
`;
exports[`when an image is attached updates background opacity 1`] = `
-"
+"
"
`;
+
+exports[`when no media is attached and overlay color is set adds image 1`] = `
+"
+
+"
+`;
diff --git a/packages/block-library/src/cover/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/cover/test/__snapshots__/transforms.native.js.snap
index dc4ba0fbb2b1f8..1995da7a75876e 100644
--- a/packages/block-library/src/cover/test/__snapshots__/transforms.native.js.snap
+++ b/packages/block-library/src/cover/test/__snapshots__/transforms.native.js.snap
@@ -3,7 +3,7 @@
exports[`Cover block transformations with Image to Columns block 1`] = `
"
-
+
@@ -14,7 +14,7 @@ exports[`Cover block transformations with Image to Columns block 1`] = `
exports[`Cover block transformations with Image to Group block 1`] = `
"
-
+
@@ -39,7 +39,7 @@ exports[`Cover block transformations with Image to Media & Text block 1`] = `
exports[`Cover block transformations with Video to Columns block 1`] = `
"
-
+
@@ -50,7 +50,7 @@ exports[`Cover block transformations with Video to Columns block 1`] = `
exports[`Cover block transformations with Video to Group block 1`] = `
"
-
+
diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js
index 74db754ff16969..d1febe3c6fa115 100644
--- a/packages/block-library/src/cover/test/edit.js
+++ b/packages/block-library/src/cover/test/edit.js
@@ -64,9 +64,9 @@ describe( 'Cover block', () => {
await setup();
expect(
- screen.getByRole( 'group', {
- name: 'To edit this block, you need permission to upload media.',
- } )
+ within( screen.getByLabelText( 'Block: Cover' ) ).getByText(
+ 'To edit this block, you need permission to upload media.'
+ )
).toBeInTheDocument();
} );
@@ -260,7 +260,7 @@ describe( 'Cover block', () => {
} )
);
expect(
- within( screen.queryByLabelText( 'Block: Cover' ) ).queryByRole(
+ within( screen.getByLabelText( 'Block: Cover' ) ).queryByRole(
'img'
)
).not.toBeInTheDocument();
diff --git a/packages/block-library/src/cover/test/edit.native.js b/packages/block-library/src/cover/test/edit.native.js
index 3eedfc45ae123f..3ca2755ee1aebb 100644
--- a/packages/block-library/src/cover/test/edit.native.js
+++ b/packages/block-library/src/cover/test/edit.native.js
@@ -11,6 +11,8 @@ import {
within,
getBlock,
openBlockSettings,
+ setupMediaPicker,
+ setupPicker,
} from 'test/helpers';
/**
@@ -68,6 +70,13 @@ const COLOR_GRAY = '#abb8c3';
const GRADIENT_GREEN =
'linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%)';
+const MEDIA_OPTIONS = [
+ 'Choose from device',
+ 'Take a Photo',
+ 'Take a Video',
+ 'WordPress Media Library',
+];
+
// Simplified tree to render Cover edit within slot.
const CoverEdit = ( props ) => (
@@ -127,6 +136,35 @@ describe( 'when no media is attached', () => {
} );
} );
+describe( 'when no media is attached and overlay color is set', () => {
+ it( 'adds image', async () => {
+ const media = {
+ type: 'image',
+ id: 2000,
+ url: 'https://test.files.wordpress.com/local-image-1.mp4',
+ };
+ const { mediaPickerCallback } = setupMediaPicker();
+ const screen = await initializeEditor( {
+ initialHtml: COVER_BLOCK_SOLID_COLOR_HTML,
+ } );
+ const { getByText } = screen;
+ const { selectOption } = setupPicker( screen, MEDIA_OPTIONS );
+
+ // Get block
+ const coverBlock = await getBlock( screen, 'Cover' );
+ fireEvent.press( coverBlock );
+
+ // Open block settings
+ await openBlockSettings( screen );
+
+ fireEvent.press( getByText( 'Add image or video' ) );
+ selectOption( 'WordPress Media Library' );
+ await mediaPickerCallback( media );
+
+ expect( getEditorHtml() ).toMatchSnapshot();
+ } );
+} );
+
describe( 'when an image is attached', () => {
it( 'edits the image', async () => {
const screen = render(
diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js
index 28902020c75e8e..2945fb0fbe888b 100644
--- a/packages/block-library/src/embed/edit.js
+++ b/packages/block-library/src/embed/edit.js
@@ -29,6 +29,7 @@ import { useDispatch, useSelect } from '@wordpress/data';
import { useBlockProps } from '@wordpress/block-editor';
import { store as coreStore } from '@wordpress/core-data';
import { View } from '@wordpress/primitives';
+import { getAuthority } from '@wordpress/url';
const EmbedEdit = ( props ) => {
const {
@@ -137,6 +138,20 @@ const EmbedEdit = ( props ) => {
setAttributes( { url: newURL } );
}, [ preview?.html, attributesUrl, cannotEmbed, fetching ] );
+ // Try a different provider in case the embed url is not supported.
+ useEffect( () => {
+ if ( ! cannotEmbed || fetching || ! url ) {
+ return;
+ }
+
+ // Until X provider is supported in WordPress, as a workaround we use Twitter provider.
+ if ( getAuthority( url ) === 'x.com' ) {
+ const newURL = new URL( url );
+ newURL.host = 'twitter.com';
+ setAttributes( { url: newURL.toString() } );
+ }
+ }, [ url, cannotEmbed, fetching, setAttributes ] );
+
// Handle incoming preview.
useEffect( () => {
if ( preview && ! isEditingURL ) {
diff --git a/packages/block-library/src/embed/edit.native.js b/packages/block-library/src/embed/edit.native.js
index eec991c7b2037b..a04e49fbd6d54e 100644
--- a/packages/block-library/src/embed/edit.native.js
+++ b/packages/block-library/src/embed/edit.native.js
@@ -33,6 +33,7 @@ import {
} from '@wordpress/block-editor';
import { store as coreStore } from '@wordpress/core-data';
import { View } from '@wordpress/primitives';
+import { getAuthority } from '@wordpress/url';
// The inline preview feature will be released progressible, for this reason
// the embed will only be considered previewable for the following providers list.
@@ -160,6 +161,20 @@ const EmbedEdit = ( props ) => {
setAttributes( { url: newURL } );
}, [ preview?.html, url, cannotEmbed, fetching ] );
+ // Try a different provider in case the embed url is not supported.
+ useEffect( () => {
+ if ( ! cannotEmbed || fetching || ! url ) {
+ return;
+ }
+
+ // Until X provider is supported in WordPress, as a workaround we use Twitter provider.
+ if ( getAuthority( url ) === 'x.com' ) {
+ const newURL = new URL( url );
+ newURL.host = 'twitter.com';
+ setAttributes( { url: newURL.toString() } );
+ }
+ }, [ url, cannotEmbed, fetching, setAttributes ] );
+
// Handle incoming preview.
useEffect( () => {
if ( preview && ! isEditingURL ) {
diff --git a/packages/block-library/src/embed/test/index.native.js b/packages/block-library/src/embed/test/index.native.js
index 761aed2ea32210..1b456c668a9c52 100644
--- a/packages/block-library/src/embed/test/index.native.js
+++ b/packages/block-library/src/embed/test/index.native.js
@@ -138,7 +138,12 @@ const mockEmbedResponses = ( mockedResponses ) => {
async function mockOtherResponses( { path } ) {
if ( path.startsWith( '/wp/v2/themes' ) ) {
- return [ { theme_supports: { 'responsive-embeds': true } } ];
+ return [
+ {
+ stylesheet: 'test-theme',
+ theme_supports: { 'responsive-embeds': true },
+ },
+ ];
}
if ( path.startsWith( '/wp/v2/block-patterns/patterns' ) ) {
diff --git a/packages/block-library/src/embed/wp-embed-preview.js b/packages/block-library/src/embed/wp-embed-preview.js
index 0e812ffa647e3e..1992310217aca4 100644
--- a/packages/block-library/src/embed/wp-embed-preview.js
+++ b/packages/block-library/src/embed/wp-embed-preview.js
@@ -4,7 +4,7 @@
import { useMergeRefs, useFocusableIframe } from '@wordpress/compose';
import { useRef, useEffect, useMemo } from '@wordpress/element';
-/** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */
+/** @typedef {import('react').SyntheticEvent} SyntheticEvent */
const attributeMap = {
class: 'className',
diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php
index f8949b2701985f..83c6dab20157df 100644
--- a/packages/block-library/src/file/index.php
+++ b/packages/block-library/src/file/index.php
@@ -70,6 +70,8 @@ static function ( $matches ) {
* Ensure that the view script has the `wp-interactivity` dependency.
*
* @since 6.4.0
+ *
+ * @global WP_Scripts $wp_scripts
*/
function block_core_file_ensure_interactivity_dependency() {
global $wp_scripts;
diff --git a/packages/block-library/src/footnotes/block.json b/packages/block-library/src/footnotes/block.json
index 28b094f24f9164..4a2db992863db2 100644
--- a/packages/block-library/src/footnotes/block.json
+++ b/packages/block-library/src/footnotes/block.json
@@ -4,7 +4,7 @@
"name": "core/footnotes",
"title": "Footnotes",
"category": "text",
- "description": "",
+ "description": "Display footnotes added to the page.",
"keywords": [ "references" ],
"textdomain": "default",
"usesContext": [ "postId", "postType" ],
diff --git a/packages/block-library/src/footnotes/index.php b/packages/block-library/src/footnotes/index.php
index 5924db3a190c20..bc6291dd21c38b 100644
--- a/packages/block-library/src/footnotes/index.php
+++ b/packages/block-library/src/footnotes/index.php
@@ -39,15 +39,20 @@ function render_block_core_footnotes( $attributes, $content, $block ) {
}
$wrapper_attributes = get_block_wrapper_attributes();
+ $footnote_index = 1;
$block_content = '';
foreach ( $footnotes as $footnote ) {
+ // Translators: %d: Integer representing the number of return links on the page.
+ $aria_label = sprintf( __( 'Jump to footnote reference %1$d' ), $footnote_index );
$block_content .= sprintf(
- '%2$s ↩︎ ',
+ '%2$s ↩︎ ',
$footnote['id'],
- $footnote['content']
+ $footnote['content'],
+ $aria_label
);
+ ++$footnote_index;
}
return sprintf(
@@ -68,9 +73,10 @@ function register_block_core_footnotes() {
$post_type,
'footnotes',
array(
- 'show_in_rest' => true,
- 'single' => true,
- 'type' => 'string',
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ 'revisions_enabled' => true,
)
);
}
@@ -84,106 +90,7 @@ function register_block_core_footnotes() {
add_action( 'init', 'register_block_core_footnotes' );
/**
- * Saves the footnotes meta value to the revision.
- *
- * @since 6.3.0
- *
- * @param int $revision_id The revision ID.
- */
-function wp_save_footnotes_meta( $revision_id ) {
- $post_id = wp_is_post_revision( $revision_id );
-
- if ( $post_id ) {
- $footnotes = get_post_meta( $post_id, 'footnotes', true );
-
- if ( $footnotes ) {
- // Can't use update_post_meta() because it doesn't allow revisions.
- update_metadata( 'post', $revision_id, 'footnotes', wp_slash( $footnotes ) );
- }
- }
-}
-add_action( 'wp_after_insert_post', 'wp_save_footnotes_meta' );
-
-/**
- * Keeps track of the revision ID for "rest_after_insert_{$post_type}".
- *
- * @since 6.3.0
- *
- * @global int $wp_temporary_footnote_revision_id The footnote revision ID.
- *
- * @param int $revision_id The revision ID.
- */
-function wp_keep_footnotes_revision_id( $revision_id ) {
- global $wp_temporary_footnote_revision_id;
- $wp_temporary_footnote_revision_id = $revision_id;
-}
-add_action( '_wp_put_post_revision', 'wp_keep_footnotes_revision_id' );
-
-/**
- * This is a specific fix for the REST API. The REST API doesn't update
- * the post and post meta in one go (through `meta_input`). While it
- * does fix the `wp_after_insert_post` hook to be called correctly after
- * updating meta, it does NOT fix hooks such as post_updated and
- * save_post, which are normally also fired after post meta is updated
- * in `wp_insert_post()`. Unfortunately, `wp_save_post_revision` is
- * added to the `post_updated` action, which means the meta is not
- * available at the time, so we have to add it afterwards through the
- * `"rest_after_insert_{$post_type}"` action.
- *
- * @since 6.3.0
- *
- * @global int $wp_temporary_footnote_revision_id The footnote revision ID.
- *
- * @param WP_Post $post The post object.
- */
-function wp_add_footnotes_revisions_to_post_meta( $post ) {
- global $wp_temporary_footnote_revision_id;
-
- if ( $wp_temporary_footnote_revision_id ) {
- $revision = get_post( $wp_temporary_footnote_revision_id );
-
- if ( ! $revision ) {
- return;
- }
-
- $post_id = $revision->post_parent;
-
- // Just making sure we're updating the right revision.
- if ( $post->ID === $post_id ) {
- $footnotes = get_post_meta( $post_id, 'footnotes', true );
-
- if ( $footnotes ) {
- // Can't use update_post_meta() because it doesn't allow revisions.
- update_metadata( 'post', $wp_temporary_footnote_revision_id, 'footnotes', wp_slash( $footnotes ) );
- }
- }
- }
-}
-
-add_action( 'rest_after_insert_post', 'wp_add_footnotes_revisions_to_post_meta' );
-add_action( 'rest_after_insert_page', 'wp_add_footnotes_revisions_to_post_meta' );
-
-/**
- * Restores the footnotes meta value from the revision.
- *
- * @since 6.3.0
- *
- * @param int $post_id The post ID.
- * @param int $revision_id The revision ID.
- */
-function wp_restore_footnotes_from_revision( $post_id, $revision_id ) {
- $footnotes = get_post_meta( $revision_id, 'footnotes', true );
-
- if ( $footnotes ) {
- update_post_meta( $post_id, 'footnotes', wp_slash( $footnotes ) );
- } else {
- delete_post_meta( $post_id, 'footnotes' );
- }
-}
-add_action( 'wp_restore_post_revision', 'wp_restore_footnotes_from_revision', 10, 2 );
-
-/**
- * Adds the footnotes field to the revision.
+ * Adds the footnotes field to the revisions display.
*
* @since 6.3.0
*
@@ -197,7 +104,7 @@ function wp_add_footnotes_to_revision( $fields ) {
add_filter( '_wp_post_revision_fields', 'wp_add_footnotes_to_revision' );
/**
- * Gets the footnotes field from the revision.
+ * Gets the footnotes field from the revision for the revisions screen.
*
* @since 6.3.0
*
@@ -211,76 +118,3 @@ function wp_get_footnotes_from_revision( $revision_field, $field, $revision ) {
return get_metadata( 'post', $revision->ID, $field, true );
}
add_filter( '_wp_post_revision_field_footnotes', 'wp_get_footnotes_from_revision', 10, 3 );
-
-/**
- * The REST API autosave endpoint doesn't save meta, so we can use the
- * `wp_creating_autosave` when it updates an exiting autosave, and
- * `_wp_put_post_revision` when it creates a new autosave.
- *
- * @since 6.3.0
- *
- * @param int|array $autosave The autosave ID or array.
- */
-function _wp_rest_api_autosave_meta( $autosave ) {
- // Ensure it's a REST API request.
- if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
- return;
- }
-
- $body = rest_get_server()->get_raw_data();
- $body = json_decode( $body, true );
-
- if ( ! isset( $body['meta']['footnotes'] ) ) {
- return;
- }
-
- // `wp_creating_autosave` passes the array,
- // `_wp_put_post_revision` passes the ID.
- $id = is_int( $autosave ) ? $autosave : $autosave['ID'];
-
- if ( ! $id ) {
- return;
- }
-
- update_post_meta( $id, 'footnotes', wp_slash( $body['meta']['footnotes'] ) );
-}
-// See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L391C1-L391C1.
-add_action( 'wp_creating_autosave', '_wp_rest_api_autosave_meta' );
-// See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L398.
-// Then https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/revision.php#L367.
-add_action( '_wp_put_post_revision', '_wp_rest_api_autosave_meta' );
-
-/**
- * This is a workaround for the autosave endpoint returning early if the
- * revision field are equal. The problem is that "footnotes" is not real
- * revision post field, so there's nothing to compare against.
- *
- * This trick sets the "footnotes" field (value doesn't matter), which will
- * cause the autosave endpoint to always update the latest revision. That should
- * be fine, it should be ok to update the revision even if nothing changed. Of
- * course, this is temporary fix.
- *
- * @since 6.3.0
- *
- * @param WP_Post $prepared_post The prepared post object.
- * @param WP_REST_Request $request The request object.
- *
- * See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L365-L384.
- * See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L219.
- */
-function _wp_rest_api_force_autosave_difference( $prepared_post, $request ) {
- // We only want to be altering POST requests.
- if ( $request->get_method() !== 'POST' ) {
- return $prepared_post;
- }
-
- // Only alter requests for the '/autosaves' route.
- if ( substr( $request->get_route(), -strlen( '/autosaves' ) ) !== '/autosaves' ) {
- return $prepared_post;
- }
-
- $prepared_post->footnotes = '[]';
- return $prepared_post;
-}
-
-add_filter( 'rest_pre_insert_post', '_wp_rest_api_force_autosave_difference', 10, 2 );
diff --git a/packages/block-library/src/html/edit.js b/packages/block-library/src/html/edit.js
index 5cb2b457633b13..3cf2ee08bb68b2 100644
--- a/packages/block-library/src/html/edit.js
+++ b/packages/block-library/src/html/edit.js
@@ -8,7 +8,13 @@ import {
PlainText,
useBlockProps,
} from '@wordpress/block-editor';
-import { ToolbarButton, Disabled, ToolbarGroup } from '@wordpress/components';
+import {
+ ToolbarButton,
+ Disabled,
+ ToolbarGroup,
+ VisuallyHidden,
+} from '@wordpress/components';
+import { useInstanceId } from '@wordpress/compose';
/**
* Internal dependencies
@@ -19,6 +25,8 @@ export default function HTMLEdit( { attributes, setAttributes, isSelected } ) {
const [ isPreview, setIsPreview ] = useState();
const isDisabled = useContext( Disabled.Context );
+ const instanceId = useInstanceId( HTMLEdit, 'html-edit-desc' );
+
function switchToPreview() {
setIsPreview( true );
}
@@ -27,8 +35,13 @@ export default function HTMLEdit( { attributes, setAttributes, isSelected } ) {
setIsPreview( false );
}
+ const blockProps = useBlockProps( {
+ className: 'block-library-html__edit',
+ 'aria-describedby': isPreview ? instanceId : undefined,
+ } );
+
return (
-
+
{ isPreview || isDisabled ? (
-
+ <>
+
+
+ { __(
+ 'HTML preview is not yet fully accessible. Please switch screen reader to virtualized mode to navigate the below iFrame.'
+ ) }
+
+ >
) : (
-
+
{ /*
An overlay is added when the block is not selected in order to register click events.
Some browsers do not bubble up the clicks from the sandboxed iframe, which makes it
diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js
index 04839a47b880af..d49a8f7cd05783 100644
--- a/packages/block-library/src/image/image.js
+++ b/packages/block-library/src/image/image.js
@@ -372,10 +372,10 @@ export default function Image( {
const lightboxSetting = useSetting( 'lightbox' );
const showLightboxToggle =
- lightboxSetting === true || lightboxSetting?.allowEditing === true;
+ !! lightbox || lightboxSetting?.allowEditing === true;
const lightboxChecked =
- lightbox?.enabled || ( ! lightbox && lightboxSetting?.enabled );
+ !! lightbox?.enabled || ( ! lightbox && !! lightboxSetting?.enabled );
const dimensionsControl = (
get_updated_html(), $block->parsed_block );
+ // This render needs to happen in a filter with priority 15 to ensure that it
+ // runs after the duotone filter and that duotone styles are applied to the image
+ // in the lightbox. We also need to ensure that the lightbox works with any plugins
+ // that might use filters as well. We can consider removing this in the future if the
+ // way the blocks are rendered changes, or if a new kind of filter is introduced.
+ add_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15, 2 );
}
return $processor->get_updated_html();
@@ -91,16 +96,16 @@ function block_core_image_get_lightbox_settings( $block ) {
}
if ( ! isset( $lightbox_settings ) ) {
- $lightbox_settings = gutenberg_get_global_settings( array( 'lightbox' ), array( 'block_name' => 'core/image' ) );
+ $lightbox_settings = wp_get_global_settings( array( 'lightbox' ), array( 'block_name' => 'core/image' ) );
// If not present in global settings, check the top-level global settings.
//
// NOTE: If no block-level settings are found, the previous call to
- // `gutenberg_get_global_settings` will return the whole `theme.json`
+ // `wp_get_global_settings` will return the whole `theme.json`
// structure in which case we can check if the "lightbox" key is present at
// the top-level of the global settings and use its value.
if ( isset( $lightbox_settings['lightbox'] ) ) {
- $lightbox_settings = gutenberg_get_global_settings( array( 'lightbox' ) );
+ $lightbox_settings = wp_get_global_settings( array( 'lightbox' ) );
}
}
@@ -117,7 +122,7 @@ function block_core_image_get_lightbox_settings( $block ) {
function block_core_image_render_lightbox( $block_content, $block ) {
$processor = new WP_HTML_Tag_Processor( $block_content );
- $aria_label = __( 'Enlarge image', 'gutenberg' );
+ $aria_label = __( 'Enlarge image' );
$alt_attribute = $processor->get_attribute( 'alt' );
@@ -127,7 +132,7 @@ function block_core_image_render_lightbox( $block_content, $block ) {
if ( $alt_attribute ) {
/* translators: %s: Image alt text. */
- $aria_label = sprintf( __( 'Enlarge image: %s', 'gutenberg' ), $alt_attribute );
+ $aria_label = sprintf( __( 'Enlarge image: %s' ), $alt_attribute );
}
$content = $processor->get_updated_html();
@@ -251,8 +256,8 @@ function block_core_image_render_lightbox( $block_content, $block ) {
$close_button_icon = ' ';
$close_button_color = esc_attr( wp_get_global_styles( array( 'color', 'text' ) ) );
- $dialog_label = $alt_attribute ? esc_attr( $alt_attribute ) : esc_attr__( 'Image', 'gutenberg' );
- $close_button_label = esc_attr__( 'Close', 'gutenberg' );
+ $dialog_label = $alt_attribute ? esc_attr( $alt_attribute ) : esc_attr__( 'Image' );
+ $close_button_label = esc_attr__( 'Close' );
$lightbox_html = <<
@@ -286,6 +293,8 @@ function block_core_image_render_lightbox( $block_content, $block ) {
* Ensure that the view script has the `wp-interactivity` dependency.
*
* @since 6.4.0
+ *
+ * @global WP_Scripts $wp_scripts
*/
function block_core_image_ensure_interactivity_dependency() {
global $wp_scripts;
diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss
index c7eec224f65878..5c3552fd80c2ee 100644
--- a/packages/block-library/src/image/style.scss
+++ b/packages/block-library/src/image/style.scss
@@ -186,8 +186,8 @@
.close-button {
position: absolute;
- top: calc(env(safe-area-inset-top) + 12.5px);
- right: calc(env(safe-area-inset-right) + 12.5px);
+ top: calc(env(safe-area-inset-top) + 20px);
+ right: calc(env(safe-area-inset-right) + 20px);
padding: 0;
cursor: pointer;
z-index: 5000000;
@@ -297,10 +297,6 @@
}
}
-html.wp-has-lightbox-open {
- overflow: hidden;
-}
-
@keyframes turn-on-visibility {
0% {
opacity: 0;
diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js
index 53a2d1f7d567fc..13f20c9cd7cb68 100644
--- a/packages/block-library/src/image/view.js
+++ b/packages/block-library/src/image/view.js
@@ -17,6 +17,69 @@ const focusableSelectors = [
'[tabindex]:not([tabindex^="-"])',
];
+/*
+ * Stores a context-bound scroll handler.
+ *
+ * This callback could be defined inline inside of the store
+ * object but it's created externally to avoid confusion about
+ * how its logic is called. This logic is not referenced directly
+ * by the directives in the markup because the scroll event we
+ * need to listen to is triggered on the window; so by defining it
+ * outside of the store, we signal that the behavior here is different.
+ * If we find a compelling reason to move it to the store, feel free.
+ *
+ * @type {Function}
+ */
+let scrollCallback;
+
+/*
+ * Tracks whether user is touching screen; used to
+ * differentiate behavior for touch and mouse input.
+ *
+ * @type {boolean}
+ */
+let isTouching = false;
+
+/*
+ * Tracks the last time the screen was touched; used to
+ * differentiate behavior for touch and mouse input.
+ *
+ * @type {number}
+ */
+let lastTouchTime = 0;
+
+/*
+ * Lightbox page-scroll handler: prevents scrolling.
+ *
+ * This handler is added to prevent scrolling behaviors that
+ * trigger content shift while the lightbox is open.
+ *
+ * It would be better to accomplish this through CSS alone, but
+ * using overflow: hidden is currently the only way to do so, and
+ * that causes the layout to shift and prevents the zoom animation
+ * from working in some cases because we're unable to account for
+ * the layout shift when doing the animation calculations. Instead,
+ * here we use JavaScript to prevent and reset the scrolling
+ * behavior. In the future, we may be able to use CSS or overflow: hidden
+ * instead to not rely on JavaScript, but this seems to be the best approach
+ * for now that provides the best visual experience.
+ *
+ * @param {Object} context Interactivity page context?
+ */
+function handleScroll( context ) {
+ // We can't override the scroll behavior on mobile devices
+ // because doing so breaks the pinch to zoom functionality, and we
+ // want to allow users to zoom in further on the high-res image.
+ if ( ! isTouching && Date.now() - lastTouchTime > 450 ) {
+ // We are unable to use event.preventDefault() to prevent scrolling
+ // because the scroll event can't be canceled, so we reset the position instead.
+ window.scrollTo(
+ context.core.image.scrollLeftReset,
+ context.core.image.scrollTopReset
+ );
+ }
+}
+
store(
{
state: {
@@ -43,54 +106,49 @@ store(
context.core.image.lightboxEnabled = true;
setStyles( context, event );
- // Hide overflow only when the animation is in progress,
- // otherwise the removal of the scrollbars will draw attention
- // to itself and look like an error
- document.documentElement.classList.add(
- 'wp-has-lightbox-open'
+
+ context.core.image.scrollTopReset =
+ window.pageYOffset ||
+ document.documentElement.scrollTop;
+
+ // In most cases, this value will be 0, but this is included
+ // in case a user has created a page with horizontal scrolling.
+ context.core.image.scrollLeftReset =
+ window.pageXOffset ||
+ document.documentElement.scrollLeft;
+
+ // We define and bind the scroll callback here so
+ // that we can pass the context and as an argument.
+ // We may be able to change this in the future if we
+ // define the scroll callback in the store instead, but
+ // this approach seems to tbe clearest for now.
+ scrollCallback = handleScroll.bind( null, context );
+
+ // We need to add a scroll event listener to the window
+ // here because we are unable to otherwise access it via
+ // the Interactivity API directives. If we add a native way
+ // to access the window, we can remove this.
+ window.addEventListener(
+ 'scroll',
+ scrollCallback,
+ false
);
},
- hideLightbox: async ( { context, event } ) => {
+ hideLightbox: async ( { context } ) => {
context.core.image.hideAnimationEnabled = true;
if ( context.core.image.lightboxEnabled ) {
- // If scrolling, wait a moment before closing the lightbox.
- if (
- context.core.image.lightboxAnimation === 'fade'
- ) {
- context.core.image.scrollDelta += event.deltaY;
- if (
- event.type === 'mousewheel' &&
- Math.abs(
- window.scrollY -
- context.core.image.scrollDelta
- ) < 10
- ) {
- return;
- }
- } else if (
- context.core.image.lightboxAnimation === 'zoom'
- ) {
- // Disable scroll until the zoom animation ends.
- // Get the current page scroll position
- const scrollTop =
- window.pageYOffset ||
- document.documentElement.scrollTop;
- const scrollLeft =
- window.pageXOffset ||
- document.documentElement.scrollLeft;
- // if any scroll is attempted, set this to the previous value.
- window.onscroll = function () {
- window.scrollTo( scrollLeft, scrollTop );
- };
- // Enable scrolling after the animation finishes
- setTimeout( function () {
- window.onscroll = function () {};
- }, 400 );
- }
-
- document.documentElement.classList.remove(
- 'wp-has-lightbox-open'
- );
+ // We want to wait until the close animation is completed
+ // before allowing a user to scroll again. The duration of this
+ // animation is defined in the styles.scss and depends on if the
+ // animation is 'zoom' or 'fade', but in any case we should wait
+ // a few milliseconds longer than the duration, otherwise a user
+ // may scroll too soon and cause the animation to look sloppy.
+ setTimeout( function () {
+ window.removeEventListener(
+ 'scroll',
+ scrollCallback
+ );
+ }, 450 );
context.core.image.lightboxEnabled = false;
context.core.image.lastFocusedElement.focus( {
@@ -139,6 +197,27 @@ store(
ref,
} );
},
+ handleTouchStart: () => {
+ isTouching = true;
+ },
+ handleTouchMove: ( { context, event } ) => {
+ // On mobile devices, we want to prevent triggering the
+ // scroll event because otherwise the page jumps around as
+ // we reset the scroll position. This also means that closing
+ // the lightbox requires that a user perform a simple tap. This
+ // may be changed in the future if we find a better alternative
+ // to override or reset the scroll position during swipe actions.
+ if ( context.core.image.lightboxEnabled ) {
+ event.preventDefault();
+ }
+ },
+ handleTouchEnd: () => {
+ // We need to wait a few milliseconds before resetting
+ // to ensure that pinch to zoom works consistently
+ // on mobile devices when the lightbox is open.
+ lastTouchTime = Date.now();
+ isTouching = false;
+ },
},
},
},
@@ -266,6 +345,13 @@ store(
}
);
+/*
+ * Computes styles for the lightbox and adds them to the document.
+ *
+ * @function
+ * @param {Object} context - An Interactivity API context
+ * @param {Object} event - A triggering event
+ */
function setStyles( context, event ) {
// The reference img element lies adjacent
// to the event target button in the DOM.
@@ -434,6 +520,13 @@ function setStyles( context, event ) {
`;
}
+/*
+ * Debounces a function call.
+ *
+ * @function
+ * @param {Function} func - A function to be called
+ * @param {number} wait - The time to wait before calling the function
+ */
function debounce( func, wait = 50 ) {
let timeout;
return () => {
diff --git a/packages/block-library/src/loginout/block.json b/packages/block-library/src/loginout/block.json
index 3593961c09cfdc..59fceec596e37c 100644
--- a/packages/block-library/src/loginout/block.json
+++ b/packages/block-library/src/loginout/block.json
@@ -19,6 +19,14 @@
},
"supports": {
"className": true,
+ "spacing": {
+ "margin": true,
+ "padding": true,
+ "__experimentalDefaultControls": {
+ "margin": false,
+ "padding": false
+ }
+ },
"typography": {
"fontSize": true,
"lineHeight": true,
diff --git a/packages/block-library/src/media-text/edit.js b/packages/block-library/src/media-text/edit.js
index 30181c9044c34d..f3baeb8c1756ba 100644
--- a/packages/block-library/src/media-text/edit.js
+++ b/packages/block-library/src/media-text/edit.js
@@ -244,7 +244,7 @@ function MediaTextEdit( { attributes, isSelected, setAttributes } ) {
setAttributes( {
imageFill: ! imageFill,
diff --git a/packages/block-library/src/navigation-link/link-ui.js b/packages/block-library/src/navigation-link/link-ui.js
index 3a7b7d28f9e800..3c3d91e7b0a052 100644
--- a/packages/block-library/src/navigation-link/link-ui.js
+++ b/packages/block-library/src/navigation-link/link-ui.js
@@ -44,7 +44,15 @@ export function getSuggestionsQuery( type, kind ) {
if ( kind === 'post-type' ) {
return { type: 'post', subtype: type };
}
- return {};
+ return {
+ // for custom link which has no type
+ // always show pages as initial suggestions
+ initialSuggestionsSearchOptions: {
+ type: 'post',
+ subtype: 'page',
+ perPage: 20,
+ },
+ };
}
}
diff --git a/packages/block-library/src/navigation-link/style.scss b/packages/block-library/src/navigation-link/style.scss
index 19080c8d384823..2a45c3c545d301 100644
--- a/packages/block-library/src/navigation-link/style.scss
+++ b/packages/block-library/src/navigation-link/style.scss
@@ -5,7 +5,6 @@
.wp-block-navigation {
// This wraps just the innermost text for custom menu items.
.wp-block-navigation-item__label {
- word-break: normal;
overflow-wrap: break-word;
}
diff --git a/packages/block-library/src/navigation/edit/deleted-navigation-warning.js b/packages/block-library/src/navigation/edit/deleted-navigation-warning.js
index c787b90b76682f..6386cee71431e0 100644
--- a/packages/block-library/src/navigation/edit/deleted-navigation-warning.js
+++ b/packages/block-library/src/navigation/edit/deleted-navigation-warning.js
@@ -4,14 +4,19 @@
import { Warning } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
+import { createInterpolateElement } from '@wordpress/element';
function DeletedNavigationWarning( { onCreateNew } ) {
return (
- { __( 'Navigation menu has been deleted or is unavailable. ' ) }
-
- { __( 'Create a new menu?' ) }
-
+ { createInterpolateElement(
+ __(
+ 'Navigation menu has been deleted or is unavailable. Create a new menu? '
+ ),
+ {
+ button: ,
+ }
+ ) }
);
}
diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js
index 483e8abaab24f1..7c29f18d4940d4 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( {
// navigation block settings.
hasSubmenuIndicatorSetting = true,
customPlaceholder: CustomPlaceholder = null,
+ __unstableLayoutClassNames: layoutClassNames,
} ) {
const {
openSubmenusOnClick,
@@ -293,23 +294,31 @@ function Navigation( {
const isResponsive = 'never' !== overlayMenu;
const blockProps = useBlockProps( {
ref: navRef,
- className: classnames( className, {
- 'items-justified-right': justifyContent === 'right',
- 'items-justified-space-between': justifyContent === 'space-between',
- 'items-justified-left': justifyContent === 'left',
- 'items-justified-center': justifyContent === 'center',
- 'is-vertical': orientation === 'vertical',
- 'no-wrap': flexWrap === 'nowrap',
- 'is-responsive': isResponsive,
- 'has-text-color': !! textColor.color || !! textColor?.class,
- [ getColorClassName( 'color', textColor?.slug ) ]:
- !! textColor?.slug,
- 'has-background': !! backgroundColor.color || backgroundColor.class,
- [ getColorClassName( 'background-color', backgroundColor?.slug ) ]:
- !! backgroundColor?.slug,
- [ `has-text-decoration-${ textDecoration }` ]: textDecoration,
- 'block-editor-block-content-overlay': hasBlockOverlay,
- } ),
+ className: classnames(
+ className,
+ {
+ 'items-justified-right': justifyContent === 'right',
+ 'items-justified-space-between':
+ justifyContent === 'space-between',
+ 'items-justified-left': justifyContent === 'left',
+ 'items-justified-center': justifyContent === 'center',
+ 'is-vertical': orientation === 'vertical',
+ 'no-wrap': flexWrap === 'nowrap',
+ 'is-responsive': isResponsive,
+ 'has-text-color': !! textColor.color || !! textColor?.class,
+ [ getColorClassName( 'color', textColor?.slug ) ]:
+ !! textColor?.slug,
+ 'has-background':
+ !! backgroundColor.color || backgroundColor.class,
+ [ getColorClassName(
+ 'background-color',
+ backgroundColor?.slug
+ ) ]: !! backgroundColor?.slug,
+ [ `has-text-decoration-${ textDecoration }` ]: textDecoration,
+ 'block-editor-block-content-overlay': hasBlockOverlay,
+ },
+ layoutClassNames
+ ),
style: {
color: ! textColor?.slug && textColor?.color,
backgroundColor: ! backgroundColor?.slug && backgroundColor?.color,
diff --git a/packages/block-library/src/navigation/edit/inner-blocks.js b/packages/block-library/src/navigation/edit/inner-blocks.js
index 812b37ea71a641..19258213f26e5f 100644
--- a/packages/block-library/src/navigation/edit/inner-blocks.js
+++ b/packages/block-library/src/navigation/edit/inner-blocks.js
@@ -118,6 +118,7 @@ export default function NavigationInnerBlocks( {
: false,
placeholder: showPlaceholder ? placeholder : undefined,
__experimentalCaptureToolbars: true,
+ __unstableDisableLayoutClassNames: true,
}
);
diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php
index e562578e474954..a07235b601abb1 100644
--- a/packages/block-library/src/navigation/index.php
+++ b/packages/block-library/src/navigation/index.php
@@ -813,6 +813,8 @@ function block_core_navigation_typographic_presets_backcompatibility( $parsed_bl
* Ensure that the view script has the `wp-interactivity` dependency.
*
* @since 6.4.0
+ *
+ * @global WP_Scripts $wp_scripts
*/
function block_core_navigation_ensure_interactivity_dependency() {
global $wp_scripts;
diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss
index 180b40b43daca1..eb8ef7683e1988 100644
--- a/packages/block-library/src/navigation/style.scss
+++ b/packages/block-library/src/navigation/style.scss
@@ -506,10 +506,10 @@ button.wp-block-navigation-item__content {
@include reduce-motion("animation");
// Try to inherit any root paddings set, so the X can align to a top-right aligned menu.
- padding-top: var(--wp--style--root--padding-top, 2rem);
- padding-right: var(--wp--style--root--padding-right, 2rem);
- padding-bottom: var(--wp--style--root--padding-bottom, 2rem);
- padding-left: var(--wp--style--root--padding-left, 2rem);
+ padding-top: clamp(1rem, var(--wp--style--root--padding-top), 20rem);
+ padding-right: clamp(1rem, var(--wp--style--root--padding-right), 20rem);
+ padding-bottom: clamp(1rem, var(--wp--style--root--padding-bottom), 20rem);
+ padding-left: clamp(1rem, var(--wp--style--root--padding-left), 20em);
// Allow modal to scroll.
overflow: auto;
diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js
index 09d0157e987fd1..21f692bd25b263 100644
--- a/packages/block-library/src/paragraph/edit.js
+++ b/packages/block-library/src/paragraph/edit.js
@@ -155,7 +155,7 @@ function ParagraphBlock( {
onRemove={ onRemove }
aria-label={
content
- ? __( 'Paragraph block' )
+ ? __( 'Block: Paragraph' )
: __(
'Empty block; start writing or type forward slash to choose a block'
)
diff --git a/packages/block-library/src/pattern/edit.js b/packages/block-library/src/pattern/edit.js
index d9f0c2c53cebcd..e8068faf5013d9 100644
--- a/packages/block-library/src/pattern/edit.js
+++ b/packages/block-library/src/pattern/edit.js
@@ -29,6 +29,7 @@ const PatternEdit = ( { attributes, clientId } ) => {
const { getBlockRootClientId, getBlockEditingMode } =
useSelect( blockEditorStore );
+ // Duplicated in packages/edit-site/src/components/start-template-options/index.js.
function injectThemeAttributeInBlockTemplateContent( block ) {
if (
block.innerBlocks.find(
diff --git a/packages/block-library/src/pattern/index.php b/packages/block-library/src/pattern/index.php
index b117e31e125cf2..fc4652a7c22e89 100644
--- a/packages/block-library/src/pattern/index.php
+++ b/packages/block-library/src/pattern/index.php
@@ -41,12 +41,12 @@ function render_block_core_pattern( $attributes ) {
}
$pattern = $registry->get_registered( $slug );
- $content = _inject_theme_attribute_in_block_template_content( $pattern['content'] );
+ $content = $pattern['content'];
+ // Backward compatibility for handling Block Hooks and injecting the theme attribute in the Gutenberg plugin.
// This can be removed when the minimum supported WordPress is >= 6.4.
- if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
- // TODO: In the long run, we'd likely want to have a filter in the `WP_Block_Patterns_Registry` class
- // instead to allow us plugging in code like this.
+ if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && ! function_exists( 'traverse_and_serialize_blocks' ) ) {
+ $content = _inject_theme_attribute_in_block_template_content( $content );
$blocks = parse_blocks( $content );
$content = gutenberg_serialize_blocks( $blocks );
}
diff --git a/packages/block-library/src/post-featured-image/style.scss b/packages/block-library/src/post-featured-image/style.scss
index 4821e634b60327..e740b8c56e608c 100644
--- a/packages/block-library/src/post-featured-image/style.scss
+++ b/packages/block-library/src/post-featured-image/style.scss
@@ -39,4 +39,8 @@
}
}
}
+
+ &:where(.alignleft, .alignright) {
+ width: 100%;
+ }
}
diff --git a/packages/block-library/src/post-template/style.scss b/packages/block-library/src/post-template/style.scss
index 00305a17123369..4af30e30b23098 100644
--- a/packages/block-library/src/post-template/style.scss
+++ b/packages/block-library/src/post-template/style.scss
@@ -37,3 +37,23 @@
grid-template-columns: 1fr;
}
}
+
+.wp-block-post-template-is-layout-constrained > li > .alignright,
+.wp-block-post-template-is-layout-flow > li > .alignright {
+ float: right;
+ margin-inline-start: 2em;
+ margin-inline-end: 0;
+}
+
+.wp-block-post-template-is-layout-constrained > li > .alignleft,
+.wp-block-post-template-is-layout-flow > li > .alignleft {
+ float: left;
+ margin-inline-start: 0;
+ margin-inline-end: 2em;
+}
+
+.wp-block-post-template-is-layout-constrained > li > .aligncenter,
+.wp-block-post-template-is-layout-flow > li > .aligncenter {
+ margin-inline-start: auto;
+ margin-inline-end: auto;
+}
diff --git a/packages/block-library/src/post-title/index.php b/packages/block-library/src/post-title/index.php
index 8b0e431b3a8be4..d0eef8572ba139 100644
--- a/packages/block-library/src/post-title/index.php
+++ b/packages/block-library/src/post-title/index.php
@@ -38,7 +38,7 @@ function render_block_core_post_title( $attributes, $content, $block ) {
if ( isset( $attributes['isLink'] ) && $attributes['isLink'] ) {
$rel = ! empty( $attributes['rel'] ) ? 'rel="' . esc_attr( $attributes['rel'] ) . '"' : '';
- $title = sprintf( '%4$s ', get_the_permalink( $block->context['postId'] ), esc_attr( $attributes['linkTarget'] ), $rel, $title );
+ $title = sprintf( '%4$s ', esc_url( get_the_permalink( $block->context['postId'] ) ), esc_attr( $attributes['linkTarget'] ), $rel, $title );
}
$classes = array();
diff --git a/packages/block-library/src/query-pagination-next/index.php b/packages/block-library/src/query-pagination-next/index.php
index 6b1861be8a1ef6..fe22410c246573 100644
--- a/packages/block-library/src/query-pagination-next/index.php
+++ b/packages/block-library/src/query-pagination-next/index.php
@@ -74,6 +74,7 @@ function render_block_core_query_pagination_next( $attributes, $content, $block
$p->set_attribute( 'data-wp-key', 'query-pagination-next' );
$p->set_attribute( 'data-wp-on--click', 'actions.core.query.navigate' );
$p->set_attribute( 'data-wp-on--mouseenter', 'actions.core.query.prefetch' );
+ $p->set_attribute( 'data-wp-effect', 'effects.core.query.prefetch' );
$content = $p->get_updated_html();
}
}
diff --git a/packages/block-library/src/query-pagination-previous/index.php b/packages/block-library/src/query-pagination-previous/index.php
index be5d62d6b0a918..dc61f5d38b2828 100644
--- a/packages/block-library/src/query-pagination-previous/index.php
+++ b/packages/block-library/src/query-pagination-previous/index.php
@@ -62,6 +62,7 @@ function render_block_core_query_pagination_previous( $attributes, $content, $bl
$p->set_attribute( 'data-wp-key', 'query-pagination-previous' );
$p->set_attribute( 'data-wp-on--click', 'actions.core.query.navigate' );
$p->set_attribute( 'data-wp-on--mouseenter', 'actions.core.query.prefetch' );
+ $p->set_attribute( 'data-wp-effect', 'effects.core.query.prefetch' );
$content = $p->get_updated_html();
}
}
diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php
index 15bc2bf7542ac7..c35bf2ba00af21 100644
--- a/packages/block-library/src/query/index.php
+++ b/packages/block-library/src/query/index.php
@@ -95,6 +95,8 @@ class="wp-block-query__enhanced-pagination-animation"
* Ensure that the view script has the `wp-interactivity` dependency.
*
* @since 6.4.0
+ *
+ * @global WP_Scripts $wp_scripts
*/
function block_core_query_ensure_interactivity_dependency() {
global $wp_scripts;
diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js
index 2f334692e32910..96be4ee513fcec 100644
--- a/packages/block-library/src/query/view.js
+++ b/packages/block-library/src/query/view.js
@@ -11,10 +11,10 @@ const isValidLink = ( ref ) =>
ref.origin === window.location.origin;
const isValidEvent = ( event ) =>
- event.button === 0 && // left clicks only
- ! event.metaKey && // open in new tab (mac)
- ! event.ctrlKey && // open in new tab (windows)
- ! event.altKey && // download
+ event.button === 0 && // Left clicks only.
+ ! event.metaKey && // Open in new tab (Mac).
+ ! event.ctrlKey && // Open in new tab (Windows).
+ ! event.altKey && // Download.
! event.shiftKey &&
! event.defaultPrevented;
@@ -39,7 +39,7 @@ store( {
const id = ref.closest( '[data-wp-navigation-id]' )
.dataset.wpNavigationId;
- // Don't announce the navigation immediately, wait 300 ms.
+ // Don't announce the navigation immediately, wait 400 ms.
const timeout = setTimeout( () => {
context.core.query.message =
context.core.query.loadingText;
@@ -62,13 +62,11 @@ store( {
: '' );
context.core.query.animation = 'finish';
+ context.core.query.url = ref.href;
// Focus the first anchor of the Query block.
- document
- .querySelector(
- `[data-wp-navigation-id=${ id }] a[href]`
- )
- ?.focus();
+ const firstAnchor = `[data-wp-navigation-id=${ id }] .wp-block-post-template a[href]`;
+ document.querySelector( firstAnchor )?.focus();
}
},
prefetch: async ( { ref } ) => {
@@ -79,4 +77,15 @@ store( {
},
},
},
+ effects: {
+ core: {
+ query: {
+ prefetch: async ( { ref, context } ) => {
+ if ( context.core.query.url && isValidLink( ref ) ) {
+ await prefetch( ref.href );
+ }
+ },
+ },
+ },
+ },
} );
diff --git a/packages/block-library/src/rss/edit.js b/packages/block-library/src/rss/edit.js
index d24de5c291d511..7c35d63f4fc894 100644
--- a/packages/block-library/src/rss/edit.js
+++ b/packages/block-library/src/rss/edit.js
@@ -69,7 +69,7 @@ export default function RSSEdit( { attributes, setAttributes } ) {
>
@@ -77,7 +77,11 @@ export default function RSSEdit( { attributes, setAttributes } ) {
}
className="wp-block-rss__placeholder-input"
/>
-
+
{ __( 'Use URL' ) }
diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js
index ff957b575c7a4f..616478d8013f72 100644
--- a/packages/block-library/src/search/edit.js
+++ b/packages/block-library/src/search/edit.js
@@ -84,7 +84,7 @@ export default function SearchEdit( {
style,
} = attributes;
- const insertedInNavigationBlock = useSelect(
+ const wasJustInsertedIntoNavigationBlock = useSelect(
( select ) => {
const { getBlockParentsByBlockName, wasBlockJustInserted } =
select( blockEditorStore );
@@ -98,15 +98,21 @@ export default function SearchEdit( {
const { __unstableMarkNextChangeAsNotPersistent } =
useDispatch( blockEditorStore );
- if ( insertedInNavigationBlock ) {
- // This side-effect should not create an undo level.
- __unstableMarkNextChangeAsNotPersistent();
- setAttributes( {
- showLabel: false,
- buttonUseIcon: true,
- buttonPosition: 'button-inside',
- } );
- }
+ useEffect( () => {
+ if ( wasJustInsertedIntoNavigationBlock ) {
+ // This side-effect should not create an undo level.
+ __unstableMarkNextChangeAsNotPersistent();
+ setAttributes( {
+ showLabel: false,
+ buttonUseIcon: true,
+ buttonPosition: 'button-inside',
+ } );
+ }
+ }, [
+ __unstableMarkNextChangeAsNotPersistent,
+ wasJustInsertedIntoNavigationBlock,
+ setAttributes,
+ ] );
const borderRadius = style?.border?.radius;
const borderProps = useBorderProps( attributes );
diff --git a/packages/block-library/src/search/editor.scss b/packages/block-library/src/search/editor.scss
index 381e009d5ee07c..effa741249e919 100644
--- a/packages/block-library/src/search/editor.scss
+++ b/packages/block-library/src/search/editor.scss
@@ -13,6 +13,7 @@
display: flex;
align-items: center;
justify-content: center;
+ text-align: center;
}
&__components-button-group {
diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php
index b2052307805651..ed3d1cf4b847a7 100644
--- a/packages/block-library/src/search/index.php
+++ b/packages/block-library/src/search/index.php
@@ -210,6 +210,8 @@ function register_block_core_search() {
* Ensure that the view script has the `wp-interactivity` dependency.
*
* @since 6.4.0
+ *
+ * @global WP_Scripts $wp_scripts
*/
function block_core_search_ensure_interactivity_dependency() {
global $wp_scripts;
diff --git a/packages/block-library/src/search/style.scss b/packages/block-library/src/search/style.scss
index 967b2282ba6d57..b8a446721241b8 100644
--- a/packages/block-library/src/search/style.scss
+++ b/packages/block-library/src/search/style.scss
@@ -55,7 +55,11 @@ $button-spacing-y: math.div($grid-unit-15, 2); // 6px
margin-left: 0;
// Prevent unintended text wrapping.
flex-shrink: 0;
- // Ensure minimum input field width in small viewports.
+ max-width: 100%;
+ }
+
+ // Ensure minimum input field width in small viewports.
+ .wp-block-search__button[aria-expanded="true"] {
max-width: calc(100% - 100px);
}
}
diff --git a/packages/block-library/src/social-link/icons/index.js b/packages/block-library/src/social-link/icons/index.js
index 62c32d2d5f35c7..85de13090ad5dc 100644
--- a/packages/block-library/src/social-link/icons/index.js
+++ b/packages/block-library/src/social-link/icons/index.js
@@ -40,5 +40,6 @@ export * from './vimeo';
export * from './vk';
export * from './whatsapp';
export * from './wordpress';
+export * from './x';
export * from './yelp';
export * from './youtube';
diff --git a/packages/block-library/src/social-link/icons/x.js b/packages/block-library/src/social-link/icons/x.js
new file mode 100644
index 00000000000000..cd70775ad7cb89
--- /dev/null
+++ b/packages/block-library/src/social-link/icons/x.js
@@ -0,0 +1,10 @@
+/**
+ * WordPress dependencies
+ */
+import { Path, SVG } from '@wordpress/primitives';
+
+export const XIcon = () => (
+
+
+
+);
diff --git a/packages/block-library/src/social-link/index.php b/packages/block-library/src/social-link/index.php
index 1ce60ff49fb41e..cda8e125097a50 100644
--- a/packages/block-library/src/social-link/index.php
+++ b/packages/block-library/src/social-link/index.php
@@ -238,6 +238,10 @@ function block_core_social_link_services( $service = '', $field = '' ) {
'name' => 'Reddit',
'icon' => ' ',
),
+ 'share' => array(
+ 'name' => 'Share Icon',
+ 'icon' => ' ',
+ ),
'skype' => array(
'name' => 'Skype',
'icon' => ' ',
@@ -294,6 +298,10 @@ function block_core_social_link_services( $service = '', $field = '' ) {
'name' => 'WhatsApp',
'icon' => ' ',
),
+ 'x' => array(
+ 'name' => 'X',
+ 'icon' => ' ',
+ ),
'yelp' => array(
'name' => 'Yelp',
'icon' => ' ',
@@ -302,10 +310,6 @@ function block_core_social_link_services( $service = '', $field = '' ) {
'name' => 'YouTube',
'icon' => ' ',
),
- 'share' => array(
- 'name' => 'Share Icon',
- 'icon' => ' ',
- ),
);
if ( ! empty( $service )
diff --git a/packages/block-library/src/social-link/social-list.js b/packages/block-library/src/social-link/social-list.js
index 38c1ef91f99384..909e9b90a54c5c 100644
--- a/packages/block-library/src/social-link/social-list.js
+++ b/packages/block-library/src/social-link/social-list.js
@@ -14,7 +14,7 @@ import { ChainIcon } from './icons';
*
* @param {string} name key for a social service (lowercase slug)
*
- * @return {WPComponent} Icon component for social service.
+ * @return {Component} Icon component for social service.
*/
export const getIconBySite = ( name ) => {
const variation = variations.find( ( v ) => v.name === name );
diff --git a/packages/block-library/src/social-link/socials-with-bg.scss b/packages/block-library/src/social-link/socials-with-bg.scss
index 042db464f6ee26..3ee9b4b5148a8a 100644
--- a/packages/block-library/src/social-link/socials-with-bg.scss
+++ b/packages/block-library/src/social-link/socials-with-bg.scss
@@ -199,6 +199,11 @@
color: #fff;
}
+.wp-social-link-x {
+ background-color: #000;
+ color: #fff;
+}
+
.wp-social-link-yelp {
background-color: #d32422;
color: #fff;
diff --git a/packages/block-library/src/social-link/socials-without-bg.scss b/packages/block-library/src/social-link/socials-without-bg.scss
index ea8fca5d7ab835..aa84b5ab1433c0 100644
--- a/packages/block-library/src/social-link/socials-without-bg.scss
+++ b/packages/block-library/src/social-link/socials-without-bg.scss
@@ -155,6 +155,10 @@
color: #3499cd;
}
+.wp-social-link-x {
+ color: #000;
+}
+
.wp-social-link-yelp {
color: #d32422;
}
diff --git a/packages/block-library/src/social-link/variations.js b/packages/block-library/src/social-link/variations.js
index 47307ca65c0882..5b03b85ae4e60e 100644
--- a/packages/block-library/src/social-link/variations.js
+++ b/packages/block-library/src/social-link/variations.js
@@ -44,6 +44,7 @@ import {
VkIcon,
WhatsAppIcon,
WordPressIcon,
+ XIcon,
YelpIcon,
YouTubeIcon,
} from './icons';
@@ -304,6 +305,13 @@ const variations = [
title: 'WhatsApp',
icon: WhatsAppIcon,
},
+ {
+ name: 'x',
+ attributes: { service: 'x' },
+ keywords: [ 'twitter' ],
+ title: 'X',
+ icon: XIcon,
+ },
{
name: 'yelp',
attributes: { service: 'yelp' },
diff --git a/packages/block-library/src/table-of-contents/edit.js b/packages/block-library/src/table-of-contents/edit.js
index 915375606b10c3..7c2d7e1f8eb148 100644
--- a/packages/block-library/src/table-of-contents/edit.js
+++ b/packages/block-library/src/table-of-contents/edit.js
@@ -19,6 +19,8 @@ import {
import { useDispatch, useSelect } from '@wordpress/data';
import { renderToString } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
+import { useInstanceId } from '@wordpress/compose';
+import { store as noticeStore } from '@wordpress/notices';
/**
* Internal dependencies
@@ -40,7 +42,7 @@ import { useObserveHeadings } from './hooks';
* @param {string} props.clientId
* @param {(attributes: Object) => void} props.setAttributes
*
- * @return {WPComponent} The component.
+ * @return {Component} The component.
*/
export default function TableOfContentsEdit( {
attributes: { headings = [], onlyIncludeCurrentPage },
@@ -50,6 +52,24 @@ export default function TableOfContentsEdit( {
useObserveHeadings( clientId );
const blockProps = useBlockProps();
+ const instanceId = useInstanceId(
+ TableOfContentsEdit,
+ 'table-of-contents'
+ );
+
+ // If a user clicks to a link prevent redirection and show a warning.
+ const { createWarningNotice, removeNotice } = useDispatch( noticeStore );
+ let noticeId;
+ const showRedirectionPreventedNotice = ( event ) => {
+ event.preventDefault();
+ // Remove previous warning if any, to show one at a time per block.
+ removeNotice( noticeId );
+ noticeId = `block-library/core/table-of-contents/redirection-prevented/${ instanceId }`;
+ createWarningNotice( __( 'Links are disabled in the editor.' ), {
+ id: noticeId,
+ type: 'snackbar',
+ } );
+ };
const canInsertList = useSelect(
( select ) => {
@@ -137,8 +157,12 @@ export default function TableOfContentsEdit( {
return (
<>
-
-
+
+
{ toolbarControls }
diff --git a/packages/block-library/src/table-of-contents/list.tsx b/packages/block-library/src/table-of-contents/list.tsx
index e327f8dfe2e86b..fcb4736bab091d 100644
--- a/packages/block-library/src/table-of-contents/list.tsx
+++ b/packages/block-library/src/table-of-contents/list.tsx
@@ -1,7 +1,7 @@
/**
- * WordPress dependencies
+ * External dependencies
*/
-import type { WPElement } from '@wordpress/element';
+import type { MouseEvent, ReactElement } from 'react';
/**
* Internal dependencies
@@ -12,16 +12,30 @@ const ENTRY_CLASS_NAME = 'wp-block-table-of-contents__entry';
export default function TableOfContentsList( {
nestedHeadingList,
+ disableLinkActivation,
+ onClick,
}: {
nestedHeadingList: NestedHeadingData[];
-} ): WPElement {
+ disableLinkActivation?: boolean;
+ onClick?: ( event: MouseEvent< HTMLAnchorElement > ) => void;
+} ): ReactElement {
return (
<>
{ nestedHeadingList.map( ( node, index ) => {
const { content, link } = node.heading;
const entry = link ? (
-
+
{ content }
) : (
@@ -35,6 +49,15 @@ export default function TableOfContentsList( {
) : null }
diff --git a/packages/block-library/src/template-part/edit/import-controls.js b/packages/block-library/src/template-part/edit/import-controls.js
index c8f4fbc2e647df..ca9708f65764b4 100644
--- a/packages/block-library/src/template-part/edit/import-controls.js
+++ b/packages/block-library/src/template-part/edit/import-controls.js
@@ -147,7 +147,7 @@ export function TemplatePartImportControls( { area, setAttributes } ) {
options={ options }
onChange={ ( value ) => setSelectedSidebar( value ) }
disabled={ ! options.length }
- __next36pxDefaultSize
+ __next40pxDefaultSize
__nextHasNoMarginBottom
/>
@@ -158,6 +158,7 @@ export function TemplatePartImportControls( { area, setAttributes } ) {
} }
>
{
+ it( 'should gracefully handle invalid URLs', async () => {
+ await initializeEditor();
+
+ await addBlock( screen, 'Video' );
+ fireEvent.press( screen.getByText( 'Insert from URL' ) );
+ fireEvent.changeText(
+ screen.getByPlaceholderText( 'Type a URL' ),
+ 'h://wordpress.org/video.mp4'
+ );
+ dismissModal( screen.getByTestId( 'bottom-sheet' ) );
+
+ expect( screen.getByText( 'Invalid URL.' ) ).toBeVisible();
+ } );
+} );
diff --git a/packages/blocks/README.md b/packages/blocks/README.md
index 91cfec30c6a726..cbde04f72fd95b 100644
--- a/packages/blocks/README.md
+++ b/packages/blocks/README.md
@@ -914,11 +914,11 @@ A Higher Order Component used to inject BlockContent using context to the wrappe
_Parameters_
-- _OriginalComponent_ `WPComponent`: The component to enhance.
+- _OriginalComponent_ `Component`: The component to enhance.
_Returns_
-- `WPComponent`: The same component.
+- `Component`: The same component.
diff --git a/packages/blocks/package.json b/packages/blocks/package.json
index 66aa2f142e4e9f..41b3e2204f6d3a 100644
--- a/packages/blocks/package.json
+++ b/packages/blocks/package.json
@@ -54,7 +54,7 @@
"remove-accents": "^0.5.0",
"showdown": "^1.9.1",
"simple-html-tokenizer": "^0.5.7",
- "uuid": "^8.3.0"
+ "uuid": "^9.0.1"
},
"peerDependencies": {
"react": "^18.0.0"
diff --git a/packages/blocks/src/api/children.js b/packages/blocks/src/api/children.js
index 8d0b3488017d5d..fe46a30870af97 100644
--- a/packages/blocks/src/api/children.js
+++ b/packages/blocks/src/api/children.js
@@ -20,7 +20,7 @@ import * as node from './node';
*
* @param {WPBlockChildren} children Block children object to convert.
*
- * @return {WPElement} A serialize-capable element.
+ * @return {Element} A serialize-capable element.
*/
export function getSerializeCapableElement( children ) {
// The fact that block children are compatible with the element serializer is
diff --git a/packages/blocks/src/api/raw-handling/paste-handler.js b/packages/blocks/src/api/raw-handling/paste-handler.js
index c4ad40e0b1f509..c63b207c1dbad6 100644
--- a/packages/blocks/src/api/raw-handling/paste-handler.js
+++ b/packages/blocks/src/api/raw-handling/paste-handler.js
@@ -121,17 +121,34 @@ export function pasteHandler( {
HTML = HTML.normalize();
}
- // Parse Markdown (and encoded HTML) if:
+ // Must be run before checking if it's inline content.
+ HTML = deepFilterHTML( HTML, [ slackParagraphCorrector ] );
+
+ // Consider plain text if:
// * There is a plain text version.
// * There is no HTML version, or it has no formatting.
- if ( plainText && ( ! HTML || isPlain( HTML ) ) ) {
+ const isPlainText = plainText && ( ! HTML || isPlain( HTML ) );
+
+ // Parse Markdown (and encoded HTML) if it's considered plain text.
+ if ( isPlainText ) {
HTML = plainText;
// The markdown converter (Showdown) trims whitespace.
if ( ! /^\s+$/.test( plainText ) ) {
HTML = markdownConverter( HTML );
}
+ }
+
+ // An array of HTML strings and block objects. The blocks replace matched
+ // shortcodes.
+ const pieces = shortcodeConverter( HTML );
+
+ // The call to shortcodeConverter will always return more than one element
+ // if shortcodes are matched. The reason is when shortcodes are matched
+ // empty HTML strings are included.
+ const hasShortcodes = pieces.length > 1;
+ if ( isPlainText && ! hasShortcodes ) {
// Switch to inline mode if:
// * The current mode is AUTO.
// * The original plain text had no line breaks.
@@ -151,18 +168,6 @@ export function pasteHandler( {
return filterInlineHTML( HTML, preserveWhiteSpace );
}
- // Must be run before checking if it's inline content.
- HTML = deepFilterHTML( HTML, [ slackParagraphCorrector ] );
-
- // An array of HTML strings and block objects. The blocks replace matched
- // shortcodes.
- const pieces = shortcodeConverter( HTML );
-
- // The call to shortcodeConverter will always return more than one element
- // if shortcodes are matched. The reason is when shortcodes are matched
- // empty HTML strings are included.
- const hasShortcodes = pieces.length > 1;
-
if (
mode === 'AUTO' &&
! hasShortcodes &&
diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js
index 818a26fdb4e5c4..b1dbee47b9b7cb 100644
--- a/packages/blocks/src/api/registration.js
+++ b/packages/blocks/src/api/registration.js
@@ -17,7 +17,7 @@ import { unlock } from '../lock-unlock';
* An icon type definition. One of a Dashicon slug, an element,
* or a component.
*
- * @typedef {(string|WPElement|WPComponent)} WPIcon
+ * @typedef {(string|Element|Component)} WPIcon
*
* @see https://developer.wordpress.org/resource/dashicons/
*/
@@ -112,10 +112,10 @@ import { unlock } from '../lock-unlock';
* @property {string[]} [keywords] Additional keywords to produce block
* type as result in search interfaces.
* @property {Object} [attributes] Block type attributes.
- * @property {WPComponent} [save] Optional component describing
+ * @property {Component} [save] Optional component describing
* serialized markup structure of a
* block type.
- * @property {WPComponent} edit Component rendering an element to
+ * @property {Component} edit Component rendering an element to
* manipulate the attributes of a block
* in the context of an editor.
* @property {WPBlockVariation[]} [variations] The list of block variations.
diff --git a/packages/blocks/src/api/serializer.js b/packages/blocks/src/api/serializer.js
index 29ac9f9a2228f3..5883f47536431d 100644
--- a/packages/blocks/src/api/serializer.js
+++ b/packages/blocks/src/api/serializer.js
@@ -174,9 +174,9 @@ export function getSaveElement(
/**
* Filters the save result of a block during serialization.
*
- * @param {WPElement} element Block save result.
- * @param {WPBlock} blockType Block type definition.
- * @param {Object} attributes Block attributes.
+ * @param {Element} element Block save result.
+ * @param {WPBlock} blockType Block type definition.
+ * @param {Object} attributes Block attributes.
*/
return applyFilters(
'blocks.getSaveElement',
diff --git a/packages/blocks/src/api/test/matchers.js b/packages/blocks/src/api/test/matchers.js
index 2068ae18c6e066..894c18abe3a01a 100644
--- a/packages/blocks/src/api/test/matchers.js
+++ b/packages/blocks/src/api/test/matchers.js
@@ -22,9 +22,9 @@ describe( 'matchers', () => {
expect( typeof source ).toBe( 'function' );
} );
- it( 'should return HTML equivalent WPElement of matched element', () => {
+ it( 'should return HTML equivalent React Element of matched element', () => {
// Assumption here is that we can cleanly convert back and forth
- // between a string and WPElement representation.
+ // between a string and React Element representation.
const html =
'A delicious sundae dessert
';
const match = parse( html, sources.children() );
@@ -42,9 +42,9 @@ describe( 'matchers', () => {
expect( typeof source ).toBe( 'function' );
} );
- it( 'should return HTML equivalent WPElement of matched element', () => {
+ it( 'should return HTML equivalent React Element of matched element', () => {
// Assumption here is that we can cleanly convert back and forth
- // between a string and WPElement representation.
+ // between a string and React Element representation.
const html =
'A delicious sundae dessert
';
const match = parse( html, sources.node() );
diff --git a/packages/blocks/src/deprecated.js b/packages/blocks/src/deprecated.js
index ec8a5b7bddb3c8..b3d01d1167bf51 100644
--- a/packages/blocks/src/deprecated.js
+++ b/packages/blocks/src/deprecated.js
@@ -9,8 +9,8 @@ import deprecated from '@wordpress/deprecated';
*
* @deprecated
*
- * @param {WPComponent} OriginalComponent The component to enhance.
- * @return {WPComponent} The same component.
+ * @param {Component} OriginalComponent The component to enhance.
+ * @return {Component} The same component.
*/
export function withBlockContentContext( OriginalComponent ) {
deprecated( 'wp.blocks.withBlockContentContext', {
diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index 7e1bbe82a95757..b29e40ba900d9c 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -2,10 +2,42 @@
## Unreleased
+### Enhancements
+
+- `SearchControl`: polish metrics for `compact` size variant ([#54663](https://github.com/WordPress/gutenberg/pull/54663)).
+- `Button`: deprecating `isPressed` prop in favour of `aria-pressed` ([#54740](https://github.com/WordPress/gutenberg/pull/54740)).
+- `DuotonePicker/ColorListPicker`: Adds appropriate label and description to 'Duotone Filter' picker ([#54473](https://github.com/WordPress/gutenberg/pull/54473)).
+- `Modal`: Accessibly hide/show outer modal when nested ([#54743](https://github.com/WordPress/gutenberg/pull/54743)).
+- `InputControl`, `NumberControl`, `UnitControl`, `SelectControl`, `CustomSelectControl`, `TreeSelect`: Add opt-in prop for next 40px default size, superseding the `__next36pxDefaultSize` prop ([#53819](https://github.com/WordPress/gutenberg/pull/53819)).
+- `Modal`: add a new `size` prop to support preset widths, including a `fill` option to eventually replace the `isFullScreen` prop ([#54471](https://github.com/WordPress/gutenberg/pull/54471)).
+- Wrapped `TextareaControl` in a `forwardRef` call ([#54975](https://github.com/WordPress/gutenberg/pull/54975)).
+
+### Bug Fix
+
+- `Placeholder`: Improved DOM structure and screen reader announcements ([#45801](https://github.com/WordPress/gutenberg/pull/45801)).
+- `DateTimePicker`: fix onChange callback check so that it also works inside iframes ([#54669](https://github.com/WordPress/gutenberg/pull/54669)).
+- `FormTokenField`: Add `box-sizing` reset style and reset default padding ([#54734](https://github.com/WordPress/gutenberg/pull/54734)).
+- `SlotFill`: Pass `Component` instance to unregisterSlot ([#54765](https://github.com/WordPress/gutenberg/pull/54765)).
+- `Button`: Remove `aria-selected` CSS selector from styling 'active' buttons ([#54931](https://github.com/WordPress/gutenberg/pull/54931)).
+- `Popover`: Apply the CSS in JS styles properly for components used within popovers. ([#54912](https://github.com/WordPress/gutenberg/pull/54912))
+- `Button`: Remove hover styles when `aria-disabled` is set to `true` for the secondary variant. ([#54978](https://github.com/WordPress/gutenberg/pull/54978))
+
+### Internal
+
+- Update `@ariakit/react` to version `0.3.3` ([#54818](https://github.com/WordPress/gutenberg/pull/54818))
+- `Tooltip`, `Shortcut`: Remove unused `ui/` components from the codebase ([#54573](https://github.com/WordPress/gutenberg/pull/54573))
+- Refactor ariakit usages to use the `render` prop instead of `as` and to use the namespace import ([#54696](https://github.com/WordPress/gutenberg/pull/54696)).
+- Update `uuid` package to 9.0.1 ([#54725](https://github.com/WordPress/gutenberg/pull/54725)).
+- `ContextSystemProvider`: Move out of `ui/` ([#54847](https://github.com/WordPress/gutenberg/pull/54847)).
+- `SlotFill`: Migrate to TypeScript and Convert to Functional Component ` `. ([#51350](https://github.com/WordPress/gutenberg/pull/51350)).
+- `Components`: move `ui/utils` to `utils` and remove `ui/` folder ([#54922](https://github.com/WordPress/gutenberg/pull/54922)).
+- Ensure `@types/` dependencies used by final type files are included in the main dependency field ([#50231](https://github.com/WordPress/gutenberg/pull/50231)).
+
## 25.8.0 (2023-09-20)
### Enhancements
+- Add new option `firstContentElement` to Modal's `focusOnMount` prop to allow consumers to focus the first element within the Modal's **contents** ([#54590](https://github.com/WordPress/gutenberg/pull/54590)).
- `Notice`: Improve accessibility by adding visually hidden text to clarify what a notice text is about and the notice type (success, error, warning, info) ([#54498](https://github.com/WordPress/gutenberg/pull/54498)).
- Making Circular Option Picker a `listbox`. Note that while this changes some public API, new props are optional, and currently have default values; this will change in another patch ([#52255](https://github.com/WordPress/gutenberg/pull/52255)).
- `ToggleGroupControl`: Rewrite backdrop animation using framer motion shared layout animations, add better support for controlled and uncontrolled modes ([#50278](https://github.com/WordPress/gutenberg/pull/50278)).
@@ -18,6 +50,7 @@
- `DuotonePicker/ColorListPicker`: Adds appropriate labels to 'Duotone Filter' color pickers ([#54468](https://github.com/WordPress/gutenberg/pull/54468)).
- `SearchControl`: support new `40px` and `32px` sizes ([#54548](https://github.com/WordPress/gutenberg/pull/54548)).
- `FormTokenField`: Add `tokenizeOnBlur` prop to add any incompleteTokenValue as a new token when field loses focus ([#54445](https://github.com/WordPress/gutenberg/pull/54445)).
+- `Sandbox`: Add `tabIndex` prop ([#54408](https://github.com/WordPress/gutenberg/pull/54408)).
### Bug Fix
@@ -112,6 +145,7 @@
- `ColorPalette`, `BorderControl`: Don't hyphenate hex value in `aria-label` ([#52932](https://github.com/WordPress/gutenberg/pull/52932)).
- `MenuItemsChoice`, `MenuItem`: Support a `disabled` prop on a menu item ([#52737](https://github.com/WordPress/gutenberg/pull/52737)).
+- `TabPanel`: Introduce a new version of `TabPanel` with updated internals and improved adherence to ARIA guidance on `tabpanel` focus behavior while maintaining the same functionality and API surface.([#52133](https://github.com/WordPress/gutenberg/pull/52133)).
### Bug Fix
diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md
index 5a94194fa63585..3d682c584f383d 100644
--- a/packages/components/CONTRIBUTING.md
+++ b/packages/components/CONTRIBUTING.md
@@ -328,7 +328,7 @@ An example of how this is used can be found in the [`Card` component family](/pa
//=========================================================================
// Simplified snippet from `packages/components/src/card/card/hook.ts`
//=========================================================================
-import { useContextSystem } from '../../ui/context';
+import { useContextSystem } from '../../context';
export function useCard( props ) {
// Read any derived registered prop from the Context System in the `Card` namespace
@@ -342,7 +342,7 @@ export function useCard( props ) {
//=========================================================================
// Simplified snippet from `packages/components/src/card/card/component.ts`
//=========================================================================
-import { contextConnect, ContextSystemProvider } from '../../ui/context';
+import { contextConnect, ContextSystemProvider } from '../../context';
function Card( props, forwardedRef ) {
const {
@@ -379,7 +379,7 @@ export default ConnectedCard;
//=========================================================================
// Simplified snippet from `packages/components/src/card/card-body/hook.ts`
//=========================================================================
-import { useContextSystem } from '../../ui/context';
+import { useContextSystem } from '../../context';
export function useCardBody( props ) {
// Read any derived registered prop from the Context System in the `CardBody` namespace.
@@ -581,7 +581,7 @@ Given a component folder (e.g. `packages/components/src/unit-control`):
6. If the component forwards its `...restProps` to an underlying element/component, you should use the `WordPressComponentProps` type for the component's props:
```tsx
- import type { WordPressComponentProps } from '../ui/context';
+ import type { WordPressComponentProps } from '../context';
import type { ComponentOwnProps } from './types';
function UnconnectedMyComponent(
diff --git a/packages/components/README.md b/packages/components/README.md
index f324cb48c66d7e..f3e4399fe3d518 100644
--- a/packages/components/README.md
+++ b/packages/components/README.md
@@ -29,17 +29,12 @@ Many components include CSS to add styles, which you will need to load in order
In non-WordPress projects, link to the `build-style/style.css` file directly, it is located at `node_modules/@wordpress/components/build-style/style.css`.
-### Popovers and Tooltips
-
-_If you're using [`Popover`](/packages/components/src/popover/README.md) or [`Tooltip`](/packages/components/src/tooltip/README.md) components outside of the editor, make sure they are rendered within a `SlotFillProvider` and with a `Popover.Slot` somewhere up the element tree._
+### Popovers
By default, the `Popover` component will render within an extra element appended to the body of the document.
If you want to precisely contol where the popovers render, you will need to use the `Popover.Slot` component.
-A `Popover` is also used as the underlying mechanism to display `Tooltip` components.
-So the same considerations should be applied to them.
-
The following example illustrates how you can wrap a component using a
`Popover` and have those popovers render to a single location in the DOM.
@@ -58,7 +53,7 @@ const Example = () => {
-
+ ;
};
```
diff --git a/packages/components/package.json b/packages/components/package.json
index 9c3cacc586fb2a..926f99de3c6278 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -30,7 +30,7 @@
],
"types": "build-types",
"dependencies": {
- "@ariakit/react": "^0.2.12",
+ "@ariakit/react": "^0.3.3",
"@babel/runtime": "^7.16.0",
"@emotion/cache": "^11.7.1",
"@emotion/css": "^11.7.1",
@@ -40,6 +40,8 @@
"@emotion/utils": "^1.0.0",
"@floating-ui/react-dom": "^2.0.1",
"@radix-ui/react-dropdown-menu": "2.0.4",
+ "@types/gradient-parser": "0.1.3",
+ "@types/highlight-words-core": "1.2.1",
"@use-gesture/react": "^10.2.24",
"@wordpress/a11y": "file:../a11y",
"@wordpress/compose": "file:../compose",
@@ -77,7 +79,7 @@
"reakit": "^1.3.11",
"remove-accents": "^0.5.0",
"use-lilius": "^2.0.1",
- "uuid": "^8.3.0",
+ "uuid": "^9.0.1",
"valtio": "1.7.0"
},
"peerDependencies": {
diff --git a/packages/components/src/alignment-matrix-control/README.md b/packages/components/src/alignment-matrix-control/README.md
index 576fe77c1f25a4..d087a177d8d414 100644
--- a/packages/components/src/alignment-matrix-control/README.md
+++ b/packages/components/src/alignment-matrix-control/README.md
@@ -9,8 +9,8 @@ AlignmentMatrixControl components enable adjustments to horizontal and vertical
## Usage
```jsx
+import { useState } from 'react';
import { __experimentalAlignmentMatrixControl as AlignmentMatrixControl } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const Example = () => {
const [ alignment, setAlignment ] = useState( 'center center' );
diff --git a/packages/components/src/alignment-matrix-control/cell.tsx b/packages/components/src/alignment-matrix-control/cell.tsx
index b6e19c56022915..00daa6fb316954 100644
--- a/packages/components/src/alignment-matrix-control/cell.tsx
+++ b/packages/components/src/alignment-matrix-control/cell.tsx
@@ -14,7 +14,7 @@ import {
Point,
} from './styles/alignment-matrix-control-styles';
import type { AlignmentMatrixControlCellProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
export default function Cell( {
isActive = false,
diff --git a/packages/components/src/alignment-matrix-control/icon.tsx b/packages/components/src/alignment-matrix-control/icon.tsx
index b294bc7551e19d..33528e63fc87f0 100644
--- a/packages/components/src/alignment-matrix-control/icon.tsx
+++ b/packages/components/src/alignment-matrix-control/icon.tsx
@@ -13,7 +13,7 @@ import {
Point,
} from './styles/alignment-matrix-control-icon-styles';
import type { AlignmentMatrixControlIconProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
const BASE_SIZE = 24;
diff --git a/packages/components/src/alignment-matrix-control/index.tsx b/packages/components/src/alignment-matrix-control/index.tsx
index 0e34e4052346da..2d52bb0e248435 100644
--- a/packages/components/src/alignment-matrix-control/index.tsx
+++ b/packages/components/src/alignment-matrix-control/index.tsx
@@ -18,7 +18,7 @@ import { Composite, CompositeGroup, useCompositeState } from '../composite';
import { Root, Row } from './styles/alignment-matrix-control-styles';
import AlignmentMatrixControlIcon from './icon';
import { GRID, getItemId } from './utils';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type {
AlignmentMatrixControlProps,
AlignmentMatrixControlValue,
diff --git a/packages/components/src/angle-picker-control/README.md b/packages/components/src/angle-picker-control/README.md
index c91a24272e0f7c..3cbc1f6c8d9e1a 100644
--- a/packages/components/src/angle-picker-control/README.md
+++ b/packages/components/src/angle-picker-control/README.md
@@ -6,7 +6,7 @@ Users can choose an angle in a visual UI with the mouse by dragging an angle ind
## Usage
```jsx
-import { useState } from '@wordpress/element';
+import { useState } from 'react';
import { AnglePickerControl } from '@wordpress/components';
function Example() {
diff --git a/packages/components/src/angle-picker-control/angle-circle.tsx b/packages/components/src/angle-picker-control/angle-circle.tsx
index 77518efb21bc1a..8558b23279302d 100644
--- a/packages/components/src/angle-picker-control/angle-circle.tsx
+++ b/packages/components/src/angle-picker-control/angle-circle.tsx
@@ -13,7 +13,7 @@ import {
CircleIndicator,
} from './styles/angle-picker-control-styles';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { AngleCircleProps } from './types';
type UseDraggingArgumentType = Parameters< typeof useDragging >[ 0 ];
diff --git a/packages/components/src/angle-picker-control/index.tsx b/packages/components/src/angle-picker-control/index.tsx
index 1fb17f86acfd2d..f90394b12078f4 100644
--- a/packages/components/src/angle-picker-control/index.tsx
+++ b/packages/components/src/angle-picker-control/index.tsx
@@ -20,7 +20,7 @@ import NumberControl from '../number-control';
import AngleCircle from './angle-circle';
import { Root, UnitText } from './styles/angle-picker-control-styles';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { AnglePickerControlProps } from './types';
function UnforwardedAnglePickerControl(
diff --git a/packages/components/src/angle-picker-control/styles/angle-picker-control-styles.tsx b/packages/components/src/angle-picker-control/styles/angle-picker-control-styles.tsx
index 65ac5b77d0da6c..08a907b21e0c99 100644
--- a/packages/components/src/angle-picker-control/styles/angle-picker-control-styles.tsx
+++ b/packages/components/src/angle-picker-control/styles/angle-picker-control-styles.tsx
@@ -9,7 +9,7 @@ import styled from '@emotion/styled';
*/
import { Flex } from '../../flex';
import { COLORS } from '../../utils';
-import { space } from '../../ui/utils/space';
+import { space } from '../../utils/space';
import { Text } from '../../text';
import CONFIG from '../../utils/config-values';
diff --git a/packages/components/src/autocomplete/types.ts b/packages/components/src/autocomplete/types.ts
index cd0e5029580ba8..ebe69648fc7e96 100644
--- a/packages/components/src/autocomplete/types.ts
+++ b/packages/components/src/autocomplete/types.ts
@@ -1,7 +1,11 @@
+/**
+ * External dependencies
+ */
+import type { ReactElement } from 'react';
+
/**
* WordPress dependencies
*/
-import type { WPElement } from '@wordpress/element';
import type { RichTextValue } from '@wordpress/rich-text';
/**
@@ -20,7 +24,7 @@ export type ReplaceOption = { action: 'replace'; value: RichTextValue };
export type OptionCompletion = React.ReactNode | InsertOption | ReplaceOption;
-type OptionLabel = string | WPElement | Array< string | WPElement >;
+type OptionLabel = string | ReactElement | Array< string | ReactElement >;
export type KeyedOption = {
key: string;
value: any;
diff --git a/packages/components/src/base-control/index.tsx b/packages/components/src/base-control/index.tsx
index fa2df39bddd458..29251a114e0343 100644
--- a/packages/components/src/base-control/index.tsx
+++ b/packages/components/src/base-control/index.tsx
@@ -15,7 +15,7 @@ import {
StyledHelp,
StyledVisualLabel,
} from './styles/base-control-styles';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
export { useBaseControlProps } from './hooks';
diff --git a/packages/components/src/base-control/styles/base-control-styles.ts b/packages/components/src/base-control/styles/base-control-styles.ts
index abd2f3affc45ae..6ddfe10229fdbb 100644
--- a/packages/components/src/base-control/styles/base-control-styles.ts
+++ b/packages/components/src/base-control/styles/base-control-styles.ts
@@ -8,7 +8,7 @@ import { css } from '@emotion/react';
* Internal dependencies
*/
import { baseLabelTypography, boxSizingReset, font, COLORS } from '../../utils';
-import { space } from '../../ui/utils/space';
+import { space } from '../../utils/space';
export const Wrapper = styled.div`
font-family: ${ font( 'default.fontFamily' ) };
diff --git a/packages/components/src/border-box-control/border-box-control-linked-button/component.tsx b/packages/components/src/border-box-control/border-box-control-linked-button/component.tsx
index f782fb89340200..ee76c39742d417 100644
--- a/packages/components/src/border-box-control/border-box-control-linked-button/component.tsx
+++ b/packages/components/src/border-box-control/border-box-control-linked-button/component.tsx
@@ -10,8 +10,8 @@ import { __ } from '@wordpress/i18n';
import Button from '../../button';
import Tooltip from '../../tooltip';
import { View } from '../../view';
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { useBorderBoxControlLinkedButton } from './hook';
import type { LinkedButtonProps } from '../types';
diff --git a/packages/components/src/border-box-control/border-box-control-linked-button/hook.ts b/packages/components/src/border-box-control/border-box-control-linked-button/hook.ts
index cd65758416ca13..3d535a6583a13c 100644
--- a/packages/components/src/border-box-control/border-box-control-linked-button/hook.ts
+++ b/packages/components/src/border-box-control/border-box-control-linked-button/hook.ts
@@ -7,8 +7,8 @@ import { useMemo } from '@wordpress/element';
* Internal dependencies
*/
import * as styles from '../styles';
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
import type { LinkedButtonProps } from '../types';
diff --git a/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx b/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx
index 922f0b39c09c97..ee7a6d86cb33fb 100644
--- a/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx
+++ b/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx
@@ -11,8 +11,8 @@ import { useMergeRefs } from '@wordpress/compose';
import BorderBoxControlVisualizer from '../border-box-control-visualizer';
import { BorderControl } from '../../border-control';
import { Grid } from '../../grid';
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { useBorderBoxControlSplitControls } from './hook';
import type { BorderControlProps } from '../../border-control/types';
diff --git a/packages/components/src/border-box-control/border-box-control-split-controls/hook.ts b/packages/components/src/border-box-control/border-box-control-split-controls/hook.ts
index ff152c9bc674ad..3a8194819ef0c8 100644
--- a/packages/components/src/border-box-control/border-box-control-split-controls/hook.ts
+++ b/packages/components/src/border-box-control/border-box-control-split-controls/hook.ts
@@ -7,8 +7,8 @@ import { useMemo } from '@wordpress/element';
* Internal dependencies
*/
import * as styles from '../styles';
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import { useCx } from '../../utils/';
import type { SplitControlsProps } from '../types';
diff --git a/packages/components/src/border-box-control/border-box-control-visualizer/component.tsx b/packages/components/src/border-box-control/border-box-control-visualizer/component.tsx
index d815b0b8c088e8..2656911d765797 100644
--- a/packages/components/src/border-box-control/border-box-control-visualizer/component.tsx
+++ b/packages/components/src/border-box-control/border-box-control-visualizer/component.tsx
@@ -7,8 +7,8 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import { View } from '../../view';
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { useBorderBoxControlVisualizer } from './hook';
import type { VisualizerProps } from '../types';
diff --git a/packages/components/src/border-box-control/border-box-control-visualizer/hook.ts b/packages/components/src/border-box-control/border-box-control-visualizer/hook.ts
index d7ae390dcd146d..8cc678c7e2708c 100644
--- a/packages/components/src/border-box-control/border-box-control-visualizer/hook.ts
+++ b/packages/components/src/border-box-control/border-box-control-visualizer/hook.ts
@@ -7,8 +7,8 @@ import { useMemo } from '@wordpress/element';
* Internal dependencies
*/
import * as styles from '../styles';
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import { useCx } from '../../utils';
import type { VisualizerProps } from '../types';
diff --git a/packages/components/src/border-box-control/border-box-control/README.md b/packages/components/src/border-box-control/border-box-control/README.md
index 4efe365cb50b7b..20352e03c8243d 100644
--- a/packages/components/src/border-box-control/border-box-control/README.md
+++ b/packages/components/src/border-box-control/border-box-control/README.md
@@ -27,9 +27,9 @@ show "Mixed" placeholder text.
## Usage
```jsx
+import { useState } from 'react';
import { __experimentalBorderBoxControl as BorderBoxControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-import { useState } from '@wordpress/element';
const colors = [
{ name: 'Blue 20', color: '#72aee6' },
diff --git a/packages/components/src/border-box-control/border-box-control/component.tsx b/packages/components/src/border-box-control/border-box-control/component.tsx
index ad3162851c2670..4f3517937f2046 100644
--- a/packages/components/src/border-box-control/border-box-control/component.tsx
+++ b/packages/components/src/border-box-control/border-box-control/component.tsx
@@ -14,8 +14,8 @@ import { BorderControl } from '../../border-control';
import { StyledLabel } from '../../base-control/styles/base-control-styles';
import { View } from '../../view';
import { VisuallyHidden } from '../../visually-hidden';
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { useBorderBoxControl } from './hook';
import type { BorderBoxControlProps } from '../types';
diff --git a/packages/components/src/border-box-control/border-box-control/hook.ts b/packages/components/src/border-box-control/border-box-control/hook.ts
index cf5aae4e09f725..a303e7d5a39e8b 100644
--- a/packages/components/src/border-box-control/border-box-control/hook.ts
+++ b/packages/components/src/border-box-control/border-box-control/hook.ts
@@ -16,8 +16,8 @@ import {
isCompleteBorder,
isEmptyBorder,
} from '../utils';
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
import type { Border } from '../../border-control/types';
diff --git a/packages/components/src/border-control/border-control-dropdown/component.tsx b/packages/components/src/border-control/border-control-dropdown/component.tsx
index 4a34d6ed7ffe34..4f43a6ed0ce55d 100644
--- a/packages/components/src/border-control/border-control-dropdown/component.tsx
+++ b/packages/components/src/border-control/border-control-dropdown/component.tsx
@@ -19,8 +19,8 @@ import ColorPalette from '../../color-palette';
import Dropdown from '../../dropdown';
import { HStack } from '../../h-stack';
import { VStack } from '../../v-stack';
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { useBorderControlDropdown } from './hook';
import { StyledLabel } from '../../base-control/styles/base-control-styles';
import DropdownContentWrapper from '../../dropdown/dropdown-content-wrapper';
diff --git a/packages/components/src/border-control/border-control-dropdown/hook.ts b/packages/components/src/border-control/border-control-dropdown/hook.ts
index 99309bb3374c58..b60aa52a34e2eb 100644
--- a/packages/components/src/border-control/border-control-dropdown/hook.ts
+++ b/packages/components/src/border-control/border-control-dropdown/hook.ts
@@ -8,8 +8,8 @@ import { useMemo } from '@wordpress/element';
*/
import * as styles from '../styles';
import { parseQuantityAndUnitFromRawValue } from '../../unit-control/utils';
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
import type { DropdownProps } from '../types';
diff --git a/packages/components/src/border-control/border-control-style-picker/component.tsx b/packages/components/src/border-control/border-control-style-picker/component.tsx
index 66858531f62cab..ffb44855be9fa4 100644
--- a/packages/components/src/border-control/border-control-style-picker/component.tsx
+++ b/packages/components/src/border-control/border-control-style-picker/component.tsx
@@ -12,8 +12,8 @@ import { StyledLabel } from '../../base-control/styles/base-control-styles';
import { View } from '../../view';
import { Flex } from '../../flex';
import { VisuallyHidden } from '../../visually-hidden';
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { useBorderControlStylePicker } from './hook';
import type { LabelProps, StylePickerProps } from '../types';
diff --git a/packages/components/src/border-control/border-control-style-picker/hook.ts b/packages/components/src/border-control/border-control-style-picker/hook.ts
index 7a77c735b9c2c9..27233c40309a3a 100644
--- a/packages/components/src/border-control/border-control-style-picker/hook.ts
+++ b/packages/components/src/border-control/border-control-style-picker/hook.ts
@@ -7,8 +7,8 @@ import { useMemo } from '@wordpress/element';
* Internal dependencies
*/
import * as styles from '../styles';
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
import type { StylePickerProps } from '../types';
diff --git a/packages/components/src/border-control/border-control/README.md b/packages/components/src/border-control/border-control/README.md
index 55c860720f2394..51fb7172b7c553 100644
--- a/packages/components/src/border-control/border-control/README.md
+++ b/packages/components/src/border-control/border-control/README.md
@@ -20,9 +20,9 @@ a "shape" abstraction.
## Usage
```jsx
+import { useState } from 'react';
import { __experimentalBorderControl as BorderControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-import { useState } from '@wordpress/element';
const colors = [
{ name: 'Blue 20', color: '#72aee6' },
diff --git a/packages/components/src/border-control/border-control/component.tsx b/packages/components/src/border-control/border-control/component.tsx
index 617ff5dd5997c2..960be4bf171ea4 100644
--- a/packages/components/src/border-control/border-control/component.tsx
+++ b/packages/components/src/border-control/border-control/component.tsx
@@ -13,8 +13,8 @@ import { HStack } from '../../h-stack';
import { StyledLabel } from '../../base-control/styles/base-control-styles';
import { View } from '../../view';
import { VisuallyHidden } from '../../visually-hidden';
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { useBorderControl } from './hook';
import type { BorderControlProps, LabelProps } from '../types';
diff --git a/packages/components/src/border-control/border-control/hook.ts b/packages/components/src/border-control/border-control/hook.ts
index 39917793de72de..63034527cfa1a8 100644
--- a/packages/components/src/border-control/border-control/hook.ts
+++ b/packages/components/src/border-control/border-control/hook.ts
@@ -8,8 +8,8 @@ import { useCallback, useMemo, useState } from '@wordpress/element';
*/
import * as styles from '../styles';
import { parseQuantityAndUnitFromRawValue } from '../../unit-control/utils';
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
import type { Border, BorderControlProps } from '../types';
diff --git a/packages/components/src/border-control/styles.ts b/packages/components/src/border-control/styles.ts
index c828536e199f9a..322e9563d58a4d 100644
--- a/packages/components/src/border-control/styles.ts
+++ b/packages/components/src/border-control/styles.ts
@@ -7,7 +7,7 @@ import { css } from '@emotion/react';
* Internal dependencies
*/
import { COLORS, CONFIG, boxSizingReset, rtl } from '../utils';
-import { space } from '../ui/utils/space';
+import { space } from '../utils/space';
import { StyledLabel } from '../base-control/styles/base-control-styles';
import {
ValueInput as UnitControlWrapper,
diff --git a/packages/components/src/box-control/README.md b/packages/components/src/box-control/README.md
index 83ccd50a6f1ffc..2fd214b79157f7 100644
--- a/packages/components/src/box-control/README.md
+++ b/packages/components/src/box-control/README.md
@@ -9,8 +9,8 @@ BoxControl components let users set values for Top, Right, Bottom, and Left. Thi
## Usage
```jsx
+import { useState } from 'react';
import { __experimentalBoxControl as BoxControl } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const Example = () => {
const [ values, setValues ] = useState( {
diff --git a/packages/components/src/box-control/icon.tsx b/packages/components/src/box-control/icon.tsx
index 6cb893648d68ab..480235aa7c0ad8 100644
--- a/packages/components/src/box-control/icon.tsx
+++ b/packages/components/src/box-control/icon.tsx
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import {
Root,
Viewbox,
diff --git a/packages/components/src/button-group/index.tsx b/packages/components/src/button-group/index.tsx
index 6185792b6e4d90..2eb7451e3e00db 100644
--- a/packages/components/src/button-group/index.tsx
+++ b/packages/components/src/button-group/index.tsx
@@ -13,7 +13,7 @@ import { forwardRef } from '@wordpress/element';
* Internal dependencies
*/
import type { ButtonGroupProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
function UnforwardedButtonGroup(
props: WordPressComponentProps< ButtonGroupProps, 'div', false >,
diff --git a/packages/components/src/button/README.md b/packages/components/src/button/README.md
index fdd46b0255ccf2..3af46280b1bc9a 100644
--- a/packages/components/src/button/README.md
+++ b/packages/components/src/button/README.md
@@ -186,6 +186,8 @@ Renders a red text-based button style to indicate destructive behavior.
Renders a pressed button style.
+If the native `aria-pressed` attribute is also set, it will take precedence.
+
- Required: No
#### `isSmall`: `boolean`
diff --git a/packages/components/src/button/index.tsx b/packages/components/src/button/index.tsx
index a199c1bb350cd7..b14e85fa52f7f6 100644
--- a/packages/components/src/button/index.tsx
+++ b/packages/components/src/button/index.tsx
@@ -34,6 +34,7 @@ function useDeprecatedProps( {
isSecondary,
isTertiary,
isLink,
+ isPressed,
isSmall,
size,
variant,
@@ -42,6 +43,11 @@ function useDeprecatedProps( {
let computedSize = size;
let computedVariant = variant;
+ const newProps: { 'aria-pressed'?: boolean } = {
+ // @TODO Mark `isPressed` as deprecated
+ 'aria-pressed': isPressed,
+ };
+
if ( isSmall ) {
computedSize ??= 'small';
}
@@ -73,6 +79,7 @@ function useDeprecatedProps( {
}
return {
+ ...newProps,
...otherProps,
size: computedSize,
variant: computedVariant,
@@ -85,7 +92,6 @@ export function UnforwardedButton(
) {
const {
__next40pxDefaultSize,
- isPressed,
isBusy,
isDestructive,
className,
@@ -106,10 +112,16 @@ export function UnforwardedButton(
...buttonOrAnchorProps
} = useDeprecatedProps( props );
- const { href, target, ...additionalProps } =
- 'href' in buttonOrAnchorProps
- ? buttonOrAnchorProps
- : { href: undefined, target: undefined, ...buttonOrAnchorProps };
+ const {
+ href,
+ target,
+ 'aria-checked': ariaChecked,
+ 'aria-pressed': ariaPressed,
+ 'aria-selected': ariaSelected,
+ ...additionalProps
+ } = 'href' in buttonOrAnchorProps
+ ? buttonOrAnchorProps
+ : { href: undefined, target: undefined, ...buttonOrAnchorProps };
const instanceId = useInstanceId(
Button,
@@ -124,6 +136,12 @@ export function UnforwardedButton(
// Tooltip should not considered as a child
children?.[ 0 ]?.props?.className !== 'components-tooltip' );
+ const truthyAriaPressedValues: ( typeof ariaPressed )[] = [
+ true,
+ 'true',
+ 'mixed',
+ ];
+
const classes = classnames( 'components-button', className, {
'is-next-40px-default-size': __next40pxDefaultSize,
'is-secondary': variant === 'secondary',
@@ -131,7 +149,10 @@ export function UnforwardedButton(
'is-small': size === 'small',
'is-compact': size === 'compact',
'is-tertiary': variant === 'tertiary',
- 'is-pressed': isPressed,
+
+ 'is-pressed': truthyAriaPressedValues.includes( ariaPressed ),
+ 'is-pressed-mixed': ariaPressed === 'mixed',
+
'is-busy': isBusy,
'is-link': variant === 'link',
'is-destructive': isDestructive,
@@ -146,7 +167,9 @@ export function UnforwardedButton(
? {
type: 'button',
disabled: trulyDisabled,
- 'aria-pressed': isPressed,
+ 'aria-checked': ariaChecked,
+ 'aria-pressed': ariaPressed,
+ 'aria-selected': ariaSelected,
}
: {};
const anchorProps: ComponentPropsWithoutRef< 'a' > =
diff --git a/packages/components/src/button/stories/index.story.tsx b/packages/components/src/button/stories/index.story.tsx
index a1dadab081d8ac..2981319cfb3233 100644
--- a/packages/components/src/button/stories/index.story.tsx
+++ b/packages/components/src/button/stories/index.story.tsx
@@ -26,6 +26,17 @@ const meta: Meta< typeof Button > = {
component: Button,
argTypes: {
// Overrides a limitation of the docgen interpreting our TS types for this as required.
+ 'aria-pressed': {
+ control: { type: 'select' },
+ description:
+ 'Indicates the current "pressed" state, implying it is a toggle button. Implicitly set by `isPressed`, but takes precedence if both are provided.',
+ options: [ undefined, 'true', 'false', 'mixed' ],
+ table: {
+ type: {
+ summary: 'boolean | "true" | "false" | "mixed"',
+ },
+ },
+ },
href: { type: { name: 'string', required: false } },
icon: {
control: { type: 'select' },
diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss
index b572e96e4335fc..e3b8c55f074e8d 100644
--- a/packages/components/src/button/style.scss
+++ b/packages/components/src/button/style.scss
@@ -145,7 +145,7 @@
color: $components-color-accent;
background: transparent;
- &:hover:not(:disabled) {
+ &:hover:not(:disabled, [aria-disabled="true"]) {
box-shadow: inset 0 0 0 $border-width $components-color-accent-darker-10;
}
}
@@ -308,7 +308,8 @@
}
// Toggled style.
- &.is-pressed {
+ &[aria-pressed="true"],
+ &[aria-pressed="mixed"] {
color: $components-color-foreground-inverted;
background: $components-color-foreground;
diff --git a/packages/components/src/button/test/index.tsx b/packages/components/src/button/test/index.tsx
index d70b8b075aa1eb..fb6c6a72b9adcb 100644
--- a/packages/components/src/button/test/index.tsx
+++ b/packages/components/src/button/test/index.tsx
@@ -72,12 +72,6 @@ describe( 'Button', () => {
expect( button ).toHaveClass( 'is-link' );
} );
- it( 'should render a button element with is-pressed without button class', () => {
- render( );
-
- expect( screen.getByRole( 'button' ) ).toHaveClass( 'is-pressed' );
- } );
-
it( 'should render a button element with has-text when children are passed', async () => {
const user = userEvent.setup();
@@ -347,6 +341,56 @@ describe( 'Button', () => {
await cleanupTooltip( user );
} );
+
+ describe( 'using `aria-pressed` prop', () => {
+ it( 'should render a button element with is-pressed when `true`', () => {
+ render( );
+
+ expect( screen.getByRole( 'button' ) ).toHaveClass(
+ 'is-pressed'
+ );
+ } );
+
+ it( 'should render a button element with is-pressed when `"true"`', () => {
+ render( );
+
+ expect( screen.getByRole( 'button' ) ).toHaveClass(
+ 'is-pressed'
+ );
+ } );
+
+ it( 'should render a button element with is-pressed/is-pressed-mixed when `"mixed"`', () => {
+ render( );
+
+ expect( screen.getByRole( 'button' ) ).toHaveClass(
+ 'is-pressed is-pressed-mixed'
+ );
+ } );
+
+ it( 'should render a button element without is-pressed when `undefined`', () => {
+ render( );
+
+ expect( screen.getByRole( 'button' ) ).not.toHaveClass(
+ 'is-pressed'
+ );
+ } );
+
+ it( 'should render a button element without is-pressed when `false`', () => {
+ render( );
+
+ expect( screen.getByRole( 'button' ) ).not.toHaveClass(
+ 'is-pressed'
+ );
+ } );
+
+ it( 'should render a button element without is-pressed when `"false"`', () => {
+ render( );
+
+ expect( screen.getByRole( 'button' ) ).not.toHaveClass(
+ 'is-pressed'
+ );
+ } );
+ } );
} );
describe( 'with href property', () => {
@@ -434,6 +478,23 @@ describe( 'Button', () => {
);
expect( screen.getByRole( 'button' ) ).toHaveClass( 'is-compact' );
} );
+
+ it( 'should not break when the legacy isPressed prop is passed', () => {
+ render( );
+
+ expect( screen.getByRole( 'button' ) ).toHaveAttribute(
+ 'aria-pressed',
+ 'true'
+ );
+ } );
+
+ it( 'should prioritize the `aria-pressed` prop over `isPressed`', () => {
+ render( );
+ expect( screen.getByRole( 'button' ) ).toHaveAttribute(
+ 'aria-pressed',
+ 'mixed'
+ );
+ } );
} );
describe( 'static typing', () => {
diff --git a/packages/components/src/button/types.ts b/packages/components/src/button/types.ts
index 13b3fa5e00207e..b781786cc12711 100644
--- a/packages/components/src/button/types.ts
+++ b/packages/components/src/button/types.ts
@@ -8,7 +8,7 @@ import type { ReactNode } from 'react';
*/
import type { Props as IconProps } from '../icon';
import type { PopoverProps } from '../popover/types';
-import type { WordPressComponentProps } from '../ui/context/wordpress-component';
+import type { WordPressComponentProps } from '../context/wordpress-component';
export type ButtonProps =
| WordPressComponentProps< ButtonAsButtonProps, 'button', false >
diff --git a/packages/components/src/card/card-body/component.tsx b/packages/components/src/card/card-body/component.tsx
index f515162bbc1b76..44f96ec951123c 100644
--- a/packages/components/src/card/card-body/component.tsx
+++ b/packages/components/src/card/card-body/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { Scrollable } from '../../scrollable';
import { View } from '../../view';
import { useCardBody } from './hook';
diff --git a/packages/components/src/card/card-body/hook.ts b/packages/components/src/card/card-body/hook.ts
index 1418571fb71f58..d4cefb9ed9e670 100644
--- a/packages/components/src/card/card-body/hook.ts
+++ b/packages/components/src/card/card-body/hook.ts
@@ -6,8 +6,8 @@ import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import * as styles from '../styles';
import { useCx } from '../../utils/hooks/use-cx';
import type { BodyProps } from '../types';
diff --git a/packages/components/src/card/card-divider/component.tsx b/packages/components/src/card/card-divider/component.tsx
index 494d3451bb5ca9..8716f7caa9f238 100644
--- a/packages/components/src/card/card-divider/component.tsx
+++ b/packages/components/src/card/card-divider/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import type { DividerProps } from '../../divider';
import { Divider } from '../../divider';
import { useCardDivider } from './hook';
diff --git a/packages/components/src/card/card-divider/hook.ts b/packages/components/src/card/card-divider/hook.ts
index 969bdf0a43b71d..e7eb5c92b08cbe 100644
--- a/packages/components/src/card/card-divider/hook.ts
+++ b/packages/components/src/card/card-divider/hook.ts
@@ -6,8 +6,8 @@ import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import * as styles from '../styles';
import { useCx } from '../../utils/hooks/use-cx';
import type { DividerProps } from '../../divider';
diff --git a/packages/components/src/card/card-footer/component.tsx b/packages/components/src/card/card-footer/component.tsx
index 1507c7139cda27..74834eb1ca72ed 100644
--- a/packages/components/src/card/card-footer/component.tsx
+++ b/packages/components/src/card/card-footer/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { Flex } from '../../flex';
import { useCardFooter } from './hook';
import type { FooterProps } from '../types';
diff --git a/packages/components/src/card/card-footer/hook.ts b/packages/components/src/card/card-footer/hook.ts
index 1530faccaf15ce..844417848b8aee 100644
--- a/packages/components/src/card/card-footer/hook.ts
+++ b/packages/components/src/card/card-footer/hook.ts
@@ -6,8 +6,8 @@ import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import * as styles from '../styles';
import { useCx } from '../../utils/hooks/use-cx';
import type { FooterProps } from '../types';
diff --git a/packages/components/src/card/card-header/component.tsx b/packages/components/src/card/card-header/component.tsx
index 01c7942e438f2c..2ba12b5ed854f5 100644
--- a/packages/components/src/card/card-header/component.tsx
+++ b/packages/components/src/card/card-header/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { Flex } from '../../flex';
import { useCardHeader } from './hook';
import type { HeaderProps } from '../types';
diff --git a/packages/components/src/card/card-header/hook.ts b/packages/components/src/card/card-header/hook.ts
index 4804e76262d1dd..b45d75ff5467d0 100644
--- a/packages/components/src/card/card-header/hook.ts
+++ b/packages/components/src/card/card-header/hook.ts
@@ -6,8 +6,8 @@ import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import * as styles from '../styles';
import { useCx } from '../../utils/hooks/use-cx';
import type { HeaderProps } from '../types';
diff --git a/packages/components/src/card/card-media/component.tsx b/packages/components/src/card/card-media/component.tsx
index 13e453c2aedcd6..36b645d98d822d 100644
--- a/packages/components/src/card/card-media/component.tsx
+++ b/packages/components/src/card/card-media/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { View } from '../../view';
import { useCardMedia } from './hook';
import type { MediaProps } from '../types';
diff --git a/packages/components/src/card/card-media/hook.ts b/packages/components/src/card/card-media/hook.ts
index dfe67b8ed19731..b9f3a7b2edff47 100644
--- a/packages/components/src/card/card-media/hook.ts
+++ b/packages/components/src/card/card-media/hook.ts
@@ -6,8 +6,8 @@ import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import * as styles from '../styles';
import { useCx } from '../../utils/hooks/use-cx';
import type { MediaProps } from '../types';
diff --git a/packages/components/src/card/card/README.md b/packages/components/src/card/card/README.md
index ffad1e2ec36e53..c7b70c8c6c907a 100644
--- a/packages/components/src/card/card/README.md
+++ b/packages/components/src/card/card/README.md
@@ -66,7 +66,7 @@ Determines the amount of padding within the component.
### Inherited props
-`Card` also inherits all of the [`Surface` props](/packages/components/src/ui/surface/README.md#props).
+`Card` also inherits all of the [`Surface` props](/packages/components/src/surface/README.md#props).
## Sub-Components
diff --git a/packages/components/src/card/card/component.tsx b/packages/components/src/card/card/component.tsx
index b4f3831ce2f7e4..8fefc33bd48027 100644
--- a/packages/components/src/card/card/component.tsx
+++ b/packages/components/src/card/card/component.tsx
@@ -12,8 +12,8 @@ import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect, ContextSystemProvider } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect, ContextSystemProvider } from '../../context';
import { Elevation } from '../../elevation';
import { View } from '../../view';
import * as styles from '../styles';
diff --git a/packages/components/src/card/card/hook.ts b/packages/components/src/card/card/hook.ts
index eb4580941d01c3..93e313934b22a2 100644
--- a/packages/components/src/card/card/hook.ts
+++ b/packages/components/src/card/card/hook.ts
@@ -7,8 +7,8 @@ import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import { useSurface } from '../../surface';
import * as styles from '../styles';
import { useCx } from '../../utils/hooks/use-cx';
diff --git a/packages/components/src/checkbox-control/README.md b/packages/components/src/checkbox-control/README.md
index 4bc19ed9bfe5f0..12f792ea8577bf 100644
--- a/packages/components/src/checkbox-control/README.md
+++ b/packages/components/src/checkbox-control/README.md
@@ -56,8 +56,8 @@ If only a few child checkboxes are checked, the parent checkbox becomes a mixed
Render an is author checkbox:
```jsx
+import { useState } from 'react';
import { CheckboxControl } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyCheckboxControl = () => {
const [ isChecked, setChecked ] = useState( true );
@@ -85,7 +85,7 @@ If no prop is passed an empty label is rendered.
- Required: No
-#### `help`: `string|WPElement`
+#### `help`: `string|Element`
If this property is added, a help text will be generated using help property as the content.
diff --git a/packages/components/src/checkbox-control/index.tsx b/packages/components/src/checkbox-control/index.tsx
index 1b7cae04cd81f6..cd70bb8485ac7e 100644
--- a/packages/components/src/checkbox-control/index.tsx
+++ b/packages/components/src/checkbox-control/index.tsx
@@ -17,7 +17,7 @@ import { Icon, check, reset } from '@wordpress/icons';
*/
import BaseControl from '../base-control';
import type { CheckboxControlProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
/**
* Checkboxes allow the user to select one or more items from a set.
diff --git a/packages/components/src/circular-option-picker/README.md b/packages/components/src/circular-option-picker/README.md
index a7595daa349cd7..1dc230686cae17 100644
--- a/packages/components/src/circular-option-picker/README.md
+++ b/packages/components/src/circular-option-picker/README.md
@@ -9,8 +9,8 @@ This component is not exported, and therefore can only be used internally to the
## Usage
```jsx
+import { useState } from 'react';
import { CircularOptionPicker } from '../circular-option-picker';
-import { useState } from '@wordpress/element';
const Example = () => {
const [ currentColor, setCurrentColor ] = useState();
diff --git a/packages/components/src/circular-option-picker/circular-option-picker-actions.tsx b/packages/components/src/circular-option-picker/circular-option-picker-actions.tsx
index 2817569b0564ed..a9c86b4240ee9b 100644
--- a/packages/components/src/circular-option-picker/circular-option-picker-actions.tsx
+++ b/packages/components/src/circular-option-picker/circular-option-picker-actions.tsx
@@ -9,7 +9,7 @@ import classnames from 'classnames';
import Button from '../button';
import Dropdown from '../dropdown';
import type { DropdownLinkActionProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { ButtonAsButtonProps } from '../button/types';
export function DropdownLinkAction( {
diff --git a/packages/components/src/circular-option-picker/circular-option-picker-option.tsx b/packages/components/src/circular-option-picker/circular-option-picker-option.tsx
index 0cfbea06538aab..2834228715115a 100644
--- a/packages/components/src/circular-option-picker/circular-option-picker-option.tsx
+++ b/packages/components/src/circular-option-picker/circular-option-picker-option.tsx
@@ -34,7 +34,14 @@ function UnforwardedOptionAsButton(
},
forwardedRef: ForwardedRef< any >
) {
- return ;
+ const { isPressed, ...additionalProps } = props;
+ return (
+
+ );
}
const OptionAsButton = forwardRef( UnforwardedOptionAsButton );
@@ -48,7 +55,7 @@ function UnforwardedOptionAsOption(
},
forwardedRef: ForwardedRef< any >
) {
- const { id, className, isSelected, context, ...additionalProps } = props;
+ const { id, isSelected, context, ...additionalProps } = props;
const { isComposite, ..._compositeState } = context;
const compositeState =
_compositeState as CircularOptionPickerCompositeState;
@@ -73,15 +80,6 @@ function UnforwardedOptionAsOption(
{ ...compositeState }
as={ Button }
id={ id }
- // Ideally we'd let the underlying `Button` component
- // handle this by passing `isPressed` as a prop.
- // Unfortunately doing so also sets `aria-pressed` as
- // an attribute on the element, which is incompatible
- // with `role="option"`, and there is no way at this
- // point to override that behaviour.
- className={ classnames( className, {
- 'is-pressed': isSelected,
- } ) }
role="option"
aria-selected={ !! isSelected }
ref={ forwardedRef }
diff --git a/packages/components/src/circular-option-picker/style.scss b/packages/components/src/circular-option-picker/style.scss
index e0e4f3e81d4190..a705bf62fdb11d 100644
--- a/packages/components/src/circular-option-picker/style.scss
+++ b/packages/components/src/circular-option-picker/style.scss
@@ -83,7 +83,8 @@ $color-palette-circle-spacing: 12px;
box-shadow: inset 0 0 0 ($color-palette-circle-size * 0.5) !important;
}
- &.is-pressed {
+ &[aria-pressed="true"],
+ &[aria-selected="true"] {
box-shadow: inset 0 0 0 4px;
position: relative;
z-index: z-index(".components-circular-option-picker__option.is-pressed");
diff --git a/packages/components/src/circular-option-picker/types.ts b/packages/components/src/circular-option-picker/types.ts
index da443c7691a95b..cd9d9dca6be8df 100644
--- a/packages/components/src/circular-option-picker/types.ts
+++ b/packages/components/src/circular-option-picker/types.ts
@@ -13,7 +13,7 @@ import type { Icon } from '@wordpress/icons';
*/
import type { ButtonAsButtonProps } from '../button/types';
import type { DropdownProps } from '../dropdown/types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { CompositeState } from '../composite';
type CommonCircularOptionPickerProps = {
diff --git a/packages/components/src/clipboard-button/README.md b/packages/components/src/clipboard-button/README.md
index 0f1bbdf44b99db..94bd50b0476a58 100644
--- a/packages/components/src/clipboard-button/README.md
+++ b/packages/components/src/clipboard-button/README.md
@@ -11,8 +11,8 @@ With a clipboard button, users copy text (or other elements) with a single click
## Usage
```jsx
+import { useState } from 'react';
import { ClipboardButton } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyClipboardButton = () => {
const [ hasCopied, setHasCopied ] = useState( false );
diff --git a/packages/components/src/clipboard-button/index.tsx b/packages/components/src/clipboard-button/index.tsx
index ded4e9ad45d1a6..5a9cec30f7d49d 100644
--- a/packages/components/src/clipboard-button/index.tsx
+++ b/packages/components/src/clipboard-button/index.tsx
@@ -15,7 +15,7 @@ import deprecated from '@wordpress/deprecated';
*/
import Button from '../button';
import type { ClipboardButtonProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
const TIMEOUT = 4000;
diff --git a/packages/components/src/color-indicator/index.tsx b/packages/components/src/color-indicator/index.tsx
index fc402eac03f8c1..00312a39cdc7de 100644
--- a/packages/components/src/color-indicator/index.tsx
+++ b/packages/components/src/color-indicator/index.tsx
@@ -12,7 +12,7 @@ import { forwardRef } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { ColorIndicatorProps } from './types';
function UnforwardedColorIndicator(
diff --git a/packages/components/src/color-palette/README.md b/packages/components/src/color-palette/README.md
index 7b13b3905dd4ae..0fbfbd4d710013 100644
--- a/packages/components/src/color-palette/README.md
+++ b/packages/components/src/color-palette/README.md
@@ -5,8 +5,8 @@
## Usage
```jsx
+import { useState } from 'react';
import { ColorPalette } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyColorPalette = () => {
const [ color, setColor ] = useState ( '#f00' )
diff --git a/packages/components/src/color-palette/index.tsx b/packages/components/src/color-palette/index.tsx
index 60aeb6ed4d39ed..86f52305587c06 100644
--- a/packages/components/src/color-palette/index.tsx
+++ b/packages/components/src/color-palette/index.tsx
@@ -32,7 +32,7 @@ import type {
PaletteObject,
SinglePaletteProps,
} from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { DropdownProps } from '../dropdown/types';
import {
extractColorNameFromCurrentValue,
diff --git a/packages/components/src/color-picker/README.md b/packages/components/src/color-picker/README.md
index 91656ca32851a9..ed6ab104cd26ab 100644
--- a/packages/components/src/color-picker/README.md
+++ b/packages/components/src/color-picker/README.md
@@ -5,8 +5,8 @@
## Usage
```jsx
+import { useState } from 'react';
import { ColorPicker } from '@wordpress/components';
-import { useState } from '@wordpress/element';
function Example() {
const [color, setColor] = useState();
diff --git a/packages/components/src/color-picker/color-copy-button.tsx b/packages/components/src/color-picker/color-copy-button.tsx
index f5d7f2978ddfce..99450b07628c21 100644
--- a/packages/components/src/color-picker/color-copy-button.tsx
+++ b/packages/components/src/color-picker/color-copy-button.tsx
@@ -10,8 +10,7 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import { CopyButton } from './styles';
-import { Text } from '../text';
-import { Tooltip } from '../ui/tooltip';
+import Tooltip from '../tooltip';
import type { ColorCopyButtonProps } from './types';
@@ -56,14 +55,11 @@ export const ColorCopyButton = ( props: ColorCopyButtonProps ) => {
return (
- { copiedColor === color.toHex()
- ? __( 'Copied!' )
- : __( 'Copy' ) }
-
+ delay={ 0 }
+ hideOnClick={ false }
+ text={
+ copiedColor === color.toHex() ? __( 'Copied!' ) : __( 'Copy' )
}
- placement="bottom"
>
screen.getByRole( 'option', { name } );
const getAllOptions = () => screen.getAllByRole( 'option' );
const getOptionSearchString = ( option: ComboboxControlOption ) =>
option.label.substring( 0, 11 );
-const setupUser = () => userEvent.setup();
const ControlledComboboxControl = ( {
value: valueProp,
@@ -112,7 +111,7 @@ describe.each( [
} );
it( 'should render with the correct options', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
render(
);
@@ -133,7 +132,7 @@ describe.each( [
} );
it( 'should select the correct option via click events', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const targetOption = timezones[ 2 ];
const onChangeSpy = jest.fn();
render(
@@ -157,7 +156,7 @@ describe.each( [
} );
it( 'should select the correct option via keypress events', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const targetIndex = 4;
const targetOption = timezones[ targetIndex ];
const onChangeSpy = jest.fn();
@@ -187,7 +186,7 @@ describe.each( [
} );
it( 'should select the correct option from a search', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const targetOption = timezones[ 13 ];
const onChangeSpy = jest.fn();
render(
@@ -214,7 +213,7 @@ describe.each( [
} );
it( 'should render aria-live announcement upon selection', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const targetOption = timezones[ 9 ];
const onChangeSpy = jest.fn();
render(
@@ -242,7 +241,7 @@ describe.each( [
} );
it( 'should process multiple entries in a single session', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const unmatchedString = 'Mordor';
const targetOption = timezones[ 6 ];
const onChangeSpy = jest.fn();
diff --git a/packages/components/src/confirm-dialog/README.md b/packages/components/src/confirm-dialog/README.md
index 9274a8fa2eaea1..86d38bccdec3c6 100644
--- a/packages/components/src/confirm-dialog/README.md
+++ b/packages/components/src/confirm-dialog/README.md
@@ -43,8 +43,8 @@ Let the parent component control when the dialog is open/closed. It's activated
- You'll want to update the state that controls `isOpen` by updating it from the `onCancel` and `onConfirm` callbacks.
```jsx
+import { useState } from 'react';
import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components';
-import { useState } from '@wordpress/element';
function Example() {
const [ isOpen, setIsOpen ] = useState( true );
diff --git a/packages/components/src/confirm-dialog/component.tsx b/packages/components/src/confirm-dialog/component.tsx
index 1de1c08ffbcf40..4a8efd06e139c7 100644
--- a/packages/components/src/confirm-dialog/component.tsx
+++ b/packages/components/src/confirm-dialog/component.tsx
@@ -14,8 +14,8 @@ import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
*/
import Modal from '../modal';
import type { OwnProps, DialogInputEvent } from './types';
-import type { WordPressComponentProps } from '../ui/context';
-import { useContextSystem, contextConnect } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
+import { useContextSystem, contextConnect } from '../context';
import { Flex } from '../flex';
import Button from '../button';
import { Text } from '../text';
diff --git a/packages/components/src/ui/context/constants.js b/packages/components/src/context/constants.js
similarity index 100%
rename from packages/components/src/ui/context/constants.js
rename to packages/components/src/context/constants.js
diff --git a/packages/components/src/ui/context/context-connect.ts b/packages/components/src/context/context-connect.ts
similarity index 97%
rename from packages/components/src/ui/context/context-connect.ts
rename to packages/components/src/context/context-connect.ts
index 8db2bbc987c0df..f938fa0b1f3241 100644
--- a/packages/components/src/ui/context/context-connect.ts
+++ b/packages/components/src/context/context-connect.ts
@@ -99,7 +99,7 @@ function _contextConnect<
}
// @ts-expect-error We can't rely on inferred types here because of the
- // `as` prop polymorphism we're handling in https://github.com/WordPress/gutenberg/blob/9620bae6fef4fde7cc2b7833f416e240207cda29/packages/components/src/ui/context/wordpress-component.ts#L32-L33
+ // `as` prop polymorphism we're handling in https://github.com/WordPress/gutenberg/blob/4f3a11243c365f94892e479bff0b922ccc4ccda3/packages/components/src/context/wordpress-component.ts#L32-L33
return Object.assign( WrappedComponent, {
[ CONNECT_STATIC_NAMESPACE ]: [ ...new Set( mergedNamespace ) ],
displayName: namespace,
diff --git a/packages/components/src/ui/context/context-system-provider.js b/packages/components/src/context/context-system-provider.js
similarity index 98%
rename from packages/components/src/ui/context/context-system-provider.js
rename to packages/components/src/context/context-system-provider.js
index b03a8c63228286..741d2d833227fa 100644
--- a/packages/components/src/ui/context/context-system-provider.js
+++ b/packages/components/src/context/context-system-provider.js
@@ -20,7 +20,7 @@ import warn from '@wordpress/warning';
/**
* Internal dependencies
*/
-import { useUpdateEffect } from '../../utils';
+import { useUpdateEffect } from '../utils';
export const ComponentsContext = createContext(
/** @type {Record} */ ( {} )
diff --git a/packages/components/src/ui/context/get-styled-class-name-from-key.ts b/packages/components/src/context/get-styled-class-name-from-key.ts
similarity index 100%
rename from packages/components/src/ui/context/get-styled-class-name-from-key.ts
rename to packages/components/src/context/get-styled-class-name-from-key.ts
diff --git a/packages/components/src/ui/context/index.ts b/packages/components/src/context/index.ts
similarity index 100%
rename from packages/components/src/ui/context/index.ts
rename to packages/components/src/context/index.ts
diff --git a/packages/components/src/ui/context/stories/ComponentsProvider.stories.js b/packages/components/src/context/stories/ComponentsProvider.stories.js
similarity index 100%
rename from packages/components/src/ui/context/stories/ComponentsProvider.stories.js
rename to packages/components/src/context/stories/ComponentsProvider.stories.js
diff --git a/packages/components/src/ui/context/test/__snapshots__/context-system-provider.js.snap b/packages/components/src/context/test/__snapshots__/context-system-provider.js.snap
similarity index 100%
rename from packages/components/src/ui/context/test/__snapshots__/context-system-provider.js.snap
rename to packages/components/src/context/test/__snapshots__/context-system-provider.js.snap
diff --git a/packages/components/src/ui/context/test/context-connect.tsx b/packages/components/src/context/test/context-connect.tsx
similarity index 100%
rename from packages/components/src/ui/context/test/context-connect.tsx
rename to packages/components/src/context/test/context-connect.tsx
diff --git a/packages/components/src/ui/context/test/context-system-provider.js b/packages/components/src/context/test/context-system-provider.js
similarity index 100%
rename from packages/components/src/ui/context/test/context-system-provider.js
rename to packages/components/src/context/test/context-system-provider.js
diff --git a/packages/components/src/ui/context/test/wordpress-component.tsx b/packages/components/src/context/test/wordpress-component.tsx
similarity index 100%
rename from packages/components/src/ui/context/test/wordpress-component.tsx
rename to packages/components/src/context/test/wordpress-component.tsx
diff --git a/packages/components/src/ui/context/use-context-system.js b/packages/components/src/context/use-context-system.js
similarity index 98%
rename from packages/components/src/ui/context/use-context-system.js
rename to packages/components/src/context/use-context-system.js
index 07297ddb30f7e9..213d4ad67a2f55 100644
--- a/packages/components/src/ui/context/use-context-system.js
+++ b/packages/components/src/context/use-context-system.js
@@ -9,7 +9,7 @@ import warn from '@wordpress/warning';
import { useComponentsContext } from './context-system-provider';
import { getNamespace, getConnectedNamespace } from './utils';
import { getStyledClassNameFromKey } from './get-styled-class-name-from-key';
-import { useCx } from '../../utils/hooks/use-cx';
+import { useCx } from '../utils/hooks/use-cx';
/**
* @template TProps
diff --git a/packages/components/src/ui/context/utils.js b/packages/components/src/context/utils.js
similarity index 100%
rename from packages/components/src/ui/context/utils.js
rename to packages/components/src/context/utils.js
diff --git a/packages/components/src/ui/context/wordpress-component.ts b/packages/components/src/context/wordpress-component.ts
similarity index 100%
rename from packages/components/src/ui/context/wordpress-component.ts
rename to packages/components/src/context/wordpress-component.ts
diff --git a/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx b/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx
index 4d09f256620219..291776b91f2ff1 100644
--- a/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx
+++ b/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx
@@ -34,7 +34,7 @@ import {
MINIMUM_SIGNIFICANT_MOVE,
KEYBOARD_CONTROL_POINT_VARIATION,
} from './constants';
-import type { WordPressComponentProps } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
import type {
ControlPointButtonProps,
ControlPointMoveState,
diff --git a/packages/components/src/custom-select-control/README.md b/packages/components/src/custom-select-control/README.md
index 3f93291c34dde4..696fca338e465c 100644
--- a/packages/components/src/custom-select-control/README.md
+++ b/packages/components/src/custom-select-control/README.md
@@ -17,11 +17,8 @@ These are the same as [the ones for `SelectControl`s](/packages/components/src/s
### Usage
```jsx
-/**
- * WordPress dependencies
- */
+import { useState } from 'react';
import { CustomSelectControl } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const options = [
{
diff --git a/packages/components/src/custom-select-control/index.js b/packages/components/src/custom-select-control/index.js
index 10b9f0a307c010..ed9e73e525e449 100644
--- a/packages/components/src/custom-select-control/index.js
+++ b/packages/components/src/custom-select-control/index.js
@@ -21,6 +21,7 @@ import { Select as SelectControlSelect } from '../select-control/styles/select-c
import SelectControlChevronDown from '../select-control/chevron-down';
import { InputBaseWithBackCompatMinWidth } from './styles';
import { StyledLabel } from '../base-control/styles/base-control-styles';
+import { useDeprecated36pxDefaultSizeProp } from '../utils/use-deprecated-props';
const itemToString = ( item ) => item?.name;
// This is needed so that in Windows, where
@@ -65,7 +66,7 @@ const stateReducer = (
export default function CustomSelectControl( props ) {
const {
/** Start opting into the larger default height that will become the default size in a future version. */
- __next36pxDefaultSize = false,
+ __next40pxDefaultSize = false,
/** Start opting into the unconstrained width that will become the default in a future version. */
__nextUnconstrainedWidth = false,
className,
@@ -82,7 +83,11 @@ export default function CustomSelectControl( props ) {
onFocus,
onBlur,
__experimentalShowSelectedHint = false,
- } = props;
+ } = useDeprecated36pxDefaultSizeProp(
+ props,
+ 'wp.components.CustomSelectControl',
+ '6.4'
+ );
const {
getLabelProps,
@@ -180,7 +185,7 @@ export default function CustomSelectControl( props ) {
) }
{
const [ date, setDate ] = useState( new Date() );
diff --git a/packages/components/src/date-time/date/styles.ts b/packages/components/src/date-time/date/styles.ts
index 0e7a3881d821c6..5500206abd40a2 100644
--- a/packages/components/src/date-time/date/styles.ts
+++ b/packages/components/src/date-time/date/styles.ts
@@ -10,7 +10,7 @@ import Button from '../../button';
import { COLORS, CONFIG } from '../../utils';
import { HStack } from '../../h-stack';
import { Heading } from '../../heading';
-import { space } from '../../ui/utils/space';
+import { space } from '../../utils/space';
export const Wrapper = styled.div`
box-sizing: border-box;
diff --git a/packages/components/src/date-time/time/index.tsx b/packages/components/src/date-time/time/index.tsx
index e5772bf7ab34c0..b0d8c20be74ac6 100644
--- a/packages/components/src/date-time/time/index.tsx
+++ b/packages/components/src/date-time/time/index.tsx
@@ -15,6 +15,7 @@ import { __ } from '@wordpress/i18n';
import BaseControl from '../../base-control';
import Button from '../../button';
import ButtonGroup from '../../button-group';
+import SelectControl from '../../select-control';
import TimeZone from './timezone';
import type { TimePickerProps } from '../types';
import {
@@ -24,7 +25,6 @@ import {
TimeSeparator,
MinutesInput,
MonthSelectWrapper,
- MonthSelect,
DayInput,
YearInput,
TimeWrapper,
@@ -127,7 +127,14 @@ export function TimePicker( {
method: 'hours' | 'minutes' | 'date' | 'year'
) => {
const callback: InputChangeCallback = ( value, { event } ) => {
- if ( ! ( event.target instanceof HTMLInputElement ) ) {
+ // `instanceof` checks need to get the instance definition from the
+ // corresponding window object — therefore, the following logic makes
+ // the component work correctly even when rendered inside an iframe.
+ const HTMLInputElementInstance =
+ ( event.target as HTMLInputElement )?.ownerDocument.defaultView
+ ?.HTMLInputElement ?? HTMLInputElement;
+
+ if ( ! ( event.target instanceof HTMLInputElementInstance ) ) {
return;
}
@@ -174,7 +181,7 @@ export function TimePicker( {
className="components-datetime__time-field components-datetime__time-field-day" // Unused, for backwards compatibility.
label={ __( 'Day' ) }
hideLabelFromVision
- __next36pxDefaultSize
+ __next40pxDefaultSize
value={ day }
step={ 1 }
min={ 1 }
@@ -190,10 +197,11 @@ export function TimePicker( {
const monthField = (
-
{ __( 'AM' ) }
@@ -303,6 +312,7 @@ export function TimePicker( {
variant={
am === 'PM' ? 'primary' : 'secondary'
}
+ __next40pxDefaultSize
onClick={ buildAmPmChangeCallback( 'PM' ) }
>
{ __( 'PM' ) }
@@ -338,7 +348,7 @@ export function TimePicker( {
className="components-datetime__time-field components-datetime__time-field-year" // Unused, for backwards compatibility.
label={ __( 'Year' ) }
hideLabelFromVision
- __next36pxDefaultSize
+ __next40pxDefaultSize
value={ year }
step={ 1 }
min={ 1 }
diff --git a/packages/components/src/date-time/time/styles.ts b/packages/components/src/date-time/time/styles.ts
index bc579a6731ee98..bba0cc500e0c8a 100644
--- a/packages/components/src/date-time/time/styles.ts
+++ b/packages/components/src/date-time/time/styles.ts
@@ -8,14 +8,12 @@ import { css } from '@emotion/react';
* Internal dependencies
*/
import { COLORS, CONFIG } from '../../utils';
-import { space } from '../../ui/utils/space';
+import { space } from '../../utils/space';
import {
Input,
BackdropUI,
} from '../../input-control/styles/input-control-styles';
import NumberControl from '../../number-control';
-import SelectControl from '../../select-control';
-import { Select } from '../../select-control/styles/select-control-styles';
export const Wrapper = styled.div`
box-sizing: border-box;
@@ -92,14 +90,6 @@ export const MonthSelectWrapper = styled.div`
flex-grow: 1;
`;
-export const MonthSelect = styled( SelectControl )`
- height: 36px;
-
- ${ Select } {
- line-height: 30px;
- }
-`;
-
export const DayInput = styled( NumberControl )`
${ baseInput }
diff --git a/packages/components/src/dimension-control/README.md b/packages/components/src/dimension-control/README.md
index 0dea7ed40e13b8..498322931b7ab6 100644
--- a/packages/components/src/dimension-control/README.md
+++ b/packages/components/src/dimension-control/README.md
@@ -9,8 +9,8 @@ This feature is still experimental. “Experimental” means this is an early im
## Usage
```jsx
+import { useState } from 'react';
import { __experimentalDimensionControl as DimensionControl } from '@wordpress/components';
-import { useState } from '@wordpress/element';
export default function MyCustomDimensionControl() {
const [ paddingSize, setPaddingSize ] = useState( '' );
diff --git a/packages/components/src/disabled/README.md b/packages/components/src/disabled/README.md
index c36afc2044bd20..918f6b785e3876 100644
--- a/packages/components/src/disabled/README.md
+++ b/packages/components/src/disabled/README.md
@@ -7,8 +7,8 @@ Disabled is a component which disables descendant tabbable elements and prevents
Assuming you have a form component, you can disable all form inputs by wrapping the form with ``.
```jsx
+import { useState } from 'react';
import { Button, Disabled, TextControl } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyDisabled = () => {
const [ isDisabled, setIsDisabled ] = useState( true );
diff --git a/packages/components/src/disabled/index.tsx b/packages/components/src/disabled/index.tsx
index 91c4316f3a6d82..32baac3411054c 100644
--- a/packages/components/src/disabled/index.tsx
+++ b/packages/components/src/disabled/index.tsx
@@ -8,7 +8,7 @@ import { createContext } from '@wordpress/element';
*/
import { disabledStyles } from './styles/disabled-styles';
import type { DisabledProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import { useCx } from '../utils';
const Context = createContext< boolean >( false );
diff --git a/packages/components/src/divider/component.tsx b/packages/components/src/divider/component.tsx
index 98b4edd61d4937..4ac2524456ab75 100644
--- a/packages/components/src/divider/component.tsx
+++ b/packages/components/src/divider/component.tsx
@@ -8,8 +8,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
-import { contextConnect, useContextSystem } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
+import { contextConnect, useContextSystem } from '../context';
import { DividerView } from './styles';
import type { DividerProps } from './types';
diff --git a/packages/components/src/divider/styles.ts b/packages/components/src/divider/styles.ts
index ad13e357c53b08..189f0a0223041e 100644
--- a/packages/components/src/divider/styles.ts
+++ b/packages/components/src/divider/styles.ts
@@ -7,7 +7,7 @@ import { css } from '@emotion/react';
/**
* Internal dependencies
*/
-import { space } from '../ui/utils/space';
+import { space } from '../utils/space';
import { rtl } from '../utils';
import type { DividerProps } from './types';
diff --git a/packages/components/src/divider/types.ts b/packages/components/src/divider/types.ts
index feaa0b4f4c19b0..03caedb6a5b3e7 100644
--- a/packages/components/src/divider/types.ts
+++ b/packages/components/src/divider/types.ts
@@ -7,7 +7,7 @@ import type { SeparatorProps } from 'reakit';
/**
* Internal dependencies
*/
-import type { SpaceInput } from '../ui/utils/space';
+import type { SpaceInput } from '../utils/space';
export type DividerProps = Omit<
SeparatorProps,
diff --git a/packages/components/src/drop-zone/README.md b/packages/components/src/drop-zone/README.md
index b06d278c5d1e5d..e06327ec774361 100644
--- a/packages/components/src/drop-zone/README.md
+++ b/packages/components/src/drop-zone/README.md
@@ -5,8 +5,8 @@
## Usage
```jsx
+import { useState } from 'react';
import { DropZone } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyDropZone = () => {
const [ hasDropped, setHasDropped ] = useState( false );
diff --git a/packages/components/src/drop-zone/index.tsx b/packages/components/src/drop-zone/index.tsx
index b6d35f79ac4a39..f620e6a03c6b85 100644
--- a/packages/components/src/drop-zone/index.tsx
+++ b/packages/components/src/drop-zone/index.tsx
@@ -23,7 +23,7 @@ import {
__unstableAnimatePresence as AnimatePresence,
} from '../animation';
import type { DropType, DropZoneProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
/**
* `DropZone` is a component creating a drop zone area taking the full size of its parent element. It supports dropping files, HTML content or any other HTML drop event.
diff --git a/packages/components/src/dropdown-menu-v2/index.tsx b/packages/components/src/dropdown-menu-v2/index.tsx
index 02f8322aa6a52f..085a1f2c144cce 100644
--- a/packages/components/src/dropdown-menu-v2/index.tsx
+++ b/packages/components/src/dropdown-menu-v2/index.tsx
@@ -19,7 +19,7 @@ import { SVG, Circle } from '@wordpress/primitives';
/**
* Internal dependencies
*/
-import { useContextSystem, contextConnectWithoutRef } from '../ui/context';
+import { useContextSystem, contextConnectWithoutRef } from '../context';
import { useSlot } from '../slot-fill';
import Icon from '../icon';
import { SLOT_NAME as POPOVER_DEFAULT_SLOT_NAME } from '../popover';
diff --git a/packages/components/src/dropdown-menu-v2/stories/index.story.tsx b/packages/components/src/dropdown-menu-v2/stories/index.story.tsx
index 78aee12bf1f93a..9c76a6f6f0f72f 100644
--- a/packages/components/src/dropdown-menu-v2/stories/index.story.tsx
+++ b/packages/components/src/dropdown-menu-v2/stories/index.story.tsx
@@ -32,7 +32,7 @@ import { menu, wordpress } from '@wordpress/icons';
* Internal dependencies
*/
import Icon from '../../icon';
-import { ContextSystemProvider } from '../../ui/context';
+import { ContextSystemProvider } from '../../context';
const meta: Meta< typeof DropdownMenu > = {
title: 'Components (Experimental)/DropdownMenu v2',
diff --git a/packages/components/src/dropdown-menu-v2/styles.ts b/packages/components/src/dropdown-menu-v2/styles.ts
index f1ba603797a7ff..cdeeed077b5f3a 100644
--- a/packages/components/src/dropdown-menu-v2/styles.ts
+++ b/packages/components/src/dropdown-menu-v2/styles.ts
@@ -9,7 +9,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
* Internal dependencies
*/
import { COLORS, font, rtl, CONFIG } from '../utils';
-import { space } from '../ui/utils/space';
+import { space } from '../utils/space';
import Icon from '../icon';
import type { DropdownMenuInternalContext } from './types';
diff --git a/packages/components/src/dropdown-menu-v2/types.ts b/packages/components/src/dropdown-menu-v2/types.ts
index 5c7d6469b656c9..c06bb5aac626a7 100644
--- a/packages/components/src/dropdown-menu-v2/types.ts
+++ b/packages/components/src/dropdown-menu-v2/types.ts
@@ -261,5 +261,5 @@ export type DropdownMenuPrivateContext = Pick<
DropdownMenuInternalContext,
'variant'
> & {
- portalContainer: HTMLElement | null;
+ portalContainer?: HTMLElement | null;
};
diff --git a/packages/components/src/dropdown-menu/index.tsx b/packages/components/src/dropdown-menu/index.tsx
index 9105555927f47c..5d0e69f528204a 100644
--- a/packages/components/src/dropdown-menu/index.tsx
+++ b/packages/components/src/dropdown-menu/index.tsx
@@ -11,7 +11,7 @@ import { menu } from '@wordpress/icons';
/**
* Internal dependencies
*/
-import { contextConnectWithoutRef, useContextSystem } from '../ui/context';
+import { contextConnectWithoutRef, useContextSystem } from '../context';
import Button from '../button';
import Dropdown from '../dropdown';
import { NavigableMenu } from '../navigable-container';
diff --git a/packages/components/src/dropdown-menu/types.ts b/packages/components/src/dropdown-menu/types.ts
index 7b141c97acf580..7deb08abaa28ec 100644
--- a/packages/components/src/dropdown-menu/types.ts
+++ b/packages/components/src/dropdown-menu/types.ts
@@ -6,7 +6,7 @@ import type { HTMLAttributes, ReactNode } from 'react';
* Internal dependencies
*/
import type { ButtonAsButtonProps } from '../button/types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { DropdownProps } from '../dropdown/types';
import type { Props as IconProps } from '../icon';
import type { NavigableMenuProps } from '../navigable-container/types';
diff --git a/packages/components/src/dropdown/dropdown-content-wrapper.tsx b/packages/components/src/dropdown/dropdown-content-wrapper.tsx
index ba9a15218f6f3a..ce41f24081f819 100644
--- a/packages/components/src/dropdown/dropdown-content-wrapper.tsx
+++ b/packages/components/src/dropdown/dropdown-content-wrapper.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
-import { contextConnect, useContextSystem } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
+import { contextConnect, useContextSystem } from '../context';
import { DropdownContentWrapperDiv } from './styles';
import type { DropdownContentWrapperProps } from './types';
diff --git a/packages/components/src/dropdown/index.tsx b/packages/components/src/dropdown/index.tsx
index 0a870049a5b697..0527f4f0c2cd8d 100644
--- a/packages/components/src/dropdown/index.tsx
+++ b/packages/components/src/dropdown/index.tsx
@@ -14,7 +14,7 @@ import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
-import { contextConnect, useContextSystem } from '../ui/context';
+import { contextConnect, useContextSystem } from '../context';
import { useControlledValue } from '../utils/hooks';
import Popover from '../popover';
import type { DropdownProps, DropdownInternalContext } from './types';
diff --git a/packages/components/src/dropdown/styles.ts b/packages/components/src/dropdown/styles.ts
index f64e886e8be264..6d5412f9a81213 100644
--- a/packages/components/src/dropdown/styles.ts
+++ b/packages/components/src/dropdown/styles.ts
@@ -7,7 +7,7 @@ import styled from '@emotion/styled';
/**
* Internal dependencies
*/
-import { space } from '../ui/utils/space';
+import { space } from '../utils/space';
import type { DropdownContentWrapperProps } from './types';
const padding = ( { paddingSize = 'small' }: DropdownContentWrapperProps ) => {
diff --git a/packages/components/src/duotone-picker/README.md b/packages/components/src/duotone-picker/README.md
index 4c1b5c13c4be10..550cf5774b2a76 100644
--- a/packages/components/src/duotone-picker/README.md
+++ b/packages/components/src/duotone-picker/README.md
@@ -3,8 +3,8 @@
## Usage
```jsx
+import { useState } from 'react';
import { DuotonePicker, DuotoneSwatch } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const DUOTONE_PALETTE = [
{ colors: [ '#8c00b7', '#fcff41' ], name: 'Purple and yellow', slug: 'purple-yellow' },
diff --git a/packages/components/src/duotone-picker/duotone-picker.tsx b/packages/components/src/duotone-picker/duotone-picker.tsx
index 70e5753175eac0..759c517b4759e1 100644
--- a/packages/components/src/duotone-picker/duotone-picker.tsx
+++ b/packages/components/src/duotone-picker/duotone-picker.tsx
@@ -67,6 +67,7 @@ function DuotonePicker( {
onChange,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledby,
+ ...otherProps
}: DuotonePickerProps ) {
const [ defaultDark, defaultLight ] = useMemo(
() => getDefaultColors( colorPalette ),
@@ -74,13 +75,15 @@ function DuotonePicker( {
);
const isUnset = value === 'unset';
+ const unsetOptionLabel = __( 'Unset' );
const unsetOption = (
{
onChange( isUnset ? undefined : 'unset' );
@@ -154,6 +157,7 @@ function DuotonePicker( {
return (
userEvent.setup();
-
describe( 'ExternalLink', () => {
test( 'should call function passed in onClick handler when clicking the link', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const onClickMock = jest.fn();
render(
@@ -32,7 +30,7 @@ describe( 'ExternalLink', () => {
} );
test( 'should prevent default action when clicking an internal anchor link without passing onClick prop', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
render(
I'm an anchor link!
@@ -55,7 +53,7 @@ describe( 'ExternalLink', () => {
} );
test( 'should call function passed in onClick handler and prevent default action when clicking an internal anchor link', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const onClickMock = jest.fn();
render(
@@ -76,7 +74,7 @@ describe( 'ExternalLink', () => {
} );
test( 'should not prevent default action when clicking a non anchor link without passing onClick prop', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
render(
diff --git a/packages/components/src/flex/flex-block/component.tsx b/packages/components/src/flex/flex-block/component.tsx
index 4d01f16e3abd94..c156b0af5a01f1 100644
--- a/packages/components/src/flex/flex-block/component.tsx
+++ b/packages/components/src/flex/flex-block/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { View } from '../../view';
import type { FlexBlockProps } from '../types';
import { useFlexBlock } from './hook';
diff --git a/packages/components/src/flex/flex-block/hook.ts b/packages/components/src/flex/flex-block/hook.ts
index 1c0497ac56d0fd..aa702e752a8121 100644
--- a/packages/components/src/flex/flex-block/hook.ts
+++ b/packages/components/src/flex/flex-block/hook.ts
@@ -1,8 +1,8 @@
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import { useFlexItem } from '../flex-item';
import type { FlexBlockProps } from '../types';
diff --git a/packages/components/src/flex/flex-item/component.tsx b/packages/components/src/flex/flex-item/component.tsx
index 446e2b94839705..c02d3dda7cb8f6 100644
--- a/packages/components/src/flex/flex-item/component.tsx
+++ b/packages/components/src/flex/flex-item/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { View } from '../../view';
import { useFlexItem } from './hook';
import type { FlexItemProps } from '../types';
diff --git a/packages/components/src/flex/flex-item/hook.ts b/packages/components/src/flex/flex-item/hook.ts
index db130f0b62aa0a..4de791905a4065 100644
--- a/packages/components/src/flex/flex-item/hook.ts
+++ b/packages/components/src/flex/flex-item/hook.ts
@@ -7,8 +7,8 @@ import { css } from '@emotion/react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import { useFlexContext } from '../context';
import * as styles from '../styles';
import { useCx } from '../../utils/hooks/use-cx';
diff --git a/packages/components/src/flex/flex/component.tsx b/packages/components/src/flex/flex/component.tsx
index 8fce9ea144c704..c1802c102cc764 100644
--- a/packages/components/src/flex/flex/component.tsx
+++ b/packages/components/src/flex/flex/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { useFlex } from './hook';
import { FlexContext } from './../context';
import { View } from '../../view';
diff --git a/packages/components/src/flex/flex/hook.ts b/packages/components/src/flex/flex/hook.ts
index 6ac032f75dc4c1..df53667915999c 100644
--- a/packages/components/src/flex/flex/hook.ts
+++ b/packages/components/src/flex/flex/hook.ts
@@ -12,10 +12,10 @@ import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
-import { useResponsiveValue } from '../../ui/utils/use-responsive-value';
-import { space } from '../../ui/utils/space';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
+import { useResponsiveValue } from '../../utils/use-responsive-value';
+import { space } from '../../utils/space';
import * as styles from '../styles';
import { useCx } from '../../utils';
import type { FlexProps } from '../types';
diff --git a/packages/components/src/flex/types.ts b/packages/components/src/flex/types.ts
index e36a906392b8d9..cc5553a660a831 100644
--- a/packages/components/src/flex/types.ts
+++ b/packages/components/src/flex/types.ts
@@ -6,8 +6,8 @@ import type { CSSProperties, ReactNode } from 'react';
/**
* Internal dependencies
*/
-import type { ResponsiveCSSValue } from '../ui/utils/types';
-import type { SpaceInput } from '../ui/utils/space';
+import type { ResponsiveCSSValue } from '../utils/types';
+import type { SpaceInput } from '../utils/space';
export type FlexDirection = ResponsiveCSSValue<
CSSProperties[ 'flexDirection' ]
diff --git a/packages/components/src/focal-point-picker/README.md b/packages/components/src/focal-point-picker/README.md
index 4d8c1324328766..9abb147831409a 100644
--- a/packages/components/src/focal-point-picker/README.md
+++ b/packages/components/src/focal-point-picker/README.md
@@ -8,8 +8,8 @@ Focal Point Picker is a component which creates a UI for identifying the most im
## Usage
```jsx
+import { useState } from 'react';
import { FocalPointPicker } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const Example = () => {
const [ focalPoint, setFocalPoint ] = useState( {
diff --git a/packages/components/src/focal-point-picker/focal-point.tsx b/packages/components/src/focal-point-picker/focal-point.tsx
index 21a25f5a832f34..5b82bef4bfb2ea 100644
--- a/packages/components/src/focal-point-picker/focal-point.tsx
+++ b/packages/components/src/focal-point-picker/focal-point.tsx
@@ -8,7 +8,7 @@ import { PointerCircle } from './styles/focal-point-style';
*/
import classnames from 'classnames';
import type { FocalPointProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
export default function FocalPoint( {
left = '50%',
diff --git a/packages/components/src/focal-point-picker/grid.tsx b/packages/components/src/focal-point-picker/grid.tsx
index 9b7ef9cff25f50..a52f5b11c1b2fd 100644
--- a/packages/components/src/focal-point-picker/grid.tsx
+++ b/packages/components/src/focal-point-picker/grid.tsx
@@ -7,7 +7,7 @@ import {
GridLineY,
} from './styles/focal-point-picker-style';
import type { FocalPointPickerGridProps } from './types';
-import type { WordPressComponentProps } from '../ui/context/wordpress-component';
+import type { WordPressComponentProps } from '../context/wordpress-component';
export default function FocalPointPickerGrid( {
bounds,
diff --git a/packages/components/src/focal-point-picker/index.tsx b/packages/components/src/focal-point-picker/index.tsx
index 1c9077483dfda3..65efdc322cf0c6 100644
--- a/packages/components/src/focal-point-picker/index.tsx
+++ b/packages/components/src/focal-point-picker/index.tsx
@@ -28,7 +28,7 @@ import {
} from './styles/focal-point-picker-style';
import { INITIAL_BOUNDS } from './utils';
import { useUpdateEffect } from '../utils/hooks';
-import type { WordPressComponentProps } from '../ui/context/wordpress-component';
+import type { WordPressComponentProps } from '../context/wordpress-component';
import type {
FocalPoint as FocalPointType,
FocalPointPickerProps,
diff --git a/packages/components/src/focusable-iframe/README.md b/packages/components/src/focusable-iframe/README.md
index a7cd2f0b31b5b4..f083cff58cacb8 100644
--- a/packages/components/src/focusable-iframe/README.md
+++ b/packages/components/src/focusable-iframe/README.md
@@ -32,7 +32,7 @@ Callback to invoke when iframe receives focus. Passes an emulated `FocusEvent` o
### `iframeRef`
-- Type: `wp.element.Ref`
+- Type: `React.Ref`
- Required: No
-If a reference to the underlying DOM element is needed, pass `iframeRef` as the result of a `wp.element.createRef` called from your component.
+If a reference to the underlying DOM element is needed, pass `iframeRef` as the result of a `React.createRef` called from your component.
diff --git a/packages/components/src/font-size-picker/README.md b/packages/components/src/font-size-picker/README.md
index 3eee5683407075..56391ad0c7c33f 100644
--- a/packages/components/src/font-size-picker/README.md
+++ b/packages/components/src/font-size-picker/README.md
@@ -6,8 +6,8 @@ The component renders a user interface that allows the user to select predefined
## Usage
```jsx
+import { useState } from 'react';
import { FontSizePicker } from '@wordpress/components';
-import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
const fontSizes = [
diff --git a/packages/components/src/font-size-picker/styles.ts b/packages/components/src/font-size-picker/styles.ts
index 8ba0ce661c5eb7..ef8ec8ebb30cb2 100644
--- a/packages/components/src/font-size-picker/styles.ts
+++ b/packages/components/src/font-size-picker/styles.ts
@@ -9,7 +9,7 @@ import styled from '@emotion/styled';
import BaseControl from '../base-control';
import Button from '../button';
import { HStack } from '../h-stack';
-import { space } from '../ui/utils/space';
+import { space } from '../utils/space';
import { COLORS } from '../utils';
export const Container = styled.fieldset`
diff --git a/packages/components/src/form-file-upload/README.md b/packages/components/src/form-file-upload/README.md
index 38dd50dd039155..f4dc4d1ef5a023 100644
--- a/packages/components/src/form-file-upload/README.md
+++ b/packages/components/src/form-file-upload/README.md
@@ -38,9 +38,9 @@ Children are passed as children of `Button`.
### icon
-The icon to render. Supported values are: Dashicons (specified as strings), functions, WPComponent instances and `null`.
+The icon to render. Supported values are: Dashicons (specified as strings), functions, Component instances and `null`.
-- Type: `String|Function|WPComponent|null`
+- Type: `String|Function|Component|null`
- Required: No
- Default: `null`
diff --git a/packages/components/src/form-file-upload/index.tsx b/packages/components/src/form-file-upload/index.tsx
index 608fdf837300f4..0600e47d7324c3 100644
--- a/packages/components/src/form-file-upload/index.tsx
+++ b/packages/components/src/form-file-upload/index.tsx
@@ -7,7 +7,7 @@ import { useRef } from '@wordpress/element';
* Internal dependencies
*/
import Button from '../button';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { FormFileUploadProps } from './types';
/**
diff --git a/packages/components/src/form-toggle/README.md b/packages/components/src/form-toggle/README.md
index 941e431654e50b..5d385e23f7bdec 100644
--- a/packages/components/src/form-toggle/README.md
+++ b/packages/components/src/form-toggle/README.md
@@ -54,8 +54,8 @@ When a user switches a toggle, its corresponding action takes effect immediately
### Usage
```jsx
+import { useState } from 'react';
import { FormToggle } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyFormToggle = () => {
const [ isChecked, setChecked ] = useState( true );
diff --git a/packages/components/src/form-toggle/index.tsx b/packages/components/src/form-toggle/index.tsx
index e7ee1ae0bebdee..19cb828023ad89 100644
--- a/packages/components/src/form-toggle/index.tsx
+++ b/packages/components/src/form-toggle/index.tsx
@@ -7,7 +7,7 @@ import classnames from 'classnames';
* Internal dependencies
*/
import type { FormToggleProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
export const noop = () => {};
diff --git a/packages/components/src/form-token-field/README.md b/packages/components/src/form-token-field/README.md
index 3a4ce2a143624a..41083789f1340f 100644
--- a/packages/components/src/form-token-field/README.md
+++ b/packages/components/src/form-token-field/README.md
@@ -67,8 +67,8 @@ The `value` property is handled in a manner similar to controlled form component
## Usage
```jsx
+import { useState } from 'react';
import { FormTokenField } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const continents = [
'Africa',
diff --git a/packages/components/src/form-token-field/style.scss b/packages/components/src/form-token-field/style.scss
index ba820f23c9d32d..72a5b7929a7b04 100644
--- a/packages/components/src/form-token-field/style.scss
+++ b/packages/components/src/form-token-field/style.scss
@@ -164,6 +164,7 @@
list-style: none;
box-shadow: inset 0 $border-width 0 0 $gray-600; // Matches the border color of the input.
margin: 0;
+ padding: 0;
}
.components-form-token-field__suggestion {
@@ -174,6 +175,7 @@
min-height: $button-size-compact;
margin: 0;
cursor: pointer;
+ box-sizing: border-box;
&.is-selected {
background: $components-color-accent;
diff --git a/packages/components/src/form-token-field/styles.ts b/packages/components/src/form-token-field/styles.ts
index ee67f44a5f3244..e4d41ad90c472c 100644
--- a/packages/components/src/form-token-field/styles.ts
+++ b/packages/components/src/form-token-field/styles.ts
@@ -8,7 +8,7 @@ import { css } from '@emotion/react';
* Internal dependencies
*/
import { Flex } from '../flex';
-import { space } from '../ui/utils/space';
+import { space } from '../utils/space';
import { boxSizingReset } from '../utils';
type TokensAndInputWrapperProps = {
diff --git a/packages/components/src/form-token-field/token-input.tsx b/packages/components/src/form-token-field/token-input.tsx
index 196ac03c799af8..a8367695670b20 100644
--- a/packages/components/src/form-token-field/token-input.tsx
+++ b/packages/components/src/form-token-field/token-input.tsx
@@ -12,7 +12,7 @@ import { forwardRef, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { TokenInputProps } from './types';
export function UnForwardedTokenInput(
diff --git a/packages/components/src/gradient-picker/README.md b/packages/components/src/gradient-picker/README.md
index 6d7a482df82820..c092c8b5673cc9 100644
--- a/packages/components/src/gradient-picker/README.md
+++ b/packages/components/src/gradient-picker/README.md
@@ -9,8 +9,8 @@ GradientPicker is a React component that renders a color gradient picker to defi
Render a GradientPicker.
```jsx
+import { useState } from 'react';
import { GradientPicker } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const myGradientPicker = () => {
const [ gradient, setGradient ] = useState( null );
diff --git a/packages/components/src/grid/component.tsx b/packages/components/src/grid/component.tsx
index 3ea6db0ba85448..f18909c8e1c9ae 100644
--- a/packages/components/src/grid/component.tsx
+++ b/packages/components/src/grid/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
-import { contextConnect } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
+import { contextConnect } from '../context';
import { View } from '../view';
import useGrid from './hook';
import type { GridProps } from './types';
diff --git a/packages/components/src/grid/hook.ts b/packages/components/src/grid/hook.ts
index 7fd54d6c4f19eb..621438c96df7b3 100644
--- a/packages/components/src/grid/hook.ts
+++ b/packages/components/src/grid/hook.ts
@@ -11,10 +11,10 @@ import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
-import { useContextSystem } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
+import { useContextSystem } from '../context';
import { getAlignmentProps } from './utils';
-import { useResponsiveValue } from '../ui/utils/use-responsive-value';
+import { useResponsiveValue } from '../utils/use-responsive-value';
import CONFIG from '../utils/config-values';
import { useCx } from '../utils/hooks/use-cx';
import type { GridProps } from './types';
diff --git a/packages/components/src/grid/types.ts b/packages/components/src/grid/types.ts
index 878e23f8ff5317..44258064e46a69 100644
--- a/packages/components/src/grid/types.ts
+++ b/packages/components/src/grid/types.ts
@@ -6,7 +6,7 @@ import type { CSSProperties, ReactNode } from 'react';
/**
* Internal dependencies
*/
-import type { ResponsiveCSSValue } from '../ui/utils/types';
+import type { ResponsiveCSSValue } from '../utils/types';
type GridAlignment =
| 'bottom'
diff --git a/packages/components/src/guide/page.tsx b/packages/components/src/guide/page.tsx
index dcad2780203c8a..04dbabb34854ac 100644
--- a/packages/components/src/guide/page.tsx
+++ b/packages/components/src/guide/page.tsx
@@ -7,7 +7,7 @@ import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
export default function GuidePage(
props: WordPressComponentProps< {}, 'div', false >
diff --git a/packages/components/src/h-stack/component.tsx b/packages/components/src/h-stack/component.tsx
index 51b25486f0b389..1d1efebe8a0912 100644
--- a/packages/components/src/h-stack/component.tsx
+++ b/packages/components/src/h-stack/component.tsx
@@ -1,8 +1,8 @@
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
-import { contextConnect } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
+import { contextConnect } from '../context';
import { View } from '../view';
import { useHStack } from './hook';
import type { Props } from './types';
diff --git a/packages/components/src/h-stack/hook.tsx b/packages/components/src/h-stack/hook.tsx
index cab7df1cad79c5..b7b8cf92954a1d 100644
--- a/packages/components/src/h-stack/hook.tsx
+++ b/packages/components/src/h-stack/hook.tsx
@@ -6,11 +6,11 @@ import type { ReactElement } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
-import { hasConnectNamespace, useContextSystem } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
+import { hasConnectNamespace, useContextSystem } from '../context';
import { FlexItem, useFlex } from '../flex';
import { getAlignmentProps } from './utils';
-import { getValidChildren } from '../ui/utils/get-valid-children';
+import { getValidChildren } from '../utils/get-valid-children';
import type { Props } from './types';
export function useHStack( props: WordPressComponentProps< Props, 'div' > ) {
diff --git a/packages/components/src/heading/component.tsx b/packages/components/src/heading/component.tsx
index b15739f9c17b87..a42af8b05f471d 100644
--- a/packages/components/src/heading/component.tsx
+++ b/packages/components/src/heading/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
-import { contextConnect } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
+import { contextConnect } from '../context';
import { View } from '../view';
import { useHeading } from './hook';
import type { HeadingProps } from './types';
diff --git a/packages/components/src/heading/hook.ts b/packages/components/src/heading/hook.ts
index 13153bc8530381..e493343967cba8 100644
--- a/packages/components/src/heading/hook.ts
+++ b/packages/components/src/heading/hook.ts
@@ -1,10 +1,10 @@
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
-import { useContextSystem } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
+import { useContextSystem } from '../context';
import { useText } from '../text';
-import { getHeadingFontSize } from '../ui/utils/font-size';
+import { getHeadingFontSize } from '../utils/font-size';
import { CONFIG, COLORS } from '../utils';
import type { HeadingProps } from './types';
diff --git a/packages/components/src/higher-order/with-constrained-tabbing/README.md b/packages/components/src/higher-order/with-constrained-tabbing/README.md
index e6ef44b061e3c7..47cb8a033dfbdd 100644
--- a/packages/components/src/higher-order/with-constrained-tabbing/README.md
+++ b/packages/components/src/higher-order/with-constrained-tabbing/README.md
@@ -7,12 +7,12 @@
Wrap your original component with `withConstrainedTabbing`.
```jsx
+import { useState } from 'react';
import {
withConstrainedTabbing,
TextControl,
Button,
} from '@wordpress/components';
-import { useState } from '@wordpress/element';
const ConstrainedTabbing = withConstrainedTabbing(
( { children } ) => children
diff --git a/packages/components/src/higher-order/with-focus-return/README.md b/packages/components/src/higher-order/with-focus-return/README.md
index b038e090390040..b99d76bc6f1c9a 100644
--- a/packages/components/src/higher-order/with-focus-return/README.md
+++ b/packages/components/src/higher-order/with-focus-return/README.md
@@ -7,8 +7,8 @@
### `withFocusReturn`
```jsx
+import { useState } from 'react';
import { withFocusReturn, TextControl, Button } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const EnhancedComponent = withFocusReturn( () => (
diff --git a/packages/components/src/higher-order/with-focus-return/index.tsx b/packages/components/src/higher-order/with-focus-return/index.tsx
index 2596617ad382d2..196226def624c1 100644
--- a/packages/components/src/higher-order/with-focus-return/index.tsx
+++ b/packages/components/src/higher-order/with-focus-return/index.tsx
@@ -36,7 +36,7 @@ type Props = {
*/
export default createHigherOrderComponent(
// @ts-expect-error TODO: Reconcile with intended `createHigherOrderComponent` types
- ( options: WPComponent | Record< string, unknown > ) => {
+ ( options: React.ComponentType | Record< string, unknown > ) => {
const HoC =
( { onFocusReturn }: Props = {} ) =>
( WrappedComponent: React.ComponentType ) => {
diff --git a/packages/components/src/higher-order/with-spoken-messages/index.tsx b/packages/components/src/higher-order/with-spoken-messages/index.tsx
index b987d44c4be315..5e0d52c81ad438 100644
--- a/packages/components/src/higher-order/with-spoken-messages/index.tsx
+++ b/packages/components/src/higher-order/with-spoken-messages/index.tsx
@@ -4,7 +4,7 @@
import { createHigherOrderComponent, useDebounce } from '@wordpress/compose';
import { speak } from '@wordpress/a11y';
-/** @typedef {import('@wordpress/element').WPComponent} WPComponent */
+/** @typedef {import('react').ComponentType} ComponentType */
/**
* A Higher Order Component used to be provide speak and debounced speak
@@ -12,9 +12,9 @@ import { speak } from '@wordpress/a11y';
*
* @see https://developer.wordpress.org/block-editor/packages/packages-a11y/#speak
*
- * @param {WPComponent} Component The component to be wrapped.
+ * @param {ComponentType} Component The component to be wrapped.
*
- * @return {WPComponent} The wrapped component.
+ * @return {ComponentType} The wrapped component.
*/
export default createHigherOrderComponent(
( Component ) => ( props ) => (
diff --git a/packages/components/src/icon/README.md b/packages/components/src/icon/README.md
index 4566ed9831a5df..5e78f029f169f7 100644
--- a/packages/components/src/icon/README.md
+++ b/packages/components/src/icon/README.md
@@ -67,9 +67,9 @@ The component accepts the following props. Any additional props are passed throu
### icon
-The icon to render. Supported values are: Dashicons (specified as strings), functions, WPComponent instances and `null`.
+The icon to render. Supported values are: Dashicons (specified as strings), functions, Component instances and `null`.
-- Type: `String|Function|WPComponent|null`
+- Type: `String|Function|Component|null`
- Required: No
- Default: `null`
diff --git a/packages/components/src/input-control/index.tsx b/packages/components/src/input-control/index.tsx
index b1586995caa54e..f9eafb7757e47e 100644
--- a/packages/components/src/input-control/index.tsx
+++ b/packages/components/src/input-control/index.tsx
@@ -16,9 +16,10 @@ import { useState, forwardRef } from '@wordpress/element';
import InputBase from './input-base';
import InputField from './input-field';
import type { InputControlProps } from './types';
-import { space } from '../ui/utils/space';
+import { space } from '../utils/space';
import { useDraft } from './utils';
import BaseControl from '../base-control';
+import { useDeprecated36pxDefaultSizeProp } from '../utils/use-deprecated-props';
const noop = () => {};
@@ -30,8 +31,11 @@ function useUniqueId( idProp?: string ) {
}
export function UnforwardedInputControl(
- {
- __next36pxDefaultSize,
+ props: InputControlProps,
+ ref: ForwardedRef< HTMLInputElement >
+) {
+ const {
+ __next40pxDefaultSize,
__unstableStateReducer: stateReducer = ( state ) => state,
__unstableInputWidth,
className,
@@ -50,10 +54,13 @@ export function UnforwardedInputControl(
style,
suffix,
value,
- ...props
- }: InputControlProps,
- ref: ForwardedRef< HTMLInputElement >
-) {
+ ...restProps
+ } = useDeprecated36pxDefaultSizeProp< InputControlProps >(
+ props,
+ 'wp.components.InputControl',
+ '6.4'
+ );
+
const [ isFocused, setIsFocused ] = useState( false );
const id = useUniqueId( idProp );
@@ -61,7 +68,7 @@ export function UnforwardedInputControl(
const draftHookProps = useDraft( {
value,
- onBlur: props.onBlur,
+ onBlur: restProps.onBlur,
onChange,
} );
@@ -78,7 +85,7 @@ export function UnforwardedInputControl(
__nextHasNoMarginBottom
>
,
+ ref: ForwardedRef< HTMLDivElement >
+) {
+ const {
+ __next40pxDefaultSize,
__unstableInputWidth,
children,
className,
@@ -74,16 +78,19 @@ export function InputBase(
prefix,
size = 'default',
suffix,
- ...props
- }: WordPressComponentProps< InputBaseProps, 'div' >,
- ref: ForwardedRef< HTMLDivElement >
-) {
+ ...restProps
+ } = useDeprecated36pxDefaultSizeProp(
+ props,
+ 'wp.components.InputBase',
+ '6.4'
+ );
+
const id = useUniqueId( idProp );
const hideLabel = hideLabelFromVision || ! label;
const { paddingLeft, paddingRight } = getSizeConfig( {
inputSize: size,
- __next36pxDefaultSize,
+ __next40pxDefaultSize,
} );
const prefixSuffixContextValue = useMemo( () => {
return {
@@ -95,7 +102,7 @@ export function InputBase(
return (
// @ts-expect-error The `direction` prop from Flex (FlexDirection) conflicts with legacy SVGAttributes `direction` (string) that come from React intrinsic prop definitions.
`
`;
type InputProps = {
- __next36pxDefaultSize?: boolean;
+ __next40pxDefaultSize?: boolean;
disabled?: boolean;
inputSize?: Size;
isDragging?: boolean;
@@ -120,14 +120,14 @@ const fontSizeStyles = ( { inputSize: size }: InputProps ) => {
export const getSizeConfig = ( {
inputSize: size,
- __next36pxDefaultSize,
+ __next40pxDefaultSize,
}: InputProps ) => {
// Paddings may be overridden by the custom paddings props.
const sizes = {
default: {
- height: 36,
+ height: 40,
lineHeight: 1,
- minHeight: 36,
+ minHeight: 40,
paddingLeft: space( 4 ),
paddingRight: space( 4 ),
},
@@ -147,7 +147,7 @@ export const getSizeConfig = ( {
},
};
- if ( ! __next36pxDefaultSize ) {
+ if ( ! __next40pxDefaultSize ) {
sizes.default = {
height: 30,
lineHeight: 1,
diff --git a/packages/components/src/input-control/test/index.js b/packages/components/src/input-control/test/index.js
index f93fe9f0915284..1ca2860442bd95 100644
--- a/packages/components/src/input-control/test/index.js
+++ b/packages/components/src/input-control/test/index.js
@@ -14,8 +14,6 @@ import { useState } from '@wordpress/element';
*/
import BaseInputControl from '../';
-const setupUser = () => userEvent.setup();
-
const getInput = () => screen.getByTestId( 'input' );
describe( 'InputControl', () => {
@@ -70,7 +68,7 @@ describe( 'InputControl', () => {
describe( 'Ensurance of focus for number inputs', () => {
it( 'should focus its input on mousedown events', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const spy = jest.fn();
render( );
const target = getInput();
@@ -87,7 +85,7 @@ describe( 'InputControl', () => {
describe( 'Value', () => {
it( 'should update value onChange', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const spy = jest.fn();
render(
spy( v ) } />
@@ -102,7 +100,7 @@ describe( 'InputControl', () => {
} );
it( 'should work as a controlled component given normal, falsy or nullish values', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const spy = jest.fn();
const heldKeySet = new Set();
const Example = () => {
@@ -173,7 +171,7 @@ describe( 'InputControl', () => {
} );
it( 'should not commit value until blurred when isPressEnterToChange is true', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const spy = jest.fn();
render(
{
} );
it( 'should commit value when blurred if value is invalid', async () => {
- const user = setupUser();
+ const user = await userEvent.setup();
const spyChange = jest.fn();
render(
= (
extra: { event: SyntheticEvent } & P
) => void;
-export interface InputFieldProps extends BaseProps {
+export interface InputFieldProps
+ extends Omit< BaseProps, '__next36pxDefaultSize' > {
/**
* Determines the drag axis.
*
diff --git a/packages/components/src/item-group/item-group/component.tsx b/packages/components/src/item-group/item-group/component.tsx
index 34f73c38e2b57a..6bfae11dd4f85f 100644
--- a/packages/components/src/item-group/item-group/component.tsx
+++ b/packages/components/src/item-group/item-group/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { useItemGroup } from './hook';
import { ItemGroupContext, useItemGroupContext } from '../context';
import { View } from '../../view';
diff --git a/packages/components/src/item-group/item-group/hook.ts b/packages/components/src/item-group/item-group/hook.ts
index 77327b91df5a6c..0078e009050fc4 100644
--- a/packages/components/src/item-group/item-group/hook.ts
+++ b/packages/components/src/item-group/item-group/hook.ts
@@ -1,8 +1,8 @@
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
/**
* Internal dependencies
diff --git a/packages/components/src/item-group/item/component.tsx b/packages/components/src/item-group/item/component.tsx
index 9104f5340377bd..f3f11617312a7c 100644
--- a/packages/components/src/item-group/item/component.tsx
+++ b/packages/components/src/item-group/item/component.tsx
@@ -8,8 +8,8 @@ import type { ForwardedRef } from 'react';
*/
import type { ItemProps } from '../types';
import { useItem } from './hook';
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { View } from '../../view';
function UnconnectedItem(
diff --git a/packages/components/src/item-group/item/hook.ts b/packages/components/src/item-group/item/hook.ts
index d1bc632ddb7f3e..188b575e072c87 100644
--- a/packages/components/src/item-group/item/hook.ts
+++ b/packages/components/src/item-group/item/hook.ts
@@ -11,8 +11,8 @@ import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import * as styles from '../styles';
import { useItemGroupContext } from '../context';
import { useCx } from '../../utils/hooks/use-cx';
diff --git a/packages/components/src/keyboard-shortcuts/README.md b/packages/components/src/keyboard-shortcuts/README.md
index 081e5519582595..9e7d8d51c0cc23 100644
--- a/packages/components/src/keyboard-shortcuts/README.md
+++ b/packages/components/src/keyboard-shortcuts/README.md
@@ -11,8 +11,8 @@ It uses the [Mousetrap](https://craig.is/killing/mice) library to implement keyb
Render ` ` with a `shortcuts` prop object:
```jsx
+import { useState } from 'react';
import { KeyboardShortcuts } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyKeyboardShortcuts = () => {
const [ isAllSelected, setIsAllSelected ] = useState( false );
diff --git a/packages/components/src/menu-item/README.md b/packages/components/src/menu-item/README.md
index a13d8dbd5eabed..9a0f97e97ac890 100644
--- a/packages/components/src/menu-item/README.md
+++ b/packages/components/src/menu-item/README.md
@@ -5,8 +5,8 @@ MenuItem is a component which renders a button intended to be used in combinatio
## Usage
```jsx
+import { useState } from 'react';
import { MenuItem } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyMenuItem = () => {
const [ isActive, setIsActive ] = useState( true );
@@ -29,7 +29,7 @@ MenuItem supports the following props. Any additional props are passed through t
### `children`
-- Type: `WPElement`
+- Type: `Element`
- Required: No
Element to render as child of button.
@@ -89,7 +89,7 @@ If shortcut is a string, it is expecting the display text. If shortcut is an obj
### `suffix`
-- Type: `WPElement`
+- Type: `Element`
- Required: No
Allows for markup other than icons or shortcuts to be added to the menu item.
diff --git a/packages/components/src/menu-item/index.tsx b/packages/components/src/menu-item/index.tsx
index 2904763cc6c58b..644d5bff2b023e 100644
--- a/packages/components/src/menu-item/index.tsx
+++ b/packages/components/src/menu-item/index.tsx
@@ -15,7 +15,7 @@ import { cloneElement, forwardRef } from '@wordpress/element';
import Shortcut from '../shortcut';
import Button from '../button';
import Icon from '../icon';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { MenuItemProps } from './types';
function UnforwardedMenuItem(
diff --git a/packages/components/src/menu-items-choice/README.md b/packages/components/src/menu-items-choice/README.md
index b5527c3bc33e3a..7aa12bbbb74ff9 100644
--- a/packages/components/src/menu-items-choice/README.md
+++ b/packages/components/src/menu-items-choice/README.md
@@ -54,8 +54,8 @@ Designs with a `MenuItemsChoice` option selected by default make a strong sugges
### Usage
```jsx
+import { useState } from 'react';
import { MenuGroup, MenuItemsChoice } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyMenuItemsChoice = () => {
const [ mode, setMode ] = useState( 'visual' );
diff --git a/packages/components/src/mobile/bottom-sheet/sub-sheet/README.md b/packages/components/src/mobile/bottom-sheet/sub-sheet/README.md
index 1e60bb74ac5347..4953f294d40970 100644
--- a/packages/components/src/mobile/bottom-sheet/sub-sheet/README.md
+++ b/packages/components/src/mobile/bottom-sheet/sub-sheet/README.md
@@ -8,13 +8,13 @@ BottomSheetSubSheet allows for adding controls inside the React Native bottom sh
/**
* External dependencies
*/
+import { useState } from 'react';
import { SafeAreaView, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
/**
* WordPress dependencies
*/
-import { useState } from '@wordpress/element';
import { Icon, chevronRight } from '@wordpress/icons';
import { BottomSheet } from '@wordpress/components';
diff --git a/packages/components/src/mobile/html-text-input/index.native.js b/packages/components/src/mobile/html-text-input/index.native.js
index 18bdc2caa3ac08..16604bbc4e3eb0 100644
--- a/packages/components/src/mobile/html-text-input/index.native.js
+++ b/packages/components/src/mobile/html-text-input/index.native.js
@@ -81,7 +81,10 @@ export class HTMLTextInput extends Component {
title,
} = this.props;
const titleStyle = [
- styles.htmlViewTitle,
+ getStylesFromColorScheme(
+ styles.htmlViewTitle,
+ styles.htmlViewTitleDark
+ ),
style?.text && { color: style.text },
];
const htmlStyle = [
diff --git a/packages/components/src/mobile/html-text-input/style.scss b/packages/components/src/mobile/html-text-input/style.scss
index 89b81e898ad4bb..47102aca755044 100644
--- a/packages/components/src/mobile/html-text-input/style.scss
+++ b/packages/components/src/mobile/html-text-input/style.scss
@@ -37,3 +37,7 @@ $textColorDark: $white;
.scrollView {
flex: 1;
}
+
+.htmlViewTitleDark {
+ color: $textColorDark;
+}
diff --git a/packages/components/src/mobile/html-text-input/test/__snapshots__/index.native.js.snap b/packages/components/src/mobile/html-text-input/test/__snapshots__/index.native.js.snap
index ace4108a9968af..dd3272bef4d715 100644
--- a/packages/components/src/mobile/html-text-input/test/__snapshots__/index.native.js.snap
+++ b/packages/components/src/mobile/html-text-input/test/__snapshots__/index.native.js.snap
@@ -14,7 +14,9 @@ exports[`HTMLTextInput HTMLTextInput renders and matches snapshot 1`] = `
placeholderTextColor="white"
style={
[
- undefined,
+ {
+ "color": "white",
+ },
undefined,
]
}
diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js
index 8b233612763814..8dc58008a3600d 100644
--- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js
+++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js
@@ -36,7 +36,7 @@ const AnimatedScrollView = Animated.createAnimatedComponent( ScrollView );
* @param {Object} props.scrollViewStyle Additional style for the ScrollView component.
* @param {boolean} props.shouldPreventAutomaticScroll Whether to prevent scrolling when there's a Keyboard offset set.
* @param {Object} props... Other props to pass to the FlatList component.
- * @return {WPComponent} KeyboardAwareFlatList component.
+ * @return {Component} KeyboardAwareFlatList component.
*/
export const KeyboardAwareFlatList = ( {
extraScrollHeight,
diff --git a/packages/components/src/modal/README.md b/packages/components/src/modal/README.md
index 01cad7d6ff2e0f..a2165afde9d5ca 100644
--- a/packages/components/src/modal/README.md
+++ b/packages/components/src/modal/README.md
@@ -120,8 +120,8 @@ The modal is used to create an accessible modal over an application.
The following example shows you how to properly implement a modal. For the modal to properly work it's important you implement the close logic for the modal properly.
```jsx
+import { useState } from 'react';
import { Button, Modal } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyModal = () => {
const [ isOpen, setOpen ] = useState( false );
@@ -187,10 +187,16 @@ Titles are required for accessibility reasons, see `aria.labelledby` and `title`
- Required: No
-#### `focusOnMount`: `boolean | 'firstElement'`
+#### `focusOnMount`: `boolean | 'firstElement'` | 'firstContentElement'
If this property is true, it will focus the first tabbable element rendered in the modal.
+If this property is false, focus will not be transferred and it is the responsibility of the consumer to ensure accessible focus management.
+
+If set to `firstElement` focus will be placed on the first tabbable element anywhere within the Modal.
+
+If set to `firstContentElement` focus will be placed on the first tabbable element within the Modal's **content** (i.e. children). Note that it is the responsibility of the consumer to ensure there is at least one tabbable element within the children **or the focus will be lost**.
+
- Required: No
- Default: `true`
@@ -215,6 +221,14 @@ This property when set to `true` will render a full screen modal.
- Required: No
- Default: `false`
+#### `size`: `'small' | 'medium' | 'large' | 'fill'`
+
+If this property is added it will cause the modal to render at a preset width, or expand to fill the screen. This prop will be ignored if `isFullScreen` is set to `true`.
+
+- Required: No
+
+Note: `Modal`'s width can also be controlled by adjusting the width of the modal's contents via CSS.
+
#### `onRequestClose`: ``
This function is called to indicate that the modal should be closed.
diff --git a/packages/components/src/modal/aria-helper.ts b/packages/components/src/modal/aria-helper.ts
index 25ef449a30d3df..6d4427ddea948e 100644
--- a/packages/components/src/modal/aria-helper.ts
+++ b/packages/components/src/modal/aria-helper.ts
@@ -6,8 +6,7 @@ const LIVE_REGION_ARIA_ROLES = new Set( [
'timer',
] );
-let hiddenElements: Element[] = [],
- isHidden = false;
+const hiddenElementsByDepth: Element[][] = [];
/**
* Hides all elements in the body element from screen-readers except
@@ -19,31 +18,28 @@ let hiddenElements: Element[] = [],
* we should consider removing these helper functions in favor of
* `aria-modal="true"`.
*
- * @param {HTMLDivElement} unhiddenElement The element that should not be hidden.
+ * @param modalElement The element that should not be hidden.
*/
-export function hideApp( unhiddenElement?: HTMLDivElement ) {
- if ( isHidden ) {
- return;
- }
+export function modalize( modalElement?: HTMLDivElement ) {
const elements = Array.from( document.body.children );
- elements.forEach( ( element ) => {
- if ( element === unhiddenElement ) {
- return;
- }
+ const hiddenElements: Element[] = [];
+ hiddenElementsByDepth.push( hiddenElements );
+ for ( const element of elements ) {
+ if ( element === modalElement ) continue;
+
if ( elementShouldBeHidden( element ) ) {
element.setAttribute( 'aria-hidden', 'true' );
hiddenElements.push( element );
}
- } );
- isHidden = true;
+ }
}
/**
* Determines if the passed element should not be hidden from screen readers.
*
- * @param {HTMLElement} element The element that should be checked.
+ * @param element The element that should be checked.
*
- * @return {boolean} Whether the element should not be hidden from screen-readers.
+ * @return Whether the element should not be hidden from screen-readers.
*/
export function elementShouldBeHidden( element: Element ) {
const role = element.getAttribute( 'role' );
@@ -56,16 +52,12 @@ export function elementShouldBeHidden( element: Element ) {
}
/**
- * Makes all elements in the body that have been hidden by `hideApp`
- * visible again to screen-readers.
+ * Accessibly reveals the elements hidden by the latest modal.
*/
-export function showApp() {
- if ( ! isHidden ) {
- return;
- }
- hiddenElements.forEach( ( element ) => {
+export function unmodalize() {
+ const hiddenElements = hiddenElementsByDepth.pop();
+ if ( ! hiddenElements ) return;
+
+ for ( const element of hiddenElements )
element.removeAttribute( 'aria-hidden' );
- } );
- hiddenElements = [];
- isHidden = false;
}
diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx
index d21a3f9ae3535e..2746c40fcaab02 100644
--- a/packages/components/src/modal/index.tsx
+++ b/packages/components/src/modal/index.tsx
@@ -66,16 +66,29 @@ function UnforwardedModal(
contentLabel,
onKeyDown,
isFullScreen = false,
+ size,
headerActions = null,
__experimentalHideHeader = false,
} = props;
const ref = useRef< HTMLDivElement >();
+
const instanceId = useInstanceId( Modal );
const headingId = title
? `components-modal-header-${ instanceId }`
: aria.labelledby;
- const focusOnMountRef = useFocusOnMount( focusOnMount );
+
+ // The focus hook does not support 'firstContentElement' but this is a valid
+ // value for the Modal's focusOnMount prop. The following code ensures the focus
+ // hook will focus the first focusable node within the element to which it is applied.
+ // When `firstContentElement` is passed as the value of the focusOnMount prop,
+ // the focus hook is applied to the Modal's content element.
+ // Otherwise, the focus hook is applied to the Modal's ref. This ensures that the
+ // focus hook will focus the first element in the Modal's **content** when
+ // `firstContentElement` is passed.
+ const focusOnMountRef = useFocusOnMount(
+ focusOnMount === 'firstContentElement' ? 'firstElement' : focusOnMount
+ );
const constrainedTabbingRef = useConstrainedTabbing();
const focusReturnRef = useFocusReturn();
const focusOutsideProps = useFocusOutside( onRequestClose );
@@ -85,6 +98,13 @@ function UnforwardedModal(
const [ hasScrolledContent, setHasScrolledContent ] = useState( false );
const [ hasScrollableContent, setHasScrollableContent ] = useState( false );
+ let sizeClass;
+ if ( isFullScreen || size === 'fill' ) {
+ sizeClass = 'is-full-screen';
+ } else if ( size ) {
+ sizeClass = `has-size-${ size }`;
+ }
+
// Determines whether the Modal content is scrollable and updates the state.
const isContentScrollable = useCallback( () => {
if ( ! contentRef.current ) {
@@ -100,11 +120,15 @@ function UnforwardedModal(
}
}, [ contentRef ] );
+ useEffect( () => {
+ ariaHelper.modalize( ref.current );
+ return () => ariaHelper.unmodalize();
+ }, [] );
+
useEffect( () => {
openModalCount++;
if ( openModalCount === 1 ) {
- ariaHelper.hideApp( ref.current );
document.body.classList.add( bodyOpenClassName );
}
@@ -113,7 +137,6 @@ function UnforwardedModal(
if ( openModalCount === 0 ) {
document.body.classList.remove( bodyOpenClassName );
- ariaHelper.showApp();
}
};
}, [ bodyOpenClassName ] );
@@ -214,16 +237,16 @@ function UnforwardedModal(
) }
-
{ children }
+
+
+ { children }
+
diff --git a/packages/components/src/modal/stories/index.story.tsx b/packages/components/src/modal/stories/index.story.tsx
index 8405a6eb0113e3..60a53947116fac 100644
--- a/packages/components/src/modal/stories/index.story.tsx
+++ b/packages/components/src/modal/stories/index.story.tsx
@@ -28,7 +28,8 @@ const meta: Meta< typeof Modal > = {
control: { type: null },
},
focusOnMount: {
- control: { type: 'boolean' },
+ options: [ true, false, 'firstElement', 'firstContentElement' ],
+ control: { type: 'select' },
},
role: {
control: { type: 'text' },
@@ -60,11 +61,7 @@ const Template: StoryFn< typeof Modal > = ( { onRequestClose, ...args } ) => {
Open Modal
{ isOpen && (
-
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et magna
@@ -100,6 +97,12 @@ Default.parameters = {
},
};
+export const WithsizeSmall: StoryFn< typeof Modal > = Template.bind( {} );
+WithsizeSmall.args = {
+ size: 'small',
+};
+WithsizeSmall.storyName = 'With size: small';
+
const LikeButton = () => {
const [ isLiked, setIsLiked ] = useState( false );
return (
diff --git a/packages/components/src/modal/style.scss b/packages/components/src/modal/style.scss
index d4ca7c311fcf19..0933f836892f1d 100644
--- a/packages/components/src/modal/style.scss
+++ b/packages/components/src/modal/style.scss
@@ -50,6 +50,26 @@
max-width: none;
}
}
+
+ &.has-size-small,
+ &.has-size-medium,
+ &.has-size-large {
+ width: 100%;
+ }
+
+ // The following widths were selected to align with existing baselines
+ // found elsewhere in the editor.
+ // See https://github.com/WordPress/gutenberg/pull/54471#issuecomment-1723818809
+ &.has-size-small {
+ max-width: $modal-width-small;
+ }
+ &.has-size-medium {
+ max-width: $modal-width-medium;
+ }
+ &.has-size-large {
+ max-width: $modal-width-large;
+ }
+
}
@include break-large() {
diff --git a/packages/components/src/modal/test/index.tsx b/packages/components/src/modal/test/index.tsx
index d3bd6aea888132..69f28508c14059 100644
--- a/packages/components/src/modal/test/index.tsx
+++ b/packages/components/src/modal/test/index.tsx
@@ -13,6 +13,7 @@ import { useState } from '@wordpress/element';
* Internal dependencies
*/
import Modal from '../';
+import type { ModalProps } from '../types';
const noop = () => {};
@@ -166,8 +167,7 @@ describe( 'Modal', () => {
expect( onRequestClose ).not.toHaveBeenCalled();
} );
- // TODO enable once nested modals hide outer modals.
- it.skip( 'should accessibly hide and show siblings including outer modals', async () => {
+ it( 'should accessibly hide and show siblings including outer modals', async () => {
const user = userEvent.setup();
const AriaDemo = () => {
@@ -236,4 +236,127 @@ describe( 'Modal', () => {
screen.getByText( 'A sweet button', { selector: 'button' } )
).toBeInTheDocument();
} );
+
+ describe( 'Focus handling', () => {
+ let originalGetClientRects: () => DOMRectList;
+
+ const FocusMountDemo = ( {
+ focusOnMount,
+ }: Pick< ModalProps, 'focusOnMount' > ) => {
+ const [ isShown, setIsShown ] = useState( false );
+ return (
+ <>
+ setIsShown( true ) }>
+ Toggle Modal
+
+ { isShown && (
+ setIsShown( false ) }
+ >
+ Modal content
+
+ First Focusable Content Element
+
+
+
+ Another Focusable Content Element
+
+
+ ) }
+ >
+ );
+ };
+
+ beforeEach( () => {
+ /**
+ * The test environment does not have a layout engine, so we need to mock
+ * the getClientRects method. This ensures that the focusable elements can be
+ * found by the `focusOnMount` logic which depends on layout information
+ * to determine if the element is visible or not.
+ * See https://github.com/WordPress/gutenberg/blob/trunk/packages/dom/src/focusable.js#L55-L61.
+ */
+ // @ts-expect-error We're not trying to comply to the DOM spec, only mocking
+ window.HTMLElement.prototype.getClientRects = function () {
+ return [ 'trick-jsdom-into-having-size-for-element-rect' ];
+ };
+ } );
+
+ afterEach( () => {
+ // Restore original HTMLElement prototype.
+ // See beforeEach for details.
+ window.HTMLElement.prototype.getClientRects =
+ originalGetClientRects;
+ } );
+
+ it( 'should focus the Modal dialog by default when `focusOnMount` prop is not provided', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const opener = screen.getByRole( 'button', {
+ name: 'Toggle Modal',
+ } );
+
+ await user.click( opener );
+
+ expect( screen.getByRole( 'dialog' ) ).toHaveFocus();
+ } );
+
+ it( 'should focus the Modal dialog when `true` passed as value for `focusOnMount` prop', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const opener = screen.getByRole( 'button', {
+ name: 'Toggle Modal',
+ } );
+
+ await user.click( opener );
+
+ expect( screen.getByRole( 'dialog' ) ).toHaveFocus();
+ } );
+
+ it( 'should focus the first focusable element in the contents (if found) when `firstContentElement` passed as value for `focusOnMount` prop', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const opener = screen.getByRole( 'button' );
+
+ await user.click( opener );
+
+ expect(
+ screen.getByText( 'First Focusable Content Element' )
+ ).toHaveFocus();
+ } );
+
+ it( 'should focus the first element anywhere within the Modal when `firstElement` passed as value for `focusOnMount` prop', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const opener = screen.getByRole( 'button' );
+
+ await user.click( opener );
+
+ expect(
+ screen.getByRole( 'button', { name: 'Close' } )
+ ).toHaveFocus();
+ } );
+
+ it( 'should not move focus when `false` passed as value for `focusOnMount` prop', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const opener = screen.getByRole( 'button', {
+ name: 'Toggle Modal',
+ } );
+
+ await user.click( opener );
+
+ expect( opener ).toHaveFocus();
+ } );
+ } );
} );
diff --git a/packages/components/src/modal/types.ts b/packages/components/src/modal/types.ts
index 2106a6087e9943..0a422718b1be14 100644
--- a/packages/components/src/modal/types.ts
+++ b/packages/components/src/modal/types.ts
@@ -68,7 +68,9 @@ export type ModalProps = {
*
* @default true
*/
- focusOnMount?: Parameters< typeof useFocusOnMount >[ 0 ];
+ focusOnMount?:
+ | Parameters< typeof useFocusOnMount >[ 0 ]
+ | 'firstContentElement';
/**
* Elements that are injected into the modal header to the left of the close button (if rendered).
* Hidden if `__experimentalHideHeader` is `true`.
@@ -94,6 +96,15 @@ export type ModalProps = {
* @default false
*/
isFullScreen?: boolean;
+ /**
+ * If this property is added it will cause the modal to render at a preset
+ * width, or expand to fill the screen. This prop will be ignored if
+ * `isFullScreen` is set to `true`.
+ *
+ * Note: `Modal`'s width can also be controlled by adjusting the width of the
+ * modal's contents, or via CSS using the `style` prop.
+ */
+ size?: 'small' | 'medium' | 'large' | 'fill';
/**
* Handle the key down on the modal frame `div`.
*/
diff --git a/packages/components/src/navigable-container/types.ts b/packages/components/src/navigable-container/types.ts
index e64ff575069acd..709aee447d3e04 100644
--- a/packages/components/src/navigable-container/types.ts
+++ b/packages/components/src/navigable-container/types.ts
@@ -6,7 +6,7 @@ import type { ForwardedRef, ReactNode } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
type BaseProps = {
/**
diff --git a/packages/components/src/navigation/styles/navigation-styles.tsx b/packages/components/src/navigation/styles/navigation-styles.tsx
index 7c3ea34f99899d..27c508b98f26dc 100644
--- a/packages/components/src/navigation/styles/navigation-styles.tsx
+++ b/packages/components/src/navigation/styles/navigation-styles.tsx
@@ -16,7 +16,7 @@ import Button from '../../button';
import { Text } from '../../text';
import { Heading } from '../../heading';
import { reduceMotion, rtl } from '../../utils';
-import { space } from '../../ui/utils/space';
+import { space } from '../../utils/space';
import SearchControl from '../../search-control';
export const NavigationUI = styled.div`
diff --git a/packages/components/src/navigator/navigator-back-button/component.tsx b/packages/components/src/navigator/navigator-back-button/component.tsx
index bf005413fdf718..71c5ac14cd00d9 100644
--- a/packages/components/src/navigator/navigator-back-button/component.tsx
+++ b/packages/components/src/navigator/navigator-back-button/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { View } from '../../view';
import { useNavigatorBackButton } from './hook';
import type { NavigatorBackButtonProps } from '../types';
diff --git a/packages/components/src/navigator/navigator-back-button/hook.ts b/packages/components/src/navigator/navigator-back-button/hook.ts
index 255a83997d071e..edf55be0f15f5b 100644
--- a/packages/components/src/navigator/navigator-back-button/hook.ts
+++ b/packages/components/src/navigator/navigator-back-button/hook.ts
@@ -6,8 +6,8 @@ import { useCallback } from '@wordpress/element';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import Button from '../../button';
import useNavigator from '../use-navigator';
import type { NavigatorBackButtonHookProps } from '../types';
diff --git a/packages/components/src/navigator/navigator-button/component.tsx b/packages/components/src/navigator/navigator-button/component.tsx
index d591758333aa9f..1b84a2315c04d3 100644
--- a/packages/components/src/navigator/navigator-button/component.tsx
+++ b/packages/components/src/navigator/navigator-button/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { View } from '../../view';
import { useNavigatorButton } from './hook';
import type { NavigatorButtonProps } from '../types';
diff --git a/packages/components/src/navigator/navigator-button/hook.ts b/packages/components/src/navigator/navigator-button/hook.ts
index 9b32c07b293c53..3952dc3fd56ba5 100644
--- a/packages/components/src/navigator/navigator-button/hook.ts
+++ b/packages/components/src/navigator/navigator-button/hook.ts
@@ -7,8 +7,8 @@ import { escapeAttribute } from '@wordpress/escape-html';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { useContextSystem } from '../../context';
import Button from '../../button';
import useNavigator from '../use-navigator';
import type { NavigatorButtonProps } from '../types';
diff --git a/packages/components/src/navigator/navigator-provider/component.tsx b/packages/components/src/navigator/navigator-provider/component.tsx
index 2d27605eab15a0..cccbb84f0d093a 100644
--- a/packages/components/src/navigator/navigator-provider/component.tsx
+++ b/packages/components/src/navigator/navigator-provider/component.tsx
@@ -20,8 +20,8 @@ import isShallowEqual from '@wordpress/is-shallow-equal';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect, useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect, useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
import { View } from '../../view';
import { NavigatorContext } from '../context';
diff --git a/packages/components/src/navigator/navigator-screen/component.tsx b/packages/components/src/navigator/navigator-screen/component.tsx
index 7a920bd7e2bc5f..ed4ab9629d3a8d 100644
--- a/packages/components/src/navigator/navigator-screen/component.tsx
+++ b/packages/components/src/navigator/navigator-screen/component.tsx
@@ -26,8 +26,8 @@ import { escapeAttribute } from '@wordpress/escape-html';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect, useContextSystem } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect, useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
import { View } from '../../view';
import { NavigatorContext } from '../context';
diff --git a/packages/components/src/navigator/navigator-to-parent-button/component.tsx b/packages/components/src/navigator/navigator-to-parent-button/component.tsx
index a717df22c74131..e73a3619f3d494 100644
--- a/packages/components/src/navigator/navigator-to-parent-button/component.tsx
+++ b/packages/components/src/navigator/navigator-to-parent-button/component.tsx
@@ -6,8 +6,8 @@ import type { ForwardedRef } from 'react';
/**
* Internal dependencies
*/
-import type { WordPressComponentProps } from '../../ui/context';
-import { contextConnect } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
+import { contextConnect } from '../../context';
import { View } from '../../view';
import { useNavigatorBackButton } from '../navigator-back-button/hook';
import type { NavigatorToParentButtonProps } from '../types';
diff --git a/packages/components/src/notice/list.tsx b/packages/components/src/notice/list.tsx
index 6129db8434ee41..5b92b6902a847d 100644
--- a/packages/components/src/notice/list.tsx
+++ b/packages/components/src/notice/list.tsx
@@ -7,7 +7,7 @@ import classnames from 'classnames';
* Internal dependencies
*/
import Notice from '.';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { NoticeListProps } from './types';
const noop = () => {};
diff --git a/packages/components/src/number-control/index.tsx b/packages/components/src/number-control/index.tsx
index a70fc500d4134f..fce0a7f76f49ed 100644
--- a/packages/components/src/number-control/index.tsx
+++ b/packages/components/src/number-control/index.tsx
@@ -20,21 +20,25 @@ import { Input, SpinButton, styles } from './styles/number-control-styles';
import * as inputControlActionTypes from '../input-control/reducer/actions';
import { add, subtract, roundClamp } from '../utils/math';
import { ensureNumber, isValueEmpty } from '../utils/values';
-import type { WordPressComponentProps } from '../ui/context/wordpress-component';
+import type { WordPressComponentProps } from '../context/wordpress-component';
import type { NumberControlProps } from './types';
import { HStack } from '../h-stack';
import { Spacer } from '../spacer';
import { useCx } from '../utils';
+import { useDeprecated36pxDefaultSizeProp } from '../utils/use-deprecated-props';
const noop = () => {};
function UnforwardedNumberControl(
- {
+ props: WordPressComponentProps< NumberControlProps, 'input', false >,
+ forwardedRef: ForwardedRef< any >
+) {
+ const {
__unstableStateReducer: stateReducerProp,
className,
dragDirection = 'n',
hideHTMLArrows = false,
- spinControls = 'native',
+ spinControls = hideHTMLArrows ? 'none' : 'native',
isDragEnabled = true,
isShiftStepEnabled = true,
label,
@@ -49,17 +53,19 @@ function UnforwardedNumberControl(
size = 'default',
suffix,
onChange = noop,
- ...props
- }: WordPressComponentProps< NumberControlProps, 'input', false >,
- forwardedRef: ForwardedRef< any >
-) {
+ ...restProps
+ } = useDeprecated36pxDefaultSizeProp< NumberControlProps >(
+ props,
+ 'wp.components.NumberControl',
+ '6.4'
+ );
+
if ( hideHTMLArrows ) {
deprecated( 'wp.components.NumberControl hideHTMLArrows prop ', {
alternative: 'spinControls="none"',
since: '6.2',
version: '6.3',
} );
- spinControls = 'none';
}
const inputRef = useRef< HTMLInputElement >();
const mergedRef = useMergeRefs( [ inputRef, forwardedRef ] );
@@ -212,7 +218,7 @@ function UnforwardedNumberControl(
{
if ( ! hideHTMLArrows ) {
diff --git a/packages/components/src/palette-edit/styles.js b/packages/components/src/palette-edit/styles.js
index 0c72493f465647..4beeb6e7c2213d 100644
--- a/packages/components/src/palette-edit/styles.js
+++ b/packages/components/src/palette-edit/styles.js
@@ -9,7 +9,7 @@ import styled from '@emotion/styled';
import Button from '../button';
import { Heading } from '../heading';
import { HStack } from '../h-stack';
-import { space } from '../ui/utils/space';
+import { space } from '../utils/space';
import { COLORS, CONFIG } from '../utils';
import { View } from '../view';
import InputControl from '../input-control';
diff --git a/packages/components/src/panel/body.tsx b/packages/components/src/panel/body.tsx
index f5ea844754cddd..46841f428fac19 100644
--- a/packages/components/src/panel/body.tsx
+++ b/packages/components/src/panel/body.tsx
@@ -14,7 +14,7 @@ import { chevronUp, chevronDown } from '@wordpress/icons';
* Internal dependencies
*/
import type { PanelBodyProps, PanelBodyTitleProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import Button from '../button';
import Icon from '../icon';
import { useControlledState, useUpdateEffect } from '../utils';
diff --git a/packages/components/src/panel/types.ts b/packages/components/src/panel/types.ts
index 9059aad53fa0a1..6b7ab35a57520a 100644
--- a/packages/components/src/panel/types.ts
+++ b/packages/components/src/panel/types.ts
@@ -2,7 +2,7 @@
* Internal dependencies
*/
import type { ButtonAsButtonProps } from '../button/types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
export type PanelProps = {
/**
diff --git a/packages/components/src/placeholder/README.md b/packages/components/src/placeholder/README.md
index f260ab15378f0f..a23fcac0e066ef 100644
--- a/packages/components/src/placeholder/README.md
+++ b/packages/components/src/placeholder/README.md
@@ -17,7 +17,7 @@ Class to set on the container div.
- Required: No
-### `icon`: `string|Function|WPComponent|null`
+### `icon`: `string|Function|Component|null`
If provided, renders an icon next to the label.
diff --git a/packages/components/src/placeholder/index.tsx b/packages/components/src/placeholder/index.tsx
index 13634f6710d945..d55741f1f41578 100644
--- a/packages/components/src/placeholder/index.tsx
+++ b/packages/components/src/placeholder/index.tsx
@@ -8,13 +8,15 @@ import classnames from 'classnames';
*/
import { useResizeObserver } from '@wordpress/compose';
import { SVG, Path } from '@wordpress/primitives';
+import { useEffect } from '@wordpress/element';
+import { speak } from '@wordpress/a11y';
/**
* Internal dependencies
*/
import Icon from '../icon';
import type { PlaceholderProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
const PlaceholderIllustration = (
{
+ if ( instructions ) {
+ speak( instructions );
+ }
+ }, [ instructions ] );
+
return (
{ withIllustration ? PlaceholderIllustration : null }
@@ -90,14 +99,12 @@ export function Placeholder(
{ label }
-
- { !! instructions && (
-
- { instructions }
-
- ) }
- { children }
-
+ { !! instructions && (
+
+ { instructions }
+
+ ) }
+ { children }
);
}
diff --git a/packages/components/src/placeholder/style.scss b/packages/components/src/placeholder/style.scss
index 1cb3edfcfdbba7..ce98350b767353 100644
--- a/packages/components/src/placeholder/style.scss
+++ b/packages/components/src/placeholder/style.scss
@@ -72,18 +72,6 @@
}
}
-// Overrides for browser and editor fieldset styles.
-.components-placeholder__fieldset.components-placeholder__fieldset {
- border: none;
- padding: 0;
-
- .components-placeholder__instructions {
- padding: 0;
- font-weight: normal;
- font-size: 1em;
- }
-}
-
.components-placeholder__fieldset.is-column-layout,
.components-placeholder__fieldset.is-column-layout form {
flex-direction: column;
diff --git a/packages/components/src/placeholder/test/index.tsx b/packages/components/src/placeholder/test/index.tsx
index c01de24eb2b055..6d9427af00b4c5 100644
--- a/packages/components/src/placeholder/test/index.tsx
+++ b/packages/components/src/placeholder/test/index.tsx
@@ -8,12 +8,13 @@ import { render, screen, within } from '@testing-library/react';
*/
import { useResizeObserver } from '@wordpress/compose';
import { SVG, Path } from '@wordpress/primitives';
+import { speak } from '@wordpress/a11y';
/**
* Internal dependencies
*/
import BasePlaceholder from '../';
-import type { WordPressComponentProps } from '../../ui/context';
+import type { WordPressComponentProps } from '../../context';
import type { PlaceholderProps } from '../types';
jest.mock( '@wordpress/compose', () => {
@@ -41,6 +42,9 @@ const Placeholder = (
const getPlaceholder = () => screen.getByTestId( 'placeholder' );
+jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn() } ) );
+const mockedSpeak = jest.mocked( speak );
+
describe( 'Placeholder', () => {
beforeEach( () => {
// @ts-ignore
@@ -48,10 +52,11 @@ describe( 'Placeholder', () => {
,
{ width: 320 },
] );
+ mockedSpeak.mockReset();
} );
describe( 'basic rendering', () => {
- it( 'should by default render label section and fieldset.', () => {
+ it( 'should by default render label section and content section.', () => {
render(
);
const placeholder = getPlaceholder();
@@ -74,9 +79,12 @@ describe( 'Placeholder', () => {
);
expect( placeholderInstructions ).not.toBeInTheDocument();
- // Test for empty fieldset.
- const placeholderFieldset =
- within( placeholder ).getByRole( 'group' );
+ // Test for empty content. When the content is empty,
+ // the only way to query the div is with `querySelector`
+ // eslint-disable-next-line testing-library/no-node-access
+ const placeholderFieldset = placeholder.querySelector(
+ '.components-placeholder__fieldset'
+ );
expect( placeholderFieldset ).toBeInTheDocument();
expect( placeholderFieldset ).toBeEmptyDOMElement();
} );
@@ -104,27 +112,38 @@ describe( 'Placeholder', () => {
expect( placeholderLabel ).toBeInTheDocument();
} );
- it( 'should display a fieldset from the children property', () => {
- const content = 'Fieldset';
+ it( 'should display content from the children property', () => {
+ const content = 'Placeholder content';
render(
{ content } );
- const placeholderFieldset = screen.getByRole( 'group' );
+ const placeholder = screen.getByText( content );
- expect( placeholderFieldset ).toBeInTheDocument();
- expect( placeholderFieldset ).toHaveTextContent( content );
+ expect( placeholder ).toBeInTheDocument();
+ expect( placeholder ).toHaveTextContent( content );
} );
- it( 'should display a legend if instructions are passed', () => {
+ it( 'should display instructions when provided', () => {
const instructions = 'Choose an option.';
render(
- Fieldset
+ Placeholder content
+
+ );
+ const placeholder = getPlaceholder();
+ const instructionsContainer =
+ within( placeholder ).getByText( instructions );
+
+ expect( instructionsContainer ).toBeInTheDocument();
+ } );
+
+ it( 'should announce instructions to screen readers', () => {
+ const instructions = 'Awesome block placeholder instructions.';
+ render(
+
+ Placeholder content
);
- const captionedFieldset = screen.getByRole( 'group', {
- name: instructions,
- } );
- expect( captionedFieldset ).toBeInTheDocument();
+ expect( speak ).toHaveBeenCalledWith( instructions );
} );
it( 'should add an additional className to the top container', () => {
diff --git a/packages/components/src/popover/README.md b/packages/components/src/popover/README.md
index 709254672be8b2..32648aa67edf76 100644
--- a/packages/components/src/popover/README.md
+++ b/packages/components/src/popover/README.md
@@ -11,8 +11,8 @@ Render a Popover adjacent to its container.
If a Popover is returned by your component, it will be shown. To hide the popover, simply omit it from your component's render value.
```jsx
+import { useState } from 'react';
import { Button, Popover } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyPopover = () => {
const [ isVisible, setIsVisible ] = useState( false );
@@ -32,8 +32,8 @@ const MyPopover = () => {
In order to pass an explicit anchor, you can use the `anchor` prop. When doing so, **the anchor element should be stored in local state** rather than a plain React ref to ensure reactive updating when it changes.
```jsx
+import { useState } from 'react';
import { Button, Popover } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyPopover = () => {
// Use internal state instead of a ref to make sure that the component
@@ -63,18 +63,17 @@ const MyPopover = () => {
By default Popovers render at the end of the body of your document. If you want Popover elements to render to a specific location on the page, you must render a `Popover.Slot` further up the element tree:
```jsx
-import { render } from '@wordpress/element';
+import { createRoot } from 'react-dom/client';
import { Popover } from '@wordpress/components';
import Content from './Content';
const app = document.getElementById( 'app' );
-
-render(
+const root = createRoot( app );
+root.render(
,
- app
+
);
```
diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx
index bb51252b549c52..19df26e0777ee1 100644
--- a/packages/components/src/popover/index.tsx
+++ b/packages/components/src/popover/index.tsx
@@ -53,13 +53,14 @@ import {
placementToMotionAnimationProps,
getReferenceElement,
} from './utils';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type {
PopoverProps,
PopoverAnchorRefReference,
PopoverAnchorRefTopBottom,
} from './types';
import { overlayMiddlewares } from './overlay-middlewares';
+import { StyleProvider } from '../style-provider';
/**
* Name of slot in which popover should fill.
@@ -447,7 +448,10 @@ const UnforwardedPopover = (
if ( shouldRenderWithinSlot ) {
content = { content } ;
} else if ( ! inline ) {
- content = createPortal( content, getPopoverFallbackContainer() );
+ content = createPortal(
+ { content } ,
+ getPopoverFallbackContainer()
+ );
}
if ( hasAnchor ) {
@@ -493,7 +497,6 @@ function PopoverSlot(
) {
return (
,
diff --git a/packages/components/src/query-controls/README.md b/packages/components/src/query-controls/README.md
index 6eea31e2af1a07..83cae88952d407 100644
--- a/packages/components/src/query-controls/README.md
+++ b/packages/components/src/query-controls/README.md
@@ -5,8 +5,8 @@
### Usage
```jsx
+import { useState } from 'react';
import { QueryControls } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const QUERY_DEFAULTS = {
category: 1,
diff --git a/packages/components/src/radio-control/README.md b/packages/components/src/radio-control/README.md
index 28708f6dbb387f..e9be7b3c669da6 100644
--- a/packages/components/src/radio-control/README.md
+++ b/packages/components/src/radio-control/README.md
@@ -58,7 +58,7 @@ Render a user interface to select the user type using radio inputs.
```jsx
import { RadioControl } from '@wordpress/components';
-import { useState } from '@wordpress/element';
+import { useState } from 'react';
const MyRadioControl = () => {
const [ option, setOption ] = useState( 'a' );
@@ -82,7 +82,7 @@ const MyRadioControl = () => {
The component accepts the following props:
-#### `help`: `string | WPElement`
+#### `help`: `string | Element`
If this property is added, a help text will be generated using help property as the content.
diff --git a/packages/components/src/radio-control/index.tsx b/packages/components/src/radio-control/index.tsx
index bbc2b2c04ed43b..4a8875bce0a2a3 100644
--- a/packages/components/src/radio-control/index.tsx
+++ b/packages/components/src/radio-control/index.tsx
@@ -13,7 +13,7 @@ import { useInstanceId } from '@wordpress/compose';
* Internal dependencies
*/
import BaseControl from '../base-control';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type { RadioControlProps } from './types';
import { VStack } from '../v-stack';
diff --git a/packages/components/src/radio-group/README.md b/packages/components/src/radio-group/README.md
index 239205ec428f75..c285e1a5d27731 100644
--- a/packages/components/src/radio-group/README.md
+++ b/packages/components/src/radio-group/README.md
@@ -53,11 +53,11 @@ Radio groups that cannot be selected can either be given a disabled state, or be
#### Controlled
```jsx
+import { useState } from 'react';
import {
__experimentalRadio as Radio,
__experimentalRadioGroup as RadioGroup,
} from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyControlledRadioRadioGroup = () => {
const [ checked, setChecked ] = useState( '25' );
@@ -77,11 +77,11 @@ const MyControlledRadioRadioGroup = () => {
When using the RadioGroup component as an uncontrolled component, the default value can be set with the `defaultChecked` prop.
```jsx
+import { useState } from 'react';
import {
__experimentalRadio as Radio,
__experimentalRadioGroup as RadioGroup,
} from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyUncontrolledRadioRadioGroup = () => {
return (
diff --git a/packages/components/src/range-control/README.md b/packages/components/src/range-control/README.md
index 2a8f7ae7a05ab1..0dd822200a7bf5 100644
--- a/packages/components/src/range-control/README.md
+++ b/packages/components/src/range-control/README.md
@@ -90,8 +90,8 @@ RangeControls should provide the full range of choices available for the user to
Render a RangeControl to make a selection from a range of incremental values.
```jsx
+import { useState } from 'react';
import { RangeControl } from '@wordpress/components';
-import { useState } from '@wordpress/element';
const MyRangeControl = () => {
const [ columns, setColumns ] = useState( 2 );
@@ -113,7 +113,7 @@ const MyRangeControl = () => {
The set of props accepted by the component will be specified below.
Props not included in this set will be applied to the input elements.
-### `afterIcon`: `string|Function|WPComponent|null`
+### `afterIcon`: `string|Function|Component|null`
If this property is added, an [Icon component](/packages/components/src/icon/README.md) will be rendered after the slider with the icon equal to `afterIcon`.
@@ -130,7 +130,7 @@ If this property is true, a button to reset the slider is rendered.
- Default: `false`
- Platform: Web | Mobile
-### `beforeIcon`: `string|Function|WPComponent|null`
+### `beforeIcon`: `string|Function|Component|null`
If this property is added, an [Icon component](/packages/components/src/icon/README.md) will be rendered before the slider with the icon equal to `beforeIcon`.
@@ -161,7 +161,7 @@ Disables the `input`, preventing new values from being applied.
- Platform: Web
-### `help`: `string|WPElement`
+### `help`: `string|Element`
If this property is added, a help text will be generated using help property as the content.
diff --git a/packages/components/src/range-control/index.tsx b/packages/components/src/range-control/index.tsx
index fbf472b66d38fd..05b162a492e849 100644
--- a/packages/components/src/range-control/index.tsx
+++ b/packages/components/src/range-control/index.tsx
@@ -36,8 +36,8 @@ import {
} from './styles/range-control-styles';
import type { RangeControlProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
-import { space } from '../ui/utils/space';
+import type { WordPressComponentProps } from '../context';
+import { space } from '../utils/space';
const noop = () => {};
diff --git a/packages/components/src/range-control/input-range.tsx b/packages/components/src/range-control/input-range.tsx
index 1daa351444686a..028f08294f3eb4 100644
--- a/packages/components/src/range-control/input-range.tsx
+++ b/packages/components/src/range-control/input-range.tsx
@@ -9,7 +9,7 @@ import { forwardRef } from '@wordpress/element';
import { InputRange as BaseInputRange } from './styles/range-control-styles';
import type { InputRangeProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
function InputRange(
props: WordPressComponentProps< InputRangeProps, 'input' >,
diff --git a/packages/components/src/range-control/mark.tsx b/packages/components/src/range-control/mark.tsx
index 892a3b3679df1e..856b540b70e1b5 100644
--- a/packages/components/src/range-control/mark.tsx
+++ b/packages/components/src/range-control/mark.tsx
@@ -9,7 +9,7 @@ import classnames from 'classnames';
import { Mark, MarkLabel } from './styles/range-control-styles';
import type { RangeMarkProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
export default function RangeMark(
props: WordPressComponentProps< RangeMarkProps, 'span' >
diff --git a/packages/components/src/range-control/rail.tsx b/packages/components/src/range-control/rail.tsx
index c284415630906c..a59cc0849246f4 100644
--- a/packages/components/src/range-control/rail.tsx
+++ b/packages/components/src/range-control/rail.tsx
@@ -9,7 +9,7 @@ import { isRTL } from '@wordpress/i18n';
import RangeMark from './mark';
import { MarksWrapper, Rail } from './styles/range-control-styles';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
import type {
MarksProps,
RangeMarkProps,
diff --git a/packages/components/src/range-control/styles/range-control-styles.ts b/packages/components/src/range-control/styles/range-control-styles.ts
index 1e4aa96bba03ba..07b744356d4f9b 100644
--- a/packages/components/src/range-control/styles/range-control-styles.ts
+++ b/packages/components/src/range-control/styles/range-control-styles.ts
@@ -9,7 +9,7 @@ import styled from '@emotion/styled';
*/
import NumberControl from '../../number-control';
import { COLORS, reduceMotion, rtl } from '../../utils';
-import { space } from '../../ui/utils/space';
+import { space } from '../../utils/space';
import type {
RangeMarkProps,
@@ -301,7 +301,7 @@ export const Tooltip = styled.span< TooltipProps >`
`;
// @todo: Refactor RangeControl with latest HStack configuration
-// @wordpress/components/ui/hstack.
+// @see: packages/components/src/h-stack
export const InputNumber = styled( NumberControl )`
display: inline-block;
font-size: 13px;
diff --git a/packages/components/src/range-control/tooltip.tsx b/packages/components/src/range-control/tooltip.tsx
index 3c8403d1275c66..c5428baab52179 100644
--- a/packages/components/src/range-control/tooltip.tsx
+++ b/packages/components/src/range-control/tooltip.tsx
@@ -14,7 +14,7 @@ import { useCallback, useEffect, useState } from '@wordpress/element';
import { Tooltip } from './styles/range-control-styles';
import type { TooltipProps } from './types';
-import type { WordPressComponentProps } from '../ui/context';
+import type { WordPressComponentProps } from '../context';
export default function SimpleTooltip(
props: WordPressComponentProps< TooltipProps, 'span' >
diff --git a/packages/components/src/sandbox/README.md b/packages/components/src/sandbox/README.md
index 406b54d3de1bdf..33d78454aaacdf 100644
--- a/packages/components/src/sandbox/README.md
+++ b/packages/components/src/sandbox/README.md
@@ -53,4 +53,10 @@ The `` of the iframe document.
The CSS class name to apply to the `` and ` ` elements of the iframe.
- Required: No
-- Default: ""
\ No newline at end of file
+- Default: ""
+
+### `tabIndex`: `HTMLElement[ 'tabIndex' ]`
+
+The `tabindex` the iframe should receive.
+
+- Required: No
diff --git a/packages/components/src/sandbox/index.tsx b/packages/components/src/sandbox/index.tsx
index ecd51e1fc26643..66c2c9cd865634 100644
--- a/packages/components/src/sandbox/index.tsx
+++ b/packages/components/src/sandbox/index.tsx
@@ -130,6 +130,7 @@ function SandBox( {
styles = [],
scripts = [],
onFocus,
+ tabIndex,
}: SandBoxProps ) {
const ref = useRef< HTMLIFrameElement >();
const [ width, setWidth ] = useState( 0 );
@@ -282,6 +283,7 @@ function SandBox( {
);
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/upload-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/upload-fonts.js
new file mode 100644
index 00000000000000..effc4a2a5227ca
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/upload-fonts.js
@@ -0,0 +1,20 @@
+/**
+ * WordPress dependencies
+ */
+import { __experimentalSpacer as Spacer } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import LocalFonts from './local-fonts';
+
+function UploadFonts() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default UploadFonts;
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/constants.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/constants.js
index 31d86b8bda2178..4b2b9d1f5fc11e 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/constants.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/constants.js
@@ -1,15 +1,7 @@
/**
* WordPress dependencies
*/
-import { __, _x } from '@wordpress/i18n';
-
-export const MODAL_TABS = [
- {
- name: 'installed-fonts',
- title: __( 'Library' ),
- className: 'installed-fonts',
- },
-];
+import { _x } from '@wordpress/i18n';
export const ALLOWED_FILE_EXTENSIONS = [ 'otf', 'ttf', 'woff', 'woff2' ];
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/filter-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/filter-fonts.js
new file mode 100644
index 00000000000000..7348eb6b054973
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/filter-fonts.js
@@ -0,0 +1,18 @@
+export default function filterFonts( fonts, filters ) {
+ const { category, search } = filters;
+ let filteredFonts = fonts || [];
+
+ if ( category && category !== 'all' ) {
+ filteredFonts = filteredFonts.filter(
+ ( font ) => font.category === category
+ );
+ }
+
+ if ( search ) {
+ filteredFonts = filteredFonts.filter( ( font ) =>
+ font.name.toLowerCase().includes( search.toLowerCase() )
+ );
+ }
+
+ return filteredFonts;
+}
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/fonts-outline.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/fonts-outline.js
new file mode 100644
index 00000000000000..bef9353e943505
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/fonts-outline.js
@@ -0,0 +1,22 @@
+export function getFontsOutline( fonts ) {
+ return fonts.reduce(
+ ( acc, font ) => ( {
+ ...acc,
+ [ font.slug ]: ( font?.fontFace || [] ).reduce(
+ ( faces, face ) => ( {
+ ...faces,
+ [ `${ face.fontStyle }-${ face.fontWeight }` ]: true,
+ } ),
+ {}
+ ),
+ } ),
+ {}
+ );
+}
+
+export function isFontFontFaceInOutline( slug, face, outline ) {
+ if ( ! face ) {
+ return !! outline[ slug ];
+ }
+ return !! outline[ slug ]?.[ `${ face.fontStyle }-${ face.fontWeight }` ];
+}
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-notice-from-response.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-notice-from-response.js
new file mode 100644
index 00000000000000..b22bd0afe23248
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-notice-from-response.js
@@ -0,0 +1,62 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+export function getNoticeFromInstallResponse( response ) {
+ const { errors = [], successes = [] } = response;
+ // Everything failed.
+ if ( errors.length && ! successes.length ) {
+ return {
+ type: 'error',
+ message: __( 'Error installing the fonts.' ),
+ };
+ }
+
+ // Eveerything succeeded.
+ if ( ! errors.length && successes.length ) {
+ return {
+ type: 'success',
+ message: __( 'Fonts were installed successfully.' ),
+ };
+ }
+
+ // Some succeeded, some failed.
+ if ( errors.length && successes.length ) {
+ return {
+ type: 'warning',
+ message: __(
+ 'Some fonts were installed successfully and some failed.'
+ ),
+ };
+ }
+}
+
+export function getNoticeFromUninstallResponse( response ) {
+ const { errors = [], successes = [] } = response;
+ // Everything failed.
+ if ( errors.length && ! successes.length ) {
+ return {
+ type: 'error',
+ message: __( 'Error uninstalling the fonts.' ),
+ };
+ }
+
+ // Everything succeeded.
+ if ( ! errors.length && successes.length ) {
+ return {
+ type: 'success',
+ message: __( 'Fonts were uninstalled successfully.' ),
+ };
+ }
+
+ // Some succeeded, some failed.
+ if ( errors.length && successes.length ) {
+ return {
+ type: 'warning',
+ message: __(
+ 'Some fonts were uninstalled successfully and some failed.'
+ ),
+ };
+ }
+}
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
index bdb3635a175272..d0a57978bcce94 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
@@ -2,6 +2,7 @@
* Internal dependencies
*/
import { FONT_WEIGHTS, FONT_STYLES } from './constants';
+import { formatFontFamily } from './preview-styles';
export function setUIValuesNeeded( font, extraValues = {} ) {
if ( ! font.name && ( font.fontFamily || font.slug ) ) {
@@ -84,10 +85,14 @@ export async function loadFontFaceInBrowser( fontFace, source, addTo = 'all' ) {
}
// eslint-disable-next-line no-undef
- const newFont = new FontFace( fontFace.fontFamily, dataSource, {
- style: fontFace.fontStyle,
- weight: fontFace.fontWeight,
- } );
+ const newFont = new FontFace(
+ formatFontFamily( fontFace.fontFamily ),
+ dataSource,
+ {
+ style: fontFace.fontStyle,
+ weight: fontFace.fontWeight,
+ }
+ );
const loadedFace = await newFont.load();
@@ -104,6 +109,10 @@ export async function loadFontFaceInBrowser( fontFace, source, addTo = 'all' ) {
}
export function getDisplaySrcFromFontFace( input, urlPrefix ) {
+ if ( ! input ) {
+ return;
+ }
+
let src;
if ( Array.isArray( input ) ) {
src = input[ 0 ];
@@ -120,71 +129,6 @@ export function getDisplaySrcFromFontFace( input, urlPrefix ) {
return src;
}
-function findNearest( input, numbers ) {
- // If the numbers array is empty, return null
- if ( numbers.length === 0 ) {
- return null;
- }
- // Sort the array based on the absolute difference with the input
- numbers.sort( ( a, b ) => Math.abs( input - a ) - Math.abs( input - b ) );
- // Return the first element (which will be the nearest) from the sorted array
- return numbers[ 0 ];
-}
-
-function extractFontWeights( fontFaces ) {
- const result = [];
-
- fontFaces.forEach( ( face ) => {
- const weights = String( face.fontWeight ).split( ' ' );
-
- if ( weights.length === 2 ) {
- const start = parseInt( weights[ 0 ] );
- const end = parseInt( weights[ 1 ] );
-
- for ( let i = start; i <= end; i += 100 ) {
- result.push( i );
- }
- } else if ( weights.length === 1 ) {
- result.push( parseInt( weights[ 0 ] ) );
- }
- } );
-
- return result;
-}
-
-export function getPreviewStyle( family ) {
- const style = { fontFamily: family.fontFamily };
-
- if ( ! Array.isArray( family.fontFace ) ) {
- style.fontWeight = '400';
- style.fontStyle = 'normal';
- return style;
- }
-
- if ( family.fontFace ) {
- //get all the font faces with normal style
- const normalFaces = family.fontFace.filter(
- ( face ) => face.fontStyle.toLowerCase() === 'normal'
- );
- if ( normalFaces.length > 0 ) {
- style.fontStyle = 'normal';
- const normalWeights = extractFontWeights( normalFaces );
- const nearestWeight = findNearest( 400, normalWeights );
- style.fontWeight = String( nearestWeight ) || '400';
- } else {
- style.fontStyle =
- ( family.fontFace.length && family.fontFace[ 0 ].fontStyle ) ||
- 'normal';
- style.fontWeight =
- ( family.fontFace.length &&
- String( family.fontFace[ 0 ].fontWeight ) ) ||
- '400';
- }
- }
-
- return style;
-}
-
export function makeFormDataFromFontFamilies( fontFamilies ) {
const formData = new FormData();
const newFontFamilies = fontFamilies.map( ( family, familyIndex ) => {
@@ -208,6 +152,6 @@ export function makeFormDataFromFontFamilies( fontFamilies ) {
}
return family;
} );
- formData.append( 'fontFamilies', JSON.stringify( newFontFamilies ) );
+ formData.append( 'font_families', JSON.stringify( newFontFamilies ) );
return formData;
}
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/preview-styles.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/preview-styles.js
new file mode 100644
index 00000000000000..b47ffb781f0486
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/preview-styles.js
@@ -0,0 +1,86 @@
+function findNearest( input, numbers ) {
+ // If the numbers array is empty, return null
+ if ( numbers.length === 0 ) {
+ return null;
+ }
+ // Sort the array based on the absolute difference with the input
+ numbers.sort( ( a, b ) => Math.abs( input - a ) - Math.abs( input - b ) );
+ // Return the first element (which will be the nearest) from the sorted array
+ return numbers[ 0 ];
+}
+
+function extractFontWeights( fontFaces ) {
+ const result = [];
+
+ fontFaces.forEach( ( face ) => {
+ const weights = String( face.fontWeight ).split( ' ' );
+
+ if ( weights.length === 2 ) {
+ const start = parseInt( weights[ 0 ] );
+ const end = parseInt( weights[ 1 ] );
+
+ for ( let i = start; i <= end; i += 100 ) {
+ result.push( i );
+ }
+ } else if ( weights.length === 1 ) {
+ result.push( parseInt( weights[ 0 ] ) );
+ }
+ } );
+
+ return result;
+}
+
+export function formatFontFamily( input ) {
+ return input
+ .split( ',' )
+ .map( ( font ) => {
+ font = font.trim(); // Remove any leading or trailing white spaces
+ // If the font doesn't have single quotes and contains a space, then add single quotes around it
+ if ( ! font.startsWith( "'" ) && font.indexOf( ' ' ) !== -1 ) {
+ return `'${ font }'`;
+ }
+ return font; // Return font as is if no transformation is needed
+ } )
+ .join( ', ' );
+}
+
+export function getFamilyPreviewStyle( family ) {
+ const style = { fontFamily: formatFontFamily( family.fontFamily ) };
+
+ if ( ! Array.isArray( family.fontFace ) ) {
+ style.fontWeight = '400';
+ style.fontStyle = 'normal';
+ return style;
+ }
+
+ if ( family.fontFace ) {
+ //get all the font faces with normal style
+ const normalFaces = family.fontFace.filter(
+ ( face ) => face.fontStyle.toLowerCase() === 'normal'
+ );
+ if ( normalFaces.length > 0 ) {
+ style.fontStyle = 'normal';
+ const normalWeights = extractFontWeights( normalFaces );
+ const nearestWeight = findNearest( 400, normalWeights );
+ style.fontWeight = String( nearestWeight ) || '400';
+ } else {
+ style.fontStyle =
+ ( family.fontFace.length && family.fontFace[ 0 ].fontStyle ) ||
+ 'normal';
+ style.fontWeight =
+ ( family.fontFace.length &&
+ String( family.fontFace[ 0 ].fontWeight ) ) ||
+ '400';
+ }
+ }
+
+ return style;
+}
+
+export function getFacePreviewStyle( face ) {
+ return {
+ fontFamily: formatFontFamily( face.fontFamily ),
+ fontStyle: face.fontStyle || 'normal',
+ fontWeight: face.fontWeight || '400',
+ };
+}
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/sort-font-faces.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/sort-font-faces.js
new file mode 100644
index 00000000000000..4d94318c9b4476
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/sort-font-faces.js
@@ -0,0 +1,33 @@
+function getNumericFontWeight( value ) {
+ switch ( value ) {
+ case 'normal':
+ return 400;
+ case 'bold':
+ return 700;
+ case 'bolder':
+ return 500;
+ case 'lighter':
+ return 300;
+ default:
+ return parseInt( value, 10 );
+ }
+}
+
+export function sortFontFaces( faces ) {
+ return faces.sort( ( a, b ) => {
+ // Ensure 'normal' fontStyle is always first
+ if ( a.fontStyle === 'normal' && b.fontStyle !== 'normal' ) return -1;
+ if ( b.fontStyle === 'normal' && a.fontStyle !== 'normal' ) return 1;
+
+ // If both fontStyles are the same, sort by fontWeight
+ if ( a.fontStyle === b.fontStyle ) {
+ return (
+ getNumericFontWeight( a.fontWeight ) -
+ getNumericFontWeight( b.fontWeight )
+ );
+ }
+
+ // Sort other fontStyles alphabetically
+ return a.fontStyle.localeCompare( b.fontStyle );
+ } );
+}
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/filter-fonts.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/filter-fonts.spec.js
new file mode 100644
index 00000000000000..4b171691d49d85
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/filter-fonts.spec.js
@@ -0,0 +1,69 @@
+/**
+ * Internal dependencies
+ */
+import filterFonts from '../filter-fonts';
+
+describe( 'filterFonts', () => {
+ const mockFonts = [
+ { name: 'Arial', category: 'sans-serif' },
+ { name: 'Arial Condensed', category: 'sans-serif' },
+ { name: 'Times New Roman', category: 'serif' },
+ { name: 'Courier New', category: 'monospace' },
+ { name: 'Romantic', category: 'cursive' },
+ ];
+
+ it( 'should return all fonts if no filters are provided', () => {
+ const result = filterFonts( mockFonts, {} );
+ expect( result ).toEqual( mockFonts );
+ } );
+
+ it( 'should filter by category', () => {
+ const result = filterFonts( mockFonts, { category: 'serif' } );
+ expect( result ).toEqual( [
+ { name: 'Times New Roman', category: 'serif' },
+ ] );
+ } );
+
+ it( 'should return all fonts if category is "all"', () => {
+ const result = filterFonts( mockFonts, { category: 'all' } );
+ expect( result ).toEqual( mockFonts );
+ } );
+
+ it( 'should filter by search', () => {
+ const result = filterFonts( mockFonts, { search: 'ari' } );
+ expect( result ).toEqual( [
+ { name: 'Arial', category: 'sans-serif' },
+ { name: 'Arial Condensed', category: 'sans-serif' },
+ ] );
+ } );
+
+ it( 'should be case insensitive when filtering by search', () => {
+ const result = filterFonts( mockFonts, { search: 'RoMANtic' } );
+ expect( result ).toEqual( [
+ { name: 'Romantic', category: 'cursive' },
+ ] );
+ } );
+
+ it( 'should filter by both category and search', () => {
+ const result = filterFonts( mockFonts, {
+ category: 'serif',
+ search: 'Times',
+ } );
+ expect( result ).toEqual( [
+ { name: 'Times New Roman', category: 'serif' },
+ ] );
+ } );
+
+ it( 'should return an empty array if no fonts match the filter criteria', () => {
+ const result = filterFonts( mockFonts, {
+ category: 'serif',
+ search: 'Arial',
+ } );
+ expect( result ).toEqual( [] );
+ } );
+
+ it( 'should return an empty array if fonts array is not provided', () => {
+ const result = filterFonts( undefined, { category: 'serif' } );
+ expect( result ).toEqual( [] );
+ } );
+} );
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/fonts-outline.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/fonts-outline.spec.js
new file mode 100644
index 00000000000000..24d9f05d028910
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/fonts-outline.spec.js
@@ -0,0 +1,109 @@
+/**
+ * Internal dependencies
+ */
+import { getFontsOutline, isFontFontFaceInOutline } from '../fonts-outline';
+
+describe( 'getFontsOutline', () => {
+ test( 'should return an empty object for an empty fonts array', () => {
+ const fonts = [];
+ const result = getFontsOutline( fonts );
+ expect( result ).toEqual( {} );
+ } );
+
+ test( 'should handle fonts with no fontFace property', () => {
+ const fonts = [
+ { slug: 'font1' },
+ {
+ slug: 'font2',
+ fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ],
+ },
+ ];
+ const result = getFontsOutline( fonts );
+ expect( result ).toEqual( {
+ font1: {},
+ font2: { 'normal-400': true },
+ } );
+ } );
+
+ test( 'should handle fonts with an empty fontFace array', () => {
+ const fonts = [ { slug: 'font1', fontFace: [] } ];
+ const result = getFontsOutline( fonts );
+ expect( result ).toEqual( {
+ font1: {},
+ } );
+ } );
+
+ test( 'should handle fonts with multiple font faces', () => {
+ const fonts = [
+ {
+ slug: 'font1',
+ fontFace: [
+ { fontStyle: 'normal', fontWeight: '400' },
+ { fontStyle: 'italic', fontWeight: '700' },
+ ],
+ },
+ ];
+ const result = getFontsOutline( fonts );
+ expect( result ).toEqual( {
+ font1: {
+ 'normal-400': true,
+ 'italic-700': true,
+ },
+ } );
+ } );
+} );
+
+describe( 'isFontFontFaceInOutline', () => {
+ test( 'should return false for an empty outline', () => {
+ const slug = 'font1';
+ const face = { fontStyle: 'normal', fontWeight: '400' };
+ const outline = {};
+
+ const result = isFontFontFaceInOutline( slug, face, outline );
+
+ expect( result ).toBe( false );
+ } );
+
+ test( 'should return false when outline has the slug but no matching fontStyle-fontWeight', () => {
+ const slug = 'font1';
+ const face = { fontStyle: 'normal', fontWeight: '400' };
+ const outline = {
+ font1: {
+ 'italic-700': true,
+ },
+ };
+
+ const result = isFontFontFaceInOutline( slug, face, outline );
+
+ expect( result ).toBe( false );
+ } );
+
+ test( 'should return false when outline does not have the slug', () => {
+ const slug = 'font1';
+ const face = { fontStyle: 'normal', fontWeight: '400' };
+ const outline = {
+ font2: {
+ 'normal-400': true,
+ },
+ };
+
+ const result = isFontFontFaceInOutline( slug, face, outline );
+
+ expect( result ).toBe( false );
+ } );
+
+ test( 'should return true when outline has the slug and the matching fontStyle-fontWeight', () => {
+ const slug = 'font1';
+ const face = { fontStyle: 'normal', fontWeight: '400' };
+ const outline = {
+ font1: {
+ 'normal-400': true,
+ 'italic-700': true,
+ },
+ };
+
+ const result = isFontFontFaceInOutline( slug, face, outline );
+
+ expect( result ).toBe( true );
+ } );
+} );
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js
index 9db0195f30072e..4adae7889cc5e5 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js
@@ -55,7 +55,7 @@ describe( 'makeFormDataFromFontFamilies', () => {
fontFamily: 'Bebas',
},
];
- expect( JSON.parse( formData.get( 'fontFamilies' ) ) ).toEqual(
+ expect( JSON.parse( formData.get( 'font_families' ) ) ).toEqual(
expectedFontFamilies
);
} );
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getPreviewStyle.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/preview-styles.spec.js
similarity index 63%
rename from packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getPreviewStyle.spec.js
rename to packages/edit-site/src/components/global-styles/font-library-modal/utils/test/preview-styles.spec.js
index 88cd91d352a807..f9f789a61fd6c0 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getPreviewStyle.spec.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/preview-styles.spec.js
@@ -1,12 +1,12 @@
/**
* Internal dependencies
*/
-import { getPreviewStyle } from '../index';
+import { getFamilyPreviewStyle, formatFontFamily } from '../preview-styles';
-describe( 'getPreviewStyle', () => {
+describe( 'getFamilyPreviewStyle', () => {
it( 'should return default fontStyle and fontWeight if fontFace is not provided', () => {
const family = { fontFamily: 'Rosario' };
- const result = getPreviewStyle( family );
+ const result = getFamilyPreviewStyle( family );
const expected = {
fontFamily: 'Rosario',
fontWeight: '400',
@@ -23,7 +23,7 @@ describe( 'getPreviewStyle', () => {
{ fontStyle: 'normal', fontWeight: '600' },
],
};
- const result = getPreviewStyle( family );
+ const result = getFamilyPreviewStyle( family );
const expected = {
fontFamily: 'Rosario',
fontStyle: 'normal',
@@ -40,7 +40,7 @@ describe( 'getPreviewStyle', () => {
{ fontStyle: 'italic', fontWeight: '600' },
],
};
- const result = getPreviewStyle( family );
+ const result = getFamilyPreviewStyle( family );
const expected = {
fontFamily: 'Rosario',
fontStyle: 'italic',
@@ -57,7 +57,7 @@ describe( 'getPreviewStyle', () => {
{ fontStyle: 'normal', fontWeight: '700' },
],
};
- const result = getPreviewStyle( family );
+ const result = getFamilyPreviewStyle( family );
const expected = {
fontFamily: 'Rosario',
fontStyle: 'normal',
@@ -74,7 +74,7 @@ describe( 'getPreviewStyle', () => {
{ fontStyle: 'normal', fontWeight: '400' },
],
};
- const result = getPreviewStyle( family );
+ const result = getFamilyPreviewStyle( family );
const expected = {
fontFamily: 'Rosario',
fontStyle: 'normal',
@@ -92,7 +92,7 @@ describe( 'getPreviewStyle', () => {
{ fontStyle: 'normal', fontWeight: '600' },
],
};
- const result = getPreviewStyle( family );
+ const result = getFamilyPreviewStyle( family );
const expected = {
fontFamily: 'Rosario',
fontStyle: 'normal',
@@ -110,7 +110,7 @@ describe( 'getPreviewStyle', () => {
{ fontStyle: 'italic', fontWeight: '200 900' },
],
};
- const result = getPreviewStyle( family );
+ const result = getFamilyPreviewStyle( family );
const expected = {
fontFamily: 'Rosario',
fontStyle: 'normal',
@@ -119,3 +119,43 @@ describe( 'getPreviewStyle', () => {
expect( result ).toEqual( expected );
} );
} );
+
+describe( 'formatFontFamily', () => {
+ it( 'should transform "Baloo 2, system-ui" correctly', () => {
+ expect( formatFontFamily( 'Baloo 2, system-ui' ) ).toBe(
+ "'Baloo 2', system-ui"
+ );
+ } );
+
+ it( 'should ignore extra spaces', () => {
+ expect( formatFontFamily( ' Baloo 2 , system-ui' ) ).toBe(
+ "'Baloo 2', system-ui"
+ );
+ } );
+
+ it( 'should keep quoted strings unchanged', () => {
+ expect(
+ formatFontFamily(
+ "Seravek, 'Gill Sans Nova', Ubuntu, Calibri, 'DejaVu Sans', source-sans-pro, sans-serif"
+ )
+ ).toBe(
+ "Seravek, 'Gill Sans Nova', Ubuntu, Calibri, 'DejaVu Sans', source-sans-pro, sans-serif"
+ );
+ } );
+
+ it( 'should wrap single font name with spaces in quotes', () => {
+ expect( formatFontFamily( 'Baloo 2' ) ).toBe( "'Baloo 2'" );
+ } );
+
+ it( 'should wrap multiple font names with spaces in quotes', () => {
+ expect( formatFontFamily( 'Baloo Bhai 2, Baloo 2' ) ).toBe(
+ "'Baloo Bhai 2', 'Baloo 2'"
+ );
+ } );
+
+ it( 'should wrap only those font names with spaces which are not already quoted', () => {
+ expect( formatFontFamily( 'Baloo Bhai 2, Arial' ) ).toBe(
+ "'Baloo Bhai 2', Arial"
+ );
+ } );
+} );
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/sort-font-faces.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/sort-font-faces.js
new file mode 100644
index 00000000000000..7f81b57a77f665
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/sort-font-faces.js
@@ -0,0 +1,74 @@
+/**
+ * Internal dependencies
+ */
+import { sortFontFaces } from '../sort-font-faces';
+
+describe( 'sortFontFaces', () => {
+ it( 'should prioritize "normal" fontStyle and then sort by numeric fontWeight values', () => {
+ const input = [
+ { fontStyle: 'normal', fontWeight: '500' },
+ { fontStyle: 'normal', fontWeight: '400' },
+ ];
+ const expected = [
+ { fontStyle: 'normal', fontWeight: '400' },
+ { fontStyle: 'normal', fontWeight: '500' },
+ ];
+ expect( sortFontFaces( input ) ).toEqual( expected );
+ } );
+
+ it( 'should correctly sort named fontWeight values within the same fontStyle', () => {
+ const input = [
+ { fontStyle: 'italic', fontWeight: 'bold' },
+ { fontStyle: 'italic', fontWeight: 'normal' },
+ ];
+ const expected = [
+ { fontStyle: 'italic', fontWeight: 'normal' },
+ { fontStyle: 'italic', fontWeight: 'bold' },
+ ];
+ expect( sortFontFaces( input ) ).toEqual( expected );
+ } );
+
+ it( 'should prioritize fontStyle "normal" over other styles', () => {
+ const input = [
+ { fontStyle: 'italic', fontWeight: '400' },
+ { fontStyle: 'normal', fontWeight: '500' },
+ ];
+ const expected = [
+ { fontStyle: 'normal', fontWeight: '500' },
+ { fontStyle: 'italic', fontWeight: '400' },
+ ];
+ expect( sortFontFaces( input ) ).toEqual( expected );
+ } );
+
+ it( 'should sort other fontStyles alphabetically after "normal"', () => {
+ const input = [
+ { fontStyle: 'oblique', fontWeight: '500' },
+ { fontStyle: 'italic', fontWeight: '500' },
+ ];
+ const expected = [
+ { fontStyle: 'italic', fontWeight: '500' },
+ { fontStyle: 'oblique', fontWeight: '500' },
+ ];
+ expect( sortFontFaces( input ) ).toEqual( expected );
+ } );
+
+ it( 'should correctly handle multiple test cases', () => {
+ const input = [
+ { fontStyle: 'oblique', fontWeight: '300' },
+ { fontStyle: 'normal', fontWeight: '400' },
+ { fontStyle: 'italic', fontWeight: '500' },
+ { fontStyle: 'italic', fontWeight: 'bold' },
+ { fontStyle: 'italic', fontWeight: '600 900' },
+ { fontStyle: 'normal', fontWeight: '500' },
+ ];
+ const expected = [
+ { fontStyle: 'normal', fontWeight: '400' },
+ { fontStyle: 'normal', fontWeight: '500' },
+ { fontStyle: 'italic', fontWeight: '500' },
+ { fontStyle: 'italic', fontWeight: '600 900' },
+ { fontStyle: 'italic', fontWeight: 'bold' },
+ { fontStyle: 'oblique', fontWeight: '300' },
+ ];
+ expect( sortFontFaces( input ) ).toEqual( expected );
+ } );
+} );
diff --git a/packages/edit-site/src/components/global-styles/screen-typography.js b/packages/edit-site/src/components/global-styles/screen-typography.js
index 644e08a6ee137a..2a895b68fa717f 100644
--- a/packages/edit-site/src/components/global-styles/screen-typography.js
+++ b/packages/edit-site/src/components/global-styles/screen-typography.js
@@ -22,7 +22,9 @@ function ScreenTypography() {
/>
- { window.__experimentalFontLibrary && }
+ { ! window.__experimentalDisableFontLibrary && (
+
+ ) }
diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js b/packages/edit-site/src/components/header-edit-mode/document-actions/index.js
index 058dd1d054aed5..33a52809b0fb09 100644
--- a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js
+++ b/packages/edit-site/src/components/header-edit-mode/document-actions/index.js
@@ -32,12 +32,19 @@ import { store as coreStore } from '@wordpress/core-data';
*/
import useEditedEntityRecord from '../../use-edited-entity-record';
import { store as editSiteStore } from '../../../store';
+import {
+ TEMPLATE_POST_TYPE,
+ NAVIGATION_POST_TYPE,
+ TEMPLATE_PART_POST_TYPE,
+ PATTERN_TYPES,
+ PATTERN_SYNC_TYPES,
+} from '../../../utils/constants';
const typeLabels = {
- wp_block: __( 'Editing pattern:' ),
- wp_navigation: __( 'Editing navigation menu:' ),
- wp_template: __( 'Editing template:' ),
- wp_template_part: __( 'Editing template part:' ),
+ [ PATTERN_TYPES.user ]: __( 'Editing pattern:' ),
+ [ NAVIGATION_POST_TYPE ]: __( 'Editing navigation menu:' ),
+ [ TEMPLATE_POST_TYPE ]: __( 'Editing template:' ),
+ [ TEMPLATE_PART_POST_TYPE ]: __( 'Editing template part:' ),
};
export default function DocumentActions() {
@@ -129,9 +136,9 @@ function TemplateDocumentActions( { className, onBack } ) {
}
let typeIcon = icon;
- if ( record.type === 'wp_navigation' ) {
+ if ( record.type === NAVIGATION_POST_TYPE ) {
typeIcon = navigationIcon;
- } else if ( record.type === 'wp_block' ) {
+ } else if ( record.type === PATTERN_TYPES.user ) {
typeIcon = symbol;
}
@@ -139,13 +146,15 @@ function TemplateDocumentActions( { className, onBack } ) {
- { typeLabels[ record.type ] ?? typeLabels.wp_template }
+ { typeLabels[ record.type ] ??
+ typeLabels[ TEMPLATE_POST_TYPE ] }
{ getTitle() }
diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js
index da682e995defd3..3528e0623fc7d5 100644
--- a/packages/edit-site/src/components/header-edit-mode/index.js
+++ b/packages/edit-site/src/components/header-edit-mode/index.js
@@ -53,7 +53,7 @@ const preventDefault = ( event ) => {
event.preventDefault();
};
-export default function HeaderEditMode() {
+export default function HeaderEditMode( { setListViewToggleElement } ) {
const inserterButton = useRef();
const {
deviceType,
@@ -259,6 +259,7 @@ export default function HeaderEditMode() {
/* translators: button label text should, if possible, be under 16 characters. */
label={ __( 'List View' ) }
onClick={ toggleListView }
+ ref={ setListViewToggleElement }
shortcut={ listViewShortcut }
showTooltip={ ! showIconLabels }
variant={
diff --git a/packages/edit-site/src/components/header-edit-mode/plugin-more-menu-item/index.js b/packages/edit-site/src/components/header-edit-mode/plugin-more-menu-item/index.js
index e507b1bf2fbc59..edb093a487e772 100644
--- a/packages/edit-site/src/components/header-edit-mode/plugin-more-menu-item/index.js
+++ b/packages/edit-site/src/components/header-edit-mode/plugin-more-menu-item/index.js
@@ -60,7 +60,7 @@ import { withPluginContext } from '@wordpress/plugins';
* );
* ```
*
- * @return {WPComponent} The component to be rendered.
+ * @return {Component} The component to be rendered.
*/
export default compose(
withPluginContext( ( context, ownProps ) => {
diff --git a/packages/edit-site/src/components/header-edit-mode/plugin-sidebar-more-menu-item/index.js b/packages/edit-site/src/components/header-edit-mode/plugin-sidebar-more-menu-item/index.js
index 3b9bbd9288142a..4d6c8008497241 100644
--- a/packages/edit-site/src/components/header-edit-mode/plugin-sidebar-more-menu-item/index.js
+++ b/packages/edit-site/src/components/header-edit-mode/plugin-sidebar-more-menu-item/index.js
@@ -48,7 +48,7 @@ import { ComplementaryAreaMoreMenuItem } from '@wordpress/interface';
* );
* ```
*
- * @return {WPComponent} The component to be rendered.
+ * @return {Component} The component to be rendered.
*/
export default function PluginSidebarMoreMenuItem( props ) {
diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js
index 80950d130a0a1b..c03ad2cd05ecd8 100644
--- a/packages/edit-site/src/components/layout/index.js
+++ b/packages/edit-site/src/components/layout/index.js
@@ -127,6 +127,8 @@ export default function Layout() {
const isEditorLoading = useIsSiteEditorLoading();
const [ isResizableFrameOversized, setIsResizableFrameOversized ] =
useState( false );
+ const [ listViewToggleElement, setListViewToggleElement ] =
+ useState( null );
// This determines which animation variant should apply to the header.
// There is also a `isDistractionFreeHovering` state that gets priority
@@ -256,7 +258,11 @@ export default function Layout() {
ease: 'easeOut',
} }
>
-
+
) }
@@ -369,6 +375,9 @@ export default function Layout() {
} }
>
- { postType === 'wp_template'
+ { postType === TEMPLATE_POST_TYPE
? _x( 'Customized', 'template' )
: _x( 'Customized', 'template part' ) }
diff --git a/packages/edit-site/src/components/list/index.js b/packages/edit-site/src/components/list/index.js
index cc01ad6ece7ff5..4fcf27239f05e2 100644
--- a/packages/edit-site/src/components/list/index.js
+++ b/packages/edit-site/src/components/list/index.js
@@ -17,6 +17,10 @@ import Header from './header';
import Table from './table';
import useTitle from '../routes/use-title';
import { unlock } from '../../lock-unlock';
+import {
+ TEMPLATE_POST_TYPE,
+ TEMPLATE_PART_POST_TYPE,
+} from '../../utils/constants';
const { useLocation } = unlock( routerPrivateApis );
@@ -25,7 +29,9 @@ export default function List() {
params: { path },
} = useLocation();
const templateType =
- path === '/wp_template/all' ? 'wp_template' : 'wp_template_part';
+ path === '/wp_template/all'
+ ? TEMPLATE_POST_TYPE
+ : TEMPLATE_PART_POST_TYPE;
useRegisterShortcuts();
diff --git a/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js b/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js
index 9d28c01164f29b..f3e021ba885244 100644
--- a/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js
+++ b/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js
@@ -9,12 +9,7 @@ import { useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
-
-const PAGE_CONTENT_BLOCK_TYPES = [
- 'core/post-title',
- 'core/post-featured-image',
- 'core/post-content',
-];
+import { PAGE_CONTENT_BLOCK_TYPES } from '../../utils/constants';
/**
* Component that when rendered, makes it so that the site editor allows only
@@ -48,8 +43,7 @@ const withDisableNonPageContentBlocks = createHigherOrderComponent(
( BlockEdit ) => ( props ) => {
const isDescendentOfQueryLoop = props.context.queryId !== undefined;
const isPageContent =
- PAGE_CONTENT_BLOCK_TYPES.includes( props.name ) &&
- ! isDescendentOfQueryLoop;
+ PAGE_CONTENT_BLOCK_TYPES[ props.name ] && ! isDescendentOfQueryLoop;
const mode = isPageContent ? 'contentOnly' : undefined;
useBlockEditingMode( mode );
return ;
diff --git a/packages/edit-site/src/components/page-content-focus-manager/index.js b/packages/edit-site/src/components/page-content-focus-manager/index.js
index 935ba1c96248cf..05bcc0da003cfc 100644
--- a/packages/edit-site/src/components/page-content-focus-manager/index.js
+++ b/packages/edit-site/src/components/page-content-focus-manager/index.js
@@ -1,8 +1,8 @@
/**
* WordPress dependencies
*/
-import { useSelect } from '@wordpress/data';
-
+import { useSelect, useDispatch } from '@wordpress/data';
+import { useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
@@ -10,12 +10,37 @@ import { store as editSiteStore } from '../../store';
import DisableNonPageContentBlocks from './disable-non-page-content-blocks';
import EditTemplateNotification from './edit-template-notification';
import BackToPageNotification from './back-to-page-notification';
+import { unlock } from '../../lock-unlock';
export default function PageContentFocusManager( { contentRef } ) {
- const hasPageContentFocus = useSelect(
- ( select ) => select( editSiteStore ).hasPageContentFocus(),
+ const { hasPageContentFocus, pageContentFocusType, canvasMode } = useSelect(
+ ( select ) => {
+ const { getPageContentFocusType, getCanvasMode } = unlock(
+ select( editSiteStore )
+ );
+ const _canvasMode = getCanvasMode();
+ return {
+ canvasMode: _canvasMode,
+ pageContentFocusType: getPageContentFocusType(),
+ hasPageContentFocus:
+ select( editSiteStore ).hasPageContentFocus(),
+ };
+ },
[]
);
+ const { setPageContentFocusType } = unlock( useDispatch( editSiteStore ) );
+
+ /*
+ * Ensure that the page content focus type is set to `disableTemplate` when
+ * the canvas mode is not `edit`. This makes the experience consistent with
+ * refreshing the page, which resets the page content focus type.
+ */
+ useEffect( () => {
+ if ( canvasMode !== 'edit' && !! pageContentFocusType ) {
+ setPageContentFocusType( null );
+ }
+ }, [ canvasMode, pageContentFocusType ] );
+
return (
<>
{ hasPageContentFocus && }
diff --git a/packages/edit-site/src/components/page-main/index.js b/packages/edit-site/src/components/page-main/index.js
index af017a8db9700a..10b5b99dc2fbf5 100644
--- a/packages/edit-site/src/components/page-main/index.js
+++ b/packages/edit-site/src/components/page-main/index.js
@@ -9,6 +9,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router';
import PagePatterns from '../page-patterns';
import PageTemplateParts from '../page-template-parts';
import PageTemplates from '../page-templates';
+import PagePages from '../page-pages';
import { unlock } from '../../lock-unlock';
const { useLocation } = unlock( routerPrivateApis );
@@ -24,6 +25,8 @@ export default function PageMain() {
return ;
} else if ( path === '/patterns' ) {
return ;
+ } else if ( window?.__experimentalAdminViews && path === '/pages' ) {
+ return ;
}
return null;
diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js
new file mode 100644
index 00000000000000..e7b8f913efe7ca
--- /dev/null
+++ b/packages/edit-site/src/components/page-pages/index.js
@@ -0,0 +1,190 @@
+/**
+ * WordPress dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+import {
+ VisuallyHidden,
+ __experimentalHeading as Heading,
+ __experimentalVStack as VStack,
+} from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { useEntityRecords } from '@wordpress/core-data';
+import { decodeEntities } from '@wordpress/html-entities';
+import { useState, useEffect, useMemo } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import Page from '../page';
+import Link from '../routes/link';
+import PageActions from '../page-actions';
+import { DataViews, PAGE_SIZE_VALUES } from '../dataviews';
+
+const EMPTY_ARRAY = [];
+
+export default function PagePages() {
+ const [ reset, setResetQuery ] = useState( ( v ) => ! v );
+ const [ globalFilter, setGlobalFilter ] = useState( '' );
+ const [ paginationInfo, setPaginationInfo ] = useState();
+ const [ { pageIndex, pageSize }, setPagination ] = useState( {
+ pageIndex: 0,
+ pageSize: PAGE_SIZE_VALUES[ 0 ],
+ } );
+ // Request post statuses to get the proper labels.
+ const [ postStatuses, setPostStatuses ] = useState( EMPTY_ARRAY );
+ useEffect( () => {
+ apiFetch( {
+ path: '/wp/v2/statuses',
+ } ).then( setPostStatuses );
+ }, [] );
+
+ // TODO: probably memo other objects passed as state(ex:https://tanstack.com/table/v8/docs/examples/react/pagination-controlled).
+ const pagination = useMemo(
+ () => ( { pageIndex, pageSize } ),
+ [ pageIndex, pageSize ]
+ );
+ const [ sorting, setSorting ] = useState( [
+ { order: 'desc', orderby: 'date' },
+ ] );
+ const queryArgs = useMemo(
+ () => ( {
+ per_page: pageSize,
+ page: pageIndex + 1, // tanstack starts from zero.
+ _embed: 'author',
+ order: sorting[ 0 ]?.desc ? 'desc' : 'asc',
+ orderby: sorting[ 0 ]?.id,
+ search: globalFilter,
+ status: [ 'publish', 'draft' ],
+ } ),
+ [
+ globalFilter,
+ sorting[ 0 ]?.id,
+ sorting[ 0 ]?.desc,
+ pageSize,
+ pageIndex,
+ reset,
+ ]
+ );
+ const { records, isResolving: isLoading } = useEntityRecords(
+ 'postType',
+ 'page',
+ queryArgs
+ );
+ useEffect( () => {
+ // Make extra request to handle controlled pagination.
+ apiFetch( {
+ path: addQueryArgs( '/wp/v2/pages', {
+ ...queryArgs,
+ _fields: 'id',
+ } ),
+ method: 'HEAD',
+ parse: false,
+ } ).then( ( res ) => {
+ const totalPages = parseInt( res.headers.get( 'X-WP-TotalPages' ) );
+ const totalItems = parseInt( res.headers.get( 'X-WP-Total' ) );
+ setPaginationInfo( {
+ totalPages,
+ totalItems,
+ } );
+ } );
+ // Status should not make extra request if already did..
+ }, [ globalFilter, pageSize, reset ] );
+
+ const fields = useMemo(
+ () => [
+ {
+ header: __( 'Title' ),
+ id: 'title',
+ accessorFn: ( page ) => page.title?.rendered || page.slug,
+ cell: ( props ) => {
+ const page = props.row.original;
+ return (
+
+
+
+ { decodeEntities( props.getValue() ) }
+
+
+
+ );
+ },
+ maxWidth: 400,
+ sortingFn: 'alphanumeric',
+ enableHiding: false,
+ },
+ {
+ header: __( 'Author' ),
+ id: 'author',
+ accessorFn: ( page ) => page._embedded?.author[ 0 ]?.name,
+ cell: ( props ) => {
+ const author = props.row.original._embedded?.author[ 0 ];
+ return (
+
+ { author.name }
+
+ );
+ },
+ },
+ {
+ header: 'Status',
+ id: 'status',
+ cell: ( props ) =>
+ postStatuses[ props.row.original.status ]?.name,
+ },
+ {
+ header: { __( 'Actions' ) } ,
+ id: 'actions',
+ cell: ( props ) => {
+ const page = props.row.original;
+ return (
+ setResetQuery() }
+ />
+ );
+ },
+ enableHiding: false,
+ },
+ ],
+ [ postStatuses ]
+ );
+
+ // TODO: we need to handle properly `data={ data || EMPTY_ARRAY }` for when `isLoading`.
+ return (
+
+ {
+ setGlobalFilter( value );
+ setPagination( { pageIndex: 0, pageSize } );
+ },
+ // TODO: check these callbacks and maybe reset the query when needed...
+ onPaginationChange: setPagination,
+ meta: { resetQuery: setResetQuery },
+ } }
+ />
+
+ );
+}
diff --git a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js
index c562ffbeeb6c83..a882de95bdaebe 100644
--- a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js
+++ b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js
@@ -70,7 +70,7 @@ export default function DuplicateMenuItem( {
const result = await saveEntityRecord(
'postType',
- 'wp_template_part',
+ TEMPLATE_PART_POST_TYPE,
{ slug, title, content, area },
{ throwOnError: true }
);
@@ -156,13 +156,13 @@ export default function DuplicateMenuItem( {
const title = sprintf(
/* translators: %s: Existing pattern title */
__( '%s (Copy)' ),
- item.title
+ item.title || item.name
);
- const categories = await getCategories( item.categories );
+ const categories = await getCategories( item.categories || [] );
const result = await saveEntityRecord(
'postType',
- 'wp_block',
+ PATTERN_TYPES.user,
{
content: isThemePattern
? item.content
@@ -179,7 +179,7 @@ export default function DuplicateMenuItem( {
sprintf(
// translators: %s: The new pattern's title e.g. 'Call to action (copy)'.
__( '"%s" duplicated.' ),
- item.title
+ item.title || item.name
),
{
type: 'snackbar',
diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js
index f1da1f925229d5..5c2802891b949a 100644
--- a/packages/edit-site/src/components/page-patterns/grid-item.js
+++ b/packages/edit-site/src/components/page-patterns/grid-item.js
@@ -75,7 +75,7 @@ function GridItem( { categoryId, item, ...props } ) {
postType: item.type,
postId: isUserPattern ? item.id : item.name,
categoryId,
- categoryType: item.type,
+ categoryType: isTemplatePart ? item.type : PATTERN_TYPES.theme,
} );
const isEmpty = ! item.blocks?.length;
@@ -113,14 +113,14 @@ function GridItem( { categoryId, item, ...props } ) {
const exportAsJSON = () => {
const json = {
__file: item.type,
- title: item.title,
+ title: item.title || item.name,
content: item.patternBlock.content.raw,
syncStatus: item.patternBlock.wp_pattern_sync_status,
};
return downloadjs(
JSON.stringify( json, null, 2 ),
- `${ kebabCase( item.title ) }.json`,
+ `${ kebabCase( item.title || item.name ) }.json`,
'application/json'
);
};
@@ -160,7 +160,7 @@ function GridItem( { categoryId, item, ...props } ) {
: sprintf(
// translators: %s: The pattern or template part's title e.g. 'Call to action'.
__( 'Are you sure you want to delete "%s"?' ),
- item.title
+ item.title || item.name
);
const additionalStyles = ! backgroundColor
@@ -246,7 +246,7 @@ function GridItem( { categoryId, item, ...props } ) {
// See https://github.com/WordPress/gutenberg/pull/51898#discussion_r1243399243.
tabIndex="-1"
>
- { item.title }
+ { item.title || item.name }
) }
diff --git a/packages/edit-site/src/components/page-patterns/grid.js b/packages/edit-site/src/components/page-patterns/grid.js
index 32fa6ebe55b39d..59a8cc431567f9 100644
--- a/packages/edit-site/src/components/page-patterns/grid.js
+++ b/packages/edit-site/src/components/page-patterns/grid.js
@@ -9,7 +9,7 @@ export default function Grid( { categoryId, items, ...props } ) {
}
return (
-
+
{ items.map( ( item ) => (
item.name || '';
@@ -141,6 +145,8 @@ function getItemSearchRank( item, searchTerm, config ) {
let rank =
categoryId === PATTERN_DEFAULT_CATEGORY ||
+ ( categoryId === PATTERN_USER_CATEGORY &&
+ item.type === PATTERN_TYPES.user ) ||
hasCategory( item, categoryId )
? 1
: 0;
diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js
index daaa9dd7822da6..6504b6f59684c6 100644
--- a/packages/edit-site/src/components/page-patterns/use-patterns.js
+++ b/packages/edit-site/src/components/page-patterns/use-patterns.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import createSelector from 'rememo';
+
/**
* WordPress dependencies
*/
@@ -16,6 +21,8 @@ import {
PATTERN_TYPES,
PATTERN_SYNC_TYPES,
TEMPLATE_PART_POST_TYPE,
+ TEMPLATE_ORIGINS,
+ TEMPLATE_PART_AREA_DEFAULT_CATEGORY,
} from '../../utils/constants';
import { unlock } from '../../lock-unlock';
import { searchItems } from './search-items';
@@ -32,7 +39,7 @@ const templatePartToPattern = ( templatePart ) => ( {
} ),
categories: [ templatePart.area ],
description: templatePart.description || '',
- isCustom: templatePart.source === 'custom',
+ isCustom: templatePart.source === TEMPLATE_ORIGINS.custom,
keywords: templatePart.keywords || [],
id: createTemplatePartId( templatePart.theme, templatePart.slug ),
name: createTemplatePartId( templatePart.theme, templatePart.slug ),
@@ -41,108 +48,140 @@ const templatePartToPattern = ( templatePart ) => ( {
templatePart,
} );
-const selectTemplatePartsAsPatterns = (
- select,
- { categoryId, search = '' } = {}
-) => {
- const { getEntityRecords, getIsResolving } = select( coreStore );
- const { __experimentalGetDefaultTemplatePartAreas } = select( editorStore );
- const query = { per_page: -1 };
- const rawTemplateParts =
- getEntityRecords( 'postType', TEMPLATE_PART_POST_TYPE, query ) ??
- EMPTY_PATTERN_LIST;
- const templateParts = rawTemplateParts.map( ( templatePart ) =>
- templatePartToPattern( templatePart )
- );
+const selectTemplatePartsAsPatterns = createSelector(
+ ( select, categoryId, search = '' ) => {
+ const { getEntityRecords, getIsResolving } = select( coreStore );
+ const { __experimentalGetDefaultTemplatePartAreas } =
+ select( editorStore );
+ const query = { per_page: -1 };
+ const rawTemplateParts =
+ getEntityRecords( 'postType', TEMPLATE_PART_POST_TYPE, query ) ??
+ EMPTY_PATTERN_LIST;
+ const templateParts = rawTemplateParts.map( ( templatePart ) =>
+ templatePartToPattern( templatePart )
+ );
- // In the case where a custom template part area has been removed we need
- // the current list of areas to cross check against so orphaned template
- // parts can be treated as uncategorized.
- const knownAreas = __experimentalGetDefaultTemplatePartAreas() || [];
- const templatePartAreas = knownAreas.map( ( area ) => area.area );
+ // In the case where a custom template part area has been removed we need
+ // the current list of areas to cross check against so orphaned template
+ // parts can be treated as uncategorized.
+ const knownAreas = __experimentalGetDefaultTemplatePartAreas() || [];
+ const templatePartAreas = knownAreas.map( ( area ) => area.area );
- const templatePartHasCategory = ( item, category ) => {
- if ( category !== 'uncategorized' ) {
- return item.templatePart.area === category;
- }
+ const templatePartHasCategory = ( item, category ) => {
+ if ( category !== TEMPLATE_PART_AREA_DEFAULT_CATEGORY ) {
+ return item.templatePart.area === category;
+ }
- return (
- item.templatePart.area === category ||
- ! templatePartAreas.includes( item.templatePart.area )
- );
- };
+ return (
+ item.templatePart.area === category ||
+ ! templatePartAreas.includes( item.templatePart.area )
+ );
+ };
+
+ const isResolving = getIsResolving( 'getEntityRecords', [
+ 'postType',
+ TEMPLATE_PART_POST_TYPE,
+ query,
+ ] );
- const isResolving = getIsResolving( 'getEntityRecords', [
- 'postType',
- 'wp_template_part',
- query,
- ] );
+ const patterns = searchItems( templateParts, search, {
+ categoryId,
+ hasCategory: templatePartHasCategory,
+ } );
- const patterns = searchItems( templateParts, search, {
- categoryId,
- hasCategory: templatePartHasCategory,
- } );
+ return { patterns, isResolving };
+ },
+ ( select ) => [
+ select( coreStore ).getEntityRecords(
+ 'postType',
+ TEMPLATE_PART_POST_TYPE,
+ {
+ per_page: -1,
+ }
+ ),
+ select( coreStore ).getIsResolving( 'getEntityRecords', [
+ 'postType',
+ TEMPLATE_PART_POST_TYPE,
+ { per_page: -1 },
+ ] ),
+ select( editorStore ).__experimentalGetDefaultTemplatePartAreas(),
+ ]
+);
- return { patterns, isResolving };
-};
+const selectThemePatterns = createSelector(
+ ( select ) => {
+ const { getSettings } = unlock( select( editSiteStore ) );
+ const settings = getSettings();
+ const blockPatterns =
+ settings.__experimentalAdditionalBlockPatterns ??
+ settings.__experimentalBlockPatterns;
-const selectThemePatterns = ( select ) => {
- const { getSettings } = unlock( select( editSiteStore ) );
- const settings = getSettings();
- const blockPatterns =
- settings.__experimentalAdditionalBlockPatterns ??
- settings.__experimentalBlockPatterns;
+ const restBlockPatterns = select( coreStore ).getBlockPatterns();
- const restBlockPatterns = select( coreStore ).getBlockPatterns();
+ const patterns = [
+ ...( blockPatterns || [] ),
+ ...( restBlockPatterns || [] ),
+ ]
+ .filter(
+ ( pattern ) => ! PATTERN_CORE_SOURCES.includes( pattern.source )
+ )
+ .filter( filterOutDuplicatesByName )
+ .filter( ( pattern ) => pattern.inserter !== false )
+ .map( ( pattern ) => ( {
+ ...pattern,
+ keywords: pattern.keywords || [],
+ type: PATTERN_TYPES.theme,
+ blocks: parse( pattern.content, {
+ __unstableSkipMigrationLogs: true,
+ } ),
+ } ) );
- const patterns = [
- ...( blockPatterns || [] ),
- ...( restBlockPatterns || [] ),
+ return { patterns, isResolving: false };
+ },
+ ( select ) => [
+ select( coreStore ).getBlockPatterns(),
+ unlock( select( editSiteStore ) ).getSettings(),
]
- .filter(
- ( pattern ) => ! PATTERN_CORE_SOURCES.includes( pattern.source )
- )
- .filter( filterOutDuplicatesByName )
- .filter( ( pattern ) => pattern.inserter !== false )
- .map( ( pattern ) => ( {
- ...pattern,
- keywords: pattern.keywords || [],
- type: PATTERN_TYPES.theme,
- blocks: parse( pattern.content, {
- __unstableSkipMigrationLogs: true,
- } ),
- } ) );
-
- return { patterns, isResolving: false };
-};
-const selectPatterns = (
- select,
- { categoryId, search = '', syncStatus } = {}
-) => {
- const { patterns: themePatterns } = selectThemePatterns( select );
- const { patterns: userPatterns } = selectUserPatterns( select );
+);
- let patterns = [ ...( themePatterns || [] ), ...( userPatterns || [] ) ];
+const selectPatterns = createSelector(
+ ( select, categoryId, syncStatus, search = '' ) => {
+ const { patterns: themePatterns } = selectThemePatterns( select );
+ const { patterns: userPatterns } = selectUserPatterns( select );
- if ( syncStatus ) {
- patterns = patterns.filter(
- ( pattern ) => pattern.syncStatus === syncStatus
- );
- }
+ let patterns = [
+ ...( themePatterns || [] ),
+ ...( userPatterns || [] ),
+ ];
- if ( categoryId ) {
- patterns = searchItems( patterns, search, {
- categoryId,
- hasCategory: ( item, currentCategory ) =>
- item.categories?.includes( currentCategory ),
- } );
- } else {
- patterns = searchItems( patterns, search, {
- hasCategory: ( item ) => ! item.hasOwnProperty( 'categories' ),
- } );
- }
- return { patterns, isResolving: false };
-};
+ if ( syncStatus ) {
+ // User patterns can have their sync statuses checked directly
+ // Non-user patterns are all unsynced for the time being.
+ patterns = patterns.filter( ( pattern ) => {
+ return pattern.id
+ ? pattern.syncStatus === syncStatus
+ : syncStatus === PATTERN_SYNC_TYPES.unsynced;
+ } );
+ }
+
+ if ( categoryId ) {
+ patterns = searchItems( patterns, search, {
+ categoryId,
+ hasCategory: ( item, currentCategory ) =>
+ item.categories?.includes( currentCategory ),
+ } );
+ } else {
+ patterns = searchItems( patterns, search, {
+ hasCategory: ( item ) => ! item.hasOwnProperty( 'categories' ),
+ } );
+ }
+ return { patterns, isResolving: false };
+ },
+ ( select ) => [
+ selectThemePatterns( select ),
+ selectUserPatterns( select ),
+ ]
+);
const patternBlockToPattern = ( patternBlock, categories ) => ( {
blocks: parse( patternBlock.content.raw, {
@@ -164,44 +203,65 @@ const patternBlockToPattern = ( patternBlock, categories ) => ( {
patternBlock,
} );
-const selectUserPatterns = ( select, { search = '', syncStatus } = {} ) => {
- const { getEntityRecords, getIsResolving, getUserPatternCategories } =
- select( coreStore );
+const selectUserPatterns = createSelector(
+ ( select, syncStatus, search = '' ) => {
+ const { getEntityRecords, getIsResolving, getUserPatternCategories } =
+ select( coreStore );
- const query = { per_page: -1 };
- const records = getEntityRecords( 'postType', PATTERN_TYPES.user, query );
- const userPatternCategories = getUserPatternCategories();
- const categories = new Map();
- userPatternCategories.forEach( ( userCategory ) =>
- categories.set( userCategory.id, userCategory )
- );
- let patterns = records
- ? records.map( ( record ) =>
- patternBlockToPattern( record, categories )
- )
- : EMPTY_PATTERN_LIST;
-
- const isResolving = getIsResolving( 'getEntityRecords', [
- 'postType',
- PATTERN_TYPES.user,
- query,
- ] );
-
- if ( syncStatus ) {
- patterns = patterns.filter(
- ( pattern ) => pattern.syncStatus === syncStatus
+ const query = { per_page: -1 };
+ const records = getEntityRecords(
+ 'postType',
+ PATTERN_TYPES.user,
+ query
);
- }
+ const userPatternCategories = getUserPatternCategories();
+ const categories = new Map();
+ userPatternCategories.forEach( ( userCategory ) =>
+ categories.set( userCategory.id, userCategory )
+ );
+ let patterns = records
+ ? records.map( ( record ) =>
+ patternBlockToPattern( record, categories )
+ )
+ : EMPTY_PATTERN_LIST;
- patterns = searchItems( patterns, search, {
- // We exit user pattern retrieval early if we aren't in the
- // catch-all category for user created patterns, so it has
- // to be in the category.
- hasCategory: () => true,
- } );
+ const isResolving = getIsResolving( 'getEntityRecords', [
+ 'postType',
+ PATTERN_TYPES.user,
+ query,
+ ] );
- return { patterns, isResolving, categories: userPatternCategories };
-};
+ if ( syncStatus ) {
+ patterns = patterns.filter(
+ ( pattern ) => pattern.syncStatus === syncStatus
+ );
+ }
+
+ patterns = searchItems( patterns, search, {
+ // We exit user pattern retrieval early if we aren't in the
+ // catch-all category for user created patterns, so it has
+ // to be in the category.
+ hasCategory: () => true,
+ } );
+
+ return {
+ patterns,
+ isResolving,
+ categories: userPatternCategories,
+ };
+ },
+ ( select ) => [
+ select( coreStore ).getEntityRecords( 'postType', PATTERN_TYPES.user, {
+ per_page: -1,
+ } ),
+ select( coreStore ).getIsResolving( 'getEntityRecords', [
+ 'postType',
+ PATTERN_TYPES.user,
+ { per_page: -1 },
+ ] ),
+ select( coreStore ).getUserPatternCategories(),
+ ]
+);
export const usePatterns = (
categoryType,
@@ -211,20 +271,20 @@ export const usePatterns = (
return useSelect(
( select ) => {
if ( categoryType === TEMPLATE_PART_POST_TYPE ) {
- return selectTemplatePartsAsPatterns( select, {
+ return selectTemplatePartsAsPatterns(
+ select,
categoryId,
- search,
- } );
+ search
+ );
} else if ( categoryType === PATTERN_TYPES.theme ) {
- return selectPatterns( select, {
- categoryId,
- search,
- syncStatus,
- } );
+ return selectPatterns( select, categoryId, syncStatus, search );
} else if ( categoryType === PATTERN_TYPES.user ) {
- return selectUserPatterns( select, { search, syncStatus } );
+ return selectUserPatterns( select, syncStatus, search );
}
- return { patterns: EMPTY_PATTERN_LIST, isResolving: false };
+ return {
+ patterns: EMPTY_PATTERN_LIST,
+ isResolving: false,
+ };
},
[ categoryId, categoryType, search, syncStatus ]
);
diff --git a/packages/edit-site/src/components/page-template-parts/add-new-template-part.js b/packages/edit-site/src/components/page-template-parts/add-new-template-part.js
index d2b52a88701fda..8d46010587242a 100644
--- a/packages/edit-site/src/components/page-template-parts/add-new-template-part.js
+++ b/packages/edit-site/src/components/page-template-parts/add-new-template-part.js
@@ -13,6 +13,7 @@ import { Button } from '@wordpress/components';
import { unlock } from '../../lock-unlock';
import { store as editSiteStore } from '../../store';
import CreateTemplatePartModal from '../create-template-part-modal';
+import { TEMPLATE_PART_POST_TYPE } from '../../utils/constants';
const { useHistory } = unlock( routerPrivateApis );
@@ -22,7 +23,9 @@ export default function AddNewTemplatePart() {
select( editSiteStore ).getSettings();
return {
canCreate: ! supportsTemplatePartsMode,
- postType: select( coreStore ).getPostType( 'wp_template_part' ),
+ postType: select( coreStore ).getPostType(
+ TEMPLATE_PART_POST_TYPE
+ ),
};
}, [] );
const [ isModalOpen, setIsModalOpen ] = useState( false );
@@ -45,7 +48,7 @@ export default function AddNewTemplatePart() {
setIsModalOpen( false );
history.push( {
postId: templatePart.id,
- postType: 'wp_template_part',
+ postType: TEMPLATE_PART_POST_TYPE,
canvas: 'edit',
} );
} }
diff --git a/packages/edit-site/src/components/page-template-parts/index.js b/packages/edit-site/src/components/page-template-parts/index.js
index e03d726a575254..2a8c41e333ce27 100644
--- a/packages/edit-site/src/components/page-template-parts/index.js
+++ b/packages/edit-site/src/components/page-template-parts/index.js
@@ -9,6 +9,7 @@ import {
import { __ } from '@wordpress/i18n';
import { useEntityRecords } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
+import { privateApis as routerPrivateApis } from '@wordpress/router';
/**
* Internal dependencies
@@ -19,11 +20,19 @@ import Link from '../routes/link';
import AddedBy from '../list/added-by';
import TemplateActions from '../template-actions';
import AddNewTemplatePart from './add-new-template-part';
+import { TEMPLATE_PART_POST_TYPE } from '../../utils/constants';
+import { unlock } from '../../lock-unlock';
+
+const { useLocation } = unlock( routerPrivateApis );
export default function PageTemplateParts() {
+ const {
+ params: { didAccessPatternsPage },
+ } = useLocation();
+
const { records: templateParts } = useEntityRecords(
'postType',
- 'wp_template_part',
+ TEMPLATE_PART_POST_TYPE,
{
per_page: -1,
}
@@ -39,8 +48,13 @@ export default function PageTemplateParts() {
params={ {
postId: templatePart.id,
postType: templatePart.type,
+ didAccessPatternsPage: !! didAccessPatternsPage
+ ? 1
+ : undefined,
+ } }
+ state={ {
+ backPath: '/wp_template_part/all',
} }
- state={ { backPath: '/wp_template_part/all' } }
>
{ decodeEntities(
templatePart.title?.rendered ||
diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js
index 5b0a306fa7fef3..55c666970b5cc2 100644
--- a/packages/edit-site/src/components/page-templates/index.js
+++ b/packages/edit-site/src/components/page-templates/index.js
@@ -20,11 +20,12 @@ import Link from '../routes/link';
import AddedBy from '../list/added-by';
import TemplateActions from '../template-actions';
import AddNewTemplate from '../add-new-template';
+import { TEMPLATE_POST_TYPE } from '../../utils/constants';
export default function PageTemplates() {
const { records: templates } = useEntityRecords(
'postType',
- 'wp_template',
+ TEMPLATE_POST_TYPE,
{
per_page: -1,
}
@@ -79,7 +80,7 @@ export default function PageTemplates() {
title={ __( 'Templates' ) }
actions={
diff --git a/packages/edit-site/src/components/plugin-template-setting-panel/index.js b/packages/edit-site/src/components/plugin-template-setting-panel/index.js
index 6a059dc2520ad2..0c279c91039a79 100644
--- a/packages/edit-site/src/components/plugin-template-setting-panel/index.js
+++ b/packages/edit-site/src/components/plugin-template-setting-panel/index.js
@@ -28,6 +28,6 @@ PluginTemplateSettingPanel.Slot = Slot;
* );
* ```
*
- * @return {WPComponent} The component to be rendered.
+ * @return {Component} The component to be rendered.
*/
export default PluginTemplateSettingPanel;
diff --git a/packages/edit-site/src/components/save-hub/index.js b/packages/edit-site/src/components/save-hub/index.js
index 9154429f770b0b..f43e843ee73c14 100644
--- a/packages/edit-site/src/components/save-hub/index.js
+++ b/packages/edit-site/src/components/save-hub/index.js
@@ -16,13 +16,14 @@ import { store as noticesStore } from '@wordpress/notices';
import SaveButton from '../save-button';
import { isPreviewingTheme } from '../../utils/is-previewing-theme';
import { unlock } from '../../lock-unlock';
+import { NAVIGATION_POST_TYPE } from '../../utils/constants';
const { useLocation } = unlock( routerPrivateApis );
const PUBLISH_ON_SAVE_ENTITIES = [
{
kind: 'postType',
- name: 'wp_navigation',
+ name: NAVIGATION_POST_TYPE,
},
];
diff --git a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js
index 0e46e99decd2f5..f86b7b8c9784d0 100644
--- a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js
+++ b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js
@@ -3,13 +3,9 @@
*/
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
-import {
- useFocusOnMount,
- useFocusReturn,
- useMergeRefs,
-} from '@wordpress/compose';
+import { useFocusOnMount, useMergeRefs } from '@wordpress/compose';
import { useDispatch } from '@wordpress/data';
-import { useRef, useState } from '@wordpress/element';
+import { useCallback, useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { closeSmall } from '@wordpress/icons';
import { ESCAPE } from '@wordpress/keycodes';
@@ -24,20 +20,27 @@ import { unlock } from '../../lock-unlock';
const { PrivateListView } = unlock( blockEditorPrivateApis );
-export default function ListViewSidebar() {
+export default function ListViewSidebar( { listViewToggleElement } ) {
const { setIsListViewOpened } = useDispatch( editSiteStore );
// This hook handles focus when the sidebar first renders.
const focusOnMountRef = useFocusOnMount( 'firstElement' );
- // The next 2 hooks handle focus for when the sidebar closes and returning focus to the element that had focus before sidebar opened.
- const headerFocusReturnRef = useFocusReturn();
- const contentFocusReturnRef = useFocusReturn();
- function closeOnEscape( event ) {
- if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) {
- setIsListViewOpened( false );
- }
- }
+ // When closing the list view, focus should return to the toggle button.
+ const closeListView = useCallback( () => {
+ setIsListViewOpened( false );
+ listViewToggleElement?.focus();
+ }, [ listViewToggleElement, setIsListViewOpened ] );
+
+ const closeOnEscape = useCallback(
+ ( event ) => {
+ if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) {
+ event.preventDefault();
+ closeListView();
+ }
+ },
+ [ closeListView ]
+ );
// Use internal state instead of a ref to make sure that the component
// re-renders when the dropZoneElement updates.
@@ -68,20 +71,26 @@ export default function ListViewSidebar() {
listViewFocusArea.focus();
}
- // This only fires when the sidebar is open because of the conditional rendering. It is the same shortcut to open but that is defined as a global shortcut and only fires when the sidebar is closed.
- useShortcut( 'core/edit-site/toggle-list-view', () => {
+ const handleToggleListViewShortcut = useCallback( () => {
// If the sidebar has focus, it is safe to close.
if (
sidebarRef.current.contains(
sidebarRef.current.ownerDocument.activeElement
)
) {
- setIsListViewOpened( false );
- // If the list view or close button does not have focus, focus should be moved to it.
+ closeListView();
} else {
+ // If the list view or close button does not have focus, focus should be moved to it.
handleSidebarFocus();
}
- } );
+ }, [ closeListView ] );
+
+ // This only fires when the sidebar is open because of the conditional rendering.
+ // It is the same shortcut to open but that is defined as a global shortcut and only fires when the sidebar is closed.
+ useShortcut(
+ 'core/edit-site/toggle-list-view',
+ handleToggleListViewShortcut
+ );
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
@@ -90,22 +99,18 @@ export default function ListViewSidebar() {
onKeyDown={ closeOnEscape }
ref={ sidebarRef }
>
-
+
{ __( 'List View' ) }
setIsListViewOpened( false ) }
+ onClick={ closeListView }
ref={ sidebarCloseButtonRef }
/>
{
- const { getEditedPostContext, getEditedPostType, getEditedPostId } =
- select( editSiteStore );
- const { getEditedEntityRecord, hasFinishedResolution } =
- select( coreStore );
- const _context = getEditedPostContext();
- const queryArgs = [
- 'postType',
- getEditedPostType(),
- getEditedPostId(),
- ];
- return {
- context: _context,
- hasResolved: hasFinishedResolution(
- 'getEditedEntityRecord',
- queryArgs
- ),
- template: getEditedEntityRecord( ...queryArgs ),
- };
- }, [] );
+ const { hasResolved, template, isTemplateHidden, postType } = useSelect(
+ ( select ) => {
+ const { getEditedPostContext, getEditedPostType, getEditedPostId } =
+ select( editSiteStore );
+ const { getCanvasMode, getPageContentFocusType } = unlock(
+ select( editSiteStore )
+ );
+ const { getEditedEntityRecord, hasFinishedResolution } =
+ select( coreStore );
+ const _context = getEditedPostContext();
+ const _postType = getEditedPostType();
+ const queryArgs = [
+ 'postType',
+ getEditedPostType(),
+ getEditedPostId(),
+ ];
+
+ return {
+ context: _context,
+ hasResolved: hasFinishedResolution(
+ 'getEditedEntityRecord',
+ queryArgs
+ ),
+ template: getEditedEntityRecord( ...queryArgs ),
+ isTemplateHidden:
+ getCanvasMode() === 'edit' &&
+ getPageContentFocusType() === 'hideTemplate',
+ postType: _postType,
+ };
+ },
+ []
+ );
+
+ const [ blocks ] = useEntityBlockEditor( 'postType', postType );
const { setHasPageContentFocus } = useDispatch( editSiteStore );
+ // Disable reason: `useDispatch` can't be called conditionally.
+ // eslint-disable-next-line @wordpress/no-unused-vars-before-return
+ const { setPageContentFocusType } = unlock( useDispatch( editSiteStore ) );
+ // Check if there are any post content block types in the blocks tree.
+ const pageContentBlocks = usePageContentBlocks( {
+ blocks,
+ isPageContentFocused: true,
+ } );
if ( ! hasResolved ) {
return null;
@@ -83,6 +108,24 @@ export default function EditTemplate() {
+ { !! pageContentBlocks?.length && (
+
+ {
+ setPageContentFocusType(
+ isTemplateHidden
+ ? 'disableTemplate'
+ : 'hideTemplate'
+ );
+ } }
+ >
+ { __( 'Template preview' ) }
+
+
+ ) }
>
) }
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/hooks.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/hooks.js
index 3000d21ab13662..92fad17cd1d3e4 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/hooks.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/hooks.js
@@ -9,6 +9,7 @@ import { store as coreStore } from '@wordpress/core-data';
* Internal dependencies
*/
import { store as editSiteStore } from '../../../store';
+import { TEMPLATE_POST_TYPE } from '../../../utils/constants';
export function useEditedPostContext() {
return useSelect(
@@ -31,10 +32,14 @@ export function useIsPostsPage() {
function useTemplates() {
return useSelect(
( select ) =>
- select( coreStore ).getEntityRecords( 'postType', 'wp_template', {
- per_page: -1,
- post_type: 'page',
- } ),
+ select( coreStore ).getEntityRecords(
+ 'postType',
+ TEMPLATE_POST_TYPE,
+ {
+ per_page: -1,
+ post_type: 'page',
+ }
+ ),
[]
);
}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js
index bc61b82a8d0057..fab050c62d2194 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js
@@ -37,7 +37,7 @@ export default function ResetDefaultTemplate( { onClick } ) {
} );
} }
>
- { __( 'Reset' ) }
+ { __( 'Use default template' ) }
);
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss
index aedcf5e46ca9ea..5501fe49e5876b 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss
+++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss
@@ -79,3 +79,9 @@
width: 30%;
}
}
+
+.edit-site-page-panels-edit-template__dropdown {
+ .components-popover__content {
+ min-width: 240px;
+ }
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js
index 7bc951524e7e5c..569bad72ad7ef9 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js
@@ -17,12 +17,7 @@ import { store as interfaceStore } from '@wordpress/interface';
import { STORE_NAME } from '../../../store/constants';
import { SIDEBAR_BLOCK, SIDEBAR_TEMPLATE } from '../constants';
import { store as editSiteStore } from '../../../store';
-
-const entityLabels = {
- wp_navigation: __( 'Navigation' ),
- wp_block: __( 'Pattern' ),
- wp_template: __( 'Template' ),
-};
+import { POST_TYPE_LABELS, TEMPLATE_POST_TYPE } from '../../../utils/constants';
const SettingsHeader = ( { sidebarName } ) => {
const { hasPageContentFocus, entityType } = useSelect( ( select ) => {
@@ -35,7 +30,9 @@ const SettingsHeader = ( { sidebarName } ) => {
};
} );
- const entityLabel = entityLabels[ entityType ] || entityLabels.wp_template;
+ const entityLabel =
+ POST_TYPE_LABELS[ entityType ] ||
+ POST_TYPE_LABELS[ TEMPLATE_POST_TYPE ];
const { enableComplementaryArea } = useDispatch( interfaceStore );
const openTemplateSettings = () =>
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js
new file mode 100644
index 00000000000000..b5e5988491396a
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js
@@ -0,0 +1,97 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import { store as coreStore } from '@wordpress/core-data';
+import { parse } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { store as editSiteStore } from '../../../store';
+import { PATTERN_CORE_SOURCES, PATTERN_TYPES } from '../../../utils/constants';
+import { unlock } from '../../../lock-unlock';
+
+function injectThemeAttributeInBlockTemplateContent(
+ block,
+ currentThemeStylesheet
+) {
+ block.innerBlocks = block.innerBlocks.map( ( innerBlock ) => {
+ return injectThemeAttributeInBlockTemplateContent(
+ innerBlock,
+ currentThemeStylesheet
+ );
+ } );
+
+ if (
+ block.name === 'core/template-part' &&
+ block.attributes.theme === undefined
+ ) {
+ block.attributes.theme = currentThemeStylesheet;
+ }
+ return block;
+}
+
+function preparePatterns( patterns, template, currentThemeStylesheet ) {
+ // Filter out duplicates.
+ const filterOutDuplicatesByName = ( currentItem, index, items ) =>
+ index === items.findIndex( ( item ) => currentItem.name === item.name );
+
+ // Filter out core patterns.
+ const filterOutCorePatterns = ( pattern ) =>
+ ! PATTERN_CORE_SOURCES.includes( pattern.source );
+
+ // Filter only the patterns that are compatible with the current template.
+ const filterCompatiblePatterns = ( pattern ) =>
+ pattern.templateTypes?.includes( template.slug );
+
+ return patterns
+ .filter(
+ filterOutCorePatterns &&
+ filterOutDuplicatesByName &&
+ filterCompatiblePatterns
+ )
+ .map( ( pattern ) => ( {
+ ...pattern,
+ keywords: pattern.keywords || [],
+ type: PATTERN_TYPES.theme,
+ blocks: parse( pattern.content, {
+ __unstableSkipMigrationLogs: true,
+ } ).map( ( block ) =>
+ injectThemeAttributeInBlockTemplateContent(
+ block,
+ currentThemeStylesheet
+ )
+ ),
+ } ) );
+}
+
+export function useAvailablePatterns( template ) {
+ const { blockPatterns, restBlockPatterns, currentThemeStylesheet } =
+ useSelect( ( select ) => {
+ const { getSettings } = unlock( select( editSiteStore ) );
+ const settings = getSettings();
+
+ return {
+ blockPatterns:
+ settings.__experimentalAdditionalBlockPatterns ??
+ settings.__experimentalBlockPatterns,
+ restBlockPatterns: select( coreStore ).getBlockPatterns(),
+ currentThemeStylesheet:
+ select( coreStore ).getCurrentTheme().stylesheet,
+ };
+ }, [] );
+
+ return useMemo( () => {
+ const mergedPatterns = [
+ ...( blockPatterns || [] ),
+ ...( restBlockPatterns || [] ),
+ ];
+ return preparePatterns(
+ mergedPatterns,
+ template,
+ currentThemeStylesheet
+ );
+ }, [ blockPatterns, restBlockPatterns, template, currentThemeStylesheet ] );
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/pattern-categories.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/pattern-categories.js
index 388094576789d6..3740b622361ff5 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/pattern-categories.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/pattern-categories.js
@@ -3,13 +3,18 @@
*/
import { __, _x, sprintf } from '@wordpress/i18n';
import { useEffect, useMemo, useState } from '@wordpress/element';
-import { FormTokenField, PanelRow } from '@wordpress/components';
+import { FormTokenField, FlexBlock, PanelRow } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { useDebounce } from '@wordpress/compose';
import { store as noticesStore } from '@wordpress/notices';
import { decodeEntities } from '@wordpress/html-entities';
+/**
+ * Internal dependencies
+ */
+import { PATTERN_TYPES } from '../../../utils/constants';
+
export const unescapeString = ( arg ) => {
return decodeEntities( arg );
};
@@ -145,7 +150,8 @@ export default function PatternCategories( { post } ) {
);
}, [ searchResults ] );
- const { saveEntityRecord, editEntityRecord } = useDispatch( coreStore );
+ const { saveEntityRecord, editEntityRecord, invalidateResolution } =
+ useDispatch( coreStore );
const { createErrorNotice } = useDispatch( noticesStore );
if ( ! hasAssignAction ) {
@@ -157,6 +163,7 @@ export default function PatternCategories( { post } ) {
const newTerm = await saveEntityRecord( 'taxonomy', slug, term, {
throwOnError: true,
} );
+ invalidateResolution( 'getUserPatternCategories' );
return unescapeTerm( newTerm );
} catch ( error ) {
if ( error.code !== 'term_exists' ) {
@@ -171,7 +178,7 @@ export default function PatternCategories( { post } ) {
}
function onUpdateTerms( newTermIds ) {
- editEntityRecord( 'postType', 'wp_block', post.id, {
+ editEntityRecord( 'postType', PATTERN_TYPES.user, post.id, {
wp_pattern_category: newTermIds,
} );
}
@@ -250,21 +257,23 @@ export default function PatternCategories( { post } ) {
return (
-
+
+
+
);
}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/replace-template-button.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/replace-template-button.js
new file mode 100644
index 00000000000000..658aacd331debc
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/replace-template-button.js
@@ -0,0 +1,89 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect, useDispatch } from '@wordpress/data';
+import { useState } from '@wordpress/element';
+import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor';
+import { MenuItem, Modal } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { store as coreStore } from '@wordpress/core-data';
+import { useAsyncList } from '@wordpress/compose';
+import { serialize } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { store as editSiteStore } from '../../../store';
+
+export default function ReplaceTemplateButton( {
+ onClick,
+ availableTemplates,
+} ) {
+ const { editEntityRecord } = useDispatch( coreStore );
+ const [ showModal, setShowModal ] = useState( false );
+ const onClose = () => {
+ setShowModal( false );
+ };
+
+ const { postId, postType } = useSelect( ( select ) => {
+ return {
+ postId: select( editSiteStore ).getEditedPostId(),
+ postType: select( editSiteStore ).getEditedPostType(),
+ };
+ }, [] );
+
+ const onTemplateSelect = async ( selectedTemplate ) => {
+ onClose(); // Close the template suggestions modal first.
+ onClick();
+ await editEntityRecord( 'postType', postType, postId, {
+ blocks: selectedTemplate.blocks,
+ content: serialize( selectedTemplate.blocks ),
+ } );
+ };
+
+ if ( ! availableTemplates.length || availableTemplates.length < 1 ) {
+ return null;
+ }
+
+ return (
+ <>
+
setShowModal( true ) }
+ >
+ { __( 'Replace template' ) }
+
+
+ { showModal && (
+
+
+
+
+
+ ) }
+ >
+ );
+}
+
+function TemplatesList( { availableTemplates, onSelect } ) {
+ const shownTemplates = useAsyncList( availableTemplates );
+
+ return (
+
+ );
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
index 4c8ef94855dcb1..6eab753e8ad285 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
@@ -37,3 +37,21 @@ h3.edit-site-template-card__template-areas-title {
font-weight: 500;
margin: 0 0 $grid-unit-10;
}
+
+
+.edit-site-template-panel__replace-template-modal {
+ z-index: z-index(".edit-site-template-panel__replace-template-modal");
+}
+
+.edit-site-template-panel__replace-template-modal__content {
+ column-count: 2;
+ column-gap: $grid-unit-30;
+
+ @include break-medium() {
+ column-count: 3;
+ }
+
+ @include break-wide() {
+ column-count: 4;
+ }
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js
index b68cf1ff617579..81acb244a11863 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js
@@ -11,13 +11,21 @@ import { moreVertical } from '@wordpress/icons';
*/
import { store as editSiteStore } from '../../../store';
import isTemplateRevertable from '../../../utils/is-template-revertable';
+import ReplaceTemplateButton from './replace-template-button';
+import { useAvailablePatterns } from './hooks';
export default function Actions( { template } ) {
+ const availablePatterns = useAvailablePatterns( template );
const { revertTemplate } = useDispatch( editSiteStore );
const isRevertable = isTemplateRevertable( template );
- if ( ! isRevertable ) {
+
+ if (
+ ! isRevertable &&
+ ( ! availablePatterns.length || availablePatterns.length < 1 )
+ ) {
return null;
}
+
return (
{ ( { onClose } ) => (
- {
- revertTemplate( template );
- onClose();
- } }
- >
- { __( 'Clear customizations' ) }
-
+ { isRevertable && (
+ {
+ revertTemplate( template );
+ onClose();
+ } }
+ >
+ { __( 'Clear customizations' ) }
+
+ ) }
+
) }
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-areas.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-areas.js
index fd00d9d2c0e24d..24dcd301bf2399 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-areas.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-areas.js
@@ -73,7 +73,7 @@ export default function TemplateAreas() {
{ templateParts.map( ( { templatePart, block } ) => (
-
+
{
const linkInfo = useLink( {
postId,
- postType: 'wp_navigation',
+ postType: NAVIGATION_POST_TYPE,
} );
return ;
};
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js
index 47c435a54e0caa..9389842391a779 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js
@@ -21,6 +21,7 @@ import {
SidebarNavigationScreenDetailsPanelLabel,
SidebarNavigationScreenDetailsPanelValue,
} from '../sidebar-navigation-screen-details-panel';
+import { TEMPLATE_POST_TYPE } from '../../utils/constants';
// Taken from packages/editor/src/components/time-to-read/index.js.
const AVERAGE_READING_RATE = 189;
@@ -106,7 +107,7 @@ export default function PageDetails( { id } ) {
const postContext = getEditedPostContext();
const templates = select( coreStore ).getEntityRecords(
'postType',
- 'wp_template',
+ TEMPLATE_POST_TYPE,
{ per_page: -1 }
);
// Template title.
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 13e5f9c3cb3266..e9a6163a0047e9 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
@@ -24,6 +24,7 @@ import SidebarNavigationItem from '../sidebar-navigation-item';
import SidebarButton from '../sidebar-button';
import AddNewPageModal from '../add-new-page';
import { unlock } from '../../lock-unlock';
+import { TEMPLATE_POST_TYPE } from '../../utils/constants';
const { useHistory } = unlock( routerPrivateApis );
@@ -50,7 +51,7 @@ export default function SidebarNavigationScreenPages() {
}
);
const { records: templates, isResolving: isLoadingTemplates } =
- useEntityRecords( 'postType', 'wp_template', {
+ useEntityRecords( 'postType', TEMPLATE_POST_TYPE, {
per_page: -1,
} );
@@ -130,11 +131,21 @@ export default function SidebarNavigationScreenPages() {
return {
icon: itemIcon,
- postType: postsPageTemplateId ? 'wp_template' : 'page',
+ postType: postsPageTemplateId ? TEMPLATE_POST_TYPE : 'page',
postId: postsPageTemplateId || id,
};
};
+ const pagesLink = useLink( { path: '/pages' } );
+ const manageAllPagesProps = window?.__experimentalAdminViews
+ ? { ...pagesLink }
+ : {
+ href: 'edit.php?post_type=page',
+ onClick: () => {
+ document.location = 'edit.php?post_type=page';
+ },
+ };
+
return (
<>
{ showAddPage && (
@@ -167,7 +178,7 @@ export default function SidebarNavigationScreenPages() {
) }
{ isHomePageBlog && homeTemplate && (
{ dynamicPageTemplates?.map( ( item ) => (
{
- document.location = 'edit.php?post_type=page';
- } }
+ { ...manageAllPagesProps }
>
{ __( 'Manage all pages' ) }
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/index.js
index d9701e5358dcb5..693d16914869ad 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/index.js
@@ -17,6 +17,7 @@ import usePatternDetails from './use-pattern-details';
import { store as editSiteStore } from '../../store';
import { unlock } from '../../lock-unlock';
import TemplateActions from '../template-actions';
+import { TEMPLATE_PART_POST_TYPE } from '../../utils/constants';
export default function SidebarNavigationScreenPattern() {
const navigator = useNavigator();
@@ -34,7 +35,7 @@ export default function SidebarNavigationScreenPattern() {
// indicates the user has arrived at the template part via the "manage all"
// page and the back button should return them to that list page.
const backPath =
- ! categoryType && postType === 'wp_template_part'
+ ! categoryType && postType === TEMPLATE_PART_POST_TYPE
? '/wp_template_part/all'
: '/patterns';
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js
index b685c766107a32..8a493130ad27db 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu-list-item.js
@@ -9,16 +9,24 @@ import { __ } from '@wordpress/i18n';
*/
import SidebarNavigationItem from '../sidebar-navigation-item';
import { useLink } from '../routes/link';
+import { NAVIGATION_POST_TYPE } from '../../utils/constants';
export default function TemplatePartNavigationMenuListItem( { id } ) {
- const [ title ] = useEntityProp( 'postType', 'wp_navigation', 'title', id );
+ const [ title ] = useEntityProp(
+ 'postType',
+ NAVIGATION_POST_TYPE,
+ 'title',
+ id
+ );
const linkInfo = useLink( {
postId: id,
- postType: 'wp_navigation',
+ postType: NAVIGATION_POST_TYPE,
} );
- if ( ! id ) return null;
+ if ( ! id || title === undefined ) {
+ return null;
+ }
return (
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js
index a124c4163fc54c..40012ec46a85e4 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/template-part-navigation-menu.js
@@ -9,11 +9,19 @@ import { useEntityProp } from '@wordpress/core-data';
* Internal dependencies
*/
import NavigationMenuEditor from '../sidebar-navigation-screen-navigation-menu/navigation-menu-editor';
+import { NAVIGATION_POST_TYPE } from '../../utils/constants';
export default function TemplatePartNavigationMenu( { id } ) {
- const [ title ] = useEntityProp( 'postType', 'wp_navigation', 'title', id );
+ const [ title ] = useEntityProp(
+ 'postType',
+ NAVIGATION_POST_TYPE,
+ 'title',
+ id
+ );
- if ( ! id ) return null;
+ if ( ! id || title === undefined ) {
+ return null;
+ }
return (
<>
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-content.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-content.js
index 0a5de0a6274d4d..249124b1054cec 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-content.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-navigation-menu-content.js
@@ -8,6 +8,17 @@ import { parse } from '@wordpress/blocks';
*/
import TemplatePartNavigationMenus from './template-part-navigation-menus';
import useEditedEntityRecord from '../use-edited-entity-record';
+import { TEMPLATE_PART_POST_TYPE } from '../../utils/constants';
+
+function getBlocksFromRecord( record ) {
+ if ( record?.blocks ) {
+ return record?.blocks;
+ }
+
+ return record?.content && typeof record.content !== 'function'
+ ? parse( record.content )
+ : [];
+}
/**
* Retrieves a list of specific blocks from a given tree of blocks.
@@ -55,15 +66,11 @@ export default function useNavigationMenuContent( postType, postId ) {
// Only managing navigation menus in template parts is supported
// to match previous behaviour. This could potentially be expanded
// to patterns as well.
- if ( postType !== 'wp_template_part' ) {
+ if ( postType !== TEMPLATE_PART_POST_TYPE ) {
return;
}
- const blocks =
- record?.content && typeof record.content !== 'function'
- ? parse( record.content )
- : [];
-
+ const blocks = getBlocksFromRecord( record );
const navigationBlocks = getBlocksOfTypeFromBlocks(
'core/navigation',
blocks
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-pattern-details.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-pattern-details.js
index 674d95e02a13b1..4e1e9ec6a11aa8 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-pattern-details.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/use-pattern-details.js
@@ -24,6 +24,12 @@ import {
SidebarNavigationScreenDetailsPanelLabel,
SidebarNavigationScreenDetailsPanelValue,
} from '../sidebar-navigation-screen-details-panel';
+import {
+ PATTERN_TYPES,
+ TEMPLATE_PART_POST_TYPE,
+ PATTERN_SYNC_TYPES,
+ TEMPLATE_ORIGINS,
+} from '../../utils/constants';
export default function usePatternDetails( postType, postId ) {
const { getDescription, getTitle, record } = useEditedEntityRecord(
@@ -53,7 +59,7 @@ export default function usePatternDetails( postType, postId ) {
if ( ! description && addedBy.text ) {
description =
- postType === 'wp_block'
+ postType === PATTERN_TYPES.user
? sprintf(
// translators: %s: pattern title e.g: "Header".
__( 'This is the %s pattern.' ),
@@ -66,7 +72,7 @@ export default function usePatternDetails( postType, postId ) {
);
}
- if ( ! description && postType === 'wp_block' && record?.title ) {
+ if ( ! description && postType === PATTERN_TYPES.user && record?.title ) {
description = sprintf(
// translators: %s: user created pattern title e.g. "Footer".
__( 'This is the %s pattern.' ),
@@ -80,11 +86,14 @@ export default function usePatternDetails( postType, postId ) {
const details = [];
- if ( postType === 'wp_block' || 'wp_template_part' ) {
+ if (
+ postType === PATTERN_TYPES.user ||
+ postType === TEMPLATE_PART_POST_TYPE
+ ) {
details.push( {
label: __( 'Syncing' ),
value:
- record.wp_pattern_sync_status === 'unsynced'
+ record.wp_pattern_sync_status === PATTERN_SYNC_TYPES.unsynced
? __( 'Not synced' )
: __( 'Fully synced' ),
} );
@@ -112,7 +121,7 @@ export default function usePatternDetails( postType, postId ) {
}
}
- if ( postType === 'wp_template_part' ) {
+ if ( postType === TEMPLATE_PART_POST_TYPE ) {
const templatePartArea = templatePartAreas.find(
( area ) => area.area === record.area
);
@@ -133,7 +142,7 @@ export default function usePatternDetails( postType, postId ) {
}
if (
- postType === 'wp_template_part' &&
+ postType === TEMPLATE_PART_POST_TYPE &&
addedBy.text &&
! isAddedByActiveTheme
) {
@@ -148,9 +157,10 @@ export default function usePatternDetails( postType, postId ) {
}
if (
- postType === 'wp_template_part' &&
+ postType === TEMPLATE_PART_POST_TYPE &&
addedBy.text &&
- ( record.origin === 'plugin' || record.has_theme_file === true )
+ ( record.origin === TEMPLATE_ORIGINS.plugin ||
+ record.has_theme_file === true )
) {
details.push( {
label: __( 'Customized' ),
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js
index 0b7be99812dd67..70325f09bd21de 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js
@@ -10,6 +10,8 @@ import { useViewportMatch } from '@wordpress/compose';
import { getTemplatePartIcon } from '@wordpress/editor';
import { __ } from '@wordpress/i18n';
import { getQueryArgs } from '@wordpress/url';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
import { file } from '@wordpress/icons';
/**
@@ -19,10 +21,15 @@ import AddNewPattern from '../add-new-pattern';
import SidebarNavigationItem from '../sidebar-navigation-item';
import SidebarNavigationScreen from '../sidebar-navigation-screen';
import CategoryItem from './category-item';
-import { PATTERN_DEFAULT_CATEGORY, PATTERN_TYPES } from '../../utils/constants';
+import {
+ PATTERN_DEFAULT_CATEGORY,
+ PATTERN_TYPES,
+ TEMPLATE_PART_POST_TYPE,
+} from '../../utils/constants';
import { useLink } from '../routes/link';
import usePatternCategories from './use-pattern-categories';
import useTemplatePartAreas from './use-template-part-areas';
+import { store as editSiteStore } from '../../store';
function TemplatePartGroup( { areas, currentArea, currentType } ) {
return (
@@ -39,10 +46,10 @@ function TemplatePartGroup( { areas, currentArea, currentType } ) {
icon={ getTemplatePartIcon( area ) }
label={ label }
id={ area }
- type="wp_template_part"
+ type={ TEMPLATE_PART_POST_TYPE }
isActive={
currentArea === area &&
- currentType === 'wp_template_part'
+ currentType === TEMPLATE_PART_POST_TYPE
}
/>
)
@@ -84,13 +91,28 @@ export default function SidebarNavigationScreenPatterns() {
const isMobileViewport = useViewportMatch( 'medium', '<' );
const { categoryType, categoryId } = getQueryArgs( window.location.href );
const currentCategory = categoryId || PATTERN_DEFAULT_CATEGORY;
- const currentType = categoryType || PATTERN_TYPES.user;
+ const currentType = categoryType || PATTERN_TYPES.theme;
const { templatePartAreas, hasTemplateParts, isLoading } =
useTemplatePartAreas();
const { patternCategories, hasPatterns } = usePatternCategories();
+ const isBlockBasedTheme = useSelect(
+ ( select ) => select( coreStore ).getCurrentTheme()?.is_block_theme,
+ []
+ );
+ const isTemplatePartsMode = useSelect( ( select ) => {
+ const settings = select( editSiteStore ).getSettings();
+ return !! settings.supportsTemplatePartsMode;
+ }, [] );
+
+ const templatePartsLink = useLink( {
+ path: '/wp_template_part/all',
+ // If a classic theme that supports template parts accessed
+ // the Patterns page directly, preserve that state in the URL.
+ didAccessPatternsPage:
+ ! isBlockBasedTheme && isTemplatePartsMode ? 1 : undefined,
+ } );
- const templatePartsLink = useLink( { path: '/wp_template_part/all' } );
const footer = ! isMobileViewport ? (
{ __( 'Manage all of my patterns' ) }
-
- { __( 'Manage all template parts' ) }
-
+ { ( isBlockBasedTheme || isTemplatePartsMode ) && (
+
+ { __( 'Manage all template parts' ) }
+
+ ) }
) : undefined;
return (
a.label.localeCompare( b.label )
);
+
+ sortedCategories.unshift( {
+ name: PATTERN_USER_CATEGORY,
+ label: __( 'My patterns' ),
+ count: userPatterns.length,
+ } );
+
sortedCategories.unshift( {
name: PATTERN_DEFAULT_CATEGORY,
- label: __( 'All Patterns' ),
+ label: __( 'All patterns' ),
description: __( 'A list of all patterns from all sources' ),
count: themePatterns.length + userPatterns.length,
} );
+
return sortedCategories;
}, [
defaultCategories,
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js
index bc538c5e7a85fa..77cbf87b3d439e 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-template-part-areas.js
@@ -5,6 +5,14 @@ import { useEntityRecords } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
+/**
+ * Internal dependencies
+ */
+import {
+ TEMPLATE_PART_AREA_DEFAULT_CATEGORY,
+ TEMPLATE_PART_POST_TYPE,
+} from '../../utils/constants';
+
const useTemplatePartsGroupedByArea = ( items ) => {
const allItems = items || [];
@@ -32,7 +40,9 @@ const useTemplatePartsGroupedByArea = ( items ) => {
);
const groupedByArea = allItems.reduce( ( accumulator, item ) => {
- const key = accumulator[ item.area ] ? item.area : 'uncategorized';
+ const key = accumulator[ item.area ]
+ ? item.area
+ : TEMPLATE_PART_AREA_DEFAULT_CATEGORY;
accumulator[ key ].templateParts.push( item );
return accumulator;
}, knownAreas );
@@ -43,7 +53,7 @@ const useTemplatePartsGroupedByArea = ( items ) => {
export default function useTemplatePartAreas() {
const { records: templateParts, isResolving: isLoading } = useEntityRecords(
'postType',
- 'wp_template_part',
+ TEMPLATE_PART_POST_TYPE,
{ per_page: -1 }
);
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-template/home-template-details.js b/packages/edit-site/src/components/sidebar-navigation-screen-template/home-template-details.js
index 5c25f7768dee0f..4f1dbcd432df25 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-template/home-template-details.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-template/home-template-details.js
@@ -7,7 +7,6 @@ import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import {
CheckboxControl,
- __experimentalUseNavigator as useNavigator,
__experimentalInputControl as InputControl,
__experimentalNumberControl as NumberControl,
__experimentalTruncate as Truncate,
@@ -28,6 +27,7 @@ import { unlock } from '../../lock-unlock';
import { store as editSiteStore } from '../../store';
import { useLink } from '../routes/link';
import SidebarNavigationItem from '../sidebar-navigation-item';
+import { TEMPLATE_PART_POST_TYPE } from '../../utils/constants';
const EMPTY_OBJECT = {};
@@ -37,7 +37,7 @@ function TemplateAreaButton( { postId, icon, title } ) {
footer,
};
const linkInfo = useLink( {
- postType: 'wp_template_part',
+ postType: TEMPLATE_PART_POST_TYPE,
postId,
} );
@@ -61,10 +61,6 @@ function TemplateAreaButton( { postId, icon, title } ) {
}
export default function HomeTemplateDetails() {
- const navigator = useNavigator();
- const {
- params: { postType, postId },
- } = navigator;
const { editEntityRecord } = useDispatch( coreStore );
const {
@@ -74,34 +70,30 @@ export default function HomeTemplateDetails() {
postsPageTitle,
postsPageId,
currentTemplateParts,
- } = useSelect(
- ( select ) => {
- const { getEntityRecord } = select( coreStore );
- const siteSettings = getEntityRecord( 'root', 'site' );
- const { getSettings } = unlock( select( editSiteStore ) );
- const _currentTemplateParts =
- select( editSiteStore ).getCurrentTemplateTemplateParts();
- const siteEditorSettings = getSettings();
- const _postsPageRecord = siteSettings?.page_for_posts
- ? select( coreStore ).getEntityRecord(
- 'postType',
- 'page',
- siteSettings?.page_for_posts
- )
- : EMPTY_OBJECT;
+ } = useSelect( ( select ) => {
+ const { getEntityRecord } = select( coreStore );
+ const { getSettings, getCurrentTemplateTemplateParts } = unlock(
+ select( editSiteStore )
+ );
+ const siteSettings = getEntityRecord( 'root', 'site' );
+ const _postsPageRecord = siteSettings?.page_for_posts
+ ? getEntityRecord(
+ 'postType',
+ 'page',
+ siteSettings?.page_for_posts
+ )
+ : EMPTY_OBJECT;
- return {
- allowCommentsOnNewPosts:
- siteSettings?.default_comment_status === 'open',
- postsPageTitle: _postsPageRecord?.title?.rendered,
- postsPageId: _postsPageRecord?.id,
- postsPerPage: siteSettings?.posts_per_page,
- templatePartAreas: siteEditorSettings?.defaultTemplatePartAreas,
- currentTemplateParts: _currentTemplateParts,
- };
- },
- [ postType, postId ]
- );
+ return {
+ allowCommentsOnNewPosts:
+ siteSettings?.default_comment_status === 'open',
+ postsPageTitle: _postsPageRecord?.title?.rendered,
+ postsPageId: _postsPageRecord?.id,
+ postsPerPage: siteSettings?.posts_per_page,
+ templatePartAreas: getSettings()?.defaultTemplatePartAreas,
+ currentTemplateParts: getCurrentTemplateTemplateParts(),
+ };
+ }, [] );
const [ commentsOnNewPostsValue, setCommentsOnNewPostsValue ] =
useState( '' );
@@ -125,11 +117,12 @@ export default function HomeTemplateDetails() {
*/
const templateAreas = useMemo( () => {
return currentTemplateParts.length && templatePartAreas
- ? currentTemplateParts.map( ( { templatePart } ) => ( {
+ ? currentTemplateParts.map( ( { templatePart, block } ) => ( {
...templatePartAreas?.find(
( { area } ) => area === templatePart?.area
),
...templatePart,
+ clientId: block.clientId,
} ) )
: [];
}, [ currentTemplateParts, templatePartAreas ] );
@@ -213,9 +206,9 @@ export default function HomeTemplateDetails() {
>
{ templateAreas.map(
- ( { label, icon, theme, slug, title } ) => (
+ ( { clientId, label, icon, theme, slug, title } ) => (
{
- const settings = select( editSiteStore ).getSettings();
-
- return !! settings.supportsTemplatePartsMode;
+ return !! select( editSiteStore ).getSettings()
+ .supportsTemplatePartsMode;
}, [] );
return (
{
const linkInfo = useLink( {
@@ -32,7 +33,7 @@ export default function SidebarNavigationScreenTemplates() {
const { records: templates, isResolving: isLoading } = useEntityRecords(
'postType',
- 'wp_template',
+ TEMPLATE_POST_TYPE,
{
per_page: -1,
}
@@ -54,7 +55,7 @@ export default function SidebarNavigationScreenTemplates() {
actions={
canCreate && (
(
+ { window?.__experimentalAdminViews && (
+
+
+
+ ) }
diff --git a/packages/edit-site/src/components/start-template-options/index.js b/packages/edit-site/src/components/start-template-options/index.js
index 6cce84deebfa97..fe4179a338504b 100644
--- a/packages/edit-site/src/components/start-template-options/index.js
+++ b/packages/edit-site/src/components/start-template-options/index.js
@@ -12,14 +12,15 @@ import { useSelect } from '@wordpress/data';
import { useAsyncList } from '@wordpress/compose';
import { store as preferencesStore } from '@wordpress/preferences';
import { parse } from '@wordpress/blocks';
+import { store as coreStore, useEntityBlockEditor } from '@wordpress/core-data';
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
import { store as editSiteStore } from '../../store';
-import { store as coreStore, useEntityBlockEditor } from '@wordpress/core-data';
-import apiFetch from '@wordpress/api-fetch';
-import { addQueryArgs } from '@wordpress/url';
+import { TEMPLATE_POST_TYPE } from '../../utils/constants';
function useFallbackTemplateContent( slug, isCustom = false ) {
const [ templateContent, setTemplateContent ] = useState( '' );
@@ -50,6 +51,37 @@ function useStartPatterns( fallbackContent ) {
};
}, [] );
+ const currentThemeStylesheet = useSelect(
+ ( select ) => select( coreStore ).getCurrentTheme().stylesheet
+ );
+
+ // Duplicated from packages/block-library/src/pattern/edit.js.
+ function injectThemeAttributeInBlockTemplateContent( block ) {
+ if (
+ block.innerBlocks.find(
+ ( innerBlock ) => innerBlock.name === 'core/template-part'
+ )
+ ) {
+ block.innerBlocks = block.innerBlocks.map( ( innerBlock ) => {
+ if (
+ innerBlock.name === 'core/template-part' &&
+ innerBlock.attributes.theme === undefined
+ ) {
+ innerBlock.attributes.theme = currentThemeStylesheet;
+ }
+ return innerBlock;
+ } );
+ }
+
+ if (
+ block.name === 'core/template-part' &&
+ block.attributes.theme === undefined
+ ) {
+ block.attributes.theme = currentThemeStylesheet;
+ }
+ return block;
+ }
+
return useMemo( () => {
// filter patterns that are supposed to be used in the current template being edited.
return [
@@ -68,7 +100,12 @@ function useStartPatterns( fallbackContent ) {
);
} )
.map( ( pattern ) => {
- return { ...pattern, blocks: parse( pattern.content ) };
+ return {
+ ...pattern,
+ blocks: parse( pattern.content ).map( ( block ) =>
+ injectThemeAttributeInBlockTemplateContent( block )
+ ),
+ };
} ),
];
}, [ fallbackContent, slug, patterns ] );
@@ -162,7 +199,7 @@ export default function StartTemplateOptions() {
shouldOpenModal:
! hasEdits &&
'' === templateRecord.content &&
- 'wp_template' === _postType &&
+ TEMPLATE_POST_TYPE === _postType &&
! select( preferencesStore ).get(
'core/edit-site',
'welcomeGuide'
diff --git a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js
index 19518f650c0be3..b178ce501301ef 100644
--- a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js
+++ b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js
@@ -11,6 +11,12 @@ import { privateApis as routerPrivateApis } from '@wordpress/router';
*/
import { store as editSiteStore } from '../../store';
import { unlock } from '../../lock-unlock';
+import {
+ TEMPLATE_POST_TYPE,
+ TEMPLATE_PART_POST_TYPE,
+ NAVIGATION_POST_TYPE,
+ PATTERN_TYPES,
+} from '../../utils/constants';
const { useLocation } = unlock( routerPrivateApis );
@@ -42,16 +48,16 @@ export default function useInitEditedEntityFromURL() {
useEffect( () => {
if ( postType && postId ) {
switch ( postType ) {
- case 'wp_template':
+ case TEMPLATE_POST_TYPE:
setTemplate( postId );
break;
- case 'wp_template_part':
+ case TEMPLATE_PART_POST_TYPE:
setTemplatePart( postId );
break;
- case 'wp_navigation':
+ case NAVIGATION_POST_TYPE:
setNavigationMenu( postId );
break;
- case 'wp_block':
+ case PATTERN_TYPES.user:
setEditedEntity( postType, postId );
break;
default:
diff --git a/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js b/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js
index 86928c1920a948..5f176dff8198d3 100644
--- a/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js
+++ b/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js
@@ -9,6 +9,11 @@ import { privateApis as routerPrivateApis } from '@wordpress/router';
* Internal dependencies
*/
import { unlock } from '../../lock-unlock';
+import {
+ TEMPLATE_POST_TYPE,
+ TEMPLATE_PART_POST_TYPE,
+ PATTERN_TYPES,
+} from '../../utils/constants';
const { useLocation, useHistory } = unlock( routerPrivateApis );
@@ -18,9 +23,9 @@ export function getPathFromURL( urlParams ) {
// Compute the navigator path based on the URL params.
if ( urlParams?.postType && urlParams?.postId ) {
switch ( urlParams.postType ) {
- case 'wp_block':
- case 'wp_template':
- case 'wp_template_part':
+ case PATTERN_TYPES.user:
+ case TEMPLATE_POST_TYPE:
+ case TEMPLATE_PART_POST_TYPE:
case 'page':
path = `/${ encodeURIComponent(
urlParams.postType
diff --git a/packages/edit-site/src/components/template-actions/index.js b/packages/edit-site/src/components/template-actions/index.js
index 2084d72ce9f8eb..f48e69245da1fe 100644
--- a/packages/edit-site/src/components/template-actions/index.js
+++ b/packages/edit-site/src/components/template-actions/index.js
@@ -22,6 +22,7 @@ import { store as editSiteStore } from '../../store';
import isTemplateRemovable from '../../utils/is-template-removable';
import isTemplateRevertable from '../../utils/is-template-revertable';
import RenameMenuItem from './rename-menu-item';
+import { TEMPLATE_POST_TYPE } from '../../utils/constants';
export default function TemplateActions( {
postType,
@@ -68,7 +69,7 @@ export default function TemplateActions( {
);
} catch ( error ) {
const fallbackErrorMessage =
- template.type === 'wp_template'
+ template.type === TEMPLATE_POST_TYPE
? __( 'An error occurred while reverting the template.' )
: __(
'An error occurred while reverting the template part.'
diff --git a/packages/edit-site/src/components/template-actions/rename-menu-item.js b/packages/edit-site/src/components/template-actions/rename-menu-item.js
index 42bec0a37ba857..d098ea13fa58f8 100644
--- a/packages/edit-site/src/components/template-actions/rename-menu-item.js
+++ b/packages/edit-site/src/components/template-actions/rename-menu-item.js
@@ -16,6 +16,11 @@ import { store as coreStore } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';
import { decodeEntities } from '@wordpress/html-entities';
+/**
+ * Internal dependencies
+ */
+import { TEMPLATE_POST_TYPE } from '../../utils/constants';
+
export default function RenameMenuItem( { template, onClose } ) {
const title = decodeEntities( template.title.rendered );
const [ editedTitle, setEditedTitle ] = useState( title );
@@ -28,7 +33,7 @@ export default function RenameMenuItem( { template, onClose } ) {
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
- if ( template.type === 'wp_template' && ! template.is_custom ) {
+ if ( template.type === TEMPLATE_POST_TYPE && ! template.is_custom ) {
return null;
}
@@ -57,7 +62,7 @@ export default function RenameMenuItem( { template, onClose } ) {
);
createSuccessNotice(
- template.type === 'wp_template'
+ template.type === TEMPLATE_POST_TYPE
? __( 'Template renamed.' )
: __( 'Template part renamed.' ),
{
@@ -66,7 +71,7 @@ export default function RenameMenuItem( { template, onClose } ) {
);
} catch ( error ) {
const fallbackErrorMessage =
- template.type === 'wp_template'
+ template.type === TEMPLATE_POST_TYPE
? __( 'An error occurred while renaming the template.' )
: __(
'An error occurred while renaming the template part.'
diff --git a/packages/edit-site/src/hooks/commands/use-common-commands.js b/packages/edit-site/src/hooks/commands/use-common-commands.js
index 16d07132ad7c74..f9d50261a12a46 100644
--- a/packages/edit-site/src/hooks/commands/use-common-commands.js
+++ b/packages/edit-site/src/hooks/commands/use-common-commands.js
@@ -3,8 +3,16 @@
*/
import { useMemo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
-import { __ } from '@wordpress/i18n';
-import { trash, backup, help, styles, external, brush } from '@wordpress/icons';
+import { __, isRTL } from '@wordpress/i18n';
+import {
+ rotateLeft,
+ rotateRight,
+ backup,
+ help,
+ styles,
+ external,
+ brush,
+} from '@wordpress/icons';
import { useCommandLoader, useCommand } from '@wordpress/commands';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { privateApis as routerPrivateApis } from '@wordpress/router';
@@ -148,8 +156,8 @@ function useGlobalStylesResetCommands() {
return [
{
name: 'core/edit-site/reset-global-styles',
- label: __( 'Reset styles to defaults' ),
- icon: trash,
+ label: __( 'Reset styles' ),
+ icon: isRTL() ? rotateRight : rotateLeft,
callback: ( { close } ) => {
close();
onReset();
diff --git a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js
index 76c7eea5137439..a10d518d53644b 100644
--- a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js
+++ b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js
@@ -33,6 +33,7 @@ import isTemplateRevertable from '../../utils/is-template-revertable';
import { KEYBOARD_SHORTCUT_HELP_MODAL_NAME } from '../../components/keyboard-shortcut-help-modal';
import { PREFERENCES_MODAL_NAME } from '../../components/preferences-modal';
import { unlock } from '../../lock-unlock';
+import { TEMPLATE_POST_TYPE } from '../../utils/constants';
const { useHistory } = unlock( routerPrivateApis );
@@ -131,7 +132,7 @@ function useManipulateDocumentCommands() {
if ( isTemplateRevertable( template ) && ! hasPageContentFocus ) {
const label =
- template.type === 'wp_template'
+ template.type === TEMPLATE_POST_TYPE
? /* translators: %1$s: template title */
sprintf(
'Reset template: %s',
@@ -155,7 +156,7 @@ function useManipulateDocumentCommands() {
if ( isTemplateRemovable( template ) && ! hasPageContentFocus ) {
const label =
- template.type === 'wp_template'
+ template.type === TEMPLATE_POST_TYPE
? /* translators: %1$s: template title */
sprintf(
'Delete template: %s',
@@ -167,7 +168,7 @@ function useManipulateDocumentCommands() {
decodeEntities( template.title )
);
const path =
- template.type === 'wp_template'
+ template.type === TEMPLATE_POST_TYPE
? '/wp_template'
: '/wp_template_part/all';
commands.push( {
diff --git a/packages/edit-site/src/hooks/navigation-menu-edit.js b/packages/edit-site/src/hooks/navigation-menu-edit.js
index d99ee57e0d953a..4c04b1b2534026 100644
--- a/packages/edit-site/src/hooks/navigation-menu-edit.js
+++ b/packages/edit-site/src/hooks/navigation-menu-edit.js
@@ -15,6 +15,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router';
*/
import { useLink } from '../components/routes/link';
import { unlock } from '../lock-unlock';
+import { NAVIGATION_POST_TYPE } from '../utils/constants';
const { useLocation } = unlock( routerPrivateApis );
@@ -26,7 +27,7 @@ function NavigationMenuEdit( { attributes } ) {
( select ) => {
return select( coreStore ).getEntityRecord(
'postType',
- 'wp_navigation',
+ NAVIGATION_POST_TYPE,
// Ideally this should be an official public API.
ref
);
diff --git a/packages/edit-site/src/hooks/template-part-edit.js b/packages/edit-site/src/hooks/template-part-edit.js
index 07fb717b85af18..66f54967c55e22 100644
--- a/packages/edit-site/src/hooks/template-part-edit.js
+++ b/packages/edit-site/src/hooks/template-part-edit.js
@@ -15,6 +15,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router';
*/
import { useLink } from '../components/routes/link';
import { unlock } from '../lock-unlock';
+import { TEMPLATE_PART_POST_TYPE } from '../utils/constants';
const { useLocation } = unlock( routerPrivateApis );
@@ -25,7 +26,7 @@ function EditTemplatePartMenuItem( { attributes } ) {
( select ) => {
return select( coreStore ).getEntityRecord(
'postType',
- 'wp_template_part',
+ TEMPLATE_PART_POST_TYPE,
// Ideally this should be an official public API.
`${ theme }//${ slug }`
);
diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js
index 61569482c69dbf..ce698a757f6bb3 100644
--- a/packages/edit-site/src/store/actions.js
+++ b/packages/edit-site/src/store/actions.js
@@ -19,7 +19,11 @@ import { decodeEntities } from '@wordpress/html-entities';
*/
import { STORE_NAME as editSiteStoreName } from './constants';
import isTemplateRevertable from '../utils/is-template-revertable';
-
+import {
+ TEMPLATE_POST_TYPE,
+ TEMPLATE_PART_POST_TYPE,
+ NAVIGATION_POST_TYPE,
+} from '../utils/constants';
/**
* Dispatches an action that toggles a feature flag.
*
@@ -67,14 +71,18 @@ export const setTemplate =
try {
const template = await registry
.resolveSelect( coreStore )
- .getEntityRecord( 'postType', 'wp_template', templateId );
+ .getEntityRecord(
+ 'postType',
+ TEMPLATE_POST_TYPE,
+ templateId
+ );
templateSlug = template?.slug;
} catch ( error ) {}
}
dispatch( {
type: 'SET_EDITED_POST',
- postType: 'wp_template',
+ postType: TEMPLATE_POST_TYPE,
id: templateId,
context: { templateSlug },
} );
@@ -92,14 +100,14 @@ export const addTemplate =
async ( { dispatch, registry } ) => {
const newTemplate = await registry
.dispatch( coreStore )
- .saveEntityRecord( 'postType', 'wp_template', template );
+ .saveEntityRecord( 'postType', TEMPLATE_POST_TYPE, template );
if ( template.content ) {
registry
.dispatch( coreStore )
.editEntityRecord(
'postType',
- 'wp_template',
+ TEMPLATE_POST_TYPE,
newTemplate.id,
{ blocks: parse( template.content ) },
{ undoIgnore: true }
@@ -108,7 +116,7 @@ export const addTemplate =
dispatch( {
type: 'SET_EDITED_POST',
- postType: 'wp_template',
+ postType: TEMPLATE_POST_TYPE,
id: newTemplate.id,
context: { templateSlug: newTemplate.slug },
} );
@@ -178,7 +186,7 @@ export const removeTemplate =
export function setTemplatePart( templatePartId ) {
return {
type: 'SET_EDITED_POST',
- postType: 'wp_template_part',
+ postType: TEMPLATE_PART_POST_TYPE,
id: templatePartId,
};
}
@@ -193,7 +201,7 @@ export function setTemplatePart( templatePartId ) {
export function setNavigationMenu( navigationMenuId ) {
return {
type: 'SET_EDITED_POST',
- postType: 'wp_navigation',
+ postType: NAVIGATION_POST_TYPE,
id: navigationMenuId,
};
}
@@ -282,7 +290,7 @@ export const setPage =
const currentTemplate = (
await registry
.resolveSelect( coreStore )
- .getEntityRecords( 'postType', 'wp_template', {
+ .getEntityRecords( 'postType', TEMPLATE_POST_TYPE, {
per_page: -1,
} )
)?.find( ( { slug } ) => slug === currentTemplateSlug );
@@ -305,7 +313,7 @@ export const setPage =
dispatch( {
type: 'SET_EDITED_POST',
- postType: 'wp_template',
+ postType: TEMPLATE_POST_TYPE,
id: template.id,
context: {
...page.context,
diff --git a/packages/edit-site/src/store/constants.js b/packages/edit-site/src/store/constants.js
index 272f2049a088b2..174341a5cb02ae 100644
--- a/packages/edit-site/src/store/constants.js
+++ b/packages/edit-site/src/store/constants.js
@@ -4,8 +4,3 @@
* @type {string}
*/
export const STORE_NAME = 'core/edit-site';
-
-export const TEMPLATE_PART_AREA_HEADER = 'header';
-export const TEMPLATE_PART_AREA_FOOTER = 'footer';
-export const TEMPLATE_PART_AREA_SIDEBAR = 'sidebar';
-export const TEMPLATE_PART_AREA_GENERAL = 'uncategorized';
diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js
index f3dd4c10cec43e..3e2bfe2ee47b24 100644
--- a/packages/edit-site/src/store/private-actions.js
+++ b/packages/edit-site/src/store/private-actions.js
@@ -49,3 +49,22 @@ export const setEditorCanvasContainerView =
view,
} );
};
+
+/**
+ * Sets the type of page content focus. Can be one of:
+ *
+ * - `'disableTemplate'`: Disable the blocks belonging to the page's template.
+ * - `'hideTemplate'`: Hide the blocks belonging to the page's template.
+ *
+ * @param {'disableTemplate'|'hideTemplate'} pageContentFocusType The type of page content focus.
+ *
+ * @return {Object} Action object.
+ */
+export const setPageContentFocusType =
+ ( pageContentFocusType ) =>
+ ( { dispatch } ) => {
+ dispatch( {
+ type: 'SET_PAGE_CONTENT_FOCUS_TYPE',
+ pageContentFocusType,
+ } );
+ };
diff --git a/packages/edit-site/src/store/private-selectors.js b/packages/edit-site/src/store/private-selectors.js
index 1f1f6e999fdb29..0d4cf2b3eefdaa 100644
--- a/packages/edit-site/src/store/private-selectors.js
+++ b/packages/edit-site/src/store/private-selectors.js
@@ -1,3 +1,8 @@
+/**
+ * Internal dependencies
+ */
+import { hasPageContentFocus } from './selectors';
+
/**
* Returns the current canvas mode.
*
@@ -19,3 +24,20 @@ export function getCanvasMode( state ) {
export function getEditorCanvasContainerView( state ) {
return state.editorCanvasContainerView;
}
+
+/**
+ * Returns the type of the current page content focus, or null if there is no
+ * page content focus.
+ *
+ * Possible values are:
+ *
+ * - `'disableTemplate'`: Disable the blocks belonging to the page's template.
+ * - `'hideTemplate'`: Hide the blocks belonging to the page's template.
+ *
+ * @param {Object} state Global application state.
+ *
+ * @return {'disableTemplate'|'hideTemplate'|null} Type of the current page content focus.
+ */
+export function getPageContentFocusType( state ) {
+ return hasPageContentFocus( state ) ? state.pageContentFocusType : null;
+}
diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js
index 4b4689e26c561e..e99c6dda1fc1d0 100644
--- a/packages/edit-site/src/store/reducer.js
+++ b/packages/edit-site/src/store/reducer.js
@@ -177,6 +177,23 @@ export function hasPageContentFocus( state = false, action ) {
return state;
}
+/**
+ * Reducer used to track the type of page content focus.
+ *
+ * @param {string} state Current state.
+ * @param {Object} action Dispatched action.
+ *
+ * @return {string} Updated state.
+ */
+export function pageContentFocusType( state = 'disableTemplate', action ) {
+ switch ( action.type ) {
+ case 'SET_PAGE_CONTENT_FOCUS_TYPE':
+ return action.pageContentFocusType;
+ }
+
+ return state;
+}
+
export default combineReducers( {
deviceType,
settings,
@@ -187,4 +204,5 @@ export default combineReducers( {
canvasMode,
editorCanvasContainerView,
hasPageContentFocus,
+ pageContentFocusType,
} );
diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js
index 654b3c321ae93f..3f44ab57ba8072 100644
--- a/packages/edit-site/src/store/selectors.js
+++ b/packages/edit-site/src/store/selectors.js
@@ -18,7 +18,10 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
* Internal dependencies
*/
import { getFilteredTemplatePartBlocks } from './utils';
-
+import {
+ TEMPLATE_POST_TYPE,
+ TEMPLATE_PART_POST_TYPE,
+} from '../utils/constants';
/**
* @typedef {'template'|'template_type'} TemplateType Template type.
*/
@@ -126,7 +129,7 @@ export const getSettings = createSelector(
__experimentalSetIsInserterOpened: setIsInserterOpen,
__experimentalReusableBlocks: getReusableBlocks( state ),
__experimentalPreferPatternsOnRoot:
- 'wp_template' === getEditedPostType( state ),
+ TEMPLATE_POST_TYPE === getEditedPostType( state ),
};
const canUserCreateMedia = getCanUserCreateMedia( state );
@@ -300,7 +303,7 @@ export const getCurrentTemplateTemplateParts = createRegistrySelector(
const templateParts = select( coreDataStore ).getEntityRecords(
'postType',
- 'wp_template_part',
+ TEMPLATE_PART_POST_TYPE,
{ per_page: -1 }
);
diff --git a/packages/edit-site/src/store/test/reducer.js b/packages/edit-site/src/store/test/reducer.js
index d3816f6ac0ac2a..a5e47ec5bbbaf3 100644
--- a/packages/edit-site/src/store/test/reducer.js
+++ b/packages/edit-site/src/store/test/reducer.js
@@ -12,6 +12,7 @@ import {
blockInserterPanel,
listViewPanel,
hasPageContentFocus,
+ pageContentFocusType,
} from '../reducer';
import { setIsInserterOpened } from '../actions';
@@ -191,4 +192,21 @@ describe( 'state', () => {
).toBe( false );
} );
} );
+
+ describe( 'pageContentFocusType', () => {
+ it( 'defaults to disableTemplate', () => {
+ expect( pageContentFocusType( undefined, {} ) ).toBe(
+ 'disableTemplate'
+ );
+ } );
+
+ it( 'can be set', () => {
+ expect(
+ pageContentFocusType( 'disableTemplate', {
+ type: 'SET_PAGE_CONTENT_FOCUS_TYPE',
+ pageContentFocusType: 'enableTemplate',
+ } )
+ ).toBe( 'enableTemplate' );
+ } );
+ } );
} );
diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss
index 111696241d0d69..e95cd3571c419f 100644
--- a/packages/edit-site/src/style.scss
+++ b/packages/edit-site/src/style.scss
@@ -4,6 +4,7 @@
@import "./components/block-editor/style.scss";
@import "./components/canvas-loader/style.scss";
@import "./components/code-editor/style.scss";
+@import "./components/dataviews/style.scss";
@import "./components/global-styles/style.scss";
@import "./components/global-styles/screen-revisions/style.scss";
@import "./components/header-edit-mode/style.scss";
diff --git a/packages/edit-site/src/utils/constants.js b/packages/edit-site/src/utils/constants.js
index d12c7f84cc356c..7d123818043801 100644
--- a/packages/edit-site/src/utils/constants.js
+++ b/packages/edit-site/src/utils/constants.js
@@ -15,12 +15,18 @@ export const NAVIGATION_POST_TYPE = 'wp_navigation';
// Templates.
export const TEMPLATE_POST_TYPE = 'wp_template';
export const TEMPLATE_PART_POST_TYPE = 'wp_template_part';
-export const TEMPLATE_CUSTOM_SOURCE = 'custom';
+export const TEMPLATE_ORIGINS = {
+ custom: 'custom',
+ theme: 'theme',
+ plugin: 'plugin',
+};
+export const TEMPLATE_PART_AREA_DEFAULT_CATEGORY = 'uncategorized';
// Patterns.
export const {
PATTERN_TYPES,
PATTERN_DEFAULT_CATEGORY,
+ PATTERN_USER_CATEGORY,
PATTERN_CORE_SOURCES,
PATTERN_SYNC_TYPES,
} = unlock( patternPrivateApis );
@@ -32,9 +38,19 @@ export const FOCUSABLE_ENTITIES = [
PATTERN_TYPES.user,
];
+/**
+ * Block types that are considered to be page content. These are the only blocks
+ * editable when hasPageContentFocus() is true.
+ */
+export const PAGE_CONTENT_BLOCK_TYPES = {
+ 'core/post-title': true,
+ 'core/post-featured-image': true,
+ 'core/post-content': true,
+};
+
export const POST_TYPE_LABELS = {
[ TEMPLATE_POST_TYPE ]: __( 'Template' ),
- [ TEMPLATE_PART_POST_TYPE ]: __( 'Template Part' ),
+ [ TEMPLATE_PART_POST_TYPE ]: __( 'Template part' ),
[ PATTERN_TYPES.user ]: __( 'Pattern' ),
[ NAVIGATION_POST_TYPE ]: __( 'Navigation' ),
};
diff --git a/packages/edit-site/src/utils/get-is-list-page.js b/packages/edit-site/src/utils/get-is-list-page.js
index 600e686618bf94..2ee661253cf063 100644
--- a/packages/edit-site/src/utils/get-is-list-page.js
+++ b/packages/edit-site/src/utils/get-is-list-page.js
@@ -14,8 +14,9 @@ export default function getIsListPage(
isMobileViewport
) {
return (
- path === '/wp_template/all' ||
- path === '/wp_template_part/all' ||
+ [ '/wp_template/all', '/wp_template_part/all', '/pages' ].includes(
+ path
+ ) ||
( path === '/patterns' &&
// Don't treat "/patterns" without categoryType and categoryId as a
// list page in mobile because the sidebar covers the whole page.
diff --git a/packages/edit-site/src/utils/is-template-removable.js b/packages/edit-site/src/utils/is-template-removable.js
index 02682e97334e59..9cb1de23daab75 100644
--- a/packages/edit-site/src/utils/is-template-removable.js
+++ b/packages/edit-site/src/utils/is-template-removable.js
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
-import { TEMPLATE_CUSTOM_SOURCE } from './constants';
+import { TEMPLATE_ORIGINS } from './constants';
/**
* Check if a template is removable.
@@ -15,6 +15,6 @@ export default function isTemplateRemovable( template ) {
}
return (
- template.source === TEMPLATE_CUSTOM_SOURCE && ! template.has_theme_file
+ template.source === TEMPLATE_ORIGINS.custom && ! template.has_theme_file
);
}
diff --git a/packages/edit-site/src/utils/is-template-revertable.js b/packages/edit-site/src/utils/is-template-revertable.js
index 89d1d8ea552a19..a6274d07ebebb6 100644
--- a/packages/edit-site/src/utils/is-template-revertable.js
+++ b/packages/edit-site/src/utils/is-template-revertable.js
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
-import { TEMPLATE_CUSTOM_SOURCE } from './constants';
+import { TEMPLATE_ORIGINS } from './constants';
/**
* Check if a template is revertable to its original theme-provided template file.
@@ -15,7 +15,7 @@ export default function isTemplateRevertable( template ) {
}
/* eslint-disable camelcase */
return (
- template?.source === TEMPLATE_CUSTOM_SOURCE && template?.has_theme_file
+ template?.source === TEMPLATE_ORIGINS.custom && template?.has_theme_file
);
/* eslint-enable camelcase */
}
diff --git a/packages/edit-site/src/utils/template-part-create.js b/packages/edit-site/src/utils/template-part-create.js
index b81a98d15684a7..955519a91a2344 100644
--- a/packages/edit-site/src/utils/template-part-create.js
+++ b/packages/edit-site/src/utils/template-part-create.js
@@ -9,12 +9,17 @@ import { paramCase as kebabCase } from 'change-case';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
+/**
+ * Internal dependencies
+ */
+import { TEMPLATE_PART_POST_TYPE } from './constants';
+
export const useExistingTemplateParts = () => {
return useSelect(
( select ) =>
select( coreStore ).getEntityRecords(
'postType',
- 'wp_template_part',
+ TEMPLATE_PART_POST_TYPE,
{
per_page: -1,
}
diff --git a/packages/edit-widgets/src/components/header/index.js b/packages/edit-widgets/src/components/header/index.js
index d6f12bff4d5235..0308c2c2171e24 100644
--- a/packages/edit-widgets/src/components/header/index.js
+++ b/packages/edit-widgets/src/components/header/index.js
@@ -27,7 +27,7 @@ import { unlock } from '../../lock-unlock';
const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis );
-function Header() {
+function Header( { setListViewToggleElement } ) {
const isMediumViewport = useViewportMatch( 'medium' );
const inserterButton = useRef();
const widgetAreaClientId = useLastSelectedWidgetArea();
@@ -140,6 +140,7 @@ function Header() {
/* translators: button label text should, if possible, be under 16 characters. */
label={ __( 'List View' ) }
onClick={ toggleListView }
+ ref={ setListViewToggleElement }
/>
>
) }
diff --git a/packages/edit-widgets/src/components/layout/interface.js b/packages/edit-widgets/src/components/layout/interface.js
index 987e3868de1337..2cb1eebcfab73b 100644
--- a/packages/edit-widgets/src/components/layout/interface.js
+++ b/packages/edit-widgets/src/components/layout/interface.js
@@ -3,7 +3,7 @@
*/
import { useViewportMatch } from '@wordpress/compose';
import { BlockBreadcrumb } from '@wordpress/block-editor';
-import { useEffect } from '@wordpress/element';
+import { useEffect, useState } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import {
InterfaceSkeleton,
@@ -68,6 +68,9 @@ function Interface( { blockEditorSettings } ) {
[]
);
+ const [ listViewToggleElement, setListViewToggleElement ] =
+ useState( null );
+
// Inserter and Sidebars are mutually exclusive
useEffect( () => {
if ( hasSidebarEnabled && ! isHugeViewport ) {
@@ -94,8 +97,16 @@ function Interface( { blockEditorSettings } ) {
...interfaceLabels,
secondarySidebar: secondarySidebarLabel,
} }
- header={ }
- secondarySidebar={ hasSecondarySidebar && }
+ header={
+
+ }
+ secondarySidebar={
+ hasSecondarySidebar && (
+
+ )
+ }
sidebar={
hasSidebarEnabled && (
diff --git a/packages/edit-widgets/src/components/layout/unsaved-changes-warning.js b/packages/edit-widgets/src/components/layout/unsaved-changes-warning.js
index 4d1675222ef99f..382bc6fa0d8905 100644
--- a/packages/edit-widgets/src/components/layout/unsaved-changes-warning.js
+++ b/packages/edit-widgets/src/components/layout/unsaved-changes-warning.js
@@ -16,7 +16,7 @@ import { store as editWidgetsStore } from '../../store';
* This is a duplicate of the component implemented in the editor package.
* Duplicated here as edit-widgets doesn't depend on editor.
*
- * @return {WPComponent} The component.
+ * @return {Component} The component.
*/
export default function UnsavedChangesWarning() {
const isDirty = useSelect( ( select ) => {
diff --git a/packages/edit-widgets/src/components/secondary-sidebar/index.js b/packages/edit-widgets/src/components/secondary-sidebar/index.js
index 49e240bd147cb2..20488e4478b980 100644
--- a/packages/edit-widgets/src/components/secondary-sidebar/index.js
+++ b/packages/edit-widgets/src/components/secondary-sidebar/index.js
@@ -13,7 +13,7 @@ import { store as editWidgetsStore } from '../../store';
import InserterSidebar from './inserter-sidebar';
import ListViewSidebar from './list-view-sidebar';
-export default function SecondarySidebar() {
+export default function SecondarySidebar( { listViewToggleElement } ) {
const { isInserterOpen, isListViewOpen } = useSelect( ( select ) => {
const { isInserterOpened, isListViewOpened } =
select( editWidgetsStore );
@@ -27,7 +27,9 @@ export default function SecondarySidebar() {
return ;
}
if ( isListViewOpen ) {
- return ;
+ return (
+
+ );
}
return null;
}
diff --git a/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js
index eeff1ea0bf0f3f..5104a587d9e2cf 100644
--- a/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js
+++ b/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js
@@ -3,13 +3,9 @@
*/
import { __experimentalListView as ListView } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
-import {
- useFocusOnMount,
- useFocusReturn,
- useMergeRefs,
-} from '@wordpress/compose';
+import { useFocusOnMount, useMergeRefs } from '@wordpress/compose';
import { useDispatch } from '@wordpress/data';
-import { useState } from '@wordpress/element';
+import { useCallback, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { closeSmall } from '@wordpress/icons';
import { ESCAPE } from '@wordpress/keycodes';
@@ -19,7 +15,7 @@ import { ESCAPE } from '@wordpress/keycodes';
*/
import { store as editWidgetsStore } from '../../store';
-export default function ListViewSidebar() {
+export default function ListViewSidebar( { listViewToggleElement } ) {
const { setIsListViewOpened } = useDispatch( editWidgetsStore );
// Use internal state instead of a ref to make sure that the component
@@ -27,15 +23,22 @@ export default function ListViewSidebar() {
const [ dropZoneElement, setDropZoneElement ] = useState( null );
const focusOnMountRef = useFocusOnMount( 'firstElement' );
- const headerFocusReturnRef = useFocusReturn();
- const contentFocusReturnRef = useFocusReturn();
- function closeOnEscape( event ) {
- if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) {
- event.preventDefault();
- setIsListViewOpened( false );
- }
- }
+ // When closing the list view, focus should return to the toggle button.
+ const closeListView = useCallback( () => {
+ setIsListViewOpened( false );
+ listViewToggleElement?.focus();
+ }, [ listViewToggleElement, setIsListViewOpened ] );
+
+ const closeOnEscape = useCallback(
+ ( event ) => {
+ if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) {
+ event.preventDefault();
+ closeListView();
+ }
+ },
+ [ closeListView ]
+ );
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
@@ -43,24 +46,17 @@ export default function ListViewSidebar() {
className="edit-widgets-editor__list-view-panel"
onKeyDown={ closeOnEscape }
>
-
+
{ __( 'List View' ) }
setIsListViewOpened( false ) }
+ onClick={ closeListView }
/>
diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js
index f3a7b84d5ded68..d538e37ad1ec8a 100644
--- a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js
+++ b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js
@@ -10,10 +10,7 @@ import {
useResourcePermissions,
} from '@wordpress/core-data';
import { useMemo } from '@wordpress/element';
-import {
- CopyHandler,
- privateApis as blockEditorPrivateApis,
-} from '@wordpress/block-editor';
+import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns';
import { store as preferencesStore } from '@wordpress/preferences';
@@ -107,7 +104,7 @@ export default function WidgetAreasBlockEditorProvider( {
useSubRegistry={ false }
{ ...props }
>
-
{ children }
+ { children }
diff --git a/packages/editor/README.md b/packages/editor/README.md
index 157136b28d0d66..12cb885db857e8 100644
--- a/packages/editor/README.md
+++ b/packages/editor/README.md
@@ -37,8 +37,8 @@ When returned by your block's `edit` implementation, renders a toolbar of icon b
Example:
```js
-( function ( editor, element ) {
- var el = element.createElement,
+( function ( editor, React ) {
+ var el = React.createElement,
BlockControls = editor.BlockControls,
AlignmentToolbar = editor.AlignmentToolbar;
@@ -64,7 +64,7 @@ Example:
),
];
}
-} )( window.wp.editor, window.wp.element );
+} )( window.wp.editor, window.React );
```
Note in this example that we render `AlignmentToolbar` as a child of the `BlockControls` element. This is another pre-configured component you can use to simplify block text alignment.
@@ -95,8 +95,8 @@ The following properties (non-exhaustive list) are made available:
Example:
```js
-( function ( editor, element ) {
- var el = element.createElement,
+( function ( editor, React ) {
+ var el = React.createElement,
RichText = editor.RichText;
function edit( props ) {
@@ -111,7 +111,7 @@ Example:
}
// blocks.registerBlockType( ..., { edit: edit, ... } );
-} )( window.wp.editor, window.wp.element );
+} )( window.wp.editor, window.React );
```
## Contributing to this package
diff --git a/packages/editor/src/components/post-featured-image/README.md b/packages/editor/src/components/post-featured-image/README.md
index 9b1520228703fb..369e4d7ef94399 100644
--- a/packages/editor/src/components/post-featured-image/README.md
+++ b/packages/editor/src/components/post-featured-image/README.md
@@ -13,7 +13,7 @@ Replace the contents of the panel:
```js
function replacePostFeaturedImage() {
return function () {
- return wp.element.createElement(
+ return React.createElement(
'div',
{},
'The replacement contents or components.'
@@ -31,12 +31,12 @@ wp.hooks.addFilter(
Prepend and append to the panel contents:
```js
-var el = wp.element.createElement;
+var el = React.createElement;
function wrapPostFeaturedImage( OriginalComponent ) {
return function ( props ) {
return el(
- wp.element.Fragment,
+ React.Fragment,
{},
'Prepend above',
el( OriginalComponent, props ),
diff --git a/packages/editor/src/components/post-saved-state/index.js b/packages/editor/src/components/post-saved-state/index.js
index ed3115d5a6c54a..9d6cb49d91e8e8 100644
--- a/packages/editor/src/components/post-saved-state/index.js
+++ b/packages/editor/src/components/post-saved-state/index.js
@@ -30,7 +30,7 @@ import { store as editorStore } from '../../store';
* @param {?boolean} props.forceIsDirty Whether to force the post to be marked
* as dirty.
* @param {?boolean} props.showIconLabels Whether interface buttons show labels instead of icons
- * @return {import('@wordpress/element').WPComponent} The component.
+ * @return {import('react').ComponentType} The component.
*/
export default function PostSavedState( {
forceIsDirty,
diff --git a/packages/editor/src/components/post-sticky/check.js b/packages/editor/src/components/post-sticky/check.js
index bd5e47da7749b1..e31c145b9bbf0f 100644
--- a/packages/editor/src/components/post-sticky/check.js
+++ b/packages/editor/src/components/post-sticky/check.js
@@ -1,28 +1,25 @@
/**
* WordPress dependencies
*/
-import { compose } from '@wordpress/compose';
-import { withSelect } from '@wordpress/data';
+import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
-export function PostStickyCheck( { hasStickyAction, postType, children } ) {
+export default function PostStickyCheck( { children } ) {
+ const { hasStickyAction, postType } = useSelect( ( select ) => {
+ const post = select( editorStore ).getCurrentPost();
+ return {
+ hasStickyAction: post._links?.[ 'wp:action-sticky' ] ?? false,
+ postType: select( editorStore ).getCurrentPostType(),
+ };
+ }, [] );
+
if ( postType !== 'post' || ! hasStickyAction ) {
return null;
}
return children;
}
-
-export default compose( [
- withSelect( ( select ) => {
- const post = select( editorStore ).getCurrentPost();
- return {
- hasStickyAction: post._links?.[ 'wp:action-sticky' ] ?? false,
- postType: select( editorStore ).getCurrentPostType(),
- };
- } ),
-] )( PostStickyCheck );
diff --git a/packages/editor/src/components/post-sticky/index.js b/packages/editor/src/components/post-sticky/index.js
index 253b927f0240c1..fe8820243962b6 100644
--- a/packages/editor/src/components/post-sticky/index.js
+++ b/packages/editor/src/components/post-sticky/index.js
@@ -3,8 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { CheckboxControl } from '@wordpress/components';
-import { withSelect, withDispatch } from '@wordpress/data';
-import { compose } from '@wordpress/compose';
+import { useDispatch, useSelect } from '@wordpress/data';
/**
* Internal dependencies
@@ -12,31 +11,22 @@ import { compose } from '@wordpress/compose';
import PostStickyCheck from './check';
import { store as editorStore } from '../../store';
-export function PostSticky( { onUpdateSticky, postSticky = false } ) {
+export default function PostSticky() {
+ const postSticky = useSelect( ( select ) => {
+ return (
+ select( editorStore ).getEditedPostAttribute( 'sticky' ) ?? false
+ );
+ }, [] );
+ const { editPost } = useDispatch( editorStore );
+
return (
onUpdateSticky( ! postSticky ) }
+ onChange={ () => editPost( { sticky: ! postSticky } ) }
/>
);
}
-
-export default compose( [
- withSelect( ( select ) => {
- return {
- postSticky:
- select( editorStore ).getEditedPostAttribute( 'sticky' ),
- };
- } ),
- withDispatch( ( dispatch ) => {
- return {
- onUpdateSticky( postSticky ) {
- dispatch( editorStore ).editPost( { sticky: postSticky } );
- },
- };
- } ),
-] )( PostSticky );
diff --git a/packages/editor/src/components/post-sticky/test/index.js b/packages/editor/src/components/post-sticky/test/index.js
index f637ec89e9582e..ff567cfe2598eb 100644
--- a/packages/editor/src/components/post-sticky/test/index.js
+++ b/packages/editor/src/components/post-sticky/test/index.js
@@ -3,40 +3,55 @@
*/
import { render, screen } from '@testing-library/react';
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+
/**
* Internal dependencies
*/
-import { PostStickyCheck } from '../check';
+import PostStickyCheck from '../check';
+
+jest.mock( '@wordpress/data/src/components/use-select', () => {
+ // This allows us to tweak the returned value on each test.
+ const mock = jest.fn();
+ return mock;
+} );
+
+function setupUseSelectMock( { hasStickyAction, postType } ) {
+ useSelect.mockImplementation( ( cb ) => {
+ return cb( () => ( {
+ getCurrentPostType: () => postType,
+ getCurrentPost: () => ( {
+ _links: {
+ 'wp:action-sticky': hasStickyAction,
+ },
+ } ),
+ } ) );
+ } );
+}
describe( 'PostSticky', () => {
it( 'should not render anything if the post type is not "post"', () => {
- render(
-
- Can Toggle Sticky
-
- );
+ setupUseSelectMock( { hasStickyAction: true, postType: 'page' } );
+ render(
Can Toggle Sticky );
expect(
screen.queryByText( 'Can Toggle Sticky' )
).not.toBeInTheDocument();
} );
it( "should not render anything if post doesn't support stickying", () => {
- render(
-
- Can Toggle Sticky
-
- );
+ setupUseSelectMock( { hasStickyAction: false, postType: 'post' } );
+ render(
Can Toggle Sticky );
expect(
screen.queryByText( 'Can Toggle Sticky' )
).not.toBeInTheDocument();
} );
it( 'should render if the post supports stickying', () => {
- render(
-
- Can Toggle Sticky
-
- );
+ setupUseSelectMock( { hasStickyAction: true, postType: 'post' } );
+ render(
Can Toggle Sticky );
expect( screen.getByText( 'Can Toggle Sticky' ) ).toBeVisible();
} );
} );
diff --git a/packages/editor/src/components/post-switch-to-draft-button/index.js b/packages/editor/src/components/post-switch-to-draft-button/index.js
index 1fb04931bfce14..590d7722b82299 100644
--- a/packages/editor/src/components/post-switch-to-draft-button/index.js
+++ b/packages/editor/src/components/post-switch-to-draft-button/index.js
@@ -6,8 +6,7 @@ import {
__experimentalConfirmDialog as ConfirmDialog,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-import { withSelect, withDispatch } from '@wordpress/data';
-import { compose } from '@wordpress/compose';
+import { useDispatch, useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
/**
@@ -15,17 +14,21 @@ import { useState } from '@wordpress/element';
*/
import { store as editorStore } from '../../store';
-function PostSwitchToDraftButton( {
- isSaving,
- isPublished,
- isScheduled,
- onClick,
-} ) {
+export default function PostSwitchToDraftButton() {
const [ showConfirmDialog, setShowConfirmDialog ] = useState( false );
- if ( ! isPublished && ! isScheduled ) {
- return null;
- }
+ const { editPost, savePost } = useDispatch( editorStore );
+ const { isSaving, isPublished, isScheduled } = useSelect( ( select ) => {
+ const { isSavingPost, isCurrentPostPublished, isCurrentPostScheduled } =
+ select( editorStore );
+ return {
+ isSaving: isSavingPost(),
+ isPublished: isCurrentPostPublished(),
+ isScheduled: isCurrentPostScheduled(),
+ };
+ }, [] );
+
+ const isDisabled = isSaving || ( ! isPublished && ! isScheduled );
let alertMessage;
if ( isPublished ) {
@@ -36,7 +39,8 @@ function PostSwitchToDraftButton( {
const handleConfirm = () => {
setShowConfirmDialog( false );
- onClick();
+ editPost( { status: 'draft' } );
+ savePost();
};
return (
@@ -44,9 +48,11 @@ function PostSwitchToDraftButton( {
{
- setShowConfirmDialog( true );
+ if ( ! isDisabled ) {
+ setShowConfirmDialog( true );
+ }
} }
- disabled={ isSaving }
+ aria-disabled={ isDisabled }
variant="secondary"
style={ { flexGrow: '1', justifyContent: 'center' } }
>
@@ -62,24 +68,3 @@ function PostSwitchToDraftButton( {
>
);
}
-
-export default compose( [
- withSelect( ( select ) => {
- const { isSavingPost, isCurrentPostPublished, isCurrentPostScheduled } =
- select( editorStore );
- return {
- isSaving: isSavingPost(),
- isPublished: isCurrentPostPublished(),
- isScheduled: isCurrentPostScheduled(),
- };
- } ),
- withDispatch( ( dispatch ) => {
- const { editPost, savePost } = dispatch( editorStore );
- return {
- onClick: () => {
- editPost( { status: 'draft' } );
- savePost();
- },
- };
- } ),
-] )( PostSwitchToDraftButton );
diff --git a/packages/editor/src/components/post-taxonomies/README.md b/packages/editor/src/components/post-taxonomies/README.md
index 90dcfcf95eb9df..941c28d4639314 100644
--- a/packages/editor/src/components/post-taxonomies/README.md
+++ b/packages/editor/src/components/post-taxonomies/README.md
@@ -19,7 +19,7 @@ For example, to render alternative UI for the taxonomy `product-type`,
we can render custom markup or use the original component as shown below.
```js
-var el = wp.element.createElement;
+var el = React.createElement;
function customizeProductTypeSelector( OriginalComponent ) {
return function ( props ) {
@@ -42,7 +42,7 @@ Or, to use the hierarchical term selector with a non-hierarchical taxonomy `trac
you can set the `HierarchicalTermSelector` component as shown below.
```js
-const el = wp.element.createElement;
+const el = React.createElement;
const HierarchicalTermSelector = wp.editor.PostTaxonomiesHierarchicalTermSelector;
function customizeTrackSelector( OriginalComponent ) {
diff --git a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js
index aeed0b4f4b6d9f..c93458ed3cfef2 100644
--- a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js
+++ b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js
@@ -149,7 +149,7 @@ export function getFilterMatcher( filterValue ) {
*
* @param {Object} props Component props.
* @param {string} props.slug Taxonomy slug.
- * @return {WPElement} Hierarchical term selector component.
+ * @return {Element} Hierarchical term selector component.
*/
export function HierarchicalTermSelector( { slug } ) {
const [ adding, setAdding ] = useState( false );
diff --git a/packages/editor/src/components/post-trash/index.js b/packages/editor/src/components/post-trash/index.js
index 08628689fc1451..e775944b5d69ed 100644
--- a/packages/editor/src/components/post-trash/index.js
+++ b/packages/editor/src/components/post-trash/index.js
@@ -2,8 +2,12 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { Button } from '@wordpress/components';
+import {
+ Button,
+ __experimentalConfirmDialog as ConfirmDialog,
+} from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
+import { useState } from '@wordpress/element';
/**
* Internal dependencies
@@ -20,21 +24,40 @@ export default function PostTrash() {
};
}, [] );
const { trashPost } = useDispatch( editorStore );
+ const [ showConfirmDialog, setShowConfirmDialog ] = useState( false );
if ( isNew || ! postId ) {
return null;
}
+ const handleConfirm = () => {
+ setShowConfirmDialog( false );
+ trashPost();
+ };
+
return (
- trashPost() }
- >
- { __( 'Move to trash' ) }
-
+ <>
+ setShowConfirmDialog( true )
+ }
+ >
+ { __( 'Move to trash' ) }
+
+ setShowConfirmDialog( false ) }
+ >
+ { __(
+ 'Are you sure you want to move this post to the trash?'
+ ) }
+
+ >
);
}
diff --git a/packages/editor/src/components/post-type-support-check/README.md b/packages/editor/src/components/post-type-support-check/README.md
index 565882fab758ba..eab70d088999b7 100644
--- a/packages/editor/src/components/post-type-support-check/README.md
+++ b/packages/editor/src/components/post-type-support-check/README.md
@@ -16,7 +16,7 @@ If the post type is not yet known, it will be assumed to be supported.
### `children`
-_Type:_ `WPElement`
+_Type:_ `Element`
Children to be rendered if post type supports.
diff --git a/packages/editor/src/components/post-type-support-check/index.js b/packages/editor/src/components/post-type-support-check/index.js
index fffabf6ab247c9..c716593f458f17 100644
--- a/packages/editor/src/components/post-type-support-check/index.js
+++ b/packages/editor/src/components/post-type-support-check/index.js
@@ -14,12 +14,12 @@ import { store as editorStore } from '../../store';
* type supports one of the given `supportKeys` prop.
*
* @param {Object} props Props.
- * @param {WPElement} props.children Children to be rendered if post
+ * @param {Element} props.children Children to be rendered if post
* type supports.
* @param {(string|string[])} props.supportKeys String or string array of keys
* to test.
*
- * @return {WPComponent} The component to be rendered.
+ * @return {Component} The component to be rendered.
*/
function PostTypeSupportCheck( { children, supportKeys } ) {
const postType = useSelect( ( select ) => {
diff --git a/packages/editor/src/components/unsaved-changes-warning/index.js b/packages/editor/src/components/unsaved-changes-warning/index.js
index b5c78644082133..49e2b7edf1f293 100644
--- a/packages/editor/src/components/unsaved-changes-warning/index.js
+++ b/packages/editor/src/components/unsaved-changes-warning/index.js
@@ -10,7 +10,7 @@ import { store as coreStore } from '@wordpress/core-data';
* Warns the user if there are unsaved changes before leaving the editor.
* Compatible with Post Editor and Site Editor.
*
- * @return {WPComponent} The component.
+ * @return {Component} The component.
*/
export default function UnsavedChangesWarning() {
const { __experimentalGetDirtyEntityRecords } = useSelect( coreStore );
diff --git a/packages/element/CHANGELOG.md b/packages/element/CHANGELOG.md
index 7b3e83b2572c96..30ff172259d2e3 100644
--- a/packages/element/CHANGELOG.md
+++ b/packages/element/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## Breaking Changes
+
+- Remove the WPElement, WPComponent, and WPSyntheticEvent types.
+
## 5.19.0 (2023-09-20)
## 5.18.0 (2023-08-31)
diff --git a/packages/element/README.md b/packages/element/README.md
index 879aefb5a46f48..96aeeea37a462e 100755
--- a/packages/element/README.md
+++ b/packages/element/README.md
@@ -1,17 +1,6 @@
# Element
-Element is, quite simply, an abstraction layer atop [React](https://reactjs.org/).
-
-You may find yourself asking, "Why an abstraction layer?". For a few reasons:
-
-- In many applications, especially those extended by a rich plugin ecosystem as is the case with WordPress, it's wise to create interfaces to underlying third-party code. The thinking is that if ever a need arises to change or even replace the underlying implementation, it can be done without catastrophic rippling effects to dependent code, so long as the interface stays the same.
-- It provides a mechanism to shield implementers by omitting features with uncertain futures (`createClass`, `PropTypes`).
-- It helps avoid incompatibilities between versions by ensuring that every plugin operates on a single centralized version of the code.
-
-On the `wp.element` global object, you will find the following, ordered roughly by the likelihood you'll encounter it in your code:
-
-- [`createElement`](https://reactjs.org/docs/react-api.html#createelement)
-- [`render`](https://reactjs.org/docs/react-dom.html#render)
+Element is a package that builds on top of [React](https://reactjs.org/) and provide a set of utilities to work with React components and React elements.
## Installation
@@ -23,39 +12,6 @@ npm install @wordpress/element --save
_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._
-## Usage
-
-Let's render a customized greeting into an empty element.
-
-**Note:** `createRoot` was introduced with React 18, which is bundled with WordPress 6.2. Therefore it may be necessary to mount your component depending on which version of WordPress (and therefore React) you are currently using. This is possible by checking for an undefined import and falling back to the React 17 method of mounting an app using `render`.
-
-Assuming the following root element is present in the page:
-
-```html
-
-```
-
-We can mount our app:
-
-```js
-import { createRoot, render, createElement } from '@wordpress/element';
-
-function Greeting( props ) {
- return createElement( 'span', null, 'Hello ' + props.toWhom + '!' );
-}
-
-const domElement = document.getElementById( 'greeting' );
-const uiElement = createElement( Greeting, { toWhom: 'World' } );
-
-if ( createRoot ) {
- createRoot( domElement ).render( uiElement );
-} else {
- render( uiElement, domElement );
-}
-```
-
-Refer to the [official React Quick Start guide](https://reactjs.org/docs/hello-world.html) for a more thorough walkthrough, in most cases substituting `React` and `ReactDOM` with `wp.element` in code examples.
-
## Why React?
At the risk of igniting debate surrounding any single "best" front-end framework, the choice to use any tool should be motivated specifically to serve the requirements of the system. In modeling the concept of a [block](https://github.com/WordPress/gutenberg/tree/HEAD/packages/blocks/README.md), we observe the following technical requirements:
@@ -67,27 +23,6 @@ At its most basic, React provides a simple input / output mechanism. **Given a s
The offerings of any framework necessarily become more complex as these requirements increase; many front-end frameworks prescribe ideas around page routing, retrieving and updating data, and managing layout. React is not immune to this, but the introduced complexity is rarely caused by React itself, but instead managing an arrangement of supporting tools. By moving these concerns out of sight to the internals of the system (WordPress core code), we can minimize the responsibilities of plugin authors to a small, clear set of touch points.
-## JSX
-
-While not at all a requirement to use React, [JSX](https://reactjs.org/docs/introducing-jsx.html) is a recommended syntax extension to compose elements more expressively. Through a build process, JSX is converted back to the `createElement` syntax you see earlier in this document.
-
-If you've configured [Babel](http://babeljs.io/) for your project, you can opt in to JSX syntax by specifying the `pragma` option of the [`transform-react-jsx` plugin](https://www.npmjs.com/package/babel-plugin-transform-react-jsx) in your [`.babelrc` configuration](http://babeljs.io/docs/usage/babelrc/).
-
-```json
-{
- "plugins": [
- [
- "transform-react-jsx",
- {
- "pragma": "createElement"
- }
- ]
- ]
-}
-```
-
-This assumes that you will import the `createElement` function in any file where you use JSX. Alternatively, consider using the [`@wordpress/babel-plugin-import-jsx-pragma` Babel plugin](https://www.npmjs.com/package/@wordpress/babel-plugin-import-jsx-pragma) to automate the import of this function.
-
## API
@@ -102,12 +37,12 @@ Creates a copy of an element with extended props.
_Parameters_
-- _element_ `WPElement`: Element
+- _element_ `Element`: Element
- _props_ `?Object`: Props to apply to cloned element
_Returns_
-- `WPElement`: Cloned element.
+- `Element`: Cloned element.
### Component
@@ -145,11 +80,11 @@ _Parameters_
- _type_ `?(string|Function)`: Tag name or element creator
- _props_ `Object`: Element properties, either attribute set to apply to DOM node or values to pass through to element creator
-- _children_ `...WPElement`: Descendant elements
+- _children_ `...Element`: Descendant elements
_Returns_
-- `WPElement`: Element.
+- `Element`: Element.
### createInterpolateElement
@@ -175,11 +110,11 @@ You would have something like this as the conversionMap value:
_Parameters_
- _interpolatedString_ `string`: The interpolation string to be parsed.
-- _conversionMap_ `Record`: The map used to convert the string to a react element.
+- _conversionMap_ `Record`: The map used to convert the string to a react element.
_Returns_
-- `WPElement`: A wp element.
+- `Element`: A wp element.
### createPortal
@@ -191,7 +126,7 @@ _Related_
_Parameters_
-- _child_ `import('./react').WPElement`: Any renderable child, such as an element, string, or fragment.
+- _child_ `import('react').ReactElement`: Any renderable child, such as an element, string, or fragment.
- _container_ `HTMLElement`: DOM node into which element should be rendered.
### createRef
@@ -220,7 +155,7 @@ Finds the dom node of a React component.
_Parameters_
-- _component_ `import('./react').WPComponent`: Component's instance.
+- _component_ `import('react').ComponentType`: Component's instance.
### flushSync
@@ -240,7 +175,7 @@ _Parameters_
_Returns_
-- `WPComponent`: Enhanced component.
+- `Component`: Enhanced component.
### Fragment
@@ -282,7 +217,7 @@ _Returns_
### isValidElement
-Checks if an object is a valid WPElement.
+Checks if an object is a valid React Element.
_Parameters_
@@ -290,7 +225,7 @@ _Parameters_
_Returns_
-- `boolean`: true if objectToTest is a valid WPElement and false otherwise.
+- `boolean`: true if objectToTest is a valid React Element and false otherwise.
### lazy
diff --git a/packages/element/src/create-interpolate-element.js b/packages/element/src/create-interpolate-element.js
index 761a01079f5a14..88f6254dad7d6e 100644
--- a/packages/element/src/create-interpolate-element.js
+++ b/packages/element/src/create-interpolate-element.js
@@ -3,7 +3,11 @@
*/
import { createElement, cloneElement, Fragment, isValidElement } from './react';
-/** @typedef {import('./react').WPElement} WPElement */
+/**
+ * Object containing a React element.
+ *
+ * @typedef {import('react').ReactElement} Element
+ */
let indoc, offset, output, stack;
@@ -29,17 +33,17 @@ const tokenizer = /<(\/)?(\w+)\s*(\/)?>/g;
*
* @typedef Frame
*
- * @property {WPElement} element A parent element which may still have
- * @property {number} tokenStart Offset at which parent element first
- * appears.
- * @property {number} tokenLength Length of string marking start of parent
- * element.
- * @property {number} [prevOffset] Running offset at which parsing should
- * continue.
- * @property {number} [leadingTextStart] Offset at which last closing element
- * finished, used for finding text between
- * elements.
- * @property {WPElement[]} children Children.
+ * @property {Element} element A parent element which may still have
+ * @property {number} tokenStart Offset at which parent element first
+ * appears.
+ * @property {number} tokenLength Length of string marking start of parent
+ * element.
+ * @property {number} [prevOffset] Running offset at which parsing should
+ * continue.
+ * @property {number} [leadingTextStart] Offset at which last closing element
+ * finished, used for finding text between
+ * elements.
+ * @property {Element[]} children Children.
*/
/**
@@ -49,17 +53,17 @@ const tokenizer = /<(\/)?(\w+)\s*(\/)?>/g;
* parsed.
*
* @private
- * @param {WPElement} element A parent element which may still have
- * nested children not yet parsed.
- * @param {number} tokenStart Offset at which parent element first
- * appears.
- * @param {number} tokenLength Length of string marking start of parent
- * element.
- * @param {number} [prevOffset] Running offset at which parsing should
- * continue.
- * @param {number} [leadingTextStart] Offset at which last closing element
- * finished, used for finding text between
- * elements.
+ * @param {Element} element A parent element which may still have
+ * nested children not yet parsed.
+ * @param {number} tokenStart Offset at which parent element first
+ * appears.
+ * @param {number} tokenLength Length of string marking start of parent
+ * element.
+ * @param {number} [prevOffset] Running offset at which parsing should
+ * continue.
+ * @param {number} [leadingTextStart] Offset at which last closing element
+ * finished, used for finding text between
+ * elements.
*
* @return {Frame} The stack frame tracking parse progress.
*/
@@ -101,11 +105,11 @@ function createFrame(
* }
* ```
*
- * @param {string} interpolatedString The interpolation string to be parsed.
- * @param {Record} conversionMap The map used to convert the string to
- * a react element.
+ * @param {string} interpolatedString The interpolation string to be parsed.
+ * @param {Record} conversionMap The map used to convert the string to
+ * a react element.
* @throws {TypeError}
- * @return {WPElement} A wp element.
+ * @return {Element} A wp element.
*/
const createInterpolateElement = ( interpolatedString, conversionMap ) => {
indoc = interpolatedString;
@@ -116,7 +120,7 @@ const createInterpolateElement = ( interpolatedString, conversionMap ) => {
if ( ! isValidConversionMap( conversionMap ) ) {
throw new TypeError(
- 'The conversionMap provided is not valid. It must be an object with values that are WPElements'
+ 'The conversionMap provided is not valid. It must be an object with values that are React Elements'
);
}
@@ -130,7 +134,7 @@ const createInterpolateElement = ( interpolatedString, conversionMap ) => {
* Validate conversion map.
*
* A map is considered valid if it's an object and every value in the object
- * is a WPElement
+ * is a React Element
*
* @private
*
diff --git a/packages/element/src/react-platform.js b/packages/element/src/react-platform.js
index bd444b2828bef7..862b02706876d2 100644
--- a/packages/element/src/react-platform.js
+++ b/packages/element/src/react-platform.js
@@ -16,16 +16,16 @@ import { createRoot, hydrateRoot } from 'react-dom/client';
*
* @see https://github.com/facebook/react/issues/10309#issuecomment-318433235
*
- * @param {import('./react').WPElement} child Any renderable child, such as an element,
- * string, or fragment.
- * @param {HTMLElement} container DOM node into which element should be rendered.
+ * @param {import('react').ReactElement} child Any renderable child, such as an element,
+ * string, or fragment.
+ * @param {HTMLElement} container DOM node into which element should be rendered.
*/
export { createPortal };
/**
* Finds the dom node of a React component.
*
- * @param {import('./react').WPComponent} component Component's instance.
+ * @param {import('react').ComponentType} component Component's instance.
*/
export { findDOMNode };
diff --git a/packages/element/src/react.js b/packages/element/src/react.js
index 1eb49ac6742c2d..6882f9ad3344a5 100644
--- a/packages/element/src/react.js
+++ b/packages/element/src/react.js
@@ -37,19 +37,19 @@ import {
/**
* Object containing a React element.
*
- * @typedef {import('react').ReactElement} WPElement
+ * @typedef {import('react').ReactElement} Element
*/
/**
* Object containing a React component.
*
- * @typedef {import('react').ComponentType} WPComponent
+ * @typedef {import('react').ComponentType} ComponentType
*/
/**
* Object containing a React synthetic event.
*
- * @typedef {import('react').SyntheticEvent} WPSyntheticEvent
+ * @typedef {import('react').SyntheticEvent} SyntheticEvent
*/
/**
@@ -67,10 +67,10 @@ export { Children };
/**
* Creates a copy of an element with extended props.
*
- * @param {WPElement} element Element
- * @param {?Object} props Props to apply to cloned element
+ * @param {Element} element Element
+ * @param {?Object} props Props to apply to cloned element
*
- * @return {WPElement} Cloned element.
+ * @return {Element} Cloned element.
*/
export { cloneElement };
@@ -96,9 +96,9 @@ export { createContext };
* @param {Object} props Element properties, either attribute
* set to apply to DOM node or values to
* pass through to element creator
- * @param {...WPElement} children Descendant elements
+ * @param {...Element} children Descendant elements
*
- * @return {WPElement} Element.
+ * @return {Element} Element.
*/
export { createElement };
@@ -120,7 +120,7 @@ export { createRef };
* @param {Function} forwarder Function passed `props` and `ref`, expected to
* return an element.
*
- * @return {WPComponent} Enhanced component.
+ * @return {Component} Enhanced component.
*/
export { forwardRef };
@@ -130,11 +130,11 @@ export { forwardRef };
export { Fragment };
/**
- * Checks if an object is a valid WPElement.
+ * Checks if an object is a valid React Element.
*
* @param {Object} objectToCheck The object to be checked.
*
- * @return {boolean} true if objectToTest is a valid WPElement and false otherwise.
+ * @return {boolean} true if objectToTest is a valid React Element and false otherwise.
*/
export { isValidElement };
diff --git a/packages/element/src/serialize.js b/packages/element/src/serialize.js
index d18a6f00fa941f..e047ccad432dc8 100644
--- a/packages/element/src/serialize.js
+++ b/packages/element/src/serialize.js
@@ -46,7 +46,7 @@ import {
import { createContext, Fragment, StrictMode, forwardRef } from './react';
import RawHTML from './raw-html';
-/** @typedef {import('./react').WPElement} WPElement */
+/** @typedef {import('react').ReactElement} ReactElement */
const { Provider, Consumer } = createContext( undefined );
const ForwardRef = forwardRef( () => {
@@ -672,15 +672,15 @@ export function renderNativeComponent(
return '<' + type + attributes + '>' + content + '' + type + '>';
}
-/** @typedef {import('./react').WPComponent} WPComponent */
+/** @typedef {import('react').ComponentType} ComponentType */
/**
* Serializes a non-native component type to string.
*
- * @param {WPComponent} Component Component type to serialize.
- * @param {Object} props Props object.
- * @param {Object} [context] Context object.
- * @param {Object} [legacyContext] Legacy context object.
+ * @param {ComponentType} Component Component type to serialize.
+ * @param {Object} props Props object.
+ * @param {Object} [context] Context object.
+ * @param {Object} [legacyContext] Legacy context object.
*
* @return {string} Serialized element
*/
diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md
index 585337a068c722..97a29a87ef2046 100644
--- a/packages/eslint-plugin/CHANGELOG.md
+++ b/packages/eslint-plugin/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Breaking Changes
+
+- Change the required major version of Prettier from v2 to v3 ([#54775](https://github.com/WordPress/gutenberg/pull/54775)).
+
## 16.0.0 (2023-09-20)
### Breaking Changes
diff --git a/packages/eslint-plugin/configs/jsdoc.js b/packages/eslint-plugin/configs/jsdoc.js
index 4aaed834363996..62effdb709b39e 100644
--- a/packages/eslint-plugin/configs/jsdoc.js
+++ b/packages/eslint-plugin/configs/jsdoc.js
@@ -21,9 +21,11 @@ const temporaryWordPressInternalTypes = [
'WPBlockTypeIcon',
'WPBlockTypeIconRender',
'WPBlockTypeIconDescriptor',
- 'WPComponent',
- 'WPElement',
'WPIcon',
+
+ // These two should be removed once we use the TS types from "react".
+ 'Component',
+ 'Element',
];
/**
diff --git a/packages/eslint-plugin/docs/rules/dependency-group.md b/packages/eslint-plugin/docs/rules/dependency-group.md
index e3910544aedb71..8d0b53996ce22b 100644
--- a/packages/eslint-plugin/docs/rules/dependency-group.md
+++ b/packages/eslint-plugin/docs/rules/dependency-group.md
@@ -12,7 +12,7 @@ Examples of **incorrect** code for this rule:
```js
import { camelCase } from 'change-case';
-import { Component } from '@wordpress/element';
+import { Component } from 'react';
import edit from './edit';
```
@@ -27,7 +27,7 @@ import { camelCase } from 'change-case';
/*
* WordPress dependencies
*/
-import { Component } from '@wordpress/element';
+import { Component } from 'react';
/*
* Internal dependencies
diff --git a/packages/eslint-plugin/docs/rules/react-no-unsafe-timeout.md b/packages/eslint-plugin/docs/rules/react-no-unsafe-timeout.md
index 87dc07bc9e62a2..8e203a2c82ba6d 100644
--- a/packages/eslint-plugin/docs/rules/react-no-unsafe-timeout.md
+++ b/packages/eslint-plugin/docs/rules/react-no-unsafe-timeout.md
@@ -19,7 +19,7 @@ class MyComponent extends Component {
}
}
-class MyComponent extends wp.element.Component {
+class MyComponent extends React.Component {
componentDidMount() {
setTimeout( fn );
}
@@ -48,7 +48,7 @@ class MyNotComponent {
}
}
-class MyComponent extends wp.element.Component {
+class MyComponent extends React.Component {
componentDidMount() {
const { setTimeout } = this.props;
setTimeout( fn );
diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json
index 2805c9a1e18d28..771b918298df28 100644
--- a/packages/eslint-plugin/package.json
+++ b/packages/eslint-plugin/package.json
@@ -52,7 +52,7 @@
"peerDependencies": {
"@babel/core": ">=7",
"eslint": ">=8",
- "prettier": ">=2",
+ "prettier": ">=3",
"typescript": ">=4"
},
"peerDependenciesMeta": {
diff --git a/packages/format-library/src/link/index.js b/packages/format-library/src/link/index.js
index 50019be4cc958e..1596da055cc402 100644
--- a/packages/format-library/src/link/index.js
+++ b/packages/format-library/src/link/index.js
@@ -130,6 +130,7 @@ export const link = {
url: 'href',
type: 'data-type',
id: 'data-id',
+ _id: 'id',
target: 'target',
rel: 'rel',
},
diff --git a/packages/format-library/src/text-color/inline.js b/packages/format-library/src/text-color/inline.js
index 7a2c2f92ea462c..f71c7766222582 100644
--- a/packages/format-library/src/text-color/inline.js
+++ b/packages/format-library/src/text-color/inline.js
@@ -137,6 +137,11 @@ export default function InlineColorUI( {
onClose,
contentRef,
} ) {
+ const popoverAnchor = useAnchor( {
+ editableContentElement: contentRef.current,
+ settings,
+ } );
+
/*
As you change the text color by typing a HEX value into a field,
the return value of document.getSelection jumps to the field you're editing,
@@ -144,12 +149,8 @@ export default function InlineColorUI( {
it will return null, since it can't find the element within the HEX input.
This caches the last truthy value of the selection anchor reference.
*/
- const popoverAnchor = useCachedTruthy(
- useAnchor( {
- editableContentElement: contentRef.current,
- settings,
- } )
- );
+ const cachedRect = useCachedTruthy( popoverAnchor.getBoundingClientRect() );
+ popoverAnchor.getBoundingClientRect = () => cachedRect;
return (
new Set() );
function onKeyDown( event ) {
if ( props.onKeyDown ) props.onKeyDown( event );
- for ( const keyboardShortcut of keyboardShortcuts.current ) {
+ for ( const keyboardShortcut of keyboardShortcuts ) {
keyboardShortcut( event );
}
}
diff --git a/packages/keyboard-shortcuts/src/store/actions.js b/packages/keyboard-shortcuts/src/store/actions.js
index d45a23a09f21da..2b77b6c1e1fe45 100644
--- a/packages/keyboard-shortcuts/src/store/actions.js
+++ b/packages/keyboard-shortcuts/src/store/actions.js
@@ -29,9 +29,9 @@
* @example
*
*```js
+ * import { useEffect } from 'react';
* import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
* import { useSelect, useDispatch } from '@wordpress/data';
- * import { useEffect } from '@wordpress/element';
* import { __ } from '@wordpress/i18n';
*
* const ExampleComponent = () => {
@@ -91,9 +91,9 @@ export function registerShortcut( {
* @example
*
*```js
+ * import { useEffect } from 'react';
* import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
* import { useSelect, useDispatch } from '@wordpress/data';
- * import { useEffect } from '@wordpress/element';
* import { __ } from '@wordpress/i18n';
*
* const ExampleComponent = () => {
diff --git a/packages/list-reusable-blocks/src/components/import-form/index.js b/packages/list-reusable-blocks/src/components/import-form/index.js
index 9ba1589e52f39b..1f5e62f9115b36 100644
--- a/packages/list-reusable-blocks/src/components/import-form/index.js
+++ b/packages/list-reusable-blocks/src/components/import-form/index.js
@@ -49,8 +49,8 @@ function ImportForm( { instanceId, onUpload } ) {
case 'Invalid JSON file':
uiMessage = __( 'Invalid JSON file' );
break;
- case 'Invalid Pattern JSON file':
- uiMessage = __( 'Invalid Pattern JSON file' );
+ case 'Invalid pattern JSON file':
+ uiMessage = __( 'Invalid pattern JSON file' );
break;
default:
uiMessage = __( 'Unknown error' );
diff --git a/packages/list-reusable-blocks/src/utils/import.js b/packages/list-reusable-blocks/src/utils/import.js
index 465fb080ce8dfe..432948ce6d6dbc 100644
--- a/packages/list-reusable-blocks/src/utils/import.js
+++ b/packages/list-reusable-blocks/src/utils/import.js
@@ -31,7 +31,7 @@ async function importReusableBlock( file ) {
( parsedContent.syncStatus &&
typeof parsedContent.syncStatus !== 'string' )
) {
- throw new Error( 'Invalid Pattern JSON file' );
+ throw new Error( 'Invalid pattern JSON file' );
}
const postType = await apiFetch( { path: `/wp/v2/types/wp_block` } );
const reusableBlock = await apiFetch( {
diff --git a/packages/notices/src/store/actions.js b/packages/notices/src/store/actions.js
index 11c38a9c931ae4..445eed10650b61 100644
--- a/packages/notices/src/store/actions.js
+++ b/packages/notices/src/store/actions.js
@@ -81,7 +81,7 @@ export function createNotice( status = DEFAULT_STATUS, content, options = {} ) {
} = options;
// The supported value shape of content is currently limited to plain text
- // strings. To avoid setting expectation that e.g. a WPElement could be
+ // strings. To avoid setting expectation that e.g. a React Element could be
// supported, cast to a string.
content = String( content );
diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js
index c9305806c7e13f..397d851d3886b9 100644
--- a/packages/patterns/src/components/category-selector.js
+++ b/packages/patterns/src/components/category-selector.js
@@ -4,7 +4,7 @@
import { __ } from '@wordpress/i18n';
import { useMemo, useState } from '@wordpress/element';
import { FormTokenField } from '@wordpress/components';
-import { useSelect, useDispatch } from '@wordpress/data';
+import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { useDebounce } from '@wordpress/compose';
import { decodeEntities } from '@wordpress/html-entities';
@@ -13,13 +13,6 @@ const unescapeString = ( arg ) => {
return decodeEntities( arg );
};
-const unescapeTerm = ( term ) => {
- return {
- ...term,
- name: unescapeString( term.name ),
- };
-};
-
const EMPTY_ARRAY = [];
const MAX_TERMS_SUGGESTIONS = 20;
const DEFAULT_QUERY = {
@@ -27,13 +20,11 @@ const DEFAULT_QUERY = {
_fields: 'id,name',
context: 'view',
};
-const slug = 'wp_pattern_category';
+export const CATEGORY_SLUG = 'wp_pattern_category';
-export default function CategorySelector( { onCategorySelection } ) {
- const [ values, setValues ] = useState( [] );
+export default function CategorySelector( { values, onChange } ) {
const [ search, setSearch ] = useState( '' );
const debouncedSearch = useDebounce( setSearch, 500 );
- const { invalidateResolution } = useDispatch( coreStore );
const { searchResults } = useSelect(
( select ) => {
@@ -41,7 +32,7 @@ export default function CategorySelector( { onCategorySelection } ) {
return {
searchResults: !! search
- ? getEntityRecords( 'taxonomy', slug, {
+ ? getEntityRecords( 'taxonomy', CATEGORY_SLUG, {
...DEFAULT_QUERY,
search,
} )
@@ -57,28 +48,7 @@ export default function CategorySelector( { onCategorySelection } ) {
);
}, [ searchResults ] );
- const { saveEntityRecord } = useDispatch( coreStore );
-
- async function findOrCreateTerm( term ) {
- try {
- const newTerm = await saveEntityRecord( 'taxonomy', slug, term, {
- throwOnError: true,
- } );
- invalidateResolution( 'getUserPatternCategories' );
- return unescapeTerm( newTerm );
- } catch ( error ) {
- if ( error.code !== 'term_exists' ) {
- throw error;
- }
-
- return {
- id: error.data.term_id,
- name: term.name,
- };
- }
- }
-
- function onChange( termNames ) {
+ function handleChange( termNames ) {
const uniqueTerms = termNames.reduce( ( terms, newTerm ) => {
if (
! terms.some(
@@ -90,15 +60,7 @@ export default function CategorySelector( { onCategorySelection } ) {
return terms;
}, [] );
- setValues( uniqueTerms );
-
- Promise.all(
- uniqueTerms.map( ( termName ) =>
- findOrCreateTerm( { name: termName } )
- )
- ).then( ( newTerms ) => {
- onCategorySelection( newTerms );
- } );
+ onChange( uniqueTerms );
}
return (
@@ -107,7 +69,7 @@ export default function CategorySelector( { onCategorySelection } ) {
className="patterns-menu-items__convert-modal-categories"
value={ values }
suggestions={ suggestions }
- onChange={ onChange }
+ onChange={ handleChange }
onInputChange={ debouncedSearch }
maxSuggestions={ MAX_TERMS_SUGGESTIONS }
label={ __( 'Categories' ) }
diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js
index 189004b6a046b2..531936da5e5c28 100644
--- a/packages/patterns/src/components/create-pattern-modal.js
+++ b/packages/patterns/src/components/create-pattern-modal.js
@@ -13,6 +13,7 @@ import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
+import { store as coreStore } from '@wordpress/core-data';
/**
* Internal dependencies
@@ -23,7 +24,7 @@ import { PATTERN_DEFAULT_CATEGORY, PATTERN_SYNC_TYPES } from '../constants';
* Internal dependencies
*/
import { store as patternsStore } from '../store';
-import CategorySelector from './category-selector';
+import CategorySelector, { CATEGORY_SLUG } from './category-selector';
import { unlock } from '../lock-unlock';
export default function CreatePatternModal( {
@@ -34,13 +35,26 @@ export default function CreatePatternModal( {
className = 'patterns-menu-items__convert-modal',
} ) {
const [ syncType, setSyncType ] = useState( PATTERN_SYNC_TYPES.full );
- const [ categories, setCategories ] = useState( [] );
+ const [ categoryTerms, setCategoryTerms ] = useState( [] );
const [ title, setTitle ] = useState( '' );
+ const [ isSaving, setIsSaving ] = useState( false );
const { createPattern } = unlock( useDispatch( patternsStore ) );
-
+ const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore );
const { createErrorNotice } = useDispatch( noticesStore );
+
async function onCreate( patternTitle, sync ) {
+ if ( ! title || isSaving ) {
+ return;
+ }
+
try {
+ setIsSaving( true );
+ const categories = await Promise.all(
+ categoryTerms.map( ( termName ) =>
+ findOrCreateTerm( termName )
+ )
+ );
+
const newPattern = await createPattern(
patternTitle,
sync,
@@ -57,12 +71,35 @@ export default function CreatePatternModal( {
id: 'convert-to-pattern-error',
} );
onError();
+ } finally {
+ setIsSaving( false );
+ setCategoryTerms( [] );
+ setTitle( '' );
}
}
- const handleCategorySelection = ( selectedCategories ) => {
- setCategories( selectedCategories.map( ( cat ) => cat.id ) );
- };
+ /**
+ * @param {string} term
+ * @return {Promise} The pattern category id.
+ */
+ async function findOrCreateTerm( term ) {
+ try {
+ const newTerm = await saveEntityRecord(
+ 'taxonomy',
+ CATEGORY_SLUG,
+ { name: term },
+ { throwOnError: true }
+ );
+ invalidateResolution( 'getUserPatternCategories' );
+ return newTerm.id;
+ } catch ( error ) {
+ if ( error.code !== 'term_exists' ) {
+ throw error;
+ }
+
+ return error.data.term_id;
+ }
+ }
return (
{
event.preventDefault();
onCreate( title, syncType );
- setTitle( '' );
} }
>
@@ -90,7 +126,8 @@ export default function CreatePatternModal( {
className="patterns-create-modal__name-input"
/>
-
+
{ __( 'Create' ) }
diff --git a/packages/patterns/src/components/pattern-convert-button.js b/packages/patterns/src/components/pattern-convert-button.js
index 84340091338714..e110be22938df9 100644
--- a/packages/patterns/src/components/pattern-convert-button.js
+++ b/packages/patterns/src/components/pattern-convert-button.js
@@ -21,6 +21,7 @@ import { store as noticesStore } from '@wordpress/notices';
import { store as patternsStore } from '../store';
import CreatePatternModal from './create-pattern-modal';
import { unlock } from '../lock-unlock';
+import { PATTERN_SYNC_TYPES } from '../constants';
/**
* Menu control to convert block(s) to a pattern block.
@@ -28,7 +29,7 @@ import { unlock } from '../lock-unlock';
* @param {Object} props Component props.
* @param {string[]} props.clientIds Client ids of selected blocks.
* @param {string} props.rootClientId ID of the currently selected top-level block.
- * @return {import('@wordpress/element').WPComponent} The menu control or null.
+ * @return {import('react').ComponentType} The menu control or null.
*/
export default function PatternConvertButton( { clientIds, rootClientId } ) {
const { createSuccessNotice } = useDispatch( noticesStore );
@@ -96,23 +97,25 @@ export default function PatternConvertButton( { clientIds, rootClientId } ) {
}
const handleSuccess = ( { pattern } ) => {
- const newBlock = createBlock( 'core/block', {
- ref: pattern.id,
- } );
+ if ( pattern.wp_pattern_sync_status !== PATTERN_SYNC_TYPES.unsynced ) {
+ const newBlock = createBlock( 'core/block', {
+ ref: pattern.id,
+ } );
- replaceBlocks( clientIds, newBlock );
- setEditingPattern( newBlock.clientId, true );
+ replaceBlocks( clientIds, newBlock );
+ setEditingPattern( newBlock.clientId, true );
+ }
createSuccessNotice(
- pattern.wp_pattern_sync_status === 'unsynced'
+ pattern.wp_pattern_sync_status === PATTERN_SYNC_TYPES.unsynced
? sprintf(
// translators: %s: the name the user has given to the pattern.
- __( 'Unsynced Pattern created: %s' ),
+ __( 'Unsynced pattern created: %s' ),
pattern.title.raw
)
: sprintf(
// translators: %s: the name the user has given to the pattern.
- __( 'Synced Pattern created: %s' ),
+ __( 'Synced pattern created: %s' ),
pattern.title.raw
),
{
diff --git a/packages/patterns/src/constants.js b/packages/patterns/src/constants.js
index ecf7c7ddb3740c..005313ea1d3788 100644
--- a/packages/patterns/src/constants.js
+++ b/packages/patterns/src/constants.js
@@ -4,6 +4,7 @@ export const PATTERN_TYPES = {
};
export const PATTERN_DEFAULT_CATEGORY = 'all-patterns';
+export const PATTERN_USER_CATEGORY = 'my-patterns';
export const PATTERN_CORE_SOURCES = [
'core',
'pattern-directory/core',
diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js
index 306476efc31212..cdbf1a6ba29408 100644
--- a/packages/patterns/src/private-apis.js
+++ b/packages/patterns/src/private-apis.js
@@ -7,6 +7,7 @@ import PatternsMenuItems from './components';
import {
PATTERN_TYPES,
PATTERN_DEFAULT_CATEGORY,
+ PATTERN_USER_CATEGORY,
PATTERN_CORE_SOURCES,
PATTERN_SYNC_TYPES,
} from './constants';
@@ -17,6 +18,7 @@ lock( privateApis, {
PatternsMenuItems,
PATTERN_TYPES,
PATTERN_DEFAULT_CATEGORY,
+ PATTERN_USER_CATEGORY,
PATTERN_CORE_SOURCES,
PATTERN_SYNC_TYPES,
} );
diff --git a/packages/patterns/src/store/actions.js b/packages/patterns/src/store/actions.js
index 2861e4ce2dac06..dfa0326a709416 100644
--- a/packages/patterns/src/store/actions.js
+++ b/packages/patterns/src/store/actions.js
@@ -69,7 +69,7 @@ export const createPatternFromFile =
( parsedContent.syncStatus &&
typeof parsedContent.syncStatus !== 'string' )
) {
- throw new Error( 'Invalid Pattern JSON file' );
+ throw new Error( 'Invalid pattern JSON file' );
}
const pattern = await dispatch.createPattern(
diff --git a/packages/plugins/README.md b/packages/plugins/README.md
index a0e8441513e5d7..e9e5a704aeca71 100644
--- a/packages/plugins/README.md
+++ b/packages/plugins/README.md
@@ -48,7 +48,7 @@ _Usage_
```js
// Using ES5 syntax
-var el = wp.element.createElement;
+var el = React.createElement;
var PluginArea = wp.plugins.PluginArea;
function Layout() {
@@ -76,7 +76,7 @@ _Parameters_
_Returns_
-- `WPComponent`: The component to be rendered.
+- `Component`: The component to be rendered.
#### registerPlugin
@@ -86,12 +86,12 @@ _Usage_
```js
// Using ES5 syntax
-var el = wp.element.createElement;
+var el = React.createElement;
var Fragment = wp.element.Fragment;
var PluginSidebar = wp.editPost.PluginSidebar;
var PluginSidebarMoreMenuItem = wp.editPost.PluginSidebarMoreMenuItem;
var registerPlugin = wp.plugins.registerPlugin;
-var moreIcon = wp.element.createElement( 'svg' ); //... svg element.
+var moreIcon = React.createElement( 'svg' ); //... svg element.
function Component() {
return el(
@@ -200,7 +200,7 @@ _Parameters_
_Returns_
-- `WPComponent`: Enhanced component with injected context as props.
+- `Component`: Enhanced component with injected context as props.
diff --git a/packages/plugins/src/api/index.ts b/packages/plugins/src/api/index.ts
index a5217e0fe2bdc6..45c8f931634198 100644
--- a/packages/plugins/src/api/index.ts
+++ b/packages/plugins/src/api/index.ts
@@ -1,4 +1,8 @@
/* eslint no-console: [ 'error', { allow: [ 'error' ] } ] */
+/**
+ * External dependencies
+ */
+import type { ComponentType } from 'react';
/**
* WordPress dependencies
@@ -6,7 +10,6 @@
import { applyFilters, doAction } from '@wordpress/hooks';
import { plugins as pluginsIcon } from '@wordpress/icons';
import type { IconType } from '@wordpress/components';
-import type { WPComponent } from '@wordpress/element';
/**
* Defined behavior of a plugin type.
@@ -27,7 +30,7 @@ export interface WPPlugin {
/**
* A component containing the UI elements to be rendered.
*/
- render: WPComponent;
+ render: ComponentType;
/**
* The optional scope to be used when rendering inside a plugin area.
@@ -53,12 +56,12 @@ const plugins = {} as Record< string, WPPlugin >;
* @example
* ```js
* // Using ES5 syntax
- * var el = wp.element.createElement;
+ * var el = React.createElement;
* var Fragment = wp.element.Fragment;
* var PluginSidebar = wp.editPost.PluginSidebar;
* var PluginSidebarMoreMenuItem = wp.editPost.PluginSidebarMoreMenuItem;
* var registerPlugin = wp.plugins.registerPlugin;
- * var moreIcon = wp.element.createElement( 'svg' ); //... svg element.
+ * var moreIcon = React.createElement( 'svg' ); //... svg element.
*
* function Component() {
* return el(
diff --git a/packages/plugins/src/components/plugin-area/index.tsx b/packages/plugins/src/components/plugin-area/index.tsx
index 9410c9cef4e5ff..231a519b030d01 100644
--- a/packages/plugins/src/components/plugin-area/index.tsx
+++ b/packages/plugins/src/components/plugin-area/index.tsx
@@ -35,7 +35,7 @@ const getPluginContext = memoize(
* @example
* ```js
* // Using ES5 syntax
- * var el = wp.element.createElement;
+ * var el = React.createElement;
* var PluginArea = wp.plugins.PluginArea;
*
* function Layout() {
@@ -61,7 +61,7 @@ const getPluginContext = memoize(
* );
* ```
*
- * @return {WPComponent} The component to be rendered.
+ * @return {Component} The component to be rendered.
*/
function PluginArea( {
scope,
diff --git a/packages/plugins/src/components/plugin-context/index.tsx b/packages/plugins/src/components/plugin-context/index.tsx
index 76fbdabe048293..c2e6f8eaa232d8 100644
--- a/packages/plugins/src/components/plugin-context/index.tsx
+++ b/packages/plugins/src/components/plugin-context/index.tsx
@@ -38,7 +38,7 @@ export function usePluginContext() {
* expected to return object of props to
* merge with the component's own props.
*
- * @return {WPComponent} Enhanced component with injected context as props.
+ * @return {Component} Enhanced component with injected context as props.
*/
export const withPluginContext = (
mapContextToProps: < T >(
diff --git a/packages/plugins/src/components/test/plugin-area.js b/packages/plugins/src/components/test/plugin-area.js
index 42ea556081ae2a..b566b487958870 100644
--- a/packages/plugins/src/components/test/plugin-area.js
+++ b/packages/plugins/src/components/test/plugin-area.js
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+// eslint-disable-next-line testing-library/no-manual-cleanup
import { act, render, cleanup } from '@testing-library/react';
/**
diff --git a/packages/prettier-config/CHANGELOG.md b/packages/prettier-config/CHANGELOG.md
index 8ed832c883a7ad..09d60e39ee72b9 100644
--- a/packages/prettier-config/CHANGELOG.md
+++ b/packages/prettier-config/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Breaking Change
+
+- Change the required major version of Prettier from v2 to v3 ([#54775](https://github.com/WordPress/gutenberg/pull/54775)).
+
## 2.25.0 (2023-09-20)
## 2.24.0 (2023-08-31)
diff --git a/packages/prettier-config/package.json b/packages/prettier-config/package.json
index e16eb823bb8883..7ea2bca4df2efd 100644
--- a/packages/prettier-config/package.json
+++ b/packages/prettier-config/package.json
@@ -28,7 +28,7 @@
"main": "lib/index.js",
"types": "build-types",
"peerDependencies": {
- "prettier": ">=2"
+ "prettier": ">=3"
},
"publishConfig": {
"access": "public"
diff --git a/packages/react-native-aztec/android/build.gradle b/packages/react-native-aztec/android/build.gradle
index 71cb631a6cbf6d..7647548360ca75 100644
--- a/packages/react-native-aztec/android/build.gradle
+++ b/packages/react-native-aztec/android/build.gradle
@@ -11,7 +11,7 @@ buildscript {
espressoVersion = '3.0.1'
// libs
- aztecVersion = 'v1.6.4'
+ aztecVersion = 'v1.8.0'
wordpressUtilsVersion = '3.3.0'
// main
@@ -49,7 +49,7 @@ List dirs = [
android {
namespace "org.wordpress.mobile.ReactNativeAztec"
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
minSdkVersion 24
diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json
index 4f1715b9656c5d..87ebdf194696ba 100644
--- a/packages/react-native-aztec/package.json
+++ b/packages/react-native-aztec/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-aztec",
- "version": "1.104.0",
+ "version": "1.105.0",
"description": "Aztec view for react-native.",
"private": true,
"author": "The WordPress Contributors",
diff --git a/packages/react-native-bridge/android/react-native-bridge/build.gradle b/packages/react-native-bridge/android/react-native-bridge/build.gradle
index 8ea42babee505c..7800be076c842a 100644
--- a/packages/react-native-bridge/android/react-native-bridge/build.gradle
+++ b/packages/react-native-bridge/android/react-native-bridge/build.gradle
@@ -35,7 +35,7 @@ android {
// File reference: `react-native-bridge/android/react-native-bridge/src/main/AndroidManifest.xml`
namespace "org.wordpress.mobile.ReactNativeGutenbergBridge"
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
minSdkVersion 24
diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt
index 87a19066f9d15f..ce427be2ad09b0 100644
--- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt
+++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt
@@ -100,14 +100,14 @@ data class GutenbergProps @JvmOverloads constructor(
private const val PROP_TRANSLATIONS = "translations"
private const val PROP_COLORS = "colors"
private const val PROP_GRADIENTS = "gradients"
- private const val PROP_STYLES = "rawStyles"
- private const val PROP_FEATURES = "rawFeatures"
private const val PROP_IS_FSE_THEME = "isFSETheme"
private const val PROP_GALLERY_WITH_IMAGE_BLOCKS = "galleryWithImageBlocks"
private const val PROP_QUOTE_BLOCK_V2 = "quoteBlockV2"
private const val PROP_LIST_BLOCK_V2 = "listBlockV2"
const val PROP_INITIAL_DATA = "initialData"
+ const val PROP_STYLES = "rawStyles"
+ const val PROP_FEATURES = "rawFeatures"
const val PROP_LOCALE = "locale"
const val PROP_CAPABILITIES = "capabilities"
const val PROP_CAPABILITIES_CONTACT_INFO_BLOCK = "contactInfoBlock"
diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json
index 1beb86e3791fa3..b6de1b5c53fff8 100644
--- a/packages/react-native-bridge/package.json
+++ b/packages/react-native-bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-bridge",
- "version": "1.104.0",
+ "version": "1.105.0",
"description": "Native bridge library used to integrate the block editor into a native App.",
"private": true,
"author": "The WordPress Contributors",
diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md
index 1debe8fbaad8e8..9316c4693cfaa6 100644
--- a/packages/react-native-editor/CHANGELOG.md
+++ b/packages/react-native-editor/CHANGELOG.md
@@ -11,6 +11,12 @@ For each user feature we should also add a importance categorization label to i
## Unreleased
+## 1.105.0
+- [*] Limit inner blocks nesting depth to avoid call stack size exceeded crash [#54382]
+- [*] Prevent crashes when setting an invalid media URL for Video or Audio blocks [#54834]
+- [**] Fallback to Twitter provider when embedding X URLs [#54876]
+- [*] [internal] Update Ruby version from 2.7.4 to 3.2.2 [#54897]
+
## 1.104.0
- [*] Fix the obscurred "Insert from URL" input for media blocks when using a device in landscape orientation. [#54096]
- [**] RichText - Update logic for the placeholder text color [#54259]
diff --git a/packages/react-native-editor/__device-tests__/helpers/utils.js b/packages/react-native-editor/__device-tests__/helpers/utils.js
index 9fb43452dcffca..6f2fbe77e7f68a 100644
--- a/packages/react-native-editor/__device-tests__/helpers/utils.js
+++ b/packages/react-native-editor/__device-tests__/helpers/utils.js
@@ -59,11 +59,12 @@ const getIOSPlatformVersions = () => {
childProcess.execSync( 'xcrun simctl list runtimes --json' ).toString()
);
- return runtimes
- .reverse()
- .filter(
- ( { name, isAvailable } ) => name.startsWith( 'iOS' ) && isAvailable
- );
+ return runtimes.reverse().filter(
+ ( { name, isAvailable, version } ) =>
+ name.startsWith( 'iOS' ) &&
+ /15(\.\d+)+/.test( version ) && // Appium 1 does not support newer iOS versions
+ isAvailable
+ );
};
// Initialises the driver and desired capabilities for appium.
@@ -122,7 +123,7 @@ const setupDriver = async () => {
const iosPlatformVersions = getIOSPlatformVersions();
if ( iosPlatformVersions.length === 0 ) {
throw new Error(
- 'No iOS simulators available! Please verify that you have iOS simulators installed.'
+ 'No compatible iOS simulators available! Please verify that you have iOS 15 simulators installed.'
);
}
// eslint-disable-next-line no-console
@@ -209,6 +210,9 @@ const typeStringIos = async ( driver, element, str, clear ) => {
await clearTextBox( driver, element );
}
await element.type( str );
+
+ // Wait for the list auto-scroll animation to finish
+ await driver.sleep( 3000 );
};
const clearTextBox = async ( driver, element ) => {
@@ -426,6 +430,15 @@ const tapPasteAboveElement = async ( driver, element ) => {
}
};
+const tapStatusBariOS = async ( driver ) => {
+ const action = new wd.TouchAction();
+ action.tap( { x: 20, y: 20 } );
+ await driver.performTouchAction( action );
+
+ // Wait for the scroll animation to finish
+ await driver.sleep( 3000 );
+};
+
const selectTextFromElement = async ( driver, element ) => {
if ( isAndroid() ) {
await longPressMiddleOfElement( driver, element, 0 );
@@ -801,6 +814,7 @@ module.exports = {
tapCopyAboveElement,
tapPasteAboveElement,
tapSelectAllAboveElement,
+ tapStatusBariOS,
timer,
toggleDarkMode,
toggleHtmlMode,
diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js
index 65446ca4b5dda8..c296c561725382 100644
--- a/packages/react-native-editor/__device-tests__/pages/editor-page.js
+++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js
@@ -23,6 +23,7 @@ const {
waitForVisible,
clickIfClickable,
launchApp,
+ tapStatusBariOS,
} = require( '../helpers/utils' );
const ADD_BLOCK_ID = isAndroid() ? 'Add block' : 'add-block-button';
@@ -53,8 +54,8 @@ class EditorPage {
}
}
- async initializeEditor( { initialData } = {} ) {
- await launchApp( this.driver, { initialData } );
+ async initializeEditor( { initialData, rawStyles, rawFeatures } = {} ) {
+ await launchApp( this.driver, { initialData, rawStyles, rawFeatures } );
// Stores initial values from the editor for different helpers.
const addButton =
@@ -192,7 +193,11 @@ class EditorPage {
: 'post-title';
if ( options.autoscroll ) {
- await swipeDown( this.driver );
+ if ( isAndroid() ) {
+ await swipeDown( this.driver );
+ } else {
+ await tapStatusBariOS( this.driver );
+ }
}
const elements =
diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java
index a3f7b599db6a14..69985317ddad33 100644
--- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java
+++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java
@@ -167,7 +167,10 @@ private Bundle getAppOptions() {
// Parse initial props from launch arguments
String initialData = null;
+ String rawStyles = null;
+ String rawFeatures = null;
Bundle extrasBundle = getIntent().getExtras();
+
if(extrasBundle != null) {
String initialProps = extrasBundle.getString(EXTRAS_INITIAL_PROPS, "{}");
try {
@@ -175,6 +178,12 @@ private Bundle getAppOptions() {
if (jsonObject.has(GutenbergProps.PROP_INITIAL_DATA)) {
initialData = jsonObject.getString(GutenbergProps.PROP_INITIAL_DATA);
}
+ if (jsonObject.has(GutenbergProps.PROP_STYLES)) {
+ rawStyles = jsonObject.getString(GutenbergProps.PROP_STYLES);
+ }
+ if (jsonObject.has(GutenbergProps.PROP_FEATURES)) {
+ rawFeatures = jsonObject.getString(GutenbergProps.PROP_FEATURES);
+ }
} catch (final JSONException e) {
Log.e("MainActivity", "Json parsing error: " + e.getMessage());
}
@@ -203,6 +212,12 @@ private Bundle getAppOptions() {
if(initialData != null) {
bundle.putString(GutenbergProps.PROP_INITIAL_DATA, initialData);
}
+ if(rawStyles != null) {
+ bundle.putString(GutenbergProps.PROP_STYLES, rawStyles);
+ }
+ if(rawFeatures != null) {
+ bundle.putString(GutenbergProps.PROP_FEATURES, rawFeatures);
+ }
return bundle;
}
diff --git a/packages/react-native-editor/android/build.gradle b/packages/react-native-editor/android/build.gradle
index 4746f9f3077bee..7baee554d330c7 100644
--- a/packages/react-native-editor/android/build.gradle
+++ b/packages/react-native-editor/android/build.gradle
@@ -4,7 +4,7 @@ buildscript {
kotlinVersion = '1.6.10'
buildToolsVersion = "33.0.0"
minSdkVersion = 24
- compileSdkVersion = 33
+ compileSdkVersion = 34
targetSdkVersion = 33
supportLibVersion = '28.0.0'
diff --git a/packages/react-native-editor/babel.config.js b/packages/react-native-editor/babel.config.js
index 62a9959bcf45a2..4f7138636e23ab 100644
--- a/packages/react-native-editor/babel.config.js
+++ b/packages/react-native-editor/babel.config.js
@@ -41,14 +41,14 @@ module.exports = function ( api ) {
/node_modules\/(react-native|@react-native-community|@react-navigation|react-native-reanimated)/,
},
{
- // Auto-add `import { createElement } from '@wordpress/element';` when JSX is found.
+ // Auto-add `import { createElement } from 'react';` when JSX is found.
plugins: [
[
'@wordpress/babel-plugin-import-jsx-pragma',
{
scopeVariable: 'createElement',
scopeVariableFrag: 'Fragment',
- source: '@wordpress/element',
+ source: 'react',
isDefault: false,
},
],
diff --git a/packages/react-native-editor/ios/.ruby-version b/packages/react-native-editor/ios/.ruby-version
index a4dd9dba4fbfc5..be94e6f53db6b3 100644
--- a/packages/react-native-editor/ios/.ruby-version
+++ b/packages/react-native-editor/ios/.ruby-version
@@ -1 +1 @@
-2.7.4
+3.2.2
diff --git a/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj b/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj
index a4e179d3baa742..5cbe46243ff4e4 100644
--- a/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj
+++ b/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj
@@ -216,6 +216,7 @@
00E356EB1AD99517003FC87E /* Frameworks */,
00E356EC1AD99517003FC87E /* Resources */,
71C0B5FDA891DD4D469D2DFB /* [CP] Embed Pods Frameworks */,
+ 6C6876BA82C8AB734B9EB348 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -237,6 +238,7 @@
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
06CE89E9A162D62B604C3B6C /* [CP] Embed Pods Frameworks */,
+ BD87D2B56A91E91E3DFD2CBA /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -333,107 +335,55 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-GutenbergDemo/Pods-GutenbergDemo-frameworks.sh",
- "${BUILT_PRODUCTS_DIR}/BVLinearGradient/BVLinearGradient.framework",
- "${BUILT_PRODUCTS_DIR}/DoubleConversion/DoubleConversion.framework",
- "${BUILT_PRODUCTS_DIR}/Gutenberg/Gutenberg.framework",
- "${BUILT_PRODUCTS_DIR}/RCT-Folly/folly.framework",
- "${BUILT_PRODUCTS_DIR}/RCTTypeSafety/RCTTypeSafety.framework",
- "${BUILT_PRODUCTS_DIR}/RNCClipboard/RNCClipboard.framework",
- "${BUILT_PRODUCTS_DIR}/RNCMaskedView/RNCMaskedView.framework",
- "${BUILT_PRODUCTS_DIR}/RNFastImage/RNFastImage.framework",
- "${BUILT_PRODUCTS_DIR}/RNGestureHandler/RNGestureHandler.framework",
- "${BUILT_PRODUCTS_DIR}/RNReanimated/RNReanimated.framework",
- "${BUILT_PRODUCTS_DIR}/RNSVG/RNSVG.framework",
- "${BUILT_PRODUCTS_DIR}/RNScreens/RNScreens.framework",
- "${BUILT_PRODUCTS_DIR}/RNTAztecView/RNTAztecView.framework",
- "${BUILT_PRODUCTS_DIR}/React-Codegen/React_Codegen.framework",
- "${BUILT_PRODUCTS_DIR}/React-Core/React.framework",
- "${BUILT_PRODUCTS_DIR}/React-CoreModules/CoreModules.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTAnimation/RCTAnimation.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTAppDelegate/React_RCTAppDelegate.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTBlob/RCTBlob.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTImage/RCTImage.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTLinking/RCTLinking.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTNetwork/RCTNetwork.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTSettings/RCTSettings.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTText/RCTText.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTVibration/RCTVibration.framework",
- "${BUILT_PRODUCTS_DIR}/React-cxxreact/cxxreact.framework",
- "${BUILT_PRODUCTS_DIR}/React-jsc/React_jsc.framework",
- "${BUILT_PRODUCTS_DIR}/React-jsi/jsi.framework",
- "${BUILT_PRODUCTS_DIR}/React-jsiexecutor/jsireact.framework",
- "${BUILT_PRODUCTS_DIR}/React-jsinspector/jsinspector.framework",
- "${BUILT_PRODUCTS_DIR}/React-logger/logger.framework",
- "${BUILT_PRODUCTS_DIR}/React-perflogger/reactperflogger.framework",
- "${BUILT_PRODUCTS_DIR}/ReactCommon/ReactCommon.framework",
- "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
- "${BUILT_PRODUCTS_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework",
- "${BUILT_PRODUCTS_DIR}/WordPress-Aztec-iOS/Aztec.framework",
- "${BUILT_PRODUCTS_DIR}/Yoga/yoga.framework",
- "${BUILT_PRODUCTS_DIR}/fmt/fmt.framework",
- "${BUILT_PRODUCTS_DIR}/glog/glog.framework",
- "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-blur/react_native_blur.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-get-random-values/react_native_get_random_values.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-safe-area/react_native_safe_area.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-safe-area-context/react_native_safe_area_context.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-slider/react_native_slider.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-webview/react_native_webview.framework",
+ "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BVLinearGradient.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DoubleConversion.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Gutenberg.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/folly.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTTypeSafety.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNCClipboard.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNCMaskedView.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNFastImage.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNGestureHandler.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNReanimated.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNSVG.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNScreens.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNTAztecView.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_Codegen.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CoreModules.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTAnimation.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_RCTAppDelegate.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTBlob.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTImage.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTLinking.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTNetwork.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTSettings.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTText.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTVibration.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cxxreact.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_jsc.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/jsi.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/jsireact.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/jsinspector.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/logger.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/reactperflogger.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactCommon.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageWebPCoder.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Aztec.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/yoga.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fmt.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_blur.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_get_random_values.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area_context.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_slider.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_webview.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GutenbergDemo/Pods-GutenbergDemo-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
+ 6C6876BA82C8AB734B9EB348 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-GutenbergDemo-GutenbergDemoTests/Pods-GutenbergDemo-GutenbergDemoTests-resources.sh",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/content-functions.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/editor-behavior-overrides.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/gutenberg-observer.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/inject-css.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/insert-block.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/local-storage-overrides.json",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/prevent-autosaves.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/wp-bar-override.css",
+ "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/WordPress-Aztec-iOS/WordPress-Aztec-iOS.bundle",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/content-functions.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/editor-behavior-overrides.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/editor-style-overrides.css",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gutenberg-observer.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/inject-css.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/insert-block.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/local-storage-overrides.json",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/prevent-autosaves.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/wp-bar-override.css",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/WordPress-Aztec-iOS.bundle",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GutenbergDemo-GutenbergDemoTests/Pods-GutenbergDemo-GutenbergDemoTests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
70B839CE7B6A174DC4DE8FB6 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -463,101 +413,11 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-GutenbergDemo-GutenbergDemoTests/Pods-GutenbergDemo-GutenbergDemoTests-frameworks.sh",
- "${BUILT_PRODUCTS_DIR}/BVLinearGradient/BVLinearGradient.framework",
- "${BUILT_PRODUCTS_DIR}/DoubleConversion/DoubleConversion.framework",
- "${BUILT_PRODUCTS_DIR}/Gutenberg/Gutenberg.framework",
- "${BUILT_PRODUCTS_DIR}/RCT-Folly/folly.framework",
- "${BUILT_PRODUCTS_DIR}/RCTTypeSafety/RCTTypeSafety.framework",
- "${BUILT_PRODUCTS_DIR}/RNCClipboard/RNCClipboard.framework",
- "${BUILT_PRODUCTS_DIR}/RNCMaskedView/RNCMaskedView.framework",
- "${BUILT_PRODUCTS_DIR}/RNFastImage/RNFastImage.framework",
- "${BUILT_PRODUCTS_DIR}/RNGestureHandler/RNGestureHandler.framework",
- "${BUILT_PRODUCTS_DIR}/RNReanimated/RNReanimated.framework",
- "${BUILT_PRODUCTS_DIR}/RNSVG/RNSVG.framework",
- "${BUILT_PRODUCTS_DIR}/RNScreens/RNScreens.framework",
- "${BUILT_PRODUCTS_DIR}/RNTAztecView/RNTAztecView.framework",
- "${BUILT_PRODUCTS_DIR}/React-Codegen/React_Codegen.framework",
- "${BUILT_PRODUCTS_DIR}/React-Core/React.framework",
- "${BUILT_PRODUCTS_DIR}/React-CoreModules/CoreModules.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTAnimation/RCTAnimation.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTAppDelegate/React_RCTAppDelegate.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTBlob/RCTBlob.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTImage/RCTImage.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTLinking/RCTLinking.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTNetwork/RCTNetwork.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTSettings/RCTSettings.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTText/RCTText.framework",
- "${BUILT_PRODUCTS_DIR}/React-RCTVibration/RCTVibration.framework",
- "${BUILT_PRODUCTS_DIR}/React-cxxreact/cxxreact.framework",
- "${BUILT_PRODUCTS_DIR}/React-jsc/React_jsc.framework",
- "${BUILT_PRODUCTS_DIR}/React-jsi/jsi.framework",
- "${BUILT_PRODUCTS_DIR}/React-jsiexecutor/jsireact.framework",
- "${BUILT_PRODUCTS_DIR}/React-jsinspector/jsinspector.framework",
- "${BUILT_PRODUCTS_DIR}/React-logger/logger.framework",
- "${BUILT_PRODUCTS_DIR}/React-perflogger/reactperflogger.framework",
- "${BUILT_PRODUCTS_DIR}/ReactCommon/ReactCommon.framework",
- "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
- "${BUILT_PRODUCTS_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework",
- "${BUILT_PRODUCTS_DIR}/WordPress-Aztec-iOS/Aztec.framework",
- "${BUILT_PRODUCTS_DIR}/Yoga/yoga.framework",
- "${BUILT_PRODUCTS_DIR}/fmt/fmt.framework",
- "${BUILT_PRODUCTS_DIR}/glog/glog.framework",
- "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-blur/react_native_blur.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-get-random-values/react_native_get_random_values.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-safe-area/react_native_safe_area.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-safe-area-context/react_native_safe_area_context.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-slider/react_native_slider.framework",
- "${BUILT_PRODUCTS_DIR}/react-native-webview/react_native_webview.framework",
+ "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BVLinearGradient.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DoubleConversion.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Gutenberg.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/folly.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTTypeSafety.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNCClipboard.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNCMaskedView.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNFastImage.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNGestureHandler.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNReanimated.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNSVG.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNScreens.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNTAztecView.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_Codegen.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CoreModules.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTAnimation.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_RCTAppDelegate.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTBlob.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTImage.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTLinking.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTNetwork.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTSettings.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTText.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTVibration.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cxxreact.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_jsc.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/jsi.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/jsireact.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/jsinspector.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/logger.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/reactperflogger.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactCommon.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageWebPCoder.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Aztec.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/yoga.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fmt.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_blur.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_get_random_values.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area_context.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_slider.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_webview.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -586,6 +446,44 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
+ BD87D2B56A91E91E3DFD2CBA /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-GutenbergDemo/Pods-GutenbergDemo-resources.sh",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/content-functions.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/editor-behavior-overrides.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/gutenberg-observer.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/inject-css.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/insert-block.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/local-storage-overrides.json",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/prevent-autosaves.js",
+ "${PODS_ROOT}/../../../react-native-bridge/common/gutenberg-web-single-block/wp-bar-override.css",
+ "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/WordPress-Aztec-iOS/WordPress-Aztec-iOS.bundle",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/content-functions.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/editor-behavior-overrides.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/editor-style-overrides.css",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gutenberg-observer.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/inject-css.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/insert-block.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/local-storage-overrides.json",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/prevent-autosaves.js",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/wp-bar-override.css",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/WordPress-Aztec-iOS.bundle",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GutenbergDemo/Pods-GutenbergDemo-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -789,7 +687,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
- "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_OPTIMIZATION_LEVEL = 0;
@@ -833,7 +731,7 @@
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
- "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift
index 5983fed0065d95..b269d1feb8ddfe 100644
--- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift
+++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift
@@ -368,6 +368,17 @@ extension GutenbergViewController: GutenbergWebDelegate {
}
extension GutenbergViewController: GutenbergBridgeDataSource {
+ class EditorSettings: GutenbergEditorSettings {
+ var isFSETheme: Bool = true
+ var galleryWithImageBlocks: Bool = true
+ var quoteBlockV2: Bool = true
+ var listBlockV2: Bool = true
+ var rawStyles: String? = nil
+ var rawFeatures: String? = nil
+ var colors: [[String: String]]? = nil
+ var gradients: [[String: String]]? = nil
+ }
+
func gutenbergLocale() -> String? {
return Locale.preferredLanguages.first ?? "en"
}
@@ -419,7 +430,14 @@ extension GutenbergViewController: GutenbergBridgeDataSource {
}
func gutenbergEditorSettings() -> GutenbergEditorSettings? {
- return nil
+ guard isUITesting(), let initialProps = getInitialPropsFromArgs() else {
+ return nil
+ }
+ let settings = EditorSettings()
+ settings.rawStyles = initialProps["rawStyles"]
+ settings.rawFeatures = initialProps["rawFeatures"]
+
+ return settings
}
func gutenbergMediaSources() -> [Gutenberg.MediaSource] {
diff --git a/packages/react-native-editor/ios/Podfile b/packages/react-native-editor/ios/Podfile
index a6100a78272652..cc7cda4894b0bf 100644
--- a/packages/react-native-editor/ios/Podfile
+++ b/packages/react-native-editor/ios/Podfile
@@ -28,7 +28,7 @@ end
target 'GutenbergDemo' do
# Comment the next line if you don't want to use dynamic frameworks
- use_frameworks!
+ use_frameworks! linkage: :static
config = use_native_modules!
@@ -37,7 +37,7 @@ target 'GutenbergDemo' do
# Hermes is now enabled by default. Disable by setting this flag to false.
# Upcoming versions of React Native may rely on get_default_flags(), but
# we make it explicit here to aid in the React Native upgrade process.
- :hermes_enabled => false,
+ :hermes_enabled => true,
:fabric_enabled => false,
# Enables Flipper.
#
@@ -93,16 +93,16 @@ target 'GutenbergDemo' do
end
end
end
- ### End workaround for https://github.com/facebook/react-native/issues/31034
+ ### End workaround for https://github.com/facebook/react-native/issues/31034
- ### Begin workaround: https://github.com/CocoaPods/CocoaPods/issues/8891#issuecomment-1201465446
- installer.pods_project.targets.each do |target|
- if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"
- target.build_configurations.each do |config|
- config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
- end
- end
- end
- ### End workaround: https://github.com/CocoaPods/CocoaPods/issues/8891#issuecomment-1201465446
+ ### Begin workaround: https://github.com/CocoaPods/CocoaPods/issues/8891#issuecomment-1201465446
+ installer.pods_project.targets.each do |target|
+ if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"
+ target.build_configurations.each do |config|
+ config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
+ end
+ end
+ end
+ ### End workaround: https://github.com/CocoaPods/CocoaPods/issues/8891#issuecomment-1201465446
end
-end
+end
\ No newline at end of file
diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock
index 73a7e20c622f9f..26a891a2402519 100644
--- a/packages/react-native-editor/ios/Podfile.lock
+++ b/packages/react-native-editor/ios/Podfile.lock
@@ -13,11 +13,15 @@ PODS:
- ReactCommon/turbomodule/core (= 0.71.11)
- fmt (6.2.1)
- glog (0.3.5)
- - Gutenberg (1.104.0):
+ - Gutenberg (1.105.0):
- React-Core (= 0.71.11)
- React-CoreModules (= 0.71.11)
- React-RCTImage (= 0.71.11)
- RNTAztecView
+ - hermes-engine (0.71.11):
+ - hermes-engine/Pre-built (= 0.71.11)
+ - hermes-engine/Pre-built (0.71.11)
+ - libevent (2.1.12)
- libwebp (1.2.3):
- libwebp/demux (= 1.2.3)
- libwebp/mux (= 1.2.3)
@@ -38,6 +42,12 @@ PODS:
- DoubleConversion
- fmt (~> 6.2.1)
- glog
+ - RCT-Folly/Futures (2021.07.22.00):
+ - boost
+ - DoubleConversion
+ - fmt (~> 6.2.1)
+ - glog
+ - libevent
- RCTRequired (0.71.11)
- RCTTypeSafety (0.71.11):
- FBLazyVector (= 0.71.11)
@@ -59,51 +69,55 @@ PODS:
- React-callinvoker (0.71.11)
- React-Codegen (0.71.11):
- FBReactNativeSpec
+ - hermes-engine
- RCT-Folly
- RCTRequired
- RCTTypeSafety
- React-Core
- - React-jsc
- React-jsi
- React-jsiexecutor
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- React-Core (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.71.11)
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/CoreModulesHeaders (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/Default (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/DevSupport (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.71.11)
- React-Core/RCTWebSocket (= 0.71.11)
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-jsinspector (= 0.71.11)
@@ -111,100 +125,110 @@ PODS:
- Yoga
- React-Core/RCTActionSheetHeaders (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/RCTAnimationHeaders (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/RCTBlobHeaders (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/RCTImageHeaders (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/RCTLinkingHeaders (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/RCTNetworkHeaders (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/RCTSettingsHeaders (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/RCTTextHeaders (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/RCTVibrationHeaders (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
- Yoga
- React-Core/RCTWebSocket (0.71.11):
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.71.11)
- React-cxxreact (= 0.71.11)
- - React-jsc
+ - React-hermes
- React-jsi (= 0.71.11)
- React-jsiexecutor (= 0.71.11)
- React-perflogger (= 0.71.11)
@@ -222,6 +246,7 @@ PODS:
- boost (= 1.76.0)
- DoubleConversion
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-callinvoker (= 0.71.11)
- React-jsi (= 0.71.11)
@@ -229,19 +254,27 @@ PODS:
- React-logger (= 0.71.11)
- React-perflogger (= 0.71.11)
- React-runtimeexecutor (= 0.71.11)
- - React-jsc (0.71.11):
- - React-jsc/Fabric (= 0.71.11)
- - React-jsi (= 0.71.11)
- - React-jsc/Fabric (0.71.11):
- - React-jsi (= 0.71.11)
+ - React-hermes (0.71.11):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2021.07.22.00)
+ - RCT-Folly/Futures (= 2021.07.22.00)
+ - React-cxxreact (= 0.71.11)
+ - React-jsi
+ - React-jsiexecutor (= 0.71.11)
+ - React-jsinspector (= 0.71.11)
+ - React-perflogger (= 0.71.11)
- React-jsi (0.71.11):
- boost (= 1.76.0)
- DoubleConversion
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-jsiexecutor (0.71.11):
- DoubleConversion
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-cxxreact (= 0.71.11)
- React-jsi (= 0.71.11)
@@ -287,6 +320,7 @@ PODS:
- React-Core
- ReactCommon/turbomodule/core
- React-RCTBlob (0.71.11):
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-Codegen (= 0.71.11)
- React-Core/RCTBlobHeaders (= 0.71.11)
@@ -334,6 +368,7 @@ PODS:
- ReactCommon/turbomodule/bridging (0.71.11):
- DoubleConversion
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-callinvoker (= 0.71.11)
- React-Core (= 0.71.11)
@@ -344,6 +379,7 @@ PODS:
- ReactCommon/turbomodule/core (0.71.11):
- DoubleConversion
- glog
+ - hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-callinvoker (= 0.71.11)
- React-Core (= 0.71.11)
@@ -375,7 +411,6 @@ PODS:
- React-Core/RCTWebSocket
- React-CoreModules
- React-cxxreact
- - React-jsc
- React-jsi
- React-jsiexecutor
- React-jsinspector
@@ -394,7 +429,7 @@ PODS:
- React-RCTImage
- RNSVG (13.9.0):
- React-Core
- - RNTAztecView (1.104.0):
+ - RNTAztecView (1.105.0):
- React-Core
- WordPress-Aztec-iOS (= 1.19.9)
- SDWebImage (5.11.1):
@@ -414,6 +449,8 @@ DEPENDENCIES:
- FBReactNativeSpec (from `../../../node_modules/react-native/React/FBReactNativeSpec`)
- glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`)
- Gutenberg (from `../../react-native-bridge`)
+ - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
+ - libevent (~> 2.1.12)
- RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTRequired (from `../../../node_modules/react-native/Libraries/RCTRequired`)
- RCTTypeSafety (from `../../../node_modules/react-native/Libraries/TypeSafety`)
@@ -424,7 +461,7 @@ DEPENDENCIES:
- React-Core/RCTWebSocket (from `../../../node_modules/react-native/`)
- React-CoreModules (from `../../../node_modules/react-native/React/CoreModules`)
- React-cxxreact (from `../../../node_modules/react-native/ReactCommon/cxxreact`)
- - React-jsc (from `../../../node_modules/react-native/ReactCommon/jsc`)
+ - React-hermes (from `../../../node_modules/react-native/ReactCommon/hermes`)
- React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`)
- React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector`)
@@ -462,6 +499,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- fmt
+ - libevent
- libwebp
- SDWebImage
- SDWebImageWebPCoder
@@ -482,6 +520,8 @@ EXTERNAL SOURCES:
:podspec: "../../../node_modules/react-native/third-party-podspecs/glog.podspec"
Gutenberg:
:path: "../../react-native-bridge"
+ hermes-engine:
+ :podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
RCT-Folly:
:podspec: "../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
RCTRequired:
@@ -500,8 +540,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native/React/CoreModules"
React-cxxreact:
:path: "../../../node_modules/react-native/ReactCommon/cxxreact"
- React-jsc:
- :path: "../../../node_modules/react-native/ReactCommon/jsc"
+ React-hermes:
+ :path: "../../../node_modules/react-native/ReactCommon/hermes"
React-jsi:
:path: "../../../node_modules/react-native/ReactCommon/jsi"
React-jsiexecutor:
@@ -577,20 +617,22 @@ SPEC CHECKSUMS:
FBReactNativeSpec: f07662560742d82a5b73cee116c70b0b49bcc220
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
- Gutenberg: a79dedde03484f2766429558a85f6ff6c90e2513
+ Gutenberg: eb59e13cd7ef341f9ef92857009d960b0831f21a
+ hermes-engine: 34c863b446d0135b85a6536fa5fd89f48196f848
+ libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: f6187ec763637e6a57f5728dd9a3bdabc6d6b4e0
RCTTypeSafety: a01aca2dd3b27fa422d5239252ad38e54e958750
React: 741b4f5187e7a2137b69c88e65f940ba40600b4b
React-callinvoker: 72ba74b2d5d690c497631191ae6eeca0c043d9cf
- React-Codegen: 6a3870e906e80066a9b707389846c692a02415d9
- React-Core: 9cf97a2d0830a024deffebe873407f6717bbcc19
+ React-Codegen: 8a7cda1633e4940de8a710f6bf5cae5dd673546e
+ React-Core: 72bb19702c465b6451a40501a2879532bec9acee
React-CoreModules: ffd19b082fc36b9b463fedf30955138b5426c053
- React-cxxreact: f88c74ac51e59c294fbf825974d377fcf9641eba
- React-jsc: 75bfda40ea4032b5018875355ab5ee089ac748bf
- React-jsi: 71ae5726d2b0fd6b0aaa0845a9294739cf4c95c6
- React-jsiexecutor: 089cd07c76ecf498960a64ba8ae0f2dddd382f44
+ React-cxxreact: 8b3dd87e3b8ea96dd4ad5c7bac8f31f1cc3da97f
+ React-hermes: be95942c3f47fc032da1387360413f00dae0ea68
+ React-jsi: 9978e2a64c2a4371b40e109f4ef30a33deaa9bcb
+ React-jsiexecutor: 18b5b33c5f2687a784a61bc8176611b73524ae77
React-jsinspector: b6ed4cb3ffa27a041cd440300503dc512b761450
React-logger: 186dd536128ae5924bc38ed70932c00aa740cd5b
react-native-blur: 3e9c8e8e9f7d17fa1b94e1a0ae9fd816675f5382
@@ -603,8 +645,8 @@ SPEC CHECKSUMS:
React-perflogger: e706562ab7eb8eb590aa83a224d26fa13963d7f2
React-RCTActionSheet: 57d4bd98122f557479a3359ad5dad8e109e20c5a
React-RCTAnimation: ccf3ef00101ea74bda73a045d79a658b36728a60
- React-RCTAppDelegate: de78bc79e1a469ffa275fbe3948356cea061c4bb
- React-RCTBlob: 519c8ecb8ef83ce461a5670bdaf2fef882af3393
+ React-RCTAppDelegate: d0c28a35c65e9a0aef287ac0dafe1b71b1ac180c
+ React-RCTBlob: 1700b92ece4357af0a49719c9638185ad2902e95
React-RCTImage: f2e4904566ccccaa4b704170fcc5ae144ca347bf
React-RCTLinking: 52a3740e3651e30aa11dff5a6debed7395dd8169
React-RCTNetwork: ea0976f2b3ffc7877cd7784e351dc460adf87b12
@@ -612,20 +654,20 @@ SPEC CHECKSUMS:
React-RCTText: c9dfc6722621d56332b4f3a19ac38105e7504145
React-RCTVibration: f09f08de63e4122deb32506e20ca4cae6e4e14c1
React-runtimeexecutor: 4817d63dbc9d658f8dc0ec56bd9b83ce531129f0
- ReactCommon: e2d70ebcd90a2eaab343fb0cc23bbdb5ac321f5c
+ ReactCommon: 864169d79e7fb2e846ad1257a2afd7ed846d6375
RNCClipboard: 3f0451a8100393908bea5c5c5b16f96d45f30bfc
RNCMaskedView: 949696f25ec596bfc697fc88e6f95cf0c79669b6
RNFastImage: 1f2cab428712a4baaf78d6169eaec7f622556dd7
RNGestureHandler: f75d81410b40aaa99e71ae8f8bb7a88620c95042
- RNReanimated: df2567658c01135f9ff4709d372675bcb9fd1d83
+ RNReanimated: d4f363f4987ae0ade3e36ff81c94e68261bf4b8d
RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789
RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315
- RNTAztecView: dfbe69cb1448f08b29846616fbb79d79f21968b3
+ RNTAztecView: 4b0ffdbaa58dcbf73b63403a888a2334fc4465dd
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
WordPress-Aztec-iOS: fbebd569c61baa252b3f5058c0a2a9a6ada686bb
Yoga: f7decafdc5e8c125e6fa0da38a687e35238420fa
-PODFILE CHECKSUM: 13786fe1bd037b8f06258137c3f1269a82608b59
+PODFILE CHECKSUM: 0965b30267e411f56545967f14da09df5daa07f4
COCOAPODS: 1.11.3
diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json
index 2d5235e41df9fa..0c6ff58c7184d5 100644
--- a/packages/react-native-editor/package.json
+++ b/packages/react-native-editor/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-editor",
- "version": "1.104.0",
+ "version": "1.105.0",
"description": "Mobile WordPress gutenberg editor.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -69,7 +69,7 @@
"react-native-sass-transformer": "^1.1.1",
"react-native-screens": "3.22.0",
"react-native-svg": "13.9.0",
- "react-native-url-polyfill": "^1.1.2",
+ "react-native-url-polyfill": "1.3.0",
"react-native-video": "https://raw.githubusercontent.com/wordpress-mobile/react-native-video/5.2.0-wp-6/react-native-video-5.2.0-wp-6.tgz",
"react-native-webview": "11.26.1"
},
@@ -87,7 +87,9 @@
"prebundle": "npm run i18n-cache:force",
"bundle": "npm run bundle:android && npm run bundle:ios",
"bundle:android": "mkdir -p bundle/android && npm run rn-bundle -- --platform android --dev false --entry-file index.js --assets-dest bundle/android --bundle-output bundle/android/App.text.js --sourcemap-output bundle/android/App.text.js.map && ./../../node_modules/react-native/sdks/hermesc/`node -e \"const platform=require('os').platform();console.log(platform === 'darwin' ? 'osx-bin' : (platform === 'linux' ? 'linux64-bin' : (platform === 'win32' ? 'win64-bin' : 'unsupported-os')));\"`/hermesc -emit-binary -O -out bundle/android/App.js bundle/android/App.text.js -output-source-map",
- "bundle:ios": "mkdir -p bundle/ios && npm run rn-bundle -- --platform ios --dev false --entry-file index.js --assets-dest bundle/ios --bundle-output bundle/ios/App.js --sourcemap-output bundle/ios/App.js.map",
+ "bundle:ios": "npm run bundle:ios:text && npm run bundle:ios:bytecode",
+ "bundle:ios:text": "mkdir -p bundle/ios && npm run rn-bundle -- --platform ios --dev false --minify false --entry-file ./index.js --assets-dest bundle/ios --bundle-output ./bundle/ios/App.text.js --sourcemap-output ./bundle/ios/App.text.js.map",
+ "bundle:ios:bytecode": "./../../node_modules/react-native/sdks/hermesc/`node -e \"const platform=require('os').platform();console.log(platform === 'darwin' ? 'osx-bin' : (platform === 'linux' ? 'linux64-bin' : (platform === 'win32' ? 'win64-bin' : 'unsupported-os')));\"`/hermesc -emit-binary -O -out bundle/ios/App.js bundle/ios/App.text.js -output-source-map",
"i18n-cache": "node i18n-cache/index.js",
"i18n-cache:force": "cross-env REFRESH_I18N_CACHE=1 node i18n-cache/index.js",
"i18n:extract-used-strings": "node bin/extract-used-strings",
@@ -110,7 +112,7 @@
"test:e2e:bundle:android": "mkdir -p android/app/src/main/assets && npm run rn-bundle -- --reset-cache --platform android --dev false --minify false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res",
"test:e2e:build-app:android": "cd android && ./gradlew clean && ./gradlew assembleDebug",
"test:e2e:android:local": "npm run test:e2e:bundle:android && npm run test:e2e:build-app:android && TEST_RN_PLATFORM=android npm run device-tests:local",
- "test:e2e:bundle:ios": "mkdir -p ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app && npm run rn-bundle -- --reset-cache --platform=ios --dev=false --minify false --entry-file=index.js --bundle-output=./ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app/main.jsbundle --assets-dest=./ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app",
+ "test:e2e:bundle:ios": "mkdir -p ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app && npm run bundle:ios && cp bundle/ios/App.js ./ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app/main.jsbundle && cp -r bundle/ios/assets ./ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app/",
"test:e2e:build-app:ios": "npm run preios && ./bin/build_e2e_ios_app",
"test:e2e:build-wda": "xcodebuild -project ../../node_modules/appium/node_modules/appium-webdriveragent/WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination 'platform=iOS Simulator,name=iPhone 13' -derivedDataPath ios/build/WDA",
"test:e2e:ios:local": "npm run test:e2e:bundle:ios && npm run test:e2e:build-app:ios && npm run test:e2e:build-wda && TEST_RN_PLATFORM=ios npm run device-tests:local",
diff --git a/packages/readable-js-assets-webpack-plugin/test/__snapshots__/build.js.snap b/packages/readable-js-assets-webpack-plugin/test/__snapshots__/build.js.snap
index c033e572e8e5e4..e5a7d6f6accc84 100644
--- a/packages/readable-js-assets-webpack-plugin/test/__snapshots__/build.js.snap
+++ b/packages/readable-js-assets-webpack-plugin/test/__snapshots__/build.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReadableJsAssetsWebpackPlugin should produce the expected output: Asset file index.js should match snapshot 1`] = `
-"/******/ (() => { // webpackBootstrap
+"/******/ (function() { // webpackBootstrap
var __webpack_exports__ = {};
function notMinified() {
// eslint-disable-next-line no-console
@@ -16,7 +16,7 @@ notMinified();
exports[`ReadableJsAssetsWebpackPlugin should produce the expected output: Asset file index.min.js should match snapshot 1`] = `"console.log("hello");"`;
exports[`ReadableJsAssetsWebpackPlugin should produce the expected output: Asset file view.js should match snapshot 1`] = `
-"/******/ (() => { // webpackBootstrap
+"/******/ (function() { // webpackBootstrap
var __webpack_exports__ = {};
function notMinified() {
// eslint-disable-next-line no-console
diff --git a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js
index de29a930e89f18..c9ed4b6502bb60 100644
--- a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js
+++ b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js
@@ -35,7 +35,7 @@ import { unlock } from '../../lock-unlock';
* @param {string[]} props.clientIds Client ids of selected blocks.
* @param {string} props.rootClientId ID of the currently selected top-level block.
* @param {()=>void} props.onClose Callback to close the menu.
- * @return {import('@wordpress/element').WPComponent} The menu control or null.
+ * @return {import('react').ComponentType} The menu control or null.
*/
export default function ReusableBlockConvertButton( {
clientIds,
@@ -115,12 +115,12 @@ export default function ReusableBlockConvertButton( {
! syncType
? sprintf(
// translators: %s: the name the user has given to the pattern.
- __( 'Synced Pattern created: %s' ),
+ __( 'Synced pattern created: %s' ),
reusableBlockTitle
)
: sprintf(
// translators: %s: the name the user has given to the pattern.
- __( 'Unsynced Pattern created: %s' ),
+ __( 'Unsynced pattern created: %s' ),
reusableBlockTitle
),
{
diff --git a/packages/reusable-blocks/src/store/actions.js b/packages/reusable-blocks/src/store/actions.js
index 292a3082146aaa..85fe31138652a4 100644
--- a/packages/reusable-blocks/src/store/actions.js
+++ b/packages/reusable-blocks/src/store/actions.js
@@ -57,7 +57,7 @@ export const __experimentalConvertBlocksToReusable =
: undefined;
const reusableBlock = {
- title: title || __( 'Untitled Pattern block' ),
+ title: title || __( 'Untitled pattern block' ),
content: serialize(
registry
.select( blockEditorStore )
diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md
index 77042d83c6fdc4..810148720da1b7 100644
--- a/packages/scripts/CHANGELOG.md
+++ b/packages/scripts/CHANGELOG.md
@@ -2,6 +2,16 @@
## Unreleased
+### Internal
+
+- The bundled `@pmmmwh/react-refresh-webpack-plugin` dependency has been updated from requiring `^0.5.2` to requiring `^0.5.11` ([#54657](https://github.com/WordPress/gutenberg/pull/54657)).
+- The bundled `browserslist` dependency has been updated from requiring `^4.21.9` to requiring `^4.21.10` ([#54657](https://github.com/WordPress/gutenberg/pull/54657)).
+- The bundled `react-refresh` dependency has been updated from requiring `^0.10.0` to requiring `^0.14.0` ([#54657](https://github.com/WordPress/gutenberg/pull/54657)).
+- The bundled `webpack` dependency has been updated from requiring `^5.47.1` to requiring `^5.88.2` ([#54657](https://github.com/WordPress/gutenberg/pull/54657)).
+- The bundled `webpack-bundle-analyzer` dependency has been updated from requiring `^4.4.2` to requiring `^4.9.1` ([#54657](https://github.com/WordPress/gutenberg/pull/54657)).
+- The bundled `webpack-cli` dependency has been updated from requiring `^4.9.1` to requiring `^5.1.4` ([#54657](https://github.com/WordPress/gutenberg/pull/54657)).
+- The bundled `webpack-dev-server` dependency has been updated from requiring `^4.4.0` to requiring `^4.15.1` ([#54657](https://github.com/WordPress/gutenberg/pull/54657)).
+
## 26.13.0 (2023-09-20)
### Enhancements
diff --git a/packages/scripts/config/playwright.config.ts b/packages/scripts/config/playwright.config.js
similarity index 94%
rename from packages/scripts/config/playwright.config.ts
rename to packages/scripts/config/playwright.config.js
index 16ee210bdfb86e..6f380017ccdc4f 100644
--- a/packages/scripts/config/playwright.config.ts
+++ b/packages/scripts/config/playwright.config.js
@@ -25,7 +25,7 @@ const config = defineConfig( {
snapshotPathTemplate:
'{testDir}/{testFileDir}/__snapshots__/{arg}-{projectName}{ext}',
globalSetup: fileURLToPath(
- new URL( './playwright/global-setup.ts', 'file:' + __filename ).href
+ new URL( './playwright/global-setup.js', 'file:' + __filename ).href
),
use: {
baseURL: process.env.WP_BASE_URL || 'http://localhost:8889',
@@ -60,4 +60,4 @@ const config = defineConfig( {
],
} );
-export default config;
+module.exports = config;
diff --git a/packages/scripts/config/playwright/global-setup.ts b/packages/scripts/config/playwright/global-setup.js
similarity index 78%
rename from packages/scripts/config/playwright/global-setup.ts
rename to packages/scripts/config/playwright/global-setup.js
index 10f2822fdfe1ae..9a4cbde9b7d241 100644
--- a/packages/scripts/config/playwright/global-setup.ts
+++ b/packages/scripts/config/playwright/global-setup.js
@@ -2,14 +2,18 @@
* External dependencies
*/
import { request } from '@playwright/test';
-import type { FullConfig } from '@playwright/test';
/**
* WordPress dependencies
*/
import { RequestUtils } from '@wordpress/e2e-test-utils-playwright';
-async function globalSetup( config: FullConfig ) {
+/**
+ *
+ * @param {import('@playwright/test').FullConfig} config
+ * @return {Promise}
+ */
+async function globalSetup( config ) {
const { storageState, baseURL } = config.projects[ 0 ].use;
const storageStatePath =
typeof storageState === 'string' ? storageState : undefined;
@@ -28,4 +32,4 @@ async function globalSetup( config: FullConfig ) {
await requestContext.dispose();
}
-export default globalSetup;
+module.exports = globalSetup;
diff --git a/packages/scripts/package.json b/packages/scripts/package.json
index e80b68e0a56756..05b57a22f81869 100644
--- a/packages/scripts/package.json
+++ b/packages/scripts/package.json
@@ -33,7 +33,7 @@
},
"dependencies": {
"@babel/core": "^7.16.0",
- "@pmmmwh/react-refresh-webpack-plugin": "^0.5.2",
+ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@svgr/webpack": "^8.0.1",
"@wordpress/babel-preset-default": "file:../babel-preset-default",
"@wordpress/browserslist-config": "file:../browserslist-config",
@@ -48,7 +48,7 @@
"adm-zip": "^0.5.9",
"babel-jest": "^29.6.2",
"babel-loader": "^8.2.3",
- "browserslist": "^4.21.9",
+ "browserslist": "^4.21.10",
"chalk": "^4.0.0",
"check-node-version": "^4.1.0",
"clean-webpack-plugin": "^3.0.0",
@@ -75,9 +75,9 @@
"playwright-core": "1.32.0",
"postcss": "^8.4.5",
"postcss-loader": "^6.2.1",
- "prettier": "npm:wp-prettier@3.0.3-beta-3",
+ "prettier": "npm:wp-prettier@3.0.3",
"puppeteer-core": "^13.2.0",
- "react-refresh": "^0.10.0",
+ "react-refresh": "^0.14.0",
"read-pkg-up": "^7.0.1",
"resolve-bin": "^0.4.0",
"sass": "^1.35.2",
@@ -86,10 +86,10 @@
"stylelint": "^14.2.0",
"terser-webpack-plugin": "^5.3.9",
"url-loader": "^4.1.1",
- "webpack": "^5.47.1",
- "webpack-bundle-analyzer": "^4.4.2",
- "webpack-cli": "^4.9.1",
- "webpack-dev-server": "^4.4.0"
+ "webpack": "^5.88.2",
+ "webpack-bundle-analyzer": "^4.9.1",
+ "webpack-cli": "^5.1.4",
+ "webpack-dev-server": "^4.15.1"
},
"peerDependencies": {
"@playwright/test": "^1.32.0",
diff --git a/packages/sync/CODE.md b/packages/sync/CODE.md
new file mode 100644
index 00000000000000..40a4b76d2cfd42
--- /dev/null
+++ b/packages/sync/CODE.md
@@ -0,0 +1,54 @@
+# Status of the sync experiment in Gutenberg
+
+The sync package is part of an ongoing research effort to lay the groundwork of Real-Time Collaboration in Gutenberg.
+
+Relevant docs:
+
+- https://make.wordpress.org/core/2023/07/13/real-time-collaboration-architecture/
+- https://github.com/WordPress/gutenberg/issues/52593
+- https://docs.yjs.dev/
+
+## Enable the experiment
+
+The experiment can be enabled in the "Guteberg > Experiments" page. When it is enabled (search for `gutenberg-sync-collaboration` in the codebase), the client receives two new pieces of data:
+
+- `window.__experimentalEnableSync`: boolean. Used by the `core-data` package to determine whether to bootstrap and use the sync provider offered by the `sync` package.
+- `window.__experimentalCollaborativeEditingSecret`: string. A secret used by the `sync` package to create a secure connection among peers.
+
+## The data flow
+
+The current experiment updates `core-data` to leverage the YJS library for synchronization and merging changes. Each core-data entity record represents a YJS document and updates to the `--edit` record are broadcasted among peers.
+
+These are the specific checkpoints:
+
+1. REGISTER.
+ - See `getSyncProvider().register( ... )` in `registerSyncConfigs`.
+ - Not all entity types are sync-enabled at the moment, look at those that declare a `syncConfig` and `syncObjectType` in `rootEntitiesConfig`.
+2. BOOTSTRAP.
+ - See `getSyncProvider().bootstrap( ... )` in `getEntityRecord`.
+ - The `bootstrap` function fetches the entity and sets up the callback that will dispatch the relevant Redux action when document changes are broadcasted from other peers.
+3. UPDATE.
+ - See `getSyncProvider().update( ... )` in `editEntityRecord`.
+ - Each change done by a peer to the `--edit` entity record (local changes, not persisted ones) is broadcasted to the others.
+ - The data that is shared is the whole block list.
+
+This is the data flow when the peer A makes a local change:
+
+- Peer A makes a local change.
+- Peer A triggers a `getSyncProvider().update( ... )` request (see `editEntityRecord`).
+- All peers (including A) receive the broadcasted change and execute the callback (see `updateHandler` in `createSyncProvider.bootstrap`).
+- All peers (including A) trigger a `EDIT_ENTITY_RECORD` redux action.
+
+## What works and what doesn't
+
+- Undo/redo does not work.
+- Changes can be persisted and the publish/update button should react accordingly for all peers.
+- Offline.
+ - Changes are stored in the browser's local storage (indexedDB) for each user/peer. Users can navigate away from the document and they'll see the changes when they come back.
+ - Offline changes can be deleted via visiting the browser's database in all peers, then reload the document.
+- Documents can get out of sync. For example:
+ - Two peers open the same document.
+ - One of them (A) leaves the document. Then, the remaining user (B) makes changes.
+ - When A comes back to the document, the changes B made are not visible to A.
+- Entities
+ - Not all entities are synced. For example, global styles are not. Look at the `base` entity config for an example (it declares `syncConfig` and `syncObjectType` properties).
diff --git a/patches/react-native-reanimated+2.17.0.patch b/patches/react-native-reanimated+2.17.0.patch
deleted file mode 100644
index 8d4d634ea13747..00000000000000
--- a/patches/react-native-reanimated+2.17.0.patch
+++ /dev/null
@@ -1,14 +0,0 @@
-This patch will be removed when the Gutenberg demo app uses Hermes.
-
-diff --git a/node_modules/react-native-reanimated/RNReanimated.podspec b/node_modules/react-native-reanimated/RNReanimated.podspec
-index 1cbeafc..34f49db 100644
---- a/node_modules/react-native-reanimated/RNReanimated.podspec
-+++ b/node_modules/react-native-reanimated/RNReanimated.podspec
-@@ -80,6 +80,7 @@ Pod::Spec.new do |s|
- s.dependency 'Yoga'
- s.dependency 'DoubleConversion'
- s.dependency 'glog'
-+ s.dependency 'React-jsc'
-
- if config[:react_native_minor_version] == 62
- s.dependency 'ReactCommon/callinvoker'
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index d383de10726d21..56cd6734e4f3ee 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -59,6 +59,9 @@
./vendor/*
./test/php/gutenberg-coding-standards/*
+
+ ./lib/compat/wordpress-*/html-api/*.php
+
gutenberg.php
@@ -92,8 +95,6 @@
phpunit/*
-
- lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php
phpunit/*
diff --git a/phpunit/block-supports/background-test.php b/phpunit/block-supports/background-test.php
new file mode 100644
index 00000000000000..bdcfc02f551a7f
--- /dev/null
+++ b/phpunit/block-supports/background-test.php
@@ -0,0 +1,187 @@
+test_block_name = null;
+ $this->theme_root = realpath( __DIR__ . '/../data/themedir1' );
+ $this->orig_theme_dir = $GLOBALS['wp_theme_directories'];
+
+ // /themes is necessary as theme.php functions assume /themes is the root if there is only one root.
+ $GLOBALS['wp_theme_directories'] = array( WP_CONTENT_DIR . '/themes', $this->theme_root );
+
+ add_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) );
+ add_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) );
+ add_filter( 'template_root', array( $this, 'filter_set_theme_root' ) );
+
+ // Clear caches.
+ wp_clean_themes_cache();
+ unset( $GLOBALS['wp_themes'] );
+ WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
+ }
+
+ public function tear_down() {
+ $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir;
+
+ // Clear up the filters to modify the theme root.
+ remove_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) );
+ remove_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) );
+ remove_filter( 'template_root', array( $this, 'filter_set_theme_root' ) );
+
+ wp_clean_themes_cache();
+ unset( $GLOBALS['wp_themes'] );
+ WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
+ unregister_block_type( $this->test_block_name );
+ $this->test_block_name = null;
+ parent::tear_down();
+ }
+
+ public function filter_set_theme_root() {
+ return $this->theme_root;
+ }
+
+ /**
+ * Tests that background image block support works as expected.
+ *
+ * @covers ::gutenberg_render_background_support
+ *
+ * @dataProvider data_background_block_support
+ *
+ * @param string $theme_name The theme to switch to.
+ * @param string $block_name The test block name to register.
+ * @param mixed $background_settings The background block support settings.
+ * @param mixed $background_style The background styles within the block attributes.
+ * @param string $expected_wrapper Expected markup for the block wrapper.
+ * @param string $wrapper Existing markup for the block wrapper.
+ */
+ public function test_background_block_support( $theme_name, $block_name, $background_settings, $background_style, $expected_wrapper, $wrapper ) {
+ switch_theme( $theme_name );
+ $this->test_block_name = $block_name;
+
+ register_block_type(
+ $this->test_block_name,
+ array(
+ 'api_version' => 2,
+ 'attributes' => array(
+ 'style' => array(
+ 'type' => 'object',
+ ),
+ ),
+ 'supports' => array(
+ 'background' => $background_settings,
+ ),
+ )
+ );
+
+ $block = array(
+ 'blockName' => $block_name,
+ 'attrs' => array(
+ 'style' => array(
+ 'background' => $background_style,
+ ),
+ ),
+ );
+
+ $actual = gutenberg_render_background_support( $wrapper, $block );
+
+ $this->assertEquals(
+ $expected_wrapper,
+ $actual,
+ 'Background block wrapper markup should be correct'
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_background_block_support() {
+ return array(
+ 'background image style is applied' => array(
+ 'theme_name' => 'block-theme-child-with-fluid-typography',
+ 'block_name' => 'test/background-rules-are-output',
+ 'background_settings' => array(
+ 'backgroundImage' => true,
+ ),
+ 'background_style' => array(
+ 'backgroundImage' => array(
+ 'url' => 'https://example.com/image.jpg',
+ 'source' => 'file',
+ ),
+ ),
+ 'expected_wrapper' => 'Content
',
+ 'wrapper' => 'Content
',
+ ),
+ 'background image style is appended if a style attribute already exists' => array(
+ 'theme_name' => 'block-theme-child-with-fluid-typography',
+ 'block_name' => 'test/background-rules-are-output',
+ 'background_settings' => array(
+ 'backgroundImage' => true,
+ ),
+ 'background_style' => array(
+ 'backgroundImage' => array(
+ 'url' => 'https://example.com/image.jpg',
+ 'source' => 'file',
+ ),
+ ),
+ 'expected_wrapper' => 'Content
',
+ 'wrapper' => 'Content
',
+ ),
+ 'background image style is appended if a style attribute containing multiple styles already exists' => array(
+ 'theme_name' => 'block-theme-child-with-fluid-typography',
+ 'block_name' => 'test/background-rules-are-output',
+ 'background_settings' => array(
+ 'backgroundImage' => true,
+ ),
+ 'background_style' => array(
+ 'backgroundImage' => array(
+ 'url' => 'https://example.com/image.jpg',
+ 'source' => 'file',
+ ),
+ ),
+ 'expected_wrapper' => 'Content
',
+ 'wrapper' => 'Content
',
+ ),
+ 'background image style is not applied if the block does not support background image' => array(
+ 'theme_name' => 'block-theme-child-with-fluid-typography',
+ 'block_name' => 'test/background-rules-are-not-output',
+ 'background_settings' => array(
+ 'backgroundImage' => false,
+ ),
+ 'background_style' => array(
+ 'backgroundImage' => array(
+ 'url' => 'https://example.com/image.jpg',
+ 'source' => 'file',
+ ),
+ ),
+ 'expected_wrapper' => 'Content
',
+ 'wrapper' => 'Content
',
+ ),
+ );
+ }
+}
diff --git a/phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php b/phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php
index 77f95b2385a4ae..1b8e672fa78199 100644
--- a/phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php
+++ b/phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php
@@ -130,7 +130,7 @@ public static function wpSetupBeforeClass( $factory ) {
),
);
- wp_update_post( $new_styles_post, true, false );
+ wp_update_post( $new_styles_post, true, true );
$new_styles_post = array(
'ID' => self::$global_styles_id,
@@ -160,7 +160,7 @@ public static function wpSetupBeforeClass( $factory ) {
),
);
- wp_update_post( $new_styles_post, true, false );
+ wp_update_post( $new_styles_post, true, true );
$new_styles_post = array(
'ID' => self::$global_styles_id,
@@ -190,7 +190,7 @@ public static function wpSetupBeforeClass( $factory ) {
),
);
- wp_update_post( $new_styles_post, true, false );
+ wp_update_post( $new_styles_post, true, true );
wp_set_current_user( 0 );
}
diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php
index 39f308a5791f60..0e212983d9080f 100644
--- a/phpunit/class-wp-theme-json-test.php
+++ b/phpunit/class-wp-theme-json-test.php
@@ -2007,29 +2007,37 @@ public function test_process_blocks_custom_css( $input, $expected ) {
*/
public function data_process_blocks_custom_css() {
return array(
- // Simple CSS without any child selectors.
- 'no child selectors' => array(
+ // Simple CSS without any nested selectors.
+ 'no nested selectors' => array(
'input' => array(
'selector' => '.foo',
'css' => 'color: red; margin: auto;',
),
'expected' => '.foo{color: red; margin: auto;}',
),
- // CSS with child selectors.
- 'with children' => array(
+ // CSS with nested selectors.
+ 'with nested selector' => array(
'input' => array(
'selector' => '.foo',
- 'css' => 'color: red; margin: auto; & .bar{color: blue;}',
+ 'css' => 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;}',
),
- 'expected' => '.foo{color: red; margin: auto;}.foo .bar{color: blue;}',
+ 'expected' => '.foo{color: red; margin: auto;}.foo.one{color: blue;}.foo .two{color: green;}',
),
- // CSS with child selectors and pseudo elements.
- 'with children and pseudo elements' => array(
+ // CSS with pseudo elements.
+ 'with pseudo elements' => array(
'input' => array(
'selector' => '.foo',
- 'css' => 'color: red; margin: auto; & .bar{color: blue;} &::before{color: green;}',
+ 'css' => 'color: red; margin: auto; &::before{color: blue;} & ::before{color: green;} &.one::before{color: yellow;} & .two::before{color: purple;}',
),
- 'expected' => '.foo{color: red; margin: auto;}.foo .bar{color: blue;}.foo::before{color: green;}',
+ 'expected' => '.foo{color: red; margin: auto;}.foo::before{color: blue;}.foo ::before{color: green;}.foo.one::before{color: yellow;}.foo .two::before{color: purple;}',
+ ),
+ // CSS with multiple root selectors.
+ 'with multiple root selectors' => array(
+ 'input' => array(
+ 'selector' => '.foo, .bar',
+ 'css' => 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;} &::before{color: yellow;} & ::before{color: purple;} &.three::before{color: orange;} & .four::before{color: skyblue;}',
+ ),
+ 'expected' => '.foo, .bar{color: red; margin: auto;}.foo.one, .bar.one{color: blue;}.foo .two, .bar .two{color: green;}.foo::before, .bar::before{color: yellow;}.foo ::before, .bar ::before{color: purple;}.foo.three::before, .bar.three::before{color: orange;}.foo .four::before, .bar .four::before{color: skyblue;}',
),
);
}
diff --git a/phpunit/experimental/block-editor-settings-mobile-test.php b/phpunit/experimental/block-editor-settings-mobile-test.php
index 224fb4bfcb188b..907b750d8c8638 100644
--- a/phpunit/experimental/block-editor-settings-mobile-test.php
+++ b/phpunit/experimental/block-editor-settings-mobile-test.php
@@ -24,6 +24,7 @@ class Gutenberg_REST_Block_Editor_Settings_Controller_Test extends WP_Test_REST_
public function set_up() {
parent::set_up();
switch_theme( 'block-theme' );
+ remove_action( 'wp_print_styles', 'print_emoji_styles' );
}
/**
diff --git a/phpunit/tests/blocks/renderHookedBlocks.php b/phpunit/tests/blocks/renderHookedBlocks.php
index de256a0ec2be19..5e9abd4d8ba050 100644
--- a/phpunit/tests/blocks/renderHookedBlocks.php
+++ b/phpunit/tests/blocks/renderHookedBlocks.php
@@ -1,7 +1,5 @@
setAccessible( true );
$config = array(
- 'id' => 'my-collection',
- 'name' => 'My Collection',
- 'description' => 'My collection description',
- 'data_json_file' => 'my-collection-data.json',
+ 'id' => 'my-collection',
+ 'name' => 'My Collection',
+ 'description' => 'My collection description',
+ 'src' => 'my-collection-data.json',
);
$font_collection = new WP_Font_Collection( $config );
@@ -51,9 +51,9 @@ public function data_should_throw_exception() {
return array(
'no id' => array(
array(
- 'name' => 'My Collection',
- 'description' => 'My collection description',
- 'data_json_file' => 'my-collection-data.json',
+ 'name' => 'My Collection',
+ 'description' => 'My collection description',
+ 'src' => 'my-collection-data.json',
),
'Font Collection config ID is required as a non-empty string.',
),
@@ -78,13 +78,13 @@ public function data_should_throw_exception() {
'Font Collection config options is required as a non-empty array.',
),
- 'missing data_json_file' => array(
+ 'missing src' => array(
array(
'id' => 'my-collection',
'name' => 'My Collection',
'description' => 'My collection description',
),
- 'Font Collection config "data_json_file" option is required as a non-empty string.',
+ 'Font Collection config "src" option is required as a non-empty string.',
),
);
diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/getData.php b/phpunit/tests/fonts/font-library/wpFontCollection/getData.php
index 55d12ac1c42fd3..4d0b2eb92b595e 100644
--- a/phpunit/tests/fonts/font-library/wpFontCollection/getData.php
+++ b/phpunit/tests/fonts/font-library/wpFontCollection/getData.php
@@ -12,6 +12,40 @@
*/
class Tests_Fonts_WpFontCollection_GetData extends WP_UnitTestCase {
+ public function set_up() {
+ parent::set_up();
+
+ // Mock the wp_remote_request() function.
+ add_filter( 'pre_http_request', array( $this, 'mock_request' ), 10, 3 );
+ }
+
+ public function tear_down() {
+ // Remove the mock to not affect other tests.
+ remove_filter( 'pre_http_request', array( $this, 'mock_request' ) );
+
+ parent::tear_down();
+ }
+
+ public function mock_request( $preempt, $args, $url ) {
+ // if the URL is not the URL you want to mock, return false.
+ if ( 'https://localhost/fonts/mock-font-collection.json' !== $url ) {
+ return false;
+ }
+
+ // Mock the response body.
+ $mock_collection_data = array(
+ 'fontFamilies' => 'mock',
+ 'categories' => 'mock',
+ );
+
+ return array(
+ 'body' => json_encode( $mock_collection_data ),
+ 'response' => array(
+ 'code' => 200,
+ ),
+ );
+ }
+
/**
* @dataProvider data_should_get_data
*
@@ -33,18 +67,35 @@ public function data_should_get_data() {
file_put_contents( $mock_file, '{"this is mock data":true}' );
return array(
- 'with a data_json_file' => array(
+ 'with a file' => array(
'config' => array(
- 'id' => 'my-collection',
- 'name' => 'My Collection',
- 'description' => 'My collection description',
- 'data_json_file' => $mock_file,
+ 'id' => 'my-collection',
+ 'name' => 'My Collection',
+ 'description' => 'My collection description',
+ 'src' => $mock_file,
),
'expected_data' => array(
'id' => 'my-collection',
'name' => 'My Collection',
'description' => 'My collection description',
- 'data' => '{"this is mock data":true}',
+ 'data' => array( 'this is mock data' => true ),
+ ),
+ ),
+ 'with a url' => array(
+ 'config' => array(
+ 'id' => 'my-collection-with-url',
+ 'name' => 'My Collection with URL',
+ 'description' => 'My collection description',
+ 'src' => 'https://localhost/fonts/mock-font-collection.json',
+ ),
+ 'expected_data' => array(
+ 'id' => 'my-collection-with-url',
+ 'name' => 'My Collection with URL',
+ 'description' => 'My collection description',
+ 'data' => array(
+ 'fontFamilies' => 'mock',
+ 'categories' => 'mock',
+ ),
),
),
);
diff --git a/phpunit/tests/fonts/font-library/wpFontFamily/install.php b/phpunit/tests/fonts/font-library/wpFontFamily/install.php
index ddd8c70a979721..79cb6ac3ba2ba4 100644
--- a/phpunit/tests/fonts/font-library/wpFontFamily/install.php
+++ b/phpunit/tests/fonts/font-library/wpFontFamily/install.php
@@ -167,6 +167,8 @@ public function test_should_move_local_fontfaces( $font_data, array $files_data,
copy( __DIR__ . '/../../../data/fonts/cooper-hewitt.woff', $file['tmp_name'] );
} elseif ( 'font/woff2' === $file['type'] ) {
copy( __DIR__ . '/../../../data/fonts/DMSans.woff2', $file['tmp_name'] );
+ } elseif ( 'application/vnd.ms-opentype' === $file['type'] ) {
+ copy( __DIR__ . '/../../../data/fonts/gilbert-color.otf', $file['tmp_name'] );
}
}
@@ -302,6 +304,32 @@ public function data_should_move_local_fontfaces() {
),
'expected' => array( 'dm-sans_regular_500.woff2' ),
),
+ // otf font type.
+ 'otf local font' => array(
+ 'font_data' => array(
+ 'name' => 'Gilbert Color',
+ 'slug' => 'gilbert-color',
+ 'fontFamily' => 'Gilbert Color',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Gilbert Color',
+ 'fontStyle' => 'regular',
+ 'fontWeight' => '500',
+ 'uploadedFile' => 'files0',
+ ),
+ ),
+ ),
+ 'files_data' => array(
+ 'files0' => array(
+ 'name' => 'gilbert-color.otf',
+ 'type' => 'application/vnd.ms-opentype',
+ 'tmp_name' => wp_tempnam( 'Gilbert-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ ),
+ 'expected' => array( 'gilbert-color_regular_500.otf' ),
+ ),
);
}
diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php
index b3d8d1f10a1efc..bfdb7258fa11aa 100644
--- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php
+++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php
@@ -14,10 +14,10 @@ class Tests_Fonts_WpFontLibrary_GetFontCollection extends WP_UnitTestCase {
public static function set_up_before_class() {
$my_font_collection_config = array(
- 'id' => 'my-font-collection',
- 'name' => 'My Font Collection',
- 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
- 'data_json_file' => path_join( __DIR__, 'my-font-collection-data.json' ),
+ 'id' => 'my-font-collection',
+ 'name' => 'My Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'src' => path_join( __DIR__, 'my-font-collection-data.json' ),
);
wp_register_font_collection( $my_font_collection_config );
diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php
index 4bda590d6e8fd0..97e66e64e87161 100644
--- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php
+++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php
@@ -13,33 +13,33 @@
class Tests_Fonts_WpFontLibrary_GetFontCollections extends WP_UnitTestCase {
public static function set_up_before_class() {
+ $font_library = new WP_Font_Library();
+
$my_font_collection_config = array(
- 'id' => 'my-font-collection',
- 'name' => 'My Font Collection',
- 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
- 'data_json_file' => path_join( __DIR__, 'my-font-collection-data.json' ),
+ 'id' => 'my-font-collection',
+ 'name' => 'My Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'src' => path_join( __DIR__, 'my-font-collection-data.json' ),
);
- wp_register_font_collection( $my_font_collection_config );
-
- $another_font_collection_config = array(
- 'id' => 'another-font-collection',
- 'name' => 'Another Font Collection',
- 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
- 'data_json_file' => path_join( __DIR__, 'another-font-collection-data.json' ),
- );
+ $font_library::register_font_collection( $my_font_collection_config );
+ }
- wp_register_font_collection( $another_font_collection_config );
+ public function test_should_get_the_default_font_collection() {
+ $font_collections = WP_Font_Library::get_font_collections();
+ $this->assertArrayHasKey( 'default-font-collection', $font_collections, 'Default Google Fonts collection should be registered' );
+ $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['default-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' );
}
- public function test_should_get_font_collections() {
+ public function test_should_get_the_right_number_of_collections() {
$font_collections = WP_Font_Library::get_font_collections();
$this->assertNotEmpty( $font_collections, 'Sould return an array of font collections.' );
$this->assertCount( 2, $font_collections, 'Should return an array with one font collection.' );
+ }
+ public function test_should_get_mock_font_collection() {
+ $font_collections = WP_Font_Library::get_font_collections();
$this->assertArrayHasKey( 'my-font-collection', $font_collections, 'The array should have the key of the registered font collection id.' );
$this->assertInstanceOf( 'WP_Font_Collection', $font_collections['my-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' );
- $this->assertArrayHasKey( 'another-font-collection', $font_collections, 'The array should have the key of the registered font collection id.' );
- $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['another-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' );
}
}
diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php
new file mode 100644
index 00000000000000..708134af69e92a
--- /dev/null
+++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php
@@ -0,0 +1,90 @@
+assertEquals( $mimes, $expected );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_should_supply_correct_mime_type_for_php_version() {
+ return array(
+ 'version 7.2' => array(
+ 'php_version_id' => 70200,
+ 'expected' => array(
+ 'otf' => 'application/vnd.ms-opentype',
+ 'ttf' => 'application/x-font-ttf',
+ 'woff' => 'application/font-woff',
+ 'woff2' => 'application/font-woff2',
+ ),
+ ),
+ 'version 7.3' => array(
+ 'php_version_id' => 70300,
+ 'expected' => array(
+ 'otf' => 'application/vnd.ms-opentype',
+ 'ttf' => 'application/font-sfnt',
+ 'woff' => 'application/font-woff',
+ 'woff2' => 'application/font-woff2',
+ ),
+ ),
+ 'version 7.4' => array(
+ 'php_version_id' => 70400,
+ 'expected' => array(
+ 'otf' => 'application/vnd.ms-opentype',
+ 'ttf' => 'font/sfnt',
+ 'woff' => 'application/font-woff',
+ 'woff2' => 'application/font-woff2',
+ ),
+ ),
+ 'version 8.0' => array(
+ 'php_version_id' => 80000,
+ 'expected' => array(
+ 'otf' => 'application/vnd.ms-opentype',
+ 'ttf' => 'font/sfnt',
+ 'woff' => 'application/font-woff',
+ 'woff2' => 'application/font-woff2',
+ ),
+ ),
+ 'version 8.1' => array(
+ 'php_version_id' => 80100,
+ 'expected' => array(
+ 'otf' => 'application/vnd.ms-opentype',
+ 'ttf' => 'font/sfnt',
+ 'woff' => 'font/woff',
+ 'woff2' => 'font/woff2',
+ ),
+ ),
+ 'version 8.2' => array(
+ 'php_version_id' => 80200,
+ 'expected' => array(
+ 'otf' => 'application/vnd.ms-opentype',
+ 'ttf' => 'font/sfnt',
+ 'woff' => 'font/woff',
+ 'woff2' => 'font/woff2',
+ ),
+ ),
+ );
+ }
+}
diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php
index 61b5eab873d6c5..6bc5fbb8161cee 100644
--- a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php
+++ b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php
@@ -14,10 +14,10 @@ class Tests_Fonts_WpFontLibrary_RegisterFontCollection extends WP_UnitTestCase {
public function test_should_register_font_collection() {
$config = array(
- 'id' => 'my-collection',
- 'name' => 'My Collection',
- 'description' => 'My Collection Description',
- 'data_json_file' => 'my-collection-data.json',
+ 'id' => 'my-collection',
+ 'name' => 'My Collection',
+ 'description' => 'My Collection Description',
+ 'src' => 'my-collection-data.json',
);
$collection = WP_Font_Library::register_font_collection( $config );
$this->assertInstanceOf( 'WP_Font_Collection', $collection );
@@ -25,9 +25,9 @@ public function test_should_register_font_collection() {
public function test_should_return_error_if_id_is_missing() {
$config = array(
- 'name' => 'My Collection',
- 'description' => 'My Collection Description',
- 'data_json_file' => 'my-collection-data.json',
+ 'name' => 'My Collection',
+ 'description' => 'My Collection Description',
+ 'src' => 'my-collection-data.json',
);
$this->expectException( 'Exception' );
$this->expectExceptionMessage( 'Font Collection config ID is required as a non-empty string.' );
@@ -36,9 +36,9 @@ public function test_should_return_error_if_id_is_missing() {
public function test_should_return_error_if_name_is_missing() {
$config = array(
- 'id' => 'my-collection',
- 'description' => 'My Collection Description',
- 'data_json_file' => 'my-collection-data.json',
+ 'id' => 'my-collection',
+ 'description' => 'My Collection Description',
+ 'src' => 'my-collection-data.json',
);
$this->expectException( 'Exception' );
$this->expectExceptionMessage( 'Font Collection config name is required as a non-empty string.' );
@@ -54,16 +54,16 @@ public function test_should_return_error_if_config_is_empty() {
public function test_should_return_error_if_id_is_repeated() {
$config1 = array(
- 'id' => 'my-collection-1',
- 'name' => 'My Collection 1',
- 'description' => 'My Collection 1 Description',
- 'data_json_file' => 'my-collection-1-data.json',
+ 'id' => 'my-collection-1',
+ 'name' => 'My Collection 1',
+ 'description' => 'My Collection 1 Description',
+ 'src' => 'my-collection-1-data.json',
);
$config2 = array(
- 'id' => 'my-collection-1',
- 'name' => 'My Collection 2',
- 'description' => 'My Collection 2 Description',
- 'data_json_file' => 'my-collection-2-data.json',
+ 'id' => 'my-collection-1',
+ 'name' => 'My Collection 2',
+ 'description' => 'My Collection 2 Description',
+ 'src' => 'my-collection-2-data.json',
);
// Register first collection.
diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php b/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php
index be15ecce898816..daa4c84aad9004 100644
--- a/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php
+++ b/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php
@@ -15,14 +15,16 @@ class Tests_Fonts_WpFontLibrary_SetUploadDir extends WP_UnitTestCase {
public function test_should_set_fonts_upload_dir() {
$defaults = array(
'subdir' => '/abc',
- 'basedir' => '/var/www/html/wp-content',
- 'baseurl' => 'http://example.com/wp-content',
+ 'basedir' => '/any/path',
+ 'baseurl' => 'http://example.com/an/arbitrary/url',
+ 'path' => '/any/path/abc',
+ 'url' => 'http://example.com/an/arbitrary/url/abc',
);
$expected = array(
'subdir' => '/fonts',
- 'basedir' => '/var/www/html/wp-content',
+ 'basedir' => WP_CONTENT_DIR,
'baseurl' => content_url(),
- 'path' => '/var/www/html/wp-content/fonts',
+ 'path' => path_join( WP_CONTENT_DIR, 'fonts' ),
'url' => content_url() . '/fonts',
);
$this->assertSame( $expected, WP_Font_Library::set_upload_dir( $defaults ) );
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php
index f1e712bab85b40..a6b02f38a5e817 100644
--- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php
@@ -7,9 +7,19 @@
*/
abstract class WP_REST_Font_Library_Controller_UnitTestCase extends WP_UnitTestCase {
+ /**
+ * Fonts directory.
+ *
+ * @var string
+ */
+ protected static $fonts_dir;
+
+
public function set_up() {
parent::set_up();
+ static::$fonts_dir = WP_Font_Library::get_fonts_dir();
+
// Create a user with administrator role.
$admin_id = $this->factory->user->create(
array(
@@ -29,6 +39,11 @@ public function tear_down() {
$reflection = new ReflectionClass( 'WP_Font_Library' );
$property = $reflection->getProperty( 'collections' );
$property->setAccessible( true );
- $property->setValue( array() );
+ $property->setValue( null, array() );
+
+ // Clean up the /fonts directory.
+ foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) {
+ @unlink( $file );
+ }
}
}
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php
index ca661a1aac6268..f1a0a6a0cd510c 100644
--- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php
@@ -14,29 +14,96 @@
class Tests_Fonts_WPRESTFontLibraryController_GetFontCollection extends WP_REST_Font_Library_Controller_UnitTestCase {
/**
- * Register a mock collection.
+ * Register mock collections.
*/
- public static function wpSetupBeforeClass() {
+ public function set_up() {
+ parent::set_up();
// Mock font collection data file.
$mock_file = wp_tempnam( 'one-collection-' );
file_put_contents( $mock_file, '{"this is mock data":true}' );
+ // Mock the wp_remote_request() function.
+ add_filter( 'pre_http_request', array( $this, 'mock_request' ), 10, 3 );
- // Add a font collection.
- $config = array(
- 'id' => 'one-collection',
- 'name' => 'One Font Collection',
- 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
- 'data_json_file' => $mock_file,
+ $config_with_file = array(
+ 'id' => 'one-collection',
+ 'name' => 'One Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'src' => $mock_file,
);
- wp_register_font_collection( $config );
+ wp_register_font_collection( $config_with_file );
+
+ $config_with_url = array(
+ 'id' => 'collection-with-url',
+ 'name' => 'Another Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'src' => 'https://wordpress.org/fonts/mock-font-collection.json',
+ );
+
+ wp_register_font_collection( $config_with_url );
+
+ $config_with_non_existing_file = array(
+ 'id' => 'collection-with-non-existing-file',
+ 'name' => 'Another Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'src' => '/home/non-existing-file.json',
+ );
+
+ wp_register_font_collection( $config_with_non_existing_file );
+
+ $config_with_non_existing_url = array(
+ 'id' => 'collection-with-non-existing-url',
+ 'name' => 'Another Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'src' => 'https://non-existing-url-1234x.com.ar/fake-path/missing-file.json',
+ );
+
+ wp_register_font_collection( $config_with_non_existing_url );
}
- public function test_get_font_collection() {
+ public function mock_request( $preempt, $args, $url ) {
+ // Check if it's the URL you want to mock.
+ if ( 'https://wordpress.org/fonts/mock-font-collection.json' === $url ) {
+
+ // Mock the response body.
+ $mock_collection_data = array(
+ 'fontFamilies' => 'mock',
+ 'categories' => 'mock',
+ );
+
+ return array(
+ 'body' => json_encode( $mock_collection_data ),
+ 'response' => array(
+ 'code' => 200,
+ ),
+ );
+ }
+
+ // For any other URL, return false which ensures the request is made as usual (or you can return other mock data).
+ return false;
+ }
+
+ public function tear_down() {
+ // Remove the mock to not affect other tests.
+ remove_filter( 'pre_http_request', array( $this, 'mock_request' ) );
+
+ parent::tear_down();
+ }
+
+ public function test_get_font_collection_from_file() {
$request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections/one-collection' );
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
$this->assertSame( 200, $response->get_status(), 'The response status is not 200.' );
$this->assertArrayHasKey( 'data', $data, 'The response data does not have the key with the file data.' );
+ $this->assertSame( array( 'this is mock data' => true ), $data['data'], 'The response data does not have the expected file data.' );
+ }
+
+ public function test_get_font_collection_from_url() {
+ $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections/collection-with-url' );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+ $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' );
+ $this->assertArrayHasKey( 'data', $data, 'The response data does not have the key with the file data.' );
}
public function test_get_non_existing_collection_should_return_404() {
@@ -44,4 +111,16 @@ public function test_get_non_existing_collection_should_return_404() {
$response = rest_get_server()->dispatch( $request );
$this->assertSame( 404, $response->get_status() );
}
+
+ public function test_get_non_existing_file_should_return_500() {
+ $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections/collection-with-non-existing-file' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertSame( 500, $response->get_status() );
+ }
+
+ public function test_get_non_existing_url_should_return_500() {
+ $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections/collection-with-non-existing-url' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertSame( 500, $response->get_status() );
+ }
}
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php
index 52678aa4cb94ae..ad120ee36fce4d 100644
--- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php
@@ -27,10 +27,10 @@ public function test_get_font_collections() {
// Add a font collection.
$config = array(
- 'id' => 'my-font-collection',
- 'name' => 'My Font Collection',
- 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
- 'data_json_file' => $mock_file,
+ 'id' => 'my-font-collection',
+ 'name' => 'My Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'src' => $mock_file,
);
wp_register_font_collection( $config );
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php
index e92776b70ed640..01ac1ff8436ed7 100644
--- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php
@@ -24,18 +24,17 @@ class Tests_Fonts_WPRESTFontLibraryController_InstallFonts extends WP_REST_Font_
public function test_install_fonts( $font_families, $files, $expected_response ) {
$install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' );
$font_families_json = json_encode( $font_families );
- $install_request->set_param( 'fontFamilies', $font_families_json );
+ $install_request->set_param( 'font_families', $font_families_json );
$install_request->set_file_params( $files );
$response = rest_get_server()->dispatch( $install_request );
$data = $response->get_data();
-
$this->assertSame( 200, $response->get_status(), 'The response status is not 200.' );
- $this->assertCount( count( $expected_response ), $data, 'Not all the font families were installed correctly.' );
+ $this->assertCount( count( $expected_response['successes'] ), $data['successes'], 'Not all the font families were installed correctly.' );
// Checks that the font families were installed correctly.
- for ( $family_index = 0; $family_index < count( $data ); $family_index++ ) {
- $installed_font = $data[ $family_index ];
- $expected_font = $expected_response[ $family_index ];
+ for ( $family_index = 0; $family_index < count( $data['successes'] ); $family_index++ ) {
+ $installed_font = $data['successes'][ $family_index ];
+ $expected_font = $expected_response['successes'][ $family_index ];
if ( isset( $installed_font['fontFace'] ) || isset( $expected_font['fontFace'] ) ) {
for ( $face_index = 0; $face_index < count( $installed_font['fontFace'] ); $face_index++ ) {
@@ -61,9 +60,10 @@ public function test_install_fonts( $font_families, $files, $expected_response )
public function data_install_fonts() {
$temp_file_path1 = wp_tempnam( 'Piazzola1-' );
- file_put_contents( $temp_file_path1, 'Mocking file content' );
+ copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $temp_file_path1 );
+
$temp_file_path2 = wp_tempnam( 'Monteserrat-' );
- file_put_contents( $temp_file_path2, 'Mocking file content' );
+ copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $temp_file_path2 );
return array(
@@ -100,32 +100,35 @@ public function data_install_fonts() {
),
'files' => array(),
'expected_response' => array(
- array(
- 'fontFamily' => 'Piazzolla',
- 'slug' => 'piazzolla',
- 'name' => 'Piazzolla',
- 'fontFace' => array(
- array(
- 'fontFamily' => 'Piazzolla',
- 'fontStyle' => 'normal',
- 'fontWeight' => '400',
- 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf',
+ 'successes' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf',
+ ),
),
),
- ),
- array(
- 'fontFamily' => 'Montserrat',
- 'slug' => 'montserrat',
- 'name' => 'Montserrat',
- 'fontFace' => array(
- array(
- 'fontFamily' => 'Montserrat',
- 'fontStyle' => 'normal',
- 'fontWeight' => '100',
- 'src' => '/wp-content/fonts/montserrat_normal_100.ttf',
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'slug' => 'montserrat',
+ 'name' => 'Montserrat',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '100',
+ 'src' => '/wp-content/fonts/montserrat_normal_100.ttf',
+ ),
),
),
),
+ 'errors' => array(),
),
),
@@ -160,33 +163,36 @@ public function data_install_fonts() {
),
'files' => array(),
'expected_response' => array(
- array(
- 'fontFamily' => 'Piazzolla',
- 'slug' => 'piazzolla',
- 'name' => 'Piazzolla',
- 'fontFace' => array(
- array(
- 'fontFamily' => 'Piazzolla',
- 'fontStyle' => 'normal',
- 'fontWeight' => '400',
- 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf',
+ 'successes' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf',
+ ),
),
),
- ),
- array(
- 'fontFamily' => 'Montserrat',
- 'slug' => 'montserrat',
- 'name' => 'Montserrat',
- 'fontFace' => array(
- array(
- 'fontFamily' => 'Montserrat',
- 'fontStyle' => 'normal',
- 'fontWeight' => '100',
- 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf',
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'slug' => 'montserrat',
+ 'name' => 'Montserrat',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '100',
+ 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf',
+ ),
),
),
),
+ 'errors' => array(),
),
),
@@ -200,11 +206,14 @@ public function data_install_fonts() {
),
'files' => array(),
'expected_response' => array(
- array(
- 'fontFamily' => 'Arial',
- 'slug' => 'arial',
- 'name' => 'Arial',
+ 'successes' => array(
+ array(
+ 'fontFamily' => 'Arial',
+ 'slug' => 'arial',
+ 'name' => 'Arial',
+ ),
),
+ 'errors' => array(),
),
),
@@ -254,32 +263,36 @@ public function data_install_fonts() {
),
),
'expected_response' => array(
- array(
- 'fontFamily' => 'Piazzolla',
- 'slug' => 'piazzolla',
- 'name' => 'Piazzolla',
- 'fontFace' => array(
- array(
- 'fontFamily' => 'Piazzolla',
- 'fontStyle' => 'normal',
- 'fontWeight' => '400',
- 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf',
+ 'successes' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf',
+ ),
),
),
- ),
- array(
- 'fontFamily' => 'Montserrat',
- 'slug' => 'montserrat',
- 'name' => 'Montserrat',
- 'fontFace' => array(
- array(
- 'fontFamily' => 'Montserrat',
- 'fontStyle' => 'normal',
- 'fontWeight' => '100',
- 'src' => '/wp-content/fonts/montserrat_normal_100.ttf',
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'slug' => 'montserrat',
+ 'name' => 'Montserrat',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '100',
+ 'src' => '/wp-content/fonts/montserrat_normal_100.ttf',
+ ),
),
),
+
),
+ 'errors' => array(),
),
),
);
@@ -296,7 +309,7 @@ public function data_install_fonts() {
public function test_install_with_improper_inputs( $font_families, $files = array() ) {
$install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' );
$font_families_json = json_encode( $font_families );
- $install_request->set_param( 'fontFamilies', $font_families_json );
+ $install_request->set_param( 'font_families', $font_families_json );
$install_request->set_file_params( $files );
$response = rest_get_server()->dispatch( $install_request );
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php
index 3082bfc87f62ef..a3b613e6f983e0 100644
--- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php
@@ -53,7 +53,7 @@ public function set_up() {
$install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' );
$font_families_json = json_encode( $mock_families );
- $install_request->set_param( 'fontFamilies', $font_families_json );
+ $install_request->set_param( 'font_families', $font_families_json );
rest_get_server()->dispatch( $install_request );
}
@@ -68,9 +68,8 @@ public function test_uninstall() {
);
$uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' );
- $uninstall_request->set_param( 'fontFamilies', $font_families_to_uninstall );
+ $uninstall_request->set_param( 'font_families', $font_families_to_uninstall );
$response = rest_get_server()->dispatch( $uninstall_request );
- echo ( print_r( $response->get_data(), true ) );
$this->assertSame( 200, $response->get_status(), 'The response status is not 200.' );
}
@@ -89,9 +88,9 @@ public function test_uninstall_non_existing_fonts() {
),
);
- $uninstall_request->set_param( 'fontFamilies', $non_existing_font_data );
+ $uninstall_request->set_param( 'font_families', $non_existing_font_data );
$response = rest_get_server()->dispatch( $uninstall_request );
- $response->get_data();
- $this->assertSame( 500, $response->get_status(), 'The response status is not 500.' );
+ $data = $response->get_data();
+ $this->assertCount( 2, $data['errors'], 'The response should have 2 errors, one for each font family uninstall failure.' );
}
}
diff --git a/platform-docs/docs/basic-concepts/block-library.md b/platform-docs/docs/basic-concepts/block-library.md
new file mode 100644
index 00000000000000..dba569f34fac13
--- /dev/null
+++ b/platform-docs/docs/basic-concepts/block-library.md
@@ -0,0 +1,112 @@
+---
+sidebar_position: 4
+---
+
+# Block Library
+
+The block editor relies on a registry of block types to render and edit blocks. The `@wordpress/block-library` package provides a set of core blocks that you can register in your application.
+
+## Registring all block types
+
+Registering blocks requires both loading the JavaScript code that makes the block type available for use and including the corresponding stylesheets.
+
+To register all blocks from the block library, you can use the `registerCoreBlocks` function:
+
+```js
+import { registerCoreBlocks } from '@wordpress/block-library';
+
+registerCoreBlocks();
+```
+
+And make sure to also load the stylesheets required for these blocks.
+
+```js
+import "@wordpress/block-library/build-style/common.css";
+import "@wordpress/block-library/build-style/style.css";
+import "@wordpress/block-library/build-style/editor.css";
+```
+
+## Registering individual blocks
+
+That said, by default the block library includes a very big number of blocks and some of them may contain some WordPress-specific logic. For this reason, if you're building a third-party block editor, it's recommended to only register the blocks that you need.
+
+### The paragraph block type
+
+The main block type that almost all block editors need is the paragraph block. You can register it with the following code:
+
+```js
+import '@wordpress/block-library/build-module/paragraph/init';
+import '@wordpress/block-library/build-style/paragraph/style.css';
+import '@wordpress/block-library/build-style/paragraph/editor.css';
+```
+
+Also, the paragraph block is often used as the "default block" in the block editor. The default block has multiple purposes:
+
+ - It's the block that is selected when the user starts typing or hits Enter.
+ - It's the block that is inserted when the user clicks on the "Add block" button.
+ - It's the block where the user can hit `/` to search for alternative block types.
+
+You can mark the paragraph block as the default block with the following code:
+
+```js
+import { setDefaultBlockName } from '@wordpress/blocks';
+
+setDefaultBlockName( 'core/paragraph' );
+```
+
+### The HTML block type
+
+Another important block type that most block editors would want to use is the HTML block. This block allows users to insert arbitrary HTML code in the block editor and is often used to insert embeds or external content.
+
+It is also used by the block editor to render blocks that are not registered in the block editor or as a fallback block type for random HTML content that can't be properly parsed into blocks.
+
+You can register the HTML block with the following code:
+
+```js
+import '@wordpress/block-library/build-module/html/init';
+import '@wordpress/block-library/build-style/html/editor.css';
+```
+
+And mark it as the fallback block type with the following code:
+
+```js
+import {
+ setFreeformContentHandlerName,
+ setUnregisteredTypeHandlerName
+} from '@wordpress/blocks';
+
+setFreeformContentHandlerName( 'core/html' );
+setUnregisteredTypeHandlerName( 'core/html' );
+```
+
+### Extra blocks
+
+In addition to these two default blocks, here's a non-exhaustive list of blocks that can be registered and used by any block editor:
+
+ - **Heading block**: `heading`
+ - **List block**: `list` and `list-item`
+ - **Quote block**: `quote`
+ - **Image block**: `image`
+ - **Gallery block**: `gallery`
+ - **Video block**: `video`
+ - **Audio block**: `audio`
+ - **Cover block**: `cover`
+ - **File block**: `file`
+ - **Code block**: `code`
+ - **Preformatted block**: `preformatted`
+ - **Pullquote block**: `pullquote`
+ - **Table block**: `table`
+ - **Verse block**: `verse`
+ - **Separator block**: `separator`
+ - **Spacer block**: `spacer`
+ - **Columns block**: `columns` and `column`
+ - **Group block**: `group`
+ - **Button block**: `buttons` and `button`
+ - **Social links block**: `social-links` and `social-link`
+
+For each block, you'll need to load the JavaScript code and stylesheets. Some blocks have two stylesheets (`style.css` and `editor.css`). For example, to register the heading block, you can use the following code:
+
+```js
+import '@wordpress/block-library/build-module/heading/init';
+import '@wordpress/block-library/build-style/heading/editor.css';
+```
diff --git a/platform-docs/docs/basic-concepts/internationalization.md b/platform-docs/docs/basic-concepts/internationalization.md
index 3b808bd71e61b1..c15a1e2e68145f 100644
--- a/platform-docs/docs/basic-concepts/internationalization.md
+++ b/platform-docs/docs/basic-concepts/internationalization.md
@@ -1,5 +1,7 @@
---
-sidebar_position: 6
+sidebar_position: 7
---
# Internationalization
+
+# RTL Support
diff --git a/platform-docs/docs/basic-concepts/rendering.md b/platform-docs/docs/basic-concepts/rendering.md
index f5458df2e81e77..9c823f4931aaa1 100644
--- a/platform-docs/docs/basic-concepts/rendering.md
+++ b/platform-docs/docs/basic-concepts/rendering.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 7
+sidebar_position: 8
---
# Rendering blocks
diff --git a/platform-docs/docs/basic-concepts/rich-text.md b/platform-docs/docs/basic-concepts/rich-text.md
index e3517bc93203e8..bfd8119f02ce41 100644
--- a/platform-docs/docs/basic-concepts/rich-text.md
+++ b/platform-docs/docs/basic-concepts/rich-text.md
@@ -3,3 +3,18 @@ sidebar_position: 5
---
# RichText and Format Library
+
+Several block types (like paragraph, heading, and more) allow users to type and manipulate rich text content. In order to do so, they use the `RichText` component and the `@wordpress/rich-text` package.
+
+The `RichText` component is a wrapper around the `@wordpress/rich-text` package that provides a React-friendly API to manipulate rich text content. It allows the user to add and apply formats to the text content.
+
+By default, no format is available. You need to register formats in order to make them available to the user. The `@wordpress/format-library` provides a set of default formats to include in your application. It includes:
+
+- bold, italic, superscript, subscript, strikethrough, links, inline code, inline image, text color, keyboard input (kbd), language (bdo).
+
+In order to register all formats from the format library, add the `@wordpress/format-library` as a dependency of your app and you can use the following code.
+
+```js
+import '@wordpress/format-library';
+import '@wordpress/format-library/build-style/style.css';
+```
diff --git a/platform-docs/docs/basic-concepts/settings.md b/platform-docs/docs/basic-concepts/settings.md
index 66d0691aef5f75..a2d376e71a29dd 100644
--- a/platform-docs/docs/basic-concepts/settings.md
+++ b/platform-docs/docs/basic-concepts/settings.md
@@ -1,5 +1,163 @@
---
-sidebar_position: 4
+sidebar_position: 3
---
# Block Editor Settings
+
+You can customize the block editor by providing a `settings` prop to the `BlockEditorProvider` component. This prop accepts an object with the following properties:
+
+## styles
+
+The styles setting is an array of editor styles to enqueue in the iframe/canvas of the block editor. Each style is an object with a `css` property. Example:
+
+```jsx
+import { BlockEditorProvider, BlockCanvas } from '@wordpress/block-editor';
+
+export const editorStyles = [
+ {
+ css: `
+ body {
+ font-family: Arial;
+ font-size: 16px;
+ }
+
+ p {
+ font-size: inherit;
+ line-height: inherit;
+ }
+
+ ul {
+ list-style-type: disc;
+ }
+
+ ol {
+ list-style-type: decimal;
+ }
+ `,
+ },
+];
+
+export default function App() {
+ return (
+
+
+
+ );
+}
+```
+
+## mediaUpload
+
+Some core blocks, like the image or video blocks, allow users to render media within the block editor. By default, you can use external URLs but if you want to allow users to upload media from their computer, you need to provide a `mediaUpload` function. Here's a quick example of such function:
+
+```jsx
+async function mediaUpload( {
+ additionalData = {},
+ filesList,
+ onError = noop,
+ onFileChange,
+} ) {
+ const uploadedMedia = [];
+ for ( const file of filesList ) {
+ try {
+ const data = await someApiCallToUploadTheFile( file );
+ const mediaObject = {
+ alt: data.alt,
+ caption: data.caption,
+ title: data.title,
+ url: data.url,
+ };
+ uploadedMedia.push( mediaObject );
+ } catch ( error ) {
+ onError( {
+ code: 'SOME_ERROR_CODE',
+ message:
+ mediaFile.name +
+ ' : Sorry, an error happened while uploading this file.',
+ file: mediaFile,
+ } );
+ }
+ if ( uploadedMedia.length ) {
+ onFileChange( uploadedMedia );
+ }
+ }
+}
+```
+
+Providing a `mediaUpload` function also enables drag and dropping files into the editor to upload them.
+
+## inserterMediaCategories
+
+The inserter media categories setting is an array of media categories to display in the inserter. Each category is an object with `name` and `labels` values, a `fetch` function and a few extra keys. Example:
+
+```jsx
+{
+ name: 'openverse',
+ labels: {
+ name: 'Openverse',
+ search_items: 'Search Openverse',
+ },
+ mediaType: 'image',
+ async fetch( query = {} ) {
+ const defaultArgs = {
+ mature: false,
+ excluded_source: 'flickr,inaturalist,wikimedia',
+ license: 'pdm,cc0',
+ };
+ const finalQuery = { ...query, ...defaultArgs };
+ // Sometimes you might need to map the supported request params according to the `InserterMediaRequest`
+ // interface. In this example the `search` query param is named `q`.
+ const mapFromInserterMediaRequest = {
+ per_page: 'page_size',
+ search: 'q',
+ };
+ const url = new URL( 'https://api.openverse.engineering/v1/images/' );
+ Object.entries( finalQuery ).forEach( ( [ key, value ] ) => {
+ const queryKey = mapFromInserterMediaRequest[ key ] || key;
+ url.searchParams.set( queryKey, value );
+ } );
+ const response = await window.fetch( url, {
+ headers: {
+ 'User-Agent': 'WordPress/inserter-media-fetch',
+ },
+ } );
+ const jsonResponse = await response.json();
+ const results = jsonResponse.results;
+ return results.map( ( result ) => ( {
+ ...result,
+ // If your response result includes an `id` prop that you want to access later, it should
+ // be mapped to `InserterMediaItem`'s `sourceId` prop. This can be useful if you provide
+ // a report URL getter.
+ // Additionally you should always clear the `id` value of your response results because
+ // it is used to identify WordPress media items.
+ sourceId: result.id,
+ id: undefined,
+ caption: result.caption,
+ previewUrl: result.thumbnail,
+ } ) );
+ },
+ getReportUrl: ( { sourceId } ) =>
+ `https://wordpress.org/openverse/image/${ sourceId }/report/`,
+ isExternalResource: true,
+}
+```
+
+## hasFixedToolbar
+
+Whether the `BlockTools` component renders the toolbar in a fixed position or if follows the position of the selected block. Defaults to `false`.
+
+## focusMode
+
+The focus mode is a special mode where any block that is not selected is shown with a reduced opacity, giving more prominence to the selected block. Defaults to `false`.
+
+## keepCaretInsideBlock
+
+By default, arrow keys can be used to navigate across blocks. You can turn off that behavior and keep the caret inside the currently selected block using this setting. Defaults to `false`.
+
+## codeEditingEnabled
+
+Allows the user to edit the currently selected block using an HTML code editor. Note that using the code editor is not supported by all blocks and that it has the potential to create invalid blocks depending on the markup the user provides. Defaults to `true`.
+
+## canLockBlocks
+
+Whether the user can lock blocks. Defaults to `true`.
diff --git a/platform-docs/docs/basic-concepts/undo-redo.md b/platform-docs/docs/basic-concepts/undo-redo.md
index 26981e6e0f5c16..d3041a21eebe9b 100644
--- a/platform-docs/docs/basic-concepts/undo-redo.md
+++ b/platform-docs/docs/basic-concepts/undo-redo.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 3
+sidebar_position: 6
---
# Undo Redo
@@ -14,7 +14,8 @@ First, make sure you add the `@wordpress/compose` package to your dependencies,
```jsx
import { useStateWithHistory } from '@wordpress/compose';
-import { createRoot, createElement, useState } from "@wordpress/element";
+import { createElement, useState } from "react";
+import { createRoot } from 'react-dom/client';
import {
BlockEditorProvider,
BlockCanvas,
diff --git a/platform-docs/docs/intro.md b/platform-docs/docs/intro.md
index 41732843164e2b..79a142731891e5 100644
--- a/platform-docs/docs/intro.md
+++ b/platform-docs/docs/intro.md
@@ -23,49 +23,12 @@ Once done, you can navigate to your application folder and run it locally using
To build a block editor, you need to install the following dependencies:
- `@wordpress/block-editor`
- - `@wordpress/element`
- `@wordpress/block-library`
- `@wordpress/components`
-## Setup vite to use JSX and @wordpress/element as a pragma
+## JSX
-We're going to be using JSX to write our UI and components. So one of the first steps we need to do is to configure our build tooling.
-
-If you're using vite, you can create a `vite.config.js` file at the root of your application and paste the following content:
-
-```js
-// vite.config.js
-import { defineConfig } from 'vite'
-
-export default defineConfig({
- esbuild: {
- jsxFactory: 'createElement',
- jsxFragment: 'Fragment',
- },
- define: {
- // This is not necessary to make JSX work
- // but it's a requirement for some @wordpress packages.
- 'process.env': {}
- },
-})
-```
-
-With the config above, you should be able to write JSX in your `*.jsx` files, just make sure you import createElement and Fragment at the top of each of your files: `import { createElement, Fragment } from '@wordpress/element';`
-
-We can now check that everything is working as expected:
-
- - Remove the default content created by vite: `main.js` and `counter.js` files.
- - Create an `index.jsx` file at the root of your application containing some JSX, example:
-```jsx
-import { createRoot, createElement } from '@wordpress/element';
-
-// Render your React component instead
-const root = createRoot(document.getElementById('app'));
-root.render(Hello, world );
-```
- - Update the script file in your `index.html` to `index.jsx` instead of `main.js`.
-
-After restarting your local build (aka `npm run dev`), you should see the **"Hello, world"** heading appearing in your browser.
+We're going to be using JSX to write our UI and components. So one of the first steps we need to do is to configure our build tooling, By default vite supports JSX and and outputs the result as a React pragma. The Block editor uses React so there's no need to configure anything here but if you're using a different bundler/build tool, make sure the JSX transpilation is setup properly.
## Bootstrap your block editor
@@ -73,7 +36,8 @@ It's time to render our first block editor.
- Update your `index.jsx` file with the following code:
```jsx
-import { createRoot, createElement, useState } from "@wordpress/element";
+import { createElement, useState } from "react";
+import { createRoot } from 'react-dom/client';
import {
BlockEditorProvider,
BlockCanvas,
diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts
index 742ca54f4f2ace..cbbfbac3be4159 100644
--- a/test/e2e/playwright.config.ts
+++ b/test/e2e/playwright.config.ts
@@ -11,7 +11,7 @@ import { defineConfig, devices } from '@playwright/test';
const baseConfig = require( '@wordpress/scripts/config/playwright.config' );
const config = defineConfig( {
- ...baseConfig.default,
+ ...baseConfig,
reporter: process.env.CI
? [ [ 'github' ], [ './config/flaky-tests-reporter.ts' ] ]
: 'list',
diff --git a/test/e2e/specs/editor/blocks/buttons.spec.js b/test/e2e/specs/editor/blocks/buttons.spec.js
index 39d6e76eaf6689..04cd358729a113 100644
--- a/test/e2e/specs/editor/blocks/buttons.spec.js
+++ b/test/e2e/specs/editor/blocks/buttons.spec.js
@@ -162,6 +162,99 @@ test.describe( 'Buttons', () => {
);
} );
+ test( 'can toggle button link settings', async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ await editor.insertBlock( { name: 'core/buttons' } );
+ await page.keyboard.type( 'WordPress' );
+ await pageUtils.pressKeys( 'primary+k' );
+ await page.keyboard.type( 'https://www.wordpress.org/' );
+ await page.keyboard.press( 'Enter' );
+
+ // Edit link.
+ await page.getByRole( 'button', { name: 'Edit' } ).click();
+
+ // Open Advanced settings panel.
+ await page
+ .getByRole( 'region', {
+ name: 'Editor content',
+ } )
+ .getByRole( 'button', {
+ name: 'Advanced',
+ } )
+ .click();
+
+ const newTabCheckbox = page.getByLabel( 'Open in new tab' );
+ const noFollowCheckbox = page.getByLabel( 'nofollow' );
+
+ // Navigate to and toggle the "Open in new tab" checkbox.
+ await newTabCheckbox.click();
+
+ // Toggle should still have focus and be checked.
+ await expect( newTabCheckbox ).toBeChecked();
+ await expect( newTabCheckbox ).toBeFocused();
+
+ 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/buttons',
+ innerBlocks: [
+ {
+ name: 'core/button',
+ attributes: {
+ text: 'WordPress',
+ url: 'https://www.wordpress.org/',
+ rel: 'noreferrer noopener',
+ linkTarget: '_blank',
+ },
+ },
+ ],
+ },
+ ] );
+
+ // Edit link again.
+ await page.getByRole( 'button', { name: 'Edit' } ).click();
+
+ // Navigate to and toggle the "nofollow" checkbox.
+ await noFollowCheckbox.click();
+
+ // expect settings for `Open in new tab` and `No follow`
+ await expect( newTabCheckbox ).toBeChecked();
+ await expect( noFollowCheckbox ).toBeChecked();
+
+ 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();
+
+ // Check the content again.
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/buttons',
+ innerBlocks: [
+ {
+ name: 'core/button',
+ attributes: {
+ text: 'WordPress',
+ url: 'https://www.wordpress.org/',
+ rel: 'noreferrer noopener nofollow',
+ linkTarget: '_blank',
+ },
+ },
+ ],
+ },
+ ] );
+ } );
+
test( 'can resize width', async ( { editor, page } ) => {
await editor.insertBlock( { name: 'core/buttons' } );
await page.keyboard.type( 'Content' );
diff --git a/test/e2e/specs/editor/blocks/columns.spec.js b/test/e2e/specs/editor/blocks/columns.spec.js
index 23443059c15224..bcb23c9a240991 100644
--- a/test/e2e/specs/editor/blocks/columns.spec.js
+++ b/test/e2e/specs/editor/blocks/columns.spec.js
@@ -143,7 +143,7 @@ test.describe( 'Columns', () => {
} );
await editor.selectBlocks(
- editor.canvas.locator( 'role=document[name="Paragraph block"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Paragraph"i]' )
);
await page.keyboard.press( 'ArrowRight' );
await page.keyboard.press( 'Enter' );
@@ -200,7 +200,7 @@ test.describe( 'Columns', () => {
await editor.selectBlocks(
editor.canvas.locator(
- 'role=document[name="Paragraph block"i] >> text="1"'
+ 'role=document[name="Block: Paragraph"i] >> text="1"'
)
);
await page.keyboard.press( 'ArrowRight' );
diff --git a/test/e2e/specs/editor/blocks/cover.spec.js b/test/e2e/specs/editor/blocks/cover.spec.js
index 98be7b85304090..fa5103ebaa4eeb 100644
--- a/test/e2e/specs/editor/blocks/cover.spec.js
+++ b/test/e2e/specs/editor/blocks/cover.spec.js
@@ -26,7 +26,6 @@ test.describe( 'Cover', () => {
test( 'can set overlay color using color picker on block placeholder', async ( {
editor,
- coverBlockUtils,
} ) => {
await editor.insertBlock( { name: 'core/cover' } );
const coverBlock = editor.canvas.getByRole( 'document', {
@@ -39,18 +38,14 @@ test.describe( 'Cover', () => {
} );
await expect( blackColorSwatch ).toBeVisible();
- // Get the RGB value of Black.
- const [ blackRGB ] =
- await coverBlockUtils.getBackgroundColorAndOpacity( coverBlock );
-
// Create the block by clicking selected color button.
await blackColorSwatch.click();
- // Get the RGB value of the background dim.
- const [ actualRGB ] =
- await coverBlockUtils.getBackgroundColorAndOpacity( coverBlock );
-
- expect( blackRGB ).toEqual( actualRGB );
+ // Assert that after clicking black, the background color is black.
+ await expect( coverBlock ).toHaveCSS(
+ 'background-color',
+ 'rgb(0, 0, 0)'
+ );
} );
test( 'can set background image using image upload on block placeholder', async ( {
@@ -76,7 +71,7 @@ test.describe( 'Cover', () => {
} ).toPass();
} );
- test( 'dims background image down by 50% by default', async ( {
+ test( 'dims background image down by 50% with the average image color when an image is uploaded', async ( {
editor,
coverBlockUtils,
} ) => {
@@ -89,15 +84,14 @@ test.describe( 'Cover', () => {
coverBlock.getByTestId( 'form-file-upload-input' )
);
- // The hidden span must be used as the target for opacity and color value.
- // Using the Cover block to calculate the opacity results in an incorrect value of 1.
- // The hidden span value returns the correct opacity at 0.5.
- const [ backgroundDimColor, backgroundDimOpacity ] =
- await coverBlockUtils.getBackgroundColorAndOpacity(
- coverBlock.locator( 'span[aria-hidden="true"]' )
- );
- expect( backgroundDimColor ).toBe( 'rgb(0, 0, 0)' );
- expect( backgroundDimOpacity ).toBe( '0.5' );
+ // The overlay is a separate aria-hidden span before the image.
+ const overlay = coverBlock.locator( '.wp-block-cover__background' );
+
+ await expect( overlay ).toHaveCSS(
+ 'background-color',
+ 'rgb(179, 179, 179)'
+ );
+ await expect( overlay ).toHaveCSS( 'opacity', '0.5' );
} );
test( 'can have the title edited', async ( { editor } ) => {
@@ -119,7 +113,7 @@ test.describe( 'Cover', () => {
// Activate the paragraph block inside the Cover block.
// The name of the block differs depending on whether text has been entered or not.
const coverBlockParagraph = coverBlock.getByRole( 'document', {
- name: /Paragraph block|Empty block; start writing or type forward slash to choose a block/,
+ name: /Block: Paragraph|Empty block; start writing or type forward slash to choose a block/,
} );
await expect( coverBlockParagraph ).toBeEditable();
@@ -200,7 +194,7 @@ test.describe( 'Cover', () => {
expect( newCoverBlockBox.height ).toBe( coverBlockBox.height + 100 );
} );
- test( 'dims the background image down by 50% when transformed from the Image block', async ( {
+ test( 'dims the background image down by 50% black when transformed from the Image block', async ( {
editor,
coverBlockUtils,
} ) => {
@@ -226,19 +220,11 @@ test.describe( 'Cover', () => {
name: 'Block: Cover',
} );
- // The hidden span must be used as the target for opacity and color value.
- // Using the Cover block to calculate the opacity results in an incorrect value of 1.
- // The hidden span value returns the correct opacity at 0.5.
- const [ backgroundDimColor, backgroundDimOpacity ] =
- await coverBlockUtils.getBackgroundColorAndOpacity(
- coverBlock.locator( 'span[aria-hidden="true"]' )
- );
-
- // The hidden span must be used as the target for opacity and color value.
- // Using the Cover block to calculate the opacity results in an incorrect value of 1.
- // The hidden span value returns the correct opacity at 0.5.
- expect( backgroundDimColor ).toBe( 'rgb(0, 0, 0)' );
- expect( backgroundDimOpacity ).toBe( '0.5' );
+ // The overlay is a separate aria-hidden span before the image.
+ const overlay = coverBlock.locator( '.wp-block-cover__background' );
+
+ await expect( overlay ).toHaveCSS( 'background-color', 'rgb(0, 0, 0)' );
+ await expect( overlay ).toHaveCSS( 'opacity', '0.5' );
} );
} );
@@ -269,11 +255,4 @@ class CoverBlockUtils {
return filename;
}
-
- async getBackgroundColorAndOpacity( locator ) {
- return await locator.evaluate( ( el ) => {
- const computedStyle = window.getComputedStyle( el );
- return [ computedStyle.backgroundColor, computedStyle.opacity ];
- } );
- }
}
diff --git a/test/e2e/specs/editor/blocks/list.spec.js b/test/e2e/specs/editor/blocks/list.spec.js
index 9fe0ce101c5e97..f4396982bb997f 100644
--- a/test/e2e/specs/editor/blocks/list.spec.js
+++ b/test/e2e/specs/editor/blocks/list.spec.js
@@ -1345,7 +1345,9 @@ test.describe( 'List (@firefox)', () => {
2
` );
- await page.getByRole( 'button', { name: 'Paragraph' } ).click();
+ await page
+ .getByRole( 'button', { name: 'Multiple blocks selected' } )
+ .click();
await page.getByRole( 'menuitem', { name: 'List' } ).click();
expect( await editor.getEditedPostContent() ).toBe( `
diff --git a/test/e2e/specs/editor/blocks/navigation-colors.spec.js b/test/e2e/specs/editor/blocks/navigation-colors.spec.js
index 1fd8bd39f520a0..42ecb29aa6650f 100644
--- a/test/e2e/specs/editor/blocks/navigation-colors.spec.js
+++ b/test/e2e/specs/editor/blocks/navigation-colors.spec.js
@@ -413,6 +413,10 @@ class ColorControl {
.getByRole( 'button', { name: 'Open menu' } )
.click();
+ // Move the mouse to avoid accidentally triggering hover
+ // state on the links once the overlay opens.
+ await this.page.mouse.move( 1000, 1000 );
+
const overlay = this.editor.canvas
.locator( '.wp-block-navigation__responsive-container' )
.filter( { hasText: 'Submenu Link' } );
diff --git a/test/e2e/specs/editor/blocks/navigation-list-view.spec.js b/test/e2e/specs/editor/blocks/navigation-list-view.spec.js
index 185297178323f3..fd7113fe170959 100644
--- a/test/e2e/specs/editor/blocks/navigation-list-view.spec.js
+++ b/test/e2e/specs/editor/blocks/navigation-list-view.spec.js
@@ -195,8 +195,8 @@ test.describe( 'Navigation block - List view editing', () => {
await expect( blockResultOptions.nth( 1 ) ).toHaveText( 'Custom Link' );
// Select the Page Link option.
- const pageLinkResult = blockResultOptions.nth( 0 );
- await pageLinkResult.click();
+ const customLinkResult = blockResultOptions.nth( 1 );
+ await customLinkResult.click();
// Expect to see the Link creation UI be focused.
const linkUIInput = linkControl.getSearchInput();
@@ -209,7 +209,26 @@ test.describe( 'Navigation block - List view editing', () => {
await expect( linkUIInput ).toBeFocused();
await expect( linkUIInput ).toBeEmpty();
+ // Provides test coverage for feature whereby Custom Link type
+ // should default to `Pages` when displaying the "initial suggestions"
+ // in the Link UI.
+ // See https://github.com/WordPress/gutenberg/pull/54622.
const firstResult = await linkControl.getNthSearchResult( 0 );
+ const secondResult = await linkControl.getNthSearchResult( 1 );
+ const thirdResult = await linkControl.getNthSearchResult( 2 );
+
+ const firstResultType =
+ await linkControl.getSearchResultType( firstResult );
+
+ const secondResultType =
+ await linkControl.getSearchResultType( secondResult );
+
+ const thirdResultType =
+ await linkControl.getSearchResultType( thirdResult );
+
+ expect( firstResultType ).toBe( 'Page' );
+ expect( secondResultType ).toBe( 'Page' );
+ expect( thirdResultType ).toBe( 'Page' );
// Grab the text from the first result so we can check (later on) that it was inserted.
const firstResultText =
@@ -570,9 +589,15 @@ class LinkControl {
await expect( result ).toBeVisible();
return result
- .locator(
- '.components-menu-item__info-wrapper .components-menu-item__item'
- ) // this is the only way to get the label text without the URL.
+ .locator( '.components-menu-item__item' ) // this is the only way to get the label text without the URL.
+ .innerText();
+ }
+
+ async getSearchResultType( result ) {
+ await expect( result ).toBeVisible();
+
+ return result
+ .locator( '.components-menu-item__shortcut' ) // this is the only way to get the type text.
.innerText();
}
}
diff --git a/test/e2e/specs/editor/blocks/quote.spec.js b/test/e2e/specs/editor/blocks/quote.spec.js
index 44645005ff05e2..13b7ee341ede72 100644
--- a/test/e2e/specs/editor/blocks/quote.spec.js
+++ b/test/e2e/specs/editor/blocks/quote.spec.js
@@ -110,7 +110,7 @@ test.describe( 'Quote', () => {
await page.keyboard.type( 'two' );
await page.keyboard.down( 'Shift' );
await editor.canvas.click(
- 'role=document[name="Paragraph block"i] >> text=one'
+ 'role=document[name="Block: Paragraph"i] >> text=one'
);
await page.keyboard.up( 'Shift' );
await editor.transformBlockTo( 'core/quote' );
diff --git a/test/e2e/specs/editor/blocks/search.spec.js b/test/e2e/specs/editor/blocks/search.spec.js
new file mode 100644
index 00000000000000..d11efd7328afeb
--- /dev/null
+++ b/test/e2e/specs/editor/blocks/search.spec.js
@@ -0,0 +1,94 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Search', () => {
+ test.beforeEach( async ( { admin, requestUtils } ) => {
+ await requestUtils.deleteAllMenus();
+ await admin.createNewPost();
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.deleteAllMenus();
+ } );
+
+ test.afterEach( async ( { requestUtils } ) => {
+ await Promise.all( [
+ requestUtils.deleteAllPosts(),
+ requestUtils.deleteAllMenus(),
+ ] );
+ } );
+
+ test( 'should auto-configure itself to sensible defaults when inserted into a Navigation block', async ( {
+ page,
+ editor,
+ requestUtils,
+ } ) => {
+ const createdMenu = await requestUtils.createNavigationMenu( {
+ title: 'Test Menu',
+ content: `
+
+`,
+ } );
+
+ await editor.insertBlock( {
+ name: 'core/navigation',
+ attributes: {
+ ref: createdMenu?.id,
+ },
+ } );
+
+ const navBlockInserter = editor.canvas.getByRole( 'button', {
+ name: 'Add block',
+ } );
+ await navBlockInserter.click();
+
+ // Expect to see the block inserter.
+ await expect(
+ page.getByRole( 'searchbox', {
+ name: 'Search for blocks and patterns',
+ } )
+ ).toBeFocused();
+
+ // Search for the Search block.
+ await page.keyboard.type( 'Search' );
+
+ const blockResults = page.getByRole( 'listbox', {
+ name: 'Blocks',
+ } );
+
+ await expect( blockResults ).toBeVisible();
+
+ const searchBlockResult = blockResults.getByRole( 'option', {
+ name: 'Search',
+ } );
+
+ // Select the Search block.
+ await searchBlockResult.click();
+
+ // Expect to see the Search block.
+ const searchBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Search',
+ } );
+
+ await expect( searchBlock ).toBeVisible();
+
+ // The only way to access the inner controlled blocks of the Navigation block
+ // is to access the edited entity record for the associated Navigation Menu record.
+ const editedMenuRecord = await page.evaluate( ( menuId ) => {
+ return window.wp.data
+ .select( 'core' )
+ .getEditedEntityRecord( 'postType', 'wp_navigation', menuId );
+ }, createdMenu?.id );
+
+ // The 2nd block in the Navigation block is the Search block.
+ const searchBlockAttributes = editedMenuRecord.blocks[ 1 ].attributes;
+
+ expect( searchBlockAttributes ).toMatchObject( {
+ showLabel: false,
+ buttonUseIcon: true,
+ buttonPosition: 'button-inside',
+ } );
+ } );
+} );
diff --git a/test/e2e/specs/editor/plugins/block-variations.spec.js b/test/e2e/specs/editor/plugins/block-variations.spec.js
index 302bac732023c5..0b445aee451c66 100644
--- a/test/e2e/specs/editor/plugins/block-variations.spec.js
+++ b/test/e2e/specs/editor/plugins/block-variations.spec.js
@@ -89,7 +89,7 @@ test.describe( 'Block variations', () => {
await page.keyboard.press( 'Enter' );
await expect(
- editor.canvas.getByRole( 'document', { name: 'Paragraph block' } )
+ editor.canvas.getByRole( 'document', { name: 'Block: Paragraph' } )
).toHaveText( 'This is a success message!' );
} );
diff --git a/test/e2e/specs/editor/plugins/hooks-api.spec.js b/test/e2e/specs/editor/plugins/hooks-api.spec.js
index 9af8e6570eef32..b3b5ed68f66f11 100644
--- a/test/e2e/specs/editor/plugins/hooks-api.spec.js
+++ b/test/e2e/specs/editor/plugins/hooks-api.spec.js
@@ -41,7 +41,7 @@ test.describe( 'Using Hooks API', () => {
await page.keyboard.type( 'First paragraph' );
const paragraphBlock = editor.canvas.locator(
- 'role=document[name="Paragraph block"i]'
+ 'role=document[name="Block: Paragraph"i]'
);
await expect( paragraphBlock ).toHaveText( 'First paragraph' );
await page.click(
diff --git a/test/e2e/specs/editor/plugins/iframed-enqueue-block-editor-settings.spec.js b/test/e2e/specs/editor/plugins/iframed-enqueue-block-editor-settings.spec.js
new file mode 100644
index 00000000000000..b4f502b2c9d0ba
--- /dev/null
+++ b/test/e2e/specs/editor/plugins/iframed-enqueue-block-editor-settings.spec.js
@@ -0,0 +1,99 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'iframed block editor settings styles', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activatePlugin(
+ 'gutenberg-test-iframed-enqueue-block-editor-settings'
+ );
+ } );
+
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost();
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.deactivatePlugin(
+ 'gutenberg-test-iframed-enqueue-block-editor-settings'
+ );
+ } );
+
+ test( 'should load styles added through block editor settings', async ( {
+ editor,
+ page,
+ } ) => {
+ const defaultBlock = editor.canvas.getByRole( 'button', {
+ name: 'Add default block',
+ } );
+
+ // Expect a red border (added in PHP).
+ await expect( defaultBlock ).toHaveCSS(
+ 'border-color',
+ 'rgb(255, 0, 0)'
+ );
+
+ await page.evaluate( () => {
+ const settings = window.wp.data
+ .select( 'core/editor' )
+ .getEditorSettings();
+ window.wp.data.dispatch( 'core/editor' ).updateEditorSettings( {
+ ...settings,
+ styles: [
+ ...settings.styles,
+ {
+ css: 'p { border-width: 2px; }',
+ __unstableType: 'plugin',
+ },
+ ],
+ } );
+ } );
+
+ // Expect a 2px border (added in JS).
+ await expect( defaultBlock ).toHaveCSS( 'border-width', '2px' );
+ } );
+
+ test( 'should load theme styles added through block editor settings', async ( {
+ editor,
+ page,
+ } ) => {
+ const defaultBlock = editor.canvas.getByRole( 'button', {
+ name: 'Add default block',
+ } );
+
+ await page.evaluate( () => {
+ // Make sure that theme styles are added even if the theme styles
+ // preference is off.
+ window.wp.data
+ .dispatch( 'core/edit-post' )
+ .toggleFeature( 'themeStyles' );
+ const settings = window.wp.data
+ .select( 'core/editor' )
+ .getEditorSettings();
+ window.wp.data.dispatch( 'core/editor' ).updateEditorSettings( {
+ ...settings,
+ styles: [
+ ...settings.styles,
+ {
+ css: 'p { border-width: 2px; }',
+ __unstableType: 'theme',
+ },
+ ],
+ } );
+ } );
+
+ // Expect a 1px border because theme styles are disabled.
+ await expect( defaultBlock ).toHaveCSS( 'border-width', '1px' );
+
+ await page.evaluate( () => {
+ // Now enable theme styles.
+ window.wp.data
+ .dispatch( 'core/edit-post' )
+ .toggleFeature( 'themeStyles' );
+ } );
+
+ // Expect a 2px border because theme styles are enabled.
+ await expect( defaultBlock ).toHaveCSS( 'border-width', '2px' );
+ } );
+} );
diff --git a/test/e2e/specs/editor/plugins/iframed-equeue-block-assets.spec.js b/test/e2e/specs/editor/plugins/iframed-equeue-block-assets.spec.js
index 391e1fdc17ec72..a523ba1f8e9e3d 100644
--- a/test/e2e/specs/editor/plugins/iframed-equeue-block-assets.spec.js
+++ b/test/e2e/specs/editor/plugins/iframed-equeue-block-assets.spec.js
@@ -3,7 +3,7 @@
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
-test.describe( 'iframed inline styles', () => {
+test.describe( 'iframed enqueue block assets', () => {
test.beforeAll( async ( { requestUtils } ) => {
await Promise.all( [
requestUtils.activateTheme( 'emptytheme' ),
diff --git a/test/e2e/specs/editor/plugins/iframed-inline-styles.spec.js b/test/e2e/specs/editor/plugins/iframed-inline-styles.spec.js
new file mode 100644
index 00000000000000..8fa4cbffa9df8c
--- /dev/null
+++ b/test/e2e/specs/editor/plugins/iframed-inline-styles.spec.js
@@ -0,0 +1,46 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'iframed inline styles', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activatePlugin(
+ 'gutenberg-test-iframed-inline-styles'
+ );
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.deactivatePlugin(
+ 'gutenberg-test-iframed-inline-styles'
+ );
+ } );
+
+ test( 'should load inline styles in iframe', async ( {
+ admin,
+ editor,
+ page,
+ } ) => {
+ let hasWarning;
+ page.on( 'console', ( msg ) => {
+ if ( msg.type() === 'warning' ) {
+ hasWarning = true;
+ }
+ } );
+
+ await admin.createNewPost( { postType: 'page' } );
+ await editor.insertBlock( { name: 'test/iframed-inline-styles' } );
+
+ const block = editor.canvas.getByRole( 'document', {
+ name: 'Block: Iframed Inline Styles',
+ } );
+ await expect( block ).toBeVisible();
+
+ // Inline styles of properly enqueued stylesheet should load.
+ await expect( block ).toHaveCSS( 'padding', '20px' );
+
+ // Inline styles of stylesheet loaded with the compatibility layer should load.
+ await expect( block ).toHaveCSS( 'border-width', '2px' );
+ expect( hasWarning ).toBe( true );
+ } );
+} );
diff --git a/test/e2e/specs/editor/plugins/iframed-masonry-block.spec.js b/test/e2e/specs/editor/plugins/iframed-masonry-block.spec.js
new file mode 100644
index 00000000000000..edc6b0a68cd4ab
--- /dev/null
+++ b/test/e2e/specs/editor/plugins/iframed-masonry-block.spec.js
@@ -0,0 +1,44 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'iframed masonry block', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activatePlugin(
+ 'gutenberg-test-iframed-masonry-block'
+ );
+ } );
+
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost();
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.deactivatePlugin(
+ 'gutenberg-test-iframed-masonry-block'
+ );
+ } );
+
+ test( 'should load script and dependencies in iframe', async ( {
+ editor,
+ } ) => {
+ await editor.insertBlock( { name: 'test/iframed-masonry-block' } );
+
+ const masonry = editor.canvas.getByRole( 'document', {
+ name: 'Block: Iframed Masonry Block',
+ } );
+ await expect( masonry ).toBeVisible();
+
+ const masonryBox = await masonry.boundingBox();
+
+ // Expect Masonry to set a non-zero height.
+ expect( masonryBox.height ).toBeGreaterThan( 0 );
+
+ // Expect Masonry to absolute position items.
+ await expect( masonry.locator( '.grid-item' ).first() ).toHaveCSS(
+ 'position',
+ 'absolute'
+ );
+ } );
+} );
diff --git a/test/e2e/specs/editor/plugins/iframed-multiple-block-stylesheets.spec.js b/test/e2e/specs/editor/plugins/iframed-multiple-block-stylesheets.spec.js
new file mode 100644
index 00000000000000..2e6f5b6b0cb6a0
--- /dev/null
+++ b/test/e2e/specs/editor/plugins/iframed-multiple-block-stylesheets.spec.js
@@ -0,0 +1,42 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'iframed multiple block stylesheets', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activatePlugin(
+ 'gutenberg-test-iframed-multiple-stylesheets'
+ );
+ } );
+
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost( { postType: 'page' } );
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.deactivatePlugin(
+ 'gutenberg-test-iframed-multiple-stylesheets'
+ );
+ } );
+
+ test( 'should load multiple block stylesheets in iframe', async ( {
+ editor,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'test/iframed-multiple-stylesheets',
+ } );
+ const block = editor.canvas.getByRole( 'document', {
+ name: 'Block: Iframed Multiple Stylesheets',
+ } );
+
+ await expect( block ).toBeVisible();
+
+ // Style loaded from the main stylesheet.
+ await expect( block ).toHaveCSS( 'border-style', 'dashed' );
+ // Style loaded from the additional stylesheet.
+ await expect( block ).toHaveCSS( 'border-color', 'rgb(255, 0, 0)' );
+ // Style loaded from the a stylesheet using path instead of handle.
+ await expect( block ).toHaveCSS( 'border-color', 'rgb(255, 0, 0)' );
+ } );
+} );
diff --git a/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js b/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js
index 13720de509e3c8..c5eafdafe918db 100644
--- a/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js
+++ b/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js
@@ -20,31 +20,26 @@ test.describe( 'WP Editor Meta Boxes', () => {
await admin.createNewPost();
// Add title to enable valid non-empty post save.
- await editor.canvas.type(
- 'role=textbox[name="Add title"i]',
- 'Hello Meta'
- );
+ await editor.canvas
+ .locator( 'role=textbox[name="Add title"i]' )
+ .type( 'Hello Meta' );
- // Type something.
- await page.click( 'role=button[name="Text"i]' );
- await page.click( '#test_tinymce_id' );
- await page.keyboard.type( 'Typing in a metabox' );
- await page.type( '#test_tinymce_id-html', 'Typing in a metabox' );
- await page.click( 'role=button[name="Visual"i]' );
+ // Switch tinymce to Text mode, first waiting for it to initialize
+ // because otherwise it will flip back to Visual mode once initialized.
+ await page.locator( '#test_tinymce_id_ifr' ).waitFor();
+ await page.locator( 'role=button[name="Text"i]' ).click();
- await editor.publishPost();
+ // Type something in the tinymce Text mode textarea.
+ const metaBoxField = page.locator( '#test_tinymce_id' );
+ await metaBoxField.type( 'Typing in a metabox' );
- // Close the publish panel so that it won't cover the tinymce editor.
- await page.click(
- 'role=region[name="Editor publish"i] >> role=button[name="Close panel"i]'
- );
+ // Switch tinymce back to Visual mode.
+ await page.locator( 'role=button[name="Visual"i]' ).click();
- await expect( page.locator( '.edit-post-layout' ) ).toBeVisible();
-
- await page.click( 'role=button[name="Text"i]' );
+ await editor.publishPost();
+ await page.reload();
- // Expect the typed text on the tinymce editor
- const content = page.locator( '#test_tinymce_id' );
- await expect( content ).toHaveValue( 'Typing in a metabox' );
+ // Expect the typed text in the tinymce Text mode textarea.
+ await expect( metaBoxField ).toHaveValue( 'Typing in a metabox' );
} );
} );
diff --git a/test/e2e/specs/editor/various/block-deletion.spec.js b/test/e2e/specs/editor/various/block-deletion.spec.js
index 084cd008cdc44c..9346412c46bcb2 100644
--- a/test/e2e/specs/editor/various/block-deletion.spec.js
+++ b/test/e2e/specs/editor/various/block-deletion.spec.js
@@ -30,7 +30,7 @@ test.describe( 'Block deletion', () => {
await expect(
editor.canvas
.getByRole( 'document', {
- name: 'Paragraph block',
+ name: 'Block: Paragraph',
} )
.last()
).toBeFocused();
@@ -78,7 +78,7 @@ test.describe( 'Block deletion', () => {
// Select the paragraph.
const paragraph = editor.canvas.getByRole( 'document', {
- name: 'Paragraph block',
+ name: 'Block: Paragraph',
} );
await editor.selectBlocks( paragraph );
@@ -128,7 +128,7 @@ test.describe( 'Block deletion', () => {
await expect(
editor.canvas
.getByRole( 'document', {
- name: 'Paragraph block',
+ name: 'Block: Paragraph',
} )
.last()
).toBeFocused();
@@ -307,7 +307,7 @@ test.describe( 'Block deletion', () => {
} );
await expect(
editor.canvas.getByRole( 'document', {
- name: 'Paragraph block',
+ name: 'Block: Paragraph',
} )
).toBeFocused();
diff --git a/test/e2e/specs/editor/various/block-locking.spec.js b/test/e2e/specs/editor/various/block-locking.spec.js
index 481c476ac065bd..67a7f0357bc2a7 100644
--- a/test/e2e/specs/editor/various/block-locking.spec.js
+++ b/test/e2e/specs/editor/various/block-locking.spec.js
@@ -104,7 +104,7 @@ test.describe( 'Block Locking', () => {
} );
const paragraph = editor.canvas.getByRole( 'document', {
- name: 'Paragraph block',
+ name: 'Block: Paragraph',
} );
await paragraph.click();
diff --git a/test/e2e/specs/editor/various/block-renaming.spec.js b/test/e2e/specs/editor/various/block-renaming.spec.js
index 8568258aaa4fda..1c8a958b23fd41 100644
--- a/test/e2e/specs/editor/various/block-renaming.spec.js
+++ b/test/e2e/specs/editor/various/block-renaming.spec.js
@@ -58,12 +58,16 @@ test.describe( 'Block Renaming', () => {
name: 'Rename',
} );
- // Check focus is transferred into modal.
- await expect( renameModal ).toBeFocused();
-
// Check the Modal is perceivable.
await expect( renameModal ).toBeVisible();
+ const nameInput = renameModal.getByRole( 'textbox', {
+ name: 'Block name',
+ } );
+
+ // Check focus is transferred into the input within the Modal.
+ await expect( nameInput ).toBeFocused();
+
const saveButton = renameModal.getByRole( 'button', {
name: 'Save',
type: 'submit',
@@ -71,8 +75,6 @@ test.describe( 'Block Renaming', () => {
await expect( saveButton ).toBeDisabled();
- const nameInput = renameModal.getByLabel( 'Block name' );
-
await expect( nameInput ).toHaveAttribute( 'placeholder', 'Group' );
await nameInput.fill( 'My new name' );
diff --git a/test/e2e/specs/editor/various/content-only-lock.spec.js b/test/e2e/specs/editor/various/content-only-lock.spec.js
index d6ea152d65f3ff..03282357a72b65 100644
--- a/test/e2e/specs/editor/various/content-only-lock.spec.js
+++ b/test/e2e/specs/editor/various/content-only-lock.spec.js
@@ -25,8 +25,53 @@ test.describe( 'Content-only lock', () => {
await pageUtils.pressKeys( 'secondary+M' );
await page.waitForSelector( 'iframe[name="editor-canvas"]' );
- await editor.canvas.click( 'role=document[name="Paragraph block"i]' );
+ await editor.canvas.click( 'role=document[name="Block: Paragraph"i]' );
await page.keyboard.type( ' World' );
expect( await editor.getEditedPostContent() ).toMatchSnapshot();
} );
+
+ // See: https://github.com/WordPress/gutenberg/pull/54618
+ test( 'should be able to edit the content of deeply nested blocks', async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ // Add content only locked block in the code editor
+ await pageUtils.pressKeys( 'secondary+M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor
+
+ await page.getByPlaceholder( 'Start writing with text or HTML' )
+ .fill( `
+
+` );
+
+ await pageUtils.pressKeys( 'secondary+M' );
+ await page.waitForSelector( 'iframe[name="editor-canvas"]' );
+ await editor.canvas.click( 'role=document[name="Block: Paragraph"i]' );
+ await page.keyboard.type( ' WP' );
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/group',
+ attributes: {
+ layout: { type: 'constrained' },
+ templateLock: 'contentOnly',
+ },
+ innerBlocks: [
+ {
+ name: 'core/group',
+ attributes: { layout: { type: 'constrained' } },
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ attributes: { content: 'Hello WP' },
+ },
+ ],
+ },
+ ],
+ },
+ ] );
+ } );
} );
diff --git a/test/e2e/specs/editor/various/draggable-blocks.spec.js b/test/e2e/specs/editor/various/draggable-blocks.spec.js
index cd5fa12fca6f4d..2d95e3bdefe975 100644
--- a/test/e2e/specs/editor/various/draggable-blocks.spec.js
+++ b/test/e2e/specs/editor/various/draggable-blocks.spec.js
@@ -43,7 +43,7 @@ test.describe( 'Draggable block', () => {
` );
await editor.canvas.focus(
- 'role=document[name="Paragraph block"i] >> text=2'
+ 'role=document[name="Block: Paragraph"i] >> text=2'
);
await editor.showBlockToolbar();
@@ -57,7 +57,7 @@ test.describe( 'Draggable block', () => {
// Move to and hover on the upper half of the paragraph block to trigger the indicator.
const firstParagraph = editor.canvas.locator(
- 'role=document[name="Paragraph block"i] >> text=1'
+ 'role=document[name="Block: Paragraph"i] >> text=1'
);
const firstParagraphBound = await firstParagraph.boundingBox();
// Call the move function twice to make sure the `dragOver` event is sent.
@@ -115,7 +115,7 @@ test.describe( 'Draggable block', () => {
` );
await editor.canvas.focus(
- 'role=document[name="Paragraph block"i] >> text=1'
+ 'role=document[name="Block: Paragraph"i] >> text=1'
);
await editor.showBlockToolbar();
@@ -129,7 +129,7 @@ test.describe( 'Draggable block', () => {
// Move to and hover on the bottom half of the paragraph block to trigger the indicator.
const secondParagraph = editor.canvas.locator(
- 'role=document[name="Paragraph block"i] >> text=2'
+ 'role=document[name="Block: Paragraph"i] >> text=2'
);
const secondParagraphBound = await secondParagraph.boundingBox();
// Call the move function twice to make sure the `dragOver` event is sent.
@@ -198,7 +198,7 @@ test.describe( 'Draggable block', () => {
} );
await editor.canvas.focus(
- 'role=document[name="Paragraph block"i] >> text=2'
+ 'role=document[name="Block: Paragraph"i] >> text=2'
);
await editor.showBlockToolbar();
@@ -212,7 +212,7 @@ test.describe( 'Draggable block', () => {
// Move to and hover on the left half of the paragraph block to trigger the indicator.
const firstParagraph = editor.canvas.locator(
- 'role=document[name="Paragraph block"i] >> text=1'
+ 'role=document[name="Block: Paragraph"i] >> text=1'
);
const firstParagraphBound = await firstParagraph.boundingBox();
// Call the move function twice to make sure the `dragOver` event is sent.
@@ -279,7 +279,7 @@ test.describe( 'Draggable block', () => {
} );
await editor.canvas.focus(
- 'role=document[name="Paragraph block"i] >> text=1'
+ 'role=document[name="Block: Paragraph"i] >> text=1'
);
await editor.showBlockToolbar();
@@ -293,7 +293,7 @@ test.describe( 'Draggable block', () => {
// Move to and hover on the right half of the paragraph block to trigger the indicator.
const secondParagraph = editor.canvas.locator(
- 'role=document[name="Paragraph block"i] >> text=2'
+ 'role=document[name="Block: Paragraph"i] >> text=2'
);
const secondParagraphBound = await secondParagraph.boundingBox();
// Call the move function twice to make sure the `dragOver` event is sent.
diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js
index e024964831dfe7..e3aa17c5a101cb 100644
--- a/test/e2e/specs/editor/various/footnotes.spec.js
+++ b/test/e2e/specs/editor/various/footnotes.spec.js
@@ -348,6 +348,7 @@ test.describe( 'Footnotes', () => {
await previewPage.close();
await editorPage.bringToFront();
+ await editor.canvas.click( 'p:text("first paragraph")' );
// Open revisions.
await editor.openDocumentSettingsSidebar();
@@ -391,9 +392,10 @@ test.describe( 'Footnotes', () => {
await page.keyboard.type( '1' );
- // Publish post.
- await editor.publishPost();
+ // Publish post with the footnote set to "1".
+ const postId = await editor.publishPost();
+ // Test previewing changes to meta.
await editor.canvas.click( 'ol.wp-block-footnotes li span' );
await page.keyboard.press( 'End' );
await page.keyboard.type( '2' );
@@ -408,8 +410,7 @@ test.describe( 'Footnotes', () => {
await previewPage.close();
await editorPage.bringToFront();
- // Test again, this time with an existing revision (different code
- // path).
+ // Test again, this time with an existing revision (different code path).
await editor.canvas.click( 'ol.wp-block-footnotes li span' );
await page.keyboard.press( 'End' );
// Test slashing.
@@ -421,5 +422,22 @@ test.describe( 'Footnotes', () => {
await expect(
previewPage2.locator( 'ol.wp-block-footnotes li' )
).toHaveText( '123″ ↩︎' );
+
+ // Verify that the published post is unchanged after previewing changes to meta.
+ await previewPage2.close();
+ await editorPage.bringToFront();
+ await editor.openDocumentSettingsSidebar();
+ await page
+ .getByRole( 'region', { name: 'Editor settings' } )
+ .getByRole( 'button', { name: 'Post' } )
+ .click();
+
+ // Visit the published post.
+ await page.goto( `/?p=${ postId }` );
+
+ // Verify that the published post footnote still says "1".
+ await expect( page.locator( 'ol.wp-block-footnotes li' ) ).toHaveText(
+ '1 ↩︎'
+ );
} );
} );
diff --git a/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js b/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js
index 3a75c1842834c5..8d1f37187fee18 100644
--- a/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js
+++ b/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js
@@ -54,7 +54,7 @@ test.describe( 'Keep styles on block transforms', () => {
await pageUtils.pressKeys( 'shift+ArrowUp' );
await pageUtils.pressKeys( 'shift+ArrowUp' );
await page.click( 'role=radio[name="Large"i]' );
- await page.click( 'role=button[name="Paragraph"i]' );
+ await page.click( 'role=button[name="Multiple blocks selected"i]' );
await page.click( 'role=menuitem[name="Heading"i]' );
await expect.poll( editor.getBlocks ).toMatchObject( [
diff --git a/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js
new file mode 100644
index 00000000000000..080abe011206a7
--- /dev/null
+++ b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js
@@ -0,0 +1,276 @@
+/* eslint-disable playwright/expect-expect */
+
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.use( {
+ KeyboardNavigableBlocks: async ( { editor, page, pageUtils }, use ) => {
+ await use( new KeyboardNavigableBlocks( { editor, page, pageUtils } ) );
+ },
+} );
+
+test.describe( 'Order of block keyboard navigation', () => {
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost();
+ } );
+
+ test( 'permits tabbing through paragraph blocks in the expected order', async ( {
+ editor,
+ KeyboardNavigableBlocks,
+ page,
+ } ) => {
+ const paragraphBlocks = [ 'Paragraph 0', 'Paragraph 1', 'Paragraph 2' ];
+
+ // Create 3 paragraphs blocks with some content.
+ for ( const paragraphBlock of paragraphBlocks ) {
+ await editor.insertBlock( { name: 'core/paragraph' } );
+ await page.keyboard.type( paragraphBlock );
+ }
+
+ // Select the middle block.
+ await page.keyboard.press( 'ArrowUp' );
+ await editor.showBlockToolbar();
+ await KeyboardNavigableBlocks.navigateToContentEditorTop();
+ await KeyboardNavigableBlocks.tabThroughParagraphBlock( 'Paragraph 1' );
+
+ // Repeat the same steps to ensure that there is no change introduced in how the focus is handled.
+ // This prevents the previous regression explained in: https://github.com/WordPress/gutenberg/issues/11773.
+ await KeyboardNavigableBlocks.navigateToContentEditorTop();
+ await KeyboardNavigableBlocks.tabThroughParagraphBlock( 'Paragraph 1' );
+ } );
+
+ test( 'allows tabbing in navigation mode if no block is selected', async ( {
+ editor,
+ KeyboardNavigableBlocks,
+ page,
+ } ) => {
+ const paragraphBlocks = [ '0', '1' ];
+
+ // Create 2 paragraphs blocks with some content.
+ for ( const paragraphBlock of paragraphBlocks ) {
+ await editor.insertBlock( { name: 'core/paragraph' } );
+ await page.keyboard.type( paragraphBlock );
+ }
+
+ // Clear the selected block.
+ const paragraph = editor.canvas
+ .locator( '[data-type="core/paragraph"]' )
+ .getByText( '1' );
+ const box = await paragraph.boundingBox();
+ await page.mouse.click( box.x - 1, box.y );
+
+ await page.keyboard.press( 'Tab' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Add title' );
+
+ await page.keyboard.press( 'Tab' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus(
+ 'Paragraph Block. Row 1. 0'
+ );
+
+ await page.keyboard.press( 'Tab' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus(
+ 'Paragraph Block. Row 2. 1'
+ );
+
+ await page.keyboard.press( 'Tab' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus(
+ 'Post (selected)'
+ );
+ } );
+
+ test( 'allows tabbing in navigation mode if no block is selected (reverse)', async ( {
+ editor,
+ KeyboardNavigableBlocks,
+ page,
+ pageUtils,
+ } ) => {
+ const paragraphBlocks = [ '0', '1' ];
+
+ // Create 2 paragraphs blocks with some content.
+ for ( const paragraphBlock of paragraphBlocks ) {
+ await editor.insertBlock( { name: 'core/paragraph' } );
+ await page.keyboard.type( paragraphBlock );
+ }
+
+ // Clear the selected block.
+ const paragraph = editor.canvas
+ .locator( '[data-type="core/paragraph"]' )
+ .getByText( '1' );
+ const box = await paragraph.boundingBox();
+ await page.mouse.click( box.x - 1, box.y );
+
+ // Put focus behind the block list.
+ await page.evaluate( () => {
+ document
+ .querySelector( '.interface-interface-skeleton__sidebar' )
+ .focus();
+ } );
+
+ await pageUtils.pressKeys( 'shift+Tab' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Add block' );
+
+ await pageUtils.pressKeys( 'shift+Tab' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus(
+ 'Add default block'
+ );
+
+ await pageUtils.pressKeys( 'shift+Tab' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus(
+ 'Paragraph Block. Row 2. 1'
+ );
+
+ await pageUtils.pressKeys( 'shift+Tab' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus(
+ 'Paragraph Block. Row 1. 0'
+ );
+
+ await pageUtils.pressKeys( 'shift+Tab' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Add title' );
+ } );
+
+ test( 'should navigate correctly with multi selection', async ( {
+ editor,
+ KeyboardNavigableBlocks,
+ page,
+ pageUtils,
+ } ) => {
+ const paragraphBlocks = [ '0', '1', '2', '3' ];
+
+ // Create 4 paragraphs blocks with some content.
+ for ( const paragraphBlock of paragraphBlocks ) {
+ await editor.insertBlock( { name: 'core/paragraph' } );
+ await page.keyboard.type( paragraphBlock );
+ }
+ await page.keyboard.press( 'ArrowUp' );
+ await pageUtils.pressKeys( 'shift+ArrowUp' );
+
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus(
+ 'Multiple selected blocks'
+ );
+
+ await page.keyboard.press( 'Tab' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Post' );
+
+ await pageUtils.pressKeys( 'shift+Tab' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus(
+ 'Multiple selected blocks'
+ );
+
+ await pageUtils.pressKeys( 'shift+Tab' );
+ await page.keyboard.press( 'ArrowRight' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Move up' );
+ } );
+
+ test( 'allows the first element within a block to receive focus', async ( {
+ editor,
+ KeyboardNavigableBlocks,
+ page,
+ } ) => {
+ // Insert a image block.
+ await editor.insertBlock( { name: 'core/image' } );
+
+ // Make sure the upload button has focus.
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Upload' );
+
+ // Try to focus the image block wrapper.
+ await page.keyboard.press( 'ArrowUp' );
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Block: Image' );
+ } );
+
+ test( 'allows the block wrapper to gain focus for a group block instead of the first element', async ( {
+ editor,
+ KeyboardNavigableBlocks,
+ } ) => {
+ // Insert a group block.
+ await editor.insertBlock( { name: 'core/group' } );
+ // Select the default, selected Group layout from the variation picker.
+ const groupButton = editor.canvas.locator(
+ 'button[aria-label="Group: Gather blocks in a container."]'
+ );
+
+ await groupButton.click();
+
+ // If active label matches, that means focus did not change from group block wrapper.
+ await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Block: Group' );
+ } );
+} );
+
+class KeyboardNavigableBlocks {
+ constructor( { editor, page, pageUtils } ) {
+ this.editor = editor;
+ this.page = page;
+ this.pageUtils = pageUtils;
+ }
+
+ async expectLabelToHaveFocus( label ) {
+ const ariaLabel = await this.page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement.contentDocument ?? document;
+ return (
+ activeElement.getAttribute( 'aria-label' ) ||
+ activeElement.innerText
+ );
+ } );
+
+ expect( ariaLabel ).toBe( label );
+ }
+
+ async navigateToContentEditorTop() {
+ // Use 'Ctrl+`' to return to the top of the editor.
+ await this.pageUtils.pressKeys( 'ctrl+`', { times: 5 } );
+ }
+
+ async tabThroughParagraphBlock( paragraphText ) {
+ await this.tabThroughBlockToolbar();
+
+ await this.page.keyboard.press( 'Tab' );
+ await this.expectLabelToHaveFocus( 'Block: Paragraph' );
+
+ const activeElement = this.editor.canvas.locator( ':focus' );
+
+ await expect( activeElement ).toHaveText( paragraphText );
+
+ await this.page.keyboard.press( 'Tab' );
+ await this.expectLabelToHaveFocus( 'Post' );
+
+ // 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' );
+ await this.expectLabelToHaveFocus( 'Block: Paragraph' );
+ }
+
+ async tabThroughBlockToolbar() {
+ await this.page.keyboard.press( 'Tab' );
+ await this.expectLabelToHaveFocus( 'Paragraph' );
+
+ await this.page.keyboard.press( 'ArrowRight' );
+ await this.expectLabelToHaveFocus( 'Move up' );
+
+ await this.page.keyboard.press( 'ArrowRight' );
+ await this.expectLabelToHaveFocus( 'Move down' );
+
+ await this.page.keyboard.press( 'ArrowRight' );
+ await this.expectLabelToHaveFocus( 'Align text' );
+
+ await this.page.keyboard.press( 'ArrowRight' );
+ await this.expectLabelToHaveFocus( 'Bold' );
+
+ await this.page.keyboard.press( 'ArrowRight' );
+ await this.expectLabelToHaveFocus( 'Italic' );
+
+ await this.page.keyboard.press( 'ArrowRight' );
+ await this.expectLabelToHaveFocus( 'Link' );
+
+ await this.page.keyboard.press( 'ArrowRight' );
+ await this.expectLabelToHaveFocus( 'More' );
+
+ await this.page.keyboard.press( 'ArrowRight' );
+ await this.expectLabelToHaveFocus( 'Options' );
+
+ await this.page.keyboard.press( 'ArrowRight' );
+ await this.expectLabelToHaveFocus( 'Paragraph' );
+ }
+}
+
+/* eslint-enable playwright/expect-expect */
diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js
index 37dc300c2e39b4..7fd21085deae2a 100644
--- a/test/e2e/specs/editor/various/list-view.spec.js
+++ b/test/e2e/specs/editor/various/list-view.spec.js
@@ -275,7 +275,7 @@ test.describe( 'List View', () => {
} );
await expect(
editor.canvas.getByRole( 'document', {
- name: 'Paragraph block',
+ name: 'Block: Paragraph',
} )
).toBeFocused();
@@ -322,12 +322,9 @@ test.describe( 'List View', () => {
// the sidebar.
await pageUtils.pressKeys( 'access+o' );
- // Focus should now be on the paragraph block since that is
- // where we opened the list view sidebar. This is not a perfect
- // solution, but current functionality prevents a better way at
- // the moment.
+ // Focus should now be on the list view toggle button.
await expect(
- editor.canvas.getByRole( 'document', { name: 'Paragraph block' } )
+ page.getByRole( 'button', { name: 'Document Overview' } )
).toBeFocused();
// List View should be closed.
diff --git a/test/e2e/specs/editor/various/multi-block-selection.spec.js b/test/e2e/specs/editor/various/multi-block-selection.spec.js
index 3f2c0e38d7b954..3374a13b98eb79 100644
--- a/test/e2e/specs/editor/various/multi-block-selection.spec.js
+++ b/test/e2e/specs/editor/various/multi-block-selection.spec.js
@@ -162,7 +162,7 @@ test.describe( 'Multi-block selection', () => {
} );
}
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.filter( { hasText: '3' } )
.click();
@@ -262,7 +262,7 @@ test.describe( 'Multi-block selection', () => {
await page.keyboard.type( '2' );
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.filter( { hasText: '1' } )
.click( { modifiers: [ 'Shift' ] } );
@@ -279,11 +279,11 @@ test.describe( 'Multi-block selection', () => {
.getByRole( 'button', { name: 'Group' } )
.click();
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.filter( { hasText: '1' } )
.click();
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.filter( { hasText: '2' } )
.click( { modifiers: [ 'Shift' ] } );
@@ -327,7 +327,7 @@ test.describe( 'Multi-block selection', () => {
} );
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.click( { modifiers: [ 'Shift' ] } );
await pageUtils.pressKeys( 'primary+a' );
await page.keyboard.type( 'new content' );
@@ -360,12 +360,12 @@ test.describe( 'Multi-block selection', () => {
} );
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.filter( { hasText: 'group' } )
.nth( 1 )
.click();
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.filter( { hasText: 'first' } )
.click( { modifiers: [ 'Shift' ] } );
@@ -389,7 +389,7 @@ test.describe( 'Multi-block selection', () => {
} );
const paragraphBlock = editor.canvas.getByRole( 'document', {
- name: 'Paragraph block',
+ name: 'Block: Paragraph',
} );
const { height } = await paragraphBlock.boundingBox();
await paragraphBlock.click( { position: { x: 0, y: height / 2 } } );
@@ -429,7 +429,7 @@ test.describe( 'Multi-block selection', () => {
await page.keyboard.press( 'ArrowDown' );
const [ paragraph1, paragraph2 ] = await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.all();
await paragraph1.hover();
@@ -466,7 +466,7 @@ test.describe( 'Multi-block selection', () => {
await page.keyboard.press( 'ArrowDown' );
const [ paragraph1, paragraph2 ] = await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.all();
await paragraph1.hover();
@@ -644,7 +644,7 @@ test.describe( 'Multi-block selection', () => {
}
const [ , paragraph2, paragraph3 ] = await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.all();
// Focus the last paragraph block and hide the block toolbar.
@@ -700,7 +700,7 @@ test.describe( 'Multi-block selection', () => {
const paragraph1 = editor.canvas
.getByRole( 'document', {
- name: 'Paragraph block',
+ name: 'Block: Paragraph',
} )
.filter( { hasText: '1' } );
await paragraph1.click( {
@@ -803,7 +803,7 @@ test.describe( 'Multi-block selection', () => {
} );
// Focus the last paragraph block.
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.nth( 1 )
.click();
@@ -1162,7 +1162,7 @@ test.describe( 'Multi-block selection', () => {
// Focus and move the caret to the right of the first paragraph.
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.filter( { hasText: 'a' } )
.click();
@@ -1200,7 +1200,7 @@ test.describe( 'Multi-block selection', () => {
} );
// Focus and move the caret to the end.
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.filter( { hasText: ']2' } )
.click();
@@ -1211,7 +1211,7 @@ test.describe( 'Multi-block selection', () => {
const strongBox = await strongText.boundingBox();
// Focus and move the caret to the end.
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.filter( { hasText: '1[' } )
.click( {
// Ensure clicking on the right half of the element.
@@ -1296,7 +1296,7 @@ test.describe( 'Multi-block selection', () => {
await page.keyboard.press( 'ArrowUp' );
await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
+ .getByRole( 'document', { name: 'Block: Paragraph' } )
.hover();
await page.mouse.down();
await editor.canvas
diff --git a/test/e2e/specs/editor/various/patterns.spec.js b/test/e2e/specs/editor/various/patterns.spec.js
new file mode 100644
index 00000000000000..941ae7e910a9b2
--- /dev/null
+++ b/test/e2e/specs/editor/various/patterns.spec.js
@@ -0,0 +1,83 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Unsynced pattern', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.deleteAllBlocks();
+ await requestUtils.deleteAllPatternCategories();
+ } );
+
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost();
+ } );
+
+ test.afterEach( async ( { requestUtils } ) => {
+ await requestUtils.deleteAllBlocks();
+ await requestUtils.deleteAllPatternCategories();
+ } );
+
+ test( 'create a new unsynced pattern via the block options menu', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: { content: 'A useful paragraph to reuse' },
+ } );
+ const before = await editor.getBlocks();
+
+ // Create an unsynced pattern from the paragraph block.
+ await editor.showBlockToolbar();
+ await page
+ .getByRole( 'toolbar', { name: 'Block tools' } )
+ .getByRole( 'button', { name: 'Options' } )
+ .click();
+ await page.getByRole( 'menuitem', { name: 'Create pattern' } ).click();
+
+ const createPatternDialog = page.getByRole( 'dialog', {
+ name: 'Create pattern',
+ } );
+ await createPatternDialog
+ .getByRole( 'textbox', { name: 'Name' } )
+ .fill( 'My unsynced pattern' );
+ const newCategory = 'Contact details';
+ await createPatternDialog
+ .getByRole( 'combobox', { name: 'Categories' } )
+ .fill( newCategory );
+ await createPatternDialog
+ .getByRole( 'checkbox', { name: 'Synced' } )
+ .setChecked( false );
+
+ await page.keyboard.press( 'Enter' );
+
+ // Check that the block content is still the same. If the pattern was added as synced
+ // the content would be wrapped by a pattern block.
+ await expect
+ .poll(
+ editor.getBlocks,
+ 'The block content should be the same after converting to an unsynced pattern'
+ )
+ .toEqual( before );
+
+ // Check that the new pattern is available in the inserter and that it gets inserted as
+ // a plain paragraph block.
+ await page.getByLabel( 'Toggle block inserter' ).click();
+ await page
+ .getByRole( 'tab', {
+ name: 'Patterns',
+ } )
+ .click();
+ await page
+ .getByRole( 'button', {
+ name: newCategory,
+ } )
+ .click();
+ await page.getByLabel( 'My unsynced pattern' ).click();
+
+ await expect
+ .poll( editor.getBlocks )
+ .toEqual( [ ...before, ...before ] );
+ } );
+} );
diff --git a/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js b/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js
index d17cef9215f4ae..23186dfff2de96 100644
--- a/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js
+++ b/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js
@@ -14,6 +14,10 @@ test.describe( 'Toolbar roving tabindex', () => {
await admin.createNewPost();
await editor.insertBlock( { name: 'core/paragraph' } );
await page.keyboard.type( 'First block' );
+
+ // Ensure the fixed toolbar option is off.
+ // See: https://github.com/WordPress/gutenberg/pull/54785.
+ await editor.setIsFixedToolbar( false );
} );
test( 'ensures base block toolbars use roving tabindex', async ( {
@@ -26,14 +30,14 @@ test.describe( 'Toolbar roving tabindex', () => {
await editor.insertBlock( { name: 'core/paragraph' } );
await page.keyboard.type( 'Paragraph' );
await ToolbarRovingTabindexUtils.testBlockToolbarKeyboardNavigation(
- 'Paragraph block',
+ 'Block: Paragraph',
'Paragraph'
);
await ToolbarRovingTabindexUtils.wrapCurrentBlockWithGroup(
'Paragraph'
);
await ToolbarRovingTabindexUtils.testGroupKeyboardNavigation(
- 'Paragraph block',
+ 'Block: Paragraph',
'Paragraph'
);
diff --git a/test/e2e/specs/editor/various/writing-flow.spec.js b/test/e2e/specs/editor/various/writing-flow.spec.js
index 44ffb33e7a00cf..81bbcbbc758b93 100644
--- a/test/e2e/specs/editor/various/writing-flow.spec.js
+++ b/test/e2e/specs/editor/various/writing-flow.spec.js
@@ -385,7 +385,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => {
// Should navigate to the next block.
await page.keyboard.press( 'ArrowDown' );
await expect(
- editor.canvas.locator( 'role=document[name="Paragraph block"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Paragraph"i]' )
).toHaveClass( /is-selected/ );
} );
@@ -805,7 +805,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => {
await page.keyboard.press( 'ArrowUp' );
const paragraphBlock = editor.canvas
- .locator( 'role=document[name="Paragraph block"i]' )
+ .locator( 'role=document[name="Block: Paragraph"i]' )
.first();
const paragraphRect = await paragraphBlock.boundingBox();
const x = paragraphRect.x + ( 2 * paragraphRect.width ) / 3;
@@ -870,7 +870,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => {
` );
const paragraphBlock = editor.canvas.locator(
- 'role=document[name="Paragraph block"i]'
+ 'role=document[name="Block: Paragraph"i]'
);
// Find a point outside the paragraph between the blocks where it's
@@ -975,7 +975,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => {
await page.mouse.up();
await expect(
- editor.canvas.locator( 'role=document[name="Paragraph block"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Paragraph"i]' )
).toHaveClass( /is-selected/ );
} );
@@ -1037,7 +1037,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => {
// Expect the "." to be added at the start of the paragraph
await expect(
- editor.canvas.locator( 'role=document[name="Paragraph block"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Paragraph"i]' )
).toHaveText( /^\.a+$/ );
} );
@@ -1071,7 +1071,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => {
// Expect the "." to be added at the start of the paragraph
await expect(
- editor.canvas.locator( 'role=document[name="Paragraph block"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Paragraph"i]' )
).toHaveText( /^a+\.a$/ );
} );
@@ -1107,7 +1107,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => {
// Expect the "." to be added at the start of the paragraph
await expect(
editor.canvas.locator(
- 'role=document[name="Paragraph block"i] >> nth = 0'
+ 'role=document[name="Block: Paragraph"i] >> nth = 0'
)
).toHaveText( /^.a+$/ );
} );
diff --git a/test/e2e/specs/site-editor/global-styles-sidebar.spec.js b/test/e2e/specs/site-editor/global-styles-sidebar.spec.js
new file mode 100644
index 00000000000000..02717f1b194327
--- /dev/null
+++ b/test/e2e/specs/site-editor/global-styles-sidebar.spec.js
@@ -0,0 +1,49 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Global styles sidebar', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'emptytheme' );
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'twentytwentyone' );
+ } );
+
+ test.beforeEach( async ( { admin, editor } ) => {
+ await admin.visitSiteEditor( {
+ postId: 'emptytheme//index',
+ postType: 'wp_template',
+ } );
+ await editor.canvas.click( 'body' );
+ } );
+
+ test( 'should filter blocks list results', async ( { page } ) => {
+ // Navigate to Styles -> Blocks.
+ await page
+ .getByRole( 'region', { name: 'Editor top bar' } )
+ .getByRole( 'button', { name: 'Styles' } )
+ .click();
+ await page
+ .getByRole( 'region', { name: 'Editor settings' } )
+ .getByRole( 'button', { name: 'Blocks styles' } )
+ .click();
+
+ await page
+ .getByRole( 'searchbox', { name: 'Search for blocks' } )
+ .fill( 'heading' );
+
+ // Matches both Heading and Table of Contents blocks.
+ // The latter contains "heading" in its description.
+ await expect(
+ page.getByRole( 'button', { name: 'Heading block styles' } )
+ ).toBeVisible();
+ await expect(
+ page.getByRole( 'button', {
+ name: 'Table of Contents block styles',
+ } )
+ ).toBeVisible();
+ } );
+} );
diff --git a/test/e2e/specs/site-editor/list-view.spec.js b/test/e2e/specs/site-editor/list-view.spec.js
index 94bac47d0ecf58..3057feaf7772b0 100644
--- a/test/e2e/specs/site-editor/list-view.spec.js
+++ b/test/e2e/specs/site-editor/list-view.spec.js
@@ -107,11 +107,10 @@ test.describe( 'Site Editor List View', () => {
await pageUtils.pressKeys( 'access+o' );
await expect( listView ).not.toBeVisible();
- // Focus should now be on the Open Navigation button since that is
- // where we opened the list view sidebar. This is not a perfect
- // solution, but current functionality prevents a better way at
- // the moment.
- await expect( openNavigationButton ).toBeFocused();
+ // Focus should now be on the list view toggle button.
+ await expect(
+ page.getByRole( 'button', { name: 'List View' } )
+ ).toBeFocused();
// Open List View.
await pageUtils.pressKeys( 'access+o' );
@@ -131,6 +130,8 @@ test.describe( 'Site Editor List View', () => {
).toBeFocused();
await pageUtils.pressKeys( 'access+o' );
await expect( listView ).not.toBeVisible();
- await expect( openNavigationButton ).toBeFocused();
+ await expect(
+ page.getByRole( 'button', { name: 'List View' } )
+ ).toBeFocused();
} );
} );
diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js
index e6a0b7f266cbf4..d1f32b9f209d75 100644
--- a/test/e2e/specs/site-editor/pages.spec.js
+++ b/test/e2e/specs/site-editor/pages.spec.js
@@ -190,7 +190,7 @@ test.describe( 'Pages', () => {
await templateOptionsButton.click();
const resetButton = page
.getByRole( 'menu', { name: 'Template options' } )
- .getByText( 'Reset' );
+ .getByText( 'Use default template' );
await expect( resetButton ).toBeVisible();
await resetButton.click();
await expect( templateOptionsButton ).toHaveText( 'Single Entries' );
diff --git a/test/e2e/specs/site-editor/patterns.spec.js b/test/e2e/specs/site-editor/patterns.spec.js
new file mode 100644
index 00000000000000..79bd8b782ecc41
--- /dev/null
+++ b/test/e2e/specs/site-editor/patterns.spec.js
@@ -0,0 +1,255 @@
+/**
+ * WordPress dependencies
+ */
+const {
+ test: base,
+ expect,
+} = require( '@wordpress/e2e-test-utils-playwright' );
+
+/** @type {ReturnType>} */
+const test = base.extend( {
+ patterns: async ( { page }, use ) => {
+ await use( new Patterns( { page } ) );
+ },
+} );
+
+test.describe( 'Patterns', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'emptytheme' );
+ await requestUtils.deleteAllBlocks();
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'twentytwentyone' );
+ } );
+
+ test.afterEach( async ( { requestUtils } ) => {
+ await requestUtils.deleteAllBlocks();
+ } );
+
+ test( 'create a new pattern', async ( {
+ page,
+ editor,
+ admin,
+ patterns,
+ } ) => {
+ await admin.visitSiteEditor();
+
+ await patterns.navigation
+ .getByRole( 'button', { name: 'Patterns' } )
+ .click();
+
+ await expect(
+ patterns.navigation.getByRole( 'heading', {
+ name: 'Patterns',
+ level: 1,
+ } )
+ ).toBeVisible();
+ await expect( patterns.content ).toContainText( 'No patterns found.' );
+
+ await patterns.navigation
+ .getByRole( 'button', { name: 'Create pattern' } )
+ .click();
+
+ const createPatternMenu = page.getByRole( 'menu', {
+ name: 'Create pattern',
+ } );
+ await expect(
+ createPatternMenu.getByRole( 'menuitem', {
+ name: 'Create pattern',
+ } )
+ ).toBeFocused();
+ await createPatternMenu
+ .getByRole( 'menuitem', { name: 'Create pattern' } )
+ .click();
+
+ const createPatternDialog = page.getByRole( 'dialog', {
+ name: 'Create pattern',
+ } );
+ await createPatternDialog
+ .getByRole( 'textbox', { name: 'Name' } )
+ .fill( 'My pattern' );
+ await page.keyboard.press( 'Enter' );
+ await expect(
+ createPatternDialog.getByRole( 'button', { name: 'Create' } )
+ ).toBeDisabled();
+
+ await expect( page ).toHaveTitle( /^My pattern/ );
+ await expect(
+ page
+ .getByRole( 'region', { name: 'Editor top bar' } )
+ .getByRole( 'heading', { name: 'My pattern', level: 1 } )
+ ).toBeVisible();
+
+ await editor.canvas
+ .getByRole( 'button', { name: 'Add default block' } )
+ .click();
+ await page.keyboard.type( 'My pattern' );
+
+ await page
+ .getByRole( 'region', { name: 'Editor top bar' } )
+ .getByRole( 'button', { name: 'Save' } )
+ .click();
+ await page
+ .getByRole( 'region', { name: 'Save panel' } )
+ .getByRole( 'button', { name: 'Save' } )
+ .click();
+ await expect(
+ page.getByRole( 'button', { name: 'Dismiss this notice' } )
+ ).toContainText( 'Site updated' );
+
+ await page.getByRole( 'button', { name: 'Open navigation' } ).click();
+ await patterns.navigation
+ .getByRole( 'button', { name: 'Back' } )
+ .click();
+ // TODO: await expect( page ).toHaveTitle( /^Patterns/ );
+
+ await expect(
+ patterns.navigation.getByRole( 'button', {
+ name: 'All patterns',
+ } )
+ ).toContainText( '1' );
+ await expect(
+ patterns.navigation.getByRole( 'button', {
+ name: 'My patterns',
+ } )
+ ).toContainText( '1' );
+ await expect(
+ patterns.navigation.getByRole( 'button', {
+ name: 'Uncategorized',
+ } )
+ ).toContainText( '1' );
+
+ await expect(
+ patterns.content.getByRole( 'heading', {
+ name: 'All patterns',
+ level: 2,
+ } )
+ ).toBeVisible();
+ await expect( patterns.list.getByRole( 'listitem' ) ).toHaveCount( 1 );
+ await expect(
+ patterns.list
+ .getByRole( 'heading', { name: 'My pattern' } )
+ .getByRole( 'button', { name: 'My pattern', exact: true } )
+ ).toBeVisible();
+ } );
+
+ test( 'search and filter patterns', async ( {
+ admin,
+ requestUtils,
+ patterns,
+ } ) => {
+ await Promise.all( [
+ requestUtils.createBlock( {
+ title: 'Unsynced header',
+ meta: { wp_pattern_sync_status: 'unsynced' },
+ status: 'publish',
+ content: `\nUnsynced header \n`,
+ wp_pattern_category: [],
+ } ),
+ requestUtils.createBlock( {
+ title: 'Unsynced footer',
+ meta: { wp_pattern_sync_status: 'unsynced' },
+ status: 'publish',
+ content: `\nUnsynced footer
\n`,
+ wp_pattern_category: [],
+ } ),
+ requestUtils.createBlock( {
+ title: 'Synced footer',
+ status: 'publish',
+ content: `\nSynced footer
\n`,
+ wp_pattern_category: [],
+ } ),
+ ] );
+
+ await admin.visitSiteEditor( { path: '/patterns' } );
+
+ await expect( patterns.list.getByRole( 'listitem' ) ).toHaveCount( 3 );
+
+ await patterns.content
+ .getByRole( 'searchbox', { name: 'Search patterns' } )
+ .fill( 'footer' );
+ await expect( patterns.list.getByRole( 'listitem' ) ).toHaveCount( 2 );
+ expect(
+ await patterns.list
+ .getByRole( 'listitem' )
+ .getByRole( 'heading' )
+ .allInnerTexts()
+ ).toEqual(
+ expect.arrayContaining( [ 'Unsynced footer', 'Synced footer' ] )
+ );
+
+ const searchBox = patterns.content.getByRole( 'searchbox', {
+ name: 'Search patterns',
+ } );
+
+ await searchBox.fill( 'no match' );
+ await expect( patterns.content ).toContainText( 'No patterns found.' );
+
+ await patterns.content
+ .getByRole( 'button', { name: 'Reset search' } )
+ .click();
+ await expect( searchBox ).toHaveValue( '' );
+ await expect( patterns.list.getByRole( 'listitem' ) ).toHaveCount( 3 );
+
+ const syncFilter = patterns.content.getByRole( 'radiogroup', {
+ name: 'Filter by sync status',
+ } );
+ await expect(
+ syncFilter.getByRole( 'radio', { name: 'All' } )
+ ).toBeChecked();
+
+ await syncFilter
+ .getByRole( 'radio', { name: 'Synced', exact: true } )
+ .click();
+ await expect( patterns.list.getByRole( 'listitem' ) ).toHaveCount( 1 );
+ await expect( patterns.list.getByRole( 'listitem' ) ).toContainText(
+ 'Synced footer'
+ );
+
+ await syncFilter.getByRole( 'radio', { name: 'Not synced' } ).click();
+ await expect( patterns.list.getByRole( 'listitem' ) ).toHaveCount( 2 );
+ expect(
+ await patterns.list
+ .getByRole( 'listitem' )
+ .getByRole( 'heading' )
+ .allInnerTexts()
+ ).toEqual(
+ expect.arrayContaining( [ 'Unsynced header', 'Unsynced footer' ] )
+ );
+
+ await searchBox.fill( 'footer' );
+ await expect( patterns.list.getByRole( 'listitem' ) ).toHaveCount( 1 );
+ await expect( patterns.list.getByRole( 'listitem' ) ).toContainText(
+ 'Unsynced footer'
+ );
+
+ await syncFilter.getByRole( 'radio', { name: 'All' } ).click();
+ await expect( patterns.list.getByRole( 'listitem' ) ).toHaveCount( 2 );
+ expect(
+ await patterns.list
+ .getByRole( 'listitem' )
+ .getByRole( 'heading' )
+ .allInnerTexts()
+ ).toEqual(
+ expect.arrayContaining( [ 'Unsynced footer', 'Synced footer' ] )
+ );
+ } );
+} );
+
+class Patterns {
+ /** @type {import('@playwright/test').Page} */
+ #page;
+
+ constructor( { page } ) {
+ this.#page = page;
+
+ this.content = this.#page.getByRole( 'region', {
+ name: 'Patterns content',
+ } );
+ this.navigation = this.#page.getByRole( 'region', {
+ name: 'Navigation',
+ } );
+ this.list = this.content.getByRole( 'list' );
+ }
+}
diff --git a/test/e2e/specs/site-editor/writing-flow.spec.js b/test/e2e/specs/site-editor/writing-flow.spec.js
index d9c63a1da1ddc5..f9681a5ea2d46f 100644
--- a/test/e2e/specs/site-editor/writing-flow.spec.js
+++ b/test/e2e/specs/site-editor/writing-flow.spec.js
@@ -66,7 +66,7 @@ test.describe( 'Site editor writing flow', () => {
// Tab to the inspector, tabbing three times to go past the two resize handles.
await pageUtils.pressKeys( 'Tab', { times: 3 } );
const inspectorTemplateTab = page.locator(
- 'role=region[name="Editor settings"i] >> role=button[name="Template"i]'
+ 'role=region[name="Editor settings"i] >> role=button[name="Template part"i]'
);
await expect( inspectorTemplateTab ).toBeFocused();
} );
diff --git a/test/e2e/specs/widgets/customizing-widgets.spec.js b/test/e2e/specs/widgets/customizing-widgets.spec.js
index d51d5cffbebb2b..e771c92fc24eef 100644
--- a/test/e2e/specs/widgets/customizing-widgets.spec.js
+++ b/test/e2e/specs/widgets/customizing-widgets.spec.js
@@ -147,7 +147,7 @@ test.describe( 'Widgets Customizer', () => {
// Go back to the widgets editor.
await backButton.click();
await expect( widgetsFooter1Heading ).toBeVisible();
- await expect( inspectorHeading ).not.toBeVisible();
+ await expect( inspectorHeading ).toBeHidden();
await editor.clickBlockToolbarButton( 'Options' );
await showMoreSettingsButton.click();
@@ -161,7 +161,7 @@ test.describe( 'Widgets Customizer', () => {
// Go back to the widgets editor.
await expect( widgetsFooter1Heading ).toBeVisible();
- await expect( inspectorHeading ).not.toBeVisible();
+ await expect( inspectorHeading ).toBeHidden();
} );
test( 'should handle the inserter outer section', async ( {
@@ -207,11 +207,11 @@ test.describe( 'Widgets Customizer', () => {
await expect( publishSettings ).toBeVisible();
// Expect the inserter outer section to be closed.
- await expect( inserterHeading ).not.toBeVisible();
+ await expect( inserterHeading ).toBeHidden();
// Focus the block and start typing to hide the block toolbar.
// Shouldn't be needed if we automatically hide the toolbar on blur.
- await page.focus( 'role=document[name="Paragraph block"i]' );
+ await page.focus( 'role=document[name="Block: Paragraph"i]' );
await page.keyboard.type( ' ' );
// Open the inserter outer section.
@@ -226,7 +226,7 @@ test.describe( 'Widgets Customizer', () => {
await page.click( 'role=button[name=/Back$/] >> visible=true' );
// Expect the inserter outer section to be closed.
- await expect( inserterHeading ).not.toBeVisible();
+ await expect( inserterHeading ).toBeHidden();
} );
test( 'should move focus to the block', async ( {
@@ -262,7 +262,7 @@ test.describe( 'Widgets Customizer', () => {
await editParagraphWidget.click();
const firstParagraphBlock = page.locator(
- 'role=document[name="Paragraph block"i] >> text="First Paragraph"'
+ 'role=document[name="Block: Paragraph"i] >> text="First Paragraph"'
);
await expect( firstParagraphBlock ).toBeFocused();
@@ -315,7 +315,7 @@ test.describe( 'Widgets Customizer', () => {
await page.click(
'role=heading[name="Customizing ▸ Widgets Footer #1"i][level=3]'
);
- await expect( blockToolbar ).not.toBeVisible();
+ await expect( blockToolbar ).toBeHidden();
await paragraphBlock.focus();
await editor.showBlockToolbar();
@@ -324,7 +324,7 @@ test.describe( 'Widgets Customizer', () => {
// Expect clicking on the preview iframe should clear the selection.
{
await page.click( '#customize-preview' );
- await expect( blockToolbar ).not.toBeVisible();
+ await expect( blockToolbar ).toBeHidden();
await paragraphBlock.focus();
await editor.showBlockToolbar();
@@ -339,7 +339,7 @@ test.describe( 'Widgets Customizer', () => {
const { x, y, width, height } = await editorContainer.boundingBox();
// Simulate Clicking on the empty space at the end of the editor.
await page.mouse.click( x + width / 2, y + height + 10 );
- await expect( blockToolbar ).not.toBeVisible();
+ await expect( blockToolbar ).toBeHidden();
}
} );
@@ -460,7 +460,7 @@ test.describe( 'Widgets Customizer', () => {
// Expect pressing the Escape key to close the dropdown,
// but not close the editor.
await page.keyboard.press( 'Escape' );
- await expect( optionsMenu ).not.toBeVisible();
+ await expect( optionsMenu ).toBeHidden();
await expect( paragraphBlock ).toBeVisible();
await paragraphBlock.focus();
@@ -496,7 +496,7 @@ test.describe( 'Widgets Customizer', () => {
// Refocus the paragraph block.
await page.focus(
- '*role=document[name="Paragraph block"i] >> text="First Paragraph"'
+ '*role=document[name="Block: Paragraph"i] >> text="First Paragraph"'
);
await editor.clickBlockToolbarButton( 'Move to widget area' );
@@ -511,7 +511,7 @@ test.describe( 'Widgets Customizer', () => {
// The paragraph block should be moved to the new sidebar and have focus.
const movedParagraphBlock = page.locator(
- '*role=document[name="Paragraph block"i] >> text="First Paragraph"'
+ '*role=document[name="Block: Paragraph"i] >> text="First Paragraph"'
);
await expect( movedParagraphBlock ).toBeVisible();
await expect( movedParagraphBlock ).toBeFocused();
@@ -528,7 +528,7 @@ test.describe( 'Widgets Customizer', () => {
// integrate the G sidebar inside the customizer.
await expect(
page.locator( 'role=heading[name=/Block Settings/][level=3]' )
- ).not.toBeVisible();
+ ).toBeHidden();
} );
test( 'should stay in block settings after making a change in that area', async ( {
@@ -554,7 +554,7 @@ test.describe( 'Widgets Customizer', () => {
).toBeDisabled();
// Select the paragraph block
- await page.focus( 'role=document[name="Paragraph block"i]' );
+ await page.focus( 'role=document[name="Block: Paragraph"i]' );
// Click the three dots button, then click "Show More Settings".
await editor.clickBlockToolbarButton( 'Options' );
diff --git a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap
index 178a57b8755535..121382d942f268 100644
--- a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap
+++ b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap
@@ -68,7 +68,7 @@ exports[`Blocks raw handling pasteHandler should strip windows data 1`] = `
"
`;
-exports[`Blocks raw handling pasteHandler slack-paragraphs 1`] = `"test with link a new linea new paragraph another new lineanother paragraph"`;
+exports[`Blocks raw handling pasteHandler slack-paragraphs 1`] = `"test with link a new line a new paragraph another new line another paragraph"`;
exports[`Blocks raw handling pasteHandler slack-quote 1`] = `"Test with link ."`;
diff --git a/test/integration/fixtures/blocks/core__cover__deprecated-12.html b/test/integration/fixtures/blocks/core__cover__deprecated-12.html
new file mode 100644
index 00000000000000..1ae933aed80105
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__cover__deprecated-12.html
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/test/integration/fixtures/blocks/core__cover__deprecated-12.json b/test/integration/fixtures/blocks/core__cover__deprecated-12.json
new file mode 100644
index 00000000000000..5991bbfd165bfb
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__cover__deprecated-12.json
@@ -0,0 +1,35 @@
+[
+ {
+ "name": "core/cover",
+ "isValid": true,
+ "attributes": {
+ "alt": "",
+ "hasParallax": false,
+ "isRepeated": false,
+ "dimRatio": 50,
+ "customOverlayColor": "#3615d9",
+ "backgroundType": "image",
+ "isDark": true,
+ "useFeaturedImage": true,
+ "tagName": "div",
+ "layout": {
+ "type": "constrained"
+ },
+ "isUserOverlayColor": true
+ },
+ "innerBlocks": [
+ {
+ "name": "core/paragraph",
+ "isValid": true,
+ "attributes": {
+ "align": "center",
+ "content": "",
+ "dropCap": false,
+ "placeholder": "Write title…",
+ "fontSize": "large"
+ },
+ "innerBlocks": []
+ }
+ ]
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__cover__deprecated-12.parsed.json b/test/integration/fixtures/blocks/core__cover__deprecated-12.parsed.json
new file mode 100644
index 00000000000000..f4e2e302cf52e7
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__cover__deprecated-12.parsed.json
@@ -0,0 +1,34 @@
+[
+ {
+ "blockName": "core/cover",
+ "attrs": {
+ "useFeaturedImage": true,
+ "dimRatio": 50,
+ "customOverlayColor": "#3615d9",
+ "layout": {
+ "type": "constrained"
+ }
+ },
+ "innerBlocks": [
+ {
+ "blockName": "core/paragraph",
+ "attrs": {
+ "align": "center",
+ "placeholder": "Write title…",
+ "fontSize": "large"
+ },
+ "innerBlocks": [],
+ "innerHTML": "\n\t
\n\t",
+ "innerContent": [
+ "\n\t
\n\t"
+ ]
+ }
+ ],
+ "innerHTML": "\n\n\t",
+ "innerContent": [
+ "\n\n\t"
+ ]
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__cover__deprecated-12.serialized.html b/test/integration/fixtures/blocks/core__cover__deprecated-12.serialized.html
new file mode 100644
index 00000000000000..d51734e18a5418
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__cover__deprecated-12.serialized.html
@@ -0,0 +1,5 @@
+
+
+
diff --git a/test/integration/fixtures/blocks/core__cover__solid-color.json b/test/integration/fixtures/blocks/core__cover__solid-color.json
index 8ddbe7fdbdc7ed..4d06b3e616e164 100644
--- a/test/integration/fixtures/blocks/core__cover__solid-color.json
+++ b/test/integration/fixtures/blocks/core__cover__solid-color.json
@@ -3,7 +3,6 @@
"name": "core/cover",
"isValid": true,
"attributes": {
- "useFeaturedImage": false,
"alt": "",
"hasParallax": false,
"isRepeated": false,
@@ -11,7 +10,9 @@
"overlayColor": "primary",
"backgroundType": "image",
"isDark": true,
- "tagName": "div"
+ "useFeaturedImage": false,
+ "tagName": "div",
+ "isUserOverlayColor": true
},
"innerBlocks": [
{
diff --git a/test/integration/fixtures/blocks/core__cover__solid-color.serialized.html b/test/integration/fixtures/blocks/core__cover__solid-color.serialized.html
index 4ac6a07e4769d0..333fb817b7283f 100644
--- a/test/integration/fixtures/blocks/core__cover__solid-color.serialized.html
+++ b/test/integration/fixtures/blocks/core__cover__solid-color.serialized.html
@@ -1,4 +1,4 @@
-
+
diff --git a/test/integration/fixtures/blocks/core__cover__video-overlay.json b/test/integration/fixtures/blocks/core__cover__video-overlay.json
index 7adcdedd68754b..7916e3f7a20b6e 100644
--- a/test/integration/fixtures/blocks/core__cover__video-overlay.json
+++ b/test/integration/fixtures/blocks/core__cover__video-overlay.json
@@ -4,7 +4,6 @@
"isValid": true,
"attributes": {
"url": "data:video/mp4;base64,AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAAAAhmcmVlAAAC721kYXQhEAUgpBv/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcCEQBSCkG//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADengAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAsJtb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPoAAAALwABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAB7HRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAIAAAAAAAAALwAAAAAAAAAAAAAAAQEAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAAC8AAAAAAAEAAAAAAWRtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAAKxEAAAIAFXEAAAAAAAtaGRscgAAAAAAAAAAc291bgAAAAAAAAAAAAAAAFNvdW5kSGFuZGxlcgAAAAEPbWluZgAAABBzbWhkAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAADTc3RibAAAAGdzdHNkAAAAAAAAAAEAAABXbXA0YQAAAAAAAAABAAAAAAAAAAAAAgAQAAAAAKxEAAAAAAAzZXNkcwAAAAADgICAIgACAASAgIAUQBUAAAAAAfQAAAHz+QWAgIACEhAGgICAAQIAAAAYc3R0cwAAAAAAAAABAAAAAgAABAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAIAAAABAAAAHHN0c3oAAAAAAAAAAAAAAAIAAAFzAAABdAAAABRzdGNvAAAAAAAAAAEAAAAsAAAAYnVkdGEAAABabWV0YQAAAAAAAAAhaGRscgAAAAAAAAAAbWRpcmFwcGwAAAAAAAAAAAAAAAAtaWxzdAAAACWpdG9vAAAAHWRhdGEAAAABAAAAAExhdmY1Ni40MC4xMDE=",
- "useFeaturedImage": false,
"alt": "",
"hasParallax": false,
"isRepeated": false,
@@ -12,7 +11,9 @@
"customOverlayColor": "#3615d9",
"backgroundType": "video",
"isDark": true,
- "tagName": "div"
+ "useFeaturedImage": false,
+ "tagName": "div",
+ "isUserOverlayColor": true
},
"innerBlocks": [
{
diff --git a/test/integration/fixtures/blocks/core__cover__video-overlay.serialized.html b/test/integration/fixtures/blocks/core__cover__video-overlay.serialized.html
index cf13928cd9d204..3cbf472fe84c4a 100644
--- a/test/integration/fixtures/blocks/core__cover__video-overlay.serialized.html
+++ b/test/integration/fixtures/blocks/core__cover__video-overlay.serialized.html
@@ -1,4 +1,4 @@
-
+
Guten Berg!
diff --git a/test/integration/fixtures/blocks/core__social-link-x.html b/test/integration/fixtures/blocks/core__social-link-x.html
new file mode 100644
index 00000000000000..a587f2d0962f6a
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__social-link-x.html
@@ -0,0 +1 @@
+
diff --git a/test/integration/fixtures/blocks/core__social-link-x.json b/test/integration/fixtures/blocks/core__social-link-x.json
new file mode 100644
index 00000000000000..d0cf492d2aa03e
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__social-link-x.json
@@ -0,0 +1,11 @@
+[
+ {
+ "name": "core/social-link",
+ "isValid": true,
+ "attributes": {
+ "url": "https://example.com/",
+ "service": "x"
+ },
+ "innerBlocks": []
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__social-link-x.parsed.json b/test/integration/fixtures/blocks/core__social-link-x.parsed.json
new file mode 100644
index 00000000000000..2119720f5db0f5
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__social-link-x.parsed.json
@@ -0,0 +1,11 @@
+[
+ {
+ "blockName": "core/social-link-x",
+ "attrs": {
+ "url": "https://example.com/"
+ },
+ "innerBlocks": [],
+ "innerHTML": "",
+ "innerContent": []
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__social-link-x.serialized.html b/test/integration/fixtures/blocks/core__social-link-x.serialized.html
new file mode 100644
index 00000000000000..be00807af4c9ea
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__social-link-x.serialized.html
@@ -0,0 +1 @@
+
diff --git a/test/performance/fixtures/index.js b/test/performance/fixtures/index.js
new file mode 100644
index 00000000000000..0f68fc5637f5ab
--- /dev/null
+++ b/test/performance/fixtures/index.js
@@ -0,0 +1 @@
+export { PerfUtils } from './perf-utils';
diff --git a/test/performance/fixtures/perf-utils.ts b/test/performance/fixtures/perf-utils.ts
new file mode 100644
index 00000000000000..a61af86684e6bb
--- /dev/null
+++ b/test/performance/fixtures/perf-utils.ts
@@ -0,0 +1,181 @@
+/**
+ * WordPress dependencies
+ */
+import { expect } from '@wordpress/e2e-test-utils-playwright';
+
+/**
+ * External dependencies
+ */
+import fs from 'fs';
+import path from 'path';
+import type { Locator, Page } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import { readFile } from '../utils.js';
+
+type PerfUtilsConstructorProps = {
+ page: Page;
+};
+
+export class PerfUtils {
+ page: Page;
+
+ constructor( { page }: PerfUtilsConstructorProps ) {
+ this.page = page;
+ }
+
+ /**
+ * Returns the locator for the editor canvas element. This supports both the
+ * legacy and the iframed canvas.
+ *
+ * @return Locator for the editor canvas element.
+ */
+ async getCanvas() {
+ return await Promise.any( [
+ ( async () => {
+ const legacyCanvasLocator = this.page.locator(
+ '.wp-block-post-content'
+ );
+ await legacyCanvasLocator.waitFor( {
+ timeout: 120_000,
+ } );
+ return legacyCanvasLocator;
+ } )(),
+ ( async () => {
+ const iframedCanvasLocator = this.page.frameLocator(
+ '[name=editor-canvas]'
+ );
+ await iframedCanvasLocator
+ .locator( 'body' )
+ .waitFor( { timeout: 120_000 } );
+ return iframedCanvasLocator;
+ } )(),
+ ] );
+ }
+
+ /**
+ * Saves the post as a draft and returns its URL.
+ *
+ * @return URL of the saved draft.
+ */
+ async saveDraft() {
+ await this.page
+ .getByRole( 'button', { name: 'Save draft' } )
+ .click( { timeout: 60_000 } );
+ await expect(
+ this.page.getByRole( 'button', { name: 'Saved' } )
+ ).toBeDisabled();
+
+ return this.page.url();
+ }
+
+ /**
+ * Disables the editor autosave function.
+ */
+ async disableAutosave() {
+ await this.page.evaluate( () => {
+ return window.wp.data
+ .dispatch( 'core/editor' )
+ .updateEditorSettings( {
+ autosaveInterval: 100000000000,
+ localAutosaveInterval: 100000000000,
+ } );
+ } );
+
+ const { autosaveInterval } = await this.page.evaluate( () => {
+ return window.wp.data.select( 'core/editor' ).getEditorSettings();
+ } );
+
+ expect( autosaveInterval ).toBe( 100000000000 );
+ }
+
+ /**
+ * Enters the Site Editor's edit mode.
+ *
+ * @return Locator for the editor canvas element.
+ */
+ async enterSiteEditorEditMode() {
+ const canvas = await this.getCanvas();
+
+ await canvas.locator( 'body' ).click();
+ await canvas
+ .getByRole( 'document', { name: /Block:( Post)? Content/ } )
+ .click();
+
+ return canvas;
+ }
+
+ /**
+ * Loads blocks from the small post with containers fixture into the editor
+ * canvas.
+ */
+ async loadBlocksForSmallPostWithContainers() {
+ return await this.loadBlocksFromHtml(
+ path.join(
+ process.env.ASSETS_PATH!,
+ 'small-post-with-containers.html'
+ )
+ );
+ }
+
+ /**
+ * Loads blocks from the large post fixture into the editor canvas.
+ */
+ async loadBlocksForLargePost() {
+ return await this.loadBlocksFromHtml(
+ path.join( process.env.ASSETS_PATH!, 'large-post.html' )
+ );
+ }
+
+ /**
+ * Loads blocks from an HTML fixture with given path into the editor canvas.
+ *
+ * @param filepath Path to the HTML fixture.
+ */
+ async loadBlocksFromHtml( filepath: string ) {
+ if ( ! fs.existsSync( filepath ) ) {
+ throw new Error( `File not found: ${ filepath }` );
+ }
+
+ return await this.page.evaluate( ( html: string ) => {
+ const { parse } = window.wp.blocks;
+ const { dispatch } = window.wp.data;
+ const blocks = parse( html );
+
+ blocks.forEach( ( block: any ) => {
+ if ( block.name === 'core/image' ) {
+ delete block.attributes.id;
+ delete block.attributes.url;
+ }
+ } );
+
+ dispatch( 'core/block-editor' ).resetBlocks( blocks );
+ }, readFile( filepath ) );
+ }
+
+ /**
+ * Generates and loads a 1000 empty paragraphs into the editor canvas.
+ */
+ async load1000Paragraphs() {
+ await this.page.evaluate( () => {
+ const { createBlock } = window.wp.blocks;
+ const { dispatch } = window.wp.data;
+ const blocks = Array.from( { length: 1000 } ).map( () =>
+ createBlock( 'core/paragraph' )
+ );
+ dispatch( 'core/block-editor' ).resetBlocks( blocks );
+ } );
+ }
+
+ async expectExpandedState( locator: Locator, state: 'true' | 'false' ) {
+ return await Promise.any( [
+ // eslint-disable-next-line playwright/missing-playwright-await
+ expect( locator ).toHaveAttribute( 'aria-expanded', state ),
+ // Legacy selector.
+ // eslint-disable-next-line playwright/missing-playwright-await
+ expect( locator ).toHaveAttribute( 'aria-pressed', state ),
+ ] );
+ }
+}
diff --git a/test/performance/playwright.config.ts b/test/performance/playwright.config.ts
index 0918b79d3144d2..a8208342ac2d81 100644
--- a/test/performance/playwright.config.ts
+++ b/test/performance/playwright.config.ts
@@ -13,7 +13,7 @@ const baseConfig = require( '@wordpress/scripts/config/playwright.config' );
process.env.ASSETS_PATH = path.join( __dirname, 'assets' );
const config = defineConfig( {
- ...baseConfig.default,
+ ...baseConfig,
reporter: process.env.CI
? './config/performance-reporter.ts'
: [ [ 'list' ], [ './config/performance-reporter.ts' ] ],
@@ -26,7 +26,7 @@ const config = defineConfig( {
new URL( './config/global-setup.ts', 'file:' + __filename ).href
),
use: {
- ...baseConfig.default.use,
+ ...baseConfig.use,
video: 'off',
},
} );
diff --git a/test/performance/specs/front-end-block-theme.spec.js b/test/performance/specs/front-end-block-theme.spec.js
index 5cd05b79495ac7..ca48535a21a467 100644
--- a/test/performance/specs/front-end-block-theme.spec.js
+++ b/test/performance/specs/front-end-block-theme.spec.js
@@ -1,7 +1,9 @@
+/* eslint-disable playwright/no-conditional-in-test, playwright/expect-expect */
+
/**
* WordPress dependencies
*/
-const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+import { test, Metrics } from '@wordpress/e2e-test-utils-playwright';
const results = {
timeToFirstByte: [],
@@ -10,7 +12,12 @@ const results = {
};
test.describe( 'Front End Performance', () => {
- test.use( { storageState: {} } ); // User will be logged out.
+ test.use( {
+ storageState: {}, // User will be logged out.
+ metrics: async ( { page }, use ) => {
+ await use( new Metrics( { page } ) );
+ },
+ } );
test.beforeAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'twentytwentythree' );
@@ -26,25 +33,22 @@ test.describe( 'Front End Performance', () => {
const samples = 16;
const throwaway = 0;
- const rounds = samples + throwaway;
- for ( let i = 0; i < rounds; i++ ) {
- test( `Measure TTFB, LCP, and LCP-TTFB (${
- i + 1
- } of ${ rounds })`, async ( { page, metrics } ) => {
+ const iterations = samples + throwaway;
+ for ( let i = 1; i <= iterations; i++ ) {
+ test( `Measure TTFB, LCP, and LCP-TTFB (${ i } of ${ iterations })`, async ( {
+ page,
+ metrics,
+ } ) => {
// Go to the base URL.
// eslint-disable-next-line playwright/no-networkidle
await page.goto( '/', { waitUntil: 'networkidle' } );
// Take the measurements.
- const lcp = await metrics.getLargestContentfulPaint();
const ttfb = await metrics.getTimeToFirstByte();
-
- // Ensure the numbers are valid.
- expect( lcp ).toBeGreaterThan( 0 );
- expect( ttfb ).toBeGreaterThan( 0 );
+ const lcp = await metrics.getLargestContentfulPaint();
// Save the results.
- if ( i >= throwaway ) {
+ if ( i > throwaway ) {
results.largestContentfulPaint.push( lcp );
results.timeToFirstByte.push( ttfb );
results.lcpMinusTtfb.push( lcp - ttfb );
@@ -52,3 +56,5 @@ test.describe( 'Front End Performance', () => {
} );
}
} );
+
+/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */
diff --git a/test/performance/specs/front-end-classic-theme.spec.js b/test/performance/specs/front-end-classic-theme.spec.js
index 68e833fe3f999f..0b6c3ec22c0465 100644
--- a/test/performance/specs/front-end-classic-theme.spec.js
+++ b/test/performance/specs/front-end-classic-theme.spec.js
@@ -1,7 +1,9 @@
+/* eslint-disable playwright/no-conditional-in-test, playwright/expect-expect */
+
/**
* WordPress dependencies
*/
-const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+import { test, Metrics } from '@wordpress/e2e-test-utils-playwright';
const results = {
timeToFirstByte: [],
@@ -10,7 +12,12 @@ const results = {
};
test.describe( 'Front End Performance', () => {
- test.use( { storageState: {} } ); // User will be logged out.
+ test.use( {
+ storageState: {}, // User will be logged out.
+ metrics: async ( { page }, use ) => {
+ await use( new Metrics( { page } ) );
+ },
+ } );
test.beforeAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'twentytwentyone' );
@@ -25,9 +32,9 @@ test.describe( 'Front End Performance', () => {
const samples = 16;
const throwaway = 0;
- const rounds = samples + throwaway;
- for ( let i = 1; i <= rounds; i++ ) {
- test( `Report TTFB, LCP, and LCP-TTFB (${ i } of ${ rounds })`, async ( {
+ const iterations = samples + throwaway;
+ for ( let i = 1; i <= iterations; i++ ) {
+ test( `Measure TTFB, LCP, and LCP-TTFB (${ i } of ${ iterations })`, async ( {
page,
metrics,
} ) => {
@@ -36,15 +43,11 @@ test.describe( 'Front End Performance', () => {
await page.goto( '/', { waitUntil: 'networkidle' } );
// Take the measurements.
- const lcp = await metrics.getLargestContentfulPaint();
const ttfb = await metrics.getTimeToFirstByte();
-
- // Ensure the numbers are valid.
- expect( lcp ).toBeGreaterThan( 0 );
- expect( ttfb ).toBeGreaterThan( 0 );
+ const lcp = await metrics.getLargestContentfulPaint();
// Save the results.
- if ( i >= throwaway ) {
+ if ( i > throwaway ) {
results.largestContentfulPaint.push( lcp );
results.timeToFirstByte.push( ttfb );
results.lcpMinusTtfb.push( lcp - ttfb );
@@ -52,3 +55,5 @@ test.describe( 'Front End Performance', () => {
} );
}
} );
+
+/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */
diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js
index 7b02ddd7f846b9..7f590330465278 100644
--- a/test/performance/specs/post-editor.spec.js
+++ b/test/performance/specs/post-editor.spec.js
@@ -1,26 +1,15 @@
-/**
- * WordPress dependencies
- */
-const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+/* eslint-disable playwright/no-conditional-in-test, playwright/expect-expect */
/**
- * External dependencies
+ * WordPress dependencies
*/
-const path = require( 'path' );
+import { test, Metrics } from '@wordpress/e2e-test-utils-playwright';
/**
* Internal dependencies
*/
-const {
- getTypingEventDurations,
- getClickEventDurations,
- getHoverEventDurations,
- getSelectionEventDurations,
- getLoadingDurations,
- loadBlocksFromHtml,
- load1000Paragraphs,
- sum,
-} = require( '../utils' );
+import { PerfUtils } from '../fixtures';
+import { sum } from '../utils.js';
// See https://github.com/WordPress/gutenberg/issues/51383#issuecomment-1613460429
const BROWSER_IDLE_WAIT = 1000;
@@ -42,6 +31,15 @@ const results = {
};
test.describe( 'Post Editor Performance', () => {
+ test.use( {
+ perfUtils: async ( { page }, use ) => {
+ await use( new PerfUtils( { page } ) );
+ },
+ metrics: async ( { page }, use ) => {
+ await use( new Metrics( { page } ) );
+ },
+ } );
+
test.afterAll( async ( {}, testInfo ) => {
await testInfo.attach( 'results', {
body: JSON.stringify( results, null, 2 ),
@@ -49,356 +47,434 @@ test.describe( 'Post Editor Performance', () => {
} );
} );
- test.beforeEach( async ( { admin, page } ) => {
- await admin.createNewPost();
- // Disable auto-save to avoid impacting the metrics.
- await page.evaluate( () => {
- window.wp.data.dispatch( 'core/editor' ).updateEditorSettings( {
- autosaveInterval: 100000000000,
- localAutosaveInterval: 100000000000,
- } );
+ test.describe( 'Loading', () => {
+ let draftURL = null;
+
+ test( 'Setup the test post', async ( { admin, perfUtils } ) => {
+ await admin.createNewPost();
+ await perfUtils.loadBlocksForLargePost();
+ draftURL = await perfUtils.saveDraft();
} );
- } );
- test( 'Loading', async ( { browser, page } ) => {
- // Turn the large post HTML into blocks and insert.
- await loadBlocksFromHtml(
- page,
- path.join( process.env.ASSETS_PATH, 'large-post.html' )
- );
-
- // Save the draft.
- await page
- .getByRole( 'button', { name: 'Save draft' } )
- .click( { timeout: 60_000 } );
- await expect(
- page.getByRole( 'button', { name: 'Saved' } )
- ).toBeDisabled();
-
- // Get the URL that we will be testing against.
- const draftURL = page.url();
-
- // Start the measurements.
const samples = 10;
const throwaway = 1;
- const rounds = throwaway + samples;
- for ( let i = 0; i < rounds; i++ ) {
- // Open a fresh page in a new context to prevent caching.
- const testPage = await browser.newPage();
-
- // Go to the test page URL.
- await testPage.goto( draftURL );
-
- // Get canvas (handles both legacy and iframed canvas).
- const canvas = await Promise.any( [
- ( async () => {
- const legacyCanvasLocator = testPage.locator(
- '.wp-block-post-content'
- );
- await legacyCanvasLocator.waitFor( { timeout: 120_000 } );
- return legacyCanvasLocator;
- } )(),
- ( async () => {
- const iframedCanvasLocator = testPage.frameLocator(
- '[name=editor-canvas]'
+ const iterations = samples + throwaway;
+ for ( let i = 1; i <= iterations; i++ ) {
+ test( `Run the test (${ i } of ${ iterations })`, async ( {
+ page,
+ perfUtils,
+ metrics,
+ } ) => {
+ // Open the test draft.
+ await page.goto( draftURL );
+ const canvas = await perfUtils.getCanvas();
+
+ // Wait for the first block.
+ await canvas.locator( '.wp-block' ).first().waitFor( {
+ timeout: 120_000,
+ } );
+
+ // Get the durations.
+ const loadingDurations = await metrics.getLoadingDurations();
+
+ // Save the results.
+ if ( i > throwaway ) {
+ Object.entries( loadingDurations ).forEach(
+ ( [ metric, duration ] ) => {
+ if ( metric === 'timeSinceResponseEnd' ) {
+ results.firstBlock.push( duration );
+ } else {
+ results[ metric ].push( duration );
+ }
+ }
);
- await iframedCanvasLocator
- .locator( 'body' )
- .waitFor( { timeout: 120_000 } );
- return iframedCanvasLocator;
- } )(),
- ] );
-
- await canvas.locator( '.wp-block' ).first().waitFor( {
- timeout: 120_000,
+ }
} );
+ }
+ } );
+
+ test.describe( 'Typing', () => {
+ let draftURL = null;
+
+ test( 'Setup the test post', async ( { admin, perfUtils, editor } ) => {
+ await admin.createNewPost();
+ await perfUtils.loadBlocksForLargePost();
+ await editor.insertBlock( { name: 'core/paragraph' } );
+ draftURL = await perfUtils.saveDraft();
+ } );
+
+ test( 'Run the test', async ( { page, perfUtils, metrics } ) => {
+ await page.goto( draftURL );
+ await perfUtils.disableAutosave();
+ const canvas = await perfUtils.getCanvas();
+
+ const paragraph = canvas.getByRole( 'document', {
+ name: /Empty block/i,
+ } );
+
+ // The first character typed triggers a longer time (isTyping change).
+ // It can impact the stability of the metric, so we exclude it. It
+ // probably deserves a dedicated metric itself, though.
+ const samples = 10;
+ const throwaway = 1;
+ const iterations = samples + throwaway;
+
+ // Start tracing.
+ await metrics.startTracing();
+
+ // Type the testing sequence into the empty paragraph.
+ await paragraph.type( 'x'.repeat( iterations ), {
+ delay: BROWSER_IDLE_WAIT,
+ // The extended timeout is needed because the typing is very slow
+ // and the `delay` value itself does not extend it.
+ timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe.
+ } );
+
+ // Stop tracing.
+ await metrics.stopTracing();
+
+ // Get the durations.
+ const [ keyDownEvents, keyPressEvents, keyUpEvents ] =
+ metrics.getTypingEventDurations();
// Save the results.
- if ( i >= throwaway ) {
- const loadingDurations = await getLoadingDurations( testPage );
- Object.entries( loadingDurations ).forEach(
- ( [ metric, duration ] ) => {
- results[ metric ].push( duration );
- }
+ for ( let i = throwaway; i < iterations; i++ ) {
+ results.type.push(
+ keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ]
);
}
-
- await testPage.close();
- }
+ } );
} );
- test( 'Typing', async ( { browser, page, editor } ) => {
- // Load the large post fixture.
- await loadBlocksFromHtml(
- page,
- path.join( process.env.ASSETS_PATH, 'large-post.html' )
- );
-
- // Append an empty paragraph.
- await editor.insertBlock( { name: 'core/paragraph' } );
+ test.describe( 'Typing within containers', () => {
+ let draftURL = null;
- // Start tracing.
- await browser.startTracing( page, {
- screenshots: false,
- categories: [ 'devtools.timeline' ],
+ test( 'Set up the test post', async ( { admin, perfUtils } ) => {
+ await admin.createNewPost();
+ await perfUtils.loadBlocksForSmallPostWithContainers();
+ draftURL = await perfUtils.saveDraft();
} );
- // The first character typed triggers a longer time (isTyping change).
- // It can impact the stability of the metric, so we exclude it. It
- // probably deserves a dedicated metric itself, though.
- const samples = 10;
- const throwaway = 1;
- const rounds = samples + throwaway;
+ test( 'Run the test', async ( { page, perfUtils, metrics } ) => {
+ await page.goto( draftURL );
+ await perfUtils.disableAutosave();
+ const canvas = await perfUtils.getCanvas();
+
+ // Select the block where we type in.
+ const firstParagraph = canvas
+ .getByRole( 'document', {
+ name: /Paragraph block|Block: Paragraph/,
+ } )
+ .first();
+ await firstParagraph.click();
+
+ // The first character typed triggers a longer time (isTyping change).
+ // It can impact the stability of the metric, so we exclude it. It
+ // probably deserves a dedicated metric itself, though.
+ const samples = 10;
+ const throwaway = 1;
+ const iterations = samples + throwaway;
+
+ // Start tracing.
+ await metrics.startTracing();
+
+ // Start typing in the middle of the text.
+ await firstParagraph.type( 'x'.repeat( iterations ), {
+ delay: BROWSER_IDLE_WAIT,
+ // The extended timeout is needed because the typing is very slow
+ // and the `delay` value itself does not extend it.
+ timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe.
+ } );
- // Type the testing sequence into the empty paragraph.
- await page.keyboard.type( 'x'.repeat( rounds ), {
- delay: BROWSER_IDLE_WAIT,
- } );
+ // Stop tracing.
+ await metrics.stopTracing();
- // Stop tracing and save results.
- const traceBuffer = await browser.stopTracing();
- const traceResults = JSON.parse( traceBuffer.toString() );
- const [ keyDownEvents, keyPressEvents, keyUpEvents ] =
- getTypingEventDurations( traceResults );
+ // Get the durations.
+ const [ keyDownEvents, keyPressEvents, keyUpEvents ] =
+ metrics.getTypingEventDurations();
- for ( let i = throwaway; i < rounds; i++ ) {
- results.type.push(
- keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ]
- );
- }
+ // Save the results.
+ for ( let i = throwaway; i < iterations; i++ ) {
+ results.typeContainer.push(
+ keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ]
+ );
+ }
+ } );
} );
- test( 'Typing within containers', async ( { browser, page, editor } ) => {
- await loadBlocksFromHtml(
- page,
- path.join(
- process.env.ASSETS_PATH,
- 'small-post-with-containers.html'
- )
- );
-
- // Select the block where we type in
- await editor.canvas
- .getByRole( 'document', { name: 'Paragraph block' } )
- .first()
- .click();
-
- await browser.startTracing( page, {
- screenshots: false,
- categories: [ 'devtools.timeline' ],
- } );
+ test.describe( 'Selecting blocks', () => {
+ let draftURL = null;
- const samples = 10;
- // The first character typed triggers a longer time (isTyping change).
- // It can impact the stability of the metric, so we exclude it. It
- // probably deserves a dedicated metric itself, though.
- const throwaway = 1;
- const rounds = samples + throwaway;
- await page.keyboard.type( 'x'.repeat( rounds ), {
- delay: BROWSER_IDLE_WAIT,
+ test( 'Set up the test post', async ( { admin, perfUtils } ) => {
+ await admin.createNewPost();
+ await perfUtils.load1000Paragraphs();
+ draftURL = await perfUtils.saveDraft();
} );
- const traceBuffer = await browser.stopTracing();
- const traceResults = JSON.parse( traceBuffer.toString() );
- const [ keyDownEvents, keyPressEvents, keyUpEvents ] =
- getTypingEventDurations( traceResults );
+ test( 'Run the test', async ( { page, perfUtils, metrics } ) => {
+ await page.goto( draftURL );
+ await perfUtils.disableAutosave();
+ const canvas = await perfUtils.getCanvas();
- for ( let i = throwaway; i < rounds; i++ ) {
- results.typeContainer.push(
- keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ]
- );
- }
- } );
+ const paragraphs = canvas.getByRole( 'document', {
+ name: /Empty block/i,
+ } );
- test( 'Selecting blocks', async ( { browser, page, editor } ) => {
- await load1000Paragraphs( page );
- const paragraphs = editor.canvas.locator( '.wp-block' );
+ const samples = 10;
+ const throwaway = 1;
+ const iterations = samples + throwaway;
+ for ( let i = 1; i <= iterations; i++ ) {
+ // Wait for the browser to be idle before starting the monitoring.
+ // eslint-disable-next-line no-restricted-syntax
+ await page.waitForTimeout( BROWSER_IDLE_WAIT );
- const samples = 10;
- const throwaway = 1;
- const rounds = samples + throwaway;
- for ( let i = 0; i < rounds; i++ ) {
- // Wait for the browser to be idle before starting the monitoring.
- // eslint-disable-next-line no-restricted-syntax
- await page.waitForTimeout( BROWSER_IDLE_WAIT );
- await browser.startTracing( page, {
- screenshots: false,
- categories: [ 'devtools.timeline' ],
- } );
+ // Start tracing.
+ await metrics.startTracing();
- await paragraphs.nth( i ).click();
+ // Click the next paragraph.
+ await paragraphs.nth( i ).click();
- const traceBuffer = await browser.stopTracing();
+ // Stop tracing.
+ await metrics.stopTracing();
- if ( i >= throwaway ) {
- const traceResults = JSON.parse( traceBuffer.toString() );
- const allDurations = getSelectionEventDurations( traceResults );
- results.focus.push(
- allDurations.reduce( ( acc, eventDurations ) => {
- return acc + sum( eventDurations );
- }, 0 )
- );
+ // Get the durations.
+ const allDurations = metrics.getSelectionEventDurations();
+
+ // Save the results.
+ if ( i > throwaway ) {
+ results.focus.push(
+ allDurations.reduce( ( acc, eventDurations ) => {
+ return acc + sum( eventDurations );
+ }, 0 )
+ );
+ }
}
- }
+ } );
} );
- test( 'Opening persistent list view', async ( { browser, page } ) => {
- await load1000Paragraphs( page );
- const listViewToggle = page.getByRole( 'button', {
- name: 'Document Overview',
+ test.describe( 'Opening persistent List View', () => {
+ let draftURL = null;
+
+ test( 'Set up the test page', async ( { admin, perfUtils } ) => {
+ await admin.createNewPost();
+ await perfUtils.load1000Paragraphs();
+ draftURL = await perfUtils.saveDraft();
} );
- const samples = 10;
- const throwaway = 1;
- const rounds = samples + throwaway;
- for ( let i = 0; i < rounds; i++ ) {
- // Wait for the browser to be idle before starting the monitoring.
- // eslint-disable-next-line no-restricted-syntax
- await page.waitForTimeout( BROWSER_IDLE_WAIT );
- await browser.startTracing( page, {
- screenshots: false,
- categories: [ 'devtools.timeline' ],
+ test( 'Run the test', async ( { page, perfUtils, metrics } ) => {
+ await page.goto( draftURL );
+ await perfUtils.disableAutosave();
+
+ const listViewToggle = page.getByRole( 'button', {
+ name: 'Document Overview',
} );
- // Open List View
- await listViewToggle.click();
+ const samples = 10;
+ const throwaway = 1;
+ const iterations = samples + throwaway;
+ for ( let i = 1; i <= iterations; i++ ) {
+ // Wait for the browser to be idle before starting the monitoring.
+ // eslint-disable-next-line no-restricted-syntax
+ await page.waitForTimeout( BROWSER_IDLE_WAIT );
- const traceBuffer = await browser.stopTracing();
+ // Start tracing.
+ await metrics.startTracing();
- if ( i >= throwaway ) {
- const traceResults = JSON.parse( traceBuffer.toString() );
- const [ mouseClickEvents ] =
- getClickEventDurations( traceResults );
- results.listViewOpen.push( mouseClickEvents[ 0 ] );
- }
+ // Open List View.
+ await listViewToggle.click();
+ await perfUtils.expectExpandedState( listViewToggle, 'true' );
- // Close List View
- await listViewToggle.click();
- }
+ // Stop tracing.
+ await metrics.stopTracing();
+
+ // Get the durations.
+ const [ mouseClickEvents ] = metrics.getClickEventDurations();
+
+ // Save the results.
+ if ( i > throwaway ) {
+ results.listViewOpen.push( mouseClickEvents[ 0 ] );
+ }
+
+ // Close List View
+ await listViewToggle.click();
+ await perfUtils.expectExpandedState( listViewToggle, 'false' );
+ }
+ } );
} );
- test( 'Opening the inserter', async ( { browser, page } ) => {
- await load1000Paragraphs( page );
- const globalInserterToggle = page.getByRole( 'button', {
- name: 'Toggle block inserter',
+ test.describe( 'Opening Inserter', () => {
+ let draftURL = null;
+
+ test( 'Set up the test page', async ( { admin, perfUtils } ) => {
+ await admin.createNewPost();
+ await perfUtils.load1000Paragraphs();
+ draftURL = await perfUtils.saveDraft();
} );
- const samples = 10;
- const throwaway = 1;
- const rounds = samples + throwaway;
- for ( let i = 0; i < rounds; i++ ) {
- // Wait for the browser to be idle before starting the monitoring.
- // eslint-disable-next-line no-restricted-syntax
- await page.waitForTimeout( BROWSER_IDLE_WAIT );
- await browser.startTracing( page, {
- screenshots: false,
- categories: [ 'devtools.timeline' ],
+ test( 'Run the test', async ( { page, perfUtils, metrics } ) => {
+ // Go to the test page.
+ await page.goto( draftURL );
+ await perfUtils.disableAutosave();
+ const globalInserterToggle = page.getByRole( 'button', {
+ name: 'Toggle block inserter',
} );
- // Open Inserter.
- await globalInserterToggle.click();
+ const samples = 10;
+ const throwaway = 1;
+ const iterations = samples + throwaway;
+ for ( let i = 1; i <= iterations; i++ ) {
+ // Wait for the browser to be idle before starting the monitoring.
+ // eslint-disable-next-line no-restricted-syntax
+ await page.waitForTimeout( BROWSER_IDLE_WAIT );
+
+ // Start tracing.
+ await metrics.startTracing();
+
+ // Open Inserter.
+ await globalInserterToggle.click();
+ await perfUtils.expectExpandedState(
+ globalInserterToggle,
+ 'true'
+ );
- const traceBuffer = await browser.stopTracing();
+ // Stop tracing.
+ await metrics.stopTracing();
- if ( i >= throwaway ) {
- const traceResults = JSON.parse( traceBuffer.toString() );
- const [ mouseClickEvents ] =
- getClickEventDurations( traceResults );
- results.inserterOpen.push( mouseClickEvents[ 0 ] );
- }
+ // Get the durations.
+ const [ mouseClickEvents ] = metrics.getClickEventDurations();
- // Close Inserter.
- await globalInserterToggle.click();
- }
- } );
+ // Save the results.
+ if ( i > throwaway ) {
+ results.inserterOpen.push( mouseClickEvents[ 0 ] );
+ }
- test( 'Searching the inserter', async ( { browser, page } ) => {
- await load1000Paragraphs( page );
- const globalInserterToggle = page.getByRole( 'button', {
- name: 'Toggle block inserter',
+ // Close Inserter.
+ await globalInserterToggle.click();
+ await perfUtils.expectExpandedState(
+ globalInserterToggle,
+ 'false'
+ );
+ }
} );
+ } );
- // Open Inserter.
- await globalInserterToggle.click();
+ test.describe( 'Searching Inserter', () => {
+ let draftURL = null;
- const samples = 10;
- const throwaway = 1;
- const rounds = samples + throwaway;
- for ( let i = 0; i < rounds; i++ ) {
- // Wait for the browser to be idle before starting the monitoring.
- // eslint-disable-next-line no-restricted-syntax
- await page.waitForTimeout( BROWSER_IDLE_WAIT );
- await browser.startTracing( page, {
- screenshots: false,
- categories: [ 'devtools.timeline' ],
+ test( 'Set up the test page', async ( { admin, perfUtils } ) => {
+ await admin.createNewPost();
+ await perfUtils.load1000Paragraphs();
+ draftURL = await perfUtils.saveDraft();
+ } );
+
+ test( 'Run the test', async ( { page, perfUtils, metrics } ) => {
+ // Go to the test page.
+ await page.goto( draftURL );
+ await perfUtils.disableAutosave();
+ const globalInserterToggle = page.getByRole( 'button', {
+ name: 'Toggle block inserter',
} );
- await page.keyboard.type( 'p' );
+ // Open Inserter.
+ await globalInserterToggle.click();
+ await perfUtils.expectExpandedState( globalInserterToggle, 'true' );
+
+ const samples = 10;
+ const throwaway = 1;
+ const iterations = samples + throwaway;
+ for ( let i = 1; i <= iterations; i++ ) {
+ // Wait for the browser to be idle before starting the monitoring.
+ // eslint-disable-next-line no-restricted-syntax
+ await page.waitForTimeout( BROWSER_IDLE_WAIT );
- const traceBuffer = await browser.stopTracing();
+ // Start tracing.
+ await metrics.startTracing();
- if ( i >= throwaway ) {
- const traceResults = JSON.parse( traceBuffer.toString() );
- const [ keyDownEvents, keyPressEvents, keyUpEvents ] =
- getTypingEventDurations( traceResults );
- results.inserterSearch.push(
- keyDownEvents[ 0 ] + keyPressEvents[ 0 ] + keyUpEvents[ 0 ]
- );
- }
+ // Type to trigger search.
+ await page.keyboard.type( 'p' );
- await page.keyboard.press( 'Backspace' );
- }
+ // Stop tracing.
+ await metrics.stopTracing();
+
+ // Get the durations.
+ const [ keyDownEvents, keyPressEvents, keyUpEvents ] =
+ metrics.getTypingEventDurations();
+
+ // Save the results.
+ if ( i > throwaway ) {
+ results.inserterSearch.push(
+ keyDownEvents[ 0 ] +
+ keyPressEvents[ 0 ] +
+ keyUpEvents[ 0 ]
+ );
+ }
- // Close Inserter.
- await globalInserterToggle.click();
+ await page.keyboard.press( 'Backspace' );
+ }
+ } );
} );
- test( 'Hovering Inserter Items', async ( { browser, page } ) => {
- await load1000Paragraphs( page );
- const globalInserterToggle = page.getByRole( 'button', {
- name: 'Toggle block inserter',
+ test.describe( 'Hovering Inserter items', () => {
+ let draftURL = null;
+
+ test( 'Set up the test page', async ( { admin, perfUtils } ) => {
+ await admin.createNewPost();
+ await perfUtils.load1000Paragraphs();
+ draftURL = await perfUtils.saveDraft();
} );
- const paragraphBlockItem = page.locator(
- '.block-editor-inserter__menu .editor-block-list-item-paragraph'
- );
- const headingBlockItem = page.locator(
- '.block-editor-inserter__menu .editor-block-list-item-heading'
- );
- // Open Inserter.
- await globalInserterToggle.click();
+ test( 'Run the test', async ( { page, perfUtils, metrics } ) => {
+ // Go to the test page.
+ await page.goto( draftURL );
+ await perfUtils.disableAutosave();
- const samples = 10;
- const throwaway = 1;
- const rounds = samples + throwaway;
- for ( let i = 0; i < rounds; i++ ) {
- // Wait for the browser to be idle before starting the monitoring.
- // eslint-disable-next-line no-restricted-syntax
- await page.waitForTimeout( BROWSER_IDLE_WAIT );
- await browser.startTracing( page, {
- screenshots: false,
- categories: [ 'devtools.timeline' ],
+ const globalInserterToggle = page.getByRole( 'button', {
+ name: 'Toggle block inserter',
} );
+ const paragraphBlockItem = page.locator(
+ '.block-editor-inserter__menu .editor-block-list-item-paragraph'
+ );
+ const headingBlockItem = page.locator(
+ '.block-editor-inserter__menu .editor-block-list-item-heading'
+ );
+
+ // Open Inserter.
+ await globalInserterToggle.click();
+ await perfUtils.expectExpandedState( globalInserterToggle, 'true' );
+
+ const samples = 10;
+ const throwaway = 1;
+ const iterations = samples + throwaway;
+ for ( let i = 1; i <= iterations; i++ ) {
+ // Wait for the browser to be idle before starting the monitoring.
+ // eslint-disable-next-line no-restricted-syntax
+ await page.waitForTimeout( BROWSER_IDLE_WAIT );
- // Hover Items.
- await paragraphBlockItem.hover();
- await headingBlockItem.hover();
+ // Start tracing.
+ await metrics.startTracing();
- const traceBuffer = await browser.stopTracing();
+ // Hover Inserter items.
+ await paragraphBlockItem.hover();
+ await headingBlockItem.hover();
- if ( i >= throwaway ) {
- const traceResults = JSON.parse( traceBuffer.toString() );
+ // Stop tracing.
+ await metrics.stopTracing();
+
+ // Get the durations.
const [ mouseOverEvents, mouseOutEvents ] =
- getHoverEventDurations( traceResults );
- for ( let k = 0; k < mouseOverEvents.length; k++ ) {
- results.inserterHover.push(
- mouseOverEvents[ k ] + mouseOutEvents[ k ]
- );
+ metrics.getHoverEventDurations();
+
+ // Save the results.
+ if ( i > throwaway ) {
+ for ( let k = 0; k < mouseOverEvents.length; k++ ) {
+ results.inserterHover.push(
+ mouseOverEvents[ k ] + mouseOutEvents[ k ]
+ );
+ }
}
}
- }
-
- // Close Inserter.
- await globalInserterToggle.click();
+ } );
} );
} );
+
+/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */
diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js
index 1a5c0a96b6846f..f2f211dd52e6e0 100644
--- a/test/performance/specs/site-editor.spec.js
+++ b/test/performance/specs/site-editor.spec.js
@@ -1,21 +1,14 @@
-/**
- * WordPress dependencies
- */
-const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+/* eslint-disable playwright/no-conditional-in-test, playwright/expect-expect */
/**
- * External dependencies
+ * WordPress dependencies
*/
-const path = require( 'path' );
+import { test, Metrics } from '@wordpress/e2e-test-utils-playwright';
/**
* Internal dependencies
*/
-const {
- getTypingEventDurations,
- getLoadingDurations,
- loadBlocksFromHtml,
-} = require( '../utils' );
+import { PerfUtils } from '../fixtures';
// See https://github.com/WordPress/gutenberg/issues/51383#issuecomment-1613460429
const BROWSER_IDLE_WAIT = 1000;
@@ -36,9 +29,16 @@ const results = {
listViewOpen: [],
};
-let testPageId;
-
test.describe( 'Site Editor Performance', () => {
+ test.use( {
+ perfUtils: async ( { page }, use ) => {
+ await use( new PerfUtils( { page } ) );
+ },
+ metrics: async ( { page }, use ) => {
+ await use( new Metrics( { page } ) );
+ },
+ } );
+
test.beforeAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'emptytheme' );
await requestUtils.deleteAllTemplates( 'wp_template' );
@@ -56,156 +56,139 @@ test.describe( 'Site Editor Performance', () => {
await requestUtils.activateTheme( 'twentytwentyone' );
} );
- test.beforeEach( async ( { admin, page } ) => {
- // Start a new page.
- await admin.createNewPost( { postType: 'page' } );
+ test.describe( 'Loading', () => {
+ let draftURL = null;
- // Disable auto-save to avoid impacting the metrics.
- await page.evaluate( () => {
- window.wp.data.dispatch( 'core/editor' ).updateEditorSettings( {
- autosaveInterval: 100000000000,
- localAutosaveInterval: 100000000000,
+ test( 'Setup the test page', async ( { page, admin, perfUtils } ) => {
+ await admin.createNewPost( { postType: 'page' } );
+ await perfUtils.loadBlocksForLargePost();
+ await perfUtils.saveDraft();
+
+ await admin.visitSiteEditor( {
+ postId: new URL( page.url() ).searchParams.get( 'post' ),
+ postType: 'page',
} );
- } );
- } );
- test( 'Loading', async ( { browser, page, admin } ) => {
- // Load the large post fixture.
- await loadBlocksFromHtml(
- page,
- path.join( process.env.ASSETS_PATH, 'large-post.html' )
- );
-
- // Save the draft.
- await page
- .getByRole( 'button', { name: 'Save draft' } )
- .click( { timeout: 60_000 } );
- await expect(
- page.getByRole( 'button', { name: 'Saved' } )
- ).toBeDisabled();
-
- // Get the ID of the saved page.
- testPageId = new URL( page.url() ).searchParams.get( 'post' );
-
- // Open the test page in Site Editor.
- await admin.visitSiteEditor( {
- postId: testPageId,
- postType: 'page',
+ draftURL = page.url();
} );
- // Get the URL that we will be testing against.
- const draftURL = page.url();
-
- // Start the measurements.
const samples = 10;
const throwaway = 1;
- const rounds = samples + throwaway;
- for ( let i = 0; i < rounds; i++ ) {
- // Open a fresh page in a new context to prevent caching.
- const testPage = await browser.newPage();
-
- // Go to the test page URL.
- await testPage.goto( draftURL );
-
- // Wait for the first block.
- await testPage
- .frameLocator( 'iframe[name="editor-canvas"]' )
- .locator( '.wp-block' )
- .first()
- .waitFor( { timeout: 120_000 } );
-
- // Save the results.
- if ( i >= throwaway ) {
- const loadingDurations = await getLoadingDurations( testPage );
- Object.entries( loadingDurations ).forEach(
- ( [ metric, duration ] ) => {
- results[ metric ].push( duration );
- }
- );
- }
-
- await testPage.close();
+ const iterations = samples + throwaway;
+ for ( let i = 1; i <= iterations; i++ ) {
+ test( `Run the test (${ i } of ${ iterations })`, async ( {
+ page,
+ perfUtils,
+ metrics,
+ } ) => {
+ // Go to the test draft.
+ await page.goto( draftURL );
+ const canvas = await perfUtils.getCanvas();
+
+ // Wait for the first block.
+ await canvas.locator( '.wp-block' ).first().waitFor( {
+ timeout: 120_000,
+ } );
+
+ // Get the durations.
+ const loadingDurations = await metrics.getLoadingDurations();
+
+ // Save the results.
+ if ( i > throwaway ) {
+ Object.entries( loadingDurations ).forEach(
+ ( [ metric, duration ] ) => {
+ if ( metric === 'timeSinceResponseEnd' ) {
+ results.firstBlock.push( duration );
+ } else {
+ results[ metric ].push( duration );
+ }
+ }
+ );
+ }
+ } );
}
} );
+ test.describe( 'Typing', () => {
+ let draftURL = null;
- test( 'Typing', async ( { browser, page, admin, editor } ) => {
- // Load the large post fixture.
- await loadBlocksFromHtml(
+ test( 'Setup the test post', async ( {
page,
- path.join( process.env.ASSETS_PATH, 'large-post.html' )
- );
-
- // Save the draft.
- await page
- .getByRole( 'button', { name: 'Save draft' } )
- // Loading the large post HTML can take some time so we need a higher
- // timeout value here.
- .click( { timeout: 60_000 } );
- await expect(
- page.getByRole( 'button', { name: 'Saved' } )
- ).toBeDisabled();
-
- // Get the ID of the saved page.
- testPageId = new URL( page.url() ).searchParams.get( 'post' );
-
- // Open the test page in Site Editor.
- await admin.visitSiteEditor( {
- postId: testPageId,
- postType: 'page',
- } );
+ admin,
+ editor,
+ perfUtils,
+ } ) => {
+ await admin.createNewPost( { postType: 'page' } );
+ await perfUtils.loadBlocksForLargePost();
+ await editor.insertBlock( { name: 'core/paragraph' } );
+ await perfUtils.saveDraft();
+
+ await admin.visitSiteEditor( {
+ postId: new URL( page.url() ).searchParams.get( 'post' ),
+ postType: 'page',
+ } );
- // Wait for the first paragraph to be ready.
- const firstParagraph = editor.canvas
- .getByText( 'Lorem ipsum dolor sit amet' )
- .first();
- await firstParagraph.waitFor( { timeout: 60_000 } );
-
- // Enter edit mode.
- await editor.canvas.locator( 'body' ).click();
- // Second click is needed for the legacy edit mode.
- await editor.canvas
- .getByRole( 'document', { name: /Block:( Post)? Content/ } )
- .click();
-
- // Append an empty paragraph.
- // Since `editor.insertBlock( { name: 'core/paragraph' } )` is not
- // working in page edit mode, we need to _manually_ insert a new
- // paragraph.
- await editor.canvas
- .getByText( 'Quamquam tu hanc copiosiorem etiam soles dicere.' )
- .last()
- .click(); // Enters edit mode for the last post's element, which is a list item.
-
- await page.keyboard.press( 'Enter' ); // Creates a new list item.
- await page.keyboard.press( 'Enter' ); // Exits the list and creates a new paragraph.
-
- // Start tracing.
- await browser.startTracing( page, {
- screenshots: false,
- categories: [ 'devtools.timeline' ],
+ draftURL = page.url();
} );
+ test( 'Run the test', async ( { page, perfUtils, metrics } ) => {
+ await page.goto( draftURL );
+ await perfUtils.disableAutosave();
+
+ // Wait for the loader overlay to disappear. This is necessary
+ // because the overlay is still visible for a while after the editor
+ // canvas is ready, and we don't want it to affect the typing
+ // timings.
+ await page
+ .locator(
+ // Spinner was used instead of the progress bar in an earlier version of the site editor.
+ '.edit-site-canvas-loader, .edit-site-canvas-spinner'
+ )
+ .waitFor( { state: 'hidden', timeout: 120_000 } );
+
+ const canvas = await perfUtils.getCanvas();
+
+ // Enter edit mode (second click is needed for the legacy edit mode).
+ await canvas.locator( 'body' ).click();
+ await canvas
+ .getByRole( 'document', { name: /Block:( Post)? Content/ } )
+ .click();
+
+ const paragraph = canvas.getByRole( 'document', {
+ name: /Empty block/i,
+ } );
- // The first character typed triggers a longer time (isTyping change).
- // It can impact the stability of the metric, so we exclude it. It
- // probably deserves a dedicated metric itself, though.
- const samples = 10;
- const throwaway = 1;
- const rounds = samples + throwaway;
+ // The first character typed triggers a longer time (isTyping change).
+ // It can impact the stability of the metric, so we exclude it. It
+ // probably deserves a dedicated metric itself, though.
+ const samples = 10;
+ const throwaway = 1;
+ const iterations = samples + throwaway;
+
+ // Start tracing.
+ await metrics.startTracing();
+
+ // Type the testing sequence into the empty paragraph.
+ await paragraph.type( 'x'.repeat( iterations ), {
+ delay: BROWSER_IDLE_WAIT,
+ // The extended timeout is needed because the typing is very slow
+ // and the `delay` value itself does not extend it.
+ timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe.
+ } );
- // Type the testing sequence into the empty paragraph.
- await page.keyboard.type( 'x'.repeat( rounds ), {
- delay: BROWSER_IDLE_WAIT,
- } );
+ // Stop tracing.
+ await metrics.stopTracing();
- // Stop tracing and save results.
- const traceBuffer = await browser.stopTracing();
- const traceResults = JSON.parse( traceBuffer.toString() );
- const [ keyDownEvents, keyPressEvents, keyUpEvents ] =
- getTypingEventDurations( traceResults );
- for ( let i = throwaway; i < rounds; i++ ) {
- results.type.push(
- keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ]
- );
- }
+ // Get the durations.
+ const [ keyDownEvents, keyPressEvents, keyUpEvents ] =
+ metrics.getTypingEventDurations();
+
+ // Save the results.
+ for ( let i = throwaway; i < iterations; i++ ) {
+ results.type.push(
+ keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ]
+ );
+ }
+ } );
} );
} );
+
+/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */
diff --git a/test/performance/utils.js b/test/performance/utils.js
index b86d09a10b301d..e14ff71436d735 100644
--- a/test/performance/utils.js
+++ b/test/performance/utils.js
@@ -58,141 +58,3 @@ export function deleteFile( filePath ) {
unlinkSync( filePath );
}
}
-
-function isEvent( item ) {
- return (
- item.cat === 'devtools.timeline' &&
- item.name === 'EventDispatch' &&
- item.dur &&
- item.args &&
- item.args.data
- );
-}
-
-function isKeyDownEvent( item ) {
- return isEvent( item ) && item.args.data.type === 'keydown';
-}
-
-function isKeyPressEvent( item ) {
- return isEvent( item ) && item.args.data.type === 'keypress';
-}
-
-function isKeyUpEvent( item ) {
- return isEvent( item ) && item.args.data.type === 'keyup';
-}
-
-function isFocusEvent( item ) {
- return isEvent( item ) && item.args.data.type === 'focus';
-}
-
-function isFocusInEvent( item ) {
- return isEvent( item ) && item.args.data.type === 'focusin';
-}
-
-function isClickEvent( item ) {
- return isEvent( item ) && item.args.data.type === 'click';
-}
-
-function isMouseOverEvent( item ) {
- return isEvent( item ) && item.args.data.type === 'mouseover';
-}
-
-function isMouseOutEvent( item ) {
- return isEvent( item ) && item.args.data.type === 'mouseout';
-}
-
-function getEventDurationsForType( trace, filterFunction ) {
- return trace.traceEvents
- .filter( filterFunction )
- .map( ( item ) => item.dur / 1000 );
-}
-
-export function getTypingEventDurations( trace ) {
- return [
- getEventDurationsForType( trace, isKeyDownEvent ),
- getEventDurationsForType( trace, isKeyPressEvent ),
- getEventDurationsForType( trace, isKeyUpEvent ),
- ];
-}
-
-export function getSelectionEventDurations( trace ) {
- return [
- getEventDurationsForType( trace, isFocusEvent ),
- getEventDurationsForType( trace, isFocusInEvent ),
- ];
-}
-
-export function getClickEventDurations( trace ) {
- return [ getEventDurationsForType( trace, isClickEvent ) ];
-}
-
-export function getHoverEventDurations( trace ) {
- return [
- getEventDurationsForType( trace, isMouseOverEvent ),
- getEventDurationsForType( trace, isMouseOutEvent ),
- ];
-}
-
-export async function getLoadingDurations( page ) {
- return await page.evaluate( () => {
- const [
- {
- requestStart,
- responseStart,
- responseEnd,
- domContentLoadedEventEnd,
- loadEventEnd,
- },
- ] = performance.getEntriesByType( 'navigation' );
- const paintTimings = performance.getEntriesByType( 'paint' );
- return {
- // Server side metric.
- serverResponse: responseStart - requestStart,
- // For client side metrics, consider the end of the response (the
- // browser receives the HTML) as the start time (0).
- firstPaint:
- paintTimings.find( ( { name } ) => name === 'first-paint' )
- .startTime - responseEnd,
- domContentLoaded: domContentLoadedEventEnd - responseEnd,
- loaded: loadEventEnd - responseEnd,
- firstContentfulPaint:
- paintTimings.find(
- ( { name } ) => name === 'first-contentful-paint'
- ).startTime - responseEnd,
- // This is evaluated right after Playwright found the block selector.
- firstBlock: performance.now() - responseEnd,
- };
- } );
-}
-
-export async function loadBlocksFromHtml( page, filepath ) {
- if ( ! existsSync( filepath ) ) {
- throw new Error( `File not found (${ filepath })` );
- }
-
- return await page.evaluate( ( html ) => {
- const { parse } = window.wp.blocks;
- const { dispatch } = window.wp.data;
- const blocks = parse( html );
-
- blocks.forEach( ( block ) => {
- if ( block.name === 'core/image' ) {
- delete block.attributes.id;
- delete block.attributes.url;
- }
- } );
-
- dispatch( 'core/block-editor' ).resetBlocks( blocks );
- }, readFile( filepath ) );
-}
-
-export async function load1000Paragraphs( page ) {
- await page.evaluate( () => {
- const { createBlock } = window.wp.blocks;
- const { dispatch } = window.wp.data;
- const blocks = Array.from( { length: 1000 } ).map( () =>
- createBlock( 'core/paragraph' )
- );
- dispatch( 'core/block-editor' ).resetBlocks( blocks );
- } );
-}
diff --git a/test/php/gutenberg-coding-standards/.phpcs.xml.dist b/test/php/gutenberg-coding-standards/.phpcs.xml.dist
index d6862f9a00fa9e..7f717f65374d7b 100644
--- a/test/php/gutenberg-coding-standards/.phpcs.xml.dist
+++ b/test/php/gutenberg-coding-standards/.phpcs.xml.dist
@@ -27,7 +27,6 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ /Gutenberg/Tests/.+$
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/NamingConventions/ValidBlockLibraryFunctionNameSniff.php b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/NamingConventions/ValidBlockLibraryFunctionNameSniff.php
index 5f72e378501278..74608921c32d89 100644
--- a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/NamingConventions/ValidBlockLibraryFunctionNameSniff.php
+++ b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/NamingConventions/ValidBlockLibraryFunctionNameSniff.php
@@ -114,22 +114,22 @@ private function processFunctionToken( File $phpcsFile, $stackPointer ) {
$parent_directory_name = basename( dirname( $phpcsFile->getFilename() ) );
$allowed_function_prefixes = array();
- $is_function_name_valid = false;
foreach ( $this->prefixes as $prefix ) {
$prefix = rtrim( $prefix, '_' );
$allowed_function_prefix = $prefix . '_' . self::sanitize_directory_name( $parent_directory_name );
$allowed_function_prefixes[] = $allowed_function_prefix;
// Validate the name's correctness and ensure it does not end with an underscore.
- $regexp = sprintf( '/^%s(|_.+)$/', preg_quote( $allowed_function_prefix, '/' ) );
- $is_function_name_valid |= ( 1 === preg_match( $regexp, $function_name ) );
- }
+ $regexp = sprintf( '/^%s(|_.+)$/', preg_quote( $allowed_function_prefix, '/' ) );
- if ( $is_function_name_valid ) {
- return;
+ if ( 1 === preg_match( $regexp, $function_name ) ) {
+ // The function has a valid prefix; bypassing further checks.
+ return;
+ }
}
- $error_message = "The function name `{$function_name}()` is invalid because PHP function names in this file should start with one of the following prefixes: `"
- . implode( '`, `', $allowed_function_prefixes ) . '`.';
+ $error_message = "The function name '{$function_name}()' is invalid."
+ . ' In this file, PHP function names must either match one of the allowed prefixes exactly or begin with one of them, followed by an underscore.'
+ . " The allowed prefixes are: '" . implode( "', '", $allowed_function_prefixes ) . "'.";
$phpcsFile->addError( $error_message, $function_token, 'FunctionNameInvalid' );
}
@@ -146,7 +146,7 @@ private function onRegisterEvent() {
* Sanitize a directory name by converting it to lowercase and replacing non-letter
* and non-digit characters with underscores.
*
- * @param string $directory_name
+ * @param string $directory_name Directory name.
*
* @return string
*/
diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php
new file mode 100644
index 00000000000000..794a088b7bc61d
--- /dev/null
+++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php
@@ -0,0 +1,116 @@
+ =>
+ */
+ public function getErrorList() {
+ return array(
+ 8 => 1,
+ 17 => 1,
+ 26 => 1,
+ 35 => 1,
+ );
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @return array =>
+ */
+ public function getWarningList() {
+ return array();
+ }
+
+ /**
+ *
+ * This method resets the 'Gutenberg' ruleset in the $GLOBALS['PHP_CODESNIFFER_RULESETS']
+ * to its original state.
+ */
+ public static function tearDownAfterClass() {
+ parent::tearDownAfterClass();
+
+ $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = self::$original_ruleset;
+ self::$original_ruleset = null;
+ }
+
+
+ /**
+ * Prepares the environment before executing tests. Specifically, sets prefixes for the
+ * ValidBlockLibraryFunctionName sniff.This is needed since AbstractSniffUnitTest class
+ * doesn't apply sniff properties from the Gutenberg/ruleset.xml file.
+ *
+ * @param string $filename The name of the file being tested.
+ * @param Config $config The config data for the run.
+ *
+ * @return void
+ */
+ public function setCliValues( $filename, $config ) {
+ parent::setCliValues( $filename, $config );
+
+ if ( ! isset( $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] )
+ || ( ! $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] instanceof Ruleset )
+ ) {
+ throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' );
+ }
+
+ // Backup the original Ruleset instance.
+ self::$original_ruleset = $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'];
+
+ $current_ruleset = clone self::$original_ruleset;
+ $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = $current_ruleset;
+
+ if ( ! isset( $current_ruleset->sniffs[ ValidBlockLibraryFunctionNameSniff::class ] )
+ || ( ! $current_ruleset->sniffs[ ValidBlockLibraryFunctionNameSniff::class ] instanceof ValidBlockLibraryFunctionNameSniff )
+ ) {
+ throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' );
+ }
+
+ $sniff = $current_ruleset->sniffs[ ValidBlockLibraryFunctionNameSniff::class ];
+ $sniff->prefixes = array(
+ 'block_core_',
+ 'render_block_core_',
+ 'register_block_core_',
+ );
+ }
+
+ /**
+ * Get a list of all test files to check.
+ *
+ * @param string $testFileBase The base path that the unit tests files will have.
+ *
+ * @return string[]
+ */
+ protected function getTestFiles( $testFileBase ) {
+ return array(
+ __DIR__ . '/../fixtures/block-library/src/my-block/index.inc',
+ );
+ }
+}
diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/fixtures/block-library/src/my-block/index.inc b/test/php/gutenberg-coding-standards/Gutenberg/Tests/fixtures/block-library/src/my-block/index.inc
new file mode 100644
index 00000000000000..3f99eb9ca6b8e8
--- /dev/null
+++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/fixtures/block-library/src/my-block/index.inc
@@ -0,0 +1,36 @@
+=5.4",
- "ext-filter": "*",
+ "php": ">=7.0",
+ "ext-libxml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlreader": "*",
"squizlabs/php_codesniffer": "^3.7.2"
},
"require-dev": {
"phpcompatibility/php-compatibility": "^9.0",
- "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0",
+ "phpunit/phpunit": "^5.0 || ^6.0 || ^7.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
- "dealerdirect/phpcodesniffer-composer-installer": "*",
- "wp-coding-standards/wpcs": "^2.2"
+ "wp-coding-standards/wpcs": "^3.0"
},
"suggest": {
"ext-mbstring": "For improved results"
@@ -47,7 +48,7 @@
"@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf"
],
"run-tests": [
- "@php ./vendor/phpunit/phpunit/phpunit -c phpunit.xml.dist --filter Gutenberg ./vendor/squizlabs/php_codesniffer/tests/AllTests.php"
+ "@php ./vendor/phpunit/phpunit/phpunit --filter Gutenberg ./vendor/squizlabs/php_codesniffer/tests/AllTests.php --no-coverage"
],
"check-all": [
"@lint",
diff --git a/tools/webpack/blocks.js b/tools/webpack/blocks.js
index 4104a791f29c6b..d599980b951a93 100644
--- a/tools/webpack/blocks.js
+++ b/tools/webpack/blocks.js
@@ -32,6 +32,7 @@ const prefixFunctions = [
'wp_enqueue_block_support_styles',
'wp_get_typography_font_size_value',
'wp_style_engine_get_styles',
+ 'wp_get_global_settings',
];
/**
diff --git a/tools/webpack/development.js b/tools/webpack/development.js
index cbfe9c40f6efcd..88ac4fba42f202 100644
--- a/tools/webpack/development.js
+++ b/tools/webpack/development.js
@@ -33,7 +33,7 @@ module.exports = [
name: 'react-refresh-runtime',
entry: {
'react-refresh-runtime': {
- import: 'react-refresh/runtime.js',
+ import: 'react-refresh/runtime',
library: {
name: 'ReactRefreshRuntime',
type: 'window',
diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js
index 3dc8407d7974b9..3f105886cd2d11 100644
--- a/tools/webpack/packages.js
+++ b/tools/webpack/packages.js
@@ -28,6 +28,7 @@ const BUNDLED_PACKAGES = [
'@wordpress/icons',
'@wordpress/interface',
'@wordpress/undo-manager',
+ '@wordpress/sync',
];
// PHP files in packages that have to be copied during build.