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

US Expandable Marketing Card component on small screens #12534

Merged
merged 7 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ export const Default = {
chromatic: {
viewports: [
breakpoints.mobile,
breakpoints.mobileMedium,
breakpoints.phablet,
breakpoints.tablet,
breakpoints.desktop,
],
Expand Down
48 changes: 44 additions & 4 deletions dotcom-rendering/src/components/ExpandableMarketingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,44 @@ interface BannersIllustrationProps {
styles?: SerializedStyles;
}

const smallIllustrationStyles = css`
display: none;
${from.phablet} {
display: inline;
}
${from.leftCol} {
display: none;
}
`;
const largeIllustrationStyles = css`
display: inline;
${from.phablet} {
display: none;
}
${from.leftCol} {
display: inline;
}
`;

const BannersIllustration = ({ type, styles }: BannersIllustrationProps) => {
const { assetOrigin } = useConfig();
const src = `${assetOrigin}static/frontend/logos/red-blue-banner-${type}.svg`;
return <img src={src} alt="" css={styles} />;
const smallSrc = `${assetOrigin}static/frontend/logos/red-blue-small-banner-${type}.svg`;
const largeSrc = `${assetOrigin}static/frontend/logos/red-blue-large-banner-${type}.svg`;

return (
<>
<img
src={smallSrc}
alt=""
css={[styles, smallIllustrationStyles]}
/>
<img
src={largeSrc}
alt=""
css={[styles, largeIllustrationStyles]}
/>
</>
);
};

const fillBarStyles = css`
Expand Down Expand Up @@ -64,6 +98,10 @@ const summaryStyles = css`
z-index: 1;
width: 100%;
`;
const contractedSummaryStyles = css`
${summaryStyles}
cursor: pointer;
`;

const headingStyles = css`
display: flex;
Expand Down Expand Up @@ -162,7 +200,6 @@ interface Props {
setIsClosed: Dispatch<SetStateAction<boolean>>;
}

// todo - semantic html accordion-details?
export const ExpandableMarketingCard = ({
guardianBaseURL,
heading,
Expand All @@ -182,7 +219,8 @@ export const ExpandableMarketingCard = ({
styles={imageTopStyles}
/>
<section
css={summaryStyles}
data-link-name="us-expandable-marketing-card expand"
css={contractedSummaryStyles}
role="button"
tabIndex={0}
onClick={() => {
Expand Down Expand Up @@ -220,6 +258,7 @@ export const ExpandableMarketingCard = ({
<div css={headingStyles}>
<h2>{heading}</h2>
<button
data-link-name="us-expandable-marketing-card close"
onClick={() => {
setIsClosed(true);
}}
Expand Down Expand Up @@ -255,6 +294,7 @@ export const ExpandableMarketingCard = ({
</p>
</section>
<LinkButton
data-link-name="us-expandable-marketing-card cta-click"
priority="tertiary"
size="xsmall"
href={`${guardianBaseURL}/email-newsletters`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { css } from '@emotion/react';
import { getCookie } from '@guardian/libs';
import { useEffect, useState } from 'react';
import type { DailyArticle } from '../lib/dailyArticleCount';
import { getDailyArticleCount } from '../lib/dailyArticleCount';
import { getLocaleCode } from '../lib/getCountryCode';
import { getZIndex } from '../lib/getZIndex';
import { useAB } from '../lib/useAB';
import { ExpandableMarketingCard } from './ExpandableMarketingCard';
import { Hide } from './Hide';

interface Props {
guardianBaseURL: string;
}
const isViewportBelowTopOfBody = (topOfBody: Element) =>
topOfBody.getBoundingClientRect().top < 0;

const isFirstArticle = () => {
const [dailyCount = {} as DailyArticle] = getDailyArticleCount() ?? [];
Expand All @@ -32,11 +34,43 @@ const isNewUSUser = async () => {
return !hasUserSelectedNonUSEdition && !isNewUser;
};

// todo - semantic html accordion-details?
const stickyContainerStyles = css`
position: sticky;
top: 0;
${getZIndex('expandableMarketingCardOverlay')};
animation: slidein 2.4s linear;

@keyframes slidein {
from {
transform: translateX(-1200px);
}

to {
transform: translateX(0);
}
}
`;

const absoluteContainerStyles = css`
position: absolute;
width: 100%;
`;

interface Props {
guardianBaseURL: string;
}

export const ExpandableMarketingCardWrapper = ({ guardianBaseURL }: Props) => {
const [isExpanded, setIsExpanded] = useState(false);
const [isClosed, setIsClosed] = useState(false);
const [isApplicableUser, setIsApplicableUser] = useState(false);
const [topOfBody, setTopOfBody] = useState<Element | null>(null);

/**
* On screen sizes below leftCol (<1140px), only display the card
* when the user scrolls past the top of the article.
*/
const [shouldDisplayCard, setShouldDisplayCard] = useState(false);

const abTestAPI = useAB()?.api;
const isInVariantFree = !!abTestAPI?.isUserInVariant(
Expand All @@ -57,6 +91,41 @@ export const ExpandableMarketingCardWrapper = ({ guardianBaseURL }: Props) => {
});
}, []);

useEffect(() => {
setTopOfBody(document.querySelector('[data-gu-name="body"]'));
}, []);

useEffect(() => {
if (!topOfBody) return;

/**
* It's possible that the viewport does not start at the top of the page.
*/
if (isViewportBelowTopOfBody(topOfBody)) {
setShouldDisplayCard(true);
return;
}

/**
* Show the card when the top of the body moves out of the viewport.
*/
const observer = new window.IntersectionObserver(
([entry]) => {
if (!entry) return;
if (entry.isIntersecting) {
setShouldDisplayCard(true);
}
},
{ rootMargin: '0px 0px -100%' },
);

observer.observe(topOfBody);

return () => {
observer.disconnect();
};
}, [topOfBody]);

if (!isInEitherVariant || !isApplicableUser || isClosed) {
return null;
}
Expand All @@ -70,13 +139,33 @@ export const ExpandableMarketingCardWrapper = ({ guardianBaseURL }: Props) => {
: 'Why the Guardian has no paywall';

return (
<ExpandableMarketingCard
guardianBaseURL={guardianBaseURL}
heading={heading}
kicker={kicker}
isExpanded={isExpanded}
setIsExpanded={setIsExpanded}
setIsClosed={setIsClosed}
/>
<>
<Hide when="below" breakpoint="leftCol">
<ExpandableMarketingCard
guardianBaseURL={guardianBaseURL}
heading={heading}
kicker={kicker}
isExpanded={isExpanded}
setIsExpanded={setIsExpanded}
setIsClosed={setIsClosed}
/>
</Hide>
<Hide when="above" breakpoint="leftCol">
{shouldDisplayCard && (
<div css={stickyContainerStyles}>
<div css={absoluteContainerStyles}>
<ExpandableMarketingCard
guardianBaseURL={guardianBaseURL}
heading={heading}
kicker={kicker}
isExpanded={isExpanded}
setIsExpanded={setIsExpanded}
setIsClosed={setIsClosed}
/>
</div>
</div>
)}
</Hide>
</>
);
};
18 changes: 18 additions & 0 deletions dotcom-rendering/src/layouts/CommentLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,24 @@ export const CommentLayout = (props: WebProps | AppsProps) => {
</div>
</GridItem>
<GridItem area="body">
{isWeb && (
<Hide when="above" breakpoint="leftCol">
<Island
priority="enhancement"
/**
* We display the card immediately if the viewport is below the top of
* the article body, so we must use "idle" instead of "visible".
*/
defer={{ until: 'idle' }}
>
<ExpandableMarketingCardWrapper
guardianBaseURL={
article.guardianBaseURL
}
/>
</Island>
</Hide>
)}
<ArticleContainer format={format}>
<div css={maxWidth}>
<ArticleBody
Expand Down
18 changes: 18 additions & 0 deletions dotcom-rendering/src/layouts/ImmersiveLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,24 @@ export const ImmersiveLayout = (props: WebProps | AppProps) => {
</div>
</GridItem>
<GridItem area="body">
{isWeb && (
<Hide when="above" breakpoint="leftCol">
<Island
priority="enhancement"
/**
* We display the card immediately if the viewport is below the top of
* the article body, so we must use "idle" instead of "visible".
*/
defer={{ until: 'idle' }}
>
<ExpandableMarketingCardWrapper
guardianBaseURL={
article.guardianBaseURL
}
/>
</Island>
</Hide>
)}
<ArticleContainer format={format}>
<ArticleBody
format={format}
Expand Down
17 changes: 0 additions & 17 deletions dotcom-rendering/src/layouts/PictureLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { Carousel } from '../components/Carousel.importable';
import { ContributorAvatar } from '../components/ContributorAvatar';
import { DecideLines } from '../components/DecideLines';
import { DiscussionLayout } from '../components/DiscussionLayout';
import { ExpandableMarketingCardWrapper } from '../components/ExpandableMarketingCardWrapper.importable';
import { Footer } from '../components/Footer';
import { GridItem } from '../components/GridItem';
import { HeaderAdSlot } from '../components/HeaderAdSlot';
Expand Down Expand Up @@ -566,22 +565,6 @@ export const PictureLayout = (props: WebProps | AppsProps) => {
/>
</ArticleContainer>
</GridItem>
{isWeb && (
Copy link
Contributor Author

@domlander domlander Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Picture Layouts aren't well suited to showing this component. We can leave out pages with the Picture Layout from the AB test and still hit sample sizes

<GridItem area="uscard" element="aside">
<Hide until="leftCol">
<Island
priority="enhancement"
defer={{ until: 'visible' }}
>
<ExpandableMarketingCardWrapper
guardianBaseURL={
article.guardianBaseURL
}
/>
</Island>
</Hide>
</GridItem>
)}
</PictureGrid>
</Section>

Expand Down
18 changes: 18 additions & 0 deletions dotcom-rendering/src/layouts/ShowcaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,24 @@ export const ShowcaseLayout = (props: WebProps | AppsProps) => {
</div>
</GridItem>
<GridItem area="body">
{isWeb && (
<Hide from="leftCol">
<Island
priority="enhancement"
/**
* We display the card immediately if the viewport is below the top of
* the article body, so we must use "idle" instead of "visible".
*/
defer={{ until: 'idle' }}
>
<ExpandableMarketingCardWrapper
guardianBaseURL={
article.guardianBaseURL
}
/>
</Island>
</Hide>
)}
<ArticleContainer format={format}>
<ArticleBody
format={format}
Expand Down
18 changes: 18 additions & 0 deletions dotcom-rendering/src/layouts/StandardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,24 @@ export const StandardLayout = (props: WebProps | AppProps) => {
)}
</GridItem>
<GridItem area="body">
{isWeb && (
<Hide from="leftCol">
<Island
priority="enhancement"
/**
* We display the card immediately if the viewport is below the top of
* the article body, so we must use "idle" instead of "visible".
*/
defer={{ until: 'idle' }}
>
<ExpandableMarketingCardWrapper
guardianBaseURL={
article.guardianBaseURL
}
/>
</Island>
</Hide>
)}
<ArticleContainer format={format}>
<ArticleBody
format={format}
Expand Down
Loading
Loading