@searchstax-inc/searchstudio-ux-react 1.0.46 → 2.0.0

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 (20) hide show
  1. package/README.md +1267 -702
  2. package/README.mustache +556 -0
  3. package/dist/@searchstax-inc/components/templates/SearchstaxAnswerWidgetTemplate.d.ts +3 -0
  4. package/dist/@searchstax-inc/components/templates/SearchstaxExternalPromotionsWidgetTemplate.d.ts +3 -0
  5. package/dist/@searchstax-inc/components/templates/SearchstaxFacetsWidgetTemplate.d.ts +8 -0
  6. package/dist/@searchstax-inc/components/templates/SearchstaxInputWidgetTemplate.d.ts +4 -0
  7. package/dist/@searchstax-inc/components/templates/SearchstaxLocationWidgetTemplate.d.ts +3 -0
  8. package/dist/@searchstax-inc/components/templates/SearchstaxLocationWidgetTemplateConfig.d.ts +13 -0
  9. package/dist/@searchstax-inc/components/templates/SearchstaxOverviewWidgetTemplate.d.ts +3 -0
  10. package/dist/@searchstax-inc/components/templates/SearchstaxPaginationWidgetTemplate.d.ts +4 -0
  11. package/dist/@searchstax-inc/components/templates/SearchstaxRelatedSearchesWidgetTemplate.d.ts +3 -0
  12. package/dist/@searchstax-inc/components/templates/SearchstaxResultWidgetTemplate.d.ts +4 -0
  13. package/dist/@searchstax-inc/components/templates/SearchstaxSortingWidgetTemplate.d.ts +3 -0
  14. package/dist/@searchstax-inc/main.d.ts +1 -12
  15. package/dist/@searchstax-inc/searchstudio-ux-react.cjs +56 -54
  16. package/dist/@searchstax-inc/searchstudio-ux-react.d.cts +1 -12
  17. package/dist/@searchstax-inc/searchstudio-ux-react.iife.js +58 -56
  18. package/dist/@searchstax-inc/searchstudio-ux-react.mjs +2263 -2094
  19. package/dist/main.d.ts +12 -0
  20. package/package.json +5 -3
package/README.md CHANGED
@@ -2,129 +2,85 @@
2
2
 
3
3
  Library to build Site Search page
4
4
 
5
- ## changelog
6
- changelog is documented at CHANGELOG.md
7
-
8
5
  ## Installation
9
6
  npm install following package
10
7
  `npm install --save @searchstax-inc/searchstudio-ux-react`
11
8
 
12
- Add following code to `<head>`
9
+
10
+ ## Usage
11
+
12
+ Add the following code to <head>:
13
13
 
14
14
  ```
15
15
  <script type="text/javascript">
16
16
  var _msq = _msq || []; //declare object
17
- var analyticsBaseUrl = 'https://analytics-us-east.searchstax.co';
17
+ var analyticsBaseUrl = '<https://analytics-us-east.searchstax.co>';
18
18
  (function () {
19
19
  var ms = document.createElement('script');
20
20
  ms.type = 'text/javascript';
21
- ms.src = 'https://static.searchstax.co/studio-js/v3/js/studio-analytics.js';
21
+ ms.src = '<https://static.searchstax.co/studio-js/v3/js/studio-analytics.js>';
22
22
  var s = document.getElementsByTagName('script')[0];
23
23
  s.parentNode.insertBefore(ms, s);
24
24
  })();
25
25
  </script>
26
26
  ```
27
- ## Usage
28
-
29
- After importing SearchstaxWrapper component needs to wrap all other components:
30
27
 
28
+ ## Initialization
29
+ sampleConfig object needs to be of type: `ISearchstaxConfig`
31
30
  ```
31
+ const sampleConfig = {
32
+ language: 'en',
33
+ searchURL: '',
34
+ suggesterURL: '',
35
+ trackApiKey: '',
36
+ searchAuth: '',
37
+ authType: 'basic',
38
+ router: {
39
+ enabled: true,
40
+ routeName: 'searchstax',
41
+ title: (result) => `Search results for: ${result.query}`,
42
+ ignoredKeys: []
43
+ },
44
+ hooks: {
45
+ beforeSearch: (props) => {
46
+ // modify props
47
+ return props;
48
+ },
49
+ afterSearch: (results) => {
50
+ // modify results
51
+ return results;
52
+ }
53
+ }
54
+ };
32
55
 
33
56
  <SearchstaxWrapper
34
- searchURL={config.searchURL}
35
- suggesterURL={config.suggesterURL}
36
- trackApiKey={config.trackApiKey}
37
- searchAuth={config.searchAuth}
38
- initialized={initialized}
39
- beforeSearch={beforeSearch}
40
- afterSearch={afterSearch}
41
- authType="basic"
42
- language="en"
43
- model=""
44
- >
45
- // other components will go there
46
-
47
- </SearchstaxWrapper>
48
- ```
57
+ language={sampleConfig.language}
58
+ searchURL={sampleConfig.searchURL}
59
+ suggesterURL={sampleConfig.suggesterURL}
60
+ trackApiKey={sampleConfig.trackApiKey}
61
+ searchAuth={sampleConfig.searchAuth}
62
+ authType={sampleConfig.authType}
63
+ router={sampleConfig.router}
64
+ beforeSearch={sampleConfig.hooks.beforeSearch}
65
+ afterSearch={sampleConfig.hooks.afterSearch}
66
+ >
67
+ </SearchstaxWrapper>
49
68
 
