Allow video, audio and iFrame in front components (#18200)

Allow video, audio and iFrame in front components

+ fix css style properties starting with `--`
This commit is contained in:
Raphaël Bosi
2026-02-24 19:04:52 +01:00
committed by GitHub
parent 1ed13182d0
commit b56f85f36a
6 changed files with 319 additions and 5 deletions
@@ -82,6 +82,16 @@ export const componentRegistry: Map<string, ComponentRegistryValue> = new Map([
['html-td', createRemoteComponentRenderer(createHtmlHostWrapper('td'))],
['html-br', createRemoteComponentRenderer(createHtmlHostWrapper('br'))],
['html-hr', createRemoteComponentRenderer(createHtmlHostWrapper('hr'))],
[
'html-iframe',
createRemoteComponentRenderer(createHtmlHostWrapper('iframe')),
],
['html-video', createRemoteComponentRenderer(createHtmlHostWrapper('video'))],
['html-audio', createRemoteComponentRenderer(createHtmlHostWrapper('audio'))],
[
'html-source',
createRemoteComponentRenderer(createHtmlHostWrapper('source')),
],
['remote-style', createRemoteComponentRenderer(RemoteStyleRenderer)],
['remote-fragment', RemoteFragmentRenderer],
]);
@@ -45,10 +45,15 @@ const parseCssString = (
const property = declaration.slice(0, colonIndex).trim();
const value = declaration.slice(colonIndex + 1).trim();
const camelProperty = property.replace(/-([a-z])/g, (_, letter: string) =>
letter.toUpperCase(),
);
style[camelProperty] = value;
const isCssCustomProperty = property.startsWith('--');
const key = isCssCustomProperty
? property
: property.replace(/-([a-z])/g, (_, letter: string) =>
letter.toUpperCase(),
);
style[key] = value;
}
return style;
@@ -140,13 +145,18 @@ const filterProps = <T extends object>(props: T): 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),
{ ...filterProps(props), ...forcedProps },
isVoid ? undefined : children,
);
};
@@ -24,7 +24,11 @@ export {
HtmlH6,
HtmlHeader,
HtmlHr,
HtmlIframe,
HtmlAudio,
HtmlImg,
HtmlSource,
HtmlVideo,
HtmlInput,
HtmlLabel,
HtmlLi,
@@ -69,7 +73,11 @@ export {
HtmlH6Element,
HtmlHeaderElement,
HtmlHrElement,
HtmlIframeElement,
HtmlAudioElement,
HtmlImgElement,
HtmlSourceElement,
HtmlVideoElement,
HtmlInputElement,
HtmlLabelElement,
HtmlLiElement,
@@ -102,7 +110,11 @@ export type {
HtmlCommonEvents,
HtmlCommonProperties,
HtmlFormProperties,
HtmlIframeProperties,
HtmlAudioProperties,
HtmlImgProperties,
HtmlSourceProperties,
HtmlVideoProperties,
HtmlInputProperties,
HtmlLabelProperties,
HtmlOptionProperties,
@@ -43,6 +43,10 @@ import {
HtmlTdElement,
HtmlBrElement,
HtmlHrElement,
HtmlIframeElement,
HtmlVideoElement,
HtmlAudioElement,
HtmlSourceElement,
RemoteStyleElement,
} from './remote-elements';
@@ -1118,6 +1122,110 @@ export const HtmlHr = createRemoteComponent('html-hr', HtmlHrElement, {
onDrag: { event: 'drag' },
},
});
export const HtmlIframe = createRemoteComponent(
'html-iframe',
HtmlIframeElement,
{
eventProps: {
onClick: { event: 'click' },
onDoubleClick: { event: 'dblclick' },
onMouseDown: { event: 'mousedown' },
onMouseUp: { event: 'mouseup' },
onMouseOver: { event: 'mouseover' },
onMouseOut: { event: 'mouseout' },
onMouseEnter: { event: 'mouseenter' },
onMouseLeave: { event: 'mouseleave' },
onKeyDown: { event: 'keydown' },
onKeyUp: { event: 'keyup' },
onKeyPress: { event: 'keypress' },
onFocus: { event: 'focus' },
onBlur: { event: 'blur' },
onChange: { event: 'change' },
onInput: { event: 'input' },
onSubmit: { event: 'submit' },
onScroll: { event: 'scroll' },
onWheel: { event: 'wheel' },
onContextMenu: { event: 'contextmenu' },
onDrag: { event: 'drag' },
},
},
);
export const HtmlVideo = createRemoteComponent('html-video', HtmlVideoElement, {
eventProps: {
onClick: { event: 'click' },
onDoubleClick: { event: 'dblclick' },
onMouseDown: { event: 'mousedown' },
onMouseUp: { event: 'mouseup' },
onMouseOver: { event: 'mouseover' },
onMouseOut: { event: 'mouseout' },
onMouseEnter: { event: 'mouseenter' },
onMouseLeave: { event: 'mouseleave' },
onKeyDown: { event: 'keydown' },
onKeyUp: { event: 'keyup' },
onKeyPress: { event: 'keypress' },
onFocus: { event: 'focus' },
onBlur: { event: 'blur' },
onChange: { event: 'change' },
onInput: { event: 'input' },
onSubmit: { event: 'submit' },
onScroll: { event: 'scroll' },
onWheel: { event: 'wheel' },
onContextMenu: { event: 'contextmenu' },
onDrag: { event: 'drag' },
},
});
export const HtmlAudio = createRemoteComponent('html-audio', HtmlAudioElement, {
eventProps: {
onClick: { event: 'click' },
onDoubleClick: { event: 'dblclick' },
onMouseDown: { event: 'mousedown' },
onMouseUp: { event: 'mouseup' },
onMouseOver: { event: 'mouseover' },
onMouseOut: { event: 'mouseout' },
onMouseEnter: { event: 'mouseenter' },
onMouseLeave: { event: 'mouseleave' },
onKeyDown: { event: 'keydown' },
onKeyUp: { event: 'keyup' },
onKeyPress: { event: 'keypress' },
onFocus: { event: 'focus' },
onBlur: { event: 'blur' },
onChange: { event: 'change' },
onInput: { event: 'input' },
onSubmit: { event: 'submit' },
onScroll: { event: 'scroll' },
onWheel: { event: 'wheel' },
onContextMenu: { event: 'contextmenu' },
onDrag: { event: 'drag' },
},
});
export const HtmlSource = createRemoteComponent(
'html-source',
HtmlSourceElement,
{
eventProps: {
onClick: { event: 'click' },
onDoubleClick: { event: 'dblclick' },
onMouseDown: { event: 'mousedown' },
onMouseUp: { event: 'mouseup' },
onMouseOver: { event: 'mouseover' },
onMouseOut: { event: 'mouseout' },
onMouseEnter: { event: 'mouseenter' },
onMouseLeave: { event: 'mouseleave' },
onKeyDown: { event: 'keydown' },
onKeyUp: { event: 'keyup' },
onKeyPress: { event: 'keypress' },
onFocus: { event: 'focus' },
onBlur: { event: 'blur' },
onChange: { event: 'change' },
onInput: { event: 'input' },
onSubmit: { event: 'submit' },
onScroll: { event: 'scroll' },
onWheel: { event: 'wheel' },
onContextMenu: { event: 'contextmenu' },
onDrag: { event: 'drag' },
},
},
);
export const RemoteStyle = createRemoteComponent(
'remote-style',
RemoteStyleElement,
@@ -601,6 +601,120 @@ export const HtmlHrElement = createRemoteElement<
events: [...HTML_COMMON_EVENTS_ARRAY],
});
export type HtmlIframeProperties = HtmlCommonProperties & {
src?: string;
width?: string;
height?: string;
allow?: string;
sandbox?: string;
allowFullScreen?: boolean;
};
export const HtmlIframeElement = createRemoteElement<
HtmlIframeProperties,
Record<string, never>,
Record<string, never>,
HtmlCommonEvents
>({
properties: {
...HTML_COMMON_PROPERTIES_CONFIG,
src: { type: String },
width: { type: String },
height: { type: String },
allow: { type: String },
sandbox: { type: String },
allowFullScreen: { type: Boolean },
},
events: [...HTML_COMMON_EVENTS_ARRAY],
});
export type HtmlVideoProperties = HtmlCommonProperties & {
src?: string;
poster?: string;
controls?: boolean;
autoPlay?: boolean;
loop?: boolean;
muted?: boolean;
preload?: string;
width?: string;
height?: string;
crossOrigin?: string;
playsInline?: boolean;
};
export const HtmlVideoElement = createRemoteElement<
HtmlVideoProperties,
Record<string, never>,
Record<string, never>,
HtmlCommonEvents
>({
properties: {
...HTML_COMMON_PROPERTIES_CONFIG,
src: { type: String },
poster: { type: String },
controls: { type: Boolean },
autoPlay: { type: Boolean },
loop: { type: Boolean },
muted: { type: Boolean },
preload: { type: String },
width: { type: String },
height: { type: String },
crossOrigin: { type: String },
playsInline: { type: Boolean },
},
events: [...HTML_COMMON_EVENTS_ARRAY],
});
export type HtmlAudioProperties = HtmlCommonProperties & {
src?: string;
controls?: boolean;
autoPlay?: boolean;
loop?: boolean;
muted?: boolean;
preload?: string;
crossOrigin?: string;
};
export const HtmlAudioElement = createRemoteElement<
HtmlAudioProperties,
Record<string, never>,
Record<string, never>,
HtmlCommonEvents
>({
properties: {
...HTML_COMMON_PROPERTIES_CONFIG,
src: { type: String },
controls: { type: Boolean },
autoPlay: { type: Boolean },
loop: { type: Boolean },
muted: { type: Boolean },
preload: { type: String },
crossOrigin: { type: String },
},
events: [...HTML_COMMON_EVENTS_ARRAY],
});
export type HtmlSourceProperties = HtmlCommonProperties & {
src?: string;
type?: string;
media?: string;
};
export const HtmlSourceElement = createRemoteElement<
HtmlSourceProperties,
Record<string, never>,
Record<string, never>,
HtmlCommonEvents
>({
properties: {
...HTML_COMMON_PROPERTIES_CONFIG,
src: { type: String },
type: { type: String },
media: { type: String },
},
events: [...HTML_COMMON_EVENTS_ARRAY],
});
export type RemoteStyleProperties = {
cssText?: string;
styleKey?: string;
@@ -660,6 +774,10 @@ customElements.define('html-th', HtmlThElement);
customElements.define('html-td', HtmlTdElement);
customElements.define('html-br', HtmlBrElement);
customElements.define('html-hr', HtmlHrElement);
customElements.define('html-iframe', HtmlIframeElement);
customElements.define('html-video', HtmlVideoElement);
customElements.define('html-audio', HtmlAudioElement);
customElements.define('html-source', HtmlSourceElement);
customElements.define('remote-style', RemoteStyleElement);
customElements.define('remote-root', RemoteRootElement);
customElements.define('remote-fragment', RemoteFragmentElement);
@@ -709,6 +827,10 @@ declare global {
'html-td': InstanceType<typeof HtmlTdElement>;
'html-br': InstanceType<typeof HtmlBrElement>;
'html-hr': InstanceType<typeof HtmlHrElement>;
'html-iframe': InstanceType<typeof HtmlIframeElement>;
'html-video': InstanceType<typeof HtmlVideoElement>;
'html-audio': InstanceType<typeof HtmlAudioElement>;
'html-source': InstanceType<typeof HtmlSourceElement>;
'remote-style': InstanceType<typeof RemoteStyleElement>;
'remote-root': InstanceType<typeof RemoteRootElement>;
'remote-fragment': InstanceType<typeof RemoteFragmentElement>;
@@ -145,4 +145,56 @@ export const ALLOWED_HTML_ELEMENTS: AllowedHtmlElement[] = [
},
{ tag: 'html-br', name: 'HtmlBr', properties: {} },
{ tag: 'html-hr', name: 'HtmlHr', properties: {} },
{
tag: 'html-iframe',
name: 'HtmlIframe',
properties: {
src: { type: 'string', optional: true },
title: { type: 'string', optional: true },
width: { type: 'string', optional: true },
height: { type: 'string', optional: true },
allow: { type: 'string', optional: true },
sandbox: { type: 'string', optional: true },
allowFullScreen: { type: 'boolean', optional: true },
},
},
{
tag: 'html-video',
name: 'HtmlVideo',
properties: {
src: { type: 'string', optional: true },
poster: { type: 'string', optional: true },
controls: { type: 'boolean', optional: true },
autoPlay: { type: 'boolean', optional: true },
loop: { type: 'boolean', optional: true },
muted: { type: 'boolean', optional: true },
preload: { type: 'string', optional: true },
width: { type: 'string', optional: true },
height: { type: 'string', optional: true },
crossOrigin: { type: 'string', optional: true },
playsInline: { type: 'boolean', optional: true },
},
},
{
tag: 'html-audio',
name: 'HtmlAudio',
properties: {
src: { type: 'string', optional: true },
controls: { type: 'boolean', optional: true },
autoPlay: { type: 'boolean', optional: true },
loop: { type: 'boolean', optional: true },
muted: { type: 'boolean', optional: true },
preload: { type: 'string', optional: true },
crossOrigin: { type: 'string', optional: true },
},
},
{
tag: 'html-source',
name: 'HtmlSource',
properties: {
src: { type: 'string', optional: true },
type: { type: 'string', optional: true },
media: { type: 'string', optional: true },
},
},
];