Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ that will suggest people matching an entered text.
- Install Prettier Extention and use this [VSCode settings](https://mate-academy.github.io/fe-program/tools/vscode/settings.json) to enable format on save.
- Implement a solution following the [React task guideline](https://github.com/mate-academy/react_task-guideline#react-tasks-guideline).
- Use the [React TypeScript cheat sheet](https://mate-academy.github.io/fe-program/js/extra/react-typescript).
- Replace `<your_account>` with your Github username in the [DEMO LINK](https://<your_account>.github.io/react_autocomplete/) and add it to the PR description.
- Replace `<your_account>` with your Github username in the [DEMO LINK](https://anastasiiadns.github.io/react_autocomplete/) and add it to the PR description.
- Don't remove the `data-qa` attributes. It is required for tests.

## Troubleshooting
Expand Down
12 changes: 7 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"devDependencies": {
"@cypress/react18": "^2.0.1",
"@faker-js/faker": "^8.4.1",
"@mate-academy/scripts": "^1.8.5",
"@mate-academy/scripts": "^2.1.3",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/lodash.debounce": "^4.0.9",
Expand Down
123 changes: 82 additions & 41 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,56 @@
import React from 'react';
import './App.scss';
import { peopleFromServer } from './data/people';
import { Person } from './types/Person';
import React, { useCallback, useState } from 'react';
import debounce from 'lodash.debounce';

export const App: React.FC = () => {
const { name, born, died } = peopleFromServer[0];
type Props = {
persons: Person[];
delay?: number;
onSelected?: (person: Person) => void;
};

export const App: React.FC<Props> = ({ persons, delay = 300, onSelected }) => {
const [query, setQuery] = useState('');
const [appliedQuery, setAppliedQuery] = useState('');
const [selectedPerson, setSelectedPerson] = useState<Person | null>(null);
const [isFocused, setIsFocused] = useState(false);

const applyQuery = useCallback(debounce(setAppliedQuery, delay), [delay]);

Check warning on line 18 in src/App.tsx

View workflow job for this annotation

GitHub Actions / run_linter (20.x)

React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debounced updater is created with useCallback(debounce(setAppliedQuery, delay), [delay]) but there is no cleanup to cancel pending debounced calls on unmount or when delay changes. It is safer to create the debounced function inside a useMemo/useEffect and call .cancel() in cleanup to avoid possible stale calls or memory leaks.


React.useEffect(() => {
return () => {
applyQuery.cancel?.();
};
}, [applyQuery]);

const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;

setQuery(value);
setSelectedPerson(null);

const trimmed = value.trim();

if (!trimmed) {
setAppliedQuery('');

return;
}

applyQuery(trimmed);
Comment on lines +31 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handler always calls the debounced updater for any non-empty trimmed value. This violates the requirement “don't run filtering again if the text has not changed (a pause in typing happened when the text was the same as before)”. Add a check comparing the new trimmed value to the current appliedQuery and skip calling applyQuery if they are equal (so filtering isn't re-run unnecessarily).

};

const filteredPeople = persons.filter(person =>
person.name.toLowerCase().includes(appliedQuery.trim().toLowerCase()),
);

return (
<div className="container">
<main className="section is-flex is-flex-direction-column">
<h1 className="title" data-cy="title">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task and tests require data-qa attributes, but this element uses data-cy="title". Rename it to data-qa="title" so tests can locate the element.

{`${name} (${born} - ${died})`}
{selectedPerson
? `${selectedPerson.name} (${selectedPerson.born} - ${selectedPerson.died})`
: 'No selected person'}
</h1>

<div className="dropdown is-active">
Expand All @@ -19,55 +60,55 @@
placeholder="Enter a part of the name"
className="input"
data-cy="search-input"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input uses data-cy="search-input". The instructions and tests require data-qa attributes. Rename to data-qa="search-input".

value={query}
onChange={handleQueryChange}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
</div>

<div className="dropdown-menu" role="menu" data-cy="suggestions-list">
<div className="dropdown-content">
<div className="dropdown-item" data-cy="suggestion-item">
<p className="has-text-link">Pieter Haverbeke</p>
</div>

<div className="dropdown-item" data-cy="suggestion-item">
<p className="has-text-link">Pieter Bernard Haverbeke</p>
</div>

<div className="dropdown-item" data-cy="suggestion-item">
<p className="has-text-link">Pieter Antone Haverbeke</p>
</div>

<div className="dropdown-item" data-cy="suggestion-item">
<p className="has-text-danger">Elisabeth Haverbeke</p>
</div>

<div className="dropdown-item" data-cy="suggestion-item">
<p className="has-text-link">Pieter de Decker</p>
</div>

<div className="dropdown-item" data-cy="suggestion-item">
<p className="has-text-danger">Petronella de Decker</p>
</div>

<div className="dropdown-item" data-cy="suggestion-item">
<p className="has-text-danger">Elisabeth Hercke</p>
{isFocused && filteredPeople.length > 0 && (
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggestions list visibility depends on filteredPeople (which is derived from the debounced appliedQuery) and isFocused. Because filteredPeople uses the debounced value, focusing an input that is immediately empty may not reliably show the full list if appliedQuery is still stale. The requirement says: "show the list of all people when the input is focused but empty" — consider using the immediate query (trimmed) to decide to show all people when focus occurs, instead of waiting for the debounced value.

<div
className="dropdown-menu is-active"
role="menu"
data-cy="suggestions-list"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggestions list uses data-cy="suggestions-list". Rename to data-qa="suggestions-list" to match the task/test requirements.

>
<div className="dropdown-content">
{filteredPeople.map(person => (
<div
key={person.name}
className="dropdown-item"
data-cy="suggestion-item"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each suggestion item uses data-cy="suggestion-item". Change it to data-qa="suggestion-item" as required by the task.

onMouseDown={() => {
setSelectedPerson(person);
setQuery(person.name);
setAppliedQuery(person.name);
onSelected?.(person);
}}
Comment on lines +86 to +92
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a suggestion is selected you set the input value and selectedPerson, but you don't explicitly close the dropdown. The spec requires you to “save selected suggestion text to the input on click and close the list;” ensure you call setIsFocused(false) (or otherwise hide the dropdown) when selecting an item so the list actually closes immediately.

>
<p className="has-text-link">{person.name}</p>
</div>
))}
</div>
</div>
</div>
)}
</div>

<div
className="
{(appliedQuery || query) && filteredPeople.length === 0 && (
<div
className="
notification
is-danger
is-light
mt-3
is-align-self-flex-start
"
role="alert"
data-cy="no-suggestions-message"
>
<p className="has-text-danger">No matching suggestions</p>
</div>
role="alert"
data-cy="no-suggestions-message"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The no-results notification uses data-cy="no-suggestions-message". Update it to data-qa="no-suggestions-message" so tests can find it.

>
<p className="has-text-danger">No matching suggestions</p>
</div>
)}
Comment on lines 101 to +116
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ‘no suggestions’ message visibility uses (appliedQuery || query) && filteredPeople.length === 0, which can show the message too early (before debounce applies) because it depends on the immediate query. To match the expected behavior, drive the “No matching suggestions” message (and dropdown visibility) from the debounced/applied value (appliedQuery) so the UI updates only after debounce.

</main>
</div>
);
Expand Down
5 changes: 4 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@ import { createRoot } from 'react-dom/client';

import 'bulma/css/bulma.css';
import { App } from './App';
import { peopleFromServer } from './data/people';

createRoot(document.getElementById('root') as HTMLDivElement).render(<App />);
createRoot(document.getElementById('root') as HTMLDivElement).render(
<App persons={peopleFromServer} />,
);
Loading