@financial-times/x-teaser 20.0.0-beta.1 → 20.0.0-beta.3

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/Props.d.ts CHANGED
@@ -66,7 +66,7 @@ export interface Meta {
66
66
  /** @deprecated - use metaLinks instead */
67
67
  metaLink?: MetaLink
68
68
  /** @deprecated - use metaLinks instead
69
- * allback used if the parentId is the same as the display concept */
69
+ * Fallback used if the parentId is the same as the display concept */
70
70
  metaAltLink?: MetaLink
71
71
  /** Promoted content type */
72
72
  promotedPrefixText?: string
@@ -0,0 +1,38 @@
1
+ const { h } = require('@financial-times/x-engine')
2
+ const { mount } = require('@financial-times/x-test-utils/enzyme')
3
+ const renderer = require('react-test-renderer')
4
+ const { Teaser } = require('../')
5
+ const article = require('../__fixtures__/article.json')
6
+
7
+ describe('x-teaser / CustomSlotTop', () => {
8
+ let props
9
+
10
+ beforeEach(() => {
11
+ props = {
12
+ ...article,
13
+ showTitle: true
14
+ }
15
+ })
16
+
17
+ it('should not render a custom slot top as default', () => {
18
+ const tree = renderer.create(h(Teaser, props)).toJSON()
19
+ expect(tree).toMatchSnapshot()
20
+ })
21
+
22
+ it('render a custom slot top with a string', () => {
23
+ const customSlotTopHTML = '<p>Some HTML</p>'
24
+ props.showCustomSlotTop = true
25
+ props.customSlotTop = customSlotTopHTML
26
+ const subject = mount(<Teaser {...props} />)
27
+ expect(subject.find('.x-teaser-custom-slot-top').html()).toBe(
28
+ `<div class="x-teaser-custom-slot-top">${customSlotTopHTML}</div>`
29
+ )
30
+ })
31
+
32
+ it('render a custom slot top with a React element', () => {
33
+ props.showCustomSlotTop = true
34
+ props.customSlotTop = <div className="custom-slot-top--with-react-element">Custom slot top content</div>
35
+ const subject = mount(<Teaser {...props} />)
36
+ expect(subject.find('.custom-slot-top--with-react-element')).toBeTruthy()
37
+ })
38
+ })
@@ -0,0 +1,25 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`x-teaser / CustomSlotTop should not render a custom slot top as default 1`] = `
4
+ <div
5
+ className="o-teaser o-teaser--article o-teaser--highlight js-teaser"
6
+ data-id=""
7
+ >
8
+ <div
9
+ className="o-teaser__content"
10
+ >
11
+ <div
12
+ className="o-teaser__heading"
13
+ >
14
+ <a
15
+ className="js-teaser-heading-link"
16
+ data-trackable="heading-link"
17
+ data-trackable-context-story-link="heading-link"
18
+ href="#"
19
+ >
20
+ Inside charity fundraiser where hostesses are put on show
21
+ </a>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ `;
@@ -256,6 +256,7 @@ exports[`x-teaser / snapshots renders a Hero teaser with opinion data 1`] = `
256
256
  className="o-teaser__heading"
257
257
  >
258
258
  <a
259
+ aria-label="Opinion content. Anti-Semitism and the threat of identity politics"
259
260
  className="js-teaser-heading-link"
260
261
  data-trackable="heading-link"
261
262
  data-trackable-context-story-link="heading-link"
@@ -346,6 +347,7 @@ exports[`x-teaser / snapshots renders a Hero teaser with packageItem data 1`] =
346
347
  className="o-teaser__heading"
347
348
  >
348
349
  <a
350
+ aria-label="Opinion content. Why so little has changed since the crash"
349
351
  className="js-teaser-heading-link"
350
352
  data-trackable="heading-link"
351
353
  data-trackable-context-story-link="heading-link"
@@ -886,6 +888,7 @@ exports[`x-teaser / snapshots renders a HeroNarrow teaser with opinion data 1`]
886
888
  className="o-teaser__heading"
887
889
  >
888
890
  <a
891
+ aria-label="Opinion content. Anti-Semitism and the threat of identity politics"
889
892
  className="js-teaser-heading-link"
890
893
  data-trackable="heading-link"
891
894
  data-trackable-context-story-link="heading-link"
@@ -963,6 +966,7 @@ exports[`x-teaser / snapshots renders a HeroNarrow teaser with packageItem data
963
966
  className="o-teaser__heading"
