diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js
index 163846d52e415a..27866f580d7373 100644
--- a/packages/block-editor/src/hooks/duotone.js
+++ b/packages/block-editor/src/hooks/duotone.js
@@ -60,82 +60,109 @@ export function getValuesFromColors( colors = [] ) {
*/
/**
- * SVG and stylesheet needed for rendering the duotone filter.
+ * Stylesheet for rendering the duotone filter.
*
* @param {Object} props Duotone props.
* @param {string} props.selector Selector to apply the filter to.
* @param {string} props.id Unique id for this duotone filter.
- * @param {Values} props.values R, G, B, and A values to filter with.
*
* @return {WPElement} Duotone element.
*/
-function DuotoneFilter( { selector, id, values } ) {
- const stylesheet = `
+function DuotoneStylesheet( { selector, id } ) {
+ const css = `
${ selector } {
filter: url( #${ id } );
}
`;
+ return ;
+}
+/**
+ * SVG for rendering the duotone filter.
+ *
+ * @param {Object} props Duotone props.
+ * @param {string} props.id Unique id for this duotone filter.
+ * @param {Values} props.values R, G, B, and A values to filter with.
+ *
+ * @return {WPElement} Duotone element.
+ */
+function DuotoneFilter( { id, values } ) {
return (
- <>
-
-
+
+
+
+
+
+ );
+}
+
+/**
+ * SVG and stylesheet needed for rendering the duotone filter.
+ *
+ * @param {Object} props Duotone props.
+ * @param {string} props.selector Selector to apply the filter to.
+ * @param {string} props.id Unique id for this duotone filter.
+ * @param {Values} props.values R, G, B, and A values to filter with.
+ *
+ * @return {WPElement} Duotone element.
+ */
+function InlineDuotone( { selector, id, values } ) {
+ return (
+ <>
+
+
>
);
}
@@ -321,7 +348,7 @@ const withDuotoneStyles = createHigherOrderComponent(
<>
{ element &&
createPortal(
-
+ );
+}
+
addFilter(
'blocks.registerBlockType',
'core/editor/duotone/add-attributes',
diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js
index 708dae62a39714..3b25875878a56e 100644
--- a/packages/block-editor/src/hooks/index.js
+++ b/packages/block-editor/src/hooks/index.js
@@ -19,3 +19,4 @@ export { getBorderClassesAndStyles, useBorderProps } from './use-border-props';
export { getColorClassesAndStyles, useColorProps } from './use-color-props';
export { getSpacingClassesAndStyles } from './use-spacing-props';
export { useCachedTruthy } from './use-cached-truthy';
+export { PresetDuotoneFilter } from './duotone';
diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js
index 0672b0c0fbfddf..078895c2c5a02d 100644
--- a/packages/block-editor/src/index.js
+++ b/packages/block-editor/src/index.js
@@ -3,6 +3,7 @@
*/
import './hooks';
export {
+ PresetDuotoneFilter as __unstablePresetDuotoneFilter,
getBorderClassesAndStyles as __experimentalGetBorderClassesAndStyles,
useBorderProps as __experimentalUseBorderProps,
getColorClassesAndStyles as __experimentalGetColorClassesAndStyles,
diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js
index c9b8d3e0e8f1d3..d8d02d7567862a 100644
--- a/packages/blocks/src/api/constants.js
+++ b/packages/blocks/src/api/constants.js
@@ -56,6 +56,10 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = {
support: [ 'color', 'text' ],
requiresOptOut: true,
},
+ filter: {
+ value: [ 'filter', 'duotone' ],
+ support: [ 'color', '__experimentalDuotone' ],
+ },
linkColor: {
value: [ 'elements', 'link', 'color', 'text' ],
support: [ 'color', 'link' ],
diff --git a/packages/edit-site/src/components/block-editor/resizable-editor.js b/packages/edit-site/src/components/block-editor/resizable-editor.js
index 91dee707ccd757..ce9e0d1bdec6b1 100644
--- a/packages/edit-site/src/components/block-editor/resizable-editor.js
+++ b/packages/edit-site/src/components/block-editor/resizable-editor.js
@@ -36,7 +36,7 @@ const HANDLE_STYLES_OVERRIDE = {
left: undefined,
};
-function ResizableEditor( { enableResizing, settings, ...props } ) {
+function ResizableEditor( { enableResizing, settings, children, ...props } ) {
const deviceType = useSelect(
( select ) =>
select( editSiteStore ).__experimentalGetPreviewDeviceType(),
@@ -182,7 +182,11 @@ function ResizableEditor( { enableResizing, settings, ...props } ) {
name="editor-canvas"
className="edit-site-visual-editor__editor-canvas"
{ ...props }
- />
+ >
+ { /* Filters need to be rendered before children to avoid Safari rendering issues. */ }
+ { settings.svgFilters }
+ { children }
+
);
}
diff --git a/packages/edit-site/src/components/editor/global-styles-renderer.js b/packages/edit-site/src/components/editor/global-styles-renderer.js
index 3abcb1813fd572..9af00a7da6775a 100644
--- a/packages/edit-site/src/components/editor/global-styles-renderer.js
+++ b/packages/edit-site/src/components/editor/global-styles-renderer.js
@@ -20,7 +20,7 @@ import { store as editSiteStore } from '../../store';
import { useGlobalStylesOutput } from '../global-styles/use-global-styles-output';
function useGlobalStylesRenderer() {
- const [ styles, settings ] = useGlobalStylesOutput();
+ const [ styles, settings, svgFilters ] = useGlobalStylesOutput();
const { getSettings } = useSelect( editSiteStore );
const { updateSettings } = useDispatch( editSiteStore );
@@ -37,6 +37,7 @@ function useGlobalStylesRenderer() {
updateSettings( {
...currentStoreSettings,
styles: [ ...nonGlobalStyles, ...styles ],
+ svgFilters,
__experimentalFeatures: settings,
} );
}, [ styles, settings ] );
diff --git a/packages/edit-site/src/components/global-styles/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/use-global-styles-output.js
index 72ed5a5afe32e4..9584b8b2f77b30 100644
--- a/packages/edit-site/src/components/global-styles/use-global-styles-output.js
+++ b/packages/edit-site/src/components/global-styles/use-global-styles-output.js
@@ -24,6 +24,7 @@ import {
} from '@wordpress/blocks';
import { useEffect, useState, useContext } from '@wordpress/element';
import { getCSSRules } from '@wordpress/style-engine';
+import { __unstablePresetDuotoneFilter as PresetDuotoneFilter } from '@wordpress/block-editor';
/**
* Internal dependencies
@@ -59,16 +60,27 @@ function compileStyleValue( uncompiledValue ) {
function getPresetsDeclarations( blockPresets = {} ) {
return reduce(
PRESET_METADATA,
- ( declarations, { path, valueKey, cssVarInfix } ) => {
+ ( declarations, { path, valueKey, valueFunc, cssVarInfix } ) => {
const presetByOrigin = get( blockPresets, path, [] );
[ 'default', 'theme', 'custom' ].forEach( ( origin ) => {
if ( presetByOrigin[ origin ] ) {
presetByOrigin[ origin ].forEach( ( value ) => {
- declarations.push(
- `--wp--preset--${ cssVarInfix }--${ kebabCase(
- value.slug
- ) }: ${ value[ valueKey ] }`
- );
+ if ( valueKey ) {
+ declarations.push(
+ `--wp--preset--${ cssVarInfix }--${ kebabCase(
+ value.slug
+ ) }: ${ value[ valueKey ] }`
+ );
+ } else if (
+ valueFunc &&
+ typeof valueFunc === 'function'
+ ) {
+ declarations.push(
+ `--wp--preset--${ cssVarInfix }--${ kebabCase(
+ value.slug
+ ) }: ${ valueFunc( value ) }`
+ );
+ }
} );
}
} );
@@ -123,6 +135,25 @@ function getPresetsClasses( blockSelector, blockPresets = {} ) {
);
}
+function getPresetsSvgFilters( blockPresets = {} ) {
+ return PRESET_METADATA.filter(
+ // Duotone are the only type of filters for now.
+ ( metadata ) => metadata.path.at( -1 ) === 'duotone'
+ ).flatMap( ( metadata ) => {
+ const presetByOrigin = get( blockPresets, metadata.path, {} );
+ return [ 'default', 'theme' ]
+ .filter( ( origin ) => presetByOrigin[ origin ] )
+ .flatMap( ( origin ) =>
+ presetByOrigin[ origin ].map( ( preset ) => (
+
+ ) )
+ );
+ } );
+}
+
function flattenTree( input = {}, prefix, token ) {
let result = [];
Object.keys( input ).forEach( ( key ) => {
@@ -215,7 +246,9 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => {
const pickStyleKeys = ( treeToPickFrom ) =>
pickBy( treeToPickFrom, ( value, key ) =>
- [ 'border', 'color', 'spacing', 'typography' ].includes( key )
+ [ 'border', 'color', 'spacing', 'typography', 'filter' ].includes(
+ key
+ )
);
// Top-level.
@@ -242,6 +275,7 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => {
nodes.push( {
styles: blockStyles,
selector: blockSelectors[ blockName ].selector,
+ duotoneSelector: blockSelectors[ blockName ].duotoneSelector,
} );
}
@@ -333,9 +367,34 @@ export const toStyles = ( tree, blockSelectors ) => {
const nodesWithStyles = getNodesWithStyles( tree, blockSelectors );
const nodesWithSettings = getNodesWithSettings( tree, blockSelectors );
- let ruleset =
- '.wp-site-blocks > * { margin-top: 0; margin-bottom: 0; }.wp-site-blocks > * + * { margin-top: var( --wp--style--block-gap ); }';
- nodesWithStyles.forEach( ( { selector, styles } ) => {
+ /*
+ * Reset default browser margin on the root body element.
+ * This is set on the root selector **before** generating the ruleset
+ * from the `theme.json`. This is to ensure that if the `theme.json` declares
+ * `margin` in its `spacing` declaration for the `body` element then these
+ * user-generated values take precedence in the CSS cascade.
+ * @link https://github.com/WordPress/gutenberg/issues/36147.
+ */
+ let ruleset = 'body {margin: 0;}';
+ nodesWithStyles.forEach( ( { selector, duotoneSelector, styles } ) => {
+ const duotoneStyles = {};
+ if ( styles?.filter ) {
+ duotoneStyles.filter = styles.filter;
+ delete styles.filter;
+ }
+
+ // Process duotone styles (they use color.__experimentalDuotone selector).
+ if ( duotoneSelector ) {
+ const duotoneDeclarations = getStylesDeclarations( duotoneStyles );
+ if ( duotoneDeclarations.length === 0 ) {
+ return;
+ }
+ ruleset =
+ ruleset +
+ `${ duotoneSelector }{${ duotoneDeclarations.join( ';' ) };}`;
+ }
+
+ // Process the remaning block styles (they use either normal block class or __experimentalSelector).
const declarations = getStylesDeclarations( styles );
if ( declarations.length === 0 ) {
return;
@@ -358,6 +417,13 @@ export const toStyles = ( tree, blockSelectors ) => {
return ruleset;
};
+export function toSvgFilters( tree, blockSelectors ) {
+ const nodesWithSettings = getNodesWithSettings( tree, blockSelectors );
+ return nodesWithSettings.flatMap( ( { presets } ) => {
+ return getPresetsSvgFilters( presets );
+ } );
+}
+
const getBlockSelectors = ( blockTypes ) => {
const result = {};
blockTypes.forEach( ( blockType ) => {
@@ -365,9 +431,12 @@ const getBlockSelectors = ( blockTypes ) => {
const selector =
blockType?.supports?.__experimentalSelector ??
'.wp-block-' + name.replace( 'core/', '' ).replace( '/', '-' );
+ const duotoneSelector =
+ blockType?.supports?.color?.__experimentalDuotone ?? null;
result[ name ] = {
name,
selector,
+ duotoneSelector,
};
} );
@@ -377,6 +446,7 @@ const getBlockSelectors = ( blockTypes ) => {
export function useGlobalStylesOutput() {
const [ stylesheets, setStylesheets ] = useState( [] );
const [ settings, setSettings ] = useState( {} );
+ const [ svgFilters, setSvgFilters ] = useState( {} );
const { merged: mergedConfig } = useContext( GlobalStylesContext );
useEffect( () => {
@@ -390,6 +460,7 @@ export function useGlobalStylesOutput() {
blockSelectors
);
const globalStyles = toStyles( mergedConfig, blockSelectors );
+ const filters = toSvgFilters( mergedConfig, blockSelectors );
setStylesheets( [
{
css: customProperties,
@@ -401,7 +472,8 @@ export function useGlobalStylesOutput() {
},
] );
setSettings( mergedConfig.settings );
+ setSvgFilters( filters );
}, [ mergedConfig ] );
- return [ stylesheets, settings ];
+ return [ stylesheets, settings, svgFilters ];
}
diff --git a/packages/edit-site/src/components/global-styles/utils.js b/packages/edit-site/src/components/global-styles/utils.js
index 8f815fd3e3daae..50c412fc06cd59 100644
--- a/packages/edit-site/src/components/global-styles/utils.js
+++ b/packages/edit-site/src/components/global-styles/utils.js
@@ -49,6 +49,12 @@ export const PRESET_METADATA = [
},
],
},
+ {
+ path: [ 'color', 'duotone' ],
+ cssVarInfix: 'duotone',
+ valueFunc: ( { slug } ) => `url( '#wp-duotone-${ slug }' )`,
+ classes: [],
+ },
{
path: [ 'typography', 'fontSizes' ],
valueKey: 'size',