Skip to content

Commit

Permalink
fix(autocomplete-js): leave the modal open on reset on pointer devices (
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahdayan authored Jun 21, 2022
1 parent 0ab2743 commit 3e387e6
Show file tree
Hide file tree
Showing 11 changed files with 938 additions and 143 deletions.
71 changes: 69 additions & 2 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
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

0 comments on commit 3e387e6

Please sign in to comment.