Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fixes unexpected autosuggest dropdown reopen when clicking outside browser window and back #3176

Merged
merged 4 commits into from
Jan 17, 2025
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
33 changes: 22 additions & 11 deletions src/autosuggest/__tests__/autosuggest.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { KeyCode } from '@cloudscape-design/test-utils-core/utils';

import '../../__a11y__/to-validate-a11y';
import Autosuggest, { AutosuggestProps } from '../../../lib/components/autosuggest';
import { documentHasFocus } from '../../../lib/components/internal/utils/dom';
import createWrapper from '../../../lib/components/test-utils/dom';

import itemStyles from '../../../lib/components/internal/components/selectable-item/styles.css.js';
Expand Down Expand Up @@ -43,8 +44,15 @@ jest.mock('@cloudscape-design/component-toolkit/internal', () => {
warnOnce: jest.fn(),
};
});

jest.mock('../../../lib/components/internal/utils/dom', () => ({
...jest.requireActual('../../../lib/components/internal/utils/dom'),
documentHasFocus: jest.fn(() => true),
}));

beforeEach(() => {
(warnOnce as any).mockClear();
(warnOnce as jest.Mock).mockClear();
(documentHasFocus as jest.Mock).mockClear();
});

test('renders correct labels when focused', () => {
Expand Down Expand Up @@ -121,21 +129,24 @@ test('entered text option should not get screenreader override', () => {
).toBeFalsy();
});

test('should not close dropdown when no realted target in blur', () => {
const { wrapper, container } = renderAutosuggest(
<div>
<Autosuggest enteredTextLabel={v => v} value="1" options={defaultOptions} />
<button id="focus-target">focus target</button>
</div>
);
test('should close dropdown on blur when document is in focus', () => {
const { wrapper } = renderAutosuggest(<Autosuggest enteredTextLabel={v => v} value="1" options={defaultOptions} />);
wrapper.findNativeInput().focus();
expect(wrapper.findDropdown().findOpenDropdown()).not.toBe(null);

document.body.focus();
wrapper.findNativeInput().blur();
expect(wrapper.findDropdown().findOpenDropdown()).toBe(null);
});

test('should not close dropdown on blur when document is not in focus', () => {
(documentHasFocus as jest.Mock).mockImplementation(() => false);

const { wrapper } = renderAutosuggest(<Autosuggest enteredTextLabel={v => v} value="1" options={defaultOptions} />);
wrapper.findNativeInput().focus();
expect(wrapper.findDropdown().findOpenDropdown()).not.toBe(null);

createWrapper(container).find('#focus-target')!.focus();
expect(wrapper.findDropdown().findOpenDropdown()).toBe(null);
wrapper.findNativeInput().blur();
expect(wrapper.findDropdown().findOpenDropdown()).not.toBe(null);
});

it('should warn if recoveryText is provided without associated handler', () => {
Expand Down
6 changes: 6 additions & 0 deletions src/internal/components/autosuggest-input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { FormFieldValidationControlProps, useFormFieldContext } from '../../cont
import { BaseKeyDetail, fireCancelableEvent, fireNonCancelableEvent, NonCancelableEventHandler } from '../../events';
import { InternalBaseComponentProps } from '../../hooks/use-base-component';
import { KeyCode } from '../../keycode';
import { documentHasFocus } from '../../utils/dom';
import { nodeBelongs } from '../../utils/node-belongs';
import Dropdown from '../dropdown';
import { ExpandToViewport } from '../dropdown/interfaces';
Expand Down Expand Up @@ -130,6 +131,11 @@ const AutosuggestInput = React.forwardRef(
}));

const handleBlur = () => {
// When the document in not in focus it means the focus moved outside the page. In that case the dropdown shall stay open
// so that when the user comes back it does not re-open unexpectedly when the focus is restored by the browser.
if (!documentHasFocus()) {
return;
}
if (!preventCloseOnBlurRef.current) {
closeDropdown();
fireNonCancelableEvent(onBlur, null);
Expand Down
8 changes: 8 additions & 0 deletions src/internal/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,11 @@ export function isSVGElement(target: unknown): target is SVGElement {
typeof target.ownerSVGElement === 'object')
);
}

export function documentHasFocus() {
// In JSDOM the hasFocus() always returns false which differs from the in-browser experience, see: https://github.com/jsdom/jsdom/issues/3794.
// Thus, when detecting JSDOM environment we return true here explicitly for the tests to work expectedly.
// The detection depends on the default userAgent set in JSDOM, see: https://github.com/jsdom/jsdom?tab=readme-ov-file#advanced-configuration.
const isJSDOM = typeof navigator !== 'undefined' && navigator.userAgent?.includes('jsdom');
return isJSDOM || (typeof document !== undefined && document.hasFocus());
}
Loading