diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 47f344575..aacbef366 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -15,11 +15,7 @@ import { BinaryFiles, UIChildrenComponents, } from "../types"; -import { - isShallowEqual, - muteFSAbortError, - ReactChildrenToObject, -} from "../utils"; +import { isShallowEqual, muteFSAbortError, getReactChildren } from "../utils"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import CollabButton from "./CollabButton"; import { ErrorDialog } from "./ErrorDialog"; @@ -111,8 +107,11 @@ const LayerUI = ({ }: LayerUIProps) => { const device = useDevice(); - const childrenComponents = - ReactChildrenToObject(children); + const [childrenComponents, restChildren] = + getReactChildren(children, { + Menu: true, + FooterCenter: true, + }); const renderJSONExportDialog = () => { if (!UIOptions.canvasActions.export) { @@ -390,6 +389,7 @@ const LayerUI = ({ return ( <> + {restChildren} {appState.isLoading && } {appState.errorMessage && ( ` component that do not belong to one of the officially supported Excalidraw children components are now rendered directly inside the Excalidraw container (previously, they weren't rendered at all) [#6096](https://github.com/excalidraw/excalidraw/pull/6096). + - Expose component API for the Excalidraw main menu [#6034](https://github.com/excalidraw/excalidraw/pull/6034), You can read more about its usage [here](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#MainMenu) - Render Footer as a component instead of render prop [#5970](https://github.com/excalidraw/excalidraw/pull/5970). You can read more about its usage [here](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Footer) diff --git a/src/utils.ts b/src/utils.ts index 6f8010d92..d3d80560b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -687,27 +687,46 @@ export const queryFocusableElements = (container: HTMLElement | null) => { : []; }; -export const ReactChildrenToObject = < - T extends { - [k in string]?: - | React.ReactPortal - | React.ReactElement>; +/** + * Partitions React children into named components and the rest of children. + * + * Returns known children as a dictionary of react children keyed by their + * displayName, and the rest children as an array. + * + * NOTE all named react components are included in the dictionary, irrespective + * of the supplied type parameter. This means you may be throwing away + * children that you aren't expecting, but should nonetheless be rendered. + * To guard against this (provided you care about the rest children at all), + * supply a second parameter with an object with keys of the expected children. + */ +export const getReactChildren = < + KnownChildren extends { + [k in string]?: React.ReactNode; }, >( children: React.ReactNode, + expectedComponents?: Record, ) => { - return React.Children.toArray(children).reduce((acc, child) => { - if ( - React.isValidElement(child) && - typeof child.type !== "string" && - //@ts-ignore - child?.type.displayName - ) { - // @ts-ignore - acc[child.type.displayName] = child; - } - return acc; - }, {} as Partial); + const restChildren: React.ReactNode[] = []; + + const knownChildren = React.Children.toArray(children).reduce( + (acc, child) => { + if ( + React.isValidElement(child) && + (!expectedComponents || + ((child.type as any).displayName as string) in expectedComponents) + ) { + // @ts-ignore + acc[child.type.displayName] = child; + } else { + restChildren.push(child); + } + return acc; + }, + {} as Partial, + ); + + return [knownChildren, restChildren] as const; }; export const isShallowEqual = >(