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(autocomplete-js): leave the modal open on reset on pointer devices #987

Merged
merged 12 commits into from
Jun 21, 2022
Merged
76 changes: 72 additions & 4 deletions packages/autocomplete-core/src/__tests__/concurrency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ describe('concurrency', () => {
expect(getSources).toHaveBeenCalledTimes(3);
});

test('keeps the panel closed on blur', async () => {
test('keeps the panel closed on Enter', async () => {
const onStateChange = jest.fn();
const { timeout, delayedGetSources } = createDelayedGetSources({
sources: [100, 200],
Expand Down Expand Up @@ -188,7 +188,74 @@ describe('concurrency', () => {
expect(getSources).toHaveBeenCalledTimes(2);
});

test('keeps the panel closed on touchstart blur', async () => {
test('keeps the panel closed on click outside', async () => {
const onStateChange = jest.fn();
const { timeout, delayedGetSources } = createDelayedGetSources({
sources: [100, 200],
});
const getSources = jest.fn(delayedGetSources);

const {
inputElement,
getEnvironmentProps,
formElement,
} = createPlayground(createAutocomplete, {
onStateChange,
getSources,
});

const panelElement = document.createElement('div');

const { onMouseDown } = getEnvironmentProps({
inputElement,
formElement,
panelElement,
});
window.addEventListener('mousedown', onMouseDown);

userEvent.type(inputElement, 'a');

await runAllMicroTasks();

// The search request is triggered
expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
status: 'loading',
query: 'a',
}),
})
);

userEvent.click(document.body);

// The status is immediately set to "idle" and the panel is closed
expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
status: 'idle',
isOpen: false,
query: 'a',
}),
})
);

await defer(noop, timeout);

// Once the request is settled, the state remains unchanged
expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
status: 'idle',
isOpen: false,
}),
})
);

expect(getSources).toHaveBeenCalledTimes(1);
});

test('keeps the panel closed on touchstart', async () => {
const onStateChange = jest.fn();
const { timeout, delayedGetSources } = createDelayedGetSources({
sources: [100, 200],
Expand Down Expand Up @@ -227,8 +294,9 @@ describe('concurrency', () => {
})
);

const customEvent = new CustomEvent('touchstart', { bubbles: true });
window.document.dispatchEvent(customEvent);
window.document.dispatchEvent(
new CustomEvent('touchstart', { bubbles: true })
);

// The status is immediately set to "idle" and the panel is closed
expect(onStateChange).toHaveBeenLastCalledWith(
Expand Down
261 changes: 260 additions & 1 deletion packages/autocomplete-core/src/__tests__/getEnvironmentProps.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import userEvent from '@testing-library/user-event';

import {
createPlayground,
createSource,
Expand Down Expand Up @@ -29,6 +31,202 @@ describe('getEnvironmentProps', () => {
);
});

describe('onMouseDown', () => {
test('is a noop when panel is not open and status is idle', () => {
const onStateChange = jest.fn();
const {
getEnvironmentProps,
inputElement,
formElement,
} = createPlayground(createAutocomplete, { onStateChange });
const panelElement = document.createElement('div');

const { onMouseDown } = getEnvironmentProps({
inputElement,
formElement,
panelElement,
});
window.addEventListener('mousedown', onMouseDown);

// Dispatch MouseDown event on window
const customEvent = new CustomEvent('mousedown', { bubbles: true });
window.dispatchEvent(customEvent);

expect(onStateChange).not.toHaveBeenCalled();

window.removeEventListener('mousedown', onMouseDown);
});

test('is a noop when the event target is the input element', async () => {
const onStateChange = jest.fn();
const {
getEnvironmentProps,
inputElement,
formElement,
} = createPlayground(createAutocomplete, {
onStateChange,
openOnFocus: true,
getSources() {
return [
createSource({
getItems: () => [{ label: '1' }],
}),
];
},
});
const panelElement = document.createElement('div');

const { onMouseDown } = getEnvironmentProps({
inputElement,
formElement,
panelElement,
});
window.addEventListener('mousedown', onMouseDown);

// Click input (focuses it, which opens the panel)
userEvent.click(inputElement);

await runAllMicroTasks();

expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
isOpen: true,
}),
})
);

onStateChange.mockClear();

// Dispatch MouseDown event on the input (bubbles to window)
const customEvent = new CustomEvent('mousedown', { bubbles: true });
inputElement.dispatchEvent(customEvent);

await runAllMicroTasks();

expect(onStateChange).not.toHaveBeenCalled();

window.removeEventListener('mousedown', onMouseDown);
});

test('closes panel and resets `activeItemId` if the target is outside Autocomplete', async () => {
const onStateChange = jest.fn();
const {
getEnvironmentProps,
inputElement,
formElement,
} = createPlayground(createAutocomplete, {
onStateChange,
openOnFocus: true,
defaultActiveItemId: 1,
getSources() {
return [
createSource({
getItems: () => [{ label: '1' }],
}),
];
},
});
const panelElement = document.createElement('div');

const { onMouseDown } = getEnvironmentProps({
inputElement,
formElement,
panelElement,
});
window.addEventListener('mousedown', onMouseDown);

// Click input (focuses it, which opens the panel)
userEvent.click(inputElement);

await runAllMicroTasks();

expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
isOpen: true,
}),
})
);

