Skip to content

Commit 3e387e6

Browse files
authored
fix(autocomplete-js): leave the modal open on reset on pointer devices (#987)
1 parent 0ab2743 commit 3e387e6

File tree

11 files changed

+938
-143
lines changed

11 files changed

+938
-143
lines changed

packages/autocomplete-core/src/__tests__/concurrency.test.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ describe('concurrency', () => {
134134
expect(getSources).toHaveBeenCalledTimes(3);
135135
});
136136

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

191-
test('keeps the panel closed on touchstart blur', async () => {
191+
test('keeps the panel closed on click outside', async () => {
192+
const onStateChange = jest.fn();
193+
const { timeout, delayedGetSources } = createDelayedGetSources({
194+
sources: [100, 200],
195+
});
196+
const getSources = jest.fn(delayedGetSources);
197+
198+
const {
199+
inputElement,
200+
getEnvironmentProps,
201+
formElement,
202+
} = createPlayground(createAutocomplete, {
203+
onStateChange,
204+
getSources,
205+
});
206+
207+
const panelElement = document.createElement('div');
208+
209+
const { onMouseDown } = getEnvironmentProps({
210+
inputElement,
211+
formElement,
212+
panelElement,
213+
});
214+
window.addEventListener('mousedown', onMouseDown);
215+
216+
userEvent.type(inputElement, 'a');
217+
218+
await runAllMicroTasks();
219+
220+
// The search request is triggered
221+
expect(onStateChange).toHaveBeenLastCalledWith(
222+
expect.objectContaining({
223+
state: expect.objectContaining({
224+
status: 'loading',
225+
query: 'a',
226+
}),
227+
})
228+
);
229+
230+
userEvent.click(document.body);
231+
232+
// The status is immediately set to "idle" and the panel is closed
233+
expect(onStateChange).toHaveBeenLastCalledWith(
234+
expect.objectContaining({
235+
state: expect.objectContaining({
236+
status: 'idle',
237+
isOpen: false,
238+
query: 'a',
239+
}),
240+
})
241+
);
242+
243+
await defer(noop, timeout);
244+
245+
// Once the request is settled, the state remains unchanged
246+
expect(onStateChange).toHaveBeenLastCalledWith(
247+
expect.objectContaining({
248+
state: expect.objectContaining({
249+
status: 'idle',
250+
isOpen: false,
251+
}),
252+
})
253+
);
254+
255+
expect(getSources).toHaveBeenCalledTimes(1);
256+
});
257+
258+
test('keeps the panel closed on touchstart', async () => {
192259
const onStateChange = jest.fn();
193260
const { timeout, delayedGetSources } = createDelayedGetSources({
194261
sources: [100, 200],

packages/autocomplete-core/src/__tests__/getEnvironmentProps.test.ts

Lines changed: 260 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import userEvent from '@testing-library/user-event';
2+
13
import {
24
createPlayground,
35
createSource,
@@ -29,6 +31,202 @@ describe('getEnvironmentProps', () => {
2931
);
3032
});
3133

34+
describe('onMouseDown', () => {
35+
test('is a noop when panel is not open and status is idle', () => {
36+
const onStateChange = jest.fn();
37+
const {
38+
getEnvironmentProps,
39+
inputElement,
40+
formElement,
41+
} = createPlayground(createAutocomplete, { onStateChange });
42+
const panelElement = document.createElement('div');
43+
44+
const { onMouseDown } = getEnvironmentProps({
45+
inputElement,
46+
formElement,
47+
panelElement,
48+
});
49+
window.addEventListener('mousedown', onMouseDown);
50+
51+
// Dispatch MouseDown event on window
52+
const customEvent = new CustomEvent('mousedown', { bubbles: true });
53+
window.dispatchEvent(customEvent);
54+
55+
expect(onStateChange).not.toHaveBeenCalled();
56+
57+
window.removeEventListener('mousedown', onMouseDown);
58+
});
59+
60+
test('is a noop when the event target is the input element', async () => {
61+
const onStateChange = jest.fn();
62+
const {
63+
getEnvironmentProps,
64+
inputElement,
65+
formElement,
66+
} = createPlayground(createAutocomplete, {
67+
onStateChange,
68+
openOnFocus: true,
69+
getSources() {
70+
return [
71+
createSource({
72+
getItems: () => [{ label: '1' }],
73+
}),
74+
];
75+
},
76+
});
77+
const panelElement = document.createElement('div');
78+
79+
const { onMouseDown } = getEnvironmentProps({
80+
inputElement,
81+
formElement,
82+
panelElement,
83+
});
84+
window.addEventListener('mousedown', onMouseDown);
85+
86+
// Click input (focuses it, which opens the panel)
87+
userEvent.click(inputElement);
88+
89+
await runAllMicroTasks();
90+
91+
expect(onStateChange).toHaveBeenLastCalledWith(
92+
expect.objectContaining({
93+
state: expect.objectContaining({
94+
isOpen: true,
95+
}),
96+
})
97+
);
98+
99+
onStateChange.mockClear();
100+
101+
// Dispatch MouseDown event on the input (bubbles to window)
102+
const customEvent = new CustomEvent('mousedown', { bubbles: true });
103+
inputElement.dispatchEvent(customEvent);
104+
105+
await runAllMicroTasks();
106+
107+
expect(onStateChange).not.toHaveBeenCalled();
108+
109+
window.removeEventListener('mousedown', onMouseDown);
110+
});
111+
112+
test('closes panel and resets `activeItemId` if the target is outside Autocomplete', async () => {
113+
const onStateChange = jest.fn();
114+
const {
115+
getEnvironmentProps,
116+
inputElement,
117+
formElement,
118+
} = createPlayground(createAutocomplete, {
119+
onStateChange,
120+
openOnFocus: true,
121+
defaultActiveItemId: 1,
122+
getSources() {
123+
return [
124+
createSource({
125+
getItems: () => [{ label: '1' }],
126+
}),
127+
];
128+
},
129+
});
130+
const panelElement = document.createElement('div');
131+
132+
const { onMouseDown } = getEnvironmentProps({
133+
inputElement,
134+
formElement,
135+
panelElement,
136+
});
137+
window.addEventListener('mousedown', onMouseDown);
138+
139+
// Click input (focuses it, which opens the panel)
140+
userEvent.click(inputElement);
141+
142+
await runAllMicroTasks();
143+
144+
expect(onStateChange).toHaveBeenLastCalledWith(
145+
expect.objectContaining({
146+
state: expect.objectContaining({
147+
isOpen: true,
148+
}),
149+
})
150+
);
151+
152+
onStateChange.mockClear();
153+
154+
// Dispatch MouseDown event on window (so, outside of Autocomplete)
155+
const customEvent = new CustomEvent('mousedown', { bubbles: true });
156+
window.document.dispatchEvent(customEvent);
157+
158+
expect(onStateChange).toHaveBeenLastCalledWith(
159+
expect.objectContaining({
160+
state: expect.objectContaining({
161+
isOpen: false,
162+
activeItemId: null,
163+
}),
164+
})
165+
);
166+
167+
window.removeEventListener('mousedown', onMouseDown);
168+
});
169+
170+
test('does not close panel nor reset `activeItemId` if the target is outside Autocomplete in debug mode', async () => {
171+
const onStateChange = jest.fn();
172+
const {
173+
getEnvironmentProps,
174+
inputElement,
175+
formElement,
176+
} = createPlayground(createAutocomplete, {
177+
onStateChange,
178+
openOnFocus: true,
179+
defaultActiveItemId: 1,
180+
debug: true,
181+
getSources() {
182+
return [
183+
createSource({
184+
getItems: () => [{ label: '1' }],
185+
}),
186+
];
187+
},
188+
});
189+
const panelElement = document.createElement('div');
190+
191+
const { onMouseDown } = getEnvironmentProps({
192+
inputElement,
193+
formElement,
194+
panelElement,
195+
});
196+
window.addEventListener('mousedown', onMouseDown);
197+
198+
// Click input (focuses it, which opens the panel)
199+
userEvent.click(inputElement);
200+
201+
await runAllMicroTasks();
202+
203+
expect(onStateChange).toHaveBeenLastCalledWith(
204+
expect.objectContaining({
205+
state: expect.objectContaining({
206+
isOpen: true,
207+
}),
208+
})
209+
);
210+
211+
onStateChange.mockClear();
212+
213+
// Dispatch MouseDown event on window (so, outside of Autocomplete)
214+
const customEvent = new CustomEvent('mousedown', { bubbles: true });
215+
window.document.dispatchEvent(customEvent);
216+
217+
expect(onStateChange).toHaveBeenLastCalledWith(
218+
expect.objectContaining({
219+
state: expect.objectContaining({
220+
isOpen: true,
221+
activeItemId: 1,
222+
}),
223+
})
224+
);
225+
226+
window.removeEventListener('mousedown', onMouseDown);
227+
});
228+
});
229+
32230
describe('onTouchStart', () => {
33231
test('is a noop when panel is not open and status is idle', () => {
34232
const onStateChange = jest.fn();
@@ -107,7 +305,7 @@ describe('getEnvironmentProps', () => {
107305
window.removeEventListener('touchstart', onTouchStart);
108306
});
109307

110-
test('closes panel if the target is outside Autocomplete', async () => {
308+
test('closes panel and resets `activeItemId` if the target is outside Autocomplete', async () => {
111309
const onStateChange = jest.fn();
112310
const {
113311
getEnvironmentProps,
@@ -116,6 +314,7 @@ describe('getEnvironmentProps', () => {
116314
} = createPlayground(createAutocomplete, {
117315
onStateChange,
118316
openOnFocus: true,
317+
defaultActiveItemId: 1,
119318
getSources() {
120319
return [
121320
createSource({
@@ -156,6 +355,66 @@ describe('getEnvironmentProps', () => {
156355
expect.objectContaining({
157356
state: expect.objectContaining({
158357
isOpen: false,
358+
activeItemId: null,
359+
}),
360+
})
361+
);
362+
363+
window.removeEventListener('touchstart', onTouchStart);
364+
});
365+
366+
test('does not close panel nor reset `activeItemId` if the target is outside Autocomplete in debug mode', async () => {
367+
const onStateChange = jest.fn();
368+
const {
369+
getEnvironmentProps,
370+
inputElement,
371+
formElement,
372+
} = createPlayground(createAutocomplete, {
373+
onStateChange,
374+
openOnFocus: true,
375+
defaultActiveItemId: 1,
376+
debug: true,
377+
getSources() {
378+
return [
379+
createSource({
380+
getItems: () => [{ label: '1' }],
381+
}),
382+
];
383+
},
384+
});
385+
const panelElement = document.createElement('div');
386+
387+
const { onTouchStart } = getEnvironmentProps({
388+
inputElement,
389+
formElement,
390+
panelElement,
391+
});
392+
window.addEventListener('touchstart', onTouchStart);
393+
394+
// Focus input (opens the panel)
395+
inputElement.focus();
396+
397+
await runAllMicroTasks();
398+
399+
expect(onStateChange).toHaveBeenLastCalledWith(
400+
expect.objectContaining({
401+
state: expect.objectContaining({
402+
isOpen: true,
403+
}),
404+
})
405+
);
406+
407+
onStateChange.mockClear();
408+
409+
// Dispatch TouchStart event on window (so, outside of Autocomplete)
410+
const customEvent = new CustomEvent('touchstart', { bubbles: true });
411+
window.document.dispatchEvent(customEvent);
412+
413+
expect(onStateChange).toHaveBeenLastCalledWith(
414+
expect.objectContaining({
415+
state: expect.objectContaining({
416+
isOpen: true,
417+
activeItemId: 1,
159418
}),
160419
})
161420
);

0 commit comments

Comments
 (0)