@constructor-io/constructorio-ui-autocomplete 1.28.2 → 1.29.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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const react_1 = tslib_1.__importStar(require("react"));
5
5
  const CioAutocompleteProvider_1 = require("../CioAutocompleteProvider");
6
+ const dataAttributeHelpers_1 = require("../../../utils/dataAttributeHelpers");
6
7
  function DefaultRenderInput({ getFormProps, getInputProps, getLabelProps, setQuery }) {
7
8
  const inputProps = getInputProps();
8
9
  return (react_1.default.createElement("form", Object.assign({}, getFormProps()),
@@ -17,7 +18,7 @@ function DefaultRenderInput({ getFormProps, getInputProps, getLabelProps, setQue
17
18
  react_1.default.createElement("div", { className: 'cio-icon' },
18
19
  react_1.default.createElement("svg", { stroke: 'currentColor', fill: 'currentColor', strokeWidth: '0', viewBox: '0 0 512 512', height: '1em', width: '1em', xmlns: 'http://www.w3.org/2000/svg' },
19
20
  react_1.default.createElement("path", { d: 'M289.94 256l95-95A24 24 0 00351 127l-95 95-95-95a24 24 0 00-34 34l95 95-95 95a24 24 0 1034 34l95-95 95 95a24 24 0 0034-34z' })))),
20
- react_1.default.createElement("button", { className: 'cio-submit-btn', "data-testid": 'cio-submit-btn', "data-cnstrc-search-submit-btn": true, onClick: (e) => {
21
+ react_1.default.createElement("button", Object.assign({ className: 'cio-submit-btn', "data-testid": 'cio-submit-btn' }, { [dataAttributeHelpers_1.cnstrcDataAttrs.autocomplete.searchSubmitButton]: '' }, { onClick: (e) => {
21
22
  var _a;
22
23
  if (!inputProps.value) {
23
24
  e.preventDefault();
@@ -25,7 +26,7 @@ function DefaultRenderInput({ getFormProps, getInputProps, getLabelProps, setQue
25
26
  (_a = document.getElementById(inputProps.id)) === null || _a === void 0 ? void 0 : _a.focus();
26
27
  }
27
28
  }
28
- }, type: 'submit', "aria-label": 'Submit Search' },
29
+ }, type: 'submit', "aria-label": 'Submit Search' }),
29
30
  react_1.default.createElement("div", { className: 'cio-icon' },
30
31
  react_1.default.createElement("svg", { stroke: 'currentColor', fill: 'currentColor', strokeWidth: '0', viewBox: '0 0 512 512', height: '1em', width: '1em', xmlns: 'http://www.w3.org/2000/svg' },
31
32
  react_1.default.createElement("path", { d: 'M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z' }))))));
@@ -8,6 +8,7 @@ const helpers_1 = require("../../../utils/helpers");
8
8
  const format_1 = require("../../../utils/format");
9
9
  const CioAutocompleteProvider_1 = require("../CioAutocompleteProvider");
10
10
  const NoResults_1 = tslib_1.__importDefault(require("../AutocompleteResults/NoResults"));
11
+ const dataAttributeHelpers_1 = require("../../../utils/dataAttributeHelpers");
11
12
  // eslint-disable-next-line func-names
12
13
  const DefaultRenderSectionItemsList = function ({ section }) {
13
14
  var _a, _b, _c;
@@ -52,7 +53,7 @@ const DefaultRenderSectionItemsList = function ({ section }) {
52
53
  displayShowAllResultsButton &&
53
54
  (typeof type === 'undefined' || type === 'autocomplete') &&
54
55
  section.indexSectionName === 'Products' && (react_1.default.createElement("div", { className: 'cio-section-footer' },
55
- react_1.default.createElement("button", { "data-cnstrc-search-submit-btn": true, className: 'cio-show-all-results-button', type: 'button', onClick: onSubmit }, (0, helpers_1.translate)('show all results', translations))))));
56
+ react_1.default.createElement("button", Object.assign({}, { [dataAttributeHelpers_1.cnstrcDataAttrs.autocomplete.searchSubmitButton]: '' }, { className: 'cio-show-all-results-button', type: 'button', onClick: onSubmit }), (0, helpers_1.translate)('show all results', translations))))));
56
57
  };
