Created
July 2, 2020 16:50
-
-
Save jacobvr/4b425f8c1b748819f469b01ee294a4dd to your computer and use it in GitHub Desktop.
Typeahead interview challenge
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect, useRef } from "react"; | |
import { arrayOf, string } from "prop-types"; | |
import ReactDOM from "react-dom"; | |
import "./styles.css"; | |
/** | |
* Please note that this app is intentionally in a broken state. You must create your | |
* component and add it to `ReactDOM.render` (at the bottom of this file) to get started. | |
*/ | |
/** | |
* Using React create a `Typeahead` component that takes `list` and `classname` props. | |
* Be sure that the component utilizes propTypes and any other best practices | |
* that you follow. Use the `carBrands` list which is defined below as the | |
* value for the `list` prop. | |
* | |
* Ensure that your component meets the following requirements: | |
* 1. As the user types in an input field, a list of options should appear below it. | |
* - The list should only appear when input is not empty. Whitespace is | |
* considered empty. | |
* - The list should contain items from the `list` prop that *start* | |
* with the user entered value. Matching should be case insensitive. | |
* - Every new character typed should filter the list. | |
* 2. Clicking on a list item should populate the input with the selected item's | |
* value and hide the list. | |
* 3. For visible option strings, style the substring the user has entered as | |
* *bold*. | |
* 4. Highlight a list item with gray background and white | |
* text when the user mouses over it. | |
* 5. The input and list should be navigable using the keyboard. | |
* - Using `tab` and `shift+tab`, the user should be able to focus the different | |
* list items. | |
* - With the cursor in the input, pressing the `tab` key should focus the | |
* first item with the default browser focus style. | |
* - Subsequent presses of the "tab" key should focus the next item in the list. | |
* - Pressing the `shift+tab` keys should focus the previous item in the list. | |
* - Pressing the `shift+tab` key when the first item is focused should focus | |
* the input again. | |
* - Mousing over other list items should highlight them while the keyboard- | |
* focused item remains focused. | |
* - Pressing the `tab` key when no list is visible should move focus away | |
* from the input. | |
* - Pressing the `return` key when an item is focused should populate the input | |
* with the focused item's value, hide the list, and focus the input | |
* again. | |
* - Pressing the `escape` key should close the list. | |
* 6. Clicking outside the input or the list should close the list. | |
*/ | |
/** | |
* Please don't change the `carBrands` list. | |
*/ | |
const carBrands = [ | |
"Alfa Romeo", | |
"Audi", | |
"BMW", | |
"Chevrolet", | |
"Chrysler", | |
"Dodge", | |
"Ferrari", | |
"Fiat", | |
"Ford", | |
"Honda", | |
"Hyundai", | |
"Jaguar", | |
"Jeep", | |
"Kia", | |
"Mazda", | |
"Mercedez-Benz", | |
"Mitsubishi", | |
"Nissan", | |
"Peugeot", | |
"Porsche", | |
"SAAB", | |
"Subaru", | |
"Suzuki", | |
"Toyota", | |
"Volkswagen", | |
"Volvo" | |
]; | |
const QueryItem = ({ query, item, handleKeyDown, handleItemClick }) => ( | |
<li | |
tabIndex="0" | |
className="item" | |
onClick={() => handleItemClick(item)} | |
onKeyDown={e => handleKeyDown(e, item)} | |
> | |
<strong>{item.substr(0, query.length)}</strong> | |
{item.substr(query.length)} | |
</li> | |
); | |
QueryItem.propTypes = { | |
query: string.isRequired, | |
item: string.isRequired | |
}; | |
const Typeahead = ({ list, classname }) => { | |
const inputRef = useRef(); | |
const [query, setQuery] = useState(""); | |
const [sortedList, setSortedList] = useState([]); | |
const [isListVisible, setIsListVisible] = useState(false); | |
useEffect(() => { | |
document.addEventListener("keydown", handleKeyDown, false); | |
document.addEventListener("click", handleDocumentClick, false); | |
return () => { | |
document.removeEventListener("keydown", handleKeyDown, false); | |
document.removeEventListener("click", handleDocumentClick, false); | |
}; | |
}); | |
useEffect(() => { | |
if (query && query.trim()) { | |
setSortedList( | |
list.filter(item => item.toLowerCase().startsWith(query.toLowerCase())) | |
); | |
} else { | |
setSortedList([]); | |
} | |
}, [query, list]); | |
const handleInputChange = query => { | |
setQuery(query); | |
setIsListVisible(true); | |
}; | |
const handleItemClick = item => { | |
setQuery(item); | |
setIsListVisible(false); | |
}; | |
const handleKeyDown = ({ key }, item) => { | |
if (key === "Escape") { | |
setIsListVisible(false); | |
} | |
if (key === "Enter" && item) { | |
setQuery(item, true); | |
setIsListVisible(false); | |
if (inputRef.current) { | |
inputRef.current.focus(); | |
} | |
} | |
}; | |
const handleDocumentClick = e => { | |
if (inputRef.current && e.target !== inputRef.current) { | |
setIsListVisible(false); | |
} | |
}; | |
return ( | |
<> | |
<input | |
value={query} | |
ref={inputRef} | |
onChange={({ target: { value } }) => handleInputChange(value)} | |
/> | |
{isListVisible && ( | |
<ul className="list"> | |
{sortedList.map(item => ( | |
<QueryItem | |
key={item} | |
item={item} | |
query={query} | |
handleKeyDown={handleKeyDown} | |
handleItemClick={handleItemClick} | |
/> | |
))} | |
</ul> | |
)} | |
</> | |
); | |
}; | |
Typeahead.propTypes = { | |
list: arrayOf(string).isRequired, | |
classname: string.isRequired | |
}; | |
ReactDOM.render( | |
<Typeahead list={carBrands} classname="" />, | |
document.getElementById("root") | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment