@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.
- package/lwc.config.json +3 -3
- package/package.json +3 -4
- package/src/.DS_Store +0 -0
- package/src/assets/svg/docs-empty-state-small.svg +72 -0
- package/src/assets/svg/sidebar-loading.svg +34 -0
- package/src/modules/.DS_Store +0 -0
- package/src/modules/dx/.DS_Store +0 -0
- package/src/modules/dx/cardGrid/__tests__/cardMockProps.ts +78 -148
- package/src/modules/dx/cardGrid/cardGrid.css +0 -32
- package/src/modules/dx/cardGrid/cardGrid.html +5 -11
- package/src/modules/dx/codeBlock/__tests__/codeBlock.test.ts +47 -5
- package/src/modules/dx/codeBlock/codeBlock.stories.ts +2 -69
- package/src/modules/dx/codeBlock/codeBlock.ts +43 -22
- package/src/modules/dx/codeBlock/darkTheme.css +0 -1
- package/src/modules/dx/codeBlock/lightTheme.css +0 -1
- package/src/modules/dx/emptyState/__tests__/emptyState.test.ts +29 -10
- package/src/modules/dx/emptyState/__tests__/mockProps.ts +1 -3
- package/src/modules/dx/emptyState/emptyState.css +45 -13
- package/src/modules/dx/emptyState/emptyState.html +6 -4
- package/src/modules/dx/emptyState/emptyState.stories.ts +18 -38
- package/src/modules/dx/emptyState/emptyState.ts +11 -20
- package/src/modules/dx/footer/links.ts +3 -5
- package/src/modules/dx/header/__tests__/mockProps.ts +7 -1
- package/src/modules/dx/headerNav/__tests__/headerNav.test.ts +2 -4
- package/src/modules/dx/headerNav/headerNav.html +8 -6
- package/src/modules/dx/headerNav/headerNav.ts +2 -2
- package/src/modules/dx/headerSearch/headerSearch.html +2 -4
- package/src/modules/dx/headerSearch/headerSearch.ts +19 -25
- package/src/modules/dx/newsletterForm/newsletterForm.html +5 -3
- package/src/modules/dx/popover/popover.ts +11 -7
- package/src/modules/dx/searchResults/coveo.css +314 -111
- package/src/modules/dx/sidebar/__tests__/sidebar.test.ts +0 -70
- package/src/modules/dx/sidebar/sidebar.css +42 -23
- package/src/modules/dx/sidebar/sidebar.html +62 -40
- package/src/modules/dx/sidebar/sidebar.stories.ts +54 -4
- package/src/modules/dx/sidebar/sidebar.ts +126 -66
- package/src/modules/dx/sidebarSearch/__tests__/preloadedState.ts +3459 -0
- package/src/modules/dx/sidebarSearch/__tests__/sidebarSearch.test.ts +44 -0
- package/src/modules/dx/sidebarSearch/sidebarSearch.css +3 -0
- package/src/modules/dx/sidebarSearch/sidebarSearch.html +26 -0
- package/src/modules/dx/sidebarSearch/sidebarSearch.ts +358 -0
- package/src/modules/dx/sidebarSearch/utils.ts +2 -0
- package/src/modules/dx/sidebarSearchResult/__tests__/sidebarSearchResult.test.ts +55 -0
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +36 -0
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.html +14 -0
- package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.ts +56 -0
- package/src/modules/dx/spinner/__tests__/spinner.test.ts +37 -0
- package/src/modules/dx/spinner/spinner.css +750 -0
- package/src/modules/dx/spinner/spinner.html +6 -0
- package/src/modules/dx/spinner/spinner.stories.ts +69 -0
- package/src/modules/dx/spinner/spinner.ts +20 -0
- package/src/modules/dx/tab/__tests__/tab.test.ts +112 -54
- package/src/modules/dx/tab/tab.css +91 -16
- package/src/modules/dx/tab/tab.html +10 -12
- package/src/modules/dx/tab/tab.stories.ts +57 -0
- package/src/modules/dx/tab/tab.ts +35 -28
- package/src/modules/dx/tooltip/tippy.css +1 -0
- package/src/modules/dx/treeTile/__tests__/treeTile.test.ts +1 -1
- package/src/modules/dx/treeTile/treeTile.css +3 -19
- package/src/modules/dx/treeTile/treeTile.html +4 -1
- package/src/modules/dx/treeTile/treeTile.ts +2 -2
- package/src/modules/helpers/sidebar/sidebar.css +21 -0
- package/src/modules/helpers/table/table.css +23 -7
- package/src/modules/utils/coveo/coveo.ts +60 -0
- package/src/modules/utils/highlight/highlight.ts +31 -0
- package/src/modules/utils/normalizers/normalizers.ts +3 -0
- package/src/modules/utils/recentSearches/__tests__/recentSearches.test.ts +79 -0
- package/src/modules/utils/recentSearches/recentSearches.ts +57 -0
- package/src/modules/utils/regexps/regexps.ts +5 -0
- package/LICENSE +0 -12
- package/src/modules/dx/headerNavOption/__tests__/headerNavOption.test.ts +0 -113
- package/src/modules/dx/headerNavOption/headerNavOption.css +0 -91
- package/src/modules/dx/headerNavOption/headerNavOption.html +0 -9
- package/src/modules/dx/headerNavOption/headerNavOption.ts +0 -31
- package/src/modules/dx/headerSearch/__tests__/mockProps.ts +0 -11
- package/src/modules/dx/tab/__tests__/mockProps.ts +0 -4
- package/src/modules/dx/tabPanel/__tests__/mockProps.ts +0 -4
- package/src/modules/dx/tabPanel/__tests__/tabPanel.test.ts +0 -46
- package/src/modules/dx/tabPanel/tabPanel.css +0 -9
- package/src/modules/dx/tabPanel/tabPanel.html +0 -13
- package/src/modules/dx/tabPanel/tabPanel.ts +0 -27
- package/src/modules/dx/tabs/__tests__/mockProps.ts +0 -3
- package/src/modules/dx/tabs/__tests__/tabs.test.ts +0 -47
- package/src/modules/dx/tabs/tabs.css +0 -7
- package/src/modules/dx/tabs/tabs.html +0 -21
- package/src/modules/dx/tabs/tabs.stories.ts +0 -74
- package/src/modules/dx/tabs/tabs.ts +0 -112
- 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,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,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
|
+
});
|