@salesforcedevs/dx-components 0.10.1-alpha.13 → 0.10.1-alpha.130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/lwc.config.json +3 -1
  2. package/package.json +2 -2
  3. package/src/assets/svg/docs-empty-state-small.svg +72 -0
  4. package/src/assets/svg/sidebar-loading.svg +34 -0
  5. package/src/modules/dx/cardGrid/__tests__/cardMockProps.ts +78 -148
  6. package/src/modules/dx/codeBlock/codeBlock.stories.ts +2 -69
  7. package/src/modules/dx/codeBlock/codeBlock.ts +5 -1
  8. package/src/modules/dx/codeBlock/darkTheme.css +0 -1
  9. package/src/modules/dx/codeBlock/lightTheme.css +0 -1
  10. package/src/modules/dx/emptyState/__tests__/emptyState.test.ts +29 -9
  11. package/src/modules/dx/emptyState/emptyState.css +14 -0
  12. package/src/modules/dx/emptyState/emptyState.html +2 -2
  13. package/src/modules/dx/emptyState/emptyState.stories.ts +8 -0
  14. package/src/modules/dx/emptyState/emptyState.ts +14 -0
  15. package/src/modules/dx/footer/links.ts +3 -5
  16. package/src/modules/dx/headerNav/__tests__/headerNav.test.ts +2 -4
  17. package/src/modules/dx/headerNav/headerNav.html +8 -6
  18. package/src/modules/dx/headerNav/headerNav.ts +2 -2
  19. package/src/modules/dx/lazy/lazy.ts +0 -7
  20. package/src/modules/dx/newsletterForm/newsletterForm.html +5 -3
  21. package/src/modules/dx/searchResults/coveo.css +314 -111
  22. package/src/modules/dx/sidebar/__tests__/mockDocContent.ts +48 -0
  23. package/src/modules/dx/sidebar/__tests__/mockPageReference.ts +8 -0
  24. package/src/modules/dx/sidebar/sidebar.css +38 -17
  25. package/src/modules/dx/sidebar/sidebar.html +62 -58
  26. package/src/modules/dx/sidebar/sidebar.stories.ts +53 -54
  27. package/src/modules/dx/sidebar/sidebar.ts +99 -10
  28. package/src/modules/dx/sidebarSearch/__tests__/mockPreloadedState.ts +8843 -0
  29. package/src/modules/dx/sidebarSearch/__tests__/sidebarSearch.test.ts +121 -0
  30. package/src/modules/dx/sidebarSearch/sidebarSearch.ts +281 -120
  31. package/src/modules/dx/sidebarSearch/utils.ts +2 -0
  32. package/src/modules/dx/sidebarSearchResult/__tests__/sidebarSearchResult.test.ts +55 -0
  33. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +12 -2
  34. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.html +1 -1
  35. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.ts +8 -2
  36. package/src/modules/dx/spinner/__tests__/spinner.test.ts +37 -0
  37. package/src/modules/dx/spinner/spinner.css +750 -0
  38. package/src/modules/dx/spinner/spinner.html +6 -0
  39. package/src/modules/dx/spinner/spinner.stories.ts +69 -0
  40. package/src/modules/dx/spinner/spinner.ts +20 -0
  41. package/src/modules/dx/{headerNavOption/__tests__/headerNavOption.test.ts → tab/__tests__/tab.test.ts} +40 -24
  42. package/src/modules/dx/{headerNavOption/headerNavOption.css → tab/tab.css} +24 -9
  43. package/src/modules/dx/{headerNavOption/headerNavOption.html → tab/tab.html} +5 -1
  44. package/src/modules/dx/tab/tab.stories.ts +57 -0
  45. package/src/modules/dx/{headerNavOption/headerNavOption.ts → tab/tab.ts} +26 -5
  46. package/src/modules/helpers/table/table.css +23 -7
  47. package/src/modules/utils/coveo/coveo.ts +50 -12
  48. package/src/modules/utils/highlight/__tests__/highlight.test.ts +154 -0
  49. package/src/modules/utils/highlight/highlight.ts +32 -0
  50. package/src/modules/utils/jest/jest.ts +35 -0
  51. package/src/modules/utils/normalizers/normalizers.ts +3 -0
  52. package/src/modules/utils/recentSearches/__tests__/recentSearches.test.ts +79 -0
  53. package/src/modules/utils/recentSearches/recentSearches.ts +12 -17
  54. package/src/modules/utils/regexps/regexps.ts +5 -0
  55. package/src/modules/utils/tests/tests.ts +18 -11
