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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@adekbadek I previously poo-poo'd this when Copilot suggested it because this version of the Author Profile block (and this issue) won't show up unless you're using a block theme. It is possible it's used with either our block theme or another theme, but the odds seem very low.

I think we should be safe without a fallback, but I'm willing to talk it out further! I might be being too laissez-faire about it 😅

"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,21 @@ 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;
// Defensive: each inner WP_Block::render() snapshots/restores
// $block_to_render around its own callback, so by this point it
// should already point at the parent's parsed_block. Re-set
// explicitly to guard against a third-party filter or future Core
// change breaking that chain — otherwise the wrapper would be built
// from the wrong block's supports 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 +251,30 @@ 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.
* When both a preset slug and a resolved value are present, emits the
* CSS variable with the value as a native fallback — so theme switches
* that redefine the slug pick up the new palette value, and theme
* switches that drop the slug fall back to the saved hex instead of
* rendering uncoloured.
*
* @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 {
if ( ! empty( $attributes[ $preset_key ] ) && is_string( $attributes[ $preset_key ] ) ) {
return sprintf( 'var(--wp--preset--color--%s)', $attributes[ $preset_key ] );
private static function resolve_color( array $attributes, string $preset_key, string $value_key ): ?string {
$slug = ! empty( $attributes[ $preset_key ] ) && is_string( $attributes[ $preset_key ] ) ? $attributes[ $preset_key ] : null;
$value = ! empty( $attributes[ $value_key ] ) && is_string( $attributes[ $value_key ] ) ? $attributes[ $value_key ] : null;

if ( $slug && $value ) {
return sprintf( 'var(--wp--preset--color--%s, %s)', $slug, $value );
}
if ( $slug ) {
return sprintf( 'var(--wp--preset--color--%s)', $slug );
}
$custom = $attributes['style']['color'][ $style_key ] ?? null;
if ( ! empty( $custom ) && is_string( $custom ) ) {
return self::preset_to_css( $custom );
if ( $value ) {
return $value;
}
return null;
}
Expand All @@ -281,7 +283,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 +293,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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call! Looks like it does -- I got both base and accent when using random colours from the theme's baked in palette:

 <!-- wp:newspack/author-profile-social {"iconColor":"base","iconColorValue":"#ffffff","iconBackgroundColor":"accent","iconBackgroundColorValue":"#003DA5","className":"is-style-default","style":{"spacing":{"padding":{"top":"var:preset|spacing|20"}}}}    
  -->       


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