fix: mount filter controls in sidebar via InspectorControls
The FilterPanel was being mounted as a free-floating sibling of the block via an `editor.BlockEdit` HOC. In WordPress, a `PanelBody` only renders in the sidebar when wrapped in `<InspectorControls>`; without that wrapper, the panel renders wherever it lands in the React tree, which here was the editor canvas. Move the InspectorControls mount into the Edit component itself (the idiomatic place for an own block to expose sidebar controls), and drop the `editor.BlockEdit` filter from `src/index.js`. Add an Edit test that pins the contract: the filter panel must mount inside InspectorControls and never float outside it. Add a manual jest mock for @wordpress/block-editor so the Edit test can render in an environment without WordPress globals.
This commit is contained in:
+81
-52
@@ -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 = (
|
||||
<InspectorControls>
|
||||
<FilterPanel
|
||||
attributes={ attributes }
|
||||
setAttributes={ setAttributes }
|
||||
/>
|
||||
</InspectorControls>
|
||||
);
|
||||
|
||||
if ( isEditing || ! imageUrl ) {
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<MediaPlaceholder
|
||||
onSelect={ ( media ) => {
|
||||
setAttributes( {
|
||||
imageId: media.id,
|
||||
imageUrl: media.url,
|
||||
imageAlt: media.alt || '',
|
||||
width: media.width,
|
||||
height: media.height,
|
||||
} );
|
||||
setIsEditing( false );
|
||||
} }
|
||||
allowedTypes={ [ 'image' ] }
|
||||
multiple={ false }
|
||||
labels={ {
|
||||
title: __( 'Filtered Image', 'solofilters-image-enhancements' ),
|
||||
instructions: __(
|
||||
'Upload or select an image to apply a filter.',
|
||||
'solofilters-image-enhancements'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
{ inspector }
|
||||
<div { ...blockProps }>
|
||||
<MediaPlaceholder
|
||||
onSelect={ ( media ) => {
|
||||
setAttributes( {
|
||||
imageId: media.id,
|
||||
imageUrl: media.url,
|
||||
imageAlt: media.alt || '',
|
||||
width: media.width,
|
||||
height: media.height,
|
||||
} );
|
||||
setIsEditing( false );
|
||||
} }
|
||||
allowedTypes={ [ 'image' ] }
|
||||
multiple={ false }
|
||||
labels={ {
|
||||
title: __( 'Filtered Image', 'solofilters-image-enhancements' ),
|
||||
instructions: __(
|
||||
'Upload or select an image to apply a filter.',
|
||||
'solofilters-image-enhancements'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<figure { ...blockProps }>
|
||||
<BlockControls>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>
|
||||
{ () => (
|
||||
<Button
|
||||
onClick={ () => setIsEditing( true ) }
|
||||
variant="secondary"
|
||||
label={ __( 'Replace image', 'solofilters-image-enhancements' ) }
|
||||
>
|
||||
{ __( 'Replace', 'solofilters-image-enhancements' ) }
|
||||
</Button>
|
||||
) }
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
</BlockControls>
|
||||
<img
|
||||
src={ imageUrl }
|
||||
alt={ imageAlt }
|
||||
width={ width }
|
||||
height={ height }
|
||||
className={ `ksolo-image-filter-img has-filter-${ filter }` }
|
||||
/>
|
||||
</figure>
|
||||
<>
|
||||
{ inspector }
|
||||
<figure { ...blockProps }>
|
||||
<BlockControls>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem>
|
||||
{ () => (
|
||||
<Button
|
||||
onClick={ () => setIsEditing( true ) }
|
||||
variant="secondary"
|
||||
label={ __( 'Replace image', 'solofilters-image-enhancements' ) }
|
||||
>
|
||||
{ __( 'Replace', 'solofilters-image-enhancements' ) }
|
||||
</Button>
|
||||
) }
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
</BlockControls>
|
||||
<img
|
||||
src={ imageUrl }
|
||||
alt={ imageAlt }
|
||||
width={ width }
|
||||
height={ height }
|
||||
className={ `ksolo-image-filter-img has-filter-${ filter }` }
|
||||
/>
|
||||
</figure>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-28
@@ -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 <BlockEdit { ...props } />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<BlockEdit { ...props } />
|
||||
<FilterPanel
|
||||
attributes={ props.attributes }
|
||||
setAttributes={ props.setAttributes }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
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 };
|
||||
Reference in New Issue
Block a user