@salesforcedevs/dx-components 0.10.0 → 0.10.1-alpha.100

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 (88) hide show
  1. package/lwc.config.json +3 -3
  2. package/package.json +3 -4
  3. package/src/.DS_Store +0 -0
  4. package/src/assets/svg/docs-empty-state-small.svg +72 -0
  5. package/src/assets/svg/sidebar-loading.svg +34 -0
  6. package/src/modules/.DS_Store +0 -0
  7. package/src/modules/dx/.DS_Store +0 -0
  8. package/src/modules/dx/cardGrid/__tests__/cardMockProps.ts +78 -148
  9. package/src/modules/dx/cardGrid/cardGrid.css +0 -32
  10. package/src/modules/dx/cardGrid/cardGrid.html +5 -11
  11. package/src/modules/dx/codeBlock/__tests__/codeBlock.test.ts +47 -5
  12. package/src/modules/dx/codeBlock/codeBlock.stories.ts +2 -69
  13. package/src/modules/dx/codeBlock/codeBlock.ts +43 -22
  14. package/src/modules/dx/codeBlock/darkTheme.css +0 -1
  15. package/src/modules/dx/codeBlock/lightTheme.css +0 -1
  16. package/src/modules/dx/emptyState/__tests__/emptyState.test.ts +29 -10
  17. package/src/modules/dx/emptyState/__tests__/mockProps.ts +1 -3
  18. package/src/modules/dx/emptyState/emptyState.css +45 -13
  19. package/src/modules/dx/emptyState/emptyState.html +6 -4
  20. package/src/modules/dx/emptyState/emptyState.stories.ts +18 -38
  21. package/src/modules/dx/emptyState/emptyState.ts +11 -20
  22. package/src/modules/dx/footer/links.ts +3 -5
  23. package/src/modules/dx/header/__tests__/mockProps.ts +7 -1
  24. package/src/modules/dx/headerNav/__tests__/headerNav.test.ts +2 -4
  25. package/src/modules/dx/headerNav/headerNav.html +8 -6
  26. package/src/modules/dx/headerNav/headerNav.ts +2 -2
  27. package/src/modules/dx/headerSearch/headerSearch.html +2 -4
  28. package/src/modules/dx/headerSearch/headerSearch.ts +19 -25
  29. package/src/modules/dx/newsletterForm/newsletterForm.html +5 -3
  30. package/src/modules/dx/popover/popover.ts +11 -7
  31. package/src/modules/dx/searchResults/coveo.css +314 -111
  32. package/src/modules/dx/sidebar/__tests__/sidebar.test.ts +0 -70
  33. package/src/modules/dx/sidebar/sidebar.css +42 -23
  34. package/src/modules/dx/sidebar/sidebar.html +62 -40
  35. package/src/modules/dx/sidebar/sidebar.stories.ts +54 -4
  36. package/src/modules/dx/sidebar/sidebar.ts +126 -66
  37. package/src/modules/dx/sidebarSearch/__tests__/preloadedState.ts +3459 -0
  38. package/src/modules/dx/sidebarSearch/__tests__/sidebarSearch.test.ts +44 -0
  39. package/src/modules/dx/sidebarSearch/sidebarSearch.css +3 -0
  40. package/src/modules/dx/sidebarSearch/sidebarSearch.html +26 -0
  41. package/src/modules/dx/sidebarSearch/sidebarSearch.ts +358 -0
  42. package/src/modules/dx/sidebarSearch/utils.ts +2 -0
  43. package/src/modules/dx/sidebarSearchResult/__tests__/sidebarSearchResult.test.ts +55 -0
  44. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +36 -0
  45. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.html +14 -0
  46. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.ts +56 -0
  47. package/src/modules/dx/spinner/__tests__/spinner.test.ts +37 -0
  48. package/src/modules/dx/spinner/spinner.css +750 -0
  49. package/src/modules/dx/spinner/spinner.html +6 -0
  50. package/src/modules/dx/spinner/spinner.stories.ts +69 -0
  51. package/src/modules/dx/spinner/spinner.ts +20 -0
  52. package/src/modules/dx/tab/__tests__/tab.test.ts +112 -54
  53. package/src/modules/dx/tab/tab.css +91 -16
  54. package/src/modules/dx/tab/tab.html +10 -12
  55. package/src/modules/dx/tab/tab.stories.ts +57 -0
  56. package/src/modules/dx/tab/tab.ts +35 -28
  57. package/src/modules/dx/tooltip/tippy.css +1 -0
  58. package/src/modules/dx/treeTile/__tests__/treeTile.test.ts +1 -1
  59. package/src/modules/dx/treeTile/treeTile.css +3 -19
  60. package/src/modules/dx/treeTile/treeTile.html +4 -1
  61. package/src/modules/dx/treeTile/treeTile.ts +2 -2
  62. package/src/modules/helpers/sidebar/sidebar.css +21 -0
  63. package/src/modules/helpers/table/table.css +23 -7
  64. package/src/modules/utils/coveo/coveo.ts +60 -0
  65. package/src/modules/utils/highlight/highlight.ts +31 -0
  66. package/src/modules/utils/normalizers/normalizers.ts +3 -0
  67. package/src/modules/utils/recentSearches/__tests__/recentSearches.test.ts +79 -0
  68. package/src/modules/utils/recentSearches/recentSearches.ts +57 -0
  69. package/src/modules/utils/regexps/regexps.ts +5 -0
  70. package/LICENSE +0 -12
  71. package/src/modules/dx/headerNavOption/__tests__/headerNavOption.test.ts +0 -113
  72. package/src/modules/dx/headerNavOption/headerNavOption.css +0 -91
  73. package/src/modules/dx/headerNavOption/headerNavOption.html +0 -9
  74. package/src/modules/dx/headerNavOption/headerNavOption.ts +0 -31
  75. package/src/modules/dx/headerSearch/__tests__/mockProps.ts +0 -11
  76. package/src/modules/dx/tab/__tests__/mockProps.ts +0 -4
  77. package/src/modules/dx/tabPanel/__tests__/mockProps.ts +0 -4
  78. package/src/modules/dx/tabPanel/__tests__/tabPanel.test.ts +0 -46
  79. package/src/modules/dx/tabPanel/tabPanel.css +0 -9
  80. package/src/modules/dx/tabPanel/tabPanel.html +0 -13
  81. package/src/modules/dx/tabPanel/tabPanel.ts +0 -27
  82. package/src/modules/dx/tabs/__tests__/mockProps.ts +0 -3
  83. package/src/modules/dx/tabs/__tests__/tabs.test.ts +0 -47
  84. package/src/modules/dx/tabs/tabs.css +0 -7
  85. package/src/modules/dx/tabs/tabs.html +0 -21
  86. package/src/modules/dx/tabs/tabs.stories.ts +0 -74
  87. package/src/modules/dx/tabs/tabs.ts +0 -112
  88. package/src/modules/dx/tabs/uniqueId.ts +0 -6
