Skip to content

Commit b5742ba

Browse files
committed
Fix #2166 fix search bar not collapse after blur
1 parent fa3a3a6 commit b5742ba

3 files changed

Lines changed: 100 additions & 0 deletions

File tree

client/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import { initCommunityVariantFavorites } from './communityVariants';
5858
import { gameSearchView, initGameSearch } from './gameSearch';
5959
import { initProfileActionOverflow } from './profileActionOverflow';
6060
import { initVariantAuthors } from './variantAuthors';
61+
import { initSearchBarDismissal } from './searchBar';
6162

6263
// redirect to correct URL except Heroku preview/dev apps
6364
if (
@@ -302,6 +303,8 @@ function start() {
302303
setTimeout(() => searchInput.focus(), 200);
303304
};
304305

306+
initSearchBarDismissal(searchBar);
307+
305308
function showResults(val: string) {
306309
const acResult = document.getElementById('ac-result') as HTMLElement;
307310
if (val.length < 4) {

client/searchBar.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export function initSearchBarDismissal(searchBar: HTMLElement): () => void {
2+
let focusOutTimer: number | undefined;
3+
4+
const collapse = () => searchBar.classList.remove('active');
5+
6+
const onDocumentClick = (event: MouseEvent) => {
7+
const target = event.target;
8+
if (target instanceof Node && !searchBar.contains(target)) collapse();
9+
};
10+
11+
const onFocusOut = () => {
12+
if (focusOutTimer !== undefined) window.clearTimeout(focusOutTimer);
13+
focusOutTimer = window.setTimeout(() => {
14+
focusOutTimer = undefined;
15+
if (!searchBar.contains(document.activeElement)) collapse();
16+
});
17+
};
18+
19+
document.addEventListener('click', onDocumentClick);
20+
searchBar.addEventListener('focusout', onFocusOut);
21+
22+
return () => {
23+
document.removeEventListener('click', onDocumentClick);
24+
searchBar.removeEventListener('focusout', onFocusOut);
25+
if (focusOutTimer !== undefined) window.clearTimeout(focusOutTimer);
26+
};
27+
}

tests/searchBar.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals';
2+
3+
import { initSearchBarDismissal } from '../client/searchBar';
4+
5+
jest.useFakeTimers();
6+
7+
let cleanup: (() => void) | undefined;
8+
9+
function setupSearchBar() {
10+
document.body.innerHTML = `
11+
<form class="search-bar active">
12+
<button class="search-icon" type="button">Search</button>
13+
<input id="search-input" type="text">
14+
<div id="ac-result"><a href="/@/tester">tester</a></div>
15+
</form>
16+
<button id="outside" type="button">Outside</button>
17+
`;
18+
19+
const searchBar = document.querySelector('.search-bar') as HTMLElement;
20+
const searchInput = document.querySelector('#search-input') as HTMLInputElement;
21+
const resultLink = document.querySelector('#ac-result a') as HTMLAnchorElement;
22+
const outside = document.querySelector('#outside') as HTMLButtonElement;
23+
cleanup = initSearchBarDismissal(searchBar);
24+
25+
return { searchBar, searchInput, resultLink, outside };
26+
}
27+
28+
beforeEach(() => {
29+
document.body.innerHTML = '';
30+
});
31+
32+
afterEach(() => {
33+
cleanup?.();
34+
cleanup = undefined;
35+
jest.clearAllTimers();
36+
document.body.innerHTML = '';
37+
});
38+
39+
describe('header user search dismissal', () => {
40+
test('stays open for clicks inside the search bar and closes for outside clicks', () => {
41+
const { searchBar, resultLink, outside } = setupSearchBar();
42+
43+
resultLink.addEventListener('click', event => event.preventDefault(), { once: true });
44+
resultLink.click();
45+
expect(searchBar.classList.contains('active')).toBe(true);
46+
47+
outside.click();
48+
expect(searchBar.classList.contains('active')).toBe(false);
49+
});
50+
51+
test('stays open when focus moves to an autocomplete result', () => {
52+
const { searchBar, searchInput, resultLink } = setupSearchBar();
53+
54+
searchInput.focus();
55+
resultLink.focus();
56+
jest.runOnlyPendingTimers();
57+
58+
expect(searchBar.classList.contains('active')).toBe(true);
59+
});
60+
61+
test('closes when the search input loses focus outside the search bar', () => {
62+
const { searchBar, searchInput, outside } = setupSearchBar();
63+
64+
searchInput.focus();
65+
outside.focus();
66+
jest.runOnlyPendingTimers();
67+
68+
expect(searchBar.classList.contains('active')).toBe(false);
69+
});
70+
});

0 commit comments

Comments
 (0)