Skip to content

Commit

Permalink
Refactor style engine border styles (#43594)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Lende authored Sep 15, 2022
1 parent 1707396 commit 61e41f0
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 124 deletions.
148 changes: 49 additions & 99 deletions packages/style-engine/src/styles/border/index.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
/**
* Internal dependencies
*/
import type {
BorderIndividualStyles,
BorderIndividualProperty,
GeneratedCSSRule,
Style,
StyleDefinition,
StyleOptions,
} from '../../types';
import { generateRule, generateBoxRules, upperFirst } from '../utils';
import type { BoxEdge, GenerateFunction, StyleDefinition } from '../../types';
import { generateRule, generateBoxRules, camelCaseJoin } from '../utils';

const color = {
/**
* Creates a function for generating CSS rules when the style path is the same as the camelCase CSS property used in React.
*
* @param path An array of strings representing the path to the style value in the style object.
*
* @return A function that generates CSS rules.
*/
function createBorderGenerateFunction( path: string[] ): GenerateFunction {
return ( style, options ) =>
generateRule( style, options, path, camelCaseJoin( path ) );
}

/**
* Creates a function for generating border-{top,bottom,left,right}-{color,style,width} CSS rules.
*
* @param edge The edge to create CSS rules for.
*
* @return A function that generates CSS rules.
*/
function createBorderEdgeGenerateFunction( edge: BoxEdge ): GenerateFunction {
return ( style, options ) => {
return [ 'color', 'style', 'width' ].flatMap( ( key ) => {
const path = [ 'border', edge, key ];
return createBorderGenerateFunction( path )( style, options );
} );
};
}

const color: StyleDefinition = {
name: 'color',
generate: (
style: Style,
options: StyleOptions,
path: string[] = [ 'border', 'color' ],
ruleKey: string = 'borderColor'
): GeneratedCSSRule[] => {
return generateRule( style, options, path, ruleKey );
},
generate: createBorderGenerateFunction( [ 'border', 'color' ] ),
};

const radius = {
const radius: StyleDefinition = {
name: 'radius',
generate: ( style: Style, options: StyleOptions ): GeneratedCSSRule[] => {
generate: ( style, options ) => {
return generateBoxRules(
style,
options,
Expand All @@ -39,104 +53,40 @@ const radius = {
},
};

const borderStyle = {
const borderStyle: StyleDefinition = {
name: 'style',
generate: (
style: Style,
options: StyleOptions,
path: string[] = [ 'border', 'style' ],
ruleKey: string = 'borderStyle'
): GeneratedCSSRule[] => {
return generateRule( style, options, path, ruleKey );
},
generate: createBorderGenerateFunction( [ 'border', 'style' ] ),
};

const width = {
const width: StyleDefinition = {
name: 'width',
generate: (
style: Style,
options: StyleOptions,
path: string[] = [ 'border', 'width' ],
ruleKey: string = 'borderWidth'
): GeneratedCSSRule[] => {
return generateRule( style, options, path, ruleKey );
},
generate: createBorderGenerateFunction( [ 'border', 'width' ] ),
};

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 = 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 = `border${ upperFirst(
individualProperty
) }${ upperFirst( key ) }`;
acc.push(
...borderDefinition.generate(
style,
options,
[ 'border', individualProperty, key ],
ruleKey
)
);
}
return acc;
},
[]
);
};

const borderTop = {
const borderTop: StyleDefinition = {
name: 'borderTop',
generate: createBorderGenerateFunction( 'top' ),
generate: createBorderEdgeGenerateFunction( 'top' ),
};

const borderRight = {
const borderRight: StyleDefinition = {
name: 'borderRight',
generate: createBorderGenerateFunction( 'right' ),
generate: createBorderEdgeGenerateFunction( 'right' ),
};

const borderBottom = {
const borderBottom: StyleDefinition = {
name: 'borderBottom',
generate: createBorderGenerateFunction( 'bottom' ),
generate: createBorderEdgeGenerateFunction( 'bottom' ),
};

const borderLeft = {
const borderLeft: StyleDefinition = {
name: 'borderLeft',
generate: createBorderGenerateFunction( 'left' ),
generate: createBorderEdgeGenerateFunction( 'left' ),
};

export default [
...borderDefinitionsWithIndividualStyles,
color,
borderStyle,
width,
radius,
borderTop,
borderRight,
Expand Down
21 changes: 17 additions & 4 deletions packages/style-engine/src/styles/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function generateRule(
options: StyleOptions,
path: string[],
ruleKey: string
) {
): GeneratedCSSRule[] {
const styleValue: string | undefined = get( style, path );

return styleValue
Expand Down Expand Up @@ -128,10 +128,23 @@ export function getCSSVarFromStyleValue( styleValue: string ): string {
/**
* Capitalizes the first letter in a string.
*
* @param {string} str The string whose first letter the function will capitalize.
* @param string The string whose first letter the function will capitalize.
*
* @return string A CSS var value.
* @return String with the first letter capitalized.
*/
export function upperFirst( [ firstLetter, ...rest ]: string ) {
export function upperFirst( string: string ): string {
const [ firstLetter, ...rest ] = string;
return firstLetter.toUpperCase() + rest.join( '' );
}

/**
* Converts an array of strings into a camelCase string.
*
* @param strings The strings to join into a camelCase string.
*
* @return camelCase string.
*/
export function camelCaseJoin( strings: string[] ): string {
const [ firstItem, ...rest ] = strings;
return firstItem.toLowerCase() + rest.map( upperFirst ).join( '' );
}
7 changes: 6 additions & 1 deletion packages/style-engine/src/test/utils.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
/**
* Internal dependencies
*/
import { upperFirst } from '../styles/utils';
import { camelCaseJoin, upperFirst } from '../styles/utils';

describe( 'utils', () => {
describe( 'upperFirst()', () => {
it( 'should return an string with a capitalized first letter', () => {
expect( upperFirst( 'toontown' ) ).toEqual( 'Toontown' );
} );
} );
describe( 'camelCaseJoin()', () => {
it( 'should return a camelCase string', () => {
expect( camelCaseJoin( [ 'toon', 'town' ] ) ).toEqual( 'toonTown' );
} );
} );
} );
43 changes: 23 additions & 20 deletions packages/style-engine/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@
*/
import type { CSSProperties } from 'react';

type BoxVariants = 'margin' | 'padding' | undefined;
export type Box< T extends BoxVariants = undefined > = {
type BoxVariant = 'margin' | 'padding';
export interface Box< T extends BoxVariant | undefined = undefined > {
top?: CSSProperties[ T extends undefined ? 'top' : `${ T }Top` ];
right?: CSSProperties[ T extends undefined ? 'right' : `${ T }Right` ];
bottom?: CSSProperties[ T extends undefined ? 'bottom' : `${ T }Bottom` ];
left?: CSSProperties[ T extends undefined ? 'left' : `${ T }Left` ];
};
}

export type BoxEdge = 'top' | 'right' | 'bottom' | 'left';

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` ];
width?: CSSProperties[ `border${ Capitalize< string & T > }Width` ];
};
export interface BorderIndividualStyles< T extends BoxEdge > {
color?: CSSProperties[ `border${ Capitalize< T > }Color` ];
style?: CSSProperties[ `border${ Capitalize< T > }Style` ];
width?: CSSProperties[ `border${ Capitalize< T > }Width` ];
}

export interface Style {
border?: {
Expand Down Expand Up @@ -65,31 +66,33 @@ export interface Style {
};
}

export type CssRulesKeys = { default: string; individual: string };
export interface CssRulesKeys {
default: string;
individual: string;
}

export type StyleOptions = {
export interface StyleOptions {
/**
* CSS selector for the generated style.
*/
selector?: string;
};
}

export type GeneratedCSSRule = {
export interface GeneratedCSSRule {
selector?: string;
value: string;
/**
* The CSS key in JS style attribute format, compatible with React.
* E.g. `paddingTop` instead of `padding-top`.
*/
key: string;
};
}

export interface GenerateFunction {
( style: Style, options: StyleOptions ): GeneratedCSSRule[];
}

export interface StyleDefinition {
name: string;
generate?: (
style: Style,
options: StyleOptions | {},
path?: string[],
ruleKey?: string
) => GeneratedCSSRule[];
generate?: GenerateFunction;
}

0 comments on commit 61e41f0

Please sign in to comment.