ColorPicker.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import React from "react";
  2. import { Popover } from "./Popover";
  3. import "./ColorPicker.css";
  4. import { KEYS } from "../keys";
  5. import { useTranslation } from "react-i18next";
  6. import { TFunction } from "i18next";
  7. // This is a narrow reimplementation of the awesome react-color Twitter component
  8. // https://github.com/casesandberg/react-color/blob/master/src/components/twitter/Twitter.js
  9. const Picker = function({
  10. colors,
  11. color,
  12. onChange,
  13. onClose,
  14. label,
  15. t,
  16. }: {
  17. colors: string[];
  18. color: string | null;
  19. onChange: (color: string) => void;
  20. onClose: () => void;
  21. label: string;
  22. t: TFunction;
  23. }) {
  24. const firstItem = React.useRef<HTMLButtonElement>();
  25. const colorInput = React.useRef<HTMLInputElement>();
  26. React.useEffect(() => {
  27. // After the component is first mounted
  28. // focus on first input
  29. if (firstItem.current) firstItem.current.focus();
  30. }, []);
  31. const handleKeyDown = (e: React.KeyboardEvent) => {
  32. if (e.key === KEYS.TAB) {
  33. const { activeElement } = document;
  34. if (e.shiftKey) {
  35. if (activeElement === firstItem.current) {
  36. colorInput.current?.focus();
  37. e.preventDefault();
  38. }
  39. } else {
  40. if (activeElement === colorInput.current) {
  41. firstItem.current?.focus();
  42. e.preventDefault();
  43. }
  44. }
  45. } else if (e.key === KEYS.ESCAPE) {
  46. onClose();
  47. e.nativeEvent.stopImmediatePropagation();
  48. }
  49. };
  50. return (
  51. <div
  52. className="color-picker"
  53. role="dialog"
  54. aria-modal="true"
  55. aria-label={t("labels.colorPicker")}
  56. onKeyDown={handleKeyDown}
  57. >
  58. <div className="color-picker-triangle-shadow"></div>
  59. <div className="color-picker-triangle"></div>
  60. <div className="color-picker-content">
  61. <div className="colors-gallery">
  62. {colors.map((color, i) => (
  63. <button
  64. className="color-picker-swatch"
  65. onClick={() => {
  66. onChange(color);
  67. }}
  68. title={color}
  69. aria-label={color}
  70. style={{ backgroundColor: color }}
  71. key={color}
  72. ref={el => {
  73. if (i === 0 && el) firstItem.current = el;
  74. }}
  75. >
  76. {color === "transparent" ? (
  77. <div className="color-picker-transparent"></div>
  78. ) : (
  79. undefined
  80. )}
  81. </button>
  82. ))}
  83. </div>
  84. <ColorInput
  85. color={color}
  86. label={label}
  87. onChange={color => {
  88. onChange(color);
  89. }}
  90. ref={colorInput}
  91. />
  92. </div>
  93. </div>
  94. );
  95. };
  96. const ColorInput = React.forwardRef(
  97. (
  98. {
  99. color,
  100. onChange,
  101. label,
  102. }: {
  103. color: string | null;
  104. onChange: (color: string) => void;
  105. label: string;
  106. },
  107. ref,
  108. ) => {
  109. const colorRegex = /^([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8}|transparent)$/;
  110. const [innerValue, setInnerValue] = React.useState(color);
  111. const inputRef = React.useRef(null);
  112. React.useEffect(() => {
  113. setInnerValue(color);
  114. }, [color]);
  115. React.useImperativeHandle(ref, () => inputRef.current);
  116. return (
  117. <div className="color-input-container">
  118. <div className="color-picker-hash">#</div>
  119. <input
  120. spellCheck={false}
  121. className="color-picker-input"
  122. aria-label={label}
  123. onChange={e => {
  124. const value = e.target.value.toLowerCase();
  125. if (value.match(colorRegex)) {
  126. onChange(value === "transparent" ? "transparent" : "#" + value);
  127. }
  128. setInnerValue(value);
  129. }}
  130. value={(innerValue || "").replace(/^#/, "")}
  131. onPaste={e => onChange(e.clipboardData.getData("text"))}
  132. onBlur={() => setInnerValue(color)}
  133. ref={inputRef}
  134. />
  135. </div>
  136. );
  137. },
  138. );
  139. export function ColorPicker({
  140. type,
  141. color,
  142. onChange,
  143. label,
  144. }: {
  145. type: "canvasBackground" | "elementBackground" | "elementStroke";
  146. color: string | null;
  147. onChange: (color: string) => void;
  148. label: string;
  149. }) {
  150. const { t } = useTranslation();
  151. const [isActive, setActive] = React.useState(false);
  152. const pickerButton = React.useRef<HTMLButtonElement>(null);
  153. return (
  154. <div>
  155. <div className="color-picker-control-container">
  156. <button
  157. className="color-picker-label-swatch"
  158. aria-label={label}
  159. style={color ? { backgroundColor: color } : undefined}
  160. onClick={() => setActive(!isActive)}
  161. ref={pickerButton}
  162. />
  163. <ColorInput
  164. color={color}
  165. label={label}
  166. onChange={color => {
  167. onChange(color);
  168. }}
  169. />
  170. </div>
  171. <React.Suspense fallback="">
  172. {isActive ? (
  173. <Popover onCloseRequest={() => setActive(false)}>
  174. <Picker
  175. colors={colors[type]}
  176. color={color || null}
  177. onChange={changedColor => {
  178. onChange(changedColor);
  179. }}
  180. onClose={() => {
  181. setActive(false);
  182. pickerButton.current?.focus();
  183. }}
  184. label={label}
  185. t={t}
  186. />
  187. </Popover>
  188. ) : null}
  189. </React.Suspense>
  190. </div>
  191. );
  192. }
  193. // https://yeun.github.io/open-color/
  194. const colors = {
  195. // Shade 0
  196. canvasBackground: [
  197. "#ffffff",
  198. "#f8f9fa",
  199. "#f1f3f5",
  200. "#fff5f5",
  201. "#fff0f6",
  202. "#f8f0fc",
  203. "#f3f0ff",
  204. "#edf2ff",
  205. "#e7f5ff",
  206. "#e3fafc",
  207. "#e6fcf5",
  208. "#ebfbee",
  209. "#f4fce3",
  210. "#fff9db",
  211. "#fff4e6",
  212. ],
  213. // Shade 6
  214. elementBackground: [
  215. "transparent",
  216. "#ced4da",
  217. "#868e96",
  218. "#fa5252",
  219. "#e64980",
  220. "#be4bdb",
  221. "#7950f2",
  222. "#4c6ef5",
  223. "#228be6",
  224. "#15aabf",
  225. "#12b886",
  226. "#40c057",
  227. "#82c91e",
  228. "#fab005",
  229. "#fd7e14",
  230. ],
  231. // Shade 9
  232. elementStroke: [
  233. "#000000",
  234. "#343a40",
  235. "#495057",
  236. "#c92a2a",
  237. "#a61e4d",
  238. "#862e9c",
  239. "#5f3dc4",
  240. "#364fc7",
  241. "#1864ab",
  242. "#0b7285",
  243. "#087f5b",
  244. "#2b8a3e",
  245. "#5c940d",
  246. "#e67700",
  247. "#d9480f",
  248. ],
  249. };