@financial-times/cp-content-pipeline-ui 13.3.2 → 13.4.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,13 @@
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
+ ## [13.4.0](https://github.com/Financial-Times/cp-content-pipeline/compare/cp-content-pipeline-ui-v13.3.2...cp-content-pipeline-ui-v13.4.0) (2026-09-17)
578
+
579
+
580
+ ### Features
581
+
582
+ * horizontal scrolling in ft-app ([6e31e9a](https://github.com/Financial-Times/cp-content-pipeline/commit/6e31e9aea847009cb426b9b83828145fd1d0bba1))
583
+
577
584
  ## [13.3.2](https://github.com/Financial-Times/cp-content-pipeline/compare/cp-content-pipeline-ui-v13.3.1...cp-content-pipeline-ui-v13.3.2) (2026-09-15)
578
585
 
579
586
 
@@ -10,7 +10,11 @@ declare class CustomCodeComponentClient {
10
10
  * Track the mount of a custom code component
11
11
  */
12
12
  tracking: CustomCodeComponentTracker;
13
- constructor(el: Element);
13
+ ftAppSwipeGestures: boolean | undefined;
14
+ element: Element;
15
+ constructor(el: Element, ftAppSwipeGestures?: boolean);
16
+ private cccTouched;
17
+ private cccReleased;
14
18
  static init(rootEl?: Element): CustomCodeComponentClient[] | void;
15
19
  static destroy(components: CustomCodeComponentClient[]): void;
16
20
  }
@@ -12,7 +12,11 @@ class CustomCodeComponentClient {
12
12
  * Track the mount of a custom code component
13
13
  */
14
14
  tracking;
15
- constructor(el) {
15
+ ftAppSwipeGestures;
16
+ element;
17
+ constructor(el, ftAppSwipeGestures) {
18
+ this.element = el;
19
+ this.ftAppSwipeGestures = ftAppSwipeGestures;
16
20
  const trackingData = {
17
21
  component: {
18
22
  id: el.getAttribute('id') ?? '',
@@ -21,14 +25,45 @@ class CustomCodeComponentClient {
21
25
  },
22
26
  };
23
27
  this.tracking = new tracking_1.CustomCodeComponentTracker(el, trackingData);
28
+ // ft-app only: ftAppSwipeGestures is false by default,
29
+ // meaning horizontal swipe gestures navigate to adjacent articles in the swipe flow
30
+ // if set to true, the ccc will signal to ft-app that the component swipe gestures should be given precendence.
31
+ // useful for horizontal scrolling of elements such as carousels
32
+ if (this.ftAppSwipeGestures) {
33
+ el.addEventListener('pointerdown', this.cccTouched);
34
+ el.addEventListener('pointerup', this.cccReleased);
35
+ el.addEventListener('pointercancel', this.cccReleased);
36
+ }
24
37
  }
38
+ cccTouched = () => {
39
+ if (!window.ftmob_scrolling) {
40
+ window.ftmob_scrolling = this;
41
+ }
42
+ };
43
+ cccReleased = () => {
44
+ if (window.ftmob_scrolling === this) {
45
+ window.ftmob_scrolling = false;
46
+ }
47
+ };
25
48
  static init(rootEl) {
26
49
  const root = rootEl instanceof Element ? rootEl : document;
27
- return Array.from(root.querySelectorAll('custom-code-component:not([data-initialised])')).map((el) => new CustomCodeComponentClient(el));
50
+ return Array.from(root.querySelectorAll('custom-code-component:not([data-initialised])')).map((el) => {
51
+ const ftAppSwipeGestures = el.getAttribute('enable-ft-app-swipe-gestures') === 'true';
52
+ const client = new CustomCodeComponentClient(el, ftAppSwipeGestures);
53
+ return client;
54
+ });
28
55
  }
29
56
  static destroy(components) {
30
57
  components.forEach((component) => {
31
58
  component.tracking.destroy();
59
+ if (component.ftAppSwipeGestures) {
60
+ component.element.removeEventListener('pointerdown', component.cccTouched);
61
+ component.element.removeEventListener('pointerup', component.cccReleased);
62
+ component.element.removeEventListener('pointercancel', component.cccReleased);
63
+ if (window.ftmob_scrolling === component) {
64
+ window.ftmob_scrolling = false;
65
+ }
66
+ }
32
67
  });
33
68
  }
34
69
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/components/content-tree/CustomCodeComponent/client/index.ts"],"names":[],"mappings":";;AAAA,yCAAuD;AAEvD;;;;;GAKG;AACH,MAAM,yBAAyB;IAC7B;;OAEG;IACH,QAAQ,CAA4B;IAEpC,YAAY,EAAW;QACrB,MAAM,YAAY,GAAG;YACnB,SAAS,EAAE;gBACT,EAAE,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE;gBAC/B,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE;gBACnC,IAAI,EAAE,uBAAuB;aAC9B;SACF,CAAA;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,qCAA0B,CAC5C,EAAiB,EACjB,YAAY,CACb,CAAA;IACH,CAAC;IAED,MAAM,CAAC,IAAI,CAAC,MAAgB;QAC1B,MAAM,IAAI,GAAG,MAAM,YAAY,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAA;QAC1D,OAAO,KAAK,CAAC,IAAI,CACf,IAAI,CAAC,gBAAgB,CAAC,+CAA+C,CAAC,CACvE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,yBAAyB,CAAC,EAAE,CAAC,CAAC,CAAA;IAClD,CAAC;IAED,MAAM,CAAC,OAAO,CAAC,UAAuC;QACpD,UAAU,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,EAAE;YAC/B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAA;QAC9B,CAAC,CAAC,CAAA;IACJ,CAAC;CACF;AAED,kBAAe,yBAAyB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/components/content-tree/CustomCodeComponent/client/index.ts"],"names":[],"mappings":";;AAAA,yCAAuD;AAEvD;;;;;GAKG;AACH,MAAM,yBAAyB;IAC7B;;OAEG;IACH,QAAQ,CAA4B;IACpC,kBAAkB,CAAqB;IACvC,OAAO,CAAS;IAEhB,YAAY,EAAW,EAAE,kBAA4B;QACnD,IAAI,CAAC,OAAO,GAAG,EAAE,CAAA;QACjB,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAA;QAC5C,MAAM,YAAY,GAAG;YACnB,SAAS,EAAE;gBACT,EAAE,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE;gBAC/B,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE;gBACnC,IAAI,EAAE,uBAAuB;aAC9B;SACF,CAAA;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,qCAA0B,CAC5C,EAAiB,EACjB,YAAY,CACb,CAAA;QAED,uDAAuD;QACvD,oFAAoF;QACpF,+GAA+G;QAC/G,gEAAgE;QAChE,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC5B,EAAE,CAAC,gBAAgB,CAAC,aAAa,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;YACnD,EAAE,CAAC,gBAAgB,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,CAAA;YAClD,EAAE,CAAC,gBAAgB,CAAC,eAAe,EAAE,IAAI,CAAC,WAAW,CAAC,CAAA;QACxD,CAAC;IACH,CAAC;IAEO,UAAU,GAAG,GAAS,EAAE;QAC9B,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;YAC5B,MAAM,CAAC,eAAe,GAAG,IAAI,CAAA;QAC/B,CAAC;IACH,CAAC,CAAA;IAEO,WAAW,GAAG,GAAS,EAAE;QAC/B,IAAI,MAAM,CAAC,eAAe,KAAK,IAAI,EAAE,CAAC;YACpC,MAAM,CAAC,eAAe,GAAG,KAAK,CAAA;QAChC,CAAC;IACH,CAAC,CAAA;IAED,MAAM,CAAC,IAAI,CAAC,MAAgB;QAC1B,MAAM,IAAI,GAAG,MAAM,YAAY,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAA;QAC1D,OAAO,KAAK,CAAC,IAAI,CACf,IAAI,CAAC,gBAAgB,CAAC,+CAA+C,CAAC,CACvE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;YACX,MAAM,kBAAkB,GACtB,EAAE,CAAC,YAAY,CAAC,8BAA8B,CAAC,KAAK,MAAM,CAAA;YAC5D,MAAM,MAAM,GAAG,IAAI,yBAAyB,CAAC,EAAE,EAAE,kBAAkB,CAAC,CAAA;YACpE,OAAO,MAAM,CAAA;QACf,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,MAAM,CAAC,OAAO,CAAC,UAAuC;QACpD,UAAU,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,EAAE;YAC/B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAA;YAC5B,IAAI,SAAS,CAAC,kBAAkB,EAAE,CAAC;gBACjC,SAAS,CAAC,OAAO,CAAC,mBAAmB,CACnC,aAAa,EACb,SAAS,CAAC,UAAU,CACrB,CAAA;gBACD,SAAS,CAAC,OAAO,CAAC,mBAAmB,CACnC,WAAW,EACX,SAAS,CAAC,WAAW,CACtB,CAAA;gBACD,SAAS,CAAC,OAAO,CAAC,mBAAmB,CACnC,eAAe,EACf,SAAS,CAAC,WAAW,CACtB,CAAA;gBACD,IAAI,MAAM,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;oBACzC,MAAM,CAAC,eAAe,GAAG,KAAK,CAAA;gBAChC,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;CACF;AAED,kBAAe,yBAAyB,CAAA"}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const tracking_1 = require("./tracking");
7
+ const index_1 = __importDefault(require("./index"));
8
+ jest.mock('./tracking', () => ({
9
+ CustomCodeComponentTracker: jest.fn(),
10
+ }));
11
+ describe('CustomCodeComponentClient', () => {
12
+ beforeEach(() => {
13
+ document.body.innerHTML = '';
14
+ });
15
+ it('should initialize correctly', () => {
16
+ const el = document.createElement('custom-code-component');
17
+ el.setAttribute('id', 'test-id');
18
+ el.setAttribute('path', 'test-path');
19
+ document.body.appendChild(el);
20
+ const clients = index_1.default.init();
21
+ expect(clients).toHaveLength(1);
22
+ const client = clients[0];
23
+ expect(client).toBeInstanceOf(index_1.default);
24
+ expect(tracking_1.CustomCodeComponentTracker).toHaveBeenCalledWith(el, {
25
+ component: {
26
+ id: 'test-id',
27
+ name: 'test-path',
28
+ type: 'custom-code-component',
29
+ },
30
+ });
31
+ });
32
+ it('should return an empty array if no elements are present', () => {
33
+ const clients = index_1.default.init();
34
+ expect(clients).toHaveLength(0);
35
+ });
36
+ it('should handle multiple elements correctly', () => {
37
+ const el1 = document.createElement('custom-code-component');
38
+ el1.setAttribute('id', 'test-id-1');
39
+ el1.setAttribute('path', 'test-path-1');
40
+ document.body.appendChild(el1);
41
+ const el2 = document.createElement('custom-code-component');
42
+ el2.setAttribute('id', 'test-id-2');
43
+ el2.setAttribute('path', 'test-path-2');
44
+ document.body.appendChild(el2);
45
+ const clients = index_1.default.init();
46
+ expect(clients).toHaveLength(2);
47
+ expect(tracking_1.CustomCodeComponentTracker).toHaveBeenCalledWith(el1, {
48
+ component: {
49
+ id: 'test-id-1',
50
+ name: 'test-path-1',
51
+ type: 'custom-code-component',
52
+ },
53
+ });
54
+ expect(tracking_1.CustomCodeComponentTracker).toHaveBeenCalledWith(el2, {
55
+ component: {
56
+ id: 'test-id-2',
57
+ name: 'test-path-2',
58
+ type: 'custom-code-component',
59
+ },
60
+ });
61
+ });
62
+ it('should initialise with ft app scrolling if the element has the appropriate attribute', () => {
63
+ const el = document.createElement('custom-code-component');
64
+ el.setAttribute('id', 'test-id');
65
+ el.setAttribute('path', 'test-path');
66
+ el.setAttribute('enable-ft-app-swipe-gestures', 'true');
67
+ document.body.appendChild(el);
68
+ const client = index_1.default.init()[0];
69
+ expect(client?.ftAppSwipeGestures).toBe(true);
70
+ });
71
+ it('should not initialize with ft app scrolling if the attribute is not set to true', () => {
72
+ const el = document.createElement('custom-code-component');
73
+ el.setAttribute('id', 'test-id');
74
+ el.setAttribute('path', 'test-path');
75
+ el.setAttribute('enable-ft-app-swipe-gestures', 'false');
76
+ document.body.appendChild(el);
77
+ const client = index_1.default.init()[0];
78
+ expect(client?.ftAppSwipeGestures).toBe(false);
79
+ });
80
+ it('should not initialize with ft app scrolling if the attribute is missing', () => {
81
+ const el = document.createElement('custom-code-component');
82
+ el.setAttribute('id', 'test-id');
83
+ el.setAttribute('path', 'test-path');
84
+ document.body.appendChild(el);
85
+ const client = index_1.default.init()[0];
86
+ expect(client?.ftAppSwipeGestures).toBe(false);
87
+ });
88
+ });
89
+ //# sourceMappingURL=index.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.spec.js","sourceRoot":"","sources":["../../../../../src/components/content-tree/CustomCodeComponent/client/index.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": "13.3.2",
3
+ "version": "13.4.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -0,0 +1,96 @@
1
+ import { CustomCodeComponentTracker } from './tracking'
2
+ import CustomCodeComponentClient from './index'
3
+
4
+ jest.mock('./tracking', () => ({
5
+ CustomCodeComponentTracker: jest.fn(),
6
+ }))
7
+
8
+ describe('CustomCodeComponentClient', () => {
9
+ beforeEach(() => {
10
+ document.body.innerHTML = ''
11
+ })
12
+ it('should initialize correctly', () => {
13
+ const el = document.createElement('custom-code-component')
14
+ el.setAttribute('id', 'test-id')
15
+ el.setAttribute('path', 'test-path')
16
+ document.body.appendChild(el)
17
+
18
+ const clients = CustomCodeComponentClient.init()
19
+ expect(clients).toHaveLength(1)
20
+ const client = clients![0]
21
+ expect(client).toBeInstanceOf(CustomCodeComponentClient)
22
+ expect(CustomCodeComponentTracker).toHaveBeenCalledWith(el, {
23
+ component: {
24
+ id: 'test-id',
25
+ name: 'test-path',
26
+ type: 'custom-code-component',
27
+ },
28
+ })
29
+ })
30
+
31
+ it('should return an empty array if no elements are present', () => {
32
+ const clients = CustomCodeComponentClient.init()
33
+ expect(clients).toHaveLength(0)
34
+ })
35
+
36
+ it('should handle multiple elements correctly', () => {
37
+ const el1 = document.createElement('custom-code-component')
38
+ el1.setAttribute('id', 'test-id-1')
39
+ el1.setAttribute('path', 'test-path-1')
40
+ document.body.appendChild(el1)
41
+
42
+ const el2 = document.createElement('custom-code-component')
43
+ el2.setAttribute('id', 'test-id-2')
44
+ el2.setAttribute('path', 'test-path-2')
45
+ document.body.appendChild(el2)
46
+
47
+ const clients = CustomCodeComponentClient.init()
48
+ expect(clients).toHaveLength(2)
49
+ expect(CustomCodeComponentTracker).toHaveBeenCalledWith(el1, {
50
+ component: {
51
+ id: 'test-id-1',
52
+ name: 'test-path-1',
53
+ type: 'custom-code-component',
54
+ },
55
+ })
56
+ expect(CustomCodeComponentTracker).toHaveBeenCalledWith(el2, {
57
+ component: {
58
+ id: 'test-id-2',
59
+ name: 'test-path-2',
60
+ type: 'custom-code-component',
61
+ },
62
+ })
63
+ })
64
+
65
+ it('should initialise with ft app scrolling if the element has the appropriate attribute', () => {
66
+ const el = document.createElement('custom-code-component')
67
+ el.setAttribute('id', 'test-id')
68
+ el.setAttribute('path', 'test-path')
69
+ el.setAttribute('enable-ft-app-swipe-gestures', 'true')
70
+ document.body.appendChild(el)
71
+
72
+ const client = CustomCodeComponentClient.init()![0]
73
+ expect(client?.ftAppSwipeGestures).toBe(true)
74
+ })
75
+
76
+ it('should not initialize with ft app scrolling if the attribute is not set to true', () => {
77
+ const el = document.createElement('custom-code-component')
78
+ el.setAttribute('id', 'test-id')
79
+ el.setAttribute('path', 'test-path')
80
+ el.setAttribute('enable-ft-app-swipe-gestures', 'false')
81
+ document.body.appendChild(el)
82
+
83
+ const client = CustomCodeComponentClient.init()![0]
84
+ expect(client?.ftAppSwipeGestures).toBe(false)
85
+ })
86
+
87
+ it('should not initialize with ft app scrolling if the attribute is missing', () => {
88
+ const el = document.createElement('custom-code-component')
89
+ el.setAttribute('id', 'test-id')
90
+ el.setAttribute('path', 'test-path')
91
+ document.body.appendChild(el)
92
+
93
+ const client = CustomCodeComponentClient.init()![0]
94
+ expect(client?.ftAppSwipeGestures).toBe(false)
95
+ })
96
+ })
@@ -11,8 +11,12 @@ class CustomCodeComponentClient {
11
11
  * Track the mount of a custom code component
12
12
  */
13
13
  tracking: CustomCodeComponentTracker
14
+ ftAppSwipeGestures: boolean | undefined
15
+ element: Element
14
16
 
15
- constructor(el: Element) {
17
+ constructor(el: Element, ftAppSwipeGestures?: boolean) {
18
+ this.element = el
19
+ this.ftAppSwipeGestures = ftAppSwipeGestures
16
20
  const trackingData = {
17
21
  component: {
18
22
  id: el.getAttribute('id') ?? '',
@@ -24,18 +28,62 @@ class CustomCodeComponentClient {
24
28
  el as HTMLElement,
25
29
  trackingData
26
30
  )
31
+
32
+ // ft-app only: ftAppSwipeGestures is false by default,
33
+ // meaning horizontal swipe gestures navigate to adjacent articles in the swipe flow
34
+ // if set to true, the ccc will signal to ft-app that the component swipe gestures should be given precendence.
35
+ // useful for horizontal scrolling of elements such as carousels
36
+ if (this.ftAppSwipeGestures) {
37
+ el.addEventListener('pointerdown', this.cccTouched)
38
+ el.addEventListener('pointerup', this.cccReleased)
39
+ el.addEventListener('pointercancel', this.cccReleased)
40
+ }
41
+ }
42
+
43
+ private cccTouched = (): void => {
44
+ if (!window.ftmob_scrolling) {
45
+ window.ftmob_scrolling = this
46
+ }
47
+ }
48
+
49
+ private cccReleased = (): void => {
50
+ if (window.ftmob_scrolling === this) {
51
+ window.ftmob_scrolling = false
52
+ }
27
53
  }
28
54
 
29
55
  static init(rootEl?: Element): CustomCodeComponentClient[] | void {
30
56
  const root = rootEl instanceof Element ? rootEl : document
31
57
  return Array.from(
32
58
  root.querySelectorAll('custom-code-component:not([data-initialised])')
33
- ).map((el) => new CustomCodeComponentClient(el))
59
+ ).map((el) => {
60
+ const ftAppSwipeGestures =
61
+ el.getAttribute('enable-ft-app-swipe-gestures') === 'true'
62
+ const client = new CustomCodeComponentClient(el, ftAppSwipeGestures)
63
+ return client
64
+ })
34
65
  }
35
66
 
36
67
  static destroy(components: CustomCodeComponentClient[]): void {
37
68
  components.forEach((component) => {
38
69
  component.tracking.destroy()
70
+ if (component.ftAppSwipeGestures) {
71
+ component.element.removeEventListener(
72
+ 'pointerdown',
73
+ component.cccTouched
74
+ )
75
+ component.element.removeEventListener(
76
+ 'pointerup',
77
+ component.cccReleased
78
+ )
79
+ component.element.removeEventListener(
80
+ 'pointercancel',
81
+ component.cccReleased
82
+ )
83
+ if (window.ftmob_scrolling === component) {
84
+ window.ftmob_scrolling = false
85
+ }
86
+ }
39
87
  })
40
88
  }
41
89
  }
@@ -1,9 +1,10 @@
1
1
  import type CarouselClient from '../components/content-tree/Carousel/client'
2
+ import type CustomCodeComponentClient from '../components/content-tree/CustomCodeComponent/client'
2
3
 
3
4
  export {}
4
5
 
5
6
  declare global {
6
7
  interface Window {
7
- ftmob_scrolling?: CarouselClient | false
8
+ ftmob_scrolling?: CarouselClient | CustomCodeComponentClient | false
8
9
  }
9
10
  }