50
- ## Initialization
51
- Initialization object needs to be of type: [ISearchstaxConfig](https://github.com/searchstax/searchstudio-ux-js/blob/main/src/interfaces/connector.interface.ts#L5)
52
69
 
53
- Initialization example
54
- ```
55
- sampleConfig = {
56
- language: "en",
57
- model:""
58
- searchURL: "",
59
- suggesterURL: "",
60
- trackApiKey: "",
61
- searchAuth: "",
62
- authType: "basic",
63
- router: {
64
- enabled: true,
65
- routeName: "searchstax",
66
- title: (result: ISearchObject) => {
67
- return "Search results for: " + result.query;
68
- },
69
- ignoredKeys: [],
70
- },
71
- hooks: {
72
- beforeSearch: function (props: ISearchObject) {
73
- const propsCopy = { ...props };
74
- return propsCopy;
75
- },
76
- afterSearch: function (results: ISearchstaxParsedResult[]) {
77
- const copy = [...results];
78
- return copy;
79
- },
80
- }
81
- };
82
70
  ```
83
71
 
84
72
  ## Initial layout
85
- Our base theme is designed with this layout in mind but it is optional as all widgets have id parameters and can be attached to any element.
73
+ Add any other Site Search components as needed:
86
74
  ```
87
- <SearchstaxWrapper
88
- searchURL={sampleConfig.searchURL}
89
- suggesterURL={sampleConfig.suggesterURL}
90
- trackApiKey={sampleConfig.trackApiKey}
91
- searchAuth={sampleConfig.searchAuth}
92
- beforeSearch={sampleConfig.hooks.beforeSearch}
93
- afterSearch={sampleConfig.hooks.afterSearch}
94
- authType="basic"
95
- language="en"
96
- model=""
97
- >
98
- <div className="searchstax-page-layout-container">
99
- <SearchstaxInputWidget
100
- afterAutosuggest={afterAutosuggest}
101
- beforeAutosuggest={beforeAutosuggest}
102
- inputTemplate={InputTemplate}
103
- ></SearchstaxInputWidget>
75
+ <SearchstaxWrapper>
104
76
 
105
- <div className="search-details-container">
106
- <div id="search-feedback-container"></div>
107
- <div id="search-sorting-container"></div>
108
- </div>
77
+ <SearchstaxInputWidget />
109
78
 
110
- <div className="searchstax-page-layout-facet-result-container">
111
- <div className="searchstax-page-layout-facet-container">
112
- <div id="searchstax-facets-container"></div>
113
- </div>
79
+ <SearchstaxResultsWidget />
114
80
 
115
- <div className="searchstax-page-layout-result-container">
116
- <div id="searchstax-external-promotions-layout-container"></div>
117
- <SearchstaxResultWidget
118
- afterLinkClick={afterLinkClick}
119
- noResultTemplate={noResultTemplate}
120
- resultsTemplate={resultsTemplate}
121
- ></SearchstaxResultWidget>
122
- <div id="searchstax-related-searches-container"></div>
123
- <div id="searchstax-pagination-container"></div>
124
- </div>
125
- </div>
126
- </div>
127
- </SearchstaxWrapper>
81
+ {/* Other components */}
82
+
83
+ </SearchstaxWrapper>
128
84
  ```
129
85
 
130
86
  ### widgets
@@ -151,19 +107,51 @@ Following widgets are available:
151
107
  [sorting Widget](#sorting-widget)
152
108
 
153
109
  ### Answer Widget ###
110
+ SearchStax Site Search solution offers React and Next.js widgets to assist in building your custom search page.
111
+
112
+ The SearchstaxAnswerWidget component provides a React and Next.js AI answer widget for your searches.
113
+
114
+ **Usage**
154
115
 
155
- example of answer widget initialization with minimum options
156
116
  ```
157
117
  <SearchstaxAnswerWidget
158
118
  showShowMoreAfterWordCount={100}
119
+ feedbackConfig={feedbackConfig}
159
120
  ></SearchstaxAnswerWidget>
160
121
  ```
161
122
 
123
+ **Props**
162
124
 
163
- example of answer widget initialization with lightweight feedback widget
125
+ - showShowMoreAfterWordCount - number(default 100) determining after how many symbols UI will show “Read More” view.
126
+ - feedbackWidget – an optional object that configures thumbs-up and thumbs-down feedback functionality.
127
+
128
+ Example of feedbackWidget config:
129
+ ```
130
+ const feedbackConfig = {
131
+ renderFeedbackWidget: true,
132
+ emailOverride: searchstaxEmailOverride,
133
+ thumbsUpValue: 10,
134
+ thumbsDownValue: 0
135
+ }
164
136
  ```
165
137
 
166
- export function answerTemplate(
138
+
139
+ **searchAnswerTemplate**
140
+
141
+ The templates prop allows customizing the answer UI.
142
+
143
+ It receives the following props:
144
+
145
+ - shouldShowAnswer – boolean
146
+ - answerErrorMessage – string
147
+ - fullAnswerFormatted – string
148
+ - showMoreButtonVisible – boolean
149
+ - answerLoading – boolean
150
+
151
+ **Example**
152
+
153
+ ```
154
+ function answerTemplate(
167
155
  answerData: null | ISearchstaxAnswerData,
168
156
  showMore: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
169
157
  ) {
@@ -198,7 +186,7 @@ export function answerTemplate(
198
186
  showMore(e);
199
187
  }}
200
188
  >
201
- Read More
189
+ Read more
202
190
  </button>
203
191
  </div>
204
192
  )}
@@ -213,67 +201,205 @@ export function answerTemplate(
213
201
  </>
214
202
  );
215
203
  }
216
-
217
- const feedbackConfig = {
218
- renderFeedbackWidget: true,
219
- emailOverride: searchstaxEmailOverride,
220
- thumbsUpValue: 10,
221
- thumbsDownValue: 0,
222
- lightweightTemplateOverride: `
223
- <div class="searchstax-lightweight-widget-container">
224
- <div class="searchstax-lightweight-widget-thumbs-up {{#thumbsUpActive}}active{{/thumbsUpActive}}">
225
- <svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
226
- <path d="M4.85079 7.59996L7.83141 1C8.4243 1 8.9929 1.23178 9.41213 1.64436C9.83136 2.05694 10.0669 2.61651 10.0669 3.19999V6.1333H14.2845C14.5005 6.13089 14.7145 6.17474 14.9116 6.26179C15.1087 6.34885 15.2842 6.47704 15.426 6.63747C15.5677 6.79791 15.6723 6.98676 15.7326 7.19094C15.7928 7.39513 15.8072 7.60975 15.7748 7.81996L14.7465 14.4199C14.6926 14.7696 14.5121 15.0884 14.2382 15.3175C13.9643 15.5466 13.6156 15.6706 13.2562 15.6666H4.85079M4.85079 7.59996V15.6666M4.85079 7.59996H2.61531C2.22006 7.59996 1.84099 7.75448 1.5615 8.02953C1.28201 8.30458 1.125 8.67763 1.125 9.06661V14.1999C1.125 14.5889 1.28201 14.9619 1.5615 15.237C1.84099 15.512 2.22006 15.6666 2.61531 15.6666H4.85079" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
227
- </svg>
228
- </div>
229
- <div class="searchstax-lightweight-widget-separator"></div>
230
- <div class="searchstax-lightweight-widget-thumbs-down {{#thumbsDownActive}}active{{/thumbsDownActive}}">
231
- <svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
232
- <path d="M12.1492 9.06801L9.16859 15.668C8.5757 15.668 8.0071 15.4362 7.58787 15.0236C7.16864 14.611 6.93311 14.0515 6.93311 13.468V10.5347H2.71552C2.4995 10.5371 2.28552 10.4932 2.08842 10.4062C1.89132 10.3191 1.71581 10.1909 1.57405 10.0305C1.43229 9.87006 1.32766 9.6812 1.26743 9.47702C1.2072 9.27284 1.19279 9.05822 1.22521 8.84801L2.25353 2.24806C2.30742 1.89833 2.48793 1.57955 2.76179 1.35046C3.03566 1.12136 3.38443 0.997398 3.74384 1.0014H12.1492M12.1492 9.06801V1.0014M12.1492 9.06801H14.3847C14.7799 9.06801 15.159 8.91349 15.4385 8.63844C15.718 8.36339 15.875 7.99034 15.875 7.60135V2.46805C15.875 2.07907 15.718 1.70602 15.4385 1.43097C15.159 1.15592 14.7799 1.0014 14.3847 1.0014H12.1492" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
233
- </svg>
234
-
235
- </div>
236
- </div>
237
- `,
238
- }
239
-
240
204
  <SearchstaxAnswerWidget
241
205
  searchAnswerTemplate={answerTemplate}
242
206
  showShowMoreAfterWordCount={100}
243
- feedbackwidget={feedbackConfig}
244
207
  ></SearchstaxAnswerWidget>
245
208
  ```
246
209
 
247
- example of answer widget initialization without feedback widget
248
- ```
210
+ ### Input Widget ###
249
211
 
250
- <SearchstaxAnswerWidget
251
- searchAnswerTemplate={answerTemplate}
252
- showShowMoreAfterWordCount={100}
253
- ></SearchstaxAnswerWidget>
254
- ```
212
+ SearchStax Site Search solution provides React and Next.js widgets to help you build your custom search page.
255
213
 
256
- ### Input Widget ###
214
+ The SearchstaxInputWidget component provides a search input with autosuggest/autocomplete functionality.
257
215
 
258
- example of input widget initialization with minimum options
216
+ **Usage**
259
217
  ```
260
218
  <SearchstaxInputWidget
219
+ suggestAfterMinChars={3}
261
220
  afterAutosuggest={afterAutosuggest}
262
221
  beforeAutosuggest={beforeAutosuggest}
263
222
  ></SearchstaxInputWidget>
264
223
  ```
265
224
 
225
+ **Props**
226
+
227
+ - suggestAfterMinChars - default 3. Number of characters needed for autosuggest to start triggering
228
+ - beforeAutosuggest - callback function that gets called before firing autosuggest. autosuggestProps are being passed as a property and can be modified, if passed along further search will execute with modified properties, if null is returned then event gets canceled and search never fires.
229
+ - afterAutosuggest - callback function that gets called after autosuggest has values but before rendering. It needs to return same type of data but it can be modified.
230
+ - inputTemplate - template override. look at examples below
266
231
 
267
- example of input widget initialization with template override
232
+
233
+ **inputWidgetTemplate**
234
+
235
+ The inputTemplate prop allows customizing the input UI.
236
+
237
+ It receives the following props:
238
+
239
+ - locationEnabled – boolean
240
+
241
+ **example**
268
242
  ```
269
- function InputTemplate(
270
- suggestions: ISearchstaxSuggestion[],
271
- onMouseLeave: () => void,
272
- onMouseOver: (suggestion: ISearchstaxSuggestion) => void,
273
- onMouseClick: () => void
243
+ const config = {
244
+ appId: "APP_ID",
245
+ relatedSearchesAPIKey: "KEY"
246
+ }
247
+
248
+ const locationWidgetConfig = {
249
+ locationDecode: (term: string): Promise<ISearchstaxLocation> => {
250
+ return new Promise((resolve) => {
251
+ // make a request to google geocoding API to retrieve lat, lon and address
252
+
253
+ const geocodingAPIKey = "GEOCODING_API_KEY";
254
+ const geocodingURL = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
255
+ term
256
+ )}&key=${geocodingAPIKey}`;
257
+ fetch(geocodingURL)
258
+ .then((response) => response.json())
259
+ .then((data) => {
260
+ if (data.status === "OK" && data.results.length > 0) {
261
+ const result = data.results[0];
262
+ const location = {
263
+ lat: result.geometry.location.lat,
264
+ lon: result.geometry.location.lng,
265
+ address: result.formatted_address,
266
+ };
267
+ resolve(location);
268
+ } else {
269
+ resolve({
270
+ address: undefined,
271
+ lat: undefined,
272
+ lon: undefined,
273
+ error: true,
274
+ });
275
+ }
276
+ })
277
+ .catch(() => {
278
+ resolve({
279
+ address: undefined,
280
+ lat: undefined,
281
+ lon: undefined,
282
+ error: true,
283
+ });
284
+ });
285
+ });
286
+ },
287
+ locationDecodeCoordinatesToAddress: (lat: string, lon: string): Promise<ISearchstaxLocation> => {
288
+ return new Promise((resolve) => {
289
+ fetch(
290
+ `https://geocoding-staging.searchstax.co/reverse?location=${lat},${lon}&components=country:US&app_id=${config.appId}`,
291
+ {
292
+ method: "GET",
293
+ headers: {
294
+ Authorization: `Token ${config.relatedSearchesAPIKey}`,
295
+ },
296
+ }
297
+ )
298
+ .then((response) => response.json())
299
+ .then((data) => {
300
+ if (data.status === "OK" && data.results.length > 0) {
301
+ const result = data.results[0];
302
+ resolve({
303
+ address: result.formatted_address,
304
+ lat: lat,
305
+ lon: lon,
306
+ error: false,
307
+ });
308
+ } else {
309
+ resolve({
310
+ address: undefined,
311
+ lat: lat,
312
+ lon: lon,
313
+ error: true,
314
+ });
315
+ }
316
+ })
317
+ .catch(() => {
318
+ resolve({
319
+ address: undefined,
320
+ lat: lat,
321
+ lon: lon,
322
+ error: true,
323
+ });
324
+ });
325
+ });
326
+ },
327
+ locationSearchEnabled: false,
328
+ locationValuesOverride: {
329
+ locationDistanceEnabled: true,
330
+ filterValues: ["any", "1000"],
331
+ filterUnit: "miles",
332
+ locationFilterDefaultValue: "any"
333
+ },
334
+ }
335
+ function LocationTemplate(
336
+ locationData: null | ISearchstaxLocationRenderData,
337
+ locationChange: (value: string) => void,
338
+ locationBlur: (value: string) => void,
339
+ radiusChange: (value: string | number) => void,
340
+ getCurrentLocation: () => void,
341
+ inputValue?: string | null,
342
+ selectValue?: string | number | undefined,
343
+ locationError?: boolean
274
344
  ): React.ReactElement {
275
345
  return (
276
- <div className="searchstax-search-input-wrapper">
346
+ <>
347
+ {locationData && (
348
+ <div
349
+ className="searchstax-location-input-container"
350
+ data-test-id="searchstax-location-input-container"
351
+ >
352
+ <div className="searchstax-location-input-wrapper">
353
+ <span className="searchstax-location-input-label">NEAR</span>
354
+ <div className="searchstax-location-input-wrapper-inner">
355
+ <input
356
+ type="text"
357
+ id="searchstax-location-input"
358
+ className={"searchstax-location-input" + (locationError ? " searchstax-input-location-error" : "")}
359
+ placeholder="Zip, Postal Code or City..."
360
+ aria-label="Search Location Input"
361
+ data-test-id="searchstax-location-input"
362
+ value={inputValue ?? ""}
363
+ onChange={(e) => locationChange(e.target.value)}
364
+ onBlur={(e) => locationBlur(e.target.value)}
365
+ />
366
+ <button onClick={getCurrentLocation} className="searchstax-get-current-location-button">Use my current location</button>
367
+ </div>
368
+ {locationData.shouldShowLocationDistanceDropdown && (
369
+ <span className="searchstax-location-input-label">WITHIN</span>
370
+ )}
371
+ {locationData.shouldShowLocationDistanceDropdown && (
372
+ <select
373
+ id="searchstax-location-radius-select"
374
+ className="searchstax-location-radius-select"
375
+ aria-label="Search Location Radius Select"
376
+ data-test-id="searchstax-location-radius-select"
377
+ onChange={(e) => radiusChange(e.target.value)}
378
+ value={selectValue}
379
+ >
380
+ {locationData.locationSearchDistanceValues.map(
381
+ ({ value, label }) => (
382
+ <option value={value} key={value}>
383
+ {label}
384
+ </option>
385
+ )
386
+ )}
387
+ </select>
388
+ )}
389
+ </div>
390
+ </div>
391
+ )}
392
+ </>)
393
+ }
394
+ function InputTemplate(
395
+ suggestions: ISearchstaxSuggestion[],
396
+ onMouseLeave: () => void,
397
+ onMouseOver: (suggestion: ISearchstaxSuggestion) => void,
398
+ onMouseClick: () => void
399
+ ): React.ReactElement {
400
+ return (
401
+ <>
402
+ <div className="searchstax-search-input-wrapper">
277
403
  <input
278
404
  type="text"
279
405
  id="searchstax-search-input"
@@ -295,6 +421,7 @@ example of input widget initialization with template override
295
421
  >
296
422
  <div
297
423
  className="searchstax-autosuggest-item-term-container"
424
+ tabIndex={0}
298
425
  dangerouslySetInnerHTML={{ __html: suggestion.term }}
299
426
  onMouseOver={() => {
300
427
  onMouseOver(suggestion);
@@ -307,16 +434,25 @@ example of input widget initialization with template override
307
434
  );
308
435
  })}
