@financial-times/cp-content-pipeline-ui 6.4.6 → 6.6.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 (52) hide show
  1. package/CHANGELOG.md +45 -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/Clip/client/index.d.ts +1 -0
  6. package/lib/components/Clip/client/index.js +77 -67
  7. package/lib/components/Clip/client/index.js.map +1 -1
  8. package/lib/components/Clip/client/progressBar.d.ts +0 -1
  9. package/lib/components/Clip/client/progressBar.js +10 -8
  10. package/lib/components/Clip/client/progressBar.js.map +1 -1
  11. package/lib/components/Clip/template/component.js +1 -1
  12. package/lib/components/Clip/template/component.js.map +1 -1
  13. package/lib/components/Clip/test/snapshot.spec.js +2 -0
  14. package/lib/components/Clip/test/snapshot.spec.js.map +1 -1
  15. package/lib/components/Flourish/client/index.d.ts +2 -0
  16. package/lib/components/Flourish/client/index.js +16 -0
  17. package/lib/components/Flourish/client/index.js.map +1 -0
  18. package/lib/components/Flourish/index.d.ts +16 -3
  19. package/lib/components/Flourish/index.js +21 -4
  20. package/lib/components/Flourish/index.js.map +1 -1
  21. package/lib/components/Flourish/test/snapshot.spec.d.ts +1 -0
  22. package/lib/components/Flourish/test/snapshot.spec.js +57 -0
  23. package/lib/components/Flourish/test/snapshot.spec.js.map +1 -0
  24. package/lib/components/Layout/index.d.ts +9 -3
  25. package/lib/components/Layout/index.js +7 -1
  26. package/lib/components/Layout/index.js.map +1 -1
  27. package/lib/components/Topper/index.d.ts +1 -2
  28. package/lib/components/Topper/index.js +6 -0
  29. package/lib/components/Topper/index.js.map +1 -1
  30. package/lib/stories/Topper.stories.d.ts +9 -0
  31. package/lib/stories/Topper.stories.js +23 -0
  32. package/lib/stories/Topper.stories.js.map +1 -0
  33. package/lib/stories/content.d.ts +65 -0
  34. package/lib/stories/content.js +82 -0
  35. package/lib/stories/content.js.map +1 -0
  36. package/package.json +6 -5
  37. package/src/client.ts +1 -0
  38. package/src/components/Clip/client/index.ts +86 -70
  39. package/src/components/Clip/client/progressBar.ts +12 -8
  40. package/src/components/Clip/template/component.tsx +1 -1
  41. package/src/components/Clip/test/snapshot.spec.tsx +2 -0
  42. package/src/components/Flourish/client/index.ts +17 -0
  43. package/src/components/Flourish/client/main.scss +38 -0
  44. package/src/components/Flourish/index.tsx +55 -6
  45. package/src/components/Flourish/test/__snapshots__/snapshot.spec.tsx.snap +310 -0
  46. package/src/components/Flourish/test/snapshot.spec.tsx +115 -0
  47. package/src/components/Layout/index.tsx +17 -4
  48. package/src/components/Topper/client/main.scss +5 -0
  49. package/src/components/Topper/index.tsx +21 -2
  50. package/src/stories/Topper.stories.tsx +22 -0
  51. package/src/stories/content.tsx +78 -0
  52. package/tsconfig.tsbuildinfo +1 -1
