@constructor-io/constructorio-ui-autocomplete 1.15.1 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constructorio-ui-autocomplete-bundled.js +11 -11
- package/lib/cjs/components/Autocomplete/SectionItemsList/SectionItemsList.js +4 -2
- package/lib/cjs/constants.js +1 -0
- package/lib/cjs/hooks/useCioAutocomplete.js +25 -8
- package/lib/cjs/hooks/useDownShift.js +24 -8
- package/lib/cjs/hooks/useRecommendationsObserver.js +53 -0
- package/lib/cjs/hooks/useSections.js +10 -14
- package/lib/cjs/utils.js +48 -10
- package/lib/cjs/version.js +1 -1
- package/lib/mjs/components/Autocomplete/SectionItemsList/SectionItemsList.js +4 -2
- package/lib/mjs/constants.js +1 -0
- package/lib/mjs/hooks/useCioAutocomplete.js +26 -10
- package/lib/mjs/hooks/useDownShift.js +24 -8
- package/lib/mjs/hooks/useRecommendationsObserver.js +51 -0
- package/lib/mjs/hooks/useSections.js +11 -15
- package/lib/mjs/utils.js +45 -9
- package/lib/mjs/version.js +1 -1
- package/lib/types/components/Autocomplete/CioAutocompleteProvider.d.ts +1 -0
- package/lib/types/constants.d.ts +1 -1
- package/lib/types/hooks/useCioAutocomplete.d.ts +3 -2
- package/lib/types/hooks/useRecommendationsObserver.d.ts +17 -0
- package/lib/types/hooks/useSections.d.ts +2 -2
- package/lib/types/types.d.ts +4 -0
- package/lib/types/utils.d.ts +4 -2
- package/lib/types/version.d.ts +1 -1
- package/package.json +5 -3
- package/lib/cjs/hooks/useItems.js +0 -12
- package/lib/mjs/hooks/useItems.js +0 -10
- 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.
|
|
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", {
|
|
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
|
};
|
package/lib/cjs/constants.js
CHANGED
|
@@ -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
|
|
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,
|
|
32
|
-
const
|
|
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({},
|
|
50
|
+
return Object.assign(Object.assign({}, getItemProps({ item, index })), { className: `cio-item ${sectionItemTestId}`, 'data-testid': sectionItemTestId });
|
|
50
51
|
},
|
|
51
|
-
getInputProps: () => (Object.assign(Object.assign({},
|
|
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
|
-
|
|
65
|
+
openMenu();
|
|
65
66
|
}
|
|
66
67
|
if (query === null || query === void 0 ? void 0 : query.length) {
|
|
67
|
-
|
|
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,
|
|
@@ -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 (
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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");
|
|
@@ -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((
|
|
126
|
-
const { identifier } =
|
|
127
|
-
let
|
|
128
|
-
if ((0, typeGuards_1.isCustomSection)(
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
sectionData = sectionResults[identifier];
|
|
137
137
|
}
|
|
138
|
-
if (Array.isArray(
|
|
139
|
-
|
|
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;
|
package/lib/cjs/version.js
CHANGED
|
@@ -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", {
|
|
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
|
};
|
package/lib/mjs/constants.js
CHANGED
|
@@ -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
|
|
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 =
|
|
28
|
-
const
|
|
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
|
-
...
|
|
51
|
+
...getItemProps({ item, index }),
|
|
51
52
|
className: `cio-item ${sectionItemTestId}`,
|
|
52
53
|
'data-testid': sectionItemTestId,
|
|
53
54
|
};
|
|
54
55
|
},
|
|
55
56
|
getInputProps: () => ({
|
|
56
|
-
...
|
|
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
|
-
|
|
71
|
+
openMenu();
|
|
71
72
|
}
|
|
72
73
|
if (query?.length) {
|
|
73
|
-
|
|
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,
|
|
@@ -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 (
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,
|