UNPKG

@ui5/webcomponents-react

Version:

React Wrapper for UI5 Web Components and additional components

180 lines (175 loc) 6.73 kB
'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] }); }); };