@constructor-io/constructorio-ui-autocomplete 1.15.0 → 1.16.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 (31) hide show
  1. package/dist/constructorio-ui-autocomplete-bundled.js +11 -11
  2. package/lib/cjs/components/Autocomplete/SectionItemsList/SectionItemsList.js +4 -2
  3. package/lib/cjs/constants.js +1 -0
  4. package/lib/cjs/hooks/useCioAutocomplete.js +25 -8
  5. package/lib/cjs/hooks/useDebouncedFetchSections.js +1 -1
  6. package/lib/cjs/hooks/useDownShift.js +24 -8
  7. package/lib/cjs/hooks/useRecommendationsObserver.js +53 -0
  8. package/lib/cjs/hooks/useSections.js +10 -14
  9. package/lib/cjs/utils.js +49 -11
  10. package/lib/cjs/version.js +1 -1
  11. package/lib/mjs/components/Autocomplete/SectionItemsList/SectionItemsList.js +4 -2
  12. package/lib/mjs/constants.js +1 -0
  13. package/lib/mjs/hooks/useCioAutocomplete.js +26 -10
  14. package/lib/mjs/hooks/useDebouncedFetchSections.js +1 -1
  15. package/lib/mjs/hooks/useDownShift.js +24 -8
  16. package/lib/mjs/hooks/useRecommendationsObserver.js +51 -0
  17. package/lib/mjs/hooks/useSections.js +11 -15
  18. package/lib/mjs/utils.js +46 -10
  19. package/lib/mjs/version.js +1 -1
  20. package/lib/types/components/Autocomplete/CioAutocompleteProvider.d.ts +1 -0
  21. package/lib/types/constants.d.ts +1 -1
  22. package/lib/types/hooks/useCioAutocomplete.d.ts +3 -2
  23. package/lib/types/hooks/useRecommendationsObserver.d.ts +17 -0
  24. package/lib/types/hooks/useSections.d.ts +2 -2
  25. package/lib/types/types.d.ts +4 -0
  26. package/lib/types/utils.d.ts +4 -2
  27. package/lib/types/version.d.ts +1 -1
  28. package/package.json +1 -1
  29. package/lib/cjs/hooks/useItems.js +0 -12
  30. package/lib/mjs/hooks/useItems.js +0 -10
  31. package/lib/types/hooks/useItems.d.ts +0 -3
@@ -1,16 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
- const react_1 = tslib_1.__importDefault(require("react"));
4
+ const react_1 = tslib_1.__importStar(require("react"));
5
5
  const SectionItem_1 = tslib_1.__importDefault(require("../SectionItem/SectionItem"));
6
6
  const utils_1 = require("../../../utils");
7
+ const CioAutocompleteProvider_1 = require("../CioAutocompleteProvider");
7
8
  // eslint-disable-next-line func-names
8
9
  const DefaultRenderSectionItemsList = function ({ section }) {
9
10
  var _a, _b;
11
+ const { getSectionProps } = (0, react_1.useContext)(CioAutocompleteProvider_1.CioAutocompleteContext);
10
12
  const sectionName = (section === null || section === void 0 ? void 0 : section.displayName) || (section === null || section === void 0 ? void 0 : section.identifier);
11
13
  if (!((_a = section === null || section === void 0 ? void 0 : section.data) === null || _a === void 0 ? void 0 : _a.length))
12
14
  return null;
13
- return (react_1.default.createElement("li", { className: `${sectionName} cio-section`, role: 'none' },
15
+ return (react_1.default.createElement("li", Object.assign({}, getSectionProps(section)),
14
16
  react_1.default.createElement("h5", { className: 'cio-sectionName', "aria-hidden": true }, (0, utils_1.camelToStartCase)(sectionName)),
15
17
  react_1.default.createElement("ul", { className: 'cio-section-items', role: 'none' }, (_b = section === null || section === void 0 ? void 0 : section.data) === null || _b === void 0 ? void 0 : _b.map((item) => (react_1.default.createElement(SectionItem_1.default, { item: item, key: item === null || item === void 0 ? void 0 : item.id, displaySearchTermHighlights: section.displaySearchTermHighlights }))))));
16
18
  };
@@ -35,6 +35,7 @@ const {
35
35
  getInputProps: () => ({...})), // prop getter for jsx input element
36
36
  getMenuProps: () => ({...})), // prop getter for jsx element rendering the results container
37
37
  getItemProps: (item) => ({...})), // prop getter for jsx element rendering each result
38
+ getSectionProps: (section: Section) => ({...})), // prop getter for jsx element rendering each section.
38
39
 
39
40
  // available for use, but not required for all use cases
40
41
  selectedItem: item, // undefined or current selected item (via hover or arrow keys)
@@ -9,7 +9,7 @@ const usePrevious_1 = tslib_1.__importDefault(require("./usePrevious"));
9
9
  const utils_1 = require("../utils");
