/** * WordPress dependencies */ import { getBlockType } from '@wordpress/blocks'; // @ts-expect-error: Not typed yet. import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { useContext, useMemo, useState } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { PanelBody, __experimentalVStack as VStack, __experimentalHasSplitBorders as hasSplitBorders, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { setStyle as setStyleHelper, setSetting as setSettingHelper, } from '@wordpress/global-styles-engine'; import type { GlobalStylesConfig } from '@wordpress/global-styles-engine'; /** * Internal dependencies */ import { ScreenHeader } from './screen-header'; import BlockPreviewPanel from './block-preview-panel'; import { Subtitle } from './subtitle'; import { useBlockVariations, VariationsPanel, } from './variations/variations-panel'; import { useStyle, useSetting } from './hooks'; import { GlobalStylesContext } from './context'; import { unlock } from './lock-unlock'; import { getValidPseudoStates, getValidViewportStates } from './utils'; // Initial control values. const BACKGROUND_BLOCK_DEFAULT_VALUES = { backgroundSize: 'cover', backgroundPosition: '50% 50%', // used only when backgroundSize is 'contain'. }; function applyFallbackStyle( border: any ) { if ( ! border ) { return border; } const hasColorOrWidth = border.color || border.width; if ( ! border.style && hasColorOrWidth ) { return { ...border, style: 'solid' }; } if ( border.style && ! hasColorOrWidth ) { return undefined; } return border; } function applyAllFallbackStyles( border: any ) { if ( ! border ) { return border; } if ( hasSplitBorders( border ) ) { return { top: applyFallbackStyle( border.top ), right: applyFallbackStyle( border.right ), bottom: applyFallbackStyle( border.bottom ), left: applyFallbackStyle( border.left ), }; } return applyFallbackStyle( border ); } const { useHasDimensionsPanel, useHasTypographyPanel, useHasBorderPanel, useSettingsForBlockElement, useHasColorPanel, useHasFiltersPanel, useHasImageSettingsPanel, useHasBackgroundPanel, BackgroundPanel: StylesBackgroundPanel, BorderPanel: StylesBorderPanel, ColorPanel: StylesColorPanel, TypographyPanel: StylesTypographyPanel, DimensionsPanel: StylesDimensionsPanel, FiltersPanel: StylesFiltersPanel, ImageSettingsPanel, AdvancedPanel: StylesAdvancedPanel, } = unlock( blockEditorPrivateApis ); interface ScreenBlockProps { name: string; variation?: string; } function ScreenBlock( { name, variation }: ScreenBlockProps ) { const { user: userConfig, onChange: onChangeGlobalStyles } = useContext( GlobalStylesContext ); let prefixParts: string[] = []; if ( variation ) { prefixParts = [ 'variations', variation ].concat( prefixParts ); } const prefix = prefixParts.join( '.' ); // State selector state const [ selectedViewport, setSelectedViewport ] = useState< string >( 'default' ); const [ selectedPseudoState, setSelectedPseudoState ] = useState< string >( 'default' ); const validViewportStates = useMemo( () => getValidViewportStates(), [] ); const validPseudoStates = useMemo( () => getValidPseudoStates( name ), [ name ] ); const stateParam = [ selectedViewport, selectedPseudoState ] .filter( ( value ) => value !== 'default' ) .join( '.' ); const hasSelectedState = stateParam.length > 0; const [ style, setStyle ] = useStyle( prefix, name, 'user', false, hasSelectedState ? stateParam : undefined ); const [ inheritedStyle ] = useStyle( prefix, name, 'merged', false, hasSelectedState ? stateParam : undefined ); const [ userSettings ] = useSetting( '', name, 'user' ); const [ rawSettings, setSettings ] = useSetting( '', name ); const settingsForBlockElement = useSettingsForBlockElement( rawSettings, name ); const blockType = getBlockType( name ); // Only allow `blockGap` support if serialization has not been skipped, to be sure global spacing can be applied. let disableBlockGap = false; if ( settingsForBlockElement?.spacing?.blockGap && blockType?.supports?.spacing?.blockGap && ( blockType?.supports?.spacing?.__experimentalSkipSerialization === true || blockType?.supports?.spacing?.__experimentalSkipSerialization?.some?.( ( spacingType: string ) => spacingType === 'blockGap' ) ) ) { disableBlockGap = true; } // Only allow `aspectRatio` support if the block is not the grouping block. // The grouping block allows the user to use Group, Row and Stack variations, // and it is highly likely that the user will not want to set an aspect ratio // for all three at once. Until there is the ability to set a different aspect // ratio for each variation, we disable the aspect ratio controls for the // grouping block in global styles. let disableAspectRatio = false; if ( settingsForBlockElement?.dimensions?.aspectRatio && name === 'core/group' ) { disableAspectRatio = true; } const settings = useMemo( () => { const updatedSettings = structuredClone( settingsForBlockElement ); if ( disableBlockGap ) { updatedSettings.spacing.blockGap = false; } if ( disableAspectRatio ) { updatedSettings.dimensions.aspectRatio = false; } return updatedSettings; }, [ settingsForBlockElement, disableBlockGap, disableAspectRatio ] ); const blockVariations = useBlockVariations( name ); const hasBackgroundPanel = useHasBackgroundPanel( settings ); const hasTypographyPanel = useHasTypographyPanel( settings ); const hasColorPanel = useHasColorPanel( settings ); const hasBorderPanel = useHasBorderPanel( settings ); const hasDimensionsPanel = useHasDimensionsPanel( settings ); const hasFiltersPanel = useHasFiltersPanel( settings ); const shouldShowFiltersPanel = hasFiltersPanel && selectedViewport === 'default'; const hasImageSettingsPanel = useHasImageSettingsPanel( name, userSettings, settings ); const hasVariationsPanel = !! blockVariations?.length && ! variation; const { canEditCSS } = useSelect( ( select ) => { const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } = select( coreStore ); const globalStylesId = __experimentalGetCurrentGlobalStylesId(); const globalStyles = globalStylesId ? getEntityRecord( 'root', 'globalStyles', globalStylesId ) : undefined; return { canEditCSS: !! ( globalStyles as GlobalStylesConfig )?._links?.[ 'wp:action-edit-css' ], }; }, [] ); const currentBlockStyle = variation ? blockVariations.find( ( s: any ) => s.name === variation ) : null; // These intermediary objects are needed because the "layout" property is stored // in settings rather than styles. const inheritedStyleWithLayout = useMemo( () => { return { ...inheritedStyle, layout: settings.layout, }; }, [ inheritedStyle, settings.layout ] ); const styleWithLayout = useMemo( () => { return { ...style, layout: userSettings.layout, }; }, [ style, userSettings.layout ] ); const onChangeDimensions = ( newStyle: any ) => { const updatedStyle = { ...newStyle }; delete updatedStyle.layout; setStyle( updatedStyle ); if ( newStyle.layout !== userSettings.layout ) { setSettings( { ...userSettings, layout: newStyle.layout, } ); } }; const onChangeLightbox = ( newSetting: any ) => { // If the newSetting is undefined, this means that the user has deselected // (reset) the lightbox setting. if ( newSetting === undefined ) { setSettings( { ...rawSettings, lightbox: undefined, } ); // Otherwise, we simply set the lightbox setting to the new value but // taking care of not overriding the other lightbox settings. } else { setSettings( { ...rawSettings, lightbox: { ...rawSettings.lightbox, ...newSetting, }, } ); } }; const onChangeTypography = ( newStyle: any ) => { // Extract settings if present (e.g., from textIndent toggle) const { settings: newSettings, ...styleWithoutSettings } = newStyle; // If there are settings changes, we need to update both styles and // settings atomically to avoid race conditions. if ( newSettings?.typography ) { // Build the state-aware path so that viewport styles (e.g. @mobile) // are written to the correct sub-path and do not overwrite the default. const stylePathForState = [ prefix, stateParam ] .filter( Boolean ) .join( '.' ); let updatedConfig = setStyleHelper( userConfig, stylePathForState, styleWithoutSettings, name ); updatedConfig = setSettingHelper( updatedConfig, 'typography', { ...userSettings.typography, ...newSettings.typography, }, name ); onChangeGlobalStyles( updatedConfig ); } else { setStyle( styleWithoutSettings ); } }; const onChangeBorders = ( newStyle: any ) => { if ( ! newStyle?.border ) { setStyle( newStyle ); return; } // As Global Styles can't conditionally generate styles based on if // other style properties have been set, we need to force split // border definitions for user set global border styles. Border // radius is derived from the same property i.e. `border.radius` if // it is a string that is used. The longhand border radii styles are // only generated if that property is an object. // // For borders (color, style, and width) those are all properties on // the `border` style property. This means if the theme.json defined // split borders and the user condenses them into a flat border or // vice-versa we'd get both sets of styles which would conflict. const { radius, ...newBorder } = newStyle.border; const border = applyAllFallbackStyles( newBorder ); const updatedBorder = ! hasSplitBorders( border ) ? { top: border, right: border, bottom: border, left: border, } : { color: null, style: null, width: null, ...border, }; setStyle( { ...newStyle, border: { ...updatedBorder, radius } } ); }; return ( <> { hasVariationsPanel && (
{ __( 'Style Variations' ) }
) } { hasBackgroundPanel && ( ) } { hasTypographyPanel && ( ) } { hasDimensionsPanel && ( ) } { hasBorderPanel && ( ) } { shouldShowFiltersPanel && ( ) } { hasColorPanel && ( ) } { hasImageSettingsPanel && ! hasSelectedState && ( ) } { canEditCSS && ( ) } ); } export default ScreenBlock;