Commit b681dadd65f for woocommerce
commit b681dadd65fe8363dd449a75476a90f14f01a972
Author: Jan Lysý <lysyjan@users.noreply.github.com>
Date: Tue Mar 17 16:36:11 2026 +0100
Email Editor: Add filter for send button disabled state (#63722)
* Add woocommerce_email_editor_send_button_disabled filter to SendButton
Allow consumers to override the send button's disabled state via filter.
The filter passes the computed disabled boolean and the individual flags
(hasEmptyContent, isEmailSent, isDirty) so consumers can make informed
decisions. Default behavior is unchanged.
This enables CIAB to keep the "Review and send" button always enabled
and auto-save unsaved changes when clicked, instead of requiring users
to manually save a draft first.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix prettier formatting in send button test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add send button disabled filter to README filters table
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add runtime validation for send button disabled filter
Validate that the filtered value is a boolean at runtime, falling back
to the default disabled state if a third-party filter returns an
unexpected type. Addresses CodeRabbit review feedback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Assert filter contract in send button disabled test
Add assertion that applyFilters is called with the full flags object
(hasEmptyContent, isEmailSent, isDirty) to lock the public hook
contract and prevent silent regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use mockImplementationOnce to prevent cross-test leakage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/packages/js/email-editor/README.md b/packages/js/email-editor/README.md
index a593bc8084d..c5e63bd7a84 100644
--- a/packages/js/email-editor/README.md
+++ b/packages/js/email-editor/README.md
@@ -318,6 +318,7 @@ We may add, update and delete any of them.
| `woocommerce_email_editor_wrap_editor_component` | `JSX.Element` Editor | `JSX.Element` Editor | The main editor component. Custom component can wrap the editor and provide additional functionality |
| `woocommerce_email_editor_send_button_label` | `string` 'Send' | `string` 'Send' (default) | Email editor send button label. The `Send` text can be updated using this filter |
| `woocommerce_email_editor_send_action_callback` | `function` sendAction | `function` sendAction | Action to perform when the Send button is clicked |
+| `woocommerce_email_editor_send_button_disabled` | `boolean` isDisabled, `object` flags | `boolean` isDisabled | Override the send button disabled state. The flags object contains `hasEmptyContent`, `isEmailSent`, and `isDirty` booleans |
| `woocommerce_email_editor_content_validation_rules` | `array` rules | `EmailContentValidationRule[]` rules | Email editor content validation rules. The validation is done on `send button` click and revalidated on `save draft` |
| `woocommerce_email_editor_check_sending_method_configuration_link` | `string` link | `string` link | Edit or remove the sending configuration link message |
| `woocommerce_email_editor_setting_sidebar_extension_component` | `JSX.Element` RichTextWithButton | `JSX.Element` Sidebar extension component | Add components to the Email settings sidebar |
diff --git a/packages/js/email-editor/changelog/add-send-button-disabled-filter b/packages/js/email-editor/changelog/add-send-button-disabled-filter
new file mode 100644
index 00000000000..73e7efa3ec8
--- /dev/null
+++ b/packages/js/email-editor/changelog/add-send-button-disabled-filter
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add `woocommerce_email_editor_send_button_disabled` filter to allow overriding the send button disabled state
diff --git a/packages/js/email-editor/src/components/header/send-button.tsx b/packages/js/email-editor/src/components/header/send-button.tsx
index 2330484e7cc..4fd691f7f7e 100644
--- a/packages/js/email-editor/src/components/header/send-button.tsx
+++ b/packages/js/email-editor/src/components/header/send-button.tsx
@@ -34,7 +34,16 @@ export function SendButton() {
}
}
- const isDisabled = hasEmptyContent || isEmailSent || isDirty;
+ const defaultIsDisabled = hasEmptyContent || isEmailSent || isDirty;
+ const filteredIsDisabled = applyFilters(
+ 'woocommerce_email_editor_send_button_disabled',
+ defaultIsDisabled,
+ { hasEmptyContent, isEmailSent, isDirty }
+ );
+ const isDisabled =
+ typeof filteredIsDisabled === 'boolean'
+ ? filteredIsDisabled
+ : defaultIsDisabled;
const label = applyFilters(
'woocommerce_email_editor_send_button_label',
diff --git a/packages/js/email-editor/src/components/header/test/send-button.spec.tsx b/packages/js/email-editor/src/components/header/test/send-button.spec.tsx
index ce3ce53ac7c..e37aa30489a 100644
--- a/packages/js/email-editor/src/components/header/test/send-button.spec.tsx
+++ b/packages/js/email-editor/src/components/header/test/send-button.spec.tsx
@@ -7,6 +7,7 @@ import '../../test/__mocks__/setup-shared-mocks';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useSelect } from '@wordpress/data';
+import { applyFilters } from '@wordpress/hooks';
import {
// @ts-expect-error -- It is not exported yet.
useEntitiesSavedStatesIsDirty,
@@ -31,6 +32,7 @@ const useSelectMock = useSelect as jest.Mock;
const useEntitiesSavedStatesIsDirtyMock =
useEntitiesSavedStatesIsDirty as jest.Mock;
const recordEventMock = recordEvent as jest.Mock;
+const applyFiltersMock = applyFilters as jest.Mock;
const mockStoreValues = {
hasEmptyContent: false,
@@ -70,6 +72,56 @@ describe( 'SendButton', () => {
expect( getByRole( 'button' ) ).toBeDisabled();
} );
+ it( 'should allow the disabled state to be overridden via filter', () => {
+ useEntitiesSavedStatesIsDirtyMock.mockReturnValue( { isDirty: true } );
+
+ // Override applyFilters to simulate a filter that removes isDirty from disabled
+ applyFiltersMock.mockImplementationOnce(
+ ( hook: string, value: unknown, ...args: unknown[] ) => {
+ if (
+ hook === 'woocommerce_email_editor_send_button_disabled'
+ ) {
+ const flags = args[ 0 ] as {
+ hasEmptyContent: boolean;
+ isEmailSent: boolean;
+ };
+ return flags.hasEmptyContent || flags.isEmailSent;
+ }
+ return value;
+ }
+ );
+
+ const { getByRole } = render( <SendButton /> );
+ expect( getByRole( 'button' ) ).not.toBeDisabled();
+ expect( applyFiltersMock ).toHaveBeenCalledWith(
+ 'woocommerce_email_editor_send_button_disabled',
+ true,
+ {
+ hasEmptyContent: false,
+ isEmailSent: false,
+ isDirty: true,
+ }
+ );
+ } );
+
+ it( 'should fall back to default when filter returns non-boolean', () => {
+ useEntitiesSavedStatesIsDirtyMock.mockReturnValue( { isDirty: true } );
+
+ applyFiltersMock.mockImplementationOnce(
+ ( hook: string, value: unknown ) => {
+ if (
+ hook === 'woocommerce_email_editor_send_button_disabled'
+ ) {
+ return 'not-a-boolean';
+ }
+ return value;
+ }
+ );
+
+ const { getByRole } = render( <SendButton /> );
+ expect( getByRole( 'button' ) ).toBeDisabled();
+ } );
+
it( 'should be disabled if hasEmptyContent is true', () => {
mockStoreValues.hasEmptyContent = true;