@financial-times/cp-content-pipeline-ui 6.9.2 → 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.
- package/CHANGELOG.md +17 -0
- package/lib/components/BackToTopButton/client/index.d.ts +1 -0
- package/lib/components/BackToTopButton/client/index.js +7 -1
- package/lib/components/BackToTopButton/client/index.js.map +1 -1
- package/lib/components/Body/index.test.js +9 -0
- package/lib/components/Body/index.test.js.map +1 -1
- package/lib/components/Clip/client/index.d.ts +2 -0
- package/lib/components/Clip/client/index.js +40 -27
- package/lib/components/Clip/client/index.js.map +1 -1
- package/lib/components/Clip/components/ClipTag.js +1 -1
- package/lib/components/Clip/components/ClipTag.js.map +1 -1
- package/lib/components/Clip/components/Container.js +2 -2
- package/lib/components/Clip/components/Container.js.map +1 -1
- package/lib/components/Clip/template/index.js +0 -1
- package/lib/components/Clip/template/index.js.map +1 -1
- package/lib/components/Clip/test/index.spec.js +21 -9
- package/lib/components/Clip/test/index.spec.js.map +1 -1
- package/lib/components/Expander/client/index.d.ts +49 -0
- package/lib/components/Expander/client/index.js +124 -0
- package/lib/components/Expander/client/index.js.map +1 -0
- package/lib/components/Expander/index.d.ts +15 -0
- package/lib/components/Expander/index.js +27 -0
- package/lib/components/Expander/index.js.map +1 -0
- package/lib/components/Expander/test/client/index.spec.d.ts +1 -0
- package/lib/components/Expander/test/client/index.spec.js +103 -0
- package/lib/components/Expander/test/client/index.spec.js.map +1 -0
- package/lib/components/Expander/test/index.spec.d.ts +1 -0
- package/lib/components/Expander/test/index.spec.js +57 -0
- package/lib/components/Expander/test/index.spec.js.map +1 -0
- package/lib/components/Expander/test/snapshot.spec.d.ts +1 -0
- package/lib/components/Expander/test/snapshot.spec.js +63 -0
- package/lib/components/Expander/test/snapshot.spec.js.map +1 -0
- package/lib/components/ImageSet/index.js +1 -1
- package/lib/components/ImageSet/index.js.map +1 -1
- package/lib/components/LiveBlogPost/client/index.d.ts +4 -0
- package/lib/components/LiveBlogPost/client/index.js +19 -0
- package/lib/components/LiveBlogPost/client/index.js.map +1 -0
- package/lib/components/LiveBlogPost/index.js +9 -21
- package/lib/components/LiveBlogPost/index.js.map +1 -1
- package/lib/components/LiveBlogWrapper/index.js +1 -1
- package/lib/components/LiveBlogWrapper/index.js.map +1 -1
- package/lib/components/Recommended/index.js +1 -1
- package/lib/components/Recommended/index.js.map +1 -1
- package/lib/components/RichText/index.d.ts +1 -1
- package/lib/components/Table/index.js +1 -1
- package/lib/components/Table/index.js.map +1 -1
- package/lib/components/Video/index.js +1 -1
- package/lib/components/Video/index.js.map +1 -1
- package/lib/components/YoutubeVideo/index.js +1 -1
- package/lib/components/YoutubeVideo/index.js.map +1 -1
- package/lib/extensions/scrollIntoView.d.ts +10 -0
- package/lib/extensions/scrollIntoView.js +32 -0
- package/lib/extensions/scrollIntoView.js.map +1 -0
- package/lib/stories/Clip.stories.d.ts +2 -1
- package/lib/stories/Clip.stories.js +5 -5
- package/lib/stories/Clip.stories.js.map +1 -1
- package/lib/stories/Expander.stories.d.ts +54 -0
- package/lib/stories/Expander.stories.js +142 -0
- package/lib/stories/Expander.stories.js.map +1 -0
- package/package.json +2 -5
- package/src/components/BackToTopButton/client/index.tsx +8 -1
- package/src/components/Body/__snapshots__/index.test.tsx.snap +55 -5
- package/src/components/Body/index.test.tsx +9 -0
- package/src/components/Clip/client/index.ts +68 -26
- package/src/components/Clip/client/main.scss +27 -12
- package/src/components/Clip/components/ClipTag.tsx +0 -1
- package/src/components/Clip/components/Container.tsx +10 -3
- package/src/components/Clip/template/index.ts +0 -1
- package/src/components/Clip/test/__snapshots__/snapshot.spec.tsx.snap +8 -16
- package/src/components/Clip/test/index.spec.ts +33 -7
- package/src/components/Expander/client/index.ts +201 -0
- package/src/components/Expander/client/main.scss +162 -0
- package/src/components/Expander/index.tsx +74 -0
- package/src/components/Expander/test/__snapshots__/snapshot.spec.tsx.snap +221 -0
- package/src/components/Expander/test/client/index.spec.tsx +129 -0
- package/src/components/Expander/test/index.spec.tsx +77 -0
- package/src/components/Expander/test/snapshot.spec.tsx +73 -0
- package/src/components/ImageSet/index.tsx +1 -0
- package/src/components/LiveBlogPost/client/index.ts +16 -0
- package/src/components/LiveBlogPost/index.tsx +29 -43
- package/src/components/LiveBlogWrapper/index.tsx +1 -0
- package/src/components/Recommended/index.tsx +1 -0
- package/src/components/Table/index.tsx +1 -0
- package/src/components/Video/index.tsx +4 -1
- package/src/components/YoutubeVideo/index.tsx +4 -1
- package/src/extensions/scrollIntoView.ts +38 -0
- package/src/stories/Clip.stories.tsx +3 -2
- package/src/stories/Expander.stories.scss +3 -0
- package/src/stories/Expander.stories.tsx +159 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`Clip Snapshot component rendered on server full-grid default render 1`] = `
|
|
4
4
|
"<div class=\\"n-content-layout\\" data-layout-width=\\"full-grid\\">
|
|
5
|
-
<div class=\\"n-content-layout__container\\">
|
|
5
|
+
<div class=\\"n-content-layout__container\\" data-component=\\"clip-set\\">
|
|
6
6
|
<div
|
|
7
7
|
data-cp-clip-layout=\\"full-grid\\"
|
|
8
8
|
data-cp-clip-poster=\\"\\"
|
|
@@ -41,7 +41,6 @@ exports[`Clip Snapshot component rendered on server full-grid default render 1`]
|
|
|
41
41
|
playsinline=\\"\\"
|
|
42
42
|
poster=\\"\\"
|
|
43
43
|
preload=\\"auto\\"
|
|
44
|
-
tabindex=\\"0\\"
|
|
45
44
|
style=\\"height: fit-content; width: 100%\\"
|
|
46
45
|
>
|
|
47
46
|
<source
|
|
@@ -64,7 +63,7 @@ exports[`Clip Snapshot component rendered on server full-grid default render 1`]
|
|
|
64
63
|
|
|
65
64
|
exports[`Clip Snapshot component rendered on server full-grid render with attributes 1`] = `
|
|
66
65
|
"<div class=\\"n-content-layout\\" data-layout-width=\\"full-grid\\">
|
|
67
|
-
<div class=\\"n-content-layout__container\\">
|
|
66
|
+
<div class=\\"n-content-layout__container\\" data-component=\\"clip-set\\">
|
|
68
67
|
<div
|
|
69
68
|
data-cp-clip-layout=\\"full-grid\\"
|
|
70
69
|
data-cp-clip-poster=\\"\\"
|
|
@@ -104,7 +103,6 @@ exports[`Clip Snapshot component rendered on server full-grid render with attrib
|
|
|
104
103
|
loop=\\"\\"
|
|
105
104
|
poster=\\"\\"
|
|
106
105
|
preload=\\"auto\\"
|
|
107
|
-
tabindex=\\"0\\"
|
|
108
106
|
style=\\"height: fit-content; width: 100%\\"
|
|
109
107
|
>
|
|
110
108
|
<source
|
|
@@ -151,7 +149,7 @@ exports[`Clip Snapshot component rendered on server full-grid render with attrib
|
|
|
151
149
|
`;
|
|
152
150
|
|
|
153
151
|
exports[`Clip Snapshot component rendered on server in-line render 1`] = `
|
|
154
|
-
"<div class=\\"n-content-layout__container--in-line\\">
|
|
152
|
+
"<div class=\\"n-content-layout__container--in-line\\" data-component=\\"clip-set\\">
|
|
155
153
|
<div
|
|
156
154
|
data-cp-clip-layout=\\"in-line\\"
|
|
157
155
|
data-cp-clip-poster=\\"\\"
|
|
@@ -190,7 +188,6 @@ exports[`Clip Snapshot component rendered on server in-line render 1`] = `
|
|
|
190
188
|
playsinline=\\"\\"
|
|
191
189
|
poster=\\"\\"
|
|
192
190
|
preload=\\"auto\\"
|
|
193
|
-
tabindex=\\"0\\"
|
|
194
191
|
style=\\"height: fit-content; width: 100%\\"
|
|
195
192
|
>
|
|
196
193
|
<source
|
|
@@ -211,7 +208,7 @@ exports[`Clip Snapshot component rendered on server in-line render 1`] = `
|
|
|
211
208
|
`;
|
|
212
209
|
|
|
213
210
|
exports[`Clip Snapshot component rendered on server in-line render with attributes 1`] = `
|
|
214
|
-
"<div class=\\"n-content-layout__container--in-line\\">
|
|
211
|
+
"<div class=\\"n-content-layout__container--in-line\\" data-component=\\"clip-set\\">
|
|
215
212
|
<div
|
|
216
213
|
data-cp-clip-layout=\\"in-line\\"
|
|
217
214
|
data-cp-clip-poster=\\"\\"
|
|
@@ -251,7 +248,6 @@ exports[`Clip Snapshot component rendered on server in-line render with attribut
|
|
|
251
248
|
loop=\\"\\"
|
|
252
249
|
poster=\\"\\"
|
|
253
250
|
preload=\\"auto\\"
|
|
254
|
-
tabindex=\\"0\\"
|
|
255
251
|
style=\\"height: fit-content; width: 100%\\"
|
|
256
252
|
>
|
|
257
253
|
<source
|
|
@@ -298,7 +294,7 @@ exports[`Clip Snapshot component rendered on server in-line render with attribut
|
|
|
298
294
|
|
|
299
295
|
exports[`Clip Snapshot component rendered on server mid-grid default render 1`] = `
|
|
300
296
|
"<div class=\\"n-content-layout\\" data-layout-width=\\"full-grid\\">
|
|
301
|
-
<div class=\\"n-content-layout__container\\">
|
|
297
|
+
<div class=\\"n-content-layout__container\\" data-component=\\"clip-set\\">
|
|
302
298
|
<div
|
|
303
299
|
data-o-grid-colspan=\\"12 S12 M12 L10 XL10\\"
|
|
304
300
|
class=\\"n-content-layout__container--mid-grid\\"
|
|
@@ -341,7 +337,6 @@ exports[`Clip Snapshot component rendered on server mid-grid default render 1`]
|
|
|
341
337
|
playsinline=\\"\\"
|
|
342
338
|
poster=\\"\\"
|
|
343
339
|
preload=\\"auto\\"
|
|
344
|
-
tabindex=\\"0\\"
|
|
345
340
|
style=\\"height: fit-content; width: 100%\\"
|
|
346
341
|
>
|
|
347
342
|
<source
|
|
@@ -365,7 +360,7 @@ exports[`Clip Snapshot component rendered on server mid-grid default render 1`]
|
|
|
365
360
|
|
|
366
361
|
exports[`Clip Snapshot component rendered on server mid-grid render with attributes 1`] = `
|
|
367
362
|
"<div class=\\"n-content-layout\\" data-layout-width=\\"full-grid\\">
|
|
368
|
-
<div class=\\"n-content-layout__container\\">
|
|
363
|
+
<div class=\\"n-content-layout__container\\" data-component=\\"clip-set\\">
|
|
369
364
|
<div
|
|
370
365
|
data-o-grid-colspan=\\"12 S12 M12 L10 XL10\\"
|
|
371
366
|
class=\\"n-content-layout__container--mid-grid\\"
|
|
@@ -409,7 +404,6 @@ exports[`Clip Snapshot component rendered on server mid-grid render with attribu
|
|
|
409
404
|
loop=\\"\\"
|
|
410
405
|
poster=\\"\\"
|
|
411
406
|
preload=\\"auto\\"
|
|
412
|
-
tabindex=\\"0\\"
|
|
413
407
|
style=\\"height: fit-content; width: 100%\\"
|
|
414
408
|
>
|
|
415
409
|
<source
|
|
@@ -463,7 +457,7 @@ exports[`Clip Snapshot component rendered on server mid-grid render with attribu
|
|
|
463
457
|
`;
|
|
464
458
|
|
|
465
459
|
exports[`Clip Snapshot component rendered on server renders multiple video sources 1`] = `
|
|
466
|
-
"<div class=\\"n-content-layout__container--in-line\\">
|
|
460
|
+
"<div class=\\"n-content-layout__container--in-line\\" data-component=\\"clip-set\\">
|
|
467
461
|
<div
|
|
468
462
|
data-cp-clip-layout=\\"in-line\\"
|
|
469
463
|
data-cp-clip-poster=\\"https://whatever/1080x1920.jpg\\"
|
|
@@ -502,7 +496,6 @@ exports[`Clip Snapshot component rendered on server renders multiple video sourc
|
|
|
502
496
|
playsinline=\\"\\"
|
|
503
497
|
poster=\\"https://whatever/1080x1920.jpg\\"
|
|
504
498
|
preload=\\"auto\\"
|
|
505
|
-
tabindex=\\"0\\"
|
|
506
499
|
style=\\"aspect-ratio: 1920/1080; width: 100%\\"
|
|
507
500
|
>
|
|
508
501
|
<source
|
|
@@ -561,7 +554,7 @@ exports[`Clip Snapshot component rendered on server renders multiple video sourc
|
|
|
561
554
|
|
|
562
555
|
exports[`Clip Snapshot component rendered on server supports new Origami images, fallbacking to the previous implementation 1`] = `
|
|
563
556
|
"<div class=\\"n-content-layout\\" data-layout-width=\\"full-grid\\">
|
|
564
|
-
<div class=\\"n-content-layout__container\\">
|
|
557
|
+
<div class=\\"n-content-layout__container\\" data-component=\\"clip-set\\">
|
|
565
558
|
<div
|
|
566
559
|
data-cp-clip-layout=\\"full-grid\\"
|
|
567
560
|
data-cp-clip-poster=\\"\\"
|
|
@@ -601,7 +594,6 @@ exports[`Clip Snapshot component rendered on server supports new Origami images,
|
|
|
601
594
|
loop=\\"\\"
|
|
602
595
|
poster=\\"\\"
|
|
603
596
|
preload=\\"auto\\"
|
|
604
|
-
tabindex=\\"0\\"
|
|
605
597
|
style=\\"height: fit-content; width: 100%\\"
|
|
606
598
|
>
|
|
607
599
|
<source
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
2
|
// @ts-nocheck
|
|
3
|
+
|
|
3
4
|
import {
|
|
4
5
|
inlineVideo,
|
|
5
6
|
inlineVideoAutoplay,
|
|
@@ -47,7 +48,7 @@ const mockVideo = (video) => {
|
|
|
47
48
|
}
|
|
48
49
|
video.pause = () => {
|
|
49
50
|
clearInterval(playInterval)
|
|
50
|
-
video.
|
|
51
|
+
video.paused = true
|
|
51
52
|
//mock that when we pause the video at least has passed 100ms
|
|
52
53
|
if (video.currentTime === 0) video.currentTime = 100
|
|
53
54
|
video.dispatchEvent(new Event('pause'))
|
|
@@ -67,6 +68,10 @@ const mockVideo = (video) => {
|
|
|
67
68
|
writable: true,
|
|
68
69
|
value: false,
|
|
69
70
|
})
|
|
71
|
+
Object.defineProperty(video, 'readyState', {
|
|
72
|
+
writable: true,
|
|
73
|
+
value: 4,
|
|
74
|
+
})
|
|
70
75
|
Object.defineProperty(video, 'currentTime', {
|
|
71
76
|
writable: true,
|
|
72
77
|
value: 0,
|
|
@@ -715,10 +720,13 @@ describe('Clip', () => {
|
|
|
715
720
|
|
|
716
721
|
it('should show pause button when the video is played and loop is true', () => {
|
|
717
722
|
const clip = clips[0]
|
|
718
|
-
|
|
723
|
+
clip.videoEl.pause()
|
|
724
|
+
expect(
|
|
725
|
+
document.body.querySelector('.cp-clip__playpause-icon-pause')
|
|
726
|
+
).toBeFalsy()
|
|
719
727
|
clip.videoEl.play()
|
|
720
728
|
expect(
|
|
721
|
-
document.body.querySelector('.cp-
|
|
729
|
+
document.body.querySelector('.cp-clip__playpause-icon-pause')
|
|
722
730
|
).toBeTruthy()
|
|
723
731
|
})
|
|
724
732
|
|
|
@@ -736,24 +744,42 @@ describe('Clip', () => {
|
|
|
736
744
|
clip.videoEl.play()
|
|
737
745
|
})
|
|
738
746
|
|
|
747
|
+
it('should toggle the pause button to a play button when the video is paused', () => {
|
|
748
|
+
const clip = clips[0]
|
|
749
|
+
clip.videoEl.play()
|
|
750
|
+
expect(
|
|
751
|
+
document.body.querySelector('.cp-clip__playpause-icon-play')
|
|
752
|
+
).toBeFalsy()
|
|
753
|
+
clip.videoEl.pause()
|
|
754
|
+
expect(
|
|
755
|
+
document.body.querySelector('.cp-clip__playpause-icon-play')
|
|
756
|
+
).toBeTruthy()
|
|
757
|
+
})
|
|
758
|
+
|
|
739
759
|
it('it should have progress loop indicator', (done) => {
|
|
740
760
|
const clip = clips[0]
|
|
741
761
|
expect(
|
|
742
|
-
document.body.querySelector(
|
|
762
|
+
document.body.querySelector(
|
|
763
|
+
'.cp-clip__playpause-icon-autoplay-progress'
|
|
764
|
+
)
|
|
743
765
|
).toBeTruthy()
|
|
744
766
|
expect(
|
|
745
767
|
isVisible(
|
|
746
|
-
document.body.querySelector(
|
|
768
|
+
document.body.querySelector(
|
|
769
|
+
'.cp-clip__playpause-icon-autoplay-progress'
|
|
770
|
+
)
|
|
747
771
|
)
|
|
748
772
|
).toEqual(false)
|
|
749
773
|
const listener = () => {
|
|
750
774
|
expect(
|
|
751
|
-
document.body.querySelector(
|
|
775
|
+
document.body.querySelector(
|
|
776
|
+
'.cp-clip__playpause-icon-autoplay-progress'
|
|
777
|
+
)
|
|
752
778
|
).toBeTruthy()
|
|
753
779
|
expect(
|
|
754
780
|
isVisible(
|
|
755
781
|
document.body.querySelector(
|
|
756
|
-
'.cp-
|
|
782
|
+
'.cp-clip__playpause-icon-autoplay-progress'
|
|
757
783
|
)
|
|
758
784
|
)
|
|
759
785
|
).not.toEqual(true)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trackEntireRead
|
|
3
|
+
* @description Track the entire read of a truncated post
|
|
4
|
+
* @param {HTMLElement} post - The post element
|
|
5
|
+
*/
|
|
6
|
+
class TrackEntireRead {
|
|
7
|
+
private observer: IntersectionObserver | null = null
|
|
8
|
+
private readElement: Element | null
|
|
9
|
+
// data can be any key value pair that needs to be tracked
|
|
10
|
+
private data: Record<string, string>
|
|
11
|
+
constructor(readElement: Element, data: Record<string, string>) {
|
|
12
|
+
this.readElement = readElement as Element
|
|
13
|
+
this.data = data
|
|
14
|
+
this.init()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private init() {
|
|
18
|
+
//Intersection observer that observes readElement and when in view tracks the entire read
|
|
19
|
+
this.observer = new IntersectionObserver((entries) => {
|
|
20
|
+
entries.forEach((entry) => {
|
|
21
|
+
if (entry.isIntersecting) {
|
|
22
|
+
this.handleEntireRead()
|
|
23
|
+
this.observer?.disconnect()
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
this.observer.observe(this.readElement as Element)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private handleEntireRead = () => {
|
|
31
|
+
const trackingData = {
|
|
32
|
+
action: 'entire_read',
|
|
33
|
+
...this.data,
|
|
34
|
+
}
|
|
35
|
+
const event = new CustomEvent('oTracking.event', {
|
|
36
|
+
detail: trackingData,
|
|
37
|
+
bubbles: true,
|
|
38
|
+
})
|
|
39
|
+
document.body.dispatchEvent(event)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
destroy(): void {
|
|
43
|
+
this.observer?.disconnect()
|
|
44
|
+
this.observer = null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class ExpanderClient {
|
|
48
|
+
/**
|
|
49
|
+
* The container element for all the elements that should be showned or be hidden.
|
|
50
|
+
*/
|
|
51
|
+
public container: HTMLElement | null
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* An optional element to which the expander will dispatch the events to.
|
|
55
|
+
* If not provided, events will not be dispatched.
|
|
56
|
+
*/
|
|
57
|
+
private dispatchBoundary?: Element | Document | null
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Element that will be showned at the end of the content when the expander is expanded.
|
|
61
|
+
* We use this to track when the entire post is read.
|
|
62
|
+
*/
|
|
63
|
+
private endContent: Element | null
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Track the entire read of a truncated post
|
|
67
|
+
*/
|
|
68
|
+
private trackingEntireRead: TrackEntireRead
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The button that will expand the content
|
|
72
|
+
*/
|
|
73
|
+
private expanderButton: HTMLElement | null
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The button that will collapse the content
|
|
77
|
+
*/
|
|
78
|
+
private collapserButton: HTMLElement | null
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The tracking data that will be sent when the entire post is read
|
|
82
|
+
*/
|
|
83
|
+
private trackingData: Record<string, string>
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
*
|
|
87
|
+
* @param el
|
|
88
|
+
* @param dispatchBoundary
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
constructor(
|
|
92
|
+
el: HTMLElement,
|
|
93
|
+
dispatchBoundary?: Element | Document | null,
|
|
94
|
+
trackingData?: Record<string, string>
|
|
95
|
+
) {
|
|
96
|
+
this.container = el
|
|
97
|
+
this.trackingData = trackingData || {}
|
|
98
|
+
this.expanderButton = el.querySelector(
|
|
99
|
+
'.cp-expander__expand [data-action="expand"]'
|
|
100
|
+
) as HTMLElement
|
|
101
|
+
this.collapserButton = el.querySelector(
|
|
102
|
+
'.cp-expander__collapse [data-action="collapse"]'
|
|
103
|
+
) as HTMLElement
|
|
104
|
+
this.endContent = el.querySelector('.cp-expander__collapse')
|
|
105
|
+
|
|
106
|
+
this.expanderButton?.addEventListener('click', this.handleExpand)
|
|
107
|
+
this.collapserButton?.addEventListener('click', this.handleCollapse)
|
|
108
|
+
|
|
109
|
+
this.container.classList.replace(
|
|
110
|
+
'cp-expander--not-initialised',
|
|
111
|
+
'cp-expander--initialised'
|
|
112
|
+
)
|
|
113
|
+
this.dispatchBoundary = dispatchBoundary || this.container
|
|
114
|
+
;(this.trackingData['post_id'] =
|
|
115
|
+
this.container
|
|
116
|
+
.querySelector('[data-trackable-context-truncated-id]')
|
|
117
|
+
?.getAttribute('data-trackable-context-truncated-id') || ''),
|
|
118
|
+
(this.trackingEntireRead = new TrackEntireRead(
|
|
119
|
+
this.endContent as Element,
|
|
120
|
+
this.trackingData
|
|
121
|
+
))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
expand() {
|
|
125
|
+
this.container?.setAttribute('data-state', 'expanded')
|
|
126
|
+
this.dispatchBoundary?.dispatchEvent(
|
|
127
|
+
new CustomEvent('expander:expanded', {
|
|
128
|
+
bubbles: true,
|
|
129
|
+
detail: {
|
|
130
|
+
component: this,
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
collapse() {
|
|
137
|
+
this.container?.setAttribute('data-state', 'collapsed')
|
|
138
|
+
this.container?.parentElement?.parentElement?.scrollIntoView()
|
|
139
|
+
this.dispatchBoundary?.dispatchEvent(
|
|
140
|
+
new CustomEvent('expander:collapsed', {
|
|
141
|
+
bubbles: true,
|
|
142
|
+
detail: {
|
|
143
|
+
component: this,
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private handleExpand = (e: Event) => {
|
|
150
|
+
e.preventDefault()
|
|
151
|
+
this.expand()
|
|
152
|
+
this.expanderButton?.setAttribute('aria-expanded', 'true')
|
|
153
|
+
this.expanderButton?.setAttribute('aria-hidden', 'true')
|
|
154
|
+
this.collapserButton?.setAttribute('aria-expanded', 'true')
|
|
155
|
+
this.collapserButton?.setAttribute('aria-hidden', 'false')
|
|
156
|
+
}
|
|
157
|
+
private handleCollapse = (e: Event) => {
|
|
158
|
+
e.preventDefault()
|
|
159
|
+
this.collapse()
|
|
160
|
+
this.expanderButton?.setAttribute('aria-expanded', 'false')
|
|
161
|
+
this.expanderButton?.setAttribute('aria-hidden', 'false')
|
|
162
|
+
this.collapserButton?.setAttribute('aria-expanded', 'false')
|
|
163
|
+
this.collapserButton?.setAttribute('aria-hidden', 'true')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
destroy(): void {
|
|
167
|
+
this.container?.setAttribute('data-state', 'collapsed')
|
|
168
|
+
this.container
|
|
169
|
+
?.querySelector('.cp-expander__expand')
|
|
170
|
+
?.removeEventListener('click', this.expand)
|
|
171
|
+
this.container
|
|
172
|
+
?.querySelector('.cp-expander__collapse')
|
|
173
|
+
?.removeEventListener('click', this.collapse)
|
|
174
|
+
this.container?.classList.remove('cp-expander--initialised')
|
|
175
|
+
this.trackingEntireRead.destroy()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static init(data?: {
|
|
179
|
+
rootElement?: HTMLElement | null
|
|
180
|
+
dispatchBoundary?: Element | Document | null
|
|
181
|
+
trackingData?: Record<string, string>
|
|
182
|
+
}): ExpanderClient[] {
|
|
183
|
+
const { rootElement, dispatchBoundary, trackingData } = data || {}
|
|
184
|
+
const root = rootElement || document.body
|
|
185
|
+
|
|
186
|
+
return (Array.from(
|
|
187
|
+
root.querySelectorAll(
|
|
188
|
+
':not(.cp-expander--initialised)[data-component="expander"]'
|
|
189
|
+
)
|
|
190
|
+
).map((el: Element) => {
|
|
191
|
+
if (!el.classList.contains('expander--initialised')) {
|
|
192
|
+
return new ExpanderClient(
|
|
193
|
+
el as HTMLElement,
|
|
194
|
+
dispatchBoundary,
|
|
195
|
+
trackingData
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
}) || []) as ExpanderClient[]
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export default ExpanderClient
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
@import '@financial-times/o-icons/main';
|
|
2
|
+
@import '@financial-times/o-colors/main';
|
|
3
|
+
@import '@financial-times/o-grid/main';
|
|
4
|
+
@import '@financial-times/o-spacing/main';
|
|
5
|
+
|
|
6
|
+
$numberIntroductoryElements: 1;
|
|
7
|
+
$dataComponentVisibleElements: (
|
|
8
|
+
'clip-set',
|
|
9
|
+
'recommended',
|
|
10
|
+
'flourish',
|
|
11
|
+
'image-set',
|
|
12
|
+
'video',
|
|
13
|
+
'youtube-video',
|
|
14
|
+
'table'
|
|
15
|
+
);
|
|
16
|
+
$alwaysVisibleComponentSelectors: ();
|
|
17
|
+
|
|
18
|
+
@each $component-name in $dataComponentVisibleElements {
|
|
19
|
+
$alwaysVisibleComponentSelectors: append(
|
|
20
|
+
$alwaysVisibleComponentSelectors,
|
|
21
|
+
'[data-component="#{$component-name}"]'
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
@mixin AlwaysVisibleElements {
|
|
25
|
+
@each $component in $alwaysVisibleComponentSelectors {
|
|
26
|
+
#{$component} {
|
|
27
|
+
display: block;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
@mixin IntroductoryElements {
|
|
32
|
+
& > :nth-child(-n + #{$numberIntroductoryElements + 1}):not(.cp-expander__expand) {
|
|
33
|
+
display: block;
|
|
34
|
+
order:1
|
|
35
|
+
}
|
|
36
|
+
& > :nth-child(n + #{$numberIntroductoryElements + 1}) {
|
|
37
|
+
order:3
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
@mixin expandeAndCollapse {
|
|
41
|
+
&[data-state='expanded'] {
|
|
42
|
+
.cp-expander-content {
|
|
43
|
+
// Show all the hidden children...
|
|
44
|
+
& > * {
|
|
45
|
+
display: block;
|
|
46
|
+
}
|
|
47
|
+
> .cp-expander__expand {
|
|
48
|
+
display: none;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
&[data-state='collapsed'] {
|
|
56
|
+
.cp-expander-content {
|
|
57
|
+
// Hide everything that comes after the expander...
|
|
58
|
+
// ... except the always visible children
|
|
59
|
+
& > * {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
&:target > * {
|
|
63
|
+
display: block;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
> .cp-expander__expand {
|
|
67
|
+
display: block;
|
|
68
|
+
order: 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
> .cp-expander__collapse {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@include AlwaysVisibleElements;
|
|
76
|
+
|
|
77
|
+
@include IntroductoryElements;
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@mixin InnerStyles {
|
|
85
|
+
|
|
86
|
+
&.cp-expander--not-initialised {
|
|
87
|
+
|
|
88
|
+
.cp-expander__expand,
|
|
89
|
+
.cp-expander__collapse {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.cp-expander-content {
|
|
94
|
+
// Target is used for the Core experience.
|
|
95
|
+
// It permits us to hide elements server-side via CSS preventing Cumulative Shift but it is also a fully functional solution without Javascript
|
|
96
|
+
&:target{
|
|
97
|
+
.cp-expander__collapse{
|
|
98
|
+
display: block;
|
|
99
|
+
}
|
|
100
|
+
.cp-expander__expand{
|
|
101
|
+
display: none;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
.cp-expander__expand,
|
|
107
|
+
.cp-expander__collapse {
|
|
108
|
+
display: block;
|
|
109
|
+
padding-bottom: oSpacingByName('s6');
|
|
110
|
+
> a {
|
|
111
|
+
$icon-color: oColorsByName('ft-grey');
|
|
112
|
+
--_o-typography-body-color: $icon-color;
|
|
113
|
+
font-feature-settings: 'clig' off, 'liga' off;
|
|
114
|
+
|
|
115
|
+
@include oTypographyBody();
|
|
116
|
+
font-weight: 400;
|
|
117
|
+
text-decoration: none;
|
|
118
|
+
font-size: 20px;
|
|
119
|
+
&:hover {
|
|
120
|
+
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
&:after {
|
|
125
|
+
content: '';
|
|
126
|
+
@include oIconsContent('arrow-down', $icon-color, $size: 20);
|
|
127
|
+
padding: 0 4px;
|
|
128
|
+
vertical-align: middle;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.cp-expander__collapse a:after {
|
|
134
|
+
transform: rotate(180deg);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.cp-expander-content {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@include expandeAndCollapse;
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.cp-expander{
|
|
148
|
+
&.cp-expander--mobile-only {
|
|
149
|
+
.cp-expander__expand,
|
|
150
|
+
.cp-expander__collapse {
|
|
151
|
+
display: none;
|
|
152
|
+
}
|
|
153
|
+
@include oGridRespondTo($until: S) {
|
|
154
|
+
@include InnerStyles;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
&.cp-expander--all-resolutions {
|
|
159
|
+
@include InnerStyles;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import classnames from 'classnames'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
export interface ExpanderProps extends React.PropsWithChildren {
|
|
5
|
+
/** The initial state of the expander */
|
|
6
|
+
state?: 'expanded' | 'collapsed'
|
|
7
|
+
/** The label for the Expand CTA */
|
|
8
|
+
expandLabel?: string
|
|
9
|
+
/** The label for the collapse CTA */
|
|
10
|
+
collapseLabel?: string
|
|
11
|
+
/** Show the expander only on mobile resolutions */
|
|
12
|
+
onlyMobile?: boolean
|
|
13
|
+
/** Id of the component */
|
|
14
|
+
id: string
|
|
15
|
+
}
|
|
16
|
+
// React functional component with children
|
|
17
|
+
export const ExpanderServer: React.FC<ExpanderProps> = ({
|
|
18
|
+
state = 'collapsed', // default value for "state
|
|
19
|
+
children,
|
|
20
|
+
id,
|
|
21
|
+
expandLabel = 'Expand',
|
|
22
|
+
collapseLabel = 'Collapse',
|
|
23
|
+
onlyMobile = false,
|
|
24
|
+
}) => {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={classnames({
|
|
28
|
+
'cp-expander': true,
|
|
29
|
+
'cp-expander--mobile-only': onlyMobile,
|
|
30
|
+
'cp-expander--all-resolutions': !onlyMobile,
|
|
31
|
+
'cp-expander--not-initialised': true,
|
|
32
|
+
})}
|
|
33
|
+
data-component="expander"
|
|
34
|
+
data-state={state}
|
|
35
|
+
id={`${id}__container`}
|
|
36
|
+
>
|
|
37
|
+
<div id={id} className="cp-expander-content">
|
|
38
|
+
<div className="cp-expander__expand">
|
|
39
|
+
<a
|
|
40
|
+
data-trackable="truncated-post"
|
|
41
|
+
data-trackable-context-truncated-post="expand"
|
|
42
|
+
data-trackable-context-truncated-id={id}
|
|
43
|
+
className="cp-expander__link"
|
|
44
|
+
href={`#${id}`}
|
|
45
|
+
aria-expanded={state === 'expanded' ? true : false}
|
|
46
|
+
aria-controls={id}
|
|
47
|
+
aria-hidden={state === 'expanded' ? true : false}
|
|
48
|
+
data-action="expand"
|
|
49
|
+
>
|
|
50
|
+
{expandLabel}
|
|
51
|
+
</a>
|
|
52
|
+
</div>
|
|
53
|
+
{children}
|
|
54
|
+
<div className="cp-expander__collapse">
|
|
55
|
+
<a
|
|
56
|
+
data-trackable="truncated-post"
|
|
57
|
+
data-trackable-context-truncated-post="collapse"
|
|
58
|
+
data-trackable-context-truncated-id={id}
|
|
59
|
+
href={`#${id}__container`}
|
|
60
|
+
className="cp-expander__link"
|
|
61
|
+
aria-expanded={state === 'expanded' ? true : false}
|
|
62
|
+
aria-controls={id}
|
|
63
|
+
aria-hidden={state === 'expanded' ? false : true}
|
|
64
|
+
data-action="collapse"
|
|
65
|
+
>
|
|
66
|
+
{collapseLabel}
|
|
67
|
+
</a>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default ExpanderServer
|