Skip to content

Commit a2bc1d7

Browse files
author
Juarez Mota
committed
Merge branch 'jm/feat-focus-designable-banner-when-visible' of github.com:guardian/dotcom-rendering into jm/feat-focus-designable-banner-when-visible
2 parents 53b44e4 + a10ac2f commit a2bc1d7

15 files changed

+210
-68
lines changed

dotcom-rendering/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"@emotion/server": "11.11.0",
2929
"@guardian/ab-core": "8.0.0",
3030
"@guardian/braze-components": "22.2.0",
31-
"@guardian/bridget": "8.5.1",
31+
"@guardian/bridget": "8.6.0",
3232
"@guardian/browserslist-config": "6.1.0",
3333
"@guardian/cdk": "61.4.0",
3434
"@guardian/commercial-core": "27.1.0",

dotcom-rendering/src/components/ArticleMeta.apps.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ export const ArticleMetaApps = ({
244244
const isAnalysis = format.design === ArticleDesign.Analysis;
245245
const isLiveBlog = format.design === ArticleDesign.LiveBlog;
246246
const isGallery = format.design === ArticleDesign.Gallery;
247+
const isVideo = format.design === ArticleDesign.Video;
247248

248249
const shouldShowFollowButtons = (layoutOrDesignType: boolean) =>
249250
layoutOrDesignType && !!byline && !isUndefined(soleContributor);
@@ -254,6 +255,9 @@ export const ArticleMetaApps = ({
254255
const isImmersiveOrAnalysisWithMultipleAuthors =
255256
(isAnalysis || isImmersive) && !!byline && isUndefined(soleContributor);
256257

258+
const shouldShowListenToArticleButton =
259+
!!pageId && !(isLiveBlog || isPicture || isGallery || isVideo);
260+
257261
return (
258262
<div
259263
className={
@@ -365,7 +369,7 @@ export const ArticleMetaApps = ({
365369
</MetaGridBranding>
366370
)}
367371
</div>
368-
{pageId !== undefined && (
372+
{shouldShowListenToArticleButton && (
369373
<Island priority="feature" defer={{ until: 'visible' }}>
370374
<ListenToArticle articleId={pageId} />
371375
</Island>

dotcom-rendering/src/components/ListenToArticle.importable.tsx

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,53 @@ import { ListenToArticleButton } from './ListenToArticleButton';
77
type Props = {
88
articleId: string;
99
};
10+
11+
export const formatAudioDuration = (
12+
durationInSeconds: number,
13+
): string | undefined => {
14+
if (durationInSeconds >= 3600 || durationInSeconds <= 0) {
15+
return undefined;
16+
}
17+
const minutes = Math.floor((durationInSeconds % 3600) / 60);
18+
const seconds = durationInSeconds % 60;
19+
20+
const formattedDuration = `${minutes.toString()}:${seconds
21+
.toString()
22+
.padStart(2, '0')}`;
23+
24+
return formattedDuration;
25+
};
26+
1027
export const ListenToArticle = ({ articleId }: Props) => {
1128
const [showButton, setShowButton] = useState<boolean>(false);
29+
const [audioDurationSeconds, setAudioDurationSeconds] = useState<
30+
number | undefined
31+
>(undefined);
1232

13-
const isBridgetCompatible = useIsBridgetCompatible('8.5.1');
14-
33+
const isBridgetCompatible = useIsBridgetCompatible('8.6.0');
1534
useEffect(() => {
1635
if (isBridgetCompatible) {
1736
Promise.all([
1837
getListenToArticleClient().isAvailable(articleId),
1938
getListenToArticleClient().isPlaying(articleId),
39+
getListenToArticleClient().getAudioDurationSeconds(articleId),
2040
])
21-
.then(() =>
22-
// setShowButton(isAvailable && !isPlaying),
23-
setShowButton(false),
24-
)
41+
.then(() => {
42+
// TODO pending design implementation and AB test set up.
43+
// .then(({ isAvailable, isPlaying, audioDurationSeconds }) => {
44+
// setAudioDuration(
45+
// audioDurationSeconds ? audioDurationSeconds : undefined,
46+
// );
47+
// setShowButton(isAvailable && !isPlaying);
48+
setAudioDurationSeconds(undefined);
49+
setShowButton(false);
50+
})
2551
.catch((error) => {
2652
console.error(
2753
'Error fetching article audio status: ',
2854
error,
2955
);
56+
3057
setShowButton(false);
3158
});
3259
}
@@ -54,7 +81,14 @@ export const ListenToArticle = ({ articleId }: Props) => {
5481
};
5582
return (
5683
showButton && (
57-
<ListenToArticleButton onClickHandler={listenToArticleHandler} />
84+
<ListenToArticleButton
85+
onClickHandler={listenToArticleHandler}
86+
audioDuration={
87+
audioDurationSeconds !== undefined
88+
? formatAudioDuration(audioDurationSeconds)
89+
: undefined
90+
}
91+
/>
5892
)
5993
);
6094
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { formatAudioDuration } from './ListenToArticle.importable';
2+
3+
describe('format Audio Duration correctly', () => {
4+
const testCases = [
5+
{ input: -60, expected: undefined },
6+
{ input: 0, expected: undefined },
7+
{ input: 1, expected: '0:01' },
8+
{ input: 59, expected: '0:59' },
9+
{ input: 60, expected: '1:00' },
10+
{ input: 61, expected: '1:01' },
11+
{ input: 3599, expected: '59:59' },
12+
{ input: 3600, expected: undefined },
13+
];
14+
15+
for (const { input, expected } of testCases) {
16+
it(`correctly formats ${input} as ${expected}`, () => {
17+
expect(formatAudioDuration(input)).toEqual(expected);
18+
});
19+
}
20+
});

dotcom-rendering/src/components/ListenToArticleButton.stories.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,16 @@ export default meta;
1010

1111
type Story = StoryObj<typeof meta>;
1212

13-
export const ListenToArticleButton = {
13+
export const ListenToArticleWithDurationButton = {
1414
args: {
1515
onClickHandler: () => undefined,
16+
audioDuration: '3:02',
17+
},
18+
} satisfies Story;
19+
20+
export const ListenToArticleNoDurationButton = {
21+
args: {
22+
onClickHandler: () => undefined,
23+
audioDuration: undefined,
1624
},
1725
} satisfies Story;
Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,73 @@
11
import { css } from '@emotion/react';
2-
import { space } from '@guardian/source/foundations';
2+
import { height, space } from '@guardian/source/foundations';
33
import {
44
Button,
55
SvgMediaControlsPlay,
66
} from '@guardian/source/react-components';
77
import { palette } from '../palette';
88

9-
const button = css`
9+
const buttonCss = (audioDuration: string | undefined) => css`
10+
display: flex;
11+
align-items: center;
1012
background-color: ${palette('--follow-icon-fill')};
13+
color: ${palette('--follow-icon-background')};
1114
&:active,
1215
&:focus,
1316
&:hover {
1417
background-color: ${palette('--follow-icon-fill')};
1518
}
16-
color: ${palette('--follow-icon-background')};
1719
margin-bottom: ${space[4]}px;
1820
margin-left: ${space[2]}px;
21+
padding-left: ${space[2]}px;
22+
padding-right: ${audioDuration === undefined ? space[4] : space[3]}px;
23+
padding-bottom: 0px;
24+
font-size: 15px;
25+
height: ${height.ctaSmall}px;
26+
min-height: ${height.ctaSmall}px;
27+
28+
.src-button-space {
29+
width: 0px;
30+
}
31+
`;
32+
33+
const dividerCss = css`
34+
width: 0.5px;
35+
height: 100%;
36+
opacity: 0.5;
37+
border-left: 1px solid ${palette('--follow-icon-background')};
38+
margin-left: ${space[2]}px;
39+
margin-right: ${space[2]}px;
40+
`;
41+
42+
const durationCss = css`
43+
font-weight: 300;
1944
`;
45+
46+
const baselineCss = css`
47+
padding-bottom: 2px;
48+
`;
49+
2050
type ButtonProps = {
2151
onClickHandler: () => void;
52+
audioDuration?: string;
2253
};
23-
export const ListenToArticleButton = ({ onClickHandler }: ButtonProps) => (
54+
export const ListenToArticleButton = ({
55+
onClickHandler,
56+
audioDuration,
57+
}: ButtonProps) => (
2458
<Button
2559
onClick={onClickHandler}
26-
size="small"
27-
cssOverrides={button}
60+
size="default"
61+
iconSide="left"
62+
cssOverrides={buttonCss(audioDuration)}
2863
icon={<SvgMediaControlsPlay />}
2964
>
30-
Listen to article
65+
{audioDuration === undefined || audioDuration === '' ? null : (
66+
<>
67+
<span css={[durationCss, baselineCss]}>{audioDuration}</span>
68+
<span css={dividerCss}></span>
69+
</>
70+
)}
71+
<span css={baselineCss}>Listen to this article</span>
3172
</Button>
3273
);

dotcom-rendering/src/devServer/docs/article.tsx

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import {
2+
ArticleDesign,
3+
ArticleDisplay,
4+
ArticleSpecial,
5+
Pillar,
6+
} from '../../lib/articleFormat';
17
import { Available } from './available';
28
import { descriptionLinks } from './styles';
39

@@ -89,6 +95,28 @@ const Examples = () => (
8995
</>
9096
);
9197

98+
/**
99+
* Joins all the `string` values of an enum into a single, comma-separated
100+
* `string`.
101+
*
102+
* `ArticleDesign`, `ArticleDisplay` and the two variants of `ArticleTheme` are
103+
* TypeScript enums. Their values contain both the `string` and `number`
104+
* representations of the enum, but here we just want the `string`s, so we
105+
* filter out the `number`s.
106+
*
107+
* https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings
108+
*/
109+
const formatValues = (
110+
a:
111+
| typeof ArticleDesign
112+
| typeof ArticleDisplay
113+
| typeof Pillar
114+
| typeof ArticleSpecial,
115+
): string =>
116+
Object.values(a)
117+
.filter((v) => typeof v !== 'number')
118+
.join(', ');
119+
92120
const Format = () => (
93121
<>
94122
<h2>Format</h2>
@@ -103,23 +131,21 @@ const Format = () => (
103131
<dd>
104132
Primarily influences the content, structure and features of an
105133
article. It's the most important of the three, and is often used
106-
as a shorthand to describe the "kind" of article. Examples
107-
include <code>Liveblog</code>, <code>Feature</code> and{' '}
108-
<code>Gallery</code>.
134+
as a shorthand to describe the "kind" of article. The values
135+
are: {formatValues(ArticleDesign)}.
109136
</dd>
110137
<dt>Display</dt>
111138
<dd>
112-
Primarily influences the layout of an article. Examples include{' '}
113-
<code>Immersive</code>, <code>Showcase</code> and{' '}
114-
<code>NumberedList</code>.
139+
Primarily influences the layout of an article. The values are:{' '}
140+
{formatValues(ArticleDisplay)}.
115141
</dd>
116142
<dt>Theme</dt>
117143
<dd>
118144
Primarily influences the fonts and colours of an article. It can
119145
can be thought of as a superset of "pillar", i.e. all the
120146
pillars are considered themes, but there are some additional
121-
themes that are not pillars. Examples include <code>News</code>,{' '}
122-
<code>Sport</code> and <code>Labs</code>.
147+
themes that are not pillars. The values are:{' '}
148+
{formatValues(Pillar)}, {formatValues(ArticleSpecial)}.
123149
</dd>
124150
</dl>
125151
<p>

dotcom-rendering/src/devServer/routers/amp.ts renamed to dotcom-rendering/src/devServer/routers/amp.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { sendReact } from '../send';
66

77
const amp = express.Router();
88

9-
amp.get('/', sendReact('AMP', Amp));
10-
amp.get('/article', sendReact('Article', Article));
11-
amp.get('/interactive', sendReact('Interactive', Interactive));
9+
amp.get('/', sendReact('AMP', <Amp />));
10+
amp.get('/article', sendReact('Article', <Article />));
11+
amp.get('/interactive', sendReact('Interactive', <Interactive />));
1212

1313
export { amp };

dotcom-rendering/src/devServer/routers/dotcom.ts renamed to dotcom-rendering/src/devServer/routers/dotcom.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,32 @@ import { sendReact } from '../send';
1515

1616
const dotcom = Router();
1717

18-
dotcom.get('/', sendReact('Dotcom', Dotcom));
19-
dotcom.get('/article', sendReact('Article', Article));
20-
dotcom.get('/front', sendReact('Front', Front));
21-
dotcom.get('/tag-page', sendReact('Tag Page', TagPage));
22-
dotcom.get('/interactive', sendReact('Interactive', Interactive));
23-
dotcom.get('/newsletters', sendReact('All Newsletters', Newsletters));
24-
dotcom.get('/football-live', sendReact('Football Live', FootballLive));
18+
dotcom.get('/', sendReact('Dotcom', <Dotcom />));
19+
dotcom.get('/article', sendReact('Article', <Article />));
20+
dotcom.get('/front', sendReact('Front', <Front />));
21+
dotcom.get('/tag-page', sendReact('Tag Page', <TagPage />));
22+
dotcom.get('/interactive', sendReact('Interactive', <Interactive />));
23+
dotcom.get('/newsletters', sendReact('All Newsletters', <Newsletters />));
24+
dotcom.get('/football-live', sendReact('Football Live', <FootballLive />));
2525
dotcom.get(
2626
'/football-fixtures',
27-
sendReact('Football Fixtures', FootballFixtures),
27+
sendReact('Football Fixtures', <FootballFixtures />),
28+
);
29+
dotcom.get(
30+
'/football-results',
31+
sendReact('Football Results', <FootballResults />),
32+
);
33+
dotcom.get(
34+
'/football-tables',
35+
sendReact('Football Tables', <FootballTables />),
2836
);
29-
dotcom.get('/football-results', sendReact('Football Results', FootballResults));
30-
dotcom.get('/football-tables', sendReact('Football Tables', FootballTables));
3137
dotcom.get(
3238
'/football-match-summary',
33-
sendReact('Football Match Summary', FootballMatchSummary),
39+
sendReact('Football Match Summary', <FootballMatchSummary />),
3440
);
3541
dotcom.get(
3642
'/cricket-scorecard',
37-
sendReact('Cricket Scorecard', CricketScorecard),
43+
sendReact('Cricket Scorecard', <CricketScorecard />),
3844
);
3945

4046
export { dotcom };

dotcom-rendering/src/devServer/routers/editionsApp.ts renamed to dotcom-rendering/src/devServer/routers/editionsApp.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { sendReact } from '../send';
55

66
const editionsApp = express.Router();
77

8-
editionsApp.get('/', sendReact('Editions App', EditionsApp));
8+
editionsApp.get('/', sendReact('Editions App', <EditionsApp />));
99
editionsApp.get(
1010
'/crosswords',
11-
sendReact('Editions Crosswords', EditionsCrosswords),
11+
sendReact('Editions Crosswords', <EditionsCrosswords />),
1212
);
1313

1414
export { editionsApp };

0 commit comments

Comments
 (0)