Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multi byline element #12496

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6a0624f
Add foundation for multi-byline element, based on mini-profiles
rhystmills Sep 25, 2024
2d23d41
Add Story for multi byline element and update model to include byline…
rhystmills Sep 25, 2024
88667b1
Show a byline in the multi-byline element
rhystmills Sep 25, 2024
3f25893
Improve styles on Multi byline element
rhystmills Sep 25, 2024
48839af
Push pending changes
rhystmills Oct 3, 2024
beffe81
fix import errors following https://github.com/guardian/dotcom-render…
rebecca-thompson Oct 28, 2024
2c8c91b
fix eslint errors
rebecca-thompson Oct 28, 2024
2ec4ce4
fix stylelint errors
rebecca-thompson Oct 29, 2024
dd6ce90
style contributor names that don't have a corresponding tag
rebecca-thompson Oct 30, 2024
0363e3e
add stories for opinion and culture pillars
rebecca-thompson Oct 30, 2024
b4e2a11
byline image styling
rebecca-thompson Oct 31, 2024
e14cc5c
add multi-bylines to DCR model
rebecca-thompson Oct 31, 2024
25ee54e
retrieve image url from contributor tag
rebecca-thompson Oct 31, 2024
3fa6d96
run 'make gen-schema'
rebecca-thompson Oct 31, 2024
44cc1d9
add end note to component
rebecca-thompson Nov 1, 2024
0902502
replace img tag with Avatar component
rebecca-thompson Nov 1, 2024
a351002
update bottom margin to match designs
rebecca-thompson Nov 1, 2024
16fb6d8
Merge branch 'main' into rm/add-multi-byline
rebecca-thompson Nov 1, 2024
86c292e
fix css linting
rebecca-thompson Nov 1, 2024
e1fd0ae
filter out audio designs in stories
rebecca-thompson Nov 1, 2024
2c78818
tidy up labs-specific styling and remove `!important` to keep linter …
rebecca-thompson Nov 1, 2024
9ba24e4
make stories shorter to fix chromatic build
rebecca-thompson Nov 1, 2024
01ae519
break up design stories to fix chromatic build
rebecca-thompson Nov 1, 2024
2d1f11f
Merge branch 'main' into rm/add-multi-byline
rebecca-thompson Nov 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
363 changes: 363 additions & 0 deletions dotcom-rendering/src/components/MultiByline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,363 @@
import { css } from '@emotion/react';
import {
from,
headlineLightItalic24,
headlineLightItalic28,
headlineLightItalic34,
headlineMediumItalic24,
headlineMediumItalic28,
neutral,
space,
textSans14,
textSans24,
textSans28,
textSans34,
textSansItalic17,
} from '@guardian/source/foundations';
import sanitise, { defaults } from 'sanitize-html';
import {
ArticleDesign,
ArticleDisplay,
type ArticleFormat,
ArticleSpecial,
} from '../lib/articleFormat';
import { slugify } from '../model/enhance-H2s';
import { palette } from '../palette';
import type { MultiByline as MultiBylineModel } from '../types/content';
import type { TagType } from '../types/tag';
import { subheadingStyles } from './Subheading';

const multiBylineItemStyles = css`
padding-top: 8px;
`;

const labsBylineStyles = css`
${textSansItalic17};
line-height: 1.4;
`;

const headingLineStyles = css`
width: 140px;
margin: 8px 0 2px 0;
border: none;
border-top: 4px solid ${palette('--heading-line')};
`;

/** Nesting is necessary in the bio styles because we receive a string of html from the
* field. This can contain the following tags:
* Blocks: p, ul, li
* Inline: strong, em, a
*/
const bioStyles = css`
${textSans14};
padding: ${space[1]}px 0;
color: ${palette('--mini-profiles-text-subdued')};
p {
margin-bottom: ${space[2]}px;
}
a {
color: ${palette('--link-kicker-text')};
text-underline-offset: 3px;
}
a:not(:hover) {
text-decoration-color: ${neutral[86]};
}
a:hover {
text-decoration: underline;
}
ul {
list-style: none;
margin: 0 0 ${space[2]}px;
padding: 0;
}
ul li {
padding-left: ${space[5]}px;
}
ul li p {
display: inline-block;
margin-bottom: 0;
}
ul li:before {
display: inline-block;
content: '';
border-radius: 0.375rem;
height: 10px;
width: 10px;
margin: 0 ${space[2]}px 0 -${space[5]}px;
background-color: ${palette('--bullet-fill')};
}
strong {
font-weight: bold;
}
`;

const bottomBorderStyles = css`
border-top: 1px solid ${palette('--article-border')};
margin-bottom: ${space[2]}px;
`;

const headingMarginStyle = css`
margin-bottom: ${space[2]}px;
`;

