Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 14 additions & 1 deletion src/autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,21 @@ export const Autocomplete = ({
}
};

const onBlur = () => {
const onBlur = (event: React.FocusEvent<HTMLInputElement>) => {
setShowPrompt(false);
let focusingOnInput = false;
let focusingOnOptions = false;
if (event.relatedTarget !== null) {
// checks to see if the element comming into focus is the specific input element
focusingOnInput = event.relatedTarget.id === id;
// checks to see if the element comming into focus is an option
focusingOnOptions = Object.keys(resultRef.current)
.map((value: string) => resultRef.current[parseInt(value, 10)])
.includes(event.relatedTarget as HTMLLIElement);
}
if (!(focusingOnInput || focusingOnOptions)) {
setShowOptions(false);
}
};

const setAccessibilityStatus = (newStatus: string) => {
Expand Down
1 change: 1 addition & 0 deletions src/autocomplete/ResultList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const ResultList = ({
aria-selected={index === activeOption}
aria-setsize={list.length}
aria-posinset={index + 1}
tabIndex={-1}
>
{optionName}
</li>
Expand Down
161 changes: 161 additions & 0 deletions src/autocomplete/__tests__/Autocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1282,4 +1282,165 @@ describe('Autocomplete', () => {
container.querySelector('label + div > div[role="status"]')
).toBeInTheDocument();
});

it('should close the options list on blur', async () => {
const user = userEvent.setup();
const { container } = render(
<Autocomplete
options={[
'Papaya',
'Persimmon',
'Paw Paw',
'Prickly Pear',
'Peach',
'Pomegranate',
'Pineapple',
]}
id="fruitTest"
labelText="search the list of fruits"
notFoundText="No fruit found"
resultId="fruit-options-container"
optionsId="fruit-option"
/>
);

const inputElm = screen.getByRole('combobox');
await user.type(inputElm, 'p');
expect(screen.getAllByRole('option').length).toBe(7);

act(() => {
fireEvent.blur(inputElm);
});
const options: any = container.querySelector('li');
expect(options).not.toBeInTheDocument();
});

it('should not close the options if the user clicks inside the input', async () => {
const user = userEvent.setup();
const { container } = render(
<Autocomplete
options={[
'Papaya',
'Persimmon',
'Paw Paw',
'Prickly Pear',
'Peach',
'Pomegranate',
'Pineapple',
]}
id="fruitTest"
labelText="search the list of fruits"
notFoundText="No fruit found"
resultId="fruit-options-container"
optionsId="fruit-option"
/>
);

const inputElm = screen.getByRole('combobox');
await user.type(inputElm, 'p');
expect(screen.getAllByRole('option').length).toBe(7);
userEvent.click(inputElm);
const options: any = container.querySelector('li');
expect(options).toBeInTheDocument();
});

it('should select the correct option and then close the options list', async () => {
const user = userEvent.setup();
const { container } = render(
<Autocomplete
options={[
'Papaya',
'Persimmon',
'Paw Paw',
'Prickly Pear',
'Peach',
'Pomegranate',
'Pineapple',
]}
id="fruitTest"
labelText="search the list of fruits"
notFoundText="No fruit found"
resultId="fruit-options-container"
optionsId="fruit-option"
/>
);

const inputElm = screen.getByRole('combobox');
await user.type(inputElm, 'p');
expect(screen.getAllByRole('option').length).toBe(7);
await userEvent.click(screen.getAllByRole('option')[2]);
const options: any = container.querySelector('li');
expect(options).not.toBeInTheDocument();
expect(inputElm.getAttribute('value')).toEqual('Paw Paw');
});

it('should close the options list when the user clicks on an uninteractive element', async () => {
const user = userEvent.setup();
const { container } = render(
<>
<Autocomplete
options={[
'Papaya',
'Persimmon',
'Paw Paw',
'Prickly Pear',
'Peach',
'Pomegranate',
'Pineapple',
]}
id="fruitTest"
labelText="search the list of fruits"
notFoundText="No fruit found"
resultId="fruit-options-container"
optionsId="fruit-option"
/>
<div>test</div>
</>
);

const inputElm = screen.getByRole('combobox');
const testElm = screen.getByText('test');
await user.type(inputElm, 'p');
expect(screen.getAllByRole('option').length).toBe(7);
await userEvent.click(testElm);
const options: any = container.querySelector('li');
expect(options).not.toBeInTheDocument();
// Shows that an option was not selected
expect(inputElm.getAttribute('value')).toEqual('p');
});

it('should close the options list when the user clicks on another input element', async () => {
const user = userEvent.setup();
const { container } = render(
<>
<Autocomplete
options={[
'Papaya',
'Persimmon',
'Paw Paw',
'Prickly Pear',
'Peach',
'Pomegranate',
'Pineapple',
]}
id="fruitTest"
labelText="search the list of fruits"
notFoundText="No fruit found"
resultId="fruit-options-container"
optionsId="fruit-option"
/>
<input type="text" id="country" name="country" />
</>
);

const inputElm = screen.getByRole('combobox');
const otherInputElm = screen.getAllByRole('textbox')[1];
await user.type(inputElm, 'p');
expect(screen.getAllByRole('option').length).toBe(7);
await userEvent.click(otherInputElm);
const options: any = container.querySelector('li');
expect(options).not.toBeInTheDocument();
// Shows that an option was not selected
expect(inputElm.getAttribute('value')).toEqual('p');
});
});
4 changes: 2 additions & 2 deletions src/formInput/FormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ type FormInputProps = {
/**
* function that will trigger when the input loses focus
**/
onBlur?: (event: React.FormEvent<HTMLInputElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
/**
* function that will check if is vald or not based on the validation rules
**/
Expand Down Expand Up @@ -207,7 +207,7 @@ export const FormInput = ({
if (onFocus) onFocus(event);
};

const handleBlur = (event: React.FormEvent<HTMLInputElement>) => {
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
if (onBlur) onBlur(event);
};

Expand Down
15 changes: 6 additions & 9 deletions stories/Autocomplete/ClassBased.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,16 @@ const options = [
export const Basic = {
name: 'Basic',
args: {
id: 'basic',
options: options,
minCharsBeforeSearch: 1,
debounceMs: 100,
containerClassName: 'govuk-form-group',
labelProps: 'govuk-label',
inputProps: { className: 'govuk-input' },
resultUlClass: 'autocomplete__menu',
resultlLiClass: 'autocomplete__option',
resultNoOptionClass: 'resultNoOptionClass',
resultActiveClass: 'autocomplete__option',
notFoundText: 'No fruit found',
resultActiveClass: 'autocomplete__option--focused',
},
Expand All @@ -253,7 +255,6 @@ export const Hint = {
resultUlClass: 'autocomplete__menu',
resultlLiClass: 'autocomplete__option',
resultNoOptionClass: 'resultNoOptionClass',
resultActiveClass: 'autocomplete__option',
notFoundText: 'No fruit found',
resultActiveClass: 'autocomplete__option--focused',
},
Expand All @@ -274,7 +275,6 @@ export const MinChars = {
resultUlClass: 'autocomplete__menu',
resultlLiClass: 'autocomplete__option',
resultNoOptionClass: 'resultNoOptionClass',
resultActiveClass: 'autocomplete__option',
notFoundText: 'No fruit found',
resultActiveClass: 'autocomplete__option--focused',
},
Expand All @@ -292,7 +292,6 @@ export const Defaultvalue = {
resultUlClass: 'autocomplete__menu',
resultlLiClass: 'autocomplete__option',
resultNoOptionClass: 'resultNoOptionClass',
resultActiveClass: 'autocomplete__option',
notFoundText: 'No fruit found',
defaultValue: 'Papaya',
resultActiveClass: 'autocomplete__option--focused',
Expand All @@ -312,7 +311,6 @@ export const NoResult = {
resultUlClass: 'autocomplete__menu',
resultlLiClass: 'autocomplete__option',
resultNoOptionClass: 'resultNoOptionClass',
resultActiveClass: 'autocomplete__option',
notFoundText: 'No fruit found',
resultActiveClass: 'autocomplete__option--focused',
},
Expand All @@ -333,7 +331,6 @@ export const Placeholder = {
resultUlClass: 'autocomplete__menu',
resultlLiClass: 'autocomplete__option',
resultNoOptionClass: 'resultNoOptionClass',
resultActiveClass: 'autocomplete__option',
notFoundText: 'No fruit found',
resultActiveClass: 'autocomplete__option--focused',
},
Expand All @@ -342,6 +339,7 @@ export const Placeholder = {
export const WithLabel = {
name: 'With label',
args: {
options: options,
minCharsBeforeSearch: 1,
debounceMs: 100,
hintText: 'Select your fruit',
Expand All @@ -350,7 +348,6 @@ export const WithLabel = {
resultUlClass: 'autocomplete__menu',
resultlLiClass: 'autocomplete__option',
resultNoOptionClass: 'resultNoOptionClass',
resultActiveClass: 'autocomplete__option',
notFoundText: 'No fruit found',
labelText: 'Search for a fruit',
resultActiveClass: 'autocomplete__option--focused',
Expand All @@ -368,14 +365,14 @@ export const WithError = {
);
},
args: {
options: options,
minCharsBeforeSearch: 1,
hintText: 'Select your fruit',
hintClass: 'autocomplete__label',
inputProps: { className: 'govuk-input' },
resultUlClass: 'autocomplete__menu',
resultlLiClas: 'autocomplete__option',
resultNoOptionClass: 'resultNoOptionClass',
resultActiveClass: 'autocomplete__option',
notFoundText: 'No fruit found',
errorPostion: 'after-hint',
errorMessageText: 'an error occured',
Expand Down Expand Up @@ -416,14 +413,14 @@ export const WithCustomSearch = {
);
},
args: {
options: options,
minCharsBeforeSearch: 1,
hintText: 'Select your fruit',
hintClass: 'autocomplete__label',
inputProps: { className: 'govuk-input' },
resultUlClass: 'autocomplete__menu',
resultlLiClas: 'autocomplete__option',
resultNoOptionClass: 'resultNoOptionClass',
resultActiveClass: 'autocomplete__option',
notFoundText: 'No fruit found',
errorPostion: 'after-hint',
errorMessageText: 'an error occured',
Expand Down