Skip to content

Commit 1e129aa

Browse files
authored
Merge pull request #2356 from Financial-Times/CULT-747
CULT-747: Opinion teaser improvements
2 parents 3a158a3 + cea2518 commit 1e129aa

File tree

21 files changed

+291
-5
lines changed

21 files changed

+291
-5
lines changed

components/o-teaser/MIGRATION.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,61 @@
11
# Migration guide
22

3+
### Migrating to v10.0.0
4+
5+
v10.0.0 introduces:
6+
7+
* **Byline** component, which replaces the legacy Headshot component
8+
* **title indicator icon** for only Opinion teaser
9+
* **titlePrefix**
10+
11+
#### Byline
12+
13+
The Byline component is enabled by default (`showByline: true`) in the presets.
14+
When `byline` data is provided, the Byline component will be rendered instead of Headshot.
15+
16+
If you are not ready to use the Byline component, pass:
17+
18+
```js
19+
showByline: false
20+
```
21+
22+
This keeps the existing Headshot behaviour.
23+
24+
If you are migrating to the Byline component, you must provide a `byline` property:
25+
26+
| Property name | Type | Note |
27+
| ------------- | ------------------------------ | ---------------------------------- |
28+
| `byline` | `[string, string?, string?][]` | `[text, linkUrl?, headshotUrl?][]` |
29+
30+
Example with single author with headshot:
31+
32+
```js
33+
byline: [
34+
['Martin Wolf', '/martin-wolf', '/martin-wolf-headshot']
35+
]
36+
```
37+
38+
Example with multiple authors:
39+
40+
```js
41+
// This will render: Martin Wolf & Paul Krugman
42+
byline: [
43+
['Martin Wolf', '/martin-wolf'],
44+
[' & '],
45+
['Paul Krugman', '/paul-krugman']
46+
]
47+
```
48+
49+
#### titlePrefix
50+
51+
`titlePrefix` is enabled by default (`showTitlePrefix: true`) in the presets.
52+
Nothing will be rendered unless a `titlePrefix` string is provided.
53+
54+
| Property name | Type | Note |
55+
| ------------- | -------- | ------------------------------- |
56+
| `titlePrefix` | `string` | Text displayed before the title |
57+
58+
359
### Migrating to v9.0.0
460

561
v9 upgrades [html-react-parser to v5.2.7](https://github.com/remarkablemark/html-react-parser), which has a dependency on React 19.

components/o-teaser/src/scss/_mixins.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@
105105
.o-teaser__visually-hidden {
106106
@include oPrivateNormaliseVisuallyHidden;
107107
}
108+
109+
.o-teaser__byline {
110+
@include _oTeaserByline;
111+
}
108112
}
109113

110114
@mixin _oTeaserElementsImages {

components/o-teaser/src/scss/elements/_default.scss

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@
1515
color: $_o-teaser-base-color;
1616
margin-top: 0;
1717
margin-bottom: 0;
18+
19+
&:before {
20+
display: none;
21+
mask-repeat: no-repeat;
22+
mask-size: contain;
23+
vertical-align: text-bottom;
24+
content: '';
25+
margin-right: 0.2em;
26+
width: 1em;
27+
height: 1em;
28+
}
29+
30+
.o-teaser__heading-prefix {
31+
font-family: inherit;
32+
font-size: inherit;
33+
font-weight: oPrivateFoundationGet('o3-font-weight-semibold');
34+
line-height: inherit;
35+
color: inherit;
36+
}
1837
}
1938

2039
/// Base styles for a teaser
@@ -64,6 +83,42 @@
6483
}
6584
}
6685

