<div id="app"></div>
* {
  font-family: system-ui, Arial;
}

.container {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
}

.list-item {
  display: flex;
  flex-direction: row;
  gap: 5px;
  align-items: flex-start;
  justify-content: start;
}

.check {
  cursor: pointer;
}

.item-checked {
  text-decoration: line-through;
}

.dropdown-container {
  position: relative;
  display: inline-block;
}

/* Style for the input */
input {
  padding: 8px;
  font-size: 16px;
}

/* Style for the dropdown */
.dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 1;
  background-color: #fff;
  border: 1px solid #ccc;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  max-height: 200px;
  overflow-y: auto;
}

/* Style for individual dropdown items */
.dropdown-item-container {
  padding: 8px;
  cursor: pointer;
  &:hover {
    background-color: #f0f0f0;
  }
}

.dropdown-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 2px;
}

.highlighted {
  background-color: #f0f0f0;
}

.left {
}

.right {
  display: flex;
  gap: 5px;
}

.done-btn {
  display: flex;
  align-items: center;
  justify-content: center;
}

.quantity {
  width: 10px;
  height: 8px;
}

/*
 * https://frontendeval.com/questions/shopping-list
 *
 * Create a shopping list app with autocomplete item entry
 */

const App = () => {
  const [list, setList] = React.useState([]);
  const [search, setSearch] = React.useState("");
  const [showDropdown, setShowDropdown] = React.useState(false);
  const [shopList, setShopList] = React.useState([]);
  const [highlightedItem, setHighlightedItem] = React.useState(null);

  React.useEffect(() => {
    if (!search.length) {
      setList([]);
    }

    async function getResults() {
      const response = await fetch(
        `https://api.frontendeval.com/fake/food/${search}`
      );
      const data = await response.json();
      const listData = data.map((item) => {
        return {
          name: item,
          quantity: 0
        };
      });
      setList(listData);
    }

    const timeoutId = setTimeout(() => {
      search.length && getResults();
    }, 500);

    return () => clearTimeout(timeoutId);
  }, [search]);

  const handleClickOutsideDropdown = (e) => {
    if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
      setShowDropdown(false);
    }
  };

  const handleAddBtnClick = (item) => {
    setShopList((current) => {
      const obj = {
        id: Date.now(),
        name: item.name,
        quantity: item.quantity,
        checked: false
      };
      return [...current, obj];
    });
  };

  const handleKeyDown = (e) => {
    if (e.key === "ArrowDown" || e.key === "ArrowUp") {
      e.preventDefault(); // Prevent scrolling on arrow keys
      const currentIndex = list.findIndex(
        (item) => item.name === highlightedItem
      );
      const nextIndex =
        e.key === "ArrowDown"
          ? (currentIndex + 1) % list.length
          : (currentIndex - 1 + list.length) % list.length;
      setHighlightedItem(list[nextIndex]?.name || ""); // Highlight the next/previous item
    } else if (e.key === "Enter") {
      const selectedItem = list.find((item) => item.name === highlightedItem);
      if (selectedItem) {
        handleDropdownItemClick(selectedItem); // Add the highlighted item to the list
        setShowDropdown(false);
        setHighlightedItem(null); // Reset highlighted item after adding to the list
      }
    }
  };

  const handleQuantityChange = (e, item, changeType) => {
    setList((currList) => {
      const updatedList = currList.map((obj) => {
        if (obj.name === item.name) {
          return {
            ...obj,
            quantity:
              changeType === "increase" ? obj.quantity + 1 : obj.quantity - 1
          };
        }
        return obj;
      });
      return updatedList;
    });
  };

  const checkItem = (item) => {
    setShopList((currentList) => {
      const found = currentList.find((obj) => obj.id === item.id);
      found.checked = !found.checked;
      return currentList.map((curr) => {
        return curr.id === item.id ? { ...curr, ...found } : curr;
      });
    });
  };

  const removeItem = (item) => {
    setShopList((currentList) => {
      return currentList.filter((obj) => obj.id !== item.id);
    });
  };

  const handleDragStart = (e, item) => {
    console.log(item);
    e.dataTransfer.setData("text/plain", item.id.toString());
  };

  const handleDragOver = (e) => {
    e.preventDefault();
  };

  const handleDrop = (e, targetIndex) => {
    e.preventDefault();

    const draggedItemId = e.dataTransfer.getData("text/plain");
    const draggedItem = shopList.find(
      (item) => item.id.toString() === draggedItemId
    );

    if (draggedItem) {
      const updatedList = [...shopList];
      const currentIndex = updatedList.findIndex(
        (item) => item.id === draggedItem.id
      );
      updatedList.splice(currentIndex, 1);
      updatedList.splice(targetIndex, 0, draggedItem);
      setShopList(updatedList);
    }
  };

  return (
    <div className="container">
      <h2>My shopping list</h2>
      <div className="dropdown-container">
        <input
          type="text"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          onFocus={() => setShowDropdown(true)}
          onKeyDown={handleKeyDown}
          placeholder="Type to search..."
          aria-haspopup="listbox"
          aria-expanded={showDropdown}
          aria-owns="dropdownList"
        />
        {showDropdown && (
          <div
            className="dropdown"
            id="dropdownList"
            role="listbox"
            aria-label="Autocomplete suggestions"
          >
            {list.map((item) => {
              return (
                <div
                  className={`dropdown-item-container ${
                    highlightedItem === item.name ? "highlighted" : ""
                  }`}
                  key={item.name}
                  role="option"
                  aria-selected={highlightedItem === item.name}
                >
                  <div className="dropdown-item">
                    <div className="left">{item.name}</div>
                    <div className="right">
                      <div
                        className="plus-btn"
                        onClick={(e) =>
                          handleQuantityChange(e, item, "increase")
                        }
                      >
                        &#43;
                      </div>
                      <span
                        key={item.name + " quantity"}
                        className="selected-quantity"
                        aria-live="polite"
                      >
                        {item.quantity}
                      </span>
                      <div
                        className="minus-btn"
                        onClick={(e) =>
                          handleQuantityChange(e, item, "decrease")
                        }
                      >
                        &minus;
                      </div>
                      <button
                        onClick={() => handleAddBtnClick(item)}
                        type="button"
                      >
                        Add
                      </button>
                    </div>
                  </div>
                </div>
              );
            })}
            {!!list.length && showDropdown && (
              <div className="done-btn">
                <button onClick={() => setShowDropdown(false)} type="button">
                  Done
                </button>
              </div>
            )}
          </div>
        )}
      </div>

      {shopList.map((item, i) => {
        return (
          <div
            className="list-item"
            draggable
            onDragStart={(e) => handleDragStart(e, item)}
            onDragOver={handleDragOver}
            onDrop={(e) => handleDrop(e, i)}
          >
            <div
              className="check"
              role="checkbox"
              aria-checked={item.checked}
              onClick={() => checkItem(item)}
            >
              &#10004;
            </div>
            <div className={item?.checked ? "item-checked" : ""}>
              {item?.name}
            </div>
            <div className="selected-quantity" aria-live="polite">
              {item?.quantity}
            </div>
            <div
              className="check"
              onClick={() => removeItem(item)}
              tabIndex={0}
              role="button"
              aria-label={`Remove ${item.name}, button`}
            >
              &#10005;
            </div>
          </div>
        );
      })}
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("app"));
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js