feat: show empty active color if no common color (#9506)
This commit is contained in:
parent
51dbd4831b
commit
c4c064982f
@ -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}
|
||||
|
@ -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 && (
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -8,7 +8,7 @@ import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
||||
|
||||
interface CustomColorListProps {
|
||||
colors: string[];
|
||||
color: string;
|
||||
color: string | null;
|
||||
onChange: (color: string) => void;
|
||||
label: string;
|
||||
}
|
||||
|
@ -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<HTMLDivElement>(null);
|
||||
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
|
||||
activeColorPickerSectionAtom,
|
||||
);
|
||||
|
||||
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,
|
||||
});
|
||||
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>
|
||||
)}
|
||||
|
||||
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}
|
||||
<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>
|
||||
<PickerHeading>{t("colorPicker.shades")}</PickerHeading>
|
||||
<ShadeList color={color} onChange={onChange} palette={palette} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<PickerHeading>{t("colorPicker.shades")}</PickerHeading>
|
||||
<ShadeList hex={color} onChange={onChange} palette={palette} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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[];
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -556,6 +556,7 @@
|
||||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"color": "Color",
|
||||
"mostUsedCustomColors": "Most used custom colors",
|
||||
"colors": "Colors",
|
||||
"shades": "Shades",
|
||||
|
Loading…
x
Reference in New Issue
Block a user