From 70a0652242c01c5db2f18e2e7a6993f5e595ca3e Mon Sep 17 00:00:00 2001 From: ramonjd Date: Mon, 20 Jun 2022 14:24:05 +1000 Subject: [PATCH 1/2] Initial commit Adding border style types and generator functions Refactoring generateBoxRules to accommodate various "individual" properties --- packages/blocks/src/api/constants.js | 16 ++ .../style-engine/src/styles/border/index.ts | 150 ++++++++++++++++++ packages/style-engine/src/styles/index.ts | 8 +- .../style-engine/src/styles/spacing/margin.ts | 10 +- .../src/styles/spacing/padding.ts | 10 +- packages/style-engine/src/styles/utils.ts | 34 ++-- packages/style-engine/src/test/index.js | 51 ++++++ packages/style-engine/src/types.ts | 33 +++- 8 files changed, 285 insertions(+), 27 deletions(-) create mode 100644 packages/style-engine/src/styles/border/index.ts diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index df0f05fdf5fe92..734334c25554ed 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -33,6 +33,7 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { borderColor: { value: [ 'border', 'color' ], support: [ '__experimentalBorder', 'color' ], + useEngine: true, }, borderRadius: { value: [ 'border', 'radius' ], @@ -43,62 +44,77 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { borderBottomLeftRadius: 'bottomLeft', borderBottomRightRadius: 'bottomRight', }, + useEngine: true, }, borderStyle: { value: [ 'border', 'style' ], support: [ '__experimentalBorder', 'style' ], + useEngine: true, }, borderWidth: { value: [ 'border', 'width' ], support: [ '__experimentalBorder', 'width' ], + useEngine: true, }, borderTopColor: { value: [ 'border', 'top', 'color' ], support: [ '__experimentalBorder', 'color' ], + useEngine: true, }, borderTopStyle: { value: [ 'border', 'top', 'style' ], support: [ '__experimentalBorder', 'style' ], + useEngine: true, }, borderTopWidth: { value: [ 'border', 'top', 'width' ], support: [ '__experimentalBorder', 'width' ], + useEngine: true, }, borderRightColor: { value: [ 'border', 'right', 'color' ], support: [ '__experimentalBorder', 'color' ], + useEngine: true, }, borderRightStyle: { value: [ 'border', 'right', 'style' ], support: [ '__experimentalBorder', 'style' ], + useEngine: true, }, borderRightWidth: { value: [ 'border', 'right', 'width' ], support: [ '__experimentalBorder', 'width' ], + useEngine: true, }, borderBottomColor: { value: [ 'border', 'bottom', 'color' ], support: [ '__experimentalBorder', 'color' ], + useEngine: true, }, borderBottomStyle: { value: [ 'border', 'bottom', 'style' ], support: [ '__experimentalBorder', 'style' ], + useEngine: true, }, borderBottomWidth: { value: [ 'border', 'bottom', 'width' ], support: [ '__experimentalBorder', 'width' ], + useEngine: true, }, borderLeftColor: { value: [ 'border', 'left', 'color' ], support: [ '__experimentalBorder', 'color' ], + useEngine: true, }, borderLeftStyle: { value: [ 'border', 'left', 'style' ], support: [ '__experimentalBorder', 'style' ], + useEngine: true, }, borderLeftWidth: { value: [ 'border', 'left', 'width' ], support: [ '__experimentalBorder', 'width' ], + useEngine: true, }, color: { value: [ 'color', 'text' ], diff --git a/packages/style-engine/src/styles/border/index.ts b/packages/style-engine/src/styles/border/index.ts new file mode 100644 index 00000000000000..427a3ecf1a444a --- /dev/null +++ b/packages/style-engine/src/styles/border/index.ts @@ -0,0 +1,150 @@ +/** + * External dependencies + */ +import { camelCase, get } from 'lodash'; + +/** + * Internal dependencies + */ +import type { + BorderIndividualStyles, + BorderIndividualProperty, + GeneratedCSSRule, + Style, + StyleDefinition, + StyleOptions, +} from '../../types'; +import { generateRule, generateBoxRules } from '../utils'; + +const color = { + name: 'color', + generate: ( + style: Style, + options: StyleOptions, + path: string[] = [ 'border', 'color' ], + ruleKey: string = 'borderColor' + ): GeneratedCSSRule[] => { + return generateRule( style, options, path, ruleKey ); + }, +}; + +const radius = { + name: 'radius', + generate: ( style: Style, options: StyleOptions ): GeneratedCSSRule[] => { + return generateBoxRules( + style, + options, + [ 'border', 'radius' ], + { + default: 'borderRadius', + individual: 'border%sRadius', + }, + [ 'topLeft', 'topRight', 'bottomLeft', 'bottomRight' ] + ); + }, +}; + +const borderStyle = { + name: 'style', + generate: ( + style: Style, + options: StyleOptions, + path: string[] = [ 'border', 'style' ], + ruleKey: string = 'borderStyle' + ): GeneratedCSSRule[] => { + return generateRule( style, options, path, ruleKey ); + }, +}; + +const width = { + name: 'width', + generate: ( + style: Style, + options: StyleOptions, + path: string[] = [ 'border', 'width' ], + ruleKey: string = 'borderWidth' + ): GeneratedCSSRule[] => { + return generateRule( style, options, path, ruleKey ); + }, +}; + +const borderDefinitionsWithIndividualStyles: StyleDefinition[] = [ + color, + borderStyle, + width, +]; + +/** + * Returns a curried generator function with the individual border property ('top' | 'right' | 'bottom' | 'left') baked in. + * + * @param individualProperty Individual border property ('top' | 'right' | 'bottom' | 'left'). + * + * @return StyleDefinition[ 'generate' ] + */ +const createBorderGenerateFunction = + ( individualProperty: BorderIndividualProperty ) => + ( style: Style, options: StyleOptions ) => { + const styleValue: + | BorderIndividualStyles< typeof individualProperty > + | undefined = get( style, [ 'border', individualProperty ] ); + + if ( ! styleValue ) { + return []; + } + + return borderDefinitionsWithIndividualStyles.reduce( + ( + acc: GeneratedCSSRule[], + borderDefinition: StyleDefinition + ): GeneratedCSSRule[] => { + const key = borderDefinition.name; + if ( + styleValue.hasOwnProperty( key ) && + typeof borderDefinition.generate === 'function' + ) { + const ruleKey = camelCase( + `border-${ individualProperty }-${ key }` + ); + acc.push( + ...borderDefinition.generate( + style, + options, + [ 'border', individualProperty, key ], + ruleKey + ) + ); + } + return acc; + }, + [] + ); + }; + +const borderTop = { + name: 'borderTop', + generate: createBorderGenerateFunction( 'top' ), +}; + +const borderRight = { + name: 'borderRight', + generate: createBorderGenerateFunction( 'right' ), +}; + +const borderBottom = { + name: 'borderBottom', + generate: createBorderGenerateFunction( 'bottom' ), +}; + +const borderLeft = { + name: 'borderLeft', + generate: createBorderGenerateFunction( 'left' ), +}; + +export default [ + ...borderDefinitionsWithIndividualStyles, + radius, + borderTop, + borderRight, + borderBottom, + borderLeft, +]; diff --git a/packages/style-engine/src/styles/index.ts b/packages/style-engine/src/styles/index.ts index 79f1c43d8d33f7..290c319778e292 100644 --- a/packages/style-engine/src/styles/index.ts +++ b/packages/style-engine/src/styles/index.ts @@ -1,8 +1,14 @@ /** * Internal dependencies */ +import border from './border'; import color from './color'; import spacing from './spacing'; import typography from './typography'; -export const styleDefinitions = [ ...color, ...spacing, ...typography ]; +export const styleDefinitions = [ + ...border, + ...color, + ...spacing, + ...typography, +]; diff --git a/packages/style-engine/src/styles/spacing/margin.ts b/packages/style-engine/src/styles/spacing/margin.ts index c8492c378954fd..fb81a58c1e33ab 100644 --- a/packages/style-engine/src/styles/spacing/margin.ts +++ b/packages/style-engine/src/styles/spacing/margin.ts @@ -7,12 +7,10 @@ import { generateBoxRules } from '../utils'; const margin = { name: 'margin', generate: ( style: Style, options: StyleOptions ) => { - return generateBoxRules( - style, - options, - [ 'spacing', 'margin' ], - 'margin' - ); + return generateBoxRules( style, options, [ 'spacing', 'margin' ], { + default: 'margin', + individual: 'margin%s', + } ); }, }; diff --git a/packages/style-engine/src/styles/spacing/padding.ts b/packages/style-engine/src/styles/spacing/padding.ts index a5a3227030e073..0b6d2a6cd928eb 100644 --- a/packages/style-engine/src/styles/spacing/padding.ts +++ b/packages/style-engine/src/styles/spacing/padding.ts @@ -7,12 +7,10 @@ import { generateBoxRules } from '../utils'; const padding = { name: 'padding', generate: ( style: Style, options: StyleOptions ) => { - return generateBoxRules( - style, - options, - [ 'spacing', 'padding' ], - 'padding' - ); + return generateBoxRules( style, options, [ 'spacing', 'padding' ], { + default: 'padding', + individual: 'padding%s', + } ); }, }; diff --git a/packages/style-engine/src/styles/utils.ts b/packages/style-engine/src/styles/utils.ts index 928d923c86821f..d66e7f0a964241 100644 --- a/packages/style-engine/src/styles/utils.ts +++ b/packages/style-engine/src/styles/utils.ts @@ -6,7 +6,13 @@ import { get, upperFirst } from 'lodash'; /** * Internal dependencies */ -import type { GeneratedCSSRule, Style, Box, StyleOptions } from '../types'; +import type { + CssRulesKeys, + GeneratedCSSRule, + Style, + Box, + StyleOptions, +} from '../types'; import { VARIABLE_REFERENCE_PREFIX, VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE, @@ -45,10 +51,11 @@ export function generateRule( /** * Returns a JSON representation of the generated CSS rules taking into account box model properties, top, right, bottom, left. * - * @param style Style object. - * @param options Options object with settings to adjust how the styles are generated. - * @param path An array of strings representing the path to the style value in the style object. - * @param ruleKey A CSS property key. + * @param style Style object. + * @param options Options object with settings to adjust how the styles are generated. + * @param path An array of strings representing the path to the style value in the style object. + * @param ruleKeys An array of CSS property keys and patterns. + * @param individualProperties The "sides" or individual properties for which to generate rules. * * @return GeneratedCSSRule[] CSS rules. */ @@ -56,7 +63,8 @@ export function generateBoxRules( style: Style, options: StyleOptions, path: string[], - ruleKey: string + ruleKeys: CssRulesKeys, + individualProperties: string[] = [ 'top', 'right', 'bottom', 'left' ] ): GeneratedCSSRule[] { const boxStyle: Box | string | undefined = get( style, path ); if ( ! boxStyle ) { @@ -67,17 +75,20 @@ export function generateBoxRules( if ( typeof boxStyle === 'string' ) { rules.push( { selector: options?.selector, - key: ruleKey, + key: ruleKeys.default, value: boxStyle, } ); } else { - const sideRules = [ 'top', 'right', 'bottom', 'left' ].reduce( + const sideRules = individualProperties.reduce( ( acc: GeneratedCSSRule[], side: string ) => { const value: string | undefined = get( boxStyle, [ side ] ); if ( value ) { acc.push( { selector: options?.selector, - key: `${ ruleKey }${ upperFirst( side ) }`, + key: ruleKeys?.individual.replace( + '%s', + upperFirst( side ) + ), value, } ); } @@ -99,10 +110,7 @@ export function generateBoxRules( * @return string A CSS var value. */ export function getCSSVarFromStyleValue( styleValue: string ): string { - if ( - typeof styleValue === 'string' && - styleValue.startsWith( VARIABLE_REFERENCE_PREFIX ) - ) { + if ( styleValue.startsWith( VARIABLE_REFERENCE_PREFIX ) ) { const variable = styleValue .slice( VARIABLE_REFERENCE_PREFIX.length ) .split( VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE ) diff --git a/packages/style-engine/src/test/index.js b/packages/style-engine/src/test/index.js index ad2acd401056b0..1b516d85ff2ce3 100644 --- a/packages/style-engine/src/test/index.js +++ b/packages/style-engine/src/test/index.js @@ -81,6 +81,57 @@ describe( 'generate', () => { } ) ).toEqual( 'color: var(--wp--preset--color--ham-sandwich);' ); } ); + + it( 'should parse border rules', () => { + expect( + generate( { + border: { + color: 'var:preset|color|perky-peppermint', + width: '0.5em', + style: 'dotted', + radius: '11px', + }, + } ) + ).toEqual( + 'border-color: var(--wp--preset--color--perky-peppermint); border-style: dotted; border-width: 0.5em; border-radius: 11px;' + ); + } ); + + it( 'should parse individual border rules', () => { + expect( + generate( { + border: { + top: { + color: 'var:preset|color|sandy-beach', + width: '9px', + style: 'dashed', + }, + right: { + color: 'var:preset|color|leafy-avenue', + width: '5rem', + }, + bottom: { + color: '#eee', + width: '2%', + style: 'solid', + }, + left: { + color: 'var:preset|color|avocado-blues', + width: '100px', + style: 'dotted', + }, + radius: { + topLeft: '1px', + topRight: '2px', + bottomLeft: '3px', + bottomRight: '4px', + }, + }, + } ) + ).toEqual( + 'border-top-left-radius: 1px; border-top-right-radius: 2px; border-bottom-left-radius: 3px; border-bottom-right-radius: 4px; border-top-color: var(--wp--preset--color--sandy-beach); border-top-style: dashed; border-top-width: 9px; border-right-color: var(--wp--preset--color--leafy-avenue); border-right-width: 5rem; border-bottom-color: #eee; border-bottom-style: solid; border-bottom-width: 2%; border-left-color: var(--wp--preset--color--avocado-blues); border-left-style: dotted; border-left-width: 100px;' + ); + } ); } ); describe( 'getCSSRules', () => { diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts index 9f60b1b0f6fc03..becb331525e313 100644 --- a/packages/style-engine/src/types.ts +++ b/packages/style-engine/src/types.ts @@ -11,7 +11,31 @@ export type Box< T extends BoxVariants = undefined > = { left?: CSSProperties[ T extends undefined ? 'left' : `${ T }Left` ]; }; +export type BorderIndividualProperty = 'top' | 'right' | 'bottom' | 'left'; +export type BorderIndividualStyles< T extends BorderIndividualProperty > = { + color?: CSSProperties[ `border${ Capitalize< string & T > }Color` ]; + style?: CSSProperties[ `border${ Capitalize< string & T > }Style` ]; + width?: CSSProperties[ `border${ Capitalize< string & T > }Width` ]; +}; + export interface Style { + border?: { + color?: CSSProperties[ 'borderColor' ]; + radius?: + | CSSProperties[ 'borderRadius' ] + | { + topLeft?: CSSProperties[ 'borderTopLeftRadius' ]; + topRight?: CSSProperties[ 'borderTopRightRadius' ]; + bottomLeft?: CSSProperties[ 'borderBottomLeftRadius' ]; + bottomRight?: CSSProperties[ 'borderBottomLeftRadius' ]; + }; + style?: CSSProperties[ 'borderStyle' ]; + width?: CSSProperties[ 'borderWidth' ]; + top?: BorderIndividualStyles< 'top' >; + right?: BorderIndividualStyles< 'right' >; + bottom?: BorderIndividualStyles< 'bottom' >; + left?: BorderIndividualStyles< 'left' >; + }; spacing?: { margin?: CSSProperties[ 'margin' ] | Box< 'margin' >; padding?: CSSProperties[ 'padding' ] | Box< 'padding' >; @@ -40,6 +64,8 @@ export interface Style { }; } +export type CssRulesKeys = { default: string; individual: string }; + export type StyleOptions = { /** * CSS selector for the generated style. @@ -59,6 +85,11 @@ export type GeneratedCSSRule = { export interface StyleDefinition { name: string; - generate?: ( style: Style, options: StyleOptions ) => GeneratedCSSRule[]; + generate?: ( + style: Style, + options: StyleOptions, + path?: string[], + ruleKey?: string + ) => GeneratedCSSRule[]; getClassNames?: ( style: Style ) => string[]; } From 93abab89cf2c65fa9b65311f8ba64051e269a1e5 Mon Sep 17 00:00:00 2001 From: ramonjd Date: Tue, 21 Jun 2022 13:02:44 +1000 Subject: [PATCH 2/2] Added explainer comment for BorderIndividualStyles type Removing lodash camelCase and upperFirst in favour of own implementation + test Removing unnecessary lodash get() --- .../style-engine/src/styles/border/index.ts | 15 +++++--------- packages/style-engine/src/styles/utils.ts | 20 ++++++++++++++++--- packages/style-engine/src/test/utils.js | 12 +++++++++++ packages/style-engine/src/types.ts | 1 + 4 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 packages/style-engine/src/test/utils.js diff --git a/packages/style-engine/src/styles/border/index.ts b/packages/style-engine/src/styles/border/index.ts index 427a3ecf1a444a..77ee189a5d92c3 100644 --- a/packages/style-engine/src/styles/border/index.ts +++ b/packages/style-engine/src/styles/border/index.ts @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { camelCase, get } from 'lodash'; - /** * Internal dependencies */ @@ -14,7 +9,7 @@ import type { StyleDefinition, StyleOptions, } from '../../types'; -import { generateRule, generateBoxRules } from '../utils'; +import { generateRule, generateBoxRules, upperFirst } from '../utils'; const color = { name: 'color', @@ -86,7 +81,7 @@ const createBorderGenerateFunction = ( style: Style, options: StyleOptions ) => { const styleValue: | BorderIndividualStyles< typeof individualProperty > - | undefined = get( style, [ 'border', individualProperty ] ); + | undefined = style?.border?.[ individualProperty ]; if ( ! styleValue ) { return []; @@ -102,9 +97,9 @@ const createBorderGenerateFunction = styleValue.hasOwnProperty( key ) && typeof borderDefinition.generate === 'function' ) { - const ruleKey = camelCase( - `border-${ individualProperty }-${ key }` - ); + const ruleKey = `border${ upperFirst( + individualProperty + ) }${ upperFirst( key ) }`; acc.push( ...borderDefinition.generate( style, diff --git a/packages/style-engine/src/styles/utils.ts b/packages/style-engine/src/styles/utils.ts index d66e7f0a964241..a905e1929fdb3f 100644 --- a/packages/style-engine/src/styles/utils.ts +++ b/packages/style-engine/src/styles/utils.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { get, upperFirst } from 'lodash'; +import { get } from 'lodash'; /** * Internal dependencies @@ -57,7 +57,7 @@ export function generateRule( * @param ruleKeys An array of CSS property keys and patterns. * @param individualProperties The "sides" or individual properties for which to generate rules. * - * @return GeneratedCSSRule[] CSS rules. + * @return GeneratedCSSRule[] CSS rules. */ export function generateBoxRules( style: Style, @@ -110,7 +110,10 @@ export function generateBoxRules( * @return string A CSS var value. */ export function getCSSVarFromStyleValue( styleValue: string ): string { - if ( styleValue.startsWith( VARIABLE_REFERENCE_PREFIX ) ) { + if ( + typeof styleValue === 'string' && + styleValue.startsWith( VARIABLE_REFERENCE_PREFIX ) + ) { const variable = styleValue .slice( VARIABLE_REFERENCE_PREFIX.length ) .split( VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE ) @@ -119,3 +122,14 @@ export function getCSSVarFromStyleValue( styleValue: string ): string { } return styleValue; } + +/** + * Capitalizes the first letter in a string. + * + * @param {string} str The string whose first letter the function will capitalize. + * + * @return string A CSS var value. + */ +export function upperFirst( [ firstLetter, ...rest ]: string ) { + return firstLetter.toUpperCase() + rest.join( '' ); +} diff --git a/packages/style-engine/src/test/utils.js b/packages/style-engine/src/test/utils.js new file mode 100644 index 00000000000000..7fccd013594420 --- /dev/null +++ b/packages/style-engine/src/test/utils.js @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import { upperFirst } from '../styles/utils'; + +describe( 'utils', () => { + describe( 'upperFirst()', () => { + it( 'should return an string with a capitalized first letter', () => { + expect( upperFirst( 'toontown' ) ).toEqual( 'Toontown' ); + } ); + } ); +} ); diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts index becb331525e313..fbc154a21879dd 100644 --- a/packages/style-engine/src/types.ts +++ b/packages/style-engine/src/types.ts @@ -12,6 +12,7 @@ export type Box< T extends BoxVariants = undefined > = { }; export type BorderIndividualProperty = 'top' | 'right' | 'bottom' | 'left'; +// `T` is one of the values in `BorderIndividualProperty`. The expected CSSProperties key is something like `borderTopColor`. export type BorderIndividualStyles< T extends BorderIndividualProperty > = { color?: CSSProperties[ `border${ Capitalize< string & T > }Color` ]; style?: CSSProperties[ `border${ Capitalize< string & T > }Style` ];