diff --git a/dotcom-rendering/src/components/ContainerOverrides.tsx b/dotcom-rendering/src/components/ContainerOverrides.tsx index 3d86ad7116c..e1d566bb356 100644 --- a/dotcom-rendering/src/components/ContainerOverrides.tsx +++ b/dotcom-rendering/src/components/ContainerOverrides.tsx @@ -1077,6 +1077,28 @@ const carouselChevronBorderDisabledDark: ContainerFunction = ( containerPalette, ) => transparentColour(cardHeadlineDark(containerPalette), 0.4); +const slideshowPaginationDotLight: ContainerFunction = (containerPalette) => { + switch (containerPalette) { + case 'BreakingPalette': + case 'InvestigationPalette': + case 'SombrePalette': + case 'SombreAltPalette': + return transparentColour(cardHeadlineLight(containerPalette), 0.4); + default: + return transparentColour(cardHeadlineLight(containerPalette), 0.2); + } +}; + +const slideshowPaginationDotDark: ContainerFunction = (containerPalette) => + transparentColour(cardHeadlineDark(containerPalette), 0.4); + +const slideshowPaginationDotActiveLight: ContainerFunction = ( + containerPalette, +) => cardHeadlineLight(containerPalette); +const slideshowPaginationDotActiveDark: ContainerFunction = ( + containerPalette, +) => cardHeadlineDark(containerPalette); + type ColourName = Parameters[0]; type ContainerFunction = (containerPalette: DCRContainerPalette) => string; @@ -1210,6 +1232,14 @@ const containerColours = { light: carouselChevronHoverLight, dark: carouselChevronHoverDark, }, + '--slideshow-pagination-dot': { + light: slideshowPaginationDotLight, + dark: slideshowPaginationDotDark, + }, + '--slideshow-pagination-dot-active': { + light: slideshowPaginationDotActiveLight, + dark: slideshowPaginationDotActiveDark, + }, '--section-border': { light: sectionBorderLight, dark: sectionBorderDark, diff --git a/dotcom-rendering/src/components/SlideshowCarousel.importable.tsx b/dotcom-rendering/src/components/SlideshowCarousel.importable.tsx index c187a25f11a..2fe1be68b27 100644 --- a/dotcom-rendering/src/components/SlideshowCarousel.importable.tsx +++ b/dotcom-rendering/src/components/SlideshowCarousel.importable.tsx @@ -1,5 +1,10 @@ import { css } from '@emotion/react'; -import { space, textSansBold12, width } from '@guardian/source/foundations'; +import { + from, + space, + textSansBold12, + width, +} from '@guardian/source/foundations'; import type { ThemeButton } from '@guardian/source/react-components'; import { Button, @@ -49,18 +54,18 @@ const carouselItemStyles = css` `; const captionStyles = css` - ${textSansBold12} position: absolute; bottom: 0; left: 0; right: 0; + ${textSansBold12} + color: ${palette('--slideshow-caption')}; background: linear-gradient( to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 100% ); - color: ${palette('--slideshow-caption')}; - padding: 60px ${space[2]}px ${space[2]}px; + padding: ${space[10]}px ${space[2]}px ${space[2]}px; `; const navigationStyles = css` @@ -70,20 +75,25 @@ const navigationStyles = css` `; const buttonStyles = css` - display: flex; - gap: ${space[2]}px; + display: none; + ${from.tablet} { + display: flex; + gap: ${space[2]}px; + } `; /** * Padding is added to the left of the scrolling navigation dots to match the - * width of the navigation buttons on the right. This allows them to be centred - * below the slideshow image. + * width of the navigation buttons on the right at tablet and above. This allows + * them to be centred below the slideshow image. */ const scrollingDotStyles = css` display: flex; justify-content: center; flex: 1 0 0; - padding-left: ${width.ctaSmall * 2 + space[2]}px; + ${from.tablet} { + padding-left: ${width.ctaSmall * 2 + space[2]}px; + } `; export const SlideshowCarousel = ({ diff --git a/dotcom-rendering/src/components/SlideshowCarousel.stories.tsx b/dotcom-rendering/src/components/SlideshowCarousel.stories.tsx index 6ea619fddf6..d1ff34ec86e 100644 --- a/dotcom-rendering/src/components/SlideshowCarousel.stories.tsx +++ b/dotcom-rendering/src/components/SlideshowCarousel.stories.tsx @@ -1,41 +1,12 @@ import { css } from '@emotion/react'; -import { breakpoints, space } from '@guardian/source/foundations'; +import { breakpoints, space, textSans17 } from '@guardian/source/foundations'; import type { Meta, StoryObj } from '@storybook/react'; import type { ReactNode } from 'react'; -import type { DCRSlideshowImage } from '../types/front'; +import { palette } from '../palette'; +import type { DCRContainerPalette, DCRSlideshowImage } from '../types/front'; +import { ContainerOverrides } from './ContainerOverrides'; import { SlideshowCarousel } from './SlideshowCarousel.importable'; -const Wrapper = ({ children }: { children: ReactNode }) => { - const styles = css` - margin: ${space[2]}px; - max-width: 460px; - `; - return
{children}
; -}; - -const meta = { - component: SlideshowCarousel, - title: 'Components/SlideshowCarousel', - render: (args) => ( - - - - ), - parameters: { - chromatic: { - viewports: [ - breakpoints.mobile, - breakpoints.tablet, - breakpoints.leftCol, - ], - }, - }, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - const images = [ { imageSrc: @@ -75,23 +46,109 @@ const images = [ }, ] as const satisfies readonly DCRSlideshowImage[]; -export const WithMultipleImages = { +const Wrapper = ({ + children, + heading, +}: { + children: ReactNode; + heading?: string; +}) => { + const sectionStyles = css` + padding: ${space[4]}px; + background: ${palette('--card-background')}; + `; + const containerStyles = css` + max-width: 460px; + `; + const headingStyles = css` + ${textSans17}; + color: ${palette('--card-headline')}; + margin-bottom: ${space[2]}px; + `; + return ( +
+
+ {!!heading &&

{heading}

} + {children} +
+
+ ); +}; + +const meta = { + component: SlideshowCarousel, + title: 'Components/SlideshowCarousel', + parameters: { + chromatic: { + viewports: [ + breakpoints.mobile, + breakpoints.tablet, + breakpoints.leftCol, + ], + }, + }, args: { images, imageSize: 'medium', }, -} satisfies Story; + render: (args) => ( + + + + ), +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const WithMultipleImages = {} satisfies Story; export const WithThreeImages = { args: { images: images.slice(0, 3), - imageSize: 'medium', }, } satisfies Story; export const WithOneImage = { args: { images: images.slice(0, 1), - imageSize: 'medium', }, } satisfies Story; + +const containerPalettes = [ + 'InvestigationPalette', + 'LongRunningPalette', + 'SombrePalette', + 'BreakingPalette', + 'EventPalette', + 'EventAltPalette', + 'LongRunningAltPalette', + 'SombreAltPalette', + 'SpecialReportAltPalette', + 'Branded', +] as const satisfies readonly Omit< + DCRContainerPalette, + 'MediaPalette' | 'PodcastPalette' +>[]; + +export const WithSpecialPaletteVariations = { + parameters: { + /** We only want one breakpoint snapshotted for special palette variations */ + chromatic: { viewports: [breakpoints.desktop] }, + }, + render: (args) => ( + <> + {containerPalettes.map((containerPalette) => ( + + + + + + ))} + + ), +} satisfies Story; diff --git a/dotcom-rendering/src/components/TableOfContents.importable.tsx b/dotcom-rendering/src/components/TableOfContents.importable.tsx index e383e662722..faa660d8c17 100644 --- a/dotcom-rendering/src/components/TableOfContents.importable.tsx +++ b/dotcom-rendering/src/components/TableOfContents.importable.tsx @@ -11,6 +11,7 @@ import { } from '@guardian/source/react-components'; import { useState } from 'react'; import { ArticleDisplay, type ArticleFormat } from '../lib/articleFormat'; +import { getZIndex } from '../lib/getZIndex'; import type { TableOfContentsItem } from '../model/enhanceTableOfContents'; import { palette } from '../palette'; @@ -72,6 +73,20 @@ const detailsStyles = css` display: none; } `; +const stickyStyles = css` + position: sticky; + top: 0; + background: ${palette('--article-background')}; + ${getZIndex('tableOfContents')} + max-height: 100vh; + overflow: scroll; + summary { + position: sticky; + top: 0; + z-index: 1; + background: ${palette('--article-background')}; + } +`; const summaryStyles = css` display: flex; @@ -126,7 +141,10 @@ export const TableOfContents = ({ tableOfContents, format }: Props) => { return (
5 ? stickyStyles : undefined, + ]} data-component="table-of-contents" > { { + setOpen((state) => !state); + }} > {item.title} diff --git a/dotcom-rendering/src/lib/getZIndex.test.ts b/dotcom-rendering/src/lib/getZIndex.test.ts index 64bfde5ec4a..19ffb9fe130 100644 --- a/dotcom-rendering/src/lib/getZIndex.test.ts +++ b/dotcom-rendering/src/lib/getZIndex.test.ts @@ -2,34 +2,35 @@ import { getZIndex } from './getZIndex'; describe('getZIndex', () => { it('gets the correct zindex for group and sibling', () => { - expect(getZIndex('sticky-video-button')).toBe('z-index: 31;'); - expect(getZIndex('sticky-video')).toBe('z-index: 30;'); - expect(getZIndex('banner')).toBe('z-index: 29;'); - expect(getZIndex('dropdown')).toBe('z-index: 28;'); - expect(getZIndex('burger')).toBe('z-index: 27;'); + expect(getZIndex('sticky-video-button')).toBe('z-index: 32;'); + expect(getZIndex('sticky-video')).toBe('z-index: 31;'); + expect(getZIndex('banner')).toBe('z-index: 30;'); + expect(getZIndex('dropdown')).toBe('z-index: 29;'); + expect(getZIndex('burger')).toBe('z-index: 28;'); expect(getZIndex('mastheadVeggieBurgerExpandedMobile')).toBe( - 'z-index: 26;', + 'z-index: 27;', ); - expect(getZIndex('expanded-veggie-menu-wrapper')).toBe('z-index: 25;'); - expect(getZIndex('expanded-veggie-menu')).toBe('z-index: 24;'); + expect(getZIndex('expanded-veggie-menu-wrapper')).toBe('z-index: 26;'); + expect(getZIndex('expanded-veggie-menu')).toBe('z-index: 25;'); expect(getZIndex('fullPageInteractiveHeaderWrapper')).toBe( - 'z-index: 23;', + 'z-index: 24;', ); - expect(getZIndex('mobileSticky')).toBe('z-index: 22;'); - expect(getZIndex('stickyAdWrapperLabsHeader')).toBe('z-index: 21;'); - expect(getZIndex('stickyAdWrapper')).toBe('z-index: 20;'); - expect(getZIndex('stickyAdWrapperNav')).toBe('z-index: 19;'); - expect(getZIndex('mastheadMyAccountDropdown')).toBe('z-index: 18;'); - expect(getZIndex('mastheadEditionDropdown')).toBe('z-index: 17;'); - expect(getZIndex('editionDropdown')).toBe('z-index: 16;'); - expect(getZIndex('summaryDetails')).toBe('z-index: 15;'); - expect(getZIndex('toast')).toBe('z-index: 14;'); - expect(getZIndex('onwardsCarousel')).toBe('z-index: 13;'); - expect(getZIndex('myAccountDropdown')).toBe('z-index: 12;'); - expect(getZIndex('searchHeaderLink')).toBe('z-index: 11;'); - expect(getZIndex('TheGuardian')).toBe('z-index: 10;'); - expect(getZIndex('editionSwitcherBanner')).toBe('z-index: 9;'); - expect(getZIndex('expandableMarketingCardOverlay')).toBe('z-index: 8;'); + expect(getZIndex('mobileSticky')).toBe('z-index: 23;'); + expect(getZIndex('stickyAdWrapperLabsHeader')).toBe('z-index: 22;'); + expect(getZIndex('stickyAdWrapper')).toBe('z-index: 21;'); + expect(getZIndex('stickyAdWrapperNav')).toBe('z-index: 20;'); + expect(getZIndex('mastheadMyAccountDropdown')).toBe('z-index: 19;'); + expect(getZIndex('mastheadEditionDropdown')).toBe('z-index: 18;'); + expect(getZIndex('editionDropdown')).toBe('z-index: 17;'); + expect(getZIndex('summaryDetails')).toBe('z-index: 16;'); + expect(getZIndex('toast')).toBe('z-index: 15;'); + expect(getZIndex('onwardsCarousel')).toBe('z-index: 14;'); + expect(getZIndex('myAccountDropdown')).toBe('z-index: 13;'); + expect(getZIndex('searchHeaderLink')).toBe('z-index: 12;'); + expect(getZIndex('TheGuardian')).toBe('z-index: 11;'); + expect(getZIndex('editionSwitcherBanner')).toBe('z-index: 10;'); + expect(getZIndex('expandableMarketingCardOverlay')).toBe('z-index: 9;'); + expect(getZIndex('tableOfContents')).toBe('z-index: 8;'); expect(getZIndex('articleHeadline')).toBe('z-index: 7;'); expect(getZIndex('immersiveBlackBox')).toBe('z-index: 6;'); expect(getZIndex('bodyArea')).toBe('z-index: 5;'); diff --git a/dotcom-rendering/src/lib/getZIndex.ts b/dotcom-rendering/src/lib/getZIndex.ts index cd8d4850ebb..8ee643a2548 100644 --- a/dotcom-rendering/src/lib/getZIndex.ts +++ b/dotcom-rendering/src/lib/getZIndex.ts @@ -77,6 +77,9 @@ const indices = [ // Overlay for expandable marketing card (currently US only) 'expandableMarketingCardOverlay', + // Sticky table of contents element + 'tableOfContents', + // Article headline (should be above main media) 'articleHeadline', 'immersiveBlackBox',