@@ -0,0 +1,121 @@
1
+ import { createElement } from "lwc";
2
+ import SidebarSearch from "dx/sidebarSearch";
3
+ import { createRenderComponent, renderLightningElement } from "utils/tests";
4
+ import { coveoXMLDocsConfig } from "utils/coveo";
5
+ import { mockQueryParameter, unmockQueryParameter } from "utils/jest";
6
+ import mockPreloadedState from "./mockPreloadedState";
7
+ import { SESSION_KEY } from "../sidebarSearch";
8
+
9
+ const TAG = "dx-sidebar-search";
10
+ const render = createRenderComponent(TAG, SidebarSearch);
11
+
12
+ const PROPS = coveoXMLDocsConfig;
13
+ const QUERY = "test";
14
+
15
+ describe(TAG, () => {
16
+ afterEach(unmockQueryParameter);
17
+
18
+ it("renders with the correct initial empty query", () => {
19
+ const element = render(PROPS);
20
+
21
+ const input = element.shadowRoot.querySelector("dx-input");
22
+
23
+ expect(input.value).toEqual("");
24
+
25
+ return expect(element).toBeAccessible();
26
+ });
27
+
28
+ it("renders with the correct initial query", () => {
29
+ mockQueryParameter(QUERY);
30
+
31
+ const element = render(PROPS);
32
+
33
+ const input = element.shadowRoot.querySelector("dx-input");
34
+
35
+ expect(input.value).toEqual(QUERY);
36
+
37
+ return expect(element).toBeAccessible();
38
+ });
39
+
40
+ it("correctly consumes a valid session storage object", () => {
41
+ mockQueryParameter(mockPreloadedState.query.q);
42
+
43
+ window.sessionStorage.setItem(
44
+ SESSION_KEY,
45
+ JSON.stringify(mockPreloadedState)
46
+ );
47
+
48
+ const mockOnChange = jest.fn();
49
+
50
+ const element = render(PROPS);
51
+ element.addEventListener("change", mockOnChange);
52
+
53
+ return Promise.resolve().then(() => {
54
+ expect(mockOnChange).toHaveBeenCalledTimes(1);
55
+
56
+ expect(element).toBeAccessible();
57
+ });
58
+ });
59
+
60
+ describe("highlighting", () => {
61
+ afterEach(unmockQueryParameter);
62
+
63
+ it("triggers on initial render when a search value exists", () => {
64
+ mockQueryParameter(QUERY);
65
+ const element = createElement(TAG, { is: SidebarSearch });
66
+ const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
67
+ renderLightningElement(element, PROPS);
68
+ expect(dispatchEventSpy).toHaveBeenCalledWith(
69
+ expect.objectContaining({
70
+ type: "highlightedtermchange",
71
+ detail: QUERY
72
+ })
73
+ );
74
+ });
75
+
76
+ it("does not trigger on initial render when a search value does not exist", () => {
77
+ const element = createElement(TAG, { is: SidebarSearch });
78
+ const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
79
+ renderLightningElement(element, PROPS);
80
+ expect(dispatchEventSpy).not.toHaveBeenCalledWith(
81
+ expect.objectContaining({ type: "highlightedtermchange" })
82
+ );
83
+ });
84
+
85
+ it("triggers on input change with non-empty value", () => {
86
+ const element = render(PROPS);
87
+ const dxInput = element.shadowRoot.querySelector("dx-input");
88
+ const inputEl = dxInput.shadowRoot.querySelector("input");
89
+ const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
90
+
91
+ inputEl.focus();
92
+ inputEl.value = QUERY;
93
+ inputEl.dispatchEvent(new KeyboardEvent("input"));
94
+
95
+ expect(dispatchEventSpy).toHaveBeenCalledWith(
96
+ expect.objectContaining({
97
+ type: "highlightedtermchange",
98
+ detail: QUERY
99
+ })
100
+ );
101
+ });
102
+
103
+ it("triggers on input change with empty value", () => {
104
+ const element = render(PROPS);
105
+ const dxInput = element.shadowRoot.querySelector("dx-input");
106
+ const inputEl = dxInput.shadowRoot.querySelector("input");
107
+ const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
108
+
109
+ inputEl.focus();
110
+ inputEl.value = "";
111
+ inputEl.dispatchEvent(new KeyboardEvent("input"));
112
+
113
+ expect(dispatchEventSpy).toHaveBeenCalledWith(
114
+ expect.objectContaining({
115
+ type: "highlightedtermchange",
116
+ detail: ""
117
+ })
118
+ );
119
+ });
120
+ });
121
+ });
@@ -1,207 +1,363 @@
1
1
  import { LightningElement, api } from "lwc";
