diff --git a/app/javascript/Search/Search.jsx b/app/javascript/Search/Search.jsx
index ba2e77ffd..64a82d5fe 100644
--- a/app/javascript/Search/Search.jsx
+++ b/app/javascript/Search/Search.jsx
@@ -21,6 +21,7 @@ export class Search extends Component {
constructor(props) {
super(props);
this.enableSearchPageChecker = true;
+ this.syncSearchUrlWithInput = this.syncSearchUrlWithInput.bind(this);
}
componentWillMount() {
@@ -48,9 +49,33 @@ export class Search extends Component {
searchPageChecker();
}
+ /**
+ * Synchronizes the search input value with the search term defined in the URL.
+ */
+ syncSearchUrlWithInput() {
+ // TODO: Consolidate search functionality.
+ // Note that push states for search occur in _search.html.erb
+ // in initializeSortingTabs(query)
+ const { searchBoxId } = this.props;
+ const searchTerm = getInitialSearchTerm(window.location.search);
+
+ // We set the value outside of React state so that there is no flickering of placeholder
+ // to search term.
+ const searchBox = document.getElementById(searchBoxId);
+ searchBox.value = searchTerm;
+
+ // Even though we set the search term directly via the DOM, it still needs to reside
+ // in component state.
+ this.setState({
+ searchTerm,
+ });
+ }
+
componentDidMount() {
this.registerGlobalKeysListener();
InstantClick.on('change', this.enableSearchPageListener);
+
+ window.addEventListener('popstate', this.syncSearchUrlWithInput);
}
enableSearchPageListener = () => {
@@ -81,8 +106,9 @@ export class Search extends Component {
}
}
- componentDidUnmount() {
+ componentWillUnmount() {
document.removeEventListener('keydown', this.globalKeysListener);
+ window.removeEventListener('popstate', this.syncSearchUrlWithInput);
InstantClick.off('change', this.enableSearchPageListener);
}
diff --git a/app/javascript/Search/__tests__/Search.test.jsx b/app/javascript/Search/__tests__/Search.test.jsx
index 3edf87eaa..09518194d 100644
--- a/app/javascript/Search/__tests__/Search.test.jsx
+++ b/app/javascript/Search/__tests__/Search.test.jsx
@@ -9,6 +9,7 @@ describe('', () => {
global.filterXSS = jest.fn();
global.InstantClick = jest.fn(() => ({
on: jest.fn(),
+ off: jest.fn(),
preload: jest.fn(),
display: jest.fn(),
}))();
@@ -78,4 +79,34 @@ describe('', () => {
expect(searchInput.value).toEqual('hello');
expect(Search.prototype.search).toHaveBeenCalledWith('Enter', 'hello');
});
+
+ it('should be listening for history state changes', async () => {
+ // This is an implementation detail, but I want to make sure that this
+ // listener is registered as it affects the UI.
+ jest.spyOn(window, 'addEventListener');
+
+ render();
+
+ expect(window.addEventListener).toHaveBeenCalledTimes(1);
+ expect(window.addEventListener).toHaveBeenCalledWith(
+ 'popstate',
+ expect.any(Function),
+ );
+ });
+
+ it('should stop listening for history state changes when the component is destroyed', async () => {
+ // This is an implementation detail, but I want to make sure that this
+ // listener is unregistered as it affects the UI.
+ jest.spyOn(window, 'removeEventListener');
+
+ const { unmount } = render();
+
+ unmount();
+
+ expect(window.removeEventListener).toHaveBeenCalledTimes(1);
+ expect(window.removeEventListener).toHaveBeenCalledWith(
+ 'popstate',
+ expect.any(Function),
+ );
+ });
});
diff --git a/app/javascript/utilities/__tests__/search.test.js b/app/javascript/utilities/__tests__/search.test.js
index 827477098..b14f274c8 100644
--- a/app/javascript/utilities/__tests__/search.test.js
+++ b/app/javascript/utilities/__tests__/search.test.js
@@ -58,6 +58,15 @@ describe('Search utilities', () => {
const actual = getInitialSearchTerm(querystring);
expect(actual).toEqual(expected);
});
+
+ it(`should return an empty string when the search term is not defined`, () => {
+ /* eslint-disable-next-line no-global-assign */
+ filterXSS = jest.fn(() => undefined);
+ const querystring = `?q=`;
+
+ const actual = getInitialSearchTerm(querystring);
+ expect(actual).toEqual('');
+ });
});
describe(`When the querystring key 'q' does not exist`, () => {
diff --git a/app/javascript/utilities/search/index.js b/app/javascript/utilities/search/index.js
index c16ba3ec7..ff873141e 100644
--- a/app/javascript/utilities/search/index.js
+++ b/app/javascript/utilities/search/index.js
@@ -70,7 +70,7 @@ export function getInitialSearchTerm(querystring) {
matches !== null && matches.length === 2
? decodeURIComponent(matches[1].replace(/\+/g, '%20'))
: '';
- const query = filterXSS(rawSearchTerm);
+ const query = filterXSS(rawSearchTerm) || '';
const divForDecode = document.createElement('div');
divForDecode.innerHTML = query;
@@ -86,11 +86,10 @@ export function preloadSearchResults({
const encodedQuery = fixedEncodeURIComponent(
searchTerm.replace(/^[ ]+|[ ]+$/g, ''),
);
- InstantClick.preload(
- `${location.origin}/search?q=${encodedQuery}${getFilterParameters(
- location.href,
- )}`,
- );
+ const searchUrl = `${
+ location.origin
+ }/search?q=${encodedQuery}${getFilterParameters(location.href)}`;
+ InstantClick.preload(searchUrl);
}
export function createSearchUrl(dataHash) {