Selaa lähdekoodia

feat: render unknown supplied children to UI (#6096)

David Luzar 2 vuotta sitten
vanhempi
commit
0982da38fe
3 muutettua tiedostoa jossa 45 lisäystä ja 24 poistoa
  1. 7 7
      src/components/LayerUI.tsx
  2. 2 0
      src/packages/excalidraw/CHANGELOG.md
  3. 36 17
      src/utils.ts

+ 7 - 7
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<UIChildrenComponents>(children);
+  const [childrenComponents, restChildren] =
+    getReactChildren<UIChildrenComponents>(children, {
+      Menu: true,
+      FooterCenter: true,
+    });
 
   const renderJSONExportDialog = () => {
     if (!UIOptions.canvasActions.export) {
@@ -390,6 +389,7 @@ const LayerUI = ({
 
   return (
     <>
+      {restChildren}
       {appState.isLoading && <LoadingMessage delay={250} />}
       {appState.errorMessage && (
         <ErrorDialog

+ 2 - 0
src/packages/excalidraw/CHANGELOG.md

@@ -15,6 +15,8 @@ Please add the latest change on the top under the correct section.
 
 ### Features
 
+- Any top-level children passed to the `<Excalidraw/>` 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)

+ 36 - 17
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<unknown, string | React.JSXElementConstructor<any>>;
+/**
+ * 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<keyof KnownChildren, any>,
 ) => {
-  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<T>);
+  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<KnownChildren>,
+  );
+
+  return [knownChildren, restChildren] as const;
 };
 
 export const isShallowEqual = <T extends Record<string, any>>(