@financial-times/cp-content-pipeline-ui 6.13.0 → 6.14.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.
package/CHANGELOG.md CHANGED
@@ -574,6 +574,21 @@
574
574
  * @financial-times/cp-content-pipeline-client bumped from ^3.7.2 to ^3.7.3
575
575
  * @financial-times/cp-content-pipeline-schema bumped from ^2.10.1 to ^2.10.2
576
576
 
577
+ ## [6.14.0](https://github.com/Financial-Times/cp-content-pipeline/compare/cp-content-pipeline-ui-v6.13.0...cp-content-pipeline-ui-v6.14.0) (2024-07-16)
578
+
579
+
580
+ ### Features
581
+
582
+ * tracking for flourish toppers ([4c19db6](https://github.com/Financial-Times/cp-content-pipeline/commit/4c19db61bf28e3a2f04c6eae8eb7344711076ea6))
583
+
584
+
585
+ ### Bug Fixes
586
+
587
+ * decrease amount of element needed to be seen ([ba8264a](https://github.com/Financial-Times/cp-content-pipeline/commit/ba8264aa99c75a30db7b878c64766816d1464bdc))
588
+ * genericise class to be used on all toppers ([1021468](https://github.com/Financial-Times/cp-content-pipeline/commit/1021468cfdd6d1f75a1d7a45ec7f7c142da6f9a8))
589
+ * remove unneeded data attribute ([6ce8529](https://github.com/Financial-Times/cp-content-pipeline/commit/6ce8529588984839dccf21d29375450671ad2841))
590
+ * return at first opportunity ([508b07c](https://github.com/Financial-Times/cp-content-pipeline/commit/508b07c7af834b6f2d02d2644ec126da195201e3))
591
+
577
592
  ## [6.13.0](https://github.com/Financial-Times/cp-content-pipeline/compare/cp-content-pipeline-ui-v6.12.0...cp-content-pipeline-ui-v6.13.0) (2024-07-10)
578
593
 
579
594
 
package/lib/client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { default as Clip } from './components/Clip/client/index';
2
2
  export { default as BackToTopButton } from './components/BackToTopButton/client';
3
3
  export { default as FlourishSSREmbed } from './components/Flourish/client/index';
4
+ export { TopperTracker } from './components/Topper/client/index';
package/lib/client.js CHANGED
@@ -3,11 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.FlourishSSREmbed = exports.BackToTopButton = exports.Clip = void 0;
6
+ exports.TopperTracker = exports.FlourishSSREmbed = exports.BackToTopButton = exports.Clip = void 0;
7
7
  var index_1 = require("./components/Clip/client/index");
8
8
  Object.defineProperty(exports, "Clip", { enumerable: true, get: function () { return __importDefault(index_1).default; } });
9
9
  var client_1 = require("./components/BackToTopButton/client");
10
10
  Object.defineProperty(exports, "BackToTopButton", { enumerable: true, get: function () { return __importDefault(client_1).default; } });
11
11
  var index_2 = require("./components/Flourish/client/index");
12
12
  Object.defineProperty(exports, "FlourishSSREmbed", { enumerable: true, get: function () { return __importDefault(index_2).default; } });
13
+ var index_3 = require("./components/Topper/client/index");
14
+ Object.defineProperty(exports, "TopperTracker", { enumerable: true, get: function () { return index_3.TopperTracker; } });
13
15
  //# sourceMappingURL=client.js.map
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;;;;AAAA,wDAAgE;AAAvD,8GAAA,OAAO,OAAQ;AACxB,8DAAgF;AAAvE,0HAAA,OAAO,OAAmB;AACnC,4DAAgF;AAAvE,0HAAA,OAAO,OAAoB"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;;;;AAAA,wDAAgE;AAAvD,8GAAA,OAAO,OAAQ;AACxB,8DAAgF;AAAvE,0HAAA,OAAO,OAAmB;AACnC,4DAAgF;AAAvE,0HAAA,OAAO,OAAoB;AACpC,0DAAgE;AAAvD,sGAAA,aAAa,OAAA"}
@@ -10,6 +10,8 @@ const DisclaimerNotice = ({ id }) => (react_1.default.createElement("div", { id:
10
10
  react_1.default.createElement("div", { className: "o-message__content" },
11
11
  react_1.default.createElement("p", { className: "o-message__content-main" }, "You are seeing a snapshot of an interactive graphic. This is most likely due to being offline or JavaScript being disabled in your browser.")))));
12
12
  function Flourish({ id, flourishType, description, layoutWidth, fallbackImage, iFrame = false, inArticleBody = true, }) {
13
+ if (!id)
14
+ return null;
13
15
  const anchorHref = `#${id}`;
14
16
  const fullGrid = layoutWidth === 'full-grid' || layoutWidth === 'grid';
15
17
  const figureClassnames = (0, classnames_1.default)({
@@ -27,13 +29,11 @@ function Flourish({ id, flourishType, description, layoutWidth, fallbackImage, i
27
29
  aspectRatio: `${fallbackImage.width}/${fallbackImage.height}`,
28
30
  }
29
31
  : {};
30
- if (!id)
31
- return null;
32
32
  return (react_1.default.createElement("div", { className: (0, classnames_1.default)({
33
33
  'n-content-layout': inArticleBody,
34
34
  flourish: iFrame,
35
35
  'flourish--iFrame': iFrame,
36
- }), "data-layout-width": fullGrid ? 'full-grid' : null, "data-component": "flourish" },
36
+ }), "data-layout-width": fullGrid ? 'full-grid' : null, "data-component": "flourish", "data-component-id": id, "data-component-type": inArticleBody ? 'flourish-in-article' : 'flourish-topper' },
37
37
  iFrame && (react_1.default.createElement("iframe", { src: `https://flo.uri.sh/visualisation/${id}/embed?hideTitle=${!inArticleBody}`, style: { width: '100%', aspectRatio: iframeAspectRatio }, className: "flourish__i-frame" })),
38
38
  react_1.default.createElement("figure", { className: figureClassnames, "data-original-image-width": fullGrid ? fallbackImage?.width : null, "data-original-image-height": fullGrid ? fallbackImage?.height : null },
39
39
  react_1.default.createElement("a", { href: anchorHref },
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/Flourish/index.tsx"],"names":[],"mappings":";;;;;AAAA,kDAAyB;AACzB,4DAAmC;AAqBnC,MAAM,gBAAgB,GAAG,CAAC,EAAE,EAAE,EAAmB,EAAE,EAAE,CAAC,CACpD,uCACE,EAAE,EAAE,EAAE,EACN,SAAS,EAAC,mEAAmE,sBAC5D,WAAW;IAE5B,uCAAK,SAAS,EAAC,sBAAsB;QACnC,uCAAK,SAAS,EAAC,oBAAoB;YACjC,qCAAG,SAAS,EAAC,yBAAyB,kJAIlC,CACA,CACF,CACF,CACP,CAAA;AACD,SAAwB,QAAQ,CAAC,EAC/B,EAAE,EACF,YAAY,EACZ,WAAW,EACX,WAAW,EACX,aAAa,EACb,MAAM,GAAG,KAAK,EACd,aAAa,GAAG,IAAI,GACN;IACd,MAAM,UAAU,GAAG,IAAI,EAAE,EAAE,CAAA;IAC3B,MAAM,QAAQ,GAAG,WAAW,KAAK,WAAW,IAAI,WAAW,KAAK,MAAM,CAAA;IACtE,MAAM,gBAAgB,GAAG,IAAA,oBAAU,EAAC;QAClC,mBAAmB,EAAE,IAAI;QACzB,6BAA6B,EAAE,IAAI;QACnC,yBAAyB,EAAE,QAAQ;QACnC,gBAAgB,EAAE,MAAM;KACzB,CAAC,CAAA;IAEF,MAAM,iBAAiB,GACrB,MAAM,IAAI,aAAa,EAAE,KAAK,IAAI,aAAa,EAAE,MAAM;QACrD,CAAC,CAAC,GAAG,aAAa,CAAC,KAAK,IAAI,aAAa,CAAC,MAAM,EAAE;QAClD,CAAC,CAAC,MAAM,CAAA;IAEZ,MAAM,gBAAgB,GACpB,CAAC,aAAa,IAAI,aAAa,EAAE,KAAK,IAAI,aAAa,EAAE,MAAM;QAC7D,CAAC,CAAC;YACE,KAAK,EAAE,MAAM;YACb,WAAW,EAAE,GAAG,aAAa,CAAC,KAAK,IAAI,aAAa,CAAC,MAAM,EAAE;SAC9D;QACH,CAAC,CAAC,EAAE,CAAA;IAER,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAA;IAEpB,OAAO,CACL,uCACE,SAAS,EAAE,IAAA,oBAAU,EAAC;YACpB,kBAAkB,EAAE,aAAa;YACjC,QAAQ,EAAE,MAAM;YAChB,kBAAkB,EAAE,MAAM;SAC3B,CAAC,uBACiB,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,oBACjC,UAAU;QAExB,MAAM,IAAI,CACT,0CACE,GAAG,EAAE,oCAAoC,EAAE,oBAAoB,CAAC,aAAa,EAAE,EAC/E,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,iBAAiB,EAAE,EACxD,SAAS,EAAC,mBAAmB,GACrB,CACX;QACD,0CACE,SAAS,EAAE,gBAAgB,+BACA,QAAQ,CAAC,CAAC,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,gCACrC,QAAQ,CAAC,CAAC,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI;YAEnE,qCAAG,IAAI,EAAE,UAAU;gBACjB,8DAGmB,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,UAAU,sBACrC,EAAE,wBACA,YAAY;oBAEhC,8BAAC,gBAAgB,IAAC,EAAE,EAAE,EAAE,GAAI;oBAC5B,uCACE,GAAG,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,EAC7B,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,gBAAgB,GACvB,CACM,CACR,CACG,CACL,CACP,CAAA;AACH,CAAC;AA1ED,2BA0EC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/Flourish/index.tsx"],"names":[],"mappings":";;;;;AAAA,kDAAyB;AACzB,4DAAmC;AAqBnC,MAAM,gBAAgB,GAAG,CAAC,EAAE,EAAE,EAAmB,EAAE,EAAE,CAAC,CACpD,uCACE,EAAE,EAAE,EAAE,EACN,SAAS,EAAC,mEAAmE,sBAC5D,WAAW;IAE5B,uCAAK,SAAS,EAAC,sBAAsB;QACnC,uCAAK,SAAS,EAAC,oBAAoB;YACjC,qCAAG,SAAS,EAAC,yBAAyB,kJAIlC,CACA,CACF,CACF,CACP,CAAA;AACD,SAAwB,QAAQ,CAAC,EAC/B,EAAE,EACF,YAAY,EACZ,WAAW,EACX,WAAW,EACX,aAAa,EACb,MAAM,GAAG,KAAK,EACd,aAAa,GAAG,IAAI,GACN;IACd,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAA;IACpB,MAAM,UAAU,GAAG,IAAI,EAAE,EAAE,CAAA;IAC3B,MAAM,QAAQ,GAAG,WAAW,KAAK,WAAW,IAAI,WAAW,KAAK,MAAM,CAAA;IACtE,MAAM,gBAAgB,GAAG,IAAA,oBAAU,EAAC;QAClC,mBAAmB,EAAE,IAAI;QACzB,6BAA6B,EAAE,IAAI;QACnC,yBAAyB,EAAE,QAAQ;QACnC,gBAAgB,EAAE,MAAM;KACzB,CAAC,CAAA;IAEF,MAAM,iBAAiB,GACrB,MAAM,IAAI,aAAa,EAAE,KAAK,IAAI,aAAa,EAAE,MAAM;QACrD,CAAC,CAAC,GAAG,aAAa,CAAC,KAAK,IAAI,aAAa,CAAC,MAAM,EAAE;QAClD,CAAC,CAAC,MAAM,CAAA;IAEZ,MAAM,gBAAgB,GACpB,CAAC,aAAa,IAAI,aAAa,EAAE,KAAK,IAAI,aAAa,EAAE,MAAM;QAC7D,CAAC,CAAC;YACE,KAAK,EAAE,MAAM;YACb,WAAW,EAAE,GAAG,aAAa,CAAC,KAAK,IAAI,aAAa,CAAC,MAAM,EAAE;SAC9D;QACH,CAAC,CAAC,EAAE,CAAA;IAER,OAAO,CACL,uCACE,SAAS,EAAE,IAAA,oBAAU,EAAC;YACpB,kBAAkB,EAAE,aAAa;YACjC,QAAQ,EAAE,MAAM;YAChB,kBAAkB,EAAE,MAAM;SAC3B,CAAC,uBACiB,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,oBACjC,UAAU,uBACN,EAAE,yBAEnB,aAAa,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,iBAAiB;QAG1D,MAAM,IAAI,CACT,0CACE,GAAG,EAAE,oCAAoC,EAAE,oBAAoB,CAAC,aAAa,EAAE,EAC/E,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,iBAAiB,EAAE,EACxD,SAAS,EAAC,mBAAmB,GACrB,CACX;QACD,0CACE,SAAS,EAAE,gBAAgB,+BACA,QAAQ,CAAC,CAAC,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,gCACrC,QAAQ,CAAC,CAAC,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI;YAEnE,qCAAG,IAAI,EAAE,UAAU;gBACjB,8DAGmB,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,UAAU,sBACrC,EAAE,wBACA,YAAY;oBAEhC,8BAAC,gBAAgB,IAAC,EAAE,EAAE,EAAE,GAAI;oBAC5B,uCACE,GAAG,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,EAC7B,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,gBAAgB,GACvB,CACM,CACR,CACG,CACL,CACP,CAAA;AACH,CAAC;AA7ED,2BA6EC"}
@@ -0,0 +1,2 @@
1
+ import { TopperTracker } from './tracking';
2
+ export { TopperTracker };
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TopperTracker = void 0;
4
+ const tracking_1 = require("./tracking");
5
+ Object.defineProperty(exports, "TopperTracker", { enumerable: true, get: function () { return tracking_1.TopperTracker; } });
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/components/Topper/client/index.ts"],"names":[],"mappings":";;;AAAA,yCAA0C;AAEjC,8FAFA,wBAAa,OAEA"}
@@ -0,0 +1,17 @@
1
+ declare class TopperTracker {
2
+ private startTime;
3
+ private totalVisibleTime;
4
+ private timeElapsedSeconds;
5
+ private type;
6
+ private component;
7
+ private id;
8
+ private observer;
9
+ constructor({ type }?: {
10
+ type?: string | undefined;
11
+ });
12
+ init(): void;
13
+ private dispatchEvent;
14
+ private onChange;
15
+ disconnect(): void;
16
+ }
17
+ export { TopperTracker };
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TopperTracker = void 0;
4
+ class TopperTracker {
5
+ constructor({ type = 'flourish-topper' } = {}) {
6
+ this.startTime = 0;
7
+ this.totalVisibleTime = 0;
8
+ this.timeElapsedSeconds = null;
9
+ this.type = type;
10
+ this.component = document.querySelector(`[data-component-type="${this.type}"]`);
11
+ this.id = this.component?.getAttribute('data-component-id') || null;
12
+ this.observer = null;
13
+ }
14
+ init() {
15
+ if (!window.IntersectionObserver || !this.component) {
16
+ return;
17
+ }
18
+ this.dispatchEvent('mount');
19
+ this.observer = new IntersectionObserver(this.onChange.bind(this), {
20
+ threshold: [0.75],
21
+ });
22
+ this.observer.observe(this.component);
23
+ }
24
+ dispatchEvent(action) {
25
+ const component = {
26
+ name: this.type,
27
+ id: this.id,
28
+ };
29
+ if (this.timeElapsedSeconds) {
30
+ component.timeElapsedSeconds = this.timeElapsedSeconds;
31
+ }
32
+ const event = new CustomEvent('oTracking.event', {
33
+ detail: {
34
+ category: 'component',
35
+ action: action,
36
+ component,
37
+ },
38
+ bubbles: true,
39
+ });
40
+ document.body.dispatchEvent(event);
41
+ }
42
+ onChange(changes) {
43
+ changes.forEach((change) => {
44
+ if (change.target !== this.component) {
45
+ return;
46
+ }
47
+ if (change.isIntersecting || change.intersectionRatio >= 1) {
48
+ this.dispatchEvent('view');
49
+ this.startTime = performance.now();
50
+ }
51
+ if (!change.isIntersecting || change.intersectionRatio === 0) {
52
+ this.totalVisibleTime = performance.now() - this.startTime;
53
+ this.timeElapsedSeconds = parseFloat((this.totalVisibleTime / 1000).toFixed(2));
54
+ this.dispatchEvent('stop-view');
55
+ this.totalVisibleTime = 0;
56
+ this.timeElapsedSeconds = null;
57
+ }
58
+ });
59
+ }
60
+ disconnect() {
61
+ if (this.observer && this.component) {
62
+ this.observer.unobserve(this.component);
63
+ this.observer.disconnect();
64
+ }
65
+ }
66
+ }
67
+ exports.TopperTracker = TopperTracker;
68
+ //# sourceMappingURL=tracking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracking.js","sourceRoot":"","sources":["../../../../src/components/Topper/client/tracking.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("./index");
4
+ describe('TopperTracker', () => {
5
+ let mockIntersectionObserver;
6
+ let mockObserve;
7
+ let mockUnobserve;
8
+ let mockDisconnect;
9
+ let mockComponent;
10
+ let dispatchEventSpy;
11
+ beforeEach(() => {
12
+ mockObserve = jest.fn();
13
+ mockUnobserve = jest.fn();
14
+ mockDisconnect = jest.fn();
15
+ mockIntersectionObserver = jest.fn(() => {
16
+ return {
17
+ observe: mockObserve,
18
+ unobserve: mockUnobserve,
19
+ disconnect: mockDisconnect,
20
+ };
21
+ });
22
+ window.IntersectionObserver = mockIntersectionObserver;
23
+ mockComponent = document.createElement('div');
24
+ mockComponent.setAttribute('data-component-type', 'flourish-topper');
25
+ mockComponent.setAttribute('data-component-id', 'test-id');
26
+ document.body.appendChild(mockComponent);
27
+ jest.spyOn(document, 'querySelector').mockReturnValue(mockComponent);
28
+ jest
29
+ .spyOn(performance, 'now')
30
+ .mockImplementationOnce(() => 1)
31
+ .mockImplementationOnce(() => 12344);
32
+ dispatchEventSpy = jest.spyOn(document.body, 'dispatchEvent');
33
+ });
34
+ afterEach(() => {
35
+ jest.clearAllMocks();
36
+ document.body.removeChild(mockComponent);
37
+ });
38
+ it('should initialise and observe the component', () => {
39
+ const tracker = new index_1.TopperTracker();
40
+ tracker.init();
41
+ expect(mockIntersectionObserver).toHaveBeenCalled();
42
+ expect(mockObserve).toHaveBeenCalledWith(mockComponent);
43
+ });
44
+ it('should dispatch a mount event on intialisation', () => {
45
+ const tracker = new index_1.TopperTracker();
46
+ tracker.init();
47
+ const mountEvent = dispatchEventSpy.mock.calls[0][0];
48
+ expect(mountEvent.type).toBe('oTracking.event');
49
+ expect(mountEvent.detail).toEqual({
50
+ category: 'component',
51
+ action: 'mount',
52
+ component: {
53
+ name: 'flourish-topper',
54
+ id: 'test-id',
55
+ },
56
+ });
57
+ });
58
+ it('should handle component visibility changes', () => {
59
+ const tracker = new index_1.TopperTracker();
60
+ tracker.init();
61
+ const mockChanges = [
62
+ {
63
+ target: mockComponent,
64
+ isIntersecting: true,
65
+ intersectionRatio: 1.0,
66
+ boundingClientRect: {},
67
+ intersectionRect: {},
68
+ rootBounds: null,
69
+ time: 1,
70
+ },
71
+ ];
72
+ tracker['onChange'](mockChanges);
73
+ const viewEvent = dispatchEventSpy.mock.calls[1][0];
74
+ expect(viewEvent.type).toBe('oTracking.event');
75
+ expect(viewEvent.detail).toEqual({
76
+ category: 'component',
77
+ action: 'view',
78
+ component: {
79
+ name: 'flourish-topper',
80
+ id: 'test-id',
81
+ },
82
+ });
83
+ const mockChangesAfter = [
84
+ {
85
+ target: mockComponent,
86
+ isIntersecting: false,
87
+ intersectionRatio: 0.0,
88
+ boundingClientRect: {},
89
+ intersectionRect: {},
90
+ rootBounds: null,
91
+ time: 12.3444,
92
+ },
93
+ ];
94
+ tracker['onChange'](mockChangesAfter);
95
+ const stopViewEvent = dispatchEventSpy.mock.calls[2][0];
96
+ expect(stopViewEvent.type).toBe('oTracking.event');
97
+ expect(stopViewEvent.detail).toEqual({
98
+ category: 'component',
99
+ action: 'stop-view',
100
+ component: {
101
+ name: 'flourish-topper',
102
+ id: 'test-id',
103
+ timeElapsedSeconds: 12.34,
104
+ },
105
+ });
106
+ });
107
+ it('should disconnect the observer', () => {
108
+ const tracker = new index_1.TopperTracker();
109
+ tracker.init();
110
+ tracker.disconnect();
111
+ expect(mockUnobserve).toHaveBeenCalledWith(mockComponent);
112
+ expect(mockDisconnect).toHaveBeenCalled();
113
+ });
114
+ });
115
+ //# sourceMappingURL=tracking.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracking.spec.js","sourceRoot":"","sources":["../../../../src/components/Topper/client/tracking.spec.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/cp-content-pipeline-ui",
3
- "version": "6.13.0",
3
+ "version": "6.14.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
package/src/client.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { default as Clip } from './components/Clip/client/index'
2
2
  export { default as BackToTopButton } from './components/BackToTopButton/client'
3
3
  export { default as FlourishSSREmbed } from './components/Flourish/client/index'
4
+ export { TopperTracker } from './components/Topper/client/index'
@@ -46,6 +46,7 @@ export default function Flourish({
46
46
  iFrame = false,
47
47
  inArticleBody = true,
48
48
  }: FlourishProps) {
49
+ if (!id) return null
49
50
  const anchorHref = `#${id}`
50
51
  const fullGrid = layoutWidth === 'full-grid' || layoutWidth === 'grid'
51
52
  const figureClassnames = classnames({
@@ -68,8 +69,6 @@ export default function Flourish({
68
69
  }
69
70
  : {}
70
71
 
71
- if (!id) return null
72
-
73
72
  return (
74
73
  <div
75
74
  className={classnames({
@@ -79,6 +78,10 @@ export default function Flourish({
79
78
  })}
80
79
  data-layout-width={fullGrid ? 'full-grid' : null}
81
80
  data-component="flourish"
81
+ data-component-id={id}
82
+ data-component-type={
83
+ inArticleBody ? 'flourish-in-article' : 'flourish-topper'
84
+ }
82
85
  >
83
86
  {iFrame && (
84
87
  <iframe
@@ -5,6 +5,8 @@ exports[`Flourish component renders ignoring layout different from full-grid 1`]
5
5
  <div
6
6
  class="n-content-layout"
7
7
  data-component="flourish"
8
+ data-component-id="123"
9
+ data-component-type="flourish-in-article"
8
10
  >
9
11
  <figure
10
12
  class="n-content-picture n-content-layout__container"
@@ -52,6 +54,8 @@ exports[`Flourish component renders with a full-grid layout and fallback image 1
52
54
  <div
53
55
  class="n-content-layout"
54
56
  data-component="flourish"
57
+ data-component-id="123"
58
+ data-component-type="flourish-in-article"
55
59
  data-layout-width="full-grid"
56
60
  >
57
61
  <figure
@@ -102,6 +106,8 @@ exports[`Flourish component renders with an iframe rather than using the embed s
102
106
  <div
103
107
  class="n-content-layout flourish flourish--iFrame"
104
108
  data-component="flourish"
109
+ data-component-id="123"
110
+ data-component-type="flourish-in-article"
105
111
  data-layout-width="full-grid"
106
112
  >
107
113
  <iframe
@@ -157,6 +163,8 @@ exports[`Flourish component renders without n-content-layout class for charts no
157
163
  <div
158
164
  class=""
159
165
  data-component="flourish"
166
+ data-component-id="123"
167
+ data-component-type="flourish-topper"
160
168
  data-layout-width="full-grid"
161
169
  >
162
170
  <figure
@@ -208,6 +216,8 @@ exports[`does not set aspect ratios when in article body 1`] = `
208
216
  <div
209
217
  class="n-content-layout"
210
218
  data-component="flourish"
219
+ data-component-id="123"
220
+ data-component-type="flourish-in-article"
211
221
  data-layout-width="full-grid"
212
222
  >
213
223
  <figure
@@ -258,6 +268,8 @@ exports[`sets the hideTitle param in the iframe url to true when not in article
258
268
  <div
259
269
  class="flourish flourish--iFrame"
260
270
  data-component="flourish"
271
+ data-component-id="123"
272
+ data-component-type="flourish-topper"
261
273
  data-layout-width="full-grid"
262
274
  >
263
275
  <iframe
@@ -0,0 +1,3 @@
1
+ import { TopperTracker } from './tracking'
2
+
3
+ export { TopperTracker }
@@ -0,0 +1,132 @@
1
+ import { TopperTracker } from './index'
2
+
3
+ describe('TopperTracker', () => {
4
+ let mockIntersectionObserver: jest.Mock
5
+ let mockObserve: jest.Mock
6
+ let mockUnobserve: jest.Mock
7
+ let mockDisconnect: jest.Mock
8
+ let mockComponent: HTMLElement
9
+ let dispatchEventSpy: jest.SpyInstance
10
+
11
+ beforeEach(() => {
12
+ mockObserve = jest.fn()
13
+ mockUnobserve = jest.fn()
14
+ mockDisconnect = jest.fn()
15
+
16
+ mockIntersectionObserver = jest.fn(() => {
17
+ return {
18
+ observe: mockObserve,
19
+ unobserve: mockUnobserve,
20
+ disconnect: mockDisconnect,
21
+ }
22
+ })
23
+
24
+ window.IntersectionObserver = mockIntersectionObserver
25
+
26
+ mockComponent = document.createElement('div')
27
+ mockComponent.setAttribute('data-component-type', 'flourish-topper')
28
+ mockComponent.setAttribute('data-component-id', 'test-id')
29
+ document.body.appendChild(mockComponent)
30
+
31
+ jest.spyOn(document, 'querySelector').mockReturnValue(mockComponent)
32
+ jest
33
+ .spyOn(performance, 'now')
34
+ .mockImplementationOnce(() => 1)
35
+ .mockImplementationOnce(() => 12344)
36
+ dispatchEventSpy = jest.spyOn(document.body, 'dispatchEvent')
37
+ })
38
+
39
+ afterEach(() => {
40
+ jest.clearAllMocks()
41
+ document.body.removeChild(mockComponent)
42
+ })
43
+
44
+ it('should initialise and observe the component', () => {
45
+ const tracker = new TopperTracker()
46
+ tracker.init()
47
+
48
+ expect(mockIntersectionObserver).toHaveBeenCalled()
49
+ expect(mockObserve).toHaveBeenCalledWith(mockComponent)
50
+ })
51
+
52
+ it('should dispatch a mount event on intialisation', () => {
53
+ const tracker = new TopperTracker()
54
+ tracker.init()
55
+
56
+ const mountEvent = dispatchEventSpy.mock.calls[0][0] as CustomEvent
57
+ expect(mountEvent.type).toBe('oTracking.event')
58
+ expect(mountEvent.detail).toEqual({
59
+ category: 'component',
60
+ action: 'mount',
61
+ component: {
62
+ name: 'flourish-topper',
63
+ id: 'test-id',
64
+ },
65
+ })
66
+ })
67
+
68
+ it('should handle component visibility changes', () => {
69
+ const tracker = new TopperTracker()
70
+ tracker.init()
71
+
72
+ const mockChanges: IntersectionObserverEntry[] = [
73
+ {
74
+ target: mockComponent,
75
+ isIntersecting: true,
76
+ intersectionRatio: 1.0,
77
+ boundingClientRect: {} as DOMRectReadOnly,
78
+ intersectionRect: {} as DOMRectReadOnly,
79
+ rootBounds: null,
80
+ time: 1,
81
+ },
82
+ ]
83
+
84
+ tracker['onChange'](mockChanges)
85
+
86
+ const viewEvent = dispatchEventSpy.mock.calls[1][0] as CustomEvent
87
+ expect(viewEvent.type).toBe('oTracking.event')
88
+ expect(viewEvent.detail).toEqual({
89
+ category: 'component',
90
+ action: 'view',
91
+ component: {
92
+ name: 'flourish-topper',
93
+ id: 'test-id',
94
+ },
95
+ })
96
+
97
+ const mockChangesAfter: IntersectionObserverEntry[] = [
98
+ {
99
+ target: mockComponent,
100
+ isIntersecting: false,
101
+ intersectionRatio: 0.0,
102
+ boundingClientRect: {} as DOMRectReadOnly,
103
+ intersectionRect: {} as DOMRectReadOnly,
104
+ rootBounds: null,
105
+ time: 12.3444,
106
+ },
107
+ ]
108
+
109
+ tracker['onChange'](mockChangesAfter)
110
+
111
+ const stopViewEvent = dispatchEventSpy.mock.calls[2][0] as CustomEvent
112
+ expect(stopViewEvent.type).toBe('oTracking.event')
113
+ expect(stopViewEvent.detail).toEqual({
114
+ category: 'component',
115
+ action: 'stop-view',
116
+ component: {
117
+ name: 'flourish-topper',
118
+ id: 'test-id',
119
+ timeElapsedSeconds: 12.34,
120
+ },
121
+ })
122
+ })
123
+
124
+ it('should disconnect the observer', () => {
125
+ const tracker = new TopperTracker()
126
+ tracker.init()
127
+ tracker.disconnect()
128
+
129
+ expect(mockUnobserve).toHaveBeenCalledWith(mockComponent)
130
+ expect(mockDisconnect).toHaveBeenCalled()
131
+ })
132
+ })
@@ -0,0 +1,88 @@
1
+ interface Component {
2
+ name: string
3
+ id: string | null
4
+ timeElapsedSeconds?: number
5
+ }
6
+ class TopperTracker {
7
+ private startTime: number
8
+ private totalVisibleTime: number
9
+ private timeElapsedSeconds: number | null
10
+ private type: string
11
+ private component: Element | null
12
+ private id: string | null
13
+ private observer: IntersectionObserver | null
14
+
15
+ constructor({ type = 'flourish-topper' } = {}) {
16
+ this.startTime = 0
17
+ this.totalVisibleTime = 0
18
+ this.timeElapsedSeconds = null
19
+ this.type = type
20
+ this.component = document.querySelector(
21
+ `[data-component-type="${this.type}"]`
22
+ )
23
+ this.id = this.component?.getAttribute('data-component-id') || null
24
+ this.observer = null
25
+ }
26
+
27
+ init(): void {
28
+ if (!window.IntersectionObserver || !this.component) {
29
+ return
30
+ }
31
+
32
+ this.dispatchEvent('mount')
33
+
34
+ this.observer = new IntersectionObserver(this.onChange.bind(this), {
35
+ threshold: [0.75],
36
+ })
37
+ this.observer.observe(this.component)
38
+ }
39
+
40
+ private dispatchEvent(action: string): void {
41
+ const component: Component = {
42
+ name: this.type,
43
+ id: this.id,
44
+ }
45
+ if (this.timeElapsedSeconds) {
46
+ component.timeElapsedSeconds = this.timeElapsedSeconds
47
+ }
48
+ const event = new CustomEvent('oTracking.event', {
49
+ detail: {
50
+ category: 'component',
51
+ action: action,
52
+ component,
53
+ },
54
+ bubbles: true,
55
+ })
56
+ document.body.dispatchEvent(event)
57
+ }
58
+
59
+ private onChange(changes: IntersectionObserverEntry[]): void {
60
+ changes.forEach((change) => {
61
+ if (change.target !== this.component) {
62
+ return
63
+ }
64
+ if (change.isIntersecting || change.intersectionRatio >= 1) {
65
+ this.dispatchEvent('view')
66
+ this.startTime = performance.now()
67
+ }
68
+ if (!change.isIntersecting || change.intersectionRatio === 0) {
69
+ this.totalVisibleTime = performance.now() - this.startTime
70
+ this.timeElapsedSeconds = parseFloat(
71
+ (this.totalVisibleTime / 1000).toFixed(2)
72
+ )
73
+ this.dispatchEvent('stop-view')
74
+ this.totalVisibleTime = 0
75
+ this.timeElapsedSeconds = null
76
+ }
77
+ })
78
+ }
79
+
80
+ disconnect(): void {
81
+ if (this.observer && this.component) {
82
+ this.observer.unobserve(this.component)
83
+ this.observer.disconnect()
84
+ }
85
+ }
86
+ }
87
+
88
+ export { TopperTracker }