|
@@ -2,6 +2,9 @@ import React from "react";
|
|
|
import { Popover } from "./Popover";
|
|
|
|
|
|
import "./ColorPicker.css";
|
|
|
+import { KEYS } from "../keys";
|
|
|
+import { useTranslation } from "react-i18next";
|
|
|
+import { TFunction } from "i18next";
|
|
|
|
|
|
// 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
|
|
@@ -10,29 +13,71 @@ const Picker = function({
|
|
|
colors,
|
|
|
color,
|
|
|
onChange,
|
|
|
+ onClose,
|
|
|
label,
|
|
|
+ t,
|
|
|
}: {
|
|
|
colors: string[];
|
|
|
color: string | null;
|
|
|
onChange: (color: string) => void;
|
|
|
+ onClose: () => void;
|
|
|
label: string;
|
|
|
+ t: TFunction;
|
|
|
}) {
|
|
|
+ const firstItem = React.useRef<HTMLButtonElement>();
|
|
|
+ const colorInput = React.useRef<HTMLInputElement>();
|
|
|
+
|
|
|
+ React.useEffect(() => {
|
|
|
+ // After the component is first mounted
|
|
|
+ // focus on first input
|
|
|
+ if (firstItem.current) firstItem.current.focus();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
|
+ if (e.key === KEYS.TAB) {
|
|
|
+ const { activeElement } = document;
|
|
|
+ if (e.shiftKey) {
|
|
|
+ if (activeElement === firstItem.current) {
|
|
|
+ colorInput.current?.focus();
|
|
|
+ e.preventDefault();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (activeElement === colorInput.current) {
|
|
|
+ firstItem.current?.focus();
|
|
|
+ e.preventDefault();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (e.key === KEYS.ESCAPE) {
|
|
|
+ onClose();
|
|
|
+ e.nativeEvent.stopImmediatePropagation();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
return (
|
|
|
- <div className="color-picker">
|
|
|
+ <div
|
|
|
+ className="color-picker"
|
|
|
+ role="dialog"
|
|
|
+ aria-modal="true"
|
|
|
+ aria-label={t("labels.colorPicker")}
|
|
|
+ onKeyDown={handleKeyDown}
|
|
|
+ >
|
|
|
<div className="color-picker-triangle-shadow"></div>
|
|
|
<div className="color-picker-triangle"></div>
|
|
|
<div className="color-picker-content">
|
|
|
<div className="colors-gallery">
|
|
|
- {colors.map(color => (
|
|
|
+ {colors.map((color, i) => (
|
|
|
<button
|
|
|
className="color-picker-swatch"
|
|
|
onClick={() => {
|
|
|
onChange(color);
|
|
|
}}
|
|
|
title={color}
|
|
|
- tabIndex={0}
|
|
|
+ aria-label={color}
|
|
|
style={{ backgroundColor: color }}
|
|
|
key={color}
|
|
|
+ ref={el => {
|
|
|
+ if (i === 0 && el) firstItem.current = el;
|
|
|
+ }}
|
|
|
>
|
|
|
{color === "transparent" ? (
|
|
|
<div className="color-picker-transparent"></div>
|
|
@@ -48,49 +93,59 @@ const Picker = function({
|
|
|
onChange={color => {
|
|
|
onChange(color);
|
|
|
}}
|
|
|
+ ref={colorInput}
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
-function ColorInput({
|
|
|
- color,
|
|
|
- onChange,
|
|
|
- label,
|
|
|
-}: {
|
|
|
- color: string | null;
|
|
|
- onChange: (color: string) => void;
|
|
|
- label: string;
|
|
|
-}) {
|
|
|
- const colorRegex = /^([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8}|transparent)$/;
|
|
|
- const [innerValue, setInnerValue] = React.useState(color);
|
|
|
+const ColorInput = React.forwardRef(
|
|
|
+ (
|
|
|
+ {
|
|
|
+ color,
|
|
|
+ onChange,
|
|
|
+ label,
|
|
|
+ }: {
|
|
|
+ color: string | null;
|
|
|
+ onChange: (color: string) => void;
|
|
|
+ label: string;
|
|
|
+ },
|
|
|
+ ref,
|
|
|
+ ) => {
|
|
|
+ const colorRegex = /^([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8}|transparent)$/;
|
|
|
+ const [innerValue, setInnerValue] = React.useState(color);
|
|
|
+ const inputRef = React.useRef(null);
|
|
|
|
|
|
- React.useEffect(() => {
|
|
|
- setInnerValue(color);
|
|
|
- }, [color]);
|
|
|
+ React.useEffect(() => {
|
|
|
+ setInnerValue(color);
|
|
|
+ }, [color]);
|
|
|
|
|
|
- return (
|
|
|
- <div className="color-input-container">
|
|
|
- <div className="color-picker-hash">#</div>
|
|
|
- <input
|
|
|
- spellCheck={false}
|
|
|
- className="color-picker-input"
|
|
|
- aria-label={label}
|
|
|
- onChange={e => {
|
|
|
- const value = e.target.value;
|
|
|
- if (value.match(colorRegex)) {
|
|
|
- onChange(value === "transparent" ? "transparent" : "#" + value);
|
|
|
- }
|
|
|
- setInnerValue(value);
|
|
|
- }}
|
|
|
- value={(innerValue || "").replace(/^#/, "")}
|
|
|
- onPaste={e => onChange(e.clipboardData.getData("text"))}
|
|
|
- onBlur={() => setInnerValue(color)}
|
|
|
- />
|
|
|
- </div>
|
|
|
- );
|
|
|
-}
|
|
|
+ React.useImperativeHandle(ref, () => inputRef.current);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="color-input-container">
|
|
|
+ <div className="color-picker-hash">#</div>
|
|
|
+ <input
|
|
|
+ spellCheck={false}
|
|
|
+ className="color-picker-input"
|
|
|
+ aria-label={label}
|
|
|
+ onChange={e => {
|
|
|
+ const value = e.target.value;
|
|
|
+ if (value.match(colorRegex)) {
|
|
|
+ onChange(value === "transparent" ? "transparent" : "#" + value);
|
|
|
+ }
|
|
|
+ setInnerValue(value);
|
|
|
+ }}
|
|
|
+ value={(innerValue || "").replace(/^#/, "")}
|
|
|
+ onPaste={e => onChange(e.clipboardData.getData("text"))}
|
|
|
+ onBlur={() => setInnerValue(color)}
|
|
|
+ ref={inputRef}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ },
|
|
|
+);
|
|
|
|
|
|
export function ColorPicker({
|
|
|
type,
|
|
@@ -103,7 +158,10 @@ export function ColorPicker({
|
|
|
onChange: (color: string) => void;
|
|
|
label: string;
|
|
|
}) {
|
|
|
+ const { t } = useTranslation();
|
|
|
+
|
|
|
const [isActive, setActive] = React.useState(false);
|
|
|
+ const pickerButton = React.useRef<HTMLButtonElement>(null);
|
|
|
|
|
|
return (
|
|
|
<div>
|
|
@@ -113,6 +171,7 @@ export function ColorPicker({
|
|
|
aria-label={label}
|
|
|
style={color ? { backgroundColor: color } : undefined}
|
|
|
onClick={() => setActive(!isActive)}
|
|
|
+ ref={pickerButton}
|
|
|
/>
|
|
|
<ColorInput
|
|
|
color={color}
|
|
@@ -131,7 +190,12 @@ export function ColorPicker({
|
|
|
onChange={changedColor => {
|
|
|
onChange(changedColor);
|
|
|
}}
|
|
|
+ onClose={() => {
|
|
|
+ setActive(false);
|
|
|
+ pickerButton.current?.focus();
|
|
|
+ }}
|
|
|
label={label}
|
|
|
+ t={t}
|
|
|
/>
|
|
|
</Popover>
|
|
|
) : null}
|