Skip to content

Commit f912e23

Browse files
Merge pull request #15123 from guardian/full-width-atoms
2 parents c1ba9c9 + 88a0995 commit f912e23

File tree

11 files changed

+162
-8
lines changed

11 files changed

+162
-8
lines changed

dotcom-rendering/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ declare namespace JSX {
9494
'data-spacefinder-role'?:
9595
| 'nested'
9696
| 'immersive'
97+
| 'fullWidth'
9798
| 'inline'
9899
| 'richLink'
99100
| 'thumbnail';

dotcom-rendering/src/components/Figure.tsx

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { css } from '@emotion/react';
2-
import { from, space, until } from '@guardian/source/foundations';
2+
import { breakpoints, from, space, until } from '@guardian/source/foundations';
33
import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat';
44
import type { FEElement, RoleType } from '../types/content';
55

66
type Props = {
77
children: React.ReactNode;
88
format: ArticleFormat;
99
isMainMedia: boolean;
10-
role?: RoleType | 'richLink';
10+
role?: RoleType | 'richLink' | 'fullWidth';
1111
id?: string;
1212
className?: string;
1313
type?: FEElement['_type'];
@@ -70,6 +70,53 @@ const roleCss = {
7070
}
7171
`,
7272

73+
fullWidth: css`
74+
margin-top: ${space[3]}px;
75+
margin-bottom: ${space[3]}px;
76+
77+
${until.tablet} {
78+
margin-left: -20px;
79+
margin-right: -20px;
80+
}
81+
${until.mobileLandscape} {
82+
margin-left: -10px;
83+
margin-right: -10px;
84+
}
85+
${from.tablet} {
86+
--scrollbar-width-fallback: 15px;
87+
--half-scrollbar-width-fallback: 7.5px;
88+
89+
width: calc(
90+
100vw - var(--scrollbar-width, var(--scrollbar-width-fallback))
91+
);
92+
max-width: calc(
93+
100vw - var(--scrollbar-width, var(--scrollbar-width-fallback))
94+
);
95+
96+
--grid-container-max-width: 740px;
97+
--grid-container-left-margin: calc(
98+
((-100vw + (var(--grid-container-max-width) - 42px)) / 2) +
99+
var(
100+
--half-scrollbar-width,
101+
var(--half-scrollbar-width-fallback)
102+
)
103+
);
104+
105+
margin-left: var(--grid-container-left-margin);
106+
}
107+
${from.desktop} {
108+
--grid-container-max-width: ${breakpoints.desktop}px;
109+
}
110+
${from.leftCol} {
111+
--grid-container-max-width: ${breakpoints.leftCol}px;
112+
--grid-left-col-width: 140px;
113+
}
114+
${from.wide} {
115+
--grid-container-max-width: ${breakpoints.wide}px;
116+
--grid-left-col-width: 219px;
117+
}
118+
`,
119+
73120
showcase: css`
74121
margin-top: ${space[3]}px;
75122
margin-bottom: ${space[3]}px;
@@ -150,7 +197,7 @@ const roleCss = {
150197

151198
// Used for vast majority of layouts.
152199
export const defaultRoleStyles = (
153-
role: RoleType | 'richLink',
200+
role: RoleType | 'richLink' | 'fullWidth',
154201
format: ArticleFormat,
155202
isTimeline = false,
156203
) => {
@@ -161,6 +208,8 @@ export const defaultRoleStyles = (
161208
return roleCss.supporting;
162209
case 'immersive':
163210
return roleCss.immersive;
211+
case 'fullWidth':
212+
return roleCss.fullWidth;
164213
case 'showcase':
165214
if (isTimeline) {
166215
return css`
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useEffect } from 'react';
2+
3+
export const InteractivesScrollbarWidth = () => {
4+
useEffect(() => {
5+
const updateScrollbarWidth = () => {
6+
const documentWidth = document.documentElement.clientWidth;
7+
if (documentWidth <= 0) return;
8+
9+
const scrollbarWidth = window.innerWidth - documentWidth;
10+
const root = document.documentElement;
11+
12+
root.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`);
13+
root.style.setProperty(
14+
'--half-scrollbar-width',
15+
`${scrollbarWidth / 2}px`,
16+
);
17+
};
18+
19+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
20+
21+
const debouncedResize = () => {
22+
if (timeoutId) {
23+
clearTimeout(timeoutId);
24+
}
25+
26+
timeoutId = setTimeout(() => {
27+
updateScrollbarWidth();
28+
}, 150);
29+
};
30+
31+
updateScrollbarWidth();
32+
33+
window.addEventListener('resize', debouncedResize);
34+
35+
return () => {
36+
if (timeoutId) {
37+
clearTimeout(timeoutId);
38+
}
39+
window.removeEventListener('resize', debouncedResize);
40+
};
41+
}, []);
42+
43+
return null;
44+
};

dotcom-rendering/src/frontend/schemas/feArticle.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2449,7 +2449,16 @@
24492449
"type": "string"
24502450
},
24512451
"role": {
2452-
"$ref": "#/definitions/RoleType"
2452+
"enum": [
2453+
"fullWidth",
2454+
"halfWidth",
2455+
"immersive",
2456+
"inline",
2457+
"showcase",
2458+
"supporting",
2459+
"thumbnail"
2460+
],
2461+
"type": "string"
24532462
}
24542463
},
24552464
"required": [

dotcom-rendering/src/layouts/InteractiveLayout.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { GridItem } from '../components/GridItem';
2525
import { HeaderAdSlot } from '../components/HeaderAdSlot';
2626
import { InteractivesDisableArticleSwipe } from '../components/InteractivesDisableArticleSwipe.importable';
2727
import { InteractivesNativePlatformWrapper } from '../components/InteractivesNativePlatformWrapper.importable';
28+
import { InteractivesScrollbarWidth } from '../components/InteractivesScrollbarWidth.importable';
2829
import { Island } from '../components/Island';
2930
import { LabsHeader } from '../components/LabsHeader';
3031
import { MainMedia } from '../components/MainMedia';
@@ -45,6 +46,7 @@ import { decideStoryPackageTrails } from '../lib/decideTrail';
4546
import type { NavType } from '../model/extract-nav';
4647
import { palette as themePalette } from '../palette';
4748
import type { ArticleDeprecated } from '../types/article';
49+
import type { RoleType } from '../types/content';
4850
import type { RenderingTarget } from '../types/renderingTarget';
4951
import {
5052
interactiveGlobalStyles,
@@ -220,8 +222,23 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => {
220222

221223
const renderAds = canRenderAds(article);
222224

225+
const includesFullWidthElement = article.blocks.some((block) =>
226+
block.elements.some((element) => {
227+
const role =
228+
'role' in element
229+
? (element.role as RoleType | 'fullWidth' | undefined)
230+
: undefined;
231+
return role === 'fullWidth';
232+
}),
233+
);
234+
223235
return (
224236
<>
237+
{includesFullWidthElement && (
238+
<Island priority="critical">
239+
<InteractivesScrollbarWidth />
240+
</Island>
241+
)}
225242
{isApps && (
226243
<>
227244
<Island priority="critical">

dotcom-rendering/src/layouts/lib/interactiveLegacyStyling.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const isInteractive = (design: ArticleDesign): boolean =>
99

1010
export const interactiveLegacyFigureClasses = (
1111
elementType: string,
12-
role?: RoleType,
12+
role?: RoleType | 'fullWidth',
1313
): string => {
1414
const elementClasses: { [key: string]: string } = {
1515
'model.dotcomrendering.pageElements.InteractiveAtomBlockElement':

dotcom-rendering/src/lib/renderElement.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1065,7 +1065,11 @@ export const RenderArticleElement = ({
10651065
});
10661066

10671067
const needsFigure = !bareElements.has(element._type);
1068-
const role = 'role' in element ? (element.role as RoleType) : undefined;
1068+
1069+
const role =
1070+
'role' in element
1071+
? (element.role as RoleType | 'fullWidth' | undefined)
1072+
: undefined;
10691073

10701074
return needsFigure ? (
10711075
<Figure

dotcom-rendering/src/model/block-schema.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1937,7 +1937,16 @@
19371937
"type": "string"
19381938
},
19391939
"role": {
1940-
"$ref": "#/definitions/RoleType"
1940+
"enum": [
1941+
"fullWidth",
1942+
"halfWidth",
1943+
"immersive",
1944+
"inline",
1945+
"showcase",
1946+
"supporting",
1947+
"thumbnail"
1948+
],
1949+
"type": "string"
19411950
}
19421951
},
19431952
"required": [
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat';
2+
import type { FEElement } from '../types/content';
3+
4+
export const enhanceInteractiveAtomElements =
5+
(format: ArticleFormat) =>
6+
(elements: FEElement[]): FEElement[] => {
7+
const isInteractiveFormat =
8+
format.design === ArticleDesign.Interactive ||
9+
format.design === ArticleDesign.FullPageInteractive;
10+
11+
const enhancedAtomElements = elements.filter((element) => {
12+
const isFullWidthInteractiveAtom =
13+
element._type ===
14+
'model.dotcomrendering.pageElements.InteractiveAtomBlockElement' &&
15+
element.role === 'fullWidth';
16+
return !(isFullWidthInteractiveAtom && !isInteractiveFormat);
17+
});
18+
return enhancedAtomElements;
19+
};

dotcom-rendering/src/model/enhanceBlocks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { enhanceDots } from './enhance-dots';
1818
import { enhanceEmbeds } from './enhance-embeds';
1919
import { enhanceH2s } from './enhance-H2s';
2020
import { enhanceElementsImages, enhanceImages } from './enhance-images';
21+
import { enhanceInteractiveAtomElements } from './enhance-interactive-atom';
2122
import { enhanceInteractiveContentsElements } from './enhance-interactive-contents-elements';
2223
import { enhanceNumberedLists } from './enhance-numbered-lists';
2324
import { enhanceProductSummary } from './enhance-product-summary';
@@ -80,6 +81,7 @@ export const enhanceElements =
8081
),
8182
enhanceDividers,
8283
enhanceH2s,
84+
enhanceInteractiveAtomElements(format),
8385
enhanceInteractiveContentsElements,
8486
enhanceBlockquotes(format),
8587
enhanceDots,

0 commit comments

Comments
 (0)