onStateChange.mockClear();

// Dispatch MouseDown event on window (so, outside of Autocomplete)
const customEvent = new CustomEvent('mousedown', { bubbles: true });
window.document.dispatchEvent(customEvent);

expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
isOpen: false,
activeItemId: null,
}),
})
);

window.removeEventListener('mousedown', onMouseDown);
});

test('does not close panel nor reset `activeItemId` if the target is outside Autocomplete in debug mode', async () => {
const onStateChange = jest.fn();
const {
getEnvironmentProps,
inputElement,
formElement,
} = createPlayground(createAutocomplete, {
onStateChange,
openOnFocus: true,
defaultActiveItemId: 1,
debug: true,
getSources() {
return [
createSource({
getItems: () => [{ label: '1' }],
}),
];
},
});
const panelElement = document.createElement('div');

const { onMouseDown } = getEnvironmentProps({
inputElement,
formElement,
panelElement,
});
window.addEventListener('mousedown', onMouseDown);

// Click input (focuses it, which opens the panel)
userEvent.click(inputElement);

await runAllMicroTasks();

expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
isOpen: true,
}),
})
);

onStateChange.mockClear();

// Dispatch MouseDown event on window (so, outside of Autocomplete)
const customEvent = new CustomEvent('mousedown', { bubbles: true });
window.document.dispatchEvent(customEvent);

expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
isOpen: true,
activeItemId: 1,
}),
})
);

window.removeEventListener('mousedown', onMouseDown);
});
});

describe('onTouchStart', () => {
test('is a noop when panel is not open and status is idle', () => {
const onStateChange = jest.fn();
Expand Down Expand Up @@ -107,7 +305,7 @@ describe('getEnvironmentProps', () => {
window.removeEventListener('touchstart', onTouchStart);
});

test('closes panel if the target is outside Autocomplete', async () => {
test('closes panel and resets `activeItemId` if the target is outside Autocomplete', async () => {
const onStateChange = jest.fn();
const {
getEnvironmentProps,
Expand All @@ -116,6 +314,7 @@ describe('getEnvironmentProps', () => {
} = createPlayground(createAutocomplete, {
onStateChange,
openOnFocus: true,
defaultActiveItemId: 1,
getSources() {
return [
createSource({
Expand Down Expand Up @@ -156,6 +355,66 @@ describe('getEnvironmentProps', () => {
expect.objectContaining({
state: expect.objectContaining({
isOpen: false,
activeItemId: null,
}),
})
);

window.removeEventListener('touchstart', onTouchStart);
});

test('does not close panel nor reset `activeItemId` if the target is outside Autocomplete in debug mode', async () => {
const onStateChange = jest.fn();
const {
getEnvironmentProps,
inputElement,
formElement,
} = createPlayground(createAutocomplete, {
onStateChange,
openOnFocus: true,
defaultActiveItemId: 1,
debug: true,
getSources() {
return [
createSource({
getItems: () => [{ label: '1' }],
}),
];
},
});
const panelElement = document.createElement('div');

const { onTouchStart } = getEnvironmentProps({
inputElement,
formElement,
panelElement,
});
window.addEventListener('touchstart', onTouchStart);

// Focus input (opens the panel)
inputElement.focus();

await runAllMicroTasks();

expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
isOpen: true,
}),
})
);

onStateChange.mockClear();

// Dispatch TouchStart event on window (so, outside of Autocomplete)
const customEvent = new CustomEvent('touchstart', { bubbles: true });
window.document.dispatchEvent(customEvent);

expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
isOpen: true,
activeItemId: 1,
}),
})
);
Expand Down
Loading