@ui5/webcomponents-react
Version:
React Wrapper for UI5 Web Components and additional components
180 lines (175 loc) • 6.73 kB
JavaScript
'use client';
import { getEffectiveScopingSuffixForTag } from '@ui5/webcomponents-base/dist/CustomElementsScope.js';
import { useIsomorphicLayoutEffect, useSyncRef } from '@ui5/webcomponents-react-base';
import { cloneElement, forwardRef, Fragment, isValidElement, useEffect, useState, version } from 'react';
import { camelToKebabCase, capitalizeFirstLetter, kebabToCamelCase, parseSemVer } from './utils.js';
import { jsxs as _jsxs } from "react/jsx-runtime";
const createEventPropName = eventName => `on${capitalizeFirstLetter(kebabToCamelCase(eventName))}`;
const isPrimitiveAttribute = value => {
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
};
const definedWebComponents = new Set([]);
export const withWebComponent = (tagName, regularProperties, booleanProperties, slotProperties, eventProperties) => {
const webComponentsSupported = parseSemVer(version).major >= 19;
// displayName will be assigned in the individual files
// eslint-disable-next-line react/display-name
return /*#__PURE__*/forwardRef((props, wcRef) => {
const {
className,
children,
waitForDefine,
...rest
} = props;
const [componentRef, ref] = useSyncRef(wcRef);
const tagNameSuffix = getEffectiveScopingSuffixForTag(tagName);
const Component = tagNameSuffix ? `${tagName}-${tagNameSuffix}` : tagName;
const [isDefined, setIsDefined] = useState(definedWebComponents.has(Component));
// regular props (no booleans, no slots and no events)
const regularProps = regularProperties.reduce((acc, name) => {
if (rest.hasOwnProperty(name) && isPrimitiveAttribute(rest[name])) {
return {
...acc,
[camelToKebabCase(name)]: rest[name]
};
}
return acc;
}, {});
// boolean properties - only attach if they are truthy
const booleanProps = booleanProperties.reduce((acc, name) => {
if (webComponentsSupported) {
return {
...acc,
[camelToKebabCase(name)]: rest[name]
};
} else {
if (rest[name] === true || rest[name] === 'true') {
return {
...acc,
[camelToKebabCase(name)]: true
};
}
return acc;
}
}, {});
const slots = slotProperties.reduce((acc, name) => {
const slotValue = rest[name];
if (!slotValue) {
return acc;
}
if (rest[name]?.$$typeof === Symbol.for('react.portal')) {
console.warn('ReactPortal is not supported for slot props.');
return acc;
}
const slottedChildren = [];
let index = 0;
const removeFragments = element => {
if (! /*#__PURE__*/isValidElement(element)) return;
if (element.type === Fragment) {
const elementChildren = element.props?.children;
if (Array.isArray(elementChildren)) {
elementChildren.forEach(item => {
if (Array.isArray(item)) {
item.forEach(removeFragments);
} else {
removeFragments(item);
}
});
} else {
removeFragments(elementChildren);
}
} else {
slottedChildren.push( /*#__PURE__*/cloneElement(element, {
key: element.key ?? `${name}-${index}`,
slot: name
}));
index++;
}
};
if (Array.isArray(slotValue)) {
slotValue.forEach(item => {
removeFragments(item);
});
} else {
removeFragments(slotValue);
}
return [...acc, ...slottedChildren];
}, []);
// event binding
useIsomorphicLayoutEffect(() => {
if (webComponentsSupported) {
return () => {
// React can handle events
};
}
const localRef = ref.current;
const eventRegistry = {};
if (!waitForDefine || isDefined) {
eventProperties.forEach(eventName => {
const eventHandler = rest[createEventPropName(eventName)];
if (typeof eventHandler === 'function') {
eventRegistry[eventName] = eventHandler;
// @ts-expect-error: all custom events can be passed here, so `keyof HTMLElementEventMap` isn't sufficient
localRef?.addEventListener(eventName, eventRegistry[eventName]);
}
});
return () => {
for (const eventName in eventRegistry) {
// @ts-expect-error: all custom events can be passed here, so `keyof HTMLElementEventMap` isn't sufficient
localRef?.removeEventListener(eventName, eventRegistry[eventName]);
}
};
}
}, [...eventProperties.map(eventName => rest[createEventPropName(eventName)]), isDefined, waitForDefine]);
const eventHandlers = eventProperties.reduce((events, eventName) => {
const eventHandlerProp = rest[createEventPropName(eventName)];
if (webComponentsSupported && eventHandlerProp) {
events[`on${eventName}`] = eventHandlerProp;
}
return events;
}, {});
// non web component related props, just pass them
const nonWebComponentRelatedProps = Object.entries(rest).filter(([key]) => !regularProperties.includes(key)).filter(([key]) => !slotProperties.includes(key)).filter(([key]) => !booleanProperties.includes(key)).filter(([key]) => !eventProperties.map(eventName => createEventPropName(eventName)).includes(key)).reduce((acc, [key, val]) => {
if (!key.startsWith('aria-') && !key.startsWith('data-') && val === false) {
return acc;
}
acc[key] = val;
return acc;
}, {});
useEffect(() => {
if (waitForDefine && !isDefined) {
customElements.whenDefined(Component).then(() => {
setIsDefined(true);
definedWebComponents.add(Component);
});
}
}, [Component, waitForDefine, isDefined]);
const propsToApply = regularProperties.map(prop => ({
name: prop,
value: props[prop]
}));
useEffect(() => {
void customElements.whenDefined(Component).then(() => {
for (const prop of propsToApply) {
if (prop.value != null && !isPrimitiveAttribute(prop.value)) {
if (ref.current) {
ref.current[prop.name] = prop.value;
}
}
}
});
}, [Component, ...propsToApply]);
if (waitForDefine && !isDefined) {
return null;
}
return /*#__PURE__*/_jsxs(Component, {
ref: componentRef,
...booleanProps,
...regularProps,
...eventHandlers,
...nonWebComponentRelatedProps,
class: className,
suppressHydrationWarning: true,
children: [slots, children]
});
});
};