964
967
  >
965
968
  <a
969
+ aria-label="Opinion content. Why so little has changed since the crash"
966
970
  className="js-teaser-heading-link"
967
971
  data-trackable="heading-link"
968
972
  data-trackable-context-story-link="heading-link"
@@ -1464,6 +1468,7 @@ exports[`x-teaser / snapshots renders a HeroOverlay teaser with opinion data 1`]
1464
1468
  className="o-teaser__heading"
1465
1469
  >
1466
1470
  <a
1471
+ aria-label="Opinion content. Anti-Semitism and the threat of identity politics"
1467
1472
  className="js-teaser-heading-link"
1468
1473
  data-trackable="heading-link"
1469
1474
  data-trackable-context-story-link="heading-link"
@@ -1554,6 +1559,7 @@ exports[`x-teaser / snapshots renders a HeroOverlay teaser with packageItem data
1554
1559
  className="o-teaser__heading"
1555
1560
  >
1556
1561
  <a
1562
+ aria-label="Opinion content. Why so little has changed since the crash"
1557
1563
  className="js-teaser-heading-link"
1558
1564
  data-trackable="heading-link"
1559
1565
  data-trackable-context-story-link="heading-link"
@@ -2042,6 +2048,7 @@ exports[`x-teaser / snapshots renders a HeroVideo teaser with opinion data 1`] =
2042
2048
  className="o-teaser__heading"
2043
2049
  >
2044
2050
  <a
2051
+ aria-label="Opinion content. Anti-Semitism and the threat of identity politics"
2045
2052
  className="js-teaser-heading-link"
2046
2053
  data-trackable="heading-link"
2047
2054
  data-trackable-context-story-link="heading-link"
@@ -2106,6 +2113,7 @@ exports[`x-teaser / snapshots renders a HeroVideo teaser with packageItem data 1
2106
2113
  className="o-teaser__heading"
2107
2114
  >
2108
2115
  <a
2116
+ aria-label="Opinion content. Why so little has changed since the crash"
2109
2117
  className="js-teaser-heading-link"
2110
2118
  data-trackable="heading-link"
2111
2119
  data-trackable-context-story-link="heading-link"
@@ -2651,6 +2659,7 @@ exports[`x-teaser / snapshots renders a Large teaser with opinion data 1`] = `
2651
2659
  className="o-teaser__heading"
2652
2660
  >
2653
2661
  <a
2662
+ aria-label="Opinion content. Anti-Semitism and the threat of identity politics"
2654
2663
  className="js-teaser-heading-link"
2655
2664
  data-trackable="heading-link"
2656
2665
  data-trackable-context-story-link="heading-link"
@@ -2754,6 +2763,7 @@ exports[`x-teaser / snapshots renders a Large teaser with packageItem data 1`] =
2754
2763
  className="o-teaser__heading"
2755
2764
  >
2756
2765
  <a
2766
+ aria-label="Opinion content. Why so little has changed since the crash"
2757
2767
  className="js-teaser-heading-link"
2758
2768
  data-trackable="heading-link"
2759
2769
  data-trackable-context-story-link="heading-link"
@@ -3307,6 +3317,7 @@ exports[`x-teaser / snapshots renders a Small teaser with opinion data 1`] = `
3307
3317
  className="o-teaser__heading"
3308
3318
  >
3309
3319
  <a
3320
+ aria-label="Opinion content. Anti-Semitism and the threat of identity politics"
3310
3321
  className="js-teaser-heading-link"
3311
3322
  data-trackable="heading-link"
3312
3323
  data-trackable-context-story-link="heading-link"
@@ -3371,6 +3382,7 @@ exports[`x-teaser / snapshots renders a Small teaser with packageItem data 1`] =
3371
3382
  className="o-teaser__heading"
3372
3383
  >
3373
3384
  <a
3385
+ aria-label="Opinion content. Why so little has changed since the crash"
3374
3386
  className="js-teaser-heading-link"
3375
3387
  data-trackable="heading-link"
3376
3388
  data-trackable-context-story-link="heading-link"
@@ -3859,6 +3871,7 @@ exports[`x-teaser / snapshots renders a SmallHeavy teaser with opinion data 1`]
3859
3871
  className="o-teaser__heading"
3860
3872
  >
3861
3873
  <a
3874
+ aria-label="Opinion content. Anti-Semitism and the threat of identity politics"
3862
3875
  className="js-teaser-heading-link"
3863
3876
  data-trackable="heading-link"
3864
3877
  data-trackable-context-story-link="heading-link"
@@ -3962,6 +3975,7 @@ exports[`x-teaser / snapshots renders a SmallHeavy teaser with packageItem data
3962
3975
  className="o-teaser__heading"
3963
3976
  >
3964
3977
  <a
3978
+ aria-label="Opinion content. Why so little has changed since the crash"
3965
3979
  className="js-teaser-heading-link"
3966
3980
  data-trackable="heading-link"
3967
3981
  data-trackable-context-story-link="heading-link"
@@ -4567,6 +4581,7 @@ exports[`x-teaser / snapshots renders a TopStory teaser with opinion data 1`] =
4567
4581
  className="o-teaser__heading"
4568
4582
  >
4569
4583
  <a
4584
+ aria-label="Opinion content. Anti-Semitism and the threat of identity politics"
4570
4585
  className="js-teaser-heading-link"
4571
4586
  data-trackable="heading-link"
4572
4587
  data-trackable-context-story-link="heading-link"
@@ -4644,6 +4659,7 @@ exports[`x-teaser / snapshots renders a TopStory teaser with packageItem data 1`
4644
4659
  className="o-teaser__heading"
4645
4660
  >
4646
4661
  <a
4662
+ aria-label="Opinion content. Why so little has changed since the crash"
4647
4663
  className="js-teaser-heading-link"
4648
4664
  data-trackable="heading-link"
4649
4665
  data-trackable-context-story-link="heading-link"
@@ -5234,6 +5250,7 @@ exports[`x-teaser / snapshots renders a TopStoryLandscape teaser with opinion da
5234
5250
  className="o-teaser__heading"
5235
5251
  >
5236
5252
  <a
5253
+ aria-label="Opinion content. Anti-Semitism and the threat of identity politics"
5237
5254
  className="js-teaser-heading-link"
5238
5255
  data-trackable="heading-link"
5239
5256
  data-trackable-context-story-link="heading-link"
@@ -5337,6 +5354,7 @@ exports[`x-teaser / snapshots renders a TopStoryLandscape teaser with packageIte
5337
5354
  className="o-teaser__heading"
5338
5355
  >
5339
5356
  <a
5357
+ aria-label="Opinion content. Why so little has changed since the crash"
5340
5358
  className="js-teaser-heading-link"
5341
5359
  data-trackable="heading-link"
5342
5360
  data-trackable-context-story-link="heading-link"
@@ -0,0 +1,43 @@
1
+ import sameLabel from '../src/concerns/same-label'
2
+
3
+ describe('same-label', () => {
4
+ describe('when label matches context.parentLabel', () => {
5
+ it('returns true', () => {
6
+ const context = { parentLabel: 'Opinion' }
7
+ const label = 'Opinion'
8
+ expect(sameLabel(context, label)).toBe(true)
9
+ })
10
+ })
11
+
12
+ describe('when label does not match context.parentLabel', () => {
13
+ it('returns false', () => {
14
+ const context = { parentLabel: 'Opinion' }
15
+ const label = 'News'
16
+ expect(sameLabel(context, label)).toBe(false)
17
+ })
18
+ })
19
+
20
+ describe('when label is undefined', () => {
21
+ it('returns false', () => {
22
+ const context = { parentLabel: 'Opinion' }
23
+ const label = undefined
24
+ expect(sameLabel(context, label)).toBe(false)
25
+ })
26
+ })
27
+
28
+ describe('when context.parentLabel is undefined', () => {
29
+ it('returns false', () => {
30
+ const context = { parentLabel: undefined }
31
+ const label = 'News'
32
+ expect(sameLabel(context, label)).toBe(false)
33
+ })
34
+ })
35
+
36
+ describe('when both label and context.parentLabel are undefined', () => {
37
+ it('returns false', () => {
38
+ const context = { parentLabel: undefined }
39
+ const label = undefined
40
+ expect(sameLabel(context, label)).toBe(false)
41
+ })
42
+ })
43
+ })
@@ -98,12 +98,34 @@ var Content = ({
98
98
  className: "o-teaser__content"
99
99
  }, children);
100
100
 
101
+ /**
102
+ * Render
103
+ * @param {String|ReactElement} slot
104
+ * @returns {ReactElement}
105
+ */
106
+ const render = slot => {
107
+ // Allow parent components to pass raw HTML strings
108
+ if (typeof slot === 'string') {
109
+ return xEngine.h("div", {
110
+ className: "x-teaser-custom-slot-top",
111
+ dangerouslySetInnerHTML: {
112
+ __html: slot
113
+ }
114
+ });
115
+ } else {
116
+ return slot;
117
+ }
118
+ };
119
+ var CustomSlotTop = ({
120
+ customSlotTop
121
+ }) => customSlotTop ? render(customSlotTop) : null;
122
+
101
123
  /**
102
124
  * Render
103
125
  * @param {String|ReactElement} action
104
126
  * @returns {ReactElement}
105
127
  */
106
- const render = action => {
128
+ const render$1 = action => {
107
129
  // Allow parent components to pass raw HTML strings
108
130
  if (typeof action === 'string') {
109
131
  return xEngine.h("span", {
@@ -119,7 +141,7 @@ var CustomSlot = ({
119
141
  customSlot
120
142
  }) => customSlot ? xEngine.h("div", {
121
143
  className: "o-teaser__action"
122
- }, render(customSlot)) : null;
144
+ }, render$1(customSlot)) : null;
123
145
 
124
146
  const ImageSizes = {
125
147
  Headshot: 75,
@@ -342,9 +364,10 @@ var Image = ({
342
364
  }))));
343
365
  };
344
366
 
345
- const sameLabel = (context = {}, label) => {
346
- return label && context && context.parentLabel && label === context.parentLabel;
347
- };
367
+ function sameLabel(context = {}, label) {
368
+ return Boolean(label && label === (context === null || context === void 0 ? void 0 : context.parentLabel));
369
+ }
370
+
348
371
  const isLegacyMode = metaLinks => !Array.isArray(metaLinks);
349
372
  const getDisplayLink = ({
350
373
  metaLink,
@@ -646,6 +669,7 @@ var Title = ({
646
669
  url,
647
670
  ...props
648
671
  }) => {
672
+ var _props$indicators;
649
673
  const displayTitle = headlineTesting && altTitle ? altTitle : title;
650
674
  const displayUrl = relativeUrl || url;
651
675
  let ariaLabel;
@@ -653,6 +677,8 @@ var Title = ({
653
677
  ariaLabel = `Watch video ${displayTitle}`;
654
678
  } else if (props.type === 'audio') {
655
679
  ariaLabel = `Listen to podcast ${displayTitle}`;
680
+ } else if (((_props$indicators = props.indicators) === null || _props$indicators === void 0 ? void 0 : _props$indicators.isOpinion) === true) {
681
+ ariaLabel = `Opinion content. ${displayTitle}`;
656
682
  }
657
683
  return xEngine.h("div", {
658
684
  className: "o-teaser__heading"
@@ -664,7 +690,7 @@ var Title = ({
664
690
  className: 'js-teaser-heading-link',
665
691
  'aria-label': ariaLabel
666
692
  }
667
- }), showTitlePrefix && titlePrefix && xEngine.h("span", {
693
+ }), showTitlePrefix && titlePrefix && !sameLabel(props.context, titlePrefix) && xEngine.h("span", {
668
694
  className: "o-teaser__heading-prefix"
669
695
  }, `${titlePrefix}. `), displayTitle));
670
696
  };
@@ -849,13 +875,14 @@ var presets = {
849
875
  TopStoryLandscape
850
876
  };
851
877
 
852
- const Teaser = props => xEngine.h(Container, props, xEngine.h(Content, null, props.showMeta ? xEngine.h(Meta, props) : null, media(props) === 'video' ? xEngine.h(Video, props) : null, props.showTitle ? xEngine.h(Title, props) : null, props.showStandfirst ? xEngine.h(Standfirst, props) : null, props.showByline ? xEngine.h(Byline, props) : null, xEngine.h(Status, props), props.showCustomSlot ? xEngine.h(CustomSlot, props) : null,
878
+ const Teaser = props => xEngine.h(Container, props, xEngine.h(Content, null, props.showCustomSlotTop ? xEngine.h(CustomSlotTop, props) : null, props.showMeta ? xEngine.h(Meta, props) : null, media(props) === 'video' ? xEngine.h(Video, props) : null, props.showTitle ? xEngine.h(Title, props) : null, props.showStandfirst ? xEngine.h(Standfirst, props) : null, props.showByline ? xEngine.h(Byline, props) : null, xEngine.h(Status, props), props.showCustomSlot ? xEngine.h(CustomSlot, props) : null,
853
879
  /* Headshot is a legacy element.
854
880
  The Byline component already includes a headshot.
855
881
  Only render the Headshot when the media rule `headshot` is true,
856
882
  which means `showByline` is falsy. */
857
883
  media(props) === 'headshot' ? xEngine.h(Headshot, props) : null), media(props) === 'promotionalContent' ? xEngine.h(PromotionalContent, props) : null, media(props) === 'image' ? xEngine.h(Image, props) : null, props.showRelatedLinks ? xEngine.h(RelatedLinks, props) : null);
858
884
 
885
+ exports.Byline = Byline;
859
886
  exports.Container = Container;
860
887
  exports.Content = Content;
861
888
  exports.CustomSlot = CustomSlot;
@@ -106,12 +106,35 @@ var Content = (function (_ref) {
106
106
  }, children);
107
107
  });
108
108
 
109
+ /**
110
+ * Render
111
+ * @param {String|ReactElement} slot
112
+ * @returns {ReactElement}
113
+ */
114
+ var render = function render(slot) {
115
+ // Allow parent components to pass raw HTML strings
116
+ if (typeof slot === 'string') {
117
+ return xEngine.h("div", {
118
+ className: "x-teaser-custom-slot-top",
119
+ dangerouslySetInnerHTML: {
120
+ __html: slot
121
+ }
122
+ });
123
+ } else {
124
+ return slot;
125
+ }
126
+ };
127
+ var CustomSlotTop = (function (_ref) {
128
+ var customSlotTop = _ref.customSlotTop;
129
+ return customSlotTop ? render(customSlotTop) : null;
130
+ });
131
+
109
132
  /**
110
133
  * Render
111
134
  * @param {String|ReactElement} action
112
135
  * @returns {ReactElement}
113
136
  */
114
- var render = function render(action) {
137
+ var render$1 = function render(action) {
115
138
  // Allow parent components to pass raw HTML strings
116
139
  if (typeof action === 'string') {
117
140
  return xEngine.h("span", {
@@ -127,7 +150,7 @@ var CustomSlot = (function (_ref) {
127
150
  var customSlot = _ref.customSlot;
128
151
  return customSlot ? xEngine.h("div", {
129
152
  className: "o-teaser__action"
130
- }, render(customSlot)) : null;
153
+ }, render$1(customSlot)) : null;
131
154
  });
132
155
 
133
156
  function _iterableToArrayLimit(r, l) {
@@ -485,11 +508,12 @@ var Image = (function (_ref4) {
485
508
  }))));
486
509
  });
487
510
 
488
- var sameLabel = function sameLabel() {
511
+ function sameLabel() {
489
512
  var context = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
490
513
  var label = arguments.length > 1 ? arguments[1] : undefined;
491
- return label && context && context.parentLabel && label === context.parentLabel;
492
- };
514
+ return Boolean(label && label === (context === null || context === void 0 ? void 0 : context.parentLabel));
515
+ }
516
+
493
517
  var isLegacyMode = function isLegacyMode(metaLinks) {
494
518
  return !Array.isArray(metaLinks);
495
519
  };
@@ -791,6 +815,7 @@ var Standfirst = (function (_ref) {
791
815
 
792
816
  var _excluded$3 = ["title", "altTitle", "showTitlePrefix", "titlePrefix", "headlineTesting", "relativeUrl", "url"];
793
817
  var Title = (function (_ref) {
818
+ var _props$indicators;
794
819
  var title = _ref.title,
795
820
  altTitle = _ref.altTitle,
796
821
  showTitlePrefix = _ref.showTitlePrefix,
@@ -806,6 +831,8 @@ var Title = (function (_ref) {
806
831
  ariaLabel = "Watch video ".concat(displayTitle);
807
832
  } else if (props.type === 'audio') {
808
833
  ariaLabel = "Listen to podcast ".concat(displayTitle);
834
+ } else if (((_props$indicators = props.indicators) === null || _props$indicators === void 0 ? void 0 : _props$indicators.isOpinion) === true) {
835
+ ariaLabel = "Opinion content. ".concat(displayTitle);
809
836
  }
810
837
  return xEngine.h("div", {
811
838
  className: "o-teaser__heading"
@@ -817,7 +844,7 @@ var Title = (function (_ref) {
817
844
  className: 'js-teaser-heading-link',
818
845
  'aria-label': ariaLabel
819
846
  }
820
- }), showTitlePrefix && titlePrefix && xEngine.h("span", {
847
+ }), showTitlePrefix && titlePrefix && !sameLabel(props.context, titlePrefix) && xEngine.h("span", {
821
848
  className: "o-teaser__heading-prefix"
822
849
  }, "".concat(titlePrefix, ". ")), displayTitle));
823
850
  });
@@ -1008,7 +1035,7 @@ var presets = {
1008
1035
  };
1009
1036
 
1010
1037
  var Teaser = function Teaser(props) {
1011
- return xEngine.h(Container, props, xEngine.h(Content, null, props.showMeta ? xEngine.h(Meta, props) : null, media(props) === 'video' ? xEngine.h(Video, props) : null, props.showTitle ? xEngine.h(Title, props) : null, props.showStandfirst ? xEngine.h(Standfirst, props) : null, props.showByline ? xEngine.h(Byline, props) : null, xEngine.h(Status, props), props.showCustomSlot ? xEngine.h(CustomSlot, props) : null,
1038
+ return xEngine.h(Container, props, xEngine.h(Content, null, props.showCustomSlotTop ? xEngine.h(CustomSlotTop, props) : null, props.showMeta ? xEngine.h(Meta, props) : null, media(props) === 'video' ? xEngine.h(Video, props) : null, props.showTitle ? xEngine.h(Title, props) : null, props.showStandfirst ? xEngine.h(Standfirst, props) : null, props.showByline ? xEngine.h(Byline, props) : null, xEngine.h(Status, props), props.showCustomSlot ? xEngine.h(CustomSlot, props) : null,
1012
1039
  /* Headshot is a legacy element.
1013
1040
  The Byline component already includes a headshot.
1014
1041
  Only render the Headshot when the media rule `headshot` is true,
@@ -1016,6 +1043,7 @@ var Teaser = function Teaser(props) {
1016
1043
  media(props) === 'headshot' ? xEngine.h(Headshot, props) : null), media(props) === 'promotionalContent' ? xEngine.h(PromotionalContent, props) : null, media(props) === 'image' ? xEngine.h(Image, props) : null, props.showRelatedLinks ? xEngine.h(RelatedLinks, props) : null);
1017
1044
  };
1018
1045
 
1046
+ exports.Byline = Byline;
1019
1047
  exports.Container = Container;
1020
1048
  exports.Content = Content;
1021
1049
  exports.CustomSlot = CustomSlot;
@@ -92,12 +92,34 @@ var Content = ({
92
92
  className: "o-teaser__content"
93
93
  }, children);
94
94
 
95
+ /**
96
+ * Render
97
+ * @param {String|ReactElement} slot
98
+ * @returns {ReactElement}
99
+ */
100
+ const render = slot => {
101
+ // Allow parent components to pass raw HTML strings
102
+ if (typeof slot === 'string') {
103
+ return h("div", {
104
+ className: "x-teaser-custom-slot-top",
105
+ dangerouslySetInnerHTML: {
106
+ __html: slot
107
+ }
108
+ });
109
+ } else {
110
+ return slot;
111
+ }
112
+ };
113
+ var CustomSlotTop = ({
114
+ customSlotTop
115
+ }) => customSlotTop ? render(customSlotTop) : null;
116
+
95
117
  /**
96
118
  * Render
97
119
  * @param {String|ReactElement} action
98
120
  * @returns {ReactElement}
99
121
  */
100
- const render = action => {
122
+ const render$1 = action => {
101
123
  // Allow parent components to pass raw HTML strings
102
124
  if (typeof action === 'string') {
103
125
  return h("span", {
@@ -113,7 +135,7 @@ var CustomSlot = ({
113
135
  customSlot
114
136
  }) => customSlot ? h("div", {
115
137
  className: "o-teaser__action"
116
- }, render(customSlot)) : null;
138
+ }, render$1(customSlot)) : null;
117
139
 
118
140
  const ImageSizes = {
119
141
  Headshot: 75,
@@ -336,9 +358,10 @@ var Image = ({
336
358
  }))));
337
359
  };
338
360
 
339
- const sameLabel = (context = {}, label) => {
340
- return label && context && context.parentLabel && label === context.parentLabel;
341
- };
361
+ function sameLabel(context = {}, label) {
362
+ return Boolean(label && label === (context === null || context === void 0 ? void 0 : context.parentLabel));
363
+ }
364
+
342
365
  const isLegacyMode = metaLinks => !Array.isArray(metaLinks);
343
366
  const getDisplayLink = ({
344
367
  metaLink,
@@ -640,6 +663,7 @@ var Title = ({
640
663
  url,
641
664
  ...props
642
665
  }) => {
666
+ var _props$indicators;
643
667
  const displayTitle = headlineTesting && altTitle ? altTitle : title;
644
668
  const displayUrl = relativeUrl || url;
645
669
  let ariaLabel;
@@ -647,6 +671,8 @@ var Title = ({
647
671
  ariaLabel = `Watch video ${displayTitle}`;
648
672
  } else if (props.type === 'audio') {
649
673
  ariaLabel = `Listen to podcast ${displayTitle}`;
674
+ } else if (((_props$indicators = props.indicators) === null || _props$indicators === void 0 ? void 0 : _props$indicators.isOpinion) === true) {
675
+ ariaLabel = `Opinion content. ${displayTitle}`;
650
676
  }
651
677
  return h("div", {
652
678
  className: "o-teaser__heading"
@@ -658,7 +684,7 @@ var Title = ({
658
684
  className: 'js-teaser-heading-link',
659
685
  'aria-label': ariaLabel
660
686
  }
661
- }), showTitlePrefix && titlePrefix && h("span", {
687
+ }), showTitlePrefix && titlePrefix && !sameLabel(props.context, titlePrefix) && h("span", {
662
688
  className: "o-teaser__heading-prefix"
663
689
  }, `${titlePrefix}. `), displayTitle));
664
690
  };
@@ -843,11 +869,11 @@ var presets = {
843
869
  TopStoryLandscape
844
870
  };
845
871
 
846
- const Teaser = props => h(Container, props, h(Content, null, props.showMeta ? h(Meta, props) : null, media(props) === 'video' ? h(Video, props) : null, props.showTitle ? h(Title, props) : null, props.showStandfirst ? h(Standfirst, props) : null, props.showByline ? h(Byline, props) : null, h(Status, props), props.showCustomSlot ? h(CustomSlot, props) : null,
872
+ const Teaser = props => h(Container, props, h(Content, null, props.showCustomSlotTop ? h(CustomSlotTop, props) : null, props.showMeta ? h(Meta, props) : null, media(props) === 'video' ? h(Video, props) : null, props.showTitle ? h(Title, props) : null, props.showStandfirst ? h(Standfirst, props) : null, props.showByline ? h(Byline, props) : null, h(Status, props), props.showCustomSlot ? h(CustomSlot, props) : null,
847
873
  /* Headshot is a legacy element.
848
874
  The Byline component already includes a headshot.
849
875
  Only render the Headshot when the media rule `headshot` is true,
850
876
  which means `showByline` is falsy. */
851
877
  media(props) === 'headshot' ? h(Headshot, props) : null), media(props) === 'promotionalContent' ? h(PromotionalContent, props) : null, media(props) === 'image' ? h(Image, props) : null, props.showRelatedLinks ? h(RelatedLinks, props) : null);
852
878
 
853
- export { Container, Content, CustomSlot, Headshot, Image, Meta, RelatedLinks, Standfirst, Status, Teaser, Title, Video, presets };
879
+ export { Byline, Container, Content, CustomSlot, Headshot, Image, Meta, RelatedLinks, Standfirst, Status, Teaser, Title, Video, presets };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/x-teaser",
3
- "version": "20.0.0-beta.1",
3
+ "version": "20.0.0-beta.3",
4
4
  "description": "This module provides templates for use with o-teaser. Teasers are used to present content.",
5
5
  "source": "src/Teaser.jsx",
6
6
  "main": "dist/Teaser.cjs.js",
@@ -18,7 +18,7 @@
18
18
  "author": "",
19
19
  "license": "ISC",
20
20
  "dependencies": {
21
- "@financial-times/x-engine": "^20.0.0-beta.1",
21
+ "@financial-times/x-engine": "^20.0.0-beta.3",
22
22
  "dateformat": "^3.0.3"
23
23
  },
24
24
  "devDependencies": {
@@ -0,0 +1,17 @@
1
+ import { h } from '@financial-times/x-engine'
2
+
3
+ /**
4
+ * Render
5
+ * @param {String|ReactElement} slot
6
+ * @returns {ReactElement}
7
+ */
8
+ const render = (slot) => {
9
+ // Allow parent components to pass raw HTML strings
10
+ if (typeof slot === 'string') {
11
+ return <div className="x-teaser-custom-slot-top" dangerouslySetInnerHTML={{ __html: slot }} />
12
+ } else {
13
+ return slot
14
+ }
15
+ }
16
+
17
+ export default ({ customSlotTop }) => (customSlotTop ? render(customSlotTop) : null)
package/src/MetaLink.jsx CHANGED
@@ -1,8 +1,5 @@
1
1
  import { h } from '@financial-times/x-engine'
2
-
3
- const sameLabel = (context = {}, label) => {
4
- return label && context && context.parentLabel && label === context.parentLabel
5
- }
2
+ import sameLabel from './concerns/same-label'
6
3
 
7
4
  const isLegacyMode = (metaLinks) => !Array.isArray(metaLinks)
8
5
 
package/src/Teaser.jsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import { h } from '@financial-times/x-engine'
2
2
  import Container from './Container'
3
3
  import Content from './Content'
4
+ import CustomSlotTop from './CustomSlotTop'
4
5
  import CustomSlot from './CustomSlot'
5
6
  import Byline from './Byline'
6
7
  import Headshot from './Headshot'
@@ -18,6 +19,7 @@ import presets from './concerns/presets'
18
19
  const Teaser = (props) => (
19
20
  <Container {...props}>
20
21
  <Content>
22
+ {props.showCustomSlotTop ? <CustomSlotTop {...props} /> : null}
21
23
  {props.showMeta ? <Meta {...props} /> : null}
22
24
  {media(props) === 'video' ? <Video {...props} /> : null}
23
25
  {props.showTitle ? <Title {...props} /> : null}
@@ -46,6 +48,7 @@ export {
46
48
  Headshot,
47
49
  Image,
48
50
  Meta,
51
+ Byline,
49
52
  RelatedLinks,
50
53
  Standfirst,
51
54
  Status,
package/src/Title.jsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import { h } from '@financial-times/x-engine'
2
2
  import Link from './Link'
3
+ import sameLabel from './concerns/same-label'
3
4
 
4
5
  export default ({
5
6
  title,
@@ -19,6 +20,8 @@ export default ({
19
20
  ariaLabel = `Watch video ${displayTitle}`
20
21
  } else if (props.type === 'audio') {
21
22
  ariaLabel = `Listen to podcast ${displayTitle}`
23
+ } else if (props.indicators?.isOpinion === true) {
24
+ ariaLabel = `Opinion content. ${displayTitle}`
22
25
  }
23
26
 
24
27
  return (
@@ -33,7 +36,7 @@ export default ({
33
36
  'aria-label': ariaLabel
34
37
  }}
35
38
  >
36
- {showTitlePrefix && titlePrefix && (
39
+ {showTitlePrefix && titlePrefix && !sameLabel(props.context, titlePrefix) && (
37
40
  <span className="o-teaser__heading-prefix">{`${titlePrefix}. `}</span>
38
41
  )}
39
42
  {displayTitle}
@@ -0,0 +1,3 @@
1
+ export default function sameLabel(context = {}, label) {
2
+ return Boolean(label && label === context?.parentLabel)
3
+ }