diff --git a/packages/block-library/src/columns/test/edit.native.js b/packages/block-library/src/columns/test/edit.native.js
index 565b0e695cad29..558041535aa574 100644
--- a/packages/block-library/src/columns/test/edit.native.js
+++ b/packages/block-library/src/columns/test/edit.native.js
@@ -93,6 +93,31 @@ describe( 'Columns block', () => {
expect( getEditorHtml() ).toMatchSnapshot();
} );
+ it( 'adds at least 15 Column blocks without limitation', async () => {
+ const screen = await initializeEditor( {
+ initialHtml: TWO_COLUMNS_BLOCK_HTML,
+ } );
+ const { getByLabelText } = screen;
+
+ // Get block
+ const columnsBlock = await getBlock( screen, 'Columns' );
+ fireEvent.press( columnsBlock );
+
+ // Open block settings
+ await openBlockSettings( screen );
+
+ // Update the number of columns
+ const columnsControl = getByLabelText( /Number of columns/ );
+
+ for ( let x = 0; x < 15; x++ ) {
+ fireEvent( columnsControl, 'accessibilityAction', {
+ nativeEvent: { actionName: 'increment' },
+ } );
+ }
+
+ expect( getEditorHtml() ).toMatchSnapshot();
+ } );
+
it( 'removes a column block when decrementing the value', async () => {
const screen = await initializeEditor( {
initialHtml: TWO_COLUMNS_BLOCK_HTML,
diff --git a/packages/block-library/src/comment-template/index.php b/packages/block-library/src/comment-template/index.php
index 6a8698f60f0ec1..3a553e802de0e7 100644
--- a/packages/block-library/src/comment-template/index.php
+++ b/packages/block-library/src/comment-template/index.php
@@ -30,8 +30,20 @@ function block_core_comment_template_render_comments( $comments, $block ) {
$context['commentId'] = $comment_id;
return $context;
};
+
+ /*
+ * We set commentId context through the `render_block_context` filter so
+ * that dynamically inserted blocks (at `render_block` filter stage)
+ * will also receive that context.
+ */
add_filter( 'render_block_context', $filter_block_context );
- $block_content = $block->render( array( 'dynamic' => false ) );
+
+ /*
+ * We construct a new WP_Block instance from the parsed block so that
+ * it'll receive any changes made by the `render_block_data` filter.
+ */
+ $block_content = ( new WP_Block( $block->parsed_block ) )->render( array( 'dynamic' => false ) );
+
remove_filter( 'render_block_context', $filter_block_context );
$children = $comment->get_children();
diff --git a/packages/block-library/src/navigation/interactivity.js b/packages/block-library/src/navigation/interactivity.js
index 80152762c9cd63..dfc9cb0f9c4a5a 100644
--- a/packages/block-library/src/navigation/interactivity.js
+++ b/packages/block-library/src/navigation/interactivity.js
@@ -17,12 +17,40 @@ const focusableSelectors = [
'[tabindex]:not([tabindex^="-"])',
];
+const openMenu = ( { context, ref }, menuOpenedOn ) => {
+ context.core.navigation.isMenuOpen[ menuOpenedOn ] = true;
+ context.core.navigation.previousFocus = ref;
+ if ( context.core.navigation.overlay ) {
+ // Add a `has-modal-open` class to the root.
+ document.documentElement.classList.add( 'has-modal-open' );
+ }
+};
+
+const closeMenu = ( { context, selectors }, menuClosedOn ) => {
+ context.core.navigation.isMenuOpen[ menuClosedOn ] = false;
+ // Check if the menu is still open or not.
+ if ( ! selectors.core.navigation.isMenuOpen( { context } ) ) {
+ if (
+ context.core.navigation.modal.contains(
+ window.document.activeElement
+ )
+ ) {
+ context.core.navigation.previousFocus.focus();
+ }
+ context.core.navigation.modal = null;
+ context.core.navigation.previousFocus = null;
+ if ( context.core.navigation.overlay ) {
+ document.documentElement.classList.remove( 'has-modal-open' );
+ }
+ }
+};
+
store( {
effects: {
core: {
navigation: {
- initMenu: ( { context, ref } ) => {
- if ( context.core.navigation.isMenuOpen ) {
+ initMenu: ( { context, selectors, ref } ) => {
+ if ( selectors.core.navigation.isMenuOpen( { context } ) ) {
const focusableElements =
ref.querySelectorAll( focusableSelectors );
context.core.navigation.modal = ref;
@@ -32,8 +60,8 @@ store( {
focusableElements[ focusableElements.length - 1 ];
}
},
- focusFirstElement: ( { context, ref } ) => {
- if ( context.core.navigation.isMenuOpen ) {
+ focusFirstElement: ( { context, selectors, ref } ) => {
+ if ( selectors.core.navigation.isMenuOpen( { context } ) ) {
ref.querySelector(
'.wp-block-navigation-item > *:first-child'
).focus();
@@ -45,62 +73,51 @@ store( {
selectors: {
core: {
navigation: {
- roleAttribute: ( { context } ) => {
- return context.core.navigation.overlay &&
- context.core.navigation.isMenuOpen
+ roleAttribute: ( { context, selectors } ) =>
+ context.core.navigation.overlay &&
+ selectors.core.navigation.isMenuOpen( { context } )
? 'dialog'
- : '';
- },
+ : '',
+ isMenuOpen: ( { context } ) =>
+ // The menu is opened if either `click` or `hover` is true.
+ Object.values( context.core.navigation.isMenuOpen ).filter(
+ Boolean
+ ).length > 0,
},
},
},
actions: {
core: {
navigation: {
- openMenu: ( { context, ref } ) => {
- context.core.navigation.isMenuOpen = true;
- context.core.navigation.previousFocus = ref;
- if ( context.core.navigation.overlay ) {
- // It adds a `has-modal-open` class to the root
- document.documentElement.classList.add(
- 'has-modal-open'
- );
- }
+ openMenuOnHover( args ) {
+ openMenu( args, 'hover' );
},
- closeMenu: ( { context } ) => {
- if ( context.core.navigation.isMenuOpen ) {
- context.core.navigation.isMenuOpen = false;
- if (
- context.core.navigation.modal.contains(
- window.document.activeElement
- )
- ) {
- context.core.navigation.previousFocus.focus();
- }
- context.core.navigation.modal = null;
- context.core.navigation.previousFocus = null;
- if ( context.core.navigation.overlay ) {
- document.documentElement.classList.remove(
- 'has-modal-open'
- );
- }
- }
+ closeMenuOnHover( args ) {
+ closeMenu( args, 'hover' );
},
- toggleMenu: ( { context, actions, ref } ) => {
- if ( context.core.navigation.isMenuOpen ) {
- actions.core.navigation.closeMenu( { context } );
+ openMenuOnClick( args ) {
+ openMenu( args, 'click' );
+ },
+ closeMenuOnClick( args ) {
+ closeMenu( args, 'click' );
+ },
+ toggleMenuOnClick: ( args ) => {
+ const { context } = args;
+ if ( context.core.navigation.isMenuOpen.click ) {
+ closeMenu( args, 'click' );
} else {
- actions.core.navigation.openMenu( { context, ref } );
+ openMenu( args, 'click' );
}
},
- handleMenuKeydown: ( { actions, context, event } ) => {
+ handleMenuKeydown: ( args ) => {
+ const { context, event } = args;
if ( context.core.navigation.isMenuOpen ) {
// If Escape close the menu
if (
event?.key === 'Escape' ||
event?.keyCode === 27
) {
- actions.core.navigation.closeMenu( { context } );
+ closeMenu( args, 'click' );
return;
}
@@ -129,20 +146,21 @@ store( {
}
}
},
- handleMenuFocusout: ( { actions, context, event } ) => {
- if ( context.core.navigation.isMenuOpen ) {
- // If focus is outside modal, and in the document, close menu
- // event.target === The element losing focus
- // event.relatedTarget === The element receiving focus (if any)
- // When focusout is outsite the document, `window.document.activeElement` doesn't change
- if (
- ! context.core.navigation.modal.contains(
- event.relatedTarget
- ) &&
- event.target !== window.document.activeElement
- ) {
- actions.core.navigation.closeMenu( { context } );
- }
+ handleMenuFocusout: ( args ) => {
+ const { context, event } = args;
+ // If focus is outside modal, and in the document, close menu
+ // event.target === The element losing focus
+ // event.relatedTarget === The element receiving focus (if any)
+ // When focusout is outsite the document,
+ // `window.document.activeElement` doesn't change
+ if (
+ context.core.navigation.isMenuOpen.click &&
+ ! context.core.navigation.modal.contains(
+ event.relatedTarget
+ ) &&
+ event.target !== window.document.activeElement
+ ) {
+ closeMenu( args, 'click' );
}
},
},
diff --git a/packages/block-library/src/post-template/index.php b/packages/block-library/src/post-template/index.php
index 3a3c207cf92ee2..cc73242f70fb63 100644
--- a/packages/block-library/src/post-template/index.php
+++ b/packages/block-library/src/post-template/index.php
@@ -85,17 +85,18 @@ function render_block_core_post_template( $attributes, $content, $block ) {
// This ensures that for the inner instances of the Post Template block, we do not render any block supports.
$block_instance['blockName'] = 'core/null';
+ $post_id = get_the_ID();
+ $post_type = get_post_type();
+ $filter_block_context = static function( $context ) use ( $post_id, $post_type ) {
+ $context['postType'] = $post_type;
+ $context['postId'] = $post_id;
+ return $context;
+ };
+ add_filter( 'render_block_context', $filter_block_context );
// Render the inner blocks of the Post Template block with `dynamic` set to `false` to prevent calling
// `render_callback` and ensure that no wrapper markup is included.
- $block_content = (
- new WP_Block(
- $block_instance,
- array(
- 'postType' => get_post_type(),
- 'postId' => get_the_ID(),
- )
- )
- )->render( array( 'dynamic' => false ) );
+ $block_content = ( new WP_Block( $block_instance ) )->render( array( 'dynamic' => false ) );
+ remove_filter( 'render_block_context', $filter_block_context );
// Wrap the render inner blocks in a `li` element with the appropriate post classes.
$post_classes = implode( ' ', get_post_class( 'wp-block-post' ) );
diff --git a/packages/block-library/src/query-pagination-next/block.json b/packages/block-library/src/query-pagination-next/block.json
index d4861519f149ee..7c4f5797efa903 100644
--- a/packages/block-library/src/query-pagination-next/block.json
+++ b/packages/block-library/src/query-pagination-next/block.json
@@ -12,7 +12,7 @@
"type": "string"
}
},
- "usesContext": [ "queryId", "query", "paginationArrow" ],
+ "usesContext": [ "queryId", "query", "paginationArrow", "showLabel" ],
"supports": {
"anchor": true,
"reusable": false,
diff --git a/packages/block-library/src/query-pagination-next/edit.js b/packages/block-library/src/query-pagination-next/edit.js
index c0b4916abccb3d..414a311283e7e2 100644
--- a/packages/block-library/src/query-pagination-next/edit.js
+++ b/packages/block-library/src/query-pagination-next/edit.js
@@ -13,7 +13,7 @@ const arrowMap = {
export default function QueryPaginationNextEdit( {
attributes: { label },
setAttributes,
- context: { paginationArrow },
+ context: { paginationArrow, showLabel },
} ) {
const displayArrow = arrowMap[ paginationArrow ];
return (
@@ -26,8 +26,8 @@ export default function QueryPaginationNextEdit( {
__experimentalVersion={ 2 }
tagName="span"
aria-label={ __( 'Next page link' ) }
- placeholder={ __( 'Next Page' ) }
- value={ label }
+ placeholder={ showLabel ? __( 'Next Page' ) : '' }
+ value={ showLabel ? label : '' }
onChange={ ( newLabel ) =>
setAttributes( { label: newLabel } )
}
diff --git a/packages/block-library/src/query-pagination-next/index.php b/packages/block-library/src/query-pagination-next/index.php
index e92b58938d53b8..bd8d86eef04afa 100644
--- a/packages/block-library/src/query-pagination-next/index.php
+++ b/packages/block-library/src/query-pagination-next/index.php
@@ -20,10 +20,15 @@ function render_block_core_query_pagination_next( $attributes, $content, $block
$max_page = isset( $block->context['query']['pages'] ) ? (int) $block->context['query']['pages'] : 0;
$wrapper_attributes = get_block_wrapper_attributes();
+ $show_label = isset( $block->context['showLabel'] ) ? (bool) $block->context['showLabel'] : true;
$default_label = __( 'Next Page' );
- $label = isset( $attributes['label'] ) && ! empty( $attributes['label'] ) ? esc_html( $attributes['label'] ) : $default_label;
+ $label_text = isset( $attributes['label'] ) && ! empty( $attributes['label'] ) ? esc_html( $attributes['label'] ) : $default_label;
+ $label = $show_label ? $label_text : '';
$pagination_arrow = get_query_pagination_arrow( $block, true );
+ if ( ! $label ) {
+ $wrapper_attributes .= ' aria-label="' . $label_text . '"';
+ }
if ( $pagination_arrow ) {
$label .= $pagination_arrow;
}
diff --git a/packages/block-library/src/query-pagination-previous/block.json b/packages/block-library/src/query-pagination-previous/block.json
index 823808b0fb054d..2efda01e20b6bd 100644
--- a/packages/block-library/src/query-pagination-previous/block.json
+++ b/packages/block-library/src/query-pagination-previous/block.json
@@ -12,7 +12,7 @@
"type": "string"
}
},
- "usesContext": [ "queryId", "query", "paginationArrow" ],
+ "usesContext": [ "queryId", "query", "paginationArrow", "showLabel" ],
"supports": {
"anchor": true,
"reusable": false,
diff --git a/packages/block-library/src/query-pagination-previous/edit.js b/packages/block-library/src/query-pagination-previous/edit.js
index c863d637f7fb51..12fdd5763b07f2 100644
--- a/packages/block-library/src/query-pagination-previous/edit.js
+++ b/packages/block-library/src/query-pagination-previous/edit.js
@@ -13,7 +13,7 @@ const arrowMap = {
export default function QueryPaginationPreviousEdit( {
attributes: { label },
setAttributes,
- context: { paginationArrow },
+ context: { paginationArrow, showLabel },
} ) {
const displayArrow = arrowMap[ paginationArrow ];
return (
@@ -34,8 +34,8 @@ export default function QueryPaginationPreviousEdit( {
__experimentalVersion={ 2 }
tagName="span"
aria-label={ __( 'Previous page link' ) }
- placeholder={ __( 'Previous Page' ) }
- value={ label }
+ placeholder={ showLabel ? __( 'Previous Page' ) : '' }
+ value={ showLabel ? label : '' }
onChange={ ( newLabel ) =>
setAttributes( { label: newLabel } )
}
diff --git a/packages/block-library/src/query-pagination-previous/index.php b/packages/block-library/src/query-pagination-previous/index.php
index 9bb9f0d1f2bfbf..15bae30a041491 100644
--- a/packages/block-library/src/query-pagination-previous/index.php
+++ b/packages/block-library/src/query-pagination-previous/index.php
@@ -19,9 +19,14 @@ function render_block_core_query_pagination_previous( $attributes, $content, $bl
$page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ];
$wrapper_attributes = get_block_wrapper_attributes();
+ $show_label = isset( $block->context['showLabel'] ) ? (bool) $block->context['showLabel'] : true;
$default_label = __( 'Previous Page' );
- $label = isset( $attributes['label'] ) && ! empty( $attributes['label'] ) ? esc_html( $attributes['label'] ) : $default_label;
+ $label_text = isset( $attributes['label'] ) && ! empty( $attributes['label'] ) ? esc_html( $attributes['label'] ) : $default_label;
+ $label = $show_label ? $label_text : '';
$pagination_arrow = get_query_pagination_arrow( $block, false );
+ if ( ! $label ) {
+ $wrapper_attributes .= ' aria-label="' . $label_text . '"';
+ }
if ( $pagination_arrow ) {
$label = $pagination_arrow . $label;
}
diff --git a/packages/block-library/src/query-pagination/block.json b/packages/block-library/src/query-pagination/block.json
index fa980575ec969d..7217a3eb4f1a38 100644
--- a/packages/block-library/src/query-pagination/block.json
+++ b/packages/block-library/src/query-pagination/block.json
@@ -11,11 +11,16 @@
"paginationArrow": {
"type": "string",
"default": "none"
+ },
+ "showLabel": {
+ "type": "boolean",
+ "default": true
}
},
"usesContext": [ "queryId", "query" ],
"providesContext": {
- "paginationArrow": "paginationArrow"
+ "paginationArrow": "paginationArrow",
+ "showLabel": "showLabel"
},
"supports": {
"anchor": true,
diff --git a/packages/block-library/src/query-pagination/edit.js b/packages/block-library/src/query-pagination/edit.js
index 28e6af19ace347..3bd003f2f8a2d8 100644
--- a/packages/block-library/src/query-pagination/edit.js
+++ b/packages/block-library/src/query-pagination/edit.js
@@ -15,6 +15,7 @@ import { PanelBody } from '@wordpress/components';
* Internal dependencies
*/
import { QueryPaginationArrowControls } from './query-pagination-arrow-controls';
+import { QueryPaginationLabelControl } from './query-pagination-label-control';
const TEMPLATE = [
[ 'core/query-pagination-previous' ],
@@ -28,7 +29,7 @@ const ALLOWED_BLOCKS = [
];
export default function QueryPaginationEdit( {
- attributes: { paginationArrow },
+ attributes: { paginationArrow, showLabel },
setAttributes,
clientId,
} ) {
@@ -36,7 +37,7 @@ export default function QueryPaginationEdit( {
const { getBlocks } = select( blockEditorStore );
const innerBlocks = getBlocks( clientId );
/**
- * Show the `paginationArrow` control only if a
+ * Show the `paginationArrow` and `showLabel` controls only if a
* `QueryPaginationNext/Previous` block exists.
*/
return innerBlocks?.find( ( innerBlock ) => {
@@ -51,6 +52,10 @@ export default function QueryPaginationEdit( {
template: TEMPLATE,
allowedBlocks: ALLOWED_BLOCKS,
} );
+ // Always show label text if paginationArrow is set to 'none'.
+ if ( paginationArrow === 'none' ) {
+ setAttributes( { showLabel: true } );
+ }
return (
<>
{ hasNextPreviousBlocks && (
@@ -62,6 +67,14 @@ export default function QueryPaginationEdit( {
setAttributes( { paginationArrow: value } );
} }
/>
+ { paginationArrow !== 'none' && (
+
{
+ setAttributes( { showLabel: value } );
+ } }
+ />
+ ) }
) }
diff --git a/packages/block-library/src/query-pagination/query-pagination-label-control.js b/packages/block-library/src/query-pagination/query-pagination-label-control.js
new file mode 100644
index 00000000000000..9ff80a663adeb5
--- /dev/null
+++ b/packages/block-library/src/query-pagination/query-pagination-label-control.js
@@ -0,0 +1,19 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { ToggleControl } from '@wordpress/components';
+
+export function QueryPaginationLabelControl( { value, onChange } ) {
+ return (
+
+ );
+}
diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index 4d6ea42e8bc0e1..e1c10790e1746a 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -2,14 +2,22 @@
## Unreleased
+### Enhancements
+
+- `Button`: Add `__next32pxSmallSize` prop to opt into the new 32px size when the `isSmall` prop is enabled ([#51012](https://github.com/WordPress/gutenberg/pull/51012)).
+
### Bug Fix
- `FocalPointUnitControl`: Add aria-labels ([#50993](https://github.com/WordPress/gutenberg/pull/50993)).
+### Enhancements
+
+- Wrapped `TabPanel` in a `forwardRef` call ([#50199](https://github.com/WordPress/gutenberg/pull/50199)).
+
### Experimental
- `DropdownMenu` v2: Tweak styles ([#50967](https://github.com/WordPress/gutenberg/pull/50967)).
-
+- `DropdownMenu` v2: Render in the default `Popover.Slot` ([#51046](https://github.com/WordPress/gutenberg/pull/51046)).
## 25.0.0 (2023-05-24)
diff --git a/packages/components/src/button/index.tsx b/packages/components/src/button/index.tsx
index 098f6eaac3b1cf..24932ab06a9b69 100644
--- a/packages/components/src/button/index.tsx
+++ b/packages/components/src/button/index.tsx
@@ -76,6 +76,7 @@ export function UnforwardedButton(
) {
const {
__next40pxDefaultSize,
+ __next32pxSmallSize,
isSmall,
isPressed,
isBusy,
@@ -117,6 +118,7 @@ export function UnforwardedButton(
const classes = classnames( 'components-button', className, {
'is-next-40px-default-size': __next40pxDefaultSize,
+ 'is-next-32px-small-size': __next32pxSmallSize,
'is-secondary': variant === 'secondary',
'is-primary': variant === 'primary',
'is-small': isSmall,
diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss
index fefc93b4fa8e0e..5916876aad589d 100644
--- a/packages/components/src/button/style.scss
+++ b/packages/components/src/button/style.scss
@@ -263,15 +263,24 @@
}
&.is-small {
- height: $icon-size;
+ height: $button-size-small-next-default-32px;
line-height: 22px;
padding: 0 8px;
font-size: 11px;
&.has-icon:not(.has-text) {
padding: 0;
- width: $icon-size;
- min-width: $icon-size;
+ width: $button-size-small-next-default-32px;
+ min-width: $button-size-small-next-default-32px;
+ }
+
+ &:not(.is-next-32px-small-size) {
+ height: $button-size-small;
+
+ &.has-icon:not(.has-text) {
+ width: $button-size-small;
+ min-width: $button-size-small;
+ }
}
}
diff --git a/packages/components/src/button/types.ts b/packages/components/src/button/types.ts
index 57e2a3df6ee61e..85188476e5f370 100644
--- a/packages/components/src/button/types.ts
+++ b/packages/components/src/button/types.ts
@@ -25,6 +25,15 @@ type BaseButtonProps = {
* @default false
*/
__next40pxDefaultSize?: boolean;
+ /**
+ * Start opting into the larger `isSmall` button size that will become the
+ * default small size in a future version.
+ *
+ * Only takes effect when the `isSmall` prop is `true`.
+ *
+ * @default false
+ */
+ __next32pxSmallSize?: boolean;
/**
* The button's children.
*/
diff --git a/packages/components/src/dropdown-menu-v2/index.tsx b/packages/components/src/dropdown-menu-v2/index.tsx
index 7a0197f69fc3dd..b0617edd77f9f0 100644
--- a/packages/components/src/dropdown-menu-v2/index.tsx
+++ b/packages/components/src/dropdown-menu-v2/index.tsx
@@ -6,7 +6,7 @@ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
/**
* WordPress dependencies
*/
-import { forwardRef } from '@wordpress/element';
+import { forwardRef, createContext, useContext } from '@wordpress/element';
import { isRTL } from '@wordpress/i18n';
import { check, chevronRightSmall, lineSolid } from '@wordpress/icons';
import { SVG, Circle } from '@wordpress/primitives';
@@ -14,7 +14,9 @@ import { SVG, Circle } from '@wordpress/primitives';
/**
* Internal dependencies
*/
+import { useSlot } from '../slot-fill';
import Icon from '../icon';
+import { SLOT_NAME as POPOVER_DEFAULT_SLOT_NAME } from '../popover';
import * as DropdownMenuStyled from './styles';
import type {
DropdownMenuProps,
@@ -34,6 +36,12 @@ const SUB_MENU_OFFSET_SIDE = 12;
// Opposite amount of the top padding of the menu item
const SUB_MENU_OFFSET_ALIGN = -8;
+const DropdownMenuPrivateContext = createContext< {
+ portalContainer: HTMLElement | null;
+} >( {
+ portalContainer: null,
+} );
+
/**
* `DropdownMenu` displays a menu to the user (such as a set of actions
* or functions) triggered by a button.
@@ -53,6 +61,10 @@ export const DropdownMenu = ( {
children,
trigger,
}: DropdownMenuProps ) => {
+ // Render the portal in the default slot used by the legacy Popover component.
+ const slot = useSlot( POPOVER_DEFAULT_SLOT_NAME );
+ const portalContainer = slot.ref?.current;
+
return (
{ trigger }
-
+
- { children }
+
+ { children }
+
@@ -118,6 +134,8 @@ export const DropdownSubMenu = ( {
children,
trigger,
}: DropdownSubMenuProps ) => {
+ const { portalContainer } = useContext( DropdownMenuPrivateContext );
+
return (
{ trigger }
-
+
{
};
const Template: ComponentStory< typeof DropdownMenu > = ( props ) => (
-
+
+
+ { /* @ts-expect-error Slot is not currently typed on Popover */ }
+
+
);
export const Default = Template.bind( {} );
Default.args = {
diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx
index e1afb10035c306..0e7fb35c5f833f 100644
--- a/packages/components/src/popover/index.tsx
+++ b/packages/components/src/popover/index.tsx
@@ -74,7 +74,7 @@ import { overlayMiddlewares } from './overlay-middlewares';
*
* @type {string}
*/
-const SLOT_NAME = 'Popover';
+export const SLOT_NAME = 'Popover';
// An SVG displaying a triangle facing down, filled with a solid
// color and bordered in such a way to create an arrow-like effect.
diff --git a/packages/components/src/tab-panel/index.tsx b/packages/components/src/tab-panel/index.tsx
index 4ed4dc76da22de..20063b2315dd39 100644
--- a/packages/components/src/tab-panel/index.tsx
+++ b/packages/components/src/tab-panel/index.tsx
@@ -2,11 +2,13 @@
* External dependencies
*/
import classnames from 'classnames';
+import type { ForwardedRef } from 'react';
/**
* WordPress dependencies
*/
import {
+ forwardRef,
useState,
useEffect,
useLayoutEffect,
@@ -76,16 +78,19 @@ const TabButton = ( {
* );
* ```
*/
-export function TabPanel( {
- className,
- children,
- tabs,
- selectOnMove = true,
- initialTabName,
- orientation = 'horizontal',
- activeClass = 'is-active',
- onSelect,
-}: WordPressComponentProps< TabPanelProps, 'div', false > ) {
+const UnforwardedTabPanel = (
+ {
+ className,
+ children,
+ tabs,
+ selectOnMove = true,
+ initialTabName,
+ orientation = 'horizontal',
+ activeClass = 'is-active',
+ onSelect,
+ }: WordPressComponentProps< TabPanelProps, 'div', false >,
+ ref: ForwardedRef< any >
+) => {
const instanceId = useInstanceId( TabPanel, 'tab-panel' );
const [ selected, setSelected ] = useState< string >();
@@ -151,7 +156,7 @@ export function TabPanel( {
}, [ tabs, selectedTab?.disabled, handleTabSelection ] );
return (
-
+
);
-}
+};
+export const TabPanel = forwardRef( UnforwardedTabPanel );
export default TabPanel;
diff --git a/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js b/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js
index b1c5a7278efda8..2d05fc08baf7fd 100644
--- a/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js
+++ b/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js
@@ -141,7 +141,7 @@ describe( 'Navigating the block hierarchy', () => {
// Navigate to the third column in the columns block.
await pressKeyWithModifier( 'ctrlShift', '`' );
await pressKeyWithModifier( 'ctrlShift', '`' );
- await pressKeyTimes( 'Tab', 4 );
+ await pressKeyTimes( 'Tab', 3 );
await pressKeyTimes( 'ArrowDown', 4 );
await page.waitForSelector(
'.is-highlighted[aria-label="Block: Column (3 of 3)"]'
diff --git a/packages/e2e-tests/specs/editor/various/links.test.js b/packages/e2e-tests/specs/editor/various/links.test.js
index 880f786bd4cbe7..fdcb7c5f793ade 100644
--- a/packages/e2e-tests/specs/editor/various/links.test.js
+++ b/packages/e2e-tests/specs/editor/various/links.test.js
@@ -17,9 +17,16 @@ describe( 'Links', () => {
} );
const waitForURLFieldAutoFocus = async () => {
- await page.waitForFunction(
- () => !! document.activeElement.closest( '.block-editor-url-input' )
- );
+ await page.waitForFunction( () => {
+ const input = document.querySelector(
+ '.block-editor-url-input__input'
+ );
+ if ( input ) {
+ input.focus();
+ return true;
+ }
+ return false;
+ } );
};
it( 'will use Post title as link text if link to existing post is created without any text selected', async () => {
@@ -520,11 +527,6 @@ describe( 'Links', () => {
await waitForURLFieldAutoFocus();
- const [ settingsToggle ] = await page.$x(
- '//button[contains(@aria-label, "Link Settings")]'
- );
- await settingsToggle.click();
-
await page.keyboard.press( 'Tab' );
// Tabbing should land us in the text input.
@@ -582,15 +584,6 @@ describe( 'Links', () => {
await editButton.click();
- await waitForURLFieldAutoFocus();
-
- const [ settingsToggle ] = await page.$x(
- '//button[contains(@aria-label, "Link Settings")]'
- );
- await settingsToggle.click();
-
- await page.keyboard.press( 'Tab' );
-
// Tabbing back should land us in the text input.
const textInputValue = await page.evaluate(
() => document.activeElement.value
@@ -617,14 +610,6 @@ describe( 'Links', () => {
'//button[contains(@aria-label, "Edit")]'
);
await editButton.click();
- await waitForURLFieldAutoFocus();
-
- const [ settingsToggle ] = await page.$x(
- '//button[contains(@aria-label, "Link Settings")]'
- );
- await settingsToggle.click();
-
- await page.keyboard.press( 'Tab' );
// Tabbing should land us in the text input.
const textInputValue = await page.evaluate(
@@ -679,7 +664,7 @@ describe( 'Links', () => {
await page.waitForXPath( `//label[text()='Open in new tab']` );
// Move focus back to RichText for the underlying link.
- await pressKeyTimes( 'Tab', 4 );
+ await pressKeyTimes( 'Tab', 3 );
// Make a selection within the RichText.
await pressKeyWithModifier( 'shift', 'ArrowRight' );
@@ -687,7 +672,7 @@ describe( 'Links', () => {
await pressKeyWithModifier( 'shift', 'ArrowRight' );
// Move back to the text input.
- await pressKeyTimes( 'Tab', 3 );
+ await pressKeyTimes( 'Tab', 2 );
// Tabbing back should land us in the text input.
const textInputValue = await page.evaluate(
@@ -887,10 +872,6 @@ describe( 'Links', () => {
await waitForURLFieldAutoFocus();
- // Link settings open
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Space' );
-
// Move to Link Text field.
await page.keyboard.press( 'Tab' );
diff --git a/packages/edit-post/src/components/header/document-title/index.js b/packages/edit-post/src/components/header/document-title/index.js
new file mode 100644
index 00000000000000..1b27a0bacf014b
--- /dev/null
+++ b/packages/edit-post/src/components/header/document-title/index.js
@@ -0,0 +1,89 @@
+/**
+ * WordPress dependencies
+ */
+import { __, isRTL } from '@wordpress/i18n';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { BlockIcon, store as blockEditorStore } from '@wordpress/block-editor';
+import {
+ Button,
+ VisuallyHidden,
+ __experimentalHStack as HStack,
+ __experimentalText as Text,
+} from '@wordpress/components';
+import { layout, chevronLeftSmall, chevronRightSmall } from '@wordpress/icons';
+import { privateApis as commandsPrivateApis } from '@wordpress/commands';
+import { displayShortcut } from '@wordpress/keycodes';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../../../private-apis';
+import { store as editPostStore } from '../../../store';
+
+const { store: commandsStore } = unlock( commandsPrivateApis );
+
+function DocumentTitle() {
+ const { template, isEditing } = useSelect( ( select ) => {
+ const { isEditingTemplate, getEditedPostTemplate } =
+ select( editPostStore );
+ const _isEditing = isEditingTemplate();
+
+ return {
+ template: _isEditing ? getEditedPostTemplate() : null,
+ isEditing: _isEditing,
+ };
+ }, [] );
+ const { clearSelectedBlock } = useDispatch( blockEditorStore );
+ const { setIsEditingTemplate } = useDispatch( editPostStore );
+ const { open: openCommandCenter } = useDispatch( commandsStore );
+
+ if ( ! isEditing || ! template ) {
+ return null;
+ }
+
+ let templateTitle = __( 'Default' );
+ if ( template?.title ) {
+ templateTitle = template.title;
+ } else if ( !! template ) {
+ templateTitle = template.slug;
+ }
+
+ return (
+
+
+ {
+ clearSelectedBlock();
+ setIsEditingTemplate( false );
+ } }
+ icon={ isRTL() ? chevronRightSmall : chevronLeftSmall }
+ >
+ { __( 'Back' ) }
+
+
+
+ openCommandCenter() }
+ >
+
+
+
+
+ { __( 'Editing template: ' ) }
+
+ { templateTitle }
+
+
+
+ openCommandCenter() }
+ >
+ { displayShortcut.primary( 'k' ) }
+
+
+ );
+}
+
+export default DocumentTitle;
diff --git a/packages/edit-post/src/components/header/document-title/style.scss b/packages/edit-post/src/components/header/document-title/style.scss
new file mode 100644
index 00000000000000..e39ecf607e4306
--- /dev/null
+++ b/packages/edit-post/src/components/header/document-title/style.scss
@@ -0,0 +1,61 @@
+.edit-post-document-title {
+ display: flex;
+ align-items: center;
+ gap: $grid-unit;
+ height: $button-size;
+ justify-content: space-between;
+ // Flex items will, by default, refuse to shrink below a minimum
+ // intrinsic width. In order to shrink this flexbox item, and
+ // subsequently truncate child text, we set an explicit min-width.
+ // See https://dev.w3.org/csswg/css-flexbox/#min-size-auto
+ min-width: 0;
+ background: $gray-100;
+ border-radius: 4px;
+ width: min(100%, 450px);
+
+ &:hover {
+ color: currentColor;
+ background: $gray-200;
+ }
+}
+
+.edit-post-document-title__title.components-button {
+ flex-grow: 1;
+ color: var(--wp-block-synced-color);
+ overflow: hidden;
+
+ &:hover {
+ color: var(--wp-block-synced-color);
+ }
+
+ h1 {
+ color: var(--wp-block-synced-color);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+
+.edit-post-document-title__shortcut {
+ flex-shrink: 0;
+ color: $gray-700;
+ padding: 0 $grid-unit-15;
+
+ &:hover {
+ color: $gray-700;
+ }
+}
+
+.edit-post-document-title__left {
+ min-width: $button-size;
+ flex-shrink: 0;
+
+ .components-button.has-icon.has-text {
+ color: $gray-700;
+ gap: 0;
+
+ &:hover {
+ color: currentColor;
+ }
+ }
+}
diff --git a/packages/edit-post/src/components/header/header-toolbar/style.scss b/packages/edit-post/src/components/header/header-toolbar/style.scss
index 87aec00004c02b..694dcb5a2d678a 100644
--- a/packages/edit-post/src/components/header/header-toolbar/style.scss
+++ b/packages/edit-post/src/components/header/header-toolbar/style.scss
@@ -1,6 +1,5 @@
.edit-post-header-toolbar {
display: inline-flex;
- flex-grow: 1;
align-items: center;
border: none;
diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js
index 09a93424f6903d..3306a0fdf1606a 100644
--- a/packages/edit-post/src/components/header/index.js
+++ b/packages/edit-post/src/components/header/index.js
@@ -18,7 +18,7 @@ import { default as DevicePreview } from '../device-preview';
import ViewLink from '../view-link';
import MainDashboardButton from './main-dashboard-button';
import { store as editPostStore } from '../../store';
-import TemplateTitle from './template-title';
+import DocumentTitle from './document-title';
function Header( { setEntitiesSavedStatesCallback } ) {
const isLargeViewport = useViewportMatch( 'large' );
@@ -70,7 +70,9 @@ function Header( { setEntitiesSavedStatesCallback } ) {
className="edit-post-header__toolbar"
>
-
+
+
+
{
- const { isEditingTemplate, getEditedPostTemplate } =
- select( editPostStore );
- const _isEditing = isEditingTemplate();
- return {
- template: _isEditing ? getEditedPostTemplate() : null,
- };
- }, [] );
- const [ showConfirmDialog, setShowConfirmDialog ] = useState( false );
-
- if ( ! template || ! template.wp_id ) {
- return null;
- }
- let templateTitle = template.slug;
- if ( template?.title ) {
- templateTitle = template.title;
- }
-
- const isRevertable = template?.has_theme_file;
-
- const onDelete = () => {
- clearSelectedBlock();
- setIsEditingTemplate( false );
- setShowConfirmDialog( false );
-
- editPost( {
- template: '',
- } );
- const settings = getEditorSettings();
- const newAvailableTemplates = Object.fromEntries(
- Object.entries( settings.availableTemplates ?? {} ).filter(
- ( [ id ] ) => id !== template.slug
- )
- );
- updateEditorSettings( {
- availableTemplates: newAvailableTemplates,
- } );
- deleteEntityRecord( 'postType', 'wp_template', template.id, {
- throwOnError: true,
- } );
- };
-
- return (
-
- <>
- {
- setShowConfirmDialog( true );
- } }
- info={
- isRevertable
- ? __( 'Use the template as supplied by the theme.' )
- : undefined
- }
- >
- { isRevertable
- ? __( 'Clear customizations' )
- : __( 'Delete template' ) }
-
- {
- setShowConfirmDialog( false );
- } }
- >
- { sprintf(
- /* translators: %s: template name */
- __(
- 'Are you sure you want to delete the %s template? It may be used by other pages or posts.'
- ),
- templateTitle
- ) }
-
- >
-
- );
-}
diff --git a/packages/edit-post/src/components/header/template-title/edit-template-title.js b/packages/edit-post/src/components/header/template-title/edit-template-title.js
deleted file mode 100644
index 447ea5e4e02d72..00000000000000
--- a/packages/edit-post/src/components/header/template-title/edit-template-title.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { __ } from '@wordpress/i18n';
-import { TextControl } from '@wordpress/components';
-import { useDispatch, useSelect } from '@wordpress/data';
-import { useState } from '@wordpress/element';
-import { store as editorStore } from '@wordpress/editor';
-import { store as coreStore } from '@wordpress/core-data';
-
-/**
- * Internal dependencies
- */
-import { store as editPostStore } from '../../../store';
-
-export default function EditTemplateTitle() {
- const [ forceEmpty, setForceEmpty ] = useState( false );
- const { template } = useSelect( ( select ) => {
- const { getEditedPostTemplate } = select( editPostStore );
- return {
- template: getEditedPostTemplate(),
- };
- }, [] );
-
- const { editEntityRecord } = useDispatch( coreStore );
- const { getEditorSettings } = useSelect( editorStore );
- const { updateEditorSettings } = useDispatch( editorStore );
-
- // Only user-created and non-default templates can change the name.
- if ( ! template.is_custom || template.has_theme_file ) {
- return null;
- }
-
- let templateTitle = __( 'Default' );
- if ( template?.title ) {
- templateTitle = template.title;
- } else if ( !! template ) {
- templateTitle = template.slug;
- }
-
- return (
-
- {
- // Allow having the field temporarily empty while typing.
- if ( ! newTitle && ! forceEmpty ) {
- setForceEmpty( true );
- return;
- }
- setForceEmpty( false );
-
- const settings = getEditorSettings();
- const newAvailableTemplates = Object.fromEntries(
- Object.entries( settings.availableTemplates ?? {} ).map(
- ( [ id, existingTitle ] ) => [
- id,
- id !== template.slug ? existingTitle : newTitle,
- ]
- )
- );
- updateEditorSettings( {
- availableTemplates: newAvailableTemplates,
- } );
- editEntityRecord( 'postType', 'wp_template', template.id, {
- title: newTitle,
- } );
- } }
- onBlur={ () => setForceEmpty( false ) }
- />
-
- );
-}
diff --git a/packages/edit-post/src/components/header/template-title/index.js b/packages/edit-post/src/components/header/template-title/index.js
deleted file mode 100644
index c0745dc0451b74..00000000000000
--- a/packages/edit-post/src/components/header/template-title/index.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { __, sprintf } from '@wordpress/i18n';
-import { useSelect, useDispatch } from '@wordpress/data';
-import {
- Dropdown,
- Button,
- __experimentalText as Text,
-} from '@wordpress/components';
-import { chevronDown } from '@wordpress/icons';
-
-/**
- * Internal dependencies
- */
-import { store as editPostStore } from '../../../store';
-import { store as blockEditorStore } from '@wordpress/block-editor';
-import { store as editorStore } from '@wordpress/editor';
-import DeleteTemplate from './delete-template';
-import EditTemplateTitle from './edit-template-title';
-import TemplateDescription from './template-description';
-
-function TemplateTitle() {
- const { template, isEditing, title } = useSelect( ( select ) => {
- const { isEditingTemplate, getEditedPostTemplate } =
- select( editPostStore );
- const { getEditedPostAttribute } = select( editorStore );
-
- const _isEditing = isEditingTemplate();
-
- return {
- template: _isEditing ? getEditedPostTemplate() : null,
- isEditing: _isEditing,
- title: getEditedPostAttribute( 'title' )
- ? getEditedPostAttribute( 'title' )
- : __( 'Untitled' ),
- };
- }, [] );
-
- const { clearSelectedBlock } = useDispatch( blockEditorStore );
- const { setIsEditingTemplate } = useDispatch( editPostStore );
-
- if ( ! isEditing || ! template ) {
- return null;
- }
-
- let templateTitle = __( 'Default' );
- if ( template?.title ) {
- templateTitle = template.title;
- } else if ( !! template ) {
- templateTitle = template.slug;
- }
-
- const hasOptions = !! (
- template.custom ||
- template.wp_id ||
- template.description
- );
-
- return (
-
- {
- clearSelectedBlock();
- setIsEditingTemplate( false );
- } }
- >
- { title }
-
- { hasOptions ? (
- (
-
- { templateTitle }
-
- ) }
- renderContent={ () => (
- <>
-
-
-
- >
- ) }
- />
- ) : (
-
- { templateTitle }
-
- ) }
-
- );
-}
-
-export default TemplateTitle;
diff --git a/packages/edit-post/src/components/header/template-title/style.scss b/packages/edit-post/src/components/header/template-title/style.scss
deleted file mode 100644
index b5fe5120bfb64c..00000000000000
--- a/packages/edit-post/src/components/header/template-title/style.scss
+++ /dev/null
@@ -1,94 +0,0 @@
-.edit-post-template-top-area {
- display: flex;
- flex-direction: column;
- align-content: space-between;
- width: 100%;
- align-items: center;
-
- .edit-post-template-title,
- .edit-post-template-post-title {
- padding: 0;
- text-decoration: none;
- height: auto;
-
- &::before {
- height: 100%;
- }
-
- &.has-icon {
- svg {
- order: 1;
- margin-right: 0;
- }
- }
- }
-
- .edit-post-template-title {
- color: $gray-900;
- }
-
- .edit-post-template-post-title {
- margin-top: $grid-unit-05;
- max-width: 160px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: block;
-
- &::before {
- left: 0;
- right: 0;
- }
-
- @include break-xlarge() {
- max-width: 400px;
- }
- }
-}
-
-.edit-post-template-top-area__popover {
- .components-popover__content {
- min-width: 280px;
- padding: 0;
- }
-
- .edit-site-template-details__group {
- padding: $grid-unit-20;
-
- .components-base-control__help {
- margin-bottom: 0;
- }
- }
-
- .edit-post-template-details__description {
- color: $gray-700;
- }
-}
-
-.edit-post-template-top-area__second-menu-group {
- border-top: $border-width solid $gray-300;
- padding: $grid-unit-20 $grid-unit-10;
-
- .edit-post-template-top-area__delete-template-button {
- display: flex;
- justify-content: center;
- padding: $grid-unit-05 $grid-unit;
-
- &.is-destructive {
- padding: inherit;
- margin-left: $grid-unit-10;
- margin-right: $grid-unit-10;
- width: calc(100% - #{($grid-unit * 2)});
-
- .components-menu-item__item {
- width: auto;
- }
- }
-
- .components-menu-item__item {
- margin-right: 0;
- min-width: 0;
- width: 100%;
- }
- }
-}
diff --git a/packages/edit-post/src/components/header/template-title/template-description.js b/packages/edit-post/src/components/header/template-title/template-description.js
deleted file mode 100644
index 3513496852c339..00000000000000
--- a/packages/edit-post/src/components/header/template-title/template-description.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { useSelect } from '@wordpress/data';
-import {
- __experimentalHeading as Heading,
- __experimentalText as Text,
-} from '@wordpress/components';
-
-/**
- * Internal dependencies
- */
-import { store as editPostStore } from '../../../store';
-
-export default function TemplateDescription() {
- const { description, title } = useSelect( ( select ) => {
- const { getEditedPostTemplate } = select( editPostStore );
- return {
- title: getEditedPostTemplate().title,
- description: getEditedPostTemplate().description,
- };
- }, [] );
- if ( ! description ) {
- return null;
- }
-
- return (
-
-
- { title }
-
-
- { description }
-
-
- );
-}
diff --git a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js
index 69aaf573b840ec..a4e6e639fbd76d 100644
--- a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js
+++ b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js
@@ -1,13 +1,8 @@
-/**
- * External dependencies
- */
-import classnames from 'classnames';
-
/**
* WordPress dependencies
*/
import { __experimentalListView as ListView } from '@wordpress/block-editor';
-import { Button } from '@wordpress/components';
+import { Button, TabPanel } from '@wordpress/components';
import {
useFocusOnMount,
useFocusReturn,
@@ -30,7 +25,9 @@ import ListViewOutline from './list-view-outline';
export default function ListViewSidebar() {
const { setIsListViewOpened } = useDispatch( editPostStore );
+ // 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();
@@ -44,18 +41,24 @@ export default function ListViewSidebar() {
// Use internal state instead of a ref to make sure that the component
// re-renders when the dropZoneElement updates.
const [ dropZoneElement, setDropZoneElement ] = useState( null );
-
+ // Tracks our current tab.
const [ tab, setTab ] = useState( 'list-view' );
// This ref refers to the sidebar as a whole.
const sidebarRef = useRef();
- // This ref refers to the list view tab button.
- const listViewTabRef = useRef();
- // This ref refers to the outline tab button.
- const outlineTabRef = useRef();
+ // This ref refers to the tab panel.
+ const tabPanelRef = useRef();
// This ref refers to the list view application area.
const listViewRef = useRef();
+ // Must merge the refs together so focus can be handled properly in the next function.
+ const listViewContainerRef = useMergeRefs( [
+ contentFocusReturnRef,
+ focusOnMountRef,
+ listViewRef,
+ setDropZoneElement,
+ ] );
+
/*
* Callback function to handle list view or outline focus.
*
@@ -64,9 +67,11 @@ export default function ListViewSidebar() {
* @return void
*/
function handleSidebarFocus( currentTab ) {
+ // Tab panel focus.
+ const tabPanelFocus = focus.tabbable.find( tabPanelRef.current )[ 0 ];
// List view tab is selected.
if ( currentTab === 'list-view' ) {
- // Either focus the list view or the list view tab button. Must have a fallback because the list view does not render when there are no blocks.
+ // Either focus the list view or the tab panel. Must have a fallback because the list view does not render when there are no blocks.
const listViewApplicationFocus = focus.tabbable.find(
listViewRef.current
)[ 0 ];
@@ -74,11 +79,11 @@ export default function ListViewSidebar() {
listViewApplicationFocus
)
? listViewApplicationFocus
- : listViewTabRef.current;
+ : tabPanelFocus;
listViewFocusArea.focus();
// Outline tab is selected.
} else {
- outlineTabRef.current.focus();
+ tabPanelFocus.focus();
}
}
@@ -97,6 +102,22 @@ export default function ListViewSidebar() {
}
} );
+ /**
+ * Render tab content for a given tab name.
+ *
+ * @param {string} tabName The name of the tab to render.
+ */
+ function renderTabContent( tabName ) {
+ if ( tabName === 'list-view' ) {
+ return (
+
+
+
+ );
+ }
+ return ;
+ }
+
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
-
setIsListViewOpened( false ) }
+ />
+
setTab( tabName ) }
+ selectOnMove={ false }
+ tabs={ [
+ {
+ name: 'list-view',
+ title: 'List View',
+ className: 'edit-post-sidebar__panel-tab',
+ },
+ {
+ name: 'outline',
+ title: 'Outline',
+ className: 'edit-post-sidebar__panel-tab',
+ },
+ ] }
>
- setIsListViewOpened( false ) }
- />
-
-
- {
- setTab( 'list-view' );
- } }
- className={ classnames(
- 'edit-post-sidebar__panel-tab',
- { 'is-active': tab === 'list-view' }
- ) }
- aria-current={ tab === 'list-view' }
- >
- { __( 'List View' ) }
-
-
-
- {
- setTab( 'outline' );
- } }
- className={ classnames(
- 'edit-post-sidebar__panel-tab',
- { 'is-active': tab === 'outline' }
- ) }
- aria-current={ tab === 'outline' }
- >
- { __( 'Outline' ) }
-
-
-
-
-
- { tab === 'list-view' && (
-
-
+ { ( currentTab ) => (
+
+ { renderTabContent( currentTab.name ) }
) }
- { tab === 'outline' &&
}
-
+
);
}
diff --git a/packages/edit-post/src/components/secondary-sidebar/style.scss b/packages/edit-post/src/components/secondary-sidebar/style.scss
index 63a3746e1b8443..d5ef3212bcfdd6 100644
--- a/packages/edit-post/src/components/secondary-sidebar/style.scss
+++ b/packages/edit-post/src/components/secondary-sidebar/style.scss
@@ -17,8 +17,29 @@
width: 350px;
}
- .edit-post-sidebar__panel-tabs {
- flex-direction: row-reverse;
+ .edit-post-editor__document-overview-panel__close-button {
+ position: absolute;
+ right: $grid-unit-10;
+ top: math.div($grid-unit-60 - $button-size, 2); // ( tab height - button size ) / 2
+ z-index: 1;
+ background: $white;
+ }
+
+ // The TabPanel style overrides in the following blocks should be removed when the new TabPanel is available.
+ .components-tab-panel__tabs {
+ border-bottom: $border-width solid $gray-300;
+ box-sizing: border-box;
+ display: flex;
+ width: 100%;
+ padding-right: $grid-unit-70;
+
+ .edit-post-sidebar__panel-tab {
+ width: 50%;
+ }
+ }
+
+ .components-tab-panel__tab-content {
+ height: calc(100% - #{$grid-unit-60});
}
}
@@ -37,34 +58,6 @@
}
}
-.edit-post-editor__document-overview-panel-header {
- border-bottom: $border-width solid $gray-300;
- display: flex;
- justify-content: space-between;
- height: $grid-unit-60;
- padding-left: $grid-unit-20;
- padding-right: $grid-unit-05;
- ul {
- width: calc(100% - #{ $grid-unit-50 });
- }
- li {
- width: 50%;
- button {
- width: 100%;
- text-align: initial;
- }
- }
- li:only-child {
- width: 100%;
- }
-
- &.components-panel__header.edit-post-sidebar__panel-tabs {
- .components-button.has-icon {
- display: flex;
- }
- }
-}
-
.edit-post-editor__list-view-panel-content,
.edit-post-editor__list-view-container > .document-outline,
.edit-post-editor__list-view-empty-headings {
@@ -118,5 +111,9 @@
.edit-post-editor__list-view-container {
display: flex;
flex-direction: column;
- height: calc(100% - #{$grid-unit-60});
+ height: 100%;
+}
+
+.edit-post-editor__document-overview-panel__tab-panel {
+ height: 100%;
}
diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js
index ac8902f6a5f7a1..638a869aa8350c 100644
--- a/packages/edit-post/src/components/visual-editor/index.js
+++ b/packages/edit-post/src/components/visual-editor/index.js
@@ -31,11 +31,9 @@ import {
__experimentaluseLayoutStyles as useLayoutStyles,
} from '@wordpress/block-editor';
import { useEffect, useRef, useMemo } from '@wordpress/element';
-import { Button, __unstableMotion as motion } from '@wordpress/components';
-import { useSelect, useDispatch } from '@wordpress/data';
+import { __unstableMotion as motion } from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
import { useMergeRefs } from '@wordpress/compose';
-import { arrowLeft } from '@wordpress/icons';
-import { __ } from '@wordpress/i18n';
import { parse } from '@wordpress/blocks';
import { store as coreStore } from '@wordpress/core-data';
@@ -175,8 +173,6 @@ export default function VisualEditor( { styles } ) {
_settings.__experimentalFeatures?.useRootPaddingAwareAlignments,
};
}, [] );
- const { clearSelectedBlock } = useDispatch( blockEditorStore );
- const { setIsEditingTemplate } = useDispatch( editPostStore );
const desktopCanvasStyles = {
height: '100%',
width: '100%',
@@ -349,18 +345,6 @@ export default function VisualEditor( { styles } ) {
} }
ref={ blockSelectionClearerRef }
>
- { isTemplateMode && (
-
{
- clearSelectedBlock();
- setIsEditingTemplate( false );
- } }
- >
- { __( 'Back' ) }
-
- ) }
- )
- }
+ notices={
}
content={
<>
diff --git a/packages/edit-site/src/components/editor/style.scss b/packages/edit-site/src/components/editor/style.scss
index b8795d9ba7cb31..17107820ffb83c 100644
--- a/packages/edit-site/src/components/editor/style.scss
+++ b/packages/edit-site/src/components/editor/style.scss
@@ -20,7 +20,7 @@
// Adjust the position of the notices
.edit-site .components-editor-notices__snackbar {
- position: fixed;
+ position: absolute;
right: 0;
bottom: 40px;
padding-left: 16px;
diff --git a/packages/edit-site/src/components/list/table.js b/packages/edit-site/src/components/list/table.js
index 59ff5a9187ab09..18198cb600ddae 100644
--- a/packages/edit-site/src/components/list/table.js
+++ b/packages/edit-site/src/components/list/table.js
@@ -13,8 +13,8 @@ import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
*/
+import TemplateActions from '../template-actions';
import Link from '../routes/link';
-import Actions from './actions';
import AddedBy from './added-by';
export default function Table( { templateType } ) {
@@ -126,7 +126,11 @@ export default function Table( { templateType } ) {
) : null }
-
+
) ) }
diff --git a/packages/edit-site/src/components/sidebar-navigation-data-list/data-list-item.js b/packages/edit-site/src/components/sidebar-navigation-data-list/data-list-item.js
deleted file mode 100644
index beeff0b6367b56..00000000000000
--- a/packages/edit-site/src/components/sidebar-navigation-data-list/data-list-item.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * WordPress dependencies
- */
-import {
- __experimentalHStack as HStack,
- __experimentalText as Text,
-} from '@wordpress/components';
-
-export default function DataListItem( { label, value } ) {
- return (
-
-
- { label }
-
-
- { value }
-
-
- );
-}
diff --git a/packages/edit-site/src/components/sidebar-navigation-data-list/index.js b/packages/edit-site/src/components/sidebar-navigation-data-list/index.js
deleted file mode 100644
index aeff422026591a..00000000000000
--- a/packages/edit-site/src/components/sidebar-navigation-data-list/index.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { __experimentalVStack as VStack } from '@wordpress/components';
-/**
- * Internal dependencies
- */
-import DataListItem from './data-list-item';
-
-export default function SidebarDetails( { details } ) {
- return (
-
- { details.map( ( detail ) => (
-
- ) ) }
-
- );
-}
diff --git a/packages/edit-site/src/components/sidebar-navigation-data-list/style.scss b/packages/edit-site/src/components/sidebar-navigation-data-list/style.scss
deleted file mode 100644
index 3a19bba8ed924d..00000000000000
--- a/packages/edit-site/src/components/sidebar-navigation-data-list/style.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-.edit-site-sidebar-navigation_data-list__item {
- .edit-site-sidebar-navigation_data-list__label {
- color: $gray-600;
- width: 100px;
- }
-
- .edit-site-sidebar-navigation_data-list__value.edit-site-sidebar-navigation_data-list__value {
- color: $gray-200;
- }
-}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js
index a7b8add9dd54fe..9fd87521806194 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js
@@ -7,6 +7,7 @@ import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { __experimentalNavigatorButton as NavigatorButton } from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
+import { BlockEditorProvider } from '@wordpress/block-editor';
/**
* Internal dependencies
@@ -19,6 +20,8 @@ import SidebarButton from '../sidebar-button';
import SidebarNavigationItem from '../sidebar-navigation-item';
import StyleBook from '../style-book';
+const noop = () => {};
+
export function SidebarNavigationItemGlobalStyles( props ) {
const { openGeneralSidebar } = useDispatch( editSiteStore );
const { setCanvasMode } = unlock( useDispatch( editSiteStore ) );
@@ -51,6 +54,33 @@ export function SidebarNavigationItemGlobalStyles( props ) {
);
}
+function SidebarNavigationScreenGlobalStylesContent() {
+ const { storedSettings } = useSelect( ( select ) => {
+ const { getSettings } = unlock( select( editSiteStore ) );
+
+ return {
+ storedSettings: getSettings( false ),
+ };
+ }, [] );
+
+ // Wrap in a BlockEditorProvider to ensure that the Iframe's dependencies are
+ // loaded. This is necessary because the Iframe component waits until
+ // the block editor store's `__internalIsInitialized` is true before
+ // rendering the iframe. Without this, the iframe previews will not render
+ // in mobile viewport sizes, where the editor canvas is hidden.
+ return (
+
+
+
+
+
+ );
+}
+
export default function SidebarNavigationScreenGlobalStyles() {
const { openGeneralSidebar } = useDispatch( editSiteStore );
const isMobileViewport = useViewportMatch( 'medium', '<' );
@@ -86,7 +116,7 @@ export default function SidebarNavigationScreenGlobalStyles() {
description={ __(
'Choose a different style combination for the theme styles.'
) }
- content={
}
+ content={
}
actions={
{ ! isMobileViewport && (
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js
index fa4baeea631cad..b4d06bfbc675ac 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js
@@ -6,25 +6,21 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { __, _x, sprintf } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import {
__experimentalUseNavigator as useNavigator,
__experimentalVStack as VStack,
ExternalLink,
__experimentalTruncate as Truncate,
+ __experimentalHStack as HStack,
+ __experimentalText as Text,
} from '@wordpress/components';
-import {
- store as coreStore,
- useEntityRecord,
- useEntityRecords,
-} from '@wordpress/core-data';
+import { store as coreStore, useEntityRecord } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
import { pencil } from '@wordpress/icons';
import { humanTimeDiff } from '@wordpress/date';
-import { count as wordCount } from '@wordpress/wordcount';
import { createInterpolateElement } from '@wordpress/element';
-import { privateApis as privateEditorApis } from '@wordpress/editor';
import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
import { escapeAttribute } from '@wordpress/escape-html';
@@ -36,148 +32,44 @@ import { unlock } from '../../private-apis';
import { store as editSiteStore } from '../../store';
import SidebarButton from '../sidebar-button';
import SidebarNavigationSubtitle from '../sidebar-navigation-subtitle';
-import SidebarDetails from '../sidebar-navigation-data-list';
-import DataListItem from '../sidebar-navigation-data-list/data-list-item';
-import StatusLabel from './status-label';
-
-// Taken from packages/editor/src/components/time-to-read/index.js.
-const AVERAGE_READING_RATE = 189;
-
-function getPageDetails( page ) {
- if ( ! page ) {
- return [];
- }
-
- const details = [
- {
- label: __( 'Status' ),
- value: (
-
- ),
- },
- {
- label: __( 'Slug' ),
- value:
{ page.slug } ,
- },
- ];
-
- if ( page?.templateTitle ) {
- details.push( {
- label: __( 'Template' ),
- value: page.templateTitle,
- } );
- }
-
- details.push( {
- label: __( 'Parent' ),
- value: page?.parentTitle,
- } );
-
- /*
- * translators: If your word count is based on single characters (e.g. East Asian characters),
- * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
- * Do not translate into your own language.
- */
- const wordCountType = _x( 'words', 'Word count type. Do not translate!' );
- const wordsCounted = page?.content?.rendered
- ? wordCount( page.content.rendered, wordCountType )
- : 0;
- const readingTime = Math.round( wordsCounted / AVERAGE_READING_RATE );
-
- if ( wordsCounted ) {
- details.push(
- {
- label: __( 'Words' ),
- value: wordsCounted.toLocaleString() || __( 'Unknown' ),
- },
- {
- label: __( 'Time to read' ),
- value:
- readingTime > 1
- ? sprintf(
- /* translators: %s: is the number of minutes. */
- __( '%s mins' ),
- readingTime.toLocaleString()
- )
- : __( '< 1 min' ),
- }
- );
- }
- return details;
-}
+import PageDetails from './page-details';
export default function SidebarNavigationScreenPage() {
const { setCanvasMode } = unlock( useDispatch( editSiteStore ) );
- const { getFeaturedMediaDetails } = unlock( privateEditorApis );
const {
params: { postId },
} = useNavigator();
const { record } = useEntityRecord( 'postType', 'page', postId );
- const {
- parentTitle,
- featuredMediaDetails: { mediaSourceUrl, altText },
- templateSlug,
- } = useSelect(
+ const { featuredMediaAltText, featuredMediaSourceUrl } = useSelect(
( select ) => {
- const { getEditedPostContext } = unlock( select( editSiteStore ) );
-
+ const { getEntityRecord } = select( coreStore );
// Featured image.
const attachedMedia = record?.featured_media
- ? select( coreStore ).getEntityRecord(
+ ? getEntityRecord(
'postType',
'attachment',
record?.featured_media
)
: null;
- // Parent page title.
- let _parentTitle = record?.parent
- ? select( coreStore ).getEntityRecord(
- 'postType',
- 'page',
- record.parent,
- { _fields: [ 'title' ] }
- )
- : null;
-
- if ( _parentTitle?.title ) {
- _parentTitle = decodeEntities(
- _parentTitle.title?.rendered || __( '(no title)' )
- );
- } else {
- _parentTitle = __( 'Top level' );
- }
-
return {
- parentTitle: _parentTitle,
- templateSlug: getEditedPostContext()?.templateSlug,
- featuredMediaDetails: {
- ...getFeaturedMediaDetails( attachedMedia, postId ),
- altText: escapeAttribute(
- attachedMedia?.alt_text ||
- attachedMedia?.description?.raw ||
- ''
- ),
- },
+ featuredMediaSourceUrl:
+ attachedMedia?.media_details.sizes?.medium?.source_url ||
+ attachedMedia?.source_url,
+ featuredMediaAltText: escapeAttribute(
+ attachedMedia?.alt_text ||
+ attachedMedia?.description?.raw ||
+ ''
+ ),
};
},
- [ record, postId ]
+ [ record ]
);
- // Match template slug to template title.
- const { records: templates, isResolving: areTemplatesLoading } =
- useEntityRecords( 'postType', 'wp_template', {
- per_page: -1,
- } );
- const templateTitle =
- ! areTemplatesLoading && templateSlug
- ? templates?.find( ( template ) => template?.slug === templateSlug )
- ?.title?.rendered || templateSlug
- : '';
+ const featureImageAltText = featuredMediaAltText
+ ? decodeEntities( featuredMediaAltText )
+ : decodeEntities( record?.title?.rendered || __( 'Featured image' ) );
return record ? (
- { !! mediaSourceUrl && (
+ { !! featuredMediaSourceUrl && (
) }
{ ! record?.featured_media && (
@@ -241,31 +126,35 @@ export default function SidebarNavigationScreenPage() {
) }
- Details
+ { __( 'Details' ) }
-
+
>
}
footer={
- %s' ),
- humanTimeDiff( record.modified )
- ),
- {
- time: ,
- }
- ) }
- />
+ !! record?.modified && (
+
+
+ { __( 'Last modified' ) }
+
+
+ { createInterpolateElement(
+ sprintf(
+ /* translators: %s: is the relative time when the post was last modified. */
+ __( '%s ' ),
+ humanTimeDiff( record.modified )
+ ),
+ {
+ time: ,
+ }
+ ) }
+
+
+ )
}
/>
) : null;
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
new file mode 100644
index 00000000000000..52491245770348
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js
@@ -0,0 +1,163 @@
+/**
+ * WordPress dependencies
+ */
+import { __, _x, sprintf } from '@wordpress/i18n';
+import {
+ __experimentalHStack as HStack,
+ __experimentalText as Text,
+ __experimentalVStack as VStack,
+ __experimentalTruncate as Truncate,
+} from '@wordpress/components';
+import { count as wordCount } from '@wordpress/wordcount';
+import { useSelect } from '@wordpress/data';
+import { decodeEntities } from '@wordpress/html-entities';
+import { store as coreStore, useEntityRecord } from '@wordpress/core-data';
+
+/**
+ * Internal dependencies
+ */
+import StatusLabel from './status-label';
+import { unlock } from '../../private-apis';
+import { store as editSiteStore } from '../../store';
+
+// Taken from packages/editor/src/components/time-to-read/index.js.
+const AVERAGE_READING_RATE = 189;
+
+function getPageDetails( page ) {
+ if ( ! page ) {
+ return [];
+ }
+
+ const details = [
+ {
+ label: __( 'Status' ),
+ value: (
+
+ ),
+ },
+ {
+ label: __( 'Slug' ),
+ value: { page.slug } ,
+ },
+ ];
+
+ if ( page?.templateTitle ) {
+ details.push( {
+ label: __( 'Template' ),
+ value: decodeEntities( page.templateTitle ),
+ } );
+ }
+
+ details.push( {
+ label: __( 'Parent' ),
+ // `null` indicates no parent.
+ value:
+ null === page?.parentTitle
+ ? __( 'Top level' )
+ : decodeEntities( page?.parentTitle || __( '(no title)' ) ),
+ } );
+
+ /*
+ * translators: If your word count is based on single characters (e.g. East Asian characters),
+ * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
+ * Do not translate into your own language.
+ */
+ const wordCountType = _x( 'words', 'Word count type. Do not translate!' );
+ const wordsCounted = page?.content?.rendered
+ ? wordCount( page.content.rendered, wordCountType )
+ : 0;
+ const readingTime = Math.round( wordsCounted / AVERAGE_READING_RATE );
+
+ if ( wordsCounted ) {
+ details.push(
+ {
+ label: __( 'Words' ),
+ value: wordsCounted.toLocaleString() || __( 'Unknown' ),
+ },
+ {
+ label: __( 'Time to read' ),
+ value:
+ readingTime > 1
+ ? sprintf(
+ /* translators: %s: is the number of minutes. */
+ __( '%s mins' ),
+ readingTime.toLocaleString()
+ )
+ : __( '< 1 min' ),
+ }
+ );
+ }
+ return details;
+}
+
+export default function PageDetails( { id } ) {
+ const { record } = useEntityRecord( 'postType', 'page', id );
+
+ const { parentTitle, templateTitle } = useSelect(
+ ( select ) => {
+ const { getEditedPostContext, getSettings } = unlock(
+ select( editSiteStore )
+ );
+ const defaultTemplateTypes = getSettings()?.defaultTemplateTypes;
+ const postContext = getEditedPostContext();
+
+ // Template title.
+ const templateSlug =
+ // Checks that the post type matches the current theme's post type, otherwise
+ // the templateSlug returns 'home'.
+ postContext?.postType === 'page'
+ ? postContext?.templateSlug
+ : null;
+ const _templateTitle =
+ defaultTemplateTypes && templateSlug
+ ? defaultTemplateTypes.find(
+ ( template ) => template.slug === templateSlug
+ )?.title
+ : null;
+
+ // Parent page title.
+ const _parentTitle = record?.parent
+ ? select( coreStore ).getEntityRecord(
+ 'postType',
+ 'page',
+ record.parent,
+ {
+ _fields: [ 'title' ],
+ }
+ )?.title?.rendered
+ : null;
+
+ return {
+ parentTitle: _parentTitle,
+ templateTitle: _templateTitle,
+ };
+ },
+ [ record ]
+ );
+ return (
+
+ { getPageDetails( {
+ parentTitle,
+ templateTitle,
+ ...record,
+ } ).map( ( { label, value } ) => (
+
+
+ { label }
+
+
+ { value }
+
+
+ ) ) }
+
+ );
+}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss
index 7f7e6d79b5029d..6ec9e565f9e8ff 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss
@@ -60,3 +60,15 @@
fill: $alert-green;
}
}
+
+.edit-site-sidebar-navigation-screen-page__details {
+ .edit-site-sidebar-navigation-screen-page__details-label {
+ color: $gray-600;
+ width: 100px;
+ }
+
+ .edit-site-sidebar-navigation-screen-page__details-value.edit-site-sidebar-navigation-screen-page__details-value {
+ color: $gray-200;
+ }
+}
+
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js
index 99139b55d87a5c..753957f2fe725a 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js
@@ -19,6 +19,7 @@ import { unlock } from '../../private-apis';
import { store as editSiteStore } from '../../store';
import SidebarButton from '../sidebar-button';
import { useAddedBy } from '../list/added-by';
+import TemplateActions from '../template-actions';
function useTemplateTitleAndDescription( postType, postId ) {
const { getDescription, getTitle, record } = useEditedEntityRecord(
@@ -87,8 +88,10 @@ function useTemplateTitleAndDescription( postType, postId ) {
}
export default function SidebarNavigationScreenTemplate() {
- const { params } = useNavigator();
- const { postType, postId } = params;
+ const navigator = useNavigator();
+ const {
+ params: { postType, postId },
+ } = navigator;
const { setCanvasMode } = unlock( useDispatch( editSiteStore ) );
const { title, description } = useTemplateTitleAndDescription(
postType,
@@ -99,11 +102,21 @@ export default function SidebarNavigationScreenTemplate() {
setCanvasMode( 'edit' ) }
- label={ __( 'Edit' ) }
- icon={ pencil }
- />
+
+ {
+ navigator.goTo( `/${ postType }/all` );
+ } }
+ />
+ setCanvasMode( 'edit' ) }
+ label={ __( 'Edit' ) }
+ icon={ pencil }
+ />
+
}
description={ description }
/>
diff --git a/packages/edit-site/src/components/list/actions/index.js b/packages/edit-site/src/components/template-actions/index.js
similarity index 79%
rename from packages/edit-site/src/components/list/actions/index.js
rename to packages/edit-site/src/components/template-actions/index.js
index 652cbe0f74e15a..0b54d6ef3ea716 100644
--- a/packages/edit-site/src/components/list/actions/index.js
+++ b/packages/edit-site/src/components/template-actions/index.js
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import { useDispatch } from '@wordpress/data';
+import { useDispatch, useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { __, sprintf } from '@wordpress/i18n';
import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
@@ -11,12 +11,23 @@ import { store as noticesStore } from '@wordpress/notices';
/**
* Internal dependencies
*/
-import { store as editSiteStore } from '../../../store';
-import isTemplateRemovable from '../../../utils/is-template-removable';
-import isTemplateRevertable from '../../../utils/is-template-revertable';
+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';
-export default function Actions( { template } ) {
+export default function TemplateActions( {
+ postType,
+ postId,
+ className,
+ toggleProps,
+ onRemove,
+} ) {
+ const template = useSelect(
+ ( select ) =>
+ select( coreStore ).getEntityRecord( 'postType', postType, postId ),
+ [ postType, postId ]
+ );
const { removeTemplate, revertTemplate } = useDispatch( editSiteStore );
const { saveEditedEntityRecord } = useDispatch( coreStore );
const { createSuccessNotice, createErrorNotice } =
@@ -62,7 +73,8 @@ export default function Actions( { template } ) {
{ ( { onClose } ) => (
@@ -77,6 +89,7 @@ export default function Actions( { template } ) {
isTertiary
onClick={ () => {
removeTemplate( template );
+ onRemove?.();
onClose();
} }
>
diff --git a/packages/edit-site/src/components/list/actions/rename-menu-item.js b/packages/edit-site/src/components/template-actions/rename-menu-item.js
similarity index 100%
rename from packages/edit-site/src/components/list/actions/rename-menu-item.js
rename to packages/edit-site/src/components/template-actions/rename-menu-item.js
diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js
index d1f41d5a62aca5..a67406349e1644 100644
--- a/packages/edit-site/src/store/actions.js
+++ b/packages/edit-site/src/store/actions.js
@@ -12,6 +12,7 @@ import { store as interfaceStore } from '@wordpress/interface';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { speak } from '@wordpress/a11y';
import { store as preferencesStore } from '@wordpress/preferences';
+import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
@@ -144,7 +145,7 @@ export const removeTemplate =
sprintf(
/* translators: The template/part's name. */
__( '"%s" deleted.' ),
- template.title.rendered
+ decodeEntities( template.title.rendered )
),
{ type: 'snackbar', id: 'site-editor-template-deleted-success' }
);
diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss
index 3aebc258cd5107..3e2b34ea65f5fd 100644
--- a/packages/edit-site/src/style.scss
+++ b/packages/edit-site/src/style.scss
@@ -29,7 +29,6 @@
@import "./components/sidebar-navigation-screen-template/style.scss";
@import "./components/sidebar-navigation-screen-templates/style.scss";
@import "./components/sidebar-navigation-subtitle/style.scss";
-@import "./components/sidebar-navigation-data-list/style.scss";
@import "./components/site-hub/style.scss";
@import "./components/sidebar-navigation-screen-navigation-menus/style.scss";
@import "./components/site-icon/style.scss";
diff --git a/packages/editor/src/components/post-featured-image/get-featured-media-details.js b/packages/editor/src/components/post-featured-image/get-featured-media-details.js
deleted file mode 100644
index a2a9ab71449c52..00000000000000
--- a/packages/editor/src/components/post-featured-image/get-featured-media-details.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { applyFilters } from '@wordpress/hooks';
-
-/**
- * The media details object.
- *
- * @typedef {Object} MediaDetails
- *
- * @property {string} mediaWidth Width value of the media.
- * @property {string} mediaHeight Height value of the media.
- * @property {string} mediaSourceUrl URL of the media.
- */
-
-/**
- * Returns the featured media dimensions and source URL.
- *
- * @param {Object} media The media object.
- * @param {number} postId The post ID of the media
- * @return {MediaDetails} The featured media details.
- */
-export default function getFeaturedMediaDetails( media, postId ) {
- if ( ! media ) {
- return {};
- }
-
- const defaultSize = applyFilters(
- 'editor.PostFeaturedImage.imageSize',
- 'large',
- media.id,
- postId
- );
- if ( defaultSize in ( media?.media_details?.sizes ?? {} ) ) {
- return {
- mediaWidth: media.media_details.sizes[ defaultSize ].width,
- mediaHeight: media.media_details.sizes[ defaultSize ].height,
- mediaSourceUrl: media.media_details.sizes[ defaultSize ].source_url,
- };
- }
-
- // Use fallbackSize when defaultSize is not available.
- const fallbackSize = applyFilters(
- 'editor.PostFeaturedImage.imageSize',
- 'thumbnail',
- media.id,
- postId
- );
- if ( fallbackSize in ( media?.media_details?.sizes ?? {} ) ) {
- return {
- mediaWidth: media.media_details.sizes[ fallbackSize ].width,
- mediaHeight: media.media_details.sizes[ fallbackSize ].height,
- mediaSourceUrl:
- media.media_details.sizes[ fallbackSize ].source_url,
- };
- }
-
- // Use full image size when fallbackSize and defaultSize are not available.
- return {
- mediaWidth: media.media_details.width,
- mediaHeight: media.media_details.height,
- mediaSourceUrl: media.source_url,
- };
-}
diff --git a/packages/editor/src/components/post-featured-image/index.js b/packages/editor/src/components/post-featured-image/index.js
index 1595e2518f25a7..9304115b0b7f11 100644
--- a/packages/editor/src/components/post-featured-image/index.js
+++ b/packages/editor/src/components/post-featured-image/index.js
@@ -2,6 +2,7 @@
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
+import { applyFilters } from '@wordpress/hooks';
import {
DropZone,
Button,
@@ -27,7 +28,6 @@ import { store as coreStore } from '@wordpress/core-data';
*/
import PostFeaturedImageCheck from './check';
import { store as editorStore } from '../../store';
-import getFeaturedMediaDetails from './get-featured-media-details';
const ALLOWED_MEDIA_TYPES = [ 'image' ];
@@ -43,6 +43,49 @@ const instructions = (
);
+function getMediaDetails( media, postId ) {
+ if ( ! media ) {
+ return {};
+ }
+
+ const defaultSize = applyFilters(
+ 'editor.PostFeaturedImage.imageSize',
+ 'large',
+ media.id,
+ postId
+ );
+ if ( defaultSize in ( media?.media_details?.sizes ?? {} ) ) {
+ return {
+ mediaWidth: media.media_details.sizes[ defaultSize ].width,
+ mediaHeight: media.media_details.sizes[ defaultSize ].height,
+ mediaSourceUrl: media.media_details.sizes[ defaultSize ].source_url,
+ };
+ }
+
+ // Use fallbackSize when defaultSize is not available.
+ const fallbackSize = applyFilters(
+ 'editor.PostFeaturedImage.imageSize',
+ 'thumbnail',
+ media.id,
+ postId
+ );
+ if ( fallbackSize in ( media?.media_details?.sizes ?? {} ) ) {
+ return {
+ mediaWidth: media.media_details.sizes[ fallbackSize ].width,
+ mediaHeight: media.media_details.sizes[ fallbackSize ].height,
+ mediaSourceUrl:
+ media.media_details.sizes[ fallbackSize ].source_url,
+ };
+ }
+
+ // Use full image size when fallbackSize and defaultSize are not available.
+ return {
+ mediaWidth: media.media_details.width,
+ mediaHeight: media.media_details.height,
+ mediaSourceUrl: media.source_url,
+ };
+}
+
function PostFeaturedImage( {
currentPostId,
featuredImageId,
@@ -58,7 +101,7 @@ function PostFeaturedImage( {
const mediaUpload = useSelect( ( select ) => {
return select( blockEditorStore ).getSettings().mediaUpload;
}, [] );
- const { mediaWidth, mediaHeight, mediaSourceUrl } = getFeaturedMediaDetails(
+ const { mediaWidth, mediaHeight, mediaSourceUrl } = getMediaDetails(
media,
currentPostId
);
diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js
index 1de3d1d70f140e..cdec27b0ab9021 100644
--- a/packages/editor/src/private-apis.js
+++ b/packages/editor/src/private-apis.js
@@ -4,11 +4,9 @@
import { ExperimentalEditorProvider } from './components/provider';
import { lock } from './lockUnlock';
import { EntitiesSavedStatesExtensible } from './components/entities-saved-states';
-import getFeaturedMediaDetails from './components/post-featured-image/get-featured-media-details';
export const privateApis = {};
lock( privateApis, {
ExperimentalEditorProvider,
EntitiesSavedStatesExtensible,
- getFeaturedMediaDetails,
} );
diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md
index 1c2b35b28da947..04a2030c1ab8a3 100644
--- a/packages/env/CHANGELOG.md
+++ b/packages/env/CHANGELOG.md
@@ -6,6 +6,10 @@
- Execute the local package's `wp-env` instead of the globally installed version if one is available.
+### Bug fix
+
+- Run `useradd` with `-l` option to prevent excessive Docker image sizes.
+
## 8.0.0 (2023-05-24)
### Breaking Change
diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js
index ee26ade75c665e..fad8d5b36f1834 100644
--- a/packages/env/lib/init-config.js
+++ b/packages/env/lib/init-config.js
@@ -133,8 +133,8 @@ ARG HOST_USERNAME
ARG HOST_UID
ARG HOST_GID
# When the IDs are already in use we can still safely move on.
-RUN groupadd -g $HOST_GID $HOST_USERNAME || true
-RUN useradd -m -u $HOST_UID -g $HOST_GID $HOST_USERNAME || true
+RUN groupadd -o -g $HOST_GID $HOST_USERNAME || true
+RUN useradd -mlo -u $HOST_UID -g $HOST_GID $HOST_USERNAME || true
# Install any dependencies we need in the container.
${ installDependencies( 'wordpress', env, config ) }`;
diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md
index 15e471a65d36a5..08632b57fd7244 100644
--- a/packages/notices/CHANGELOG.md
+++ b/packages/notices/CHANGELOG.md
@@ -2,6 +2,11 @@
## Unreleased
+### New Feature
+
+- Add a new action `removeNotices` which allows bulk removal of notices by their IDs. ([#39940](https://github.com/WordPress/gutenberg/pull/39940))
+- Add a new action `removeAllNotices` which removes all notices from a given context. ([#44059](https://github.com/WordPress/gutenberg/pull/44059))
+
## 4.2.0 (2023-05-24)
## 4.1.0 (2023-05-10)
diff --git a/packages/notices/src/store/actions.js b/packages/notices/src/store/actions.js
index a8fdb13f9352fc..6ffa7aede2a884 100644
--- a/packages/notices/src/store/actions.js
+++ b/packages/notices/src/store/actions.js
@@ -313,3 +313,106 @@ export function removeNotice( id, context = DEFAULT_CONTEXT ) {
context,
};
}
+
+/**
+ * Removes all notices from a given context. Defaults to the default context.
+ *
+ * @param {string} noticeType The context to remove all notices from.
+ * @param {string} context The context to remove all notices from.
+ *
+ * @example
+ * ```js
+ * import { __ } from '@wordpress/i18n';
+ * import { useDispatch, useSelect } from '@wordpress/data';
+ * import { store as noticesStore } from '@wordpress/notices';
+ * import { Button } from '@wordpress/components';
+ *
+ * export const ExampleComponent = () => {
+ * const notices = useSelect( ( select ) =>
+ * select( noticesStore ).getNotices()
+ * );
+ * const { removeNotices } = useDispatch( noticesStore );
+ * return (
+ * <>
+ *
+ * { notices.map( ( notice ) => (
+ * { notice.content }
+ * ) ) }
+ *
+ *
+ * removeAllNotices()
+ * }
+ * >
+ * { __( 'Clear all notices', 'woo-gutenberg-products-block' ) }
+ *
+ *
+ * removeAllNotices( 'snackbar' )
+ * }
+ * >
+ * { __( 'Clear all snackbar notices', 'woo-gutenberg-products-block' ) }
+ *
+ * >
+ * );
+ * };
+ * ```
+ *
+ * @return {Object} Action object.
+ */
+export function removeAllNotices(
+ noticeType = 'default',
+ context = DEFAULT_CONTEXT
+) {
+ return {
+ type: 'REMOVE_ALL_NOTICES',
+ noticeType,
+ context,
+ };
+}
+
+/**
+ * Returns an action object used in signalling that several notices are to be removed.
+ *
+ * @param {string[]} ids List of unique notice identifiers.
+ * @param {string} [context='global'] Optional context (grouping) in which the notices are
+ * intended to appear. Defaults to default context.
+ * @example
+ * ```js
+ * import { __ } from '@wordpress/i18n';
+ * import { useDispatch, useSelect } from '@wordpress/data';
+ * import { store as noticesStore } from '@wordpress/notices';
+ * import { Button } from '@wordpress/components';
+ *
+ * const ExampleComponent = () => {
+ * const notices = useSelect( ( select ) =>
+ * select( noticesStore ).getNotices()
+ * );
+ * const { removeNotices } = useDispatch( noticesStore );
+ * return (
+ * <>
+ *
+ * { notices.map( ( notice ) => (
+ * { notice.content }
+ * ) ) }
+ *
+ *
+ * removeNotices( notices.map( ( { id } ) => id ) )
+ * }
+ * >
+ * { __( 'Clear all notices' ) }
+ *
+ * >
+ * );
+ * };
+ * ```
+ * @return {Object} Action object.
+ */
+export function removeNotices( ids, context = DEFAULT_CONTEXT ) {
+ return {
+ type: 'REMOVE_NOTICES',
+ ids,
+ context,
+ };
+}
diff --git a/packages/notices/src/store/reducer.js b/packages/notices/src/store/reducer.js
index ff2359b61cc013..5f4d88c04af299 100644
--- a/packages/notices/src/store/reducer.js
+++ b/packages/notices/src/store/reducer.js
@@ -23,6 +23,12 @@ const notices = onSubKey( 'context' )( ( state = [], action ) => {
case 'REMOVE_NOTICE':
return state.filter( ( { id } ) => id !== action.id );
+
+ case 'REMOVE_NOTICES':
+ return state.filter( ( { id } ) => ! action.ids.includes( id ) );
+
+ case 'REMOVE_ALL_NOTICES':
+ return state.filter( ( { type } ) => type !== action.noticeType );
}
return state;
diff --git a/packages/notices/src/store/test/actions.js b/packages/notices/src/store/test/actions.js
index db6aa72b468dec..37fefc5c3558f4 100644
--- a/packages/notices/src/store/test/actions.js
+++ b/packages/notices/src/store/test/actions.js
@@ -8,6 +8,8 @@ import {
createErrorNotice,
createWarningNotice,
removeNotice,
+ removeAllNotices,
+ removeNotices,
} from '../actions';
import { DEFAULT_CONTEXT, DEFAULT_STATUS } from '../constants';
@@ -215,4 +217,55 @@ describe( 'actions', () => {
} );
} );
} );
+
+ describe( 'removeNotices', () => {
+ it( 'should return action', () => {
+ const ids = [ 'id', 'id2' ];
+
+ expect( removeNotices( ids ) ).toEqual( {
+ type: 'REMOVE_NOTICES',
+ ids,
+ context: DEFAULT_CONTEXT,
+ } );
+ } );
+
+ it( 'should return action with custom context', () => {
+ const ids = [ 'id', 'id2' ];
+ const context = 'foo';
+
+ expect( removeNotices( ids, context ) ).toEqual( {
+ type: 'REMOVE_NOTICES',
+ ids,
+ context,
+ } );
+ } );
+ } );
+
+ describe( 'removeAllNotices', () => {
+ it( 'should return action', () => {
+ expect( removeAllNotices() ).toEqual( {
+ type: 'REMOVE_ALL_NOTICES',
+ noticeType: 'default',
+ context: DEFAULT_CONTEXT,
+ } );
+ } );
+
+ it( 'should return action with custom context', () => {
+ const context = 'foo';
+
+ expect( removeAllNotices( 'default', context ) ).toEqual( {
+ type: 'REMOVE_ALL_NOTICES',
+ noticeType: 'default',
+ context,
+ } );
+ } );
+
+ it( 'should return action with type', () => {
+ expect( removeAllNotices( 'snackbar' ) ).toEqual( {
+ type: 'REMOVE_ALL_NOTICES',
+ noticeType: 'snackbar',
+ context: DEFAULT_CONTEXT,
+ } );
+ } );
+ } );
} );
diff --git a/packages/notices/src/store/test/reducer.js b/packages/notices/src/store/test/reducer.js
index 52ba278c79d854..d807b814a51f47 100644
--- a/packages/notices/src/store/test/reducer.js
+++ b/packages/notices/src/store/test/reducer.js
@@ -7,7 +7,12 @@ import deepFreeze from 'deep-freeze';
* Internal dependencies
*/
import reducer from '../reducer';
-import { createNotice, removeNotice } from '../actions';
+import {
+ createNotice,
+ removeNotice,
+ removeNotices,
+ removeAllNotices,
+} from '../actions';
import { getNotices } from '../selectors';
import { DEFAULT_CONTEXT } from '../constants';
@@ -141,6 +146,44 @@ describe( 'reducer', () => {
expect( state[ DEFAULT_CONTEXT ] ).toHaveLength( 1 );
} );
+ it( 'should omit several removed notices', () => {
+ const action = createNotice( 'error', 'save error' );
+ const action2 = createNotice( 'error', 'second error' );
+ const stateWithOneNotice = reducer( undefined, action );
+ const original = deepFreeze( reducer( stateWithOneNotice, action2 ) );
+ const ids = [
+ getNotices( original )[ 0 ].id,
+ getNotices( original )[ 1 ].id,
+ ];
+
+ const state = reducer( original, removeNotices( ids ) );
+
+ expect( state ).toEqual( {
+ [ DEFAULT_CONTEXT ]: [],
+ } );
+ } );
+
+ it( 'should omit several removed notices across contexts', () => {
+ const action = createNotice( 'error', 'save error' );
+ const action2 = createNotice( 'error', 'second error', {
+ context: 'foo',
+ } );
+ const action3 = createNotice( 'error', 'third error', {
+ context: 'foo',
+ } );
+ const stateWithOneNotice = reducer( undefined, action );
+ const stateWithTwoNotices = reducer( stateWithOneNotice, action2 );
+ const original = deepFreeze( reducer( stateWithTwoNotices, action3 ) );
+ const ids = [
+ getNotices( original, 'foo' )[ 0 ].id,
+ getNotices( original, 'foo' )[ 1 ].id,
+ ];
+
+ const state = reducer( original, removeNotices( ids, 'foo' ) );
+
+ expect( state[ DEFAULT_CONTEXT ] ).toHaveLength( 1 );
+ } );
+
it( 'should dedupe distinct ids, preferring new', () => {
let action = createNotice( 'error', 'save error (1)', {
id: 'error-message',
@@ -170,4 +213,84 @@ describe( 'reducer', () => {
],
} );
} );
+
+ it( 'should remove all notices', () => {
+ let action = createNotice( 'error', 'save error' );
+ const original = deepFreeze( reducer( undefined, action ) );
+
+ action = createNotice( 'success', 'successfully saved' );
+ let state = reducer( original, action );
+ state = reducer( state, removeAllNotices() );
+
+ expect( state ).toEqual( {
+ [ DEFAULT_CONTEXT ]: [],
+ } );
+ } );
+
+ it( 'should remove all notices in a given context but leave other contexts intact', () => {
+ let action = createNotice( 'error', 'save error', {
+ context: 'foo',
+ id: 'foo-error',
+ } );
+ const original = deepFreeze( reducer( undefined, action ) );
+
+ action = createNotice( 'success', 'successfully saved', {
+ context: 'bar',
+ } );
+
+ let state = reducer( original, action );
+ state = reducer( state, removeAllNotices( 'default', 'bar' ) );
+
+ expect( state ).toEqual( {
+ bar: [],
+ foo: [
+ {
+ id: 'foo-error',
+ content: 'save error',
+ spokenMessage: 'save error',
+ __unstableHTML: undefined,
+ status: 'error',
+ isDismissible: true,
+ actions: [],
+ type: 'default',
+ icon: null,
+ explicitDismiss: false,
+ onDismiss: undefined,
+ },
+ ],
+ } );
+ } );
+
+ it( 'should remove all notices of a given type', () => {
+ let action = createNotice( 'error', 'save error', {
+ id: 'global-error',
+ } );
+ const original = deepFreeze( reducer( undefined, action ) );
+
+ action = createNotice( 'success', 'successfully saved', {
+ type: 'snackbar',
+ id: 'snackbar-success',
+ } );
+
+ let state = reducer( original, action );
+ state = reducer( state, removeAllNotices( 'default' ) );
+
+ expect( state ).toEqual( {
+ [ DEFAULT_CONTEXT ]: [
+ {
+ id: 'snackbar-success',
+ content: 'successfully saved',
+ spokenMessage: 'successfully saved',
+ __unstableHTML: undefined,
+ status: 'success',
+ isDismissible: true,
+ actions: [],
+ type: 'snackbar',
+ icon: null,
+ explicitDismiss: false,
+ onDismiss: undefined,
+ },
+ ],
+ } );
+ } );
} );
diff --git a/phpunit/blocks/render-comment-template-test.php b/phpunit/blocks/render-comment-template-test.php
new file mode 100644
index 00000000000000..c297d1729d0dc5
--- /dev/null
+++ b/phpunit/blocks/render-comment-template-test.php
@@ -0,0 +1,175 @@
+ $original_value ) {
+ update_option( $option, $original_value );
+ }
+
+ parent::tear_down();
+ }
+
+ public function set_up() {
+ parent::set_up();
+
+ update_option( 'page_comments', true );
+ update_option( 'comments_per_page', self::$per_page );
+
+ self::$custom_post = self::factory()->post->create_and_get(
+ array(
+ 'post_type' => 'dogs',
+ 'post_status' => 'publish',
+ 'post_name' => 'metaldog',
+ 'post_title' => 'Metal Dog',
+ 'post_content' => 'Metal Dog content',
+ 'post_excerpt' => 'Metal Dog',
+ )
+ );
+
+ self::$comment_ids = self::factory()->comment->create_post_comments(
+ self::$custom_post->ID,
+ 1,
+ array(
+ 'comment_author' => 'Test',
+ 'comment_author_email' => 'test@example.org',
+ 'comment_author_url' => 'http://example.com/author-url/',
+ 'comment_content' => 'Hello world',
+ )
+ );
+ }
+
+ public function test_rendering_comment_template_sets_comment_id_context() {
+ $parsed_comment_author_name_block = parse_blocks( '' )[0];
+ $comment_author_name_block = new WP_Block(
+ $parsed_comment_author_name_block,
+ array(
+ 'commentId' => self::$comment_ids[0],
+ )
+ );
+ $comment_author_name_block_markup = $comment_author_name_block->render();
+ $this->assertNotEmpty(
+ $comment_author_name_block_markup,
+ 'Comment Author Name block rendered markup is empty.'
+ );
+
+ $render_block_callback = static function( $block_content, $block ) use ( $parsed_comment_author_name_block ) {
+ // Insert a Comment Author Name block (which requires `commentId`
+ // block context to work) after the Comment Content block.
+ if ( 'core/comment-content' !== $block['blockName'] ) {
+ return $block_content;
+ }
+
+ $inserted_content = render_block( $parsed_comment_author_name_block );
+ return $inserted_content . $block_content;
+ };
+
+ add_filter( 'render_block', $render_block_callback, 10, 3 );
+ $parsed_blocks = parse_blocks(
+ ''
+ );
+ $block = new WP_Block(
+ $parsed_blocks[0],
+ array(
+ 'postId' => self::$custom_post->ID,
+ )
+ );
+ $markup = $block->render();
+ remove_filter( 'render_block', $render_block_callback );
+
+ $this->assertStringContainsString(
+ $comment_author_name_block_markup,
+ $markup,
+ "Rendered markup doesn't contain Comment Author Name block."
+ );
+ }
+
+ public function test_inner_block_inserted_by_render_block_data_is_retained() {
+ $render_block_callback = new MockAction();
+ add_filter( 'render_block', array( $render_block_callback, 'filter' ), 10, 3 );
+
+ $render_block_data_callback = static function( $parsed_block ) {
+ // Add a Social Links block to a Comment Template block's inner blocks.
+ if ( 'core/comment-template' === $parsed_block['blockName'] ) {
+ $inserted_block_markup = <<
+
+'
+END;
+
+ $inserted_blocks = parse_blocks( $inserted_block_markup );
+
+ $parsed_block['innerBlocks'][] = $inserted_blocks[0];
+ }
+ return $parsed_block;
+ };
+
+ add_filter( 'render_block_data', $render_block_data_callback, 10, 1 );
+ $parsed_blocks = parse_blocks(
+ ''
+ );
+ $block = new WP_Block(
+ $parsed_blocks[0],
+ array(
+ 'postId' => self::$custom_post->ID,
+ )
+ );
+ $block->render();
+ remove_filter( 'render_block_data', $render_block_data_callback );
+
+ $this->assertSame( 5, $render_block_callback->get_call_count() );
+
+ $args = $render_block_callback->get_args();
+ $this->assertSame( 'core/comment-content', $args[0][2]->name );
+ $this->assertSame( 'core/comment-template', $args[1][2]->name );
+ $this->assertCount( 2, $args[1][2]->inner_blocks, "Inner block inserted by render_block_data filter wasn't retained." );
+ $this->assertInstanceOf(
+ 'WP_Block',
+ $args[1][2]->inner_blocks[1],
+ "Inner block inserted by render_block_data isn't a WP_Block class instance."
+ );
+ $this->assertSame(
+ 'core/social-links',
+ $args[1][2]->inner_blocks[1]->name,
+ "Inner block inserted by render_block_data isn't named as expected."
+ );
+ }
+}
diff --git a/phpunit/blocks/render-post-template-test.php b/phpunit/blocks/render-post-template-test.php
new file mode 100644
index 00000000000000..13b5623cdd5dda
--- /dev/null
+++ b/phpunit/blocks/render-post-template-test.php
@@ -0,0 +1,73 @@
+post->create_and_get(
+ array(
+ 'post_type' => 'post',
+ 'post_status' => 'publish',
+ 'post_name' => 'metaldog',
+ 'post_title' => 'Metal Dog',
+ 'post_content' => 'Metal Dog content',
+ 'post_excerpt' => 'Metal Dog',
+ )
+ );
+
+ self::$other_post = self::factory()->post->create_and_get(
+ array(
+ 'post_type' => 'post',
+ 'post_status' => 'publish',
+ 'post_name' => 'ceilingcat',
+ 'post_title' => 'Ceiling Cat',
+ 'post_content' => 'Ceiling Cat content',
+ 'post_excerpt' => 'Ceiling Cat',
+ )
+ );
+ }
+
+ public function test_rendering_post_template() {
+ $parsed_blocks = parse_blocks(
+ ''
+ );
+ $block = new WP_Block( $parsed_blocks[0] );
+ $markup = $block->render();
+
+ $post_id = self::$post->ID;
+ $other_post_id = self::$other_post->ID;
+
+ $expected = <<
+
+ Ceiling Cat
+
+
+
+ Metal Dog
+
+
+
+END;
+ $this->assertSame(
+ str_replace( array( "\n", "\t" ), '', $expected ),
+ str_replace( array( "\n", "\t" ), '', $markup )
+ );
+ }
+}
diff --git a/test/e2e/specs/editor/blocks/links.spec.js b/test/e2e/specs/editor/blocks/links.spec.js
index 208aca3b6e5b37..6493b9effe8aab 100644
--- a/test/e2e/specs/editor/blocks/links.spec.js
+++ b/test/e2e/specs/editor/blocks/links.spec.js
@@ -103,7 +103,7 @@ test.describe( 'Links', () => {
// Edit link.
await pageUtils.pressKeys( 'primary+k' );
- await pageUtils.pressKeys( 'primary+a' );
+ await page.getByPlaceholder( 'Search or type url' ).fill( '' );
await page.keyboard.type( 'wordpress.org' );
// Update the link.
diff --git a/test/e2e/specs/editor/various/block-locking.spec.js b/test/e2e/specs/editor/various/block-locking.spec.js
index 2e977690a1708d..2c0a9d3f0bf565 100644
--- a/test/e2e/specs/editor/various/block-locking.spec.js
+++ b/test/e2e/specs/editor/various/block-locking.spec.js
@@ -73,7 +73,7 @@ test.describe( 'Block Locking', () => {
await page.click( 'role=checkbox[name="Lock all"]' );
await page.click( 'role=button[name="Apply"]' );
- await editor.clickBlockToolbarButton( 'Unlock Paragraph' );
+ await editor.clickBlockToolbarButton( 'Unlock' );
await page.click( 'role=checkbox[name="Lock all"]' );
await page.click( 'role=button[name="Apply"]' );
diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js
index 971d571128bce7..2dc138e7819183 100644
--- a/test/e2e/specs/editor/various/list-view.spec.js
+++ b/test/e2e/specs/editor/various/list-view.spec.js
@@ -331,9 +331,8 @@ test.describe( 'List View', () => {
// Focus the list view close button and make sure the shortcut will
// close the list view. This is to catch a bug where elements could be
- // out of range of the sidebar region. Must shift+tab 3 times to reach
- // close button before tabs.
- await pageUtils.pressKeys( 'shift+Tab' );
+ // out of range of the sidebar region. Must shift+tab 2 times to reach
+ // close button before tab panel.
await pageUtils.pressKeys( 'shift+Tab' );
await pageUtils.pressKeys( 'shift+Tab' );
await expect(
@@ -354,7 +353,8 @@ test.describe( 'List View', () => {
// Focus the outline tab and select it. This test ensures the outline
// tab receives similar focus events based on the shortcut.
await pageUtils.pressKeys( 'shift+Tab' );
- const outlineButton = editor.canvas.getByRole( 'button', {
+ await page.keyboard.press( 'ArrowRight' );
+ const outlineButton = editor.canvas.getByRole( 'tab', {
name: 'Outline',
} );
await expect( outlineButton ).toBeFocused();
diff --git a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js
index c7aed497dc25cc..9bbecb51223fc0 100644
--- a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js
+++ b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js
@@ -101,188 +101,6 @@ test.describe( 'Post Editor Template mode', () => {
)
).toBeVisible();
} );
-
- test( 'Allow editing the title of a new custom template', async ( {
- page,
- postEditorTemplateMode,
- } ) => {
- async function editTemplateTitle( newTitle ) {
- await page
- .getByRole( 'button', { name: 'Template Options' } )
- .click();
-
- await page
- .getByRole( 'textbox', { name: 'Title' } )
- .fill( newTitle );
-
- const editorContent = page.getByLabel( 'Editor Content' );
- await editorContent.click();
- }
-
- await postEditorTemplateMode.createPostAndSaveDraft();
- await postEditorTemplateMode.createNewTemplate( 'Foobar' );
- await editTemplateTitle( 'Barfoo' );
-
- await expect(
- page.getByRole( 'button', { name: 'Template Options' } )
- ).toHaveText( 'Barfoo' );
- } );
-
- test.describe( 'Delete Post Template Confirmation Dialog', () => {
- test.beforeAll( async ( { requestUtils } ) => {
- await requestUtils.activateTheme( 'twentytwentyone' );
- } );
-
- test.beforeEach( async ( { postEditorTemplateMode } ) => {
- await postEditorTemplateMode.createPostAndSaveDraft();
- } );
-
- [ 'large', 'small' ].forEach( ( viewport ) => {
- test( `should retain template if deletion is canceled when the viewport is ${ viewport }`, async ( {
- editor,
- page,
- pageUtils,
- postEditorTemplateMode,
- } ) => {
- await pageUtils.setBrowserViewport( viewport );
-
- await postEditorTemplateMode.disableTemplateWelcomeGuide();
-
- const templateTitle = `${ viewport } Viewport Deletion Test`;
-
- await postEditorTemplateMode.createNewTemplate( templateTitle );
-
- // Close the settings in small viewport.
- if ( viewport === 'small' ) {
- await page.click( 'role=button[name="Close settings"i]' );
- }
-
- // Edit the template.
- await editor.insertBlock( { name: 'core/paragraph' } );
- await page.keyboard.type(
- 'Just a random paragraph added to the template'
- );
-
- await postEditorTemplateMode.saveTemplateWithoutPublishing();
-
- // Test deletion dialog.
- {
- const templateDropdown =
- postEditorTemplateMode.editorTopBar.locator(
- 'role=button[name="Template Options"i]'
- );
- await templateDropdown.click();
- await page.click(
- 'role=menuitem[name="Delete template"i]'
- );
-
- const confirmDeletionDialog = page.locator( 'role=dialog' );
- await expect( confirmDeletionDialog ).toBeFocused();
- await expect(
- confirmDeletionDialog.locator(
- `text=Are you sure you want to delete the ${ templateTitle } template? It may be used by other pages or posts.`
- )
- ).toBeVisible();
-
- await confirmDeletionDialog
- .locator( 'role=button[name="Cancel"i]' )
- .click();
- }
-
- // Exit template mode.
- await page.click( 'role=button[name="Back"i]' );
-
- await editor.openDocumentSettingsSidebar();
-
- // Move focus to the "Post" panel in the editor sidebar.
- const postPanel =
- postEditorTemplateMode.editorSettingsSidebar.locator(
- 'role=button[name="Post"i]'
- );
- await postPanel.click();
-
- await postEditorTemplateMode.openTemplatePopover();
-
- const templateSelect = page.locator(
- 'role=combobox[name="Template"i]'
- );
- await expect( templateSelect ).toHaveValue(
- `${ viewport }-viewport-deletion-test`
- );
- } );
-
- test( `should delete template if deletion is confirmed when the viewport is ${ viewport }`, async ( {
- editor,
- page,
- pageUtils,
- postEditorTemplateMode,
- } ) => {
- const templateTitle = `${ viewport } Viewport Deletion Test`;
-
- await pageUtils.setBrowserViewport( viewport );
-
- await postEditorTemplateMode.createNewTemplate( templateTitle );
-
- // Close the settings in small viewport.
- if ( viewport === 'small' ) {
- await page.click( 'role=button[name="Close settings"i]' );
- }
-
- // Edit the template.
- await editor.insertBlock( { name: 'core/paragraph' } );
- await page.keyboard.type(
- 'Just a random paragraph added to the template'
- );
-
- await postEditorTemplateMode.saveTemplateWithoutPublishing();
-
- {
- const templateDropdown =
- postEditorTemplateMode.editorTopBar.locator(
- 'role=button[name="Template Options"i]'
- );
- await templateDropdown.click();
- await page.click(
- 'role=menuitem[name="Delete template"i]'
- );
-
- const confirmDeletionDialog = page.locator( 'role=dialog' );
- await expect( confirmDeletionDialog ).toBeFocused();
- await expect(
- confirmDeletionDialog.locator(
- `text=Are you sure you want to delete the ${ templateTitle } template? It may be used by other pages or posts.`
- )
- ).toBeVisible();
-
- await confirmDeletionDialog
- .locator( 'role=button[name="OK"i]' )
- .click();
- }
-
- // Saving isn't technically necessary, but for themes without any specified templates,
- // the removal of the Templates dropdown is delayed. A save and reload allows for this
- // delay and prevents flakiness
- {
- await page.click( 'role=button[name="Save draft"i]' );
- await page.waitForSelector(
- 'role=button[name="Dismiss this notice"] >> text=Draft saved'
- );
- await page.reload();
- }
-
- const templateOptions =
- postEditorTemplateMode.editorSettingsSidebar.locator(
- 'role=combobox[name="Template:"i] >> role=menuitem'
- );
- const availableTemplates =
- await templateOptions.allTextContents();
-
- expect( availableTemplates ).not.toContain(
- `${ viewport } Viewport Deletion Test`
- );
- } );
- } );
- } );
} );
class PostEditorTemplateMode {
@@ -331,7 +149,9 @@ class PostEditorTemplateMode {
'role=button[name="Dismiss this notice"] >> text=Editing template. Changes made here affect all posts and pages that use the template.'
);
- await expect( this.editorTopBar ).toHaveText( /Just an FSE Post/ );
+ await expect(
+ this.editorTopBar.getByRole( 'heading[level=1]' )
+ ).toHaveText( 'Editing template: Singular' );
}
async createPostAndSaveDraft() {
diff --git a/test/e2e/specs/site-editor/command-center.spec.js b/test/e2e/specs/site-editor/command-center.spec.js
index 9661a91a6abc78..57b07faf4d9b48 100644
--- a/test/e2e/specs/site-editor/command-center.spec.js
+++ b/test/e2e/specs/site-editor/command-center.spec.js
@@ -28,6 +28,9 @@ test.describe( 'Site editor command center', () => {
await page.getByRole( 'option', { name: 'Add new page' } ).click();
await page.waitForSelector( 'iframe[name="editor-canvas"]' );
const frame = page.frame( 'editor-canvas' );
+ await expect( page ).toHaveURL(
+ '/wp-admin/post-new.php?post_type=page'
+ );
await expect(
frame.getByRole( 'textbox', { name: 'Add title' } )
).toBeVisible();
diff --git a/test/integration/fixtures/blocks/core__query-pagination.json b/test/integration/fixtures/blocks/core__query-pagination.json
index 66b604f14b8a17..49d6992bac3a53 100644
--- a/test/integration/fixtures/blocks/core__query-pagination.json
+++ b/test/integration/fixtures/blocks/core__query-pagination.json
@@ -3,7 +3,8 @@
"name": "core/query-pagination",
"isValid": true,
"attributes": {
- "paginationArrow": "none"
+ "paginationArrow": "none",
+ "showLabel": true
},
"innerBlocks": []
}
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-3.json b/test/integration/fixtures/blocks/core__query__deprecated-3.json
index 2c682de49bdda6..6b3327eacdc3f3 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-3.json
+++ b/test/integration/fixtures/blocks/core__query__deprecated-3.json
@@ -87,7 +87,8 @@
"name": "core/query-pagination",
"isValid": true,
"attributes": {
- "paginationArrow": "none"
+ "paginationArrow": "none",
+ "showLabel": true
},
"innerBlocks": [
{