@salesforcedevs/dx-components 0.16.2 → 0.18.1

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 (52) hide show
  1. package/lwc.config.json +1 -0
  2. package/package.json +3 -3
  3. package/src/assets/svg/sidebar-loading.svg +34 -0
  4. package/src/modules/dx/dropdown/dropdown.html +2 -0
  5. package/src/modules/dx/dropdown/dropdown.ts +4 -2
  6. package/src/modules/dx/dropdownOption/__tests__/dropdownOption.test.ts +32 -2
  7. package/src/modules/dx/dropdownOption/dropdownOption.ts +9 -6
  8. package/src/modules/dx/footer/footer.ts +13 -1
  9. package/src/modules/dx/header/__tests__/header.test.ts +15 -21
  10. package/src/modules/dx/header/__tests__/mockProps.ts +7 -1
  11. package/src/modules/dx/headerNav/headerNav.html +1 -0
  12. package/src/modules/dx/headerSearch/__tests__/headerSearch.test.ts +24 -39
  13. package/src/modules/dx/headerSearch/__tests__/mockProps.ts +2 -7
  14. package/src/modules/dx/headerSearch/headerSearch.ts +13 -19
  15. package/src/modules/dx/lazy/lazy.ts +0 -7
  16. package/src/modules/dx/newsletterForm/newsletterForm.ts +20 -11
  17. package/src/modules/dx/sidebar/__tests__/mockSearchChangeEmptyEvent.ts +6 -0
  18. package/src/modules/dx/sidebar/__tests__/mockSearchChangeEvent.ts +527 -0
  19. package/src/modules/dx/sidebar/__tests__/sidebar.test.ts +210 -247
  20. package/src/modules/dx/sidebar/sidebar.css +68 -34
  21. package/src/modules/dx/sidebar/sidebar.html +58 -34
  22. package/src/modules/dx/sidebar/sidebar.stories.ts +50 -4
  23. package/src/modules/dx/sidebar/sidebar.ts +141 -73
  24. package/src/modules/dx/sidebarOld/__tests__/mockProps.ts +178 -0
  25. package/src/modules/dx/sidebarOld/__tests__/sidebarOld.test.ts +574 -0
  26. package/src/modules/dx/sidebarOld/sidebarOld.css +160 -0
  27. package/src/modules/dx/sidebarOld/sidebarOld.html +70 -0
  28. package/src/modules/dx/sidebarOld/sidebarOld.stories.ts +139 -0
  29. package/src/modules/dx/sidebarOld/sidebarOld.ts +187 -0
  30. package/src/modules/dx/sidebarSearch/__tests__/mockPreloadedState.ts +8843 -0
  31. package/src/modules/dx/sidebarSearch/__tests__/sidebarSearch.test.ts +113 -0
  32. package/src/modules/dx/sidebarSearch/sidebarSearch.css +3 -0
  33. package/src/modules/dx/sidebarSearch/sidebarSearch.html +26 -0
  34. package/src/modules/dx/sidebarSearch/sidebarSearch.ts +421 -0
  35. package/src/modules/dx/sidebarSearchResult/__tests__/sidebarSearchResult.test.ts +55 -0
  36. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +36 -0
  37. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.html +14 -0
  38. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.ts +56 -0
  39. package/src/modules/dx/treeTile/__tests__/treeTile.test.ts +27 -38
  40. package/src/modules/dx/treeTile/treeTile.css +3 -19
  41. package/src/modules/dx/treeTile/treeTile.html +4 -1
  42. package/src/modules/dx/treeTile/treeTile.ts +2 -2
  43. package/src/modules/helpers/sidebar/sidebar.css +21 -0
  44. package/src/modules/utils/coveo/coveo.ts +69 -0
  45. package/src/modules/utils/headerBase/headerBase.ts +10 -2
  46. package/src/modules/utils/highlight/__tests__/highlight.test.ts +108 -0
  47. package/src/modules/utils/highlight/__tests__/mockProps.ts +45 -0
  48. package/src/modules/utils/highlight/highlight.ts +2 -1
  49. package/src/modules/utils/jest/jest.ts +35 -0
  50. package/src/modules/utils/recentSearches/__tests__/recentSearches.test.ts +79 -0
  51. package/src/modules/utils/recentSearches/recentSearches.ts +57 -0
  52. package/src/modules/utils/tests/tests.ts +18 -11
