From a30e82accf3b537cee5f78c4aafd26413a2f4b82 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sat, 8 Aug 2026 18:35:12 -0700 Subject: [PATCH] fix(site-builder): HtmlBlock render stops applying style so editor matches published output Co-Authored-By: Claude Opus 5 (1M context) --- .../basic/HtmlBlock.render.test.tsx | 51 +++++++++++++++++++ .../components/basic/HtmlBlock.toHtml.test.ts | 10 ++++ craft/src/components/basic/HtmlBlock.tsx | 9 +++- 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 craft/src/components/basic/HtmlBlock.render.test.tsx diff --git a/craft/src/components/basic/HtmlBlock.render.test.tsx b/craft/src/components/basic/HtmlBlock.render.test.tsx new file mode 100644 index 0000000..36f776c --- /dev/null +++ b/craft/src/components/basic/HtmlBlock.render.test.tsx @@ -0,0 +1,51 @@ +import { describe, test, expect, vi } from 'vitest'; +import React from 'react'; +import { createRoot, Root } from 'react-dom/client'; +import { act } from 'react-dom/test-utils'; + +vi.mock('@craftjs/core', () => ({ + useNode: (collect?: (node: any) => any) => { + const node = { events: { selected: false } }; + return { + connectors: { connect: (el: any) => el, drag: (el: any) => el }, + actions: { setProp: vi.fn() }, + ...(collect ? collect(node) : {}), + }; + }, +})); + +import { HtmlBlock } from './HtmlBlock'; + +let container: HTMLDivElement; +let root: Root; + +function render(ui: React.ReactElement) { + container = document.createElement('div'); + document.body.appendChild(container); + act(() => { + root = createRoot(container); + root.render(ui); + }); +} + +describe('HtmlBlock render ignores the style prop (matches toHtml)', () => { + test('a stored backgroundColor/color/padding is NOT applied to the wrapper', () => { + render( + React.createElement(HtmlBlock as any, { + code: '

hello

', + style: { backgroundColor: 'rgb(255, 0, 0)', color: 'rgb(0, 0, 255)', padding: '40px' }, + }), + ); + const wrapper = container.firstElementChild as HTMLElement; + expect(wrapper.style.backgroundColor).toBe(''); + expect(wrapper.style.color).toBe(''); + expect(wrapper.style.padding).toBe(''); + }); + + test('the editor affordances survive: minHeight is kept, content still renders', () => { + render(React.createElement(HtmlBlock as any, { code: '

hello

', style: {} })); + const wrapper = container.firstElementChild as HTMLElement; + expect(wrapper.style.minHeight).toBe('40px'); + expect(wrapper.innerHTML).toContain('hello'); + }); +}); diff --git a/craft/src/components/basic/HtmlBlock.toHtml.test.ts b/craft/src/components/basic/HtmlBlock.toHtml.test.ts index 35e939e..dafc090 100644 --- a/craft/src/components/basic/HtmlBlock.toHtml.test.ts +++ b/craft/src/components/basic/HtmlBlock.toHtml.test.ts @@ -23,3 +23,13 @@ describe('HtmlBlock.toHtml sanitizes raw code (A4.1)', () => { expect(html).toBe('

hi

'); }); }); + +test('toHtml never emits the style prop (the other half of the render/export contract)', () => { + const out = (HtmlBlock as any).toHtml( + { code: '

hi

', style: { backgroundColor: '#ff0000', padding: '40px' } }, + '', + ); + expect(out.html).toBe('

hi

'); + expect(out.html).not.toContain('background'); + expect(out.html).not.toContain('40px'); +}); diff --git a/craft/src/components/basic/HtmlBlock.tsx b/craft/src/components/basic/HtmlBlock.tsx index 0f41cc4..367d7f2 100644 --- a/craft/src/components/basic/HtmlBlock.tsx +++ b/craft/src/components/basic/HtmlBlock.tsx @@ -62,16 +62,21 @@ export function purifyHtml(input: string): string { } } -export const HtmlBlock: UserComponent = ({ code = '', style = {} }) => { +export const HtmlBlock: UserComponent = ({ code = '' }) => { const { connectors: { connect, drag }, selected } = useNode((node) => ({ selected: node.events.selected })); const clean = useMemo(() => purifyHtml(code), [code]); const setRef = (ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }; + // The `style` prop is deliberately NOT applied. `toHtml()` emits only the + // purified `code`, so anything styled here would show in the editor and + // vanish on the live page -- the exact bug this block was reported for. + // Wrapper styling belongs in the user's own markup (see the Edit HTML + // toolbar's colour control). `style` stays on the props interface so + // already-saved sites keep deserializing cleanly. return React.createElement('div', { ref: setRef, style: { minHeight: '40px', outline: selected ? '2px solid #3b82f6' : 'none', - ...style, }, dangerouslySetInnerHTML: { __html: clean }, });