57
58
  function SectionItemsList(props) {
58
59
  const { section, children = DefaultRenderSectionItemsList } = props;
@@ -17,6 +17,7 @@ const useRecommendationsObserver_1 = tslib_1.__importDefault(require("./useRecom
17
17
  const typeGuards_1 = require("../typeGuards");
18
18
  const useNormalizedProps_1 = tslib_1.__importDefault(require("./useNormalizedProps"));
19
19
  const useCustomBlur_1 = tslib_1.__importDefault(require("./useCustomBlur"));
20
+ const dataAttributeHelpers_1 = require("../utils/dataAttributeHelpers");
20
21
  const shopifyDefaults_1 = require("../utils/shopifyDefaults");
21
22
  exports.defaultSections = [
22
23
  {
@@ -54,7 +55,7 @@ const useCioAutocomplete = (options) => {
54
55
  cioJsClientOptions: options.cioJsClientOptions,
55
56
  });
56
57
  // Get autocomplete sections (autocomplete + recommendations + custom)
57
- const { fetchRecommendationResults, activeSections, activeSectionsWithData, zeroStateActiveSections, request, totalNumResultsPerSection, } = (0, useSections_1.default)(query, cioClient, sections, zeroStateSections, advancedParameters);
58
+ const { fetchRecommendationResults, activeSections, activeSectionsWithData, zeroStateActiveSections, request, totalNumResultsPerSection, podsData, } = (0, useSections_1.default)(query, cioClient, sections, zeroStateSections, advancedParameters);
58
59
  const features = (0, react_1.useMemo)(() => (0, features_1.getFeatures)(request), [request]);
59
60
  // Get dropdown items array from active sections (autocomplete + recommendations + custom)
60
61
  const items = (0, react_1.useMemo)(() => (0, helpers_1.getItemsForActiveSections)(activeSectionsWithData), [activeSectionsWithData]);
@@ -75,12 +76,12 @@ const useCioAutocomplete = (options) => {
75
76
  request,
76
77
  featureToggles: features,
77
78
  isOpen,
78
- getMenuProps: () => (Object.assign(Object.assign({}, getMenuProps()), { className: 'cio-results', 'data-testid': 'cio-results', 'data-cnstrc-autosuggest': '' })),
79
+ getMenuProps: () => (Object.assign(Object.assign({}, getMenuProps()), { className: 'cio-results', 'data-testid': 'cio-results', [dataAttributeHelpers_1.cnstrcDataAttrs.autocomplete.autocompleteContainer]: '' })),
79
80
  getLabelProps,
80
81
  openMenu,
81
82
  closeMenu,
82
83
  getItemProps: (item) => {
83
- var _a, _b, _c, _d;
84
+ var _a;
84
85
  const { index, sectionId } = (0, helpers_1.getItemPosition)({ item, items });
85
86
  const sectionItemTestId = `cio-item-${sectionId === null || sectionId === void 0 ? void 0 : sectionId.replace(' ', '')}`;
86
87
  // Products always have links, Search Suggestions with getSearchResultsUrl have links
@@ -97,9 +98,11 @@ const useCioAutocomplete = (options) => {
97
98
  }
98
99
  },
99
100
  };
100
- return Object.assign(Object.assign(Object.assign({}, getItemPropsDownShift({ item, index })), {
101
+ // Get all data attributes using the helper function
102
+ const dataAttributes = (0, dataAttributeHelpers_1.getItemCnstrcDataAttributes)(item);
103
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, getItemPropsDownShift({ item, index })), {
101
104
  // @deprecated `sectionItemTestId` will be removed as a className in the next major version
102
- className: `cio-item ${sectionItemTestId}`, 'data-testid': sectionItemTestId, 'data-cnstrc-item-section': item.section, 'data-cnstrc-item-group': item.groupId, 'data-cnstrc-item-name': item.value, 'data-cnstrc-item-id': (_b = item.data) === null || _b === void 0 ? void 0 : _b.id, 'data-cnstrc-sl-campaign-id': (_c = item.labels) === null || _c === void 0 ? void 0 : _c.sl_campaign_id, 'data-cnstrc-sl-campaign-owner': (_d = item.labels) === null || _d === void 0 ? void 0 : _d.sl_campaign_owner }), (hasLink ? {} : nonInteractiveItemsProps));
105
+ className: `cio-item ${sectionItemTestId}`, 'data-testid': sectionItemTestId }), dataAttributes), (hasLink ? {} : nonInteractiveItemsProps));
103
106
  },
