@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.
Files changed (90) 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/components/ClipTag.js +1 -1
  11. package/lib/components/Clip/components/ClipTag.js.map +1 -1
  12. package/lib/components/Clip/components/Container.js +2 -2
  13. package/lib/components/Clip/components/Container.js.map +1 -1
  14. package/lib/components/Clip/template/index.js +0 -1
  15. package/lib/components/Clip/template/index.js.map +1 -1
  16. package/lib/components/Clip/test/index.spec.js +21 -9
  17. package/lib/components/Clip/test/index.spec.js.map +1 -1
  18. package/lib/components/Expander/client/index.d.ts +49 -0
  19. package/lib/components/Expander/client/index.js +124 -0
  20. package/lib/components/Expander/client/index.js.map +1 -0
  21. package/lib/components/Expander/index.d.ts +15 -0
  22. package/lib/components/Expander/index.js +27 -0
  23. package/lib/components/Expander/index.js.map +1 -0
  24. package/lib/components/Expander/test/client/index.spec.d.ts +1 -0
  25. package/lib/components/Expander/test/client/index.spec.js +103 -0
  26. package/lib/components/Expander/test/client/index.spec.js.map +1 -0
  27. package/lib/components/Expander/test/index.spec.d.ts +1 -0
  28. package/lib/components/Expander/test/index.spec.js +57 -0
  29. package/lib/components/Expander/test/index.spec.js.map +1 -0
  30. package/lib/components/Expander/test/snapshot.spec.d.ts +1 -0
  31. package/lib/components/Expander/test/snapshot.spec.js +63 -0
  32. package/lib/components/Expander/test/snapshot.spec.js.map +1 -0
  33. package/lib/components/ImageSet/index.js +1 -1
  34. package/lib/components/ImageSet/index.js.map +1 -1
  35. package/lib/components/LiveBlogPost/client/index.d.ts +4 -0
  36. package/lib/components/LiveBlogPost/client/index.js +19 -0
  37. package/lib/components/LiveBlogPost/client/index.js.map +1 -0
  38. package/lib/components/LiveBlogPost/index.js +9 -21
  39. package/lib/components/LiveBlogPost/index.js.map +1 -1
  40. package/lib/components/LiveBlogWrapper/index.js +1 -1
  41. package/lib/components/LiveBlogWrapper/index.js.map +1 -1
  42. package/lib/components/Recommended/index.js +1 -1
  43. package/lib/components/Recommended/index.js.map +1 -1
  44. package/lib/components/RichText/index.d.ts +1 -1
  45. package/lib/components/Table/index.js +1 -1
  46. package/lib/components/Table/index.js.map +1 -1
  47. package/lib/components/Video/index.js +1 -1
  48. package/lib/components/Video/index.js.map +1 -1
  49. package/lib/components/YoutubeVideo/index.js +1 -1
  50. package/lib/components/YoutubeVideo/index.js.map +1 -1
  51. package/lib/extensions/scrollIntoView.d.ts +10 -0
  52. package/lib/extensions/scrollIntoView.js +32 -0
  53. package/lib/extensions/scrollIntoView.js.map +1 -0
  54. package/lib/stories/Clip.stories.d.ts +2 -1
  55. package/lib/stories/Clip.stories.js +5 -5
  56. package/lib/stories/Clip.stories.js.map +1 -1
  57. package/lib/stories/Expander.stories.d.ts +54 -0
  58. package/lib/stories/Expander.stories.js +142 -0
  59. package/lib/stories/Expander.stories.js.map +1 -0
  60. package/package.json +2 -5
  61. package/src/components/BackToTopButton/client/index.tsx +8 -1
  62. package/src/components/Body/__snapshots__/index.test.tsx.snap +55 -5
  63. package/src/components/Body/index.test.tsx +9 -0
  64. package/src/components/Clip/client/index.ts +68 -26
  65. package/src/components/Clip/client/main.scss +27 -12
  66. package/src/components/Clip/components/ClipTag.tsx +0 -1
  67. package/src/components/Clip/components/Container.tsx +10 -3
  68. package/src/components/Clip/template/index.ts +0 -1
  69. package/src/components/Clip/test/__snapshots__/snapshot.spec.tsx.snap +8 -16
  70. package/src/components/Clip/test/index.spec.ts +33 -7
  71. package/src/components/Expander/client/index.ts +201 -0
  72. package/src/components/Expander/client/main.scss +162 -0
  73. package/src/components/Expander/index.tsx +74 -0
  74. package/src/components/Expander/test/__snapshots__/snapshot.spec.tsx.snap +221 -0
  75. package/src/components/Expander/test/client/index.spec.tsx +129 -0
  76. package/src/components/Expander/test/index.spec.tsx +77 -0
  77. package/src/components/Expander/test/snapshot.spec.tsx +73 -0
  78. package/src/components/ImageSet/index.tsx +1 -0
  79. package/src/components/LiveBlogPost/client/index.ts +16 -0
  80. package/src/components/LiveBlogPost/index.tsx +29 -43
  81. package/src/components/LiveBlogWrapper/index.tsx +1 -0
  82. package/src/components/Recommended/index.tsx +1 -0
  83. package/src/components/Table/index.tsx +1 -0
  84. package/src/components/Video/index.tsx +4 -1
  85. package/src/components/YoutubeVideo/index.tsx +4 -1
  86. package/src/extensions/scrollIntoView.ts +38 -0
  87. package/src/stories/Clip.stories.tsx +3 -2
  88. package/src/stories/Expander.stories.scss +3 -0
  89. package/src/stories/Expander.stories.tsx +159 -0
  90. 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.pause = true
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
- expect(document.body.querySelector('.cp-clip__pause-icon')).toBeFalsy()
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-clip__pause-icon-autoplay')
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('.cp-clip__pause-icon-autoplay-progress')
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('.cp-clip__pause-icon-autoplay-progress')
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('.cp-clip__pause-icon-autoplay-progress')
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-clip__pause-icon-autoplay-progress'
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