From c4c064982f3aaab0a24b70d5738d11c17da581e6 Mon Sep 17 00:00:00 2001
From: David Luzar <5153846+dwelle@users.noreply.github.com>
Date: Sun, 11 May 2025 15:07:57 +0200
Subject: [PATCH] feat: show empty active color if no common color (#9506)
---
.../excalidraw/actions/actionProperties.tsx | 6 +-
.../components/ColorPicker/ColorInput.tsx | 3 +
.../components/ColorPicker/ColorPicker.scss | 11 +
.../components/ColorPicker/ColorPicker.tsx | 27 +-
.../ColorPicker/CustomColorList.tsx | 2 +-
.../components/ColorPicker/Picker.tsx | 267 +++++++++---------
.../ColorPicker/PickerColorList.tsx | 6 +-
.../components/ColorPicker/ShadeList.tsx | 6 +-
.../components/ColorPicker/TopPicks.tsx | 2 +-
.../ColorPicker/colorPickerUtils.ts | 5 +-
.../ColorPicker/keyboardNavHandlers.ts | 4 +-
packages/excalidraw/components/icons.tsx | 8 +
packages/excalidraw/locales/en.json | 1 +
13 files changed, 193 insertions(+), 155 deletions(-)
diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx
index 0b7310629..c379de7f8 100644
--- a/packages/excalidraw/actions/actionProperties.tsx
+++ b/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}
diff --git a/packages/excalidraw/components/ColorPicker/ColorInput.tsx b/packages/excalidraw/components/ColorPicker/ColorInput.tsx
index a3f6722eb..e5e6f3a77 100644
--- a/packages/excalidraw/components/ColorPicker/ColorInput.tsx
+++ b/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 && (
diff --git a/packages/excalidraw/components/ColorPicker/ColorPicker.scss b/packages/excalidraw/components/ColorPicker/ColorPicker.scss
index 56b40869b..7a78395d6 100644
--- a/packages/excalidraw/components/ColorPicker/ColorPicker.scss
+++ b/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;
diff --git a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx
index eb6d82d9e..270d61f4c 100644
--- a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx
+++ b/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 = ({
{t("colorPicker.hexCode")}
{
onChange(color);
}}
colorPickerType={type}
+ placeholder={t("colorPicker.color")}
/>
);
- const popoverRef = useRef(null);
+ const colorPickerContentRef = useRef(null);
const focusPickerContent = () => {
- popoverRef.current
- ?.querySelector(".color-picker-content")
- ?.focus();
+ colorPickerContentRef.current?.focus();
};
return (
@@ -133,6 +137,7 @@ const ColorPickerPopupContent = ({
>
{palette ? (
{
@@ -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 = ({
-
+ {!color && slashIcon}
);
};
diff --git a/packages/excalidraw/components/ColorPicker/CustomColorList.tsx b/packages/excalidraw/components/ColorPicker/CustomColorList.tsx
index 45d5db84c..acc7b3e0e 100644
--- a/packages/excalidraw/components/ColorPicker/CustomColorList.tsx
+++ b/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;
}
diff --git a/packages/excalidraw/components/ColorPicker/Picker.tsx b/packages/excalidraw/components/ColorPicker/Picker.tsx
index 3c54c6769..f784912f4 100644
--- a/packages/excalidraw/components/ColorPicker/Picker.tsx
+++ b/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]);
+ return getMostUsedCustomColors(elements, type, palette);
+ });
- const pickerRef = React.useRef(null);
+ const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
+ activeColorPickerSectionAtom,
+ );
- return (
-
-
{
- const handled = colorPickerKeyNavHandler({
- event,
- activeColorPickerSection,
- palette,
- color,
- onChange,
- onEyeDropperToggle,
- customColors,
- setActiveColorPickerSection,
- updateData,
- activeShade,
- onEscape,
- });
+ 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
(null);
+
+ useImperativeHandle(ref, () => pickerRef.current!);
+
+ useEffect(() => {
+ pickerRef?.current?.focus();
+ }, []);
+
+ return (
+
+
{
+ 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 && (
+
+
+ {t("colorPicker.mostUsedCustomColors")}
+
+
+
+ )}
- 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 && (
-
- {t("colorPicker.mostUsedCustomColors")}
-
-
{t("colorPicker.colors")}
+
- )}
-
-
{t("colorPicker.colors")}
-
+
+
{t("colorPicker.shades")}
+
+
+ {children}
-
-
-
{t("colorPicker.shades")}
-
-
- {children}
-
- );
-};
+ );
+ },
+);
diff --git a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx b/packages/excalidraw/components/ColorPicker/PickerColorList.tsx
index 38e5cf8c5..4fd6815e4 100644
--- a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx
+++ b/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(
diff --git a/packages/excalidraw/components/ColorPicker/ShadeList.tsx b/packages/excalidraw/components/ColorPicker/ShadeList.tsx
index 1c8e4c4eb..db33402b0 100644
--- a/packages/excalidraw/components/ColorPicker/ShadeList.tsx
+++ b/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,
});
diff --git a/packages/excalidraw/components/ColorPicker/TopPicks.tsx b/packages/excalidraw/components/ColorPicker/TopPicks.tsx
index 8531172fb..6a00b1817 100644
--- a/packages/excalidraw/components/ColorPicker/TopPicks.tsx
+++ b/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[];
}
diff --git a/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts b/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
index f572bd49f..d5a6f5b81 100644
--- a/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
+++ b/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);
diff --git a/packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts b/packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts
index 3e27229bc..15a2748f1 100644
--- a/packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts
+++ b/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,
diff --git a/packages/excalidraw/components/icons.tsx b/packages/excalidraw/components/icons.tsx
index f3808a69d..2fc05579a 100644
--- a/packages/excalidraw/components/icons.tsx
+++ b/packages/excalidraw/components/icons.tsx
@@ -580,6 +580,14 @@ export const bucketFillIcon = createIcon(
tablerIconProps,
);
+// simple / icon
+export const slashIcon = createIcon(
+
+
+ ,
+ tablerIconProps,
+);
+
export const ExportImageIcon = createIcon(
diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json
index f89fa5f1f..fad13c705 100644
--- a/packages/excalidraw/locales/en.json
+++ b/packages/excalidraw/locales/en.json
@@ -556,6 +556,7 @@
}
},
"colorPicker": {
+ "color": "Color",
"mostUsedCustomColors": "Most used custom colors",
"colors": "Colors",
"shades": "Shades",