@financial-times/cp-content-pipeline-ui 6.9.1 → 6.10.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 (93) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/lib/components/BackToTopButton/client/index.d.ts +1 -0
  3. package/lib/components/BackToTopButton/client/index.js +7 -1
  4. package/lib/components/BackToTopButton/client/index.js.map +1 -1
  5. package/lib/components/Body/index.test.js +9 -0
  6. package/lib/components/Body/index.test.js.map +1 -1
  7. package/lib/components/Clip/client/index.d.ts +2 -0
  8. package/lib/components/Clip/client/index.js +40 -27
  9. package/lib/components/Clip/client/index.js.map +1 -1
  10. package/lib/components/Clip/client/progressBar.js +4 -2
  11. package/lib/components/Clip/client/progressBar.js.map +1 -1
  12. package/lib/components/Clip/components/ClipTag.js +1 -1
  13. package/lib/components/Clip/components/ClipTag.js.map +1 -1
  14. package/lib/components/Clip/components/Container.js +2 -2
  15. package/lib/components/Clip/components/Container.js.map +1 -1
  16. package/lib/components/Clip/template/index.js +0 -1
  17. package/lib/components/Clip/template/index.js.map +1 -1
  18. package/lib/components/Clip/test/index.spec.js +21 -9
  19. package/lib/components/Clip/test/index.spec.js.map +1 -1
  20. package/lib/components/Expander/client/index.d.ts +49 -0
  21. package/lib/components/Expander/client/index.js +124 -0
  22. package/lib/components/Expander/client/index.js.map +1 -0
  23. package/lib/components/Expander/index.d.ts +15 -0
  24. package/lib/components/Expander/index.js +27 -0
  25. package/lib/components/Expander/index.js.map +1 -0
  26. package/lib/components/Expander/test/client/index.spec.d.ts +1 -0
  27. package/lib/components/Expander/test/client/index.spec.js +103 -0
  28. package/lib/components/Expander/test/client/index.spec.js.map +1 -0
  29. package/lib/components/Expander/test/index.spec.d.ts +1 -0
  30. package/lib/components/Expander/test/index.spec.js +57 -0
  31. package/lib/components/Expander/test/index.spec.js.map +1 -0
  32. package/lib/components/Expander/test/snapshot.spec.d.ts +1 -0
  33. package/lib/components/Expander/test/snapshot.spec.js +63 -0
  34. package/lib/components/Expander/test/snapshot.spec.js.map +1 -0
  35. package/lib/components/ImageSet/index.js +1 -1
  36. package/lib/components/ImageSet/index.js.map +1 -1
  37. package/lib/components/LiveBlogPost/client/index.d.ts +4 -0
  38. package/lib/components/LiveBlogPost/client/index.js +19 -0
  39. package/lib/components/LiveBlogPost/client/index.js.map +1 -0
  40. package/lib/components/LiveBlogPost/index.js +9 -21
  41. package/lib/components/LiveBlogPost/index.js.map +1 -1
  42. package/lib/components/LiveBlogWrapper/index.js +1 -1
  43. package/lib/components/LiveBlogWrapper/index.js.map +1 -1
  44. package/lib/components/Recommended/index.js +1 -1
  45. package/lib/components/Recommended/index.js.map +1 -1
  46. package/lib/components/RichText/index.d.ts +1 -1
  47. package/lib/components/Table/index.js +1 -1
  48. package/lib/components/Table/index.js.map +1 -1
  49. package/lib/components/Video/index.js +1 -1
  50. package/lib/components/Video/index.js.map +1 -1
  51. package/lib/components/YoutubeVideo/index.js +1 -1
  52. package/lib/components/YoutubeVideo/index.js.map +1 -1
  53. package/lib/extensions/scrollIntoView.d.ts +10 -0
  54. package/lib/extensions/scrollIntoView.js +32 -0
  55. package/lib/extensions/scrollIntoView.js.map +1 -0
  56. package/lib/stories/Clip.stories.d.ts +2 -1
  57. package/lib/stories/Clip.stories.js +5 -5
  58. package/lib/stories/Clip.stories.js.map +1 -1
  59. package/lib/stories/Expander.stories.d.ts +54 -0
  60. package/lib/stories/Expander.stories.js +142 -0
  61. package/lib/stories/Expander.stories.js.map +1 -0
  62. package/package.json +2 -5
  63. package/src/components/BackToTopButton/client/index.tsx +8 -1
  64. package/src/components/Body/__snapshots__/index.test.tsx.snap +55 -5
  65. package/src/components/Body/index.test.tsx +9 -0
  66. package/src/components/Clip/client/index.ts +68 -26
  67. package/src/components/Clip/client/main.scss +27 -12
  68. package/src/components/Clip/client/progressBar.ts +5 -2
  69. package/src/components/Clip/components/ClipTag.tsx +0 -1
  70. package/src/components/Clip/components/Container.tsx +10 -3
  71. package/src/components/Clip/template/index.ts +0 -1
  72. package/src/components/Clip/test/__snapshots__/snapshot.spec.tsx.snap +8 -16
  73. package/src/components/Clip/test/index.spec.ts +33 -7
  74. package/src/components/Expander/client/index.ts +201 -0
  75. package/src/components/Expander/client/main.scss +162 -0
  76. package/src/components/Expander/index.tsx +74 -0
  77. package/src/components/Expander/test/__snapshots__/snapshot.spec.tsx.snap +221 -0
  78. package/src/components/Expander/test/client/index.spec.tsx +129 -0
  79. package/src/components/Expander/test/index.spec.tsx +77 -0
  80. package/src/components/Expander/test/snapshot.spec.tsx +73 -0
  81. package/src/components/ImageSet/index.tsx +1 -0
  82. package/src/components/LiveBlogPost/client/index.ts +16 -0
  83. package/src/components/LiveBlogPost/index.tsx +29 -43
  84. package/src/components/LiveBlogWrapper/index.tsx +1 -0
  85. package/src/components/Recommended/index.tsx +1 -0
  86. package/src/components/Table/index.tsx +1 -0
  87. package/src/components/Video/index.tsx +4 -1
  88. package/src/components/YoutubeVideo/index.tsx +4 -1
  89. package/src/extensions/scrollIntoView.ts +38 -0
  90. package/src/stories/Clip.stories.tsx +3 -2
  91. package/src/stories/Expander.stories.scss +3 -0
  92. package/src/stories/Expander.stories.tsx +159 -0
  93. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.OnlyMobileServer = exports.OnlyMobileWithClient = exports.WithClient = exports.OnlyServer = void 0;
