Browse Source

Add keybindings for color picker (#647)

* Add keybindings for color picker

This adds the ability to navigate using left/right/bottom/up keys and shows key bindings for all the different colors. This is only optimized for the qwerty keyboard layout, but unfortunately it's not possible to detect other keyboard layouts :(

* add aria-keyshortcuts and keybinding in title

* make focus select color, confirm on enter

Co-authored-by: David Luzar <luzar.david@gmail.com>
Christopher Chedeau 5 years ago
parent
commit
46791e6da1
2 changed files with 76 additions and 12 deletions
  1. 8 0
      src/components/ColorPicker.css
  2. 68 12
      src/components/ColorPicker.tsx

+ 8 - 0
src/components/ColorPicker.css

@@ -107,3 +107,11 @@
   margin-right: 0.25rem;
   border: 1px solid #dee2e6;
 }
+
+.color-picker-keybinding {
+  position: absolute;
+  bottom: 2px;
+  right: 2px;
+  font-size: 0.7em;
+  color: #ccc;
+}

+ 68 - 12
src/components/ColorPicker.tsx

@@ -8,6 +8,14 @@ import { t } from "../i18n";
 // 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
 
+// Unfortunately, we can't detect keyboard layout in the browser. So this will
+// only work well for QWERTY but not AZERTY or others...
+const keyBindings = [
+  ["1", "2", "3", "4", "5"],
+  ["q", "w", "e", "r", "t"],
+  ["a", "s", "d", "f", "g"],
+].flat();
+
 const Picker = function({
   colors,
   color,
@@ -22,12 +30,18 @@ const Picker = function({
   label: string;
 }) {
   const firstItem = React.useRef<HTMLButtonElement>();
+  const activeItem = React.useRef<HTMLButtonElement>();
+  const gallery = React.useRef<HTMLDivElement>();
   const colorInput = React.useRef<HTMLInputElement>();
 
   React.useEffect(() => {
     // After the component is first mounted
     // focus on first input
-    if (firstItem.current) firstItem.current.focus();
+    if (activeItem.current) {
+      activeItem.current.focus();
+    } else if (firstItem.current) {
+      firstItem.current.focus();
+    }
   }, []);
 
   const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -44,10 +58,41 @@ const Picker = function({
           e.preventDefault();
         }
       }
-    } else if (e.key === KEYS.ESCAPE) {
+    } else if (
+      e.key === KEYS.ARROW_RIGHT ||
+      e.key === KEYS.ARROW_LEFT ||
+      e.key === KEYS.ARROW_UP ||
+      e.key === KEYS.ARROW_DOWN
+    ) {
+      const { activeElement } = document;
+      const index = Array.prototype.indexOf.call(
+        gallery!.current!.children,
+        activeElement,
+      );
+      if (index !== -1) {
+        const length = gallery!.current!.children.length;
+        const nextIndex =
+          e.key === KEYS.ARROW_RIGHT
+            ? (index + 1) % length
+            : e.key === KEYS.ARROW_LEFT
+            ? (length + index - 1) % length
+            : e.key === KEYS.ARROW_DOWN
+            ? (index + 5) % length
+            : e.key === KEYS.ARROW_UP
+            ? (length + index - 5) % length
+            : index;
+        (gallery!.current!.children![nextIndex] as any).focus();
+      }
+      e.preventDefault();
+    } else if (keyBindings.includes(e.key.toLowerCase())) {
+      const index = keyBindings.indexOf(e.key.toLowerCase());
+      (gallery!.current!.children![index] as any).focus();
+      e.preventDefault();
+    } else if (e.key === KEYS.ESCAPE || e.key === KEYS.ENTER) {
+      e.preventDefault();
       onClose();
-      e.nativeEvent.stopImmediatePropagation();
     }
+    e.nativeEvent.stopImmediatePropagation();
   };
 
   return (
@@ -61,26 +106,37 @@ const Picker = function({
       <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, i) => (
+        <div
+          className="colors-gallery"
+          ref={el => {
+            if (el) gallery.current = el;
+          }}
+        >
+          {colors.map((_color, i) => (
             <button
               className="color-picker-swatch"
               onClick={() => {
-                onChange(color);
+                onChange(_color);
               }}
-              title={color}
-              aria-label={color}
-              style={{ backgroundColor: color }}
-              key={color}
+              title={`${_color} — ${keyBindings[i].toUpperCase()}`}
+              aria-label={_color}
+              aria-keyshortcuts={keyBindings[i]}
+              style={{ backgroundColor: _color }}
+              key={_color}
               ref={el => {
-                if (i === 0 && el) firstItem.current = el;
+                if (el && i === 0) firstItem.current = el;
+                if (el && _color === color) activeItem.current = el;
+              }}
+              onFocus={() => {
+                onChange(_color);
               }}
             >
-              {color === "transparent" ? (
+              {_color === "transparent" ? (
                 <div className="color-picker-transparent"></div>
               ) : (
                 undefined
               )}
+              <span className="color-picker-keybinding">{keyBindings[i]}</span>
             </button>
           ))}
         </div>