@@ -38,6 +38,7 @@ interface TrackingData {
38
38
  category: string
39
39
  contentId?: string
40
40
  action: string
41
+ url: string
41
42
  progress?: number
42
43
  amount?: number
43
44
  amountPercentage?: number
@@ -194,91 +195,105 @@ class Clip extends ClipInterface {
194
195
  }
195
196
 
196
197
  async init() {
197
- this.performSourceErrorCheck()
198
+ try {
199
+ this.performSourceErrorCheck()
200
+
201
+ if (
202
+ !this.opts.noDescription ||
203
+ !this.opts.noInfoBox ||
204
+ !this.opts.noCaption
205
+ ) {
206
+ await loadExpander()
207
+ }
198
208
 
199
- if (
200
- !this.opts.noDescription ||
201
- !this.opts.noInfoBox ||
202
- !this.opts.noCaption
203
- ) {
204
- await loadExpander()
205
- }
209
+ this.videoEl.addEventListener('playing', this.markPlayStart.bind(this))
210
+ this.videoEl.addEventListener(
211
+ 'pause',
212
+ this.updateAmountWatched.bind(this)
213
+ )
206
214
 
207
- this.videoEl.addEventListener('playing', this.markPlayStart.bind(this))
208
- this.videoEl.addEventListener('pause', this.updateAmountWatched.bind(this))
209
-
210
- this.addEvents([
211
- 'playing',
212
- 'pause',
213
- 'ended',
214
- 'seeked',
215
- 'error',
216
- 'stalled',
217
- 'waiting',
218
- 'timeupdate',
219
- ])
220
-
221
- /* When a video starts to autoplay the controls will be visible for about a second.
215
+ this.addEvents([
216
+ 'playing',
217
+ 'pause',
218
+ 'ended',
219
+ 'seeked',
220
+ 'error',
221
+ 'stalled',
222
+ 'waiting',
223
+ 'timeupdate',
224
+ ])
225
+
226
+ /* When a video starts to autoplay the controls will be visible for about a second.
222
227
  We want to remove the controls for the initial play, but also add them back in when the user
223
228
  wants to see the controls (mouseover focus, and press on a touchscreen).
224
229
  */
225
- if (this.opts.autoplay && !this.useCustomPlayer) {
226
- this.videoEl.addEventListener('mouseenter', () => {
227
- this.videoEl.setAttribute('controls', '')
228
- })
230
+ if (this.opts.autoplay && !this.useCustomPlayer) {
231
+ this.videoEl.addEventListener('mouseenter', () => {
232
+ this.videoEl.setAttribute('controls', '')
233
+ })
229
234
 
230
- this.videoEl.addEventListener('touchenter', () => {
231
- this.videoEl.setAttribute('controls', '')
232
- })
235
+ this.videoEl.addEventListener('touchenter', () => {
236
+ this.videoEl.setAttribute('controls', '')
237
+ })
233
238
 
234
- this.videoEl.addEventListener('focus', () => {
235
- this.videoEl.setAttribute('controls', '')
236
- })
237
- }
239
+ this.videoEl.addEventListener('focus', () => {
240
+ this.videoEl.setAttribute('controls', '')
241
+ })
242
+ }
238
243
 
239
- // send 'watched' event on page unload,
240
- window.addEventListener(unloadEventName, this.fireWatchedEvent, {
241
- capture: true,
242
- })
244
+ // send 'watched' event on page unload,
245
+ window.addEventListener(unloadEventName, this.fireWatchedEvent, {
246
+ capture: true,
247
+ })
243
248
 
244
- this.fireEvent('mount')
249
+ this.fireEvent('mount')
245
250
 
246
- const expander = Expander?.init(this.containerEl)
247
- this.expander = (Array.isArray(expander) ? expander[0] : expander) ?? null
251
+ const expander = Expander?.init(this.containerEl)
252
+ this.expander = (Array.isArray(expander) ? expander[0] : expander) ?? null
248
253
 
249
- const oExpanderElement = this.expander?.oExpanderElement
254
+ const oExpanderElement = this.expander?.oExpanderElement
250
255
 
251
- if (oExpanderElement) {
252
- const buttonElement = oExpanderElement.querySelector('button')
253
- if (buttonElement) {
254
- buttonElement.setAttribute('tabindex', '-1')
256
+ if (oExpanderElement) {
257
+ const buttonElement = oExpanderElement.querySelector('button')
258
+ if (buttonElement) {
259
+ buttonElement.setAttribute('tabindex', '-1')
260
+ }
255
261
  }
256
- }
257
262
 
258
- // Listen to offline/online events
259
- window.addEventListener('offline', this.onOfflineEvent)
260
- window.addEventListener('online', this.onOnlineEvent)
261
- // Listen to stalled event to show offline message
262
- this.videoEl.addEventListener('stalled', this.onOfflineEvent)
263
- // If the user play a buffered video offline, we need to hide the offline message.
264
- this.videoEl.addEventListener('playing', this.onOfflineEvent)
265
-
266
- // Mobile App, differently from DotCom, can render article offline, so we need to check if we are already offline.
267
- if (!window.navigator.onLine && this.expander) {
268
- this.showOffLineMessage()
269
- }
263
+ // Listen to offline/online events
264
+ window.addEventListener('offline', this.onOfflineEvent)
265
+ window.addEventListener('online', this.onOnlineEvent)
266
+ // Listen to stalled event to show offline message
267
+ this.videoEl.addEventListener('stalled', this.onOfflineEvent)
268
+ // If the user play a buffered video offline, we need to hide the offline message.
269
+ this.videoEl.addEventListener('playing', this.onOfflineEvent)
270
+
271
+ // Mobile App, differently from DotCom, can render article offline, so we need to check if we are already offline.
272
+ if (!window.navigator.onLine && this.expander) {
273
+ this.showOffLineMessage()
274
+ }
270
275
 
271
- if (this.expander) {
272
- this.descriptionToggle()
273
- }
274
- //Add custom player
275
- if (this.useCustomPlayer) {
276
- this.createCustomPlayer()
277
- //set default value for mute
278
- this.muted =
279
- !this.muteIcon ||
280
- // TODO: fix this type properly
281
- (this.opts.autoplay as boolean)
276
+ if (this.expander) {
277
+ this.descriptionToggle()
278
+ }
279
+ //Add custom player
280
+ if (this.useCustomPlayer) {
281
+ this.createCustomPlayer()
282
+ //set default value for mute
283
+ this.muted =
284
+ !this.muteIcon ||
285
+ // TODO: fix this type properly
286
+ (this.opts.autoplay as boolean)
287
+ }
288
+ } catch (error) {
289
+ const customEvent = new CustomEvent(
290
+ 'cpContentPipeline.clipComponent.initFailure',
291
+ {
292
+ detail: { clipInstance: this, error },
293
+ bubbles: true,
294
+ }
295
+ )
296
+ this.containerEl.dispatchEvent(customEvent)
282
297
  }
283
298
  }
284
299
 
@@ -332,6 +347,7 @@ class Clip extends ClipInterface {
332
347
  //contentId: this.opts.id,
333
348
  action: eventType,
334
349
  progress: this.getRelevantProgress(),
350
+ url: window.location.href,
335
351
  video: {
336
352
  duration: this.getDuration(),
337
353
  source_url: this.opts.id,
@@ -26,13 +26,13 @@ class ProgressBar {
26
26
  private onWindowMouseMoveListener: (e: MouseEvent) => void
27
27
  private onWindowMouseUpListener: (e: MouseEvent) => void
28
28
  private opts: Required<Opts>
29
- private isMobileDevice = false
29
+
30
30
  constructor(videoEl: HTMLVideoElement, opts: Opts) {
31
31
  this.videoEl = videoEl
32
32
  this.opts = { ...defaultOpts, ...opts }
33
33
  this.onWindowMouseMoveListener = this.windowMouseMoveListener.bind(this)
34
34
  this.onWindowMouseUpListener = this.windowMouseUpListener.bind(this)
35
- this.isMobileDevice = isMobile()
35
+
36
36
  const container = document.createElement('div')
37
37
  container.classList.add('cp-clip__video-progress-bar')
38
38
  const mainBar = document.createElement('div')
@@ -88,17 +88,21 @@ class ProgressBar {
88
88
 
89
89
  //When mousedown for progressBarTimeTriggerTap time we dispatch the tap event
90
90
  container.addEventListener('mousedown', () => {
91
- this.mousePressed = true
92
- window.addEventListener('mouseup', this.onWindowMouseUpListener)
93
- window.addEventListener('mousemove', this.onWindowMouseMoveListener)
91
+ if (!isMobile()) {
92
+ this.mousePressed = true
93
+ window.addEventListener('mouseup', this.onWindowMouseUpListener)
94
+ window.addEventListener('mousemove', this.onWindowMouseMoveListener)
95
+ }
94
96
  })
95
97
 
96
98
  container.addEventListener('mouseover', () => {
97
- mainBar.classList.add('cp-clip__progress-enlarged')
99
+ if (!isMobile()) {
100
+ mainBar.classList.add('cp-clip__progress-enlarged')
101
+ }
98
102
  })
99
103
 
100
104
  container.addEventListener('mouseleave', () => {
101
- if (!this.mousePressed) {
105
+ if (!this.mousePressed && !isMobile()) {
102
106
  mainBar.classList.remove('cp-clip__progress-enlarged')
103
107
  }
104
108
  })
@@ -235,7 +239,7 @@ class ProgressBar {
235
239
  }
236
240
 
237
241
  windowMouseUpListener(e: MouseEvent) {
238
- if (this.mousePressed && !this.isMobileDevice) {
242
+ if (this.mousePressed) {
239
243
  this.scrub(e.clientX)
240
244
  this.mousePressed = false
241
245
  if (!e.target || !this?.container?.contains(e.target as Node)) {
@@ -72,7 +72,7 @@ export default function ClipComponent({
72
72
  <ClipTag
73
73
  id={id}
74
74
  clip={clip}
75
- poster={posterAttribute}
75
+ poster={poster}
76
76
  accessibility={accessibility}
77
77
  autoplay={autoplay}
78
78
  loop={loop}
@@ -1,6 +1,8 @@
1
1
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2
2
  // @ts-nocheck
3
3
  import React from 'react'
4
+ // TODO: React test renderer is depracated, we should use @testing-library/react
5
+ // https://react.dev/warnings/react-test-renderer
4
6
  import renderer from 'react-test-renderer'
5
7
  import prettier from 'prettier'
6
8
  import { TextEncoder } from 'util'
@@ -0,0 +1,17 @@
1
+ function hideIFrame(): void {
2
+ const flourishComponent = document.querySelectorAll(
3
+ '[data-component="flourish"]'
4
+ )
5
+ flourishComponent.forEach((component) => {
6
+ component.classList.add('disable-iframe')
7
+ })
8
+ }
9
+
10
+ function init(featureFlag: boolean = true): void {
11
+ // turning the feature flag off will hide the iframe and display the fallback image
12
+ if (!featureFlag) {
13
+ hideIFrame()
14
+ }
15
+ }
16
+
17
+ export default init
@@ -0,0 +1,38 @@
1
+ .flourish {
2
+ &.flourish--iFrame {
3
+ @include oGridRespondTo(XL) {
4
+ max-width: oGridColspan(12);
5
+ }
6
+ margin-bottom: 0;
7
+ margin-top: oSpacingByName("s4")
8
+ }
9
+ // We need to force the iframe to be hidden if we turn the feature flag off
10
+ &.disable-iframe {
11
+ .flourish__i-frame {
12
+ display: none;
13
+ }
14
+
15
+ .flourish__figure {
16
+ display: block;
17
+ }
18
+ }
19
+ }
20
+ // Without JS the i-frame will not work, so we hide it
21
+ .no-js {
22
+ .flourish__i-frame {
23
+ display: none;
24
+ }
25
+ }
26
+ .js {
27
+ .flourish__i-frame {
28
+ display: block;
29
+ border: 0;
30
+ @media screen and (max-width: 739px) {
31
+ // We need to enforce this otherwise it will use the inline styling
32
+ aspect-ratio: 3/4 !important;
33
+ }
34
+ }
35
+ .flourish__figure {
36
+ display: none;
37
+ }
38
+ }
@@ -1,12 +1,25 @@
1
1
  import React from 'react'
2
2
  import classnames from 'classnames'
3
- import { ContentTree } from '@financial-times/content-tree'
4
- import { FlourishFragment } from '@financial-times/cp-content-pipeline-client'
5
3
 
6
4
  type disclaimerProps = {
7
5
  id: string
8
6
  }
9
7
 
8
+ type FlourishProps = {
9
+ id: string
10
+ type?: string
11
+ flourishType?: string
12
+ description?: string
13
+ layoutWidth?: string
14
+ fallbackImage: {
15
+ url?: string | null | undefined
16
+ width?: number | null | undefined
17
+ height?: number | null | undefined
18
+ }
19
+ iFrame?: boolean
20
+ inArticleBody?: boolean
21
+ }
22
+
10
23
  const DisclaimerNotice = ({ id }: disclaimerProps) => (
11
24
  <div
12
25
  id={id}
@@ -30,20 +43,50 @@ export default function Flourish({
30
43
  description,
31
44
  layoutWidth,
32
45
  fallbackImage,
33
- }: ContentTree.Flourish & FlourishFragment) {
46
+ iFrame = false,
47
+ inArticleBody = true,
48
+ }: FlourishProps) {
34
49
  const anchorHref = `#${id}`
35
50
  const fullGrid = layoutWidth === 'full-grid' || layoutWidth === 'grid'
36
51
  const figureClassnames = classnames({
37
52
  'n-content-picture': true,
38
53
  'n-content-layout__container': true,
39
54
  'n-content-picture--wide': fullGrid,
55
+ flourish__figure: iFrame,
40
56
  })
41
57
 
58
+ const iframeAspectRatio =
59
+ iFrame && fallbackImage?.width && fallbackImage?.height
60
+ ? `${fallbackImage.width}/${fallbackImage.height}`
61
+ : '16/9'
62
+
63
+ const imageAspectRatio =
64
+ !inArticleBody && fallbackImage?.width && fallbackImage?.height
65
+ ? {
66
+ width: '100%',
67
+ aspectRatio: `${fallbackImage.width}/${fallbackImage.height}`,
68
+ }
69
+ : {}
70
+
71
+ if (!id) return null
72
+
42
73
  return (
43
74
  <div
44
- className="n-content-layout"
75
+ className={classnames({
76
+ 'n-content-layout': inArticleBody,
77
+ flourish: iFrame,
78
+ 'flourish--iFrame': iFrame,
79
+ })}
45
80
  data-layout-width={fullGrid ? 'full-grid' : null}
81
+ data-component="flourish"
46
82
  >
83
+ {iFrame && (
84
+ <iframe
85
+ src={`https://flo.uri.sh/visualisation/${id}/embed?hideTitle=${!inArticleBody}`} // hide the title of if in topper
86
+ style={{ width: '100%', aspectRatio: iframeAspectRatio }}
87
+ className="flourish__i-frame"
88
+ ></iframe>
89
+ )}
47
90
  <figure
48
91
  className={figureClassnames}
49
92
  data-original-image-width={fullGrid ? fallbackImage?.width : null}
@@ -51,12 +94,18 @@ export default function Flourish({
51
94
  >
52
95
  <a href={anchorHref}>
53
96
  <picture
54
- data-asset-type="flourish"
97
+ // `flourish-embed` loads an iframe containing the JS version of a flourish chart
98
+ // `flourish` will be instead targeted by the flourish JS, that will replace the picture with the flourish JS version contained in an iframe
99
+ data-asset-type={iFrame ? 'flourish-embed' : 'flourish'}
55
100
  data-flourish-id={id}
56
101
  data-flourish-type={flourishType}
57
102
  >
58
103
  <DisclaimerNotice id={id} />
59
- <img src={fallbackImage?.url} alt={description} />
104
+ <img
105
+ src={fallbackImage?.url || ''}
106
+ alt={description}
107
+ style={imageAspectRatio}
108
+ />
60
109
  </picture>
61
110
  </a>
62
111
  </figure>