From c48e8bb10e5c754f1275f064d3dc9faa9687c642 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sat, 17 May 2025 01:37:04 +0300 Subject: [PATCH] fix: normalize html attributes only when not defined by component --- .../props-section/props-section.stories.tsx | 4 +- .../features/settings-panel/shared.tsx | 38 ++++++++-------- apps/builder/app/canvas/canvas.tsx | 7 +-- .../webstudio-component.tsx | 16 +++---- .../app/shared/nano-states/components.ts | 21 --------- apps/builder/app/shared/sync/sync-stores.ts | 5 --- .../src/component-generator.test.tsx | 45 ++++++++++++++++++- packages/react-sdk/src/component-generator.ts | 8 ++-- packages/sdk/src/core-metas.ts | 39 ++-------------- packages/sdk/src/schema/component-meta.ts | 10 ----- 10 files changed, 81 insertions(+), 112 deletions(-) diff --git a/apps/builder/app/builder/features/settings-panel/props-section/props-section.stories.tsx b/apps/builder/app/builder/features/settings-panel/props-section/props-section.stories.tsx index ebb6bd8cb757..31ba8e8d668a 100644 --- a/apps/builder/app/builder/features/settings-panel/props-section/props-section.stories.tsx +++ b/apps/builder/app/builder/features/settings-panel/props-section/props-section.stories.tsx @@ -199,10 +199,10 @@ const checkProp = (options = defaultOptions, label?: string): PropMeta => ({ registerComponentLibrary({ components: {}, - metas: {}, templates: {}, - propsMetas: { + metas: { Box: { + icon: "", props: { initialText: textProp("", "multi\nline"), initialShortText: shortTextProp(), diff --git a/apps/builder/app/builder/features/settings-panel/shared.tsx b/apps/builder/app/builder/features/settings-panel/shared.tsx index 466b0c5fab2c..dacf43ea637d 100644 --- a/apps/builder/app/builder/features/settings-panel/shared.tsx +++ b/apps/builder/app/builder/features/settings-panel/shared.tsx @@ -39,7 +39,7 @@ import { import { $dataSourceVariables, $dataSources, - $registeredComponentPropsMetas, + $registeredComponentMetas, $variableValuesByInstanceSelector, } from "~/shared/nano-states"; import type { BindingVariant } from "~/builder/shared/binding-popover"; @@ -455,62 +455,60 @@ const attributeToMeta = (attribute: Attribute): PropMeta => { }; export const $selectedInstancePropsMetas = computed( - [$selectedInstance, $registeredComponentPropsMetas, $instanceTags], - (instance, componentPropsMetas, instanceTags): Map => { + [$selectedInstance, $registeredComponentMetas, $instanceTags], + (instance, metas, instanceTags): Map => { if (instance === undefined) { return new Map(); } - const propsMetas = componentPropsMetas.get(instance.component)?.props ?? {}; + const meta = metas.get(instance.component); const tag = instanceTags.get(instance.id); - const metas = new Map(); + const propsMetas = new Map(); // add html attributes only when instance has tag if (tag) { for (const attribute of [...ariaAttributes].reverse()) { - metas.set(attribute.name, attributeToMeta(attribute)); + propsMetas.set(attribute.name, attributeToMeta(attribute)); } if (attributesByTag["*"]) { for (const attribute of [...attributesByTag["*"]].reverse()) { - metas.set(attribute.name, attributeToMeta(attribute)); + propsMetas.set(attribute.name, attributeToMeta(attribute)); } } if (attributesByTag[tag]) { for (const attribute of [...attributesByTag[tag]].reverse()) { - metas.set(attribute.name, attributeToMeta(attribute)); + propsMetas.set(attribute.name, attributeToMeta(attribute)); } } } - for (const [name, propMeta] of Object.entries(propsMetas).reverse()) { + for (const [name, propMeta] of Object.entries( + meta?.props ?? {} + ).reverse()) { // when component property has the same name as html attribute in react // override to deduplicate similar properties // for example component can have own "className" and html has "class" const htmlName = reactPropsToStandardAttributes[name]; if (htmlName) { - metas.delete(htmlName); + propsMetas.delete(htmlName); } - metas.set(name, propMeta); + propsMetas.set(name, propMeta); } - metas.set(showAttribute, showAttributeMeta); + propsMetas.set(showAttribute, showAttributeMeta); // ui should render in the following order // 1. system properties // 2. component properties // 3. specific tag attributes // 4. global html attributes // 5. aria attributes - return new Map(Array.from(metas.entries()).reverse()); + return new Map(Array.from(propsMetas.entries()).reverse()); } ); export const $selectedInstanceInitialPropNames = computed( - [ - $selectedInstance, - $registeredComponentPropsMetas, - $selectedInstancePropsMetas, - ], - (selectedInstance, componentPropsMetas, instancePropsMetas) => { + [$selectedInstance, $registeredComponentMetas, $selectedInstancePropsMetas], + (selectedInstance, metas, instancePropsMetas) => { const initialPropNames = new Set(); if (selectedInstance) { const initialProps = - componentPropsMetas.get(selectedInstance.component)?.initialProps ?? []; + metas.get(selectedInstance.component)?.initialProps ?? []; for (const propName of initialProps) { // className -> class if (instancePropsMetas.has(reactPropsToStandardAttributes[propName])) { diff --git a/apps/builder/app/canvas/canvas.tsx b/apps/builder/app/canvas/canvas.tsx index ee9d7e6ddbea..0db362e372e2 100644 --- a/apps/builder/app/canvas/canvas.tsx +++ b/apps/builder/app/canvas/canvas.tsx @@ -1,7 +1,7 @@ import { useMemo, useEffect, useState, useLayoutEffect, useRef } from "react"; import { ErrorBoundary, type FallbackProps } from "react-error-boundary"; import { useStore } from "@nanostores/react"; -import { type Instances, coreMetas, corePropsMetas } from "@webstudio-is/sdk"; +import { type Instances, coreMetas } from "@webstudio-is/sdk"; import { coreTemplates } from "@webstudio-is/sdk/core-templates"; import type { Components } from "@webstudio-is/react-sdk"; import { wsImageLoader } from "@webstudio-is/image"; @@ -240,13 +240,11 @@ export const Canvas = () => { registerComponentLibrary({ components: {}, metas: coreMetas, - propsMetas: corePropsMetas, templates: coreTemplates, }); registerComponentLibrary({ components: baseComponents, metas: baseComponentMetas, - propsMetas: {}, hooks: baseComponentHooks, templates: baseComponentTemplates, }); @@ -257,14 +255,12 @@ export const Canvas = () => { Body, }, metas: {}, - propsMetas: {}, templates: {}, }); registerComponentLibrary({ namespace: "@webstudio-is/sdk-components-react-radix", components: radixComponents, metas: radixComponentMetas, - propsMetas: {}, hooks: radixComponentHooks, templates: radixTemplates, }); @@ -272,7 +268,6 @@ export const Canvas = () => { namespace: "@webstudio-is/sdk-components-animation", components: animationComponents, metas: animationComponentMetas, - propsMetas: {}, hooks: animationComponentHooks, templates: animationTemplates, }); diff --git a/apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx b/apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx index a1870961708e..060f711d402a 100644 --- a/apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx +++ b/apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx @@ -301,9 +301,8 @@ const useInstanceProps = (instanceSelector: InstanceSelector) => { if (tag !== undefined) { instancePropsObject[tagProperty] = tag; } - const hasTags = - Object.keys(metas.get(instance?.component ?? "")?.presetStyle ?? {}) - .length > 0; + const meta = metas.get(instance?.component ?? ""); + const hasTags = Object.keys(meta?.presetStyle ?? {}).length > 0; const index = indexesWithinAncestors.get(instanceId); if (index !== undefined) { instancePropsObject[indexProperty] = index.toString(); @@ -311,12 +310,13 @@ const useInstanceProps = (instanceSelector: InstanceSelector) => { const instanceProps = propValuesByInstanceSelector.get(instanceKey); if (instanceProps) { for (const [name, value] of instanceProps) { - if (hasTags) { - const reactName = standardAttributesToReactProps[name] ?? name; - instancePropsObject[reactName] = value; - } else { - instancePropsObject[name] = value; + let propName = name; + // convert html attribute only when component has tags + // and does not specify own property with this name + if (hasTags && !meta?.props?.[propName]) { + propName = standardAttributesToReactProps[propName] ?? propName; } + instancePropsObject[propName] = value; } } return instancePropsObject; diff --git a/apps/builder/app/shared/nano-states/components.ts b/apps/builder/app/shared/nano-states/components.ts index f4ea5136e121..2572ae0ece48 100644 --- a/apps/builder/app/shared/nano-states/components.ts +++ b/apps/builder/app/shared/nano-states/components.ts @@ -12,7 +12,6 @@ import { getIndexesWithinAncestors, type Instance, type WsComponentMeta, - type WsComponentPropsMeta, } from "@webstudio-is/sdk"; import type { InstanceSelector } from "../tree-utils"; import { $memoryProps, $props } from "./misc"; @@ -176,15 +175,10 @@ export const $registeredTemplates = atom( new Map() ); -export const $registeredComponentPropsMetas = atom( - new Map() -); - export const registerComponentLibrary = ({ namespace, components, metas, - propsMetas, hooks, templates, }: { @@ -193,7 +187,6 @@ export const registerComponentLibrary = ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any components: Record>; metas: Record; - propsMetas: Record; hooks?: Hook[]; templates: Record; }) => { @@ -206,22 +199,10 @@ export const registerComponentLibrary = ({ } $registeredComponents.set(nextComponents); - const prevPropsMetas = $registeredComponentPropsMetas.get(); - const nextPropsMetas = new Map(prevPropsMetas); - for (const [componentName, propsMeta] of Object.entries(propsMetas)) { - nextPropsMetas.set(`${prefix}${componentName}`, propsMeta); - } - const prevMetas = $registeredComponentMetas.get(); const nextMetas = new Map(prevMetas); for (const [componentName, meta] of Object.entries(metas)) { nextMetas.set(`${prefix}${componentName}`, meta); - if (meta.initialProps || meta.props) { - nextPropsMetas.set(`${prefix}${componentName}`, { - initialProps: meta.initialProps, - props: meta.props ?? {}, - }); - } } $registeredComponentMetas.set(nextMetas); @@ -241,6 +222,4 @@ export const registerComponentLibrary = ({ const nextHooks = [...prevHooks, ...hooks]; $registeredComponentHooks.set(nextHooks); } - - $registeredComponentPropsMetas.set(nextPropsMetas); }; diff --git a/apps/builder/app/shared/sync/sync-stores.ts b/apps/builder/app/shared/sync/sync-stores.ts index d2e8919ad77c..c202f6c465ee 100644 --- a/apps/builder/app/shared/sync/sync-stores.ts +++ b/apps/builder/app/shared/sync/sync-stores.ts @@ -43,7 +43,6 @@ import { $blockChildOutline, $textToolbar, $registeredComponentMetas, - $registeredComponentPropsMetas, $registeredTemplates, $modifierKeys, } from "~/shared/nano-states"; @@ -147,10 +146,6 @@ export const createObjectPool = () => { "registeredComponentMetas", $registeredComponentMetas ), - new NanostoresSyncObject( - "registeredComponentPropsMetas", - $registeredComponentPropsMetas - ), new NanostoresSyncObject("registeredTemplates", $registeredTemplates), new NanostoresSyncObject("canvasScrollbarWidth", $canvasScrollbarSize), new NanostoresSyncObject("systemDataByPage", $systemDataByPage), diff --git a/packages/react-sdk/src/component-generator.test.tsx b/packages/react-sdk/src/component-generator.test.tsx index 677bd100f766..738bf43024e5 100644 --- a/packages/react-sdk/src/component-generator.test.tsx +++ b/packages/react-sdk/src/component-generator.test.tsx @@ -3,6 +3,7 @@ import { expect, test } from "vitest"; import stripIndent from "strip-indent"; import { createScope, + elementComponent, ROOT_INSTANCE_ID, SYSTEM_VARIABLE_ID, WsComponentMeta, @@ -1386,7 +1387,9 @@ test("convert attributes to react compatible when render ws:element", () => { name: "Page", rootInstanceId: "bodyId", parameters: [], - metas: new Map(), + metas: new Map([ + [elementComponent, { icon: "", presetStyle: { div: [] } }], + ]), ...renderData( <$.Body ws:id="bodyId"> { + expect( + generateWebstudioComponent({ + classesMap: new Map(), + scope: createScope(), + name: "Page", + rootInstanceId: "bodyId", + parameters: [], + metas: new Map([ + [ + "Vimeo", + { + icon: "", + presetStyle: { div: [] }, + props: { + autoplay: { type: "boolean", control: "boolean", required: true }, + }, + }, + ], + ]), + ...renderData( + <$.Body ws:id="bodyId"> + <$.Vimeo autoplay={true}> + + ), + }) + ).toEqual( + validateJSX( + clear(` + const Page = () => { + return + + + } + `) + ) + ); +}); + test("ignore props similar to standard attributes on react components without tags", () => { expect( generateWebstudioComponent({ diff --git a/packages/react-sdk/src/component-generator.ts b/packages/react-sdk/src/component-generator.ts index 9462c2e1adcf..a2c009990f18 100644 --- a/packages/react-sdk/src/component-generator.ts +++ b/packages/react-sdk/src/component-generator.ts @@ -170,8 +170,8 @@ export const generateJsxElement = ({ return ""; } - const hasTags = - Object.keys(metas.get(instance.component)?.presetStyle ?? {}).length > 0; + const meta = metas.get(instance.component); + const hasTags = Object.keys(meta?.presetStyle ?? {}).length > 0; let generatedProps = ""; @@ -204,7 +204,9 @@ export const generateJsxElement = ({ continue; } let name = prop.name; - if (instance.component === elementComponent || hasTags) { + // convert html attribute only when component has tags + // and does not specify own property with this name + if (hasTags && !meta?.props?.[prop.name]) { name = standardAttributesToReactProps[prop.name] ?? prop.name; } diff --git a/packages/sdk/src/core-metas.ts b/packages/sdk/src/core-metas.ts index 9f9bd0da8069..4438ff50d1b1 100644 --- a/packages/sdk/src/core-metas.ts +++ b/packages/sdk/src/core-metas.ts @@ -8,10 +8,7 @@ import { } from "@webstudio-is/icons/svg"; import { html } from "./__generated__/normalize.css"; import * as normalize from "./__generated__/normalize.css"; -import type { - WsComponentMeta, - WsComponentPropsMeta, -} from "./schema/component-meta"; +import type { WsComponentMeta } from "./schema/component-meta"; import type { Instance } from "./schema/instances"; import { tagProperty } from "./runtime"; import { tags } from "./__generated__/tags"; @@ -26,10 +23,6 @@ const rootMeta: WsComponentMeta = { }, }; -const rootPropsMeta: WsComponentPropsMeta = { - props: {}, -}; - export const elementComponent = "ws:element"; const elementMeta: WsComponentMeta = { @@ -37,9 +30,6 @@ const elementMeta: WsComponentMeta = { icon: BoxIcon, // convert [object Module] to [object Object] to enable structured cloning presetStyle: { ...normalize }, -}; - -const elementPropsMeta: WsComponentPropsMeta = { initialProps: [tagProperty, "id", "class"], props: { [tagProperty]: { @@ -62,9 +52,7 @@ const collectionMeta: WsComponentMeta = { category: "instance", children: ["instance"], }, -}; - -const collectionPropsMeta: WsComponentPropsMeta = { + initialProps: ["data"], props: { data: { required: true, @@ -72,7 +60,6 @@ const collectionPropsMeta: WsComponentPropsMeta = { type: "json", }, }, - initialProps: ["data"], }; export const descendantComponent = "ws:descendant"; @@ -86,9 +73,7 @@ const descendantMeta: WsComponentMeta = { }, // @todo infer possible presets presetStyle: {}, -}; - -const descendantPropsMeta: WsComponentPropsMeta = { + initialProps: ["selector"], props: { selector: { required: true, @@ -114,7 +99,6 @@ const descendantPropsMeta: WsComponentPropsMeta = { ], }, }, - initialProps: ["selector"], }; export const blockComponent = "ws:block"; @@ -129,10 +113,6 @@ export const blockTemplateMeta: WsComponentMeta = { }, }; -const blockTemplatePropsMeta: WsComponentPropsMeta = { - props: {}, -}; - const blockMeta: WsComponentMeta = { label: "Content Block", icon: ContentBlockIcon, @@ -142,10 +122,6 @@ const blockMeta: WsComponentMeta = { }, }; -const blockPropsMeta: WsComponentPropsMeta = { - props: {}, -}; - export const coreMetas = { [rootComponent]: rootMeta, [elementComponent]: elementMeta, @@ -155,15 +131,6 @@ export const coreMetas = { [blockTemplateComponent]: blockTemplateMeta, }; -export const corePropsMetas = { - [rootComponent]: rootPropsMeta, - [elementComponent]: elementPropsMeta, - [collectionComponent]: collectionPropsMeta, - [descendantComponent]: descendantPropsMeta, - [blockComponent]: blockPropsMeta, - [blockTemplateComponent]: blockTemplatePropsMeta, -}; - // components with custom implementation // should not be imported as react component export const isCoreComponent = (component: Instance["component"]) => diff --git a/packages/sdk/src/schema/component-meta.ts b/packages/sdk/src/schema/component-meta.ts index 9995577d9a35..52cfa29c9ba5 100644 --- a/packages/sdk/src/schema/component-meta.ts +++ b/packages/sdk/src/schema/component-meta.ts @@ -21,16 +21,6 @@ export type PresetStyle = Partial< Record >; -// props are separated from the rest of the meta -// so they can be exported separately and potentially tree-shaken -const WsComponentPropsMeta = z.object({ - props: z.record(PropMeta), - // Props that will be always visible in properties panel. - initialProps: z.array(z.string()).optional(), -}); - -export type WsComponentPropsMeta = z.infer; - export const componentCategories = [ "general", "typography",