export const nonAnchorHeadlineStyles = ({
format,
fontWeight,
}: {
format: ArticleFormat;
fontWeight: 'light' | 'medium' | 'bold';
}) => css`
${format.display === ArticleDisplay.Immersive
? headlineLightItalic28
: `
/**
* Typography preset styles should not be overridden.
* This has been done because the styles do not directly map to the new presets.
* Please speak to your team's designer and update this to use a more appropriate preset.
*/
${fontWeight === 'medium' ? headlineMediumItalic24 : headlineLightItalic24};
`};

${from.tablet} {
${format.display === ArticleDisplay.Immersive
? headlineLightItalic34
: `
/**
* Typography preset styles should not be overridden.
* This has been done because the styles do not directly map to the new presets.
* Please speak to your team's designer and update this to use a more appropriate preset.
*/
${fontWeight === 'medium' ? headlineMediumItalic28 : headlineLightItalic28};
`};
}

/** Labs uses sans text */
${format.theme === ArticleSpecial.Labs &&
css`
${format.display === ArticleDisplay.Immersive
? `
/**
* Typography preset styles should not be overridden.
* This has been done because the styles do not directly map to the new presets.
* Please speak to your team's designer and update this to use a more appropriate preset.
*/
${textSans28};
font-weight: 300;
line-height: 1.15;
`
: `
/**
* Typography preset styles should not be overridden.
* This has been done because the styles do not directly map to the new presets.
* Please speak to your team's designer and update this to use a more appropriate preset.
*/
${textSans24};
${
fontWeight === 'light'
? 'font-weight: 300;'
: fontWeight === 'medium'
? 'font-weight: 500;'
: 'font-weight: 700;'
};
line-height: 1.15;
`};

${from.tablet} {
${format.display === ArticleDisplay.Immersive
? `
/**
* Typography preset styles should not be overridden.
* This has been done because the styles do not directly map to the new presets.
* Please speak to your team's designer and update this to use a more appropriate preset.
*/
${textSans34};
font-weight: 300;
line-height: 1.15;
`
: `
/**
* Typography preset styles should not be overridden.
* This has been done because the styles do not directly map to the new presets.
* Please speak to your team's designer and update this to use a more appropriate preset.
*/
${textSans28};
${
fontWeight === 'light'
? 'font-weight: 300;'
: fontWeight === 'medium'
? 'font-weight: 500;'
: 'font-weight: 700;'
};
line-height: 1.15;
`};
}
`}

color: ${palette('--subheading-text')};

/* We don't allow additional font weight inside h2 tags except for immersive articles */
strong {
font-weight: ${format.display === ArticleDisplay.Immersive
? 'bold'
: 'inherit'};
}
`;

export const multiBylineBylineStyles = (format: ArticleFormat) => css`
${nonAnchorHeadlineStyles({ format, fontWeight: 'light' })}
padding-bottom: 8px;
/* stylelint-disable-next-line declaration-no-important */
font-style: italic !important;
margin-top: -4px;
/* stylelint-disable-next-line declaration-no-important */
font-weight: 300 !important;
color: ${neutral[46]};
a {
${subheadingStyles(format)}
color: ${palette('--link-kicker-text')};
text-decoration: none;
font-style: normal;
:hover {
text-decoration: underline;
}
}
span[data-contributor-rel='author'] {
${subheadingStyles(format)}
color: ${neutral[46]};
font-style: normal;
}
`;

const bylineWrapperStyles = css`
display: flex;
width: 100%;
overflow: hidden;
border-bottom: 1px solid ${palette('--article-border')};
margin-bottom: ${space[2]}px;
`;

const bylineTextStyles = css`
flex-grow: 1;
`;

const bylineImageStyles = css`
width: 80px;
border-radius: 50%;
margin-left: 10px;
margin-bottom: -8px;
height: 80px;
min-width: 80px;
overflow: hidden;
align-self: flex-end;
background-color: ${palette('--multi-byline-avatar-background')};
${from.tablet} {
height: 120px;
min-width: 120px;
width: 120px;
margin-bottom: -12px;
}
`;

interface MultiBylineItemProps {
multiByline: MultiBylineModel;
format: ArticleFormat;
tags: TagType[];
children: React.ReactNode;
}

export const MultiByline = ({
multiByline,
format,
tags,
children,
}: MultiBylineItemProps) => {
return (
<>
<li css={multiBylineItemStyles} data-spacefinder-role="nested">
<Byline
title={multiByline.title}
byline={multiByline.byline ?? ''}
bylineHtml={multiByline.bylineHtml ?? ''}
contributorIds={multiByline.contributorIds ?? []}
imageOverrideUrl={multiByline.imageOverrideUrl}
format={format}
tags={tags}
/>
<Bio html={multiByline.bio} />
{children}
</li>
</>
);
};

type BylineProps = {
title: string;
bylineHtml: string;
byline: string;
imageOverrideUrl?: string;
contributorIds: string[];
format: ArticleFormat;
tags: TagType[];
};

const Byline = ({
title,
bylineHtml,
byline,
imageOverrideUrl,
contributorIds,
format,
tags,
}: BylineProps) => {
const sanitizedHtml = sanitise(bylineHtml, {
allowedAttributes: {
...defaults.allowedAttributes,
span: ['data-contributor-rel'],
},
});
const imageUrl =
imageOverrideUrl ??
tags.find((tag) => tag.id === contributorIds[0])?.bylineImageUrl;

return (
<div css={bylineWrapperStyles}>
<div css={bylineTextStyles}>
<hr css={headingLineStyles} />
<h3
id={slugify(title)}
css={[subheadingStyles(format), headingMarginStyle]}
>
{title}
</h3>
{bylineHtml ? (
<h3
css={[
multiBylineBylineStyles(format),
format.theme === ArticleSpecial.Labs &&
labsBylineStyles,
format.design === ArticleDesign.LiveBlog,
]}
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
/>
) : null}
</div>
{!!imageUrl && (
<img src={imageUrl} alt={byline} css={bylineImageStyles}></img>
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
)}
</div>
);
};

const Bio = ({ html }: { html?: string }) => {
if (!html) return null;
const sanitizedHtml = sanitise(html, {});
return (
<>
<div
css={bioStyles}
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
/>
<div css={bottomBorderStyles} />
</>
);
};
Loading
Loading