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

Added support for custom CSS properties in the StyleWrapper #5581

Merged
merged 13 commits into from
Jan 2, 2024
Merged
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
75 changes: 73 additions & 2 deletions docs/source/blocks/block-style-wrapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ myst:

# Block style wrapper

```{versionadded} 16.0.0
```

The block style wrapper is part of a block anatomy.
It allows you to inject styles from the block schema into the block wrapper in the form of class names.
It wraps the block edit and the view components.
Expand Down Expand Up @@ -109,7 +112,7 @@ config.blocks.blocksConfig.listing.schemaEnhancer = composeSchema(

The `styles` field is mapped to an `objectWidget`.
The following is an example of a possible set of styles.
The style wrapper will read the styles and inject them into the edit and view components as shown in the next sections.
The style wrapper will read the styles and inject the resultant class names into the edit and view components as shown in the next sections.

```json
{
Expand All @@ -127,7 +130,7 @@ The style wrapper will read the styles and inject them into the edit and view co
## Using the injected class names in your block

The resultant class names are injected as a `className` prop into the wrapped block.
Thus you can use it in the root component of your block view and edit components as follows:
Thus you can use it in the root component of your block view component as follows:

```jsx
const BlockView = (props)=> (
Expand All @@ -149,6 +152,8 @@ The resultant HTML would be the following:
```
Then it's at your discretion how you define the CSS class names in your theme.

## Customize the injected class names

stevepiercy marked this conversation as resolved.
Show resolved Hide resolved
If you need other style of classnames generated, you can use the classname
converters defined in `config.settings.styleClassNameConverters`, by
registering fieldnames suffixed with the converter name. For example, a style
Expand All @@ -165,6 +170,72 @@ data like:

will generate classnames `primary inverted`

## Inject custom CSS properties

```{versionadded} 17.8.0
```

The style wrapper also allows to inject custom CSS properties.
This is useful where the property that you want to inject is customizable per project.
For example, imagine you have an existing global CSS custom property `--image-aspect-ratio` that you use for all images in all blocks.
Then in your customized theme, you could set CSS attributes for this property, changing it in runtime.
The key feature of custom CSS properties is that they can be applied also using the cascade.
This means that they can be placed anywhere in either CSS definitions or HTML structure, and they will be applied only in the context where they are defined.

As an example, first define the style of a teaser block image as follows.

```css
.block.teaser img {
aspect-ratio: var(--image-aspect-ratio, 16 / 9);
}
```

Next, you can enhance a block's schema by injecting the custom CSS property as follows.

```js
schema.properties.styles.schema.fieldsets[0].fields = [
...schema.properties.styles.schema.fieldsets[0].fields,
'--image-aspect-ratio',
];

// We use a select widget and set a default
schema.properties.styles.schema.properties['--image-aspect-ratio'] = {
widget: 'select',
title: 'Aspect Ratio',
choices: [
['1', '1:1'],
['16 / 9', '16/9'],
],
default: '1',
};
```

Finally, assuming that you select the default value for the {guilabel}`Aspect Ratio` for the custom CSS property, then the markup of the block will contain the custom property as shown.

```html
<div class="block teaser" style="--image-aspect-ratio: 1">
<img src="example.png">
</div>
```

The custom CSS property definition will only apply within the div that it's defined.
As you can see, the custom CSS property applies only within the `div` in which it is defined.

If you want to use it in your custom components, you need to apply it in the root of your block's view component as follows:

```jsx
const BlockView = (props)=> (
<div style={props.style}>
// Block's view component code
</div>
)
```

```{note}
You need to manually add the above code in your view component block code to benefit from the style injection.
The styles in the block edit component are injected automatically into the blocks engine editor wrappers, so you don't have to take any action.
```

## Align class injection

There is an automatic class name injection happening at the same time the style wrapper class names injection.
Expand Down
1 change: 1 addition & 0 deletions packages/volto/news/5581.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for custom CSS properties in the `StyleWrapper`. @sneridagh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Icon } from '@plone/volto/components';
import {
blockHasValue,
buildStyleClassNamesFromData,
buildStyleObjectFromData,
} from '@plone/volto/helpers';
import dragSVG from '@plone/volto/icons/drag.svg';
import { Button } from 'semantic-ui-react';
Expand Down Expand Up @@ -57,7 +58,8 @@ const EditBlockWrapper = (props) => {
? data.required
: includes(config.blocks.requiredBlocks, type);

const styles = buildStyleClassNamesFromData(data.styles);
const classNames = buildStyleClassNamesFromData(data.styles);
const style = buildStyleObjectFromData(data.styles);

return (
<div
Expand All @@ -66,9 +68,10 @@ const EditBlockWrapper = (props) => {
// Right now, we can have the alignment information in the styles property or in the
// block data root, we inject the classname here for having control over the whole
// Block Edit wrapper
className={cx(`block-editor-${data['@type']}`, styles, {
className={cx(`block-editor-${data['@type']}`, classNames, {
[data.align]: data.align,
})}
style={style}
>
<div style={{ position: 'relative' }}>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import cx from 'classnames';
import {
buildStyleClassNamesFromData,
buildStyleClassNamesExtenders,
buildStyleObjectFromData,
} from '@plone/volto/helpers';

const StyleWrapper = (props) => {
let classNames = [];
let classNames,
style = [];
const { children, content, data = {}, block } = props;
classNames = buildStyleClassNamesFromData(data.styles);

Expand All @@ -16,11 +18,15 @@ const StyleWrapper = (props) => {
data,
classNames,
});

style = buildStyleObjectFromData(data.styles);

const rewrittenChildren = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
const childProps = {
...props,
className: cx([child.props.className, ...classNames]),
style: { ...child.props.style, ...style },
};
return React.cloneElement(child, childProps);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/volto/src/components/manage/Blocks/Grid/View.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { withBlockExtensions } from '@plone/volto/helpers';
import config from '@plone/volto/registry';

const GridBlockView = (props) => {
const { data, path, className } = props;
const { data, path, className, style } = props;
const metadata = props.metadata || props.properties;
const columns = data.blocks_layout.items;
const blocksConfig =
Expand All @@ -22,6 +22,7 @@ const GridBlockView = (props) => {
three: columns?.length === 3,
four: columns?.length === 4,
})}
style={style}
>
{data.headline && <h2 className="headline">{data.headline}</h2>}

Expand Down
3 changes: 2 additions & 1 deletion packages/volto/src/components/manage/Blocks/Image/View.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@plone/volto/helpers';
import config from '@plone/volto/registry';

export const View = ({ className, data, detached, properties }) => {
export const View = ({ className, data, detached, properties, style }) => {
const href = data?.href?.[0]?.['@id'] || '';

const Image = config.getComponent({ name: 'Image' }).component;
Expand All @@ -25,6 +25,7 @@ export const View = ({ className, data, detached, properties }) => {
data.align,
className,
)}
style={style}
>
{data.url && (
<>
Expand Down
3 changes: 2 additions & 1 deletion packages/volto/src/components/manage/Blocks/Listing/View.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { withBlockExtensions } from '@plone/volto/helpers';
import { ListingBlockBody as ListingBody } from '@plone/volto/components';

const View = (props) => {
const { data, path, pathname, className } = props;
const { data, path, pathname, className, style } = props;

return (
<div
className={cx('block listing', data.variation || 'default', className)}
style={style}
>
<ListingBody {...props} path={path ?? pathname} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const messages = defineMessages({
});

const TeaserDefaultTemplate = (props) => {
const { className, data, isEditMode } = props;
const { className, data, isEditMode, style } = props;
const intl = useIntl();
const href = data.href?.[0];
const image = data.preview_image?.[0];
Expand All @@ -27,7 +27,7 @@ const TeaserDefaultTemplate = (props) => {
const { openExternalLinkInNewTab } = config.settings;

return (
<div className={cx('block teaser', className)}>
<div className={cx('block teaser', className)} style={style}>
<>
{!href && isEditMode && (
<Message>
Expand Down
57 changes: 56 additions & 1 deletion packages/volto/src/helpers/Blocks/Blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,14 +563,15 @@ export const styleToClassName = (key, value, prefix = '') => {
};

export const buildStyleClassNamesFromData = (obj = {}, prefix = '') => {
// styles has the form:
// style wrapper object has the form:
// const styles = {
// color: 'red',
// backgroundColor: '#AABBCC',
// }
// Returns: ['has--color--red', 'has--backgroundColor--AABBCC']

return Object.entries(obj)
.filter(([k, v]) => !k.startsWith('--'))
.reduce(
(acc, [k, v]) => [
...acc,
Expand Down Expand Up @@ -602,6 +603,60 @@ export const buildStyleClassNamesExtenders = ({
);
};

/**
* Converts a name+value style pair (ex: color/red) to a pair of [k, v],
* such as ["color", "red"] so it can be converted back to an object.
* For now, only covering the 'CSSProperty' use case.
*/
export const styleDataToStyleObject = (key, value, prefix = '') => {
if (prefix) {
return [`--${prefix}${key.replace('--', '')}`, value];
} else {
return [key, value];
}
};

/**
* Generate styles object from data
*
* @function buildStyleObjectFromData
* @param {Object} obj A style wrapper object data
* @param {string} prefix The prefix (could be dragged from a recursive call, initially empty)
* @return {Object} The style object ready to be passed as prop
*/
export const buildStyleObjectFromData = (obj = {}, prefix = '') => {
// style wrapper object has the form:
// const styles = {
// color: 'red',
// '--background-color': '#AABBCC',
// }
// Returns: {'--background-color: '#AABBCC'}

return Object.fromEntries(
Object.entries(obj)
.filter(([k, v]) => k.startsWith('--') || isObject(v))
.reduce(
(acc, [k, v]) => [
...acc,
// Kept for easy debugging
// ...(() => {
// if (isObject(v)) {
// return Object.entries(
// buildStyleObjectFromData(v, `${prefix}${k}--`),
// );
// }
// return [styleDataToStyleObject(k, v, prefix)];
// })(),
...(isObject(v)
? Object.entries(buildStyleObjectFromData(v, `${prefix}${k}--`))
: [styleDataToStyleObject(k, v, prefix)]),
],
[],
)
.filter((v) => !!v),
);
};

/**
* Return previous/next blocks given the content object and the current block id
*
Expand Down
57 changes: 57 additions & 0 deletions packages/volto/src/helpers/Blocks/Blocks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
applySchemaDefaults,
buildStyleClassNamesFromData,
buildStyleClassNamesExtenders,
buildStyleObjectFromData,
getPreviousNextBlock,
blocksFormGenerator,
findBlocks,
Expand Down Expand Up @@ -1066,6 +1067,62 @@ describe('Blocks', () => {
};
expect(buildStyleClassNamesFromData(styles)).toEqual([]);
});

it('It does not output any className for style converter values', () => {
const styles = {
color: 'red',
'--background-color': '#FFF',
};
expect(buildStyleClassNamesFromData(styles)).toEqual(['has--color--red']);
});

it.skip('It does not output any className for unknown converter values', () => {
const styles = {
color: 'red',
'backgroundColor:style': '#FFF',
};
expect(buildStyleClassNamesFromData(styles)).toEqual(['has--color--red']);
});
});

describe('buildStyleObjectFromData', () => {
it('Understands style converter for style values, no styles found', () => {
const styles = {
color: 'red',
backgroundColor: '#FFF',
};
expect(buildStyleObjectFromData(styles)).toEqual({});
});

it('Understands style converter for style values', () => {
const styles = {
color: 'red',
'--background-color': '#FFF',
};
expect(buildStyleObjectFromData(styles)).toEqual({
'--background-color': '#FFF',
});
});

it('Supports multiple nested levels', () => {
const styles = {
'--color': 'red',
backgroundColor: '#AABBCC',
nested: {
l1: 'white',
'--foo': 'white',
level2: {
'--foo': '#fff',
bar: '#000',
},
},
};
expect(buildStyleObjectFromData(styles)).toEqual({
'--color': 'red',
'--nested--foo': 'white',
'--nested--level2--foo': '#fff',
});
});
});

describe('getPreviousNextBlock', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/volto/src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export {
blocksFormGenerator,
buildStyleClassNamesFromData,
buildStyleClassNamesExtenders,
buildStyleObjectFromData,
getPreviousNextBlock,
findBlocks,
} from '@plone/volto/helpers/Blocks/Blocks';
Expand Down