Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shadow: Add UI tools to set custom shadow to a block #45507

Draft
wants to merge 1 commit into
base: trunk
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions packages/block-library/src/button/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"text": true
}
},
"shadow": true,
"typography": {
"fontSize": true,
"lineHeight": true,
Expand Down
71 changes: 71 additions & 0 deletions packages/edit-site/src/components/global-styles/block-preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* WordPress dependencies
*/
import {
__unstableIframe as Iframe,
__unstableEditorStyles as EditorStyles,
} from '@wordpress/block-editor';
import { useMemo } from '@wordpress/element';

/**
* Internal dependencies
*/
import { useGlobalStylesOutput } from './use-global-styles-output';
import { useStyle } from './hooks';

// const normalizedWidth = 248;
const normalizedHeight = 152;


// // NOTE: This is a very early prototype component to show preview of a block
// it is created for the purpose of previewing box-shadow
// and will eventually be replaced when https://github.com/WordPress/gutenberg/issues/42919 is complete
const BlockPreview = ( { label, name } ) => {
const [ styles ] = useGlobalStylesOutput();
const editorStyles = useMemo( () => {
if ( styles ) {
return [
...styles,
{
css: 'html{overflow:hidden}body{min-width: 0;padding: 0;border: none}',
isGlobalStyles: true,
}
];
}

return styles;
}, [ styles ] );

const wrapperStyles = {
position: 'absolute',
top: '0',
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
};

// get the styles to render the preview
let blockStyles = { padding: '0.5rem 1rem', border: '1px solid' };
const [ shadow ] = useStyle( 'shadow', name );
blockStyles = { ...blockStyles, boxShadow: shadow };

return <>
<Iframe
className="edit-site-block-styles-preview__iframe"
head={ <EditorStyles styles={editorStyles} /> }
style={ {
height: normalizedHeight * 1,
visibility: 'visible',
} }
tabIndex={ -1 }
>
<div style={wrapperStyles}>
<p style={blockStyles}>{ label || 'Preview' }</p>
</div>
</Iframe>
</>
}

export default BlockPreview;
11 changes: 11 additions & 0 deletions packages/edit-site/src/components/global-styles/context-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import { __ } from '@wordpress/i18n';
*/
import { useHasBorderPanel } from './border-panel';
import { useHasColorPanel } from './color-utils';
import { useHasShadowPanel } from './shadows-panel';
import { useHasDimensionsPanel } from './dimensions-panel';
import { useHasTypographyPanel } from './typography-panel';
import { NavigationButtonAsItem } from './navigation-button';

function ContextMenu( { name, parentMenu = '' } ) {
const hasTypographyPanel = useHasTypographyPanel( name );
const hasColorPanel = useHasColorPanel( name );
const hasShadowPanel = useHasShadowPanel( name );
const hasBorderPanel = useHasBorderPanel( name );
const hasDimensionsPanel = useHasDimensionsPanel( name );
const hasLayoutPanel = hasBorderPanel || hasDimensionsPanel;
Expand All @@ -41,6 +43,15 @@ function ContextMenu( { name, parentMenu = '' } ) {
{ __( 'Colors' ) }
</NavigationButtonAsItem>
) }
{ hasShadowPanel && (
<NavigationButtonAsItem
icon={ color }
path={ parentMenu + '/shadows' }
aria-label={ __( 'Shadow styles' ) }
>
{ __( 'Shadows' ) }
</NavigationButtonAsItem>
) }
{ hasLayoutPanel && (
<NavigationButtonAsItem
icon={ layout }
Expand Down
5 changes: 5 additions & 0 deletions packages/edit-site/src/components/global-styles/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ export function getSupportedGlobalStylesPanels( name ) {
supportKeys.push( 'blockGap' );
}

// check for shadow support
if (blockType?.supports?.shadow) {
supportKeys.push( 'shadow' );
}

Object.keys( STYLE_PROPERTY ).forEach( ( styleName ) => {
if ( ! STYLE_PROPERTY[ styleName ].support ) {
return;
Expand Down
103 changes: 103 additions & 0 deletions packages/edit-site/src/components/global-styles/screen-shadows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* External dependencies
*/
import styled from '@emotion/styled';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
__experimentalVStack as VStack,
Button,
Panel,
} from '@wordpress/components';
import { plus } from '@wordpress/icons';

/**
* Internal dependencies
*/
import ScreenHeader from './header';
import { useStyle } from './hooks';
import Subtitle from './subtitle';
import BlockPreview from './block-preview';
import { getShadowString, parseShadowString } from './shadow-utils';
import { ShadowPanel } from './shadows-panel';

const ShadowsContainer = styled.div`
// NOTE: this is a temporary component to add left and right borders to Panel
// if the Panel is the finalized component, then it needs to be updated to have borders on all sides based on a prop
// and this component will be removed.
& .components-panel__body {
border-left: 1px solid #e0e0e0;
border-right: 1px solid #e0e0e0;
}
`;

function ScreenShadows( { name } ) {
const [ shadow, setShadow ] = useStyle( 'shadow', name );
// if given shadow is an array, convert to string
const shadows = parseShadowString([].concat(shadow).flat().join(', '));

function handleShadowChange(newShadow, index) {
const shadowsCopy = [...shadows];
shadowsCopy[index] = newShadow;
const shadowString = getShadowString(shadowsCopy);
setShadow(shadowString);
}

function handleShadowDelete(index) {
const shadowsCopy = [...shadows];
shadowsCopy.splice(index, 1);
const shadowString = getShadowString(shadowsCopy);
setShadow(shadowString);
}

function addNewShadow() {
const newShadow = parseShadowString('5px 5px 0 0 var(--wp--preset--color--primary)').shift();
handleShadowChange(newShadow, shadows.length);
}

return (
<>
<ScreenHeader
title={ __( 'Shadows' ) }
description={ __(
'Manage block shadows of different global elements on the site.'
) }
/>

<div className="edit-site-global-styles-screen-colors">
<VStack spacing={ 5 }>

<VStack spacing={ 3 }>
<Subtitle>{ __( 'Preview' ) }</Subtitle>
<BlockPreview label={__( 'Preview' )} name={ name } />
</VStack>

<VStack spacing={ 3 }>
<Subtitle>{ __( 'Shadows' ) }</Subtitle>

<Panel>
<ShadowsContainer>
{ shadows.map((shadowObj, index) =>
<ShadowPanel
key={index}
shadow={shadowObj}
onChange={(s) => handleShadowChange(s, index)}
onDelete={() => handleShadowDelete(index)} />
)
}
</ShadowsContainer>
</Panel>

<Button variant='tertiary' icon={plus} onClick={addNewShadow}>{__( 'Add new shadow' )}</Button>
</VStack>

</VStack>
</div>
</>
);
}

export default ScreenShadows;
51 changes: 51 additions & 0 deletions packages/edit-site/src/components/global-styles/shadow-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@

const SHADOWS_REG = /,(?![^\(]*\))/
const SHADOW_PARTS_REG = /\s(?![^(]*\))/

export function parseShadowString(shadowStr) {
try {
const shadows = shadowStr.split(SHADOWS_REG).map(parseSingleShadow);
return shadows.filter(shadow => !!shadow);
} catch (error) {
return [];
}
}

function parseSingleShadow(shadow) {
shadow = shadow.trim();

// check if the string is valid value for box-shadow
// this will be extended to parse presets, but for now returning any invalid string
const isValidShadow = CSS.supports('box-shadow', shadow);
if ( !isValidShadow ) return null;

const parts = shadow.split(SHADOW_PARTS_REG);
// check if shadow had inset
const inset = parts.includes('inset');
// check for the color right to left as per the CSS spec
const color = parts.reverse().filter(p => CSS.supports('color', p)).shift()
// remove inset and color to capture remaining values
const [offsetX, offsetY, blur, spread] = parts.filter(p => p!=='inset' && p!==color).reverse()

return {
offsetX,
offsetY,
blur,
spread,
color,
inset,
}
}

export function getShadowString(shadows) {
if ( !Array.isArray(shadows) ) return ''

return shadows.map(shadow => {
let str = `${shadow.offsetX || 0} ${shadow.offsetY || 0}`;
str += shadow.blur ? ` ${shadow.blur}` : '';
str += shadow.spread ? ` ${shadow.spread}` : '';
str += shadow.color ? ` ${shadow.color}` : '';
str += shadow.inset ? ` inset` : '';
return str;
}).join(', ')
}
Loading