Explorar el Código

feat: show empty active color if no common color (#9506)

David Luzar hace 3 meses
padre
commit
c4c064982f

+ 4 - 2
packages/excalidraw/actions/actionProperties.tsx

@@ -333,7 +333,8 @@ export const actionChangeStrokeColor = register({
           app,
           (element) => element.strokeColor,
           true,
-          appState.currentItemStrokeColor,
+          (hasSelection) =>
+            !hasSelection ? appState.currentItemStrokeColor : null,
         )}
         onChange={(color) => updateData({ currentItemStrokeColor: color })}
         elements={elements}
@@ -379,7 +380,8 @@ export const actionChangeBackgroundColor = register({
           app,
           (element) => element.backgroundColor,
           true,
-          appState.currentItemBackgroundColor,
+          (hasSelection) =>
+            !hasSelection ? appState.currentItemBackgroundColor : null,
         )}
         onChange={(color) => updateData({ currentItemBackgroundColor: color })}
         elements={elements}

+ 3 - 0
packages/excalidraw/components/ColorPicker/ColorInput.tsx

@@ -19,6 +19,7 @@ interface ColorInputProps {
   onChange: (color: string) => void;
   label: string;
   colorPickerType: ColorPickerType;
+  placeholder?: string;
 }
 
 export const ColorInput = ({
@@ -26,6 +27,7 @@ export const ColorInput = ({
   onChange,
   label,
   colorPickerType,
+  placeholder,
 }: ColorInputProps) => {
   const device = useDevice();
   const [innerValue, setInnerValue] = useState(color);
@@ -93,6 +95,7 @@ export const ColorInput = ({
           }
           event.stopPropagation();
         }}
+        placeholder={placeholder}
       />
       {/* TODO reenable on mobile with a better UX */}
       {!device.editor.isMobile && (

+ 11 - 0
packages/excalidraw/components/ColorPicker/ColorPicker.scss

@@ -69,6 +69,17 @@
       }
     }
 
+    .color-picker__button-outline {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      svg {
+        color: var(--color-gray-60);
+        width: 1.25rem;
+        height: 1.25rem;
+      }
+    }
+
     &.active {
       .color-picker__button-outline {
         position: absolute;

+ 16 - 11
packages/excalidraw/components/ColorPicker/ColorPicker.tsx

@@ -18,6 +18,7 @@ import { useExcalidrawContainer } from "../App";
 import { ButtonSeparator } from "../ButtonSeparator";
 import { activeEyeDropperAtom } from "../EyeDropper";
 import { PropertiesPopover } from "../PropertiesPopover";
+import { slashIcon } from "../icons";
 
 import { ColorInput } from "./ColorInput";
 import { Picker } from "./Picker";
@@ -54,7 +55,11 @@ export const getColor = (color: string): string | null => {
 
 interface ColorPickerProps {
   type: ColorPickerType;
-  color: string;
+  /**
+   * null indicates no color should be displayed as active
+   * (e.g. when multiple shapes selected with different colors)
+   */
+  color: string | null;
   onChange: (color: string) => void;
   label: string;
   elements: readonly ExcalidrawElement[];
@@ -91,22 +96,21 @@ const ColorPickerPopupContent = ({
     <div>
       <PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
       <ColorInput
-        color={color}
+        color={color || ""}
         label={label}
         onChange={(color) => {
           onChange(color);
         }}
         colorPickerType={type}
+        placeholder={t("colorPicker.color")}
       />
     </div>
   );
 
-  const popoverRef = useRef<HTMLDivElement>(null);
+  const colorPickerContentRef = useRef<HTMLDivElement>(null);
 
   const focusPickerContent = () => {
-    popoverRef.current
-      ?.querySelector<HTMLDivElement>(".color-picker-content")
-      ?.focus();
+    colorPickerContentRef.current?.focus();
   };
 
   return (
@@ -133,6 +137,7 @@ const ColorPickerPopupContent = ({
     >
       {palette ? (
         <Picker
+          ref={colorPickerContentRef}
           palette={palette}
           color={color}
           onChange={(changedColor) => {
@@ -166,7 +171,6 @@ const ColorPickerPopupContent = ({
               updateData({ openPopup: null });
             }
           }}
-          label={label}
           type={type}
           elements={elements}
           updateData={updateData}
@@ -185,7 +189,7 @@ const ColorPickerTrigger = ({
   color,
   type,
 }: {
-  color: string;
+  color: string | null;
   label: string;
   type: ColorPickerType;
 }) => {
@@ -193,8 +197,9 @@ const ColorPickerTrigger = ({
     <Popover.Trigger
       type="button"
       className={clsx("color-picker__button active-color properties-trigger", {
-        "is-transparent": color === "transparent" || !color,
-        "has-outline": !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
+        "is-transparent": !color || color === "transparent",
+        "has-outline":
+          !color || !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
       })}
       aria-label={label}
       style={color ? { "--swatch-color": color } : undefined}
@@ -204,7 +209,7 @@ const ColorPickerTrigger = ({
           : t("labels.showBackground")
       }
     >
-      <div className="color-picker__button-outline" />
+      <div className="color-picker__button-outline">{!color && slashIcon}</div>
     </Popover.Trigger>
   );
 };

+ 1 - 1
packages/excalidraw/components/ColorPicker/CustomColorList.tsx

@@ -8,7 +8,7 @@ import { activeColorPickerSectionAtom } from "./colorPickerUtils";
 
 interface CustomColorListProps {
   colors: string[];
-  color: string;
+  color: string | null;
   onChange: (color: string) => void;
   label: string;
 }

+ 140 - 133
packages/excalidraw/components/ColorPicker/Picker.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useImperativeHandle, useState } from "react";
 
 import { EVENT } from "@excalidraw/common";
 
@@ -30,9 +30,8 @@ import { colorPickerKeyNavHandler } from "./keyboardNavHandlers";
 import type { ColorPickerType } from "./colorPickerUtils";
 
 interface PickerProps {
-  color: string;
+  color: string | null;
   onChange: (color: string) => void;
-  label: string;
   type: ColorPickerType;
   elements: readonly ExcalidrawElement[];
   palette: ColorPaletteCustom;
@@ -42,142 +41,150 @@ interface PickerProps {
   onEscape: (event: React.KeyboardEvent | KeyboardEvent) => void;
 }
 
-export const Picker = ({
-  color,
-  onChange,
-  label,
-  type,
-  elements,
-  palette,
-  updateData,
-  children,
-  onEyeDropperToggle,
-  onEscape,
-}: PickerProps) => {
-  const [customColors] = React.useState(() => {
-    if (type === "canvasBackground") {
-      return [];
-    }
-    return getMostUsedCustomColors(elements, type, palette);
-  });
-
-  const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
-    activeColorPickerSectionAtom,
-  );
-
-  const colorObj = getColorNameAndShadeFromColor({
-    color,
-    palette,
-  });
-
-  useEffect(() => {
-    if (!activeColorPickerSection) {
-      const isCustom = isCustomColor({ color, palette });
-      const isCustomButNotInList = isCustom && !customColors.includes(color);
-
-      setActiveColorPickerSection(
-        isCustomButNotInList
-          ? "hex"
-          : isCustom
-          ? "custom"
-          : colorObj?.shade != null
-          ? "shades"
-          : "baseColors",
-      );
-    }
-  }, [
-    activeColorPickerSection,
-    color,
-    palette,
-    setActiveColorPickerSection,
-    colorObj,
-    customColors,
-  ]);
-
-  const [activeShade, setActiveShade] = useState(
-    colorObj?.shade ??
-      (type === "elementBackground"
-        ? DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX
-        : DEFAULT_ELEMENT_STROKE_COLOR_INDEX),
-  );
-
-  useEffect(() => {
-    if (colorObj?.shade != null) {
-      setActiveShade(colorObj.shade);
-    }
-
-    const keyup = (event: KeyboardEvent) => {
-      if (event.key === KEYS.ALT) {
-        onEyeDropperToggle(false);
+export const Picker = React.forwardRef(
+  (
+    {
+      color,
+      onChange,
+      type,
+      elements,
+      palette,
+      updateData,
+      children,
+      onEyeDropperToggle,
+      onEscape,
+    }: PickerProps,
+    ref,
+  ) => {
+    const [customColors] = React.useState(() => {
+      if (type === "canvasBackground") {
+        return [];
       }
-    };
-    document.addEventListener(EVENT.KEYUP, keyup, { capture: true });
-    return () => {
-      document.removeEventListener(EVENT.KEYUP, keyup, { capture: true });
-    };
-  }, [colorObj, onEyeDropperToggle]);
-
-  const pickerRef = React.useRef<HTMLDivElement>(null);
-
-  return (
-    <div role="dialog" aria-modal="true" aria-label={t("labels.colorPicker")}>
-      <div
-        ref={pickerRef}
-        onKeyDown={(event) => {
-          const handled = colorPickerKeyNavHandler({
-            event,
-            activeColorPickerSection,
-            palette,
-            color,
-            onChange,
-            onEyeDropperToggle,
-            customColors,
-            setActiveColorPickerSection,
-            updateData,
-            activeShade,
-            onEscape,
-          });
-
-          if (handled) {
-            event.preventDefault();
-            event.stopPropagation();
-          }
-        }}
-        className="color-picker-content properties-content"
-        // to allow focusing by clicking but not by tabbing
-        tabIndex={-1}
-      >
-        {!!customColors.length && (
+      return getMostUsedCustomColors(elements, type, palette);
+    });
+
+    const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
+      activeColorPickerSectionAtom,
+    );
+
+    const colorObj = getColorNameAndShadeFromColor({
+      color,
+      palette,
+    });
+
+    useEffect(() => {
+      if (!activeColorPickerSection) {
+        const isCustom = !!color && isCustomColor({ color, palette });
+        const isCustomButNotInList = isCustom && !customColors.includes(color);
+
+        setActiveColorPickerSection(
+          isCustomButNotInList
+            ? null
+            : isCustom
+            ? "custom"
+            : colorObj?.shade != null
+            ? "shades"
+            : "baseColors",
+        );
+      }
+    }, [
+      activeColorPickerSection,
+      color,
+      palette,
+      setActiveColorPickerSection,
+      colorObj,
+      customColors,
+    ]);
+
+    const [activeShade, setActiveShade] = useState(
+      colorObj?.shade ??
+        (type === "elementBackground"
+          ? DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX
+          : DEFAULT_ELEMENT_STROKE_COLOR_INDEX),
+    );
+
+    useEffect(() => {
+      if (colorObj?.shade != null) {
+        setActiveShade(colorObj.shade);
+      }
+
+      const keyup = (event: KeyboardEvent) => {
+        if (event.key === KEYS.ALT) {
+          onEyeDropperToggle(false);
+        }
+      };
+      document.addEventListener(EVENT.KEYUP, keyup, { capture: true });
+      return () => {
+        document.removeEventListener(EVENT.KEYUP, keyup, { capture: true });
+      };
+    }, [colorObj, onEyeDropperToggle]);
+    const pickerRef = React.useRef<HTMLDivElement>(null);
+
+    useImperativeHandle(ref, () => pickerRef.current!);
+
+    useEffect(() => {
+      pickerRef?.current?.focus();
+    }, []);
+
+    return (
+      <div role="dialog" aria-modal="true" aria-label={t("labels.colorPicker")}>
+        <div
+          ref={pickerRef}
+          onKeyDown={(event) => {
+            const handled = colorPickerKeyNavHandler({
+              event,
+              activeColorPickerSection,
+              palette,
+              color,
+              onChange,
+              onEyeDropperToggle,
+              customColors,
+              setActiveColorPickerSection,
+              updateData,
+              activeShade,
+              onEscape,
+            });
+
+            if (handled) {
+              event.preventDefault();
+              event.stopPropagation();
+            }
+          }}
+          className="color-picker-content properties-content"
+          // to allow focusing by clicking but not by tabbing
+          tabIndex={-1}
+        >
+          {!!customColors.length && (
+            <div>
+              <PickerHeading>
+                {t("colorPicker.mostUsedCustomColors")}
+              </PickerHeading>
+              <CustomColorList
+                colors={customColors}
+                color={color}
+                label={t("colorPicker.mostUsedCustomColors")}
+                onChange={onChange}
+              />
+            </div>
+          )}
+
           <div>
-            <PickerHeading>
-              {t("colorPicker.mostUsedCustomColors")}
-            </PickerHeading>
-            <CustomColorList
-              colors={customColors}
+            <PickerHeading>{t("colorPicker.colors")}</PickerHeading>
+            <PickerColorList
               color={color}
-              label={t("colorPicker.mostUsedCustomColors")}
+              palette={palette}
               onChange={onChange}
+              activeShade={activeShade}
             />
           </div>
-        )}
-
-        <div>
-          <PickerHeading>{t("colorPicker.colors")}</PickerHeading>
-          <PickerColorList
-            color={color}
-            label={label}
-            palette={palette}
-            onChange={onChange}
-            activeShade={activeShade}
-          />
-        </div>
 
-        <div>
-          <PickerHeading>{t("colorPicker.shades")}</PickerHeading>
-          <ShadeList hex={color} onChange={onChange} palette={palette} />
+          <div>
+            <PickerHeading>{t("colorPicker.shades")}</PickerHeading>
+            <ShadeList color={color} onChange={onChange} palette={palette} />
+          </div>
+          {children}
         </div>
-        {children}
       </div>
-    </div>
-  );
-};
+    );
+  },
+);

+ 2 - 4
packages/excalidraw/components/ColorPicker/PickerColorList.tsx

@@ -17,9 +17,8 @@ import type { TranslationKeys } from "../../i18n";
 
 interface PickerColorListProps {
   palette: ColorPaletteCustom;
-  color: string;
+  color: string | null;
   onChange: (color: string) => void;
-  label: string;
   activeShade: number;
 }
 
@@ -27,11 +26,10 @@ const PickerColorList = ({
   palette,
   color,
   onChange,
-  label,
   activeShade,
 }: PickerColorListProps) => {
   const colorObj = getColorNameAndShadeFromColor({
-    color: color || "transparent",
+    color,
     palette,
   });
   const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(

+ 3 - 3
packages/excalidraw/components/ColorPicker/ShadeList.tsx

@@ -13,14 +13,14 @@ import {
 } from "./colorPickerUtils";
 
 interface ShadeListProps {
-  hex: string;
+  color: string | null;
   onChange: (color: string) => void;
   palette: ColorPaletteCustom;
 }
 
-export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
+export const ShadeList = ({ color, onChange, palette }: ShadeListProps) => {
   const colorObj = getColorNameAndShadeFromColor({
-    color: hex || "transparent",
+    color: color || "transparent",
     palette,
   });
 

+ 1 - 1
packages/excalidraw/components/ColorPicker/TopPicks.tsx

@@ -14,7 +14,7 @@ import type { ColorPickerType } from "./colorPickerUtils";
 interface TopPicksProps {
   onChange: (color: string) => void;
   type: ColorPickerType;
-  activeColor: string;
+  activeColor: string | null;
   topPicks?: readonly string[];
 }
 

+ 4 - 1
packages/excalidraw/components/ColorPicker/colorPickerUtils.ts

@@ -11,11 +11,14 @@ export const getColorNameAndShadeFromColor = ({
   color,
 }: {
   palette: ColorPaletteCustom;
-  color: string;
+  color: string | null;
 }): {
   colorName: ColorPickerColor;
   shade: number | null;
 } | null => {
+  if (!color) {
+    return null;
+  }
   for (const [colorName, colorVal] of Object.entries(palette)) {
     if (Array.isArray(colorVal)) {
       const shade = colorVal.indexOf(color);

+ 2 - 2
packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts

@@ -109,7 +109,7 @@ interface ColorPickerKeyNavHandlerProps {
   event: React.KeyboardEvent;
   activeColorPickerSection: ActiveColorPickerSectionAtomType;
   palette: ColorPaletteCustom;
-  color: string;
+  color: string | null;
   onChange: (color: string) => void;
   customColors: string[];
   setActiveColorPickerSection: (
@@ -270,7 +270,7 @@ export const colorPickerKeyNavHandler = ({
   }
 
   if (activeColorPickerSection === "custom") {
-    const indexOfColor = customColors.indexOf(color);
+    const indexOfColor = color != null ? customColors.indexOf(color) : 0;
 
     const newColorIndex = arrowHandler(
       event.key,

+ 8 - 0
packages/excalidraw/components/icons.tsx

@@ -580,6 +580,14 @@ export const bucketFillIcon = createIcon(
   tablerIconProps,
 );
 
+// simple / icon
+export const slashIcon = createIcon(
+  <g strokeWidth={1.5}>
+    <path d="M6 18l12 -12" />
+  </g>,
+  tablerIconProps,
+);
+
 export const ExportImageIcon = createIcon(
   <g strokeWidth="1.25">
     <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>

+ 1 - 0
packages/excalidraw/locales/en.json

@@ -556,6 +556,7 @@
     }
   },
   "colorPicker": {
+    "color": "Color",
     "mostUsedCustomColors": "Most used custom colors",
     "colors": "Colors",
     "shades": "Shades",