@searchspring/snap-preact-components 0.73.7 → 0.74.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.
@@ -4,7 +4,9 @@ export interface UseIntersectionOptions {
4
4
  fireOnce?: boolean;
5
5
  threshold?: number | number[];
6
6
  minVisibleTime?: number;
7
- resetKey?: string;
8
7
  }
9
- export declare const useIntersectionAdvanced: (ref: MutableRef<HTMLElement | null>, options?: UseIntersectionOptions) => boolean;
8
+ export declare const useIntersectionAdvanced: (ref: MutableRef<HTMLElement | null>, options?: UseIntersectionOptions) => {
9
+ inViewport: boolean;
10
+ updateRef: (el: HTMLElement | null) => void;
11
+ };
10
12
  //# sourceMappingURL=useIntersectionAdvanced.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useIntersectionAdvanced.d.ts","sourceRoot":"","sources":["../../../src/hooks/useIntersectionAdvanced.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,UAAU,EAAE,MAAM,cAAc,CAAC;AAEvE,MAAM,WAAW,sBAAsB;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,eAAO,MAAM,uBAAuB,QAAS,WAAW,WAAW,GAAG,IAAI,CAAC,YAAW,sBAAsB,KAAQ,OAsInH,CAAC"}
1
+ {"version":3,"file":"useIntersectionAdvanced.d.ts","sourceRoot":"","sources":["../../../src/hooks/useIntersectionAdvanced.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA4C,UAAU,EAAE,MAAM,cAAc,CAAC;AAEpF,MAAM,WAAW,sBAAsB;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAGD,eAAO,MAAM,uBAAuB,QAC9B,WAAW,WAAW,GAAG,IAAI,CAAC,YAC1B,sBAAsB;gBACf,OAAO;oBAAkB,WAAW,GAAG,IAAI,KAAK,IAAI;CA6HpE,CAAC"}
@@ -5,26 +5,18 @@ var hooks_1 = require("preact/hooks");
5
5
  var VISIBILITY_POLL_INTERVAL = 250;
6
6
  var useIntersectionAdvanced = function (ref, options) {
7
7
  if (options === void 0) { options = {}; }
8
- var _a = options.rootMargin, rootMargin = _a === void 0 ? '0px' : _a, _b = options.fireOnce, fireOnce = _b === void 0 ? false : _b, _c = options.threshold, threshold = _c === void 0 ? 0 : _c, _d = options.minVisibleTime, minVisibleTime = _d === void 0 ? 0 : _d, resetKey = options.resetKey;
8
+ var _a = options.rootMargin, rootMargin = _a === void 0 ? '0px' : _a, _b = options.fireOnce, fireOnce = _b === void 0 ? false : _b, _c = options.threshold, threshold = _c === void 0 ? 0 : _c, _d = options.minVisibleTime, minVisibleTime = _d === void 0 ? 0 : _d;
9
9
  // State and setter for storing whether element is visible
10
10
  var _e = (0, hooks_1.useState)(false), isIntersecting = _e[0], setIntersecting = _e[1];
11
11
  // Timer reference to track visibility duration
12
12
  var visibleTimerRef = (0, hooks_1.useRef)(null);
13
13
  // Track when the element started being visible
14
14
  var visibleStartRef = (0, hooks_1.useRef)(null);
15
- // Track the last reset key to detect changes
16
- var lastResetKeyRef = (0, hooks_1.useRef)(resetKey);
17
- // Reset state if resetKey has changed
18
- if (resetKey !== lastResetKeyRef.current) {
19
- setIntersecting(false);
20
- if (visibleTimerRef.current) {
21
- window.clearTimeout(visibleTimerRef.current);
22
- visibleTimerRef.current = null;
23
- }
24
- visibleStartRef.current = null;
25
- lastResetKeyRef.current = resetKey;
26
- return false;
27
- }
15
+ var _f = (0, hooks_1.useState)(0), counter = _f[0], setCounter = _f[1];
16
+ var updateRef = (0, hooks_1.useCallback)(function (el) {
17
+ ref.current = el;
18
+ setCounter(function (c) { return c + 1; });
19
+ }, []);
28
20
  (0, hooks_1.useEffect)(function () {
29
21
  setIntersecting(false);
30
22
  var observer = null;
@@ -119,8 +111,8 @@ var useIntersectionAdvanced = function (ref, options) {
119
111
  observer.unobserve(ref.current);
120
112
  }
121
113
  };
122
- }, [ref, resetKey]);
123
- return isIntersecting;
114
+ }, [ref, counter]);
115
+ return { inViewport: isIntersecting, updateRef: updateRef };
124
116
  };
