From d2049ec3670e2faab35c74a3d0981ee8d2ac41c0 Mon Sep 17 00:00:00 2001
From: Ben Life <77246839+benlife5@users.noreply.github.com>
Date: Tue, 22 Oct 2024 17:16:54 -0400
Subject: [PATCH] fix: sidebar loses focus on input (#100)

creates a ref for the themeHistory state, which allows access to the
latest values inside the callback without regenerating the components

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
---
 .../components/InternalThemeEditor.tsx        | 34 ++++++---
 src/internal/components/ThemeEditor.tsx       |  4 +-
 .../puck/components/ColorSelector.tsx         | 75 ++++++++-----------
 src/internal/puck/components/ThemeSidebar.tsx | 16 +++-
 .../utils/constructThemePuckFields.ts         | 13 +++-
 src/utils/devLogger.ts                        |  2 +-
 6 files changed, 79 insertions(+), 65 deletions(-)

diff --git a/src/internal/components/InternalThemeEditor.tsx b/src/internal/components/InternalThemeEditor.tsx
index 278c638e..49ea4aec 100644
--- a/src/internal/components/InternalThemeEditor.tsx
+++ b/src/internal/components/InternalThemeEditor.tsx
@@ -1,5 +1,5 @@
 import { Puck, Config, InitialHistory } from "@measured/puck";
-import React from "react";
+import React, { useCallback, useEffect, useRef } from "react";
 import { useState } from "react";
 import { TemplateMetadata } from "../types/templateMetadata.ts";
 import { EntityFieldProvider } from "../../components/EntityField.tsx";
@@ -45,6 +45,11 @@ export const InternalThemeEditor = ({
   buildThemeLocalStorageKey,
 }: InternalThemeEditorProps) => {
   const [canEdit, setCanEdit] = useState<boolean>(false); // helps sync puck preview and save state
+  const themeHistoriesRef = useRef(themeHistories);
+
+  useEffect(() => {
+    themeHistoriesRef.current = themeHistories;
+  }, [themeHistories]);
 
   const handlePublishTheme = async () => {
     devLogger.logFunc("saveThemeData");
@@ -69,21 +74,22 @@ export const InternalThemeEditor = ({
   };
 
   const handleThemeChange = (topLevelKey: string, newValue: any) => {
-    if (!themeHistories || !themeConfig) {
+    if (!themeHistoriesRef.current || !themeConfig) {
       return;
     }
 
     const newThemeValues = {
-      ...themeHistories.histories[themeHistories.index]?.data,
+      ...themeHistoriesRef.current.histories[themeHistoriesRef.current.index]
+        ?.data,
       ...generateCssVariablesFromPuckFields(newValue, topLevelKey),
     };
 
     const newHistory = {
       histories: [
-        ...themeHistories.histories,
+        ...themeHistoriesRef.current.histories,
         { id: uuidv4(), data: newThemeValues },
       ] as ThemeHistory[],
-      index: themeHistories.histories.length,
+      index: themeHistoriesRef.current.histories.length,
     };
 
     window.localStorage.setItem(
@@ -124,6 +130,16 @@ export const InternalThemeEditor = ({
     }
   };
 
+  const fieldsOverride = useCallback(() => {
+    return (
+      <ThemeSidebar
+        themeHistoriesRef={themeHistoriesRef}
+        themeConfig={themeConfig}
+        onThemeChange={handleThemeChange}
+      />
+    );
+  }, []);
+
   return (
     <EntityFieldProvider>
       <Puck
@@ -152,13 +168,7 @@ export const InternalThemeEditor = ({
           ),
           actionBar: () => <></>,
           components: () => <></>,
-          fields: () => (
-            <ThemeSidebar
-              themeConfig={themeConfig}
-              themeHistory={themeHistories!.histories}
-              onThemeChange={handleThemeChange}
-            />
-          ),
+          fields: fieldsOverride,
         }}
       />
     </EntityFieldProvider>
diff --git a/src/internal/components/ThemeEditor.tsx b/src/internal/components/ThemeEditor.tsx
index 6301f8da..e3f66623 100644
--- a/src/internal/components/ThemeEditor.tsx
+++ b/src/internal/components/ThemeEditor.tsx
@@ -182,9 +182,9 @@ export const ThemeEditor = (props: ThemeEditorProps) => {
     buildThemeLocalStorageKey,
   ]);
 
-  // Log THEME_INITIAL_HISTORY on load and update theme in editor to reflect save state
+  // Log THEME_HISTORIES on load and update theme in editor to reflect save state
   useEffect(() => {
-    devLogger.logData("THEME_INITIAL_HISTORY", themeHistories);
+    devLogger.logData("THEME_HISTORIES", themeHistories);
     if (themeHistories && themeConfig) {
       updateThemeInEditor(
         themeHistories.histories[themeHistories.index]?.data as SavedTheme,
diff --git a/src/internal/puck/components/ColorSelector.tsx b/src/internal/puck/components/ColorSelector.tsx
index b78eb882..e6c2c63c 100644
--- a/src/internal/puck/components/ColorSelector.tsx
+++ b/src/internal/puck/components/ColorSelector.tsx
@@ -1,52 +1,39 @@
 import React, { useState } from "react";
-import { Field, FieldLabel } from "@measured/puck";
+import { FieldLabel } from "@measured/puck";
 import { RenderProps } from "../../utils/renderEntityFields.ts";
 import { Color, ColorResult, SketchPicker } from "react-color";
 
-export type ColorSelectorProps = {
-  label: string;
-};
-
-export const ColorSelector = (props: ColorSelectorProps): Field => {
-  return {
-    type: "custom",
-    label: props.label,
-    render: ({ field, value, onChange }: RenderProps) => {
-      const [isOpen, setIsOpen] = useState(false);
+export const ColorSelector = ({ field, value, onChange }: RenderProps) => {
+  const [isOpen, setIsOpen] = useState(false);
 
-      const fieldStyles = colorPickerStyles(value);
-      return (
-        <>
-          <FieldLabel
-            label={field.label || "Label is undefined"}
-            className="ve-mt-2.5"
-          >
-            <div
-              style={fieldStyles.swatch}
-              onClick={() => setIsOpen((current) => !current)}
-            >
-              <div style={fieldStyles.color} />
-            </div>
-            {isOpen && (
-              <div style={fieldStyles.popover}>
-                <div
-                  style={fieldStyles.cover}
-                  onClick={() => setIsOpen(false)}
-                />
-                <SketchPicker
-                  disableAlpha={true}
-                  color={value}
-                  onChange={(colorResult: ColorResult) => {
-                    onChange(colorResult.hex);
-                  }}
-                />
-              </div>
-            )}
-          </FieldLabel>
-        </>
-      );
-    },
-  };
+  const fieldStyles = colorPickerStyles(value);
+  return (
+    <>
+      <FieldLabel
+        label={field.label || "Label is undefined"}
+        className="ve-mt-2.5"
+      >
+        <div
+          style={fieldStyles.swatch}
+          onClick={() => setIsOpen((current) => !current)}
+        >
+          <div style={fieldStyles.color} />
+        </div>
+        {isOpen && (
+          <div style={fieldStyles.popover}>
+            <div style={fieldStyles.cover} onClick={() => setIsOpen(false)} />
+            <SketchPicker
+              disableAlpha={true}
+              color={value}
+              onChange={(colorResult: ColorResult) => {
+                onChange(colorResult.hex);
+              }}
+            />
+          </div>
+        )}
+      </FieldLabel>
+    </>
+  );
 };
 
 const colorPickerStyles = (color: Color) => {
diff --git a/src/internal/puck/components/ThemeSidebar.tsx b/src/internal/puck/components/ThemeSidebar.tsx
index 6409cd8d..b04ead59 100644
--- a/src/internal/puck/components/ThemeSidebar.tsx
+++ b/src/internal/puck/components/ThemeSidebar.tsx
@@ -6,16 +6,21 @@ import {
   constructThemePuckFields,
   constructThemePuckValues,
 } from "../../utils/constructThemePuckFields.ts";
-import { ThemeHistory } from "../../types/themeData.ts";
+import { ThemeHistories } from "../../types/themeData.ts";
 
 type ThemeSidebarProps = {
+  themeHistoriesRef: React.MutableRefObject<ThemeHistories | undefined>;
   themeConfig?: ThemeConfig;
-  themeHistory: ThemeHistory[];
   onThemeChange: (parentStyleKey: string, value: Record<string, any>) => void;
 };
 
 const ThemeSidebar = (props: ThemeSidebarProps) => {
-  const { themeConfig, themeHistory, onThemeChange } = props;
+  const { themeConfig, themeHistoriesRef, onThemeChange } = props;
+
+  if (!themeHistoriesRef.current) {
+    return;
+  }
+
   if (!themeConfig) {
     return (
       <div>
@@ -28,6 +33,9 @@ const ThemeSidebar = (props: ThemeSidebarProps) => {
     );
   }
 
+  const themeData =
+    themeHistoriesRef.current?.histories[themeHistoriesRef.current?.index].data;
+
   return (
     <div>
       <Alert>
@@ -39,7 +47,7 @@ const ThemeSidebar = (props: ThemeSidebarProps) => {
       {Object.entries(themeConfig).map(([parentStyleKey, parentStyle]) => {
         const field = constructThemePuckFields(parentStyle);
         const values = constructThemePuckValues(
-          themeHistory[themeHistory.length - 1]?.data,
+          themeData,
           parentStyle,
           parentStyleKey
         );
diff --git a/src/internal/utils/constructThemePuckFields.ts b/src/internal/utils/constructThemePuckFields.ts
index d72f220a..b3b5311a 100644
--- a/src/internal/utils/constructThemePuckFields.ts
+++ b/src/internal/utils/constructThemePuckFields.ts
@@ -1,4 +1,9 @@
-import { ObjectField, SelectField, NumberField } from "@measured/puck";
+import {
+  ObjectField,
+  SelectField,
+  NumberField,
+  CustomField,
+} from "@measured/puck";
 import { ParentStyle, SavedTheme, Style } from "../../utils/themeResolver.ts";
 import { ColorSelector } from "../puck/components/ColorSelector.tsx";
 
@@ -36,7 +41,11 @@ export const convertStyleToPuckField = (style: Style) => {
         options: style.options,
       } as SelectField;
     case "color":
-      return ColorSelector({ label: style.label });
+      return {
+        label: style.label,
+        type: "custom",
+        render: ColorSelector,
+      } as CustomField;
   }
 };
 
diff --git a/src/utils/devLogger.ts b/src/utils/devLogger.ts
index 1f276c5f..309bd2f7 100644
--- a/src/utils/devLogger.ts
+++ b/src/utils/devLogger.ts
@@ -5,7 +5,7 @@ export type DevLoggerPrefix =
   | "THEME_SAVE_STATE"
   | "VISUAL_CONFIGURATION_DATA"
   | "THEME_DATA"
-  | "THEME_INITIAL_HISTORY"
+  | "THEME_HISTORIES"
   | "DOCUMENT"
   | "PUCK_INDEX"
   | "PUCK_HISTORY"