30
+ const Expander_1 = require("../components/Expander");
31
+ const client_1 = require("../components/Expander/client");
32
+ const react_1 = __importStar(require("react"));
33
+ const main_1 = __importDefault(require("@financial-times/o-tracking/main"));
34
+ require("./Expander.stories.scss");
35
+ const client_2 = __importDefault(require("../components/Clip/client"));
36
+ require("./Clip.stories.scss");
37
+ const RichText_1 = __importDefault(require("../components/RichText"));
38
+ const structuredContent = {
39
+ type: 'body',
40
+ version: 1,
41
+ children: [
42
+ {
43
+ type: 'paragraph',
44
+ children: [
45
+ {
46
+ type: 'text',
47
+ value: 'Mike Johnson has survived an attempt to oust him as speaker of the House of Representatives by Marjorie Taylor Greene, a hardline Republican congresswoman from Georgia. ',
48
+ },
49
+ ],
50
+ },
51
+ {
52
+ type: 'paragraph',
53
+ children: [
54
+ {
55
+ type: 'text',
56
+ value: 'In a vote on Wednesday night, the lower chamber of Congress rejected Greene’s bid with overwhelming support from Democrats as well as Republicans. ',
57
+ },
58
+ ],
59
+ },
60
+ ,
61
+ {
62
+ type: 'paragraph',
63
+ children: [
64
+ {
65
+ type: 'text',
66
+ value: 'The vote all but ensures that Johnson will remain in his post until after the November election, and will put a lid on a debate over his political future that has lasted for weeks among Republicans in Congress.  ',
67
+ },
68
+ ],
69
+ },
70
+ {
71
+ type: 'clip-set',
72
+ id: 'http://api.ft.com/content/23a00095-d9fc-4cac-ad7c-477e61e3c159',
73
+ autoplay: false,
74
+ loop: false,
75
+ muted: false,
76
+ dataLayout: 'in-line',
77
+ data: {
78
+ referenceIndex: 3,
79
+ },
80
+ },
81
+ ],
82
+ };
83
+ const storyArgs = {
84
+ expandLabel: 'Open',
85
+ collapseLabel: 'Close',
86
+ onlyMobile: false,
87
+ id: '111232323-12123-2323',
88
+ };
89
+ exports.default = {
90
+ title: 'Expander',
91
+ component: Expander_1.ExpanderServer,
92
+ tags: ['autodocs'],
93
+ };
94
+ const LiveBLogWrapper = (args) => {
95
+ const { classNames } = args;
96
+ return (react_1.default.createElement("div", { className: classNames },
97
+ react_1.default.createElement("article", { id: "site-content", role: "main", className: "article article-grid article-grid--no-full-width-graphics js enhanced", "data-article-type": "no-full-width-graphics" },
98
+ react_1.default.createElement("div", { className: "article__content", "data-trackable": "article-body", "data-component": "article-body", style: { position: 'relative' } },
99
+ react_1.default.createElement("div", { className: "article__content-body n-content-body js-article__content-body", "data-attribute": "article-content-body" },
100
+ react_1.default.createElement(LiveBlogPostWrapper, { ...args, id: "111" }))),
101
+ react_1.default.createElement("div", { className: "article__right " }))));
102
+ };
103
+ const LiveBlogPostWrapper = (args) => {
104
+ (0, react_1.useEffect)(() => {
105
+ main_1.default.init({
106
+ context: { product: 'next' },
107
+ test: true,
108
+ });
109
+ const clips = client_2.default.init(null, {});
110
+ return () => {
111
+ clips.forEach((clip) => clip.unload());
112
+ };
113
+ }, []);
114
+ return (react_1.default.createElement("article", { id: args.id },
115
+ react_1.default.createElement("div", { className: "n-content-body" },
116
+ react_1.default.createElement("div", { "data-trackable": "truncated-post", "data-trackable-context-post": "1234", "data-component": "example-boundary" },
117
+ react_1.default.createElement(Expander_1.ExpanderServer, { ...args, id: `${args.id}_expander` },
118
+ react_1.default.createElement(RichText_1.default, { structuredContent: { tree: structuredContent } }))))));
119
+ };
120
+ const OnlyServer = (args) => {
121
+ return react_1.default.createElement(LiveBLogWrapper, { ...args });
122
+ };
123
+ exports.OnlyServer = OnlyServer;
124
+ exports.OnlyServer.args = { ...storyArgs };
125
+ const WithClient = (args) => {
126
+ (0, react_1.useEffect)(() => {
127
+ const clips = client_2.default.init(null, {});
128
+ const expanders = client_1.ExpanderClient.init();
129
+ return () => {
130
+ clips.forEach((clip) => clip.unload());
131
+ expanders.forEach((expander) => expander?.destroy());
132
+ };
133
+ }, []);
134
+ return react_1.default.createElement(LiveBLogWrapper, { ...args });
135
+ };
136
+ exports.WithClient = WithClient;
137
+ exports.WithClient.args = { ...storyArgs };
138
+ exports.OnlyMobileWithClient = exports.WithClient;
139
+ exports.OnlyMobileWithClient.args = { ...storyArgs, onlyMobile: true };
140
+ exports.OnlyMobileServer = exports.OnlyServer;
141
+ exports.OnlyMobileServer.args = { ...storyArgs, onlyMobile: true };
142
+ //# sourceMappingURL=Expander.stories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Expander.stories.js","sourceRoot":"","sources":["../../src/stories/Expander.stories.tsx"],"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.9.1",
3
+ "version": "6.10.0-beta.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -17,9 +17,6 @@
17
17
  "@babel/preset-react": "^7.22.5",
