From 731453588b93e02a113332d68f81e48bc11f7da7 Mon Sep 17 00:00:00 2001
From: Blaster4385 <blaster4385@tablaster.dev>
Date: Wed, 15 May 2024 22:55:13 +0530
Subject: feat: added keyboard accessibility

---
 .../src/components/CustomSelect/CustomSelect.jsx   | 111 ++++++++++++++++++---
 1 file changed, 98 insertions(+), 13 deletions(-)

(limited to 'client/src/components/CustomSelect/CustomSelect.jsx')

diff --git a/client/src/components/CustomSelect/CustomSelect.jsx b/client/src/components/CustomSelect/CustomSelect.jsx
index 3155700..4a906bd 100644
--- a/client/src/components/CustomSelect/CustomSelect.jsx
+++ b/client/src/components/CustomSelect/CustomSelect.jsx
@@ -1,35 +1,110 @@
 import React, { useEffect, useState, useRef } from "react";
+import { SUPPORTED_LANGUAGES } from "../../utils/constants";
+
 import styles from "./CustomSelect.module.css";
 
-const CustomSelect = ({ options, onSelect }) => {
+const CustomSelect = ({ onSelect, textAreaRef, isModalOpen }) => {
   const [isOpen, setIsOpen] = useState(false);
+  const [options, setOptions] = useState(SUPPORTED_LANGUAGES);
   const [selectedOption, setSelectedOption] = useState(
     options.length > 0 ? options[0] : null,
   );
+  const [focusedOptionIndex, setFocusedOptionIndex] = useState(0);
   const selectRef = useRef(null);
+  const searchRef = useRef(null);
+  const optionsRef = useRef([]);
 
   const toggleDropdown = () => {
     setIsOpen(!isOpen);
   };
 
-  const handleOptionClick = (option) => {
+  const handleOptionClick = (option, index) => {
     setSelectedOption(option);
     onSelect(option.value);
+    setFocusedOptionIndex(index);
     setIsOpen(false);
+    setOptions(SUPPORTED_LANGUAGES);
+    if (textAreaRef.current) {
+      textAreaRef.current.focus();
+    }
   };
 
   const handleClickOutside = (event) => {
     if (selectRef.current && !selectRef.current.contains(event.target)) {
       setIsOpen(false);
+      setOptions(SUPPORTED_LANGUAGES);
+
+      if (textAreaRef.current) {
+        textAreaRef.current.focus();
+      }
+    }
+  };
+
+  const handleChange = (e) => {
+    const searchVal = e.target.value;
+    const filteredOptions =
+      searchVal.length > 0
+        ? SUPPORTED_LANGUAGES.filter((option) =>
+            option.label.toLowerCase().includes(searchVal.toLowerCase()),
+          )
+        : SUPPORTED_LANGUAGES;
+
+    setOptions(filteredOptions);
+    setFocusedOptionIndex(0);
+  };
+
+  const handleKeyDown = (event) => {
+    if (isOpen) {
+      switch (event.key) {
+        case "ArrowDown":
+          setFocusedOptionIndex((prevIndex) => {
+            const newIndex =
+              prevIndex < options.length - 1 ? prevIndex + 1 : prevIndex;
+            optionsRef.current[newIndex].scrollIntoView({ block: "nearest" });
+            return newIndex;
+          });
+          break;
+        case "ArrowUp":
+          setFocusedOptionIndex((prevIndex) => {
+            const newIndex = prevIndex > 0 ? prevIndex - 1 : prevIndex;
+            optionsRef.current[newIndex].scrollIntoView({ block: "nearest" });
+            return newIndex;
+          });
+          break;
+        case "Enter":
+          event.preventDefault();
+          handleOptionClick(options[focusedOptionIndex], focusedOptionIndex);
+          break;
+        case "Escape":
+          setIsOpen(false);
+          setOptions(SUPPORTED_LANGUAGES);
+          if (textAreaRef.current) {
+            textAreaRef.current.focus();
+          }
+          break;
+        default:
+          break;
+      }
+    } else if (!isModalOpen && event.ctrlKey && event.key === "l") {
+      event.preventDefault();
+      setIsOpen(true);
     }
   };
 
   useEffect(() => {
     document.addEventListener("mousedown", handleClickOutside);
+    document.addEventListener("keydown", handleKeyDown);
     return () => {
       document.removeEventListener("mousedown", handleClickOutside);
+      document.removeEventListener("keydown", handleKeyDown);
     };
-  }, []);
+  }, [isOpen, options, focusedOptionIndex]);
+
+  useEffect(() => {
+    if (isOpen && searchRef.current) {
+      searchRef.current.focus();
+    }
+  }, [isOpen]);
 
   return (
     <div className={styles.select} ref={selectRef}>
@@ -45,16 +120,26 @@ const CustomSelect = ({ options, onSelect }) => {
         )}
       </div>
       {isOpen && (
-        <div className={styles.options}>
-          {options.map((option) => (
-            <div
-              key={option.value}
-              className={styles.option}
-              onClick={() => handleOptionClick(option)}
-            >
-              {option.label}
-            </div>
-          ))}
+        <div className={styles.options__container}>
+          <input
+            onChange={handleChange}
+            className={styles.options__search}
+            ref={searchRef}
+            placeholder="Search..."
+          />
+          <div className={styles.options}>
+            {options.map((option, index) => (
+              <div
+                key={option.value}
+                className={`${styles.option} ${index === focusedOptionIndex ? styles.focused : ""}`}
+                onClick={() => handleOptionClick(option, index)}
+                tabIndex={0}
+                ref={(el) => (optionsRef.current[index] = el)}
+              >
+                {option.label}
+              </div>
+            ))}
+          </div>
         </div>
       )}
     </div>
-- 
cgit v1.2.3-73-gaa49b