2
2
  import debounce from "debounce";
3
- import { buildSearchEngine } from "utils/coveo";
4
- import { RecentSearches } from "utils/recentSearches";
5
3
  import {
6
4
  Result,
7
5
  ResultList,
6
+ ResultsPerPage,
8
7
  SearchBox,
9
- SearchBoxOptions,
10
8
  buildResultList,
9
+ buildResultsPerPage,
11
10
  buildSearchBox,
12
11
  loadAdvancedSearchQueryActions,
13
- SearchEngine
12
+ SearchEngine,
13
+ SearchAppState,
14
+ loadQueryActions
14
15
  } from "@coveo/headless";
16
+ import { buildSearchEngine, getSidebarSearchParams } from "utils/coveo";
17
+ import { RecentSearches } from "utils/recentSearches";
18
+ import { toJson } from "utils/normalizers";
15
19
  import { Option, PopoverRequestCloseType } from "typings/custom";
16
20
 
21
+ export const SESSION_KEY = "dx-sidebar-search-state";
22
+
23
+ const DEFAULT_RESULT_COUNT = 20;
24
+ const SEARCH_DEBOUNCE_DELAY = 780;
25
+
17
26
  const UserRecentSearches = new RecentSearches();
18
27
 
19
- const toSearchVersionString = (version: string): string =>
28
+ const normalizeCoveoAdvancedQueryValue = (version: string): string =>
20
29
  version.split(".").join("\\.");
21
30
 