104
107
  getInputProps: () => (Object.assign(Object.assign({}, getInputProps({
105
108
  onChange: (e) => {
@@ -131,7 +134,7 @@ const useCioAutocomplete = (options) => {
131
134
  // eslint-disable-next-line no-console
132
135
  console.log(error);
133
136
  }
134
- }, className: 'cio-input', 'data-testid': 'cio-input', 'data-cnstrc-search-input': '', placeholder, onKeyDownCapture: ({ code, key, nativeEvent }) => {
137
+ }, className: 'cio-input', 'data-testid': 'cio-input', [dataAttributeHelpers_1.cnstrcDataAttrs.autocomplete.input]: '', placeholder, onKeyDownCapture: ({ code, key, nativeEvent }) => {
135
138
  const isEnter = code === 'Enter' || key === 'Enter';
136
139
  const isHome = code === 'Home' || key === 'Home';
137
140
  const isEnd = code === 'End' || key === 'End';
@@ -170,7 +173,7 @@ const useCioAutocomplete = (options) => {
170
173
  },
171
174
  className: 'cio-form',
172
175
  'data-testid': 'cio-form',
173
- 'data-cnstrc-search-form': '',
176
+ [dataAttributeHelpers_1.cnstrcDataAttrs.autocomplete.inputForm]: '',
174
177
  }),
175
178
  getSectionProps: (section) => {
176
179
  var _a, _b;
@@ -207,16 +210,18 @@ const useCioAutocomplete = (options) => {
207
210
  className: `cio-section cio-section-${sectionListingType} ${getDeprecatedClassNames()}`,
208
211
  ref: section.ref,
209
212
  role: 'none',
210
- 'data-cnstrc-section': (_b = section.data[0]) === null || _b === void 0 ? void 0 : _b.section,
213
+ [dataAttributeHelpers_1.cnstrcDataAttrs.common.section]: (_b = section.data[0]) === null || _b === void 0 ? void 0 : _b.section,
211
214
  };
212
215
  if ((0, typeGuards_1.isCustomSection)(section)) {
213
- attributes['data-cnstrc-custom-section'] = true;
214
- attributes['data-cnstrc-custom-section-name'] = section.displayName;
216
+ attributes[dataAttributeHelpers_1.cnstrcDataAttrs.custom.customSection] = true;
217
+ attributes[dataAttributeHelpers_1.cnstrcDataAttrs.custom.customSectionName] = section.displayName;
215
218
  }
216
- // Add data attributes for recommendations
219
+ // Add data attributes for recommendations using helper
217
220
  if ((0, typeGuards_1.isRecommendationsSection)(section)) {
218
- attributes['data-cnstrc-recommendations'] = true;
219
- attributes['data-cnstrc-recommendations-pod-id'] = section.podId;
221
+ const podData = podsData === null || podsData === void 0 ? void 0 : podsData[section.podId];
222
+ const seedItems = (0, dataAttributeHelpers_1.normalizeSeedItems)(section.itemIds);
223
+ const recommendationAttributes = (0, dataAttributeHelpers_1.getRecommendationsSectionCnstrcDataAttributes)(section, podData === null || podData === void 0 ? void 0 : podData.resultId, podData === null || podData === void 0 ? void 0 : podData.numResults, seedItems);
224
+ Object.assign(attributes, recommendationAttributes);
220
225
  }
221
226
  return attributes;
222
227
  },
@@ -14,7 +14,8 @@ const useFetchRecommendationPod = (cioClient, recommendationPods, fetchZeroState
14
14
  }));
15
15
  const recommendationsPodResults = {};
16
16
  const recommendationsPodsData = {};
17
- responses.forEach(({ response, request }, index) => {
17
+ responses.forEach(({ response, request, result_id: resultId }, index) => {
18
+ var _a;
18
19
  const { pod, results } = response;
19
20
  if (pod === null || pod === void 0 ? void 0 : pod.id) {
20
21
  recommendationsPodResults[pod.id] = results === null || results === void 0 ? void 0 : results.map((item) => {
@@ -25,6 +26,8 @@ const useFetchRecommendationPod = (cioClient, recommendationPods, fetchZeroState
25
26
  displayName: pod.display_name,
26
27
  podId: pod.id,
27
28
  request,
29
+ resultId,
30
+ numResults: (_a = response.total_num_results) !== null && _a !== void 0 ? _a : 0,
28
31
  };
29
32
  }
30
33
  });
@@ -33,6 +33,7 @@ function useSections(query, cioClient, sections, zeroStateSections, advancedPara
33
33
  zeroStateActiveSections: showZeroStateSections,
34
34
  request: autocomplete.request,
35
35
  totalNumResultsPerSection: autocomplete.totalNumResultsPerSection,
36
+ podsData: recommendations.podsData,
36
37
  };
37
38
  }
38
39
  exports.default = useSections;
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getConversionButtonCnstrcDataAttributes = exports.getRecommendationsSectionCnstrcDataAttributes = exports.getItemCnstrcDataAttributes = exports.normalizeSeedItems = exports.cnstrcDataAttrs = void 0;
4
+ const typeGuards_1 = require("../typeGuards");
5
+ exports.cnstrcDataAttrs = {
6
+ common: {
7
+ itemId: 'data-cnstrc-item-id',
8
+ itemName: 'data-cnstrc-item-name',
9
+ itemPrice: 'data-cnstrc-item-price',
10
+ variationId: 'data-cnstrc-item-variation-id',
11
+ numResults: 'data-cnstrc-num-results',
12
+ conversionBtn: 'data-cnstrc-btn',
13
+ resultId: 'data-cnstrc-result-id',
14
+ itemSection: 'data-cnstrc-item-section',
15
+ itemGroup: 'data-cnstrc-item-group',
16
+ slCampaignId: 'data-cnstrc-sl-campaign-id',
17
+ slCampaignOwner: 'data-cnstrc-sl-campaign-owner',
18
+ section: 'data-cnstrc-section',
19
+ },
20
+ autocomplete: {
21
+ input: 'data-cnstrc-search-input',
22
+ inputForm: 'data-cnstrc-search-form',
23
+ searchSubmitButton: 'data-cnstrc-search-submit-btn',
24
+ autocompleteContainer: 'data-cnstrc-autosuggest',
25
+ },
26
+ recommendations: {
27
+ recommendationsContainer: 'data-cnstrc-recommendations',
28
+ recommendationsPodId: 'data-cnstrc-recommendations-pod-id',
29
+ item: 'data-cnstrc-item',
30
+ strategyId: 'data-cnstrc-strategy-id',
31
+ seedItems: 'data-cnstrc-recommendations-seed-items',
32
+ },
33
+ custom: {
34
+ customSection: 'data-cnstrc-custom-section',
35
+ customSectionName: 'data-cnstrc-custom-section-name',
36
+ },
37
+ };
38
+ /**
39
+ * Normalize itemIds to array format for seed items
40
+ */
41
+ function normalizeSeedItems(itemIds) {
42
+ if (!itemIds)
43
+ return undefined;
44
+ return Array.isArray(itemIds) ? itemIds : [itemIds];
45
+ }
46
+ exports.normalizeSeedItems = normalizeSeedItems;
47
+ /**
48
+ * Get data attributes for autocomplete/recommendation items
49
+ *
50
+ * @param item - The item to generate data attributes for
51
+ * @returns An object containing data attributes to be spread onto the element
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * // For autocomplete items
56
+ * const itemProps = getItemCnstrcDataAttributes(searchSuggestionItem);
57
+ * <li {...itemProps}>Search Suggestion</li>
58
+ *
59
+ * // For recommendation items
60
+ * const recItemProps = getItemCnstrcDataAttributes(recommendationItem);
61
+ * <div {...recItemProps}>Recommended Product</div>
62
+ * ```
63
+ */
64
+ function getItemCnstrcDataAttributes(item) {
65
+ var _a, _b, _c, _d;
66
+ const dataCnstrc = {
67
+ [exports.cnstrcDataAttrs.common.itemSection]: item.section,
68
+ [exports.cnstrcDataAttrs.common.itemName]: item.value,
69
+ };
70
+ // Add item ID only for non-Search Suggestions
71
+ if (!(0, typeGuards_1.isSearchSuggestion)(item) && ((_a = item.data) === null || _a === void 0 ? void 0 : _a.id)) {
72
+ dataCnstrc[exports.cnstrcDataAttrs.common.itemId] = item.data.id;
73
+ }
74
+ // Add variation ID if exists
75
+ if ((_b = item.data) === null || _b === void 0 ? void 0 : _b.variation_id) {
76
+ dataCnstrc[exports.cnstrcDataAttrs.common.variationId] = item.data.variation_id;
77
+ }
78
+ // Add group ID for in-group suggestions
79
+ if ((0, typeGuards_1.isInGroupSuggestion)(item)) {
80
+ dataCnstrc[exports.cnstrcDataAttrs.common.itemGroup] = item.groupId;
81
+ }
82
+ // Add recommendation-specific attributes (check if item has podId and strategy)
83
+ if (item.podId && item.strategy) {
84
+ dataCnstrc[exports.cnstrcDataAttrs.recommendations.item] = 'recommendation';
85
+ // Add strategy ID if available
86
+ if (item.strategy.id) {
87
+ dataCnstrc[exports.cnstrcDataAttrs.recommendations.strategyId] = item.strategy.id;
88
+ }
89
+ }
90
+ // Add sponsored listing data if available
91
+ if ((_c = item.labels) === null || _c === void 0 ? void 0 : _c.sl_campaign_id) {
92
+ dataCnstrc[exports.cnstrcDataAttrs.common.slCampaignId] = String(item.labels.sl_campaign_id);
93
+ }
94
+ if ((_d = item.labels) === null || _d === void 0 ? void 0 : _d.sl_campaign_owner) {
95
+ dataCnstrc[exports.cnstrcDataAttrs.common.slCampaignOwner] = String(item.labels.sl_campaign_owner);
96
+ }
97
+ return dataCnstrc;
98
+ }
99
+ exports.getItemCnstrcDataAttributes = getItemCnstrcDataAttributes;
100
+ /**
101
+ * Get data attributes for recommendation sections
102
+ */
103
+ function getRecommendationsSectionCnstrcDataAttributes(section, resultId, numResults, seedItems) {
104
+ if (!(0, typeGuards_1.isRecommendationsSection)(section)) {
105
+ return {};
106
+ }
107
+ const dataCnstrc = {
108
+ [exports.cnstrcDataAttrs.recommendations.recommendationsContainer]: true,
109
+ [exports.cnstrcDataAttrs.recommendations.recommendationsPodId]: section.podId,
110
+ };
111
+ // Add result ID if available
112
+ if (resultId) {
113
+ dataCnstrc[exports.cnstrcDataAttrs.common.resultId] = resultId;
114
+ }
115
+ // Add number of results
116
+ if (numResults !== undefined) {
117
+ dataCnstrc[exports.cnstrcDataAttrs.common.numResults] = numResults;
118
+ }
119
+ // Add seed items if available
120
+ if (seedItems && seedItems.length > 0) {
121
+ dataCnstrc[exports.cnstrcDataAttrs.recommendations.seedItems] = seedItems.join(',');
122
+ }
123
+ return dataCnstrc;
124
+ }
125
+ exports.getRecommendationsSectionCnstrcDataAttributes = getRecommendationsSectionCnstrcDataAttributes;
126
+ /**
127
+ * Get data attributes for conversion buttons
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * <button {...getConversionButtonCnstrcDataAttributes('add_to_cart')}>
132
+ * Add to Cart
133
+ * </button>
134
+ * ```
135
+ */
136
+ function getConversionButtonCnstrcDataAttributes(conversionType) {
137
+ return {
138
+ [exports.cnstrcDataAttrs.common.conversionBtn]: conversionType,
139
+ };
140
+ }
141
+ exports.getConversionButtonCnstrcDataAttributes = getConversionButtonCnstrcDataAttributes;
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = '1.28.2';
3
+ exports.default = '1.29.0';
@@ -1,5 +1,6 @@
1
1
  import React, { useContext } from 'react';
2
2
  import { CioAutocompleteContext } from '../CioAutocompleteProvider';
3
+ import { cnstrcDataAttrs } from '../../../utils/dataAttributeHelpers';
3
4
  function DefaultRenderInput({ getFormProps, getInputProps, getLabelProps, setQuery }) {
4
5
  const inputProps = getInputProps();
5
6
  return (React.createElement("form", { ...getFormProps() },
@@ -14,7 +15,7 @@ function DefaultRenderInput({ getFormProps, getInputProps, getLabelProps, setQue
14
15
  React.createElement("div", { className: 'cio-icon' },
15
16
  React.createElement("svg", { stroke: 'currentColor', fill: 'currentColor', strokeWidth: '0', viewBox: '0 0 512 512', height: '1em', width: '1em', xmlns: 'http://www.w3.org/2000/svg' },
16
17
  React.createElement("path", { d: 'M289.94 256l95-95A24 24 0 00351 127l-95 95-95-95a24 24 0 00-34 34l95 95-95 95a24 24 0 1034 34l95-95 95 95a24 24 0 0034-34z' })))),
17
- React.createElement("button", { className: 'cio-submit-btn', "data-testid": 'cio-submit-btn', "data-cnstrc-search-submit-btn": true, onClick: (e) => {
18
+ React.createElement("button", { className: 'cio-submit-btn', "data-testid": 'cio-submit-btn', ...{ [cnstrcDataAttrs.autocomplete.searchSubmitButton]: '' }, onClick: (e) => {
18
19
  if (!inputProps.value) {
19
20
  e.preventDefault();
20
21
  if (inputProps.id) {
@@ -5,6 +5,7 @@ import { translate } from '../../../utils/helpers';
5
5
  import { camelToStartCase } from '../../../utils/format';
6
6
  import { CioAutocompleteContext } from '../CioAutocompleteProvider';
7
7
  import NoResults from '../AutocompleteResults/NoResults';
8
+ import { cnstrcDataAttrs } from '../../../utils/dataAttributeHelpers';
8
9
  // eslint-disable-next-line func-names
9
10
  const DefaultRenderSectionItemsList = function ({ section }) {
10
11
  const { getSectionProps, query, getFormProps, advancedParameters } = useContext(CioAutocompleteContext);
@@ -47,7 +48,7 @@ const DefaultRenderSectionItemsList = function ({ section }) {
47
48
  displayShowAllResultsButton &&
48
49
  (typeof type === 'undefined' || type === 'autocomplete') &&
49
50
  section.indexSectionName === 'Products' && (React.createElement("div", { className: 'cio-section-footer' },
50
- React.createElement("button", { "data-cnstrc-search-submit-btn": true, className: 'cio-show-all-results-button', type: 'button', onClick: onSubmit }, translate('show all results', translations))))));
51
+ React.createElement("button", { ...{ [cnstrcDataAttrs.autocomplete.searchSubmitButton]: '' }, className: 'cio-show-all-results-button', type: 'button', onClick: onSubmit }, translate('show all results', translations))))));
51
52
  };
52
53
  export default function SectionItemsList(props) {
53
54
  const { section, children = DefaultRenderSectionItemsList } = props;
@@ -13,6 +13,7 @@ import useRecommendationsObserver from './useRecommendationsObserver';
13
13
  import { isCustomSection, isRecommendationsSection } from '../typeGuards';
14
14
  import useNormalizedProps from './useNormalizedProps';
15
15
  import useCustomBlur from './useCustomBlur';
16
+ import { getItemCnstrcDataAttributes, getRecommendationsSectionCnstrcDataAttributes, cnstrcDataAttrs, normalizeSeedItems, } from '../utils/dataAttributeHelpers';
16
17
  import { shopifyDefaults } from '../utils/shopifyDefaults';
17
18
  export const defaultSections = [
18
19
  {
@@ -50,7 +51,7 @@ const useCioAutocomplete = (options) => {
50
51
  cioJsClientOptions: options.cioJsClientOptions,
51
52
  });
52
53
  // Get autocomplete sections (autocomplete + recommendations + custom)
53
- const { fetchRecommendationResults, activeSections, activeSectionsWithData, zeroStateActiveSections, request, totalNumResultsPerSection, } = useSections(query, cioClient, sections, zeroStateSections, advancedParameters);
54
+ const { fetchRecommendationResults, activeSections, activeSectionsWithData, zeroStateActiveSections, request, totalNumResultsPerSection, podsData, } = useSections(query, cioClient, sections, zeroStateSections, advancedParameters);
54
55
  const features = useMemo(() => getFeatures(request), [request]);
55
56
  // Get dropdown items array from active sections (autocomplete + recommendations + custom)
56
57
  const items = useMemo(() => getItemsForActiveSections(activeSectionsWithData), [activeSectionsWithData]);
@@ -78,7 +79,7 @@ const useCioAutocomplete = (options) => {
78
79
  ...getMenuProps(),
79
80
  className: 'cio-results',
80
81
  'data-testid': 'cio-results',
81
- 'data-cnstrc-autosuggest': '',
82
+ [cnstrcDataAttrs.autocomplete.autocompleteContainer]: '',
82
83
  }),
83
84
  getLabelProps,
84
85
  openMenu,
@@ -100,17 +101,14 @@ const useCioAutocomplete = (options) => {
100
101
  }
101
102
  },
102
103
  };
104
+ // Get all data attributes using the helper function
105
+ const dataAttributes = getItemCnstrcDataAttributes(item);
103
106
  return {
104
107
  ...getItemPropsDownShift({ item, index }),
105
108
  // @deprecated `sectionItemTestId` will be removed as a className in the next major version
106
109
  className: `cio-item ${sectionItemTestId}`,
107
110
  'data-testid': sectionItemTestId,
108
- 'data-cnstrc-item-section': item.section,
109
- 'data-cnstrc-item-group': item.groupId,
110
- 'data-cnstrc-item-name': item.value,
111
- 'data-cnstrc-item-id': item.data?.id,
112
- 'data-cnstrc-sl-campaign-id': item.labels?.sl_campaign_id,
113
- 'data-cnstrc-sl-campaign-owner': item.labels?.sl_campaign_owner,
111
+ ...dataAttributes,
114
112
  ...(hasLink ? {} : nonInteractiveItemsProps),
115
113
  };
116
114
  },
@@ -149,7 +147,7 @@ const useCioAutocomplete = (options) => {
149
147
  },
150
148
  className: 'cio-input',
151
149
  'data-testid': 'cio-input',
152
- 'data-cnstrc-search-input': '',
150
+ [cnstrcDataAttrs.autocomplete.input]: '',
153
151
  placeholder,
154
152
  onKeyDownCapture: ({ code, key, nativeEvent }) => {
155
153
  const isEnter = code === 'Enter' || key === 'Enter';
@@ -191,7 +189,7 @@ const useCioAutocomplete = (options) => {
191
189
  },
192
190
  className: 'cio-form',
193
191
  'data-testid': 'cio-form',
194
- 'data-cnstrc-search-form': '',
192
+ [cnstrcDataAttrs.autocomplete.inputForm]: '',
195
193
  }),
196
194
  getSectionProps: (section) => {
197
195
  // @deprecated ClassNames derived from this fn will be removed in the next major version
@@ -227,16 +225,18 @@ const useCioAutocomplete = (options) => {
227
225
  className: `cio-section cio-section-${sectionListingType} ${getDeprecatedClassNames()}`,
228
226
  ref: section.ref,
229
227
  role: 'none',
230
- 'data-cnstrc-section': section.data[0]?.section,
228
+ [cnstrcDataAttrs.common.section]: section.data[0]?.section,
231
229
  };
232
230
  if (isCustomSection(section)) {
233
- attributes['data-cnstrc-custom-section'] = true;
234
- attributes['data-cnstrc-custom-section-name'] = section.displayName;
231
+ attributes[cnstrcDataAttrs.custom.customSection] = true;
232
+ attributes[cnstrcDataAttrs.custom.customSectionName] = section.displayName;
235
233
  }
236
- // Add data attributes for recommendations
234
+ // Add data attributes for recommendations using helper
237
235
  if (isRecommendationsSection(section)) {
238
- attributes['data-cnstrc-recommendations'] = true;
239
- attributes['data-cnstrc-recommendations-pod-id'] = section.podId;
236
+ const podData = podsData?.[section.podId];
237
+ const seedItems = normalizeSeedItems(section.itemIds);
238
+ const recommendationAttributes = getRecommendationsSectionCnstrcDataAttributes(section, podData?.resultId, podData?.numResults, seedItems);
239
+ Object.assign(attributes, recommendationAttributes);
240
240
  }
241
241
  return attributes;
242
242
  },
@@ -11,7 +11,7 @@ const useFetchRecommendationPod = (cioClient, recommendationPods, fetchZeroState
11
11
  })));
12
12
  const recommendationsPodResults = {};
13
13
  const recommendationsPodsData = {};
14
- responses.forEach(({ response, request }, index) => {
14
+ responses.forEach(({ response, request, result_id: resultId }, index) => {
15
15
  const { pod, results } = response;
16
16
  if (pod?.id) {
17
17
  recommendationsPodResults[pod.id] = results?.map((item) => ({
@@ -24,6 +24,8 @@ const useFetchRecommendationPod = (cioClient, recommendationPods, fetchZeroState
24
24
  displayName: pod.display_name,
25
25
  podId: pod.id,
26
26
  request,
27
+ resultId,
28
+ numResults: response.total_num_results ?? 0,
27
29
  };
28
30
  }
29
31
  });
@@ -30,5 +30,6 @@ export default function useSections(query, cioClient, sections, zeroStateSection
30
30
  zeroStateActiveSections: showZeroStateSections,
31
31
  request: autocomplete.request,
32
32
  totalNumResultsPerSection: autocomplete.totalNumResultsPerSection,
33
+ podsData: recommendations.podsData,
33
34
  };
34
35
  }
@@ -0,0 +1,133 @@
1
+ import { isInGroupSuggestion, isRecommendationsSection, isSearchSuggestion } from '../typeGuards';
2
+ export const cnstrcDataAttrs = {
3
+ common: {
4
+ itemId: 'data-cnstrc-item-id',
5
+ itemName: 'data-cnstrc-item-name',
6
+ itemPrice: 'data-cnstrc-item-price',
7
+ variationId: 'data-cnstrc-item-variation-id',
8
+ numResults: 'data-cnstrc-num-results',
9
+ conversionBtn: 'data-cnstrc-btn',
10
+ resultId: 'data-cnstrc-result-id',
11
+ itemSection: 'data-cnstrc-item-section',
12
+ itemGroup: 'data-cnstrc-item-group',
13
+ slCampaignId: 'data-cnstrc-sl-campaign-id',
14
+ slCampaignOwner: 'data-cnstrc-sl-campaign-owner',
15
+ section: 'data-cnstrc-section',
16
+ },
17
+ autocomplete: {
18
+ input: 'data-cnstrc-search-input',
19
+ inputForm: 'data-cnstrc-search-form',
20
+ searchSubmitButton: 'data-cnstrc-search-submit-btn',
21
+ autocompleteContainer: 'data-cnstrc-autosuggest',
22
+ },
23
+ recommendations: {
24
+ recommendationsContainer: 'data-cnstrc-recommendations',
25
+ recommendationsPodId: 'data-cnstrc-recommendations-pod-id',
26
+ item: 'data-cnstrc-item',
27
+ strategyId: 'data-cnstrc-strategy-id',
28
+ seedItems: 'data-cnstrc-recommendations-seed-items',
29
+ },
30
+ custom: {
31
+ customSection: 'data-cnstrc-custom-section',
32
+ customSectionName: 'data-cnstrc-custom-section-name',
33
+ },
34
+ };
35
+ /**
36
+ * Normalize itemIds to array format for seed items
37
+ */
38
+ export function normalizeSeedItems(itemIds) {
39
+ if (!itemIds)
40
+ return undefined;
41
+ return Array.isArray(itemIds) ? itemIds : [itemIds];
42
+ }
43
+ /**
44
+ * Get data attributes for autocomplete/recommendation items
45
+ *
46
+ * @param item - The item to generate data attributes for
47
+ * @returns An object containing data attributes to be spread onto the element
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * // For autocomplete items
52
+ * const itemProps = getItemCnstrcDataAttributes(searchSuggestionItem);
53
+ * <li {...itemProps}>Search Suggestion</li>
54
+ *
55
+ * // For recommendation items
56
+ * const recItemProps = getItemCnstrcDataAttributes(recommendationItem);
57
+ * <div {...recItemProps}>Recommended Product</div>
58
+ * ```
59
+ */
60
+ export function getItemCnstrcDataAttributes(item) {
61
+ const dataCnstrc = {
62
+ [cnstrcDataAttrs.common.itemSection]: item.section,
63
+ [cnstrcDataAttrs.common.itemName]: item.value,
64
+ };
65
+ // Add item ID only for non-Search Suggestions
66
+ if (!isSearchSuggestion(item) && item.data?.id) {
67
+ dataCnstrc[cnstrcDataAttrs.common.itemId] = item.data.id;
68
+ }
69
+ // Add variation ID if exists
70
+ if (item.data?.variation_id) {
71
+ dataCnstrc[cnstrcDataAttrs.common.variationId] = item.data.variation_id;
72
+ }
73
+ // Add group ID for in-group suggestions
74
+ if (isInGroupSuggestion(item)) {
75
+ dataCnstrc[cnstrcDataAttrs.common.itemGroup] = item.groupId;
76
+ }
77
+ // Add recommendation-specific attributes (check if item has podId and strategy)
78
+ if (item.podId && item.strategy) {
79
+ dataCnstrc[cnstrcDataAttrs.recommendations.item] = 'recommendation';
80
+ // Add strategy ID if available
81
+ if (item.strategy.id) {
82
+ dataCnstrc[cnstrcDataAttrs.recommendations.strategyId] = item.strategy.id;
83
+ }
84
+ }
85
+ // Add sponsored listing data if available
86
+ if (item.labels?.sl_campaign_id) {
87
+ dataCnstrc[cnstrcDataAttrs.common.slCampaignId] = String(item.labels.sl_campaign_id);
88
+ }
89
+ if (item.labels?.sl_campaign_owner) {
90
+ dataCnstrc[cnstrcDataAttrs.common.slCampaignOwner] = String(item.labels.sl_campaign_owner);
91
+ }
92
+ return dataCnstrc;
93
+ }
94
+ /**
95
+ * Get data attributes for recommendation sections
96
+ */
97
+ export function getRecommendationsSectionCnstrcDataAttributes(section, resultId, numResults, seedItems) {
98
+ if (!isRecommendationsSection(section)) {
99
+ return {};
100
+ }
101
+ const dataCnstrc = {
102
+ [cnstrcDataAttrs.recommendations.recommendationsContainer]: true,
103
+ [cnstrcDataAttrs.recommendations.recommendationsPodId]: section.podId,
104
+ };
105
+ // Add result ID if available
106
+ if (resultId) {
107
+ dataCnstrc[cnstrcDataAttrs.common.resultId] = resultId;
108
+ }
109
+ // Add number of results
110
+ if (numResults !== undefined) {
111
+ dataCnstrc[cnstrcDataAttrs.common.numResults] = numResults;
112
+ }
113
+ // Add seed items if available
114
+ if (seedItems && seedItems.length > 0) {
115
+ dataCnstrc[cnstrcDataAttrs.recommendations.seedItems] = seedItems.join(',');
116
+ }
117
+ return dataCnstrc;
118
+ }
119
+ /**
120
+ * Get data attributes for conversion buttons
121
+ *
122
+ * @example
123
+ * ```tsx
124
+ * <button {...getConversionButtonCnstrcDataAttributes('add_to_cart')}>
125
+ * Add to Cart
126
+ * </button>
127
+ * ```
128
+ */
129
+ export function getConversionButtonCnstrcDataAttributes(conversionType) {
130
+ return {
131
+ [cnstrcDataAttrs.common.conversionBtn]: conversionType,
132
+ };
133
+ }
@@ -1 +1 @@
1
- export default '1.28.2';
1
+ export default '1.29.0';
@@ -18,12 +18,14 @@ export declare const CioAutocompleteContext: React.Context<{
18
18
  getItemProps: (item: import("../../types").Item) => any;
19
19
  getInputProps: () => any;
20
20
  getFormProps: () => {
21
+ [x: string]: string | ((event: any) => {
22
+ query: string;
23
+ });
21
24
  onSubmit: (event: any) => {
22
25
  query: string;
23
26
  };
24
27
  className: string;
25
28
  'data-testid': string;
26
- 'data-cnstrc-search-form': string;
27
29
  };
28
30
  getSectionProps: (section: import("../../types").Section) => import("../../types").HTMLPropsWithCioDataAttributes<any>;
29
31
  setQuery: React.Dispatch<React.SetStateAction<string>>;
@@ -18,12 +18,14 @@ declare const useCioAutocomplete: (options: UseCioAutocompleteOptions) => {
18
18
  getItemProps: (item: Item) => any;
19
19
  getInputProps: () => any;
20
20
  getFormProps: () => {
21
+ [x: string]: string | ((event: any) => {
22
+ query: string;
23
+ });
21
24
  onSubmit: (event: any) => {
22
25
  query: string;
23
26
  };
24
27
  className: string;
25
28
  'data-testid': string;
26
- 'data-cnstrc-search-form': string;
27
29
  };
28
30
  getSectionProps: (section: Section) => HTMLPropsWithCioDataAttributes<any>;
29
31
  setQuery: import("react").Dispatch<import("react").SetStateAction<string>>;
@@ -1,6 +1,6 @@
1
1
  import ConstructorIO from '@constructor-io/constructorio-client-javascript';
2
2
  import { Nullable } from '@constructor-io/constructorio-client-javascript/lib/types';
3
- import { AdvancedParameters, UserDefinedSection } from '../../types';
3
+ import { AdvancedParameters, PodData, UserDefinedSection } from '../../types';
4
4
  export default function useSections(query: string, cioClient: Nullable<ConstructorIO>, sections: UserDefinedSection[], zeroStateSections?: UserDefinedSection[], advancedParameters?: AdvancedParameters): {
5
5
  fetchRecommendationResults: () => Promise<void>;
6
6
  activeSections: UserDefinedSection[];
@@ -8,4 +8,5 @@ export default function useSections(query: string, cioClient: Nullable<Construct
8
8
  zeroStateActiveSections: boolean;
9
9
  request: Partial<import("@constructor-io/constructorio-client-javascript").AutocompleteRequestType>;
10
10
  totalNumResultsPerSection: Record<string, number>;
11
+ podsData: Record<string, PodData>;
11
12
  };