excalidraw/src/components/ColorPicker.tsx

294 lines
8.0 KiB
TypeScript
Raw Normal View History

import React from "react";
2020-01-07 07:50:59 +05:00
import { Popover } from "./Popover";
import "./ColorPicker.scss";
import { isArrowKey, KEYS } from "../keys";
import { t, getLanguage } from "../i18n";
import { isWritableElement } from "../utils";
import colors from "../colors";
const isValidColor = (color: string) => {
2020-04-25 23:06:16 +02:00
const style = new Option().style;
style.color = color;
return !!style.color;
};
2020-04-25 23:06:16 +02:00
const getColor = (color: string): string | null => {
if (color === "transparent") {
return color;
}
2020-04-25 23:06:16 +02:00
return isValidColor(color)
? color
: isValidColor(`#${color}`)
? `#${color}`
: null;
};
// This is a narrow reimplementation of the awesome react-color Twitter component
// https://github.com/casesandberg/react-color/blob/master/src/components/twitter/Twitter.js
// Unfortunately, we can't detect keyboard layout in the browser. So this will
// only work well for QWERTY but not AZERTY or others...
const keyBindings = [
["1", "2", "3", "4", "5"],
["q", "w", "e", "r", "t"],
["a", "s", "d", "f", "g"],
].flat();
const Picker = ({
colors,
color,
2020-01-24 12:04:54 +02:00
onChange,
onClose,
label,
showInput = true,
type,
}: {
colors: string[];
color: string | null;
onChange: (color: string) => void;
onClose: () => void;
label: string;
showInput: boolean;
type: "canvasBackground" | "elementBackground" | "elementStroke";
}) => {
const firstItem = React.useRef<HTMLButtonElement>();
const activeItem = React.useRef<HTMLButtonElement>();
const gallery = React.useRef<HTMLDivElement>();
const colorInput = React.useRef<HTMLInputElement>();
React.useEffect(() => {
// After the component is first mounted focus on first input
if (activeItem.current) {
activeItem.current.focus();
} else if (colorInput.current) {
colorInput.current.focus();
} else if (gallery.current) {
gallery.current.focus();
}
}, []);
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === KEYS.TAB) {
const { activeElement } = document;
if (event.shiftKey) {
if (activeElement === firstItem.current) {
colorInput.current?.focus();
event.preventDefault();
}
2020-11-06 22:06:30 +02:00
} else if (activeElement === colorInput.current) {
firstItem.current?.focus();
event.preventDefault();
}
} else if (isArrowKey(event.key)) {
const { activeElement } = document;
const isRTL = getLanguage().rtl;
const index = Array.prototype.indexOf.call(
gallery!.current!.children,
activeElement,
);
if (index !== -1) {
const length = gallery!.current!.children.length - (showInput ? 1 : 0);
const nextIndex =
event.key === (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT)
? (index + 1) % length
: event.key === (isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT)
? (length + index - 1) % length
: event.key === KEYS.ARROW_DOWN
? (index + 5) % length
: event.key === KEYS.ARROW_UP
? (length + index - 5) % length
: index;
(gallery!.current!.children![nextIndex] as any).focus();
}
event.preventDefault();
} else if (
keyBindings.includes(event.key.toLowerCase()) &&
!isWritableElement(event.target)
) {
const index = keyBindings.indexOf(event.key.toLowerCase());
(gallery!.current!.children![index] as any).focus();
event.preventDefault();
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
event.preventDefault();
onClose();
}
event.nativeEvent.stopImmediatePropagation();
};
return (
<div
className={`color-picker color-picker-type-${type}`}
role="dialog"
aria-modal="true"
aria-label={t("labels.colorPicker")}
onKeyDown={handleKeyDown}
>
<div className="color-picker-triangle color-picker-triangle-shadow"></div>
<div className="color-picker-triangle"></div>
<div
className="color-picker-content"
ref={(el) => {
if (el) {
gallery.current = el;
}
}}
tabIndex={0}
>
{colors.map((_color, i) => (
<button
className="color-picker-swatch"
2020-04-10 18:09:29 -04:00
onClick={(event) => {
(event.currentTarget as HTMLButtonElement).focus();
onChange(_color);
}}
title={`${_color}${keyBindings[i].toUpperCase()}`}
aria-label={_color}
aria-keyshortcuts={keyBindings[i]}
2020-04-10 18:09:29 -04:00
style={{ color: _color }}
key={_color}
ref={(el) => {
if (el && i === 0) {
firstItem.current = el;
}
if (el && _color === color) {
activeItem.current = el;
}
}}
onFocus={() => {
onChange(_color);
}}
>
{_color === "transparent" ? (
<div className="color-picker-transparent"></div>
) : undefined}
<span className="color-picker-keybinding">{keyBindings[i]}</span>
</button>
))}
{showInput && (
<ColorInput
color={color}
label={label}
onChange={(color) => {
onChange(color);
}}
ref={colorInput}
/>
)}
</div>
</div>
);
};
2020-01-07 23:37:22 +05:00
const ColorInput = React.forwardRef(
(
{
color,
onChange,
label,
}: {
color: string | null;
onChange: (color: string) => void;
label: string;
},
ref,
) => {
const [innerValue, setInnerValue] = React.useState(color);
const inputRef = React.useRef(null);
React.useEffect(() => {
setInnerValue(color);
}, [color]);
React.useImperativeHandle(ref, () => inputRef.current);
const changeColor = React.useCallback(
(inputValue: string) => {
const value = inputValue.toLowerCase();
2020-04-25 23:06:16 +02:00
const color = getColor(value);
if (color) {
onChange(color);
}
setInnerValue(value);
},
2020-04-25 23:06:16 +02:00
[onChange],
);
return (
<label className="color-input-container">
<div className="color-picker-hash">#</div>
<input
spellCheck={false}
className="color-picker-input"
aria-label={label}
onChange={(event) => changeColor(event.target.value)}
value={(innerValue || "").replace(/^#/, "")}
onBlur={() => setInnerValue(color)}
ref={inputRef}
/>
</label>
);
},
);
export const ColorPicker = ({
type,
color,
2020-01-24 12:04:54 +02:00
onChange,
label,
}: {
type: "canvasBackground" | "elementBackground" | "elementStroke";
color: string | null;
onChange: (color: string) => void;
label: string;
}) => {
const [isActive, setActive] = React.useState(false);
const pickerButton = React.useRef<HTMLButtonElement>(null);
return (
<div>
<div className="color-picker-control-container">
<button
className="color-picker-label-swatch"
aria-label={label}
style={color ? { "--swatch-color": color } : undefined}
onClick={() => setActive(!isActive)}
ref={pickerButton}
/>
<ColorInput
color={color}
label={label}
onChange={(color) => {
onChange(color);
}}
/>
</div>
2020-01-07 23:37:22 +05:00
<React.Suspense fallback="">
{isActive ? (
2020-04-10 18:09:29 -04:00
<Popover
onCloseRequest={(event) =>
event.target !== pickerButton.current && setActive(false)
}
>
<Picker
colors={colors[type]}
color={color || null}
onChange={(changedColor) => {
onChange(changedColor);
2020-01-07 23:37:22 +05:00
}}
onClose={() => {
setActive(false);
pickerButton.current?.focus();
}}
label={label}
showInput={false}
type={type}
2020-01-07 23:37:22 +05:00
/>
</Popover>
) : null}
</React.Suspense>
</div>
);
};