From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001 From: kj_sh604 Date: Sun, 15 Mar 2026 16:19:35 -0400 Subject: refactor: packages/ --- packages/excalidraw/components/IconPicker.tsx | 239 ++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 packages/excalidraw/components/IconPicker.tsx (limited to 'packages/excalidraw/components/IconPicker.tsx') diff --git a/packages/excalidraw/components/IconPicker.tsx b/packages/excalidraw/components/IconPicker.tsx new file mode 100644 index 0000000..ee83f90 --- /dev/null +++ b/packages/excalidraw/components/IconPicker.tsx @@ -0,0 +1,239 @@ +import type { JSX } from "react"; +import React, { useEffect } from "react"; +import * as Popover from "@radix-ui/react-popover"; +import { isArrowKey, KEYS } from "../keys"; +import { getLanguage, t } from "../i18n"; +import clsx from "clsx"; +import Collapsible from "./Stats/Collapsible"; +import { atom, useAtom } from "../editor-jotai"; +import { useDevice } from "./App"; + +import "./IconPicker.scss"; + +const moreOptionsAtom = atom(false); + +type Option = { + value: T; + text: string; + icon: JSX.Element; + keyBinding: string | null; +}; + +function Picker({ + options, + value, + label, + onChange, + onClose, + numberOfOptionsToAlwaysShow = options.length, +}: { + label: string; + value: T; + options: readonly Option[]; + onChange: (value: T) => void; + onClose: () => void; + numberOfOptionsToAlwaysShow?: number; +}) { + const device = useDevice(); + + const handleKeyDown = (event: React.KeyboardEvent) => { + const pressedOption = options.find( + (option) => option.keyBinding === event.key.toLowerCase(), + )!; + + if (!(event.metaKey || event.altKey || event.ctrlKey) && pressedOption) { + // Keybinding navigation + onChange(pressedOption.value); + + event.preventDefault(); + } else if (event.key === KEYS.TAB) { + const index = options.findIndex((option) => option.value === value); + const nextIndex = event.shiftKey + ? (options.length + index - 1) % options.length + : (index + 1) % options.length; + onChange(options[nextIndex].value); + } else if (isArrowKey(event.key)) { + // Arrow navigation + const isRTL = getLanguage().rtl; + const index = options.findIndex((option) => option.value === value); + if (index !== -1) { + const length = options.length; + let nextIndex = index; + + switch (event.key) { + // Select the next option + case isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT: + nextIndex = (index + 1) % length; + break; + // Select the previous option + case isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT: + nextIndex = (length + index - 1) % length; + break; + // Go the next row + case KEYS.ARROW_DOWN: { + nextIndex = (index + (numberOfOptionsToAlwaysShow ?? 1)) % length; + break; + } + // Go the previous row + case KEYS.ARROW_UP: { + nextIndex = + (length + index - (numberOfOptionsToAlwaysShow ?? 1)) % length; + break; + } + } + + onChange(options[nextIndex].value); + } + event.preventDefault(); + } else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) { + // Close on escape or enter + event.preventDefault(); + onClose(); + } + event.nativeEvent.stopImmediatePropagation(); + event.stopPropagation(); + }; + + const [showMoreOptions, setShowMoreOptions] = useAtom(moreOptionsAtom); + + const alwaysVisibleOptions = React.useMemo( + () => options.slice(0, numberOfOptionsToAlwaysShow), + [options, numberOfOptionsToAlwaysShow], + ); + const moreOptions = React.useMemo( + () => options.slice(numberOfOptionsToAlwaysShow), + [options, numberOfOptionsToAlwaysShow], + ); + + useEffect(() => { + if (!alwaysVisibleOptions.some((option) => option.value === value)) { + setShowMoreOptions(true); + } + }, [value, alwaysVisibleOptions, setShowMoreOptions]); + + const renderOptions = (options: Option[]) => { + return ( +
+ {options.map((option, i) => ( + + ))} +
+ ); + }; + + return ( + +
+ {renderOptions(alwaysVisibleOptions)} + + {moreOptions.length > 0 && ( + { + setShowMoreOptions((value) => !value); + }} + className="picker-collapsible" + > + {renderOptions(moreOptions)} + + )} +
+
+ ); +} + +export function IconPicker({ + value, + label, + options, + onChange, + group = "", + numberOfOptionsToAlwaysShow, +}: { + label: string; + value: T; + options: readonly { + value: T; + text: string; + icon: JSX.Element; + keyBinding: string | null; + }[]; + onChange: (value: T) => void; + numberOfOptionsToAlwaysShow?: number; + group?: string; +}) { + const [isActive, setActive] = React.useState(false); + const rPickerButton = React.useRef(null); + + return ( +
+ setActive(open)}> + setActive(!isActive)} + ref={rPickerButton} + className={isActive ? "active" : ""} + > + {options.find((option) => option.value === value)?.icon} + + {isActive && ( + { + setActive(false); + }} + numberOfOptionsToAlwaysShow={numberOfOptionsToAlwaysShow} + /> + )} + +
+ ); +} -- cgit v1.2.3