10
10
  const useConsoleErrors_1 = tslib_1.__importDefault(require("./useConsoleErrors"));
11
11
  const useSections_1 = tslib_1.__importDefault(require("./useSections"));
12
- const useItems_1 = tslib_1.__importDefault(require("./useItems"));
12
+ const useRecommendationsObserver_1 = tslib_1.__importDefault(require("./useRecommendationsObserver"));
13
13
  exports.defaultSections = [
14
14
  {
15
15
  identifier: 'Search Suggestions',
@@ -28,11 +28,12 @@ const useCioAutocomplete = (options) => {
28
28
  // Get autocomplete sections (autocomplete + recommendations + custom)
29
29
  const { activeSections, activeSectionsWithData, zeroStateActiveSections, request } = (0, useSections_1.default)(query, cioClient, sections, zeroStateSections, advancedParameters);
30
30
  // Get dropdown items array from active sections (autocomplete + recommendations + custom)
31
- const items = (0, useItems_1.default)(activeSectionsWithData);
32
- const downshift = (0, useDownShift_1.default)({ setQuery, items, onSubmit, cioClient, previousQuery });
33
- const { isOpen, getMenuProps, getLabelProps, openMenu, closeMenu, highlightedIndex } = downshift;
31
+ const items = (0, react_1.useMemo)(() => (0, utils_1.getItemsForActiveSections)(activeSectionsWithData), [activeSectionsWithData]);
32
+ const { isOpen, getMenuProps, getLabelProps, openMenu, closeMenu, highlightedIndex, getInputProps, getItemProps, } = (0, useDownShift_1.default)({ setQuery, items, onSubmit, cioClient, previousQuery });
34
33
  // Log console errors
35
34
  (0, useConsoleErrors_1.default)(sections, activeSections);
35
+ // Track recommendation view
36
+ (0, useRecommendationsObserver_1.default)(isOpen, activeSectionsWithData, cioClient, utils_1.trackRecommendationView);
36
37
  return {
37
38
  query,
38
39
  sections: activeSectionsWithData,
@@ -46,9 +47,9 @@ const useCioAutocomplete = (options) => {
46
47
  getItemProps: (item) => {
47
48
  const { index, sectionId } = (0, utils_1.getItemPosition)({ item, items });
48
49
  const sectionItemTestId = `cio-item-${sectionId === null || sectionId === void 0 ? void 0 : sectionId.replace(' ', '')}`;
49
- return Object.assign(Object.assign({}, downshift.getItemProps({ item, index })), { className: `cio-item ${sectionItemTestId}`, 'data-testid': sectionItemTestId });
50
+ return Object.assign(Object.assign({}, getItemProps({ item, index })), { className: `cio-item ${sectionItemTestId}`, 'data-testid': sectionItemTestId });
50
51
  },
51
- getInputProps: () => (Object.assign(Object.assign({}, downshift.getInputProps({
52
+ getInputProps: () => (Object.assign(Object.assign({}, getInputProps({
52
53
  onChange: (e) => {
53
54
  setQuery(e.target.value);
54
55
  if (onChange) {
@@ -61,10 +62,10 @@ const useCioAutocomplete = (options) => {
61
62
  options.onFocus();
62
63
  }
63
64
  if (zeroStateActiveSections && openOnFocus !== false) {
64
- downshift.openMenu();
65
+ openMenu();
65
66
  }
66
67
  if (query === null || query === void 0 ? void 0 : query.length) {
67
- downshift.openMenu();
68
+ openMenu();
68
69
  }
69
70
  try {
70
71
  (_a = cioClient === null || cioClient === void 0 ? void 0 : cioClient.tracker) === null || _a === void 0 ? void 0 : _a.trackInputFocus();
@@ -107,6 +108,22 @@ const useCioAutocomplete = (options) => {
107
108
  className: 'cio-form',
108
109
  'data-testid': 'cio-form',
109
110
  }),
111
+ getSectionProps: (section) => {
112
+ var _a;
113
+ const sectionName = (section === null || section === void 0 ? void 0 : section.displayName) || (section === null || section === void 0 ? void 0 : section.identifier);
114
+ const attributes = {
115
+ className: `${sectionName} cio-section`,
116
+ ref: section.ref,
117
+ role: 'none',
118
+ 'data-cnstrc-section': (_a = section.data[0]) === null || _a === void 0 ? void 0 : _a.section,
119
+ };
120
+ // Add data attributes for recommendations
121
+ if (section.type === 'recommendations') {
122
+ attributes['data-cnstrc-recommendations'] = true;
123
+ attributes['data-cnstrc-recommendations-pod-id'] = section.identifier;
124
+ }
125
+ return attributes;
126
+ },
110
127
  setQuery,
111
128
  cioClient,
112
129
  autocompleteClassName,
@@ -6,7 +6,7 @@ const useDebounce_1 = tslib_1.__importDefault(require("./useDebounce"));
6
6
  const transformResponse = (response, options) => {
7
7
  const { numTermsWithGroupSuggestions, numGroupsSuggestedPerTerm } = options;
8
8
  const newSectionsData = {};
9
- Object.keys(response.sections).forEach((section) => {
9
+ Object.keys((response === null || response === void 0 ? void 0 : response.sections) || {}).forEach((section) => {
10
10
  newSectionsData[section] = [];
11
11
  const sectionItems = response.sections[section].map((item) => {
12
12
  var _a;
@@ -9,22 +9,38 @@ const useDownShift = ({ setQuery, items, onSubmit, cioClient, previousQuery = ''
9
9
  onSelectedItemChange({ selectedItem }) {
10
10
  var _a;
11
11
  if (selectedItem) {
12
- if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.section) === 'Search Suggestions') {
13
- setQuery(selectedItem.value || '');
14
- }
15
12
  if (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.value) {
16
13
  if (onSubmit)
17
14
  onSubmit({ item: selectedItem, originalQuery: previousQuery });
18
15
  try {
19
- if (!((_a = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.data) === null || _a === void 0 ? void 0 : _a.url)) {
16
+ if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.section) === 'Search Suggestions') {
17
+ setQuery(selectedItem.value || '');
20
18
  cioClient === null || cioClient === void 0 ? void 0 : cioClient.tracker.trackSearchSubmit(selectedItem.value, {
21
19
  originalQuery: previousQuery,
22
20
  });
23
21
  }
24
- cioClient === null || cioClient === void 0 ? void 0 : cioClient.tracker.trackAutocompleteSelect(selectedItem.value, {
25
- originalQuery: previousQuery,
26
- section: selectedItem.section,
27
- });
22
+ // Autocomplete Select tracking
23
+ // Recommendation Select tracking
24
+ if (selectedItem.podId && ((_a = selectedItem.data) === null || _a === void 0 ? void 0 : _a.id)) {
25
+ cioClient === null || cioClient === void 0 ? void 0 : cioClient.tracker.trackRecommendationClick({
26
+ itemName: selectedItem.value,
27
+ itemId: selectedItem.data.id,
28
+ variationId: selectedItem.data.variation_id,
29
+ podId: selectedItem.podId,
30
+ strategyId: selectedItem.strategy.id,
31
+ section: selectedItem.section,
32
+ resultId: selectedItem.result_id,
33
+ });
34
+ // Select tracking for all other Constructor sections:
35
+ // (ie: Search Suggestions, Products, Custom Cio sections, etc)
36
+ // This does not apply to custom user defined sections that aren't part of Constructor index
37
+ }
38
+ else if (selectedItem.result_id) {
39
+ cioClient === null || cioClient === void 0 ? void 0 : cioClient.tracker.trackAutocompleteSelect(selectedItem.value, {
40
+ originalQuery: previousQuery,
41
+ section: selectedItem.section,
42
+ });
43
+ }
28
44
  }
29
45
  catch (error) {
30
46
  // eslint-disable-next-line no-console
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_1 = require("react");
4
+ /**
5
+ * Custom hook that observes the visibility of recommendation sections and calls trackRecommendationView event.
6
+ * This is done by using the IntersectionObserver API to observe the visibility of each recommendation section.
7
+ * That is done by passing the ref of each recommendation section to the IntersectionObserver.
8
+ * The refs are either passed as a prop in `SectionConfiguration` or created by the library by default.
9
+ * Either way the refs are stored in the sections array.
10
+ *
11
+ * @param menuIsOpen - A boolean indicating whether the menu is open.
12
+ * @param sections - An array of sections to observe.
13
+ * @param constructorIO - An instance of the ConstructorIO client.
14
+ * @param trackRecommendationView - A callback function to track the recommendation view event.
15
+ */
16
+ function useRecommendationsObserver(menuIsOpen, sections, constructorIO, trackRecommendationView) {
17
+ // Get refs for each section
18
+ const refs = sections
19
+ .filter((section) => section.type === 'recommendations')
20
+ .map((section) => section.ref);
21
+ (0, react_1.useEffect)(() => {
22
+ const intersectionObserverOptions = {
23
+ // Root element is the bounding target for the observer to observe. If null, then the document viewport is used.
24
+ root: null,
25
+ // 0.1 indicate the callback should be called when that proportion of the target is visible (e.g., 10% visible).
26
+ threshold: 0.1,
27
+ };
28
+ const observer = new IntersectionObserver((entries) => {
29
+ // For each section, check if it's intersecting
30
+ entries.forEach((entry) => {
31
+ if (entry.isIntersecting) {
32
+ trackRecommendationView(entry.target, sections, constructorIO);
33
+ }
34
+ });
35
+ }, intersectionObserverOptions);
36
+ // Observe each section
37
+ refs.forEach((ref) => {
38
+ if (ref === null || ref === void 0 ? void 0 : ref.current) {
39
+ observer.observe(ref.current);
40
+ }
41
+ });
42
+ return () => {
43
+ // Unobserve each section
44
+ refs.forEach((ref) => {
45
+ if (ref === null || ref === void 0 ? void 0 : ref.current) {
46
+ observer.unobserve(ref.current);
47
+ }
48
+ });
49
+ };
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, [menuIsOpen]);
52
+ }
53
+ exports.default = useRecommendationsObserver;
@@ -1,32 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
+ /* eslint-disable max-params */
4
5
  const react_1 = require("react");
5
6
  const utils_1 = require("../utils");
6
7
  const useDebouncedFetchSections_1 = tslib_1.__importDefault(require("./useDebouncedFetchSections"));
7
8
  const useFetchRecommendationPod_1 = tslib_1.__importDefault(require("./useFetchRecommendationPod"));
8
9
  function useSections(query, cioClient, sections, zeroStateSections, advancedParameters) {
9
10
  const zeroStateActiveSections = !query.length && zeroStateSections;
10
- const [activeSections, setActiveSections] = (0, react_1.useState)(zeroStateActiveSections ? zeroStateSections : sections);
11
+ // Define All Sections
12
+ const activeSections = zeroStateActiveSections ? zeroStateSections : sections;
13
+ const sectionsRefs = (0, react_1.useRef)(activeSections.map(() => (0, react_1.createRef)()));
14
+ const [activeSectionsWithData, setActiveSectionsWithData] = (0, react_1.useState)([]);
11
15
  const autocompleteSections = (0, react_1.useMemo)(() => activeSections === null || activeSections === void 0 ? void 0 : activeSections.filter((config) => config.type === 'autocomplete' || !config.type), [activeSections]);
12
- const recommendationsSections = activeSections === null || activeSections === void 0 ? void 0 : activeSections.filter((config) => config.type === 'recommendations');
16
+ const recommendationsSections = (0, react_1.useMemo)(() => activeSections === null || activeSections === void 0 ? void 0 : activeSections.filter((config) => config.type === 'recommendations'), [activeSections]);
13
17
  // Fetch Autocomplete Results
14
18
  const { sectionsData: autocompleteResults, request } = (0, useDebouncedFetchSections_1.default)(query, cioClient, autocompleteSections, advancedParameters);
15
19
  // Fetch Recommendations Results
16
20
  const recommendationsResults = (0, useFetchRecommendationPod_1.default)(cioClient, recommendationsSections);
17
- const sectionResults = Object.assign(Object.assign({}, autocompleteResults), recommendationsResults);
18
- const activeSectionsWithData = (0, utils_1.getActiveSectionsWithData)(activeSections, sectionResults);
21
+ // Add to active sections the results data and refs when autocomplete results or recommendation results fetched
19
22
  (0, react_1.useEffect)(() => {
20
- setActiveSections(zeroStateActiveSections ? zeroStateSections : sections);
21
- }, [query, sections, zeroStateSections, zeroStateActiveSections]);
22
- (0, react_1.useEffect)(() => {
23
- if (sections && !Array.isArray(sections)) {
24
- setActiveSections([]);
25
- }
26
- if (zeroStateSections && !Array.isArray(zeroStateSections)) {
27
- setActiveSections([]);
28
- }
29
- }, [sections, zeroStateSections]);
23
+ const sectionsResults = Object.assign(Object.assign({}, autocompleteResults), recommendationsResults);
24
+ setActiveSectionsWithData((0, utils_1.getActiveSectionsWithData)(activeSections, sectionsResults, sectionsRefs));
25
+ }, [autocompleteResults, recommendationsResults, activeSections]);
30
26
  return {
31
27
  activeSections,
32
28
  activeSectionsWithData,
package/lib/cjs/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.escapeRegExp = exports.getActiveSectionsWithData = exports.getCioClient = exports.disableStoryActions = exports.stringifyWithDefaults = exports.functionStrings = exports.getStoryParams = exports.sleep = exports.clearConstructorRequests = exports.isTrackingRequestSent = exports.camelToStartCase = exports.getItemPosition = exports.getSearchSuggestionFeatures = void 0;
3
+ exports.getItemsForActiveSections = exports.trackRecommendationView = exports.escapeRegExp = exports.getActiveSectionsWithData = exports.getCioClient = exports.disableStoryActions = exports.stringifyWithDefaults = exports.functionStrings = exports.getStoryParams = exports.sleep = exports.clearConstructorRequests = exports.isTrackingRequestSent = exports.camelToStartCase = exports.getItemPosition = exports.getSearchSuggestionFeatures = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const constructorio_client_javascript_1 = tslib_1.__importDefault(require("@constructor-io/constructorio-client-javascript"));
6
6
  const typeGuards_1 = require("./typeGuards");
@@ -111,7 +111,7 @@ const disableStoryActions = (story) => {
111
111
  };
112
112
  exports.disableStoryActions = disableStoryActions;
113
113
  const getCioClient = (apiKey, cioJsClientOptions) => {
114
- if (apiKey) {
114
+ if (apiKey && typeof window !== 'undefined') {
115
115
  const cioClient = new constructorio_client_javascript_1.default(Object.assign({ apiKey, sendTrackingEvents: true, version: `cio-ui-autocomplete-${version_1.default}` }, cioJsClientOptions));
116
116
  // eslint-disable-next-line no-console
117
117
  cioClient.tracker.on('error', (error) => console.error(error));
@@ -120,23 +120,29 @@ const getCioClient = (apiKey, cioJsClientOptions) => {
120
120
  return null;
121
121
  };
122
122
  exports.getCioClient = getCioClient;
123
- const getActiveSectionsWithData = (activeSections, sectionResults) => {
123
+ const getActiveSectionsWithData = (activeSections, sectionResults, sectionsRefs) => {
124
124
  const activeSectionsWithData = [];
125
- activeSections === null || activeSections === void 0 ? void 0 : activeSections.forEach((config) => {
126
- const { identifier } = config;
127
- let data;
128
- if ((0, typeGuards_1.isCustomSection)(config)) {
125
+ activeSections === null || activeSections === void 0 ? void 0 : activeSections.forEach((sectionConfig, index) => {
126
+ const { identifier } = sectionConfig;
127
+ let sectionData;
128
+ if ((0, typeGuards_1.isCustomSection)(sectionConfig)) {
129
129
  // Copy id from data to the top level
130
- data = config.data.map((item) => {
130
+ sectionData = sectionConfig.data.map((item) => {
131
131
  var _a;
132
132
  return (Object.assign(Object.assign({}, item), { id: (item === null || item === void 0 ? void 0 : item.id) || ((_a = item === null || item === void 0 ? void 0 : item.data) === null || _a === void 0 ? void 0 : _a.id) }));
133
133
  });
134
134
  }
135
135
  else {
136
- data = sectionResults[identifier];
136
+ sectionData = sectionResults[identifier];
137
137
  }
138
- if (Array.isArray(data)) {
139
- activeSectionsWithData.push(Object.assign(Object.assign({}, config), { data }));
138
+ if (Array.isArray(sectionData)) {
139
+ const section = Object.assign(Object.assign({}, sectionConfig), { data: sectionData });
140
+ // If ref passed as part of `SectionConfiguration`, use it.
141
+ // Otherwise, use the ref from our library generated refs array
142
+ const userDefinedSectionRef = sectionConfig.ref;
143
+ const libraryGeneratedSectionRef = sectionsRefs.current[index];
144
+ section.ref = userDefinedSectionRef || libraryGeneratedSectionRef;
145
+ activeSectionsWithData.push(section);
140
146
  }
141
147
  });
142
148
  return activeSectionsWithData;
@@ -144,3 +150,35 @@ const getActiveSectionsWithData = (activeSections, sectionResults) => {
144
150
  exports.getActiveSectionsWithData = getActiveSectionsWithData;
145
151
  const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
146
152
  exports.escapeRegExp = escapeRegExp;
153
+ const trackRecommendationView = (target, activeSectionsWithData, cioClient) => {
154
+ if (target.dataset.cnstrcRecommendationsPodId) {
155
+ // Pull recommendations from activeSectionsWithData by podId surfaced on target
156
+ const recommendationSection = activeSectionsWithData.find((section) => section.identifier === target.dataset.cnstrcRecommendationsPodId);
157
+ const recommendationItems = recommendationSection === null || recommendationSection === void 0 ? void 0 : recommendationSection.data.map((item) => {
158
+ var _a, _b;
159
+ return ({
160
+ itemId: (_a = item.data) === null || _a === void 0 ? void 0 : _a.id,
161
+ itemName: item.value,
162
+ variationId: (_b = item.data) === null || _b === void 0 ? void 0 : _b.variation_id,
163
+ });
164
+ });
165
+ cioClient === null || cioClient === void 0 ? void 0 : cioClient.tracker.trackRecommendationView({
166
+ podId: target.dataset.cnstrcRecommendationsPodId,
167
+ numResultsViewed: (recommendationItems === null || recommendationItems === void 0 ? void 0 : recommendationItems.length) || 0,
168
+ url: window.location.href,
169
+ section: target.dataset.cnstrcSection,
170
+ items: recommendationItems,
171
+ });
172
+ }
173
+ };
174
+ exports.trackRecommendationView = trackRecommendationView;
175
+ const getItemsForActiveSections = (activeSectionsWithData) => {
176
+ const items = [];
177
+ activeSectionsWithData === null || activeSectionsWithData === void 0 ? void 0 : activeSectionsWithData.forEach((config) => {
178
+ if (config === null || config === void 0 ? void 0 : config.data) {
179
+ items.push(...config.data);
180
+ }
181
+ });
182
+ return items;
183
+ };
184
+ exports.getItemsForActiveSections = getItemsForActiveSections;
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = '1.15.0';
3
+ exports.default = '1.16.0';
@@ -1,12 +1,14 @@
1
- import React from 'react';
1
+ import React, { useContext } from 'react';
2
2
  import SectionItem from '../SectionItem/SectionItem';
3
3
  import { camelToStartCase } from '../../../utils';
4
+ import { CioAutocompleteContext } from '../CioAutocompleteProvider';
4
5
  // eslint-disable-next-line func-names
5
6
  const DefaultRenderSectionItemsList = function ({ section }) {
7
+ const { getSectionProps } = useContext(CioAutocompleteContext);
6
8
  const sectionName = section?.displayName || section?.identifier;
7
9
  if (!section?.data?.length)
8
10
  return null;
9
- return (React.createElement("li", { className: `${sectionName} cio-section`, role: 'none' },
11
+ return (React.createElement("li", { ...getSectionProps(section) },
10
12
  React.createElement("h5", { className: 'cio-sectionName', "aria-hidden": true }, camelToStartCase(sectionName)),
11
13
  React.createElement("ul", { className: 'cio-section-items', role: 'none' }, section?.data?.map((item) => (React.createElement(SectionItem, { item: item, key: item?.id, displaySearchTermHighlights: section.displaySearchTermHighlights }))))));
12
14
  };
@@ -32,6 +32,7 @@ const {
32
32
  getInputProps: () => ({...})), // prop getter for jsx input element
33
33
  getMenuProps: () => ({...})), // prop getter for jsx element rendering the results container
34
34
  getItemProps: (item) => ({...})), // prop getter for jsx element rendering each result
35
+ getSectionProps: (section: Section) => ({...})), // prop getter for jsx element rendering each section.
35
36
 
36
37
  // available for use, but not required for all use cases
37
38
  selectedItem: item, // undefined or current selected item (via hover or arrow keys)
@@ -1,11 +1,11 @@
1
- import { useState } from 'react';
1
+ import { useMemo, useState } from 'react';
2
2
  import useCioClient from './useCioClient';
3
3
  import useDownShift from './useDownShift';
4
4
  import usePrevious from './usePrevious';
5
- import { getItemPosition, getSearchSuggestionFeatures } from '../utils';
5
+ import { getItemPosition, getItemsForActiveSections, getSearchSuggestionFeatures, trackRecommendationView, } from '../utils';
6
6
  import useConsoleErrors from './useConsoleErrors';
7
7
  import useSections from './useSections';
8
- import useItems from './useItems';
8
+ import useRecommendationsObserver from './useRecommendationsObserver';
9
9
  export const defaultSections = [
10
10
  {
11
11
  identifier: 'Search Suggestions',
@@ -24,11 +24,12 @@ const useCioAutocomplete = (options) => {
24
24
  // Get autocomplete sections (autocomplete + recommendations + custom)
25
25
  const { activeSections, activeSectionsWithData, zeroStateActiveSections, request } = useSections(query, cioClient, sections, zeroStateSections, advancedParameters);
26
26
  // Get dropdown items array from active sections (autocomplete + recommendations + custom)
27
- const items = useItems(activeSectionsWithData);
28
- const downshift = useDownShift({ setQuery, items, onSubmit, cioClient, previousQuery });
29
- const { isOpen, getMenuProps, getLabelProps, openMenu, closeMenu, highlightedIndex } = downshift;
27
+ const items = useMemo(() => getItemsForActiveSections(activeSectionsWithData), [activeSectionsWithData]);
28
+ const { isOpen, getMenuProps, getLabelProps, openMenu, closeMenu, highlightedIndex, getInputProps, getItemProps, } = useDownShift({ setQuery, items, onSubmit, cioClient, previousQuery });
30
29
  // Log console errors
31
30
  useConsoleErrors(sections, activeSections);
31
+ // Track recommendation view
32
+ useRecommendationsObserver(isOpen, activeSectionsWithData, cioClient, trackRecommendationView);
32
33
  return {
33
34
  query,
34
35
  sections: activeSectionsWithData,
@@ -47,13 +48,13 @@ const useCioAutocomplete = (options) => {
47
48
  const { index, sectionId } = getItemPosition({ item, items });
48
49
  const sectionItemTestId = `cio-item-${sectionId?.replace(' ', '')}`;
49
50
  return {
50
- ...downshift.getItemProps({ item, index }),
51
+ ...getItemProps({ item, index }),
51
52
  className: `cio-item ${sectionItemTestId}`,
52
53
  'data-testid': sectionItemTestId,
53
54
  };
54
55
  },
55
56
  getInputProps: () => ({
56
- ...downshift.getInputProps({
57
+ ...getInputProps({
57
58
  onChange: (e) => {
58
59
  setQuery(e.target.value);
59
60
  if (onChange) {
@@ -67,10 +68,10 @@ const useCioAutocomplete = (options) => {
67
68
  options.onFocus();
68
69
  }
69
70
  if (zeroStateActiveSections && openOnFocus !== false) {
70
- downshift.openMenu();
71
+ openMenu();
71
72
  }
72
73
  if (query?.length) {
73
- downshift.openMenu();
74
+ openMenu();
74
75
  }
75
76
  try {
76
77
  cioClient?.tracker?.trackInputFocus();
@@ -118,6 +119,21 @@ const useCioAutocomplete = (options) => {
118
119
  className: 'cio-form',
119
120
  'data-testid': 'cio-form',
120
121
  }),
122
+ getSectionProps: (section) => {
123
+ const sectionName = section?.displayName || section?.identifier;
124
+ const attributes = {
125
+ className: `${sectionName} cio-section`,
126
+ ref: section.ref,
127
+ role: 'none',
128
+ 'data-cnstrc-section': section.data[0]?.section,
129
+ };
130
+ // Add data attributes for recommendations
131
+ if (section.type === 'recommendations') {
132
+ attributes['data-cnstrc-recommendations'] = true;
133
+ attributes['data-cnstrc-recommendations-pod-id'] = section.identifier;
134
+ }
135
+ return attributes;
136
+ },
121
137
  setQuery,
122
138
  cioClient,
123
139
  autocompleteClassName,
@@ -3,7 +3,7 @@ import useDebounce from './useDebounce';
3
3
  const transformResponse = (response, options) => {
4
4
  const { numTermsWithGroupSuggestions, numGroupsSuggestedPerTerm } = options;
5
5
  const newSectionsData = {};
6
- Object.keys(response.sections).forEach((section) => {
6
+ Object.keys(response?.sections || {}).forEach((section) => {
7
7
  newSectionsData[section] = [];
8
8
  const sectionItems = response.sections[section].map((item) => ({
9
9
  ...item,
@@ -6,22 +6,38 @@ const useDownShift = ({ setQuery, items, onSubmit, cioClient, previousQuery = ''
6
6
  itemToString: (item) => item?.value || '',
7
7
  onSelectedItemChange({ selectedItem }) {
8
8
  if (selectedItem) {
9
- if (selectedItem?.section === 'Search Suggestions') {
10
- setQuery(selectedItem.value || '');
11
- }
12
9
  if (selectedItem?.value) {
13
10
  if (onSubmit)
14
11
  onSubmit({ item: selectedItem, originalQuery: previousQuery });
15
12
  try {
16
- if (!selectedItem?.data?.url) {
13
+ if (selectedItem?.section === 'Search Suggestions') {
14
+ setQuery(selectedItem.value || '');
17
15
  cioClient?.tracker.trackSearchSubmit(selectedItem.value, {
18
16
  originalQuery: previousQuery,
19
17
  });
20
18
  }
21
- cioClient?.tracker.trackAutocompleteSelect(selectedItem.value, {
22
- originalQuery: previousQuery,
23
- section: selectedItem.section,
24
- });
19
+ // Autocomplete Select tracking
20
+ // Recommendation Select tracking
21
+ if (selectedItem.podId && selectedItem.data?.id) {
22
+ cioClient?.tracker.trackRecommendationClick({
23
+ itemName: selectedItem.value,
24
+ itemId: selectedItem.data.id,
25
+ variationId: selectedItem.data.variation_id,
26
+ podId: selectedItem.podId,
27
+ strategyId: selectedItem.strategy.id,
28
+ section: selectedItem.section,
29
+ resultId: selectedItem.result_id,
30
+ });
31
+ // Select tracking for all other Constructor sections:
32
+ // (ie: Search Suggestions, Products, Custom Cio sections, etc)
33
+ // This does not apply to custom user defined sections that aren't part of Constructor index
34
+ }
35
+ else if (selectedItem.result_id) {
36
+ cioClient?.tracker.trackAutocompleteSelect(selectedItem.value, {
37
+ originalQuery: previousQuery,
38
+ section: selectedItem.section,
39
+ });
40
+ }
25
41
  }
26
42
  catch (error) {
27
43
  // eslint-disable-next-line no-console
@@ -0,0 +1,51 @@
1
+ import { useEffect } from 'react';
2
+ /**
3
+ * Custom hook that observes the visibility of recommendation sections and calls trackRecommendationView event.
4
+ * This is done by using the IntersectionObserver API to observe the visibility of each recommendation section.
5
+ * That is done by passing the ref of each recommendation section to the IntersectionObserver.
6
+ * The refs are either passed as a prop in `SectionConfiguration` or created by the library by default.
7
+ * Either way the refs are stored in the sections array.
8
+ *
9
+ * @param menuIsOpen - A boolean indicating whether the menu is open.
10
+ * @param sections - An array of sections to observe.
11
+ * @param constructorIO - An instance of the ConstructorIO client.
12
+ * @param trackRecommendationView - A callback function to track the recommendation view event.
13
+ */
14
+ function useRecommendationsObserver(menuIsOpen, sections, constructorIO, trackRecommendationView) {
15
+ // Get refs for each section
16
+ const refs = sections
17
+ .filter((section) => section.type === 'recommendations')
18
+ .map((section) => section.ref);
19
+ useEffect(() => {
20
+ const intersectionObserverOptions = {
21
+ // Root element is the bounding target for the observer to observe. If null, then the document viewport is used.
22
+ root: null,
23
+ // 0.1 indicate the callback should be called when that proportion of the target is visible (e.g., 10% visible).
24
+ threshold: 0.1,
25
+ };
26
+ const observer = new IntersectionObserver((entries) => {
27
+ // For each section, check if it's intersecting
28
+ entries.forEach((entry) => {
29
+ if (entry.isIntersecting) {
30
+ trackRecommendationView(entry.target, sections, constructorIO);
31
+ }
32
+ });
33
+ }, intersectionObserverOptions);
34
+ // Observe each section
35
+ refs.forEach((ref) => {
36
+ if (ref?.current) {
37
+ observer.observe(ref.current);
38
+ }
39
+ });
40
+ return () => {
41
+ // Unobserve each section
42
+ refs.forEach((ref) => {
43
+ if (ref?.current) {
44
+ observer.unobserve(ref.current);
45
+ }
46
+ });
47
+ };
48
+ // eslint-disable-next-line react-hooks/exhaustive-deps
49
+ }, [menuIsOpen]);
50
+ }
51
+ export default useRecommendationsObserver;
@@ -1,29 +1,25 @@
1
- import { useEffect, useMemo, useState } from 'react';
1
+ /* eslint-disable max-params */
2
+ import { createRef, useEffect, useMemo, useRef, useState } from 'react';
2
3
  import { getActiveSectionsWithData } from '../utils';
3
4
  import useDebouncedFetchSection from './useDebouncedFetchSections';
4
5
  import useFetchRecommendationPod from './useFetchRecommendationPod';
5
6
  export default function useSections(query, cioClient, sections, zeroStateSections, advancedParameters) {
6
7
  const zeroStateActiveSections = !query.length && zeroStateSections;
7
- const [activeSections, setActiveSections] = useState(zeroStateActiveSections ? zeroStateSections : sections);
8
+ // Define All Sections
9
+ const activeSections = zeroStateActiveSections ? zeroStateSections : sections;
10
+ const sectionsRefs = useRef(activeSections.map(() => createRef()));
11
+ const [activeSectionsWithData, setActiveSectionsWithData] = useState([]);
8
12
  const autocompleteSections = useMemo(() => activeSections?.filter((config) => config.type === 'autocomplete' || !config.type), [activeSections]);
9
- const recommendationsSections = activeSections?.filter((config) => config.type === 'recommendations');
13
+ const recommendationsSections = useMemo(() => activeSections?.filter((config) => config.type === 'recommendations'), [activeSections]);
10
14
  // Fetch Autocomplete Results
11
15
  const { sectionsData: autocompleteResults, request } = useDebouncedFetchSection(query, cioClient, autocompleteSections, advancedParameters);
12
16
  // Fetch Recommendations Results
13
17
  const recommendationsResults = useFetchRecommendationPod(cioClient, recommendationsSections);
14
- const sectionResults = { ...autocompleteResults, ...recommendationsResults };
15
- const activeSectionsWithData = getActiveSectionsWithData(activeSections, sectionResults);
18
+ // Add to active sections the results data and refs when autocomplete results or recommendation results fetched
16
19
  useEffect(() => {
17
- setActiveSections(zeroStateActiveSections ? zeroStateSections : sections);
18
- }, [query, sections, zeroStateSections, zeroStateActiveSections]);
19
- useEffect(() => {
20
- if (sections && !Array.isArray(sections)) {
21
- setActiveSections([]);
22
- }
23
- if (zeroStateSections && !Array.isArray(zeroStateSections)) {
24
- setActiveSections([]);
25
- }
26
- }, [sections, zeroStateSections]);
20
+ const sectionsResults = { ...autocompleteResults, ...recommendationsResults };
21
+ setActiveSectionsWithData(getActiveSectionsWithData(activeSections, sectionsResults, sectionsRefs));
22
+ }, [autocompleteResults, recommendationsResults, activeSections]);
27
23
  return {
28
24
  activeSections,
29
25
  activeSectionsWithData,