Skip to content

Commit 648653e

Browse files
authored
feat(Raw:Img): ENG-9527 Added new inputs for raw:img to handle the metadata (BuilderIO#4097)
## Description Previously, images imported from sources like Figma were treated as basic img tags and lacked the advanced editing capabilities of Builder's Image component (e.g., aspect ratio, overlays, etc.). This created an inconsistent user experience. This change ensures that all images, regardless of their source, can be fully customized within the editor.
1 parent 251146d commit 648653e

File tree

7 files changed

+514
-26
lines changed

7 files changed

+514
-26
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@builder.io/react": patch
3+
"@builder.io/sdk": patch
4+
"@builder.io/sdk-angular": patch
5+
"@builder.io/sdk-react-nextjs": patch
6+
"@builder.io/sdk-qwik": patch
7+
"@builder.io/sdk-react": patch
8+
"@builder.io/sdk-react-native": patch
9+
"@builder.io/sdk-solid": patch
10+
"@builder.io/sdk-svelte": patch
11+
"@builder.io/sdk-vue": patch
12+
---
13+
14+
FEAT: Updated 'Raw:Img' componentInfo with extra inputs field

packages/react/src/blocks/raw/Img.tsx

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export interface ImgProps {
99
attributes?: any;
1010
image?: string;
1111
builderBlock?: BuilderElement;
12+
aspectRatio?: number;
13+
backgroundSize?: string;
14+
backgroundPosition?: string;
15+
altText?: string;
16+
title?: string;
1217
}
1318

1419
// TODO: srcset, alt text input, object size/position input, etc
@@ -31,32 +36,101 @@ class ImgComponent extends React.Component<ImgProps> {
3136
render() {
3237
const attributes = this.props.attributes || {};
3338
const srcset = this.getSrcSet();
39+
40+
const { style: userStyle, ...restAttributes } = attributes;
41+
42+
const defaultStyle: React.CSSProperties = {
43+
objectFit: this.props.backgroundSize as React.CSSProperties['objectFit'],
44+
objectPosition: this.props.backgroundPosition as React.CSSProperties['objectPosition'],
45+
aspectRatio: this.props.aspectRatio as unknown as React.CSSProperties['aspectRatio'],
46+
};
47+
48+
const mergedStyle = {
49+
...defaultStyle,
50+
...(userStyle as React.CSSProperties),
51+
};
52+
3453
return (
3554
<img
3655
loading="lazy"
37-
{...this.props.attributes}
56+
{...restAttributes}
3857
src={this.props.image || attributes.src}
3958
srcSet={srcset}
59+
alt={this.props.altText}
60+
title={this.props.title}
61+
style={mergedStyle}
62+
className="builder-raw-img"
4063
/>
4164
);
4265
}
4366
}
4467

4568
export const Img = withBuilder(ImgComponent, {
46-
// friendlyName?
4769
name: 'Raw:Img',
4870
hideFromInsertMenu: true,
71+
4972
image:
5073
'https://firebasestorage.googleapis.com/v0/b/builder-3b0a2.appspot.com/o/images%2Fbaseline-insert_photo-24px.svg?alt=media&token=4e5d0ef4-f5e8-4e57-b3a9-38d63a9b9dc4',
5174
inputs: [
5275
{
53-
name: 'image',
76+
name: "image",
5477
bubble: true,
55-
type: 'file',
78+
type: "file",
5679
allowedFileTypes: IMAGE_FILE_TYPES,
5780
required: true,
5881
},
82+
{
83+
name: 'backgroundSize',
84+
type: 'text',
85+
defaultValue: 'cover',
86+
enum: [
87+
{
88+
label: 'contain',
89+
value: 'contain',
90+
helperText: 'The image should never get cropped',
91+
},
92+
{
93+
label: 'cover',
94+
value: 'cover',
95+
helperText: "The image should fill it's box, cropping when needed",
96+
},
97+
],
98+
},
99+
{
100+
name: 'backgroundPosition',
101+
type: 'text',
102+
defaultValue: 'center',
103+
enum: [
104+
'center',
105+
'top',
106+
'left',
107+
'right',
108+
'bottom',
109+
'top left',
110+
'top right',
111+
'bottom left',
112+
'bottom right',
113+
],
114+
},
115+
{
116+
name: 'altText',
117+
type: 'string',
118+
helperText: 'Text to display when the user has images off',
119+
},
120+
{
121+
name: 'title',
122+
type: 'string',
123+
helperText: 'Text to display when hovering over the asset',
124+
},
125+
{
126+
name: 'aspectRatio',
127+
type: 'number',
128+
helperText:
129+
"This is the ratio of height/width, e.g. set to 1.5 for a 300px wide and 200px tall photo. Set to 0 to not force the image to maintain it's aspect ratio",
130+
advanced: true,
131+
defaultValue: 0.7041,
132+
},
59133
],
134+
60135
noWrap: true,
61-
static: true,
62-
});
136+
});

