diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index 6185f1605b..78c540d506 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -49,7 +49,10 @@ class Portal extends React.PureComponent { private portalRef = React.createRef(); componentDidUpdate(prevProps: Readonly) { - if (prevProps.props.appearance !== this.props.props.appearance) { + if ( + prevProps.props.appearance !== this.props.props.appearance || + prevProps.props?.customPages?.length !== this.props.props?.customPages?.length + ) { this.props.updateProps({ node: this.portalRef.current, props: this.props.props }); } } diff --git a/packages/react/src/utils/useCustomElementPortal.tsx b/packages/react/src/utils/useCustomElementPortal.tsx index 5d031e7c8c..147dd2d8e0 100644 --- a/packages/react/src/utils/useCustomElementPortal.tsx +++ b/packages/react/src/utils/useCustomElementPortal.tsx @@ -1,24 +1,36 @@ import React, { useState } from 'react'; import { createPortal } from 'react-dom'; +export type UseCustomElementPortalParams = { + component: React.ReactNode; + id: number; +}; + +export type UseCustomElementPortalReturn = { + portal: () => JSX.Element; + mount: (node: Element) => void; + unmount: () => void; + id: number; +}; + // This function takes a component as prop, and returns functions that mount and unmount // the given component into a given node +export const useCustomElementPortal = (elements: UseCustomElementPortalParams[]) => { + const [nodes, setNodes] = useState<(Element | null)[]>(Array(elements.length).fill(null)); -export const useCustomElementPortal = (component: JSX.Element) => { - const [node, setNode] = useState(null); - - const mount = (node: Element) => { - setNode(node); - }; - const unmount = () => { - setNode(null); - }; + const portals: UseCustomElementPortalReturn[] = []; - // If mount has been called, CustomElementPortal returns a portal that renders `component` - // into the passed node + elements.forEach((el, index) => { + const mount = (node: Element) => { + setNodes(prevState => prevState.map((n, i) => (i === index ? node : n))); + }; + const unmount = () => { + setNodes(prevState => prevState.map((n, i) => (i === index ? null : n))); + }; - // Otherwise, CustomElementPortal returns nothing - const CustomElementPortal = () => <>{node ? createPortal(component, node) : null}; + const portal = () => <>{nodes[index] ? createPortal(el.component, nodes[index] as Element) : null}; + portals.push({ portal, mount, unmount, id: el.id }); + }); - return { CustomElementPortal, mount, unmount }; + return portals; }; diff --git a/packages/react/src/utils/useCustomPages.tsx b/packages/react/src/utils/useCustomPages.tsx index a1ec050b14..1b6096d519 100644 --- a/packages/react/src/utils/useCustomPages.tsx +++ b/packages/react/src/utils/useCustomPages.tsx @@ -5,6 +5,8 @@ import React from 'react'; import { UserProfileLink, UserProfilePage } from '../components/uiComponents'; import { customPagesIngoredComponent, userProfileLinkWrongProps, userProfilePageWrongProps } from '../errors'; +import type { UserProfilePageProps } from '../types'; +import type { UseCustomElementPortalParams, UseCustomElementPortalReturn } from './useCustomElementPortal'; import { useCustomElementPortal } from './useCustomElementPortal'; const errorInDevMode = (message: string) => { @@ -21,12 +23,16 @@ const isLinkComponent = (v: any): v is React.ReactNode => { return !!v && React.isValidElement(v) && (v as React.ReactElement)?.type === UserProfileLink; }; +type CustomPageWithIdType = UserProfilePageProps & { children?: React.ReactNode }; + export const useCustomPages = (userProfileChildren: React.ReactNode | React.ReactNode[]) => { - const customPages: CustomPage[] = []; - const customPagesPortals: React.ComponentType[] = []; + const validUserProfileChildren: CustomPageWithIdType[] = []; + React.Children.forEach(userProfileChildren, child => { if (!isPageComponent(child) && !isLinkComponent(child)) { - errorInDevMode(customPagesIngoredComponent); + if (child) { + errorInDevMode(customPagesIngoredComponent); + } return; } @@ -37,25 +43,10 @@ export const useCustomPages = (userProfileChildren: React.ReactNode | React.Reac if (isPageComponent(child)) { if (isReorderItem(props)) { // This is a reordering item - customPages.push({ label }); + validUserProfileChildren.push({ label }); } else if (isCustomPage(props)) { // this is a custom page - const { CustomElementPortal, mount, unmount } = useCustomElementPortal(children); - const { - CustomElementPortal: labelPortal, - mount: mountIcon, - unmount: unmountIcon, - } = useCustomElementPortal(labelIcon); - customPages.push({ - url, - label, - mountIcon, - unmountIcon, - mount, - unmount, - }); - customPagesPortals.push(CustomElementPortal); - customPagesPortals.push(labelPortal); + validUserProfileChildren.push({ label, labelIcon, children, url }); } else { errorInDevMode(userProfilePageWrongProps); return; @@ -65,13 +56,7 @@ export const useCustomPages = (userProfileChildren: React.ReactNode | React.Reac if (isLinkComponent(child)) { if (isExternalLink(props)) { // This is an external link - const { - CustomElementPortal: labelPortal, - mount: mountIcon, - unmount: unmountIcon, - } = useCustomElementPortal(labelIcon); - customPages.push({ label, url, mountIcon, unmountIcon }); - customPagesPortals.push(labelPortal); + validUserProfileChildren.push({ label, labelIcon, url }); } else { errorInDevMode(userProfileLinkWrongProps); return; @@ -79,6 +64,61 @@ export const useCustomPages = (userProfileChildren: React.ReactNode | React.Reac } }); + const customPageContents: UseCustomElementPortalParams[] = []; + const customPageLabelIcons: UseCustomElementPortalParams[] = []; + const customLinkLabelIcons: UseCustomElementPortalParams[] = []; + + validUserProfileChildren.forEach((cp, index) => { + if (isCustomPage(cp)) { + customPageContents.push({ component: cp.children, id: index }); + customPageLabelIcons.push({ component: cp.labelIcon, id: index }); + return; + } + if (isExternalLink(cp)) { + customLinkLabelIcons.push({ component: cp.labelIcon, id: index }); + } + }); + + const customPageContentsPortals = useCustomElementPortal(customPageContents); + const customPageLabelIconsPortals = useCustomElementPortal(customPageLabelIcons); + const customLinkLabelIconsPortals = useCustomElementPortal(customLinkLabelIcons); + + const customPages: CustomPage[] = []; + const customPagesPortals: React.ComponentType[] = []; + + validUserProfileChildren.forEach((cp, index) => { + if (isReorderItem(cp)) { + customPages.push({ label: cp.label }); + return; + } + if (isCustomPage(cp)) { + const { + portal: contentPortal, + mount, + unmount, + } = customPageContentsPortals.find(p => p.id === index) as UseCustomElementPortalReturn; + const { + portal: labelPortal, + mount: mountIcon, + unmount: unmountIcon, + } = customPageLabelIconsPortals.find(p => p.id === index) as UseCustomElementPortalReturn; + customPages.push({ label: cp.label, url: cp.url, mount, unmount, mountIcon, unmountIcon }); + customPagesPortals.push(contentPortal); + customPagesPortals.push(labelPortal); + return; + } + if (isExternalLink(cp)) { + const { + portal: labelPortal, + mount: mountIcon, + unmount: unmountIcon, + } = customLinkLabelIconsPortals.find(p => p.id === index) as UseCustomElementPortalReturn; + customPages.push({ label: cp.label, url: cp.url, mountIcon, unmountIcon }); + customPagesPortals.push(labelPortal); + return; + } + }); + return { customPages, customPagesPortals }; };