Skip to content

Commit 7d8eef4

Browse files
committed
docs(website): Introduce React code editor and preview (#13183)
- Adds a live React code editor (Monaco) alongside the existing HTML playground for every component sample, letting users view, edit, and preview UI5 Web Components as React/TSX code with full TypeScript intellisense - Introduces createComponent in @ui5/webcomponents-base — a lightweight React wrapper factory that documentation samples use to wrap UI5 Web Component classes into typed React components - Generates ~380 React sample files (sample.tsx) covering main, fiori, ai, compat, and patterns packages https://github.com/user-attachments/assets/c4619908-607d-4d00-ac2d-421b41ca1993 #### What's included - **ReactPlayground.tsx** — Monaco editor + live preview with Babel transpilation, error boundary, theme/density/direction support - **monaco-ui5-types.d.ts** — Auto-generated TypeScript definitions for all components (props, events, slots) powering Monaco autocomplete - **generate-monaco-types.mjs** — Script to regenerate Monaco types from component .d.ts files - **createComponent.tsx** — Typed React wrapper factory (@ui5/webcomponents-base/dist/createComponent.js, not in barrel export) - HTML/React toggle in the Editor toolbar to switch between views - React >=18 version label in the editor tab bar #### Technical notes - **createComponent** is NOT in the barrel export to avoid forcing a React dependency on non-React consumers - Samples import via direct path: @ui5/webcomponents-base/dist/createComponent.js - React is an optional peer dependency of @ui5/webcomponents-base
1 parent 2642c2e commit 7d8eef4

772 files changed

Lines changed: 28395 additions & 409 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/base/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,13 @@
7676
"typescript": "^5.6.2",
7777
"vite": "5.4.21"
7878
},
79+
"peerDependencies": {
80+
"react": ">=18"
81+
},
82+
"peerDependenciesMeta": {
83+
"react": {
84+
"optional": true
85+
}
86+
},
7987
"customElements": "dist/custom-elements.json"
8088
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* React wrapper factory for UI5 Web Components.
3+
*
4+
* This lightweight factory creates typed React components that wrap UI5 Web Components.
5+
* It handles:
6+
* - Event prop conversion (onXxx → ui5-xxx event listeners)
7+
* - Ref forwarding
8+
* - Children handling
9+
*
10+
* Note: This is for documentation samples only - for production React apps,
11+
* use the official @ui5/webcomponents-react library.
12+
*/
13+
14+
import * as React from "react";
15+
import type { ReactNode } from "react";
16+
import {
17+
useRef,
18+
useEffect,
19+
forwardRef,
20+
} from "react";
21+
import type UI5Element from "./UI5Element.js";
22+
23+
type EventHandler<E = Event> = (event: E) => void;
24+
25+
// Interface for UI5 Web Component classes with _jsxProps support
26+
interface UI5ComponentClass<T extends UI5Element = UI5Element> {
27+
new (): T;
28+
getMetadata(): {
29+
getTag(): string;
30+
};
31+
}
32+
33+
// Helper to convert event name
34+
const toEventName = (propName: string): string => {
35+
return propName
36+
.slice(2) // Remove "on"
37+
.replace(/([A-Z])/g, (match: string, letter: string, index: number): string => {
38+
return index === 0 ? letter.toLowerCase() : `-${letter.toLowerCase()}`;
39+
});
40+
};
41+
42+
// Helper to create cleanup function for event listener
43+
const createEventCleanup = (element: UI5Element, eventName: string, handler: EventHandler): (() => void) => {
44+
element.addEventListener(eventName, handler);
45+
return () => element.removeEventListener(eventName, handler);
46+
};
47+
48+
/**
49+
* Creates a React component wrapper for a UI5 Web Component.
50+
* Uses the component's _jsxProps type for full TypeScript support.
51+
*
52+
* @param ComponentClass - The UI5 Web Component class (e.g., Button from "@ui5/webcomponents/dist/Button.js")
53+
* @returns A React component that renders the custom element with proper TypeScript types
54+
*
55+
* @example
56+
* import Button from "@ui5/webcomponents/dist/Button.js";
57+
* const ReactButton = createComponent(Button);
58+
* // ReactButton props are typed based on Button's _jsxProps
59+
*/
60+
export function createComponent<T extends UI5Element>(
61+
ComponentClass: UI5ComponentClass<T>,
62+
): React.ForwardRefExoticComponent<
63+
React.PropsWithoutRef<T["_jsxProps"] & { children?: ReactNode }> & React.RefAttributes<T>
64+
> {
65+
const tagName = ComponentClass.getMetadata().getTag();
66+
67+
const Component = forwardRef<T, T["_jsxProps"] & { children?: ReactNode }>((props, ref) => {
68+
const { children, ...restProps } = props;
69+
const elementRef = useRef<T>(null);
70+
71+
// Forward ref
72+
useEffect(() => {
73+
if (ref) {
74+
if (typeof ref === "function") {
75+
ref(elementRef.current);
76+
} else {
77+
ref.current = elementRef.current;
78+
}
79+
}
80+
}, [ref]);
81+
82+
// Handle event props and boolean props imperatively
83+
useEffect(() => {
84+
const element = elementRef.current;
85+
if (!element) {
86+
return;
87+
}
88+
89+
const eventCleanups: Array<() => void> = [];
90+
91+
Object.keys(restProps).forEach(propName => {
92+
const propValue = (restProps as Record<string, unknown>)[propName];
93+
if (propName.startsWith("on") && typeof propValue === "function") {
94+
// Convert React event naming (onClick, onSelectionChange) to DOM event naming
95+
// onClick -> click, onSelectionChange -> selection-change
96+
const eventName = toEventName(propName);
97+
const handler = propValue as EventHandler;
98+
eventCleanups.push(createEventCleanup(element, eventName, handler));
99+
} else if (typeof propValue === "boolean") {
100+
// React 18 sets false booleans as empty string attributes on custom elements.
101+
// Set as property directly to avoid this.
102+
(element as any)[propName] = propValue;
103+
}
104+
});
105+
106+
return () => {
107+
eventCleanups.forEach(cleanup => cleanup());
108+
};
109+
}, [restProps]);
110+
111+
// Filter out event handlers and booleans from DOM props
112+
const domProps: Record<string, unknown> = {};
113+
Object.keys(restProps).forEach(propName => {
114+
const propValue = (restProps as Record<string, unknown>)[propName];
115+
if (propName.startsWith("on") && typeof propValue === "function") { return; }
116+
if (typeof propValue === "boolean") { return; } // handled in useEffect
117+
// className → class for React compatibility
118+
if (propName === "className") {
119+
// eslint-disable-next-line dot-notation
120+
domProps["class"] = propValue;
121+
return;
122+
}
123+
// Convert camelCase to kebab-case for HTML attributes
124+
const attrName = propName.replace(/([A-Z])/g, "-$1").toLowerCase();
125+
domProps[attrName] = propValue;
126+
});
127+
128+
return React.createElement(tagName, { ref: elementRef, ...domProps }, children);
129+
});
130+
131+
Component.displayName = tagName
132+
.split("-")
133+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
134+
.join("");
135+
136+
return Component;
137+
}
138+
139+
export default createComponent;
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import html from '!!raw-loader!./sample.html';
22
import js from '!!raw-loader!./main.js';
3+
import react from '!!raw-loader!./sample.tsx';
34

