Skip to content

Commit

Permalink
Edit/create shadows in global styles (WordPress#60706)
Browse files Browse the repository at this point in the history
* add shadows panel in global styles

* add shadows editor view

* add color edit popover

* refine ui between default and custom shadows

* update shadows UI to match new designs

* add unit tests

* Try different approach to parsing shadow strings

* add more unit tests and improve util functions

* update shadows edit panel

* fix unit conversion issues and other ui improvements

* add shadow rename and delete functions

* address design feedback

* add option to reset default and theme shadows

* add custom shadows in block styles

* remove local state and use momoize

* fix lint issue

* move reset option from shadows panel to shadow edit panel

* split shadow dropdown button into two buttons

* fix item height to 40px

* validate invalid shadow strings

* update spacing

* delete comments

---------

Co-authored-by: Aaron Robertshaw <[email protected]>
  • Loading branch information
2 people authored and carstingaxion committed Jun 4, 2024
1 parent 4535dbb commit 4733390
Show file tree
Hide file tree
Showing 9 changed files with 1,207 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,11 @@ export function useShadowPresets( settings ) {
}

const defaultPresetsEnabled = settings?.shadow?.defaultPresets;
const { default: defaultShadows, theme: themeShadows } =
settings?.shadow?.presets ?? {};
const {
default: defaultShadows,
theme: themeShadows,
custom: customShadows,
} = settings?.shadow?.presets ?? {};
const unsetShadow = {
name: __( 'Unset' ),
slug: 'unset',
Expand All @@ -183,6 +186,7 @@ export function useShadowPresets( settings ) {
const shadowPresets = [
...( ( defaultPresetsEnabled && defaultShadows ) || EMPTY_ARRAY ),
...( themeShadows || EMPTY_ARRAY ),
...( customShadows || EMPTY_ARRAY ),
];
if ( shadowPresets.length ) {
shadowPresets.unshift( unsetShadow );
Expand Down
18 changes: 17 additions & 1 deletion packages/edit-site/src/components/global-styles/root-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
* WordPress dependencies
*/
import { __experimentalItemGroup as ItemGroup } from '@wordpress/components';
import { typography, color, layout, image } from '@wordpress/icons';
import {
typography,
color,
layout,
image,
shadow as shadowIcon,
} from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';

Expand All @@ -26,6 +32,7 @@ function RootMenu() {
const settings = useSettingsForBlockElement( rawSettings );
const hasTypographyPanel = useHasTypographyPanel( settings );
const hasColorPanel = useHasColorPanel( settings );
const hasShadowPanel = true; // useHasShadowPanel( settings );
const hasDimensionsPanel = useHasDimensionsPanel( settings );
const hasLayoutPanel = hasDimensionsPanel;
const hasBackgroundPanel = useHasBackgroundPanel( settings );
Expand All @@ -51,6 +58,15 @@ function RootMenu() {
{ __( 'Colors' ) }
</NavigationButtonAsItem>
) }
{ hasShadowPanel && (
<NavigationButtonAsItem
icon={ shadowIcon }
path="/shadows"
aria-label={ __( 'Shadow styles' ) }
>
{ __( 'Shadows' ) }
</NavigationButtonAsItem>
) }
{ hasLayoutPanel && (
<NavigationButtonAsItem
icon={ layout }
Expand Down
13 changes: 13 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,13 @@
/**
* Internal dependencies
*/
import ShadowsPanel from './shadows-panel';
import ShadowsEditPanel from './shadows-edit-panel';

export function ScreenShadows() {
return <ShadowsPanel />;
}

export function ScreenShadowsEdit() {
return <ShadowsEditPanel />;
}
158 changes: 158 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,158 @@
export const CUSTOM_VALUE_SETTINGS = {
px: { max: 20, step: 1 },
'%': { max: 100, step: 1 },
vw: { max: 100, step: 1 },
vh: { max: 100, step: 1 },
em: { max: 10, step: 0.1 },
rm: { max: 10, step: 0.1 },
svw: { max: 100, step: 1 },
lvw: { max: 100, step: 1 },
dvw: { max: 100, step: 1 },
svh: { max: 100, step: 1 },
lvh: { max: 100, step: 1 },
dvh: { max: 100, step: 1 },
vi: { max: 100, step: 1 },
svi: { max: 100, step: 1 },
lvi: { max: 100, step: 1 },
dvi: { max: 100, step: 1 },
vb: { max: 100, step: 1 },
svb: { max: 100, step: 1 },
lvb: { max: 100, step: 1 },
dvb: { max: 100, step: 1 },
vmin: { max: 100, step: 1 },
svmin: { max: 100, step: 1 },
lvmin: { max: 100, step: 1 },
dvmin: { max: 100, step: 1 },
vmax: { max: 100, step: 1 },
svmax: { max: 100, step: 1 },
lvmax: { max: 100, step: 1 },
dvmax: { max: 100, step: 1 },
};

export function getShadowParts( shadow ) {
const shadowValues = shadow.match( /(?:[^,(]|\([^)]*\))+/g ) || [];
return shadowValues.map( ( value ) => value.trim() );
}

export function shadowStringToObject( shadowValue ) {
/*
* Shadow spec: https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow
* Shadow string format: <offset-x> <offset-y> <blur-radius> <spread-radius> <color> [inset]
*
* A shadow to be valid it must satisfy the following.
*
* 1. Should not contain "none" keyword.
* 2. Values x, y, blur, spread should be in the order. Color and inset can be anywhere in the string except in between x, y, blur, spread values.
* 3. Should not contain more than one set of x, y, blur, spread values.
* 4. Should contain at least x and y values. Others are optional.
* 5. Should not contain more than one "inset" (case insensitive) keyword.
* 6. Should not contain more than one color value.
*/

const defaultShadow = {
x: '0',
y: '0',
blur: '0',
spread: '0',
color: '#000',
inset: false,
};

if ( ! shadowValue ) {
return defaultShadow;
}

// Rule 1: Should not contain "none" keyword.
// if the shadow has "none" keyword, it is not a valid shadow string
if ( shadowValue.includes( 'none' ) ) {
return defaultShadow;
}

// Rule 2: Values x, y, blur, spread should be in the order.
// Color and inset can be anywhere in the string except in between x, y, blur, spread values.
// Extract length values (x, y, blur, spread) from shadow string
// Regex match groups of 1 to 4 length values.
const lengthsRegex =
/((?:^|\s+)(-?\d*\.?\d+(?:px|%|in|cm|mm|em|rem|ex|pt|pc|vh|vw|vmin|vmax|ch|lh)?)(?=\s|$)(?![^(]*\))){1,4}/g;
const matches = shadowValue.match( lengthsRegex ) || [];

// Rule 3: Should not contain more than one set of x, y, blur, spread values.
// if the string doesn't contain exactly 1 set of x, y, blur, spread values,
// it is not a valid shadow string
if ( matches.length !== 1 ) {
return defaultShadow;
}

// Extract length values (x, y, blur, spread) from shadow string
const lengths = matches[ 0 ]
.split( ' ' )
.map( ( value ) => value.trim() )
.filter( ( value ) => value );

// Rule 4: Should contain at least x and y values. Others are optional.
if ( lengths.length < 2 ) {
return defaultShadow;
}

// Rule 5: Should not contain more than one "inset" (case insensitive) keyword.
// check if the shadow string contains "inset" keyword
const insets = shadowValue.match( /inset/gi ) || [];
if ( insets.length > 1 ) {
return defaultShadow;
}

// Strip lengths and inset from shadow string, leaving just color.
const hasInset = insets.length === 1;
let colorString = shadowValue.replace( lengthsRegex, '' ).trim();
if ( hasInset ) {
colorString = colorString
.replace( 'inset', '' )
.replace( 'INSET', '' )
.trim();
}

// Rule 6: Should not contain more than one color value.
// validate color string with regular expression
// check if color has matching hex, rgb or hsl values
const colorRegex =
/^#([0-9a-f]{3}){1,2}$|^#([0-9a-f]{4}){1,2}$|^(?:rgb|hsl)a?\(?[\d*\.?\d+%?,?\/?\s]*\)$/gi;
let colorMatches = ( colorString.match( colorRegex ) || [] )
.map( ( value ) => value?.trim() )
.filter( ( value ) => value );

// If color string has more than one color values, it is not a valid
if ( colorMatches.length > 1 ) {
return defaultShadow;
} else if ( colorMatches.length === 0 ) {
// check if color string has multiple named color values separated by space
colorMatches = colorString
.trim()
.split( ' ' )
.filter( ( value ) => value );
// If color string has more than one color values, it is not a valid
if ( colorMatches.length > 1 ) {
return defaultShadow;
}
}

// Return parsed shadow object.
const [ x, y, blur, spread ] = lengths;
return {
x,
y,
blur: blur || defaultShadow.blur,
spread: spread || defaultShadow.spread,
inset: hasInset,
color: colorString || defaultShadow.color,
};
}

export function shadowObjectToString( shadowObj ) {
const shadowString = `${ shadowObj.x || '0px' } ${ shadowObj.y || '0px' } ${
shadowObj.blur || '0px'
} ${ shadowObj.spread || '0px' }`;

return `${ shadowObj.inset ? 'inset' : '' } ${ shadowString } ${
shadowObj.color || ''
}`.trim();
}
Loading

0 comments on commit 4733390

Please sign in to comment.