22
- const normalizeCoveoResult = ({
23
- title,
24
- titleHighlights,
25
- excerpt,
26
- excerptHighlights,
27
- uniqueId,
28
- raw: { sfhtml_url__c }
29
- }: Result) => ({
30
- title,
31
- titleHighlights,
32
- excerpt,
33
- excerptHighlights,
34
- uniqueId,
35
- href: `/docs/${sfhtml_url__c}`
36
- });
37
-
38
31
  export default class SidebarSearch extends LightningElement {
39
32
  @api coveoOrganizationId!: string;
40
33
  @api coveoPublicAccessToken!: string;
41
34
  @api coveoSearchHub!: string;
42
- @api coveoObjectType!: string;
43
- @api coveoLanguage!: string;
44
- @api coveoRelease!: string;
45
- @api coveoDeliverable!: string;
35
+ @api
36
+ get coveoAdvancedQueryConfig(): { [key: string]: any } {
37
+ return this._coveoAdvancedQueryConfig;
38
+ }
39
+ set coveoAdvancedQueryConfig(value) {
40
+ this._coveoAdvancedQueryConfig = toJson(value);
41
+ }
46
42
 
43
+ @api
44
+ public fetchMoreResults() {
45
+ if (this.resultList?.state?.moreResultsAvailable) {
46
+ this.fetchingMoreResults = true;
47
+ this.resultList.fetchMoreResults();
48
+ }
49
+ }
50
+
51
+ @api
52
+ setInputValue(searchTerm: string) {
53
+ this.value = searchTerm || "";
54
+ }
55
+
56
+ private _coveoAdvancedQueryConfig!: object;
47
57
  private dropdownRequestedOpen: boolean = false;
48
58
  private recentSearches: Option[] = [];
49
59
  private resultList: ResultList | null = null;
60
+ private resultsPerPage: ResultsPerPage | null = null;
61
+ private preloadedState?: SearchAppState;
50
62
  private searchBox: SearchBox | null = null;
51
- private value?: string = undefined;
63
+ private value?: string = "";
52
64
  private engine: SearchEngine | null = null;
65
+ private fetchingMoreResults: boolean = false;
66
+ private didRender = false;
67
+
68
+ private get hasCorrectCoveoConfig(): boolean {
69
+ const coveoConfigurations = [
70
+ this.coveoOrganizationId,
71
+ this.coveoPublicAccessToken,
72
+ this.coveoSearchHub,
73
+ this.coveoAdvancedQueryConfig
74
+ ];
75
+
76
+ return coveoConfigurations.every(
77
+ (val) =>
78
+ !!val && (typeof val === "string" || typeof val === "object")
79
+ );
80
+ }
53
81
 
54
82
  private get isDropdownOpen() {
55
83
  return (
56
84
  this.dropdownRequestedOpen &&
57
85
  this.recentSearches &&
58
86
  this.recentSearches.length > 0 &&
59
- this.isValueEmpty
87
+ !this.value
60
88
  );
61
89
  }
62
90
 
63
- private get isValueEmpty(): boolean {
64
- return !this.value || this.value === "";
65
- }
91
+ constructor() {
92
+ super();
66
93
 
67
- connectedCallback() {
68
- this.recentSearches = UserRecentSearches.get();
94
+ this.updateRecentSearches();
95
+ this.value = getSidebarSearchParams() || "";
69
96
  }
70
97
 
71
- renderedCallback() {
72
- const coveoConfigurations = [
73
- this.coveoOrganizationId,
74
- this.coveoPublicAccessToken,
75
- this.coveoSearchHub,
76
- this.coveoObjectType,
77
- this.coveoLanguage,
78
- this.coveoRelease,
79
- this.coveoDeliverable
80
- ];
98
+ queryConfigValue: string | null = null;
81
99
 
82
- if (
83
- coveoConfigurations.every((val) => !!val && typeof val === "string")
84
- ) {
85
- if (!this.engine) {
86
- this.instantiateSearchbox();
87
- }
88
- this.setAdvancedFilters();
89
- } else {
100
+ renderedCallback() {
101
+ // in prod some of the critical coveo configuration attributes seem to arrive later
102
+ // so I needed to put this coveo startup flow here where we can wait for them to arrive
103
+ if (this.hasCorrectCoveoConfig && !this.engine) {
104
+ console.log("coveo config is good");
105
+ this.initializeUserSession();
106
+ this.initializeCoveo();
107
+ } else if (!this.hasCorrectCoveoConfig) {
90
108
  console.error(
91
- "Incorrect coveo configuration provided. All coveo configuration attributes must be defined and a string.",
109
+ "Incorrect coveo search configuration provided. All coveo configuration attributes must be defined.",
92
110
  {
93
111
  coveoOrganizationId: this.coveoOrganizationId,
94
112
  coveoPublicAccessToken: this.coveoPublicAccessToken,
95
113
  coveoSearchHub: this.coveoSearchHub,
96
- coveoObjectType: this.coveoObjectType,
97
- coveoLanguage: this.coveoLanguage,
98
- coveoRelease: this.coveoRelease,
99
- coveoDeliverable: this.coveoDeliverable
114
+ coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig
100
115
  }
101
116
  );
102
117
  }
118
+
119
+ const queryConfigValue = JSON.stringify(this.coveoAdvancedQueryConfig);
120
+ console.log('query config value', queryConfigValue);
121
+ if (this.queryConfigValue !== queryConfigValue) {
122
+ if (this.queryConfigValue && this.engine) {
123
+ console.log('setting query config again')
124
+ this.setAdvancedFilters(this.engine);
125
+ }
126
+ this.queryConfigValue = queryConfigValue;
127
+ }
128
+
129
+ // If this is the first render and there is a search value, trigger
130
+ // term highlighting
131
+ if (!this.didRender && this.value) {
132
+ this.dispatchHighlightedTermChange();
133
+ }
134
+
135
+ this.didRender = true;
103
136
  }
104
137
 
105
- private instantiateSearchbox() {
106
- const options: SearchBoxOptions = {
107
- numberOfSuggestions: 3
108
- };
138
+ private initializeUserSession() {
139
+ const stringified = window.sessionStorage.getItem(SESSION_KEY);
140
+ if (!stringified) {
141
+ return;
142
+ }
109
143
 
110
- this.engine = buildSearchEngine({
111
- organizationId: this.coveoOrganizationId,
112
- publicAccessToken: this.coveoPublicAccessToken,
113
- searchHub: this.coveoSearchHub
114
- });
144
+ try {
145
+ const json = JSON.parse(stringified);
146
+ const hasSameConfig = Object.entries(
147
+ json.coveoAdvancedQueryConfig
148
+ ).every(
149
+ ([key, value]: [string, any]) =>
150
+ this.coveoAdvancedQueryConfig[key] === value
151
+ );
152
+ const hasSameQuery = json?.state?.query?.q === this.value;
153
+ if (hasSameConfig && hasSameQuery) {
154
+ console.log(
155
+ "valid session storage found for value =",
156
+ this.value
157
+ );
158
+ this.preloadedState = json?.state;
159
+ } else {
160
+ window.sessionStorage.removeItem(SESSION_KEY);
161
+ }
162
+ } catch (e) {
163
+ console.error(e);
164
+ window.sessionStorage.removeItem(SESSION_KEY);
165
+ }
166
+ }
115
167
 
116
- if (this.engine) {
117
- this.resultList = buildResultList(this.engine, {
118
- options: { fieldsToInclude: ["sfhtml_url__c"] }
168
+ private initializeCoveo = () => {
169
+ try {
170
+ this.engine = buildSearchEngine({
171
+ organizationId: this.coveoOrganizationId,
172
+ publicAccessToken: this.coveoPublicAccessToken,
173
+ searchHub: this.coveoSearchHub,
174
+ preloadedState: this.preloadedState
119
175
  });
120
- this.searchBox = buildSearchBox(this.engine, { options });
176
+ } catch (ex) {
177
+ console.error(`Coveo engine failed to initialize (${ex})`);
121
178
  }
122
179
 
123
- this.resultList?.subscribe(() => {
124
- if (!this.resultList) {
125
- return;
126
- }
180
+ if (!this.engine) {
181
+ console.error("Coveo engine failed to initialize!");
182
+ return;
183
+ }
127
184
 
128
- const { isLoading, results } = this.resultList.state;
185
+ if (this.preloadedState) {
186
+ const actions = loadQueryActions(this.engine);
187
+ this.engine.dispatch(actions.updateQuery({ q: this.value || "" }));
188
+ }
189
+
190
+ this.resultList = buildResultList(this.engine, {
191
+ options: { fieldsToInclude: ["sfhtml_url__c"] }
192
+ });
129
193
 
130
- if (!isLoading) {
131
- this.dispatchOnChangeEvent(results);
194
+ this.resultsPerPage = buildResultsPerPage(this.engine, {
195
+ initialState: {
196
+ numberOfResults: DEFAULT_RESULT_COUNT
132
197
  }
198
+ });
133
199
 
134
- this.dispatchOnLoading(isLoading);
200
+ this.searchBox = buildSearchBox(this.engine, {
201
+ options: { numberOfSuggestions: 3 }
135
202
  });
136
- }
137
203
 
138
- private setAdvancedFilters() {
139
- if (!this.engine) {
204
+ this.setAdvancedFilters(this.engine);
205
+
206
+ this.resultList?.subscribe(this.onResultListChange);
207
+
208
+ if (this.value) {
209
+ this.dispatchHighlightedTermChange();
210
+ }
211
+
212
+ if (!this.preloadedState) {
213
+ if (this.value) {
214
+ this.submitSearch();
215
+ } else {
216
+ this.dispatchOnLoading(false);
217
+ }
218
+ }
219
+ };
220
+
221
+ private onResultListChange = () => {
222
+ if (!this.resultList) {
140
223
  return;
141
224
  }
142
225
 
143
- const aqParts = {
144
- objecttype: this.coveoObjectType,
145
- sflanguage: this.coveoLanguage,
146
- sfrelease__c: toSearchVersionString(this.coveoRelease),
147
- sfdelivarable__c: this.coveoDeliverable
148
- };
226
+ const { isLoading, firstSearchExecuted, results } =
227
+ this.resultList.state;
228
+
229
+ if ((!firstSearchExecuted && !isLoading) || !this.value) {
230
+ // coveo search is firing off some unwanted payloads on startup
231
+ console.log(
232
+ "returning in results list change",
233
+ this.value,
234
+ !firstSearchExecuted && !isLoading
235
+ );
236
+ return;
237
+ }
238
+
239
+ if (!isLoading) {
240
+ console.log("dispatching on change event");
241
+ this.dispatchOnChangeEvent(results);
242
+ }
243
+
244
+ if (!this.fetchingMoreResults) {
245
+ this.dispatchOnLoading(isLoading);
246
+ }
149
247
 
150
- const aq = Object.entries(aqParts).reduce(
151
- (result, [key, value]) => `${result}(@${key}==${value})`,
248
+ if (!isLoading && this.fetchingMoreResults) {
249
+ this.fetchingMoreResults = false;
250
+ }
251
+ };
252
+
253
+ private setAdvancedFilters(engine: SearchEngine) {
254
+ const aq = Object.entries(this.coveoAdvancedQueryConfig).reduce(
255
+ (result, [key, value]) =>
256
+ `${result}(@${key}=="${normalizeCoveoAdvancedQueryValue(
257
+ value
258
+ )}")`,
152
259
  ""
153
260
  );
154
- const advancedSearchQueryActions = loadAdvancedSearchQueryActions(
155
- this.engine
156
- );
157
261
 
158
- this.engine.dispatch(
159
- advancedSearchQueryActions.registerAdvancedSearchQueries({
160
- aq: aq
262
+ const registerSearchQueryAction = loadAdvancedSearchQueryActions(
263
+ engine
264
+ ).registerAdvancedSearchQueries({
265
+ aq: aq
266
+ });
267
+
268
+ engine.dispatch(registerSearchQueryAction);
269
+ }
270
+
271
+ private normalizeCoveoResult = ({
272
+ clickUri,
273
+ excerpt,
274
+ excerptHighlights,
275
+ raw: { sfhtml_url__c },
276
+ title,
277
+ titleHighlights,
278
+ uniqueId
279
+ }: Result) => {
280
+ const pathname = sfhtml_url__c
281
+ ? `/docs/${sfhtml_url__c}`
282
+ : `${new URL(clickUri).pathname}.html`;
283
+ const href = `${pathname}?q=${this.value}`;
284
+
285
+ return {
286
+ title,
287
+ titleHighlights,
288
+ excerpt,
289
+ excerptHighlights,
290
+ uniqueId,
291
+ href,
292
+ selected: pathname === window.location.pathname
293
+ };
294
+ };
295
+
296
+ private updateSessionStorage() {
297
+ console.log("updating session storage");
298
+ window.sessionStorage.setItem(
299
+ SESSION_KEY,
300
+ JSON.stringify({
301
+ coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig,
302
+ state: this.engine?.state
161
303
  })
162
304
  );
163
305
  }
164
306
 
165
307
  private dispatchOnChangeEvent(results: Result[]) {
308
+ this.updateSessionStorage();
166
309
  this.dispatchEvent(
167
310
  new CustomEvent("change", {
168
311
  detail: {
169
- results: results.map(normalizeCoveoResult),
312
+ results: results.map(this.normalizeCoveoResult),
170
313
  value: this.value
171
314
  }
172
315
  })
173
316
  );
174
317
  }
175
318
 
176
- private dispatchOnLoading(isLoading: boolean) {
319
+ private dispatchOnLoading(loading: boolean) {
320
+ console.log("dispatching onloading", loading);
321
+ this.dispatchEvent(new CustomEvent("loading", { detail: loading }));
322
+ }
323
+
324
+ private dispatchHighlightedTermChange() {
177
325
  this.dispatchEvent(
178
- new CustomEvent("loading", {
179
- detail: isLoading
326
+ new CustomEvent("highlightedtermchange", {
327
+ detail: this.value,
328
+ composed: true,
329
+ bubbles: true
180
330
  })
181
331
  );
182
332
  }
183
333
 
184
- private updateHistory(): void {
185
- const queryParams = new URLSearchParams(window.location.search);
186
- if (this.isValueEmpty) {
187
- console.log('IS EMPTY')
188
- queryParams.delete('q');
189
- window.history.replaceState({}, '', `?${queryParams.toString()}`);
190
- return;
191
- }
334
+ private updateRecentSearches = () =>
335
+ (this.recentSearches = UserRecentSearches.get().map(
336
+ (item: { id: string; label: string }) => ({
337
+ id: item,
338
+ label: item
339
+ })
340
+ ));
192
341
 
193
- if (this.value) {
194
- queryParams.set("q", this.value);
195
- }
196
- window.history.replaceState({}, '', `?${queryParams.toString()}`);
342
+ private dispatchSidebarSearchChange(searchTerm: string | undefined): void {
343
+ this.dispatchEvent(
344
+ new CustomEvent("sidebarsearchchange", {
345
+ detail: searchTerm ?? "",
346
+ bubbles: true,
347
+ composed: true
348
+ })
349
+ );
197
350
  }
198
351
 
199
352
  private submitSearch = debounce(() => {
200
- this.searchBox?.updateText(this.value || "");
201
- UserRecentSearches.add(this.value);
202
- this.recentSearches = UserRecentSearches.get();
203
- this.searchBox?.submit();
204
- }, 200).bind(this);
353
+ if (this.searchBox) {
354
+ UserRecentSearches.add(this.value);
355
+ this.updateRecentSearches();
356
+
357
+ this.searchBox.updateText(this.value || "");
358
+ this.searchBox.submit();
359
+ }
360
+ }, SEARCH_DEBOUNCE_DELAY);
205
361
 
206
362
  private onClickRecentSearch(e: CustomEvent) {
207
363
  this.value = e.detail;
@@ -222,15 +378,20 @@ export default class SidebarSearch extends LightningElement {
222
378
  private onInputChange(e: CustomEvent) {
223
379
  this.value = e.detail;
224
380
 
225
- if (this.searchBox && !this.isValueEmpty) {
226
- this.submitSearch();
227
- }
228
-
229
- if (this.isValueEmpty) {
381
+ if (this.value) {
382
+ this.dispatchOnLoading(true);
383
+ if (this.searchBox) {
384
+ this.submitSearch();
385
+ }
386
+ } else {
387
+ // coveo's reaction to an empty value triggers a return of results
388
+ // bootlegging our own ux here`
389
+ this.dispatchOnLoading(false);
230
390
  this.dispatchOnChangeEvent([]);
231
391
  }
232
392
 
233
- this.updateHistory();
393
+ this.dispatchHighlightedTermChange();
394
+ this.dispatchSidebarSearchChange(this.value);
234
395
  }
235
396
 
236
397
  private onInputFocus() {
@@ -0,0 +1,2 @@
1
+ export const getSidebarSearchParams = () =>
2
+ new URLSearchParams(window.location.search).get("q");
@@ -0,0 +1,55 @@
1
+ import SidebarSearchResult from "dx/sidebarSearchResult";
2
+ import { createRenderComponent } from "utils/tests";
3
+
4
+ const TAG = "dx-sidebar-search-result";
5
+ const render = createRenderComponent(TAG, SidebarSearchResult);
6
+
7
+ const PROPS = {
8
+ description: "test description here",
9
+ descriptionHighlights: [{ length: 5, offset: 5 }],
10
+ href: "/test",
11
+ selected: false,
12
+ title: "test title here",
13
+ titleHighlights: [{ length: 5, offset: 5 }]
14
+ };
15
+
16
+ describe(TAG, () => {
17
+ it("renders a result with the correct highlighted content", () => {
18
+ const element = render(PROPS);
19
+
20
+ const titleBolds = element.shadowRoot.querySelectorAll(".title .bold");
21
+ expect(titleBolds.length).toEqual(1);
22
+ expect(titleBolds[0].textContent).toEqual("title");
23
+
24
+ const descriptionBolds =
25
+ element.shadowRoot.querySelectorAll(".description .bold");
26
+ expect(descriptionBolds.length).toEqual(1);
27
+ expect(descriptionBolds[0].textContent).toEqual("descr");
28
+
29
+ return expect(element).toBeAccessible();
30
+ });
31
+
32
+ it("renders a result as selected when specified", () => {
33
+ const element = render({ ...PROPS, selected: true });
34
+
35
+ const selected = element.shadowRoot.querySelector(
36
+ ".sidebar-item-selected"
37
+ );
38
+ expect(selected).not.toBeNull();
39
+
40
+ return expect(element).toBeAccessible();
41
+ });
42
+
43
+ it("doesn't add bold class when there is no highlighted term", () => {
44
+ const element = render({
45
+ ...PROPS,
46
+ descriptionHighlights: [],
47
+ titleHighlights: []
48
+ });
49
+
50
+ const selected = element.shadowRoot.querySelector(".bold");
51
+ expect(selected).toBeNull();
52
+
53
+ return expect(element).toBeAccessible();
54
+ });
55
+ });