125
117
  exports.useIntersectionAdvanced = useIntersectionAdvanced;
126
118
  function elementIsVisible(el) {
@@ -1 +1 @@
1
- {"version":3,"file":"withTracking.d.ts","sourceRoot":"","sources":["../../../src/providers/withTracking.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAK,aAAa,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAA8B,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACjG,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACxH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAIhF,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAC/C,UAAU,iBAAiB;IAC1B,UAAU,CAAC,EAAE,gBAAgB,GAAG,sBAAsB,GAAG,wBAAwB,CAAC;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,KAAK,SAAS,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,CAAC,KAAK,CAAC,4BAiFnG"}
1
+ {"version":3,"file":"withTracking.d.ts","sourceRoot":"","sources":["../../../src/providers/withTracking.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAK,aAAa,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAA8B,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACjG,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACxH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAIhF,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAC/C,UAAU,iBAAiB;IAC1B,UAAU,CAAC,EAAE,gBAAgB,GAAG,sBAAsB,GAAG,wBAAwB,CAAC;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,KAAK,SAAS,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,CAAC,KAAK,CAAC,4BAgGnG"}
@@ -29,7 +29,7 @@ var hooks_1 = require("preact/hooks");
29
29
  exports.TRACKING_ATTRIBUTE = 'sstracking';
30
30
  function withTracking(WrappedComponent) {
31
31
  var WithTracking = function (props) {
32
- var _a;
32
+ var _a, _b;
33
33
  var controller = props.controller, result = props.result, banner = props.banner, type = props.type, content = props.content, restProps = __rest(props, ["controller", "result", "banner", "type", "content"]);
34
34
  if (props.trackingRef) {
35
35
  // case where withTracking may get used more than once
@@ -41,29 +41,34 @@ function withTracking(WrappedComponent) {
41
41
  if (!result && !banner && (!type || !content)) {
42
42
  console.warn('Warning: No result or banner provided to withTracking');
43
43
  }
44
- var resetKey;
45
- if ((controller === null || controller === void 0 ? void 0 : controller.type) === 'search' || (controller === null || controller === void 0 ? void 0 : controller.type) === 'autocomplete') {
46
- var urlManager = controller.urlManager;
47
- resetKey = JSON.stringify({
48
- q: urlManager.state.query,
49
- p: urlManager.state.page,
50
- ps: urlManager.state.pageSize,
51
- s: urlManager.state.sort,
52
- f: urlManager.state.filter,
53
- });
44
+ var _c = (0, utilities_1.createImpressionObserver)(), ref = _c.ref, inViewport = _c.inViewport, updateRef = _c.updateRef;
45
+ // Reset impression tracking when the result identity changes (e.g. new search context).
46
+ // Each Product/Banner gets a new responseId per search response, so this naturally
47
+ // resets when query/sort/filters change without needing global controller state.
48
+ // Calling updateRef(ref.current) re-observes the same element with fresh state.
49
+ var resultIdentity = (_b = (result || banner || (type && ((_a = content === null || content === void 0 ? void 0 : content[type]) === null || _a === void 0 ? void 0 : _a[0])))) === null || _b === void 0 ? void 0 : _b.responseId;
50
+ var prevIdentityRef = (0, hooks_1.useRef)(resultIdentity);
51
+ // Tracks whether we're waiting for the observer to reset after an identity change.
52
+ // Set synchronously during render to block impressions immediately when identity
53
+ // changes, preventing a stale inViewport=true from firing before the observer resets.
54
+ var awaitingReobservationRef = (0, hooks_1.useRef)(false);
55
+ if (prevIdentityRef.current !== resultIdentity) {
56
+ awaitingReobservationRef.current = true;
54
57
  }
55
- else if ((controller === null || controller === void 0 ? void 0 : controller.type) === 'recommendation') {
56
- // For recommendations, use a combination of tag and other relevant state
57
- var recStore = controller.store;
58
- resetKey = JSON.stringify({
59
- tag: (_a = recStore.profile) === null || _a === void 0 ? void 0 : _a.tag,
60
- ids: recStore.results.map(function (result) { return result.id; }).join(','),
61
- });
62
- }
63
- var _b = (0, utilities_1.createImpressionObserver)({ resetKey: resetKey }), ref = _b.ref, inViewport = _b.inViewport;
64
- if (inViewport) {
65
- // TODO: add support for disabling tracking events via config like in ResultTracker
66
- if (type && content && !result && ['search', 'autocomplete'].includes((controller === null || controller === void 0 ? void 0 : controller.type) || '')) {
58
+ (0, hooks_1.useEffect)(function () {
59
+ if (prevIdentityRef.current !== resultIdentity) {
60
+ prevIdentityRef.current = resultIdentity;
61
+ updateRef(ref.current);
62
+ }
63
+ }, [resultIdentity, updateRef]);
64
+ (0, hooks_1.useEffect)(function () {
65
+ if (awaitingReobservationRef.current && !inViewport) {
66
+ awaitingReobservationRef.current = false;
67
+ }
68
+ }, [inViewport, resultIdentity]);
69
+ var isBannerTracking = type && content && !result && ['search', 'autocomplete'].includes((controller === null || controller === void 0 ? void 0 : controller.type) || '');
70
+ if (inViewport && !awaitingReobservationRef.current) {
71
+ if (isBannerTracking) {
67
72
  controller === null || controller === void 0 ? void 0 : controller.track.banner.impression(content[type][0]);
68
73
  }
69
74
  else if (!(result === null || result === void 0 ? void 0 : result.bundleSeed)) {
@@ -71,7 +76,7 @@ function withTracking(WrappedComponent) {
71
76
  }
72
77
  }
73
78
  var handleClick = (0, hooks_1.useCallback)(function (e) {
74
- if (type && content && !result && ['search', 'autocomplete'].includes((controller === null || controller === void 0 ? void 0 : controller.type) || '')) {
79
+ if (isBannerTracking) {
75
80
  controller === null || controller === void 0 ? void 0 : controller.track.banner.click(e, content[type][0]);
76
81
  }
77
82
  else {
@@ -87,8 +92,10 @@ function withTracking(WrappedComponent) {
87
92
  currentRef.removeEventListener('click', handleClick, true);
88
93
  };
89
94
  }
90
- }, [ref, handleClick]);
91
- var trackingProps = __assign(__assign({}, restProps), { controller: controller, result: result, banner: banner, type: type, content: content, trackingRef: ref });
95
+ }, [handleClick]);
96
+ var trackingProps = __assign(__assign({}, restProps), { controller: controller, result: result, banner: banner, type: type, content: content, trackingRef: (0, hooks_1.useCallback)(function (el) {
97
+ updateRef(el);
98
+ }, [updateRef]) });
92
99
  return (0, preact_1.h)(WrappedComponent, __assign({}, trackingProps));
93
100
  };
94
101
  return WithTracking;
@@ -3,5 +3,6 @@ import { UseIntersectionOptions } from '../hooks';
3
3
  export declare function createImpressionObserver(options?: UseIntersectionOptions): {
4
4
  ref: Ref<HTMLElement | null>;
5
5
  inViewport: boolean;
6
+ updateRef: (el: HTMLElement | null) => void;
6
7
  };
7
8
  //# sourceMappingURL=createImpressionObserver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createImpressionObserver.d.ts","sourceRoot":"","sources":["../../../src/utilities/createImpressionObserver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAU,MAAM,cAAc,CAAC;AAChD,OAAO,EAA2B,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAI3E,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG;IAC3E,GAAG,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;CACpB,CAYA"}
1
+ {"version":3,"file":"createImpressionObserver.d.ts","sourceRoot":"","sources":["../../../src/utilities/createImpressionObserver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAU,MAAM,cAAc,CAAC;AAChD,OAAO,EAA2B,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAI3E,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG;IAC3E,GAAG,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;CAC5C,CAaA"}
@@ -18,10 +18,11 @@ var IMPRESSION_VISIBILITY_THRESHOLD = 0.7;
18
18
  var IMPRESSION_MIN_VISIBLE_TIME = 1000;
19
19
  function createImpressionObserver(options) {
20
20
  var ref = (0, hooks_1.useRef)(null);
21
- var inViewport = (0, hooks_2.useIntersectionAdvanced)(ref, __assign(__assign({}, options), { fireOnce: true, threshold: IMPRESSION_VISIBILITY_THRESHOLD, minVisibleTime: IMPRESSION_MIN_VISIBLE_TIME }));
21
+ var _a = (0, hooks_2.useIntersectionAdvanced)(ref, __assign(__assign({}, options), { fireOnce: true, threshold: IMPRESSION_VISIBILITY_THRESHOLD, minVisibleTime: IMPRESSION_MIN_VISIBLE_TIME })), inViewport = _a.inViewport, updateRef = _a.updateRef;
22
22
  return {
23
23
  ref: ref,
24
24
  inViewport: inViewport,
25
+ updateRef: updateRef,
25
26
  };
26
27
  }
27
28
  exports.createImpressionObserver = createImpressionObserver;
@@ -4,7 +4,9 @@ export interface UseIntersectionOptions {
4
4
  fireOnce?: boolean;
5
5
  threshold?: number | number[];
6
6
  minVisibleTime?: number;
7
- resetKey?: string;
8
7
  }
9
- export declare const useIntersectionAdvanced: (ref: MutableRef<HTMLElement | null>, options?: UseIntersectionOptions) => boolean;
8
+ export declare const useIntersectionAdvanced: (ref: MutableRef<HTMLElement | null>, options?: UseIntersectionOptions) => {
9
+ inViewport: boolean;
10
+ updateRef: (el: HTMLElement | null) => void;
11
+ };
10
12
  //# sourceMappingURL=useIntersectionAdvanced.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useIntersectionAdvanced.d.ts","sourceRoot":"","sources":["../../../src/hooks/useIntersectionAdvanced.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,UAAU,EAAE,MAAM,cAAc,CAAC;AAEvE,MAAM,WAAW,sBAAsB;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,eAAO,MAAM,uBAAuB,QAAS,WAAW,WAAW,GAAG,IAAI,CAAC,YAAW,sBAAsB,KAAQ,OAsInH,CAAC"}
1
+ {"version":3,"file":"useIntersectionAdvanced.d.ts","sourceRoot":"","sources":["../../../src/hooks/useIntersectionAdvanced.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA4C,UAAU,EAAE,MAAM,cAAc,CAAC;AAEpF,MAAM,WAAW,sBAAsB;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAGD,eAAO,MAAM,uBAAuB,QAC9B,WAAW,WAAW,GAAG,IAAI,CAAC,YAC1B,sBAAsB;gBACf,OAAO;oBAAkB,WAAW,GAAG,IAAI,KAAK,IAAI;CA6HpE,CAAC"}
@@ -1,26 +1,18 @@
1
- import { useState, useEffect, useRef } from 'preact/hooks';
1
+ import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
2
2
  const VISIBILITY_POLL_INTERVAL = 250;
3
3
  export const useIntersectionAdvanced = (ref, options = {}) => {
4
- const { rootMargin = '0px', fireOnce = false, threshold = 0, minVisibleTime = 0, resetKey } = options;
4
+ const { rootMargin = '0px', fireOnce = false, threshold = 0, minVisibleTime = 0 } = options;
5
5
  // State and setter for storing whether element is visible
6
6
  const [isIntersecting, setIntersecting] = useState(false);
7
7
  // Timer reference to track visibility duration
8
8
  const visibleTimerRef = useRef(null);
9
9
  // Track when the element started being visible
10
10
  const visibleStartRef = useRef(null);
11
- // Track the last reset key to detect changes
12
- const lastResetKeyRef = useRef(resetKey);
13
- // Reset state if resetKey has changed
14
- if (resetKey !== lastResetKeyRef.current) {
15
- setIntersecting(false);
16
- if (visibleTimerRef.current) {
17
- window.clearTimeout(visibleTimerRef.current);
18
- visibleTimerRef.current = null;
19
- }
20
- visibleStartRef.current = null;
21
- lastResetKeyRef.current = resetKey;
22
- return false;
23
- }
11
+ const [counter, setCounter] = useState(0);
12
+ const updateRef = useCallback((el) => {
13
+ ref.current = el;
14
+ setCounter((c) => c + 1);
15
+ }, []);
24
16
  useEffect(() => {
25
17
  setIntersecting(false);
26
18
  let observer = null;
@@ -114,8 +106,8 @@ export const useIntersectionAdvanced = (ref, options = {}) => {
114
106
  observer.unobserve(ref.current);
115
107
  }
116
108
  };
117
- }, [ref, resetKey]);
118
- return isIntersecting;
109
+ }, [ref, counter]);
110
+ return { inViewport: isIntersecting, updateRef };
119
111
  };
120
112
  function elementIsVisible(el) {
121
113
  if (el && 'checkVisibility' in el) {
@@ -1 +1 @@
1
- {"version":3,"file":"withTracking.d.ts","sourceRoot":"","sources":["../../../src/providers/withTracking.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAK,aAAa,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAA8B,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACjG,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACxH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAIhF,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAC/C,UAAU,iBAAiB;IAC1B,UAAU,CAAC,EAAE,gBAAgB,GAAG,sBAAsB,GAAG,wBAAwB,CAAC;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,KAAK,SAAS,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,CAAC,KAAK,CAAC,4BAiFnG"}
1
+ {"version":3,"file":"withTracking.d.ts","sourceRoot":"","sources":["../../../src/providers/withTracking.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAK,aAAa,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAA8B,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACjG,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACxH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAIhF,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAC/C,UAAU,iBAAiB;IAC1B,UAAU,CAAC,EAAE,gBAAgB,GAAG,sBAAsB,GAAG,wBAAwB,CAAC;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,KAAK,SAAS,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,CAAC,KAAK,CAAC,4BAgGnG"}
@@ -1,6 +1,6 @@
1
1
  import { h } from 'preact';
2
2
  import { createImpressionObserver } from '../utilities';
3
- import { useEffect, useCallback } from 'preact/hooks';
3
+ import { useEffect, useCallback, useRef } from 'preact/hooks';
4
4
  export const TRACKING_ATTRIBUTE = 'sstracking';
5
5
  export function withTracking(WrappedComponent) {
6
6
  const WithTracking = (props) => {
@@ -15,29 +15,34 @@ export function withTracking(WrappedComponent) {
15
15
  if (!result && !banner && (!type || !content)) {
16
16
  console.warn('Warning: No result or banner provided to withTracking');
17
17
  }
18
- let resetKey;
19
- if (controller?.type === 'search' || controller?.type === 'autocomplete') {
20
- const urlManager = controller.urlManager;
21
- resetKey = JSON.stringify({
22
- q: urlManager.state.query,
23
- p: urlManager.state.page,
24
- ps: urlManager.state.pageSize,
25
- s: urlManager.state.sort,
26
- f: urlManager.state.filter,
27
- });
18
+ const { ref, inViewport, updateRef } = createImpressionObserver();
19
+ // Reset impression tracking when the result identity changes (e.g. new search context).
20
+ // Each Product/Banner gets a new responseId per search response, so this naturally
21
+ // resets when query/sort/filters change without needing global controller state.
22
+ // Calling updateRef(ref.current) re-observes the same element with fresh state.
23
+ const resultIdentity = (result || banner || (type && content?.[type]?.[0]))?.responseId;
24
+ const prevIdentityRef = useRef(resultIdentity);
25
+ // Tracks whether we're waiting for the observer to reset after an identity change.
26
+ // Set synchronously during render to block impressions immediately when identity
27
+ // changes, preventing a stale inViewport=true from firing before the observer resets.
28
+ const awaitingReobservationRef = useRef(false);
29
+ if (prevIdentityRef.current !== resultIdentity) {
30
+ awaitingReobservationRef.current = true;
28
31
  }
29
- else if (controller?.type === 'recommendation') {
30
- // For recommendations, use a combination of tag and other relevant state
31
- const recStore = controller.store;
32
- resetKey = JSON.stringify({
33
- tag: recStore.profile?.tag,
34
- ids: recStore.results.map((result) => result.id).join(','),
35
- });
36
- }
37
- const { ref, inViewport } = createImpressionObserver({ resetKey });
38
- if (inViewport) {
39
- // TODO: add support for disabling tracking events via config like in ResultTracker
40
- if (type && content && !result && ['search', 'autocomplete'].includes(controller?.type || '')) {
32
+ useEffect(() => {
33
+ if (prevIdentityRef.current !== resultIdentity) {
34
+ prevIdentityRef.current = resultIdentity;
35
+ updateRef(ref.current);
36
+ }
37
+ }, [resultIdentity, updateRef]);
38
+ useEffect(() => {
39
+ if (awaitingReobservationRef.current && !inViewport) {
40
+ awaitingReobservationRef.current = false;
41
+ }
42
+ }, [inViewport, resultIdentity]);
43
+ const isBannerTracking = type && content && !result && ['search', 'autocomplete'].includes(controller?.type || '');
44
+ if (inViewport && !awaitingReobservationRef.current) {
45
+ if (isBannerTracking) {
41
46
  controller?.track.banner.impression(content[type][0]);
42
47
  }
43
48
  else if (!result?.bundleSeed) {
@@ -45,7 +50,7 @@ export function withTracking(WrappedComponent) {
45
50
  }
46
51
  }
47
52
  const handleClick = useCallback((e) => {
48
- if (type && content && !result && ['search', 'autocomplete'].includes(controller?.type || '')) {
53
+ if (isBannerTracking) {
49
54
  controller?.track.banner.click(e, content[type][0]);
50
55
  }
51
56
  else {
@@ -61,7 +66,7 @@ export function withTracking(WrappedComponent) {
61
66
  currentRef.removeEventListener('click', handleClick, true);
62
67
  };
63
68
  }
64
- }, [ref, handleClick]);
69
+ }, [handleClick]);
65
70
  const trackingProps = {
66
71
  ...restProps,
67
72
  controller,
@@ -69,7 +74,9 @@ export function withTracking(WrappedComponent) {
69
74
  banner,
70
75
  type,
71
76
  content,
72
- trackingRef: ref,
77
+ trackingRef: useCallback((el) => {
78
+ updateRef(el);
79
+ }, [updateRef]),
73
80
  };
74
81
  return h(WrappedComponent, { ...trackingProps });
75
82
  };
@@ -3,5 +3,6 @@ import { UseIntersectionOptions } from '../hooks';
3
3
  export declare function createImpressionObserver(options?: UseIntersectionOptions): {
4
4
  ref: Ref<HTMLElement | null>;
5
5
  inViewport: boolean;
6
+ updateRef: (el: HTMLElement | null) => void;
6
7
  };
7
8
  //# sourceMappingURL=createImpressionObserver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createImpressionObserver.d.ts","sourceRoot":"","sources":["../../../src/utilities/createImpressionObserver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAU,MAAM,cAAc,CAAC;AAChD,OAAO,EAA2B,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAI3E,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG;IAC3E,GAAG,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;CACpB,CAYA"}
1
+ {"version":3,"file":"createImpressionObserver.d.ts","sourceRoot":"","sources":["../../../src/utilities/createImpressionObserver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAU,MAAM,cAAc,CAAC;AAChD,OAAO,EAA2B,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAI3E,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG;IAC3E,GAAG,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;CAC5C,CAaA"}
@@ -4,7 +4,7 @@ const IMPRESSION_VISIBILITY_THRESHOLD = 0.7;
4
4
  const IMPRESSION_MIN_VISIBLE_TIME = 1000;
5
5
  export function createImpressionObserver(options) {
6
6
  const ref = useRef(null);
7
- const inViewport = useIntersectionAdvanced(ref, {
7
+ const { inViewport, updateRef } = useIntersectionAdvanced(ref, {
8
8
  ...options,
9
9
  fireOnce: true,
10
10
  threshold: IMPRESSION_VISIBILITY_THRESHOLD,
@@ -13,5 +13,6 @@ export function createImpressionObserver(options) {
13
13
  return {
14
14
  ref,
15
15
  inViewport,
16
+ updateRef,
16
17
  };
17
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@searchspring/snap-preact-components",
3
- "version": "0.73.7",
3
+ "version": "0.74.0",
4
4
  "description": "Snap Preact Component Library",
5
5
  "author": "Searchspring",
6
6
  "license": "MIT",
@@ -29,7 +29,7 @@
29
29
  "dependencies": {
30
30
  "@cypress/react": "^8.0.0",
31
31
  "@emotion/react": "11.9.0",
32
- "@searchspring/snap-toolbox": "0.73.7",
32
+ "@searchspring/snap-toolbox": "0.74.0",
33
33
  "classnames": "^2.3.2",
34
34
  "cypress": "^13.7.1",
35
35
  "cypress-wait-until": "^1.7.2",
@@ -52,14 +52,14 @@
52
52
  "@babel/preset-env": "^7.21.4",
53
53
  "@babel/preset-react": "^7.18.6",
54
54
  "@babel/runtime": "^7.21.0",
55
- "@searchspring/snap-client": "0.73.7",
56
- "@searchspring/snap-controller": "0.73.7",
57
- "@searchspring/snap-event-manager": "0.73.7",
58
- "@searchspring/snap-logger": "0.73.7",
59
- "@searchspring/snap-profiler": "0.73.7",
60
- "@searchspring/snap-store-mobx": "0.73.7",
61
- "@searchspring/snap-tracker": "0.73.7",
62
- "@searchspring/snap-url-manager": "0.73.7",
55
+ "@searchspring/snap-client": "0.74.0",
56
+ "@searchspring/snap-controller": "0.74.0",
57
+ "@searchspring/snap-event-manager": "0.74.0",
58
+ "@searchspring/snap-logger": "0.74.0",
59
+ "@searchspring/snap-profiler": "0.74.0",
60
+ "@searchspring/snap-store-mobx": "0.74.0",
61
+ "@searchspring/snap-tracker": "0.74.0",
62
+ "@searchspring/snap-url-manager": "0.74.0",
63
63
  "@storybook/addon-actions": "6.4.22",
64
64
  "@storybook/addon-controls": "6.4.22",
65
65
  "@storybook/addon-docs": "6.4.22",
@@ -84,5 +84,5 @@
84
84
  "webpack-merge": "^5.8.0"
85
85
  },
86
86
  "sideEffects": false,
87
- "gitHead": "8da3a6b990f69a5cea07aa4570db108a83952f4c"
87
+ "gitHead": "fd005b3943aa6eb62ac16a92e6d932dc55499865"
88
88
  }