Skip to content
2 changes: 2 additions & 0 deletions dotcom-rendering/src/components/ArticlePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DecideLayout } from '../layouts/DecideLayout';
import { buildAdTargeting } from '../lib/ad-targeting';
import { ArticleDesign } from '../lib/articleFormat';
import { rootStyles } from '../lib/rootStyles';
import { isFilterPageId } from '../lib/theFilter';
import type { NavType } from '../model/extract-nav';
import type { Article } from '../types/article';
import type { RenderingTarget } from '../types/renderingTarget';
Expand Down Expand Up @@ -78,6 +79,7 @@ export const ArticlePage = (props: WebProps | AppProps) => {
<Lightbox
format={format}
switches={frontendData.config.switches}
isFilterArticle={isFilterPageId(frontendData.pageId)}
{...(renderingTarget === 'Web'
? {
lightboxImages: frontendData.imagesForLightbox,
Expand Down
4 changes: 3 additions & 1 deletion dotcom-rendering/src/components/Button/ProductLinkButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type ProductLinkButtonProps = {
minimisePadding?: boolean;
dataComponent?: string;
xCustComponentId?: string;
themeOverrides?: Partial<ThemeButton>;
};

const fullWidthStyle = css`
Expand Down Expand Up @@ -56,6 +57,7 @@ export const ProductLinkButton = ({
priority = 'primary',
dataComponent,
xCustComponentId,
themeOverrides,
}: ProductLinkButtonProps) => {
const cssOverrides: SerializedStyles[] = [
heightAutoStyle,
Expand All @@ -69,7 +71,7 @@ export const ProductLinkButton = ({
href={url}
rel={SKIMLINK_REL}
priority={priority}
theme={theme}
theme={{ ...theme, ...themeOverrides }}
data-component={dataComponent}
data-ignore="global-link-styling"
data-link-name={`product link button ${priority}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const HorizontalSummaryProductCard = ({
<ProductCardImage
xCustComponentId={'horizontal-summary-card'}
format={format}
elementId={product.elementId}
image={product.image}
url={cardCta.url}
/>
Expand Down
2 changes: 2 additions & 0 deletions dotcom-rendering/src/components/HostedContentPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { HostedGalleryLayout } from '../layouts/HostedGalleryLayout';
import { HostedVideoLayout } from '../layouts/HostedVideoLayout';
import { ArticleDesign } from '../lib/articleFormat';
import { rootStyles } from '../lib/rootStyles';
import { isFilterPageId } from '../lib/theFilter';
import type { Article } from '../types/article';
import type { RenderingTarget } from '../types/renderingTarget';
import { AlreadyVisited } from './AlreadyVisited.island';
Expand Down Expand Up @@ -91,6 +92,7 @@ export const HostedContentPage = (props: WebProps | AppProps) => {
<Lightbox
format={format}
switches={frontendData.config.switches}
isFilterArticle={isFilterPageId(frontendData.pageId)}
{...(renderingTarget === 'Web'
? {
lightboxImages: frontendData.imagesForLightbox,
Expand Down
45 changes: 44 additions & 1 deletion dotcom-rendering/src/components/Lightbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ArticleSpecial,
Pillar,
} from '../lib/articleFormat';
import type { ImageForLightbox } from '../types/content';
import type { ImageForLightbox, ProductCta } from '../types/content';
import { LightboxLayout } from './LightboxLayout.island';

const Initialise = ({ children }: { children: React.ReactNode }) => {
Expand Down Expand Up @@ -92,6 +92,49 @@ export const WithCredit = {
},
} satisfies Story;

const testProductCtas: ProductCta[] = [
{
url: 'https://example.com/buy-1',
text: '',
retailer: 'Amazon',
price: '£19.99',
},
{
url: 'https://example.com/buy-2',
text: '',
retailer: 'John Lewis',
price: '£21.00',
},
];

export const WithProductCtas = {
args: {
format: {
display: ArticleDisplay.Standard,
design: ArticleDesign.Standard,
theme: Pillar.News,
},
images: [
{ ...testImage, title: 'Title', productCtas: testProductCtas },
],
isFilterArticle: true,
},
} satisfies Story;

export const WithProductCtasNonFilterArticle = {
args: {
format: {
display: ArticleDisplay.Standard,
design: ArticleDesign.Standard,
theme: Pillar.News,
},
images: [
{ ...testImage, title: 'Title', productCtas: testProductCtas },
],
isFilterArticle: false,
},
} satisfies Story;

export const WithRating = {
args: {
format: {
Expand Down
3 changes: 3 additions & 0 deletions dotcom-rendering/src/components/Lightbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface BaseProps {
format: ArticleFormat;
renderingTarget: RenderingTarget;
switches: Switches;
isFilterArticle: boolean;
}

interface WebProps extends BaseProps {
Expand All @@ -29,6 +30,7 @@ export const Lightbox = ({
lightboxImages,
renderingTarget,
switches,
isFilterArticle,
}: WebProps | AppProps) => {
switch (renderingTarget) {
case 'Web':
Expand All @@ -46,6 +48,7 @@ export const Lightbox = ({
<LightboxLayout
format={format}
images={lightboxImages}
isFilterArticle={isFilterArticle}
/>
</Island>
<Island priority="feature" defer={{ until: 'idle' }}>
Expand Down
48 changes: 46 additions & 2 deletions dotcom-rendering/src/components/LightboxImages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import type { ImageForLightbox } from '../types/content';
import { LightboxCaption } from './LightboxCaption';
import { LightboxLoader } from './LightboxLoader';
import { Picture } from './Picture';
import { ProductCardButtons } from './ProductCardButtons';
import { StarRating } from './StarRating/StarRating';

type Props = {
format: ArticleFormat;
images: ImageForLightbox[];
isFilterArticle?: boolean;
};

const liStyles = css`
Expand Down Expand Up @@ -134,6 +136,16 @@ const starRatingMarginStyles = css`
margin-bottom: ${space[3]}px;
}
`;

const productCtasStyles = css`
display: flex;
flex-direction: column;
gap: ${space[1]}px;
margin-bottom: ${space[2]}px;
${from.tablet} {
margin-bottom: ${space[3]}px;
}
`;
const Selection = ({
countOfImages,
initialPosition = 1,
Expand Down Expand Up @@ -179,7 +191,11 @@ const Selection = ({
);
};

export const LightboxImages = ({ format, images }: Props) => {
export const LightboxImages = ({
format,
images,
isFilterArticle = false,
}: Props) => {
const [loaded, setLoaded] = useState(new Set<number>());

useEffect(() => {
Expand All @@ -200,6 +216,11 @@ export const LightboxImages = ({ format, images }: Props) => {
return previousSize !== newSize ? new Set(set) : set;
});

const hasProductCtas = !!image.productCtas?.length;
// The "show info without needing to click 'i'" override is
// currently scoped to Filter articles only.
const alwaysShowInfo = hasProductCtas && isFilterArticle;

return (
<li
key={`${image.masterUrl}-${index}`}
Expand Down Expand Up @@ -231,7 +252,14 @@ export const LightboxImages = ({ format, images }: Props) => {
onLoad={onLoad}
loading="lazy"
/>
<aside css={asideStyles}>
<aside
css={asideStyles}
className={
alwaysShowInfo
? 'lightbox-product-info'
: undefined
}
>
{!!image.title && (
<h2
css={css`
Expand All @@ -258,6 +286,22 @@ export const LightboxImages = ({ format, images }: Props) => {
</div>
)}

{!!image.productCtas?.length && (
<div
css={productCtasStyles}
data-component="product-lightbox-link"
>
<ProductCardButtons
productCtas={image.productCtas}
xCustComponentId="lightbox"
themeOverrides={{
backgroundTertiary:
palette.neutral[100],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't this just go in the current theme object?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Given that the lightbox is always a dark background, we want to have the overrides.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess my question is more - is the background of the secondary button in the leftCol card deliberately transparent or should it just be white like this?

}}
/>
</div>
)}

<Hide from="tablet">
<Selection
countOfImages={images.length}
Expand Down
48 changes: 36 additions & 12 deletions dotcom-rendering/src/components/LightboxJavascript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ const getTabbableElements = (
): HTMLElement[] => {
function getElements(parent: HTMLElement): HTMLElement[] {
return Array.from(
parent.querySelectorAll(
parent.querySelectorAll<HTMLElement>(
'button:not([disabled]), a:not([disabled]), input:not([disabled]), select:not([disabled])',
),
);
// Ignore elements that aren't rendered (e.g. the info toggle is
// hidden with `display: none` on Filter articles). They can't
// receive focus, so leaving them in the list would trap keyboard
// tabbing on the element before them.
).filter((element) => element.getClientRects().length > 0);
}
const currentPosition = getPosition(lightbox, imageList);
if (currentPosition == null) {
Expand Down Expand Up @@ -307,7 +311,10 @@ const toggleInfo = (
*
*/

const initialiseLightbox = (lightbox: HTMLElement) => {
const initialiseLightbox = (
lightbox: HTMLElement,
isFilterArticle: boolean,
) => {
log('dotcom', '💡 Initialising lightbox');

// --------------------------------------------------------------------------------
Expand Down Expand Up @@ -357,6 +364,17 @@ const initialiseLightbox = (lightbox: HTMLElement) => {
// FUNCTIONS
// --------------------------------------------------------------------------------

// On Filter articles the info panel is always shown and the toggle button is
// hidden, so we disable every path that would toggle the caption. This keeps
// the info visible and avoids the mobile nav being hidden (a `hide-info` side
// effect) or polluting the shared `gu.prefs.lightbox-info` preference.
const handleToggleInfo = (force?: 'hide' | 'show') => {
if (isFilterArticle) {
return;
}
toggleInfo(lightbox, infoButton, force);
};

const handleKeydown = (event: KeyboardEvent) => {
if (event.ctrlKey || event.metaKey || event.altKey) {
return;
Expand Down Expand Up @@ -407,11 +425,11 @@ const initialiseLightbox = (lightbox: HTMLElement) => {
case 'ArrowRight':
return goForward(lightbox, images, nextButton, imageList);
case 'KeyI':
return toggleInfo(lightbox, infoButton);
return handleToggleInfo();
case 'ArrowUp':
return toggleInfo(lightbox, infoButton, 'show');
return handleToggleInfo('show');
case 'ArrowDown':
return toggleInfo(lightbox, infoButton, 'hide');
return handleToggleInfo('hide');
case 'KeyQ':
case 'Escape':
return void close(lightbox, handleKeydown);
Expand All @@ -424,7 +442,7 @@ const initialiseLightbox = (lightbox: HTMLElement) => {
for (const picture of pictures) {
// Clicking on the image toggles the caption
picture.addEventListener('mousedown', (event) => {
toggleInfo(lightbox, infoButton);
handleToggleInfo();
// We want to maintain focus so halt all further actions
event.preventDefault();
event.stopPropagation();
Expand Down Expand Up @@ -468,7 +486,7 @@ const initialiseLightbox = (lightbox: HTMLElement) => {
goForward(lightbox, images, nextButton, imageList);
});
infoButton.addEventListener('click', () => {
toggleInfo(lightbox, infoButton);
handleToggleInfo();
});

for (const link of captionLinks) {
Expand Down Expand Up @@ -536,7 +554,7 @@ const initialiseLightbox = (lightbox: HTMLElement) => {
// Check the user's preferences to decide if we show the caption or not
const info = storage.local.get('gu.prefs.lightbox-info');
if (info === 'hide') {
toggleInfo(lightbox, infoButton, 'hide');
handleToggleInfo('hide');
}

// Open the lightbox at the position given in the url hash
Expand Down Expand Up @@ -590,9 +608,11 @@ const ulStyles = css`
export const LightboxJavascript = ({
format,
images,
isFilterArticle = false,
}: {
format: ArticleFormat;
images: ImageForLightbox[];
isFilterArticle?: boolean;
}) => {
/**
* Hydration has been requested so the first step is to render the list of images and put them into
Expand All @@ -617,9 +637,9 @@ export const LightboxJavascript = ({
log('dotcom', '💡 Lightbox already initialised, skipping');
return;
}
initialiseLightbox(lightbox);
initialiseLightbox(lightbox, isFilterArticle);
setInitialised(true);
}, [initialised, lightbox]);
}, [initialised, lightbox, isFilterArticle]);

if (!lightbox) {
return null;
Expand All @@ -628,7 +648,11 @@ export const LightboxJavascript = ({
log('dotcom', '💡 Generating HTML for lightbox images...');
return (
<ul css={ulStyles} aria-label="All images">
<LightboxImages format={format} images={images} />
<LightboxImages
format={format}
images={images}
isFilterArticle={isFilterArticle}
/>
</ul>
);
};
Loading
Loading