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