86+
@mixin _oTeaserByline {
87+
@include _oTeaserLink();
88+
a:focus,
89+
a:hover {
90+
color: $_o-teaser-focus-hover;
91+
}
92+
a:visited {
93+
color: $_o-teaser-base-color;
94+
}
95+
font-family: oPrivateFoundationGet('o3-font-family-metric');
96+
font-size: oPrivateFoundationGet('o3-font-size-metric2-0');
97+
font-weight: oPrivateFoundationGet('o3-font-weight-regular');
98+
line-height: oPrivateFoundationGet('o3-font-lineheight-2');
99+
color: $_o-teaser-base-color;
100+
margin-top: oPrivateSpacingByName('s3');
101+
margin-bottom: 0;
102+
display: flex;
103+
align-items: center;
104+
flex-wrap: wrap;
105+
gap: oPrivateSpacingByName('s1');
106+
107+
.o-teaser__byline-link {
108+
display: flex;
109+
align-items: center;
110+
111+
img {
112+
width: 40px;
113+
height: 40px;
114+
border-radius: 50%;
115+
border: 1px solid oPrivateFoundationGet('o3-color-palette-black-20');
116+
background-color: oPrivateFoundationGet('o3-color-palette-black-10');
117+
margin-right: oPrivateSpacingByName('s3');
118+
}
119+
}
120+
}
121+
67122
/// Links within title or standfirst
68123
@mixin _oTeaserLink {
69124
a {

components/o-teaser/src/scss/themes/_hero.scss

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
.o-teaser__heading a:focus,
3838
.o-teaser__heading a:visited,
3939
.o-teaser__standfirst a:visited,
40+
.o-teaser__byline a:hover,
41+
.o-teaser__byline a:focus,
4042
.o-teaser__tag:hover,
4143
.o-teaser__tag:focus {
4244
color: oPrivateColorsMix(
@@ -116,6 +118,8 @@
116118
.o-teaser__heading,
117119
.o-teaser__heading a,
118120
.o-teaser__heading a:visited,
121+
.o-teaser__byline a,
122+
.o-teaser__byline a:visited,
119123
.o-teaser__meta,
120124
.o-teaser__standfirst,
121125
.o-teaser__standfirst a,
@@ -177,6 +181,10 @@
177181
font-weight: oPrivateFoundationGet('o3-type-headline-md-font-weight');
178182
line-height: oPrivateFoundationGet('o3-type-headline-md-line-height');
179183
}
184+
185+
.o-teaser__byline {
186+
justify-self: center;
187+
}
180188
}
181189

182190
/// Centred hero teaser styles to centre image
@@ -213,7 +221,9 @@
213221
$hero-extra-highlight-border: oPrivateFoundationGet('o3-color-palette-lemon');
214222
$hero-extra-highlight-color: oPrivateFoundationGet('o3-color-palette-lemon');
215223
.o-teaser__heading a:hover,
216-
.o-teaser__heading a:focus {
224+
.o-teaser__heading a:focus,
225+
.o-teaser__byline a:hover,
226+
.o-teaser__byline a:focus {
217227
color: $hero-extra-highlight-color;
218228
}
219229

components/o-teaser/src/scss/themes/_live.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
.o-teaser__meta a:hover,
2727
.o-teaser__heading a:focus,
2828
.o-teaser__heading a:hover,
29+
.o-teaser__byline a:focus,
30+
.o-teaser__byline a:hover,
2931
.o-teaser__standfirst a:visited {
3032
color: oPrivateColorsMix('o3-color-palette-white', 'o3-color-palette-crimson', 90);
3133
outline-color: currentColor;

components/o-teaser/src/scss/themes/_standard.scss

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
.o-teaser__heading a:hover,
77
.o-teaser__heading a:focus,
88
.o-teaser__heading a:visited,
9+
.o-teaser__byline a:hover,
10+
.o-teaser__byline a:focus,
911
.o-teaser__standfirst a:visited,
1012
.o-teaser__tag:hover,
1113
.o-teaser__tag:focus {
@@ -17,6 +19,8 @@
1719
}
1820

1921
.o-teaser__meta,
22+
.o-teaser__byline,
23+
.o-teaser__byline a:visited,
2024
.o-teaser__tag-suffix,
2125
.o-teaser__heading {
2226
color: oPrivateFoundationGet('o3-color-palette-white');
@@ -53,6 +57,23 @@
5357
.o-teaser__meta {
5458
color: oPrivateFoundationGet('o3-color-palette-oxford');
5559
}
60+
61+
.o-teaser__heading {
62+
&:before {
63+
display: inline-block;
64+
background-color: oPrivateFoundationGet('o3-color-palette-oxford');
65+
mask-image: oPrivateFoundationGet(o3-icon-quote-left);
66+
width: 1.1em;
67+
height: 1.1em;
68+
}
69+
}
70+
71+
&.o-teaser--inverse,
72+
&.o-teaser--hero {
73+
.o-teaser__heading:before {
74+
background-color: oPrivateFoundationGet('o3-color-palette-white');
75+
}
76+
}
5677
}
5778

5879
/// Opinion background theme - colours background blue and adjust text
@@ -100,6 +121,8 @@
100121
.o-teaser__heading a:hover,
101122
.o-teaser__heading a:focus,
102123
.o-teaser__heading a:visited,
124+
.o-teaser__byline a:hover,
125+
.o-teaser__byline a:focus,
103126
.o-teaser__standfirst a:visited,
104127
.o-teaser__tag:hover,
105128
.o-teaser__tag:focus {

components/o-teaser/src/tsx/Props.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,20 @@ export type ImageSize =
3737
export interface Features {
3838
showMeta?: boolean;
3939
showTitle?: boolean;
40+
showTitlePrefix?: boolean;
4041
showStandfirst?: boolean;
4142
showStatus?: boolean;
4243
showImage?: boolean;
4344
/**
4445
* Takes precedence over image
4546
*/
4647
showHeadshot?: boolean;
48+
/**
49+
* Takes precedence over legacy headshot.
50+
* byline component includes headshot
51+
*/
52+
showByline?: boolean;
53+
showBylineHeadshot?: boolean;
4754
/**
4855
* Takes precedence over image or headshot
4956
*/
@@ -80,6 +87,7 @@ export interface Title {
8087
title: string;
8188
/** Used for testing headline variations */
8289
altTitle: string;
90+
titlePrefix?: string;
8391
}
8492

8593
export interface Standfirst {
@@ -160,6 +168,12 @@ export interface MetaLink {
160168
prefLabel: string;
161169
}
162170

171+
/** [text, url, headshot][] */
172+
type StructuredByline = [string, string?, string?][];
173+
export interface Byline {
174+
byline?: StructuredByline
175+
}
176+
163177
export interface Link {
164178
/** Content UUID */
165179
id: string;
@@ -204,6 +218,7 @@ export interface TeaserProps
204218
Status,
205219
Image,
206220
Headshot,
221+
Byline,
207222
Video,
208223
RelatedLinks,
209224
Context,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {ImageSizes} from './concerns/constants';
2+
import imageService from './concerns/image-service';
3+
import {Byline, Headshot} from './props';
4+
import Link from './link';
5+
6+
interface BylineProps extends Byline, Headshot {
7+
showBylineHeadshot?: boolean;
8+
}
9+
10+
const BylineLink = ({text, url, headshot, showBylineHeadshot}: {text: string, url: string, headshot?: string, showBylineHeadshot?: boolean}) => (
11+
<Link
12+
url={url}
13+
attrs={{
14+
'data-trackable': 'byline-link',
15+
className: "o-teaser__byline-link"
16+
}}
17+
>
18+
<>
19+
{showBylineHeadshot && headshot ?
20+
<img
21+
className="o-teaser__byline-headshot"
22+
width={ImageSizes.BylineHeadshot}
23+
height={ImageSizes.BylineHeadshot}
24+
alt=""
25+
aria-hidden="true"
26+
src={imageService(headshot, ImageSizes.BylineHeadshot, {})}
27+
srcSet={
28+
`${imageService(headshot, ImageSizes.BylineHeadshot*2, {})} 2x`
29+
}
30+
/>
31+
: null}
32+
<span className="o-teaser__byline-link-label">{text}</span>
33+
</>
34+
</Link>
35+
);
36+
37+
const BylineText = ({text}: {text: string}) => (
38+
text ? <span className="o-teaser__byline-text">{text}</span> : null
39+
);
40+
41+
export default ({byline, showBylineHeadshot}: BylineProps) => (
42+
Array.isArray(byline) && byline.length > 0 ? (
43+
<div className="o-teaser__byline">
44+
{byline.map(([text, url, headshot], index) => (
45+
text && url ?
46+
<BylineLink
47+
key={`byline-link-${index}-${text}`}
48+
text={text}
49+
url={url}
50+
headshot={headshot}
51+
showBylineHeadshot={showBylineHeadshot}
52+
/>
53+
: <BylineText key={`byline-text-${index}-${text}`} text={text} />
54+
))}
55+
</div>
56+
) : null
57+
);

components/o-teaser/src/tsx/concerns/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const ImageSizes = {
22
Headshot: 75,
3+
BylineHeadshot: 40,
34
XS: 180,
45
Small: 240,
56
Medium: 340,

0 commit comments

Comments
 (0)