@financial-times/cp-content-pipeline-ui 6.11.5 → 6.13.0-beta.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/lib/client.d.ts +1 -0
  3. package/lib/client.js +3 -1
  4. package/lib/client.js.map +1 -1
  5. package/lib/components/Body/index.test.js +1 -0
  6. package/lib/components/Body/index.test.js.map +1 -1
  7. package/lib/components/Flourish/index.js +1 -1
  8. package/lib/components/Flourish/index.js.map +1 -1
  9. package/lib/components/ImageSet/index.d.ts +1 -1
  10. package/lib/components/ImageSet/index.js +10 -2
  11. package/lib/components/ImageSet/index.js.map +1 -1
  12. package/lib/components/LiveBlogWrapper/index.js +1 -1
  13. package/lib/components/LiveBlogWrapper/index.js.map +1 -1
  14. package/lib/components/PartnerContentHeader/index.d.ts +1 -1
  15. package/lib/components/PartnerContentHeader/index.js +2 -2
  16. package/lib/components/PartnerContentHeader/index.js.map +1 -1
  17. package/lib/components/Topper/Picture.js +2 -2
  18. package/lib/components/Topper/Picture.js.map +1 -1
  19. package/lib/components/Topper/client/flourish-tracking.d.ts +14 -0
  20. package/lib/components/Topper/client/flourish-tracking.js +69 -0
  21. package/lib/components/Topper/client/flourish-tracking.js.map +1 -0
  22. package/lib/components/Topper/client/index.d.ts +2 -0
  23. package/lib/components/Topper/client/index.js +6 -0
  24. package/lib/components/Topper/client/index.js.map +1 -0
  25. package/lib/components/Topper/test/client/flourish-tracking.spec.d.ts +1 -0
  26. package/lib/components/Topper/test/client/flourish-tracking.spec.js +117 -0
  27. package/lib/components/Topper/test/client/flourish-tracking.spec.js.map +1 -0
  28. package/package.json +1 -1
  29. package/src/client.ts +1 -0
  30. package/src/components/Body/index.test.tsx +1 -0
  31. package/src/components/Flourish/index.tsx +5 -0
  32. package/src/components/Flourish/test/__snapshots__/snapshot.spec.tsx.snap +18 -0
  33. package/src/components/ImageSet/index.tsx +21 -11
  34. package/src/components/LiveBlogWrapper/index.tsx +1 -1
  35. package/src/components/PartnerContentHeader/index.tsx +2 -6
  36. package/src/components/Topper/Picture.tsx +2 -2
  37. package/src/components/Topper/client/flourish-tracking.ts +83 -0
  38. package/src/components/Topper/client/index.ts +3 -0
  39. package/src/components/Topper/test/client/flourish-tracking.spec.ts +135 -0
  40. package/tsconfig.tsbuildinfo +1 -1
