Commit f81d6f7b0f7 for woocommerce
commit f81d6f7b0f78ccaada5cfcc08aeea0925298af19
Author: Rostislav Wolný <1082140+costasovo@users.noreply.github.com>
Date: Fri Aug 7 16:20:08 2026 +0200
[Email Editor] Fix back button rendering on WordPress 7.1 (#67470)
* Adapt email editor back button to the WordPress 7.1 header
WordPress 7.1 shows the admin bar in the editor by default and shrank
the header's back-button slot from 64px to 32px, rendering it as a
compact chevron. The email editor's fullscreen-style WordPress-logo
button collapsed into its own padding in that slot and appeared as an
empty dark rectangle.
The back button now measures the width the header reserves for it and
renders a compact chevron matching core in the narrow slot, keeping the
fullscreen-style button in the wide slot of older WordPress versions.
Detecting the layout instead of the WordPress version keeps the fix
working for consumers that embed the editor with their own
configuration. The legacy button and the detection are temporary and go
away when WordPress 7.0 support is dropped.
The close action now guards on the URL it navigates to and validates
the woocommerce_email_editor_close_action_callback filter return before
calling it, matching how other filter values are validated.
diff --git a/packages/js/email-editor/changelog/update-back-button-wp71 b/packages/js/email-editor/changelog/update-back-button-wp71
new file mode 100644
index 00000000000..c2d9be93b8a
--- /dev/null
+++ b/packages/js/email-editor/changelog/update-back-button-wp71
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Render the editor back button as a compact chevron on WordPress 7.1+ to match the redesigned header, keeping the fullscreen-style button on older versions
diff --git a/packages/js/email-editor/src/components/header/back-button-content.tsx b/packages/js/email-editor/src/components/header/back-button-content.tsx
index c5367d43df9..97ea14ae61c 100644
--- a/packages/js/email-editor/src/components/header/back-button-content.tsx
+++ b/packages/js/email-editor/src/components/header/back-button-content.tsx
@@ -2,8 +2,15 @@
* External dependencies
*/
import { Button, __unstableMotion as motion } from '@wordpress/components';
-import { __ } from '@wordpress/i18n';
-import { Icon, arrowLeft, wordpress } from '@wordpress/icons';
+import { useLayoutEffect, useRef, useState } from '@wordpress/element';
+import { __, isRTL } from '@wordpress/i18n';
+import {
+ Icon,
+ arrowLeft,
+ chevronLeft,
+ chevronRight,
+ wordpress,
+} from '@wordpress/icons';
import { applyFilters } from '@wordpress/hooks';
import { useSelect } from '@wordpress/data';
@@ -14,6 +21,12 @@ import { BackButton } from '../../private-apis';
import { recordEvent } from '../../events';
import { storeName } from '../../store';
+// The WordPress 7.1+ header reserves a compact 32px slot for the back button
+// and renders it as a plain chevron; older versions reserve a 64px slot filled
+// with a fullscreen-style logo button. The slot width is what our button must
+// fit into, so detect it instead of the WordPress version.
+const COMPACT_SLOT_MAX_WIDTH = 48;
+
const toggleHomeIconVariants = {
edit: {
opacity: 0,
@@ -38,10 +51,7 @@ const siteIconVariants = {
},
};
-/**
- * Back button content component with animation effects.
- */
-const DefaultBackButtonContent = () => {
+function useCloseAction() {
const { urls } = useSelect(
( select ) => ( {
urls: select( storeName ).getUrls(),
@@ -49,11 +59,59 @@ const DefaultBackButtonContent = () => {
[]
);
- function backAction() {
- if ( urls.listings ) {
- window.location.href = urls.back;
- }
- }
+ return () => {
+ recordEvent( 'header_close_button_clicked' );
+ const defaultAction = () => {
+ if ( ! urls.back ) {
+ return;
+ }
+ try {
+ // Resolve against the full current URL so relative paths
+ // keep the same meaning as a direct location assignment.
+ const backUrl = new URL( urls.back, window.location.href );
+ // Only navigate to web URLs so schemes like javascript:
+ // cannot reach window.location.
+ if ( [ 'http:', 'https:' ].includes( backUrl.protocol ) ) {
+ window.location.href = backUrl.href;
+ }
+ } catch {
+ // Do not navigate to an invalid URL.
+ }
+ };
+ const action = applyFilters(
+ 'woocommerce_email_editor_close_action_callback',
+ defaultAction
+ );
+ ( typeof action === 'function' ? action : defaultAction )();
+ };
+}
+
+/**
+ * Compact back button fitting the narrow slot of the WordPress 7.1+ header,
+ * rendered as a plain chevron matching core.
+ */
+const CompactBackButtonContent = () => {
+ const onClose = useCloseAction();
+
+ return (
+ <Button
+ size="compact"
+ icon={ isRTL() ? chevronRight : chevronLeft }
+ label={ __( 'Close editor', __i18n_text_domain__ ) }
+ showTooltip
+ tooltipPosition="middle right"
+ onClick={ onClose }
+ />
+ );
+};
+
+/**
+ * Fullscreen-style back button filling the 64px slot of the WordPress ≤ 7.0
+ * header, rendered as the WordPress logo with a hover arrow. This button will
+ * be dropped after we drop support for WordPress 7.0.
+ */
+const FullscreenBackButtonContent = () => {
+ const onClose = useCloseAction();
return (
<motion.div
@@ -70,14 +128,7 @@ const DefaultBackButtonContent = () => {
label={ __( 'Close editor', __i18n_text_domain__ ) }
showTooltip
tooltipPosition="middle right"
- onClick={ () => {
- recordEvent( 'header_close_button_clicked' );
- const action = applyFilters(
- 'woocommerce_email_editor_close_action_callback',
- backAction
- ) as () => void;
- action();
- } }
+ onClick={ onClose }
>
<motion.div variants={ siteIconVariants }>
<div className="woocommerce-email-editor__view-mode-toggle-icon">
@@ -99,6 +150,39 @@ const DefaultBackButtonContent = () => {
);
};
+/**
+ * Back button content component. Picks the variant fitting the width the
+ * editor header reserves for the back button. The detection is temporary and
+ * will be dropped along with the fullscreen-style button after we drop
+ * support for WordPress 7.0.
+ */
+const DefaultBackButtonContent = () => {
+ const measureRef = useRef< HTMLDivElement >( null );
+ const [ isCompactSlot, setIsCompactSlot ] = useState< boolean | null >(
+ null
+ );
+
+ useLayoutEffect( () => {
+ const slot = measureRef.current?.closest< HTMLElement >(
+ '.editor-header__back-button'
+ );
+ const slotWidth = slot?.getBoundingClientRect().width ?? 0;
+ setIsCompactSlot(
+ slotWidth > 0 && slotWidth <= COMPACT_SLOT_MAX_WIDTH
+ );
+ }, [] );
+
+ if ( isCompactSlot === null ) {
+ return <div ref={ measureRef } />;
+ }
+
+ return isCompactSlot ? (
+ <CompactBackButtonContent />
+ ) : (
+ <FullscreenBackButtonContent />
+ );
+};
+
export const BackButtonContent = () => {
const BackButtonUsedContent = applyFilters(
'woocommerce_email_editor_close_content',
diff --git a/packages/js/email-editor/src/components/header/test/back-button-content.spec.tsx b/packages/js/email-editor/src/components/header/test/back-button-content.spec.tsx
index e9e29046448..16ccbabedd8 100644
--- a/packages/js/email-editor/src/components/header/test/back-button-content.spec.tsx
+++ b/packages/js/email-editor/src/components/header/test/back-button-content.spec.tsx
@@ -7,6 +7,7 @@ import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useSelect } from '@wordpress/data';
import { applyFilters } from '@wordpress/hooks';
+import { isRTL } from '@wordpress/i18n';
/**
* Internal dependencies
@@ -15,8 +16,8 @@ import { BackButtonContent } from '../back-button-content';
import { storeName } from '../../../store';
jest.mock( '@wordpress/components', () => ( {
- Button: ( { children, label, onClick } ) => (
- <button aria-label={ label } onClick={ onClick }>
+ Button: ( { children, label, onClick, icon } ) => (
+ <button aria-label={ label } onClick={ onClick } data-icon={ icon }>
{ children }
</button>
),
@@ -30,6 +31,8 @@ jest.mock( '@wordpress/components', () => ( {
jest.mock( '@wordpress/icons', () => ( {
Icon: () => <span>Icon</span>,
arrowLeft: 'arrowLeft',
+ chevronLeft: 'chevronLeft',
+ chevronRight: 'chevronRight',
wordpress: 'wordpress',
} ) );
@@ -46,6 +49,21 @@ const mockUrls = {
send: 'https://example.com/send',
};
+// Renders the component inside the editor header's back button slot with the
+// given width. jsdom has no layout, so getBoundingClientRect is stubbed.
+const renderInSlot = ( slotWidth: number ) => {
+ jest.spyOn(
+ HTMLElement.prototype,
+ 'getBoundingClientRect'
+ ).mockReturnValue( { width: slotWidth } as DOMRect );
+
+ return render(
+ <div className="editor-header__back-button">
+ <BackButtonContent />
+ </div>
+ );
+};
+
describe( 'BackButtonContent', () => {
beforeEach( () => {
jest.clearAllMocks();
@@ -67,6 +85,10 @@ describe( 'BackButtonContent', () => {
);
} );
+ afterEach( () => {
+ jest.restoreAllMocks();
+ } );
+
it( 'should render the back button', () => {
const { container } = render( <BackButtonContent /> );
expect(
@@ -92,6 +114,35 @@ describe( 'BackButtonContent', () => {
expect( button.onclick ).not.toBeNull();
} );
+ it( 'should render the fullscreen-style button in a wide slot (WordPress ≤ 7.0 header)', () => {
+ const { container } = renderInSlot( 64 );
+ expect(
+ container.querySelector(
+ '.woocommerce-email-editor__view-mode-toggle'
+ )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'should render the compact button in a narrow slot (WordPress 7.1+ header)', () => {
+ const { container, getByRole } = renderInSlot( 32 );
+ expect(
+ container.querySelector(
+ '.woocommerce-email-editor__view-mode-toggle'
+ )
+ ).not.toBeInTheDocument();
+ expect(
+ getByRole( 'button', { name: 'Close editor' } )
+ ).toHaveAttribute( 'data-icon', 'chevronLeft' );
+ } );
+
+ it( 'should render the right chevron in a narrow slot in RTL', () => {
+ ( isRTL as jest.Mock ).mockReturnValueOnce( true );
+ const { getByRole } = renderInSlot( 32 );
+ expect(
+ getByRole( 'button', { name: 'Close editor' } )
+ ).toHaveAttribute( 'data-icon', 'chevronRight' );
+ } );
+
it( 'should apply woocommerce_email_editor_close_content filter to render custom component', () => {
// Mock the filter to return a custom component
const CustomComponent = () => (
diff --git a/packages/js/email-editor/src/components/test/__mocks__/wordpress-i18n.ts b/packages/js/email-editor/src/components/test/__mocks__/wordpress-i18n.ts
index 8883d664180..db10fc7d75f 100644
--- a/packages/js/email-editor/src/components/test/__mocks__/wordpress-i18n.ts
+++ b/packages/js/email-editor/src/components/test/__mocks__/wordpress-i18n.ts
@@ -1,4 +1,5 @@
jest.mock( '@wordpress/i18n', () => ( {
__: ( str: string ) => str,
sprintf: ( format: string, value: string ) => format.replace( '%s', value ),
+ isRTL: jest.fn( () => false ),
} ) );