@@ -0,0 +1,44 @@
1
+ import SidebarSearch from "dx/sidebarSearch";
2
+ import { createRenderComponent } from "utils/tests";
3
+ import { coveoXMLDocsConfig } from "utils/coveo"; //coveoSFDocsConfig
4
+
5
+ const TAG = "dx-sidebar-search";
6
+ const render = createRenderComponent(TAG, SidebarSearch);
7
+
8
+ const PROPS = coveoXMLDocsConfig;
9
+ const QUERY = "test";
10
+
11
+ const mockQuery = (q: string): void => {
12
+ global.window = Object.create(window);
13
+ const url = `http://dummy.com?q="${q}"`;
14
+ Object.defineProperty(window, "location", {
15
+ value: {
16
+ href: url,
17
+ search: `?q=${q}`
18
+ }
19
+ });
20
+ };
21
+
22
+ describe(TAG, () => {
23
+ it("renders with the correct initial empty query", () => {
24
+ const element = render(PROPS);
25
+
26
+ const input = element.shadowRoot.querySelector("dx-input");
27
+
28
+ expect(input.value).toEqual("");
29
+
30
+ return expect(element).toBeAccessible();
31
+ });
32
+
33
+ it("renders with the correct initial query", () => {
34
+ mockQuery(QUERY);
35
+
36
+ const element = render(PROPS);
37
+
38
+ const input = element.shadowRoot.querySelector("dx-input");
39
+
40
+ expect(input.value).toEqual(QUERY);
41
+
42
+ return expect(element).toBeAccessible();
43
+ });
44
+ });
@@ -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,358 @@
1
+ import { LightningElement, api } from "lwc";
2
+ import debounce from "debounce";
3
+ import {
4
+ Result,
5
+ ResultList,
6
+ ResultsPerPage,
7
+ SearchBox,
8
+ buildResultList,
9
+ buildResultsPerPage,
10
+ buildSearchBox,
11
+ loadAdvancedSearchQueryActions,
12
+ SearchEngine,
13
+ buildSearchStatus,
14
+ SearchAppState
15
+ } from "@coveo/headless";
16
+ import { buildSearchEngine, getSidebarSearchParams } from "utils/coveo";
17
+ import { RecentSearches } from "utils/recentSearches";
18
+ import { toJson } from "utils/normalizers";
19
+ import { Option, PopoverRequestCloseType } from "typings/custom";
20
+
21
+ const DEFAULT_RESULT_COUNT = 20;
22
+
23
+ const UserRecentSearches = new RecentSearches();
24
+
25
+ const normalizeCoveoAdvancedQueryValue = (version: string): string =>
26
+ version.split(".").join("\\.");
27
+
28
+ function executeFirstSearch(engine: SearchEngine) {
29
+ return new Promise((resolve) => {
30
+ const searchStatus = buildSearchStatus(engine);
31
+ searchStatus.subscribe(
32
+ () => searchStatus.state.firstSearchExecuted && resolve(true)
33
+ );
34
+ engine.executeFirstSearch();
35
+ });
36
+ }
37
+
38
+ export default class SidebarSearch extends LightningElement {
39
+ @api coveoOrganizationId!: string;
40
+ @api coveoPublicAccessToken!: string;
41
+ @api coveoSearchHub!: string;
42
+ @api
43
+ get coveoAdvancedQueryConfig() {
44
+ return this._coveoAdvancedQueryConfig;
45
+ }
46
+ set coveoAdvancedQueryConfig(value) {
47
+ this._coveoAdvancedQueryConfig = toJson(value);
48
+ }
49
+
50
+ @api
51
+ public fetchMoreResults() {
52
+ if (this.resultList) {
53
+ this.resultList.fetchMoreResults();
54
+ }
55
+ }
56
+
57
+ private _coveoAdvancedQueryConfig!: object;
58
+ private dropdownRequestedOpen: boolean = false;
59
+ private recentSearches: Option[] = [];
60
+ private resultList: ResultList | null = null;
61
+ private resultsPerPage: ResultsPerPage | null = null;
62
+ private preloadedState?: SearchAppState;
63
+ private searchBox: SearchBox | null = null;
64
+ private value?: string = "";
65
+ private engine: SearchEngine | null = null;
66
+
67
+ private get hasCorrectCoveoConfig(): boolean {
68
+ const coveoConfigurations = [
69
+ this.coveoOrganizationId,
70
+ this.coveoPublicAccessToken,
71
+ this.coveoSearchHub,
72
+ this.coveoAdvancedQueryConfig
73
+ ];
74
+
75
+ return coveoConfigurations.every(
76
+ (val) =>
77
+ !!val && (typeof val === "string" || typeof val === "object")
78
+ );
79
+ }
80
+
81
+ private get isDropdownOpen() {
82
+ return (
83
+ this.dropdownRequestedOpen &&
84
+ this.recentSearches &&
85
+ this.recentSearches.length > 0 &&
86
+ this.isValueEmpty
87
+ );
88
+ }
89
+
90
+ private get isValueEmpty(): boolean {
91
+ return !this.value || this.value === "";
92
+ }
93
+
94
+ constructor() {
95
+ super();
96
+
97
+ this.updateRecentSearches();
98
+ this.value = getSidebarSearchParams() || "";
99
+ this.initializeUserSession();
100
+ }
101
+
102
+ renderedCallback() {
103
+ if (this.hasCorrectCoveoConfig && !this.engine) {
104
+ this.initializeCoveo();
105
+ } else if (!this.hasCorrectCoveoConfig) {
106
+ console.error(
107
+ "Incorrect coveo search configuration provided. All coveo configuration attributes must be defined.",
108
+ {
109
+ coveoOrganizationId: this.coveoOrganizationId,
110
+ coveoPublicAccessToken: this.coveoPublicAccessToken,
111
+ coveoSearchHub: this.coveoSearchHub,
112
+ coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig
113
+ }
114
+ );
115
+ }
116
+ }
117
+
118
+ private initializeUserSession() {
119
+ const stringified = window.sessionStorage.getItem(
120
+ "dx-sidebar-search-state"
121
+ );
122
+ if (!stringified) {
123
+ return;
124
+ }
125
+ const state = JSON.parse(stringified);
126
+ console.log(
127
+ { state },
128
+ state?.query?.q,
129
+ this.value,
130
+ state?.query?.q === this.value
131
+ );
132
+ if (state?.query?.q === this.value) {
133
+ console.log("assigning state");
134
+ this.preloadedState = state;
135
+ }
136
+ }
137
+
138
+ private initializeCoveo = async () => {
139
+ this.engine = buildSearchEngine({
140
+ organizationId: this.coveoOrganizationId,
141
+ publicAccessToken: this.coveoPublicAccessToken,
142
+ searchHub: this.coveoSearchHub,
143
+ preloadedState: this.preloadedState
144
+ });
145
+
146
+ if (!this.engine) {
147
+ return;
148
+ }
149
+
150
+ this.resultList = buildResultList(this.engine, {
151
+ options: { fieldsToInclude: ["sfhtml_url__c"] }
152
+ });
153
+
154
+ this.resultsPerPage = buildResultsPerPage(this.engine, {
155
+ initialState: {
156
+ numberOfResults: DEFAULT_RESULT_COUNT
157
+ }
158
+ });
159
+ this.searchBox = buildSearchBox(this.engine, {
160
+ options: { numberOfSuggestions: 3 }
161
+ });
162
+ this.setAdvancedFilters(this.engine);
163
+
164
+ this.resultList?.subscribe(this.onResultListChange);
165
+
166
+ await executeFirstSearch(this.engine);
167
+
168
+ console.log({ resultList: this.resultList, engine: this.engine });
169
+
170
+ if (!this.isValueEmpty) {
171
+ this.dispatchHighlightedTermChange();
172
+ }
173
+ this.dispatchOnLoading(false); // might not be needed
174
+ };
175
+
176
+ private onResultListChange = () => {
177
+ if (!this.resultList) {
178
+ return;
179
+ }
180
+
181
+ const { isLoading, firstSearchExecuted, results } =
182
+ this.resultList.state;
183
+
184
+ console.log("on results list change", {
185
+ isLoading,
186
+ firstSearchExecuted,
187
+ results
188
+ });
189
+
190
+ if ((!firstSearchExecuted && !isLoading) || this.isValueEmpty) {
191
+ // coveo search is firing off some unwanted payloads on startup
192
+ console.log(
193
+ "returning in results list change",
194
+ this.value,
195
+ this.isValueEmpty,
196
+ !firstSearchExecuted && !isLoading
197
+ );
198
+ return;
199
+ }
200
+
201
+ if (!isLoading) {
202
+ console.log(results);
203
+ this.dispatchOnChangeEvent(results);
204
+ }
205
+
206
+ this.dispatchOnLoading(isLoading);
207
+ };
208
+
209
+ private setAdvancedFilters(engine: SearchEngine) {
210
+ const aq = Object.entries(this.coveoAdvancedQueryConfig).reduce(
211
+ (result, [key, value]) =>
212
+ `${result}(@${key}=="${normalizeCoveoAdvancedQueryValue(
213
+ value
214
+ )}")`,
215
+ ""
216
+ );
217
+
218
+ const registerSearchQueryAction = loadAdvancedSearchQueryActions(
219
+ engine
220
+ ).registerAdvancedSearchQueries({
221
+ aq: aq
222
+ });
223
+
224
+ engine.dispatch(registerSearchQueryAction);
225
+ }
226
+
227
+ private normalizeCoveoResult = ({
228
+ clickUri,
229
+ excerpt,
230
+ excerptHighlights,
231
+ raw: { sfhtml_url__c },
232
+ title,
233
+ titleHighlights,
234
+ uniqueId
235
+ }: Result) => {
236
+ const pathname = sfhtml_url__c
237
+ ? `/docs/${sfhtml_url__c}`
238
+ : `${new URL(clickUri).pathname}.html`;
239
+ const href = `${pathname}?q=${this.value}`;
240
+
241
+ return {
242
+ title,
243
+ titleHighlights,
244
+ excerpt,
245
+ excerptHighlights,
246
+ uniqueId,
247
+ href,
248
+ selected: pathname === window.location.pathname
249
+ };
250
+ };
251
+
252
+ private updateSessionStorage() {
253
+ window.sessionStorage.setItem(
254
+ "dx-sidebar-search-state",
255
+ JSON.stringify(this.engine?.state)
256
+ );
257
+ }
258
+
259
+ private dispatchOnChangeEvent(results: Result[]) {
260
+ this.updateSessionStorage();
261
+ this.dispatchEvent(
262
+ new CustomEvent("change", {
263
+ detail: {
264
+ results: results.map(this.normalizeCoveoResult),
265
+ value: this.value
266
+ }
267
+ })
268
+ );
269
+ }
270
+
271
+ private dispatchOnLoading(loading: boolean) {
272
+ this.dispatchEvent(new CustomEvent("loading", { detail: loading }));
273
+ }
274
+
275
+ private dispatchHighlightedTermChange() {
276
+ this.dispatchEvent(
277
+ new CustomEvent("highlightedtermchange", {
278
+ detail: this.value,
279
+ composed: true,
280
+ bubbles: true
281
+ })
282
+ );
283
+ }
284
+
285
+ private updateRecentSearches = () =>
286
+ (this.recentSearches = UserRecentSearches.get().map(
287
+ (item: { id: string; label: string }) => ({
288
+ id: item,
289
+ label: item
290
+ })
291
+ ));
292
+
293
+ private updateHistory(): void {
294
+ const { pathname, hash, search } = window.location;
295
+ if (this.isValueEmpty) {
296
+ window.history.pushState({}, "", `${pathname}${hash}`);
297
+ return;
298
+ }
299
+
300
+ const queryParams = new URLSearchParams(search);
301
+ if (this.value) {
302
+ queryParams.set("q", this.value);
303
+ }
304
+ window.history.pushState({}, "", `?${queryParams.toString()}${hash}`);
305
+ }
306
+
307
+ private submitSearch = debounce(() => {
308
+ if (this.searchBox) {
309
+ this.searchBox.updateText(this.value || "");
310
+ UserRecentSearches.add(this.value);
311
+ this.updateRecentSearches();
312
+ console.log(this.value);
313
+ this.searchBox.submit();
314
+ }
315
+ }, 360).bind(this);
316
+
317
+ private onClickRecentSearch(e: CustomEvent) {
318
+ this.value = e.detail;
319
+ this.submitSearch();
320
+ }
321
+
322
+ private onDropdownRequestClose(e: CustomEvent) {
323
+ switch (e.detail as PopoverRequestCloseType) {
324
+ case "interact-outside":
325
+ case "keypress-escape":
326
+ this.dropdownRequestedOpen = false;
327
+ break;
328
+ default:
329
+ break;
330
+ }
331
+ }
332
+
333
+ private onInputChange(e: CustomEvent) {
334
+ this.value = e.detail;
335
+
336
+ if (this.searchBox && !this.isValueEmpty) {
337
+ this.submitSearch();
338
+ }
339
+
340
+ if (this.isValueEmpty) {
341
+ // coveo's reaction to an empty value triggers a return of results
342
+ // bootlegging our own ux here`
343
+ this.dispatchOnLoading(false);
344
+ this.dispatchOnChangeEvent([]);
345
+ }
346
+
347
+ this.dispatchHighlightedTermChange();
348
+ this.updateHistory();
349
+ }
350
+
351
+ private onInputFocus() {
352
+ this.dropdownRequestedOpen = true;
353
+ }
354
+
355
+ private onInputClear() {
356
+ this.dropdownRequestedOpen = false;
357
+ }
358
+ }
@@ -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
+ });
@@ -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>
@@ -0,0 +1,56 @@
1
+ import { LightningElement, api } from "lwc";
2
+ import cx from "classnames";
3
+ import { CoveoHighlights } from "typings/custom";
4
+
5
+ const toChunks = (value: string, highlights: CoveoHighlights) => {
6
+ if (!highlights || highlights.length < 1) {
7
+ return [
8
+ {
9
+ value,
10
+ id: 0
11
+ }
12
+ ];
13
+ }
14
+
15
+ let endIndex = 0;
16
+ return highlights.reduce((acc, { length, offset }) => {
17
+ const plainValue = value.slice(endIndex, offset);
18
+ const plainChunk = {
19
+ value: plainValue,
20
+ id: endIndex
21
+ };
22
+
23
+ endIndex = offset + length;
24
+ const boldValue = value.slice(offset, endIndex);
25
+ const boldChunk = {
26
+ value: boldValue,
27
+ class: "bold",
28
+ id: offset
29
+ };
30
+
31
+ return plainValue.length > 0
32
+ ? [...acc, plainChunk, boldChunk]
33
+ : [...acc, boldChunk];
34
+ }, []);
35
+ };
36
+
37
+ export default class SidebarSearchResult extends LightningElement {
38
+ @api description!: string;
39
+ @api descriptionHighlights!: CoveoHighlights;
40
+ @api href!: string;
41
+ @api selected!: boolean;
42
+ @api title!: string;
43
+ @api titleHighlights!: CoveoHighlights;
44
+
45
+ private get titleChunks() {
46
+ return toChunks(this.title, this.titleHighlights);
47
+ }
48
+
49
+ private get descriptionChunks() {
50
+ return toChunks(this.description, this.descriptionHighlights);
51
+ }
52
+
53
+ private get className(): string {
54
+ return cx("sidebar-item", this.selected && "sidebar-item-selected");
55
+ }
56
+ }
@@ -0,0 +1,37 @@
1
+ import Spinner from "dx/spinner";
2
+ import { createRenderComponent } from "utils/tests";
3
+
4
+ const TAG = "dx-spinner";
5
+ const render = createRenderComponent(TAG, Spinner);
6
+
7
+ describe(TAG, () => {
8
+ it("renders", () => {
9
+ const component: HTMLElement = render();
10
+
11
+ expect(component.classList).toHaveLength(1);
12
+ expect(component.classList).toContain("spinner_container");
13
+ const container = component.shadowRoot!.querySelector(".spinner");
14
+
15
+ expect(container).not.toBeNull();
16
+ expect(container?.classList).toHaveLength(2);
17
+ expect(container?.classList).toContain(`spinner_medium`);
18
+
19
+ return expect(component).toBeAccessible();
20
+ });
21
+
22
+ it("renders with size and variant", () => {
23
+ const size = "large";
24
+ const variant = "inverse";
25
+ const component: HTMLElement = render({
26
+ size,
27
+ variant
28
+ });
29
+
30
+ const container = component.shadowRoot?.querySelector(".spinner");
31
+ expect(container?.classList).toHaveLength(3);
32
+ expect(container?.classList).toContain(`spinner_${size}`);
33
+ expect(container?.classList).toContain(`spinner_${variant}`);
34
+
35
+ return expect(component).toBeAccessible();
36
+ });
37
+ });