ColorPicker.tsx 6.0 KB

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