Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions src/blocks/author-profile-social/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,31 @@
"iconSize": {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker: Removing color.text / color.background supports without a deprecated registration is a silent-data-loss path for the ~30 days of posts (production since v6.37.0 / 2026-04-13) that have been saving textColor / backgroundColor / style.color.text / style.color.background under the previous schema. Because save: () => <InnerBlocks.Content /> returns no attribute markup, block validation won't fail loudly — the old keys are dropped from parsed attributes on first load, and on the frontend the new PHP resolve_color() never reads them, so the configured colours disappear silently and the data is gone on next resave. Verified in-env by saving a post with the old shape and loading the frontend: no --icon-color / --icon-background set despite the saved attrs.

Needs a deprecated entry registering the previous attribute shape with a migrate(attrs) mapping textColor → iconColor, style.color.text → iconColorValue, backgroundColor → iconBackgroundColor, style.color.background → iconBackgroundColorValue (and stripping the empty style.color).

"type": "number",
"default": 24
},
"iconColor": {
"type": "string"
},
"customIconColor": {
"type": "string"
},
"iconColorValue": {
"type": "string"
},
"iconBackgroundColor": {
"type": "string"
},
"customIconBackgroundColor": {
"type": "string"
},
"iconBackgroundColorValue": {
"type": "string"
}
},
"styles": [
{ "name": "default", "label": "Default", "isDefault": true },
{ "name": "brand", "label": "Brand" }
],
"supports": {
"color": {
"enableContrastChecker": false,
"background": true,
"text": true,
"link": false,
"__experimentalSkipSerialization": [ "text", "background" ]
},
"html": false,
"layout": {
"allowSwitching": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,16 +221,18 @@ private static function render_social_flat( array $attributes, WP_Block $block,

/**
* Get wrapper attributes (class, style, etc.) for the block.
* Sets block context so core includes default class, custom className, and other supports.
* Color serialization is skipped via block.json so colors are applied only as CSS vars.
*
* @param WP_Block $block Block instance.
* @param array $attributes Block attributes.
* @param int $icon_size Icon size in pixels.
* @return string HTML attributes for the wrapper element.
*/
private static function get_block_wrapper_attributes( WP_Block $block, array $attributes, int $icon_size ): string {
$previous = \WP_Block_Supports::$block_to_render ?? null;
// Inner blocks have already rendered by this point in the InnerBlocks
// path, leaving $block_to_render pointing at the last child — so the
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Good that the rationale is documented now. The actual reason for the snapshot is more defensive than "the inner blocks have already rendered": each inner WP_Block::render() snapshots/restores $block_to_render around its own callback, so this should already be back to the parent's parsed_block by the time get_block_wrapper_attributes() is called. The snapshot here guards against a third-party filter or future Core change breaking that chain. Worth rewording the comment to reflect "defensive against the inner-render chain leaking the static" rather than "already rendered".

// wrapper would otherwise be built from the wrong block's attributes
// and lose this block's className, spacing, default class, etc.
$previous = \WP_Block_Supports::$block_to_render ?? null;
\WP_Block_Supports::$block_to_render = $block->parsed_block;

$wrapper_attributes = get_block_wrapper_attributes(
Expand All @@ -246,33 +248,21 @@ private static function get_block_wrapper_attributes( WP_Block $block, array $at
}

/**
* Convert a preset token (var:preset|type|slug) to a CSS variable reference.
*
* @param string $value Raw value, e.g. "var:preset|color|primary" or "#fff".
* @return string CSS value, e.g. "var(--wp--preset--color--primary)" or "#fff".
*/
private static function preset_to_css( string $value ): string {
if ( preg_match( '/^var:preset\|([^|]+)\|(.+)$/', $value, $matches ) ) {
return sprintf( 'var(--wp--preset--%s--%s)', $matches[1], $matches[2] );
}
return $value;
}

/**
* Resolve a color value from attributes (preset slug or custom style token).
* Resolve a color value from the block's icon color attributes.
* Prefers the preset slug (so theme switches reflect new palette values)
* and falls back to the saved hex/CSS value when no preset is set.
*
* @param array $attributes Block attributes.
* @param string $preset_key Top-level preset attribute key (e.g. "textColor").
* @param string $style_key Key under style.color (e.g. "text").
* @param string $preset_key Preset slug attribute key (e.g. "iconColor").
* @param string $value_key Resolved CSS value attribute key (e.g. "iconColorValue").
* @return string|null CSS color value or null.
*/
private static function resolve_color( array $attributes, string $preset_key, string $style_key ): ?string {
private static function resolve_color( array $attributes, string $preset_key, string $value_key ): ?string {
if ( ! empty( $attributes[ $preset_key ] ) && is_string( $attributes[ $preset_key ] ) ) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: resolve_color() returns var(--wp--preset--color--<slug>) whenever the slug is set, even if the active theme doesn't define that slug — the resulting CSS variable is unresolved and the icon renders uncoloured on the frontend. The editor side (via withColors's getColorObjectByAttributeValues) falls back to iconColorValue in that case, so editor preview and frontend can diverge on theme switch.

Either make the fallback symmetric (prefer iconColorValue when present, regardless of slug), or drop the "theme switches reflect new palette values" claim from the docblock.

return sprintf( 'var(--wp--preset--color--%s)', $attributes[ $preset_key ] );
}
$custom = $attributes['style']['color'][ $style_key ] ?? null;
if ( ! empty( $custom ) && is_string( $custom ) ) {
return self::preset_to_css( $custom );
if ( ! empty( $attributes[ $value_key ] ) && is_string( $attributes[ $value_key ] ) ) {
return $attributes[ $value_key ];
}
return null;
}
Expand All @@ -281,7 +271,6 @@ private static function resolve_color( array $attributes, string $preset_key, st
* Build wrapper inline style with CSS variables for icon sizing and color.
* Margin is handled natively by get_block_wrapper_attributes().
* Gap is handled by WP layout support (outputs scoped <style> tag per block).
* Color classes/inline styles are skipped via __experimentalSkipSerialization in block.json.
*
* @param array $attributes Block attributes.
* @param int $icon_size Icon size in pixels.
Expand All @@ -292,8 +281,8 @@ private static function get_wrapper_style( array $attributes, int $icon_size ):
$is_brand = ! empty( $attributes['className'] ) && str_contains( $attributes['className'], 'is-style-brand' );

if ( ! $is_brand ) {
$icon_color = self::resolve_color( $attributes, 'textColor', 'text' );
$icon_background = self::resolve_color( $attributes, 'backgroundColor', 'background' );
$icon_color = self::resolve_color( $attributes, 'iconColor', 'iconColorValue' );
$icon_background = self::resolve_color( $attributes, 'iconBackgroundColor', 'iconBackgroundColorValue' );
Comment thread
laurelfulford marked this conversation as resolved.

if ( null !== $icon_color ) {
$parts[] = sprintf( '--icon-color: %s;', $icon_color );
Expand Down
128 changes: 60 additions & 68 deletions src/blocks/author-profile-social/edit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@
* WordPress dependencies
*/
import { useContext, useEffect, useMemo, useRef, useState } from '@wordpress/element';
import { BlockControls, useBlockProps, useInnerBlocksProps, InspectorControls } from '@wordpress/block-editor';
import {
BlockControls,
useBlockProps,
useInnerBlocksProps,
InspectorControls,
withColors,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients,
} from '@wordpress/block-editor';
import { PanelBody, SelectControl, Button, ToolbarButton, ToolbarGroup, Tooltip } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
Expand All @@ -28,91 +38,63 @@ const fetchAllServiceKeys = () => {
return allServiceKeysCache;
};

const presetToVar = value => {
if ( typeof value !== 'string' ) {
return value;
}
return value.replace( /^var:preset\|([^|]+)\|(.+)$/, 'var(--wp--preset--$1--$2)' );
};

const resolveColor = ( presetSlug, customValue ) => {
if ( presetSlug ) {
return `var(--wp--preset--color--${ presetSlug })`;
}
if ( typeof customValue === 'string' ) {
return presetToVar( customValue ) || customValue;
}
return undefined;
};

/**
* Edit component for the Author Social Links inner block.
*
* @param {Object} props Block props.
* @param {Object} props.attributes Block attributes.
* @param {Function} props.setAttributes Function to update attributes.
* @param {string} props.clientId Block client ID.
* @param {Object} props Block props.
* @param {Object} props.attributes Block attributes.
* @param {Function} props.setAttributes Function to update attributes.
* @param {string} props.clientId Block client ID.
* @param {Object} props.iconColor Resolved icon color (from withColors).
* @param {Function} props.setIconColor Setter for icon color (from withColors).
* @param {Object} props.iconBackgroundColor Resolved icon background color (from withColors).
* @param {Function} props.setIconBackgroundColor Setter for icon background color (from withColors).
* @return {JSX.Element} The edit component.
*/
export default function Edit( { attributes, setAttributes, clientId } ) {
function Edit( { attributes, setAttributes, clientId, iconColor, setIconColor, iconBackgroundColor, setIconBackgroundColor } ) {
const AuthorContext = getSharedAuthorContext();
const author = useContext( AuthorContext );
const { iconSize, style: styleAttr, textColor, backgroundColor, className } = attributes;
const { iconSize, iconColorValue, iconBackgroundColorValue, className } = attributes;
const hasPopulated = useRef( false );
const [ allServiceKeys, setAllServiceKeys ] = useState( null ); // null = loading

const isBrand = ( className || '' ).split( ' ' ).includes( 'is-style-brand' );
const iconSizeValue = typeof iconSize === 'number' ? iconSize : parseInt( iconSize ?? 24, 10 ) || 24;
const iconColor = ! isBrand ? resolveColor( textColor, styleAttr?.color?.text ) : undefined;
const iconBackground = ! isBrand ? resolveColor( backgroundColor, styleAttr?.color?.background ) : undefined;

// Hide color panel when "Brand" is active; rename labels when "Default".
useEffect( () => {
const sidebar = document.querySelector( '.interface-complementary-area' );
if ( ! sidebar ) {
return;
}

const COLOR_LABEL_MAP = {
Text: __( 'Icon color', 'newspack-plugin' ),
Background: __( 'Icon background', 'newspack-plugin' ),
};

const updateColorPanel = container => {
container.querySelectorAll( '.color-block-support-panel' ).forEach( el => {
el.style.display = isBrand ? 'none' : '';
} );

if ( isBrand ) {
return;
}

container.querySelectorAll( '.block-editor-panel-color-gradient-settings__color-name' ).forEach( el => {
if ( COLOR_LABEL_MAP[ el.textContent ] ) {
el.textContent = COLOR_LABEL_MAP[ el.textContent ];
}
} );
container.querySelectorAll( '.components-menu-item__item' ).forEach( el => {
if ( COLOR_LABEL_MAP[ el.textContent ] ) {
el.textContent = COLOR_LABEL_MAP[ el.textContent ];
}
} );
};

updateColorPanel( sidebar );

const observer = new MutationObserver( () => updateColorPanel( sidebar ) );
observer.observe( sidebar, { childList: true, subtree: true } );

return () => observer.disconnect();
}, [ isBrand ] );
const resolvedIconColor = ! isBrand ? iconColor?.color || iconColorValue : undefined;
const resolvedIconBackground = ! isBrand ? iconBackgroundColor?.color || iconBackgroundColorValue : undefined;
Comment thread
laurelfulford marked this conversation as resolved.

// Color panel is hidden entirely when the "Brand" style is active —
// brand uses each service's own colors, so neither icon nor background
// apply. The settings render as ToolsPanelItems directly inside the
// Styles tab's Color slot, so they integrate with WP's native panel
// (no nested panel-in-a-panel).
const colorGradientSettings = useMultipleOriginColorsAndGradients();
const colorSettings = [
{
colorValue: iconColor?.color || iconColorValue,
onColorChange: value => {
setIconColor( value );
setAttributes( { iconColorValue: value } );
},
label: __( 'Icon color', 'newspack-plugin' ),
},
{
colorValue: iconBackgroundColor?.color || iconBackgroundColorValue,
onColorChange: value => {
setIconBackgroundColor( value );
setAttributes( { iconBackgroundColorValue: value } );
},
label: __( 'Icon background', 'newspack-plugin' ),
},
];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Worth manually confirming that picking a theme preset colour writes to iconColor (the slug attribute), not just iconColorValue. ColorGradientSettingsDropdown's onColorChange passes a single value, and withColors' setter is responsible for resolving it back to a slug by iterating the palette — Core's core/social-links uses the same pattern, but if for any reason iconColor never gets populated, the docblock claim in class-author-profile-social-block.php ("Prefers the preset slug so theme switches reflect new palette values") doesn't hold and the frontend always emits a raw hex.


const blockProps = useBlockProps( {
className: 'author-profile-social__list',
style: {
'--icon-size': `${ roundIconSize( iconSizeValue ) }px`,
...( iconColor && { '--icon-color': iconColor } ),
...( iconBackground && { '--icon-background': iconBackground } ),
...( resolvedIconColor && { '--icon-color': resolvedIconColor } ),
...( resolvedIconBackground && { '--icon-background': resolvedIconBackground } ),
},
} );

Expand Down Expand Up @@ -201,7 +183,17 @@ export default function Edit( { attributes, setAttributes, clientId } ) {
) }
</PanelBody>
</InspectorControls>
{ ! isBrand && (
<InspectorControls group="color">
<ColorGradientSettingsDropdown settings={ colorSettings } panelId={ clientId } { ...colorGradientSettings } />
</InspectorControls>
) }
<ul { ...innerBlocksProps } />
</>
);
}

export default withColors( {
iconColor: 'icon-color',
iconBackgroundColor: 'icon-background-color',
} )( Edit );
Loading