18
18
  "@dotcom-tool-kit/npm": "^3.1.4",
19
19
  "@financial-times/content-tree": "github:financial-times/content-tree#14370e3",
20
- "@financial-times/cp-content-pipeline-client": "^3.8.0",
21
- "@financial-times/cp-content-pipeline-schema": "^2.11.0",
22
- "@financial-times/cp-content-pipeline-styles": "^2.5.2",
23
20
  "@financial-times/n-scrollytelling-image": "^1.1.0",
24
21
  "@financial-times/o-colors": "^6.6.2",
25
22
  "@financial-times/o-grid": "^6.1.5",
@@ -61,7 +58,7 @@
61
58
  "@financial-times/o-loading": "^5.2.3",
62
59
  "@financial-times/o-teaser": "^6.2.2",
63
60
  "@financial-times/x-teaser": "^14.6.5",
64
- "classnames": "^2.3.1"
61
+ "classnames": "^2.5.1"
65
62
  },
66
63
  "peerDependencies": {
67
64
  "@financial-times/x-interaction": ">=14",
@@ -1,3 +1,5 @@
1
+ //To know when scroll event was triggered by scrollIntoView, it needs to polyfill scrollIntoView method
2
+ import '../../../extensions/scrollIntoView'
1
3
  import React, { useState, useEffect, useRef } from 'react'
2
4
  import { throttle } from './utils'
3
5
 
@@ -51,7 +53,12 @@ export default function BackToTop({
51
53
  const hasPassedTriggerPosition = startElement
52
54
  ? startElement?.getBoundingClientRect()?.bottom <= 0
53
55
  : (element?.current?.getBoundingClientRect()?.top ?? 0) <= 0 // Add a fallback value of 0 if element is undefined
54
- if (hasPassedTriggerPosition) {
56
+ // if the user has scrolled past the trigger element and the scroll is not programatic
57
+ const window =
58
+ ((scrollableElement as HTMLElement)?.ownerDocument
59
+ ?.defaultView as Window & { programmaticScroll?: boolean }) ||
60
+ (scrollableElement as Window & { programmaticScroll?: boolean })
61
+ if (hasPassedTriggerPosition && !window?.programmaticScroll) {
55
62
  result = true
56
63
  }
57
64
  }
@@ -145,6 +145,7 @@ exports[`Body default renderers renders default renderer for LiveBlogPackage 1`]
145
145
  <DocumentFragment>
146
146
  <div
147
147
  class="x-live-blog-wrapper"
148
+ data-component="live-blog-wrapper"
148
149
  data-live-blog-wrapper-id="live-blog-wrapper"
149
150
  >
150
151
  <article
@@ -172,7 +173,7 @@ exports[`Body default renderers renders default renderer for LiveBlogPackage 1`]
172
173
  <time
173
174
  class="o-date x-live-blog-post__timestamp"
174
175
  data-o-component="o-date"
175
- data-o-date-format="time-ago-no-seconds"
176
+ data-o-date-format="MMM dd, HH:mm"
176
177
  data-o-date-text-case="sentence"
177
178
  datetime="2024-06-11T14:13:25.527Z"
178
179
  itemprop="datePublished"
@@ -180,7 +181,7 @@ exports[`Body default renderers renders default renderer for LiveBlogPackage 1`]
180
181
  <span
181
182
  data-o-date-printer="true"
182
183
  >
183
- 6/11/2024, 2:13:25 PM
184
+ 11/06/2024, 14:13:25
184
185
  </span>
185
186
  </time>
186
187
  </span>
@@ -202,10 +203,59 @@ exports[`Body default renderers renders default renderer for LiveBlogPackage 1`]
202
203
  </h2>
203
204
  <div
204
205
  class="x-live-blog-post__body n-content-body article--body"
206
+ data-trackable="truncated-post"
207
+ data-trackable-category="live-blog"
208
+ data-trackable-context-post="00000000-0000-0000-0000-000000000000"
205
209
  >
206
- <p>
207
- Lorem ipsum dolor sit amet
208
- </p>
210
+ <div
211
+ class="cp-expander cp-expander--mobile-only cp-expander--not-initialised"
212
+ data-component="expander"
213
+ data-state="collapsed"
214
+ id="truncated-00000000-0000-0000-0000-000000000000__container"
215
+ >
216
+ <div
217
+ class="cp-expander-content"
218
+ id="truncated-00000000-0000-0000-0000-000000000000"
219
+ >
220
+ <div
221
+ class="cp-expander__expand"
222
+ >
223
+ <a
224
+ aria-controls="truncated-00000000-0000-0000-0000-000000000000"
225
+ aria-expanded="false"
226
+ aria-hidden="false"
227
+ class="cp-expander__link"
228
+ data-action="expand"
229
+ data-trackable="truncated-post"
230
+ data-trackable-context-truncated-id="truncated-00000000-0000-0000-0000-000000000000"
231
+ data-trackable-context-truncated-post="expand"
232
+ href="#truncated-00000000-0000-0000-0000-000000000000"
233
+ >
234
+ Expand post
235
+ </a>
236
+ </div>
237
+ <p>
238
+ Lorem ipsum dolor sit amet
239
+ </p>
240
+ <div
241
+ class="cp-expander__collapse"
242
+ >
243
+ <a
244
+ aria-controls="truncated-00000000-0000-0000-0000-000000000000"
245
+ aria-expanded="false"
246
+ aria-hidden="true"
247
+ class="cp-expander__link"
248
+ data-action="collapse"
249
+ data-trackable="truncated-post"
250
+ data-trackable-context-truncated-id="truncated-00000000-0000-0000-0000-000000000000"
251
+ data-trackable-context-truncated-post="collapse"
252
+ href="#truncated-00000000-0000-0000-0000-000000000000__container"
253
+ >
254
+ Collapse post
255
+ </a>
256
+ </div>
257
+ </div>
258
+ </div>
209
259
  </div>
210
260
  <div
211
261
  class="x-live-blog-post__controls"
@@ -103,6 +103,15 @@ const legacyOverridesToTest: [
103
103
  ]
104
104
 
105
105
  describe('Body', () => {
106
+ let mockDate: jest.SpyInstance
107
+ beforeAll(() => {
108
+ mockDate = jest
109
+ .spyOn(Date.prototype, 'toLocaleString')
110
+ .mockReturnValue('11/06/2024, 14:13:25')
111
+ })
112
+ afterAll(() => {
113
+ mockDate.mockRestore()
114
+ })
106
115
  describe('default renderers', () => {
107
116
  test.each(typesToTest)('renders default renderer for %s', (type) => {
108
117
  const { asFragment } = render(
@@ -134,6 +134,7 @@ class Clip extends ClipInterface {
134
134
  private lastTimeUpdate: number | null = null
135
135
  private muteIcon: HTMLElement | null = null
136
136
  private controls: HTMLElement | null = null
137
+ private playPauseButton: HTMLElement | null = null
137
138
  private bottomBar: HTMLElement | null = null
138
139
  private playText: HTMLElement | null = null
139
140
  private progressBar: ProgressBar | null = null
@@ -516,27 +517,42 @@ class Clip extends ClipInterface {
516
517
  this.muteIcon.addEventListener('click', () => {
517
518
  this.muted = !this.muted
518
519
  })
520
+ // this will ensure that the controls can fade out for mouse-based users
521
+ this.muteIcon.onmouseup = () => {
522
+ this.muteIcon?.blur()
523
+ }
524
+
525
+ const playPauseButton = document.createElement('button')
526
+ playPauseButton.setAttribute('type', 'button')
527
+ this.playPauseButton = playPauseButton
528
+ this.togglePlayPauseButton()
519
529
 
520
- const pauseIcon = document.createElement('button')
521
530
  if (this.opts.loop) {
531
+ playPauseButton.classList.add('cp-clip__playpause-icon-autoplay')
522
532
  customControls.classList.add('cp-clip__video-mode-autoplay')
523
- pauseIcon.classList.add('cp-clip__pause-icon-autoplay')
524
- pauseIcon.setAttribute('tabindex', '-1')
525
- pauseIcon.setAttribute('data-trackable', 'video-clip-pause')
526
- pauseIcon.setAttribute('aria-label', 'Pause')
527
533
  const progressRing = document.createElement('div')
528
- progressRing.classList.add('cp-clip__pause-icon-autoplay-progress')
529
- pauseIcon.appendChild(progressRing)
534
+ progressRing.classList.add('cp-clip__playpause-icon-autoplay-progress')
535
+ playPauseButton.appendChild(progressRing)
530
536
  this.progressRing = progressRing
531
537
  } else {
532
- pauseIcon.classList.add('cp-clip__pause-icon')
533
- pauseIcon.setAttribute('aria-label', 'Pause')
534
- pauseIcon.setAttribute('data-trackable', 'video-clip-pause')
535
- pauseIcon.setAttribute('tabindex', '-1')
538
+ playPauseButton.classList.add('cp-clip__playpause-icon')
536
539
  this.progressBar = this.createProgressBar()
540
+ // if we have a progress bar then we have the ability to step back and forward, so permit focussing on the video
541
+ video.setAttribute('tabIndex', '0')
542
+ video.setAttribute(
543
+ 'aria-label',
544
+ 'Video clip: use left and right arrow keys to skip backward or forwards'
545
+ )
537
546
  }
538
547
 
539
- pauseIcon.onclick = () => video.pause()
548
+ playPauseButton.onclick = () => {
549
+ this.togglePlay()
550
+ }
551
+
552
+ // this will ensure that the controls can fade out for mouse-based users
553
+ playPauseButton.onmouseup = () => {
554
+ playPauseButton.blur()
555
+ }
540
556
 
541
557
  const noAudioIcon = this.createNoAudioIcon()
542
558
 
@@ -560,7 +576,7 @@ class Clip extends ClipInterface {
560
576
  bottomBar.append(closedCaptionIcon)
561
577
  }
562
578
 
563
- bottomBar.append(pauseIcon)
579
+ bottomBar.append(playPauseButton)
564
580
  bottomBar.append(this.videoHasNoAudio() ? noAudioIcon : muteIcon)
565
581
  customControls.prepend(playIconContainer)
566
582
  customControls.append(bottomBar)
@@ -575,15 +591,24 @@ class Clip extends ClipInterface {
575
591
  video.addEventListener('playing', () => {
576
592
  //resize controls in case poster is different size than video at the end
577
593
  this.resizeControls()
578
-
594
+ this.togglePlayPauseButton()
595
+ this.containerEl.classList.remove(
596
+ 'cp-clip--paused',
597
+ 'cp-clip--loading',
598
+ 'cp-clip--ready'
599
+ )
579
600
  this.containerEl.classList.add('cp-clip--playing')
580
- this.containerEl.classList.remove('cp-clip--loading')
581
601
  this.fadeOut()
582
602
  })
583
603
 
584
604
  video.addEventListener('pause', () => {
585
- this.containerEl.classList.remove('cp-clip--playing')
586
- this.containerEl.classList.remove('cp-clip--loading')
605
+ this.togglePlayPauseButton()
606
+ this.containerEl.classList.remove(
607
+ 'cp-clip--playing',
608
+ 'cp-clip--loading',
609
+ 'cp-clip--ready'
610
+ )
611
+ this.containerEl.classList.add('cp-clip--paused')
587
612
  playIcon.setAttribute('data-trackable', 'video-clip-resume')
588
613
 
589
614
  this.fadeIn()
@@ -630,6 +655,7 @@ class Clip extends ClipInterface {
630
655
  this.playText.innerText = `PLAY | ${duration}`
631
656
  this.playText.classList.add('cp-clip__play-text')
632
657
  playIconContainer.append(this.playText)
658
+ this.containerEl.classList.add('cp-clip--ready')
633
659
  }
634
660
 
635
661
  if (video.readyState >= 1) {
@@ -1051,14 +1077,7 @@ class Clip extends ClipInterface {
1051
1077
  const hasFocus = this.isFocused()
1052
1078
  const isPlaying = this.isVideoPlaying()
1053
1079
  if (!hasFocus) return
1054
- if (event.keyCode === 32) {
1055
- // The default action from pressing the spacebar is to scroll down the page
1056
- // We prevent the page from scrolling down if the video is in view and someone presses the space bar
1057
- event.preventDefault()
1058
- event.stopImmediatePropagation()
1059
- const currentElement: HTMLElement = document.activeElement as HTMLElement
1060
- if (currentElement === this.videoEl) this.togglePlay()
1061
- } else if (isPlaying && event.key === 'ArrowLeft') {
1080
+ if (isPlaying && event.key === 'ArrowLeft') {
1062
1081
  this.progressBar?.stepBackward()
1063
1082
  event.preventDefault()
1064
1083
  event.stopImmediatePropagation()
@@ -1087,7 +1106,30 @@ class Clip extends ClipInterface {
1087
1106
  this.videoEl.play()
1088
1107
  }
1089
1108
  this.videoEl.classList.add('no-focus-style')
1090
- this.videoEl.focus()
1109
+ }
1110
+
1111
+ togglePlayPauseButton() {
1112
+ if (!this.playPauseButton) return
1113
+
1114
+ const isPlaying = this.isVideoPlaying()
1115
+
1116
+ this.playPauseButton.classList.toggle(
1117
+ 'cp-clip__playpause-icon-pause',
1118
+ isPlaying
1119
+ )
1120
+ this.playPauseButton.classList.toggle(
1121
+ 'cp-clip__playpause-icon-play',
1122
+ !isPlaying
1123
+ )
1124
+
1125
+ this.playPauseButton.setAttribute(
1126
+ 'aria-label',
1127
+ isPlaying ? 'Pause' : 'Play'
1128
+ )
1129
+ this.playPauseButton.setAttribute(
1130
+ 'data-trackable',
1131
+ isPlaying ? 'video-clip-pause' : 'video-clip-play'
1132
+ )
1091
1133
  }
1092
1134
 
1093
1135
  createProgressBar(): ProgressBar {
@@ -50,7 +50,7 @@
50
50
  }
51
51
 
52
52
  .cp-clip__video-controls-bottom-bar {
53
- display: none;
53
+ display: flex;
54
54
  width: 100%;
55
55
  height: 35%;
56
56
  bottom: 0px;
@@ -64,7 +64,7 @@
64
64
  }
65
65
  background: linear-gradient(0deg, rgba(0, 0, 0, 0.3) 38.38%, rgba(153, 153, 153, 0) 100%);
66
66
 
67
- &--fade-out {
67
+ &--fade-out:not(:has(button:focus)) {
68
68
  @include fade-out-animation();
69
69
  }
70
70
  }
@@ -121,9 +121,21 @@
121
121
  background-position: 50%;
122
122
  }
123
123
 
124
- &.cp-clip--playing .cp-clip__play-icon-container {
124
+ &.cp-clip--ready .cp-clip__video-controls-bottom-bar:not(:has(button:focus)),
125
+ &.cp-clip--paused .cp-clip__video-controls-bottom-bar:not(:has(button:focus)) {
126
+ @include fade-out-animation();
127
+ }
128
+
129
+ // do not show the large play button when any of these are true:
130
+ // if the video is playing
131
+ // if the video is loading
132
+ // if the bottom bar area has a focussed button
133
+ &.cp-clip--playing .cp-clip__play-icon-container,
134
+ &.cp-clip--loading .cp-clip__play-icon-container,
135
+ .cp-clip__video-controls:has(.cp-clip__video-controls-bottom-bar button:focus) .cp-clip__play-icon-container {
125
136
  display: none;
126
137
  }
138
+
127
139
  &.cp-clip--playing .cp-clip__video-controls-bottom-bar {
128
140
  display: flex;
129
141
  }
@@ -132,27 +144,30 @@
132
144
  display: block;
133
145
  }
134
146
 
135
- &.cp-clip--loading .cp-clip__play-icon-container {
136
- display: none;
147
+ .cp-clip__playpause-icon-pause {
148
+ // The icon will be cached in the FT-App
149
+ // See: https://github.com/Financial-Times/ft-app/blob/main/tools/webpack/client/webpack.config.common.js#L218
150
+ @include oIconsContent($icon-name: 'pause', $size: 32px, $color: oColorsByName('white'));
137
151
  }
138
152
 
139
- .cp-clip__pause-icon {
153
+ .cp-clip__playpause-icon-play {
140
154
  // The icon will be cached in the FT-App
141
155
  // See: https://github.com/Financial-Times/ft-app/blob/main/tools/webpack/client/webpack.config.common.js#L218
142
- @include oIconsContent($icon-name: 'pause', $size: 32px, $color: oColorsByName('white'));
143
- border: none;
156
+ @include oIconsContent($icon-name: 'play', $size: 32px, $color: oColorsByName('white'));
144
157
  }
145
158
 
159
+ .cp-clip__playpause-icon {
160
+ border: none;
161
+ }
146
162
 
147
- .cp-clip__pause-icon-autoplay {
148
- @include oIconsContent($icon-name: 'pause', $size: 32px, $color: oColorsByName('white'));
163
+ .cp-clip__playpause-icon-autoplay {
149
164
  border-radius: 50%;
150
165
  border: 2px solid oColorsByName('ft-grey');
151
166
  padding:0px;
152
167
  margin-left:oSpacingByName('s2');
153
168
  }
154
169
 
155
- .cp-clip__pause-icon-autoplay-progress {
170
+ .cp-clip__playpause-icon-autoplay-progress {
156
171
  width: 100%;
157
172
  height: 100%;
158
173
  border-radius: 50%;
@@ -198,7 +213,7 @@
198
213
  @include oIconsContent($icon-name: 'closed-captions-on', $size: 32px, $color: oColorsByName('white'));
199
214
  }
200
215
 
201
- .cp-clip--fade-out {
216
+ .cp-clip--fade-out:not(:has(button:focus)) {
202
217
  @include fade-out-animation();
203
218
  }
204
219
 
@@ -68,7 +68,7 @@ class ProgressBar {
68
68
  }
69
69
  })
70
70
 
71
- container.addEventListener('touchend', (e: TouchEvent) => {
71
+ const touchEnd = (e: TouchEvent) => {
72
72
  this.touchStarted = false
73
73
  this.timeoutPressed = window.setTimeout(() => {
74
74
  mainBar.classList.remove('cp-clip__progress-enlarged')
@@ -77,7 +77,10 @@ class ProgressBar {
77
77
  this.scrub(e.changedTouches[0].clientX)
78
78
  }
79
79
  e.preventDefault()
80
- })
80
+ }
81
+
82
+ container.addEventListener('touchend', touchEnd)
83
+ container.addEventListener('touchcancel', touchEnd)
81
84
 
82
85
  container.addEventListener('touchmove', (e) => {
83
86
  if (e.changedTouches[0]) {
@@ -88,7 +88,6 @@ export const ClipTag = ({
88
88
  loop={loop}
89
89
  poster={poster}
90
90
  preload={preload}
91
- tabIndex={0}
92
91
  >
93
92
  {clip?.dataSource?.map(({ binaryUrl, mediaType }, index) => {
94
93
  return (
@@ -7,9 +7,11 @@ interface ContainerProps {
7
7
 
8
8
  export const Container = ({ dataLayout, children }: ContainerProps) => {
9
9
  return dataLayout === 'full-grid' ? (
10
- <div className="n-content-layout__container">{children}</div>
10
+ <div className="n-content-layout__container" data-component="clip-set">
11
+ {children}
12
+ </div>
11
13
  ) : dataLayout === 'mid-grid' ? (
12
- <div className="n-content-layout__container">
14
+ <div className="n-content-layout__container" data-component="clip-set">
13
15
  <div
14
16
  data-o-grid-colspan="12 S12 M12 L10 XL10"
15
17
  className="n-content-layout__container--mid-grid"
@@ -18,6 +20,11 @@ export const Container = ({ dataLayout, children }: ContainerProps) => {
18
20
  </div>
19
21
  </div>
20
22
  ) : (
21
- <div className="n-content-layout__container--in-line">{children}</div>
23
+ <div
24
+ className="n-content-layout__container--in-line"
25
+ data-component="clip-set"
26
+ >
27
+ {children}
28
+ </div>
22
29
  )
23
30
  }
@@ -1,5 +1,4 @@
1
1
  // This file will is used by next-es-interface and it renders the component as a string
2
- /* eslint-disable import/no-unused-modules */
3
2
  // TODO: Remove this file once next-es-interface and Elastic Search won't be used anymore to render articles (After r13n API will be used in Production by all the consumers: FT-App, next-article, FT Edit)
4
3
  import React from 'react'
5
4
  import ReactDOMServer from 'react-dom/server'