309
436
  </div>
310
-
311
- <button
312
- className="searchstax-spinner-icon"
313
- id="searchstax-search-input-action-button"
314
- aria-label="search"
315
- ></button>
316
437
  </div>
317
- );
318
- }
319
-
438
+ <SearchstaxLocationWidget
439
+ searchLocationTemplate={LocationTemplate}
440
+ hooks={{
441
+ locationDecode: locationWidgetConfig.locationDecode,
442
+ locationDecodeCoordinatesToAddress:
443
+ locationWidgetConfig.locationDecodeCoordinatesToAddress,
444
+ }}
445
+ locationSearchEnabled={locationWidgetConfig.locationSearchEnabled}
446
+ locationValuesOverride={locationWidgetConfig.locationValuesOverride}
447
+ />
448
+ <button
449
+ className="searchstax-spinner-icon"
450
+ id="searchstax-search-input-action-button"
451
+ aria-label="search"
452
+ ></button>
453
+ </>
454
+ );
455
+ }
320
456
 
321
457
  <SearchstaxInputWidget
322
458
  afterAutosuggest={afterAutosuggest}
@@ -324,38 +460,220 @@ example of input widget initialization with template override
324
460
  inputTemplate={InputTemplate}
325
461
  ></SearchstaxInputWidget>
326
462
  ```
327
-
328
463
  ### Location Widget ###
329
- example of location widget initialization with minimum options
464
+
465
+ SearchStax Site Search solution offers a React search-location widget to assist with your custom search page.
466
+
467
+ The SearchstaxLocationWidget provides a location search input with location-based search functionality.
468
+
469
+ **Usage**
470
+
330
471
  ```
331
- function locationDecodeFunction(term: string): Promise<ISearchstaxLocation>{
332
- return new Promise((resolve) => {
333
- // make a request to geocoding API to retrieve lat, lon and address and then resolve promise
334
- resolve({
335
- address:'Address',
336
- lat: lat,
337
- lon: lon,
338
- error: false
339
- });
340
- });
341
- }
342
- function locationDecodeCoordinatesToAddress(lat, lon): Promise<ISearchstaxLocation>{
472
+ const config = {
473
+ appId: "APP_ID",
474
+ relatedSearchesAPIKey: "KEY"
475
+ }
476
+
477
+ const locationWidgetConfig = {
478
+ locationDecode: (term: string): Promise<ISearchstaxLocation> => {
343
479
  return new Promise((resolve) => {
344
- // make a request to geocoding API to convert lat, lon to address and then resolve promise
345
- resolve({
346
- address:'Address',
347
- lat: lat,
348
- lon: lon,
349
- error: false
350
- });
480
+ // make a request to google geocoding API to retrieve lat, lon and address
481
+
482
+ const geocodingAPIKey = "GEOCODING_API_KEY";
483
+ const geocodingURL = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
484
+ term
485
+ )}&key=${geocodingAPIKey}`;
486
+ fetch(geocodingURL)
487
+ .then((response) => response.json())
488
+ .then((data) => {
489
+ if (data.status === "OK" && data.results.length > 0) {
490
+ const result = data.results[0];
491
+ const location = {
492
+ lat: result.geometry.location.lat,
493
+ lon: result.geometry.location.lng,
494
+ address: result.formatted_address,
495
+ };
496
+ resolve(location);
497
+ } else {
498
+ resolve({
499
+ address: undefined,
500
+ lat: undefined,
501
+ lon: undefined,
502
+ error: true,
503
+ });
504
+ }
505
+ })
506
+ .catch(() => {
507
+ resolve({
508
+ address: undefined,
509
+ lat: undefined,
510
+ lon: undefined,
511
+ error: true,
512
+ });
513
+ });
351
514
  });
352
- }
353
-
354
- <SearchstaxLocationWidget hooks={ {locationDecode: locationDecodeFunction, locationDecodeCoordinatesToAddress: locationDecodeCoordinatesToAddress} } />
515
+ },
516
+ locationDecodeCoordinatesToAddress: (lat: string, lon: string): Promise<ISearchstaxLocation> => {
517
+ return new Promise((resolve) => {
518
+ fetch(
519
+ `https://geocoding-staging.searchstax.co/reverse?location=${lat},${lon}&components=country:US&app_id=${config.appId}`,
520
+ {
521
+ method: "GET",
522
+ headers: {
523
+ Authorization: `Token ${config.relatedSearchesAPIKey}`,
524
+ },
525
+ }
526
+ )
527
+ .then((response) => response.json())
528
+ .then((data) => {
529
+ if (data.status === "OK" && data.results.length > 0) {
530
+ const result = data.results[0];
531
+ resolve({
532
+ address: result.formatted_address,
533
+ lat: lat,
534
+ lon: lon,
535
+ error: false,
536
+ });
537
+ } else {
538
+ resolve({
539
+ address: undefined,
540
+ lat: lat,
541
+ lon: lon,
542
+ error: true,
543
+ });
544
+ }
545
+ })
546
+ .catch(() => {
547
+ resolve({
548
+ address: undefined,
549
+ lat: lat,
550
+ lon: lon,
551
+ error: true,
552
+ });
553
+ });
554
+ });
555
+ },
556
+ locationSearchEnabled: false,
557
+ locationValuesOverride: {
558
+ locationDistanceEnabled: true,
559
+ filterValues: ["any", "1000"],
560
+ filterUnit: "miles",
561
+ locationFilterDefaultValue: "any"
562
+ },
563
+ }
564
+ <SearchstaxLocationWidget hooks={ {locationDecode: locationDecodeFunction, locationDecodeCoordinatesToAddress: locationDecodeCoordinatesToAddress} } />
355
565
  ```
356
566
 
357
- example of location widget initialization with template override
567
+ **Props**
568
+
569
+ - locationDecode - callback function to override location decoding
570
+ - locationDecodeCoordinatesToAddress - callback function to override location decoding
571
+
572
+ **Template Override**
573
+
574
+ The searchLocationTemplate prop allows customizing the location input UI.
575
+
576
+ It receives the following props:
577
+
578
+ - locationSearchDistanceValues – location distance values
579
+ - shouldShowLocationDistanceDropdown – boolean determining if distance dropdown should be shown
580
+
581
+
582
+ **Example**
583
+
358
584
  ```
