diff --git a/src/edit.js b/src/edit.js
index fd9c605..a77f19a 100644
--- a/src/edit.js
+++ b/src/edit.js
@@ -3,19 +3,29 @@
*
* The MediaPlaceholder is the same UX as the core Image block. After an
* image is selected, the block renders the image with the filter class
- * and intensity CSS custom property. The Inspector panel is mounted
- * separately.
+ * and intensity CSS custom property. The filter presets and intensity
+ * slider live in the sidebar via `InspectorControls` — a `PanelBody` only
+ * renders in the sidebar when wrapped in `InspectorControls`, so the
+ * block's edit component itself mounts the panel rather than relying on
+ * an `editor.BlockEdit` HOC.
*
* @package SoloFiltersImageEnhancements
*/
import { __ } from '@wordpress/i18n';
-import { useBlockProps, MediaPlaceholder, BlockControls } from '@wordpress/block-editor';
+import {
+ useBlockProps,
+ MediaPlaceholder,
+ BlockControls,
+ InspectorControls,
+} from '@wordpress/block-editor';
import { Button, ToolbarGroup, ToolbarItem } from '@wordpress/components';
import { useState } from '@wordpress/element';
+import { FilterPanel } from './inspector';
+
export default function Edit( { attributes, setAttributes } ) {
- const { filter, intensity, imageId, imageUrl, imageAlt, width, height } = attributes;
+ const { filter, intensity, imageUrl, imageAlt, width, height } = attributes;
const blockProps = useBlockProps( {
className: `wp-block-ksolo-image-filter has-filter-${ filter }`,
style: { '--filter-intensity': String( intensity / 100 ) },
@@ -23,58 +33,77 @@ export default function Edit( { attributes, setAttributes } ) {
const [ isEditing, setIsEditing ] = useState( ! imageUrl );
+ // The inspector panel is mounted alongside whatever the block renders
+ // in the canvas. It is available in both the empty-placeholder state
+ // and the rendered-image state, so users can pre-pick a filter before
+ // uploading.
+ const inspector = (
+
+
+
+ );
+
if ( isEditing || ! imageUrl ) {
return (
-
+ >
);
}
return (
-
-
-
-
- { () => (
-
- ) }
-
-
-
-
-
+ <>
+ { inspector }
+
+
+
+
+ { () => (
+
+ ) }
+
+
+
+
+
+ >
);
-}
+}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index 560866f..fea04a5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -14,7 +14,6 @@ import metadata from './block.json';
import edit from './edit';
import save from './save';
import { PRESETS, DEFAULT_PRESET, isValidPresetSlug } from './presets';
-import { FilterPanel } from './inspector';
import { transforms } from './transforms';
registerBlockType( metadata.name, {
@@ -24,32 +23,6 @@ registerBlockType( metadata.name, {
transforms,
} );
-/**
- * Add the filter panel to the Image block's inspector when the active
- * block is the Filtered Image block. We use the editor.BlockEdit filter
- * so we don't have to re-implement the entire MediaPlaceholder UX.
- *
- * @param {Function} BlockEdit
- * @return {Function}
- */
-function withFilterPanel( BlockEdit ) {
- return ( props ) => {
- if ( props.name !== 'ksolo/image-filter' ) {
- return ;
- }
- return (
- <>
-
-
- >
- );
- };
-}
-addFilter( 'editor.BlockEdit', 'solofilters-image-enhancements/with-filter-panel', withFilterPanel );
-
/**
* Normalise the filter attribute on save. If the saved slug is unknown
* (e.g. the post was edited by hand), fall back to DEFAULT_PRESET so the
@@ -76,4 +49,4 @@ addFilter(
// Re-export PRESETS so the consuming downstream code (e.g. a future
// design-tool integration) can grab them from this module.
-export { PRESETS };
+export { PRESETS };
\ No newline at end of file
diff --git a/tests/jest/__mocks__/@wordpress/block-editor.js b/tests/jest/__mocks__/@wordpress/block-editor.js
new file mode 100644
index 0000000..230b2a8
--- /dev/null
+++ b/tests/jest/__mocks__/@wordpress/block-editor.js
@@ -0,0 +1,39 @@
+/**
+ * Manual mock for @wordpress/block-editor.
+ *
+ * The package is provided by WordPress at runtime via the
+ * dependency-extraction-webpack-plugin and is not installed as a dev
+ * dependency. This mock lets the Jest environment resolve it.
+ *
+ * Slot components (`InspectorControls`, `BlockControls`) are rendered as
+ * lightweight `
` wrappers so tests can assert that
+ * `Edit` mounts them in the right place. Hooks (`useBlockProps`) are
+ * stubbed to return the supplied props unchanged, which lets the rest
+ * of the React tree render with stable className/style.
+ */
+import { createElement } from '@wordpress/element';
+
+const Slot = ( { name, children } ) =>
+ createElement( 'div', { 'data-slot': name }, children );
+
+function makeUseBlockProps( props = {} ) {
+ return {
+ className: props.className || '',
+ style: props.style || {},
+ };
+}
+
+const useBlockPropsFn = ( props ) => makeUseBlockProps( props );
+useBlockPropsFn.save = ( props ) => makeUseBlockProps( props );
+
+export const useBlockProps = useBlockPropsFn;
+export const InspectorControls = ( { children } ) =>
+ createElement( Slot, { name: 'inspector-controls' }, children );
+export const BlockControls = ( { children } ) =>
+ createElement( Slot, { name: 'block-controls' }, children );
+export const MediaPlaceholder = ( props ) =>
+ createElement(
+ 'div',
+ { 'data-mock': 'MediaPlaceholder' },
+ props.labels ? props.labels.title : null
+ );
diff --git a/tests/jest/edit.test.js b/tests/jest/edit.test.js
new file mode 100644
index 0000000..1918c41
--- /dev/null
+++ b/tests/jest/edit.test.js
@@ -0,0 +1,98 @@
+/**
+ * @jest-environment jsdom
+ *
+ * Tests for the Edit component. The core contract these tests pin down
+ * is that the filter controls are mounted in the sidebar via
+ * `InspectorControls` (not floating in the canvas). The block's `Edit`
+ * component is responsible for wrapping the `FilterPanel` in
+ * `InspectorControls` itself — see the comment block at the top of
+ * src/edit.js.
+ */
+// @wordpress/element, @wordpress/block-editor, @wordpress/components,
+// @wordpress/i18n are all stubbed via __mocks__/* and inline jest.mock
+// calls so the test environment has no WordPress globals.
+import { render, screen } from '@testing-library/react';
+import Edit from '../../src/edit';
+
+const defaultAttributes = {
+ filter: 'normal',
+ intensity: 100,
+ imageId: 0,
+ imageUrl: '',
+ imageAlt: '',
+ width: 100,
+ height: 100,
+ linkUrl: '',
+ caption: '',
+};
+
+const baseProps = {
+ attributes: defaultAttributes,
+ setAttributes: jest.fn(),
+ isSelected: true,
+ clientId: 'test-client-id',
+ context: {},
+ name: 'ksolo/image-filter',
+};
+
+describe( 'Edit (Filtered Image block)', () => {
+ test( 'mounts the filter controls inside InspectorControls (sidebar)', () => {
+ const { container } = render( );
+
+ // InspectorControls is rendered as
.
+ const sidebar = container.querySelector(
+ '[data-slot="inspector-controls"]'
+ );
+ expect( sidebar ).not.toBeNull();
+
+ // The PanelBody inside FilterPanel uses the title "Filter". Even
+ // in the empty/placeholder branch (no imageUrl), the inspector
+ // should be present so users can pick a preset before uploading.
+ expect(
+ sidebar.querySelector( '.ksolo-image-filter-panel' )
+ ).not.toBeNull();
+ } );
+
+ test( 'does NOT mount a floating FilterPanel outside InspectorControls', () => {
+ // Regression guard: earlier the `FilterPanel` was rendered as a
+ // bare `` (no InspectorControls wrapper) via an
+ // `editor.BlockEdit` HOC. That caused the controls to appear in
+ // the editor canvas rather than the sidebar. The new wiring
+ // guarantees the panel only appears inside InspectorControls.
+ const { container } = render( );
+ const allPanels = container.querySelectorAll(
+ '.ksolo-image-filter-panel'
+ );
+ expect( allPanels.length ).toBe( 1 );
+ expect(
+ allPanels[ 0 ].closest( '[data-slot="inspector-controls"]' )
+ ).not.toBeNull();
+ } );
+
+ test( 'shows the MediaPlaceholder in the canvas when no image is set', () => {
+ render( );
+ // The MediaPlaceholder mock renders a div with
+ // `data-mock="MediaPlaceholder"` containing the labels.title
+ // string ("Filtered Image").
+ const placeholder = screen.getByText( 'Filtered Image' );
+ expect( placeholder ).not.toBeNull();
+ } );
+
+ test( 'renders the in the canvas when an imageUrl is set', () => {
+ const props = {
+ ...baseProps,
+ attributes: {
+ ...defaultAttributes,
+ imageUrl: 'https://example.com/a.jpg',
+ imageAlt: 'A',
+ },
+ };
+ render( );
+ const img = screen.getByAltText( 'A' );
+ expect( img ).not.toBeNull();
+ // The className on the must include the preset class so the
+ // compiled CSS can target it.
+ expect( img.className ).toContain( 'ksolo-image-filter-img' );
+ expect( img.className ).toContain( 'has-filter-normal' );
+ } );
+} );