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

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 +282 -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,364 @@
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
+ console.log('set coveoAdvancedQueryConfig', value.sfrelease__c, value.sflanguage)
41
+ this._coveoAdvancedQueryConfig = toJson(value);
42
+ }
46
43
 
44
+ @api
45
+ public fetchMoreResults() {
46
+ if (this.resultList?.state?.moreResultsAvailable) {
47
+ this.fetchingMoreResults = true;
48
+ this.resultList.fetchMoreResults();
49
+ }
50
+ }
51
+
52
+ @api
53
+ setInputValue(searchTerm: string) {
54
+ this.value = searchTerm || "";
55
+ }
56
+
57
+ private _coveoAdvancedQueryConfig!: object;
47
58
  private dropdownRequestedOpen: boolean = false;
48
59
  private recentSearches: Option[] = [];
49
60
  private resultList: ResultList | null = null;
61
+ private resultsPerPage: ResultsPerPage | null = null;
62
+ private preloadedState?: SearchAppState;
50
63
  private searchBox: SearchBox | null = null;
51
- private value?: string = undefined;
64
+ private value?: string = "";
52
65
  private engine: SearchEngine | null = null;
66
+ private fetchingMoreResults: boolean = false;
67
+ private didRender = false;
68
+
69
+ private get hasCorrectCoveoConfig(): boolean {
70
+ const coveoConfigurations = [
71
+ this.coveoOrganizationId,
72
+ this.coveoPublicAccessToken,
73
+ this.coveoSearchHub,
74
+ this.coveoAdvancedQueryConfig
75
+ ];
76
+
77
+ return coveoConfigurations.every(
78
+ (val) =>
79
+ !!val && (typeof val === "string" || typeof val === "object")
80
+ );
81
+ }
53
82
 
54
83
  private get isDropdownOpen() {
55
84
  return (
56
85
  this.dropdownRequestedOpen &&
57
86
  this.recentSearches &&
58
87
  this.recentSearches.length > 0 &&
59
- this.isValueEmpty
88
+ !this.value
60
89
  );
61
90
  }
62
91
 
63
- private get isValueEmpty(): boolean {
64
- return !this.value || this.value === "";
65
- }
92
+ constructor() {
93
+ super();
66
94
 
67
- connectedCallback() {
68
- this.recentSearches = UserRecentSearches.get();
95
+ this.updateRecentSearches();
96
+ this.value = getSidebarSearchParams() || "";
69
97
  }
70
98
 
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
- ];
99
+ queryConfigValue: string | null = null;
81
100
 