585
+ const config = {
586
+ appId: "APP_ID",
587
+ relatedSearchesAPIKey: "KEY"
588
+ }
589
+
590
+ const locationWidgetConfig = {
591
+ locationDecode: (term: string): Promise<ISearchstaxLocation> => {
592
+ return new Promise((resolve) => {
593
+ // make a request to google geocoding API to retrieve lat, lon and address
594
+
595
+ const geocodingAPIKey = "GEOCODING_API_KEY";
596
+ const geocodingURL = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
597
+ term
598
+ )}&key=${geocodingAPIKey}`;
599
+ fetch(geocodingURL)
600
+ .then((response) => response.json())
601
+ .then((data) => {
602
+ if (data.status === "OK" && data.results.length > 0) {
603
+ const result = data.results[0];
604
+ const location = {
605
+ lat: result.geometry.location.lat,
606
+ lon: result.geometry.location.lng,
607
+ address: result.formatted_address,
608
+ };
609
+ resolve(location);
610
+ } else {
611
+ resolve({
612
+ address: undefined,
613
+ lat: undefined,
614
+ lon: undefined,
615
+ error: true,
616
+ });
617
+ }
618
+ })
619
+ .catch(() => {
620
+ resolve({
621
+ address: undefined,
622
+ lat: undefined,
623
+ lon: undefined,
624
+ error: true,
625
+ });
626
+ });
627
+ });
628
+ },
629
+ locationDecodeCoordinatesToAddress: (lat: string, lon: string): Promise<ISearchstaxLocation> => {
630
+ return new Promise((resolve) => {
631
+ fetch(
632
+ `https://geocoding-staging.searchstax.co/reverse?location=${lat},${lon}&components=country:US&app_id=${config.appId}`,
633
+ {
634
+ method: "GET",
635
+ headers: {
636
+ Authorization: `Token ${config.relatedSearchesAPIKey}`,
637
+ },
638
+ }
639
+ )
640
+ .then((response) => response.json())
641
+ .then((data) => {
642
+ if (data.status === "OK" && data.results.length > 0) {
643
+ const result = data.results[0];
644
+ resolve({
645
+ address: result.formatted_address,
646
+ lat: lat,
647
+ lon: lon,
648
+ error: false,
649
+ });
650
+ } else {
651
+ resolve({
652
+ address: undefined,
653
+ lat: lat,
654
+ lon: lon,
655
+ error: true,
656
+ });
657
+ }
658
+ })
659
+ .catch(() => {
660
+ resolve({
661
+ address: undefined,
662
+ lat: lat,
663
+ lon: lon,
664
+ error: true,
665
+ });
666
+ });
667
+ });
668
+ },
669
+ locationSearchEnabled: false,
670
+ locationValuesOverride: {
671
+ locationDistanceEnabled: true,
672
+ filterValues: ["any", "1000"],
673
+ filterUnit: "miles",
674
+ locationFilterDefaultValue: "any"
675
+ },
676
+ }
359
677
  function LocationTemplate(
360
678
  locationData: null | ISearchstaxLocationRenderData,
361
679
  locationChange: (value: string) => void,
@@ -420,7 +738,12 @@ function LocationTemplate(
420
738
 
421
739
  ### Result Widget ###
422
740
 
423
- example of results widget initialization with minimum options
741
+ The SearchStax Site Search solution provides a React and Next.js results widget to support your custom search page.
742
+
743
+ The SearchstaxResultsWidget displays the search results.
744
+
745
+ **Usage**
746
+
424
747
  ```
425
748
  <SearchstaxResultWidget
426
749
  afterLinkClick={afterLinkClick}
@@ -428,62 +751,121 @@ example of results widget initialization with minimum options
428
751
  ></SearchstaxResultWidget>
429
752
  ```
430
753
 
431
- example of results widget initialization with minimum options for infinite scroll behavior
754
+ **Result Template Override**
755
+
756
+ The resultsTemplate prop allows customizing the result UI.
757
+
758
+ It receives no props:
759
+ - custom?: any - custom properties that may be added through aftersearchHook
760
+ - ribbon: string | null
761
+ - paths: string | null;
762
+ - url: string | null;
763
+ - title: string | null;
764
+ - titleTracking: string | null;
765
+ - promoted: boolean | null;
766
+ - thumbnail: string | null;
767
+ - date: string | null;
768
+ - snippet: string | null;
769
+ - description: string | null;
770
+ - uniqueId: string;
771
+ - position: number;
772
+ - distance: number | null;
773
+ - unit: string | null;
774
+ - unmappedFields: {
775
+ key: string;
776
+ value: string | string[] | boolean;
777
+ isImage?: boolean;
778
+ }[] - fields that were not mapped.
779
+ - allFields: { key: string; value: string | string[] | boolean }[] - all fields
780
+
781
+
782
+ **No Results Template Override**
783
+
784
+ The noResultTemplate prop allows customizing the result UI.
785
+
786
+ It receives following props:
787
+
788
+ - spellingSuggestion - suggestion of corrected spelling
789
+ - searchExecuted - boolean if search was executed
790
+ - searchTerm - term that was searched
791
+
792
+ **Example of default render method**
432
793
  ```
433
- <SearchstaxResultWidget
434
- afterLinkClick={afterLinkClick}
435
- renderMethod={'infiniteScroll'}
436
- ></SearchstaxResultWidget>
794
+
437
795
  ```
438
796
 
439
- example of result widget initialization with template override
797
+ **Example of infinite scroll and pagination render methods**
440
798
  ```
441
- function noResultTemplate(searchTerm: string, metaData: ISearchstaxSearchMetadata | null, executeSearch: (term:string) => void): React.ReactElement {
442
- return (
443
- <div>
444
- <div className="searchstax-no-results">
445
- {" "}
446
- Showing <strong>no results</strong> for <strong>"{searchTerm}"</strong>
447
- <br />
448
- {metaData?.spellingSuggestion && (
449
- <span>
450
- &nbsp;Did you mean{" "}
451
- <a href="#" className="searchstax-suggestion-term" onClick={(e) => {
452
- e.preventDefault();
453
- executeSearch(metaData?.spellingSuggestion);
454
- }}>
455
- {metaData?.spellingSuggestion}
456
- </a>
457
- ?
458
- </span>
459
- )}
460
- </div>
461
- <ul>
462
- <li>
463
- {" "}
464
- Try searching for search related terms or topics. We offer a wide variety of content to help you get the
465
- information you need.{" "}
466
- </li>
467
- <li>Lost? Click on the ‘X” in the Search Box to reset your search.</li>
468
- </ul>
799
+ function noResultTemplate(
800
+ searchTerm: string,
801
+ metaData: ISearchstaxSearchMetadata | null,
802
+ executeSearch: (searchTerm: string) => void
803
+ ): React.ReactElement {
804
+ return (
805
+ <div>
806
+ <div className="searchstax-no-results">
807
+ {" "}
808
+ Showing <strong>no results</strong> for <strong>"{searchTerm}"</strong>
809
+ <br />
810
+ {metaData?.spellingSuggestion && (
811
+ <span>
812
+ &nbsp;Did you mean{" "}
813
+ <a
814
+ href="#"
815
+ aria-label={`Did you mean ${metaData?.spellingSuggestion}`}
816
+ className="searchstax-suggestion-term"
817
+ onClick={(e) => {
818
+ e.preventDefault();
819
+ executeSearch(metaData?.spellingSuggestion);
820
+ }}
821
+ >
822
+ {metaData?.spellingSuggestion}
823
+ </a>
824
+ ?
825
+ </span>
826
+ )}
469
827
  </div>
470
- );
471
- }
828
+ <ul>
829
+ <li>
830
+ {" "}
831
+ Try searching for search related terms or topics. We offer a wide
832
+ variety of content to help you get the information you need.{" "}
833
+ </li>
834
+ <li>Lost? Click on the ‘X” in the Search Box to reset your search.</li>
835
+ </ul>
836
+ </div>
837
+ );
838
+ }
472
839
 
473
- function resultsTemplate(
474
- searchResults: ISearchstaxParsedResult[],
475
- resultClicked: (results: ISearchstaxParsedResult, event: any) => ISearchstaxParsedResult[]
476
- ): React.ReactElement {
477
- return (
478
- <>
479
- {searchResults && searchResults.length && (
480
- <div className="searchstax-search-results">
481
- {searchResults !== null &&
482
- searchResults.map(function (searchResult) {
483
- return (
484
- <a href={searchResult.url ?? ''} key={searchResult.uniqueId} onClick={event => {
840
+ function resultsTemplate(
841
+ searchResults: ISearchstaxParsedResult[],
842
+ resultClicked: (
843
+ results: ISearchstaxParsedResult,
844
+ event: any
845
+ ) => void
846
+ ): React.ReactElement {
847
+ return (
848
+ <>
849
+ {searchResults && searchResults.length && (
850
+ <div className="searchstax-search-results" aria-live="polite">
851
+ {searchResults !== null &&
852
+ searchResults.map(function (searchResult) {
853
+ return (
854
+ <a
855
+ href={searchResult.url ?? ""}
856
+ onClick={(event) => {
485
857
  resultClicked(searchResult, event);
486
- }} tabIndex={0} data-searchstax-unique-result-id={ searchResult.uniqueId} className="searchstax-result-item-link searchstax-result-item-link-wrapping">
858
+ }}
859
+ onKeyDown={(e) => {
860
+ if (e.key === "Enter" || e.key === " ") {
861
+ resultClicked(searchResult, e);
862
+ }
863
+ }}
864
+ data-searchstax-unique-result-id={searchResult.uniqueId}
865
+ key={searchResult.uniqueId}
866
+ className="searchstax-result-item-link searchstax-result-item-link-wrapping"
867
+ tabIndex={0}
868
+ >
487
869
  <div
488
870
  className={`searchstax-search-result ${
489
871
  searchResult.thumbnail ? "has-thumbnail" : ""
@@ -494,36 +876,46 @@ function noResultTemplate(searchTerm: string, metaData: ISearchstaxSearchMetadat
494
876
  <div className="searchstax-search-result-promoted"></div>
495
877
  )}
496
878
 
497
-
498
879
  {searchResult.ribbon && (
499
- <div className="searchstax-search-result-ribbon">
500
- {searchResult.ribbon}
501
- </div>
880
+ <div
881
+ className="searchstax-search-result-ribbon"
882
+ dangerouslySetInnerHTML={{
883
+ __html: searchResult.ribbon,
884
+ }}
885
+ ></div>
502
886
  )}
503
887
 
504
888
  {searchResult.thumbnail && (
505
889
  <img
890
+ alt=""
506
891
  src={searchResult.thumbnail}
507
892
  className="searchstax-thumbnail"
508
893
  />
509
894
  )}
510
895
 
511
896
  <div className="searchstax-search-result-title-container">
512
- <span className="searchstax-search-result-title">
513
- {searchResult.title}
514
- </span>
897
+ <span
898
+ className="searchstax-search-result-title"
899
+ dangerouslySetInnerHTML={{
900
+ __html: searchResult.title ?? "",
901
+ }}
902
+ ></span>
515
903
  </div>
516
904
 
517
905
  {searchResult.paths && (
518
- <p className="searchstax-search-result-common">
519
- {searchResult.paths}
520
- </p>
906
+ <p
907
+ className="searchstax-search-result-common"
908
+ dangerouslySetInnerHTML={{ __html: searchResult.paths }}
909
+ ></p>
521
910
  )}
522
911
 
523
912
  {searchResult.description && (
524
- <p className="searchstax-search-result-description searchstax-search-result-common">
525
- {searchResult.description}
526
- </p>
913
+ <p
914
+ className="searchstax-search-result-description searchstax-search-result-common"
915
+ dangerouslySetInnerHTML={{
916
+ __html: searchResult.description,
917
+ }}
918
+ ></p>
527
919
  )}
528
920
 
529
921
  {searchResult.unmappedFields.map(function (
@@ -535,6 +927,7 @@ function noResultTemplate(searchTerm: string, metaData: ISearchstaxSearchMetadat
535
927
  typeof unmappedField.value === "string" && (
536
928
  <div className="searchstax-search-result-image-container">
537
929
  <img
930
+ alt=""
538
931
  src={unmappedField.value}
539
932
  className="searchstax-result-image"
540
933
  />
@@ -543,62 +936,103 @@ function noResultTemplate(searchTerm: string, metaData: ISearchstaxSearchMetadat
543
936
 
544
937
  {!unmappedField.isImage && (
545
938
  <div>
546
- <p className="searchstax-search-result-common">
547
- {unmappedField.value}
548
- </p>
939
+ <p
940
+ className="searchstax-search-result-common"
941
+ dangerouslySetInnerHTML={{
942
+ __html: unmappedField.value,
943
+ }}
944
+ ></p>
549
945
  </div>
550
946
  )}
551
947
  </div>
552
948
  );
553
949
  })}
554
950
  </div>
555
- </a>
556
- );
557
- })}
558
- </div>
559
- )}
560
- </>
561
- );
562
- }
563
-
951
+ </a>
952
+ );
953
+ })}
954
+ </div>
955
+ )}
956
+ </>
957
+ );
958
+ }
959
+ <SearchstaxResultWidget
960
+ afterLinkClick={afterLinkClick}
961
+ noResultTemplate={noResultTemplate}
962
+ resultsTemplate={resultsTemplate}
963
+ renderMethod={'pagination'}
964
+ ></SearchstaxResultWidget>
965
+ // Infinite scroll
564
966
 
565
967
  <SearchstaxResultWidget
566
968
  afterLinkClick={afterLinkClick}
567
969
  noResultTemplate={noResultTemplate}
568
970
  resultsTemplate={resultsTemplate}
971
+ renderMethod={'infiniteScroll'}
569
972
  ></SearchstaxResultWidget>
973
+
570
974
  ```
571
975
 
572
976
  ### Pagination Widget ###
573
977
 
978
+ The SearchStax Site Search solution offers a pagination widget for React and Next.js search pages.
979
+
980
+ The SearchstaxPaginationWidget displays pagination controls for search results.
981
+
982
+ **Usage**
574
983
 
575
- example of pagination widget initialization with minimum options
576
984
  ```
577
985
  <SearchstaxPaginationWidget></SearchstaxPaginationWidget>
578
986
  ```
579
987
 
580
- example of pagination widget initialization with various options
988
+ **Main Template Override**
989
+
990
+ Main template for the pagination controls.
991
+
992
+ It receives following props:
993
+
994
+ - nextPageLink - link of next page;
995
+ - previousPageLink - link of previous page;
996
+
997
+
998
+ **Infinite Scroll Template Override**
999
+
1000
+ Main template for the pagination controls in infinite scroll mode.
1001
+
1002
+ It receives following props:
1003
+ - isLastPage - boolean, true if its last page
1004
+ - results - results.length can be used if there are results
1005
+
1006
+ **Example**
1007
+
581
1008
  ```
