aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts')
-rw-r--r--packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts286
1 files changed, 286 insertions, 0 deletions
diff --git a/packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts b/packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts
new file mode 100644
index 0000000..7767692
--- /dev/null
+++ b/packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts
@@ -0,0 +1,286 @@
+import { KEYS } from "../../keys";
+import type {
+ ColorPickerColor,
+ ColorPalette,
+ ColorPaletteCustom,
+} from "../../colors";
+import { COLORS_PER_ROW, COLOR_PALETTE } from "../../colors";
+import type { ValueOf } from "../../utility-types";
+import type { ActiveColorPickerSectionAtomType } from "./colorPickerUtils";
+import {
+ colorPickerHotkeyBindings,
+ getColorNameAndShadeFromColor,
+} from "./colorPickerUtils";
+
+const arrowHandler = (
+ eventKey: string,
+ currentIndex: number | null,
+ length: number,
+) => {
+ const rows = Math.ceil(length / COLORS_PER_ROW);
+
+ currentIndex = currentIndex ?? -1;
+
+ switch (eventKey) {
+ case "ArrowLeft": {
+ const prevIndex = currentIndex - 1;
+ return prevIndex < 0 ? length - 1 : prevIndex;
+ }
+ case "ArrowRight": {
+ return (currentIndex + 1) % length;
+ }
+ case "ArrowDown": {
+ const nextIndex = currentIndex + COLORS_PER_ROW;
+ return nextIndex >= length ? currentIndex % COLORS_PER_ROW : nextIndex;
+ }
+ case "ArrowUp": {
+ const prevIndex = currentIndex - COLORS_PER_ROW;
+ const newIndex =
+ prevIndex < 0 ? COLORS_PER_ROW * rows + prevIndex : prevIndex;
+ return newIndex >= length ? undefined : newIndex;
+ }
+ }
+};
+
+interface HotkeyHandlerProps {
+ e: React.KeyboardEvent;
+ colorObj: { colorName: ColorPickerColor; shade: number | null } | null;
+ onChange: (color: string) => void;
+ palette: ColorPaletteCustom;
+ customColors: string[];
+ setActiveColorPickerSection: (
+ update: React.SetStateAction<ActiveColorPickerSectionAtomType>,
+ ) => void;
+ activeShade: number;
+}
+
+/**
+ * @returns true if the event was handled
+ */
+const hotkeyHandler = ({
+ e,
+ colorObj,
+ onChange,
+ palette,
+ customColors,
+ setActiveColorPickerSection,
+ activeShade,
+}: HotkeyHandlerProps): boolean => {
+ if (colorObj?.shade != null) {
+ // shift + numpad is extremely messed up on windows apparently
+ if (
+ ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5"].includes(e.code) &&
+ e.shiftKey
+ ) {
+ const newShade = Number(e.code.slice(-1)) - 1;
+ onChange(palette[colorObj.colorName][newShade]);
+ setActiveColorPickerSection("shades");
+ return true;
+ }
+ }
+
+ if (["1", "2", "3", "4", "5"].includes(e.key)) {
+ const c = customColors[Number(e.key) - 1];
+ if (c) {
+ onChange(customColors[Number(e.key) - 1]);
+ setActiveColorPickerSection("custom");
+ return true;
+ }
+ }
+
+ if (colorPickerHotkeyBindings.includes(e.key)) {
+ const index = colorPickerHotkeyBindings.indexOf(e.key);
+ const paletteKey = Object.keys(palette)[index] as keyof ColorPalette;
+ const paletteValue = palette[paletteKey];
+ const r = Array.isArray(paletteValue)
+ ? paletteValue[activeShade]
+ : paletteValue;
+ onChange(r);
+ setActiveColorPickerSection("baseColors");
+ return true;
+ }
+ return false;
+};
+
+interface ColorPickerKeyNavHandlerProps {
+ event: React.KeyboardEvent;
+ activeColorPickerSection: ActiveColorPickerSectionAtomType;
+ palette: ColorPaletteCustom;
+ color: string;
+ onChange: (color: string) => void;
+ customColors: string[];
+ setActiveColorPickerSection: (
+ update: React.SetStateAction<ActiveColorPickerSectionAtomType>,
+ ) => void;
+ updateData: (formData?: any) => void;
+ activeShade: number;
+ onEyeDropperToggle: (force?: boolean) => void;
+ onEscape: (event: React.KeyboardEvent | KeyboardEvent) => void;
+}
+
+/**
+ * @returns true if the event was handled
+ */
+export const colorPickerKeyNavHandler = ({
+ event,
+ activeColorPickerSection,
+ palette,
+ color,
+ onChange,
+ customColors,
+ setActiveColorPickerSection,
+ updateData,
+ activeShade,
+ onEyeDropperToggle,
+ onEscape,
+}: ColorPickerKeyNavHandlerProps): boolean => {
+ if (event[KEYS.CTRL_OR_CMD]) {
+ return false;
+ }
+
+ if (event.key === KEYS.ESCAPE) {
+ onEscape(event);
+ return true;
+ }
+
+ // checkt using `key` to ignore combos with Alt modifier
+ if (event.key === KEYS.ALT) {
+ onEyeDropperToggle(true);
+ return true;
+ }
+
+ if (event.key === KEYS.I) {
+ onEyeDropperToggle();
+ return true;
+ }
+
+ const colorObj = getColorNameAndShadeFromColor({ color, palette });
+
+ if (event.key === KEYS.TAB) {
+ const sectionsMap: Record<
+ NonNullable<ActiveColorPickerSectionAtomType>,
+ boolean
+ > = {
+ custom: !!customColors.length,
+ baseColors: true,
+ shades: colorObj?.shade != null,
+ hex: true,
+ };
+
+ const sections = Object.entries(sectionsMap).reduce((acc, [key, value]) => {
+ if (value) {
+ acc.push(key as ActiveColorPickerSectionAtomType);
+ }
+ return acc;
+ }, [] as ActiveColorPickerSectionAtomType[]);
+
+ const activeSectionIndex = sections.indexOf(activeColorPickerSection);
+ const indexOffset = event.shiftKey ? -1 : 1;
+ const nextSectionIndex =
+ activeSectionIndex + indexOffset > sections.length - 1
+ ? 0
+ : activeSectionIndex + indexOffset < 0
+ ? sections.length - 1
+ : activeSectionIndex + indexOffset;
+
+ const nextSection = sections[nextSectionIndex];
+
+ if (nextSection) {
+ setActiveColorPickerSection(nextSection);
+ }
+
+ if (nextSection === "custom") {
+ onChange(customColors[0]);
+ } else if (nextSection === "baseColors") {
+ const baseColorName = (
+ Object.entries(palette) as [string, ValueOf<ColorPalette>][]
+ ).find(([name, shades]) => {
+ if (Array.isArray(shades)) {
+ return shades.includes(color);
+ } else if (shades === color) {
+ return name;
+ }
+ return null;
+ });
+
+ if (!baseColorName) {
+ onChange(COLOR_PALETTE.black);
+ }
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ return true;
+ }
+
+ if (
+ hotkeyHandler({
+ e: event,
+ colorObj,
+ onChange,
+ palette,
+ customColors,
+ setActiveColorPickerSection,
+ activeShade,
+ })
+ ) {
+ return true;
+ }
+
+ if (activeColorPickerSection === "shades") {
+ if (colorObj) {
+ const { shade } = colorObj;
+ const newShade = arrowHandler(event.key, shade, COLORS_PER_ROW);
+
+ if (newShade !== undefined) {
+ onChange(palette[colorObj.colorName][newShade]);
+ return true;
+ }
+ }
+ }
+
+ if (activeColorPickerSection === "baseColors") {
+ if (colorObj) {
+ const { colorName } = colorObj;
+ const colorNames = Object.keys(palette) as (keyof ColorPalette)[];
+ const indexOfColorName = colorNames.indexOf(colorName);
+
+ const newColorIndex = arrowHandler(
+ event.key,
+ indexOfColorName,
+ colorNames.length,
+ );
+
+ if (newColorIndex !== undefined) {
+ const newColorName = colorNames[newColorIndex];
+ const newColorNameValue = palette[newColorName];
+
+ onChange(
+ Array.isArray(newColorNameValue)
+ ? newColorNameValue[activeShade]
+ : newColorNameValue,
+ );
+ return true;
+ }
+ }
+ }
+
+ if (activeColorPickerSection === "custom") {
+ const indexOfColor = customColors.indexOf(color);
+
+ const newColorIndex = arrowHandler(
+ event.key,
+ indexOfColor,
+ customColors.length,
+ );
+
+ if (newColorIndex !== undefined) {
+ const newColor = customColors[newColorIndex];
+ onChange(newColor);
+ return true;
+ }
+ }
+
+ return false;
+};