packages/sdks-tests/src/e2e-tests/blocks.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,66 @@ test.describe('Blocks', () => {
258258
});
259259
});
260260

261+
test.describe('Raw:Img', () => {
262+
test('Image title attribute', async ({ page, sdk, packageName }) => {
263+
test.skip(checkIsRN(sdk));
264+
test.skip(
265+
isSSRFramework(packageName),
266+
'SSR frameworks get the images from the server so page.route intercept does not work'
267+
);
268+
const mockImgPath = path.join(mockFolderPath, 'placeholder-img.png');
269+
const mockImgBuffer = fs.readFileSync(mockImgPath);
270+
271+
await page.route('**/*', route => {
272+
const request = route.request();
273+
if (request.url().includes('cdn.builder.io/api/v1/image')) {
274+
return route.fulfill({
275+
status: 200,
276+
contentType: 'image/png',
277+
body: mockImgBuffer,
278+
});
279+
} else {
280+
return route.continue();
281+
}
282+
});
283+
284+
await page.goto('/raw-img');
285+
286+
const img = page.getByTitle('title test');
287+
await expect(img).toHaveAttribute('title', 'title test');
288+
});
289+
290+
test('aspect ratio is respected', async ({ page, sdk, packageName }) => {
291+
test.skip(checkIsRN(sdk));
292+
test.skip(
293+
isSSRFramework(packageName),
294+
'SSR frameworks fetch images server-side, so Playwright route interception fails'
295+
);
296+
297+
// mock any CDN image so we don't hit the network
298+
const mockImgPath = path.join(mockFolderPath, 'placeholder-img.png');
299+
const mockImgBuffer = fs.readFileSync(mockImgPath);
300+
await page.route('**/*', route => {
301+
const req = route.request();
302+
if (req.url().includes('cdn.builder.io/api/v1/image')) {
303+
return route.fulfill({ status: 200, contentType: 'image/png', body: mockImgBuffer });
304+
}
305+
return route.continue();
306+
});
307+
308+
await page.goto('/raw-img');
309+
310+
const rawImgs = page.locator('.builder-raw-img');
311+
const expected = ['1.11 / 1', '0.19 / 1', '0.2 / 1', 'auto'];
312+
await expect(rawImgs).toHaveCount(expected.length);
313+
314+
for (let i = 0; i < expected.length; i++) {
315+
const img = rawImgs.nth(i);
316+
await expect(img).toHaveCSS('aspect-ratio', expected[i]);
317+
}
318+
});
319+
});
320+
261321
test.describe('Video', () => {
262322
test('video render and styles', async ({ page, sdk }) => {
263323
test.skip(checkIsRN(sdk));

packages/sdks-tests/src/specs/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
CONTENT_2 as imageHighPriority,
2929
CONTENT_3 as imageNoWebp,
3030
} from './image.js';
31+
import { CONTENT as rawImg} from './raw-img.js';
3132
import { INPUT_DEFAULT_VALUE } from './input-default-value.js';
3233
import { JS_CODE_CONTENT } from './js-code.js';
3334
import { JS_CONTENT_IS_BROWSER } from './js-content-is-browser.js';
@@ -142,6 +143,7 @@ export const PAGES: Record<string, Page> = {
142143
'/content-bindings': { content: contentBindings },
143144
'/content-input-bindings': { content: contentInputBindings, isGen1VisualEditingTest: true },
144145
'/image': { content: image },
146+
'/raw-img': { content: rawImg },
145147
'/image-high-priority': { content: imageHighPriority },
146148
'/image-no-webp': { content: imageNoWebp },
147149
'/data-bindings': { content: dataBindings },

0 commit comments

Comments
 (0)