4-
<Editor html={html} js={js} />
5+
<Editor html={html} js={js} react={react} />
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { useState, useRef, useCallback, useEffect } from "react";
2+
import { createComponent } from "@ui5/webcomponents-base/dist/createComponent.js";
3+
import { type UI5CustomEvent } from "@ui5/webcomponents-base";
4+
import AIButtonClass from "@ui5/webcomponents-ai/dist/Button.js";
5+
import AIButtonStateClass from "@ui5/webcomponents-ai/dist/ButtonState.js";
6+
import MenuClass from "@ui5/webcomponents/dist/Menu.js";
7+
import MenuItemClass from "@ui5/webcomponents/dist/MenuItem.js";
8+
import MenuSeparatorClass from "@ui5/webcomponents/dist/MenuSeparator.js";
9+
import "@ui5/webcomponents-icons/dist/ai.js";
10+
import "@ui5/webcomponents-icons/dist/stop.js";
11+
import "@ui5/webcomponents-icons/dist/navigation-down-arrow.js";
12+
13+
const AIButton = createComponent(AIButtonClass);
14+
const AIButtonState = createComponent(AIButtonStateClass);
15+
const Menu = createComponent(MenuClass);
16+
const MenuItem = createComponent(MenuItemClass);
17+
const MenuSeparator = createComponent(MenuSeparatorClass);
18+
19+
function App() {
20+
const [buttonState, setButtonState] = useState("generate");
21+
const [menuOpen, setMenuOpen] = useState(false);
22+
const buttonRef = useRef(null);
23+
const menuRef = useRef(null);
24+
const generationIdRef = useRef<ReturnType<typeof setTimeout> | null>(null);
25+
26+
useEffect(() => {
27+
return () => {
28+
if (generationIdRef.current) {
29+
clearTimeout(generationIdRef.current);
30+
}
31+
};
32+
}, []);
33+
34+
const startGeneration = useCallback(() => {
35+
generationIdRef.current = setTimeout(() => {
36+
setButtonState("revise");
37+
const btn = buttonRef.current;
38+
if (btn) {
39+
btn.accessibilityAttributes = {
40+
root: {
41+
hasPopup: "menu",
42+
roleDescription: "Menu Button",
43+
},
44+
};
45+
}
46+
}, 3000);
47+
}, []);
48+
49+
const stopGeneration = useCallback(() => {
50+
if (generationIdRef.current) {
51+
clearTimeout(generationIdRef.current);
52+
generationIdRef.current = null;
53+
}
54+
const btn = buttonRef.current;
55+
if (btn) {
56+
btn.accessibilityAttributes = {
57+
root: {
58+
hasPopup: "false",
59+
},
60+
};
61+
}
62+
}, []);
63+
64+
const handleButtonClick = useCallback(() => {
65+
switch (buttonState) {
66+
case "generate":
67+
setButtonState("generating");
68+
startGeneration();
69+
break;
70+
case "generating":
71+
setButtonState("generate");
72+
stopGeneration();
73+
break;
74+
case "revise":
75+
if (menuRef.current && buttonRef.current) {
76+
menuRef.current!.opener = buttonRef.current;
77+
menuRef.current!.open = true;
78+
setMenuOpen(true);
79+
}
80+
break;
81+
}
82+
}, [buttonState, startGeneration, stopGeneration]);
83+
84+
const handleMenuItemClick = useCallback((e: UI5CustomEvent<MenuClass, "item-click">) => {
85+
if (e.detail.text === "Regenerate") {
86+
setButtonState("generating");
87+
startGeneration();
88+
}
89+
}, [startGeneration]);
90+
91+
return (
92+
<>
93+
<AIButton ref={buttonRef} id="myAiButton" state={buttonState} onClick={handleButtonClick}>
94+
<AIButtonState name="generate" text="Generate" icon="ai" />
95+
<AIButtonState name="generating" text="Stop Generating" icon="stop" />
96+
<AIButtonState name="revise" text="Revise" icon="ai" endIcon="navigation-down-arrow" />
97+
</AIButton>
98+
99+
<Menu ref={menuRef} id="menu" onItemClick={handleMenuItemClick}>
100+
<MenuItem text="Regenerate" />
101+
<MenuSeparator />
102+
<MenuItem text="Fix Spelling & Grammar" />
103+
<MenuItem text="Change Tone">
104+
<MenuItem text="Option 1" />
105+
<MenuItem text="Option 2" />
106+
<MenuItem text="Option 3" />
107+
</MenuItem>
108+
<MenuItem text="Adjust Length">
109+
<MenuItem text="Shorten text" />
110+
<MenuItem text="Lengthen text" />
111+
</MenuItem>
112+
<MenuItem text="Bulleted List" />
113+
<MenuItem text="Translate">
114+
<MenuItem text="English" />
115+
<MenuItem text="German" />
116+
<MenuItem text="Spanish" />
117+
</MenuItem>
118+
</Menu>
119+
</>
120+
);
121+
}
122+
123+
export default App;
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import html from '!!raw-loader!./sample.html';
22
import js from '!!raw-loader!./main.js';
3+
import react from '!!raw-loader!./sample.tsx';
34

4-
<Editor html={html} js={js} />
5+
<Editor html={html} js={js} react={react} />

0 commit comments

Comments
 (0)