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",