82
- if (
83
- coveoConfigurations.every((val) => !!val && typeof val === "string")
84
- ) {
85
- if (!this.engine) {
86
- this.instantiateSearchbox();
87
- }
88
- this.setAdvancedFilters();
89
- } else {
101
+ renderedCallback() {
102
+ // in prod some of the critical coveo configuration attributes seem to arrive later
103
+ // so I needed to put this coveo startup flow here where we can wait for them to arrive
104
+ if (this.hasCorrectCoveoConfig && !this.engine) {
105
+ console.log("coveo config is good");
106
+ this.initializeUserSession();
107
+ this.initializeCoveo();
108
+ } else if (!this.hasCorrectCoveoConfig) {
90
109
  console.error(
91
- "Incorrect coveo configuration provided. All coveo configuration attributes must be defined and a string.",
110
+ "Incorrect coveo search configuration provided. All coveo configuration attributes must be defined.",
92
111
  {
93
112
  coveoOrganizationId: this.coveoOrganizationId,
94
113
  coveoPublicAccessToken: this.coveoPublicAccessToken,
95
114
  coveoSearchHub: this.coveoSearchHub,
96
- coveoObjectType: this.coveoObjectType,
97
- coveoLanguage: this.coveoLanguage,
98
- coveoRelease: this.coveoRelease,
99
- coveoDeliverable: this.coveoDeliverable
115
+ coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig
100
116
  }
101
117
  );
102
118
  }
119
+
120
+ const queryConfigValue = JSON.stringify(this.coveoAdvancedQueryConfig);
121
+ console.log('query config value', queryConfigValue);
122
+ if (this.queryConfigValue !== queryConfigValue) {
123
+ if (this.queryConfigValue && this.engine) {
124
+ console.log('setting query config again')
125
+ this.setAdvancedFilters(this.engine);
126
+ }
127
+ this.queryConfigValue = queryConfigValue;
128
+ }
129
+
130
+ // If this is the first render and there is a search value, trigger
131
+ // term highlighting
132
+ if (!this.didRender && this.value) {
133
+ this.dispatchHighlightedTermChange();
134
+ }
135
+
136
+ this.didRender = true;
103
137
  }
104
138
 
105
- private instantiateSearchbox() {
106
- const options: SearchBoxOptions = {
107
- numberOfSuggestions: 3
108
- };
139
+ private initializeUserSession() {
140
+ const stringified = window.sessionStorage.getItem(SESSION_KEY);
141
+ if (!stringified) {
142
+ return;
143
+ }
109
144
 
110
- this.engine = buildSearchEngine({
111
- organizationId: this.coveoOrganizationId,
112
- publicAccessToken: this.coveoPublicAccessToken,
113
- searchHub: this.coveoSearchHub
114
- });
145
+ try {
146
+ const json = JSON.parse(stringified);
147
+ const hasSameConfig = Object.entries(
148
+ json.coveoAdvancedQueryConfig
149
+ ).every(
150
+ ([key, value]: [string, any]) =>
151
+ this.coveoAdvancedQueryConfig[key] === value
152
+ );
153
+ const hasSameQuery = json?.state?.query?.q === this.value;
154
+ if (hasSameConfig && hasSameQuery) {
155
+ console.log(
156
+ "valid session storage found for value =",
157
+ this.value
158
+ );
159
+ this.preloadedState = json?.state;
160
+ } else {
161
+ window.sessionStorage.removeItem(SESSION_KEY);
162
+ }
163
+ } catch (e) {
164
+ console.error(e);
165
+ window.sessionStorage.removeItem(SESSION_KEY);
166
+ }
167
+ }
115
168
 
116
- if (this.engine) {
117
- this.resultList = buildResultList(this.engine, {
118
- options: { fieldsToInclude: ["sfhtml_url__c"] }
169
+ private initializeCoveo = () => {
170
+ try {
171
+ this.engine = buildSearchEngine({
172
+ organizationId: this.coveoOrganizationId,
173
+ publicAccessToken: this.coveoPublicAccessToken,
174
+ searchHub: this.coveoSearchHub,
175
+ preloadedState: this.preloadedState
119
176
  });
120
- this.searchBox = buildSearchBox(this.engine, { options });
177
+ } catch (ex) {
178
+ console.error(`Coveo engine failed to initialize (${ex})`);
121
179
  }
122
180
 
123
- this.resultList?.subscribe(() => {
124
- if (!this.resultList) {
125
- return;
126
- }
181
+ if (!this.engine) {
182
+ console.error("Coveo engine failed to initialize!");
183
+ return;
184
+ }
127
185
 
128
- const { isLoading, results } = this.resultList.state;
186
+ if (this.preloadedState) {
187
+ const actions = loadQueryActions(this.engine);
188
+ this.engine.dispatch(actions.updateQuery({ q: this.value || "" }));
189
+ }
190
+
191
+ this.resultList = buildResultList(this.engine, {
192
+ options: { fieldsToInclude: ["sfhtml_url__c"] }
193
+ });
129
194
 
130
- if (!isLoading) {
131
- this.dispatchOnChangeEvent(results);
195
+ this.resultsPerPage = buildResultsPerPage(this.engine, {
196
+ initialState: {
197
+ numberOfResults: DEFAULT_RESULT_COUNT
132
198
  }
199
+ });
133
200
 
134
- this.dispatchOnLoading(isLoading);
201
+ this.searchBox = buildSearchBox(this.engine, {
202
+ options: { numberOfSuggestions: 3 }
135
203
  });
136
- }
137
204
 
138
- private setAdvancedFilters() {
139
- if (!this.engine) {
205
+ this.setAdvancedFilters(this.engine);
206
+
207
+ this.resultList?.subscribe(this.onResultListChange);
208
+
209
+ if (this.value) {
210
+ this.dispatchHighlightedTermChange();
211
+ }
212
+
213
+ if (!this.preloadedState) {
214
+ if (this.value) {
215
+ this.submitSearch();
216
+ } else {
217
+ this.dispatchOnLoading(false);
218
+ }
219
+ }
220
+ };
221
+
222
+ private onResultListChange = () => {
223
+ if (!this.resultList) {
140
224
  return;
141
225
  }
142
226
 
143
- const aqParts = {
144
- objecttype: this.coveoObjectType,
145
- sflanguage: this.coveoLanguage,
146
- sfrelease__c: toSearchVersionString(this.coveoRelease),
147
- sfdelivarable__c: this.coveoDeliverable
148
- };
227
+ const { isLoading, firstSearchExecuted, results } =
228
+ this.resultList.state;
229
+
230
+ if ((!firstSearchExecuted && !isLoading) || !this.value) {
231
+ // coveo search is firing off some unwanted payloads on startup
232
+ console.log(
233
+ "returning in results list change",
234
+ this.value,
235
+ !firstSearchExecuted && !isLoading
236
+ );
237
+ return;
238
+ }
239
+
240
+ if (!isLoading) {
241
+ console.log("dispatching on change event");
242
+ this.dispatchOnChangeEvent(results);
243
+ }
244
+
245
+ if (!this.fetchingMoreResults) {
246
+ this.dispatchOnLoading(isLoading);
247
+ }
149
248
 
150
- const aq = Object.entries(aqParts).reduce(
151
- (result, [key, value]) => `${result}(@${key}==${value})`,
249
+ if (!isLoading && this.fetchingMoreResults) {
250
+ this.fetchingMoreResults = false;
251
+ }
252
+ };
253
+
254
+ private setAdvancedFilters(engine: SearchEngine) {
255
+ const aq = Object.entries(this.coveoAdvancedQueryConfig).reduce(
256
+ (result, [key, value]) =>
257
+ `${result}(@${key}=="${normalizeCoveoAdvancedQueryValue(
258
+ value
259
+ )}")`,
152
260
  ""
153
261
  );
154
- const advancedSearchQueryActions = loadAdvancedSearchQueryActions(
155
- this.engine
156
- );
157
262
 
158
- this.engine.dispatch(
159
- advancedSearchQueryActions.registerAdvancedSearchQueries({
160
- aq: aq
263
+ const registerSearchQueryAction = loadAdvancedSearchQueryActions(
264
+ engine
265
+ ).registerAdvancedSearchQueries({
266
+ aq: aq
267
+ });
268
+
269
+ engine.dispatch(registerSearchQueryAction);
270
+ }
271
+
272
+ private normalizeCoveoResult = ({
273
+ clickUri,
274
+ excerpt,
275
+ excerptHighlights,
276
+ raw: { sfhtml_url__c },
277
+ title,
278
+ titleHighlights,
279
+ uniqueId
280
+ }: Result) => {
281
+ const pathname = sfhtml_url__c
282
+ ? `/docs/${sfhtml_url__c}`
283
+ : `${new URL(clickUri).pathname}.html`;
284
+ const href = `${pathname}?q=${this.value}`;
285
+
286
+ return {
287
+ title,
288
+ titleHighlights,
289
+ excerpt,
290
+ excerptHighlights,
291
+ uniqueId,
292
+ href,
293
+ selected: pathname === window.location.pathname
294
+ };
295
+ };
296
+
297
+ private updateSessionStorage() {
298
+ console.log("updating session storage");
299
+ window.sessionStorage.setItem(
300
+ SESSION_KEY,
301
+ JSON.stringify({
302
+ coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig,
303
+ state: this.engine?.state
161
304
  })
162
305
  );
163
306
  }
164
307
 
165
308
  private dispatchOnChangeEvent(results: Result[]) {
309
+ this.updateSessionStorage();
166
310
  this.dispatchEvent(
167
311
  new CustomEvent("change", {
168
312
  detail: {
169
- results: results.map(normalizeCoveoResult),
313
+ results: results.map(this.normalizeCoveoResult),
170
314
  value: this.value
171
315
  }
172
316
  })
173
317
  );
174
318
  }
175
319
 
176
- private dispatchOnLoading(isLoading: boolean) {
320
+ private dispatchOnLoading(loading: boolean) {
321
+ console.log("dispatching onloading", loading);
322
+ this.dispatchEvent(new CustomEvent("loading", { detail: loading }));
323
+ }
324
+
325
+ private dispatchHighlightedTermChange() {
177
326
  this.dispatchEvent(
178
- new CustomEvent("loading", {
179
- detail: isLoading
327
+ new CustomEvent("highlightedtermchange", {
328
+ detail: this.value,
329
+ composed: true,
330
+ bubbles: true
180
331
  })
181
332
  );
182
333
  }
183
334
 
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
- }
335
+ private updateRecentSearches = () =>
336
+ (this.recentSearches = UserRecentSearches.get().map(
337
+ (item: { id: string; label: string }) => ({
338
+ id: item,
339
+ label: item
340
+ })
341
+ ));
192
342
 
193
- if (this.value) {
194
- queryParams.set("q", this.value);
195
- }
196
- window.history.replaceState({}, '', `?${queryParams.toString()}`);
343
+ private dispatchSidebarSearchChange(searchTerm: string | undefined): void {
344
+ this.dispatchEvent(
345
+ new CustomEvent("sidebarsearchchange", {
346
+ detail: searchTerm ?? "",
347
+ bubbles: true,
348
+ composed: true
349
+ })
350
+ );
197
351
  }
198
352
 
199
353
  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);
354
+ if (this.searchBox) {
355
+ UserRecentSearches.add(this.value);
356
+ this.updateRecentSearches();
357
+
358
+ this.searchBox.updateText(this.value || "");
359
+ this.searchBox.submit();
360
+ }
361
+ }, SEARCH_DEBOUNCE_DELAY);
205
362
 
206
363
  private onClickRecentSearch(e: CustomEvent) {
207
364
  this.value = e.detail;
@@ -222,15 +379,20 @@ export default class SidebarSearch extends LightningElement {
222
379
  private onInputChange(e: CustomEvent) {
223
380
  this.value = e.detail;
224
381
 
225
- if (this.searchBox && !this.isValueEmpty) {
226
- this.submitSearch();
227
- }
228
-
229
- if (this.isValueEmpty) {
382
+ if (this.value) {
383
+ this.dispatchOnLoading(true);
384
+ if (this.searchBox) {
385
+ this.submitSearch();
386
+ }
387
+ } else {
388
+ // coveo's reaction to an empty value triggers a return of results
389
+ // bootlegging our own ux here`
390
+ this.dispatchOnLoading(false);
230
391
  this.dispatchOnChangeEvent([]);
231
392
  }
232
393
 
233
- this.updateHistory();
394
+ this.dispatchHighlightedTermChange();
395
+ this.dispatchSidebarSearchChange(this.value);
234
396
  }
235
397
 
236
398
  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
+ });