Commit cd8eae6f4ad for woocommerce
commit cd8eae6f4ad2947b7336330e8f9685ccbfb3a090
Author: Yuliyan Slavchev <yuliyan.slavchev@gmail.com>
Date: Tue Sep 15 13:40:33 2026 +0300
Email Editor: Fix template selector layout with Gutenberg 23.9+ (#68551)
* Email Editor: Fix template selector layout with Gutenberg 23.9+
* Use Notice component for the Recent templates category
* Replace HStack with Stack in templates list component
* Use EmptyState component in TemplateNoResults
* Fix changelog type
* Update changelog significance to minor
diff --git a/packages/js/email-editor/README.md b/packages/js/email-editor/README.md
index c848349e283..cb98caa0c30 100644
--- a/packages/js/email-editor/README.md
+++ b/packages/js/email-editor/README.md
@@ -261,18 +261,24 @@ For Jest (or any non-webpack test runner), the runtime fallback applies, so unit
globalThis.__i18n_text_domain__ = 'your-text-domain';
```
-#### Global Styles Engine
+#### Packages that must be bundled
-A of 1.4.3 the email editor package depends on `@wordpress/global-styles-engine`, which is **not enqueued by WordPress core**. Unlike most `@wordpress/*` packages, this package is not available globally in WordPress environments and must be bundled.
+The email editor package depends on a few `@wordpress/*` packages that are **not enqueued by WordPress core**. Unlike most `@wordpress/*` packages, they are not available globally in WordPress environments and must be bundled:
-**Impact**: If your build configuration marks all `@wordpress/*` packages as externals (common in webpack configs), you will encounter runtime errors: `"Cannot find module '@wordpress/global-styles-engine'"`.
+- `@wordpress/global-styles-engine` (since 1.4.3)
+- `@wordpress/ui` (since 2.5.0)
-**Solution**: Configure your webpack dependency extraction plugin to bundle this package instead of treating it as an external:
+**Impact**: If your build configuration marks all `@wordpress/*` packages as externals (common in webpack configs), you will encounter runtime errors such as `"Cannot find module '@wordpress/global-styles-engine'"`.
+
+**Solution**: Configure your webpack dependency extraction plugin to bundle these packages instead of treating them as externals:
```javascript
new DependencyExtractionWebpackPlugin( {
requestToExternal( request ) {
- if ( request === '@wordpress/global-styles-engine' ) {
+ if (
+ request === '@wordpress/global-styles-engine' ||
+ request === '@wordpress/ui'
+ ) {
// Return null to bundle this package instead of treating it as external
return null;
}
diff --git a/packages/js/email-editor/changelog/wooplug-7713-template-selector-layout-breaks-with-gutenberg-239 b/packages/js/email-editor/changelog/wooplug-7713-template-selector-layout-breaks-with-gutenberg-239
new file mode 100644
index 00000000000..c4cec85dae3
--- /dev/null
+++ b/packages/js/email-editor/changelog/wooplug-7713-template-selector-layout-breaks-with-gutenberg-239
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Fix the template selector layout with Gutenberg 23.9+ by giving it its own styles and using Tabs from @wordpress/ui for the category list
diff --git a/packages/js/email-editor/package.json b/packages/js/email-editor/package.json
index 7f7c3183a62..e83a879fa5b 100644
--- a/packages/js/email-editor/package.json
+++ b/packages/js/email-editor/package.json
@@ -112,6 +112,7 @@
"@wordpress/priority-queue": "catalog:wp-min",
"@wordpress/private-apis": "catalog:wp-min",
"@wordpress/rich-text": "catalog:wp-min",
+ "@wordpress/ui": "catalog:wp-bundled",
"@wordpress/url": "catalog:wp-min",
"clsx": "2.1.x",
"deepmerge": "^4.3.1",
diff --git a/packages/js/email-editor/src/components/template-select/select-modal.tsx b/packages/js/email-editor/src/components/template-select/select-modal.tsx
index df740d90225..515717c1f7f 100644
--- a/packages/js/email-editor/src/components/template-select/select-modal.tsx
+++ b/packages/js/email-editor/src/components/template-select/select-modal.tsx
@@ -8,6 +8,7 @@ import type { UserPatternCategory } from '@wordpress/core-data/build-types/selec
import { dispatch, useSelect } from '@wordpress/data';
import { Modal, Button, Flex, FlexItem } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
+import { Tabs } from '@wordpress/ui';
/**
* Internal dependencies
@@ -102,26 +103,43 @@ function SelectTemplateBody( {
return () => clearTimeout( timeoutId );
}, [ displayCategories, selectedCategory ] );
- return (
- <div
- className={ `block-editor-block-patterns-explorer${
- displayCategories.length === 0 ? ' no-sidebar' : ''
- }` }
- >
- { displayCategories.length > 0 && (
- <TemplateCategoriesListSidebar
- templateCategories={ displayCategories }
+ if ( displayCategories.length === 0 ) {
+ return (
+ <div className="email-editor-template-select">
+ <TemplateList
+ templates={ templates }
+ onTemplateSelection={ handleTemplateSelection }
selectedCategory={ selectedCategory }
- onClickCategory={ handleCategorySelection }
/>
- ) }
+ </div>
+ );
+ }
- <TemplateList
- templates={ templates }
- onTemplateSelection={ handleTemplateSelection }
- selectedCategory={ selectedCategory }
+ return (
+ <Tabs.Root
+ className="email-editor-template-select"
+ orientation="vertical"
+ value={ selectedCategory }
+ onValueChange={ handleCategorySelection }
+ >
+ <TemplateCategoriesListSidebar
+ templateCategories={ displayCategories }
/>
- </div>
+ { displayCategories.map( ( { name } ) => (
+ <Tabs.Panel
+ key={ name }
+ value={ name }
+ tabIndex={ -1 }
+ className="email-editor-template-select__panel"
+ >
+ <TemplateList
+ templates={ templates }
+ onTemplateSelection={ handleTemplateSelection }
+ selectedCategory={ name }
+ />
+ </Tabs.Panel>
+ ) ) }
+ </Tabs.Root>
);
}
diff --git a/packages/js/email-editor/src/components/template-select/style.scss b/packages/js/email-editor/src/components/template-select/style.scss
index 922906b2658..50a394fbf82 100644
--- a/packages/js/email-editor/src/components/template-select/style.scss
+++ b/packages/js/email-editor/src/components/template-select/style.scss
@@ -1,54 +1,88 @@
-/* Move the less specific selector before the more specific one */
+@import "~@wordpress/base-styles/variables";
+@import "~@wordpress/base-styles/breakpoints";
+@import "~@wordpress/base-styles/mixins";
-.block-editor-block-patterns-list__item .block-editor-block-preview__container {
- align-items: flex-start !important;
+$email-editor-template-select-sidebar-width: 280px;
+
+.email-editor-template-select__sidebar {
+ padding-bottom: $grid-unit-30;
+
+ @include break-medium() {
+ bottom: 0;
+ inset-inline-start: 0;
+ overflow-x: visible;
+ overflow-y: auto;
+ padding: $grid-unit-30;
+ position: absolute;
+ top: $header-height + $grid-unit-10;
+ width: $email-editor-template-select-sidebar-width;
+ }
}
-.block-editor-block-patterns-explorer
- .block-editor-block-patterns-list
- .block-editor-block-preview__container {
- max-height: 300px !important;
+.email-editor-template-select__panel {
+ @include break-medium() {
+ margin-inline-start: $email-editor-template-select-sidebar-width;
+ }
}
-.block-editor-block-patterns-list__list-item {
- overflow: hidden;
+.email-editor-template-select__list {
+ padding: $grid-unit-30 0 $grid-unit-40;
}
-.email-editor-recent-templates-info {
- background-color: #f0f6fc;
- margin-bottom: 1.5rem;
- padding: 0.5rem;
+.email-editor-template-select__templates {
+ display: grid;
+ grid-gap: $grid-unit-40;
+ grid-template-columns: repeat(1, 1fr);
+ margin-bottom: $grid-unit-20;
+
+ @include break-xlarge() {
+ grid-template-columns: repeat(2, 1fr);
+ }
- path {
- fill: #007cba;
+ @include break-huge() {
+ grid-template-columns: repeat(3, 1fr);
}
}
-.email-editor-pattern__list-item {
+.email-editor-template-select__template {
border: 1px solid #ddd;
border-radius: 4px;
+ cursor: pointer;
+ min-height: 240px;
+ overflow: hidden;
+ position: relative;
+}
- .block-editor-block-preview__container::after {
- outline: unset !important;
- outline-offset: unset !important;
- }
+.email-editor-template-select__template-button {
+ height: 100%;
- .block-editor-block-patterns-list__item-title {
- font-size: unset !important;
- padding-left: 1rem;
+ .block-editor-block-preview__container {
+ align-items: flex-start;
+ border-radius: 4px;
+ display: flex;
+ max-height: 300px;
}
}
-.block-editor-block-patterns-explorer.no-sidebar
- .block-editor-block-patterns-explorer__list {
- margin-left: 0;
- padding-left: 2rem;
+.email-editor-template-select__template-details {
+ margin-top: $grid-unit-10;
+ padding-bottom: $grid-unit-05;
+}
+
+.email-editor-template-select__template-title {
+ flex-grow: 1;
+ padding-inline-start: 1rem;
+ text-align: start;
+}
+
+.email-editor-template-select__no-results {
+ margin-inline: auto;
}
.email-editor-modal-footer {
background-color: #fff;
bottom: 0;
+ inset-inline-end: 0;
padding: 1rem 2rem;
position: absolute;
- right: 0;
}
diff --git a/packages/js/email-editor/src/components/template-select/template-categories-list-sidebar.tsx b/packages/js/email-editor/src/components/template-select/template-categories-list-sidebar.tsx
index 5025f78a7f4..b3b286486cf 100644
--- a/packages/js/email-editor/src/components/template-select/template-categories-list-sidebar.tsx
+++ b/packages/js/email-editor/src/components/template-select/template-categories-list-sidebar.tsx
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { Button } from '@wordpress/components';
+import { Tabs } from '@wordpress/ui';
/**
* Internal dependencies
@@ -9,36 +9,19 @@ import { Button } from '@wordpress/components';
import { TemplateCategory } from '../../store';
type Props = {
- selectedCategory?: TemplateCategory;
templateCategories: Array< { name: TemplateCategory; label: string } >;
- onClickCategory: ( name: TemplateCategory ) => void;
};
-export function TemplateCategoriesListSidebar( {
- selectedCategory,
- templateCategories,
- onClickCategory,
-}: Props ) {
- const baseClassName = 'block-editor-block-patterns-explorer__sidebar';
+export function TemplateCategoriesListSidebar( { templateCategories }: Props ) {
return (
- <div className={ baseClassName }>
- <div className={ `${ baseClassName }__categories-list` }>
- { templateCategories.map( ( { name, label } ) => {
- return (
- <Button
- key={ name }
- label={ label }
- className={ `${ baseClassName }__categories-list__item` }
- isPressed={ selectedCategory === name }
- onClick={ () => {
- onClickCategory( name );
- } }
- >
- { label }
- </Button>
- );
- } ) }
- </div>
+ <div className="email-editor-template-select__sidebar">
+ <Tabs.List>
+ { templateCategories.map( ( { name, label } ) => (
+ <Tabs.Tab key={ name } value={ name }>
+ { label }
+ </Tabs.Tab>
+ ) ) }
+ </Tabs.List>
</div>
);
}
diff --git a/packages/js/email-editor/src/components/template-select/template-list.tsx b/packages/js/email-editor/src/components/template-select/template-list.tsx
index 51ddf0aa09f..cf40f04d125 100644
--- a/packages/js/email-editor/src/components/template-select/template-list.tsx
+++ b/packages/js/email-editor/src/components/template-select/template-list.tsx
@@ -4,11 +4,9 @@
import { useMemo, memo } from '@wordpress/element';
import { store as editorStore } from '@wordpress/editor';
import { useSelect } from '@wordpress/data';
-import { Icon, info, blockDefault } from '@wordpress/icons';
+import { blockDefault } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
-import {
- __experimentalHStack as HStack, // eslint-disable-line
-} from '@wordpress/components';
+import { EmptyState, Notice, Stack } from '@wordpress/ui';
// @ts-expect-error No types available for this component
// eslint-disable-next-line
import { BlockPreview } from '@wordpress/block-editor';
@@ -28,19 +26,18 @@ type Props = {
function TemplateNoResults() {
return (
- <div className="block-editor-inserter__no-results">
- <Icon
- className="block-editor-inserter__no-results-icon"
- icon={ blockDefault }
- />
- <p>{ __( 'No recent templates.', __i18n_text_domain__ ) }</p>
- <p>
+ <EmptyState.Root className="email-editor-template-select__no-results">
+ <EmptyState.Icon icon={ blockDefault } />
+ <EmptyState.Title>
+ { __( 'No recent templates.', __i18n_text_domain__ ) }
+ </EmptyState.Title>
+ <EmptyState.Description>
{ __(
'Your recent creations will appear here as soon as you begin.',
__i18n_text_domain__
) }
- </p>
- </div>
+ </EmptyState.Description>
+ </EmptyState.Root>
);
}
@@ -73,14 +70,14 @@ function TemplateListBox( {
}
return (
- <div className="block-editor-block-patterns-list" role="listbox">
+ <div className="email-editor-template-select__templates" role="listbox">
{ templates.map( ( template ) => (
<div
key={ `${ template.slug }_${ template.displayName }_${ template.id }` }
- className="block-editor-block-patterns-list__list-item email-editor-pattern__list-item"
+ className="email-editor-template-select__template"
>
<div
- className="block-editor-block-patterns-list__item"
+ className="email-editor-template-select__template-button"
role="button"
tabIndex={ 0 }
onClick={ () => {
@@ -113,11 +110,15 @@ function TemplateListBox( {
] }
/>
- <HStack className="block-editor-patterns__pattern-details">
- <h4 className="block-editor-block-patterns-list__item-title">
+ <Stack
+ direction="row"
+ align="center"
+ className="email-editor-template-select__template-details"
+ >
+ <h4 className="email-editor-template-select__template-title">
{ template.displayName }
</h4>
- </HStack>
+ </Stack>
</Async>
</div>
</div>
@@ -148,19 +149,20 @@ export function TemplateList( {
);
return (
- <div className="block-editor-block-patterns-explorer__list">
+ <Stack
+ direction="column"
+ gap="xl"
+ className="email-editor-template-select__list"
+ >
{ selectedCategory === 'recent' && (
- <div className="email-editor-recent-templates-info">
- <HStack spacing={ 1 } expanded={ false } justify="start">
- <Icon icon={ info } />
- <p>
- { __(
- 'Templates created on the legacy editor will not appear here.',
- __i18n_text_domain__
- ) }
- </p>
- </HStack>
- </div>
+ <Notice.Root intent="info">
+ <Notice.Description>
+ { __(
+ 'Templates created on the legacy editor will not appear here.',
+ __i18n_text_domain__
+ ) }
+ </Notice.Description>
+ </Notice.Root>
) }
<MemorizedTemplateListBox
@@ -168,6 +170,6 @@ export function TemplateList( {
onTemplateSelection={ onTemplateSelection }
selectedCategory={ selectedCategory }
/>
- </div>
+ </Stack>
);
}
diff --git a/packages/js/email-editor/src/components/template-select/test/template-categories-list-sidebar.test.tsx b/packages/js/email-editor/src/components/template-select/test/template-categories-list-sidebar.test.tsx
new file mode 100644
index 00000000000..13bc04e76f0
--- /dev/null
+++ b/packages/js/email-editor/src/components/template-select/test/template-categories-list-sidebar.test.tsx
@@ -0,0 +1,57 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { Tabs } from '@wordpress/ui';
+
+/**
+ * Internal dependencies
+ */
+import { TemplateCategoriesListSidebar } from '../template-categories-list-sidebar';
+import type { TemplateCategory } from '../../../store';
+
+const categories: Array< { name: TemplateCategory; label: string } > = [
+ { name: 'recent', label: 'Recent' },
+ { name: 'basic', label: 'Basic' },
+];
+
+describe( 'TemplateCategoriesListSidebar', () => {
+ it( 'renders one tab per category', () => {
+ render(
+ <Tabs.Root orientation="vertical" value="basic">
+ <TemplateCategoriesListSidebar
+ templateCategories={ categories }
+ />
+ </Tabs.Root>
+ );
+
+ const tabs = screen.getAllByRole( 'tab' );
+ expect( tabs ).toHaveLength( 2 );
+ expect( tabs[ 0 ] ).toHaveTextContent( 'Recent' );
+ expect( tabs[ 1 ] ).toHaveTextContent( 'Basic' );
+ expect( tabs[ 1 ] ).toHaveAttribute( 'aria-selected', 'true' );
+ } );
+
+ it( 'reports the clicked category', async () => {
+ const onValueChange = jest.fn();
+ render(
+ <Tabs.Root
+ orientation="vertical"
+ value="basic"
+ onValueChange={ onValueChange }
+ >
+ <TemplateCategoriesListSidebar
+ templateCategories={ categories }
+ />
+ </Tabs.Root>
+ );
+
+ await userEvent.click( screen.getByRole( 'tab', { name: 'Recent' } ) );
+
+ expect( onValueChange ).toHaveBeenCalledWith(
+ 'recent',
+ expect.anything()
+ );
+ } );
+} );
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4122aee3bbc..5faae16a9b9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1312,6 +1312,9 @@ importers:
'@wordpress/rich-text':
specifier: catalog:wp-min
version: 7.33.2(@types/react@18.3.28)(react@18.3.1)
+ '@wordpress/ui':
+ specifier: catalog:wp-bundled
+ version: 0.17.0(@date-fns/tz@1.4.1)(@types/react@18.3.28)(date-fns@4.1.0)(postcss@8.5.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@14.16.1)
'@wordpress/url':
specifier: catalog:wp-min
version: 4.33.1