Generalize color normalization (#1479) (#1483)

Following #1478, a bug was found related to transparent backgrounds. As Excalidraw only supports `transparent` as a valid transparent color, this commits generalizes the use of canvas to normalize color values.

It changes a few details:
- `rgba()` or `hsla()` syntaxes are not accepted anymore
- pasting values goes through the same normalization step, avoiding invalid values
- color validation is not regex-based anymore
- any CSS-valid black color is now accepted (e.g. previously, `rgb(0,0,0)` was rejected)
This commit is contained in:
Warren Seine 2020-04-24 20:06:54 +02:00 committed by GitHub
parent 0220341966
commit 71e7f130bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -7,15 +7,23 @@ import { t, getLanguage } from "../i18n";
import { isWritableElement } from "../utils"; import { isWritableElement } from "../utils";
import colors from "../colors"; import colors from "../colors";
const standardizeColor = ( const normalizeColor = (
value: string, color: string,
canvasContext: CanvasRenderingContext2D, canvasContext: CanvasRenderingContext2D,
): string | null => { ): string | null => {
const defaultHexColor = "#000000"; // Excalidraw only supports "transparent" as a valid transparent value.
canvasContext.fillStyle = value; // Default canvas fill style value is `#000000`, which is also a valid
// Excalidraw color. Let's set it to another Canvas-valid but
// Excalidraw-invalid value to detect successful normalizations.
if (color === "transparent") {
return color;
}
const defaultColor = "rgba(0,0,0,0)";
canvasContext.fillStyle = defaultColor;
canvasContext.fillStyle = color;
const hexColor = canvasContext.fillStyle; const hexColor = canvasContext.fillStyle;
canvasContext.fillStyle = defaultHexColor; return hexColor.startsWith("#") ? hexColor : null;
return hexColor !== defaultHexColor || value === "black" ? hexColor : null;
}; };
// This is a narrow reimplementation of the awesome react-color Twitter component // This is a narrow reimplementation of the awesome react-color Twitter component
@ -190,7 +198,6 @@ const ColorInput = React.forwardRef(
}, },
ref, ref,
) => { ) => {
const colorRegex = /^([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8}|transparent)$/;
const [innerValue, setInnerValue] = React.useState(color); const [innerValue, setInnerValue] = React.useState(color);
const inputRef = React.useRef(null); const inputRef = React.useRef(null);
const canvasContext = React.useRef<CanvasRenderingContext2D>( const canvasContext = React.useRef<CanvasRenderingContext2D>(
@ -203,6 +210,20 @@ const ColorInput = React.forwardRef(
React.useImperativeHandle(ref, () => inputRef.current); React.useImperativeHandle(ref, () => inputRef.current);
const changeColor = React.useCallback(
(inputValue: string) => {
const value = inputValue.toLowerCase();
if (canvasContext.current) {
const normalizedValue = normalizeColor(value, canvasContext.current);
if (normalizedValue) {
onChange(normalizedValue);
}
}
setInnerValue(value);
},
[canvasContext, onChange, setInnerValue],
);
return ( return (
<label className="color-input-container"> <label className="color-input-container">
<div className="color-picker-hash">#</div> <div className="color-picker-hash">#</div>
@ -210,20 +231,9 @@ const ColorInput = React.forwardRef(
spellCheck={false} spellCheck={false}
className="color-picker-input" className="color-picker-input"
aria-label={label} aria-label={label}
onChange={(event) => { onChange={(event) => changeColor(event.target.value)}
const value = event.target.value.toLowerCase();
if (value.match(colorRegex)) {
onChange(value === "transparent" ? "transparent" : `#${value}`);
} else if (canvasContext.current) {
const hexColor = standardizeColor(value, canvasContext.current);
if (hexColor) {
onChange(hexColor);
}
}
setInnerValue(value);
}}
value={(innerValue || "").replace(/^#/, "")} value={(innerValue || "").replace(/^#/, "")}
onPaste={(event) => onChange(event.clipboardData.getData("text"))} onPaste={(event) => changeColor(event.clipboardData.getData("text"))}
onBlur={() => setInnerValue(color)} onBlur={() => setInnerValue(color)}
ref={inputRef} ref={inputRef}
/> />