<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")
}
>
+
</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")
}
>
−
</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)}
>
✔
</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`}
>
✕
</div>
</div>
);
})}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("app"));
View Compiled
This Pen doesn't use any external CSS resources.