Files
twenty/packages/twenty-front-component-renderer/src/host/utils/createHtmlHostWrapper.ts
T
Raphaël BosiandGitHub 7ba5fe32f8 Add new html tags to the remote elements (#19723)
- Add 72 missing HTML and SVG elements to the remote-dom component
registry (48 HTML + 24 SVG), bringing the total from 47 to 119 supported
elements
- HTML additions include semantic inline text (b, i, u, s, mark, sub,
sup, kbd, etc.), description lists, ruby annotations, structural
elements (figure, details, dialog), and form utilities (fieldset,
progress, meter, optgroup)
- SVG additions include containers (svg, g, defs), shapes (path, circle,
rect, line, polygon), text (text, tspan), gradients (linearGradient,
radialGradient, stop), and utilities (clipPath, mask, foreignObject,
marker)
- Add htmlTag override to support SVG elements with camelCase names
(e.g. clipPath, foreignObject) while keeping custom element tags
lowercase per the Web Components spec
2026-04-15 14:53:23 +00:00

184 lines
5.9 KiB
TypeScript

import React from 'react';
import { EVENT_TO_REACT } from '@/constants/EventToReact';
import { type SerializedEventData } from '@/constants/SerializedEventData';
const INTERNAL_PROPS = new Set(['element', 'receiver', 'components']);
const EVENT_NAME_MAP: Record<string, string> = Object.fromEntries(
Object.entries(EVENT_TO_REACT).map(([domEvent, reactProp]) => [
`on${domEvent}`,
reactProp,
]),
);
const VOID_ELEMENTS = new Set([
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'link',
'meta',
'source',
'track',
'wbr',
]);
const parseCssString = (
styleString: string | undefined,
): React.CSSProperties | undefined => {
if (!styleString || typeof styleString !== 'string') {
return styleString as React.CSSProperties | undefined;
}
const style: Record<string, string> = {};
const declarations = styleString.split(';').filter(Boolean);
for (const declaration of declarations) {
const colonIndex = declaration.indexOf(':');
if (colonIndex === -1) continue;
const property = declaration.slice(0, colonIndex).trim();
const value = declaration.slice(colonIndex + 1).trim();
const isCssCustomProperty = property.startsWith('--');
const key = isCssCustomProperty
? property
: property.replace(/-([a-z])/g, (_, letter: string) =>
letter.toUpperCase(),
);
style[key] = value;
}
return style;
};
const serializeEvent = (event: unknown): SerializedEventData => {
if (!event || typeof event !== 'object') {
return { type: 'unknown' };
}
const domEvent = event as Record<string, unknown>;
const serialized: SerializedEventData = {
type: typeof domEvent.type === 'string' ? domEvent.type : 'unknown',
};
if ('altKey' in domEvent) serialized.altKey = domEvent.altKey as boolean;
if ('ctrlKey' in domEvent) serialized.ctrlKey = domEvent.ctrlKey as boolean;
if ('metaKey' in domEvent) serialized.metaKey = domEvent.metaKey as boolean;
if ('shiftKey' in domEvent)
serialized.shiftKey = domEvent.shiftKey as boolean;
if ('clientX' in domEvent) serialized.clientX = domEvent.clientX as number;
if ('clientY' in domEvent) serialized.clientY = domEvent.clientY as number;
if ('pageX' in domEvent) serialized.pageX = domEvent.pageX as number;
if ('pageY' in domEvent) serialized.pageY = domEvent.pageY as number;
if ('screenX' in domEvent) serialized.screenX = domEvent.screenX as number;
if ('screenY' in domEvent) serialized.screenY = domEvent.screenY as number;
if ('button' in domEvent) serialized.button = domEvent.button as number;
if ('buttons' in domEvent) serialized.buttons = domEvent.buttons as number;
if ('key' in domEvent) serialized.key = domEvent.key as string;
if ('code' in domEvent) serialized.code = domEvent.code as string;
if ('repeat' in domEvent) serialized.repeat = domEvent.repeat as boolean;
if ('deltaX' in domEvent) serialized.deltaX = domEvent.deltaX as number;
if ('deltaY' in domEvent) serialized.deltaY = domEvent.deltaY as number;
if ('deltaZ' in domEvent) serialized.deltaZ = domEvent.deltaZ as number;
if ('deltaMode' in domEvent)
serialized.deltaMode = domEvent.deltaMode as number;
const target = domEvent.target as Record<string, unknown> | undefined;
if (target && typeof target === 'object') {
if ('value' in target && typeof target.value === 'string') {
serialized.value = target.value;
}
if ('checked' in target && typeof target.checked === 'boolean') {
serialized.checked = target.checked;
}
if ('scrollTop' in target && typeof target.scrollTop === 'number') {
serialized.scrollTop = target.scrollTop;
}
if ('scrollLeft' in target && typeof target.scrollLeft === 'number') {
serialized.scrollLeft = target.scrollLeft;
}
if ('currentTime' in target && typeof target.currentTime === 'number') {
serialized.currentTime = target.currentTime;
}
if ('duration' in target && typeof target.duration === 'number') {
serialized.duration = target.duration;
}
if ('paused' in target && typeof target.paused === 'boolean') {
serialized.paused = target.paused;
}
if ('ended' in target && typeof target.ended === 'boolean') {
serialized.ended = target.ended;
}
if ('volume' in target && typeof target.volume === 'number') {
serialized.volume = target.volume;
}
if ('muted' in target && typeof target.muted === 'boolean') {
serialized.muted = target.muted;
}
if ('playbackRate' in target && typeof target.playbackRate === 'number') {
serialized.playbackRate = target.playbackRate;
}
}
return serialized;
};
const wrapEventHandler = (handler: (detail: SerializedEventData) => void) => {
return (event: unknown) => {
handler(serializeEvent(event));
};
};
const filterProps = <T extends object>(props: T): T => {
const filtered: Record<string, unknown> = {};
for (const [key, value] of Object.entries(props)) {
if (INTERNAL_PROPS.has(key) || value === undefined) continue;
if (key === 'style') {
filtered.style = parseCssString(value as string | undefined);
} else {
const normalizedKey = EVENT_NAME_MAP[key.toLowerCase()] || key;
if (normalizedKey.startsWith('on') && typeof value === 'function') {
filtered[normalizedKey] = wrapEventHandler(
value as (detail: SerializedEventData) => void,
);
} else {
filtered[normalizedKey] = value;
}
}
}
return filtered as T;
};
type WrapperProps = { children?: React.ReactNode } & Record<string, unknown>;
const FORCED_PROPS_BY_TAG: Record<string, Record<string, unknown>> = {
iframe: { sandbox: '' },
};
export const createHtmlHostWrapper = (htmlTag: string) => {
const isVoid = VOID_ELEMENTS.has(htmlTag);
const forcedProps = FORCED_PROPS_BY_TAG[htmlTag];
return ({ children, ...props }: WrapperProps) =>
React.createElement(
htmlTag,
{ ...filterProps(props), ...forcedProps },
isVoid ? undefined : children,
);
};