@@ -43,7 +43,7 @@ export type AliasedBreakpoints = {
43
43
 
44
44
  const defaultGetBreakpoints = (
45
45
  image: ImageFragment
46
- ): Breakpoint | AliasedBreakpoints | undefined => {
46
+ ): Breakpoint | Array<Breakpoint> | AliasedBreakpoints | undefined => {
47
47
  switch (image.format) {
48
48
  case 'mobile':
49
49
  return { maxWidth: 490 }
@@ -73,7 +73,11 @@ function hasSourceSet(image: ImageFragment): image is ImageWithSourceSet {
73
73
  function isAliasedBreakpoints(
74
74
  breakpoints: ReturnType<BreakpointGetter>
75
75
  ): breakpoints is AliasedBreakpoints {
76
- return Boolean(breakpoints && Object.values(breakpoints)[0] instanceof Object)
76
+ return Boolean(
77
+ breakpoints &&
78
+ Object.values(breakpoints)[0] instanceof Object &&
79
+ !Array.isArray(breakpoints)
80
+ )
77
81
  }
78
82
 
79
83
  type SourceProps = {
@@ -113,16 +117,22 @@ function Source({
113
117
  {Object.entries(breakpointsDict)
114
118
  .map(([key, breakpoint]) => {
115
119
  const sourceSet = (image as ImageWithAliases)[key]
120
+ const sourceSetMap = Array.isArray(breakpoint)
121
+ ? breakpoint
122
+ : [breakpoint]
116
123
  return (
117
- sourceSet && (
118
- <source
119
- key={key}
120
- media={breakpointToMedia(breakpoint)}
121
- srcSet={formatSourceSet(sourceSet)}
122
- width={image.width ?? undefined}
123
- height={image.height ?? undefined}
124
- />
125
- )
124
+ sourceSet &&
125
+ sourceSetMap.map((breakpoint) => {
126
+ return (
127
+ <source
128
+ key={key}
129
+ media={breakpointToMedia(breakpoint)}
130
+ srcSet={formatSourceSet(sourceSet)}
131
+ width={image.width ?? undefined}
132
+ height={image.height ?? undefined}
133
+ />
134
+ )
135
+ })
126
136
  )
127
137
  })
128
138
  .filter((elem) => elem)}
@@ -111,7 +111,7 @@ class BaseLiveBlogWrapper extends Component<
111
111
  */
112
112
  const config = {
113
113
  query:
114
- 'article[data-trackable="live-post"],article[data-trackable="pinned-post"]',
114
+ 'article[data-trackable="live-post"],article[data-trackable="pinned-post"],[data-component-type="flourish-topper"]',
115
115
  minMillisecondsToReport: 5000,
116
116
  returnVisibleElement: true,
117
117
  observerUpdateEventString: 'LiveBlogWrapper.INSERT_POST',
@@ -1,11 +1,7 @@
1
1
  import React from 'react'
2
2
  import { TooltipToggle } from '@financial-times/o3-tooltip'
3
3
 
4
- const PartnerContentHeader = ({
5
- name = 'Default Partner',
6
- }: {
7
- name: string
8
- }) => {
4
+ const PartnerContentHeader = ({ name = 'Partner' }: { name: string }) => {
9
5
  return (
10
6
  <div className="partner-content-header">
11
7
  <div className="partner-content-header__content">
@@ -15,7 +11,7 @@ const PartnerContentHeader = ({
15
11
  <div data-o3-brand="core">
16
12
  <TooltipToggle
17
13
  placement="bottom"
18
- content={`This advertisement has been produced by the commercial department of the Financial Times on behalf of ${name}`}
14
+ content={`This content was paid for by ${name} and produced in partnership with the Financial Times Commercial department`}
19
15
  infoLabel="More information on Partner Content"
20
16
  />
21
17
  </div>
@@ -15,9 +15,9 @@ const topperGetBreakpointsMap: Partial<
15
15
  SplitTextTopper(image) {
16
16
  switch (image.format) {
17
17
  case 'square':
18
- return { maxWidth: 490 }
18
+ return [{ maxWidth: 490 }, { minWidth: 740, maxWidth: 1220 }]
19
19
  case 'standard':
20
- return { minWidth: 491, maxWidth: 1220 }
20
+ return { minWidth: 491, maxWidth: 739 }
21
21
  case 'wide':
22
22
  return {
23
23
  normal: { minWidth: 1221, maxWidth: 1440 },
@@ -0,0 +1,83 @@
1
+ class FlourishTopperTracker {
2
+ private startTime: number
3
+ private totalVisibleTime: number
4
+ private timeElapsedSeconds: number | null
5
+ private component: Element | null
6
+ private id: string | null
7
+ private observer: IntersectionObserver | null
8
+
9
+ constructor() {
10
+ this.startTime = 0
11
+ this.totalVisibleTime = 0
12
+ this.timeElapsedSeconds = null
13
+ this.component = document.querySelector(
14
+ '[data-component-type="flourish-topper"]'
15
+ )
16
+ this.id = this.component?.getAttribute('data-component-id') || null
17
+ this.observer = null
18
+ }
19
+
20
+ init(): void {
21
+ if (!window.IntersectionObserver || !this.component) {
22
+ return
23
+ }
24
+
25
+ if (this.component) {
26
+ this.dispatchEvent('mount')
27
+ }
28
+
29
+ this.observer = new IntersectionObserver(this.onChange.bind(this), {
30
+ threshold: [1.0],
31
+ })
32
+ this.observer.observe(this.component)
33
+ }
34
+
35
+ private dispatchEvent(action: string): void {
36
+ let component: object = {
37
+ name: 'flourish-topper',
38
+ id: this.id,
39
+ }
40
+ if (this.timeElapsedSeconds) {
41
+ component = { ...component, timeElapsedSeconds: this.timeElapsedSeconds }
42
+ }
43
+ const event = new CustomEvent('oTracking.event', {
44
+ detail: {
45
+ category: 'component',
46
+ action: action,
47
+ component,
48
+ },
49
+ bubbles: true,
50
+ })
51
+ document.body.dispatchEvent(event)
52
+ }
53
+
54
+ private onChange(changes: IntersectionObserverEntry[]): void {
55
+ changes.forEach((change) => {
56
+ if (change.target !== this.component) {
57
+ return
58
+ }
59
+ if (change.isIntersecting || change.intersectionRatio >= 1) {
60
+ this.dispatchEvent('view')
61
+ this.startTime = performance.now()
62
+ }
63
+ if (!change.isIntersecting || change.intersectionRatio === 0) {
64
+ this.totalVisibleTime = performance.now() - this.startTime
65
+ this.timeElapsedSeconds = parseFloat(
66
+ (this.totalVisibleTime / 1000).toFixed(2)
67
+ )
68
+ this.dispatchEvent('stop-view')
69
+ this.totalVisibleTime = 0
70
+ this.timeElapsedSeconds = null
71
+ }
72
+ })
73
+ }
74
+
75
+ disconnect(): void {
76
+ if (this.observer && this.component) {
77
+ this.observer.unobserve(this.component)
78
+ this.observer.disconnect()
79
+ }
80
+ }
81
+ }
82
+
83
+ export { FlourishTopperTracker }
@@ -0,0 +1,3 @@
1
+ import { FlourishTopperTracker } from './flourish-tracking'
2
+
3
+ export { FlourishTopperTracker }
@@ -0,0 +1,135 @@
1
+ import { FlourishTopperTracker } from '../../client'
2
+
3
+ // const CustomEventMock = jest.fn()
4
+ // global.CustomEvent = CustomEventMock
5
+
6
+ describe('FlourishTopperTracker', () => {
7
+ let mockIntersectionObserver: jest.Mock
8
+ let mockObserve: jest.Mock
9
+ let mockUnobserve: jest.Mock
10
+ let mockDisconnect: jest.Mock
11
+ let mockComponent: HTMLElement
12
+ let dispatchEventSpy: jest.SpyInstance
13
+
14
+ beforeEach(() => {
15
+ mockObserve = jest.fn()
16
+ mockUnobserve = jest.fn()
17
+ mockDisconnect = jest.fn()
18
+
19
+ mockIntersectionObserver = jest.fn(() => {
20
+ return {
21
+ observe: mockObserve,
22
+ unobserve: mockUnobserve,
23
+ disconnect: mockDisconnect,
24
+ }
25
+ })
26
+
27
+ window.IntersectionObserver = mockIntersectionObserver
28
+
29
+ mockComponent = document.createElement('div')
30
+ mockComponent.setAttribute('data-component-type', 'flourish-topper')
31
+ mockComponent.setAttribute('data-component-id', 'test-id')
32
+ document.body.appendChild(mockComponent)
33
+
34
+ jest.spyOn(document, 'querySelector').mockReturnValue(mockComponent)
35
+ jest
36
+ .spyOn(performance, 'now')
37
+ .mockImplementationOnce(() => 1)
38
+ .mockImplementationOnce(() => 12344)
39
+ dispatchEventSpy = jest.spyOn(document.body, 'dispatchEvent')
40
+ })
41
+
42
+ afterEach(() => {
43
+ jest.clearAllMocks()
44
+ document.body.removeChild(mockComponent)
45
+ })
46
+
47
+ it('should initialise and observe the component', () => {
48
+ const tracker = new FlourishTopperTracker()
49
+ tracker.init()
50
+
51
+ expect(mockIntersectionObserver).toHaveBeenCalled()
52
+ expect(mockObserve).toHaveBeenCalledWith(mockComponent)
53
+ })
54
+
55
+ it('should dispatch a mount event on intialisation', () => {
56
+ const tracker = new FlourishTopperTracker()
57
+ tracker.init()
58
+
59
+ const mountEvent = dispatchEventSpy.mock.calls[0][0] as CustomEvent
60
+ expect(mountEvent.type).toBe('oTracking.event')
61
+ expect(mountEvent.detail).toEqual({
62
+ category: 'component',
63
+ action: 'mount',
64
+ component: {
65
+ name: 'flourish-topper',
66
+ id: 'test-id',
67
+ },
68
+ })
69
+ })
70
+
71
+ it('should handle component visibility changes', () => {
72
+ const tracker = new FlourishTopperTracker()
73
+ tracker.init()
74
+
75
+ const mockChanges: IntersectionObserverEntry[] = [
76
+ {
77
+ target: mockComponent,
78
+ isIntersecting: true,
79
+ intersectionRatio: 1.0,
80
+ boundingClientRect: {} as DOMRectReadOnly,
81
+ intersectionRect: {} as DOMRectReadOnly,
82
+ rootBounds: null,
83
+ time: 1,
84
+ },
85
+ ]
86
+
87
+ tracker['onChange'](mockChanges)
88
+
89
+ const viewEvent = dispatchEventSpy.mock.calls[1][0] as CustomEvent
90
+ expect(viewEvent.type).toBe('oTracking.event')
91
+ expect(viewEvent.detail).toEqual({
92
+ category: 'component',
93
+ action: 'view',
94
+ component: {
95
+ name: 'flourish-topper',
96
+ id: 'test-id',
97
+ },
98
+ })
99
+
100
+ const mockChangesAfter: IntersectionObserverEntry[] = [
101
+ {
102
+ target: mockComponent,
103
+ isIntersecting: false,
104
+ intersectionRatio: 0.0,
105
+ boundingClientRect: {} as DOMRectReadOnly,
106
+ intersectionRect: {} as DOMRectReadOnly,
107
+ rootBounds: null,
108
+ time: 12.3444,
109
+ },
110
+ ]
111
+
112
+ tracker['onChange'](mockChangesAfter)
113
+
114
+ const stopViewEvent = dispatchEventSpy.mock.calls[2][0] as CustomEvent
115
+ expect(stopViewEvent.type).toBe('oTracking.event')
116
+ expect(stopViewEvent.detail).toEqual({
117
+ category: 'component',
118
+ action: 'stop-view',
119
+ component: {
120
+ name: 'flourish-topper',
121
+ id: 'test-id',
122
+ timeElapsedSeconds: 12.34,
123
+ },
124
+ })
125
+ })
126
+
127
+ it('should disconnect the observer', () => {
128
+ const tracker = new FlourishTopperTracker()
129
+ tracker.init()
130
+ tracker.disconnect()
131
+
132
+ expect(mockUnobserve).toHaveBeenCalledWith(mockComponent)
133
+ expect(mockDisconnect).toHaveBeenCalled()
134
+ })
135
+ })