@@ -0,0 +1,113 @@
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
+
7
+ const TAG = "dx-sidebar-search";
8
+ const render = createRenderComponent(TAG, SidebarSearch);
9
+
10
+ const PROPS = coveoXMLDocsConfig;
11
+ const QUERY = "test";
12
+
13
+ describe(TAG, () => {
14
+ afterEach(unmockQueryParameter);
15
+
16
+ it("renders with the correct initial empty query", () => {
17
+ const element = render(PROPS);
18
+
19
+ const input = element.shadowRoot.querySelector("dx-input");
20
+
21
+ expect(input.value).toEqual("");
22
+
23
+ return expect(element).toBeAccessible();
24
+ });
25
+
26
+ it("renders with the correct initial query", () => {
27
+ mockQueryParameter(QUERY);
28
+
29
+ const element = render(PROPS);
30
+
31
+ const input = element.shadowRoot.querySelector("dx-input");
32
+
33
+ expect(input.value).toEqual(QUERY);
34
+
35
+ return expect(element).toBeAccessible();
36
+ });
37
+
38
+ describe("highlighting", () => {
39
+ afterEach(unmockQueryParameter);
40
+
41
+ it("triggers on initial render when a search value exists", () => {
42
+ mockQueryParameter(QUERY);
43
+ const element = createElement(TAG, { is: SidebarSearch });
44
+ const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
45
+ renderLightningElement(element, PROPS);
46
+ expect(dispatchEventSpy).toHaveBeenCalledWith(
47
+ expect.objectContaining({
48
+ type: "highlightedtermchange",
49
+ detail: QUERY
50
+ })
51
+ );
52
+ });
53
+
54
+ it("does not trigger on initial render when a search value does not exist", () => {
55
+ const element = createElement(TAG, { is: SidebarSearch });
56
+ const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
57
+ renderLightningElement(element, PROPS);
58
+ expect(dispatchEventSpy).not.toHaveBeenCalledWith(
59
+ expect.objectContaining({ type: "highlightedtermchange" })
60
+ );
61
+ });
62
+
63
+ it("triggers on input change with non-empty value", () => {
64
+ const element = render(PROPS);
65
+ const dxInput = element.shadowRoot.querySelector("dx-input");
66
+ const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
67
+
68
+ dxInput.dispatchEvent(new CustomEvent("change", { detail: QUERY }));
69
+
70
+ expect(dispatchEventSpy).toHaveBeenCalledWith(
71
+ expect.objectContaining({
72
+ type: "highlightedtermchange",
73
+ detail: QUERY
74
+ })
75
+ );
76
+ });
77
+
78
+ it("triggers on input change with empty value", () => {
79
+ const element = render(PROPS);
80
+ const dxInput = element.shadowRoot.querySelector("dx-input");
81
+ const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
82
+
83
+ dxInput.dispatchEvent(new CustomEvent("change", { detail: "" }));
84
+
85
+ expect(dispatchEventSpy).toHaveBeenCalledWith(
86
+ expect.objectContaining({
87
+ type: "highlightedtermchange",
88
+ detail: ""
89
+ })
90
+ );
91
+ });
92
+ });
93
+ });
94
+
95
+ // git("correctly consumes a valid session storage object", () => {
96
+ // mockQueryParameter(mockPreloadedState.query.q);
97
+
98
+ // window.sessionStorage.setItem(
99
+ // SESSION_KEY,
100
+ // JSON.stringify(mockPreloadedState)
101
+ // );
102
+
103
+ // const mockOnChange = jest.fn();
104
+
105
+ // const element = render(PROPS);
106
+ // element.addEventListener("change", mockOnChange);
107
+
108
+ // return Promise.resolve().then(() => {
109
+ // expect(mockOnChange).toHaveBeenCalledTimes(1);
110
+
111
+ // expect(element).toBeAccessible();
112
+ // });
113
+ // });
@@ -0,0 +1,3 @@
1
+ .search-box {
2
+ --dx-c-input-width: 100%;
3
+ }
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <dx-dropdown
3
+ aria-label="Recent Searches"
4
+ open={isDropdownOpen}
5
+ options={recentSearches}
6
+ onchange={onClickRecentSearch}
7
+ onrequestclose={onDropdownRequestClose}
8
+ full-width
9
+ >
10
+ <dx-input
11
+ class="search-box"
12
+ type="search"
13
+ placeholder="Search this list..."
14
+ role="search"
15
+ aria-label="Content Search"
16
+ icon-symbol="search"
17
+ size="small"
18
+ value={value}
19
+ onchange={onInputChange}
20
+ onfocus={onInputFocus}
21
+ onclear={onInputClear}
22
+ shortcut-key="j"
23
+ clearable
24
+ ></dx-input>
25
+ </dx-dropdown>
26
+ </template>
@@ -0,0 +1,421 @@
1
+ import { LightningElement, api } from "lwc";
2
+ import debounce from "debounce";
3
+ import {
4
+ Result,
5
+ ResultList,
6
+ Unsubscribe,
7
+ ResultsPerPage,
8
+ SearchBox,
9
+ buildResultList,
10
+ buildResultsPerPage,
11
+ buildSearchBox,
12
+ loadAdvancedSearchQueryActions,
13
+ SearchEngine,
14
+ SearchAppState,
15
+ loadQueryActions
16
+ } from "@coveo/headless";
17
+ import { buildSearchEngine, getSidebarSearchParams } from "utils/coveo";
18
+ import { RecentSearches } from "utils/recentSearches";
19
+ import { toJson } from "utils/normalizers";
20
+ import {
21
+ Option,
22
+ PopoverRequestCloseType,
23
+ SidebarSearchResult
24
+ } from "typings/custom";
25
+
26
+ export const SESSION_KEY = "dx-sidebar-search-state";
27
+
28
+ const DEFAULT_RESULT_COUNT = 20;
29
+ const SEARCH_DEBOUNCE_DELAY = 780;
30
+
31
+ const UserRecentSearches = new RecentSearches();
32
+
33
+ const normalizeCoveoAdvancedQueryValue = (version: string): string =>
34
+ version.split(".").join("\\.");
35
+
36
+ export default class SidebarSearch extends LightningElement {
37
+ @api coveoOrganizationId!: string;
38
+ @api coveoPublicAccessToken!: string;
39
+ @api coveoSearchHub!: string;
40
+ @api
41
+ get coveoAdvancedQueryConfig(): { [key: string]: any } {
42
+ return this._coveoAdvancedQueryConfig;
43
+ }
44
+ set coveoAdvancedQueryConfig(value) {
45
+ const jsonValue = toJson(value);
46
+ const hasChanged =
47
+ this._coveoAdvancedQueryConfig &&
48
+ Object.entries(jsonValue).some(
49
+ ([key, val]: [string, any]) =>
50
+ this._coveoAdvancedQueryConfig[key] !== val
51
+ );
52
+ this._coveoAdvancedQueryConfig = jsonValue;
53
+
54
+ if (hasChanged && this.engine) {
55
+ // set advanced filters needs access to the latest coveoAdvancedQueryConfig
56
+ this.dispatchOnLoading(true);
57
+ this.setAdvancedFilters(this.engine);
58
+ this.submitSearch(true);
59
+ }
60
+ }
61
+
62
+ @api
63
+ public fetchMoreResults() {
64
+ if (this.resultList?.state?.moreResultsAvailable) {
65
+ this.fetchingMoreResults = true;
66
+ this.resultList.fetchMoreResults();
67
+ }
68
+ }
69
+
70
+ @api
71
+ setInputValue(searchTerm: string) {
72
+ this.value = searchTerm || "";
73
+ this.handleValueChange(true);
74
+ }
75
+
76
+ private _coveoAdvancedQueryConfig!: { [key: string]: any };
77
+ private dropdownRequestedOpen: boolean = false;
78
+ private recentSearches: Option[] = [];
79
+ private unsubscribeFromResultList: Unsubscribe = () => {};
80
+ private resultList: ResultList | null = null;
81
+ private resultsPerPage: ResultsPerPage | null = null;
82
+ private preloadedState?: SearchAppState;
83
+ private searchBox: SearchBox | null = null;
84
+ private value?: string = "";
85
+ private engine: SearchEngine | null = null;
86
+ private fetchingMoreResults: boolean = false;
87
+ private didRender = false;
88
+
89
+ private get hasCorrectCoveoConfig(): boolean {
90
+ const coveoConfigurations = [
91
+ this.coveoOrganizationId,
92
+ this.coveoPublicAccessToken,
93
+ this.coveoSearchHub,
94
+ this.coveoAdvancedQueryConfig
95
+ ];
96
+
97
+ return coveoConfigurations.every(
98
+ (val) =>
99
+ !!val && (typeof val === "string" || typeof val === "object")
100
+ );
101
+ }
102
+
103
+ private get isDropdownOpen() {
104
+ return (
105
+ this.dropdownRequestedOpen &&
106
+ this.recentSearches &&
107
+ this.recentSearches.length > 0 &&
108
+ !this.value
109
+ );
110
+ }
111
+
112
+ constructor() {
113
+ super();
114
+
115
+ this.updateRecentSearches();
116
+ this.value = getSidebarSearchParams() || "";
117
+ }
118
+
119
+ disconnectedCallback() {
120
+ this.unsubscribeFromResultList();
121
+ }
122
+
123
+ renderedCallback() {
124
+ // in prod some of the critical coveo configuration attributes seem to arrive later
125
+ // so I needed to put this coveo startup flow here where we can wait for them to arrive
126
+ if (this.hasCorrectCoveoConfig && !this.engine) {
127
+ this.initializeUserSession();
128
+ this.initializeCoveo();
129
+ } else if (!this.hasCorrectCoveoConfig) {
130
+ console.error(
131
+ "Incorrect coveo search configuration provided. All coveo configuration attributes must be defined.",
132
+ {
133
+ coveoOrganizationId: this.coveoOrganizationId,
134
+ coveoPublicAccessToken: this.coveoPublicAccessToken,
135
+ coveoSearchHub: this.coveoSearchHub,
136
+ coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig
137
+ }
138
+ );
139
+ }
140
+
141
+ // If this is the first render and there is a search value, trigger
142
+ // term highlighting
143
+ if (!this.didRender && this.value) {
144
+ this.dispatchHighlightedTermChange();
145
+ }
146
+
147
+ this.didRender = true;
148
+ }
149
+
150
+ private initializeUserSession() {
151
+ const stringified = window.sessionStorage.getItem(SESSION_KEY);
152
+ if (!stringified) {
153
+ return;
154
+ }
155
+
156
+ try {
157
+ const json = JSON.parse(stringified);
158
+ const hasSameConfig = Object.entries(
159
+ json.coveoAdvancedQueryConfig
160
+ ).every(
161
+ ([key, value]: [string, any]) =>
162
+ this.coveoAdvancedQueryConfig[key] === value
163
+ );
164
+ const hasSameQuery = json?.state?.query?.q === this.value;
165
+ if (hasSameConfig && hasSameQuery) {
166
+ this.preloadedState = json?.state;
167
+ } else {
168
+ window.sessionStorage.removeItem(SESSION_KEY);
169
+ }
170
+ } catch (e) {
171
+ console.error(e);
172
+ window.sessionStorage.removeItem(SESSION_KEY);
173
+ }
174
+ }
175
+
176
+ private initializeCoveo = () => {
177
+ try {
178
+ this.engine = buildSearchEngine({
179
+ organizationId: this.coveoOrganizationId,
180
+ publicAccessToken: this.coveoPublicAccessToken,
181
+ searchHub: this.coveoSearchHub,
182
+ preloadedState: this.preloadedState
183
+ });
184
+ } catch (ex) {
185
+ console.error(`Coveo engine failed to initialize (${ex})`);
186
+ }
187
+
188
+ if (!this.engine) {
189
+ console.error("Coveo engine failed to initialize!");
190
+ return;
191
+ }
192
+
193
+ if (this.preloadedState) {
194
+ const actions = loadQueryActions(this.engine);
195
+ this.engine.dispatch(actions.updateQuery({ q: this.value || "" }));
196
+ }
197
+
198
+ this.resultList = buildResultList(this.engine, {
199
+ options: { fieldsToInclude: ["sfhtml_url__c"] }
200
+ });
201
+
202
+ this.resultsPerPage = buildResultsPerPage(this.engine, {
203
+ initialState: {
204
+ numberOfResults: DEFAULT_RESULT_COUNT
205
+ }
206
+ });
207
+
208
+ this.searchBox = buildSearchBox(this.engine, {
209
+ options: { numberOfSuggestions: 3 }
210
+ });
211
+
212
+ this.setAdvancedFilters(this.engine);
213
+
214
+ this.unsubscribeFromResultList = this.resultList?.subscribe(
215
+ this.onResultListChange
216
+ );
217
+
218
+ if (this.value) {
219
+ this.dispatchHighlightedTermChange();
220
+ }
221
+
222
+ if (!this.preloadedState) {
223
+ if (this.value) {
224
+ this.submitSearch();
225
+ } else {
226
+ this.dispatchOnLoading(false);
227
+ }
228
+ }
229
+ };
230
+
231
+ private onResultListChange = () => {
232
+ if (!this.resultList) {
233
+ return;
234
+ }
235
+
236
+ const { isLoading, firstSearchExecuted, results } =
237
+ this.resultList.state;
238
+
239
+ if ((!firstSearchExecuted && !isLoading) || !this.value) {
240
+ // coveo search is firing off some unwanted payloads on startup
241
+ return;
242
+ }
243
+
244
+ if (!isLoading) {
245
+ this.dispatchOnChangeEvent(results);
246
+ }
247
+
248
+ if (!this.fetchingMoreResults) {
249
+ this.dispatchOnLoading(isLoading);
250
+ }
251
+
252
+ if (!isLoading && this.fetchingMoreResults) {
253
+ this.fetchingMoreResults = false;
254
+ }
255
+ };
256
+
257
+ private setAdvancedFilters(engine: SearchEngine) {
258
+ const aq = Object.entries(this.coveoAdvancedQueryConfig).reduce(
259
+ (result, [key, value]) =>
260
+ `${result}(@${key}=="${normalizeCoveoAdvancedQueryValue(
261
+ value
262
+ )}")`,
263
+ ""
264
+ );
265
+
266
+ const registerSearchQueryAction = loadAdvancedSearchQueryActions(
267
+ engine
268
+ ).registerAdvancedSearchQueries({
269
+ aq: aq
270
+ });
271
+
272
+ engine.dispatch(registerSearchQueryAction);
273
+ }
274
+
275
+ private normalizeCoveoResult = ({
276
+ clickUri,
277
+ excerpt,
278
+ excerptHighlights,
279
+ raw: { sfhtml_url__c },
280
+ title,
281
+ titleHighlights,
282
+ uniqueId
283
+ }: Result): SidebarSearchResult => {
284
+ // discussion about this normalization here: https://salesforce-internal.slack.com/archives/C020S6784JX/p1639506238376600
285
+ const extension =
286
+ this.coveoAdvancedQueryConfig.path_segment_4 === "references"
287
+ ? ""
288
+ : ".html";
289
+ const pathname = sfhtml_url__c
290
+ ? `/docs/${sfhtml_url__c}`
291
+ : `${new URL(clickUri).pathname}${extension}`;
292
+ const href = `${pathname}?q=${this.value}`;
293
+
294
+ return {
295
+ title,
296
+ titleHighlights,
297
+ excerpt,
298
+ excerptHighlights,
299
+ uniqueId,
300
+ href,
301
+ selected: pathname === window.location.pathname
302
+ };
303
+ };
304
+
305
+ private updateSessionStorage() {
306
+ window.sessionStorage.setItem(
307
+ SESSION_KEY,
308
+ JSON.stringify({
309
+ coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig,
310
+ state: this.engine?.state
311
+ })
312
+ );
313
+ }
314
+
315
+ private dispatchOnChangeEvent(results: Result[]) {
316
+ this.updateSessionStorage();
317
+ this.dispatchEvent(
318
+ new CustomEvent("change", {
319
+ detail: {
320
+ results: results.map(this.normalizeCoveoResult),
321
+ value: this.value
322
+ }
323
+ })
324
+ );
325
+ }
326
+
327
+ private dispatchOnLoading(loading: boolean) {
328
+ this.dispatchEvent(new CustomEvent("loading", { detail: loading }));
329
+ }
330
+
331
+ private dispatchHighlightedTermChange() {
332
+ this.dispatchEvent(
333
+ new CustomEvent("highlightedtermchange", {
334
+ detail: this.value,
335
+ composed: true,
336
+ bubbles: true
337
+ })
338
+ );
339
+ }
340
+
341
+ private updateRecentSearches = () =>
342
+ (this.recentSearches = UserRecentSearches.get().map((item: string) => ({
343
+ id: item,
344
+ label: item
345
+ })));
346
+
347
+ private dispatchSidebarSearchChange(searchTerm: string | undefined): void {
348
+ this.dispatchEvent(
349
+ new CustomEvent("sidebarsearchchange", {
350
+ detail: searchTerm ?? "",
351
+ bubbles: true,
352
+ composed: true
353
+ })
354
+ );
355
+ }
356
+
357
+ private submitSearch = debounce((isSyncingSearchValue = false) => {
358
+ if (this.searchBox) {
359
+ UserRecentSearches.add(this.value);
360
+ this.updateRecentSearches();
361
+
362
+ // When `isSyncingSearchValue` is true, we are submitting in a case
363
+ // where the search box is already synced correctly.
364
+ if (!isSyncingSearchValue) {
365
+ this.dispatchSidebarSearchChange(this.value);
366
+ }
367
+
368
+ this.searchBox.updateText(this.value || "");
369
+ this.searchBox.submit();
370
+ }
371
+ }, SEARCH_DEBOUNCE_DELAY);
372
+
373
+ private onClickRecentSearch(e: CustomEvent) {
374
+ this.value = e.detail;
375
+ this.dispatchSidebarSearchChange(this.value);
376
+ this.submitSearch(true);
377
+ }
378
+
379
+ private onDropdownRequestClose(e: CustomEvent) {
380
+ switch (e.detail as PopoverRequestCloseType) {
381
+ case "interact-outside":
382
+ case "keypress-escape":
383
+ this.dropdownRequestedOpen = false;
384
+ break;
385
+ default:
386
+ break;
387
+ }
388
+ }
389
+
390
+ private onInputChange(e: CustomEvent) {
391
+ this.value = e.detail;
392
+
393
+ this.handleValueChange(false);
394
+ this.dispatchHighlightedTermChange();
395
+ }
396
+
397
+ private onInputFocus() {
398
+ this.dropdownRequestedOpen = true;
399
+ }
400
+
401
+ private onInputClear() {
402
+ this.dropdownRequestedOpen = false;
403
+ }
404
+
405
+ private handleValueChange(isSyncingSearchValue = false) {
406
+ if (this.value) {
407
+ this.dispatchOnLoading(true);
408
+ if (this.searchBox) {
409
+ this.submitSearch(isSyncingSearchValue);
410
+ }
411
+ } else {
412
+ // coveo's reaction to an empty value triggers a return of results
413
+ // bootlegging our own ux here`
414
+ this.dispatchOnLoading(false);
415
+ this.dispatchOnChangeEvent([]);
416
+ // Empty search values are not submitted for search, so we need to manually
417
+ // trigger a search change on our own here
418
+ this.dispatchSidebarSearchChange(this.value);
419
+ }
420
+ }
421
+ }
@@ -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
+ });
@@ -0,0 +1,36 @@
1
+ @import "helpers/reset";
2
+ @import "helpers/text";
3
+ @import "helpers/sidebar";
4
+
5
+ a {
6
+ display: flex;
7
+ flex-direction: column;
8
+ }
9
+
10
+ .sidebar-item {
11
+ padding: var(--dx-g-spacing-smd) var(--dx-g-spacing-md);
12
+ }
13
+
14
+ .search-text {
15
+ font-size: var(--dx-g-text-xs);
16
+ font-family: var(--dx-g-font-sans);
17
+ line-height: 18px;
18
+ overflow-wrap: break-word;
19
+ }
20
+
21
+ .description {
22
+ -webkit-line-clamp: 3;
23
+ overflow-wrap: break-word;
24
+ }
25
+
26
+ .title {
27
+ color: var(--dx-g-text-heading-color);
28
+ }
29
+
30
+ .bold {
31
+ font-weight: 700;
32
+ }
33
+
34
+ a > *:not(:last-child) {
35
+ margin-bottom: 2px;
36
+ }
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <a class={className} href={href}>
3
+ <span class="title search-text">
4
+ <template for:each={titleChunks} for:item="chunk">
5
+ <span class={chunk.class} key={chunk.id}>{chunk.value}</span>
6
+ </template>
7
+ </span>
8
+ <span class="description dx-text-body-3 sidebar-item-truncate-text">
9
+ <template for:each={descriptionChunks} for:item="chunk">
10
+ <span class={chunk.class} key={chunk.id}>{chunk.value}</span>
11
+ </template>
12
+ </span>
13
+ </a>
14
+ </template>