582
1009
  function paginationTemplate(
583
1010
  paginationData: IPaginationData | null,
584
1011
  nextPage: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void,
585
- previousPage: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void
1012
+ previousPage: (
1013
+ event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
1014
+ ) => void
586
1015
  ) {
587
1016
  return (
588
- <>
1017
+ <>
589
1018
  {paginationData && paginationData?.totalResults !== 0 && (
590
1019
  <div className="searchstax-pagination-container">
591
1020
  <div className="searchstax-pagination-content">
592
1021
  <a
593
- className="searchstax-pagination-previous"
1022
+ className={`searchstax-pagination-previous ${paginationData.isFirstPage ? "disabled" : ""}`}
1023
+ tabIndex={0}
594
1024
  style={
595
1025
  paginationData?.isFirstPage ? { pointerEvents: "none" } : {}
596
1026
  }
597
1027
  onClick={(e) => {
598
1028
  previousPage(e);
599
1029
  }}
600
- tabIndex={0}
601
- id="searchstax-pagination-previous"
1030
+ onKeyDown={(e) => {
1031
+ if(e.key === 'Enter' || e.key === ' ') {
1032
+ previousPage(e as any);
1033
+ }
1034
+ }}
1035
+ id="searchstax-pagination-previous"
602
1036
  >
603
1037
  {" "}
604
1038
  &lt; Previous{" "}
@@ -610,14 +1044,19 @@ function paginationTemplate(
610
1044
  {paginationData?.totalResults}
611
1045
  </div>
612
1046
  <a
613
- className="searchstax-pagination-next"
1047
+ className={`searchstax-pagination-next ${paginationData.isLastPage ? "disabled" : ""}`}
1048
+ tabIndex={0}
614
1049
  style={
615
1050
  paginationData?.isLastPage ? { pointerEvents: "none" } : {}
616
1051
  }
617
1052
  onClick={(e) => {
618
1053
  nextPage(e);
619
1054
  }}
620
- tabIndex={0}
1055
+ onKeyDown={(e) => {
1056
+ if(e.key === 'Enter' || e.key === ' ') {
1057
+ nextPage(e as any);
1058
+ }
1059
+ }}
621
1060
  id="searchstax-pagination-next"
622
1061
  >
623
1062
  Next &gt;
@@ -629,12 +1068,7 @@ function paginationTemplate(
629
1068
  );
630
1069
  }
631
1070
 
632
- <SearchstaxPaginationWidget paginationTemplate={paginationTemplate}></SearchstaxPaginationWidget>
633
- ```
634
-
635
- example of pagination widget for infinite scroll
636
- ```
637
- function infiniteScrollTemplate(
1071
+ function infiniteScrollTemplate(
638
1072
  paginationData: IPaginationData | null,
639
1073
  nextPage: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void
640
1074
  ) {
@@ -647,6 +1081,7 @@ example of pagination widget for infinite scroll
647
1081
  <div className="searchstax-pagination-container">
648
1082
  <a
649
1083
  className="searchstax-pagination-load-more"
1084
+ tabIndex={0}
650
1085
  onClick={(e) => {
651
1086
  nextPage(e);
652
1087
  }}
@@ -663,13 +1098,21 @@ example of pagination widget for infinite scroll
663
1098
  </>
664
1099
  );
665
1100
  }
666
-
667
- <SearchstaxPaginationWidget infiniteScrollTemplate={infiniteScrollTemplate}></SearchstaxPaginationWidget>
1101
+ <SearchstaxPaginationWidget paginationTemplate={paginationTemplate}></SearchstaxPaginationWidget>
1102
+ // example of pagination widget for infinite scroll
1103
+ <SearchstaxPaginationWidget infiniteScrollTemplate={infiniteScrollTemplate}></SearchstaxPaginationWidget>
668
1104
  ```
669
1105
 
670
1106
  ### Facets Widget ###
671
1107
 
672
- example of facets widget initialization with minimum options
1108
+ The SearchStax Site Search solution offers a SearchstaxFacetsWidget component for React and Next.js. This widget displays the search facets.
1109
+
1110
+ **Facet Selection and Order**
1111
+
1112
+ Facet lists are configured and ordered on the Site Search [Faceting Tab](https://www.searchstax.com/docs/searchstudio/faceting-tab/).
1113
+
1114
+ **Usage**
1115
+
673
1116
  ```
674
1117
  <SearchstaxFacetsWidget
675
1118
  facetingType="and"
@@ -679,362 +1122,395 @@ example of facets widget initialization with minimum options
679
1122
  ></SearchstaxFacetsWidget>
680
1123
  ```
681
1124
 
1125
+ **Props**
1126
+
1127
+ - facetingType: "and" | "or" | "showUnavailable" | "tabs"; // type that determines how facets will behave
1128
+ - specificFacets?: string[]; // optional array of facet names that if provided will only render those facets
1129
+ - itemsPerPageDesktop: number; // default expanded facets for desktop
1130
+ - itemsPerPageMobile: number; // default expanded facets for mobile
1131
+ - templates - see examples below
1132
+
1133
+
1134
+ **Main Template Desktop Override**
1135
+
1136
+ Main wrapper template for desktop facets display.
1137
+
1138
+ It receives following props from [IFacetsTemplateData](https://www.searchstax.com/docs/searchstudio/interfaces/#h-facets-interfaces)
1139
+
682
1140
 
683
- example of facets widget initialization with template overrides
1141
+ **Main Template Mobile Override**
1142
+
1143
+ Main wrapper template for mobile facets display.
1144
+
1145
+ It receives following props from [IFacetsTemplateData](https://www.searchstax.com/docs/searchstudio/interfaces/#h-facets-interfaces)
1146
+
1147
+
1148
+ **Example**
684
1149
  ```
685
- function facetsTemplateDesktop(
686
- facetsTemplateDataDesktop: IFacetsTemplateData | null,
687
- facetContainers: {
688
- [key: string]: React.LegacyRef<HTMLDivElement> | undefined;
689
- },
690
- isNotDeactivated: (name: string) => boolean,
691
- toggleFacetGroup: (name: string) => void,
692
- selectFacet: (
693
- index: string,
694
- event: React.MouseEvent<HTMLDivElement, MouseEvent>,
695
- data: IFacetValueData,
696
- isInput: boolean
697
- ) => void,
698
- isChecked: (facetValue: IFacetValueData) => boolean | undefined,
699
- showMoreLessDesktop: (
700
- e: React.MouseEvent<HTMLDivElement, MouseEvent>,
701
- data: IFacetData
702
- ) => void
703
- ) {
704
- return (
705
- <div className="searchstax-facets-container-desktop">
706
- {facetsTemplateDataDesktop?.facets.map((facet) => {
707
- return (
708
- <div
709
- className={`searchstax-facet-container ${
710
- isNotDeactivated(facet.name) ? "active" : ""
711
- }`}
712
- key={facet.name + "desktop"}
713
- >
714
- <div>
715
- <div
716
- className="searchstax-facet-title-container"
717
- onClick={() => {
718
- toggleFacetGroup(facet.name);
719
- }}
720
- >
721
- <div className="searchstax-facet-title"> {facet.label}</div>
722
- <div className="searchstax-facet-title-arrow active"></div>
1150
+ function facetsTemplateDesktop(
1151
+ facetsTemplateDataDesktop: IFacetsTemplateData | null,
1152
+ facetContainers: {
1153
+ [key: string]: React.LegacyRef<HTMLDivElement> | undefined;
1154
+ },
1155
+ isNotDeactivated: (name: string) => boolean,
1156
+ toggleFacetGroup: (name: string) => void,
1157
+ selectFacet: (
1158
+ index: string,
1159
+ event: React.MouseEvent<HTMLDivElement, MouseEvent>,
1160
+ data: IFacetValueData,
1161
+ isInput: boolean
1162
+ ) => void,
1163
+ isChecked: (facetValue: IFacetValueData) => boolean | undefined,
1164
+ showMoreLessDesktop: (
1165
+ e: React.MouseEvent<HTMLDivElement, MouseEvent>,
1166
+ data: IFacetData
1167
+ ) => void
1168
+ ) {
1169
+ return (
1170
+ <div className="searchstax-facets-container-desktop">
1171
+ {facetsTemplateDataDesktop?.facets.map((facet) => {
1172
+ return (
1173
+ <div
1174
+ className={`searchstax-facet-container ${
1175
+ isNotDeactivated(facet.name) ? "active" : ""
1176
+ }`}
1177
+ key={facet.name + "desktop"}
1178
+ >
1179
+ <div>
1180
+ <div
1181
+ className="searchstax-facet-title-container"
1182
+ onClick={() => {
1183
+ toggleFacetGroup(facet.name);
1184
+ }}
1185
+ >
1186
+ <div className="searchstax-facet-title">
1187
+ {" "}
1188
+ {facet.label}
723
1189
  </div>
724
- <div className="searchstax-facet-values-container">
725
- {facet.values.map(
726
- //@ts-ignore
727
- (facetValue: IFacetValueData, key) => {
728
- facetContainers[key + facet.name] = createRef();
729
- return (
1190
+ <div className="searchstax-facet-title-arrow active"></div>
1191
+ </div>
1192
+ <div className="searchstax-facet-values-container" aria-live="polite">
1193
+ {facet.values.map(
1194
+ //@ts-ignore
1195
+ (facetValue: IFacetValueData, key) => {
1196
+ facetContainers[key + facet.name] = createRef();
1197
+ return (
1198
+ <div
1199
+ key={facetValue.value + facetValue.parentName}
1200
+ className={`searchstax-facet-value-container ${
1201
+ facetValue.disabled
1202
+ ? "searchstax-facet-value-disabled"
1203
+ : ""
1204
+ }`}
1205
+ ref={facetContainers[key + facet.name]}
1206
+ >
730
1207
  <div
731
- key={facetValue.value + facetValue.parentName}
732
- className={`searchstax-facet-value-container ${
733
- facetValue.disabled
734
- ? "searchstax-facet-value-disabled"
735
- : ""
736
- }`}
737
- ref={facetContainers[key + facet.name]}
1208
+ className={
1209
+ "searchstax-facet-input" +
1210
+ " desktop-" +
1211
+ key +
1212
+ facet.name
1213
+ }
738
1214
  >
739
- <div className={
740
- "searchstax-facet-input" +
741
- " desktop-" +
742
- key +
743
- facet.name
744
- }>
745
- <input
746
- type="checkbox"
747
- className="searchstax-facet-input-checkbox"
748
- checked={isChecked(facetValue)}
749
- readOnly={true}
750
- aria-label={facetValue.value + ' ' + facetValue.count}
751
- disabled={facetValue.disabled}
752
- onClick={(e) => {
753
- selectFacet(
754
- key + facet.name,
755
- e,
756
- facetValue,
757
- true
758
- );
759
- }}
760
- />
761
- </div>
762
- <div
763
- className="searchstax-facet-value-label"
764
- onClick={(e) => {
765
- selectFacet(
766
- key + facet.name,
767
- e,
768
- facetValue,
769
- false
770
- );
771
- }}
772
- >
773
- {facetValue.value}
774
- </div>
775
- <div
776
- className="searchstax-facet-value-count"
1215
+ <input
1216
+ type="checkbox"
1217
+ className="searchstax-facet-input-checkbox"
1218
+ checked={isChecked(facetValue)}
1219
+ readOnly={true}
1220
+ aria-label={facetValue.value + ' ' + facetValue.count}
1221
+ disabled={facetValue.disabled}
777
1222
  onClick={(e) => {
778
1223
  selectFacet(
779
1224
  key + facet.name,
780
1225
  e,
781
1226
  facetValue,
782
- false
1227
+ true
783
1228
  );
784
1229
  }}
785
- >
786
- ({facetValue.count})
787
- </div>
1230
+ />
1231
+ </div>
1232
+ <div
1233
+ className="searchstax-facet-value-label"
1234
+ onClick={(e) => {
1235
+ selectFacet(key + facet.name, e, facetValue, false);
1236
+ }}
1237
+ >
1238
+ {facetValue.value}
1239
+ </div>
1240
+ <div
1241
+ className="searchstax-facet-value-count"
1242
+ onClick={(e) => {
1243
+ selectFacet(key + facet.name, e, facetValue, false);
1244
+ }}
1245
+ >
1246
+ ({facetValue.count})
788
1247
  </div>
789
- );
790
- }
791
- )}
792
-
793
- {facet.hasMoreFacets && (
794
- <div className="searchstax-facet-show-more-container">
795
- <div
796
- className="searchstax-facet-show-more-container"
797
- onClick={(e) => {
798
- showMoreLessDesktop(e, facet);
799
- }}
800
- >
801
- {facet.showingAllFacets && (
802
- <div className="searchstax-facet-show-less-button searchstax-facet-show-button">
803
- less
804
- </div>
805
- )}
806
- {!facet.showingAllFacets && (
807
- <div className="searchstax-facet-show-more-button searchstax-facet-show-button">
808
- more
809
- </div>
810
- )}
811
1248
  </div>
1249
+ );
1250
+ }
1251
+ )}
1252
+
1253
+ {facet.hasMoreFacets && (
1254
+ <div className="searchstax-facet-show-more-container">
1255
+ <div
1256
+ className="searchstax-facet-show-more-container"
1257
+ onClick={(e) => {
1258
+ showMoreLessDesktop(e, facet);
1259
+ }}
1260
+ onKeyDown={(e) => {
1261
+ if(e.key === 'Enter' || e.key === ' ') {
1262
+ showMoreLessDesktop(e as any, facet);
1263
+ }
1264
+ }}
1265
+ tabIndex={0}
1266
+ >
1267
+ {facet.showingAllFacets && (
1268
+ <div className="searchstax-facet-show-less-button searchstax-facet-show-button">
1269
+ less
1270
+ </div>
1271
+ )}
1272
+ {!facet.showingAllFacets && (
1273
+ <div className="searchstax-facet-show-more-button searchstax-facet-show-button">
1274
+ more
1275
+ </div>
1276
+ )}
812
1277
  </div>
813
- )}
814
- </div>
1278
+ </div>
1279
+ )}
815
1280
  </div>
816
1281
  </div>
817
- );
818
- })}
819
- </div>
820
- );
821
- }
822
-
823
- function facetsTemplateMobile(
824
- facetsTemplateDataMobile: IFacetsTemplateData | null,
825
- selectedFacetsCheckboxes: IFacetValue[],
826
- facetContainers: {
827
- [key: string]: React.LegacyRef<HTMLDivElement> | undefined;
828
- },
829
- isNotDeactivated: (name: string) => boolean,
830
- toggleFacetGroup: (name: string) => void,
831
- selectFacet: (
832
- index: string,
833
- event: React.MouseEvent<HTMLDivElement, MouseEvent>,
834
- data: IFacetValueData,
835
- isInput: boolean
836
- ) => void,
837
- isChecked: (facetValue: IFacetValueData) => boolean | undefined,
838
- unselectFacet: (facet: IFacetValue) => void,
839
- showMoreLessMobile: (
840
- e: React.MouseEvent<HTMLDivElement, MouseEvent>,
841
- data: IFacetData
842
- ) => void,
843
- openOverlay: () => void,
844
- closeOverlay: () => void,
845
- unselectAll: () => void
846
- ) {
847
- return facetsTemplateDataMobile ? (
848
- <div className="searchstax-facets-container-mobile">
849
- <div className="searchstax-facets-pills-container">
850
- <div
851
- className="searchstax-facets-pill searchstax-facets-pill-filter-by"
852
- onClick={() => {
853
- openOverlay();
854
- }}
855
- >
856
- <div className="searchstax-facets-pill-label">Filter By</div>
857
1282
  </div>
858
- <div className="searchstax-facets-pills-selected">
859
- {selectedFacetsCheckboxes.map((facet) => {
860
- return (
861
- <div
862
- className="searchstax-facets-pill searchstax-facets-pill-facets"
863
- key={facet.value}
864
- onClick={() => {
865
- unselectFacet(facet);
866
- }}
867
- >
868
- <div className="searchstax-facets-pill-label">
869
- {facet.value} ({facet.count})
870
- </div>
871
- <div className="searchstax-facets-pill-icon-close"></div>
872
- </div>
873
- );
874
- })}
875
- {selectedFacetsCheckboxes.length !== 0 && (
1283
+ );
1284
+ })}
1285
+ </div>
1286
+ );
1287
+ }
1288
+
1289
+ function facetsTemplateMobile(
1290
+ facetsTemplateDataMobile: IFacetsTemplateData | null,
1291
+ selectedFacetsCheckboxes: IFacetValue[],
1292
+ facetContainers: {
1293
+ [key: string]: React.LegacyRef<HTMLDivElement> | undefined;
1294
+ },
1295
+ isNotDeactivated: (name: string) => boolean,
1296
+ toggleFacetGroup: (name: string) => void,
1297
+ selectFacet: (
1298
+ index: string,
1299
+ event: React.MouseEvent<HTMLDivElement, MouseEvent>,
1300
+ data: IFacetValueData,
1301
+ isInput: boolean,
1302
+ isMobile?: boolean
1303
+ ) => void,
1304
+ isChecked: (facetValue: IFacetValueData) => boolean | undefined,
1305
+ unselectFacet: (facet: IFacetValue) => void,
1306
+ showMoreLessMobile: (
1307
+ e: React.MouseEvent<HTMLDivElement, MouseEvent>,
1308
+ data: IFacetData
1309
+ ) => void,
1310
+ openOverlay: () => void,
1311
+ closeOverlay: () => void,
1312
+ unselectAll: () => void
1313
+ ) {
1314
+ return facetsTemplateDataMobile ? (
1315
+ <div className="searchstax-facets-container-mobile">
1316
+ <div className="searchstax-facets-pills-container">
1317
+ <div
1318
+ className="searchstax-facets-pill searchstax-facets-pill-filter-by"
1319
+ onClick={() => {
1320
+ openOverlay();
1321
+ }}
1322
+ >
1323
+ <div className="searchstax-facets-pill-label">Filter By</div>
1324
+ </div>
1325
+ <div className="searchstax-facets-pills-selected">
1326
+ {selectedFacetsCheckboxes.map((facet) => {
1327
+ return (
876
1328
  <div
877
- className="searchstax-facets-pill searchstax-clear-filters searchstax-facets-pill-clear-all"
1329
+ className="searchstax-facets-pill searchstax-facets-pill-facets"
1330
+ key={facet.value}
878
1331
  onClick={() => {
879
- unselectAll();
1332
+ unselectFacet(facet);
880
1333
  }}
881
1334
  >
882
1335
  <div className="searchstax-facets-pill-label">
883
- Clear Filters
1336
+ {facet.value} ({facet.count})
884
1337
  </div>
1338
+ <div className="searchstax-facets-pill-icon-close"></div>
885
1339
  </div>
886
- )}
887
- </div>
888
- <div
889
- className={`searchstax-facets-mobile-overlay ${
890
- //@ts-ignore
891
- facetsTemplateDataMobile.overlayOpened ? "searchstax-show" : ""
892
- }`}
893
- >
894
- <div className="searchstax-facets-mobile-overlay-header">
895
- <div className="searchstax-facets-mobile-overlay-header-title">
896
- Filter By
897
- </div>
898
- <div
899
- className="searchstax-search-close"
900
- onClick={() => {
901
- closeOverlay();
902
- }}
903
- ></div>
1340
+ );
1341
+ })}
1342
+ {selectedFacetsCheckboxes.length !== 0 && (
1343
+ <div
1344
+ className="searchstax-facets-pill searchstax-clear-filters searchstax-facets-pill-clear-all"
1345
+ onClick={() => {
1346
+ unselectAll();
1347
+ }}
1348
+ >
1349
+ <div className="searchstax-facets-pill-label">Clear Filters</div>
904
1350
  </div>
905
- <div className="searchstax-facets-container-mobile">
906
- {facetsTemplateDataMobile?.facets.map((facet) => {
907
- return (
908
- <div
909
- key={facet.name + "mobile"}
910
- className={`searchstax-facet-container ${
911
- isNotDeactivated(facet.name) ? `active` : ``
912
- }`}
913
- >
914
- <div>
915
- <div
916
- className="searchstax-facet-title-container"
917
- onClick={() => {
918
- toggleFacetGroup(facet.name);
919
- }}
920
- >
921
- <div className="searchstax-facet-title">
922
- {" "}
923
- {facet.label}{" "}
924
- </div>
925
- <div className="searchstax-facet-title-arrow active"></div>
1351
+ )}
1352
+ </div>
1353
+ <div
1354
+ className={`searchstax-facets-mobile-overlay ${
1355
+ //@ts-ignore
1356
+ facetsTemplateDataMobile.overlayOpened ? "searchstax-show" : ""
1357
+ }`}
1358
+ >
1359
+ <div className="searchstax-facets-mobile-overlay-header">
1360
+ <div className="searchstax-facets-mobile-overlay-header-title">
1361
+ Filter By
1362
+ </div>
1363
+ <div
1364
+ className="searchstax-search-close"
1365
+ onClick={() => {
1366
+ closeOverlay();
1367
+ }}
1368
+ ></div>
1369
+ </div>
1370
+ <div className="searchstax-facets-container-mobile">
1371
+ {facetsTemplateDataMobile?.facets.map((facet) => {
1372
+ return (
1373
+ <div
1374
+ key={facet.name + "mobile"}
1375
+ className={`searchstax-facet-container ${
1376
+ isNotDeactivated(facet.name) ? `active` : ``
1377
+ }`}
1378
+ >
1379
+ <div>
1380
+ <div
1381
+ className="searchstax-facet-title-container"
1382
+ onClick={() => {
1383
+ toggleFacetGroup(facet.name);
1384
+ }}
1385
+ >
1386
+ <div className="searchstax-facet-title">
1387
+ {" "}
1388
+ {facet.label}{" "}
926
1389
  </div>
927
- <div className="searchstax-facet-values-container">
928
- {facet.values.map(
929
- //@ts-ignore
930
- (facetValue: IFacetValueData, key) => {
931
- facetContainers[key + facet.name] = createRef();
932
-
933
- return (
1390
+ <div className="searchstax-facet-title-arrow active"></div>
1391
+ </div>
1392
+ <div className="searchstax-facet-values-container" aria-live="polite">
1393
+ {facet.values.map(
1394
+ //@ts-ignore
1395
+ (facetValue: IFacetValueData, key) => {
1396
+ facetContainers[key + facet.name] = createRef();
1397
+
1398
+ return (
1399
+ <div
1400
+ key={facetValue.value + facetValue.parentName}
1401
+ className={`searchstax-facet-value-container ${
1402
+ facetValue.disabled
1403
+ ? `searchstax-facet-value-disabled`
1404
+ : ``
1405
+ }`}
1406
+ ref={facetContainers[key + facet.name]}
1407
+ >
934
1408
  <div
935
- key={facetValue.value + facetValue.parentName}
936
- className={`searchstax-facet-value-container ${
937
- facetValue.disabled
938
- ? `searchstax-facet-value-disabled`
939
- : ``
940
- }`}
941
- ref={facetContainers[key + facet.name]}
1409
+ className={
1410
+ "searchstax-facet-input" +
1411
+ " mobile-" +
1412
+ key +
1413
+ facet.name
1414
+ }
942
1415
  >
943
- <div className={
944
- "searchstax-facet-input" +
945
- " mobile-" +
946
- key +
947
- facet.name
948
- }>
949
- <input
950
- type="checkbox"
951
- className="searchstax-facet-input-checkbox"
952
- checked={isChecked(facetValue)}
953
- readOnly={true}
954
- aria-label={facetValue.value + ' ' + facetValue.count}
955
- disabled={facetValue.disabled}
956
- onClick={(e) => {
957
- selectFacet(
958
- key + facet.name,
959
- e,
960
- facetValue,
961
- true
962
- );
963
- }}
964
- />
965
- </div>
966
- <div
967
- className="searchstax-facet-value-label"
1416
+ <input
1417
+ type="checkbox"
1418
+ className="searchstax-facet-input-checkbox"
1419
+ checked={isChecked(facetValue)}
1420
+ readOnly={true}
1421
+ aria-label={facetValue.value + ' ' + facetValue.count}
1422
+ disabled={facetValue.disabled}
968
1423
  onClick={(e) => {
969
1424
  selectFacet(
970
1425
  key + facet.name,
971
1426
  e,
972
1427
  facetValue,
973
- false
1428
+ true,
1429
+ true
974
1430
  );
975
1431
  }}
976
- >
977
- {facetValue.value}
978
- </div>
979
- <div
980
- className="searchstax-facet-value-count"
981
- onClick={(e) => {
982
- selectFacet(
983
- key + facet.name,
984
- e,
985
- facetValue,
986
- false
987
- );
988
- }}
989
- >
990
- ({facetValue.count})
991
- </div>
1432
+ />
992
1433
  </div>
993
- );
994
- }
995
- )}
1434
+ <div
1435
+ className="searchstax-facet-value-label"
1436
+ onClick={(e) => {
1437
+ selectFacet(
1438
+ key + facet.name,
1439
+ e,
1440
+ facetValue,
1441
+ false
1442
+ );
1443
+ }}
1444
+ >
1445
+ {facetValue.value}
1446
+ </div>
1447
+ <div
1448
+ className="searchstax-facet-value-count"
1449
+ onClick={(e) => {
1450
+ selectFacet(
1451
+ key + facet.name,
1452
+ e,
1453
+ facetValue,
1454
+ false
1455
+ );
1456
+ }}
1457
+ >
1458
+ ({facetValue.count})
1459
+ </div>
1460
+ </div>
1461
+ );
1462
+ }
1463
+ )}
996
1464
 
1465
+ <div
1466
+ className="searchstax-facet-show-more-container"
1467
+ v-if="facet.hasMoreFacets"
1468
+ >
997
1469
  <div
998
1470
  className="searchstax-facet-show-more-container"
999
- v-if="facet.hasMoreFacets"
1471
+ onClick={(e) => {
1472
+ showMoreLessMobile(e, facet);
1473
+ }}
1474
+ onKeyDown={(e) => {
1475
+ if(e.key === 'Enter' || e.key === ' ') {
1476
+ showMoreLessMobile(e as any, facet);
1477
+ }
1478
+ }}
1479
+ tabIndex={0}
1000
1480
  >
1001
- <div
1002
- className="searchstax-facet-show-more-container"
1003
- onClick={(e) => {
1004
- showMoreLessMobile(e, facet);
1005
- }}
1006
- >
1007
- {facet.showingAllFacets && (
1008
- <div className="searchstax-facet-show-less-button searchstax-facet-show-button">
1009
- less
1010
- </div>
1011
- )}
1012
- {!facet.showingAllFacets && (
1013
- <div className="searchstax-facet-show-more-button searchstax-facet-show-button">
1014
- more
1015
- </div>
1016
- )}
1017
- </div>
1481
+ {facet.showingAllFacets && (
1482
+ <div className="searchstax-facet-show-less-button searchstax-facet-show-button">
1483
+ less
1484
+ </div>
1485
+ )}
1486
+ {!facet.showingAllFacets && (
1487
+ <div className="searchstax-facet-show-more-button searchstax-facet-show-button">
1488
+ more
1489
+ </div>
1490
+ )}
1018
1491
  </div>
1019
1492
  </div>
1020
1493
  </div>
1021
1494
  </div>
1022
- );
1023
- })}
1024
- </div>
1025
- <button
1026
- className="searchstax-facets-mobile-overlay-done"
1027
- onClick={() => {
1028
- closeOverlay();
1029
- }}
1030
- >
1031
- Done
1032
- </button>
1495
+ </div>
1496
+ );
1497
+ })}
1033
1498
  </div>
1499
+ <button
1500
+ className="searchstax-facets-mobile-overlay-done"
1501
+ onClick={() => {
1502
+ closeOverlay();
1503
+ }}
1504
+ >
1505
+ Done
1506
+ </button>
1034
1507
  </div>
1035
1508
  </div>
1036
- ) : (<></>);
1037
- }
1509
+ </div>
1510
+ ) : (
1511
+ <></>
1512
+ );
1513
+ }
1038
1514
  <SearchstaxFacetsWidget
1039
1515
  facetingType="and"
1040
1516
  itemsPerPageDesktop={2}
@@ -1047,102 +1523,151 @@ function facetsTemplateDesktop(
1047
1523
 
1048
1524
  ### SearchFeedback Widget ###
1049
1525
 
1050
- example of search feedback widget initialization with minimum options
1526
+ The SearchStax Site Search solution provides a search feedback widget to support your React and Next.js search pages.
1527
+
1528
+ The SearchstaxFeedbackWidget displays search feedback and stats.
1529
+
1530
+ **Usage**
1531
+
1051
1532
  ```
1052
1533
  <SearchstaxOverviewWidget></SearchstaxOverviewWidget>
1053
1534
  ```
1054
1535
 
1536
+ **Main Template Override**
1537
+
1538
+ Main template for the search feedback message.
1539
+
1540
+ It receives following props:
1541
+ - searchExecuted - boolean, true if search was executed
1542
+ - hasResults - boolean, true if search has results
1543
+ - startResultIndex - number, start of page results
1544
+ - endResultIndex - number, end of page results
1545
+ - totalResults - number, total results
1546
+ - searchTerm - term that was searched
1547
+ - autoCorrectedQuery - query that search was autocorrected to
1548
+ - originalQuery - original query
1055
1549
 
1056
- example of search feedback widget initialization with template overrides
1550
+ **Example**
1057
1551
  ```
1058
1552
  function searchOverviewTemplate(
1059
1553
  searchFeedbackData: null | ISearchstaxSearchFeedbackData,
1060
- onOriginalQueryClick: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void
1554
+ onOriginalQueryClick: (
1555
+ event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
1556
+ ) => void
1061
1557
  ) {
1062
1558
  return (
1063
1559
  <>
1064
- {searchFeedbackData && searchFeedbackData?.searchExecuted && searchFeedbackData?.totalResults > 0 && (
1065
- <>
1066
- Showing{" "}
1067
- <b>
1068
- {searchFeedbackData.startResultIndex} - {searchFeedbackData.endResultIndex}
1069
- </b>{" "}
1070
- of
1071
- <b> {searchFeedbackData.totalResults}</b> results
1072
- {searchFeedbackData.searchTerm && (
1073
- <span>
1074
- &nbsp;for "<b>{searchFeedbackData.searchTerm}</b>"{" "}
1075
- </span>
1076
- )}
1077
- <div className="searchstax-feedback-container-suggested">
1078
- {searchFeedbackData.autoCorrectedQuery && (
1079
- <div>
1080
- {" "}
1081
- Search instead for{" "}
1082
- <a
1083
- href="#"
1084
- onClick={(e) => {
1085
- onOriginalQueryClick(e);
1086
- }}
1087
- className="searchstax-feedback-original-query"
1088
- >
1089
- {searchFeedbackData.originalQuery}
1090
- </a>
1091
- </div>
1560
+ {searchFeedbackData &&
1561
+ searchFeedbackData?.searchExecuted &&
1562
+ searchFeedbackData?.totalResults > 0 && (
1563
+ <>
1564
+ Showing{" "}
1565
+ <b>
1566
+ {searchFeedbackData.startResultIndex} -{" "}
1567
+ {searchFeedbackData.endResultIndex}
1568
+ </b>{" "}
1569
+ of
1570
+ <b> {searchFeedbackData.totalResults}</b> results
1571
+ {searchFeedbackData.searchTerm && (
1572
+ <span>
1573
+ &nbsp;for "<b>{searchFeedbackData.searchTerm}</b>"{" "}
1574
+ </span>
1092
1575
  )}
1093
- </div>
1094
- </>
1095
- )}
1576
+ <div className="searchstax-feedback-container-suggested">
1577
+ {searchFeedbackData.autoCorrectedQuery && (
1578
+ <div>
1579
+ {" "}
1580
+ Search instead for{" "}
1581
+ <a
1582
+ href="#"
1583
+ onClick={(e) => {
1584
+ onOriginalQueryClick(e);
1585
+ }}
1586
+ className="searchstax-feedback-original-query"
1587
+ aria-label={`Search instead for ${searchFeedbackData.originalQuery}`}
1588
+ >
1589
+ {searchFeedbackData.originalQuery}
1590
+ </a>
1591
+ </div>
1592
+ )}
1593
+ </div>
1594
+ </>
1595
+ )}
1096
1596
  </>
1097
1597
  );
1098
1598
  }
1099
- <SearchstaxOverviewWidget searchOverviewTemplate={searchOverviewTemplate}></SearchstaxOverviewWidget>
1599
+ <SearchstaxOverviewWidget searchOverviewTemplate={searchOverviewTemplate}></SearchstaxOverviewWidget>
1100
1600
  ```
1101
1601
 
1102
- ## RelatedSearches widget
1602
+ ### RelatedSearches widget ###
1103
1603
 
1604
+ The SearchStax Site Search solution offers a related-searches widget for React and Next.js search pages.
1104
1605
 
1606
+ The SearchstaxRelatedSearchesWidget displays related searches.
1105
1607
 
1106
- example of search feedback widget initialization with minimum options
1608
+ **Usage**
1107
1609
  ```
1108
- <SearchstaxRelatedSearchesWidget
1610
+ <SearchstaxRelatedSearchesWidget
1109
1611
  relatedSearchesURL={config.relatedSearchesURL}
1110
1612
  relatedSearchesAPIKey={config.relatedSearchesAPIKey}
1111
1613
  ></SearchstaxRelatedSearchesWidget>
1112
1614
  ```
1113
1615
 
1616
+ **Main Template Override**
1617
+
1618
+ Main template for related searches.
1619
+
1620
+ It receives following props:
1621
+
1622
+ - hasRelatedSearches - boolean, true if has related searches
1623
+ - relatedSearches - array of related searches
1624
+ - related_search - string, related searc
1625
+ - last - boolean, true if last related search
1626
+
1627
+ **Example**
1114
1628
 
1115
- example of search feedback widget initialization with template overrides
1116
1629
  ```
1117
- function searchRelatedSearchesTemplate(
1630
+ function searchRelatedSearchesTemplate(
1118
1631
  relatedData: null | ISearchstaxRelatedSearchesData,
1119
1632
  executeSearch: (result: ISearchstaxRelatedSearchResult) => void
1120
1633
  ) {
1121
1634
  return (
1122
1635
  <>
1123
- {relatedData && relatedData?.searchExecuted && relatedData?.hasRelatedSearches && (
1124
- <div className="searchstax-related-searches-container" id="searchstax-related-searches-container">
1125
- {" "}
1126
- Related searches: <span id="searchstax-related-searches"></span>
1127
- {relatedData.relatedSearches && (
1128
- <span className="searchstax-related-search">
1129
- {relatedData.relatedSearches.map((related) => (
1130
- <span
1131
- key={related.related_search}
1132
- onClick={() => {
1133
- executeSearch(related);
1134
- }}
1135
- className="searchstax-related-search searchstax-related-search-item"
1136
- >
1137
- {" "}
1138
- {related.related_search}
1139
- {!related.last && <span>,</span>}
1140
- </span>
1141
- ))}
1142
- </span>
1143
- )}
1144
- </div>
1145
- )}
1636
+ {relatedData &&
1637
+ relatedData?.searchExecuted &&
1638
+ relatedData?.hasRelatedSearches && (
1639
+ <div
1640
+ className="searchstax-related-searches-container"
1641
+ id="searchstax-related-searches-container"
1642
+ >
1643
+ {" "}
1644
+ Related searches: <span id="searchstax-related-searches"></span>
1645
+ {relatedData.relatedSearches && (
1646
+ <span className="searchstax-related-search">
1647
+ {relatedData.relatedSearches.map((related) => (
1648
+ <span
1649
+ key={related.related_search}
1650
+ onClick={() => {
1651
+ executeSearch(related);
1652
+ }}
1653
+ onKeyDown={(e) => {
1654
+ if(e.key === 'Enter' || e.key === ' ') {
1655
+ executeSearch(related);
1656
+ }
1657
+ }}
1658
+ tabIndex={0}
1659
+ className="searchstax-related-search searchstax-related-search-item"
1660
+ aria-label={'Related search: ' + related.related_search}
1661
+ >
1662
+ {" "}
1663
+ {related.related_search}
1664
+ {!related.last && <span>,</span>}
1665
+ </span>
1666
+ ))}
1667
+ </span>
1668
+ )}
1669
+ </div>
1670
+ )}
1146
1671
  </>
1147
1672
  );
1148
1673
  }
@@ -1151,60 +1676,83 @@ example of search feedback widget initialization with template overrides
1151
1676
  relatedSearchesAPIKey={config.relatedSearchesAPIKey}
1152
1677
  searchRelatedSearchesTemplate={searchRelatedSearchesTemplate}
1153
1678
  ></SearchstaxRelatedSearchesWidget>
1154
-
1155
1679
  ```
1156
1680
 
1157
1681
  ### ExternalPromotions widget ###
1158
1682
 
1159
- example of search feedback widget initialization with minimum options
1683
+ The SearchStax Site Search solution offers an external-promotions widget to assist with your React and Next.js custom search pages.
1684
+
1685
+ The SearchstaxExternalPromotionsWidget displays external promotions fetched from the API.
1686
+
1687
+ **Usage**
1688
+
1160
1689
  ```
1161
1690
  <SearchstaxExternalPromotionsWidget></SearchstaxExternalPromotionsWidget>
1162
1691
  ```
1163
1692
 
1693
+ **Main Template Override**
1694
+
1695
+ Main template for external promotions.
1696
+
1697
+ It receives following props:
1164
1698
 
1165
- example of search feedback widget initialization with template overrides
1699
+ - hasExternalPromotions - boolean, true if has external promotions
1700
+ - url - url of external promotion
1701
+ - uniqueId - unique id
1702
+ - name - name of promotion
1703
+ - description - description
1704
+
1705
+ **Example**
1166
1706
  ```
1167
- function searchExternalPromotionsTemplate(
1168
- externalPromotionsData: null | ISearchstaxExternalPromotionsData,
1169
- trackClick: (externalPromotion: IExternalPromotion, event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void
1170
- ) {
1171
- return (
1172
- <>
1173
- {externalPromotionsData &&
1174
- externalPromotionsData?.searchExecuted &&
1175
- externalPromotionsData?.hasExternalPromotions &&
1176
- externalPromotionsData.externalPromotions.map((externalPromotion) => (
1177
- <div className="searchstax-external-promotion searchstax-search-result" key={externalPromotion.id}>
1178
- <div className="icon-elevated"></div>
1179
- {externalPromotion.url && (
1180
- <a
1181
- href={externalPromotion.url}
1182
- onClick={(event) => {
1183
- trackClick(externalPromotion, event);
1184
- }}
1185
- className="searchstax-result-item-link"
1186
- ></a>
1187
- )}
1188
- <div className="searchstax-search-result-title-container">
1189
- <span className="searchstax-search-result-title">{externalPromotion.name}</span>
1190
- </div>
1191
- {externalPromotion.description && (
1192
- <p className="searchstax-search-result-description searchstax-search-result-common">
1193
- {" "}
1194
- {externalPromotion.description}{" "}
1195
- </p>
1196
- )}
1197
- {externalPromotion.url && (
1198
- <p className="searchstax-search-result-description searchstax-search-result-common">
1199
- {" "}
1200
- {externalPromotion.url}{" "}
1201
- </p>
1202
- )}
1707
+ function searchExternalPromotionsTemplate(
1708
+ externalPromotionsData: null | ISearchstaxExternalPromotionsData,
1709
+ trackClick: (
1710
+ externalPromotion: IExternalPromotion,
1711
+ event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
1712
+ ) => void
1713
+ ) {
1714
+ return (
1715
+ <>
1716
+ {externalPromotionsData &&
1717
+ externalPromotionsData?.searchExecuted &&
1718
+ externalPromotionsData?.hasExternalPromotions &&
1719
+ externalPromotionsData.externalPromotions.map((externalPromotion) => (
1720
+ <div
1721
+ className="searchstax-external-promotion searchstax-search-result"
1722
+ key={externalPromotion.id}
1723
+ >
1724
+ <div className="icon-elevated"></div>
1725
+ {externalPromotion.url && (
1726
+ <a
1727
+ href={externalPromotion.url}
1728
+ onClick={(event) => {
1729
+ trackClick(externalPromotion, event);
1730
+ }}
1731
+ className="searchstax-result-item-link"
1732
+ ></a>
1733
+ )}
1734
+ <div className="searchstax-search-result-title-container">
1735
+ <span className="searchstax-search-result-title">
1736
+ {externalPromotion.name}
1737
+ </span>
1203
1738
  </div>
1204
- ))}
1205
- </>
1206
- );
1207
- }
1739
+ {externalPromotion.description && (
1740
+ <p className="searchstax-search-result-description searchstax-search-result-common">
1741
+ {" "}
1742
+ {externalPromotion.description}{" "}
1743
+ </p>
1744
+ )}
1745
+ {externalPromotion.url && (
1746
+ <p className="searchstax-search-result-description searchstax-search-result-common">
1747
+ {" "}
1748
+ {externalPromotion.url}{" "}
1749
+ </p>
1750
+ )}
1751
+ </div>
1752
+ ))}
1753
+ </>
1754
+ );
1755
+ }
1208
1756
  <SearchstaxExternalPromotionsWidget
1209
1757
  searchExternalPromotionsTemplate={searchExternalPromotionsTemplate}
1210
1758
  ></SearchstaxExternalPromotionsWidget>
@@ -1213,44 +1761,61 @@ example of search feedback widget initialization with template overrides
1213
1761
 
1214
1762
  ### Sorting Widget ###
1215
1763
 
1216
- example of sorting widget initialization with minimum options
1764
+ The SearchStax Site Search solution offers a sorting widget for your React or Next.js custom search page.
1765
+
1766
+ The SearchstaxSortingWidget displays sorting options for search results.
1767
+
1768
+ **Usage**
1769
+
1217
1770
  ```
1218
1771
  <SearchstaxSortingWidget></SearchstaxSortingWidget>
1219
1772
  ```
1220
1773
 
1774
+ **Main Template Override**
1775
+
1776
+ Main template for sorting widget.
1777
+
1778
+ It receives following props:
1779
+
1780
+ - searchExecuted - boolean, true if search was executed
1781
+ - hasResultsOrExternalPromotions - boolean, true if there are results or external promotions
1782
+ - sortOptions - array of sort options, has key/value
1783
+
1784
+ **Example**
1221
1785
 
1222
- example of sorting widget initialization with template override
1223
1786
  ```
1224
- function searchSortingTemplate(
1787
+ function searchSortingTemplate(
1225
1788
  sortingData: null | ISearchstaxSearchSortingData,
1226
1789
  orderChange: (value: string) => void,
1227
1790
  selectedSorting: string
1228
1791
  ) {
1229
1792
  return (
1230
1793
  <>
1231
- {sortingData && sortingData?.searchExecuted && sortingData?.hasResultsOrExternalPromotions && (
1232
- <div className="searchstax-sorting-container">
1233
- <label className="searchstax-sorting-label" htmlFor="searchstax-search-order-select">
1234
- Sort By
1235
- </label>
1236
- <select
1237
- id="searchstax-search-order-select"
1238
- className="searchstax-search-order-select"
1239
- value={selectedSorting}
1240
- onChange={(e) => {
1241
- orderChange(e.target.value);
1242
- }}
1243
- >
1244
- {sortingData.sortOptions.map(function (sortOption) {
1794
+ {sortingData &&
1795
+ sortingData?.searchExecuted &&
1796
+ sortingData?.hasResultsOrExternalPromotions && (
1797
+ <div className="searchstax-sorting-container">
1798
+ <label className="searchstax-sorting-label" htmlFor="searchstax-search-order-select">
1799
+ Sort By
1800
+ </label>
1801
+ <select
1802
+ id="searchstax-search-order-select"
1803
+ className="searchstax-search-order-select"
1804
+ value={selectedSorting}
1805
+ onChange={(e) => {
1806
+ orderChange(e.target.value);
1807
+ }}
1808
+ >
1809
+ {sortingData.sortOptions.map(function (sortOption) {
1245
1810
  return (
1246
1811
  <option key={sortOption.key} value={sortOption.key}>
1247
1812
  {sortOption.value}
1248
1813
  </option>
1249
1814
  );
1250
1815
  })}
1251
- </select>
1252
- </div>
1253
- )}
1816
+ </select>
1817
+ </div>
1818
+ )}
1254
1819
  </>
1255
1820
  );
1256
1821
  }
@@ -1259,7 +1824,7 @@ example of sorting widget initialization with template override
1259
1824
 
1260
1825
 
1261
1826
  ## Template overrides
1262
- Templates use vue slots.
1827
+ Templates use relatedsearches templating.
1263
1828
 
1264
1829
  ## STYLING
1265
1830
 
@@ -1271,4 +1836,4 @@ css can be taken from
1271
1836
 
1272
1837
  ```
1273
1838
  ./../node_modules/@searchstax-inc/searchstudio-ux-js/dist/styles/mainTheme.css
1274
- ```
1839
+ ```